Merge pull request #136 from RaidMax/feature/issue-135-enhanced-search

[issue 135] enhanced search
This commit is contained in:
RaidMax 2020-05-22 20:35:42 -05:00 committed by GitHub
commit 4afd1f3cdc
23 changed files with 1744 additions and 12 deletions

View File

@ -225,7 +225,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
Port = sv.Port,
EndPoint = sv.ToString(),
ServerId = serverId,
GameName = sv.GameName
GameName = sv.GameName,
HostName = sv.Hostname
};
server = serverSet.Add(server).Entity;
@ -240,6 +241,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
ctx.Entry(server).Property(_prop => _prop.GameName).IsModified = true;
ctx.SaveChanges();
}
if (server.HostName == null || server.HostName != sv.Hostname)
{
server.HostName = sv.Hostname;
ctx.Entry(server).Property(_prop => _prop.HostName).IsModified = true;
ctx.SaveChanges();
}
}
// check to see if the stats have ever been initialized

View File

@ -15,5 +15,6 @@ namespace IW4MAdmin.Plugins.Stats.Models
public int Port { get; set; }
public string EndPoint { get; set; }
public Game? GameName { get; set; }
public string HostName { get; set; }
}
}

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

View File

@ -348,7 +348,24 @@ ___
* Shows a client's information and history
`Web Console`
* Allows logged in privileged users to execute commands as if they are in-game
* Allows logged in privileged users to execute commands as if they are in-
`Search`
* Query clients and messages
Advanced filters can be constructed to search for resources using the following filter table.
| Filter | Description | Format | Example |
|-----------|--------------------------------------------------------|-----------------------|---------------------|
| before | include items occurring on or before the provided date | YYYY-MM-DD hh:mm:ss (UTC inferred) | 2020-05-21 23:00:00 |
| after | include items occurring on or after the provided date | YYYY-MM-DD hh:mm:ss (UTC inferred) | 2015-01-01 |
| server | include items matching the server id | ip:port | 127.0.0.1:28960 |
| client | include items matching the client id | integer | 8947 |
| contains | include items containing this substring | string | hack |
| sort | display results in this order | ascending\|descending | descending |
Any number of filters can be combined in any order.
Example &mdash; `chat|before 2020-05-21|after 2020-05-01|server 127.0.0.1:28960|client 444|contains cheating|sort descending`
---
### Game Log Server
The game log server provides a way to remotely host your server's log over a http rest-ful api.

View File

@ -0,0 +1,26 @@
using System.Collections.Generic;
namespace SharedLibraryCore.Helpers
{
/// <summary>
/// generic class for passing information about a resource query
/// </summary>
/// <typeparam name="QueryResultType">Type of query result</typeparam>
public class ResourceQueryHelperResult<QueryResultType>
{
/// <summary>
/// indicates the total number of results found
/// </summary>
public long TotalResultCount { get; set; }
/// <summary>
/// indicates the total number of results retrieved
/// </summary>
public int RetrievedResultCount { get; set; }
/// <summary>
/// collection of results
/// </summary>
public IEnumerable<QueryResultType> Results { get; set; }
}
}

View File

@ -0,0 +1,20 @@
using SharedLibraryCore.Helpers;
using System.Threading.Tasks;
namespace SharedLibraryCore.Interfaces
{
/// <summary>
/// defines the capabilities of a resource queryier
/// </summary>
/// <typeparam name="QueryType">Type of query</typeparam>
/// <typeparam name="ResultType">Type of result</typeparam>
public interface IResourceQueryHelper<QueryType, ResultType>
{
/// <summary>
/// queries a resource and returns the result of the query
/// </summary>
/// <param name="query">query params</param>
/// <returns></returns>
Task<ResourceQueryHelperResult<ResultType>> QueryResource(QueryType query);
}
}

View File

