From 9bab47ea3248e170abd7dd6e24bc39d1030b89bc Mon Sep 17 00:00:00 2001 From: Simon <63975668+Simyon264@users.noreply.github.com> Date: Tue, 17 Jun 2025 19:03:24 +0200 Subject: [PATCH] Switch Discord integration to use NetCord instead of Discord.Net (#38400) --- Content.Packaging/ServerPackaging.cs | 2 +- Content.Server/Content.Server.csproj | 2 +- .../Discord/DiscordLink/DiscordChatLink.cs | 11 +-- .../Discord/DiscordLink/DiscordLink.cs | 96 +++++++------------ .../DiscordLink/DiscordSawmillLogger.cs | 34 +++++++ Directory.Packages.props | 4 +- 6 files changed, 75 insertions(+), 74 deletions(-) create mode 100644 Content.Server/Discord/DiscordLink/DiscordSawmillLogger.cs diff --git a/Content.Packaging/ServerPackaging.cs b/Content.Packaging/ServerPackaging.cs index 947a12601c..91ebc41226 100644 --- a/Content.Packaging/ServerPackaging.cs +++ b/Content.Packaging/ServerPackaging.cs @@ -47,7 +47,7 @@ public static class ServerPackaging // Python script had Npgsql. though we want Npgsql.dll as well soooo "Npgsql", "Microsoft", - "Discord", + "NetCord", }; private static readonly List ServerNotExtraAssemblies = new() diff --git a/Content.Server/Content.Server.csproj b/Content.Server/Content.Server.csproj index a0ac12163f..ffc4df7ed1 100644 --- a/Content.Server/Content.Server.csproj +++ b/Content.Server/Content.Server.csproj @@ -14,8 +14,8 @@ true - + diff --git a/Content.Server/Discord/DiscordLink/DiscordChatLink.cs b/Content.Server/Discord/DiscordLink/DiscordChatLink.cs index 5a2c064e4d..358bc4ab3e 100644 --- a/Content.Server/Discord/DiscordLink/DiscordChatLink.cs +++ b/Content.Server/Discord/DiscordLink/DiscordChatLink.cs @@ -1,8 +1,7 @@ -using System.Threading.Tasks; -using Content.Server.Chat.Managers; +using Content.Server.Chat.Managers; using Content.Shared.CCVar; using Content.Shared.Chat; -using Discord.WebSocket; +using NetCord.Gateway; using Robust.Shared.Asynchronous; using Robust.Shared.Configuration; @@ -59,18 +58,18 @@ public sealed class DiscordChatLink : IPostInjectInit _adminChannelId = ulong.Parse(channelId); } - private void OnMessageReceived(SocketMessage message) + private void OnMessageReceived(Message message) { if (message.Author.IsBot) return; var contents = message.Content.ReplaceLineEndings(" "); - if (message.Channel.Id == _oocChannelId) + if (message.ChannelId == _oocChannelId) { _taskManager.RunOnMainThread(() => _chatManager.SendHookOOC(message.Author.Username, contents)); } - else if (message.Channel.Id == _adminChannelId) + else if (message.ChannelId == _adminChannelId) { _taskManager.RunOnMainThread(() => _chatManager.SendHookAdmin(message.Author.Username, contents)); } diff --git a/Content.Server/Discord/DiscordLink/DiscordLink.cs b/Content.Server/Discord/DiscordLink/DiscordLink.cs index abf3189a99..cbfe12f180 100644 --- a/Content.Server/Discord/DiscordLink/DiscordLink.cs +++ b/Content.Server/Discord/DiscordLink/DiscordLink.cs @@ -1,12 +1,9 @@ -using System.Linq; -using System.Threading.Tasks; +using System.Threading.Tasks; using Content.Shared.CCVar; -using Discord; -using Discord.WebSocket; +using NetCord; +using NetCord.Gateway; +using NetCord.Rest; using Robust.Shared.Configuration; -using Robust.Shared.Reflection; -using Robust.Shared.Utility; -using LogMessage = Discord.LogMessage; namespace Content.Server.Discord.DiscordLink; @@ -28,7 +25,7 @@ public sealed class CommandReceivedEventArgs /// 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!; + public Message Message { get; init; } = default!; } /// @@ -45,7 +42,7 @@ public sealed class DiscordLink : IPostInjectInit /// /// 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 GatewayClient? _client; private ISawmill _sawmill = default!; private ISawmill _sawmillLog = default!; @@ -67,7 +64,7 @@ public sealed class DiscordLink : IPostInjectInit /// /// Event that is raised when a message is received from Discord. This is raised for every message, including commands. /// - public event Action? OnMessageReceived; + public event Action? OnMessageReceived; public void RegisterCommandCallback(Action callback, string command) { @@ -101,33 +98,34 @@ public sealed class DiscordLink : IPostInjectInit return; } - _client = new DiscordSocketClient(new DiscordSocketConfig() + _client = new GatewayClient(new BotToken(token), new GatewayClientConfiguration() { - GatewayIntents = GatewayIntents.Guilds - | GatewayIntents.GuildMembers + Intents = GatewayIntents.Guilds + | GatewayIntents.GuildUsers | GatewayIntents.GuildMessages | GatewayIntents.MessageContent | GatewayIntents.DirectMessages, + Logger = new DiscordSawmillLogger(_sawmillLog), }); - _client.Log += Log; - _client.MessageReceived += OnCommandReceivedInternal; - _client.MessageReceived += OnMessageReceivedInternal; + _client.MessageCreate += OnCommandReceivedInternal; + _client.MessageCreate += 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 += () => + _client.Ready += _ => { _sawmill.Info("Discord client ready."); - return Task.CompletedTask; + return default; }; Task.Run(async () => { try { - await LoginAsync(token); + await _client.StartAsync(); + _sawmill.Info("Connected to Discord."); } catch (Exception e) { @@ -143,12 +141,11 @@ public sealed class DiscordLink : IPostInjectInit _sawmill.Info("Disconnecting from Discord."); // Unsubscribe from the events. - _client.MessageReceived -= OnCommandReceivedInternal; - _client.MessageReceived -= OnMessageReceivedInternal; + _client.MessageCreate -= OnCommandReceivedInternal; + _client.MessageCreate -= OnMessageReceivedInternal; - await _client.LogoutAsync(); - await _client.StopAsync(); - await _client.DisposeAsync(); + await _client.CloseAsync(); + _client.Dispose(); _client = null; } @@ -172,45 +169,12 @@ public sealed class DiscordLink : IPostInjectInit 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) + private ValueTask OnCommandReceivedInternal(Message message) { var content = message.Content; // If the message doesn't start with the bot prefix, ignore it. if (!content.StartsWith(BotPrefix)) - return Task.CompletedTask; + return ValueTask.CompletedTask; // Split the message into the command and the arguments. var trimmedInput = content[BotPrefix.Length..].Trim(); @@ -236,13 +200,13 @@ public sealed class DiscordLink : IPostInjectInit Arguments = arguments, Message = message, }); - return Task.CompletedTask; + return ValueTask.CompletedTask; } - private Task OnMessageReceivedInternal(SocketMessage message) + private ValueTask OnMessageReceivedInternal(Message message) { OnMessageReceived?.Invoke(message); - return Task.CompletedTask; + return ValueTask.CompletedTask; } #region Proxy methods @@ -257,14 +221,18 @@ public sealed class DiscordLink : IPostInjectInit return; } - var channel = _client.GetChannel(channelId) as IMessageChannel; + var channel = await _client.Rest.GetChannelAsync(channelId) as TextChannel; 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); + await channel.SendMessageAsync(new MessageProperties() + { + AllowedMentions = AllowedMentionsProperties.None, + Content = message, + }); } #endregion diff --git a/Content.Server/Discord/DiscordLink/DiscordSawmillLogger.cs b/Content.Server/Discord/DiscordLink/DiscordSawmillLogger.cs new file mode 100644 index 0000000000..c6ca6acd0e --- /dev/null +++ b/Content.Server/Discord/DiscordLink/DiscordSawmillLogger.cs @@ -0,0 +1,34 @@ +using NetCord.Logging; +using NLogLevel = NetCord.Logging.LogLevel; +using LogLevel = Robust.Shared.Log.LogLevel; + +namespace Content.Server.Discord.DiscordLink; + +public sealed class DiscordSawmillLogger(ISawmill sawmill) : IGatewayLogger, IRestLogger, IVoiceLogger +{ + private static LogLevel GetLogLevel(NLogLevel logLevel) + { + return logLevel switch + { + NLogLevel.Critical => LogLevel.Fatal, + NLogLevel.Error => LogLevel.Error, + NLogLevel.Warning => LogLevel.Warning, + _ => LogLevel.Debug, + }; + } + + void IGatewayLogger.Log(NetCord.Logging.LogLevel logLevel, TState state, Exception? exception, Func formatter) + { + sawmill.Log(GetLogLevel(logLevel), exception, formatter(state, exception)); + } + + void IRestLogger.Log(NetCord.Logging.LogLevel logLevel, TState state, Exception? exception, Func formatter) + { + sawmill.Log(GetLogLevel(logLevel), exception, formatter(state, exception)); + } + + void IVoiceLogger.Log(NetCord.Logging.LogLevel logLevel, TState state, Exception? exception, Func formatter) + { + sawmill.Log(GetLogLevel(logLevel), exception, formatter(state, exception)); + } +} diff --git a/Directory.Packages.props b/Directory.Packages.props index d18f894bf7..b2f8d0d844 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,16 +7,16 @@ - all runtime; build; native; contentfiles; analyzers; buildtransitive + - + \ No newline at end of file