Compare commits

...

58 Commits

Author SHA1 Message Date
444c06e65e make sure color tokens are mapped for kick messages 2022-07-23 13:48:46 -05:00
561909158f improve penalty display on mobile view 2022-07-23 11:22:16 -05:00
cd12c3f26e set default permission for read message to user 2022-07-23 11:13:21 -05:00
c817f9a810 improve audit log display on mobile 2022-07-23 11:09:23 -05:00
b27ae1517e fix issue with duplicate key on top stats page 2022-07-22 10:28:26 -05:00
507688a175 small tweaks for notes/tags 2022-07-20 11:39:46 -05:00
d2cfd50e39 update webfront permission types 2022-07-20 10:34:33 -05:00
51e8b31e42 add client note command and feature 2022-07-20 10:32:26 -05:00
fa1567d3f5 add set client tag to webfront profile as button 2022-07-19 20:37:48 -05:00
f97e266c24 send correct type to inc/dec meta service in game interface 2022-07-16 17:47:07 -05:00
506b17dbb3 Merge branch 'release/pre' of https://github.com/RaidMax/IW4M-Admin into release/pre 2022-07-16 09:56:48 -05:00
bef8c08d90 misc performance graph display tweaks 2022-07-16 09:56:41 -05:00
b78c467539 tweaks and persistent guid update to game integration/interface 2022-07-16 09:32:07 -05:00
Edo
c3e042521a Improvements to game scripts (#253) 2022-07-16 08:40:10 -05:00
cb5f490d3b fix incorrect js bundle input source 2022-07-13 16:27:47 -05:00
0a55c54c42 update to game interface/integration for persistent stat data 2022-07-13 16:10:16 -05:00
f43f7b5040 misc webfront tweaks 2022-07-10 21:06:58 -05:00
540cf7489d update pluto t6 parser for unknown ip 2022-07-10 20:09:57 -05:00
1a72faee60 add date stamp to performance graphs / increase number of performance rating snapshots / localize graph timestamps 2022-07-10 17:06:46 -05:00
4e44bb5ea1 fix rcon issue on restart 2022-07-09 20:57:00 -05:00
9e17bcc38f improve ban management display and additional translations 2022-07-09 16:32:23 -05:00
4b33b33d01 fix issue with alert on warn in game interface 2022-07-09 14:23:08 -05:00
6f1bc7ab90 cleanup table display of admins on mobile display 2022-07-09 13:54:35 -05:00
63e1774cb6 gracefully handle when infoString does not include all expected data 2022-07-09 10:52:27 -05:00
61df873bb1 more localization tweaks 2022-07-08 20:40:27 -05:00
052eeb0615 fix tag on welcome issue 2022-07-08 20:39:58 -05:00
88e67747fe add option to normalize diacritics for rcon parsers (applied to T6) 2022-07-06 15:42:31 -05:00
5db94723aa Merge branch 'release/pre' of https://github.com/RaidMax/IW4M-Admin into release/pre 2022-07-06 10:02:09 -05:00
ea8216ecdf Add H1 maps and gametypes (#252) 2022-07-06 10:01:01 -05:00
6abbcbe464 prevent waiting for response on quit command 2022-07-06 09:55:06 -05:00
57484690b6 clean up display and uniformity of social icons 2022-07-06 09:49:44 -05:00
7a022a1973 fix grouping of commands on help page 2022-07-05 15:57:39 -05:00
7108e23a03 fix issue with context menu close not working on mobile 2022-07-05 15:15:25 -05:00
77d25890da clean up some more translations 2022-07-05 12:42:17 -05:00
2fca68a7ea update webfront translation strings 2022-07-05 12:02:43 -05:00
a6c0a94f6c support per-command override of rcon timeouts / update t5 parser to reflect 2022-07-01 09:59:11 -05:00
71abaac9e1 remove reports on ban/tempban 2022-07-01 09:14:57 -05:00
e07651b931 fix toast message issue on pages with query params 2022-06-28 10:03:05 -05:00
5a2ee36df9 use "unknown" ip as bot indicator 2022-06-28 09:15:37 -05:00
2daa4991d1 fix issue with previous change 2022-06-21 16:57:06 -05:00
775c0a91b5 small parser changes 2022-06-21 16:33:11 -05:00
55bccc7d3d ensure commands are not displayed/usable for unsupported games 2022-06-17 13:11:44 -05:00
4322e8d882 add migration logic for MySQL case sensitivity 2022-06-17 09:44:14 -05:00
a92f9fc29c optimize client searching 2022-06-16 18:44:49 -05:00
fbf424c77d optimize chat filtering/searching 2022-06-16 18:03:23 -05:00
b8e001fcfe misc ui tweaks 2022-06-16 14:02:44 -05:00
5ab5b73ecf order report servers by most recent report 2022-06-16 10:11:01 -05:00
4534d24fe6 fix token auth issue 2022-06-16 10:07:03 -05:00
73c8d0da33 improve icon alignment for nav menu 2022-06-16 09:46:01 -05:00
16d75470b5 fix login persistence issue 2022-06-15 21:00:01 -05:00
f02552faa1 fix up query/check 2022-06-15 20:19:22 -05:00
a4923d03f9 hide token generation button for non-logged-in users 2022-06-15 19:39:53 -05:00
8ae6561f4e update schema to support unique guid + game combinations 2022-06-15 19:37:34 -05:00
deeb1dea87 set the rcon parser game name for retail WaW 2022-06-14 15:12:19 -05:00
9ab34614c5 don't publish disconnect event if no client id 2022-06-14 15:00:23 -05:00
2cff25d6b3 make alert menu scrollable for large # of alerts 2022-06-13 11:03:39 -05:00
df3e226dc9 actually fix the previous issue 2022-06-12 16:37:07 -05:00
ef3db63ba7 fix issue that shouldn't actually be an issue 2022-06-12 15:09:26 -05:00
135 changed files with 12199 additions and 989 deletions

View File

@ -61,7 +61,7 @@ namespace IW4MAdmin.Application
public IConfigurationHandler<ApplicationConfiguration> ConfigHandler;
readonly IPageList PageList;
private readonly TimeSpan _throttleTimeout = new TimeSpan(0, 1, 0);
private readonly CancellationTokenSource _tokenSource;
private CancellationTokenSource _tokenSource;
private readonly Dictionary<string, Task<IList>> _operationLookup = new Dictionary<string, Task<IList>>();
private readonly ITranslationLookup _translationLookup;
private readonly IConfigurationHandler<CommandConfiguration> _commandConfiguration;
@ -94,8 +94,8 @@ namespace IW4MAdmin.Application
ConfigHandler = appConfigHandler;
StartTime = DateTime.UtcNow;
PageList = new PageList();
AdditionalEventParsers = new List<IEventParser>() { new BaseEventParser(parserRegexFactory, logger, _appConfig) };
AdditionalRConParsers = new List<IRConParser>() { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) };
AdditionalEventParsers = new List<IEventParser> { new BaseEventParser(parserRegexFactory, logger, _appConfig) };
AdditionalRConParsers = new List<IRConParser> { new BaseRConParser(serviceProvider.GetRequiredService<ILogger<BaseRConParser>>(), parserRegexFactory) };
TokenAuthenticator = new TokenAuthentication();
_logger = logger;
_tokenSource = new CancellationTokenSource();
@ -613,6 +613,7 @@ namespace IW4MAdmin.Application
{
IsRestartRequested = true;
Stop().GetAwaiter().GetResult();
_tokenSource = new CancellationTokenSource();
}
[Obsolete]
@ -632,9 +633,9 @@ namespace IW4MAdmin.Application
return _servers.SelectMany(s => s.Clients).ToList().Where(p => p != null).ToList();
}
public EFClient FindActiveClient(EFClient client) =>client.ClientNumber < 0 ?
public EFClient FindActiveClient(EFClient client) => client.ClientNumber < 0 ?
GetActiveClients()
.FirstOrDefault(c => c.NetworkId == client.NetworkId) ?? client :
.FirstOrDefault(c => c.NetworkId == client.NetworkId && c.GameName == client.GameName) ?? client :
client;
public ClientService GetClientService()

View File

@ -0,0 +1,52 @@
using System;
using System.Threading.Tasks;
using Data.Models.Client;
using IW4MAdmin.Application.Meta;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Commands;
public class AddClientNoteCommand : Command
{
private readonly IMetaServiceV2 _metaService;
public AddClientNoteCommand(CommandConfiguration config, ITranslationLookup layout, IMetaServiceV2 metaService) : base(config, layout)
{
Name = "addnote";
Description = _translationLookup["COMMANDS_ADD_CLIENT_NOTE_DESCRIPTION"];
Alias = "an";
Permission = EFClient.Permission.Moderator;
RequiresTarget = true;
Arguments = new[]
{
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_PLAYER"],
Required = true
},
new CommandArgument
{
Name = _translationLookup["COMMANDS_ARGS_NOTE"],
Required = false
}
};
_metaService = metaService;
}
public override async Task ExecuteAsync(GameEvent gameEvent)
{
var note = new ClientNoteMetaResponse
{
Note = gameEvent.Data?.Trim(),
OriginEntityId = gameEvent.Origin.ClientId,
ModifiedDate = DateTime.UtcNow
};
await _metaService.SetPersistentMetaValue("ClientNotes", note, gameEvent.Target.ClientId);
gameEvent.Origin.Tell(_translationLookup["COMMANDS_ADD_CLIENT_NOTE_SUCCESS"]);
}
}

View File

@ -24,7 +24,7 @@ namespace IW4MAdmin.Application.Commands
Name = "readmessage";
Description = _translationLookup["COMMANDS_READ_MESSAGE_DESC"];
Alias = "rm";
Permission = EFClient.Permission.Flagged;
Permission = EFClient.Permission.User;
_contextFactory = contextFactory;
_logger = logger;
@ -76,4 +76,4 @@ namespace IW4MAdmin.Application.Commands
}
}
}
}
}

View File

@ -564,7 +564,56 @@
"Alias": "Momentum"
}
]
}
},
{
"Game": "H1",
"Gametypes": [
{
"Name": "conf",
"Alias": "Kill Confirmed"
},
{
"Name": "ctf",
"Alias": "Capture The Flag"
},
{
"Name": "dd",
"Alias": "Demolition"
},
{
"Name": "dm",
"Alias": "Free For All"
},
{
"Name": "dom",
"Alias": "Domination"
},
{
"Name": "gun",
"Alias": "Gun Game"
},
{
"Name": "hp",
"Alias": "Hardpoint"
},
{
"Name": "koth",
"Alias": "Headquarters"
},
{
"Name": "sab",
"Alias": "Sabotage"
},
{
"Name": "sd",
"Alias": "Search & Destroy"
},
{
"Name": "war",
"Alias": "Team Deathmatch"
}
]
}
],
"Maps": [
{
@ -1768,6 +1817,103 @@
}
]
},
{
"Game": "H1",
"Maps": [
{
"Alias": "Ambush",
"Name": "mp_convoy"
},
{
"Alias": "Backlot",
"Name": "mp_backlot"
},
{
"Alias": "Bloc",
"Name": "mp_bloc"
},
{
"Alias": "Bog",
"Name": "mp_bog"
},
{
"Alias": "Countdown",
"Name": "mp_countdown"
},
{
"Alias": "Crash",
"Name": "mp_crash"
},
{
"Alias": "Crossfire",
"Name": "mp_crossfire"
},
{
"Alias": "District",
"Name": "mp_citystreets"
},
{
"Alias": "Downpour",
"Name": "mp_farm"
},
{
"Alias": "Overgrown",
"Name": "mp_overgrown"
},
{
"Alias": "Pipeline",
"Name": "mp_pipeline"
},
{
"Alias": "Shipment",
"Name": "mp_shipment"
},
{
"Alias": "Showdown",
"Name": "mp_showdown"
},
{
"Alias": "Strike",
"Name": "mp_strike"
},
{
"Alias": "Vacant",
"Name": "mp_vacant"
},
{
"Alias": "Wet Work",
"Name": "mp_cargoship"
},
{
"Alias": "Winter Crash",
"Name": "mp_crash_snow"
},
{
"Alias": "Broadcast",
"Name": "mp_broadcast"
},
{
"Alias": "Creek",
"Name": "mp_creek"
},
{
"Alias": "Chinatown",
"Name": "mp_carentan"
},
{
"Alias": "Killhouse",
"Name": "mp_killhouse"
},
{
"Alias": "Day Break",
"Name": "mp_farm_spring"
},
{
"Alias": "Beach Bog",
"Name": "mp_bog_summer"
}
]
},
{
"Game": "CSGO",
"Maps": [

View File

@ -9,6 +9,7 @@ using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
@ -75,7 +76,7 @@ namespace IW4MAdmin
{
ServerLogger.LogDebug("Client slot #{clientNumber} now reserved", clientFromLog.ClientNumber);
EFClient client = await Manager.GetClientService().GetUnique(clientFromLog.NetworkId);
var client = await Manager.GetClientService().GetUnique(clientFromLog.NetworkId, GameName);
// first time client is connecting to server
if (client == null)
@ -118,7 +119,7 @@ namespace IW4MAdmin
public override async Task OnClientDisconnected(EFClient client)
{
if (!GetClientsAsList().Any(_client => _client.NetworkId == client.NetworkId))
if (GetClientsAsList().All(eachClient => eachClient.NetworkId != client.NetworkId))
{
using (LogContext.PushProperty("Server", ToString()))
{
@ -154,10 +155,10 @@ namespace IW4MAdmin
{
if (E.IsBlocking)
{
await E.Origin?.Lock();
await E.Origin.Lock();
}
bool canExecuteCommand = true;
var canExecuteCommand = true;
try
{
@ -166,30 +167,30 @@ namespace IW4MAdmin
return;
}
Command C = null;
Command command = null;
if (E.Type == GameEvent.EventType.Command)
{
try
{
C = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration(), _commandConfiguration);
command = await SharedLibraryCore.Commands.CommandProcessing.ValidateCommand(E, Manager.GetApplicationSettings().Configuration(), _commandConfiguration);
}
catch (CommandException e)
{
ServerLogger.LogWarning(e, "Error validating command from event {@event}",
ServerLogger.LogWarning(e, "Error validating command from event {@Event}",
new { E.Type, E.Data, E.Message, E.Subtype, E.IsRemote, E.CorrelationId });
E.FailReason = GameEvent.EventFailReason.Invalid;
}
if (C != null)
if (command != null)
{
E.Extra = C;
E.Extra = command;
}
}
try
{
var loginPlugin = Manager.Plugins.FirstOrDefault(_plugin => _plugin.Name == "Login");
var loginPlugin = Manager.Plugins.FirstOrDefault(plugin => plugin.Name == "Login");
if (loginPlugin != null)
{
@ -204,15 +205,15 @@ namespace IW4MAdmin
}
// hack: this prevents commands from getting executing that 'shouldn't' be
if (E.Type == GameEvent.EventType.Command && E.Extra is Command command &&
if (E.Type == GameEvent.EventType.Command && E.Extra is Command cmd &&
(canExecuteCommand || E.Origin?.Level == Permission.Console))
{
ServerLogger.LogInformation("Executing command {comamnd} for {client}", command.Name, E.Origin.ToString());
await command.ExecuteAsync(E);
ServerLogger.LogInformation("Executing command {Command} for {Client}", cmd.Name, E.Origin.ToString());
await cmd.ExecuteAsync(E);
}
var pluginTasks = Manager.Plugins
.Where(_plugin => _plugin.Name != "Login")
.Where(plugin => plugin.Name != "Login")
.Select(async plugin => await CreatePluginTask(plugin, E));
await Task.WhenAll(pluginTasks);
@ -373,9 +374,9 @@ namespace IW4MAdmin
var clientTag = await _metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2,
EFMeta.ClientTagNameV2, E.Origin.ClientId, Manager.CancellationToken);
if (clientTag?.LinkedMeta != null)
if (clientTag?.Value != null)
{
E.Origin.Tag = clientTag.LinkedMeta.Value;
E.Origin.Tag = clientTag.Value;
}
try
@ -449,7 +450,7 @@ namespace IW4MAdmin
Clients[E.Origin.ClientNumber] = E.Origin;
try
{
E.Origin.GameName = (Reference.Game?)GameName;
E.Origin.GameName = (Reference.Game)GameName;
E.Origin = await OnClientConnected(E.Origin);
E.Target = E.Origin;
}
@ -517,7 +518,7 @@ namespace IW4MAdmin
E.Target.SetLevel(Permission.User, E.Origin);
await Manager.GetPenaltyService().RemoveActivePenalties(E.Target.AliasLinkId, E.Target.NetworkId,
E.Target.CurrentAlias?.IPAddress);
E.Target.GameName, E.Target.CurrentAlias?.IPAddress);
await Manager.GetPenaltyService().Create(unflagPenalty);
}
@ -689,23 +690,50 @@ namespace IW4MAdmin
else
{
Gametype = dict["gametype"];
Hostname = dict["hostname"];
if (dict.ContainsKey("gametype"))
{
Gametype = dict["gametype"];
}
string mapname = dict["mapname"] ?? CurrentMap.Name;
UpdateMap(mapname);
if (dict.ContainsKey("hostname"))
{
Hostname = dict["hostname"];
}
var newMapName = dict.ContainsKey("mapname")
? dict["mapname"] ?? CurrentMap.Name
: CurrentMap.Name;
UpdateMap(newMapName);
}
}
else
{
var dict = (Dictionary<string, string>) E.Extra;
Gametype = dict["g_gametype"];
Hostname = dict["sv_hostname"];
MaxClients = int.Parse(dict["sv_maxclients"]);
var dict = (Dictionary<string, string>)E.Extra;
if (dict.ContainsKey("g_gametype"))
{
Gametype = dict["g_gametype"];
}
string mapname = dict["mapname"];
UpdateMap(mapname);
if (dict.ContainsKey("sv_hostname"))
{
Hostname = dict["sv_hostname"];
}
if (dict.ContainsKey("sv_maxclients"))
{
MaxClients = int.Parse(dict["sv_maxclients"]);
}
else if (dict.ContainsKey("com_maxclients"))
{
MaxClients = int.Parse(dict["com_maxclients"]);
}
if (dict.ContainsKey("mapname"))
{
UpdateMap(dict["mapname"]);
}
}
if (E.GameTime.HasValue)
@ -741,6 +769,34 @@ namespace IW4MAdmin
{
E.Origin.UpdateTeam(E.Extra as string);
}
else if (E.Type == GameEvent.EventType.MetaUpdated)
{
if (E.Extra is "PersistentClientGuid")
{
var parts = E.Data.Split(",");
if (parts.Length == 2 && int.TryParse(parts[0], out var high) &&
int.TryParse(parts[1], out var low))
{
var guid = long.Parse(high.ToString("X") + low.ToString("X"), NumberStyles.HexNumber);
var penalties = await Manager.GetPenaltyService()
.GetActivePenaltiesByIdentifier(null, guid, (Reference.Game)GameName);
var banPenalty =
penalties.FirstOrDefault(penalty => penalty.Type == EFPenalty.PenaltyType.Ban);
if (banPenalty is not null && E.Origin.Level != Permission.Banned)
{
ServerLogger.LogInformation(
"Banning {Client} as they have have provided a persistent clientId of {PersistentClientId}, which is banned",
E.Origin.ToString(), guid);
E.Origin.Ban(loc["SERVER_BAN_EVADE"].FormatExt(guid),
Utilities.IW4MAdminClient(this), true);
}
}
}
}
lock (ChatHistory)
{
@ -763,7 +819,7 @@ namespace IW4MAdmin
private async Task OnClientUpdate(EFClient origin)
{
var client = Manager.GetActiveClients().FirstOrDefault(c => c.NetworkId == origin.NetworkId);
var client = GetClientsAsList().FirstOrDefault(c => c.NetworkId == origin.NetworkId);
if (client == null)
{
@ -980,7 +1036,7 @@ namespace IW4MAdmin
!string.IsNullOrEmpty(client.Name) && (client.Ping != 999 || client.IsBot)))
{
client.CurrentServer = this;
client.GameName = (Reference.Game?)GameName;
client.GameName = (Reference.Game)GameName;
var e = new GameEvent
{
@ -1245,28 +1301,17 @@ namespace IW4MAdmin
this.GamePassword = gamePassword.Value;
UpdateMap(mapname);
if (RconParser.CanGenerateLogPath)
if (RconParser.CanGenerateLogPath && string.IsNullOrEmpty(ServerConfig.ManualLogPath))
{
bool needsRestart = false;
if (logsync.Value == 0)
{
await this.SetDvarAsync("g_logsync", 2, Manager.CancellationToken); // set to 2 for continous in other games, clamps to 1 for IW4
needsRestart = true;
}
if (string.IsNullOrWhiteSpace(logfile.Value))
{
logfile.Value = "games_mp.log";
await this.SetDvarAsync("g_log", logfile.Value, Manager.CancellationToken);
needsRestart = true;
}
if (needsRestart)
{
// disabling this for the time being
/*Logger.WriteWarning("Game log file not properly initialized, restarting map...");
await this.ExecuteCommandAsync("map_restart");*/
}
// this DVAR isn't set until the a map is loaded
@ -1472,6 +1517,11 @@ namespace IW4MAdmin
ServerLogger.LogDebug("Creating tempban penalty for {TargetClient}", targetClient.ToString());
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
foreach (var reports in Manager.GetServers().Select(server => server.Reports))
{
reports.RemoveAll(report => report.Target.ClientId == targetClient.ClientId);
}
if (activeClient.IsIngame)
{
@ -1502,6 +1552,11 @@ namespace IW4MAdmin
activeClient.SetLevel(Permission.Banned, originClient);
await newPenalty.TryCreatePenalty(Manager.GetPenaltyService(), ServerLogger);
foreach (var reports in Manager.GetServers().Select(server => server.Reports))
{
reports.RemoveAll(report => report.Target.ClientId == targetClient.ClientId);
}
if (activeClient.IsIngame)
{
ServerLogger.LogDebug("Attempting to kicking newly banned client {ActiveClient}", activeClient.ToString());
@ -1530,7 +1585,7 @@ namespace IW4MAdmin
ServerLogger.LogDebug("Creating unban penalty for {targetClient}", targetClient.ToString());
targetClient.SetLevel(Permission.User, originClient);
await Manager.GetPenaltyService().RemoveActivePenalties(targetClient.AliasLink.AliasLinkId,
targetClient.NetworkId, targetClient.CurrentAlias?.IPAddress);
targetClient.NetworkId, targetClient.GameName, targetClient.CurrentAlias?.IPAddress);
await Manager.GetPenaltyService().Create(unbanPenalty);
}

View File

@ -33,7 +33,7 @@ namespace IW4MAdmin.Application.Misc
builder.Append(header);
builder.Append(config.NoticeLineSeparator);
// build the reason
var reason = _transLookup["GAME_MESSAGE_PENALTY_REASON"].FormatExt(penalty.Offense);
var reason = _transLookup["GAME_MESSAGE_PENALTY_REASON"].FormatExt(penalty.Offense.FormatMessageForEngine(config));
if (isNewLineSeparator)
{
@ -117,4 +117,4 @@ namespace IW4MAdmin.Application.Misc
return segments;
}
}
}
}

View File

@ -10,6 +10,7 @@ namespace IW4MAdmin.Application.Misc
{
public event EventHandler<GameEvent> OnClientDisconnect;
public event EventHandler<GameEvent> OnClientConnect;
public event EventHandler<GameEvent> OnClientMetaUpdated;
private readonly ILogger _logger;
@ -29,10 +30,15 @@ namespace IW4MAdmin.Application.Misc
OnClientConnect?.Invoke(this, gameEvent);
}
if (gameEvent.Type == GameEvent.EventType.Disconnect)
if (gameEvent.Type == GameEvent.EventType.Disconnect && gameEvent.Origin.ClientId != 0)
{
OnClientDisconnect?.Invoke(this, gameEvent);
}
if (gameEvent.Type == GameEvent.EventType.MetaUpdated)
{
OnClientMetaUpdated?.Invoke(this, gameEvent);
}
}
catch (Exception ex)
@ -41,4 +47,4 @@ namespace IW4MAdmin.Application.Misc
}
}
}
}
}

