Switch Discord integration to use NetCord instead of Discord.Net (#38400)

This commit is contained in:
Simon
2025-06-17 19:03:24 +02:00
committed by GitHub
parent 22e6aef2c5
commit 9bab47ea32
6 changed files with 75 additions and 74 deletions

View File

@@ -47,7 +47,7 @@ public static class ServerPackaging
// Python script had Npgsql. though we want Npgsql.dll as well soooo // Python script had Npgsql. though we want Npgsql.dll as well soooo
"Npgsql", "Npgsql",
"Microsoft", "Microsoft",
"Discord", "NetCord",
}; };
private static readonly List<string> ServerNotExtraAssemblies = new() private static readonly List<string> ServerNotExtraAssemblies = new()

View File

@@ -14,8 +14,8 @@
<ServerGarbageCollection>true</ServerGarbageCollection> <ServerGarbageCollection>true</ServerGarbageCollection>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Discord.Net" />
<PackageReference Include="JetBrains.Annotations" PrivateAssets="All" /> <PackageReference Include="JetBrains.Annotations" PrivateAssets="All" />
<PackageReference Include="NetCord" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Content.Packaging\Content.Packaging.csproj" /> <ProjectReference Include="..\Content.Packaging\Content.Packaging.csproj" />

View File

@@ -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.CCVar;
using Content.Shared.Chat; using Content.Shared.Chat;
using Discord.WebSocket; using NetCord.Gateway;
using Robust.Shared.Asynchronous; using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
@@ -59,18 +58,18 @@ public sealed class DiscordChatLink : IPostInjectInit
_adminChannelId = ulong.Parse(channelId); _adminChannelId = ulong.Parse(channelId);
} }
private void OnMessageReceived(SocketMessage message) private void OnMessageReceived(Message message)
{ {
if (message.Author.IsBot) if (message.Author.IsBot)
return; return;
var contents = message.Content.ReplaceLineEndings(" "); var contents = message.Content.ReplaceLineEndings(" ");
if (message.Channel.Id == _oocChannelId) if (message.ChannelId == _oocChannelId)
{ {
_taskManager.RunOnMainThread(() => _chatManager.SendHookOOC(message.Author.Username, contents)); _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)); _taskManager.RunOnMainThread(() => _chatManager.SendHookAdmin(message.Author.Username, contents));
} }

View File

