Compare commits
20 Commits
2022.06.02
...
2022.06.12
Author | SHA1 | Date | |
---|---|---|---|
ef3db63ba7 | |||
49fe4520ff | |||
6587187a34 | |||
b337e232a2 | |||
a44b4e9475 | |||
ffb0e5cac1 | |||
ecc2b5bf54 | |||
2ac9cc4379 | |||
215037095f | |||
5433d7d1d2 | |||
0446fe1ec5 | |||
cf2a00e5b3 | |||
ab494a22cb | |||
b690579154 | |||
acc967e50a | |||
c493fbe13d | |||
ee56a5db1f | |||
f235d0fafd | |||
7ecf516278 | |||
210f1ca336 |
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,6 +57,7 @@ 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);
|
||||
@ -82,13 +83,14 @@ 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();
|
||||
@ -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))
|
||||
@ -697,5 +700,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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
@ -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))
|
||||
@ -790,12 +808,10 @@ namespace IW4MAdmin
|
||||
/// array index 2 = updated clients
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
async Task<List<EFClient>[]> PollPlayersAsync()
|
||||
async Task<List<EFClient>[]> PollPlayersAsync(CancellationToken token)
|
||||
{
|
||||
var tokenSource = new CancellationTokenSource();
|
||||
tokenSource.CancelAfter(TimeSpan.FromSeconds(5));
|
||||
var currentClients = GetClientsAsList();
|
||||
var statusResponse = await this.GetStatusAsync(tokenSource.Token);
|
||||
var statusResponse = await this.GetStatusAsync(token);
|
||||
|
||||
if (statusResponse is null)
|
||||
{
|
||||
@ -918,11 +934,11 @@ namespace IW4MAdmin
|
||||
private DateTime _lastMessageSent = DateTime.Now;
|
||||
private DateTime _lastPlayerCount = DateTime.Now;
|
||||
|
||||
public override async Task<bool> ProcessUpdatesAsync(CancellationToken cts)
|
||||
public override async Task<bool> ProcessUpdatesAsync(CancellationToken token)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (cts.IsCancellationRequested)
|
||||
if (token.IsCancellationRequested)
|
||||
{
|
||||
await ShutdownInternal();
|
||||
return true;
|
||||
@ -936,7 +952,7 @@ namespace IW4MAdmin
|
||||
return true;
|
||||
}
|
||||
|
||||
var polledClients = await PollPlayersAsync();
|
||||
var polledClients = await PollPlayersAsync(token);
|
||||
|
||||
if (polledClients is null)
|
||||
{
|
||||
@ -947,7 +963,7 @@ namespace IW4MAdmin
|
||||
.Where(client => !client.IsZombieClient /* ignores "fake" zombie clients */))
|
||||
{
|
||||
disconnectingClient.CurrentServer = this;
|
||||
var e = new GameEvent()
|
||||
var e = new GameEvent
|
||||
{
|
||||
Type = GameEvent.EventType.PreDisconnect,
|
||||
Origin = disconnectingClient,
|
||||
|
@ -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);
|
||||
|
@ -5,6 +5,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
using Data.Models.Client;
|
||||
using Data.Models.Client.Stats;
|
||||
using Data.Models.Server;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -22,18 +23,20 @@ namespace IW4MAdmin.Application.Misc
|
||||
private readonly IDataValueCache<EFServerSnapshot, (int?, DateTime?)> _snapshotCache;
|
||||
private readonly IDataValueCache<EFClient, (int, int)> _serverStatsCache;
|
||||
private readonly IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> _clientHistoryCache;
|
||||
private readonly IDataValueCache<EFClientRankingHistory, int> _rankedClientsCache;
|
||||
|
||||
private readonly TimeSpan? _cacheTimeSpan =
|
||||
Utilities.IsDevelopment ? TimeSpan.FromSeconds(30) : (TimeSpan?) TimeSpan.FromMinutes(10);
|
||||
|
||||
public ServerDataViewer(ILogger<ServerDataViewer> logger, IDataValueCache<EFServerSnapshot, (int?, DateTime?)> snapshotCache,
|
||||
IDataValueCache<EFClient, (int, int)> serverStatsCache,
|
||||
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache)
|
||||
IDataValueCache<EFServerSnapshot, List<ClientHistoryInfo>> clientHistoryCache, IDataValueCache<EFClientRankingHistory, int> rankedClientsCache)
|
||||
{
|
||||
_logger = logger;
|
||||
_snapshotCache = snapshotCache;
|
||||
_serverStatsCache = serverStatsCache;
|
||||
_clientHistoryCache = clientHistoryCache;
|
||||
_rankedClientsCache = rankedClientsCache;
|
||||
}
|
||||
|
||||
public async Task<(int?, DateTime?)>
|
||||
@ -160,5 +163,30 @@ namespace IW4MAdmin.Application.Misc
|
||||
return Enumerable.Empty<ClientHistoryInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<int> RankedClientsCountAsync(long? serverId = null, CancellationToken token = default)
|
||||
{
|
||||
_rankedClientsCache.SetCacheItem(async (set, cancellationToken) =>
|
||||
{
|
||||
var fifteenDaysAgo = DateTime.UtcNow.AddDays(-15);
|
||||
return await set
|
||||
.Where(rating => rating.Newest)
|
||||
.Where(rating => rating.ServerId == serverId)
|
||||
.Where(rating => rating.CreatedDateTime >= fifteenDaysAgo)
|
||||
.Where(rating => rating.Client.Level != EFClient.Permission.Banned)
|
||||
.Where(rating => rating.Ranking != null)
|
||||
.CountAsync(cancellationToken);
|
||||
}, nameof(_rankedClientsCache), serverId is null ? null: new[] { (object)serverId }, _cacheTimeSpan);
|
||||
|
||||
try
|
||||
{
|
||||
return await _rankedClientsCache.GetCacheItem(nameof(_rankedClientsCache), serverId, token);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not retrieve data for {Name}", nameof(RankedClientsCountAsync));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
@ -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]];
|
||||
|
@ -46,7 +46,7 @@ namespace IW4MAdmin.Application.RConParsers
|
||||
{ColorCodes.White.ToString(), "^7"},
|
||||
{ColorCodes.Map.ToString(), "^8"},
|
||||
{ColorCodes.Grey.ToString(), "^9"},
|
||||
{ColorCodes.Wildcard.ToString(), ":^"},
|
||||
{ColorCodes.Wildcard.ToString(), "^:"}
|
||||
};
|
||||
|
||||
public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory)
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
@ -9,6 +10,11 @@ namespace Data.Abstractions
|
||||
{
|
||||
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
|
||||
TimeSpan? expirationTime = null, bool autoRefresh = false);
|
||||
|
||||
void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> itemGetter, string keyName,
|
||||
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false);
|
||||
|
||||
Task<TReturnType> GetCacheItem(string keyName, CancellationToken token = default);
|
||||
Task<TReturnType> GetCacheItem(string keyName, object id = null, CancellationToken token = default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
@ -15,8 +17,8 @@ namespace Data.Helpers
|
||||
private readonly ILogger _logger;
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
|
||||
private readonly ConcurrentDictionary<string, CacheState<TReturnType>> _cacheStates =
|
||||
new ConcurrentDictionary<string, CacheState<TReturnType>>();
|
||||
private readonly ConcurrentDictionary<string, Dictionary<object, CacheState<TReturnType>>> _cacheStates = new();
|
||||
private readonly object _defaultKey = new();
|
||||
|
||||
private bool _autoRefresh;
|
||||
private const int DefaultExpireMinutes = 15;
|
||||
@ -51,41 +53,61 @@ namespace Data.Helpers
|
||||
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
|
||||
TimeSpan? expirationTime = null, bool autoRefresh = false)
|
||||
{
|
||||
if (_cacheStates.ContainsKey(key))
|
||||
{
|
||||
_logger.LogDebug("Cache key {Key} is already added", key);
|
||||
return;
|
||||
}
|
||||
|
||||
var state = new CacheState<TReturnType>
|
||||
{
|
||||
Key = key,
|
||||
Getter = getter,
|
||||
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
|
||||
};
|
||||
|
||||
_autoRefresh = autoRefresh;
|
||||
|
||||
_cacheStates.TryAdd(key, state);
|
||||
|
||||
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_timer = new Timer(state.ExpirationTime.TotalMilliseconds);
|
||||
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None);
|
||||
_timer.Start();
|
||||
SetCacheItem(getter, key, null, expirationTime, autoRefresh);
|
||||
}
|
||||
|
||||
public async Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default)
|
||||
public void SetCacheItem(Func<DbSet<TEntityType>, CancellationToken, Task<TReturnType>> getter, string key,
|
||||
IEnumerable<object> ids = null, TimeSpan? expirationTime = null, bool autoRefresh = false)
|
||||
{
|
||||
ids ??= new[] { _defaultKey };
|
||||
|
||||
if (!_cacheStates.ContainsKey(key))
|
||||
{
|
||||
_cacheStates.TryAdd(key, new Dictionary<object, CacheState<TReturnType>>());
|
||||
}
|
||||
|
||||
foreach (var id in ids)
|
||||
{
|
||||
if (_cacheStates[key].ContainsKey(id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var state = new CacheState<TReturnType>
|
||||
{
|
||||
Key = key,
|
||||
Getter = getter,
|
||||
ExpirationTime = expirationTime ?? TimeSpan.FromMinutes(DefaultExpireMinutes)
|
||||
};
|
||||
|
||||
_cacheStates[key].Add(id, state);
|
||||
|
||||
_autoRefresh = autoRefresh;
|
||||
|
||||
|
||||
if (!_autoRefresh || expirationTime == TimeSpan.MaxValue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_timer = new Timer(state.ExpirationTime.TotalMilliseconds);
|
||||
_timer.Elapsed += async (sender, args) => await RunCacheUpdate(state, CancellationToken.None);
|
||||
_timer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<TReturnType> GetCacheItem(string keyName, CancellationToken cancellationToken = default) =>
|
||||
await GetCacheItem(keyName, null, cancellationToken);
|
||||
|
||||
public async Task<TReturnType> GetCacheItem(string keyName, object id = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!_cacheStates.ContainsKey(keyName))
|
||||
{
|
||||
throw new ArgumentException("No cache found for key {key}", keyName);
|
||||
}
|
||||
|
||||
var state = _cacheStates[keyName];
|
||||
var state = id is null ? _cacheStates[keyName].Values.First() : _cacheStates[keyName][id];
|
||||
|
||||
// when auto refresh is off we want to check the expiration and value
|
||||
// when auto refresh is on, we want to only check the value, because it'll be refreshed automatically
|
||||
@ -115,4 +137,4 @@ namespace Data.Helpers
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
1638
Data/Migrations/MySql/20220609135128_AddIndexToEFRankingHistoryCreatedDatetime.Designer.cs
generated
Normal file
1638
Data/Migrations/MySql/20220609135128_AddIndexToEFRankingHistoryCreatedDatetime.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.MySql
|
||||
{
|
||||
public partial class AddIndexToEFRankingHistoryCreatedDatetime : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EFClientRankingHistory_CreatedDateTime",
|
||||
table: "EFClientRankingHistory",
|
||||
column: "CreatedDateTime");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_EFClientRankingHistory_CreatedDateTime",
|
||||
table: "EFClientRankingHistory");
|
||||
}
|
||||
}
|
||||
}
|
@ -456,6 +456,8 @@ namespace Data.Migrations.MySql
|
||||
|
||||
b.HasIndex("ClientId");
|
||||
|
||||
b.HasIndex("CreatedDateTime");
|
||||
|
||||
b.HasIndex("Ranking");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
1695
Data/Migrations/Postgresql/20220609135210_AddIndexToEFRankingHistoryCreatedDatetime.Designer.cs
generated
Normal file
1695
Data/Migrations/Postgresql/20220609135210_AddIndexToEFRankingHistoryCreatedDatetime.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 AddIndexToEFRankingHistoryCreatedDatetime : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EFClientRankingHistory_CreatedDateTime",
|
||||
table: "EFClientRankingHistory",
|
||||
column: "CreatedDateTime");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_EFClientRankingHistory_CreatedDateTime",
|
||||
table: "EFClientRankingHistory");
|
||||
}
|
||||
}
|
||||
}
|
@ -475,6 +475,8 @@ namespace Data.Migrations.Postgresql
|
||||
|
||||
b.HasIndex("ClientId");
|
||||
|
||||
b.HasIndex("CreatedDateTime");
|
||||
|
||||
b.HasIndex("Ranking");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
1636
Data/Migrations/Sqlite/20220609022511_AddIndexToEFRankingHistoryCreatedDatetime.Designer.cs
generated
Normal file
1636
Data/Migrations/Sqlite/20220609022511_AddIndexToEFRankingHistoryCreatedDatetime.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.Sqlite
|
||||
{
|
||||
public partial class AddIndexToEFRankingHistoryCreatedDatetime : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_EFClientRankingHistory_CreatedDateTime",
|
||||
table: "EFClientRankingHistory",
|
||||
column: "CreatedDateTime");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_EFClientRankingHistory_CreatedDateTime",
|
||||
table: "EFClientRankingHistory");
|
||||
}
|
||||
}
|
||||
}
|
@ -454,6 +454,8 @@ namespace Data.Migrations.Sqlite
|
||||
|
||||
b.HasIndex("ClientId");
|
||||
|
||||
b.HasIndex("CreatedDateTime");
|
||||
|
||||
b.HasIndex("Ranking");
|
||||
|
||||
b.HasIndex("ServerId");
|
||||
|
@ -86,7 +86,8 @@ namespace Data.Models.Configuration
|
||||
entity.HasIndex(ranking => ranking.Ranking);
|
||||
entity.HasIndex(ranking => ranking.ZScore);
|
||||
entity.HasIndex(ranking => ranking.UpdatedDateTime);
|
||||
entity.HasIndex(ranking => ranking.CreatedDateTime);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,8 @@
|
||||
T6 = 7,
|
||||
T7 = 8,
|
||||
SHG1 = 9,
|
||||
CSGO = 10
|
||||
CSGO = 10,
|
||||
H1 = 11
|
||||
}
|
||||
|
||||
public enum ConnectionType
|
||||
@ -24,4 +25,4 @@
|
||||
Disconnect
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +54,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ScriptPlugins", "ScriptPlug
|
||||
Plugins\ScriptPlugins\SubnetBan.js = Plugins\ScriptPlugins\SubnetBan.js
|
||||
Plugins\ScriptPlugins\BanBroadcasting.js = Plugins\ScriptPlugins\BanBroadcasting.js
|
||||
Plugins\ScriptPlugins\ParserH1MOD.js = Plugins\ScriptPlugins\ParserH1MOD.js
|
||||
Plugins\ScriptPlugins\ParserPlutoniumT5.js = Plugins\ScriptPlugins\ParserPlutoniumT5.js
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutomessageFeed", "Plugins\AutomessageFeed\AutomessageFeed.csproj", "{F5815359-CFC7-44B4-9A3B-C04BACAD5836}"
|
||||
|
@ -368,7 +368,9 @@ namespace Integrations.Cod
|
||||
throw new RConException("Unexpected response header from server");
|
||||
}
|
||||
|
||||
var splitResponse = headerSplit.Last().Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var splitResponse = headerSplit.Last().Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(line => line.StartsWith("^7") ? line[2..] : line).ToArray();
|
||||
|
||||
return splitResponse;
|
||||
}
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -16,7 +16,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -19,7 +19,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -16,7 +16,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
39
Plugins/ScriptPlugins/ParserPlutoniumT5.js
Normal file
39
Plugins/ScriptPlugins/ParserPlutoniumT5.js
Normal file
@ -0,0 +1,39 @@
|
||||
var rconParser;
|
||||
var eventParser;
|
||||
|
||||
var plugin = {
|
||||
author: 'RaidMax',
|
||||
version: 0.1,
|
||||
name: 'Plutonium T5 Parser',
|
||||
isParser: true,
|
||||
|
||||
onEventAsync: function (gameEvent, server) {
|
||||
},
|
||||
|
||||
onLoadAsync: function (manager) {
|
||||
rconParser = manager.GenerateDynamicRConParser(this.name);
|
||||
eventParser = manager.GenerateDynamicEventParser(this.name);
|
||||
|
||||
rconParser.Configuration.DefaultInstallationDirectoryHint = '{LocalAppData}/Plutonium/storage/t5';
|
||||
rconParser.Configuration.CommandPrefixes.RConResponse = '\xff\xff\xff\xffprint\n';
|
||||
rconParser.Configuration.Dvar.Pattern = '^(?:\\^7)?\\"(.+)\\" is: \\"(.+)?\\" default: \\"(.+)?\\"\\n?(?:latched: \\"(.+)?\\"\\n)?\\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.Version = 'Call of Duty Multiplayer - Ship COD_T5_S MP build 7.0.189 CL(1022875) CODPCAB-V64 CEG Wed Nov 02 18:02:23 2011 win-x86';
|
||||
rconParser.GameName = 6; // T5
|
||||
eventParser.Version = 'Call of Duty Multiplayer - Ship COD_T5_S MP build 7.0.189 CL(1022875) CODPCAB-V64 CEG Wed Nov 02 18:02:23 2011 win-x86';
|
||||
eventParser.GameName = 6; // T5
|
||||
eventParser.Configuration.GuidNumberStyle = 7; // Integer
|
||||
|
||||
},
|
||||
|
||||
onUnloadAsync: function () {
|
||||
},
|
||||
|
||||
onTickAsync: function (server) {
|
||||
}
|
||||
};
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
using Data.Models.Client;
|
||||
@ -88,8 +89,8 @@ namespace Stats.Client
|
||||
return zScore ?? 0;
|
||||
}, MaxZScoreCacheKey, Utilities.IsDevelopment ? TimeSpan.FromMinutes(5) : TimeSpan.FromMinutes(30));
|
||||
|
||||
await _distributionCache.GetCacheItem(DistributionCacheKey);
|
||||
await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey);
|
||||
await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken());
|
||||
await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new CancellationToken());
|
||||
|
||||
/*foreach (var serverId in _serverIds)
|
||||
{
|
||||
@ -132,7 +133,7 @@ namespace Stats.Client
|
||||
|
||||
public async Task<double> GetZScoreForServer(long serverId, double value)
|
||||
{
|
||||
var serverParams = await _distributionCache.GetCacheItem(DistributionCacheKey);
|
||||
var serverParams = await _distributionCache.GetCacheItem(DistributionCacheKey, new CancellationToken());
|
||||
if (!serverParams.ContainsKey(serverId))
|
||||
{
|
||||
return 0.0;
|
||||
@ -150,7 +151,7 @@ namespace Stats.Client
|
||||
|
||||
public async Task<double?> GetRatingForZScore(double? value)
|
||||
{
|
||||
var maxZScore = await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey);
|
||||
var maxZScore = await _maxZScoreCache.GetCacheItem(MaxZScoreCacheKey, new CancellationToken());
|
||||
return maxZScore == 0 ? null : value.GetRatingForZScore(maxZScore);
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ namespace IW4MAdmin.Plugins.Stats.Commands
|
||||
}
|
||||
else
|
||||
{
|
||||
gameEvent.Owner.Broadcast(topStats);
|
||||
await gameEvent.Owner.BroadcastAsync(topStats);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ namespace Stats.Dtos
|
||||
public EFClient.Permission Level { get; set; }
|
||||
public double? Performance { get; set; }
|
||||
public int? Ranking { get; set; }
|
||||
public int TotalRankedClients { get; set; }
|
||||
public double? ZScore { get; set; }
|
||||
public double? Rating { get; set; }
|
||||
public List<ServerInfo> Servers { get; set; }
|
||||
@ -25,4 +26,4 @@ namespace Stats.Dtos
|
||||
public List<EFClientRankingHistory> Ratings { get; set; }
|
||||
public List<EFClientStatistics> LegacyStats { get; set; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -42,10 +42,11 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
private readonly ILogger<Plugin> _logger;
|
||||
private readonly List<IClientStatisticCalculator> _statCalculators;
|
||||
private readonly IServerDistributionCalculator _serverDistributionCalculator;
|
||||
private readonly IServerDataViewer _serverDataViewer;
|
||||
|
||||
public Plugin(ILogger<Plugin> logger, IConfigurationHandlerFactory configurationHandlerFactory, IDatabaseContextFactory databaseContextFactory,
|
||||
ITranslationLookup translationLookup, IMetaServiceV2 metaService, IResourceQueryHelper<ChatSearchQuery, MessageResponse> chatQueryHelper, ILogger<StatManager> managerLogger,
|
||||
IEnumerable<IClientStatisticCalculator> statCalculators, IServerDistributionCalculator serverDistributionCalculator)
|
||||
IEnumerable<IClientStatisticCalculator> statCalculators, IServerDistributionCalculator serverDistributionCalculator, IServerDataViewer serverDataViewer)
|
||||
{
|
||||
Config = configurationHandlerFactory.GetConfigurationHandler<StatsConfiguration>("StatsPluginSettings");
|
||||
_databaseContextFactory = databaseContextFactory;
|
||||
@ -56,6 +57,7 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
_logger = logger;
|
||||
_statCalculators = statCalculators.ToList();
|
||||
_serverDistributionCalculator = serverDistributionCalculator;
|
||||
_serverDataViewer = serverDataViewer;
|
||||
}
|
||||
|
||||
public async Task OnEventAsync(GameEvent gameEvent, Server server)
|
||||
@ -201,13 +203,17 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
var performancePlayTime = validPerformanceValues.Sum(s => s.TimePlayed);
|
||||
var performance = Math.Round(validPerformanceValues.Sum(c => c.Performance * c.TimePlayed / performancePlayTime), 2);
|
||||
var spm = Math.Round(clientStats.Sum(c => c.SPM) / clientStats.Count(c => c.SPM > 0), 1);
|
||||
var overallRanking = await Manager.GetClientOverallRanking(request.ClientId);
|
||||
|
||||
return new List<InformationResponse>
|
||||
{
|
||||
new InformationResponse
|
||||
{
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING"],
|
||||
Value = "#" + (await Manager.GetClientOverallRanking(request.ClientId)).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Value = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_CLIENT_META_RANKING_FORMAT"].FormatExt((overallRanking == 0 ? "--" :
|
||||
overallRanking.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))),
|
||||
(await _serverDataViewer.RankedClientsCountAsync(token: token)).ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName))
|
||||
),
|
||||
Column = 0,
|
||||
Order = 0,
|
||||
Type = MetaType.Information
|
||||
|
@ -17,7 +17,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -20,7 +20,7 @@
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.3.23.1" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.9.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();
|
||||
}
|
||||
}
|
@ -20,6 +20,8 @@ namespace SharedLibraryCore
|
||||
{
|
||||
public class BaseController : Controller
|
||||
{
|
||||
protected readonly IAlertManager AlertManager;
|
||||
|
||||
/// <summary>
|
||||
/// life span in months
|
||||
/// </summary>
|
||||
@ -34,6 +36,7 @@ namespace SharedLibraryCore
|
||||
|
||||
public BaseController(IManager manager)
|
||||
{
|
||||
AlertManager = manager.AlertManager;
|
||||
Manager = manager;
|
||||
Localization ??= Utilities.CurrentLocalization.LocalizationIndex;
|
||||
AppConfig = Manager.GetApplicationSettings().Configuration();
|
||||
@ -169,6 +172,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);
|
||||
}
|
||||
|
@ -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 System;
|
||||
using Data.Models;
|
||||
using Data.Models.Client;
|
||||
|
||||
namespace SharedLibraryCore.Dtos
|
||||
@ -10,6 +11,7 @@ namespace SharedLibraryCore.Dtos
|
||||
public int LinkId { get; set; }
|
||||
public EFClient.Permission Level { get; set; }
|
||||
public DateTime LastConnection { get; set; }
|
||||
public Reference.Game Game { get; set; }
|
||||
public bool IsMasked { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ namespace SharedLibraryCore.Dtos
|
||||
public class PlayerInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public Reference.Game Game { get; set; }
|
||||
public int ClientId { get; set; }
|
||||
public string Level { get; set; }
|
||||
public string Tag { get; set; }
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Data.Models;
|
||||
using SharedLibraryCore.Helpers;
|
||||
|
||||
namespace SharedLibraryCore.Dtos
|
||||
@ -40,5 +41,6 @@ namespace SharedLibraryCore.Dtos
|
||||
return Math.Round(valid.Select(player => player.ZScore.Value).Average(), 2);
|
||||
}
|
||||
}
|
||||
public Reference.Game Game { get; set; }
|
||||
}
|
||||
}
|
||||
|
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; }
|
||||
|
||||
}
|
@ -102,5 +102,7 @@ namespace SharedLibraryCore.Interfaces
|
||||
/// event executed when event has finished executing
|
||||
/// </summary>
|
||||
event EventHandler<GameEvent> OnGameEventExecuted;
|
||||
|
||||
IAlertManager AlertManager { get; }
|
||||
}
|
||||
}
|
||||
|
@ -37,5 +37,13 @@ namespace SharedLibraryCore.Interfaces
|
||||
/// <returns></returns>
|
||||
Task<IEnumerable<ClientHistoryInfo>> ClientHistoryAsync(TimeSpan? overPeriod = null,
|
||||
CancellationToken token = default);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the number of ranked clients for given server id
|
||||
/// </summary>
|
||||
/// <param name="serverId">ServerId to query on</param>
|
||||
/// <param name="token">CancellationToken</param>
|
||||
/// <returns></returns>
|
||||
Task<int> RankedClientsCountAsync(long? serverId = null, CancellationToken token = default);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ namespace SharedLibraryCore.Database.Models
|
||||
set => CurrentAlias.IPAddress = value;
|
||||
}
|
||||
|
||||
[NotMapped] public string IPAddressString => IPAddress.ConvertIPtoString();
|
||||
[NotMapped] public string IPAddressString => IPAddress is null ? null : IPAddress.ConvertIPtoString();
|
||||
|
||||
[NotMapped] public bool IsIngame => ClientNumber >= 0;
|
||||
|
||||
@ -100,7 +100,7 @@ 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();
|
||||
|
||||
[NotMapped] public bool IsZombieClient => IsBot && Name == "Zombie";
|
||||
|
||||
|
@ -33,7 +33,8 @@ namespace SharedLibraryCore
|
||||
T6 = 7,
|
||||
T7 = 8,
|
||||
SHG1 = 9,
|
||||
CSGO = 10
|
||||
CSGO = 10,
|
||||
H1 = 11
|
||||
}
|
||||
|
||||
// only here for performance
|
||||
@ -200,7 +201,7 @@ namespace SharedLibraryCore
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public virtual Task<bool> ProcessUpdatesAsync(CancellationToken cts)
|
||||
public virtual Task<bool> ProcessUpdatesAsync(CancellationToken token)
|
||||
{
|
||||
return (Task<bool>)Task.CompletedTask;
|
||||
}
|
||||
|
@ -178,6 +178,7 @@ namespace SharedLibraryCore.Services
|
||||
.Select(_client => new EFClient
|
||||
{
|
||||
ClientId = _client.ClientId,
|
||||
GameName = _client.GameName,
|
||||
AliasLinkId = _client.AliasLinkId,
|
||||
Level = _client.Level,
|
||||
Connections = _client.Connections,
|
||||
@ -789,7 +790,8 @@ namespace SharedLibraryCore.Services
|
||||
PasswordSalt = client.PasswordSalt,
|
||||
NetworkId = client.NetworkId,
|
||||
LastConnection = client.LastConnection,
|
||||
Masked = client.Masked
|
||||
Masked = client.Masked,
|
||||
GameName = client.GameName
|
||||
};
|
||||
|
||||
return await iqClients.ToListAsync();
|
||||
|
@ -4,7 +4,7 @@
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
|
||||
<Version>2022.3.23.1</Version>
|
||||
<Version>2022.6.9.1</Version>
|
||||
<Authors>RaidMax</Authors>
|
||||
<Company>Forever None</Company>
|
||||
<Configurations>Debug;Release;Prerelease</Configurations>
|
||||
@ -19,7 +19,7 @@
|
||||
<IsPackable>true</IsPackable>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<Description>Shared Library for IW4MAdmin</Description>
|
||||
<PackageVersion>2022.3.23.1</PackageVersion>
|
||||
<PackageVersion>2022.6.9.1</PackageVersion>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
@ -47,6 +47,9 @@ namespace SharedLibraryCore
|
||||
public static char[] DirectorySeparatorChars = { '\\', '/' };
|
||||
public static char CommandPrefix { get; set; } = '!';
|
||||
|
||||
public static string ToStandardFormat(this DateTime? time) => time?.ToString("yyyy-MM-dd H:mm:ss UTC");
|
||||
public static string ToStandardFormat(this DateTime time) => time.ToString("yyyy-MM-dd H:mm:ss UTC");
|
||||
|
||||
public static EFClient IW4MAdminClient(Server server = null)
|
||||
{
|
||||
return new EFClient
|
||||
|
@ -24,6 +24,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 +65,9 @@ namespace WebfrontCore.Controllers
|
||||
case nameof(SetLevelCommand):
|
||||
_setLevelCommandName = cmd.Name;
|
||||
break;
|
||||
case "OfflineMessageCommand":
|
||||
_offlineMessageCommandName = cmd.Name;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -142,7 +146,7 @@ namespace WebfrontCore.Controllers
|
||||
}));
|
||||
}
|
||||
|
||||
public IActionResult UnbanForm()
|
||||
public IActionResult UnbanForm(long? id)
|
||||
{
|
||||
var info = new ActionInfo
|
||||
{
|
||||
@ -159,6 +163,15 @@ namespace WebfrontCore.Controllers
|
||||
Action = "UnbanAsync",
|
||||
ShouldRefresh = true
|
||||
};
|
||||
if (id is not null)
|
||||
{
|
||||
info.Inputs.Add(new()
|
||||
{
|
||||
Name = "targetId",
|
||||
Value = id.ToString(),
|
||||
Type = "hidden"
|
||||
});
|
||||
}
|
||||
|
||||
return View("_ActionForm", info);
|
||||
}
|
||||
@ -204,7 +217,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()
|
||||
@ -318,26 +331,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);
|
||||
@ -464,6 +478,105 @@ namespace WebfrontCore.Controllers
|
||||
}));
|
||||
}
|
||||
|
||||
public IActionResult DismissAlertForm(Guid id)
|
||||
{
|
||||
var info = new ActionInfo
|
||||
{
|
||||
ActionButtonLabel = "Dismiss",
|
||||
Name = "Dismiss Alert?",
|
||||
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 = "Alert dismissed"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public IActionResult DismissAllAlertsForm()
|
||||
{
|
||||
var info = new ActionInfo
|
||||
{
|
||||
ActionButtonLabel = "Dismiss",
|
||||
Name = "Dismiss All Alerts?",
|
||||
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 = "Alerts dismissed"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public IActionResult OfflineMessageForm()
|
||||
{
|
||||
var info = new ActionInfo
|
||||
{
|
||||
ActionButtonLabel = "Send",
|
||||
Name = "Compose Message",
|
||||
Inputs = new List<InputInfo>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Name = "message",
|
||||
Label = "Message 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>()))
|
||||
|
@ -4,6 +4,7 @@ using SharedLibraryCore;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System.Threading.Tasks;
|
||||
using WebfrontCore.QueryHelpers.Models;
|
||||
|
||||
namespace WebfrontCore.Controllers
|
||||
{
|
||||
@ -11,12 +12,16 @@ namespace WebfrontCore.Controllers
|
||||
{
|
||||
private readonly IAuditInformationRepository _auditInformationRepository;
|
||||
private readonly ITranslationLookup _translationLookup;
|
||||
private readonly IResourceQueryHelper<BanInfoRequest, BanInfo> _banInfoQueryHelper;
|
||||
private static readonly int DEFAULT_COUNT = 25;
|
||||
|
||||
public AdminController(IManager manager, IAuditInformationRepository auditInformationRepository, ITranslationLookup translationLookup) : base(manager)
|
||||
public AdminController(IManager manager, IAuditInformationRepository auditInformationRepository,
|
||||
ITranslationLookup translationLookup,
|
||||
IResourceQueryHelper<BanInfoRequest, BanInfo> banInfoQueryHelper) : base(manager)
|
||||
{
|
||||
_auditInformationRepository = auditInformationRepository;
|
||||
_translationLookup = translationLookup;
|
||||
_banInfoQueryHelper = banInfoQueryHelper;
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
@ -27,7 +32,7 @@ namespace WebfrontCore.Controllers
|
||||
ViewBag.Title = _translationLookup["WEBFRONT_NAV_AUDIT_LOG"];
|
||||
ViewBag.InitialOffset = DEFAULT_COUNT;
|
||||
|
||||
var auditItems = await _auditInformationRepository.ListAuditInformation(new PaginationRequest()
|
||||
var auditItems = await _auditInformationRepository.ListAuditInformation(new PaginationRequest
|
||||
{
|
||||
Count = DEFAULT_COUNT
|
||||
});
|
||||
@ -41,5 +46,25 @@ namespace WebfrontCore.Controllers
|
||||
var auditItems = await _auditInformationRepository.ListAuditInformation(paginationInfo);
|
||||
return PartialView("_ListAuditLog", auditItems);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> BanManagement([FromQuery] BanInfoRequest request)
|
||||
{
|
||||
var results = await _banInfoQueryHelper.QueryResource(request);
|
||||
|
||||
ViewBag.ClientName = request.ClientName;
|
||||
ViewBag.ClientId = request.ClientId;
|
||||
ViewBag.ClientIP = request.ClientIP;
|
||||
ViewBag.ClientGuid = request.ClientGuid;
|
||||
|
||||
ViewBag.Title = "Ban Management";
|
||||
|
||||
return View(results.Results);
|
||||
}
|
||||
|
||||
public async Task<IActionResult> BanManagementList([FromQuery] BanInfoRequest request)
|
||||
{
|
||||
var results = await _banInfoQueryHelper.QueryResource(request);
|
||||
return PartialView("_BanEntries", results.Results);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -88,6 +88,7 @@ namespace WebfrontCore.Controllers
|
||||
var clientDto = new PlayerInfo
|
||||
{
|
||||
Name = client.Name,
|
||||
Game = client.GameName ?? Reference.Game.UKN,
|
||||
Level = displayLevel,
|
||||
LevelInt = displayLevelInt,
|
||||
ClientId = client.ClientId,
|
||||
@ -181,7 +182,8 @@ namespace WebfrontCore.Controllers
|
||||
Name = admin.Name,
|
||||
ClientId = admin.ClientId,
|
||||
LastConnection = admin.LastConnection,
|
||||
IsMasked = admin.Masked
|
||||
IsMasked = admin.Masked,
|
||||
Game = admin.GameName ?? Reference.Game.UKN
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,7 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using IW4MAdmin.Plugins.Stats.Helpers;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Configuration;
|
||||
@ -13,26 +15,38 @@ namespace WebfrontCore.Controllers
|
||||
{
|
||||
private IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo> _queryHelper;
|
||||
private readonly DefaultSettings _defaultConfig;
|
||||
private readonly IServerDataViewer _serverDataViewer;
|
||||
|
||||
public ClientStatisticsController(IManager manager,
|
||||
IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo> queryHelper,
|
||||
DefaultSettings defaultConfig) : base(manager)
|
||||
DefaultSettings defaultConfig, IServerDataViewer serverDataViewer) : base(manager)
|
||||
{
|
||||
_queryHelper = queryHelper;
|
||||
_defaultConfig = defaultConfig;
|
||||
_serverDataViewer = serverDataViewer;
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/advanced")]
|
||||
public async Task<IActionResult> Advanced(int id, [FromQuery] string serverId)
|
||||
public async Task<IActionResult> Advanced(int id, [FromQuery] string serverId, CancellationToken token = default)
|
||||
{
|
||||
ViewBag.Config = _defaultConfig.GameStrings;
|
||||
var hitInfo = await _queryHelper.QueryResource(new StatsInfoRequest
|
||||
var hitInfo = (await _queryHelper.QueryResource(new StatsInfoRequest
|
||||
{
|
||||
ClientId = id,
|
||||
ServerEndpoint = serverId
|
||||
});
|
||||
})).Results.First();
|
||||
|
||||
var server = Manager.GetServers().FirstOrDefault(server => server.ToString() == serverId);
|
||||
long? matchedServerId = null;
|
||||
|
||||
return View("~/Views/Client/Statistics/Advanced.cshtml", hitInfo.Results.First());
|
||||
if (server != null)
|
||||
{
|
||||
matchedServerId = StatManager.GetIdForServer(server);
|
||||
}
|
||||
|
||||
hitInfo.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId, token);
|
||||
|
||||
return View("~/Views/Client/Statistics/Advanced.cshtml", hitInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ using Stats.Dtos;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
@ -28,10 +29,11 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
|
||||
private readonly ITranslationLookup _translationLookup;
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
private readonly StatsConfiguration _config;
|
||||
private readonly IServerDataViewer _serverDataViewer;
|
||||
|
||||
public StatsController(ILogger<StatsController> logger, IManager manager, IResourceQueryHelper<ChatSearchQuery,
|
||||
MessageResponse> resourceQueryHelper, ITranslationLookup translationLookup,
|
||||
IDatabaseContextFactory contextFactory, StatsConfiguration config) : base(manager)
|
||||
IDatabaseContextFactory contextFactory, StatsConfiguration config, IServerDataViewer serverDataViewer) : base(manager)
|
||||
{
|
||||
_logger = logger;
|
||||
_manager = manager;
|
||||
@ -39,15 +41,27 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
|
||||
_translationLookup = translationLookup;
|
||||
_contextFactory = contextFactory;
|
||||
_config = config;
|
||||
_serverDataViewer = serverDataViewer;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IActionResult TopPlayers(string serverId = null)
|
||||
public async Task<IActionResult> TopPlayers(string serverId = null, CancellationToken token = default)
|
||||
{
|
||||
ViewBag.Title = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_TITLE"];
|
||||
ViewBag.Description = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_STATS_INDEX_DESC"];
|
||||
ViewBag.Localization = _translationLookup;
|
||||
ViewBag.SelectedServerId = serverId;
|
||||
|
||||
var server = _manager.GetServers().FirstOrDefault(server => server.ToString() == serverId);
|
||||
long? matchedServerId = null;
|
||||
|
||||
if (server != null)
|
||||
{
|
||||
matchedServerId = StatManager.GetIdForServer(server);
|
||||
}
|
||||
|
||||
ViewBag.TotalRankedClients = await _serverDataViewer.RankedClientsCountAsync(matchedServerId, token);
|
||||
ViewBag.ServerId = matchedServerId;
|
||||
|
||||
return View("~/Views/Client/Statistics/Index.cshtml", _manager.GetServers()
|
||||
.Select(server => new ServerInfo
|
||||
|
@ -5,6 +5,7 @@ using SharedLibraryCore;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System.Linq;
|
||||
using Data.Models;
|
||||
using Data.Models.Client.Stats;
|
||||
using IW4MAdmin.Plugins.Stats.Helpers;
|
||||
using WebfrontCore.ViewModels;
|
||||
@ -34,6 +35,7 @@ namespace WebfrontCore.Controllers
|
||||
ID = s.EndPoint,
|
||||
Port = s.Port,
|
||||
Map = s.CurrentMap.Alias,
|
||||
Game = (Reference.Game)s.GameName,
|
||||
ClientCount = s.Clients.Count(client => client != null),
|
||||
MaxClients = s.MaxClients,
|
||||
GameType = s.GametypeName,
|
||||
|
@ -1,6 +1,10 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
using Data.Models;
|
||||
using Data.Models.Client;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore;
|
||||
@ -21,16 +25,26 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, B
|
||||
|
||||
public async Task<ResourceQueryHelperResult<BanInfo>> QueryResource(BanInfoRequest query)
|
||||
{
|
||||
if (query.Count > 30)
|
||||
if (query.Count > 10)
|
||||
{
|
||||
query.Count = 30;
|
||||
query.Count = 10;
|
||||
}
|
||||
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
|
||||
var matchingClients = await context.Clients.Where(client =>
|
||||
EF.Functions.ILike(client.CurrentAlias.SearchableName ?? client.CurrentAlias.Name, $"%{query.ClientName.Trim()}%"))
|
||||
.Where(client => client.Level == EFClient.Permission.Banned)
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
|
||||
var iqMatchingClients = context.Clients.Where(client => client.Level == EFClient.Permission.Banned);
|
||||
iqMatchingClients = SetupSearchArgs(query, iqMatchingClients);
|
||||
|
||||
if (string.IsNullOrEmpty(query.ClientName) && string.IsNullOrEmpty(query.ClientGuid) &&
|
||||
query.ClientId is null && string.IsNullOrEmpty(query.ClientIP))
|
||||
{
|
||||
return new ResourceQueryHelperResult<BanInfo>
|
||||
{
|
||||
Results = Enumerable.Empty<BanInfo>()
|
||||
};
|
||||
}
|
||||
|
||||
var matchingClients = await iqMatchingClients
|
||||
.OrderByDescending(client => client.LastConnection)
|
||||
.Skip(query.Offset)
|
||||
.Take(query.Count)
|
||||
@ -39,52 +53,117 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, B
|
||||
client.CurrentAlias.Name,
|
||||
client.NetworkId,
|
||||
client.AliasLinkId,
|
||||
client.ClientId
|
||||
client.ClientId,
|
||||
client.CurrentAlias.IPAddress
|
||||
}).ToListAsync();
|
||||
|
||||
var usedIps = await context.Aliases
|
||||
.Where(alias => matchingClients.Select(client => client.AliasLinkId).Contains(alias.LinkId))
|
||||
.Where(alias => alias.IPAddress != null)
|
||||
.Select(alias => new { alias.IPAddress, alias.LinkId })
|
||||
.ToListAsync();
|
||||
var results = new List<BanInfo>();
|
||||
var matchedClientIds = new List<int?>();
|
||||
var lateDateTime = DateTime.Now.AddYears(100);
|
||||
|
||||
var usedIpsGrouped = usedIps
|
||||
.GroupBy(alias => alias.LinkId)
|
||||
.ToDictionary(key => key.Key, value => value.Select(alias => alias.IPAddress).Distinct());
|
||||
|
||||
var searchingNetworkIds = matchingClients.Select(client => client.NetworkId);
|
||||
var searchingIps = usedIpsGrouped.SelectMany(group => group.Value);
|
||||
|
||||
var matchedPenalties = await context.PenaltyIdentifiers.Where(identifier =>
|
||||
searchingNetworkIds.Contains(identifier.NetworkId) ||
|
||||
searchingIps.Contains(identifier.IPv4Address))
|
||||
.Select(penalty => new
|
||||
{
|
||||
penalty.CreatedDateTime,
|
||||
PunisherName = penalty.Penalty.Punisher.CurrentAlias.Name,
|
||||
Offense = string.IsNullOrEmpty(penalty.Penalty.AutomatedOffense) ? penalty.Penalty.Offense : "Anticheat Detection",
|
||||
LinkId = penalty.Penalty.Offender.AliasLinkId,
|
||||
penalty.Penalty.PunisherId
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
var groupedPenalties = matchedPenalties.GroupBy(penalty => penalty.LinkId)
|
||||
.ToDictionary(key => key.Key, value => value.FirstOrDefault());
|
||||
|
||||
var results = matchingClients.Select(client =>
|
||||
// would prefer not to loop this, but unfortunately due to the data design
|
||||
// we can't properly group on ip and alias link
|
||||
foreach (var matchingClient in matchingClients)
|
||||
{
|
||||
var matchedPenalty =
|
||||
groupedPenalties.ContainsKey(client.AliasLinkId) ? groupedPenalties[client.AliasLinkId] : null;
|
||||
return new BanInfo
|
||||
var usedIps = await context.Aliases
|
||||
.Where(alias => matchingClient.AliasLinkId == alias.LinkId)
|
||||
.Where(alias => alias.IPAddress != null)
|
||||
.Select(alias => new { alias.IPAddress, alias.LinkId })
|
||||
.ToListAsync();
|
||||
|
||||
var searchingNetworkId = matchingClient.NetworkId;
|
||||
var searchingIps = usedIps.Select(ip => ip.IPAddress).Distinct();
|
||||
|
||||
var matchedPenalties = await context.PenaltyIdentifiers.Where(identifier =>
|
||||
identifier.NetworkId == searchingNetworkId ||
|
||||
searchingIps.Contains(identifier.IPv4Address))
|
||||
.Where(identifier => identifier.Penalty.Expires == null || identifier.Penalty.Expires > lateDateTime)
|
||||
.Select(penalty => new
|
||||
{
|
||||
penalty.CreatedDateTime,
|
||||
PunisherName = penalty.Penalty.Punisher.CurrentAlias.Name,
|
||||
OffenderName = penalty.Penalty.Offender.CurrentAlias.Name,
|
||||
Offense = string.IsNullOrEmpty(penalty.Penalty.AutomatedOffense)
|
||||
? penalty.Penalty.Offense
|
||||
: "Anticheat Detection",
|
||||
LinkId = penalty.Penalty.Offender.AliasLinkId,
|
||||
penalty.Penalty.OffenderId,
|
||||
penalty.Penalty.PunisherId,
|
||||
penalty.IPv4Address,
|
||||
penalty.Penalty.Offender.NetworkId
|
||||
})
|
||||
.ToListAsync();
|
||||
|
||||
if (!matchedPenalties.Any())
|
||||
{
|
||||
DateTime = matchedPenalty?.CreatedDateTime,
|
||||
OffenderName = client.Name.StripColors(),
|
||||
OffenderId = client.ClientId,
|
||||
PunisherName = matchedPenalty?.PunisherName.StripColors(),
|
||||
PunisherId = matchedPenalty?.PunisherId,
|
||||
Offense = matchedPenalty?.Offense
|
||||
};
|
||||
}).ToList();
|
||||
var linkIds = (await context.Aliases
|
||||
.Where(alias => alias.IPAddress != null && searchingIps.Contains(alias.IPAddress))
|
||||
.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))
|
||||
.OrderByDescending(penalty => penalty.When)
|
||||
.Select(penalty => new
|
||||
{
|
||||
CreatedDateTime = penalty.When,
|
||||
PunisherName = penalty.Punisher.CurrentAlias.Name,
|
||||
OffenderName = penalty.Offender.CurrentAlias.Name,
|
||||
Offense = string.IsNullOrEmpty(penalty.AutomatedOffense)
|
||||
? penalty.Offense
|
||||
: "Anticheat Detection",
|
||||
LinkId = penalty.Offender.AliasLinkId,
|
||||
penalty.OffenderId,
|
||||
penalty.PunisherId,
|
||||
IPv4Address = penalty.Offender.CurrentAlias.IPAddress,
|
||||
penalty.Offender.NetworkId
|
||||
}).ToListAsync();
|
||||
}
|
||||
|
||||
var allPenalties = matchedPenalties.Select(penalty => new PenaltyInfo
|
||||
{
|
||||
DateTime = penalty.CreatedDateTime,
|
||||
Offense = penalty.Offense,
|
||||
PunisherInfo = new RelatedClientInfo
|
||||
{
|
||||
ClientName = penalty.PunisherName.StripColors(),
|
||||
ClientId = penalty.PunisherId,
|
||||
},
|
||||
OffenderInfo = new RelatedClientInfo
|
||||
{
|
||||
ClientName = penalty.OffenderName.StripColors(),
|
||||
ClientId = penalty.OffenderId,
|
||||
IPAddress = penalty.IPv4Address,
|
||||
NetworkId = penalty.NetworkId
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
|
||||
if (matchedClientIds.Contains(matchingClient.ClientId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
matchedClientIds.Add(matchingClient.ClientId);
|
||||
var relatedEntities =
|
||||
allPenalties.Where(penalty => penalty.OffenderInfo.ClientId != matchingClient.ClientId);
|
||||
|
||||
matchedClientIds.AddRange(relatedEntities.Select(client => client.OffenderInfo.ClientId));
|
||||
|
||||
results.Add(new BanInfo
|
||||
{
|
||||
ClientName = matchingClient.Name.StripColors(),
|
||||
ClientId = matchingClient.ClientId,
|
||||
NetworkId = matchingClient.NetworkId,
|
||||
IPAddress = matchingClient.IPAddress,
|
||||
|
||||
AssociatedPenalties = relatedEntities,
|
||||
AttachedPenalty = allPenalties.FirstOrDefault(penalty =>
|
||||
penalty.OffenderInfo.ClientId == matchingClient.ClientId)
|
||||
});
|
||||
}
|
||||
|
||||
return new ResourceQueryHelperResult<BanInfo>
|
||||
{
|
||||
@ -93,4 +172,61 @@ public class BanInfoResourceQueryHelper : IResourceQueryHelper<BanInfoRequest, B
|
||||
Results = results
|
||||
};
|
||||
}
|
||||
|
||||
private IQueryable<EFClient> SetupSearchArgs(BanInfoRequest query, IQueryable<EFClient> source)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(query.ClientName))
|
||||
{
|
||||
var nameToSearch = query.ClientName.Trim().ToLower();
|
||||
source = source.Where(client =>
|
||||
EF.Functions.Like(client.CurrentAlias.SearchableName ?? client.CurrentAlias.Name.ToLower(),
|
||||
$"%{nameToSearch}%"));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(query.ClientGuid))
|
||||
{
|
||||
long? parsedGuid = null;
|
||||
if (!long.TryParse(query.ClientGuid, NumberStyles.HexNumber, null, out var guid))
|
||||
{
|
||||
if (!long.TryParse(query.ClientGuid, out var guid2))
|
||||
{
|
||||
}
|
||||
else
|
||||
{
|
||||
parsedGuid = guid2;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
parsedGuid = guid;
|
||||
}
|
||||
|
||||
if (parsedGuid is not null)
|
||||
{
|
||||
source = source.Where(client => client.NetworkId == parsedGuid);
|
||||
}
|
||||
}
|
||||
|
||||
if (query.ClientId is not null)
|
||||
{
|
||||
source = source.Where(client => client.ClientId == query.ClientId);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(query.ClientIP))
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
var parsedIp = query.ClientIP.ConvertToIP();
|
||||
if (parsedIp is not null)
|
||||
{
|
||||
source = source.Where(client => client.CurrentAlias.IPAddress == parsedIp);
|
||||
}
|
||||
else
|
||||
{
|
||||
query.ClientIP = null;
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,33 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace WebfrontCore.QueryHelpers.Models;
|
||||
|
||||
public class BanInfo
|
||||
{
|
||||
public string OffenderName { get; set; }
|
||||
public int OffenderId { get; set; }
|
||||
public string PunisherName { get; set; }
|
||||
public int? PunisherId { get; set; }
|
||||
public string ClientName { get; set; }
|
||||
public int ClientId { get; set; }
|
||||
public int? IPAddress { get; set; }
|
||||
public long NetworkId { get; set; }
|
||||
public PenaltyInfo AttachedPenalty { get; set; }
|
||||
public IEnumerable<PenaltyInfo> AssociatedPenalties { get; set; }
|
||||
}
|
||||
|
||||
public class PenaltyInfo
|
||||
{
|
||||
public RelatedClientInfo OffenderInfo { get; set; }
|
||||
public RelatedClientInfo PunisherInfo { get; set; }
|
||||
public string Offense { get; set; }
|
||||
public DateTime? DateTime { get; set; }
|
||||
public long? TimeStamp => DateTime.HasValue ? new DateTimeOffset(DateTime.Value, TimeSpan.Zero).ToUnixTimeSeconds() : null;
|
||||
|
||||
public long? TimeStamp =>
|
||||
DateTime.HasValue ? new DateTimeOffset(DateTime.Value, TimeSpan.Zero).ToUnixTimeSeconds() : null;
|
||||
}
|
||||
|
||||
public class RelatedClientInfo
|
||||
{
|
||||
public string ClientName { get; set; }
|
||||
public int? ClientId { get; set; }
|
||||
public int? IPAddress { get; set; }
|
||||
public long? NetworkId { get; set; }
|
||||
}
|
||||
|
@ -5,4 +5,7 @@ namespace WebfrontCore.QueryHelpers.Models;
|
||||
public class BanInfoRequest : PaginationRequest
|
||||
{
|
||||
public string ClientName { get; set; }
|
||||
public string ClientGuid { get; set; }
|
||||
public int? ClientId { get; set; }
|
||||
public string ClientIP { get; set; }
|
||||
}
|
||||
|
@ -4,11 +4,9 @@ using Microsoft.AspNetCore.Mvc;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models;
|
||||
using Data.Models.Client.Stats;
|
||||
using Microsoft.AspNetCore.Hosting.Server;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using static SharedLibraryCore.Server;
|
||||
@ -72,6 +70,7 @@ namespace WebfrontCore.ViewComponents
|
||||
ID = server.EndPoint,
|
||||
Port = server.Port,
|
||||
Map = server.CurrentMap.Alias,
|
||||
Game = (Reference.Game)server.GameName,
|
||||
ClientCount = server.Clients.Count(client => client != null),
|
||||
MaxClients = server.MaxClients,
|
||||
GameType = server.GametypeName,
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Html;
|
||||
|
||||
namespace WebfrontCore.ViewModels;
|
||||
|
||||
@ -19,7 +20,7 @@ public class TableInfo
|
||||
|
||||
public class RowDefinition
|
||||
{
|
||||
public List<string> Datum { get; } = new();
|
||||
public List<ColumnTypeDefinition> Datum { get; } = new();
|
||||
}
|
||||
|
||||
public class ColumnDefinition
|
||||
@ -28,6 +29,23 @@ public class ColumnDefinition
|
||||
public string ColumnSpan { get; set; }
|
||||
}
|
||||
|
||||
public enum ColumnType
|
||||
{
|
||||
Text,
|
||||
Link,
|
||||
Icon,
|
||||
Button
|
||||
}
|
||||
|
||||
public class ColumnTypeDefinition
|
||||
{
|
||||
public ColumnType Type { get; set; }
|
||||
public string Value { get; set; }
|
||||
public string Data { get; set; }
|
||||
public IHtmlContent Template { get; set; }
|
||||
public int Id { get; set; }
|
||||
}
|
||||
|
||||
public static class TableInfoExtensions
|
||||
{
|
||||
public static TableInfo WithColumns(this TableInfo info, IEnumerable<string> columns)
|
||||
@ -42,6 +60,16 @@ public static class TableInfoExtensions
|
||||
|
||||
public static TableInfo WithRows<T>(this TableInfo info, IEnumerable<T> source,
|
||||
Func<T, IEnumerable<string>> selector)
|
||||
{
|
||||
return WithRows(info, source, (outer) => selector(outer).Select(item => new ColumnTypeDefinition
|
||||
{
|
||||
Value = item,
|
||||
Type = ColumnType.Text
|
||||
}));
|
||||
}
|
||||
|
||||
public static TableInfo WithRows<T>(this TableInfo info, IEnumerable<T> source,
|
||||
Func<T, IEnumerable<ColumnTypeDefinition>> selector)
|
||||
{
|
||||
info.Rows.AddRange(source.Select(row =>
|
||||
{
|
||||
|
@ -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');">Close</a>
|
||||
</div>
|
||||
</form>
|
||||
|
66
WebfrontCore/Views/Admin/BanManagement.cshtml
Normal file
66
WebfrontCore/Views/Admin/BanManagement.cshtml
Normal file
@ -0,0 +1,66 @@
|
||||
@model IEnumerable<WebfrontCore.QueryHelpers.Models.BanInfo>
|
||||
|
||||
<div class="content mt-0">
|
||||
<h2 class="content-title mt-20 mb-10">@ViewBag.Title</h2>
|
||||
@if (!Model.Any())
|
||||
{
|
||||
<div class="text-muted mb-10">Search for records...</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">
|
||||
<div class="input-group-append">
|
||||
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
|
||||
<i class="oi oi-magnifying-glass"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</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">
|
||||
<div class="input-group-append">
|
||||
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
|
||||
<i class="oi oi-magnifying-glass"></i>
|
||||
</button>
|
||||
</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">
|
||||
<div class="input-group-append">
|
||||
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
|
||||
<i class="oi oi-magnifying-glass"></i>
|
||||
</button>
|
||||
</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">
|
||||
<div class="input-group-append">
|
||||
<button class="btn bg-dark-dm bg-light-ex-lm" type="submit">
|
||||
<i class="oi oi-magnifying-glass"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="ban_entry_list">
|
||||
<partial name="_BanEntries" for="@Model"/>
|
||||
</div>
|
||||
|
||||
@if (Model.Any())
|
||||
{
|
||||
<div class="w-full text-center">
|
||||
<i id="loaderLoad" class="oi oi-chevron-bottom mt-10 loader-load-more text-primary m-auto" aria-hidden="true"></i>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@section scripts {
|
||||
<script>
|
||||
initLoader('/Admin/BanManagementList', '#ban_entry_list', 10, 10, [{ 'name': 'clientIP', 'value': () => $('#clientIPInput').val() },
|
||||
{ 'name': 'clientGuid', 'value': () => $('#clientGuidInput').val() },
|
||||
{ 'name': 'clientName', 'value': () => $('#clientNameInput').val() },
|
||||
{ 'name': 'clientId', 'value': () => $('#clientIdInput').val() }]);
|
||||
</script>
|
||||
}
|
63
WebfrontCore/Views/Admin/_BanEntries.cshtml
Normal file
63
WebfrontCore/Views/Admin/_BanEntries.cshtml
Normal file
@ -0,0 +1,63 @@
|
||||
@model IEnumerable<WebfrontCore.QueryHelpers.Models.BanInfo>
|
||||
|
||||
@foreach (var ban in Model)
|
||||
{
|
||||
if (ban.AttachedPenalty is null && !ban.AssociatedPenalties.Any())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
<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>
|
||||
<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>
|
||||
@if (ban.AttachedPenalty is not null)
|
||||
{
|
||||
<br/>
|
||||
<div class="text-muted font-weight-light">@ban.AttachedPenalty.Offense.CapClientName(30)</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>
|
||||
}
|
||||
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>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@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>
|
||||
</div>
|
||||
<has-permission entity="ClientGuid" required-permission="Read">
|
||||
<div class="text-muted">@associatedEntity.OffenderInfo.NetworkId?.ToString("X")</div>
|
||||
</has-permission>
|
||||
<has-permission entity="ClientIPAddress" required-permission="Read">
|
||||
<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-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>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
@ -5,10 +5,11 @@
|
||||
|
||||
@foreach (var key in Model.Keys)
|
||||
{
|
||||
<table class="table mb-20">
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -33,6 +34,9 @@
|
||||
<color-code value="@client.Name"></color-code>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<div class="badge">@ViewBag.Localization[$"GAME_{client.Game}"]</div>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
@client.LastConnection.HumanizeForCurrentCulture()
|
||||
</td>
|
||||
|
@ -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,8 @@
|
||||
</has-permission>
|
||||
}
|
||||
|
||||
<h2 class="content-title mb-10">Player Profile</h2>
|
||||
<h2 class="content-title mb-0">Player Profile</h2>
|
||||
<div class="font-size-12 text-muted">@ViewBag.Localization[$"GAME_{Model.Game}"]</div>
|
||||
|
||||
<div id="profile_wrapper" class="mb-10 mt-10">
|
||||
|
||||
@ -200,7 +201,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<hr class="mr-5 ml-5"/>
|
||||
<!-- meta info block -->
|
||||
@ -278,6 +279,18 @@
|
||||
EntityId = Model.ClientId
|
||||
});
|
||||
}
|
||||
|
||||
if (ViewBag.Authorized)
|
||||
{
|
||||
menuItems.Items.Add(new SideContextMenuItem
|
||||
{
|
||||
Title = "Message",
|
||||
IsButton = true,
|
||||
Reference = "OfflineMessage",
|
||||
Icon = "oi oi-envelope-closed",
|
||||
EntityId = Model.ClientId
|
||||
});
|
||||
}
|
||||
|
||||
menuItems.Items.Add(new SideContextMenuItem
|
||||
{
|
||||
@ -299,7 +312,7 @@
|
||||
EntityId = Model.ClientId
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
if (Model.LevelInt < (int)ViewBag.User.Level && Model.Online)
|
||||
{
|
||||
menuItems.Items.Add(new SideContextMenuItem
|
||||
@ -335,6 +348,7 @@
|
||||
EntityId = Model.ClientId
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
<partial name="_SideContextMenu" for="@menuItems"></partial>
|
||||
|
||||
|
@ -232,7 +232,7 @@
|
||||
|
||||
<div class="content row mt-20">
|
||||
<!-- main content -->
|
||||
<div class="col-12 col-lg-9 col-xl-10 mt-0">
|
||||
<div class="col-12 col-lg-9 mt-0">
|
||||
<h2 class="content-title mb-0">Player Stats</h2>
|
||||
<span class="text-muted">
|
||||
<color-code value="@(Model.Servers.FirstOrDefault(server => server.Endpoint == Model.ServerEndpoint)?.Name ?? ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"])"></color-code>
|
||||
@ -256,7 +256,7 @@
|
||||
{
|
||||
if (Model.Ranking > 0)
|
||||
{
|
||||
<div class="h5 mb-0">@Html.Raw((ViewBag.Localization["WEBFRONT_ADV_STATS_RANKED"] as string).FormatExt(Model.Ranking))</div>
|
||||
<div class="h5 mb-0">@Html.Raw((ViewBag.Localization["WEBFRONT_ADV_STATS_RANKED_V2"] as string).FormatExt(Model.Ranking?.ToString("#,##0"), Model.TotalRankedClients.ToString("#,##0")))</div>
|
||||
}
|
||||
|
||||
else
|
||||
|
@ -2,10 +2,11 @@
|
||||
@using WebfrontCore.ViewModels
|
||||
|
||||
<div class="content mt-20 row">
|
||||
<div class="col-12 col-lg-9 col-xl-10 mt-0">
|
||||
<div class="col-12 col-lg-9 mt-0">
|
||||
<h2 class="content-title mb-0">Top Players</h2>
|
||||
<span class="text-muted">
|
||||
<color-code value="@(Model.FirstOrDefault(m => m.Endpoint == ViewBag.SelectedServerId)?.Name ?? ViewBag.Localization["WEBFRONT_STATS_INDEX_ALL_SERVERS"])"></color-code>
|
||||
— <span class="text-primary">@ViewBag.TotalRankedClients.ToString("#,##0")</span> Ranked Players
|
||||
</span>
|
||||
|
||||
<div id="topPlayersContainer">
|
||||
@ -42,6 +43,7 @@
|
||||
{
|
||||
<environment include="Development">
|
||||
<script type="text/javascript" src="~/js/stats.js"></script>
|
||||
<script type="text/javascript" src="~/lib/canvas.js/canvasjs.js"></script>
|
||||
</environment>
|
||||
<script>initLoader('/Stats/GetTopPlayersAsync', '#topPlayersContainer', 25);</script>
|
||||
<script>initLoader('/Stats/GetTopPlayersAsync', '#topPlayersContainer', 25, 25, [{ 'name': 'serverId', 'value' : () => @(ViewBag.ServerId ?? 0) }]);</script>
|
||||
}
|
||||
|
@ -11,7 +11,7 @@
|
||||
}
|
||||
}
|
||||
<div class="content mt-20 row">
|
||||
<div class="col-12 col-lg-9 col-xl-10">
|
||||
<div class="col-12 col-lg-9">
|
||||
<h2 class="content-title mb-0">Server Overview</h2>
|
||||
@if (Model.Game.HasValue)
|
||||
{
|
||||
|
@ -13,7 +13,7 @@
|
||||
</style>
|
||||
|
||||
<div class="content mt-20 row">
|
||||
<div class="col-12 col-lg-9 col-xl-10">
|
||||
<div class="col-12 col-lg-9">
|
||||
<h2 class="content-title mb-0">Live Radar</h2>
|
||||
<div class="text-muted mb-15">
|
||||
<color-code value="@((Model.FirstOrDefault(server => server.Endpoint == ViewBag.SelectedServerId) ?? Model.First()).Name)"></color-code>
|
||||
|
@ -5,7 +5,7 @@
|
||||
}
|
||||
|
||||
<div class="content mt-20 row">
|
||||
<div class="col-12 col-lg-9 col-xl-10">
|
||||
<div class="col-12 col-lg-9">
|
||||
@if (Model is not null)
|
||||
{
|
||||
<div class=" scoreboard-container" data-server-id="@ViewBag.SelectedServerId">
|
||||
|
@ -15,20 +15,23 @@
|
||||
foreach (var snapshot in Model.ClientHistory.ClientCounts)
|
||||
{
|
||||
snapshot.MapAlias = GetMapName(snapshot.Map);
|
||||
}
|
||||
};
|
||||
|
||||
string MakeAbbreviation(string gameName) => string.Join("", gameName.Split(' ').Select(word => char.ToUpper(word.First())).ToArray());
|
||||
}
|
||||
|
||||
<div class="card mt-20 mb-20 ml-0 mr-0 p-0">
|
||||
<div class="p-5 pl-10 pr-10 bg-primary rounded-top d-flex flex-column flex-md-row flex-wrap justify-content-between text-light" id="server_header_@Model.ID">
|
||||
<div class="d-flex align-self-center flex-column-reverse flex-md-row">
|
||||
<div class="ml-5 mr-5 text-center">
|
||||
<div class="p-5 pl-10 pr-10 bg-primary rounded-top d-flex flex-column flex-lg-row flex-wrap justify-content-between text-light" id="server_header_@Model.ID">
|
||||
<!-- first column -->
|
||||
<div class="d-flex align-self-center flex-column-reverse flex-lg-row col-12 col-lg-6">
|
||||
<div class="ml-5 mr-5 text-center text-lg-left">
|
||||
<color-code value="@Model.Name"></color-code>
|
||||
<div class="server-header-ip-address font-weight-light" style="display:none">@(Model.ExternalIPAddress):@(Model.Port)</div>
|
||||
</div>
|
||||
<div class="d-flex justify-content-center">
|
||||
<!-- connect button -->
|
||||
<a href="@Model.ConnectProtocolUrl" class="text-light align-self-center" title="@Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_HOME_JOIN_DESC"]">
|
||||
<a href="@Model.ConnectProtocolUrl" class="text-light align-self-center server-join-button" title="@Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_HOME_JOIN_DESC"]">
|
||||
<i class="oi oi-play-circle ml-5 mr-5"></i>
|
||||
<span class="server-header-ip-address" style="display:none;">@(Model.ExternalIPAddress):@(Model.Port)</span>
|
||||
</a>
|
||||
<has-permission entity="AdminMenu" required-permission="Update">
|
||||
<!-- send message button -->
|
||||
@ -41,17 +44,21 @@
|
||||
class="text-light align-self-center">
|
||||
<i class="oi oi-spreadsheet ml-5 mr-5"></i>
|
||||
</a>
|
||||
<span class="ml-5 mr-5 text-light badge font-weight-light" data-toggle="tooltip" data-title="@ViewBag.Localization[$"GAME_{Model.Game}"]">@MakeAbbreviation(ViewBag.Localization[$"GAME_{Model.Game}"])</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="align-self-center">
|
||||
<!-- second column -->
|
||||
<div class="col-12 align-self-center text-center text-lg-left col-lg-4">
|
||||
<span>@Model.Map</span>
|
||||
@if (!string.IsNullOrEmpty(Model.GameType) && Model.GameType.Length > 1)
|
||||
{
|
||||
<span>–</span>
|
||||
<span>@Model.GameType</span>
|
||||
}
|
||||
|
||||
</div>
|
||||
<div class="align-self-center d-flex flex-column flex-md-row">
|
||||
<!-- third column -->
|
||||
<div class="align-self-center d-flex flex-column flex-lg-row col-12 col-lg-2 justify-content-end">
|
||||
@if (Model.LobbyZScore != null)
|
||||
{
|
||||
<div data-toggle="tooltip" data-title="@ViewBag.Localization["WEBFRONT_HOME_RATING_DESC"]" class="cursor-help d-flex flex-row-reverse flex-md-row justify-content-center">
|
||||
|
@ -55,10 +55,10 @@
|
||||
}
|
||||
start++;
|
||||
|
||||
<div class="profile-meta-entry loader-data-time" data-time="@meta.When.ToFileTimeUtc()" onclick="$('#metaContextDateToggle@(start)').show()">
|
||||
<div class="profile-meta-entry loader-data-time" data-time="@meta.When.ToFileTimeUtc()" onclick="$('#metaContextDateToggle@(meta.When.ToFileTimeUtc())').show()">
|
||||
<partial name="~/Views/Client/Profile/Meta/_@(meta.GetType().Name).cshtml" model="meta"/>
|
||||
<div style="display:none" id="metaContextDateToggle@(start)">
|
||||
Event occured at <span class="text-light">@meta.When.ToString()</span>
|
||||
<div style="display:none" id="metaContextDateToggle@(meta.When.ToFileTimeUtc())">
|
||||
Event occured at <span class="text-light">@meta.When.ToStandardFormat()</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
66
WebfrontCore/Views/Shared/Partials/_Notifications.cshtml
Normal file
66
WebfrontCore/Views/Shared/Partials/_Notifications.cshtml
Normal file
@ -0,0 +1,66 @@
|
||||
@using SharedLibraryCore.Alerts
|
||||
@using Humanizer
|
||||
@model IEnumerable<SharedLibraryCore.Alerts.Alert.AlertState>
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
<div class="dropdown with-arrow" data-toggle="dropdown" id="alert-toggle" aria-haspopup="true" aria-expanded="false">
|
||||
<div data-toggle="tooltip" data-title="@(Model.Any() ? "View Alerts" : "No Alerts")" data-placement="bottom">
|
||||
<i class="oi oi-bell mt-5"></i>
|
||||
</div>
|
||||
@if (Model.Any())
|
||||
{
|
||||
<div class="position-absolute bg-danger rounded-circle ml-10" style="width: 0.5em;height: 0.5em;top: 0;"></div>
|
||||
<div class="dropdown-menu dropdown-menu-right w-250 w-md-400" aria-labelledby="alert-toggle">
|
||||
<div class="d-flex">
|
||||
<h6 class="dropdown-header">@ViewBag.Alerts.Count Alerts</h6>
|
||||
<i class="oi oi-circle-x font-size-12 ml-auto mr-10 text-danger align-self-center profile-action" data-action="DismissAllAlerts" data-action-id="@ViewBag.User.ClientId"></i>
|
||||
</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
@foreach (var alert in Model)
|
||||
{
|
||||
<div class="d-flex p-5 pl-10 pr-10">
|
||||
<div class="align-self-center">
|
||||
@if (alert.Category == Alert.AlertCategory.Error)
|
||||
{
|
||||
<i class="oi oi-warning text-danger font-size-12 mr-5"></i>
|
||||
}
|
||||
@if (alert.Category == Alert.AlertCategory.Warning)
|
||||
{
|
||||
<i class="oi oi-warning text-secondary font-size-12 mr-5"></i>
|
||||
}
|
||||
@if (alert.Category == Alert.AlertCategory.Information)
|
||||
{
|
||||
<i class="oi oi-circle-check font-size-12 mr-5 text-primary"></i>
|
||||
}
|
||||
@if (alert.Category == Alert.AlertCategory.Message)
|
||||
{
|
||||
<i class="oi oi-envelope-closed font-size-12 mr-5 text-primary"></i>
|
||||
}
|
||||
</div>
|
||||
<div class="font-size-12 p-5">
|
||||
<span class="text-force-break">@alert.Message</span>
|
||||
<div class="text-muted d-flex">
|
||||
<span>@alert.OccuredAt.Humanize()</span>
|
||||
@if (!string.IsNullOrEmpty(alert.Source))
|
||||
{
|
||||
<span class="ml-5 mr-5">•</span>
|
||||
@if (alert.SourceId is null)
|
||||
{
|
||||
<div class="text-white font-weight-light">@alert.Source.StripColors()</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a asp-controller="Client" asp-action="Profile" asp-route-id="@alert.SourceId" class="no-decoration">@alert.Source</a>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-auto">
|
||||
<i class="oi oi-circle-x font-size-12 ml-5 align-self-center profile-action" data-action="DismissAlert" data-action-id="@alert.AlertId"></i>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
@ -1,4 +1,5 @@
|
||||
@model WebfrontCore.ViewModels.TableInfo
|
||||
@using WebfrontCore.ViewModels
|
||||
@model WebfrontCore.ViewModels.TableInfo
|
||||
@{
|
||||
Layout = null;
|
||||
}
|
||||
@ -17,7 +18,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@{ var start = 0;}
|
||||
@{ var start = 0; }
|
||||
@if (!Model.Rows.Any())
|
||||
{
|
||||
<!-- desktop -->
|
||||
@ -35,15 +36,40 @@
|
||||
@foreach (var row in Model.Rows)
|
||||
{
|
||||
<!-- desktop -->
|
||||
<tr class="bg-dark-dm bg-light-lm @(Model.InitialRowCount > 0 && start >= Model.InitialRowCount ? "d-none hidden-row-lg": "d-none d-lg-table-row")">
|
||||
<tr class="bg-dark-dm bg-light-lm @(Model.InitialRowCount > 0 && start >= Model.InitialRowCount ? "d-none hidden-row-lg" : "d-none d-lg-table-row")">
|
||||
@for (var i = 0; i < Model.Columns.Count; i++)
|
||||
{
|
||||
<td>@row.Datum[i]</td>
|
||||
var data = row.Datum[i];
|
||||
<td>
|
||||
@if (data.Template is null)
|
||||
{
|
||||
if (data.Type == ColumnType.Text)
|
||||
{
|
||||
<span>@data.Value</span>
|
||||
}
|
||||
if (data.Type == ColumnType.Link)
|
||||
{
|
||||
<a href="@data.Data" class="no-decoration">@data.Value</a>
|
||||
}
|
||||
if (data.Type == ColumnType.Icon)
|
||||
{
|
||||
<span class="oi @data.Value profile-action" data-action="@data.Data" data-action-id="@data.Id"></span>
|
||||
}
|
||||
if (data.Type == ColumnType.Button)
|
||||
{
|
||||
<div class="btn profile-action" data-action="@data.Data" data-action-id="@data.Id">@data.Value</div>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@data.Template
|
||||
}
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
|
||||
<!-- mobile -->
|
||||
<tr class="@(Model.InitialRowCount > 0 && start >= Model.InitialRowCount ? "d-none hidden-row": "d-flex d-table-row d-lg-none")">
|
||||
<tr class="@(Model.InitialRowCount > 0 && start >= Model.InitialRowCount ? "d-none hidden-row" : "d-flex d-table-row d-lg-none")">
|
||||
<td class="bg-primary text-light text-right w-125">
|
||||
@foreach (var column in Model.Columns)
|
||||
{
|
||||
@ -53,7 +79,21 @@
|
||||
<td class="bg-dark-dm bg-light-lm flex-fill w-200">
|
||||
@for (var i = 0; i < Model.Columns.Count; i++)
|
||||
{
|
||||
<div class="mt-5 mb-5 text-truncate" style="min-width:0">@row.Datum[i]</div>
|
||||
var data = row.Datum[i];
|
||||
<div class="mt-5 mb-5 text-truncate" style="min-width:0">
|
||||
@if (data.Type == ColumnType.Text)
|
||||
{
|
||||
<span>@data.Value</span>
|
||||
}
|
||||
@if (data.Type == ColumnType.Link)
|
||||
{
|
||||
<a href="@data.Data">@data.Value</a>
|
||||
}
|
||||
@if (data.Type == ColumnType.Icon)
|
||||
{
|
||||
<span class="oi @data.Value profile-action" data-action="@data.Data" data-action-id="@data.Id"></span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
|
@ -119,11 +119,13 @@
|
||||
</div>
|
||||
|
||||
<div class="d-flex ml-auto">
|
||||
<div class="align-self-center">
|
||||
@await Html.PartialAsync("Partials/_Notifications", (object)ViewBag.Alerts)
|
||||
</div>
|
||||
<div class="btn btn-action mr-10 ml-10" onclick="halfmoon.toggleDarkMode()" data-toggle="tooltip" data-title="Toggle display mode" data-placement="bottom">
|
||||
<i class="oi oi-moon"></i>
|
||||
</div>
|
||||
<div class="d-none d-md-block ">
|
||||
|
||||
<partial name="_SearchResourceForm"/>
|
||||
</div>
|
||||
<div class="d-flex d-lg-none">
|
||||
|
@ -115,30 +115,35 @@
|
||||
<span class="name">@ViewBag.Localization["WEBFRONT_NAV_CONSOLE"]</span>
|
||||
</a>
|
||||
</has-permission>
|
||||
@if (ViewBag.User.Level >= EFClient.Permission.Owner)
|
||||
{
|
||||
<a asp-controller="Configuration" asp-action="Edit" class="sidebar-link">
|
||||
<i class="oi oi-cog mr-5"></i>
|
||||
<span class="name">Configuration</span>
|
||||
</a>
|
||||
}
|
||||
<has-permission entity="AuditPage" required-permission="Read">
|
||||
<a asp-controller="Admin" asp-action="AuditLog" class="sidebar-link">
|
||||
<i class="oi oi-book mr-5"></i>
|
||||
<span class="name">@ViewBag.Localization["WEBFRONT_NAV_AUDIT_LOG"]</span>
|
||||
</a>
|
||||
</has-permission>
|
||||
<has-permission entity="RecentPlayersPage" required-permission="Read">
|
||||
<a class="sidebar-link profile-action" href="#actionModal" data-action="RecentClients" title="@ViewBag.Localization["WEBFRONT_ACTION_RECENT_CLIENTS"]">
|
||||
<i class="oi oi-timer mr-5"></i>
|
||||
<span class="name">@ViewBag.Localization["WEBFRONT_ACTION_RECENT_CLIENTS"]</span>
|
||||
</a>
|
||||
</has-permission>
|
||||
<a class="sidebar-link profile-action" href="#actionModal" data-action="GenerateLoginToken" data-response-duration="30000" title="@ViewBag.Localization["WEBFRONT_ACTION_TOKEN"]">
|
||||
<i class="oi oi-key mr-5"></i>
|
||||
<span class="name">@ViewBag.Localization["WEBFRONT_ACTION_TOKEN"]</span>
|
||||
<has-permission entity="Penalty" required-permission="Read"></has-permission>
|
||||
<a asp-controller="Admin" asp-action="BanManagement" class="sidebar-link">
|
||||
<i class="oi oi-ban mr-5"></i>
|
||||
<span class="name">Ban Management</span>
|
||||
</a>
|
||||
</has-permission>
|
||||
@if (ViewBag.User.Level >= EFClient.Permission.Owner)
|
||||
{
|
||||
<a asp-controller="Configuration" asp-action="Edit" class="sidebar-link">
|
||||
<i class="oi oi-cog mr-5"></i>
|
||||
<span class="name">Configuration</span>
|
||||
</a>
|
||||
}
|
||||
<has-permission entity="AuditPage" required-permission="Read">
|
||||
<a asp-controller="Admin" asp-action="AuditLog" class="sidebar-link">
|
||||
<i class="oi oi-book mr-5"></i>
|
||||
<span class="name">@ViewBag.Localization["WEBFRONT_NAV_AUDIT_LOG"]</span>
|
||||
</a>
|
||||
</has-permission>
|
||||
<has-permission entity="RecentPlayersPage" required-permission="Read">
|
||||
<a class="sidebar-link profile-action" href="#actionModal" data-action="RecentClients" title="@ViewBag.Localization["WEBFRONT_ACTION_RECENT_CLIENTS"]">
|
||||
<i class="oi oi-timer mr-5"></i>
|
||||
<span class="name">@ViewBag.Localization["WEBFRONT_ACTION_RECENT_CLIENTS"]</span>
|
||||
</a>
|
||||
</has-permission>
|
||||
<a class="sidebar-link profile-action" href="#actionModal" data-action="GenerateLoginToken" data-response-duration="30000" title="@ViewBag.Localization["WEBFRONT_ACTION_TOKEN"]">
|
||||
<i class="oi oi-key mr-5"></i>
|
||||
<span class="name">@ViewBag.Localization["WEBFRONT_ACTION_TOKEN"]</span>
|
||||
</a>
|
||||
@if (ViewBag.Authorized)
|
||||
{
|
||||
<a asp-controller="Account" asp-action="Logout" class="sidebar-link">
|
||||
|
@ -1,7 +1,7 @@
|
||||
@model WebfrontCore.ViewModels.SideContextMenuItems
|
||||
@{ Layout = null; }
|
||||
|
||||
<div class="d-none d-lg-flex col-3 col-xl-2">
|
||||
<div class="d-none d-lg-flex col-3">
|
||||
<div class="content mt-0">
|
||||
<div class="on-this-page-nav pt-0">
|
||||
<div class="title">@Model.MenuTitle</div>
|
||||
|
@ -150,7 +150,7 @@ function refreshClientActivity(serverId) {
|
||||
|
||||
$(document).ready(function () {
|
||||
$('.server-join-button').click(function (e) {
|
||||
$(this).children('.server-header-ip-address').show();
|
||||
$(this).parent().parent().find('.server-header-ip-address').show();
|
||||
});
|
||||
|
||||
$('.server-history-row').each(function (index, element) {
|
||||
|
Reference in New Issue
Block a user