View File

@ -7,7 +7,10 @@ using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using SharedLibraryCore.QueryHelper;
@ -19,13 +22,15 @@ public class MetaServiceV2 : IMetaServiceV2
{
private readonly IDictionary<MetaType, List<dynamic>> _metaActions;
private readonly IDatabaseContextFactory _contextFactory;
private readonly IServiceProvider _serviceProvider;
private readonly ILogger _logger;
public MetaServiceV2(ILogger<MetaServiceV2> logger, IDatabaseContextFactory contextFactory)
public MetaServiceV2(ILogger<MetaServiceV2> logger, IDatabaseContextFactory contextFactory, IServiceProvider serviceProvider)
{
_logger = logger;
_metaActions = new Dictionary<MetaType, List<dynamic>>();
_contextFactory = contextFactory;
_serviceProvider = serviceProvider;
}
public async Task SetPersistentMeta(string metaKey, string metaValue, int clientId,
@ -64,6 +69,26 @@ public class MetaServiceV2 : IMetaServiceV2
}
await context.SaveChangesAsync(token);
var manager = _serviceProvider.GetRequiredService<IManager>();
var matchingClient = manager.GetActiveClients().FirstOrDefault(client => client.ClientId == clientId);
var server = matchingClient?.CurrentServer ?? manager.GetServers().FirstOrDefault();
if (server is not null)
{
manager.AddEvent(new GameEvent
{
Type = GameEvent.EventType.MetaUpdated,
Origin = matchingClient ?? new EFClient
{
ClientId = clientId
},
Data = metaValue,
Extra = metaKey,
Owner = server
});
}
}
public async Task SetPersistentMetaValue<T>(string metaKey, T metaValue, int clientId,

View File

@ -116,7 +116,8 @@ namespace IW4MAdmin.Application.Misc
typeof(System.Net.Http.HttpClient).Assembly,
typeof(EFClient).Assembly,
typeof(Utilities).Assembly,
typeof(Encoding).Assembly
typeof(Encoding).Assembly,
typeof(CancellationTokenSource).Assembly
})
.CatchClrExceptions()
.AddObjectConverter(new PermissionLevelToStringConverter()));

View File

@ -9,40 +9,41 @@ namespace IW4MAdmin.Application.Misc
{
internal class TokenAuthentication : ITokenAuthentication
{
private readonly ConcurrentDictionary<long, TokenState> _tokens;
private readonly ConcurrentDictionary<int, TokenState> _tokens;
private readonly RandomNumberGenerator _random;
private static readonly TimeSpan TimeoutPeriod = new TimeSpan(0, 0, 120);
private static readonly TimeSpan TimeoutPeriod = new(0, 0, 120);
private const short TokenLength = 4;
public TokenAuthentication()
{
_tokens = new ConcurrentDictionary<long, TokenState>();
_tokens = new ConcurrentDictionary<int, TokenState>();
_random = RandomNumberGenerator.Create();
}
public bool AuthorizeToken(long networkId, string token)
public bool AuthorizeToken(ITokenIdentifier authInfo)
{
var authorizeSuccessful = _tokens.ContainsKey(networkId) && _tokens[networkId].Token == token;
var authorizeSuccessful = _tokens.ContainsKey(authInfo.ClientId) &&
_tokens[authInfo.ClientId].Token == authInfo.Token;
if (authorizeSuccessful)
{
_tokens.TryRemove(networkId, out _);
_tokens.TryRemove(authInfo.ClientId, out _);
}
return authorizeSuccessful;
}
public TokenState GenerateNextToken(long networkId)
public TokenState GenerateNextToken(ITokenIdentifier authInfo)
{
TokenState state;
if (_tokens.ContainsKey(networkId))
if (_tokens.ContainsKey(authInfo.ClientId))
{
state = _tokens[networkId];
state = _tokens[authInfo.ClientId];
if ((DateTime.Now - state.RequestTime) > TimeoutPeriod)
if (DateTime.Now - state.RequestTime > TimeoutPeriod)
{
_tokens.TryRemove(networkId, out _);
_tokens.TryRemove(authInfo.ClientId, out _);
}
else
@ -53,17 +54,16 @@ namespace IW4MAdmin.Application.Misc
state = new TokenState
{
NetworkId = networkId,
Token = _generateToken(),
TokenDuration = TimeoutPeriod
};
_tokens.TryAdd(networkId, state);
_tokens.TryAdd(authInfo.ClientId, state);
// perform some housekeeping so we don't have built up tokens if they're not ever used
foreach (var (key, value) in _tokens)
{
if ((DateTime.Now - value.RequestTime) > TimeoutPeriod)
if (DateTime.Now - value.RequestTime > TimeoutPeriod)
{
_tokens.TryRemove(key, out _);
}

View File

@ -53,7 +53,7 @@ namespace IW4MAdmin.Application.RConParsers
Configuration.Status.AddMapping(ParserRegex.GroupType.RConName, 5);
Configuration.Status.AddMapping(ParserRegex.GroupType.RConIpAddress, 7);
Configuration.Dvar.Pattern = "^\"(.+)\" is: \"(.+)?\" default: \"(.+)?\"\n(?:latched: \"(.+)?\"\n)? *(.+)$";
Configuration.Dvar.Pattern = "^\"(.+)\" is: \"(.+)?\" default: \"(.+)?\"\n?(?:latched: \"(.+)?\"\n?)? *(.+)$";
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarName, 1);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarValue, 2);
Configuration.Dvar.AddMapping(ParserRegex.GroupType.RConDvarDefaultValue, 3);
@ -82,7 +82,7 @@ namespace IW4MAdmin.Application.RConParsers
public async Task<string[]> ExecuteCommandAsync(IRConConnection connection, string command, CancellationToken token = default)
{
command = command.FormatMessageForEngine(Configuration?.ColorCodeMapping);
command = command.FormatMessageForEngine(Configuration);
var response = await connection.SendQueryAsync(StaticHelpers.QueryType.COMMAND, command, token);
return response.Where(item => item != Configuration.CommandPrefixes.RConResponse).ToArray();
}
@ -105,7 +105,7 @@ namespace IW4MAdmin.Application.RConParsers
lineSplit = Array.Empty<string>();
}
var response = string.Join('\n', lineSplit).TrimEnd('\0');
var response = string.Join('\n', lineSplit).Replace("\n", "").TrimEnd('\0');
var match = Regex.Match(response, Configuration.Dvar.Pattern);
if (response.Contains("Unknown command") ||
@ -314,9 +314,9 @@ namespace IW4MAdmin.Application.RConParsers
continue;
}
var client = new EFClient()
var client = new EFClient
{
CurrentAlias = new EFAlias()
CurrentAlias = new EFAlias
{
Name = name,
IPAddress = ip
@ -368,15 +368,28 @@ namespace IW4MAdmin.Application.RConParsers
(T)Convert.ChangeType(Configuration.DefaultDvarValues[dvarName], typeof(T)) :
default;
public TimeSpan OverrideTimeoutForCommand(string command)
public TimeSpan? OverrideTimeoutForCommand(string command)
{
if (command.Contains("map_rotate", StringComparison.InvariantCultureIgnoreCase) ||
command.StartsWith("map ", StringComparison.InvariantCultureIgnoreCase))
if (string.IsNullOrEmpty(command))
{
return TimeSpan.FromSeconds(30);
return TimeSpan.Zero;
}
var commandToken = command.Split(' ', StringSplitOptions.RemoveEmptyEntries).First().ToLower();
if (!Configuration.OverrideCommandTimeouts.ContainsKey(commandToken))
{
return TimeSpan.Zero;
}
return TimeSpan.Zero;
var timeoutValue = Configuration.OverrideCommandTimeouts[commandToken];
if (timeoutValue.HasValue && timeoutValue.Value != 0) // JINT doesn't seem to be able to properly set nulls on dictionaries
{
return TimeSpan.FromSeconds(timeoutValue.Value);
}
return null;
}
}
}

View File

@ -26,12 +26,14 @@ namespace IW4MAdmin.Application.RConParsers
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
public IDictionary<string, string> OverrideDvarNameMapping { get; set; } = new Dictionary<string, string>();
public IDictionary<string, string> DefaultDvarValues { get; set; } = new Dictionary<string, string>();
public IDictionary<string, int?> OverrideCommandTimeouts { get; set; } = new Dictionary<string, int?>();
public int NoticeMaximumLines { get; set; } = 8;
public int NoticeMaxCharactersPerLine { get; set; } = 50;
public string NoticeLineSeparator { get; set; } = Environment.NewLine;
public int? DefaultRConPort { get; set; }
public string DefaultInstallationDirectoryHint { get; set; }
public short FloodProtectInterval { get; set; } = 750;
public bool ShouldRemoveDiacritics { get; set; }
public ColorCodeMapping ColorCodeMapping { get; set; } = new ColorCodeMapping
{
@ -58,6 +60,25 @@ namespace IW4MAdmin.Application.RConParsers
StatusHeader = parserRegexFactory.CreateParserRegex();
HostnameStatus = parserRegexFactory.CreateParserRegex();
MaxPlayersStatus = parserRegexFactory.CreateParserRegex();
const string mapRotateCommand = "map_rotate";
const string mapCommand = "map";
const string fastRestartCommand = "fast_restart";
const string quitCommand = "quit";
foreach (var command in new[] { mapRotateCommand, mapCommand, fastRestartCommand})
{
if (!OverrideCommandTimeouts.ContainsKey(command))
{
OverrideCommandTimeouts.Add(command, 45);
}
}
if (!OverrideCommandTimeouts.ContainsKey(quitCommand))
{
OverrideCommandTimeouts.Add(quitCommand, 0); // we don't want to wait for a response when we quit the server
}
}
}
}

View File

@ -85,7 +85,15 @@ namespace Data.Context
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// make network id unique
modelBuilder.Entity<EFClient>(entity => { entity.HasIndex(e => e.NetworkId).IsUnique(); });
modelBuilder.Entity<EFClient>(entity =>
{
entity.HasIndex(e => e.NetworkId);
entity.HasAlternateKey(client => new
{
client.NetworkId,
client.GameName
});
});
modelBuilder.Entity<EFPenalty>(entity =>
{

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,63 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.MySql
{
public partial class AddAlternateKeyToEFClients : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients");
migrationBuilder.Sql("UPDATE `EFClients` set `GameName` = 0 WHERE `GameName` IS NULL");
migrationBuilder.AlterColumn<int>(
name: "GameName",
table: "EFClients",
type: "int",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "int",
oldNullable: true);
migrationBuilder.AddUniqueConstraint(
name: "AK_EFClients_NetworkId_GameName",
table: "EFClients",
columns: new[] { "NetworkId", "GameName" });
migrationBuilder.CreateIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients",
column: "NetworkId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropUniqueConstraint(
name: "AK_EFClients_NetworkId_GameName",
table: "EFClients");
migrationBuilder.DropIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients");
migrationBuilder.AlterColumn<int>(
name: "GameName",
table: "EFClients",
type: "int",
nullable: true,
oldClrType: typeof(int),
oldType: "int");
migrationBuilder.CreateIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients",
column: "NetworkId",
unique: true);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,27 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.MySql
{
public partial class AddDescendingTimeSentIndexEFClientMessages : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
try
{
migrationBuilder.Sql(@"create index IX_EFClientMessages_TimeSentDesc on EFClientMessages (TimeSent desc);");
}
catch
{
migrationBuilder.Sql(@"create index IX_EFClientMessages_TimeSentDesc on efclientmessages (TimeSent desc);");
}
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"drop index IX_EFClientMessages_TimeSentDesc on EFClientMessages;");
}
}
}

View File

@ -64,7 +64,7 @@ namespace Data.Migrations.MySql
b.Property<DateTime>("FirstConnection")
.HasColumnType("datetime(6)");
b.Property<int?>("GameName")
b.Property<int>("GameName")
.HasColumnType("int");
b.Property<DateTime>("LastConnection")
@ -90,12 +90,13 @@ namespace Data.Migrations.MySql
b.HasKey("ClientId");
b.HasAlternateKey("NetworkId", "GameName");
b.HasIndex("AliasLinkId");
b.HasIndex("CurrentAliasId");
b.HasIndex("NetworkId")
.IsUnique();
b.HasIndex("NetworkId");
b.ToTable("EFClients", (string)null);
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,63 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Postgresql
{
public partial class AddAlternateKeyToEFClients : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients");
migrationBuilder.Sql("UPDATE \"EFClients\" SET \"GameName\" = 0 WHERE \"GameName\" IS NULL");
migrationBuilder.AlterColumn<int>(
name: "GameName",
table: "EFClients",
type: "integer",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "integer",
oldNullable: true);
migrationBuilder.AddUniqueConstraint(
name: "AK_EFClients_NetworkId_GameName",
table: "EFClients",
columns: new[] { "NetworkId", "GameName" });
migrationBuilder.CreateIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients",
column: "NetworkId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropUniqueConstraint(
name: "AK_EFClients_NetworkId_GameName",
table: "EFClients");
migrationBuilder.DropIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients");
migrationBuilder.AlterColumn<int>(
name: "GameName",
table: "EFClients",
type: "integer",
nullable: true,
oldClrType: typeof(int),
oldType: "integer");
migrationBuilder.CreateIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients",
column: "NetworkId",
unique: true);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Postgresql
{
public partial class AddDescendingTimeSentIndexEFClientMessages : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(
@"CREATE INDEX""IX_EFClientMessages_TimeSentDesc""
ON public.""EFClientMessages"" USING btree
(""TimeSent"" DESC NULLS LAST)
TABLESPACE pg_default;"
);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"DROP INDEX public.""IX_EFClientMessages_TimeSentDesc""");
}
}
}

View File

@ -71,7 +71,7 @@ namespace Data.Migrations.Postgresql
b.Property<DateTime>("FirstConnection")
.HasColumnType("timestamp without time zone");
b.Property<int?>("GameName")
b.Property<int>("GameName")
.HasColumnType("integer");
b.Property<DateTime>("LastConnection")
@ -97,12 +97,13 @@ namespace Data.Migrations.Postgresql
b.HasKey("ClientId");
b.HasAlternateKey("NetworkId", "GameName");
b.HasIndex("AliasLinkId");
b.HasIndex("CurrentAliasId");
b.HasIndex("NetworkId")
.IsUnique();
b.HasIndex("NetworkId");
b.ToTable("EFClients", (string)null);
});

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,61 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Sqlite
{
public partial class AddAlternateKeyToEFClients : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients");
migrationBuilder.AlterColumn<int>(
name: "GameName",
table: "EFClients",
type: "INTEGER",
nullable: false,
defaultValue: 0,
oldClrType: typeof(int),
oldType: "INTEGER",
oldNullable: true);
migrationBuilder.AddUniqueConstraint(
name: "AK_EFClients_NetworkId_GameName",
table: "EFClients",
columns: new[] { "NetworkId", "GameName" });
migrationBuilder.CreateIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients",
column: "NetworkId");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropUniqueConstraint(
name: "AK_EFClients_NetworkId_GameName",
table: "EFClients");
migrationBuilder.DropIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients");
migrationBuilder.AlterColumn<int>(
name: "GameName",
table: "EFClients",
type: "INTEGER",
nullable: true,
oldClrType: typeof(int),
oldType: "INTEGER");
migrationBuilder.CreateIndex(
name: "IX_EFClients_NetworkId",
table: "EFClients",
column: "NetworkId",
unique: true);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,19 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Sqlite
{
public partial class AddDescendingTimeSentIndexEFClientMessages : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
}
protected override void Down(MigrationBuilder migrationBuilder)
{
}
}
}

View File

@ -62,7 +62,7 @@ namespace Data.Migrations.Sqlite
b.Property<DateTime>("FirstConnection")
.HasColumnType("TEXT");
b.Property<int?>("GameName")
b.Property<int>("GameName")
.HasColumnType("INTEGER");
b.Property<DateTime>("LastConnection")
@ -88,12 +88,13 @@ namespace Data.Migrations.Sqlite
b.HasKey("ClientId");
b.HasAlternateKey("NetworkId", "GameName");
b.HasIndex("AliasLinkId");
b.HasIndex("CurrentAliasId");
b.HasIndex("NetworkId")
.IsUnique();
b.HasIndex("NetworkId");
b.ToTable("EFClients", (string)null);
});

View File

@ -63,7 +63,7 @@ namespace Data.Models.Client
public DateTime FirstConnection { get; set; }
[Required]
public DateTime LastConnection { get; set; }
public Reference.Game? GameName { get; set; } = Reference.Game.UKN;
public Reference.Game GameName { get; set; } = Reference.Game.UKN;
public bool Masked { get; set; }
[Required]
public int AliasLinkId { get; set; }

View File

@ -7,8 +7,6 @@ namespace Data.Models.Client.Stats
{
public class EFClientRankingHistory: AuditFields
{
public const int MaxRankingCount = 30;
[Key]
public long ClientRankingHistoryId { get; set; }
@ -28,4 +26,4 @@ namespace Data.Models.Client.Stats
public double? ZScore { get; set; }
public double? PerformanceMetric { get; set; }
}
}
}

View File

@ -29,8 +29,9 @@ onPlayerConnect( player )
for( ;; )
{
level waittill( "connected", player );
player setClientDvar("cl_autorecord", 1);
player setClientDvar("cl_demosKeep", 200);
player setClientDvars( "cl_autorecord", 1,
"cl_demosKeep", 200 );
player thread waitForFrameThread();
player thread waitForAttack();
}
@ -60,7 +61,7 @@ getHttpString( url )
runRadarUpdates()
{
interval = int(getDvar("sv_printradar_updateinterval"));
interval = getDvarInt( "sv_printradar_updateinterval", 500 );
for ( ;; )
{
@ -191,7 +192,7 @@ waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
i++;
}
lastAttack = int(getTime()) - int(self.lastAttackTime);
lastAttack = getTime() - self.lastAttackTime;
isAlive = isAlive(self);
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );

View File

@ -53,7 +53,7 @@ waitForAttack()
runRadarUpdates()
{
interval = int(getDvar("sv_printradar_updateinterval"));
interval = getDvarInt( "sv_printradar_updateinterval", 500 );
for ( ;; )
{
@ -183,7 +183,7 @@ waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
i++;
}
lastAttack = int(getTime()) - int(self.lastAttackTime);
lastAttack = getTime() - self.lastAttackTime;
isAlive = isAlive(self);
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );

View File

