Compare commits

...

62 Commits

Author SHA1 Message Date
0a55c54c42 update to game interface/integration for persistent stat data 2022-07-13 16:10:16 -05:00
f43f7b5040 misc webfront tweaks 2022-07-10 21:06:58 -05:00
540cf7489d update pluto t6 parser for unknown ip 2022-07-10 20:09:57 -05:00
1a72faee60 add date stamp to performance graphs / increase number of performance rating snapshots / localize graph timestamps 2022-07-10 17:06:46 -05:00
4e44bb5ea1 fix rcon issue on restart 2022-07-09 20:57:00 -05:00
9e17bcc38f improve ban management display and additional translations 2022-07-09 16:32:23 -05:00
4b33b33d01 fix issue with alert on warn in game interface 2022-07-09 14:23:08 -05:00
6f1bc7ab90 cleanup table display of admins on mobile display 2022-07-09 13:54:35 -05:00
63e1774cb6 gracefully handle when infoString does not include all expected data 2022-07-09 10:52:27 -05:00
61df873bb1 more localization tweaks 2022-07-08 20:40:27 -05:00
052eeb0615 fix tag on welcome issue 2022-07-08 20:39:58 -05:00
88e67747fe add option to normalize diacritics for rcon parsers (applied to T6) 2022-07-06 15:42:31 -05:00
5db94723aa Merge branch 'release/pre' of https://github.com/RaidMax/IW4M-Admin into release/pre 2022-07-06 10:02:09 -05:00
ea8216ecdf Add H1 maps and gametypes (#252) 2022-07-06 10:01:01 -05:00
6abbcbe464 prevent waiting for response on quit command 2022-07-06 09:55:06 -05:00
57484690b6 clean up display and uniformity of social icons 2022-07-06 09:49:44 -05:00
7a022a1973 fix grouping of commands on help page 2022-07-05 15:57:39 -05:00
7108e23a03 fix issue with context menu close not working on mobile 2022-07-05 15:15:25 -05:00
77d25890da clean up some more translations 2022-07-05 12:42:17 -05:00
2fca68a7ea update webfront translation strings 2022-07-05 12:02:43 -05:00
a6c0a94f6c support per-command override of rcon timeouts / update t5 parser to reflect 2022-07-01 09:59:11 -05:00
71abaac9e1 remove reports on ban/tempban 2022-07-01 09:14:57 -05:00
e07651b931 fix toast message issue on pages with query params 2022-06-28 10:03:05 -05:00
5a2ee36df9 use "unknown" ip as bot indicator 2022-06-28 09:15:37 -05:00
2daa4991d1 fix issue with previous change 2022-06-21 16:57:06 -05:00
775c0a91b5 small parser changes 2022-06-21 16:33:11 -05:00
55bccc7d3d ensure commands are not displayed/usable for unsupported games 2022-06-17 13:11:44 -05:00
4322e8d882 add migration logic for MySQL case sensitivity 2022-06-17 09:44:14 -05:00
a92f9fc29c optimize client searching 2022-06-16 18:44:49 -05:00
fbf424c77d optimize chat filtering/searching 2022-06-16 18:03:23 -05:00
b8e001fcfe misc ui tweaks 2022-06-16 14:02:44 -05:00
5ab5b73ecf order report servers by most recent report 2022-06-16 10:11:01 -05:00
4534d24fe6 fix token auth issue 2022-06-16 10:07:03 -05:00
73c8d0da33 improve icon alignment for nav menu 2022-06-16 09:46:01 -05:00
16d75470b5 fix login persistence issue 2022-06-15 21:00:01 -05:00
f02552faa1 fix up query/check 2022-06-15 20:19:22 -05:00
a4923d03f9 hide token generation button for non-logged-in users 2022-06-15 19:39:53 -05:00
8ae6561f4e update schema to support unique guid + game combinations 2022-06-15 19:37:34 -05:00
deeb1dea87 set the rcon parser game name for retail WaW 2022-06-14 15:12:19 -05:00
9ab34614c5 don't publish disconnect event if no client id 2022-06-14 15:00:23 -05:00
2cff25d6b3 make alert menu scrollable for large # of alerts 2022-06-13 11:03:39 -05:00
df3e226dc9 actually fix the previous issue 2022-06-12 16:37:07 -05:00
ef3db63ba7 fix issue that shouldn't actually be an issue 2022-06-12 15:09:26 -05:00
49fe4520ff improve alert display for mobile 2022-06-12 12:20:08 -05:00
6587187a34 fix memory/database leak with ranked player count cache 2022-06-12 12:19:32 -05:00
b337e232a2 use bot ip address when determining if client is bot 2022-06-12 10:09:56 -05:00
a44b4e9475 add alert/notification functionality (for server connection events and messages) 2022-06-11 11:34:00 -05:00
ffb0e5cac1 update for t5 dvar format change 2022-06-11 09:56:28 -05:00
ecc2b5bf54 increase width of side context menu for longer server names 2022-06-09 13:59:00 -05:00
2ac9cc4379 fix bug with loading top stats for individual servers 2022-06-09 13:50:58 -05:00
215037095f remove extra parenthesis oops.. 2022-06-09 10:15:43 -05:00
5433d7d1d2 add total ranked client number for stats pages 2022-06-09 09:56:41 -05:00
0446fe1ec5 revert time out for status preventing server from entering unreachable state 2022-06-08 09:10:31 -05:00
cf2a00e5b3 add game to player profile and admins page 2022-06-07 21:58:32 -05:00
ab494a22cb add mwr to game list (h1) 2022-06-07 12:10:39 -05:00
b690579154 fix issue with meta event context after 1st page load 2022-06-05 16:35:39 -05:00
acc967e50a add ban management page 2022-06-05 16:27:56 -05:00
c493fbe13d add game badge to server overview 2022-06-04 09:58:30 -05:00
ee56a5db1f fix map/gametype alignment on server overview and add back ip display on connect click 2022-06-04 09:21:08 -05:00
f235d0fafd update for pluto t5 rcon issue 2022-06-03 17:01:58 -05:00
7ecf516278 add plutonium T5 parser. Must use ManualLogPath 2022-06-03 16:26:58 -05:00
210f1ca336 fix incorrect wildcard colorcode 2022-06-02 19:59:09 -05:00
158 changed files with 17986 additions and 977 deletions

View File

@ -0,0 +1,55 @@
using System;
using SharedLibraryCore;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Database.Models;
namespace IW4MAdmin.Application.Alerts;
public static class AlertExtensions
{
public static Alert.AlertState BuildAlert(this EFClient client, Alert.AlertCategory? type = null)
{
return new Alert.AlertState
{
RecipientId = client.ClientId,
Category = type ?? Alert.AlertCategory.Information
};
}
public static Alert.AlertState WithCategory(this Alert.AlertState state, Alert.AlertCategory category)
{
state.Category = category;
return state;
}
public static Alert.AlertState OfType(this Alert.AlertState state, string type)
{
state.Type = type;
return state;
}
public static Alert.AlertState WithMessage(this Alert.AlertState state, string message)
{
state.Message = message;
return state;
}
public static Alert.AlertState ExpiresIn(this Alert.AlertState state, TimeSpan expiration)
{
state.ExpiresAt = DateTime.Now.Add(expiration);
return state;
}
public static Alert.AlertState FromSource(this Alert.AlertState state, string source)
{
state.Source = source;
return state;
}
public static Alert.AlertState FromClient(this Alert.AlertState state, EFClient client)
{
state.Source = client.Name.StripColors();
state.SourceId = client.ClientId;
return state;
}
}

View File

@ -0,0 +1,137 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Alerts;
public class AlertManager : IAlertManager
{
private readonly ApplicationConfiguration _appConfig;
private readonly ConcurrentDictionary<int, List<Alert.AlertState>> _states = new();
private readonly List<Func<Task<IEnumerable<Alert.AlertState>>>> _staticSources = new();
public AlertManager(ApplicationConfiguration appConfig)
{
_appConfig = appConfig;
_states.TryAdd(0, new List<Alert.AlertState>());
}
public EventHandler<Alert.AlertState> OnAlertConsumed { get; set; }
public async Task Initialize()
{
foreach (var source in _staticSources)
{
var alerts = await source();
foreach (var alert in alerts)
{
AddAlert(alert);
}
}
}
public IEnumerable<Alert.AlertState> RetrieveAlerts(EFClient client)
{
lock (_states)
{
var alerts = Enumerable.Empty<Alert.AlertState>();
if (client.Level > Data.Models.Client.EFClient.Permission.Trusted)
{
alerts = alerts.Concat(_states[0].Where(alert =>
alert.MinimumPermission is null || alert.MinimumPermission <= client.Level));
}
if (_states.ContainsKey(client.ClientId))
{
alerts = alerts.Concat(_states[client.ClientId].AsReadOnly());
}
return alerts.OrderByDescending(alert => alert.OccuredAt);
}
}
public void MarkAlertAsRead(Guid alertId)
{
lock (_states)
{
foreach (var items in _states.Values)
{
var matchingEvent = items.FirstOrDefault(item => item.AlertId == alertId);
if (matchingEvent is null)
{
continue;
}
items.Remove(matchingEvent);
OnAlertConsumed?.Invoke(this, matchingEvent);
}
}
}
public void MarkAllAlertsAsRead(int recipientId)
{
lock (_states)
{
foreach (var items in _states.Values)
{
items.RemoveAll(item =>
{
if (item.RecipientId != null && item.RecipientId != recipientId)
{
return false;
}
OnAlertConsumed?.Invoke(this, item);
return true;
});
}
}
}
public void AddAlert(Alert.AlertState alert)
{
lock (_states)
{
if (alert.RecipientId is null)
{
_states[0].Add(alert);
return;
}
if (!_states.ContainsKey(alert.RecipientId.Value))
{
_states[alert.RecipientId.Value] = new List<Alert.AlertState>();
}
if (_appConfig.MinimumAlertPermissions.ContainsKey(alert.Type))
{
alert.MinimumPermission = _appConfig.MinimumAlertPermissions[alert.Type];
}
_states[alert.RecipientId.Value].Add(alert);
PruneOldAlerts();
}
}
public void RegisterStaticAlertSource(Func<Task<IEnumerable<Alert.AlertState>>> alertSource)
{
_staticSources.Add(alertSource);
}
private void PruneOldAlerts()
{
foreach (var value in _states.Values)
{
value.RemoveAll(item => item.ExpiresAt < DateTime.UtcNow);
}
}
}

View File

@ -57,10 +57,11 @@ namespace IW4MAdmin.Application
private readonly List<MessageToken> MessageTokens;
private readonly ClientService ClientSvc;
readonly PenaltyService PenaltySvc;
private readonly IAlertManager _alertManager;
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;
@ -82,18 +83,19 @@ namespace IW4MAdmin.Application
IEnumerable<IPlugin> plugins, IParserRegexFactory parserRegexFactory, IEnumerable<IRegisterEvent> customParserEvents,
IEventHandler eventHandler, IScriptCommandFactory scriptCommandFactory, IDatabaseContextFactory contextFactory,
IMetaRegistration metaRegistration, IScriptPluginServiceResolver scriptPluginServiceResolver, ClientService clientService, IServiceProvider serviceProvider,
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService)
ChangeHistoryService changeHistoryService, ApplicationConfiguration appConfig, PenaltyService penaltyService, IAlertManager alertManager)
{
MiddlewareActionHandler = actionHandler;
_servers = new ConcurrentBag<Server>();
MessageTokens = new List<MessageToken>();
ClientSvc = clientService;
PenaltySvc = penaltyService;
_alertManager = alertManager;
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();
@ -508,6 +510,7 @@ namespace IW4MAdmin.Application
#endregion
_metaRegistration.Register();
await _alertManager.Initialize();
#region CUSTOM_EVENTS
foreach (var customEvent in _customParserEvents.SelectMany(_events => _events.Events))
@ -610,6 +613,7 @@ namespace IW4MAdmin.Application
{
IsRestartRequested = true;
Stop().GetAwaiter().GetResult();
_tokenSource = new CancellationTokenSource();
}
[Obsolete]
@ -629,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()
@ -697,5 +701,6 @@ namespace IW4MAdmin.Application
}
public void RemoveCommandByName(string commandName) => _commands.RemoveAll(_command => _command.Name == commandName);
public IAlertManager AlertManager => _alertManager;
}
}

