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