Compare commits
51 Commits
2022.06.09
...
2022.07.13
Author | SHA1 | Date | |
---|---|---|---|
0a55c54c42 | |||
f43f7b5040 | |||
540cf7489d | |||
1a72faee60 | |||
4e44bb5ea1 | |||
9e17bcc38f | |||
4b33b33d01 | |||
6f1bc7ab90 | |||
63e1774cb6 | |||
61df873bb1 | |||
052eeb0615 | |||
88e67747fe | |||
5db94723aa | |||
ea8216ecdf | |||
6abbcbe464 | |||
57484690b6 | |||
7a022a1973 | |||
7108e23a03 | |||
77d25890da | |||
2fca68a7ea | |||
a6c0a94f6c | |||
71abaac9e1 | |||
e07651b931 | |||
5a2ee36df9 | |||
2daa4991d1 | |||
775c0a91b5 | |||
55bccc7d3d | |||
4322e8d882 | |||
a92f9fc29c | |||
fbf424c77d | |||
b8e001fcfe | |||
5ab5b73ecf | |||
4534d24fe6 | |||
73c8d0da33 | |||
16d75470b5 | |||
f02552faa1 | |||
a4923d03f9 | |||
8ae6561f4e | |||
deeb1dea87 | |||
9ab34614c5 | |||
2cff25d6b3 | |||
df3e226dc9 | |||
ef3db63ba7 | |||
49fe4520ff | |||
6587187a34 | |||
b337e232a2 | |||
a44b4e9475 | |||
ffb0e5cac1 | |||
ecc2b5bf54 | |||
2ac9cc4379 | |||
215037095f |
55
Application/Alerts/AlertExtensions.cs
Normal file
55
Application/Alerts/AlertExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
137
Application/Alerts/AlertManager.cs
Normal file
137
Application/Alerts/AlertManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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": [
|
||||
|
@ -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)
|
||||
{
|
||||
@ -962,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
|
||||
{
|
||||
@ -1454,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)
|
||||
{
|
||||
@ -1484,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());
|
||||
@ -1512,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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -10,6 +10,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
{
|
||||
public event EventHandler<GameEvent> OnClientDisconnect;
|
||||
public event EventHandler<GameEvent> OnClientConnect;
|
||||
public event EventHandler<GameEvent> OnClientMetaUpdated;
|
||||
|
||||
private readonly ILogger _logger;
|
||||
|
||||
@ -29,10 +30,15 @@ namespace IW4MAdmin.Application.Misc
|
||||
OnClientConnect?.Invoke(this, gameEvent);
|
||||
}
|
||||
|
||||
if (gameEvent.Type == GameEvent.EventType.Disconnect)
|
||||
if (gameEvent.Type == GameEvent.EventType.Disconnect && gameEvent.Origin.ClientId != 0)
|
||||
{
|
||||
OnClientDisconnect?.Invoke(this, gameEvent);
|
||||
}
|
||||
|
||||
if (gameEvent.Type == GameEvent.EventType.MetaUpdated)
|
||||
{
|
||||
OnClientMetaUpdated?.Invoke(this, gameEvent);
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
@ -41,4 +47,4 @@ namespace IW4MAdmin.Application.Misc
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,10 @@ using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
using Data.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.QueryHelper;
|
||||
@ -19,13 +22,15 @@ public class MetaServiceV2 : IMetaServiceV2
|
||||
{
|
||||
private readonly IDictionary<MetaType, List<dynamic>> _metaActions;
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public MetaServiceV2(ILogger<MetaServiceV2> logger, IDatabaseContextFactory contextFactory)
|
||||
public MetaServiceV2(ILogger<MetaServiceV2> logger, IDatabaseContextFactory contextFactory, IServiceProvider serviceProvider)
|
||||
{
|
||||
_logger = logger;
|
||||
_metaActions = new Dictionary<MetaType, List<dynamic>>();
|
||||
_contextFactory = contextFactory;
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public async Task SetPersistentMeta(string metaKey, string metaValue, int clientId,
|
||||
@ -64,6 +69,26 @@ public class MetaServiceV2 : IMetaServiceV2
|
||||
}
|
||||
|
||||
await context.SaveChangesAsync(token);
|
||||
|
||||
|
||||
var manager = _serviceProvider.GetRequiredService<IManager>();
|
||||
var matchingClient = manager.GetActiveClients().FirstOrDefault(client => client.ClientId == clientId);
|
||||
var server = matchingClient?.CurrentServer ?? manager.GetServers().FirstOrDefault();
|
||||
|
||||
if (server is not null)
|
||||
{
|
||||
manager.AddEvent(new GameEvent
|
||||
{
|
||||
Type = GameEvent.EventType.MetaUpdated,
|
||||
Origin = matchingClient ?? new EFClient
|
||||
{
|
||||
ClientId = clientId
|
||||
},
|
||||
Data = metaValue,
|
||||
Extra = metaKey,
|
||||
Owner = server
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async Task SetPersistentMetaValue<T>(string metaKey, T metaValue, int clientId,
|
||||
|
@ -116,7 +116,8 @@ namespace IW4MAdmin.Application.Misc
|
||||
typeof(System.Net.Http.HttpClient).Assembly,
|
||||
typeof(EFClient).Assembly,
|
||||
typeof(Utilities).Assembly,
|
||||
typeof(Encoding).Assembly
|
||||
typeof(Encoding).Assembly,
|
||||
typeof(CancellationTokenSource).Assembly
|
||||
})
|
||||
.CatchClrExceptions()
|
||||
.AddObjectConverter(new PermissionLevelToStringConverter()));
|
||||
|
@ -176,7 +176,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
.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, true);
|
||||
}, nameof(_rankedClientsCache), serverId is null ? null: new[] { (object)serverId }, _cacheTimeSpan);
|
||||
|
||||
try
|
||||
{
|
||||
|
@ -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 _);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,12 +26,14 @@ namespace IW4MAdmin.Application.RConParsers
|
||||
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
|
||||
public IDictionary<string, string> OverrideDvarNameMapping { get; set; } = new Dictionary<string, string>();
|
||||
public IDictionary<string, string> DefaultDvarValues { get; set; } = new Dictionary<string, string>();
|
||||
public IDictionary<string, int?> OverrideCommandTimeouts { get; set; } = new Dictionary<string, int?>();
|
||||
public int NoticeMaximumLines { get; set; } = 8;
|
||||
public int NoticeMaxCharactersPerLine { get; set; } = 50;
|
||||
public string NoticeLineSeparator { get; set; } = Environment.NewLine;
|
||||
public int? DefaultRConPort { get; set; }
|
||||
public string DefaultInstallationDirectoryHint { get; set; }
|
||||
public short FloodProtectInterval { get; set; } = 750;
|
||||
public bool ShouldRemoveDiacritics { get; set; }
|
||||
|
||||
public ColorCodeMapping ColorCodeMapping { get; set; } = new ColorCodeMapping
|
||||
{
|
||||
@ -58,6 +60,25 @@ namespace IW4MAdmin.Application.RConParsers
|
||||
StatusHeader = parserRegexFactory.CreateParserRegex();
|
||||
HostnameStatus = parserRegexFactory.CreateParserRegex();
|
||||
MaxPlayersStatus = parserRegexFactory.CreateParserRegex();
|
||||
|
||||
|
||||
const string mapRotateCommand = "map_rotate";
|
||||
const string mapCommand = "map";
|
||||
const string fastRestartCommand = "fast_restart";
|
||||
const string quitCommand = "quit";
|
||||
|
||||
foreach (var command in new[] { mapRotateCommand, mapCommand, fastRestartCommand})
|
||||
{
|
||||
if (!OverrideCommandTimeouts.ContainsKey(command))
|
||||
{
|
||||
OverrideCommandTimeouts.Add(command, 45);
|
||||
}
|
||||
}
|
||||
|
||||
if (!OverrideCommandTimeouts.ContainsKey(quitCommand))
|
||||
{
|
||||
OverrideCommandTimeouts.Add(quitCommand, 0); // we don't want to wait for a response when we quit the server
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -85,7 +85,15 @@ namespace Data.Context
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
// make network id unique
|
||||
modelBuilder.Entity<EFClient>(entity => { entity.HasIndex(e => e.NetworkId).IsUnique(); });
|
||||
modelBuilder.Entity<EFClient>(entity =>
|
||||
{
|
||||
entity.HasIndex(e => e.NetworkId);
|
||||
entity.HasAlternateKey(client => new
|
||||
{
|
||||
client.NetworkId,
|
||||
client.GameName
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity<EFPenalty>(entity =>
|
||||
{
|
||||
|
@ -18,6 +18,7 @@ namespace Data.Helpers
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Dictionary<object, CacheState<TReturnType>>> _cacheStates = new();
|
||||
private readonly object _defaultKey = new();
|
||||
|
||||
private bool _autoRefresh;
|
||||
private const int DefaultExpireMinutes = 15;
|
||||
@ -58,7 +59,7 @@ namespace Data.Helpers
|
||||
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
|
||||
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false)
|
||||
{
|
||||
ids ??= new[] { new object() };
|
||||
ids ??= new[] { _defaultKey };
|
||||
|
||||
if (!_cacheStates.ContainsKey(key))
|
||||
{
|
||||
|
1639
Data/Migrations/MySql/20220613192602_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
1639
Data/Migrations/MySql/20220613192602_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,63 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.MySql
|
||||
{
|
||||
public partial class AddAlternateKeyToEFClients : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients");
|
||||
|
||||
migrationBuilder.Sql("UPDATE `EFClients` set `GameName` = 0 WHERE `GameName` IS NULL");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "GameName",
|
||||
table: "EFClients",
|
||||
type: "int",
|
||||
nullable: false,
|
||||
defaultValue: 0,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "int",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AddUniqueConstraint(
|
||||
name: "AK_EFClients_NetworkId_GameName",
|
||||
table: "EFClients",
|
||||
columns: new[] { "NetworkId", "GameName" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients",
|
||||
column: "NetworkId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropUniqueConstraint(
|
||||
name: "AK_EFClients_NetworkId_GameName",
|
||||
table: "EFClients");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "GameName",
|
||||
table: "EFClients",
|
||||
type: "int",
|
||||
nullable: true,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "int");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients",
|
||||
column: "NetworkId",
|
||||
unique: true);
|
||||
}
|
||||
}
|
||||
}
|
1639
Data/Migrations/MySql/20220616224602_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
1639
Data/Migrations/MySql/20220616224602_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.MySql
|
||||
{
|
||||
public partial class AddDescendingTimeSentIndexEFClientMessages : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
try
|
||||
{
|
||||
migrationBuilder.Sql(@"create index IX_EFClientMessages_TimeSentDesc on EFClientMessages (TimeSent desc);");
|
||||
}
|
||||
catch
|
||||
{
|
||||
migrationBuilder.Sql(@"create index IX_EFClientMessages_TimeSentDesc on efclientmessages (TimeSent desc);");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(@"drop index IX_EFClientMessages_TimeSentDesc on EFClientMessages;");
|
||||
}
|
||||
}
|
||||
}
|
@ -64,7 +64,7 @@ namespace Data.Migrations.MySql
|
||||
b.Property<DateTime>("FirstConnection")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<int?>("GameName")
|
||||
b.Property<int>("GameName")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime>("LastConnection")
|
||||
@ -90,12 +90,13 @@ namespace Data.Migrations.MySql
|
||||
|
||||
b.HasKey("ClientId");
|
||||
|
||||
b.HasAlternateKey("NetworkId", "GameName");
|
||||
|
||||
b.HasIndex("AliasLinkId");
|
||||
|
||||
b.HasIndex("CurrentAliasId");
|
||||
|
||||
b.HasIndex("NetworkId")
|
||||
.IsUnique();
|
||||
b.HasIndex("NetworkId");
|
||||
|
||||
b.ToTable("EFClients", (string)null);
|
||||
});
|
||||
|
1696
Data/Migrations/Postgresql/20220613181913_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
1696
Data/Migrations/Postgresql/20220613181913_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,63 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.Postgresql
|
||||
{
|
||||
public partial class AddAlternateKeyToEFClients : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients");
|
||||
|
||||
migrationBuilder.Sql("UPDATE \"EFClients\" SET \"GameName\" = 0 WHERE \"GameName\" IS NULL");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "GameName",
|
||||
table: "EFClients",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "integer",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AddUniqueConstraint(
|
||||
name: "AK_EFClients_NetworkId_GameName",
|
||||
table: "EFClients",
|
||||
columns: new[] { "NetworkId", "GameName" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients",
|
||||
column: "NetworkId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropUniqueConstraint(
|
||||
name: "AK_EFClients_NetworkId_GameName",
|
||||
table: "EFClients");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "GameName",
|
||||
table: "EFClients",
|
||||
type: "integer",
|
||||
nullable: true,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "integer");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients",
|
||||
column: "NetworkId",
|
||||
unique: true);
|
||||
}
|
||||
}
|
||||
}
|
1696
Data/Migrations/Postgresql/20220616224145_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
1696
Data/Migrations/Postgresql/20220616224145_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.Postgresql
|
||||
{
|
||||
public partial class AddDescendingTimeSentIndexEFClientMessages : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(
|
||||
@"CREATE INDEX""IX_EFClientMessages_TimeSentDesc""
|
||||
ON public.""EFClientMessages"" USING btree
|
||||
(""TimeSent"" DESC NULLS LAST)
|
||||
TABLESPACE pg_default;"
|
||||
);
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql(@"DROP INDEX public.""IX_EFClientMessages_TimeSentDesc""");
|
||||
}
|
||||
}
|
||||
}
|
@ -71,7 +71,7 @@ namespace Data.Migrations.Postgresql
|
||||
b.Property<DateTime>("FirstConnection")
|
||||
.HasColumnType("timestamp without time zone");
|
||||
|
||||
b.Property<int?>("GameName")
|
||||
b.Property<int>("GameName")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTime>("LastConnection")
|
||||
@ -97,12 +97,13 @@ namespace Data.Migrations.Postgresql
|
||||
|
||||
b.HasKey("ClientId");
|
||||
|
||||
b.HasAlternateKey("NetworkId", "GameName");
|
||||
|
||||
b.HasIndex("AliasLinkId");
|
||||
|
||||
b.HasIndex("CurrentAliasId");
|
||||
|
||||
b.HasIndex("NetworkId")
|
||||
.IsUnique();
|
||||
b.HasIndex("NetworkId");
|
||||
|
||||
b.ToTable("EFClients", (string)null);
|
||||
});
|
||||
|
1637
Data/Migrations/Sqlite/20220613160952_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
1637
Data/Migrations/Sqlite/20220613160952_AddAlternateKeyToEFClients.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,61 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.Sqlite
|
||||
{
|
||||
public partial class AddAlternateKeyToEFClients : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "GameName",
|
||||
table: "EFClients",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: 0,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "INTEGER",
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AddUniqueConstraint(
|
||||
name: "AK_EFClients_NetworkId_GameName",
|
||||
table: "EFClients",
|
||||
columns: new[] { "NetworkId", "GameName" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients",
|
||||
column: "NetworkId");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropUniqueConstraint(
|
||||
name: "AK_EFClients_NetworkId_GameName",
|
||||
table: "EFClients");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients");
|
||||
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "GameName",
|
||||
table: "EFClients",
|
||||
type: "INTEGER",
|
||||
nullable: true,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "INTEGER");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EFClients_NetworkId",
|
||||
table: "EFClients",
|
||||
column: "NetworkId",
|
||||
unique: true);
|
||||
}
|
||||
}
|
||||
}
|
1637
Data/Migrations/Sqlite/20220616225008_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
1637
Data/Migrations/Sqlite/20220616225008_AddDescendingTimeSentIndexEFClientMessages.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,19 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Data.Migrations.Sqlite
|
||||
{
|
||||
public partial class AddDescendingTimeSentIndexEFClientMessages : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -62,7 +62,7 @@ namespace Data.Migrations.Sqlite
|
||||
b.Property<DateTime>("FirstConnection")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<int?>("GameName")
|
||||
b.Property<int>("GameName")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<DateTime>("LastConnection")
|
||||
@ -88,12 +88,13 @@ namespace Data.Migrations.Sqlite
|
||||
|
||||
b.HasKey("ClientId");
|
||||
|
||||
b.HasAlternateKey("NetworkId", "GameName");
|
||||
|
||||
b.HasIndex("AliasLinkId");
|
||||
|
||||
b.HasIndex("CurrentAliasId");
|
||||
|
||||
b.HasIndex("NetworkId")
|
||||
.IsUnique();
|
||||
b.HasIndex("NetworkId");
|
||||
|
||||
b.ToTable("EFClients", (string)null);
|
||||
});
|
||||
|
@ -63,7 +63,7 @@ namespace Data.Models.Client
|
||||
public DateTime FirstConnection { get; set; }
|
||||
[Required]
|
||||
public DateTime LastConnection { get; set; }
|
||||
public Reference.Game? GameName { get; set; } = Reference.Game.UKN;
|
||||
public Reference.Game GameName { get; set; } = Reference.Game.UKN;
|
||||
public bool Masked { get; set; }
|
||||
[Required]
|
||||
public int AliasLinkId { get; set; }
|
||||
|
@ -7,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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 )
|
||||
|
@ -253,8 +253,10 @@ namespace Integrations.Cod
|
||||
try
|
||||
{
|
||||
connectionState.LastQuery = DateTime.Now;
|
||||
var timeout = _parser.OverrideTimeoutForCommand(parameters);
|
||||
waitForResponse = waitForResponse && timeout.HasValue;
|
||||
response = await SendPayloadAsync(payload, waitForResponse,
|
||||
_parser.OverrideTimeoutForCommand(parameters), token);
|
||||
timeout ?? TimeSpan.Zero, token);
|
||||
|
||||
if ((response?.Length == 0 || response[0].Length == 0) && waitForResponse)
|
||||
{
|
||||
@ -456,6 +458,12 @@ namespace Integrations.Cod
|
||||
connectionState.SendEventArgs.DisconnectReuseSocket = true;
|
||||
}
|
||||
|
||||
if (connectionState.ReceiveEventArgs.UserToken is ConnectionUserToken { CancellationToken.IsCancellationRequested: true })
|
||||
{
|
||||
// after a graceful restart we need to reset the receive user token as the cancellation has been updated
|
||||
connectionState.ReceiveEventArgs.UserToken = connectionState.SendEventArgs.UserToken;
|
||||
}
|
||||
|
||||
connectionState.SendEventArgs.SetBuffer(payload);
|
||||
|
||||
// send the data to the server
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -16,7 +16,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -4,6 +4,7 @@ using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System.Threading.Tasks;
|
||||
using SharedLibraryCore.Helpers;
|
||||
|
||||
namespace IW4MAdmin.Plugins.Login.Commands
|
||||
{
|
||||
@ -18,7 +19,7 @@ namespace IW4MAdmin.Plugins.Login.Commands
|
||||
RequiresTarget = false;
|
||||
Arguments = new CommandArgument[]
|
||||
{
|
||||
new CommandArgument()
|
||||
new()
|
||||
{
|
||||
Name = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_ARGS_PASSWORD"],
|
||||
Required = true
|
||||
@ -26,24 +27,28 @@ namespace IW4MAdmin.Plugins.Login.Commands
|
||||
};
|
||||
}
|
||||
|
||||
public override async Task ExecuteAsync(GameEvent E)
|
||||
public override async Task ExecuteAsync(GameEvent gameEvent)
|
||||
{
|
||||
bool success = E.Owner.Manager.TokenAuthenticator.AuthorizeToken(E.Origin.NetworkId, E.Data);
|
||||
var success = gameEvent.Owner.Manager.TokenAuthenticator.AuthorizeToken(new TokenIdentifier
|
||||
{
|
||||
ClientId = gameEvent.Origin.ClientId,
|
||||
Token = gameEvent.Data
|
||||
});
|
||||
|
||||
if (!success)
|
||||
{
|
||||
string[] hashedPassword = await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(E.Data, E.Origin.PasswordSalt));
|
||||
success = hashedPassword[0] == E.Origin.Password;
|
||||
var hashedPassword = await Task.FromResult(Hashing.Hash(gameEvent.Data, gameEvent.Origin.PasswordSalt));
|
||||
success = hashedPassword[0] == gameEvent.Origin.Password;
|
||||
}
|
||||
|
||||
if (success)
|
||||
{
|
||||
Plugin.AuthorizedClients[E.Origin.ClientId] = true;
|
||||
Plugin.AuthorizedClients[gameEvent.Origin.ClientId] = true;
|
||||
}
|
||||
|
||||
_ = success ?
|
||||
E.Origin.Tell(_translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS"]) :
|
||||
E.Origin.Tell(_translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL"]);
|
||||
gameEvent.Origin.Tell(_translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_SUCCESS"]) :
|
||||
gameEvent.Origin.Tell(_translationLookup["PLUGINS_LOGIN_COMMANDS_LOGIN_FAIL"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -16,7 +16,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -46,7 +46,7 @@ let plugin = {
|
||||
break;
|
||||
case 'warn':
|
||||
const warningTitle = _localization.LocalizationIndex['GLOBAL_WARNING'];
|
||||
sendScriptCommand(server, 'Alert', gameEvent.Target, {
|
||||
sendScriptCommand(server, 'Alert', gameEvent.Origin, gameEvent.Target, {
|
||||
alertType: warningTitle + '!',
|
||||
message: gameEvent.Data
|
||||
});
|
||||
@ -463,7 +463,11 @@ function onReceivedDvar(server, dvarName, dvarValue, success) {
|
||||
if (input.length > 0) {
|
||||
const event = parseEvent(input)
|
||||
|
||||
logger.WriteDebug(`Processing input... ${event.eventType} ${event.subType} ${event.data} ${event.clientNumber}`);
|
||||
logger.WriteDebug(`Processing input... ${event.eventType} ${event.subType} ${event.data.toString()} ${event.clientNumber}`);
|
||||
|
||||
const metaService = _serviceResolver.ResolveService('IMetaServiceV2');
|
||||
const threading = importNamespace('System.Threading');
|
||||
const token = new threading.CancellationTokenSource().Token;
|
||||
|
||||
// todo: refactor to mapping if possible
|
||||
if (event.eventType === 'ClientDataRequested') {
|
||||
@ -475,8 +479,8 @@ function onReceivedDvar(server, dvarName, dvarValue, success) {
|
||||
let data = [];
|
||||
|
||||
if (event.subType === 'Meta') {
|
||||
const metaService = _serviceResolver.ResolveService('IMetaService');
|
||||
const meta = metaService.GetPersistentMeta(event.data, client).GetAwaiter().GetResult();
|
||||
const metaService = _serviceResolver.ResolveService('IMetaServiceV2');
|
||||
const meta = metaService.GetPersistentMeta(event.data, client, token).GetAwaiter().GetResult();
|
||||
data[event.data] = meta === null ? '' : meta.Value;
|
||||
} else {
|
||||
data = {
|
||||
@ -510,19 +514,19 @@ function onReceivedDvar(server, dvarName, dvarValue, success) {
|
||||
sendEvent(server, false, 'SetClientDataCompleted', 'Meta', {ClientNumber: event.clientNumber}, undefined, {status: 'Fail'});
|
||||
} else {
|
||||
if (event.subType === 'Meta') {
|
||||
const metaService = _serviceResolver.ResolveService('IMetaService');
|
||||
try {
|
||||
logger.WriteDebug(`Key=${event.data['key']}, Value=${event.data['value']}`);
|
||||
logger.WriteDebug(`Key=${event.data['key']}, Value=${event.data['value']}, Direction=${event.data['direction']} ${token}`);
|
||||
if (event.data['direction'] != null) {
|
||||
event.data['direction'] = 'up'
|
||||
? metaService.IncrementPersistentMeta(event.data['key'], event.data['value'], clientId).GetAwaiter().GetResult()
|
||||
: metaService.DecrementPersistentMeta(event.data['key'], event.data['value'], clientId).GetAwaiter().GetResult();
|
||||
? metaService.IncrementPersistentMeta(event.data['key'], 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ var plugin = {
|
||||
rconParser.Configuration.CommandPrefixes.Ban = 'clientkick {0} "{1}"';
|
||||
rconParser.Configuration.CommandPrefixes.TempBan = 'clientkick {0} "{1}"';
|
||||
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n';
|
||||
rconParser.Configuration.Dvar.Pattern = '^ *\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n(?:latched: \\"(.+)?\\"\\n)? *(.+)$';
|
||||
rconParser.Configuration.Dvar.Pattern = '^ *\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n?(?:latched: \\"(.+)?\\"\\n?)? *(.+)$';
|
||||
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +(Yes|No) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback|unknown|bot) +(-*[0-9]+) *$';
|
||||
rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +address +qport *';
|
||||
rconParser.Configuration.WaitForResponse = false;
|
||||
|
@ -3,7 +3,7 @@ var eventParser;
|
||||
|
||||
var plugin = {
|
||||
author: 'RaidMax, Xerxes',
|
||||
version: 1.2,
|
||||
version: 1.4,
|
||||
name: 'Plutonium T6 Parser',
|
||||
isParser: true,
|
||||
|
||||
@ -29,9 +29,10 @@ var plugin = {
|
||||
rconParser.Configuration.NoticeLineSeparator = '. ';
|
||||
rconParser.Configuration.DefaultRConPort = 4976;
|
||||
rconParser.Configuration.DefaultInstallationDirectoryHint = '{LocalAppData}/Plutonium/storage/t6';
|
||||
rconParser.Configuration.ShouldRemoveDiacritics = true;
|
||||
|
||||
rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +lastmsg +address +qport +rate *';
|
||||
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +([0-9]+) +(?:[0-1]{1}) +([0-9]+) +([A-F0-9]+|0) +(.+?) +(?:[0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+:-?\\d{1,5}|loopback) +(?:-?[0-9]+) +(?:[0-9]+) *$';
|
||||
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +([0-9]+) +(?:[0-1]{1}) +([0-9]+) +([A-F0-9]+|0) +(.+?) +(?:[0-9]+) +(\\d+\\.\\d+\\.\\d+\\.\\d+\\:-?\\d{1,5}|0+\\.0+:-?\\d{1,5}|loopback|unknown) +(?:-?[0-9]+) +(?:[0-9]+) *$';
|
||||
rconParser.Configuration.Status.AddMapping(100, 1);
|
||||
rconParser.Configuration.Status.AddMapping(101, 2);
|
||||
rconParser.Configuration.Status.AddMapping(102, 3);
|
||||
|
@ -3,7 +3,7 @@ var eventParser;
|
||||
|
||||
var plugin = {
|
||||
author: 'RaidMax',
|
||||
version: 0.1,
|
||||
version: 0.2,
|
||||
name: 'Plutonium T5 Parser',
|
||||
isParser: true,
|
||||
|
||||
@ -16,13 +16,18 @@ var plugin = {
|
||||
|
||||
rconParser.Configuration.DefaultInstallationDirectoryHint = '{LocalAppData}/Plutonium/storage/t5';
|
||||
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n';
|
||||
rconParser.Configuration.Dvar.Pattern = '^(?:\\^7)?\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n(?:latched: \\"(.+)?\\"\\n)? *(.+)$';
|
||||
rconParser.Configuration.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';
|
||||
|
@ -17,7 +17,7 @@ var plugin = {
|
||||
rconParser.Configuration.CommandPrefixes.Ban = 'kickClient {0} "{1}"';
|
||||
rconParser.Configuration.CommandPrefixes.TempBan = 'kickClient {0} "{1}"';
|
||||
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint';
|
||||
rconParser.Configuration.Dvar.Pattern = '^ *\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n(?:latched: \\"(.+)?\\"\\n)? *(.+)$';
|
||||
rconParser.Configuration.Dvar.Pattern = '^ *\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n?(?:latched: \\"(.+)?\\"\\n?)? *(.+)$';
|
||||
rconParser.Configuration.Status.Pattern = '^ *([0-9]+) +-?([0-9]+) +(Yes|No) +((?:[A-Z]+|[0-9]+)) +((?:[a-z]|[0-9]){8,32}|(?:[a-z]|[0-9]){8,32}|bot[0-9]+|(?:[0-9]+)) *(.{0,32}) +(\\d+\\.\\d+\\.\\d+.\\d+\\:-*\\d{1,5}|0+.0+:-*\\d{1,5}|loopback|unknown|bot) +(-*[0-9]+) *$';
|
||||
rconParser.Configuration.StatusHeader.Pattern = 'num +score +bot +ping +guid +name +address +qport *';
|
||||
rconParser.Configuration.Status.AddMapping(102, 4);
|
||||
@ -37,4 +37,4 @@ var plugin = {
|
||||
onUnloadAsync: function() {},
|
||||
|
||||
onTickAsync: function(server) {}
|
||||
};
|
||||
};
|
||||
|
@ -3,7 +3,7 @@ var eventParser;
|
||||
|
||||
var plugin = {
|
||||
author: 'RaidMax',
|
||||
version: 0.2,
|
||||
version: 0.3,
|
||||
name: 'Call of Duty 5: World at War Parser',
|
||||
isParser: true,
|
||||
|
||||
@ -17,6 +17,7 @@ var plugin = {
|
||||
rconParser.Configuration.GuidNumberStyle = 7; // Integer
|
||||
rconParser.Configuration.DefaultRConPort = 28960;
|
||||
rconParser.Version = 'Call of Duty Multiplayer COD_WaW MP build 1.7.1263 CL(350073) JADAMS2 Thu Oct 29 15:43:55 2009 win-x86';
|
||||
rconParser.GameName = 5; // T4
|
||||
|
||||
eventParser.Configuration.GuidNumberStyle = 7; // Integer
|
||||
eventParser.GameName = 5; // T4
|
||||
|
@ -23,7 +23,7 @@ namespace Stats.Dtos
|
||||
/// <summary>
|
||||
/// only look for messages sent after this date
|
||||
/// </summary>
|
||||
public DateTime SentAfter { get; set; } = DateTime.UtcNow.AddYears(-100);
|
||||
public DateTime? SentAfter { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// only look for messages sent before this date0
|
||||
|
@ -19,8 +19,14 @@ namespace IW4MAdmin.Plugins.Stats.Web.Dtos
|
||||
public int Kills { get; set; }
|
||||
public int Deaths { get; set; }
|
||||
public int RatingChange { get; set; }
|
||||
public List<double> PerformanceHistory { get; set; }
|
||||
public List<PerformanceHistory> PerformanceHistory { get; set; }
|
||||
public double? ZScore { get; set; }
|
||||
public long? ServerId { get; set; }
|
||||
}
|
||||
|
||||
public class PerformanceHistory
|
||||
{
|
||||
public double? Performance { get; set; }
|
||||
public DateTime OccurredAt { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
using Data.Models;
|
||||
using Data.Models.Client;
|
||||
using Data.Models.Client.Stats;
|
||||
using IW4MAdmin.Plugins.Stats;
|
||||
@ -12,7 +13,6 @@ using Microsoft.Extensions.Logging;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using Stats.Client.Abstractions;
|
||||
using Stats.Dtos;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
|
||||
@ -50,7 +50,8 @@ namespace Stats.Helpers
|
||||
{
|
||||
client.ClientId,
|
||||
client.CurrentAlias.Name,
|
||||
client.Level
|
||||
client.Level,
|
||||
client.GameName
|
||||
}).FirstOrDefaultAsync(client => client.ClientId == query.ClientId);
|
||||
|
||||
if (clientInfo == null)
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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..];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -17,7 +17,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -53,7 +53,7 @@ namespace IW4MAdmin.Plugins.Welcome
|
||||
{
|
||||
var newPlayer = gameEvent.Origin;
|
||||
if (newPlayer.Level >= Permission.Trusted && !gameEvent.Origin.Masked||
|
||||
!string.IsNullOrEmpty(newPlayer.GetAdditionalProperty<string>("ClientTag")) &&
|
||||
!string.IsNullOrEmpty(newPlayer.Tag) &&
|
||||
newPlayer.Level != Permission.Flagged && newPlayer.Level != Permission.Banned &&
|
||||
!newPlayer.Masked)
|
||||
gameEvent.Owner.Broadcast(
|
||||
@ -88,7 +88,7 @@ namespace IW4MAdmin.Plugins.Welcome
|
||||
{
|
||||
msg = msg.Replace("{{ClientName}}", joining.Name);
|
||||
msg = msg.Replace("{{ClientLevel}}",
|
||||
$"{Utilities.ConvertLevelToColor(joining.Level, joining.ClientPermission.Name)}{(string.IsNullOrEmpty(joining.GetAdditionalProperty<string>("ClientTag")) ? "" : $" (Color::White)({joining.GetAdditionalProperty<string>("ClientTag")}(Color::White))")}");
|
||||
$"{Utilities.ConvertLevelToColor(joining.Level, joining.ClientPermission.Name)}{(string.IsNullOrEmpty(joining.Tag) ? "" : $" (Color::White){joining.Tag}(Color::White)")}");
|
||||
// this prevents it from trying to evaluate it every message
|
||||
if (msg.Contains("{{ClientLocation}}"))
|
||||
{
|
||||
@ -111,7 +111,7 @@ namespace IW4MAdmin.Plugins.Welcome
|
||||
try
|
||||
{
|
||||
var response =
|
||||
await wc.GetStringAsync(new Uri($"http://ip-api.com/json/{ip}"));
|
||||
await wc.GetStringAsync(new Uri($"http://ip-api.com/json/{ip}?lang={Utilities.CurrentLocalization.LocalizationName.Split("-").First().ToLower()}"));
|
||||
var responseObj = JObject.Parse(response);
|
||||
response = responseObj["country"]?.ToString();
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
33
SharedLibraryCore/Alerts/Alert.cs
Normal file
33
SharedLibraryCore/Alerts/Alert.cs
Normal 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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -11,163 +11,180 @@ namespace SharedLibraryCore.Commands
|
||||
{
|
||||
public class CommandProcessing
|
||||
{
|
||||
public static async Task<Command> ValidateCommand(GameEvent E, ApplicationConfiguration appConfig,
|
||||
public static async Task<Command> ValidateCommand(GameEvent gameEvent, ApplicationConfiguration appConfig,
|
||||
CommandConfiguration commandConfig)
|
||||
{
|
||||
var loc = Utilities.CurrentLocalization.LocalizationIndex;
|
||||
var Manager = E.Owner.Manager;
|
||||
var isBroadcast = E.Data.StartsWith(appConfig.BroadcastCommandPrefix);
|
||||
var manager = gameEvent.Owner.Manager;
|
||||
var isBroadcast = gameEvent.Data.StartsWith(appConfig.BroadcastCommandPrefix);
|
||||
var prefixLength = isBroadcast ? appConfig.BroadcastCommandPrefix.Length : appConfig.CommandPrefix.Length;
|
||||
|
||||
var CommandString = E.Data.Substring(prefixLength, E.Data.Length - prefixLength).Split(' ')[0];
|
||||
E.Message = E.Data;
|
||||
var commandString =
|
||||
gameEvent.Data.Substring(prefixLength, gameEvent.Data.Length - prefixLength).Split(' ')[0];
|
||||
gameEvent.Message = gameEvent.Data;
|
||||
|
||||
Command C = null;
|
||||
foreach (Command cmd in Manager.GetCommands()
|
||||
Command matchedCommand = null;
|
||||
foreach (var availableCommand in manager.GetCommands()
|
||||
.Where(c => c.Name != null))
|
||||
if (cmd.Name.Equals(CommandString, StringComparison.OrdinalIgnoreCase) ||
|
||||
(cmd.Alias ?? "").Equals(CommandString, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if ((availableCommand.SupportedGames?.Any() ?? false) &&
|
||||
!availableCommand.SupportedGames.Contains(gameEvent.Owner.GameName))
|
||||
{
|
||||
C = cmd;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (C == null)
|
||||
{
|
||||
E.Origin.Tell(loc["COMMAND_UNKNOWN"]);
|
||||
throw new CommandException($"{E.Origin} entered unknown command \"{CommandString}\"");
|
||||
}
|
||||
|
||||
C.IsBroadcast = isBroadcast;
|
||||
|
||||
var allowImpersonation = commandConfig?.Commands?.ContainsKey(C.GetType().Name) ?? false
|
||||
? commandConfig.Commands[C.GetType().Name].AllowImpersonation
|
||||
: C.AllowImpersonation;
|
||||
|
||||
if (!allowImpersonation && E.ImpersonationOrigin != null)
|
||||
{
|
||||
E.ImpersonationOrigin.Tell(loc["COMMANDS_RUN_AS_FAIL"]);
|
||||
throw new CommandException($"Command {C.Name} cannot be run as another client");
|
||||
}
|
||||
|
||||
E.Data = E.Data.RemoveWords(1);
|
||||
var Args = E.Data.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
// todo: the code below can be cleaned up
|
||||
|
||||
if (E.Origin.Level < C.Permission)
|
||||
{
|
||||
E.Origin.Tell(loc["COMMAND_NOACCESS"]);
|
||||
throw new CommandException($"{E.Origin} does not have access to \"{C.Name}\"");
|
||||
}
|
||||
|
||||
if (Args.Length < C.RequiredArgumentCount)
|
||||
{
|
||||
E.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
|
||||
E.Origin.Tell(C.Syntax);
|
||||
throw new CommandException($"{E.Origin} did not supply enough arguments for \"{C.Name}\"");
|
||||
}
|
||||
|
||||
if (C.RequiresTarget)
|
||||
{
|
||||
if (Args.Length > 0)
|
||||
if (availableCommand.Name.Equals(commandString, StringComparison.OrdinalIgnoreCase) ||
|
||||
(availableCommand.Alias ?? "").Equals(commandString, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!int.TryParse(Args[0], out var cNum))
|
||||
matchedCommand = (Command)availableCommand;
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedCommand == null)
|
||||
{
|
||||
gameEvent.Origin.Tell(loc["COMMAND_UNKNOWN"]);
|
||||
throw new CommandException($"{gameEvent.Origin} entered unknown command \"{commandString}\"");
|
||||
}
|
||||
|
||||
matchedCommand.IsBroadcast = isBroadcast;
|
||||
|
||||
var allowImpersonation = commandConfig?.Commands?.ContainsKey(matchedCommand.GetType().Name) ?? false
|
||||
? commandConfig.Commands[matchedCommand.GetType().Name].AllowImpersonation
|
||||
: matchedCommand.AllowImpersonation;
|
||||
|
||||
if (!allowImpersonation && gameEvent.ImpersonationOrigin != null)
|
||||
{
|
||||
gameEvent.ImpersonationOrigin.Tell(loc["COMMANDS_RUN_AS_FAIL"]);
|
||||
throw new CommandException($"Command {matchedCommand.Name} cannot be run as another client");
|
||||
}
|
||||
|
||||
gameEvent.Data = gameEvent.Data.RemoveWords(1);
|
||||
var args = gameEvent.Data.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
|
||||
// todo: the code below can be cleaned up
|
||||
if (gameEvent.Origin.Level < matchedCommand.Permission)
|
||||
{
|
||||
gameEvent.Origin.Tell(loc["COMMAND_NOACCESS"]);
|
||||
throw new CommandException($"{gameEvent.Origin} does not have access to \"{matchedCommand.Name}\"");
|
||||
}
|
||||
|
||||
if (args.Length < matchedCommand.RequiredArgumentCount)
|
||||
{
|
||||
gameEvent.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
|
||||
gameEvent.Origin.Tell(matchedCommand.Syntax);
|
||||
throw new CommandException(
|
||||
$"{gameEvent.Origin} did not supply enough arguments for \"{matchedCommand.Name}\"");
|
||||
}
|
||||
|
||||
if (matchedCommand.RequiresTarget)
|
||||
{
|
||||
if (args.Length > 0)
|
||||
{
|
||||
if (!int.TryParse(args[0], out var cNum))
|
||||
{
|
||||
cNum = -1;
|
||||
}
|
||||
|
||||
if (Args[0][0] == '@') // user specifying target by database ID
|
||||
if (args[0][0] == '@') // user specifying target by database ID
|
||||
{
|
||||
int.TryParse(Args[0].Substring(1, Args[0].Length - 1), out var dbID);
|
||||
int.TryParse(args[0].Substring(1, args[0].Length - 1), out var dbID);
|
||||
|
||||
var found = await Manager.GetClientService().Get(dbID);
|
||||
var found = await manager.GetClientService().Get(dbID);
|
||||
if (found != null)
|
||||
{
|
||||
found = Manager.FindActiveClient(found);
|
||||
E.Target = found;
|
||||
E.Target.CurrentServer = found.CurrentServer ?? E.Owner;
|
||||
E.Data = string.Join(" ", Args.Skip(1));
|
||||
found = manager.FindActiveClient(found);
|
||||
gameEvent.Target = found;
|
||||
gameEvent.Target.CurrentServer = found.CurrentServer ?? gameEvent.Owner;
|
||||
gameEvent.Data = string.Join(" ", args.Skip(1));
|
||||
}
|
||||
}
|
||||
|
||||
else if (Args[0].Length < 3 && cNum > -1 && cNum < E.Owner.MaxClients
|
||||
else if (args[0].Length < 3 && cNum > -1 && cNum < gameEvent.Owner.MaxClients
|
||||
) // user specifying target by client num
|
||||
{
|
||||
if (E.Owner.Clients[cNum] != null)
|
||||
if (gameEvent.Owner.Clients[cNum] != null)
|
||||
{
|
||||
E.Target = E.Owner.Clients[cNum];
|
||||
E.Data = string.Join(" ", Args.Skip(1));
|
||||
gameEvent.Target = gameEvent.Owner.Clients[cNum];
|
||||
gameEvent.Data = string.Join(" ", args.Skip(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<EFClient> matchingPlayers;
|
||||
|
||||
if (E.Target == null && C.RequiresTarget) // Find active player including quotes (multiple words)
|
||||
if (gameEvent.Target == null &&
|
||||
matchedCommand.RequiresTarget) // Find active player including quotes (multiple words)
|
||||
{
|
||||
matchingPlayers = E.Owner.GetClientByName(E.Data);
|
||||
matchingPlayers = gameEvent.Owner.GetClientByName(gameEvent.Data);
|
||||
if (matchingPlayers.Count > 1)
|
||||
{
|
||||
E.Origin.Tell(loc["COMMAND_TARGET_MULTI"]);
|
||||
throw new CommandException($"{E.Origin} had multiple players found for {C.Name}");
|
||||
gameEvent.Origin.Tell(loc["COMMAND_TARGET_MULTI"]);
|
||||
throw new CommandException(
|
||||
$"{gameEvent.Origin} had multiple players found for {matchedCommand.Name}");
|
||||
}
|
||||
|
||||
if (matchingPlayers.Count == 1)
|
||||
{
|
||||
E.Target = matchingPlayers.First();
|
||||
gameEvent.Target = matchingPlayers.First();
|
||||
|
||||
var escapedName = Regex.Escape(E.Target.CleanedName);
|
||||
var escapedName = Regex.Escape(gameEvent.Target.CleanedName);
|
||||
var reg = new Regex($"(\"{escapedName}\")|({escapedName})", RegexOptions.IgnoreCase);
|
||||
E.Data = reg.Replace(E.Data, "", 1).Trim();
|
||||
gameEvent.Data = reg.Replace(gameEvent.Data, "", 1).Trim();
|
||||
|
||||
if (E.Data.Length == 0 && C.RequiredArgumentCount > 1)
|
||||
if (gameEvent.Data.Length == 0 && matchedCommand.RequiredArgumentCount > 1)
|
||||
{
|
||||
E.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
|
||||
E.Origin.Tell(C.Syntax);
|
||||
throw new CommandException($"{E.Origin} did not supply enough arguments for \"{C.Name}\"");
|
||||
gameEvent.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
|
||||
gameEvent.Origin.Tell(matchedCommand.Syntax);
|
||||
throw new CommandException(
|
||||
$"{gameEvent.Origin} did not supply enough arguments for \"{matchedCommand.Name}\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (E.Target == null && C.RequiresTarget && Args.Length > 0) // Find active player as single word
|
||||
if (gameEvent.Target == null && matchedCommand.RequiresTarget &&
|
||||
args.Length > 0) // Find active player as single word
|
||||
{
|
||||
matchingPlayers = E.Owner.GetClientByName(Args[0]);
|
||||
matchingPlayers = gameEvent.Owner.GetClientByName(args[0]);
|
||||
if (matchingPlayers.Count > 1)
|
||||
{
|
||||
E.Origin.Tell(loc["COMMAND_TARGET_MULTI"]);
|
||||
gameEvent.Origin.Tell(loc["COMMAND_TARGET_MULTI"]);
|
||||
foreach (var p in matchingPlayers)
|
||||
E.Origin.Tell($"[(Color::Yellow){p.ClientNumber}(Color::White)] {p.Name}");
|
||||
throw new CommandException($"{E.Origin} had multiple players found for {C.Name}");
|
||||
gameEvent.Origin.Tell($"[(Color::Yellow){p.ClientNumber}(Color::White)] {p.Name}");
|
||||
throw new CommandException(
|
||||
$"{gameEvent.Origin} had multiple players found for {matchedCommand.Name}");
|
||||
}
|
||||
|
||||
if (matchingPlayers.Count == 1)
|
||||
{
|
||||
E.Target = matchingPlayers.First();
|
||||
gameEvent.Target = matchingPlayers.First();
|
||||
|
||||
var escapedName = Regex.Escape(E.Target.CleanedName);
|
||||
var escapedArg = Regex.Escape(Args[0]);
|
||||
var escapedName = Regex.Escape(gameEvent.Target.CleanedName);
|
||||
var escapedArg = Regex.Escape(args[0]);
|
||||
var reg = new Regex($"({escapedName})|({escapedArg})", RegexOptions.IgnoreCase);
|
||||
E.Data = reg.Replace(E.Data, "", 1).Trim();
|
||||
gameEvent.Data = reg.Replace(gameEvent.Data, "", 1).Trim();
|
||||
|
||||
if ((E.Data.Trim() == E.Target.CleanedName.ToLower().Trim() ||
|
||||
E.Data == string.Empty) &&
|
||||
C.RequiresTarget)
|
||||
if ((gameEvent.Data.Trim() == gameEvent.Target.CleanedName.ToLower().Trim() ||
|
||||
gameEvent.Data == string.Empty) &&
|
||||
matchedCommand.RequiresTarget)
|
||||
{
|
||||
E.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
|
||||
E.Origin.Tell(C.Syntax);
|
||||
throw new CommandException($"{E.Origin} did not supply enough arguments for \"{C.Name}\"");
|
||||
gameEvent.Origin.Tell(loc["COMMAND_MISSINGARGS"]);
|
||||
gameEvent.Origin.Tell(matchedCommand.Syntax);
|
||||
throw new CommandException(
|
||||
$"{gameEvent.Origin} did not supply enough arguments for \"{matchedCommand.Name}\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (E.Target == null && C.RequiresTarget)
|
||||
if (gameEvent.Target == null && matchedCommand.RequiresTarget)
|
||||
{
|
||||
E.Origin.Tell(loc["COMMAND_TARGET_NOTFOUND"]);
|
||||
throw new CommandException($"{E.Origin} specified invalid player for \"{C.Name}\"");
|
||||
gameEvent.Origin.Tell(loc["COMMAND_TARGET_NOTFOUND"]);
|
||||
throw new CommandException(
|
||||
$"{gameEvent.Origin} specified invalid player for \"{matchedCommand.Name}\"");
|
||||
}
|
||||
}
|
||||
|
||||
E.Data = E.Data.Trim();
|
||||
return C;
|
||||
gameEvent.Data = gameEvent.Data.Trim();
|
||||
return matchedCommand;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -381,7 +381,7 @@ namespace SharedLibraryCore.Commands
|
||||
{
|
||||
// todo: don't do the lookup here
|
||||
var penalties = await gameEvent.Owner.Manager.GetPenaltyService().GetActivePenaltiesAsync(gameEvent.Target.AliasLinkId,
|
||||
gameEvent.Target.CurrentAliasId, gameEvent.Target.NetworkId, gameEvent.Target.CurrentAlias.IPAddress);
|
||||
gameEvent.Target.CurrentAliasId, gameEvent.Target.NetworkId, gameEvent.Target.GameName, gameEvent.Target.CurrentAlias.IPAddress);
|
||||
|
||||
if (penalties
|
||||
.FirstOrDefault(p =>
|
||||
@ -897,7 +897,7 @@ namespace SharedLibraryCore.Commands
|
||||
public override async Task ExecuteAsync(GameEvent E)
|
||||
{
|
||||
var existingPenalties = await E.Owner.Manager.GetPenaltyService()
|
||||
.GetActivePenaltiesAsync(E.Target.AliasLinkId, E.Target.CurrentAliasId, E.Target.NetworkId, E.Target.IPAddress);
|
||||
.GetActivePenaltiesAsync(E.Target.AliasLinkId, E.Target.CurrentAliasId, E.Target.NetworkId, E.Target.GameName, E.Target.IPAddress);
|
||||
var penalty = existingPenalties.FirstOrDefault(b => b.Type > EFPenalty.PenaltyType.Kick);
|
||||
|
||||
if (penalty == null)
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models.Client;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
|
||||
namespace SharedLibraryCore.Commands
|
||||
@ -19,13 +20,16 @@ namespace SharedLibraryCore.Commands
|
||||
RequiresTarget = false;
|
||||
}
|
||||
|
||||
public override Task ExecuteAsync(GameEvent E)
|
||||
public override Task ExecuteAsync(GameEvent gameEvent)
|
||||
{
|
||||
var state = E.Owner.Manager.TokenAuthenticator.GenerateNextToken(E.Origin.NetworkId);
|
||||
E.Origin.Tell(string.Format(_translationLookup["COMMANDS_GENERATETOKEN_SUCCESS"], state.Token,
|
||||
$"{state.RemainingTime} {_translationLookup["GLOBAL_MINUTES"]}", E.Origin.ClientId));
|
||||
var state = gameEvent.Owner.Manager.TokenAuthenticator.GenerateNextToken(new TokenIdentifier
|
||||
{
|
||||
ClientId = gameEvent.Origin.ClientId
|
||||
});
|
||||
gameEvent.Origin.Tell(string.Format(_translationLookup["COMMANDS_GENERATETOKEN_SUCCESS"], state.Token,
|
||||
$"{state.RemainingTime} {_translationLookup["GLOBAL_MINUTES"]}", gameEvent.Origin.ClientId));
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -204,6 +204,11 @@ namespace SharedLibraryCore
|
||||
/// client logged out of webfront
|
||||
/// </summary>
|
||||
Logout = 113,
|
||||
|
||||
/// <summary>
|
||||
/// meta value updated on client
|
||||
/// </summary>
|
||||
MetaUpdated = 114,
|
||||
|
||||
// events "generated" by IW4MAdmin
|
||||
/// <summary>
|
||||
|
9
SharedLibraryCore/Helpers/TokenIdentifier.cs
Normal file
9
SharedLibraryCore/Helpers/TokenIdentifier.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using SharedLibraryCore.Interfaces;
|
||||
|
||||
namespace SharedLibraryCore.Helpers;
|
||||
|
||||
public class TokenIdentifier : ITokenIdentifier
|
||||
{
|
||||
public int ClientId { get; set; }
|
||||
public string Token { get; set; }
|
||||
}
|
@ -4,7 +4,6 @@ namespace SharedLibraryCore.Helpers
|
||||
{
|
||||
public sealed class TokenState
|
||||
{
|
||||
public long NetworkId { get; set; }
|
||||
public DateTime RequestTime { get; set; } = DateTime.Now;
|
||||
public TimeSpan TokenDuration { get; set; }
|
||||
public string Token { get; set; }
|
||||
@ -12,4 +11,4 @@ namespace SharedLibraryCore.Helpers
|
||||
public string RemainingTime => Math.Round(-(DateTime.Now - RequestTime).Subtract(TokenDuration).TotalMinutes, 1)
|
||||
.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
54
SharedLibraryCore/Interfaces/IAlertManager.cs
Normal file
54
SharedLibraryCore/Interfaces/IAlertManager.cs
Normal 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; }
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -102,5 +102,7 @@ namespace SharedLibraryCore.Interfaces
|
||||
/// event executed when event has finished executing
|
||||
/// </summary>
|
||||
event EventHandler<GameEvent> OnGameEventExecuted;
|
||||
|
||||
IAlertManager AlertManager { get; }
|
||||
}
|
||||
}
|
||||
|
@ -110,6 +110,6 @@ namespace SharedLibraryCore.Interfaces
|
||||
/// </summary>
|
||||
/// <param name="command">name of command being executed</param>
|
||||
/// <returns></returns>
|
||||
TimeSpan OverrideTimeoutForCommand(string command);
|
||||
TimeSpan? OverrideTimeoutForCommand(string command);
|
||||
}
|
||||
}
|
||||
|
@ -74,6 +74,11 @@ namespace SharedLibraryCore.Interfaces
|
||||
/// </summary>
|
||||
IDictionary<string, string> DefaultDvarValues { get; }
|
||||
|
||||
/// <summary>
|
||||
/// contains a setup of commands that have override timeouts
|
||||
/// </summary>
|
||||
IDictionary<string, int?> OverrideCommandTimeouts { get; }
|
||||
|
||||
/// <summary>
|
||||
/// specifies how many lines can be used for ingame notice
|
||||
/// </summary>
|
||||
@ -100,7 +105,11 @@ namespace SharedLibraryCore.Interfaces
|
||||
string DefaultInstallationDirectoryHint { get; }
|
||||
|
||||
ColorCodeMapping ColorCodeMapping { get; }
|
||||
|
||||
|
||||
short FloodProtectInterval { get; }
|
||||
/// <summary>
|
||||
/// indicates if diacritics (accented characters) should be normalized
|
||||
/// </summary>
|
||||
bool ShouldRemoveDiacritics { get; }
|
||||
}
|
||||
}
|
||||
|
@ -7,16 +7,15 @@ namespace SharedLibraryCore.Interfaces
|
||||
/// <summary>
|
||||
/// generates and returns a token for the given network id
|
||||
/// </summary>
|
||||
/// <param name="networkId">network id of the players to generate the token for</param>
|
||||
/// <param name="authInfo">auth information for next token generation</param>
|
||||
/// <returns>4 character string token</returns>
|
||||
TokenState GenerateNextToken(long networkId);
|
||||
TokenState GenerateNextToken(ITokenIdentifier authInfo);
|
||||
|
||||
/// <summary>
|
||||
/// authorizes given token
|
||||
/// </summary>
|
||||
/// <param name="networkId">network id of the client to authorize</param>
|
||||
/// <param name="token">token to authorize</param>
|
||||
/// <param name="authInfo">auth information</param>
|
||||
/// <returns>true if token authorized successfully, false otherwise</returns>
|
||||
bool AuthorizeToken(long networkId, string token);
|
||||
bool AuthorizeToken(ITokenIdentifier authInfo);
|
||||
}
|
||||
}
|
||||
|
7
SharedLibraryCore/Interfaces/ITokenIdentifier.cs
Normal file
7
SharedLibraryCore/Interfaces/ITokenIdentifier.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace SharedLibraryCore.Interfaces;
|
||||
|
||||
public interface ITokenIdentifier
|
||||
{
|
||||
int ClientId { get; }
|
||||
string Token { get; }
|
||||
}
|
@ -76,7 +76,7 @@ namespace SharedLibraryCore.Database.Models
|
||||
[NotMapped]
|
||||
public virtual int? IPAddress
|
||||
{
|
||||
get => CurrentAlias.IPAddress;
|
||||
get => CurrentAlias?.IPAddress;
|
||||
set => CurrentAlias.IPAddress = value;
|
||||
}
|
||||
|
||||
@ -100,7 +100,10 @@ namespace SharedLibraryCore.Database.Models
|
||||
|
||||
[NotMapped] public int Score { get; set; }
|
||||
|
||||
[NotMapped] public bool IsBot => NetworkId == Name.GenerateGuidFromString();
|
||||
[NotMapped]
|
||||
public bool IsBot => NetworkId == Name.GenerateGuidFromString() ||
|
||||
IPAddressString == System.Net.IPAddress.Broadcast.ToString() ||
|
||||
IPAddressString == "unknown";
|
||||
|
||||
[NotMapped] public bool IsZombieClient => IsBot && Name == "Zombie";
|
||||
|
||||
@ -170,7 +173,7 @@ namespace SharedLibraryCore.Database.Models
|
||||
?.CorrelationId ?? Guid.NewGuid()
|
||||
};
|
||||
|
||||
e.Output.Add(message.FormatMessageForEngine(CurrentServer?.RconParser.Configuration.ColorCodeMapping)
|
||||
e.Output.Add(message.FormatMessageForEngine(CurrentServer?.RconParser.Configuration)
|
||||
.StripColors());
|
||||
|
||||
CurrentServer?.Manager.AddEvent(e);
|
||||
@ -682,7 +685,7 @@ namespace SharedLibraryCore.Database.Models
|
||||
|
||||
// we want to get any penalties that are tied to their IP or AliasLink (but not necessarily their GUID)
|
||||
var activePenalties = await CurrentServer.Manager.GetPenaltyService()
|
||||
.GetActivePenaltiesAsync(AliasLinkId, CurrentAliasId, NetworkId, ipAddress);
|
||||
.GetActivePenaltiesAsync(AliasLinkId, CurrentAliasId, NetworkId, GameName, ipAddress);
|
||||
var banPenalty = activePenalties.FirstOrDefault(_penalty => _penalty.Type == EFPenalty.PenaltyType.Ban);
|
||||
var tempbanPenalty =
|
||||
activePenalties.FirstOrDefault(_penalty => _penalty.Type == EFPenalty.PenaltyType.TempBan);
|
||||
|
@ -225,7 +225,7 @@ namespace SharedLibraryCore
|
||||
var formattedMessage = string.Format(RconParser.Configuration.CommandPrefixes.Say ?? "",
|
||||
$"{(CustomSayEnabled && GameName == Game.IW4 ? $"{CustomSayName}: " : "")}{message}");
|
||||
ServerLogger.LogDebug("All-> {Message}",
|
||||
message.FormatMessageForEngine(RconParser.Configuration.ColorCodeMapping).StripColors());
|
||||
message.FormatMessageForEngine(RconParser.Configuration).StripColors());
|
||||
|
||||
var e = new GameEvent
|
||||
{
|
||||
@ -289,13 +289,13 @@ namespace SharedLibraryCore
|
||||
else
|
||||
{
|
||||
ServerLogger.LogDebug("Tell[{ClientNumber}]->{Message}", targetClient.ClientNumber,
|
||||
message.FormatMessageForEngine(RconParser.Configuration.ColorCodeMapping).StripColors());
|
||||
message.FormatMessageForEngine(RconParser.Configuration).StripColors());
|
||||
}
|
||||
|
||||
if (targetClient.Level == Data.Models.Client.EFClient.Permission.Console)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
var cleanMessage = message.FormatMessageForEngine(RconParser.Configuration.ColorCodeMapping)
|
||||
var cleanMessage = message.FormatMessageForEngine(RconParser.Configuration)
|
||||
.StripColors();
|
||||
using (LogContext.PushProperty("Server", ToString()))
|
||||
{
|
||||
|
@ -23,25 +23,26 @@ namespace SharedLibraryCore.Services
|
||||
{
|
||||
public class ClientService : IEntityService<EFClient>, IResourceQueryHelper<FindClientRequest, FindClientResult>
|
||||
{
|
||||
private static readonly Func<DatabaseContext, long, Task<EFClient>> _getUniqueQuery =
|
||||
EF.CompileAsyncQuery((DatabaseContext context, long networkId) =>
|
||||
private static readonly Func<DatabaseContext, long, Reference.Game, Task<EFClient>> GetUniqueQuery =
|
||||
EF.CompileAsyncQuery((DatabaseContext context, long networkId, Reference.Game game) =>
|
||||
context.Clients
|
||||
.Select(_client => new EFClient
|
||||
.Select(client => new EFClient
|
||||
{
|
||||
ClientId = _client.ClientId,
|
||||
AliasLinkId = _client.AliasLinkId,
|
||||
Level = _client.Level,
|
||||
Connections = _client.Connections,
|
||||
FirstConnection = _client.FirstConnection,
|
||||
LastConnection = _client.LastConnection,
|
||||
Masked = _client.Masked,
|
||||
NetworkId = _client.NetworkId,
|
||||
TotalConnectionTime = _client.TotalConnectionTime,
|
||||
AliasLink = _client.AliasLink,
|
||||
Password = _client.Password,
|
||||
PasswordSalt = _client.PasswordSalt
|
||||
ClientId = client.ClientId,
|
||||
AliasLinkId = client.AliasLinkId,
|
||||
Level = client.Level,
|
||||
Connections = client.Connections,
|
||||
FirstConnection = client.FirstConnection,
|
||||
LastConnection = client.LastConnection,
|
||||
Masked = client.Masked,
|
||||
NetworkId = client.NetworkId,
|
||||
TotalConnectionTime = client.TotalConnectionTime,
|
||||
AliasLink = client.AliasLink,
|
||||
Password = client.Password,
|
||||
PasswordSalt = client.PasswordSalt,
|
||||
GameName = client.GameName
|
||||
})
|
||||
.FirstOrDefault(c => c.NetworkId == networkId)
|
||||
.FirstOrDefault(client => client.NetworkId == networkId && client.GameName == game)
|
||||
);
|
||||
|
||||
private readonly ApplicationConfiguration _appConfig;
|
||||
@ -235,10 +236,10 @@ namespace SharedLibraryCore.Services
|
||||
return foundClient.Client;
|
||||
}
|
||||
|
||||
public virtual async Task<EFClient> GetUnique(long entityAttribute)
|
||||
public virtual async Task<EFClient> GetUnique(long entityAttribute, object altKey = null)
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
return await _getUniqueQuery(context, entityAttribute);
|
||||
return await GetUniqueQuery(context, entityAttribute, (Reference.Game)altKey);
|
||||
}
|
||||
|
||||
public async Task<EFClient> Update(EFClient temporalClient)
|
||||
@ -285,7 +286,7 @@ namespace SharedLibraryCore.Services
|
||||
entity.PasswordSalt = temporalClient.PasswordSalt;
|
||||
}
|
||||
|
||||
entity.GameName ??= temporalClient.GameName;
|
||||
entity.GameName = temporalClient.GameName;
|
||||
|
||||
// update in database
|
||||
await context.SaveChangesAsync();
|
||||
@ -758,19 +759,20 @@ namespace SharedLibraryCore.Services
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
return await context.Clients
|
||||
.Select(_client => new EFClient
|
||||
.Select(client => new EFClient
|
||||
{
|
||||
NetworkId = _client.NetworkId,
|
||||
ClientId = _client.ClientId,
|
||||
NetworkId = client.NetworkId,
|
||||
ClientId = client.ClientId,
|
||||
CurrentAlias = new EFAlias
|
||||
{
|
||||
Name = _client.CurrentAlias.Name
|
||||
Name = client.CurrentAlias.Name
|
||||
},
|
||||
Password = _client.Password,
|
||||
PasswordSalt = _client.PasswordSalt,
|
||||
Level = _client.Level
|
||||
Password = client.Password,
|
||||
PasswordSalt = client.PasswordSalt,
|
||||
GameName = client.GameName,
|
||||
Level = client.Level
|
||||
})
|
||||
.FirstAsync(_client => _client.ClientId == clientId);
|
||||
.FirstAsync(client => client.ClientId == clientId);
|
||||
}
|
||||
|
||||
public async Task<List<EFClient>> GetPrivilegedClients(bool includeName = true)
|
||||
@ -851,31 +853,35 @@ namespace SharedLibraryCore.Services
|
||||
|
||||
else
|
||||
{
|
||||
iqClients = iqClients.Where(_client => networkId == _client.NetworkId ||
|
||||
linkIds.Contains(_client.AliasLinkId)
|
||||
|| !_appConfig.EnableImplicitAccountLinking &&
|
||||
_client.CurrentAlias.IPAddress != null &&
|
||||
_client.CurrentAlias.IPAddress == ipAddress);
|
||||
iqClients = iqClients.Where(client => networkId == client.NetworkId || linkIds.Contains(client.AliasLinkId));
|
||||
}
|
||||
|
||||
if (ipAddress is not null && !_appConfig.EnableImplicitAccountLinking)
|
||||
{
|
||||
iqClients = iqClients.Union(context.Clients.Where(client => client.CurrentAlias.IPAddress == ipAddress));
|
||||
}
|
||||
|
||||
// we want to project our results
|
||||
var iqClientProjection = iqClients.OrderByDescending(_client => _client.LastConnection)
|
||||
.Select(_client => new PlayerInfo
|
||||
var iqClientProjection = iqClients.OrderByDescending(client => client.LastConnection)
|
||||
.Select(client => new PlayerInfo
|
||||
{
|
||||
Name = _client.CurrentAlias.Name,
|
||||
LevelInt = (int)_client.Level,
|
||||
LastConnection = _client.LastConnection,
|
||||
ClientId = _client.ClientId,
|
||||
IPAddress = _client.CurrentAlias.IPAddress.HasValue
|
||||
? _client.CurrentAlias.SearchableIPAddress
|
||||
: ""
|
||||
Name = client.CurrentAlias.Name,
|
||||
LevelInt = (int)client.Level,
|
||||
LastConnection = client.LastConnection,
|
||||
ClientId = client.ClientId,
|
||||
IPAddress = client.CurrentAlias.IPAddress.HasValue
|
||||
? client.CurrentAlias.SearchableIPAddress
|
||||
: "",
|
||||
Game = client.GameName
|
||||
});
|
||||
|
||||
var clients = await iqClientProjection.ToListAsync();
|
||||
|
||||
// this is so we don't try to evaluate this in the linq to entities query
|
||||
foreach (var client in clients)
|
||||
{
|
||||
client.Level = ((Permission)client.LevelInt).ToLocalizedLevelName();
|
||||
}
|
||||
|
||||
return clients;
|
||||
}
|
||||
|
@ -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())
|
||||
{
|
||||
|
@ -4,7 +4,7 @@
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
|
||||
<Version>2022.6.9.1</Version>
|
||||
<Version>2022.6.16.1</Version>
|
||||
<Authors>RaidMax</Authors>
|
||||
<Company>Forever None</Company>
|
||||
<Configurations>Debug;Release;Prerelease</Configurations>
|
||||
@ -19,7 +19,7 @@
|
||||
<IsPackable>true</IsPackable>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<Description>Shared Library for IW4MAdmin</Description>
|
||||
<PackageVersion>2022.6.9.1</PackageVersion>
|
||||
<PackageVersion>2022.6.16.1</PackageVersion>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
@ -71,31 +71,7 @@ namespace SharedLibraryCore
|
||||
/// </summary>
|
||||
public const long WORLD_ID = -1;
|
||||
|
||||
public static Dictionary<Permission, string> PermissionLevelOverrides { get; } =
|
||||
new Dictionary<Permission, string>();
|
||||
|
||||
public static string HttpRequest(string location, string header, string headerValue)
|
||||
{
|
||||
using (var RequestClient = new HttpClient())
|
||||
{
|
||||
RequestClient.DefaultRequestHeaders.Add(header, headerValue);
|
||||
var response = RequestClient.GetStringAsync(location).Result;
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
//Get string with specified number of spaces -- really only for visual output
|
||||
public static string GetSpaces(int Num)
|
||||
{
|
||||
var SpaceString = string.Empty;
|
||||
while (Num > 0)
|
||||
{
|
||||
SpaceString += ' ';
|
||||
Num--;
|
||||
}
|
||||
|
||||
return SpaceString;
|
||||
}
|
||||
public static Dictionary<Permission, string> PermissionLevelOverrides { get; } = new ();
|
||||
|
||||
//Remove words from a space delimited string
|
||||
public static string RemoveWords(this string str, int num)
|
||||
@ -133,12 +109,12 @@ namespace SharedLibraryCore
|
||||
{
|
||||
var lookingFor = str.ToLower();
|
||||
|
||||
for (var Perm = Permission.User; Perm < Permission.Console; Perm++)
|
||||
if (lookingFor.Contains(Perm.ToString().ToLower())
|
||||
for (var perm = Permission.User; perm < Permission.Console; perm++)
|
||||
if (lookingFor.Contains(perm.ToString().ToLower())
|
||||
|| lookingFor.Contains(CurrentLocalization
|
||||
.LocalizationIndex[$"GLOBAL_PERMISSION_{Perm.ToString().ToUpper()}"].ToLower()))
|
||||
.LocalizationIndex[$"GLOBAL_PERMISSION_{perm.ToString().ToUpper()}"].ToLower()))
|
||||
{
|
||||
return Perm;
|
||||
return perm;
|
||||
}
|
||||
|
||||
return Permission.Banned;
|
||||
@ -171,9 +147,25 @@ namespace SharedLibraryCore
|
||||
return str.Replace("//", "/ /");
|
||||
}
|
||||
|
||||
public static string FormatMessageForEngine(this string str, ColorCodeMapping mapping)
|
||||
public static string RemoveDiacritics(this string text)
|
||||
{
|
||||
if (mapping == null || string.IsNullOrEmpty(str))
|
||||
var normalizedString = text.Normalize(NormalizationForm.FormD);
|
||||
var stringBuilder = new StringBuilder();
|
||||
|
||||
foreach (var c in from c in normalizedString.EnumerateRunes()
|
||||
let unicodeCategory = Rune.GetUnicodeCategory(c)
|
||||
where unicodeCategory != UnicodeCategory.NonSpacingMark
|
||||
select c)
|
||||
{
|
||||
stringBuilder.Append(c);
|
||||
}
|
||||
|
||||
return stringBuilder.ToString().Normalize(NormalizationForm.FormC);
|
||||
}
|
||||
|
||||
public static string FormatMessageForEngine(this string str, IRConParserConfiguration config)
|
||||
{
|
||||
if (config == null || string.IsNullOrEmpty(str))
|
||||
{
|
||||
return str;
|
||||
}
|
||||
@ -184,13 +176,19 @@ namespace SharedLibraryCore
|
||||
foreach (var match in colorCodeMatches.Where(m => m.Success))
|
||||
{
|
||||
var key = match.Groups[1].ToString();
|
||||
output = output.Replace(match.Value, mapping.TryGetValue(key, out var code) ? code : "");
|
||||
output = output.Replace(match.Value, config.ColorCodeMapping.TryGetValue(key, out var code) ? code : "");
|
||||
}
|
||||
|
||||
if (config.ShouldRemoveDiacritics)
|
||||
{
|
||||
output = output.RemoveDiacritics();
|
||||
}
|
||||
|
||||
return output.FixIW4ForwardSlash();
|
||||
}
|
||||
|
||||
private static readonly IList<string> _zmGameTypes = new[] { "zclassic", "zstandard", "zcleansed", "zgrief" };
|
||||
private static readonly IList<string> ZmGameTypes = new[]
|
||||
{ "zclassic", "zstandard", "zcleansed", "zgrief", "zom", "cmp" };
|
||||
|
||||
/// <summary>
|
||||
/// indicates if the given server is running a zombie game mode
|
||||
@ -199,7 +197,8 @@ namespace SharedLibraryCore
|
||||
/// <returns></returns>
|
||||
public static bool IsZombieServer(this Server server)
|
||||
{
|
||||
return server.GameName == Game.T6 && _zmGameTypes.Contains(server.Gametype.ToLower());
|
||||
return new[] { Game.T4, Game.T5, Game.T6 }.Contains(server.GameName) &&
|
||||
ZmGameTypes.Contains(server.Gametype.ToLower());
|
||||
}
|
||||
|
||||
public static bool IsCodGame(this Server server)
|
||||
@ -233,7 +232,7 @@ namespace SharedLibraryCore
|
||||
{
|
||||
var localized =
|
||||
CurrentLocalization.LocalizationIndex[$"GLOBAL_PERMISSION_{permission.ToString().ToUpper()}"];
|
||||
return PermissionLevelOverrides.ContainsKey(permission) && PermissionLevelOverrides[permission] != localized
|
||||
return PermissionLevelOverrides.ContainsKey(permission) && PermissionLevelOverrides[permission] != permission.ToString()
|
||||
? PermissionLevelOverrides[permission]
|
||||
: localized;
|
||||
}
|
||||
@ -262,11 +261,6 @@ namespace SharedLibraryCore
|
||||
return str.StartsWith(broadcastCommandPrefix);
|
||||
}
|
||||
|
||||
public static IManagerCommand AsCommand(this GameEvent gameEvent)
|
||||
{
|
||||
return gameEvent.Extra as IManagerCommand;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the full gametype name
|
||||
/// </summary>
|
||||
@ -433,7 +427,7 @@ namespace SharedLibraryCore
|
||||
{
|
||||
var success = IPAddress.TryParse(str, out var ip);
|
||||
return success && ip.GetAddressBytes().Count(_byte => _byte == 0) != 4
|
||||
? (int?)BitConverter.ToInt32(ip.GetAddressBytes(), 0)
|
||||
? BitConverter.ToInt32(ip.GetAddressBytes(), 0)
|
||||
: null;
|
||||
}
|
||||
|
||||
@ -482,11 +476,6 @@ namespace SharedLibraryCore
|
||||
return Game.UKN;
|
||||
}
|
||||
|
||||
public static string EscapeMarkdown(this string markdownString)
|
||||
{
|
||||
return markdownString.Replace("<", "\\<").Replace(">", "\\>").Replace("|", "\\|");
|
||||
}
|
||||
|
||||
public static TimeSpan ParseTimespan(this string input)
|
||||
{
|
||||
var expressionMatch = Regex.Match(input, @"([0-9]+)(\w+)");
|
||||
@ -608,7 +597,7 @@ namespace SharedLibraryCore
|
||||
public static bool PromptBool(this string question, string description = null, bool defaultValue = true)
|
||||
{
|
||||
Console.Write($"{question}?{(string.IsNullOrEmpty(description) ? " " : $" ({description}) ")}[y/n]: ");
|
||||
var response = Console.ReadLine().ToLower().FirstOrDefault();
|
||||
var response = Console.ReadLine()?.ToLower().FirstOrDefault();
|
||||
return response != 0 ? response == 'y' : defaultValue;
|
||||
}
|
||||
|
||||
@ -639,7 +628,7 @@ namespace SharedLibraryCore
|
||||
Console.WriteLine(new string('=', 52));
|
||||
|
||||
var selectionIndex = PromptInt(CurrentLocalization.LocalizationIndex["SETUP_PROMPT_MAKE_SELECTION"], null,
|
||||
hasDefault ? 0 : 1, selections.Length, hasDefault ? 0 : (int?)null);
|
||||
hasDefault ? 0 : 1, selections.Length, hasDefault ? 0 : null);
|
||||
|
||||
if (!hasDefault)
|
||||
{
|
||||
@ -667,13 +656,13 @@ namespace SharedLibraryCore
|
||||
$"{question}{(string.IsNullOrEmpty(description) ? "" : $" ({description})")}{(defaultValue == null ? "" : $" [{CurrentLocalization.LocalizationIndex["SETUP_PROMPT_DEFAULT"]} {defaultValue.Value.ToString()}]")}: ");
|
||||
int response;
|
||||
|
||||
string inputOrDefault()
|
||||
string InputOrDefault()
|
||||
{
|
||||
var input = Console.ReadLine();
|
||||
return string.IsNullOrEmpty(input) && defaultValue != null ? defaultValue.ToString() : input;
|
||||
}
|
||||
|
||||
while (!int.TryParse(inputOrDefault(), out response) ||
|
||||
while (!int.TryParse(InputOrDefault(), out response) ||
|
||||
response < minValue ||
|
||||
response > maxValue)
|
||||
{
|
||||
@ -698,7 +687,7 @@ namespace SharedLibraryCore
|
||||
/// <returns></returns>
|
||||
public static string PromptString(this string question, string description = null, string defaultValue = null)
|
||||
{
|
||||
string inputOrDefault()
|
||||
string InputOrDefault()
|
||||
{
|
||||
var input = Console.ReadLine();
|
||||
return string.IsNullOrEmpty(input) && defaultValue != null ? defaultValue : input;
|
||||
@ -709,7 +698,7 @@ namespace SharedLibraryCore
|
||||
{
|
||||
Console.Write(
|
||||
$"{question}{(string.IsNullOrEmpty(description) ? "" : $" ({description})")}{(defaultValue == null ? "" : $" [{CurrentLocalization.LocalizationIndex["SETUP_PROMPT_DEFAULT"]} {defaultValue}]")}: ");
|
||||
response = inputOrDefault();
|
||||
response = InputOrDefault();
|
||||
} while (string.IsNullOrWhiteSpace(response) && response != defaultValue);
|
||||
|
||||
return response;
|
||||
@ -1181,7 +1170,8 @@ namespace SharedLibraryCore
|
||||
Meta = client.Meta,
|
||||
ReceivedPenalties = client.ReceivedPenalties,
|
||||
AdministeredPenalties = client.AdministeredPenalties,
|
||||
Active = client.Active
|
||||
Active = client.Active,
|
||||
GameName = client.GameName
|
||||
};
|
||||
}
|
||||
|
||||
@ -1264,5 +1254,8 @@ namespace SharedLibraryCore
|
||||
|
||||
return allRules[index];
|
||||
}
|
||||
|
||||
public static string MakeAbbreviation(string gameName) => string.Join("",
|
||||
gameName.Split(' ').Select(word => char.ToUpper(word.First())).ToArray());
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Helpers;
|
||||
using SharedLibraryCore.Services;
|
||||
using WebfrontCore.Controllers.API.Dtos;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
@ -100,9 +101,15 @@ namespace WebfrontCore.Controllers.API
|
||||
|
||||
if (!Authorized)
|
||||
{
|
||||
var tokenData = new TokenIdentifier
|
||||
{
|
||||
ClientId = clientId,
|
||||
Token = request.Password
|
||||
};
|
||||
|
||||
loginSuccess =
|
||||
Manager.TokenAuthenticator.AuthorizeToken(privilegedClient.NetworkId, request.Password) ||
|
||||
(await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(request.Password,
|
||||
Manager.TokenAuthenticator.AuthorizeToken(tokenData) ||
|
||||
(await Task.FromResult(Hashing.Hash(request.Password,
|
||||
privilegedClient.PasswordSalt)))[0] == privilegedClient.Password;
|
||||
}
|
||||
|
||||
@ -120,7 +127,7 @@ namespace WebfrontCore.Controllers.API
|
||||
var claimsPrinciple = new ClaimsPrincipal(claimsIdentity);
|
||||
await SignInAsync(claimsPrinciple);
|
||||
|
||||
Manager.AddEvent(new GameEvent()
|
||||
Manager.AddEvent(new GameEvent
|
||||
{
|
||||
Origin = privilegedClient,
|
||||
Type = GameEvent.EventType.Login,
|
||||
@ -149,7 +156,7 @@ namespace WebfrontCore.Controllers.API
|
||||
{
|
||||
if (Authorized)
|
||||
{
|
||||
Manager.AddEvent(new GameEvent()
|
||||
Manager.AddEvent(new GameEvent
|
||||
{
|
||||
Origin = Client,
|
||||
Type = GameEvent.EventType.Logout,
|
||||
|
@ -7,7 +7,7 @@ using System;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using SharedLibraryCore.Helpers;
|
||||
|
||||
namespace WebfrontCore.Controllers
|
||||
{
|
||||
@ -19,24 +19,33 @@ namespace WebfrontCore.Controllers
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Obsolete]
|
||||
public async Task<IActionResult> Login(int clientId, string password)
|
||||
{
|
||||
if (clientId == 0 || string.IsNullOrEmpty(password))
|
||||
{
|
||||
return Unauthorized("Invalid credentials");
|
||||
return Unauthorized(Localization["WEBFRONT_ACTION_LOGIN_ERROR"]);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var privilegedClient = await Manager.GetClientService().GetClientForLogin(clientId);
|
||||
bool loginSuccess = false;
|
||||
#if DEBUG
|
||||
loginSuccess = clientId == 1;
|
||||
#endif
|
||||
var loginSuccess = false;
|
||||
|
||||
if (Utilities.IsDevelopment)
|
||||
{
|
||||
loginSuccess = clientId == 1;
|
||||
}
|
||||
|
||||
if (!Authorized && !loginSuccess)
|
||||
{
|
||||
loginSuccess = Manager.TokenAuthenticator.AuthorizeToken(privilegedClient.NetworkId, password) ||
|
||||
(await Task.FromResult(SharedLibraryCore.Helpers.Hashing.Hash(password, privilegedClient.PasswordSalt)))[0] == privilegedClient.Password;
|
||||
loginSuccess = Manager.TokenAuthenticator.AuthorizeToken(new TokenIdentifier
|
||||
{
|
||||
ClientId = clientId,
|
||||
Token = password
|
||||
}) ||
|
||||
(await Task.FromResult(Hashing.Hash(password, privilegedClient.PasswordSalt)))[0] ==
|
||||
privilegedClient.Password;
|
||||
}
|
||||
|
||||
if (loginSuccess)
|
||||
@ -46,33 +55,34 @@ namespace WebfrontCore.Controllers
|
||||
new Claim(ClaimTypes.NameIdentifier, privilegedClient.Name),
|
||||
new Claim(ClaimTypes.Role, privilegedClient.Level.ToString()),
|
||||
new Claim(ClaimTypes.Sid, privilegedClient.ClientId.ToString()),
|
||||
new Claim(ClaimTypes.PrimarySid, privilegedClient.NetworkId.ToString("X"))
|
||||
new Claim(ClaimTypes.PrimarySid, privilegedClient.NetworkId.ToString("X")),
|
||||
new Claim(ClaimTypes.PrimaryGroupSid, privilegedClient.GameName.ToString())
|
||||
};
|
||||
|
||||
var claimsIdentity = new ClaimsIdentity(claims, "login");
|
||||
var claimsPrinciple = new ClaimsPrincipal(claimsIdentity);
|
||||
await SignInAsync(claimsPrinciple);
|
||||
|
||||
Manager.AddEvent(new GameEvent()
|
||||
Manager.AddEvent(new GameEvent
|
||||
{
|
||||
Origin = privilegedClient,
|
||||
Type = GameEvent.EventType.Login,
|
||||
Owner = Manager.GetServers().First(),
|
||||
Data = HttpContext.Request.Headers.ContainsKey("X-Forwarded-For")
|
||||
? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
|
||||
: HttpContext.Connection.RemoteIpAddress.ToString()
|
||||
: HttpContext.Connection.RemoteIpAddress?.ToString()
|
||||
});
|
||||
|
||||
return Ok($"Welcome {privilegedClient.Name}. You are now logged in");
|
||||
return Ok(Localization["WEBFRONT_ACTION_LOGIN_SUCCESS"].FormatExt(privilegedClient.CleanedName));
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception)
|
||||
{
|
||||
return Unauthorized("Could not validate credentials");
|
||||
return Unauthorized(Localization["WEBFRONT_ACTION_LOGIN_ERROR"]);
|
||||
}
|
||||
|
||||
return Unauthorized("Invalid credentials");
|
||||
return Unauthorized(Localization["WEBFRONT_ACTION_LOGIN_ERROR"]);
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -80,14 +90,14 @@ namespace WebfrontCore.Controllers
|
||||
{
|
||||
if (Authorized)
|
||||
{
|
||||
Manager.AddEvent(new GameEvent()
|
||||
Manager.AddEvent(new GameEvent
|
||||
{
|
||||
Origin = Client,
|
||||
Type = GameEvent.EventType.Logout,
|
||||
Owner = Manager.GetServers().First(),
|
||||
Data = HttpContext.Request.Headers.ContainsKey("X-Forwarded-For")
|
||||
? HttpContext.Request.Headers["X-Forwarded-For"].ToString()
|
||||
: HttpContext.Connection.RemoteIpAddress.ToString()
|
||||
: HttpContext.Connection.RemoteIpAddress?.ToString()
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
@ -147,7 +152,7 @@ namespace WebfrontCore.Controllers
|
||||
var info = new ActionInfo
|
||||
{
|
||||
ActionButtonLabel = Localization["WEBFRONT_ACTION_UNBAN_NAME"],
|
||||
Name = "Unban",
|
||||
Name = Localization["WEBFRONT_ACTION_UNBAN_NAME"],
|
||||
Inputs = new List<InputInfo>
|
||||
{
|
||||
new()
|
||||
@ -188,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()
|
||||
@ -213,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()
|
||||
@ -221,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()
|
||||
@ -270,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"]}",
|
||||
@ -282,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()
|
||||
@ -327,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);
|
||||
@ -357,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()
|
||||
@ -396,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()
|
||||
@ -428,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()
|
||||
@ -473,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>()))
|
||||
|
@ -56,7 +56,7 @@ namespace WebfrontCore.Controllers
|
||||
ViewBag.ClientIP = request.ClientIP;
|
||||
ViewBag.ClientGuid = request.ClientGuid;
|
||||
|
||||
ViewBag.Title = "Ban Management";
|
||||
ViewBag.Title = Localization["WEBFRONT_NAV_TITLE_BAN_MANAGEMENT"];
|
||||
|
||||
return View(results.Results);
|
||||
}
|
||||
|
@ -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,7 +88,7 @@ namespace WebfrontCore.Controllers
|
||||
var clientDto = new PlayerInfo
|
||||
{
|
||||
Name = client.Name,
|
||||
Game = client.GameName ?? Reference.Game.UKN,
|
||||
Game = client.GameName,
|
||||
Level = displayLevel,
|
||||
LevelInt = displayLevelInt,
|
||||
ClientId = client.ClientId,
|
||||
@ -146,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);
|
||||
@ -183,7 +179,7 @@ namespace WebfrontCore.Controllers
|
||||
ClientId = admin.ClientId,
|
||||
LastConnection = admin.LastConnection,
|
||||
IsMasked = admin.Masked,
|
||||
Game = admin.GameName ?? Reference.Game.UKN
|
||||
Game = admin.GameName
|
||||
});
|
||||
}
|
||||
|
||||
@ -214,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);
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,12 @@ namespace WebfrontCore.Controllers
|
||||
{
|
||||
ClientId = id,
|
||||
ServerEndpoint = serverId
|
||||
})).Results.First();
|
||||
}))?.Results?.First();
|
||||
|
||||
if (hitInfo is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var server = Manager.GetServers().FirstOrDefault(server => server.ToString() == serverId);
|
||||
long? matchedServerId = null;
|
||||
|
@ -61,6 +61,7 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
|
||||
}
|
||||
|
||||
ViewBag.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId, token);
|
||||
ViewBag.ServerId = matchedServerId;
|
||||
|
||||
return View("~/Views/Client/Statistics/Index.cshtml", _manager.GetServers()
|
||||
.Select(server => new ServerInfo
|
||||
@ -220,7 +221,7 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
|
||||
{
|
||||
return View("~/Views/Client/_MessageContext.cshtml", new List<MessageResponse>
|
||||
{
|
||||
new MessageResponse()
|
||||
new()
|
||||
{
|
||||
ClientId = penalty.OffenderId,
|
||||
Message = penalty.AutomatedOffense,
|
||||
|
@ -100,7 +100,7 @@ namespace WebfrontCore.Controllers
|
||||
new CommandResponseInfo
|
||||
{
|
||||
ClientId = client.ClientId,
|
||||
Response = Utilities.CurrentLocalization.LocalizationIndex["COMMADS_RESTART_SUCCESS"]
|
||||
Response = Utilities.CurrentLocalization.LocalizationIndex["COMMANDS_RESTART_SUCCESS"]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -74,21 +74,28 @@ namespace WebfrontCore.Controllers
|
||||
ViewBag.CommandPrefix = Manager.GetApplicationSettings().Configuration().CommandPrefix;
|
||||
|
||||
// we don't need to the name of the shared library assembly
|
||||
var excludedAssembly = typeof(BaseController).Assembly;
|
||||
var commands = Manager.GetCommands()
|
||||
.Where(_cmd => _cmd.Permission <= Client.Level)
|
||||
.OrderByDescending(_cmd => _cmd.Permission)
|
||||
.GroupBy(_cmd =>
|
||||
.Where(command => command.Permission <= Client.Level)
|
||||
.OrderByDescending(command => command.Permission)
|
||||
.GroupBy(command =>
|
||||
{
|
||||
// we need the plugin type the command is defined in
|
||||
var pluginType = _cmd.GetType().Assembly.GetTypes().FirstOrDefault(_type =>
|
||||
_type.Assembly != excludedAssembly && typeof(IPlugin).IsAssignableFrom(_type));
|
||||
return pluginType == null ? _translationLookup["WEBFRONT_HELP_COMMAND_NATIVE"] :
|
||||
pluginType.Name == "ScriptPlugin" ? _translationLookup["WEBFRONT_HELP_SCRIPT_PLUGIN"] :
|
||||
Manager.Plugins.FirstOrDefault(_plugin => _plugin.GetType().FullName == pluginType.FullName)?
|
||||
.Name; // for now we're just returning the name of the plugin, maybe later we'll include more info
|
||||
if (command.GetType().Name == "ScriptCommand")
|
||||
{
|
||||
return _translationLookup["WEBFRONT_HELP_SCRIPT_PLUGIN"];
|
||||
}
|
||||
|
||||
var assemblyName = command.GetType().Assembly.GetName().Name;
|
||||
if (assemblyName is "IW4MAdmin" or "SharedLibraryCore")
|
||||
{
|
||||
return _translationLookup["WEBFRONT_HELP_COMMAND_NATIVE"];
|
||||
}
|
||||
|
||||
var pluginType = command.GetType().Assembly.GetTypes()
|
||||
.FirstOrDefault(type => typeof(IPlugin).IsAssignableFrom(type));
|
||||
return Manager.Plugins.FirstOrDefault(plugin => plugin.GetType() == pluginType)?.Name ??
|
||||
_translationLookup["WEBFRONT_HELP_COMMAND_NATIVE"];
|
||||
})
|
||||
.Select(_grp => (_grp.Key, _grp.AsEnumerable()));
|
||||
.Select(group => (group.Key, group.AsEnumerable()));
|
||||
|
||||
return View(commands);
|
||||
}
|
||||
|
@ -36,24 +36,26 @@ namespace WebfrontCore.Middleware
|
||||
/// <param name="gameEvent"></param>
|
||||
private void OnGameEvent(object sender, GameEvent gameEvent)
|
||||
{
|
||||
if (gameEvent.Type == EventType.ChangePermission &&
|
||||
gameEvent.Extra is EFClient.Permission perm)
|
||||
if (gameEvent.Type != EventType.ChangePermission || gameEvent.Extra is not EFClient.Permission perm)
|
||||
{
|
||||
// we want to remove the claims when the client is demoted
|
||||
if (perm < EFClient.Permission.Trusted)
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_privilegedClientIds)
|
||||
{
|
||||
switch (perm)
|
||||
{
|
||||
lock (_privilegedClientIds)
|
||||
// we want to remove the claims when the client is demoted
|
||||
case < EFClient.Permission.Trusted:
|
||||
{
|
||||
_privilegedClientIds.RemoveAll(id => id == gameEvent.Target.ClientId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// and add if promoted
|
||||
else if (perm > EFClient.Permission.Trusted &&
|
||||
!_privilegedClientIds.Contains(gameEvent.Target.ClientId))
|
||||
{
|
||||
lock (_privilegedClientIds)
|
||||
// and add if promoted
|
||||
case > EFClient.Permission.Trusted when !_privilegedClientIds.Contains(gameEvent.Target.ClientId):
|
||||
{
|
||||
_privilegedClientIds.Add(gameEvent.Target.ClientId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -62,10 +64,16 @@ namespace WebfrontCore.Middleware
|
||||
public async Task Invoke(HttpContext context)
|
||||
{
|
||||
// we want to load the initial list of privileged clients
|
||||
if (_privilegedClientIds.Count == 0)
|
||||
bool hasAny;
|
||||
lock (_privilegedClientIds)
|
||||
{
|
||||
hasAny = _privilegedClientIds.Any();
|
||||
}
|
||||
|
||||
if (!hasAny)
|
||||
{
|
||||
var ids = (await _manager.GetClientService().GetPrivilegedClients())
|
||||
.Select(_client => _client.ClientId);
|
||||
.Select(client => client.ClientId);
|
||||
|
||||
lock (_privilegedClientIds)
|
||||
{
|
||||
@ -74,13 +82,19 @@ namespace WebfrontCore.Middleware
|
||||
}
|
||||
|
||||
// sid stores the clientId
|
||||
string claimsId = context.User.Claims.FirstOrDefault(_claim => _claim.Type == ClaimTypes.Sid)?.Value;
|
||||
var claimsId = context.User.Claims.FirstOrDefault(claim => claim.Type == ClaimTypes.Sid)?.Value;
|
||||
|
||||
if (!string.IsNullOrEmpty(claimsId))
|
||||
{
|
||||
int clientId = int.Parse(claimsId);
|
||||
var clientId = int.Parse(claimsId);
|
||||
bool hasKey;
|
||||
lock (_privilegedClientIds)
|
||||
{
|
||||
hasKey = _privilegedClientIds.Contains(clientId);
|
||||
}
|
||||
|
||||
// they've been removed
|
||||
if (!_privilegedClientIds.Contains(clientId) && clientId != 1)
|
||||
if (!hasKey && clientId != 1)
|
||||
{
|
||||
await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||
}
|
||||
|
@ -54,7 +54,8 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, B
|
||||
client.NetworkId,
|
||||
client.AliasLinkId,
|
||||
client.ClientId,
|
||||
client.CurrentAlias.IPAddress
|
||||
client.CurrentAlias.IPAddress,
|
||||
client.GameName
|
||||
}).ToListAsync();
|
||||
|
||||
var results = new List<BanInfo>();
|
||||
@ -85,7 +86,7 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, B
|
||||
OffenderName = penalty.Penalty.Offender.CurrentAlias.Name,
|
||||
Offense = string.IsNullOrEmpty(penalty.Penalty.AutomatedOffense)
|
||||
? penalty.Penalty.Offense
|
||||
: "Anticheat Detection",
|
||||
: Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_BAN_INFO_AC_DETECTION"],
|
||||
LinkId = penalty.Penalty.Offender.AliasLinkId,
|
||||
penalty.Penalty.OffenderId,
|
||||
penalty.Penalty.PunisherId,
|
||||
@ -101,7 +102,6 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, B
|
||||
.Select(alias => alias.LinkId)
|
||||
.ToListAsync()).Distinct();
|
||||
|
||||
|
||||
matchedPenalties = await context.Penalties.Where(penalty => penalty.Type == EFPenalty.PenaltyType.Ban)
|
||||
.Where(penalty => penalty.Expires == null || penalty.Expires > lateDateTime)
|
||||
.Where(penalty => penalty.LinkId != null && linkIds.Contains(penalty.LinkId.Value))
|
||||
@ -113,7 +113,7 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, B
|
||||
OffenderName = penalty.Offender.CurrentAlias.Name,
|
||||
Offense = string.IsNullOrEmpty(penalty.AutomatedOffense)
|
||||
? penalty.Offense
|
||||
: "Anticheat Detection",
|
||||
: Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_BAN_INFO_AC_DETECTION"],
|
||||
LinkId = penalty.Offender.AliasLinkId,
|
||||
penalty.OffenderId,
|
||||
penalty.PunisherId,
|
||||
@ -158,6 +158,7 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, B
|
||||
ClientId = matchingClient.ClientId,
|
||||
NetworkId = matchingClient.NetworkId,
|
||||
IPAddress = matchingClient.IPAddress,
|
||||
Game = matchingClient.GameName,
|
||||
|
||||
AssociatedPenalties = relatedEntities,
|
||||
AttachedPenalty = allPenalties.FirstOrDefault(penalty =>
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Data.Models;
|
||||
|
||||
namespace WebfrontCore.QueryHelpers.Models;
|
||||
|
||||
@ -9,6 +10,7 @@ public class BanInfo
|
||||
public int ClientId { get; set; }
|
||||
public int? IPAddress { get; set; }
|
||||
public long NetworkId { get; set; }
|
||||
public Reference.Game Game { get; set; }
|
||||
public PenaltyInfo AttachedPenalty { get; set; }
|
||||
public IEnumerable<PenaltyInfo> AssociatedPenalties { get; set; }
|
||||
}
|
||||
|
@ -35,19 +35,19 @@
|
||||
@foreach (var social in Model.CommunityInformation.SocialAccounts ?? Array.Empty<SocialAccountConfiguration>())
|
||||
{
|
||||
<div>
|
||||
<a href="@social.Url" target="_blank" title="@social.Title">
|
||||
<a href="@social.Url" target="_blank" title="@social.Title" class="d-flex no-decoration">
|
||||
@if (!string.IsNullOrWhiteSpace(social.IconId))
|
||||
{
|
||||
<span class="oi @social.IconId"></span>
|
||||
<i class="oi @social.IconId mr-5" style="width: 1.6rem;"></i>
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(social.IconUrl))
|
||||
{
|
||||
var url = Uri.TryCreate(social.IconUrl, UriKind.Absolute, out var parsedUrl)
|
||||
? parsedUrl.AbsoluteUri
|
||||
: $"images/community/{social.IconUrl}";
|
||||
<img class="img-fluid" style="max-width: 1rem; fill: white" src="@url" alt="@social.Title"/>
|
||||
<img class="img-fluid mr-5" style="width: 1.6rem; fill: white" src="@url" alt="@social.Title"/>
|
||||
}
|
||||
<span class="ml-1">@social.Title</span>
|
||||
<div class="ml-1">@social.Title</div>
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
Layout = null;
|
||||
}
|
||||
<h5 class="modal-title mb-10">@Model.Name.Titleize()</h5>
|
||||
@if (Model.Inputs.Any())
|
||||
@if (Model.Inputs.Any(input => input.Type != "hidden"))
|
||||
{
|
||||
<hr class="mb-10"/>
|
||||
}
|
||||
@ -55,12 +55,12 @@
|
||||
<input type="@inputType" name="@input.Name" value="@value" hidden="hidden">
|
||||
}
|
||||
}
|
||||
@if (Model.Inputs.Any())
|
||||
@if (Model.Inputs.Any(input => input.Type != "hidden"))
|
||||
{
|
||||
<hr class="mb-10"/>
|
||||
}
|
||||
<div class="ml-auto">
|
||||
<button type="submit" class="btn btn-primary">@Model.ActionButtonLabel</button>
|
||||
<a href="#" class="btn mr-5" role="button" onclick="halfmoon.toggleModal('actionModal');">Close</a>
|
||||
<a href="#" class="btn mr-5 ml-5" role="button" onclick="halfmoon.toggleModal('actionModal');">@ViewBag.Localization["WEBFRONT_ACTION_MODAL_BUTTON_CLOSE"]</a>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -4,12 +4,12 @@
|
||||
<h2 class="content-title mt-20 mb-10">@ViewBag.Title</h2>
|
||||
@if (!Model.Any())
|
||||
{
|
||||
<div class="text-muted mb-10">Search for records...</div>
|
||||
<div class="text-muted mb-10">@ViewBag.Localization["WEBFRONT_BAN_MGMT_SUBTITLE"]</div>
|
||||
}
|
||||
<form method="get" class="mt-10">
|
||||
<div class="d-flex flex-column flex-md-row">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientNameInput" name="clientName" value="@ViewBag.ClientName" placeholder="Client Name">
|
||||
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientNameInput" name="clientName" value="@ViewBag.ClientName" placeholder="@ViewBag.Localization["WEBFRONT_BAN_MGMT_FORM_NAME"]">
|
||||
<div class="input-group-append">
|
||||
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
|
||||
<i class="oi oi-magnifying-glass"></i>
|
||||
@ -18,7 +18,7 @@
|
||||
|
||||
</div>
|
||||
<div class="input-group mr-md-5 ml-md-10 mt-10 mb-5 mt-md-0 mb-md-0">
|
||||
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientGuidInput" name="clientGuid" value="@ViewBag.ClientGuid" placeholder="Client GUID">
|
||||
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientGuidInput" name="clientGuid" value="@ViewBag.ClientGuid" placeholder="@ViewBag.Localization["WEBFRONT_BAN_MGMT_FORM_GUID"]">
|
||||
<div class="input-group-append">
|
||||
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
|
||||
<i class="oi oi-magnifying-glass"></i>
|
||||
@ -26,7 +26,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group mr-md-10 ml-md-5 mb-10 mt-5 mt-md-0 mb-md-0">
|
||||
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientIPInput" name="clientIP" value="@ViewBag.ClientIP" placeholder="Client IP">
|
||||
<input type="text" class="form-control bg-dark-dm bg-light-ex-lm" id="clientIPInput" name="clientIP" value="@ViewBag.ClientIP" placeholder="@ViewBag.Localization["WEBFRONT_BAN_MGMT_FORM_IP"]">
|
||||
<div class="input-group-append">
|
||||
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
|
||||
<i class="oi oi-magnifying-glass"></i>
|
||||
@ -34,7 +34,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input type="number" class="form-control bg-dark-dm bg-light-ex-lm" id="clientIdInput" name="clientId" value="@ViewBag.ClientId" placeholder="Client Id">
|
||||
<input type="number" class="form-control bg-dark-dm bg-light-ex-lm" id="clientIdInput" name="clientId" value="@ViewBag.ClientId" placeholder="@ViewBag.Localization["WEBFRONT_BAN_MGMT_FORM_ID"]">
|
||||
<div class="input-group-append">
|
||||
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
|
||||
<i class="oi oi-magnifying-glass"></i>
|
||||
|
@ -10,27 +10,37 @@
|
||||
<div class="card p-10 m-0 mt-15 mb-15">
|
||||
<div class="d-flex flex-row flex-wrap">
|
||||
<div class="d-flex p-15 mr-md-10 w-full w-md-200 bg-very-dark-dm bg-light-ex-lm rounded">
|
||||
<div class="align-self-center ">
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@ban.ClientId" class="font-size-18 no-decoration">@ban.ClientName</a>
|
||||
<div class="align-self-center w-full">
|
||||
<div class="d-flex font-size-16">
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@ban.ClientId" class="no-decoration flex-fill text-force-break mr-5">
|
||||
<color-code value="@ban.ClientName"></color-code>
|
||||
</a>
|
||||
<div data-toggle="tooltip" data-title="@ViewBag.Localization[$"GAME_{ban.Game}"]">
|
||||
<div class="badge align-self-center">@Utilities.MakeAbbreviation(ViewBag.Localization[$"GAME_{ban.Game}"])</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<has-permission entity="ClientGuid" required-permission="Read">
|
||||
<div class="text-muted">@ban.NetworkId.ToString("X")</div>
|
||||
</has-permission>
|
||||
<has-permission entity="ClientIPAddress" required-permission="Read">
|
||||
<div class="text-muted">@ban.IPAddress.ConvertIPtoString()</div>
|
||||
</has-permission>
|
||||
<br/>
|
||||
@if (ban.AttachedPenalty is not null)
|
||||
{
|
||||
<br/>
|
||||
<div class="text-muted font-weight-light">@ban.AttachedPenalty.Offense.CapClientName(30)</div>
|
||||
<div class="text-muted font-weight-light">
|
||||
<color-code value="@ban.AttachedPenalty.Offense.CapClientName(30)"></color-code>
|
||||
</div>
|
||||
<div class="text-danger font-weight-light">@ban.AttachedPenalty.DateTime.ToStandardFormat()</div>
|
||||
<div class="btn profile-action mt-10 w-100" data-action="unban" data-action-id="@ban.ClientId">Unban</div>
|
||||
<br/>
|
||||
<div class="btn profile-action" ata-action="unban" data-action-id="@ban.ClientId">@ViewBag.Localization["WEBFRONT_BAN_MGMT_ACTION_UNBAN"]</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<br/>
|
||||
<div class="align-self-end text-muted font-weight-light">
|
||||
<span class="oi oi-warning font-size-12"></span>
|
||||
<span>Link-Only Ban</span>
|
||||
<span>@ViewBag.Localization["WEBFRONT_BAN_MGMT_LINK_ONLY"]</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@ -39,13 +49,22 @@
|
||||
|
||||
@foreach (var associatedEntity in ban.AssociatedPenalties)
|
||||
{
|
||||
<div class="d-flex flex-wrap flex-column w-full w-md-200 p-10">
|
||||
<div data-toggle="tooltip" data-title="Linked via shared IP" class="d-flex">
|
||||
<i class="oi oi-link-intact align-self-center"></i>
|
||||
<div class="text-truncate ml-5 mr-5">
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@associatedEntity.OffenderInfo.ClientId" class="font-size-18 no-decoration">@associatedEntity.OffenderInfo.ClientName</a>
|
||||
<div class="d-flex flex-wrap flex-column w-full w-md-200 p-10 border rounded mt-10 mt-md-0" style="border-style: dashed !important;">
|
||||
<div class="d-flex font-size-16">
|
||||
<div data-toggle="tooltip" data-title="@ViewBag.Localization["WEBFRONT_BAN_MGMT_TOOLTIP_LINKED"]" class="d-flex flex-fill">
|
||||
<i class="oi oi-link-intact align-self-center"></i>
|
||||
<div class="text-truncate ml-5 mr-5">
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@associatedEntity.OffenderInfo.ClientId" class="no-decoration text-force-break">
|
||||
<color-code value="@associatedEntity.OffenderInfo.ClientName"></color-code>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div data-toggle="tooltip" data-title="@ViewBag.Localization[$"GAME_{ban.Game}"]">
|
||||
<div class="badge">@Utilities.MakeAbbreviation(ViewBag.Localization[$"GAME_{ban.Game}"])</div>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<has-permission entity="ClientGuid" required-permission="Read">
|
||||
<div class="text-muted">@associatedEntity.OffenderInfo.NetworkId?.ToString("X")</div>
|
||||
</has-permission>
|
||||
@ -53,9 +72,11 @@
|
||||
<div class="text-muted">@associatedEntity.OffenderInfo.IPAddress.ConvertIPtoString()</div>
|
||||
</has-permission>
|
||||
<br/>
|
||||
<div class="text-muted font-weight-light">@associatedEntity.Offense.CapClientName(30)</div>
|
||||
<div class="text-muted font-weight-light">
|
||||
<color-code value="@associatedEntity.Offense.CapClientName(30)"></color-code>
|
||||
</div>
|
||||
<div class="text-danger font-weight-light">@associatedEntity.DateTime.ToStandardFormat()</div>
|
||||
<div class="btn profile-action mt-10 w-100" data-action="unban" data-action-id="@associatedEntity.OffenderInfo.ClientId">Unban</div>
|
||||
<div class="btn profile-action mt-10" data-action="unban" data-action-id="@associatedEntity.OffenderInfo.ClientId">@ViewBag.Localization["WEBFRONT_BAN_MGMT_ACTION_UNBAN"]</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
@ -30,11 +30,11 @@
|
||||
</td>
|
||||
<td>
|
||||
@info.Data
|
||||
<td >
|
||||
<td>
|
||||
@info.NewValue
|
||||
</td>
|
||||
<td class="text-right">
|
||||
@info.When.ToString()
|
||||
@info.When.ToStandardFormat()
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -63,9 +63,9 @@
|
||||
{
|
||||
<div class="mt-5 mb-5">–</div>
|
||||
}
|
||||
<div class="mt-5 mb-5"> @info.Data</div>
|
||||
<div class="mt-5 mb-5">@info.Data</div>
|
||||
<div class="mt-5 mb-5">@info.NewValue</div>
|
||||
<div class="mt-5 mb-5">@info.When.ToString()</div>
|
||||
<div class="mt-5 mb-5">@info.When</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
@ -5,8 +5,30 @@
|
||||
|
||||
<!-- desktop -->
|
||||
<div class="content mt-0">
|
||||
<h2 class="content-title mt-20 mb-0">Search Results</h2>
|
||||
<div class="text-muted mb-15"><span class="badge">@ViewBag.SearchTerm</span> returned <span class="text-primary">@ViewBag.ResultCount</span> matche(s)</div>
|
||||
<h2 class="content-title mt-20 mb-0">@loc["WEBFRONT_SEARCH_RESULTS_TITLE"]</h2>
|
||||
<div class="text-muted mb-15">
|
||||
@foreach (var match in Utilities.SplitTranslationTokens("WEBFRONT_SEARCH_RESULTS_SUBTITLE_FORMAT"))
|
||||
{
|
||||
if (match.IsInterpolation)
|
||||
{
|
||||
if (match.MatchValue == "searchTerm")
|
||||
{
|
||||
<span class="badge">
|
||||
@ViewBag.SearchTerm
|
||||
</span>
|
||||
}
|
||||
|
||||
else if (match.MatchValue == "searchCount")
|
||||
{
|
||||
<span class="text-primary">@ViewBag.ResultCount</span>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>@match.MatchValue</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<table class="table d-none d-md-table">
|
||||
<thead>
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
else
|
||||
{
|
||||
<h2 class="content-title mt-20 mb-0">Search Results</h2>
|
||||
<h2 class="content-title mt-20 mb-0">@ViewBag.Localization["WEBFRONT_SEARCH_RESULTS_TITLE"]</h2>
|
||||
<div class="text-muted mb-15">@Html.Raw(Utilities.FormatExt(ViewBag.Localization["WEBFRONT_STATS_MESSAGES_FOUND"], $"<span class=\"badge\">{Model.TotalResultCount.ToString("N0")}</span>"))</div>
|
||||
|
||||
<table class="table bg-dark-dm bg-light-lm rounded" style="table-layout: fixed">
|
||||
@ -27,7 +27,7 @@ else
|
||||
</table>
|
||||
|
||||
<div id="loaderLoad" class="mt-10 m-auto text-center d-none d-lg-block">
|
||||
<i class="loader-load-more oi oi-chevron-bottom "></i>
|
||||
<i class="loader-load-more oi oi-chevron-bottom"></i>
|
||||
</div>
|
||||
|
||||
@section scripts {
|
||||
|
@ -24,7 +24,7 @@
|
||||
<color-code value="@(message.ServerName ?? "--")"></color-code>
|
||||
</td>
|
||||
<td colspan="15%" class="text-right text-break">
|
||||
@message.When
|
||||
@message.When.ToStandardFormat()
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -53,8 +53,7 @@
|
||||
<div>
|
||||
<color-code value="@(message.ServerName ?? "--")"></color-code>
|
||||
</div>
|
||||
<div> @message.When</div>
|
||||
|
||||
<div>@message.When.ToStandardFormat()</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
|
@ -2,15 +2,14 @@
|
||||
<div class="content mt-0">
|
||||
<h4 class="content-title mt-20">@ViewBag.Title</h4>
|
||||
|
||||
|
||||
@foreach (var key in Model.Keys)
|
||||
{
|
||||
<table class="table mb-20" style="table-layout:fixed;">
|
||||
<thead>
|
||||
<tr class="level-bgcolor-@((int)key)">
|
||||
<th class="text-light">@key.ToLocalizedLevelName()</th>
|
||||
<th>Game</th>
|
||||
<th class="text-right font-weight-bold">Last Connected</th>
|
||||
<th colspan="50%" class="text-light">@key.ToLocalizedLevelName()</th>
|
||||
<th colspan="20%">@ViewBag.Localization["WEBFRONT_CONTEXT_MENU_GLOBAL_GAME"]</th>
|
||||
<th colspan="30%" class="text-right font-weight-bold text-force-break">@ViewBag.Localization["WEBFRONT_SEARCH_LAST_CONNECTED"]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -22,22 +21,25 @@
|
||||
continue;
|
||||
}
|
||||
<tr class="bg-dark-dm bg-light-lm">
|
||||
<td>
|
||||
<td colspan="50%">
|
||||
@if (client.IsMasked)
|
||||
{
|
||||
<span data-toggle="tooltip" data-title="Client is masked">
|
||||
<span data-toggle="tooltip" data-title="@ViewBag.Localization["WEBFRONT_PRIVILEGED_TOOLTIP_MASKED"]">
|
||||
<span class="oi oi-shield mr-5 font-size-12"></span>
|
||||
</span>
|
||||
}
|
||||
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@client.ClientId">
|
||||
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@client.ClientId" class="text-force-break">
|
||||
<color-code value="@client.Name"></color-code>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<td colspan="20%" class="d-none d-md-table-cell">
|
||||
<div class="badge">@ViewBag.Localization[$"GAME_{client.Game}"]</div>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<td colspan="20%" class="d-table-cell d-md-none">
|
||||
<div class="badge">@(Utilities.MakeAbbreviation(ViewBag.Localization[$"GAME_{client.Game}"] as string))</div>
|
||||
</td>
|
||||
<td colspan="30%" class="text-right">
|
||||
@client.LastConnection.HumanizeForCurrentCulture()
|
||||
</td>
|
||||
</tr>
|
||||
@ -46,5 +48,4 @@
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
@ -32,7 +32,7 @@
|
||||
}
|
||||
|
||||
<div class="content row mt-20">
|
||||
<div class="col-12 col-lg-9 col-xl-10">
|
||||
<div class="col-12 col-lg-9">
|
||||
@if (Model.ActivePenalty != null)
|
||||
{
|
||||
<has-permission entity="ClientLevel" required-permission="Read">
|
||||
@ -58,7 +58,7 @@
|
||||
</has-permission>
|
||||
}
|
||||
|
||||
<h2 class="content-title mb-0">Player Profile</h2>
|
||||
<h2 class="content-title mb-0">@ViewBag.Localization["WEBFRONT_PROFILE_TITLE"]</h2>
|
||||
<div class="font-size-12 text-muted">@ViewBag.Localization[$"GAME_{Model.Game}"]</div>
|
||||
|
||||
<div id="profile_wrapper" class="mb-10 mt-10">
|
||||
@ -66,7 +66,7 @@
|
||||
<!-- online status indicator -->
|
||||
@if (Model.Online)
|
||||
{
|
||||
<div class="bg-success rounded-circle position-absolute status-indicator z-20 mt-10 ml-10" data-toggle="tooltip" data-placement="bottom" data-title="Client is online"></div>
|
||||
<div class="bg-success rounded-circle position-absolute status-indicator z-20 mt-10 ml-10" data-toggle="tooltip" data-placement="bottom" data-title="@ViewBag.Localization["WEBFRONT_PROFILE_TOOLTIP_ONLINE"]"></div>
|
||||
<div class="bg-success rounded-circle position-absolute status-indicator with-ripple z-10 mt-10 ml-10"></div>
|
||||
}
|
||||
|
||||
@ -82,7 +82,7 @@
|
||||
<div class="d-flex flex-column align-self-center ml-20 mr-20 mt-10 mb-10 mt-md-0 mb-md-0 text-center text-md-left">
|
||||
<!-- name -->
|
||||
<div id="profile_name">
|
||||
<span class="font-size-20 font-weight-medium">
|
||||
<span class="font-size-20 font-weight-medium text-force-break">
|
||||
<color-code value="@Model.Name"></color-code>
|
||||
</span>
|
||||
<has-permission entity="MetaAliasUpdate" required-permission="Read">
|
||||
@ -105,7 +105,7 @@
|
||||
@if (Model.Aliases.Count > 15)
|
||||
{
|
||||
<div class="dropdown-divider"></div>
|
||||
<span class="dropdown-item bg-dark-dm bg-light-lm">...and @(Model.Aliases.Count - 15) more</span>
|
||||
<span class="dropdown-item bg-dark-dm bg-light-lm">@((ViewBag.Localization["WEBFRONT_PROFILE_ALIAS_COUNT_MORE_FORMAT"] as string).FormatExt(Model.Aliases.Count - 15))</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@ -115,7 +115,7 @@
|
||||
<has-permission entity="ClientLevel" required-permission="Read">
|
||||
<div class="align-self-center align-self-md-start font-weight-bold font-size-16 level-color-@Model.LevelInt">
|
||||
<div class="d-flex flex-row">
|
||||
<span>@Model.Level</span>
|
||||
<color-code value="@Model.Level"></color-code>
|
||||
</div>
|
||||
</div>
|
||||
</has-permission>
|
||||
@ -126,7 +126,7 @@
|
||||
<div class="text-muted" data-toggle="dropdown" id="altGuidFormatsDropdown" aria-haspopup="true" aria-expanded="false">@Model.NetworkId.ToString("X")</div>
|
||||
<div class="dropdown-menu" aria-labelledby="altGuidFormatsDropdown">
|
||||
<div class="p-10 font-size-12">
|
||||
<div class="">Alternative Formats</div>
|
||||
<div class="">@ViewBag.Localization["WEBFRONT_PROFILE_POPOVER_ALTERNATIVE_GUID"]</div>
|
||||
<div class="dropdown-divider mt-5 mb-5"></div>
|
||||
<div class="text-muted font-weight-lighter">@((ulong)Model.NetworkId)</div>
|
||||
</div>
|
||||
@ -164,7 +164,7 @@
|
||||
@if (Model.IPs.Count > 15)
|
||||
{
|
||||
<div class="dropdown-divider"></div>
|
||||
<span class="dropdown-item bg-dark-dm bg-light-lm">...and @(Model.IPs.Count - 15) more</span>
|
||||
<span class="dropdown-item bg-dark-dm bg-light-lm">@((ViewBag.Localization["WEBFRONT_PROFILE_ALIAS_COUNT_MORE_FORMAT"] as string).FormatExt(Model.IPs.Count - 15))</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@ -252,18 +252,18 @@
|
||||
@{
|
||||
var menuItems = new SideContextMenuItems
|
||||
{
|
||||
MenuTitle = "Actions",
|
||||
MenuTitle = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_TITLE"]
|
||||
};
|
||||
|
||||
if (Model.Online)
|
||||
{
|
||||
menuItems.Items.Add(new SideContextMenuItem
|
||||
{
|
||||
Title = "Join Game",
|
||||
Title = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_JOIN"],
|
||||
IsLink = true,
|
||||
IsButton = true,
|
||||
Reference = Model.ConnectProtocolUrl,
|
||||
Tooltip = $"Playing on {Model.CurrentServerName.StripColors()}",
|
||||
Tooltip = (ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_TOOLTIP_JOIN"] as string).FormatExt(Model.CurrentServerName.StripColors()),
|
||||
Icon = "oi-play-circle"
|
||||
});
|
||||
}
|
||||
@ -272,17 +272,29 @@
|
||||
{
|
||||
menuItems.Items.Add(new SideContextMenuItem
|
||||
{
|
||||
Title = "Change Level",
|
||||
Title = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_LEVEL"],
|
||||
IsButton = true,
|
||||
Reference = "edit",
|
||||
Icon = "oi-cog",
|
||||
EntityId = Model.ClientId
|
||||
});
|
||||
}
|
||||
|
||||
if (ViewBag.Authorized)
|
||||
{
|
||||
menuItems.Items.Add(new SideContextMenuItem
|
||||
{
|
||||
Title = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MESSAGE"],
|
||||
IsButton = true,
|
||||
Reference = "OfflineMessage",
|
||||
Icon = "oi oi-envelope-closed",
|
||||
EntityId = Model.ClientId
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.Items.Add(new SideContextMenuItem
|
||||
{
|
||||
Title = "View Stats",
|
||||
Title = ViewBag.Localization["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_STATS"],
|
||||
IsButton = true,
|
||||
IsLink = true,
|
||||
Reference = Url.Action("Advanced", "ClientStatistics", new { id = Model.ClientId }),
|
||||
@ -293,7 +305,7 @@
|
||||
{
|
||||
menuItems.Items.Add(new SideContextMenuItem
|
||||
{
|
||||
Title = isFlagged ? "Unflag" : "Flag",
|
||||
Title = isFlagged ? ViewBag.Localization["WEBFRONT_ACTION_UNFLAG_NAME"] : ViewBag.Localization["WEBFRONT_ACTION_FLAG_NAME"],
|
||||
IsButton = true,
|
||||
Reference = isFlagged ? "unflag" : "flag",
|
||||
Icon = "oi-flag",
|
||||
@ -305,7 +317,7 @@
|
||||
{
|
||||
menuItems.Items.Add(new SideContextMenuItem
|
||||
{
|
||||
Title = "Kick",
|
||||
Title = ViewBag.Localization["WEBFRONT_ACTION_KICK_NAME"],
|
||||
IsButton = true,
|
||||
Reference = "kick",
|
||||
Icon = "oi-circle-x",
|
||||
@ -317,7 +329,7 @@
|
||||
{
|
||||
menuItems.Items.Add(new SideContextMenuItem
|
||||
{
|
||||
Title = "Ban",
|
||||
Title = ViewBag.Localization["WEBFRONT_ACTION_BAN_NAME"],
|
||||
IsButton = true,
|
||||
Reference = "ban",
|
||||
Icon = "oi-lock-unlocked",
|
||||
@ -329,13 +341,14 @@
|
||||
{
|
||||
menuItems.Items.Add(new SideContextMenuItem
|
||||
{
|
||||
Title = "Unban",
|
||||
Title = ViewBag.Localization["WEBFRONT_ACTION_UNBAN_NAME"],
|
||||
IsButton = true,
|
||||
Reference = "unban",
|
||||
Icon = "oi-lock-locked",
|
||||
EntityId = Model.ClientId
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
<partial name="_SideContextMenu" for="@menuItems"></partial>
|
||||
|
||||
|
@ -41,7 +41,6 @@
|
||||
{
|
||||
<color-code value="@Model.Offense"></color-code>
|
||||
}
|
||||
|
||||
</span>
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user