View File

@ -1,11 +1,14 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models.Client;
using Data.Models.Misc;
using IW4MAdmin.Application.Alerts;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using SharedLibraryCore;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
using ILogger = Microsoft.Extensions.Logging.ILogger;
@ -16,19 +19,66 @@ namespace IW4MAdmin.Application.Commands
{
private readonly IDatabaseContextFactory _contextFactory;
private readonly ILogger _logger;
private readonly IAlertManager _alertManager;
private const short MaxLength = 1024;
public OfflineMessageCommand(CommandConfiguration config, ITranslationLookup layout,
IDatabaseContextFactory contextFactory, ILogger<IDatabaseContextFactory> logger) : base(config, layout)
IDatabaseContextFactory contextFactory, ILogger<IDatabaseContextFactory> logger, IAlertManager alertManager)
: base(config, layout)
{
Name = "offlinemessage";
Description = _translationLookup["COMMANDS_OFFLINE_MESSAGE_DESC"];
Alias = "om";
Permission = EFClient.Permission.Moderator;
RequiresTarget = true;
_contextFactory = contextFactory;
_logger = logger;
_alertManager = alertManager;
_alertManager.RegisterStaticAlertSource(async () =>
{
var context = contextFactory.CreateContext(false);
return await context.InboxMessages.Where(message => !message.IsDelivered)
.Where(message => message.CreatedDateTime >= DateTime.UtcNow.AddDays(-7))
.Where(message => message.DestinationClient.Level > EFClient.Permission.User)
.Select(message => new Alert.AlertState
{
OccuredAt = message.CreatedDateTime,
Message = message.Message,
ExpiresAt = DateTime.UtcNow.AddDays(7),
Category = Alert.AlertCategory.Message,
Source = message.SourceClient.CurrentAlias.Name.StripColors(),
SourceId = message.SourceClientId,
RecipientId = message.DestinationClientId,
ReferenceId = message.InboxMessageId,
Type = nameof(EFInboxMessage)
}).ToListAsync();
});
_alertManager.OnAlertConsumed += (_, state) =>
{
if (state.Category != Alert.AlertCategory.Message || state.ReferenceId is null)
{
return;
}
try
{
var context = contextFactory.CreateContext(true);
foreach (var message in context.InboxMessages
.Where(message => message.InboxMessageId == state.ReferenceId.Value).ToList())
{
message.IsDelivered = true;
}
context.SaveChanges();
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not update message state for alert {@Alert}", state);
}
};
}
public override async Task ExecuteAsync(GameEvent gameEvent)
@ -38,23 +88,24 @@ namespace IW4MAdmin.Application.Commands
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_TOO_LONG"].FormatExt(MaxLength));
return;
}
if (gameEvent.Target.ClientId == gameEvent.Origin.ClientId)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_SELF"].FormatExt(MaxLength));
return;
}
if (gameEvent.Target.IsIngame)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_INGAME"].FormatExt(gameEvent.Target.Name));
gameEvent.Origin.Tell(_translationLookup["COMMANDS_OFFLINE_MESSAGE_INGAME"]
.FormatExt(gameEvent.Target.Name));
return;
}
await using var context = _contextFactory.CreateContext(enableTracking: false);
var server = await context.Servers.FirstAsync(srv => srv.EndPoint == gameEvent.Owner.ToString());
var newMessage = new EFInboxMessage()
var newMessage = new EFInboxMessage
{
SourceClientId = gameEvent.Origin.ClientId,
DestinationClientId = gameEvent.Target.ClientId,
@ -62,6 +113,12 @@ namespace IW4MAdmin.Application.Commands
Message = gameEvent.Data,
};
_alertManager.AddAlert(gameEvent.Target.BuildAlert(Alert.AlertCategory.Message)
.WithMessage(gameEvent.Data.Trim())
.FromClient(gameEvent.Origin)
.OfType(nameof(EFInboxMessage))
.ExpiresIn(TimeSpan.FromDays(7)));
try
{
context.Set<EFInboxMessage>().Add(newMessage);
@ -75,4 +132,4 @@ namespace IW4MAdmin.Application.Commands
}
}
}
}
}