@ -60,7 +60,7 @@ waitForAttack()
runRadarUpdates()
{
interval = int(getDvar("sv_printradar_updateinterval"));
interval = getDvarInt( "sv_printradar_updateinterval" );
for ( ;; )
{
@ -190,7 +190,7 @@ waitForAdditionalAngles( logString, beforeFrameCount, afterFrameCount )
i++;
}
lastAttack = int(getTime()) - int(self.lastAttackTime);
lastAttack = getTime() - self.lastAttackTime;
isAlive = isAlive(self);
logPrint(logString + ";" + anglesStr + ";" + isAlive + ";" + lastAttack + "\n" );

View File

@ -53,15 +53,13 @@ init()
level thread OnPlayerConnect();
}
//////////////////////////////////
// Client Methods
//////////////////////////////////
OnPlayerConnect()
{
level endon ( "disconnect" );
level endon ( "game_ended" );
for ( ;; )
{
@ -69,7 +67,7 @@ OnPlayerConnect()
level.iw4adminIntegrationDebug = GetDvarInt( "sv_iw4madmin_integration_debug" );
if ( isDefined(player.pers["isBot"]) && player.pers["isBot"] )
if ( isDefined( player.pers["isBot"] ) && player.pers["isBot"] )
{
// we don't want to track bots
continue;
@ -106,7 +104,7 @@ OnPlayerSpawned()
OnPlayerDisconnect()
{
level endon ( "disconnect" );
self endon ( "disconnect" );
for ( ;; )
{
@ -141,8 +139,6 @@ OnPlayerJoinedSpectators()
OnGameEnded()
{
level endon ( "disconnect" );
for ( ;; )
{
level waittill( "game_ended" );
@ -167,6 +163,33 @@ DisplayWelcomeData()
self IPrintLnBold( "You were last seen ^5" + clientData.lastConnection );
}
SetPersistentData()
{
guidHigh = self GetPlayerData( "bests", "none" );
guidLow = self GetPlayerData( "awards", "none" );
persistentGuid = guidHigh + "," + guidLow;
if ( guidHigh != 0 && guidLow != 0)
{
if ( level.iw4adminIntegrationDebug == 1 )
{
IPrintLn( "Uploading persistent guid " + persistentGuid );
}
SetClientMeta( "PersistentClientGuid", persistentGuid );
}
if ( level.iw4adminIntegrationDebug == 1 )
{
IPrintLn( "Persisting client guid " + persistentGuid );
}
guid = self SplitGuid();
self SetPlayerData( "bests", "none", guid["high"] );
self SetPlayerData( "awards", "none", guid["low"] );
}
PlayerConnectEvents()
{
self endon( "disconnect" );
@ -208,8 +231,7 @@ PlayerTrackingOnInterval()
MonitorClientEvents()
{
level endon( "disconnect" );
self endon( "disconnect" );
level endon( "game_ended" );
for ( ;; )
{
@ -304,6 +326,107 @@ DecrementClientMeta( metaKey, decrementValue, clientId )
SetClientMeta( metaKey, decrementValue, clientId, "decrement" );
}
SplitGuid()
{
guid = self GetGuid();
if ( isDefined( self.guid ) )
{
guid = self.guid;
}
firstPart = 0;
secondPart = 0;
stringLength = 17;
firstPartExp = 0;
secondPartExp = 0;
for ( i = stringLength - 1; i > 0; i-- )
{
char = GetSubStr( guid, i - 1, i );
if ( char == "" )
{
char = "0";
}
if ( i > stringLength / 2 )
{
value = GetIntForHexChar( char );
power = Pow( 16, secondPartExp );
secondPart = secondPart + ( value * power );
secondPartExp++;
}
else
{
value = GetIntForHexChar( char );
power = Pow( 16, firstPartExp );
firstPart = firstPart + ( value * power );
firstPartExp++;
}
}
split = [];
split["low"] = int( secondPart );
split["high"] = int( firstPart );
return split;
}
Pow( num, exponent )
{
result = 1;
while( exponent != 0 )
{
result = result * num;
exponent--;
}
return result;
}
GetIntForHexChar( char )
{
char = ToLower( char );
// generated by co-pilot because I can't be bothered to make it more "elegant"
switch( char )
{
case "0":
return 0;
case "1":
return 1;
case "2":
return 2;
case "3":
return 3;
case "4":
return 4;
case "5":
return 5;
case "6":
return 6;
case "7":
return 7;
case "8":
return 8;
case "9":
return 9;
case "a":
return 10;
case "b":
return 11;
case "c":
return 12;
case "d":
return 13;
case "e":
return 14;
case "f":
return 15;
default:
return 0;
}
}
GenerateJoinTeamString( isSpectator )
{
team = self.team;
@ -456,7 +579,7 @@ MonitorBus()
QueueEvent( request, eventType, notifyEntity )
{
level endon( "disconnect" );
level endon( "game_ended" );
start = GetTime();
maxWait = level.eventBus.timeout * 1000; // 30 seconds
@ -490,6 +613,8 @@ QueueEvent( request, eventType, notifyEntity )
{
notifyEntity NotifyClientEventTimeout( eventType );
}
SetDvar( level.eventBus.inVar, "" );
return;
}
@ -643,6 +768,7 @@ OnClientDataReceived( event )
self.persistentClientId = event.data["clientId"];
self thread DisplayWelcomeData();
self setPersistentData();
}
OnExecuteCommand( event )
@ -902,7 +1028,7 @@ GotoCoordImpl( data )
return;
}
position = ( int(data["x"]), int(data["y"]), int(data["z"]) );
position = ( int( data["x"] ), int( data["y"] ), int( data["z"]) );
self SetOrigin( position );
self IPrintLnBold( "Moved to " + "("+ position[0] + "," + position[1] + "," + position[2] + ")" );
}

View File

@ -253,8 +253,10 @@ namespace Integrations.Cod
try
{
connectionState.LastQuery = DateTime.Now;
var timeout = _parser.OverrideTimeoutForCommand(parameters);
waitForResponse = waitForResponse && timeout.HasValue;
response = await SendPayloadAsync(payload, waitForResponse,
_parser.OverrideTimeoutForCommand(parameters), token);
timeout ?? TimeSpan.Zero, token);
if ((response?.Length == 0 || response[0].Length == 0) && waitForResponse)
{
@ -456,6 +458,12 @@ namespace Integrations.Cod
connectionState.SendEventArgs.DisconnectReuseSocket = true;
}
if (connectionState.ReceiveEventArgs.UserToken is ConnectionUserToken { CancellationToken.IsCancellationRequested: true })
{
// after a graceful restart we need to reset the receive user token as the cancellation has been updated
connectionState.ReceiveEventArgs.UserToken = connectionState.SendEventArgs.UserToken;
}
connectionState.SendEventArgs.SetBuffer(payload);
// send the data to the server

View File

@ -10,7 +10,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -16,7 +16,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -4,6 +4,7 @@ using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
using System.Threading.Tasks;
using SharedLibraryCore.Helpers;
namespace IW4MAdmin.Plugins.Login.Commands
{
@ -18,7 +19,7 @@ namespace IW4MAdmin.Plugins.Login.Commands
RequiresTarget = false;
Arguments = new CommandArgument[]
{
new CommandArgument()
new()
{
Name = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_ARGS_PASSWORD"],
Required = true
@ -26,24 +27,28 @@ namespace IW4MAdmin.Plugins.Login.Commands
};
}
public override async Task ExecuteAsync(GameEvent E)
public override async Task ExecuteAsync(GameEvent gameEvent)
{
bool success = E.Owner.Manager.TokenAuthenticator.AuthorizeToken(E.Origin.NetworkId, E.Data);
var success = gameEvent.Owner.Manager.TokenAuthenticator.AuthorizeToken(new TokenIdentifier
{
ClientId = gameEvent.Origin.ClientId,
Token = gameEvent.Data
});
if (!success)
{
string[] hashedPassword = await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(E.Data, E.Origin.PasswordSalt));
success = hashedPassword[0] == E.Origin.Password;
var hashedPassword = await Task.FromResult(Hashing.Hash(gameEvent.Data, gameEvent.Origin.PasswordSalt));
success = hashedPassword[0] == gameEvent.Origin.Password;
}
if (success)
{
Plugin.AuthorizedClients[E.Origin.ClientId] = true;
Plugin.AuthorizedClients[gameEvent.Origin.ClientId] = true;
}
_ = success ?
E.Origin.Tell(_translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS"]) :
E.Origin.Tell(_translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL"]);
gameEvent.Origin.Tell(_translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS"]) :
gameEvent.Origin.Tell(_translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL"]);
}
}
}

View File

@ -19,7 +19,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -16,7 +16,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -46,7 +46,7 @@ let plugin = {
break;
case 'warn':
const warningTitle = _localization.LocalizationIndex['GLOBAL_WARNING'];
sendScriptCommand(server, 'Alert', gameEvent.Target, {
sendScriptCommand(server, 'Alert', gameEvent.Origin, gameEvent.Target, {
alertType: warningTitle + '!',
message: gameEvent.Data
});
@ -463,7 +463,11 @@ function onReceivedDvar(server, dvarName, dvarValue, success) {
if (input.length > 0) {
const event = parseEvent(input)
logger.WriteDebug(`Processing input... ${event.eventType} ${event.subType} ${event.data} ${event.clientNumber}`);
logger.WriteDebug(`Processing input... ${event.eventType} ${event.subType} ${event.data.toString()} ${event.clientNumber}`);
const metaService = _serviceResolver.ResolveService('IMetaServiceV2');
const threading = importNamespace('System.Threading');
const token = new threading.CancellationTokenSource().Token;
// todo: refactor to mapping if possible
if (event.eventType === 'ClientDataRequested') {
@ -475,8 +479,8 @@ function onReceivedDvar(server, dvarName, dvarValue, success) {
let data = [];
if (event.subType === 'Meta') {
const metaService = _serviceResolver.ResolveService('IMetaService');
const meta = metaService.GetPersistentMeta(event.data, client).GetAwaiter().GetResult();
const metaService = _serviceResolver.ResolveService('IMetaServiceV2');
const meta = metaService.GetPersistentMeta(event.data, client, token).GetAwaiter().GetResult();
data[event.data] = meta === null ? '' : meta.Value;
} else {
data = {
@ -510,19 +514,19 @@ function onReceivedDvar(server, dvarName, dvarValue, success) {
sendEvent(server, false, 'SetClientDataCompleted', 'Meta', {ClientNumber: event.clientNumber}, undefined, {status: 'Fail'});
} else {
if (event.subType === 'Meta') {
const metaService = _serviceResolver.ResolveService('IMetaService');
try {
logger.WriteDebug(`Key=${event.data['key']}, Value=${event.data['value']}`);
logger.WriteDebug(`Key=${event.data['key']}, Value=${event.data['value']}, Direction=${event.data['direction']} ${token}`);
if (event.data['direction'] != null) {
event.data['direction'] = 'up'
? metaService.IncrementPersistentMeta(event.data['key'], event.data['value'], clientId).GetAwaiter().GetResult()
: metaService.DecrementPersistentMeta(event.data['key'], event.data['value'], clientId).GetAwaiter().GetResult();
? metaService.IncrementPersistentMeta(event.data['key'], parseInt(event.data['value']), clientId, token).GetAwaiter().GetResult()
: metaService.DecrementPersistentMeta(event.data['key'], parseInt(event.data['value']), clientId, token).GetAwaiter().GetResult();
} else {
metaService.SetPersistentMeta(event.data['key'], event.data['value'], clientId).GetAwaiter().GetResult();
metaService.SetPersistentMeta(event.data['key'], event.data['value'], clientId, token).GetAwaiter().GetResult();
}
sendEvent(server, false, 'SetClientDataCompleted', 'Meta', {ClientNumber: event.clientNumber}, undefined, {status: 'Complete'});
} catch (error) {
sendEvent(server, false, 'SetClientDataCompleted', 'Meta', {ClientNumber: event.clientNumber}, undefined, {status: 'Fail'});
logger.WriteError('Could not persist client meta ' + error.toString());
}
}
}

View File

@ -20,7 +20,7 @@ var plugin = {
rconParser.Configuration.CommandPrefixes.Ban = 'clientkick {0} "{1}"';
rconParser.Configuration.CommandPrefixes.TempBan = 'clientkick {0} "{1}"';
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n';
rconParser.Configuration.Dvar.Pattern = '^ *\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n(?:latched: \\"(.+)?\\"\\n)? *(.+)$';
rconParser.Configuration.Dvar.Pattern = '^ *\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n?(?:latched: \\"(.+)?\\"\\n?)? *(.+)$';
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +(Yes|No) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback|unknown|bot) +(-*[0-9]+) *$';
rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +address +qport *';
rconParser.Configuration.WaitForResponse = false;

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = {
author: 'RaidMax, Xerxes',
version: 1.2,
version: 1.4,
name: 'Plutonium T6 Parser',
isParser: true,
@ -29,9 +29,10 @@ var plugin = {
rconParser.Configuration.NoticeLineSeparator = '. ';
rconParser.Configuration.DefaultRConPort = 4976;
rconParser.Configuration.DefaultInstallationDirectoryHint = '{LocalAppData}/Plutonium/storage/t6';
rconParser.Configuration.ShouldRemoveDiacritics = true;
rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +lastmsg +address +qport +rate *';
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +([0-9]+) +(?:[0-1]{1}) +([0-9]+) +([A-F0-9]+|0) +(.+?) +(?:[0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+:-?\\d{1,5}|loopback) +(?:-?[0-9]+) +(?:[0-9]+) *$';
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +([0-9]+) +(?:[0-1]{1}) +([0-9]+) +([A-F0-9]+|0) +(.+?) +(?:[0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+:-?\\d{1,5}|loopback|unknown) +(?:-?[0-9]+) +(?:[0-9]+) *$';
rconParser.Configuration.Status.AddMapping(100, 1);
rconParser.Configuration.Status.AddMapping(101, 2);
rconParser.Configuration.Status.AddMapping(102, 3);

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = {
author: 'RaidMax',
version: 0.1,
version: 0.2,
name: 'Plutonium T5 Parser',
isParser: true,
@ -23,6 +23,11 @@ var plugin = {
rconParser.Configuration.DefaultRConPort = 3074;
rconParser.Configuration.CanGenerateLogPath = false;
rconParser.Configuration.OverrideCommandTimeouts.Clear();
rconParser.Configuration.OverrideCommandTimeouts.Add('map', 0);
rconParser.Configuration.OverrideCommandTimeouts.Add('map_rotate', 0);
rconParser.Configuration.OverrideCommandTimeouts.Add('fast_restart', 0);
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';

View File

@ -17,7 +17,7 @@ var plugin = {
rconParser.Configuration.CommandPrefixes.Ban = 'kickClient {0} "{1}"';
rconParser.Configuration.CommandPrefixes.TempBan = 'kickClient {0} "{1}"';
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint';
rconParser.Configuration.Dvar.Pattern = '^ *\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n(?:latched: \\"(.+)?\\"\\n)? *(.+)$';
rconParser.Configuration.Dvar.Pattern = '^ *\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n?(?:latched: \\"(.+)?\\"\\n?)? *(.+)$';
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +(Yes|No) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback|unknown|bot) +(-*[0-9]+) *$';
rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +address +qport *';
rconParser.Configuration.Status.AddMapping(102, 4);
@ -37,4 +37,4 @@ var plugin = {
onUnloadAsync: function() {},
onTickAsync: function(server) {}
};
};

View File

@ -3,7 +3,7 @@ var eventParser;
var plugin = {
author: 'RaidMax',
version: 0.2,
version: 0.3,
name: 'Call of Duty 5: World at War Parser',
isParser: true,
@ -17,6 +17,7 @@ var plugin = {
rconParser.Configuration.GuidNumberStyle = 7; // Integer
rconParser.Configuration.DefaultRConPort = 28960;
rconParser.Version = 'Call of Duty Multiplayer COD_WaW MP build 1.7.1263 CL(350073) JADAMS2 Thu Oct 29 15:43:55 2009 win-x86';
rconParser.GameName = 5; // T4
eventParser.Configuration.GuidNumberStyle = 7; // Integer
eventParser.GameName = 5; // T4

View File

@ -23,7 +23,7 @@ namespace Stats.Dtos
/// <summary>
/// only look for messages sent after this date
/// </summary>
public DateTime SentAfter { get; set; } = DateTime.UtcNow.AddYears(-100);
public DateTime? SentAfter { get; set; }
/// <summary>
/// only look for messages sent before this date0

View File

@ -19,8 +19,14 @@ namespace IW4MAdmin.Plugins.Stats.Web.Dtos
public int Kills { get; set; }
public int Deaths { get; set; }
public int RatingChange { get; set; }
public List<double> PerformanceHistory { get; set; }
public List<PerformanceHistory> PerformanceHistory { get; set; }
public double? ZScore { get; set; }
public long? ServerId { get; set; }
}
public class PerformanceHistory
{
public double? Performance { get; set; }
public DateTime OccurredAt { get; set; }
}
}

View File

@ -3,6 +3,7 @@ using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models;
using Data.Models.Client;
using Data.Models.Client.Stats;
using IW4MAdmin.Plugins.Stats;
@ -12,7 +13,6 @@ using Microsoft.Extensions.Logging;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using Stats.Client.Abstractions;
using Stats.Dtos;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -50,7 +50,8 @@ namespace Stats.Helpers
{
client.ClientId,
client.CurrentAlias.Name,
client.Level
client.Level,
client.GameName
}).FirstOrDefaultAsync(client => client.ClientId == query.ClientId);
if (clientInfo == null)
@ -77,7 +78,8 @@ namespace Stats.Helpers
.Where(r => r.ClientId == clientInfo.ClientId)
.Where(r => r.ServerId == serverId)
.Where(r => r.Ranking != null)
.OrderByDescending(r => r.UpdatedDateTime)
.OrderByDescending(r => r.CreatedDateTime)
.Take(250)
.ToListAsync();
var mostRecentRanking = ratings.FirstOrDefault(ranking => ranking.Newest);
@ -111,8 +113,9 @@ namespace Stats.Helpers
Rating = mostRecentRanking?.PerformanceMetric,
All = hitStats,
Servers = _manager.GetServers()
.Select(server => new ServerInfo()
{Name = server.Hostname, IPAddress = server.IP, Port = server.Port})
.Select(server => new ServerInfo
{Name = server.Hostname, IPAddress = server.IP, Port = server.Port, Game = (Reference.Game)server.GameName})
.Where(server => server.Game == clientInfo.GameName)
.ToList(),
Aggregate = hitStats.FirstOrDefault(hit =>
hit.HitLocationId == null && hit.ServerId == serverId && hit.WeaponId == null &&
@ -153,4 +156,4 @@ namespace Stats.Helpers
&& (zScore == null || stats.ZScore > zScore);
}
}
}
}

View File

@ -45,58 +45,53 @@ namespace Stats.Helpers
var result = new ResourceQueryHelperResult<MessageResponse>();
await using var context = _contextFactory.CreateContext(enableTracking: false);
if (serverCache == null)
{
serverCache = await context.Set<EFServer>().ToListAsync();
}
serverCache ??= await context.Set<EFServer>().ToListAsync();
if (int.TryParse(query.ServerId, out int serverId))
if (int.TryParse(query.ServerId, out var serverId))
{
query.ServerId = serverCache.FirstOrDefault(_server => _server.ServerId == serverId)?.EndPoint ?? query.ServerId;
query.ServerId = serverCache.FirstOrDefault(server => server.ServerId == serverId)?.EndPoint ?? query.ServerId;
}
var iqMessages = context.Set<EFClientMessage>()
.Where(_message => _message.TimeSent >= query.SentAfter)
.Where(_message => _message.TimeSent < query.SentBefore);
.Where(message => message.TimeSent < query.SentBefore);
if (query.ClientId != null)
if (query.SentAfter is not null)
{
iqMessages = iqMessages.Where(_message => _message.ClientId == query.ClientId.Value);
iqMessages = iqMessages.Where(message => message.TimeSent >= query.SentAfter);
}
if (query.ServerId != null)
if (query.ClientId is not null)
{
iqMessages = iqMessages.Where(_message => _message.Server.EndPoint == query.ServerId);
iqMessages = iqMessages.Where(message => message.ClientId == query.ClientId.Value);
}
if (query.ServerId is not null)
{
iqMessages = iqMessages.Where(message => message.Server.EndPoint == query.ServerId);
}
if (!string.IsNullOrEmpty(query.MessageContains))
{
iqMessages = iqMessages.Where(_message => EF.Functions.Like(_message.Message.ToLower(), $"%{query.MessageContains.ToLower()}%"));
iqMessages = iqMessages.Where(message => EF.Functions.Like(message.Message.ToLower(), $"%{query.MessageContains.ToLower()}%"));
}
var iqResponse = iqMessages
.Select(_message => new MessageResponse
.Select(message => new MessageResponse
{
ClientId = _message.ClientId,
ClientName = query.IsProfileMeta ? "" : _message.Client.CurrentAlias.Name,
ServerId = _message.ServerId,
When = _message.TimeSent,
Message = _message.Message,
ServerName = query.IsProfileMeta ? "" : _message.Server.HostName,
GameName = _message.Server.GameName == null ? Server.Game.IW4 : (Server.Game)_message.Server.GameName.Value,
SentIngame = _message.SentIngame
ClientId = message.ClientId,
ClientName = query.IsProfileMeta ? "" : message.Client.CurrentAlias.Name,
ServerId = message.ServerId,
When = message.TimeSent,
Message = message.Message,
ServerName = query.IsProfileMeta ? "" : message.Server.HostName,
GameName = message.Server.GameName == null ? Server.Game.IW4 : (Server.Game)message.Server.GameName.Value,
SentIngame = message.SentIngame
});
if (query.Direction == SharedLibraryCore.Dtos.SortDirection.Descending)
{
iqResponse = iqResponse.OrderByDescending(_message => _message.When);
}
else
{
iqResponse = iqResponse.OrderBy(_message => _message.When);
}
iqResponse = query.Direction == SharedLibraryCore.Dtos.SortDirection.Descending
? iqResponse.OrderByDescending(message => message.When)
: iqResponse.OrderBy(message => message.When);
var resultList = await iqResponse
.Skip(query.Offset)
.Take(query.Count)
@ -115,13 +110,13 @@ namespace Stats.Helpers
{
var quickMessages = _defaultSettings
.QuickMessages
.First(_qm => _qm.Game == message.GameName);
.First(qm => qm.Game == message.GameName);
message.Message = quickMessages.Messages[message.Message.Substring(1)];
message.IsQuickMessage = true;
}
catch
{
message.Message = message.Message.Substring(1);
message.Message = message.Message[1..];
}
}

View File

@ -86,7 +86,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public async Task<int> GetClientOverallRanking(int clientId, long? serverId = null)
{
await using var context = _contextFactory.CreateContext(enableTracking: false);
if (_config.EnableAdvancedMetrics)
{
var clientRanking = await context.Set<EFClientRankingHistory>()
@ -117,7 +117,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
return 0;
}
public Expression<Func<EFClientRankingHistory, bool>> GetNewRankingFunc(int? clientId = null, long? serverId = null)
public Expression<Func<EFClientRankingHistory, bool>> GetNewRankingFunc(int? clientId = null,
long? serverId = null)
{
return (ranking) => ranking.ServerId == serverId
&& ranking.Client.Level != Data.Models.Client.EFClient.Permission.Banned
@ -138,6 +139,17 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
.CountAsync();
}
public class RankingSnapshot
{
public int ClientId { get; set; }
public string Name { get; set; }
public DateTime LastConnection { get; set; }
public double? PerformanceMetric { get; set; }
public double? ZScore { get; set; }
public int? Ranking { get; set; }
public DateTime CreatedDateTime { get; set; }
}
public async Task<List<TopStatsInfo>> GetNewTopStats(int start, int count, long? serverId = null)
{
await using var context = _contextFactory.CreateContext(false);
@ -150,24 +162,38 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
.Take(count)
.ToListAsync();
var rankings = await context.Set<EFClientRankingHistory>()
.Where(ranking => clientIdsList.Contains(ranking.ClientId))
.Where(ranking => ranking.ServerId == serverId)
.Select(ranking => new
{
ranking.ClientId,
ranking.Client.CurrentAlias.Name,
ranking.Client.LastConnection,
ranking.PerformanceMetric,
ranking.ZScore,
ranking.Ranking,
ranking.CreatedDateTime
})
.ToListAsync();
var rankingsDict = new Dictionary<int, List<RankingSnapshot>>();
foreach (var clientId in clientIdsList)
{
var eachRank = await context.Set<EFClientRankingHistory>()
.Where(ranking => ranking.ClientId == clientId)
.Where(ranking => ranking.ServerId == serverId)
.OrderByDescending(ranking => ranking.CreatedDateTime)
.Select(ranking => new RankingSnapshot
{
ClientId = ranking.ClientId,
Name = ranking.Client.CurrentAlias.Name,
LastConnection = ranking.Client.LastConnection,
PerformanceMetric = ranking.PerformanceMetric,
ZScore = ranking.ZScore,
Ranking = ranking.Ranking,
CreatedDateTime = ranking.CreatedDateTime
})
.Take(60)
.ToListAsync();
if (rankingsDict.ContainsKey(clientId))
{
rankingsDict[clientId] = rankingsDict[clientId].Concat(eachRank).Distinct()
.OrderByDescending(ranking => ranking.CreatedDateTime).ToList();
}
else
{
rankingsDict.Add(clientId, eachRank);
}
}
var rankingsDict = rankings.GroupBy(rank => rank.ClientId)
.ToDictionary(rank => rank.Key, rank => rank.OrderBy(r => r.CreatedDateTime).ToList());
var statsInfo = await context.Set<EFClientStatistics>()
.Where(stat => clientIdsList.Contains(stat.ClientId))
.Where(stat => stat.TimePlayed > 0)
@ -179,7 +205,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
ClientId = s.Key,
Kills = s.Sum(c => c.Kills),
Deaths = s.Sum(c => c.Deaths),
KDR = s.Sum(c => (c.Kills / (double) (c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) /
KDR = s.Sum(c => (c.Kills / (double)(c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) /
s.Sum(c => c.TimePlayed),
TotalTimePlayed = s.Sum(c => c.TimePlayed),
UpdatedAt = s.Max(c => c.UpdatedAt)
@ -187,30 +213,32 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
.ToListAsync();
var finished = statsInfo
.OrderByDescending(stat => rankingsDict[stat.ClientId].Last().PerformanceMetric)
.Select((s, index) => new TopStatsInfo()
{
ClientId = s.ClientId,
Id = (int?) serverId ?? 0,
Deaths = s.Deaths,
Kills = s.Kills,
KDR = Math.Round(s.KDR, 2),
LastSeen = (DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].Last().LastConnection))
.HumanizeForCurrentCulture(1, TimeUnit.Week, TimeUnit.Second, ",", false),
LastSeenValue = DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].Last().LastConnection),
Name = rankingsDict[s.ClientId].First().Name,
Performance = Math.Round(rankingsDict[s.ClientId].Last().PerformanceMetric ?? 0, 2),
RatingChange = (rankingsDict[s.ClientId].First().Ranking -
rankingsDict[s.ClientId].Last().Ranking) ?? 0,
PerformanceHistory = rankingsDict[s.ClientId].Select(ranking => ranking.PerformanceMetric ?? 0).ToList(),
TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"),
TimePlayedValue = TimeSpan.FromSeconds(s.TotalTimePlayed),
Ranking = index + start + 1,
ZScore = rankingsDict[s.ClientId].Last().ZScore,
ServerId = serverId
})
.OrderBy(r => r.Ranking)
.ToList();
.OrderByDescending(stat => rankingsDict[stat.ClientId].First().PerformanceMetric)
.Select((s, index) => new TopStatsInfo
{
ClientId = s.ClientId,
Id = (int?)serverId ?? 0,
Deaths = s.Deaths,
Kills = s.Kills,
KDR = Math.Round(s.KDR, 2),
LastSeen = (DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].First().LastConnection))
.HumanizeForCurrentCulture(1, TimeUnit.Week, TimeUnit.Second, ",", false),
LastSeenValue = DateTime.UtcNow - (s.UpdatedAt ?? rankingsDict[s.ClientId].First().LastConnection),
Name = rankingsDict[s.ClientId].First().Name,
Performance = Math.Round(rankingsDict[s.ClientId].First().PerformanceMetric ?? 0, 2),
RatingChange = (rankingsDict[s.ClientId].Last().Ranking -
rankingsDict[s.ClientId].First().Ranking) ?? 0,
PerformanceHistory = rankingsDict[s.ClientId].Select(ranking => new PerformanceHistory
{ Performance = ranking.PerformanceMetric ?? 0, OccurredAt = ranking.CreatedDateTime })
.ToList(),
TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"),
TimePlayedValue = TimeSpan.FromSeconds(s.TotalTimePlayed),
Ranking = index + start + 1,
ZScore = rankingsDict[s.ClientId].First().ZScore,
ServerId = serverId
})
.OrderBy(r => r.Ranking)
.ToList();
return finished;
}
@ -221,7 +249,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
return await GetNewTopStats(start, count, serverId);
}
await using var context = _contextFactory.CreateContext(enableTracking: false);
// setup the query for the clients within the given rating range
var iqClientRatings = (from rating in context.Set<EFRating>()
@ -264,7 +292,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
.Select(grp => new
{
grp.Key,
Ratings = grp.Select(r => new {r.Performance, r.Ranking, r.When})
Ratings = grp.Select(r => new { r.Performance, r.Ranking, r.When })
});
var iqStatsInfo = (from stat in context.Set<EFClientStatistics>()
@ -278,7 +306,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
ClientId = s.Key,
Kills = s.Sum(c => c.Kills),
Deaths = s.Sum(c => c.Deaths),
KDR = s.Sum(c => (c.Kills / (double) (c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) /
KDR = s.Sum(c => (c.Kills / (double)(c.Deaths == 0 ? 1 : c.Deaths)) * c.TimePlayed) /
s.Sum(c => c.TimePlayed),
TotalTimePlayed = s.Sum(c => c.TimePlayed),
});
@ -289,7 +317,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
var finished = topPlayers.Select(s => new TopStatsInfo()
{
ClientId = s.ClientId,
Id = (int?) serverId ?? 0,
Id = (int?)serverId ?? 0,
Deaths = s.Deaths,
Kills = s.Kills,
KDR = Math.Round(s.KDR, 2),
@ -302,9 +330,19 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
ratingInfo.First(r => r.Key == s.ClientId).Ratings.Last().Ranking,
PerformanceHistory = ratingInfo.First(r => r.Key == s.ClientId).Ratings.Count() > 1
? ratingInfo.First(r => r.Key == s.ClientId).Ratings.OrderBy(r => r.When)
.Select(r => r.Performance).ToList()
: new List<double>()
{clientRatingsDict[s.ClientId].Performance, clientRatingsDict[s.ClientId].Performance},
.Select(r => new PerformanceHistory { Performance = r.Performance, OccurredAt = r.When })
.ToList()
: new List<PerformanceHistory>
{
new()
{
Performance = clientRatingsDict[s.ClientId].Performance, OccurredAt = DateTime.UtcNow
},
new()
{
Performance = clientRatingsDict[s.ClientId].Performance, OccurredAt = DateTime.UtcNow
}
},
TimePlayed = Math.Round(s.TotalTimePlayed / 3600.0, 1).ToString("#,##0"),
TimePlayedValue = TimeSpan.FromSeconds(s.TotalTimePlayed)
})
@ -366,7 +404,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
Port = sv.Port,
EndPoint = sv.ToString(),
ServerId = serverId,
GameName = (Reference.Game?) sv.GameName,
GameName = (Reference.Game?)sv.GameName,
HostName = sv.Hostname
};
@ -376,9 +414,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
}
// we want to set the gamename up if it's never been set, or it changed
else if (!server.GameName.HasValue || server.GameName.Value != (Reference.Game) sv.GameName)
else if (!server.GameName.HasValue || server.GameName.Value != (Reference.Game)sv.GameName)
{
server.GameName = (Reference.Game) sv.GameName;
server.GameName = (Reference.Game)sv.GameName;
ctx.Entry(server).Property(_prop => _prop.GameName).IsModified = true;
ctx.SaveChanges();
}
@ -469,7 +507,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
Active = true,
HitCount = 0,
Location = (int) hl
Location = (int)hl
}).ToList()
};
@ -489,7 +527,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
Active = true,
HitCount = 0,
Location = (int) hl
Location = (int)hl
})
.ToList();
@ -521,9 +559,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
}
catch (DbUpdateException updateException) when (
updateException.InnerException is PostgresException {SqlState: "23503"}
|| updateException.InnerException is SqliteException {SqliteErrorCode: 787}
|| updateException.InnerException is MySqlException {SqlState: "23503"})
updateException.InnerException is PostgresException { SqlState: "23503" }
|| updateException.InnerException is SqliteException { SqliteErrorCode: 787 }
|| updateException.InnerException is MySqlException { SqlState: "23503" })
{
_log.LogWarning("Trying to add {Client} to stats before they have been added to the database",
pl.ToString());
@ -644,9 +682,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
ServerId = serverId,
DeathOrigin = vDeathOrigin,
KillOrigin = vKillOrigin,
DeathType = (int) ParseEnum<IW4Info.MeansOfDeath>.Get(type, typeof(IW4Info.MeansOfDeath)),
DeathType = (int)ParseEnum<IW4Info.MeansOfDeath>.Get(type, typeof(IW4Info.MeansOfDeath)),
Damage = int.Parse(damage),
HitLoc = (int) ParseEnum<IW4Info.HitLocation>.Get(hitLoc, typeof(IW4Info.HitLocation)),
HitLoc = (int)ParseEnum<IW4Info.HitLocation>.Get(hitLoc, typeof(IW4Info.HitLocation)),
WeaponReference = weapon,
ViewAngles = vViewAngles,
TimeOffset = long.Parse(offset),
@ -660,21 +698,21 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
AnglesList = snapshotAngles,
IsAlive = isAlive == "1",
TimeSinceLastAttack = long.Parse(lastAttackTime),
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}",
_log.LogError(ex,
"Could not parse script hit data. Damage={Damage}, TimeOffset={Offset}, TimeSinceLastAttack={LastAttackTime}",
damage, offset, lastAttackTime);
return;
}
hit.SetAdditionalProperty("HitLocationReference", hitLoc);
if (hit.HitLoc == (int) IW4Info.HitLocation.shield)
if (hit.HitLoc == (int)IW4Info.HitLocation.shield)
{
// we don't care about shield hits
return;
@ -693,9 +731,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
await waiter.WaitAsync(Utilities.DefaultCommandTimeout, Plugin.ServerManager.CancellationToken);
// increment their hit count
if (hit.DeathType == (int) IW4Info.MeansOfDeath.MOD_PISTOL_BULLET ||
hit.DeathType == (int) IW4Info.MeansOfDeath.MOD_RIFLE_BULLET ||
hit.DeathType == (int) IW4Info.MeansOfDeath.MOD_HEAD_SHOT)
if (hit.DeathType == (int)IW4Info.MeansOfDeath.MOD_PISTOL_BULLET ||
hit.DeathType == (int)IW4Info.MeansOfDeath.MOD_RIFLE_BULLET ||
hit.DeathType == (int)IW4Info.MeansOfDeath.MOD_HEAD_SHOT)
{
clientStats.HitLocations.First(hl => hl.Location == hit.HitLoc).HitCount += 1;
}
@ -838,7 +876,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
catch (KeyNotFoundException)
{
}
try
{
if (!gameDetectionTypes[server.GameName].Contains(detectionType))
@ -870,7 +908,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
new EFPenalty()
{
AutomatedOffense = penalty.Type == Detection.DetectionType.Bone
? $"{penalty.Type}-{(int) penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}"
? $"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}"
: $"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}",
}
};
@ -887,7 +925,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
}
string flagReason = penalty.Type == Cheat.Detection.DetectionType.Bone
? $"{penalty.Type}-{(int) penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}"
? $"{penalty.Type}-{(int)penalty.Location}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}"
: $"{penalty.Type}-{Math.Round(penalty.Value, 2)}@{penalty.HitCount}";
penaltyClient.AdministeredPenalties = new List<EFPenalty>()
@ -926,19 +964,19 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
// update the total stats
_servers[serverId].ServerStatistics.TotalKills += 1;
if (attackerStats == null)
{
_log.LogWarning("Stats for {Client} are not yet initialized", attacker.ToString());
return;
}
if (victimStats == null)
{
_log.LogWarning("Stats for {Client} are not yet initialized", victim.ToString());
return;
}
// this happens when the round has changed
if (attackerStats.SessionScore == 0)
{
@ -951,10 +989,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
}
var estimatedAttackerScore = attacker.CurrentServer.GameName != Server.Game.CSGO
? attacker.Score
? attacker.Score
: attackerStats.SessionKills * 50;
var estimatedVictimScore = attacker.CurrentServer.GameName != Server.Game.CSGO
? victim.Score
var estimatedVictimScore = attacker.CurrentServer.GameName != Server.Game.CSGO
? victim.Score
: victimStats.SessionKills * 50;
attackerStats.SessionScore = estimatedAttackerScore;
@ -1042,7 +1080,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
/// <returns></returns>
public async Task UpdateStatHistory(EFClient client, EFClientStatistics clientStats)
{
int currentSessionTime = (int) (DateTime.UtcNow - client.LastConnection).TotalSeconds;
int currentSessionTime = (int)(DateTime.UtcNow - client.LastConnection).TotalSeconds;
// don't update their stat history if they haven't played long
if (currentSessionTime < 60)
@ -1215,7 +1253,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
{
await using var context = _contextFactory.CreateContext();
var minPlayTime = _config.TopPlayersMinPlayTime;
var performances = await context.Set<EFClientStatistics>()
.AsNoTracking()
.Where(stat => stat.ClientId == clientId)
@ -1223,7 +1261,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
.Where(stats => stats.UpdatedAt >= Extensions.FifteenDaysAgo())
.Where(stats => stats.TimePlayed >= minPlayTime)
.ToListAsync();
if (clientStats.TimePlayed >= minPlayTime)
{
clientStats.ZScore = await _serverDistributionCalculator.GetZScoreForServer(serverId,
@ -1254,8 +1292,9 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
if (performances.Any(performance => performance.TimePlayed >= minPlayTime))
{
var aggregateZScore = performances.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), minPlayTime);
var aggregateZScore =
performances.WeightValueByPlaytime(nameof(EFClientStatistics.ZScore), minPlayTime);
int? aggregateRanking = await context.Set<EFClientStatistics>()
.Where(stat => stat.ClientId != clientId)
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(minPlayTime))
@ -1274,7 +1313,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
clientStats.Client?.ToString(), aggregateZScore);
return;
}
var aggregateRankingSnapshot = new EFClientRankingHistory
{
ClientId = clientId,
@ -1297,7 +1336,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
.Where(r => r.ClientId == clientId)
.Where(r => r.ServerId == serverId)
.CountAsync();
var mostRecent = await context.Set<EFClientRankingHistory>()
.Where(r => r.ClientId == clientId)
.Where(r => r.ServerId == serverId)
@ -1309,14 +1348,20 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
context.Update(mostRecent);
}
if (totalRankingEntries > EFClientRankingHistory.MaxRankingCount)
const int maxRankingCount = 1728; // 60 / 2.5 * 24 * 3 ( 3 days at sample every 2.5 minutes)
if (totalRankingEntries > maxRankingCount)
{
var lastRating = await context.Set<EFClientRankingHistory>()
.Where(r => r.ClientId == clientId)
.Where(r => r.ServerId == serverId)
.OrderBy(r => r.CreatedDateTime)
.FirstOrDefaultAsync();
context.Remove(lastRating);
if (lastRating is not null)
{
context.Remove(lastRating);
}
}
}
@ -1325,7 +1370,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
/// </summary>
/// <param name="attackerStats">Stats of the attacker</param>
/// <param name="victimStats">Stats of the victim</param>
public void CalculateKill(EFClientStatistics attackerStats, EFClientStatistics victimStats,
public void CalculateKill(EFClientStatistics attackerStats, EFClientStatistics victimStats,
EFClient attacker, EFClient victim)
{
bool suicide = attackerStats.ClientId == victimStats.ClientId;
@ -1351,7 +1396,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
// calculate elo
var attackerEloDifference = Math.Log(Math.Max(1, victimStats.EloRating)) -
Math.Log(Math.Max(1, attackerStats.EloRating));
Math.Log(Math.Max(1, attackerStats.EloRating));
var winPercentage = 1.0 / (1 + Math.Pow(10, attackerEloDifference / Math.E));
attackerStats.EloRating += 6.0 * (1 - winPercentage);
@ -1361,8 +1406,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
victimStats.EloRating = Math.Max(0, Math.Round(victimStats.EloRating, 2));
// update after calculation
attackerStats.TimePlayed += (int) (DateTime.UtcNow - attackerStats.LastActive).TotalSeconds;
victimStats.TimePlayed += (int) (DateTime.UtcNow - victimStats.LastActive).TotalSeconds;
attackerStats.TimePlayed += (int)(DateTime.UtcNow - attackerStats.LastActive).TotalSeconds;
victimStats.TimePlayed += (int)(DateTime.UtcNow - victimStats.LastActive).TotalSeconds;
attackerStats.LastActive = DateTime.UtcNow;
victimStats.LastActive = DateTime.UtcNow;
}
@ -1400,11 +1445,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
var killSpm = scoreDifference / timeSinceLastCalc;
var spmMultiplier = 2.934 *
Math.Pow(
_servers[clientStats.ServerId]
.TeamCount((IW4Info.Team) clientStats.Team == IW4Info.Team.Allies
? IW4Info.Team.Axis
: IW4Info.Team.Allies), -0.454);
Math.Pow(
_servers[clientStats.ServerId]
.TeamCount((IW4Info.Team)clientStats.Team == IW4Info.Team.Allies
? IW4Info.Team.Axis
: IW4Info.Team.Allies), -0.454);
killSpm *= Math.Max(1, spmMultiplier);
// update this for ac tracking
@ -1421,8 +1466,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
// calculate the weight of the new play time against last 10 hours of gameplay
int totalPlayTime = (clientStats.TimePlayed == 0)
? (int) (DateTime.UtcNow - clientStats.LastActive).TotalSeconds
: clientStats.TimePlayed + (int) (DateTime.UtcNow - clientStats.LastActive).TotalSeconds;
? (int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds
: clientStats.TimePlayed + (int)(DateTime.UtcNow - clientStats.LastActive).TotalSeconds;
double SPMAgainstPlayWeight = timeSinceLastCalc / Math.Min(600, (totalPlayTime / 60.0));
@ -1442,7 +1487,10 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
if (double.IsNaN(clientStats.SPM) || double.IsNaN(clientStats.Skill))
{
_log.LogWarning("clientStats SPM/Skill NaN {@killInfo}",
new {killSPM = killSpm, KDRWeight, totalPlayTime, SPMAgainstPlayWeight, clientStats, scoreDifference});
new
{
killSPM = killSpm, KDRWeight, totalPlayTime, SPMAgainstPlayWeight, clientStats, scoreDifference
});
clientStats.SPM = 0;
clientStats.Skill = 0;
}
@ -1483,11 +1531,11 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
public void ResetKillstreaks(Server sv)
{
foreach (var session in sv.GetClientsAsList()
.Select(_client => new
{
stat = _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY),
detection = _client.GetAdditionalProperty<Detection>(CLIENT_DETECTIONS_KEY)
}))
.Select(_client => new
{
stat = _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY),
detection = _client.GetAdditionalProperty<Detection>(CLIENT_DETECTIONS_KEY)
}))
{
session.stat?.StartNewSession();
session.detection?.OnMapChange();
@ -1549,8 +1597,8 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
await ctx.SaveChangesAsync();
foreach (var stats in sv.GetClientsAsList()
.Select(_client => _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY))
.Where(_stats => _stats != null))
.Select(_client => _client.GetAdditionalProperty<EFClientStatistics>(CLIENT_STATS_KEY))
.Where(_stats => _stats != null))
{
await SaveClientStats(stats);
}

View File

@ -17,7 +17,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -53,7 +53,7 @@ namespace IW4MAdmin.Plugins.Welcome
{
var newPlayer = gameEvent.Origin;
if (newPlayer.Level >= Permission.Trusted && !gameEvent.Origin.Masked||
!string.IsNullOrEmpty(newPlayer.GetAdditionalProperty<string>("ClientTag")) &&
!string.IsNullOrEmpty(newPlayer.Tag) &&
newPlayer.Level != Permission.Flagged && newPlayer.Level != Permission.Banned &&
!newPlayer.Masked)
gameEvent.Owner.Broadcast(
@ -88,7 +88,7 @@ namespace IW4MAdmin.Plugins.Welcome
{
msg = msg.Replace("{{ClientName}}", joining.Name);
msg = msg.Replace("{{ClientLevel}}",
$"{Utilities.ConvertLevelToColor(joining.Level, joining.ClientPermission.Name)}{(string.IsNullOrEmpty(joining.GetAdditionalProperty<string>("ClientTag")) ? "" : $" (Color::White)({joining.GetAdditionalProperty<string>("ClientTag")}(Color::White))")}");
$"{Utilities.ConvertLevelToColor(joining.Level, joining.ClientPermission.Name)}{(string.IsNullOrEmpty(joining.Tag) ? "" : $" (Color::White){joining.Tag}(Color::White)")}");
// this prevents it from trying to evaluate it every message
if (msg.Contains("{{ClientLocation}}"))
{
@ -111,7 +111,7 @@ namespace IW4MAdmin.Plugins.Welcome
try
{
var response =
await wc.GetStringAsync(new Uri($"http://ip-api.com/json/{ip}"));
await wc.GetStringAsync(new Uri($"http://ip-api.com/json/{ip}?lang={Utilities.CurrentLocalization.LocalizationName.Split("-").First().ToLower()}"));
var responseObj = JObject.Parse(response);
response = responseObj["country"]?.ToString();

View File

@ -20,7 +20,7 @@
</Target>
<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
</ItemGroup>
</Project>

View File

@ -4,7 +4,6 @@ using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Data.Context;
using Data.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
@ -25,26 +24,32 @@ namespace SharedLibraryCore
/// <summary>
/// life span in months
/// </summary>
private const int COOKIE_LIFESPAN = 3;
private const int CookieLifespan = 3;
private static readonly byte[] LocalHost = { 127, 0, 0, 1 };
private static string SocialLink;
private static string SocialTitle;
protected readonly DatabaseContext Context;
private static string _socialLink;
private static string _socialTitle;
protected List<Page> Pages;
protected List<string> PermissionsSet;
protected bool Authorized { get; set; }
protected TranslationLookup Localization { get; }
protected EFClient Client { get; }
protected ApplicationConfiguration AppConfig { get; }
public IManager Manager { get; }
public BaseController(IManager manager)
{
AlertManager = manager.AlertManager;
Manager = manager;
Localization ??= Utilities.CurrentLocalization.LocalizationIndex;
Localization = Utilities.CurrentLocalization.LocalizationIndex;
AppConfig = Manager.GetApplicationSettings().Configuration();
if (AppConfig.EnableSocialLink && SocialLink == null)
if (AppConfig.EnableSocialLink && _socialLink == null)
{
SocialLink = AppConfig.SocialLinkAddress;
SocialTitle = AppConfig.SocialLinkTitle;
_socialLink = AppConfig.SocialLinkAddress;
_socialTitle = AppConfig.SocialLinkTitle;
}
Pages = Manager.GetPageList().Pages
@ -59,7 +64,7 @@ namespace SharedLibraryCore
ViewBag.EnableColorCodes = AppConfig.EnableColorCodes;
ViewBag.Language = Utilities.CurrentLocalization.Culture.TwoLetterISOLanguageName;
Client ??= new EFClient
Client = new EFClient
{
ClientId = -1,
Level = Data.Models.Client.EFClient.Permission.Banned,
@ -67,11 +72,7 @@ namespace SharedLibraryCore
};
}
public IManager Manager { get; }
protected bool Authorized { get; set; }
protected TranslationLookup Localization { get; }
protected EFClient Client { get; }
protected ApplicationConfiguration AppConfig { get; }
protected async Task SignInAsync(ClaimsPrincipal claimsPrinciple)
{
@ -79,7 +80,7 @@ namespace SharedLibraryCore
new AuthenticationProperties
{
AllowRefresh = true,
ExpiresUtc = DateTime.UtcNow.AddMonths(COOKIE_LIFESPAN),
ExpiresUtc = DateTime.UtcNow.AddMonths(CookieLifespan),
IsPersistent = true,
IssuedUtc = DateTime.UtcNow
});
@ -99,7 +100,7 @@ namespace SharedLibraryCore
Client.ClientId = clientId;
Client.NetworkId = clientId == 1
? 0
: User.Claims.First(_claim => _claim.Type == ClaimTypes.PrimarySid).Value
: User.Claims.First(claim => claim.Type == ClaimTypes.PrimarySid).Value
.ConvertGuidToLong(NumberStyles.HexNumber);
Client.Level = (Data.Models.Client.EFClient.Permission)Enum.Parse(
typeof(Data.Models.Client.EFClient.Permission),
@ -107,6 +108,9 @@ namespace SharedLibraryCore
Client.CurrentAlias = new EFAlias
{ Name = User.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value };
Authorized = Client.ClientId >= 0;
Client.GameName =
Enum.Parse<Reference.Game>(User.Claims
.First(claim => claim.Type == ClaimTypes.PrimaryGroupSid).Value);
}
}
@ -134,6 +138,7 @@ namespace SharedLibraryCore
new Claim(ClaimTypes.Role, Client.Level.ToString()),
new Claim(ClaimTypes.Sid, Client.ClientId.ToString()),
new Claim(ClaimTypes.PrimarySid, Client.NetworkId.ToString("X")),
new Claim(ClaimTypes.PrimaryGroupSid, Client.GameName.ToString())
};
var claimsIdentity = new ClaimsIdentity(claims, "login");
SignInAsync(new ClaimsPrincipal(claimsIdentity)).Wait();
@ -153,8 +158,8 @@ namespace SharedLibraryCore
ViewBag.Url = AppConfig.WebfrontUrl;
ViewBag.User = Client;
ViewBag.Version = Manager.Version;
ViewBag.SocialLink = SocialLink ?? "";
ViewBag.SocialTitle = SocialTitle;
ViewBag.SocialLink = _socialLink ?? "";
ViewBag.SocialTitle = _socialTitle;
ViewBag.Pages = Pages;
ViewBag.Localization = Utilities.CurrentLocalization.LocalizationIndex;
ViewBag.CustomBranding = shouldUseCommunityName

View File

@ -11,163 +11,180 @@ namespace SharedLibraryCore.Commands
{
public class CommandProcessing
{
public static async Task<Command> ValidateCommand(GameEvent E, ApplicationConfiguration appConfig,
public static async Task<Command> ValidateCommand(GameEvent gameEvent, ApplicationConfiguration appConfig,
CommandConfiguration commandConfig)
{
var loc = Utilities.CurrentLocalization.LocalizationIndex;
var Manager = E.Owner.Manager;
var isBroadcast = E.Data.StartsWith(appConfig.BroadcastCommandPrefix);
var manager = gameEvent.Owner.Manager;
var isBroadcast = gameEvent.Data.StartsWith(appConfig.BroadcastCommandPrefix);
var prefixLength = isBroadcast ? appConfig.BroadcastCommandPrefix.Length : appConfig.CommandPrefix.Length;
var CommandString = E.Data.Substring(prefixLength, E.Data.Length - prefixLength).Split(' ')[0];
E.Message = E.Data;
var commandString =
gameEvent.Data.Substring(prefixLength, gameEvent.Data.Length - prefixLength).Split(' ')[0];
gameEvent.Message = gameEvent.Data;
Command C = null;
foreach (Command cmd in Manager.GetCommands()
Command matchedCommand = null;
foreach (var availableCommand in manager.GetCommands()
.Where(c => c.Name != null))
if (cmd.Name.Equals(CommandString, StringComparison.OrdinalIgnoreCase) ||
(cmd.Alias ?? "").Equals(CommandString, StringComparison.OrdinalIgnoreCase))
{
if ((availableCommand.SupportedGames?.Any() ?? false) &&
!availableCommand.SupportedGames.Contains(gameEvent.Owner.GameName))
{
C = cmd;
continue;
}
if (C == null)
{
E.Origin.Tell(loc["COMMAND_UNKNOWN"]);
throw new CommandException($"{E.Origin} entered unknown command \"{CommandString}\"");
}
C.IsBroadcast = isBroadcast;
var allowImpersonation = commandConfig?.Commands?.ContainsKey(C.GetType().Name) ?? false
? commandConfig.Commands[C.GetType().Name].AllowImpersonation
: C.AllowImpersonation;
if (!allowImpersonation && E.ImpersonationOrigin != null)
{
E.ImpersonationOrigin.Tell(loc["COMMANDS_RUN_AS_FAIL"]);
throw new CommandException($"Command {C.Name} cannot be run as another client");
}
E.Data = E.Data.RemoveWords(1);
var Args = E.Data.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
// todo: the code below can be cleaned up
if (E.Origin.Level < C.Permission)
{
E.Origin.Tell(loc["COMMAND_NOACCESS"]);
throw new CommandException($"{E.Origin} does not have access to \"{C.Name}\"");
}
if (Args.Length < C.RequiredArgumentCount)
{
E.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
E.Origin.Tell(C.Syntax);
throw new CommandException($"{E.Origin} did not supply enough arguments for \"{C.Name}\"");
}
if (C.RequiresTarget)
{
if (Args.Length > 0)
if (availableCommand.Name.Equals(commandString, StringComparison.OrdinalIgnoreCase) ||
(availableCommand.Alias ?? "").Equals(commandString, StringComparison.OrdinalIgnoreCase))
{
if (!int.TryParse(Args[0], out var cNum))
matchedCommand = (Command)availableCommand;
}
}
if (matchedCommand == null)
{
gameEvent.Origin.Tell(loc["COMMAND_UNKNOWN"]);
throw new CommandException($"{gameEvent.Origin} entered unknown command \"{commandString}\"");
}
matchedCommand.IsBroadcast = isBroadcast;
var allowImpersonation = commandConfig?.Commands?.ContainsKey(matchedCommand.GetType().Name) ?? false
? commandConfig.Commands[matchedCommand.GetType().Name].AllowImpersonation
: matchedCommand.AllowImpersonation;
if (!allowImpersonation && gameEvent.ImpersonationOrigin != null)
{
gameEvent.ImpersonationOrigin.Tell(loc["COMMANDS_RUN_AS_FAIL"]);
throw new CommandException($"Command {matchedCommand.Name} cannot be run as another client");
}
gameEvent.Data = gameEvent.Data.RemoveWords(1);
var args = gameEvent.Data.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
// todo: the code below can be cleaned up
if (gameEvent.Origin.Level < matchedCommand.Permission)
{
gameEvent.Origin.Tell(loc["COMMAND_NOACCESS"]);
throw new CommandException($"{gameEvent.Origin} does not have access to \"{matchedCommand.Name}\"");
}
if (args.Length < matchedCommand.RequiredArgumentCount)
{
gameEvent.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
gameEvent.Origin.Tell(matchedCommand.Syntax);
throw new CommandException(
$"{gameEvent.Origin} did not supply enough arguments for \"{matchedCommand.Name}\"");
}
if (matchedCommand.RequiresTarget)
{
if (args.Length > 0)
{
if (!int.TryParse(args[0], out var cNum))
{
cNum = -1;
}
if (Args[0][0] == '@') // user specifying target by database ID
if (args[0][0] == '@') // user specifying target by database ID
{
int.TryParse(Args[0].Substring(1, Args[0].Length - 1), out var dbID);
int.TryParse(args[0].Substring(1, args[0].Length - 1), out var dbID);
var found = await Manager.GetClientService().Get(dbID);
var found = await manager.GetClientService().Get(dbID);
if (found != null)
{
found = Manager.FindActiveClient(found);
E.Target = found;
E.Target.CurrentServer = found.CurrentServer ?? E.Owner;
E.Data = string.Join(" ", Args.Skip(1));
found = manager.FindActiveClient(found);
gameEvent.Target = found;
gameEvent.Target.CurrentServer = found.CurrentServer ?? gameEvent.Owner;
gameEvent.Data = string.Join(" ", args.Skip(1));
}
}
else if (Args[0].Length < 3 && cNum > -1 && cNum < E.Owner.MaxClients
else if (args[0].Length < 3 && cNum > -1 && cNum < gameEvent.Owner.MaxClients
) // user specifying target by client num
{
if (E.Owner.Clients[cNum] != null)
if (gameEvent.Owner.Clients[cNum] != null)
{
E.Target = E.Owner.Clients[cNum];
E.Data = string.Join(" ", Args.Skip(1));
gameEvent.Target = gameEvent.Owner.Clients[cNum];
gameEvent.Data = string.Join(" ", args.Skip(1));
}
}
}
List<EFClient> matchingPlayers;
if (E.Target == null && C.RequiresTarget) // Find active player including quotes (multiple words)
if (gameEvent.Target == null &&
matchedCommand.RequiresTarget) // Find active player including quotes (multiple words)
{
matchingPlayers = E.Owner.GetClientByName(E.Data);
matchingPlayers = gameEvent.Owner.GetClientByName(gameEvent.Data);
if (matchingPlayers.Count > 1)
{
E.Origin.Tell(loc["COMMAND_TARGET_MULTI"]);
throw new CommandException($"{E.Origin} had multiple players found for {C.Name}");
gameEvent.Origin.Tell(loc["COMMAND_TARGET_MULTI"]);
throw new CommandException(
$"{gameEvent.Origin} had multiple players found for {matchedCommand.Name}");
}
if (matchingPlayers.Count == 1)
{
E.Target = matchingPlayers.First();
gameEvent.Target = matchingPlayers.First();
var escapedName = Regex.Escape(E.Target.CleanedName);
var escapedName = Regex.Escape(gameEvent.Target.CleanedName);
var reg = new Regex($"(\"{escapedName}\")|({escapedName})", RegexOptions.IgnoreCase);
E.Data = reg.Replace(E.Data, "", 1).Trim();
gameEvent.Data = reg.Replace(gameEvent.Data, "", 1).Trim();
if (E.Data.Length == 0 && C.RequiredArgumentCount > 1)
if (gameEvent.Data.Length == 0 && matchedCommand.RequiredArgumentCount > 1)
{
E.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
E.Origin.Tell(C.Syntax);
throw new CommandException($"{E.Origin} did not supply enough arguments for \"{C.Name}\"");
gameEvent.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
gameEvent.Origin.Tell(matchedCommand.Syntax);
throw new CommandException(
$"{gameEvent.Origin} did not supply enough arguments for \"{matchedCommand.Name}\"");
}
}
}
if (E.Target == null && C.RequiresTarget && Args.Length > 0) // Find active player as single word
if (gameEvent.Target == null && matchedCommand.RequiresTarget &&
args.Length > 0) // Find active player as single word
{
matchingPlayers = E.Owner.GetClientByName(Args[0]);
matchingPlayers = gameEvent.Owner.GetClientByName(args[0]);
if (matchingPlayers.Count > 1)
{
E.Origin.Tell(loc["COMMAND_TARGET_MULTI"]);
gameEvent.Origin.Tell(loc["COMMAND_TARGET_MULTI"]);
foreach (var p in matchingPlayers)
E.Origin.Tell($"[(Color::Yellow){p.ClientNumber}(Color::White)] {p.Name}");
throw new CommandException($"{E.Origin} had multiple players found for {C.Name}");
gameEvent.Origin.Tell($"[(Color::Yellow){p.ClientNumber}(Color::White)] {p.Name}");
throw new CommandException(
$"{gameEvent.Origin} had multiple players found for {matchedCommand.Name}");
}
if (matchingPlayers.Count == 1)
{
E.Target = matchingPlayers.First();
gameEvent.Target = matchingPlayers.First();
var escapedName = Regex.Escape(E.Target.CleanedName);
var escapedArg = Regex.Escape(Args[0]);
var escapedName = Regex.Escape(gameEvent.Target.CleanedName);
var escapedArg = Regex.Escape(args[0]);
var reg = new Regex($"({escapedName})|({escapedArg})", RegexOptions.IgnoreCase);
E.Data = reg.Replace(E.Data, "", 1).Trim();
gameEvent.Data = reg.Replace(gameEvent.Data, "", 1).Trim();
if ((E.Data.Trim() == E.Target.CleanedName.ToLower().Trim() ||
E.Data == string.Empty) &&
C.RequiresTarget)
if ((gameEvent.Data.Trim() == gameEvent.Target.CleanedName.ToLower().Trim() ||
gameEvent.Data == string.Empty) &&
matchedCommand.RequiresTarget)
{
E.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
E.Origin.Tell(C.Syntax);
throw new CommandException($"{E.Origin} did not supply enough arguments for \"{C.Name}\"");
gameEvent.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
gameEvent.Origin.Tell(matchedCommand.Syntax);
throw new CommandException(
$"{gameEvent.Origin} did not supply enough arguments for \"{matchedCommand.Name}\"");
}
}
}
if (E.Target == null && C.RequiresTarget)
if (gameEvent.Target == null && matchedCommand.RequiresTarget)
{
E.Origin.Tell(loc["COMMAND_TARGET_NOTFOUND"]);
throw new CommandException($"{E.Origin} specified invalid player for \"{C.Name}\"");
gameEvent.Origin.Tell(loc["COMMAND_TARGET_NOTFOUND"]);
throw new CommandException(
$"{gameEvent.Origin} specified invalid player for \"{matchedCommand.Name}\"");
}
}
E.Data = E.Data.Trim();
return C;
gameEvent.Data = gameEvent.Data.Trim();
return matchedCommand;
}
}
}
}

View File

@ -381,7 +381,7 @@ namespace SharedLibraryCore.Commands
{
// todo: don't do the lookup here
var penalties = await gameEvent.Owner.Manager.GetPenaltyService().GetActivePenaltiesAsync(gameEvent.Target.AliasLinkId,
gameEvent.Target.CurrentAliasId, gameEvent.Target.NetworkId, gameEvent.Target.CurrentAlias.IPAddress);
gameEvent.Target.CurrentAliasId, gameEvent.Target.NetworkId, gameEvent.Target.GameName, gameEvent.Target.CurrentAlias.IPAddress);
if (penalties
.FirstOrDefault(p =>
@ -897,7 +897,7 @@ namespace SharedLibraryCore.Commands
public override async Task ExecuteAsync(GameEvent E)
{
var existingPenalties = await E.Owner.Manager.GetPenaltyService()
.GetActivePenaltiesAsync(E.Target.AliasLinkId, E.Target.CurrentAliasId, E.Target.NetworkId, E.Target.IPAddress);
.GetActivePenaltiesAsync(E.Target.AliasLinkId, E.Target.CurrentAliasId, E.Target.NetworkId, E.Target.GameName, E.Target.IPAddress);
var penalty = existingPenalties.FirstOrDefault(b => b.Type > EFPenalty.PenaltyType.Kick);
if (penalty == null)

View File

@ -1,6 +1,7 @@
using System.Threading.Tasks;
using Data.Models.Client;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
namespace SharedLibraryCore.Commands
@ -19,13 +20,16 @@ namespace SharedLibraryCore.Commands
RequiresTarget = false;
}
public override Task ExecuteAsync(GameEvent E)
public override Task ExecuteAsync(GameEvent gameEvent)
{
var state = E.Owner.Manager.TokenAuthenticator.GenerateNextToken(E.Origin.NetworkId);
E.Origin.Tell(string.Format(_translationLookup["COMMANDS_GENERATETOKEN_SUCCESS"], state.Token,
$"{state.RemainingTime} {_translationLookup["GLOBAL_MINUTES"]}", E.Origin.ClientId));
var state = gameEvent.Owner.Manager.TokenAuthenticator.GenerateNextToken(new TokenIdentifier
{
ClientId = gameEvent.Origin.ClientId
});
gameEvent.Origin.Tell(string.Format(_translationLookup["COMMANDS_GENERATETOKEN_SUCCESS"], state.Token,
$"{state.RemainingTime} {_translationLookup["GLOBAL_MINUTES"]}", gameEvent.Origin.ClientId));
return Task.CompletedTask;
}
}
}
}

View File

@ -1,4 +1,5 @@
using Newtonsoft.Json;
using System;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using static Data.Models.Client.EFClient;
using static SharedLibraryCore.Server;
@ -35,6 +36,6 @@ namespace SharedLibraryCore.Configuration
/// Specifies the games supporting the functionality of the command
/// </summary>
[JsonProperty(ItemConverterType = typeof(StringEnumConverter))]
public Game[] SupportedGames { get; set; } = new Game[0];
public Game[] SupportedGames { get; set; } = Array.Empty<Game>();
}
}
}

View File

@ -0,0 +1,13 @@
using System;
using System.Text.Json.Serialization;
namespace SharedLibraryCore.Dtos.Meta.Responses;
public class ClientNoteMetaResponse
{
public string Note { get; set; }
public int OriginEntityId { get; set; }
[JsonIgnore]
public string OriginEntityName { get; set; }
public DateTime ModifiedDate { get; set; }
}

View File

@ -33,5 +33,6 @@ namespace SharedLibraryCore.Dtos
public string ConnectProtocolUrl { get;set; }
public string CurrentServerName { get; set; }
public IGeoLocationResult GeoLocationInfo { get; set; }
public ClientNoteMetaResponse NoteMeta { get; set; }
}
}

View File

@ -204,6 +204,11 @@ namespace SharedLibraryCore
/// client logged out of webfront
/// </summary>
Logout = 113,
/// <summary>
/// meta value updated on client
/// </summary>
MetaUpdated = 114,
// events "generated" by IW4MAdmin
/// <summary>

View File

@ -0,0 +1,9 @@
using SharedLibraryCore.Interfaces;
namespace SharedLibraryCore.Helpers;
public class TokenIdentifier : ITokenIdentifier
{
public int ClientId { get; set; }
public string Token { get; set; }
}

View File

@ -4,7 +4,6 @@ namespace SharedLibraryCore.Helpers
{
public sealed class TokenState
{
public long NetworkId { get; set; }
public DateTime RequestTime { get; set; } = DateTime.Now;
public TimeSpan TokenDuration { get; set; }
public string Token { get; set; }
@ -12,4 +11,4 @@ namespace SharedLibraryCore.Helpers
public string RemainingTime => Math.Round(-(DateTime.Now - RequestTime).Subtract(TokenDuration).TotalMinutes, 1)
.ToString();
}
}
}

View File

@ -10,7 +10,7 @@ namespace SharedLibraryCore.Interfaces
Task<T> Delete(T entity);
Task<T> Update(T entity);
Task<T> Get(int entityID);
Task<T> GetUnique(long entityProperty);
Task<T> GetUnique(long entityProperty, object altKey);
Task<IList<T>> Find(Func<T, bool> expression);
}
}
}

