forked from syntaxbullet/AuroraBot-discord
refactor: initial moves
This commit is contained in:
198
shared/modules/moderation/prune.service.ts
Normal file
198
shared/modules/moderation/prune.service.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user