more consistent/enhanced game penalty messages per issue #171

This commit is contained in:
RaidMax 2020-11-17 18:24:54 -06:00
parent a574fb0d4b
commit 941d9cea73
11 changed files with 192 additions and 32 deletions

View File

@ -1,4 +1,5 @@
using System; using System;
using Microsoft.Extensions.DependencyInjection;
using SharedLibraryCore; using SharedLibraryCore;
using SharedLibraryCore.Configuration; using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces; using SharedLibraryCore.Interfaces;
@ -36,7 +37,7 @@ namespace IW4MAdmin.Application.Factories
/// <returns></returns> /// <returns></returns>
public Server CreateServer(ServerConfiguration config, IManager manager) public Server CreateServer(ServerConfiguration config, IManager manager)
{ {
return new IW4MServer(config, _translationLookup, _metaService, _serviceProvider); return new IW4MServer(config, _translationLookup, _metaService, _serviceProvider, _serviceProvider.GetRequiredService<IClientNoticeMessageFormatter>());
} }
} }
} }

View File

@ -32,13 +32,15 @@ namespace IW4MAdmin
private int lastGameTime = 0; private int lastGameTime = 0;
public int Id { get; private set; } public int Id { get; private set; }
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly IClientNoticeMessageFormatter _messageFormatter;
public IW4MServer( public IW4MServer(
ServerConfiguration serverConfiguration, ServerConfiguration serverConfiguration,
ITranslationLookup lookup, ITranslationLookup lookup,
IMetaService metaService, IMetaService metaService,
IServiceProvider serviceProvider) : base(serviceProvider.GetRequiredService<ILogger<Server>>(), IServiceProvider serviceProvider,
IClientNoticeMessageFormatter messageFormatter) : base(serviceProvider.GetRequiredService<ILogger<Server>>(),
serviceProvider.GetRequiredService<SharedLibraryCore.Interfaces.ILogger>(), serviceProvider.GetRequiredService<SharedLibraryCore.Interfaces.ILogger>(),
serverConfiguration, serverConfiguration,
serviceProvider.GetRequiredService<IManager>(), serviceProvider.GetRequiredService<IManager>(),
@ -48,6 +50,7 @@ namespace IW4MAdmin
_translationLookup = lookup; _translationLookup = lookup;
_metaService = metaService; _metaService = metaService;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_messageFormatter = messageFormatter;
} }
public override async Task<EFClient> OnClientConnected(EFClient clientFromLog) public override async Task<EFClient> OnClientConnected(EFClient clientFromLog)
@ -454,7 +457,6 @@ namespace IW4MAdmin
else if (E.Type == GameEvent.EventType.TempBan) else if (E.Type == GameEvent.EventType.TempBan)
{ {
await TempBan(E.Data, (TimeSpan) E.Extra, E.Target, E.ImpersonationOrigin ?? E.Origin); await TempBan(E.Data, (TimeSpan) E.Extra, E.Target, E.ImpersonationOrigin ?? E.Origin);
;
} }
else if (E.Type == GameEvent.EventType.Ban) else if (E.Type == GameEvent.EventType.Ban)
@ -470,7 +472,7 @@ namespace IW4MAdmin
else if (E.Type == GameEvent.EventType.Kick) else if (E.Type == GameEvent.EventType.Kick)
{ {
await Kick(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin); await Kick(E.Data, E.Target, E.ImpersonationOrigin ?? E.Origin, E.Extra as EFPenalty);
} }
else if (E.Type == GameEvent.EventType.Warn) else if (E.Type == GameEvent.EventType.Warn)
@ -1015,6 +1017,12 @@ namespace IW4MAdmin
{ {
Website = loc["SERVER_WEBSITE_GENERIC"]; Website = loc["SERVER_WEBSITE_GENERIC"];
} }
// todo: remove this once _website is weaned off
if (string.IsNullOrEmpty(Manager.GetApplicationSettings().Configuration().ContactUri))
{
Manager.GetApplicationSettings().Configuration().ContactUri = Website;
}
InitializeMaps(); InitializeMaps();
@ -1190,7 +1198,7 @@ namespace IW4MAdmin
} }
} }
public override async Task Kick(string Reason, EFClient targetClient, EFClient originClient) public override async Task Kick(string reason, EFClient targetClient, EFClient originClient, EFPenalty previousPenalty)
{ {
targetClient = targetClient.ClientNumber < 0 ? targetClient = targetClient.ClientNumber < 0 ?
Manager.GetActiveClients() Manager.GetActiveClients()
@ -1202,7 +1210,7 @@ namespace IW4MAdmin
Type = EFPenalty.PenaltyType.Kick, Type = EFPenalty.PenaltyType.Kick,
Expires = DateTime.UtcNow, Expires = DateTime.UtcNow,
Offender = targetClient, Offender = targetClient,
Offense = Reason, Offense = reason,
Punisher = originClient, Punisher = originClient,
Link = targetClient.AliasLink Link = targetClient.AliasLink
}; };
@ -1221,8 +1229,11 @@ namespace IW4MAdmin
Manager.AddEvent(e); Manager.AddEvent(e);
// todo: move to translation sheet var formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick,
string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_KICK_TEXT"]} - ^5{Reason}^7"); targetClient.ClientNumber,
_messageFormatter.BuildFormattedMessage(RconParser.Configuration,
newPenalty,
previousPenalty));
await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick); await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick);
} }
} }
@ -1250,14 +1261,15 @@ namespace IW4MAdmin
if (targetClient.IsIngame) if (targetClient.IsIngame)
{ {
// todo: move to translation sheet var formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick,
string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"^7{loc["SERVER_TB_TEXT"]}- ^5{Reason}"); targetClient.ClientNumber,
_messageFormatter.BuildFormattedMessage(RconParser.Configuration, newPenalty));
ServerLogger.LogDebug("Executing tempban kick command for {targetClient}", targetClient.ToString()); ServerLogger.LogDebug("Executing tempban kick command for {targetClient}", targetClient.ToString());
await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick); await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick);
} }
} }
override public async Task Ban(string reason, EFClient targetClient, EFClient originClient, bool isEvade = false) public override async Task Ban(string reason, EFClient targetClient, EFClient originClient, bool isEvade = false)
{ {
// ensure player gets kicked if command not performed on them in the same server // ensure player gets kicked if command not performed on them in the same server
targetClient = targetClient.ClientNumber < 0 ? targetClient = targetClient.ClientNumber < 0 ?
@ -1283,8 +1295,9 @@ namespace IW4MAdmin
if (targetClient.IsIngame) if (targetClient.IsIngame)
{ {
ServerLogger.LogDebug("Attempting to kicking newly banned client {targetClient}", targetClient.ToString()); ServerLogger.LogDebug("Attempting to kicking newly banned client {targetClient}", targetClient.ToString());
// todo: move to translation sheet var formattedString = string.Format(RconParser.Configuration.CommandPrefixes.Kick,
string formattedString = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_BAN_TEXT"]} - ^5{reason} ^7{loc["SERVER_BAN_APPEAL"].FormatExt(Website)}^7"); targetClient.ClientNumber,
_messageFormatter.BuildFormattedMessage(RconParser.Configuration, newPenalty));
await targetClient.CurrentServer.ExecuteCommandAsync(formattedString); await targetClient.CurrentServer.ExecuteCommandAsync(formattedString);
} }
} }

