diff --git a/backend/src/pluginUtils.ts b/backend/src/pluginUtils.ts index 23b21d0f..f0912677 100644 --- a/backend/src/pluginUtils.ts +++ b/backend/src/pluginUtils.ts @@ -183,7 +183,7 @@ export function getPluginConfigPreprocessor( }; } -export function sendSuccessMessage( +export async function sendSuccessMessage( pluginData: AnyPluginData, channel: TextChannel, body: string, @@ -194,8 +194,9 @@ export function sendSuccessMessage( const content: MessageOptions = allowedMentions ? { content: formattedBody, allowedMentions } : { content: formattedBody }; + return channel - .send({ content }) // Force line break + .send({ ...content, split: false }) // Force line break .catch(err => { const channelInfo = channel.guild ? `${channel.id} (${channel.guild.id})` : `${channel.id}`; logger.warn(`Failed to send success message to ${channelInfo}): ${err.code} ${err.message}`); @@ -203,7 +204,7 @@ export function sendSuccessMessage( }); } -export function sendErrorMessage( +export async function sendErrorMessage( pluginData: AnyPluginData, channel: TextChannel, body: string, @@ -214,6 +215,7 @@ export function sendErrorMessage( const content: MessageOptions = allowedMentions ? { content: formattedBody, allowedMentions } : { content: formattedBody }; + return channel .send({ ...content, split: false }) // Force line break .catch(err => { diff --git a/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts b/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts index faa67c18..87b61934 100644 --- a/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts +++ b/backend/src/plugins/ChannelArchiver/commands/ArchiveChannelCmd.ts @@ -32,7 +32,6 @@ export const ArchiveChannelCmd = channelArchiverCmd({ async run({ message: msg, args, pluginData }) { if (!args["attachment-channel"]) { const confirmed = await confirm( - pluginData.client, msg.channel, msg.author.id, "No `-attachment-channel` specified. Continue? Attachments will not be available in the log if their message is deleted.", diff --git a/backend/src/plugins/Counters/commands/ResetAllCounterValuesCmd.ts b/backend/src/plugins/Counters/commands/ResetAllCounterValuesCmd.ts index 6b632f95..2b16f10b 100644 --- a/backend/src/plugins/Counters/commands/ResetAllCounterValuesCmd.ts +++ b/backend/src/plugins/Counters/commands/ResetAllCounterValuesCmd.ts @@ -31,7 +31,6 @@ export const ResetAllCounterValuesCmd = typedGuildCommand()( const counterName = counter.name || args.counterName; const confirmed = await confirm( - pluginData.client, message.channel, message.author.id, trimMultilineString(` diff --git a/backend/src/plugins/Logs/util/log.ts b/backend/src/plugins/Logs/util/log.ts index ea65690d..0bb8c616 100644 --- a/backend/src/plugins/Logs/util/log.ts +++ b/backend/src/plugins/Logs/util/log.ts @@ -137,7 +137,7 @@ export async function log(pluginData: GuildPluginData, type: Log const batched = opts.batched ?? true; const batchTime = opts.batch_time ?? 1000; const cfg = pluginData.config.get(); - const parse: MessageMentionTypes[] | undefined = cfg.allow_user_mentions ? ["users"] : undefined; + const parse: MessageMentionTypes[] = cfg.allow_user_mentions ? ["users"] : []; if (batched) { // If we're batching log messages, gather all log messages within the set batch_time into a single message diff --git a/backend/src/plugins/ModActions/commands/BanCmd.ts b/backend/src/plugins/ModActions/commands/BanCmd.ts index f8cb4ff9..1d113168 100644 --- a/backend/src/plugins/ModActions/commands/BanCmd.ts +++ b/backend/src/plugins/ModActions/commands/BanCmd.ts @@ -12,6 +12,7 @@ import { CasesPlugin } from "../../../plugins/Cases/CasesPlugin"; import { CaseTypes } from "../../../data/CaseTypes"; import { LogType } from "../../../data/LogType"; import { banLock } from "../../../utils/lockNameHelpers"; +import { waitForButtonConfirm } from "../../../utils/waitForInteraction"; const opts = { mod: ct.member({ option: true }), @@ -76,11 +77,12 @@ export const BanCmd = modActionsCmd({ } // Ask the mod if we should update the existing ban - const alreadyBannedMsg = await msg.channel.send("User is already banned, update ban?"); - const reply = false; // await waitForReaction(pluginData.client, alreadyBannedMsg, ["✅", "❌"], msg.author.id); FIXME waiting on waitForButton - - alreadyBannedMsg.delete().catch(noop); - if (!reply /* || reply.name === "❌"*/) { + const reply = await waitForButtonConfirm( + msg.channel, + { content: "Failed to message the user. Log the warning anyway?" }, + { confirmText: "Yes", cancelText: "No", restrictToId: msg.member.id }, + ); + if (!reply) { sendErrorMessage(pluginData, msg.channel, "User already banned, update cancelled by moderator"); lock.unlock(); return; @@ -124,11 +126,12 @@ export const BanCmd = modActionsCmd({ } } else { // Ask the mod if we should upgrade to a forceban as the user is not on the server - const notOnServerMsg = await msg.channel.send("User not found on the server, forceban instead?"); - const reply = false; // await waitForReaction(pluginData.client, notOnServerMsg, ["✅", "❌"], msg.author.id); Waiting for waitForButton - - notOnServerMsg.delete().catch(noop); - if (!reply /*|| reply.name === "❌"*/) { + const reply = await waitForButtonConfirm( + msg.channel, + { content: "User not on server, forceban instead?" }, + { confirmText: "Yes", cancelText: "No", restrictToId: msg.member.id }, + ); + if (!reply) { sendErrorMessage(pluginData, msg.channel, "User not on server, ban cancelled by moderator"); lock.unlock(); return; diff --git a/backend/src/plugins/ModActions/commands/MuteCmd.ts b/backend/src/plugins/ModActions/commands/MuteCmd.ts index 67f5ce71..4229bd6f 100644 --- a/backend/src/plugins/ModActions/commands/MuteCmd.ts +++ b/backend/src/plugins/ModActions/commands/MuteCmd.ts @@ -5,6 +5,7 @@ import { noop, resolveMember, resolveUser } from "../../../utils"; import { isBanned } from "../functions/isBanned"; import { actualMuteUserCmd } from "../functions/actualMuteUserCmd"; +import { waitForButtonConfirm } from "../../../utils/waitForInteraction"; const opts = { mod: ct.member({ option: true }), @@ -54,11 +55,13 @@ export const MuteCmd = modActionsCmd({ return; } else { // Ask the mod if we should upgrade to a forcemute as the user is not on the server - const notOnServerMsg = await msg.channel.send("User not found on the server, forcemute instead?"); - const reply = false; // await waitForReaction(pluginData.client, notOnServerMsg, ["✅", "❌"], msg.author.id); FIXME waiting on waitForButton + const reply = await waitForButtonConfirm( + msg.channel, + { content: "User not found on the server, forcemute instead?" }, + { confirmText: "Yes", cancelText: "No", restrictToId: msg.member.id }, + ); - notOnServerMsg.delete().catch(noop); - if (!reply /*|| reply.name === "❌"*/) { + if (!reply) { sendErrorMessage(pluginData, msg.channel, "User not on server, mute cancelled by moderator"); return; } diff --git a/backend/src/plugins/ModActions/commands/UnmuteCmd.ts b/backend/src/plugins/ModActions/commands/UnmuteCmd.ts index 0c3d09ac..dae00bea 100644 --- a/backend/src/plugins/ModActions/commands/UnmuteCmd.ts +++ b/backend/src/plugins/ModActions/commands/UnmuteCmd.ts @@ -5,6 +5,7 @@ import { resolveUser, resolveMember, noop } from "../../../utils"; import { MutesPlugin } from "../../../plugins/Mutes/MutesPlugin"; import { actualUnmuteCmd } from "../functions/actualUnmuteUserCmd"; import { isBanned } from "../functions/isBanned"; +import { waitForButtonConfirm } from "../../../utils/waitForInteraction"; const opts = { mod: ct.member({ option: true }), @@ -60,11 +61,13 @@ export const UnmuteCmd = modActionsCmd({ return; } else { // Ask the mod if we should upgrade to a forceunmute as the user is not on the server - const notOnServerMsg = await msg.channel.send("User not found on the server, forceunmute instead?"); - const reply = false; // await waitForReaction(pluginData.client, notOnServerMsg, ["✅", "❌"], msg.author.id); FIXME waiting on waitForButton + const reply = await waitForButtonConfirm( + msg.channel, + { content: "User not on server, forceunmute instead?" }, + { confirmText: "Yes", cancelText: "No", restrictToId: msg.member.id }, + ); - notOnServerMsg.delete().catch(noop); - if (!reply /*|| reply.name === "❌"*/) { + if (!reply) { sendErrorMessage(pluginData, msg.channel, "User not on server, unmute cancelled by moderator"); return; } diff --git a/backend/src/plugins/ModActions/commands/WarnCmd.ts b/backend/src/plugins/ModActions/commands/WarnCmd.ts index b50caa35..37f8d814 100644 --- a/backend/src/plugins/ModActions/commands/WarnCmd.ts +++ b/backend/src/plugins/ModActions/commands/WarnCmd.ts @@ -6,11 +6,12 @@ import { formatReasonWithAttachments } from "../functions/formatReasonWithAttach import { CasesPlugin } from "../../Cases/CasesPlugin"; import { LogType } from "../../../data/LogType"; import { CaseTypes } from "../../../data/CaseTypes"; -import { errorMessage, resolveMember, resolveUser, stripObjectToScalars } from "../../../utils"; +import { errorMessage, resolveMember, resolveUser } from "../../../utils"; import { isBanned } from "../functions/isBanned"; import { readContactMethodsFromArgs } from "../functions/readContactMethodsFromArgs"; import { warnMember } from "../functions/warnMember"; import { TextChannel } from "discord.js"; +import { waitForButtonConfirm } from "../../../utils/waitForInteraction"; export const WarnCmd = modActionsCmd({ trigger: "warn", @@ -69,13 +70,12 @@ export const WarnCmd = modActionsCmd({ const casesPlugin = pluginData.getPlugin(CasesPlugin); const priorWarnAmount = await casesPlugin.getCaseTypeAmountForUserId(memberToWarn.id, CaseTypes.Warn); if (config.warn_notify_enabled && priorWarnAmount >= config.warn_notify_threshold) { - const tooManyWarningsMsg = await msg.channel.send( - config.warn_notify_message.replace("{priorWarnings}", `${priorWarnAmount}`), + const reply = await waitForButtonConfirm( + msg.channel, + { content: config.warn_notify_message.replace("{priorWarnings}", `${priorWarnAmount}`) }, + { confirmText: "Yes", cancelText: "No", restrictToId: msg.member.id }, ); - - const reply = false; // await waitForReaction(pluginData.client, tooManyWarningsMsg, ["✅", "❌"], msg.author.id); FIXME waiting on waitForButton - tooManyWarningsMsg.delete(); - if (!reply /*|| reply.name === "❌"*/) { + if (!reply) { msg.channel.send(errorMessage("Warn cancelled by moderator")); return; } diff --git a/backend/src/plugins/ModActions/functions/isBanned.ts b/backend/src/plugins/ModActions/functions/isBanned.ts index 4496fe1a..1be820fa 100644 --- a/backend/src/plugins/ModActions/functions/isBanned.ts +++ b/backend/src/plugins/ModActions/functions/isBanned.ts @@ -20,7 +20,10 @@ export async function isBanned( } try { - const potentialBan = await Promise.race([pluginData.guild.bans.fetch({ user: userId }), sleep(timeout)]); + const potentialBan = await Promise.race([ + pluginData.guild.bans.fetch({ user: userId }).catch(() => null), + sleep(timeout), + ]); return potentialBan != null; } catch (e) { if (isDiscordRESTError(e) && e.code === 10026) { diff --git a/backend/src/plugins/ModActions/functions/warnMember.ts b/backend/src/plugins/ModActions/functions/warnMember.ts index cf4e8eee..af6e267e 100644 --- a/backend/src/plugins/ModActions/functions/warnMember.ts +++ b/backend/src/plugins/ModActions/functions/warnMember.ts @@ -14,7 +14,8 @@ import { CasesPlugin } from "../../Cases/CasesPlugin"; import { CaseTypes } from "../../../data/CaseTypes"; import { LogType } from "../../../data/LogType"; import { renderTemplate } from "../../../templateFormatter"; -import { GuildMember } from "discord.js"; +import { GuildMember, MessageOptions } from "discord.js"; +import { waitForButtonConfirm } from "../../../utils/waitForInteraction"; export async function warnMember( pluginData: GuildPluginData, @@ -43,12 +44,13 @@ export async function warnMember( if (!notifyResult.success) { if (warnOptions.retryPromptChannel && pluginData.guild.channels.resolve(warnOptions.retryPromptChannel.id)) { - const failedMsg = await warnOptions.retryPromptChannel.send( - "Failed to message the user. Log the warning anyway?", + const reply = await waitForButtonConfirm( + warnOptions.retryPromptChannel, + { content: "Failed to message the user. Log the warning anyway?" }, + { confirmText: "Yes", cancelText: "No", restrictToId: warnOptions.caseArgs?.modId }, ); - const reply = false; // await waitForReaction(pluginData.client, failedMsg, ["✅", "❌"]); FIXME waiting on waitForButton - failedMsg.delete(); - if (!reply /*|| reply.name === "❌"*/) { + + if (!reply) { return { status: "failed", error: "Failed to message user", @@ -74,7 +76,7 @@ export async function warnMember( noteDetails: notifyResult.text ? [ucfirst(notifyResult.text)] : [], }); - const mod = await resolveUser(pluginData.client, modId); + const mod = await pluginData.guild.members.fetch(modId); pluginData.state.serverLogs.log(LogType.MEMBER_WARN, { mod: stripObjectToScalars(mod), member: stripObjectToScalars(member, ["user", "roles"]), diff --git a/backend/src/plugins/ReactionRoles/util/applyReactionRoleReactionsToMessage.ts b/backend/src/plugins/ReactionRoles/util/applyReactionRoleReactionsToMessage.ts index ea143838..e8280a8f 100644 --- a/backend/src/plugins/ReactionRoles/util/applyReactionRoleReactionsToMessage.ts +++ b/backend/src/plugins/ReactionRoles/util/applyReactionRoleReactionsToMessage.ts @@ -51,7 +51,7 @@ export async function applyReactionRoleReactionsToMessage( // Remove old reactions, if any try { - await targetMessage.removeReactions(); + await targetMessage.reactions.removeAll(); } catch (e) { if (isDiscordRESTError(e)) { errors.push(`Error ${e.code} while removing old reactions: ${e.message}`); @@ -74,7 +74,7 @@ export async function applyReactionRoleReactionsToMessage( const emoji = isSnowflake(rawEmoji) ? `foo:${rawEmoji}` : rawEmoji; try { - await targetMessage.addReaction(emoji); + await targetMessage.reactions.add(emoji); await sleep(1250); // Make sure we don't hit rate limits } catch (e) { if (isDiscordRESTError(e)) { diff --git a/backend/src/utils.ts b/backend/src/utils.ts index 91dab927..30a411ab 100644 --- a/backend/src/utils.ts +++ b/backend/src/utils.ts @@ -27,9 +27,12 @@ import { GuildAuditLogsEntry, GuildChannel, GuildMember, + Interaction, Invite, Message, + MessageActionRow, MessageAttachment, + MessageComponent, MessageEmbed, MessageEmbedOptions, MessageMentionOptions, @@ -41,6 +44,7 @@ import { User, } from "discord.js"; import { ChannelTypeStrings } from "./types"; +import { waitForButtonConfirm } from "./utils/waitForInteraction"; const fsp = fs.promises; @@ -1160,7 +1164,7 @@ export async function resolveUser(bot, value) { // If we have the user cached, return that directly if (bot.users.cache.has(userId)) { - return bot.users.get(userId); + return bot.users.fetch(userId); } // We don't want to spam the API by trying to fetch unknown users again and again, @@ -1169,9 +1173,8 @@ export async function resolveUser(bot, value) { return new UnknownUser({ id: userId }); } - const freshUser = await bot.getRESTUser(userId).catch(noop); + const freshUser = await bot.users.fetch(userId, true, true).catch(noop); if (freshUser) { - bot.users.add(freshUser, bot); return freshUser; } @@ -1272,15 +1275,11 @@ export async function resolveInvite( } export async function confirm( - bot: Client, channel: TextChannel, userId: string, content: StringResolvable | MessageOptions, -) { - const msg = await channel.send(content); - const reply: any = {}; // await helpers.waitForReaction(bot, msg, ["✅", "❌"], userId); FIXME waiting on waitForButton - msg.delete().catch(noop); - return reply && reply.name === "✅"; +): Promise { + return waitForButtonConfirm(channel, { content }, { restrictToId: userId }); } export function messageSummary(msg: SavedMessage) { diff --git a/backend/src/utils/waitForInteraction.ts b/backend/src/utils/waitForInteraction.ts new file mode 100644 index 00000000..2ee91e84 --- /dev/null +++ b/backend/src/utils/waitForInteraction.ts @@ -0,0 +1,72 @@ +import { + TextChannel, + MessageActionRow, + MessageOptions, + MessageButton, + Client, + Interaction, + MessageComponentInteraction, +} from "discord.js"; +import { PluginError } from "knub"; +import { noop } from "knub/dist/utils"; +import moment from "moment"; + +export async function waitForComponent( + channel: TextChannel, + toPost: MessageOptions, + components: MessageActionRow[], + options?: WaitForOptions, +) { + throw new PluginError("Unimplemented method waitForComponent called."); +} + +export async function waitForButtonConfirm( + channel: TextChannel, + toPost: MessageOptions, + options?: WaitForOptions, +): Promise { + return new Promise(async resolve => { + const idMod = `${channel.guild.id}-${moment.utc().valueOf()}`; + const row = new MessageActionRow().addComponents([ + new MessageButton() + .setStyle("SUCCESS") + .setLabel(options?.confirmText || "Confirm") + .setType("BUTTON") + .setCustomID(`confirmButton:${idMod}`), + + new MessageButton() + .setStyle("DANGER") + .setLabel(options?.cancelText || "Cancel") + .setType("BUTTON") + .setCustomID(`cancelButton:${idMod}`), + ]); + const message = await channel.send({ ...toPost, components: [row], split: false }); + + const filter = (iac: MessageComponentInteraction) => iac.message.id === message.id; + const collector = message.createMessageComponentInteractionCollector(filter, { time: 10000 }); + + collector.on("collect", (interaction: MessageComponentInteraction) => { + if (options?.restrictToId && options.restrictToId !== interaction.user.id) { + interaction.reply(`You are not permitted to use these buttons.`, { ephemeral: true }); + } else { + if (interaction.customID === `confirmButton:${idMod}`) { + message.delete(); + resolve(true); + } else if (interaction.customID === `cancelButton:${idMod}`) { + message.delete(); + resolve(false); + } + } + }); + collector.on("end", () => { + if (!message.deleted) message.delete().catch(noop); + resolve(false); + }); + }); +} + +export interface WaitForOptions { + restrictToId?: string; + confirmText?: string; + cancelText?: string; +}