-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDiscordBotService.cs
More file actions
135 lines (119 loc) · 5.45 KB
/
Copy pathDiscordBotService.cs
File metadata and controls
135 lines (119 loc) · 5.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using Discord;
using Discord.Interactions;
using Discord.WebSocket;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace RustPlusBot.Discord;
/// <summary>
/// Hosted service that logs the bot in, loads interaction modules, and registers slash commands
/// per guild on ready/join (per-guild registration is instant, unlike global commands).
/// </summary>
/// <param name="client">The Discord socket client.</param>
/// <param name="interactions">The interaction service that dispatches slash commands.</param>
/// <param name="services">The root service provider used to construct interaction modules.</param>
/// <param name="moduleAssemblies">Additional assemblies to scan for interaction modules contributed by feature projects.</param>
/// <param name="options">The Discord options carrying the bot token.</param>
/// <param name="logger">The logger.</param>
[SuppressMessage("Performance", "CA1873:Avoid potentially expensive logging",
Justification =
"Log-bridge arguments are cheap LogMessage property reads; an IsEnabled guard would be redundant noise.")]
public sealed class DiscordBotService(
DiscordSocketClient client,
InteractionService interactions,
IServiceProvider services,
IEnumerable<InteractionModuleAssembly> moduleAssemblies,
IOptions<DiscordOptions> options,
ILogger<DiscordBotService> logger) : IHostedService
{
private readonly DiscordOptions _options = options.Value;
private bool _hasRegisteredCommands;
/// <inheritdoc />
public async Task StartAsync(CancellationToken cancellationToken)
{
client.Log += OnLogAsync;
interactions.Log += OnLogAsync;
client.Ready += OnReadyAsync;
client.InteractionCreated += OnInteractionCreatedAsync;
client.JoinedGuild += OnJoinedGuildAsync;
await interactions.AddModulesAsync(Assembly.GetExecutingAssembly(), services).ConfigureAwait(false);
foreach (var moduleAssembly in moduleAssemblies)
{
await interactions.AddModulesAsync(moduleAssembly.Assembly, services).ConfigureAwait(false);
}
await client.LoginAsync(TokenType.Bot, _options.Token).ConfigureAwait(false);
await client.StartAsync().ConfigureAwait(false);
}
/// <inheritdoc />
public async Task StopAsync(CancellationToken cancellationToken)
{
await client.LogoutAsync().ConfigureAwait(false);
await client.StopAsync().ConfigureAwait(false);
}
private Task OnReadyAsync()
{
// Ready fires on every gateway (re)connect; only register commands once per process.
// Ready is dispatched serially on the gateway thread, so no synchronization is needed —
// set the flag before offloading so a re-fired Ready can't double-register.
if (_hasRegisteredCommands)
{
return Task.CompletedTask;
}
_hasRegisteredCommands = true;
// Registration is REST work (one call per guild); doing it inline blocks the gateway task
// and stalls event dispatch, so offload it. Failures must be caught here — nothing awaits this.
_ = Task.Run(RegisterCommandsAsync);
return Task.CompletedTask;
}
private async Task RegisterCommandsAsync()
{
try
{
if (_options.ResetCommandsOnStartup)
{
await client.Rest.DeleteAllGlobalCommandsAsync().ConfigureAwait(false);
logger.LogWarning(
"ResetCommandsOnStartup is enabled: deleted all global application commands before registration.");
}
foreach (var guild in client.Guilds)
{
await interactions.RegisterCommandsToGuildAsync(guild.Id).ConfigureAwait(false);
}
logger.LogInformation("Registered commands to {GuildCount} guild(s).", client.Guilds.Count);
}
#pragma warning disable CA1031 // Broad catch: fire-and-forget — an unobserved exception would vanish; log it instead.
catch (Exception ex)
#pragma warning restore CA1031
{
logger.LogError(ex, "Registering slash commands failed.");
}
}
[SuppressMessage("Performance", "CA1859:Use concrete types when possible for improved performance",
Justification =
"Method signature is constrained by the Discord.Net JoinedGuild event delegate (Func<SocketGuild, Task>).")]
private Task OnJoinedGuildAsync(SocketGuild guild) =>
interactions.RegisterCommandsToGuildAsync(guild.Id);
private async Task OnInteractionCreatedAsync(SocketInteraction interaction)
{
var context = new SocketInteractionContext(client, interaction);
await interactions.ExecuteCommandAsync(context, services).ConfigureAwait(false);
}
private Task OnLogAsync(LogMessage message)
{
logger.Log(ToLogLevel(message.Severity), message.Exception, "[{Source}] {Message}", message.Source,
message.Message);
return Task.CompletedTask;
}
private static LogLevel ToLogLevel(LogSeverity severity) => severity switch
{
LogSeverity.Critical => LogLevel.Critical,
LogSeverity.Error => LogLevel.Error,
LogSeverity.Warning => LogLevel.Warning,
LogSeverity.Info => LogLevel.Information,
LogSeverity.Verbose => LogLevel.Debug,
LogSeverity.Debug => LogLevel.Trace,
_ => LogLevel.Information,
};
}