]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add basic discord client integration with ooc and admin chat support (#33840)
authorSimon <63975668+Simyon264@users.noreply.github.com>
Sat, 24 May 2025 18:49:52 +0000 (20:49 +0200)
committerGitHub <noreply@github.com>
Sat, 24 May 2025 18:49:52 +0000 (20:49 +0200)
* Add basic discord client integration with ooc and admin chat support

* Use username instead of global name

WHY IS GLOBAL NAME NOT JUST THE USERNAME??? WHY ARE THERE NO DOC COMMENTS???

* stuff

* Reviews, methinks.

* Reviews

* reviews

17 files changed:
Content.Server/Chat/Managers/ChatManager.cs
Content.Server/Chat/Managers/IChatManager.cs
Content.Server/Content.Server.csproj
Content.Server/Discord/DiscordLink/DiscordChatLink.cs [new file with mode: 0644]
Content.Server/Discord/DiscordLink/DiscordLink.cs [new file with mode: 0644]
Content.Server/Entry/EntryPoint.cs
Content.Server/IoC/ServerContentIoC.cs
Content.Server/MoMMI/IMoMMILink.cs [deleted file]
Content.Server/MoMMI/MoMMILink.cs [deleted file]
Content.Shared/CCVar/CCVars.Chat.Admin.cs [new file with mode: 0644]
Content.Shared/CCVar/CCVars.Chat.Ooc.cs
Content.Shared/CCVar/CCVars.Discord.cs
Content.Shared/CCVar/CCVars.Status.cs [deleted file]
Content.Shared/Chat/ChatChannel.cs
Directory.Packages.props
Resources/Locale/en-US/chat/chat-channel.ftl [new file with mode: 0644]
Resources/Locale/en-US/chat/managers/chat-manager.ftl

index 535ca8658d22e7ed866ec498f767763f14edd9ad..f8eedd5404283cfd2861716e72b729de47c976bb 100644 (file)
@@ -4,7 +4,7 @@ using System.Runtime.InteropServices;
 using Content.Server.Administration.Logs;
 using Content.Server.Administration.Managers;
 using Content.Server.Administration.Systems;
-using Content.Server.MoMMI;
+using Content.Server.Discord.DiscordLink;
 using Content.Server.Players.RateLimiting;
 using Content.Server.Preferences.Managers;
 using Content.Shared.Administration;
@@ -36,7 +36,6 @@ internal sealed partial class ChatManager : IChatManager
 
     [Dependency] private readonly IReplayRecordingManager _replay = default!;
     [Dependency] private readonly IServerNetManager _netManager = default!;
-    [Dependency] private readonly IMoMMILink _mommiLink = default!;
     [Dependency] private readonly IAdminManager _adminManager = default!;
     [Dependency] private readonly IAdminLogManager _adminLogger = default!;
     [Dependency] private readonly IServerPreferencesManager _preferencesManager = default!;
@@ -45,6 +44,7 @@ internal sealed partial class ChatManager : IChatManager
     [Dependency] private readonly IEntityManager _entityManager = default!;
     [Dependency] private readonly PlayerRateLimitManager _rateLimitManager = default!;
     [Dependency] private readonly ISharedPlayerManager _player = default!;
+    [Dependency] private readonly DiscordChatLink _discordLink = default!;
 
     /// <summary>
     /// The maximum length a player-sent message can be sent
@@ -199,6 +199,13 @@ internal sealed partial class ChatManager : IChatManager
         _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Hook OOC from {sender}: {message}");
     }
 
+    public void SendHookAdmin(string sender, string message)
+    {
+        var wrappedMessage = Loc.GetString("chat-manager-send-hook-admin-wrap-message", ("senderName", sender), ("message", FormattedMessage.EscapeText(message)));
+        ChatMessageToAll(ChatChannel.AdminChat, message, wrappedMessage, source: EntityUid.Invalid, hideChat: false, recordReplay: false);
+        _adminLogger.Add(LogType.Chat, LogImpact.Low, $"Hook admin from {sender}: {message}");
+    }
+
     #endregion
 
     #region Public OOC Chat API
@@ -264,7 +271,7 @@ internal sealed partial class ChatManager : IChatManager
 
         //TODO: player.Name color, this will need to change the structure of the MsgChatMessage
         ChatMessageToAll(ChatChannel.OOC, message, wrappedMessage, EntityUid.Invalid, hideChat: false, recordReplay: true, colorOverride: colorOverride, author: player.UserId);