View File

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

View File

@ -24,8 +24,10 @@ using Serilog.Context;
using static SharedLibraryCore.Database.Models.EFClient;
using Data.Models;
using Data.Models.Server;
using IW4MAdmin.Application.Alerts;
using IW4MAdmin.Application.Commands;
using Microsoft.EntityFrameworkCore;
using SharedLibraryCore.Alerts;
using static Data.Models.Client.EFClient;
namespace IW4MAdmin
@ -73,7 +75,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)
@ -116,7 +118,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()))
{
@ -152,10 +154,10 @@ namespace IW4MAdmin
{
if (E.IsBlocking)
{
await E.Origin?.Lock();
await E.Origin.Lock();
}
bool canExecuteCommand = true;
var canExecuteCommand = true;
try
{
@ -164,30 +166,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)
{
@ -202,15 +204,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);
@ -306,8 +308,16 @@ namespace IW4MAdmin
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
{
Console.WriteLine(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"));
var alert = Alert.AlertState.Build().OfType(E.Type.ToString())
.WithCategory(Alert.AlertCategory.Error)
.FromSource("System")
.WithMessage(loc["SERVER_ERROR_COMMUNICATION"].FormatExt($"{IP}:{Port}"))
.ExpiresIn(TimeSpan.FromDays(1));
Manager.AlertManager.AddAlert(alert);
}
Throttled = true;
}
@ -318,7 +328,15 @@ namespace IW4MAdmin
if (!Manager.GetApplicationSettings().Configuration().IgnoreServerConnectionLost)
{
Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"[{IP}:{Port}]"));
Console.WriteLine(loc["MANAGER_CONNECTION_REST"].FormatExt($"{IP}:{Port}"));
var alert = Alert.AlertState.Build().OfType(E.Type.ToString())
.WithCategory(Alert.AlertCategory.Information)
.FromSource("System")
.WithMessage(loc["MANAGER_CONNECTION_REST"].FormatExt($"{IP}:{Port}"))
.ExpiresIn(TimeSpan.FromDays(1));
Manager.AlertManager.AddAlert(alert);
}
if (!string.IsNullOrEmpty(CustomSayName))
@ -355,9 +373,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
@ -431,7 +449,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;
}
@ -499,7 +517,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);
}
@ -671,23 +689,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)
@ -723,6 +768,23 @@ namespace IW4MAdmin
{
E.Origin.UpdateTeam(E.Extra as string);
}
else if (E.Type == GameEvent.EventType.MetaUpdated)
{
if (E.Extra is "PersistentStatClientId" && int.TryParse(E.Data, out var persistentClientId))
{
var penalties = await Manager.GetPenaltyService().GetActivePenaltiesByClientId(persistentClientId);
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(), persistentClientId);
E.Origin.Ban(loc["SERVER_BAN_EVADE"].FormatExt(persistentClientId), Utilities.IW4MAdminClient(this), true);
}
}
}
lock (ChatHistory)
{
@ -745,7 +807,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)
{
@ -790,12 +852,10 @@ namespace IW4MAdmin
/// array index 2 = updated clients
/// </summary>
/// <returns></returns>
async Task<List<EFClient>[]> PollPlayersAsync()
async Task<List<EFClient>[]> PollPlayersAsync(CancellationToken token)
{
var tokenSource = new CancellationTokenSource();
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
var currentClients = GetClientsAsList();
var statusResponse = await this.GetStatusAsync(tokenSource.Token);
var statusResponse = await this.GetStatusAsync(token);
if (statusResponse is null)
{
@ -918,11 +978,11 @@ namespace IW4MAdmin
private DateTime _lastMessageSent = DateTime.Now;
private DateTime _lastPlayerCount = DateTime.Now;
public override async Task<bool> ProcessUpdatesAsync(CancellationToken cts)
public override async Task<bool> ProcessUpdatesAsync(CancellationToken token)
{
try
{
if (cts.IsCancellationRequested)
if (token.IsCancellationRequested)
{
await ShutdownInternal();
return true;
@ -936,7 +996,7 @@ namespace IW4MAdmin
return true;
}
var polledClients = await PollPlayersAsync();
var polledClients = await PollPlayersAsync(token);
if (polledClients is null)
{
@ -947,7 +1007,7 @@ namespace IW4MAdmin
.Where(client => !client.IsZombieClient /* ignores "fake" zombie clients */))
{
disconnectingClient.CurrentServer = this;
var e = new GameEvent()
var e = new GameEvent
{
Type = GameEvent.EventType.PreDisconnect,
Origin = disconnectingClient,
@ -964,7 +1024,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
{
@ -1456,6 +1516,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)
{
@ -1486,6 +1551,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());
@ -1514,7 +1584,7 @@ namespace IW4MAdmin
ServerLogger.LogDebug("Creating unban penalty for {targetClient}", targetClient.ToString());
targetClient.SetLevel(Permission.User, originClient);
await Manager.GetPenaltyService().RemoveActivePenalties(targetClient.AliasLink.AliasLinkId,
targetClient.NetworkId, targetClient.CurrentAlias?.IPAddress);
targetClient.NetworkId, targetClient.GameName, targetClient.CurrentAlias?.IPAddress);
await Manager.GetPenaltyService().Create(unbanPenalty);
}

View File

@ -27,6 +27,7 @@ using System.Threading.Tasks;
using Data.Abstractions;
using Data.Helpers;
using Integrations.Source.Extensions;
using IW4MAdmin.Application.Alerts;
using IW4MAdmin.Application.Extensions;
using IW4MAdmin.Application.Localization;
using Microsoft.Extensions.Logging;
@ -448,6 +449,7 @@ namespace IW4MAdmin.Application
.AddSingleton<IServerDataCollector, ServerDataCollector>()
.AddSingleton<IEventPublisher, EventPublisher>()
.AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb")))
.AddSingleton<IAlertManager, AlertManager>()
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
.AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig);

View File

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

View File

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

View File

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

View File

@ -5,6 +5,7 @@ using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models.Client;
using Data.Models.Client.Stats;
using Data.Models.Server;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
@ -22,18 +23,20 @@ namespace IW4MAdmin.Application.Misc
private readonly IDataValueCache<EFServerSnapshot, (int?, DateTime?)> _snapshotCache;
private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache;
private readonly IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> _clientHistoryCache;
private readonly IDataValueCache<EFClientRankingHistory, int> _rankedClientsCache;
private readonly TimeSpan? _cacheTimeSpan =
Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?) TimeSpan.FromMinutes(10);
public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, (int?, DateTime?)> snapshotCache,
IDataValueCache<EFClient, (int, int)> serverStatsCache,
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache)
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache, IDataValueCache<EFClientRankingHistory, int> rankedClientsCache)
{
_logger = logger;
_snapshotCache = snapshotCache;
_serverStatsCache = serverStatsCache;
_clientHistoryCache = clientHistoryCache;
_rankedClientsCache = rankedClientsCache;
}
public async Task<(int?, DateTime?)>
@ -160,5 +163,30 @@ namespace IW4MAdmin.Application.Misc
return Enumerable.Empty<ClientHistoryInfo>();
}
}
public async Task<int> RankedClientsCountAsync(long? serverId = null, CancellationToken token = default)
{
_rankedClientsCache.SetCacheItem(async (set, cancellationToken) =>
{
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
return await set
.Where(rating => rating.Newest)
.Where(rating => rating.ServerId == serverId)
.Where(rating => rating.CreatedDateTime >= fifteenDaysAgo)
.Where(rating => rating.Client.Level != EFClient.Permission.Banned)
.Where(rating => rating.Ranking != null)
.CountAsync(cancellationToken);
}, nameof(_rankedClientsCache), serverId is null ? null: new[] { (object)serverId }, _cacheTimeSpan);
try
{
return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), serverId, token);
}
catch (Exception ex)
{
_logger.LogError(ex, "Could not retrieve data for {Name}", nameof(RankedClientsCountAsync));
return 0;
}
}
}
}