View File

@ -350,6 +350,7 @@ namespace IW4MAdmin.Application
.AddSingleton<IMasterCommunication, MasterCommunication>() .AddSingleton<IMasterCommunication, MasterCommunication>()
.AddSingleton<IManager, ApplicationManager>() .AddSingleton<IManager, ApplicationManager>()
.AddSingleton<SharedLibraryCore.Interfaces.ILogger, Logger>() .AddSingleton<SharedLibraryCore.Interfaces.ILogger, Logger>()
.AddSingleton<IClientNoticeMessageFormatter, ClientNoticeMessageFormatter>()
.AddSingleton(translationLookup); .AddSingleton(translationLookup);
if (args.Contains("serialevents")) if (args.Contains("serialevents"))

View File

@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Database.Models;
using SharedLibraryCore.Interfaces;
namespace IW4MAdmin.Application.Misc
{
/// <summary>
/// implementation of IClientNoticeMessageFormatter
/// </summary>
public class ClientNoticeMessageFormatter : IClientNoticeMessageFormatter
{
private readonly ITranslationLookup _transLookup;
private readonly ApplicationConfiguration _appConfig;
public ClientNoticeMessageFormatter(ITranslationLookup transLookup, ApplicationConfiguration appConfig)
{
_transLookup = transLookup;
_appConfig = appConfig;
}
public string BuildFormattedMessage(IRConParserConfiguration config, EFPenalty currentPenalty, EFPenalty originalPenalty = null)
{
var penalty = originalPenalty ?? currentPenalty;
var builder = new StringBuilder();
// build the top level header
var header = _transLookup[$"SERVER_{penalty.Type.ToString().ToUpper()}_TEXT"];
builder.Append(header);
builder.Append(Environment.NewLine);
// build the reason
var reason = _transLookup["GAME_MESSAGE_PENALTY_REASON"].FormatExt(penalty.Offense);
foreach (var splitReason in SplitOverMaxLength(reason, config.NoticeMaxCharactersPerLine))
{
builder.Append(splitReason);
builder.Append(Environment.NewLine);
}
if (penalty.Type == EFPenalty.PenaltyType.TempBan)
{
// build the time remaining if temporary
var timeRemainingValue = penalty.Expires.HasValue
? (penalty.Expires - DateTime.UtcNow).Value.HumanizeForCurrentCulture()
: "--";
var timeRemaining = _transLookup["GAME_MESSAGE_PENALTY_TIME_REMAINING"].FormatExt(timeRemainingValue);
foreach (var splitReason in SplitOverMaxLength(timeRemaining, config.NoticeMaxCharactersPerLine))
{
builder.Append(splitReason);
builder.Append(Environment.NewLine);
}
}
if (penalty.Type == EFPenalty.PenaltyType.Ban)
{
// provide a place to appeal the ban (should always be specified but including a placeholder just incase)
builder.Append(_transLookup["GAME_MESSAGE_PENALTY_APPEAL"].FormatExt(_appConfig.ContactUri ?? "--"));
}
// final format looks something like:
/*
* You are permanently banned
* Reason - toxic behavior
* Visit example.com to appeal
*/
return builder.ToString();
}
private static IEnumerable<string> SplitOverMaxLength(string source, int maxCharactersPerLine)
{
if (source.Length <= maxCharactersPerLine)
{
return new[] {source};
}
var segments = new List<string>();
var currentLocation = 0;
while (currentLocation < source.Length)
{
var nextLocation = currentLocation + maxCharactersPerLine;
// there's probably a more efficient way to do this but this is readable
segments.Add(string.Concat(
source
.Skip(currentLocation)
.Take(Math.Min(maxCharactersPerLine, source.Length - currentLocation))));
currentLocation = nextLocation;
}
if (currentLocation < source.Length)
{
segments.Add(source.Substring(currentLocation, source.Length - currentLocation));
}
return segments;
}
}
}