@@ -1,12 +1,9 @@
using System.Linq; using System.Threading.Tasks;
using System.Threading.Tasks;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Discord; using NetCord;
using Discord.WebSocket; using NetCord.Gateway;
using NetCord.Rest;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Reflection;
using Robust.Shared.Utility;
using LogMessage = Discord.LogMessage;
namespace Content.Server.Discord.DiscordLink; 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. /// 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. /// Use this to reply to the message, delete it, etc.
/// </summary> /// </summary>
public SocketMessage Message { get; init; } = default!; public Message Message { get; init; } = default!;
} }
/// <summary> /// <summary>
@@ -45,7 +42,7 @@ public sealed class DiscordLink : IPostInjectInit
/// <remarks> /// <remarks>
/// This should not be used directly outside of DiscordLink. So please do not make it public. Use the methods in this class instead. /// This should not be used directly outside of DiscordLink. So please do not make it public. Use the methods in this class instead.
/// </remarks> /// </remarks>
private DiscordSocketClient? _client; private GatewayClient? _client;
private ISawmill _sawmill = default!; private ISawmill _sawmill = default!;
private ISawmill _sawmillLog = default!; private ISawmill _sawmillLog = default!;
@@ -67,7 +64,7 @@ public sealed class DiscordLink : IPostInjectInit
/// <summary> /// <summary>
/// Event that is raised when a message is received from Discord. This is raised for every message, including commands. /// Event that is raised when a message is received from Discord. This is raised for every message, including commands.
/// </summary> /// </summary>
public event Action<SocketMessage>? OnMessageReceived; public event Action<Message>? OnMessageReceived;
public void RegisterCommandCallback(Action<CommandReceivedEventArgs> callback, string command) public void RegisterCommandCallback(Action<CommandReceivedEventArgs> callback, string command)
{ {
@@ -101,33 +98,34 @@ public sealed class DiscordLink : IPostInjectInit
return; return;
} }
_client = new DiscordSocketClient(new DiscordSocketConfig() _client = new GatewayClient(new BotToken(token), new GatewayClientConfiguration()
{ {
GatewayIntents = GatewayIntents.Guilds Intents = GatewayIntents.Guilds
| GatewayIntents.GuildMembers | GatewayIntents.GuildUsers
| GatewayIntents.GuildMessages | GatewayIntents.GuildMessages
| GatewayIntents.MessageContent | GatewayIntents.MessageContent
| GatewayIntents.DirectMessages, | GatewayIntents.DirectMessages,
Logger = new DiscordSawmillLogger(_sawmillLog),
}); });
_client.Log += Log; _client.MessageCreate += OnCommandReceivedInternal;
_client.MessageReceived += OnCommandReceivedInternal; _client.MessageCreate += OnMessageReceivedInternal;
_client.MessageReceived += OnMessageReceivedInternal;
_botToken = token; _botToken = token;
// Since you cannot change the token while the server is running / the DiscordLink is initialized, // 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. // we can just set the token without updating it every time the cvar changes.
_client.Ready += () => _client.Ready += _ =>
{ {
_sawmill.Info("Discord client ready."); _sawmill.Info("Discord client ready.");
return Task.CompletedTask; return default;
}; };
Task.Run(async () => Task.Run(async () =>
{ {
try try
{ {
await LoginAsync(token); await _client.StartAsync();
_sawmill.Info("Connected to Discord.");
} }
catch (Exception e) catch (Exception e)
{ {
@@ -143,12 +141,11 @@ public sealed class DiscordLink : IPostInjectInit
_sawmill.Info("Disconnecting from Discord."); _sawmill.Info("Disconnecting from Discord.");
// Unsubscribe from the events. // Unsubscribe from the events.
_client.MessageReceived -= OnCommandReceivedInternal; _client.MessageCreate -= OnCommandReceivedInternal;
_client.MessageReceived -= OnMessageReceivedInternal; _client.MessageCreate -= OnMessageReceivedInternal;
await _client.LogoutAsync(); await _client.CloseAsync();
await _client.StopAsync(); _client.Dispose();
await _client.DisposeAsync();
_client = null; _client = null;
} }
@@ -172,45 +169,12 @@ public sealed class DiscordLink : IPostInjectInit
BotPrefix = prefix; BotPrefix = prefix;
} }
private async Task LoginAsync(string token) private ValueTask OnCommandReceivedInternal(Message message)
{
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; var content = message.Content;
// If the message doesn't start with the bot prefix, ignore it. // If the message doesn't start with the bot prefix, ignore it.
if (!content.StartsWith(BotPrefix)) if (!content.StartsWith(BotPrefix))
return Task.CompletedTask; return ValueTask.CompletedTask;
// Split the message into the command and the arguments. // Split the message into the command and the arguments.
var trimmedInput = content[BotPrefix.Length..].Trim(); var trimmedInput = content[BotPrefix.Length..].Trim();
@@ -236,13 +200,13 @@ public sealed class DiscordLink : IPostInjectInit
Arguments = arguments, Arguments = arguments,
Message = message, Message = message,
}); });
return Task.CompletedTask; return ValueTask.CompletedTask;
} }
private Task OnMessageReceivedInternal(SocketMessage message) private ValueTask OnMessageReceivedInternal(Message message)
{ {
OnMessageReceived?.Invoke(message); OnMessageReceived?.Invoke(message);
return Task.CompletedTask; return ValueTask.CompletedTask;
} }
#region Proxy methods #region Proxy methods
@@ -257,14 +221,18 @@ public sealed class DiscordLink : IPostInjectInit
return; return;
} }
var channel = _client.GetChannel(channelId) as IMessageChannel; var channel = await _client.Rest.GetChannelAsync(channelId) as TextChannel;
if (channel == null) if (channel == null)
{ {
_sawmill.Error("Tried to send a message to Discord but the channel {Channel} was not found.", channel); _sawmill.Error("Tried to send a message to Discord but the channel {Channel} was not found.", channel);
return; return;
} }
await channel.SendMessageAsync(message, allowedMentions: AllowedMentions.None); await channel.SendMessageAsync(new MessageProperties()
{
AllowedMentions = AllowedMentionsProperties.None,
Content = message,
});
} }
#endregion #endregion

View File

@@ -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<TState>(NetCord.Logging.LogLevel logLevel, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
sawmill.Log(GetLogLevel(logLevel), exception, formatter(state, exception));
}
void IRestLogger.Log<TState>(NetCord.Logging.LogLevel logLevel, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
sawmill.Log(GetLogLevel(logLevel), exception, formatter(state, exception));
}
void IVoiceLogger.Log<TState>(NetCord.Logging.LogLevel logLevel, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
sawmill.Log(GetLogLevel(logLevel), exception, formatter(state, exception));
}
}

View File

@@ -7,13 +7,13 @@
<PackageVersion Remove="Npgsql.EntityFrameworkCore.PostgreSQL" /> <PackageVersion Remove="Npgsql.EntityFrameworkCore.PostgreSQL" />
<PackageVersion Remove="Microsoft.EntityFrameworkCore.Design" /> <PackageVersion Remove="Microsoft.EntityFrameworkCore.Design" />
<PackageVersion Include="CsvHelper" Version="33.0.1" /> <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="ImGui.NET" Version="1.87.3" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.1"> <PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageVersion> </PackageVersion>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="9.0.1" /> <PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="9.0.1" />
<PackageVersion Include="NetCord" Version="1.0.0-alpha.388" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.3" /> <PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.3" />
<PackageVersion Include="OpenTK" Version="4.7.2" /> <PackageVersion Include="OpenTK" Version="4.7.2" />
<PackageVersion Include="Veldrid" Version="4.8.0" /> <PackageVersion Include="Veldrid" Version="4.8.0" />