Compare commits

...

12 Commits

23 changed files with 421 additions and 120 deletions

View File

@ -24,7 +24,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Jint" Version="3.0.0-beta-2037" /> <PackageReference Include="Jint" Version="3.0.0-beta-2038" />
<PackageReference Include="MaxMind.GeoIP2" Version="5.1.0" /> <PackageReference Include="MaxMind.GeoIP2" Version="5.1.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

View File

@ -792,8 +792,16 @@ namespace IW4MAdmin
/// <returns></returns> /// <returns></returns>
async Task<List<EFClient>[]> PollPlayersAsync() async Task<List<EFClient>[]> PollPlayersAsync()
{ {
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
var currentClients = GetClientsAsList(); var currentClients = GetClientsAsList();
var statusResponse = await this.GetStatusAsync(Manager.CancellationToken); var statusResponse = await this.GetStatusAsync(tokenSource.Token);
if (statusResponse is null)
{
return null;
}
var polledClients = statusResponse.Clients.AsEnumerable(); var polledClients = statusResponse.Clients.AsEnumerable();
if (Manager.GetApplicationSettings().Configuration().IgnoreBots) if (Manager.GetApplicationSettings().Configuration().IgnoreBots)
@ -930,6 +938,11 @@ namespace IW4MAdmin
var polledClients = await PollPlayersAsync(); var polledClients = await PollPlayersAsync();
if (polledClients is null)
{
return true;
}
foreach (var disconnectingClient in polledClients[1] foreach (var disconnectingClient in polledClients[1]
.Where(client => !client.IsZombieClient /* ignores "fake" zombie clients */)) .Where(client => !client.IsZombieClient /* ignores "fake" zombie clients */))
{ {

View File

@ -0,0 +1,12 @@
using System;
using System.Threading;
namespace IW4MAdmin.Application.Misc;
public class AsyncResult : IAsyncResult
{
public object AsyncState { get; set; }
public WaitHandle AsyncWaitHandle { get; set; }
public bool CompletedSynchronously { get; set; }
public bool IsCompleted { get; set; }
}

View File

@ -276,8 +276,8 @@ namespace IW4MAdmin.Application.Misc
{ {
_logger.LogDebug("OnLoad executing for {Name}", Name); _logger.LogDebug("OnLoad executing for {Name}", Name);
_scriptEngine.SetValue("_manager", manager); _scriptEngine.SetValue("_manager", manager);
_scriptEngine.SetValue("getDvar", GetDvarAsync); _scriptEngine.SetValue("getDvar", BeginGetDvar);
_scriptEngine.SetValue("setDvar", SetDvarAsync); _scriptEngine.SetValue("setDvar", BeginSetDvar);
_scriptEngine.Evaluate("plugin.onLoadAsync(_manager)"); _scriptEngine.Evaluate("plugin.onLoadAsync(_manager)");
return Task.CompletedTask; return Task.CompletedTask;
@ -343,7 +343,8 @@ namespace IW4MAdmin.Application.Misc
/// <param name="commands">commands value from jint parser</param> /// <param name="commands">commands value from jint parser</param>
/// <param name="scriptCommandFactory">factory to create the command from</param> /// <param name="scriptCommandFactory">factory to create the command from</param>
/// <returns></returns> /// <returns></returns>
private IEnumerable<IManagerCommand> GenerateScriptCommands(JsValue commands, IScriptCommandFactory scriptCommandFactory) private IEnumerable<IManagerCommand> GenerateScriptCommands(JsValue commands,
IScriptCommandFactory scriptCommandFactory)
{ {
var commandList = new List<IManagerCommand>(); var commandList = new List<IManagerCommand>();
@ -454,65 +455,54 @@ namespace IW4MAdmin.Application.Misc
return commandList; return commandList;
} }
private void GetDvarAsync(Server server, string dvarName, Delegate onCompleted) private void BeginGetDvar(Server server, string dvarName, Delegate onCompleted)
{
Task.Run(() =>
{ {
var tokenSource = new CancellationTokenSource(); var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(5)); tokenSource.CancelAfter(TimeSpan.FromSeconds(15));
string result = null;
var success = true;
try
{
result = server.GetDvarAsync<string>(dvarName, token: tokenSource.Token).GetAwaiter().GetResult().Value;
}
catch
{
success = false;
}
_onProcessing.Wait(); server.BeginGetDvar(dvarName, result =>
{
var shouldRelease = false;
try try
{ {
_onProcessing.Wait(tokenSource.Token);
shouldRelease = true;
var (success, value) = (ValueTuple<bool, string>)result.AsyncState;
onCompleted.DynamicInvoke(JsValue.Undefined, onCompleted.DynamicInvoke(JsValue.Undefined,
new[] new[]
{ {
JsValue.FromObject(_scriptEngine, server), JsValue.FromObject(_scriptEngine, server),
JsValue.FromObject(_scriptEngine, dvarName), JsValue.FromObject(_scriptEngine, dvarName),
JsValue.FromObject(_scriptEngine, result), JsValue.FromObject(_scriptEngine, value),
JsValue.FromObject(_scriptEngine, success), JsValue.FromObject(_scriptEngine, success)
}); });
} }
finally finally
{ {
if (_onProcessing.CurrentCount == 0) if (_onProcessing.CurrentCount == 0 && shouldRelease)
{ {
_onProcessing.Release(); _onProcessing.Release();
} }
} }
}); }, tokenSource.Token);
} }
private void SetDvarAsync(Server server, string dvarName, string dvarValue, Delegate onCompleted)
{ private void BeginSetDvar(Server server, string dvarName, string dvarValue, Delegate onCompleted)
Task.Run(() =>
{ {
var tokenSource = new CancellationTokenSource(); var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(5)); tokenSource.CancelAfter(TimeSpan.FromSeconds(15));
var success = true;
server.BeginSetDvar(dvarName, dvarValue, result =>
{
var shouldRelease = false;
try try
{ {
server.SetDvarAsync(dvarName, dvarValue, tokenSource.Token).GetAwaiter().GetResult(); _onProcessing.Wait(tokenSource.Token);
} shouldRelease = true;
catch var success = (bool)result.AsyncState;
{
success = false;
}
_onProcessing.Wait();
try
{
onCompleted.DynamicInvoke(JsValue.Undefined, onCompleted.DynamicInvoke(JsValue.Undefined,
new[] new[]
{ {
@ -522,15 +512,14 @@ namespace IW4MAdmin.Application.Misc
JsValue.FromObject(_scriptEngine, success) JsValue.FromObject(_scriptEngine, success)
}); });
} }
finally finally
{ {
if (_onProcessing.CurrentCount == 0) if (_onProcessing.CurrentCount == 0 && shouldRelease)
{ {
_onProcessing.Release(); _onProcessing.Release();
} }
} }
}); }, tokenSource.Token);
} }
} }

View File

@ -10,6 +10,7 @@ using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Models; using Data.Models;
using IW4MAdmin.Application.Misc;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using static SharedLibraryCore.Server; using static SharedLibraryCore.Server;
using ILogger = Microsoft.Extensions.Logging.ILogger; using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -141,6 +142,30 @@ namespace IW4MAdmin.Application.RConParsers
}; };
} }
public void BeginGetDvar(IRConConnection connection, string dvarName, AsyncCallback callback, CancellationToken token = default)
{
GetDvarAsync<string>(connection, dvarName, token: token).ContinueWith(action =>
{
if (action.Exception is null)
{
callback?.Invoke(new AsyncResult
{
IsCompleted = true,
AsyncState = (true, action.Result.Value)
});
}
else
{
callback?.Invoke(new AsyncResult
{
IsCompleted = true,
AsyncState = (false, (string)null)
});
}
}, token);
}
public virtual async Task<IStatusResponse> GetStatusAsync(IRConConnection connection, CancellationToken token = default) public virtual async Task<IStatusResponse> GetStatusAsync(IRConConnection connection, CancellationToken token = default)
{ {
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS, "status", token); var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND_STATUS, "status", token);
@ -196,6 +221,31 @@ namespace IW4MAdmin.Application.RConParsers
return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString, token)).Length > 0; return (await connection.SendQueryAsync(StaticHelpers.QueryType.SET_DVAR, dvarString, token)).Length > 0;
} }
public void BeginSetDvar(IRConConnection connection, string dvarName, object dvarValue, AsyncCallback callback,
CancellationToken token = default)
{
SetDvarAsync(connection, dvarName, dvarValue, token).ContinueWith(action =>
{
if (action.Exception is null)
{
callback?.Invoke(new AsyncResult
{
IsCompleted = true,
AsyncState = true
});
}
else
{
callback?.Invoke(new AsyncResult
{
IsCompleted = true,
AsyncState = false
});
}
}, token);
}
private List<EFClient> ClientsFromStatus(string[] Status) private List<EFClient> ClientsFromStatus(string[] Status)
{ {
List<EFClient> StatusPlayers = new List<EFClient>(); List<EFClient> StatusPlayers = new List<EFClient>();

View File

@ -46,7 +46,7 @@ namespace IW4MAdmin.Application.RConParsers
{ColorCodes.White.ToString(), "^7"}, {ColorCodes.White.ToString(), "^7"},
{ColorCodes.Map.ToString(), "^8"}, {ColorCodes.Map.ToString(), "^8"},
{ColorCodes.Grey.ToString(), "^9"}, {ColorCodes.Grey.ToString(), "^9"},
{ColorCodes.Wildcard.ToString(), ":^"}, {ColorCodes.Wildcard.ToString(), "^:"}
}; };
public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory) public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory)

