diff --git a/src/lib/moderation/actions/base/ModerationAction.ts b/src/lib/moderation/actions/base/ModerationAction.ts index ae44d496461..53f860ef218 100644 --- a/src/lib/moderation/actions/base/ModerationAction.ts +++ b/src/lib/moderation/actions/base/ModerationAction.ts @@ -197,7 +197,9 @@ export abstract class ModerationAction) { + protected async cancelLastModerationEntryTaskFromUser( + options: ModerationAction.ModerationEntryFetchOptions + ): Promise | null> { const entry = await this.retrieveLastModerationEntryFromUser(options); if (isNullish(entry)) return null; @@ -212,7 +214,9 @@ export abstract class ModerationAction) { + protected async retrieveLastModerationEntryFromUser( + options: ModerationAction.ModerationEntryFetchOptions + ): Promise | null> { // Retrieve all the entries const entries = await getModeration(options.guild).fetch({ userId: options.userId }); @@ -230,7 +234,7 @@ export abstract class ModerationAction)) continue; - return entry; + return entry as ModerationManager.Entry; } return null; diff --git a/src/lib/moderation/managers/ModerationManager.ts b/src/lib/moderation/managers/ModerationManager.ts index d1dd46b4d92..c6d22cc666d 100644 --- a/src/lib/moderation/managers/ModerationManager.ts +++ b/src/lib/moderation/managers/ModerationManager.ts @@ -10,7 +10,7 @@ import { AsyncQueue } from '@sapphire/async-queue'; import type { GuildTextBasedChannelTypes } from '@sapphire/discord.js-utilities'; import { UserError, container } from '@sapphire/framework'; import { isNullish } from '@sapphire/utilities'; -import { DiscordAPIError, type Guild, type Snowflake } from 'discord.js'; +import type { Guild, Snowflake } from 'discord.js'; enum CacheActions { None, @@ -22,7 +22,7 @@ export class ModerationManager { /** * The Guild instance that manages this manager */ - public guild: Guild; + public readonly guild: Guild; /** * The cache of the moderation entries, sorted by their case ID in @@ -33,7 +33,7 @@ export class ModerationManager { /** * A queue for save tasks, prevents case_id duplication */ - #saveQueue = new AsyncQueue(); + readonly #saveQueue = new AsyncQueue(); /** * The latest moderation case ID. @@ -73,21 +73,13 @@ export class ModerationManager { } /** - * Fetch 100 messages from the modlogs channel + * Retrieves the latest recent cached entry for a given user created in the last 30 seconds. + * + * @param userId - The ID of the user. + * @returns The latest recent cached entry for the user, or `null` if no entry is found. */ - public async fetchChannelMessages(remainingRetries = 5): Promise { - const channel = await this.fetchChannel(); - if (channel === null) return; - try { - await channel.messages.fetch({ limit: 100 }); - } catch (error) { - if (error instanceof DiscordAPIError) throw error; - return this.fetchChannelMessages(--remainingRetries); - } - } - - public getLatestLogForUser(userId: string) { - const minimumTime = Date.now() - seconds(15); + public getLatestRecentCachedEntryForUser(userId: string) { + const minimumTime = Date.now() - seconds(30); for (const entry of this.#cache.values()) { if (entry.userId !== userId) continue; if (entry.createdAt < minimumTime) break; @@ -119,7 +111,7 @@ export class ModerationManager { const id = (await this.getCurrentId()) + 1; const entry = new ModerationManagerEntry({ ...data.toData(), id, createdAt: Date.now() }); await this.#performInsert(entry); - return this._cache(entry, CacheActions.Insert); + return this.#addToCache(entry, CacheActions.Insert); } finally { this.#saveQueue.shift(); } @@ -192,17 +184,17 @@ export class ModerationManager { ): Promise | null> { // Case number if (typeof options === 'number') { - return this.#getSingle(options) ?? this._cache(await this.#fetchSingle(options), CacheActions.None); + return this.#getSingle(options) ?? this.#addToCache(await this.#fetchSingle(options), CacheActions.None); } if (options.moderatorId || options.userId) { return this.#count === this.#cache.size // ? this.#getMany(options) - : this._cache(await this.#fetchMany(options), CacheActions.None); + : this.#addToCache(await this.#fetchMany(options), CacheActions.None); } if (this.#count !== this.#cache.size) { - this._cache(await this.#fetchAll(), CacheActions.Fetch); + this.#addToCache(await this.#fetchAll(), CacheActions.Fetch); } return this.#cache; @@ -280,9 +272,9 @@ export class ModerationManager { return false; } - private _cache(entry: ModerationManagerEntry | null, type: CacheActions): ModerationManagerEntry; - private _cache(entries: ModerationManagerEntry[], type: CacheActions): SortedCollection; - private _cache( + #addToCache(entry: ModerationManagerEntry | null, type: CacheActions): ModerationManagerEntry; + #addToCache(entries: ModerationManagerEntry[], type: CacheActions): SortedCollection; + #addToCache( entries: ModerationManagerEntry | ModerationManagerEntry[] | null, type: CacheActions ): SortedCollection | ModerationManagerEntry | null { @@ -306,7 +298,12 @@ export class ModerationManager { }, seconds(30)); } - return Array.isArray(entries) ? new SortedCollection(entries.map((entry) => [entry.id, entry])) : entries; + return Array.isArray(entries) + ? new SortedCollection( + entries.map((entry) => [entry.id, entry]), + desc + ) + : entries; } async #resolveEntry(entryOrId: ModerationManager.EntryResolvable) { diff --git a/src/lib/structures/managers/ScheduleManager.ts b/src/lib/structures/managers/ScheduleManager.ts index 09d449d7c9c..9e637bfdf61 100644 --- a/src/lib/structures/managers/ScheduleManager.ts +++ b/src/lib/structures/managers/ScheduleManager.ts @@ -89,7 +89,7 @@ export class ScheduleManager { } private _remove(entity: ScheduleEntity) { - const index = this.queue.findIndex((entry) => entry === entity); + const index = this.queue.indexOf(entity); if (index !== -1) this.queue.splice(index, 1); } diff --git a/src/listeners/guilds/members/rawMemberRemoveNotify.ts b/src/listeners/guilds/members/rawMemberRemoveNotify.ts index b7e6b95a7fe..230876cc210 100644 --- a/src/listeners/guilds/members/rawMemberRemoveNotify.ts +++ b/src/listeners/guilds/members/rawMemberRemoveNotify.ts @@ -56,7 +56,7 @@ export class UserListener extends Listener { const moderation = getModeration(guild); await moderation.waitLock(); - const latestLogForUser = moderation.getLatestLogForUser(user.id); + const latestLogForUser = moderation.getLatestRecentCachedEntryForUser(user.id); if (latestLogForUser === null) { return { diff --git a/src/listeners/moderation/moderationEntryEdit.ts b/src/listeners/moderation/moderationEntryEdit.ts index 8c33efe4957..e08d58ff2f7 100644 --- a/src/listeners/moderation/moderationEntryEdit.ts +++ b/src/listeners/moderation/moderationEntryEdit.ts @@ -1,14 +1,16 @@ import { GuildSettings, writeSettings } from '#lib/database'; import type { ModerationManager } from '#lib/moderation'; import { getEmbed, getUndoTaskName } from '#lib/moderation/common'; +import type { GuildMessage } from '#lib/types'; import { resolveOnErrorCodes } from '#utils/common'; import { getModeration } from '#utils/functions'; import { SchemaKeys } from '#utils/moderationConstants'; +import { isUserSelf } from '#utils/util'; import { canSendEmbeds, type GuildTextBasedChannelTypes } from '@sapphire/discord.js-utilities'; import { Listener } from '@sapphire/framework'; import { fetchT } from '@sapphire/plugin-i18next'; import { isNullish, isNumber } from '@sapphire/utilities'; -import { RESTJSONErrorCodes, type Embed, type Message } from 'discord.js'; +import { DiscordAPIError, RESTJSONErrorCodes, type Collection, type Embed, type Message, type Snowflake } from 'discord.js'; export class UserListener extends Listener { public run(old: ModerationManager.Entry, entry: ModerationManager.Entry) { @@ -45,7 +47,7 @@ export class UserListener extends Listener { if (channel === null || !canSendEmbeds(channel)) return; const t = await fetchT(entry.guild); - const previous = this.fetchModerationLogMessage(entry, channel); + const previous = await this.fetchModerationLogMessage(entry, channel); const options = { embeds: [await getEmbed(t, entry)] }; try { await resolveOnErrorCodes( @@ -58,21 +60,34 @@ export class UserListener extends Listener { } } - private fetchModerationLogMessage(entry: ModerationManager.Entry, channel: GuildTextBasedChannelTypes) { - for (const message of channel.messages.cache.values()) { + private async fetchModerationLogMessage(entry: ModerationManager.Entry, channel: GuildTextBasedChannelTypes) { + const messages = await this.fetchChannelMessages(channel); + for (const message of messages.values()) { if (this.validateModerationLogMessage(message, entry.id)) return message; } return null; } + /** + * Fetch 100 messages from the modlogs channel + */ + private async fetchChannelMessages(channel: GuildTextBasedChannelTypes, remainingRetries = 5): Promise> { + try { + return (await channel.messages.fetch({ limit: 100 })) as Collection; + } catch (error) { + if (error instanceof DiscordAPIError) throw error; + return this.fetchChannelMessages(channel, --remainingRetries); + } + } + private validateModerationLogMessage(message: Message, caseId: number) { return ( - message.author.id === process.env.CLIENT_ID && + isUserSelf(message.author.id) && message.attachments.size === 0 && message.embeds.length === 1 && this.validateModerationLogMessageEmbed(message.embeds[0]) && - message.embeds[0].footer!.text === `Case ${caseId}` + message.embeds[0].footer!.text.includes(caseId.toString()) ); }