Skip to content

Commit

Permalink
refactor: handle errors in the reaction add logger (#2581)
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranet authored Feb 11, 2024
1 parent f512fa6 commit e7f4bb4
Show file tree
Hide file tree
Showing 3 changed files with 97 additions and 47 deletions.
5 changes: 3 additions & 2 deletions src/languages/en-US/events/reactions.json
Original file line number Diff line number Diff line change
@@ -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!"
}
9 changes: 5 additions & 4 deletions src/lib/i18n/languageKeys/keys/events/reactions/All.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FT, T } from '#lib/types';

export const Reaction = T<string>('events/reactions:reaction');
export const Filter = FT<{ user: string }, string>('events/reactions:filter');
export const FilterFooter = T<string>('events/reactions:filterFooter');
export const SelfRoleHierarchy = T<string>('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');
130 changes: 89 additions & 41 deletions src/listeners/reactions/rawReactionAddNotify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,90 +2,113 @@ 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<Listener.Options>({ event: Events.RawReactionAdd })
export class UserListener extends Listener {
private readonly kCountCache = new Collection<string, InternalCacheEntry>();
private readonly kSyncCache = new Collection<string, Promise<InternalCacheEntry>>();
private readonly kSyncCache = new Collection<string, Promise<InternalCacheEntry | null>>();
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]
]
);

const emojiId = getEmojiId(emoji);
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() {
super.onUnload();
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<number | null> {
const id = `${data.messageId}.${getEmojiId(emoji)}`;

// Pull from sync queue, and if it exists, await
Expand All @@ -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<InternalCacheEntry | null> {
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);

Expand All @@ -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 {
Expand Down

0 comments on commit e7f4bb4

Please sign in to comment.