From 0c92f38c6cfee688ffdeaaffd324c50d781759c7 Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Tue, 1 Oct 2024 10:19:12 -0400 Subject: [PATCH 01/31] WIP: Update DSharpPlus to 5.0.0-nightly-02379 --- Cliptok.csproj | 5 +- CommandChecks/HomeServerPerms.cs | 14 +- CommandChecks/OwnerChecks.cs | 4 +- CommandChecks/UserRoleChecks.cs | 4 +- Commands/Announcements.cs | 8 +- Commands/Bans.cs | 23 +-- Commands/Debug.cs | 93 ++++++------ Commands/Dehoist.cs | 48 ++++--- Commands/DmRelayBlock.cs | 7 +- Commands/FunCmds.cs | 10 +- Commands/Grant.cs | 7 +- .../AnnouncementInteractions.cs | 85 ++++++----- .../InteractionCommands/BanInteractions.cs | 40 +++--- .../InteractionCommands/ClearInteractions.cs | 60 ++++---- .../InteractionCommands/ContextCommands.cs | 46 +++--- .../InteractionCommands/DebugInteractions.cs | 47 ++++--- .../DehoistInteractions.cs | 31 ++-- .../JoinwatchInteractions.cs | 29 ++-- .../LockdownInteractions.cs | 86 ++++++----- .../InteractionCommands/MuteInteractions.cs | 52 ++++--- .../NicknameLockInteraction.cs | 23 +-- .../RaidmodeInteractions.cs | 29 ++-- .../InteractionCommands/RoleInteractions.cs | 64 +++++---- .../InteractionCommands/RulesInteractions.cs | 21 +-- .../SecurityActionInteractions.cs | 18 ++- .../SlowmodeInteractions.cs | 14 +- .../InteractionCommands/StatusInteractions.cs | 39 ++--- .../TechSupportInteractions.cs | 33 +++-- .../TrackingInteractions.cs | 36 +++-- .../UserNoteInteractions.cs | 111 ++++++++------- .../WarningInteractions.cs | 133 ++++++++++-------- Commands/Kick.cs | 13 +- Commands/Lists.cs | 24 ++-- Commands/Lockdown.cs | 14 +- Commands/Mutes.cs | 13 +- Commands/Raidmode.cs | 21 +-- Commands/Reminders.cs | 7 +- Commands/SecurityActions.cs | 8 +- Commands/TechSupport.cs | 5 +- Commands/Threads.cs | 11 +- Commands/Timestamp.cs | 27 ++-- Commands/UserRoles.cs | 90 +++++++----- Commands/Utility.cs | 16 ++- Commands/Warnings.cs | 56 +++++--- Events/ErrorEvents.cs | 38 ++++- Events/InteractionEvents.cs | 31 ++-- GlobalUsings.cs | 16 ++- Helpers/InteractionHelpers.cs | 14 +- Program.cs | 42 +++--- 49 files changed, 961 insertions(+), 705 deletions(-) diff --git a/Cliptok.csproj b/Cliptok.csproj index 0f93087a..574260b2 100644 --- a/Cliptok.csproj +++ b/Cliptok.csproj @@ -13,9 +13,8 @@ - - - + + diff --git a/CommandChecks/HomeServerPerms.cs b/CommandChecks/HomeServerPerms.cs index 22f3a710..187a5170 100644 --- a/CommandChecks/HomeServerPerms.cs +++ b/CommandChecks/HomeServerPerms.cs @@ -66,7 +66,7 @@ public static async Task GetPermLevelAsync(DiscordMember target } [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public class RequireHomeserverPermAttribute : CheckBaseAttribute + public class RequireHomeserverPermAttribute : ContextCheckAttribute // TODO(#202): checks changed!! see the checks section of https://dsharpplus.github.io/DSharpPlus/articles/migration/slashcommands_to_commands.html { public ServerPermLevel TargetLvl { get; set; } public bool WorkOutside { get; set; } @@ -80,7 +80,7 @@ public RequireHomeserverPermAttribute(ServerPermLevel targetlvl, bool workOutsid TargetLvl = targetlvl; } - public override async Task ExecuteCheckAsync(CommandContext ctx, bool help) + public async Task ExecuteCheckAsync(CommandContext ctx, bool help) { // If the command is supposed to stay within the server and its being used outside, fail silently if (!WorkOutside && (ctx.Channel.IsPrivate || ctx.Guild.Id != Program.cfgjson.ServerID)) @@ -112,7 +112,7 @@ public override async Task ExecuteCheckAsync(CommandContext ctx, bool help if (level >= TargetLvl) return true; - else if (!help && ctx.Command.QualifiedName != "edit") + else if (!help && ctx.Command.FullName != "edit") { var levelText = level.ToString(); if (level == ServerPermLevel.Nothing && Program.rand.Next(1, 100) == 69) @@ -126,22 +126,22 @@ await ctx.RespondAsync( } } - public class HomeServerAttribute : CheckBaseAttribute + public class HomeServerAttribute : ContextCheckAttribute { - public override async Task ExecuteCheckAsync(CommandContext ctx, bool help) + public async Task ExecuteCheckAsync(CommandContext ctx) { return !ctx.Channel.IsPrivate && ctx.Guild.Id == Program.cfgjson.ServerID; } } - public class SlashRequireHomeserverPermAttribute : SlashCheckBaseAttribute + public class SlashRequireHomeserverPermAttribute : ContextCheckAttribute { public ServerPermLevel TargetLvl; public SlashRequireHomeserverPermAttribute(ServerPermLevel targetlvl) => TargetLvl = targetlvl; - public override async Task ExecuteChecksAsync(InteractionContext ctx) + public async Task ExecuteChecksAsync(CommandContext ctx) { if (ctx.Guild.Id != Program.cfgjson.ServerID) return false; diff --git a/CommandChecks/OwnerChecks.cs b/CommandChecks/OwnerChecks.cs index 66e7a301..cebcadb3 100644 --- a/CommandChecks/OwnerChecks.cs +++ b/CommandChecks/OwnerChecks.cs @@ -1,8 +1,8 @@ namespace Cliptok.CommandChecks { - public class IsBotOwnerAttribute : CheckBaseAttribute + public class IsBotOwnerAttribute : ContextCheckAttribute { - public override async Task ExecuteCheckAsync(CommandContext ctx, bool help) + public async Task ExecuteCheckAsync(CommandContext ctx, bool help) { if (Program.cfgjson.BotOwners.Contains(ctx.User.Id)) { diff --git a/CommandChecks/UserRoleChecks.cs b/CommandChecks/UserRoleChecks.cs index a03a6221..5dd03384 100644 --- a/CommandChecks/UserRoleChecks.cs +++ b/CommandChecks/UserRoleChecks.cs @@ -1,8 +1,8 @@ namespace Cliptok.CommandChecks { - public class UserRolesPresentAttribute : CheckBaseAttribute + public class UserRolesPresentAttribute : ContextCheckAttribute { - public override async Task ExecuteCheckAsync(CommandContext ctx, bool help) + public async Task ExecuteCheckAsync(CommandContext ctx) { return Program.cfgjson.UserRoles is not null; } diff --git a/Commands/Announcements.cs b/Commands/Announcements.cs index 042e200e..f5114215 100644 --- a/Commands/Announcements.cs +++ b/Commands/Announcements.cs @@ -1,13 +1,14 @@ namespace Cliptok.Commands { - internal class Announcements : BaseCommandModule + internal class Announcements { [Command("editannounce")] [Description("Edit an announcement, preserving the ping highlight.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [RequireHomeserverPerm(ServerPermLevel.Moderator)] public async Task EditAnnounce( - CommandContext ctx, + TextCommandContext ctx, [Description("The ID of the message to edit.")] ulong messageId, [Description("The short name for the role to ping.")] string roleName, [RemainingText, Description("The new message content, excluding the ping.")] string content @@ -40,8 +41,9 @@ public async Task EditAnnounce( [Command("announce")] [Description("Announces something in the current channel, pinging an Insider role in the process.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] - public async Task AnnounceCmd(CommandContext ctx, [Description("'canary', 'dev', 'beta', 'beta10', 'rp', 'rp10', 'patch', 'rpbeta', 'rpbeta10', 'betadev', 'candev'")] string roleName, [RemainingText, Description("The announcement message to send.")] string announcementMessage) + public async Task AnnounceCmd(TextCommandContext ctx, [Description("'canary', 'dev', 'beta', 'beta10', 'rp', 'rp10', 'patch', 'rpbeta', 'rpbeta10', 'betadev', 'candev'")] string roleName, [RemainingText, Description("The announcement message to send.")] string announcementMessage) { DiscordRole discordRole; diff --git a/Commands/Bans.cs b/Commands/Bans.cs index 4bc5e5c0..c17a5256 100644 --- a/Commands/Bans.cs +++ b/Commands/Bans.cs @@ -2,12 +2,13 @@ namespace Cliptok.Commands { - class Bans : BaseCommandModule + class Bans { [Command("massban")] - [Aliases("bigbonk")] + [TextAlias("bigbonk")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] - public async Task MassBanCmd(CommandContext ctx, [RemainingText] string input) + public async Task MassBanCmd(TextCommandContext ctx, [RemainingText] string input) { List usersString = input.Replace("\n", " ").Replace("\r", "").Split(' ').ToList(); @@ -21,7 +22,8 @@ public async Task MassBanCmd(CommandContext ctx, [RemainingText] string input) List> taskList = new(); int successes = 0; - var loading = await ctx.RespondAsync("Processing, please wait."); + await ctx.RespondAsync("Processing, please wait."); + var loading = await ctx.GetResponseAsync(); foreach (ulong user in users) { @@ -41,10 +43,11 @@ public async Task MassBanCmd(CommandContext ctx, [RemainingText] string input) } [Command("ban")] - [Aliases("tempban", "bonk", "isekaitruck")] + [TextAlias("tempban", "bonk", "isekaitruck")] [Description("Bans a user that you have permission to ban, deleting all their messages in the process. See also: bankeep.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.BanMembers)] - public async Task BanCmd(CommandContext ctx, + public async Task BanCmd(TextCommandContext ctx, [Description("The user you wish to ban. Accepts many formats")] DiscordUser targetMember, [RemainingText, Description("The time and reason for the ban. e.g. '14d trolling' NOTE: Add 'appeal' to the start of the reason to include an appeal link")] string timeAndReason = "No reason specified.") { @@ -133,9 +136,10 @@ public async Task BanCmd(CommandContext ctx, /// I CANNOT find a way to do this as alias so I made it a separate copy of the command. /// Sue me, I beg you. [Command("bankeep")] - [Aliases("bansave")] + [TextAlias("bansave")] [Description("Bans a user but keeps their messages around."), HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.BanMembers)] - public async Task BankeepCmd(CommandContext ctx, + [AllowedProcessors(typeof(TextCommandProcessor))] + public async Task BankeepCmd(TextCommandContext ctx, [Description("The user you wish to ban. Accepts many formats")] DiscordUser targetMember, [RemainingText, Description("The time and reason for the ban. e.g. '14d trolling' NOTE: Add 'appeal' to the start of the reason to include an appeal link")] string timeAndReason = "No reason specified.") { @@ -216,8 +220,9 @@ public async Task BankeepCmd(CommandContext ctx, [Command("unban")] [Description("Unbans a user who has been previously banned.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.BanMembers)] - public async Task UnbanCmd(CommandContext ctx, [Description("The user to unban, usually a mention or ID")] DiscordUser targetUser, [Description("Used in audit log only currently")] string reason = "No reason specified.") + public async Task UnbanCmd(TextCommandContext ctx, [Description("The user to unban, usually a mention or ID")] DiscordUser targetUser, [Description("Used in audit log only currently")] string reason = "No reason specified.") { if ((await Program.db.HashExistsAsync("bans", targetUser.Id))) { diff --git a/Commands/Debug.cs b/Commands/Debug.cs index 83ff050b..16bd2574 100644 --- a/Commands/Debug.cs +++ b/Commands/Debug.cs @@ -1,17 +1,20 @@ -namespace Cliptok.Commands +using DSharpPlus.Commands.Trees.Metadata; + +namespace Cliptok.Commands { - internal class Debug : BaseCommandModule + internal class Debug { public static Dictionary OverridesPendingAddition = new(); - [Group("debug")] - [Aliases("troubleshoot", "unbug", "bugn't", "helpsomethinghasgoneverywrong")] + [Command("debug")] + [TextAlias("troubleshoot", "unbug", "bugn't", "helpsomethinghasgoneverywrong")] [Description("Commands and things for fixing the bot in the unlikely event that it breaks a bit.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] - class DebugCmds : BaseCommandModule + class DebugCmds { [Command("mutestatus")] - public async Task MuteStatus(CommandContext ctx, DiscordUser targetUser = default) + public async Task MuteStatus(TextCommandContext ctx, DiscordUser targetUser = default) { if (targetUser == default) targetUser = ctx.User; @@ -20,9 +23,9 @@ public async Task MuteStatus(CommandContext ctx, DiscordUser targetUser = defaul } [Command("mutes")] - [Aliases("mute")] + [TextAlias("mute")] [Description("Debug the list of mutes.")] - public async Task MuteDebug(CommandContext ctx, DiscordUser targetUser = default) + public async Task MuteDebug(TextCommandContext ctx, DiscordUser targetUser = default) { await DiscordHelpers.SafeTyping(ctx.Channel); @@ -61,9 +64,9 @@ public async Task MuteDebug(CommandContext ctx, DiscordUser targetUser = default } [Command("bans")] - [Aliases("ban")] + [TextAlias("ban")] [Description("Debug the list of bans.")] - public async Task BanDebug(CommandContext ctx, DiscordUser targetUser = default) + public async Task BanDebug(TextCommandContext ctx, DiscordUser targetUser = default) { await DiscordHelpers.SafeTyping(ctx.Channel); @@ -101,7 +104,7 @@ public async Task BanDebug(CommandContext ctx, DiscordUser targetUser = default) [Command("restart")] [RequireHomeserverPerm(ServerPermLevel.Admin, ownerOverride: true), Description("Restart the bot. If not under Docker (Cliptok is, dw) this WILL exit instead.")] - public async Task Restart(CommandContext ctx) + public async Task Restart(TextCommandContext ctx) { await ctx.RespondAsync("Bot is restarting. Please hold."); Environment.Exit(1); @@ -109,7 +112,7 @@ public async Task Restart(CommandContext ctx) [Command("shutdown")] [RequireHomeserverPerm(ServerPermLevel.Admin, ownerOverride: true), Description("Panics and shuts the bot down. Check the arguments for usage.")] - public async Task Shutdown(CommandContext ctx, [Description("This MUST be set to \"I understand what I am doing\" for the command to work."), RemainingText] string verificationArgument) + public async Task Shutdown(TextCommandContext ctx, [Description("This MUST be set to \"I understand what I am doing\" for the command to work."), RemainingText] string verificationArgument) { if (verificationArgument == "I understand what I am doing") { @@ -126,9 +129,10 @@ public async Task Restart(CommandContext ctx) [Command("refresh")] [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] [Description("Manually run all the automatic actions.")] - public async Task Refresh(CommandContext ctx) + public async Task Refresh(TextCommandContext ctx) { - var msg = await ctx.RespondAsync("Checking for pending scheduled tasks..."); + await ctx.RespondAsync("Checking for pending scheduled tasks..."); + var msg = await ctx.GetResponseAsync(); bool bans = await Tasks.PunishmentTasks.CheckBansAsync(); bool mutes = await Tasks.PunishmentTasks.CheckMutesAsync(); bool warns = await Tasks.PunishmentTasks.CheckAutomaticWarningsAsync(); @@ -142,10 +146,10 @@ public async Task Refresh(CommandContext ctx) } [Command("sh")] - [Aliases("cmd")] + [TextAlias("cmd")] [IsBotOwner] [Description("Run shell commands! Bash for Linux/macOS, batch for Windows!")] - public async Task Shell(CommandContext ctx, [RemainingText] string command) + public async Task Shell(TextCommandContext ctx, [RemainingText] string command) { if (string.IsNullOrWhiteSpace(command)) { @@ -153,7 +157,8 @@ public async Task Shell(CommandContext ctx, [RemainingText] string command) return; } - DiscordMessage msg = await ctx.RespondAsync("executing.."); + await ctx.RespondAsync("executing.."); + DiscordMessage msg = await ctx.GetResponseAsync(); ShellResult finishedShell = RunShellCommand(command); string result = Regex.Replace(finishedShell.result, "ghp_[0-9a-zA-Z]{36}", "ghp_REDACTED").Replace(Environment.GetEnvironmentVariable("CLIPTOK_TOKEN"), "REDACTED").Replace(Environment.GetEnvironmentVariable("CLIPTOK_ANTIPHISHING_ENDPOINT") ?? "DUMMYVALUE", "REDACTED"); @@ -166,7 +171,7 @@ public async Task Shell(CommandContext ctx, [RemainingText] string command) } [Command("logs")] - public async Task Logs(CommandContext ctx) + public async Task Logs(TextCommandContext ctx) { if (Program.cfgjson.LogLevel is Level.Verbose) { @@ -189,7 +194,7 @@ public async Task Logs(CommandContext ctx) [Command("dumpwarnings"), Description("Dump all warning data. EXTREMELY computationally expensive, use with caution.")] [IsBotOwner] [RequireHomeserverPerm(ServerPermLevel.Moderator)] - public async Task MostWarningsCmd(CommandContext ctx) + public async Task MostWarningsCmd(TextCommandContext ctx) { await DiscordHelpers.SafeTyping(ctx.Channel); @@ -221,10 +226,10 @@ public async Task MostWarningsCmd(CommandContext ctx) } [Command("checkpendingchannelevents")] - [Aliases("checkpendingevents", "pendingevents")] + [TextAlias("checkpendingevents", "pendingevents")] [Description("Check pending events to handle in the Channel Update and Channel Delete handlers.")] [IsBotOwner] - public async Task CheckPendingChannelEvents(CommandContext ctx) + public async Task CheckPendingChannelEvents(TextCommandContext ctx) { var pendingUpdateEvents = Tasks.EventTasks.PendingChannelUpdateEvents; var pendingDeleteEvents = Tasks.EventTasks.PendingChannelDeleteEvents; @@ -259,12 +264,12 @@ public async Task CheckPendingChannelEvents(CommandContext ctx) await ctx.RespondAsync(await StringHelpers.CodeOrHasteBinAsync(list)); } - [Group("overrides")] + [Command("overrides")] [Description("Commands for managing stored permission overrides.")] - public class Overrides : BaseCommandModule + public class Overrides { - [GroupCommand] - public async Task ShowOverrides(CommandContext ctx, + [DefaultGroupCommand] + public async Task ShowOverrides(TextCommandContext ctx, [Description("The user whose overrides to show.")] DiscordUser user) { var userOverrides = await Program.db.HashGetAsync("overrides", user.Id.ToString()); @@ -314,7 +319,7 @@ await ctx.RespondAsync(new DiscordMessageBuilder().WithContent(response) [Command("import")] [Description("Import overrides from a channel to the database.")] - public async Task Import(CommandContext ctx, + public async Task Import(TextCommandContext ctx, [Description("The channel to import overrides from.")] DiscordChannel channel) { // Import overrides @@ -330,10 +335,11 @@ await ctx.RespondAsync( [Command("importall")] [Description("Import all overrides from all channels to the database.")] - public async Task ImportAll(CommandContext ctx) + public async Task ImportAll(TextCommandContext ctx) { - var msg = await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working..."); - + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working..."); + var msg = await ctx.GetResponseAsync(); + // Get all channels var channels = await ctx.Guild.GetChannelsAsync(); @@ -356,7 +362,7 @@ public async Task ImportAll(CommandContext ctx) [Command("add")] [Description("Insert an override into the db. Useful if you want to add an override for a user who has left.")] [IsBotOwner] - public async Task Add(CommandContext ctx, + public async Task Add(TextCommandContext ctx, [Description("The user to add an override for.")] DiscordUser user, [Description("The channel to add the override to.")] DiscordChannel channel, [Description("Allowed permissions. Use a permission integer. See https://discordlookup.com/permissions-calculator.")] int allowedPermissions, @@ -369,11 +375,12 @@ public async Task Add(CommandContext ctx, var confirmButton = new DiscordButtonComponent(DiscordButtonStyle.Success, "debug-overrides-add-confirm-callback", "Yes"); var cancelButton = new DiscordButtonComponent(DiscordButtonStyle.Danger, "debug-overrides-add-cancel-callback", "No"); - var confirmationMessage = await ctx.RespondAsync(new DiscordMessageBuilder().WithContent( + await ctx.RespondAsync(new DiscordMessageBuilder().WithContent( $"{Program.cfgjson.Emoji.ShieldHelp} Just to confirm, you want to add the following override for {user.Mention} to {channel.Mention}?\n" + $"**Allowed:** {parsedAllowedPerms}\n" + $"**Denied:** {parsedDeniedPerms}\n") .AddComponents([confirmButton, cancelButton])); + var confirmationMessage = await ctx.GetResponseAsync(); OverridesPendingAddition.Add(confirmationMessage.Id, new PendingUserOverride { @@ -389,7 +396,7 @@ public async Task Add(CommandContext ctx, [Command("remove")] [Description("Remove a user's overrides for a channel from the database.")] - public async Task Remove(CommandContext ctx, + public async Task Remove(TextCommandContext ctx, [Description("The user whose overrides to remove.")] DiscordUser user, [Description("The channel to remove overrides from.")] DiscordChannel channel) { @@ -421,11 +428,12 @@ await Program.db.HashSetAsync("overrides", user.Id, [Command("apply")] [Description("Apply a user's overrides from the db.")] [IsBotOwner] - public async Task Apply(CommandContext ctx, + public async Task Apply(TextCommandContext ctx, [Description("The user whose overrides to apply.")] DiscordUser user) { - var msg = await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it..."); - + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it..."); + var msg = await ctx.GetResponseAsync(); + // Try fetching member to determine whether they are in the server. If they are not, we can't apply overrides for them. DiscordMember member; try @@ -485,7 +493,7 @@ public async Task Apply(CommandContext ctx, [Command("dumpchanneloverrides")] [Description("Dump all of a channel's overrides. This pulls from Discord, not the database.")] [IsBotOwner] - public async Task DumpChannelOverrides(CommandContext ctx, + public async Task DumpChannelOverrides(TextCommandContext ctx, [Description("The channel to dump overrides for.")] DiscordChannel channel) { var overwrites = channel.PermissionOverwrites; @@ -502,7 +510,7 @@ public async Task DumpChannelOverrides(CommandContext ctx, [Command("dmchannel")] [Description("Create or find a DM channel ID for a user.")] [IsBotOwner] - public async Task GetDMChannel(CommandContext ctx, DiscordUser user) + public async Task GetDMChannel(TextCommandContext ctx, DiscordUser user) { var dmChannel = await user.CreateDmChannelAsync(); await ctx.RespondAsync(dmChannel.Id.ToString()); @@ -511,7 +519,7 @@ public async Task GetDMChannel(CommandContext ctx, DiscordUser user) [Command("dumpdmchannels")] [Description("Dump all DM channels")] [IsBotOwner] - public async Task DumpDMChannels(CommandContext ctx) + public async Task DumpDMChannels(TextCommandContext ctx) { var dmChannels = ctx.Client.PrivateChannels; @@ -523,11 +531,12 @@ public async Task DumpDMChannels(CommandContext ctx) [Command("searchmembers")] [Description("Search member list with a regex. Restricted to bot owners bc regexes are scary.")] [IsBotOwner] - public async Task SearchMembersCmd(CommandContext ctx, string regex) + public async Task SearchMembersCmd(TextCommandContext ctx, string regex) { var rx = new Regex(regex); - var msg = await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it. This will take a while."); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it. This will take a while."); + var msg = await ctx.GetResponseAsync(); var discordMembers = await ctx.Guild.GetAllMembersAsync().ToListAsync(); var matchedMembers = discordMembers.Where(discordMember => discordMember.Username is not null && rx.IsMatch(discordMember.Username)).ToList(); @@ -540,9 +549,9 @@ public async Task SearchMembersCmd(CommandContext ctx, string regex) [Command("rawmessage")] [Description("Dumps the raw data for a message.")] - [Aliases("rawmsg")] + [TextAlias("rawmsg")] [IsBotOwner] - public async Task DumpRawMessage(CommandContext ctx, [Description("The message whose raw data to get.")] string msgLinkOrId) + public async Task DumpRawMessage(TextCommandContext ctx, [Description("The message whose raw data to get.")] string msgLinkOrId) { DiscordMessage message; if (Constants.RegexConstants.discord_link_rx.IsMatch(msgLinkOrId)) diff --git a/Commands/Dehoist.cs b/Commands/Dehoist.cs index 3e1a61b5..5fbc3cf9 100644 --- a/Commands/Dehoist.cs +++ b/Commands/Dehoist.cs @@ -1,11 +1,14 @@ -namespace Cliptok.Commands +using DSharpPlus.Commands.Trees.Metadata; + +namespace Cliptok.Commands { - internal class Dehoist : BaseCommandModule + internal class Dehoist { [Command("dehoist")] [Description("Adds an invisible character to someone's nickname that drops them to the bottom of the member list. Accepts multiple members.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public async Task DehoistCmd(CommandContext ctx, [Description("List of server members to dehoist")] params DiscordMember[] discordMembers) + public async Task DehoistCmd(TextCommandContext ctx, [Description("List of server members to dehoist")] params DiscordMember[] discordMembers) { if (discordMembers.Length == 0) { @@ -35,7 +38,8 @@ await discordMembers[0].ModifyAsync(a => return; } - var msg = await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it..."); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it..."); + var msg = await ctx.GetResponseAsync(); int failedCount = 0; foreach (DiscordMember discordMember in discordMembers) @@ -67,10 +71,12 @@ await discordMember.ModifyAsync(a => [Command("massdehoist")] [Description("Dehoist everyone on the server who has a bad name. This may take a while and can exhaust rate limits.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] - public async Task MassDehoist(CommandContext ctx) + public async Task MassDehoist(TextCommandContext ctx) { - var msg = await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it. This will take a while."); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it. This will take a while."); + var msg = await ctx.GetResponseAsync(); var discordMembers = await ctx.Guild.GetAllMembersAsync().ToListAsync(); int failedCount = 0; @@ -87,8 +93,9 @@ public async Task MassDehoist(CommandContext ctx) [Command("massundehoist")] [Description("Remove the dehoist for users attached via a txt file.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] - public async Task MassUndhoist(CommandContext ctx) + public async Task MassUndhoist(TextCommandContext ctx) { int failedCount = 0; @@ -106,7 +113,8 @@ public async Task MassUndhoist(CommandContext ctx) var list = strList.Split(' '); - var msg = await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it. This will take a while."); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it. This will take a while."); + var msg = await ctx.GetResponseAsync(); foreach (string strID in list) { @@ -143,14 +151,15 @@ await member.ModifyAsync(a => } } - [Group("permadehoist")] + [Command("permadehoist")] [Description("Permanently/persistently dehoist members.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public class Permadehoist : BaseCommandModule + public class Permadehoist { // Toggle - [GroupCommand] - public async Task PermadehoistToggleCmd(CommandContext ctx, [Description("The member(s) to permadehoist.")] params DiscordUser[] discordUsers) + [DefaultGroupCommand] + public async Task PermadehoistToggleCmd(TextCommandContext ctx, [Description("The member(s) to permadehoist.")] params DiscordUser[] discordUsers) { if (discordUsers.Length == 0) { @@ -200,7 +209,8 @@ await ctx.RespondAsync(new DiscordMessageBuilder() // Toggle permadehoist for multiple members - var msg = await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it..."); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it..."); + var msg = await ctx.GetResponseAsync(); int failedCount = 0; foreach (var discordUser in discordUsers) @@ -215,7 +225,7 @@ await ctx.RespondAsync(new DiscordMessageBuilder() [Command("enable")] [Description("Permanently dehoist a member (or members). They will be automatically dehoisted until disabled.")] - public async Task PermadehoistEnableCmd(CommandContext ctx, [Description("The member(s) to permadehoist.")] params DiscordUser[] discordUsers) + public async Task PermadehoistEnableCmd(TextCommandContext ctx, [Description("The member(s) to permadehoist.")] params DiscordUser[] discordUsers) { if (discordUsers.Length == 0) { @@ -249,7 +259,8 @@ await ctx.RespondAsync(new DiscordMessageBuilder() // Permadehoist multiple members - var msg = await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it..."); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it..."); + var msg = await ctx.GetResponseAsync(); int failedCount = 0; foreach (var discordUser in discordUsers) @@ -264,7 +275,7 @@ await ctx.RespondAsync(new DiscordMessageBuilder() [Command("disable")] [Description("Disable permadehoist for a member (or members).")] - public async Task PermadehoistDisableCmd(CommandContext ctx, [Description("The member(s) to remove the permadehoist for.")] params DiscordUser[] discordUsers) + public async Task PermadehoistDisableCmd(TextCommandContext ctx, [Description("The member(s) to remove the permadehoist for.")] params DiscordUser[] discordUsers) { if (discordUsers.Length == 0) { @@ -298,7 +309,8 @@ await ctx.RespondAsync(new DiscordMessageBuilder() // Un-permadehoist multiple members - var msg = await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it..."); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it..."); + var msg = await ctx.GetResponseAsync(); int failedCount = 0; foreach (var discordUser in discordUsers) @@ -313,7 +325,7 @@ await ctx.RespondAsync(new DiscordMessageBuilder() [Command("status")] [Description("Check the status of permadehoist for a member.")] - public async Task PermadehoistStatus(CommandContext ctx, [Description("The member whose permadehoist status to check.")] DiscordUser discordUser) + public async Task PermadehoistStatus(TextCommandContext ctx, [Description("The member whose permadehoist status to check.")] DiscordUser discordUser) { if (await Program.db.SetContainsAsync("permadehoists", discordUser.Id)) await ctx.RespondAsync(new DiscordMessageBuilder() diff --git a/Commands/DmRelayBlock.cs b/Commands/DmRelayBlock.cs index 69c11e38..60cbe8fd 100644 --- a/Commands/DmRelayBlock.cs +++ b/Commands/DmRelayBlock.cs @@ -1,12 +1,13 @@ namespace Cliptok.Commands { - internal class DmRelayBlock : BaseCommandModule + internal class DmRelayBlock { [Command("dmrelayblock")] [Description("Stop a member's DMs from being relayed to the configured DM relay channel.")] - [Aliases("dmblock")] + [AllowedProcessors(typeof(TextCommandProcessor))] + [TextAlias("dmblock")] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public async Task DmRelayBlockCommand(CommandContext ctx, [Description("The member to stop relaying DMs from.")] DiscordUser user) + public async Task DmRelayBlockCommand(TextCommandContext ctx, [Description("The member to stop relaying DMs from.")] DiscordUser user) { // Only function in configured DM relay channel/thread; do nothing if in wrong channel if (ctx.Channel.Id != Program.cfgjson.DmLogChannelId && Program.cfgjson.LogChannels.All(a => a.Value.ChannelId != ctx.Channel.Id)) return; diff --git a/Commands/FunCmds.cs b/Commands/FunCmds.cs index 4769ed5d..3b717310 100644 --- a/Commands/FunCmds.cs +++ b/Commands/FunCmds.cs @@ -1,11 +1,12 @@ namespace Cliptok.Commands { - internal class FunCmds : BaseCommandModule + internal class FunCmds { [Command("tellraw")] [Description("Nothing of interest.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] - public async Task TellRaw(CommandContext ctx, [Description("???")] DiscordChannel discordChannel, [RemainingText, Description("???")] string output) + public async Task TellRaw(TextCommandContext ctx, [Description("???")] DiscordChannel discordChannel, [RemainingText, Description("???")] string output) { try { @@ -22,9 +23,10 @@ public async Task TellRaw(CommandContext ctx, [Description("???")] DiscordChanne [Command("no")] [Description("Makes Cliptok choose something for you. Outputs either Yes or No.")] - [Aliases("yes")] + [TextAlias("yes")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Tier5)] - public async Task No(CommandContext ctx) + public async Task No(TextCommandContext ctx) { List noResponses = new() { diff --git a/Commands/Grant.cs b/Commands/Grant.cs index 78207849..a3483028 100644 --- a/Commands/Grant.cs +++ b/Commands/Grant.cs @@ -1,12 +1,13 @@ namespace Cliptok.Commands { - internal class Grant : BaseCommandModule + internal class Grant { [Command("grant")] [Description("Grant a user access to the server, by giving them the Tier 1 role.")] - [Aliases("clipgrant", "verify")] + [TextAlias("grant", "clipgrant", "verify")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public async Task GrantCommand(CommandContext ctx, [Description("The member to grant Tier 1 role to.")] DiscordUser _) + public async Task GrantCommand(TextCommandContext ctx, [Description("The member to grant Tier 1 role to.")] DiscordUser _) { await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} This command is deprecated and no longer works. Please right click (or tap and hold on mobile) the user and click \"Verify Member\" if available."); } diff --git a/Commands/InteractionCommands/AnnouncementInteractions.cs b/Commands/InteractionCommands/AnnouncementInteractions.cs index 2a7907fc..43983327 100644 --- a/Commands/InteractionCommands/AnnouncementInteractions.cs +++ b/Commands/InteractionCommands/AnnouncementInteractions.cs @@ -1,37 +1,32 @@ namespace Cliptok.Commands.InteractionCommands { - internal class AnnouncementInteractions : ApplicationCommandModule + internal class AnnouncementInteractions { - [SlashCommand("announcebuild", "Announce a Windows Insider build in the current channel.", defaultPermission: false)] + [Command("announcebuild")] + [Description("Announce a Windows Insider build in the current channel.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator)] - [SlashCommandPermissions(DiscordPermissions.ModerateMembers)] - public async Task AnnounceBuildSlashCommand(InteractionContext ctx, - [Choice("Windows 10", 10)] - [Choice("Windows 11", 11)] - [Option("windows_version", "The Windows version to announce a build of. Must be either 10 or 11.")] long windowsVersion, - - [Option("build_number", "Windows build number, including any decimals (Decimals are optional). Do not include the word Build.")] string buildNumber, - - [Option("blog_link", "The link to the Windows blog entry relating to this build.")] string blogLink, - - [Choice("Canary Channel", "Canary")] - [Choice("Dev Channel", "Dev")] - [Choice("Beta Channel", "Beta")] - [Choice("Release Preview Channel", "RP")] - [Option("insider_role1", "The first insider role to ping.")] string insiderChannel1, - - [Choice("Canary Channel", "Canary")] - [Choice("Dev Channel", "Dev")] - [Choice("Beta Channel", "Beta")] - [Choice("Release Preview Channel", "RP")] - [Option("insider_role2", "The second insider role to ping.")] string insiderChannel2 = "", - - [Option("canary_create_new_thread", "Enable this option if you want to create a new Canary thread for some reason")] bool canaryCreateNewThread = false, - [Option("thread", "The thread to mention in the announcement.")] DiscordChannel threadChannel = default, - [Option("flavour_text", "Extra text appended on the end of the main line, replacing :WindowsInsider: or :Windows10:")] string flavourText = "", - [Option("autothread_name", "If no thread is given, create a thread with this name.")] string autothreadName = "Build {0} ({1})", - - [Option("lockdown", "Set 0 to not lock. Lock the channel for a certain period of time after announcing the build.")] string lockdownTime = "auto" + [RequirePermissions(DiscordPermissions.ModerateMembers)] + public async Task AnnounceBuildSlashCommand(SlashCommandContext ctx, + [SlashChoiceProvider(typeof(WindowsVersionChoiceProvider))] + [Parameter("windows_version"), Description("The Windows version to announce a build of. Must be either 10 or 11.")] long windowsVersion, + + [Parameter("build_number"), Description("Windows build number, including any decimals (Decimals are optional). Do not include the word Build.")] string buildNumber, + + [Parameter("blog_link"), Description("The link to the Windows blog entry relating to this build.")] string blogLink, + + [SlashChoiceProvider(typeof(WindowsInsiderChannelChoiceProvider))] + [Parameter("insider_role1"), Description("The first insider role to ping.")] string insiderChannel1, // TODO(#202): test choices!!! + + [SlashChoiceProvider(typeof(WindowsInsiderChannelChoiceProvider))] + [Parameter("insider_role2"), Description("The second insider role to ping.")] string insiderChannel2 = "", // TODO(#202): test choices!!! + + [Parameter("canary_create_new_thread"), Description("Enable this option if you want to create a new Canary thread for some reason")] bool canaryCreateNewThread = false, + [Parameter("thread"), Description("The thread to mention in the announcement.")] DiscordChannel threadChannel = default, + [Parameter("flavour_text"), Description("Extra text appended on the end of the main line, replacing :WindowsInsider: or :Windows10:")] string flavourText = "", + [Parameter("autothread_name"), Description("If no thread is given, create a thread with this name.")] string autothreadName = "Build {0} ({1})", + + [Parameter("lockdown"), Description("Set 0 to not lock. Lock the channel for a certain period of time after announcing the build.")] string lockdownTime = "auto" ) { if (Program.cfgjson.InsiderCommandLockedToChannel != 0 && ctx.Channel.Id != Program.cfgjson.InsiderCommandLockedToChannel) @@ -168,7 +163,7 @@ public async Task AnnounceBuildSlashCommand(InteractionContext ctx, await insiderRole2.ModifyAsync(mentionable: true); await ctx.RespondAsync(pingMsgString); - messageSent = await ctx.GetOriginalResponseAsync(); + messageSent = await ctx.GetResponseAsync(); await insiderRole1.ModifyAsync(mentionable: false); if (insiderChannel2 != "") @@ -202,7 +197,7 @@ public async Task AnnounceBuildSlashCommand(InteractionContext ctx, } await ctx.RespondAsync(noPingMsgString); - messageSent = await ctx.GetOriginalResponseAsync(); + messageSent = await ctx.GetResponseAsync(); } if (threadChannel == default) @@ -263,6 +258,32 @@ public async Task AnnounceBuildSlashCommand(InteractionContext ctx, await LockdownHelpers.LockChannelAsync(user: ctx.User, channel: ctx.Channel, duration: lockDuration); } } + + internal class WindowsVersionChoiceProvider : IChoiceProvider + { + public async ValueTask> ProvideAsync(CommandParameter _) + { + return new Dictionary + { + { "Windows 10", "10" }, + { "Windows 11", "11" } + }; + } + } + + internal class WindowsInsiderChannelChoiceProvider : IChoiceProvider + { + public async ValueTask> ProvideAsync(CommandParameter _) + { + return new Dictionary + { + { "Canary Channel", "Canary" }, + { "Dev Channel", "Dev" }, + { "Beta Channel", "Beta" }, + { "Release Preview Channel", "RP" } + }; + } + } } } diff --git a/Commands/InteractionCommands/BanInteractions.cs b/Commands/InteractionCommands/BanInteractions.cs index 64bcfbf8..7d9ce41c 100644 --- a/Commands/InteractionCommands/BanInteractions.cs +++ b/Commands/InteractionCommands/BanInteractions.cs @@ -2,21 +2,23 @@ namespace Cliptok.Commands.InteractionCommands { - internal class BanInteractions : ApplicationCommandModule + internal class BanInteractions { - [SlashCommand("ban", "Bans a user from the server, either permanently or temporarily.", defaultPermission: false)] - [SlashRequireHomeserverPerm(ServerPermLevel.Moderator), SlashCommandPermissions(DiscordPermissions.BanMembers)] - public async Task BanSlashCommand(InteractionContext ctx, - [Option("user", "The user to ban")] DiscordUser user, - [Option("reason", "The reason the user is being banned")] string reason, - [Option("keep_messages", "Whether to keep the users messages when banning")] bool keepMessages = false, - [Option("time", "The length of time the user is banned for")] string time = null, - [Option("appeal_link", "Whether to show the user an appeal URL in the DM")] bool appealable = false + [Command("ban")] + [Description("Bans a user from the server, either permanently or temporarily.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [SlashRequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.BanMembers)] + public async Task BanSlashCommand(SlashCommandContext ctx, + [Parameter("user"), Description("The user to ban")] DiscordUser user, + [Parameter("reason"), Description("The reason the user is being banned")] string reason, + [Parameter("keep_messages"), Description("Whether to keep the users messages when banning")] bool keepMessages = false, + [Parameter("time"), Description("The length of time the user is banned for")] string time = null, + [Parameter("appeal_link"), Description("Whether to show the user an appeal URL in the DM")] bool appealable = false ) { // Initial response to avoid the 3 second timeout, will edit later. var eout = new DiscordInteractionResponseBuilder().AsEphemeral(true); - await ctx.CreateResponseAsync(DiscordInteractionResponseType.DeferredChannelMessageWithSource, eout); + await ctx.DeferResponseAsync(); // TODO(#202): ephemeral // Edits need a webhook rather than interaction..? DiscordWebhookBuilder webhookOut = new(); @@ -55,7 +57,7 @@ public async Task BanSlashCommand(InteractionContext ctx, { try { - banDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(ctx.Interaction.CreationTimestamp.DateTime); + banDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(DateTime.UtcNow); // TODO(#202): this used InteractionContext#Interaction.CreationTimestamp.LocalDateTime before, please test!! } catch { @@ -112,9 +114,11 @@ public async Task BanSlashCommand(InteractionContext ctx, await ctx.EditResponseAsync(webhookOut); } - [SlashCommand("unban", "Unbans a user who has been previously banned.", defaultPermission: false)] - [SlashRequireHomeserverPerm(ServerPermLevel.Moderator), SlashCommandPermissions(DiscordPermissions.BanMembers)] - public async Task SlashUnbanCommand(InteractionContext ctx, [Option("user", "The ID or mention of the user to unban. Ignore the suggestions, IDs work.")] SnowflakeObject userId, [Option("reason", "Used in audit log only currently")] string reason = "No reason specified.") + [Command("unban")] + [Description("Unbans a user who has been previously banned.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [SlashRequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.BanMembers)] + public async Task SlashUnbanCommand(SlashCommandContext ctx, [Parameter("user"), Description("The ID or mention of the user to unban. Ignore the suggestions, IDs work.")] SnowflakeObject userId, [Parameter("reason"), Description("Used in audit log only currently")] string reason = "No reason specified.") { DiscordUser targetUser = default; try @@ -143,9 +147,11 @@ public async Task SlashUnbanCommand(InteractionContext ctx, [Option("user", "The } } - [SlashCommand("kick", "Kicks a user, removing them from the server until they rejoin.", defaultPermission: false)] - [SlashRequireHomeserverPerm(ServerPermLevel.Moderator), SlashCommandPermissions(DiscordPermissions.KickMembers)] - public async Task KickCmd(InteractionContext ctx, [Option("user", "The user you want to kick from the server.")] DiscordUser target, [Option("reason", "The reason for kicking this user.")] string reason = "No reason specified.") + [Command("kick")] + [Description("Kicks a user, removing them from the server until they rejoin.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [SlashRequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.KickMembers)] + public async Task KickCmd(SlashCommandContext ctx, [Parameter("user"), Description("The user you want to kick from the server.")] DiscordUser target, [Parameter("reason"), Description("The reason for kicking this user.")] string reason = "No reason specified.") { if (target.IsBot) { diff --git a/Commands/InteractionCommands/ClearInteractions.cs b/Commands/InteractionCommands/ClearInteractions.cs index 1d8c8233..bcc6ec21 100644 --- a/Commands/InteractionCommands/ClearInteractions.cs +++ b/Commands/InteractionCommands/ClearInteractions.cs @@ -1,37 +1,39 @@ namespace Cliptok.Commands.InteractionCommands { - public class ClearInteractions : ApplicationCommandModule + public class ClearInteractions { public static Dictionary> MessagesToClear = new(); - [SlashCommand("clear", "Delete many messages from the current channel.", defaultPermission: false)] - [HomeServer, SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequireBotPermissions(DiscordPermissions.ManageMessages), SlashCommandPermissions(DiscordPermissions.ModerateMembers)] - public async Task ClearSlashCommand(InteractionContext ctx, - [Option("count", "The number of messages to consider for deletion. Required if you don't use the 'up_to' argument.")] long count = 0, - [Option("up_to", "Optionally delete messages up to (not including) this one. Accepts IDs and links.")] string upTo = "", - [Option("user", "Optionally filter the deletion to a specific user.")] DiscordUser user = default, - [Option("ignore_mods", "Optionally filter the deletion to only messages sent by users who are not Moderators.")] bool ignoreMods = false, - [Option("match", "Optionally filter the deletion to only messages containing certain text.")] string match = "", - [Option("bots_only", "Optionally filter the deletion to only bots.")] bool botsOnly = false, - [Option("humans_only", "Optionally filter the deletion to only humans.")] bool humansOnly = false, - [Option("attachments_only", "Optionally filter the deletion to only messages with attachments.")] bool attachmentsOnly = false, - [Option("stickers_only", "Optionally filter the deletion to only messages with stickers.")] bool stickersOnly = false, - [Option("links_only", "Optionally filter the deletion to only messages containing links.")] bool linksOnly = false, - [Option("dry_run", "Don't actually delete the messages, just output what would be deleted.")] bool dryRun = false + [Command("clear")] + [Description("Delete many messages from the current channel.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [HomeServer, SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ManageMessages, DiscordPermissions.ModerateMembers)] + public async Task ClearSlashCommand(SlashCommandContext ctx, + [Parameter("count"), Description("The number of messages to consider for deletion. Required if you don't use the 'up_to' argument.")] long count = 0, + [Parameter("up_to"), Description("Optionally delete messages up to (not including) this one. Accepts IDs and links.")] string upTo = "", + [Parameter("user"), Description("Optionally filter the deletion to a specific user.")] DiscordUser user = default, + [Parameter("ignore_mods"), Description("Optionally filter the deletion to only messages sent by users who are not Moderators.")] bool ignoreMods = false, + [Parameter("match"), Description("Optionally filter the deletion to only messages containing certain text.")] string match = "", + [Parameter("bots_only"), Description("Optionally filter the deletion to only bots.")] bool botsOnly = false, + [Parameter("humans_only"), Description("Optionally filter the deletion to only humans.")] bool humansOnly = false, + [Parameter("attachments_only"), Description("Optionally filter the deletion to only messages with attachments.")] bool attachmentsOnly = false, + [Parameter("stickers_only"), Description("Optionally filter the deletion to only messages with stickers.")] bool stickersOnly = false, + [Parameter("links_only"), Description("Optionally filter the deletion to only messages containing links.")] bool linksOnly = false, + [Parameter("dry_run"), Description("Don't actually delete the messages, just output what would be deleted.")] bool dryRun = false ) { - await ctx.DeferAsync(ephemeral: !dryRun); + await ctx.DeferResponseAsync(ephemeral: !dryRun); // If all args are unset if (count == 0 && upTo == "" && user == default && ignoreMods == false && match == "" && botsOnly == false && humansOnly == false && attachmentsOnly == false && stickersOnly == false && linksOnly == false) { - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} You must provide at least one argument! I need to know which messages to delete.").AsEphemeral(true)); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} You must provide at least one argument! I need to know which messages to delete.").AsEphemeral(true)); return; } if (count == 0 && upTo == "") { - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} I need to know how many messages to delete! Please provide a value for `count` or `up_to`.").AsEphemeral(true)); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} I need to know how many messages to delete! Please provide a value for `count` or `up_to`.").AsEphemeral(true)); return; } @@ -39,13 +41,13 @@ public async Task ClearSlashCommand(InteractionContext ctx, if (count < 0) { - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} I can't delete a negative number of messages! Try setting `count` to a positive number.").AsEphemeral(true)); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} I can't delete a negative number of messages! Try setting `count` to a positive number.").AsEphemeral(true)); return; } if (count >= 1000) { - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} Deleting that many messages poses a risk of something disastrous happening, so I'm refusing your request, sorry.").AsEphemeral(true)); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} Deleting that many messages poses a risk of something disastrous happening, so I'm refusing your request, sorry.").AsEphemeral(true)); return; } @@ -53,7 +55,7 @@ public async Task ClearSlashCommand(InteractionContext ctx, if (upTo != "" && count != 0) { - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} You can't provide both a count of messages and a message to delete up to! Please only provide one of the two arguments.").AsEphemeral(true)); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} You can't provide both a count of messages and a message to delete up to! Please only provide one of the two arguments.").AsEphemeral(true)); return; } @@ -71,7 +73,7 @@ public async Task ClearSlashCommand(InteractionContext ctx, { if (!ulong.TryParse(upTo, out messageId)) { - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} That doesn't look like a valid message ID or link! Please try again.")); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} That doesn't look like a valid message ID or link! Please try again.")); return; } } @@ -82,7 +84,7 @@ public async Task ClearSlashCommand(InteractionContext ctx, || !ulong.TryParse(Constants.RegexConstants.discord_link_rx.Match(upTo).Groups[3].Value, out messageId) ) { - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} Please provide a valid link to a message in this channel!").AsEphemeral(true)); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} Please provide a valid link to a message in this channel!").AsEphemeral(true)); return; } } @@ -159,7 +161,7 @@ public async Task ClearSlashCommand(InteractionContext ctx, { if (humansOnly) { - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} You can't use `bots_only` and `humans_only` together! Pick one or the other please.").AsEphemeral(true)); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} You can't use `bots_only` and `humans_only` together! Pick one or the other please.").AsEphemeral(true)); return; } @@ -234,7 +236,7 @@ public async Task ClearSlashCommand(InteractionContext ctx, if (messagesToClear.Count == 0 && skipped) { - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} All of the messages to delete are older than 2 weeks, so I can't delete them!").AsEphemeral(true)); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} All of the messages to delete are older than 2 weeks, so I can't delete them!").AsEphemeral(true)); return; } @@ -245,7 +247,7 @@ public async Task ClearSlashCommand(InteractionContext ctx, var msg = await LogChannelHelper.CreateDumpMessageAsync($"{Program.cfgjson.Emoji.Information} **{messagesToClear.Count}** messages would have been deleted, but are instead logged below.", messagesToClear, ctx.Channel); - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent(msg.Content).AddFiles(msg.Files).AddEmbeds(msg.Embeds).AsEphemeral(false)); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent(msg.Content).AddFiles(msg.Files).AddEmbeds(msg.Embeds).AsEphemeral(false)); return; } @@ -253,7 +255,7 @@ public async Task ClearSlashCommand(InteractionContext ctx, if (messagesToClear.Count >= 50) { DiscordButtonComponent confirmButton = new(DiscordButtonStyle.Danger, "clear-confirm-callback", "Delete Messages"); - DiscordMessage confirmationMessage = await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Muted} You're about to delete {messagesToClear.Count} messages. Are you sure?").AddComponents(confirmButton).AsEphemeral(true)); + DiscordMessage confirmationMessage = await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Muted} You're about to delete {messagesToClear.Count} messages. Are you sure?").AddComponents(confirmButton).AsEphemeral(true)); MessagesToClear.Add(confirmationMessage.Id, messagesToClear); } @@ -275,11 +277,11 @@ await LogChannelHelper.LogMessageAsync("mod", .WithContent($"{Program.cfgjson.Emoji.Deleted} **{messagesToClear.Count}** messages were cleared in {ctx.Channel.Mention} by {ctx.User.Mention}.") .WithAllowedMentions(Mentions.None) ); - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Success} Done!").AsEphemeral(true)); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Success} Done!").AsEphemeral(true)); } else { - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} There were no messages that matched all of the arguments you provided! Nothing to do.")); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} There were no messages that matched all of the arguments you provided! Nothing to do.")); } await LogChannelHelper.LogDeletedMessagesAsync( diff --git a/Commands/InteractionCommands/ContextCommands.cs b/Commands/InteractionCommands/ContextCommands.cs index 19cb4de4..6fb48819 100644 --- a/Commands/InteractionCommands/ContextCommands.cs +++ b/Commands/InteractionCommands/ContextCommands.cs @@ -1,47 +1,57 @@ namespace Cliptok.Commands.InteractionCommands { - internal class ContextCommands : ApplicationCommandModule + internal class ContextCommands { - [ContextMenu(DiscordApplicationCommandType.UserContextMenu, "Show Avatar", defaultPermission: true)] - public async Task ContextAvatar(ContextMenuContext ctx) + [Command("Show Avatar")] + [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] + [AllowedProcessors(typeof(UserCommandProcessor))] + public async Task ContextAvatar(CommandContext ctx, DiscordUser targetUser) { - string avatarUrl = await LykosAvatarMethods.UserOrMemberAvatarURL(ctx.TargetUser, ctx.Guild); + string avatarUrl = await LykosAvatarMethods.UserOrMemberAvatarURL(targetUser, ctx.Guild); DiscordEmbedBuilder embed = new DiscordEmbedBuilder() .WithColor(new DiscordColor(0xC63B68)) .WithTimestamp(DateTime.UtcNow) .WithImageUrl(avatarUrl) .WithAuthor( - $"Avatar for {ctx.TargetUser.Username} (Click to open in browser)", + $"Avatar for {targetUser.Username} (Click to open in browser)", avatarUrl ); await ctx.RespondAsync(null, embed, ephemeral: true); } - [ContextMenu(DiscordApplicationCommandType.UserContextMenu, "Show Notes", defaultPermission: false)] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), SlashCommandPermissions(DiscordPermissions.ModerateMembers)] - public async Task ShowNotes(ContextMenuContext ctx) + [Command("Show Notes")] + [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] + [AllowedProcessors(typeof(UserCommandProcessor))] + [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] + public async Task ShowNotes(CommandContext ctx, DiscordUser targetUser) { - await ctx.RespondAsync(embed: await UserNoteHelpers.GenerateUserNotesEmbedAsync(ctx.TargetUser), ephemeral: true); + await ctx.RespondAsync(embed: await UserNoteHelpers.GenerateUserNotesEmbedAsync(targetUser), ephemeral: true); } - [ContextMenu(DiscordApplicationCommandType.UserContextMenu, "Show Warnings", defaultPermission: true)] - public async Task ContextWarnings(ContextMenuContext ctx) + [Command("Show Warnings")] + [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] + [AllowedProcessors(typeof(UserCommandProcessor))] + public async Task ContextWarnings(CommandContext ctx, DiscordUser targetUser) { - await ctx.RespondAsync(embed: await WarningHelpers.GenerateWarningsEmbedAsync(ctx.TargetUser), ephemeral: true); + await ctx.RespondAsync(embed: await WarningHelpers.GenerateWarningsEmbedAsync(targetUser), ephemeral: true); } - [ContextMenu(DiscordApplicationCommandType.UserContextMenu, "User Information", defaultPermission: true)] - public async Task ContextUserInformation(ContextMenuContext ctx) + [Command("User Information")] + [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] + [AllowedProcessors(typeof(UserCommandProcessor))] + public async Task ContextUserInformation(CommandContext ctx, DiscordUser targetUser) { - await ctx.RespondAsync(embed: await DiscordHelpers.GenerateUserEmbed(ctx.TargetUser, ctx.Guild), ephemeral: true); + await ctx.RespondAsync(embed: await DiscordHelpers.GenerateUserEmbed(targetUser, ctx.Guild), ephemeral: true); } - [ContextMenu(DiscordApplicationCommandType.UserContextMenu, "Hug", defaultPermission: true),] - public async Task Hug(ContextMenuContext ctx) + [Command("Hug")] + [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] + [AllowedProcessors(typeof(UserCommandProcessor))] + public async Task Hug(CommandContext ctx, DiscordUser targetUser) { - var user = ctx.TargetUser; + var user = targetUser; if (user is not null) { diff --git a/Commands/InteractionCommands/DebugInteractions.cs b/Commands/InteractionCommands/DebugInteractions.cs index 64cd5669..5400f357 100644 --- a/Commands/InteractionCommands/DebugInteractions.cs +++ b/Commands/InteractionCommands/DebugInteractions.cs @@ -2,12 +2,13 @@ namespace Cliptok.Commands.InteractionCommands { - internal class DebugInteractions : ApplicationCommandModule + internal class DebugInteractions { - [SlashCommand("scamcheck", "Check if a link or message is known to the anti-phishing API.", defaultPermission: false)] + [Command("scamcheck")] [Description("Check if a link or message is known to the anti-phishing API.")] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), SlashCommandPermissions(DiscordPermissions.ModerateMembers)] - public async Task ScamCheck(InteractionContext ctx, [Option("input", "Domain or message content to scan.")] string content) + [AllowedProcessors(typeof(SlashCommandProcessor))] + [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] + public async Task ScamCheck(SlashCommandContext ctx, [Parameter("input"), Description("Domain or message content to scan.")] string content) { var urlMatches = Constants.RegexConstants.url_rx.Matches(content); if (urlMatches.Count > 0 && Environment.GetEnvironmentVariable("CLIPTOK_ANTIPHISHING_ENDPOINT") is not null && Environment.GetEnvironmentVariable("CLIPTOK_ANTIPHISHING_ENDPOINT") != "useyourimagination") @@ -34,9 +35,11 @@ public async Task ScamCheck(InteractionContext ctx, [Option("input", "Domain or } } - [SlashCommand("tellraw", "You know what you're here for.", defaultPermission: false)] - [SlashRequireHomeserverPerm(ServerPermLevel.Moderator), SlashCommandPermissions(DiscordPermissions.ModerateMembers)] - public async Task TellRaw(InteractionContext ctx, [Option("input", "???")] string input, [Option("reply_msg_id", "ID of message to use in a reply context.")] string replyID = "0", [Option("pingreply", "Ping pong.")] bool pingreply = true, [Option("channel", "Either mention or ID. Not a name.")] string discordChannel = default) + [Command("tellraw")] + [Description("You know what you're here for.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [SlashRequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ModerateMembers)] + public async Task TellRaw(SlashCommandContext ctx, [Parameter("input"), Description("???")] string input, [Parameter("reply_msg_id"), Description("ID of message to use in a reply context.")] string replyID = "0", [Parameter("pingreply"), Description("Ping pong.")] bool pingreply = true, [Parameter("channel"), Description("Either mention or ID. Not a name.")] string discordChannel = default) { DiscordChannel channelObj = default; @@ -89,30 +92,36 @@ await LogChannelHelper.LogMessageAsync("secret", ); } - [SlashCommand("userinfo", "Retrieve information about a given user.")] - public async Task UserInfoSlashCommand(InteractionContext ctx, [Option("user", "The user to retrieve information about.")] DiscordUser user, [Option("public", "Whether to show the output publicly.")] bool publicMessage = false) + [Command("userinfo")] + [Description("Retrieve information about a given user.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + public async Task UserInfoSlashCommand(SlashCommandContext ctx, [Parameter("user"), Description("The user to retrieve information about.")] DiscordUser user, [Parameter("public"), Description("Whether to show the output publicly.")] bool publicMessage = false) { await ctx.RespondAsync(embed: await DiscordHelpers.GenerateUserEmbed(user, ctx.Guild), ephemeral: !publicMessage); } - [SlashCommand("muteinfo", "Show information about the mute for a user.")] + [Command("muteinfo")] + [Description("Show information about the mute for a user.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator)] - [SlashCommandPermissions(DiscordPermissions.ModerateMembers)] + [RequirePermissions(DiscordPermissions.ModerateMembers)] public async Task MuteInfoSlashCommand( - InteractionContext ctx, - [Option("user", "The user whose mute information to show.")] DiscordUser targetUser, - [Option("public", "Whether to show the output publicly. Default: false")] bool isPublic = false) + SlashCommandContext ctx, + [Parameter("user"), Description("The user whose mute information to show.")] DiscordUser targetUser, + [Parameter("public"), Description("Whether to show the output publicly. Default: false")] bool isPublic = false) { await ctx.RespondAsync(embed: await MuteHelpers.MuteStatusEmbed(targetUser, ctx.Guild), ephemeral: !isPublic); } - [SlashCommand("baninfo", "Show information about the ban for a user.")] + [Command("baninfo")] + [Description("Show information about the ban for a user.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator)] - [SlashCommandPermissions(DiscordPermissions.ModerateMembers)] + [RequirePermissions(DiscordPermissions.ModerateMembers)] public async Task BanInfoSlashCommand( - InteractionContext ctx, - [Option("user", "The user whose ban information to show.")] DiscordUser targetUser, - [Option("public", "Whether to show the output publicly. Default: false")] bool isPublic = false) + SlashCommandContext ctx, + [Parameter("user"), Description("The user whose ban information to show.")] DiscordUser targetUser, + [Parameter("public"), Description("Whether to show the output publicly. Default: false")] bool isPublic = false) { await ctx.RespondAsync(embed: await BanHelpers.BanStatusEmbed(targetUser, ctx.Guild), ephemeral: !isPublic); } diff --git a/Commands/InteractionCommands/DehoistInteractions.cs b/Commands/InteractionCommands/DehoistInteractions.cs index fec1389c..646e1020 100644 --- a/Commands/InteractionCommands/DehoistInteractions.cs +++ b/Commands/InteractionCommands/DehoistInteractions.cs @@ -1,10 +1,12 @@ namespace Cliptok.Commands.InteractionCommands { - internal class DehoistInteractions : ApplicationCommandModule + internal class DehoistInteractions { - [SlashCommand("dehoist", "Dehoist a member, dropping them to the bottom of the list. Lasts until they change nickname.", defaultPermission: false)] - [SlashRequireHomeserverPerm(ServerPermLevel.Moderator), SlashCommandPermissions(DiscordPermissions.ManageNicknames)] - public async Task DehoistSlashCmd(InteractionContext ctx, [Option("member", "The member to dehoist.")] DiscordUser user) + [Command("dehoist")] + [Description("Dehoist a member, dropping them to the bottom of the list. Lasts until they change nickname.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [SlashRequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ManageNicknames)] + public async Task DehoistSlashCmd(SlashCommandContext ctx, [Parameter("member"), Description("The member to dehoist.")] DiscordUser user) { DiscordMember member; try @@ -38,12 +40,15 @@ await member.ModifyAsync(a => await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Successfuly dehoisted {member.Mention}!", mentions: false); } - [SlashCommandGroup("permadehoist", "Permanently/persistently dehoist members.", defaultPermission: false)] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), SlashCommandPermissions(DiscordPermissions.ManageNicknames)] + [Command("permadehoist")] + [Description("Permanently/persistently dehoist members.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ManageNicknames)] public class PermadehoistSlashCommands { - [SlashCommand("enable", "Permanently dehoist a member. They will be automatically dehoisted until disabled.")] - public async Task PermadehoistEnableSlashCmd(InteractionContext ctx, [Option("member", "The member to permadehoist.")] DiscordUser discordUser) + [Command("enable")] + [Description("Permanently dehoist a member. They will be automatically dehoisted until disabled.")] + public async Task PermadehoistEnableSlashCmd(SlashCommandContext ctx, [Parameter("member"), Description("The member to permadehoist.")] DiscordUser discordUser) { var (success, isPermissionError) = await DehoistHelpers.PermadehoistMember(discordUser, ctx.User, ctx.Guild); @@ -57,8 +62,9 @@ public async Task PermadehoistEnableSlashCmd(InteractionContext ctx, [Option("me await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Failed to permadehoist {discordUser.Mention}!", mentions: false); } - [SlashCommand("disable", "Disable permadehoist for a member.")] - public async Task PermadehoistDisableSlashCmd(InteractionContext ctx, [Option("member", "The member to remove the permadehoist for.")] DiscordUser discordUser) + [Command("disable")] + [Description("Disable permadehoist for a member.")] + public async Task PermadehoistDisableSlashCmd(SlashCommandContext ctx, [Parameter("member"), Description("The member to remove the permadehoist for.")] DiscordUser discordUser) { var (success, isPermissionError) = await DehoistHelpers.UnpermadehoistMember(discordUser, ctx.User, ctx.Guild); @@ -72,8 +78,9 @@ public async Task PermadehoistDisableSlashCmd(InteractionContext ctx, [Option("m await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Failed to remove the permadehoist for {discordUser.Mention}!", mentions: false); } - [SlashCommand("status", "Check the status of permadehoist for a member.")] - public async Task PermadehoistStatusSlashCmd(InteractionContext ctx, [Option("member", "The member whose permadehoist status to check.")] DiscordUser discordUser) + [Command("status")] + [Description("Check the status of permadehoist for a member.")] + public async Task PermadehoistStatusSlashCmd(SlashCommandContext ctx, [Parameter("member"), Description("The member whose permadehoist status to check.")] DiscordUser discordUser) { if (await Program.db.SetContainsAsync("permadehoists", discordUser.Id)) await ctx.RespondAsync($"{Program.cfgjson.Emoji.On} {discordUser.Mention} is permadehoisted.", mentions: false); diff --git a/Commands/InteractionCommands/JoinwatchInteractions.cs b/Commands/InteractionCommands/JoinwatchInteractions.cs index 2bb23bfd..d112e94f 100644 --- a/Commands/InteractionCommands/JoinwatchInteractions.cs +++ b/Commands/InteractionCommands/JoinwatchInteractions.cs @@ -1,15 +1,18 @@ namespace Cliptok.Commands.InteractionCommands { - internal class JoinwatchInteractions : ApplicationCommandModule + internal class JoinwatchInteractions { - [SlashCommandGroup("joinwatch", "Watch for joins and leaves of a given user. Output goes to #investigations.", defaultPermission: false)] + [Command("joinwatch")] + [Description("Watch for joins and leaves of a given user. Output goes to #investigations.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator)] public class JoinwatchSlashCmds { - [SlashCommand("add", "Watch for joins and leaves of a given user. Output goes to #investigations.")] - public async Task JoinwatchAdd(InteractionContext ctx, - [Option("user", "The user to watch for joins and leaves of.")] DiscordUser user, - [Option("note", "An optional note for context.")] string note = "") + [Command("add")] + [Description("Watch for joins and leaves of a given user. Output goes to #investigations.")] + public async Task JoinwatchAdd(SlashCommandContext ctx, + [Parameter("user"), Description("The user to watch for joins and leaves of.")] DiscordUser user, + [Parameter("note"), Description("An optional note for context.")] string note = "") { var joinWatchlist = await Program.db.ListRangeAsync("joinWatchedUsers"); @@ -50,9 +53,10 @@ public async Task JoinwatchAdd(InteractionContext ctx, } } - [SlashCommand("remove", "Stop watching for joins and leaves of a user.")] - public async Task JoinwatchRemove(InteractionContext ctx, - [Option("user", "The user to stop watching for joins and leaves of.")] DiscordUser user) + [Command("remove")] + [Description("Stop watching for joins and leaves of a user.")] + public async Task JoinwatchRemove(SlashCommandContext ctx, + [Parameter("user"), Description("The user to stop watching for joins and leaves of.")] DiscordUser user) { var joinWatchlist = await Program.db.ListRangeAsync("joinWatchedUsers"); @@ -68,9 +72,10 @@ public async Task JoinwatchRemove(InteractionContext ctx, await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Successfully unwatched {user.Mention}!"); } - [SlashCommand("status", "Check the joinwatch status for a user.")] - public async Task JoinwatchStatus(InteractionContext ctx, - [Option("user", "The user whose joinwatch status to check.")] DiscordUser user) + [Command("status")] + [Description("Check the joinwatch status for a user.")] + public async Task JoinwatchStatus(SlashCommandContext ctx, + [Parameter("user"), Description("The user whose joinwatch status to check.")] DiscordUser user) { var joinWatchlist = await Program.db.ListRangeAsync("joinWatchedUsers"); diff --git a/Commands/InteractionCommands/LockdownInteractions.cs b/Commands/InteractionCommands/LockdownInteractions.cs index cd132bf8..eda4734b 100644 --- a/Commands/InteractionCommands/LockdownInteractions.cs +++ b/Commands/InteractionCommands/LockdownInteractions.cs @@ -1,27 +1,30 @@ namespace Cliptok.Commands.InteractionCommands { - class LockdownInteractions : ApplicationCommandModule + class LockdownInteractions { public static bool ongoingLockdown = false; - [SlashCommandGroup("lockdown", "Lock the current channel or all channels in the server, preventing new messages. See also: unlock")] - [HomeServer, SlashRequireHomeserverPerm(ServerPermLevel.Moderator), RequireBotPermissions(DiscordPermissions.ManageChannels)] + [Command("lockdown")] + [Description("Lock the current channel or all channels in the server, preventing new messages. See also: unlock")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [HomeServer, SlashRequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ManageChannels, DiscordPermissions.None)] public class LockdownCmds { - [SlashCommand("channel", "Lock the current channel. See also: unlock channel")] + [Command("channel")] + [Description("Lock the current channel. See also: unlock channel")] public async Task LockdownChannelCommand( - InteractionContext ctx, - [Option("reason", "The reason for the lockdown.")] string reason = "No reason specified.", - [Option("time", "The length of time to lock the channel for.")] string time = null, - [Option("lockthreads", "Whether to lock this channel's threads. Disables sending messages, but does not archive them.")] bool lockThreads = false) + SlashCommandContext ctx, + [Parameter("reason"), Description("The reason for the lockdown.")] string reason = "No reason specified.", + [Parameter("time"), Description("The length of time to lock the channel for.")] string time = null, + [Parameter("lockthreads"), Description("Whether to lock this channel's threads. Disables sending messages, but does not archive them.")] bool lockThreads = false) { - await ctx.DeferAsync(ephemeral: true); + await ctx.DeferResponseAsync(ephemeral: true); if (ctx.Channel.Type is DiscordChannelType.PublicThread or DiscordChannelType.PrivateThread or DiscordChannelType.NewsThread) { if (lockThreads) { - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Denied} You can't lock this channel!\n`/lockdown` with `lockthreads` cannot be used inside of a thread. If you meant to lock {ctx.Channel.Parent.Mention} and all of its threads, use the command there.\n\nIf you meant to only lock this thread, use `!lock` instead, or use `/lockdown` with `lockthreads` set to False.").AsEphemeral(true)); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Denied} You can't lock this channel!\n`/lockdown` with `lockthreads` cannot be used inside of a thread. If you meant to lock {ctx.Channel.Parent.Mention} and all of its threads, use the command there.\n\nIf you meant to only lock this thread, use `!lock` instead, or use `/lockdown` with `lockthreads` set to False.").AsEphemeral(true)); return; } @@ -33,7 +36,7 @@ await thread.ModifyAsync(a => a.Locked = true; }); - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent("Thread locked successfully!").AsEphemeral(true)); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Thread locked successfully!").AsEphemeral(true)); return; } @@ -41,46 +44,47 @@ await thread.ModifyAsync(a => if (!string.IsNullOrWhiteSpace(time)) { - lockDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(ctx.Interaction.CreationTimestamp.LocalDateTime); + lockDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(DateTime.Now); // TODO(#202): this used InteractionContext#Interaction.CreationTimestamp.DateTime before, please test!! } var currentChannel = ctx.Channel; if (!Program.cfgjson.LockdownEnabledChannels.Contains(currentChannel.Id)) { - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Denied} You can't lock or unlock this channel!\nIf this is in error, add its ID (`{currentChannel.Id}`) to the lockdown whitelist.")); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Denied} You can't lock or unlock this channel!\nIf this is in error, add its ID (`{currentChannel.Id}`) to the lockdown whitelist.")); return; } if (ongoingLockdown) { - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} A mass lockdown or unlock is already ongoing. Refusing your request to avoid conflicts, sorry.")); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} A mass lockdown or unlock is already ongoing. Refusing your request to avoid conflicts, sorry.")); return; } bool success = await LockdownHelpers.LockChannelAsync(user: ctx.User, channel: currentChannel, duration: lockDuration, reason: reason, lockThreads: lockThreads); if (success) - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent("Channel locked successfully.").AsEphemeral(true)); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Channel locked successfully.").AsEphemeral(true)); else - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent("Failed to lock this channel!").AsEphemeral(true)); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Failed to lock this channel!").AsEphemeral(true)); } - [SlashCommand("all", "Lock all lockable channels in the server. See also: unlock all")] + [Command("all")] + [Description("Lock all lockable channels in the server. See also: unlock all")] public async Task LockdownAllCommand( - InteractionContext ctx, - [Option("reason", "The reason for the lockdown.")] string reason = "", - [Option("time", "The length of time to lock the channels for.")] string time = null, - [Option("lockthreads", "Whether to lock threads. Disables sending messages, but does not archive them.")] bool lockThreads = false) + SlashCommandContext ctx, + [Parameter("reason"), Description("The reason for the lockdown.")] string reason = "", + [Parameter("time"), Description("The length of time to lock the channels for.")] string time = null, + [Parameter("lockthreads"), Description("Whether to lock threads. Disables sending messages, but does not archive them.")] bool lockThreads = false) { - await ctx.DeferAsync(); + await ctx.DeferResponseAsync(); ongoingLockdown = true; - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Loading} Working on it, please hold...")); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Loading} Working on it, please hold...")); TimeSpan? lockDuration = null; if (!string.IsNullOrWhiteSpace(time)) { - lockDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(ctx.Interaction.CreationTimestamp.LocalDateTime); + lockDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(DateTime.Now); // TODO(#202): this used InteractionContext#Interaction.CreationTimestamp.LocalDateTime before, please test!! } foreach (var chanID in Program.cfgjson.LockdownEnabledChannels) @@ -96,47 +100,51 @@ public async Task LockdownAllCommand( } } - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Success} Done!")); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Success} Done!")); ongoingLockdown = false; return; } } - [SlashCommandGroup("unlock", "Unlock the current channel or all channels in the server, allowing new messages. See also: lockdown")] - [HomeServer, SlashRequireHomeserverPerm(ServerPermLevel.Moderator), RequireBotPermissions(DiscordPermissions.ManageChannels)] + [Command("unlock")] + [Description("Unlock the current channel or all channels in the server, allowing new messages. See also: lockdown")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [HomeServer, SlashRequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ManageChannels, DiscordPermissions.None)] public class UnlockCmds { - [SlashCommand("channel", "Unlock the current channel. See also: lockdown")] - public async Task UnlockChannelCommand(InteractionContext ctx, [Option("reason", "The reason for the unlock.")] string reason = "") + [Command("channel")] + [Description("Unlock the current channel. See also: lockdown")] + public async Task UnlockChannelCommand(SlashCommandContext ctx, [Parameter("reason"), Description("The reason for the unlock.")] string reason = "") { - await ctx.DeferAsync(ephemeral: true); + await ctx.DeferResponseAsync(ephemeral: true); var currentChannel = ctx.Channel; if (!Program.cfgjson.LockdownEnabledChannels.Contains(currentChannel.Id)) { - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Denied} You can't lock or unlock this channel!\nIf this is in error, add its ID (`{currentChannel.Id}`) to the lockdown whitelist.").AsEphemeral(true)); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Denied} You can't lock or unlock this channel!\nIf this is in error, add its ID (`{currentChannel.Id}`) to the lockdown whitelist.").AsEphemeral(true)); return; } if (ongoingLockdown) { - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} A mass lockdown or unlock is already ongoing. Refusing your request. sorry.").AsEphemeral(true)); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} A mass lockdown or unlock is already ongoing. Refusing your request. sorry.").AsEphemeral(true)); return; } bool success = await LockdownHelpers.UnlockChannel(currentChannel, ctx.Member); if (success) - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent("Channel unlocked successfully.").AsEphemeral(true)); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Channel unlocked successfully.").AsEphemeral(true)); else - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent("Failed to unlock this channel!").AsEphemeral(true)); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Failed to unlock this channel!").AsEphemeral(true)); } - [SlashCommand("all", "Unlock all lockable channels in the server. See also: lockdown all")] - public async Task UnlockAllCommand(InteractionContext ctx, [Option("reason", "The reason for the unlock.")] string reason = "") + [Command("all")] + [Description("Unlock all lockable channels in the server. See also: lockdown all")] + public async Task UnlockAllCommand(SlashCommandContext ctx, [Parameter("reason"), Description("The reason for the unlock.")] string reason = "") { - await ctx.CreateResponseAsync(DiscordInteractionResponseType.DeferredChannelMessageWithSource); + await ctx.DeferResponseAsync(); ongoingLockdown = true; - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Loading} Working on it, please hold...")); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Loading} Working on it, please hold...")); foreach (var chanID in Program.cfgjson.LockdownEnabledChannels) { try @@ -149,7 +157,7 @@ public class UnlockCmds } } - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Success} Done!")); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Success} Done!")); ongoingLockdown = false; return; } diff --git a/Commands/InteractionCommands/MuteInteractions.cs b/Commands/InteractionCommands/MuteInteractions.cs index c9d95f7b..53a305b5 100644 --- a/Commands/InteractionCommands/MuteInteractions.cs +++ b/Commands/InteractionCommands/MuteInteractions.cs @@ -1,18 +1,20 @@ namespace Cliptok.Commands.InteractionCommands { - internal class MuteInteractions : ApplicationCommandModule + internal class MuteInteractions { - [SlashCommand("mute", "Mute a user, temporarily or permanently.")] + [Command("mute")] + [Description("Mute a user, temporarily or permanently.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator)] - [SlashCommandPermissions(DiscordPermissions.ModerateMembers)] + [RequirePermissions(DiscordPermissions.ModerateMembers)] public async Task MuteSlashCommand( - InteractionContext ctx, - [Option("user", "The user you wish to mute.")] DiscordUser targetUser, - [Option("reason", "The reason for the mute.")] string reason, - [Option("time", "The length of time to mute for.")] string time = "" + SlashCommandContext ctx, + [Parameter("user"), Description("The user you wish to mute.")] DiscordUser targetUser, + [Parameter("reason"), Description("The reason for the mute.")] string reason, + [Parameter("time"), Description("The length of time to mute for.")] string time = "" ) { - await ctx.DeferAsync(ephemeral: true); + await ctx.DeferResponseAsync(ephemeral: true); DiscordMember targetMember = default; try { @@ -35,7 +37,7 @@ public async Task MuteSlashCommand( { try { - muteDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(ctx.Interaction.CreationTimestamp.DateTime); + muteDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(DateTime.UtcNow); // TODO(#202): this used InteractionContext#Interaction.CreationTimestamp.DateTime before, please test!! } catch { @@ -48,16 +50,18 @@ public async Task MuteSlashCommand( await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent("Command completed successfully.")); } - [SlashCommand("unmute", "Unmute a user.")] + [Command("unmute")] + [Description("Unmute a user.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator)] - [SlashCommandPermissions(DiscordPermissions.ModerateMembers)] + [RequirePermissions(DiscordPermissions.ModerateMembers)] public async Task UnmuteSlashCommand( - InteractionContext ctx, - [Option("user", "The user you wish to mute.")] DiscordUser targetUser, - [Option("reason", "The reason for the unmute.")] string reason = "No reason specified." + SlashCommandContext ctx, + [Parameter("user"), Description("The user you wish to mute.")] DiscordUser targetUser, + [Parameter("reason"), Description("The reason for the unmute.")] string reason = "No reason specified." ) { - await ctx.DeferAsync(ephemeral: false); + await ctx.DeferResponseAsync(ephemeral: false); reason = $"[Manual unmute by {DiscordHelpers.UniqueUsername(ctx.User)}]: {reason}"; @@ -77,29 +81,31 @@ public async Task UnmuteSlashCommand( if ((await Program.db.HashExistsAsync("mutes", targetUser.Id)) || (member != default && member.Roles.Contains(mutedRole))) { await MuteHelpers.UnmuteUserAsync(targetUser, reason, true, ctx.User); - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Information} Successfully unmuted **{DiscordHelpers.UniqueUsername(targetUser)}**.")); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Information} Successfully unmuted **{DiscordHelpers.UniqueUsername(targetUser)}**.")); } else try { await MuteHelpers.UnmuteUserAsync(targetUser, reason, true, ctx.User); - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Warning} According to Discord that user is not muted, but I tried to unmute them anyway. Hope it works.")); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Warning} According to Discord that user is not muted, but I tried to unmute them anyway. Hope it works.")); } catch (Exception e) { Program.discord.Logger.LogError(e, "An error occurred unmuting {user}", targetUser.Id); - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} That user doesn't appear to be muted, *and* an error occurred while attempting to unmute them anyway. Please contact the bot owner, the error has been logged.")); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} That user doesn't appear to be muted, *and* an error occurred while attempting to unmute them anyway. Please contact the bot owner, the error has been logged.")); } } - [SlashCommand("tqsmute", "Temporarily mute a user in tech support channels.")] + [Command("tqsmute")] + [Description("Temporarily mute a user in tech support channels.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] [SlashRequireHomeserverPerm(ServerPermLevel.TechnicalQueriesSlayer)] public async Task TqsMuteSlashCommand( - InteractionContext ctx, - [Option("user", "The user to mute.")] DiscordUser targetUser, - [Option("reason", "The reason for the mute.")] string reason) + SlashCommandContext ctx, + [Parameter("user"), Description("The user to mute.")] DiscordUser targetUser, + [Parameter("reason"), Description("The reason for the mute.")] string reason) { - await ctx.DeferAsync(ephemeral: true); + await ctx.DeferResponseAsync(ephemeral: true); // only work if TQS mute role is configured if (Program.cfgjson.TqsMutedRole == 0) diff --git a/Commands/InteractionCommands/NicknameLockInteraction.cs b/Commands/InteractionCommands/NicknameLockInteraction.cs index af83509b..f64bf32c 100644 --- a/Commands/InteractionCommands/NicknameLockInteraction.cs +++ b/Commands/InteractionCommands/NicknameLockInteraction.cs @@ -6,14 +6,17 @@ namespace Cliptok.Commands.InteractionCommands { - public class NicknameLockInteraction : ApplicationCommandModule + public class NicknameLockInteraction { - [SlashCommandGroup("nicknamelock", "Prevent a member from changing their nickname.", defaultPermission: false)] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), SlashCommandPermissions(DiscordPermissions.ManageNicknames)] + [Command("nicknamelock")] + [Description("Prevent a member from changing their nickname.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ManageNicknames)] public class NicknameLockSlashCommands { - [SlashCommand("enable", "Prevent a member from changing their nickname.")] - public async Task NicknameLockEnableSlashCmd(InteractionContext ctx, [Option("member", "The member to nickname lock.")] DiscordUser discordUser) + [Command("enable")] + [Description("Prevent a member from changing their nickname.")] + public async Task NicknameLockEnableSlashCmd(SlashCommandContext ctx, [Parameter("member"), Description("The member to nickname lock.")] DiscordUser discordUser) { DiscordMember member = default; @@ -40,8 +43,9 @@ public async Task NicknameLockEnableSlashCmd(InteractionContext ctx, [Option("me } } - [SlashCommand("disable", "Allow a member to change their nickname again.")] - public async Task NicknameLockDisableSlashCmd(InteractionContext ctx, [Option("member", "The member to remove the nickname lock for.")] DiscordUser discordUser) + [Command("disable")] + [Description("Allow a member to change their nickname again.")] + public async Task NicknameLockDisableSlashCmd(SlashCommandContext ctx, [Parameter("member"), Description("The member to remove the nickname lock for.")] DiscordUser discordUser) { DiscordMember member = default; @@ -70,8 +74,9 @@ public async Task NicknameLockEnableSlashCmd(InteractionContext ctx, [Option("me } } - [SlashCommand("status", "Check the status of nickname lock for a member.")] - public async Task NicknameLockStatusSlashCmd(InteractionContext ctx, [Option("member", "The member whose nickname lock status to check.")] DiscordUser discordUser) + [Command("status")] + [Description("Check the status of nickname lock for a member.")] + public async Task NicknameLockStatusSlashCmd(SlashCommandContext ctx, [Parameter("member"), Description("The member whose nickname lock status to check.")] DiscordUser discordUser) { if ((await Program.db.HashGetAsync("nicknamelock", discordUser.Id)).HasValue) await ctx.RespondAsync($"{Program.cfgjson.Emoji.On} {discordUser.Mention} is nickname locked.", mentions: false); diff --git a/Commands/InteractionCommands/RaidmodeInteractions.cs b/Commands/InteractionCommands/RaidmodeInteractions.cs index 6998389d..7fa671c0 100644 --- a/Commands/InteractionCommands/RaidmodeInteractions.cs +++ b/Commands/InteractionCommands/RaidmodeInteractions.cs @@ -1,14 +1,17 @@ namespace Cliptok.Commands.InteractionCommands { - internal class RaidmodeInteractions : ApplicationCommandModule + internal class RaidmodeInteractions { - [SlashCommandGroup("raidmode", "Commands relating to Raidmode", defaultPermission: false)] + [Command("raidmode")] + [Description("Commands relating to Raidmode")] + [AllowedProcessors(typeof(SlashCommandProcessor))] [SlashRequireHomeserverPerm(ServerPermLevel.Moderator)] - [SlashCommandPermissions(DiscordPermissions.ModerateMembers)] - public class RaidmodeSlashCommands : ApplicationCommandModule + [RequirePermissions(DiscordPermissions.ModerateMembers)] + public class RaidmodeSlashCommands { - [SlashCommand("status", "Check the current state of raidmode.")] - public async Task RaidmodeStatus(InteractionContext ctx) + [Command("status")] + [Description("Check the current state of raidmode.")] + public async Task RaidmodeStatus(SlashCommandContext ctx) { if (Program.db.HashExists("raidmode", ctx.Guild.Id)) { @@ -30,10 +33,11 @@ public async Task RaidmodeStatus(InteractionContext ctx) } - [SlashCommand("on", "Enable raidmode. Defaults to 3 hour length if not specified.")] - public async Task RaidmodeOnSlash(InteractionContext ctx, - [Option("duration", "How long to keep raidmode enabled for.")] string duration = default, - [Option("allowed_account_age", "How old an account can be to be allowed to bypass raidmode. Relative to right now.")] string allowedAccountAge = "" + [Command("on")] + [Description("Enable raidmode. Defaults to 3 hour length if not specified.")] + public async Task RaidmodeOnSlash(SlashCommandContext ctx, + [Parameter("duration"), Description("How long to keep raidmode enabled for.")] string duration = default, + [Parameter("allowed_account_age"), Description("How old an account can be to be allowed to bypass raidmode. Relative to right now.")] string allowedAccountAge = "" ) { if (Program.db.HashExists("raidmode", ctx.Guild.Id)) @@ -96,8 +100,9 @@ public async Task RaidmodeOnSlash(InteractionContext ctx, } } - [SlashCommand("off", "Disable raidmode immediately.")] - public async Task RaidmodeOffSlash(InteractionContext ctx) + [Command("off")] + [Description("Disable raidmode immediately.")] + public async Task RaidmodeOffSlash(SlashCommandContext ctx) { if (Program.db.HashExists("raidmode", ctx.Guild.Id)) { diff --git a/Commands/InteractionCommands/RoleInteractions.cs b/Commands/InteractionCommands/RoleInteractions.cs index de51601b..5eccada4 100644 --- a/Commands/InteractionCommands/RoleInteractions.cs +++ b/Commands/InteractionCommands/RoleInteractions.cs @@ -1,30 +1,28 @@ namespace Cliptok.Commands.InteractionCommands { - internal class RoleInteractions : ApplicationCommandModule + internal class RoleInteractions { - [SlashCommand("grant", "Grant a user Tier 1, bypassing any verification requirements.", defaultPermission: false)] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), SlashCommandPermissions(DiscordPermissions.ModerateMembers)] - public async Task SlashGrant(InteractionContext ctx, [Option("user", "The user to grant Tier 1 to.")] DiscordUser _) + [Command("grant")] + [Description("Grant a user Tier 1, bypassing any verification requirements.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] + public async Task SlashGrant(SlashCommandContext ctx, [Parameter("user"), Description("The user to grant Tier 1 to.")] DiscordUser _) { await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} This command is deprecated and no longer works. Please right click (or tap and hold on mobile) the user and click \"Verify Member\" if available."); } [HomeServer] - [SlashCommandGroup("roles", "Opt in/out of roles.")] + [Command("roles")] + [Description("Opt in/out of roles.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] internal class RoleSlashCommands { - [SlashCommand("grant", "Opt into a role.")] + [Command("grant")] + [Description("Opt into a role.")] public async Task GrantRole( - InteractionContext ctx, - [Choice("Windows 11 Insiders (Canary)", "insiderCanary")] - [Choice("Windows 11 Insiders (Dev)", "insiderDev")] - [Choice("Windows 11 Insiders (Beta)", "insiderBeta")] - [Choice("Windows 11 Insiders (Release Preview)", "insiderRP")] - [Choice("Windows 10 Insiders (Release Preview)", "insider10RP")] - [Choice("Windows 10 Insiders (Beta)", "insider10Beta")] - [Choice("Patch Tuesday", "patchTuesday")] - [Choice("Giveaways", "giveaways")] - [Option("role", "The role to opt into.")] string role) + SlashCommandContext ctx, + [SlashChoiceProvider(typeof(RoleCommandChoiceProvider))] + [Parameter("role"), Description("The role to opt into.")] string role) // TODO(#202): test choices!!! { DiscordMember member = ctx.Member; @@ -47,18 +45,12 @@ public async Task GrantRole( await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} The role {roleData.Mention} has been successfully granted!", ephemeral: true, mentions: false); } - [SlashCommand("remove", "Opt out of a role.")] + [Command("remove")] + [Description("Opt out of a role.")] public async Task RemoveRole( - InteractionContext ctx, - [Choice("Windows 11 Insiders (Canary)", "insiderCanary")] - [Choice("Windows 11 Insiders (Dev)", "insiderDev")] - [Choice("Windows 11 Insiders (Beta)", "insiderBeta")] - [Choice("Windows 11 Insiders (Release Preview)", "insiderRP")] - [Choice("Windows 10 Insiders (Release Preview)", "insider10RP")] - [Choice("Windows 10 Insiders (Beta)", "insider10Beta")] - [Choice("Patch Tuesday", "patchTuesday")] - [Choice("Giveaways", "giveaways")] - [Option("role", "The role to opt out of.")] string role) + SlashCommandContext ctx, + [SlashChoiceProvider(typeof(RoleCommandChoiceProvider))] + [Parameter("role"), Description("The role to opt out of.")] string role) // TODO(#202): test choices!!! { DiscordMember member = ctx.Member; @@ -81,5 +73,23 @@ public async Task RemoveRole( await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} The role {roleData.Mention} has been successfully removed!", ephemeral: true, mentions: false); } } + + internal class RoleCommandChoiceProvider : IChoiceProvider + { + public async ValueTask> ProvideAsync(CommandParameter _) + { + return new Dictionary + { + { "Windows 11 Insiders (Canary)", "insiderCanary" }, + { "Windows 11 Insiders (Dev)", "insiderDev" }, + { "Windows 11 Insiders (Beta)", "insiderBeta" }, + { "Windows 11 Insiders (Release Preview)", "insiderRP" }, + { "Windows 10 Insiders (Release Preview)", "insider10RP" }, + { "Windows 10 Insiders (Beta)", "insider10Beta" }, + { "Patch Tuesday", "patchTuesday" }, + { "Giveaways", "giveaways" } + }; + } + } } } diff --git a/Commands/InteractionCommands/RulesInteractions.cs b/Commands/InteractionCommands/RulesInteractions.cs index edb6d938..71bada1f 100644 --- a/Commands/InteractionCommands/RulesInteractions.cs +++ b/Commands/InteractionCommands/RulesInteractions.cs @@ -1,13 +1,16 @@ namespace Cliptok.Commands.InteractionCommands { - public class RulesInteractions : ApplicationCommandModule + public class RulesInteractions { [HomeServer] - [SlashCommandGroup("rules", "Misc. commands related to server rules", defaultPermission: true)] + [Command("rules")] + [Description("Misc. commands related to server rules")] + [AllowedProcessors(typeof(SlashCommandProcessor))] internal class RulesSlashCommands { - [SlashCommand("all", "Shows all of the community rules.", defaultPermission: true)] - public async Task RulesAllCommand(InteractionContext ctx) + [Command("all")] + [Description("Shows all of the community rules.")] + public async Task RulesAllCommand(SlashCommandContext ctx) { List rules = default; @@ -34,8 +37,9 @@ public async Task RulesAllCommand(InteractionContext ctx) } - [SlashCommand("rule", "Shows a specific rule.", defaultPermission: true)] - public async Task RuleCommand(InteractionContext ctx, [Option("rule_number", "The rule number to show.")] long ruleNumber) + [Command("rule")] + [Description("Shows a specific rule.")] + public async Task RuleCommand(SlashCommandContext ctx, [Parameter("rule_number"), Description("The rule number to show.")] long ruleNumber) { IReadOnlyList rules = default; @@ -62,8 +66,9 @@ public async Task RuleCommand(InteractionContext ctx, [Option("rule_number", "Th await ctx.RespondAsync(embed: embed); } - [SlashCommand("search", "Search for a rule by keyword.", defaultPermission: true)] - public async Task RuleSearchCommand(InteractionContext ctx, [Option("keyword", "The keyword to search for.")] string keyword) + [Command("search")] + [Description("Search for a rule by keyword.")] + public async Task RuleSearchCommand(SlashCommandContext ctx, [Parameter("keyword"), Description("The keyword to search for.")] string keyword) { List rules = default; diff --git a/Commands/InteractionCommands/SecurityActionInteractions.cs b/Commands/InteractionCommands/SecurityActionInteractions.cs index 93fc18d7..6dfb6bb5 100644 --- a/Commands/InteractionCommands/SecurityActionInteractions.cs +++ b/Commands/InteractionCommands/SecurityActionInteractions.cs @@ -1,10 +1,12 @@ namespace Cliptok.Commands.InteractionCommands { - public class SecurityActionInteractions : ApplicationCommandModule + public class SecurityActionInteractions { - [SlashCommand("pausedms", "Temporarily pause DMs between server members.", defaultPermission: false)] - [HomeServer, SlashRequireHomeserverPerm(ServerPermLevel.Moderator), SlashCommandPermissions(DiscordPermissions.ModerateMembers)] - public async Task SlashPauseDMs(InteractionContext ctx, [Option("time", "The amount of time to pause DMs for. Cannot be greater than 24 hours.")] string time) + [Command("pausedms")] + [Description("Temporarily pause DMs between server members.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [HomeServer, SlashRequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ModerateMembers)] + public async Task SlashPauseDMs(SlashCommandContext ctx, [Parameter("time"), Description("The amount of time to pause DMs for. Cannot be greater than 24 hours.")] string time) { // need to make our own api calls because D#+ can't do this natively? @@ -50,9 +52,11 @@ public async Task SlashPauseDMs(InteractionContext ctx, [Option("time", "The amo } } - [SlashCommand("unpausedms", "Unpause DMs between server members.", defaultPermission: false)] - [HomeServer, SlashRequireHomeserverPerm(ServerPermLevel.Moderator), SlashCommandPermissions(DiscordPermissions.ModerateMembers)] - public async Task SlashUnpauseDMs(InteractionContext ctx) + [Command("unpausedms")] + [Description("Unpause DMs between server members.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [HomeServer, SlashRequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ModerateMembers)] + public async Task SlashUnpauseDMs(SlashCommandContext ctx) { // need to make our own api calls because D#+ can't do this natively? diff --git a/Commands/InteractionCommands/SlowmodeInteractions.cs b/Commands/InteractionCommands/SlowmodeInteractions.cs index 2d1fa948..227ea4ba 100644 --- a/Commands/InteractionCommands/SlowmodeInteractions.cs +++ b/Commands/InteractionCommands/SlowmodeInteractions.cs @@ -1,14 +1,16 @@ namespace Cliptok.Commands.InteractionCommands { - internal class SlowmodeInteractions : ApplicationCommandModule + internal class SlowmodeInteractions { - [SlashCommand("slowmode", "Slow down the channel...", defaultPermission: false)] + [Command("slowmode")] + [Description("Slow down the channel...")] + [AllowedProcessors(typeof(SlashCommandProcessor))] [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator)] - [SlashCommandPermissions(DiscordPermissions.ModerateMembers)] + [RequirePermissions(DiscordPermissions.ModerateMembers)] public async Task SlowmodeSlashCommand( - InteractionContext ctx, - [Option("slow_time", "Allowed time between each users messages. 0 for off. A number of seconds or a parseable time.")] string timeToParse, - [Option("channel", "The channel to slow down, if not the current one.")] DiscordChannel channel = default + SlashCommandContext ctx, + [Parameter("slow_time"), Description("Allowed time between each users messages. 0 for off. A number of seconds or a parseable time.")] string timeToParse, + [Parameter("channel"), Description("The channel to slow down, if not the current one.")] DiscordChannel channel = default ) { if (channel == default) diff --git a/Commands/InteractionCommands/StatusInteractions.cs b/Commands/InteractionCommands/StatusInteractions.cs index c6eb0ca9..b757ee3f 100644 --- a/Commands/InteractionCommands/StatusInteractions.cs +++ b/Commands/InteractionCommands/StatusInteractions.cs @@ -1,25 +1,24 @@ -namespace Cliptok.Commands.InteractionCommands +using DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers; + +namespace Cliptok.Commands.InteractionCommands { - internal class StatusInteractions : ApplicationCommandModule + internal class StatusInteractions { - [SlashCommandGroup("status", "Status commands")] + [Command("status")] + [Description("Status commands")] [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator)] - [SlashCommandPermissions(DiscordPermissions.ModerateMembers)] + [RequirePermissions(DiscordPermissions.ModerateMembers)] public class StatusSlashCommands { - [SlashCommand("set", "Set Cliptoks status.", defaultPermission: false)] + [Command("set")] + [Description("Set Cliptoks status.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] public async Task StatusSetCommand( - InteractionContext ctx, - [Option("text", "The text to use for the status.")] string statusText, - [Choice("Custom", (long)DiscordActivityType.Custom)] - [Choice("Playing", (long)DiscordActivityType.Playing)] - [Choice("Streaming", (long)DiscordActivityType.Streaming)] - [Choice("Listening to", (long)DiscordActivityType.ListeningTo)] - [Choice("Watching", (long)DiscordActivityType.Watching)] - [Choice("Competing", (long)DiscordActivityType.Competing)] - [Option("type", "Defaults to custom. The type of status to use.")] long statusType = (long)DiscordActivityType.Custom + SlashCommandContext ctx, + [Parameter("text"), Description("The text to use for the status.")] string statusText, + [Parameter("type"), Description("Defaults to custom. The type of status to use.")] DiscordActivityType statusType = DiscordActivityType.Custom // TODO(#202): test this!!!! ) { if (statusText.Length > 128) @@ -28,15 +27,17 @@ public async Task StatusSetCommand( } await Program.db.StringSetAsync("config:status", statusText); - await Program.db.StringSetAsync("config:status_type", statusType); + await Program.db.StringSetAsync("config:status_type", (long)statusType); - await ctx.Client.UpdateStatusAsync(new DiscordActivity(statusText, (DiscordActivityType)statusType)); + await ctx.Client.UpdateStatusAsync(new DiscordActivity(statusText, statusType)); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Status has been updated!\nType: `{((DiscordActivityType)statusType).ToString()}`\nText: `{statusText}`"); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Status has been updated!\nType: `{statusType.ToString()}`\nText: `{statusText}`"); } - [SlashCommand("clear", "Clear Cliptoks status.", defaultPermission: false)] - public async Task StatusClearCommand(InteractionContext ctx) + [Command("clear")] + [Description("Clear Cliptoks status.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + public async Task StatusClearCommand(SlashCommandContext ctx) { await Program.db.KeyDeleteAsync("config:status"); await Program.db.KeyDeleteAsync("config:status_type"); diff --git a/Commands/InteractionCommands/TechSupportInteractions.cs b/Commands/InteractionCommands/TechSupportInteractions.cs index 2f1dd4a2..102d0105 100644 --- a/Commands/InteractionCommands/TechSupportInteractions.cs +++ b/Commands/InteractionCommands/TechSupportInteractions.cs @@ -2,19 +2,16 @@ namespace Cliptok.Commands.InteractionCommands { - public class TechSupportInteractions : ApplicationCommandModule + public class TechSupportInteractions { - [SlashCommand("vcredist", "Outputs download URLs for the specified Visual C++ Redistributables version")] + [Command("vcredist")] + [Description("Outputs download URLs for the specified Visual C++ Redistributables version")] + [AllowedProcessors(typeof(SlashCommandProcessor))] public async Task RedistsCommand( - InteractionContext ctx, + SlashCommandContext ctx, - [Choice("Visual Studio 2015+ - v140", 140)] - [Choice("Visual Studio 2013 - v120", 120)] - [Choice("Visual Studio 2012 - v110", 110)] - [Choice("Visual Studio 2010 - v100", 100)] - [Choice("Visual Studio 2008 - v90", 90)] - [Choice("Visual Studio 2005 - v80", 80)] - [Option("version", "Visual Studio version number or year")] long version + [SlashChoiceProvider(typeof(VcRedistChoiceProvider))] + [Parameter("version"), Description("Visual Studio version number or year")] long version // TODO(#202): test choices!!! ) { VcRedist redist = VcRedistConstants.VcRedists @@ -36,4 +33,20 @@ public async Task RedistsCommand( await ctx.RespondAsync(null, embed.Build(), false); } } + + internal class VcRedistChoiceProvider : IChoiceProvider + { + public async ValueTask> ProvideAsync(CommandParameter _) + { + return new Dictionary + { + { "Visual Studio 2015+ - v140", "140" }, + { "Visual Studio 2013 - v120", "120" }, + { "Visual Studio 2012 - v110", "110" }, + { "Visual Studio 2010 - v100", "100" }, + { "Visual Studio 2008 - v90", "90" }, + { "Visual Studio 2005 - v80", "80" } + }; + } + } } diff --git a/Commands/InteractionCommands/TrackingInteractions.cs b/Commands/InteractionCommands/TrackingInteractions.cs index c7078da8..e616076b 100644 --- a/Commands/InteractionCommands/TrackingInteractions.cs +++ b/Commands/InteractionCommands/TrackingInteractions.cs @@ -1,19 +1,24 @@ -namespace Cliptok.Commands.InteractionCommands +using System.Runtime.CompilerServices; + +namespace Cliptok.Commands.InteractionCommands { - internal class TrackingInteractions : ApplicationCommandModule + internal class TrackingInteractions { - [SlashCommandGroup("tracking", "Commands to manage message tracking of users", defaultPermission: false)] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), SlashCommandPermissions(DiscordPermissions.ModerateMembers)] + [Command("tracking")] + [Description("Commands to manage message tracking of users")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] public class PermadehoistSlashCommands { - [SlashCommand("add", "Track a users messages.")] - public async Task TrackingAddSlashCmd(InteractionContext ctx, [Option("member", "The member to track.")] DiscordUser discordUser) + [Command("add")] + [Description("Track a users messages.")] + public async Task TrackingAddSlashCmd(SlashCommandContext ctx, [Parameter("member"), Description("The member to track.")] DiscordUser discordUser) { - await ctx.DeferAsync(ephemeral: false); + await ctx.DeferResponseAsync(ephemeral: false); if (Program.db.SetContains("trackedUsers", discordUser.Id)) { - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} This user is already tracked!")); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} This user is already tracked!")); return; } @@ -26,7 +31,7 @@ public async Task TrackingAddSlashCmd(InteractionContext ctx, [Option("member", await thread.SendMessageAsync($"{Program.cfgjson.Emoji.On} Now tracking {discordUser.Mention} in this thread! :eyes:"); thread.AddThreadMemberAsync(ctx.Member); - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.On} Now tracking {discordUser.Mention} in {thread.Mention}!")); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.On} Now tracking {discordUser.Mention} in {thread.Mention}!")); } else @@ -35,18 +40,19 @@ public async Task TrackingAddSlashCmd(InteractionContext ctx, [Option("member", await Program.db.HashSetAsync("trackingThreads", discordUser.Id, thread.Id); await thread.SendMessageAsync($"{Program.cfgjson.Emoji.On} Now tracking {discordUser.Mention} in this thread! :eyes:"); await thread.AddThreadMemberAsync(ctx.Member); - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.On} Now tracking {discordUser.Mention} in {thread.Mention}!")); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.On} Now tracking {discordUser.Mention} in {thread.Mention}!")); } } - [SlashCommand("remove", "Stop tracking a users messages.")] - public async Task TrackingRemoveSlashCmd(InteractionContext ctx, [Option("member", "The member to track.")] DiscordUser discordUser) + [Command("remove")] + [Description("Stop tracking a users messages.")] + public async Task TrackingRemoveSlashCmd(SlashCommandContext ctx, [Parameter("member"), Description("The member to track.")] DiscordUser discordUser) { - await ctx.DeferAsync(ephemeral: false); + await ctx.DeferResponseAsync(ephemeral: false); if (!Program.db.SetContains("trackedUsers", discordUser.Id)) { - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} This user is not being tracked.")); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} This user is not being tracked.")); return; } @@ -61,7 +67,7 @@ await thread.ModifyAsync(thread => thread.IsArchived = true; }); - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Off} No longer tracking {discordUser.Mention}! Thread has been archived for now.")); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Off} No longer tracking {discordUser.Mention}! Thread has been archived for now.")); } } diff --git a/Commands/InteractionCommands/UserNoteInteractions.cs b/Commands/InteractionCommands/UserNoteInteractions.cs index 763ebd5b..eaab5274 100644 --- a/Commands/InteractionCommands/UserNoteInteractions.cs +++ b/Commands/InteractionCommands/UserNoteInteractions.cs @@ -2,22 +2,25 @@ namespace Cliptok.Commands.InteractionCommands { - internal class UserNoteInteractions : ApplicationCommandModule + internal class UserNoteInteractions { - [SlashCommandGroup("note", "Manage user notes", defaultPermission: false)] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), SlashCommandPermissions(DiscordPermissions.ModerateMembers)] + [Command("note")] + [Description("Manage user notes")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] public class UserNoteSlashCommands { - [SlashCommand("add", "Add a note to a user. Only visible to mods.")] - public async Task AddUserNoteAsync(InteractionContext ctx, - [Option("user", "The user to add a note for.")] DiscordUser user, - [Option("note", "The note to add.")] string noteText, - [Option("show_on_modmail", "Whether to show the note when the user opens a modmail thread. Default: true")] bool showOnModmail = true, - [Option("show_on_warn", "Whether to show the note when the user is warned. Default: true")] bool showOnWarn = true, - [Option("show_all_mods", "Whether to show this note to all mods, versus just yourself. Default: true")] bool showAllMods = true, - [Option("show_once", "Whether to show this note once and then discard it. Default: false")] bool showOnce = false) + [Command("add")] + [Description("Add a note to a user. Only visible to mods.")] + public async Task AddUserNoteAsync(SlashCommandContext ctx, + [Parameter("user"), Description("The user to add a note for.")] DiscordUser user, + [Parameter("note"), Description("The note to add.")] string noteText, + [Parameter("show_on_modmail"), Description("Whether to show the note when the user opens a modmail thread. Default: true")] bool showOnModmail = true, + [Parameter("show_on_warn"), Description("Whether to show the note when the user is warned. Default: true")] bool showOnWarn = true, + [Parameter("show_all_mods"), Description("Whether to show this note to all mods, versus just yourself. Default: true")] bool showAllMods = true, + [Parameter("show_once"), Description("Whether to show this note once and then discard it. Default: false")] bool showOnce = false) { - await ctx.DeferAsync(); + await ctx.DeferResponseAsync(); // Assemble new note long noteId = Program.db.StringIncrement("totalWarnings"); @@ -42,13 +45,14 @@ public async Task AddUserNoteAsync(InteractionContext ctx, await LogChannelHelper.LogMessageAsync("mod", $"{Program.cfgjson.Emoji.Information} New note for {user.Mention}!", embed); // Respond - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Success} Successfully added note!").AsEphemeral()); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Success} Successfully added note!").AsEphemeral()); } - [SlashCommand("delete", "Delete a note.")] - public async Task RemoveUserNoteAsync(InteractionContext ctx, - [Option("user", "The user whose note to delete.")] DiscordUser user, - [Autocomplete(typeof(NotesAutocompleteProvider))][Option("note", "The note to delete.")] string targetNote) + [Command("delete")] + [Description("Delete a note.")] + public async Task RemoveUserNoteAsync(SlashCommandContext ctx, + [Parameter("user"), Description("The user whose note to delete.")] DiscordUser user, + [SlashAutoCompleteProvider(typeof(NotesAutocompleteProvider))][Parameter("note"), Description("The note to delete.")] string targetNote) { // Get note UserNote note; @@ -58,14 +62,14 @@ public async Task RemoveUserNoteAsync(InteractionContext ctx, } catch { - await ctx.CreateResponseAsync(new DiscordInteractionResponseBuilder().WithContent($"{Program.cfgjson.Emoji.Error} I couldn't find that note! Make sure you've got the right ID.").AsEphemeral()); + await ctx.RespondAsync(new DiscordInteractionResponseBuilder().WithContent($"{Program.cfgjson.Emoji.Error} I couldn't find that note! Make sure you've got the right ID.").AsEphemeral()); return; } // If user manually provided an ID of a warning, refuse the request and suggest /delwarn instead if (note.Type == WarningType.Warning) { - await ctx.CreateResponseAsync(new DiscordInteractionResponseBuilder().WithContent($"{Program.cfgjson.Emoji.Error} That's a warning, not a note! Try using `/delwarn` instead, or make sure you've got the right note ID.").AsEphemeral()); + await ctx.RespondAsync(new DiscordInteractionResponseBuilder().WithContent($"{Program.cfgjson.Emoji.Error} That's a warning, not a note! Try using `/delwarn` instead, or make sure you've got the right note ID.").AsEphemeral()); return; } @@ -77,18 +81,19 @@ public async Task RemoveUserNoteAsync(InteractionContext ctx, await LogChannelHelper.LogMessageAsync("mod", $"{Program.cfgjson.Emoji.Deleted} Note deleted: `{note.NoteId}` (belonging to {user.Mention}, deleted by {ctx.User.Mention})", embed); // Respond - await ctx.CreateResponseAsync(new DiscordInteractionResponseBuilder().WithContent($"{Program.cfgjson.Emoji.Success} Successfully deleted note!").AsEphemeral()); + await ctx.RespondAsync(new DiscordInteractionResponseBuilder().WithContent($"{Program.cfgjson.Emoji.Success} Successfully deleted note!").AsEphemeral()); } - [SlashCommand("edit", "Edit a note for a user.")] - public async Task EditUserNoteAsync(InteractionContext ctx, - [Option("user", "The user to edit a note for.")] DiscordUser user, - [Autocomplete(typeof(NotesAutocompleteProvider))][Option("note", "The note to edit.")] string targetNote, - [Option("new_text", "The new note text. Leave empty to not change.")] string newNoteText = default, - [Option("show_on_modmail", "Whether to show the note when the user opens a modmail thread.")] bool? showOnModmail = null, - [Option("show_on_warn", "Whether to show the note when the user is warned.")] bool? showOnWarn = null, - [Option("show_all_mods", "Whether to show this note to all mods, versus just yourself.")] bool? showAllMods = null, - [Option("show_once", "Whether to show this note once and then discard it.")] bool? showOnce = null) + [Command("edit")] + [Description("Edit a note for a user.")] + public async Task EditUserNoteAsync(SlashCommandContext ctx, + [Parameter("user"), Description("The user to edit a note for.")] DiscordUser user, + [SlashAutoCompleteProvider(typeof(NotesAutocompleteProvider))][Parameter("note"), Description("The note to edit.")] string targetNote, + [Parameter("new_text"), Description("The new note text. Leave empty to not change.")] string newNoteText = default, + [Parameter("show_on_modmail"), Description("Whether to show the note when the user opens a modmail thread.")] bool? showOnModmail = null, + [Parameter("show_on_warn"), Description("Whether to show the note when the user is warned.")] bool? showOnWarn = null, + [Parameter("show_all_mods"), Description("Whether to show this note to all mods, versus just yourself.")] bool? showAllMods = null, + [Parameter("show_once"), Description("Whether to show this note once and then discard it.")] bool? showOnce = null) { // Get note UserNote note; @@ -98,7 +103,7 @@ public async Task EditUserNoteAsync(InteractionContext ctx, } catch { - await ctx.CreateResponseAsync(new DiscordInteractionResponseBuilder().WithContent($"{Program.cfgjson.Emoji.Error} I couldn't find that note! Make sure you've got the right ID.").AsEphemeral()); + await ctx.RespondAsync(new DiscordInteractionResponseBuilder().WithContent($"{Program.cfgjson.Emoji.Error} I couldn't find that note! Make sure you've got the right ID.").AsEphemeral()); return; } @@ -109,14 +114,14 @@ public async Task EditUserNoteAsync(InteractionContext ctx, // If no changes are made, refuse the request if (note.NoteText == newNoteText && showOnModmail is null && showOnWarn is null && showAllMods is null && showOnce is null) { - await ctx.CreateResponseAsync(new DiscordInteractionResponseBuilder().WithContent($"{Program.cfgjson.Emoji.Error} You didn't change anything about the note!").AsEphemeral()); + await ctx.RespondAsync(new DiscordInteractionResponseBuilder().WithContent($"{Program.cfgjson.Emoji.Error} You didn't change anything about the note!").AsEphemeral()); return; } // If user manually provided an ID of a warning, refuse the request and suggest /editwarn instead if (note.Type == WarningType.Warning) { - await ctx.CreateResponseAsync(new DiscordInteractionResponseBuilder().WithContent($"{Program.cfgjson.Emoji.Error} That's a warning, not a note! Try using `/editwarn` instead, or make sure you've got the right note ID.").AsEphemeral()); + await ctx.RespondAsync(new DiscordInteractionResponseBuilder().WithContent($"{Program.cfgjson.Emoji.Error} That's a warning, not a note! Try using `/editwarn` instead, or make sure you've got the right note ID.").AsEphemeral()); return; } @@ -146,22 +151,24 @@ public async Task EditUserNoteAsync(InteractionContext ctx, await LogChannelHelper.LogMessageAsync("mod", $"{Program.cfgjson.Emoji.Information} Note edited: `{note.NoteId}` (belonging to {user.Mention})", embed); // Respond - await ctx.CreateResponseAsync(new DiscordInteractionResponseBuilder().WithContent($"{Program.cfgjson.Emoji.Success} Successfully edited note!").AsEphemeral()); + await ctx.RespondAsync(new DiscordInteractionResponseBuilder().WithContent($"{Program.cfgjson.Emoji.Success} Successfully edited note!").AsEphemeral()); } - [SlashCommand("list", "List all notes for a user.")] - public async Task ListUserNotesAsync(InteractionContext ctx, - [Option("user", "The user whose notes to list.")] DiscordUser user, - [Option("public", "Whether to show the notes in public chat. Default: false")] bool showPublicly = false) + [Command("list")] + [Description("List all notes for a user.")] + public async Task ListUserNotesAsync(SlashCommandContext ctx, + [Parameter("user"), Description("The user whose notes to list.")] DiscordUser user, + [Parameter("public"), Description("Whether to show the notes in public chat. Default: false")] bool showPublicly = false) { - await ctx.CreateResponseAsync(new DiscordInteractionResponseBuilder().AddEmbed(await GenerateUserNotesEmbedAsync(user)).AsEphemeral(!showPublicly)); + await ctx.RespondAsync(new DiscordInteractionResponseBuilder().AddEmbed(await GenerateUserNotesEmbedAsync(user)).AsEphemeral(!showPublicly)); } - [SlashCommand("details", "Show the details of a specific note for a user.")] - public async Task ShowUserNoteAsync(InteractionContext ctx, - [Option("user", "The user whose note to show details for.")] DiscordUser user, - [Autocomplete(typeof(NotesAutocompleteProvider))][Option("note", "The note to show.")] string targetNote, - [Option("public", "Whether to show the note in public chat. Default: false")] bool showPublicly = false) + [Command("details")] + [Description("Show the details of a specific note for a user.")] + public async Task ShowUserNoteAsync(SlashCommandContext ctx, + [Parameter("user"), Description("The user whose note to show details for.")] DiscordUser user, + [SlashAutoCompleteProvider(typeof(NotesAutocompleteProvider))][Parameter("note"), Description("The note to show.")] string targetNote, + [Parameter("public"), Description("Whether to show the note in public chat. Default: false")] bool showPublicly = false) { // Get note UserNote note; @@ -171,26 +178,26 @@ public async Task ShowUserNoteAsync(InteractionContext ctx, } catch { - await ctx.CreateResponseAsync(new DiscordInteractionResponseBuilder().WithContent($"{Program.cfgjson.Emoji.Error} I couldn't find that note! Make sure you've got the right ID.").AsEphemeral()); + await ctx.RespondAsync(new DiscordInteractionResponseBuilder().WithContent($"{Program.cfgjson.Emoji.Error} I couldn't find that note! Make sure you've got the right ID.").AsEphemeral()); return; } // If user manually provided an ID of a warning, refuse the request and suggest /warndetails instead if (note.Type == WarningType.Warning) { - await ctx.CreateResponseAsync(new DiscordInteractionResponseBuilder().WithContent($"{Program.cfgjson.Emoji.Error} That's a warning, not a note! Try using `/warndetails` instead, or make sure you've got the right note ID.").AsEphemeral()); + await ctx.RespondAsync(new DiscordInteractionResponseBuilder().WithContent($"{Program.cfgjson.Emoji.Error} That's a warning, not a note! Try using `/warndetails` instead, or make sure you've got the right note ID.").AsEphemeral()); return; } // Respond - await ctx.CreateResponseAsync(new DiscordInteractionResponseBuilder().AddEmbed(await GenerateUserNoteDetailEmbedAsync(note, user)).AsEphemeral(!showPublicly)); + await ctx.RespondAsync(new DiscordInteractionResponseBuilder().AddEmbed(await GenerateUserNoteDetailEmbedAsync(note, user)).AsEphemeral(!showPublicly)); } - private class NotesAutocompleteProvider : IAutocompleteProvider + private class NotesAutocompleteProvider : IAutoCompleteProvider { - public async Task> Provider(AutocompleteContext ctx) + public async ValueTask> AutoCompleteAsync(AutoCompleteContext ctx) { - var list = new List(); + var list = new Dictionary(); var useroption = ctx.Options.FirstOrDefault(x => x.Name == "user"); if (useroption == default) @@ -213,8 +220,10 @@ public async Task> Provider(AutocompleteC string noteString = $"{StringHelpers.Pad(note.Value.NoteId)} - {StringHelpers.Truncate(note.Value.NoteText, 29, true)} - {TimeHelpers.TimeToPrettyFormat(DateTime.Now - note.Value.Timestamp, true)}"; - if (ctx.FocusedOption.Value.ToString() == "" || note.Value.NoteText.Contains((string)ctx.FocusedOption.Value) || noteString.ToLower().Contains(ctx.FocusedOption.Value.ToString().ToLower())) - list.Add(new DiscordAutoCompleteChoice(noteString, StringHelpers.Pad(note.Value.NoteId))); + var focusedOption = ctx.Options.FirstOrDefault(option => option.Focused); + if (focusedOption is not null) // TODO(#202): is this right? + if (note.Value.NoteText.Contains((string)focusedOption.Value) || noteString.ToLower().Contains(focusedOption.Value.ToString().ToLower())) + list.Add(noteString, StringHelpers.Pad(note.Value.NoteId)); } return list; diff --git a/Commands/InteractionCommands/WarningInteractions.cs b/Commands/InteractionCommands/WarningInteractions.cs index d8a674f8..f1621d68 100644 --- a/Commands/InteractionCommands/WarningInteractions.cs +++ b/Commands/InteractionCommands/WarningInteractions.cs @@ -1,22 +1,26 @@ -using static Cliptok.Helpers.WarningHelpers; +using DSharpPlus.Commands.Processors.SlashCommands; +using DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers; +using static Cliptok.Helpers.WarningHelpers; namespace Cliptok.Commands.InteractionCommands { - internal class WarningInteractions : ApplicationCommandModule + internal class WarningInteractions { - [SlashCommand("warn", "Formally warn a user, usually for breaking the server rules.", defaultPermission: false)] + [Command("warn")] + [Description("Formally warn a user, usually for breaking the server rules.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator)] - [SlashCommandPermissions(DiscordPermissions.ModerateMembers)] - public async Task WarnSlashCommand(InteractionContext ctx, - [Option("user", "The user to warn.")] DiscordUser user, - [Option("reason", "The reason they're being warned.")] string reason, - [Option("reply_msg_id", "The ID of a message to reply to, must be in the same channel.")] string replyMsgId = "0", - [Option("channel", "The channel to warn the user in, implied if not supplied.")] DiscordChannel channel = null + [RequirePermissions(DiscordPermissions.ModerateMembers)] + public async Task WarnSlashCommand(SlashCommandContext ctx, + [Parameter("user"), Description("The user to warn.")] DiscordUser user, + [Parameter("reason"), Description("The reason they're being warned.")] string reason, + [Parameter("reply_msg_id"), Description("The ID of a message to reply to, must be in the same channel.")] string replyMsgId = "0", + [Parameter("channel"), Description("The channel to warn the user in, implied if not supplied.")] DiscordChannel channel = null ) { // Initial response to avoid the 3 second timeout, will edit later. var eout = new DiscordInteractionResponseBuilder().AsEphemeral(true); - await ctx.CreateResponseAsync(DiscordInteractionResponseType.DeferredChannelMessageWithSource, eout); + await ctx.RespondAsync(eout); // Edits need a webhook rather than interaction..? DiscordWebhookBuilder webhookOut; @@ -41,9 +45,6 @@ public async Task WarnSlashCommand(InteractionContext ctx, if (channel is null) channel = ctx.Channel; - if (channel is null) - channel = await ctx.Client.GetChannelAsync(ctx.Interaction.ChannelId); - var messageBuild = new DiscordMessageBuilder() .WithContent($"{Program.cfgjson.Emoji.Warning} {user.Mention} was warned: **{reason.Replace("`", "\\`").Replace("*", "\\*")}**"); @@ -57,34 +58,38 @@ public async Task WarnSlashCommand(InteractionContext ctx, await ctx.EditResponseAsync(webhookOut); } - [SlashCommand("warnings", "Fetch the warnings for a user.")] - public async Task WarningsSlashCommand(InteractionContext ctx, - [Option("user", "The user to find the warnings for.")] DiscordUser user, - [Option("public", "Whether to show the warnings in public chat. Do not disrupt chat with this.")] bool publicWarnings = false + [Command("warnings")] + [Description("Fetch the warnings for a user.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + public async Task WarningsSlashCommand(SlashCommandContext ctx, + [Parameter("user"), Description("The user to find the warnings for.")] DiscordUser user, + [Parameter("public"), Description("Whether to show the warnings in public chat. Do not disrupt chat with this.")] bool publicWarnings = false ) { var eout = new DiscordInteractionResponseBuilder().AddEmbed(await WarningHelpers.GenerateWarningsEmbedAsync(user)); if (!publicWarnings) eout.AsEphemeral(true); - await ctx.CreateResponseAsync(DiscordInteractionResponseType.ChannelMessageWithSource, eout); + await ctx.RespondAsync(eout); } - [SlashCommand("transfer_warnings", "Transfer warnings from one user to another.", defaultPermission: false)] + [Command("transfer_warnings")] + [Description("Transfer warnings from one user to another.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] [SlashRequireHomeserverPerm(ServerPermLevel.Moderator)] - [SlashCommandPermissions(DiscordPermissions.ModerateMembers)] - public async Task TransferWarningsSlashCommand(InteractionContext ctx, - [Option("source_user", "The user currently holding the warnings.")] DiscordUser sourceUser, - [Option("target_user", "The user receiving the warnings.")] DiscordUser targetUser, - [Option("merge", "Whether to merge the source user's warnings and the target user's warnings.")] bool merge = false, - [Option("force_override", "DESTRUCTIVE OPERATION: Whether to OVERRIDE and DELETE the target users existing warnings.")] bool forceOverride = false + [RequirePermissions(DiscordPermissions.ModerateMembers)] + public async Task TransferWarningsSlashCommand(SlashCommandContext ctx, + [Parameter("source_user"), Description("The user currently holding the warnings.")] DiscordUser sourceUser, + [Parameter("target_user"), Description("The user receiving the warnings.")] DiscordUser targetUser, + [Parameter("merge"), Description("Whether to merge the source user's warnings and the target user's warnings.")] bool merge = false, + [Parameter("force_override"), Description("DESTRUCTIVE OPERATION: Whether to OVERRIDE and DELETE the target users existing warnings.")] bool forceOverride = false ) { - await ctx.DeferAsync(ephemeral: false); + await ctx.DeferResponseAsync(); // TODO(#202): how do you make this ephemeral? if (sourceUser == targetUser) { - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} The source and target users cannot be the same!")); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} The source and target users cannot be the same!")); return; } @@ -93,7 +98,7 @@ public async Task TransferWarningsSlashCommand(InteractionContext ctx, if (sourceWarnings.Length == 0) { - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} The source user has no warnings to transfer.").AddEmbed(await GenerateWarningsEmbedAsync(sourceUser))); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} The source user has no warnings to transfer.").AddEmbed(await GenerateWarningsEmbedAsync(sourceUser))); return; } else if (merge) @@ -106,7 +111,7 @@ public async Task TransferWarningsSlashCommand(InteractionContext ctx, } else if (targetWarnings.Length > 0 && !forceOverride) { - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Warning} **CAUTION**: The target user has warnings.\n\n" + + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Warning} **CAUTION**: The target user has warnings.\n\n" + $"If you are sure you want to **OVERRIDE** and **DELETE** these warnings, please consider the consequences before adding `force_override: True` to the command.\nIf you wish to **NOT** override the target's warnings, please use `merge: True` instead.") .AddEmbed(await GenerateWarningsEmbedAsync(targetUser))); return; @@ -131,14 +136,14 @@ await LogChannelHelper.LogMessageAsync("mod", .WithContent($"{Program.cfgjson.Emoji.Information} Warnings from {sourceUser.Mention} were {operationText}transferred to {targetUser.Mention} by `{DiscordHelpers.UniqueUsername(ctx.User)}`") .AddEmbed(await GenerateWarningsEmbedAsync(targetUser)) ); - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Success} Successfully {operationText}transferred warnings from {sourceUser.Mention} to {targetUser.Mention}!")); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Success} Successfully {operationText}transferred warnings from {sourceUser.Mention} to {targetUser.Mention}!")); } - internal partial class WarningsAutocompleteProvider : IAutocompleteProvider + internal partial class WarningsAutocompleteProvider : IAutoCompleteProvider { - public async Task> Provider(AutocompleteContext ctx) + public async ValueTask> AutoCompleteAsync(AutoCompleteContext ctx) { - var list = new List(); + var list = new Dictionary(); var useroption = ctx.Options.FirstOrDefault(x => x.Name == "user"); if (useroption == default) @@ -161,8 +166,10 @@ public async Task> Provider(AutocompleteC string warningString = $"{StringHelpers.Pad(warning.Value.WarningId)} - {StringHelpers.Truncate(warning.Value.WarnReason, 29, true)} - {TimeHelpers.TimeToPrettyFormat(DateTime.Now - warning.Value.WarnTimestamp, true)}"; - if (ctx.FocusedOption.Value.ToString() == "" || warning.Value.WarnReason.Contains((string)ctx.FocusedOption.Value) || warningString.ToLower().Contains(ctx.FocusedOption.Value.ToString().ToLower())) - list.Add(new DiscordAutoCompleteChoice(warningString, StringHelpers.Pad(warning.Value.WarningId))); + var focusedOption = ctx.Options.FirstOrDefault(option => option.Focused); + if (focusedOption is not null) // TODO(#202): is this right? + if (warning.Value.WarnReason.Contains((string)focusedOption.Value) || warningString.ToLower().Contains(focusedOption.Value.ToString().ToLower())) + list.Add(warningString, StringHelpers.Pad(warning.Value.WarningId)); } return list; @@ -170,12 +177,14 @@ public async Task> Provider(AutocompleteC } } - [SlashCommand("warndetails", "Search for a warning and return its details.", defaultPermission: false)] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), SlashCommandPermissions(DiscordPermissions.ModerateMembers)] - public async Task WarndetailsSlashCommand(InteractionContext ctx, - [Option("user", "The user to fetch a warning for.")] DiscordUser user, - [Autocomplete(typeof(WarningsAutocompleteProvider)), Option("warning", "Type to search! Find the warning you want to fetch.")] string warning, - [Option("public", "Whether to show the output publicly.")] bool publicWarnings = false + [Command("warndetails")] + [Description("Search for a warning and return its details.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] + public async Task WarndetailsSlashCommand(SlashCommandContext ctx, + [Parameter("user"), Description("The user to fetch a warning for.")] DiscordUser user, + [SlashAutoCompleteProvider(typeof(WarningsAutocompleteProvider)), Parameter("warning"), Description("Type to search! Find the warning you want to fetch.")] string warning, + [Parameter("public"), Description("Whether to show the output publicly.")] bool publicWarnings = false ) { if (warning.Contains(' ')) @@ -202,17 +211,19 @@ public async Task WarndetailsSlashCommand(InteractionContext ctx, await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} That's a note, not a warning! Try using `/note details` instead, or make sure you've got the right warning ID.", ephemeral: true); else { - await ctx.DeferAsync(ephemeral: !publicWarnings); - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().AddEmbed(await FancyWarnEmbedAsync(warningObject, true, userID: user.Id))); + await ctx.DeferResponseAsync(ephemeral: !publicWarnings); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().AddEmbed(await FancyWarnEmbedAsync(warningObject, true, userID: user.Id))); } } - [SlashCommand("delwarn", "Search for a warning and delete it!", defaultPermission: false)] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), SlashCommandPermissions(DiscordPermissions.ModerateMembers)] - public async Task DelwarnSlashCommand(InteractionContext ctx, - [Option("user", "The user to delete a warning for.")] DiscordUser targetUser, - [Autocomplete(typeof(WarningsAutocompleteProvider))][Option("warning", "Type to search! Find the warning you want to delete.")] string warningId, - [Option("public", "Whether to show the output publicly. Default: false")] bool showPublic = false + [Command("delwarn")] + [Description("Search for a warning and delete it!")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] + public async Task DelwarnSlashCommand(SlashCommandContext ctx, + [Parameter("user"), Description("The user to delete a warning for.")] DiscordUser targetUser, + [SlashAutoCompleteProvider(typeof(WarningsAutocompleteProvider))][Parameter("warning"), Description("Type to search! Find the warning you want to delete.")] string warningId, + [Parameter("public"), Description("Whether to show the output publicly. Default: false")] bool showPublic = false ) { if (warningId.Contains(' ')) @@ -245,7 +256,7 @@ public async Task DelwarnSlashCommand(InteractionContext ctx, } else { - await ctx.DeferAsync(ephemeral: !showPublic); + await ctx.DeferResponseAsync(ephemeral: !showPublic); bool success = await DelWarningAsync(warning, targetUser.Id); if (success) @@ -258,7 +269,7 @@ await LogChannelHelper.LogMessageAsync("mod", .WithAllowedMentions(Mentions.None) ); - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Deleted} Successfully deleted warning `{StringHelpers.Pad(warnId)}` (belonging to {targetUser.Mention})")); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Deleted} Successfully deleted warning `{StringHelpers.Pad(warnId)}` (belonging to {targetUser.Mention})")); } @@ -269,13 +280,15 @@ await LogChannelHelper.LogMessageAsync("mod", } } - [SlashCommand("editwarn", "Search for a warning and edit it!", defaultPermission: false)] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), SlashCommandPermissions(DiscordPermissions.ModerateMembers)] - public async Task EditWarnSlashCommand(InteractionContext ctx, - [Option("user", "The user to fetch a warning for.")] DiscordUser user, - [Autocomplete(typeof(WarningsAutocompleteProvider))][Option("warning", "Type to search! Find the warning you want to edit.")] string warning, - [Option("new_reason", "The new reason for the warning")] string reason, - [Option("public", "Whether to show the output publicly. Default: false")] bool showPublic = false) + [Command("editwarn")] + [Description("Search for a warning and edit it!")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] + public async Task EditWarnSlashCommand(SlashCommandContext ctx, + [Parameter("user"), Description("The user to fetch a warning for.")] DiscordUser user, + [SlashAutoCompleteProvider(typeof(WarningsAutocompleteProvider))][Parameter("warning"), Description("Type to search! Find the warning you want to edit.")] string warning, + [Parameter("new_reason"), Description("The new reason for the warning")] string reason, + [Parameter("public"), Description("Whether to show the output publicly. Default: false")] bool showPublic = false) { if (warning.Contains(' ')) { @@ -313,7 +326,7 @@ public async Task EditWarnSlashCommand(InteractionContext ctx, } else { - await ctx.DeferAsync(ephemeral: !showPublic); + await ctx.DeferResponseAsync(ephemeral: !showPublic); await LogChannelHelper.LogMessageAsync("mod", new DiscordMessageBuilder() @@ -323,7 +336,7 @@ await LogChannelHelper.LogMessageAsync("mod", ); await EditWarning(user, warnId, ctx.User, reason); - await ctx.FollowUpAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Information} Successfully edited warning `{StringHelpers.Pad(warnId)}` (belonging to {user.Mention})") + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Information} Successfully edited warning `{StringHelpers.Pad(warnId)}` (belonging to {user.Mention})") .AddEmbed(await FancyWarnEmbedAsync(GetWarning(user.Id, warnId), userID: user.Id))); } } diff --git a/Commands/Kick.cs b/Commands/Kick.cs index 2f969519..87b49b58 100644 --- a/Commands/Kick.cs +++ b/Commands/Kick.cs @@ -1,12 +1,13 @@ namespace Cliptok.Commands { - internal class Kick : BaseCommandModule + internal class Kick { [Command("kick")] - [Aliases("yeet", "shoo", "goaway", "defenestrate")] + [TextAlias("yeet", "shoo", "goaway", "defenestrate")] [Description("Kicks a user, removing them from the server until they rejoin. Generally not very useful.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [RequirePermissions(DiscordPermissions.KickMembers), HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] - public async Task KickCmd(CommandContext ctx, DiscordUser target, [RemainingText] string reason = "No reason specified.") + public async Task KickCmd(TextCommandContext ctx, DiscordUser target, [RemainingText] string reason = "No reason specified.") { if (target.IsBot) { @@ -50,8 +51,9 @@ public async Task KickCmd(CommandContext ctx, DiscordUser target, [RemainingText } [Command("masskick")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] - public async Task MassKickCmd(CommandContext ctx, [RemainingText] string input) + public async Task MassKickCmd(TextCommandContext ctx, [RemainingText] string input) { List usersString = input.Replace("\n", " ").Replace("\r", "").Split(' ').ToList(); @@ -65,7 +67,8 @@ public async Task MassKickCmd(CommandContext ctx, [RemainingText] string input) List> taskList = new(); int successes = 0; - var loading = await ctx.RespondAsync("Processing, please wait."); + await ctx.RespondAsync("Processing, please wait."); + var loading = await ctx.GetResponseAsync(); foreach (ulong user in users) { diff --git a/Commands/Lists.cs b/Commands/Lists.cs index 61190cb4..76f0fe01 100644 --- a/Commands/Lists.cs +++ b/Commands/Lists.cs @@ -1,6 +1,6 @@ namespace Cliptok.Commands { - internal class Lists : BaseCommandModule + internal class Lists { public class GitHubDispatchBody { @@ -25,8 +25,9 @@ public class GitHubDispatchInputs [Command("listupdate")] [Description("Updates the private lists from the GitHub repository, then reloads them into memory.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [RequireHomeserverPerm(ServerPermLevel.Moderator)] - public async Task ListUpdate(CommandContext ctx) + public async Task ListUpdate(TextCommandContext ctx) { if (Program.cfgjson.GitListDirectory is null || Program.cfgjson.GitListDirectory == "") { @@ -35,7 +36,8 @@ public async Task ListUpdate(CommandContext ctx) } string command = $"cd Lists/{Program.cfgjson.GitListDirectory} && git pull"; - DiscordMessage msg = await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Updating private lists.."); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Updating private lists.."); + DiscordMessage msg = await ctx.GetResponseAsync(); ShellResult finishedShell = RunShellCommand(command); @@ -55,9 +57,10 @@ public async Task ListUpdate(CommandContext ctx) [Command("listadd")] [Description("Add a piece of text to a public list.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] public async Task ListAdd( - CommandContext ctx, + TextCommandContext ctx, [Description("The filename of the public list to add to. For example scams.txt")] string fileName, [RemainingText, Description("The text to add the list. Can be in a codeblock and across multiple line.")] string content ) @@ -134,8 +137,9 @@ await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} An error with code `{resp [Command("scamcheck")] [Description("Check if a link or message is known to the anti-phishing API.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public async Task ScamCheck(CommandContext ctx, [RemainingText, Description("Domain or message content to scan.")] string content) + public async Task ScamCheck(TextCommandContext ctx, [RemainingText, Description("Domain or message content to scan.")] string content) { var urlMatches = Constants.RegexConstants.url_rx.Matches(content); if (urlMatches.Count > 0 && Environment.GetEnvironmentVariable("CLIPTOK_ANTIPHISHING_ENDPOINT") is not null && Environment.GetEnvironmentVariable("CLIPTOK_ANTIPHISHING_ENDPOINT") != "useyourimagination") @@ -164,11 +168,12 @@ public async Task ScamCheck(CommandContext ctx, [RemainingText, Description("Dom } [Command("joinwatch")] - [Aliases("joinnotify", "leavewatch", "leavenotify")] + [TextAlias("joinnotify", "leavewatch", "leavenotify")] [Description("Watch for joins and leaves of a given user. Output goes to #investigations.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] public async Task JoinWatch( - CommandContext ctx, + TextCommandContext ctx, [Description("The user to watch for joins and leaves of.")] DiscordUser user, [Description("An optional note for context."), RemainingText] string note = "" ) @@ -212,11 +217,12 @@ public async Task JoinWatch( } [Command("appealblock")] - [Aliases("superduperban", "ablock")] + [TextAlias("superduperban", "ablock")] [Description("Prevents a user from submitting ban appeals.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] public async Task AppealBlock( - CommandContext ctx, + TextCommandContext ctx, [Description("The user to block from ban appeals.")] DiscordUser user ) { diff --git a/Commands/Lockdown.cs b/Commands/Lockdown.cs index c55df3a4..68af2cf8 100644 --- a/Commands/Lockdown.cs +++ b/Commands/Lockdown.cs @@ -1,15 +1,16 @@ namespace Cliptok.Commands { - class Lockdown : BaseCommandModule + class Lockdown { public bool ongoingLockdown = false; [Command("lockdown")] - [Aliases("lock")] + [TextAlias("lock")] [Description("Locks the current channel, preventing any new messages. See also: unlock")] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequireBotPermissions(DiscordPermissions.ManageChannels)] + [AllowedProcessors(typeof(TextCommandProcessor))] + [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ManageChannels, DiscordPermissions.None)] public async Task LockdownCommand( - CommandContext ctx, + TextCommandContext ctx, [RemainingText, Description("The time and reason for the lockdown. For example '3h' or '3h spam'. Default is permanent with no reason.")] string timeAndReason = "" ) { @@ -98,8 +99,9 @@ await thread.ModifyAsync(a => [Command("unlock")] [Description("Unlocks a previously locked channel. See also: lockdown")] - [Aliases("unlockdown"), HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequireBotPermissions(DiscordPermissions.ManageChannels)] - public async Task UnlockCommand(CommandContext ctx, [RemainingText] string reason = "") + [AllowedProcessors(typeof(TextCommandProcessor))] + [TextAlias("unlockdown"), HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ManageChannels, DiscordPermissions.None)] + public async Task UnlockCommand(TextCommandContext ctx, [RemainingText] string reason = "") { var currentChannel = ctx.Channel; if (!Program.cfgjson.LockdownEnabledChannels.Contains(currentChannel.Id)) diff --git a/Commands/Mutes.cs b/Commands/Mutes.cs index 9f8116d0..85c89e7b 100644 --- a/Commands/Mutes.cs +++ b/Commands/Mutes.cs @@ -1,12 +1,13 @@ namespace Cliptok.Commands { - internal class Mutes : BaseCommandModule + internal class Mutes { [Command("unmute")] - [Aliases("umute")] + [TextAlias("umute")] [Description("Unmutes a previously muted user, typically ahead of the standard expiration time. See also: mute")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public async Task UnmuteCmd(CommandContext ctx, [Description("The user you're trying to unmute.")] DiscordUser targetUser, string reason = "No reason provided.") + public async Task UnmuteCmd(TextCommandContext ctx, [Description("The user you're trying to unmute.")] DiscordUser targetUser, string reason = "No reason provided.") { reason = $"[Manual unmute by {DiscordHelpers.UniqueUsername(ctx.User)}]: {reason}"; @@ -46,9 +47,10 @@ public async Task UnmuteCmd(CommandContext ctx, [Description("The user you're tr [Command("mute")] [Description("Mutes a user, preventing them from sending messages until they're unmuted. See also: unmute")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] public async Task MuteCmd( - CommandContext ctx, [Description("The user you're trying to mute")] DiscordUser targetUser, + TextCommandContext ctx, [Description("The user you're trying to mute")] DiscordUser targetUser, [RemainingText, Description("Combined argument for the time and reason for the mute. For example '1h rule 7' or 'rule 10'")] string timeAndReason = "No reason specified." ) { @@ -100,9 +102,10 @@ public async Task MuteCmd( [Command("tqsmute")] [Description( "Temporarily mutes a user, preventing them from sending messages in #tech-support and related channels until they're unmuted.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TechnicalQueriesSlayer)] public async Task TqsMuteCmd( - CommandContext ctx, [Description("The user to mute")] DiscordUser targetUser, + TextCommandContext ctx, [Description("The user to mute")] DiscordUser targetUser, [RemainingText, Description("The reason for the mute")] string reason = "No reason specified.") { if (Program.cfgjson.TqsMutedRole == 0) diff --git a/Commands/Raidmode.cs b/Commands/Raidmode.cs index 0da42988..365dcb62 100644 --- a/Commands/Raidmode.cs +++ b/Commands/Raidmode.cs @@ -1,16 +1,19 @@ -namespace Cliptok.Commands +using DSharpPlus.Commands.Trees.Metadata; + +namespace Cliptok.Commands { - internal class Raidmode : BaseCommandModule + internal class Raidmode { - [Group("clipraidmode")] + [Command("clipraidmode")] [Description("Manage the server's raidmode, preventing joins while on.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [RequireHomeserverPerm(ServerPermLevel.Moderator)] - class RaidmodeCommands : BaseCommandModule + class RaidmodeCommands { - [GroupCommand] + [DefaultGroupCommand] [Description("Check whether raidmode is enabled or not, and when it ends.")] - [Aliases("status")] - public async Task RaidmodeStatus(CommandContext ctx) + [TextAlias("status")] + public async Task RaidmodeStatus(TextCommandContext ctx) { if (Program.db.HashExists("raidmode", ctx.Guild.Id)) { @@ -27,7 +30,7 @@ public async Task RaidmodeStatus(CommandContext ctx) [Command("on")] [Description("Enable raidmode.")] - public async Task RaidmodeOn(CommandContext ctx, [Description("The amount of time to keep raidmode enabled for. Default is 3 hours.")] string duration = default) + public async Task RaidmodeOn(TextCommandContext ctx, [Description("The amount of time to keep raidmode enabled for. Default is 3 hours.")] string duration = default) { if (Program.db.HashExists("raidmode", ctx.Guild.Id)) { @@ -60,7 +63,7 @@ await LogChannelHelper.LogMessageAsync("mod", [Command("off")] [Description("Disable raidmode.")] - public async Task RaidmdodeOff(CommandContext ctx) + public async Task RaidmdodeOff(TextCommandContext ctx) { if (Program.db.HashExists("raidmode", ctx.Guild.Id)) { diff --git a/Commands/Reminders.cs b/Commands/Reminders.cs index c152e95a..9e49af29 100644 --- a/Commands/Reminders.cs +++ b/Commands/Reminders.cs @@ -1,6 +1,6 @@ namespace Cliptok.Commands { - public class Reminders : BaseCommandModule + public class Reminders { public class Reminder { @@ -28,10 +28,11 @@ public class Reminder [Command("remindme")] [Description("Set a reminder for yourself. Example: !reminder 1h do the thing")] - [Aliases("reminder", "rember", "wemember", "remember", "remind")] + [TextAlias("reminder", "rember", "wemember", "remember", "remind")] + [AllowedProcessors(typeof(TextCommandProcessor))] [RequireHomeserverPerm(ServerPermLevel.Tier4, WorkOutside = true)] public async Task RemindMe( - CommandContext ctx, + TextCommandContext ctx, [Description("The amount of time to wait before reminding you. For example: 2s, 5m, 1h, 1d")] string timetoParse, [RemainingText, Description("The text to send when the reminder triggers.")] string reminder ) diff --git a/Commands/SecurityActions.cs b/Commands/SecurityActions.cs index b1c7851a..2ea5b5e5 100644 --- a/Commands/SecurityActions.cs +++ b/Commands/SecurityActions.cs @@ -1,11 +1,12 @@ namespace Cliptok.Commands { - public class SecurityActions : BaseCommandModule + public class SecurityActions { [Command("pausedms")] [Description("Temporarily pause DMs between server members.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] - public async Task PauseDMs(CommandContext ctx, [Description("The amount of time to pause DMs for."), RemainingText] string time) + public async Task PauseDMs(TextCommandContext ctx, [Description("The amount of time to pause DMs for."), RemainingText] string time) { if (string.IsNullOrWhiteSpace(time)) { @@ -68,8 +69,9 @@ public async Task PauseDMs(CommandContext ctx, [Description("The amount of time [Command("unpausedms")] [Description("Unpause DMs between server members.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] - public async Task UnpauseDMs(CommandContext ctx) + public async Task UnpauseDMs(TextCommandContext ctx) { // need to make our own api calls because D#+ can't do this natively? diff --git a/Commands/TechSupport.cs b/Commands/TechSupport.cs index 6a73ceb9..f1728bb7 100644 --- a/Commands/TechSupport.cs +++ b/Commands/TechSupport.cs @@ -1,11 +1,12 @@ namespace Cliptok.Commands { - internal class TechSupport : BaseCommandModule + internal class TechSupport { [Command("ask")] [Description("Outputs information on how and where to ask tech support questions. Replying to a message while triggering the command will mirror the reply in the response.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer] - public async Task AskCmd(CommandContext ctx, [Description("Optional, a user to ping with the information")] DiscordUser user = default) + public async Task AskCmd(TextCommandContext ctx, [Description("Optional, a user to ping with the information")] DiscordUser user = default) { await ctx.Message.DeleteAsync(); DiscordEmbedBuilder embed = new DiscordEmbedBuilder() diff --git a/Commands/Threads.cs b/Commands/Threads.cs index dd7fa4ea..c412876a 100644 --- a/Commands/Threads.cs +++ b/Commands/Threads.cs @@ -1,11 +1,12 @@ namespace Cliptok.Commands { - internal class Threads : BaseCommandModule + internal class Threads { [Command("archive")] [Description("Archive the current thread or another thread.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public async Task ArchiveCommand(CommandContext ctx, DiscordChannel channel = default) + public async Task ArchiveCommand(TextCommandContext ctx, DiscordChannel channel = default) { if (channel == default) channel = ctx.Channel; @@ -27,8 +28,9 @@ await thread.ModifyAsync(a => [Command("lockthread")] [Description("Lock the current thread or another thread.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public async Task LockThreadCommand(CommandContext ctx, DiscordChannel channel = default) + public async Task LockThreadCommand(TextCommandContext ctx, DiscordChannel channel = default) { if (channel == default) channel = ctx.Channel; @@ -50,8 +52,9 @@ await thread.ModifyAsync(a => [Command("unarchive")] [Description("Unarchive a thread")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public async Task UnarchiveCommand(CommandContext ctx, DiscordChannel channel = default) + public async Task UnarchiveCommand(TextCommandContext ctx, DiscordChannel channel = default) { if (channel == default) channel = ctx.Channel; diff --git a/Commands/Timestamp.cs b/Commands/Timestamp.cs index 47c07e5e..af532575 100644 --- a/Commands/Timestamp.cs +++ b/Commands/Timestamp.cs @@ -1,17 +1,20 @@ -namespace Cliptok.Commands +using DSharpPlus.Commands.Trees.Metadata; + +namespace Cliptok.Commands { - internal class Timestamp : BaseCommandModule + internal class Timestamp { - [Group("timestamp")] - [Aliases("ts", "time")] + [Command("timestamp")] + [TextAlias("ts", "time")] [Description("Returns various timestamps for a given Discord ID/snowflake")] + [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer] - class TimestampCmds : BaseCommandModule + class TimestampCmds { - [GroupCommand] - [Aliases("u", "unix", "epoch")] + [DefaultGroupCommand] + [TextAlias("u", "unix", "epoch")] [Description("Returns the Unix timestamp of a given Discord ID/snowflake")] - public async Task TimestampUnixCmd(CommandContext ctx, [Description("The ID/snowflake to fetch the Unix timestamp for")] ulong snowflake) + public async Task TimestampUnixCmd(TextCommandContext ctx, [Description("The ID/snowflake to fetch the Unix timestamp for")] ulong snowflake) { var msSinceEpoch = snowflake >> 22; var msUnix = msSinceEpoch + 1420070400000; @@ -19,9 +22,9 @@ public async Task TimestampUnixCmd(CommandContext ctx, [Description("The ID/snow } [Command("relative")] - [Aliases("r")] + [TextAlias("r")] [Description("Returns the amount of time between now and a given Discord ID/snowflake")] - public async Task TimestampRelativeCmd(CommandContext ctx, [Description("The ID/snowflake to fetch the relative timestamp for")] ulong snowflake) + public async Task TimestampRelativeCmd(TextCommandContext ctx, [Description("The ID/snowflake to fetch the relative timestamp for")] ulong snowflake) { var msSinceEpoch = snowflake >> 22; var msUnix = msSinceEpoch + 1420070400000; @@ -29,9 +32,9 @@ public async Task TimestampRelativeCmd(CommandContext ctx, [Description("The ID/ } [Command("fulldate")] - [Aliases("f", "datetime")] + [TextAlias("f", "datetime")] [Description("Returns the fully-formatted date and time of a given Discord ID/snowflake")] - public async Task TimestampFullCmd(CommandContext ctx, [Description("The ID/snowflake to fetch the full timestamp for")] ulong snowflake) + public async Task TimestampFullCmd(TextCommandContext ctx, [Description("The ID/snowflake to fetch the full timestamp for")] ulong snowflake) { var msSinceEpoch = snowflake >> 22; var msUnix = msSinceEpoch + 1420070400000; diff --git a/Commands/UserRoles.cs b/Commands/UserRoles.cs index 0b874ec7..45c27f89 100644 --- a/Commands/UserRoles.cs +++ b/Commands/UserRoles.cs @@ -1,14 +1,14 @@ namespace Cliptok.Commands { [UserRolesPresent] - public class UserRoleCmds : BaseCommandModule + public class UserRoleCmds { - public static async Task GiveUserRoleAsync(CommandContext ctx, ulong role) + public static async Task GiveUserRoleAsync(TextCommandContext ctx, ulong role) { await GiveUserRolesAsync(ctx, x => (ulong)x.GetValue(Program.cfgjson.UserRoles, null) == role); } - public static async Task GiveUserRolesAsync(CommandContext ctx, Func predicate) + public static async Task GiveUserRolesAsync(TextCommandContext ctx, Func predicate) { if (Program.cfgjson.UserRoles is null) { @@ -42,13 +42,13 @@ public static async Task GiveUserRolesAsync(CommandContext ctx, Func (ulong)x.GetValue(Program.cfgjson.UserRoles, null) == role); } - public static async Task RemoveUserRolesAsync(CommandContext ctx, Func predicate) + public static async Task RemoveUserRolesAsync(TextCommandContext ctx, Func predicate) { if (Program.cfgjson.UserRoles is null) { @@ -73,11 +73,12 @@ public static async Task RemoveUserRolesAsync(CommandContext ctx, Func true); } [ Command("leave-insiders"), - Aliases("leave-insider"), + TextAlias("leave-insider"), Description("Removes you from Insider roles"), + AllowedProcessors(typeof(TextCommandProcessor)), HomeServer ] - public async Task LeaveInsiders(CommandContext ctx) + public async Task LeaveInsiders(TextCommandContext ctx) { foreach (ulong roleId in new ulong[] { Program.cfgjson.UserRoles.InsiderDev, Program.cfgjson.UserRoles.InsiderBeta, Program.cfgjson.UserRoles.InsiderRP, Program.cfgjson.UserRoles.InsiderCanary, Program.cfgjson.UserRoles.InsiderDev }) { await RemoveUserRoleAsync(ctx, roleId); } - var msg = await ctx.RespondAsync($"{Program.cfgjson.Emoji.Insider} You are no longer receiving Windows Insider notifications. If you ever wish to receive Insider notifications again, you can check the <#740272437719072808> description for the commands."); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Insider} You are no longer receiving Windows Insider notifications. If you ever wish to receive Insider notifications again, you can check the <#740272437719072808> description for the commands."); + var msg = await ctx.GetResponseAsync(); await Task.Delay(10000); await msg.DeleteAsync(); } @@ -193,64 +204,70 @@ public async Task LeaveInsiders(CommandContext ctx) [ Command("dont-keep-me-updated"), Description("Takes away from you all opt-in roles"), + AllowedProcessors(typeof(TextCommandProcessor)), HomeServer ] - public async Task DontKeepMeUpdated(CommandContext ctx) + public async Task DontKeepMeUpdated(TextCommandContext ctx) { await RemoveUserRolesAsync(ctx, x => true); } [ Command("leave-insider-dev"), - Aliases("leave-insiders-dev"), + TextAlias("leave-insiders-dev"), Description("Removes the Windows 11 Insiders (Dev) role"), + AllowedProcessors(typeof(TextCommandProcessor)), HomeServer ] - public async Task LeaveInsiderDevCmd(CommandContext ctx) + public async Task LeaveInsiderDevCmd(TextCommandContext ctx) { await RemoveUserRoleAsync(ctx, Program.cfgjson.UserRoles.InsiderDev); } [ Command("leave-insider-canary"), - Aliases("leave-insiders-canary", "leave-insider-can", "leave-insiders-can"), + TextAlias("leave-insiders-canary", "leave-insider-can", "leave-insiders-can"), Description("Removes the Windows 11 Insiders (Canary) role"), + AllowedProcessors(typeof(TextCommandProcessor)), HomeServer ] - public async Task LeaveInsiderCanaryCmd(CommandContext ctx) + public async Task LeaveInsiderCanaryCmd(TextCommandContext ctx) { await RemoveUserRoleAsync(ctx, Program.cfgjson.UserRoles.InsiderCanary); } [ Command("leave-insider-beta"), - Aliases("leave-insiders-beta"), + TextAlias("leave-insiders-beta"), Description("Removes the Windows 11 Insiders (Beta) role"), + AllowedProcessors(typeof(TextCommandProcessor)), HomeServer ] - public async Task LeaveInsiderBetaCmd(CommandContext ctx) + public async Task LeaveInsiderBetaCmd(TextCommandContext ctx) { await RemoveUserRoleAsync(ctx, Program.cfgjson.UserRoles.InsiderBeta); } [ Command("leave-insider-10"), - Aliases("leave-insiders-10"), + TextAlias("leave-insiders-10"), Description("Removes the Windows 10 Insiders (Release Preview) role"), + AllowedProcessors(typeof(TextCommandProcessor)), HomeServer ] - public async Task LeaveInsiderRPCmd(CommandContext ctx) + public async Task LeaveInsiderRPCmd(TextCommandContext ctx) { await RemoveUserRoleAsync(ctx, Program.cfgjson.UserRoles.Insider10RP); } [ Command("leave-insider-rp"), - Aliases("leave-insiders-rp", "leave-insiders-11-rp", "leave-insider-11-rp"), + TextAlias("leave-insiders-rp", "leave-insiders-11-rp", "leave-insider-11-rp"), Description("Removes the Windows 11 Insiders (Release Preview) role"), + AllowedProcessors(typeof(TextCommandProcessor)), HomeServer ] - public async Task LeaveInsider10RPCmd(CommandContext ctx) + public async Task LeaveInsider10RPCmd(TextCommandContext ctx) { await RemoveUserRoleAsync(ctx, Program.cfgjson.UserRoles.InsiderRP); } @@ -258,9 +275,10 @@ public async Task LeaveInsider10RPCmd(CommandContext ctx) [ Command("leave-patch-tuesday"), Description("Removes the 💻 Patch Tuesday role"), + AllowedProcessors(typeof(TextCommandProcessor)), HomeServer ] - public async Task LeavePatchTuesday(CommandContext ctx) + public async Task LeavePatchTuesday(TextCommandContext ctx) { await RemoveUserRoleAsync(ctx, Program.cfgjson.UserRoles.PatchTuesday); } diff --git a/Commands/Utility.cs b/Commands/Utility.cs index fff1edc6..1a557a1d 100644 --- a/Commands/Utility.cs +++ b/Commands/Utility.cs @@ -1,10 +1,11 @@ namespace Cliptok.Commands { - internal class Utility : BaseCommandModule + internal class Utility { [Command("ping")] [Description("Pong? This command lets you know whether I'm working well.")] - public async Task Ping(CommandContext ctx) + [AllowedProcessors(typeof(TextCommandProcessor))] + public async Task Ping(TextCommandContext ctx) { ctx.Client.Logger.LogDebug(ctx.Client.GetConnectionLatency(Program.cfgjson.ServerID).ToString()); DiscordMessage return_message = await ctx.Message.RespondAsync("Pinging..."); @@ -18,9 +19,10 @@ await return_message.ModifyAsync($"P{letter}ng! 🏓\n" + [Command("edit")] [Description("Edit a message.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [RequireHomeserverPerm(ServerPermLevel.Moderator)] public async Task Edit( - CommandContext ctx, + TextCommandContext ctx, [Description("The ID of the message to edit.")] ulong messageId, [RemainingText, Description("New message content.")] string content ) @@ -37,9 +39,10 @@ public async Task Edit( [Command("editappend")] [Description("Append content to an existing bot message with a newline.")] + [AllowedProcessors(typeof(TextCommandProcessor))] [RequireHomeserverPerm(ServerPermLevel.Moderator)] public async Task EditAppend( - CommandContext ctx, + TextCommandContext ctx, [Description("The ID of the message to edit")] ulong messageId, [RemainingText, Description("Content to append on the end of the message.")] string content ) @@ -63,9 +66,10 @@ public async Task EditAppend( [Command("userinfo")] [Description("Show info about a user.")] - [Aliases("user-info", "whois")] + [TextAlias("userinfo", "user-info", "whois")] + [AllowedProcessors(typeof(TextCommandProcessor))] public async Task UserInfoCommand( - CommandContext ctx, + TextCommandContext ctx, DiscordUser user = null) { if (user is null) diff --git a/Commands/Warnings.cs b/Commands/Warnings.cs index 196dc199..01958be0 100644 --- a/Commands/Warnings.cs +++ b/Commands/Warnings.cs @@ -3,16 +3,17 @@ namespace Cliptok.Commands { - public class Warnings : BaseCommandModule + public class Warnings { [ - Command("warn"), + Command("6158e255-e8b3-4467-8d1a-79f89829"), Description("Issues a formal warning to a user."), - Aliases("wam", "warm"), + TextAlias("warn", "wam", "warm"), + AllowedProcessors(typeof(TextCommandProcessor)), HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator) ] public async Task WarnCmd( - CommandContext ctx, + TextCommandContext ctx, [Description("The user you are warning. Accepts many formats.")] DiscordUser targetUser, [RemainingText, Description("The reason for giving this warning.")] string reason = null ) @@ -54,11 +55,12 @@ public async Task WarnCmd( [ Command("anonwarn"), Description("Issues a formal warning to a user from a private channel."), - Aliases("anonwam", "anonwarm"), + TextAlias("anonwam", "anonwarm"), + AllowedProcessors(typeof(TextCommandProcessor)), HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator) ] public async Task AnonWarnCmd( - CommandContext ctx, + TextCommandContext ctx, [Description("The channel you wish for the warning message to appear in.")] DiscordChannel targetChannel, [Description("The user you are warning. Accepts many formats.")] DiscordUser targetUser, [RemainingText, Description("The reason for giving this warning.")] string reason = null @@ -91,13 +93,14 @@ public async Task AnonWarnCmd( } [ - Command("warnings"), + Command("6158e255-e8b3-4467-8d1a-79f89810"), Description("Shows a list of warnings that a user has been given. For more in-depth information, use the 'warnlookup' command."), - Aliases("infractions", "warnfractions", "wammings", "wamfractions"), + TextAlias("warnings", "infractions", "warnfractions", "wammings", "wamfractions"), + AllowedProcessors(typeof(TextCommandProcessor)), HomeServer ] public async Task WarningCmd( - CommandContext ctx, + TextCommandContext ctx, [Description("The user you want to look up warnings for. Accepts many formats.")] DiscordUser targetUser = null ) { @@ -108,13 +111,14 @@ public async Task WarningCmd( } [ - Command("delwarn"), + Command("6158e255-e8b3-4467-8d1a-79f89811"), Description("Delete a warning that was issued by mistake or later became invalid."), - Aliases("delwarm", "delwam", "deletewarn", "delwarning", "deletewarning"), + TextAlias("delwarn", "delwarm", "delwam", "deletewarn", "delwarning", "deletewarning"), + AllowedProcessors(typeof(TextCommandProcessor)), HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator) ] public async Task DelwarnCmd( - CommandContext ctx, + TextCommandContext ctx, [Description("The user you're removing a warning from. Accepts many formats.")] DiscordUser targetUser, [Description("The ID of the warning you want to delete.")] long warnId ) @@ -155,11 +159,12 @@ await LogChannelHelper.LogMessageAsync("mod", [ Command("warnlookup"), Description("Looks up information about a warning. Shows only publicly available information."), - Aliases("warning", "warming", "waming", "wamming", "lookup", "lookylooky", "peek", "investigate", "what-did-i-do-wrong-there", "incident"), + TextAlias("warning", "warming", "waming", "wamming", "lookup", "lookylooky", "peek", "investigate", "what-did-i-do-wrong-there", "incident"), + AllowedProcessors(typeof(TextCommandProcessor)), HomeServer ] public async Task WarnlookupCmd( - CommandContext ctx, + TextCommandContext ctx, [Description("The user you're looking at a warning for. Accepts many formats.")] DiscordUser targetUser, [Description("The ID of the warning you want to see")] long warnId ) @@ -172,14 +177,15 @@ public async Task WarnlookupCmd( } [ - Command("warndetails"), - Aliases("warninfo", "waminfo", "wamdetails", "warndetail", "wamdetail"), + Command("6158e255-e8b3-4467-8d1a-79f89822"), + TextAlias("warndetails", "warninfo", "waminfo", "wamdetails", "warndetail", "wamdetail"), Description("Check the details of a warning in depth. Shows extra information (Such as responsible Mod) that may not be wanted to be public."), + AllowedProcessors(typeof(TextCommandProcessor)), HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator) ] public async Task WarnDetailsCmd( - CommandContext ctx, + TextCommandContext ctx, [Description("The user you're looking up detailed warn information for. Accepts many formats.")] DiscordUser targetUser, [Description("The ID of the warning you're looking at in detail.")] long warnId ) @@ -198,15 +204,16 @@ public async Task WarnDetailsCmd( } [ - Command("editwarn"), - Aliases("warnedit", "editwarning"), + Command("6158e255-e8b3-4467-8d1a-79f89812"), + TextAlias("editwarn", "warnedit", "editwarning"), Description("Edit the reason of an existing warning.\n" + "The Moderator who is editing the reason will become responsible for the case."), + AllowedProcessors(typeof(TextCommandProcessor)), HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator) ] public async Task EditwarnCmd( - CommandContext ctx, + TextCommandContext ctx, [Description("The user you're editing a warning for. Accepts many formats.")] DiscordUser targetUser, [Description("The ID of the warning you want to edit.")] long warnId, [RemainingText, Description("The new reason for the warning.")] string newReason) @@ -217,7 +224,8 @@ public async Task EditwarnCmd( return; } - var msg = await ctx.RespondAsync("Processing your request..."); + await ctx.RespondAsync("Processing your request..."); + var msg = await ctx.GetResponseAsync(); var warning = GetWarning(targetUser.Id, warnId); if (warning is null) await msg.ModifyAsync($"{Program.cfgjson.Emoji.Error} I couldn't find a warning for that user with that ID! Please check again."); @@ -245,8 +253,9 @@ await LogChannelHelper.LogMessageAsync("mod", } [Command("mostwarnings"), Description("Who has the most warnings???")] + [AllowedProcessors(typeof(TextCommandProcessor))] [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public async Task MostWarningsCmd(CommandContext ctx) + public async Task MostWarningsCmd(TextCommandContext ctx) { await DiscordHelpers.SafeTyping(ctx.Channel); @@ -276,8 +285,9 @@ public async Task MostWarningsCmd(CommandContext ctx) } [Command("mostwarningsday"), Description("Which day has the most warnings???")] + [AllowedProcessors(typeof(TextCommandProcessor))] [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public async Task MostWarningsDayCmd(CommandContext ctx) + public async Task MostWarningsDayCmd(TextCommandContext ctx) { await DiscordHelpers.SafeTyping(ctx.Channel); diff --git a/Events/ErrorEvents.cs b/Events/ErrorEvents.cs index 7f8bb503..c84360a9 100644 --- a/Events/ErrorEvents.cs +++ b/Events/ErrorEvents.cs @@ -4,16 +4,40 @@ namespace Cliptok.Events { public class ErrorEvents { - public static async Task CommandsNextService_CommandErrored(CommandsNextExtension _, CommandErrorEventArgs e) + public static async Task CommandErrored(CommandsExtension _, CommandErroredEventArgs e) { - if (e.Exception is CommandNotFoundException && (e.Command is null || e.Command.QualifiedName != "help")) + // Because we no longer have DSharpPlus.CommandsNext or DSharpPlus.SlashCommands (only DSharpPlus.Commands), we can't point to different + // error handlers based on command type in our command handler configuration. Instead, we can start here, and jump to the correct + // handler based on the command type. TODO(#202): hopefully. + + // This is a lazy approach that just takes error type and points to the error handlers we already had. + // Maybe it can be improved later? + + if (e.Context is TextCommandContext) + { + // Text command error + await TextCommandErrored(e); + } + else if (e.Context is SlashCommandContext) + { + // Interaction command error (slash, user ctx, message ctx) + } + else + { + // Maybe left as CommandContext... TODO(#202): how to handle? + } + } + + public static async Task TextCommandErrored(CommandErroredEventArgs e) + { + if (e.Exception is CommandNotFoundException && (e.Context.Command is null || e.Context.Command.FullName != "help")) return; // avoid conflicts with modmail - if (e.Command.QualifiedName == "edit" || e.Command.QualifiedName == "timestamp") + if (e.Context.Command.FullName == "edit" || e.Context.Command.FullName == "timestamp") return; - e.Context.Client.Logger.LogError(CliptokEventID, e.Exception, "Exception occurred during {user}s invocation of {command}", e.Context.User.Username, e.Context.Command.QualifiedName); + e.Context.Client.Logger.LogError(CliptokEventID, e.Exception, "Exception occurred during {user}s invocation of {command}", e.Context.User.Username, e.Context.Command.FullName); var exs = new List(); if (e.Exception is AggregateException ae) @@ -23,17 +47,17 @@ public static async Task CommandsNextService_CommandErrored(CommandsNextExtensio foreach (var ex in exs) { - if (ex is CommandNotFoundException && (e.Command is null || e.Command.QualifiedName != "help")) + if (ex is CommandNotFoundException && (e.Context.Command is null || e.Context.Command.FullName != "help")) return; - if (ex is ChecksFailedException && (e.Command.Name != "help")) + if (ex is ChecksFailedException && (e.Context.Command.Name != "help")) return; var embed = new DiscordEmbedBuilder { Color = new DiscordColor("#FF0000"), Title = "An exception occurred when executing a command", - Description = $"{cfgjson.Emoji.BSOD} `{e.Exception.GetType()}` occurred when executing `{e.Command.QualifiedName}`.", + Description = $"{cfgjson.Emoji.BSOD} `{e.Exception.GetType()}` occurred when executing `{e.Context.Command.FullName}`.", Timestamp = DateTime.UtcNow }; embed.WithFooter(discord.CurrentUser.Username, discord.CurrentUser.AvatarUrl) diff --git a/Events/InteractionEvents.cs b/Events/InteractionEvents.cs index ec478095..92159032 100644 --- a/Events/InteractionEvents.cs +++ b/Events/InteractionEvents.cs @@ -207,54 +207,51 @@ await LogChannelHelper.LogDeletedMessagesAsync( } - public static async Task SlashCommandErrorEvent(SlashCommandsExtension _, DSharpPlus.SlashCommands.EventArgs.SlashCommandErrorEventArgs e) + public static async Task SlashCommandErrored(CommandErroredEventArgs e) { - if (e.Exception is SlashExecutionChecksFailedException slex) + if (e.Exception is ChecksFailedException slex) { - foreach (var check in slex.FailedChecks) - if (check is SlashRequireHomeserverPermAttribute att && e.Context.CommandName != "edit") + foreach (var check in slex.Errors) // TODO(#202): test this!!! + if (check.ContextCheckAttribute is SlashRequireHomeserverPermAttribute att && e.Context.Command.Name != "edit") { var level = (await GetPermLevelAsync(e.Context.Member)); var levelText = level.ToString(); if (level == ServerPermLevel.Nothing && rand.Next(1, 100) == 69) levelText = $"naught but a thing, my dear human. Congratulations, you win {rand.Next(1, 10)} bonus points."; - await e.Context.CreateResponseAsync( - DiscordInteractionResponseType.ChannelMessageWithSource, - new DiscordInteractionResponseBuilder().WithContent( - $"{cfgjson.Emoji.NoPermissions} Invalid permission level to use command **{e.Context.CommandName}**!\n" + + await e.Context.RespondAsync(new DiscordInteractionResponseBuilder().WithContent( + $"{cfgjson.Emoji.NoPermissions} Invalid permission level to use command **{e.Context.Command.Name}**!\n" + $"Required: `{att.TargetLvl}`\n" + $"You have: `{levelText}`") .AsEphemeral(true) ); } } - e.Context.Client.Logger.LogError(CliptokEventID, e.Exception, "Error during invocation of interaction command {command} by {user}", e.Context.CommandName, $"{DiscordHelpers.UniqueUsername(e.Context.User)}"); + e.Context.Client.Logger.LogError(CliptokEventID, e.Exception, "Error during invocation of interaction command {command} by {user}", e.Context.Command.Name, $"{DiscordHelpers.UniqueUsername(e.Context.User)}"); } - public static async Task ContextCommandErrorEvent(SlashCommandsExtension _, DSharpPlus.SlashCommands.EventArgs.ContextMenuErrorEventArgs e) + public static async Task ContextCommandErrored(CommandErroredEventArgs e) { - if (e.Exception is SlashExecutionChecksFailedException slex) + if (e.Exception is ChecksFailedException slex) { - foreach (var check in slex.FailedChecks) - if (check is SlashRequireHomeserverPermAttribute att && e.Context.CommandName != "edit") + foreach (var check in slex.Errors) // TODO(#202): test this!!! + if (check.ContextCheckAttribute is SlashRequireHomeserverPermAttribute att && e.Context.Command.Name != "edit") { var level = (await GetPermLevelAsync(e.Context.Member)); var levelText = level.ToString(); if (level == ServerPermLevel.Nothing && rand.Next(1, 100) == 69) levelText = $"naught but a thing, my dear human. Congratulations, you win {rand.Next(1, 10)} bonus points."; - await e.Context.CreateResponseAsync( - DiscordInteractionResponseType.ChannelMessageWithSource, + await e.Context.RespondAsync( new DiscordInteractionResponseBuilder().WithContent( - $"{cfgjson.Emoji.NoPermissions} Invalid permission level to use command **{e.Context.CommandName}**!\n" + + $"{cfgjson.Emoji.NoPermissions} Invalid permission level to use command **{e.Context.Command.Name}**!\n" + $"Required: `{att.TargetLvl}`\n" + $"You have: `{levelText}`") .AsEphemeral(true) ); } } - e.Context.Client.Logger.LogError(CliptokEventID, e.Exception, "Error during invocation of context command {command} by {user}", e.Context.CommandName, $"{DiscordHelpers.UniqueUsername(e.Context.User)}"); + e.Context.Client.Logger.LogError(CliptokEventID, e.Exception, "Error during invocation of context command {command} by {user}", e.Context.Command.Name, $"{DiscordHelpers.UniqueUsername(e.Context.User)}"); } } diff --git a/GlobalUsings.cs b/GlobalUsings.cs index 3b26ef20..e936a843 100644 --- a/GlobalUsings.cs +++ b/GlobalUsings.cs @@ -3,12 +3,19 @@ global using Cliptok.Events; global using Cliptok.Helpers; global using DSharpPlus; -global using DSharpPlus.CommandsNext; -global using DSharpPlus.CommandsNext.Attributes; -global using DSharpPlus.CommandsNext.Exceptions; +global using DSharpPlus.Commands; +global using DSharpPlus.Commands.ArgumentModifiers; +global using DSharpPlus.Commands.ContextChecks; +global using DSharpPlus.Commands.EventArgs; +global using DSharpPlus.Commands.Exceptions; +global using DSharpPlus.Commands.Processors.SlashCommands; +global using DSharpPlus.Commands.Processors.SlashCommands.ArgumentModifiers; +global using DSharpPlus.Commands.Processors.TextCommands; +global using DSharpPlus.Commands.Processors.UserCommands; +global using DSharpPlus.Commands.Trees; +global using DSharpPlus.Commands.Trees.Metadata; global using DSharpPlus.Entities; global using DSharpPlus.EventArgs; -global using DSharpPlus.SlashCommands; global using Microsoft.Extensions.Logging; global using Newtonsoft.Json; global using Newtonsoft.Json.Linq; @@ -17,6 +24,7 @@ global using StackExchange.Redis; global using System; global using System.Collections.Generic; +global using System.ComponentModel; global using System.Diagnostics; global using System.IO; global using System.Linq; diff --git a/Helpers/InteractionHelpers.cs b/Helpers/InteractionHelpers.cs index e69c112a..70b42ad6 100644 --- a/Helpers/InteractionHelpers.cs +++ b/Helpers/InteractionHelpers.cs @@ -2,12 +2,12 @@ { public static class BaseContextExtensions { - public static async Task PrepareResponseAsync(this BaseContext ctx) + public static async Task PrepareResponseAsync(this CommandContext ctx) { - await ctx.CreateResponseAsync(DiscordInteractionResponseType.DeferredChannelMessageWithSource); + await ctx.DeferResponseAsync(); } - public static async Task RespondAsync(this BaseContext ctx, string text = null, DiscordEmbed embed = null, bool ephemeral = false, bool mentions = true, params DiscordComponent[] components) + public static async Task RespondAsync(this CommandContext ctx, string text = null, DiscordEmbed embed = null, bool ephemeral = false, bool mentions = true, params DiscordComponent[] components) { DiscordInteractionResponseBuilder response = new(); @@ -18,10 +18,10 @@ public static async Task RespondAsync(this BaseContext ctx, string text = null, response.AsEphemeral(ephemeral); response.AddMentions(mentions ? Mentions.All : Mentions.None); - await ctx.CreateResponseAsync(DiscordInteractionResponseType.ChannelMessageWithSource, response); + await ctx.RespondAsync(response); } - public static async Task EditAsync(this BaseContext ctx, string text = null, DiscordEmbed embed = null, params DiscordComponent[] components) + public static async Task EditAsync(this CommandContext ctx, string text = null, DiscordEmbed embed = null, params DiscordComponent[] components) { DiscordWebhookBuilder response = new(); @@ -32,7 +32,7 @@ public static async Task EditAsync(this BaseContext ctx, string text = null, Dis await ctx.EditResponseAsync(response); } - public static async Task FollowAsync(this BaseContext ctx, string text = null, DiscordEmbed embed = null, bool ephemeral = false, params DiscordComponent[] components) + public static async Task FollowAsync(this CommandContext ctx, string text = null, DiscordEmbed embed = null, bool ephemeral = false, params DiscordComponent[] components) { DiscordFollowupMessageBuilder response = new(); @@ -44,7 +44,7 @@ public static async Task FollowAsync(this BaseContext ctx, string text = null, D response.AsEphemeral(ephemeral); - await ctx.FollowUpAsync(response); + await ctx.FollowupAsync(response); } } } diff --git a/Program.cs b/Program.cs index 340b9877..ed1cd7c1 100644 --- a/Program.cs +++ b/Program.cs @@ -2,6 +2,7 @@ using DSharpPlus.Net.Gateway; using Serilog.Sinks.Grafana.Loki; using System.Reflection; +using DSharpPlus.Commands.EventArgs; namespace Cliptok { @@ -36,10 +37,9 @@ public async Task SessionInvalidatedAsync(IGatewayClient _) { } } - class Program : BaseCommandModule + class Program { public static DiscordClient discord; - static CommandsNextExtension commands; public static Random rnd = new(); public static ConfigJson cfgjson; public static ConnectionMultiplexer redis; @@ -179,6 +179,20 @@ static async Task Main(string[] _) discordBuilder.ConfigureServices(services => { services.Replace(); + services.AddCommandsExtension(builder => + { + builder.CommandErrored += ErrorEvents.CommandErrored; + + // Interaction commands + var slashCommandClasses = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsClass && t.Namespace == "Cliptok.Commands.InteractionCommands" && !t.IsNested); + foreach (var type in slashCommandClasses) + builder.AddCommands(type, cfgjson.ServerID); + + // Text commands TODO(#202): [Error] Failed to build command '"editwarn"' System.ArgumentException: An item with the same key has already been added. Key: editwarn + var commandClasses = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsClass && t.Namespace == "Cliptok.Commands" && !t.IsNested); + foreach (var type in commandClasses) + builder.AddCommands(type); + }); }); discordBuilder.ConfigureExtraFeatures(clientConfig => @@ -211,30 +225,6 @@ static async Task Main(string[] _) .HandleAutoModerationRuleExecuted(AutoModEvents.AutoModerationRuleExecuted) ); -#pragma warning disable CS0618 // Type or member is obsolete - discordBuilder.UseSlashCommands(slash => - { - slash.SlashCommandErrored += InteractionEvents.SlashCommandErrorEvent; - slash.ContextMenuErrored += InteractionEvents.ContextCommandErrorEvent; - - var slashCommandClasses = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsClass && t.Namespace == "Cliptok.Commands.InteractionCommands" && !t.IsNested); - foreach (var type in slashCommandClasses) - slash.RegisterCommands(type, cfgjson.ServerID); ; - }); -#pragma warning restore CS0618 // Type or member is obsolete - - discordBuilder.UseCommandsNext(commands => - { - var commandClasses = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsClass && t.Namespace == "Cliptok.Commands" && !t.IsNested); - foreach (var type in commandClasses) - commands.RegisterCommands(type); - - commands.CommandErrored += ErrorEvents.CommandsNextService_CommandErrored; - }, new CommandsNextConfiguration - { - StringPrefixes = cfgjson.Core.Prefixes - }); - // TODO(erisa): At some point we might be forced to ConnectAsync() the builder directly // and then we will need to rework some other pieces that rely on Program.discord discord = discordBuilder.Build(); From 78d5e655f9c73b527b4e1584693f00cfd5752dea Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Fri, 25 Oct 2024 14:12:00 -0400 Subject: [PATCH 02/31] Fix missed breaking changes --- .../AnnouncementInteractions.cs | 20 +++++++------- .../InteractionCommands/RoleInteractions.cs | 20 +++++++------- .../TechSupportInteractions.cs | 16 +++++------ .../UserNoteInteractions.cs | 6 ++--- .../WarningInteractions.cs | 6 ++--- Program.cs | 27 ++++++++++--------- 6 files changed, 49 insertions(+), 46 deletions(-) diff --git a/Commands/InteractionCommands/AnnouncementInteractions.cs b/Commands/InteractionCommands/AnnouncementInteractions.cs index 43983327..b372ba9d 100644 --- a/Commands/InteractionCommands/AnnouncementInteractions.cs +++ b/Commands/InteractionCommands/AnnouncementInteractions.cs @@ -261,26 +261,26 @@ public async Task AnnounceBuildSlashCommand(SlashCommandContext ctx, internal class WindowsVersionChoiceProvider : IChoiceProvider { - public async ValueTask> ProvideAsync(CommandParameter _) + public async ValueTask> ProvideAsync(CommandParameter _) { - return new Dictionary + return new List { - { "Windows 10", "10" }, - { "Windows 11", "11" } + new("Windows 10", "10"), + new("Windows 11", "11") }; } } internal class WindowsInsiderChannelChoiceProvider : IChoiceProvider { - public async ValueTask> ProvideAsync(CommandParameter _) + public async ValueTask> ProvideAsync(CommandParameter _) { - return new Dictionary + return new List { - { "Canary Channel", "Canary" }, - { "Dev Channel", "Dev" }, - { "Beta Channel", "Beta" }, - { "Release Preview Channel", "RP" } + new("Canary Channel", "Canary"), + new("Dev Channel", "Dev"), + new("Beta Channel", "Beta"), + new("Release Preview Channel", "RP") }; } } diff --git a/Commands/InteractionCommands/RoleInteractions.cs b/Commands/InteractionCommands/RoleInteractions.cs index 5eccada4..8df0af88 100644 --- a/Commands/InteractionCommands/RoleInteractions.cs +++ b/Commands/InteractionCommands/RoleInteractions.cs @@ -76,18 +76,18 @@ public async Task RemoveRole( internal class RoleCommandChoiceProvider : IChoiceProvider { - public async ValueTask> ProvideAsync(CommandParameter _) + public async ValueTask> ProvideAsync(CommandParameter _) { - return new Dictionary + return new List { - { "Windows 11 Insiders (Canary)", "insiderCanary" }, - { "Windows 11 Insiders (Dev)", "insiderDev" }, - { "Windows 11 Insiders (Beta)", "insiderBeta" }, - { "Windows 11 Insiders (Release Preview)", "insiderRP" }, - { "Windows 10 Insiders (Release Preview)", "insider10RP" }, - { "Windows 10 Insiders (Beta)", "insider10Beta" }, - { "Patch Tuesday", "patchTuesday" }, - { "Giveaways", "giveaways" } + new("Windows 11 Insiders (Canary)", "insiderCanary"), + new("Windows 11 Insiders (Dev)", "insiderDev"), + new("Windows 11 Insiders (Beta)", "insiderBeta"), + new("Windows 11 Insiders (Release Preview)", "insiderRP"), + new("Windows 10 Insiders (Release Preview)", "insider10RP"), + new("Windows 10 Insiders (Beta)", "insider10Beta"), + new("Patch Tuesday", "patchTuesday"), + new("Giveaways", "giveaways") }; } } diff --git a/Commands/InteractionCommands/TechSupportInteractions.cs b/Commands/InteractionCommands/TechSupportInteractions.cs index 102d0105..8b7da512 100644 --- a/Commands/InteractionCommands/TechSupportInteractions.cs +++ b/Commands/InteractionCommands/TechSupportInteractions.cs @@ -36,16 +36,16 @@ public async Task RedistsCommand( internal class VcRedistChoiceProvider : IChoiceProvider { - public async ValueTask> ProvideAsync(CommandParameter _) + public async ValueTask> ProvideAsync(CommandParameter _) { - return new Dictionary + return new List { - { "Visual Studio 2015+ - v140", "140" }, - { "Visual Studio 2013 - v120", "120" }, - { "Visual Studio 2012 - v110", "110" }, - { "Visual Studio 2010 - v100", "100" }, - { "Visual Studio 2008 - v90", "90" }, - { "Visual Studio 2005 - v80", "80" } + new("Visual Studio 2015+ - v140", "140"), + new("Visual Studio 2013 - v120", "120"), + new("Visual Studio 2012 - v110", "110"), + new("Visual Studio 2010 - v100", "100"), + new("Visual Studio 2008 - v90", "90"), + new("Visual Studio 2005 - v80", "80") }; } } diff --git a/Commands/InteractionCommands/UserNoteInteractions.cs b/Commands/InteractionCommands/UserNoteInteractions.cs index eaab5274..49091e8d 100644 --- a/Commands/InteractionCommands/UserNoteInteractions.cs +++ b/Commands/InteractionCommands/UserNoteInteractions.cs @@ -195,9 +195,9 @@ public async Task ShowUserNoteAsync(SlashCommandContext ctx, private class NotesAutocompleteProvider : IAutoCompleteProvider { - public async ValueTask> AutoCompleteAsync(AutoCompleteContext ctx) + public async ValueTask> AutoCompleteAsync(AutoCompleteContext ctx) { - var list = new Dictionary(); + var list = new List(); var useroption = ctx.Options.FirstOrDefault(x => x.Name == "user"); if (useroption == default) @@ -223,7 +223,7 @@ public async ValueTask> AutoCompleteAsync(Au var focusedOption = ctx.Options.FirstOrDefault(option => option.Focused); if (focusedOption is not null) // TODO(#202): is this right? if (note.Value.NoteText.Contains((string)focusedOption.Value) || noteString.ToLower().Contains(focusedOption.Value.ToString().ToLower())) - list.Add(noteString, StringHelpers.Pad(note.Value.NoteId)); + list.Add(new DiscordAutoCompleteChoice(noteString, StringHelpers.Pad(note.Value.NoteId))); } return list; diff --git a/Commands/InteractionCommands/WarningInteractions.cs b/Commands/InteractionCommands/WarningInteractions.cs index f1621d68..9f399cd9 100644 --- a/Commands/InteractionCommands/WarningInteractions.cs +++ b/Commands/InteractionCommands/WarningInteractions.cs @@ -141,9 +141,9 @@ await LogChannelHelper.LogMessageAsync("mod", internal partial class WarningsAutocompleteProvider : IAutoCompleteProvider { - public async ValueTask> AutoCompleteAsync(AutoCompleteContext ctx) + public async ValueTask> AutoCompleteAsync(AutoCompleteContext ctx) { - var list = new Dictionary(); + var list = new List(); var useroption = ctx.Options.FirstOrDefault(x => x.Name == "user"); if (useroption == default) @@ -169,7 +169,7 @@ public async ValueTask> AutoCompleteAsync(Au var focusedOption = ctx.Options.FirstOrDefault(option => option.Focused); if (focusedOption is not null) // TODO(#202): is this right? if (warning.Value.WarnReason.Contains((string)focusedOption.Value) || warningString.ToLower().Contains(focusedOption.Value.ToString().ToLower())) - list.Add(warningString, StringHelpers.Pad(warning.Value.WarningId)); + list.Add(new DiscordAutoCompleteChoice(warningString, StringHelpers.Pad(warning.Value.WarningId))); } return list; diff --git a/Program.cs b/Program.cs index ed1cd7c1..571eb830 100644 --- a/Program.cs +++ b/Program.cs @@ -32,6 +32,8 @@ public async Task ReconnectRequestedAsync(IGatewayClient _) { } public async Task ReconnectFailedAsync(IGatewayClient _) { } public async Task SessionInvalidatedAsync(IGatewayClient _) { } + + public async Task ResumeAttemptedAsync(IGatewayClient _) { } @@ -179,20 +181,21 @@ static async Task Main(string[] _) discordBuilder.ConfigureServices(services => { services.Replace(); - services.AddCommandsExtension(builder => - { - builder.CommandErrored += ErrorEvents.CommandErrored; + }); + + discordBuilder.UseCommands((_, builder) => + { + builder.CommandErrored += ErrorEvents.CommandErrored; - // Interaction commands - var slashCommandClasses = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsClass && t.Namespace == "Cliptok.Commands.InteractionCommands" && !t.IsNested); - foreach (var type in slashCommandClasses) - builder.AddCommands(type, cfgjson.ServerID); + // Interaction commands + var slashCommandClasses = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsClass && t.Namespace == "Cliptok.Commands.InteractionCommands" && !t.IsNested); + foreach (var type in slashCommandClasses) + builder.AddCommands(type, cfgjson.ServerID); - // Text commands TODO(#202): [Error] Failed to build command '"editwarn"' System.ArgumentException: An item with the same key has already been added. Key: editwarn - var commandClasses = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsClass && t.Namespace == "Cliptok.Commands" && !t.IsNested); - foreach (var type in commandClasses) - builder.AddCommands(type); - }); + // Text commands TODO(#202): [Error] Failed to build command '"editwarn"' System.ArgumentException: An item with the same key has already been added. Key: editwarn + var commandClasses = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsClass && t.Namespace == "Cliptok.Commands" && !t.IsNested); + foreach (var type in commandClasses) + builder.AddCommands(type); }); discordBuilder.ConfigureExtraFeatures(clientConfig => From 3f0012ab952dbe9e8a94e872626aba7889f5e3c0 Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Thu, 14 Nov 2024 11:53:53 -0500 Subject: [PATCH 03/31] Adjust text command names to avoid duplicate commands when registering Text command names are in the format "commandtextcmd", like "bantextcmd"; the original name ("ban") is now the first alias --- Commands/Announcements.cs | 6 ++-- Commands/Bans.cs | 15 +++++---- Commands/Debug.cs | 4 +-- Commands/Dehoist.cs | 12 ++++--- Commands/DmRelayBlock.cs | 4 +-- Commands/FunCmds.cs | 7 ++-- Commands/Grant.cs | 2 +- Commands/Kick.cs | 7 ++-- Commands/Lists.cs | 17 ++++++---- Commands/Lockdown.cs | 9 +++--- Commands/Mutes.cs | 13 ++++---- Commands/Raidmode.cs | 3 +- Commands/Reminders.cs | 4 +-- Commands/SecurityActions.cs | 6 ++-- Commands/TechSupport.cs | 3 +- Commands/Threads.cs | 9 ++++-- Commands/Timestamp.cs | 4 +-- Commands/UserRoles.cs | 64 ++++++++++++++++++++----------------- Commands/Utility.cs | 13 +++++--- Commands/Warnings.cs | 30 +++++++++-------- 20 files changed, 132 insertions(+), 100 deletions(-) diff --git a/Commands/Announcements.cs b/Commands/Announcements.cs index f5114215..e25213d1 100644 --- a/Commands/Announcements.cs +++ b/Commands/Announcements.cs @@ -3,7 +3,8 @@ internal class Announcements { - [Command("editannounce")] + [Command("editannouncetextcmd")] + [TextAlias("editannounce")] [Description("Edit an announcement, preserving the ping highlight.")] [AllowedProcessors(typeof(TextCommandProcessor))] [RequireHomeserverPerm(ServerPermLevel.Moderator)] @@ -39,7 +40,8 @@ public async Task EditAnnounce( } } - [Command("announce")] + [Command("announcetextcmd")] + [TextAlias("announce")] [Description("Announces something in the current channel, pinging an Insider role in the process.")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] diff --git a/Commands/Bans.cs b/Commands/Bans.cs index c17a5256..deee90b3 100644 --- a/Commands/Bans.cs +++ b/Commands/Bans.cs @@ -4,8 +4,8 @@ namespace Cliptok.Commands { class Bans { - [Command("massban")] - [TextAlias("bigbonk")] + [Command("massbantextcmd")] + [TextAlias("massban", "bigbonk")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] public async Task MassBanCmd(TextCommandContext ctx, [RemainingText] string input) @@ -42,8 +42,8 @@ public async Task MassBanCmd(TextCommandContext ctx, [RemainingText] string inpu await loading.DeleteAsync(); } - [Command("ban")] - [TextAlias("tempban", "bonk", "isekaitruck")] + [Command("bantextcmd")] + [TextAlias("ban", "tempban", "bonk", "isekaitruck")] [Description("Bans a user that you have permission to ban, deleting all their messages in the process. See also: bankeep.")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.BanMembers)] @@ -135,8 +135,8 @@ public async Task BanCmd(TextCommandContext ctx, /// I CANNOT find a way to do this as alias so I made it a separate copy of the command. /// Sue me, I beg you. - [Command("bankeep")] - [TextAlias("bansave")] + [Command("bankeeptextcmd")] + [TextAlias("bankeep", "bansave")] [Description("Bans a user but keeps their messages around."), HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.BanMembers)] [AllowedProcessors(typeof(TextCommandProcessor))] public async Task BankeepCmd(TextCommandContext ctx, @@ -218,7 +218,8 @@ public async Task BankeepCmd(TextCommandContext ctx, await ctx.Channel.SendMessageAsync($"{Program.cfgjson.Emoji.Banned} {targetMember.Mention} has been banned for **{TimeHelpers.TimeToPrettyFormat(banDuration, false)}**: **{reason}**"); } - [Command("unban")] + [Command("unbantextcmd")] + [TextAlias("unban")] [Description("Unbans a user who has been previously banned.")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.BanMembers)] diff --git a/Commands/Debug.cs b/Commands/Debug.cs index 16bd2574..317aa825 100644 --- a/Commands/Debug.cs +++ b/Commands/Debug.cs @@ -6,8 +6,8 @@ internal class Debug { public static Dictionary OverridesPendingAddition = new(); - [Command("debug")] - [TextAlias("troubleshoot", "unbug", "bugn't", "helpsomethinghasgoneverywrong")] + [Command("debugtextcmd")] + [TextAlias("debug", "troubleshoot", "unbug", "bugn't", "helpsomethinghasgoneverywrong")] [Description("Commands and things for fixing the bot in the unlikely event that it breaks a bit.")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] diff --git a/Commands/Dehoist.cs b/Commands/Dehoist.cs index 5fbc3cf9..ff42067a 100644 --- a/Commands/Dehoist.cs +++ b/Commands/Dehoist.cs @@ -4,7 +4,8 @@ namespace Cliptok.Commands { internal class Dehoist { - [Command("dehoist")] + [Command("dehoisttextcmd")] + [TextAlias("dehoist")] [Description("Adds an invisible character to someone's nickname that drops them to the bottom of the member list. Accepts multiple members.")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] @@ -69,7 +70,8 @@ await discordMember.ModifyAsync(a => _ = await msg.ModifyAsync($"{Program.cfgjson.Emoji.Success} Successfully dehoisted {discordMembers.Length - failedCount} of {discordMembers.Length} member(s)! (Check Audit Log for details)"); } - [Command("massdehoist")] + [Command("massdehoisttextcmd")] + [TextAlias("massdehoist")] [Description("Dehoist everyone on the server who has a bad name. This may take a while and can exhaust rate limits.")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] @@ -91,7 +93,8 @@ public async Task MassDehoist(TextCommandContext ctx) await ctx.Channel.SendMessageAsync(new DiscordMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Success} Successfully dehoisted {discordMembers.Count() - failedCount} of {discordMembers.Count()} member(s)! (Check Audit Log for details)").WithReply(ctx.Message.Id, true, false)); } - [Command("massundehoist")] + [Command("massundehoisttextcmd")] + [TextAlias("massundehoist")] [Description("Remove the dehoist for users attached via a txt file.")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] @@ -151,7 +154,8 @@ await member.ModifyAsync(a => } } - [Command("permadehoist")] + [Command("permadehoisttextcmd")] + [TextAlias("permadehoist")] [Description("Permanently/persistently dehoist members.")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] diff --git a/Commands/DmRelayBlock.cs b/Commands/DmRelayBlock.cs index 60cbe8fd..40b7ea33 100644 --- a/Commands/DmRelayBlock.cs +++ b/Commands/DmRelayBlock.cs @@ -2,10 +2,10 @@ { internal class DmRelayBlock { - [Command("dmrelayblock")] + [Command("dmrelayblocktextcmd")] + [TextAlias("dmrelayblock", "dmblock")] [Description("Stop a member's DMs from being relayed to the configured DM relay channel.")] [AllowedProcessors(typeof(TextCommandProcessor))] - [TextAlias("dmblock")] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] public async Task DmRelayBlockCommand(TextCommandContext ctx, [Description("The member to stop relaying DMs from.")] DiscordUser user) { diff --git a/Commands/FunCmds.cs b/Commands/FunCmds.cs index 3b717310..5fec2614 100644 --- a/Commands/FunCmds.cs +++ b/Commands/FunCmds.cs @@ -2,7 +2,8 @@ { internal class FunCmds { - [Command("tellraw")] + [Command("tellrawtextcmd")] + [TextAlias("tellraw")] [Description("Nothing of interest.")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] @@ -21,9 +22,9 @@ public async Task TellRaw(TextCommandContext ctx, [Description("???")] DiscordCh } - [Command("no")] + [Command("notextcmd")] + [TextAlias("no", "yes")] [Description("Makes Cliptok choose something for you. Outputs either Yes or No.")] - [TextAlias("yes")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Tier5)] public async Task No(TextCommandContext ctx) diff --git a/Commands/Grant.cs b/Commands/Grant.cs index a3483028..20eae9f2 100644 --- a/Commands/Grant.cs +++ b/Commands/Grant.cs @@ -2,7 +2,7 @@ { internal class Grant { - [Command("grant")] + [Command("granttextcmd")] [Description("Grant a user access to the server, by giving them the Tier 1 role.")] [TextAlias("grant", "clipgrant", "verify")] [AllowedProcessors(typeof(TextCommandProcessor))] diff --git a/Commands/Kick.cs b/Commands/Kick.cs index 87b49b58..5fbb43bb 100644 --- a/Commands/Kick.cs +++ b/Commands/Kick.cs @@ -2,8 +2,8 @@ { internal class Kick { - [Command("kick")] - [TextAlias("yeet", "shoo", "goaway", "defenestrate")] + [Command("kicktextcmd")] + [TextAlias("kick", "yeet", "shoo", "goaway", "defenestrate")] [Description("Kicks a user, removing them from the server until they rejoin. Generally not very useful.")] [AllowedProcessors(typeof(TextCommandProcessor))] [RequirePermissions(DiscordPermissions.KickMembers), HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] @@ -50,7 +50,8 @@ public async Task KickCmd(TextCommandContext ctx, DiscordUser target, [Remaining } } - [Command("masskick")] + [Command("masskicktextcmd")] + [TextAlias("masskick")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] public async Task MassKickCmd(TextCommandContext ctx, [RemainingText] string input) diff --git a/Commands/Lists.cs b/Commands/Lists.cs index 76f0fe01..537f40dd 100644 --- a/Commands/Lists.cs +++ b/Commands/Lists.cs @@ -23,7 +23,8 @@ public class GitHubDispatchInputs public string User { get; set; } } - [Command("listupdate")] + [Command("listupdatetextcmd")] + [TextAlias("listupdate")] [Description("Updates the private lists from the GitHub repository, then reloads them into memory.")] [AllowedProcessors(typeof(TextCommandProcessor))] [RequireHomeserverPerm(ServerPermLevel.Moderator)] @@ -55,7 +56,8 @@ public async Task ListUpdate(TextCommandContext ctx) } - [Command("listadd")] + [Command("listaddtextcmd")] + [TextAlias("listadd")] [Description("Add a piece of text to a public list.")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] @@ -135,7 +137,8 @@ await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} An error with code `{resp $"Body: ```json\n{responseText}```"); } - [Command("scamcheck")] + [Command("scamchecktextcmd")] + [TextAlias("scamcheck")] [Description("Check if a link or message is known to the anti-phishing API.")] [AllowedProcessors(typeof(TextCommandProcessor))] [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] @@ -167,8 +170,8 @@ public async Task ScamCheck(TextCommandContext ctx, [RemainingText, Description( } } - [Command("joinwatch")] - [TextAlias("joinnotify", "leavewatch", "leavenotify")] + [Command("joinwatchtextcmd")] + [TextAlias("joinwatch", "joinnotify", "leavewatch", "leavenotify")] [Description("Watch for joins and leaves of a given user. Output goes to #investigations.")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] @@ -216,8 +219,8 @@ public async Task JoinWatch( } } - [Command("appealblock")] - [TextAlias("superduperban", "ablock")] + [Command("appealblocktextcmd")] + [TextAlias("appealblock", "superduperban", "ablock")] [Description("Prevents a user from submitting ban appeals.")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] diff --git a/Commands/Lockdown.cs b/Commands/Lockdown.cs index 68af2cf8..27307e05 100644 --- a/Commands/Lockdown.cs +++ b/Commands/Lockdown.cs @@ -4,8 +4,8 @@ class Lockdown { public bool ongoingLockdown = false; - [Command("lockdown")] - [TextAlias("lock")] + [Command("lockdowntextcmd")] + [TextAlias("lockdown", "lock")] [Description("Locks the current channel, preventing any new messages. See also: unlock")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ManageChannels, DiscordPermissions.None)] @@ -97,10 +97,11 @@ await thread.ModifyAsync(a => await LockdownHelpers.LockChannelAsync(user: ctx.User, channel: currentChannel, duration: lockDuration, reason: reason); } - [Command("unlock")] + [Command("unlocktextcmd")] + [TextAlias("unlock", "unlockdown")] [Description("Unlocks a previously locked channel. See also: lockdown")] [AllowedProcessors(typeof(TextCommandProcessor))] - [TextAlias("unlockdown"), HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ManageChannels, DiscordPermissions.None)] + [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ManageChannels, DiscordPermissions.None)] public async Task UnlockCommand(TextCommandContext ctx, [RemainingText] string reason = "") { var currentChannel = ctx.Channel; diff --git a/Commands/Mutes.cs b/Commands/Mutes.cs index 85c89e7b..18f46399 100644 --- a/Commands/Mutes.cs +++ b/Commands/Mutes.cs @@ -2,8 +2,8 @@ namespace Cliptok.Commands { internal class Mutes { - [Command("unmute")] - [TextAlias("umute")] + [Command("unmutetextcmd")] + [TextAlias("unmute", "umute")] [Description("Unmutes a previously muted user, typically ahead of the standard expiration time. See also: mute")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] @@ -45,7 +45,8 @@ public async Task UnmuteCmd(TextCommandContext ctx, [Description("The user you'r } } - [Command("mute")] + [Command("mutetextcmd")] + [TextAlias("mute")] [Description("Mutes a user, preventing them from sending messages until they're unmuted. See also: unmute")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] @@ -99,9 +100,9 @@ public async Task MuteCmd( _ = MuteHelpers.MuteUserAsync(targetUser, reason, ctx.User.Id, ctx.Guild, ctx.Channel, muteDuration, true); } - [Command("tqsmute")] - [Description( - "Temporarily mutes a user, preventing them from sending messages in #tech-support and related channels until they're unmuted.")] + [Command("tqsmutetextcmd")] + [TextAlias("tqsmute")] + [Description("Temporarily mutes a user, preventing them from sending messages in #tech-support and related channels until they're unmuted.")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TechnicalQueriesSlayer)] public async Task TqsMuteCmd( diff --git a/Commands/Raidmode.cs b/Commands/Raidmode.cs index 365dcb62..ab8347c7 100644 --- a/Commands/Raidmode.cs +++ b/Commands/Raidmode.cs @@ -4,7 +4,8 @@ namespace Cliptok.Commands { internal class Raidmode { - [Command("clipraidmode")] + [Command("clipraidmodetextcmd")] + [TextAlias("clipraidmode")] [Description("Manage the server's raidmode, preventing joins while on.")] [AllowedProcessors(typeof(TextCommandProcessor))] [RequireHomeserverPerm(ServerPermLevel.Moderator)] diff --git a/Commands/Reminders.cs b/Commands/Reminders.cs index 9e49af29..8566c271 100644 --- a/Commands/Reminders.cs +++ b/Commands/Reminders.cs @@ -26,9 +26,9 @@ public class Reminder public DateTime OriginalTime { get; set; } } - [Command("remindme")] + [Command("remindmetextcmd")] [Description("Set a reminder for yourself. Example: !reminder 1h do the thing")] - [TextAlias("reminder", "rember", "wemember", "remember", "remind")] + [TextAlias("remindme", "reminder", "rember", "wemember", "remember", "remind")] [AllowedProcessors(typeof(TextCommandProcessor))] [RequireHomeserverPerm(ServerPermLevel.Tier4, WorkOutside = true)] public async Task RemindMe( diff --git a/Commands/SecurityActions.cs b/Commands/SecurityActions.cs index 2ea5b5e5..a5dfafd0 100644 --- a/Commands/SecurityActions.cs +++ b/Commands/SecurityActions.cs @@ -2,7 +2,8 @@ namespace Cliptok.Commands { public class SecurityActions { - [Command("pausedms")] + [Command("pausedmstextcmd")] + [TextAlias("pausedms")] [Description("Temporarily pause DMs between server members.")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] @@ -67,7 +68,8 @@ public async Task PauseDMs(TextCommandContext ctx, [Description("The amount of t } } - [Command("unpausedms")] + [Command("unpausedmstextcmd")] + [TextAlias("unpausedms")] [Description("Unpause DMs between server members.")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] diff --git a/Commands/TechSupport.cs b/Commands/TechSupport.cs index f1728bb7..cb9d8483 100644 --- a/Commands/TechSupport.cs +++ b/Commands/TechSupport.cs @@ -2,7 +2,8 @@ { internal class TechSupport { - [Command("ask")] + [Command("asktextcmd")] + [TextAlias("ask")] [Description("Outputs information on how and where to ask tech support questions. Replying to a message while triggering the command will mirror the reply in the response.")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer] diff --git a/Commands/Threads.cs b/Commands/Threads.cs index c412876a..b4a6faec 100644 --- a/Commands/Threads.cs +++ b/Commands/Threads.cs @@ -2,7 +2,8 @@ { internal class Threads { - [Command("archive")] + [Command("archivetextcmd")] + [TextAlias("archive")] [Description("Archive the current thread or another thread.")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] @@ -26,7 +27,8 @@ await thread.ModifyAsync(a => }); } - [Command("lockthread")] + [Command("lockthreadtextcmd")] + [TextAlias("lockthread")] [Description("Lock the current thread or another thread.")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] @@ -50,7 +52,8 @@ await thread.ModifyAsync(a => }); } - [Command("unarchive")] + [Command("unarchivetextcmd")] + [TextAlias("unarchive")] [Description("Unarchive a thread")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] diff --git a/Commands/Timestamp.cs b/Commands/Timestamp.cs index af532575..dc828434 100644 --- a/Commands/Timestamp.cs +++ b/Commands/Timestamp.cs @@ -4,8 +4,8 @@ namespace Cliptok.Commands { internal class Timestamp { - [Command("timestamp")] - [TextAlias("ts", "time")] + [Command("timestamptextcmd")] + [TextAlias("timestamp", "ts", "time")] [Description("Returns various timestamps for a given Discord ID/snowflake")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer] diff --git a/Commands/UserRoles.cs b/Commands/UserRoles.cs index 45c27f89..dadc19b0 100644 --- a/Commands/UserRoles.cs +++ b/Commands/UserRoles.cs @@ -72,8 +72,8 @@ public static async Task RemoveUserRolesAsync(TextCommandContext ctx, Func Date: Fri, 15 Nov 2024 11:23:46 -0500 Subject: [PATCH 04/31] Resolve remaining TODOs --- Cliptok.csproj | 5 +- CommandChecks/HomeServerPerms.cs | 60 ++++++------------- CommandChecks/OwnerChecks.cs | 14 ++--- CommandChecks/UserRoleChecks.cs | 8 ++- .../AnnouncementInteractions.cs | 6 +- .../InteractionCommands/BanInteractions.cs | 10 ++-- .../InteractionCommands/ClearInteractions.cs | 2 +- .../InteractionCommands/ContextCommands.cs | 2 +- .../InteractionCommands/DebugInteractions.cs | 8 +-- .../DehoistInteractions.cs | 4 +- .../JoinwatchInteractions.cs | 2 +- .../LockdownInteractions.cs | 8 +-- .../InteractionCommands/MuteInteractions.cs | 8 +-- .../NicknameLockInteraction.cs | 2 +- .../RaidmodeInteractions.cs | 2 +- .../InteractionCommands/RoleInteractions.cs | 13 ++-- .../InteractionCommands/RulesInteractions.cs | 2 +- .../SecurityActionInteractions.cs | 4 +- .../SlowmodeInteractions.cs | 2 +- .../InteractionCommands/StatusInteractions.cs | 4 +- .../TechSupportInteractions.cs | 2 +- .../TrackingInteractions.cs | 2 +- .../UserNoteInteractions.cs | 4 +- .../WarningInteractions.cs | 17 +++--- Commands/TechSupportCommands.cs | 2 +- Events/ErrorEvents.cs | 8 +-- Events/InteractionEvents.cs | 8 +-- Program.cs | 25 +++++++- 28 files changed, 112 insertions(+), 122 deletions(-) diff --git a/Cliptok.csproj b/Cliptok.csproj index 8db4b462..54833409 100644 --- a/Cliptok.csproj +++ b/Cliptok.csproj @@ -13,9 +13,8 @@ - - - + + diff --git a/CommandChecks/HomeServerPerms.cs b/CommandChecks/HomeServerPerms.cs index 187a5170..25d8a509 100644 --- a/CommandChecks/HomeServerPerms.cs +++ b/CommandChecks/HomeServerPerms.cs @@ -66,7 +66,7 @@ public static async Task GetPermLevelAsync(DiscordMember target } [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = false)] - public class RequireHomeserverPermAttribute : ContextCheckAttribute // TODO(#202): checks changed!! see the checks section of https://dsharpplus.github.io/DSharpPlus/articles/migration/slashcommands_to_commands.html + public class RequireHomeserverPermAttribute : ContextCheckAttribute { public ServerPermLevel TargetLvl { get; set; } public bool WorkOutside { get; set; } @@ -79,16 +79,19 @@ public RequireHomeserverPermAttribute(ServerPermLevel targetlvl, bool workOutsid OwnerOverride = ownerOverride; TargetLvl = targetlvl; } + } - public async Task ExecuteCheckAsync(CommandContext ctx, bool help) + public class RequireHomeserverPermCheck : IContextCheck + { + public async ValueTask ExecuteCheckAsync(RequireHomeserverPermAttribute attribute, CommandContext ctx) { // If the command is supposed to stay within the server and its being used outside, fail silently - if (!WorkOutside && (ctx.Channel.IsPrivate || ctx.Guild.Id != Program.cfgjson.ServerID)) - return false; + if (!attribute.WorkOutside && (ctx.Channel.IsPrivate || ctx.Guild.Id != Program.cfgjson.ServerID)) + return "This command must be used in the home server, but was executed outside of it."; // bot owners can bypass perm checks ONLY if the command allows it. - if (OwnerOverride && Program.cfgjson.BotOwners.Contains(ctx.User.Id)) - return true; + if (attribute.OwnerOverride && Program.cfgjson.BotOwners.Contains(ctx.User.Id)) + return null; DiscordMember member; if (ctx.Channel.IsPrivate || ctx.Guild.Id != Program.cfgjson.ServerID) @@ -100,7 +103,7 @@ public async Task ExecuteCheckAsync(CommandContext ctx, bool help) } catch (DSharpPlus.Exceptions.NotFoundException) { - return false; + return "The invoking user must be a member of the home server; they are not."; } } else @@ -109,50 +112,21 @@ public async Task ExecuteCheckAsync(CommandContext ctx, bool help) } var level = await GetPermLevelAsync(member); - if (level >= TargetLvl) - return true; - - else if (!help && ctx.Command.FullName != "edit") - { - var levelText = level.ToString(); - if (level == ServerPermLevel.Nothing && Program.rand.Next(1, 100) == 69) - levelText = $"naught but a thing, my dear human. Congratulations, you win {Program.rand.Next(1, 10)} bonus points."; + if (level >= attribute.TargetLvl) + return null; - await ctx.RespondAsync( - $"{Program.cfgjson.Emoji.NoPermissions} Invalid permissions to use command **{ctx.Command.Name}**!\n" + - $"Required: `{TargetLvl}`\nYou have: `{levelText}`"); - } - return false; + return "The invoking user does not have permission to use this command."; } } - public class HomeServerAttribute : ContextCheckAttribute - { - public async Task ExecuteCheckAsync(CommandContext ctx) - { - return !ctx.Channel.IsPrivate && ctx.Guild.Id == Program.cfgjson.ServerID; - } - } + public class HomeServerAttribute : ContextCheckAttribute; - public class SlashRequireHomeserverPermAttribute : ContextCheckAttribute + public class HomeServerCheck : IContextCheck { - public ServerPermLevel TargetLvl; - - public SlashRequireHomeserverPermAttribute(ServerPermLevel targetlvl) - => TargetLvl = targetlvl; - - public async Task ExecuteChecksAsync(CommandContext ctx) + public async ValueTask ExecuteCheckAsync(HomeServerAttribute attribute, CommandContext ctx) { - if (ctx.Guild.Id != Program.cfgjson.ServerID) - return false; - - var level = await GetPermLevelAsync(ctx.Member); - if (level >= TargetLvl) - return true; - else - return false; + return !ctx.Channel.IsPrivate && ctx.Guild.Id == Program.cfgjson.ServerID ? null : "This command must be used in the home server, but was executed outside of it."; } } - } } diff --git a/CommandChecks/OwnerChecks.cs b/CommandChecks/OwnerChecks.cs index cebcadb3..acf91d97 100644 --- a/CommandChecks/OwnerChecks.cs +++ b/CommandChecks/OwnerChecks.cs @@ -1,20 +1,18 @@ namespace Cliptok.CommandChecks { - public class IsBotOwnerAttribute : ContextCheckAttribute + public class IsBotOwnerAttribute : ContextCheckAttribute; + + public class IsBotOwnerCheck : IContextCheck { - public async Task ExecuteCheckAsync(CommandContext ctx, bool help) + public async ValueTask ExecuteCheckAsync(IsBotOwnerAttribute attribute, CommandContext ctx) { if (Program.cfgjson.BotOwners.Contains(ctx.User.Id)) { - return true; + return null; } else { - if (!help) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.NoPermissions} This command is only accessible to bot owners."); - } - return false; + return "Bot owner-only command was executed by a non-owner."; } } } diff --git a/CommandChecks/UserRoleChecks.cs b/CommandChecks/UserRoleChecks.cs index 5dd03384..7a5a7388 100644 --- a/CommandChecks/UserRoleChecks.cs +++ b/CommandChecks/UserRoleChecks.cs @@ -1,10 +1,12 @@ namespace Cliptok.CommandChecks { - public class UserRolesPresentAttribute : ContextCheckAttribute + public class UserRolesPresentAttribute : ContextCheckAttribute; + + public class UserRolesPresentCheck : IContextCheck { - public async Task ExecuteCheckAsync(CommandContext ctx) + public async ValueTask ExecuteCheckAsync(UserRolesPresentAttribute attribute, CommandContext ctx) { - return Program.cfgjson.UserRoles is not null; + return Program.cfgjson.UserRoles is null ? "A user role command was executed, but user roles are not configured in config.json." : null; } } } diff --git a/Commands/InteractionCommands/AnnouncementInteractions.cs b/Commands/InteractionCommands/AnnouncementInteractions.cs index b372ba9d..14c2cf07 100644 --- a/Commands/InteractionCommands/AnnouncementInteractions.cs +++ b/Commands/InteractionCommands/AnnouncementInteractions.cs @@ -5,7 +5,7 @@ internal class AnnouncementInteractions [Command("announcebuild")] [Description("Announce a Windows Insider build in the current channel.")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator)] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] [RequirePermissions(DiscordPermissions.ModerateMembers)] public async Task AnnounceBuildSlashCommand(SlashCommandContext ctx, [SlashChoiceProvider(typeof(WindowsVersionChoiceProvider))] @@ -16,10 +16,10 @@ public async Task AnnounceBuildSlashCommand(SlashCommandContext ctx, [Parameter("blog_link"), Description("The link to the Windows blog entry relating to this build.")] string blogLink, [SlashChoiceProvider(typeof(WindowsInsiderChannelChoiceProvider))] - [Parameter("insider_role1"), Description("The first insider role to ping.")] string insiderChannel1, // TODO(#202): test choices!!! + [Parameter("insider_role1"), Description("The first insider role to ping.")] string insiderChannel1, [SlashChoiceProvider(typeof(WindowsInsiderChannelChoiceProvider))] - [Parameter("insider_role2"), Description("The second insider role to ping.")] string insiderChannel2 = "", // TODO(#202): test choices!!! + [Parameter("insider_role2"), Description("The second insider role to ping.")] string insiderChannel2 = "", [Parameter("canary_create_new_thread"), Description("Enable this option if you want to create a new Canary thread for some reason")] bool canaryCreateNewThread = false, [Parameter("thread"), Description("The thread to mention in the announcement.")] DiscordChannel threadChannel = default, diff --git a/Commands/InteractionCommands/BanInteractions.cs b/Commands/InteractionCommands/BanInteractions.cs index b0a25d33..4efa716e 100644 --- a/Commands/InteractionCommands/BanInteractions.cs +++ b/Commands/InteractionCommands/BanInteractions.cs @@ -7,7 +7,7 @@ internal class BanInteractions [Command("ban")] [Description("Bans a user from the server, either permanently or temporarily.")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.BanMembers)] + [RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.BanMembers)] public async Task BanSlashCommand(SlashCommandContext ctx, [Parameter("user"), Description("The user to ban")] DiscordUser user, [Parameter("reason"), Description("The reason the user is being banned")] string reason, @@ -19,7 +19,7 @@ public async Task BanSlashCommand(SlashCommandContext ctx, { // Initial response to avoid the 3 second timeout, will edit later. var eout = new DiscordInteractionResponseBuilder().AsEphemeral(true); - await ctx.DeferResponseAsync(); // TODO(#202): ephemeral + await ctx.DeferResponseAsync(true); // Edits need a webhook rather than interaction..? DiscordWebhookBuilder webhookOut = new(); @@ -58,7 +58,7 @@ public async Task BanSlashCommand(SlashCommandContext ctx, { try { - banDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(DateTime.UtcNow); // TODO(#202): this used InteractionContext#Interaction.CreationTimestamp.LocalDateTime before, please test!! + banDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(ctx.Interaction.CreationTimestamp.DateTime); } catch { @@ -118,7 +118,7 @@ public async Task BanSlashCommand(SlashCommandContext ctx, [Command("unban")] [Description("Unbans a user who has been previously banned.")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.BanMembers)] + [RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.BanMembers)] public async Task SlashUnbanCommand(SlashCommandContext ctx, [Parameter("user"), Description("The ID or mention of the user to unban. Ignore the suggestions, IDs work.")] SnowflakeObject userId, [Parameter("reason"), Description("Used in audit log only currently")] string reason = "No reason specified.") { DiscordUser targetUser = default; @@ -151,7 +151,7 @@ public async Task SlashUnbanCommand(SlashCommandContext ctx, [Parameter("user"), [Command("kick")] [Description("Kicks a user, removing them from the server until they rejoin.")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.KickMembers)] + [RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.KickMembers)] public async Task KickCmd(SlashCommandContext ctx, [Parameter("user"), Description("The user you want to kick from the server.")] DiscordUser target, [Parameter("reason"), Description("The reason for kicking this user.")] string reason = "No reason specified.") { if (target.IsBot) diff --git a/Commands/InteractionCommands/ClearInteractions.cs b/Commands/InteractionCommands/ClearInteractions.cs index bcc6ec21..022502bf 100644 --- a/Commands/InteractionCommands/ClearInteractions.cs +++ b/Commands/InteractionCommands/ClearInteractions.cs @@ -7,7 +7,7 @@ public class ClearInteractions [Command("clear")] [Description("Delete many messages from the current channel.")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [HomeServer, SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ManageMessages, DiscordPermissions.ModerateMembers)] + [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ManageMessages, DiscordPermissions.ModerateMembers)] public async Task ClearSlashCommand(SlashCommandContext ctx, [Parameter("count"), Description("The number of messages to consider for deletion. Required if you don't use the 'up_to' argument.")] long count = 0, [Parameter("up_to"), Description("Optionally delete messages up to (not including) this one. Accepts IDs and links.")] string upTo = "", diff --git a/Commands/InteractionCommands/ContextCommands.cs b/Commands/InteractionCommands/ContextCommands.cs index 6fb48819..b45e67db 100644 --- a/Commands/InteractionCommands/ContextCommands.cs +++ b/Commands/InteractionCommands/ContextCommands.cs @@ -24,7 +24,7 @@ public async Task ContextAvatar(CommandContext ctx, DiscordUser targetUser) [Command("Show Notes")] [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] [AllowedProcessors(typeof(UserCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] public async Task ShowNotes(CommandContext ctx, DiscordUser targetUser) { await ctx.RespondAsync(embed: await UserNoteHelpers.GenerateUserNotesEmbedAsync(targetUser), ephemeral: true); diff --git a/Commands/InteractionCommands/DebugInteractions.cs b/Commands/InteractionCommands/DebugInteractions.cs index 5400f357..72f46bed 100644 --- a/Commands/InteractionCommands/DebugInteractions.cs +++ b/Commands/InteractionCommands/DebugInteractions.cs @@ -7,7 +7,7 @@ internal class DebugInteractions [Command("scamcheck")] [Description("Check if a link or message is known to the anti-phishing API.")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] public async Task ScamCheck(SlashCommandContext ctx, [Parameter("input"), Description("Domain or message content to scan.")] string content) { var urlMatches = Constants.RegexConstants.url_rx.Matches(content); @@ -38,7 +38,7 @@ public async Task ScamCheck(SlashCommandContext ctx, [Parameter("input"), Descri [Command("tellraw")] [Description("You know what you're here for.")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ModerateMembers)] + [RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ModerateMembers)] public async Task TellRaw(SlashCommandContext ctx, [Parameter("input"), Description("???")] string input, [Parameter("reply_msg_id"), Description("ID of message to use in a reply context.")] string replyID = "0", [Parameter("pingreply"), Description("Ping pong.")] bool pingreply = true, [Parameter("channel"), Description("Either mention or ID. Not a name.")] string discordChannel = default) { DiscordChannel channelObj = default; @@ -103,7 +103,7 @@ public async Task UserInfoSlashCommand(SlashCommandContext ctx, [Parameter("user [Command("muteinfo")] [Description("Show information about the mute for a user.")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator)] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] [RequirePermissions(DiscordPermissions.ModerateMembers)] public async Task MuteInfoSlashCommand( SlashCommandContext ctx, @@ -116,7 +116,7 @@ public async Task MuteInfoSlashCommand( [Command("baninfo")] [Description("Show information about the ban for a user.")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator)] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] [RequirePermissions(DiscordPermissions.ModerateMembers)] public async Task BanInfoSlashCommand( SlashCommandContext ctx, diff --git a/Commands/InteractionCommands/DehoistInteractions.cs b/Commands/InteractionCommands/DehoistInteractions.cs index 646e1020..ed6543a3 100644 --- a/Commands/InteractionCommands/DehoistInteractions.cs +++ b/Commands/InteractionCommands/DehoistInteractions.cs @@ -5,7 +5,7 @@ internal class DehoistInteractions [Command("dehoist")] [Description("Dehoist a member, dropping them to the bottom of the list. Lasts until they change nickname.")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ManageNicknames)] + [RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ManageNicknames)] public async Task DehoistSlashCmd(SlashCommandContext ctx, [Parameter("member"), Description("The member to dehoist.")] DiscordUser user) { DiscordMember member; @@ -43,7 +43,7 @@ await member.ModifyAsync(a => [Command("permadehoist")] [Description("Permanently/persistently dehoist members.")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ManageNicknames)] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ManageNicknames)] public class PermadehoistSlashCommands { [Command("enable")] diff --git a/Commands/InteractionCommands/JoinwatchInteractions.cs b/Commands/InteractionCommands/JoinwatchInteractions.cs index d112e94f..47e3a1a7 100644 --- a/Commands/InteractionCommands/JoinwatchInteractions.cs +++ b/Commands/InteractionCommands/JoinwatchInteractions.cs @@ -5,7 +5,7 @@ internal class JoinwatchInteractions [Command("joinwatch")] [Description("Watch for joins and leaves of a given user. Output goes to #investigations.")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator)] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] public class JoinwatchSlashCmds { [Command("add")] diff --git a/Commands/InteractionCommands/LockdownInteractions.cs b/Commands/InteractionCommands/LockdownInteractions.cs index eda4734b..5b135105 100644 --- a/Commands/InteractionCommands/LockdownInteractions.cs +++ b/Commands/InteractionCommands/LockdownInteractions.cs @@ -7,7 +7,7 @@ class LockdownInteractions [Command("lockdown")] [Description("Lock the current channel or all channels in the server, preventing new messages. See also: unlock")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [HomeServer, SlashRequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ManageChannels, DiscordPermissions.None)] + [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ManageChannels, DiscordPermissions.None)] public class LockdownCmds { [Command("channel")] @@ -44,7 +44,7 @@ await thread.ModifyAsync(a => if (!string.IsNullOrWhiteSpace(time)) { - lockDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(DateTime.Now); // TODO(#202): this used InteractionContext#Interaction.CreationTimestamp.DateTime before, please test!! + lockDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(ctx.Interaction.CreationTimestamp.DateTime); } var currentChannel = ctx.Channel; @@ -84,7 +84,7 @@ public async Task LockdownAllCommand( if (!string.IsNullOrWhiteSpace(time)) { - lockDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(DateTime.Now); // TODO(#202): this used InteractionContext#Interaction.CreationTimestamp.LocalDateTime before, please test!! + lockDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(ctx.Interaction.CreationTimestamp.DateTime); } foreach (var chanID in Program.cfgjson.LockdownEnabledChannels) @@ -109,7 +109,7 @@ public async Task LockdownAllCommand( [Command("unlock")] [Description("Unlock the current channel or all channels in the server, allowing new messages. See also: lockdown")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [HomeServer, SlashRequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ManageChannels, DiscordPermissions.None)] + [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ManageChannels, DiscordPermissions.None)] public class UnlockCmds { [Command("channel")] diff --git a/Commands/InteractionCommands/MuteInteractions.cs b/Commands/InteractionCommands/MuteInteractions.cs index 53a305b5..e525666f 100644 --- a/Commands/InteractionCommands/MuteInteractions.cs +++ b/Commands/InteractionCommands/MuteInteractions.cs @@ -5,7 +5,7 @@ internal class MuteInteractions [Command("mute")] [Description("Mute a user, temporarily or permanently.")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator)] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] [RequirePermissions(DiscordPermissions.ModerateMembers)] public async Task MuteSlashCommand( SlashCommandContext ctx, @@ -37,7 +37,7 @@ public async Task MuteSlashCommand( { try { - muteDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(DateTime.UtcNow); // TODO(#202): this used InteractionContext#Interaction.CreationTimestamp.DateTime before, please test!! + muteDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(ctx.Interaction.CreationTimestamp.LocalDateTime); } catch { @@ -53,7 +53,7 @@ public async Task MuteSlashCommand( [Command("unmute")] [Description("Unmute a user.")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator)] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] [RequirePermissions(DiscordPermissions.ModerateMembers)] public async Task UnmuteSlashCommand( SlashCommandContext ctx, @@ -99,7 +99,7 @@ public async Task UnmuteSlashCommand( [Command("tqsmute")] [Description("Temporarily mute a user in tech support channels.")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.TechnicalQueriesSlayer)] + [RequireHomeserverPerm(ServerPermLevel.TechnicalQueriesSlayer)] public async Task TqsMuteSlashCommand( SlashCommandContext ctx, [Parameter("user"), Description("The user to mute.")] DiscordUser targetUser, diff --git a/Commands/InteractionCommands/NicknameLockInteraction.cs b/Commands/InteractionCommands/NicknameLockInteraction.cs index f64bf32c..303314fa 100644 --- a/Commands/InteractionCommands/NicknameLockInteraction.cs +++ b/Commands/InteractionCommands/NicknameLockInteraction.cs @@ -11,7 +11,7 @@ public class NicknameLockInteraction [Command("nicknamelock")] [Description("Prevent a member from changing their nickname.")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ManageNicknames)] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ManageNicknames)] public class NicknameLockSlashCommands { [Command("enable")] diff --git a/Commands/InteractionCommands/RaidmodeInteractions.cs b/Commands/InteractionCommands/RaidmodeInteractions.cs index 7fa671c0..1ef613e4 100644 --- a/Commands/InteractionCommands/RaidmodeInteractions.cs +++ b/Commands/InteractionCommands/RaidmodeInteractions.cs @@ -5,7 +5,7 @@ internal class RaidmodeInteractions [Command("raidmode")] [Description("Commands relating to Raidmode")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.Moderator)] + [RequireHomeserverPerm(ServerPermLevel.Moderator)] [RequirePermissions(DiscordPermissions.ModerateMembers)] public class RaidmodeSlashCommands { diff --git a/Commands/InteractionCommands/RoleInteractions.cs b/Commands/InteractionCommands/RoleInteractions.cs index b1c64928..abf23f8d 100644 --- a/Commands/InteractionCommands/RoleInteractions.cs +++ b/Commands/InteractionCommands/RoleInteractions.cs @@ -5,7 +5,7 @@ internal class RoleInteractions [Command("grant")] [Description("Grant a user Tier 1, bypassing any verification requirements.")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] public async Task SlashGrant(SlashCommandContext ctx, [Parameter("user"), Description("The user to grant Tier 1 to.")] DiscordUser _) { await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} This command is deprecated and no longer works. Please right click (or tap and hold on mobile) the user and click \"Verify Member\" if available."); @@ -22,7 +22,7 @@ internal class RoleSlashCommands public async Task GrantRole( SlashCommandContext ctx, [SlashAutoCompleteProvider(typeof(RolesAutocompleteProvider))] - [Parameter("role"), Description("The role to opt into.")] string role) // TODO(#202): test choices!!! + [Parameter("role"), Description("The role to opt into.")] string role) { DiscordMember member = ctx.Member; @@ -63,7 +63,7 @@ public async Task GrantRole( public async Task RemoveRole( SlashCommandContext ctx, [SlashAutoCompleteProvider(typeof(RolesAutocompleteProvider))] - [Parameter("role"), Description("The role to opt out of.")] string role) // TODO(#202): test choices!!! + [Parameter("role"), Description("The role to opt out of.")] string role) { DiscordMember member = ctx.Member; @@ -94,9 +94,9 @@ public async Task RemoveRole( } } - internal class RolesAutocompleteProvider : IAutocompleteProvider + internal class RolesAutocompleteProvider : IAutoCompleteProvider { - public async Task> Provider(AutocompleteContext ctx) + public async ValueTask> AutoCompleteAsync(AutoCompleteContext ctx) { Dictionary options = new() { @@ -117,7 +117,8 @@ public async Task> Provider(AutocompleteC foreach (var option in options) { - if (ctx.FocusedOption.Value.ToString() == "" || option.Key.Contains(ctx.FocusedOption.Value.ToString(), StringComparison.OrdinalIgnoreCase)) + var focusedOption = ctx.Options.FirstOrDefault(option => option.Focused); + if (focusedOption.Value.ToString() == "" || option.Key.Contains(focusedOption.Value.ToString(), StringComparison.OrdinalIgnoreCase)) { if (option.Value == "cts" && !memberHasTqs) continue; list.Add(new DiscordAutoCompleteChoice(option.Key, option.Value)); diff --git a/Commands/InteractionCommands/RulesInteractions.cs b/Commands/InteractionCommands/RulesInteractions.cs index c54e6daa..a49b168e 100644 --- a/Commands/InteractionCommands/RulesInteractions.cs +++ b/Commands/InteractionCommands/RulesInteractions.cs @@ -10,7 +10,7 @@ internal class RulesSlashCommands { [Command("all")] [Description("Shows all of the community rules.")] - public async Task RulesAllCommand(SlashCommandContext ctx) + public async Task RulesAllCommand(SlashCommandContext ctx, [Parameter("public"), Description("Whether to show the response publicly.")] bool? isPublic = null) { var publicResponse = await DeterminePublicResponse(ctx.Member, ctx.Channel, isPublic); diff --git a/Commands/InteractionCommands/SecurityActionInteractions.cs b/Commands/InteractionCommands/SecurityActionInteractions.cs index 6dfb6bb5..d10055e9 100644 --- a/Commands/InteractionCommands/SecurityActionInteractions.cs +++ b/Commands/InteractionCommands/SecurityActionInteractions.cs @@ -5,7 +5,7 @@ public class SecurityActionInteractions [Command("pausedms")] [Description("Temporarily pause DMs between server members.")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [HomeServer, SlashRequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ModerateMembers)] + [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ModerateMembers)] public async Task SlashPauseDMs(SlashCommandContext ctx, [Parameter("time"), Description("The amount of time to pause DMs for. Cannot be greater than 24 hours.")] string time) { // need to make our own api calls because D#+ can't do this natively? @@ -55,7 +55,7 @@ public async Task SlashPauseDMs(SlashCommandContext ctx, [Parameter("time"), Des [Command("unpausedms")] [Description("Unpause DMs between server members.")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [HomeServer, SlashRequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ModerateMembers)] + [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ModerateMembers)] public async Task SlashUnpauseDMs(SlashCommandContext ctx) { // need to make our own api calls because D#+ can't do this natively? diff --git a/Commands/InteractionCommands/SlowmodeInteractions.cs b/Commands/InteractionCommands/SlowmodeInteractions.cs index 227ea4ba..e332574e 100644 --- a/Commands/InteractionCommands/SlowmodeInteractions.cs +++ b/Commands/InteractionCommands/SlowmodeInteractions.cs @@ -5,7 +5,7 @@ internal class SlowmodeInteractions [Command("slowmode")] [Description("Slow down the channel...")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator)] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] [RequirePermissions(DiscordPermissions.ModerateMembers)] public async Task SlowmodeSlashCommand( SlashCommandContext ctx, diff --git a/Commands/InteractionCommands/StatusInteractions.cs b/Commands/InteractionCommands/StatusInteractions.cs index b757ee3f..23ee7000 100644 --- a/Commands/InteractionCommands/StatusInteractions.cs +++ b/Commands/InteractionCommands/StatusInteractions.cs @@ -6,7 +6,7 @@ internal class StatusInteractions { [Command("status")] [Description("Status commands")] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator)] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] [RequirePermissions(DiscordPermissions.ModerateMembers)] public class StatusSlashCommands @@ -18,7 +18,7 @@ public class StatusSlashCommands public async Task StatusSetCommand( SlashCommandContext ctx, [Parameter("text"), Description("The text to use for the status.")] string statusText, - [Parameter("type"), Description("Defaults to custom. The type of status to use.")] DiscordActivityType statusType = DiscordActivityType.Custom // TODO(#202): test this!!!! + [Parameter("type"), Description("Defaults to custom. The type of status to use.")] DiscordActivityType statusType = DiscordActivityType.Custom ) { if (statusText.Length > 128) diff --git a/Commands/InteractionCommands/TechSupportInteractions.cs b/Commands/InteractionCommands/TechSupportInteractions.cs index 8b7da512..8d3b1cb9 100644 --- a/Commands/InteractionCommands/TechSupportInteractions.cs +++ b/Commands/InteractionCommands/TechSupportInteractions.cs @@ -11,7 +11,7 @@ public async Task RedistsCommand( SlashCommandContext ctx, [SlashChoiceProvider(typeof(VcRedistChoiceProvider))] - [Parameter("version"), Description("Visual Studio version number or year")] long version // TODO(#202): test choices!!! + [Parameter("version"), Description("Visual Studio version number or year")] long version ) { VcRedist redist = VcRedistConstants.VcRedists diff --git a/Commands/InteractionCommands/TrackingInteractions.cs b/Commands/InteractionCommands/TrackingInteractions.cs index dcd9ca79..3f92702d 100644 --- a/Commands/InteractionCommands/TrackingInteractions.cs +++ b/Commands/InteractionCommands/TrackingInteractions.cs @@ -5,7 +5,7 @@ internal class TrackingInteractions [Command("tracking")] [Description("Commands to manage message tracking of users")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] public class TrackingSlashCommands { [Command("add")] diff --git a/Commands/InteractionCommands/UserNoteInteractions.cs b/Commands/InteractionCommands/UserNoteInteractions.cs index 49091e8d..cf0c2e63 100644 --- a/Commands/InteractionCommands/UserNoteInteractions.cs +++ b/Commands/InteractionCommands/UserNoteInteractions.cs @@ -7,7 +7,7 @@ internal class UserNoteInteractions [Command("note")] [Description("Manage user notes")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] public class UserNoteSlashCommands { [Command("add")] @@ -221,7 +221,7 @@ public async ValueTask> AutoCompleteAsync string noteString = $"{StringHelpers.Pad(note.Value.NoteId)} - {StringHelpers.Truncate(note.Value.NoteText, 29, true)} - {TimeHelpers.TimeToPrettyFormat(DateTime.Now - note.Value.Timestamp, true)}"; var focusedOption = ctx.Options.FirstOrDefault(option => option.Focused); - if (focusedOption is not null) // TODO(#202): is this right? + if (focusedOption is not null) if (note.Value.NoteText.Contains((string)focusedOption.Value) || noteString.ToLower().Contains(focusedOption.Value.ToString().ToLower())) list.Add(new DiscordAutoCompleteChoice(noteString, StringHelpers.Pad(note.Value.NoteId))); } diff --git a/Commands/InteractionCommands/WarningInteractions.cs b/Commands/InteractionCommands/WarningInteractions.cs index c5a80d68..c49741a3 100644 --- a/Commands/InteractionCommands/WarningInteractions.cs +++ b/Commands/InteractionCommands/WarningInteractions.cs @@ -7,7 +7,7 @@ internal class WarningInteractions [Command("warn")] [Description("Formally warn a user, usually for breaking the server rules.")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator)] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] [RequirePermissions(DiscordPermissions.ModerateMembers)] public async Task WarnSlashCommand(SlashCommandContext ctx, [Parameter("user"), Description("The user to warn.")] DiscordUser user, @@ -17,8 +17,7 @@ public async Task WarnSlashCommand(SlashCommandContext ctx, ) { // Initial response to avoid the 3 second timeout, will edit later. - var eout = new DiscordInteractionResponseBuilder().AsEphemeral(true); - await ctx.RespondAsync(eout); + await ctx.DeferResponseAsync(true); // Edits need a webhook rather than interaction..? DiscordWebhookBuilder webhookOut; @@ -81,7 +80,7 @@ public async Task WarningsSlashCommand(SlashCommandContext ctx, [Command("transfer_warnings")] [Description("Transfer warnings from one user to another.")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.Moderator)] + [RequireHomeserverPerm(ServerPermLevel.Moderator)] [RequirePermissions(DiscordPermissions.ModerateMembers)] public async Task TransferWarningsSlashCommand(SlashCommandContext ctx, [Parameter("source_user"), Description("The user currently holding the warnings.")] DiscordUser sourceUser, @@ -90,7 +89,7 @@ public async Task TransferWarningsSlashCommand(SlashCommandContext ctx, [Parameter("force_override"), Description("DESTRUCTIVE OPERATION: Whether to OVERRIDE and DELETE the target users existing warnings.")] bool forceOverride = false ) { - await ctx.DeferResponseAsync(); // TODO(#202): how do you make this ephemeral? + await ctx.DeferResponseAsync(false); if (sourceUser == targetUser) { @@ -172,7 +171,7 @@ public async ValueTask> AutoCompleteAsync string warningString = $"{StringHelpers.Pad(warning.Value.WarningId)} - {StringHelpers.Truncate(warning.Value.WarnReason, 29, true)} - {TimeHelpers.TimeToPrettyFormat(DateTime.Now - warning.Value.WarnTimestamp, true)}"; var focusedOption = ctx.Options.FirstOrDefault(option => option.Focused); - if (focusedOption is not null) // TODO(#202): is this right? + if (focusedOption is not null) if (warning.Value.WarnReason.Contains((string)focusedOption.Value) || warningString.ToLower().Contains(focusedOption.Value.ToString().ToLower())) list.Add(new DiscordAutoCompleteChoice(warningString, StringHelpers.Pad(warning.Value.WarningId))); } @@ -185,7 +184,7 @@ public async ValueTask> AutoCompleteAsync [Command("warndetails")] [Description("Search for a warning and return its details.")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] public async Task WarndetailsSlashCommand(SlashCommandContext ctx, [Parameter("user"), Description("The user to fetch a warning for.")] DiscordUser user, [SlashAutoCompleteProvider(typeof(WarningsAutocompleteProvider)), Parameter("warning"), Description("Type to search! Find the warning you want to fetch.")] string warning, @@ -224,7 +223,7 @@ public async Task WarndetailsSlashCommand(SlashCommandContext ctx, [Command("delwarn")] [Description("Search for a warning and delete it!")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] public async Task DelwarnSlashCommand(SlashCommandContext ctx, [Parameter("user"), Description("The user to delete a warning for.")] DiscordUser targetUser, [SlashAutoCompleteProvider(typeof(WarningsAutocompleteProvider))][Parameter("warning"), Description("Type to search! Find the warning you want to delete.")] string warningId, @@ -288,7 +287,7 @@ await LogChannelHelper.LogMessageAsync("mod", [Command("editwarn")] [Description("Search for a warning and edit it!")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [SlashRequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermissions.ModerateMembers)] public async Task EditWarnSlashCommand(SlashCommandContext ctx, [Parameter("user"), Description("The user to fetch a warning for.")] DiscordUser user, [SlashAutoCompleteProvider(typeof(WarningsAutocompleteProvider))][Parameter("warning"), Description("Type to search! Find the warning you want to edit.")] string warning, diff --git a/Commands/TechSupportCommands.cs b/Commands/TechSupportCommands.cs index ed7576a7..dcf37d94 100644 --- a/Commands/TechSupportCommands.cs +++ b/Commands/TechSupportCommands.cs @@ -1,6 +1,6 @@ namespace Cliptok.Commands { - internal class TechSupportCommands : BaseCommandModule + internal class TechSupportCommands { [Command("on-call")] [Description("Give yourself the CTS role.")] diff --git a/Events/ErrorEvents.cs b/Events/ErrorEvents.cs index c84360a9..bf1c32f0 100644 --- a/Events/ErrorEvents.cs +++ b/Events/ErrorEvents.cs @@ -8,7 +8,7 @@ public static async Task CommandErrored(CommandsExtension _, CommandErroredEvent { // Because we no longer have DSharpPlus.CommandsNext or DSharpPlus.SlashCommands (only DSharpPlus.Commands), we can't point to different // error handlers based on command type in our command handler configuration. Instead, we can start here, and jump to the correct - // handler based on the command type. TODO(#202): hopefully. + // handler based on the command type. // This is a lazy approach that just takes error type and points to the error handlers we already had. // Maybe it can be improved later? @@ -21,10 +21,8 @@ public static async Task CommandErrored(CommandsExtension _, CommandErroredEvent else if (e.Context is SlashCommandContext) { // Interaction command error (slash, user ctx, message ctx) - } - else - { - // Maybe left as CommandContext... TODO(#202): how to handle? + if (e.Context is UserCommandContext) await InteractionEvents.ContextCommandErrored(e); // this works because UserCommandContext inherits from SlashCommandContext + else await InteractionEvents.SlashCommandErrored(e); } } diff --git a/Events/InteractionEvents.cs b/Events/InteractionEvents.cs index 92159032..cb55e7c1 100644 --- a/Events/InteractionEvents.cs +++ b/Events/InteractionEvents.cs @@ -211,8 +211,8 @@ public static async Task SlashCommandErrored(CommandErroredEventArgs e) { if (e.Exception is ChecksFailedException slex) { - foreach (var check in slex.Errors) // TODO(#202): test this!!! - if (check.ContextCheckAttribute is SlashRequireHomeserverPermAttribute att && e.Context.Command.Name != "edit") + foreach (var check in slex.Errors) + if (check.ContextCheckAttribute is RequireHomeserverPermAttribute att && e.Context.Command.Name != "edit") { var level = (await GetPermLevelAsync(e.Context.Member)); var levelText = level.ToString(); @@ -234,8 +234,8 @@ public static async Task ContextCommandErrored(CommandErroredEventArgs e) { if (e.Exception is ChecksFailedException slex) { - foreach (var check in slex.Errors) // TODO(#202): test this!!! - if (check.ContextCheckAttribute is SlashRequireHomeserverPermAttribute att && e.Context.Command.Name != "edit") + foreach (var check in slex.Errors) + if (check.ContextCheckAttribute is RequireHomeserverPermAttribute att && e.Context.Command.Name != "edit") { var level = (await GetPermLevelAsync(e.Context.Member)); var levelText = level.ToString(); diff --git a/Program.cs b/Program.cs index fd3d6734..56463cce 100644 --- a/Program.cs +++ b/Program.cs @@ -2,6 +2,8 @@ using DSharpPlus.Net.Gateway; using Serilog.Sinks.Grafana.Loki; using System.Reflection; +using Cliptok.Commands.InteractionCommands; +using DSharpPlus.Commands.Processors.TextCommands.Parsing; namespace Cliptok { @@ -176,14 +178,31 @@ static async Task Main(string[] _) builder.CommandErrored += ErrorEvents.CommandErrored; // Interaction commands - var slashCommandClasses = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsClass && t.Namespace == "Cliptok.Commands.InteractionCommands" && !t.IsNested); + var slashCommandClasses = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsClass && t.Namespace == "Cliptok.Commands.InteractionCommands"); foreach (var type in slashCommandClasses) builder.AddCommands(type, cfgjson.ServerID); - // Text commands TODO(#202): [Error] Failed to build command '"editwarn"' System.ArgumentException: An item with the same key has already been added. Key: editwarn - var commandClasses = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsClass && t.Namespace == "Cliptok.Commands" && !t.IsNested); + // Text commands + var commandClasses = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsClass && t.Namespace == "Cliptok.Commands"); foreach (var type in commandClasses) builder.AddCommands(type); + + // Register command checks + builder.AddCheck(); + builder.AddCheck(); + builder.AddCheck(); + builder.AddCheck(); + + // Set custom prefixes from config.json + TextCommandProcessor textCommandProcessor = new(new TextCommandConfiguration + { + PrefixResolver = new DefaultPrefixResolver(true, Program.cfgjson.Core.Prefixes.ToArray()).ResolvePrefixAsync + }); + builder.AddProcessor(textCommandProcessor); + }, new CommandsConfiguration + { + // Disable the default D#+ error handler because we are using our own + UseDefaultCommandErrorHandler = false }); discordBuilder.ConfigureExtraFeatures(clientConfig => From 427fa59321bad8a063a909df9dec05abfd583963 Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Fri, 15 Nov 2024 14:16:34 -0500 Subject: [PATCH 05/31] Fix (at least partially) group commands --- Commands/Debug.cs | 1 + Commands/Dehoist.cs | 3 ++- Commands/Raidmode.cs | 2 +- Commands/Timestamp.cs | 3 ++- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Commands/Debug.cs b/Commands/Debug.cs index ad131ef7..6893ab4f 100644 --- a/Commands/Debug.cs +++ b/Commands/Debug.cs @@ -254,6 +254,7 @@ public async Task CheckPendingChannelEvents(TextCommandContext ctx) public class Overrides { [DefaultGroupCommand] + [Command("show")] public async Task ShowOverrides(TextCommandContext ctx, [Description("The user whose overrides to show.")] DiscordUser user) { diff --git a/Commands/Dehoist.cs b/Commands/Dehoist.cs index ff42067a..b70330e8 100644 --- a/Commands/Dehoist.cs +++ b/Commands/Dehoist.cs @@ -161,8 +161,9 @@ await member.ModifyAsync(a => [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] public class Permadehoist { - // Toggle [DefaultGroupCommand] + [Command("toggle")] + [Description("Toggle permadehoist status for a member (or members).")] public async Task PermadehoistToggleCmd(TextCommandContext ctx, [Description("The member(s) to permadehoist.")] params DiscordUser[] discordUsers) { if (discordUsers.Length == 0) diff --git a/Commands/Raidmode.cs b/Commands/Raidmode.cs index ab8347c7..e1945709 100644 --- a/Commands/Raidmode.cs +++ b/Commands/Raidmode.cs @@ -12,8 +12,8 @@ internal class Raidmode class RaidmodeCommands { [DefaultGroupCommand] + [Command("status")] [Description("Check whether raidmode is enabled or not, and when it ends.")] - [TextAlias("status")] public async Task RaidmodeStatus(TextCommandContext ctx) { if (Program.db.HashExists("raidmode", ctx.Guild.Id)) diff --git a/Commands/Timestamp.cs b/Commands/Timestamp.cs index dc828434..0026d86b 100644 --- a/Commands/Timestamp.cs +++ b/Commands/Timestamp.cs @@ -12,7 +12,8 @@ internal class Timestamp class TimestampCmds { [DefaultGroupCommand] - [TextAlias("u", "unix", "epoch")] + [Command("unix")] + [TextAlias("u", "epoch")] [Description("Returns the Unix timestamp of a given Discord ID/snowflake")] public async Task TimestampUnixCmd(TextCommandContext ctx, [Description("The ID/snowflake to fetch the Unix timestamp for")] ulong snowflake) { From 05c47ffec600c3b8d8e328d9959675a1319cfe4b Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Fri, 15 Nov 2024 14:40:03 -0500 Subject: [PATCH 06/31] Error handling: Strip "textcmd" from cmd names, fix check failure handling --- Events/ErrorEvents.cs | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/Events/ErrorEvents.cs b/Events/ErrorEvents.cs index bf1c32f0..1646a978 100644 --- a/Events/ErrorEvents.cs +++ b/Events/ErrorEvents.cs @@ -28,14 +28,17 @@ public static async Task CommandErrored(CommandsExtension _, CommandErroredEvent public static async Task TextCommandErrored(CommandErroredEventArgs e) { - if (e.Exception is CommandNotFoundException && (e.Context.Command is null || e.Context.Command.FullName != "help")) + // strip out "textcmd" from text command names + var commandName = e.Context.Command.FullName.Replace("textcmd", ""); + + if (e.Exception is CommandNotFoundException && (e.Context.Command is null || commandName != "help")) return; // avoid conflicts with modmail - if (e.Context.Command.FullName == "edit" || e.Context.Command.FullName == "timestamp") + if (commandName == "edit" || commandName == "timestamp") return; - e.Context.Client.Logger.LogError(CliptokEventID, e.Exception, "Exception occurred during {user}s invocation of {command}", e.Context.User.Username, e.Context.Command.FullName); + e.Context.Client.Logger.LogError(CliptokEventID, e.Exception, "Exception occurred during {user}s invocation of {command}", e.Context.User.Username, commandName); var exs = new List(); if (e.Exception is AggregateException ae) @@ -45,17 +48,33 @@ public static async Task TextCommandErrored(CommandErroredEventArgs e) foreach (var ex in exs) { - if (ex is CommandNotFoundException && (e.Context.Command is null || e.Context.Command.FullName != "help")) + if (ex is CommandNotFoundException && (e.Context.Command is null || commandName != "help")) return; - if (ex is ChecksFailedException && (e.Context.Command.Name != "help")) + if (ex is ChecksFailedException cfex && (commandName != "help")) + { + foreach (var check in cfex.Errors) + { + if (check.ContextCheckAttribute is RequireHomeserverPermAttribute att) + { + var level = (await GetPermLevelAsync(e.Context.Member)); + var levelText = level.ToString(); + if (level == ServerPermLevel.Nothing && Program.rand.Next(1, 100) == 69) + levelText = $"naught but a thing, my dear human. Congratulations, you win {Program.rand.Next(1, 10)} bonus points."; + + await e.Context.RespondAsync( + $"{Program.cfgjson.Emoji.NoPermissions} Invalid permissions to use command **{commandName}**!\n" + + $"Required: `{att.TargetLvl}`\nYou have: `{levelText}`"); + } + } return; + } var embed = new DiscordEmbedBuilder { Color = new DiscordColor("#FF0000"), Title = "An exception occurred when executing a command", - Description = $"{cfgjson.Emoji.BSOD} `{e.Exception.GetType()}` occurred when executing `{e.Context.Command.FullName}`.", + Description = $"{cfgjson.Emoji.BSOD} `{e.Exception.GetType()}` occurred when executing `{commandName}`.", Timestamp = DateTime.UtcNow }; embed.WithFooter(discord.CurrentUser.Username, discord.CurrentUser.AvatarUrl) From 56e9aebfda215a75446c2f865d5877418ac1ab1d Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Mon, 18 Nov 2024 22:15:06 -0500 Subject: [PATCH 07/31] Fix [RequirePermissions] usage --- Commands/InteractionCommands/LockdownInteractions.cs | 4 ++-- Commands/Lockdown.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Commands/InteractionCommands/LockdownInteractions.cs b/Commands/InteractionCommands/LockdownInteractions.cs index 474fd45a..937ed29b 100644 --- a/Commands/InteractionCommands/LockdownInteractions.cs +++ b/Commands/InteractionCommands/LockdownInteractions.cs @@ -7,7 +7,7 @@ class LockdownInteractions [Command("lockdown")] [Description("Lock the current channel or all channels in the server, preventing new messages. See also: unlock")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermission.ManageChannels, DiscordPermissions.None)] + [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions([DiscordPermission.ManageChannels], [])] public class LockdownCmds { [Command("channel")] @@ -109,7 +109,7 @@ public async Task LockdownAllCommand( [Command("unlock")] [Description("Unlock the current channel or all channels in the server, allowing new messages. See also: lockdown")] [AllowedProcessors(typeof(SlashCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermissions.ManageChannels, DiscordPermissions.None)] + [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions([DiscordPermission.ManageChannels], [])] public class UnlockCmds { [Command("channel")] diff --git a/Commands/Lockdown.cs b/Commands/Lockdown.cs index 4648833d..a1680356 100644 --- a/Commands/Lockdown.cs +++ b/Commands/Lockdown.cs @@ -8,7 +8,7 @@ class Lockdown [TextAlias("lockdown", "lock")] [Description("Locks the current channel, preventing any new messages. See also: unlock")] [AllowedProcessors(typeof(TextCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermission.ManageChannels, DiscordPermissions.None)] + [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions([DiscordPermission.ManageChannels], [])] public async Task LockdownCommand( TextCommandContext ctx, [RemainingText, Description("The time and reason for the lockdown. For example '3h' or '3h spam'. Default is permanent with no reason.")] string timeAndReason = "" @@ -101,7 +101,7 @@ await thread.ModifyAsync(a => [TextAlias("unlock", "unlockdown")] [Description("Unlocks a previously locked channel. See also: lockdown")] [AllowedProcessors(typeof(TextCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermission.ManageChannels, DiscordPermissions.None)] + [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions([DiscordPermission.ManageChannels], [])] public async Task UnlockCommand(TextCommandContext ctx, [RemainingText] string reason = "") { var currentChannel = ctx.Channel; From e262b45afe9192a161d1613b9e8fef017cd6a497 Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Wed, 20 Nov 2024 10:29:22 -0500 Subject: [PATCH 08/31] Combine commands that can be combined, move all commands to Commands namespace --- ...entInteractions.cs => AnnouncementCmds.cs} | 161 +++++++- Commands/Announcements.cs | 160 -------- Commands/{Bans.cs => BanCmds.cs} | 179 +++++++-- .../ClearInteractions.cs => ClearCmds.cs} | 4 +- Commands/{Debug.cs => DebugCmds.cs} | 13 +- Commands/Dehoist.cs | 346 ----------------- Commands/DehoistCmds.cs | 217 +++++++++++ Commands/{DmRelayBlock.cs => DmRelayCmds.cs} | 6 +- Commands/FunCmds.cs | 87 ++++- Commands/Grant.cs | 15 - .../InteractionCommands/BanInteractions.cs | 199 ---------- .../InteractionCommands/ContextCommands.cs | 80 ---- .../InteractionCommands/DebugInteractions.cs | 129 ------- .../DehoistInteractions.cs | 92 ----- .../LockdownInteractions.cs | 166 -------- .../InteractionCommands/MuteInteractions.cs | 163 -------- .../InteractionCommands/RoleInteractions.cs | 129 ------- .../TechSupportInteractions.cs | 52 --- ...nwatchInteractions.cs => JoinwatchCmds.cs} | 61 ++- Commands/{Kick.cs => KickCmds.cs} | 25 +- Commands/{Lists.cs => ListCmds.cs} | 63 +-- Commands/Lockdown.cs | 144 ------- Commands/LockdownCmds.cs | 217 +++++++++++ Commands/MuteCmds.cs | 290 ++++++++++++++ Commands/Mutes.cs | 165 -------- ...LockInteraction.cs => NicknameLockCmds.cs} | 13 +- Commands/Raidmode.cs | 87 ----- ...aidmodeInteractions.cs => RaidmodeCmds.cs} | 35 +- Commands/{Reminders.cs => ReminderCmds.cs} | 7 +- Commands/{UserRoles.cs => RoleCmds.cs} | 181 ++++++++- .../RulesInteractions.cs => RulesCmds.cs} | 4 +- Commands/SecurityActions.cs | 121 ------ ...Interactions.cs => SecurityActionsCmds.cs} | 12 +- ...lowmodeInteractions.cs => SlowmodeCmds.cs} | 7 +- .../StatusInteractions.cs => StatusCmds.cs} | 2 +- Commands/TechSupport.cs | 48 --- Commands/TechSupportCmds.cs | 126 ++++++ Commands/TechSupportCommands.cs | 33 -- Commands/Threads.cs | 80 ---- Commands/Timestamp.cs | 48 --- ...rackingInteractions.cs => TrackingCmds.cs} | 2 +- ...serNoteInteractions.cs => UserNoteCmds.cs} | 11 +- Commands/Utility.cs | 84 ---- Commands/UtilityCmds.cs | 149 +++++++ .../WarningInteractions.cs => WarningCmds.cs} | 365 +++++++++++++++++- Commands/Warnings.cs | 363 ----------------- Events/InteractionEvents.cs | 8 +- Program.cs | 9 +- Tasks/ReminderTasks.cs | 2 +- 49 files changed, 2049 insertions(+), 2911 deletions(-) rename Commands/{InteractionCommands/AnnouncementInteractions.cs => AnnouncementCmds.cs} (64%) delete mode 100644 Commands/Announcements.cs rename Commands/{Bans.cs => BanCmds.cs} (61%) rename Commands/{InteractionCommands/ClearInteractions.cs => ClearCmds.cs} (99%) rename Commands/{Debug.cs => DebugCmds.cs} (99%) delete mode 100644 Commands/Dehoist.cs create mode 100644 Commands/DehoistCmds.cs rename Commands/{DmRelayBlock.cs => DmRelayCmds.cs} (95%) delete mode 100644 Commands/Grant.cs delete mode 100644 Commands/InteractionCommands/BanInteractions.cs delete mode 100644 Commands/InteractionCommands/ContextCommands.cs delete mode 100644 Commands/InteractionCommands/DebugInteractions.cs delete mode 100644 Commands/InteractionCommands/DehoistInteractions.cs delete mode 100644 Commands/InteractionCommands/LockdownInteractions.cs delete mode 100644 Commands/InteractionCommands/MuteInteractions.cs delete mode 100644 Commands/InteractionCommands/RoleInteractions.cs delete mode 100644 Commands/InteractionCommands/TechSupportInteractions.cs rename Commands/{InteractionCommands/JoinwatchInteractions.cs => JoinwatchCmds.cs} (61%) rename Commands/{Kick.cs => KickCmds.cs} (85%) rename Commands/{Lists.cs => ListCmds.cs} (72%) delete mode 100644 Commands/Lockdown.cs create mode 100644 Commands/LockdownCmds.cs create mode 100644 Commands/MuteCmds.cs delete mode 100644 Commands/Mutes.cs rename Commands/{InteractionCommands/NicknameLockInteraction.cs => NicknameLockCmds.cs} (95%) delete mode 100644 Commands/Raidmode.cs rename Commands/{InteractionCommands/RaidmodeInteractions.cs => RaidmodeCmds.cs} (81%) rename Commands/{Reminders.cs => ReminderCmds.cs} (97%) rename Commands/{UserRoles.cs => RoleCmds.cs} (61%) rename Commands/{InteractionCommands/RulesInteractions.cs => RulesCmds.cs} (98%) delete mode 100644 Commands/SecurityActions.cs rename Commands/{InteractionCommands/SecurityActionInteractions.cs => SecurityActionsCmds.cs} (90%) rename Commands/{InteractionCommands/SlowmodeInteractions.cs => SlowmodeCmds.cs} (97%) rename Commands/{InteractionCommands/StatusInteractions.cs => StatusCmds.cs} (98%) delete mode 100644 Commands/TechSupport.cs create mode 100644 Commands/TechSupportCmds.cs delete mode 100644 Commands/TechSupportCommands.cs delete mode 100644 Commands/Threads.cs delete mode 100644 Commands/Timestamp.cs rename Commands/{InteractionCommands/TrackingInteractions.cs => TrackingCmds.cs} (99%) rename Commands/{InteractionCommands/UserNoteInteractions.cs => UserNoteCmds.cs} (96%) delete mode 100644 Commands/Utility.cs create mode 100644 Commands/UtilityCmds.cs rename Commands/{InteractionCommands/WarningInteractions.cs => WarningCmds.cs} (50%) delete mode 100644 Commands/Warnings.cs diff --git a/Commands/InteractionCommands/AnnouncementInteractions.cs b/Commands/AnnouncementCmds.cs similarity index 64% rename from Commands/InteractionCommands/AnnouncementInteractions.cs rename to Commands/AnnouncementCmds.cs index e229a052..6f70ed70 100644 --- a/Commands/InteractionCommands/AnnouncementInteractions.cs +++ b/Commands/AnnouncementCmds.cs @@ -1,6 +1,6 @@ -namespace Cliptok.Commands.InteractionCommands +namespace Cliptok.Commands { - internal class AnnouncementInteractions + public class AnnouncementCmds { [Command("announcebuild")] [Description("Announce a Windows Insider build in the current channel.")] @@ -257,6 +257,160 @@ public async Task AnnounceBuildSlashCommand(SlashCommandContext ctx, } } + [Command("editannouncetextcmd")] + [TextAlias("editannounce")] + [Description("Edit an announcement, preserving the ping highlight.")] + [AllowedProcessors(typeof(TextCommandProcessor))] + [RequireHomeserverPerm(ServerPermLevel.Moderator)] + public async Task EditAnnounce( + TextCommandContext ctx, + [Description("The ID of the message to edit.")] ulong messageId, + [Description("The short name for the role to ping.")] string roleName, + [RemainingText, Description("The new message content, excluding the ping.")] string content + ) + { + DiscordRole discordRole; + + if (Program.cfgjson.AnnouncementRoles.ContainsKey(roleName)) + { + discordRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.AnnouncementRoles[roleName]); + await discordRole.ModifyAsync(mentionable: true); + try + { + await ctx.Message.DeleteAsync(); + var msg = await ctx.Channel.GetMessageAsync(messageId); + await msg.ModifyAsync($"{discordRole.Mention} {content}"); + } + catch + { + // We still need to remember to make it unmentionable even if the msg fails. + } + await discordRole.ModifyAsync(mentionable: false); + } + else + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} That role name isn't recognised!"); + return; + } + } + + [Command("announcetextcmd")] + [TextAlias("announce")] + [Description("Announces something in the current channel, pinging an Insider role in the process.")] + [AllowedProcessors(typeof(TextCommandProcessor))] + [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] + public async Task AnnounceCmd(TextCommandContext ctx, [Description("'canary', 'dev', 'beta', 'beta10', 'rp', 'rp10', 'patch', 'rpbeta', 'rpbeta10', 'betadev', 'candev'")] string roleName, [RemainingText, Description("The announcement message to send.")] string announcementMessage) + { + DiscordRole discordRole; + + if (Program.cfgjson.AnnouncementRoles.ContainsKey(roleName)) + { + discordRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.AnnouncementRoles[roleName]); + await discordRole.ModifyAsync(mentionable: true); + try + { + await ctx.Message.DeleteAsync(); + await ctx.Channel.SendMessageAsync($"{discordRole.Mention} {announcementMessage}"); + } + catch + { + // We still need to remember to make it unmentionable even if the msg fails. + } + await discordRole.ModifyAsync(mentionable: false); + } + else if (roleName == "rpbeta") + { + var rpRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.AnnouncementRoles["rp"]); + var betaRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.AnnouncementRoles["beta"]); + + await rpRole.ModifyAsync(mentionable: true); + await betaRole.ModifyAsync(mentionable: true); + + try + { + await ctx.Message.DeleteAsync(); + await ctx.Channel.SendMessageAsync($"{rpRole.Mention} {betaRole.Mention}\n{announcementMessage}"); + } + catch + { + // We still need to remember to make it unmentionable even if the msg fails. + } + + await rpRole.ModifyAsync(mentionable: false); + await betaRole.ModifyAsync(mentionable: false); + } + // this is rushed pending an actual solution + else if (roleName == "rpbeta10") + { + var rpRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.AnnouncementRoles["rp10"]); + var betaRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.AnnouncementRoles["beta10"]); + + await rpRole.ModifyAsync(mentionable: true); + await betaRole.ModifyAsync(mentionable: true); + + try + { + await ctx.Message.DeleteAsync(); + await ctx.Channel.SendMessageAsync($"{rpRole.Mention} {betaRole.Mention}\n{announcementMessage}"); + } + catch + { + // We still need to remember to make it unmentionable even if the msg fails. + } + + await rpRole.ModifyAsync(mentionable: false); + await betaRole.ModifyAsync(mentionable: false); + } + else if (roleName == "betadev") + { + var betaRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.AnnouncementRoles["beta"]); + var devRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.AnnouncementRoles["dev"]); + + await betaRole.ModifyAsync(mentionable: true); + await devRole.ModifyAsync(mentionable: true); + + try + { + await ctx.Message.DeleteAsync(); + await ctx.Channel.SendMessageAsync($"{betaRole.Mention} {devRole.Mention}\n{announcementMessage}"); + } + catch + { + // We still need to remember to make it unmentionable even if the msg fails. + } + + await betaRole.ModifyAsync(mentionable: false); + await devRole.ModifyAsync(mentionable: false); + } + else if (roleName == "candev") + { + var canaryRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.AnnouncementRoles["canary"]); + var devRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.AnnouncementRoles["dev"]); + + await canaryRole.ModifyAsync(mentionable: true); + await devRole.ModifyAsync(mentionable: true); + + try + { + await ctx.Message.DeleteAsync(); + await ctx.Channel.SendMessageAsync($"{canaryRole.Mention} {devRole.Mention}\n{announcementMessage}"); + } + catch + { + // We still need to remember to make it unmentionable even if the msg fails. + } + + await canaryRole.ModifyAsync(mentionable: false); + await devRole.ModifyAsync(mentionable: false); + } + else + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} That role name isn't recognised!"); + return; + } + + } + internal class WindowsVersionChoiceProvider : IChoiceProvider { public async ValueTask> ProvideAsync(CommandParameter _) @@ -282,6 +436,5 @@ public async ValueTask> Provi }; } } - } -} +} \ No newline at end of file diff --git a/Commands/Announcements.cs b/Commands/Announcements.cs deleted file mode 100644 index e25213d1..00000000 --- a/Commands/Announcements.cs +++ /dev/null @@ -1,160 +0,0 @@ -namespace Cliptok.Commands -{ - internal class Announcements - { - - [Command("editannouncetextcmd")] - [TextAlias("editannounce")] - [Description("Edit an announcement, preserving the ping highlight.")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [RequireHomeserverPerm(ServerPermLevel.Moderator)] - public async Task EditAnnounce( - TextCommandContext ctx, - [Description("The ID of the message to edit.")] ulong messageId, - [Description("The short name for the role to ping.")] string roleName, - [RemainingText, Description("The new message content, excluding the ping.")] string content - ) - { - DiscordRole discordRole; - - if (Program.cfgjson.AnnouncementRoles.ContainsKey(roleName)) - { - discordRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.AnnouncementRoles[roleName]); - await discordRole.ModifyAsync(mentionable: true); - try - { - await ctx.Message.DeleteAsync(); - var msg = await ctx.Channel.GetMessageAsync(messageId); - await msg.ModifyAsync($"{discordRole.Mention} {content}"); - } - catch - { - // We still need to remember to make it unmentionable even if the msg fails. - } - await discordRole.ModifyAsync(mentionable: false); - } - else - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} That role name isn't recognised!"); - return; - } - } - - [Command("announcetextcmd")] - [TextAlias("announce")] - [Description("Announces something in the current channel, pinging an Insider role in the process.")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] - public async Task AnnounceCmd(TextCommandContext ctx, [Description("'canary', 'dev', 'beta', 'beta10', 'rp', 'rp10', 'patch', 'rpbeta', 'rpbeta10', 'betadev', 'candev'")] string roleName, [RemainingText, Description("The announcement message to send.")] string announcementMessage) - { - DiscordRole discordRole; - - if (Program.cfgjson.AnnouncementRoles.ContainsKey(roleName)) - { - discordRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.AnnouncementRoles[roleName]); - await discordRole.ModifyAsync(mentionable: true); - try - { - await ctx.Message.DeleteAsync(); - await ctx.Channel.SendMessageAsync($"{discordRole.Mention} {announcementMessage}"); - } - catch - { - // We still need to remember to make it unmentionable even if the msg fails. - } - await discordRole.ModifyAsync(mentionable: false); - } - else if (roleName == "rpbeta") - { - var rpRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.AnnouncementRoles["rp"]); - var betaRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.AnnouncementRoles["beta"]); - - await rpRole.ModifyAsync(mentionable: true); - await betaRole.ModifyAsync(mentionable: true); - - try - { - await ctx.Message.DeleteAsync(); - await ctx.Channel.SendMessageAsync($"{rpRole.Mention} {betaRole.Mention}\n{announcementMessage}"); - } - catch - { - // We still need to remember to make it unmentionable even if the msg fails. - } - - await rpRole.ModifyAsync(mentionable: false); - await betaRole.ModifyAsync(mentionable: false); - } - // this is rushed pending an actual solution - else if (roleName == "rpbeta10") - { - var rpRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.AnnouncementRoles["rp10"]); - var betaRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.AnnouncementRoles["beta10"]); - - await rpRole.ModifyAsync(mentionable: true); - await betaRole.ModifyAsync(mentionable: true); - - try - { - await ctx.Message.DeleteAsync(); - await ctx.Channel.SendMessageAsync($"{rpRole.Mention} {betaRole.Mention}\n{announcementMessage}"); - } - catch - { - // We still need to remember to make it unmentionable even if the msg fails. - } - - await rpRole.ModifyAsync(mentionable: false); - await betaRole.ModifyAsync(mentionable: false); - } - else if (roleName == "betadev") - { - var betaRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.AnnouncementRoles["beta"]); - var devRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.AnnouncementRoles["dev"]); - - await betaRole.ModifyAsync(mentionable: true); - await devRole.ModifyAsync(mentionable: true); - - try - { - await ctx.Message.DeleteAsync(); - await ctx.Channel.SendMessageAsync($"{betaRole.Mention} {devRole.Mention}\n{announcementMessage}"); - } - catch - { - // We still need to remember to make it unmentionable even if the msg fails. - } - - await betaRole.ModifyAsync(mentionable: false); - await devRole.ModifyAsync(mentionable: false); - } - else if (roleName == "candev") - { - var canaryRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.AnnouncementRoles["canary"]); - var devRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.AnnouncementRoles["dev"]); - - await canaryRole.ModifyAsync(mentionable: true); - await devRole.ModifyAsync(mentionable: true); - - try - { - await ctx.Message.DeleteAsync(); - await ctx.Channel.SendMessageAsync($"{canaryRole.Mention} {devRole.Mention}\n{announcementMessage}"); - } - catch - { - // We still need to remember to make it unmentionable even if the msg fails. - } - - await canaryRole.ModifyAsync(mentionable: false); - await devRole.ModifyAsync(mentionable: false); - } - else - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} That role name isn't recognised!"); - return; - } - - } - } -} diff --git a/Commands/Bans.cs b/Commands/BanCmds.cs similarity index 61% rename from Commands/Bans.cs rename to Commands/BanCmds.cs index 6f076bff..e7a7b2ac 100644 --- a/Commands/Bans.cs +++ b/Commands/BanCmds.cs @@ -1,16 +1,162 @@ -using static Cliptok.Helpers.BanHelpers; +using static Cliptok.Helpers.BanHelpers; namespace Cliptok.Commands { - class Bans + public class BanCmds { + [Command("ban")] + [Description("Bans a user from the server, either permanently or temporarily.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermission.BanMembers)] + public async Task BanSlashCommand(SlashCommandContext ctx, + [Parameter("user"), Description("The user to ban")] DiscordUser user, + [Parameter("reason"), Description("The reason the user is being banned")] string reason, + [Parameter("keep_messages"), Description("Whether to keep the users messages when banning")] bool keepMessages = false, + [Parameter("time"), Description("The length of time the user is banned for")] string time = null, + [Parameter("appeal_link"), Description("Whether to show the user an appeal URL in the DM")] bool appealable = false, + [Parameter("compromised_account"), Description("Whether to include special instructions for compromised accounts")] bool compromisedAccount = false + ) + { + // Initial response to avoid the 3 second timeout, will edit later. + var eout = new DiscordInteractionResponseBuilder().AsEphemeral(true); + await ctx.DeferResponseAsync(true); + + // Edits need a webhook rather than interaction..? + DiscordWebhookBuilder webhookOut = new(); + int messageDeleteDays = 7; + if (keepMessages) + messageDeleteDays = 0; + + if (user.IsBot) + { + webhookOut.Content = $"{Program.cfgjson.Emoji.Error} To prevent accidents, I won't ban bots. If you really need to do this, do it manually in Discord."; + await ctx.EditResponseAsync(webhookOut); + return; + } + + DiscordMember targetMember; + + try + { + targetMember = await ctx.Guild.GetMemberAsync(user.Id); + if ((await GetPermLevelAsync(ctx.Member)) == ServerPermLevel.TrialModerator && ((await GetPermLevelAsync(targetMember)) >= ServerPermLevel.TrialModerator)) + { + webhookOut.Content = $"{Program.cfgjson.Emoji.Error} As a Trial Moderator you cannot perform moderation actions on other staff members."; + await ctx.EditResponseAsync(webhookOut); + return; + } + } + catch + { + // do nothing :/ + } + + TimeSpan banDuration; + if (time is null) + banDuration = default; + else + { + try + { + banDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(ctx.Interaction.CreationTimestamp.DateTime); + } + catch + { + webhookOut.Content = $"{Program.cfgjson.Emoji.Error} There was an error parsing your supplied ban length!"; + await ctx.EditResponseAsync(webhookOut); + return; + } + + } + + DiscordMember member; + try + { + member = await ctx.Guild.GetMemberAsync(user.Id); + } + catch + { + member = null; + } + + if (member is null) + { + await BanHelpers.BanFromServerAsync(user.Id, reason, ctx.User.Id, ctx.Guild, messageDeleteDays, ctx.Channel, banDuration, appealable); + } + else + { + if (DiscordHelpers.AllowedToMod(ctx.Member, member)) + { + if (DiscordHelpers.AllowedToMod(await ctx.Guild.GetMemberAsync(ctx.Client.CurrentUser.Id), member)) + { + await BanHelpers.BanFromServerAsync(user.Id, reason, ctx.User.Id, ctx.Guild, messageDeleteDays, ctx.Channel, banDuration, appealable); + } + else + { + webhookOut.Content = $"{Program.cfgjson.Emoji.Error} I don't have permission to ban **{DiscordHelpers.UniqueUsername(user)}**!"; + await ctx.EditResponseAsync(webhookOut); + return; + } + } + else + { + webhookOut.Content = $"{Program.cfgjson.Emoji.Error} You don't have permission to ban **{DiscordHelpers.UniqueUsername(user)}**!"; + await ctx.EditResponseAsync(webhookOut); + return; + } + } + reason = reason.Replace("`", "\\`").Replace("*", "\\*"); + if (banDuration == default) + await ctx.Channel.SendMessageAsync($"{Program.cfgjson.Emoji.Banned} {user.Mention} has been banned: **{reason}**"); + else + await ctx.Channel.SendMessageAsync($"{Program.cfgjson.Emoji.Banned} {user.Mention} has been banned for **{TimeHelpers.TimeToPrettyFormat(banDuration, false)}**: **{reason}**"); + + webhookOut.Content = $"{Program.cfgjson.Emoji.Success} User was successfully bonked."; + await ctx.EditResponseAsync(webhookOut); + } + + [Command("unban")] + [Description("Unbans a user who has been previously banned.")] + [AllowedProcessors(typeof(SlashCommandProcessor), typeof(TextCommandProcessor))] + [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(permissions: DiscordPermission.BanMembers)] + public async Task UnbanCmd(CommandContext ctx, [Description("The user to unban, usually a mention or ID")] DiscordUser targetUser, [Description("Used in audit log only currently")] string reason = "No reason specified.") + { + if ((await Program.db.HashExistsAsync("bans", targetUser.Id))) + { + await UnbanUserAsync(ctx.Guild, targetUser, $"[Unban by {DiscordHelpers.UniqueUsername(ctx.User)}]: {reason}"); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Unbanned} Successfully unbanned **{DiscordHelpers.UniqueUsername(targetUser)}**."); + } + else + { + bool banSuccess = await UnbanUserAsync(ctx.Guild, targetUser); + if (banSuccess) + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Unbanned} Successfully unbanned **{DiscordHelpers.UniqueUsername(targetUser)}**."); + else + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {ctx.Member.Mention}, that user doesn't appear to be banned, *and* an error occurred while attempting to unban them anyway.\nPlease contact the bot owner if this wasn't expected, the error has been logged."); + } + } + } + + [Command("baninfo")] + [Description("Show information about the ban for a user.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] + [RequirePermissions(DiscordPermission.ModerateMembers)] + public async Task BanInfoSlashCommand( + SlashCommandContext ctx, + [Parameter("user"), Description("The user whose ban information to show.")] DiscordUser targetUser, + [Parameter("public"), Description("Whether to show the output publicly. Default: false")] bool isPublic = false) + { + await ctx.RespondAsync(embed: await BanHelpers.BanStatusEmbed(targetUser, ctx.Guild), ephemeral: !isPublic); + } + [Command("massbantextcmd")] [TextAlias("massban", "bigbonk")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] public async Task MassBanCmd(TextCommandContext ctx, [RemainingText] string input) { - List usersString = input.Replace("\n", " ").Replace("\r", "").Split(' ').ToList(); List users = usersString.Select(x => Convert.ToUInt64(x)).ToList(); if (users.Count == 1 || users.Count == 0) @@ -217,30 +363,5 @@ public async Task BankeepCmd(TextCommandContext ctx, else await ctx.Channel.SendMessageAsync($"{Program.cfgjson.Emoji.Banned} {targetMember.Mention} has been banned for **{TimeHelpers.TimeToPrettyFormat(banDuration, false)}**: **{reason}**"); } - - [Command("unbantextcmd")] - [TextAlias("unban")] - [Description("Unbans a user who has been previously banned.")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(permissions: DiscordPermission.BanMembers)] - public async Task UnbanCmd(TextCommandContext ctx, [Description("The user to unban, usually a mention or ID")] DiscordUser targetUser, [Description("Used in audit log only currently")] string reason = "No reason specified.") - { - if ((await Program.db.HashExistsAsync("bans", targetUser.Id))) - { - await UnbanUserAsync(ctx.Guild, targetUser, $"[Unban by {DiscordHelpers.UniqueUsername(ctx.User)}]: {reason}"); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Unbanned} Successfully unbanned **{DiscordHelpers.UniqueUsername(targetUser)}**."); - } - else - { - bool banSuccess = await UnbanUserAsync(ctx.Guild, targetUser); - if (banSuccess) - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Unbanned} Successfully unbanned **{DiscordHelpers.UniqueUsername(targetUser)}**."); - else - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {ctx.Member.Mention}, that user doesn't appear to be banned, *and* an error occurred while attempting to unban them anyway.\nPlease contact the bot owner if this wasn't expected, the error has been logged."); - } - } - } - } -} +} \ No newline at end of file diff --git a/Commands/InteractionCommands/ClearInteractions.cs b/Commands/ClearCmds.cs similarity index 99% rename from Commands/InteractionCommands/ClearInteractions.cs rename to Commands/ClearCmds.cs index 26417d8e..bc363a25 100644 --- a/Commands/InteractionCommands/ClearInteractions.cs +++ b/Commands/ClearCmds.cs @@ -1,6 +1,6 @@ -namespace Cliptok.Commands.InteractionCommands +namespace Cliptok.Commands { - public class ClearInteractions + public class ClearCmds { public static Dictionary> MessagesToClear = new(); diff --git a/Commands/Debug.cs b/Commands/DebugCmds.cs similarity index 99% rename from Commands/Debug.cs rename to Commands/DebugCmds.cs index baad7c7d..9d0c8df7 100644 --- a/Commands/Debug.cs +++ b/Commands/DebugCmds.cs @@ -1,8 +1,6 @@ -using DSharpPlus.Commands.Trees.Metadata; - namespace Cliptok.Commands { - internal class Debug + public class DebugCmds { public static Dictionary OverridesPendingAddition = new(); @@ -11,7 +9,7 @@ internal class Debug [Description("Commands and things for fixing the bot in the unlikely event that it breaks a bit.")] [AllowedProcessors(typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] - class DebugCmds + class DebugCmd { [Command("mutestatus")] public async Task MuteStatus(TextCommandContext ctx, DiscordUser targetUser = default) @@ -325,7 +323,7 @@ public async Task ImportAll(TextCommandContext ctx) { await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working..."); var msg = await ctx.GetResponseAsync(); - + // Get all channels var channels = await ctx.Guild.GetChannelsAsync(); @@ -419,7 +417,7 @@ public async Task Apply(TextCommandContext ctx, { await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it..."); var msg = await ctx.GetResponseAsync(); - + // Try fetching member to determine whether they are in the server. If they are not, we can't apply overrides for them. DiscordMember member; try @@ -657,6 +655,5 @@ await Program.db.HashSetAsync("overrides", overwrite.Id.ToString(), } } - } -} +} \ No newline at end of file diff --git a/Commands/Dehoist.cs b/Commands/Dehoist.cs deleted file mode 100644 index b70330e8..00000000 --- a/Commands/Dehoist.cs +++ /dev/null @@ -1,346 +0,0 @@ -using DSharpPlus.Commands.Trees.Metadata; - -namespace Cliptok.Commands -{ - internal class Dehoist - { - [Command("dehoisttextcmd")] - [TextAlias("dehoist")] - [Description("Adds an invisible character to someone's nickname that drops them to the bottom of the member list. Accepts multiple members.")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public async Task DehoistCmd(TextCommandContext ctx, [Description("List of server members to dehoist")] params DiscordMember[] discordMembers) - { - if (discordMembers.Length == 0) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} You need to tell me who to dehoist!"); - return; - } - else if (discordMembers.Length == 1) - { - if (discordMembers[0].DisplayName[0] == DehoistHelpers.dehoistCharacter) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {discordMembers[0].Mention} is already dehoisted!"); - return; - } - try - { - await discordMembers[0].ModifyAsync(a => - { - a.Nickname = DehoistHelpers.DehoistName(discordMembers[0].DisplayName); - a.AuditLogReason = $"[Dehoist by {DiscordHelpers.UniqueUsername(ctx.User)}]"; - }); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Successfully dehoisted {discordMembers[0].Mention}!"); - } - catch - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Failed to dehoist {discordMembers[0].Mention}!"); - } - return; - } - - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it..."); - var msg = await ctx.GetResponseAsync(); - int failedCount = 0; - - foreach (DiscordMember discordMember in discordMembers) - { - var origName = discordMember.DisplayName; - if (origName[0] == '\u17b5') - { - failedCount++; - } - else - { - try - { - await discordMember.ModifyAsync(a => - { - a.Nickname = DehoistHelpers.DehoistName(origName); - a.AuditLogReason = $"[Dehoist by {DiscordHelpers.UniqueUsername(ctx.User)}]"; - }); - } - catch - { - failedCount++; - } - } - - } - _ = await msg.ModifyAsync($"{Program.cfgjson.Emoji.Success} Successfully dehoisted {discordMembers.Length - failedCount} of {discordMembers.Length} member(s)! (Check Audit Log for details)"); - } - - [Command("massdehoisttextcmd")] - [TextAlias("massdehoist")] - [Description("Dehoist everyone on the server who has a bad name. This may take a while and can exhaust rate limits.")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] - public async Task MassDehoist(TextCommandContext ctx) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it. This will take a while."); - var msg = await ctx.GetResponseAsync(); - var discordMembers = await ctx.Guild.GetAllMembersAsync().ToListAsync(); - int failedCount = 0; - - foreach (DiscordMember discordMember in discordMembers) - { - bool success = await DehoistHelpers.CheckAndDehoistMemberAsync(discordMember, ctx.User, true); - if (!success) - failedCount++; - } - - _ = msg.DeleteAsync(); - await ctx.Channel.SendMessageAsync(new DiscordMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Success} Successfully dehoisted {discordMembers.Count() - failedCount} of {discordMembers.Count()} member(s)! (Check Audit Log for details)").WithReply(ctx.Message.Id, true, false)); - } - - [Command("massundehoisttextcmd")] - [TextAlias("massundehoist")] - [Description("Remove the dehoist for users attached via a txt file.")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] - public async Task MassUndhoist(TextCommandContext ctx) - { - int failedCount = 0; - - if (ctx.Message.Attachments.Count == 0) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Please upload an attachment as well."); - } - else - { - string strList; - using (HttpClient client = new()) - { - strList = await client.GetStringAsync(ctx.Message.Attachments[0].Url); - } - - var list = strList.Split(' '); - - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it. This will take a while."); - var msg = await ctx.GetResponseAsync(); - - foreach (string strID in list) - { - ulong id = Convert.ToUInt64(strID); - DiscordMember member = default; - try - { - member = await ctx.Guild.GetMemberAsync(id); - } - catch (DSharpPlus.Exceptions.NotFoundException) - { - failedCount++; - continue; - } - - if (member.DisplayName[0] == DehoistHelpers.dehoistCharacter) - { - var newNickname = member.Nickname[1..]; - await member.ModifyAsync(a => - { - a.Nickname = newNickname; - a.AuditLogReason = $"[Mass undehoist by {DiscordHelpers.UniqueUsername(ctx.User)}]"; - } - ); - } - else - { - failedCount++; - } - } - - await msg.ModifyAsync($"{Program.cfgjson.Emoji.Success} Successfully undehoisted {list.Length - failedCount} of {list.Length} member(s)! (Check Audit Log for details)"); - - } - } - - [Command("permadehoisttextcmd")] - [TextAlias("permadehoist")] - [Description("Permanently/persistently dehoist members.")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public class Permadehoist - { - [DefaultGroupCommand] - [Command("toggle")] - [Description("Toggle permadehoist status for a member (or members).")] - public async Task PermadehoistToggleCmd(TextCommandContext ctx, [Description("The member(s) to permadehoist.")] params DiscordUser[] discordUsers) - { - if (discordUsers.Length == 0) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} You need to tell me who to permadehoist!"); - return; - } - - if (discordUsers.Length == 1) - { - // Toggle permadehoist for single member - - var (success, isPermissionError, isDehoist) = await DehoistHelpers.TogglePermadehoist(discordUsers[0], ctx.User, ctx.Guild); - - if (success) - { - if (isDehoist) - { - await ctx.RespondAsync(new DiscordMessageBuilder() - .WithContent($"{Program.cfgjson.Emoji.On} Successfully permadehoisted {discordUsers[0].Mention}!") - .WithAllowedMentions(Mentions.None)); - } - else - { - await ctx.RespondAsync(new DiscordMessageBuilder() - .WithContent($"{Program.cfgjson.Emoji.Off} Successfully removed the permadehoist for {discordUsers[0].Mention}!") - .WithAllowedMentions(Mentions.None)); - } - } - else - { - if (isDehoist) - { - await ctx.RespondAsync(new DiscordMessageBuilder() - .WithContent(isPermissionError ? $"{Program.cfgjson.Emoji.Error} Failed to permadehoist {discordUsers[0].Mention}! Do I have permission?" : $"{Program.cfgjson.Emoji.Error} Failed to permadehoist {discordUsers[0].Mention}!") - .WithAllowedMentions(Mentions.None)); - } - else - { - await ctx.RespondAsync(new DiscordMessageBuilder() - .WithContent(isPermissionError ? $"{Program.cfgjson.Emoji.Error} Failed to remove the permadehoist for {discordUsers[0].Mention}! Do I have permission?" : $"{Program.cfgjson.Emoji.Error} Failed to remove the permadehoist for {discordUsers[0].Mention}!") - .WithAllowedMentions(Mentions.None)); - } - } - - return; - } - - // Toggle permadehoist for multiple members - - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it..."); - var msg = await ctx.GetResponseAsync(); - int failedCount = 0; - - foreach (var discordUser in discordUsers) - { - var (success, _, _) = await DehoistHelpers.TogglePermadehoist(discordUser, ctx.User, ctx.Guild); - - if (!success) - failedCount++; - } - _ = await msg.ModifyAsync($"{Program.cfgjson.Emoji.Success} Successfully toggled permadehoist for {discordUsers.Length - failedCount} of {discordUsers.Length} member(s)! (Check Audit Log for details)"); - } - - [Command("enable")] - [Description("Permanently dehoist a member (or members). They will be automatically dehoisted until disabled.")] - public async Task PermadehoistEnableCmd(TextCommandContext ctx, [Description("The member(s) to permadehoist.")] params DiscordUser[] discordUsers) - { - if (discordUsers.Length == 0) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} You need to tell me who to permadehoist!"); - return; - } - - if (discordUsers.Length == 1) - { - // Permadehoist single member - - var (success, isPermissionError) = await DehoistHelpers.PermadehoistMember(discordUsers[0], ctx.User, ctx.Guild); - - if (success) - await ctx.RespondAsync(new DiscordMessageBuilder() - .WithContent($"{Program.cfgjson.Emoji.On} Successfully permadehoisted {discordUsers[0].Mention}!") - .WithAllowedMentions(Mentions.None)); - - if (!success & !isPermissionError) - await ctx.RespondAsync(new DiscordMessageBuilder() - .WithContent($"{Program.cfgjson.Emoji.Error} {discordUsers[0].Mention} is already permadehoisted!") - .WithAllowedMentions(Mentions.None)); - - if (!success && isPermissionError) - await ctx.RespondAsync(new DiscordMessageBuilder() - .WithContent($"{Program.cfgjson.Emoji.Error} Failed to permadehoist {discordUsers[0].Mention}!") - .WithAllowedMentions(Mentions.None)); - - return; - } - - // Permadehoist multiple members - - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it..."); - var msg = await ctx.GetResponseAsync(); - int failedCount = 0; - - foreach (var discordUser in discordUsers) - { - var (success, _) = await DehoistHelpers.PermadehoistMember(discordUser, ctx.User, ctx.Guild); - - if (!success) - failedCount++; - } - _ = await msg.ModifyAsync($"{Program.cfgjson.Emoji.Success} Successfully permadehoisted {discordUsers.Length - failedCount} of {discordUsers.Length} member(s)! (Check Audit Log for details)"); - } - - [Command("disable")] - [Description("Disable permadehoist for a member (or members).")] - public async Task PermadehoistDisableCmd(TextCommandContext ctx, [Description("The member(s) to remove the permadehoist for.")] params DiscordUser[] discordUsers) - { - if (discordUsers.Length == 0) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} You need to tell me who to un-permadehoist!"); - return; - } - - if (discordUsers.Length == 1) - { - // Un-permadehoist single member - - var (success, isPermissionError) = await DehoistHelpers.UnpermadehoistMember(discordUsers[0], ctx.User, ctx.Guild); - - if (success) - await ctx.RespondAsync(new DiscordMessageBuilder() - .WithContent($"{Program.cfgjson.Emoji.Off} Successfully removed the permadehoist for {discordUsers[0].Mention}!") - .WithAllowedMentions(Mentions.None)); - - if (!success & !isPermissionError) - await ctx.RespondAsync(new DiscordMessageBuilder() - .WithContent($"{Program.cfgjson.Emoji.Error} {discordUsers[0].Mention} isn't permadehoisted!") - .WithAllowedMentions(Mentions.None)); - - if (!success && isPermissionError) - await ctx.RespondAsync(new DiscordMessageBuilder() - .WithContent($"{Program.cfgjson.Emoji.Error} Failed to remove the permadehoist for {discordUsers[0].Mention}!") - .WithAllowedMentions(Mentions.None)); - - return; - } - - // Un-permadehoist multiple members - - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it..."); - var msg = await ctx.GetResponseAsync(); - int failedCount = 0; - - foreach (var discordUser in discordUsers) - { - var (success, _) = await DehoistHelpers.UnpermadehoistMember(discordUser, ctx.User, ctx.Guild); - - if (!success) - failedCount++; - } - _ = await msg.ModifyAsync($"{Program.cfgjson.Emoji.Success} Successfully removed the permadehoist for {discordUsers.Length - failedCount} of {discordUsers.Length} member(s)! (Check Audit Log for details)"); - } - - [Command("status")] - [Description("Check the status of permadehoist for a member.")] - public async Task PermadehoistStatus(TextCommandContext ctx, [Description("The member whose permadehoist status to check.")] DiscordUser discordUser) - { - if (await Program.db.SetContainsAsync("permadehoists", discordUser.Id)) - await ctx.RespondAsync(new DiscordMessageBuilder() - .WithContent($"{Program.cfgjson.Emoji.On} {discordUser.Mention} is permadehoisted.") - .WithAllowedMentions(Mentions.None)); - else - await ctx.RespondAsync(new DiscordMessageBuilder() - .WithContent($"{Program.cfgjson.Emoji.Off} {discordUser.Mention} is not permadehoisted.") - .WithAllowedMentions(Mentions.None)); - } - } - } -} diff --git a/Commands/DehoistCmds.cs b/Commands/DehoistCmds.cs new file mode 100644 index 00000000..380be075 --- /dev/null +++ b/Commands/DehoistCmds.cs @@ -0,0 +1,217 @@ +namespace Cliptok.Commands +{ + public class DehoistCmds + { + [Command("dehoist")] + [Description("Dehoist a member, dropping them to the bottom of the list. Lasts until they change nickname.")] + [AllowedProcessors(typeof(SlashCommandProcessor), typeof(TextCommandProcessor))] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] + public async Task DehoistCmd(CommandContext ctx, [Parameter("member"), Description("The member to dehoist.")] DiscordUser user) + { + DiscordMember member; + try + { + member = await ctx.Guild.GetMemberAsync(user.Id); + } + catch + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Failed to find {user.Mention} as a member! Are they in the server?", ephemeral: true); + return; + } + + if (member.DisplayName[0] == DehoistHelpers.dehoistCharacter) + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {member.Mention} is already dehoisted!", ephemeral: true); + return; + } + + try + { + await member.ModifyAsync(a => + { + a.Nickname = DehoistHelpers.DehoistName(member.DisplayName); + a.AuditLogReason = $"[Dehoist by {DiscordHelpers.UniqueUsername(ctx.User)}]"; + }); + } + catch + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Failed to dehoist {member.Mention}! Do I have permission?", ephemeral: true); + return; + } + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Successfuly dehoisted {member.Mention}!", mentions: false); + } + + [Command("permadehoist")] + [Description("Permanently/persistently dehoist members.")] + [AllowedProcessors(typeof(SlashCommandProcessor), typeof(TextCommandProcessor))] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermission.ManageNicknames)] + public class PermadehoistCmds + { + [DefaultGroupCommand] + [Command("toggle")] + [Description("Toggle permadehoist status for a member.")] + [AllowedProcessors(typeof(TextCommandProcessor))] + public async Task PermadehoistToggleCmd(CommandContext ctx, [Description("The member to permadehoist.")] DiscordUser user) + { + var (success, isPermissionError, isDehoist) = await DehoistHelpers.TogglePermadehoist(user, ctx.User, ctx.Guild); + + if (success) + { + if (isDehoist) + { + await ctx.RespondAsync(new DiscordMessageBuilder() + .WithContent($"{Program.cfgjson.Emoji.On} Successfully permadehoisted {user.Mention}!") + .WithAllowedMentions(Mentions.None)); + } + else + { + await ctx.RespondAsync(new DiscordMessageBuilder() + .WithContent($"{Program.cfgjson.Emoji.Off} Successfully removed the permadehoist for {user.Mention}!") + .WithAllowedMentions(Mentions.None)); + } + } + else + { + if (isDehoist) + { + await ctx.RespondAsync(new DiscordMessageBuilder() + .WithContent(isPermissionError ? $"{Program.cfgjson.Emoji.Error} Failed to permadehoist {user.Mention}! Do I have permission?" : $"{Program.cfgjson.Emoji.Error} Failed to permadehoist {user.Mention}!") + .WithAllowedMentions(Mentions.None)); + } + else + { + await ctx.RespondAsync(new DiscordMessageBuilder() + .WithContent(isPermissionError ? $"{Program.cfgjson.Emoji.Error} Failed to remove the permadehoist for {user.Mention}! Do I have permission?" : $"{Program.cfgjson.Emoji.Error} Failed to remove the permadehoist for {user.Mention}!") + .WithAllowedMentions(Mentions.None)); + } + } + } + + [Command("enable")] + [Description("Permanently dehoist a member. They will be automatically dehoisted until disabled.")] + public async Task PermadehoistEnableSlashCmd(CommandContext ctx, [Parameter("member"), Description("The member to permadehoist.")] DiscordUser discordUser) + { + var (success, isPermissionError) = await DehoistHelpers.PermadehoistMember(discordUser, ctx.User, ctx.Guild); + + if (success) + await ctx.RespondAsync($"{Program.cfgjson.Emoji.On} Successfully permadehoisted {discordUser.Mention}!", mentions: false); + + if (!success & !isPermissionError) + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {discordUser.Mention} is already permadehoisted!", mentions: false); + + if (!success && isPermissionError) + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Failed to permadehoist {discordUser.Mention}!", mentions: false); + } + + [Command("disable")] + [Description("Disable permadehoist for a member.")] + public async Task PermadehoistDisableSlashCmd(CommandContext ctx, [Parameter("member"), Description("The member to remove the permadehoist for.")] DiscordUser discordUser) + { + var (success, isPermissionError) = await DehoistHelpers.UnpermadehoistMember(discordUser, ctx.User, ctx.Guild); + + if (success) + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Off} Successfully removed the permadehoist for {discordUser.Mention}!", mentions: false); + + if (!success & !isPermissionError) + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {discordUser.Mention} isn't permadehoisted!", mentions: false); + + if (!success && isPermissionError) + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Failed to remove the permadehoist for {discordUser.Mention}!", mentions: false); + } + + [Command("status")] + [Description("Check the status of permadehoist for a member.")] + public async Task PermadehoistStatusSlashCmd(CommandContext ctx, [Parameter("member"), Description("The member whose permadehoist status to check.")] DiscordUser discordUser) + { + if (await Program.db.SetContainsAsync("permadehoists", discordUser.Id)) + await ctx.RespondAsync($"{Program.cfgjson.Emoji.On} {discordUser.Mention} is permadehoisted.", mentions: false); + else + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Off} {discordUser.Mention} is not permadehoisted.", mentions: false); + } + } + + [Command("massdehoisttextcmd")] + [TextAlias("massdehoist")] + [Description("Dehoist everyone on the server who has a bad name. This may take a while and can exhaust rate limits.")] + [AllowedProcessors(typeof(TextCommandProcessor))] + [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] + public async Task MassDehoist(TextCommandContext ctx) + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it. This will take a while."); + var msg = await ctx.GetResponseAsync(); + var discordMembers = await ctx.Guild.GetAllMembersAsync().ToListAsync(); + int failedCount = 0; + + foreach (DiscordMember discordMember in discordMembers) + { + bool success = await DehoistHelpers.CheckAndDehoistMemberAsync(discordMember, ctx.User, true); + if (!success) + failedCount++; + } + + _ = msg.DeleteAsync(); + await ctx.Channel.SendMessageAsync(new DiscordMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Success} Successfully dehoisted {discordMembers.Count() - failedCount} of {discordMembers.Count()} member(s)! (Check Audit Log for details)").WithReply(ctx.Message.Id, true, false)); + } + + [Command("massundehoisttextcmd")] + [TextAlias("massundehoist")] + [Description("Remove the dehoist for users attached via a txt file.")] + [AllowedProcessors(typeof(TextCommandProcessor))] + [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] + public async Task MassUndhoist(TextCommandContext ctx) + { + int failedCount = 0; + + if (ctx.Message.Attachments.Count == 0) + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Please upload an attachment as well."); + } + else + { + string strList; + using (HttpClient client = new()) + { + strList = await client.GetStringAsync(ctx.Message.Attachments[0].Url); + } + + var list = strList.Split(' '); + + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it. This will take a while."); + var msg = await ctx.GetResponseAsync(); + + foreach (string strID in list) + { + ulong id = Convert.ToUInt64(strID); + DiscordMember member = default; + try + { + member = await ctx.Guild.GetMemberAsync(id); + } + catch (DSharpPlus.Exceptions.NotFoundException) + { + failedCount++; + continue; + } + + if (member.DisplayName[0] == DehoistHelpers.dehoistCharacter) + { + var newNickname = member.Nickname[1..]; + await member.ModifyAsync(a => + { + a.Nickname = newNickname; + a.AuditLogReason = $"[Mass undehoist by {DiscordHelpers.UniqueUsername(ctx.User)}]"; + } + ); + } + else + { + failedCount++; + } + } + + await msg.ModifyAsync($"{Program.cfgjson.Emoji.Success} Successfully undehoisted {list.Length - failedCount} of {list.Length} member(s)! (Check Audit Log for details)"); + + } + } + } +} \ No newline at end of file diff --git a/Commands/DmRelayBlock.cs b/Commands/DmRelayCmds.cs similarity index 95% rename from Commands/DmRelayBlock.cs rename to Commands/DmRelayCmds.cs index 40b7ea33..c292ffbe 100644 --- a/Commands/DmRelayBlock.cs +++ b/Commands/DmRelayCmds.cs @@ -1,6 +1,6 @@ -namespace Cliptok.Commands +namespace Cliptok.Commands { - internal class DmRelayBlock + public class DmRelayCmds { [Command("dmrelayblocktextcmd")] [TextAlias("dmrelayblock", "dmblock")] @@ -26,4 +26,4 @@ public async Task DmRelayBlockCommand(TextCommandContext ctx, [Description("The await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} {user.Mention} has been blocked. Their DMs will not appear here."); } } -} +} \ No newline at end of file diff --git a/Commands/FunCmds.cs b/Commands/FunCmds.cs index 5fec2614..e007f3ef 100644 --- a/Commands/FunCmds.cs +++ b/Commands/FunCmds.cs @@ -1,25 +1,88 @@ -namespace Cliptok.Commands +using Cliptok.Constants; + +namespace Cliptok.Commands { - internal class FunCmds + public class FunCmds { - [Command("tellrawtextcmd")] - [TextAlias("tellraw")] - [Description("Nothing of interest.")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] - public async Task TellRaw(TextCommandContext ctx, [Description("???")] DiscordChannel discordChannel, [RemainingText, Description("???")] string output) + [Command("Hug")] + [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] + [AllowedProcessors(typeof(UserCommandProcessor))] + public async Task Hug(UserCommandContext ctx, DiscordUser targetUser) + { + var user = targetUser; + + if (user is not null) + { + switch (new Random().Next(4)) + { + case 0: + await ctx.RespondAsync($"*{ctx.User.Mention} snuggles {user.Mention}*"); + break; + + case 1: + await ctx.RespondAsync($"*{ctx.User.Mention} huggles {user.Mention}*"); + break; + + case 2: + await ctx.RespondAsync($"*{ctx.User.Mention} cuddles {user.Mention}*"); + break; + + case 3: + await ctx.RespondAsync($"*{ctx.User.Mention} hugs {user.Mention}*"); + break; + } + } + } + + [Command("tellraw")] + [Description("You know what you're here for.")] + [AllowedProcessors(typeof(SlashCommandProcessor), typeof(TextCommandProcessor))] + [RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermission.ModerateMembers)] + public async Task TellRaw(CommandContext ctx, [Parameter("channel"), Description("Either mention or ID. Not a name.")] string discordChannel, [Parameter("input"), Description("???")] string input, [Parameter("reply_msg_id"), Description("ID of message to use in a reply context.")] string replyID = "0", [Parameter("pingreply"), Description("Ping pong.")] bool pingreply = true) { + DiscordChannel channelObj = default; + ulong channelId; + if (!ulong.TryParse(discordChannel, out channelId)) + { + var captures = RegexConstants.channel_rx.Match(discordChannel).Groups[1].Captures; + if (captures.Count > 0) + channelId = Convert.ToUInt64(captures[0].Value); + else + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} The channel you gave can't be parsed. Please give either an ID or a mention of a channel.", ephemeral: true); + return; + } + } try { - await discordChannel.SendMessageAsync(output); + channelObj = await ctx.Client.GetChannelAsync(channelId); } catch { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Your dumb message didn't want to send. Congrats, I'm proud of you."); + // caught immediately after + } + if (channelObj == default) + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} I can't find a channel with the provided ID!", ephemeral: true); return; } - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} I sent your stupid message to {discordChannel.Mention}."); + try + { + await channelObj.SendMessageAsync(new DiscordMessageBuilder().WithContent(input).WithReply(Convert.ToUInt64(replyID), pingreply, false)); + } + catch + { + await ctx.RespondAsync($"Your dumb message didn't want to send. Congrats, I'm proud of you.", ephemeral: true); + return; + } + await ctx.RespondAsync($"I sent your stupid message to {channelObj.Mention}.", ephemeral: true); + await LogChannelHelper.LogMessageAsync("secret", + new DiscordMessageBuilder() + .WithContent($"{ctx.User.Mention} used tellraw in {channelObj.Mention}:") + .WithAllowedMentions(Mentions.None) + .AddEmbed(new DiscordEmbedBuilder().WithDescription(input)) + ); } [Command("notextcmd")] @@ -64,4 +127,4 @@ public async Task No(TextCommandContext ctx) } } -} +} \ No newline at end of file diff --git a/Commands/Grant.cs b/Commands/Grant.cs deleted file mode 100644 index 20eae9f2..00000000 --- a/Commands/Grant.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Cliptok.Commands -{ - internal class Grant - { - [Command("granttextcmd")] - [Description("Grant a user access to the server, by giving them the Tier 1 role.")] - [TextAlias("grant", "clipgrant", "verify")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public async Task GrantCommand(TextCommandContext ctx, [Description("The member to grant Tier 1 role to.")] DiscordUser _) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} This command is deprecated and no longer works. Please right click (or tap and hold on mobile) the user and click \"Verify Member\" if available."); - } - } -} diff --git a/Commands/InteractionCommands/BanInteractions.cs b/Commands/InteractionCommands/BanInteractions.cs deleted file mode 100644 index 96267a58..00000000 --- a/Commands/InteractionCommands/BanInteractions.cs +++ /dev/null @@ -1,199 +0,0 @@ -using static Cliptok.Helpers.BanHelpers; - -namespace Cliptok.Commands.InteractionCommands -{ - internal class BanInteractions - { - [Command("ban")] - [Description("Bans a user from the server, either permanently or temporarily.")] - [AllowedProcessors(typeof(SlashCommandProcessor))] - [RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermission.BanMembers)] - public async Task BanSlashCommand(SlashCommandContext ctx, - [Parameter("user"), Description("The user to ban")] DiscordUser user, - [Parameter("reason"), Description("The reason the user is being banned")] string reason, - [Parameter("keep_messages"), Description("Whether to keep the users messages when banning")] bool keepMessages = false, - [Parameter("time"), Description("The length of time the user is banned for")] string time = null, - [Parameter("appeal_link"), Description("Whether to show the user an appeal URL in the DM")] bool appealable = false, - [Parameter("compromised_account"), Description("Whether to include special instructions for compromised accounts")] bool compromisedAccount = false - ) - { - // Initial response to avoid the 3 second timeout, will edit later. - var eout = new DiscordInteractionResponseBuilder().AsEphemeral(true); - await ctx.DeferResponseAsync(true); - - // Edits need a webhook rather than interaction..? - DiscordWebhookBuilder webhookOut = new(); - int messageDeleteDays = 7; - if (keepMessages) - messageDeleteDays = 0; - - if (user.IsBot) - { - webhookOut.Content = $"{Program.cfgjson.Emoji.Error} To prevent accidents, I won't ban bots. If you really need to do this, do it manually in Discord."; - await ctx.EditResponseAsync(webhookOut); - return; - } - - DiscordMember targetMember; - - try - { - targetMember = await ctx.Guild.GetMemberAsync(user.Id); - if ((await GetPermLevelAsync(ctx.Member)) == ServerPermLevel.TrialModerator && ((await GetPermLevelAsync(targetMember)) >= ServerPermLevel.TrialModerator)) - { - webhookOut.Content = $"{Program.cfgjson.Emoji.Error} As a Trial Moderator you cannot perform moderation actions on other staff members."; - await ctx.EditResponseAsync(webhookOut); - return; - } - } - catch - { - // do nothing :/ - } - - TimeSpan banDuration; - if (time is null) - banDuration = default; - else - { - try - { - banDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(ctx.Interaction.CreationTimestamp.DateTime); - } - catch - { - webhookOut.Content = $"{Program.cfgjson.Emoji.Error} There was an error parsing your supplied ban length!"; - await ctx.EditResponseAsync(webhookOut); - return; - } - - } - - DiscordMember member; - try - { - member = await ctx.Guild.GetMemberAsync(user.Id); - } - catch - { - member = null; - } - - if (member is null) - { - await BanHelpers.BanFromServerAsync(user.Id, reason, ctx.User.Id, ctx.Guild, messageDeleteDays, ctx.Channel, banDuration, appealable); - } - else - { - if (DiscordHelpers.AllowedToMod(ctx.Member, member)) - { - if (DiscordHelpers.AllowedToMod(await ctx.Guild.GetMemberAsync(ctx.Client.CurrentUser.Id), member)) - { - await BanHelpers.BanFromServerAsync(user.Id, reason, ctx.User.Id, ctx.Guild, messageDeleteDays, ctx.Channel, banDuration, appealable); - } - else - { - webhookOut.Content = $"{Program.cfgjson.Emoji.Error} I don't have permission to ban **{DiscordHelpers.UniqueUsername(user)}**!"; - await ctx.EditResponseAsync(webhookOut); - return; - } - } - else - { - webhookOut.Content = $"{Program.cfgjson.Emoji.Error} You don't have permission to ban **{DiscordHelpers.UniqueUsername(user)}**!"; - await ctx.EditResponseAsync(webhookOut); - return; - } - } - reason = reason.Replace("`", "\\`").Replace("*", "\\*"); - if (banDuration == default) - await ctx.Channel.SendMessageAsync($"{Program.cfgjson.Emoji.Banned} {user.Mention} has been banned: **{reason}**"); - else - await ctx.Channel.SendMessageAsync($"{Program.cfgjson.Emoji.Banned} {user.Mention} has been banned for **{TimeHelpers.TimeToPrettyFormat(banDuration, false)}**: **{reason}**"); - - webhookOut.Content = $"{Program.cfgjson.Emoji.Success} User was successfully bonked."; - await ctx.EditResponseAsync(webhookOut); - } - - [Command("unban")] - [Description("Unbans a user who has been previously banned.")] - [AllowedProcessors(typeof(SlashCommandProcessor))] - [RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermission.BanMembers)] - public async Task SlashUnbanCommand(SlashCommandContext ctx, [Parameter("user"), Description("The ID or mention of the user to unban. Ignore the suggestions, IDs work.")] SnowflakeObject userId, [Parameter("reason"), Description("Used in audit log only currently")] string reason = "No reason specified.") - { - DiscordUser targetUser = default; - try - { - targetUser = await ctx.Client.GetUserAsync(userId.Id); - } - catch (Exception ex) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Exception of type `{ex.GetType()}` thrown fetching user:\n```\n{ex.Message}\n{ex.StackTrace}```", ephemeral: true); - return; - } - if ((await Program.db.HashExistsAsync("bans", targetUser.Id))) - { - await UnbanUserAsync(ctx.Guild, targetUser, $"[Unban by {DiscordHelpers.UniqueUsername(ctx.User)}]: {reason}"); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Unbanned} Successfully unbanned **{DiscordHelpers.UniqueUsername(targetUser)}**."); - } - else - { - bool banSuccess = await UnbanUserAsync(ctx.Guild, targetUser); - if (banSuccess) - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Unbanned} Successfully unbanned **{DiscordHelpers.UniqueUsername(targetUser)}**."); - else - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} That user doesn't appear to be banned, *and* an error occurred while attempting to unban them anyway.\nPlease contact the bot owner if this wasn't expected, the error has been logged."); - } - } - } - - [Command("kick")] - [Description("Kicks a user, removing them from the server until they rejoin.")] - [AllowedProcessors(typeof(SlashCommandProcessor))] - [RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermission.KickMembers)] - public async Task KickCmd(SlashCommandContext ctx, [Parameter("user"), Description("The user you want to kick from the server.")] DiscordUser target, [Parameter("reason"), Description("The reason for kicking this user.")] string reason = "No reason specified.") - { - if (target.IsBot) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} To prevent accidents, I won't kick bots. If you really need to do this, do it manually in Discord."); - return; - } - - reason = reason.Replace("`", "\\`").Replace("*", "\\*"); - - DiscordMember member; - try - { - member = await ctx.Guild.GetMemberAsync(target.Id); - } - catch - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} That user doesn't appear to be in the server!"); - return; - } - - if (DiscordHelpers.AllowedToMod(ctx.Member, member)) - { - if (DiscordHelpers.AllowedToMod(await ctx.Guild.GetMemberAsync(ctx.Client.CurrentUser.Id), member)) - { - await Kick.KickAndLogAsync(member, reason, ctx.Member); - await ctx.Channel.SendMessageAsync($"{Program.cfgjson.Emoji.Ejected} {target.Mention} has been kicked: **{reason}**"); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Done!", ephemeral: true); - return; - } - else - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} I don't have permission to kick **{DiscordHelpers.UniqueUsername(target)}**!", ephemeral: true); - return; - } - } - else - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} You aren't allowed to kick **{DiscordHelpers.UniqueUsername(target)}**!", ephemeral: true); - return; - } - } - - } -} diff --git a/Commands/InteractionCommands/ContextCommands.cs b/Commands/InteractionCommands/ContextCommands.cs deleted file mode 100644 index 3adfc720..00000000 --- a/Commands/InteractionCommands/ContextCommands.cs +++ /dev/null @@ -1,80 +0,0 @@ -namespace Cliptok.Commands.InteractionCommands -{ - internal class ContextCommands - { - [Command("Show Avatar")] - [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] - [AllowedProcessors(typeof(UserCommandProcessor))] - public async Task ContextAvatar(CommandContext ctx, DiscordUser targetUser) - { - string avatarUrl = await LykosAvatarMethods.UserOrMemberAvatarURL(targetUser, ctx.Guild); - - DiscordEmbedBuilder embed = new DiscordEmbedBuilder() - .WithColor(new DiscordColor(0xC63B68)) - .WithTimestamp(DateTime.UtcNow) - .WithImageUrl(avatarUrl) - .WithAuthor( - $"Avatar for {targetUser.Username} (Click to open in browser)", - avatarUrl - ); - - await ctx.RespondAsync(null, embed, ephemeral: true); - } - - [Command("Show Notes")] - [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] - [AllowedProcessors(typeof(UserCommandProcessor))] - [RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermission.ModerateMembers)] - public async Task ShowNotes(CommandContext ctx, DiscordUser targetUser) - { - await ctx.RespondAsync(embed: await UserNoteHelpers.GenerateUserNotesEmbedAsync(targetUser), ephemeral: true); - } - - [Command("Show Warnings")] - [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] - [AllowedProcessors(typeof(UserCommandProcessor))] - public async Task ContextWarnings(CommandContext ctx, DiscordUser targetUser) - { - await ctx.RespondAsync(embed: await WarningHelpers.GenerateWarningsEmbedAsync(targetUser), ephemeral: true); - } - - [Command("User Information")] - [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] - [AllowedProcessors(typeof(UserCommandProcessor))] - public async Task ContextUserInformation(CommandContext ctx, DiscordUser targetUser) - { - await ctx.RespondAsync(embed: await DiscordHelpers.GenerateUserEmbed(targetUser, ctx.Guild), ephemeral: true); - } - - [Command("Hug")] - [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] - [AllowedProcessors(typeof(UserCommandProcessor))] - public async Task Hug(CommandContext ctx, DiscordUser targetUser) - { - var user = targetUser; - - if (user is not null) - { - switch (new Random().Next(4)) - { - case 0: - await ctx.RespondAsync($"*{ctx.User.Mention} snuggles {user.Mention}*"); - break; - - case 1: - await ctx.RespondAsync($"*{ctx.User.Mention} huggles {user.Mention}*"); - break; - - case 2: - await ctx.RespondAsync($"*{ctx.User.Mention} cuddles {user.Mention}*"); - break; - - case 3: - await ctx.RespondAsync($"*{ctx.User.Mention} hugs {user.Mention}*"); - break; - } - } - } - - } -} diff --git a/Commands/InteractionCommands/DebugInteractions.cs b/Commands/InteractionCommands/DebugInteractions.cs deleted file mode 100644 index f9692cb4..00000000 --- a/Commands/InteractionCommands/DebugInteractions.cs +++ /dev/null @@ -1,129 +0,0 @@ -using Cliptok.Constants; - -namespace Cliptok.Commands.InteractionCommands -{ - internal class DebugInteractions - { - [Command("scamcheck")] - [Description("Check if a link or message is known to the anti-phishing API.")] - [AllowedProcessors(typeof(SlashCommandProcessor))] - [RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermission.ModerateMembers)] - public async Task ScamCheck(SlashCommandContext ctx, [Parameter("input"), Description("Domain or message content to scan.")] string content) - { - var urlMatches = Constants.RegexConstants.url_rx.Matches(content); - if (urlMatches.Count > 0 && Environment.GetEnvironmentVariable("CLIPTOK_ANTIPHISHING_ENDPOINT") is not null && Environment.GetEnvironmentVariable("CLIPTOK_ANTIPHISHING_ENDPOINT") != "useyourimagination") - { - var (match, httpStatus, responseText, _) = await APIs.PhishingAPI.PhishingAPICheckAsync(content); - - string responseToSend; - if (match) - { - responseToSend = $"Match found:\n`"; - } - else - { - responseToSend = $"No valid match found.\nHTTP Status `{(int)httpStatus}`, result:\n"; - } - - responseToSend += await StringHelpers.CodeOrHasteBinAsync(responseText, "json"); - - await ctx.RespondAsync(responseToSend); - } - else - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Anti-phishing API is not configured, nothing for me to do."); - } - } - - [Command("tellraw")] - [Description("You know what you're here for.")] - [AllowedProcessors(typeof(SlashCommandProcessor))] - [RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermission.ModerateMembers)] - public async Task TellRaw(SlashCommandContext ctx, [Parameter("input"), Description("???")] string input, [Parameter("reply_msg_id"), Description("ID of message to use in a reply context.")] string replyID = "0", [Parameter("pingreply"), Description("Ping pong.")] bool pingreply = true, [Parameter("channel"), Description("Either mention or ID. Not a name.")] string discordChannel = default) - { - DiscordChannel channelObj = default; - - if (discordChannel == default) - channelObj = ctx.Channel; - else - { - ulong channelId; - if (!ulong.TryParse(discordChannel, out channelId)) - { - var captures = RegexConstants.channel_rx.Match(discordChannel).Groups[1].Captures; - if (captures.Count > 0) - channelId = Convert.ToUInt64(captures[0].Value); - else - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} The channel you gave can't be parsed. Please give either an ID or a mention of a channel.", ephemeral: true); - return; - } - } - try - { - channelObj = await ctx.Client.GetChannelAsync(channelId); - } - catch - { - // caught immediately after - } - if (channelObj == default) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} I can't find a channel with the provided ID!", ephemeral: true); - return; - } - } - - try - { - await channelObj.SendMessageAsync(new DiscordMessageBuilder().WithContent(input).WithReply(Convert.ToUInt64(replyID), pingreply, false)); - } - catch - { - await ctx.RespondAsync($"Your dumb message didn't want to send. Congrats, I'm proud of you.", ephemeral: true); - return; - } - await ctx.RespondAsync($"I sent your stupid message to {channelObj.Mention}.", ephemeral: true); - await LogChannelHelper.LogMessageAsync("secret", - new DiscordMessageBuilder() - .WithContent($"{ctx.User.Mention} used tellraw in {channelObj.Mention}:") - .WithAllowedMentions(Mentions.None) - .AddEmbed(new DiscordEmbedBuilder().WithDescription(input)) - ); - } - - [Command("userinfo")] - [Description("Retrieve information about a given user.")] - [AllowedProcessors(typeof(SlashCommandProcessor))] - public async Task UserInfoSlashCommand(SlashCommandContext ctx, [Parameter("user"), Description("The user to retrieve information about.")] DiscordUser user, [Parameter("public"), Description("Whether to show the output publicly.")] bool publicMessage = false) - { - await ctx.RespondAsync(embed: await DiscordHelpers.GenerateUserEmbed(user, ctx.Guild), ephemeral: !publicMessage); - } - - [Command("muteinfo")] - [Description("Show information about the mute for a user.")] - [AllowedProcessors(typeof(SlashCommandProcessor))] - [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - [RequirePermissions(DiscordPermission.ModerateMembers)] - public async Task MuteInfoSlashCommand( - SlashCommandContext ctx, - [Parameter("user"), Description("The user whose mute information to show.")] DiscordUser targetUser, - [Parameter("public"), Description("Whether to show the output publicly. Default: false")] bool isPublic = false) - { - await ctx.RespondAsync(embed: await MuteHelpers.MuteStatusEmbed(targetUser, ctx.Guild), ephemeral: !isPublic); - } - - [Command("baninfo")] - [Description("Show information about the ban for a user.")] - [AllowedProcessors(typeof(SlashCommandProcessor))] - [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - [RequirePermissions(DiscordPermission.ModerateMembers)] - public async Task BanInfoSlashCommand( - SlashCommandContext ctx, - [Parameter("user"), Description("The user whose ban information to show.")] DiscordUser targetUser, - [Parameter("public"), Description("Whether to show the output publicly. Default: false")] bool isPublic = false) - { - await ctx.RespondAsync(embed: await BanHelpers.BanStatusEmbed(targetUser, ctx.Guild), ephemeral: !isPublic); - } - } -} diff --git a/Commands/InteractionCommands/DehoistInteractions.cs b/Commands/InteractionCommands/DehoistInteractions.cs deleted file mode 100644 index b15e0781..00000000 --- a/Commands/InteractionCommands/DehoistInteractions.cs +++ /dev/null @@ -1,92 +0,0 @@ -namespace Cliptok.Commands.InteractionCommands -{ - internal class DehoistInteractions - { - [Command("dehoist")] - [Description("Dehoist a member, dropping them to the bottom of the list. Lasts until they change nickname.")] - [AllowedProcessors(typeof(SlashCommandProcessor))] - [RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermission.ManageNicknames)] - public async Task DehoistSlashCmd(SlashCommandContext ctx, [Parameter("member"), Description("The member to dehoist.")] DiscordUser user) - { - DiscordMember member; - try - { - member = await ctx.Guild.GetMemberAsync(user.Id); - } - catch - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Failed to find {user.Mention} as a member! Are they in the server?", ephemeral: true); - return; - } - - if (member.DisplayName[0] == DehoistHelpers.dehoistCharacter) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {member.Mention} is already dehoisted!", ephemeral: true); - } - - try - { - await member.ModifyAsync(a => - { - a.Nickname = DehoistHelpers.DehoistName(member.DisplayName); - a.AuditLogReason = $"[Dehoist by {DiscordHelpers.UniqueUsername(ctx.User)}]"; - }); - } - catch - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Failed to dehoist {member.Mention}! Do I have permission?", ephemeral: true); - return; - } - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Successfuly dehoisted {member.Mention}!", mentions: false); - } - - [Command("permadehoist")] - [Description("Permanently/persistently dehoist members.")] - [AllowedProcessors(typeof(SlashCommandProcessor))] - [RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermission.ManageNicknames)] - public class PermadehoistSlashCommands - { - [Command("enable")] - [Description("Permanently dehoist a member. They will be automatically dehoisted until disabled.")] - public async Task PermadehoistEnableSlashCmd(SlashCommandContext ctx, [Parameter("member"), Description("The member to permadehoist.")] DiscordUser discordUser) - { - var (success, isPermissionError) = await DehoistHelpers.PermadehoistMember(discordUser, ctx.User, ctx.Guild); - - if (success) - await ctx.RespondAsync($"{Program.cfgjson.Emoji.On} Successfully permadehoisted {discordUser.Mention}!", mentions: false); - - if (!success & !isPermissionError) - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {discordUser.Mention} is already permadehoisted!", mentions: false); - - if (!success && isPermissionError) - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Failed to permadehoist {discordUser.Mention}!", mentions: false); - } - - [Command("disable")] - [Description("Disable permadehoist for a member.")] - public async Task PermadehoistDisableSlashCmd(SlashCommandContext ctx, [Parameter("member"), Description("The member to remove the permadehoist for.")] DiscordUser discordUser) - { - var (success, isPermissionError) = await DehoistHelpers.UnpermadehoistMember(discordUser, ctx.User, ctx.Guild); - - if (success) - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Off} Successfully removed the permadehoist for {discordUser.Mention}!", mentions: false); - - if (!success & !isPermissionError) - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {discordUser.Mention} isn't permadehoisted!", mentions: false); - - if (!success && isPermissionError) - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Failed to remove the permadehoist for {discordUser.Mention}!", mentions: false); - } - - [Command("status")] - [Description("Check the status of permadehoist for a member.")] - public async Task PermadehoistStatusSlashCmd(SlashCommandContext ctx, [Parameter("member"), Description("The member whose permadehoist status to check.")] DiscordUser discordUser) - { - if (await Program.db.SetContainsAsync("permadehoists", discordUser.Id)) - await ctx.RespondAsync($"{Program.cfgjson.Emoji.On} {discordUser.Mention} is permadehoisted.", mentions: false); - else - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Off} {discordUser.Mention} is not permadehoisted.", mentions: false); - } - } - } -} diff --git a/Commands/InteractionCommands/LockdownInteractions.cs b/Commands/InteractionCommands/LockdownInteractions.cs deleted file mode 100644 index 937ed29b..00000000 --- a/Commands/InteractionCommands/LockdownInteractions.cs +++ /dev/null @@ -1,166 +0,0 @@ -namespace Cliptok.Commands.InteractionCommands -{ - class LockdownInteractions - { - public static bool ongoingLockdown = false; - - [Command("lockdown")] - [Description("Lock the current channel or all channels in the server, preventing new messages. See also: unlock")] - [AllowedProcessors(typeof(SlashCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions([DiscordPermission.ManageChannels], [])] - public class LockdownCmds - { - [Command("channel")] - [Description("Lock the current channel. See also: unlock channel")] - public async Task LockdownChannelCommand( - SlashCommandContext ctx, - [Parameter("reason"), Description("The reason for the lockdown.")] string reason = "No reason specified.", - [Parameter("time"), Description("The length of time to lock the channel for.")] string time = null, - [Parameter("lockthreads"), Description("Whether to lock this channel's threads. Disables sending messages, but does not archive them.")] bool lockThreads = false) - { - await ctx.DeferResponseAsync(ephemeral: true); - - if (ctx.Channel.Type is DiscordChannelType.PublicThread or DiscordChannelType.PrivateThread or DiscordChannelType.NewsThread) - { - if (lockThreads) - { - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Denied} You can't lock this channel!\n`/lockdown` with `lockthreads` cannot be used inside of a thread. If you meant to lock {ctx.Channel.Parent.Mention} and all of its threads, use the command there.\n\nIf you meant to only lock this thread, use `!lock` instead, or use `/lockdown` with `lockthreads` set to False.").AsEphemeral(true)); - return; - } - - var thread = (DiscordThreadChannel)ctx.Channel; - - await thread.ModifyAsync(a => - { - a.IsArchived = true; - a.Locked = true; - }); - - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Thread locked successfully!").AsEphemeral(true)); - return; - } - - TimeSpan? lockDuration = null; - - if (!string.IsNullOrWhiteSpace(time)) - { - lockDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(ctx.Interaction.CreationTimestamp.DateTime); - } - - var currentChannel = ctx.Channel; - if (!Program.cfgjson.LockdownEnabledChannels.Contains(currentChannel.Id)) - { - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Denied} You can't lock or unlock this channel!\nIf this is in error, add its ID (`{currentChannel.Id}`) to the lockdown whitelist.")); - return; - } - - if (ongoingLockdown) - { - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} A mass lockdown or unlock is already ongoing. Refusing your request to avoid conflicts, sorry.")); - return; - } - - bool success = await LockdownHelpers.LockChannelAsync(user: ctx.User, channel: currentChannel, duration: lockDuration, reason: reason, lockThreads: lockThreads); - if (success) - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Channel locked successfully.").AsEphemeral(true)); - else - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Failed to lock this channel!").AsEphemeral(true)); - } - - [Command("all")] - [Description("Lock all lockable channels in the server. See also: unlock all")] - public async Task LockdownAllCommand( - SlashCommandContext ctx, - [Parameter("reason"), Description("The reason for the lockdown.")] string reason = "", - [Parameter("time"), Description("The length of time to lock the channels for.")] string time = null, - [Parameter("lockthreads"), Description("Whether to lock threads. Disables sending messages, but does not archive them.")] bool lockThreads = false) - { - await ctx.DeferResponseAsync(); - - ongoingLockdown = true; - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Loading} Working on it, please hold...")); - - TimeSpan? lockDuration = null; - - if (!string.IsNullOrWhiteSpace(time)) - { - lockDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(ctx.Interaction.CreationTimestamp.DateTime); - } - - foreach (var chanID in Program.cfgjson.LockdownEnabledChannels) - { - try - { - var channel = await ctx.Client.GetChannelAsync(chanID); - await LockdownHelpers.LockChannelAsync(user: ctx.User, channel: channel, duration: lockDuration, reason: reason, lockThreads: lockThreads); - } - catch - { - - } - - } - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Success} Done!")); - ongoingLockdown = false; - return; - } - } - - [Command("unlock")] - [Description("Unlock the current channel or all channels in the server, allowing new messages. See also: lockdown")] - [AllowedProcessors(typeof(SlashCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions([DiscordPermission.ManageChannels], [])] - public class UnlockCmds - { - [Command("channel")] - [Description("Unlock the current channel. See also: lockdown")] - public async Task UnlockChannelCommand(SlashCommandContext ctx, [Parameter("reason"), Description("The reason for the unlock.")] string reason = "") - { - await ctx.DeferResponseAsync(ephemeral: true); - - var currentChannel = ctx.Channel; - if (!Program.cfgjson.LockdownEnabledChannels.Contains(currentChannel.Id)) - { - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Denied} You can't lock or unlock this channel!\nIf this is in error, add its ID (`{currentChannel.Id}`) to the lockdown whitelist.").AsEphemeral(true)); - return; - } - - if (ongoingLockdown) - { - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} A mass lockdown or unlock is already ongoing. Refusing your request. sorry.").AsEphemeral(true)); - return; - } - bool success = await LockdownHelpers.UnlockChannel(currentChannel, ctx.Member); - if (success) - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Channel unlocked successfully.").AsEphemeral(true)); - else - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Failed to unlock this channel!").AsEphemeral(true)); - } - - [Command("all")] - [Description("Unlock all lockable channels in the server. See also: lockdown all")] - public async Task UnlockAllCommand(SlashCommandContext ctx, [Parameter("reason"), Description("The reason for the unlock.")] string reason = "") - { - await ctx.DeferResponseAsync(); - - ongoingLockdown = true; - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Loading} Working on it, please hold...")); - foreach (var chanID in Program.cfgjson.LockdownEnabledChannels) - { - try - { - var currentChannel = await ctx.Client.GetChannelAsync(chanID); - await LockdownHelpers.UnlockChannel(currentChannel, ctx.Member, reason, true); - } - catch - { - - } - } - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Success} Done!")); - ongoingLockdown = false; - return; - } - } - } -} diff --git a/Commands/InteractionCommands/MuteInteractions.cs b/Commands/InteractionCommands/MuteInteractions.cs deleted file mode 100644 index aa93052c..00000000 --- a/Commands/InteractionCommands/MuteInteractions.cs +++ /dev/null @@ -1,163 +0,0 @@ -namespace Cliptok.Commands.InteractionCommands -{ - internal class MuteInteractions - { - [Command("mute")] - [Description("Mute a user, temporarily or permanently.")] - [AllowedProcessors(typeof(SlashCommandProcessor))] - [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - [RequirePermissions(DiscordPermission.ModerateMembers)] - public async Task MuteSlashCommand( - SlashCommandContext ctx, - [Parameter("user"), Description("The user you wish to mute.")] DiscordUser targetUser, - [Parameter("reason"), Description("The reason for the mute.")] string reason, - [Parameter("time"), Description("The length of time to mute for.")] string time = "" - ) - { - await ctx.DeferResponseAsync(ephemeral: true); - DiscordMember targetMember = default; - try - { - targetMember = await ctx.Guild.GetMemberAsync(targetUser.Id); - } - catch (DSharpPlus.Exceptions.NotFoundException) - { - // is this worth logging? - } - - if (targetMember != default && (await GetPermLevelAsync(ctx.Member)) == ServerPermLevel.TrialModerator && ((await GetPermLevelAsync(targetMember)) >= ServerPermLevel.TrialModerator || targetMember.IsBot)) - { - await ctx.Channel.SendMessageAsync($"{Program.cfgjson.Emoji.Error} {ctx.User.Mention}, as a Trial Moderator you cannot perform moderation actions on other staff members or bots."); - return; - } - - TimeSpan muteDuration = default; - - if (time != "") - { - try - { - muteDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(ctx.Interaction.CreationTimestamp.LocalDateTime); - } - catch - { - await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent($"{Program.cfgjson.Emoji.Error} Failed to parse time argument.")); - throw; - } - } - - await MuteHelpers.MuteUserAsync(targetUser, reason, ctx.User.Id, ctx.Guild, ctx.Channel, muteDuration, true); - await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent("Command completed successfully.")); - } - - [Command("unmute")] - [Description("Unmute a user.")] - [AllowedProcessors(typeof(SlashCommandProcessor))] - [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - [RequirePermissions(DiscordPermission.ModerateMembers)] - public async Task UnmuteSlashCommand( - SlashCommandContext ctx, - [Parameter("user"), Description("The user you wish to mute.")] DiscordUser targetUser, - [Parameter("reason"), Description("The reason for the unmute.")] string reason = "No reason specified." - ) - { - await ctx.DeferResponseAsync(ephemeral: false); - - reason = $"[Manual unmute by {DiscordHelpers.UniqueUsername(ctx.User)}]: {reason}"; - - // todo: store per-guild - DiscordRole mutedRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.MutedRole); - - DiscordMember member = default; - try - { - member = await ctx.Guild.GetMemberAsync(targetUser.Id); - } - catch (DSharpPlus.Exceptions.NotFoundException ex) - { - Program.discord.Logger.LogWarning(eventId: Program.CliptokEventID, exception: ex, message: "Failed to unmute {user} in {server} because they weren't in the server.", $"{DiscordHelpers.UniqueUsername(targetUser)}", ctx.Guild.Name); - } - - if ((await Program.db.HashExistsAsync("mutes", targetUser.Id)) || (member != default && member.Roles.Contains(mutedRole))) - { - await MuteHelpers.UnmuteUserAsync(targetUser, reason, true, ctx.User); - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Information} Successfully unmuted **{DiscordHelpers.UniqueUsername(targetUser)}**.")); - } - else - try - { - await MuteHelpers.UnmuteUserAsync(targetUser, reason, true, ctx.User); - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Warning} According to Discord that user is not muted, but I tried to unmute them anyway. Hope it works.")); - } - catch (Exception e) - { - Program.discord.Logger.LogError(e, "An error occurred unmuting {user}", targetUser.Id); - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} That user doesn't appear to be muted, *and* an error occurred while attempting to unmute them anyway. Please contact the bot owner, the error has been logged.")); - } - } - - [Command("tqsmute")] - [Description("Temporarily mute a user in tech support channels.")] - [AllowedProcessors(typeof(SlashCommandProcessor))] - [RequireHomeserverPerm(ServerPermLevel.TechnicalQueriesSlayer)] - public async Task TqsMuteSlashCommand( - SlashCommandContext ctx, - [Parameter("user"), Description("The user to mute.")] DiscordUser targetUser, - [Parameter("reason"), Description("The reason for the mute.")] string reason) - { - await ctx.DeferResponseAsync(ephemeral: true); - - // only work if TQS mute role is configured - if (Program.cfgjson.TqsMutedRole == 0) - { - await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent($"{Program.cfgjson.Emoji.Error} TQS mutes are not configured, so this command does nothing. Please contact the bot maintainer if this is unexpected.")); - return; - } - - // Only allow usage in #tech-support, #tech-support-forum, and their threads - if (ctx.Channel.Id != Program.cfgjson.TechSupportChannel && - ctx.Channel.Id != Program.cfgjson.SupportForumId && - ctx.Channel.Parent.Id != Program.cfgjson.TechSupportChannel && - ctx.Channel.Parent.Id != Program.cfgjson.SupportForumId) - { - await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent($"{Program.cfgjson.Emoji.Error} This command can only be used in <#{Program.cfgjson.TechSupportChannel}>, <#{Program.cfgjson.SupportForumId}>, and threads in those channels!")); - return; - } - - // Check if the user is already muted; disallow TQS-mute if so - - DiscordRole mutedRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.MutedRole); - DiscordRole tqsMutedRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.TqsMutedRole); - - // Get member - DiscordMember targetMember = default; - try - { - targetMember = await ctx.Guild.GetMemberAsync(targetUser.Id); - } - catch (DSharpPlus.Exceptions.NotFoundException) - { - // blah - } - - if (await Program.db.HashExistsAsync("mutes", targetUser.Id) || (targetMember is not null && (targetMember.Roles.Contains(mutedRole) || targetMember.Roles.Contains(tqsMutedRole)))) - { - await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent($"{Program.cfgjson.Emoji.Error} {ctx.User.Mention}, that user is already muted.")); - return; - } - - // Check if user to be muted is staff or TQS, and disallow if so - if (targetMember != default && (await GetPermLevelAsync(ctx.Member)) == ServerPermLevel.TechnicalQueriesSlayer && ((await GetPermLevelAsync(targetMember)) >= ServerPermLevel.TechnicalQueriesSlayer || targetMember.IsBot)) - { - await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent($"{Program.cfgjson.Emoji.Error} {ctx.User.Mention}, you cannot mute other TQS or staff members.")); - return; - } - - // mute duration is static for TQS mutes - TimeSpan muteDuration = TimeSpan.FromHours(Program.cfgjson.TqsMuteDurationHours); - - await MuteHelpers.MuteUserAsync(targetUser, reason, ctx.User.Id, ctx.Guild, ctx.Channel, muteDuration, true, true); - await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent("Done. Please open a modmail thread for this user if you haven't already!")); - } - } -} diff --git a/Commands/InteractionCommands/RoleInteractions.cs b/Commands/InteractionCommands/RoleInteractions.cs deleted file mode 100644 index 0f6b34e2..00000000 --- a/Commands/InteractionCommands/RoleInteractions.cs +++ /dev/null @@ -1,129 +0,0 @@ -namespace Cliptok.Commands.InteractionCommands -{ - internal class RoleInteractions - { - [Command("grant")] - [Description("Grant a user Tier 1, bypassing any verification requirements.")] - [AllowedProcessors(typeof(SlashCommandProcessor))] - [RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermission.ModerateMembers)] - public async Task SlashGrant(SlashCommandContext ctx, [Parameter("user"), Description("The user to grant Tier 1 to.")] DiscordUser _) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} This command is deprecated and no longer works. Please right click (or tap and hold on mobile) the user and click \"Verify Member\" if available."); - } - - [HomeServer] - [Command("roles")] - [Description("Opt in/out of roles.")] - [AllowedProcessors(typeof(SlashCommandProcessor))] - internal class RoleSlashCommands - { - [Command("grant")] - [Description("Opt into a role.")] - public async Task GrantRole( - SlashCommandContext ctx, - [SlashAutoCompleteProvider(typeof(RolesAutocompleteProvider))] - [Parameter("role"), Description("The role to opt into.")] string role) - { - DiscordMember member = ctx.Member; - - ulong roleId = role switch - { - "insiderCanary" => Program.cfgjson.UserRoles.InsiderCanary, - "insiderDev" => Program.cfgjson.UserRoles.InsiderDev, - "insiderBeta" => Program.cfgjson.UserRoles.InsiderBeta, - "insiderRP" => Program.cfgjson.UserRoles.InsiderRP, - "insider10RP" => Program.cfgjson.UserRoles.Insider10RP, - "patchTuesday" => Program.cfgjson.UserRoles.PatchTuesday, - "giveaways" => Program.cfgjson.UserRoles.Giveaways, - "cts" => Program.cfgjson.CommunityTechSupportRoleID, - _ => 0 - }; - - if (roleId == 0) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Invalid role! Please choose from the list.", ephemeral: true); - return; - } - - if (roleId == Program.cfgjson.CommunityTechSupportRoleID && await GetPermLevelAsync(ctx.Member) < ServerPermLevel.TechnicalQueriesSlayer) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.NoPermissions} You must be a TQS member to get the CTS role!", ephemeral: true); - return; - } - - var roleData = await ctx.Guild.GetRoleAsync(roleId); - - await member.GrantRoleAsync(roleData, $"/roles grant used by {DiscordHelpers.UniqueUsername(ctx.User)}"); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} The role {roleData.Mention} has been successfully granted!", ephemeral: true, mentions: false); - } - - [Command("remove")] - [Description("Opt out of a role.")] - public async Task RemoveRole( - SlashCommandContext ctx, - [SlashAutoCompleteProvider(typeof(RolesAutocompleteProvider))] - [Parameter("role"), Description("The role to opt out of.")] string role) - { - DiscordMember member = ctx.Member; - - ulong roleId = role switch - { - "insiderCanary" => Program.cfgjson.UserRoles.InsiderCanary, - "insiderDev" => Program.cfgjson.UserRoles.InsiderDev, - "insiderBeta" => Program.cfgjson.UserRoles.InsiderBeta, - "insiderRP" => Program.cfgjson.UserRoles.InsiderRP, - "insider10RP" => Program.cfgjson.UserRoles.Insider10RP, - "patchTuesday" => Program.cfgjson.UserRoles.PatchTuesday, - "giveaways" => Program.cfgjson.UserRoles.Giveaways, - "cts" => Program.cfgjson.CommunityTechSupportRoleID, - _ => 0 - }; - - if (roleId == 0) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Invalid role! Please choose from the list.", ephemeral: true); - return; - } - - var roleData = await ctx.Guild.GetRoleAsync(roleId); - - await member.RevokeRoleAsync(roleData, $"/roles remove used by {DiscordHelpers.UniqueUsername(ctx.User)}"); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} The role {roleData.Mention} has been successfully removed!", ephemeral: true, mentions: false); - } - } - - internal class RolesAutocompleteProvider : IAutoCompleteProvider - { - public async ValueTask> AutoCompleteAsync(AutoCompleteContext ctx) - { - Dictionary options = new() - { - { "Windows 11 Insiders (Canary)", "insiderCanary" }, - { "Windows 11 Insiders (Dev)", "insiderDev" }, - { "Windows 11 Insiders (Beta)", "insiderBeta" }, - { "Windows 11 Insiders (Release Preview)", "insiderRP" }, - { "Windows 10 Insiders (Release Preview)", "insider10RP" }, - { "Patch Tuesday", "patchTuesday" }, - { "Giveaways", "giveaways" }, - { "Community Tech Support (CTS)", "cts" } - }; - - var memberHasTqs = await GetPermLevelAsync(ctx.Member) >= ServerPermLevel.TechnicalQueriesSlayer; - - List list = new(); - - foreach (var option in options) - { - var focusedOption = ctx.Options.FirstOrDefault(option => option.Focused); - if (focusedOption.Value.ToString() == "" || option.Key.Contains(focusedOption.Value.ToString(), StringComparison.OrdinalIgnoreCase)) - { - if (option.Value == "cts" && !memberHasTqs) continue; - list.Add(new DiscordAutoCompleteChoice(option.Key, option.Value)); - } - } - - return list; - } - } - } -} diff --git a/Commands/InteractionCommands/TechSupportInteractions.cs b/Commands/InteractionCommands/TechSupportInteractions.cs deleted file mode 100644 index 8d3b1cb9..00000000 --- a/Commands/InteractionCommands/TechSupportInteractions.cs +++ /dev/null @@ -1,52 +0,0 @@ -using Cliptok.Constants; - -namespace Cliptok.Commands.InteractionCommands -{ - public class TechSupportInteractions - { - [Command("vcredist")] - [Description("Outputs download URLs for the specified Visual C++ Redistributables version")] - [AllowedProcessors(typeof(SlashCommandProcessor))] - public async Task RedistsCommand( - SlashCommandContext ctx, - - [SlashChoiceProvider(typeof(VcRedistChoiceProvider))] - [Parameter("version"), Description("Visual Studio version number or year")] long version - ) - { - VcRedist redist = VcRedistConstants.VcRedists - .First((e) => - { - return version == e.Version; - }); - - DiscordEmbedBuilder embed = new DiscordEmbedBuilder() - .WithTitle($"Visual C++ {redist.Year}{(redist.Year == 2015 ? "+" : "")} Redistributables (version {redist.Version})") - .WithFooter("The above links are official and safe to download.") - .WithColor(new("7160e8")); - - foreach (var url in redist.DownloadUrls) - { - embed.AddField($"{url.Key.ToString("G")}", $"{url.Value}"); - } - - await ctx.RespondAsync(null, embed.Build(), false); - } - } - - internal class VcRedistChoiceProvider : IChoiceProvider - { - public async ValueTask> ProvideAsync(CommandParameter _) - { - return new List - { - new("Visual Studio 2015+ - v140", "140"), - new("Visual Studio 2013 - v120", "120"), - new("Visual Studio 2012 - v110", "110"), - new("Visual Studio 2010 - v100", "100"), - new("Visual Studio 2008 - v90", "90"), - new("Visual Studio 2005 - v80", "80") - }; - } - } -} diff --git a/Commands/InteractionCommands/JoinwatchInteractions.cs b/Commands/JoinwatchCmds.cs similarity index 61% rename from Commands/InteractionCommands/JoinwatchInteractions.cs rename to Commands/JoinwatchCmds.cs index 47e3a1a7..46c7d806 100644 --- a/Commands/InteractionCommands/JoinwatchInteractions.cs +++ b/Commands/JoinwatchCmds.cs @@ -1,16 +1,62 @@ -namespace Cliptok.Commands.InteractionCommands +namespace Cliptok.Commands { - internal class JoinwatchInteractions + public class JoinwatchCmds { [Command("joinwatch")] [Description("Watch for joins and leaves of a given user. Output goes to #investigations.")] - [AllowedProcessors(typeof(SlashCommandProcessor))] + [AllowedProcessors(typeof(SlashCommandProcessor), typeof(TextCommandProcessor))] [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public class JoinwatchSlashCmds + public class JoinwatchCmd { + [DefaultGroupCommand] + [Command("toggle")] + [Description("Toggle joinwatch for a given user.")] + [AllowedProcessors(typeof(TextCommandProcessor))] + public async Task JoinwatchToggle(CommandContext ctx, + [Parameter("user"), Description("The user to watch for joins and leaves of.")] DiscordUser user, + [Parameter("note"), Description("An optional note for context.")] string note = "") + { + var joinWatchlist = await Program.db.ListRangeAsync("joinWatchedUsers"); + + if (joinWatchlist.Contains(user.Id)) + { + if (note != "") + { + // User is already joinwatched, just update note + + // Get current note; if it's the same, do nothing + var currentNote = await Program.db.HashGetAsync("joinWatchedUsersNotes", user.Id); + if (currentNote == note || (string.IsNullOrWhiteSpace(currentNote) && string.IsNullOrWhiteSpace(note))) + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {user.Mention} is already being watched with the same note! Nothing to do."); + return; + } + + // If note is different, update it + await Program.db.HashSetAsync("joinWatchedUsersNotes", user.Id, note); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Successfully updated the note for {user.Mention} (run again with no note to unwatch):\n> {note}"); + return; + } + + // User is already joinwatched, unwatch + Program.db.ListRemove("joinWatchedUsers", joinWatchlist.First(x => x == user.Id)); + await Program.db.HashDeleteAsync("joinWatchedUsersNotes", user.Id); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Successfully unwatched {user.Mention}, since they were already in the list."); + } + else + { + // User is not joinwatched, watch + await Program.db.ListRightPushAsync("joinWatchedUsers", user.Id); + if (note != "") + await Program.db.HashSetAsync("joinWatchedUsersNotes", user.Id, note); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Now watching for joins/leaves of {user.Mention} to send to the investigations channel" + + (note == "" ? "!" : $" with the following note:\n>>> {note}")); + } + } + [Command("add")] [Description("Watch for joins and leaves of a given user. Output goes to #investigations.")] - public async Task JoinwatchAdd(SlashCommandContext ctx, + public async Task JoinwatchAdd(CommandContext ctx, [Parameter("user"), Description("The user to watch for joins and leaves of.")] DiscordUser user, [Parameter("note"), Description("An optional note for context.")] string note = "") { @@ -55,7 +101,7 @@ public async Task JoinwatchAdd(SlashCommandContext ctx, [Command("remove")] [Description("Stop watching for joins and leaves of a user.")] - public async Task JoinwatchRemove(SlashCommandContext ctx, + public async Task JoinwatchRemove(CommandContext ctx, [Parameter("user"), Description("The user to stop watching for joins and leaves of.")] DiscordUser user) { var joinWatchlist = await Program.db.ListRangeAsync("joinWatchedUsers"); @@ -74,7 +120,7 @@ public async Task JoinwatchRemove(SlashCommandContext ctx, [Command("status")] [Description("Check the joinwatch status for a user.")] - public async Task JoinwatchStatus(SlashCommandContext ctx, + public async Task JoinwatchStatus(CommandContext ctx, [Parameter("user"), Description("The user whose joinwatch status to check.")] DiscordUser user) { var joinWatchlist = await Program.db.ListRangeAsync("joinWatchedUsers"); @@ -95,5 +141,4 @@ public async Task JoinwatchStatus(SlashCommandContext ctx, } } } - } \ No newline at end of file diff --git a/Commands/Kick.cs b/Commands/KickCmds.cs similarity index 85% rename from Commands/Kick.cs rename to Commands/KickCmds.cs index 2b0a46e2..e125183e 100644 --- a/Commands/Kick.cs +++ b/Commands/KickCmds.cs @@ -1,13 +1,13 @@ -namespace Cliptok.Commands +namespace Cliptok.Commands { - internal class Kick + public class KickCmds { - [Command("kicktextcmd")] - [TextAlias("kick", "yeet", "shoo", "goaway", "defenestrate")] - [Description("Kicks a user, removing them from the server until they rejoin. Generally not very useful.")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [RequirePermissions(permissions: DiscordPermission.KickMembers), HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] - public async Task KickCmd(TextCommandContext ctx, DiscordUser target, [RemainingText] string reason = "No reason specified.") + [Command("kick")] + [TextAlias("yeet", "shoo", "goaway", "defenestrate")] + [Description("Kicks a user, removing them from the server until they rejoin.")] + [AllowedProcessors(typeof(SlashCommandProcessor), typeof(TextCommandProcessor))] + [RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermission.KickMembers)] + public async Task KickCmd(CommandContext ctx, [Parameter("user"), Description("The user you want to kick from the server.")] DiscordUser target, [Parameter("reason"), Description("The reason for kicking this user.")] string reason = "No reason specified.") { if (target.IsBot) { @@ -32,20 +32,20 @@ public async Task KickCmd(TextCommandContext ctx, DiscordUser target, [Remaining { if (DiscordHelpers.AllowedToMod(await ctx.Guild.GetMemberAsync(ctx.Client.CurrentUser.Id), member)) { - await ctx.Message.DeleteAsync(); await KickAndLogAsync(member, reason, ctx.Member); await ctx.Channel.SendMessageAsync($"{Program.cfgjson.Emoji.Ejected} {target.Mention} has been kicked: **{reason}**"); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Done!", ephemeral: true); return; } else { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} I don't have permission to kick **{DiscordHelpers.UniqueUsername(target)}**!"); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} I don't have permission to kick **{DiscordHelpers.UniqueUsername(target)}**!", ephemeral: true); return; } } else { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} You aren't allowed to kick **{DiscordHelpers.UniqueUsername(target)}**!"); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} You aren't allowed to kick **{DiscordHelpers.UniqueUsername(target)}**!", ephemeral: true); return; } } @@ -128,6 +128,5 @@ await LogChannelHelper.LogMessageAsync("mod", return false; } } - } -} +} \ No newline at end of file diff --git a/Commands/Lists.cs b/Commands/ListCmds.cs similarity index 72% rename from Commands/Lists.cs rename to Commands/ListCmds.cs index 537f40dd..3ebd4909 100644 --- a/Commands/Lists.cs +++ b/Commands/ListCmds.cs @@ -1,6 +1,6 @@ namespace Cliptok.Commands { - internal class Lists + internal class ListCmds { public class GitHubDispatchBody { @@ -137,12 +137,11 @@ await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} An error with code `{resp $"Body: ```json\n{responseText}```"); } - [Command("scamchecktextcmd")] - [TextAlias("scamcheck")] + [Command("scamcheck")] [Description("Check if a link or message is known to the anti-phishing API.")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public async Task ScamCheck(TextCommandContext ctx, [RemainingText, Description("Domain or message content to scan.")] string content) + [AllowedProcessors(typeof(SlashCommandProcessor), typeof(TextCommandProcessor))] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermission.ModerateMembers)] + public async Task ScamCheck(CommandContext ctx, [Parameter("input"), Description("Domain or message content to scan.")] string content) { var urlMatches = Constants.RegexConstants.url_rx.Matches(content); if (urlMatches.Count > 0 && Environment.GetEnvironmentVariable("CLIPTOK_ANTIPHISHING_ENDPOINT") is not null && Environment.GetEnvironmentVariable("CLIPTOK_ANTIPHISHING_ENDPOINT") != "useyourimagination") @@ -152,8 +151,7 @@ public async Task ScamCheck(TextCommandContext ctx, [RemainingText, Description( string responseToSend; if (match) { - responseToSend = $"Match found:\n```json\n{responseText}\n"; - + responseToSend = $"Match found:\n"; } else { @@ -170,55 +168,6 @@ public async Task ScamCheck(TextCommandContext ctx, [RemainingText, Description( } } - [Command("joinwatchtextcmd")] - [TextAlias("joinwatch", "joinnotify", "leavewatch", "leavenotify")] - [Description("Watch for joins and leaves of a given user. Output goes to #investigations.")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public async Task JoinWatch( - TextCommandContext ctx, - [Description("The user to watch for joins and leaves of.")] DiscordUser user, - [Description("An optional note for context."), RemainingText] string note = "" - ) - { - var joinWatchlist = await Program.db.ListRangeAsync("joinWatchedUsers"); - - if (joinWatchlist.Contains(user.Id)) - { - if (note != "") - { - // User is already joinwatched, just update note - - // Get current note; if it's the same, do nothing - var currentNote = await Program.db.HashGetAsync("joinWatchedUsersNotes", user.Id); - if (currentNote == note || (string.IsNullOrWhiteSpace(currentNote) && string.IsNullOrWhiteSpace(note))) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {user.Mention} is already being watched with the same note! Nothing to do."); - return; - } - - // If note is different, update it - await Program.db.HashSetAsync("joinWatchedUsersNotes", user.Id, note); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Successfully updated the note for {user.Mention} (run again with no note to unwatch):\n> {note}"); - return; - } - - // User is already joinwatched, unwatch - Program.db.ListRemove("joinWatchedUsers", joinWatchlist.First(x => x == user.Id)); - await Program.db.HashDeleteAsync("joinWatchedUsersNotes", user.Id); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Successfully unwatched {user.Mention}, since they were already in the list."); - } - else - { - // User is not joinwatched, watch - await Program.db.ListRightPushAsync("joinWatchedUsers", user.Id); - if (note != "") - await Program.db.HashSetAsync("joinWatchedUsersNotes", user.Id, note); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Now watching for joins/leaves of {user.Mention} to send to the investigations channel" - + (note == "" ? "!" : $" with the following note:\n>>> {note}")); - } - } - [Command("appealblocktextcmd")] [TextAlias("appealblock", "superduperban", "ablock")] [Description("Prevents a user from submitting ban appeals.")] diff --git a/Commands/Lockdown.cs b/Commands/Lockdown.cs deleted file mode 100644 index a1680356..00000000 --- a/Commands/Lockdown.cs +++ /dev/null @@ -1,144 +0,0 @@ -namespace Cliptok.Commands -{ - class Lockdown - { - public bool ongoingLockdown = false; - - [Command("lockdowntextcmd")] - [TextAlias("lockdown", "lock")] - [Description("Locks the current channel, preventing any new messages. See also: unlock")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions([DiscordPermission.ManageChannels], [])] - public async Task LockdownCommand( - TextCommandContext ctx, - [RemainingText, Description("The time and reason for the lockdown. For example '3h' or '3h spam'. Default is permanent with no reason.")] string timeAndReason = "" - ) - { - if (ctx.Channel.Type is DiscordChannelType.PublicThread or DiscordChannelType.PrivateThread or DiscordChannelType.NewsThread) - { - var thread = (DiscordThreadChannel)ctx.Channel; - - await thread.ModifyAsync(a => - { - a.IsArchived = true; - a.Locked = true; - }); - return; - } - - bool timeParsed = false; - TimeSpan? lockDuration = null; - string reason = ""; - - if (timeAndReason != "") - { - string possibleTime = timeAndReason.Split(' ').First(); - try - { - lockDuration = HumanDateParser.HumanDateParser.Parse(possibleTime).Subtract(ctx.Message.Timestamp.DateTime); - timeParsed = true; - } - catch - { - // keep null - } - - reason = timeAndReason; - - if (timeParsed) - { - int i = reason.IndexOf(" ") + 1; - - if (i == 0) - reason = ""; - else - reason = reason[i..]; - } - } - - var currentChannel = ctx.Channel; - if (!Program.cfgjson.LockdownEnabledChannels.Contains(currentChannel.Id)) - { - await ctx.Channel.SendMessageAsync($"{Program.cfgjson.Emoji.Denied} You can't lock or unlock this channel!\nIf this is in error, add its ID (`{currentChannel.Id}`) to the lockdown whitelist."); - return; - } - - if (ongoingLockdown) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} A mass lockdown or unlock is already ongoing. Refusing your request to avoid conflicts, sorry."); - return; - } - - if (timeAndReason == "all") - { - ongoingLockdown = true; - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it, please hold..."); - foreach (var chanID in Program.cfgjson.LockdownEnabledChannels) - { - try - { - var channel = await ctx.Client.GetChannelAsync(chanID); - await LockdownHelpers.LockChannelAsync(user: ctx.User, channel: channel); - } - catch - { - - } - - } - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Done!"); - ongoingLockdown = false; - return; - - } - - await ctx.Message.DeleteAsync(); - - await LockdownHelpers.LockChannelAsync(user: ctx.User, channel: currentChannel, duration: lockDuration, reason: reason); - } - - [Command("unlocktextcmd")] - [TextAlias("unlock", "unlockdown")] - [Description("Unlocks a previously locked channel. See also: lockdown")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions([DiscordPermission.ManageChannels], [])] - public async Task UnlockCommand(TextCommandContext ctx, [RemainingText] string reason = "") - { - var currentChannel = ctx.Channel; - if (!Program.cfgjson.LockdownEnabledChannels.Contains(currentChannel.Id)) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Denied} You can't lock or unlock this channel!\nIf this is in error, add its ID (`{currentChannel.Id}`) to the lockdown whitelist."); - return; - } - - if (ongoingLockdown) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} A mass lockdown or unlock is already ongoing. Refusing your request. sorry."); - return; - } - - if (reason == "all") - { - ongoingLockdown = true; - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it, please hold..."); - foreach (var chanID in Program.cfgjson.LockdownEnabledChannels) - { - try - { - currentChannel = await ctx.Client.GetChannelAsync(chanID); - await LockdownHelpers.UnlockChannel(currentChannel, ctx.Member); - } - catch - { - - } - } - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Done!"); - ongoingLockdown = false; - return; - } - await LockdownHelpers.UnlockChannel(currentChannel, ctx.Member, reason); - } - - } -} diff --git a/Commands/LockdownCmds.cs b/Commands/LockdownCmds.cs new file mode 100644 index 00000000..4d908df4 --- /dev/null +++ b/Commands/LockdownCmds.cs @@ -0,0 +1,217 @@ +namespace Cliptok.Commands +{ + public class LockdownCmds + { + public static bool ongoingLockdown = false; + + [Command("lockdown")] + [Description("Lock the current channel or all channels in the server, preventing new messages. See also: unlock")] + [TextAlias("lock")] + [AllowedProcessors(typeof(SlashCommandProcessor), typeof(TextCommandProcessor))] + [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions([DiscordPermission.ManageChannels], [])] + public class LockdownCmd + { + [DefaultGroupCommand] + [Command("channel")] + [Description("Lock the current channel. See also: unlock channel")] + public async Task LockdownChannelCommand( + CommandContext ctx, + [Parameter("reason"), Description("The reason for the lockdown.")] string reason = "No reason specified.", + [Parameter("time"), Description("The length of time to lock the channel for.")] string time = null, + [Parameter("lockthreads"), Description("Whether to lock this channel's threads. Disables sending messages, but does not archive them.")] bool lockThreads = false) + { + if (ctx is SlashCommandContext) + await ctx.As().DeferResponseAsync(ephemeral: true); + + if (ctx.Channel.Type is DiscordChannelType.PublicThread or DiscordChannelType.PrivateThread or DiscordChannelType.NewsThread) + { + if (lockThreads) + { + if (ctx is SlashCommandContext) + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Denied} You can't lock this channel!\n`/lockdown` with `lockthreads` cannot be used inside of a thread. If you meant to lock {ctx.Channel.Parent.Mention} and all of its threads, use the command there.\n\nIf you meant to only lock this thread, use `!lock` instead, or use `/lockdown` with `lockthreads` set to False.").AsEphemeral(true)); + else + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Denied} You can't lock this channel!\n`/lockdown` with `lockthreads` cannot be used inside of a thread. If you meant to lock {ctx.Channel.Parent.Mention} and all of its threads, use the command there.\n\nIf you meant to only lock this thread, use `!lock` instead, or use `/lockdown` with `lockthreads` set to False."); + return; + } + + var thread = (DiscordThreadChannel)ctx.Channel; + + await thread.ModifyAsync(a => + { + a.IsArchived = true; + a.Locked = true; + }); + + if (ctx is SlashCommandContext) + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Thread locked successfully!").AsEphemeral(true)); + return; + } + + TimeSpan? lockDuration = null; + + if (!string.IsNullOrWhiteSpace(time)) + { + if (ctx is SlashCommandContext) + lockDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(ctx.As().Interaction.CreationTimestamp.DateTime); + else + lockDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(ctx.As().Message.Timestamp.DateTime); + } + + var currentChannel = ctx.Channel; + if (!Program.cfgjson.LockdownEnabledChannels.Contains(currentChannel.Id)) + { + if (ctx is SlashCommandContext) + ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Denied} You can't lock or unlock this channel!\nIf this is in error, add its ID (`{currentChannel.Id}`) to the lockdown whitelist.")); + else + ctx.RespondAsync($"{Program.cfgjson.Emoji.Denied} You can't lock or unlock this channel!\nIf this is in error, add its ID (`{currentChannel.Id}`) to the lockdown whitelist."); + return; + } + + if (ongoingLockdown) + { + if (ctx is SlashCommandContext) + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} A mass lockdown or unlock is already ongoing. Refusing your request to avoid conflicts, sorry.")); + else + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} A mass lockdown or unlock is already ongoing. Refusing your request to avoid conflicts, sorry."); + return; + } + + if (ctx is TextCommandContext) + await ctx.As().Message.DeleteAsync(); + + bool success = await LockdownHelpers.LockChannelAsync(user: ctx.User, channel: currentChannel, duration: lockDuration, reason: reason, lockThreads: lockThreads); + if (ctx is SlashCommandContext) + { + if (success) + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Channel locked successfully.").AsEphemeral(true)); + else + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Failed to lock this channel!").AsEphemeral(true)); + } + } + + [Command("all")] + [Description("Lock all lockable channels in the server. See also: unlock all")] + public async Task LockdownAllCommand( + CommandContext ctx, + [Parameter("reason"), Description("The reason for the lockdown.")] string reason = "", + [Parameter("time"), Description("The length of time to lock the channels for.")] string time = null, + [Parameter("lockthreads"), Description("Whether to lock threads. Disables sending messages, but does not archive them.")] bool lockThreads = false) + { + if (ctx is SlashCommandContext) + await ctx.DeferResponseAsync(); + + ongoingLockdown = true; + if (ctx is SlashCommandContext) + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Loading} Working on it, please hold...")); + else + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it, please hold..."); + + TimeSpan? lockDuration = null; + + if (!string.IsNullOrWhiteSpace(time)) + { + if (ctx is SlashCommandContext) + lockDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(ctx.As().Interaction.CreationTimestamp.DateTime); + else + lockDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(ctx.As().Message.Timestamp.DateTime); + } + + foreach (var chanID in Program.cfgjson.LockdownEnabledChannels) + { + try + { + var channel = await ctx.Client.GetChannelAsync(chanID); + await LockdownHelpers.LockChannelAsync(user: ctx.User, channel: channel, duration: lockDuration, reason: reason, lockThreads: lockThreads); + } + catch + { + + } + + } + if (ctx is SlashCommandContext) + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Success} Done!")); + else + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Done!"); + ongoingLockdown = false; + return; + } + } + + [Command("unlock")] + [TextAlias("unlockdown")] + [Description("Unlock the current channel or all channels in the server, allowing new messages. See also: lockdown")] + [AllowedProcessors(typeof(SlashCommandProcessor), typeof(TextCommandProcessor))] + [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions([DiscordPermission.ManageChannels], [])] + public class UnlockCmds + { + [DefaultGroupCommand] + [Command("channel")] + [Description("Unlock the current channel. See also: lockdown")] + public async Task UnlockChannelCommand(CommandContext ctx, [Parameter("reason"), Description("The reason for the unlock.")] string reason = "") + { + if (ctx is SlashCommandContext) + await ctx.As().DeferResponseAsync(ephemeral: true); + + var currentChannel = ctx.Channel; + if (!Program.cfgjson.LockdownEnabledChannels.Contains(currentChannel.Id)) + { + if (ctx is SlashCommandContext) + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Denied} You can't lock or unlock this channel!\nIf this is in error, add its ID (`{currentChannel.Id}`) to the lockdown whitelist.").AsEphemeral(true)); + else + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Denied} You can't lock or unlock this channel!\nIf this is in error, add its ID (`{currentChannel.Id}`) to the lockdown whitelist."); + return; + } + + if (ongoingLockdown) + { + if (ctx is SlashCommandContext) + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} A mass lockdown or unlock is already ongoing. Refusing your request. sorry.").AsEphemeral(true)); + else + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} A mass lockdown or unlock is already ongoing. Refusing your request. sorry."); + return; + } + bool success = await LockdownHelpers.UnlockChannel(currentChannel, ctx.Member); + if (ctx is SlashCommandContext) + { + if (success) + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Channel unlocked successfully.").AsEphemeral(true)); + else + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Failed to unlock this channel!").AsEphemeral(true)); + } + } + + [Command("all")] + [Description("Unlock all lockable channels in the server. See also: lockdown all")] + public async Task UnlockAllCommand(CommandContext ctx, [Parameter("reason"), Description("The reason for the unlock.")] string reason = "") + { + if (ctx is SlashCommandContext) + ctx.DeferResponseAsync(); + + ongoingLockdown = true; + if (ctx is SlashCommandContext) + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Loading} Working on it, please hold...")); + else + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it, please hold..."); + foreach (var chanID in Program.cfgjson.LockdownEnabledChannels) + { + try + { + var currentChannel = await ctx.Client.GetChannelAsync(chanID); + await LockdownHelpers.UnlockChannel(currentChannel, ctx.Member, reason, true); + } + catch + { + + } + } + if (ctx is SlashCommandContext) + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Success} Done!")); + else + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Done!"); + ongoingLockdown = false; + return; + } + } + } +} \ No newline at end of file diff --git a/Commands/MuteCmds.cs b/Commands/MuteCmds.cs new file mode 100644 index 00000000..0d87864b --- /dev/null +++ b/Commands/MuteCmds.cs @@ -0,0 +1,290 @@ +namespace Cliptok.Commands +{ + public class MuteCmds + { + [Command("mute")] + [Description("Mute a user, temporarily or permanently.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] + [RequirePermissions(DiscordPermission.ModerateMembers)] + public async Task MuteSlashCommand( + SlashCommandContext ctx, + [Parameter("user"), Description("The user you wish to mute.")] DiscordUser targetUser, + [Parameter("reason"), Description("The reason for the mute.")] string reason, + [Parameter("time"), Description("The length of time to mute for.")] string time = "" + ) + { + await ctx.DeferResponseAsync(ephemeral: true); + DiscordMember targetMember = default; + try + { + targetMember = await ctx.Guild.GetMemberAsync(targetUser.Id); + } + catch (DSharpPlus.Exceptions.NotFoundException) + { + // is this worth logging? + } + + if (targetMember != default && (await GetPermLevelAsync(ctx.Member)) == ServerPermLevel.TrialModerator && ((await GetPermLevelAsync(targetMember)) >= ServerPermLevel.TrialModerator || targetMember.IsBot)) + { + await ctx.Channel.SendMessageAsync($"{Program.cfgjson.Emoji.Error} {ctx.User.Mention}, as a Trial Moderator you cannot perform moderation actions on other staff members or bots."); + return; + } + + TimeSpan muteDuration = default; + + if (time != "") + { + try + { + muteDuration = HumanDateParser.HumanDateParser.Parse(time).Subtract(ctx.Interaction.CreationTimestamp.LocalDateTime); + } + catch + { + await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent($"{Program.cfgjson.Emoji.Error} Failed to parse time argument.")); + throw; + } + } + + await MuteHelpers.MuteUserAsync(targetUser, reason, ctx.User.Id, ctx.Guild, ctx.Channel, muteDuration, true); + await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent("Command completed successfully.")); + } + + [Command("unmute")] + [Description("Unmute a user.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] + [RequirePermissions(DiscordPermission.ModerateMembers)] + public async Task UnmuteSlashCommand( + SlashCommandContext ctx, + [Parameter("user"), Description("The user you wish to mute.")] DiscordUser targetUser, + [Parameter("reason"), Description("The reason for the unmute.")] string reason = "No reason specified." + ) + { + await ctx.DeferResponseAsync(ephemeral: false); + + reason = $"[Manual unmute by {DiscordHelpers.UniqueUsername(ctx.User)}]: {reason}"; + + // todo: store per-guild + DiscordRole mutedRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.MutedRole); + + DiscordMember member = default; + try + { + member = await ctx.Guild.GetMemberAsync(targetUser.Id); + } + catch (DSharpPlus.Exceptions.NotFoundException ex) + { + Program.discord.Logger.LogWarning(eventId: Program.CliptokEventID, exception: ex, message: "Failed to unmute {user} in {server} because they weren't in the server.", $"{DiscordHelpers.UniqueUsername(targetUser)}", ctx.Guild.Name); + } + + if ((await Program.db.HashExistsAsync("mutes", targetUser.Id)) || (member != default && member.Roles.Contains(mutedRole))) + { + await MuteHelpers.UnmuteUserAsync(targetUser, reason, true, ctx.User); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Information} Successfully unmuted **{DiscordHelpers.UniqueUsername(targetUser)}**.")); + } + else + try + { + await MuteHelpers.UnmuteUserAsync(targetUser, reason, true, ctx.User); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Warning} According to Discord that user is not muted, but I tried to unmute them anyway. Hope it works.")); + } + catch (Exception e) + { + Program.discord.Logger.LogError(e, "An error occurred unmuting {user}", targetUser.Id); + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Program.cfgjson.Emoji.Error} That user doesn't appear to be muted, *and* an error occurred while attempting to unmute them anyway. Please contact the bot owner, the error has been logged.")); + } + } + + [Command("tqsmute")] + [Description("Temporarily mute a user in tech support channels.")] + [AllowedProcessors(typeof(SlashCommandProcessor), typeof(TextCommandProcessor))] + [RequireHomeserverPerm(ServerPermLevel.TechnicalQueriesSlayer)] + public async Task TqsMuteSlashCommand( + CommandContext ctx, + [Parameter("user"), Description("The user to mute.")] DiscordUser targetUser, + [Parameter("reason"), Description("The reason for the mute.")] string reason) + { + if (ctx is SlashCommandContext) + await ctx.As().DeferResponseAsync(ephemeral: true); + else + await ctx.As().Message.DeleteAsync(); + + // only work if TQS mute role is configured + if (Program.cfgjson.TqsMutedRole == 0) + { + if (ctx is SlashCommandContext) + await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent($"{Program.cfgjson.Emoji.Error} TQS mutes are not configured, so this command does nothing. Please contact the bot maintainer if this is unexpected.")); + else + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} TQS mutes are not configured, so this command does nothing. Please contact the bot maintainer if this is unexpected."); + return; + } + + // Only allow usage in #tech-support, #tech-support-forum, and their threads + if (ctx.Channel.Id != Program.cfgjson.TechSupportChannel && + ctx.Channel.Id != Program.cfgjson.SupportForumId && + ctx.Channel.Parent.Id != Program.cfgjson.TechSupportChannel && + ctx.Channel.Parent.Id != Program.cfgjson.SupportForumId) + { + if (ctx is SlashCommandContext) + await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent($"{Program.cfgjson.Emoji.Error} This command can only be used in <#{Program.cfgjson.TechSupportChannel}>, <#{Program.cfgjson.SupportForumId}>, and threads in those channels!")); + else + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} This command can only be used in <#{Program.cfgjson.TechSupportChannel}>, <#{Program.cfgjson.SupportForumId}>, and threads in those channels!"); + return; + } + + // Check if the user is already muted; disallow TQS-mute if so + + DiscordRole mutedRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.MutedRole); + DiscordRole tqsMutedRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.TqsMutedRole); + + // Get member + DiscordMember targetMember = default; + try + { + targetMember = await ctx.Guild.GetMemberAsync(targetUser.Id); + } + catch (DSharpPlus.Exceptions.NotFoundException) + { + // blah + } + + if (await Program.db.HashExistsAsync("mutes", targetUser.Id) || (targetMember is not null && (targetMember.Roles.Contains(mutedRole) || targetMember.Roles.Contains(tqsMutedRole)))) + { + if (ctx is SlashCommandContext) + await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent($"{Program.cfgjson.Emoji.Error} {ctx.User.Mention}, that user is already muted.")); + else + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {ctx.User.Mention}, that user is already muted."); + return; + } + + // Check if user to be muted is staff or TQS, and disallow if so + if (targetMember != default && (await GetPermLevelAsync(ctx.Member)) == ServerPermLevel.TechnicalQueriesSlayer && ((await GetPermLevelAsync(targetMember)) >= ServerPermLevel.TechnicalQueriesSlayer || targetMember.IsBot)) + { + if (ctx is SlashCommandContext) + await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent($"{Program.cfgjson.Emoji.Error} {ctx.User.Mention}, you cannot mute other TQS or staff members.")); + else + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {ctx.User.Mention}, you cannot mute other TQS or staff members."); + return; + } + + // mute duration is static for TQS mutes + TimeSpan muteDuration = TimeSpan.FromHours(Program.cfgjson.TqsMuteDurationHours); + + await MuteHelpers.MuteUserAsync(targetUser, reason, ctx.User.Id, ctx.Guild, ctx.Channel, muteDuration, true, true); + if (ctx is SlashCommandContext) + await ctx.EditResponseAsync(new DiscordWebhookBuilder().WithContent("Done. Please open a modmail thread for this user if you haven't already!")); + } + + [Command("muteinfo")] + [Description("Show information about the mute for a user.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] + [RequirePermissions(DiscordPermission.ModerateMembers)] + public async Task MuteInfoSlashCommand( + SlashCommandContext ctx, + [Parameter("user"), Description("The user whose mute information to show.")] DiscordUser targetUser, + [Parameter("public"), Description("Whether to show the output publicly. Default: false")] bool isPublic = false) + { + await ctx.RespondAsync(embed: await MuteHelpers.MuteStatusEmbed(targetUser, ctx.Guild), ephemeral: !isPublic); + } + + [Command("unmutetextcmd")] + [TextAlias("unmute", "umute")] + [Description("Unmutes a previously muted user, typically ahead of the standard expiration time. See also: mute")] + [AllowedProcessors(typeof(TextCommandProcessor))] + [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] + public async Task UnmuteCmd(TextCommandContext ctx, [Description("The user you're trying to unmute.")] DiscordUser targetUser, string reason = "No reason provided.") + { + reason = $"[Manual unmute by {DiscordHelpers.UniqueUsername(ctx.User)}]: {reason}"; + + // todo: store per-guild + DiscordRole mutedRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.MutedRole); + DiscordRole tqsMutedRole = default; + if (Program.cfgjson.TqsMutedRole != 0) + tqsMutedRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.TqsMutedRole); + + DiscordMember member = default; + try + { + member = await ctx.Guild.GetMemberAsync(targetUser.Id); + } + catch (DSharpPlus.Exceptions.NotFoundException ex) + { + Program.discord.Logger.LogWarning(eventId: Program.CliptokEventID, exception: ex, message: "Failed to unmute {user} in {server} because they weren't in the server.", $"{DiscordHelpers.UniqueUsername(targetUser)}", ctx.Guild.Name); + } + + if ((await Program.db.HashExistsAsync("mutes", targetUser.Id)) || (member != default && (member.Roles.Contains(mutedRole) || member.Roles.Contains(tqsMutedRole)))) + { + await MuteHelpers.UnmuteUserAsync(targetUser, reason, true, ctx.User); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Information} Successfully unmuted **{DiscordHelpers.UniqueUsername(targetUser)}**."); + } + else + try + { + await MuteHelpers.UnmuteUserAsync(targetUser, reason, true, ctx.User); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Warning} According to Discord that user is not muted, but I tried to unmute them anyway. Hope it works."); + } + catch (Exception e) + { + Program.discord.Logger.LogError(e, "An error occurred unmuting {user}", targetUser.Id); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} That user doesn't appear to be muted, *and* an error occurred while attempting to unmute them anyway. Please contact the bot owner, the error has been logged."); + } + } + + [Command("mutetextcmd")] + [TextAlias("mute")] + [Description("Mutes a user, preventing them from sending messages until they're unmuted. See also: unmute")] + [AllowedProcessors(typeof(TextCommandProcessor))] + [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] + public async Task MuteCmd( + TextCommandContext ctx, [Description("The user you're trying to mute")] DiscordUser targetUser, + [RemainingText, Description("Combined argument for the time and reason for the mute. For example '1h rule 7' or 'rule 10'")] string timeAndReason = "No reason specified." + ) + { + DiscordMember targetMember = default; + try + { + targetMember = await ctx.Guild.GetMemberAsync(targetUser.Id); + } + catch (DSharpPlus.Exceptions.NotFoundException) + { + // is this worth logging? + } + + if (targetMember != default && ((await GetPermLevelAsync(ctx.Member))) == ServerPermLevel.TrialModerator && ((await GetPermLevelAsync(targetMember)) >= ServerPermLevel.TrialModerator || targetMember.IsBot)) + { + await ctx.Channel.SendMessageAsync($"{Program.cfgjson.Emoji.Error} {ctx.User.Mention}, as a Trial Moderator you cannot perform moderation actions on other staff members or bots."); + return; + } + + await ctx.Message.DeleteAsync(); + bool timeParsed = false; + + TimeSpan muteDuration = default; + string possibleTime = timeAndReason.Split(' ').First(); + string reason = timeAndReason; + + try + { + muteDuration = HumanDateParser.HumanDateParser.Parse(possibleTime).Subtract(ctx.Message.Timestamp.DateTime); + timeParsed = true; + } + catch + { + // keep default + } + + if (timeParsed) + { + int i = reason.IndexOf(" ") + 1; + reason = reason[i..]; + } + + if (timeParsed && possibleTime == reason) + reason = "No reason specified."; + + _ = MuteHelpers.MuteUserAsync(targetUser, reason, ctx.User.Id, ctx.Guild, ctx.Channel, muteDuration, true); + } + } +} \ No newline at end of file diff --git a/Commands/Mutes.cs b/Commands/Mutes.cs deleted file mode 100644 index 18f46399..00000000 --- a/Commands/Mutes.cs +++ /dev/null @@ -1,165 +0,0 @@ -namespace Cliptok.Commands -{ - internal class Mutes - { - [Command("unmutetextcmd")] - [TextAlias("unmute", "umute")] - [Description("Unmutes a previously muted user, typically ahead of the standard expiration time. See also: mute")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public async Task UnmuteCmd(TextCommandContext ctx, [Description("The user you're trying to unmute.")] DiscordUser targetUser, string reason = "No reason provided.") - { - reason = $"[Manual unmute by {DiscordHelpers.UniqueUsername(ctx.User)}]: {reason}"; - - // todo: store per-guild - DiscordRole mutedRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.MutedRole); - DiscordRole tqsMutedRole = default; - if (Program.cfgjson.TqsMutedRole != 0) - tqsMutedRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.TqsMutedRole); - - DiscordMember member = default; - try - { - member = await ctx.Guild.GetMemberAsync(targetUser.Id); - } - catch (DSharpPlus.Exceptions.NotFoundException ex) - { - Program.discord.Logger.LogWarning(eventId: Program.CliptokEventID, exception: ex, message: "Failed to unmute {user} in {server} because they weren't in the server.", $"{DiscordHelpers.UniqueUsername(targetUser)}", ctx.Guild.Name); - } - - if ((await Program.db.HashExistsAsync("mutes", targetUser.Id)) || (member != default && (member.Roles.Contains(mutedRole) || member.Roles.Contains(tqsMutedRole)))) - { - await MuteHelpers.UnmuteUserAsync(targetUser, reason, true, ctx.User); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Information} Successfully unmuted **{DiscordHelpers.UniqueUsername(targetUser)}**."); - } - else - try - { - await MuteHelpers.UnmuteUserAsync(targetUser, reason, true, ctx.User); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Warning} According to Discord that user is not muted, but I tried to unmute them anyway. Hope it works."); - } - catch (Exception e) - { - Program.discord.Logger.LogError(e, "An error occurred unmuting {user}", targetUser.Id); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} That user doesn't appear to be muted, *and* an error occurred while attempting to unmute them anyway. Please contact the bot owner, the error has been logged."); - } - } - - [Command("mutetextcmd")] - [TextAlias("mute")] - [Description("Mutes a user, preventing them from sending messages until they're unmuted. See also: unmute")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public async Task MuteCmd( - TextCommandContext ctx, [Description("The user you're trying to mute")] DiscordUser targetUser, - [RemainingText, Description("Combined argument for the time and reason for the mute. For example '1h rule 7' or 'rule 10'")] string timeAndReason = "No reason specified." - ) - { - DiscordMember targetMember = default; - try - { - targetMember = await ctx.Guild.GetMemberAsync(targetUser.Id); - } - catch (DSharpPlus.Exceptions.NotFoundException) - { - // is this worth logging? - } - - if (targetMember != default && ((await GetPermLevelAsync(ctx.Member))) == ServerPermLevel.TrialModerator && ((await GetPermLevelAsync(targetMember)) >= ServerPermLevel.TrialModerator || targetMember.IsBot)) - { - await ctx.Channel.SendMessageAsync($"{Program.cfgjson.Emoji.Error} {ctx.User.Mention}, as a Trial Moderator you cannot perform moderation actions on other staff members or bots."); - return; - } - - await ctx.Message.DeleteAsync(); - bool timeParsed = false; - - TimeSpan muteDuration = default; - string possibleTime = timeAndReason.Split(' ').First(); - string reason = timeAndReason; - - try - { - muteDuration = HumanDateParser.HumanDateParser.Parse(possibleTime).Subtract(ctx.Message.Timestamp.DateTime); - timeParsed = true; - } - catch - { - // keep default - } - - if (timeParsed) - { - int i = reason.IndexOf(" ") + 1; - reason = reason[i..]; - } - - if (timeParsed && possibleTime == reason) - reason = "No reason specified."; - - _ = MuteHelpers.MuteUserAsync(targetUser, reason, ctx.User.Id, ctx.Guild, ctx.Channel, muteDuration, true); - } - - [Command("tqsmutetextcmd")] - [TextAlias("tqsmute")] - [Description("Temporarily mutes a user, preventing them from sending messages in #tech-support and related channels until they're unmuted.")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.TechnicalQueriesSlayer)] - public async Task TqsMuteCmd( - TextCommandContext ctx, [Description("The user to mute")] DiscordUser targetUser, - [RemainingText, Description("The reason for the mute")] string reason = "No reason specified.") - { - if (Program.cfgjson.TqsMutedRole == 0) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} TQS mutes are not configured, so this command does nothing. Please contact the bot maintainer if this is unexpected."); - return; - } - - // Only allow usage in #tech-support, #tech-support-forum, and their threads - if (ctx.Channel.Id != Program.cfgjson.TechSupportChannel && - ctx.Channel.Id != Program.cfgjson.SupportForumId && - ctx.Channel.Parent.Id != Program.cfgjson.TechSupportChannel && - ctx.Channel.Parent.Id != Program.cfgjson.SupportForumId) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} This command can only be used in <#{Program.cfgjson.TechSupportChannel}>, <#{Program.cfgjson.SupportForumId}>, and threads in those channels!"); - return; - } - - // Check if the user is already muted; disallow TQS-mute if so - - DiscordRole mutedRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.MutedRole); - DiscordRole tqsMutedRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.TqsMutedRole); - - // Get member - DiscordMember targetMember = default; - try - { - targetMember = await ctx.Guild.GetMemberAsync(targetUser.Id); - } - catch (DSharpPlus.Exceptions.NotFoundException) - { - // blah - } - - if (await Program.db.HashExistsAsync("mutes", targetUser.Id) || (targetMember != default && (targetMember.Roles.Contains(mutedRole) || targetMember.Roles.Contains(tqsMutedRole)))) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {ctx.User.Mention}, that user is already muted."); - return; - } - - // Check if user to be muted is staff or TQS, and disallow if so - if (targetMember != default && (await GetPermLevelAsync(ctx.Member)) == ServerPermLevel.TechnicalQueriesSlayer && ((await GetPermLevelAsync(targetMember)) >= ServerPermLevel.TechnicalQueriesSlayer || targetMember.IsBot)) - { - await ctx.Channel.SendMessageAsync($"{Program.cfgjson.Emoji.Error} {ctx.User.Mention}, you cannot mute other TQS or staff members."); - return; - } - - await ctx.Message.DeleteAsync(); - - // mute duration is static for TQS mutes - TimeSpan muteDuration = TimeSpan.FromHours(Program.cfgjson.TqsMuteDurationHours); - - MuteHelpers.MuteUserAsync(targetUser, reason, ctx.User.Id, ctx.Guild, ctx.Channel, muteDuration, true, true); - } - } -} diff --git a/Commands/InteractionCommands/NicknameLockInteraction.cs b/Commands/NicknameLockCmds.cs similarity index 95% rename from Commands/InteractionCommands/NicknameLockInteraction.cs rename to Commands/NicknameLockCmds.cs index 75834187..cc75110a 100644 --- a/Commands/InteractionCommands/NicknameLockInteraction.cs +++ b/Commands/NicknameLockCmds.cs @@ -1,12 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Cliptok.Commands.InteractionCommands +namespace Cliptok.Commands { - public class NicknameLockInteraction + public class NicknameLockCmds { [Command("nicknamelock")] [Description("Prevent a member from changing their nickname.")] @@ -85,5 +79,4 @@ public async Task NicknameLockEnableSlashCmd(SlashCommandContext ctx, [Parameter } } } - -} +} \ No newline at end of file diff --git a/Commands/Raidmode.cs b/Commands/Raidmode.cs deleted file mode 100644 index e1945709..00000000 --- a/Commands/Raidmode.cs +++ /dev/null @@ -1,87 +0,0 @@ -using DSharpPlus.Commands.Trees.Metadata; - -namespace Cliptok.Commands -{ - internal class Raidmode - { - [Command("clipraidmodetextcmd")] - [TextAlias("clipraidmode")] - [Description("Manage the server's raidmode, preventing joins while on.")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [RequireHomeserverPerm(ServerPermLevel.Moderator)] - class RaidmodeCommands - { - [DefaultGroupCommand] - [Command("status")] - [Description("Check whether raidmode is enabled or not, and when it ends.")] - public async Task RaidmodeStatus(TextCommandContext ctx) - { - if (Program.db.HashExists("raidmode", ctx.Guild.Id)) - { - string output = $"{Program.cfgjson.Emoji.On} Raidmode is currently **enabled**."; - ulong expirationTimeUnix = (ulong)Program.db.HashGet("raidmode", ctx.Guild.Id); - output += $"\nRaidmode ends "; - await ctx.RespondAsync(output); - } - else - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Banned} Raidmode is currently **disabled**."); - } - } - - [Command("on")] - [Description("Enable raidmode.")] - public async Task RaidmodeOn(TextCommandContext ctx, [Description("The amount of time to keep raidmode enabled for. Default is 3 hours.")] string duration = default) - { - if (Program.db.HashExists("raidmode", ctx.Guild.Id)) - { - string output = $"{Program.cfgjson.Emoji.On} Raidmode is already **enabled**."; - - ulong expirationTimeUnix = (ulong)Program.db.HashGet("raidmode", ctx.Guild.Id); - output += $"\nRaidmode ends "; - await ctx.RespondAsync(output); - } - else - { - DateTime parsedExpiration; - - if (duration == default) - parsedExpiration = DateTime.Now.AddHours(3); - else - parsedExpiration = HumanDateParser.HumanDateParser.Parse(duration); - - long unixExpiration = TimeHelpers.ToUnixTimestamp(parsedExpiration); - Program.db.HashSet("raidmode", ctx.Guild.Id, unixExpiration); - - await ctx.RespondAsync($"{Program.cfgjson.Emoji.On} Raidmode is now **enabled** and will end ."); - await LogChannelHelper.LogMessageAsync("mod", - new DiscordMessageBuilder() - .WithContent($"{Program.cfgjson.Emoji.On} Raidmode was **enabled** by {ctx.User.Mention} and ends .") - .WithAllowedMentions(Mentions.None) - ); - } - } - - [Command("off")] - [Description("Disable raidmode.")] - public async Task RaidmdodeOff(TextCommandContext ctx) - { - if (Program.db.HashExists("raidmode", ctx.Guild.Id)) - { - long expirationTimeUnix = (long)Program.db.HashGet("raidmode", ctx.Guild.Id); - Program.db.HashDelete("raidmode", ctx.Guild.Id); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Off} Raidmode is now **disabled**.\nIt was supposed to end ."); - await LogChannelHelper.LogMessageAsync("mod", - new DiscordMessageBuilder() - .WithContent($"{Program.cfgjson.Emoji.Off} Raidmode was **disabled** by {ctx.User.Mention}.\nIt was supposed to end .") - .WithAllowedMentions(Mentions.None) - ); - } - else - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Off} Raidmode is already **disabled**."); - } - } - } - } -} diff --git a/Commands/InteractionCommands/RaidmodeInteractions.cs b/Commands/RaidmodeCmds.cs similarity index 81% rename from Commands/InteractionCommands/RaidmodeInteractions.cs rename to Commands/RaidmodeCmds.cs index 81b9b719..3ee3d2b1 100644 --- a/Commands/InteractionCommands/RaidmodeInteractions.cs +++ b/Commands/RaidmodeCmds.cs @@ -1,18 +1,26 @@ -namespace Cliptok.Commands.InteractionCommands +namespace Cliptok.Commands { - internal class RaidmodeInteractions + public class RaidmodeCmds { [Command("raidmode")] + [TextAlias("clipraidmode")] [Description("Commands relating to Raidmode")] - [AllowedProcessors(typeof(SlashCommandProcessor))] + [AllowedProcessors(typeof(SlashCommandProcessor), typeof(TextCommandProcessor))] [RequireHomeserverPerm(ServerPermLevel.Moderator)] [RequirePermissions(DiscordPermission.ModerateMembers)] public class RaidmodeSlashCommands { + [DefaultGroupCommand] [Command("status")] [Description("Check the current state of raidmode.")] - public async Task RaidmodeStatus(SlashCommandContext ctx) + public async Task RaidmodeStatus(CommandContext ctx) { + // Avoid conflicts with Modmail's !raidmode + if (ctx is TextCommandContext + && !ctx.As().Message.Content.Contains("clipraidmode") + && !ctx.As().Message.Content.Contains("c!raidmode")) + return; + if (Program.db.HashExists("raidmode", ctx.Guild.Id)) { string output = $"Raidmode is currently **enabled**."; @@ -35,11 +43,17 @@ public async Task RaidmodeStatus(SlashCommandContext ctx) [Command("on")] [Description("Enable raidmode. Defaults to 3 hour length if not specified.")] - public async Task RaidmodeOnSlash(SlashCommandContext ctx, + public async Task RaidmodeOnSlash(CommandContext ctx, [Parameter("duration"), Description("How long to keep raidmode enabled for.")] string duration = default, [Parameter("allowed_account_age"), Description("How old an account can be to be allowed to bypass raidmode. Relative to right now.")] string allowedAccountAge = "" ) { + // Avoid conflicts with Modmail's !raidmode + if (ctx is TextCommandContext + && !ctx.As().Message.Content.Contains("clipraidmode") + && !ctx.As().Message.Content.Contains("c!raidmode")) + return; + if (Program.db.HashExists("raidmode", ctx.Guild.Id)) { string output = $"Raidmode is already **enabled**."; @@ -102,8 +116,14 @@ public async Task RaidmodeOnSlash(SlashCommandContext ctx, [Command("off")] [Description("Disable raidmode immediately.")] - public async Task RaidmodeOffSlash(SlashCommandContext ctx) + public async Task RaidmodeOffSlash(CommandContext ctx) { + // Avoid conflicts with Modmail's !raidmode + if (ctx is TextCommandContext + && !ctx.As().Message.Content.Contains("clipraidmode") + && !ctx.As().Message.Content.Contains("c!raidmode")) + return; + if (Program.db.HashExists("raidmode", ctx.Guild.Id)) { long expirationTimeUnix = (long)Program.db.HashGet("raidmode", ctx.Guild.Id); @@ -128,6 +148,5 @@ public async Task RaidmodeOffSlash(SlashCommandContext ctx) } } } - } -} +} \ No newline at end of file diff --git a/Commands/Reminders.cs b/Commands/ReminderCmds.cs similarity index 97% rename from Commands/Reminders.cs rename to Commands/ReminderCmds.cs index 8566c271..2c3087d9 100644 --- a/Commands/Reminders.cs +++ b/Commands/ReminderCmds.cs @@ -1,6 +1,6 @@ -namespace Cliptok.Commands +namespace Cliptok.Commands { - public class Reminders + public class ReminderCmds { public class Reminder { @@ -71,6 +71,5 @@ public async Task RemindMe( await Program.db.ListRightPushAsync("reminders", JsonConvert.SerializeObject(reminderObject)); await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} I'll try my best to remind you about that on ()"); // (In roughly **{TimeHelpers.TimeToPrettyFormat(t.Subtract(ctx.Message.Timestamp.DateTime), false)}**)"); } - } -} +} \ No newline at end of file diff --git a/Commands/UserRoles.cs b/Commands/RoleCmds.cs similarity index 61% rename from Commands/UserRoles.cs rename to Commands/RoleCmds.cs index dadc19b0..6c6e59ec 100644 --- a/Commands/UserRoles.cs +++ b/Commands/RoleCmds.cs @@ -1,8 +1,131 @@ namespace Cliptok.Commands { - [UserRolesPresent] - public class UserRoleCmds + public class RoleCmds { + [Command("grant")] + [Description("Grant a user Tier 1, bypassing any verification requirements.")] + [AllowedProcessors(typeof(SlashCommandProcessor), typeof(TextCommandProcessor))] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermission.ModerateMembers)] + public async Task Grant(CommandContext ctx, [Parameter("user"), Description("The user to grant Tier 1 to.")] DiscordUser _) + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} This command is deprecated and no longer works. Please right click (or tap and hold on mobile) the user and click \"Verify Member\" if available."); + } + + [HomeServer] + [Command("roles")] + [Description("Opt in/out of roles.")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + internal class RoleSlashCommands + { + [Command("grant")] + [Description("Opt into a role.")] + public async Task GrantRole( + SlashCommandContext ctx, + [SlashAutoCompleteProvider(typeof(RolesAutocompleteProvider))] + [Parameter("role"), Description("The role to opt into.")] string role) + { + DiscordMember member = ctx.Member; + + ulong roleId = role switch + { + "insiderCanary" => Program.cfgjson.UserRoles.InsiderCanary, + "insiderDev" => Program.cfgjson.UserRoles.InsiderDev, + "insiderBeta" => Program.cfgjson.UserRoles.InsiderBeta, + "insiderRP" => Program.cfgjson.UserRoles.InsiderRP, + "insider10RP" => Program.cfgjson.UserRoles.Insider10RP, + "patchTuesday" => Program.cfgjson.UserRoles.PatchTuesday, + "giveaways" => Program.cfgjson.UserRoles.Giveaways, + "cts" => Program.cfgjson.CommunityTechSupportRoleID, + _ => 0 + }; + + if (roleId == 0) + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Invalid role! Please choose from the list.", ephemeral: true); + return; + } + + if (roleId == Program.cfgjson.CommunityTechSupportRoleID && await GetPermLevelAsync(ctx.Member) < ServerPermLevel.TechnicalQueriesSlayer) + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.NoPermissions} You must be a TQS member to get the CTS role!", ephemeral: true); + return; + } + + var roleData = await ctx.Guild.GetRoleAsync(roleId); + + await member.GrantRoleAsync(roleData, $"/roles grant used by {DiscordHelpers.UniqueUsername(ctx.User)}"); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} The role {roleData.Mention} has been successfully granted!", ephemeral: true, mentions: false); + } + + [Command("remove")] + [Description("Opt out of a role.")] + public async Task RemoveRole( + SlashCommandContext ctx, + [SlashAutoCompleteProvider(typeof(RolesAutocompleteProvider))] + [Parameter("role"), Description("The role to opt out of.")] string role) + { + DiscordMember member = ctx.Member; + + ulong roleId = role switch + { + "insiderCanary" => Program.cfgjson.UserRoles.InsiderCanary, + "insiderDev" => Program.cfgjson.UserRoles.InsiderDev, + "insiderBeta" => Program.cfgjson.UserRoles.InsiderBeta, + "insiderRP" => Program.cfgjson.UserRoles.InsiderRP, + "insider10RP" => Program.cfgjson.UserRoles.Insider10RP, + "patchTuesday" => Program.cfgjson.UserRoles.PatchTuesday, + "giveaways" => Program.cfgjson.UserRoles.Giveaways, + "cts" => Program.cfgjson.CommunityTechSupportRoleID, + _ => 0 + }; + + if (roleId == 0) + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Invalid role! Please choose from the list.", ephemeral: true); + return; + } + + var roleData = await ctx.Guild.GetRoleAsync(roleId); + + await member.RevokeRoleAsync(roleData, $"/roles remove used by {DiscordHelpers.UniqueUsername(ctx.User)}"); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} The role {roleData.Mention} has been successfully removed!", ephemeral: true, mentions: false); + } + } + + internal class RolesAutocompleteProvider : IAutoCompleteProvider + { + public async ValueTask> AutoCompleteAsync(AutoCompleteContext ctx) + { + Dictionary options = new() + { + { "Windows 11 Insiders (Canary)", "insiderCanary" }, + { "Windows 11 Insiders (Dev)", "insiderDev" }, + { "Windows 11 Insiders (Beta)", "insiderBeta" }, + { "Windows 11 Insiders (Release Preview)", "insiderRP" }, + { "Windows 10 Insiders (Release Preview)", "insider10RP" }, + { "Patch Tuesday", "patchTuesday" }, + { "Giveaways", "giveaways" }, + { "Community Tech Support (CTS)", "cts" } + }; + + var memberHasTqs = await GetPermLevelAsync(ctx.Member) >= ServerPermLevel.TechnicalQueriesSlayer; + + List list = new(); + + foreach (var option in options) + { + var focusedOption = ctx.Options.FirstOrDefault(option => option.Focused); + if (focusedOption.Value.ToString() == "" || option.Key.Contains(focusedOption.Value.ToString(), StringComparison.OrdinalIgnoreCase)) + { + if (option.Value == "cts" && !memberHasTqs) continue; + list.Add(new DiscordAutoCompleteChoice(option.Key, option.Value)); + } + } + + return list; + } + } + public static async Task GiveUserRoleAsync(TextCommandContext ctx, ulong role) { await GiveUserRolesAsync(ctx, x => (ulong)x.GetValue(Program.cfgjson.UserRoles, null) == role); @@ -76,7 +199,8 @@ public static async Task RemoveUserRolesAsync(TextCommandContext ctx, Func DateTime.Now.AddHours(24)) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Time can't be greater than 24 hours!"); - return; - } - var dmsDisabledUntil = t.ToUniversalTime().ToString("o"); - - // get current security actions to avoid unintentionally resetting invites_disabled_until - var currentActions = await SecurityActionHelpers.GetCurrentSecurityActions(ctx.Guild.Id); - JToken invitesDisabledUntil; - if (currentActions is null || !currentActions.HasValues) - invitesDisabledUntil = null; - else - invitesDisabledUntil = currentActions["invites_disabled_until"]; - - // create json body - var newSecurityActions = JsonConvert.SerializeObject(new - { - invites_disabled_until = invitesDisabledUntil, - dms_disabled_until = dmsDisabledUntil, - }); - - // set actions - var setActionsResponse = await SecurityActionHelpers.SetCurrentSecurityActions(ctx.Guild.Id, newSecurityActions); - - // respond - if (setActionsResponse.IsSuccessStatusCode) - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Successfully paused DMs for **{TimeHelpers.TimeToPrettyFormat(t.Subtract(ctx.Message.Timestamp.DateTime), false)}**!"); - else - { - ctx.Client.Logger.LogError("Failed to set Security Actions.\nPayload: {payload}\nResponse: {statuscode} {body}", newSecurityActions.ToString(), (int)setActionsResponse.StatusCode, await setActionsResponse.Content.ReadAsStringAsync()); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Something went wrong and I wasn't able to pause DMs! Discord returned status code `{setActionsResponse.StatusCode}`."); - } - } - - [Command("unpausedmstextcmd")] - [TextAlias("unpausedms")] - [Description("Unpause DMs between server members.")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] - public async Task UnpauseDMs(TextCommandContext ctx) - { - // need to make our own api calls because D#+ can't do this natively? - - // get current security actions to avoid unintentionally resetting invites_disabled_until - var currentActions = await SecurityActionHelpers.GetCurrentSecurityActions(ctx.Guild.Id); - JToken dmsDisabledUntil, invitesDisabledUntil; - if (currentActions is null || !currentActions.HasValues) - { - dmsDisabledUntil = null; - invitesDisabledUntil = null; - } - else - { - dmsDisabledUntil = currentActions["dms_disabled_until"]; - invitesDisabledUntil = currentActions["invites_disabled_until"]; - } - - // if dms are already unpaused, return error - if (dmsDisabledUntil is null) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} DMs are already unpaused!"); - return; - } - - // create json body - var newSecurityActions = JsonConvert.SerializeObject(new - { - invites_disabled_until = invitesDisabledUntil, - dms_disabled_until = (object)null, - }); - - // set actions - var setActionsResponse = await SecurityActionHelpers.SetCurrentSecurityActions(ctx.Guild.Id, newSecurityActions); - - // respond - if (setActionsResponse.IsSuccessStatusCode) - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Successfully unpaused DMs!"); - else - { - ctx.Client.Logger.LogError("Failed to set Security Actions.\nPayload: {payload}\nResponse: {statuscode} {body}", newSecurityActions.ToString(), (int)setActionsResponse.StatusCode, await setActionsResponse.Content.ReadAsStringAsync()); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Something went wrong and I wasn't able to unpause DMs! Discord returned status code `{setActionsResponse.StatusCode}`."); - } - } - } -} \ No newline at end of file diff --git a/Commands/InteractionCommands/SecurityActionInteractions.cs b/Commands/SecurityActionsCmds.cs similarity index 90% rename from Commands/InteractionCommands/SecurityActionInteractions.cs rename to Commands/SecurityActionsCmds.cs index 3ee3d13e..7bbaa901 100644 --- a/Commands/InteractionCommands/SecurityActionInteractions.cs +++ b/Commands/SecurityActionsCmds.cs @@ -1,12 +1,12 @@ -namespace Cliptok.Commands.InteractionCommands +namespace Cliptok.Commands { - public class SecurityActionInteractions + public class SecurityActionsCmds { [Command("pausedms")] [Description("Temporarily pause DMs between server members.")] - [AllowedProcessors(typeof(SlashCommandProcessor))] + [AllowedProcessors(typeof(SlashCommandProcessor), typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermission.ModerateMembers)] - public async Task SlashPauseDMs(SlashCommandContext ctx, [Parameter("time"), Description("The amount of time to pause DMs for. Cannot be greater than 24 hours.")] string time) + public async Task SlashPauseDMs(CommandContext ctx, [Parameter("time"), Description("The amount of time to pause DMs for. Cannot be greater than 24 hours.")] string time) { // need to make our own api calls because D#+ can't do this natively? @@ -54,9 +54,9 @@ public async Task SlashPauseDMs(SlashCommandContext ctx, [Parameter("time"), Des [Command("unpausedms")] [Description("Unpause DMs between server members.")] - [AllowedProcessors(typeof(SlashCommandProcessor))] + [AllowedProcessors(typeof(SlashCommandProcessor), typeof(TextCommandProcessor))] [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator), RequirePermissions(DiscordPermission.ModerateMembers)] - public async Task SlashUnpauseDMs(SlashCommandContext ctx) + public async Task SlashUnpauseDMs(CommandContext ctx) { // need to make our own api calls because D#+ can't do this natively? diff --git a/Commands/InteractionCommands/SlowmodeInteractions.cs b/Commands/SlowmodeCmds.cs similarity index 97% rename from Commands/InteractionCommands/SlowmodeInteractions.cs rename to Commands/SlowmodeCmds.cs index 5dfaeb47..37e3aada 100644 --- a/Commands/InteractionCommands/SlowmodeInteractions.cs +++ b/Commands/SlowmodeCmds.cs @@ -1,6 +1,6 @@ -namespace Cliptok.Commands.InteractionCommands +namespace Cliptok.Commands { - internal class SlowmodeInteractions + public class SlowmodeCmds { [Command("slowmode")] [Description("Slow down the channel...")] @@ -73,6 +73,5 @@ await ctx.RespondAsync($"{Program.cfgjson.Emoji.ClockTime} Slowmode has been set } } } - } -} +} \ No newline at end of file diff --git a/Commands/InteractionCommands/StatusInteractions.cs b/Commands/StatusCmds.cs similarity index 98% rename from Commands/InteractionCommands/StatusInteractions.cs rename to Commands/StatusCmds.cs index fa95d445..8f33262a 100644 --- a/Commands/InteractionCommands/StatusInteractions.cs +++ b/Commands/StatusCmds.cs @@ -1,6 +1,6 @@ namespace Cliptok.Commands.InteractionCommands { - internal class StatusInteractions + internal class StatusCmds { [Command("status")] [Description("Status commands")] diff --git a/Commands/TechSupport.cs b/Commands/TechSupport.cs deleted file mode 100644 index cb9d8483..00000000 --- a/Commands/TechSupport.cs +++ /dev/null @@ -1,48 +0,0 @@ -namespace Cliptok.Commands -{ - internal class TechSupport - { - [Command("asktextcmd")] - [TextAlias("ask")] - [Description("Outputs information on how and where to ask tech support questions. Replying to a message while triggering the command will mirror the reply in the response.")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [HomeServer] - public async Task AskCmd(TextCommandContext ctx, [Description("Optional, a user to ping with the information")] DiscordUser user = default) - { - await ctx.Message.DeleteAsync(); - DiscordEmbedBuilder embed = new DiscordEmbedBuilder() - .WithColor(13920845); - if (ctx.Channel.Id == Program.cfgjson.TechSupportChannel || ctx.Channel.ParentId == Program.cfgjson.SupportForumId) - { - embed.Title = "**__Need help?__**"; - embed.Description = $"You are in the right place! Please state your question with *plenty of detail* and mention the <@&{Program.cfgjson.CommunityTechSupportRoleID}> role and someone may be able to help you.\n\n" + - $"Details includes error codes and other specific information."; - } - else - { - embed.Title = "**__Need Help Or Have a Problem?__**"; - embed.Description = $"You're probably looking for <#{Program.cfgjson.TechSupportChannel}> or <#{Program.cfgjson.SupportForumId}>!\n\n" + - $"Once there, please be sure to provide **plenty of details**, ping the <@&{Program.cfgjson.CommunityTechSupportRoleID}> role, and *be patient!*\n\n" + - $"Look under the `🔧 Support` category for the appropriate channel for your issue. See <#413274922413195275> for more info."; - } - - if (user != default) - { - await ctx.Channel.SendMessageAsync(user.Mention, embed); - } - else if (ctx.Message.ReferencedMessage is not null) - { - var messageBuild = new DiscordMessageBuilder() - .AddEmbed(embed) - .WithReply(ctx.Message.ReferencedMessage.Id, mention: true); - - await ctx.Channel.SendMessageAsync(messageBuild); - } - else - { - await ctx.Channel.SendMessageAsync(embed); - } - } - - } -} diff --git a/Commands/TechSupportCmds.cs b/Commands/TechSupportCmds.cs new file mode 100644 index 00000000..0c0a2eef --- /dev/null +++ b/Commands/TechSupportCmds.cs @@ -0,0 +1,126 @@ +using Cliptok.Constants; + +namespace Cliptok.Commands +{ + internal class TechSupportCmds + { + [Command("vcredist")] + [Description("Outputs download URLs for the specified Visual C++ Redistributables version")] + [AllowedProcessors(typeof(SlashCommandProcessor))] + public async Task RedistsCommand( + SlashCommandContext ctx, + + [SlashChoiceProvider(typeof(VcRedistChoiceProvider))] + [Parameter("version"), Description("Visual Studio version number or year")] long version + ) + { + VcRedist redist = VcRedistConstants.VcRedists + .First((e) => + { + return version == e.Version; + }); + + DiscordEmbedBuilder embed = new DiscordEmbedBuilder() + .WithTitle($"Visual C++ {redist.Year}{(redist.Year == 2015 ? "+" : "")} Redistributables (version {redist.Version})") + .WithFooter("The above links are official and safe to download.") + .WithColor(new("7160e8")); + + foreach (var url in redist.DownloadUrls) + { + embed.AddField($"{url.Key.ToString("G")}", $"{url.Value}"); + } + + await ctx.RespondAsync(null, embed.Build(), false); + } + + [Command("asktextcmd")] + [TextAlias("ask")] + [Description("Outputs information on how and where to ask tech support questions. Replying to a message while triggering the command will mirror the reply in the response.")] + [AllowedProcessors(typeof(TextCommandProcessor))] + [HomeServer] + public async Task AskCmd(TextCommandContext ctx, [Description("Optional, a user to ping with the information")] DiscordUser user = default) + { + await ctx.Message.DeleteAsync(); + DiscordEmbedBuilder embed = new DiscordEmbedBuilder() + .WithColor(13920845); + if (ctx.Channel.Id == Program.cfgjson.TechSupportChannel || ctx.Channel.ParentId == Program.cfgjson.SupportForumId) + { + embed.Title = "**__Need help?__**"; + embed.Description = $"You are in the right place! Please state your question with *plenty of detail* and mention the <@&{Program.cfgjson.CommunityTechSupportRoleID}> role and someone may be able to help you.\n\n" + + $"Details includes error codes and other specific information."; + } + else + { + embed.Title = "**__Need Help Or Have a Problem?__**"; + embed.Description = $"You're probably looking for <#{Program.cfgjson.TechSupportChannel}> or <#{Program.cfgjson.SupportForumId}>!\n\n" + + $"Once there, please be sure to provide **plenty of details**, ping the <@&{Program.cfgjson.CommunityTechSupportRoleID}> role, and *be patient!*\n\n" + + $"Look under the `🔧 Support` category for the appropriate channel for your issue. See <#413274922413195275> for more info."; + } + + if (user != default) + { + await ctx.Channel.SendMessageAsync(user.Mention, embed); + } + else if (ctx.Message.ReferencedMessage is not null) + { + var messageBuild = new DiscordMessageBuilder() + .AddEmbed(embed) + .WithReply(ctx.Message.ReferencedMessage.Id, mention: true); + + await ctx.Channel.SendMessageAsync(messageBuild); + } + else + { + await ctx.Channel.SendMessageAsync(embed); + } + } + + [Command("on-call")] + [Description("Give yourself the CTS role.")] + [AllowedProcessors(typeof(TextCommandProcessor))] + [HomeServer] + [RequireHomeserverPerm(ServerPermLevel.TechnicalQueriesSlayer)] + public async Task OnCallCommand(CommandContext ctx) + { + var ctsRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.CommunityTechSupportRoleID); + await ctx.Member.GrantRoleAsync(ctsRole, "Used !on-call"); + await ctx.RespondAsync(new DiscordMessageBuilder().AddEmbed(new DiscordEmbedBuilder() + .WithTitle($"{Program.cfgjson.Emoji.On} Received Community Tech Support Role") + .WithDescription($"{ctx.User.Mention} is available to help out in **#tech-support**.\n(Use `!off-call` when you're no longer available)") + .WithColor(DiscordColor.Green) + )); + } + + [Command("off-call")] + [Description("Remove the CTS role.")] + [AllowedProcessors(typeof(TextCommandProcessor))] + [HomeServer] + [RequireHomeserverPerm(ServerPermLevel.TechnicalQueriesSlayer)] + public async Task OffCallCommand(CommandContext ctx) + { + var ctsRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.CommunityTechSupportRoleID); + await ctx.Member.RevokeRoleAsync(ctsRole, "Used !off-call"); + await ctx.RespondAsync(new DiscordMessageBuilder().AddEmbed(new DiscordEmbedBuilder() + .WithTitle($"{Program.cfgjson.Emoji.Off} Removed Community Tech Support Role") + .WithDescription($"{ctx.User.Mention} is no longer available to help out in **#tech-support**.\n(Use `!on-call` again when you're available)") + .WithColor(DiscordColor.Red) + )); + } + } + + internal class VcRedistChoiceProvider : IChoiceProvider + { + public async ValueTask> ProvideAsync(CommandParameter _) + { + return new List + { + new("Visual Studio 2015+ - v140", "140"), + new("Visual Studio 2013 - v120", "120"), + new("Visual Studio 2012 - v110", "110"), + new("Visual Studio 2010 - v100", "100"), + new("Visual Studio 2008 - v90", "90"), + new("Visual Studio 2005 - v80", "80") + }; + } + } +} diff --git a/Commands/TechSupportCommands.cs b/Commands/TechSupportCommands.cs deleted file mode 100644 index dcf37d94..00000000 --- a/Commands/TechSupportCommands.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Cliptok.Commands -{ - internal class TechSupportCommands - { - [Command("on-call")] - [Description("Give yourself the CTS role.")] - [RequireHomeserverPerm(ServerPermLevel.TechnicalQueriesSlayer)] - public async Task OnCallCommand(CommandContext ctx) - { - var ctsRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.CommunityTechSupportRoleID); - await ctx.Member.GrantRoleAsync(ctsRole, "Used !on-call"); - await ctx.RespondAsync(new DiscordMessageBuilder().AddEmbed(new DiscordEmbedBuilder() - .WithTitle($"{Program.cfgjson.Emoji.On} Received Community Tech Support Role") - .WithDescription($"{ctx.User.Mention} is available to help out in **#tech-support**.\n(Use `!off-call` when you're no longer available)") - .WithColor(DiscordColor.Green) - )); - } - - [Command("off-call")] - [Description("Remove the CTS role.")] - [RequireHomeserverPerm(ServerPermLevel.TechnicalQueriesSlayer)] - public async Task OffCallCommand(CommandContext ctx) - { - var ctsRole = await ctx.Guild.GetRoleAsync(Program.cfgjson.CommunityTechSupportRoleID); - await ctx.Member.RevokeRoleAsync(ctsRole, "Used !off-call"); - await ctx.RespondAsync(new DiscordMessageBuilder().AddEmbed(new DiscordEmbedBuilder() - .WithTitle($"{Program.cfgjson.Emoji.Off} Removed Community Tech Support Role") - .WithDescription($"{ctx.User.Mention} is no longer available to help out in **#tech-support**.\n(Use `!on-call` again when you're available)") - .WithColor(DiscordColor.Red) - )); - } - } -} diff --git a/Commands/Threads.cs b/Commands/Threads.cs deleted file mode 100644 index b4a6faec..00000000 --- a/Commands/Threads.cs +++ /dev/null @@ -1,80 +0,0 @@ -namespace Cliptok.Commands -{ - internal class Threads - { - [Command("archivetextcmd")] - [TextAlias("archive")] - [Description("Archive the current thread or another thread.")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public async Task ArchiveCommand(TextCommandContext ctx, DiscordChannel channel = default) - { - if (channel == default) - channel = ctx.Channel; - - if (channel.Type is not DiscordChannelType.PrivateThread && channel.Type is not DiscordChannelType.PublicThread && channel.Type is not DiscordChannelType.NewsThread) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {channel.Mention} is not a thread!"); - return; - } - - var thread = (DiscordThreadChannel)channel; - - await thread.ModifyAsync(a => - { - a.IsArchived = true; - a.Locked = false; - }); - } - - [Command("lockthreadtextcmd")] - [TextAlias("lockthread")] - [Description("Lock the current thread or another thread.")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public async Task LockThreadCommand(TextCommandContext ctx, DiscordChannel channel = default) - { - if (channel == default) - channel = ctx.Channel; - - if (channel.Type is not DiscordChannelType.PrivateThread && channel.Type is not DiscordChannelType.PublicThread) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {channel.Mention} is not a thread!"); - return; - } - - var thread = (DiscordThreadChannel)channel; - - await thread.ModifyAsync(a => - { - a.IsArchived = true; - a.Locked = true; - }); - } - - [Command("unarchivetextcmd")] - [TextAlias("unarchive")] - [Description("Unarchive a thread")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public async Task UnarchiveCommand(TextCommandContext ctx, DiscordChannel channel = default) - { - if (channel == default) - channel = ctx.Channel; - - if (channel.Type is not DiscordChannelType.PrivateThread && channel.Type is not DiscordChannelType.PublicThread) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {channel.Mention} is not a thread!"); - return; - } - - var thread = (DiscordThreadChannel)(channel); - - await thread.ModifyAsync(a => - { - a.IsArchived = false; - a.Locked = false; - }); - } - } -} diff --git a/Commands/Timestamp.cs b/Commands/Timestamp.cs deleted file mode 100644 index 0026d86b..00000000 --- a/Commands/Timestamp.cs +++ /dev/null @@ -1,48 +0,0 @@ -using DSharpPlus.Commands.Trees.Metadata; - -namespace Cliptok.Commands -{ - internal class Timestamp - { - [Command("timestamptextcmd")] - [TextAlias("timestamp", "ts", "time")] - [Description("Returns various timestamps for a given Discord ID/snowflake")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [HomeServer] - class TimestampCmds - { - [DefaultGroupCommand] - [Command("unix")] - [TextAlias("u", "epoch")] - [Description("Returns the Unix timestamp of a given Discord ID/snowflake")] - public async Task TimestampUnixCmd(TextCommandContext ctx, [Description("The ID/snowflake to fetch the Unix timestamp for")] ulong snowflake) - { - var msSinceEpoch = snowflake >> 22; - var msUnix = msSinceEpoch + 1420070400000; - await ctx.RespondAsync($"{msUnix / 1000}"); - } - - [Command("relative")] - [TextAlias("r")] - [Description("Returns the amount of time between now and a given Discord ID/snowflake")] - public async Task TimestampRelativeCmd(TextCommandContext ctx, [Description("The ID/snowflake to fetch the relative timestamp for")] ulong snowflake) - { - var msSinceEpoch = snowflake >> 22; - var msUnix = msSinceEpoch + 1420070400000; - await ctx.RespondAsync($"{Program.cfgjson.Emoji.ClockTime} "); - } - - [Command("fulldate")] - [TextAlias("f", "datetime")] - [Description("Returns the fully-formatted date and time of a given Discord ID/snowflake")] - public async Task TimestampFullCmd(TextCommandContext ctx, [Description("The ID/snowflake to fetch the full timestamp for")] ulong snowflake) - { - var msSinceEpoch = snowflake >> 22; - var msUnix = msSinceEpoch + 1420070400000; - await ctx.RespondAsync($"{Program.cfgjson.Emoji.ClockTime} "); - } - - } - - } -} diff --git a/Commands/InteractionCommands/TrackingInteractions.cs b/Commands/TrackingCmds.cs similarity index 99% rename from Commands/InteractionCommands/TrackingInteractions.cs rename to Commands/TrackingCmds.cs index cceddba0..688e3024 100644 --- a/Commands/InteractionCommands/TrackingInteractions.cs +++ b/Commands/TrackingCmds.cs @@ -1,6 +1,6 @@ namespace Cliptok.Commands.InteractionCommands { - internal class TrackingInteractions + internal class TrackingCmds { [Command("tracking")] [Description("Commands to manage message tracking of users")] diff --git a/Commands/InteractionCommands/UserNoteInteractions.cs b/Commands/UserNoteCmds.cs similarity index 96% rename from Commands/InteractionCommands/UserNoteInteractions.cs rename to Commands/UserNoteCmds.cs index 49310910..55cdce6e 100644 --- a/Commands/InteractionCommands/UserNoteInteractions.cs +++ b/Commands/UserNoteCmds.cs @@ -2,8 +2,17 @@ namespace Cliptok.Commands.InteractionCommands { - internal class UserNoteInteractions + internal class UserNoteCmds { + [Command("Show Notes")] + [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] + [AllowedProcessors(typeof(UserCommandProcessor))] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermission.ModerateMembers)] + public async Task ShowNotes(UserCommandContext ctx, DiscordUser targetUser) + { + await ctx.RespondAsync(embed: await UserNoteHelpers.GenerateUserNotesEmbedAsync(targetUser), ephemeral: true); + } + [Command("note")] [Description("Manage user notes")] [AllowedProcessors(typeof(SlashCommandProcessor))] diff --git a/Commands/Utility.cs b/Commands/Utility.cs deleted file mode 100644 index 7b4d8ef0..00000000 --- a/Commands/Utility.cs +++ /dev/null @@ -1,84 +0,0 @@ -namespace Cliptok.Commands -{ - internal class Utility - { - [Command("pingtextcmd")] - [TextAlias("ping")] - [Description("Pong? This command lets you know whether I'm working well.")] - [AllowedProcessors(typeof(TextCommandProcessor))] - public async Task Ping(TextCommandContext ctx) - { - ctx.Client.Logger.LogDebug(ctx.Client.GetConnectionLatency(Program.cfgjson.ServerID).ToString()); - DiscordMessage return_message = await ctx.Message.RespondAsync("Pinging..."); - ulong ping = (return_message.Id - ctx.Message.Id) >> 22; - char[] choices = new char[] { 'a', 'e', 'o', 'u', 'i', 'y' }; - char letter = choices[Program.rand.Next(0, choices.Length)]; - await return_message.ModifyAsync($"P{letter}ng! 🏓\n" + - $"• It took me `{ping}ms` to reply to your message!\n" + - $"• Last Websocket Heartbeat took `{Math.Round(ctx.Client.GetConnectionLatency(0).TotalMilliseconds, 0)}ms`!"); - } - - [Command("edittextcmd")] - [TextAlias("edit")] - [Description("Edit a message.")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [RequireHomeserverPerm(ServerPermLevel.Moderator)] - public async Task Edit( - TextCommandContext ctx, - [Description("The ID of the message to edit.")] ulong messageId, - [RemainingText, Description("New message content.")] string content - ) - { - var msg = await ctx.Channel.GetMessageAsync(messageId); - - if (msg is null || msg.Author.Id != ctx.Client.CurrentUser.Id) - return; - - await ctx.Message.DeleteAsync(); - - await msg.ModifyAsync(content); - } - - [Command("editappendtextcmd")] - [TextAlias("editappend")] - [Description("Append content to an existing bot message with a newline.")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [RequireHomeserverPerm(ServerPermLevel.Moderator)] - public async Task EditAppend( - TextCommandContext ctx, - [Description("The ID of the message to edit")] ulong messageId, - [RemainingText, Description("Content to append on the end of the message.")] string content - ) - { - var msg = await ctx.Channel.GetMessageAsync(messageId); - - if (msg is null || msg.Author.Id != ctx.Client.CurrentUser.Id) - return; - - var newContent = msg.Content + "\n" + content; - if (newContent.Length > 2000) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} New content exceeded 2000 characters."); - } - else - { - await ctx.Message.DeleteAsync(); - await msg.ModifyAsync(newContent); - } - } - - [Command("userinfotextcmd")] - [TextAlias("userinfo", "userinfo", "user-info", "whois")] - [Description("Show info about a user.")] - [AllowedProcessors(typeof(TextCommandProcessor))] - public async Task UserInfoCommand( - TextCommandContext ctx, - DiscordUser user = null) - { - if (user is null) - user = ctx.User; - - await ctx.RespondAsync(embed: await DiscordHelpers.GenerateUserEmbed(user, ctx.Guild)); - } - } -} diff --git a/Commands/UtilityCmds.cs b/Commands/UtilityCmds.cs new file mode 100644 index 00000000..55d1ef8d --- /dev/null +++ b/Commands/UtilityCmds.cs @@ -0,0 +1,149 @@ +namespace Cliptok.Commands +{ + public class UtilityCmds + { + [Command("Show Avatar")] + [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] + [AllowedProcessors(typeof(UserCommandProcessor))] + public async Task ContextAvatar(UserCommandContext ctx, DiscordUser targetUser) + { + string avatarUrl = await LykosAvatarMethods.UserOrMemberAvatarURL(targetUser, ctx.Guild); + + DiscordEmbedBuilder embed = new DiscordEmbedBuilder() + .WithColor(new DiscordColor(0xC63B68)) + .WithTimestamp(DateTime.UtcNow) + .WithImageUrl(avatarUrl) + .WithAuthor( + $"Avatar for {targetUser.Username} (Click to open in browser)", + avatarUrl + ); + + await ctx.RespondAsync(null, embed, ephemeral: true); + } + + [Command("User Information")] + [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] + [AllowedProcessors(typeof(UserCommandProcessor))] + public async Task ContextUserInformation(UserCommandContext ctx, DiscordUser targetUser) + { + await ctx.RespondAsync(embed: await DiscordHelpers.GenerateUserEmbed(targetUser, ctx.Guild), ephemeral: true); + } + + [Command("userinfo")] + [TextAlias("user-info", "whois")] + [Description("Show info about a user.")] + [AllowedProcessors(typeof(SlashCommandProcessor), typeof(TextCommandProcessor))] + public async Task UserInfoSlashCommand(CommandContext ctx, [Parameter("user"), Description("The user to retrieve information about.")] DiscordUser user = null, [Parameter("public"), Description("Whether to show the output publicly.")] bool publicMessage = false) + { + if (user is null) + user = ctx.User; + + await ctx.RespondAsync(embed: await DiscordHelpers.GenerateUserEmbed(user, ctx.Guild), ephemeral: !publicMessage); + } + + [Command("pingtextcmd")] + [TextAlias("ping")] + [Description("Pong? This command lets you know whether I'm working well.")] + [AllowedProcessors(typeof(TextCommandProcessor))] + public async Task Ping(TextCommandContext ctx) + { + ctx.Client.Logger.LogDebug(ctx.Client.GetConnectionLatency(Program.cfgjson.ServerID).ToString()); + DiscordMessage return_message = await ctx.Message.RespondAsync("Pinging..."); + ulong ping = (return_message.Id - ctx.Message.Id) >> 22; + char[] choices = new char[] { 'a', 'e', 'o', 'u', 'i', 'y' }; + char letter = choices[Program.rand.Next(0, choices.Length)]; + await return_message.ModifyAsync($"P{letter}ng! 🏓\n" + + $"• It took me `{ping}ms` to reply to your message!\n" + + $"• Last Websocket Heartbeat took `{Math.Round(ctx.Client.GetConnectionLatency(0).TotalMilliseconds, 0)}ms`!"); + } + + [Command("edittextcmd")] + [TextAlias("edit")] + [Description("Edit a message.")] + [AllowedProcessors(typeof(TextCommandProcessor))] + [RequireHomeserverPerm(ServerPermLevel.Moderator)] + public async Task Edit( + TextCommandContext ctx, + [Description("The ID of the message to edit.")] ulong messageId, + [RemainingText, Description("New message content.")] string content + ) + { + var msg = await ctx.Channel.GetMessageAsync(messageId); + + if (msg is null || msg.Author.Id != ctx.Client.CurrentUser.Id) + return; + + await ctx.Message.DeleteAsync(); + + await msg.ModifyAsync(content); + } + + [Command("editappendtextcmd")] + [TextAlias("editappend")] + [Description("Append content to an existing bot message with a newline.")] + [AllowedProcessors(typeof(TextCommandProcessor))] + [RequireHomeserverPerm(ServerPermLevel.Moderator)] + public async Task EditAppend( + TextCommandContext ctx, + [Description("The ID of the message to edit")] ulong messageId, + [RemainingText, Description("Content to append on the end of the message.")] string content + ) + { + var msg = await ctx.Channel.GetMessageAsync(messageId); + + if (msg is null || msg.Author.Id != ctx.Client.CurrentUser.Id) + return; + + var newContent = msg.Content + "\n" + content; + if (newContent.Length > 2000) + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} New content exceeded 2000 characters."); + } + else + { + await ctx.Message.DeleteAsync(); + await msg.ModifyAsync(newContent); + } + } + + [Command("timestamptextcmd")] + [TextAlias("timestamp", "ts", "time")] + [Description("Returns various timestamps for a given Discord ID/snowflake")] + [AllowedProcessors(typeof(TextCommandProcessor))] + [HomeServer] + class TimestampCmds + { + [DefaultGroupCommand] + [Command("unix")] + [TextAlias("u", "epoch")] + [Description("Returns the Unix timestamp of a given Discord ID/snowflake")] + public async Task TimestampUnixCmd(TextCommandContext ctx, [Description("The ID/snowflake to fetch the Unix timestamp for")] ulong snowflake) + { + var msSinceEpoch = snowflake >> 22; + var msUnix = msSinceEpoch + 1420070400000; + await ctx.RespondAsync($"{msUnix / 1000}"); + } + + [Command("relative")] + [TextAlias("r")] + [Description("Returns the amount of time between now and a given Discord ID/snowflake")] + public async Task TimestampRelativeCmd(TextCommandContext ctx, [Description("The ID/snowflake to fetch the relative timestamp for")] ulong snowflake) + { + var msSinceEpoch = snowflake >> 22; + var msUnix = msSinceEpoch + 1420070400000; + await ctx.RespondAsync($"{Program.cfgjson.Emoji.ClockTime} "); + } + + [Command("fulldate")] + [TextAlias("f", "datetime")] + [Description("Returns the fully-formatted date and time of a given Discord ID/snowflake")] + public async Task TimestampFullCmd(TextCommandContext ctx, [Description("The ID/snowflake to fetch the full timestamp for")] ulong snowflake) + { + var msSinceEpoch = snowflake >> 22; + var msUnix = msSinceEpoch + 1420070400000; + await ctx.RespondAsync($"{Program.cfgjson.Emoji.ClockTime} "); + } + + } + } +} \ No newline at end of file diff --git a/Commands/InteractionCommands/WarningInteractions.cs b/Commands/WarningCmds.cs similarity index 50% rename from Commands/InteractionCommands/WarningInteractions.cs rename to Commands/WarningCmds.cs index 5cde264c..9edc1541 100644 --- a/Commands/InteractionCommands/WarningInteractions.cs +++ b/Commands/WarningCmds.cs @@ -2,8 +2,16 @@ namespace Cliptok.Commands.InteractionCommands { - internal class WarningInteractions + internal class WarningCmds { + [Command("Show Warnings")] + [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] + [AllowedProcessors(typeof(UserCommandProcessor))] + public async Task ContextWarnings(UserCommandContext ctx, DiscordUser targetUser) + { + await ctx.RespondAsync(embed: await WarningHelpers.GenerateWarningsEmbedAsync(targetUser), ephemeral: true); + } + [Command("warn")] [Description("Formally warn a user, usually for breaking the server rules.")] [AllowedProcessors(typeof(SlashCommandProcessor))] @@ -345,5 +353,360 @@ await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent($"{Progr .AddEmbed(await FancyWarnEmbedAsync(GetWarning(user.Id, warnId), userID: user.Id))); } } + + [ + Command("warntextcmd"), + Description("Issues a formal warning to a user."), + TextAlias("warn", "wam", "warm"), + AllowedProcessors(typeof(TextCommandProcessor)), + HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator) + ] + public async Task WarnCmd( + TextCommandContext ctx, + [Description("The user you are warning. Accepts many formats.")] DiscordUser targetUser, + [RemainingText, Description("The reason for giving this warning.")] string reason = null + ) + { + DiscordMember targetMember; + try + { + targetMember = await ctx.Guild.GetMemberAsync(targetUser.Id); + if ((await GetPermLevelAsync(ctx.Member)) == ServerPermLevel.TrialModerator && ((await GetPermLevelAsync(targetMember)) >= ServerPermLevel.TrialModerator || targetMember.IsBot)) + { + await ctx.Channel.SendMessageAsync($"{Program.cfgjson.Emoji.Error} {ctx.User.Mention}, as a Trial Moderator you cannot perform moderation actions on other staff members or bots."); + return; + } + } + catch + { + // do nothing :/ + } + + var reply = ctx.Message.ReferencedMessage; + + await ctx.Message.DeleteAsync(); + if (reason is null) + { + await ctx.Member.SendMessageAsync($"{Program.cfgjson.Emoji.Warning} Reason must be included for the warning command to work."); + return; + } + + var messageBuild = new DiscordMessageBuilder() + .WithContent($"{Program.cfgjson.Emoji.Warning} <@{targetUser.Id}> was warned: **{reason.Replace("`", "\\`").Replace("*", "\\*")}**"); + + if (reply is not null) + messageBuild.WithReply(reply.Id, true, false); + + var msg = await ctx.Channel.SendMessageAsync(messageBuild); + _ = await GiveWarningAsync(targetUser, ctx.User, reason, msg, ctx.Channel); + } + + [ + Command("anonwarntextcmd"), + TextAlias("anonwarn", "anonwam", "anonwarm"), + Description("Issues a formal warning to a user from a private channel."), + AllowedProcessors(typeof(TextCommandProcessor)), + HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator) + ] + public async Task AnonWarnCmd( + TextCommandContext ctx, + [Description("The channel you wish for the warning message to appear in.")] DiscordChannel targetChannel, + [Description("The user you are warning. Accepts many formats.")] DiscordUser targetUser, + [RemainingText, Description("The reason for giving this warning.")] string reason = null + ) + { + DiscordMember targetMember; + try + { + targetMember = await ctx.Guild.GetMemberAsync(targetUser.Id); + if ((await GetPermLevelAsync(ctx.Member)) == ServerPermLevel.TrialModerator && ((await GetPermLevelAsync(targetMember)) >= ServerPermLevel.TrialModerator || targetMember.IsBot)) + { + await ctx.Channel.SendMessageAsync($"{Program.cfgjson.Emoji.Error} {ctx.User.Mention}, as a Trial Moderator you cannot perform moderation actions on other staff members or bots."); + return; + } + } + catch + { + // do nothing :/ + } + + await ctx.Message.DeleteAsync(); + if (reason is null) + { + await ctx.Member.SendMessageAsync($"{Program.cfgjson.Emoji.Warning} Reason must be included for the warning command to work."); + return; + } + DiscordMessage msg = await targetChannel.SendMessageAsync($"{Program.cfgjson.Emoji.Warning} {targetUser.Mention} was warned: **{reason.Replace("`", "\\`").Replace("*", "\\*")}**"); + await ctx.Channel.SendMessageAsync($"{Program.cfgjson.Emoji.Warning} {targetUser.Mention} was warned in {targetChannel.Mention}: **{reason.Replace("`", "\\`").Replace("*", "\\*")}**"); + _ = await GiveWarningAsync(targetUser, ctx.User, reason, msg, ctx.Channel); + } + + [ + Command("warningstextcmd"), + TextAlias("warnings", "infractions", "warnfractions", "wammings", "wamfractions"), + Description("Shows a list of warnings that a user has been given. For more in-depth information, use the 'warnlookup' command."), + AllowedProcessors(typeof(TextCommandProcessor)), + HomeServer + ] + public async Task WarningCmd( + TextCommandContext ctx, + [Description("The user you want to look up warnings for. Accepts many formats.")] DiscordUser targetUser = null + ) + { + if (targetUser is null) + targetUser = ctx.User; + + await ctx.RespondAsync(null, await GenerateWarningsEmbedAsync(targetUser)); + } + + [ + Command("delwarntextcmd"), + TextAlias("delwarn", "delwarm", "delwam", "deletewarn", "delwarning", "deletewarning"), + Description("Delete a warning that was issued by mistake or later became invalid."), + AllowedProcessors(typeof(TextCommandProcessor)), + HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator) + ] + public async Task DelwarnCmd( + TextCommandContext ctx, + [Description("The user you're removing a warning from. Accepts many formats.")] DiscordUser targetUser, + [Description("The ID of the warning you want to delete.")] long warnId + ) + { + UserWarning warning = GetWarning(targetUser.Id, warnId); + if (warning is null) + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} I couldn't find a warning for that user with that ID! Please check again."); + else if (warning.Type == WarningType.Note) + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} That's a note, not a warning! Try using `/note delete` instead, or make sure you've got the right warning ID."); + } + else if ((await GetPermLevelAsync(ctx.Member)) == ServerPermLevel.TrialModerator && warning.ModUserId != ctx.User.Id && warning.ModUserId != ctx.Client.CurrentUser.Id) + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {ctx.User.Mention}, as a Trial Moderator you cannot edit or delete warnings that aren't issued by you or the bot!"); + } + else + { + bool success = await DelWarningAsync(warning, targetUser.Id); + if (success) + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Deleted} Successfully deleted warning `{StringHelpers.Pad(warnId)}` (belonging to {targetUser.Mention})"); + + await LogChannelHelper.LogMessageAsync("mod", + new DiscordMessageBuilder() + .WithContent($"{Program.cfgjson.Emoji.Deleted} Warning deleted:" + + $"`{StringHelpers.Pad(warnId)}` (belonging to {targetUser.Mention}, deleted by {ctx.Member.Mention})") + .AddEmbed(await FancyWarnEmbedAsync(warning, true, 0xf03916, true, targetUser.Id)) + .WithAllowedMentions(Mentions.None) + ); + } + else + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Failed to delete warning `{StringHelpers.Pad(warnId)}` from {targetUser.Mention}!\nPlease contact the bot author."); + } + } + } + + [ + Command("warnlookuptextcmd"), + Description("Looks up information about a warning. Shows only publicly available information."), + TextAlias("warnlookup", "warning", "warming", "waming", "wamming", "lookup", "lookylooky", "peek", "investigate", "what-did-i-do-wrong-there", "incident"), + AllowedProcessors(typeof(TextCommandProcessor)), + HomeServer + ] + public async Task WarnlookupCmd( + TextCommandContext ctx, + [Description("The user you're looking at a warning for. Accepts many formats.")] DiscordUser targetUser, + [Description("The ID of the warning you want to see")] long warnId + ) + { + UserWarning warning = GetWarning(targetUser.Id, warnId); + if (warning is null || warning.Type == WarningType.Note) + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} I couldn't find a warning for that user with that ID! Please check again."); + else + await ctx.RespondAsync(null, await FancyWarnEmbedAsync(warning, userID: targetUser.Id)); + } + + [ + Command("warndetailstextcmd"), + TextAlias("warndetails", "warninfo", "waminfo", "wamdetails", "warndetail", "wamdetail"), + Description("Check the details of a warning in depth. Shows extra information (Such as responsible Mod) that may not be wanted to be public."), + AllowedProcessors(typeof(TextCommandProcessor)), + HomeServer, + RequireHomeserverPerm(ServerPermLevel.TrialModerator) + ] + public async Task WarnDetailsCmd( + TextCommandContext ctx, + [Description("The user you're looking up detailed warn information for. Accepts many formats.")] DiscordUser targetUser, + [Description("The ID of the warning you're looking at in detail.")] long warnId + ) + { + UserWarning warning = GetWarning(targetUser.Id, warnId); + + if (warning is null) + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} I couldn't find a warning for that user with that ID! Please check again."); + else if (warning.Type == WarningType.Note) + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} That's a note, not a warning! Try using `/note details` instead, or make sure you've got the right warning ID."); + } + else + await ctx.RespondAsync(null, await FancyWarnEmbedAsync(warning, true, userID: targetUser.Id)); + + } + + [ + Command("editwarntextcmd"), + TextAlias("editwarn", "warnedit", "editwarning"), + Description("Edit the reason of an existing warning.\n" + + "The Moderator who is editing the reason will become responsible for the case."), + AllowedProcessors(typeof(TextCommandProcessor)), + HomeServer, + RequireHomeserverPerm(ServerPermLevel.TrialModerator) + ] + public async Task EditwarnCmd( + TextCommandContext ctx, + [Description("The user you're editing a warning for. Accepts many formats.")] DiscordUser targetUser, + [Description("The ID of the warning you want to edit.")] long warnId, + [RemainingText, Description("The new reason for the warning.")] string newReason) + { + if (string.IsNullOrWhiteSpace(newReason)) + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} You haven't given a new reason to set for the warning!"); + return; + } + + await ctx.RespondAsync("Processing your request..."); + var msg = await ctx.GetResponseAsync(); + var warning = GetWarning(targetUser.Id, warnId); + if (warning is null) + await msg.ModifyAsync($"{Program.cfgjson.Emoji.Error} I couldn't find a warning for that user with that ID! Please check again."); + else if (warning.Type == WarningType.Note) + { + await msg.ModifyAsync($"{Program.cfgjson.Emoji.Error} That's a note, not a warning! Try using `/note edit` instead, or make sure you've got the right warning ID."); + } + else if ((await GetPermLevelAsync(ctx.Member)) == ServerPermLevel.TrialModerator && warning.ModUserId != ctx.User.Id && warning.ModUserId != ctx.Client.CurrentUser.Id) + { + await msg.ModifyAsync($"{Program.cfgjson.Emoji.Error} {ctx.User.Mention}, as a Trial Moderator you cannot edit or delete warnings that aren't issued by you or the bot!"); + } + else + { + await EditWarning(targetUser, warnId, ctx.User, newReason); + await msg.ModifyAsync($"{Program.cfgjson.Emoji.Information} Successfully edited warning `{StringHelpers.Pad(warnId)}` (belonging to {targetUser.Mention})", + await FancyWarnEmbedAsync(GetWarning(targetUser.Id, warnId), userID: targetUser.Id)); + + await LogChannelHelper.LogMessageAsync("mod", + new DiscordMessageBuilder() + .WithContent($"{Program.cfgjson.Emoji.Information} Warning edited:" + + $"`{StringHelpers.Pad(warnId)}` (belonging to {targetUser.Mention})") + .AddEmbed(await FancyWarnEmbedAsync(GetWarning(targetUser.Id, warnId), true, userID: targetUser.Id)) + ); + } + } + + [Command("mostwarningstextcmd")] + [TextAlias("mostwarnings")] + [Description("Who has the most warnings???")] + [AllowedProcessors(typeof(TextCommandProcessor))] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] + public async Task MostWarningsCmd(TextCommandContext ctx) + { + await DiscordHelpers.SafeTyping(ctx.Channel); + + var server = Program.redis.GetServer(Program.redis.GetEndPoints()[0]); + var keys = server.Keys(); + + Dictionary counts = new(); + foreach (var key in keys) + { + if (ulong.TryParse(key.ToString(), out ulong number)) + { + counts[key.ToString()] = Program.db.HashGetAll(key).Count(x => JsonConvert.DeserializeObject(x.Value.ToString()).Type == WarningType.Warning); + } + } + + List> myList = counts.ToList(); + myList.Sort( + delegate (KeyValuePair pair1, + KeyValuePair pair2) + { + return pair1.Value.CompareTo(pair2.Value); + } + ); + + var user = await ctx.Client.GetUserAsync(Convert.ToUInt64(myList.Last().Key)); + await ctx.RespondAsync($":thinking: The user with the most warnings is **{DiscordHelpers.UniqueUsername(user)}** with a total of **{myList.Last().Value} warnings!**\nThis includes users who have left or been banned."); + } + + [Command("mostwarningsdaytextcmd")] + [TextAlias("mostwarningsday")] + [Description("Which day has the most warnings???")] + [AllowedProcessors(typeof(TextCommandProcessor))] + [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] + public async Task MostWarningsDayCmd(TextCommandContext ctx) + { + await DiscordHelpers.SafeTyping(ctx.Channel); + + var server = Program.redis.GetServer(Program.redis.GetEndPoints()[0]); + var keys = server.Keys(); + + Dictionary counts = new(); + Dictionary noAutoCounts = new(); + + foreach (var key in keys) + { + if (ulong.TryParse(key.ToString(), out ulong number)) + { + var warningsOutput = Program.db.HashGetAll(key.ToString()).ToDictionary( + x => x.Name.ToString(), + x => JsonConvert.DeserializeObject(x.Value) + ); + + foreach (var warning in warningsOutput) + { + if (warning.Value.Type != WarningType.Warning) continue; + + var day = warning.Value.WarnTimestamp.ToString("yyyy-MM-dd"); + if (!counts.ContainsKey(day)) + { + counts[day] = 1; + } + else + { + counts[day] += 1; + } + if (warning.Value.ModUserId != 159985870458322944 && warning.Value.ModUserId != Program.discord.CurrentUser.Id) + { + if (!noAutoCounts.ContainsKey(day)) + { + noAutoCounts[day] = 1; + } + else + { + noAutoCounts[day] += 1; + } + } + } + } + } + + List> countList = counts.ToList(); + countList.Sort( + delegate (KeyValuePair pair1, + KeyValuePair pair2) + { + return pair1.Value.CompareTo(pair2.Value); + } + ); + + List> noAutoCountList = noAutoCounts.ToList(); + noAutoCountList.Sort( + delegate (KeyValuePair pair1, + KeyValuePair pair2) + { + return pair1.Value.CompareTo(pair2.Value); + } + ); + + await ctx.RespondAsync($":thinking: As far as I can tell, the day with the most warnings issued was **{countList.Last().Key}** with a total of **{countList.Last().Value} warnings!**" + + $"\nExcluding automatic warnings, the most was on **{noAutoCountList.Last().Key}** with a total of **{noAutoCountList.Last().Value}** warnings!"); + } } } \ No newline at end of file diff --git a/Commands/Warnings.cs b/Commands/Warnings.cs deleted file mode 100644 index 9cae2cdc..00000000 --- a/Commands/Warnings.cs +++ /dev/null @@ -1,363 +0,0 @@ -using static Cliptok.Helpers.WarningHelpers; - -namespace Cliptok.Commands -{ - - public class Warnings - { - [ - Command("warntextcmd"), - Description("Issues a formal warning to a user."), - TextAlias("warn", "wam", "warm"), - AllowedProcessors(typeof(TextCommandProcessor)), - HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator) - ] - public async Task WarnCmd( - TextCommandContext ctx, - [Description("The user you are warning. Accepts many formats.")] DiscordUser targetUser, - [RemainingText, Description("The reason for giving this warning.")] string reason = null - ) - { - DiscordMember targetMember; - try - { - targetMember = await ctx.Guild.GetMemberAsync(targetUser.Id); - if ((await GetPermLevelAsync(ctx.Member)) == ServerPermLevel.TrialModerator && ((await GetPermLevelAsync(targetMember)) >= ServerPermLevel.TrialModerator || targetMember.IsBot)) - { - await ctx.Channel.SendMessageAsync($"{Program.cfgjson.Emoji.Error} {ctx.User.Mention}, as a Trial Moderator you cannot perform moderation actions on other staff members or bots."); - return; - } - } - catch - { - // do nothing :/ - } - - var reply = ctx.Message.ReferencedMessage; - - await ctx.Message.DeleteAsync(); - if (reason is null) - { - await ctx.Member.SendMessageAsync($"{Program.cfgjson.Emoji.Warning} Reason must be included for the warning command to work."); - return; - } - - var messageBuild = new DiscordMessageBuilder() - .WithContent($"{Program.cfgjson.Emoji.Warning} <@{targetUser.Id}> was warned: **{reason.Replace("`", "\\`").Replace("*", "\\*")}**"); - - if (reply is not null) - messageBuild.WithReply(reply.Id, true, false); - - var msg = await ctx.Channel.SendMessageAsync(messageBuild); - _ = await GiveWarningAsync(targetUser, ctx.User, reason, msg, ctx.Channel); - } - - [ - Command("anonwarntextcmd"), - TextAlias("anonwarn", "anonwam", "anonwarm"), - Description("Issues a formal warning to a user from a private channel."), - AllowedProcessors(typeof(TextCommandProcessor)), - HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator) - ] - public async Task AnonWarnCmd( - TextCommandContext ctx, - [Description("The channel you wish for the warning message to appear in.")] DiscordChannel targetChannel, - [Description("The user you are warning. Accepts many formats.")] DiscordUser targetUser, - [RemainingText, Description("The reason for giving this warning.")] string reason = null - ) - { - DiscordMember targetMember; - try - { - targetMember = await ctx.Guild.GetMemberAsync(targetUser.Id); - if ((await GetPermLevelAsync(ctx.Member)) == ServerPermLevel.TrialModerator && ((await GetPermLevelAsync(targetMember)) >= ServerPermLevel.TrialModerator || targetMember.IsBot)) - { - await ctx.Channel.SendMessageAsync($"{Program.cfgjson.Emoji.Error} {ctx.User.Mention}, as a Trial Moderator you cannot perform moderation actions on other staff members or bots."); - return; - } - } - catch - { - // do nothing :/ - } - - await ctx.Message.DeleteAsync(); - if (reason is null) - { - await ctx.Member.SendMessageAsync($"{Program.cfgjson.Emoji.Warning} Reason must be included for the warning command to work."); - return; - } - DiscordMessage msg = await targetChannel.SendMessageAsync($"{Program.cfgjson.Emoji.Warning} {targetUser.Mention} was warned: **{reason.Replace("`", "\\`").Replace("*", "\\*")}**"); - await ctx.Channel.SendMessageAsync($"{Program.cfgjson.Emoji.Warning} {targetUser.Mention} was warned in {targetChannel.Mention}: **{reason.Replace("`", "\\`").Replace("*", "\\*")}**"); - _ = await GiveWarningAsync(targetUser, ctx.User, reason, msg, ctx.Channel); - } - - [ - Command("warningstextcmd"), - TextAlias("warnings", "infractions", "warnfractions", "wammings", "wamfractions"), - Description("Shows a list of warnings that a user has been given. For more in-depth information, use the 'warnlookup' command."), - AllowedProcessors(typeof(TextCommandProcessor)), - HomeServer - ] - public async Task WarningCmd( - TextCommandContext ctx, - [Description("The user you want to look up warnings for. Accepts many formats.")] DiscordUser targetUser = null - ) - { - if (targetUser is null) - targetUser = ctx.User; - - await ctx.RespondAsync(null, await GenerateWarningsEmbedAsync(targetUser)); - } - - [ - Command("delwarntextcmd"), - TextAlias("delwarn", "delwarm", "delwam", "deletewarn", "delwarning", "deletewarning"), - Description("Delete a warning that was issued by mistake or later became invalid."), - AllowedProcessors(typeof(TextCommandProcessor)), - HomeServer, RequireHomeserverPerm(ServerPermLevel.TrialModerator) - ] - public async Task DelwarnCmd( - TextCommandContext ctx, - [Description("The user you're removing a warning from. Accepts many formats.")] DiscordUser targetUser, - [Description("The ID of the warning you want to delete.")] long warnId - ) - { - UserWarning warning = GetWarning(targetUser.Id, warnId); - if (warning is null) - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} I couldn't find a warning for that user with that ID! Please check again."); - else if (warning.Type == WarningType.Note) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} That's a note, not a warning! Try using `/note delete` instead, or make sure you've got the right warning ID."); - } - else if ((await GetPermLevelAsync(ctx.Member)) == ServerPermLevel.TrialModerator && warning.ModUserId != ctx.User.Id && warning.ModUserId != ctx.Client.CurrentUser.Id) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {ctx.User.Mention}, as a Trial Moderator you cannot edit or delete warnings that aren't issued by you or the bot!"); - } - else - { - bool success = await DelWarningAsync(warning, targetUser.Id); - if (success) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Deleted} Successfully deleted warning `{StringHelpers.Pad(warnId)}` (belonging to {targetUser.Mention})"); - - await LogChannelHelper.LogMessageAsync("mod", - new DiscordMessageBuilder() - .WithContent($"{Program.cfgjson.Emoji.Deleted} Warning deleted:" + - $"`{StringHelpers.Pad(warnId)}` (belonging to {targetUser.Mention}, deleted by {ctx.Member.Mention})") - .AddEmbed(await FancyWarnEmbedAsync(warning, true, 0xf03916, true, targetUser.Id)) - .WithAllowedMentions(Mentions.None) - ); - } - else - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Failed to delete warning `{StringHelpers.Pad(warnId)}` from {targetUser.Mention}!\nPlease contact the bot author."); - } - } - } - - [ - Command("warnlookuptextcmd"), - Description("Looks up information about a warning. Shows only publicly available information."), - TextAlias("warnlookup", "warning", "warming", "waming", "wamming", "lookup", "lookylooky", "peek", "investigate", "what-did-i-do-wrong-there", "incident"), - AllowedProcessors(typeof(TextCommandProcessor)), - HomeServer - ] - public async Task WarnlookupCmd( - TextCommandContext ctx, - [Description("The user you're looking at a warning for. Accepts many formats.")] DiscordUser targetUser, - [Description("The ID of the warning you want to see")] long warnId - ) - { - UserWarning warning = GetWarning(targetUser.Id, warnId); - if (warning is null || warning.Type == WarningType.Note) - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} I couldn't find a warning for that user with that ID! Please check again."); - else - await ctx.RespondAsync(null, await FancyWarnEmbedAsync(warning, userID: targetUser.Id)); - } - - [ - Command("warndetailstextcmd"), - TextAlias("warndetails", "warninfo", "waminfo", "wamdetails", "warndetail", "wamdetail"), - Description("Check the details of a warning in depth. Shows extra information (Such as responsible Mod) that may not be wanted to be public."), - AllowedProcessors(typeof(TextCommandProcessor)), - HomeServer, - RequireHomeserverPerm(ServerPermLevel.TrialModerator) - ] - public async Task WarnDetailsCmd( - TextCommandContext ctx, - [Description("The user you're looking up detailed warn information for. Accepts many formats.")] DiscordUser targetUser, - [Description("The ID of the warning you're looking at in detail.")] long warnId - ) - { - UserWarning warning = GetWarning(targetUser.Id, warnId); - - if (warning is null) - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} I couldn't find a warning for that user with that ID! Please check again."); - else if (warning.Type == WarningType.Note) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} That's a note, not a warning! Try using `/note details` instead, or make sure you've got the right warning ID."); - } - else - await ctx.RespondAsync(null, await FancyWarnEmbedAsync(warning, true, userID: targetUser.Id)); - - } - - [ - Command("editwarntextcmd"), - TextAlias("editwarn", "warnedit", "editwarning"), - Description("Edit the reason of an existing warning.\n" + - "The Moderator who is editing the reason will become responsible for the case."), - AllowedProcessors(typeof(TextCommandProcessor)), - HomeServer, - RequireHomeserverPerm(ServerPermLevel.TrialModerator) - ] - public async Task EditwarnCmd( - TextCommandContext ctx, - [Description("The user you're editing a warning for. Accepts many formats.")] DiscordUser targetUser, - [Description("The ID of the warning you want to edit.")] long warnId, - [RemainingText, Description("The new reason for the warning.")] string newReason) - { - if (string.IsNullOrWhiteSpace(newReason)) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} You haven't given a new reason to set for the warning!"); - return; - } - - await ctx.RespondAsync("Processing your request..."); - var msg = await ctx.GetResponseAsync(); - var warning = GetWarning(targetUser.Id, warnId); - if (warning is null) - await msg.ModifyAsync($"{Program.cfgjson.Emoji.Error} I couldn't find a warning for that user with that ID! Please check again."); - else if (warning.Type == WarningType.Note) - { - await msg.ModifyAsync($"{Program.cfgjson.Emoji.Error} That's a note, not a warning! Try using `/note edit` instead, or make sure you've got the right warning ID."); - } - else if ((await GetPermLevelAsync(ctx.Member)) == ServerPermLevel.TrialModerator && warning.ModUserId != ctx.User.Id && warning.ModUserId != ctx.Client.CurrentUser.Id) - { - await msg.ModifyAsync($"{Program.cfgjson.Emoji.Error} {ctx.User.Mention}, as a Trial Moderator you cannot edit or delete warnings that aren't issued by you or the bot!"); - } - else - { - await EditWarning(targetUser, warnId, ctx.User, newReason); - await msg.ModifyAsync($"{Program.cfgjson.Emoji.Information} Successfully edited warning `{StringHelpers.Pad(warnId)}` (belonging to {targetUser.Mention})", - await FancyWarnEmbedAsync(GetWarning(targetUser.Id, warnId), userID: targetUser.Id)); - - await LogChannelHelper.LogMessageAsync("mod", - new DiscordMessageBuilder() - .WithContent($"{Program.cfgjson.Emoji.Information} Warning edited:" + - $"`{StringHelpers.Pad(warnId)}` (belonging to {targetUser.Mention})") - .AddEmbed(await FancyWarnEmbedAsync(GetWarning(targetUser.Id, warnId), true, userID: targetUser.Id)) - ); - } - } - - [Command("mostwarningstextcmd")] - [TextAlias("mostwarnings")] - [Description("Who has the most warnings???")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public async Task MostWarningsCmd(TextCommandContext ctx) - { - await DiscordHelpers.SafeTyping(ctx.Channel); - - var server = Program.redis.GetServer(Program.redis.GetEndPoints()[0]); - var keys = server.Keys(); - - Dictionary counts = new(); - foreach (var key in keys) - { - if (ulong.TryParse(key.ToString(), out ulong number)) - { - counts[key.ToString()] = Program.db.HashGetAll(key).Count(x => JsonConvert.DeserializeObject(x.Value.ToString()).Type == WarningType.Warning); - } - } - - List> myList = counts.ToList(); - myList.Sort( - delegate (KeyValuePair pair1, - KeyValuePair pair2) - { - return pair1.Value.CompareTo(pair2.Value); - } - ); - - var user = await ctx.Client.GetUserAsync(Convert.ToUInt64(myList.Last().Key)); - await ctx.RespondAsync($":thinking: The user with the most warnings is **{DiscordHelpers.UniqueUsername(user)}** with a total of **{myList.Last().Value} warnings!**\nThis includes users who have left or been banned."); - } - - [Command("mostwarningsdaytextcmd")] - [TextAlias("mostwarningsday")] - [Description("Which day has the most warnings???")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [RequireHomeserverPerm(ServerPermLevel.TrialModerator)] - public async Task MostWarningsDayCmd(TextCommandContext ctx) - { - await DiscordHelpers.SafeTyping(ctx.Channel); - - var server = Program.redis.GetServer(Program.redis.GetEndPoints()[0]); - var keys = server.Keys(); - - Dictionary counts = new(); - Dictionary noAutoCounts = new(); - - foreach (var key in keys) - { - if (ulong.TryParse(key.ToString(), out ulong number)) - { - var warningsOutput = Program.db.HashGetAll(key.ToString()).ToDictionary( - x => x.Name.ToString(), - x => JsonConvert.DeserializeObject(x.Value) - ); - - foreach (var warning in warningsOutput) - { - if (warning.Value.Type != WarningType.Warning) continue; - - var day = warning.Value.WarnTimestamp.ToString("yyyy-MM-dd"); - if (!counts.ContainsKey(day)) - { - counts[day] = 1; - } - else - { - counts[day] += 1; - } - if (warning.Value.ModUserId != 159985870458322944 && warning.Value.ModUserId != Program.discord.CurrentUser.Id) - { - if (!noAutoCounts.ContainsKey(day)) - { - noAutoCounts[day] = 1; - } - else - { - noAutoCounts[day] += 1; - } - } - } - } - } - - List> countList = counts.ToList(); - countList.Sort( - delegate (KeyValuePair pair1, - KeyValuePair pair2) - { - return pair1.Value.CompareTo(pair2.Value); - } - ); - - List> noAutoCountList = noAutoCounts.ToList(); - noAutoCountList.Sort( - delegate (KeyValuePair pair1, - KeyValuePair pair2) - { - return pair1.Value.CompareTo(pair2.Value); - } - ); - - await ctx.RespondAsync($":thinking: As far as I can tell, the day with the most warnings issued was **{countList.Last().Key}** with a total of **{countList.Last().Value} warnings!**" + - $"\nExcluding automatic warnings, the most was on **{noAutoCountList.Last().Key}** with a total of **{noAutoCountList.Last().Value}** warnings!"); - } - } -} diff --git a/Events/InteractionEvents.cs b/Events/InteractionEvents.cs index 4a4eb492..9e7d1553 100644 --- a/Events/InteractionEvents.cs +++ b/Events/InteractionEvents.cs @@ -32,7 +32,7 @@ public static async Task ComponentInteractionCreateEvent(DiscordClient _, Compon } else if (e.Id == "clear-confirm-callback") { - Dictionary> messagesToClear = Commands.InteractionCommands.ClearInteractions.MessagesToClear; + Dictionary> messagesToClear = Commands.ClearCmds.MessagesToClear; if (!messagesToClear.ContainsKey(e.Message.Id)) { @@ -70,7 +70,7 @@ await LogChannelHelper.LogDeletedMessagesAsync( { await e.Interaction.CreateResponseAsync(DiscordInteractionResponseType.DeferredMessageUpdate); - var overridesPendingAddition = Commands.Debug.OverridesPendingAddition; + var overridesPendingAddition = Commands.DebugCmds.OverridesPendingAddition; if (!overridesPendingAddition.ContainsKey(e.Message.Id)) { await e.Channel.SendMessageAsync(new DiscordMessageBuilder().WithContent($"{cfgjson.Emoji.Error} {e.User.Mention}, this action has already been completed!").WithReply(e.Message.Id)); @@ -134,7 +134,7 @@ await LogChannelHelper.LogDeletedMessagesAsync( { await e.Interaction.CreateResponseAsync(DiscordInteractionResponseType.DeferredMessageUpdate); - var overridesPendingAddition = Commands.Debug.OverridesPendingAddition; + var overridesPendingAddition = Commands.DebugCmds.OverridesPendingAddition; if (!overridesPendingAddition.ContainsKey(e.Message.Id)) { await e.Channel.SendMessageAsync(new DiscordMessageBuilder().WithContent($"{cfgjson.Emoji.Error} {e.User.Mention}, this action has already been completed!").WithReply(e.Message.Id)); @@ -157,7 +157,7 @@ await LogChannelHelper.LogDeletedMessagesAsync( await e.Interaction.CreateResponseAsync(DiscordInteractionResponseType.DeferredMessageUpdate); - var overridesPendingAddition = Commands.Debug.OverridesPendingAddition; + var overridesPendingAddition = Commands.DebugCmds.OverridesPendingAddition; if (!overridesPendingAddition.ContainsKey(e.Message.Id)) { await e.Channel.SendMessageAsync(new DiscordMessageBuilder().WithContent($"{cfgjson.Emoji.Error} {e.User.Mention}, this action has already been completed!").WithReply(e.Message.Id)); diff --git a/Program.cs b/Program.cs index 56463cce..81df1e9e 100644 --- a/Program.cs +++ b/Program.cs @@ -177,15 +177,10 @@ static async Task Main(string[] _) { builder.CommandErrored += ErrorEvents.CommandErrored; - // Interaction commands - var slashCommandClasses = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsClass && t.Namespace == "Cliptok.Commands.InteractionCommands"); - foreach (var type in slashCommandClasses) - builder.AddCommands(type, cfgjson.ServerID); - - // Text commands + // Register commands var commandClasses = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsClass && t.Namespace == "Cliptok.Commands"); foreach (var type in commandClasses) - builder.AddCommands(type); + builder.AddCommands(type, cfgjson.ServerID); // Register command checks builder.AddCheck(); diff --git a/Tasks/ReminderTasks.cs b/Tasks/ReminderTasks.cs index 30d49ce9..aeda19dc 100644 --- a/Tasks/ReminderTasks.cs +++ b/Tasks/ReminderTasks.cs @@ -8,7 +8,7 @@ public static async Task CheckRemindersAsync() foreach (var reminder in Program.db.ListRange("reminders", 0, -1)) { bool DmFallback = false; - var reminderObject = JsonConvert.DeserializeObject(reminder); + var reminderObject = JsonConvert.DeserializeObject(reminder); if (reminderObject.ReminderTime <= DateTime.Now) { var user = await Program.discord.GetUserAsync(reminderObject.UserID); From 6ad00ac111f3d0d043c8a0090ef52206e3644cc0 Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Wed, 20 Nov 2024 10:47:48 -0500 Subject: [PATCH 09/31] Upgrade DSharpPlus to 5.0.0-nightly-02422 --- Cliptok.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cliptok.csproj b/Cliptok.csproj index a389e8c4..ea58429f 100644 --- a/Cliptok.csproj +++ b/Cliptok.csproj @@ -13,8 +13,8 @@ - - + + From 24a56a41cd59751d5ccbc9ce4442d1ec5d7b0018 Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Wed, 20 Nov 2024 21:13:52 -0500 Subject: [PATCH 10/31] Remove mistakenly-added file --- Commands/InteractionCommands/DebugInteractions.cs | 1 - 1 file changed, 1 deletion(-) delete mode 100644 Commands/InteractionCommands/DebugInteractions.cs diff --git a/Commands/InteractionCommands/DebugInteractions.cs b/Commands/InteractionCommands/DebugInteractions.cs deleted file mode 100644 index 5f282702..00000000 --- a/Commands/InteractionCommands/DebugInteractions.cs +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file From 269b2c2535e1252b058945f72c4f87ce49fb7afd Mon Sep 17 00:00:00 2001 From: Erisa A Date: Fri, 22 Nov 2024 04:40:55 +0000 Subject: [PATCH 11/31] Fix incorrect namespaces --- Commands/StatusCmds.cs | 2 +- Commands/TrackingCmds.cs | 2 +- Commands/UserNoteCmds.cs | 2 +- Commands/WarningCmds.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Commands/StatusCmds.cs b/Commands/StatusCmds.cs index 8f33262a..66758b57 100644 --- a/Commands/StatusCmds.cs +++ b/Commands/StatusCmds.cs @@ -1,4 +1,4 @@ -namespace Cliptok.Commands.InteractionCommands +namespace Cliptok.Commands { internal class StatusCmds { diff --git a/Commands/TrackingCmds.cs b/Commands/TrackingCmds.cs index 688e3024..88e6cbd6 100644 --- a/Commands/TrackingCmds.cs +++ b/Commands/TrackingCmds.cs @@ -1,4 +1,4 @@ -namespace Cliptok.Commands.InteractionCommands +namespace Cliptok.Commands { internal class TrackingCmds { diff --git a/Commands/UserNoteCmds.cs b/Commands/UserNoteCmds.cs index 55cdce6e..3b2634a3 100644 --- a/Commands/UserNoteCmds.cs +++ b/Commands/UserNoteCmds.cs @@ -1,6 +1,6 @@ using static Cliptok.Helpers.UserNoteHelpers; -namespace Cliptok.Commands.InteractionCommands +namespace Cliptok.Commands { internal class UserNoteCmds { diff --git a/Commands/WarningCmds.cs b/Commands/WarningCmds.cs index 9edc1541..89d04c52 100644 --- a/Commands/WarningCmds.cs +++ b/Commands/WarningCmds.cs @@ -1,6 +1,6 @@ using static Cliptok.Helpers.WarningHelpers; -namespace Cliptok.Commands.InteractionCommands +namespace Cliptok.Commands { internal class WarningCmds { From e88706ee2fde2c1b40f8365266128a33feffa873 Mon Sep 17 00:00:00 2001 From: Erisa A Date: Fri, 22 Nov 2024 04:41:53 +0000 Subject: [PATCH 12/31] Remove import of deleted namespace --- Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Program.cs b/Program.cs index 81df1e9e..01bf139a 100644 --- a/Program.cs +++ b/Program.cs @@ -2,7 +2,6 @@ using DSharpPlus.Net.Gateway; using Serilog.Sinks.Grafana.Loki; using System.Reflection; -using Cliptok.Commands.InteractionCommands; using DSharpPlus.Commands.Processors.TextCommands.Parsing; namespace Cliptok From 8d0a063830109965ab2ffb1cad39f61b82463cdc Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Sun, 24 Nov 2024 10:52:42 -0500 Subject: [PATCH 13/31] Restore friendlier note in command syntax error embed --- Commands/SlowmodeCmds.cs | 2 +- Events/ErrorEvents.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Commands/SlowmodeCmds.cs b/Commands/SlowmodeCmds.cs index 37e3aada..6b15256a 100644 --- a/Commands/SlowmodeCmds.cs +++ b/Commands/SlowmodeCmds.cs @@ -66,7 +66,7 @@ await ctx.RespondAsync($"{Program.cfgjson.Emoji.ClockTime} Slowmode has been set }; embed.WithFooter(Program.discord.CurrentUser.Username, Program.discord.CurrentUser.AvatarUrl) .AddField("Message", ex.Message); - if (ex is ArgumentException) + if (ex is ArgumentException or DSharpPlus.Commands.Exceptions.ArgumentParseException) embed.AddField("Note", "This usually means that you used the command incorrectly.\n" + "Please double-check how to use this command."); await ctx.RespondAsync(embed: embed.Build(), ephemeral: true).ConfigureAwait(false); diff --git a/Events/ErrorEvents.cs b/Events/ErrorEvents.cs index 1646a978..752eb243 100644 --- a/Events/ErrorEvents.cs +++ b/Events/ErrorEvents.cs @@ -79,7 +79,7 @@ await e.Context.RespondAsync( }; embed.WithFooter(discord.CurrentUser.Username, discord.CurrentUser.AvatarUrl) .AddField("Message", ex.Message); - if (e.Exception is System.ArgumentException) + if (e.Exception is System.ArgumentException or DSharpPlus.Commands.Exceptions.ArgumentParseException) embed.AddField("Note", "This usually means that you used the command incorrectly.\n" + "Please double-check how to use this command."); await e.Context.RespondAsync(embed: embed.Build()).ConfigureAwait(false); From 4ee23a24160dca1ea90a001e752801ff3c23f3b1 Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Sun, 24 Nov 2024 10:56:22 -0500 Subject: [PATCH 14/31] Use SlashCommandContext for user context commands as workaround for potential DSharpPlus bug --- Commands/FunCmds.cs | 2 +- Commands/UserNoteCmds.cs | 2 +- Commands/UtilityCmds.cs | 4 ++-- Commands/WarningCmds.cs | 2 +- Events/ErrorEvents.cs | 3 +-- Events/InteractionEvents.cs | 24 ------------------------ 6 files changed, 6 insertions(+), 31 deletions(-) diff --git a/Commands/FunCmds.cs b/Commands/FunCmds.cs index e007f3ef..2e5f54ce 100644 --- a/Commands/FunCmds.cs +++ b/Commands/FunCmds.cs @@ -7,7 +7,7 @@ public class FunCmds [Command("Hug")] [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] [AllowedProcessors(typeof(UserCommandProcessor))] - public async Task Hug(UserCommandContext ctx, DiscordUser targetUser) + public async Task Hug(SlashCommandContext ctx, DiscordUser targetUser) { var user = targetUser; diff --git a/Commands/UserNoteCmds.cs b/Commands/UserNoteCmds.cs index 3b2634a3..376a07ec 100644 --- a/Commands/UserNoteCmds.cs +++ b/Commands/UserNoteCmds.cs @@ -8,7 +8,7 @@ internal class UserNoteCmds [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] [AllowedProcessors(typeof(UserCommandProcessor))] [RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermission.ModerateMembers)] - public async Task ShowNotes(UserCommandContext ctx, DiscordUser targetUser) + public async Task ShowNotes(SlashCommandContext ctx, DiscordUser targetUser) { await ctx.RespondAsync(embed: await UserNoteHelpers.GenerateUserNotesEmbedAsync(targetUser), ephemeral: true); } diff --git a/Commands/UtilityCmds.cs b/Commands/UtilityCmds.cs index 55d1ef8d..a32686d2 100644 --- a/Commands/UtilityCmds.cs +++ b/Commands/UtilityCmds.cs @@ -5,7 +5,7 @@ public class UtilityCmds [Command("Show Avatar")] [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] [AllowedProcessors(typeof(UserCommandProcessor))] - public async Task ContextAvatar(UserCommandContext ctx, DiscordUser targetUser) + public async Task ContextAvatar(SlashCommandContext ctx, DiscordUser targetUser) { string avatarUrl = await LykosAvatarMethods.UserOrMemberAvatarURL(targetUser, ctx.Guild); @@ -24,7 +24,7 @@ public async Task ContextAvatar(UserCommandContext ctx, DiscordUser targetUser) [Command("User Information")] [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] [AllowedProcessors(typeof(UserCommandProcessor))] - public async Task ContextUserInformation(UserCommandContext ctx, DiscordUser targetUser) + public async Task ContextUserInformation(SlashCommandContext ctx, DiscordUser targetUser) { await ctx.RespondAsync(embed: await DiscordHelpers.GenerateUserEmbed(targetUser, ctx.Guild), ephemeral: true); } diff --git a/Commands/WarningCmds.cs b/Commands/WarningCmds.cs index 89d04c52..5bc3cf54 100644 --- a/Commands/WarningCmds.cs +++ b/Commands/WarningCmds.cs @@ -7,7 +7,7 @@ internal class WarningCmds [Command("Show Warnings")] [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] [AllowedProcessors(typeof(UserCommandProcessor))] - public async Task ContextWarnings(UserCommandContext ctx, DiscordUser targetUser) + public async Task ContextWarnings(SlashCommandContext ctx, DiscordUser targetUser) { await ctx.RespondAsync(embed: await WarningHelpers.GenerateWarningsEmbedAsync(targetUser), ephemeral: true); } diff --git a/Events/ErrorEvents.cs b/Events/ErrorEvents.cs index 752eb243..9d1071ba 100644 --- a/Events/ErrorEvents.cs +++ b/Events/ErrorEvents.cs @@ -21,8 +21,7 @@ public static async Task CommandErrored(CommandsExtension _, CommandErroredEvent else if (e.Context is SlashCommandContext) { // Interaction command error (slash, user ctx, message ctx) - if (e.Context is UserCommandContext) await InteractionEvents.ContextCommandErrored(e); // this works because UserCommandContext inherits from SlashCommandContext - else await InteractionEvents.SlashCommandErrored(e); + await InteractionEvents.SlashCommandErrored(e); } } diff --git a/Events/InteractionEvents.cs b/Events/InteractionEvents.cs index 9e7d1553..db1a1f60 100644 --- a/Events/InteractionEvents.cs +++ b/Events/InteractionEvents.cs @@ -230,29 +230,5 @@ await e.Context.RespondAsync(new DiscordInteractionResponseBuilder().WithContent e.Context.Client.Logger.LogError(CliptokEventID, e.Exception, "Error during invocation of interaction command {command} by {user}", e.Context.Command.Name, $"{DiscordHelpers.UniqueUsername(e.Context.User)}"); } - public static async Task ContextCommandErrored(CommandErroredEventArgs e) - { - if (e.Exception is ChecksFailedException slex) - { - foreach (var check in slex.Errors) - if (check.ContextCheckAttribute is RequireHomeserverPermAttribute att && e.Context.Command.Name != "edit") - { - var level = (await GetPermLevelAsync(e.Context.Member)); - var levelText = level.ToString(); - if (level == ServerPermLevel.Nothing && rand.Next(1, 100) == 69) - levelText = $"naught but a thing, my dear human. Congratulations, you win {rand.Next(1, 10)} bonus points."; - - await e.Context.RespondAsync( - new DiscordInteractionResponseBuilder().WithContent( - $"{cfgjson.Emoji.NoPermissions} Invalid permission level to use command **{e.Context.Command.Name}**!\n" + - $"Required: `{att.TargetLvl}`\n" + - $"You have: `{levelText}`") - .AsEphemeral(true) - ); - } - } - e.Context.Client.Logger.LogError(CliptokEventID, e.Exception, "Error during invocation of context command {command} by {user}", e.Context.Command.Name, $"{DiscordHelpers.UniqueUsername(e.Context.User)}"); - } - } } From 0220804247db2c8fb5493f38262adeaba0da2a07 Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Sun, 24 Nov 2024 14:27:30 -0500 Subject: [PATCH 15/31] Prevent !debug overrides from being registered as a slash command --- Commands/DebugCmds.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/Commands/DebugCmds.cs b/Commands/DebugCmds.cs index 9d0c8df7..d3752820 100644 --- a/Commands/DebugCmds.cs +++ b/Commands/DebugCmds.cs @@ -249,6 +249,7 @@ public async Task CheckPendingChannelEvents(TextCommandContext ctx) [Command("overrides")] [Description("Commands for managing stored permission overrides.")] + [AllowedProcessors(typeof(TextCommandProcessor))] public class Overrides { [DefaultGroupCommand] From 1dabc79999e7a120b1efaa6be2696c442e3cea2b Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Sun, 24 Nov 2024 14:31:58 -0500 Subject: [PATCH 16/31] Upgrade DSharpPlus and switch back to UserCommandContext Nightly 02426 resolves the bug that was present in DSharpPlus --- Cliptok.csproj | 4 ++-- Commands/FunCmds.cs | 2 +- Commands/UserNoteCmds.cs | 2 +- Commands/UtilityCmds.cs | 4 ++-- Commands/WarningCmds.cs | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cliptok.csproj b/Cliptok.csproj index ea58429f..96a0c0fa 100644 --- a/Cliptok.csproj +++ b/Cliptok.csproj @@ -13,8 +13,8 @@ - - + + diff --git a/Commands/FunCmds.cs b/Commands/FunCmds.cs index 2e5f54ce..e007f3ef 100644 --- a/Commands/FunCmds.cs +++ b/Commands/FunCmds.cs @@ -7,7 +7,7 @@ public class FunCmds [Command("Hug")] [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] [AllowedProcessors(typeof(UserCommandProcessor))] - public async Task Hug(SlashCommandContext ctx, DiscordUser targetUser) + public async Task Hug(UserCommandContext ctx, DiscordUser targetUser) { var user = targetUser; diff --git a/Commands/UserNoteCmds.cs b/Commands/UserNoteCmds.cs index 376a07ec..3b2634a3 100644 --- a/Commands/UserNoteCmds.cs +++ b/Commands/UserNoteCmds.cs @@ -8,7 +8,7 @@ internal class UserNoteCmds [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] [AllowedProcessors(typeof(UserCommandProcessor))] [RequireHomeserverPerm(ServerPermLevel.TrialModerator), RequirePermissions(DiscordPermission.ModerateMembers)] - public async Task ShowNotes(SlashCommandContext ctx, DiscordUser targetUser) + public async Task ShowNotes(UserCommandContext ctx, DiscordUser targetUser) { await ctx.RespondAsync(embed: await UserNoteHelpers.GenerateUserNotesEmbedAsync(targetUser), ephemeral: true); } diff --git a/Commands/UtilityCmds.cs b/Commands/UtilityCmds.cs index a32686d2..55d1ef8d 100644 --- a/Commands/UtilityCmds.cs +++ b/Commands/UtilityCmds.cs @@ -5,7 +5,7 @@ public class UtilityCmds [Command("Show Avatar")] [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] [AllowedProcessors(typeof(UserCommandProcessor))] - public async Task ContextAvatar(SlashCommandContext ctx, DiscordUser targetUser) + public async Task ContextAvatar(UserCommandContext ctx, DiscordUser targetUser) { string avatarUrl = await LykosAvatarMethods.UserOrMemberAvatarURL(targetUser, ctx.Guild); @@ -24,7 +24,7 @@ public async Task ContextAvatar(SlashCommandContext ctx, DiscordUser targetUser) [Command("User Information")] [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] [AllowedProcessors(typeof(UserCommandProcessor))] - public async Task ContextUserInformation(SlashCommandContext ctx, DiscordUser targetUser) + public async Task ContextUserInformation(UserCommandContext ctx, DiscordUser targetUser) { await ctx.RespondAsync(embed: await DiscordHelpers.GenerateUserEmbed(targetUser, ctx.Guild), ephemeral: true); } diff --git a/Commands/WarningCmds.cs b/Commands/WarningCmds.cs index 5bc3cf54..89d04c52 100644 --- a/Commands/WarningCmds.cs +++ b/Commands/WarningCmds.cs @@ -7,7 +7,7 @@ internal class WarningCmds [Command("Show Warnings")] [SlashCommandTypes(DiscordApplicationCommandType.UserContextMenu)] [AllowedProcessors(typeof(UserCommandProcessor))] - public async Task ContextWarnings(SlashCommandContext ctx, DiscordUser targetUser) + public async Task ContextWarnings(UserCommandContext ctx, DiscordUser targetUser) { await ctx.RespondAsync(embed: await WarningHelpers.GenerateWarningsEmbedAsync(targetUser), ephemeral: true); } From 18b66998eee4bb224cea1b3a15e9663b319f62c1 Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Tue, 10 Dec 2024 17:56:20 -0500 Subject: [PATCH 17/31] Fix missed merge conflict changes / adjust for DSharpPlus.Commands --- Commands/DebugCmds.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Commands/DebugCmds.cs b/Commands/DebugCmds.cs index 67498bfa..e2c52a4b 100644 --- a/Commands/DebugCmds.cs +++ b/Commands/DebugCmds.cs @@ -543,12 +543,13 @@ public async Task DumpFromDb(CommandContext ctx, } [Command("cleanup")] - [Aliases("clean", "prune")] + [TextAlias("clean", "prune")] [Description("Removes overrides from the db for channels that no longer exist.")] [IsBotOwner] public async Task CleanUpOverrides(CommandContext ctx) { - var msg = await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it..."); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it..."); + var msg = await ctx.GetResponseAsync(); var removedOverridesCount = 0; var dbOverwrites = await Program.db.HashGetAllAsync("overrides"); From dee9be8f0e6945fa8f1b2fd493e9df7add473e2d Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Tue, 17 Dec 2024 17:39:18 -0500 Subject: [PATCH 18/31] Implement a help command for text cmds & fix global cmd registration --- Commands/GlobalCmds.cs | 355 +++++++++++++++++++++++++++++++++++++++ Commands/ReminderCmds.cs | 75 --------- Commands/UtilityCmds.cs | 28 --- Events/ErrorEvents.cs | 2 +- Program.cs | 7 +- Tasks/ReminderTasks.cs | 2 +- 6 files changed, 362 insertions(+), 107 deletions(-) create mode 100644 Commands/GlobalCmds.cs delete mode 100644 Commands/ReminderCmds.cs diff --git a/Commands/GlobalCmds.cs b/Commands/GlobalCmds.cs new file mode 100644 index 00000000..45d0bf58 --- /dev/null +++ b/Commands/GlobalCmds.cs @@ -0,0 +1,355 @@ +using System.Reflection; + +namespace Cliptok.Commands +{ + public class GlobalCmds + { + // These commands will be registered outside of the home server and can be used anywhere, even in DMs. + + // Most of this is taken from DSharpPlus.CommandsNext and adapted to fit here. + // https://github.com/DSharpPlus/DSharpPlus/blob/1c1aa15/DSharpPlus.CommandsNext/CommandsNextExtension.cs#L829 + [Command("helptextcmd"), Description("Displays command help.")] + [TextAlias("help")] + [AllowedProcessors(typeof(TextCommandProcessor))] + public async Task Help(CommandContext ctx, [Description("Command to provide help for."), RemainingText] string command = "") + { + var commandSplit = command.Split(' '); + + DiscordEmbedBuilder helpEmbed = new() + { + Title = "Help", + Color = new DiscordColor("#0080ff") + }; + + IEnumerable cmds = ctx.Extension.Commands.Values.Where(cmd => + cmd.Attributes.Any(attr => attr is AllowedProcessorsAttribute apAttr + && apAttr.Processors.Contains(typeof(TextCommandProcessor)))); + + if (commandSplit.Length != 0 && commandSplit[0] != "") + { + commandSplit[0] += "textcmd"; + + Command? cmd = null; + IEnumerable? searchIn = cmds; + foreach (string c in commandSplit) + { + if (searchIn is null) + { + cmd = null; + break; + } + + StringComparison comparison = StringComparison.InvariantCultureIgnoreCase; + StringComparer comparer = StringComparer.InvariantCultureIgnoreCase; + cmd = searchIn.FirstOrDefault(xc => xc.Name.Equals(c, comparison) || ((xc.Attributes.FirstOrDefault(x => x is TextAliasAttribute) as TextAliasAttribute)?.Aliases.Contains(c.Replace("textcmd", ""), comparer) ?? false)); + + if (cmd is null) + { + break; + } + + IEnumerable failedChecks = await CheckPermissionsAsync(ctx, cmd); + if (failedChecks.Any()) + { + return; + } + + searchIn = cmd.Subcommands.Any() ? cmd.Subcommands : null; + } + + if (cmd is null) + { + throw new CommandNotFoundException(string.Join(" ", commandSplit)); + } + + helpEmbed.Description = $"`{cmd.Name.Replace("textcmd", "")}`: {cmd.Description ?? "No description provided."}"; + + + if (cmd.Subcommands.Count > 0 && cmd.Subcommands.Any(subCommand => subCommand.Attributes.Any(attr => attr is DefaultGroupCommandAttribute))) + { + helpEmbed.Description += "\n\nThis group can be executed as a standalone command."; + } + + var aliases = cmd.Method?.GetCustomAttributes().FirstOrDefault()?.Aliases ?? (cmd.Attributes.FirstOrDefault(x => x is TextAliasAttribute) as TextAliasAttribute)?.Aliases ?? null; + if (aliases is not null && aliases.Length > 1) + { + var aliasStr = ""; + foreach (var alias in aliases) + { + if (alias == cmd.Name.Replace("textcmd", "")) + continue; + + aliasStr += $"`{alias}`, "; + } + aliasStr = aliasStr.TrimEnd(',', ' '); + helpEmbed.AddField("Aliases", aliasStr); + } + + var arguments = cmd.Method?.GetParameters(); + if (arguments is not null && arguments.Length > 0) + { + var argumentsStr = $"`{cmd.Name.Replace("textcmd", "")}"; + foreach (var arg in arguments) + { + if (arg.ParameterType is CommandContext || arg.ParameterType.IsSubclassOf(typeof(CommandContext))) + continue; + + bool isCatchAll = arg.GetCustomAttribute() != null; + argumentsStr += $"{(arg.IsOptional || isCatchAll ? " [" : " <")}{arg.Name}{(isCatchAll ? "..." : "")}{(arg.IsOptional || isCatchAll ? "]" : ">")}"; + } + + argumentsStr += "`\n"; + + foreach (var arg in arguments) + { + if (arg.ParameterType is CommandContext || arg.ParameterType.IsSubclassOf(typeof(CommandContext))) + continue; + + argumentsStr += $"`{arg.Name} ({arg.ParameterType.Name})`: {arg.GetCustomAttribute()?.Description ?? "No description provided."}\n"; + } + + helpEmbed.AddField("Arguments", argumentsStr.Trim()); + } + //helpBuilder.WithCommand(cmd); + + if (cmd.Subcommands.Any()) + { + IEnumerable commandsToSearch = cmd.Subcommands; + List eligibleCommands = []; + foreach (Command? candidateCommand in commandsToSearch) + { + var executionChecks = candidateCommand.Attributes.Where(x => x is ContextCheckAttribute) as List; + + if (executionChecks == null || !executionChecks.Any()) + { + eligibleCommands.Add(candidateCommand); + continue; + } + + IEnumerable candidateFailedChecks = await CheckPermissionsAsync(ctx, candidateCommand); + if (!candidateFailedChecks.Any()) + { + eligibleCommands.Add(candidateCommand); + } + } + + if (eligibleCommands.Count != 0) + { + eligibleCommands = eligibleCommands.OrderBy(x => x.Name).ToList(); + string cmdList = ""; + foreach (var subCommand in eligibleCommands) + { + cmdList += $"`{subCommand.Name}`, "; + } + helpEmbed.AddField("Subcommands", cmdList.TrimEnd(',', ' ')); + //helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name)); + } + } + } + else + { + IEnumerable commandsToSearch = cmds; + List eligibleCommands = []; + foreach (Command? sc in commandsToSearch) + { + var executionChecks = sc.Attributes.Where(x => x is ContextCheckAttribute); + + if (!executionChecks.Any()) + { + eligibleCommands.Add(sc); + continue; + } + + IEnumerable candidateFailedChecks = await CheckPermissionsAsync(ctx, sc); + if (!candidateFailedChecks.Any()) + { + eligibleCommands.Add(sc); + } + } + + if (eligibleCommands.Count != 0) + { + eligibleCommands = eligibleCommands.OrderBy(x => x.Name).ToList(); + string cmdList = ""; + foreach (var eligibleCommand in eligibleCommands) + { + cmdList += $"`{eligibleCommand.Name.Replace("textcmd", "")}`, "; + } + helpEmbed.AddField("Commands", cmdList.TrimEnd(',', ' ')); + helpEmbed.Description = "Listing all top-level commands and groups. Specify a command to see more information."; + //helpBuilder.WithSubcommands(eligibleCommands.OrderBy(xc => xc.Name)); + } + } + + DiscordMessageBuilder builder = new DiscordMessageBuilder().AddEmbed(helpEmbed); + + await ctx.RespondAsync(builder); + } + + [Command("pingtextcmd")] + [TextAlias("ping")] + [Description("Pong? This command lets you know whether I'm working well.")] + [AllowedProcessors(typeof(TextCommandProcessor))] + public async Task Ping(TextCommandContext ctx) + { + ctx.Client.Logger.LogDebug(ctx.Client.GetConnectionLatency(Program.cfgjson.ServerID).ToString()); + DiscordMessage return_message = await ctx.Message.RespondAsync("Pinging..."); + ulong ping = (return_message.Id - ctx.Message.Id) >> 22; + char[] choices = new char[] { 'a', 'e', 'o', 'u', 'i', 'y' }; + char letter = choices[Program.rand.Next(0, choices.Length)]; + await return_message.ModifyAsync($"P{letter}ng! 🏓\n" + + $"• It took me `{ping}ms` to reply to your message!\n" + + $"• Last Websocket Heartbeat took `{Math.Round(ctx.Client.GetConnectionLatency(0).TotalMilliseconds, 0)}ms`!"); + } + + [Command("userinfo")] + [TextAlias("user-info", "whois")] + [Description("Show info about a user.")] + [AllowedProcessors(typeof(SlashCommandProcessor), typeof(TextCommandProcessor))] + public async Task UserInfoSlashCommand(CommandContext ctx, [Parameter("user"), Description("The user to retrieve information about.")] DiscordUser user = null, [Parameter("public"), Description("Whether to show the output publicly.")] bool publicMessage = false) + { + if (user is null) + user = ctx.User; + + await ctx.RespondAsync(embed: await DiscordHelpers.GenerateUserEmbed(user, ctx.Guild), ephemeral: !publicMessage); + } + + [Command("remindmetextcmd")] + [Description("Set a reminder for yourself. Example: !reminder 1h do the thing")] + [TextAlias("remindme", "reminder", "rember", "wemember", "remember", "remind")] + [AllowedProcessors(typeof(TextCommandProcessor))] + [RequireHomeserverPerm(ServerPermLevel.Tier4, WorkOutside = true)] + public async Task RemindMe( + TextCommandContext ctx, + [Description("The amount of time to wait before reminding you. For example: 2s, 5m, 1h, 1d")] string timetoParse, + [RemainingText, Description("The text to send when the reminder triggers.")] string reminder + ) + { + DateTime t = HumanDateParser.HumanDateParser.Parse(timetoParse); + if (t <= DateTime.Now) + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Time can't be in the past!"); + return; + } +#if !DEBUG + else if (t < (DateTime.Now + TimeSpan.FromSeconds(59))) + { + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Time must be at least a minute in the future!"); + return; + } +#endif + string guildId; + + if (ctx.Channel.IsPrivate) + guildId = "@me"; + else + guildId = ctx.Guild.Id.ToString(); + + var reminderObject = new Reminder() + { + UserID = ctx.User.Id, + ChannelID = ctx.Channel.Id, + MessageID = ctx.Message.Id, + MessageLink = $"https://discord.com/channels/{guildId}/{ctx.Channel.Id}/{ctx.Message.Id}", + ReminderText = reminder, + ReminderTime = t, + OriginalTime = DateTime.Now + }; + + await Program.db.ListRightPushAsync("reminders", JsonConvert.SerializeObject(reminderObject)); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} I'll try my best to remind you about that on ()"); // (In roughly **{TimeHelpers.TimeToPrettyFormat(t.Subtract(ctx.Message.Timestamp.DateTime), false)}**)"); + } + + public class Reminder + { + [JsonProperty("userID")] + public ulong UserID { get; set; } + + [JsonProperty("channelID")] + public ulong ChannelID { get; set; } + + [JsonProperty("messageID")] + public ulong MessageID { get; set; } + + [JsonProperty("messageLink")] + public string MessageLink { get; set; } + + [JsonProperty("reminderText")] + public string ReminderText { get; set; } + + [JsonProperty("reminderTime")] + public DateTime ReminderTime { get; set; } + + [JsonProperty("originalTime")] + public DateTime OriginalTime { get; set; } + } + + // Runs command context checks manually. Returns a list of failed checks. + // Unfortunately DSharpPlus.Commands does not provide a way to execute a command's context checks manually, + // so this will have to do. This may not include all checks, but it includes everything I could think of. -Milkshake + private async Task> CheckPermissionsAsync(CommandContext ctx, Command cmd) + { + var contextChecks = cmd.Attributes.Where(x => x is ContextCheckAttribute); + var failedChecks = new List(); + + foreach (var check in contextChecks) + { + if (check is HomeServerAttribute homeServerAttribute) + { + if (ctx.Channel.IsPrivate || ctx.Guild is null || ctx.Guild.Id != Program.cfgjson.ServerID) + { + failedChecks.Add(homeServerAttribute); + } + } + + if (check is RequireHomeserverPermAttribute requireHomeserverPermAttribute) + { + if (ctx.Member is null && !requireHomeserverPermAttribute.WorkOutside) + { + failedChecks.Add(requireHomeserverPermAttribute); + } + else + { + if (!requireHomeserverPermAttribute.WorkOutside) + { + var level = await GetPermLevelAsync(ctx.Member); + if (level < requireHomeserverPermAttribute.TargetLvl) + { + failedChecks.Add(requireHomeserverPermAttribute); + } + } + } + + } + + if (check is RequirePermissionsAttribute requirePermissionsAttribute) + { + if (ctx.Member is null || ctx.Guild is null + || !ctx.Channel.PermissionsFor(ctx.Member).HasAllPermissions(requirePermissionsAttribute.UserPermissions) + || !ctx.Channel.PermissionsFor(ctx.Guild.CurrentMember).HasAllPermissions(requirePermissionsAttribute.BotPermissions)) + { + failedChecks.Add(requirePermissionsAttribute); + } + } + + if (check is IsBotOwnerAttribute isBotOwnerAttribute) + { + if (!Program.cfgjson.BotOwners.Contains(ctx.User.Id)) + { + failedChecks.Add(isBotOwnerAttribute); + } + } + + if (check is UserRolesPresentAttribute userRolesPresentAttribute) + { + if (Program.cfgjson.UserRoles is null) + { + failedChecks.Add(userRolesPresentAttribute); + } + } + } + + return failedChecks; + } + } +} \ No newline at end of file diff --git a/Commands/ReminderCmds.cs b/Commands/ReminderCmds.cs deleted file mode 100644 index 2c3087d9..00000000 --- a/Commands/ReminderCmds.cs +++ /dev/null @@ -1,75 +0,0 @@ -namespace Cliptok.Commands -{ - public class ReminderCmds - { - public class Reminder - { - [JsonProperty("userID")] - public ulong UserID { get; set; } - - [JsonProperty("channelID")] - public ulong ChannelID { get; set; } - - [JsonProperty("messageID")] - public ulong MessageID { get; set; } - - [JsonProperty("messageLink")] - public string MessageLink { get; set; } - - [JsonProperty("reminderText")] - public string ReminderText { get; set; } - - [JsonProperty("reminderTime")] - public DateTime ReminderTime { get; set; } - - [JsonProperty("originalTime")] - public DateTime OriginalTime { get; set; } - } - - [Command("remindmetextcmd")] - [Description("Set a reminder for yourself. Example: !reminder 1h do the thing")] - [TextAlias("remindme", "reminder", "rember", "wemember", "remember", "remind")] - [AllowedProcessors(typeof(TextCommandProcessor))] - [RequireHomeserverPerm(ServerPermLevel.Tier4, WorkOutside = true)] - public async Task RemindMe( - TextCommandContext ctx, - [Description("The amount of time to wait before reminding you. For example: 2s, 5m, 1h, 1d")] string timetoParse, - [RemainingText, Description("The text to send when the reminder triggers.")] string reminder - ) - { - DateTime t = HumanDateParser.HumanDateParser.Parse(timetoParse); - if (t <= DateTime.Now) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Time can't be in the past!"); - return; - } -#if !DEBUG - else if (t < (DateTime.Now + TimeSpan.FromSeconds(59))) - { - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Time must be at least a minute in the future!"); - return; - } -#endif - string guildId; - - if (ctx.Channel.IsPrivate) - guildId = "@me"; - else - guildId = ctx.Guild.Id.ToString(); - - var reminderObject = new Reminder() - { - UserID = ctx.User.Id, - ChannelID = ctx.Channel.Id, - MessageID = ctx.Message.Id, - MessageLink = $"https://discord.com/channels/{guildId}/{ctx.Channel.Id}/{ctx.Message.Id}", - ReminderText = reminder, - ReminderTime = t, - OriginalTime = DateTime.Now - }; - - await Program.db.ListRightPushAsync("reminders", JsonConvert.SerializeObject(reminderObject)); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} I'll try my best to remind you about that on ()"); // (In roughly **{TimeHelpers.TimeToPrettyFormat(t.Subtract(ctx.Message.Timestamp.DateTime), false)}**)"); - } - } -} \ No newline at end of file diff --git a/Commands/UtilityCmds.cs b/Commands/UtilityCmds.cs index c6f54f16..d8b3ebc7 100644 --- a/Commands/UtilityCmds.cs +++ b/Commands/UtilityCmds.cs @@ -38,34 +38,6 @@ public async Task ContextUserInformation(UserCommandContext ctx, DiscordUser tar await ctx.RespondAsync(embed: await DiscordHelpers.GenerateUserEmbed(targetUser, ctx.Guild), ephemeral: true); } - [Command("userinfo")] - [TextAlias("user-info", "whois")] - [Description("Show info about a user.")] - [AllowedProcessors(typeof(SlashCommandProcessor), typeof(TextCommandProcessor))] - public async Task UserInfoSlashCommand(CommandContext ctx, [Parameter("user"), Description("The user to retrieve information about.")] DiscordUser user = null, [Parameter("public"), Description("Whether to show the output publicly.")] bool publicMessage = false) - { - if (user is null) - user = ctx.User; - - await ctx.RespondAsync(embed: await DiscordHelpers.GenerateUserEmbed(user, ctx.Guild), ephemeral: !publicMessage); - } - - [Command("pingtextcmd")] - [TextAlias("ping")] - [Description("Pong? This command lets you know whether I'm working well.")] - [AllowedProcessors(typeof(TextCommandProcessor))] - public async Task Ping(TextCommandContext ctx) - { - ctx.Client.Logger.LogDebug(ctx.Client.GetConnectionLatency(Program.cfgjson.ServerID).ToString()); - DiscordMessage return_message = await ctx.Message.RespondAsync("Pinging..."); - ulong ping = (return_message.Id - ctx.Message.Id) >> 22; - char[] choices = new char[] { 'a', 'e', 'o', 'u', 'i', 'y' }; - char letter = choices[Program.rand.Next(0, choices.Length)]; - await return_message.ModifyAsync($"P{letter}ng! 🏓\n" + - $"• It took me `{ping}ms` to reply to your message!\n" + - $"• Last Websocket Heartbeat took `{Math.Round(ctx.Client.GetConnectionLatency(0).TotalMilliseconds, 0)}ms`!"); - } - [Command("edittextcmd")] [TextAlias("edit")] [Description("Edit a message.")] diff --git a/Events/ErrorEvents.cs b/Events/ErrorEvents.cs index 9d1071ba..d7cfdaa7 100644 --- a/Events/ErrorEvents.cs +++ b/Events/ErrorEvents.cs @@ -77,7 +77,7 @@ await e.Context.RespondAsync( Timestamp = DateTime.UtcNow }; embed.WithFooter(discord.CurrentUser.Username, discord.CurrentUser.AvatarUrl) - .AddField("Message", ex.Message); + .AddField("Message", ex.Message.Replace("textcmd", "")); if (e.Exception is System.ArgumentException or DSharpPlus.Commands.Exceptions.ArgumentParseException) embed.AddField("Note", "This usually means that you used the command incorrectly.\n" + "Please double-check how to use this command."); diff --git a/Program.cs b/Program.cs index d07ed3c5..2f6100ed 100644 --- a/Program.cs +++ b/Program.cs @@ -177,9 +177,12 @@ static async Task Main(string[] _) builder.CommandErrored += ErrorEvents.CommandErrored; // Register commands - var commandClasses = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsClass && t.Namespace == "Cliptok.Commands"); + var commandClasses = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsClass && t.Namespace == "Cliptok.Commands" && !t.IsNested); foreach (var type in commandClasses) - builder.AddCommands(type, cfgjson.ServerID); + if (type.Name == "GlobalCmds") + builder.AddCommands(type); + else + builder.AddCommands(type, cfgjson.ServerID); // Register command checks builder.AddCheck(); diff --git a/Tasks/ReminderTasks.cs b/Tasks/ReminderTasks.cs index aeda19dc..eeb08e76 100644 --- a/Tasks/ReminderTasks.cs +++ b/Tasks/ReminderTasks.cs @@ -8,7 +8,7 @@ public static async Task CheckRemindersAsync() foreach (var reminder in Program.db.ListRange("reminders", 0, -1)) { bool DmFallback = false; - var reminderObject = JsonConvert.DeserializeObject(reminder); + var reminderObject = JsonConvert.DeserializeObject(reminder); if (reminderObject.ReminderTime <= DateTime.Now) { var user = await Program.discord.GetUserAsync(reminderObject.UserID); From 718023cfa01b785373b1421abbe1ea4741cdd77e Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Tue, 17 Dec 2024 21:11:11 -0500 Subject: [PATCH 19/31] Help: Properly filter out args of type CommandContext --- Commands/GlobalCmds.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Commands/GlobalCmds.cs b/Commands/GlobalCmds.cs index 45d0bf58..10ed366f 100644 --- a/Commands/GlobalCmds.cs +++ b/Commands/GlobalCmds.cs @@ -48,7 +48,7 @@ public async Task Help(CommandContext ctx, [Description("Command to provide help break; } - IEnumerable failedChecks = await CheckPermissionsAsync(ctx, cmd); + IEnumerable failedChecks = (await CheckPermissionsAsync(ctx, cmd)).ToList(); if (failedChecks.Any()) { return; @@ -91,7 +91,7 @@ public async Task Help(CommandContext ctx, [Description("Command to provide help var argumentsStr = $"`{cmd.Name.Replace("textcmd", "")}"; foreach (var arg in arguments) { - if (arg.ParameterType is CommandContext || arg.ParameterType.IsSubclassOf(typeof(CommandContext))) + if (arg.ParameterType == typeof(CommandContext) || arg.ParameterType.IsSubclassOf(typeof(CommandContext))) continue; bool isCatchAll = arg.GetCustomAttribute() != null; From 34769df4374744f8618b679f7d7fe5657fba4535 Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Tue, 17 Dec 2024 21:17:56 -0500 Subject: [PATCH 20/31] Help: Show perms error when user lacks perms for requested cmd --- Commands/GlobalCmds.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/Commands/GlobalCmds.cs b/Commands/GlobalCmds.cs index 10ed366f..d88b5db0 100644 --- a/Commands/GlobalCmds.cs +++ b/Commands/GlobalCmds.cs @@ -51,6 +51,22 @@ public async Task Help(CommandContext ctx, [Description("Command to provide help IEnumerable failedChecks = (await CheckPermissionsAsync(ctx, cmd)).ToList(); if (failedChecks.Any()) { + if (failedChecks.All(x => x is RequireHomeserverPermAttribute)) + { + var att = failedChecks.FirstOrDefault(x => x is RequireHomeserverPermAttribute) as RequireHomeserverPermAttribute; + if (att is not null) + { + var level = (await GetPermLevelAsync(ctx.Member)); + var levelText = level.ToString(); + if (level == ServerPermLevel.Nothing && Program.rand.Next(1, 100) == 69) + levelText = $"naught but a thing, my dear human. Congratulations, you win {Program.rand.Next(1, 10)} bonus points."; + + await ctx.RespondAsync( + $"{Program.cfgjson.Emoji.NoPermissions} Invalid permissions to use command **{cmd.Name.Replace("textcmd", "")}**!\n" + + $"Required: `{att.TargetLvl}`\nYou have: `{levelText}`"); + } + } + return; } @@ -102,7 +118,7 @@ public async Task Help(CommandContext ctx, [Description("Command to provide help foreach (var arg in arguments) { - if (arg.ParameterType is CommandContext || arg.ParameterType.IsSubclassOf(typeof(CommandContext))) + if (arg.ParameterType == typeof(CommandContext) || arg.ParameterType.IsSubclassOf(typeof(CommandContext))) continue; argumentsStr += $"`{arg.Name} ({arg.ParameterType.Name})`: {arg.GetCustomAttribute()?.Description ?? "No description provided."}\n"; From f73c93b48fff07f05cbcf562061a9f6ebbe60168 Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Tue, 17 Dec 2024 21:22:16 -0500 Subject: [PATCH 21/31] Fix command registration --- Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Program.cs b/Program.cs index 2f6100ed..f735e3dd 100644 --- a/Program.cs +++ b/Program.cs @@ -177,7 +177,7 @@ static async Task Main(string[] _) builder.CommandErrored += ErrorEvents.CommandErrored; // Register commands - var commandClasses = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsClass && t.Namespace == "Cliptok.Commands" && !t.IsNested); + var commandClasses = Assembly.GetExecutingAssembly().GetTypes().Where(t => t.IsClass && t.Namespace == "Cliptok.Commands"); foreach (var type in commandClasses) if (type.Name == "GlobalCmds") builder.AddCommands(type); From 1547e696bd8755b5aec87f99664be9c256ad2a84 Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Tue, 17 Dec 2024 21:36:26 -0500 Subject: [PATCH 22/31] Add missing returns to permission error messages --- Commands/GlobalCmds.cs | 2 ++ Events/ErrorEvents.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Commands/GlobalCmds.cs b/Commands/GlobalCmds.cs index d88b5db0..613cc0ee 100644 --- a/Commands/GlobalCmds.cs +++ b/Commands/GlobalCmds.cs @@ -64,6 +64,8 @@ public async Task Help(CommandContext ctx, [Description("Command to provide help await ctx.RespondAsync( $"{Program.cfgjson.Emoji.NoPermissions} Invalid permissions to use command **{cmd.Name.Replace("textcmd", "")}**!\n" + $"Required: `{att.TargetLvl}`\nYou have: `{levelText}`"); + + return; } } diff --git a/Events/ErrorEvents.cs b/Events/ErrorEvents.cs index d7cfdaa7..b234ec4c 100644 --- a/Events/ErrorEvents.cs +++ b/Events/ErrorEvents.cs @@ -64,6 +64,8 @@ public static async Task TextCommandErrored(CommandErroredEventArgs e) await e.Context.RespondAsync( $"{Program.cfgjson.Emoji.NoPermissions} Invalid permissions to use command **{commandName}**!\n" + $"Required: `{att.TargetLvl}`\nYou have: `{levelText}`"); + + return; } } return; From 887b069ef316fe7821729f37e679401e95e2667d Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Tue, 17 Dec 2024 21:59:38 -0500 Subject: [PATCH 23/31] Correctly show permission errors even on ArgumentParseException --- Events/ErrorEvents.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/Events/ErrorEvents.cs b/Events/ErrorEvents.cs index b234ec4c..20aa0350 100644 --- a/Events/ErrorEvents.cs +++ b/Events/ErrorEvents.cs @@ -49,6 +49,25 @@ public static async Task TextCommandErrored(CommandErroredEventArgs e) { if (ex is CommandNotFoundException && (e.Context.Command is null || commandName != "help")) return; + + // If the only exception thrown was an ArgumentParseException, run permission checks. + // If the user fails the permission checks, show a permission error instead of the ArgumentParseException. + if (ex is ArgumentParseException && exs.Count == 1) + { + var att = e.Context.Command.Attributes.FirstOrDefault(x => x is RequireHomeserverPermAttribute) as RequireHomeserverPermAttribute; + var level = (await GetPermLevelAsync(e.Context.Member)); + var levelText = level.ToString(); + if (level == ServerPermLevel.Nothing && Program.rand.Next(1, 100) == 69) + levelText = $"naught but a thing, my dear human. Congratulations, you win {Program.rand.Next(1, 10)} bonus points."; + + if (att is not null && level < att.TargetLvl) + { + await e.Context.RespondAsync( + $"{Program.cfgjson.Emoji.NoPermissions} Invalid permissions to use command **{commandName}**!\n" + + $"Required: `{att.TargetLvl}`\nYou have: `{levelText}`"); + return; + } + } if (ex is ChecksFailedException cfex && (commandName != "help")) { From e8dc35c94cb705e2139a42959f7a4ebbaf938972 Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Tue, 17 Dec 2024 23:35:43 -0500 Subject: [PATCH 24/31] Fix permission checks Show correct command names & permissions in RequireHomeserverPerm failure messages, properly honor OwnerOverride --- Commands/GlobalCmds.cs | 49 ++++++++++++++++++++++++------------------ Events/ErrorEvents.cs | 11 ++++++++-- 2 files changed, 37 insertions(+), 23 deletions(-) diff --git a/Commands/GlobalCmds.cs b/Commands/GlobalCmds.cs index 613cc0ee..c02e67b0 100644 --- a/Commands/GlobalCmds.cs +++ b/Commands/GlobalCmds.cs @@ -31,7 +31,7 @@ public async Task Help(CommandContext ctx, [Description("Command to provide help Command? cmd = null; IEnumerable? searchIn = cmds; - foreach (string c in commandSplit) + for (int i = 0; i < commandSplit.Length; i++) { if (searchIn is null) { @@ -41,35 +41,41 @@ public async Task Help(CommandContext ctx, [Description("Command to provide help StringComparison comparison = StringComparison.InvariantCultureIgnoreCase; StringComparer comparer = StringComparer.InvariantCultureIgnoreCase; - cmd = searchIn.FirstOrDefault(xc => xc.Name.Equals(c, comparison) || ((xc.Attributes.FirstOrDefault(x => x is TextAliasAttribute) as TextAliasAttribute)?.Aliases.Contains(c.Replace("textcmd", ""), comparer) ?? false)); + cmd = searchIn.FirstOrDefault(xc => xc.Name.Equals(commandSplit[i], comparison) || ((xc.Attributes.FirstOrDefault(x => x is TextAliasAttribute) as TextAliasAttribute)?.Aliases.Contains(commandSplit[i].Replace("textcmd", ""), comparer) ?? false)); if (cmd is null) { break; } - IEnumerable failedChecks = (await CheckPermissionsAsync(ctx, cmd)).ToList(); - if (failedChecks.Any()) + // Only run checks on the last command in the chain. + // So if we are looking at a command group here, only run checks against the actual command, + // not the group(s) it's under. + if (i == commandSplit.Length - 1) { - if (failedChecks.All(x => x is RequireHomeserverPermAttribute)) + IEnumerable failedChecks = (await CheckPermissionsAsync(ctx, cmd)).ToList(); + if (failedChecks.Any()) { - var att = failedChecks.FirstOrDefault(x => x is RequireHomeserverPermAttribute) as RequireHomeserverPermAttribute; - if (att is not null) + if (failedChecks.All(x => x is RequireHomeserverPermAttribute)) { - var level = (await GetPermLevelAsync(ctx.Member)); - var levelText = level.ToString(); - if (level == ServerPermLevel.Nothing && Program.rand.Next(1, 100) == 69) - levelText = $"naught but a thing, my dear human. Congratulations, you win {Program.rand.Next(1, 10)} bonus points."; + var att = failedChecks.FirstOrDefault(x => x is RequireHomeserverPermAttribute) as RequireHomeserverPermAttribute; + if (att is not null) + { + var level = (await GetPermLevelAsync(ctx.Member)); + var levelText = level.ToString(); + if (level == ServerPermLevel.Nothing && Program.rand.Next(1, 100) == 69) + levelText = $"naught but a thing, my dear human. Congratulations, you win {Program.rand.Next(1, 10)} bonus points."; - await ctx.RespondAsync( - $"{Program.cfgjson.Emoji.NoPermissions} Invalid permissions to use command **{cmd.Name.Replace("textcmd", "")}**!\n" + - $"Required: `{att.TargetLvl}`\nYou have: `{levelText}`"); + await ctx.RespondAsync( + $"{Program.cfgjson.Emoji.NoPermissions} Invalid permissions to use command **{command.Replace("textcmd", "")}**!\n" + + $"Required: `{att.TargetLvl}`\nYou have: `{levelText}`"); - return; + return; + } } - } - return; + return; + } } searchIn = cmd.Subcommands.Any() ? cmd.Subcommands : null; @@ -322,22 +328,23 @@ private async Task> CheckPermissionsAsync(Com if (check is RequireHomeserverPermAttribute requireHomeserverPermAttribute) { + // Fail if guild member is null but this cmd does not work outside of the home server if (ctx.Member is null && !requireHomeserverPermAttribute.WorkOutside) { failedChecks.Add(requireHomeserverPermAttribute); } else { - if (!requireHomeserverPermAttribute.WorkOutside) + var level = await GetPermLevelAsync(ctx.Member); + if (level < requireHomeserverPermAttribute.TargetLvl) { - var level = await GetPermLevelAsync(ctx.Member); - if (level < requireHomeserverPermAttribute.TargetLvl) + if (requireHomeserverPermAttribute.OwnerOverride && !Program.cfgjson.BotOwners.Contains(ctx.User.Id) + || !requireHomeserverPermAttribute.OwnerOverride) { failedChecks.Add(requireHomeserverPermAttribute); } } } - } if (check is RequirePermissionsAttribute requirePermissionsAttribute) diff --git a/Events/ErrorEvents.cs b/Events/ErrorEvents.cs index 20aa0350..2fd1dc8d 100644 --- a/Events/ErrorEvents.cs +++ b/Events/ErrorEvents.cs @@ -71,10 +71,16 @@ await e.Context.RespondAsync( if (ex is ChecksFailedException cfex && (commandName != "help")) { - foreach (var check in cfex.Errors) + // Iterate over RequireHomeserverPermAttribute failures. + // Only evaluate the last one, so that if we are looking at a command in a group (say, debug shutdown), + // we only evaluate against permissions for the command (shutdown) instead of the group (debug) in case they differ. + var permErrIndex = 1; + foreach(var permErr in cfex.Errors.Where(x => x.ContextCheckAttribute is RequireHomeserverPermAttribute)) { - if (check.ContextCheckAttribute is RequireHomeserverPermAttribute att) + // Only evaluate the last failed RequireHomeserverPermAttribute + if (permErrIndex == cfex.Errors.Count(x => x.ContextCheckAttribute is RequireHomeserverPermAttribute)) { + var att = permErr.ContextCheckAttribute as RequireHomeserverPermAttribute; var level = (await GetPermLevelAsync(e.Context.Member)); var levelText = level.ToString(); if (level == ServerPermLevel.Nothing && Program.rand.Next(1, 100) == 69) @@ -86,6 +92,7 @@ await e.Context.RespondAsync( return; } + permErrIndex++; } return; } From 1ae753c80cd620013966bdd5a034781fdfb2c092 Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Thu, 2 Jan 2025 15:15:28 -0500 Subject: [PATCH 25/31] Lockdown: Only try to FollowUpAsync when slash cmd is used --- Commands/LockdownCmds.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Commands/LockdownCmds.cs b/Commands/LockdownCmds.cs index fd3babe0..051010ad 100644 --- a/Commands/LockdownCmds.cs +++ b/Commands/LockdownCmds.cs @@ -82,11 +82,13 @@ await thread.ModifyAsync(a => try { await LockdownHelpers.LockChannelAsync(user: ctx.User, channel: currentChannel, duration: lockDuration, reason: reason, lockThreads: lockThreads); - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Channel locked successfully.").AsEphemeral(true)); + if (ctx is SlashCommandContext) + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Channel locked successfully.").AsEphemeral(true)); } catch (ArgumentException) { - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Failed to lock this channel!").AsEphemeral(true)); + if (ctx is SlashCommandContext) + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Failed to lock this channel!").AsEphemeral(true)); } } @@ -175,11 +177,13 @@ public class UnlockCmds try { await LockdownHelpers.UnlockChannel(currentChannel, ctx.Member); - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Channel locked successfully.").AsEphemeral(true)); + if (ctx is SlashCommandContext) + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Channel locked successfully.").AsEphemeral(true)); } catch (ArgumentException) { - await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Failed to lock this channel!").AsEphemeral(true)); + if (ctx is SlashCommandContext) + await ctx.FollowupAsync(new DiscordFollowupMessageBuilder().WithContent("Failed to lock this channel!").AsEphemeral(true)); } } From 15f5e94c8f275ecd66c2cdf8ede8876c61f07945 Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Thu, 2 Jan 2025 15:16:50 -0500 Subject: [PATCH 26/31] Fix flaws in help handler Fixes aliases not showing for some commands + help failing to show for commands that are not suffixed with 'textcmd' --- Commands/GlobalCmds.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Commands/GlobalCmds.cs b/Commands/GlobalCmds.cs index c02e67b0..4864bee6 100644 --- a/Commands/GlobalCmds.cs +++ b/Commands/GlobalCmds.cs @@ -41,7 +41,7 @@ public async Task Help(CommandContext ctx, [Description("Command to provide help StringComparison comparison = StringComparison.InvariantCultureIgnoreCase; StringComparer comparer = StringComparer.InvariantCultureIgnoreCase; - cmd = searchIn.FirstOrDefault(xc => xc.Name.Equals(commandSplit[i], comparison) || ((xc.Attributes.FirstOrDefault(x => x is TextAliasAttribute) as TextAliasAttribute)?.Aliases.Contains(commandSplit[i].Replace("textcmd", ""), comparer) ?? false)); + cmd = searchIn.FirstOrDefault(xc => xc.Name.Equals(commandSplit[i], comparison) || xc.Name.Equals(commandSplit[i].Replace("textcmd", ""), comparison) || ((xc.Attributes.FirstOrDefault(x => x is TextAliasAttribute) as TextAliasAttribute)?.Aliases.Contains(commandSplit[i].Replace("textcmd", ""), comparer) ?? false)); if (cmd is null) { @@ -95,7 +95,7 @@ await ctx.RespondAsync( } var aliases = cmd.Method?.GetCustomAttributes().FirstOrDefault()?.Aliases ?? (cmd.Attributes.FirstOrDefault(x => x is TextAliasAttribute) as TextAliasAttribute)?.Aliases ?? null; - if (aliases is not null && aliases.Length > 1) + if (aliases is not null && (aliases.Length > 1 || (aliases.Length == 1 && aliases[0] != cmd.Name.Replace("textcmd", "")))) { var aliasStr = ""; foreach (var alias in aliases) From 67bf5b9adfb6863f933091a62aad29361fb4886a Mon Sep 17 00:00:00 2001 From: Erisa A Date: Sun, 12 Jan 2025 20:13:28 +0000 Subject: [PATCH 27/31] fix inverted error msgs in security actions --- Commands/SecurityActionsCmds.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Commands/SecurityActionsCmds.cs b/Commands/SecurityActionsCmds.cs index 7bbaa901..cbf715be 100644 --- a/Commands/SecurityActionsCmds.cs +++ b/Commands/SecurityActionsCmds.cs @@ -48,7 +48,7 @@ public async Task SlashPauseDMs(CommandContext ctx, [Parameter("time"), Descript else { ctx.Client.Logger.LogError("Failed to set Security Actions.\nPayload: {payload}\nResponse: {statuscode} {body}", newSecurityActions.ToString(), (int)setActionsResponse.StatusCode, await setActionsResponse.Content.ReadAsStringAsync()); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Something went wrong and I wasn't able to unpause DMs! Discord returned status code `{setActionsResponse.StatusCode}`."); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Something went wrong and I wasn't able to pause DMs! Discord returned status code `{setActionsResponse.StatusCode}`."); } } @@ -97,7 +97,7 @@ public async Task SlashUnpauseDMs(CommandContext ctx) else { ctx.Client.Logger.LogError("Failed to set Security Actions.\nPayload: {payload}\nResponse: {statuscode} {body}", newSecurityActions.ToString(), (int)setActionsResponse.StatusCode, await setActionsResponse.Content.ReadAsStringAsync()); - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Something went wrong and I wasn't able to pause DMs! Discord returned status code `{setActionsResponse.StatusCode}`."); + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} Something went wrong and I wasn't able to unpause DMs! Discord returned status code `{setActionsResponse.StatusCode}`."); } } } From bc1e2cac22d62639bef83a3993d713493940c866 Mon Sep 17 00:00:00 2001 From: Erisa A Date: Sun, 12 Jan 2025 21:09:13 +0000 Subject: [PATCH 28/31] fix command permission checks in DMs --- CommandChecks/HomeServerPerms.cs | 2 +- Commands/GlobalCmds.cs | 25 ++++++++++++++++++++++--- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/CommandChecks/HomeServerPerms.cs b/CommandChecks/HomeServerPerms.cs index 25d8a509..6d77158c 100644 --- a/CommandChecks/HomeServerPerms.cs +++ b/CommandChecks/HomeServerPerms.cs @@ -25,7 +25,7 @@ public enum ServerPermLevel public static async Task GetPermLevelAsync(DiscordMember target) { - if (target.Guild.Id != Program.cfgjson.ServerID) + if (target is null || target.Guild is null || target.Guild.Id != Program.cfgjson.ServerID) return ServerPermLevel.Nothing; // Torch approved of this. diff --git a/Commands/GlobalCmds.cs b/Commands/GlobalCmds.cs index 4864bee6..0f01ac1d 100644 --- a/Commands/GlobalCmds.cs +++ b/Commands/GlobalCmds.cs @@ -316,6 +316,25 @@ private async Task> CheckPermissionsAsync(Com var contextChecks = cmd.Attributes.Where(x => x is ContextCheckAttribute); var failedChecks = new List(); + // similar to home server perm check logic + DiscordMember member = null; + if (ctx.Channel.IsPrivate || ctx.Guild.Id != Program.cfgjson.ServerID) + { + var guild = await ctx.Client.GetGuildAsync(Program.cfgjson.ServerID); + try + { + member = await guild.GetMemberAsync(ctx.User.Id); + } + catch (DSharpPlus.Exceptions.NotFoundException) + { + // member is null, remember this for later + } + } + else + { + member = ctx.Member; + } + foreach (var check in contextChecks) { if (check is HomeServerAttribute homeServerAttribute) @@ -329,13 +348,13 @@ private async Task> CheckPermissionsAsync(Com if (check is RequireHomeserverPermAttribute requireHomeserverPermAttribute) { // Fail if guild member is null but this cmd does not work outside of the home server - if (ctx.Member is null && !requireHomeserverPermAttribute.WorkOutside) + if (member is null && !requireHomeserverPermAttribute.WorkOutside) { failedChecks.Add(requireHomeserverPermAttribute); } else { - var level = await GetPermLevelAsync(ctx.Member); + var level = await GetPermLevelAsync(member); if (level < requireHomeserverPermAttribute.TargetLvl) { if (requireHomeserverPermAttribute.OwnerOverride && !Program.cfgjson.BotOwners.Contains(ctx.User.Id) @@ -349,7 +368,7 @@ private async Task> CheckPermissionsAsync(Com if (check is RequirePermissionsAttribute requirePermissionsAttribute) { - if (ctx.Member is null || ctx.Guild is null + if (member is null || ctx.Guild is null || !ctx.Channel.PermissionsFor(ctx.Member).HasAllPermissions(requirePermissionsAttribute.UserPermissions) || !ctx.Channel.PermissionsFor(ctx.Guild.CurrentMember).HasAllPermissions(requirePermissionsAttribute.BotPermissions)) { From 39ef1d3a6ff85675367de3b5974eff193ee04a41 Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Sun, 12 Jan 2025 19:26:08 -0500 Subject: [PATCH 29/31] Move 'overrides' group out of 'debug', make 'overrides dump' text-only --- Commands/DebugCmds.cs | 143 ++++++++++++++++++++++-------------------- 1 file changed, 75 insertions(+), 68 deletions(-) diff --git a/Commands/DebugCmds.cs b/Commands/DebugCmds.cs index 5dab896f..82b021d7 100644 --- a/Commands/DebugCmds.cs +++ b/Commands/DebugCmds.cs @@ -247,9 +247,79 @@ public async Task CheckPendingChannelEvents(TextCommandContext ctx) await ctx.RespondAsync(await StringHelpers.CodeOrHasteBinAsync(list)); } - [Command("overrides")] + [Command("dmchannel")] + [Description("Create or find a DM channel ID for a user.")] + [IsBotOwner] + public async Task GetDMChannel(TextCommandContext ctx, DiscordUser user) + { + var dmChannel = await user.CreateDmChannelAsync(); + await ctx.RespondAsync(dmChannel.Id.ToString()); + } + + [Command("dumpdmchannels")] + [Description("Dump all DM channels")] + [IsBotOwner] + public async Task DumpDMChannels(TextCommandContext ctx) + { + var dmChannels = ctx.Client.PrivateChannels; + + var json = JsonConvert.SerializeObject(dmChannels, Formatting.Indented); + + await ctx.RespondAsync(await StringHelpers.CodeOrHasteBinAsync(json, "json")); + } + + [Command("searchmembers")] + [Description("Search member list with a regex. Restricted to bot owners bc regexes are scary.")] + [IsBotOwner] + public async Task SearchMembersCmd(TextCommandContext ctx, string regex) + { + var rx = new Regex(regex); + + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it. This will take a while."); + var msg = await ctx.GetResponseAsync(); + var discordMembers = await ctx.Guild.GetAllMembersAsync().ToListAsync(); + + var matchedMembers = discordMembers.Where(discordMember => discordMember.Username is not null && rx.IsMatch(discordMember.Username)).ToList(); + + Dictionary memberIdsTonames = matchedMembers.Select(member => new KeyValuePair(member.Id, member.Username)).ToDictionary(x => x.Key, x => x.Value); + + _ = msg.DeleteAsync(); + await ctx.Channel.SendMessageAsync(await StringHelpers.CodeOrHasteBinAsync(JsonConvert.SerializeObject(memberIdsTonames, Formatting.Indented), "json")); + } + + [Command("testnre")] + [Description("throw a System.NullReferenceException error. dont spam this please.")] + [IsBotOwner] + public async Task ThrowNRE(TextCommandContext ctx, bool catchAsWarning = false) + { + if (catchAsWarning) + { + try + { + throw new NullReferenceException(); + } + catch (NullReferenceException e) + { + ctx.Client.Logger.LogWarning(e, "logging test NRE as warning"); + await ctx.RespondAsync("thrown NRE and logged as warning, check logs"); + } + } + else + { + throw new NullReferenceException(); + } + } + + } + + class OverridesCmd + { + // This is outside of the debug class/group to avoid issues caused by DSP.Commands that are out of our control + [Command("debugoverrides")] + [TextAlias("overrides")] [Description("Commands for managing stored permission overrides.")] [AllowedProcessors(typeof(TextCommandProcessor))] + [HomeServer, RequireHomeserverPerm(ServerPermLevel.Moderator)] public class Overrides { [DefaultGroupCommand] @@ -488,12 +558,13 @@ public async Task Apply(TextCommandContext ctx, [Command("dump")] [Description("Dump all of a channel's overrides from Discord or the database.")] [IsBotOwner] + [AllowedProcessors(typeof(TextCommandProcessor))] public class DumpChannelOverrides { [DefaultGroupCommand] [Command("discord")] [Description("Dump all of a channel's overrides as they exist on the Discord channel. Does not read from db.")] - public async Task DumpFromDiscord(CommandContext ctx, + public async Task DumpFromDiscord(TextCommandContext ctx, [Description("The channel to dump overrides for.")] DiscordChannel channel) { var overwrites = channel.PermissionOverwrites; @@ -510,7 +581,7 @@ public async Task DumpFromDiscord(CommandContext ctx, [Command("db")] [TextAlias("database")] [Description("Dump all of a channel's overrides as they are stored in the db.")] - public async Task DumpFromDb(CommandContext ctx, + public async Task DumpFromDb(TextCommandContext ctx, [Description("The channel to dump overrides for.")] DiscordChannel channel) { List overwrites = new(); @@ -592,70 +663,7 @@ public async Task CleanUpOverrides(CommandContext ctx) await msg.ModifyAsync($"{Program.cfgjson.Emoji.Success} Done! Cleaned up {removedOverridesCount} overrides."); } } - - [Command("dmchannel")] - [Description("Create or find a DM channel ID for a user.")] - [IsBotOwner] - public async Task GetDMChannel(TextCommandContext ctx, DiscordUser user) - { - var dmChannel = await user.CreateDmChannelAsync(); - await ctx.RespondAsync(dmChannel.Id.ToString()); - } - - [Command("dumpdmchannels")] - [Description("Dump all DM channels")] - [IsBotOwner] - public async Task DumpDMChannels(TextCommandContext ctx) - { - var dmChannels = ctx.Client.PrivateChannels; - - var json = JsonConvert.SerializeObject(dmChannels, Formatting.Indented); - - await ctx.RespondAsync(await StringHelpers.CodeOrHasteBinAsync(json, "json")); - } - - [Command("searchmembers")] - [Description("Search member list with a regex. Restricted to bot owners bc regexes are scary.")] - [IsBotOwner] - public async Task SearchMembersCmd(TextCommandContext ctx, string regex) - { - var rx = new Regex(regex); - - await ctx.RespondAsync($"{Program.cfgjson.Emoji.Loading} Working on it. This will take a while."); - var msg = await ctx.GetResponseAsync(); - var discordMembers = await ctx.Guild.GetAllMembersAsync().ToListAsync(); - - var matchedMembers = discordMembers.Where(discordMember => discordMember.Username is not null && rx.IsMatch(discordMember.Username)).ToList(); - - Dictionary memberIdsTonames = matchedMembers.Select(member => new KeyValuePair(member.Id, member.Username)).ToDictionary(x => x.Key, x => x.Value); - - _ = msg.DeleteAsync(); - await ctx.Channel.SendMessageAsync(await StringHelpers.CodeOrHasteBinAsync(JsonConvert.SerializeObject(memberIdsTonames, Formatting.Indented), "json")); - } - - [Command("testnre")] - [Description("throw a System.NullReferenceException error. dont spam this please.")] - [IsBotOwner] - public async Task ThrowNRE(TextCommandContext ctx, bool catchAsWarning = false) - { - if (catchAsWarning) - { - try - { - throw new NullReferenceException(); - } - catch (NullReferenceException e) - { - ctx.Client.Logger.LogWarning(e, "logging test NRE as warning"); - await ctx.RespondAsync("thrown NRE and logged as warning, check logs"); - } - } - else - { - throw new NullReferenceException(); - } - } - + private static async Task<(bool success, ulong failedOverwrite)> ImportOverridesFromChannelAsync(DiscordChannel channel) { // Imports overrides from the specified channel to the database. See 'debug overrides import' and 'debug overrides importall' @@ -701,7 +709,6 @@ await Program.db.HashSetAsync("overrides", overwrite.Id.ToString(), return (true, 0); } - } } } \ No newline at end of file From ef70e6502a30f889953587902e8e0a53ac01cffe Mon Sep 17 00:00:00 2001 From: FloatingMilkshake Date: Sun, 12 Jan 2025 21:12:48 -0500 Subject: [PATCH 30/31] Properly hide subcommands from help that the user lacks perms for --- Commands/GlobalCmds.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Commands/GlobalCmds.cs b/Commands/GlobalCmds.cs index 0f01ac1d..0a5f1493 100644 --- a/Commands/GlobalCmds.cs +++ b/Commands/GlobalCmds.cs @@ -142,7 +142,7 @@ await ctx.RespondAsync( List eligibleCommands = []; foreach (Command? candidateCommand in commandsToSearch) { - var executionChecks = candidateCommand.Attributes.Where(x => x is ContextCheckAttribute) as List; + var executionChecks = candidateCommand.Attributes.Where(x => x is ContextCheckAttribute); if (executionChecks == null || !executionChecks.Any()) { From db7b569a49712ea720bea11c8b8f963d8b614c96 Mon Sep 17 00:00:00 2001 From: Erisa A Date: Mon, 13 Jan 2025 04:05:49 +0000 Subject: [PATCH 31/31] fix !help outside server showing extra commands it shouldnt --- Commands/GlobalCmds.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Commands/GlobalCmds.cs b/Commands/GlobalCmds.cs index 0a5f1493..02ee7a5d 100644 --- a/Commands/GlobalCmds.cs +++ b/Commands/GlobalCmds.cs @@ -337,18 +337,20 @@ private async Task> CheckPermissionsAsync(Com foreach (var check in contextChecks) { - if (check is HomeServerAttribute homeServerAttribute) + // if command requiring homes erver is used outside, fail the check + if (check is HomeServerAttribute homeServerAttribute + && (ctx.Channel.IsPrivate || ctx.Guild is null || ctx.Guild.Id != Program.cfgjson.ServerID) + ) { - if (ctx.Channel.IsPrivate || ctx.Guild is null || ctx.Guild.Id != Program.cfgjson.ServerID) - { - failedChecks.Add(homeServerAttribute); - } + failedChecks.Add(homeServerAttribute); } if (check is RequireHomeserverPermAttribute requireHomeserverPermAttribute) { - // Fail if guild member is null but this cmd does not work outside of the home server - if (member is null && !requireHomeserverPermAttribute.WorkOutside) + // Fail if guild is wrong but this command does not work outside of the home server + if ( + (ctx.Channel.IsPrivate || ctx.Guild is null || ctx.Guild.Id != Program.cfgjson.ServerID) + && !requireHomeserverPermAttribute.WorkOutside) { failedChecks.Add(requireHomeserverPermAttribute); }