View File

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

View File

@ -20,6 +20,7 @@ namespace IW4MAdmin.Application.RConParsers
public class BaseRConParser : IRConParser
{
private readonly ILogger _logger;
private static string _botIpIndicator = "00000000.";
public BaseRConParser(ILogger<BaseRConParser> logger, IParserRegexFactory parserRegexFactory)
{
@ -52,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);
@ -81,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();
}
@ -104,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") ||
@ -290,8 +291,15 @@ namespace IW4MAdmin.Application.RConParsers
long networkId;
var name = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConName]].TrimNewLine();
string networkIdString;
var ip = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]].Split(':')[0].ConvertToIP();
if (match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConIpAddress]]
.Contains(_botIpIndicator))
{
ip = System.Net.IPAddress.Broadcast.ToString().ConvertToIP();
}
try
{
networkIdString = match.Values[Configuration.Status.GroupMapping[ParserRegex.GroupType.RConNetworkId]];
@ -306,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
@ -360,15 +368,28 @@ namespace IW4MAdmin.Application.RConParsers
(T)Convert.ChangeType(Configuration.DefaultDvarValues[dvarName], typeof(T)) :
default;
public TimeSpan OverrideTimeoutForCommand(string command)
public TimeSpan? OverrideTimeoutForCommand(string command)
{
if (command.Contains("map_rotate", StringComparison.InvariantCultureIgnoreCase) ||
command.StartsWith("map ", StringComparison.InvariantCultureIgnoreCase))
if (string.IsNullOrEmpty(command))
{
return TimeSpan.FromSeconds(30);
return TimeSpan.Zero;
}
var commandToken = command.Split(' ', StringSplitOptions.RemoveEmptyEntries).First().ToLower();
if (!Configuration.OverrideCommandTimeouts.ContainsKey(commandToken))
{
return TimeSpan.Zero;
}
return TimeSpan.Zero;
var timeoutValue = Configuration.OverrideCommandTimeouts[commandToken];
if (timeoutValue.HasValue && timeoutValue.Value != 0) // JINT doesn't seem to be able to properly set nulls on dictionaries
{
return TimeSpan.FromSeconds(timeoutValue.Value);
}
return null;
}
}
}

View File

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

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
@ -9,6 +10,11 @@ namespace Data.Abstractions
{
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
TimeSpan? expirationTime = null, bool autoRefresh = false);
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false);
Task<TReturnType> GetCacheItem(string keyName, CancellationToken token = default);
Task<TReturnType> GetCacheItem(string keyName, object id = null, CancellationToken token = default);
}
}
}

View File

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

View File

@ -1,5 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
@ -15,8 +17,8 @@ namespace Data.Helpers
private readonly ILogger _logger;
private readonly IDatabaseContextFactory _contextFactory;
private readonly ConcurrentDictionary<string, CacheState<TReturnType>> _cacheStates =
new ConcurrentDictionary<string, CacheState<TReturnType>>();
private readonly ConcurrentDictionary<string, Dictionary<object, CacheState<TReturnType>>> _cacheStates = new();
private readonly object _defaultKey = new();
private bool _autoRefresh;
private const int DefaultExpireMinutes = 15;
@ -51,41 +53,61 @@ namespace Data.Helpers
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
TimeSpan? expirationTime = null, bool autoRefresh = false)
{
if (_cacheStates.ContainsKey(key))
{
_logger.LogDebug("Cache key {Key} is already added", key);
return;
}
var state = new CacheState<TReturnType>
{
Key = key,
Getter = getter,
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
};
_autoRefresh = autoRefresh;
_cacheStates.TryAdd(key, state);
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
{
return;
}
_timer = new Timer(state.ExpirationTime.TotalMilliseconds);
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None);
_timer.Start();
SetCacheItem(getter, key, null, expirationTime, autoRefresh);
}
public async Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default)
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false)
{
ids ??= new[] { _defaultKey };
if (!_cacheStates.ContainsKey(key))
{
_cacheStates.TryAdd(key, new Dictionary<object, CacheState<TReturnType>>());
}
foreach (var id in ids)
{
if (_cacheStates[key].ContainsKey(id))
{
continue;
}
var state = new CacheState<TReturnType>
{
Key = key,
Getter = getter,
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
};
_cacheStates[key].Add(id, state);
_autoRefresh = autoRefresh;
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
{
return;
}
_timer = new Timer(state.ExpirationTime.TotalMilliseconds);
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None);
_timer.Start();
}
}
public async Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default) =>
await GetCacheItem(keyName, null, cancellationToken);
public async Task<TReturnType> GetCacheItem(string keyName, object id = null,
CancellationToken cancellationToken = default)
{
if (!_cacheStates.ContainsKey(keyName))
{
throw new ArgumentException("No cache found for key {key}", keyName);
}
var state = _cacheStates[keyName];
var state = id is null ? _cacheStates[keyName].Values.First() : _cacheStates[keyName][id];
// when auto refresh is off we want to check the expiration and value
// when auto refresh is on, we want to only check the value, because it'll be refreshed automatically
@ -115,4 +137,4 @@ namespace Data.Helpers
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.MySql
{
public partial class AddIndexToEFRankingHistoryCreatedDatetime : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory",
column: "CreatedDateTime");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory");
}
}
}

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Postgresql
{
public partial class AddIndexToEFRankingHistoryCreatedDatetime : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory",
column: "CreatedDateTime");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory");
}
}
}

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Data.Migrations.Sqlite
{
public partial class AddIndexToEFRankingHistoryCreatedDatetime : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory",
column: "CreatedDateTime");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_EFClientRankingHistory_CreatedDateTime",
table: "EFClientRankingHistory");
}
}
}

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@ -86,7 +86,8 @@ namespace Data.Models.Configuration
entity.HasIndex(ranking => ranking.Ranking);
entity.HasIndex(ranking => ranking.ZScore);
entity.HasIndex(ranking => ranking.UpdatedDateTime);
entity.HasIndex(ranking => ranking.CreatedDateTime);
});
}
}
}
}

