diff --git a/Plugins/Stats/Helpers/StatManager.cs b/Plugins/Stats/Helpers/StatManager.cs index e8f295985..1af6dfe17 100644 --- a/Plugins/Stats/Helpers/StatManager.cs +++ b/Plugins/Stats/Helpers/StatManager.cs @@ -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 diff --git a/Plugins/Stats/Models/EFServer.cs b/Plugins/Stats/Models/EFServer.cs index 104254165..62f0fc140 100644 --- a/Plugins/Stats/Models/EFServer.cs +++ b/Plugins/Stats/Models/EFServer.cs @@ -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; } } } diff --git a/Plugins/Web/StatsWeb/ChatResourceQueryHelper.cs b/Plugins/Web/StatsWeb/ChatResourceQueryHelper.cs new file mode 100644 index 000000000..54e7d6be1 --- /dev/null +++ b/Plugins/Web/StatsWeb/ChatResourceQueryHelper.cs @@ -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 +{ + /// + /// implementation of IResourceQueryHelper + /// + public class ChatResourceQueryHelper : IResourceQueryHelper + { + private readonly IDatabaseContextFactory _contextFactory; + private readonly ILogger _logger; + + public ChatResourceQueryHelper(ILogger logger, IDatabaseContextFactory contextFactory) + { + _contextFactory = contextFactory; + _logger = logger; + } + + /// + public async Task> QueryResource(ChatSearchQuery query) + { + if (query == null) + { + throw new ArgumentException("Query must be specified"); + } + + var result = new ResourceQueryHelperResult(); + using var context = _contextFactory.CreateContext(enableTracking: false); + + var iqMessages = context.Set() + .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; + } + } +} diff --git a/Plugins/Web/StatsWeb/Controllers/StatsController.cs b/Plugins/Web/StatsWeb/Controllers/StatsController.cs index d7c6a7883..690538b92 100644 --- a/Plugins/Web/StatsWeb/Controllers/StatsController.cs +++ b/Plugins/Web/StatsWeb/Controllers/StatsController.cs @@ -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 _chatResourceQueryHelper; + private readonly ITranslationLookup _translationLookup; - public StatsController(IManager manager) : base(manager) + public StatsController(ILogger logger, IManager manager, IResourceQueryHelper 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 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 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 GetAutomatedPenaltyInfoAsync(int penaltyId) diff --git a/Plugins/Web/StatsWeb/Dtos/ChatSearchQuery.cs b/Plugins/Web/StatsWeb/Dtos/ChatSearchQuery.cs new file mode 100644 index 000000000..a63185fdb --- /dev/null +++ b/Plugins/Web/StatsWeb/Dtos/ChatSearchQuery.cs @@ -0,0 +1,33 @@ +using SharedLibraryCore.Dtos; +using System; + +namespace StatsWeb.Dtos +{ + public class ChatSearchQuery : PaginationInfo + { + /// + /// specifies the partial content of the message to search for + /// + public string MessageContains { get; set; } + + /// + /// identifier for the server + /// + public string ServerId { get; set; } + + /// + /// identifier for the client + /// + public int? ClientId { get; set; } + + /// + /// only look for messages sent after this date + /// + public DateTime SentAfter { get; set; } = DateTime.UtcNow.AddYears(-100); + + /// + /// only look for messages sent before this date0 + /// + public DateTime SentBefore { get; set; } = DateTime.UtcNow; + } +} diff --git a/Plugins/Web/StatsWeb/Dtos/ChatSearchResult.cs b/Plugins/Web/StatsWeb/Dtos/ChatSearchResult.cs new file mode 100644 index 000000000..9390e1d18 --- /dev/null +++ b/Plugins/Web/StatsWeb/Dtos/ChatSearchResult.cs @@ -0,0 +1,32 @@ +using System; + +namespace StatsWeb.Dtos +{ + public class ChatSearchResult + { + /// + /// name of the client + /// + public string ClientName { get; set; } + + /// + /// client id + /// + public int ClientId { get; set; } + + /// + /// hostname of the server + /// + public string ServerName { get; set; } + + /// + /// chat message + /// + public string Message { get; set; } + + /// + /// date the chat occured on + /// + public DateTime Date { get; set; } + } +} diff --git a/Plugins/Web/StatsWeb/Extensions/SearchQueryExtensions.cs b/Plugins/Web/StatsWeb/Extensions/SearchQueryExtensions.cs new file mode 100644 index 000000000..66f582286 --- /dev/null +++ b/Plugins/Web/StatsWeb/Extensions/SearchQueryExtensions.cs @@ -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; + + /// + /// todo: lets abstract this out to a generic buildable query + /// this is just a dirty PoC + /// + /// + /// + 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(args[1], ignoreCase: true); + break; + } + } + } + + return searchRequest; + } + + throw new ArgumentException("No filters specified for chat search"); + } + } +} diff --git a/Plugins/Web/StatsWeb/StatsWeb.csproj b/Plugins/Web/StatsWeb/StatsWeb.csproj index bb7d9a4a5..c578cc61c 100644 --- a/Plugins/Web/StatsWeb/StatsWeb.csproj +++ b/Plugins/Web/StatsWeb/StatsWeb.csproj @@ -7,14 +7,14 @@ false true Debug;Release;Prerelease - 7.1 + 8.0 Library Always - + diff --git a/Plugins/Web/StatsWeb/Views/Stats/Message/Find.cshtml b/Plugins/Web/StatsWeb/Views/Stats/Message/Find.cshtml new file mode 100644 index 000000000..d4bc1aa87 --- /dev/null +++ b/Plugins/Web/StatsWeb/Views/Stats/Message/Find.cshtml @@ -0,0 +1,38 @@ +@model SharedLibraryCore.Helpers.ResourceQueryHelperResult + +@if (ViewBag.Error != null) +{ +

@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_INVALID_QUERY"], ViewBag.Error.Message)

+} + +else +{ +

@SharedLibraryCore.Utilities.FormatExt(ViewBag.Localization["WEBFRONT_STATS_MESSAGES_FOUND"], Model.TotalResultCount.ToString("N0"))

+ + + + + + + + + + + + + +
@ViewBag.Localization["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]@ViewBag.Localization["WEBFRONT_ACTION_LABEL_MESSAGE"]@ViewBag.Localization["WEBFRONT_STATS_MESSAGE_SERVER_NAME"]@ViewBag.Localization["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]
+ + + + @section scripts { + + + + + } +} \ No newline at end of file diff --git a/Plugins/Web/StatsWeb/Views/Stats/Message/_Item.cshtml b/Plugins/Web/StatsWeb/Views/Stats/Message/_Item.cshtml new file mode 100644 index 000000000..6a5388fe5 --- /dev/null +++ b/Plugins/Web/StatsWeb/Views/Stats/Message/_Item.cshtml @@ -0,0 +1,53 @@ +@model IEnumerable + +@foreach (var message in Model) +{ + + + + + + + + + + + + + + + @message.Date + + + + + + @ViewBag.Localization["WEBFRONT_PENALTY_TEMPLATE_ADMIN"] + + + + + + + + + @ViewBag.Localization["WEBFRONT_ACTION_LABEL_MESSAGE"] + + + + + + + @ViewBag.Localization["WEBFRONT_STATS_MESSAGE_SERVER_NAME"] + + + + + + + @ViewBag.Localization["WEBFRONT_ADMIN_AUDIT_LOG_TIME"] + + @message.Date + + +} \ No newline at end of file diff --git a/README.md b/README.md index f6b073c7b..c5c5abb9d 100644 --- a/README.md +++ b/README.md @@ -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 — `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. diff --git a/SharedLibraryCore/Helpers/ResourceQueryHelperResult.cs b/SharedLibraryCore/Helpers/ResourceQueryHelperResult.cs new file mode 100644 index 000000000..48076e774 --- /dev/null +++ b/SharedLibraryCore/Helpers/ResourceQueryHelperResult.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace SharedLibraryCore.Helpers +{ + /// + /// generic class for passing information about a resource query + /// + /// Type of query result + public class ResourceQueryHelperResult + { + /// + /// indicates the total number of results found + /// + public long TotalResultCount { get; set; } + + /// + /// indicates the total number of results retrieved + /// + public int RetrievedResultCount { get; set; } + + /// + /// collection of results + /// + public IEnumerable Results { get; set; } + } +} diff --git a/SharedLibraryCore/Interfaces/IResourceQueryHelper.cs b/SharedLibraryCore/Interfaces/IResourceQueryHelper.cs new file mode 100644 index 000000000..ede8251c6 --- /dev/null +++ b/SharedLibraryCore/Interfaces/IResourceQueryHelper.cs @@ -0,0 +1,20 @@ +using SharedLibraryCore.Helpers; +using System.Threading.Tasks; + +namespace SharedLibraryCore.Interfaces +{ + /// + /// defines the capabilities of a resource queryier + /// + /// Type of query + /// Type of result + public interface IResourceQueryHelper + { + /// + /// queries a resource and returns the result of the query + /// + /// query params + /// + Task> QueryResource(QueryType query); + } +} diff --git a/SharedLibraryCore/Migrations/20200521203304_AddHostnameToEFServer.Designer.cs b/SharedLibraryCore/Migrations/20200521203304_AddHostnameToEFServer.Designer.cs new file mode 100644 index 000000000..f741702e2 --- /dev/null +++ b/SharedLibraryCore/Migrations/20200521203304_AddHostnameToEFServer.Designer.cs @@ -0,0 +1,922 @@ +// +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("SnapshotId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("CurrentSessionLength") + .HasColumnType("INTEGER"); + + b.Property("CurrentStrain") + .HasColumnType("REAL"); + + b.Property("CurrentViewAngleId") + .HasColumnType("INTEGER"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("Distance") + .HasColumnType("REAL"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("HitDestinationId") + .HasColumnType("INTEGER"); + + b.Property("HitLocation") + .HasColumnType("INTEGER"); + + b.Property("HitOriginId") + .HasColumnType("INTEGER"); + + b.Property("HitType") + .HasColumnType("INTEGER"); + + b.Property("Hits") + .HasColumnType("INTEGER"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("LastStrainAngleId") + .HasColumnType("INTEGER"); + + b.Property("RecoilOffset") + .HasColumnType("REAL"); + + b.Property("SessionAngleOffset") + .HasColumnType("REAL"); + + b.Property("SessionAverageSnapValue") + .HasColumnType("REAL"); + + b.Property("SessionSPM") + .HasColumnType("REAL"); + + b.Property("SessionScore") + .HasColumnType("INTEGER"); + + b.Property("SessionSnapHits") + .HasColumnType("INTEGER"); + + b.Property("StrainAngleBetween") + .HasColumnType("REAL"); + + b.Property("TimeSinceLastEvent") + .HasColumnType("INTEGER"); + + b.Property("WeaponId") + .HasColumnType("INTEGER"); + + b.Property("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("ACSnapshotVector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("SnapshotId") + .HasColumnType("INTEGER"); + + b.Property("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("KillId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AttackerId") + .HasColumnType("INTEGER"); + + b.Property("Damage") + .HasColumnType("INTEGER"); + + b.Property("DeathOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("DeathType") + .HasColumnType("INTEGER"); + + b.Property("Fraction") + .HasColumnType("REAL"); + + b.Property("HitLoc") + .HasColumnType("INTEGER"); + + b.Property("IsKill") + .HasColumnType("INTEGER"); + + b.Property("KillOriginVector3Id") + .HasColumnType("INTEGER"); + + b.Property("Map") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("VictimId") + .HasColumnType("INTEGER"); + + b.Property("ViewAnglesVector3Id") + .HasColumnType("INTEGER"); + + b.Property("VisibilityPercentage") + .HasColumnType("REAL"); + + b.Property("Weapon") + .HasColumnType("INTEGER"); + + b.Property("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("MessageId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("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("RatingHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.HasKey("RatingHistoryId"); + + b.HasIndex("ClientId"); + + b.ToTable("EFClientRatingHistory"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFClientStatistics", b => + { + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AverageRecoilOffset") + .HasColumnType("REAL"); + + b.Property("AverageSnapValue") + .HasColumnType("REAL"); + + b.Property("Deaths") + .HasColumnType("INTEGER"); + + b.Property("EloRating") + .HasColumnType("REAL"); + + b.Property("Kills") + .HasColumnType("INTEGER"); + + b.Property("MaxStrain") + .HasColumnType("REAL"); + + b.Property("RollingWeightedKDR") + .HasColumnType("REAL"); + + b.Property("SPM") + .HasColumnType("REAL"); + + b.Property("Skill") + .HasColumnType("REAL"); + + b.Property("SnapHitCount") + .HasColumnType("INTEGER"); + + b.Property("TimePlayed") + .HasColumnType("INTEGER"); + + b.Property("VisionAverage") + .HasColumnType("REAL"); + + b.HasKey("ClientId", "ServerId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFClientStatistics"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFHitLocationCount", b => + { + b.Property("HitLocationCountId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EFClientStatisticsClientId") + .HasColumnName("EFClientStatisticsClientId") + .HasColumnType("INTEGER"); + + b.Property("EFClientStatisticsServerId") + .HasColumnName("EFClientStatisticsServerId") + .HasColumnType("INTEGER"); + + b.Property("HitCount") + .HasColumnType("INTEGER"); + + b.Property("HitOffsetAverage") + .HasColumnType("REAL"); + + b.Property("Location") + .HasColumnType("INTEGER"); + + b.Property("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("RatingId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ActivityAmount") + .HasColumnType("INTEGER"); + + b.Property("Newest") + .HasColumnType("INTEGER"); + + b.Property("Performance") + .HasColumnType("REAL"); + + b.Property("Ranking") + .HasColumnType("INTEGER"); + + b.Property("RatingHistoryId") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("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("ServerId") + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("EndPoint") + .HasColumnType("TEXT"); + + b.Property("GameName") + .HasColumnType("INTEGER"); + + b.Property("HostName") + .HasColumnType("TEXT"); + + b.Property("Port") + .HasColumnType("INTEGER"); + + b.HasKey("ServerId"); + + b.ToTable("EFServers"); + }); + + modelBuilder.Entity("IW4MAdmin.Plugins.Stats.Models.EFServerStatistics", b => + { + b.Property("StatisticId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("TotalKills") + .HasColumnType("INTEGER"); + + b.Property("TotalPlayTime") + .HasColumnType("INTEGER"); + + b.HasKey("StatisticId"); + + b.HasIndex("ServerId"); + + b.ToTable("EFServerStatistics"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFAlias", b => + { + b.Property("AliasId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("IPAddress") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(24); + + b.Property("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("AliasLinkId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.HasKey("AliasLinkId"); + + b.ToTable("EFAliasLinks"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFChangeHistory", b => + { + b.Property("ChangeHistoryId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Comment") + .HasColumnType("TEXT") + .HasMaxLength(128); + + b.Property("CurrentValue") + .HasColumnType("TEXT"); + + b.Property("ImpersonationEntityId") + .HasColumnType("INTEGER"); + + b.Property("OriginEntityId") + .HasColumnType("INTEGER"); + + b.Property("PreviousValue") + .HasColumnType("TEXT"); + + b.Property("TargetEntityId") + .HasColumnType("INTEGER"); + + b.Property("TimeChanged") + .HasColumnType("TEXT"); + + b.Property("TypeOfChange") + .HasColumnType("INTEGER"); + + b.HasKey("ChangeHistoryId"); + + b.ToTable("EFChangeHistory"); + }); + + modelBuilder.Entity("SharedLibraryCore.Database.Models.EFClient", b => + { + b.Property("ClientId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AliasLinkId") + .HasColumnType("INTEGER"); + + b.Property("Connections") + .HasColumnType("INTEGER"); + + b.Property("CurrentAliasId") + .HasColumnType("INTEGER"); + + b.Property("FirstConnection") + .HasColumnType("TEXT"); + + b.Property("LastConnection") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Masked") + .HasColumnType("INTEGER"); + + b.Property("NetworkId") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("PasswordSalt") + .HasColumnType("TEXT"); + + b.Property("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("MetaId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("INTEGER"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("Extra") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(32); + + b.Property("Updated") + .HasColumnType("TEXT"); + + b.Property("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("PenaltyId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AutomatedOffense") + .HasColumnType("TEXT"); + + b.Property("Expires") + .HasColumnType("TEXT"); + + b.Property("IsEvadedOffense") + .HasColumnType("INTEGER"); + + b.Property("LinkId") + .HasColumnType("INTEGER"); + + b.Property("OffenderId") + .HasColumnType("INTEGER"); + + b.Property("Offense") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PunisherId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("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("Vector3Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("X") + .HasColumnType("REAL"); + + b.Property("Y") + .HasColumnType("REAL"); + + b.Property("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 + } + } +} diff --git a/SharedLibraryCore/Migrations/20200521203304_AddHostnameToEFServer.cs b/SharedLibraryCore/Migrations/20200521203304_AddHostnameToEFServer.cs new file mode 100644 index 000000000..95d24d172 --- /dev/null +++ b/SharedLibraryCore/Migrations/20200521203304_AddHostnameToEFServer.cs @@ -0,0 +1,22 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace SharedLibraryCore.Migrations +{ + public partial class AddHostnameToEFServer : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "HostName", + table: "EFServers", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "HostName", + table: "EFServers"); + } + } +} diff --git a/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs b/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs index 2620ced31..01743b0c8 100644 --- a/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs +++ b/SharedLibraryCore/Migrations/DatabaseContextModelSnapshot.cs @@ -405,6 +405,9 @@ namespace SharedLibraryCore.Migrations b.Property("GameName") .HasColumnType("INTEGER"); + b.Property("HostName") + .HasColumnType("TEXT"); + b.Property("Port") .HasColumnType("INTEGER"); diff --git a/SharedLibraryCore/SharedLibraryCore.csproj b/SharedLibraryCore/SharedLibraryCore.csproj index 0c9e3d18c..28f2fcd17 100644 --- a/SharedLibraryCore/SharedLibraryCore.csproj +++ b/SharedLibraryCore/SharedLibraryCore.csproj @@ -6,7 +6,7 @@ RaidMax.IW4MAdmin.SharedLibraryCore - 2.2.12 + 2.4.0 RaidMax Forever None Debug;Release;Prerelease @@ -20,8 +20,8 @@ true MIT Shared Library for IW4MAdmin - 2.2.12.0 - 2.2.12.0 + 2.4.0.0 + 2.4.0.0 diff --git a/Tests/ApplicationTests/ApplicationTests.csproj b/Tests/ApplicationTests/ApplicationTests.csproj index 1c0b41a26..e638f886c 100644 --- a/Tests/ApplicationTests/ApplicationTests.csproj +++ b/Tests/ApplicationTests/ApplicationTests.csproj @@ -17,6 +17,7 @@ + diff --git a/Tests/ApplicationTests/Fixtures/MessageGenerators.cs b/Tests/ApplicationTests/Fixtures/MessageGenerators.cs new file mode 100644 index 000000000..19461e59e --- /dev/null +++ b/Tests/ApplicationTests/Fixtures/MessageGenerators.cs @@ -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(":", "")) + } + }; + } + } +} diff --git a/Tests/ApplicationTests/StatsWebTests.cs b/Tests/ApplicationTests/StatsWebTests.cs new file mode 100644 index 000000000..b3069a6a8 --- /dev/null +++ b/Tests/ApplicationTests/StatsWebTests.cs @@ -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() + .BuildBase() + .BuildServiceProvider(); + + SetupDatabase(); + + queryHelper = serviceProvider.GetRequiredService(); + } + + private void SetupDatabase() + { + var contextFactory = serviceProvider.GetRequiredService(); + 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(() => "player|test".ParseSearchInfo(0, 0)); + } + + [Test] + public void Test_ParseSearchInfo_NoQueryType() + { + Assert.Throws(() => "".ParseSearchInfo(0, 0)); + } + #endregion] + + #region CHAT_RESOURCE_QUERY_HELPER + [Test] + public void Test_ChatResourceQueryHelper_Invalid() + { + var helper = serviceProvider.GetRequiredService(); + + Assert.ThrowsAsync(() => helper.QueryResource(null)); + } + + [Test] + public async Task Test_ChatResourceQueryHelper_SentAfter() + { + var oneHourAhead = DateTime.Now.AddHours(1); + var msg = MessageGenerators.GenerateMessage(sent: oneHourAhead); + + dbContext.Set() + .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() + .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() + .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() + .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() + .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() + .Add(firstMessage); + dbContext.Set() + .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 + } +} diff --git a/WebfrontCore/Startup.cs b/WebfrontCore/Startup.cs index 2d6341c1c..9344ed8cb 100644 --- a/WebfrontCore/Startup.cs +++ b/WebfrontCore/Startup.cs @@ -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, ChatResourceQueryHelper>(); // todo: this needs to be handled more gracefully services.AddSingleton(Program.ApplicationServiceProvider.GetService()); services.AddSingleton(Program.ApplicationServiceProvider.GetService()); services.AddSingleton(Program.ApplicationServiceProvider.GetService()); services.AddSingleton(Program.ApplicationServiceProvider.GetService()); + services.AddSingleton(Program.ApplicationServiceProvider.GetService()); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/WebfrontCore/WebfrontCore.csproj b/WebfrontCore/WebfrontCore.csproj index 2a60ad4ac..0d8fba8a5 100644 --- a/WebfrontCore/WebfrontCore.csproj +++ b/WebfrontCore/WebfrontCore.csproj @@ -78,6 +78,7 @@ + diff --git a/WebfrontCore/wwwroot/js/search.js b/WebfrontCore/wwwroot/js/search.js index e12342ffd..c24850b2e 100644 --- a/WebfrontCore/wwwroot/js/search.js +++ b/WebfrontCore/wwwroot/js/search.js @@ -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(); } }); }); \ No newline at end of file