mirror of
https://github.com/NoiTheCat/BirthdayBot.git
synced 2024-11-24 01:14:12 +00:00
Begin switch to Interaction Framework
This commit is contained in:
parent
74f876c4af
commit
b1af7922af
4 changed files with 65 additions and 123 deletions
|
@ -1,4 +1,4 @@
|
||||||
using BirthdayBot.Data;
|
using Discord.Interactions;
|
||||||
using NodaTime;
|
using NodaTime;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
@ -6,11 +6,9 @@ using System.Text.RegularExpressions;
|
||||||
namespace BirthdayBot.ApplicationCommands;
|
namespace BirthdayBot.ApplicationCommands;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Base class for classes handling slash command execution.
|
/// Base class for our interaction module classes. Contains common data for use in implementing classes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal abstract class BotApplicationCommand {
|
public abstract class BotModuleBase : InteractionModuleBase<SocketInteractionContext> {
|
||||||
public delegate Task CommandResponder(ShardInstance instance, GuildConfiguration gconf, SocketSlashCommand arg);
|
|
||||||
|
|
||||||
protected const string HelpPfxModOnly = "Bot moderators only: ";
|
protected const string HelpPfxModOnly = "Bot moderators only: ";
|
||||||
protected const string ErrGuildOnly = ":x: This command can only be run within a server.";
|
protected const string ErrGuildOnly = ":x: This command can only be run within a server.";
|
||||||
protected const string ErrNotAllowed = ":x: Only server moderators may use this command.";
|
protected const string ErrNotAllowed = ":x: Only server moderators may use this command.";
|
||||||
|
@ -20,24 +18,17 @@ internal abstract class BotApplicationCommand {
|
||||||
protected const string HelpOptDate = "A date, including the month and day. For example, \"15 January\".";
|
protected const string HelpOptDate = "A date, including the month and day. For example, \"15 January\".";
|
||||||
protected const string HelpOptZone = "A 'tzdata'-compliant time zone name. See help for more details.";
|
protected const string HelpOptZone = "A 'tzdata'-compliant time zone name. See help for more details.";
|
||||||
|
|
||||||
protected static ReadOnlyDictionary<string, string> TzNameMap { get; }
|
#pragma warning disable CS8618
|
||||||
|
public DiscordSocketClient BotClient { get; set; }
|
||||||
|
public ShardInstance Instance { get; set; }
|
||||||
|
#pragma warning restore CS8618
|
||||||
|
|
||||||
/// <summary>
|
protected static IReadOnlyDictionary<string, string> TzNameMap { get; }
|
||||||
/// Returns a list of application command definitions handled by the implementing class,
|
|
||||||
/// for use when registering/updating this bot's available slash commands.
|
|
||||||
/// </summary>
|
|
||||||
public abstract IEnumerable<ApplicationCommandProperties> GetCommands();
|
|
||||||
|
|
||||||
/// <summary>
|
static BotModuleBase() {
|
||||||
/// Given the command name, returns the designated handler to execute to fulfill the command.
|
|
||||||
/// Returns null if this class does not contain a handler for the given command.
|
|
||||||
/// </summary>
|
|
||||||
public abstract CommandResponder? GetHandlerFor(string commandName);
|
|
||||||
|
|
||||||
static BotApplicationCommand() {
|
|
||||||
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
foreach (var name in DateTimeZoneProviders.Tzdb.Ids) dict.Add(name, name);
|
foreach (var name in DateTimeZoneProviders.Tzdb.Ids) dict.Add(name, name);
|
||||||
TzNameMap = new(dict);
|
TzNameMap = new ReadOnlyDictionary<string, string>(dict);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
|
@ -22,7 +22,8 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
||||||
<PackageReference Include="Discord.Net" Version="3.2.1" />
|
<PackageReference Include="Discord.Net" Version="3.3.2" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
<PackageReference Include="NodaTime" Version="3.0.9" />
|
<PackageReference Include="NodaTime" Version="3.0.9" />
|
||||||
<PackageReference Include="Npgsql" Version="6.0.3" />
|
<PackageReference Include="Npgsql" Version="6.0.3" />
|
||||||
|
|
132
ShardInstance.cs
132
ShardInstance.cs
|
@ -1,8 +1,9 @@
|
||||||
using BirthdayBot.ApplicationCommands;
|
using BirthdayBot.BackgroundServices;
|
||||||
using BirthdayBot.BackgroundServices;
|
|
||||||
using BirthdayBot.Data;
|
using BirthdayBot.Data;
|
||||||
|
using Discord.Interactions;
|
||||||
using Discord.Net;
|
using Discord.Net;
|
||||||
using static BirthdayBot.ApplicationCommands.BotApplicationCommand;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System.Reflection;
|
||||||
using static BirthdayBot.TextCommands.CommandsCommon;
|
using static BirthdayBot.TextCommands.CommandsCommon;
|
||||||
|
|
||||||
namespace BirthdayBot;
|
namespace BirthdayBot;
|
||||||
|
@ -10,23 +11,24 @@ namespace BirthdayBot;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Single shard instance for Birthday Bot. This shard independently handles all input and output to Discord.
|
/// Single shard instance for Birthday Bot. This shard independently handles all input and output to Discord.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class ShardInstance : IDisposable {
|
public class ShardInstance : IDisposable {
|
||||||
private readonly ShardManager _manager;
|
private readonly ShardManager _manager;
|
||||||
private readonly ShardBackgroundWorker _background;
|
private readonly ShardBackgroundWorker _background;
|
||||||
private readonly Dictionary<string, CommandHandler> _textDispatch;
|
private readonly Dictionary<string, CommandHandler> _textDispatch;
|
||||||
private readonly IEnumerable<BotApplicationCommand> _slashCmdHandlers;
|
private readonly InteractionService _interactionService;
|
||||||
|
private readonly IServiceProvider _services;
|
||||||
|
|
||||||
public DiscordSocketClient DiscordClient { get; }
|
internal DiscordSocketClient DiscordClient { get; }
|
||||||
public int ShardId => DiscordClient.ShardId;
|
public int ShardId => DiscordClient.ShardId;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a value showing the time in which the last background run successfully completed.
|
/// Returns a value showing the time in which the last background run successfully completed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTimeOffset LastBackgroundRun => _background.LastBackgroundRun;
|
internal DateTimeOffset LastBackgroundRun => _background.LastBackgroundRun;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the name of the background service currently in execution.
|
/// Returns the name of the background service currently in execution.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? CurrentExecutingService => _background.CurrentExecutingService;
|
internal string? CurrentExecutingService => _background.CurrentExecutingService;
|
||||||
public Configuration Config => _manager.Config;
|
internal Configuration Config => _manager.Config;
|
||||||
|
|
||||||
public const string InternalError = ":x: An unknown error occurred. If it persists, please notify the bot owner.";
|
public const string InternalError = ":x: An unknown error occurred. If it persists, please notify the bot owner.";
|
||||||
public const string UnknownCommandError = "Oops, that command isn't supposed to be there... Please try something else.";
|
public const string UnknownCommandError = "Oops, that command isn't supposed to be there... Please try something else.";
|
||||||
|
@ -34,17 +36,20 @@ class ShardInstance : IDisposable {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Prepares and configures the shard instances, but does not yet start its connection.
|
/// Prepares and configures the shard instances, but does not yet start its connection.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public ShardInstance(ShardManager manager, DiscordSocketClient client,
|
internal ShardInstance(ShardManager manager, IServiceProvider services, Dictionary<string, CommandHandler> textCmds) {
|
||||||
Dictionary<string, CommandHandler> textCmds, IEnumerable<BotApplicationCommand> appCmdHandlers) {
|
|
||||||
_manager = manager;
|
_manager = manager;
|
||||||
|
_services = services;
|
||||||
_textDispatch = textCmds;
|
_textDispatch = textCmds;
|
||||||
_slashCmdHandlers = appCmdHandlers;
|
|
||||||
|
|
||||||
DiscordClient = client;
|
DiscordClient = _services.GetRequiredService<DiscordSocketClient>();
|
||||||
DiscordClient.Log += Client_Log;
|
DiscordClient.Log += Client_Log;
|
||||||
DiscordClient.Ready += Client_Ready;
|
DiscordClient.Ready += Client_Ready;
|
||||||
DiscordClient.MessageReceived += Client_MessageReceived;
|
DiscordClient.MessageReceived += Client_MessageReceived;
|
||||||
DiscordClient.SlashCommandExecuted += DiscordClient_SlashCommandExecuted;
|
|
||||||
|
_interactionService = _services.GetRequiredService<InteractionService>();
|
||||||
|
_interactionService.AddModulesAsync(Assembly.GetExecutingAssembly(), null);
|
||||||
|
DiscordClient.InteractionCreated += DiscordClient_InteractionCreated;
|
||||||
|
_interactionService.SlashCommandExecuted += InteractionService_SlashCommandExecuted;
|
||||||
|
|
||||||
// Background task constructor begins background processing immediately.
|
// Background task constructor begins background processing immediately.
|
||||||
_background = new ShardBackgroundWorker(this);
|
_background = new ShardBackgroundWorker(this);
|
||||||
|
@ -62,10 +67,14 @@ class ShardInstance : IDisposable {
|
||||||
/// Does all necessary steps to stop this shard, including canceling background tasks and disconnecting.
|
/// Does all necessary steps to stop this shard, including canceling background tasks and disconnecting.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
|
// TODO are these necessary?
|
||||||
|
_interactionService.SlashCommandExecuted -= InteractionService_SlashCommandExecuted;
|
||||||
|
DiscordClient.InteractionCreated -= DiscordClient_InteractionCreated;
|
||||||
DiscordClient.Log -= Client_Log;
|
DiscordClient.Log -= Client_Log;
|
||||||
DiscordClient.Ready -= Client_Ready;
|
DiscordClient.Ready -= Client_Ready;
|
||||||
DiscordClient.MessageReceived -= Client_MessageReceived;
|
DiscordClient.MessageReceived -= Client_MessageReceived;
|
||||||
|
|
||||||
|
_interactionService.Dispose();
|
||||||
_background.Dispose();
|
_background.Dispose();
|
||||||
DiscordClient.LogoutAsync().Wait(5000);
|
DiscordClient.LogoutAsync().Wait(5000);
|
||||||
DiscordClient.StopAsync().Wait(5000);
|
DiscordClient.StopAsync().Wait(5000);
|
||||||
|
@ -109,36 +118,13 @@ class ShardInstance : IDisposable {
|
||||||
await DiscordClient.SetGameAsync(CommandPrefix + "help");
|
await DiscordClient.SetGameAsync(CommandPrefix + "help");
|
||||||
|
|
||||||
#if !DEBUG
|
#if !DEBUG
|
||||||
// Update our commands here, only when the first shard connects
|
// Update slash/interaction commands
|
||||||
if (ShardId != 0) return;
|
await _interactionService.RegisterCommandsGloballyAsync(true).ConfigureAwait(false);
|
||||||
#endif
|
|
||||||
var commands = new List<ApplicationCommandProperties>();
|
|
||||||
foreach (var source in _slashCmdHandlers) {
|
|
||||||
commands.AddRange(source.GetCommands());
|
|
||||||
}
|
|
||||||
#if !DEBUG
|
|
||||||
// Remove any unneeded/unused commands
|
|
||||||
var existingcmdnames = commands.Select(c => c.Name.Value).ToHashSet();
|
|
||||||
foreach (var gcmd in await DiscordClient.GetGlobalApplicationCommandsAsync()) {
|
|
||||||
if (!existingcmdnames.Contains(gcmd.Name)) {
|
|
||||||
Log("Command registration", $"Found registered unused command /{gcmd.Name} - sending removal request");
|
|
||||||
await gcmd.DeleteAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// And update what we have
|
|
||||||
Log("Command registration", $"Bulk updating {commands.Count} global command(s)");
|
|
||||||
await DiscordClient.BulkOverwriteGlobalApplicationCommandsAsync(commands.ToArray()).ConfigureAwait(false);
|
|
||||||
#else
|
#else
|
||||||
// Debug: Register our commands locally instead, in each guild we're in
|
// Debug: Register our commands locally instead, in each guild we're in
|
||||||
foreach (var g in DiscordClient.Guilds) {
|
foreach (var g in DiscordClient.Guilds) {
|
||||||
await g.DeleteApplicationCommandsAsync().ConfigureAwait(false);
|
await _interactionService.RegisterCommandsToGuildAsync(g.Id, true).ConfigureAwait(false);
|
||||||
await g.BulkOverwriteApplicationCommandAsync(commands.ToArray()).ConfigureAwait(false);
|
// TODO log?
|
||||||
Log("Command registration", $"Sent bulk overrides for {commands.Count} commands.");
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var gcmd in await DiscordClient.GetGlobalApplicationCommandsAsync()) {
|
|
||||||
Log("Command registration", $"Found global command /{gcmd.Name} and we're DEBUG - sending removal request");
|
|
||||||
await gcmd.DeleteAsync();
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
@ -183,57 +169,25 @@ class ShardInstance : IDisposable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private async Task InteractionService_SlashCommandExecuted(SlashCommandInfo arg1, IInteractionContext arg2, IResult arg3) {
|
||||||
/// Dispatches to the appropriate slash command handler while catching any exceptions that may occur.
|
if (arg3.IsSuccess) return;
|
||||||
/// </summary>
|
Log("Interaction error", Enum.GetName(typeof(InteractionCommandError), arg3.Error) + " " + arg3.ErrorReason);
|
||||||
private async Task DiscordClient_SlashCommandExecuted(SocketSlashCommand arg) {
|
// TODO finish this up
|
||||||
SocketGuildChannel? rptChannel = arg.Channel as SocketGuildChannel;
|
|
||||||
string rpt = "";
|
|
||||||
if (rptChannel != null) rpt += rptChannel.Guild.Name + "!";
|
|
||||||
rpt += arg.User;
|
|
||||||
var rptId = rptChannel?.Guild.Id ?? arg.User.Id;
|
|
||||||
var logLine = $"/{arg.CommandName} at {rpt}; { (rptChannel != null ? "Guild" : "User") } ID {rptId}.";
|
|
||||||
|
|
||||||
// Specific reply for DM messages
|
|
||||||
if (rptChannel == null) {
|
|
||||||
// TODO do not hardcode message
|
|
||||||
// TODO figure out appropriate message
|
|
||||||
Log("Command", logLine + " Sending default reply.");
|
|
||||||
await arg.RespondAsync("don't dm me").ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine handler to use
|
private async Task DiscordClient_InteractionCreated(SocketInteraction arg) {
|
||||||
CommandResponder? handler = null;
|
// TODO this is straight from the example - look it over
|
||||||
foreach (var source in _slashCmdHandlers) {
|
|
||||||
handler = source.GetHandlerFor(arg.CommandName);
|
|
||||||
if (handler != null) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (handler == null) { // Handler not found
|
|
||||||
Log("Command", logLine + " Unknown command.");
|
|
||||||
await arg.RespondAsync(UnknownCommandError,
|
|
||||||
ephemeral: true).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var gconf = await GuildConfiguration.LoadAsync(rptChannel.Guild.Id, false);
|
|
||||||
// Blocklist/moderated check
|
|
||||||
if (!gconf!.IsBotModerator((SocketGuildUser)arg.User)) // Except if moderator
|
|
||||||
{
|
|
||||||
if (await gconf.IsUserBlockedAsync(arg.User.Id)) {
|
|
||||||
Log("Command", logLine + " Blocked per guild policy.");
|
|
||||||
await arg.RespondAsync(AccessDeniedError, ephemeral: true).ConfigureAwait(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Execute the handler
|
|
||||||
try {
|
try {
|
||||||
await handler(this, gconf, arg).ConfigureAwait(false);
|
// Create an execution context that matches the generic type parameter of your InteractionModuleBase<T> modules
|
||||||
Log("Command", logLine);
|
var context = new SocketInteractionContext(DiscordClient, arg);
|
||||||
} catch (Exception e) when (e is not HttpException) {
|
await _interactionService.ExecuteCommandAsync(context, _services);
|
||||||
Log("Command", $"{logLine} {e}");
|
} catch (Exception ex) {
|
||||||
|
Console.WriteLine(ex);
|
||||||
|
|
||||||
|
// If a Slash Command execution fails it is most likely that the original interaction acknowledgement will persist. It is a good idea to delete the original
|
||||||
|
// response, or at least let the user know that something went wrong during the command execution.
|
||||||
|
if (arg.Type == InteractionType.ApplicationCommand)
|
||||||
|
await arg.GetOriginalResponseAsync().ContinueWith(async (msg) => await msg.Result.DeleteAsync());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
global using Discord;
|
global using Discord;
|
||||||
global using Discord.WebSocket;
|
global using Discord.WebSocket;
|
||||||
using BirthdayBot.ApplicationCommands;
|
|
||||||
using BirthdayBot.BackgroundServices;
|
using BirthdayBot.BackgroundServices;
|
||||||
using BirthdayBot.TextCommands;
|
using BirthdayBot.TextCommands;
|
||||||
|
using Discord.Interactions;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using static BirthdayBot.TextCommands.CommandsCommon;
|
using static BirthdayBot.TextCommands.CommandsCommon;
|
||||||
|
|
||||||
|
@ -45,7 +46,6 @@ class ShardManager : IDisposable {
|
||||||
private readonly Dictionary<int, ShardInstance?> _shards;
|
private readonly Dictionary<int, ShardInstance?> _shards;
|
||||||
|
|
||||||
private readonly Dictionary<string, CommandHandler> _textCommands;
|
private readonly Dictionary<string, CommandHandler> _textCommands;
|
||||||
private readonly List<BotApplicationCommand> _appCommands;
|
|
||||||
|
|
||||||
private readonly Task _statusTask;
|
private readonly Task _statusTask;
|
||||||
private readonly CancellationTokenSource _mainCancel;
|
private readonly CancellationTokenSource _mainCancel;
|
||||||
|
@ -70,14 +70,6 @@ class ShardManager : IDisposable {
|
||||||
var cmdsMods = new ManagerCommands(cfg, cmdsUser.Commands);
|
var cmdsMods = new ManagerCommands(cfg, cmdsUser.Commands);
|
||||||
foreach (var item in cmdsMods.Commands) _textCommands.Add(item.Item1, item.Item2);
|
foreach (var item in cmdsMods.Commands) _textCommands.Add(item.Item1, item.Item2);
|
||||||
|
|
||||||
_appCommands = new List<BotApplicationCommand>() {
|
|
||||||
new HelpCommands(),
|
|
||||||
new RegistrationCommands(),
|
|
||||||
new RegistrationOverrideCommands(),
|
|
||||||
new QueryCommands(),
|
|
||||||
new ModCommands(this)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Allocate shards based on configuration
|
// Allocate shards based on configuration
|
||||||
_shards = new Dictionary<int, ShardInstance?>();
|
_shards = new Dictionary<int, ShardInstance?>();
|
||||||
for (int i = Config.ShardStart; i < (Config.ShardStart + Config.ShardAmount); i++) {
|
for (int i = Config.ShardStart; i < (Config.ShardStart + Config.ShardAmount); i++) {
|
||||||
|
@ -124,8 +116,12 @@ class ShardManager : IDisposable {
|
||||||
DefaultRetryMode = RetryMode.AlwaysRetry,
|
DefaultRetryMode = RetryMode.AlwaysRetry,
|
||||||
GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMembers | GatewayIntents.GuildMessages
|
GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMembers | GatewayIntents.GuildMessages
|
||||||
};
|
};
|
||||||
var newClient = new DiscordSocketClient(clientConf);
|
var services = new ServiceCollection()
|
||||||
newInstance = new ShardInstance(this, newClient, _textCommands, _appCommands);
|
.AddSingleton(s => new ShardInstance(this, s, _textCommands))
|
||||||
|
.AddSingleton(s => new DiscordSocketClient(clientConf))
|
||||||
|
.AddSingleton(s => new InteractionService(s.GetRequiredService<DiscordSocketClient>()))
|
||||||
|
.BuildServiceProvider();
|
||||||
|
newInstance = services.GetRequiredService<ShardInstance>();
|
||||||
await newInstance.StartAsync().ConfigureAwait(false);
|
await newInstance.StartAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
return newInstance;
|
return newInstance;
|
||||||
|
|
Loading…
Reference in a new issue