View File

@ -110,6 +110,6 @@ namespace SharedLibraryCore.Interfaces
/// </summary>
/// <param name="command">name of command being executed</param>
/// <returns></returns>
TimeSpan OverrideTimeoutForCommand(string command);
TimeSpan? OverrideTimeoutForCommand(string command);
}
}

View File

@ -74,6 +74,11 @@ namespace SharedLibraryCore.Interfaces
/// </summary>
IDictionary<string, string> DefaultDvarValues { get; }
/// <summary>
/// contains a setup of commands that have override timeouts
/// </summary>
IDictionary<string, int?> OverrideCommandTimeouts { get; }
/// <summary>
/// specifies how many lines can be used for ingame notice
/// </summary>
@ -100,7 +105,11 @@ namespace SharedLibraryCore.Interfaces
string DefaultInstallationDirectoryHint { get; }
ColorCodeMapping ColorCodeMapping { get; }
short FloodProtectInterval { get; }
/// <summary>
/// indicates if diacritics (accented characters) should be normalized
/// </summary>
bool ShouldRemoveDiacritics { get; }
}
}

View File

@ -7,16 +7,15 @@ namespace SharedLibraryCore.Interfaces
/// <summary>
/// generates and returns a token for the given network id
/// </summary>
/// <param name="networkId">network id of the players to generate the token for</param>
/// <param name="authInfo">auth information for next token generation</param>
/// <returns>4 character string token</returns>
TokenState GenerateNextToken(long networkId);
TokenState GenerateNextToken(ITokenIdentifier authInfo);
/// <summary>
/// authorizes given token
/// </summary>
/// <param name="networkId">network id of the client to authorize</param>
/// <param name="token">token to authorize</param>
/// <param name="authInfo">auth information</param>
/// <returns>true if token authorized successfully, false otherwise</returns>
bool AuthorizeToken(long networkId, string token);
bool AuthorizeToken(ITokenIdentifier authInfo);
}
}

