diff --git a/Content.Server/Chat/Commands/SayCommand.cs b/Content.Server/Chat/Commands/SayCommand.cs index f53302fb75..5a31e561d7 100644 --- a/Content.Server/Chat/Commands/SayCommand.cs +++ b/Content.Server/Chat/Commands/SayCommand.cs @@ -36,6 +36,7 @@ namespace Content.Server.Chat.Commands return; var chat = IoCManager.Resolve(); + var chatSanitizer = IoCManager.Resolve(); var playerEntity = player.AttachedEntity; if (playerEntity == null) @@ -62,7 +63,11 @@ namespace Content.Server.Chat.Commands return; } - chat.EntitySay(mindComponent.OwnedEntity, message); + var emote = chatSanitizer.TrySanitizeOutSmilies(message, mindComponent.OwnedEntity, out var sanitized, out var emoteStr); + if (sanitized.Length != 0) + chat.EntitySay(mindComponent.OwnedEntity, sanitized); + if (emote) + chat.EntityMe(mindComponent.OwnedEntity, emoteStr!); } } diff --git a/Content.Server/Chat/Managers/ChatSanitizationManager.cs b/Content.Server/Chat/Managers/ChatSanitizationManager.cs new file mode 100644 index 0000000000..65a4cc04ba --- /dev/null +++ b/Content.Server/Chat/Managers/ChatSanitizationManager.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Content.Shared.CCVar; +using Robust.Shared.Configuration; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; +using Robust.Shared.Localization; + +namespace Content.Server.Chat.Managers; + +public class ChatSanitizationManager : IChatSanitizationManager +{ + [Dependency] private IConfigurationManager _configurationManager = default!; + + private static readonly Dictionary SmileyToEmote = new() + { + // I could've done this with regex, but felt it wasn't the right idea. + { ":)", "chatsan-smiles" }, + { ":]", "chatsan-smiles" }, + { "=)", "chatsan-smiles" }, + { "=]", "chatsan-smiles" }, + { "(:", "chatsan-smiles" }, + { "[:", "chatsan-smiles" }, + { "(=", "chatsan-smiles" }, + { "[=", "chatsan-smiles" }, + { ":(", "chatsan-frowns" }, + { ":[", "chatsan-frowns" }, + { "=(", "chatsan-frowns" }, + { "=[", "chatsan-frowns" }, + { "):", "chatsan-frowns" }, + { ")=", "chatsan-frowns" }, + { "]:", "chatsan-frowns" }, + { "]=", "chatsan-frowns" }, + { ":D", "chatsan-smiles-widely" }, + { "D:", "chatsan-frowns-deeply" }, + { ":O", "chatsan-surprised" }, + { ":3", "chatsan-smiles" }, //nope + { ":S", "chatsan-uncertain" }, + { ":>", "chatsan-grins" }, + { ":<", "chatsan-pouts" }, + { "xD", "chatsan-laughs" }, + { ";-;", "chatsan-cries" }, + { ";_;", "chatsan-cries" }, + { ":u", "chatsan-smiles-smugly" }, + { ":v", "chatsan-smiles-smugly" }, + { ">:i", "chatsan-annoyed" }, + { ":i", "chatsan-sighs" }, + { ":|", "chatsan-sighs" }, + { ":p", "chatsan-stick-out-tongue" }, + { ":b", "chatsan-stick-out-tongue" }, + { "0-0", "chatsan-wide-eyed" }, + { "o-o", "chatsan-wide-eyed" }, + { "o.o", "chatsan-wide-eyed" }, + { "._.", "chatsan-surprised" }, + { ".-.", "chatsan-confused" }, + { "-_-", "chatsan-unimpressed" }, + { "o/", "chatsan-waves" }, + { "^^/", "chatsan-waves" }, + { ":/", "chatsan-uncertain" }, + { ":\\", "chatsan-uncertain" }, + }; + + private bool doSanitize = false; + + public void Initialize() + { + _configurationManager.OnValueChanged(CCVars.ChatSanitizerEnabled, x => doSanitize = x, true); + } + + public bool TrySanitizeOutSmilies(string input, IEntity speaker, out string sanitized, [NotNullWhen(true)] out string? emote) + { + if (!doSanitize) + { + sanitized = input; + emote = null; + return false; + } + + input = input.TrimEnd(); + + foreach (var (smiley, replacement) in SmileyToEmote) + { + if (input.EndsWith(smiley, true, CultureInfo.InvariantCulture)) + { + sanitized = input.Remove(input.Length - smiley.Length).TrimEnd(); + emote = Loc.GetString(replacement, ("ent", speaker)); + return true; + } + } + + sanitized = input; + emote = null; + return false; + } +} diff --git a/Content.Server/Chat/Managers/IChatSanitizationManager.cs b/Content.Server/Chat/Managers/IChatSanitizationManager.cs new file mode 100644 index 0000000000..ebd2a79b03 --- /dev/null +++ b/Content.Server/Chat/Managers/IChatSanitizationManager.cs @@ -0,0 +1,11 @@ +using System.Diagnostics.CodeAnalysis; +using Robust.Shared.GameObjects; + +namespace Content.Server.Chat.Managers; + +public interface IChatSanitizationManager +{ + public void Initialize(); + + public bool TrySanitizeOutSmilies(string input, IEntity speaker, out string sanitized, [NotNullWhen(true)] out string? emote); +} diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index 0628a1e04a..db052cd6e3 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -62,6 +62,7 @@ namespace Content.Server.Entry _euiManager = IoCManager.Resolve(); _voteManager = IoCManager.Resolve(); + IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); var playerManager = IoCManager.Resolve(); diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs index 6152a9d3ae..b44ea2fd57 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -34,6 +34,7 @@ namespace Content.Server.IoC public static void Register() { IoCManager.Register(); + IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 74d69a3451..3986dfbfd4 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -553,6 +553,9 @@ namespace Content.Shared.CCVar public static readonly CVarDef ChatMaxMessageLength = CVarDef.Create("chat.max_message_length", 1000, CVar.SERVER | CVar.REPLICATED); + public static readonly CVarDef ChatSanitizerEnabled = + CVarDef.Create("chat.chat_sanitizer_enabled", true, CVar.SERVERONLY); + /* * AFK */ diff --git a/Resources/Locale/en-US/chat/sanitizer-replacements.ftl b/Resources/Locale/en-US/chat/sanitizer-replacements.ftl new file mode 100644 index 0000000000..0f98a18fd3 --- /dev/null +++ b/Resources/Locale/en-US/chat/sanitizer-replacements.ftl @@ -0,0 +1,19 @@ +chatsan-smiles = smiles +chatsan-frowns = frowns +chatsan-smiles-widely = smiles widely +chatsan-frowns-deeply = frowns deeply +chatsan-surprised = looks surprised +chatsan-uncertain = looks uncertain +chatsan-grins = grins +chatsan-pouts = pouts +chatsan-laughs = laughs +chatsan-cries = cries +chatsan-smiles-smugly = smiles smugly +chatsan-annoyed = looks annoyed +chatsan-sighs = sighs +chatsan-stick-out-tongue = sticks { POSS-ADJ($ent) } tongue out +chatsan-wide-eyed = looks shocked +chatsan-surprised = looks surpised +chatsan-confused = looks confused +chatsan-unimpressed = seems unimpressed +chatsan-waves = waves