diff options
| author | Fuwn <[email protected]> | 2025-11-10 03:29:18 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2025-11-10 03:29:18 -0800 |
| commit | ee414ef0ee6c37ca1921d0ff598c4d687f25b796 (patch) | |
| tree | af398a3d4c3699dd5887801166952ad85e3b13af | |
| parent | fix(gateway:reactionRoles): Update intents (diff) | |
| download | umabotdiscord-ee414ef0ee6c37ca1921d0ff598c4d687f25b796.tar.xz umabotdiscord-ee414ef0ee6c37ca1921d0ff598c4d687f25b796.zip | |
feat(gateway:commands): Add message range deletion command
| -rw-r--r-- | packages/gateway/src/commands/commandHandler.ts | 2 | ||||
| -rw-r--r-- | packages/gateway/src/commands/deleteRange.ts | 235 |
2 files changed, 237 insertions, 0 deletions
diff --git a/packages/gateway/src/commands/commandHandler.ts b/packages/gateway/src/commands/commandHandler.ts index 7442c47..efa621a 100644 --- a/packages/gateway/src/commands/commandHandler.ts +++ b/packages/gateway/src/commands/commandHandler.ts @@ -12,6 +12,7 @@ import { handleWebhookCommand } from "./webhook"; import { handleDeleteWebhookCommand } from "./deleteWebhook"; import { handleCharacterClaimUsageCommand } from "./characterClaimUsage"; import { handleTimeoutCommand } from "./timeout"; +import { handleDeleteRangeCommand } from "./deleteRange"; export const handleCommandHandler = (client: Client) => { client.on(Events.MessageCreate, async (message: Message) => { @@ -29,6 +30,7 @@ export const handleCommandHandler = (client: Client) => { handleCrpCommand(message), handleReactCommand(message), handleDeleteCommand(message), + handleDeleteRangeCommand(message), handlePinCommand(message), handleRoleCommand(message), handleWebhookCommand(message), diff --git a/packages/gateway/src/commands/deleteRange.ts b/packages/gateway/src/commands/deleteRange.ts new file mode 100644 index 0000000..a0c55a2 --- /dev/null +++ b/packages/gateway/src/commands/deleteRange.ts @@ -0,0 +1,235 @@ +import { Message } from "discord.js"; +import { logUnexpectedDiscordAPIError, replyWithCleanup } from "../utilities"; +import { logUnexpectedDiscordAPIResult } from "../../../shared/log"; +import { + disableMessageDeletionListener, + enableMessageDeletionListener, +} from "../listeners/messageDeletion"; + +export const handleDeleteRangeCommand = async (message: Message) => { + if (message.author.bot) return; + + if (message.content.toLowerCase().startsWith("uma!deleterange")) { + const application = await message.client.application?.fetch(); + const ownerId = application?.owner?.id; + + if (message.author.id !== ownerId) { + await replyWithCleanup( + message, + "❌ Only the server owner can use this command.", + ); + + return; + } + + const parameters = message.content.split(" ").slice(1); + + if (parameters.length < 3) { + await replyWithCleanup( + message, + "❌ Usage: `uma!deleterange <channel_id> <start_message_id> <end_message_id>`\nExample: `uma!deleterange 1234567890123456789 1111111111111111111 2222222222222222222`", + ); + + return; + } + + const [channelId, startMessageId, endMessageId] = parameters; + + if (!/^\d{17,19}$/.test(channelId)) { + await replyWithCleanup( + message, + "❌ Invalid channel ID format. Please provide a valid Discord channel ID.", + ); + + return; + } + + if ( + !/^\d{17,19}$/.test(startMessageId) || + !/^\d{17,19}$/.test(endMessageId) + ) { + await replyWithCleanup( + message, + "❌ Invalid message ID format. Please provide valid Discord message IDs.", + ); + + return; + } + + const targetChannel = message.client.channels.cache.get(channelId); + + if (!targetChannel || !targetChannel.isTextBased()) { + await replyWithCleanup( + message, + "❌ Channel not found or is not a text channel.", + ); + + return; + } + + disableMessageDeletionListener(); + + try { + const startMessage = await targetChannel.messages.fetch(startMessageId); + const endMessage = await targetChannel.messages.fetch(endMessageId); + const startTimestamp = startMessage.createdTimestamp; + const endTimestamp = endMessage.createdTimestamp; + const earlierTimestamp = Math.min(startTimestamp, endTimestamp); + const laterTimestamp = Math.max(startTimestamp, endTimestamp); + const earlierMessageId = + startTimestamp < endTimestamp ? startMessageId : endMessageId; + const laterMessageId = + startTimestamp < endTimestamp ? endMessageId : startMessageId; + const messagesToDelete: Message[] = []; + const messageIdSet = new Set<string>(); + + messagesToDelete.push( + earlierMessageId === startMessageId ? startMessage : endMessage, + ); + messagesToDelete.push( + laterMessageId === startMessageId ? startMessage : endMessage, + ); + messageIdSet.add(earlierMessageId); + messageIdSet.add(laterMessageId); + + let lastMessageId: string | undefined = laterMessageId; + let hasMoreMessages = true; + + while (hasMoreMessages) { + const fetchOptions: any = { limit: 100 }; + + if (lastMessageId) fetchOptions.before = lastMessageId; + + const fetchedMessages = (await targetChannel.messages.fetch( + fetchOptions, + )) as any; + + if (fetchedMessages.size === 0) { + hasMoreMessages = false; + + break; + } + + for (const message of fetchedMessages.values()) { + const messageTimestamp = message.createdTimestamp; + + if (messageTimestamp < earlierTimestamp) { + hasMoreMessages = false; + + break; + } + + if ( + messageTimestamp >= earlierTimestamp && + messageTimestamp <= laterTimestamp && + !messageIdSet.has(message.id) + ) { + messagesToDelete.push(message); + messageIdSet.add(message.id); + } + } + + lastMessageId = fetchedMessages.last()?.id; + + if ( + !lastMessageId || + fetchedMessages.last()!.createdTimestamp < earlierTimestamp + ) + hasMoreMessages = false; + } + + if (messagesToDelete.length === 0) { + await replyWithCleanup( + message, + "❌ No messages found in the specified range.", + ); + + return; + } + + messagesToDelete.sort((a, b) => a.createdTimestamp - b.createdTimestamp); + + if (messagesToDelete.length > 1 && "bulkDelete" in targetChannel) { + try { + const chunkSize = 100; + let failedDeleteCount = 0; + + for (let i = 0; i < messagesToDelete.length; i += chunkSize) { + const chunk = messagesToDelete.slice(i, i + chunkSize); + + if (chunk.length < 2) { + for (const message of chunk) + try { + await message.delete(); + } catch { + failedDeleteCount += 1; + } + } else { + try { + await targetChannel.bulkDelete(chunk); + } catch (chunkError) { + logUnexpectedDiscordAPIError(chunkError); + + for (const message of chunk) + try { + await message.delete(); + } catch { + failedDeleteCount += 1; + } + } + } + } + + await message.delete(); + + if (failedDeleteCount > 0) + logUnexpectedDiscordAPIResult( + `Failed to delete ${failedDeleteCount} out of ${messagesToDelete.length} messages`, + ); + } catch (bulkError) { + logUnexpectedDiscordAPIError(bulkError); + + let failedDeleteCount = 0; + + for (const message of messagesToDelete) + try { + await message.delete(); + } catch { + failedDeleteCount += 1; + } + + await message.delete(); + + if (failedDeleteCount > 0) + logUnexpectedDiscordAPIResult( + `Failed to delete ${failedDeleteCount} out of ${messagesToDelete.length} messages`, + ); + } + } else { + let failedDeleteCount = 0; + + for (const message of messagesToDelete) + try { + await message.delete(); + } catch { + failedDeleteCount += 1; + } + + await message.delete(); + + if (failedDeleteCount > 0) + logUnexpectedDiscordAPIResult( + `Failed to delete ${failedDeleteCount} out of ${messagesToDelete.length} messages`, + ); + } + } catch (error) { + logUnexpectedDiscordAPIError(error); + await replyWithCleanup( + message, + "❌ Failed to delete messages. Check bot permissions and try again.", + ); + } finally { + enableMessageDeletionListener(); + } + } +}; |