Mute Banner for Profile & Prevent Self-Target & Correctly Expire Early Unmutes (#272)

* Fix self-targeting
Remove creation of penalty on mute expiration

* Display mute penalties on profile
Expire mute penalties on unmute

* Resolves issues in code review
Added comment in ClientController.cs
Fixed order of operations in MuteManager.cs
Fixed condition in MuteManager.cs

* Fix self-targeting
Remove creation of penalty on mute expiration

* Display mute penalties on profile
Expire mute penalties on unmute

* Resolves issues in code review
Added comment in ClientController.cs
Fixed order of operations in MuteManager.cs
Fixed condition in MuteManager.cs

* Changed localisation value to be more generic
Fix null reference warning (it should never be null) (34da216)
This commit is contained in:
Amos 2022-10-25 00:58:12 +01:00 committed by GitHub
parent dbca3675ba
commit a16986f7a3
9 changed files with 72 additions and 21 deletions

View File

@ -34,6 +34,12 @@ public class MuteCommand : Command
public override async Task ExecuteAsync(GameEvent gameEvent) public override async Task ExecuteAsync(GameEvent gameEvent)
{ {
if (gameEvent.Origin.ClientId == gameEvent.Target.ClientId)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_DENY_SELF_TARGET"]);
return;
}
if (await Plugin.MuteManager.Mute(gameEvent.Owner, gameEvent.Origin, gameEvent.Target, null, gameEvent.Data)) if (await Plugin.MuteManager.Mute(gameEvent.Owner, gameEvent.Origin, gameEvent.Target, null, gameEvent.Data))
{ {
gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_MUTE_MUTED"] gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_MUTE_MUTED"]

View File

@ -42,6 +42,12 @@ public class TempMuteCommand : Command
public override async Task ExecuteAsync(GameEvent gameEvent) public override async Task ExecuteAsync(GameEvent gameEvent)
{ {
if (gameEvent.Origin.ClientId == gameEvent.Target.ClientId)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_DENY_SELF_TARGET"]);
return;
}
var match = Regex.Match(gameEvent.Data, TempBanRegex); var match = Regex.Match(gameEvent.Data, TempBanRegex);
if (match.Success) if (match.Success)
{ {

View File

@ -34,6 +34,12 @@ public class UnmuteCommand : Command
public override async Task ExecuteAsync(GameEvent gameEvent) public override async Task ExecuteAsync(GameEvent gameEvent)
{ {
if (gameEvent.Origin.ClientId == gameEvent.Target.ClientId)
{
gameEvent.Origin.Tell(_translationLookup["COMMANDS_DENY_SELF_TARGET"]);
return;
}
if (await Plugin.MuteManager.Unmute(gameEvent.Owner, gameEvent.Origin, gameEvent.Target, gameEvent.Data)) if (await Plugin.MuteManager.Unmute(gameEvent.Owner, gameEvent.Origin, gameEvent.Target, gameEvent.Data))
{ {
gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_UNMUTE_UNMUTED"] gameEvent.Origin.Tell(_translationLookup["PLUGINS_MUTE_COMMANDS_UNMUTE_UNMUTED"]

View File

@ -1,4 +1,7 @@
using Data.Models; using Data.Abstractions;
using Data.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Database.Models; using SharedLibraryCore.Database.Models;
@ -13,13 +16,15 @@ public class MuteManager
private readonly IMetaServiceV2 _metaService; private readonly IMetaServiceV2 _metaService;
private readonly ITranslationLookup _translationLookup; private readonly ITranslationLookup _translationLookup;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IDatabaseContextFactory _databaseContextFactory;
private readonly SemaphoreSlim _onMuteAction = new(1, 1); private readonly SemaphoreSlim _onMuteAction = new(1, 1);
public MuteManager(IMetaServiceV2 metaService, ITranslationLookup translationLookup, ILogger logger) public MuteManager(IServiceProvider serviceProvider)
{ {
_metaService = metaService; _metaService = serviceProvider.GetRequiredService<IMetaServiceV2>();
_translationLookup = translationLookup; _translationLookup = serviceProvider.GetRequiredService<ITranslationLookup>();
_logger = logger; _logger = serviceProvider.GetRequiredService<ILogger<MuteManager>>();
_databaseContextFactory = serviceProvider.GetRequiredService<IDatabaseContextFactory>();
} }
public static bool IsExpiredMute(MuteStateMeta muteStateMeta) => public static bool IsExpiredMute(MuteStateMeta muteStateMeta) =>
@ -98,11 +103,13 @@ public class MuteManager
if (clientMuteMeta.MuteState is MuteState.Unmuted && clientMuteMeta.CommandExecuted) return false; if (clientMuteMeta.MuteState is MuteState.Unmuted && clientMuteMeta.CommandExecuted) return false;
if (!target.IsIngame && clientMuteMeta.MuteState is MuteState.Unmuting) return false; if (!target.IsIngame && clientMuteMeta.MuteState is MuteState.Unmuting) return false;
if (clientMuteMeta.MuteState is not MuteState.Unmuting) if (clientMuteMeta.MuteState is not MuteState.Unmuting && origin.ClientId != 1)
{ {
await CreatePenalty(MuteState.Unmuted, origin, target, DateTime.UtcNow, reason); await CreatePenalty(MuteState.Unmuted, origin, target, DateTime.UtcNow, reason);
} }
await ExpireMutePenalties(target);
clientMuteMeta = new MuteStateMeta clientMuteMeta = new MuteStateMeta
{ {
Expiration = DateTime.UtcNow, Expiration = DateTime.UtcNow,
@ -130,7 +137,6 @@ public class MuteManager
Offender = target, Offender = target,
Offense = reason, Offense = reason,
Punisher = origin, Punisher = origin,
Active = true,
Link = target.AliasLink Link = target.AliasLink
}; };
_logger.LogDebug("Creating new {MuteState} Penalty for {Target} with reason {Reason}", _logger.LogDebug("Creating new {MuteState} Penalty for {Target} with reason {Reason}",
@ -138,6 +144,24 @@ public class MuteManager
await newPenalty.TryCreatePenalty(Plugin.Manager.GetPenaltyService(), _logger); await newPenalty.TryCreatePenalty(Plugin.Manager.GetPenaltyService(), _logger);
} }
private async Task ExpireMutePenalties(EFClient client)
{
await using var context = _databaseContextFactory.CreateContext();
var mutePenalties = await context.Penalties
.Where(penalty => penalty.OffenderId == client.ClientId &&
(penalty.Type == EFPenalty.PenaltyType.Mute ||
penalty.Type == EFPenalty.PenaltyType.TempMute) &&
(penalty.Expires == null || penalty.Expires > DateTime.UtcNow))
.ToListAsync();
foreach (var mutePenalty in mutePenalties)
{
mutePenalty.Expires = DateTime.UtcNow;
}
await context.SaveChangesAsync();
}
public static async Task PerformGameCommand(Server server, EFClient? client, MuteStateMeta muteStateMeta) public static async Task PerformGameCommand(Server server, EFClient? client, MuteStateMeta muteStateMeta)
{ {
if (client is null || !client.IsIngame) return; if (client is null || !client.IsIngame) return;

View File

@ -4,7 +4,7 @@ namespace Mute;
public class MuteStateMeta public class MuteStateMeta
{ {
public string Reason { get; set; } = string.Empty; public string? Reason { get; set; }
public DateTime? Expiration { get; set; } public DateTime? Expiration { get; set; }
public MuteState MuteState { get; set; } public MuteState MuteState { get; set; }
[JsonIgnore] public bool CommandExecuted { get; set; } [JsonIgnore] public bool CommandExecuted { get; set; }

View File

@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging; using Data.Abstractions;
using Microsoft.Extensions.Logging;
using Mute.Commands; using Mute.Commands;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Commands; using SharedLibraryCore.Commands;
@ -12,7 +13,7 @@ namespace Mute;
public class Plugin : IPlugin public class Plugin : IPlugin
{ {
public string Name => "Mute"; public string Name => "Mute";
public float Version => (float) Utilities.GetVersionAsDouble(); public float Version => (float)Utilities.GetVersionAsDouble();
public string Author => "Amos"; public string Author => "Amos";
public const string MuteKey = "IW4MMute"; public const string MuteKey = "IW4MMute";
@ -24,12 +25,12 @@ public class Plugin : IPlugin
private readonly IRemoteCommandService _remoteCommandService; private readonly IRemoteCommandService _remoteCommandService;
private static readonly string MuteInteraction = "Webfront::Profile::Mute"; private static readonly string MuteInteraction = "Webfront::Profile::Mute";
public Plugin(ILogger<Plugin> logger, IMetaServiceV2 metaService, IInteractionRegistration interactionRegistration, public Plugin(ILogger<Plugin> logger, IInteractionRegistration interactionRegistration,
ITranslationLookup translationLookup, IRemoteCommandService remoteCommandService) IRemoteCommandService remoteCommandService, IServiceProvider serviceProvider)
{ {
_interactionRegistration = interactionRegistration; _interactionRegistration = interactionRegistration;
_remoteCommandService = remoteCommandService; _remoteCommandService = remoteCommandService;
MuteManager = new MuteManager(metaService, translationLookup, logger); MuteManager = new MuteManager(serviceProvider);
} }
public async Task OnEventAsync(GameEvent gameEvent, Server server) public async Task OnEventAsync(GameEvent gameEvent, Server server)
@ -56,7 +57,7 @@ public class Plugin : IPlugin
case MuteState.Unmuting: case MuteState.Unmuting:
// Handle unmute of unmuted players. // Handle unmute of unmuted players.
await MuteManager.Unmute(server, Utilities.IW4MAdminClient(), gameEvent.Origin, await MuteManager.Unmute(server, Utilities.IW4MAdminClient(), gameEvent.Origin,
muteMetaJoin.Reason); muteMetaJoin.Reason ?? string.Empty);
gameEvent.Origin.Tell(Utilities.CurrentLocalization gameEvent.Origin.Tell(Utilities.CurrentLocalization
.LocalizationIndex["PLUGINS_MUTE_COMMANDS_UNMUTE_TARGET_UNMUTED"] .LocalizationIndex["PLUGINS_MUTE_COMMANDS_UNMUTE_TARGET_UNMUTED"]
.FormatExt(muteMetaJoin.Reason)); .FormatExt(muteMetaJoin.Reason));
@ -134,7 +135,7 @@ public class Plugin : IPlugin
_interactionRegistration.RegisterInteraction(MuteInteraction, async (targetClientId, game, token) => _interactionRegistration.RegisterInteraction(MuteInteraction, async (targetClientId, game, token) =>
{ {
if (!targetClientId.HasValue || game.HasValue && !SupportedGames.Contains((Server.Game) game.Value)) if (!targetClientId.HasValue || game.HasValue && !SupportedGames.Contains((Server.Game)game.Value))
{ {
return null; return null;
} }
@ -162,7 +163,7 @@ public class Plugin : IPlugin
Name = "Reason", Name = "Reason",
Label = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ACTION_LABEL_REASON"], Label = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ACTION_LABEL_REASON"],
Type = "text", Type = "text",
Values = (Dictionary<string, string>?) null Values = (Dictionary<string, string>?)null
}; };
var durationInput = new var durationInput = new
@ -170,7 +171,7 @@ public class Plugin : IPlugin
Name = "Duration", Name = "Duration",
Label = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ACTION_LABEL_DURATION"], Label = Utilities.CurrentLocalization.LocalizationIndex["WEBFRONT_ACTION_LABEL_DURATION"],
Type = "select", Type = "select",
Values = (Dictionary<string, string>?) new Dictionary<string, string> Values = (Dictionary<string, string>?)new Dictionary<string, string>
{ {
{"5m", TimeSpan.FromMinutes(5).HumanizeForCurrentCulture()}, {"5m", TimeSpan.FromMinutes(5).HumanizeForCurrentCulture()},
{"30m", TimeSpan.FromMinutes(30).HumanizeForCurrentCulture()}, {"30m", TimeSpan.FromMinutes(30).HumanizeForCurrentCulture()},

View File

@ -131,7 +131,7 @@ namespace SharedLibraryCore.Services
} }
private static readonly EFPenalty.PenaltyType[] LinkedPenalties = private static readonly EFPenalty.PenaltyType[] LinkedPenalties =
{ EFPenalty.PenaltyType.Ban, EFPenalty.PenaltyType.Flag, EFPenalty.PenaltyType.TempBan }; { EFPenalty.PenaltyType.Ban, EFPenalty.PenaltyType.Flag, EFPenalty.PenaltyType.TempBan, EFPenalty.PenaltyType.TempMute, EFPenalty.PenaltyType.Mute };
private static readonly Expression<Func<EFPenalty, bool>> Filter = p => private static readonly Expression<Func<EFPenalty, bool>> Filter = p =>
LinkedPenalties.Contains(p.Type) && p.Active && (p.Expires == null || p.Expires > DateTime.UtcNow); LinkedPenalties.Contains(p.Type) && p.Active && (p.Expires == null || p.Expires > DateTime.UtcNow);

View File

@ -162,7 +162,13 @@ namespace WebfrontCore.Controllers
}); });
} }
clientDto.ActivePenalty = activePenalties.OrderByDescending(_penalty => _penalty.Type).FirstOrDefault(); // Reducing the enum value for Temp/Mute so bans appear in client banner first
clientDto.ActivePenalty = activePenalties.MaxBy(penalty => penalty.Type switch
{
EFPenalty.PenaltyType.TempMute => 0,
EFPenalty.PenaltyType.Mute => 1,
_ => (int)penalty.Type
});
clientDto.Meta.AddRange(Authorized ? meta : meta.Where(m => !m.IsSensitive)); clientDto.Meta.AddRange(Authorized ? meta : meta.Where(m => !m.IsSensitive));
var strippedName = clientDto.Name.StripColors(); var strippedName = clientDto.Name.StripColors();

View File

@ -21,6 +21,8 @@
EFPenalty.PenaltyType.Ban => "alert-danger", EFPenalty.PenaltyType.Ban => "alert-danger",
EFPenalty.PenaltyType.Flag => "alert-secondary", EFPenalty.PenaltyType.Flag => "alert-secondary",
EFPenalty.PenaltyType.TempBan => "alert-secondary", EFPenalty.PenaltyType.TempBan => "alert-secondary",
EFPenalty.PenaltyType.TempMute => "alert-secondary",
EFPenalty.PenaltyType.Mute => "alert-secondary",
_ => "alert" _ => "alert"
}; };
} }
@ -367,7 +369,7 @@
}); });
} }
if ((Model.LevelInt < (int)ViewBag.User.Level && !Model.HasActivePenalty || isTempBanned) && ViewBag.Authorized) if ((Model.LevelInt < (int)ViewBag.User.Level && !isPermBanned || isTempBanned) && ViewBag.Authorized)
{ {
menuItems.Items.Add(new SideContextMenuItem menuItems.Items.Add(new SideContextMenuItem
{ {
@ -379,7 +381,7 @@
}); });
} }
if ((Model.LevelInt < (int)ViewBag.User.Level && Model.HasActivePenalty || isTempBanned) && ViewBag.Authorized) if ((Model.LevelInt < (int)ViewBag.User.Level && isPermBanned || isTempBanned) && ViewBag.Authorized)
{ {
menuItems.Items.Add(new SideContextMenuItem menuItems.Items.Add(new SideContextMenuItem
{ {