From 12c421ef1790484980d74fb37ddf4585bb3b7e58 Mon Sep 17 00:00:00 2001 From: Noi Date: Fri, 28 Jan 2022 20:30:43 -0800 Subject: [PATCH] Implement slash commands Functionality between existing text and new slash commands have been reimplemented, with common functionality between both methods moved to a common base class. This may see another rewrite later to make use of the library's interactions framework. --- CommandsCommon.cs | 121 +++++++++++++ CommandsSlash.cs | 304 +++++++++++++++++++++++++++++++++ Commands.cs => CommandsText.cs | 120 +------------ WorldTime.cs | 8 +- WorldTime.csproj | 6 +- 5 files changed, 438 insertions(+), 121 deletions(-) create mode 100644 CommandsCommon.cs create mode 100644 CommandsSlash.cs rename Commands.cs => CommandsText.cs (72%) diff --git a/CommandsCommon.cs b/CommandsCommon.cs new file mode 100644 index 0000000..6d97a20 --- /dev/null +++ b/CommandsCommon.cs @@ -0,0 +1,121 @@ +using NodaTime; +using System.Collections.ObjectModel; +using System.Globalization; +using System.Text; + +namespace WorldTime; + +internal abstract class CommandsCommon { + protected readonly Database _database; + protected readonly WorldTime _instance; + + private static readonly ReadOnlyDictionary _tzNameMap; + + protected const string ErrInvalidZone = ":x: Not a valid zone name." + + " To find your time zone, refer to: ."; + protected const string ErrTargetUserNotFound = ":x: Unable to find the target user."; + protected const string ErrNoUserCache = ":warning: Please try the command again."; + protected const int MaxSingleLineLength = 750; + protected const int MaxSingleOutputLength = 900; + + static CommandsCommon() { + Dictionary tzNameMap = new(StringComparer.OrdinalIgnoreCase); + foreach (var name in DateTimeZoneProviders.Tzdb.Ids) tzNameMap.Add(name, name); + _tzNameMap = new(tzNameMap); + } + + public CommandsCommon(Database database, WorldTime instance) { + _database = database; + _instance = instance; + } + + /// + /// Returns a string displaying the current time in the given time zone. + /// The result begins with four numbers for sorting purposes. Must be trimmed before output. + /// + protected static string TzPrint(string zone) { + var tzdb = DateTimeZoneProviders.Tzdb; + DateTimeZone tz = tzdb.GetZoneOrNull(zone)!; + if (tz == null) throw new Exception("Encountered unknown time zone: " + zone); + + var now = SystemClock.Instance.GetCurrentInstant().InZone(tz); + var sortpfx = now.ToString("MMdd", DateTimeFormatInfo.InvariantInfo); + var fullstr = now.ToString("dd'-'MMM' 'HH':'mm' 'x' (UTC'o')'", DateTimeFormatInfo.InvariantInfo); + return $"{sortpfx}● `{fullstr}`"; + } + + /// + /// Checks given time zone input. Returns a valid string for use with NodaTime, or null. + /// + protected static string? ParseTimeZone(string tzinput) { + if (tzinput.Equals("Asia/Calcutta", StringComparison.OrdinalIgnoreCase)) tzinput = "Asia/Kolkata"; + if (_tzNameMap.TryGetValue(tzinput, out var name)) return name; + return null; + } + + /// + /// Formats a user's name to a consistent, readable format which makes use of their nickname. + /// + protected static string FormatName(SocketGuildUser user) { + static string escapeFormattingCharacters(string input) { + var result = new StringBuilder(); + foreach (var c in input) { + if (c is '\\' or '_' or '~' or '*' or '@') { + result.Append('\\'); + } + result.Append(c); + } + return result.ToString(); + } + + var username = escapeFormattingCharacters(user.Username); + if (user.Nickname != null) { + return $"**{escapeFormattingCharacters(user.Nickname)}** ({username}#{user.Discriminator})"; + } + return $"**{username}**#{user.Discriminator}"; + } + + /// + /// Checks if the given user can be considered a guild admin ('Manage Server' is set). + /// + protected static bool IsUserAdmin(SocketGuildUser user) + => user.GuildPermissions.Administrator || user.GuildPermissions.ManageGuild; + // TODO port modrole feature from BB, implement in here + + /// + /// Checks if the member cache for the specified guild needs to be filled, and sends a request if needed. + /// + /// + /// Due to a quirk in Discord.Net, the user cache cannot be filled until the command handler is no longer executing, + /// regardless of if the request runs on its own thread. + /// + /// + /// True if the guild's members are already downloaded. If false, the command handler must notify the user. + /// + protected static async Task AreUsersDownloadedAsync(SocketGuild guild) { + if (HasMostMembersDownloaded(guild)) return true; + else { + // Event handler hangs if awaited normally or used with Task.Run + await Task.Factory.StartNew(guild.DownloadUsersAsync).ConfigureAwait(false); + return false; + } + } + + /// + /// An alternative to . + /// Returns true if *most* members have been downloaded. + /// Used as a workaround check due to Discord.Net occasionally unable to actually download all members. + /// + /// Copied directly from BirthdayBot. Try to coordinate changes between projects... + private static bool HasMostMembersDownloaded(SocketGuild guild) { + if (guild.HasAllMembers) return true; + if (guild.MemberCount > 30) { + // For guilds of size over 30, require 85% or more of the members to be known + // (26/30, 42/50, 255/300, etc) + return guild.DownloadedMemberCount >= (int)(guild.MemberCount * 0.85); + } else { + // For smaller guilds, fail if two or more members are missing + return guild.MemberCount - guild.DownloadedMemberCount <= 2; + } + } +} diff --git a/CommandsSlash.cs b/CommandsSlash.cs new file mode 100644 index 0000000..def5d80 --- /dev/null +++ b/CommandsSlash.cs @@ -0,0 +1,304 @@ +using System.Text; + +namespace WorldTime; + +internal class CommandsSlash : CommandsCommon { + delegate Task CommandResponder(SocketSlashCommand arg); + + const string ErrGuildOnly = ":x: This command can only be run within a server."; + const string ErrNotAllowed = ":x: Only server moderators may use this command."; + + const string EmbedHelpField1 = $"`/help` - {HelpHelp}\n" + + $"`/list` - {HelpList}\n" + + $"`/set` - {HelpSet}\n" + + $"`/remove` - {HelpRemove}"; + const string EmbedHelpField2 = $"`/set-for` - {HelpSetFor}\n`/remove-for` - {HelpRemoveFor}"; + + #region Help strings + const string HelpHelp = "Displays a list of available bot commands."; + const string HelpList = "Shows the current time for all recently active known users."; + const string HelpSet = "Adds or updates your time zone to the bot."; + const string HelpSetFor = "Sets/updates time zone for a given user."; + const string HelpRemove = "Removes your time zone information from this bot."; + const string HelpRemoveFor = "Removes time zone for a given user."; + #endregion + + public CommandsSlash(WorldTime inst, Database db) : base(db, inst) { + inst.DiscordClient.SlashCommandExecuted += DiscordClient_SlashCommandExecuted; + inst.DiscordClient.ShardReady += DiscordClient_ShardReady; + } + + private async Task DiscordClient_ShardReady(DiscordSocketClient arg) { +#if !DEBUG + // Update our commands here, only when the first shard connects + if (arg.ShardId != 0) return; +#endif + var cmds = new ApplicationCommandProperties[] { + new SlashCommandBuilder() + .WithName("help").WithDescription(HelpHelp).Build(), + new SlashCommandBuilder() + .WithName("list") + .WithDescription(HelpList) + .AddOption("user", ApplicationCommandOptionType.User, "A specific user whose time to look up.", isRequired: false) + .Build(), + new SlashCommandBuilder() + .WithName("set") + .WithDescription(HelpSet) + .AddOption("zone", ApplicationCommandOptionType.String, "The new time zone to set.", isRequired: true) + .Build(), + new SlashCommandBuilder() + .WithName("set-for") + .WithDescription(HelpSetFor) + .AddOption("user", ApplicationCommandOptionType.User, "The user whose time zone to modify.", isRequired: true) + .AddOption("zone", ApplicationCommandOptionType.String, "The new time zone to set.", isRequired: true) + .Build(), + new SlashCommandBuilder() + .WithName("remove").WithDescription(HelpRemove).Build(), + new SlashCommandBuilder() + .WithName("remove-for") + .WithDescription(HelpRemoveFor) + .AddOption("user", ApplicationCommandOptionType.User, "The user whose time zone to remove.", isRequired: true) + .Build() + }; +#if !DEBUG + // Remove any unneeded/unused commands + var existingcmdnames = cmds.Select(c => c.Name.Value).ToHashSet(); + foreach (var gcmd in await arg.GetGlobalApplicationCommandsAsync()) { + if (!existingcmdnames.Contains(gcmd.Name)) { + Program.Log("Command registration", $"Found registered unused command /{gcmd.Name} - sending removal request"); + await gcmd.DeleteAsync(); + } + } + // And update what we have + Program.Log("Command registration", $"Bulk updating {cmds.Length} global command(s)"); + await arg.BulkOverwriteGlobalApplicationCommandsAsync(cmds).ConfigureAwait(false); +#else + // Debug: Register our commands locally instead, in each guild we're in + foreach (var g in arg.Guilds) { + await g.DeleteApplicationCommandsAsync().ConfigureAwait(false); + await g.BulkOverwriteApplicationCommandAsync(cmds).ConfigureAwait(false); + } + + foreach (var gcmd in await arg.GetGlobalApplicationCommandsAsync()) { + Program.Log("Command registration", $"Found global command /{gcmd.Name} and we're DEBUG - sending removal request"); + await gcmd.DeleteAsync(); + } +#endif + } + + private async Task DiscordClient_SlashCommandExecuted(SocketSlashCommand arg) { + SocketGuildChannel? rptChannel = arg.Channel as SocketGuildChannel; + var rptId = rptChannel?.Guild.Id ?? arg.User.Id; + Program.Log("Command executed", $"/{arg.CommandName} by {arg.User} { (rptChannel != null ? "in guild" : "with ID") } {rptId}"); + + CommandResponder responder = arg.Data.Name switch { + "help" => CmdHelp, + "list" => CmdList, + "set" => CmdSet, + "set-for" => CmdSetFor, + "remove" => CmdRemove, + "remove-for" => CmdRemoveFor, + _ => UnknownCommandHandler + }; + try { + await responder(arg).ConfigureAwait(false); + } catch (Exception e) { + Program.Log("Command exception", e.ToString()); + // TODO respond with error message? + } + } + + private async Task UnknownCommandHandler(SocketSlashCommand arg) { + string place; + // Unknown command - set up a report + if (arg.Channel is SocketGuildChannel gch) { + place = $"Guild {gch.Guild.Id}"; + } else { + place = "Global command"; + } + Program.Log("Command invoked", $"/{arg.Data.Name} is an unknown command! Executed at: {place}"); + await arg.RespondAsync("Oops, that command isn't supposed to be there... Please try something else.", + ephemeral: true).ConfigureAwait(false); + } + + private async Task CmdHelp(SocketSlashCommand arg) { + var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version!.ToString(3); + var guildct = _instance.DiscordClient.Guilds.Count; + var uniquetz = await _database.GetDistinctZoneCountAsync(); + await arg.RespondAsync(embed: new EmbedBuilder() { + Title = "Help & About", + Description = $"World Time v{version} - Serving {guildct} communities across {uniquetz} time zones.\n\n" + + "This bot is provided for free, without any paywalled 'premium' features. " + + "If you've found this bot useful, please consider contributing via the " + + "bot author's page on Ko-fi: https://ko-fi.com/noithecat.", + Footer = new EmbedFooterBuilder() { + IconUrl = _instance.DiscordClient.CurrentUser.GetAvatarUrl(), + Text = "World Time" + } + }.AddField(inline: false, name: "Commands", value: EmbedHelpField1 + ).AddField(inline: false, name: "Admin commands", value: EmbedHelpField2 + ).AddField(inline: false, name: "Zones", value: + "This bot accepts zone names from the IANA Time Zone Database (a.k.a. Olson Database). " + + "A useful tool to determine yours can be found at: https://kevinnovak.github.io/Time-Zone-Picker/" + ).Build()); + } + + private async Task CmdList(SocketSlashCommand arg) { + if (arg.Channel is not SocketGuildChannel gc) { + await arg.RespondAsync(ErrGuildOnly).ConfigureAwait(false); + return; + } + + if (arg.Data.Options.FirstOrDefault()?.Value is SocketGuildUser parameter) { + await CmdListUser(arg, parameter); + return; + } + + var guild = gc.Guild; + if (!await AreUsersDownloadedAsync(guild).ConfigureAwait(false)) { + await arg.RespondAsync(ErrNoUserCache, ephemeral: true).ConfigureAwait(false); + return; + } + + var userlist = await _database.GetGuildZonesAsync(guild.Id).ConfigureAwait(false); + if (userlist.Count == 0) { + await arg.RespondAsync(":x: Nothing to show. " + + $"To register your time zone with the bot, use the `/set` command.").ConfigureAwait(false); + return; + } + // Order times by popularity to limit how many are shown, group by printed name + var sortedlist = new SortedDictionary>(); + foreach ((string area, List users) in userlist.OrderByDescending(o => o.Value.Count).Take(20)) { + // Filter further to top 20 distinct timezones, even if they are not displayed in the final result + var areaprint = TzPrint(area); + if (!sortedlist.ContainsKey(areaprint)) sortedlist.Add(areaprint, new List()); + sortedlist[areaprint].AddRange(users); + } + + // Build zone listings with users + var outputlines = new List(); + foreach ((string area, List users) in sortedlist) { + var buffer = new StringBuilder(); + buffer.Append(area[4..] + ": "); + bool empty = true; + foreach (var userid in users) { + var userinstance = guild.GetUser(userid); + if (userinstance == null) continue; + if (empty) empty = !empty; + else buffer.Append(", "); + var useradd = FormatName(userinstance); + if (buffer.Length + useradd.Length > MaxSingleLineLength) { + buffer.Append("others..."); + break; + } else buffer.Append(useradd); + } + if (!empty) outputlines.Add(buffer.ToString()); + } + + // Prepare for output - send buffers out if they become too large + outputlines.Sort(); + bool hasOutputOneLine = false; + // First output is shown as an interaction response, followed then as regular channel messages + async Task doOutput(Embed msg) { + if (!hasOutputOneLine) { + await arg.RespondAsync(embed: msg).ConfigureAwait(false); + hasOutputOneLine = true; + } else { + await arg.Channel.SendMessageAsync(embed: msg).ConfigureAwait(false); + } + } + + var resultout = new StringBuilder(); + foreach (var line in outputlines) { + if (resultout.Length + line.Length > MaxSingleOutputLength) { + await doOutput(new EmbedBuilder().WithDescription(resultout.ToString()).Build()).ConfigureAwait(false); + resultout.Clear(); + } + if (resultout.Length > 0) resultout.AppendLine(); // avoids trailing newline by adding to the previous line + resultout.Append(line); + } + if (resultout.Length > 0) { + await doOutput(new EmbedBuilder().WithDescription(resultout.ToString()).Build()).ConfigureAwait(false); + } + } + + private async Task CmdListUser(SocketSlashCommand arg, SocketGuildUser parameter) { + // Not meant as a command handler - called by CmdList + var result = await _database.GetUserZoneAsync(parameter).ConfigureAwait(false); + if (result == null) { + bool isself = arg.User.Id == parameter.Id; + if (isself) await arg.RespondAsync(":x: You do not have a time zone. Set it with `tz.set`.", ephemeral: true) + .ConfigureAwait(false); + else await arg.RespondAsync(":x: The given user does not have a time zone set.", ephemeral: true).ConfigureAwait(false); + return; + } + + var resulttext = TzPrint(result)[4..] + ": " + FormatName(parameter); + await arg.RespondAsync(embed: new EmbedBuilder().WithDescription(resulttext).Build()).ConfigureAwait(false); + } + + private async Task CmdSet(SocketSlashCommand arg) { + if (arg.Channel is not SocketGuildChannel) { + await arg.RespondAsync(ErrGuildOnly).ConfigureAwait(false); + return; + } + + var input = (string)arg.Data.Options.First().Value; + input = ParseTimeZone(input); + if (input == null) { + await arg.RespondAsync(ErrInvalidZone, ephemeral: true).ConfigureAwait(false); + return; + } + await _database.UpdateUserAsync((SocketGuildUser)arg.User, input).ConfigureAwait(false); + await arg.RespondAsync($":white_check_mark: Your time zone has been set to **{input}**.").ConfigureAwait(false); + } + + private async Task CmdSetFor(SocketSlashCommand arg) { + if (arg.Channel is not SocketGuildChannel) { + await arg.RespondAsync(ErrGuildOnly).ConfigureAwait(false); + return; + } + + if (!IsUserAdmin((SocketGuildUser)arg.User)) { + await arg.RespondAsync(ErrNotAllowed, ephemeral: true).ConfigureAwait(false); + return; + } + + // Extract parameters + var opts = arg.Data.Options.ToDictionary(o => o.Name, o => o); + var user = (SocketGuildUser)opts["user"].Value; + var zone = (string)opts["zone"].Value; + + var newtz = ParseTimeZone(zone); + if (newtz == null) { + await arg.RespondAsync(ErrInvalidZone).ConfigureAwait(false); + return; + } + + await _database.UpdateUserAsync(user, newtz).ConfigureAwait(false); + await arg.RespondAsync($":white_check_mark: Time zone for **{user}** set to **{newtz}**.").ConfigureAwait(false); + } + + private async Task CmdRemove(SocketSlashCommand arg) { + if (arg.Channel is not SocketGuildChannel) { + await arg.RespondAsync(ErrGuildOnly).ConfigureAwait(false); + return; + } + + var success = await _database.DeleteUserAsync((SocketGuildUser)arg.User).ConfigureAwait(false); + if (success) await arg.RespondAsync(":white_check_mark: Your zone has been removed.").ConfigureAwait(false); + else await arg.RespondAsync(":x: You don't have a time zone set.").ConfigureAwait(false); + } + + private async Task CmdRemoveFor(SocketSlashCommand arg) { + if (arg.Channel is not SocketGuildChannel) { + await arg.RespondAsync(ErrGuildOnly).ConfigureAwait(false); + return; + } + + if (!IsUserAdmin((SocketGuildUser)arg.User)) { + await arg.RespondAsync(ErrNotAllowed, ephemeral: true).ConfigureAwait(false); + return; + } + } +} diff --git a/Commands.cs b/CommandsText.cs similarity index 72% rename from Commands.cs rename to CommandsText.cs index 97c4e8e..01ec6bf 100644 --- a/Commands.cs +++ b/CommandsText.cs @@ -1,45 +1,27 @@ -using NodaTime; -using System.Collections.ObjectModel; -using System.Globalization; -using System.Text; +using System.Text; using System.Text.RegularExpressions; namespace WorldTime; -internal class Commands { +internal class CommandsText : CommandsCommon { #if DEBUG public const string CommandPrefix = "tt."; #else public const string CommandPrefix = "tz."; #endif - const string ErrInvalidZone = ":x: Not a valid zone name." - + " To find your time zone, refer to: ."; - const string ErrTargetUserNotFound = ":x: Unable to find the target user."; - const string ErrNoUserCache = ":warning: Please try the command again."; - const int MaxSingleLineLength = 750; - const int MaxSingleOutputLength = 900; - delegate Task Command(SocketTextChannel channel, SocketGuildUser sender, SocketMessage message); private readonly Dictionary _commands; - private readonly Database _database; - private readonly WorldTime _instance; - private static readonly ReadOnlyDictionary _tzNameMap; + private static readonly Regex _userExplicit; private static readonly Regex _userMention; - static Commands() { - Dictionary tzNameMap = new(StringComparer.OrdinalIgnoreCase); - foreach (var name in DateTimeZoneProviders.Tzdb.Ids) tzNameMap.Add(name, name); - _tzNameMap = new(tzNameMap); + static CommandsText() { _userExplicit = new Regex(@"(.+)#(\d{4})", RegexOptions.Compiled); _userMention = new Regex(@"\!?(\d+)>", RegexOptions.Compiled); } - public Commands(WorldTime inst, Database db) { - _instance = inst; - _database = db; - + public CommandsText(WorldTime inst, Database db) : base(db, inst) { _commands = new(StringComparer.OrdinalIgnoreCase) { { "help", CmdHelp }, { "list", CmdList }, @@ -262,60 +244,6 @@ internal class Commands { await channel.SendMessageAsync($":white_check_mark: Removed zone information for {targetuser}."); } - #region Helper methods - /// - /// Returns a string displaying the current time in the given time zone. - /// The result begins with four numbers for sorting purposes. Must be trimmed before output. - /// - private static string TzPrint(string zone) { - var tzdb = DateTimeZoneProviders.Tzdb; - DateTimeZone tz = tzdb.GetZoneOrNull(zone)!; - if (tz == null) throw new Exception("Encountered unknown time zone: " + zone); - - var now = SystemClock.Instance.GetCurrentInstant().InZone(tz); - var sortpfx = now.ToString("MMdd", DateTimeFormatInfo.InvariantInfo); - var fullstr = now.ToString("dd'-'MMM' 'HH':'mm' 'x' (UTC'o')'", DateTimeFormatInfo.InvariantInfo); - return $"{sortpfx}● `{fullstr}`"; - } - - /// - /// Checks given time zone input. Returns a valid string for use with NodaTime, or null. - /// - private static string? ParseTimeZone(string tzinput) { - if (tzinput.Equals("Asia/Calcutta", StringComparison.OrdinalIgnoreCase)) tzinput = "Asia/Kolkata"; - if (_tzNameMap.TryGetValue(tzinput, out var name)) return name; - return null; - } - - /// - /// Formats a user's name to a consistent, readable format which makes use of their nickname. - /// - private static string FormatName(SocketGuildUser user) { - static string escapeFormattingCharacters(string input) { - var result = new StringBuilder(); - foreach (var c in input) { - if (c is '\\' or '_' or '~' or '*' or '@') { - result.Append('\\'); - } - result.Append(c); - } - return result.ToString(); - } - - var username = escapeFormattingCharacters(user.Username); - if (user.Nickname != null) { - return $"**{escapeFormattingCharacters(user.Nickname)}** ({username}#{user.Discriminator})"; - } - return $"**{username}**#{user.Discriminator}"; - } - - /// - /// Checks if the given user can be considered a guild admin ('Manage Server' is set). - /// - private static bool IsUserAdmin(SocketGuildUser user) - => user.GuildPermissions.Administrator || user.GuildPermissions.ManageGuild; - // TODO port modrole feature from BB, implement in here - /// /// Given parameter input, attempts to find the corresponding SocketGuildUser. /// @@ -346,42 +274,4 @@ internal class Commands { return null; } - - /// - /// Checks if the member cache for the specified guild needs to be filled, and sends a request if needed. - /// - /// - /// Due to a quirk in Discord.Net, the user cache cannot be filled until the command handler is no longer executing, - /// regardless of if the request runs on its own thread. - /// - /// - /// True if the guild's members are already downloaded. If false, the command handler must notify the user. - /// - private static async Task AreUsersDownloadedAsync(SocketGuild guild) { - if (HasMostMembersDownloaded(guild)) return true; - else { - // Event handler hangs if awaited normally or used with Task.Run - await Task.Factory.StartNew(guild.DownloadUsersAsync).ConfigureAwait(false); - return false; - } - } - - /// - /// An alternative to . - /// Returns true if *most* members have been downloaded. - /// Used as a workaround check due to Discord.Net occasionally unable to actually download all members. - /// - /// Copied directly from BirthdayBot. Try to coordinate changes between projects... - private static bool HasMostMembersDownloaded(SocketGuild guild) { - if (guild.HasAllMembers) return true; - if (guild.MemberCount > 30) { - // For guilds of size over 30, require 85% or more of the members to be known - // (26/30, 42/50, 255/300, etc) - return guild.DownloadedMemberCount>= (int)(guild.MemberCount * 0.85); - } else { - // For smaller guilds, fail if two or more members are missing - return guild.MemberCount - guild.DownloadedMemberCount <= 2; - } - } - #endregion } diff --git a/WorldTime.cs b/WorldTime.cs index b286ef7..b09703b 100644 --- a/WorldTime.cs +++ b/WorldTime.cs @@ -25,7 +25,8 @@ internal class WorldTime : IDisposable { private readonly Task _statusTask; private readonly CancellationTokenSource _mainCancel; - private readonly Commands _commands; + private readonly CommandsSlash _commands; + private readonly CommandsText _commandsTxt; internal Configuration Config { get; } internal DiscordShardedClient DiscordClient { get; } @@ -48,7 +49,8 @@ internal class WorldTime : IDisposable { DiscordClient.Log += DiscordClient_Log; DiscordClient.ShardReady += DiscordClient_ShardReady; DiscordClient.MessageReceived += DiscordClient_MessageReceived; - _commands = new Commands(this, Database); + _commands = new CommandsSlash(this, Database); + _commandsTxt = new CommandsText(this, Database); // Start status reporting thread _mainCancel = new CancellationTokenSource(); @@ -146,7 +148,7 @@ internal class WorldTime : IDisposable { return Task.CompletedTask; } - private Task DiscordClient_ShardReady(DiscordSocketClient arg) => arg.SetGameAsync(Commands.CommandPrefix + "help"); + private Task DiscordClient_ShardReady(DiscordSocketClient arg) => arg.SetGameAsync(CommandsText.CommandPrefix + "help"); /// /// Non-specific handler for incoming events. diff --git a/WorldTime.csproj b/WorldTime.csproj index a328aa0..dd69993 100644 --- a/WorldTime.csproj +++ b/WorldTime.csproj @@ -5,16 +5,16 @@ net6.0 enable enable - 2.0.1 + 2.1.0 NoiTheCat - + - +