-        _mommiLink.SendOOCMessage(player.Name, message.Replace("@", "\\@").Replace("<", "\\<").Replace("/", "\\/")); // @ and < are both problematic for discord due to pinging. / is sanitized solely to kneecap links to murder embeds via blunt force
+        _discordLink.SendMessage(message, player.Name, ChatChannel.OOC);
         _adminLogger.Add(LogType.Chat, LogImpact.Low, $"OOC from {player:Player}: {message}");
     }
 
@@ -295,6 +302,7 @@ internal sealed partial class ChatManager : IChatManager
                 author: player.UserId);
         }
 
+        _discordLink.SendMessage(message, player.Name, ChatChannel.AdminChat);
         _adminLogger.Add(LogType.Chat, $"Admin chat from {player:Player}: {message}");
     }
 
index 23211c28fa09bd5e528a932dc9d9b5832122f387..9ac2a27c4ecca88a2119ea2dcf4a744c86dd21e3 100644 (file)
@@ -21,6 +21,7 @@ namespace Content.Server.Chat.Managers
         void TrySendOOCMessage(ICommonSession player, string message, OOCChatType type);
 
         void SendHookOOC(string sender, string message);
+        void SendHookAdmin(string sender, string message);
         void SendAdminAnnouncement(string message, AdminFlags? flagBlacklist = null, AdminFlags? flagWhitelist = null);
         void SendAdminAnnouncementMessage(ICommonSession player, string message, bool suppressLog = true);
 
index 99b834683cefed16dd8c82f7cfd224e8bf16afa3..a0ac12163fc6861c30841bcab644f57fd0425d1c 100644 (file)
@@ -14,6 +14,7 @@
     <ServerGarbageCollection>true</ServerGarbageCollection>
   </PropertyGroup>
   <ItemGroup>
+    <PackageReference Include="Discord.Net" />
     <PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
   </ItemGroup>
   <ItemGroup>