View File

@ -53,6 +53,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlug
Plugins\ScriptPlugins\GameInterface.js = Plugins\ScriptPlugins\GameInterface.js Plugins\ScriptPlugins\GameInterface.js = Plugins\ScriptPlugins\GameInterface.js
Plugins\ScriptPlugins\SubnetBan.js = Plugins\ScriptPlugins\SubnetBan.js Plugins\ScriptPlugins\SubnetBan.js = Plugins\ScriptPlugins\SubnetBan.js
Plugins\ScriptPlugins\BanBroadcasting.js = Plugins\ScriptPlugins\BanBroadcasting.js Plugins\ScriptPlugins\BanBroadcasting.js = Plugins\ScriptPlugins\BanBroadcasting.js
Plugins\ScriptPlugins\ParserH1MOD.js = Plugins\ScriptPlugins\ParserH1MOD.js
Plugins\ScriptPlugins\ParserPlutoniumT5.js = Plugins\ScriptPlugins\ParserPlutoniumT5.js
EndProjectSection EndProjectSection
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutomessageFeed", "Plugins\AutomessageFeed\AutomessageFeed.csproj", "{F5815359-CFC7-44B4-9A3B-C04BACAD5836}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutomessageFeed", "Plugins\AutomessageFeed\AutomessageFeed.csproj", "{F5815359-CFC7-44B4-9A3B-C04BACAD5836}"

