199 lines
6.4 KiB
TypeScript
199 lines
6.4 KiB
TypeScript
import { Collection, Message, PermissionFlagsBits } from "discord.js";
|
|
import type { TextBasedChannel } from "discord.js";
|
|
import type { PruneOptions, PruneResult, PruneProgress } from "./prune.types";
|
|
import { config } from "@/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<void>
|
|
): Promise<PruneResult> {
|
|
// 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<Collection<string, Message>> {
|
|
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<string, Message>,
|
|
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<number> {
|
|
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<void> {
|
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
}
|
|
}
|