diff --git a/Content.Server/Discord/DiscordLink/DiscordChatLink.cs b/Content.Server/Discord/DiscordLink/DiscordChatLink.cs
new file mode 100644 (file)
index 0000000..5460c9e
--- /dev/null
@@ -0,0 +1,96 @@
+using System.Threading.Tasks;
+using Content.Server.Chat.Managers;
+using Content.Shared.CCVar;
+using Content.Shared.Chat;
+using Discord.WebSocket;
+using Robust.Shared.Asynchronous;
+using Robust.Shared.Configuration;
+
+namespace Content.Server.Discord.DiscordLink;
+
+public sealed class DiscordChatLink
+{
+    [Dependency] private readonly DiscordLink _discordLink = default!;
+    [Dependency] private readonly IConfigurationManager _configurationManager = default!;
+    [Dependency] private readonly IChatManager _chatManager = default!;
+    [Dependency] private readonly ITaskManager _taskManager = default!;
+
+    private ulong? _oocChannelId;
+    private ulong? _adminChannelId;
+
+    public void Initialize()
+    {
+        _discordLink.OnMessageReceived += OnMessageReceived;
+
+        _configurationManager.OnValueChanged(CCVars.OocDiscordChannelId, OnOocChannelIdChanged, true);
+        _configurationManager.OnValueChanged(CCVars.AdminChatDiscordChannelId, OnAdminChannelIdChanged, true);
+    }
+
+    public void Shutdown()
+    {
+        _discordLink.OnMessageReceived -= OnMessageReceived;
+
+        _configurationManager.UnsubValueChanged(CCVars.OocDiscordChannelId, OnOocChannelIdChanged);
+        _configurationManager.UnsubValueChanged(CCVars.AdminChatDiscordChannelId, OnAdminChannelIdChanged);
+    }
+
+    private void OnOocChannelIdChanged(string channelId)
+    {
+        if (string.IsNullOrEmpty(channelId))
+        {
+            _oocChannelId = null;
+            return;
+        }
+
+        _oocChannelId = ulong.Parse(channelId);
+    }
+
+    private void OnAdminChannelIdChanged(string channelId)
+    {
+        if (string.IsNullOrEmpty(channelId))
+        {
+            _adminChannelId = null;
+            return;
+        }
+
+        _adminChannelId = ulong.Parse(channelId);
+    }
+
+    private void OnMessageReceived(SocketMessage message)
+    {
+        if (message.Author.IsBot)
+            return;
+
+        var contents = message.Content.ReplaceLineEndings(" ");
+
+        if (message.Channel.Id == _oocChannelId)
+        {
+            _taskManager.RunOnMainThread(() => _chatManager.SendHookOOC(message.Author.Username, contents));
+        }
+        else if (message.Channel.Id == _adminChannelId)
+        {
+            _taskManager.RunOnMainThread(() => _chatManager.SendHookAdmin(message.Author.Username, contents));
+        }
+    }
+
+    public async Task SendMessage(string message, string author, ChatChannel channel)
+    {
+        var channelId = channel switch
+        {
+            ChatChannel.OOC => _oocChannelId,
+            ChatChannel.AdminChat => _adminChannelId,
+            _ => throw new InvalidOperationException("Channel not linked to Discord."),
+        };
+
+        if (channelId == null)
+        {
+            // Configuration not set up. Ignore.
+            return;
+        }
+
+        // @ and < are both problematic for discord due to pinging. / is sanitized solely to kneecap links to murder embeds via blunt force
+        message = message.Replace("@", "\\@").Replace("<", "\\<").Replace("/", "\\/");
+
+        await _discordLink.SendMessageAsync(channelId.Value, $"**{channel.GetString()}**: `{author}`: {message}");
+    }
+}
diff --git a/Content.Server/Discord/DiscordLink/DiscordLink.cs b/Content.Server/Discord/DiscordLink/DiscordLink.cs
new file mode 100644 (file)
index 0000000..a9172c9
--- /dev/null
@@ -0,0 +1,267 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Content.Shared.CCVar;
+using Discord;
+using Discord.WebSocket;
+using Robust.Shared.Configuration;
+using Robust.Shared.Reflection;
+using Robust.Shared.Utility;
+using LogMessage = Discord.LogMessage;
+
+namespace Content.Server.Discord.DiscordLink;
+
+/// <summary>
+/// Represents the arguments for the <see cref="DiscordLink.OnCommandReceived"/> event.
+/// </summary>
+public sealed class CommandReceivedEventArgs
+{
+    /// <summary>
+    /// The command that was received. This is the first word in the message, after the bot prefix.
+    /// </summary>
+    public string Command { get; init; } = string.Empty;
+
+    /// <summary>
+    /// The arguments to the command. This is everything after the command
+    /// </summary>
+    public string Arguments { get; init; } = string.Empty;
+    /// <summary>
+    /// Information about the message that the command was received from. This includes the message content, author, etc.
+    /// Use this to reply to the message, delete it, etc.
+    /// </summary>
+    public SocketMessage Message { get; init; } = default!;
+}
+
+/// <summary>
+/// Handles the connection to Discord and provides methods to interact with it.
+/// </summary>
+public sealed class DiscordLink : IPostInjectInit
+{
+    [Dependency] private readonly ILogManager _logManager = default!;
+    [Dependency] private readonly IConfigurationManager _configuration = default!;
+
+    /// <summary>
+    ///    The Discord client. This is null if the bot is not connected.
+    /// </summary>
+    /// <remarks>
+    ///     This should not be used directly outside of DiscordLink. So please do not make it public. Use the methods in this class instead.
+    /// </remarks>
+    private DiscordSocketClient? _client;
+    private ISawmill _sawmill = default!;
+    private ISawmill _sawmillLog = default!;
+
+    private ulong _guildId;
+    private string _botToken = string.Empty;
+
+    public string BotPrefix = default!;
+    /// <summary>
+    /// If the bot is currently connected to Discord.
+    /// </summary>
+    public bool IsConnected => _client != null;
+
+    #region Events
+
+    /// <summary>
+    ///     Event that is raised when a command is received from Discord.
+    /// </summary>
+    public event Action<CommandReceivedEventArgs>? OnCommandReceived;
+    /// <summary>
+    ///     Event that is raised when a message is received from Discord. This is raised for every message, including commands.
+    /// </summary>
+    public event Action<SocketMessage>? OnMessageReceived;
+
+    public void RegisterCommandCallback(Action<CommandReceivedEventArgs> callback, string command)
+    {
+        OnCommandReceived += args =>
+        {
+            if (args.Command == command)
+                callback(args);
+        };
+    }
+
+    #endregion
+
+    public void Initialize()
+    {
+        _configuration.OnValueChanged(CCVars.DiscordGuildId, OnGuildIdChanged, true);
+        _configuration.OnValueChanged(CCVars.DiscordPrefix, OnPrefixChanged, true);
+
+        if (_configuration.GetCVar(CCVars.DiscordToken) is not { } token || token == string.Empty)
+        {
+            _sawmill.Info("No Discord token specified, not connecting.");
+            return;
+        }
+
+        // If the Guild ID is empty OR the prefix is empty, we don't want to connect to Discord.
+        if (_guildId == 0 || BotPrefix == string.Empty)
+        {
+            // This is a warning, not info, because it's a configuration error.
+            // It is valid to not have a Discord token set which is why the above check is an info.
+            // But if you have a token set, you should also have a guild ID and prefix set.
+            _sawmill.Warning("No Discord guild ID or prefix specified, not connecting.");
+            return;
+        }
+
+        _client = new DiscordSocketClient(new DiscordSocketConfig()
+        {
+            GatewayIntents = GatewayIntents.All
+        });
+        _client.Log += Log;
+        _client.MessageReceived += OnCommandReceivedInternal;
+        _client.MessageReceived += OnMessageReceivedInternal;
+
+        _botToken = token;
+        // Since you cannot change the token while the server is running / the DiscordLink is initialized,
+        // we can just set the token without updating it every time the cvar changes.
+
+        _client.Ready += () =>
+        {
+            _sawmill.Info("Discord client ready.");
+            return Task.CompletedTask;
+        };
+
+        Task.Run(async () =>
+        {
+            try
+            {
+                await LoginAsync(token);
+            }
+            catch (Exception e)
+            {
+                _sawmill.Error("Failed to connect to Discord!", e);
+            }
+        });
+    }
+
+    public async Task Shutdown()
+    {
+        if (_client != null)
+        {
+            _sawmill.Info("Disconnecting from Discord.");
+
+            // Unsubscribe from the events.
+            _client.MessageReceived -= OnCommandReceivedInternal;
+            _client.MessageReceived -= OnMessageReceivedInternal;
+
+            await _client.LogoutAsync();
+            await _client.StopAsync();
+            await _client.DisposeAsync();
+            _client = null;
+        }
+
+        _configuration.UnsubValueChanged(CCVars.DiscordGuildId, OnGuildIdChanged);
+        _configuration.UnsubValueChanged(CCVars.DiscordPrefix, OnPrefixChanged);
+    }
+
+    void IPostInjectInit.PostInject()
+    {
+        _sawmill = _logManager.GetSawmill("discord.link");
+        _sawmillLog = _logManager.GetSawmill("discord.link.log");
+    }
+
+    private void OnGuildIdChanged(string guildId)
+    {
+        _guildId = ulong.TryParse(guildId, out var id) ? id : 0;
+    }
+
+    private void OnPrefixChanged(string prefix)
+    {
+        BotPrefix = prefix;
+    }
+
+    private async Task LoginAsync(string token)
+    {
+        DebugTools.Assert(_client != null);
+        DebugTools.Assert(_client.LoginState == LoginState.LoggedOut);
+
+        await _client.LoginAsync(TokenType.Bot, token);
+        await _client.StartAsync();
+
+
+        _sawmill.Info("Connected to Discord.");
+    }
+
+    private string FormatLog(LogMessage msg)
+    {
+        return msg.Exception is null
+            ? $"{msg.Source}: {msg.Message}"
+            : $"{msg.Source}: {msg.Message}\n{msg.Exception}";
+    }
+
+    private Task Log(LogMessage msg)
+    {
+        var logLevel = msg.Severity switch
+        {
+            LogSeverity.Critical => LogLevel.Fatal,
+            LogSeverity.Error => LogLevel.Error,
+            LogSeverity.Warning => LogLevel.Warning,
+            _ => LogLevel.Debug
+        };
+
+        _sawmillLog.Log(logLevel, FormatLog(msg));
+        return Task.CompletedTask;
+    }
+
+    private Task OnCommandReceivedInternal(SocketMessage message)
+    {
+        var content = message.Content;
+        // If the message doesn't start with the bot prefix, ignore it.
+        if (!content.StartsWith(BotPrefix))
+            return Task.CompletedTask;
+
+        // Split the message into the command and the arguments.
+        var trimmedInput = content[BotPrefix.Length..].Trim();
+        var firstSpaceIndex = trimmedInput.IndexOf(' ');
+
+        string command, arguments;
+
+        if (firstSpaceIndex == -1)
+        {
+            command = trimmedInput;
+            arguments = string.Empty;
+        }
+        else
+        {
+            command = trimmedInput[..firstSpaceIndex];
+            arguments = trimmedInput[(firstSpaceIndex + 1)..].Trim();
+        }
+
+        // Raise the event!
+        OnCommandReceived?.Invoke(new CommandReceivedEventArgs
+        {
+            Command = command,
+            Arguments = arguments,
+            Message = message,
+        });
+        return Task.CompletedTask;
+    }
+
+    private Task OnMessageReceivedInternal(SocketMessage message)
+    {
+        OnMessageReceived?.Invoke(message);
+        return Task.CompletedTask;
+    }
+
+    #region Proxy methods
+
+    /// <summary>
+    /// Sends a message to a Discord channel with the specified ID. Without any mentions.
+    /// </summary>
+    public async Task SendMessageAsync(ulong channelId, string message)
+    {
+        if (_client == null)
+        {
+            return;
+        }
+
+        var channel = _client.GetChannel(channelId) as IMessageChannel;
+        if (channel == null)
+        {
+            _sawmill.Error("Tried to send a message to Discord but the channel {Channel} was not found.", channel);
+            return;
+        }
+
+        await channel.SendMessageAsync(message, allowedMentions: AllowedMentions.None);
+    }
+
+    #endregion
+}
index f6d99ea687d5070684971daf55841ab5a3b77651..df4af14f1e38e9c73fda1bee86a71bc5a3814d1a 100644 (file)
@@ -6,6 +6,7 @@ using Content.Server.Afk;
 using Content.Server.Chat.Managers;
 using Content.Server.Connection;
 using Content.Server.Database;
