diff --git a/Commands/MuteCmds.cs b/Commands/MuteCmds.cs index 3d2b9a88..e12517ad 100644 --- a/Commands/MuteCmds.cs +++ b/Commands/MuteCmds.cs @@ -202,6 +202,89 @@ public async Task TqsMuteSlashCommand( 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("tqsunmute")] + [TextAlias("tqs-unmute", "untqsmute")] + [Description("Removes a TQS Mute from a previously TQS-muted user. See also: tqsmute")] + [AllowedProcessors(typeof(TextCommandProcessor), typeof(SlashCommandProcessor))] + [HomeServer, RequireHomeserverPerm(ServerPermLevel.TechnicalQueriesSlayer)] + public async Task TqsUnmuteCmd(CommandContext ctx, [Parameter("user"), Description("The user you're trying to unmute.")] DiscordUser targetUser, [Description("The reason for the unmute.")] string reason) + { + if (ctx is SlashCommandContext) + await ctx.As().DeferResponseAsync(); + + // 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 + #bot-commands + 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 && + ctx.Channel.Id != Program.cfgjson.BotCommandsChannel) + { + 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}>, their threads, and <#{Program.cfgjson.BotCommandsChannel}>!"); + return; + } + + // Get muted roles + 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) + { + // couldn't fetch member, fail + if (ctx is SlashCommandContext) + await ctx.EditResponseAsync($"{Program.cfgjson.Emoji.Error} That user doesn't appear to be in the server!"); + else + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} That user doesn't appear to be in the server!"); + return; + } + + if (await Program.db.HashExistsAsync("mutes", targetUser.Id) && targetMember is not null && targetMember.Roles.Contains(tqsMutedRole)) + { + // If the member has a regular mute, leave the TQS mute alone (it's only a role now & it has no effect if they also have Muted); it will be removed when they are unmuted + if (targetMember.Roles.Contains(mutedRole)) + { + if (ctx is SlashCommandContext) + await ctx.EditResponseAsync($"{Program.cfgjson.Emoji.Error} {targetUser.Mention} has been muted by a Moderator! Their TQS Mute will be removed when the Moderator-issued mute expires."); + else + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} {targetUser.Mention} has been muted by a Moderator! Their TQS Mute will be removed when the Moderator-issued mute expires."); + return; + } + + // user is TQS-muted; unmute + await MuteHelpers.UnmuteUserAsync(targetUser, reason, true, ctx.User, true); + if (ctx is SlashCommandContext) + await ctx.EditResponseAsync($"{Program.cfgjson.Emoji.Success} Successfully unmuted {targetUser.Mention}!"); + else + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Success} Successfully unmuted {targetUser.Mention}!"); + } + else + { + // member is not TQS-muted, fail + if (ctx is SlashCommandContext) + await ctx.EditResponseAsync($"{Program.cfgjson.Emoji.Error} That user doesn't appear to be TQS-muted!"); + else + await ctx.RespondAsync($"{Program.cfgjson.Emoji.Error} That user doesn't appear to be TQS-muted!"); + } + } [Command("muteinfo")] [Description("Show information about the mute for a user.")] @@ -223,8 +306,6 @@ public async Task MuteInfoSlashCommand( [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; diff --git a/Helpers/MuteHelpers.cs b/Helpers/MuteHelpers.cs index 22272345..5f69031c 100644 --- a/Helpers/MuteHelpers.cs +++ b/Helpers/MuteHelpers.cs @@ -319,8 +319,12 @@ public static (int MuteHours, int WarnsSinceThreshold) GetHoursToMuteFor(Diction return output; } - public static async Task UnmuteUserAsync(DiscordUser targetUser, string reason = "", bool manual = true, DiscordUser modUser = default) + public static async Task UnmuteUserAsync(DiscordUser targetUser, string reason = "", bool manual = true, DiscordUser modUser = default, bool isTqsUnmute = false) { + var auditLogReason = reason; + if (manual && modUser is not null) + auditLogReason = $"[Manual {(isTqsUnmute ? "TQS " : "")}unmute by {DiscordHelpers.UniqueUsername(modUser)}]: {reason}"; + var muteDetailsJson = await Program.db.HashGetAsync("mutes", targetUser.Id); bool success = false; bool wasTqsMute = false; @@ -346,7 +350,7 @@ public static async Task UnmuteUserAsync(DiscordUser targetUser, string re { await LogChannelHelper.LogMessageAsync("mod", new DiscordMessageBuilder() - .WithContent($"{Program.cfgjson.Emoji.Information} Attempt to remove Muted role from {targetUser.Mention} failed because the user could not be found.\nThis is expected if the user was banned or left.") + .WithContent($"{Program.cfgjson.Emoji.Information} Attempt to remove {(isTqsUnmute ? "TQS " : "")}Muted role from {targetUser.Mention} failed because the user could not be found.\nThis is expected if the user was banned or left.") .WithAllowedMentions(Mentions.None) ); } @@ -361,29 +365,33 @@ await LogChannelHelper.LogMessageAsync("mod", // If both attempts fail, do standard failure error handling. try { - await member.RevokeRoleAsync(role: mutedRole, reason); + await member.RevokeRoleAsync(role: mutedRole, auditLogReason); } finally { // Check member roles for TQS mute role if (member.Roles.Contains(tqsMutedRole)) { - await member.RevokeRoleAsync(role: tqsMutedRole, reason); + await member.RevokeRoleAsync(role: tqsMutedRole, auditLogReason); wasTqsMute = true; // only true if TQS mute role was found & removed } } - foreach (var role in member.Roles) + // Skip if not TQS unmute... + if (!isTqsUnmute) { - if (role.Name == "Muted" && role.Id != Program.cfgjson.MutedRole) + foreach (var role in member.Roles) { - try - { - await member.RevokeRoleAsync(role: role, reason: reason); - } - catch + if (role.Name == "Muted" && role.Id != Program.cfgjson.MutedRole) { - // ignore, continue to next role + try + { + await member.RevokeRoleAsync(role: role, reason: auditLogReason); + } + catch + { + // ignore, continue to next role + } } } } @@ -393,7 +401,7 @@ await LogChannelHelper.LogMessageAsync("mod", { await LogChannelHelper.LogMessageAsync("mod", new DiscordMessageBuilder() - .WithContent($"{Program.cfgjson.Emoji.Error} Attempt to removed Muted role from {targetUser.Mention} failed because of a Discord API error!" + + .WithContent($"{Program.cfgjson.Emoji.Error} Attempt to remove {(isTqsUnmute ? "TQS " : "")}Muted role from {targetUser.Mention} failed because of a Discord API error!" + $"\nIf the role was removed manually, this error can be disregarded safely.") .WithAllowedMentions(Mentions.None) ); @@ -404,7 +412,7 @@ await LogChannelHelper.LogMessageAsync("mod", // TQS mutes are not server-wide so this would fail every time for TQS mutes, // and we don't want to log a failure for every removed TQS mute if (!wasTqsMute) - await member.TimeoutAsync(until: null, reason: reason); + await member.TimeoutAsync(until: null, reason: auditLogReason); } catch (Exception ex) { @@ -414,7 +422,7 @@ await LogChannelHelper.LogMessageAsync("mod", if (success) { string unmuteMsg = manual - ? $"{Program.cfgjson.Emoji.Information} {targetUser.Mention} was successfully unmuted by {modUser.Mention}!" + ? $"{Program.cfgjson.Emoji.Information} {targetUser.Mention} was successfully {(isTqsUnmute ? "TQS-" : "")}unmuted by {modUser.Mention}!\nReason: **{reason}**" : $"{Program.cfgjson.Emoji.Information} Successfully unmuted {targetUser.Mention}!"; await LogChannelHelper.LogMessageAsync("mod", new DiscordMessageBuilder().WithContent(unmuteMsg).WithAllowedMentions(Mentions.None)); diff --git a/Structs.cs b/Structs.cs index b039c037..0b134ceb 100644 --- a/Structs.cs +++ b/Structs.cs @@ -336,6 +336,9 @@ public ulong InsidersChannel [JsonProperty("githubWorkflowSucessString")] public string GithubWorkflowSucessString { get; private set; } = ""; + + [JsonProperty("botCommandsChannel")] + public ulong BotCommandsChannel { get; private set; } } public enum Level { Information, Warning, Error, Debug, Verbose } diff --git a/config.json b/config.json index 16f0355b..ee067741 100644 --- a/config.json +++ b/config.json @@ -348,5 +348,6 @@ 450181490345508884 ], "pingBotOwnersOnBadErrors": true, - "githubWorkflowSucessString": "[lists:main] 1 new commit" + "githubWorkflowSucessString": "[lists:main] 1 new commit", + "botCommandsChannel": 740272437719072808 }