import { Collection, Message, PermissionFlagsBits } from "discord.js"; import type { TextBasedChannel } from "discord.js"; import type { PruneOptions, PruneResult, PruneProgress } from "@/modules/moderation/prune.types"; import { config } from "@shared/lib/config"; export class PruneService { /** * Delete messages from a channel based on provided options */ static async deleteMessages( channel: TextBasedChannel, options: PruneOptions, progressCallback?: (progress: PruneProgress) => Promise ): Promise { // Validate channel permissions if (!('permissionsFor' in channel)) { throw new Error("Cannot check permissions for this channel type"); } const permissions = channel.permissionsFor(channel.client.user!); if (!permissions?.has(PermissionFlagsBits.ManageMessages)) { throw new Error("Missing permission to manage messages in this channel"); } const { amount, userId, all } = options; const batchSize = config.moderation.prune.batchSize; const batchDelay = config.moderation.prune.batchDelayMs; let totalDeleted = 0; let totalSkipped = 0; let requestedCount = amount || 10; let lastMessageId: string | undefined; let username: string | undefined; if (all) { // Delete all messages in batches const estimatedTotal = await this.estimateMessageCount(channel); requestedCount = estimatedTotal; while (true) { const messages = await this.fetchMessages(channel, batchSize, lastMessageId); if (messages.size === 0) break; const { deleted, skipped } = await this.processBatch( channel, messages, userId ); totalDeleted += deleted; totalSkipped += skipped; // Update progress if (progressCallback) { await progressCallback({ current: totalDeleted, total: estimatedTotal }); } // If we deleted fewer than we fetched, we've hit old messages if (deleted < messages.size) { break; } // Get the ID of the last message for pagination const lastMessage = Array.from(messages.values()).pop(); lastMessageId = lastMessage?.id; // Delay to avoid rate limits if (messages.size >= batchSize) { await this.delay(batchDelay); } } } else { // Delete specific amount const limit = Math.min(amount || 10, config.moderation.prune.maxAmount); const messages = await this.fetchMessages(channel, limit, undefined); const { deleted, skipped } = await this.processBatch( channel, messages, userId ); totalDeleted = deleted; totalSkipped = skipped; requestedCount = limit; } // Get username if filtering by user if (userId && totalDeleted > 0) { try { const user = await channel.client.users.fetch(userId); username = user.username; } catch { username = "Unknown User"; } } return { deletedCount: totalDeleted, requestedCount, filtered: !!userId, username, skippedOld: totalSkipped > 0 ? totalSkipped : undefined }; } /** * Fetch messages from a channel */ private static async fetchMessages( channel: TextBasedChannel, limit: number, before?: string ): Promise> { if (!('messages' in channel)) { return new Collection(); } return await channel.messages.fetch({ limit, before }); } /** * Process a batch of messages for deletion */ private static async processBatch( channel: TextBasedChannel, messages: Collection, userId?: string ): Promise<{ deleted: number; skipped: number }> { if (!('bulkDelete' in channel)) { throw new Error("This channel type does not support bulk deletion"); } // Filter by user if specified let messagesToDelete = messages; if (userId) { messagesToDelete = messages.filter(msg => msg.author.id === userId); } if (messagesToDelete.size === 0) { return { deleted: 0, skipped: 0 }; } try { // bulkDelete with filterOld=true will automatically skip messages >14 days const deleted = await channel.bulkDelete(messagesToDelete, true); const skipped = messagesToDelete.size - deleted.size; return { deleted: deleted.size, skipped }; } catch (error) { console.error("Error during bulk delete:", error); throw new Error("Failed to delete messages"); } } /** * Estimate the total number of messages in a channel */ static async estimateMessageCount(channel: TextBasedChannel): Promise { if (!('messages' in channel)) { return 0; } try { // Fetch a small sample to get the oldest message const sample = await channel.messages.fetch({ limit: 1 }); if (sample.size === 0) return 0; // This is a rough estimate - Discord doesn't provide exact counts // We'll return a conservative estimate const oldestMessage = sample.first(); const channelAge = Date.now() - (oldestMessage?.createdTimestamp || Date.now()); const estimatedRate = 100; // messages per day (conservative) const daysOld = channelAge / (1000 * 60 * 60 * 24); return Math.max(100, Math.round(daysOld * estimatedRate)); } catch { return 100; // Default estimate } } /** * Helper to delay execution */ private static delay(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)); } }