+using Content.Server.Discord.DiscordLink;
 using Content.Server.EUI;
 using Content.Server.GameTicking;
 using Content.Server.GhostKick;
@@ -146,6 +147,10 @@ namespace Content.Server.Entry
                 IoCManager.Resolve<IAdminManager>().Initialize();
                 IoCManager.Resolve<IAfkManager>().Initialize();
                 IoCManager.Resolve<RulesManager>().Initialize();
+
+                IoCManager.Resolve<DiscordLink>().Initialize();
+                IoCManager.Resolve<DiscordChatLink>().Initialize();
+
                 _euiManager.Initialize();
 
                 IoCManager.Resolve<IGameMapManager>().Initialize();
@@ -184,6 +189,9 @@ namespace Content.Server.Entry
             _playTimeTracking?.Shutdown();
             _dbManager?.Shutdown();
             IoCManager.Resolve<ServerApi>().Shutdown();
+
+            IoCManager.Resolve<DiscordLink>().Shutdown();
+            IoCManager.Resolve<DiscordChatLink>().Shutdown();
         }
 
         private static void LoadConfigPresets(IConfigurationManager cfg, IResourceManager res, ISawmill sawmill)
index fb3ba193b8cbc8c3805b8d793c764faa9af5b874..b4d999bef41e11d19cce254b9648a47b2d1fcede 100644 (file)
@@ -7,13 +7,13 @@ using Content.Server.Chat.Managers;
 using Content.Server.Connection;
 using Content.Server.Database;
 using Content.Server.Discord;