View File

@ -0,0 +1,39 @@
var rconParser;
var eventParser;
var plugin = {
author: 'RaidMax',
version: 0.1,
name: 'Plutonium T5 Parser',
isParser: true,
onEventAsync: function (gameEvent, server) {
},
onLoadAsync: function (manager) {
rconParser = manager.GenerateDynamicRConParser(this.name);
eventParser = manager.GenerateDynamicEventParser(this.name);
rconParser.Configuration.DefaultInstallationDirectoryHint = '{LocalAppData}/Plutonium/storage/t5';
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n';
rconParser.Configuration.Dvar.Pattern = '^(?:\\^7)?\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n(?:latched: \\"(.+)?\\"\\n)? *(.+)$';
rconParser.Configuration.CommandPrefixes.Tell = 'tell {0} {1}';
rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined;
rconParser.Configuration.GuidNumberStyle = 7; // Integer
rconParser.Configuration.DefaultRConPort = 3074;
rconParser.Configuration.CanGenerateLogPath = false;
rconParser.Version = 'Call of Duty Multiplayer - Ship COD_T5_S MP build 7.0.189 CL(1022875) CODPCAB-V64 CEG Wed Nov 02 18:02:23 2011 win-x86';
rconParser.GameName = 6; // T5
eventParser.Version = 'Call of Duty Multiplayer - Ship COD_T5_S MP build 7.0.189 CL(1022875) CODPCAB-V64 CEG Wed Nov 02 18:02:23 2011 win-x86';
eventParser.GameName = 6; // T5
eventParser.Configuration.GuidNumberStyle = 7; // Integer
},
onUnloadAsync: function () {
},
onTickAsync: function (server) {
}
};