View File

@ -15,7 +15,8 @@
T6 = 7,
T7 = 8,
SHG1 = 9,
CSGO = 10
CSGO = 10,
H1 = 11
}
public enum ConnectionType
@ -24,4 +25,4 @@
Disconnect
}
}
}
}

View File

@ -53,8 +53,6 @@ init()
level thread OnPlayerConnect();
}
//////////////////////////////////
// Client Methods
//////////////////////////////////
@ -167,6 +165,28 @@ DisplayWelcomeData()
self IPrintLnBold( "You were last seen ^5" + clientData.lastConnection );
}
SetPersistentData()
{
storedClientId = self GetPlayerData( "bests", "none" );
if ( storedClientId != 0 )
{
if ( level.iw4adminIntegrationDebug == 1 )
{
IPrintLn( "Uploading persistent client id " + storedClientId );
}
SetClientMeta( "PersistentStatClientId", storedClientId );
}
if ( level.iw4adminIntegrationDebug == 1 )
{
IPrintLn( "Persisting client id " + self.persistentClientId );
}
self SetPlayerData( "bests", "none", int( self.persistentClientId ) );
}
PlayerConnectEvents()
{
self endon( "disconnect" );
@ -643,6 +663,7 @@ OnClientDataReceived( event )
self.persistentClientId = event.data["clientId"];
self thread DisplayWelcomeData();
self setPersistentData();
}
OnExecuteCommand( event )

View File

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

View File

@ -253,8 +253,10 @@ namespace Integrations.Cod
try
{
connectionState.LastQuery = DateTime.Now;
var timeout = _parser.OverrideTimeoutForCommand(parameters);
waitForResponse = waitForResponse && timeout.HasValue;
response = await SendPayloadAsync(payload, waitForResponse,
_parser.OverrideTimeoutForCommand(parameters), token);
timeout ?? TimeSpan.Zero, token);
if ((response?.Length == 0 || response[0].Length == 0) && waitForResponse)
{
@ -368,7 +370,9 @@ namespace Integrations.Cod
throw new RConException("Unexpected response header from server");
}
var splitResponse = headerSplit.Last().Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
var splitResponse = headerSplit.Last().Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.StartsWith("^7") ? line[2..] : line).ToArray();
return splitResponse;
}
@ -454,6 +458,12 @@ namespace Integrations.Cod
connectionState.SendEventArgs.DisconnectReuseSocket = true;
}
if (connectionState.ReceiveEventArgs.UserToken is ConnectionUserToken { CancellationToken.IsCancellationRequested: true })
{
// after a graceful restart we need to reset the receive user token as the cancellation has been updated
connectionState.ReceiveEventArgs.UserToken = connectionState.SendEventArgs.UserToken;
}
connectionState.SendEventArgs.SetBuffer(payload);
// send the data to the server

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,44 @@
var rconParser;
var eventParser;
var plugin = {
author: 'RaidMax',
version: 0.2,
name: 'Plutonium T5 Parser',
isParser: true,
onEventAsync: function (gameEvent, server) {
},
onLoadAsync: function (manager) {
rconParser = manager.GenerateDynamicRConParser(this.name);
eventParser = manager.GenerateDynamicEventParser(this.name);
rconParser.Configuration.DefaultInstallationDirectoryHint = '{LocalAppData}/Plutonium/storage/t5';
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n';
rconParser.Configuration.Dvar.Pattern = '^(?:\\^7)?\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n?(?:latched: \\"(.+)?\\"\\n)?\\w*(.+)*$';
rconParser.Configuration.CommandPrefixes.Tell = 'tell {0} {1}';
rconParser.Configuration.CommandPrefixes.RConGetInfo = undefined;
rconParser.Configuration.GuidNumberStyle = 7; // Integer
rconParser.Configuration.DefaultRConPort = 3074;
rconParser.Configuration.CanGenerateLogPath = false;
rconParser.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';
eventParser.GameName = 6; // T5
eventParser.Configuration.GuidNumberStyle = 7; // Integer
},
onUnloadAsync: function () {
},
onTickAsync: function (server) {
}
};

View File

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