+using Content.Server.Discord.DiscordLink;
 using Content.Server.Discord.WebhookMessages;
 using Content.Server.EUI;
 using Content.Server.GhostKick;
 using Content.Server.Info;
 using Content.Server.Mapping;
 using Content.Server.Maps;
-using Content.Server.MoMMI;
 using Content.Server.NodeContainer.NodeGroups;
 using Content.Server.Players.JobWhitelist;
 using Content.Server.Players.PlayTimeTracking;
@@ -39,7 +39,6 @@ namespace Content.Server.IoC
             IoCManager.Register<IChatManager, ChatManager>();
             IoCManager.Register<ISharedChatManager, ChatManager>();
             IoCManager.Register<IChatSanitizationManager, ChatSanitizationManager>();
-            IoCManager.Register<IMoMMILink, MoMMILink>();
             IoCManager.Register<IServerPreferencesManager, ServerPreferencesManager>();
             IoCManager.Register<IServerDbManager, ServerDbManager>();
             IoCManager.Register<RecipeManager, RecipeManager>();
@@ -77,6 +76,9 @@ namespace Content.Server.IoC
             IoCManager.Register<ConnectionManager>();
             IoCManager.Register<MultiServerKickManager>();
             IoCManager.Register<CVarControlManager>();
+
+            IoCManager.Register<DiscordLink>();
+            IoCManager.Register<DiscordChatLink>();
         }
     }
 }