@ -0,0 +1,922 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using SharedLibraryCore.Database;
namespace SharedLibraryCore.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20200521203304_AddHostnameToEFServer")]
partial class AddHostnameToEFServer
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "3.1.3");
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", b =>
{
b.Property<int>("SnapshotId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<int>("ClientId")
.HasColumnType("INTEGER");
b.Property<int>("CurrentSessionLength")
.HasColumnType("INTEGER");
b.Property<double>("CurrentStrain")
.HasColumnType("REAL");
b.Property<int>("CurrentViewAngleId")
.HasColumnType("INTEGER");
b.Property<int>("Deaths")
.HasColumnType("INTEGER");
b.Property<double>("Distance")
.HasColumnType("REAL");
b.Property<double>("EloRating")
.HasColumnType("REAL");
b.Property<int>("HitDestinationId")
.HasColumnType("INTEGER");
b.Property<int>("HitLocation")
.HasColumnType("INTEGER");
b.Property<int>("HitOriginId")
.HasColumnType("INTEGER");
b.Property<int>("HitType")
.HasColumnType("INTEGER");
b.Property<int>("Hits")
.HasColumnType("INTEGER");
b.Property<int>("Kills")
.HasColumnType("INTEGER");
b.Property<int>("LastStrainAngleId")
.HasColumnType("INTEGER");
b.Property<double>("RecoilOffset")
.HasColumnType("REAL");
b.Property<double>("SessionAngleOffset")
.HasColumnType("REAL");
b.Property<double>("SessionAverageSnapValue")
.HasColumnType("REAL");
b.Property<double>("SessionSPM")
.HasColumnType("REAL");
b.Property<int>("SessionScore")
.HasColumnType("INTEGER");
b.Property<int>("SessionSnapHits")
.HasColumnType("INTEGER");
b.Property<double>("StrainAngleBetween")
.HasColumnType("REAL");
b.Property<int>("TimeSinceLastEvent")
.HasColumnType("INTEGER");
b.Property<int>("WeaponId")
.HasColumnType("INTEGER");
b.Property<DateTime>("When")
.HasColumnType("TEXT");
b.HasKey("SnapshotId");
b.HasIndex("ClientId");
b.HasIndex("CurrentViewAngleId");
b.HasIndex("HitDestinationId");
b.HasIndex("HitOriginId");
b.HasIndex("LastStrainAngleId");
b.ToTable("EFACSnapshot");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshotVector3", b =>
{
b.Property<int>("ACSnapshotVector3Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<int>("SnapshotId")
.HasColumnType("INTEGER");
b.Property<int>("Vector3Id")
.HasColumnType("INTEGER");
b.HasKey("ACSnapshotVector3Id");
b.HasIndex("SnapshotId");
b.HasIndex("Vector3Id");
b.ToTable("EFACSnapshotVector3");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b =>
{
b.Property<long>("KillId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<int>("AttackerId")
.HasColumnType("INTEGER");
b.Property<int>("Damage")
.HasColumnType("INTEGER");
b.Property<int?>("DeathOriginVector3Id")
.HasColumnType("INTEGER");
b.Property<int>("DeathType")
.HasColumnType("INTEGER");
b.Property<double>("Fraction")
.HasColumnType("REAL");
b.Property<int>("HitLoc")
.HasColumnType("INTEGER");
b.Property<bool>("IsKill")
.HasColumnType("INTEGER");
b.Property<int?>("KillOriginVector3Id")
.HasColumnType("INTEGER");
b.Property<int>("Map")
.HasColumnType("INTEGER");
b.Property<long>("ServerId")
.HasColumnType("INTEGER");
b.Property<int>("VictimId")
.HasColumnType("INTEGER");
b.Property<int?>("ViewAnglesVector3Id")
.HasColumnType("INTEGER");
b.Property<double>("VisibilityPercentage")
.HasColumnType("REAL");
b.Property<int>("Weapon")
.HasColumnType("INTEGER");
b.Property<DateTime>("When")
.HasColumnType("TEXT");
b.HasKey("KillId");
b.HasIndex("AttackerId");
b.HasIndex("DeathOriginVector3Id");
b.HasIndex("KillOriginVector3Id");
b.HasIndex("ServerId");
b.HasIndex("VictimId");
b.HasIndex("ViewAnglesVector3Id");
b.ToTable("EFClientKills");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b =>
{
b.Property<long>("MessageId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<int>("ClientId")
.HasColumnType("INTEGER");
b.Property<string>("Message")
.HasColumnType("TEXT");
b.Property<long>("ServerId")
.HasColumnType("INTEGER");
b.Property<DateTime>("TimeSent")
.HasColumnType("TEXT");
b.HasKey("MessageId");
b.HasIndex("ClientId");
b.HasIndex("ServerId");
b.HasIndex("TimeSent");
b.ToTable("EFClientMessages");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", b =>
{
b.Property<int>("RatingHistoryId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<int>("ClientId")
.HasColumnType("INTEGER");
b.HasKey("RatingHistoryId");
b.HasIndex("ClientId");
b.ToTable("EFClientRatingHistory");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b =>
{
b.Property<int>("ClientId")
.HasColumnType("INTEGER");
b.Property<long>("ServerId")
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<double>("AverageRecoilOffset")
.HasColumnType("REAL");
b.Property<double>("AverageSnapValue")
.HasColumnType("REAL");
b.Property<int>("Deaths")
.HasColumnType("INTEGER");
b.Property<double>("EloRating")
.HasColumnType("REAL");
b.Property<int>("Kills")
.HasColumnType("INTEGER");
b.Property<double>("MaxStrain")
.HasColumnType("REAL");
b.Property<double>("RollingWeightedKDR")
.HasColumnType("REAL");
b.Property<double>("SPM")
.HasColumnType("REAL");
b.Property<double>("Skill")
.HasColumnType("REAL");
b.Property<int>("SnapHitCount")
.HasColumnType("INTEGER");
b.Property<int>("TimePlayed")
.HasColumnType("INTEGER");
b.Property<double>("VisionAverage")
.HasColumnType("REAL");
b.HasKey("ClientId", "ServerId");
b.HasIndex("ServerId");
b.ToTable("EFClientStatistics");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b =>
{
b.Property<int>("HitLocationCountId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<int>("EFClientStatisticsClientId")
.HasColumnName("EFClientStatisticsClientId")
.HasColumnType("INTEGER");
b.Property<long>("EFClientStatisticsServerId")
.HasColumnName("EFClientStatisticsServerId")
.HasColumnType("INTEGER");
b.Property<int>("HitCount")
.HasColumnType("INTEGER");
b.Property<float>("HitOffsetAverage")
.HasColumnType("REAL");
b.Property<int>("Location")
.HasColumnType("INTEGER");
b.Property<float>("MaxAngleDistance")
.HasColumnType("REAL");
b.HasKey("HitLocationCountId");
b.HasIndex("EFClientStatisticsServerId");
b.HasIndex("EFClientStatisticsClientId", "EFClientStatisticsServerId");
b.ToTable("EFHitLocationCounts");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFRating", b =>
{
b.Property<int>("RatingId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<int>("ActivityAmount")
.HasColumnType("INTEGER");
b.Property<bool>("Newest")
.HasColumnType("INTEGER");
b.Property<double>("Performance")
.HasColumnType("REAL");
b.Property<int>("Ranking")
.HasColumnType("INTEGER");
b.Property<int>("RatingHistoryId")
.HasColumnType("INTEGER");
b.Property<long?>("ServerId")
.HasColumnType("INTEGER");
b.Property<DateTime>("When")
.HasColumnType("TEXT");
b.HasKey("RatingId");
b.HasIndex("RatingHistoryId");
b.HasIndex("ServerId");
b.HasIndex("Performance", "Ranking", "When");
b.ToTable("EFRating");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServer", b =>
{
b.Property<long>("ServerId")
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<string>("EndPoint")
.HasColumnType("TEXT");
b.Property<int?>("GameName")
.HasColumnType("INTEGER");
b.Property<string>("HostName")
.HasColumnType("TEXT");
b.Property<int>("Port")
.HasColumnType("INTEGER");
b.HasKey("ServerId");
b.ToTable("EFServers");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b =>
{
b.Property<int>("StatisticId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<long>("ServerId")
.HasColumnType("INTEGER");
b.Property<long>("TotalKills")
.HasColumnType("INTEGER");
b.Property<long>("TotalPlayTime")
.HasColumnType("INTEGER");
b.HasKey("StatisticId");
b.HasIndex("ServerId");
b.ToTable("EFServerStatistics");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b =>
{
b.Property<int>("AliasId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<DateTime>("DateAdded")
.HasColumnType("TEXT");
b.Property<int?>("IPAddress")
.HasColumnType("INTEGER");
b.Property<int>("LinkId")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(24);
b.Property<string>("SearchableName")
.HasColumnType("TEXT")
.HasMaxLength(24);
b.HasKey("AliasId");
b.HasIndex("IPAddress");
b.HasIndex("LinkId");
b.HasIndex("Name");
b.HasIndex("SearchableName");
b.HasIndex("Name", "IPAddress")
.IsUnique();
b.ToTable("EFAlias");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAliasLink", b =>
{
b.Property<int>("AliasLinkId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.HasKey("AliasLinkId");
b.ToTable("EFAliasLinks");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFChangeHistory", b =>
{
b.Property<int>("ChangeHistoryId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<string>("Comment")
.HasColumnType("TEXT")
.HasMaxLength(128);
b.Property<string>("CurrentValue")
.HasColumnType("TEXT");
b.Property<int?>("ImpersonationEntityId")
.HasColumnType("INTEGER");
b.Property<int>("OriginEntityId")
.HasColumnType("INTEGER");
b.Property<string>("PreviousValue")
.HasColumnType("TEXT");
b.Property<int>("TargetEntityId")
.HasColumnType("INTEGER");
b.Property<DateTime>("TimeChanged")
.HasColumnType("TEXT");
b.Property<int>("TypeOfChange")
.HasColumnType("INTEGER");
b.HasKey("ChangeHistoryId");
b.ToTable("EFChangeHistory");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b =>
{
b.Property<int>("ClientId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<int>("AliasLinkId")
.HasColumnType("INTEGER");
b.Property<int>("Connections")
.HasColumnType("INTEGER");
b.Property<int>("CurrentAliasId")
.HasColumnType("INTEGER");
b.Property<DateTime>("FirstConnection")
.HasColumnType("TEXT");
b.Property<DateTime>("LastConnection")
.HasColumnType("TEXT");
b.Property<int>("Level")
.HasColumnType("INTEGER");
b.Property<bool>("Masked")
.HasColumnType("INTEGER");
b.Property<long>("NetworkId")
.HasColumnType("INTEGER");
b.Property<string>("Password")
.HasColumnType("TEXT");
b.Property<string>("PasswordSalt")
.HasColumnType("TEXT");
b.Property<int>("TotalConnectionTime")
.HasColumnType("INTEGER");
b.HasKey("ClientId");
b.HasIndex("AliasLinkId");
b.HasIndex("CurrentAliasId");
b.HasIndex("NetworkId")
.IsUnique();
b.ToTable("EFClients");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFMeta", b =>
{
b.Property<int>("MetaId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<int>("ClientId")
.HasColumnType("INTEGER");
b.Property<DateTime>("Created")
.HasColumnType("TEXT");
b.Property<string>("Extra")
.HasColumnType("TEXT");
b.Property<string>("Key")
.IsRequired()
.HasColumnType("TEXT")
.HasMaxLength(32);
b.Property<DateTime>("Updated")
.HasColumnType("TEXT");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("TEXT");
b.HasKey("MetaId");
b.HasIndex("ClientId");
b.HasIndex("Key");
b.ToTable("EFMeta");
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b =>
{
b.Property<int>("PenaltyId")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("Active")
.HasColumnType("INTEGER");
b.Property<string>("AutomatedOffense")
.HasColumnType("TEXT");
b.Property<DateTime?>("Expires")
.HasColumnType("TEXT");
b.Property<bool>("IsEvadedOffense")
.HasColumnType("INTEGER");
b.Property<int>("LinkId")
.HasColumnType("INTEGER");
b.Property<int>("OffenderId")
.HasColumnType("INTEGER");
b.Property<string>("Offense")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("PunisherId")
.HasColumnType("INTEGER");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.Property<DateTime>("When")
.HasColumnType("TEXT");
b.HasKey("PenaltyId");
b.HasIndex("LinkId");
b.HasIndex("OffenderId");
b.HasIndex("PunisherId");
b.ToTable("EFPenalties");
});
modelBuilder.Entity("SharedLibraryCore.Helpers.Vector3", b =>
{
b.Property<int>("Vector3Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<float>("X")
.HasColumnType("REAL");
b.Property<float>("Y")
.HasColumnType("REAL");
b.Property<float>("Z")
.HasColumnType("REAL");
b.HasKey("Vector3Id");
b.ToTable("Vector3");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SharedLibraryCore.Helpers.Vector3", "CurrentViewAngle")
.WithMany()
.HasForeignKey("CurrentViewAngleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SharedLibraryCore.Helpers.Vector3", "HitDestination")
.WithMany()
.HasForeignKey("HitDestinationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SharedLibraryCore.Helpers.Vector3", "HitOrigin")
.WithMany()
.HasForeignKey("HitOriginId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SharedLibraryCore.Helpers.Vector3", "LastStrainAngle")
.WithMany()
.HasForeignKey("LastStrainAngleId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFACSnapshotVector3", b =>
{
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFACSnapshot", "Snapshot")
.WithMany("PredictedViewAngles")
.HasForeignKey("SnapshotId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SharedLibraryCore.Helpers.Vector3", "Vector")
.WithMany()
.HasForeignKey("Vector3Id")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientKill", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Attacker")
.WithMany()
.HasForeignKey("AttackerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SharedLibraryCore.Helpers.Vector3", "DeathOrigin")
.WithMany()
.HasForeignKey("DeathOriginVector3Id");
b.HasOne("SharedLibraryCore.Helpers.Vector3", "KillOrigin")
.WithMany()
.HasForeignKey("KillOriginVector3Id");
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Victim")
.WithMany()
.HasForeignKey("VictimId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SharedLibraryCore.Helpers.Vector3", "ViewAngles")
.WithMany()
.HasForeignKey("ViewAnglesVector3Id");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientMessage", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
.WithMany()
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
.WithMany()
.HasForeignKey("EFClientStatisticsClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
.WithMany()
.HasForeignKey("EFClientStatisticsServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", null)
.WithMany("HitLocations")
.HasForeignKey("EFClientStatisticsClientId", "EFClientStatisticsServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFRating", b =>
{
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFClientRatingHistory", "RatingHistory")
.WithMany("Ratings")
.HasForeignKey("RatingHistoryId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId");
});
modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b =>
{
b.HasOne("IW4MAdmin.Plugins.Stats.Models.EFServer", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link")
.WithMany("Children")
.HasForeignKey("LinkId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "AliasLink")
.WithMany()
.HasForeignKey("AliasLinkId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SharedLibraryCore.Database.Models.EFAlias", "CurrentAlias")
.WithMany()
.HasForeignKey("CurrentAliasId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFMeta", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Client")
.WithMany("Meta")
.HasForeignKey("ClientId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
});
modelBuilder.Entity("SharedLibraryCore.Database.Models.EFPenalty", b =>
{
b.HasOne("SharedLibraryCore.Database.Models.EFAliasLink", "Link")
.WithMany("ReceivedPenalties")
.HasForeignKey("LinkId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Offender")
.WithMany("ReceivedPenalties")
.HasForeignKey("OffenderId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("SharedLibraryCore.Database.Models.EFClient", "Punisher")
.WithMany("AdministeredPenalties")
.HasForeignKey("PunisherId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,22 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace SharedLibraryCore.Migrations
{
public partial class AddHostnameToEFServer : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "HostName",
table: "EFServers",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "HostName",
table: "EFServers");
}
}
}

View File

@ -405,6 +405,9 @@ namespace SharedLibraryCore.Migrations
b.Property<int?>("GameName")
.HasColumnType("INTEGER");
b.Property<string>("HostName")
.HasColumnType("TEXT");
b.Property<int>("Port")
.HasColumnType("INTEGER");

View File

@ -6,7 +6,7 @@
<ApplicationIcon />
<StartupObject />
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
<Version>2.2.12</Version>
<Version>2.4.0</Version>
<Authors>RaidMax</Authors>
<Company>Forever None</Company>
<Configurations>Debug;Release;Prerelease</Configurations>
@ -20,8 +20,8 @@
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Description>Shared Library for IW4MAdmin</Description>
<AssemblyVersion>2.2.12.0</AssemblyVersion>
<FileVersion>2.2.12.0</FileVersion>
<AssemblyVersion>2.4.0.0</AssemblyVersion>
<FileVersion>2.4.0.0</FileVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'">

View File

@ -17,6 +17,7 @@
<ItemGroup>
<ProjectReference Include="..\..\Application\Application.csproj" />
<ProjectReference Include="..\..\Plugins\Stats\Stats.csproj" />
<ProjectReference Include="..\..\Plugins\Web\StatsWeb\StatsWeb.csproj" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,40 @@
using IW4MAdmin.Plugins.Stats.Models;
using SharedLibraryCore.Database.Models;
using System;
namespace ApplicationTests.Fixtures
{
public class MessageGenerators
{
public static EFClientMessage GenerateMessage(string content = null, DateTime? sent = null)
{
if (!sent.HasValue)
{
sent = DateTime.Now;
}
var rand = new Random();
string endPoint = $"127.0.0.1:{rand.Next(1000, short.MaxValue)}";
return new EFClientMessage()
{
Active = true,
Message = content,
TimeSent = sent.Value,
Client = new EFClient()
{
NetworkId = -1,
CurrentAlias = new EFAlias()
{
Name = "test"
}
},
Server = new EFServer()
{
EndPoint = endPoint,
ServerId = long.Parse(endPoint.Replace(".", "").Replace(":", ""))
}
};
}
}
}

View File

@ -0,0 +1,269 @@
using ApplicationTests.Fixtures;
using IW4MAdmin.Plugins.Stats.Models;
using Microsoft.Extensions.DependencyInjection;
using NUnit.Framework;
using SharedLibraryCore.Database;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using StatsWeb;
using StatsWeb.Extensions;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace ApplicationTests
{
[TestFixture]
public class StatsWebTests
{
private IServiceProvider serviceProvider;
private DatabaseContext dbContext;
private ChatResourceQueryHelper queryHelper;
~StatsWebTests()
{
dbContext.Dispose();
}
[SetUp]
public void Setup()
{
serviceProvider = new ServiceCollection()
.AddSingleton<ChatResourceQueryHelper>()
.BuildBase()
.BuildServiceProvider();
SetupDatabase();
queryHelper = serviceProvider.GetRequiredService<ChatResourceQueryHelper>();
}
private void SetupDatabase()
{
var contextFactory = serviceProvider.GetRequiredService<IDatabaseContextFactory>();
dbContext = contextFactory.CreateContext();
}
#region PARSE_SEARCH_INFO
[Test]
public void Test_ParseSearchInfo_SanityChecks()
{
var query = "chat|".ParseSearchInfo(-1, -1);
Assert.AreEqual(0, query.Count);
Assert.AreEqual(0, query.Offset);
query = "chat|".ParseSearchInfo(int.MaxValue, int.MaxValue);
Assert.Greater(int.MaxValue, query.Count);
}
[Test]
public void Test_ParseSearchInfo_BeforeFilter_Happy()
{
var now = DateTime.Now;
var date = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second);
var query = $"chat|before {date.ToString()}".ParseSearchInfo(0, 0);
Assert.AreEqual(date, query.SentBefore);
}
[Test]
public void Test_ParseSearchInfo_AfterFilter_Happy()
{
var now = DateTime.Now;
var date = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second);
var query = $"chat|after {date.ToString()}".ParseSearchInfo(0, 0);
Assert.AreEqual(date, query.SentAfter);
}
[Test]
public void Test_ParseSearchInfo_ServerFilter_Happy()
{
string serverId = "127.0.0.1:28960";
var query = $"chat|server {serverId}".ParseSearchInfo(0, 0);
Assert.AreEqual(serverId, query.ServerId);
}
[Test]
public void Test_ParseSearchInfo_ClientFilter_Happy()
{
int clientId = 123;
var query = $"chat|client {clientId.ToString()}".ParseSearchInfo(0, 0);
Assert.AreEqual(clientId, query.ClientId);
}
[Test]
public void Test_ParseSearchInfo_ContainsFilter_Happy()
{
string content = "test";
var query = $"chat|contains {content}".ParseSearchInfo(0, 0);
Assert.AreEqual(content, query.MessageContains);
}
[Test]
public void Test_ParseSearchInfo_SortFilter_Happy()
{
var direction = SortDirection.Ascending;
var query = $"chat|sort {direction.ToString().ToLower()}".ParseSearchInfo(0, 0);
Assert.AreEqual(direction, query.Direction);
direction = SortDirection.Descending;
query = $"chat|sort {direction.ToString().ToLower()}".ParseSearchInfo(0, 0);
Assert.AreEqual(direction, query.Direction);
}
[Test]
public void Test_ParseSearchInfo_InvalidQueryType()
{
Assert.Throws<ArgumentException>(() => "player|test".ParseSearchInfo(0, 0));
}
[Test]
public void Test_ParseSearchInfo_NoQueryType()
{
Assert.Throws<ArgumentException>(() => "".ParseSearchInfo(0, 0));
}
#endregion]
#region CHAT_RESOURCE_QUERY_HELPER
[Test]
public void Test_ChatResourceQueryHelper_Invalid()
{
var helper = serviceProvider.GetRequiredService<ChatResourceQueryHelper>();
Assert.ThrowsAsync<ArgumentException>(() => helper.QueryResource(null));
}
[Test]
public async Task Test_ChatResourceQueryHelper_SentAfter()
{
var oneHourAhead = DateTime.Now.AddHours(1);
var msg = MessageGenerators.GenerateMessage(sent: oneHourAhead);
dbContext.Set<EFClientMessage>()
.Add(msg);
await dbContext.SaveChangesAsync();
var query = $"chat|after {DateTime.Now.ToString()}".ParseSearchInfo(1, 0);
var result = await queryHelper.QueryResource(query);
Assert.AreEqual(oneHourAhead, result.Results.First().Date);
dbContext.Remove(msg);
await dbContext.SaveChangesAsync();
}
[Test]
public async Task Test_ChatResourceQueryHelper_SentBefore()
{
var oneHourAgo = DateTime.Now.AddHours(-1);
var msg = MessageGenerators.GenerateMessage(sent: oneHourAgo);
dbContext.Set<EFClientMessage>()
.Add(msg);
await dbContext.SaveChangesAsync();
var query = $"chat|before {DateTime.Now.ToString()}".ParseSearchInfo(1, 0);
var result = await queryHelper.QueryResource(query);
Assert.AreEqual(oneHourAgo, result.Results.First().Date);
dbContext.Remove(msg);
await dbContext.SaveChangesAsync();
}
[Test]
public async Task Test_ChatResourceQueryHelper_Server()
{
var msg = MessageGenerators.GenerateMessage(sent: DateTime.Now);
dbContext.Set<EFClientMessage>()
.Add(msg);
await dbContext.SaveChangesAsync();
string serverId = msg.Server.EndPoint;
var query = $"chat|server {serverId}".ParseSearchInfo(1, 0);
var result = await queryHelper.QueryResource(query);
Assert.IsNotEmpty(result.Results);
dbContext.Remove(msg);
await dbContext.SaveChangesAsync();
}
[Test]
public async Task Test_ChatResourceQueryHelper_Client()
{
var msg = MessageGenerators.GenerateMessage(sent: DateTime.Now);
dbContext.Set<EFClientMessage>()
.Add(msg);
await dbContext.SaveChangesAsync();
int clientId = msg.Client.ClientId;
var query = $"chat|client {clientId}".ParseSearchInfo(1, 0);
var result = await queryHelper.QueryResource(query);
Assert.AreEqual(clientId, result.Results.First().ClientId);
dbContext.Remove(msg);
await dbContext.SaveChangesAsync();
}
[Test]
public async Task Test_ChatResourceQueryHelper_Contains()
{
var msg = MessageGenerators.GenerateMessage(sent: DateTime.Now);
msg.Message = "this is a test";
dbContext.Set<EFClientMessage>()
.Add(msg);
await dbContext.SaveChangesAsync();
var query = $"chat|contains {msg.Message}".ParseSearchInfo(1, 0);
var result = await queryHelper.QueryResource(query);
Assert.AreEqual(msg.Message, result.Results.First().Message);
dbContext.Remove(msg);
await dbContext.SaveChangesAsync();
}
[Test]
public async Task Test_ChatResourceQueryHelper_Sort()
{
var firstMessage = MessageGenerators.GenerateMessage(sent: DateTime.Now.AddHours(-1));
var secondMessage = MessageGenerators.GenerateMessage(sent: DateTime.Now);
dbContext.Set<EFClientMessage>()
.Add(firstMessage);
dbContext.Set<EFClientMessage>()
.Add(secondMessage);
await dbContext.SaveChangesAsync();
var query = $"chat|sort {SortDirection.Ascending}".ParseSearchInfo(2, 0);
var result = await queryHelper.QueryResource(query);
Assert.AreEqual(firstMessage.TimeSent, result.Results.First().Date);
Assert.AreEqual(secondMessage.TimeSent, result.Results.Last().Date);
query = $"chat|sort {SortDirection.Descending}".ParseSearchInfo(2, 0);
result = await queryHelper.QueryResource(query);
Assert.AreEqual(firstMessage.TimeSent, result.Results.Last().Date);
Assert.AreEqual(secondMessage.TimeSent, result.Results.First().Date);
dbContext.Remove(firstMessage);
dbContext.Remove(secondMessage);
await dbContext.SaveChangesAsync();
}
#endregion
}
}

View File

@ -9,7 +9,10 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Database;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using StatsWeb;
using StatsWeb.Dtos;
using System.Collections.Generic;
using System.IO;
using System.Linq;
@ -101,12 +104,14 @@ namespace WebfrontCore
#endif
services.AddSingleton(Program.Manager);
services.AddSingleton<IResourceQueryHelper<ChatSearchQuery, ChatSearchResult>, ChatResourceQueryHelper>();
// todo: this needs to be handled more gracefully
services.AddSingleton(Program.ApplicationServiceProvider.GetService<IConfigurationHandlerFactory>());
services.AddSingleton(Program.ApplicationServiceProvider.GetService<IDatabaseContextFactory>());
services.AddSingleton(Program.ApplicationServiceProvider.GetService<IAuditInformationRepository>());
services.AddSingleton(Program.ApplicationServiceProvider.GetService<ITranslationLookup>());
services.AddSingleton(Program.ApplicationServiceProvider.GetService<SharedLibraryCore.Interfaces.ILogger>());
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

View File

@ -78,6 +78,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Plugins\Web\StatsWeb\StatsWeb.csproj" />
<ProjectReference Include="..\SharedLibraryCore\SharedLibraryCore.csproj" />
</ItemGroup>

View File

@ -5,18 +5,22 @@
$('#client_search')
.addClass('input-text-danger')
.delay(25)
.queue(function(){
.queue(function () {
$(this).addClass('input-border-transition').dequeue();
})
.delay(1000)
.queue(function() {
.queue(function () {
$(this).removeClass('input-text-danger').dequeue();
})
.delay(500)
.queue(function() {
.queue(function () {
$(this).removeClass('input-border-transition').dequeue();
});
}
else if ($('#client_search').val().startsWith("chat|")) {
e.preventDefault();
window.location = "/Message/Find?query=" + $('#client_search').val();
}
});
});