diff --git a/Content.Server/Chat/Managers/ChatSanitizationManager.cs b/Content.Server/Chat/Managers/ChatSanitizationManager.cs
index c9566244357..c5ac2d2935b 100644
--- a/Content.Server/Chat/Managers/ChatSanitizationManager.cs
+++ b/Content.Server/Chat/Managers/ChatSanitizationManager.cs
@@ -33,7 +33,7 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
{ ":D", "chatsan-smiles-widely" },
{ "D:", "chatsan-frowns-deeply" },
{ ":O", "chatsan-surprised" },
- { ":3", "chatsan-smiles" }, //nope
+ { ":3", "chatsan-smiles" },
{ ":S", "chatsan-uncertain" },
{ ":>", "chatsan-grins" },
{ ":<", "chatsan-pouts" },
diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs
index 94af12ea201..6b000532bec 100644
--- a/Content.Server/Chat/Systems/ChatSystem.cs
+++ b/Content.Server/Chat/Systems/ChatSystem.cs
@@ -263,6 +263,8 @@ public void TrySendInGameICMessage(
}
}
+ message = FormattedMessage.EscapeText(message);
+
// Otherwise, send whatever type.
switch (desiredType)
{
@@ -408,7 +410,7 @@ private void SendEntitySpeak(
return;
// The original message
- var message = TransformSpeech(source, FormattedMessage.RemoveMarkup(originalMessage), language);
+ var message = TransformSpeech(source, FormattedMessage.RemoveMarkupPermissive(originalMessage), language);
if (message.Length == 0)
return;
@@ -481,7 +483,7 @@ private void SendEntityWhisper(
if (!_actionBlocker.CanSpeak(source) && !ignoreActionBlocker)
return;
- var message = TransformSpeech(source, FormattedMessage.RemoveMarkup(originalMessage), language);
+ var message = TransformSpeech(source, FormattedMessage.RemoveMarkupPermissive(originalMessage), language);
if (message.Length == 0)
return;
@@ -517,7 +519,7 @@ private void SendEntityWhisper(
var canUnderstandLanguage = _language.CanUnderstand(listener, language.ID);
// How the entity perceives the message depends on whether it can understand its language
- var perceivedMessage = FormattedMessage.EscapeText(canUnderstandLanguage ? message : languageObfuscatedMessage);
+ var perceivedMessage = canUnderstandLanguage ? message : languageObfuscatedMessage;
// Result is the intermediate message derived from the perceived one via obfuscation
// Wrapped message is the result wrapped in an "x says y" string
@@ -544,7 +546,7 @@ private void SendEntityWhisper(
_chatManager.ChatMessageToOne(ChatChannel.Whisper, result, wrappedMessage, source, false, session.Channel);
}
- var replayWrap = WrapWhisperMessage(source, "chat-manager-entity-whisper-wrap-message", name, FormattedMessage.EscapeText(message), language);
+ var replayWrap = WrapWhisperMessage(source, "chat-manager-entity-whisper-wrap-message", name, message, language);
_replay.RecordServerMessage(new ChatMessage(ChatChannel.Whisper, message, replayWrap, GetNetEntity(source), null, MessageRangeHideChatForReplay(range)));
var ev = new EntitySpokeEvent(source, message, channel, true, language);
@@ -591,7 +593,7 @@ private void SendEntityEmote(
var wrappedMessage = Loc.GetString("chat-manager-entity-me-wrap-message",
("entityName", name),
("entity", ent),
- ("message", FormattedMessage.RemoveMarkup(action)));
+ ("message", FormattedMessage.RemoveMarkupPermissive(action)));
if (checkEmote)
TryEmoteChatInput(source, action);
@@ -771,8 +773,10 @@ private bool CanSendInGame(string message, IConsoleShell? shell = null, ICommonS
// ReSharper disable once InconsistentNaming
private string SanitizeInGameICMessage(EntityUid source, string message, out string? emoteStr, bool capitalize = true, bool punctuate = false, bool capitalizeTheWordI = true)
{
- var newMessage = message.Trim();
- newMessage = SanitizeMessageReplaceWords(newMessage);
+ var newMessage = SanitizeMessageReplaceWords(message.Trim());
+
+ GetRadioKeycodePrefix(source, newMessage, out newMessage, out var prefix);
+ _sanitizer.TrySanitizeOutSmilies(newMessage, source, out newMessage, out emoteStr);
if (capitalize)
newMessage = SanitizeMessageCapital(newMessage);
@@ -781,9 +785,7 @@ private string SanitizeInGameICMessage(EntityUid source, string message, out str
if (punctuate)
newMessage = SanitizeMessagePeriod(newMessage);
- _sanitizer.TrySanitizeOutSmilies(newMessage, source, out newMessage, out emoteStr);
-
- return newMessage;
+ return prefix + newMessage;
}
private string SanitizeInGameOOCMessage(string message)
diff --git a/Content.Shared/Chat/SharedChatSystem.cs b/Content.Shared/Chat/SharedChatSystem.cs
index ff2a30ccbdb..fe5da426d9d 100644
--- a/Content.Shared/Chat/SharedChatSystem.cs
+++ b/Content.Shared/Chat/SharedChatSystem.cs
@@ -86,6 +86,35 @@ public SpeechVerbPrototype GetSpeechVerb(EntityUid source, string message, Speec
return current ?? _prototypeManager.Index(speech.SpeechVerb);
}
+ ///
+ /// Splits the input message into a radio prefix part and the rest to preserve it during sanitization.
+ ///
+ ///
+ /// This is primarily for the chat emote sanitizer, which can match against ":b" as an emote, which is a valid radio keycode.
+ ///
+ public void GetRadioKeycodePrefix(EntityUid source,
+ string input,
+ out string output,
+ out string prefix)
+ {
+ prefix = string.Empty;
+ output = input;
+
+ // If the string is less than 2, then it's probably supposed to be an emote.
+ // No one is sending empty radio messages!
+ if (input.Length <= 2)
+ return;
+
+ if (!(input.StartsWith(RadioChannelPrefix) || input.StartsWith(RadioChannelAltPrefix)))
+ return;
+
+ if (!_keyCodes.TryGetValue(input[1], out _))
+ return;
+
+ prefix = input[..2];
+ output = input[2..];
+ }
+
///
/// Attempts to resolve radio prefixes in chat messages (e.g., remove a leading ":e" and resolve the requested
/// channel. Returns true if a radio message was attempted, even if the channel is invalid.
diff --git a/Content.Shared/Preferences/HumanoidCharacterProfile.cs b/Content.Shared/Preferences/HumanoidCharacterProfile.cs
index 56684deb19e..ac3c5c8dd11 100644
--- a/Content.Shared/Preferences/HumanoidCharacterProfile.cs
+++ b/Content.Shared/Preferences/HumanoidCharacterProfile.cs
@@ -23,7 +23,7 @@ namespace Content.Shared.Preferences;
[Serializable, NetSerializable]
public sealed partial class HumanoidCharacterProfile : ICharacterProfile
{
- private static readonly Regex RestrictedNameRegex = new("[^A-Z,a-z,0-9, -]");
+ private static readonly Regex RestrictedNameRegex = new(@"[^A-Za-z0-9 '\-]");
private static readonly Regex ICNameCaseRegex = new(@"^(?\w)|\b(?\w)(?=\w*$)");
public const int MaxNameLength = 64;