diff --git a/Content.Server/MoMMI/IMoMMILink.cs b/Content.Server/MoMMI/IMoMMILink.cs
deleted file mode 100644 (file)
index b33a406..0000000
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Content.Server.MoMMI
-{
-    public interface IMoMMILink
-    {
-        void SendOOCMessage(string sender, string message);
-    }
-}
diff --git a/Content.Server/MoMMI/MoMMILink.cs b/Content.Server/MoMMI/MoMMILink.cs
deleted file mode 100644 (file)
index eff4d3e..0000000
+++ /dev/null
@@ -1,151 +0,0 @@
-using System.Net;
-using System.Net.Http;
-using System.Net.Http.Json;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using System.Threading.Tasks;
-using Content.Server.Chat.Managers;
-using Content.Shared.CCVar;
-using Robust.Server.ServerStatus;
-using Robust.Shared.Asynchronous;
-using Robust.Shared.Configuration;
-
-namespace Content.Server.MoMMI
-{
-    internal sealed class MoMMILink : IMoMMILink, IPostInjectInit
-    {
-        [Dependency] private readonly IConfigurationManager _configurationManager = default!;
-        [Dependency] private readonly IStatusHost _statusHost = default!;
-        [Dependency] private readonly IChatManager _chatManager = default!;
-        [Dependency] private readonly ITaskManager _taskManager = default!;
-
-        private readonly HttpClient _httpClient = new();
-
-        void IPostInjectInit.PostInject()
-        {
-            _statusHost.AddHandler(HandleChatPost);
-        }
-
-        public async void SendOOCMessage(string sender, string message)
-        {
-            var sentMessage = new MoMMIMessageOOC
-            {
-                Sender = sender,
-                Contents = message
-            };
-
-            await SendMessageInternal("ooc", sentMessage);
-        }
-
-        private async Task SendMessageInternal(string type, object messageObject)
-        {
-            var url = _configurationManager.GetCVar(CCVars.StatusMoMMIUrl);
-            var password = _configurationManager.GetCVar(CCVars.StatusMoMMIPassword);
-            if (string.IsNullOrWhiteSpace(url))
-            {
-                return;
-            }
-
-            if (string.IsNullOrWhiteSpace(password))
-            {
-                Logger.WarningS("mommi", "MoMMI URL specified but not password!");
-                return;
-            }
-
-            var sentMessage = new MoMMIMessageBase
-            {
-                Password = password,
-                Type = type,
-                Contents = messageObject
-            };
-
-            var request = await _httpClient.PostAsJsonAsync(url, sentMessage);
-
-            if (!request.IsSuccessStatusCode)
-            {
-                throw new Exception($"MoMMI returned bad status code: {request.StatusCode}");
-            }
-        }
-
-        private async Task<bool> HandleChatPost(IStatusHandlerContext context)
-        {
-            if (context.RequestMethod != HttpMethod.Post || context.Url.AbsolutePath != "/ooc")
-            {
-                return false;
-            }
-
-            var password = _configurationManager.GetCVar(CCVars.StatusMoMMIPassword);
-
-            if (string.IsNullOrEmpty(password))
-            {
-                await context.RespondErrorAsync(HttpStatusCode.Forbidden);
-                return true;
-            }
-
-            OOCPostMessage? message = null;
-            try
-            {
-                message = await context.RequestBodyJsonAsync<OOCPostMessage>();
-            }
-            catch (JsonException)
-            {
-                // message null so enters block down below.
-            }
-
-            if (message == null)
-            {
-                await context.RespondErrorAsync(HttpStatusCode.BadRequest);
-                return true;
-            }
-
-            if (message.Password != password)
-            {
-                await context.RespondErrorAsync(HttpStatusCode.Forbidden);
-                return true;
-            }
-
-            var sender = message.Sender;
-            var contents = message.Contents.ReplaceLineEndings(" ");
-
-            _taskManager.RunOnMainThread(() => _chatManager.SendHookOOC(sender, contents));
-
-            await context.RespondAsync("Success", HttpStatusCode.OK);
-            return true;
-        }
-
-        private sealed class MoMMIMessageBase
-        {
-            [JsonInclude] [JsonPropertyName("password")]
-            public string Password = null!;
-
-            [JsonInclude] [JsonPropertyName("type")]
-            public string Type = null!;
-
-            [JsonInclude] [JsonPropertyName("contents")]
-            public object Contents = null!;
-        }
-
-        private sealed class MoMMIMessageOOC
-        {
-            [JsonInclude] [JsonPropertyName("sender")]
-            public string Sender = null!;
-
-            [JsonInclude] [JsonPropertyName("contents")]
-            public string Contents = null!;
-        }
-
-        private sealed class OOCPostMessage
-        {
-#pragma warning disable CS0649
-            [JsonInclude] [JsonPropertyName("password")]
-            public string Password = null!;
-
-            [JsonInclude] [JsonPropertyName("sender")]
-            public string Sender = null!;
-
-            [JsonInclude] [JsonPropertyName("contents")]
-            public string Contents = null!;
-#pragma warning restore CS0649
-        }
-    }
-}
diff --git a/Content.Shared/CCVar/CCVars.Chat.Admin.cs b/Content.Shared/CCVar/CCVars.Chat.Admin.cs
new file mode 100644 (file)
index 0000000..808d2c9
--- /dev/null
@@ -0,0 +1,12 @@
+using Robust.Shared.Configuration;
+
+namespace Content.Shared.CCVar;
+
+public sealed partial class CCVars
+{
+    /// <summary>
+    ///     The discord channel ID to send admin chat messages to (also receive them). This requires the Discord Integration to be enabled and configured.
+    /// </summary>
+    public static readonly CVarDef<string> AdminChatDiscordChannelId =
+        CVarDef.Create("admin.chat_discord_channel_id", string.Empty, CVar.SERVERONLY);
+}
index ba5e41053b6d851b2952870d4114332cda3538f4..e4617fc470b45d7789ed75c6896efd118ee02791 100644 (file)
@@ -24,4 +24,10 @@ public sealed partial class CCVars
 
     public static readonly CVarDef<bool> ShowOocPatronColor =
         CVarDef.Create("ooc.show_ooc_patron_color", true, CVar.ARCHIVE | CVar.REPLICATED | CVar.CLIENT);
