Skip to content

Commit

Permalink
implement profile interaction registration through plugins (mute and …
Browse files Browse the repository at this point in the history
…vpn detection implementation)
  • Loading branch information
RaidMax committed Sep 8, 2022
1 parent 7ad4615 commit 8d8a8d8
Show file tree
Hide file tree
Showing 26 changed files with 452 additions and 26 deletions.
1 change: 1 addition & 0 deletions Application/Main.cs
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,7 @@ private static async Task<IServiceCollection> ConfigureServices(string[] args)
.AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb")))
.AddSingleton<IAlertManager, AlertManager>()
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
.AddSingleton<IInteractionRegistration, InteractionRegistration>()
.AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig);

Expand Down
132 changes: 132 additions & 0 deletions Application/Misc/InteractionRegistration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SharedLibraryCore.Interfaces;
using InteractionRegistrationCallback =
System.Func<int?, Data.Models.Reference.Game?, System.Threading.CancellationToken,
System.Threading.Tasks.Task<SharedLibraryCore.Interfaces.IInteractionData>>;

namespace IW4MAdmin.Application.Misc;

public class InteractionRegistration : IInteractionRegistration
{
private readonly ILogger<InteractionRegistration> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly ConcurrentDictionary<string, InteractionRegistrationCallback> _interactions = new();

public InteractionRegistration(ILogger<InteractionRegistration> logger, IServiceProvider serviceProvider)
{
_logger = logger;
_serviceProvider = serviceProvider;
}

public void RegisterScriptInteraction(string interactionName, string source, Delegate interactionRegistration)
{
var plugin = _serviceProvider.GetRequiredService<IEnumerable<IPlugin>>()
.FirstOrDefault(plugin => plugin.Name == source);

if (plugin is not ScriptPlugin scriptPlugin)
{
return;
}

var wrappedDelegate = (int? clientId, Reference.Game? game, CancellationToken token) =>
Task.FromResult(
scriptPlugin.WrapDelegate<IInteractionData>(interactionRegistration, clientId, game, token));

if (!_interactions.ContainsKey(interactionName))
{
_interactions.TryAdd(interactionName, wrappedDelegate);
}
else
{
_interactions[interactionName] = wrappedDelegate;
}
}

public void RegisterInteraction(string interactionName, InteractionRegistrationCallback interactionRegistration)
{
if (!_interactions.ContainsKey(interactionName))
{
_interactions.TryAdd(interactionName, interactionRegistration);
}
else
{
_interactions[interactionName] = interactionRegistration;
}
}

public void UnregisterInteraction(string interactionName)
{
if (_interactions.ContainsKey(interactionName))
{
_interactions.TryRemove(interactionName, out _);
}
}

public async Task<IEnumerable<IInteractionData>> GetInteractions(int? clientId = null,
Reference.Game? game = null, CancellationToken token = default)
{
return (await Task.WhenAll(_interactions.Select(async kvp =>
{
try
{
return await kvp.Value(clientId, game, token);
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"Could not get interaction for interaction {InteractionName} and ClientId {ClientId}", kvp.Key,
clientId);
return null;
}
}))).Where(interaction => interaction is not null);
}

public async Task<string> ProcessInteraction(string interactionId, int? clientId = null,
Reference.Game? game = null, CancellationToken token = default)
{
if (!_interactions.ContainsKey(interactionId))
{
throw new ArgumentException($"Interaction with ID {interactionId} has not been registered");
}

try
{
var interaction = await _interactions[interactionId](clientId, game, token);

if (interaction.Action is not null)
{
return await interaction.Action(clientId, game, token);
}

if (interaction.ScriptAction is not null)
{
foreach (var plugin in _serviceProvider.GetRequiredService<IEnumerable<IPlugin>>())
{
if (plugin is not ScriptPlugin scriptPlugin || scriptPlugin.Name != interaction.Source)
{
continue;
}

return scriptPlugin.ExecuteAction<string>(interaction.ScriptAction, clientId, game, token);
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex,
"Could not process interaction for interaction {InteractionName} and ClientId {ClientId}",
interactionId,
clientId);
}

return null;
}
}
35 changes: 35 additions & 0 deletions Application/Misc/ScriptPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,41 @@ public Task OnUnloadAsync()
return Task.CompletedTask;
}