View File

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

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Abstractions;
using Data.Models.Client;
@ -88,8 +89,8 @@ namespace Stats.Client
return zScore ?? 0;
}, MaxZScoreCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(5) : TimeSpan.FromMinutes(30));
await _distributionCache.GetCacheItem(DistributionCacheKey);
await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey);
await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken());
await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new CancellationToken());
/*foreach (var serverId in _serverIds)
{
@ -132,7 +133,7 @@ namespace Stats.Client
public async Task<double> GetZScoreForServer(long serverId, double value)
{
var serverParams = await _distributionCache.GetCacheItem(DistributionCacheKey);
var serverParams = await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken());
if (!serverParams.ContainsKey(serverId))
{
return 0.0;
@ -150,7 +151,7 @@ namespace Stats.Client
public async Task<double?> GetRatingForZScore(double? value)
{
var maxZScore = await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey);
var maxZScore = await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new CancellationToken());
return maxZScore == 0 ? null : value.GetRatingForZScore(maxZScore);
}
}

View File

@ -79,7 +79,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
}
else
{
gameEvent.Owner.Broadcast(topStats);
await gameEvent.Owner.BroadcastAsync(topStats);
}
}
}

View File

@ -14,6 +14,7 @@ namespace Stats.Dtos
public EFClient.Permission Level { get; set; }
public double? Performance { get; set; }
public int? Ranking { get; set; }
public int TotalRankedClients { get; set; }
public double? ZScore { get; set; }
public double? Rating { get; set; }
public List<ServerInfo> Servers { get; set; }
@ -25,4 +26,4 @@ namespace Stats.Dtos
public List<EFClientRankingHistory> Ratings { get; set; }
public List<EFClientStatistics> LegacyStats { get; set; }
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -188,29 +188,32 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
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();
.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 => 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].Last().ZScore,
ServerId = serverId
})
.OrderBy(r => r.Ranking)
.Take(60)
.ToList();
return finished;
}
@ -289,7 +292,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 +305,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)
})

View File

@ -42,10 +42,11 @@ namespace IW4MAdmin.Plugins.Stats
private readonly ILogger<Plugin> _logger;
private readonly List<IClientStatisticCalculator> _statCalculators;
private readonly IServerDistributionCalculator _serverDistributionCalculator;
private readonly IServerDataViewer _serverDataViewer;
public Plugin(ILogger<Plugin> logger, IConfigurationHandlerFactory configurationHandlerFactory, IDatabaseContextFactory databaseContextFactory,
ITranslationLookup translationLookup, IMetaServiceV2 metaService, IResourceQueryHelper<ChatSearchQuery, MessageResponse> chatQueryHelper, ILogger<StatManager> managerLogger,
IEnumerable<IClientStatisticCalculator> statCalculators, IServerDistributionCalculator serverDistributionCalculator)
IEnumerable<IClientStatisticCalculator> statCalculators, IServerDistributionCalculator serverDistributionCalculator, IServerDataViewer serverDataViewer)
{
Config = configurationHandlerFactory.GetConfigurationHandler<StatsConfiguration>("StatsPluginSettings");
_databaseContextFactory = databaseContextFactory;
@ -56,6 +57,7 @@ namespace IW4MAdmin.Plugins.Stats
_logger = logger;
_statCalculators = statCalculators.ToList();
_serverDistributionCalculator = serverDistributionCalculator;
_serverDataViewer = serverDataViewer;
}
public async Task OnEventAsync(GameEvent gameEvent, Server server)
@ -201,13 +203,17 @@ namespace IW4MAdmin.Plugins.Stats
var performancePlayTime = validPerformanceValues.Sum(s => s.TimePlayed);
var performance = Math.Round(validPerformanceValues.Sum(c => c.Performance * c.TimePlayed / performancePlayTime), 2);
var spm = Math.Round(clientStats.Sum(c => c.SPM) / clientStats.Count(c => c.SPM > 0), 1);
var overallRanking = await Manager.GetClientOverallRanking(request.ClientId);
return new List<InformationResponse>
{
new InformationResponse
{
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING"],
Value = "#" + (await Manager.GetClientOverallRanking(request.ClientId)).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
Value = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING_FORMAT"].FormatExt((overallRanking == 0 ? "--" :
overallRanking.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))),
(await _serverDataViewer.RankedClientsCountAsync(token: token)).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))
),
Column = 0,
Order = 0,
Type = MetaType.Information

View File

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

View File

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

View File

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

View File

@ -0,0 +1,33 @@
using System;
using Data.Models.Client;
namespace SharedLibraryCore.Alerts;
public class Alert
{
public enum AlertCategory
{
Information,
Warning,
Error,
Message,
}
public class AlertState
{
public Guid AlertId { get; } = Guid.NewGuid();
public AlertCategory Category { get; set; }
public DateTime OccuredAt { get; set; } = DateTime.UtcNow;
public DateTime? ExpiresAt { get; set; }
public string Message { get; set; }
public string Source { get; set; }
public int? RecipientId { get; set; }
public int? SourceId { get; set; }
public int? ReferenceId { get; set; }
public bool? Delivered { get; set; }
public bool? Consumed { get; set; }
public EFClient.Permission? MinimumPermission { get; set; }
public string Type { get; set; }
public static AlertState Build() => new();
}
}

View File

@ -4,7 +4,6 @@ using System.Globalization;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Data.Context;
using Data.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
@ -20,28 +19,37 @@ namespace SharedLibraryCore
{
public class BaseController : Controller
{
protected readonly IAlertManager AlertManager;
/// <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
@ -56,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,
@ -64,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)
{
@ -76,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
});
@ -96,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),
@ -104,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);
}
}
@ -131,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();
@ -150,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
@ -169,6 +177,7 @@ namespace SharedLibraryCore
ViewBag.ReportCount = Manager.GetServers().Sum(server =>
server.Reports.Count(report => DateTime.UtcNow - report.ReportedOn <= TimeSpan.FromHours(24)));
ViewBag.PermissionsSet = PermissionsSet;
ViewBag.Alerts = AlertManager.RetrieveAlerts(Client).ToList();
base.OnActionExecuting(context);
}

View File

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

View File

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

View File

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

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using Data.Models.Misc;
using Newtonsoft.Json;
using SharedLibraryCore.Configuration.Attributes;
using SharedLibraryCore.Interfaces;
@ -154,6 +155,13 @@ namespace SharedLibraryCore.Configuration
{ Permission.Console.ToString(), new List<string> { "*" } }
};
public Dictionary<string, Permission> MinimumAlertPermissions { get; set; } = new()
{
{ nameof(EFInboxMessage), Permission.Trusted },
{ GameEvent.EventType.ConnectionLost.ToString(), Permission.Administrator },
{ GameEvent.EventType.ConnectionRestored.ToString(), Permission.Administrator }
};
[ConfigurationIgnore]
[LocalizedDisplayName("WEBFRONT_CONFIGURATION_PRESET_BAN_REASONS")]
public Dictionary<string, string> PresetPenaltyReasons { get; set; } = new()

View File

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

View File

@ -1,4 +1,5 @@
using System;
using Data.Models;
using Data.Models.Client;
namespace SharedLibraryCore.Dtos
@ -10,6 +11,7 @@ namespace SharedLibraryCore.Dtos
public int LinkId { get; set; }
public EFClient.Permission Level { get; set; }
public DateTime LastConnection { get; set; }
public Reference.Game Game { get; set; }
public bool IsMasked { get; set; }
}
}

View File

@ -9,6 +9,7 @@ namespace SharedLibraryCore.Dtos
public class PlayerInfo
{
public string Name { get; set; }
public Reference.Game Game { get; set; }
public int ClientId { get; set; }
public string Level { get; set; }
public string Tag { get; set; }

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Data.Models;
using SharedLibraryCore.Helpers;
namespace SharedLibraryCore.Dtos
@ -40,5 +41,6 @@ namespace SharedLibraryCore.Dtos
return Math.Round(valid.Select(player => player.ZScore.Value).Average(), 2);
}
}
public Reference.Game Game { get; set; }
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using SharedLibraryCore.Alerts;
using SharedLibraryCore.Database.Models;
namespace SharedLibraryCore.Interfaces;
public interface IAlertManager
{
/// <summary>
/// Initializes the manager
/// </summary>
/// <returns></returns>
Task Initialize();
/// <summary>
/// Get all the alerts for given client
/// </summary>
/// <param name="client">client to retrieve alerts for</param>
/// <returns></returns>
IEnumerable<Alert.AlertState> RetrieveAlerts(EFClient client);
/// <summary>
/// Trigger a new alert
/// </summary>
/// <param name="alert">Alert to trigger</param>
void AddAlert(Alert.AlertState alert);
/// <summary>
/// Marks an alert as read and removes it from the manager
/// </summary>
/// <param name="alertId">Id of the alert to mark as read</param>
void MarkAlertAsRead(Guid alertId);
/// <summary>
/// Mark all alerts intended for the given recipientId as read
/// </summary>
/// <param name="recipientId">Identifier of the recipient</param>
void MarkAllAlertsAsRead(int recipientId);
/// <summary>
/// Registers a static (persistent) event source eg datastore that
/// gets initialized at startup
/// </summary>
/// <param name="alertSource">Source action</param>
void RegisterStaticAlertSource(Func<Task<IEnumerable<Alert.AlertState>>> alertSource);
/// <summary>
/// Fires when an alert has been consumed (dimissed)
/// </summary>
EventHandler<Alert.AlertState> OnAlertConsumed { get; set; }
}

View File

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

View File

@ -102,5 +102,7 @@ namespace SharedLibraryCore.Interfaces
/// event executed when event has finished executing
/// </summary>
event EventHandler<GameEvent> OnGameEventExecuted;
IAlertManager AlertManager { get; }
}
}

View File

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

View File

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

View File

@ -37,5 +37,13 @@ namespace SharedLibraryCore.Interfaces
/// <returns></returns>
Task<IEnumerable<ClientHistoryInfo>> ClientHistoryAsync(TimeSpan? overPeriod = null,
CancellationToken token = default);
/// <summary>
/// Retrieves the number of ranked clients for given server id
/// </summary>
/// <param name="serverId">ServerId to query on</param>
/// <param name="token">CancellationToken</param>
/// <returns></returns>
Task<int> RankedClientsCountAsync(long? serverId = null, CancellationToken token = default);
}
}
}

View File

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

View File

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

View File

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

View File

@ -33,7 +33,8 @@ namespace SharedLibraryCore
T6 = 7,
T7 = 8,
SHG1 = 9,
CSGO = 10
CSGO = 10,
H1 = 11
}
// only here for performance
@ -200,7 +201,7 @@ namespace SharedLibraryCore
.ToList();
}
public virtual Task<bool> ProcessUpdatesAsync(CancellationToken cts)
public virtual Task<bool> ProcessUpdatesAsync(CancellationToken token)
{
return (Task<bool>)Task.CompletedTask;
}
@ -224,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
{
@ -288,13 +289,13 @@ namespace SharedLibraryCore
else
{
ServerLogger.LogDebug("Tell[{ClientNumber}]->{Message}", targetClient.ClientNumber,
message.FormatMessageForEngine(RconParser.Configuration.ColorCodeMapping).StripColors());
message.FormatMessageForEngine(RconParser.Configuration).StripColors());
}
if (targetClient.Level == Data.Models.Client.EFClient.Permission.Console)
{
Console.ForegroundColor = ConsoleColor.Green;
var cleanMessage = message.FormatMessageForEngine(RconParser.Configuration.ColorCodeMapping)
var cleanMessage = message.FormatMessageForEngine(RconParser.Configuration)
.StripColors();
using (LogContext.PushProperty("Server", ToString()))
{

View File

@ -23,25 +23,26 @@ namespace SharedLibraryCore.Services
{
public class ClientService : IEntityService<EFClient>, IResourceQueryHelper<FindClientRequest, FindClientResult>
{
private static readonly Func<DatabaseContext, long, Task<EFClient>> _getUniqueQuery =
EF.CompileAsyncQuery((DatabaseContext context, long networkId) =>
private static readonly Func<DatabaseContext, long, Reference.Game, Task<EFClient>> GetUniqueQuery =
EF.CompileAsyncQuery((DatabaseContext context, long networkId, Reference.Game game) =>
context.Clients
.Select(_client => new EFClient
.Select(client => new EFClient
{
ClientId = _client.ClientId,
AliasLinkId = _client.AliasLinkId,
Level = _client.Level,
Connections = _client.Connections,
FirstConnection = _client.FirstConnection,
LastConnection = _client.LastConnection,
Masked = _client.Masked,
NetworkId = _client.NetworkId,
TotalConnectionTime = _client.TotalConnectionTime,
AliasLink = _client.AliasLink,
Password = _client.Password,
PasswordSalt = _client.PasswordSalt
ClientId = client.ClientId,
AliasLinkId = client.AliasLinkId,
Level = client.Level,
Connections = client.Connections,
FirstConnection = client.FirstConnection,
LastConnection = client.LastConnection,
Masked = client.Masked,
NetworkId = client.NetworkId,
TotalConnectionTime = client.TotalConnectionTime,
AliasLink = client.AliasLink,
Password = client.Password,
PasswordSalt = client.PasswordSalt,
GameName = client.GameName
})
.FirstOrDefault(c => c.NetworkId == networkId)
.FirstOrDefault(client => client.NetworkId == networkId && client.GameName == game)
);
private readonly ApplicationConfiguration _appConfig;
@ -178,6 +179,7 @@ namespace SharedLibraryCore.Services
.Select(_client => new EFClient
{
ClientId = _client.ClientId,
GameName = _client.GameName,
AliasLinkId = _client.AliasLinkId,
Level = _client.Level,
Connections = _client.Connections,
@ -234,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)
@ -284,7 +286,7 @@ namespace SharedLibraryCore.Services
entity.PasswordSalt = temporalClient.PasswordSalt;
}
entity.GameName ??= temporalClient.GameName;
entity.GameName = temporalClient.GameName;
// update in database
await context.SaveChangesAsync();
@ -757,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)
@ -789,7 +792,8 @@ namespace SharedLibraryCore.Services
PasswordSalt = client.PasswordSalt,
NetworkId = client.NetworkId,
LastConnection = client.LastConnection,
Masked = client.Masked
Masked = client.Masked,
GameName = client.GameName
};
return await iqClients.ToListAsync();
@ -849,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;
}

View File

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

View File

@ -4,7 +4,7 @@
<OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework>
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
<Version>2022.3.23.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.3.23.1</PackageVersion>
<PackageVersion>2022.6.16.1</PackageVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

View File

@ -47,6 +47,9 @@ namespace SharedLibraryCore
public static char[] DirectorySeparatorChars = { '\\', '/' };
public static char CommandPrefix { get; set; } = '!';
public static string ToStandardFormat(this DateTime? time) => time?.ToString("yyyy-MM-dd H:mm:ss UTC");
public static string ToStandardFormat(this DateTime time) => time.ToString("yyyy-MM-dd H:mm:ss UTC");
public static EFClient IW4MAdminClient(Server server = null)
{
return new EFClient
@ -68,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)
@ -130,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;
@ -168,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;
}
@ -181,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
@ -196,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)
@ -230,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;
}
@ -259,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>
@ -430,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;
}
@ -479,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+)");
@ -605,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;
}
@ -636,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)
{
@ -664,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)
{
@ -695,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;
@ -706,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;
@ -1178,7 +1170,8 @@ namespace SharedLibraryCore
Meta = client.Meta,
ReceivedPenalties = client.ReceivedPenalties,
AdministeredPenalties = client.AdministeredPenalties,
Active = client.Active
Active = client.Active,
GameName = client.GameName
};
}
@ -1261,5 +1254,8 @@ namespace SharedLibraryCore
return allRules[index];
}
public static string MakeAbbreviation(string gameName) => string.Join("",
gameName.Split(' ').Select(word => char.ToUpper(word.First())).ToArray());
}
}

View File

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

View File

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

View File

@ -10,6 +10,7 @@ using SharedLibraryCore;
using SharedLibraryCore.Commands;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;
using WebfrontCore.Permissions;
using WebfrontCore.ViewModels;
@ -24,6 +25,7 @@ namespace WebfrontCore.Controllers
private readonly string _unbanCommandName;
private readonly string _sayCommandName;
private readonly string _kickCommandName;
private readonly string _offlineMessageCommandName;
private readonly string _flagCommandName;
private readonly string _unflagCommandName;
private readonly string _setLevelCommandName;
@ -64,6 +66,9 @@ namespace WebfrontCore.Controllers
case nameof(SetLevelCommand):
_setLevelCommandName = cmd.Name;
break;
case "OfflineMessageCommand":
_offlineMessageCommandName = cmd.Name;
break;
}
}
}
@ -73,7 +78,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()
@ -142,12 +147,12 @@ namespace WebfrontCore.Controllers
}));
}
public IActionResult UnbanForm()
public IActionResult UnbanForm(long? id)
{
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_UNBAN_NAME"],
Name = "Unban",
Name = Localization["WEBFRONT_ACTION_UNBAN_NAME"],
Inputs = new List<InputInfo>
{
new()
@ -159,6 +164,15 @@ namespace WebfrontCore.Controllers
Action = "UnbanAsync",
ShouldRefresh = true
};
if (id is not null)
{
info.Inputs.Add(new()
{
Name = "targetId",
Value = id.ToString(),
Type = "hidden"
});
}
return View("_ActionForm", info);
}
@ -179,7 +193,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()
@ -204,7 +218,7 @@ namespace WebfrontCore.Controllers
public async Task<IActionResult> Login(int clientId, string password)
{
return await Task.FromResult(RedirectToAction("Login", "Account", new {clientId, password}));
return await Task.FromResult(RedirectToAction("Login", "Account", new { clientId, password }));
}
public IActionResult EditForm()
@ -212,7 +226,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()
@ -261,7 +275,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"]}",
@ -273,7 +291,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()
@ -318,26 +336,27 @@ namespace WebfrontCore.Controllers
public async Task<IActionResult> RecentClientsForm(PaginationRequest request)
{
ViewBag.First = request.Offset == 0;
if (request.Count > 20)
{
request.Count = 20;
}
var clients = await Manager.GetClientService().GetRecentClients(request);
return request.Offset == 0
? View("~/Views/Shared/Components/Client/_RecentClientsContainer.cshtml", clients)
: View("~/Views/Shared/Components/Client/_RecentClients.cshtml", clients);
}
public IActionResult RecentReportsForm()
{
var serverInfo = Manager.GetServers().Select(server =>
new ServerInfo
{
Name = server.Hostname,
Reports = server.Reports.Where(report => (DateTime.UtcNow - report.ReportedOn).TotalHours <= 24).ToList()
Reports = server.Reports.Where(report => (DateTime.UtcNow - report.ReportedOn).TotalHours <= 24)
.ToList()
});
return View("Partials/_Reports", serverInfo);
@ -348,7 +367,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()
@ -387,7 +406,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()
@ -419,7 +438,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()
@ -464,6 +483,105 @@ namespace WebfrontCore.Controllers
}));
}
public IActionResult DismissAlertForm(Guid id)
{
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_DISMISS_ALERT_FORM_SUBMIT"],
Name = Localization["WEBFRONT_ACTION_DISMISS_ALERT_SINGLE"],
Inputs = new List<InputInfo>
{
new()
{
Name = "alertId",
Type = "hidden",
Value = id.ToString()
}
},
Action = nameof(DismissAlert),
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public IActionResult DismissAlert(Guid alertId)
{
AlertManager.MarkAlertAsRead(alertId);
return Json(new[]
{
new CommandResponseInfo
{
Response = Localization["WEBFRONT_ACTION_DISMISS_ALERT_SINGLE_RESPONSE"]
}
});
}
public IActionResult DismissAllAlertsForm()
{
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_DISMISS_ALERT_FORM_SUBMIT"],
Name = Localization["WEBFRONT_ACTION_DISMISS_ALERT_MANY"],
Inputs = new List<InputInfo>
{
new()
{
Name = "targetId",
Type = "hidden",
Value = Client.ClientId.ToString()
}
},
Action = nameof(DismissAllAlerts),
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public IActionResult DismissAllAlerts(int targetId)
{
AlertManager.MarkAllAlertsAsRead(targetId);
return Json(new[]
{
new CommandResponseInfo
{
Response = Localization["WEBFRONT_ACTION_DISMISS_ALERT_MANY_RESPONSE"]
}
});
}
public IActionResult OfflineMessageForm()
{
var info = new ActionInfo
{
ActionButtonLabel = Localization["WEBFRONT_ACTION_OFFLINE_MESSAGE_FORM_SUBMIT"],
Name = Localization["WEBFRONT_ACTION_OFFLINE_MESSAGE_BUTTON_COMPOSE"],
Inputs = new List<InputInfo>
{
new()
{
Name = "message",
Label = Localization["WEBFRONT_ACTION_OFFLINE_MESSAGE_FORM_CONTENT"],
},
},
Action = "OfflineMessage",
ShouldRefresh = true
};
return View("_ActionForm", info);
}
public async Task<IActionResult> OfflineMessage(int targetId, string message)
{
var server = Manager.GetServers().First();
return await Task.FromResult(RedirectToAction("Execute", "Console", new
{
serverId = server.EndPoint,
command =
$"{_appConfig.CommandPrefix}{_offlineMessageCommandName} @{targetId} {message.CapClientName(500)}"
}));
}
private Dictionary<string, string> GetPresetPenaltyReasons() => _appConfig.PresetPenaltyReasons.Values
.Concat(_appConfig.GlobalRules)
.Concat(_appConfig.Servers.SelectMany(server => server.Rules ?? Array.Empty<string>()))

View File

@ -4,6 +4,7 @@ using SharedLibraryCore;
using SharedLibraryCore.Dtos;
using SharedLibraryCore.Interfaces;
using System.Threading.Tasks;
using WebfrontCore.QueryHelpers.Models;
namespace WebfrontCore.Controllers
{
@ -11,12 +12,16 @@ namespace WebfrontCore.Controllers
{
private readonly IAuditInformationRepository _auditInformationRepository;
private readonly ITranslationLookup _translationLookup;
private readonly IResourceQueryHelper<BanInfoRequest, BanInfo> _banInfoQueryHelper;
private static readonly int DEFAULT_COUNT = 25;
public AdminController(IManager manager, IAuditInformationRepository auditInformationRepository, ITranslationLookup translationLookup) : base(manager)
public AdminController(IManager manager, IAuditInformationRepository auditInformationRepository,
ITranslationLookup translationLookup,
IResourceQueryHelper<BanInfoRequest, BanInfo> banInfoQueryHelper) : base(manager)
{
_auditInformationRepository = auditInformationRepository;
_translationLookup = translationLookup;
_banInfoQueryHelper = banInfoQueryHelper;
}
[Authorize]
@ -27,7 +32,7 @@ namespace WebfrontCore.Controllers
ViewBag.Title = _translationLookup["WEBFRONT_NAV_AUDIT_LOG"];
ViewBag.InitialOffset = DEFAULT_COUNT;
var auditItems = await _auditInformationRepository.ListAuditInformation(new PaginationRequest()
var auditItems = await _auditInformationRepository.ListAuditInformation(new PaginationRequest
{
Count = DEFAULT_COUNT
});
@ -41,5 +46,25 @@ namespace WebfrontCore.Controllers
var auditItems = await _auditInformationRepository.ListAuditInformation(paginationInfo);
return PartialView("_ListAuditLog", auditItems);
}
public async Task<IActionResult> BanManagement([FromQuery] BanInfoRequest request)
{
var results = await _banInfoQueryHelper.QueryResource(request);
ViewBag.ClientName = request.ClientName;
ViewBag.ClientId = request.ClientId;
ViewBag.ClientIP = request.ClientIP;
ViewBag.ClientGuid = request.ClientGuid;
ViewBag.Title = Localization["WEBFRONT_NAV_TITLE_BAN_MANAGEMENT"];
return View(results.Results);
}
public async Task<IActionResult> BanManagementList([FromQuery] BanInfoRequest request)
{
var results = await _banInfoQueryHelper.QueryResource(request);
return PartialView("_BanEntries", results.Results);
}
}
}

View File

@ -47,7 +47,7 @@ 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[]
{
@ -88,6 +88,7 @@ namespace WebfrontCore.Controllers
var clientDto = new PlayerInfo
{
Name = client.Name,
Game = client.GameName,
Level = displayLevel,
LevelInt = displayLevelInt,
ClientId = client.ClientId,
@ -145,12 +146,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);
@ -181,7 +178,8 @@ namespace WebfrontCore.Controllers
Name = admin.Name,
ClientId = admin.ClientId,
LastConnection = admin.LastConnection,
IsMasked = admin.Masked
IsMasked = admin.Masked,
Game = admin.GameName
});
}
@ -212,6 +210,8 @@ namespace WebfrontCore.Controllers
ViewBag.SearchTerm = clientName;
ViewBag.ResultCount = clientsDto.Count;
ViewBag.Title = Localization["WEBFRONT_SEARCH_RESULTS_TITLE"];
return View("Find/Index", clientsDto);
}

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