+
+    /// <summary>
+    ///     The discord channel ID to send OOC messages to (also recieve them). This requires the Discord Integration to be enabled and configured.
+    /// </summary>
+    public static readonly CVarDef<string> OocDiscordChannelId =
+        CVarDef.Create("ooc.discord_channel_id", string.Empty, CVar.SERVERONLY);
 }
index 4be8680d2c7a0af9fcfecb332518320a1e2ea431..86eb423d2f772363e91492506cdeb0fd50f1303e 100644 (file)
@@ -1,5 +1,4 @@
-using Robust.Shared.Configuration;
-using Robust.Shared.Maths;
+using Robust.Shared.Configuration;
 
 namespace Content.Shared.CCVar;
 
@@ -60,6 +59,28 @@ public sealed partial class CCVars
     public static readonly CVarDef<string> DiscordRoundEndRoleWebhook =
         CVarDef.Create("discord.round_end_role", string.Empty, CVar.SERVERONLY);
 
+
+    /// <summary>
+    ///     The token used to authenticate with Discord. For the Bot to function set: discord.token, discord.guild_id, and discord.prefix.
+    ///     If this is empty, the bot will not connect.
+    /// </summary>
+    public static readonly CVarDef<string> DiscordToken =
+        CVarDef.Create("discord.token", string.Empty, CVar.SERVERONLY | CVar.CONFIDENTIAL);
+
+    /// <summary>
+    ///     The Discord guild ID to use for commands as well as for several other features.
+    ///     If this is empty, the bot will not connect.
+    /// </summary>
+    public static readonly CVarDef<string> DiscordGuildId =
+        CVarDef.Create("discord.guild_id", string.Empty, CVar.SERVERONLY);
+
+    /// <summary>
+    ///     Prefix used for commands for the Discord bot.
+    ///     If this is empty, the bot will not connect.
+    /// </summary>
+    public static readonly CVarDef<string> DiscordPrefix =
+        CVarDef.Create("discord.prefix", "!", CVar.SERVERONLY);
+
     /// <summary>
     ///     URL of the Discord webhook which will relay watchlist connection notifications. If left empty, disables the webhook.
     /// </summary>
