Compare commits
58 Commits
2022.06.12
...
2022.07.23
Author | SHA1 | Date | |
---|---|---|---|
444c06e65e | |||
561909158f | |||
cd12c3f26e | |||
c817f9a810 | |||
b27ae1517e | |||
507688a175 | |||
d2cfd50e39 | |||
51e8b31e42 | |||
fa1567d3f5 | |||
f97e266c24 | |||
506b17dbb3 | |||
bef8c08d90 | |||
b78c467539 | |||
c3e042521a | |||
cb5f490d3b | |||
0a55c54c42 | |||
f43f7b5040 | |||
540cf7489d | |||
1a72faee60 | |||
4e44bb5ea1 | |||
9e17bcc38f | |||
4b33b33d01 | |||
6f1bc7ab90 | |||
63e1774cb6 | |||
61df873bb1 | |||
052eeb0615 | |||
88e67747fe | |||
5db94723aa | |||
ea8216ecdf | |||
6abbcbe464 | |||
57484690b6 | |||
7a022a1973 | |||
7108e23a03 | |||
77d25890da | |||
2fca68a7ea | |||
a6c0a94f6c | |||
71abaac9e1 | |||
e07651b931 | |||
5a2ee36df9 | |||
2daa4991d1 | |||
775c0a91b5 | |||
55bccc7d3d | |||
4322e8d882 | |||
a92f9fc29c | |||
fbf424c77d | |||
b8e001fcfe | |||
5ab5b73ecf | |||
4534d24fe6 | |||
73c8d0da33 | |||
16d75470b5 | |||
f02552faa1 | |||
a4923d03f9 | |||
8ae6561f4e | |||
deeb1dea87 | |||
9ab34614c5 | |||
2cff25d6b3 | |||
df3e226dc9 | |||
ef3db63ba7 |
@ -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()
|
||||
|
52
Application/Commands/AddClientNoteCommand.cs
Normal file
52
Application/Commands/AddClientNoteCommand.cs
Normal 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"]);
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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": [
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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()));
|
||||
|
@ -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 _);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 =>
|
||||
{
|
||||
|
1639
Data/Migrations/MySql/20220613192602_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
1639
Data/Migrations/MySql/20220613192602_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
1639
Data/Migrations/MySql/20220616224602_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
1639
Data/Migrations/MySql/20220616224602_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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;");
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
|
1696
Data/Migrations/Postgresql/20220613181913_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
1696
Data/Migrations/Postgresql/20220613181913_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
1696
Data/Migrations/Postgresql/20220616224145_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
1696
Data/Migrations/Postgresql/20220616224145_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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""");
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
|
1637
Data/Migrations/Sqlite/20220613160952_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
1637
Data/Migrations/Sqlite/20220613160952_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
1637
Data/Migrations/Sqlite/20220616225008_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
1637
Data/Migrations/Sqlite/20220616225008_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
|
@ -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; }
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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" );
|
||||
|
@ -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" );
|
||||
|
@ -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" );
|
||||
|
@ -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] + ")" );
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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">
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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';
|
||||
|
@ -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) {}
|
||||
};
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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..];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
9
SharedLibraryCore/Helpers/TokenIdentifier.cs
Normal file
9
SharedLibraryCore/Helpers/TokenIdentifier.cs
Normal 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; }
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
7
SharedLibraryCore/Interfaces/ITokenIdentifier.cs
Normal file
7
SharedLibraryCore/Interfaces/ITokenIdentifier.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace SharedLibraryCore.Interfaces;
|
||||
|
||||
public interface ITokenIdentifier
|
||||
{
|
||||
int ClientId { get; }
|
||||
string Token { get; }
|
||||
}
|
@ -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);
|
||||
|
@ -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()))
|
||||
{
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
{
|
||||
|
@ -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>
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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>()))
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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"]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -14,13 +14,13 @@ public enum WebfrontEntity
|
||||
AuditPage,
|
||||
RecentPlayersPage,
|
||||
ProfilePage,
|
||||
AdminMenu
|
||||
AdminMenu,
|
||||
ClientNote
|
||||
}
|
||||
|
||||
public enum WebfrontPermission
|
||||
{
|
||||
Read,
|
||||
Create,
|
||||
Update,
|
||||
Write,
|
||||
Delete
|
||||
}
|
||||
|
@ -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 =>
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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">–</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>
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>— @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",
|
||||
|
@ -41,7 +41,6 @@
|
||||
{
|
||||
<color-code value="@Model.Offense"></color-code>
|
||||
}
|
||||
|
||||
</span>
|
||||
}
|
||||
|
||||
|
@ -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
Reference in New Issue
Block a user