public T ExecuteAction<T>(Delegate action, params object[] param)
{
try
{
_onProcessing.Wait();
var args = param.Select(p => JsValue.FromObject(_scriptEngine, p)).ToArray();
var result = action.DynamicInvoke(JsValue.Undefined, args);
return (T)(result as JsValue)?.ToObject();
}
finally
{
if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release(1);
}
}
}

public T WrapDelegate<T>(Delegate act, params object[] args)
{
try
{
_onProcessing.Wait();
return (T)(act.DynamicInvoke(JsValue.Null,
args.Select(arg => JsValue.FromObject(_scriptEngine, arg)).ToArray()) as ObjectWrapper)?.ToObject();
}
finally
{
if (_onProcessing.CurrentCount == 0)
{
_onProcessing.Release(1);
}
}
}

/// <summary>
/// finds declared script commands in the script plugin
/// </summary>
Expand Down
2 changes: 1 addition & 1 deletion Plugins/AutomessageFeed/AutomessageFeed.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.9.8.1" PrivateAssets="All" />
</ItemGroup>

<Target Name="PostBuild" AfterTargets="PostBuildEvent">
Expand Down
2 changes: 1 addition & 1 deletion Plugins/LiveRadar/LiveRadar.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.9.8.1" PrivateAssets="All" />
</ItemGroup>

<Target Name="PostBuild" AfterTargets="PostBuildEvent">
Expand Down
2 changes: 1 addition & 1 deletion Plugins/Login/Login.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.9.8.1" PrivateAssets="All" />
</ItemGroup>

<Target Name="PostBuild" AfterTargets="PostBuildEvent">
Expand Down
4 changes: 2 additions & 2 deletions Plugins/Mute/Mute.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1"/>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.9.8.1" PrivateAssets="All"/>
</ItemGroup>

<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies"/>
<Exec Command="dotnet publish $(ProjectPath) -c $(ConfigurationName) -o $(ProjectDir)..\..\Build\Plugins --no-build --no-restore --no-dependencies" />
</Target>
</Project>
55 changes: 54 additions & 1 deletion Plugins/Mute/Plugin.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces;

namespace Mute;

