implement profile interaction registration through plugins (mute and vpn detection implementation)

This commit is contained in:
RaidMax 2022-09-08 15:03:38 -05:00
parent 3cffdfdd9d
commit 2380f23dbe
26 changed files with 452 additions and 26 deletions

View File

@ -451,6 +451,7 @@ namespace IW4MAdmin.Application
.AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb"))) .AddSingleton<IGeoLocationService>(new GeoLocationService(Path.Join(".", "Resources", "GeoLite2-Country.mmdb")))
.AddSingleton<IAlertManager, AlertManager>() .AddSingleton<IAlertManager, AlertManager>()
.AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>() .AddTransient<IScriptPluginTimerHelper, ScriptPluginTimerHelper>()
.AddSingleton<IInteractionRegistration, InteractionRegistration>()
.AddSingleton(translationLookup) .AddSingleton(translationLookup)
.AddDatabaseContextOptions(appConfig); .AddDatabaseContextOptions(appConfig);

View File

@ -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;
}
}

View File

@ -339,6 +339,41 @@ namespace IW4MAdmin.Application.Misc
return Task.CompletedTask; 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> /// <summary>
/// finds declared script commands in the script plugin /// finds declared script commands in the script plugin
/// </summary> /// </summary>

View File

@ -10,7 +10,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" /> <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> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -16,7 +16,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -19,7 +19,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -11,10 +11,10 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.6.16.1"/> <PackageReference Include="RaidMax.IW4MAdmin.SharedLibraryCore" Version="2022.9.8.1" PrivateAssets="All"/>
</ItemGroup> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <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> </Target>
</Project> </Project>

View File

