diff --git a/Application/Factories/GameServerInstanceFactory.cs b/Application/Factories/GameServerInstanceFactory.cs
index 9e91d54b7..8cadbc73b 100644
--- a/Application/Factories/GameServerInstanceFactory.cs
+++ b/Application/Factories/GameServerInstanceFactory.cs
@@ -1,4 +1,5 @@
using System;
+using Microsoft.Extensions.DependencyInjection;
using SharedLibraryCore;
using SharedLibraryCore.Configuration;
using SharedLibraryCore.Interfaces;
@@ -36,7 +37,7 @@ namespace IW4MAdmin.Application.Factories
///
public Server CreateServer(ServerConfiguration config, IManager manager)
{
- return new IW4MServer(config, _translationLookup, _metaService, _serviceProvider);
+ return new IW4MServer(config, _translationLookup, _metaService, _serviceProvider, _serviceProvider.GetRequiredService());
}
}
}
diff --git a/Application/IW4MServer.cs b/Application/IW4MServer.cs
index 75a1331fb..7357d0e2c 100644
--- a/Application/IW4MServer.cs
+++ b/Application/IW4MServer.cs
@@ -32,13 +32,15 @@ namespace IW4MAdmin
private int lastGameTime = 0;
public int Id { get; private set; }
- private readonly IServiceProvider _serviceProvider;
+ private readonly IServiceProvider _serviceProvider;
+ private readonly IClientNoticeMessageFormatter _messageFormatter;
public IW4MServer(
ServerConfiguration serverConfiguration,
ITranslationLookup lookup,
IMetaService metaService,
- IServiceProvider serviceProvider) : base(serviceProvider.GetRequiredService>(),
+ IServiceProvider serviceProvider,
+ IClientNoticeMessageFormatter messageFormatter) : base(serviceProvider.GetRequiredService>(),
serviceProvider.GetRequiredService(),
serverConfiguration,
serviceProvider.GetRequiredService(),
@@ -48,6 +50,7 @@ namespace IW4MAdmin
_translationLookup = lookup;
_metaService = metaService;
_serviceProvider = serviceProvider;
+ _messageFormatter = messageFormatter;
}
public override async Task OnClientConnected(EFClient clientFromLog)
@@ -454,7 +457,6 @@ namespace IW4MAdmin
else if (E.Type == GameEvent.EventType.TempBan)
{
await TempBan(E.Data, (TimeSpan) E.Extra, E.Target, E.ImpersonationOrigin ?? E.Origin);
- ;
}
else if (E.Type == GameEvent.EventType.Ban)
@@ -470,7 +472,7 @@ namespace IW4MAdmin
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)
@@ -1015,6 +1017,12 @@ namespace IW4MAdmin
{
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();
@@ -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 ?
Manager.GetActiveClients()
@@ -1202,7 +1210,7 @@ namespace IW4MAdmin
Type = EFPenalty.PenaltyType.Kick,
Expires = DateTime.UtcNow,
Offender = targetClient,
- Offense = Reason,
+ Offense = reason,
Punisher = originClient,
Link = targetClient.AliasLink
};
@@ -1221,8 +1229,11 @@ namespace IW4MAdmin
Manager.AddEvent(e);
- // todo: move to translation sheet
- string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_KICK_TEXT"]} - ^5{Reason}^7");
+ var formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick,
+ targetClient.ClientNumber,
+ _messageFormatter.BuildFormattedMessage(RconParser.Configuration,
+ newPenalty,
+ previousPenalty));
await targetClient.CurrentServer.ExecuteCommandAsync(formattedKick);
}
}
@@ -1250,14 +1261,15 @@ namespace IW4MAdmin
if (targetClient.IsIngame)
{
- // todo: move to translation sheet
- string formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"^7{loc["SERVER_TB_TEXT"]}- ^5{Reason}");
+ var formattedKick = string.Format(RconParser.Configuration.CommandPrefixes.Kick,
+ targetClient.ClientNumber,
+ _messageFormatter.BuildFormattedMessage(RconParser.Configuration, newPenalty));
ServerLogger.LogDebug("Executing tempban kick command for {targetClient}", targetClient.ToString());
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
targetClient = targetClient.ClientNumber < 0 ?
@@ -1283,8 +1295,9 @@ namespace IW4MAdmin
if (targetClient.IsIngame)
{
ServerLogger.LogDebug("Attempting to kicking newly banned client {targetClient}", targetClient.ToString());
- // todo: move to translation sheet
- string formattedString = string.Format(RconParser.Configuration.CommandPrefixes.Kick, targetClient.ClientNumber, $"{loc["SERVER_BAN_TEXT"]} - ^5{reason} ^7{loc["SERVER_BAN_APPEAL"].FormatExt(Website)}^7");
+ var formattedString = string.Format(RconParser.Configuration.CommandPrefixes.Kick,
+ targetClient.ClientNumber,
+ _messageFormatter.BuildFormattedMessage(RconParser.Configuration, newPenalty));
await targetClient.CurrentServer.ExecuteCommandAsync(formattedString);
}
}
diff --git a/Application/Main.cs b/Application/Main.cs
index 4d922277b..6b7ecab89 100644
--- a/Application/Main.cs
+++ b/Application/Main.cs
@@ -350,6 +350,7 @@ namespace IW4MAdmin.Application
.AddSingleton()
.AddSingleton()
.AddSingleton()
+ .AddSingleton()
.AddSingleton(translationLookup);
if (args.Contains("serialevents"))
diff --git a/Application/Misc/ClientNoticeMessageFormatter.cs b/Application/Misc/ClientNoticeMessageFormatter.cs
new file mode 100644
index 000000000..e0d9302cc
--- /dev/null
+++ b/Application/Misc/ClientNoticeMessageFormatter.cs
@@ -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
+{
+ ///
+ /// implementation of IClientNoticeMessageFormatter
+ ///
+ 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 SplitOverMaxLength(string source, int maxCharactersPerLine)
+ {
+ if (source.Length <= maxCharactersPerLine)
+ {
+ return new[] {source};
+ }
+
+ var segments = new List();
+ 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;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Application/RconParsers/DynamicRConParserConfiguration.cs b/Application/RconParsers/DynamicRConParserConfiguration.cs
index 61295f923..c57589092 100644
--- a/Application/RconParsers/DynamicRConParserConfiguration.cs
+++ b/Application/RconParsers/DynamicRConParserConfiguration.cs
@@ -22,6 +22,8 @@ namespace IW4MAdmin.Application.RconParsers
public NumberStyles GuidNumberStyle { get; set; } = NumberStyles.HexNumber;
public IDictionary OverrideDvarNameMapping { get; set; } = new Dictionary();
public IDictionary DefaultDvarValues { get; set; } = new Dictionary();
+ public int NoticeMaximumLines { get; set; } = 8;
+ public int NoticeMaxCharactersPerLine { get; set; } = 50;
public DynamicRConParserConfiguration(IParserRegexFactory parserRegexFactory)
{
diff --git a/SharedLibraryCore/Configuration/ApplicationConfiguration.cs b/SharedLibraryCore/Configuration/ApplicationConfiguration.cs
index 4764366f7..134eb677c 100644
--- a/SharedLibraryCore/Configuration/ApplicationConfiguration.cs
+++ b/SharedLibraryCore/Configuration/ApplicationConfiguration.cs
@@ -47,6 +47,8 @@ namespace SharedLibraryCore.Configuration
public string SocialLinkAddress { get; set; }
[LocalizedDisplayName("SETUP_SOCIAL_TITLE")]
public string SocialLinkTitle { get; set; }
+ [LocalizedDisplayName("SETUP_CONTACT_URI")]
+ public string ContactUri { get; set; }
[LocalizedDisplayName("SETUP_USE_CUSTOMENCODING")]
[ConfigurationLinked("CustomParserEncoding")]
diff --git a/SharedLibraryCore/Interfaces/IClientNoticeMessageFormatter.cs b/SharedLibraryCore/Interfaces/IClientNoticeMessageFormatter.cs
new file mode 100644
index 000000000..503daf897
--- /dev/null
+++ b/SharedLibraryCore/Interfaces/IClientNoticeMessageFormatter.cs
@@ -0,0 +1,16 @@
+using SharedLibraryCore.Database.Models;
+
+namespace SharedLibraryCore.Interfaces
+{
+ public interface IClientNoticeMessageFormatter
+ {
+ ///
+ /// builds a game formatted notice message
+ ///
+ /// current penalty the message is for
+ /// previous penalty the current penalty relates to
+ /// RCon parser config
+ ///
+ string BuildFormattedMessage(IRConParserConfiguration config, EFPenalty currentPenalty, EFPenalty originalPenalty = null);
+ }
+}
\ No newline at end of file
diff --git a/SharedLibraryCore/Interfaces/IGameServer.cs b/SharedLibraryCore/Interfaces/IGameServer.cs
new file mode 100644
index 000000000..614b0b46d
--- /dev/null
+++ b/SharedLibraryCore/Interfaces/IGameServer.cs
@@ -0,0 +1,18 @@
+using System.Threading.Tasks;
+using SharedLibraryCore.Database.Models;
+
+namespace SharedLibraryCore.Interfaces
+{
+ public interface IGameServer
+ {
+ ///
+ /// kicks target on behalf of origin for given reason
+ ///
+ /// reason client is being kicked
+ /// client to kick
+ /// source of kick action
+ /// previous penalty the kick is occuring for (if applicable)
+ ///
+ public Task Kick(string reason, EFClient target, EFClient origin, EFPenalty previousPenalty = null);
+ }
+}
\ No newline at end of file
diff --git a/SharedLibraryCore/Interfaces/IRConParserConfiguration.cs b/SharedLibraryCore/Interfaces/IRConParserConfiguration.cs
index aee38e284..819aa9561 100644
--- a/SharedLibraryCore/Interfaces/IRConParserConfiguration.cs
+++ b/SharedLibraryCore/Interfaces/IRConParserConfiguration.cs
@@ -62,5 +62,9 @@ namespace SharedLibraryCore.Interfaces
/// specifies the default dvar values for games that don't support certain dvars
///
IDictionary DefaultDvarValues { get; set; }
+
+ int NoticeMaximumLines { get; set; }
+
+ int NoticeMaxCharactersPerLine { get; set; }
}
}
diff --git a/SharedLibraryCore/PartialEntities/EFClient.cs b/SharedLibraryCore/PartialEntities/EFClient.cs
index 2782ed38e..df2bc1cb1 100644
--- a/SharedLibraryCore/PartialEntities/EFClient.cs
+++ b/SharedLibraryCore/PartialEntities/EFClient.cs
@@ -320,7 +320,15 @@ namespace SharedLibraryCore.Database.Models
///
/// reason to kick for
/// client performing the kick
- public GameEvent Kick(string kickReason, EFClient sender)
+ public GameEvent Kick(string kickReason, EFClient sender) => Kick(kickReason, sender, null);
+
+ ///
+ /// kick a client for the given reason
+ ///
+ /// reason to kick for
+ /// client performing the kick
+ /// original client penalty
+ public GameEvent Kick(string kickReason, EFClient sender, EFPenalty originalPenalty)
{
var e = new GameEvent()
{
@@ -329,6 +337,7 @@ namespace SharedLibraryCore.Database.Models
Target = this,
Origin = sender,
Data = kickReason,
+ Extra = originalPenalty,
Owner = sender.CurrentServer
};
@@ -597,7 +606,6 @@ namespace SharedLibraryCore.Database.Models
{
var loc = Utilities.CurrentLocalization.LocalizationIndex;
var autoKickClient = Utilities.IW4MAdminClient(CurrentServer);
-
bool isAbleToConnectSimple = IsAbleToConnectSimple();
if (!isAbleToConnectSimple)
@@ -617,23 +625,18 @@ namespace SharedLibraryCore.Database.Models
// we want to kick them if any account is banned
if (banPenalty != null)
{
- 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
+ if (Level != Permission.Banned)
{
Utilities.DefaultLogger.LogInformation(
"Client {client} is banned, but using a new GUID, we we're updating their level and kicking them",
ToString());
await SetLevel(Permission.Banned, autoKickClient).WaitAsync(Utilities.DefaultCommandTimeout,
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
@@ -641,9 +644,7 @@ namespace SharedLibraryCore.Database.Models
{
Utilities.DefaultLogger.LogInformation("Kicking {client} because their GUID is temporarily banned",
ToString());
- Kick(
- $"{loc["SERVER_TB_REMAIN"]} ({(tempbanPenalty.Expires.Value - DateTime.UtcNow).HumanizeForCurrentCulture()} {loc["WEBFRONT_PENALTY_TEMPLATE_REMAINING"]})",
- autoKickClient);
+ Kick(loc["WEBFRONT_PENALTY_LIST_TEMPBANNED_REASON"], autoKickClient, tempbanPenalty);
return false;
}
diff --git a/SharedLibraryCore/Server.cs b/SharedLibraryCore/Server.cs
index 63c7ae4a9..d129d289a 100644
--- a/SharedLibraryCore/Server.cs
+++ b/SharedLibraryCore/Server.cs
@@ -14,7 +14,7 @@ using ILogger = Microsoft.Extensions.Logging.ILogger;
namespace SharedLibraryCore
{
- public abstract class Server
+ public abstract class Server : IGameServer
{
public enum Game
{
@@ -205,9 +205,10 @@ namespace SharedLibraryCore
///
/// Kick a player from the server
///
- /// Reason for kicking
+ /// Reason for kicking
/// EFClient to kick
- 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);
///
/// Temporarily ban a player ( default 1 hour ) from the server