From 67eb2217b18ffed6aa2c51b680f88b12aa8dd214 Mon Sep 17 00:00:00 2001 From: dev-737 <73829355+dev-737@users.noreply.github.com> Date: Sat, 5 Oct 2024 21:52:35 +0530 Subject: [PATCH 1/2] feat(message-info): improve message info command - Refactor `modActionHandlers` to be a readonly property - Handle nullable `guildId` and `userId` in `checkBlacklists` function - Remove `expiry` parameter from `buildButtons` function as it's not needed - Simplify `displayName` calculation in `buildUserInfoEmbed` - Add SonarLint configuration to VSCode settings --- .vscode/settings.json | 6 +- src/commands/context-menu/messageInfo.ts | 24 +- src/commands/context-menu/modActions.ts | 2 +- src/core/BaseCommand.ts | 4 +- src/core/FileLoader.ts | 4 +- .../NetworkReactionInteraction.ts | 340 +++++++++++------- src/utils/reaction/helpers.ts | 20 +- 7 files changed, 242 insertions(+), 158 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 30e8939e..8175e631 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,5 +6,9 @@ "eslint.validate": ["javascript", "typescript"], "eslint.useFlatConfig": true, "typescript.tsdk": "node_modules/typescript/lib", - "typescript.preferences.importModuleSpecifier": "non-relative" + "typescript.preferences.importModuleSpecifier": "non-relative", + "sonarlint.connectedMode.project": { + "connectionId": "discord-interchat", + "projectKey": "Discord-InterChat_InterChat" + } } diff --git a/src/commands/context-menu/messageInfo.ts b/src/commands/context-menu/messageInfo.ts index 2709a2a9..e266e30c 100644 --- a/src/commands/context-menu/messageInfo.ts +++ b/src/commands/context-menu/messageInfo.ts @@ -89,8 +89,7 @@ export default class MessageInfo extends BaseCommand { const connection = (await getHubConnections(originalMsg.hub.id))?.find( (c) => c.connected && c.serverId === originalMsg.serverId, ); - const expiry = new Date(Date.now() + 5 * 60 * 1000); // 5 minutes - const components = this.buildButtons(expiry, locale, { + const components = this.buildButtons(locale, { buildModActions: isStaffOrHubMod(interaction.user.id, originalMsg.hub), inviteButtonUrl: connection?.invite, }); @@ -258,7 +257,7 @@ export default class MessageInfo extends BaseCommand { await interaction.deferUpdate(); const createdAt = Math.round(author.createdTimestamp / 1000); const hubsOwned = await db.hub.count({ where: { ownerId: author.id } }); - const displayName = author.globalName || 'Not Set.'; + const displayName = author.globalName ?? 'Not Set.'; const userEmbed = new EmbedBuilder() .setDescription(`### ${emojis.info} ${author.username}`) @@ -320,7 +319,13 @@ export default class MessageInfo extends BaseCommand { { originalMsg }: ModActionsOpts, ) { if (!isValidDbMsgWithHubId(originalMsg)) return; - if (!originalMsg.hub || isStaffOrHubMod(interaction.user.id, originalMsg.hub)) return; + if (!originalMsg.hub || !isStaffOrHubMod(interaction.user.id, originalMsg.hub)) { + await interaction.reply({ + content: t({ phrase: 'hub.notFound_mod', locale: 'en' }, { emoji: emojis.no }), + ephemeral: true, + }); + return; + }; const { buttons, embed } = await modActionsPanel.buildMessage(interaction, originalMsg); await interaction.reply({ embeds: [embed], components: buttons, ephemeral: true }); @@ -387,7 +392,6 @@ export default class MessageInfo extends BaseCommand { } private buildButtons( - expiry: Date, locale: supportedLocaleCodes = 'en', opts?: { buildModActions?: boolean; inviteButtonUrl?: string | null }, ) { @@ -396,7 +400,7 @@ export default class MessageInfo extends BaseCommand { .setLabel(t({ phrase: 'msgInfo.buttons.report', locale })) .setStyle(ButtonStyle.Danger) .setCustomId( - new CustomID().setIdentifier('msgInfo', 'report').setExpiry(expiry).toString(), + new CustomID().setIdentifier('msgInfo', 'report').toString(), ), ]; @@ -407,7 +411,7 @@ export default class MessageInfo extends BaseCommand { .setEmoji('🛠️') .setLabel('Mod Actions') .setCustomId( - new CustomID().setIdentifier('msgInfo', 'modActions').setExpiry(expiry).toString(), + new CustomID().setIdentifier('msgInfo', 'modActions').toString(), ), ); } @@ -428,19 +432,19 @@ export default class MessageInfo extends BaseCommand { .setStyle(ButtonStyle.Secondary) .setDisabled(true) .setCustomId( - new CustomID().setIdentifier('msgInfo', 'msgInfo').setExpiry(expiry).toString(), + new CustomID().setIdentifier('msgInfo', 'msgInfo').toString(), ), new ButtonBuilder() .setLabel(t({ phrase: 'msgInfo.buttons.server', locale })) .setStyle(ButtonStyle.Secondary) .setCustomId( - new CustomID().setIdentifier('msgInfo', 'serverInfo').setExpiry(expiry).toString(), + new CustomID().setIdentifier('msgInfo', 'serverInfo').toString(), ), new ButtonBuilder() .setLabel(t({ phrase: 'msgInfo.buttons.user', locale })) .setStyle(ButtonStyle.Secondary) .setCustomId( - new CustomID().setIdentifier('msgInfo', 'userInfo').setExpiry(expiry).toString(), + new CustomID().setIdentifier('msgInfo', 'userInfo').toString(), ), ), new ActionRowBuilder({ components: extras }), diff --git a/src/commands/context-menu/modActions.ts b/src/commands/context-menu/modActions.ts index e37b75f2..78651309 100644 --- a/src/commands/context-menu/modActions.ts +++ b/src/commands/context-menu/modActions.ts @@ -37,7 +37,7 @@ export default class Blacklist extends BaseCommand { dm_permission: false, }; - private modActionHandlers: Record; + private readonly modActionHandlers: Record; constructor() { super(); diff --git a/src/core/BaseCommand.ts b/src/core/BaseCommand.ts index 3a197489..a78bb376 100644 --- a/src/core/BaseCommand.ts +++ b/src/core/BaseCommand.ts @@ -1,7 +1,6 @@ import { emojis } from '#main/config/Constants.js'; import { MetadataHandler } from '#main/core/FileLoader.js'; import { InteractionFunction } from '#main/decorators/Interaction.js'; -import { TranslationKeys } from '#main/types/locale.js'; import { InfoEmbed } from '#main/utils/EmbedUtils.js'; import { supportedLocaleCodes, t } from '#main/utils/Locale.js'; import Logger from '#main/utils/Logger.js'; @@ -127,7 +126,7 @@ export default abstract class BaseCommand { async replyEmbed( interaction: RepliableInteraction | MessageComponentInteraction, - desc: keyof TranslationKeys | (string & {}), + desc: string, opts?: { content?: string; title?: string; @@ -161,7 +160,6 @@ export default abstract class BaseCommand { opts.commandsMap.set(this.data.name, this); this.loadCommandInteractions(this, opts.interactionsMap); } - else { const parentCommand = Object.getPrototypeOf(this.constructor) as typeof BaseCommand; parentCommand.subcommands?.set(fileName.replace('.js', ''), this); diff --git a/src/core/FileLoader.ts b/src/core/FileLoader.ts index 405f7485..129d1f31 100644 --- a/src/core/FileLoader.ts +++ b/src/core/FileLoader.ts @@ -38,8 +38,8 @@ export class MetadataHandler { } export class FileLoader { - private baseDir: string; - private recursive: boolean; + private readonly baseDir: string; + private readonly recursive: boolean; constructor(baseDir: string, opts?: { recursive: boolean }) { this.baseDir = baseDir; diff --git a/src/interactions/NetworkReactionInteraction.ts b/src/interactions/NetworkReactionInteraction.ts index 4f6acecc..87a4c221 100644 --- a/src/interactions/NetworkReactionInteraction.ts +++ b/src/interactions/NetworkReactionInteraction.ts @@ -2,39 +2,68 @@ import Constants, { emojis } from '#main/config/Constants.js'; import { RegisterInteractionHandler } from '#main/decorators/Interaction.js'; import { HubSettingsBitField } from '#main/modules/BitFields.js'; import HubSettingsManager from '#main/modules/HubSettingsManager.js'; -import { CustomID } from '#main/utils/CustomID.js'; +import { CustomID, ParsedCustomId } from '#main/utils/CustomID.js'; import db from '#main/utils/Db.js'; import { t } from '#main/utils/Locale.js'; -import { removeReaction, addReaction, updateReactions } from '#main/utils/reaction/actions.js'; +import { addReaction, removeReaction, updateReactions } from '#main/utils/reaction/actions.js'; import { checkBlacklists } from '#main/utils/reaction/helpers.js'; import sortReactions from '#main/utils/reaction/sortReactions.js'; import { getEmojiId } from '#main/utils/Utils.js'; +import { broadcastedMessages, Hub, originalMessages } from '@prisma/client'; import { stripIndents } from 'common-tags'; import { - ButtonInteraction, + ActionRowBuilder, AnySelectMenuInteraction, + ButtonInteraction, + EmbedBuilder, Snowflake, - ActionRowBuilder, StringSelectMenuBuilder, - EmbedBuilder, time, } from 'discord.js'; +type OriginalMessageT = originalMessages & { hub: Hub, broadcastMsgs: broadcastedMessages[] }; +type DbMessageT = broadcastedMessages & { originalMsg: OriginalMessageT }; + export default class NetworkReactionInteraction { @RegisterInteractionHandler('reaction_') async listenForReactionButton( interaction: ButtonInteraction | AnySelectMenuInteraction, ): Promise { - /** Listens for a reaction button or select menu interaction and updates the reactions accordingly. */ await interaction.deferUpdate(); - if (!interaction.inCachedGuild()) return; + const { customId, messageId } = this.getInteractionDetails(interaction); + const messageInDb = await this.fetchMessageFromDb(messageId); + + if (!this.isReactionAllowed(messageInDb)) return; + + const { userBlacklisted, serverBlacklisted } = await this.checkUserPermissions( + messageInDb, + interaction, + ); + if (userBlacklisted || serverBlacklisted) { + await this.handleBlacklistedUser(interaction, userBlacklisted); + return; + } + + if (await this.isUserOnCooldown(interaction)) return; + + if (customId.suffix === 'view_all') { + await this.handleViewAllReactions(interaction, messageId); + } + else { + await this.handleReactionToggle(interaction, messageInDb, customId); + } + } + + private getInteractionDetails(interaction: ButtonInteraction | AnySelectMenuInteraction) { const customId = CustomID.parseCustomId(interaction.customId); - const cooldown = interaction.client.reactionCooldowns.get(interaction.user.id); const messageId = interaction.isButton() ? interaction.message.id : customId.args[0]; + return { customId, messageId }; + } - const messageInDb = await db.broadcastedMessages.findFirst({ + private async fetchMessageFromDb(messageId: string) { + return db.broadcastedMessages.findFirst({ where: { messageId }, include: { originalMsg: { @@ -45,151 +74,196 @@ export default class NetworkReactionInteraction { }, }, }); + } - if ( - !messageInDb?.originalMsg.hub || - !new HubSettingsBitField(messageInDb.originalMsg.hub.settings).has('Reactions') - ) { - return; - } + private isReactionAllowed(messageInDb: DbMessageT | null): messageInDb is DbMessageT { + return Boolean( + messageInDb?.originalMsg.hub && + new HubSettingsBitField(messageInDb.originalMsg.hub.settings).has('Reactions'), + ); + } - const { userBlacklisted, serverBlacklisted } = await checkBlacklists( + private async checkUserPermissions( + messageInDb: DbMessageT, + interaction: ButtonInteraction | AnySelectMenuInteraction, + ) { + return checkBlacklists( messageInDb.originalMsg.hub.id, - interaction.guildId, + interaction.guild?.id ?? null, interaction.user.id, ); + } + + private async handleBlacklistedUser( + interaction: ButtonInteraction | AnySelectMenuInteraction, + userBlacklisted: boolean, + ) { + const { userManager } = interaction.client; + const locale = await userManager.getUserLocale(interaction.user.id); + const phrase = userBlacklisted ? 'errors.userBlacklisted' : 'errors.serverBlacklisted'; + await interaction.followUp({ + content: t({ phrase, locale }, { emoji: emojis.no }), + ephemeral: true, + }); + } - // add user to cooldown list + private async isUserOnCooldown(interaction: ButtonInteraction | AnySelectMenuInteraction) { + const cooldown = interaction.client.reactionCooldowns.get(interaction.user.id); + if (cooldown && cooldown > Date.now()) { + const timeString = time(Math.round(cooldown / 1000), 'R'); + await interaction.followUp({ + content: `A little quick there! You can react again ${timeString}!`, + ephemeral: true, + }); + return true; + } interaction.client.reactionCooldowns.set(interaction.user.id, Date.now() + 3000); + return false; + } - const dbReactions = (messageInDb.originalMsg.reactions?.valueOf() ?? {}) as { - [key: string]: Snowflake[]; - }; + private async handleViewAllReactions( + interaction: ButtonInteraction | AnySelectMenuInteraction, + messageId: string, + ) { + const networkMessage = await this.fetchNetworkMessage(messageId); + if (!networkMessage?.originalMsg.reactions || !networkMessage?.originalMsg.hub) { + await interaction.followUp({ + content: 'There are no more reactions to view.', + ephemeral: true, + }); + return; + } - if (customId.suffix === 'view_all') { - const networkMessage = await db.broadcastedMessages.findFirst({ - where: { messageId }, - include: { - originalMsg: { - include: { hub: { include: { connections: { where: { connected: true } } } } }, - }, + const dbReactions = networkMessage.originalMsg.reactions as { [key: string]: Snowflake[] }; + const { reactionMenu, reactionString, totalReactions } = this.buildReactionMenu( + dbReactions, + interaction, + networkMessage.originalMsg.hub, + ); + + const embed = this.buildReactionEmbed(reactionString, totalReactions); + + await interaction.followUp({ + embeds: [embed], + components: [reactionMenu], + ephemeral: true, + }); + } + + private async fetchNetworkMessage(messageId: string) { + return db.broadcastedMessages.findFirst({ + where: { messageId }, + include: { + originalMsg: { + include: { hub: { include: { connections: { where: { connected: true } } } } }, }, - }); + }, + }); + } - if (!networkMessage?.originalMsg.reactions || !networkMessage?.originalMsg.hub) { - await interaction.followUp({ - content: 'There are no more reactions to view.', - ephemeral: true, - }); - return; - } - - const sortedReactions = sortReactions(dbReactions); - let totalReactions = 0; - let reactionString = ''; - const reactionMenu = new ActionRowBuilder().addComponents( - new StringSelectMenuBuilder() - .setCustomId( - new CustomID().setIdentifier('reaction_').addArgs(interaction.message.id).toString(), - ) - .setPlaceholder('Add a reaction'), - ); - - const { hub } = networkMessage.originalMsg; - const hubSettings = new HubSettingsManager(hub.id, hub.settings); - if (!hubSettings.getSetting('Reactions')) reactionMenu.components[0].setDisabled(true); - - sortedReactions.forEach((r, index) => { - if (r[1].length === 0 || index >= 10) return; - reactionMenu.components[0].addOptions({ - label: 'React/Unreact', - value: r[0], - emoji: getEmojiId(r[0]), - }); - totalReactions++; - reactionString += `- ${r[0]}: ${r[1].length}\n`; + private buildReactionMenu( + dbReactions: { [key: string]: Snowflake[] }, + interaction: ButtonInteraction | AnySelectMenuInteraction, + hub: Hub, + ) { + const sortedReactions = sortReactions(dbReactions); + let totalReactions = 0; + let reactionString = ''; + const reactionMenu = new ActionRowBuilder().addComponents( + new StringSelectMenuBuilder() + .setCustomId( + new CustomID().setIdentifier('reaction_').addArgs(interaction.message.id).toString(), + ) + .setPlaceholder('Add a reaction'), + ); + + const hubSettings = new HubSettingsManager(hub.id, hub.settings); + if (!hubSettings.getSetting('Reactions')) reactionMenu.components[0].setDisabled(true); + + sortedReactions.forEach((r, index) => { + if (r[1].length === 0 || index >= 10) return; + reactionMenu.components[0].addOptions({ + label: 'React/Unreact', + value: r[0], + emoji: getEmojiId(r[0]), }); + totalReactions++; + reactionString += `- ${r[0]}: ${r[1].length}\n`; + }); - const embed = new EmbedBuilder() - .setThumbnail(interaction.client.user.displayAvatarURL()) - .setDescription( - stripIndents` - ## ${emojis.clipart} Reactions - - ${reactionString || 'No reactions yet!'} - - **Total Reactions:** - __${totalReactions}__ - `, - ) - .setColor(Constants.Colors.invisible); + return { reactionMenu, reactionString, totalReactions }; + } + private buildReactionEmbed(reactionString: string, totalReactions: number) { + return new EmbedBuilder() + .setDescription( + stripIndents` + ## ${emojis.clipart} Reactions + + ${reactionString || 'No reactions yet!'} + + **Total Reactions:** + __${totalReactions}__ + `, + ) + .setColor(Constants.Colors.invisible); + } + + private async handleReactionToggle( + interaction: ButtonInteraction | AnySelectMenuInteraction, + messageInDb: DbMessageT, + customId: ParsedCustomId, + ) { + const dbReactions = (messageInDb.originalMsg.reactions?.valueOf() ?? {}) as { + [key: string]: Snowflake[]; + }; + + const reactedEmoji = interaction.isStringSelectMenu() ? interaction.values[0] : customId.suffix; + const emojiAlreadyReacted = dbReactions[reactedEmoji]; + + if (!emojiAlreadyReacted) { await interaction.followUp({ - embeds: [embed], - components: [reactionMenu], + content: `${emojis.no} This reaction doesn't exist.`, ephemeral: true, }); + return; + } + + if (emojiAlreadyReacted.includes(interaction.user.id)) { + removeReaction(dbReactions, interaction.user.id, reactedEmoji); } else { - const { userManager } = interaction.client; - const locale = await userManager.getUserLocale(interaction.user.id); + addReaction(dbReactions, interaction.user.id, reactedEmoji); + } - if (userBlacklisted || serverBlacklisted) { - const phrase = userBlacklisted ? 'errors.userBlacklisted' : 'errors.serverBlacklisted'; - await interaction.followUp({ - content: t({ phrase, locale }, { emoji: emojis.no }), - ephemeral: true, - }); - return; - } - - if (cooldown && cooldown > Date.now()) { - const timeString = time(Math.round(cooldown / 1000), 'R'); - await interaction.followUp({ - content: `A little quick there! You can react again ${timeString}!`, - ephemeral: true, - }); - return; - } - - const reactedEmoji = interaction.isStringSelectMenu() - ? interaction.values[0] - : customId.suffix; - const emojiAlreadyReacted = dbReactions[reactedEmoji]; - - if (!emojiAlreadyReacted) { - await interaction.followUp({ - content: `${emojis.no} This reaction doesn't exist.`, - ephemeral: true, - }); - return; - } - - if (emojiAlreadyReacted.includes(interaction.user.id)) { - removeReaction(dbReactions, interaction.user.id, reactedEmoji); - } - else { - addReaction(dbReactions, interaction.user.id, reactedEmoji); - } - - await db.originalMessages.update({ - where: { messageId: messageInDb.originalMsgId }, - data: { reactions: dbReactions }, - }); + await this.updateReactionsInDb(messageInDb, dbReactions); + await this.sendReactionConfirmation(interaction, emojiAlreadyReacted, reactedEmoji); + await updateReactions(messageInDb.originalMsg.broadcastMsgs, dbReactions); + } + + private async updateReactionsInDb( + messageInDb: DbMessageT, + dbReactions: { [key: string]: Snowflake[] }, + ) { + await db.originalMessages.update({ + where: { messageId: messageInDb.originalMsgId }, + data: { reactions: dbReactions }, + }); + } - if (interaction.isStringSelectMenu()) { - /** FIXME: seems like `emojiAlreadyReacted` is getting mutated somewhere */ - const action = emojiAlreadyReacted.includes(interaction.user.id) ? 'reacted' : 'unreacted'; - interaction - .followUp({ - content: `You have ${action} with ${reactedEmoji}!`, - ephemeral: true, - }) - .catch(() => null); - } - - // reflect the changes in the message's buttons - await updateReactions(messageInDb.originalMsg.broadcastMsgs, dbReactions); + private async sendReactionConfirmation( + interaction: ButtonInteraction | AnySelectMenuInteraction, + emojiAlreadyReacted: Snowflake[], + reactedEmoji: string, + ) { + if (interaction.isStringSelectMenu()) { + const action = emojiAlreadyReacted.includes(interaction.user.id) ? 'reacted' : 'unreacted'; + interaction + .followUp({ + content: `You have ${action} with ${reactedEmoji}!`, + ephemeral: true, + }) + .catch(() => null); } } } diff --git a/src/utils/reaction/helpers.ts b/src/utils/reaction/helpers.ts index 37b0831c..e195d498 100644 --- a/src/utils/reaction/helpers.ts +++ b/src/utils/reaction/helpers.ts @@ -12,17 +12,21 @@ import { isBlacklisted } from '#main/utils/moderation/blacklistUtils.js'; */ export const checkBlacklists = async ( hubId: string, - guildId: string, - userId: string, + guildId: string | null, + userId: string | null, ) => { - const userBlacklistManager = new BlacklistManager(new UserInfractionManager(userId)); - const guildBlacklistManager = new BlacklistManager(new ServerInfractionManager(guildId)); + const userBlacklistManager = userId + ? new BlacklistManager(new UserInfractionManager(userId)) + : undefined; + const guildBlacklistManager = guildId + ? new BlacklistManager(new ServerInfractionManager(guildId)) + : undefined; - const userBlacklist = await userBlacklistManager.fetchBlacklist(hubId); - const serverBlacklist = await guildBlacklistManager.fetchBlacklist(hubId); + const userBlacklist = await userBlacklistManager?.fetchBlacklist(hubId); + const serverBlacklist = await guildBlacklistManager?.fetchBlacklist(hubId); return { - userBlacklisted: isBlacklisted(userBlacklist), - serverBlacklisted: isBlacklisted(serverBlacklist), + userBlacklisted: isBlacklisted(userBlacklist ?? null), + serverBlacklisted: isBlacklisted(serverBlacklist ?? null), }; }; From e2603eea3bee6f5de0853947b47e5ea787c11626 Mon Sep 17 00:00:00 2001 From: codefactor-io Date: Sat, 5 Oct 2024 16:22:58 +0000 Subject: [PATCH 2/2] [CodeFactor] Apply fixes to commit 67eb221 [ci skip] [skip ci] --- src/commands/context-menu/messageInfo.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/context-menu/messageInfo.ts b/src/commands/context-menu/messageInfo.ts index e266e30c..7f6bedbb 100644 --- a/src/commands/context-menu/messageInfo.ts +++ b/src/commands/context-menu/messageInfo.ts @@ -325,7 +325,7 @@ export default class MessageInfo extends BaseCommand { ephemeral: true, }); return; - }; + } const { buttons, embed } = await modActionsPanel.buildMessage(interaction, originalMsg); await interaction.reply({ embeds: [embed], components: buttons, ephemeral: true });