summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2025-11-10 03:29:18 -0800
committerFuwn <[email protected]>2025-11-10 03:29:18 -0800
commitee414ef0ee6c37ca1921d0ff598c4d687f25b796 (patch)
treeaf398a3d4c3699dd5887801166952ad85e3b13af
parentfix(gateway:reactionRoles): Update intents (diff)
downloadumabotdiscord-ee414ef0ee6c37ca1921d0ff598c4d687f25b796.tar.xz
umabotdiscord-ee414ef0ee6c37ca1921d0ff598c4d687f25b796.zip
feat(gateway:commands): Add message range deletion command
-rw-r--r--packages/gateway/src/commands/commandHandler.ts2
-rw-r--r--packages/gateway/src/commands/deleteRange.ts235
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();
+ }
+ }
+};