using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.InteropServices; using Content.Shared.Chat.V2; using Content.Shared.Chat.V2.Repository; using Robust.Server.Player; using Robust.Shared.Network; using Robust.Shared.Replays; namespace Content.Server.Chat.V2.Repository; /// /// Stores , gives them UIDs, and issues . /// Allows for deletion of messages. /// public sealed class ChatRepositorySystem : EntitySystem { [Dependency] private readonly IReplayRecordingManager _replay = default!; [Dependency] private readonly IPlayerManager _player = default!; // Clocks should start at 1, as 0 indicates "clock not set" or "clock forgotten to be set by bad programmer". private uint _nextMessageId = 1; private Dictionary _messages = new(); private Dictionary> _playerMessages = new(); public override void Initialize() { Refresh(); _replay.RecordingFinished += _ => { // TODO: resolve https://github.com/space-wizards/space-station-14/issues/25485 so we can dump the chat to disc. Refresh(); }; } /// /// Adds an to the repo and raises it with a UID for consumption elsewhere. /// /// The event to store and raise /// If storing and raising succeeded. public bool Add(IChatEvent ev) { if (!_player.TryGetSessionByEntity(ev.Sender, out var session)) { return false; } var messageId = _nextMessageId; _nextMessageId++; ev.Id = messageId; var storedEv = new ChatRecord { UserName = session.Name, UserId = session.UserId, EntityName = Name(ev.Sender), StoredEvent = ev }; _messages[messageId] = storedEv; CollectionsMarshal.GetValueRefOrAddDefault(_playerMessages, storedEv.UserId, out _)?.Add(messageId); RaiseLocalEvent(ev.Sender, new MessageCreatedEvent(ev), true); return true; } /// /// Returns the event associated with a UID, if it exists. /// /// The UID of a event. /// The event, if it exists. public IChatEvent? GetEventFor(uint id) { return _messages.TryGetValue(id, out var record) ? record.StoredEvent : null; } /// /// Edits a specific message and issues a that says this happened both locally and /// on the network. Note that this doesn't replay the message (yet), so translators and mutators won't act on it. /// /// The ID to edit /// The new message to send /// If patching did anything did anything /// Should be used for admining and admemeing only. public bool Patch(uint id, string message) { if (!_messages.TryGetValue(id, out var ev)) { return false; } ev.StoredEvent.Message = message; RaiseLocalEvent(new MessagePatchedEvent(id, message)); return true; } /// /// Deletes a message from the repository and issues a that says this has happened /// both locally and on the network. /// /// The ID to delete /// If deletion did anything /// Should only be used for adminning public bool Delete(uint id) { if (!_messages.TryGetValue(id, out var ev)) { return false; } _messages.Remove(id); if (_playerMessages.TryGetValue(ev.UserId, out var set)) { set.Remove(id); } RaiseLocalEvent(new MessageDeletedEvent(id)); return true; } /// /// Nukes a user's entire chat history from the repo and issues a saying this has /// happened. /// /// The user ID to nuke. /// Why nuking failed, if it did. /// If nuking did anything. /// Note that this could be a very large event, as we send every single event ID over the wire. /// By necessity we can't leak the player-source of chat messages (or if they even have the same origin) because of /// client modders who could use that information to cheat/metagrudge/etc >:( public bool NukeForUsername(string userName, [NotNullWhen(false)] out string? reason) { if (!_player.TryGetUserId(userName, out var userId)) { reason = Loc.GetString("command-error-nukechatmessages-usernames-usernamenotexist", ("username", userName)); return false; } return NukeForUserId(userId, out reason); } /// /// Nukes a user's entire chat history from the repo and issues a saying this has /// happened. /// /// The user ID to nuke. /// Why nuking failed, if it did. /// If nuking did anything. /// Note that this could be a very large event, as we send every single event ID over the wire. /// By necessity we can't leak the player-source of chat messages (or if they even have the same origin) because of /// client modders who could use that information to cheat/metagrudge/etc >:( public bool NukeForUserId(NetUserId userId, [NotNullWhen(false)] out string? reason) { if (!_playerMessages.TryGetValue(userId, out var dict)) { reason = Loc.GetString("command-error-nukechatmessages-usernames-usernamenomessages", ("userId", userId.UserId.ToString())); return false; } foreach (var id in dict) { _messages.Remove(id); } var ev = new MessagesNukedEvent(dict); CollectionsMarshal.GetValueRefOrAddDefault(_playerMessages, userId, out _)?.Clear(); RaiseLocalEvent(ev); reason = null; return true; } /// /// Dumps held chat storage data and refreshes the repo. /// public void Refresh() { _nextMessageId = 1; _messages.Clear(); _playerMessages.Clear(); } }