From d3ac9d53a4d9d07039dd5e0e9e2701d08243484d Mon Sep 17 00:00:00 2001 From: RaidMax Date: Sat, 15 Apr 2023 14:27:51 -0500 Subject: [PATCH] add configuration update callback for script plugins & update plugins to utilize --- Application/IO/BaseConfigurationHandlerV2.cs | 3 ++ .../ScriptPluginConfigurationWrapper.cs | 41 ++++++++++++++++- Application/Plugin/Script/ScriptPluginV2.cs | 9 ++++ Plugins/ScriptPlugins/ActionOnReport.js | 46 ++++++++++++------- Plugins/ScriptPlugins/BanBroadcasting.js | 12 +++-- Plugins/ScriptPlugins/VPNDetection.js | 40 +++++++++++----- .../Interfaces/IConfigurationHandlerV2.cs | 4 +- 7 files changed, 120 insertions(+), 35 deletions(-) diff --git a/Application/IO/BaseConfigurationHandlerV2.cs b/Application/IO/BaseConfigurationHandlerV2.cs index 1287f13b..bd33ea7d 100644 --- a/Application/IO/BaseConfigurationHandlerV2.cs +++ b/Application/IO/BaseConfigurationHandlerV2.cs @@ -118,6 +118,8 @@ public class BaseConfigurationHandlerV2 : IConfigurationHand } } + public event Action Updated; + private async Task InternalSet(TConfigurationType configuration, bool awaitSemaphore) { try @@ -163,6 +165,7 @@ public class BaseConfigurationHandlerV2 : IConfigurationHand else { CopyUpdatedProperties(readConfiguration); + Updated?.Invoke(readConfiguration); } } catch (Exception ex) diff --git a/Application/Plugin/Script/ScriptPluginConfigurationWrapper.cs b/Application/Plugin/Script/ScriptPluginConfigurationWrapper.cs index 1d0114bf..cd1261f5 100644 --- a/Application/Plugin/Script/ScriptPluginConfigurationWrapper.cs +++ b/Application/Plugin/Script/ScriptPluginConfigurationWrapper.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Text.Json; @@ -6,15 +7,20 @@ using System.Threading.Tasks; using IW4MAdmin.Application.Configuration; using Jint; using Jint.Native; +using Jint.Native.Json; using SharedLibraryCore.Interfaces; namespace IW4MAdmin.Application.Plugin.Script; public class ScriptPluginConfigurationWrapper { + public event Action ConfigurationUpdated; + private readonly ScriptPluginConfiguration _config; private readonly IConfigurationHandlerV2 _configHandler; private readonly Engine _scriptEngine; + private readonly JsonParser _engineParser; + private readonly List<(string, Delegate)> _updateCallbackActions = new(); private string _pluginName; public ScriptPluginConfigurationWrapper(string pluginName, Engine scriptEngine, IConfigurationHandlerV2 configHandler) @@ -22,9 +28,16 @@ public class ScriptPluginConfigurationWrapper _pluginName = pluginName; _scriptEngine = scriptEngine; _configHandler = configHandler; + _configHandler.Updated += OnConfigurationUpdated; _config = configHandler.Get("ScriptPluginSettings", new ScriptPluginConfiguration()).GetAwaiter().GetResult(); + _engineParser = new JsonParser(_scriptEngine); } + ~ScriptPluginConfigurationWrapper() + { + _configHandler.Updated -= OnConfigurationUpdated; + } + public void SetName(string name) { _pluginName = name; @@ -63,8 +76,10 @@ public class ScriptPluginConfigurationWrapper await _configHandler.Set(_config); } + + public JsValue GetValue(string key) => GetValue(key, null); - public JsValue GetValue(string key) + public JsValue GetValue(string key, Delegate updateCallback) { if (!_config.ContainsKey(_pluginName)) { @@ -83,6 +98,20 @@ public class ScriptPluginConfigurationWrapper item = jElem.Deserialize>(); } + if (updateCallback is not null) + { + _updateCallbackActions.Add((key, updateCallback)); + } + + try + { + return _engineParser.Parse(item!.ToString()!); + } + catch + { + // ignored + } + return JsValue.FromObject(_scriptEngine, item); } @@ -90,4 +119,12 @@ public class ScriptPluginConfigurationWrapper { return int.TryParse(value.ToString(CultureInfo.InvariantCulture), out var parsed) ? parsed : null; } + + private void OnConfigurationUpdated(ScriptPluginConfiguration config) + { + foreach (var callback in _updateCallbackActions) + { + ConfigurationUpdated?.Invoke(GetValue(callback.Item1), callback.Item2); + } + } } diff --git a/Application/Plugin/Script/ScriptPluginV2.cs b/Application/Plugin/Script/ScriptPluginV2.cs index bfbf7990..19b70c79 100644 --- a/Application/Plugin/Script/ScriptPluginV2.cs +++ b/Application/Plugin/Script/ScriptPluginV2.cs @@ -291,6 +291,15 @@ public class ScriptPluginV2 : IPluginV2 _scriptPluginConfigurationWrapper = new ScriptPluginConfigurationWrapper(_fileName.Split(Path.DirectorySeparatorChar).Last(), ScriptEngine, _configHandler); + + _scriptPluginConfigurationWrapper.ConfigurationUpdated += (configValue, callbackAction) => + { + WrapJavaScriptErrorHandling(() => + { + callbackAction.DynamicInvoke(JsValue.Undefined, new[] { configValue }); + return Task.CompletedTask; + }, _logger, _fileName, _onProcessingScript); + }; } private void UnregisterScriptEntities(IManager manager) diff --git a/Plugins/ScriptPlugins/ActionOnReport.js b/Plugins/ScriptPlugins/ActionOnReport.js index 24ca455a..b9cd26e5 100644 --- a/Plugins/ScriptPlugins/ActionOnReport.js +++ b/Plugins/ScriptPlugins/ActionOnReport.js @@ -1,5 +1,5 @@ -const init = (registerEventCallback, serviceResolver, _) => { - plugin.onLoad(serviceResolver); +const init = (registerEventCallback, serviceResolver, configWrapper) => { + plugin.onLoad(serviceResolver, configWrapper); registerEventCallback('IManagementEventSubscriptions.ClientPenaltyAdministered', (penaltyEvent, _) => { plugin.onPenalty(penaltyEvent); @@ -10,21 +10,20 @@ const init = (registerEventCallback, serviceResolver, _) => { const plugin = { author: 'RaidMax', - version: '2.0', + version: '2.1', name: 'Action on Report', - enabled: false, // indicates if the plugin is enabled - reportAction: 'TempBan', // can be TempBan or Ban - maxReportCount: 5, // how many reports before action is taken - tempBanDurationMinutes: 60, // how long to temporarily ban the player - penaltyType: { - 'report': 0 + config: { + enabled: false, // indicates if the plugin is enabled + reportAction: 'TempBan', // can be TempBan or Ban + maxReportCount: 5, // how many reports before action is taken + tempBanDurationMinutes: 60 // how long to temporarily ban the player }, onPenalty: function (penaltyEvent) { - if (!this.enabled || penaltyEvent.penalty.type !== this.penaltyType['report']) { + if (!this.config.enabled || penaltyEvent.penalty.type !== 'Report') { return; } - + if (!penaltyEvent.client.isIngame || (penaltyEvent.client.level !== 'User' && penaltyEvent.client.level !== 'Flagged')) { this.logger.logInformation(`Ignoring report for client (id) ${penaltyEvent.client.clientId} because they are privileged or not in-game`); return; @@ -34,11 +33,11 @@ const plugin = { reportCount++; this.reportCounts[penaltyEvent.client.networkId] = reportCount; - if (reportCount >= this.maxReportCount) { - switch (this.reportAction) { + if (reportCount >= this.config.maxReportCount) { + switch (this.config.reportAction) { case 'TempBan': this.logger.logInformation(`TempBanning client (id) ${penaltyEvent.client.clientId} because they received ${reportCount} reports`); - penaltyEvent.client.tempBan(this.translations['PLUGINS_REPORT_ACTION'], System.TimeSpan.FromMinutes(this.tempBanDurationMinutes), penaltyEvent.Client.CurrentServer.asConsoleClient()); + penaltyEvent.client.tempBan(this.translations['PLUGINS_REPORT_ACTION'], System.TimeSpan.FromMinutes(this.config.tempBanDurationMinutes), penaltyEvent.Client.CurrentServer.asConsoleClient()); break; case 'Ban': this.logger.logInformation(`Banning client (id) ${penaltyEvent.client.clientId} because they received ${reportCount} reports`); @@ -48,10 +47,25 @@ const plugin = { } }, - onLoad: function (serviceResolver) { + onLoad: function (serviceResolver, configWrapper) { 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); + this.configWrapper = configWrapper; + + const storedConfig = this.configWrapper.getValue('config', newConfig => { + if (newConfig) { + plugin.logger.logInformation('ActionOnReport config reloaded. Enabled={Enabled}', newConfig.enabled); + plugin.config = newConfig; + } + }); + + if (storedConfig != null) { + this.config = storedConfig + } else { + this.configWrapper.setValue('config', this.config); + } + + this.logger.logInformation('ActionOnReport {version} by {author} loaded. Enabled={Enabled}', this.version, this.author, this.config.enabled); this.reportCounts = {}; } }; diff --git a/Plugins/ScriptPlugins/BanBroadcasting.js b/Plugins/ScriptPlugins/BanBroadcasting.js index 24f5b285..3fc55496 100644 --- a/Plugins/ScriptPlugins/BanBroadcasting.js +++ b/Plugins/ScriptPlugins/BanBroadcasting.js @@ -7,15 +7,16 @@ const init = (registerNotify, serviceResolver, config) => { const plugin = { author: 'Amos, RaidMax', - version: '2.0', + version: '2.1', name: 'Broadcast Bans', config: null, logger: null, translations: null, manager: null, + enableBroadcastBans: false, onClientPenalty: function (penaltyEvent) { - if (!this.enableBroadcastBans || penaltyEvent.penalty.type !== 5) { + if (!this.enableBroadcastBans || penaltyEvent.penalty.type !== 'Ban') { return; } @@ -43,7 +44,10 @@ const plugin = { onLoad: function (serviceResolver, config) { this.config = config; this.config.setName(this.name); - this.enableBroadcastBans = this.config.getValue('EnableBroadcastBans'); + this.enableBroadcastBans = this.config.getValue('EnableBroadcastBans', newConfig => { + plugin.logger.logInformation('{Name} config reloaded. Enabled={Enabled}', plugin.name, newConfig); + plugin.enableBroadcastBans = newConfig; + }); this.manager = serviceResolver.resolveService('IManager'); this.logger = serviceResolver.resolveService('ILogger', ['ScriptPluginV2']); @@ -54,7 +58,7 @@ const plugin = { this.config.setValue('EnableBroadcastBans', this.enableBroadcastBans); } - this.logger.logInformation('{Name} {Version} by {Author} loaded. Enabled={enabled}', this.name, this.version, + this.logger.logInformation('{Name} {Version} by {Author} loaded. Enabled={Enabled}', this.name, this.version, this.author, this.enableBroadcastBans); } }; diff --git a/Plugins/ScriptPlugins/VPNDetection.js b/Plugins/ScriptPlugins/VPNDetection.js index 69b8b3d3..7494b8b4 100644 --- a/Plugins/ScriptPlugins/VPNDetection.js +++ b/Plugins/ScriptPlugins/VPNDetection.js @@ -2,23 +2,24 @@ let vpnExceptionIds = []; const vpnAllowListKey = 'Webfront::Nav::Admin::VPNAllowList'; const vpnWhitelistKey = 'Webfront::Profile::VPNWhitelist'; -const init = (registerNotify, serviceResolver, config, pluginHelper) => { +const init = (registerNotify, serviceResolver, configWrapper, pluginHelper) => { registerNotify('IManagementEventSubscriptions.ClientStateAuthorized', (authorizedEvent, token) => plugin.onClientAuthorized(authorizedEvent, token)); - plugin.onLoad(serviceResolver, config, pluginHelper); + plugin.onLoad(serviceResolver, configWrapper, pluginHelper); return plugin; }; const plugin = { author: 'RaidMax', - version: '2.0', + version: '2.1', name: 'VPN Detection Plugin', manager: null, - config: null, + configWrapper: null, logger: null, serviceResolver: null, translations: null, pluginHelper: null, + enabled: true, commands: [{ name: 'whitelistvpn', @@ -32,7 +33,7 @@ const plugin = { }], execute: (gameEvent) => { vpnExceptionIds.push(gameEvent.Target.ClientId); - plugin.config.setValue('vpnExceptionIds', vpnExceptionIds); + plugin.configWrapper.setValue('vpnExceptionIds', vpnExceptionIds); gameEvent.origin.tell(`Successfully whitelisted ${gameEvent.target.name}`); } @@ -49,7 +50,7 @@ const plugin = { }], execute: (gameEvent) => { vpnExceptionIds = vpnExceptionIds.filter(exception => parseInt(exception) !== parseInt(gameEvent.Target.ClientId)); - plugin.config.setValue('vpnExceptionIds', vpnExceptionIds); + plugin.configWrapper.setValue('vpnExceptionIds', vpnExceptionIds); gameEvent.origin.tell(`Successfully disallowed ${gameEvent.target.name} from connecting with VPN`); } @@ -148,30 +149,45 @@ const plugin = { ], onClientAuthorized: async function (authorizeEvent, token) { - if (authorizeEvent.client.isBot) { + if (authorizeEvent.client.isBot || !this.enabled) { return; } await this.checkForVpn(authorizeEvent.client, token); }, - onLoad: function (serviceResolver, config, pluginHelper) { + onLoad: function (serviceResolver, configWrapper, pluginHelper) { this.serviceResolver = serviceResolver; - this.config = config; + this.configWrapper = configWrapper; this.pluginHelper = pluginHelper; this.manager = this.serviceResolver.resolveService('IManager'); this.logger = this.serviceResolver.resolveService('ILogger', ['ScriptPluginV2']); this.translations = this.serviceResolver.resolveService('ITranslationLookup'); - this.config.setName(this.name); // use legacy key - this.config.getValue('vpnExceptionIds').forEach(element => vpnExceptionIds.push(parseInt(element))); + this.configWrapper.setName(this.name); // use legacy key + this.configWrapper.getValue('vpnExceptionIds').forEach(element => vpnExceptionIds.push(parseInt(element))); this.logger.logInformation(`Loaded ${vpnExceptionIds.length} ids into whitelist`); + + this.enabled = this.configWrapper.getValue('enabled', newValue => { + if (newValue) { + plugin.logger.logInformation('{Name} configuration updated. Enabled={Enabled}', newValue); + plugin.enabled = newValue; + } + }); + + if (this.enabled === undefined) { + this.configWrapper.setValue('enabled', true); + this.enabled = true; + } this.interactionRegistration = this.serviceResolver.resolveService('IInteractionRegistration'); this.interactionRegistration.unregisterInteraction(vpnWhitelistKey); this.interactionRegistration.unregisterInteraction(vpnAllowListKey); + + this.logger.logInformation('{Name} {Version} by {Author} loaded. Enabled={Enabled}', this.name, this.version, + this.author, this.enabled); }, - checkForVpn: async function (origin, token) { + checkForVpn: async function (origin, _) { let exempt = false; // prevent players that are exempt from being kicked vpnExceptionIds.forEach(function (id) { diff --git a/SharedLibraryCore/Interfaces/IConfigurationHandlerV2.cs b/SharedLibraryCore/Interfaces/IConfigurationHandlerV2.cs index 521d38b6..d6894b62 100644 --- a/SharedLibraryCore/Interfaces/IConfigurationHandlerV2.cs +++ b/SharedLibraryCore/Interfaces/IConfigurationHandlerV2.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; namespace SharedLibraryCore.Interfaces; @@ -7,4 +8,5 @@ public interface IConfigurationHandlerV2 where TConfiguratio Task Get(string configurationName, TConfigurationType defaultConfiguration = null); Task Set(TConfigurationType configuration); Task Set(); + event Action Updated; }