public class Plugin : IPlugin
{
public Plugin(IMetaServiceV2 metaService)
private readonly IInteractionRegistration _interactionRegistration;
private static readonly string MuteInteraction = nameof(MuteInteraction);

public Plugin(IMetaServiceV2 metaService, IInteractionRegistration interactionRegistration)
{
_interactionRegistration = interactionRegistration;
DataManager = new DataManager(metaService);
}

Expand Down Expand Up @@ -45,11 +51,58 @@ public async Task OnEventAsync(GameEvent gameEvent, Server server)

public Task OnLoadAsync(IManager manager)
{
_interactionRegistration.RegisterInteraction(MuteInteraction, async (clientId, game, token) =>
{
if (!clientId.HasValue || game.HasValue && !SupportedGames.Contains((Server.Game)game.Value))
{
return null;
}

var muteState = await DataManager.ReadPersistentData(new EFClient { ClientId = clientId.Value });

return muteState is MuteState.Unmuted or MuteState.Unmuting
? new InteractionData
{
EntityId = clientId,
Name = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MUTE"],
DisplayMeta = "oi-volume-off",
ActionPath = "DynamicAction",
ActionMeta = new()
{
{ "InteractionId", "command" },
{ "Data", $"mute @{clientId.Value}" },
{ "ActionButtonLabel", Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MUTE"] },
{ "Name", Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_MUTE"] },
{ "ShouldRefresh", true.ToString() }
},
MinimumPermission = Data.Models.Client.EFClient.Permission.Moderator,
Source = Name
}
: new InteractionData
{
EntityId = clientId,
Name = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_UNMUTE"],
DisplayMeta = "oi-volume-high",
ActionPath = "DynamicAction",
ActionMeta = new()
{
{ "InteractionId", "command" },
{ "Data", $"mute @{clientId.Value}" },
{ "ActionButtonLabel", Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_UNMUTE"] },
{ "Name", Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_PROFILE_CONTEXT_MENU_ACTION_UNMUTE"] },
{ "ShouldRefresh", true.ToString() }
},
MinimumPermission = Data.Models.Client.EFClient.Permission.Moderator,
Source = Name
};
});

return Task.CompletedTask;
}

public Task OnUnloadAsync()
{
_interactionRegistration.UnregisterInteraction(MuteInteraction);
return Task.CompletedTask;
}

Expand Down
2 changes: 1 addition & 1 deletion Plugins/ProfanityDeterment/ProfanityDeterment.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.9.8.1" PrivateAssets="All" />
</ItemGroup>

<Target Name="PostBuild" AfterTargets="PostBuildEvent">
Expand Down
28 changes: 27 additions & 1 deletion Plugins/ScriptPlugins/VPNDetection.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const commands = [{

const plugin = {
author: 'RaidMax',
version: 1.3,
version: 1.4,
name: 'VPN Detection Plugin',
manager: null,
logger: null,
Expand Down Expand Up @@ -82,9 +82,35 @@ const plugin = {
this.configHandler = _configHandler;
this.configHandler.GetValue('vpnExceptionIds').forEach(element => vpnExceptionIds.push(element));
this.logger.WriteInfo(`Loaded ${vpnExceptionIds.length} ids into whitelist`);

this.interactionRegistration = _serviceResolver.ResolveService('IInteractionRegistration');
this.interactionRegistration.RegisterScriptInteraction('WhitelistVPN', this.name, (clientId, game, token) => {
if (vpnExceptionIds.includes(clientId)) {
return;
}

const helpers = importNamespace('SharedLibraryCore.Helpers');
const interactionData = new helpers.InteractionData();

interactionData.EntityId = clientId;
interactionData.Name = 'Whitelist VPN';
interactionData.DisplayMeta = 'oi-circle-check';

interactionData.ActionMeta.Add('InteractionId', 'command');
interactionData.ActionMeta.Add('Data', `whitelistvpn @${clientId}`);
interactionData.ActionMeta.Add('ActionButtonLabel', 'Allow');
interactionData.ActionMeta.Add('Name', 'Allow VPN Connection');
interactionData.ActionMeta.Add('ShouldRefresh', true.toString());

interactionData.ActionPath = 'DynamicAction';
interactionData.MinimumPermission = 3;
interactionData.Source = this.name;
return interactionData;
});
},

onUnloadAsync: function () {
this.interactionRegistration.UnregisterInteraction('WhitelistVPN');
},

onTickAsync: function (server) {
Expand Down
2 changes: 1 addition & 1 deletion Plugins/Stats/Stats.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.9.8.1" PrivateAssets="All" />
</ItemGroup>

<Target Name="PostBuild" AfterTargets="PostBuildEvent">
Expand Down
2 changes: 1 addition & 1 deletion Plugins/Welcome/Welcome.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
</Target>

<ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1" PrivateAssets="All" />
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.9.8.1" PrivateAssets="All" />
</ItemGroup>

</Project>
1 change: 1 addition & 0 deletions SharedLibraryCore/Dtos/PlayerInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ public class PlayerInfo
public string CurrentServerName { get; set; }
public IGeoLocationResult GeoLocationInfo { get; set; }
public ClientNoteMetaResponse NoteMeta { get; set; }
public List<IInteractionData> Interactions { get; set; }
}
}
Loading

0 comments on commit 8d8a8d8

Please sign in to comment.