Compare commits
22 Commits
2022.01.27
...
2022.02.02
Author | SHA1 | Date | |
---|---|---|---|
b7a76cc4a2 | |||
261da918c7 | |||
2ed5e00bcb | |||
6ca94f8da8 | |||
3b532cf1f7 | |||
40966ed74d | |||
45eacabc28 | |||
0b02b7627a | |||
fc3a24ca17 | |||
209cb6cdd0 | |||
cfd4296f5c | |||
b275fbaced | |||
b2a3625288 | |||
0d3e2cb0bc | |||
505a2c4c2d | |||
8730a3fab8 | |||
3539101a40 | |||
7ccdee7d1b | |||
f4b160b735 | |||
73036dc1c7 | |||
6cfcce23cc | |||
8649b0efe9 |
@ -355,10 +355,10 @@ namespace IW4MAdmin.Application
|
||||
// copy over default config if it doesn't exist
|
||||
if (!_appConfig.Servers?.Any() ?? true)
|
||||
{
|
||||
var defaultConfig = new BaseConfigurationHandler<DefaultSettings>("DefaultSettings").Configuration();
|
||||
//ConfigHandler.Set((ApplicationConfiguration)new ApplicationConfiguration().Generate());
|
||||
//var newConfig = ConfigHandler.Configuration();
|
||||
|
||||
var defaultHandler = new BaseConfigurationHandler<DefaultSettings>("DefaultSettings");
|
||||
await defaultHandler.BuildAsync();
|
||||
var defaultConfig = defaultHandler.Configuration();
|
||||
|
||||
_appConfig.AutoMessages = defaultConfig.AutoMessages;
|
||||
_appConfig.GlobalRules = defaultConfig.GlobalRules;
|
||||
_appConfig.DisallowedClientNames = defaultConfig.DisallowedClientNames;
|
||||
|
@ -47,6 +47,10 @@ echo making start scripts
|
||||
@(echo @echo off && echo @title IW4MAdmin && echo set DOTNET_CLI_TELEMETRY_OPTOUT=1 && echo dotnet Lib\IW4MAdmin.dll && echo pause) > "%PublishDir%\StartIW4MAdmin.cmd"
|
||||
@(echo #!/bin/bash&& echo export DOTNET_CLI_TELEMETRY_OPTOUT=1&& echo dotnet Lib/IW4MAdmin.dll) > "%PublishDir%\StartIW4MAdmin.sh"
|
||||
|
||||
echo copying update scripts
|
||||
copy "%SourceDir%\DeploymentFiles\UpdateIW4MAdmin.ps1" "%PublishDir%\UpdateIW4MAdmin.ps1"
|
||||
copy "%SourceDir%\DeploymentFiles\UpdateIW4MAdmin.sh" "%PublishDir%\UpdateIW4MAdmin.sh"
|
||||
|
||||
echo moving front-end library dependencies
|
||||
if not exist "%PublishDir%\wwwroot\font" mkdir "%PublishDir%\wwwroot\font"
|
||||
move "WebfrontCore\wwwroot\lib\open-iconic\font\fonts\*.*" "%PublishDir%\wwwroot\font\"
|
||||
|
@ -94,7 +94,7 @@ namespace IW4MAdmin.Application.Extensions
|
||||
postgresqlOptions =>
|
||||
{
|
||||
postgresqlOptions.EnableRetryOnFailure();
|
||||
postgresqlOptions.SetPostgresVersion(new Version("9.4"));
|
||||
postgresqlOptions.SetPostgresVersion(new Version("12.9"));
|
||||
})
|
||||
.UseLoggerFactory(sp.GetRequiredService<ILoggerFactory>()).Options);
|
||||
return services;
|
||||
|
@ -1,4 +1,5 @@
|
||||
using IW4MAdmin.Application.Misc;
|
||||
using System.Threading.Tasks;
|
||||
using IW4MAdmin.Application.Misc;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
|
||||
namespace IW4MAdmin.Application.Factories
|
||||
@ -17,7 +18,17 @@ namespace IW4MAdmin.Application.Factories
|
||||
/// <returns></returns>
|
||||
public IConfigurationHandler<T> GetConfigurationHandler<T>(string name) where T : IBaseConfiguration
|
||||
{
|
||||
return new BaseConfigurationHandler<T>(name);
|
||||
var handler = new BaseConfigurationHandler<T>(name);
|
||||
handler.BuildAsync().Wait();
|
||||
return handler;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<IConfigurationHandler<T>> GetConfigurationHandlerAsync<T>(string name) where T : IBaseConfiguration
|
||||
{
|
||||
var handler = new BaseConfigurationHandler<T>(name);
|
||||
await handler.BuildAsync();
|
||||
return handler;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
@ -25,7 +26,6 @@ using Data.Models;
|
||||
using Data.Models.Server;
|
||||
using IW4MAdmin.Application.Commands;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore.Formatting;
|
||||
using static Data.Models.Client.EFClient;
|
||||
|
||||
namespace IW4MAdmin
|
||||
@ -355,7 +355,7 @@ namespace IW4MAdmin
|
||||
try
|
||||
{
|
||||
var factory = _serviceProvider.GetRequiredService<IDatabaseContextFactory>();
|
||||
await using var context = factory.CreateContext();
|
||||
await using var context = factory.CreateContext(enableTracking: false);
|
||||
|
||||
var messageCount = await context.InboxMessages
|
||||
.CountAsync(msg => msg.DestinationClientId == E.Origin.ClientId && !msg.IsDelivered);
|
||||
@ -1076,19 +1076,26 @@ namespace IW4MAdmin
|
||||
{
|
||||
try
|
||||
{
|
||||
ResolvedIpEndPoint = new IPEndPoint((await Dns.GetHostAddressesAsync(IP)).First(), Port);
|
||||
ResolvedIpEndPoint =
|
||||
new IPEndPoint(
|
||||
(await Dns.GetHostAddressesAsync(IP)).First(address =>
|
||||
address.AddressFamily == AddressFamily.InterNetwork), Port);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ServerLogger.LogWarning(ex, "Could not resolve hostname or IP for RCon connection {IP}:{Port}", IP, Port);
|
||||
ResolvedIpEndPoint = new IPEndPoint(IPAddress.Parse(IP), Port);
|
||||
}
|
||||
|
||||
|
||||
RconParser = Manager.AdditionalRConParsers
|
||||
.FirstOrDefault(_parser => _parser.Version == ServerConfig.RConParserVersion);
|
||||
.FirstOrDefault(parser =>
|
||||
parser.Version == ServerConfig.RConParserVersion ||
|
||||
parser.Name == ServerConfig.RConParserVersion);
|
||||
|
||||
EventParser = Manager.AdditionalEventParsers
|
||||
.FirstOrDefault(_parser => _parser.Version == ServerConfig.EventParserVersion);
|
||||
.FirstOrDefault(parser =>
|
||||
parser.Version == ServerConfig.EventParserVersion ||
|
||||
parser.Name == ServerConfig.RConParserVersion);
|
||||
|
||||
RconParser ??= Manager.AdditionalRConParsers[0];
|
||||
EventParser ??= Manager.AdditionalEventParsers[0];
|
||||
@ -1105,7 +1112,7 @@ namespace IW4MAdmin
|
||||
GameName = RconParser.GameName;
|
||||
}
|
||||
|
||||
if (version?.Value?.Length != 0)
|
||||
if (version.Value?.Length != 0)
|
||||
{
|
||||
var matchedRconParser = Manager.AdditionalRConParsers.FirstOrDefault(_parser => _parser.Version == version.Value);
|
||||
RconParser.Configuration = matchedRconParser != null ? matchedRconParser.Configuration : RconParser.Configuration;
|
||||
|
@ -34,6 +34,7 @@ using IW4MAdmin.Plugins.Stats.Client.Abstractions;
|
||||
using IW4MAdmin.Plugins.Stats.Client;
|
||||
using Stats.Client.Abstractions;
|
||||
using Stats.Client;
|
||||
using Stats.Config;
|
||||
using Stats.Helpers;
|
||||
|
||||
namespace IW4MAdmin.Application
|
||||
@ -41,9 +42,9 @@ namespace IW4MAdmin.Application
|
||||
public class Program
|
||||
{
|
||||
public static BuildNumber Version { get; } = BuildNumber.Parse(Utilities.GetVersionAsString());
|
||||
public static ApplicationManager ServerManager;
|
||||
private static Task ApplicationTask;
|
||||
private static ServiceProvider serviceProvider;
|
||||
private static ApplicationManager _serverManager;
|
||||
private static Task _applicationTask;
|
||||
private static ServiceProvider _serviceProvider;
|
||||
|
||||
/// <summary>
|
||||
/// entrypoint of the application
|
||||
@ -75,10 +76,10 @@ namespace IW4MAdmin.Application
|
||||
/// <param name="e"></param>
|
||||
private static async void OnCancelKey(object sender, ConsoleCancelEventArgs e)
|
||||
{
|
||||
ServerManager?.Stop();
|
||||
if (ApplicationTask != null)
|
||||
_serverManager?.Stop();
|
||||
if (_applicationTask != null)
|
||||
{
|
||||
await ApplicationTask;
|
||||
await _applicationTask;
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,7 +93,6 @@ namespace IW4MAdmin.Application
|
||||
ITranslationLookup translationLookup = null;
|
||||
var logger = BuildDefaultLogger<Program>(new ApplicationConfiguration());
|
||||
Utilities.DefaultLogger = logger;
|
||||
IServiceCollection services = null;
|
||||
logger.LogInformation("Begin IW4MAdmin startup. Version is {Version} {@Args}", Version, args);
|
||||
|
||||
try
|
||||
@ -102,18 +102,18 @@ namespace IW4MAdmin.Application
|
||||
ConfigurationMigration.CheckDirectories();
|
||||
ConfigurationMigration.RemoveObsoletePlugins20210322();
|
||||
logger.LogDebug("Configuring services...");
|
||||
services = ConfigureServices(args);
|
||||
serviceProvider = services.BuildServiceProvider();
|
||||
var versionChecker = serviceProvider.GetRequiredService<IMasterCommunication>();
|
||||
ServerManager = (ApplicationManager) serviceProvider.GetRequiredService<IManager>();
|
||||
translationLookup = serviceProvider.GetRequiredService<ITranslationLookup>();
|
||||
var services = await ConfigureServices(args);
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
var versionChecker = _serviceProvider.GetRequiredService<IMasterCommunication>();
|
||||
_serverManager = (ApplicationManager) _serviceProvider.GetRequiredService<IManager>();
|
||||
translationLookup = _serviceProvider.GetRequiredService<ITranslationLookup>();
|
||||
|
||||
ApplicationTask = RunApplicationTasksAsync(logger, services);
|
||||
_applicationTask = RunApplicationTasksAsync(logger, services);
|
||||
var tasks = new[]
|
||||
{
|
||||
versionChecker.CheckVersion(),
|
||||
ServerManager.Init(),
|
||||
ApplicationTask
|
||||
_serverManager.Init(),
|
||||
_applicationTask
|
||||
};
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
@ -160,12 +160,12 @@ namespace IW4MAdmin.Application
|
||||
return;
|
||||
}
|
||||
|
||||
if (ServerManager.IsRestartRequested)
|
||||
if (_serverManager.IsRestartRequested)
|
||||
{
|
||||
goto restart;
|
||||
}
|
||||
|
||||
await serviceProvider.DisposeAsync();
|
||||
await _serviceProvider.DisposeAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -174,11 +174,11 @@ namespace IW4MAdmin.Application
|
||||
/// <returns></returns>
|
||||
private static async Task RunApplicationTasksAsync(ILogger logger, IServiceCollection services)
|
||||
{
|
||||
var webfrontTask = ServerManager.GetApplicationSettings().Configuration().EnableWebFront
|
||||
? WebfrontCore.Program.Init(ServerManager, serviceProvider, services, ServerManager.CancellationToken)
|
||||
var webfrontTask = _serverManager.GetApplicationSettings().Configuration().EnableWebFront
|
||||
? WebfrontCore.Program.Init(_serverManager, _serviceProvider, services, _serverManager.CancellationToken)
|
||||
: Task.CompletedTask;
|
||||
|
||||
var collectionService = serviceProvider.GetRequiredService<IServerDataCollector>();
|
||||
var collectionService = _serviceProvider.GetRequiredService<IServerDataCollector>();
|
||||
|
||||
// we want to run this one on a manual thread instead of letting the thread pool handle it,
|
||||
// because we can't exit early from waiting on console input, and it prevents us from restarting
|
||||
@ -190,10 +190,10 @@ namespace IW4MAdmin.Application
|
||||
var tasks = new[]
|
||||
{
|
||||
webfrontTask,
|
||||
ServerManager.Start(),
|
||||
serviceProvider.GetRequiredService<IMasterCommunication>()
|
||||
.RunUploadStatus(ServerManager.CancellationToken),
|
||||
collectionService.BeginCollectionAsync(cancellationToken: ServerManager.CancellationToken)
|
||||
_serverManager.Start(),
|
||||
_serviceProvider.GetRequiredService<IMasterCommunication>()
|
||||
.RunUploadStatus(_serverManager.CancellationToken),
|
||||
collectionService.BeginCollectionAsync(cancellationToken: _serverManager.CancellationToken)
|
||||
};
|
||||
|
||||
logger.LogDebug("Starting webfront and input tasks");
|
||||
@ -215,14 +215,19 @@ namespace IW4MAdmin.Application
|
||||
return;
|
||||
}
|
||||
|
||||
string lastCommand;
|
||||
EFClient origin = null;
|
||||
|
||||
try
|
||||
{
|
||||
while (!ServerManager.CancellationToken.IsCancellationRequested)
|
||||
while (!_serverManager.CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
lastCommand = await Console.In.ReadLineAsync();
|
||||
if (!_serverManager.IsInitialized)
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
var lastCommand = await Console.In.ReadLineAsync();
|
||||
|
||||
if (lastCommand == null)
|
||||
{
|
||||
@ -238,12 +243,12 @@ namespace IW4MAdmin.Application
|
||||
{
|
||||
Type = GameEvent.EventType.Command,
|
||||
Data = lastCommand,
|
||||
Origin = origin ??= Utilities.IW4MAdminClient(ServerManager.Servers.FirstOrDefault()),
|
||||
Owner = ServerManager.Servers[0]
|
||||
Origin = origin ??= Utilities.IW4MAdminClient(_serverManager.Servers.FirstOrDefault()),
|
||||
Owner = _serverManager.Servers[0]
|
||||
};
|
||||
|
||||
ServerManager.AddEvent(gameEvent);
|
||||
await gameEvent.WaitAsync(Utilities.DefaultCommandTimeout, ServerManager.CancellationToken);
|
||||
_serverManager.AddEvent(gameEvent);
|
||||
await gameEvent.WaitAsync(Utilities.DefaultCommandTimeout, _serverManager.CancellationToken);
|
||||
Console.Write('>');
|
||||
}
|
||||
}
|
||||
@ -273,9 +278,9 @@ namespace IW4MAdmin.Application
|
||||
// register the native commands
|
||||
foreach (var commandType in typeof(SharedLibraryCore.Commands.QuitCommand).Assembly.GetTypes()
|
||||
.Concat(typeof(Program).Assembly.GetTypes().Where(type => type.Namespace == "IW4MAdmin.Application.Commands"))
|
||||
.Where(_command => _command.BaseType == typeof(Command)))
|
||||
.Where(command => command.BaseType == typeof(Command)))
|
||||
{
|
||||
defaultLogger.LogDebug("Registered native command type {name}", commandType.Name);
|
||||
defaultLogger.LogDebug("Registered native command type {Name}", commandType.Name);
|
||||
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
|
||||
}
|
||||
|
||||
@ -283,23 +288,23 @@ namespace IW4MAdmin.Application
|
||||
var (plugins, commands, configurations) = pluginImporter.DiscoverAssemblyPluginImplementations();
|
||||
foreach (var pluginType in plugins)
|
||||
{
|
||||
defaultLogger.LogDebug("Registered plugin type {name}", pluginType.FullName);
|
||||
defaultLogger.LogDebug("Registered plugin type {Name}", pluginType.FullName);
|
||||
serviceCollection.AddSingleton(typeof(IPlugin), pluginType);
|
||||
}
|
||||
|
||||
// register the plugin commands
|
||||
foreach (var commandType in commands)
|
||||
{
|
||||
defaultLogger.LogDebug("Registered plugin command type {name}", commandType.FullName);
|
||||
defaultLogger.LogDebug("Registered plugin command type {Name}", commandType.FullName);
|
||||
serviceCollection.AddSingleton(typeof(IManagerCommand), commandType);
|
||||
}
|
||||
|
||||
foreach (var configurationType in configurations)
|
||||
{
|
||||
defaultLogger.LogDebug("Registered plugin config type {name}", configurationType.Name);
|
||||
defaultLogger.LogDebug("Registered plugin config type {Name}", configurationType.Name);
|
||||
var configInstance = (IBaseConfiguration) Activator.CreateInstance(configurationType);
|
||||
var handlerType = typeof(BaseConfigurationHandler<>).MakeGenericType(configurationType);
|
||||
var handlerInstance = Activator.CreateInstance(handlerType, new[] {configInstance.Name()});
|
||||
var handlerInstance = Activator.CreateInstance(handlerType, configInstance.Name());
|
||||
var genericInterfaceType = typeof(IConfigurationHandler<>).MakeGenericType(configurationType);
|
||||
|
||||
serviceCollection.AddSingleton(genericInterfaceType, handlerInstance);
|
||||
@ -313,10 +318,10 @@ namespace IW4MAdmin.Application
|
||||
|
||||
// register any eventable types
|
||||
foreach (var assemblyType in typeof(Program).Assembly.GetTypes()
|
||||
.Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType))
|
||||
.Union(plugins.SelectMany(_asm => _asm.Assembly.GetTypes())
|
||||
.Where(asmType => typeof(IRegisterEvent).IsAssignableFrom(asmType))
|
||||
.Union(plugins.SelectMany(asm => asm.Assembly.GetTypes())
|
||||
.Distinct()
|
||||
.Where(_asmType => typeof(IRegisterEvent).IsAssignableFrom(_asmType))))
|
||||
.Where(asmType => typeof(IRegisterEvent).IsAssignableFrom(asmType))))
|
||||
{
|
||||
var instance = Activator.CreateInstance(assemblyType) as IRegisterEvent;
|
||||
serviceCollection.AddSingleton(instance);
|
||||
@ -329,7 +334,7 @@ namespace IW4MAdmin.Application
|
||||
/// <summary>
|
||||
/// Configures the dependency injection services
|
||||
/// </summary>
|
||||
private static IServiceCollection ConfigureServices(string[] args)
|
||||
private static async Task<IServiceCollection> ConfigureServices(string[] args)
|
||||
{
|
||||
// todo: this is a quick fix
|
||||
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
||||
@ -337,7 +342,13 @@ namespace IW4MAdmin.Application
|
||||
// setup the static resources (config/master api/translations)
|
||||
var serviceCollection = new ServiceCollection();
|
||||
var appConfigHandler = new BaseConfigurationHandler<ApplicationConfiguration>("IW4MAdminSettings");
|
||||
await appConfigHandler.BuildAsync();
|
||||
var defaultConfigHandler = new BaseConfigurationHandler<DefaultSettings>("DefaultSettings");
|
||||
await defaultConfigHandler.BuildAsync();
|
||||
var commandConfigHandler = new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration");
|
||||
await commandConfigHandler.BuildAsync();
|
||||
var statsCommandHandler = new BaseConfigurationHandler<StatsConfiguration>();
|
||||
await statsCommandHandler.BuildAsync();
|
||||
var defaultConfig = defaultConfigHandler.Configuration();
|
||||
var appConfig = appConfigHandler.Configuration();
|
||||
var masterUri = Utilities.IsDevelopment
|
||||
@ -355,7 +366,7 @@ namespace IW4MAdmin.Application
|
||||
{
|
||||
appConfig = (ApplicationConfiguration) new ApplicationConfiguration().Generate();
|
||||
appConfigHandler.Set(appConfig);
|
||||
appConfigHandler.Save().RunSynchronously();
|
||||
await appConfigHandler.Save();
|
||||
}
|
||||
|
||||
// register override level names
|
||||
@ -373,15 +384,14 @@ namespace IW4MAdmin.Application
|
||||
serviceCollection
|
||||
.AddBaseLogger(appConfig)
|
||||
.AddSingleton(defaultConfig)
|
||||
.AddSingleton<IServiceCollection>(_serviceProvider => serviceCollection)
|
||||
.AddSingleton<IServiceCollection>(serviceCollection)
|
||||
.AddSingleton<IConfigurationHandler<DefaultSettings>, BaseConfigurationHandler<DefaultSettings>>()
|
||||
.AddSingleton((IConfigurationHandler<ApplicationConfiguration>) appConfigHandler)
|
||||
.AddSingleton(
|
||||
new BaseConfigurationHandler<CommandConfiguration>("CommandConfiguration") as
|
||||
IConfigurationHandler<CommandConfiguration>)
|
||||
.AddSingleton<IConfigurationHandler<CommandConfiguration>>(commandConfigHandler)
|
||||
.AddSingleton(appConfig)
|
||||
.AddSingleton(_serviceProvider =>
|
||||
_serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>()
|
||||
.AddSingleton(statsCommandHandler.Configuration() ?? new StatsConfiguration())
|
||||
.AddSingleton(serviceProvider =>
|
||||
serviceProvider.GetRequiredService<IConfigurationHandler<CommandConfiguration>>()
|
||||
.Configuration() ?? new CommandConfiguration())
|
||||
.AddSingleton<IPluginImporter, PluginImporter>()
|
||||
.AddSingleton<IMiddlewareActionHandler, MiddlewareActionHandler>()
|
||||
|
@ -1,11 +1,13 @@
|
||||
using Newtonsoft.Json;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Exceptions;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||
|
||||
namespace IW4MAdmin.Application.Misc
|
||||
{
|
||||
@ -15,19 +17,24 @@ namespace IW4MAdmin.Application.Misc
|
||||
/// <typeparam name="T">base configuration type</typeparam>
|
||||
public class BaseConfigurationHandler<T> : IConfigurationHandler<T> where T : IBaseConfiguration
|
||||
{
|
||||
T _configuration;
|
||||
private T _configuration;
|
||||
private readonly SemaphoreSlim _onSaving;
|
||||
private readonly JsonSerializerOptions _serializerOptions;
|
||||
|
||||
public BaseConfigurationHandler(string fn)
|
||||
|
||||
public BaseConfigurationHandler(string fileName)
|
||||
{
|
||||
_serializerOptions = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
};
|
||||
_serializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
_onSaving = new SemaphoreSlim(1, 1);
|
||||
FileName = Path.Join(Utilities.OperatingDirectory, "Configuration", $"{fn}.json");
|
||||
Build();
|
||||
FileName = Path.Join(Utilities.OperatingDirectory, "Configuration", $"{fileName}.json");
|
||||
}
|
||||
|
||||
public BaseConfigurationHandler() : this(typeof(T).Name)
|
||||
{
|
||||
_onSaving = new SemaphoreSlim(1, 1);
|
||||
}
|
||||
|
||||
~BaseConfigurationHandler()
|
||||
@ -37,12 +44,12 @@ namespace IW4MAdmin.Application.Misc
|
||||
|
||||
public string FileName { get; }
|
||||
|
||||
public void Build()
|
||||
public async Task BuildAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var configContent = File.ReadAllText(FileName);
|
||||
_configuration = JsonConvert.DeserializeObject<T>(configContent);
|
||||
await using var fileStream = File.OpenRead(FileName);
|
||||
_configuration = await JsonSerializer.DeserializeAsync<T>(fileStream, _serializerOptions);
|
||||
}
|
||||
|
||||
catch (FileNotFoundException)
|
||||
@ -65,14 +72,9 @@ namespace IW4MAdmin.Application.Misc
|
||||
try
|
||||
{
|
||||
await _onSaving.WaitAsync();
|
||||
var settings = new JsonSerializerSettings()
|
||||
{
|
||||
Formatting = Formatting.Indented
|
||||
};
|
||||
settings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter());
|
||||
|
||||
var appConfigJson = JsonConvert.SerializeObject(_configuration, settings);
|
||||
await File.WriteAllTextAsync(FileName, appConfigJson);
|
||||
await using var fileStream = File.Create(FileName);
|
||||
await JsonSerializer.SerializeAsync(fileStream, _configuration, _serializerOptions);
|
||||
}
|
||||
|
||||
finally
|
||||
|
@ -26,7 +26,8 @@ namespace IW4MAdmin.Application.Misc
|
||||
private readonly ApplicationConfiguration _appConfig;
|
||||
private readonly BuildNumber _fallbackVersion = BuildNumber.Parse("99.99.99.99");
|
||||
private readonly int _apiVersion = 1;
|
||||
private bool firstHeartBeat = true;
|
||||
private bool _firstHeartBeat = true;
|
||||
private static readonly TimeSpan Interval = TimeSpan.FromSeconds(30);
|
||||
|
||||
public MasterCommunication(ILogger<MasterCommunication> logger, ApplicationConfiguration appConfig, ITranslationLookup translationLookup, IMasterApi apiInstance, IManager manager)
|
||||
{
|
||||
@ -97,7 +98,10 @@ namespace IW4MAdmin.Application.Misc
|
||||
{
|
||||
try
|
||||
{
|
||||
await UploadStatus();
|
||||
if (_manager.IsRunning)
|
||||
{
|
||||
await UploadStatus();
|
||||
}
|
||||
}
|
||||
|
||||
catch (Exception ex)
|
||||
@ -107,7 +111,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
|
||||
try
|
||||
{
|
||||
await Task.Delay(30000, token);
|
||||
await Task.Delay(Interval, token);
|
||||
}
|
||||
|
||||
catch
|
||||
@ -119,7 +123,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
|
||||
private async Task UploadStatus()
|
||||
{
|
||||
if (firstHeartBeat)
|
||||
if (_firstHeartBeat)
|
||||
{
|
||||
var token = await _apiInstance.Authenticate(new AuthenticationId
|
||||
{
|
||||
@ -153,7 +157,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
|
||||
Response<ResultMessage> response = null;
|
||||
|
||||
if (firstHeartBeat)
|
||||
if (_firstHeartBeat)
|
||||
{
|
||||
response = await _apiInstance.AddInstance(instance);
|
||||
}
|
||||
@ -161,7 +165,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
else
|
||||
{
|
||||
response = await _apiInstance.UpdateInstance(instance.Id, instance);
|
||||
firstHeartBeat = false;
|
||||
_firstHeartBeat = false;
|
||||
}
|
||||
|
||||
if (response.ResponseMessage.StatusCode != System.Net.HttpStatusCode.OK)
|
||||
|
@ -207,42 +207,30 @@ namespace IW4MAdmin.Application.Misc
|
||||
|
||||
public async Task<IEnumerable<IClientMeta>> GetRuntimeMeta(ClientPaginationRequest request)
|
||||
{
|
||||
var meta = new List<IClientMeta>();
|
||||
var metas = await Task.WhenAll(_metaActions.Where(kvp => kvp.Key != MetaType.Information)
|
||||
.Select(async kvp => await kvp.Value[0](request)));
|
||||
|
||||
foreach (var (type, actions) in _metaActions)
|
||||
{
|
||||
// information is not listed chronologically
|
||||
if (type != MetaType.Information)
|
||||
{
|
||||
var metaItems = await actions[0](request);
|
||||
meta.AddRange(metaItems);
|
||||
}
|
||||
}
|
||||
|
||||
return meta.OrderByDescending(_meta => _meta.When)
|
||||
return metas.SelectMany(m => (IEnumerable<IClientMeta>)m)
|
||||
.OrderByDescending(m => m.When)
|
||||
.Take(request.Count)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<T>> GetRuntimeMeta<T>(ClientPaginationRequest request, MetaType metaType) where T : IClientMeta
|
||||
{
|
||||
IEnumerable<T> meta;
|
||||
if (metaType == MetaType.Information)
|
||||
{
|
||||
var allMeta = new List<T>();
|
||||
|
||||
foreach (var individualMetaRegistration in _metaActions[metaType])
|
||||
{
|
||||
allMeta.AddRange(await individualMetaRegistration(request));
|
||||
}
|
||||
|
||||
var completedMeta = await Task.WhenAll(_metaActions[metaType].Select(async individualMetaRegistration =>
|
||||
(IEnumerable<T>)await individualMetaRegistration(request)));
|
||||
|
||||
allMeta.AddRange(completedMeta.SelectMany(meta => meta));
|
||||
|
||||
return ProcessInformationMeta(allMeta);
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
meta = await _metaActions[metaType][0](request) as IEnumerable<T>;
|
||||
}
|
||||
var meta = await _metaActions[metaType][0](request) as IEnumerable<T>;
|
||||
|
||||
return meta;
|
||||
}
|
||||
|
@ -168,22 +168,26 @@ namespace IW4MAdmin.Application.Misc
|
||||
}
|
||||
}
|
||||
|
||||
_scriptEngine.SetValue("_configHandler", new ScriptPluginConfigurationWrapper(Name, _scriptEngine));
|
||||
await OnLoadAsync(manager);
|
||||
|
||||
try
|
||||
{
|
||||
if (pluginObject.isParser)
|
||||
{
|
||||
await OnLoadAsync(manager);
|
||||
IsParser = true;
|
||||
IEventParser eventParser = (IEventParser)_scriptEngine.GetValue("eventParser").ToObject();
|
||||
IRConParser rconParser = (IRConParser)_scriptEngine.GetValue("rconParser").ToObject();
|
||||
var eventParser = (IEventParser)_scriptEngine.GetValue("eventParser").ToObject();
|
||||
var rconParser = (IRConParser)_scriptEngine.GetValue("rconParser").ToObject();
|
||||
manager.AdditionalEventParsers.Add(eventParser);
|
||||
manager.AdditionalRConParsers.Add(rconParser);
|
||||
}
|
||||
}
|
||||
|
||||
catch (RuntimeBinderException) { }
|
||||
catch (RuntimeBinderException)
|
||||
{
|
||||
var configWrapper = new ScriptPluginConfigurationWrapper(Name, _scriptEngine);
|
||||
await configWrapper.InitializeAsync();
|
||||
_scriptEngine.SetValue("_configHandler", configWrapper);
|
||||
await OnLoadAsync(manager);
|
||||
}
|
||||
|
||||
if (!firstRun)
|
||||
{
|
||||
|
@ -12,19 +12,24 @@ namespace IW4MAdmin.Application.Misc
|
||||
public class ScriptPluginConfigurationWrapper
|
||||
{
|
||||
private readonly BaseConfigurationHandler<ScriptPluginConfiguration> _handler;
|
||||
private readonly ScriptPluginConfiguration _config;
|
||||
private ScriptPluginConfiguration _config;
|
||||
private readonly string _pluginName;
|
||||
private readonly Engine _scriptEngine;
|
||||
|
||||
public ScriptPluginConfigurationWrapper(string pluginName, Engine scriptEngine)
|
||||
{
|
||||
_handler = new BaseConfigurationHandler<ScriptPluginConfiguration>("ScriptPluginSettings");
|
||||
_config = _handler.Configuration() ??
|
||||
(ScriptPluginConfiguration) new ScriptPluginConfiguration().Generate();
|
||||
_pluginName = pluginName;
|
||||
_scriptEngine = scriptEngine;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
await _handler.BuildAsync();
|
||||
_config = _handler.Configuration() ??
|
||||
(ScriptPluginConfiguration) new ScriptPluginConfiguration().Generate();
|
||||
}
|
||||
|
||||
private static int? AsInteger(double d)
|
||||
{
|
||||
return int.TryParse(d.ToString(CultureInfo.InvariantCulture), out var parsed) ? parsed : (int?) null;
|
||||
@ -87,4 +92,4 @@ namespace IW4MAdmin.Application.Misc
|
||||
return JsValue.FromObject(_scriptEngine, item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ namespace Data.Context
|
||||
{
|
||||
var link = new EFAliasLink();
|
||||
|
||||
context.Clients.Add(new EFClient()
|
||||
context.Clients.Add(new EFClient
|
||||
{
|
||||
Active = false,
|
||||
Connections = 0,
|
||||
@ -33,7 +33,7 @@ namespace Data.Context
|
||||
Masked = true,
|
||||
NetworkId = 0,
|
||||
AliasLink = link,
|
||||
CurrentAlias = new EFAlias()
|
||||
CurrentAlias = new EFAlias
|
||||
{
|
||||
Link = link,
|
||||
Active = true,
|
||||
@ -46,4 +46,4 @@ namespace Data.Context
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -102,7 +102,7 @@ namespace Data.Helpers
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext();
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
_cachedItems = await context.Set<T>().ToDictionaryAsync(item => item.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@ -111,4 +111,4 @@ namespace Data.Helpers
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,10 +25,10 @@ namespace Data.MigrationContext
|
||||
{
|
||||
optionsBuilder.UseNpgsql(
|
||||
"Host=127.0.0.1;Database=IW4MAdmin_Migration;Username=postgres;Password=password;",
|
||||
options => options.SetPostgresVersion(new Version("9.4")))
|
||||
options => options.SetPostgresVersion(new Version("12.9")))
|
||||
.EnableDetailedErrors(true)
|
||||
.EnableSensitiveDataLogging(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.RegularExpressions;
|
||||
// ReSharper disable CompareOfFloatsByEqualityOperator
|
||||
#pragma warning disable CS0659
|
||||
|
||||
namespace Data.Models
|
||||
{
|
||||
@ -29,9 +30,7 @@ namespace Data.Models
|
||||
return $"({X}, {Y}, {Z})";
|
||||
}
|
||||
|
||||
#pragma warning disable CS0659
|
||||
public override bool Equals(object obj)
|
||||
#pragma warning restore CS0659
|
||||
{
|
||||
if (obj is Vector3 vec)
|
||||
{
|
||||
|
118
DeploymentFiles/UpdateIW4MAdmin.ps1
Normal file
118
DeploymentFiles/UpdateIW4MAdmin.ps1
Normal file
@ -0,0 +1,118 @@
|
||||
param (
|
||||
[Parameter(HelpMessage = "Do not prompt for any user input")]
|
||||
[switch]$Silent = $False,
|
||||
|
||||
[Parameter(HelpMessage = "Clean unneeded files listed in _delete.txt after update")]
|
||||
[switch]$Clean = $False,
|
||||
|
||||
[Parameter(HelpMessage = "Only update releases in the verified stream")]
|
||||
[switch]$Verified = $False,
|
||||
|
||||
[Parameter(HelpMessage = "Directory to install to")]
|
||||
[ValidateScript({
|
||||
if (-Not($_ | Test-Path))
|
||||
{
|
||||
throw "File or folder does not exist"
|
||||
} return $true
|
||||
})]
|
||||
[System.IO.FileInfo]$Directory
|
||||
)
|
||||
|
||||
Write-Output "======================================="
|
||||
Write-Output " IW4MAdmin Updater v1 "
|
||||
Write-Output " by XERXES & RaidMax "
|
||||
Write-Output "======================================="
|
||||
|
||||
$stopwatch = [system.diagnostics.stopwatch]::StartNew()
|
||||
$repoName = "RaidMax/IW4M-Admin"
|
||||
$assetPattern = "IW4MAdmin-20*.zip"
|
||||
|
||||
if ($Verified)
|
||||
{
|
||||
$releasesUri = "https://api.github.com/repos/$repoName/releases/latest"
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
$releasesUri = "https://api.github.com/repos/$repoName/releases"
|
||||
}
|
||||
|
||||
Write-Output "Retrieving latest version info..."
|
||||
|
||||
$releaseInfo = (Invoke-WebRequest $releasesUri | ConvertFrom-Json) | Select -First 1
|
||||
$asset = $releaseInfo.assets | Where-Object name -like $assetPattern | Select -First 1
|
||||
$downloadUri = $asset.browser_download_url
|
||||
$filename = Split-Path $downloadUri -leaf
|
||||
|
||||
Write-Output "The latest version is $( $releaseInfo.tag_name ) released $( $releaseInfo.published_at )"
|
||||
|
||||
if (!$Silent)
|
||||
{
|
||||
$stopwatch.Stop()
|
||||
Write-Warning "All IW4MAdmin files will be updated. Your database and configuration will not be modified. Are you sure you want to continue?" -WarningAction Inquire
|
||||
$stopwatch.Start()
|
||||
}
|
||||
|
||||
Write-Output "Downloading update. This might take a moment..."
|
||||
|
||||
$fileDownload = Invoke-WebRequest -Uri $downloadUri
|
||||
if ($fileDownload.StatusDescription -ne "OK")
|
||||
{
|
||||
throw "Could not update IW4MAdmin. ($fileDownload.StatusDescription)"
|
||||
}
|
||||
|
||||
$remoteHash = $fileDownload.Headers['Content-MD5']
|
||||
$decodedHash = [System.BitConverter]::ToString([System.Convert]::FromBase64String($remoteHash)).replace('-', '')
|
||||
$directoryPath = Get-Location
|
||||
$fullPath = "$directoryPath\$filename"
|
||||
$outputFile = [System.IO.File]::Open($fullPath, 2)
|
||||
$stream = [System.IO.BinaryWriter]::new($outputFile)
|
||||
|
||||
if ($Directory)
|
||||
{
|
||||
$outputDir = $Directory
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
$outputDir = Get-Location
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$stream.Write($fileDownload.Content)
|
||||
}
|
||||
finally
|
||||
{
|
||||
$stream.Dispose()
|
||||
$outputFile.Dispose()
|
||||
}
|
||||
|
||||
$localHash = (Get-FileHash -Path $fullPath -Algorithm MD5).Hash
|
||||
|
||||
if ($localHash -ne $decodedHash)
|
||||
{
|
||||
throw "Failed to update. File hashes don't match!"
|
||||
}
|
||||
|
||||
Write-Output "Extracting $filename to $outputDir"
|
||||
Expand-Archive -Path $fullPath -DestinationPath $outputDir -Force
|
||||
|
||||
if ($Clean)
|
||||
{
|
||||
Write-Output "Running post-update clean..."
|
||||
$DeleteList = Get-Content -Path ./_delete.txt
|
||||
ForEach ($file in $DeleteList)
|
||||
{
|
||||
Write-Output "Deleting $file"
|
||||
Remove-Item -Path $file
|
||||
}
|
||||
}
|
||||
|
||||
Write-Output "Removing temporary files..."
|
||||
Remove-Item -Force $fullPath
|
||||
|
||||
$stopwatch.Stop()
|
||||
$executionTime = [math]::Round($stopwatch.Elapsed.TotalSeconds, 0)
|
||||
|
||||
Write-Output "Update completed successfully in $executionTime seconds!"
|
106
DeploymentFiles/UpdateIW4MAdmin.sh
Normal file
106
DeploymentFiles/UpdateIW4MAdmin.sh
Normal file
@ -0,0 +1,106 @@
|
||||
#!/bin/bash
|
||||
echo "======================================="
|
||||
echo " IW4MAdmin Updater v1 "
|
||||
echo "======================================="
|
||||
|
||||
while getopts scvd: flag
|
||||
do
|
||||
case "${flag}" in
|
||||
s) silent='true';;
|
||||
c) clean='true';;
|
||||
v) verified='true';;
|
||||
d) directory=${OPTARG};;
|
||||
*) exit 1;;
|
||||
esac
|
||||
done
|
||||
|
||||
start=$SECONDS
|
||||
repoName="RaidMax/IW4M-Admin"
|
||||
releaseUri="https://api.github.com/repos/$repoName/releases"
|
||||
|
||||
echo "Retrieving latest version info..."
|
||||
|
||||
if [ ! "$directory" ]
|
||||
then
|
||||
directory=$(pwd)
|
||||
else
|
||||
if [ ! -d "$directory" ]
|
||||
then
|
||||
mkdir "$directory"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$verified" ]
|
||||
then
|
||||
releaseUri="https://api.github.com/repos/$repoName/releases/latest"
|
||||
fi
|
||||
|
||||
releaseInfo=$(curl -s "${releaseUri}")
|
||||
downloadUri=$(echo "$releaseInfo" | grep "browser_download_url" | cut -d '"' -f 4"" | head -n1)
|
||||
publishDate=$(echo "$releaseInfo"| grep "published_at" | cut -d '"' -f 4"" | head -n1)
|
||||
releaseTitle=$(echo "$releaseInfo" | grep "tag_name" | cut -d '"' -f 4"" | head -n1)
|
||||
filename=$(basename $downloadUri)
|
||||
fullpath="$directory/$filename"
|
||||
|
||||
echo "The latest version is $releaseTitle released $publishDate"
|
||||
|
||||
if [[ ! "$silent" ]]
|
||||
then
|
||||
echo -e "\033[33mAll IW4MAdmin files will be updated.\033[0m"
|
||||
echo -e "\033[33mYour database and configuration will not be modified.\033[0m"
|
||||
read -p "Are you sure you want to continue [Y/N]? " -n 1 -r
|
||||
echo
|
||||
if ! [[ $REPLY =~ ^[Yy]$ ]]
|
||||
then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Downloading update. This might take a moment..."
|
||||
|
||||
wget -q "$downloadUri" -O "$fullpath"
|
||||
|
||||
if [[ $? -ne 0 ]]
|
||||
then
|
||||
echo "Could not download update files!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Extracting $filename to $directory"
|
||||
|
||||
unzip -o -q "$fullpath" -d "$directory"
|
||||
|
||||
if [[ $? -ne 0 ]]
|
||||
then
|
||||
echo "Could not extract update files!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$clean" ]]
|
||||
then
|
||||
echo "Running post-update clean..."
|
||||
cat "_delete.txt" | while read -r line || [[ -n $line ]];
|
||||
do
|
||||
rm -f "$directory/$line"
|
||||
if [[ $? -ne 0 ]]
|
||||
then
|
||||
echo "Could not clean $directory/$line!"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
echo "Removing temporary files..."
|
||||
rm -f "$fullpath"
|
||||
|
||||
if [[ $? -ne 0 ]]
|
||||
then
|
||||
echo "Could not remove update files!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chmod +x "$directory/StartIW4MAdmin.sh"
|
||||
chmod +x "$directory/UpdateIW4MAdmin.sh"
|
||||
|
||||
executionTime=$(($SECONDS - start))
|
||||
echo "Update completed successfully in $executionTime seconds!"
|
@ -121,6 +121,7 @@ steps:
|
||||
script: |
|
||||
echo changing to encoding for linux start script
|
||||
dos2unix $(outputFolder)\StartIW4MAdmin.sh
|
||||
dos2unix $(outputFolder)\UpdateIW4MAdmin.sh
|
||||
echo creating website version filename
|
||||
@echo IW4MAdmin-$(Build.BuildNumber) > $(Build.ArtifactStagingDirectory)\version_$(releaseType).txt
|
||||
workingDirectory: '$(Build.Repository.LocalPath)\Application\BuildScripts'
|
||||
|
@ -12,6 +12,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
||||
DeploymentFiles\PostPublish.ps1 = DeploymentFiles\PostPublish.ps1
|
||||
README.md = README.md
|
||||
version.txt = version.txt
|
||||
DeploymentFiles\UpdateIW4MAdmin.ps1 = DeploymentFiles\UpdateIW4MAdmin.ps1
|
||||
DeploymentFiles\UpdateIW4MAdmin.sh = DeploymentFiles\UpdateIW4MAdmin.sh
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharedLibraryCore", "SharedLibraryCore\SharedLibraryCore.csproj", "{AA0541A2-8D51-4AD9-B0AC-3D1F5B162481}"
|
||||
|
@ -10,7 +10,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.1.25.2" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.1.28.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -65,6 +65,7 @@ namespace AutomessageFeed
|
||||
|
||||
public async Task OnLoadAsync(IManager manager)
|
||||
{
|
||||
await _configurationHandler.BuildAsync();
|
||||
if (_configurationHandler.Configuration() == null)
|
||||
{
|
||||
_configurationHandler.Set((Configuration)new Configuration().Generate());
|
||||
|
@ -10,7 +10,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.1.25.2" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.1.28.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -1,29 +1,25 @@
|
||||
using LiveRadar.Configuration;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Newtonsoft.Json;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Dtos;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace LiveRadar.Web.Controllers
|
||||
{
|
||||
public class RadarController : BaseController
|
||||
{
|
||||
private static readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings()
|
||||
{
|
||||
ReferenceLoopHandling = ReferenceLoopHandling.Ignore,
|
||||
ContractResolver = new Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()
|
||||
};
|
||||
|
||||
private readonly IManager _manager;
|
||||
private readonly LiveRadarConfiguration _config;
|
||||
private static LiveRadarConfiguration _config;
|
||||
private readonly IConfigurationHandler<LiveRadarConfiguration> _configurationHandler;
|
||||
|
||||
public RadarController(IManager manager, IConfigurationHandlerFactory configurationHandlerFactory) : base(manager)
|
||||
{
|
||||
_manager = manager;
|
||||
_config = configurationHandlerFactory.GetConfigurationHandler<LiveRadarConfiguration>("LiveRadarConfiguration").Configuration() ?? new LiveRadarConfiguration();
|
||||
_configurationHandler =
|
||||
configurationHandlerFactory.GetConfigurationHandler<LiveRadarConfiguration>("LiveRadarConfiguration");
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -46,7 +42,7 @@ namespace LiveRadar.Web.Controllers
|
||||
|
||||
[HttpGet]
|
||||
[Route("Radar/{serverId}/Map")]
|
||||
public IActionResult Map(long? serverId = null)
|
||||
public async Task<IActionResult> Map(long? serverId = null)
|
||||
{
|
||||
var server = serverId == null ? _manager.GetServers().FirstOrDefault() : _manager.GetServers().FirstOrDefault(_server => _server.EndPoint == serverId);
|
||||
|
||||
@ -54,6 +50,12 @@ namespace LiveRadar.Web.Controllers
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (_config == null)
|
||||
{
|
||||
await _configurationHandler.BuildAsync();
|
||||
_config = _configurationHandler.Configuration() ?? new LiveRadarConfiguration();
|
||||
}
|
||||
|
||||
var map = _config.Maps.FirstOrDefault(_map => _map.Name == server.CurrentMap.Name);
|
||||
|
||||
@ -93,4 +95,4 @@ namespace LiveRadar.Web.Controllers
|
||||
return Ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,7 +23,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.1.25.2" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.1.28.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -103,6 +103,7 @@ namespace LiveRadar
|
||||
|
||||
public async Task OnLoadAsync(IManager manager)
|
||||
{
|
||||
await _configurationHandler.BuildAsync();
|
||||
if (_configurationHandler.Configuration() == null)
|
||||
{
|
||||
_configurationHandler.Set((LiveRadarConfiguration)new LiveRadarConfiguration().Generate());
|
||||
|
@ -3,6 +3,7 @@ using SharedLibraryCore;
|
||||
using System;
|
||||
using System.Linq;
|
||||
// ReSharper disable CompareOfFloatsByEqualityOperator
|
||||
#pragma warning disable CS0659
|
||||
|
||||
namespace LiveRadar
|
||||
{
|
||||
@ -23,9 +24,7 @@ namespace LiveRadar
|
||||
public Vector3 RadianAngles => new Vector3(ViewAngles.X.ToRadians(), ViewAngles.Y.ToRadians(), ViewAngles.Z.ToRadians());
|
||||
public int Id => GetHashCode();
|
||||
|
||||
#pragma warning disable CS0659
|
||||
public override bool Equals(object obj)
|
||||
#pragma warning restore CS0659
|
||||
{
|
||||
if (obj is RadarEvent re)
|
||||
{
|
||||
|
@ -19,7 +19,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.1.25.2" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.1.28.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -76,6 +76,7 @@ namespace IW4MAdmin.Plugins.Login
|
||||
{
|
||||
AuthorizedClients = new ConcurrentDictionary<int, bool>();
|
||||
|
||||
await _configHandler.BuildAsync();
|
||||
if (_configHandler.Configuration() == null)
|
||||
{
|
||||
_configHandler.Set((Configuration)new Configuration().Generate());
|
||||
|
@ -109,6 +109,7 @@ namespace IW4MAdmin.Plugins.ProfanityDeterment
|
||||
|
||||
public async Task OnLoadAsync(IManager manager)
|
||||
{
|
||||
await _configHandler.BuildAsync();
|
||||
if (_configHandler.Configuration() == null)
|
||||
{
|
||||
_configHandler.Set((Configuration)new Configuration().Generate());
|
||||
|
@ -16,7 +16,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.1.25.2" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.1.28.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -48,6 +48,7 @@ namespace Stats.Client
|
||||
await LoadServers();
|
||||
_distributionCache.SetCacheItem((async (set, token) =>
|
||||
{
|
||||
await _configurationHandler.BuildAsync();
|
||||
var validPlayTime = _configurationHandler.Configuration()?.TopPlayersMinPlayTime ?? 3600 * 3;
|
||||
|
||||
var distributions = new Dictionary<long, Extensions.LogParams>();
|
||||
@ -73,6 +74,7 @@ namespace Stats.Client
|
||||
|
||||
_maxZScoreCache.SetCacheItem(async (set, token) =>
|
||||
{
|
||||
await _configurationHandler.BuildAsync();
|
||||
var validPlayTime = _configurationHandler.Configuration()?.TopPlayersMinPlayTime ?? 3600 * 3;
|
||||
|
||||
var zScore = await set
|
||||
|
@ -3,9 +3,7 @@ using Stats.Client.Abstractions;
|
||||
using Stats.Client.Game;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using IW4MAdmin.Plugins.Stats.Config;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using Stats.Config;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
|
||||
@ -16,10 +14,10 @@ namespace Stats.Client
|
||||
private readonly ILogger _logger;
|
||||
private readonly StatsConfiguration _config;
|
||||
|
||||
public WeaponNameParser(ILogger<WeaponNameParser> logger, IConfigurationHandler<StatsConfiguration> config)
|
||||
public WeaponNameParser(ILogger<WeaponNameParser> logger, StatsConfiguration config)
|
||||
{
|
||||
_logger = logger;
|
||||
_config = config.Configuration();
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public WeaponInfo Parse(string weaponName, Server.Game gameName)
|
||||
@ -67,4 +65,4 @@ namespace Stats.Client
|
||||
return weaponInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
private readonly ConcurrentDictionary<long, ServerStats> _servers;
|
||||
private readonly ILogger _log;
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
private readonly IConfigurationHandler<StatsConfiguration> _configHandler;
|
||||
private readonly StatsConfiguration _config;
|
||||
private static List<EFServer> serverModels;
|
||||
public static string CLIENT_STATS_KEY = "ClientStats";
|
||||
public static string CLIENT_DETECTIONS_KEY = "ClientDetections";
|
||||
@ -47,13 +47,13 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
private readonly IServerDistributionCalculator _serverDistributionCalculator;
|
||||
|
||||
public StatManager(ILogger<StatManager> logger, IManager mgr, IDatabaseContextFactory contextFactory,
|
||||
IConfigurationHandler<StatsConfiguration> configHandler,
|
||||
StatsConfiguration statsConfig,
|
||||
IServerDistributionCalculator serverDistributionCalculator)
|
||||
{
|
||||
_servers = new ConcurrentDictionary<long, ServerStats>();
|
||||
_log = logger;
|
||||
_contextFactory = contextFactory;
|
||||
_configHandler = configHandler;
|
||||
_config = statsConfig;
|
||||
_serverDistributionCalculator = serverDistributionCalculator;
|
||||
}
|
||||
|
||||
@ -75,7 +75,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
r.When > fifteenDaysAgo &&
|
||||
r.RatingHistory.Client.Level != EFClient.Permission.Banned &&
|
||||
r.Newest &&
|
||||
r.ActivityAmount >= _configHandler.Configuration().TopPlayersMinPlayTime;
|
||||
r.ActivityAmount >= _config.TopPlayersMinPlayTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -87,7 +87,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext(enableTracking: false);
|
||||
|
||||
if (_configHandler.Configuration().EnableAdvancedMetrics)
|
||||
if (_config.EnableAdvancedMetrics)
|
||||
{
|
||||
var clientRanking = await context.Set<EFClientRankingHistory>()
|
||||
.Where(r => r.ClientId == clientId)
|
||||
@ -126,7 +126,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
&& ranking.PerformanceMetric != null
|
||||
&& ranking.Newest
|
||||
&& ranking.Client.TotalConnectionTime >=
|
||||
_configHandler.Configuration().TopPlayersMinPlayTime;
|
||||
_config.TopPlayersMinPlayTime;
|
||||
}
|
||||
|
||||
public async Task<int> GetTotalRankedPlayers(long serverId)
|
||||
@ -217,7 +217,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
public async Task<List<TopStatsInfo>> GetTopStats(int start, int count, long? serverId = null)
|
||||
{
|
||||
if (_configHandler.Configuration().EnableAdvancedMetrics)
|
||||
if (_config.EnableAdvancedMetrics)
|
||||
{
|
||||
return await GetNewTopStats(start, count, serverId);
|
||||
}
|
||||
@ -570,7 +570,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
{
|
||||
clientStats = UpdateStats(clientStats, pl);
|
||||
await SaveClientStats(clientStats);
|
||||
if (_configHandler.Configuration().EnableAdvancedMetrics)
|
||||
if (_config.EnableAdvancedMetrics)
|
||||
{
|
||||
await UpdateHistoricalRanking(pl.ClientId, clientStats, serverId);
|
||||
}
|
||||
@ -973,7 +973,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
|
||||
// update their performance
|
||||
if ((DateTime.UtcNow - attackerStats.LastStatHistoryUpdate).TotalMinutes >=
|
||||
(Utilities.IsDevelopment ? 0.5 : _configHandler.Configuration().EnableAdvancedMetrics ? 5.0 : 2.5))
|
||||
(Utilities.IsDevelopment ? 0.5 : _config.EnableAdvancedMetrics ? 5.0 : 2.5))
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -982,7 +982,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
// for stat history update, but one is already processing that invalidates the original
|
||||
await attackerStats.ProcessingHit.WaitAsync(Utilities.DefaultCommandTimeout,
|
||||
Plugin.ServerManager.CancellationToken);
|
||||
if (_configHandler.Configuration().EnableAdvancedMetrics)
|
||||
if (_config.EnableAdvancedMetrics)
|
||||
{
|
||||
await UpdateHistoricalRanking(attacker.ClientId, attackerStats, serverId);
|
||||
}
|
||||
@ -1190,7 +1190,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
public async Task UpdateHistoricalRanking(int clientId, EFClientStatistics clientStats, long serverId)
|
||||
{
|
||||
await using var context = _contextFactory.CreateContext();
|
||||
var minPlayTime = _configHandler.Configuration().TopPlayersMinPlayTime;
|
||||
var minPlayTime = _config.TopPlayersMinPlayTime;
|
||||
|
||||
var performances = await context.Set<EFClientStatistics>()
|
||||
.AsNoTracking()
|
||||
@ -1208,7 +1208,7 @@ namespace IW4MAdmin.Plugins.Stats.Helpers
|
||||
var serverRanking = await context.Set<EFClientStatistics>()
|
||||
.Where(stats => stats.ClientId != clientStats.ClientId)
|
||||
.Where(AdvancedClientStatsResourceQueryHelper.GetRankingFunc(
|
||||
_configHandler.Configuration().TopPlayersMinPlayTime, clientStats.ZScore, serverId))
|
||||
_config.TopPlayersMinPlayTime, clientStats.ZScore, serverId))
|
||||
.CountAsync();
|
||||
|
||||
var serverRankingSnapshot = new EFClientRankingHistory
|
||||
|
@ -11,7 +11,6 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
using Data.Models.Client;
|
||||
using Data.Models.Client.Stats;
|
||||
using Data.Models.Server;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -172,6 +171,7 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
|
||||
public async Task OnLoadAsync(IManager manager)
|
||||
{
|
||||
await Config.BuildAsync();
|
||||
// load custom configuration
|
||||
if (Config.Configuration() == null)
|
||||
{
|
||||
@ -191,10 +191,8 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
async Task<IEnumerable<InformationResponse>> getStats(ClientPaginationRequest request)
|
||||
{
|
||||
IList<EFClientStatistics> clientStats;
|
||||
int messageCount = 0;
|
||||
await using var ctx = _databaseContextFactory.CreateContext(enableTracking: false);
|
||||
clientStats = await ctx.Set<EFClientStatistics>().Where(c => c.ClientId == request.ClientId).ToListAsync();
|
||||
messageCount = await ctx.Set<EFClientMessage>().CountAsync(_message => _message.ClientId == request.ClientId);
|
||||
|
||||
int kills = clientStats.Sum(c => c.Kills);
|
||||
int deaths = clientStats.Sum(c => c.Deaths);
|
||||
@ -253,14 +251,6 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
Column = 0,
|
||||
Order = 5,
|
||||
Type = MetaType.Information
|
||||
},
|
||||
new InformationResponse()
|
||||
{
|
||||
Key = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_MESSAGES"],
|
||||
Value = messageCount.ToString("#,##0", new System.Globalization.CultureInfo(Utilities.CurrentLocalization.LocalizationName)),
|
||||
Column = 1,
|
||||
Order = 4,
|
||||
Type = MetaType.Information
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -451,7 +441,7 @@ namespace IW4MAdmin.Plugins.Stats
|
||||
}
|
||||
|
||||
ServerManager = manager;
|
||||
Manager = new StatManager(_managerLogger, manager, _databaseContextFactory, Config, _serverDistributionCalculator);
|
||||
Manager = new StatManager(_managerLogger, manager, _databaseContextFactory, Config.Configuration(), _serverDistributionCalculator);
|
||||
await _serverDistributionCalculator.Initialize();
|
||||
}
|
||||
|
||||
|
@ -17,7 +17,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.1.25.2" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.1.28.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
|
||||
|
@ -35,6 +35,7 @@ namespace IW4MAdmin.Plugins.Welcome
|
||||
|
||||
public async Task OnLoadAsync(IManager manager)
|
||||
{
|
||||
await _configHandler.BuildAsync();
|
||||
if (_configHandler.Configuration() == null)
|
||||
{
|
||||
_configHandler.Set((WelcomeConfiguration) new WelcomeConfiguration().Generate());
|
||||
@ -94,7 +95,7 @@ namespace IW4MAdmin.Plugins.Welcome
|
||||
msg = msg.Replace("{{ClientLocation}}", await GetCountryName(joining.IPAddressString));
|
||||
}
|
||||
|
||||
msg = msg.Replace("{{TimesConnected}}", joining.Connections.Ordinalize());
|
||||
msg = msg.Replace("{{TimesConnected}}", joining.Connections.Ordinalize(Utilities.CurrentLocalization.Culture));
|
||||
|
||||
return msg;
|
||||
}
|
||||
@ -110,7 +111,7 @@ namespace IW4MAdmin.Plugins.Welcome
|
||||
try
|
||||
{
|
||||
var response =
|
||||
await wc.GetStringAsync(new Uri($"http://extreme-ip-lookup.com/json/{ip}?key=demo"));
|
||||
await wc.GetStringAsync(new Uri($"http://ip-api.com/json/{ip}"));
|
||||
var responseObj = JObject.Parse(response);
|
||||
response = responseObj["country"]?.ToString();
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.1.25.2" PrivateAssets="All" />
|
||||
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.1.28.1" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
11
README.md
11
README.md
@ -35,11 +35,22 @@ Linux
|
||||
* You will need to retrieve your login credentials by typing `!rt` ingame
|
||||
|
||||
### Updating
|
||||
**Manually**
|
||||
1. Download the latest version of **IW4MAdmin**
|
||||
2. Extract the newer version of **IW4MAdmin** into pre-existing **IW4MAdmin** folder and overwrite existing files
|
||||
|
||||
_Your configuration and database will be saved_
|
||||
|
||||
**OR**
|
||||
Use the provided `UpdateIW4MAdmin` script to download and install automatically
|
||||
|
||||
| Argument Windows (Linux) | Description |
|
||||
|--------------------------|-----------------------------------------------------------|
|
||||
| -Silent (s) | Do not prompt for any user input |
|
||||
| -Clean (c) | Clean unneeded files listed in `_delete.txt` after update |
|
||||
| -Verified (v) | Only update releases in the verified stream |
|
||||
| -Directory (d) | Directory to install to |
|
||||
|
||||
## Help
|
||||
Feel free to join the **IW4MAdmin** [Discord](https://discord.gg/ZZFK5p3)
|
||||
If you come across an issue, bug, or feature request please post an [issue](https://github.com/RaidMax/IW4M-Admin/issues)
|
||||
|
@ -381,9 +381,10 @@ namespace SharedLibraryCore.Commands
|
||||
public override async Task ExecuteAsync(GameEvent E)
|
||||
{
|
||||
// todo: don't do the lookup here
|
||||
var penalties = await E.Owner.Manager.GetPenaltyService().GetActivePenaltiesAsync(E.Target.AliasLinkId);
|
||||
if (penalties.Where(p => p.Type == EFPenalty.PenaltyType.Ban || p.Type == EFPenalty.PenaltyType.TempBan)
|
||||
.FirstOrDefault() != null)
|
||||
var penalties = await E.Owner.Manager.GetPenaltyService().GetActivePenaltiesAsync(E.Target.AliasLinkId, E.Target.CurrentAliasId);
|
||||
if (penalties
|
||||
.FirstOrDefault(p =>
|
||||
p.Type == EFPenalty.PenaltyType.Ban || p.Type == EFPenalty.PenaltyType.TempBan) != null)
|
||||
{
|
||||
switch ((await E.Target.Unban(E.Data, E.Origin)
|
||||
.WaitAsync(Utilities.DefaultCommandTimeout, E.Owner.Manager.CancellationToken)).FailReason)
|
||||
@ -897,7 +898,7 @@ namespace SharedLibraryCore.Commands
|
||||
public override async Task ExecuteAsync(GameEvent E)
|
||||
{
|
||||
var existingPenalties = await E.Owner.Manager.GetPenaltyService()
|
||||
.GetActivePenaltiesAsync(E.Target.AliasLinkId, E.Target.IPAddress);
|
||||
.GetActivePenaltiesAsync(E.Target.AliasLinkId, E.Target.CurrentAliasId, E.Target.IPAddress);
|
||||
var penalty = existingPenalties.FirstOrDefault(b => b.Type > EFPenalty.PenaltyType.Kick);
|
||||
|
||||
if (penalty == null)
|
||||
@ -1247,4 +1248,4 @@ namespace SharedLibraryCore.Commands
|
||||
E.Origin.Tell(await GetNextMap(E.Owner, _translationLookup));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -147,8 +147,8 @@ namespace SharedLibraryCore.Configuration
|
||||
}
|
||||
|
||||
_selectedParser = _rconParsers.FirstOrDefault(p => p.Name == parser);
|
||||
RConParserVersion = _selectedParser?.Version;
|
||||
EventParserVersion = _selectedParser?.Version;
|
||||
RConParserVersion = _selectedParser?.Name;
|
||||
EventParserVersion = _selectedParser?.Name;
|
||||
|
||||
if (index <= 0 || _rconParsers[index].CanGenerateLogPath)
|
||||
{
|
||||
|
@ -5,4 +5,4 @@
|
||||
string Name();
|
||||
IBaseConfiguration Generate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ namespace SharedLibraryCore.Interfaces
|
||||
{
|
||||
string FileName { get; }
|
||||
Task Save();
|
||||
void Build();
|
||||
Task BuildAsync();
|
||||
T Configuration();
|
||||
void Set(T config);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,27 @@
|
||||
namespace SharedLibraryCore.Interfaces
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SharedLibraryCore.Interfaces
|
||||
{
|
||||
/// <summary>
|
||||
/// defines the capabilities of the configuration handler factory
|
||||
/// used to generate new instance of configuration handlers
|
||||
/// defines the capabilities of the configuration handler factory
|
||||
/// used to generate new instance of configuration handlers
|
||||
/// </summary>
|
||||
public interface IConfigurationHandlerFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// generates a new configuration handler
|
||||
/// generates a new configuration handler
|
||||
/// </summary>
|
||||
/// <typeparam name="T">base configuration type</typeparam>
|
||||
/// <param name="name">file name of configuration</param>
|
||||
/// <returns>new configuration handler instance</returns>
|
||||
IConfigurationHandler<T> GetConfigurationHandler<T>(string name) where T : IBaseConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// generates a new configuration handler and builds the configuration automatically
|
||||
/// </summary>
|
||||
/// <typeparam name="T">base configuration type</typeparam>
|
||||
/// <param name="name">file name of configuration</param>
|
||||
/// <returns>new configuration handler instance</returns>
|
||||
Task<IConfigurationHandler<T>> GetConfigurationHandlerAsync<T>(string name) where T : IBaseConfiguration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -653,7 +653,7 @@ namespace SharedLibraryCore.Database.Models
|
||||
|
||||
// we want to get any penalties that are tied to their IP or AliasLink (but not necessarily their GUID)
|
||||
var activePenalties = await CurrentServer.Manager.GetPenaltyService()
|
||||
.GetActivePenaltiesAsync(AliasLinkId, ipAddress);
|
||||
.GetActivePenaltiesAsync(AliasLinkId, CurrentAliasId, ipAddress);
|
||||
var banPenalty = activePenalties.FirstOrDefault(_penalty => _penalty.Type == EFPenalty.PenaltyType.Ban);
|
||||
var tempbanPenalty =
|
||||
activePenalties.FirstOrDefault(_penalty => _penalty.Type == EFPenalty.PenaltyType.TempBan);
|
||||
@ -740,4 +740,4 @@ namespace SharedLibraryCore.Database.Models
|
||||
return IsBot ? ClientNumber : (int)NetworkId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -168,10 +168,9 @@ namespace SharedLibraryCore.Services
|
||||
|
||||
public async Task<EFClient> Get(int entityId)
|
||||
{
|
||||
// todo: this needs to be optimized for large linked accounts
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
|
||||
var client = context.Clients
|
||||
var client = await context.Clients
|
||||
.Select(_client => new EFClient
|
||||
{
|
||||
ClientId = _client.ClientId,
|
||||
@ -187,26 +186,28 @@ namespace SharedLibraryCore.Services
|
||||
Name = _client.CurrentAlias.Name,
|
||||
IPAddress = _client.CurrentAlias.IPAddress
|
||||
},
|
||||
TotalConnectionTime = _client.TotalConnectionTime
|
||||
TotalConnectionTime = _client.TotalConnectionTime,
|
||||
AliasLink = new EFAliasLink
|
||||
{
|
||||
AliasLinkId = _client.AliasLinkId,
|
||||
Children = _client.AliasLink.Children
|
||||
},
|
||||
LinkedAccounts = new Dictionary<int, long>()
|
||||
{
|
||||
{_client.ClientId, _client.NetworkId}
|
||||
}
|
||||
})
|
||||
.FirstOrDefault(_client => _client.ClientId == entityId);
|
||||
.FirstOrDefaultAsync(_client => _client.ClientId == entityId);
|
||||
|
||||
if (client == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
client.AliasLink = new EFAliasLink
|
||||
if (!_appConfig.EnableImplicitAccountLinking)
|
||||
{
|
||||
AliasLinkId = client.AliasLinkId,
|
||||
Children = await context.Aliases
|
||||
.Where(_alias => _alias.LinkId == client.AliasLinkId)
|
||||
.Select(_alias => new EFAlias
|
||||
{
|
||||
Name = _alias.Name,
|
||||
IPAddress = _alias.IPAddress
|
||||
}).ToListAsync()
|
||||
};
|
||||
return client;
|
||||
}
|
||||
|
||||
var foundClient = new
|
||||
{
|
||||
@ -220,11 +221,6 @@ namespace SharedLibraryCore.Services
|
||||
.ToListAsync()
|
||||
};
|
||||
|
||||
if (foundClient == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foundClient.Client.LinkedAccounts = new Dictionary<int, long>();
|
||||
// todo: find out the best way to do this
|
||||
// I'm doing this here because I don't know the best way to have multiple awaits in the query
|
||||
|
@ -148,7 +148,7 @@ namespace SharedLibraryCore.Services
|
||||
return await iqPenalties.Distinct().ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<List<EFPenalty>> GetActivePenaltiesAsync(int linkId, int? ip = null,
|
||||
public async Task<List<EFPenalty>> GetActivePenaltiesAsync(int linkId, int currentAliasId, int? ip = null,
|
||||
bool includePunisherName = false)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
@ -163,9 +163,6 @@ namespace SharedLibraryCore.Services
|
||||
(p.Expires == null || p.Expires > now);
|
||||
|
||||
await using var context = _contextFactory.CreateContext(false);
|
||||
var iqLinkPenalties = context.Penalties
|
||||
.Where(p => p.LinkId == linkId)
|
||||
.Where(filter);
|
||||
|
||||
IQueryable<EFPenalty> iqIpPenalties;
|
||||
|
||||
@ -178,22 +175,21 @@ namespace SharedLibraryCore.Services
|
||||
}
|
||||
else
|
||||
{
|
||||
var aliasIps = await context.Aliases.Where(alias => alias.LinkId == linkId && alias.IPAddress != null)
|
||||
.Select(alias => alias.IPAddress)
|
||||
.ToListAsync();
|
||||
if (ip != null)
|
||||
{
|
||||
aliasIps.Add(ip);
|
||||
}
|
||||
var usedIps = await context.Aliases.AsNoTracking()
|
||||
.Where(alias => (alias.LinkId == linkId || alias.AliasId == currentAliasId) && alias.IPAddress != null)
|
||||
.Select(alias => alias.IPAddress).ToListAsync();
|
||||
|
||||
iqIpPenalties = context.Penalties
|
||||
.Where(penalty => aliasIps.Contains(penalty.Offender.CurrentAlias.IPAddress))
|
||||
var aliasedIds = await context.Aliases.AsNoTracking().Where(alias => usedIps.Contains(alias.IPAddress))
|
||||
.Select(alias => alias.LinkId)
|
||||
.ToListAsync();
|
||||
|
||||
iqIpPenalties = context.Penalties.AsNoTracking()
|
||||
.Where(penalty => aliasedIds.Contains(penalty.LinkId) || penalty.LinkId == linkId)
|
||||
.Where(filter);
|
||||
}
|
||||
|
||||
var activePenalties = (await iqLinkPenalties.ToListAsync())
|
||||
.Union(await iqIpPenalties.ToListAsync())
|
||||
.Distinct();
|
||||
var activeIpPenalties = await iqIpPenalties.ToListAsync();
|
||||
var activePenalties = activeIpPenalties.Distinct();
|
||||
|
||||
// this is a bit more performant in memory (ordering)
|
||||
return activePenalties.OrderByDescending(p => p.When).ToList();
|
||||
@ -221,4 +217,4 @@ namespace SharedLibraryCore.Services
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,12 +4,12 @@
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
|
||||
<Version>2022.01.25.2</Version>
|
||||
<Version>2022.01.28.1</Version>
|
||||
<Authors>RaidMax</Authors>
|
||||
<Company>Forever None</Company>
|
||||
<Configurations>Debug;Release;Prerelease</Configurations>
|
||||
<PublishWithAspNetCoreTargetManifest>false</PublishWithAspNetCoreTargetManifest>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<LangVersion>default</LangVersion>
|
||||
<PackageTags>IW4MAdmin</PackageTags>
|
||||
<RepositoryUrl>https://github.com/RaidMax/IW4M-Admin/</RepositoryUrl>
|
||||
<PackageProjectUrl>https://www.raidmax.org/IW4MAdmin/</PackageProjectUrl>
|
||||
@ -19,7 +19,7 @@
|
||||
<IsPackable>true</IsPackable>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<Description>Shared Library for IW4MAdmin</Description>
|
||||
<PackageVersion>2022.01.25.2</PackageVersion>
|
||||
<PackageVersion>2022.01.28.1</PackageVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Prerelease|AnyCPU'">
|
||||
|
@ -125,28 +125,6 @@ namespace SharedLibraryCore
|
||||
return str.Length > maxLength ? $"{str.Substring(0, maxLength - 3)}..." : str;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// helper method to get the information about an exception and inner exceptions
|
||||
/// </summary>
|
||||
/// <param name="ex"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetExceptionInfo(this Exception ex)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var depth = 0;
|
||||
while (ex != null)
|
||||
{
|
||||
sb.AppendLine($"Exception[{depth}] Name: {ex.GetType().FullName}");
|
||||
sb.AppendLine($"Exception[{depth}] Message: {ex.Message}");
|
||||
sb.AppendLine($"Exception[{depth}] Call Stack: {ex.StackTrace}");
|
||||
sb.AppendLine($"Exception[{depth}] Source: {ex.Source}");
|
||||
depth++;
|
||||
ex = ex.InnerException;
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public static Permission MatchPermission(string str)
|
||||
{
|
||||
var lookingFor = str.ToLower();
|
||||
@ -1190,4 +1168,4 @@ namespace SharedLibraryCore
|
||||
return allRules[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models;
|
||||
using IW4MAdmin.Plugins.Stats.Config;
|
||||
using Stats.Config;
|
||||
using WebfrontCore.ViewComponents;
|
||||
|
||||
@ -19,13 +18,12 @@ namespace WebfrontCore.Controllers
|
||||
public class ClientController : BaseController
|
||||
{
|
||||
private readonly IMetaService _metaService;
|
||||
private readonly IConfigurationHandler<StatsConfiguration> _configurationHandler;
|
||||
private readonly StatsConfiguration _config;
|
||||
|
||||
public ClientController(IManager manager, IMetaService metaService,
|
||||
IConfigurationHandler<StatsConfiguration> configurationHandler) : base(manager)
|
||||
public ClientController(IManager manager, IMetaService metaService, StatsConfiguration config) : base(manager)
|
||||
{
|
||||
_metaService = metaService;
|
||||
_configurationHandler = configurationHandler;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public async Task<IActionResult> ProfileAsync(int id, MetaType? metaFilterType)
|
||||
@ -37,9 +35,18 @@ namespace WebfrontCore.Controllers
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var activePenalties = (await Manager.GetPenaltyService().GetActivePenaltiesAsync(client.AliasLinkId, client.IPAddress));
|
||||
var activePenalties = await Manager.GetPenaltyService().GetActivePenaltiesAsync(client.AliasLinkId, client.CurrentAliasId, client.IPAddress);
|
||||
|
||||
var persistentMetaTask = new[]
|
||||
{
|
||||
_metaService.GetPersistentMeta(EFMeta.ClientTag, client),
|
||||
_metaService.GetPersistentMeta("GravatarEmail", client)
|
||||
};
|
||||
|
||||
var persistentMeta = await Task.WhenAll(persistentMetaTask);
|
||||
var tag = persistentMeta[0];
|
||||
var gravatar = persistentMeta[1];
|
||||
|
||||
var tag = await _metaService.GetPersistentMeta(EFMeta.ClientTag, client);
|
||||
if (tag?.LinkedMeta != null)
|
||||
{
|
||||
client.SetAdditionalProperty(EFMeta.ClientTag, tag.LinkedMeta.Value);
|
||||
@ -56,7 +63,7 @@ namespace WebfrontCore.Controllers
|
||||
|
||||
displayLevel = string.IsNullOrEmpty(client.Tag) ? displayLevel : $"{displayLevel} ({client.Tag})";
|
||||
|
||||
var clientDto = new PlayerInfo()
|
||||
var clientDto = new PlayerInfo
|
||||
{
|
||||
Name = client.Name,
|
||||
Level = displayLevel,
|
||||
@ -93,7 +100,6 @@ namespace WebfrontCore.Controllers
|
||||
Before = DateTime.UtcNow
|
||||
}, MetaType.Information);
|
||||
|
||||
var gravatar = await _metaService.GetPersistentMeta("GravatarEmail", client);
|
||||
if (gravatar != null)
|
||||
{
|
||||
clientDto.Meta.Add(new InformationResponse()
|
||||
@ -107,14 +113,14 @@ namespace WebfrontCore.Controllers
|
||||
clientDto.ActivePenalty = activePenalties.OrderByDescending(_penalty => _penalty.Type).FirstOrDefault();
|
||||
clientDto.Meta.AddRange(Authorized ? meta : meta.Where(m => !m.IsSensitive));
|
||||
|
||||
string strippedName = clientDto.Name.StripColors();
|
||||
var strippedName = clientDto.Name.StripColors();
|
||||
ViewBag.Title = strippedName.Substring(strippedName.Length - 1).ToLower()[0] == 's' ?
|
||||
strippedName + "'" :
|
||||
strippedName + "'s";
|
||||
ViewBag.Title += " " + Localization["WEBFRONT_CLIENT_PROFILE_TITLE"];
|
||||
ViewBag.Description = $"Client information for {strippedName}";
|
||||
ViewBag.Keywords = $"IW4MAdmin, client, profile, {strippedName}";
|
||||
ViewBag.UseNewStats = _configurationHandler.Configuration()?.EnableAdvancedMetrics ?? true;
|
||||
ViewBag.UseNewStats = _config?.EnableAdvancedMetrics ?? true;
|
||||
|
||||
return View("Profile/Index", clientDto);
|
||||
}
|
||||
|
@ -16,10 +16,10 @@ namespace WebfrontCore.Controllers
|
||||
|
||||
public ClientStatisticsController(IManager manager,
|
||||
IResourceQueryHelper<StatsInfoRequest, AdvancedStatsInfo> queryHelper,
|
||||
IConfigurationHandler<DefaultSettings> configurationHandler) : base(manager)
|
||||
DefaultSettings defaultConfig) : base(manager)
|
||||
{
|
||||
_queryHelper = queryHelper;
|
||||
_defaultConfig = configurationHandler.Configuration();
|
||||
_defaultConfig = defaultConfig;
|
||||
}
|
||||
|
||||
[HttpGet("{id:int}/advanced")]
|
||||
@ -35,4 +35,4 @@ namespace WebfrontCore.Controllers
|
||||
return View("~/Views/Client/Statistics/Advanced.cshtml", hitInfo.Results.First());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -27,19 +27,18 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
|
||||
private readonly IResourceQueryHelper<ChatSearchQuery, MessageResponse> _chatResourceQueryHelper;
|
||||
private readonly ITranslationLookup _translationLookup;
|
||||
private readonly IDatabaseContextFactory _contextFactory;
|
||||
private readonly IConfigurationHandler<StatsConfiguration> _configurationHandler;
|
||||
private readonly StatsConfiguration _config;
|
||||
|
||||
public StatsController(ILogger<StatsController> logger, IManager manager, IResourceQueryHelper<ChatSearchQuery,
|
||||
MessageResponse> resourceQueryHelper, ITranslationLookup translationLookup,
|
||||
IDatabaseContextFactory contextFactory,
|
||||
IConfigurationHandler<StatsConfiguration> configurationHandler) : base(manager)
|
||||
IDatabaseContextFactory contextFactory, StatsConfiguration config) : base(manager)
|
||||
{
|
||||
_logger = logger;
|
||||
_manager = manager;
|
||||
_chatResourceQueryHelper = resourceQueryHelper;
|
||||
_translationLookup = translationLookup;
|
||||
_contextFactory = contextFactory;
|
||||
_configurationHandler = configurationHandler;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -70,7 +69,7 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
|
||||
serverId = StatManager.GetIdForServer(server);
|
||||
}
|
||||
|
||||
var results = _configurationHandler.Configuration().EnableAdvancedMetrics
|
||||
var results = _config?.EnableAdvancedMetrics ?? true
|
||||
? await Plugin.Manager.GetNewTopStats(offset, count, serverId)
|
||||
: await Plugin.Manager.GetTopStats(offset, count, serverId);
|
||||
|
||||
@ -80,7 +79,7 @@ namespace IW4MAdmin.Plugins.Web.StatsWeb.Controllers
|
||||
return Ok();
|
||||
}
|
||||
|
||||
ViewBag.UseNewStats = _configurationHandler.Configuration().EnableAdvancedMetrics;
|
||||
ViewBag.UseNewStats = _config?.EnableAdvancedMetrics;
|
||||
return View("~/Views/Client/Statistics/Components/TopPlayers/_List.cshtml", results);
|
||||
}
|
||||
|
||||
|
@ -129,7 +129,7 @@ namespace WebfrontCore
|
||||
services.AddSingleton(Program.ApplicationServiceProvider
|
||||
.GetRequiredService<IConfigurationHandler<DefaultSettings>>());
|
||||
services.AddSingleton(Program.ApplicationServiceProvider
|
||||
.GetRequiredService<IConfigurationHandler<StatsConfiguration>>());
|
||||
.GetRequiredService<StatsConfiguration>());
|
||||
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IServerDataViewer>());
|
||||
}
|
||||
|
||||
|
@ -3,18 +3,17 @@ using System.Threading.Tasks;
|
||||
using IW4MAdmin.Plugins.Stats;
|
||||
using IW4MAdmin.Plugins.Stats.Helpers;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using Stats.Config;
|
||||
|
||||
namespace WebfrontCore.ViewComponents
|
||||
{
|
||||
public class TopPlayersViewComponent : ViewComponent
|
||||
{
|
||||
private readonly IConfigurationHandler<StatsConfiguration> _configurationHandler;
|
||||
private readonly StatsConfiguration _config;
|
||||
|
||||
public TopPlayersViewComponent(IConfigurationHandler<StatsConfiguration> configurationHandler)
|
||||
public TopPlayersViewComponent(StatsConfiguration config)
|
||||
{
|
||||
_configurationHandler = configurationHandler;
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public async Task<IViewComponentResult> InvokeAsync(int count, int offset, long? serverId = null)
|
||||
@ -32,7 +31,7 @@ namespace WebfrontCore.ViewComponents
|
||||
}
|
||||
|
||||
|
||||
ViewBag.UseNewStats = _configurationHandler.Configuration()?.EnableAdvancedMetrics ?? true;
|
||||
ViewBag.UseNewStats = _config?.EnableAdvancedMetrics ?? true;
|
||||
return View("~/Views/Client/Statistics/Components/TopPlayers/_List.cshtml",
|
||||
ViewBag.UseNewStats
|
||||
? await Plugin.Manager.GetNewTopStats(offset, count, serverId)
|
||||
|
@ -96,37 +96,31 @@
|
||||
$('.ip-locate-link').click(function (e) {
|
||||
e.preventDefault();
|
||||
const ip = $(this).data("ip");
|
||||
$.getJSON('https://extreme-ip-lookup.com/json/' + ip + '?key=demo')
|
||||
$.getJSON(`https://ipwhois.app/json/${ip}`)
|
||||
.done(function (response) {
|
||||
$('#mainModal .modal-title').text(ip);
|
||||
$('#mainModal .modal-body').text('');
|
||||
if (response.ipName.length > 0) {
|
||||
$('#mainModal .modal-body').append(`${_localization['WEBFRONT_PROFILE_LOOKUP_HOSTNAME']} — ${response.ipName}<br/>`);
|
||||
}
|
||||
if (response.isp.length > 0) {
|
||||
$('#mainModal .modal-body').append(`${_localization['WEBFRONT_PROFILE_LOOKUP_ISP']} — ${response.isp}<br/>`);
|
||||
}
|
||||
if (response.org.length > 0) {
|
||||
$('#mainModal .modal-body').append(`${_localization['WEBFRONT_PROFILE_LOOKUP_ORG']} — ${response.org}<br/>`);
|
||||
}
|
||||
if (response['businessName'].length > 0) {
|
||||
$('#mainModal .modal-body').append(`${_localization['WEBFRONT_PROFILE_LOOKUP_BUSINESS']} — ${response.businessName}<br/>`);
|
||||
}
|
||||
if (response['businessWebsite'].length > 0) {
|
||||
$('#mainModal .modal-body').append(`${_localization['WEBFRONT_PROFILE_LOOKUP_WEBSITE']} — ${response.businessWebsite}<br/>`);
|
||||
}
|
||||
if (response.city.length > 0 || response.region.length > 0 || response.country.length > 0) {
|
||||
if (response.region.length > 0 || response.city.length > 0 || response.country.length > 0 || response.timezone_gmt.length > 0) {
|
||||
$('#mainModal .modal-body').append(`${_localization['WEBFRONT_PROFILE_LOOKUP_LOCATION']} — `);
|
||||
}
|
||||
if (response.city.length > 0) {
|
||||
$('#mainModal .modal-body').append(response.city);
|
||||
}
|
||||
if (response.region.length > 0) {
|
||||
$('#mainModal .modal-body').append((response.city.length > 0 ? ', ' : '') + response.region);
|
||||
$('#mainModal .modal-body').append((response.region.length > 0 ? ', ' : '') + response.region);
|
||||
}
|
||||
if (response.country.length > 0) {
|
||||
$('#mainModal .modal-body').append((response.country.length > 0 ? ', ' : '') + response.country);
|
||||
}
|
||||
if (response.timezone_gmt.length > 0) {
|
||||
$('#mainModal .modal-body').append((response.timezone_gmt.length > 0 ? ', ' : '') + response.timezone_gmt);
|
||||
}
|
||||
|
||||
$('#mainModal').modal();
|
||||
})
|
||||
|
@ -11,6 +11,10 @@
|
||||
}
|
||||
|
||||
$(document).ready(() => {
|
||||
if ($('.scoreboard-container').length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$(window.location.hash).tab('show');
|
||||
$(`${window.location.hash}_nav`).addClass('active');
|
||||
|
||||
|
Reference in New Issue
Block a user