@ -1,12 +1,18 @@
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Helpers;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
namespace Mute; namespace Mute;
public class Plugin : IPlugin 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); DataManager = new DataManager(metaService);
} }
@ -45,11 +51,58 @@ public class Plugin : IPlugin
public Task OnLoadAsync(IManager manager) 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; return Task.CompletedTask;
} }
public Task OnUnloadAsync() public Task OnUnloadAsync()
{ {
_interactionRegistration.UnregisterInteraction(MuteInteraction);
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@ -16,7 +16,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -19,7 +19,7 @@ const commands = [{
const plugin = { const plugin = {
author: 'RaidMax', author: 'RaidMax',
version: 1.3, version: 1.4,
name: 'VPN Detection Plugin', name: 'VPN Detection Plugin',
manager: null, manager: null,
logger: null, logger: null,
@ -82,9 +82,35 @@ const plugin = {
this.configHandler = _configHandler; this.configHandler = _configHandler;
this.configHandler.GetValue('vpnExceptionIds').forEach(element => vpnExceptionIds.push(element)); this.configHandler.GetValue('vpnExceptionIds').forEach(element => vpnExceptionIds.push(element));
this.logger.WriteInfo(`Loaded ${vpnExceptionIds.length} ids into whitelist`); 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 () { onUnloadAsync: function () {
this.interactionRegistration.UnregisterInteraction('WhitelistVPN');
}, },
onTickAsync: function (server) { onTickAsync: function (server) {

View File

@ -17,7 +17,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <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> </ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent"> <Target Name="PostBuild" AfterTargets="PostBuildEvent">

View File

@ -20,7 +20,7 @@
</Target> </Target>
<ItemGroup> <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> </ItemGroup>
</Project> </Project>

View File

@ -34,5 +34,6 @@ namespace SharedLibraryCore.Dtos
public string CurrentServerName { get; set; } public string CurrentServerName { get; set; }
public IGeoLocationResult GeoLocationInfo { get; set; } public IGeoLocationResult GeoLocationInfo { get; set; }
public ClientNoteMetaResponse NoteMeta { get; set; } public ClientNoteMetaResponse NoteMeta { get; set; }
public List<IInteractionData> Interactions { get; set; }
} }
} }

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Data.Models.Client;
using SharedLibraryCore.Interfaces;
using InteractionCallback = System.Func<int?, Data.Models.Reference.Game?, System.Threading.CancellationToken, System.Threading.Tasks.Task<string>>;
using ScriptInteractionCallback = System.Func<int?, Data.Models.Reference.Game?, System.Threading.CancellationToken, System.Threading.Tasks.Task<string>>;
namespace SharedLibraryCore.Helpers;
public class InteractionData : IInteractionData
{
public int? EntityId { get; set; }
public bool Enabled { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string DisplayMeta { get; set; }
public string ActionPath { get; set; }
public Dictionary<string, string> ActionMeta { get; set; } = new();
public string ActionUri => ActionPath + "?" + string.Join('&', ActionMeta.Select(kvp => $"{kvp.Key}={kvp.Value}"));
public EFClient.Permission? MinimumPermission { get; set; }
public string PermissionEntity { get; set; } = "Interaction";
public string PermissionAccess { get; set; } = "Read";
public string Source { get; set; }
public InteractionCallback Action { get; set; }
public Delegate ScriptAction { get; set; }
}

View File

@ -0,0 +1,25 @@
using System;
using System.Collections.Generic;
using Data.Models.Client;
using InteractionCallback = System.Func<int?, Data.Models.Reference.Game?, System.Threading.CancellationToken, System.Threading.Tasks.Task<string>>;
using ScriptInteractionCallback = System.Func<int?, Data.Models.Reference.Game?, System.Threading.CancellationToken, System.Threading.Tasks.Task<string>>;
namespace SharedLibraryCore.Interfaces;
public interface IInteractionData
{
int? EntityId { get; }
bool Enabled { get; }
string Name { get; }
string Description { get; }
string DisplayMeta { get; }
string ActionPath { get; }
Dictionary<string, string> ActionMeta { get; }
string ActionUri { get; }
EFClient.Permission? MinimumPermission { get; }
string PermissionEntity { get; }
string PermissionAccess { get; }
string Source { get; }
InteractionCallback Action { get; }
Delegate ScriptAction { get; }
}

View File

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Data.Models;
namespace SharedLibraryCore.Interfaces;
public interface IInteractionRegistration
{
void RegisterScriptInteraction(string interactionName, string source, Delegate interactionRegistration);
void RegisterInteraction(string interactionName, Func<int?, Reference.Game?, CancellationToken, Task<IInteractionData>> interactionRegistration);
void UnregisterInteraction(string interactionName);
Task<IEnumerable<IInteractionData>> GetInteractions(int? clientId = null,
Reference.Game? game = null, CancellationToken token = default);
Task<string> ProcessInteraction(string interactionId, int? clientId = null, Reference.Game? game = null, CancellationToken token = default);
}

View File

@ -4,7 +4,7 @@
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework> <TargetFramework>net6.0</TargetFramework>
<PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId> <PackageId>RaidMax.IW4MAdmin.SharedLibraryCore</PackageId>
<Version>2022.6.16.1</Version> <Version>2022.9.8.1</Version>
<Authors>RaidMax</Authors> <Authors>RaidMax</Authors>
<Company>Forever None</Company> <Company>Forever None</Company>
<Configurations>Debug;Release;Prerelease</Configurations> <Configurations>Debug;Release;Prerelease</Configurations>
@ -19,7 +19,7 @@
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
<Description>Shared Library for IW4MAdmin</Description> <Description>Shared Library for IW4MAdmin</Description>
<PackageVersion>2022.6.16.1</PackageVersion> <PackageVersion>2022.9.8.1</PackageVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn> <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup> </PropertyGroup>

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Data.Models; using Data.Models;
@ -24,6 +25,7 @@ namespace WebfrontCore.Controllers
{ {
private readonly ApplicationConfiguration _appConfig; private readonly ApplicationConfiguration _appConfig;
private readonly IMetaServiceV2 _metaService; private readonly IMetaServiceV2 _metaService;
private readonly IInteractionRegistration _interactionRegistration;
private readonly string _banCommandName; private readonly string _banCommandName;
private readonly string _tempbanCommandName; private readonly string _tempbanCommandName;
private readonly string _unbanCommandName; private readonly string _unbanCommandName;
@ -37,10 +39,12 @@ namespace WebfrontCore.Controllers
private readonly string _addClientNoteCommandName; private readonly string _addClientNoteCommandName;
public ActionController(IManager manager, IEnumerable<IManagerCommand> registeredCommands, public ActionController(IManager manager, IEnumerable<IManagerCommand> registeredCommands,
ApplicationConfiguration appConfig, IMetaServiceV2 metaService) : base(manager) ApplicationConfiguration appConfig, IMetaServiceV2 metaService,
IInteractionRegistration interactionRegistration) : base(manager)
{ {
_appConfig = appConfig; _appConfig = appConfig;
_metaService = metaService; _metaService = metaService;
_interactionRegistration = interactionRegistration;
foreach (var cmd in registeredCommands) foreach (var cmd in registeredCommands)
{ {
@ -86,6 +90,81 @@ namespace WebfrontCore.Controllers
} }
} }
public IActionResult DynamicActionForm(int? id, string meta)
{
var metaDict = JsonSerializer.Deserialize<Dictionary<string, string>>(meta);
if (metaDict is null)
{
return BadRequest();
}
metaDict.TryGetValue(nameof(ActionInfo.ActionButtonLabel), out var label);
metaDict.TryGetValue(nameof(ActionInfo.Name), out var name);
metaDict.TryGetValue(nameof(ActionInfo.ShouldRefresh), out var refresh);
metaDict.TryGetValue("Data", out var data);
metaDict.TryGetValue("InteractionId", out var interactionId);
bool.TryParse(refresh, out var shouldRefresh);
var inputs = new List<InputInfo>
{
new()
{
Name = "InteractionId",
Value = interactionId,
Type = "hidden"
},
new()
{
Name = "data",
Value = data,
Type = "hidden"
},
new()
{
Name = "TargetId",
Value = id?.ToString(),
Type = "hidden"
}
};
var info = new ActionInfo
{
ActionButtonLabel = label,
Name = name,
Action = nameof(DynamicActionAsync),
ShouldRefresh = shouldRefresh,
Inputs = inputs
};
return View("_ActionForm", info);
}
public async Task<IActionResult> DynamicActionAsync(string interactionId, string data, int? targetId,
CancellationToken token = default)
{
if (interactionId == "command")
{
var server = Manager.GetServers().First();
return await Task.FromResult(RedirectToAction("Execute", "Console", new
{
serverId = server.EndPoint,
command = $"{_appConfig.CommandPrefix}{data}"
}));
}
var game = (Reference.Game?)null;
if (targetId.HasValue)
{
game = (await Manager.GetClientService().Get(targetId.Value))?.GameName;
}
return Ok(await _interactionRegistration.ProcessInteraction(interactionId, targetId, game, token));
}
public IActionResult BanForm() public IActionResult BanForm()
{ {
var info = new ActionInfo var info = new ActionInfo

View File

@ -25,14 +25,16 @@ namespace WebfrontCore.Controllers
private readonly StatsConfiguration _config; private readonly StatsConfiguration _config;
private readonly IGeoLocationService _geoLocationService; private readonly IGeoLocationService _geoLocationService;
private readonly ClientService _clientService; private readonly ClientService _clientService;
private readonly IInteractionRegistration _interactionRegistration;
public ClientController(IManager manager, IMetaServiceV2 metaService, StatsConfiguration config, public ClientController(IManager manager, IMetaServiceV2 metaService, StatsConfiguration config,
IGeoLocationService geoLocationService, ClientService clientService) : base(manager) IGeoLocationService geoLocationService, ClientService clientService, IInteractionRegistration interactionRegistration) : base(manager)
{ {
_metaService = metaService; _metaService = metaService;
_config = config; _config = config;
_geoLocationService = geoLocationService; _geoLocationService = geoLocationService;
_clientService = clientService; _clientService = clientService;
_interactionRegistration = interactionRegistration;
} }
[Obsolete] [Obsolete]
@ -75,6 +77,8 @@ namespace WebfrontCore.Controllers
note.OriginEntityName = await _clientService.GetClientNameById(note.OriginEntityId); note.OriginEntityName = await _clientService.GetClientNameById(note.OriginEntityId);
} }
var interactions = await _interactionRegistration.GetInteractions(id, client.GameName, token);
// even though we haven't set their level to "banned" yet // even though we haven't set their level to "banned" yet
// (ie they haven't reconnected with the infringing player identifier) // (ie they haven't reconnected with the infringing player identifier)
// we want to show them as banned as to not confuse people. // we want to show them as banned as to not confuse people.
@ -137,7 +141,8 @@ namespace WebfrontCore.Controllers
ingameClient.CurrentServer.Port), ingameClient.CurrentServer.Port),
CurrentServerName = ingameClient?.CurrentServer?.Hostname, CurrentServerName = ingameClient?.CurrentServer?.Hostname,
GeoLocationInfo = await _geoLocationService.Locate(client.IPAddressString), GeoLocationInfo = await _geoLocationService.Locate(client.IPAddressString),
NoteMeta = string.IsNullOrWhiteSpace(note?.Note) ? null: note NoteMeta = string.IsNullOrWhiteSpace(note?.Note) ? null: note,
Interactions = interactions.ToList()
}; };
var meta = await _metaService.GetRuntimeMeta<InformationResponse>(new ClientPaginationRequest var meta = await _metaService.GetRuntimeMeta<InformationResponse>(new ClientPaginationRequest

View File

@ -15,7 +15,8 @@ public enum WebfrontEntity
RecentPlayersPage, RecentPlayersPage,
ProfilePage, ProfilePage,
AdminMenu, AdminMenu,
ClientNote ClientNote,
Interaction
} }
public enum WebfrontPermission public enum WebfrontPermission

View File

@ -105,6 +105,8 @@ namespace WebfrontCore
{ {
options.AccessDeniedPath = "/"; options.AccessDeniedPath = "/";
options.LoginPath = "/"; options.LoginPath = "/";
options.Events.OnValidatePrincipal += ClaimsPermissionRemoval.ValidateAsync;
options.Events.OnSignedIn += ClaimsPermissionRemoval.OnSignedIn;
}); });
services.AddSingleton(Program.Manager); services.AddSingleton(Program.Manager);
@ -138,6 +140,7 @@ namespace WebfrontCore
services.AddSingleton(Program.ApplicationServiceProvider services.AddSingleton(Program.ApplicationServiceProvider
.GetRequiredService<StatsConfiguration>()); .GetRequiredService<StatsConfiguration>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IServerDataViewer>()); services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IServerDataViewer>());
services.AddSingleton(Program.ApplicationServiceProvider.GetRequiredService<IInteractionRegistration>());
} }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