View File

@ -22,6 +22,8 @@ namespace IW4MAdmin.Application.RconParsers
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber; public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
public IDictionary<string, string> OverrideDvarNameMapping { get; set; } = new Dictionary<string, string>(); public IDictionary<string, string> OverrideDvarNameMapping { get; set; } = new Dictionary<string, string>();
public IDictionary<string, string> DefaultDvarValues { get; set; } = new Dictionary<string, string>(); public IDictionary<string, string> DefaultDvarValues { get; set; } = new Dictionary<string, string>();
public int NoticeMaximumLines { get; set; } = 8;
public int NoticeMaxCharactersPerLine { get; set; } = 50;
public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory) public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory)
{ {

View File

@ -47,6 +47,8 @@ namespace SharedLibraryCore.Configuration
public string SocialLinkAddress { get; set; } public string SocialLinkAddress { get; set; }
[LocalizedDisplayName("SETUP_SOCIAL_TITLE")] [LocalizedDisplayName("SETUP_SOCIAL_TITLE")]
public string SocialLinkTitle { get; set; } public string SocialLinkTitle { get; set; }
[LocalizedDisplayName("SETUP_CONTACT_URI")]
public string ContactUri { get; set; }
[LocalizedDisplayName("SETUP_USE_CUSTOMENCODING")] [LocalizedDisplayName("SETUP_USE_CUSTOMENCODING")]
[ConfigurationLinked("CustomParserEncoding")] [ConfigurationLinked("CustomParserEncoding")]

View File

@ -0,0 +1,16 @@
using SharedLibraryCore.Database.Models;
namespace SharedLibraryCore.Interfaces
{
public interface IClientNoticeMessageFormatter
{
/// <summary>
/// builds a game formatted notice message
/// </summary>
/// <param name="currentPenalty">current penalty the message is for</param>
/// <param name="originalPenalty">previous penalty the current penalty relates to</param>
/// <param name="config">RCon parser config</param>
/// <returns></returns>
string BuildFormattedMessage(IRConParserConfiguration config, EFPenalty currentPenalty, EFPenalty originalPenalty = null);
}
}

View File

@ -0,0 +1,18 @@
using System.Threading.Tasks;
using SharedLibraryCore.Database.Models;
namespace SharedLibraryCore.Interfaces
{
public interface IGameServer
{
/// <summary>
/// kicks target on behalf of origin for given reason
/// </summary>
/// <param name="reason">reason client is being kicked</param>
/// <param name="target">client to kick</param>
/// <param name="origin">source of kick action</param>
/// <param name="previousPenalty">previous penalty the kick is occuring for (if applicable)</param>
/// <returns></returns>
public Task Kick(string reason, EFClient target, EFClient origin, EFPenalty previousPenalty = null);
}
}

View File

@ -62,5 +62,9 @@ namespace SharedLibraryCore.Interfaces
/// specifies the default dvar values for games that don't support certain dvars /// specifies the default dvar values for games that don't support certain dvars
/// </summary> /// </summary>
IDictionary<string, string> DefaultDvarValues { get; set; } IDictionary<string, string> DefaultDvarValues { get; set; }
int NoticeMaximumLines { get; set; }
int NoticeMaxCharactersPerLine { get; set; }
} }
} }