diff --git a/Content.Shared/CCVar/CCVars.Status.cs b/Content.Shared/CCVar/CCVars.Status.cs
deleted file mode 100644 (file)
index 007f678..0000000
+++ /dev/null
@@ -1,13 +0,0 @@
-using Robust.Shared.Configuration;
-
-namespace Content.Shared.CCVar;
-
-public sealed partial class CCVars
-{
-    public static readonly CVarDef<string> StatusMoMMIUrl =
-        CVarDef.Create("status.mommiurl", "", CVar.SERVERONLY);
-
-    public static readonly CVarDef<string> StatusMoMMIPassword =
-        CVarDef.Create("status.mommipassword", "", CVar.SERVERONLY | CVar.CONFIDENTIAL);
-
-}
index e8715a6ecb04d522c2e11396d9d1468727d74c98..88218c0bfada0e19c5b9f6cf2fea086c54a97142 100644 (file)
@@ -92,4 +92,24 @@ namespace Content.Shared.Chat
 
         AdminRelated = Admin | AdminAlert | AdminChat,
     }
+
+    /// <summary>
+    /// Contains extension methods for <see cref="ChatChannel"/>
+    /// </summary>
+    public static class ChatChannelExt
+    {
+        /// <summary>
+        /// Gets a string representation of a chat channel.
+        /// </summary>
+        /// <exception cref="ArgumentOutOfRangeException">Thrown when this channel does not have a string representation set.</exception>
+        public static string GetString(this ChatChannel channel)
+        {
+            return channel switch
+            {
+                ChatChannel.OOC => Loc.GetString("chat-channel-humanized-ooc"),
+                ChatChannel.Admin => Loc.GetString("chat-channel-humanized-admin"),
+                _ => throw new ArgumentOutOfRangeException(nameof(channel), channel, null)
+            };
+        }
+    }
 }
index 69b5ad1945d65bf6b1805f0faadb16193d5529bd..d18f894bf760165258eebada5c5efb33493639fb 100644 (file)
@@ -7,6 +7,7 @@
     <PackageVersion Remove="Npgsql.EntityFrameworkCore.PostgreSQL" />
     <PackageVersion Remove="Microsoft.EntityFrameworkCore.Design" />
     <PackageVersion Include="CsvHelper" Version="33.0.1" />
+    <PackageVersion Include="Discord.Net" Version="3.16.0" />
     <PackageVersion Include="ImGui.NET" Version="1.87.3" />
     <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.1">
       <PrivateAssets>all</PrivateAssets>
@@ -18,4 +19,4 @@
     <PackageVersion Include="Veldrid" Version="4.8.0" />
     <PackageVersion Include="Veldrid.SPIRV" Version="1.0.15" />
   </ItemGroup>
-</Project>
\ No newline at end of file
+</Project>
diff --git a/Resources/Locale/en-US/chat/chat-channel.ftl b/Resources/Locale/en-US/chat/chat-channel.ftl
new file mode 100644 (file)
index 0000000..10ab1fc
--- /dev/null
@@ -0,0 +1,2 @@
+chat-channel-humanized-ooc = OOC
+chat-channel-humanized-admin = ADMIN
index 704d96cc15f117fc228012d0b566aeeea668167d..c20852468bfc46d61e8909ca140c0007c21786ad 100644 (file)
@@ -44,6 +44,7 @@ chat-manager-send-admin-chat-wrap-message = {$adminChannelName}: [bold]{$playerN
 chat-manager-send-admin-announcement-wrap-message = [bold]{$adminChannelName}: {$message}[/bold]
 
 chat-manager-send-hook-ooc-wrap-message = OOC: [bold](D){$senderName}:[/bold] {$message}
+chat-manager-send-hook-admin-wrap-message = ADMIN: [bold](D){$senderName}:[/bold] {$message}
 
 chat-manager-dead-channel-name = DEAD
 chat-manager-admin-channel-name = ADMIN