View File

@ -12,6 +12,7 @@ public class SideContextMenuItem
public string Icon { get; set; } public string Icon { get; set; }
public string Tooltip { get; set; } public string Tooltip { get; set; }
public int? EntityId { get; set; } public int? EntityId { get; set; }
public string Meta { get; set; }
} }

View File

@ -3,7 +3,7 @@
@{ @{
Layout = null; Layout = null;
} }
<h5 class="modal-title mb-10">@Model.Name.Titleize()</h5> <h5 class="modal-title mb-10">@Model.Name?.Titleize()</h5>
@if (Model.Inputs.Any(input => input.Type != "hidden")) @if (Model.Inputs.Any(input => input.Type != "hidden"))
{ {
<hr class="mb-10"/> <hr class="mb-10"/>

View File

@ -22,7 +22,7 @@
EFPenalty.PenaltyType.Flag => "alert-secondary", EFPenalty.PenaltyType.Flag => "alert-secondary",
EFPenalty.PenaltyType.TempBan => "alert-secondary", EFPenalty.PenaltyType.TempBan => "alert-secondary",
_ => "alert" _ => "alert"
}; };
} }
string ClassForProfileBackground() string ClassForProfileBackground()
@ -391,6 +391,20 @@
}); });
} }
foreach (var interaction in Model.Interactions.Where(i => (int)ViewBag.User.Level >= ((int?)i.MinimumPermission ?? 0)))
{
menuItems.Items.Add(new SideContextMenuItem
{
Title = interaction.Name,
Tooltip = interaction.Description,
EntityId = interaction.EntityId,
Icon = interaction.DisplayMeta,
Reference = interaction.ActionPath,
Meta = System.Text.Json.JsonSerializer.Serialize(interaction.ActionMeta),
IsButton = true
});
}
} }
<partial name="_SideContextMenu" for="@menuItems"></partial> <partial name="_SideContextMenu" for="@menuItems"></partial>