View File

@ -320,7 +320,15 @@ namespace SharedLibraryCore.Database.Models
/// </summary> /// </summary>
/// <param name="kickReason">reason to kick for</param> /// <param name="kickReason">reason to kick for</param>
/// <param name="sender">client performing the kick</param> /// <param name="sender">client performing the kick</param>
public GameEvent Kick(string kickReason, EFClient sender) public GameEvent Kick(string kickReason, EFClient sender) => Kick(kickReason, sender, null);
/// <summary>
/// kick a client for the given reason
/// </summary>
/// <param name="kickReason">reason to kick for</param>
/// <param name="sender">client performing the kick</param>
/// <param name="originalPenalty">original client penalty</param>
public GameEvent Kick(string kickReason, EFClient sender, EFPenalty originalPenalty)
{ {
var e = new GameEvent() var e = new GameEvent()
{ {
@ -329,6 +337,7 @@ namespace SharedLibraryCore.Database.Models
Target = this, Target = this,
Origin = sender, Origin = sender,
Data = kickReason, Data = kickReason,
Extra = originalPenalty,
Owner = sender.CurrentServer Owner = sender.CurrentServer
}; };
@ -597,7 +606,6 @@ namespace SharedLibraryCore.Database.Models
{ {
var loc = Utilities.CurrentLocalization.LocalizationIndex; var loc = Utilities.CurrentLocalization.LocalizationIndex;
var autoKickClient = Utilities.IW4MAdminClient(CurrentServer); var autoKickClient = Utilities.IW4MAdminClient(CurrentServer);
bool isAbleToConnectSimple = IsAbleToConnectSimple(); bool isAbleToConnectSimple = IsAbleToConnectSimple();
if (!isAbleToConnectSimple) if (!isAbleToConnectSimple)
@ -617,23 +625,18 @@ namespace SharedLibraryCore.Database.Models
// we want to kick them if any account is banned // we want to kick them if any account is banned
if (banPenalty != null) if (banPenalty != null)
{ {
if (Level == Permission.Banned) if (Level != Permission.Banned)
{
Utilities.DefaultLogger.LogInformation("Kicking {client} because they are banned", ToString());
Kick(loc["SERVER_BAN_PREV"].FormatExt(banPenalty?.Offense), autoKickClient);
return false;
}
else
{ {
Utilities.DefaultLogger.LogInformation( Utilities.DefaultLogger.LogInformation(
"Client {client} is banned, but using a new GUID, we we're updating their level and kicking them", "Client {client} is banned, but using a new GUID, we we're updating their level and kicking them",
ToString()); ToString());
await SetLevel(Permission.Banned, autoKickClient).WaitAsync(Utilities.DefaultCommandTimeout, await SetLevel(Permission.Banned, autoKickClient).WaitAsync(Utilities.DefaultCommandTimeout,
CurrentServer.Manager.CancellationToken); CurrentServer.Manager.CancellationToken);
Kick(loc["SERVER_BAN_PREV"].FormatExt(banPenalty?.Offense), autoKickClient);
return false;
} }
Utilities.DefaultLogger.LogInformation("Kicking {client} because they are banned", ToString());
Kick(loc["WEBFRONT_PENALTY_LIST_BANNED_REASON"], autoKickClient, banPenalty);
return false;
} }
// we want to kick them if any account is tempbanned // we want to kick them if any account is tempbanned
@ -641,9 +644,7 @@ namespace SharedLibraryCore.Database.Models
{ {
Utilities.DefaultLogger.LogInformation("Kicking {client} because their GUID is temporarily banned", Utilities.DefaultLogger.LogInformation("Kicking {client} because their GUID is temporarily banned",
ToString()); ToString());
Kick( Kick(loc["WEBFRONT_PENALTY_LIST_TEMPBANNED_REASON"], autoKickClient, tempbanPenalty);
$"{loc["SERVER_TB_REMAIN"]} ({(tempbanPenalty.Expires.Value - DateTime.UtcNow).HumanizeForCurrentCulture()} {loc["WEBFRONT_PENALTY_TEMPLATE_REMAINING"]})",
autoKickClient);
return false; return false;
} }

View File

@ -14,7 +14,7 @@ using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace SharedLibraryCore namespace SharedLibraryCore
{ {
public abstract class Server public abstract class Server : IGameServer
{ {
public enum Game public enum Game
{ {
@ -205,9 +205,10 @@ namespace SharedLibraryCore
/// <summary> /// <summary>
/// Kick a player from the server /// Kick a player from the server
/// </summary> /// </summary>
/// <param name="Reason">Reason for kicking</param> /// <param name="reason">Reason for kicking</param>
/// <param name="Target">EFClient to kick</param> /// <param name="Target">EFClient to kick</param>
abstract public Task Kick(String Reason, EFClient Target, EFClient Origin); public Task Kick(String reason, EFClient Target, EFClient Origin) => Kick(reason, Target, Origin, null);
public abstract Task Kick(string reason, EFClient target, EFClient origin, EFPenalty originalPenalty);
/// <summary> /// <summary>
/// Temporarily ban a player ( default 1 hour ) from the server /// Temporarily ban a player ( default 1 hour ) from the server