From 7867cbec00e5157696ebd9f09b283c539e9a0551 Mon Sep 17 00:00:00 2001 From: Simon <63975668+Simyon264@users.noreply.github.com> Date: Sat, 24 May 2025 20:49:52 +0200 Subject: [PATCH] Add basic discord client integration with ooc and admin chat support (#33840) * 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 --- Content.Server/Chat/Managers/ChatManager.cs | 14 +- Content.Server/Chat/Managers/IChatManager.cs | 1 + Content.Server/Content.Server.csproj | 1 + .../Discord/DiscordLink/DiscordChatLink.cs | 96 +++++++ .../Discord/DiscordLink/DiscordLink.cs | 267 ++++++++++++++++++ Content.Server/Entry/EntryPoint.cs | 8 + Content.Server/IoC/ServerContentIoC.cs | 6 +- Content.Server/MoMMI/IMoMMILink.cs | 7 - Content.Server/MoMMI/MoMMILink.cs | 151 ---------- Content.Shared/CCVar/CCVars.Chat.Admin.cs | 12 + Content.Shared/CCVar/CCVars.Chat.Ooc.cs | 6 + Content.Shared/CCVar/CCVars.Discord.cs | 25 +- Content.Shared/CCVar/CCVars.Status.cs | 13 - Content.Shared/Chat/ChatChannel.cs | 20 ++ Directory.Packages.props | 3 +- Resources/Locale/en-US/chat/chat-channel.ftl | 2 + .../en-US/chat/managers/chat-manager.ftl | 1 + 17 files changed, 454 insertions(+), 179 deletions(-) create mode 100644 Content.Server/Discord/DiscordLink/DiscordChatLink.cs create mode 100644 Content.Server/Discord/DiscordLink/DiscordLink.cs delete mode 100644 Content.Server/MoMMI/IMoMMILink.cs delete mode 100644 Content.Server/MoMMI/MoMMILink.cs create mode 100644 Content.Shared/CCVar/CCVars.Chat.Admin.cs delete mode 100644 Content.Shared/CCVar/CCVars.Status.cs create mode 100644 Resources/Locale/en-US/chat/chat-channel.ftl diff --git a/Content.Server/Chat/Managers/ChatManager.cs b/Content.Server/Chat/Managers/ChatManager.cs index 535ca8658d..f8eedd5404 100644 --- a/Content.Server/Chat/Managers/ChatManager.cs +++ b/Content.Server/Chat/Managers/ChatManager.cs @@ -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!; /// /// 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}"); } diff --git a/Content.Server/Chat/Managers/IChatManager.cs b/Content.Server/Chat/Managers/IChatManager.cs index 23211c28fa..9ac2a27c4e 100644 --- a/Content.Server/Chat/Managers/IChatManager.cs +++ b/Content.Server/Chat/Managers/IChatManager.cs @@ -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); diff --git a/Content.Server/Content.Server.csproj b/Content.Server/Content.Server.csproj index 99b834683c..a0ac12163f 100644 --- a/Content.Server/Content.Server.csproj +++ b/Content.Server/Content.Server.csproj @@ -14,6 +14,7 @@ true + diff --git a/Content.Server/Discord/DiscordLink/DiscordChatLink.cs b/Content.Server/Discord/DiscordLink/DiscordChatLink.cs new file mode 100644 index 0000000000..5460c9e274 --- /dev/null +++ b/Content.Server/Discord/DiscordLink/DiscordChatLink.cs @@ -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 index 0000000000..a9172c98f7 --- /dev/null +++ b/Content.Server/Discord/DiscordLink/DiscordLink.cs @@ -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; + +/// +/// Represents the arguments for the event. +/// +public sealed class CommandReceivedEventArgs +{ + /// + /// The command that was received. This is the first word in the message, after the bot prefix. + /// + public string Command { get; init; } = string.Empty; + + /// + /// The arguments to the command. This is everything after the command + /// + public string Arguments { get; init; } = string.Empty; + /// + /// 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. + /// + public SocketMessage Message { get; init; } = default!; +} + +/// +/// Handles the connection to Discord and provides methods to interact with it. +/// +public sealed class DiscordLink : IPostInjectInit +{ + [Dependency] private readonly ILogManager _logManager = default!; + [Dependency] private readonly IConfigurationManager _configuration = default!; + + /// + /// The Discord client. This is null if the bot is not connected. + /// + /// + /// This should not be used directly outside of DiscordLink. So please do not make it public. Use the methods in this class instead. + /// + private DiscordSocketClient? _client; + private ISawmill _sawmill = default!; + private ISawmill _sawmillLog = default!; + + private ulong _guildId; + private string _botToken = string.Empty; + + public string BotPrefix = default!; + /// + /// If the bot is currently connected to Discord. + /// + public bool IsConnected => _client != null; + + #region Events + + /// + /// Event that is raised when a command is received from Discord. + /// + public event Action? OnCommandReceived; + /// + /// Event that is raised when a message is received from Discord. This is raised for every message, including commands. + /// + public event Action? OnMessageReceived; + + public void RegisterCommandCallback(Action 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 + + /// + /// Sends a message to a Discord channel with the specified ID. Without any mentions. + /// + 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 +} diff --git a/Content.Server/Entry/EntryPoint.cs b/Content.Server/Entry/EntryPoint.cs index f6d99ea687..df4af14f1e 100644 --- a/Content.Server/Entry/EntryPoint.cs +++ b/Content.Server/Entry/EntryPoint.cs @@ -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().Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); + + IoCManager.Resolve().Initialize(); + IoCManager.Resolve().Initialize(); + _euiManager.Initialize(); IoCManager.Resolve().Initialize(); @@ -184,6 +189,9 @@ namespace Content.Server.Entry _playTimeTracking?.Shutdown(); _dbManager?.Shutdown(); IoCManager.Resolve().Shutdown(); + + IoCManager.Resolve().Shutdown(); + IoCManager.Resolve().Shutdown(); } private static void LoadConfigPresets(IConfigurationManager cfg, IResourceManager res, ISawmill sawmill) diff --git a/Content.Server/IoC/ServerContentIoC.cs b/Content.Server/IoC/ServerContentIoC.cs index fb3ba193b8..b4d999bef4 100644 --- a/Content.Server/IoC/ServerContentIoC.cs +++ b/Content.Server/IoC/ServerContentIoC.cs @@ -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(); IoCManager.Register(); IoCManager.Register(); - IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); @@ -77,6 +76,9 @@ namespace Content.Server.IoC IoCManager.Register(); IoCManager.Register(); IoCManager.Register(); + + IoCManager.Register(); + IoCManager.Register(); } } } diff --git a/Content.Server/MoMMI/IMoMMILink.cs b/Content.Server/MoMMI/IMoMMILink.cs deleted file mode 100644 index b33a406770..0000000000 --- a/Content.Server/MoMMI/IMoMMILink.cs +++ /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 index eff4d3e3f7..0000000000 --- a/Content.Server/MoMMI/MoMMILink.cs +++ /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 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(); - } - 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 index 0000000000..808d2c91d8 --- /dev/null +++ b/Content.Shared/CCVar/CCVars.Chat.Admin.cs @@ -0,0 +1,12 @@ +using Robust.Shared.Configuration; + +namespace Content.Shared.CCVar; + +public sealed partial class CCVars +{ + /// + /// The discord channel ID to send admin chat messages to (also receive them). This requires the Discord Integration to be enabled and configured. + /// + public static readonly CVarDef AdminChatDiscordChannelId = + CVarDef.Create("admin.chat_discord_channel_id", string.Empty, CVar.SERVERONLY); +} diff --git a/Content.Shared/CCVar/CCVars.Chat.Ooc.cs b/Content.Shared/CCVar/CCVars.Chat.Ooc.cs index ba5e41053b..e4617fc470 100644 --- a/Content.Shared/CCVar/CCVars.Chat.Ooc.cs +++ b/Content.Shared/CCVar/CCVars.Chat.Ooc.cs @@ -24,4 +24,10 @@ public sealed partial class CCVars public static readonly CVarDef ShowOocPatronColor = CVarDef.Create("ooc.show_ooc_patron_color", true, CVar.ARCHIVE | CVar.REPLICATED | CVar.CLIENT); + + /// + /// The discord channel ID to send OOC messages to (also recieve them). This requires the Discord Integration to be enabled and configured. + /// + public static readonly CVarDef OocDiscordChannelId = + CVarDef.Create("ooc.discord_channel_id", string.Empty, CVar.SERVERONLY); } diff --git a/Content.Shared/CCVar/CCVars.Discord.cs b/Content.Shared/CCVar/CCVars.Discord.cs index 4be8680d2c..86eb423d2f 100644 --- a/Content.Shared/CCVar/CCVars.Discord.cs +++ b/Content.Shared/CCVar/CCVars.Discord.cs @@ -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 DiscordRoundEndRoleWebhook = CVarDef.Create("discord.round_end_role", string.Empty, CVar.SERVERONLY); + + /// + /// 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. + /// + public static readonly CVarDef DiscordToken = + CVarDef.Create("discord.token", string.Empty, CVar.SERVERONLY | CVar.CONFIDENTIAL); + + /// + /// The Discord guild ID to use for commands as well as for several other features. + /// If this is empty, the bot will not connect. + /// + public static readonly CVarDef DiscordGuildId = + CVarDef.Create("discord.guild_id", string.Empty, CVar.SERVERONLY); + + /// + /// Prefix used for commands for the Discord bot. + /// If this is empty, the bot will not connect. + /// + public static readonly CVarDef DiscordPrefix = + CVarDef.Create("discord.prefix", "!", CVar.SERVERONLY); + /// /// URL of the Discord webhook which will relay watchlist connection notifications. If left empty, disables the webhook. /// diff --git a/Content.Shared/CCVar/CCVars.Status.cs b/Content.Shared/CCVar/CCVars.Status.cs deleted file mode 100644 index 007f678106..0000000000 --- a/Content.Shared/CCVar/CCVars.Status.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Robust.Shared.Configuration; - -namespace Content.Shared.CCVar; - -public sealed partial class CCVars -{ - public static readonly CVarDef StatusMoMMIUrl = - CVarDef.Create("status.mommiurl", "", CVar.SERVERONLY); - - public static readonly CVarDef StatusMoMMIPassword = - CVarDef.Create("status.mommipassword", "", CVar.SERVERONLY | CVar.CONFIDENTIAL); - -} diff --git a/Content.Shared/Chat/ChatChannel.cs b/Content.Shared/Chat/ChatChannel.cs index e8715a6ecb..88218c0bfa 100644 --- a/Content.Shared/Chat/ChatChannel.cs +++ b/Content.Shared/Chat/ChatChannel.cs @@ -92,4 +92,24 @@ namespace Content.Shared.Chat AdminRelated = Admin | AdminAlert | AdminChat, } + + /// + /// Contains extension methods for + /// + public static class ChatChannelExt + { + /// + /// Gets a string representation of a chat channel. + /// + /// Thrown when this channel does not have a string representation set. + 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) + }; + } + } } diff --git a/Directory.Packages.props b/Directory.Packages.props index 69b5ad1945..d18f894bf7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,6 +7,7 @@ + all @@ -18,4 +19,4 @@ - \ No newline at end of file + diff --git a/Resources/Locale/en-US/chat/chat-channel.ftl b/Resources/Locale/en-US/chat/chat-channel.ftl new file mode 100644 index 0000000000..10ab1fc756 --- /dev/null +++ b/Resources/Locale/en-US/chat/chat-channel.ftl @@ -0,0 +1,2 @@ +chat-channel-humanized-ooc = OOC +chat-channel-humanized-admin = ADMIN diff --git a/Resources/Locale/en-US/chat/managers/chat-manager.ftl b/Resources/Locale/en-US/chat/managers/chat-manager.ftl index 704d96cc15..c20852468b 100644 --- a/Resources/Locale/en-US/chat/managers/chat-manager.ftl +++ b/Resources/Locale/en-US/chat/managers/chat-manager.ftl @@ -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 -- 2.51.2