mirror of
https://github.com/RaidMax/IW4M-Admin.git
synced 2025-06-10 23:31:13 -05:00
[issue 135] enhanced search
implement enhanced search for chat messages
This commit is contained in:
88
Plugins/Web/StatsWeb/ChatResourceQueryHelper.cs
Normal file
88
Plugins/Web/StatsWeb/ChatResourceQueryHelper.cs
Normal file
@ -0,0 +1,88 @@
|
||||
using IW4MAdmin.Plugins.Stats.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using StatsWeb.Dtos;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace StatsWeb
|
||||
{
|
||||
/// <summary>
|
||||
/// implementation of IResourceQueryHelper
|
||||
/// </summary>
|
||||
public class ChatResourceQueryHelper : IResourceQueryHelper<ChatSearchQuery, ChatSearchResult>
|
||||
{
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public ChatResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory)
|
||||
{
|
||||
_contextFactory = contextFactory;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<ResourceQueryHelperResult<ChatSearchResult>> QueryResource(ChatSearchQuery query)
|
||||
{
|
||||
if (query == null)
|
||||
{
|
||||
throw new ArgumentException("Query must be specified");
|
||||
}
|
||||
|
||||
var result = new ResourceQueryHelperResult<ChatSearchResult>();
|
||||
using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||
|
||||
var iqMessages = context.Set<EFClientMessage>()
|
||||
.Where(_message => _message.TimeSent >= query.SentAfter)
|
||||
.Where(_message => _message.TimeSent <= query.SentBefore);
|
||||
|
||||
if (query.ClientId != null)
|
||||
{
|
||||
iqMessages = iqMessages.Where(_message => _message.ClientId == query.ClientId.Value);
|
||||
}
|
||||
|
||||
if (query.ServerId != null)
|
||||
{
|
||||
iqMessages = iqMessages.Where(_message => _message.Server.EndPoint == query.ServerId);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(query.MessageContains))
|
||||
{
|
||||
iqMessages = iqMessages.Where(_message => EF.Functions.Like(_message.Message, $"%{query.MessageContains}%"));
|
||||
}
|
||||
|
||||
var iqResponse = iqMessages
|
||||
.Select(_message => new ChatSearchResult
|
||||
{
|
||||
ClientId = _message.ClientId,
|
||||
ClientName = _message.Client.CurrentAlias.Name,
|
||||
Date = _message.TimeSent,
|
||||
Message = _message.Message,
|
||||
ServerName = _message.Server.HostName
|
||||
});
|
||||
|
||||
if (query.Direction == SharedLibraryCore.Dtos.SortDirection.Descending)
|
||||
{
|
||||
iqResponse = iqResponse.OrderByDescending(_message => _message.Date);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
iqResponse = iqResponse.OrderBy(_message => _message.Date);
|
||||
}
|
||||
|
||||
var resultList = await iqResponse
|
||||
.Skip(query.Offset)
|
||||
.Take(query.Count)
|
||||
.ToListAsync();
|
||||
|
||||
result.TotalResultCount = await iqResponse.CountAsync();
|
||||
result.Results = resultList;
|
||||
result.RetrievedResultCount = resultList.Count;
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,8 @@ using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using StatsWeb.Dtos;
|
||||
using StatsWeb.Extensions;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@ -14,11 +16,18 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
|
||||
{
|
||||
public class StatsController : BaseController
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IManager _manager;
|
||||
private readonly IResourceQueryHelper<ChatSearchQuery, ChatSearchResult> _chatResourceQueryHelper;
|
||||
private readonly ITranslationLookup _translationLookup;
|
||||
|
||||
public StatsController(IManager manager) : base(manager)
|
||||
public StatsController(ILogger logger, IManager manager, IResourceQueryHelper<ChatSearchQuery, ChatSearchResult> resourceQueryHelper,
|
||||
ITranslationLookup translationLookup) : base(manager)
|
||||
{
|
||||
_logger = logger;
|
||||
_manager = manager;
|
||||
_chatResourceQueryHelper = resourceQueryHelper;
|
||||
_translationLookup = translationLookup;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -105,6 +114,69 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("Message/Find")]
|
||||
public async Task<IActionResult> FindMessage([FromQuery]string query)
|
||||
{
|
||||
ViewBag.Localization = _translationLookup;
|
||||
ViewBag.EnableColorCodes = _manager.GetApplicationSettings().Configuration().EnableColorCodes;
|
||||
ViewBag.Query = query;
|
||||
ViewBag.QueryLimit = 100;
|
||||
ViewBag.Title = _translationLookup["WEBFRONT_STATS_MESSAGES_TITLE"];
|
||||
ViewBag.Error = null;
|
||||
ViewBag.IsFluid = true;
|
||||
ChatSearchQuery searchRequest = null;
|
||||
|
||||
try
|
||||
{
|
||||
searchRequest = query.ParseSearchInfo(int.MaxValue, 0);
|
||||
}
|
||||
|
||||
catch (ArgumentException e)
|
||||
{
|
||||
_logger.WriteWarning($"Could not parse chat message search query - {query}");
|
||||
_logger.WriteDebug(e.GetExceptionInfo());
|
||||
ViewBag.Error = e;
|
||||
}
|
||||
|
||||
catch (FormatException e)
|
||||
{
|
||||
_logger.WriteWarning($"Could not parse chat message search query filter format - {query}");
|
||||
_logger.WriteDebug(e.GetExceptionInfo());
|
||||
ViewBag.Error = e;
|
||||
}
|
||||
|
||||
var result = searchRequest != null ? await _chatResourceQueryHelper.QueryResource(searchRequest) : null;
|
||||
return View("Message/Find", result);
|
||||
}
|
||||
|
||||
[HttpGet("Message/FindNext")]
|
||||
public async Task<IActionResult> FindNextMessages([FromQuery]string query, [FromQuery]int count, [FromQuery]int offset)
|
||||
{
|
||||
ChatSearchQuery searchRequest;
|
||||
|
||||
try
|
||||
{
|
||||
searchRequest = query.ParseSearchInfo(count, offset);
|
||||
}
|
||||
|
||||
catch (ArgumentException e)
|
||||
{
|
||||
_logger.WriteWarning($"Could not parse chat message search query - {query}");
|
||||
_logger.WriteDebug(e.GetExceptionInfo());
|
||||
throw;
|
||||
}
|
||||
|
||||
catch (FormatException e)
|
||||
{
|
||||
_logger.WriteWarning($"Could not parse chat message search query filter format - {query}");
|
||||
_logger.WriteDebug(e.GetExceptionInfo());
|
||||
throw;
|
||||
}
|
||||
|
||||
var result = await _chatResourceQueryHelper.QueryResource(searchRequest);
|
||||
return PartialView("Message/_Item", result.Results);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Authorize]
|
||||
public async Task<IActionResult> GetAutomatedPenaltyInfoAsync(int penaltyId)
|
||||
|
33
Plugins/Web/StatsWeb/Dtos/ChatSearchQuery.cs
Normal file
33
Plugins/Web/StatsWeb/Dtos/ChatSearchQuery.cs
Normal file
@ -0,0 +1,33 @@
|
||||
using SharedLibraryCore.Dtos;
|
||||
using System;
|
||||
|
||||
namespace StatsWeb.Dtos
|
||||
{
|
||||
public class ChatSearchQuery : PaginationInfo
|
||||
{
|
||||
/// <summary>
|
||||
/// specifies the partial content of the message to search for
|
||||
/// </summary>
|
||||
public string MessageContains { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// identifier for the server
|
||||
/// </summary>
|
||||
public string ServerId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// identifier for the client
|
||||
/// </summary>
|
||||
public int? ClientId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// only look for messages sent after this date
|
||||
/// </summary>
|
||||
public DateTime SentAfter { get; set; } = DateTime.UtcNow.AddYears(-100);
|
||||
|
||||
/// <summary>
|
||||
/// only look for messages sent before this date0
|
||||
/// </summary>
|
||||
public DateTime SentBefore { get; set; } = DateTime.UtcNow;
|
||||
}
|
||||
}
|
32
Plugins/Web/StatsWeb/Dtos/ChatSearchResult.cs
Normal file
32
Plugins/Web/StatsWeb/Dtos/ChatSearchResult.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
|
||||
namespace StatsWeb.Dtos
|
||||
{
|
||||
public class ChatSearchResult
|
||||
{
|
||||
/// <summary>
|
||||
/// name of the client
|
||||
/// </summary>
|
||||
public string ClientName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// client id
|
||||
/// </summary>
|
||||
public int ClientId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// hostname of the server
|
||||
/// </summary>
|
||||
public string ServerName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// chat message
|
||||
/// </summary>
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// date the chat occured on
|
||||
/// </summary>
|
||||
public DateTime Date { get; set; }
|
||||
}
|
||||
}
|
77
Plugins/Web/StatsWeb/Extensions/SearchQueryExtensions.cs
Normal file
77
Plugins/Web/StatsWeb/Extensions/SearchQueryExtensions.cs
Normal file
@ -0,0 +1,77 @@
|
||||
using SharedLibraryCore.Dtos;
|
||||
using StatsWeb.Dtos;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace StatsWeb.Extensions
|
||||
{
|
||||
public static class SearchQueryExtensions
|
||||
{
|
||||
private const int MAX_MESSAGES = 100;
|
||||
|
||||
/// <summary>
|
||||
/// todo: lets abstract this out to a generic buildable query
|
||||
/// this is just a dirty PoC
|
||||
/// </summary>
|
||||
/// <param name="query"></param>
|
||||
/// <returns></returns>
|
||||
public static ChatSearchQuery ParseSearchInfo(this string query, int count, int offset)
|
||||
{
|
||||
string[] filters = query.Split('|');
|
||||
var searchRequest = new ChatSearchQuery
|
||||
{
|
||||
Filter = query,
|
||||
Count = count,
|
||||
Offset = offset
|
||||
};
|
||||
|
||||
// sanity checks
|
||||
searchRequest.Count = Math.Min(searchRequest.Count, MAX_MESSAGES);
|
||||
searchRequest.Count = Math.Max(searchRequest.Count, 0);
|
||||
searchRequest.Offset = Math.Max(searchRequest.Offset, 0);
|
||||
|
||||
if (filters.Length > 1)
|
||||
{
|
||||
if (filters[0].ToLower() != "chat")
|
||||
{
|
||||
throw new ArgumentException("Query is not compatible with chat");
|
||||
}
|
||||
|
||||
foreach (string filter in filters.Skip(1))
|
||||
{
|
||||
string[] args = filter.Split(' ');
|
||||
|
||||
if (args.Length > 1)
|
||||
{
|
||||
string recombinedArgs = string.Join(' ', args.Skip(1));
|
||||
switch (args[0].ToLower())
|
||||
{
|
||||
case "before":
|
||||
searchRequest.SentBefore = DateTime.Parse(recombinedArgs);
|
||||
break;
|
||||
case "after":
|
||||
searchRequest.SentAfter = DateTime.Parse(recombinedArgs);
|
||||
break;
|
||||
case "server":
|
||||
searchRequest.ServerId = args[1];
|
||||
break;
|
||||
case "client":
|
||||
searchRequest.ClientId = int.Parse(args[1]);
|
||||
break;
|
||||
case "contains":
|
||||
searchRequest.MessageContains = string.Join(' ', args.Skip(1));
|
||||
break;
|
||||
case "sort":
|
||||
searchRequest.Direction = Enum.Parse<SortDirection>(args[1], ignoreCase: true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return searchRequest;
|
||||
}
|
||||
|
||||
throw new ArgumentException("No filters specified for chat search");
|
||||
}
|
||||
}
|
||||
}
|
@ -7,14 +7,14 @@
|
||||
<CopyLocalLockFileAssemblies>false</CopyLocalLockFileAssemblies>
|
||||
<AddRazorSupportForMvc>true</AddRazorSupportForMvc>
|
||||
<Configurations>Debug;Release;Prerelease</Configurations>
|
||||
<LangVersion>7.1</LangVersion>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<ApplicationIcon />
|
||||
<OutputType>Library</OutputType>
|
||||
<StartupObject />
|
||||
<RunPostBuildEvent>Always</RunPostBuildEvent>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.2.11" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2.4.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
38
Plugins/Web/StatsWeb/Views/Stats/Message/Find.cshtml
Normal file
38
Plugins/Web/StatsWeb/Views/Stats/Message/Find.cshtml
Normal file
@ -0,0 +1,38 @@
|
||||
@model SharedLibraryCore.Helpers.ResourceQueryHelperResult<StatsWeb.Dtos.ChatSearchResult>
|
||||
|
||||
@if (ViewBag.Error != null)
|
||||
{
|
||||
<h4 class="text-red">@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_INVALID_QUERY"], ViewBag.Error.Message)</h4>
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
<h4 class="pb-3 text-center">@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_STATS_MESSAGES_FOUND"], Model.TotalResultCount.ToString("N0"))</h4>
|
||||
|
||||
<table class="table table-striped table-hover">
|
||||
<thead class="d-none d-lg-table-header-group">
|
||||
<tr class="bg-primary pt-2 pb-2">
|
||||
<th scope="col">@ViewBag.Localization["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</th>
|
||||
<th scope="col">@ViewBag.Localization["WEBFRONT_ACTION_LABEL_MESSAGE"]</th>
|
||||
<th scope="col">@ViewBag.Localization["WEBFRONT_STATS_MESSAGE_SERVER_NAME"]</th>
|
||||
<th scope="col" class="text-right">@ViewBag.Localization["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="message_table_body" class="border-bottom bg-dark">
|
||||
<partial name="Message/_Item" model="@Model.Results" />
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<span id="load_more_messages_button" class="loader-load-more oi oi-chevron-bottom text-center text-primary w-100 h3 pb-0 mb-0 d-none d-lg-block"></span>
|
||||
|
||||
@section scripts {
|
||||
<environment include="Development">
|
||||
<script type="text/javascript" src="~/js/loader.js"></script>
|
||||
</environment>
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
initLoader('/Message/FindNext?query=@ViewBag.Query', '#message_table_body', @Model.RetrievedResultCount, @ViewBag.QueryLimit);
|
||||
});
|
||||
</script>
|
||||
}
|
||||
}
|
53
Plugins/Web/StatsWeb/Views/Stats/Message/_Item.cshtml
Normal file
53
Plugins/Web/StatsWeb/Views/Stats/Message/_Item.cshtml
Normal file
@ -0,0 +1,53 @@
|
||||
@model IEnumerable<StatsWeb.Dtos.ChatSearchResult>
|
||||
|
||||
@foreach (var message in Model)
|
||||
{
|
||||
<!-- desktop -->
|
||||
<tr class="d-none d-lg-table-row">
|
||||
<td>
|
||||
<a asp-controller="Client" asp-action="ProfileAsync" asp-route-id="@message.ClientId" class="link-inverse">
|
||||
<color-code value="@message.ClientName" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
</a>
|
||||
</td>
|
||||
<td class="text-light w-50 text-break">
|
||||
<color-code value="@message.Message" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
</td>
|
||||
<td class="text-light">
|
||||
<color-code value="@(message.ServerName ?? "--")" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
</td>
|
||||
<td class="text-right text-light">
|
||||
@message.Date
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- mobile -->
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary">@ViewBag.Localization["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</th>
|
||||
<td class="text-light">
|
||||
<a asp-controller="Client" asp-action="ProfileAsync" asp-route-id="@message.ClientId" class="link-inverse">
|
||||
<color-code value="@message.ClientName" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary">@ViewBag.Localization["WEBFRONT_ACTION_LABEL_MESSAGE"]</th>
|
||||
<td class="text-light">
|
||||
<color-code value="@message.Message" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary">@ViewBag.Localization["WEBFRONT_STATS_MESSAGE_SERVER_NAME"]</th>
|
||||
<td class="text-light">
|
||||
<color-code value="@(message.ServerName ?? "--")" allow="@ViewBag.EnableColorCodes"></color-code>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr class="d-table-row d-lg-none bg-dark">
|
||||
<th scope="row" class="bg-primary" style="border-bottom: 1px solid #222">@ViewBag.Localization["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</th>
|
||||
<td class="text-light mb-2 border-bottom">
|
||||
@message.Date
|
||||
</td>
|
||||
</tr>
|
||||
}
|
Reference in New Issue
Block a user