1
0

ChatRepository.cs 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Linq;
  3. using System.Runtime.InteropServices;
  4. using Content.Shared.Chat.V2;
  5. using Content.Shared.Chat.V2.Repository;
  6. using Robust.Server.Player;
  7. using Robust.Shared.Network;
  8. using Robust.Shared.Replays;
  9. namespace Content.Server.Chat.V2.Repository;
  10. /// <summary>
  11. /// Stores <see cref="IChatEvent"/>, gives them UIDs, and issues <see cref="MessageCreatedEvent"/>.
  12. /// Allows for deletion of messages.
  13. /// </summary>
  14. public sealed class ChatRepositorySystem : EntitySystem
  15. {
  16. [Dependency] private readonly IReplayRecordingManager _replay = default!;
  17. [Dependency] private readonly IPlayerManager _player = default!;
  18. // Clocks should start at 1, as 0 indicates "clock not set" or "clock forgotten to be set by bad programmer".
  19. private uint _nextMessageId = 1;
  20. private Dictionary<uint, ChatRecord> _messages = new();
  21. private Dictionary<NetUserId, List<uint>> _playerMessages = new();
  22. public override void Initialize()
  23. {
  24. Refresh();
  25. _replay.RecordingFinished += _ =>
  26. {
  27. // TODO: resolve https://github.com/space-wizards/space-station-14/issues/25485 so we can dump the chat to disc.
  28. Refresh();
  29. };
  30. }
  31. /// <summary>
  32. /// Adds an <see cref="IChatEvent"/> to the repo and raises it with a UID for consumption elsewhere.
  33. /// </summary>
  34. /// <param name="ev">The event to store and raise</param>
  35. /// <returns>If storing and raising succeeded.</returns>
  36. public bool Add(IChatEvent ev)
  37. {
  38. if (!_player.TryGetSessionByEntity(ev.Sender, out var session))
  39. {
  40. return false;
  41. }
  42. var messageId = _nextMessageId;
  43. _nextMessageId++;
  44. ev.Id = messageId;
  45. var storedEv = new ChatRecord
  46. {
  47. UserName = session.Name,
  48. UserId = session.UserId,
  49. EntityName = Name(ev.Sender),
  50. StoredEvent = ev
  51. };
  52. _messages[messageId] = storedEv;
  53. CollectionsMarshal.GetValueRefOrAddDefault(_playerMessages, storedEv.UserId, out _)?.Add(messageId);
  54. RaiseLocalEvent(ev.Sender, new MessageCreatedEvent(ev), true);
  55. return true;
  56. }
  57. /// <summary>
  58. /// Returns the event associated with a UID, if it exists.
  59. /// </summary>
  60. /// <param name="id">The UID of a event.</param>
  61. /// <returns>The event, if it exists.</returns>
  62. public IChatEvent? GetEventFor(uint id)
  63. {
  64. return _messages.TryGetValue(id, out var record) ? record.StoredEvent : null;
  65. }
  66. /// <summary>
  67. /// Edits a specific message and issues a <see cref="MessagePatchedEvent"/> that says this happened both locally and
  68. /// on the network. Note that this doesn't replay the message (yet), so translators and mutators won't act on it.
  69. /// </summary>
  70. /// <param name="id">The ID to edit</param>
  71. /// <param name="message">The new message to send</param>
  72. /// <returns>If patching did anything did anything</returns>
  73. /// <remarks>Should be used for admining and admemeing only.</remarks>
  74. public bool Patch(uint id, string message)
  75. {
  76. if (!_messages.TryGetValue(id, out var ev))
  77. {
  78. return false;
  79. }
  80. ev.StoredEvent.Message = message;
  81. RaiseLocalEvent(new MessagePatchedEvent(id, message));
  82. return true;
  83. }
  84. /// <summary>
  85. /// Deletes a message from the repository and issues a <see cref="MessageDeletedEvent"/> that says this has happened
  86. /// both locally and on the network.
  87. /// </summary>
  88. /// <param name="id">The ID to delete</param>
  89. /// <returns>If deletion did anything</returns>
  90. /// <remarks>Should only be used for adminning</remarks>
  91. public bool Delete(uint id)
  92. {
  93. if (!_messages.TryGetValue(id, out var ev))
  94. {
  95. return false;
  96. }
  97. _messages.Remove(id);
  98. if (_playerMessages.TryGetValue(ev.UserId, out var set))
  99. {
  100. set.Remove(id);
  101. }
  102. RaiseLocalEvent(new MessageDeletedEvent(id));
  103. return true;
  104. }
  105. /// <summary>
  106. /// Nukes a user's entire chat history from the repo and issues a <see cref="MessageDeletedEvent"/> saying this has
  107. /// happened.
  108. /// </summary>
  109. /// <param name="userName">The user ID to nuke.</param>
  110. /// <param name="reason">Why nuking failed, if it did.</param>
  111. /// <returns>If nuking did anything.</returns>
  112. /// <remarks>Note that this could be a <b>very large</b> event, as we send every single event ID over the wire.
  113. /// By necessity we can't leak the player-source of chat messages (or if they even have the same origin) because of
  114. /// client modders who could use that information to cheat/metagrudge/etc >:(</remarks>
  115. public bool NukeForUsername(string userName, [NotNullWhen(false)] out string? reason)
  116. {
  117. if (!_player.TryGetUserId(userName, out var userId))
  118. {
  119. reason = Loc.GetString("command-error-nukechatmessages-usernames-usernamenotexist", ("username", userName));
  120. return false;
  121. }
  122. return NukeForUserId(userId, out reason);
  123. }
  124. /// <summary>
  125. /// Nukes a user's entire chat history from the repo and issues a <see cref="MessageDeletedEvent"/> saying this has
  126. /// happened.
  127. /// </summary>
  128. /// <param name="userId">The user ID to nuke.</param>
  129. /// <param name="reason">Why nuking failed, if it did.</param>
  130. /// <returns>If nuking did anything.</returns>
  131. /// <remarks>Note that this could be a <b>very large</b> event, as we send every single event ID over the wire.
  132. /// By necessity we can't leak the player-source of chat messages (or if they even have the same origin) because of
  133. /// client modders who could use that information to cheat/metagrudge/etc >:(</remarks>
  134. public bool NukeForUserId(NetUserId userId, [NotNullWhen(false)] out string? reason)
  135. {
  136. if (!_playerMessages.TryGetValue(userId, out var dict))
  137. {
  138. reason = Loc.GetString("command-error-nukechatmessages-usernames-usernamenomessages", ("userId", userId.UserId.ToString()));
  139. return false;
  140. }
  141. foreach (var id in dict)
  142. {
  143. _messages.Remove(id);
  144. }
  145. var ev = new MessagesNukedEvent(dict);
  146. CollectionsMarshal.GetValueRefOrAddDefault(_playerMessages, userId, out _)?.Clear();
  147. RaiseLocalEvent(ev);
  148. reason = null;
  149. return true;
  150. }
  151. /// <summary>
  152. /// Dumps held chat storage data and refreshes the repo.
  153. /// </summary>
  154. public void Refresh()
  155. {
  156. _nextMessageId = 1;
  157. _messages.Clear();
  158. _playerMessages.Clear();
  159. }
  160. }