implement PluginV2 for script plugins
This commit is contained in:
parent
ad20572879
commit
fab3cf95d6
@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using IW4MAdmin.Application.Misc;
|
||||
using IW4MAdmin.Application.Plugin;
|
||||
using Newtonsoft.Json;
|
||||
using RestEase;
|
||||
using SharedLibraryCore.Helpers;
|
||||
|
@ -25,7 +25,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Jint" Version="3.0.0-beta-2042" />
|
||||
<PackageReference Include="Jint" Version="3.0.0-beta-2047" />
|
||||
<PackageReference Include="MaxMind.GeoIP2" Version="5.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="6.0.8">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
|
@ -24,6 +24,7 @@ using System.Threading.Tasks;
|
||||
using Data.Abstractions;
|
||||
using Data.Context;
|
||||
using IW4MAdmin.Application.Migration;
|
||||
using IW4MAdmin.Application.Plugin.Script;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog.Context;
|
||||
|
@ -1,8 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using IW4MAdmin.Application.Misc;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System.Linq;
|
||||
using IW4MAdmin.Application.Plugin.Script;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Configuration;
|
||||
|
||||
|
@ -2,7 +2,6 @@
|
||||
using System.Linq;
|
||||
using Data.Models.Client.Stats;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore;
|
||||
|
||||
namespace IW4MAdmin.Application.Extensions;
|
||||
|
||||
@ -26,9 +25,4 @@ public static class ScriptPluginExtensions
|
||||
{
|
||||
return set.Where(stat => clientIds.Contains(stat.ClientId) && stat.ServerId == (long)serverId).ToList();
|
||||
}
|
||||
|
||||
public static object GetId(this Server server)
|
||||
{
|
||||
return server.GetIdForServer().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
using IW4MAdmin.Application.Misc;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Commands;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models;
|
||||
using Data.Models.Client;
|
||||
using IW4MAdmin.Application.Plugin.Script;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
@ -31,16 +31,11 @@ namespace IW4MAdmin.Application.Factories
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission,
|
||||
bool isTargetRequired, IEnumerable<(string, bool)> args, Func<GameEvent, Task> executeAction, Server.Game[] supportedGames)
|
||||
bool isTargetRequired, IEnumerable<CommandArgument> args, Func<GameEvent, Task> executeAction, IEnumerable<Reference.Game> supportedGames)
|
||||
{
|
||||
var permissionEnum = Enum.Parse<EFClient.Permission>(permission);
|
||||
var argsArray = args.Select(_arg => new CommandArgument
|
||||
{
|
||||
Name = _arg.Item1,
|
||||
Required = _arg.Item2
|
||||
}).ToArray();
|
||||
|
||||
return new ScriptCommand(name, alias, description, isTargetRequired, permissionEnum, argsArray, executeAction,
|
||||
return new ScriptCommand(name, alias, description, isTargetRequired, permissionEnum, args, executeAction,
|
||||
_config, _transLookup, _serviceProvider.GetRequiredService<ILogger<ScriptCommand>>(), supportedGames);
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,8 @@ using Data.Models;
|
||||
using Data.Models.Server;
|
||||
using IW4MAdmin.Application.Alerts;
|
||||
using IW4MAdmin.Application.Commands;
|
||||
using IW4MAdmin.Application.Plugin.Script;
|
||||
using IW4MAdmin.Plugins.Stats.Helpers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using SharedLibraryCore.Alerts;
|
||||
using static Data.Models.Client.EFClient;
|
||||
|
@ -30,6 +30,8 @@ using Integrations.Source.Extensions;
|
||||
using IW4MAdmin.Application.Alerts;
|
||||
using IW4MAdmin.Application.Extensions;
|
||||
using IW4MAdmin.Application.Localization;
|
||||
using IW4MAdmin.Application.Plugin;
|
||||
using IW4MAdmin.Application.Plugin.Script;
|
||||
using IW4MAdmin.Application.QueryHelpers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
@ -321,10 +323,13 @@ namespace IW4MAdmin.Application
|
||||
serviceCollection.AddSingleton(genericInterfaceType, handlerInstance);
|
||||
}
|
||||
|
||||
// register any script plugins
|
||||
foreach (var plugin in pluginImporter.DiscoverScriptPlugins())
|
||||
var scriptPlugins = pluginImporter.DiscoverScriptPlugins();
|
||||
|
||||
foreach (var scriptPlugin in scriptPlugins)
|
||||
{
|
||||
serviceCollection.AddSingleton(plugin);
|
||||
serviceCollection.AddSingleton(scriptPlugin.Item1, sp =>
|
||||
sp.GetRequiredService<IScriptPluginFactory>()
|
||||
.CreateScriptPlugin(scriptPlugin.Item1, scriptPlugin.Item2));
|
||||
}
|
||||
|
||||
// register any eventable types
|
||||
|
@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models;
|
||||
using IW4MAdmin.Application.Plugin.Script;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
@ -86,8 +87,6 @@ public class InteractionRegistration : IInteractionRegistration
|
||||
int? clientId = null,
|
||||
Reference.Game? game = null, CancellationToken token = default)
|
||||
{
|
||||
return Enumerable.Empty<IInteractionData>();
|
||||
// fixme: multi-threading is broken when dealing with script plugins
|
||||
return await GetInteractionsInternal(interactionPrefix, clientId, game, token);
|
||||
}
|
||||
|
||||
@ -120,6 +119,18 @@ public class InteractionRegistration : IInteractionRegistration
|
||||
return scriptPlugin.ExecuteAction<string>(interaction.ScriptAction, token, originId, targetId, game, meta,
|
||||
token);
|
||||
}
|
||||
|
||||
foreach (var plugin in _serviceProvider.GetRequiredService<IEnumerable<IPluginV2>>())
|
||||
{
|
||||
if (plugin is not ScriptPluginV2 scriptPlugin || scriptPlugin.Name != interaction.Source)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
return scriptPlugin
|
||||
.QueryWithErrorHandling(interaction.ScriptAction, originId, targetId, game, meta, token)
|
||||
?.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
@ -1,96 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using IW4MAdmin.Application.Configuration;
|
||||
using Jint;
|
||||
using Jint.Native;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace IW4MAdmin.Application.Misc
|
||||
{
|
||||
public class ScriptPluginConfigurationWrapper
|
||||
{
|
||||
private readonly BaseConfigurationHandler<ScriptPluginConfiguration> _handler;
|
||||
private ScriptPluginConfiguration _config;
|
||||
private readonly string _pluginName;
|
||||
private readonly Engine _scriptEngine;
|
||||
|
||||
public ScriptPluginConfigurationWrapper(string pluginName, Engine scriptEngine)
|
||||
{
|
||||
_handler = new BaseConfigurationHandler<ScriptPluginConfiguration>("ScriptPluginSettings");
|
||||
_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;
|
||||
}
|
||||
|
||||
public async Task SetValue(string key, object value)
|
||||
{
|
||||
var castValue = value;
|
||||
|
||||
if (value is double d)
|
||||
{
|
||||
castValue = AsInteger(d) ?? value;
|
||||
}
|
||||
|
||||
if (value is object[] array && array.All(item => item is double d && AsInteger(d) != null))
|
||||
{
|
||||
castValue = array.Select(item => AsInteger((double)item)).ToArray();
|
||||
}
|
||||
|
||||
if (!_config.ContainsKey(_pluginName))
|
||||
{
|
||||
_config.Add(_pluginName, new Dictionary<string, object>());
|
||||
}
|
||||
|
||||
var plugin = _config[_pluginName];
|
||||
|
||||
if (plugin.ContainsKey(key))
|
||||
{
|
||||
plugin[key] = castValue;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
plugin.Add(key, castValue);
|
||||
}
|
||||
|
||||
_handler.Set(_config);
|
||||
await _handler.Save();
|
||||
}
|
||||
|
||||
public JsValue GetValue(string key)
|
||||
{
|
||||
if (!_config.ContainsKey(_pluginName))
|
||||
{
|
||||
return JsValue.Undefined;
|
||||
}
|
||||
|
||||
if (!_config[_pluginName].ContainsKey(key))
|
||||
{
|
||||
return JsValue.Undefined;
|
||||
}
|
||||
|
||||
var item = _config[_pluginName][key];
|
||||
|
||||
if (item is JsonElement { ValueKind: JsonValueKind.Array } jElem)
|
||||
{
|
||||
item = jElem.Deserialize<List<dynamic>>();
|
||||
}
|
||||
|
||||
return JsValue.FromObject(_scriptEngine, item);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,17 +1,17 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using SharedLibraryCore;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using IW4MAdmin.Application.API.Master;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
|
||||
namespace IW4MAdmin.Application.Misc
|
||||
namespace IW4MAdmin.Application.Plugin
|
||||
{
|
||||
/// <summary>
|
||||
/// implementation of IPluginImporter
|
||||
@ -20,13 +20,15 @@ namespace IW4MAdmin.Application.Misc
|
||||
public class PluginImporter : IPluginImporter
|
||||
{
|
||||
private IEnumerable<PluginSubscriptionContent> _pluginSubscription;
|
||||
private static readonly string PLUGIN_DIR = "Plugins";
|
||||
private static readonly string PluginDir = "Plugins";
|
||||
private const string PluginV2Match = "^ *((?:var|const|let) +init)|function init";
|
||||
private readonly ILogger _logger;
|
||||
private readonly IRemoteAssemblyHandler _remoteAssemblyHandler;
|
||||
private readonly IMasterApi _masterApi;
|
||||
private readonly ApplicationConfiguration _appConfig;
|
||||
|
||||
public PluginImporter(ILogger<PluginImporter> logger, ApplicationConfiguration appConfig, IMasterApi masterApi, IRemoteAssemblyHandler remoteAssemblyHandler)
|
||||
public PluginImporter(ILogger<PluginImporter> logger, ApplicationConfiguration appConfig, IMasterApi masterApi,
|
||||
IRemoteAssemblyHandler remoteAssemblyHandler)
|
||||
{
|
||||
_logger = logger;
|
||||
_masterApi = masterApi;
|
||||
@ -38,25 +40,34 @@ namespace IW4MAdmin.Application.Misc
|
||||
/// discovers all the script plugins in the plugins dir
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public IEnumerable<IPlugin> DiscoverScriptPlugins()
|
||||
public IEnumerable<(Type, string)> DiscoverScriptPlugins()
|
||||
{
|
||||
var pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}";
|
||||
var pluginDir = $"{Utilities.OperatingDirectory}{PluginDir}{Path.DirectorySeparatorChar}";
|
||||
|
||||
if (!Directory.Exists(pluginDir))
|
||||
{
|
||||
return Enumerable.Empty<IPlugin>();
|
||||
return Enumerable.Empty<(Type, string)>();
|
||||
}
|
||||
|
||||
var scriptPluginFiles =
|
||||
Directory.GetFiles(pluginDir, "*.js").AsEnumerable().Union(GetRemoteScripts()).ToList();
|
||||
|
||||
_logger.LogDebug("Discovered {count} potential script plugins", scriptPluginFiles.Count);
|
||||
|
||||
return scriptPluginFiles.Select(fileName =>
|
||||
var bothVersionPlugins = scriptPluginFiles.Select(fileName =>
|
||||
{
|
||||
_logger.LogDebug("Discovered script plugin {fileName}", fileName);
|
||||
return new ScriptPlugin(_logger, fileName);
|
||||
_logger.LogDebug("Discovered script plugin {FileName}", fileName);
|
||||
try
|
||||
{
|
||||
var fileContents = File.ReadAllLines(fileName);
|
||||
var isValidV2 = fileContents.Any(line => Regex.IsMatch(line, PluginV2Match));
|
||||
return isValidV2 ? (typeof(IPluginV2), fileName) : (typeof(IPlugin), fileName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return (typeof(IPlugin), fileName);
|
||||
}
|
||||
}).ToList();
|
||||
|
||||
return bothVersionPlugins;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -65,7 +76,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
/// <returns></returns>
|
||||
public (IEnumerable<Type>, IEnumerable<Type>, IEnumerable<Type>) DiscoverAssemblyPluginImplementations()
|
||||
{
|
||||
var pluginDir = $"{Utilities.OperatingDirectory}{PLUGIN_DIR}{Path.DirectorySeparatorChar}";
|
||||
var pluginDir = $"{Utilities.OperatingDirectory}{PluginDir}{Path.DirectorySeparatorChar}";
|
||||
var pluginTypes = Enumerable.Empty<Type>();
|
||||
var commandTypes = Enumerable.Empty<Type>();
|
||||
var configurationTypes = Enumerable.Empty<Type>();
|
||||
@ -73,45 +84,47 @@ namespace IW4MAdmin.Application.Misc
|
||||
if (Directory.Exists(pluginDir))
|
||||
{
|
||||
var dllFileNames = Directory.GetFiles(pluginDir, "*.dll");
|
||||
_logger.LogDebug("Discovered {count} potential plugin assemblies", dllFileNames.Length);
|
||||
_logger.LogDebug("Discovered {Count} potential plugin assemblies", dllFileNames.Length);
|
||||
|
||||
if (dllFileNames.Length > 0)
|
||||
{
|
||||
// we only want to load the most recent assembly in case of duplicates
|
||||
var assemblies = dllFileNames.Select(_name => Assembly.LoadFrom(_name))
|
||||
var assemblies = dllFileNames.Select(name => Assembly.LoadFrom(name))
|
||||
.Union(GetRemoteAssemblies())
|
||||
.GroupBy(_assembly => _assembly.FullName).Select(_assembly => _assembly.OrderByDescending(_assembly => _assembly.GetName().Version).First());
|
||||
.GroupBy(assembly => assembly.FullName).Select(assembly => assembly.OrderByDescending(asm => asm.GetName().Version).First());
|
||||
|
||||
pluginTypes = assemblies
|
||||
.SelectMany(_asm =>
|
||||
.SelectMany(asm =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return _asm.GetTypes();
|
||||
return asm.GetTypes();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Enumerable.Empty<Type>();
|
||||
}
|
||||
})
|
||||
.Where(_assemblyType => _assemblyType.GetInterface(nameof(IPlugin), false) != null);
|
||||
.Where(assemblyType => (assemblyType.GetInterface(nameof(IPlugin), false) ?? assemblyType.GetInterface(nameof(IPluginV2), false)) != null)
|
||||
.Where(assemblyType => !assemblyType.Namespace?.StartsWith(nameof(SharedLibraryCore)) ?? false);
|
||||
|
||||
_logger.LogDebug("Discovered {count} plugin implementations", pluginTypes.Count());
|
||||
|
||||
commandTypes = assemblies
|
||||
.SelectMany(_asm =>{
|
||||
.SelectMany(asm =>{
|
||||
try
|
||||
{
|
||||
return _asm.GetTypes();
|
||||
return asm.GetTypes();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Enumerable.Empty<Type>();
|
||||
}
|
||||
})
|
||||
.Where(_assemblyType => _assemblyType.IsClass && _assemblyType.BaseType == typeof(Command));
|
||||
.Where(assemblyType => assemblyType.IsClass && assemblyType.BaseType == typeof(Command))
|
||||
.Where(assemblyType => !assemblyType.Namespace?.StartsWith(nameof(SharedLibraryCore)) ?? false);
|
||||
|
||||
_logger.LogDebug("Discovered {count} plugin commands", commandTypes.Count());
|
||||
_logger.LogDebug("Discovered {Count} plugin commands", commandTypes.Count());
|
||||
|
||||
configurationTypes = assemblies
|
||||
.SelectMany(asm => {
|
||||
@ -125,9 +138,10 @@ namespace IW4MAdmin.Application.Misc
|
||||
}
|
||||
})
|
||||
.Where(asmType =>
|
||||
asmType.IsClass && asmType.GetInterface(nameof(IBaseConfiguration), false) != null);
|
||||
asmType.IsClass && asmType.GetInterface(nameof(IBaseConfiguration), false) != null)
|
||||
.Where(assemblyType => !assemblyType.Namespace?.StartsWith(nameof(SharedLibraryCore)) ?? false);
|
||||
|
||||
_logger.LogDebug("Discovered {count} configuration implementations", configurationTypes.Count());
|
||||
_logger.LogDebug("Discovered {Count} configuration implementations", configurationTypes.Count());
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,8 +169,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_pluginSubscription == null)
|
||||
_pluginSubscription = _masterApi.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result;
|
||||
_pluginSubscription ??= _masterApi.GetPluginSubscription(Guid.Parse(_appConfig.Id), _appConfig.SubscriptionId).Result;
|
||||
|
||||
return _remoteAssemblyHandler.DecryptScripts(_pluginSubscription.Where(sub => sub.Type == PluginType.Script).Select(sub => sub.Content).ToArray());
|
||||
}
|
@ -1,14 +1,17 @@
|
||||
using SharedLibraryCore;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models;
|
||||
using Data.Models.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Commands;
|
||||
using SharedLibraryCore.Configuration;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models.Client;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
|
||||
namespace IW4MAdmin.Application.Misc
|
||||
namespace IW4MAdmin.Application.Plugin.Script
|
||||
{
|
||||
/// <summary>
|
||||
/// generic script command implementation
|
||||
@ -20,8 +23,8 @@ namespace IW4MAdmin.Application.Misc
|
||||
|
||||
public ScriptCommand(string name, string alias, string description, bool isTargetRequired,
|
||||
EFClient.Permission permission,
|
||||
CommandArgument[] args, Func<GameEvent, Task> executeAction, CommandConfiguration config,
|
||||
ITranslationLookup layout, ILogger<ScriptCommand> logger, Server.Game[] supportedGames)
|
||||
IEnumerable<CommandArgument> args, Func<GameEvent, Task> executeAction, CommandConfiguration config,
|
||||
ITranslationLookup layout, ILogger<ScriptCommand> logger, IEnumerable<Reference.Game> supportedGames)
|
||||
: base(config, layout)
|
||||
{
|
||||
_executeAction = executeAction;
|
||||
@ -31,8 +34,8 @@ namespace IW4MAdmin.Application.Misc
|
||||
Description = description;
|
||||
RequiresTarget = isTargetRequired;
|
||||
Permission = permission;
|
||||
Arguments = args;
|
||||
SupportedGames = supportedGames;
|
||||
Arguments = args.ToArray();
|
||||
SupportedGames = supportedGames?.Select(game => (Server.Game)game).ToArray();
|
||||
}
|
||||
|
||||
public override async Task ExecuteAsync(GameEvent e)
|
||||
@ -48,7 +51,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to execute ScriptCommand action for command {command} {@event}", Name, e);
|
||||
_logger.LogError(ex, "Failed to execute ScriptCommand action for command {Command} {@Event}", Name, e);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +1,4 @@
|
||||
using System;
|
||||
using Jint;
|
||||
using Jint.Native;
|
||||
using Jint.Runtime;
|
||||
using Microsoft.CSharp.RuntimeBinder;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Exceptions;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -14,13 +6,25 @@ using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models;
|
||||
using IW4MAdmin.Application.Configuration;
|
||||
using IW4MAdmin.Application.Extensions;
|
||||
using IW4MAdmin.Application.Misc;
|
||||
using Jint;
|
||||
using Jint.Native;
|
||||
using Jint.Runtime;
|
||||
using Jint.Runtime.Interop;
|
||||
using Microsoft.CSharp.RuntimeBinder;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog.Context;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Commands;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Exceptions;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
|
||||
namespace IW4MAdmin.Application.Misc
|
||||
namespace IW4MAdmin.Application.Plugin.Script
|
||||
{
|
||||
/// <summary>
|
||||
/// implementation of IPlugin
|
||||
@ -70,7 +74,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
}
|
||||
|
||||
public async Task Initialize(IManager manager, IScriptCommandFactory scriptCommandFactory,
|
||||
IScriptPluginServiceResolver serviceResolver)
|
||||
IScriptPluginServiceResolver serviceResolver, IConfigurationHandlerV2<ScriptPluginConfiguration> configHandler)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -130,9 +134,15 @@ namespace IW4MAdmin.Application.Misc
|
||||
.AddObjectConverter(new PermissionLevelToStringConverter()));
|
||||
|
||||
_scriptEngine.Execute(script);
|
||||
if (!_scriptEngine.GetValue("init").IsUndefined())
|
||||
{
|
||||
// this is a v2 plugin and we don't want to try to load it
|
||||
Watcher.EnableRaisingEvents = false;
|
||||
Watcher.Dispose();
|
||||
return;
|
||||
}
|
||||
_scriptEngine.SetValue("_localization", Utilities.CurrentLocalization);
|
||||
_scriptEngine.SetValue("_serviceResolver", serviceResolver);
|
||||
_scriptEngine.SetValue("_lock", _onProcessing);
|
||||
dynamic pluginObject = _scriptEngine.Evaluate("plugin").ToObject();
|
||||
|
||||
Author = pluginObject.author;
|
||||
@ -191,8 +201,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
|
||||
catch (RuntimeBinderException)
|
||||
{
|
||||
var configWrapper = new ScriptPluginConfigurationWrapper(Name, _scriptEngine);
|
||||
await configWrapper.InitializeAsync();
|
||||
var configWrapper = new ScriptPluginConfigurationWrapper(Name, _scriptEngine, configHandler);
|
||||
|
||||
if (!loadComplete)
|
||||
{
|
||||
@ -252,7 +261,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
|
||||
try
|
||||
{
|
||||
await _onProcessing.WaitAsync();
|
||||
await _onProcessing.WaitAsync(Utilities.DefaultCommandTimeout / 2);
|
||||
shouldRelease = true;
|
||||
WrapJavaScriptErrorHandling(() =>
|
||||
{
|
||||
@ -269,7 +278,6 @@ namespace IW4MAdmin.Application.Misc
|
||||
_onProcessing.Release(1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public Task OnLoadAsync(IManager manager)
|
||||
@ -279,8 +287,6 @@ namespace IW4MAdmin.Application.Misc
|
||||
WrapJavaScriptErrorHandling(() =>
|
||||
{
|
||||
_scriptEngine.SetValue("_manager", manager);
|
||||
_scriptEngine.SetValue("getDvar", BeginGetDvar);
|
||||
_scriptEngine.SetValue("setDvar", BeginSetDvar);
|
||||
return _scriptEngine.Evaluate("plugin.onLoadAsync(_manager)");
|
||||
});
|
||||
|
||||
@ -289,12 +295,6 @@ namespace IW4MAdmin.Application.Misc
|
||||
|
||||
public Task OnTickAsync(Server server)
|
||||
{
|
||||
WrapJavaScriptErrorHandling(() =>
|
||||
{
|
||||
_scriptEngine.SetValue("_server", server);
|
||||
return _scriptEngine.Evaluate("plugin.onTickAsync(_server)");
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@ -415,10 +415,10 @@ namespace IW4MAdmin.Application.Misc
|
||||
}
|
||||
|
||||
string permission = dynamicCommand.permission;
|
||||
List<Server.Game> supportedGames = null;
|
||||
List<Reference.Game> supportedGames = null;
|
||||
var targetRequired = false;
|
||||
|
||||
var args = new List<(string, bool)>();
|
||||
var args = new List<CommandArgument>();
|
||||
dynamic arguments = null;
|
||||
|
||||
try
|
||||
@ -445,7 +445,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
{
|
||||
foreach (var arg in dynamicCommand.arguments)
|
||||
{
|
||||
args.Add((arg.name, (bool)arg.required));
|
||||
args.Add(new CommandArgument { Name = arg.name, Required = (bool)arg.required });
|
||||
}
|
||||
}
|
||||
|
||||
@ -453,8 +453,8 @@ namespace IW4MAdmin.Application.Misc
|
||||
{
|
||||
foreach (var game in dynamicCommand.supportedGames)
|
||||
{
|
||||
supportedGames ??= new List<Server.Game>();
|
||||
supportedGames.Add(Enum.Parse(typeof(Server.Game), game.ToString()));
|
||||
supportedGames ??= new List<Reference.Game>();
|
||||
supportedGames.Add(Enum.Parse(typeof(Reference.Game), game.ToString()));
|
||||
}
|
||||
}
|
||||
catch (RuntimeBinderException)
|
||||
@ -507,175 +507,12 @@ namespace IW4MAdmin.Application.Misc
|
||||
}
|
||||
|
||||
commandList.Add(scriptCommandFactory.CreateScriptCommand(name, alias, description, permission,
|
||||
targetRequired, args, Execute, supportedGames?.ToArray()));
|
||||
targetRequired, args, Execute, supportedGames));
|
||||
}
|
||||
|
||||
return commandList;
|
||||
}
|
||||
|
||||
private void BeginGetDvar(Server server, string dvarName, Delegate onCompleted)
|
||||
{
|
||||
var operationTimeout = TimeSpan.FromSeconds(5);
|
||||
|
||||
void OnComplete(IAsyncResult result)
|
||||
{
|
||||
try
|
||||
{
|
||||
_onProcessing.Wait();
|
||||
|
||||
var (success, value) = (ValueTuple<bool, string>)result.AsyncState;
|
||||
onCompleted.DynamicInvoke(JsValue.Undefined,
|
||||
new[]
|
||||
{
|
||||
JsValue.FromObject(_scriptEngine, server),
|
||||
JsValue.FromObject(_scriptEngine, dvarName),
|
||||
JsValue.FromObject(_scriptEngine, value),
|
||||
JsValue.FromObject(_scriptEngine, success)
|
||||
});
|
||||
}
|
||||
catch (JavaScriptException ex)
|
||||
{
|
||||
using (LogContext.PushProperty("Server", server.ToString()))
|
||||
{
|
||||
_logger.LogError(ex, "Could not invoke BeginGetDvar callback for {Filename} {@Location}",
|
||||
Path.GetFileName(_fileName), ex.Location);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not complete {BeginGetDvar} for {Class}", nameof(BeginGetDvar), Name);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_onProcessing.CurrentCount == 0)
|
||||
{
|
||||
_onProcessing.Release(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new Thread(() =>
|
||||
{
|
||||
if (DateTime.Now - (server.MatchEndTime ?? server.MatchStartTime) < TimeSpan.FromSeconds(15))
|
||||
{
|
||||
using (LogContext.PushProperty("Server", server.ToString()))
|
||||
{
|
||||
_logger.LogDebug("Not getting DVar because match recently ended");
|
||||
}
|
||||
|
||||
OnComplete(new AsyncResult
|
||||
{
|
||||
IsCompleted = false,
|
||||
AsyncState = (false, (string)null)
|
||||
});
|
||||
}
|
||||
|
||||
using var tokenSource = new CancellationTokenSource();
|
||||
tokenSource.CancelAfter(operationTimeout);
|
||||
|
||||
server.GetDvarAsync<string>(dvarName, token: tokenSource.Token).ContinueWith(action =>
|
||||
{
|
||||
if (action.IsCompletedSuccessfully)
|
||||
{
|
||||
OnComplete(new AsyncResult
|
||||
{
|
||||
IsCompleted = true,
|
||||
AsyncState = (true, action.Result.Value)
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
OnComplete(new AsyncResult
|
||||
{
|
||||
IsCompleted = false,
|
||||
AsyncState = (false, (string)null)
|
||||
});
|
||||
}
|
||||
});
|
||||
}).Start();
|
||||
}
|
||||
|
||||
private void BeginSetDvar(Server server, string dvarName, string dvarValue, Delegate onCompleted)
|
||||
{
|
||||
var operationTimeout = TimeSpan.FromSeconds(5);
|
||||
|
||||
void OnComplete(IAsyncResult result)
|
||||
{
|
||||
try
|
||||
{
|
||||
_onProcessing.Wait();
|
||||
var success = (bool)result.AsyncState;
|
||||
onCompleted.DynamicInvoke(JsValue.Undefined,
|
||||
new[]
|
||||
{
|
||||
JsValue.FromObject(_scriptEngine, server),
|
||||
JsValue.FromObject(_scriptEngine, dvarName),
|
||||
JsValue.FromObject(_scriptEngine, dvarValue),
|
||||
JsValue.FromObject(_scriptEngine, success)
|
||||
});
|
||||
}
|
||||
catch (JavaScriptException ex)
|
||||
{
|
||||
using (LogContext.PushProperty("Server", server.ToString()))
|
||||
{
|
||||
_logger.LogError(ex, "Could complete BeginSetDvar for {Filename} {@Location}",
|
||||
Path.GetFileName(_fileName), ex.Location);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Could not complete {BeginSetDvar} for {Class}", nameof(BeginSetDvar), Name);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (_onProcessing.CurrentCount == 0)
|
||||
{
|
||||
_onProcessing.Release(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new Thread(() =>
|
||||
{
|
||||
if (DateTime.Now - (server.MatchEndTime ?? server.MatchStartTime) < TimeSpan.FromSeconds(15))
|
||||
{
|
||||
using (LogContext.PushProperty("Server", server.ToString()))
|
||||
{
|
||||
_logger.LogDebug("Not setting DVar because match recently ended");
|
||||
}
|
||||
|
||||
OnComplete(new AsyncResult
|
||||
{
|
||||
IsCompleted = false,
|
||||
AsyncState = false
|
||||
});
|
||||
}
|
||||
|
||||
using var tokenSource = new CancellationTokenSource();
|
||||
tokenSource.CancelAfter(operationTimeout);
|
||||
|
||||
server.SetDvarAsync(dvarName, dvarValue, token: tokenSource.Token).ContinueWith(action =>
|
||||
{
|
||||
if (action.IsCompletedSuccessfully)
|
||||
{
|
||||
OnComplete(new AsyncResult
|
||||
{
|
||||
IsCompleted = true,
|
||||
AsyncState = true
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
OnComplete(new AsyncResult
|
||||
{
|
||||
IsCompleted = false,
|
||||
AsyncState = false
|
||||
});
|
||||
}
|
||||
});
|
||||
}).Start();
|
||||
}
|
||||
|
||||
private T WrapJavaScriptErrorHandling<T>(Func<T> work, object additionalData = null, Server server = null,
|
||||
[CallerMemberName] string methodName = "")
|
||||
{
|
@ -0,0 +1,93 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using IW4MAdmin.Application.Configuration;
|
||||
using Jint;
|
||||
using Jint.Native;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
|
||||
namespace IW4MAdmin.Application.Plugin.Script;
|
||||
|
||||
public class ScriptPluginConfigurationWrapper
|
||||
{
|
||||
private readonly ScriptPluginConfiguration _config;
|
||||
private readonly IConfigurationHandlerV2<ScriptPluginConfiguration> _configHandler;
|
||||
private readonly Engine _scriptEngine;
|
||||
private string _pluginName;
|
||||
|
||||
public ScriptPluginConfigurationWrapper(string pluginName, Engine scriptEngine, IConfigurationHandlerV2<ScriptPluginConfiguration> configHandler)
|
||||
{
|
||||
_pluginName = pluginName;
|
||||
_scriptEngine = scriptEngine;
|
||||
_configHandler = configHandler;
|
||||
_config = configHandler.Get("ScriptPluginSettings", new ScriptPluginConfiguration()).GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
public void SetName(string name)
|
||||
{
|
||||
_pluginName = name;
|
||||
}
|
||||
|
||||
public async Task SetValue(string key, object value)
|
||||
{
|
||||
var castValue = value;
|
||||
|
||||
if (value is double doubleValue)
|
||||
{
|
||||
castValue = AsInteger(doubleValue) ?? value;
|
||||
}
|
||||
|
||||
if (value is object[] array && array.All(item => item is double d && AsInteger(d) != null))
|
||||
{
|
||||
castValue = array.Select(item => AsInteger((double)item)).ToArray();
|
||||
}
|
||||
|
||||
if (!_config.ContainsKey(_pluginName))
|
||||
{
|
||||
_config.Add(_pluginName, new Dictionary<string, object>());
|
||||
}
|
||||
|
||||
var plugin = _config[_pluginName];
|
||||
|
||||
if (plugin.ContainsKey(key))
|
||||
{
|
||||
plugin[key] = castValue;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
plugin.Add(key, castValue);
|
||||
}
|
||||
|
||||
await _configHandler.Set(_config);
|
||||
}
|
||||
|
||||
public JsValue GetValue(string key)
|
||||
{
|
||||
if (!_config.ContainsKey(_pluginName))
|
||||
{
|
||||
return JsValue.Undefined;
|
||||
}
|
||||
|
||||
if (!_config[_pluginName].ContainsKey(key))
|
||||
{
|
||||
return JsValue.Undefined;
|
||||
}
|
||||
|
||||
var item = _config[_pluginName][key];
|
||||
|
||||
if (item is JsonElement { ValueKind: JsonValueKind.Array } jElem)
|
||||
{
|
||||
item = jElem.Deserialize<List<dynamic>>();
|
||||
}
|
||||
|
||||
return JsValue.FromObject(_scriptEngine, item);
|
||||
}
|
||||
|
||||
private static int? AsInteger(double value)
|
||||
{
|
||||
return int.TryParse(value.ToString(CultureInfo.InvariantCulture), out var parsed) ? parsed : null;
|
||||
}
|
||||
}
|
32
Application/Plugin/Script/ScriptPluginFactory.cs
Normal file
32
Application/Plugin/Script/ScriptPluginFactory.cs
Normal file
@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using IW4MAdmin.Application.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
|
||||
namespace IW4MAdmin.Application.Plugin.Script;
|
||||
|
||||
public class ScriptPluginFactory : IScriptPluginFactory
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
|
||||
public ScriptPluginFactory(IServiceProvider serviceProvider)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
}
|
||||
|
||||
public object CreateScriptPlugin(Type type, string fileName)
|
||||
{
|
||||
if (type == typeof(IPlugin))
|
||||
{
|
||||
return new ScriptPlugin(_serviceProvider.GetRequiredService<ILogger<ScriptPlugin>>(),
|
||||
fileName);
|
||||
}
|
||||
|
||||
return new ScriptPluginV2(fileName, _serviceProvider.GetRequiredService<ILogger<ScriptPluginV2>>(),
|
||||
_serviceProvider.GetRequiredService<IScriptPluginServiceResolver>(),
|
||||
_serviceProvider.GetRequiredService<IScriptCommandFactory>(),
|
||||
_serviceProvider.GetRequiredService<IConfigurationHandlerV2<ScriptPluginConfiguration>>(),
|
||||
_serviceProvider.GetRequiredService<IInteractionRegistration>());
|
||||
}
|
||||
}
|
136
Application/Plugin/Script/ScriptPluginHelper.cs
Normal file
136
Application/Plugin/Script/ScriptPluginHelper.cs
Normal file
@ -0,0 +1,136 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Jint.Native;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
|
||||
namespace IW4MAdmin.Application.Plugin.Script;
|
||||
|
||||
public class ScriptPluginHelper
|
||||
{
|
||||
private readonly IManager _manager;
|
||||
private readonly ScriptPluginV2 _scriptPlugin;
|
||||
private readonly SemaphoreSlim _onRequestRunning = new(1, 5);
|
||||
private const int RequestTimeout = 500;
|
||||
|
||||
public ScriptPluginHelper(IManager manager, ScriptPluginV2 scriptPlugin)
|
||||
{
|
||||
_manager = manager;
|
||||
_scriptPlugin = scriptPlugin;
|
||||
}
|
||||
|
||||
public void GetUrl(string url, Delegate callback)
|
||||
{
|
||||
RequestUrl(new ScriptPluginWebRequest(url), callback);
|
||||
}
|
||||
|
||||
public void GetUrl(string url, Dictionary<string, string> headers, Delegate callback)
|
||||
{
|
||||
RequestUrl(new ScriptPluginWebRequest(url, Headers: headers), callback);
|
||||
}
|
||||
|
||||
public void PostUrl(string url, Dictionary<string, string> headers, Delegate callback)
|
||||
{
|
||||
RequestUrl(new ScriptPluginWebRequest(url, null, "POST", Headers: headers), callback);
|
||||
}
|
||||
|
||||
public void RequestUrl(ScriptPluginWebRequest request, Delegate callback)
|
||||
{
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = RequestInternal(request);
|
||||
_scriptPlugin.ExecuteWithErrorHandling(scriptEngine =>
|
||||
{
|
||||
callback.DynamicInvoke(JsValue.Undefined, new[] { JsValue.FromObject(scriptEngine, response) });
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void RequestNotify(int delayMs, Delegate callback)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(delayMs, _manager.CancellationToken);
|
||||
_scriptPlugin.ExecuteWithErrorHandling(_ => callback.DynamicInvoke(JsValue.Undefined));
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private object RequestInternal(ScriptPluginWebRequest request)
|
||||
{
|
||||
var entered = false;
|
||||
using var tokenSource = new CancellationTokenSource(RequestTimeout);
|
||||
|
||||
using var client = new HttpClient();
|
||||
|
||||
try
|
||||
{
|
||||
_onRequestRunning.Wait(tokenSource.Token);
|
||||
|
||||
entered = true;
|
||||
var requestMessage = new HttpRequestMessage(new HttpMethod(request.Method), request.Url);
|
||||
|
||||
if (request.Body is not null)
|
||||
{
|
||||
requestMessage.Content = new StringContent(request.Body.ToString() ?? string.Empty, Encoding.Default,
|
||||
request.ContentType ?? "text/plain");
|
||||
}
|
||||
|
||||
if (request.Headers is not null)
|
||||
{
|
||||
foreach (var (key, value) in request.Headers)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
requestMessage.Headers.Add(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var response = client.Send(requestMessage, tokenSource.Token);
|
||||
using var reader = new StreamReader(response.Content.ReadAsStream());
|
||||
return reader.ReadToEnd();
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return new
|
||||
{
|
||||
ex.StatusCode,
|
||||
ex.Message,
|
||||
IsError = true
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new
|
||||
{
|
||||
ex.Message,
|
||||
IsError = true
|
||||
};
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (entered)
|
||||
{
|
||||
_onRequestRunning.Release(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
|
||||
namespace IW4MAdmin.Application.Misc
|
||||
namespace IW4MAdmin.Application.Plugin.Script
|
||||
{
|
||||
/// <summary>
|
||||
/// implementation of IScriptPluginServiceResolver
|
||||
@ -25,7 +25,7 @@ namespace IW4MAdmin.Application.Misc
|
||||
public object ResolveService(string serviceName, string[] genericParameters)
|
||||
{
|
||||
var serviceType = DetermineRootType(serviceName, genericParameters.Length);
|
||||
var genericTypes = genericParameters.Select(_genericTypeParam => DetermineRootType(_genericTypeParam));
|
||||
var genericTypes = genericParameters.Select(genericTypeParam => DetermineRootType(genericTypeParam));
|
||||
var resolvedServiceType = serviceType.MakeGenericType(genericTypes.ToArray());
|
||||
return _serviceProvider.GetService(resolvedServiceType);
|
||||
}
|
||||
@ -34,8 +34,8 @@ namespace IW4MAdmin.Application.Misc
|
||||
{
|
||||
var typeCollection = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(t => t.GetTypes());
|
||||
string generatedName = $"{serviceName}{(genericParamCount == 0 ? "" : $"`{genericParamCount}")}".ToLower();
|
||||
var serviceType = typeCollection.FirstOrDefault(_type => _type.Name.ToLower() == generatedName);
|
||||
var generatedName = $"{serviceName}{(genericParamCount == 0 ? "" : $"`{genericParamCount}")}".ToLower();
|
||||
var serviceType = typeCollection.FirstOrDefault(type => type.Name.ToLower() == generatedName);
|
||||
|
||||
if (serviceType == null)
|
||||
{
|
@ -6,8 +6,9 @@ using Microsoft.Extensions.Logging;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
|
||||
namespace IW4MAdmin.Application.Misc;
|
||||
namespace IW4MAdmin.Application.Plugin.Script;
|
||||
|
||||
[Obsolete("This architecture is superseded by the request notify delay architecture")]
|
||||
public class ScriptPluginTimerHelper : IScriptPluginTimerHelper
|
||||
{
|
||||
private Timer _timer;
|
568
Application/Plugin/Script/ScriptPluginV2.cs
Normal file
568
Application/Plugin/Script/ScriptPluginV2.cs
Normal file
@ -0,0 +1,568 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Dynamic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models;
|
||||
using IW4MAdmin.Application.Configuration;
|
||||
using IW4MAdmin.Application.Extensions;
|
||||
using Jint;
|
||||
using Jint.Native;
|
||||
using Jint.Runtime;
|
||||
using Jint.Runtime.Interop;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Serilog.Context;
|
||||
using SharedLibraryCore;
|
||||
using SharedLibraryCore.Commands;
|
||||
using SharedLibraryCore.Database.Models;
|
||||
using SharedLibraryCore.Events.Server;
|
||||
using SharedLibraryCore.Exceptions;
|
||||
using SharedLibraryCore.Interfaces;
|
||||
using SharedLibraryCore.Interfaces.Events;
|
||||
using ILogger = Microsoft.Extensions.Logging.ILogger;
|
||||
using JavascriptEngine = Jint.Engine;
|
||||
|
||||
namespace IW4MAdmin.Application.Plugin.Script;
|
||||
|
||||
public class ScriptPluginV2 : IPluginV2
|
||||
{
|
||||
public string Name { get; private set; } = string.Empty;
|
||||
public string Author { get; private set; } = string.Empty;
|
||||
public string Version { get; private set; }
|
||||
|
||||
private readonly string _fileName;
|
||||
private readonly ILogger<ScriptPluginV2> _logger;
|
||||
private readonly IScriptPluginServiceResolver _pluginServiceResolver;
|
||||
private readonly IScriptCommandFactory _scriptCommandFactory;
|
||||
private readonly IConfigurationHandlerV2<ScriptPluginConfiguration> _configHandler;
|
||||
private readonly IInteractionRegistration _interactionRegistration;
|
||||
private readonly SemaphoreSlim _onProcessingScript = new(1, 1);
|
||||
private readonly SemaphoreSlim _onLoadingFile = new(1, 1);
|
||||
private readonly FileSystemWatcher _scriptWatcher;
|
||||
private readonly List<string> _registeredCommandNames = new();
|
||||
private readonly List<string> _registeredInteractions = new();
|
||||
private readonly Dictionary<MethodInfo, List<object>> _registeredEvents = new();
|
||||
private bool _firstInitialization = true;
|
||||
|
||||
private record ScriptPluginDetails(string Name, string Author, string Version,
|
||||
ScriptPluginCommandDetails[] Commands, ScriptPluginInteractionDetails[] Interactions);
|
||||
|
||||
private record ScriptPluginCommandDetails(string Name, string Description, string Alias, string Permission,
|
||||
bool TargetRequired, CommandArgument[] Arguments, IEnumerable<Reference.Game> SupportedGames, Delegate Execute);
|
||||
|
||||
private JavascriptEngine ScriptEngine
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (ActiveEngines)
|
||||
{
|
||||
return ActiveEngines[$"{GetHashCode()}-{_nextEngineId}"];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record ScriptPluginInteractionDetails(string Name, Delegate Action);
|
||||
|
||||
private ScriptPluginConfigurationWrapper _scriptPluginConfigurationWrapper;
|
||||
private int _nextEngineId;
|
||||
private static readonly Dictionary<string, JavascriptEngine> ActiveEngines = new();
|
||||
|
||||
public ScriptPluginV2(string fileName, ILogger<ScriptPluginV2> logger,
|
||||
IScriptPluginServiceResolver pluginServiceResolver, IScriptCommandFactory scriptCommandFactory,
|
||||
IConfigurationHandlerV2<ScriptPluginConfiguration> configHandler,
|
||||
IInteractionRegistration interactionRegistration)
|
||||
{
|
||||
_fileName = fileName;
|
||||
_logger = logger;
|
||||
_pluginServiceResolver = pluginServiceResolver;
|
||||
_scriptCommandFactory = scriptCommandFactory;
|
||||
_configHandler = configHandler;
|
||||
_interactionRegistration = interactionRegistration;
|
||||
_scriptWatcher = new FileSystemWatcher
|
||||
{
|
||||
Path = Path.Join(Utilities.OperatingDirectory, "Plugins"),
|
||||
NotifyFilter = NotifyFilters.LastWrite,
|
||||
Filter = _fileName.Split(Path.DirectorySeparatorChar).Last()
|
||||
};
|
||||
|
||||
IManagementEventSubscriptions.Load += OnLoad;
|
||||
}
|
||||
|
||||
public void ExecuteWithErrorHandling(Action<Engine> work)
|
||||
{
|
||||
WrapJavaScriptErrorHandling(() =>
|
||||
{
|
||||
work(ScriptEngine);
|
||||
return true;
|
||||
}, _logger, _fileName, _onProcessingScript);
|
||||
}
|
||||
|
||||
public object QueryWithErrorHandling(Delegate action, params object[] args)
|
||||
{
|
||||
return WrapJavaScriptErrorHandling(() =>
|
||||
{
|
||||
var jsArgs = args?.Select(param => JsValue.FromObject(ScriptEngine, param)).ToArray();
|
||||
var result = action.DynamicInvoke(JsValue.Undefined, jsArgs);
|
||||
return result;
|
||||
}, _logger, _fileName, _onProcessingScript);
|
||||
}
|
||||
|
||||
private async Task OnLoad(IManager manager, CancellationToken token)
|
||||
{
|
||||
var entered = false;
|
||||
try
|
||||
{
|
||||
await _onLoadingFile.WaitAsync(token);
|
||||
entered = true;
|
||||
|
||||
_logger.LogDebug("{Method} executing for {Plugin}", nameof(OnLoad), _fileName);
|
||||
|
||||
if (new FileInfo(_fileName).Length == 0L)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_scriptWatcher.EnableRaisingEvents = false;
|
||||
|
||||
UnregisterScriptEntities(manager);
|
||||
ResetEngineState();
|
||||
|
||||
if (_firstInitialization)
|
||||
{
|
||||
_scriptWatcher.Changed += async (_, _) => await OnLoad(manager, token);
|
||||
_firstInitialization = false;
|
||||
}
|
||||
|
||||
await using var stream =
|
||||
new FileStream(_fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
using var reader = new StreamReader(stream, Encoding.Default);
|
||||
var pluginScript = await reader.ReadToEndAsync();
|
||||
|
||||
var pluginDetails = WrapJavaScriptErrorHandling(() =>
|
||||
{
|
||||
if (IsEngineDisposed(GetHashCode(), _nextEngineId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
ScriptEngine.Execute(pluginScript);
|
||||
var initResult = ScriptEngine.Call("init", JsValue.FromObject(ScriptEngine, EventCallbackWrapper),
|
||||
JsValue.FromObject(ScriptEngine, _pluginServiceResolver),
|
||||
JsValue.FromObject(ScriptEngine, _scriptPluginConfigurationWrapper),
|
||||
JsValue.FromObject(ScriptEngine, new ScriptPluginHelper(manager, this)));
|
||||
|
||||
if (initResult.IsNull() || initResult.IsUndefined())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return AsScriptPluginInstance(initResult.ToObject());
|
||||
}, _logger, _fileName, _onProcessingScript);
|
||||
|
||||
if (pluginDetails is null)
|
||||
{
|
||||
_logger.LogInformation("No valid script plugin signature found for {FilePath}", _fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var command in pluginDetails.Commands)
|
||||
{
|
||||
RegisterCommand(manager, command);
|
||||
|
||||
_logger.LogDebug("Registered script plugin command {Command} for {Plugin}", command.Name,
|
||||
pluginDetails.Name);
|
||||
}
|
||||
|
||||
foreach (var interaction in pluginDetails.Interactions)
|
||||
{
|
||||
RegisterInteraction(interaction);
|
||||
|
||||
_logger.LogDebug("Registered script plugin interaction {Interaction} for {Plugin}", interaction.Name,
|
||||
pluginDetails.Name);
|
||||
}
|
||||
|
||||
Name = pluginDetails.Name;
|
||||
Author = pluginDetails.Author;
|
||||
Version = pluginDetails.Version;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unexpected error encountered loading script plugin {Name}", _fileName);
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (entered)
|
||||
{
|
||||
_onLoadingFile.Release(1);
|
||||
_scriptWatcher.EnableRaisingEvents = true;
|
||||
}
|
||||
|
||||
_logger.LogDebug("{Method} completed for {Plugin}", nameof(OnLoad), _fileName);
|
||||
}
|
||||
}
|
||||
|
||||
private void RegisterInteraction(ScriptPluginInteractionDetails interaction)
|
||||
{
|
||||
Task<IInteractionData> Action(int? targetId, Reference.Game? game, CancellationToken token) =>
|
||||
WrapJavaScriptErrorHandling(() =>
|
||||
{
|
||||
if (IsEngineDisposed(GetHashCode(), _nextEngineId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var args = new object[] { targetId, game, token }.Select(arg => JsValue.FromObject(ScriptEngine, arg))
|
||||
.ToArray();
|
||||
|
||||
if (interaction.Action.DynamicInvoke(JsValue.Undefined, args) is not ObjectWrapper result)
|
||||
{
|
||||
throw new PluginException("Invalid interaction object returned");
|
||||
}
|
||||
|
||||
return Task.FromResult((IInteractionData)result.ToObject());
|
||||
}, _logger, _fileName, _onProcessingScript);
|
||||
|
||||
_interactionRegistration.RegisterInteraction(interaction.Name, Action);
|
||||
_registeredInteractions.Add(interaction.Name);
|
||||
}
|
||||
|
||||
private void RegisterCommand(IManager manager, ScriptPluginCommandDetails command)
|
||||
{
|
||||
Task Execute(GameEvent gameEvent) =>
|
||||
WrapJavaScriptErrorHandling(() =>
|
||||
{
|
||||
if (IsEngineDisposed(GetHashCode(), _nextEngineId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
command.Execute.DynamicInvoke(JsValue.Undefined,
|
||||
new[] { JsValue.FromObject(ScriptEngine, gameEvent) });
|
||||
return Task.CompletedTask;
|
||||
}, _logger, _fileName, _onProcessingScript);
|
||||
|
||||
var scriptCommand = _scriptCommandFactory.CreateScriptCommand(command.Name, command.Alias,
|
||||
command.Description,
|
||||
command.Permission, command.TargetRequired,
|
||||
command.Arguments, Execute, command.SupportedGames);
|
||||
|
||||
manager.AddAdditionalCommand(scriptCommand);
|
||||
_registeredCommandNames.Add(scriptCommand.Name);
|
||||
}
|
||||
|
||||
private void ResetEngineState()
|
||||
{
|
||||
JavascriptEngine oldEngine = null;
|
||||
|
||||
lock (ActiveEngines)
|
||||
{
|
||||
if (ActiveEngines.ContainsKey($"{GetHashCode()}-{_nextEngineId}"))
|
||||
{
|
||||
oldEngine = ActiveEngines[$"{GetHashCode()}-{_nextEngineId}"];
|
||||
_logger.LogDebug("Removing script engine from active list {HashCode}", _nextEngineId);
|
||||
ActiveEngines.Remove($"{GetHashCode()}-{_nextEngineId}");
|
||||
}
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref _nextEngineId);
|
||||
oldEngine?.Dispose();
|
||||
var newEngine = new JavascriptEngine(cfg =>
|
||||
cfg.AddExtensionMethods(typeof(Utilities), typeof(Enumerable), typeof(Queryable),
|
||||
typeof(ScriptPluginExtensions), typeof(LoggerExtensions))
|
||||
.AllowClr(typeof(System.Net.Http.HttpClient).Assembly, typeof(EFClient).Assembly,
|
||||
typeof(Utilities).Assembly, typeof(Encoding).Assembly, typeof(CancellationTokenSource).Assembly,
|
||||
typeof(Data.Models.Client.EFClient).Assembly, typeof(IW4MAdmin.Plugins.Stats.Plugin).Assembly)
|
||||
.CatchClrExceptions()
|
||||
.AddObjectConverter(new EnumsToStringConverter()));
|
||||
|
||||
lock (ActiveEngines)
|
||||
{
|
||||
_logger.LogDebug("Adding script engine to active list {HashCode}", _nextEngineId);
|
||||
ActiveEngines.Add($"{GetHashCode()}-{_nextEngineId}", newEngine);
|
||||
}
|
||||
|
||||
_scriptPluginConfigurationWrapper =
|
||||
new ScriptPluginConfigurationWrapper(_fileName.Split(Path.DirectorySeparatorChar).Last(), ScriptEngine,
|
||||
_configHandler);
|
||||
}
|
||||
|
||||
private void UnregisterScriptEntities(IManager manager)
|
||||
{
|
||||
foreach (var commandName in _registeredCommandNames)
|
||||
{
|
||||
manager.RemoveCommandByName(commandName);
|
||||
_logger.LogDebug("Unregistered script plugin command {Command} for {Plugin}", commandName, Name);
|
||||
}
|
||||
|
||||
_registeredCommandNames.Clear();
|
||||
|
||||
foreach (var interactionName in _registeredInteractions)
|
||||
{
|
||||
_interactionRegistration.UnregisterInteraction(interactionName);
|
||||
}
|
||||
|
||||
_registeredInteractions.Clear();
|
||||
|
||||
foreach (var (removeMethod, subscriptions) in _registeredEvents)
|
||||
{
|
||||
foreach (var subscription in subscriptions)
|
||||
{
|
||||
removeMethod.Invoke(null, new[] { subscription });
|
||||
}
|
||||
|
||||
subscriptions.Clear();
|
||||
}
|
||||
|
||||
_registeredEvents.Clear();
|
||||
}
|
||||
|
||||
private void EventCallbackWrapper(string eventCallbackName, Delegate javascriptAction)
|
||||
{
|
||||
var eventCategory = eventCallbackName.Split(".")[0];
|
||||
|
||||
var eventCategoryType = eventCategory switch
|
||||
{
|
||||
nameof(IManagementEventSubscriptions) => typeof(IManagementEventSubscriptions),
|
||||
nameof(IGameEventSubscriptions) => typeof(IGameEventSubscriptions),
|
||||
nameof(IGameServerEventSubscriptions) => typeof(IGameServerEventSubscriptions),
|
||||
_ => null
|
||||
};
|
||||
|
||||
if (eventCategoryType is null)
|
||||
{
|
||||
_logger.LogWarning("{EventCategory} is not a valid subscription category", eventCategory);
|
||||
return;
|
||||
}
|
||||
|
||||
var eventName = eventCallbackName.Split(".")[1];
|
||||
var eventAddMethod = eventCategoryType.GetMethods()
|
||||
.FirstOrDefault(method => method.Name.StartsWith($"add_{eventName}"));
|
||||
var eventRemoveMethod = eventCategoryType.GetMethods()
|
||||
.FirstOrDefault(method => method.Name.StartsWith($"remove_{eventName}"));
|
||||
|
||||
if (eventAddMethod is null || eventRemoveMethod is null)
|
||||
{
|
||||
_logger.LogWarning("{EventName} is not a valid subscription event", eventName);
|
||||
return;
|
||||
}
|
||||
|
||||
var genericType = eventAddMethod.GetParameters()[0].ParameterType.GetGenericArguments()[0];
|
||||
|
||||
var eventWrapper =
|
||||
typeof(ScriptPluginV2).GetMethod(nameof(BuildEventWrapper), BindingFlags.Static | BindingFlags.NonPublic)!
|
||||
.MakeGenericMethod(genericType)
|
||||
.Invoke(null,
|
||||
new object[]
|
||||
{ _logger, _fileName, javascriptAction, GetHashCode(), _nextEngineId, _onProcessingScript });
|
||||
|
||||
eventAddMethod.Invoke(null, new[] { eventWrapper });
|
||||
|
||||
if (!_registeredEvents.ContainsKey(eventRemoveMethod))
|
||||
{
|
||||
_registeredEvents.Add(eventRemoveMethod, new List<object> { eventWrapper });
|
||||
}
|
||||
else
|
||||
{
|
||||
_registeredEvents[eventRemoveMethod].Add(eventWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
private static Func<TEventType, CancellationToken, Task> BuildEventWrapper<TEventType>(ILogger logger,
|
||||
string fileName, Delegate javascriptAction, int hashCode, int engineId, SemaphoreSlim onProcessingScript)
|
||||
{
|
||||
return (coreEvent, token) =>
|
||||
{
|
||||
return WrapJavaScriptErrorHandling(() =>
|
||||
{
|
||||
if (IsEngineDisposed(hashCode, engineId))
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
JavascriptEngine engine;
|
||||
|
||||
lock (ActiveEngines)
|
||||
{
|
||||
engine = ActiveEngines[$"{hashCode}-{engineId}"];
|
||||
}
|
||||
|
||||
var args = new object[] { coreEvent, token }
|
||||
.Select(param => JsValue.FromObject(engine, param))
|
||||
.ToArray();
|
||||
javascriptAction.DynamicInvoke(JsValue.Undefined, args);
|
||||
return Task.CompletedTask;
|
||||
}, logger, fileName, onProcessingScript, (coreEvent as GameServerEvent)?.Server,
|
||||
additionalData: coreEvent.GetType().Name);
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsEngineDisposed(int hashCode, int engineId)
|
||||
{
|
||||
lock (ActiveEngines)
|
||||
{
|
||||
return !ActiveEngines.ContainsKey($"{hashCode}-{engineId}");
|
||||
}
|
||||
}
|
||||
|
||||
private static TResultType WrapJavaScriptErrorHandling<TResultType>(Func<TResultType> work, ILogger logger,
|
||||
string fileName, SemaphoreSlim onProcessingScript, IGameServer server = null, object additionalData = null,
|
||||
bool throwException = false,
|
||||
[CallerMemberName] string methodName = "")
|
||||
{
|
||||
using (LogContext.PushProperty("Server", server?.Id))
|
||||
{
|
||||
var waitCompleted = false;
|
||||
try
|
||||
{
|
||||
onProcessingScript.Wait();
|
||||
waitCompleted = true;
|
||||
return work();
|
||||
}
|
||||
catch (JavaScriptException ex)
|
||||
{
|
||||
logger.LogError(ex,
|
||||
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} at {@LocationInfo} StackTrace={StackTrace} {@AdditionalData}",
|
||||
methodName, Path.GetFileName(fileName), ex.Location, ex.StackTrace, additionalData);
|
||||
|
||||
if (throwException)
|
||||
{
|
||||
throw new PluginException("A runtime error occured while executing action for script plugin");
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex.InnerException is JavaScriptException jsEx)
|
||||
{
|
||||
logger.LogError(ex,
|
||||
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin} initialization {@LocationInfo} StackTrace={StackTrace} {@AdditionalData}",
|
||||
methodName, fileName, jsEx.Location, jsEx.JavaScriptStackTrace, additionalData);
|
||||
|
||||
if (throwException)
|
||||
{
|
||||
throw new PluginException("A runtime error occured while executing action for script plugin");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex,
|
||||
"Encountered JavaScript runtime error while executing {MethodName} for script plugin {Plugin}",
|
||||
methodName, Path.GetFileName(fileName));
|
||||
|
||||
if (throwException)
|
||||
{
|
||||
throw new PluginException("An error occured while executing action for script plugin");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (waitCompleted)
|
||||
{
|
||||
onProcessingScript.Release(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static ScriptPluginDetails AsScriptPluginInstance(dynamic source)
|
||||
{
|
||||
var commandDetails = Array.Empty<ScriptPluginCommandDetails>();
|
||||
if (HasProperty(source, "commands") && source.commands is dynamic[])
|
||||
{
|
||||
commandDetails = ((dynamic[])source.commands).Select(command =>
|
||||
{
|
||||
var commandArgs = Array.Empty<CommandArgument>();
|
||||
if (HasProperty(command, "arguments") && command.arguments is dynamic[])
|
||||
{
|
||||
commandArgs = ((dynamic[])command.arguments).Select(argument => new CommandArgument
|
||||
{
|
||||
Name = HasProperty(argument, "name") ? argument.name : string.Empty,
|
||||
Required = HasProperty(argument, "required") && argument.required is bool &&
|
||||
(bool)argument.required
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
var name = HasProperty(command, "name") && command.name is string
|
||||
? (string)command.name
|
||||
: string.Empty;
|
||||
var description = HasProperty(command, "description") && command.description is string
|
||||
? (string)command.description
|
||||
: string.Empty;
|
||||
var alias = HasProperty(command, "alias") && command.alias is string
|
||||
? (string)command.alias
|
||||
: string.Empty;
|
||||
var permission = HasProperty(command, "permission") && command.permission is string
|
||||
? (string)command.permission
|
||||
: string.Empty;
|
||||
var isTargetRequired = HasProperty(command, "targetRequired") && command.targetRequired is bool &&
|
||||
(bool)command.targetRequired;
|
||||
var supportedGames =
|
||||
HasProperty(command, "supportedGames") && command.supportedGames is IEnumerable<object>
|
||||
? ((IEnumerable<object>)command.supportedGames).Where(game => game?.ToString() is not null)
|
||||
.Select(game =>
|
||||
Enum.Parse<Reference.Game>(game.ToString()!))
|
||||
: Array.Empty<Reference.Game>();
|
||||
var execute = HasProperty(command, "execute") && command.execute is Delegate
|
||||
? (Delegate)command.execute
|
||||
: (GameEvent _) => Task.CompletedTask;
|
||||
|
||||
return new ScriptPluginCommandDetails(name, description, alias, permission, isTargetRequired,
|
||||
commandArgs, supportedGames, execute);
|
||||
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
var interactionDetails = Array.Empty<ScriptPluginInteractionDetails>();
|
||||
if (HasProperty(source, "interactions") && source.interactions is dynamic[])
|
||||
{
|
||||
interactionDetails = ((dynamic[])source.interactions).Select(interaction =>
|
||||
{
|
||||
var name = HasProperty(interaction, "name") && interaction.name is string
|
||||
? (string)interaction.name
|
||||
: string.Empty;
|
||||
var action = HasProperty(interaction, "action") && interaction.action is Delegate
|
||||
? (Delegate)interaction.action
|
||||
: null;
|
||||
|
||||
return new ScriptPluginInteractionDetails(name, action);
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
var name = HasProperty(source, "name") && source.name is string ? (string)source.name : string.Empty;
|
||||
var author = HasProperty(source, "author") && source.author is string ? (string)source.author : string.Empty;
|
||||
var version = HasProperty(source, "version") && source.version is string ? (string)source.author : string.Empty;
|
||||
|
||||
return new ScriptPluginDetails(name, author, version, commandDetails, interactionDetails);
|
||||
}
|
||||
|
||||
private static bool HasProperty(dynamic source, string name)
|
||||
{
|
||||
Type objType = source.GetType();
|
||||
|
||||
if (objType == typeof(ExpandoObject))
|
||||
{
|
||||
return ((IDictionary<string, object>)source).ContainsKey(name);
|
||||
}
|
||||
|
||||
return objType.GetProperty(name) != null;
|
||||
}
|
||||
|
||||
public class EnumsToStringConverter : IObjectConverter
|
||||
{
|
||||
public bool TryConvert(Engine engine, object value, out JsValue result)
|
||||
{
|
||||
if (value is Enum)
|
||||
{
|
||||
result = value.ToString();
|
||||
return true;
|
||||
}
|
||||
|
||||
result = JsValue.Null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
6
Application/Plugin/Script/ScriptPluginWebRequest.cs
Normal file
6
Application/Plugin/Script/ScriptPluginWebRequest.cs
Normal file
@ -0,0 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace IW4MAdmin.Application.Plugin.Script;
|
||||
|
||||
public record ScriptPluginWebRequest(string Url, object Body = null, string Method = "GET", string ContentType = "text/plain",
|
||||
Dictionary<string, string> Headers = null);
|
@ -1,7 +1,7 @@
|
||||
const init = (registerEventCallback, serviceResolver, _) => {
|
||||
plugin.onLoad(serviceResolver);
|
||||
|
||||
registerEventCallback('IManagementEventSubscriptions.ClientPenaltyAdministered', (penaltyEvent, token) => {
|
||||
registerEventCallback('IManagementEventSubscriptions.ClientPenaltyAdministered', (penaltyEvent, _) => {
|
||||
plugin.onPenalty(penaltyEvent);
|
||||
});
|
||||
|
||||
@ -10,7 +10,7 @@ const init = (registerEventCallback, serviceResolver, _) => {
|
||||
|
||||
const plugin = {
|
||||
author: 'RaidMax',
|
||||
version: '1.2',
|
||||
version: '2.0',
|
||||
name: 'Action on Report',
|
||||
enabled: false, // indicates if the plugin is enabled
|
||||
reportAction: 'TempBan', // can be TempBan or Ban
|
||||
@ -20,7 +20,7 @@ const plugin = {
|
||||
'report': 0
|
||||
},
|
||||
|
||||
onPenalty: function(penaltyEvent) {
|
||||
onPenalty: function (penaltyEvent) {
|
||||
if (!this.enabled || penaltyEvent.penalty.type !== this.penaltyType['report']) {
|
||||
return;
|
||||
}
|
||||
@ -48,7 +48,7 @@ const plugin = {
|
||||
}
|
||||
},
|
||||
|
||||
onLoad: function(serviceResolver) {
|
||||
onLoad: function (serviceResolver) {
|
||||
this.translations = serviceResolver.resolveService('ITranslationLookup');
|
||||
this.logger = serviceResolver.resolveService('ILogger', ['ScriptPluginV2']);
|
||||
this.logger.logInformation('ActionOnReport {version} by {author} loaded. Enabled={enabled}', this.version, this.author, this.enabled);
|
||||
|
@ -1,48 +1,60 @@
|
||||
const broadcastMessage = (server, message) => {
|
||||
server.Manager.GetServers().forEach(s => {
|
||||
s.Broadcast(message);
|
||||
});
|
||||
const init = (registerNotify, serviceResolver, config) => {
|
||||
registerNotify('IManagementEventSubscriptions.ClientPenaltyAdministered', (penaltyEvent, _) => plugin.onClientPenalty(penaltyEvent));
|
||||
|
||||
plugin.onLoad(serviceResolver, config);
|
||||
return plugin;
|
||||
};
|
||||
|
||||
const plugin = {
|
||||
author: 'Amos',
|
||||
version: 1.0,
|
||||
author: 'Amos, RaidMax',
|
||||
version: '2.0',
|
||||
name: 'Broadcast Bans',
|
||||
config: null,
|
||||
logger: null,
|
||||
translations: null,
|
||||
manager: null,
|
||||
|
||||
onEventAsync: function (gameEvent, server) {
|
||||
onClientPenalty: function (penaltyEvent) {
|
||||
if (!this.enableBroadcastBans) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (gameEvent.TypeName === 'Ban') {
|
||||
let penalty = undefined;
|
||||
gameEvent.Origin.AdministeredPenalties?.forEach(p => {
|
||||
penalty = p.AutomatedOffense;
|
||||
})
|
||||
let automatedPenaltyMessage;
|
||||
|
||||
if (gameEvent.Origin.ClientId === 1 && penalty !== undefined) {
|
||||
let localization = _localization.LocalizationIndex['PLUGINS_BROADCAST_BAN_ACMESSAGE'].replace('{{targetClient}}', gameEvent.Target.CleanedName);
|
||||
broadcastMessage(server, localization);
|
||||
penaltyEvent.penalty.punisher.administeredPenalties?.forEach(penalty => {
|
||||
automatedPenaltyMessage = penalty.automatedOffense;
|
||||
});
|
||||
|
||||
if (penaltyEvent.penalty.punisher.clientId === 1 && automatedPenaltyMessage !== undefined) {
|
||||
let message = this.translations['PLUGINS_BROADCAST_BAN_ACMESSAGE'].replace('{{targetClient}}', penaltyEvent.client.cleanedName);
|
||||
this.broadcastMessage(message);
|
||||
} else {
|
||||
let localization = _localization.LocalizationIndex['PLUGINS_BROADCAST_BAN_MESSAGE'].replace('{{targetClient}}', gameEvent.Target.CleanedName);
|
||||
broadcastMessage(server, localization);
|
||||
}
|
||||
let message = this.translations['PLUGINS_BROADCAST_BAN_MESSAGE'].replace('{{targetClient}}', penaltyEvent.client.cleanedName);
|
||||
this.broadcastMessage(message);
|
||||
}
|
||||
},
|
||||
|
||||
onLoadAsync: function (manager) {
|
||||
this.configHandler = _configHandler;
|
||||
this.enableBroadcastBans = this.configHandler.GetValue('EnableBroadcastBans');
|
||||
broadcastMessage: function (message) {
|
||||
this.manager.getServers().forEach(server => {
|
||||
server.broadcast(message);
|
||||
});
|
||||
},
|
||||
|
||||
onLoad: function (serviceResolver, config) {
|
||||
this.config = config;
|
||||
this.config.setName(this.name);
|
||||
this.enableBroadcastBans = this.config.getValue('EnableBroadcastBans');
|
||||
|
||||
this.manager = serviceResolver.resolveService('IManager');
|
||||
this.logger = serviceResolver.resolveService('ILogger', ['ScriptPluginV2']);
|
||||
this.translations = serviceResolver.resolveService('ITranslationLookup');
|
||||
|
||||
if (this.enableBroadcastBans === undefined) {
|
||||
this.enableBroadcastBans = false;
|
||||
this.configHandler.SetValue('EnableBroadcastBans', this.enableBroadcastBans);
|
||||
this.config.setValue('EnableBroadcastBans', this.enableBroadcastBans);
|
||||
}
|
||||
},
|
||||
|
||||
onUnloadAsync: function () {
|
||||
},
|
||||
|
||||
onTickAsync: function (server) {
|
||||
this.logger.logInformation('{Name} {Version} by {Author} loaded. Enabled={enabled}', this.name, this.version,
|
||||
this.author, this.enableBroadcastBans);
|
||||
}
|
||||
};
|
||||
|
@ -1,77 +1,376 @@
|
||||
const servers = {};
|
||||
const inDvar = 'sv_iw4madmin_in';
|
||||
const outDvar = 'sv_iw4madmin_out';
|
||||
const pollRate = 900;
|
||||
const enableCheckTimeout = 10000;
|
||||
let logger = {};
|
||||
const maxQueuedMessages = 25;
|
||||
const integrationEnabledDvar = 'sv_iw4madmin_integration_enabled';
|
||||
const pollingRate = 300;
|
||||
|
||||
let plugin = {
|
||||
const init = (registerNotify, serviceResolver, config) => {
|
||||
registerNotify('IManagementEventSubscriptions.ClientStateInitialized', (clientEvent, _) => plugin.onClientEnteredMatch(clientEvent));
|
||||
registerNotify('IGameServerEventSubscriptions.ServerValueReceived', (serverValueEvent, _) => plugin.onServerValueReceived(serverValueEvent));
|
||||
registerNotify('IGameServerEventSubscriptions.ServerValueSetCompleted', (serverValueEvent, _) => plugin.onServerValueSetCompleted(serverValueEvent));
|
||||
registerNotify('IGameServerEventSubscriptions.MonitoringStarted', (monitorStartEvent, _) => plugin.onServerMonitoringStart(monitorStartEvent));
|
||||
registerNotify('IManagementEventSubscriptions.ClientPenaltyAdministered', (penaltyEvent, _) => plugin.onPenalty(penaltyEvent));
|
||||
|
||||
plugin.onLoad(serviceResolver, config);
|
||||
return plugin;
|
||||
};
|
||||
|
||||
const plugin = {
|
||||
author: 'RaidMax',
|
||||
version: 1.1,
|
||||
version: '2.0',
|
||||
name: 'Game Interface',
|
||||
serviceResolver: null,
|
||||
eventManager: null,
|
||||
logger: null,
|
||||
commands: null,
|
||||
|
||||
onEventAsync: (gameEvent, server) => {
|
||||
if (servers[server.EndPoint] != null && !servers[server.EndPoint].enabled) {
|
||||
onLoad: function (serviceResolver, config) {
|
||||
this.serviceResolver = serviceResolver;
|
||||
this.eventManager = serviceResolver.resolveService('IManager');
|
||||
this.logger = serviceResolver.resolveService('ILogger', ['ScriptPluginV2']);
|
||||
this.commands = commands;
|
||||
this.config = config;
|
||||
},
|
||||
|
||||
onClientEnteredMatch: function (clientEvent) {
|
||||
const serverState = servers[clientEvent.client.currentServer.id];
|
||||
|
||||
if (serverState === undefined || serverState == null) {
|
||||
this.initializeServer(clientEvent.client.currentServer);
|
||||
} else if (!serverState.running && !serverState.initializationInProgress) {
|
||||
serverState.running = true;
|
||||
this.requestGetDvar(inDvar, clientEvent.client.currentServer);
|
||||
}
|
||||
},
|
||||
|
||||
onPenalty: function (penaltyEvent) {
|
||||
const warning = 1;
|
||||
if (penaltyEvent.penalty.type !== warning || !penaltyEvent.client.isIngame) {
|
||||
return;
|
||||
}
|
||||
|
||||
const eventType = String(gameEvent.TypeName).toLowerCase();
|
||||
|
||||
if (eventType === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (eventType) {
|
||||
case 'start':
|
||||
const enabled = initialize(server);
|
||||
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case 'preconnect':
|
||||
// when the plugin is reloaded after the servers are started
|
||||
if (servers[server.EndPoint] === undefined || servers[server.EndPoint] == null) {
|
||||
const enabled = initialize(server);
|
||||
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const timer = servers[server.EndPoint].timer;
|
||||
if (!timer.IsRunning) {
|
||||
timer.Start(0, pollRate);
|
||||
}
|
||||
break;
|
||||
case 'warn':
|
||||
const warningTitle = _localization.LocalizationIndex['GLOBAL_WARNING'];
|
||||
sendScriptCommand(server, 'Alert', gameEvent.Origin, gameEvent.Target, {
|
||||
alertType: warningTitle + '!',
|
||||
message: gameEvent.Data
|
||||
sendScriptCommand(penaltyEvent.client.currentServer, 'Alert', penaltyEvent.penalty.punisher, penaltyEvent.client, {
|
||||
alertType: this.translations('GLOBAL_WARNING') + '!',
|
||||
message: penaltyEvent.penalty.offense
|
||||
});
|
||||
break;
|
||||
},
|
||||
|
||||
onServerValueReceived: function (serverValueEvent) {
|
||||
const name = serverValueEvent.response.name;
|
||||
if (name === integrationEnabledDvar) {
|
||||
this.handleInitializeServerData(serverValueEvent);
|
||||
} else if (name === inDvar) {
|
||||
this.handleIncomingServerData(serverValueEvent);
|
||||
}
|
||||
},
|
||||
|
||||
onLoadAsync: manager => {
|
||||
logger = _serviceResolver.ResolveService('ILogger');
|
||||
logger.WriteInfo('Game Interface Startup');
|
||||
onServerValueSetCompleted: async function (serverValueEvent) {
|
||||
if (serverValueEvent.valueName !== inDvar && serverValueEvent.valueName !== outDvar) {
|
||||
this.logger.logDebug('Ignoring set complete of {name}', serverValueEvent.valueName);
|
||||
return;
|
||||
}
|
||||
|
||||
const serverState = servers[serverValueEvent.server.id];
|
||||
serverState.outQueue.shift();
|
||||
|
||||
this.logger.logDebug('outQueue len = {outLen}, inQueue len = {inLen}', serverState.outQueue.length, serverState.inQueue.length);
|
||||
|
||||
// if it didn't succeed, we need to retry
|
||||
if (!serverValueEvent.success && !this.eventManager.cancellationToken.isCancellationRequested) {
|
||||
this.logger.logDebug('Set of server value failed... retrying');
|
||||
this.requestSetDvar(serverValueEvent.valueName, serverValueEvent.value, serverValueEvent.server);
|
||||
return;
|
||||
}
|
||||
|
||||
// we informed the server that we received the event
|
||||
if (serverState.inQueue.length > 0 && serverValueEvent.valueName === inDvar) {
|
||||
const input = serverState.inQueue.shift();
|
||||
|
||||
// if we queued an event then the next loop will be at the value set complete
|
||||
if (await this.processEventMessage(input, serverValueEvent.server)) {
|
||||
// return;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.logDebug('loop complete');
|
||||
// loop restarts
|
||||
this.requestGetDvar(inDvar, serverValueEvent.server);
|
||||
},
|
||||
|
||||
onUnloadAsync: () => {
|
||||
for (let i = 0; i < servers.length; i++) {
|
||||
if (servers[i].enabled) {
|
||||
servers[i].timer.Stop();
|
||||
initializeServer: function (server) {
|
||||
servers[server.id] = {
|
||||
enabled: false,
|
||||
running: false,
|
||||
initializationInProgress: true,
|
||||
queuedMessages: [],
|
||||
inQueue: [],
|
||||
outQueue: [],
|
||||
commandQueue: []
|
||||
};
|
||||
|
||||
this.logger.logDebug('Initializing game interface for {serverId}', server.id);
|
||||
this.requestGetDvar(integrationEnabledDvar, server);
|
||||
},
|
||||
|
||||
handleInitializeServerData: function (responseEvent) {
|
||||
this.logger.logInformation('GSC integration enabled = {integrationValue} for {server}',
|
||||
responseEvent.response.value, responseEvent.server.id);
|
||||
|
||||
if (responseEvent.response.value !== '1') {
|
||||
return;
|
||||
}
|
||||
|
||||
const serverState = servers[responseEvent.server.id];
|
||||
serverState.outQueue.shift();
|
||||
serverState.enabled = true;
|
||||
serverState.running = true;
|
||||
serverState.initializationInProgress = false;
|
||||
|
||||
this.requestGetDvar(inDvar, responseEvent.server);
|
||||
},
|
||||
|
||||
handleIncomingServerData: function (responseEvent) {
|
||||
this.logger.logDebug('Received {dvarName}={dvarValue} success={success} from {server}', responseEvent.response.name,
|
||||
responseEvent.response.value, responseEvent.success, responseEvent.server.id);
|
||||
|
||||
const serverState = servers[responseEvent.server.id];
|
||||
serverState.outQueue.shift();
|
||||
|
||||
if (responseEvent.server.connectedClients.count === 0) {
|
||||
// no clients connected so we don't need to query
|
||||
serverState.running = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// read failed, so let's retry
|
||||
if (!responseEvent.success && !this.eventManager.cancellationToken.isCancellationRequested) {
|
||||
this.logger.logDebug('Get of server value failed... retrying');
|
||||
this.requestGetDvar(responseEvent.response.name, responseEvent.server);
|
||||
return;
|
||||
}
|
||||
|
||||
let input = responseEvent.response.value;
|
||||
const server = responseEvent.server;
|
||||
|
||||
if (this.eventManager.cancellationToken.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
// no data available so we poll again or send any outgoing messages
|
||||
if (isEmpty(input)) {
|
||||
this.logger.logDebug('No data to process from server');
|
||||
if (serverState.commandQueue.length > 0) {
|
||||
this.logger.logDebug('Sending next out message');
|
||||
const nextMessage = serverState.commandQueue.shift();
|
||||
this.requestSetDvar(outDvar, nextMessage, server);
|
||||
} else {
|
||||
this.requestGetDvar(inDvar, server);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
serverState.inQueue.push(input);
|
||||
|
||||
// let server know that we received the data
|
||||
this.requestSetDvar(inDvar, '', server);
|
||||
},
|
||||
|
||||
processEventMessage: async function (input, server) {
|
||||
let messageQueued = false;
|
||||
const event = parseEvent(input);
|
||||
|
||||
this.logger.logDebug('Processing input... {eventType} {subType} {data} {clientNumber}', event.eventType,
|
||||
event.subType, event.data.toString(), event.clientNumber);
|
||||
|
||||
const metaService = this.serviceResolver.ResolveService('IMetaServiceV2');
|
||||
const threading = importNamespace('System.Threading');
|
||||
const tokenSource = new threading.CancellationTokenSource();
|
||||
const token = tokenSource.token;
|
||||
|
||||
// todo: refactor to mapping if possible
|
||||
if (event.eventType === 'ClientDataRequested') {
|
||||
const client = server.getClientByNumber(event.clientNumber);
|
||||
|
||||
if (client != null) {
|
||||
this.logger.logDebug('Found client {name}', client.name);
|
||||
|
||||
let data = [];
|
||||
|
||||
const metaService = this.serviceResolver.ResolveService('IMetaServiceV2');
|
||||
|
||||
if (event.subType === 'Meta') {
|
||||
const meta = (await metaService.getPersistentMeta(event.data, client.clientId, token)).result;
|
||||
data[event.data] = meta === null ? '' : meta.Value;
|
||||
this.logger.logDebug('event data is {data}', event.data);
|
||||
} else {
|
||||
const clientStats = getClientStats(client, server);
|
||||
const tagMeta = (await metaService.getPersistentMetaByLookup('ClientTagV2', 'ClientTagNameV2', client.clientId, token)).result;
|
||||
data = {
|
||||
level: client.level,
|
||||
clientId: client.clientId,
|
||||
lastConnection: client.lastConnection,
|
||||
tag: tagMeta?.value ?? '',
|
||||
performance: clientStats?.performance ?? 200.0
|
||||
};
|
||||
}
|
||||
|
||||
this.sendEventMessage(server, false, 'ClientDataReceived', event.subType, client, undefined, data);
|
||||
messageQueued = true;
|
||||
} else {
|
||||
this.logger.logWarning('Could not find client slot {clientNumber} when processing {eventType}', event.clientNumber, event.eventType);
|
||||
this.sendEventMessage(server, false, 'ClientDataReceived', 'Fail', event.clientNumber, undefined, {
|
||||
ClientNumber: event.clientNumber
|
||||
});
|
||||
messageQueued = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (event.eventType === 'SetClientDataRequested') {
|
||||
let client = server.getClientByNumber(event.clientNumber);
|
||||
let clientId;
|
||||
|
||||
if (client != null) {
|
||||
clientId = client.clientId;
|
||||
} else {
|
||||
clientId = parseInt(event.data['clientId']);
|
||||
}
|
||||
|
||||
this.logger.logDebug('ClientId={clientId}', clientId);
|
||||
|
||||
if (clientId == null) {
|
||||
this.logger.logWarning('Could not find client slot {clientNumber} when processing {eventType}', event.clientNumber, event.eventType);
|
||||
this.sendEventMessage(server, false, 'SetClientDataCompleted', 'Meta', {
|
||||
ClientNumber: event.clientNumber
|
||||
}, undefined, {
|
||||
status: 'Fail'
|
||||
});
|
||||
messageQueued = true;
|
||||
} else {
|
||||
if (event.subType === 'Meta') {
|
||||
try {
|
||||
if (event.data['value'] != null && event.data['key'] != null) {
|
||||
this.logger.logDebug('Key={key}, Value={value}, Direction={direction} {token}', event.data['key'], event.data['value'], event.data['direction'], token);
|
||||
if (event.data['direction'] != null) {
|
||||
const parsedValue = parseInt(event.data['value']);
|
||||
const key = event.data['key'].toString();
|
||||
if (!isNaN(parsedValue)) {
|
||||
event.data['direction'] = 'up' ?
|
||||
(await metaService.incrementPersistentMeta(key, parsedValue, clientId, token)).result :
|
||||
(await metaService.decrementPersistentMeta(key, parsedValue, clientId, token)).result;
|
||||
}
|
||||
} else {
|
||||
const _ = (await metaService.setPersistentMeta(event.data['key'], event.data['value'], clientId, token)).result;
|
||||
}
|
||||
|
||||
if (event.data['key'] === 'PersistentClientGuid') {
|
||||
const serverEvents = importNamespace('SharedLibraryCore.Events.Management');
|
||||
const persistentIdEvent = new serverEvents.ClientPersistentIdReceiveEvent(client, event.data['value']);
|
||||
this.eventManager.queueEvent(persistentIdEvent);
|
||||
}
|
||||
}
|
||||
this.sendEventMessage(server, false, 'SetClientDataCompleted', 'Meta', {
|
||||
ClientNumber: event.clientNumber
|
||||
}, undefined, {
|
||||
status: 'Complete'
|
||||
});
|
||||
messageQueued = true;
|
||||
} catch (error) {
|
||||
this.sendEventMessage(server, false, 'SetClientDataCompleted', 'Meta', {
|
||||
ClientNumber: event.clientNumber
|
||||
}, undefined, {
|
||||
status: 'Fail'
|
||||
});
|
||||
this.logger.logError('Could not persist client meta {Key}={Value} {error} for {Client}', event.data['key'], event.data['value'], error.toString(), clientId);
|
||||
messageQueued = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tokenSource.dispose();
|
||||
return messageQueued;
|
||||
},
|
||||
|
||||
sendEventMessage: function (server, responseExpected, event, subtype, origin, target, data) {
|
||||
let targetClientNumber = -1;
|
||||
if (target != null) {
|
||||
targetClientNumber = target.ClientNumber;
|
||||
}
|
||||
|
||||
const output = `${responseExpected ? '1' : '0'};${event};${subtype};${origin.ClientNumber};${targetClientNumber};${buildDataString(data)}`;
|
||||
this.logger.logDebug('Queuing output for server {output}', output);
|
||||
|
||||
servers[server.id].commandQueue.push(output);
|
||||
},
|
||||
|
||||
requestGetDvar: function (dvarName, server) {
|
||||
const serverState = servers[server.id];
|
||||
const serverEvents = importNamespace('SharedLibraryCore.Events.Server');
|
||||
const requestEvent = new serverEvents.ServerValueRequestEvent(dvarName, server);
|
||||
requestEvent.delayMs = pollingRate;
|
||||
requestEvent.timeoutMs = 2000;
|
||||
requestEvent.source = this.name;
|
||||
|
||||
if (server.matchEndTime !== null) {
|
||||
const extraDelay = 15000;
|
||||
const end = new Date(server.matchEndTime.toString());
|
||||
const diff = new Date().getTime() - end.getTime();
|
||||
|
||||
if (diff < extraDelay) {
|
||||
requestEvent.delayMs = (extraDelay - diff) + pollingRate;
|
||||
this.logger.logDebug('Increasing delay time to {Delay}ms due to recent map change', requestEvent.delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.logDebug('requesting {dvar}', dvarName);
|
||||
|
||||
serverState.outQueue.push(requestEvent);
|
||||
|
||||
if (serverState.outQueue.length <= 1) {
|
||||
this.eventManager.queueEvent(requestEvent);
|
||||
} else {
|
||||
this.logger.logError('[requestGetDvar] Queue is full!');
|
||||
}
|
||||
},
|
||||
|
||||
onTickAsync: server => {
|
||||
requestSetDvar: function (dvarName, dvarValue, server) {
|
||||
const serverState = servers[server.id];
|
||||
|
||||
const serverEvents = importNamespace('SharedLibraryCore.Events.Server');
|
||||
const requestEvent = new serverEvents.ServerValueSetRequestEvent(dvarName, dvarValue, server);
|
||||
requestEvent.delayMs = pollingRate;
|
||||
requestEvent.timeoutMs = 2000;
|
||||
requestEvent.source = this.name;
|
||||
|
||||
if (server.matchEndTime !== null) {
|
||||
const extraDelay = 15000;
|
||||
const end = new Date(server.matchEndTime.toString());
|
||||
const diff = new Date().getTime() - end.getTime();
|
||||
|
||||
if (diff < extraDelay) {
|
||||
requestEvent.delayMs = (extraDelay - diff) + pollingRate;
|
||||
this.logger.logDebug('Increasing delay time to {Delay}ms due to recent map change', requestEvent.delayMs);
|
||||
}
|
||||
}
|
||||
|
||||
serverState.outQueue.push(requestEvent);
|
||||
|
||||
this.logger.logDebug('outQueue size = {length}', serverState.outQueue.length);
|
||||
|
||||
// if this is the only item in the out-queue we can send it immediately
|
||||
if (serverState.outQueue.length === 1) {
|
||||
this.eventManager.queueEvent(requestEvent);
|
||||
} else {
|
||||
this.logger.logError('[requestSetDvar] Queue is full!');
|
||||
}
|
||||
},
|
||||
|
||||
onServerMonitoringStart: function (monitorStartEvent) {
|
||||
this.initializeServer(monitorStartEvent.server);
|
||||
}
|
||||
};
|
||||
|
||||
let commands = [{
|
||||
const commands = [{
|
||||
name: 'giveweapon',
|
||||
description: 'gives specified weapon',
|
||||
alias: 'gw',
|
||||
@ -84,15 +383,18 @@ let commands = [{
|
||||
{
|
||||
name: 'weapon name',
|
||||
required: true
|
||||
}],
|
||||
}
|
||||
],
|
||||
supportedGames: ['IW4', 'IW5', 'T5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
|
||||
return;
|
||||
}
|
||||
sendScriptCommand(gameEvent.Owner, 'GiveWeapon', gameEvent.Origin, gameEvent.Target, {weaponName: gameEvent.Data});
|
||||
sendScriptCommand(gameEvent.owner, 'GiveWeapon', gameEvent.origin, gameEvent.target, {
|
||||
weaponName: gameEvent.data
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'takeweapons',
|
||||
description: 'take all weapons from specified player',
|
||||
@ -105,10 +407,10 @@ let commands = [{
|
||||
}],
|
||||
supportedGames: ['IW4', 'IW5', 'T5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
|
||||
return;
|
||||
}
|
||||
sendScriptCommand(gameEvent.Owner, 'TakeWeapons', gameEvent.Origin, gameEvent.Target, undefined);
|
||||
sendScriptCommand(gameEvent.owner, 'TakeWeapons', gameEvent.origin, gameEvent.target, undefined);
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -123,10 +425,10 @@ let commands = [{
|
||||
}],
|
||||
supportedGames: ['IW4', 'IW5', 'T5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
|
||||
return;
|
||||
}
|
||||
sendScriptCommand(gameEvent.Owner, 'SwitchTeams', gameEvent.Origin, gameEvent.Target, undefined);
|
||||
sendScriptCommand(gameEvent.owner, 'SwitchTeams', gameEvent.origin, gameEvent.target, undefined);
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -141,10 +443,10 @@ let commands = [{
|
||||
}],
|
||||
supportedGames: ['IW4', 'IW5', 'T5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
|
||||
return;
|
||||
}
|
||||
sendScriptCommand(gameEvent.Owner, 'LockControls', gameEvent.Origin, gameEvent.Target, undefined);
|
||||
sendScriptCommand(gameEvent.owner, 'LockControls', gameEvent.origin, gameEvent.target, undefined);
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -156,10 +458,10 @@ let commands = [{
|
||||
arguments: [],
|
||||
supportedGames: ['IW4', 'IW5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
|
||||
return;
|
||||
}
|
||||
sendScriptCommand(gameEvent.Owner, 'NoClip', gameEvent.Origin, gameEvent.Origin, undefined);
|
||||
sendScriptCommand(gameEvent.owner, 'NoClip', gameEvent.origin, gameEvent.origin, undefined);
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -171,10 +473,10 @@ let commands = [{
|
||||
arguments: [],
|
||||
supportedGames: ['IW4', 'IW5', 'T5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
|
||||
return;
|
||||
}
|
||||
sendScriptCommand(gameEvent.Owner, 'Hide', gameEvent.Origin, gameEvent.Origin, undefined);
|
||||
sendScriptCommand(gameEvent.owner, 'Hide', gameEvent.origin, gameEvent.origin, undefined);
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -190,13 +492,14 @@ let commands = [{
|
||||
{
|
||||
name: 'message',
|
||||
required: true
|
||||
}],
|
||||
}
|
||||
],
|
||||
supportedGames: ['IW4', 'IW5', 'T5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
|
||||
return;
|
||||
}
|
||||
sendScriptCommand(gameEvent.Owner, 'Alert', gameEvent.Origin, gameEvent.Target, {
|
||||
sendScriptCommand(gameEvent.Owner, 'Alert', gameEvent.origin, gameEvent.target, {
|
||||
alertType: 'Alert',
|
||||
message: gameEvent.Data
|
||||
});
|
||||
@ -214,10 +517,10 @@ let commands = [{
|
||||
}],
|
||||
supportedGames: ['IW4', 'IW5', 'T5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
|
||||
return;
|
||||
}
|
||||
sendScriptCommand(gameEvent.Owner, 'Goto', gameEvent.Origin, gameEvent.Target, undefined);
|
||||
sendScriptCommand(gameEvent.owner, 'Goto', gameEvent.origin, gameEvent.target, undefined);
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -232,10 +535,10 @@ let commands = [{
|
||||
}],
|
||||
supportedGames: ['IW4', 'IW5', 'T5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
|
||||
return;
|
||||
}
|
||||
sendScriptCommand(gameEvent.Owner, 'PlayerToMe', gameEvent.Origin, gameEvent.Target, undefined);
|
||||
sendScriptCommand(gameEvent.owner, 'PlayerToMe', gameEvent.origin, gameEvent.target, undefined);
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -255,15 +558,16 @@ let commands = [{
|
||||
{
|
||||
name: 'z',
|
||||
required: true
|
||||
}],
|
||||
}
|
||||
],
|
||||
supportedGames: ['IW4', 'IW5', 'T5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const args = String(gameEvent.Data).split(' ');
|
||||
sendScriptCommand(gameEvent.Owner, 'Goto', gameEvent.Origin, gameEvent.Target, {
|
||||
sendScriptCommand(gameEvent.owner, 'Goto', gameEvent.origin, gameEvent.target, {
|
||||
x: args[0],
|
||||
y: args[1],
|
||||
z: args[2]
|
||||
@ -282,10 +586,10 @@ let commands = [{
|
||||
}],
|
||||
supportedGames: ['IW4', 'IW5', 'T5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
|
||||
return;
|
||||
}
|
||||
sendScriptCommand(gameEvent.Owner, 'Kill', gameEvent.Origin, gameEvent.Target, undefined);
|
||||
sendScriptCommand(gameEvent.owner, 'Kill', gameEvent.origin, gameEvent.target, undefined);
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -300,244 +604,30 @@ let commands = [{
|
||||
}],
|
||||
supportedGames: ['IW4', 'IW5', 'T5'],
|
||||
execute: (gameEvent) => {
|
||||
if (!validateEnabled(gameEvent.Owner, gameEvent.Origin)) {
|
||||
if (!validateEnabled(gameEvent.owner, gameEvent.origin)) {
|
||||
return;
|
||||
}
|
||||
sendScriptCommand(gameEvent.Owner, 'SetSpectator', gameEvent.Origin, gameEvent.Target, undefined);
|
||||
sendScriptCommand(gameEvent.owner, 'SetSpectator', gameEvent.origin, gameEvent.target, undefined);
|
||||
}
|
||||
}];
|
||||
}
|
||||
];
|
||||
|
||||
const sendScriptCommand = (server, command, origin, target, data) => {
|
||||
const state = servers[server.EndPoint];
|
||||
if (state === undefined || !state.enabled) {
|
||||
const serverState = servers[server.id];
|
||||
if (serverState === undefined || !serverState.enabled) {
|
||||
return;
|
||||
}
|
||||
sendEvent(server, false, 'ExecuteCommandRequested', command, origin, target, data);
|
||||
}
|
||||
|
||||
const sendEvent = (server, responseExpected, event, subtype, origin, target, data) => {
|
||||
const logger = _serviceResolver.ResolveService('ILogger');
|
||||
const state = servers[server.EndPoint];
|
||||
|
||||
if (state.queuedMessages.length >= maxQueuedMessages) {
|
||||
logger.WriteWarning('Too many queued messages so we are skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
let targetClientNumber = -1;
|
||||
if (target != null) {
|
||||
targetClientNumber = target.ClientNumber;
|
||||
}
|
||||
|
||||
const output = `${responseExpected ? '1' : '0'};${event};${subtype};${origin.ClientNumber};${targetClientNumber};${buildDataString(data)}`;
|
||||
logger.WriteDebug(`Queuing output for server ${output}`);
|
||||
|
||||
state.queuedMessages.push(output);
|
||||
plugin.sendEventMessage(server, false, 'ExecuteCommandRequested', command, origin, target, data);
|
||||
};
|
||||
|
||||
const initialize = (server) => {
|
||||
const logger = _serviceResolver.ResolveService('ILogger');
|
||||
|
||||
servers[server.EndPoint] = {
|
||||
enabled: false
|
||||
}
|
||||
|
||||
let enabled = false;
|
||||
try {
|
||||
enabled = server.GetServerDvar('sv_iw4madmin_integration_enabled', enableCheckTimeout) === '1';
|
||||
} catch (error) {
|
||||
logger.WriteError(`Could not get integration status of ${server.EndPoint} - ${error}`);
|
||||
}
|
||||
|
||||
logger.WriteInfo(`GSC Integration enabled = ${enabled}`);
|
||||
|
||||
if (!enabled) {
|
||||
return false;
|
||||
}
|
||||
|
||||
logger.WriteDebug(`Setting up bus timer for ${server.EndPoint}`);
|
||||
|
||||
let timer = _serviceResolver.ResolveService('IScriptPluginTimerHelper');
|
||||
timer.OnTick(() => pollForEvents(server), `GameEventPoller ${server.ToString()}`);
|
||||
// necessary to prevent multi-threaded access to the JS context
|
||||
timer.SetDependency(_lock);
|
||||
|
||||
servers[server.EndPoint].timer = timer;
|
||||
servers[server.EndPoint].enabled = true;
|
||||
servers[server.EndPoint].waitingOnInput = false;
|
||||
servers[server.EndPoint].waitingOnOutput = false;
|
||||
servers[server.EndPoint].queuedMessages = [];
|
||||
|
||||
setDvar(server, inDvar, '', onSetDvar);
|
||||
setDvar(server, outDvar, '', onSetDvar);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const getClientStats = (client, server) => {
|
||||
const contextFactory = _serviceResolver.ResolveService('IDatabaseContextFactory');
|
||||
const context = contextFactory.CreateContext(false);
|
||||
const stats = context.ClientStatistics.GetClientsStatData([client.ClientId], server.GetId()); // .Find(client.ClientId, serverId);
|
||||
context.Dispose();
|
||||
const contextFactory = plugin.serviceResolver.ResolveService('IDatabaseContextFactory');
|
||||
const context = contextFactory.createContext(false);
|
||||
const stats = context.clientStatistics.getClientsStatData([client.ClientId], server.legacyDatabaseId);
|
||||
context.dispose();
|
||||
|
||||
return stats.length > 0 ? stats[0] : undefined;
|
||||
}
|
||||
|
||||
function onReceivedDvar(server, dvarName, dvarValue, success) {
|
||||
const logger = _serviceResolver.ResolveService('ILogger');
|
||||
logger.WriteDebug(`Received ${dvarName}=${dvarValue} success=${success}`);
|
||||
|
||||
let input = dvarValue;
|
||||
const state = servers[server.EndPoint];
|
||||
|
||||
if (state.waitingOnOutput && dvarName === outDvar && isEmpty(dvarValue)) {
|
||||
logger.WriteDebug('Setting out bus to read to send');
|
||||
// reset our flag letting use the out bus is open
|
||||
state.waitingOnOutput = !success;
|
||||
}
|
||||
|
||||
if (state.waitingOnInput && dvarName === inDvar) {
|
||||
logger.WriteDebug('Setting in bus to ready to receive');
|
||||
// we've received the data so now we can mark it as ready for more
|
||||
state.waitingOnInput = false;
|
||||
}
|
||||
|
||||
if (isEmpty(input)) {
|
||||
input = '';
|
||||
}
|
||||
|
||||
if (input.length > 0) {
|
||||
const event = parseEvent(input)
|
||||
|
||||
logger.WriteDebug(`Processing input... ${event.eventType} ${event.subType} ${event.data.toString()} ${event.clientNumber}`);
|
||||
|
||||
const metaService = _serviceResolver.ResolveService('IMetaServiceV2');
|
||||
const threading = importNamespace('System.Threading');
|
||||
const token = new threading.CancellationTokenSource().Token;
|
||||
|
||||
// todo: refactor to mapping if possible
|
||||
if (event.eventType === 'ClientDataRequested') {
|
||||
const client = server.GetClientByNumber(event.clientNumber);
|
||||
|
||||
if (client != null) {
|
||||
logger.WriteDebug(`Found client ${client.Name}`);
|
||||
|
||||
let data = [];
|
||||
|
||||
const metaService = _serviceResolver.ResolveService('IMetaServiceV2');
|
||||
|
||||
if (event.subType === 'Meta') {
|
||||
const meta = metaService.GetPersistentMeta(event.data, client.ClientId, token).GetAwaiter().GetResult();
|
||||
data[event.data] = meta === null ? '' : meta.Value;
|
||||
logger.WriteDebug(`event data is ${event.data}`);
|
||||
} else {
|
||||
const clientStats = getClientStats(client, server);
|
||||
const tagMeta = metaService.GetPersistentMetaByLookup('ClientTagV2', 'ClientTagNameV2', client.ClientId, token).GetAwaiter().GetResult();
|
||||
data = {
|
||||
level: client.Level,
|
||||
clientId: client.ClientId,
|
||||
lastConnection: client.LastConnection,
|
||||
tag: tagMeta?.Value ?? '',
|
||||
performance: clientStats?.Performance ?? 200.0
|
||||
};
|
||||
}
|
||||
|
||||
sendEvent(server, false, 'ClientDataReceived', event.subType, client, undefined, data);
|
||||
} else {
|
||||
logger.WriteWarning(`Could not find client slot ${event.clientNumber} when processing ${event.eventType}`);
|
||||
sendEvent(server, false, 'ClientDataReceived', 'Fail', event.clientNumber, undefined, {ClientNumber: event.clientNumber});
|
||||
}
|
||||
}
|
||||
|
||||
if (event.eventType === 'SetClientDataRequested') {
|
||||
let client = server.GetClientByNumber(event.clientNumber);
|
||||
let clientId;
|
||||
|
||||
if (client != null) {
|
||||
clientId = client.ClientId;
|
||||
} else {
|
||||
clientId = parseInt(event.data.clientId);
|
||||
}
|
||||
|
||||
logger.WriteDebug(`ClientId=${clientId}`);
|
||||
|
||||
if (clientId == null) {
|
||||
logger.WriteWarning(`Could not find client slot ${event.clientNumber} when processing ${event.eventType}`);
|
||||
sendEvent(server, false, 'SetClientDataCompleted', 'Meta', {ClientNumber: event.clientNumber}, undefined, {status: 'Fail'});
|
||||
} else {
|
||||
if (event.subType === 'Meta') {
|
||||
try {
|
||||
if (event.data['value'] != null && event.data['key'] != null) {
|
||||
logger.WriteDebug(`Key=${event.data['key']}, Value=${event.data['value']}, Direction=${event.data['direction']} ${token}`);
|
||||
if (event.data['direction'] != null) {
|
||||
event.data['direction'] = 'up'
|
||||
? metaService.IncrementPersistentMeta(event.data['key'], parseInt(event.data['value']), clientId, token).GetAwaiter().GetResult()
|
||||
: metaService.DecrementPersistentMeta(event.data['key'], parseInt(event.data['value']), clientId, token).GetAwaiter().GetResult();
|
||||
} else {
|
||||
metaService.SetPersistentMeta(event.data['key'], event.data['value'], clientId, token).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
sendEvent(server, false, 'SetClientDataCompleted', 'Meta', {ClientNumber: event.clientNumber}, undefined, {status: 'Complete'});
|
||||
} catch (error) {
|
||||
sendEvent(server, false, 'SetClientDataCompleted', 'Meta', {ClientNumber: event.clientNumber}, undefined, {status: 'Fail'});
|
||||
logger.WriteError('Could not persist client meta ' + error.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setDvar(server, inDvar, '', onSetDvar);
|
||||
} else if (server.ClientNum === 0) {
|
||||
servers[server.EndPoint].timer.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
function onSetDvar(server, dvarName, dvarValue, success) {
|
||||
const logger = _serviceResolver.ResolveService('ILogger');
|
||||
logger.WriteDebug(`Completed set of dvar ${dvarName}=${dvarValue}, success=${success}`);
|
||||
|
||||
const state = servers[server.EndPoint];
|
||||
|
||||
if (dvarName === inDvar && success && isEmpty(dvarValue)) {
|
||||
logger.WriteDebug('In bus is ready for new data');
|
||||
// reset our flag letting use the in bus is ready for more data
|
||||
state.waitingOnInput = false;
|
||||
}
|
||||
}
|
||||
|
||||
const pollForEvents = server => {
|
||||
const state = servers[server.EndPoint];
|
||||
|
||||
if (state === null || !state.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (server.Throttled) {
|
||||
logger.WriteDebug('Server is throttled so we are not polling for game data');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.waitingOnInput) {
|
||||
state.waitingOnInput = true;
|
||||
logger.WriteDebug('Attempting to get in dvar value');
|
||||
getDvar(server, inDvar, onReceivedDvar);
|
||||
}
|
||||
|
||||
if (!state.waitingOnOutput) {
|
||||
if (state.queuedMessages.length === 0) {
|
||||
logger.WriteDebug('No messages in queue');
|
||||
return;
|
||||
}
|
||||
|
||||
state.waitingOnOutput = true;
|
||||
const nextMessage = state.queuedMessages.splice(0, 1);
|
||||
setDvar(server, outDvar, nextMessage, onSetDvar);
|
||||
}
|
||||
|
||||
if (state.waitingOnOutput) {
|
||||
getDvar(server, outDvar, onReceivedDvar);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const parseEvent = (input) => {
|
||||
if (input === undefined) {
|
||||
@ -551,8 +641,8 @@ const parseEvent = (input) => {
|
||||
subType: eventInfo[2],
|
||||
clientNumber: eventInfo[3],
|
||||
data: eventInfo.length > 4 ? parseDataString(eventInfo[4]) : undefined
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const buildDataString = data => {
|
||||
if (data === undefined) {
|
||||
@ -561,21 +651,23 @@ const buildDataString = data => {
|
||||
|
||||
let formattedData = '';
|
||||
|
||||
for (const prop in data) {
|
||||
formattedData += `${prop}=${data[prop]}|`;
|
||||
for (let [key, value] of Object.entries(data)) {
|
||||
formattedData += `${key}=${value}|`;
|
||||
}
|
||||
|
||||
return formattedData.substring(0, Math.max(0, formattedData.length - 1));
|
||||
}
|
||||
return formattedData.slice(0, -1);
|
||||
};
|
||||
|
||||
const parseDataString = data => {
|
||||
if (data === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const dict = {}
|
||||
const dict = {};
|
||||
const split = data.split('|');
|
||||
|
||||
for (const segment of data.split('|')) {
|
||||
for (let i = 0; i < split.length; i++) {
|
||||
const segment = split[i];
|
||||
const keyValue = segment.split('=');
|
||||
if (keyValue.length !== 2) {
|
||||
continue;
|
||||
@ -584,16 +676,16 @@ const parseDataString = data => {
|
||||
}
|
||||
|
||||
return Object.keys(dict).length === 0 ? data : dict;
|
||||
}
|
||||
};
|
||||
|
||||
const validateEnabled = (server, origin) => {
|
||||
const enabled = servers[server.EndPoint] != null && servers[server.EndPoint].enabled;
|
||||
const enabled = servers[server.id] != null && servers[server.id].enabled;
|
||||
if (!enabled) {
|
||||
origin.Tell('Game interface is not enabled on this server');
|
||||
origin.tell('Game interface is not enabled on this server');
|
||||
}
|
||||
return enabled;
|
||||
}
|
||||
};
|
||||
|
||||
function isEmpty(value) {
|
||||
const isEmpty = (value) => {
|
||||
return value == null || false || value === '' || value === 'null';
|
||||
}
|
||||
};
|
||||
|
@ -1,90 +0,0 @@
|
||||
let commands = [{
|
||||
// required
|
||||
name: "pingpong",
|
||||
// required
|
||||
description: "pongs a ping",
|
||||
// required
|
||||
alias: "pp",
|
||||
// required
|
||||
permission: "User",
|
||||
// optional (defaults to false)
|
||||
targetRequired: false,
|
||||
// optional
|
||||
arguments: [{
|
||||
name: "times to ping",
|
||||
required: true
|
||||
}],
|
||||
// required
|
||||
execute: (gameEvent) => {
|
||||
// parse the first argument (number of times)
|
||||
let times = parseInt(gameEvent.Data);
|
||||
|
||||
// we only want to allow ping pong up to 5 times
|
||||
if (times > 5 || times <= 0) {
|
||||
gameEvent.Origin.Tell("You can only ping pong between 1 and 5 times");
|
||||
return;
|
||||
}
|
||||
|
||||
// we want to print out a pong message for the number of times they requested
|
||||
for (var i = 0; i < times; i++) {
|
||||
gameEvent.Origin.Tell(`^${i}pong #${i + 1}^7`);
|
||||
|
||||
// don't want to wait if it's the last pong
|
||||
if (i < times - 1) {
|
||||
System.Threading.Tasks.Task.Delay(1000).Wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
let plugin = {
|
||||
author: 'RaidMax',
|
||||
version: 1.1,
|
||||
name: 'Ping Pong Sample Command Plugin',
|
||||
|
||||
onEventAsync: function (gameEvent, server) {
|
||||
},
|
||||
|
||||
onLoadAsync: function (manager) {
|
||||
this.logger = _serviceResolver.ResolveService("ILogger");
|
||||
this.logger.WriteDebug("sample plugin loaded");
|
||||
|
||||
const intArray = [
|
||||
1337,
|
||||
1505,
|
||||
999
|
||||
];
|
||||
|
||||
const stringArray = [
|
||||
"ping",
|
||||
"pong",
|
||||
"hello"
|
||||
];
|
||||
|
||||
this.configHandler = _configHandler;
|
||||
|
||||
this.configHandler.SetValue("SampleIntegerValue", 123);
|
||||
this.configHandler.SetValue("SampleStringValue", this.author);
|
||||
this.configHandler.SetValue("SampleFloatValue", this.version);
|
||||
this.configHandler.SetValue("SampleNumericalArray", intArray);
|
||||
this.configHandler.SetValue("SampleStringArray", stringArray);
|
||||
|
||||
this.logger.WriteDebug(this.configHandler.GetValue("SampleIntegerValue"));
|
||||
this.logger.WriteDebug(this.configHandler.GetValue("SampleStringValue"));
|
||||
this.logger.WriteDebug(this.configHandler.GetValue("SampleFloatValue"));
|
||||
|
||||
this.configHandler.GetValue("SampleNumericalArray").forEach((element) => {
|
||||
this.logger.WriteDebug(element);
|
||||
});
|
||||
|
||||
this.configHandler.GetValue("SampleStringArray").forEach((element) => {
|
||||
this.logger.WriteDebug(element);
|
||||
});
|
||||
},
|
||||
|
||||
onUnloadAsync: function () {
|
||||
},
|
||||
|
||||
onTickAsync: function (server) {
|
||||
}
|
||||
};
|
@ -1,39 +0,0 @@
|
||||
var plugin = {
|
||||
author: 'RaidMax',
|
||||
version: 1.1,
|
||||
name: 'Shared GUID Kicker Plugin',
|
||||
|
||||
onEventAsync: function (gameEvent, server) {
|
||||
// make sure we only check for IW4(x)
|
||||
if (server.GameName !== 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// connect or join event
|
||||
if (gameEvent.Type === 3) {
|
||||
// this GUID seems to have been packed in a IW4 torrent and results in an unreasonable amount of people using the same GUID
|
||||
if (gameEvent.Origin.NetworkId === -805366929435212061 ||
|
||||
gameEvent.Origin.NetworkId === 3150799945255696069 ||
|
||||
gameEvent.Origin.NetworkId === 5859032128210324569 ||
|
||||
gameEvent.Origin.NetworkId === 2908745942105435771 ||
|
||||
gameEvent.Origin.NetworkId === -6492697076432899192 ||
|
||||
gameEvent.Origin.NetworkId === 1145760003260769995 ||
|
||||
gameEvent.Origin.NetworkId === -7102887284306116957 ||
|
||||
gameEvent.Origin.NetworkId === 3474936520447289592 ||
|
||||
gameEvent.Origin.NetworkId === -1168897558496584395 ||
|
||||
gameEvent.Origin.NetworkId === 8348020621355817691 ||
|
||||
gameEvent.Origin.NetworkId === 3259219574061214058 ||
|
||||
gameEvent.Origin.NetworkId === 3304388024725980231) {
|
||||
gameEvent.Origin.Kick('Your GUID is generic. Delete players/guids.dat and rejoin', _IW4MAdminClient);
|
||||
}
|
||||
}
|
||||
},
|
||||
onLoadAsync: function (manager) {
|
||||
},
|
||||
|
||||
onUnloadAsync: function () {
|
||||
},
|
||||
|
||||
onTickAsync: function (server) {
|
||||
}
|
||||
};
|
@ -3,31 +3,48 @@ const validCIDR = input => cidrRegex.test(input);
|
||||
const subnetBanlistKey = 'Webfront::Nav::Admin::SubnetBanlist';
|
||||
let subnetList = [];
|
||||
|
||||
const commands = [{
|
||||
name: "bansubnet",
|
||||
description: "bans an IPv4 subnet",
|
||||
alias: "bs",
|
||||
permission: "SeniorAdmin",
|
||||
const init = (registerNotify, serviceResolver, config) => {
|
||||
registerNotify('IManagementEventSubscriptions.ClientStateAuthorized', (authorizedEvent, _) => plugin.onClientAuthorized(authorizedEvent));
|
||||
|
||||
plugin.onLoad(serviceResolver, config);
|
||||
return plugin;
|
||||
};
|
||||
|
||||
const plugin = {
|
||||
author: 'RaidMax',
|
||||
version: '2.0',
|
||||
name: 'Subnet Banlist Plugin',
|
||||
manager: null,
|
||||
logger: null,
|
||||
config: null,
|
||||
serviceResolver: null,
|
||||
banMessage: '',
|
||||
|
||||
commands: [{
|
||||
name: 'bansubnet',
|
||||
description: 'bans an IPv4 subnet',
|
||||
alias: 'bs',
|
||||
permission: 'SeniorAdmin',
|
||||
targetRequired: false,
|
||||
arguments: [{
|
||||
name: "subnet in IPv4 CIDR notation",
|
||||
name: 'subnet in IPv4 CIDR notation',
|
||||
required: true
|
||||
}],
|
||||
|
||||
execute: (gameEvent) => {
|
||||
const input = String(gameEvent.Data).trim();
|
||||
const input = String(gameEvent.data).trim();
|
||||
|
||||
if (!validCIDR(input)) {
|
||||
gameEvent.Origin.Tell('Invalid CIDR input');
|
||||
gameEvent.origin.tell('Invalid CIDR input');
|
||||
return;
|
||||
}
|
||||
|
||||
subnetList.push(input);
|
||||
_configHandler.SetValue('SubnetBanList', subnetList);
|
||||
plugin.config.setValue('SubnetBanList', subnetList);
|
||||
|
||||
gameEvent.Origin.Tell(`Added ${input} to subnet banlist`);
|
||||
gameEvent.origin.tell(`Added ${input} to subnet banlist`);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'unbansubnet',
|
||||
description: 'unbans an IPv4 subnet',
|
||||
@ -39,115 +56,39 @@ const commands = [{
|
||||
required: true
|
||||
}],
|
||||
execute: (gameEvent) => {
|
||||
const input = String(gameEvent.Data).trim();
|
||||
const input = String(gameEvent.data).trim();
|
||||
|
||||
if (!validCIDR(input)) {
|
||||
gameEvent.Origin.Tell('Invalid CIDR input');
|
||||
gameEvent.origin.tell('Invalid CIDR input');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!subnetList.includes(input)) {
|
||||
gameEvent.Origin.Tell('Subnet is not banned');
|
||||
gameEvent.origin.tell('Subnet is not banned');
|
||||
return;
|
||||
}
|
||||
|
||||
subnetList = subnetList.filter(item => item !== input);
|
||||
_configHandler.SetValue('SubnetBanList', subnetList);
|
||||
plugin.config.setValue('SubnetBanList', subnetList);
|
||||
|
||||
gameEvent.Origin.Tell(`Removed ${input} from subnet banlist`);
|
||||
gameEvent.origin.tell(`Removed ${input} from subnet banlist`);
|
||||
}
|
||||
}];
|
||||
|
||||
convertIPtoLong = ip => {
|
||||
let components = String(ip).match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
||||
if (components) {
|
||||
let ipLong = 0;
|
||||
let power = 1;
|
||||
for (let i = 4; i >= 1; i -= 1) {
|
||||
ipLong += power * parseInt(components[i]);
|
||||
power *= 256;
|
||||
}
|
||||
return ipLong;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
],
|
||||
|
||||
isInSubnet = (ip, subnet) => {
|
||||
const mask = subnet.match(/^(.*?)\/(\d{1,2})$/);
|
||||
|
||||
if (!mask) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const baseIP = convertIPtoLong(mask[1]);
|
||||
const longIP = convertIPtoLong(ip);
|
||||
|
||||
if (mask && baseIP >= 0) {
|
||||
const freedom = Math.pow(2, 32 - parseInt(mask[2]));
|
||||
return (longIP > baseIP) && (longIP < baseIP + freedom - 1);
|
||||
} else return false;
|
||||
};
|
||||
|
||||
isSubnetBanned = (ip, list) => {
|
||||
const matchingSubnets = list.filter(subnet => isInSubnet(ip, subnet));
|
||||
return matchingSubnets.length !== 0;
|
||||
}
|
||||
|
||||
const plugin = {
|
||||
author: 'RaidMax',
|
||||
version: 1.1,
|
||||
name: 'Subnet Banlist Plugin',
|
||||
manager: null,
|
||||
logger: null,
|
||||
banMessage: '',
|
||||
|
||||
onEventAsync: (gameEvent, server) => {
|
||||
if (gameEvent.TypeName === 'Join') {
|
||||
if (!isSubnetBanned(gameEvent.Origin.IPAddressString, subnetList, this.logger)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.WriteInfo(`Kicking ${gameEvent.Origin} because they are subnet banned.`);
|
||||
gameEvent.Origin.Kick(this.banMessage, _IW4MAdminClient);
|
||||
}
|
||||
},
|
||||
onLoadAsync: manager => {
|
||||
this.manager = manager;
|
||||
this.logger = manager.GetLogger(0);
|
||||
this.configHandler = _configHandler;
|
||||
subnetList = [];
|
||||
this.interactionRegistration = _serviceResolver.ResolveService('IInteractionRegistration');
|
||||
|
||||
const list = this.configHandler.GetValue('SubnetBanList');
|
||||
if (list !== undefined) {
|
||||
list.forEach(element => {
|
||||
const ban = String(element);
|
||||
subnetList.push(ban)
|
||||
});
|
||||
this.logger.WriteInfo(`Loaded ${list.length} banned subnets`);
|
||||
} else {
|
||||
this.configHandler.SetValue('SubnetBanList', []);
|
||||
}
|
||||
|
||||
this.banMessage = this.configHandler.GetValue('BanMessage');
|
||||
|
||||
if (this.banMessage === undefined) {
|
||||
this.banMessage = 'You are not allowed to join this server.';
|
||||
this.configHandler.SetValue('BanMessage', this.banMessage);
|
||||
}
|
||||
|
||||
this.interactionRegistration.RegisterScriptInteraction(subnetBanlistKey, plugin.name, (targetId, game, token) => {
|
||||
interactions: [{
|
||||
name: subnetBanlistKey,
|
||||
action: function (_, __, ___) {
|
||||
const helpers = importNamespace('SharedLibraryCore.Helpers');
|
||||
const interactionData = new helpers.InteractionData();
|
||||
|
||||
interactionData.Name = 'Subnet Banlist'; // navigation link name
|
||||
interactionData.Description = `List of banned subnets (${subnetList.length} Total)`; // alt and title
|
||||
interactionData.DisplayMeta = 'oi-circle-x'; // nav icon
|
||||
interactionData.InteractionId = subnetBanlistKey;
|
||||
interactionData.MinimumPermission = 3; // moderator
|
||||
interactionData.InteractionType = 2; // 1 is RawContent for apis etc..., 2 is
|
||||
interactionData.Source = plugin.name;
|
||||
interactionData.name = 'Subnet Banlist'; // navigation link name
|
||||
interactionData.description = `List of banned subnets (${subnetList.length} Total)`; // alt and title
|
||||
interactionData.displayMeta = 'oi-circle-x'; // nav icon
|
||||
interactionData.interactionId = subnetBanlistKey;
|
||||
interactionData.minimumPermission = 3;
|
||||
interactionData.interactionType = 2;
|
||||
interactionData.source = plugin.name;
|
||||
|
||||
interactionData.ScriptAction = (sourceId, targetId, game, meta, token) => {
|
||||
let table = '<table class="table bg-dark-dm bg-light-lm">';
|
||||
@ -160,7 +101,7 @@ const plugin = {
|
||||
};
|
||||
|
||||
subnetList.forEach(subnet => {
|
||||
unbanSubnetInteraction.Data += ' ' + subnet
|
||||
unbanSubnetInteraction.Data += ' ' + subnet;
|
||||
table += `<tr>
|
||||
<td>
|
||||
<p>${subnet}</p>
|
||||
@ -180,16 +121,84 @@ const plugin = {
|
||||
table += '</table>';
|
||||
|
||||
return table;
|
||||
}
|
||||
};
|
||||
|
||||
return interactionData;
|
||||
}
|
||||
}],
|
||||
|
||||
onLoad: function (serviceResolver, config) {
|
||||
this.serviceResolver = serviceResolver;
|
||||
this.config = config;
|
||||
this.logger = serviceResolver.resolveService('ILogger', ['ScriptPluginV2']);
|
||||
subnetList = [];
|
||||
|
||||
const list = this.config.getValue('SubnetBanList');
|
||||
if (list !== undefined) {
|
||||
list.forEach(element => {
|
||||
const ban = String(element);
|
||||
subnetList.push(ban);
|
||||
});
|
||||
this.logger.logInformation('Loaded {Count} banned subnets', list.length);
|
||||
} else {
|
||||
this.config.setValue('SubnetBanList', []);
|
||||
}
|
||||
|
||||
this.banMessage = this.config.getValue('BanMessage');
|
||||
|
||||
if (this.banMessage === undefined) {
|
||||
this.banMessage = 'You are not allowed to join this server.';
|
||||
this.config.setValue('BanMessage', this.banMessage);
|
||||
}
|
||||
|
||||
const interactionRegistration = serviceResolver.resolveService('IInteractionRegistration');
|
||||
interactionRegistration.unregisterInteraction(subnetBanlistKey);
|
||||
|
||||
this.logger.logInformation('Subnet Ban loaded');
|
||||
},
|
||||
|
||||
onUnloadAsync: () => {
|
||||
this.interactionRegistration.UnregisterInteraction(subnetBanlistKey);
|
||||
},
|
||||
onClientAuthorized: (clientEvent) => {
|
||||
if (!isSubnetBanned(clientEvent.client.ipAddressString, subnetList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
onTickAsync: server => {
|
||||
this.logger.logInformation(`Kicking {Client} because they are subnet banned.`, clientEvent.client);
|
||||
clientEvent.client.kick(this.banMessage, clientEvent.client.currentServer.asConsoleClient());
|
||||
}
|
||||
};
|
||||
|
||||
const convertIPtoLong = ip => {
|
||||
let components = String(ip).match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
||||
if (components) {
|
||||
let ipLong = 0;
|
||||
let power = 1;
|
||||
for (let i = 4; i >= 1; i -= 1) {
|
||||
ipLong += power * parseInt(components[i]);
|
||||
power *= 256;
|
||||
}
|
||||
return ipLong;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
const isInSubnet = (ip, subnet) => {
|
||||
const mask = subnet.match(/^(.*?)\/(\d{1,2})$/);
|
||||
|
||||
if (!mask) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const baseIP = convertIPtoLong(mask[1]);
|
||||
const longIP = convertIPtoLong(ip);
|
||||
|
||||
if (mask && baseIP >= 0) {
|
||||
const freedom = Math.pow(2, 32 - parseInt(mask[2]));
|
||||
return (longIP > baseIP) && (longIP < baseIP + freedom - 1);
|
||||
} else return false;
|
||||
};
|
||||
|
||||
const isSubnetBanned = (ip, list) => {
|
||||
const matchingSubnets = list.filter(subnet => isInSubnet(ip, subnet));
|
||||
return matchingSubnets.length !== 0;
|
||||
};
|
||||
|
@ -2,22 +2,23 @@ let vpnExceptionIds = [];
|
||||
const vpnAllowListKey = 'Webfront::Nav::Admin::VPNAllowList';
|
||||
const vpnWhitelistKey = 'Webfront::Profile::VPNWhitelist';
|
||||
|
||||
const init = (registerNotify, serviceResolver, config) => {
|
||||
registerNotify('IManagementEventSubscriptions.ClientStateAuthorized', (authorizedEvent, _) => plugin.onClientAuthorized(authorizedEvent));
|
||||
const init = (registerNotify, serviceResolver, config, pluginHelper) => {
|
||||
registerNotify('IManagementEventSubscriptions.ClientStateAuthorized', (authorizedEvent, token) => plugin.onClientAuthorized(authorizedEvent, token));
|
||||
|
||||
plugin.onLoad(serviceResolver, config);
|
||||
plugin.onLoad(serviceResolver, config, pluginHelper);
|
||||
return plugin;
|
||||
};
|
||||
|
||||
const plugin = {
|
||||
author: 'RaidMax',
|
||||
version: '1.6',
|
||||
version: '2.0',
|
||||
name: 'VPN Detection Plugin',
|
||||
manager: null,
|
||||
config: null,
|
||||
logger: null,
|
||||
serviceResolver: null,
|
||||
translations: null,
|
||||
pluginHelper: null,
|
||||
|
||||
commands: [{
|
||||
name: 'whitelistvpn',
|
||||
@ -58,7 +59,7 @@ const plugin = {
|
||||
interactions: [{
|
||||
// registers the profile action
|
||||
name: vpnWhitelistKey,
|
||||
action: function(targetId, game, token) {
|
||||
action: function (targetId, game, token) {
|
||||
const helpers = importNamespace('SharedLibraryCore.Helpers');
|
||||
const interactionData = new helpers.InteractionData();
|
||||
|
||||
@ -91,7 +92,7 @@ const plugin = {
|
||||
},
|
||||
{
|
||||
name: vpnAllowListKey,
|
||||
action: function(targetId, game, token) {
|
||||
action: function (targetId, game, token) {
|
||||
const helpers = importNamespace('SharedLibraryCore.Helpers');
|
||||
const interactionData = new helpers.InteractionData();
|
||||
|
||||
@ -146,13 +147,17 @@ const plugin = {
|
||||
}
|
||||
],
|
||||
|
||||
onClientAuthorized: function(authorizeEvent) {
|
||||
this.checkForVpn(authorizeEvent.client);
|
||||
onClientAuthorized: async function (authorizeEvent, token) {
|
||||
if (authorizeEvent.client.isBot) {
|
||||
return;
|
||||
}
|
||||
await this.checkForVpn(authorizeEvent.client, token);
|
||||
},
|
||||
|
||||
onLoad: function(serviceResolver, config) {
|
||||
onLoad: function (serviceResolver, config, pluginHelper) {
|
||||
this.serviceResolver = serviceResolver;
|
||||
this.config = config;
|
||||
this.pluginHelper = pluginHelper;
|
||||
this.manager = this.serviceResolver.resolveService('IManager');
|
||||
this.logger = this.serviceResolver.resolveService('ILogger', ['ScriptPluginV2']);
|
||||
this.translations = this.serviceResolver.resolveService('ITranslationLookup');
|
||||
@ -166,10 +171,10 @@ const plugin = {
|
||||
this.interactionRegistration.unregisterInteraction(vpnAllowListKey);
|
||||
},
|
||||
|
||||
checkForVpn: function(origin) {
|
||||
checkForVpn: async function (origin, token) {
|
||||
let exempt = false;
|
||||
// prevent players that are exempt from being kicked
|
||||
vpnExceptionIds.forEach(function(id) {
|
||||
vpnExceptionIds.forEach(function (id) {
|
||||
if (parseInt(id) === parseInt(origin.clientId)) {
|
||||
exempt = true;
|
||||
return false;
|
||||
@ -181,25 +186,40 @@ const plugin = {
|
||||
return;
|
||||
}
|
||||
|
||||
let usingVPN = false;
|
||||
|
||||
try {
|
||||
const cl = new System.Net.Http.HttpClient();
|
||||
const re = cl.getAsync(`https://api.xdefcon.com/proxy/check/?ip=${origin.IPAddressString}`).result;
|
||||
const userAgent = `IW4MAdmin-${this.manager.getApplicationSettings().configuration().id}`;
|
||||
cl.defaultRequestHeaders.add('User-Agent', userAgent);
|
||||
const co = re.content;
|
||||
const parsedJSON = JSON.parse(co.readAsStringAsync().result);
|
||||
co.dispose();
|
||||
re.dispose();
|
||||
cl.dispose();
|
||||
usingVPN = parsedJSON.success && parsedJSON.proxy;
|
||||
} catch (ex) {
|
||||
this.logger.logWarning('There was a problem checking client IP for VPN {message}', ex.message);
|
||||
if (origin.IPAddressString === null) {
|
||||
this.logger.logDebug('{Client} does not have an IP Address yet, so we are no checking their VPN status', origin);
|
||||
}
|
||||
|
||||
const userAgent = `IW4MAdmin-${this.manager.getApplicationSettings().configuration().id}`;
|
||||
const headers = {
|
||||
'User-Agent': userAgent
|
||||
};
|
||||
|
||||
try {
|
||||
this.pluginHelper.getUrl(`https://api.xdefcon.com/proxy/check/?ip=${origin.IPAddressString}`, headers,
|
||||
(response) => this.onVpnResponse(response, origin));
|
||||
|
||||
} catch (ex) {
|
||||
this.logger.logWarning('There was a problem checking client IP ({IP}) for VPN - {message}',
|
||||
origin.IPAddressString, ex.message);
|
||||
}
|
||||
},
|
||||
|
||||
onVpnResponse: function (response, origin) {
|
||||
let parsedJSON = null;
|
||||
|
||||
try {
|
||||
parsedJSON = JSON.parse(response);
|
||||
} catch {
|
||||
this.logger.logWarning('There was a problem checking client IP ({IP}) for VPN - {message}',
|
||||
origin.IPAddressString, response);
|
||||
return;
|
||||
}
|
||||
|
||||
const usingVPN = parsedJSON.success && parsedJSON.proxy;
|
||||
|
||||
if (usingVPN) {
|
||||
this.logger.logInformation('{origin} is using a VPN ({ip})', origin.toString(), origin.ipAddressString);
|
||||
this.logger.logInformation('{origin} is using a VPN ({ip})', origin.toString(), origin.IPAddressString);
|
||||
const contactUrl = this.manager.getApplicationSettings().configuration().contactUri;
|
||||
let additionalInfo = '';
|
||||
if (contactUrl) {
|
||||
@ -207,11 +227,11 @@ const plugin = {
|
||||
}
|
||||
origin.kick(this.translations['SERVER_KICK_VPNS_NOTALLOWED'] + ' ' + additionalInfo, origin.currentServer.asConsoleClient());
|
||||
} else {
|
||||
this.logger.logDebug('{client} is not using a VPN', origin);
|
||||
this.logger.logDebug('{Client} is not using a VPN', origin);
|
||||
}
|
||||
},
|
||||
|
||||
getClientsData: function(clientIds) {
|
||||
getClientsData: function (clientIds) {
|
||||
const contextFactory = this.serviceResolver.resolveService('IDatabaseContextFactory');
|
||||
const context = contextFactory.createContext(false);
|
||||
const clientSet = context.clients;
|
||||
|
@ -18,6 +18,6 @@ namespace SharedLibraryCore.Interfaces
|
||||
/// discovers the script plugins
|
||||
/// </summary>
|
||||
/// <returns>initialized script plugin collection</returns>
|
||||
IEnumerable<IPlugin> DiscoverScriptPlugins();
|
||||
IEnumerable<(Type, string)> DiscoverScriptPlugins();
|
||||
}
|
||||
}
|
||||
|
11
SharedLibraryCore/Interfaces/IPluginV2.cs
Normal file
11
SharedLibraryCore/Interfaces/IPluginV2.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace SharedLibraryCore.Interfaces;
|
||||
|
||||
|
||||
public interface IPluginV2 : IModularAssembly
|
||||
{
|
||||
static void RegisterDependencies(IServiceCollection serviceProvider)
|
||||
{
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Data.Models;
|
||||
using SharedLibraryCore.Commands;
|
||||
|
||||
namespace SharedLibraryCore.Interfaces
|
||||
{
|
||||
@ -18,11 +20,11 @@ namespace SharedLibraryCore.Interfaces
|
||||
/// <param name="permission">minimum required permission</param>
|
||||
/// <param name="isTargetRequired">target required or not</param>
|
||||
/// <param name="args">command arguments (name, is required)</param>
|
||||
/// <param name="executeAction">action to peform when commmand is executed</param>
|
||||
/// <param name="executeAction">action to perform when command is executed</param>
|
||||
/// <param name="supportedGames"></param>
|
||||
/// <returns></returns>
|
||||
IManagerCommand CreateScriptCommand(string name, string alias, string description, string permission,
|
||||
bool isTargetRequired, IEnumerable<(string, bool)> args, Func<GameEvent, Task> executeAction,
|
||||
Server.Game[] supportedGames);
|
||||
bool isTargetRequired, IEnumerable<CommandArgument> args, Func<GameEvent, Task> executeAction,
|
||||
IEnumerable<Reference.Game> supportedGames);
|
||||
}
|
||||
}
|
||||
|
8
SharedLibraryCore/Interfaces/IScriptPluginFactory.cs
Normal file
8
SharedLibraryCore/Interfaces/IScriptPluginFactory.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using System;
|
||||
|
||||
namespace SharedLibraryCore.Interfaces;
|
||||
|
||||
public interface IScriptPluginFactory
|
||||
{
|
||||
object CreateScriptPlugin(Type type, string fileName);
|
||||
}
|
@ -27,11 +27,18 @@ public class InteractionController : BaseController
|
||||
}
|
||||
|
||||
ViewBag.Title = interactionData.Description;
|
||||
var meta = HttpContext.Request.Query.ToDictionary(key => key.Key, value => value.Value.ToString());
|
||||
var result = await _interactionRegistration.ProcessInteraction(interactionName, Client.ClientId, meta: meta, token: token);
|
||||
|
||||
var result = await _interactionRegistration.ProcessInteraction(interactionName, Client.ClientId, token: token);
|
||||
if (interactionData.InteractionType == InteractionType.TemplateContent)
|
||||
{
|
||||
return View("Render", result ?? "");
|
||||
}
|
||||
|
||||
return interactionData.InteractionType == InteractionType.TemplateContent
|
||||
? View("Render", result ?? "")
|
||||
: Ok(result);
|
||||
return new ContentResult
|
||||
{
|
||||
Content = result,
|
||||
ContentType = interactionData.DisplayMeta ?? "text/html"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user