View File

@ -8,7 +8,7 @@
@foreach (var item in Model.Items) @foreach (var item in Model.Items)
{ {
<a href="@(item.IsLink ? item.Reference : "#")" class="@(item.IsLink ? "" : "profile-action")" data-action="@(item.IsLink ? "" : item.Reference)" data-action-id="@item.EntityId"> <a href="@(item.IsLink ? item.Reference : "#")" class="@(item.IsLink ? "" : "profile-action")" data-action="@(item.IsLink ? "" : item.Reference)" data-action-id="@item.EntityId" data-action-meta="@item.Meta">
<div class="@(item.IsButton ? "btn btn-block" : "")" data-title="@item.Tooltip" data-placement="left" data-toggle="@(string.IsNullOrEmpty(item.Tooltip) ? "" : "tooltip")"> <div class="@(item.IsButton ? "btn btn-block" : "")" data-title="@item.Tooltip" data-placement="left" data-toggle="@(string.IsNullOrEmpty(item.Tooltip) ? "" : "tooltip")">
<i class="@(string.IsNullOrEmpty(item.Icon) ? "" : $"oi {item.Icon}") mr-5 font-size-12"></i> <i class="@(string.IsNullOrEmpty(item.Icon) ? "" : $"oi {item.Icon}") mr-5 font-size-12"></i>
<span class="@(item.IsActive ? "text-primary" : "") text-truncate">@item.Title</span> <span class="@(item.IsActive ? "text-primary" : "") text-truncate">@item.Title</span>
@ -28,7 +28,7 @@
@foreach (var item in Model.Items) @foreach (var item in Model.Items)
{ {
<div class="mt-15 mb-15"> <div class="mt-15 mb-15">
<a href="@(item.IsLink ? item.Reference : "#")" class="@(item.IsLink ? "" : "profile-action") no-decoration" data-action="@(item.IsLink ? "" : item.Reference)" data-action-id="@item.EntityId"> <a href="@(item.IsLink ? item.Reference : "#")" class="@(item.IsLink ? "" : "profile-action") no-decoration" data-action="@(item.IsLink ? "" : item.Reference)" data-action-id="@item.EntityId" data-action-meta="@item.Meta">
<div class="btn btn-block btn-lg @(item.IsActive ? "btn-primary" : "") text-truncate" data-title="@item.Tooltip" data-toggle="@(string.IsNullOrEmpty(item.Tooltip) ? "" : "tooltip")"> <div class="btn btn-block btn-lg @(item.IsActive ? "btn-primary" : "") text-truncate" data-title="@item.Tooltip" data-toggle="@(string.IsNullOrEmpty(item.Tooltip) ? "" : "tooltip")">
<i class="@(string.IsNullOrEmpty(item.Icon) ? "" : $"oi {item.Icon}") mr-5 font-size-12"></i> <i class="@(string.IsNullOrEmpty(item.Icon) ? "" : $"oi {item.Icon}") mr-5 font-size-12"></i>
<span>@item.Title</span> <span>@item.Title</span>

View File

@ -91,12 +91,18 @@ $(document).ready(function () {
$(document).off('click', '.profile-action'); $(document).off('click', '.profile-action');
$(document).on('click', '.profile-action', function (e) { $(document).on('click', '.profile-action', function (e) {
e.preventDefault(); e.preventDefault();
const actionType = $(this).data('action'); const action = $(this).data('action');
const actionId = $(this).data('action-id'); const actionId = $(this).data('action-id');
const actionMeta = $(this).data('action-meta');
const responseDuration = $(this).data('response-duration') || 5000; const responseDuration = $(this).data('response-duration') || 5000;
const actionIdKey = actionId === undefined ? '' : `?id=${actionId}`; let actionKeys = actionId === undefined ? '' : `?id=${actionId}`;
if (actionMeta !== undefined) {
actionKeys = actionKeys + '&meta=' + JSON.stringify(actionMeta);
}
showLoader(); showLoader();
$.get(`/Action/${actionType}Form/${actionIdKey}`)
$.get(`/Action/${action}Form/${actionKeys}`)
.done(function (response) { .done(function (response) {
$('#actionModal .modal-message').fadeOut('fast') $('#actionModal .modal-message').fadeOut('fast')
$('#actionModal').attr('data-response-duration', responseDuration); $('#actionModal').attr('data-response-duration', responseDuration);