View File

@ -0,0 +1,7 @@
namespace SharedLibraryCore.Interfaces;
public interface ITokenIdentifier
{
int ClientId { get; }
string Token { get; }
}

View File

@ -76,7 +76,7 @@ namespace SharedLibraryCore.Database.Models
[NotMapped]
public virtual int? IPAddress
{
get => CurrentAlias.IPAddress;
get => CurrentAlias?.IPAddress;
set => CurrentAlias.IPAddress = value;
}
@ -100,7 +100,10 @@ namespace SharedLibraryCore.Database.Models
[NotMapped] public int Score { get; set; }
[NotMapped] public bool IsBot => NetworkId == Name.GenerateGuidFromString() || IPAddressString == System.Net.IPAddress.Broadcast.ToString();
[NotMapped]
public bool IsBot => NetworkId == Name.GenerateGuidFromString() ||
IPAddressString == System.Net.IPAddress.Broadcast.ToString() ||
IPAddressString == "unknown";
[NotMapped] public bool IsZombieClient => IsBot && Name == "Zombie";
@ -170,7 +173,7 @@ namespace SharedLibraryCore.Database.Models
?.CorrelationId ?? Guid.NewGuid()
};
e.Output.Add(message.FormatMessageForEngine(CurrentServer?.RconParser.Configuration.ColorCodeMapping)
e.Output.Add(message.FormatMessageForEngine(CurrentServer?.RconParser.Configuration)
.StripColors());
CurrentServer?.Manager.AddEvent(e);
@ -682,7 +685,7 @@ namespace SharedLibraryCore.Database.Models
// we want to get any penalties that are tied to their IP or AliasLink (but not necessarily their GUID)
var activePenalties = await CurrentServer.Manager.GetPenaltyService()
.GetActivePenaltiesAsync(AliasLinkId, CurrentAliasId, NetworkId, ipAddress);
.GetActivePenaltiesAsync(AliasLinkId, CurrentAliasId, NetworkId, GameName, ipAddress);
var banPenalty = activePenalties.FirstOrDefault(_penalty => _penalty.Type == EFPenalty.PenaltyType.Ban);
var tempbanPenalty =
activePenalties.FirstOrDefault(_penalty => _penalty.Type == EFPenalty.PenaltyType.TempBan);

View File

@ -225,7 +225,7 @@ namespace SharedLibraryCore
var formattedMessage = string.Format(RconParser.Configuration.CommandPrefixes.Say ?? "",
$"{(CustomSayEnabled && GameName == Game.IW4 ? $"{CustomSayName}: " : "")}{message}");
ServerLogger.LogDebug("All-> {Message}",
message.FormatMessageForEngine(RconParser.Configuration.ColorCodeMapping).StripColors());
message.FormatMessageForEngine(RconParser.Configuration).StripColors());
var e = new GameEvent
{
@ -289,13 +289,13 @@ namespace SharedLibraryCore
else
{
ServerLogger.LogDebug("Tell[{ClientNumber}]->{Message}", targetClient.ClientNumber,
message.FormatMessageForEngine(RconParser.Configuration.ColorCodeMapping).StripColors());
message.FormatMessageForEngine(RconParser.Configuration).StripColors());
}
if (targetClient.Level == Data.Models.Client.EFClient.Permission.Console)
{
Console.ForegroundColor = ConsoleColor.Green;
var cleanMessage = message.FormatMessageForEngine(RconParser.Configuration.ColorCodeMapping)
var cleanMessage = message.FormatMessageForEngine(RconParser.Configuration)
.StripColors();
using (LogContext.PushProperty("Server", ToString()))
{

View File

@ -23,25 +23,26 @@ namespace SharedLibraryCore.Services
{
public class ClientService : IEntityService<EFClient>, IResourceQueryHelper<FindClientRequest, FindClientResult>
{
private static readonly Func<DatabaseContext, long, Task<EFClient>> _getUniqueQuery =
EF.CompileAsyncQuery((DatabaseContext context, long networkId) =>
private static readonly Func<DatabaseContext, long, Reference.Game, Task<EFClient>> GetUniqueQuery =
EF.CompileAsyncQuery((DatabaseContext context, long networkId, Reference.Game game) =>
context.Clients
.Select(_client => new EFClient
.Select(client => new EFClient
{
ClientId = _client.ClientId,
AliasLinkId = _client.AliasLinkId,
Level = _client.Level,
Connections = _client.Connections,
FirstConnection = _client.FirstConnection,
LastConnection = _client.LastConnection,
Masked = _client.Masked,
NetworkId = _client.NetworkId,
TotalConnectionTime = _client.TotalConnectionTime,
AliasLink = _client.AliasLink,
Password = _client.Password,
PasswordSalt = _client.PasswordSalt
ClientId = client.ClientId,
AliasLinkId = client.AliasLinkId,
Level = client.Level,
Connections = client.Connections,
FirstConnection = client.FirstConnection,
LastConnection = client.LastConnection,
Masked = client.Masked,
NetworkId = client.NetworkId,
TotalConnectionTime = client.TotalConnectionTime,
AliasLink = client.AliasLink,
Password = client.Password,
PasswordSalt = client.PasswordSalt,
GameName = client.GameName
})
.FirstOrDefault(c => c.NetworkId == networkId)
.FirstOrDefault(client => client.NetworkId == networkId && client.GameName == game)
);
private readonly ApplicationConfiguration _appConfig;
@ -235,10 +236,10 @@ namespace SharedLibraryCore.Services
return foundClient.Client;
}
public virtual async Task<EFClient> GetUnique(long entityAttribute)
public virtual async Task<EFClient> GetUnique(long entityAttribute, object altKey = null)
{
await using var context = _contextFactory.CreateContext(false);
return await _getUniqueQuery(context, entityAttribute);
return await GetUniqueQuery(context, entityAttribute, (Reference.Game)altKey);
}
public async Task<EFClient> Update(EFClient temporalClient)
@ -285,7 +286,7 @@ namespace SharedLibraryCore.Services
entity.PasswordSalt = temporalClient.PasswordSalt;
}
entity.GameName ??= temporalClient.GameName;
entity.GameName = temporalClient.GameName;
// update in database
await context.SaveChangesAsync();
@ -758,19 +759,20 @@ namespace SharedLibraryCore.Services
{
await using var context = _contextFactory.CreateContext(false);
return await context.Clients
.Select(_client => new EFClient
.Select(client => new EFClient
{
NetworkId = _client.NetworkId,
ClientId = _client.ClientId,
NetworkId = client.NetworkId,
ClientId = client.ClientId,
CurrentAlias = new EFAlias
{
Name = _client.CurrentAlias.Name
Name = client.CurrentAlias.Name
},
Password = _client.Password,
PasswordSalt = _client.PasswordSalt,
Level = _client.Level
Password = client.Password,
PasswordSalt = client.PasswordSalt,
GameName = client.GameName,
Level = client.Level
})
.FirstAsync(_client => _client.ClientId == clientId);
.FirstAsync(client => client.ClientId == clientId);
}
public async Task<List<EFClient>> GetPrivilegedClients(bool includeName = true)
@ -851,31 +853,35 @@ namespace SharedLibraryCore.Services
else
{
iqClients = iqClients.Where(_client => networkId == _client.NetworkId ||
linkIds.Contains(_client.AliasLinkId)
|| !_appConfig.EnableImplicitAccountLinking &&
_client.CurrentAlias.IPAddress != null &&
_client.CurrentAlias.IPAddress == ipAddress);
iqClients = iqClients.Where(client => networkId == client.NetworkId || linkIds.Contains(client.AliasLinkId));
}
if (ipAddress is not null && !_appConfig.EnableImplicitAccountLinking)
{
iqClients = iqClients.Union(context.Clients.Where(client => client.CurrentAlias.IPAddress == ipAddress));
}
// we want to project our results
var iqClientProjection = iqClients.OrderByDescending(_client => _client.LastConnection)
.Select(_client => new PlayerInfo
var iqClientProjection = iqClients.OrderByDescending(client => client.LastConnection)
.Select(client => new PlayerInfo
{
Name = _client.CurrentAlias.Name,
LevelInt = (int)_client.Level,
LastConnection = _client.LastConnection,
ClientId = _client.ClientId,
IPAddress = _client.CurrentAlias.IPAddress.HasValue
? _client.CurrentAlias.SearchableIPAddress
: ""
Name = client.CurrentAlias.Name,
LevelInt = (int)client.Level,
LastConnection = client.LastConnection,
ClientId = client.ClientId,
IPAddress = client.CurrentAlias.IPAddress.HasValue
? client.CurrentAlias.SearchableIPAddress
: "",
Game = client.GameName
});
var clients = await iqClientProjection.ToListAsync();
// this is so we don't try to evaluate this in the linq to entities query
foreach (var client in clients)
{
client.Level = ((Permission)client.LevelInt).ToLocalizedLevelName();
}
return clients;
}
@ -932,6 +938,14 @@ namespace SharedLibraryCore.Services
return clientList;
}
public async Task<string> GetClientNameById(int clientId)
{
await using var context = _contextFactory.CreateContext();
var match = await context.Clients.Select(client => new { client.CurrentAlias.Name, client.ClientId })
.FirstOrDefaultAsync(client => client.ClientId == clientId);
return match?.Name;
}
#endregion
}
}

View File

@ -88,7 +88,7 @@ namespace SharedLibraryCore.Services
throw new NotImplementedException();
}
public Task<EFPenalty> GetUnique(long entityProperty)
public Task<EFPenalty> GetUnique(long entityProperty, object altKey)
{
throw new NotImplementedException();
}
@ -139,10 +139,10 @@ namespace SharedLibraryCore.Services
LinkedPenalties.Contains(pi.Penalty.Type) && pi.Penalty.Active &&
(pi.Penalty.Expires == null || pi.Penalty.Expires > DateTime.UtcNow);
public async Task<List<EFPenalty>> GetActivePenaltiesAsync(int linkId, int currentAliasId, long networkId,
public async Task<List<EFPenalty>> GetActivePenaltiesAsync(int linkId, int currentAliasId, long networkId, Reference.Game game,
int? ip = null)
{
var penaltiesByIdentifier = await GetActivePenaltiesByIdentifier(ip, networkId);
var penaltiesByIdentifier = await GetActivePenaltiesByIdentifier(ip, networkId, game);
if (penaltiesByIdentifier.Any())
{
@ -183,16 +183,16 @@ namespace SharedLibraryCore.Services
return activePenalties.OrderByDescending(p => p.When).ToList();
}
public async Task<List<EFPenalty>> GetActivePenaltiesByIdentifier(int? ip, long networkId)
public async Task<List<EFPenalty>> GetActivePenaltiesByIdentifier(int? ip, long networkId, Reference.Game game)
{
await using var context = _contextFactory.CreateContext(false);
var activePenaltiesIds = context.PenaltyIdentifiers.Where(identifier =>
identifier.IPv4Address != null && identifier.IPv4Address == ip || identifier.NetworkId == networkId)
identifier.IPv4Address != null && identifier.IPv4Address == ip || identifier.NetworkId == networkId && identifier.Penalty.Offender.GameName == game)
.Where(FilterById);
return await activePenaltiesIds.Select(ids => ids.Penalty).ToListAsync();
}
public async Task<List<EFPenalty>> ActivePenaltiesByRecentIdentifiers(int linkId)
{
await using var context = _contextFactory.CreateContext(false);
@ -214,12 +214,12 @@ namespace SharedLibraryCore.Services
return await activePenaltiesIds.Select(ids => ids.Penalty).ToListAsync();
}
public virtual async Task RemoveActivePenalties(int aliasLinkId, long networkId, int? ipAddress = null)
public virtual async Task RemoveActivePenalties(int aliasLinkId, long networkId, Reference.Game game, int? ipAddress = null)
{
await using var context = _contextFactory.CreateContext();
var now = DateTime.UtcNow;
var activePenalties = await GetActivePenaltiesByIdentifier(ipAddress, networkId);
var activePenalties = await GetActivePenaltiesByIdentifier(ipAddress, networkId, game);
if (activePenalties.Any())
{

View File

@ -4,7 +4,7 @@
<OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework>
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
<Version>2022.6.9.1</Version>
<Version>2022.6.16.1</Version>
<Authors>RaidMax</Authors>
<Company>Forever None</Company>
<Configurations>Debug;Release;Prerelease</Configurations>
@ -19,7 +19,7 @@
<IsPackable>true</IsPackable>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<Description>Shared Library for IW4MAdmin</Description>
<PackageVersion>2022.6.9.1</PackageVersion>
<PackageVersion>2022.6.16.1</PackageVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

View File

@ -71,31 +71,7 @@ namespace SharedLibraryCore
/// </summary>
public const long WORLD_ID = -1;
public static Dictionary<Permission, string> PermissionLevelOverrides { get; } =
new Dictionary<Permission, string>();
public static string HttpRequest(string location, string header, string headerValue)
{
using (var RequestClient = new HttpClient())
{
RequestClient.DefaultRequestHeaders.Add(header, headerValue);
var response = RequestClient.GetStringAsync(location).Result;
return response;
}
}
//Get string with specified number of spaces -- really only for visual output
public static string GetSpaces(int Num)
{
var SpaceString = string.Empty;
while (Num > 0)
{
SpaceString += ' ';
Num--;
}
return SpaceString;
}
public static Dictionary<Permission, string> PermissionLevelOverrides { get; } = new ();
//Remove words from a space delimited string
public static string RemoveWords(this string str, int num)
@ -133,12 +109,12 @@ namespace SharedLibraryCore
{
var lookingFor = str.ToLower();
for (var Perm = Permission.User; Perm < Permission.Console; Perm++)
if (lookingFor.Contains(Perm.ToString().ToLower())
for (var perm = Permission.User; perm < Permission.Console; perm++)
if (lookingFor.Contains(perm.ToString().ToLower())
|| lookingFor.Contains(CurrentLocalization
.LocalizationIndex[$"GLOBAL_PERMISSION_{Perm.ToString().ToUpper()}"].ToLower()))
.LocalizationIndex[$"GLOBAL_PERMISSION_{perm.ToString().ToUpper()}"].ToLower()))
{
return Perm;
return perm;
}
return Permission.Banned;
@ -171,9 +147,25 @@ namespace SharedLibraryCore
return str.Replace("//", "/ /");
}
public static string FormatMessageForEngine(this string str, ColorCodeMapping mapping)
public static string RemoveDiacritics(this string text)
{
if (mapping == null || string.IsNullOrEmpty(str))
var normalizedString = text.Normalize(NormalizationForm.FormD);
var stringBuilder = new StringBuilder();
foreach (var c in from c in normalizedString.EnumerateRunes()
let unicodeCategory = Rune.GetUnicodeCategory(c)
where unicodeCategory != UnicodeCategory.NonSpacingMark
select c)
{
stringBuilder.Append(c);
}
return stringBuilder.ToString().Normalize(NormalizationForm.FormC);
}
public static string FormatMessageForEngine(this string str, IRConParserConfiguration config)
{
if (config == null || string.IsNullOrEmpty(str))
{
return str;
}
@ -184,13 +176,19 @@ namespace SharedLibraryCore
foreach (var match in colorCodeMatches.Where(m => m.Success))
{
var key = match.Groups[1].ToString();
output = output.Replace(match.Value, mapping.TryGetValue(key, out var code) ? code : "");
output = output.Replace(match.Value, config.ColorCodeMapping.TryGetValue(key, out var code) ? code : "");
}
if (config.ShouldRemoveDiacritics)
{
output = output.RemoveDiacritics();
}
return output.FixIW4ForwardSlash();
}
private static readonly IList<string> _zmGameTypes = new[] { "zclassic", "zstandard", "zcleansed", "zgrief" };
private static readonly IList<string> ZmGameTypes = new[]
{ "zclassic", "zstandard", "zcleansed", "zgrief", "zom", "cmp" };
/// <summary>
/// indicates if the given server is running a zombie game mode
@ -199,7 +197,8 @@ namespace SharedLibraryCore
/// <returns></returns>
public static bool IsZombieServer(this Server server)
{
return server.GameName == Game.T6 && _zmGameTypes.Contains(server.Gametype.ToLower());
return new[] { Game.T4, Game.T5, Game.T6 }.Contains(server.GameName) &&
ZmGameTypes.Contains(server.Gametype.ToLower());
}
public static bool IsCodGame(this Server server)
@ -233,7 +232,7 @@ namespace SharedLibraryCore
{
var localized =
CurrentLocalization.LocalizationIndex[$"GLOBAL_PERMISSION_{permission.ToString().ToUpper()}"];
return PermissionLevelOverrides.ContainsKey(permission) && PermissionLevelOverrides[permission] != localized
return PermissionLevelOverrides.ContainsKey(permission) && PermissionLevelOverrides[permission] != permission.ToString()
? PermissionLevelOverrides[permission]
: localized;
}
@ -262,11 +261,6 @@ namespace SharedLibraryCore
return str.StartsWith(broadcastCommandPrefix);
}
public static IManagerCommand AsCommand(this GameEvent gameEvent)
{
return gameEvent.Extra as IManagerCommand;
}
/// <summary>
/// Get the full gametype name
/// </summary>
@ -433,7 +427,7 @@ namespace SharedLibraryCore
{
var success = IPAddress.TryParse(str, out var ip);
return success && ip.GetAddressBytes().Count(_byte => _byte == 0) != 4
? (int?)BitConverter.ToInt32(ip.GetAddressBytes(), 0)
? BitConverter.ToInt32(ip.GetAddressBytes(), 0)
: null;
}
@ -482,11 +476,6 @@ namespace SharedLibraryCore
return Game.UKN;
}
public static string EscapeMarkdown(this string markdownString)
{
return markdownString.Replace("<", "\\<").Replace(">", "\\>").Replace("|", "\\|");
}
public static TimeSpan ParseTimespan(this string input)
{
var expressionMatch = Regex.Match(input, @"([0-9]+)(\w+)");
@ -608,7 +597,7 @@ namespace SharedLibraryCore
public static bool PromptBool(this string question, string description = null, bool defaultValue = true)
{
Console.Write($"{question}?{(string.IsNullOrEmpty(description) ? " " : $" ({description}) ")}[y/n]: ");
var response = Console.ReadLine().ToLower().FirstOrDefault();
var response = Console.ReadLine()?.ToLower().FirstOrDefault();
return response != 0 ? response == 'y' : defaultValue;
}
@ -639,7 +628,7 @@ namespace SharedLibraryCore
Console.WriteLine(new string('=', 52));
var selectionIndex = PromptInt(CurrentLocalization.LocalizationIndex["SETUP_PROMPT_MAKE_SELECTION"], null,
hasDefault ? 0 : 1, selections.Length, hasDefault ? 0 : (int?)null);
hasDefault ? 0 : 1, selections.Length, hasDefault ? 0 : null);
if (!hasDefault)
{
@ -667,13 +656,13 @@ namespace SharedLibraryCore
$"{question}{(string.IsNullOrEmpty(description) ? "" : $" ({description})")}{(defaultValue == null ? "" : $" [{CurrentLocalization.LocalizationIndex["SETUP_PROMPT_DEFAULT"]} {defaultValue.Value.ToString()}]")}: ");
int response;
string inputOrDefault()
string InputOrDefault()
{
var input = Console.ReadLine();
return string.IsNullOrEmpty(input) && defaultValue != null ? defaultValue.ToString() : input;
}
while (!int.TryParse(inputOrDefault(), out response) ||
while (!int.TryParse(InputOrDefault(), out response) ||
response < minValue ||
response > maxValue)
{
@ -698,7 +687,7 @@ namespace SharedLibraryCore
/// <returns></returns>
public static string PromptString(this string question, string description = null, string defaultValue = null)
{
string inputOrDefault()
string InputOrDefault()
{
var input = Console.ReadLine();
return string.IsNullOrEmpty(input) && defaultValue != null ? defaultValue : input;
@ -709,7 +698,7 @@ namespace SharedLibraryCore
{
Console.Write(
$"{question}{(string.IsNullOrEmpty(description) ? "" : $" ({description})")}{(defaultValue == null ? "" : $" [{CurrentLocalization.LocalizationIndex["SETUP_PROMPT_DEFAULT"]} {defaultValue}]")}: ");
response = inputOrDefault();
response = InputOrDefault();
} while (string.IsNullOrWhiteSpace(response) && response != defaultValue);
return response;
@ -1181,7 +1170,8 @@ namespace SharedLibraryCore
Meta = client.Meta,
ReceivedPenalties = client.ReceivedPenalties,
AdministeredPenalties = client.AdministeredPenalties,
Active = client.Active
Active = client.Active,
GameName = client.GameName
};
}
@ -1264,5 +1254,8 @@ namespace SharedLibraryCore
return allRules[index];
}
public static string MakeAbbreviation(string gameName) => string.Join("",
gameName.Split(' ').Select(word => char.ToUpper(word.First())).ToArray());
}
}

