From e7f4bb4064fa8de9f61080a8b412f2f7865e7d4a Mon Sep 17 00:00:00 2001 From: Aura Date: Sun, 11 Feb 2024 16:45:05 +0100 Subject: [PATCH] refactor: handle errors in the reaction add logger (#2581) --- src/languages/en-US/events/reactions.json | 5 +- .../languageKeys/keys/events/reactions/All.ts | 9 +- .../reactions/rawReactionAddNotify.ts | 130 ++++++++++++------ 3 files changed, 97 insertions(+), 47 deletions(-) diff --git a/src/languages/en-US/events/reactions.json b/src/languages/en-US/events/reactions.json index c80bdf75fad..94ab655d53d 100644 --- a/src/languages/en-US/events/reactions.json +++ b/src/languages/en-US/events/reactions.json @@ -1,6 +1,7 @@ { - "reaction": "Reaction Added", + "reactionDescription": "Reacted with {{emoji}} on {{message}}", + "reactionFooter": "First Reaction Added", "filterFooter": "Filtered Reaction", "filter": "{{REDCROSS}} Hey {{user}}, please do not add that reaction!", - "selfRoleHierarchy": "{{REDCROSS}} My role needs to be higher than all self-assignable roles, otherwise I can't grant them to people!" + "selfRoleHierarchy": "{{REDCROSS}} My role needs to be higher than all self-assignable roles, otherwise I can't grant them to people!" } diff --git a/src/lib/i18n/languageKeys/keys/events/reactions/All.ts b/src/lib/i18n/languageKeys/keys/events/reactions/All.ts index d934eb706d0..1ffed9c401c 100644 --- a/src/lib/i18n/languageKeys/keys/events/reactions/All.ts +++ b/src/lib/i18n/languageKeys/keys/events/reactions/All.ts @@ -1,6 +1,7 @@ import { FT, T } from '#lib/types'; -export const Reaction = T('events/reactions:reaction'); -export const Filter = FT<{ user: string }, string>('events/reactions:filter'); -export const FilterFooter = T('events/reactions:filterFooter'); -export const SelfRoleHierarchy = T('events/reactions:selfRoleHierarchy'); +export const ReactionDescription = FT<{ emoji: string; message: string }>('events/reactions:reactionDescription'); +export const ReactionFooter = T('events/reactions:reactionFooter'); +export const Filter = FT<{ user: string }>('events/reactions:filter'); +export const FilterFooter = T('events/reactions:filterFooter'); +export const SelfRoleHierarchy = T('events/reactions:selfRoleHierarchy'); diff --git a/src/listeners/reactions/rawReactionAddNotify.ts b/src/listeners/reactions/rawReactionAddNotify.ts index 788c03b837d..44ce947b35e 100644 --- a/src/listeners/reactions/rawReactionAddNotify.ts +++ b/src/listeners/reactions/rawReactionAddNotify.ts @@ -2,38 +2,56 @@ import { GuildSettings, readSettings } from '#lib/database'; import { api } from '#lib/discord/Api'; import { LanguageKeys } from '#lib/i18n/languageKeys'; import { Events } from '#lib/types'; -import type { LLRCData } from '#utils/LongLivingReactionCollector'; +import type { LLRCData, LLRCDataEmoji } from '#utils/LongLivingReactionCollector'; +import { toErrorCodeResult } from '#utils/common'; import { Colors } from '#utils/constants'; -import { getCustomEmojiUrl, getEmojiId, getEmojiReactionFormat, getEncodedTwemoji, getTwemojiUrl, type SerializedEmoji } from '#utils/functions'; +import { + getCodeStyle, + getCustomEmojiUrl, + getEmojiId, + getEmojiReactionFormat, + getEncodedTwemoji, + getLogPrefix, + getLogger, + getTwemojiUrl, + type SerializedEmoji +} from '#utils/functions'; import { getFullEmbedAuthor } from '#utils/util'; import { EmbedBuilder } from '@discordjs/builders'; import { ApplyOptions } from '@sapphire/decorators'; import type { GuildTextBasedChannelTypes } from '@sapphire/discord.js-utilities'; import { Listener } from '@sapphire/framework'; +import type { TFunction } from '@sapphire/plugin-i18next'; import { isNullish } from '@sapphire/utilities'; -import { Collection, PermissionFlagsBits } from 'discord.js'; +import { + Collection, + PermissionFlagsBits, + RESTJSONErrorCodes, + inlineCode, + messageLink, + type RESTGetAPIChannelMessageReactionUsersResult +} from 'discord.js'; @ApplyOptions({ event: Events.RawReactionAdd }) export class UserListener extends Listener { private readonly kCountCache = new Collection(); - private readonly kSyncCache = new Collection>(); + private readonly kSyncCache = new Collection>(); private kTimerSweeper: NodeJS.Timeout | null = null; public async run(data: LLRCData, emoji: SerializedEmoji) { // If the bot cannot fetch messages, do not proceed: - if (!this.canFetchMessages(data.channel)) return; + if (!this.#canFetchMessages(data.channel)) return; - const key = GuildSettings.Channels.Logs.Reaction; - const [allowedEmojis, logChannelId, twemojiEnabled, ignoreChannels, ignoreReactionAdd, ignoreAllEvents, t] = await readSettings( + const [t, targetChannelId, allowedEmojis, twemojiEnabled, ignoreChannels, ignoreReactionAdd, ignoreAllEvents] = await readSettings( data.guild, (settings) => [ + settings.getLanguage(), + settings[GuildSettings.Channels.Logs.Reaction], settings[GuildSettings.Selfmod.Reactions.Allowed], - settings[key], settings[GuildSettings.Events.IncludeTwemoji], settings[GuildSettings.Messages.IgnoreChannels], settings[GuildSettings.Channels.Ignore.ReactionAdd], - settings[GuildSettings.Channels.Ignore.All], - settings.getLanguage() + settings[GuildSettings.Channels.Ignore.All] ] ); @@ -41,38 +59,30 @@ export class UserListener extends Listener { if (allowedEmojis.some((allowedEmoji) => getEmojiId(allowedEmoji) === emojiId)) return; this.container.client.emit(Events.ReactionBlocked, data, emoji); - if (isNullish(logChannelId) || (!twemojiEnabled && data.emoji.id === null)) return; + if (isNullish(targetChannelId) || (!twemojiEnabled && data.emoji.id === null)) return; if (ignoreChannels.includes(data.channel.id)) return; if (ignoreReactionAdd.some((id) => id === data.channel.id || data.channel.parentId === id)) return; if (ignoreAllEvents.some((id) => id === data.channel.id || data.channel.parentId === id)) return; - if ((await this.retrieveCount(data, emoji)) > 1) return; + const count = await this.#retrieveCount(data, emoji); + if (isNullish(count) || count > 1) return; const user = await this.container.client.users.fetch(data.userId); if (user.bot) return; - this.container.client.emit(Events.GuildMessageLog, data.guild, logChannelId, key, () => - new EmbedBuilder() - .setColor(Colors.Green) - .setAuthor(getFullEmbedAuthor(user)) - .setThumbnail( - data.emoji.id === null // - ? getTwemojiUrl(getEncodedTwemoji(data.emoji.name!)) - : getCustomEmojiUrl(data.emoji.id, data.emoji.animated) - ) - .setDescription( - [ - `**Emoji**: ${data.emoji.name}${data.emoji.id === null ? '' : ` [${data.emoji.id}]`}`, - `**Channel**: ${data.channel}`, - `**Message**: [${t(LanguageKeys.Misc.JumpTo)}](https://discord.com/channels/${data.guild.id}/${data.channel.id}/${ - data.messageId - })` - ].join('\n') - ) - .setFooter({ text: `${t(LanguageKeys.Events.Reactions.Reaction)} • ${data.channel.name}` }) - .setTimestamp() - ); + await getLogger(data.guild).send({ + key: GuildSettings.Channels.Logs.Reaction, + channelId: targetChannelId, + makeMessage: () => + new EmbedBuilder() + .setColor(Colors.Green) + .setAuthor(getFullEmbedAuthor(user)) + .setThumbnail(this.#renderThumbnail(data.emoji)) + .setDescription(this.#renderDescription(t, data)) + .setFooter({ text: t(LanguageKeys.Events.Reactions.ReactionFooter) }) + .setTimestamp() + }); } public override onUnload() { @@ -80,12 +90,25 @@ export class UserListener extends Listener { if (this.kTimerSweeper) clearInterval(this.kTimerSweeper); } - private canFetchMessages(channel: GuildTextBasedChannelTypes) { + #renderThumbnail(emoji: LLRCDataEmoji) { + return emoji.id === null // + ? getTwemojiUrl(getEncodedTwemoji(emoji.name!)) + : getCustomEmojiUrl(emoji.id, emoji.animated); + } + + #renderDescription(t: TFunction, data: LLRCData) { + return t(LanguageKeys.Events.Reactions.ReactionDescription, { + emoji: data.emoji.id ? `${data.emoji.name} (${inlineCode(data.emoji.id)})` : data.emoji.name, + message: messageLink(data.channel.id, data.messageId, data.guild.id) + }); + } + + #canFetchMessages(channel: GuildTextBasedChannelTypes) { const permissions = channel.permissionsFor(this.container.client.id!); - return !isNullish(permissions) && permissions.has(PermissionFlagsBits.ReadMessageHistory); + return !isNullish(permissions) && permissions.has(PermissionFlagsBits.ViewChannel | PermissionFlagsBits.ReadMessageHistory); } - private async retrieveCount(data: LLRCData, emoji: SerializedEmoji) { + async #retrieveCount(data: LLRCData, emoji: SerializedEmoji): Promise { const id = `${data.messageId}.${getEmojiId(emoji)}`; // Pull from sync queue, and if it exists, await @@ -101,14 +124,23 @@ export class UserListener extends Listener { } // Pull the reactions from the API - const promise = this.fetchCount(data, emoji, id); + const promise = this.#fetchCount(data, emoji, id); this.kSyncCache.set(id, promise); - return (await promise).count; + + const resolved = await promise; + return isNullish(resolved) ? null : resolved.count; } - private async fetchCount(data: LLRCData, emoji: SerializedEmoji, id: string) { - const users = await api().channels.getMessageReactions(data.channel.id, data.messageId, getEmojiReactionFormat(emoji)); - const count: InternalCacheEntry = { count: users.length, sweepAt: Date.now() + 120000 }; + async #fetchCount(data: LLRCData, emoji: SerializedEmoji, id: string): Promise { + const result = await toErrorCodeResult(api().channels.getMessageReactions(data.channel.id, data.messageId, getEmojiReactionFormat(emoji))); + return result.match({ + ok: (data) => this.#fetchCountOk(data, id), + err: (error) => this.#fetchCountErr(error) + }); + } + + #fetchCountOk(data: RESTGetAPIChannelMessageReactionUsersResult, id: string): InternalCacheEntry { + const count: InternalCacheEntry = { count: data.length, sweepAt: Date.now() + 120000 }; this.kCountCache.set(id, count); this.kSyncCache.delete(id); @@ -125,6 +157,22 @@ export class UserListener extends Listener { return count; } + + #fetchCountErr(code: RESTJSONErrorCodes): InternalCacheEntry | null { + if (!UserListener.IgnoreReactionCountFetchErrors.includes(code)) { + this.container.logger.error(`${getLogPrefix(this)} ${getCodeStyle(code)} Failed to fetch message reaction count.`); + } + + return null; + } + + private static readonly IgnoreReactionCountFetchErrors = [ + RESTJSONErrorCodes.UnknownMessage, + RESTJSONErrorCodes.UnknownChannel, + RESTJSONErrorCodes.UnknownGuild, + RESTJSONErrorCodes.UnknownEmoji, + RESTJSONErrorCodes.MissingAccess + ]; } interface InternalCacheEntry {