import { GuildBasedChannel, Message, OmitPartialGroupDMChannel, Snowflake, TextBasedChannel } from "discord.js"; import { GuildPluginData } from "knub"; import { SavedMessage } from "../../../data/entities/SavedMessage.js"; import { humanizeDurationShort } from "../../../humanizeDuration.js"; import { allowTimeout } from "../../../RegExpRunner.js"; import { DAYS, getInviteCodesInString } from "../../../utils.js"; import { snowflakeToTimestamp } from "../../../utils/snowflakeToTimestamp.js"; import { UtilityPluginType } from "../types.js"; const MAX_CLEAN_COUNT = 300; const MAX_CLEAN_TIME = 1 * DAYS; const MAX_CLEAN_API_REQUESTS = 20; export interface FetchChannelMessagesToCleanOpts { count: number; beforeId: string; upToId?: string; authorId?: string; includePins?: boolean; onlyBotMessages?: boolean; onlyWithInvites?: boolean; matchContent?: RegExp; } export interface SuccessResult { messages: SavedMessage[]; note: string; } export interface ErrorResult { error: string; } export type FetchChannelMessagesToCleanResult = SuccessResult | ErrorResult; export async function fetchChannelMessagesToClean( pluginData: GuildPluginData, targetChannel: GuildBasedChannel & TextBasedChannel, opts: FetchChannelMessagesToCleanOpts, ): Promise { if (opts.count > MAX_CLEAN_COUNT || opts.count <= 0) { return { error: `Clean count must be between 1 and ${MAX_CLEAN_COUNT}` }; } const result: FetchChannelMessagesToCleanResult = { messages: [], note: "", }; const timestampCutoff = snowflakeToTimestamp(opts.beforeId) - MAX_CLEAN_TIME; let foundId = false; let pinIds: Set = new Set(); if (!opts.includePins) { pinIds = new Set((await targetChannel.messages.fetchPinned()).keys()); } const rawMessagesToClean: Array>> = []; let beforeId = opts.beforeId; let requests = 0; while (rawMessagesToClean.length < opts.count) { const potentialMessages = await targetChannel.messages.fetch({ before: beforeId, limit: 100, }); if (potentialMessages.size === 0) break; requests++; const filtered: Array>> = []; for (const message of potentialMessages.values()) { const contentString = message.content || ""; if (opts.authorId && message.author.id !== opts.authorId) continue; if (opts.onlyBotMessages && !message.author.bot) continue; if (pinIds.has(message.id)) continue; if (opts.onlyWithInvites && getInviteCodesInString(contentString).length === 0) continue; if (opts.upToId && message.id < opts.upToId) { foundId = true; break; } if (message.createdTimestamp < timestampCutoff) continue; if ( opts.matchContent && !(await pluginData.state.regexRunner.exec(opts.matchContent, contentString).catch(allowTimeout)) ) { continue; } filtered.push(message); } const remaining = opts.count - rawMessagesToClean.length; const withoutOverflow = filtered.slice(0, remaining); rawMessagesToClean.push(...withoutOverflow); beforeId = potentialMessages.lastKey()!; if (foundId) { break; } if (rawMessagesToClean.length < opts.count) { if (potentialMessages.last()!.createdTimestamp < timestampCutoff) { result.note = `stopped looking after reaching ${humanizeDurationShort(MAX_CLEAN_TIME)} old messages`; break; } if (requests >= MAX_CLEAN_API_REQUESTS) { result.note = `stopped looking after ${requests * 100} messages`; break; } } } // Discord messages -> SavedMessages const existingStored = await pluginData.state.savedMessages.getMultiple(rawMessagesToClean.map((m) => m.id)); const alreadyStored = existingStored.map((stored) => stored.id); const messagesToStore = rawMessagesToClean.filter((potentialMsg) => !alreadyStored.includes(potentialMsg.id)); await pluginData.state.savedMessages.createFromMessages(messagesToStore); result.messages = await pluginData.state.savedMessages.getMultiple(rawMessagesToClean.map((m) => m.id)); return result; }