View File

@ -29,7 +29,7 @@ const plugin = {
let exempt = false; let exempt = false;
// prevent players that are exempt from being kicked // prevent players that are exempt from being kicked
vpnExceptionIds.forEach(function (id) { vpnExceptionIds.forEach(function (id) {
if (id === origin.ClientId) { if (id == origin.ClientId) { // when loaded from the config the "id" type is not the same as the ClientId type
exempt = true; exempt = true;
return false; return false;
} }

View File

@ -1,4 +1,6 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq;
using static IW4MAdmin.Plugins.Stats.Cheat.Detection; using static IW4MAdmin.Plugins.Stats.Cheat.Detection;
using static SharedLibraryCore.Server; using static SharedLibraryCore.Server;
@ -7,7 +9,15 @@ namespace Stats.Config
public class AnticheatConfiguration public class AnticheatConfiguration
{ {
public bool Enable { get; set; } public bool Enable { get; set; }
[Obsolete]
public IDictionary<long, DetectionType[]> ServerDetectionTypes { get; set; } = new Dictionary<long, DetectionType[]>(); public IDictionary<long, DetectionType[]> ServerDetectionTypes { get; set; } = new Dictionary<long, DetectionType[]>();
public IDictionary<Game, DetectionType[]> GameDetectionTypes { get; set; } =
new Dictionary<Game, DetectionType[]>()
{
{ Game.IW4, Enum.GetValues(typeof(DetectionType)).Cast<DetectionType>().ToArray() },
{ Game.T6, new[] { DetectionType.Offset, DetectionType.Snap, DetectionType.Strain } }
};
public IList<long> IgnoredClientIds { get; set; } = new List<long>(); public IList<long> IgnoredClientIds { get; set; } = new List<long>();
public IDictionary<Game, IDictionary<DetectionType, string[]>> IgnoredDetectionSpecification{ get; set; } = new Dictionary<Game, IDictionary<DetectionType, string[]>> public IDictionary<Game, IDictionary<DetectionType, string[]>> IgnoredDetectionSpecification{ get; set; } = new Dictionary<Game, IDictionary<DetectionType, string[]>>
{ {

View File

@ -633,7 +633,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return; return;
} }
var hit = new EFClientKill() EFClientKill hit;
try
{
hit = new EFClientKill
{ {
Active = true, Active = true,
AttackerId = attacker.ClientId, AttackerId = attacker.ClientId,
@ -660,6 +663,15 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
GameName = (int) attacker.CurrentServer.GameName GameName = (int) attacker.CurrentServer.GameName
}; };
}
catch (Exception ex)
{
_log.LogError(ex, "Could not parse script hit data. Damage={Damage}, TimeOffset={Offset}, TimeSinceLastAttack={LastAttackTime}",
damage, offset, lastAttackTime);
return;
}
hit.SetAdditionalProperty("HitLocationReference", hitLoc); hit.SetAdditionalProperty("HitLocationReference", hitLoc);
if (hit.HitLoc == (int) IW4Info.HitLocation.shield) if (hit.HitLoc == (int) IW4Info.HitLocation.shield)
@ -769,7 +781,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
} }
catch (Exception ex) catch (Exception ex)
{ {
_log.LogError(ex, "Could not save hit or anti-cheat info {@attacker} {@victim} {server}", attacker, _log.LogError(ex, "Could not save hit or anti-cheat info {Attacker} {Victim} {Server}", attacker,
victim, serverId); victim, serverId);
} }
@ -806,7 +818,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
private bool ShouldUseDetection(Server server, DetectionType detectionType, long clientId) private bool ShouldUseDetection(Server server, DetectionType detectionType, long clientId)
{ {
var detectionTypes = Plugin.Config.Configuration().AnticheatConfiguration.ServerDetectionTypes; var serverDetectionTypes = Plugin.Config.Configuration().AnticheatConfiguration.ServerDetectionTypes;
var gameDetectionTypes = Plugin.Config.Configuration().AnticheatConfiguration.GameDetectionTypes;
var ignoredClients = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredClientIds; var ignoredClients = Plugin.Config.Configuration().AnticheatConfiguration.IgnoredClientIds;
if (ignoredClients.Contains(clientId)) if (ignoredClients.Contains(clientId))
@ -814,10 +827,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return false; return false;
} }
try try
{ {
if (!detectionTypes[server.EndPoint].Contains(detectionType)) if (!serverDetectionTypes[server.EndPoint].Contains(detectionType))
{ {
return false; return false;
} }
@ -827,6 +839,18 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{ {
} }
try
{
if (!gameDetectionTypes[server.GameName].Contains(detectionType))
{
return false;
}
}
catch
{
// ignored
}
return true; return true;
} }

View File

@ -13,7 +13,7 @@ namespace SharedLibraryCore.Dtos
public int Offset { get; set; } public int Offset { get; set; }
/// <summary> /// <summary>
/// how many itesm to take /// how many items to take
/// </summary> /// </summary>
public int Count { get; set; } = 100; public int Count { get; set; } = 100;

View File

@ -56,6 +56,8 @@ namespace SharedLibraryCore.Interfaces
/// <returns></returns> /// <returns></returns>
Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default, CancellationToken token = default); Task<Dvar<T>> GetDvarAsync<T>(IRConConnection connection, string dvarName, T fallbackValue = default, CancellationToken token = default);
void BeginGetDvar(IRConConnection connection, string dvarName, AsyncCallback callback, CancellationToken token = default);
/// <summary> /// <summary>
/// set value of DVAR by name /// set value of DVAR by name
/// </summary> /// </summary>
@ -66,6 +68,8 @@ namespace SharedLibraryCore.Interfaces
/// <returns></returns> /// <returns></returns>
Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue, CancellationToken token = default); Task<bool> SetDvarAsync(IRConConnection connection, string dvarName, object dvarValue, CancellationToken token = default);
void BeginSetDvar(IRConConnection connection, string dvarName, object dvarValue, AsyncCallback callback, CancellationToken token = default);
/// <summary> /// <summary>
/// executes a console command on the server /// executes a console command on the server
/// </summary> /// </summary>

View File

@ -774,6 +774,11 @@ namespace SharedLibraryCore
return await server.RconParser.GetDvarAsync(server.RemoteConnection, dvarName, fallbackValue, token); return await server.RconParser.GetDvarAsync(server.RemoteConnection, dvarName, fallbackValue, token);
} }
public static void BeginGetDvar(this Server server, string dvarName, AsyncCallback callback, CancellationToken token = default)
{
server.RconParser.BeginGetDvar(server.RemoteConnection, dvarName, callback, token);
}
public static async Task<Dvar<T>> GetDvarAsync<T>(this Server server, string dvarName, public static async Task<Dvar<T>> GetDvarAsync<T>(this Server server, string dvarName,
T fallbackValue = default) T fallbackValue = default)
{ {
@ -809,6 +814,12 @@ namespace SharedLibraryCore
await server.RconParser.SetDvarAsync(server.RemoteConnection, dvarName, dvarValue, token); await server.RconParser.SetDvarAsync(server.RemoteConnection, dvarName, dvarValue, token);
} }
public static void BeginSetDvar(this Server server, string dvarName, object dvarValue,
AsyncCallback callback, CancellationToken token = default)
{
server.RconParser.BeginSetDvar(server.RemoteConnection, dvarName, dvarValue, callback, token);
}
public static async Task SetDvarAsync(this Server server, string dvarName, object dvarValue) public static async Task SetDvarAsync(this Server server, string dvarName, object dvarValue)
{ {
await SetDvarAsync(server, dvarName, dvarValue, default); await SetDvarAsync(server, dvarName, dvarValue, default);
@ -824,9 +835,17 @@ namespace SharedLibraryCore
return await ExecuteCommandAsync(server, commandName, default); return await ExecuteCommandAsync(server, commandName, default);
} }
public static Task<IStatusResponse> GetStatusAsync(this Server server, CancellationToken token) public static async Task<IStatusResponse> GetStatusAsync(this Server server, CancellationToken token)
{ {
return server.RconParser.GetStatusAsync(server.RemoteConnection, token); try
{
return await server.RconParser.GetStatusAsync(server.RemoteConnection, token);
}
catch (TaskCanceledException)
{
return null;
}
} }
/// <summary> /// <summary>

View File

@ -0,0 +1,25 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Interfaces;
using WebfrontCore.QueryHelpers.Models;
namespace WebfrontCore.Controllers.API;
[Route("api/[controller]")]
public class PenaltyController : BaseController
{
private readonly IResourceQueryHelper<BanInfoRequest, BanInfo> _banInfoQueryHelper;
public PenaltyController(IManager manager, IResourceQueryHelper<BanInfoRequest, BanInfo> banInfoQueryHelper) : base(manager)
{
_banInfoQueryHelper = banInfoQueryHelper;
}
[HttpGet("BanInfo/{clientName}")]
public async Task<IActionResult> BanInfo(BanInfoRequest request)
{
var result = await _banInfoQueryHelper.QueryResource(request);
return Json(result);
}
}

View File

@ -0,0 +1,96 @@
using System.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models.Client;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using WebfrontCore.QueryHelpers.Models;
namespace WebfrontCore.QueryHelpers;
public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, BanInfo>
{
private readonly IDatabaseContextFactory _contextFactory;
public BanInfoResourceQueryHelper(IDatabaseContextFactory contextFactory)
{
_contextFactory = contextFactory;
}
public async Task<ResourceQueryHelperResult<BanInfo>> QueryResource(BanInfoRequest query)
{
if (query.Count > 30)
{
query.Count = 30;
}
await using var context = _contextFactory.CreateContext(false);
var matchingClients = await context.Clients.Where(client =>
EF.Functions.ILike(client.CurrentAlias.SearchableName ?? client.CurrentAlias.Name, $"%{query.ClientName.Trim()}%"))
.Where(client => client.Level == EFClient.Permission.Banned)
.OrderByDescending(client => client.LastConnection)
.Skip(query.Offset)
.Take(query.Count)
.Select(client => new
{
client.CurrentAlias.Name,
client.NetworkId,
client.AliasLinkId,
client.ClientId
}).ToListAsync();
var usedIps = await context.Aliases
.Where(alias => matchingClients.Select(client => client.AliasLinkId).Contains(alias.LinkId))
.Where(alias => alias.IPAddress != null)
.Select(alias => new { alias.IPAddress, alias.LinkId })
.ToListAsync();
var usedIpsGrouped = usedIps
.GroupBy(alias => alias.LinkId)
.ToDictionary(key => key.Key, value => value.Select(alias => alias.IPAddress).Distinct());
var searchingNetworkIds = matchingClients.Select(client => client.NetworkId);
var searchingIps = usedIpsGrouped.SelectMany(group => group.Value);
var matchedPenalties = await context.PenaltyIdentifiers.Where(identifier =>
searchingNetworkIds.Contains(identifier.NetworkId) ||
searchingIps.Contains(identifier.IPv4Address))
.Select(penalty => new
{
penalty.CreatedDateTime,
PunisherName = penalty.Penalty.Punisher.CurrentAlias.Name,
Offense = string.IsNullOrEmpty(penalty.Penalty.AutomatedOffense) ? penalty.Penalty.Offense : "Anticheat Detection",
LinkId = penalty.Penalty.Offender.AliasLinkId,
penalty.Penalty.PunisherId
})
.ToListAsync();
var groupedPenalties = matchedPenalties.GroupBy(penalty => penalty.LinkId)
.ToDictionary(key => key.Key, value => value.FirstOrDefault());
var results = matchingClients.Select(client =>
{
var matchedPenalty =
groupedPenalties.ContainsKey(client.AliasLinkId) ? groupedPenalties[client.AliasLinkId] : null;
return new BanInfo
{
DateTime = matchedPenalty?.CreatedDateTime,
OffenderName = client.Name.StripColors(),
OffenderId = client.ClientId,
PunisherName = matchedPenalty?.PunisherName.StripColors(),
PunisherId = matchedPenalty?.PunisherId,
Offense = matchedPenalty?.Offense
};
}).ToList();
return new ResourceQueryHelperResult<BanInfo>
{
RetrievedResultCount = results.Count,
TotalResultCount = results.Count,
Results = results
};
}
}

View File

@ -0,0 +1,12 @@
using System;
public class BanInfo
{
public string OffenderName { get; set; }
public int OffenderId { get; set; }
public string PunisherName { get; set; }
public int? PunisherId { get; set; }
public string Offense { get; set; }
public DateTime? DateTime { get; set; }
public long? TimeStamp => DateTime.HasValue ? new DateTimeOffset(DateTime.Value, TimeSpan.Zero).ToUnixTimeSeconds() : null;
}

View File

@ -0,0 +1,8 @@
using SharedLibraryCore.Dtos;
namespace WebfrontCore.QueryHelpers.Models;
public class BanInfoRequest : PaginationRequest
{
public string ClientName { get; set; }
}

View File

@ -24,11 +24,12 @@ using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Abstractions; using Data.Abstractions;
using Data.Helpers; using Data.Helpers;
using IW4MAdmin.Plugins.Stats.Config;
using Stats.Client.Abstractions; using Stats.Client.Abstractions;
using Stats.Config; using Stats.Config;
using WebfrontCore.Controllers.API.Validation; using WebfrontCore.Controllers.API.Validation;
using WebfrontCore.Middleware; using WebfrontCore.Middleware;
using WebfrontCore.QueryHelpers;
using WebfrontCore.QueryHelpers.Models;
namespace WebfrontCore namespace WebfrontCore
{ {
@ -127,6 +128,7 @@ namespace WebfrontCore
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IMetaServiceV2>()); services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IMetaServiceV2>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<ApplicationConfiguration>()); services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<ApplicationConfiguration>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<ClientService>()); services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<ClientService>());
services.AddSingleton<IResourceQueryHelper<BanInfoRequest, BanInfo>, BanInfoResourceQueryHelper>();
services.AddSingleton( services.AddSingleton(
Program.ApplicationServiceProvider.GetRequiredService<IServerDistributionCalculator>()); Program.ApplicationServiceProvider.GetRequiredService<IServerDistributionCalculator>());
services.AddSingleton(Program.ApplicationServiceProvider services.AddSingleton(Program.ApplicationServiceProvider

View File

@ -19,7 +19,7 @@
<partial name="_ListAuditLog"/> <partial name="_ListAuditLog"/>
</tbody> </tbody>
</table> </table>
<i id="loaderLoad" class="loader-load-more oi oi-chevron-bottom text-center text-primary d-none d-lg-block mt-10"></i> <i id="loaderLoad" class="loader-load-more oi oi-chevron-bottom text-center w-full text-primary mt-10"></i>
</div> </div>
@section scripts { @section scripts {

View File

@ -5,7 +5,7 @@
<div class="col-12 col-lg-9 col-xl-10 mt-0"> <div class="col-12 col-lg-9 col-xl-10 mt-0">
<h2 class="content-title mb-0">Top Players</h2> <h2 class="content-title mb-0">Top Players</h2>
<span class="text-muted"> <span class="text-muted">
<color-code value="@(ViewBag.SelectedServerName ?? ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"])"></color-code> <color-code value="@(Model.FirstOrDefault(m => m.Endpoint == ViewBag.SelectedServerId)?.Name ?? ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"])"></color-code>
</span> </span>
<div id="topPlayersContainer"> <div id="topPlayersContainer">

View File

@ -22,10 +22,10 @@
} }
} }
<div class="pt-15 pl-15 pr-15 d-flex flex-wrap flex-column flex-md-row justify-content-between w-full w-auto-lg"> <div class="pt-15 pl-15 pr-15 d-flex flex-wrap flex-column flex-md-row w-full w-auto-lg">
@if (groupedClients.Count > 0) @if (groupedClients.Count > 0)
{ {
<div class="flex-fill flex-lg-grow-0 w-full w-md-half pr-md-10 pb-md-10"> <div class="flex-fill flex-lg-grow-0 w-full w-md-half">
@foreach (var chat in Model.ChatHistory) @foreach (var chat in Model.ChatHistory)
{ {
var message = chat.IsHidden && !ViewBag.Authorized ? chat.HiddenMessage : chat.Message; var message = chat.IsHidden && !ViewBag.Authorized ? chat.HiddenMessage : chat.Message;
@ -33,7 +33,6 @@
<div class="text-truncate"> <div class="text-truncate">
<i class="oi @stateIcon"></i> <i class="oi @stateIcon"></i>
<span> <span>
<color-code value="@chat.Name"></color-code> <color-code value="@chat.Name"></color-code>
</span> </span>
@ -47,27 +46,25 @@
</span> </span>
} }
</div> </div>
} }
<hr class="d-block d-md-none"/> <hr class="d-block d-md-none"/>
</div> </div>
} }
<div class="d-flex flex-row w-full w-md-half pl-md-10 pb-md-10"> <div class="d-flex flex-row w-full w-md-half">
@foreach (var clientIndex in groupedClients) @foreach (var clientIndex in groupedClients)
{ {
<div class="@(clientIndex.index == 1 ? "pl-md-10" : "pr-md-10") w-half w-xl-full"> <div class="w-half">
@foreach (var client in clientIndex.group) @foreach (var client in clientIndex.group)
{ {
var levelColorClass = !ViewBag.Authorized || client.client.LevelInt == 0 ? "text-light-dm text-dark-lm" : $"level-color-{client.client.LevelInt}"; var levelColorClass = !ViewBag.Authorized || client.client.LevelInt == 0 ? "text-light-dm text-dark-lm" : $"level-color-{client.client.LevelInt}";
<div class="d-flex @(clientIndex.index == 1 ? "justify-content-start ml-auto flex-row-reverse" : "ml-auto") w-full w-xl-200"> <div class="d-flex @(clientIndex.index == 1 ? "justify-content-start flex-row-reverse" : "")">
<has-permission entity="AdminMenu" required-permission="Update"> <has-permission entity="AdminMenu" required-permission="Update">
<a href="#actionModal" class="profile-action" data-action="kick" data-action-id="@client.client.ClientId" aria-hidden="true"> <a href="#actionModal" class="profile-action" data-action="kick" data-action-id="@client.client.ClientId" aria-hidden="true">
<i class="oi oi-circle-x font-size-12 @levelColorClass @(clientIndex.index == 1 ? "ml-5" : "mr-5")"></i> <i class="oi oi-circle-x font-size-12 @levelColorClass"></i>
</a> </a>
</has-permission> </has-permission>
<a asp-controller="Client" asp-action="Profile" asp-route-id="@client.client.ClientId" class="@levelColorClass no-decoration text-truncate"> <a asp-controller="Client" asp-action="Profile" asp-route-id="@client.client.ClientId" class="@levelColorClass no-decoration text-truncate ml-5 mr-5">
<color-code value="@client.client.Name"></color-code> <color-code value="@client.client.Name"></color-code>
</a> </a>
</div> </div>

View File

@ -56,7 +56,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Folder Include="ref" />
<Folder Include="wwwroot\lib\canvas.js\" /> <Folder Include="wwwroot\lib\canvas.js\" />
</ItemGroup> </ItemGroup>