View File

@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Services;
using WebfrontCore.Controllers.API.Dtos;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -100,9 +101,15 @@ namespace WebfrontCore.Controllers.API
if (!Authorized)
{
var tokenData = new TokenIdentifier
{
ClientId = clientId,
Token = request.Password
};
loginSuccess =
Manager.TokenAuthenticator.AuthorizeToken(privilegedClient.NetworkId, request.Password) ||
(await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(request.Password,
Manager.TokenAuthenticator.AuthorizeToken(tokenData) ||
(await Task.FromResult(Hashing.Hash(request.Password,
privilegedClient.PasswordSalt)))[0] == privilegedClient.Password;
}
@ -120,7 +127,7 @@ namespace WebfrontCore.Controllers.API
var claimsPrinciple = new ClaimsPrincipal(claimsIdentity);
await SignInAsync(claimsPrinciple);
Manager.AddEvent(new GameEvent()
Manager.AddEvent(new GameEvent
{
Origin = privilegedClient,
Type = GameEvent.EventType.Login,
@ -149,7 +156,7 @@ namespace WebfrontCore.Controllers.API
{
if (Authorized)
{
Manager.AddEvent(new GameEvent()
Manager.AddEvent(new GameEvent
{
Origin = Client,
Type = GameEvent.EventType.Logout,

View File

@ -7,7 +7,7 @@ using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using SharedLibraryCore.Helpers;
namespace WebfrontCore.Controllers
{
@ -19,24 +19,33 @@ namespace WebfrontCore.Controllers
}
[HttpGet]
[Obsolete]
public async Task<IActionResult> Login(int clientId, string password)
{
if (clientId == 0 || string.IsNullOrEmpty(password))
{
return Unauthorized("Invalid credentials");
return Unauthorized(Localization["WEBFRONT_ACTION_LOGIN_ERROR"]);
}
try
{
var privilegedClient = await Manager.GetClientService().GetClientForLogin(clientId);
bool loginSuccess = false;
#if DEBUG
loginSuccess = clientId == 1;
#endif
var loginSuccess = false;
if (Utilities.IsDevelopment)
{
loginSuccess = clientId == 1;
}
if (!Authorized && !loginSuccess)
{
loginSuccess = Manager.TokenAuthenticator.AuthorizeToken(privilegedClient.NetworkId, password) ||
(await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(password, privilegedClient.PasswordSalt)))[0] == privilegedClient.Password;
loginSuccess = Manager.TokenAuthenticator.AuthorizeToken(new TokenIdentifier
{
ClientId = clientId,
Token = password
}) ||
(await Task.FromResult(Hashing.Hash(password, privilegedClient.PasswordSalt)))[0] ==
privilegedClient.Password;
}
if (loginSuccess)
@ -46,33 +55,34 @@ namespace WebfrontCore.Controllers
new Claim(ClaimTypes.NameIdentifier, privilegedClient.Name),
new Claim(ClaimTypes.Role, privilegedClient.Level.ToString()),
new Claim(ClaimTypes.Sid, privilegedClient.ClientId.ToString()),
new Claim(ClaimTypes.PrimarySid, privilegedClient.NetworkId.ToString("X"))
new Claim(ClaimTypes.PrimarySid, privilegedClient.NetworkId.ToString("X")),
new Claim(ClaimTypes.PrimaryGroupSid, privilegedClient.GameName.ToString())
};
var claimsIdentity = new ClaimsIdentity(claims, "login");
var claimsPrinciple = new ClaimsPrincipal(claimsIdentity);
await SignInAsync(claimsPrinciple);
Manager.AddEvent(new GameEvent()
Manager.AddEvent(new GameEvent
{
Origin = privilegedClient,
Type = GameEvent.EventType.Login,
Owner = Manager.GetServers().First(),
Data = HttpContext.Request.Headers.ContainsKey("X-Forwarded-For")
? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
: HttpContext.Connection.RemoteIpAddress.ToString()
: HttpContext.Connection.RemoteIpAddress?.ToString()
});
return Ok($"Welcome {privilegedClient.Name}. You are now logged in");
return Ok(Localization["WEBFRONT_ACTION_LOGIN_SUCCESS"].FormatExt(privilegedClient.CleanedName));
}
}
catch (Exception)
{
return Unauthorized("Could not validate credentials");
return Unauthorized(Localization["WEBFRONT_ACTION_LOGIN_ERROR"]);
}
return Unauthorized("Invalid credentials");
return Unauthorized(Localization["WEBFRONT_ACTION_LOGIN_ERROR"]);
}
[HttpGet]
@ -80,14 +90,14 @@ namespace WebfrontCore.Controllers
{
if (Authorized)
{
Manager.AddEvent(new GameEvent()
Manager.AddEvent(new GameEvent
{
Origin = Client,
Type = GameEvent.EventType.Logout,
Owner = Manager.GetServers().First(),
Data = HttpContext.Request.Headers.ContainsKey("X-Forwarded-For")
? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
: HttpContext.Connection.RemoteIpAddress.ToString()
: HttpContext.Connection.RemoteIpAddress?.ToString()
});
}

View File

@ -2,16 +2,20 @@
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
using Data.Models.Client;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Dtos.Meta.Responses;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using WebfrontCore.Permissions;
using WebfrontCore.ViewModels;
namespace WebfrontCore.Controllers
@ -19,6 +23,7 @@ namespace WebfrontCore.Controllers
public class ActionController : BaseController
{
private readonly ApplicationConfiguration _appConfig;
private readonly IMetaServiceV2 _metaService;
private readonly string _banCommandName;
private readonly string _tempbanCommandName;
private readonly string _unbanCommandName;
@ -28,11 +33,14 @@ namespace WebfrontCore.Controllers
private readonly string _flagCommandName;
private readonly string _unflagCommandName;
private readonly string _setLevelCommandName;
private readonly string _setClientTagCommandName;
private readonly string _addClientNoteCommandName;
public ActionController(IManager manager, IEnumerable<IManagerCommand> registeredCommands,
ApplicationConfiguration appConfig) : base(manager)
ApplicationConfiguration appConfig, IMetaServiceV2 metaService) : base(manager)
{
_appConfig = appConfig;
_metaService = metaService;
foreach (var cmd in registeredCommands)
{
@ -68,6 +76,12 @@ namespace WebfrontCore.Controllers
case "OfflineMessageCommand":
_offlineMessageCommandName = cmd.Name;
break;
case "SetClientTagCommand":
_setClientTagCommandName = cmd.Name;
break;
case "AddClientNoteCommand":
_addClientNoteCommandName = cmd.Name;
break;
}
}
}
@ -77,7 +91,7 @@ namespace WebfrontCore.Controllers
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_BAN_NAME"],
Name = "Ban",
Name = Localization["WEBFRONT_ACTION_BAN_NAME"],
Inputs = new List<InputInfo>
{
new()
@ -151,7 +165,7 @@ namespace WebfrontCore.Controllers
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_UNBAN_NAME"],
Name = "Unban",
Name = Localization["WEBFRONT_ACTION_UNBAN_NAME"],
Inputs = new List<InputInfo>
{
new()
@ -192,7 +206,7 @@ namespace WebfrontCore.Controllers
var login = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_LOGIN_NAME"],
Name = "Login",
Name = Localization["WEBFRONT_ACTION_LOGIN_NAME"],
Inputs = new List<InputInfo>
{
new()
@ -225,7 +239,7 @@ namespace WebfrontCore.Controllers
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_LABEL_EDIT"],
Name = "Edit",
Name = Localization["WEBFRONT_ACTION_LABEL_EDIT"],
Inputs = new List<InputInfo>
{
new()
@ -274,7 +288,11 @@ namespace WebfrontCore.Controllers
[Authorize]
public string GenerateLoginTokenAsync()
{
var state = Manager.TokenAuthenticator.GenerateNextToken(Client.NetworkId);
var state = Manager.TokenAuthenticator.GenerateNextToken(new TokenIdentifier
{
ClientId = Client.ClientId
});
return string.Format(Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_GENERATETOKEN_SUCCESS"],
state.Token,
$"{state.RemainingTime} {Utilities.CurrentLocalization.LocalizationIndex["GLOBAL_MINUTES"]}",
@ -286,7 +304,7 @@ namespace WebfrontCore.Controllers
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_LABEL_SUBMIT_MESSAGE"],
Name = "Chat",
Name = Localization["WEBFRONT_ACTION_LABEL_SUBMIT_MESSAGE"],
Inputs = new List<InputInfo>
{
new()
@ -362,7 +380,7 @@ namespace WebfrontCore.Controllers
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_FLAG_NAME"],
Name = "Flag",
Name = Localization["WEBFRONT_ACTION_FLAG_NAME"],
Inputs = new List<InputInfo>
{
new()
@ -401,7 +419,7 @@ namespace WebfrontCore.Controllers
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_UNFLAG_NAME"],
Name = "Unflag",
Name = Localization["WEBFRONT_ACTION_UNFLAG_NAME"],
Inputs = new List<InputInfo>
{
new()
@ -433,7 +451,7 @@ namespace WebfrontCore.Controllers
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_KICK_NAME"],
Name = "Kick",
Name = Localization["WEBFRONT_ACTION_KICK_NAME"],
Inputs = new List<InputInfo>
{
new()
@ -482,8 +500,8 @@ namespace WebfrontCore.Controllers
{
var info = new ActionInfo
{
ActionButtonLabel = "Dismiss",
Name = "Dismiss Alert?",
ActionButtonLabel = Localization["WEBFRONT_ACTION_DISMISS_ALERT_FORM_SUBMIT"],
Name = Localization["WEBFRONT_ACTION_DISMISS_ALERT_SINGLE"],
Inputs = new List<InputInfo>
{
new()
@ -507,7 +525,7 @@ namespace WebfrontCore.Controllers
{
new CommandResponseInfo
{
Response = "Alert dismissed"
Response = Localization["WEBFRONT_ACTION_DISMISS_ALERT_SINGLE_RESPONSE"]
}
});
}
@ -516,8 +534,8 @@ namespace WebfrontCore.Controllers
{
var info = new ActionInfo
{
ActionButtonLabel = "Dismiss",
Name = "Dismiss All Alerts?",
ActionButtonLabel = Localization["WEBFRONT_ACTION_DISMISS_ALERT_FORM_SUBMIT"],
Name = Localization["WEBFRONT_ACTION_DISMISS_ALERT_MANY"],
Inputs = new List<InputInfo>
{
new()
@ -541,7 +559,7 @@ namespace WebfrontCore.Controllers
{
new CommandResponseInfo
{
Response = "Alerts dismissed"
Response = Localization["WEBFRONT_ACTION_DISMISS_ALERT_MANY_RESPONSE"]
}
});
}
@ -550,14 +568,14 @@ namespace WebfrontCore.Controllers
{
var info = new ActionInfo
{
ActionButtonLabel = "Send",
Name = "Compose Message",
ActionButtonLabel = Localization["WEBFRONT_ACTION_OFFLINE_MESSAGE_FORM_SUBMIT"],
Name = Localization["WEBFRONT_ACTION_OFFLINE_MESSAGE_BUTTON_COMPOSE"],
Inputs = new List<InputInfo>
{
new()
{
Name = "message",
Label = "Message Content",
Label = Localization["WEBFRONT_ACTION_OFFLINE_MESSAGE_FORM_CONTENT"],
},
},
Action = "OfflineMessage",
@ -577,6 +595,103 @@ namespace WebfrontCore.Controllers
}));
}
public async Task<IActionResult> SetClientTagForm(int id, CancellationToken token)
{
var tags = await _metaService.GetPersistentMetaValue<List<LookupValue<string>>>(EFMeta.ClientTagNameV2,
token) ?? new List<LookupValue<string>>();
var existingTag = await _metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2,
EFMeta.ClientTagNameV2, id, Manager.CancellationToken);
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_SET_CLIENT_TAG_SUBMIT"],
Name = Localization["WEBFRONT_PROFILE_CONTEXT_MENU_TAG"],
Inputs = new List<InputInfo>
{
new()
{
Name = "clientTag",
Type = "select",
Label = Localization["WEBFRONT_ACTION_SET_CLIENT_TAG_FORM_TAG"],
Values = tags.ToDictionary(
item => item.Value == existingTag?.Value ? $"!selected!{item.Value}" : item.Value,
item => item.Value)
}
},
Action = nameof(SetClientTag),
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public async Task<IActionResult> SetClientTag(int targetId, string clientTag)
{
if (targetId <= 0 || string.IsNullOrWhiteSpace(clientTag))
{
return Json(new[]
{
new CommandResponseInfo
{
Response = Localization["WEBFRONT_ACTION_SET_CLIENT_TAG_NONE"]
}
});
}
var server = Manager.GetServers().First();
return await Task.FromResult(RedirectToAction("Execute", "Console", new
{
serverId = server.EndPoint,
command =
$"{_appConfig.CommandPrefix}{_setClientTagCommandName} @{targetId} {clientTag}"
}));
}
public async Task<IActionResult> AddClientNoteForm(int id)
{
var existingNote = await _metaService.GetPersistentMetaValue<ClientNoteMetaResponse>("ClientNotes", id);
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_CONFIGURATION_BUTTON_SAVE"],
Name = Localization["WEBFRONT_PROFILE_CONTEXT_MENU_NOTE"],
Inputs = new List<InputInfo>
{
new()
{
Name = "note",
Label = Localization["WEBFRONT_ACTION_NOTE_FORM_NOTE"],
Value = existingNote?.Note,
Type = "textarea"
}
},
Action = nameof(AddClientNote),
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public async Task<IActionResult> AddClientNote(int targetId, string note)
{
if (note?.Length > 350 || note?.Count(c => c == '\n') > 4)
{
return StatusCode(StatusCodes.Status400BadRequest, new[]
{
new CommandResponseInfo
{
Response = Localization["WEBFRONT_ACTION_NOTE_INVALID_LENGTH"]
}
});
}
var server = Manager.GetServers().First();
return await Task.FromResult(RedirectToAction("Execute", "Console", new
{
serverId = server.EndPoint,
command =
$"{_appConfig.CommandPrefix}{_addClientNoteCommandName} @{targetId} {note}"
}));
}
private Dictionary<string, string> GetPresetPenaltyReasons() => _appConfig.PresetPenaltyReasons.Values
.Concat(_appConfig.GlobalRules)
.Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? Array.Empty<string>()))

View File

@ -56,7 +56,7 @@ namespace WebfrontCore.Controllers
ViewBag.ClientIP = request.ClientIP;
ViewBag.ClientGuid = request.ClientGuid;
ViewBag.Title = "Ban Management";
ViewBag.Title = Localization["WEBFRONT_NAV_TITLE_BAN_MANAGEMENT"];
return View(results.Results);
}

View File

@ -12,6 +12,7 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
using SharedLibraryCore.Services;
using Stats.Config;
using WebfrontCore.Permissions;
using WebfrontCore.ViewComponents;
@ -23,13 +24,15 @@ namespace WebfrontCore.Controllers
private readonly IMetaServiceV2 _metaService;
private readonly StatsConfiguration _config;
private readonly IGeoLocationService _geoLocationService;
private readonly ClientService _clientService;
public ClientController(IManager manager, IMetaServiceV2 metaService, StatsConfiguration config,
IGeoLocationService geoLocationService) : base(manager)
IGeoLocationService geoLocationService, ClientService clientService) : base(manager)
{
_metaService = metaService;
_config = config;
_geoLocationService = geoLocationService;
_clientService = clientService;
}
[Obsolete]
@ -47,24 +50,31 @@ namespace WebfrontCore.Controllers
}
var activePenalties = await Manager.GetPenaltyService().GetActivePenaltiesAsync(client.AliasLinkId,
client.CurrentAliasId, client.NetworkId, client.IPAddress);
client.CurrentAliasId, client.NetworkId, client.GameName, client.IPAddress);
var persistentMetaTask = new[]
{
_metaService.GetPersistentMetaByLookup(EFMeta.ClientTagV2, EFMeta.ClientTagNameV2, client.ClientId,
token),
_metaService.GetPersistentMeta("GravatarEmail", client.ClientId, token)
_metaService.GetPersistentMeta("GravatarEmail", client.ClientId, token),
};
var persistentMeta = await Task.WhenAll(persistentMetaTask);
var tag = persistentMeta[0];
var gravatar = persistentMeta[1];
var note = await _metaService.GetPersistentMetaValue<ClientNoteMetaResponse>("ClientNotes", client.ClientId,
token);
if (tag?.Value != null)
{
client.SetAdditionalProperty(EFMeta.ClientTagV2, tag.Value);
}
if (!string.IsNullOrWhiteSpace(note?.Note))
{
note.OriginEntityName = await _clientService.GetClientNameById(note.OriginEntityId);
}
// even though we haven't set their level to "banned" yet
// (ie they haven't reconnected with the infringing player identifier)
// we want to show them as banned as to not confuse people.
@ -88,7 +98,7 @@ namespace WebfrontCore.Controllers
var clientDto = new PlayerInfo
{
Name = client.Name,
Game = client.GameName ?? Reference.Game.UKN,
Game = client.GameName,
Level = displayLevel,
LevelInt = displayLevelInt,
ClientId = client.ClientId,
@ -123,7 +133,8 @@ namespace WebfrontCore.Controllers
: ingameClient.CurrentServer.IP,
ingameClient.CurrentServer.Port),
CurrentServerName = ingameClient?.CurrentServer?.Hostname,
GeoLocationInfo = await _geoLocationService.Locate(client.IPAddressString)
GeoLocationInfo = await _geoLocationService.Locate(client.IPAddressString),
NoteMeta = string.IsNullOrWhiteSpace(note?.Note) ? null: note
};
var meta = await _metaService.GetRuntimeMeta<InformationResponse>(new ClientPaginationRequest
@ -146,12 +157,8 @@ namespace WebfrontCore.Controllers
clientDto.Meta.AddRange(Authorized ? meta : meta.Where(m => !m.IsSensitive));
var strippedName = clientDto.Name.StripColors();
ViewBag.Title = strippedName.Substring(strippedName.Length - 1).ToLower()[0] == 's'
? strippedName + "'"
: strippedName + "'s";
ViewBag.Title += " " + Localization["WEBFRONT_CLIENT_PROFILE_TITLE"];
ViewBag.Description = $"Client information for {strippedName}";
ViewBag.Keywords = $"IW4MAdmin, client, profile, {strippedName}";
ViewBag.Title = $"{strippedName} | {Localization["WEBFRONT_CLIENT_PROFILE_TITLE"]}";
ViewBag.Description = Localization["WEBFRONT_PROFILE_DESCRIPTION"].FormatExt(strippedName);
ViewBag.UseNewStats = _config?.EnableAdvancedMetrics ?? true;
return View("Profile/Index", clientDto);
@ -183,7 +190,7 @@ namespace WebfrontCore.Controllers
ClientId = admin.ClientId,
LastConnection = admin.LastConnection,
IsMasked = admin.Masked,
Game = admin.GameName ?? Reference.Game.UKN
Game = admin.GameName
});
}
@ -214,6 +221,8 @@ namespace WebfrontCore.Controllers
ViewBag.SearchTerm = clientName;
ViewBag.ResultCount = clientsDto.Count;
ViewBag.Title = Localization["WEBFRONT_SEARCH_RESULTS_TITLE"];
return View("Find/Index", clientsDto);
}

View File

@ -34,7 +34,12 @@ namespace WebfrontCore.Controllers
{
ClientId = id,
ServerEndpoint = serverId
})).Results.First();
}))?.Results?.First();
if (hitInfo is null)
{
return NotFound();
}
var server = Manager.GetServers().FirstOrDefault(server => server.ToString() == serverId);
long? matchedServerId = null;

View File

@ -221,7 +221,7 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
{
return View("~/Views/Client/_MessageContext.cshtml", new List<MessageResponse>
{
new MessageResponse()
new()
{
ClientId = penalty.OffenderId,
Message = penalty.AutomatedOffense,

View File

@ -100,7 +100,7 @@ namespace WebfrontCore.Controllers
new CommandResponseInfo
{
ClientId = client.ClientId,
Response = Utilities.CurrentLocalization.LocalizationIndex["COMMADS_RESTART_SUCCESS"]
Response = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_RESTART_SUCCESS"]
}
};
}

View File

@ -74,21 +74,28 @@ namespace WebfrontCore.Controllers
ViewBag.CommandPrefix = Manager.GetApplicationSettings().Configuration().CommandPrefix;
// we don't need to the name of the shared library assembly
var excludedAssembly = typeof(BaseController).Assembly;
var commands = Manager.GetCommands()
.Where(_cmd => _cmd.Permission <= Client.Level)
.OrderByDescending(_cmd => _cmd.Permission)
.GroupBy(_cmd =>
.Where(command => command.Permission <= Client.Level)
.OrderByDescending(command => command.Permission)
.GroupBy(command =>
{
// we need the plugin type the command is defined in
var pluginType = _cmd.GetType().Assembly.GetTypes().FirstOrDefault(_type =>
_type.Assembly != excludedAssembly && typeof(IPlugin).IsAssignableFrom(_type));
return pluginType == null ? _translationLookup["WEBFRONT_HELP_COMMAND_NATIVE"] :
pluginType.Name == "ScriptPlugin" ? _translationLookup["WEBFRONT_HELP_SCRIPT_PLUGIN"] :
Manager.Plugins.FirstOrDefault(_plugin => _plugin.GetType().FullName == pluginType.FullName)?
.Name; // for now we're just returning the name of the plugin, maybe later we'll include more info
if (command.GetType().Name == "ScriptCommand")
{
return _translationLookup["WEBFRONT_HELP_SCRIPT_PLUGIN"];
}
var assemblyName = command.GetType().Assembly.GetName().Name;
if (assemblyName is "IW4MAdmin" or "SharedLibraryCore")
{
return _translationLookup["WEBFRONT_HELP_COMMAND_NATIVE"];
}
var pluginType = command.GetType().Assembly.GetTypes()
.FirstOrDefault(type => typeof(IPlugin).IsAssignableFrom(type));
return Manager.Plugins.FirstOrDefault(plugin => plugin.GetType() == pluginType)?.Name ??
_translationLookup["WEBFRONT_HELP_COMMAND_NATIVE"];
})
.Select(_grp => (_grp.Key, _grp.AsEnumerable()));
.Select(group => (group.Key, group.AsEnumerable()));
return View(commands);
}

View File

@ -36,24 +36,26 @@ namespace WebfrontCore.Middleware
/// <param name="gameEvent"></param>
private void OnGameEvent(object sender, GameEvent gameEvent)
{
if (gameEvent.Type == EventType.ChangePermission &&
gameEvent.Extra is EFClient.Permission perm)
if (gameEvent.Type != EventType.ChangePermission || gameEvent.Extra is not EFClient.Permission perm)
{
// we want to remove the claims when the client is demoted
if (perm < EFClient.Permission.Trusted)
return;
}
lock (_privilegedClientIds)
{
switch (perm)
{
lock (_privilegedClientIds)
// we want to remove the claims when the client is demoted
case < EFClient.Permission.Trusted:
{
_privilegedClientIds.RemoveAll(id => id == gameEvent.Target.ClientId);
break;
}
}
// and add if promoted
else if (perm > EFClient.Permission.Trusted &&
!_privilegedClientIds.Contains(gameEvent.Target.ClientId))
{
lock (_privilegedClientIds)
// and add if promoted
case > EFClient.Permission.Trusted when !_privilegedClientIds.Contains(gameEvent.Target.ClientId):
{
_privilegedClientIds.Add(gameEvent.Target.ClientId);
break;
}
}
}
@ -62,10 +64,16 @@ namespace WebfrontCore.Middleware
public async Task Invoke(HttpContext context)
{
// we want to load the initial list of privileged clients
if (_privilegedClientIds.Count == 0)
bool hasAny;
lock (_privilegedClientIds)
{
hasAny = _privilegedClientIds.Any();
}
if (!hasAny)
{
var ids = (await _manager.GetClientService().GetPrivilegedClients())
.Select(_client => _client.ClientId);
.Select(client => client.ClientId);
lock (_privilegedClientIds)
{
@ -74,13 +82,19 @@ namespace WebfrontCore.Middleware
}
// sid stores the clientId
string claimsId = context.User.Claims.FirstOrDefault(_claim => _claim.Type == ClaimTypes.Sid)?.Value;
var claimsId = context.User.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.Sid)?.Value;
if (!string.IsNullOrEmpty(claimsId))
{
int clientId = int.Parse(claimsId);
var clientId = int.Parse(claimsId);
bool hasKey;
lock (_privilegedClientIds)
{
hasKey = _privilegedClientIds.Contains(clientId);
}
// they've been removed
if (!_privilegedClientIds.Contains(clientId) && clientId != 1)
if (!hasKey && clientId != 1)
{
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
}

View File

@ -14,13 +14,13 @@ public enum WebfrontEntity
AuditPage,
RecentPlayersPage,
ProfilePage,
AdminMenu
AdminMenu,
ClientNote
}
public enum WebfrontPermission
{
Read,
Create,
Update,
Write,
Delete
}

View File

@ -54,7 +54,8 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, B
client.NetworkId,
client.AliasLinkId,
client.ClientId,
client.CurrentAlias.IPAddress
client.CurrentAlias.IPAddress,
client.GameName
}).ToListAsync();
var results = new List<BanInfo>();
@ -85,7 +86,7 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, B
OffenderName = penalty.Penalty.Offender.CurrentAlias.Name,
Offense = string.IsNullOrEmpty(penalty.Penalty.AutomatedOffense)
? penalty.Penalty.Offense
: "Anticheat Detection",
: Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_BAN_INFO_AC_DETECTION"],
LinkId = penalty.Penalty.Offender.AliasLinkId,
penalty.Penalty.OffenderId,
penalty.Penalty.PunisherId,
@ -101,7 +102,6 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, B
.Select(alias => alias.LinkId)
.ToListAsync()).Distinct();
matchedPenalties = await context.Penalties.Where(penalty => penalty.Type == EFPenalty.PenaltyType.Ban)
.Where(penalty => penalty.Expires == null || penalty.Expires > lateDateTime)
.Where(penalty => penalty.LinkId != null && linkIds.Contains(penalty.LinkId.Value))
@ -113,7 +113,7 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, B
OffenderName = penalty.Offender.CurrentAlias.Name,
Offense = string.IsNullOrEmpty(penalty.AutomatedOffense)
? penalty.Offense
: "Anticheat Detection",
: Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_BAN_INFO_AC_DETECTION"],
LinkId = penalty.Offender.AliasLinkId,
penalty.OffenderId,
penalty.PunisherId,
@ -158,6 +158,7 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, B
ClientId = matchingClient.ClientId,
NetworkId = matchingClient.NetworkId,
IPAddress = matchingClient.IPAddress,
Game = matchingClient.GameName,
AssociatedPenalties = relatedEntities,
AttachedPenalty = allPenalties.FirstOrDefault(penalty =>

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Data.Models;
namespace WebfrontCore.QueryHelpers.Models;
@ -9,6 +10,7 @@ public class BanInfo
public int ClientId { get; set; }
public int? IPAddress { get; set; }
public long NetworkId { get; set; }
public Reference.Game Game { get; set; }
public PenaltyInfo AttachedPenalty { get; set; }
public IEnumerable<PenaltyInfo> AssociatedPenalties { get; set; }
}

View File

@ -35,19 +35,19 @@
@foreach (var social in Model.CommunityInformation.SocialAccounts ?? Array.Empty<SocialAccountConfiguration>())
{
<div>
<a href="@social.Url" target="_blank" title="@social.Title">
<a href="@social.Url" target="_blank" title="@social.Title" class="d-flex no-decoration">
@if (!string.IsNullOrWhiteSpace(social.IconId))
{
<span class="oi @social.IconId"></span>
<i class="oi @social.IconId mr-5" style="width: 1.6rem;"></i>
}
else if (!string.IsNullOrWhiteSpace(social.IconUrl))
{
var url = Uri.TryCreate(social.IconUrl, UriKind.Absolute, out var parsedUrl)
? parsedUrl.AbsoluteUri
: $"images/community/{social.IconUrl}";
<img class="img-fluid" style="max-width: 1rem; fill: white" src="@url" alt="@social.Title"/>
<img class="img-fluid mr-5" style="width: 1.6rem; fill: white" src="@url" alt="@social.Title"/>
}
<span class="ml-1">@social.Title</span>
<div class="ml-1">@social.Title</div>
</a>
</div>
}

View File

@ -25,11 +25,20 @@
@if (inputType == "select")
{
<select name="@input.Name" class="form-control" aria-label="@input.Name" aria-describedby="basic-addon-@input.Name">
@foreach (var item in input.Values)
@foreach (var (key, item) in input.Values)
{
<option value="@item.Key">
<color-code value="@item.Value"></color-code>
</option>
if (key.StartsWith("!selected!"))
{
<option value="@key.Replace("!selected!", "")" selected>
<color-code value="@item"></color-code>
</option>
}
else
{
<option value="@key">
<color-code value="@item"></color-code>
</option>
}
}
</select>
}
@ -42,6 +51,11 @@
</label>
</div>
}
else if (inputType == "textarea")
{
<textarea name="@input.Name" class="form-control @(input.Required ? "required" : "")" placeholder="@input.Placeholder" aria-label="@input.Name" aria-describedby="basic-addon-@input.Name">@value</textarea>
}
else
{
@ -61,6 +75,6 @@
}
<div class="ml-auto">
<button type="submit" class="btn btn-primary">@Model.ActionButtonLabel</button>
<a href="#" class="btn mr-5 ml-5" role="button" onclick="halfmoon.toggleModal('actionModal');">Close</a>
<a href="#" class="btn mr-5 ml-5" role="button" onclick="halfmoon.toggleModal('actionModal');">@ViewBag.Localization["WEBFRONT_ACTION_MODAL_BUTTON_CLOSE"]</a>
</div>
</form>

View File

@ -4,12 +4,12 @@
<h2 class="content-title mt-20 mb-10">@ViewBag.Title</h2>
@if (!Model.Any())
{
<div class="text-muted mb-10">Search for records...</div>
<div class="text-muted mb-10">@ViewBag.Localization["WEBFRONT_BAN_MGMT_SUBTITLE"]</div>
}
<form method="get" class="mt-10">
<div class="d-flex flex-column flex-md-row">
<div class="input-group">
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientNameInput" name="clientName" value="@ViewBag.ClientName" placeholder="Client Name">
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientNameInput" name="clientName" value="@ViewBag.ClientName" placeholder="@ViewBag.Localization["WEBFRONT_BAN_MGMT_FORM_NAME"]">
<div class="input-group-append">
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
<i class="oi oi-magnifying-glass"></i>
@ -18,7 +18,7 @@
</div>
<div class="input-group mr-md-5 ml-md-10 mt-10 mb-5 mt-md-0 mb-md-0">
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientGuidInput" name="clientGuid" value="@ViewBag.ClientGuid" placeholder="Client GUID">
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientGuidInput" name="clientGuid" value="@ViewBag.ClientGuid" placeholder="@ViewBag.Localization["WEBFRONT_BAN_MGMT_FORM_GUID"]">
<div class="input-group-append">
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
<i class="oi oi-magnifying-glass"></i>
@ -26,7 +26,7 @@
</div>
</div>
<div class="input-group mr-md-10 ml-md-5 mb-10 mt-5 mt-md-0 mb-md-0">
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientIPInput" name="clientIP" value="@ViewBag.ClientIP" placeholder="Client IP">
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientIPInput" name="clientIP" value="@ViewBag.ClientIP" placeholder="@ViewBag.Localization["WEBFRONT_BAN_MGMT_FORM_IP"]">
<div class="input-group-append">
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
<i class="oi oi-magnifying-glass"></i>
@ -34,7 +34,7 @@
</div>
</div>
<div class="input-group">
<input type="number" class="form-control bg-dark-dm bg-light-ex-lm" id="clientIdInput" name="clientId" value="@ViewBag.ClientId" placeholder="Client Id">
<input type="number" class="form-control bg-dark-dm bg-light-ex-lm" id="clientIdInput" name="clientId" value="@ViewBag.ClientId" placeholder="@ViewBag.Localization["WEBFRONT_BAN_MGMT_FORM_ID"]">
<div class="input-group-append">
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
<i class="oi oi-magnifying-glass"></i>

View File

@ -10,27 +10,37 @@
<div class="card p-10 m-0 mt-15 mb-15">
<div class="d-flex flex-row flex-wrap">
<div class="d-flex p-15 mr-md-10 w-full w-md-200 bg-very-dark-dm bg-light-ex-lm rounded">
<div class="align-self-center ">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@ban.ClientId" class="font-size-18 no-decoration">@ban.ClientName</a>
<div class="align-self-center w-full">
<div class="d-flex font-size-16">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@ban.ClientId" class="no-decoration flex-fill text-force-break mr-5">
<color-code value="@ban.ClientName"></color-code>
</a>
<div data-toggle="tooltip" data-title="@ViewBag.Localization[$"GAME_{ban.Game}"]">
<div class="badge align-self-center">@Utilities.MakeAbbreviation(ViewBag.Localization[$"GAME_{ban.Game}"])</div>
</div>
</div>
<has-permission entity="ClientGuid" required-permission="Read">
<div class="text-muted">@ban.NetworkId.ToString("X")</div>
</has-permission>
<has-permission entity="ClientIPAddress" required-permission="Read">
<div class="text-muted">@ban.IPAddress.ConvertIPtoString()</div>
</has-permission>
<br/>
@if (ban.AttachedPenalty is not null)
{
<br/>
<div class="text-muted font-weight-light">@ban.AttachedPenalty.Offense.CapClientName(30)</div>
<div class="text-muted font-weight-light">
<color-code value="@ban.AttachedPenalty.Offense.CapClientName(30)"></color-code>
</div>
<div class="text-danger font-weight-light">@ban.AttachedPenalty.DateTime.ToStandardFormat()</div>
<div class="btn profile-action mt-10 w-100" data-action="unban" data-action-id="@ban.ClientId">Unban</div>
<br/>
<div class="btn profile-action" ata-action="unban" data-action-id="@ban.ClientId">@ViewBag.Localization["WEBFRONT_BAN_MGMT_ACTION_UNBAN"]</div>
}
else
{
<br/>
<div class="align-self-end text-muted font-weight-light">
<span class="oi oi-warning font-size-12"></span>
<span>Link-Only Ban</span>
<span>@ViewBag.Localization["WEBFRONT_BAN_MGMT_LINK_ONLY"]</span>
</div>
}
</div>
@ -39,13 +49,22 @@
@foreach (var associatedEntity in ban.AssociatedPenalties)
{
<div class="d-flex flex-wrap flex-column w-full w-md-200 p-10">
<div data-toggle="tooltip" data-title="Linked via shared IP" class="d-flex">
<i class="oi oi-link-intact align-self-center"></i>
<div class="text-truncate ml-5 mr-5">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@associatedEntity.OffenderInfo.ClientId" class="font-size-18 no-decoration">@associatedEntity.OffenderInfo.ClientName</a>
<div class="d-flex flex-wrap flex-column w-full w-md-200 p-10 border rounded mt-10 mt-md-0" style="border-style: dashed !important;">
<div class="d-flex font-size-16">
<div data-toggle="tooltip" data-title="@ViewBag.Localization["WEBFRONT_BAN_MGMT_TOOLTIP_LINKED"]" class="d-flex flex-fill">
<i class="oi oi-link-intact align-self-center"></i>
<div class="text-truncate ml-5 mr-5">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@associatedEntity.OffenderInfo.ClientId" class="no-decoration text-force-break">
<color-code value="@associatedEntity.OffenderInfo.ClientName"></color-code>
</a>
</div>
</div>
<div data-toggle="tooltip" data-title="@ViewBag.Localization[$"GAME_{ban.Game}"]">
<div class="badge">@Utilities.MakeAbbreviation(ViewBag.Localization[$"GAME_{ban.Game}"])</div>
</div>
</div>
<br/>
<has-permission entity="ClientGuid" required-permission="Read">
<div class="text-muted">@associatedEntity.OffenderInfo.NetworkId?.ToString("X")</div>
</has-permission>
@ -53,9 +72,11 @@
<div class="text-muted">@associatedEntity.OffenderInfo.IPAddress.ConvertIPtoString()</div>
</has-permission>
<br/>
<div class="text-muted font-weight-light">@associatedEntity.Offense.CapClientName(30)</div>
<div class="text-muted font-weight-light">
<color-code value="@associatedEntity.Offense.CapClientName(30)"></color-code>
</div>
<div class="text-danger font-weight-light">@associatedEntity.DateTime.ToStandardFormat()</div>
<div class="btn profile-action mt-10 w-100" data-action="unban" data-action-id="@associatedEntity.OffenderInfo.ClientId">Unban</div>
<div class="btn profile-action mt-10" data-action="unban" data-action-id="@associatedEntity.OffenderInfo.ClientId">@ViewBag.Localization["WEBFRONT_BAN_MGMT_ACTION_UNBAN"]</div>
</div>
}
</div>

View File

@ -30,42 +30,46 @@
</td>
<td>
@info.Data
<td >
<td class="text-force-break font-weight-light">
@info.NewValue
</td>
<td class="text-right">
@info.When.ToString()
@info.When.ToStandardFormat()
</td>
</tr>
<!-- mobile -->
<tr class="d-table-row d-lg-none d-flex bg-dark-dm bg-light-lm">
<td class="bg-primary text-light text-right flex-grow-0">
<td class="bg-primary text-light text-right flex-grow-0 w-quarter d-flex flex-column">
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_TYPE"]</div>
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_ADMIN"]</div>
<div class="mt-5 mb-5">@loc["WEBFRONT_PENALTY_TEMPLATE_NAME"]</div>
<div class="mt-5 mb-5">@loc["WEBFRONT_ADMIN_AUDIT_LOG_INFO"]</div>
<div class="mt-5 mb-5">@loc["WEBFRONT_ADMIN_AUDIT_LOG_CURRENT"]</div>
<div class="mt-5 mb-5">@loc["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</div>
<div class="mt-5 mb-5 mt-auto">@loc["WEBFRONT_ADMIN_AUDIT_LOG_TIME"]</div>
</td>
<td>
<td class="w-three-quarter d-flex flex-column">
<div class="mt-5 mb-5">@info.Action</div>
<a asp-controller="Client" asp-action="Profile" asp-route-id="@info.OriginId" class="link-inverse">
<color-code value="@info.OriginName"></color-code>
</a>
<div class="mt-5 mb-5">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@info.OriginId" class="link-inverse">
<color-code value="@info.OriginName"></color-code>
</a>
</div>
@if (info.TargetId != null)
{
<a asp-controller="Client" asp-action="Profile" asp-route-id="@info.TargetId" class="mt-5 mb-5">
<color-code value="@info.TargetName"></color-code>
</a>
<div class="mt-5 mb-5">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@info.TargetId">
<color-code value="@info.TargetName"></color-code>
</a>
</div>
}
else
{
<div class="mt-5 mb-5">&ndash;</div>
}
<div class="mt-5 mb-5"> @info.Data</div>
<div class="mt-5 mb-5">@info.NewValue</div>
<div class="mt-5 mb-5">@info.When.ToString()</div>
<div class="mt-5 mb-5">@info.Data</div>
<div class="mt-5 mb-5 text-force-break">@info.NewValue</div>
<div class="mt-5 mb-5 text-muted">@info.When.ToStandardFormat()</div>
</td>
</tr>
}

View File

@ -5,8 +5,30 @@
<!-- desktop -->
<div class="content mt-0">
<h2 class="content-title mt-20 mb-0">Search Results</h2>
<div class="text-muted mb-15"><span class="badge">@ViewBag.SearchTerm</span> returned <span class="text-primary">@ViewBag.ResultCount</span> matche(s)</div>
<h2 class="content-title mt-20 mb-0">@loc["WEBFRONT_SEARCH_RESULTS_TITLE"]</h2>
<div class="text-muted mb-15">
@foreach (var match in Utilities.SplitTranslationTokens("WEBFRONT_SEARCH_RESULTS_SUBTITLE_FORMAT"))
{
if (match.IsInterpolation)
{
if (match.MatchValue == "searchTerm")
{
<span class="badge">
@ViewBag.SearchTerm
</span>
}
else if (match.MatchValue == "searchCount")
{
<span class="text-primary">@ViewBag.ResultCount</span>
}
}
else
{
<span>@match.MatchValue</span>
}
}
</div>
<table class="table d-none d-md-table">
<thead>

View File

@ -9,7 +9,7 @@
else
{
<h2 class="content-title mt-20 mb-0">Search Results</h2>
<h2 class="content-title mt-20 mb-0">@ViewBag.Localization["WEBFRONT_SEARCH_RESULTS_TITLE"]</h2>
<div class="text-muted mb-15">@Html.Raw(Utilities.FormatExt(ViewBag.Localization["WEBFRONT_STATS_MESSAGES_FOUND"], $"<span class=\"badge\">{Model.TotalResultCount.ToString("N0")}</span>"))</div>
<table class="table bg-dark-dm bg-light-lm rounded" style="table-layout: fixed">
@ -27,7 +27,7 @@ else
</table>
<div id="loaderLoad" class="mt-10 m-auto text-center d-none d-lg-block">
<i class="loader-load-more oi oi-chevron-bottom "></i>
<i class="loader-load-more oi oi-chevron-bottom"></i>
</div>
@section scripts {

View File

@ -24,7 +24,7 @@
<color-code value="@(message.ServerName ?? "--")"></color-code>
</td>
<td colspan="15%" class="text-right text-break">
@message.When
@message.When.ToStandardFormat()
</td>
</tr>
@ -53,8 +53,7 @@
<div>
<color-code value="@(message.ServerName ?? "--")"></color-code>
</div>
<div> @message.When</div>
<div>@message.When.ToStandardFormat()</div>
</td>
</tr>
}

View File

@ -2,15 +2,14 @@
<div class="content mt-0">
<h4 class="content-title mt-20">@ViewBag.Title</h4>
@foreach (var key in Model.Keys)
{
<table class="table mb-20" style="table-layout:fixed;">
<thead>
<tr class="level-bgcolor-@((int)key)">
<th class="text-light">@key.ToLocalizedLevelName()</th>
<th>Game</th>
<th class="text-right font-weight-bold">Last Connected</th>
<th colspan="50%" class="text-light">@key.ToLocalizedLevelName()</th>
<th colspan="20%">@ViewBag.Localization["WEBFRONT_CONTEXT_MENU_GLOBAL_GAME"]</th>
<th colspan="30%" class="text-right font-weight-bold text-force-break">@ViewBag.Localization["WEBFRONT_SEARCH_LAST_CONNECTED"]</th>
</tr>
</thead>
<tbody>
@ -22,22 +21,25 @@
continue;
}
<tr class="bg-dark-dm bg-light-lm">
<td>
<td colspan="50%">
@if (client.IsMasked)
{
<span data-toggle="tooltip" data-title="Client is masked">
<span data-toggle="tooltip" data-title="@ViewBag.Localization["WEBFRONT_PRIVILEGED_TOOLTIP_MASKED"]">
<span class="oi oi-shield mr-5 font-size-12"></span>
</span>
}
<a asp-controller="Client" asp-action="Profile" asp-route-id="@client.ClientId">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@client.ClientId" class="text-force-break">
<color-code value="@client.Name"></color-code>
</a>
</td>
<td>
<td colspan="20%" class="d-none d-md-table-cell">
<div class="badge">@ViewBag.Localization[$"GAME_{client.Game}"]</div>
</td>
<td class="text-right">
<td colspan="20%" class="d-table-cell d-md-none">
<div class="badge">@(Utilities.MakeAbbreviation(ViewBag.Localization[$"GAME_{client.Game}"] as string))</div>
</td>
<td colspan="30%" class="text-right">
@client.LastConnection.HumanizeForCurrentCulture()
</td>
</tr>
@ -46,5 +48,4 @@
</tbody>
</table>
}
</div>

View File

@ -58,7 +58,7 @@
</has-permission>
}
<h2 class="content-title mb-0">Player Profile</h2>
<h2 class="content-title mb-0">@ViewBag.Localization["WEBFRONT_PROFILE_TITLE"]</h2>
<div class="font-size-12 text-muted">@ViewBag.Localization[$"GAME_{Model.Game}"]</div>
<div id="profile_wrapper" class="mb-10 mt-10">
@ -66,7 +66,7 @@
<!-- online status indicator -->
@if (Model.Online)
{
<div class="bg-success rounded-circle position-absolute status-indicator z-20 mt-10 ml-10" data-toggle="tooltip" data-placement="bottom" data-title="Client is online"></div>
<div class="bg-success rounded-circle position-absolute status-indicator z-20 mt-10 ml-10" data-toggle="tooltip" data-placement="bottom" data-title="@ViewBag.Localization["WEBFRONT_PROFILE_TOOLTIP_ONLINE"]"></div>
<div class="bg-success rounded-circle position-absolute status-indicator with-ripple z-10 mt-10 ml-10"></div>
}
@ -82,7 +82,7 @@
<div class="d-flex flex-column align-self-center ml-20 mr-20 mt-10 mb-10 mt-md-0 mb-md-0 text-center text-md-left">
<!-- name -->
<div id="profile_name">
<span class="font-size-20 font-weight-medium">
<span class="font-size-20 font-weight-medium text-force-break">
<color-code value="@Model.Name"></color-code>
</span>
<has-permission entity="MetaAliasUpdate" required-permission="Read">
@ -105,7 +105,7 @@
@if (Model.Aliases.Count > 15)
{
<div class="dropdown-divider"></div>
<span class="dropdown-item bg-dark-dm bg-light-lm">...and @(Model.Aliases.Count - 15) more</span>
<span class="dropdown-item bg-dark-dm bg-light-lm">@((ViewBag.Localization["WEBFRONT_PROFILE_ALIAS_COUNT_MORE_FORMAT"] as string).FormatExt(Model.Aliases.Count - 15))</span>
}
</div>
</div>
@ -115,7 +115,7 @@
<has-permission entity="ClientLevel" required-permission="Read">
<div class="align-self-center align-self-md-start font-weight-bold font-size-16 level-color-@Model.LevelInt">
<div class="d-flex flex-row">
<span>@Model.Level</span>
<color-code value="@Model.Level"></color-code>
</div>
</div>
</has-permission>
@ -126,7 +126,7 @@
<div class="text-muted" data-toggle="dropdown" id="altGuidFormatsDropdown" aria-haspopup="true" aria-expanded="false">@Model.NetworkId.ToString("X")</div>
<div class="dropdown-menu" aria-labelledby="altGuidFormatsDropdown">
<div class="p-10 font-size-12">
<div class="">Alternative Formats</div>
<div class="">@ViewBag.Localization["WEBFRONT_PROFILE_POPOVER_ALTERNATIVE_GUID"]</div>
<div class="dropdown-divider mt-5 mb-5"></div>
<div class="text-muted font-weight-lighter">@((ulong)Model.NetworkId)</div>
</div>
@ -164,7 +164,7 @@
@if (Model.IPs.Count > 15)
{
<div class="dropdown-divider"></div>
<span class="dropdown-item bg-dark-dm bg-light-lm">...and @(Model.IPs.Count - 15) more</span>
<span class="dropdown-item bg-dark-dm bg-light-lm">@((ViewBag.Localization["WEBFRONT_PROFILE_ALIAS_COUNT_MORE_FORMAT"] as string).FormatExt(Model.IPs.Count - 15))</span>
}
</div>
</div>
@ -173,6 +173,27 @@
</div>
</div>
@if (!string.IsNullOrWhiteSpace(Model.NoteMeta?.Note))
{
<has-permission entity="ClientNote" required-permission="Read">
<div class="rounded border p-10 m-10 d-flex flex-column flex-md-row" style="border-style: dashed !important">
<i class="align-self-center oi oi-clipboard"></i>
<div class="align-self-center font-size-12 font-weight-light pl-10 pr-10">
@foreach (var line in Model.NoteMeta.Note.Split("\n"))
{
<div class="text-force-break">@line.TrimEnd('\r')</div>
}
<div class="mt-5">
<a asp-controller="Client" asp-action="Profile" asp-route-id="@Model.NoteMeta.OriginEntityId" class="no-decoration ">
<color-code value="@Model.NoteMeta.OriginEntityName"></color-code>
</a>
<span>&mdash; @Model.NoteMeta.ModifiedDate.HumanizeForCurrentCulture()</span>
</div>
</div>
</div>
</has-permission>
}
<div class="flex-fill d-flex justify-content-center justify-content-md-end mt-10 mt-md-0">
<!-- country flag -->
<div id="ipGeoDropdown" class="dropdown with-arrow align-self-center">
@ -252,18 +273,18 @@
@{
var menuItems = new SideContextMenuItems
{
MenuTitle = "Actions",
MenuTitle = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_TITLE"]
};
if (Model.Online)
{
menuItems.Items.Add(new SideContextMenuItem
{
Title = "Join Game",
Title = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_JOIN"],
IsLink = true,
IsButton = true,
Reference = Model.ConnectProtocolUrl,
Tooltip = $"Playing on {Model.CurrentServerName.StripColors()}",
Tooltip = (ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_TOOLTIP_JOIN"] as string).FormatExt(Model.CurrentServerName.StripColors()),
Icon = "oi-play-circle"
});
}
@ -272,19 +293,40 @@
{
menuItems.Items.Add(new SideContextMenuItem
{
Title = "Change Level",
Title = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_LEVEL"],
IsButton = true,
Reference = "edit",
Icon = "oi-cog",
EntityId = Model.ClientId
});
}
if (ViewBag.Authorized)
{
menuItems.Items.Add(new SideContextMenuItem
{
Title = "Message",
Title = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_TAG"],
IsButton = true,
Reference = "SetClientTag",
Icon = "oi-tag",
EntityId = Model.ClientId
});
if ((ViewBag.PermissionsSet as IEnumerable<string>).HasPermission(WebfrontEntity.ClientNote, WebfrontPermission.Write))
{
menuItems.Items.Add(new SideContextMenuItem
{
Title = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_NOTE"],
IsButton = true,
Reference = "AddClientNote",
Icon = "oi-clipboard",
EntityId = Model.ClientId
});
}
menuItems.Items.Add(new SideContextMenuItem
{
Title = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MESSAGE"],
IsButton = true,
Reference = "OfflineMessage",
Icon = "oi oi-envelope-closed",
@ -294,7 +336,7 @@
menuItems.Items.Add(new SideContextMenuItem
{
Title = "View Stats",
Title = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_STATS"],
IsButton = true,
IsLink = true,
Reference = Url.Action("Advanced", "ClientStatistics", new { id = Model.ClientId }),
@ -305,7 +347,7 @@
{
menuItems.Items.Add(new SideContextMenuItem
{
Title = isFlagged ? "Unflag" : "Flag",
Title = isFlagged ? ViewBag.Localization["WEBFRONT_ACTION_UNFLAG_NAME"] : ViewBag.Localization["WEBFRONT_ACTION_FLAG_NAME"],
IsButton = true,
Reference = isFlagged ? "unflag" : "flag",
Icon = "oi-flag",
@ -317,7 +359,7 @@
{
menuItems.Items.Add(new SideContextMenuItem
{
Title = "Kick",
Title = ViewBag.Localization["WEBFRONT_ACTION_KICK_NAME"],
IsButton = true,
Reference = "kick",
Icon = "oi-circle-x",
@ -329,7 +371,7 @@
{
menuItems.Items.Add(new SideContextMenuItem
{
Title = "Ban",
Title = ViewBag.Localization["WEBFRONT_ACTION_BAN_NAME"],
IsButton = true,
Reference = "ban",
Icon = "oi-lock-unlocked",
@ -341,7 +383,7 @@
{
menuItems.Items.Add(new SideContextMenuItem
{
Title = "Unban",
Title = ViewBag.Localization["WEBFRONT_ACTION_UNBAN_NAME"],
IsButton = true,
Reference = "unban",
Icon = "oi-lock-locked",

View File

@ -41,7 +41,6 @@
{
<color-code value="@Model.Offense"></color-code>
}
</span>
}

View File

@ -18,7 +18,7 @@
@{var results = Utilities.SplitTranslationTokens(meta.meta.Key);}
@if (results.Any(_result => _result.IsInterpolation))
@if (results.Any(result => result.IsInterpolation))
{
foreach (var result in results)
{
@ -41,6 +41,5 @@
}
</div>
}
<!-- </div> -->
}
</div>

Some files were not shown because too many files have changed in this diff Show More