ChatSanitizationManager.cs 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Text.RegularExpressions;
  3. using Content.Shared.CCVar;
  4. using Robust.Shared.Configuration;
  5. namespace Content.Server.Chat.Managers;
  6. /// <summary>
  7. /// Sanitizes messages!
  8. /// It currently ony removes the shorthands for emotes (like "lol" or "^-^") from a chat message and returns the last
  9. /// emote in their message
  10. /// </summary>
  11. public sealed class ChatSanitizationManager : IChatSanitizationManager
  12. {
  13. private static readonly Dictionary<string, string> ShorthandToEmote = new()
  14. {
  15. { ":)", "chatsan-smiles" },
  16. { ":]", "chatsan-smiles" },
  17. { "=)", "chatsan-smiles" },
  18. { "=]", "chatsan-smiles" },
  19. { "(:", "chatsan-smiles" },
  20. { "[:", "chatsan-smiles" },
  21. { "(=", "chatsan-smiles" },
  22. { "[=", "chatsan-smiles" },
  23. { "^^", "chatsan-smiles" },
  24. { "^-^", "chatsan-smiles" },
  25. { ":(", "chatsan-frowns" },
  26. { ":[", "chatsan-frowns" },
  27. { "=(", "chatsan-frowns" },
  28. { "=[", "chatsan-frowns" },
  29. { "):", "chatsan-frowns" },
  30. { ")=", "chatsan-frowns" },
  31. { "]:", "chatsan-frowns" },
  32. { "]=", "chatsan-frowns" },
  33. { ":D", "chatsan-smiles-widely" },
  34. { "D:", "chatsan-frowns-deeply" },
  35. { ":O", "chatsan-surprised" },
  36. { ":3", "chatsan-smiles" },
  37. { ":S", "chatsan-uncertain" },
  38. { ":>", "chatsan-grins" },
  39. { ":<", "chatsan-pouts" },
  40. { "xD", "chatsan-laughs" },
  41. { ":'(", "chatsan-cries" },
  42. { ":'[", "chatsan-cries" },
  43. { "='(", "chatsan-cries" },
  44. { "='[", "chatsan-cries" },
  45. { ")':", "chatsan-cries" },
  46. { "]':", "chatsan-cries" },
  47. { ")'=", "chatsan-cries" },
  48. { "]'=", "chatsan-cries" },
  49. { ";-;", "chatsan-cries" },
  50. { ";_;", "chatsan-cries" },
  51. { "qwq", "chatsan-cries" },
  52. { ":u", "chatsan-smiles-smugly" },
  53. { ":v", "chatsan-smiles-smugly" },
  54. { ">:i", "chatsan-annoyed" },
  55. { ":i", "chatsan-sighs" },
  56. { ":|", "chatsan-sighs" },
  57. { ":p", "chatsan-stick-out-tongue" },
  58. { ";p", "chatsan-stick-out-tongue" },
  59. { ":b", "chatsan-stick-out-tongue" },
  60. { "0-0", "chatsan-wide-eyed" },
  61. { "o-o", "chatsan-wide-eyed" },
  62. { "o.o", "chatsan-wide-eyed" },
  63. { "._.", "chatsan-surprised" },
  64. { ".-.", "chatsan-confused" },
  65. { "-_-", "chatsan-unimpressed" },
  66. { "smh", "chatsan-unimpressed" },
  67. { "o/", "chatsan-waves" },
  68. { "^^/", "chatsan-waves" },
  69. { ":/", "chatsan-uncertain" },
  70. { ":\\", "chatsan-uncertain" },
  71. { "lmao", "chatsan-laughs" },
  72. { "lmfao", "chatsan-laughs" },
  73. { "lol", "chatsan-laughs" },
  74. { "lel", "chatsan-laughs" },
  75. { "kek", "chatsan-laughs" },
  76. { "rofl", "chatsan-laughs" },
  77. { "o7", "chatsan-salutes" },
  78. { ";_;7", "chatsan-tearfully-salutes" },
  79. { "idk", "chatsan-shrugs" },
  80. { ";)", "chatsan-winks" },
  81. { ";]", "chatsan-winks" },
  82. { "(;", "chatsan-winks" },
  83. { "[;", "chatsan-winks" },
  84. { ":')", "chatsan-tearfully-smiles" },
  85. { ":']", "chatsan-tearfully-smiles" },
  86. { "=')", "chatsan-tearfully-smiles" },
  87. { "=']", "chatsan-tearfully-smiles" },
  88. { "(':", "chatsan-tearfully-smiles" },
  89. { "[':", "chatsan-tearfully-smiles" },
  90. { "('=", "chatsan-tearfully-smiles" },
  91. { "['=", "chatsan-tearfully-smiles" }
  92. };
  93. [Dependency] private readonly IConfigurationManager _configurationManager = default!;
  94. [Dependency] private readonly ILocalizationManager _loc = default!;
  95. private bool _doSanitize;
  96. public void Initialize()
  97. {
  98. _configurationManager.OnValueChanged(CCVars.ChatSanitizerEnabled, x => _doSanitize = x, true);
  99. }
  100. /// <summary>
  101. /// Remove the shorthands from the message, returning the last one found as the emote
  102. /// </summary>
  103. /// <param name="message">The pre-sanitized message</param>
  104. /// <param name="speaker">The speaker</param>
  105. /// <param name="sanitized">The sanitized message with shorthands removed</param>
  106. /// <param name="emote">The localized emote</param>
  107. /// <returns>True if emote has been sanitized out</returns>
  108. public bool TrySanitizeEmoteShorthands(string message,
  109. EntityUid speaker,
  110. out string sanitized,
  111. [NotNullWhen(true)] out string? emote)
  112. {
  113. emote = null;
  114. sanitized = message;
  115. if (!_doSanitize)
  116. return false;
  117. // -1 is just a canary for nothing found yet
  118. var lastEmoteIndex = -1;
  119. foreach (var (shorthand, emoteKey) in ShorthandToEmote)
  120. {
  121. // We have to escape it because shorthands like ":)" or "-_-" would break the regex otherwise.
  122. var escaped = Regex.Escape(shorthand);
  123. // So there are 2 cases:
  124. // - If there is whitespace before it and after it is either punctuation, whitespace, or the end of the line
  125. // Delete the word and the whitespace before
  126. // - If it is at the start of the string and is followed by punctuation, whitespace, or the end of the line
  127. // Delete the word and the punctuation if it exists.
  128. var pattern =
  129. $@"\s{escaped}(?=\p{{P}}|\s|$)|^{escaped}(?:\p{{P}}|(?=\s|$))";
  130. var r = new Regex(pattern, RegexOptions.RightToLeft | RegexOptions.IgnoreCase);
  131. // We're using sanitized as the original message until the end so that we can make sure the indices of
  132. // the emotes are accurate.
  133. var lastMatch = r.Match(sanitized);
  134. if (!lastMatch.Success)
  135. continue;
  136. if (lastMatch.Index > lastEmoteIndex)
  137. {
  138. lastEmoteIndex = lastMatch.Index;
  139. emote = _loc.GetString(emoteKey, ("ent", speaker));
  140. }
  141. message = r.Replace(message, string.Empty);
  142. }
  143. sanitized = message.Trim();
  144. return emote is not null;
  145. }
  146. }