diff --git a/src/commands/RestoreCommand.ts b/src/commands/RestoreCommand.ts index 8e850ce..1559369 100644 --- a/src/commands/RestoreCommand.ts +++ b/src/commands/RestoreCommand.ts @@ -5,27 +5,19 @@ import { RegisterBehavior, } from "@sapphire/framework" import { - type APIMessage, ApplicationCommandType, - type BaseMessageOptions, - ButtonStyle, - CategoryChannel, ChatInputCommandInteraction, - ComponentType, ContextMenuCommandBuilder, ContextMenuCommandInteraction, - type GuildBasedChannel, - GuildMember, - Message, PermissionFlagsBits, SlashCommandBuilder, - time, } from "discord.js" -import { getSelf } from "../lib/guilds/getSelf" -import { fetchAndRestoreMessage } from "../lib/messages/fetchAndRestoreMessage" +import { + fetchAndRestoreMessage, + restoreMessageAndReply, + RestoreMode, +} from "../lib/messages/fetchAndRestoreMessage" import { parseMessageOption } from "../lib/messages/parseMessageOption" -import { restoreMessage } from "../lib/messages/restoreMessage" -import { fetchWebhooks } from "../lib/webhooks/fetchWebhooks" export class RestoreCommand extends Command { constructor(context: PieceContext) { @@ -44,7 +36,7 @@ export class RestoreCommand extends Command { const [channelId, messageId] = await parseMessageOption(interaction) if (!channelId || !messageId) return - fetchAndRestoreMessage(interaction, channelId, messageId, false) + fetchAndRestoreMessage(interaction, channelId, messageId, RestoreMode.Restore) } override async contextMenuRun(interaction: ContextMenuCommandInteraction) { @@ -52,62 +44,7 @@ export class RestoreCommand extends Command { await interaction.deferReply({ ephemeral: true }) - const response = await restoreMessage( - interaction.targetMessage as APIMessage | Message, - ) - - const channel = interaction.targetMessage.channel as Exclude< - GuildBasedChannel, - CategoryChannel - > - - const components: BaseMessageOptions["components"] = [] - const webhookId = interaction.targetMessage.webhookId - - if ( - webhookId && - interaction.guild && - channel - .permissionsFor(await getSelf(interaction.guild)) - .has(PermissionFlagsBits.ManageWebhooks) - ) { - const member = - interaction.member instanceof GuildMember - ? interaction.member - : await interaction.guild.members.fetch(interaction.user.id) - - if ( - channel.permissionsFor(member).has(PermissionFlagsBits.ManageWebhooks) - ) { - const webhooks = await fetchWebhooks(channel) - - if (webhooks.some((webhook) => webhook.id === webhookId)) { - components.push({ - type: ComponentType.ActionRow, - components: [ - { - type: ComponentType.Button, - style: ButtonStyle.Secondary, - label: "Quick Edit", - customId: `@discohook/restore-quick-edit/${channel.id}-${interaction.targetId}`, - }, - ], - }) - } - } - } - - await interaction.editReply({ - embeds: [ - { - title: "Restored message", - description: - `The restored message can be found at ${response.url}. This link ` + - `will expire ${time(new Date(response.expires), "R")}.`, - }, - ], - components, - }) + await restoreMessageAndReply(interaction, interaction.targetMessage, RestoreMode.Open) } override async registerApplicationCommands( diff --git a/src/interaction-handlers/RestoreQuickEditHandler.ts b/src/interaction-handlers/RestoreQuickEditHandler.ts index fcddfec..f7180bb 100644 --- a/src/interaction-handlers/RestoreQuickEditHandler.ts +++ b/src/interaction-handlers/RestoreQuickEditHandler.ts @@ -4,7 +4,7 @@ import { type PieceContext, } from "@sapphire/framework" import type { ButtonInteraction, Snowflake } from "discord.js" -import { fetchAndRestoreMessage } from "../lib/messages/fetchAndRestoreMessage" +import { fetchAndRestoreMessage, RestoreMode } from "../lib/messages/fetchAndRestoreMessage" type RestoreQuickEditOptions = { channelId: Snowflake @@ -29,7 +29,7 @@ export class RestoreQuickEditHandler extends InteractionHandler { interaction, options.channelId, options.messageId, - true, + RestoreMode.QuickEdit ) } diff --git a/src/lib/messages/fetchAndRestoreMessage.ts b/src/lib/messages/fetchAndRestoreMessage.ts index e6a1a65..80cf4c4 100644 --- a/src/lib/messages/fetchAndRestoreMessage.ts +++ b/src/lib/messages/fetchAndRestoreMessage.ts @@ -1,5 +1,4 @@ import { - type APIMessage, type BaseMessageOptions, ButtonStyle, CommandInteraction, @@ -8,9 +7,8 @@ import { Message, MessageComponentInteraction, PermissionFlagsBits, - PermissionsBitField, - ThreadChannel, time, + ThreadChannel, Webhook, } from "discord.js" import { getSelf } from "../guilds/getSelf" @@ -19,26 +17,32 @@ import { fetchWebhooks } from "../webhooks/fetchWebhooks" import { fetchMessage } from "./fetchMessage" import { restoreMessage } from "./restoreMessage" -export const fetchAndRestoreMessage = async ( +export enum RestoreMode { + // Just restore the message + Restore, + // Require that the message is from a webhook, and include the webhook + QuickEdit, + // Act like quick edit if the message is from a webhook, and like restore otherwise + Open, +} + +export const restoreMessageAndReply = async ( interaction: CommandInteraction | MessageComponentInteraction, - channelId: string, - messageId: string, - quickEdit = false, + message: Message, + mode: RestoreMode = RestoreMode.Restore, ) => { - const message = await fetchMessage(interaction, channelId, messageId) - if (!message) return - - const selfPermissions = - "guild" in message.channel && message.channel.guild - ? message.channel.permissionsFor(await getSelf(message.channel.guild)) - : new PermissionsBitField(PermissionsBitField.Default) - let webhook: Webhook | undefined = undefined const components: BaseMessageOptions["components"] = [] + if ( message.webhookId && - selfPermissions.has(PermissionFlagsBits.ManageWebhooks) + message.inGuild() && + // Check permissions for the bot + message.channel + .permissionsFor(await getSelf(message.channel.guild)) + .has(PermissionFlagsBits.ManageWebhooks) ) { + // Now check permissions for the member triggering this const member = interaction.member instanceof GuildMember ? interaction.member @@ -46,7 +50,6 @@ export const fetchAndRestoreMessage = async ( if ( member && - "guild" in message.channel && message.channel .permissionsFor(member) .has(PermissionFlagsBits.ManageWebhooks) @@ -58,7 +61,7 @@ export const fetchAndRestoreMessage = async ( const webhooks = await fetchWebhooks(root!) webhook = webhooks.find((webhook) => webhook.id === message.webhookId) - if (webhook && !quickEdit) { + if (webhook && mode == RestoreMode.Restore) { components.push({ type: ComponentType.ActionRow, components: [ @@ -66,7 +69,7 @@ export const fetchAndRestoreMessage = async ( type: ComponentType.Button, style: ButtonStyle.Secondary, label: "Quick Edit", - customId: `@discohook/restore-quick-edit/${channelId}-${messageId}`, + customId: `@discohook/restore-quick-edit/${message.channelId}-${message.id}`, }, ], }) @@ -74,7 +77,7 @@ export const fetchAndRestoreMessage = async ( } } - if (!webhook && quickEdit) { + if (mode == RestoreMode.QuickEdit && !webhook) { await reply(interaction, { content: "I can't find the webhook this message belongs to, therefore " + @@ -84,25 +87,10 @@ export const fetchAndRestoreMessage = async ( } if (message.content || message.embeds.length > 0) { - const response = await restoreMessage( - message, - quickEdit ? webhook : undefined, - ) - await reply(interaction, { - embeds: [ - { - title: "Restored message", - description: - `The restored message can be found at ${response.url}. This link ` + - `will expire ${time(new Date(response.expires), "R")}.`, - }, - ], - components, - }) - return - } - - if (!webhook) { + message = message + } else if (webhook) { + message = await webhook.fetchMessage(message.id) + } else { await reply(interaction, { content: "I can't read the message because of Discord's privacy restrictions. " + @@ -112,20 +100,30 @@ export const fetchAndRestoreMessage = async ( return } - const webhookMessage = await webhook.fetchMessage(messageId) - const response = await restoreMessage( - webhookMessage as Message | APIMessage, - quickEdit ? webhook : undefined, - ) + const editTarget = mode == RestoreMode.Restore ? undefined : webhook + const response = await restoreMessage(message, editTarget) + await reply(interaction, { embeds: [ { - title: "Restored message", + title: editTarget ? "Opened for editing" : "Restored message", description: - `The restored message can be found at ${response.url}. This link ` + + `The message editor can be found at ${response.url}. This link ` + `will expire ${time(new Date(response.expires), "R")}.`, }, ], components, }) } + +export const fetchAndRestoreMessage = async ( + interaction: CommandInteraction | MessageComponentInteraction, + channelId: string, + messageId: string, + mode: RestoreMode = RestoreMode.Restore, +) => { + const message = await fetchMessage(interaction, channelId, messageId) + if (!message) return + + restoreMessageAndReply(interaction, message, mode) +} diff --git a/src/lib/messages/restoreMessage.ts b/src/lib/messages/restoreMessage.ts index e841bf0..e99ce69 100644 --- a/src/lib/messages/restoreMessage.ts +++ b/src/lib/messages/restoreMessage.ts @@ -1,17 +1,10 @@ +import { URL } from "node:url" import { fetch } from "@sapphire/fetch" -import { deepClone } from "@sapphire/utilities" -import { type APIMessage, Embed, Message, Webhook } from "discord.js" +import { ThreadChannel, Message, Webhook } from "discord.js" -export const restoreMessage = async ( - message: APIMessage | Message, - target?: Webhook, -) => { - const embeds = message.embeds.map((embed) => { - if (embed instanceof Embed) { - embed = embed.toJSON() - } else { - embed = deepClone(embed) - } +export const restoreMessage = async (message: Message, target?: Webhook) => { + const embeds = message.embeds.map((embedObject) => { + const embed = embedObject.toJSON() delete embed.type delete embed.video @@ -28,6 +21,13 @@ export const restoreMessage = async ( return embed }) + let webhookUrl = target?.url + if (webhookUrl && message.channel instanceof ThreadChannel) { + let newUrl = new URL(webhookUrl) + newUrl.searchParams.set("thread_id", message.channel.id) + webhookUrl = newUrl.toString() + } + const data = JSON.stringify({ messages: [ { @@ -35,14 +35,10 @@ export const restoreMessage = async ( content: message.content || undefined, embeds: embeds.length === 0 ? undefined : embeds, }, - reference: target - ? message instanceof Message - ? message.url - : message.id - : undefined, + reference: target ? message.url : undefined, }, ], - targets: target ? [{ url: target.url }] : undefined, + targets: [{ url: webhookUrl }], }) const encodedData = Buffer.from(data, "utf-8").toString("base64url") const url = `https://discohook.app/?data=${encodedData}`