1
0
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:
RaidMax
2020-05-22 20:29:41 -05:00
parent 7b9eb2fa5e
commit 6f9051120d
23 changed files with 1744 additions and 12 deletions

View 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;
}
}
}

View File

@ -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)

View 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;
}
}

View 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; }
}
}

View 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");
}
}
}

View File

@ -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>

View 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>
}
}

View 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>
}