From 585585be904f2a566b73fe6aa107cdc22ddb1480 Mon Sep 17 00:00:00 2001 From: dev-737 <73829355+dev-737@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:25:54 +0530 Subject: [PATCH 1/6] reply mentions --- src/commands/context-menu/editMsg.ts | 6 +- src/commands/slash/Main/set/index.ts | 13 ++ src/commands/slash/Main/set/reply_mentions.ts | 27 ++++ src/core/BaseClient.ts | 7 ++ src/events/messageCreate.ts | 118 +++++++++++------- src/events/webhooksUpdate.ts | 3 +- src/modules/ServerBlacklistManager.ts | 2 +- src/modules/UserDbManager.ts | 20 ++- src/scripts/guilds/goals.ts | 14 ++- src/scripts/network/Types.d.ts | 42 +++++++ src/scripts/network/helpers.ts | 42 ++++--- src/scripts/network/messageFormatters.ts | 100 ++++++--------- src/typings/index.d.ts | 3 +- src/utils/Channels.ts | 5 + src/utils/ConnectedList.ts | 112 +++++++++-------- src/utils/HubLogger/Default.ts | 6 +- src/utils/RandomComponents.ts | 8 +- 17 files changed, 334 insertions(+), 194 deletions(-) create mode 100644 src/commands/slash/Main/set/reply_mentions.ts create mode 100644 src/scripts/network/Types.d.ts create mode 100644 src/utils/Channels.ts diff --git a/src/commands/context-menu/editMsg.ts b/src/commands/context-menu/editMsg.ts index 1c9b7c03f..60fe0d8be 100644 --- a/src/commands/context-menu/editMsg.ts +++ b/src/commands/context-menu/editMsg.ts @@ -163,8 +163,8 @@ export default class EditMessage extends BaseCommand { where: { channelId: { in: originalMsg.broadcastMsgs.map((c) => c.channelId) } }, }); - const results = originalMsg.broadcastMsgs.map(async (element) => { - const settings = channelSettingsArr.find((c) => c.channelId === element.channelId); + const results = originalMsg.broadcastMsgs.map(async (msg) => { + const settings = channelSettingsArr.find((c) => c.channelId === msg.channelId); if (!settings) return false; const webhookURL = settings.webhookURL.split('/'); @@ -182,7 +182,7 @@ export default class EditMessage extends BaseCommand { // finally, edit the message return await webhook - .editMessage(element.messageId, { + .editMessage(msg.messageId, { content, embeds, threadId: settings.parentId ? settings.channelId : undefined, diff --git a/src/commands/slash/Main/set/index.ts b/src/commands/slash/Main/set/index.ts index 65f97a3c9..3a5bfed0f 100644 --- a/src/commands/slash/Main/set/index.ts +++ b/src/commands/slash/Main/set/index.ts @@ -37,6 +37,19 @@ export default class Set extends BaseCommand { }, ], }, + { + name: 'reply_mentions', + description: 'Get pinged when someone replies to your messages.', + type: ApplicationCommandOptionType.Subcommand, + options: [ + { + type: ApplicationCommandOptionType.Boolean, + name: 'enable', + description: 'Enable this setting', + required: true, + }, + ], + }, ], }; diff --git a/src/commands/slash/Main/set/reply_mentions.ts b/src/commands/slash/Main/set/reply_mentions.ts new file mode 100644 index 000000000..64729cfe3 --- /dev/null +++ b/src/commands/slash/Main/set/reply_mentions.ts @@ -0,0 +1,27 @@ +import { emojis } from '#main/utils/Constants.js'; +import { ChatInputCommandInteraction } from 'discord.js'; +import Set from './index.js'; +export default class replyMention extends Set { + async execute(interaction: ChatInputCommandInteraction) { + const { userManager } = interaction.client; + const dbUser = await userManager.getUser(interaction.user.id); + + const mentionOnReply = interaction.options.getBoolean('enable', true); + if (!dbUser) { + await userManager.createUser({ + id: interaction.user.id, + username: interaction.user.username, + mentionOnReply, + }); + } + else { + await userManager.updateUser(interaction.user.id, { mentionOnReply }); + } + + await this.replyEmbed( + interaction, + `${emojis.tick} You will ${mentionOnReply ? 'now' : '**no longer**'} get pinged when someone replies to your messages.`, + { ephemeral: true }, + ); + } +} diff --git a/src/core/BaseClient.ts b/src/core/BaseClient.ts index 4f2723590..cb68f49b7 100644 --- a/src/core/BaseClient.ts +++ b/src/core/BaseClient.ts @@ -11,6 +11,7 @@ import { type Snowflake, type WebhookClient, ActivityType, + Channel, Client, Collection, GatewayIntentBits, @@ -20,6 +21,7 @@ import { RemoveMethods } from '../typings/index.js'; import Constants from '../utils/Constants.js'; import { loadLocales } from '../utils/Locale.js'; import { resolveEval } from '../utils/Utils.js'; +import { isGuildTextBasedChannel } from '#main/utils/Channels.js'; export default class SuperClient extends Client { public static instance: SuperClient; @@ -118,4 +120,9 @@ export default class SuperClient extends Client { getScheduler(): Scheduler { return this.scheduler; } + + /** Check if a channel is a guild channel and is text based. This utility method exists to be used inside broadcastEvals */ + isGuildTextBasedChannel(channel: Channel | null | undefined) { + return isGuildTextBasedChannel(channel); + } } diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index b75ceea67..65ff75962 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -5,11 +5,7 @@ import { getReferredMsgData, trimAndCensorBannedWebhookWords, } from '#main/scripts/network/helpers.js'; -import { - BroadcastOpts, - getCompactMessageFormat, - getEmbedMessageFormat, -} from '#main/scripts/network/messageFormatters.js'; +import type { BroadcastOpts, ReferredMsgData } from '#main/scripts/network/Types.d.ts'; import { runChecks } from '#main/scripts/network/runChecks.js'; import storeMessageData, { NetworkWebhookSendResult, @@ -21,6 +17,11 @@ import { censor } from '#main/utils/Profanity.js'; import { generateJumpButton, getAttachmentURL, isHumanMessage } from '#main/utils/Utils.js'; import { connectedList, hubs } from '@prisma/client'; import { HexColorString, Message, WebhookClient, WebhookMessageCreateOptions } from 'discord.js'; +import { + getCompactMessageFormat, + getEmbedMessageFormat, + getReplyMention, +} from '#main/scripts/network/messageFormatters.js'; export default class MessageCreate extends BaseEventListener<'messageCreate'> { readonly name = 'messageCreate'; @@ -48,26 +49,20 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> { message.channel.sendTyping().catch(() => null); - // fetch the referred message (message being replied to) from discord + // fetch the message being replied-to from discord const referredMessage = message.reference ? await message.fetchReference().catch(() => null) : null; - const { dbReferrence, referredAuthor } = await getReferredMsgData(referredMessage); + const referredMsgData = await getReferredMsgData(referredMessage); const sendResult = await this.broadcastMessage(message, hub, hubConnections, settings, { attachmentURL, - dbReferrence, - referredAuthor, - referredMessage, + referredMsgData, embedColor: connection.embedColor as HexColorString, }); // store the message in the db - await storeMessageData(message, sendResult, connection.hubId, dbReferrence); - } - - private async resolveAttachmentURL(message: Message) { - return message.attachments.first()?.url ?? (await getAttachmentURL(message.content)); + await storeMessageData(message, sendResult, connection.hubId, referredMsgData.dbReferrence); } private async broadcastMessage( @@ -77,27 +72,21 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> { settings: HubSettingsBitField, opts: BroadcastOpts, ) { - const censoredContent = censor(message.content); - const referredContent = - opts.referredMessage && opts.dbReferrence - ? getReferredContent(opts.referredMessage) - : undefined; + const { referredMsgData } = opts; + const referredContent = this.getReferredContent(referredMsgData); + const censoredContent = censor(message.content); const username = this.getUsername(settings, message); - const { embed, censoredEmbed } = buildNetworkEmbed(message, username, censoredContent, { - attachmentURL: opts.attachmentURL, - referredContent, - embedCol: opts.embedColor ?? undefined, - }); const results: NetworkWebhookSendResult[] = await Promise.all( hubConnections.map(async (connection) => { try { const author = { username, avatarURL: message.author.displayAvatarURL() }; const reply = - opts.dbReferrence?.broadcastMsgs.find( - (msg) => msg.channelId === connection.channelId, - ) ?? opts.dbReferrence; + referredMsgData.dbReferrence?.broadcastMsgs.find( + (m) => m.channelId === connection.channelId, + ) ?? referredMsgData.dbReferrence; + const jumpButton = reply ? [ generateJumpButton(author.username, { @@ -108,24 +97,45 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> { ] : undefined; - const messageFormat = connection.compact - ? getCompactMessageFormat(connection, opts, { + let messageFormat; + + if (connection.compact) { + const contents = { + normal: message.content, + referred: referredContent, + censored: censoredContent, + }; + + messageFormat = getCompactMessageFormat(connection, opts, { servername: trimAndCensorBannedWebhookWords(message.guild.name), - referredAuthorName: opts.referredAuthor?.username.slice(0, 30) ?? 'Unknown User', totalAttachments: message.attachments.size, - contents: { - normal: message.content, - referred: referredContent, - censored: censoredContent, - }, + contents, author, jumpButton, - }) - : getEmbedMessageFormat(connection, hub, { - jumpButton, - embeds: { normal: embed, censored: censoredEmbed }, + }); + } + else { + const embeds = buildNetworkEmbed(message, username, censoredContent, { + attachmentURL: opts.attachmentURL, + referredContent, + embedCol: opts.embedColor, }); + messageFormat = getEmbedMessageFormat(connection, hub, { jumpButton, embeds }); + } + + const replyMention = getReplyMention(referredMsgData.dbReferredAuthor); + const { dbReferrence } = referredMsgData; + + // NOTE: If multiple connections to same hub is possible in the future, checking for serverId only won't be enough + if (replyMention && connection.serverId === dbReferrence?.serverId) { + messageFormat.content = `${replyMention}, ${messageFormat.content ?? ''}`; + messageFormat.allowedMentions = { + ...messageFormat.allowedMentions, + users: [...(messageFormat.allowedMentions?.users ?? []), dbReferrence.authorId], + }; + } + const messageRes = await this.sendMessage(connection.webhookURL, messageFormat); return { messageRes, webhookURL: connection.webhookURL }; } @@ -138,17 +148,37 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> { return results; } - private async getConnectionAndHubConnections(message: Message) { + private async resolveAttachmentURL(message: Message) { + return message.attachments.first()?.url ?? (await getAttachmentURL(message.content)); + } + + private getReferredContent(data: ReferredMsgData) { + return data?.referredMessage && data.dbReferrence + ? getReferredContent(data.referredMessage) + : undefined; + } + + private async getConnectionAndHubConnections(message: Message): Promise<{ + connection: connectedList | null; + hubConnections: connectedList[] | null; + }> { // check if the message was sent in a network channel const connectionHubId = await getConnectionHubId(message.channelId); - if (!connectionHubId) return {}; + if (!connectionHubId) return { connection: null, hubConnections: null }; const hubConnections = await getHubConnections(connectionHubId); - const connection = hubConnections?.find(({ channelId }) => channelId === message.channelId); + + let connection: connectedList | null = null; + const filteredHubConnections: connectedList[] = []; + + hubConnections?.forEach((conn) => { + if (conn.channelId === message.channelId) connection = conn; + else filteredHubConnections.push(conn); + }); return { connection, - hubConnections: hubConnections?.filter((c) => c.channelId !== message.channelId), + hubConnections: filteredHubConnections.length > 0 ? filteredHubConnections : null, }; } diff --git a/src/events/webhooksUpdate.ts b/src/events/webhooksUpdate.ts index 5e51edfdc..ff9d90683 100644 --- a/src/events/webhooksUpdate.ts +++ b/src/events/webhooksUpdate.ts @@ -1,4 +1,5 @@ import BaseEventListener from '#main/core/BaseEventListener.js'; +import { isGuildTextBasedChannel } from '#main/utils/Channels.js'; import { updateConnection } from '#main/utils/ConnectedList.js'; import { emojis } from '#main/utils/Constants.js'; import db from '#main/utils/Db.js'; @@ -34,7 +35,7 @@ export default class Ready extends BaseEventListener<'webhooksUpdate'> { ? await channel.client.channels.fetch(connection.channelId) : channel; - if (networkChannel?.isTextBased()) { + if (isGuildTextBasedChannel(networkChannel)) { await networkChannel.send( t({ phrase: 'misc.webhookNoLongerExists', locale: 'en' }, { emoji: emojis.info }), ); diff --git a/src/modules/ServerBlacklistManager.ts b/src/modules/ServerBlacklistManager.ts index b61db746a..75c8756be 100644 --- a/src/modules/ServerBlacklistManager.ts +++ b/src/modules/ServerBlacklistManager.ts @@ -112,7 +112,7 @@ export default class ServerBlacklisManager extends BaseBlacklistManager { const channel = await _client.channels.fetch(ctx.channelId).catch(() => null); - if (!channel?.isTextBased()) return; + if (!_client.isGuildTextBasedChannel(channel)) return; await channel.send({ embeds: [ctx.embed] }).catch(() => null); }, diff --git a/src/modules/UserDbManager.ts b/src/modules/UserDbManager.ts index fbdb17aa0..014ed0468 100644 --- a/src/modules/UserDbManager.ts +++ b/src/modules/UserDbManager.ts @@ -4,13 +4,13 @@ import { RedisKeys } from '#main/utils/Constants.js'; import db from '#main/utils/Db.js'; import { logUserUnblacklist } from '#main/utils/HubLogger/ModLogs.js'; import { supportedLocaleCodes } from '#main/utils/Locale.js'; -import { userData } from '@prisma/client'; +import { Prisma, userData } from '@prisma/client'; import { Snowflake, User } from 'discord.js'; export default class UserDbManager extends BaseBlacklistManager { protected modelName = 'userData' as const; - private serializeBlacklist(blacklist: ConvertDatesToString): userData { + private serializeBlacklists(blacklist: ConvertDatesToString): userData { return { ...blacklist, lastVoted: blacklist.lastVoted ? new Date(blacklist.lastVoted) : null, @@ -30,7 +30,7 @@ export default class UserDbManager extends BaseBlacklistManager { if (!results.data) return null; if (!results.cached) this.addToCache(results.data); - return this.serializeBlacklist(results.data); + return this.serializeBlacklists(results.data); } async getUserLocale(userOrId: string | userData | null | undefined) { @@ -38,9 +38,21 @@ export default class UserDbManager extends BaseBlacklistManager { return (dbUser?.locale as supportedLocaleCodes | null | undefined) ?? 'en'; } + async createUser(data: Prisma.userDataCreateInput) { + const createdUser = await db.userData.create({ data }); + await this.addToCache(createdUser); + return createdUser; + } + + async updateUser(id: Snowflake, data: Prisma.userDataUpdateInput) { + const updatedUser = await db.userData.update({ where: { id }, data }); + await this.addToCache(updatedUser); + return updatedUser; + } + async userVotedToday(id: Snowflake): Promise { const user = await this.getUser(id); - const twenty4HoursAgo = new Date(Date.now() - (60 * 60 * 24 * 1000)); + const twenty4HoursAgo = new Date(Date.now() - 60 * 60 * 24 * 1000); return Boolean(user?.lastVoted && new Date(user.lastVoted) >= twenty4HoursAgo); } diff --git a/src/scripts/guilds/goals.ts b/src/scripts/guilds/goals.ts index 0ec2f0428..48009c2fe 100644 --- a/src/scripts/guilds/goals.ts +++ b/src/scripts/guilds/goals.ts @@ -29,7 +29,12 @@ export const logGuildJoin = async (guild: Guild, channelId: string) => { const goalChannel = client.channels.cache.get(ctx.goalChannel); const inviteLogChannel = client.channels.cache.get(ctx.inviteLogs); - if (!goalChannel?.isTextBased() || !inviteLogChannel?.isTextBased()) return; + if ( + !client.isGuildTextBasedChannel(goalChannel) || + !client.isGuildTextBasedChannel(inviteLogChannel) + ) { + return; + } const count = (await client.cluster.fetchClientValues('guilds.cache.size')) as number[]; const guildCount = count.reduce((p, n) => p + n, 0); @@ -73,7 +78,12 @@ export const logGuildLeave = async (guild: Guild, channelId: string) => { const goalChannel = await client.channels.fetch(ctx.goalChannel).catch(() => null); const inviteLogChannel = client.channels.cache.get(ctx.inviteLogs); - if (!goalChannel?.isTextBased() || !inviteLogChannel?.isTextBased()) return; + if ( + !client.isGuildTextBasedChannel(goalChannel) || + !client.isGuildTextBasedChannel(inviteLogChannel) + ) { + return; + } await inviteLogChannel.send({ embeds: [ctx.logsEmbed] }); await goalChannel.send({ diff --git a/src/scripts/network/Types.d.ts b/src/scripts/network/Types.d.ts new file mode 100644 index 000000000..1ec2bf2ba --- /dev/null +++ b/src/scripts/network/Types.d.ts @@ -0,0 +1,42 @@ +import type { originalMessages, broadcastedMessages, userData } from '@prisma/client'; +import type { + User, + Message, + HexColorString, + ActionRowBuilder, + ButtonBuilder, + EmbedBuilder, +} from 'discord.js'; + +export interface ReferredMsgData { + dbReferrence: (originalMessages & { broadcastMsgs: broadcastedMessages[] }) | null; + referredAuthor: User | null; + dbReferredAuthor: userData | null; + referredMessage?: Message; +} + +export interface BroadcastOpts { + referredMsgData: ReferredMsgData; + embedColor?: HexColorString; + attachmentURL?: string | null; +} + +export type CompactFormatOpts = { + servername: string; + totalAttachments: number; + author: { + username: string; + avatarURL: string; + }; + contents: { + normal: string; + censored: string; + referred: string | undefined; + }; + jumpButton?: ActionRowBuilder[]; +}; + +export type EmbedFormatOpts = { + embeds: { normal: EmbedBuilder; censored: EmbedBuilder }; + jumpButton?: ActionRowBuilder[]; +}; diff --git a/src/scripts/network/helpers.ts b/src/scripts/network/helpers.ts index 335206f7e..3e702ec95 100644 --- a/src/scripts/network/helpers.ts +++ b/src/scripts/network/helpers.ts @@ -1,3 +1,4 @@ +import type { ReferredMsgData } from '#main/scripts/network/Types.d.ts'; import Constants, { emojis } from '#main/utils/Constants.js'; import db from '#main/utils/Db.js'; import { supportedLocaleCodes, t } from '#main/utils/Locale.js'; @@ -31,8 +32,16 @@ export const getReferredContent = (referredMessage: Message) => { return referredContent; }; -export const getReferredMsgData = async (referredMessage: Message | null) => { - if (!referredMessage) return { dbReferrence: null, referredAuthor: null }; +export const getReferredMsgData = async ( + referredMessage: Message | null, +): Promise => { + if (!referredMessage) { + return { + dbReferrence: null, + referredAuthor: null, + dbReferredAuthor: null, + }; + } const { client } = referredMessage; @@ -51,14 +60,19 @@ export const getReferredMsgData = async (referredMessage: Message | null) => { dbReferrence = broadcastedMsg?.originalMsg ?? null; } - if (!dbReferrence) return { dbReferrence: null, referredAuthor: null }; + if (!dbReferrence) { + return { + dbReferrence: null, + referredAuthor: null, + dbReferredAuthor: null, + }; + } - const referredAuthor = - referredMessage.author.id === client.user.id - ? client.user - : await client.users.fetch(dbReferrence.authorId).catch(() => null); // fetch the acttual user ("referredMessage" is a webhook message) + // fetch the acttual user ("referredMessage" is a webhook message) + const referredAuthor = await client.users.fetch(dbReferrence.authorId).catch(() => null); + const dbReferredAuthor = await client.userManager.getUser(dbReferrence.authorId); - return { dbReferrence, referredAuthor }; + return { dbReferrence, referredAuthor, dbReferredAuthor, referredMessage }; }; export const removeImgLinks = (content: string, imgUrl: string) => @@ -96,7 +110,7 @@ export const buildNetworkEmbed = ( censoredMsg = removeImgLinks(censoredContent, opts.attachmentURL); } - const embed = new EmbedBuilder() + const normal = new EmbedBuilder() .setImage(opts?.attachmentURL ?? null) .setColor(opts?.embedCol ?? Constants.Colors.invisible) .setAuthor({ @@ -109,19 +123,19 @@ export const buildNetworkEmbed = ( iconURL: message.guild?.iconURL() ?? undefined, }); - const censoredEmbed = EmbedBuilder.from(embed).setDescription(censoredMsg || null); + const censored = EmbedBuilder.from(normal).setDescription(censoredMsg || null); const formattedReply = opts?.referredContent?.replaceAll('\n', '\n> '); if (formattedReply) { - embed.setFields({ name: 'Replying To:', value: `> ${formattedReply}` }); - censoredEmbed.setFields({ name: 'Replying To:', value: `> ${censor(formattedReply)}` }); + normal.setFields({ name: 'Replying To:', value: `> ${formattedReply}` }); + censored.setFields({ name: 'Replying To:', value: `> ${censor(formattedReply)}` }); } - return { embed, censoredEmbed }; + return { normal, censored }; }; export const sendWelcomeMsg = async ( - message: Message, + message: Message, locale: supportedLocaleCodes, opts: { totalServers: string; hub: string }, ) => { diff --git a/src/scripts/network/messageFormatters.ts b/src/scripts/network/messageFormatters.ts index e75c33703..c95920de3 100644 --- a/src/scripts/network/messageFormatters.ts +++ b/src/scripts/network/messageFormatters.ts @@ -1,44 +1,12 @@ +import type { + BroadcastOpts, + CompactFormatOpts, + EmbedFormatOpts, +} from '#main/scripts/network/Types.d.ts'; import Constants from '#main/utils/Constants.js'; import { censor } from '#main/utils/Profanity.js'; -import type { broadcastedMessages, connectedList, hubs, originalMessages } from '@prisma/client'; -import { - ActionRowBuilder, - ButtonBuilder, - EmbedBuilder, - type HexColorString, - type Message, - type User, - type WebhookMessageCreateOptions, -} from 'discord.js'; - -export interface BroadcastOpts { - embedColor?: HexColorString | null; - attachmentURL?: string | null; - referredMessage: Message | null; - dbReferrence: (originalMessages & { broadcastMsgs: broadcastedMessages[] }) | null; - referredAuthor: User | null; -} - -type CompactFormatOpts = { - servername: string; - referredAuthorName: string; - totalAttachments: number; - author: { - username: string; - avatarURL: string; - }; - contents: { - normal: string; - censored: string; - referred: string | undefined; - }; - jumpButton?: ActionRowBuilder[]; -}; - -type EmbedFormatOpts = { - embeds: { normal: EmbedBuilder; censored: EmbedBuilder }; - jumpButton?: ActionRowBuilder[]; -}; +import type { connectedList, hubs, userData } from '@prisma/client'; +import { EmbedBuilder, userMention, type WebhookMessageCreateOptions } from 'discord.js'; export const getEmbedMessageFormat = ( connection: connectedList, @@ -53,46 +21,52 @@ export const getEmbedMessageFormat = ( allowedMentions: { parse: [] }, }); +const getReplyContent = (content: string | undefined, profFilter: boolean) => { + if (!content) return null; + return profFilter ? censor(content) : content; +}; + +export const getReplyMention = (dbReferredAuthor: userData | null) => { + if (!dbReferredAuthor?.mentionOnReply) return null; + return userMention(dbReferredAuthor.id); +}; + export const getCompactMessageFormat = ( connection: connectedList, opts: BroadcastOpts, - { - author, - contents, - servername, - jumpButton, - totalAttachments, - referredAuthorName, - }: CompactFormatOpts, + { author, contents, servername, jumpButton, totalAttachments }: CompactFormatOpts, ): WebhookMessageCreateOptions => { - const replyContent = - connection.profFilter && contents.referred ? censor(contents.referred) : contents.referred; - let replyEmbed; + const { referredAuthor } = opts.referredMsgData; + + // check if the person being replied to explicitly allowed mentionOnReply setting for themself + const replyContent = getReplyContent(contents.referred, connection.profFilter); // discord displays either an embed or an attachment url in a compact message (embeds take priority, so image will not display) // which is why if there is an image, we don't send the reply embed. Reply button remains though - if (replyContent && !opts.attachmentURL) { - replyEmbed = [ - new EmbedBuilder() - .setDescription(replyContent) - .setAuthor({ - name: referredAuthorName, - iconURL: opts.referredAuthor?.displayAvatarURL(), - }) - .setColor(Constants.Colors.invisible), - ]; - } + const replyEmbed = + replyContent && !opts.attachmentURL + ? [ + new EmbedBuilder() + .setDescription(replyContent) + .setAuthor({ + name: referredAuthor?.username.slice(0, 30) ?? 'Unknown User', + iconURL: referredAuthor?.displayAvatarURL(), + }) + .setColor(Constants.Colors.invisible), + ] + : undefined; // compact mode doesn't need new attachment url for tenor and direct image links // we can just slap them right in the content without any problems - const attachmentUrl = totalAttachments > 0 ? `\n[.](${opts.attachmentURL})` : ''; + const attachmentURL = totalAttachments > 0 ? `\n[.](${opts.attachmentURL})` : ''; + const messageContent = `${connection.profFilter ? contents.censored : contents.normal} ${attachmentURL}`; return { username: `@${author.username} • ${servername}`, avatarURL: author.avatarURL, embeds: replyEmbed, components: jumpButton, - content: `${connection.profFilter ? contents.censored : contents.normal} ${attachmentUrl}`, + content: messageContent, threadId: connection.parentId ? connection.channelId : undefined, allowedMentions: { parse: [] }, }; diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts index 93b525d2c..5d56df493 100644 --- a/src/typings/index.d.ts +++ b/src/typings/index.d.ts @@ -4,7 +4,7 @@ import UserDbManager from '#main/modules/UserDbManager.js'; import ServerBlacklistManager from '#main/modules/ServerBlacklistManager.js'; import { ClusterClient } from 'discord-hybrid-sharding'; import { InteractionFunction } from '#main/decorators/Interaction.ts'; -import { Collection, Snowflake } from 'discord.js'; +import { Collection, Snowflake, Channel } from 'discord.js'; import CooldownService from '#main/modules/CooldownService.js'; type RemoveMethods = { @@ -27,5 +27,6 @@ declare module 'discord.js' { fetchGuild(guildId: Snowflake): Promise | undefined>; getScheduler(): Scheduler; + isGuildTextBasedChannel(channel: Channel | null | undefined): channel is GuildTextBasedChannel; } } diff --git a/src/utils/Channels.ts b/src/utils/Channels.ts new file mode 100644 index 000000000..7287e34f2 --- /dev/null +++ b/src/utils/Channels.ts @@ -0,0 +1,5 @@ +import { Channel, GuildTextBasedChannel } from 'discord.js'; + +export const isGuildTextBasedChannel = ( + channel: Channel | null | undefined, +): channel is GuildTextBasedChannel => Boolean(channel?.isTextBased() && !channel.isDMBased()); diff --git a/src/utils/ConnectedList.ts b/src/utils/ConnectedList.ts index 1f52cf5e6..beff0aaf4 100644 --- a/src/utils/ConnectedList.ts +++ b/src/utils/ConnectedList.ts @@ -8,7 +8,7 @@ import { cacheData, getCachedData } from './cache/cacheUtils.js'; type whereUniuqeInput = Prisma.connectedListWhereUniqueInput; type whereInput = Prisma.connectedListWhereInput; type dataInput = Prisma.connectedListUpdateInput; -type ConnectionAction = 'create' | 'modify' | 'delete'; +type ConnectionOperation = 'create' | 'modify' | 'delete'; const purgeConnectionCache = async (channelId: string) => await cacheClient.del(`${RedisKeys.connectionHubId}:${channelId}`); @@ -33,46 +33,51 @@ export const getHubConnections = async (hubId: string) => { return connections.data?.map(serializeConnection) || null; }; -export const syncHubConnCache = async (connection: connectedList, action: ConnectionAction) => { +export const syncHubConnCache = async ( + connection: connectedList, + operation: ConnectionOperation, +) => { const start = performance.now(); - const hubConnections = ( - ( - await getCachedData( - `${RedisKeys.hubConnections}:${connection.hubId}`, - async () => (await getHubConnections(connection.hubId)) || [], - ) - ).data || [] - ).map(serializeConnection); - - Logger.debug(`[HubCon Sync]: Started syncing ${hubConnections.length} hub connections...`); - - let updatedConnections: connectedList[]; - switch (action) { - case 'create': - updatedConnections = [...hubConnections, connection]; - break; - case 'modify': - updatedConnections = hubConnections.map((conn) => - conn.id === connection.id ? connection : conn, - ); - break; - case 'delete': - updatedConnections = hubConnections.filter((conn) => conn.id !== connection.id); - break; - default: - return; - } + const hubConnections = await getHubConnections(connection.hubId); - await cacheData( - `${RedisKeys.hubConnections}:${connection.hubId}`, - JSON.stringify(updatedConnections), + const totalConnections = hubConnections?.length ?? 0; + Logger.debug( + `[HubConnectionSync]: Started syncing ${totalConnections} hub connections with operation: ${operation}...`, ); + + if (hubConnections && hubConnections?.length > 0) { + let updatedConnections = hubConnections; + switch (operation) { + case 'create': + updatedConnections = updatedConnections.concat(connection); + break; + case 'modify': { + const index = updatedConnections.findIndex((c) => c.id === connection.id); + + if (index !== -1) updatedConnections[index] = connection; + else updatedConnections = updatedConnections.concat(connection); + + break; + } + case 'delete': + updatedConnections = updatedConnections.filter((conn) => conn.id !== connection.id); + break; + default: + return; + } + + await cacheData( + `${RedisKeys.hubConnections}:${connection.hubId}`, + JSON.stringify(updatedConnections), + ); + } + Logger.debug( - `[HubCon Sync]: Finished syncing ${hubConnections.length} hub connections in ${performance.now() - start}ms`, + `[HubConnectionSync]: Finished syncing ${totalConnections} hub connections with operation ${operation} in ${performance.now() - start}ms`, ); }; -const cacheConnectionStatus = async (connection: connectedList) => { +const cacheConnectionHubId = async (connection: connectedList) => { if (!connection.connected) { await cacheClient.del(`${RedisKeys.connectionHubId}:${connection.channelId}`); } @@ -81,14 +86,16 @@ const cacheConnectionStatus = async (connection: connectedList) => { } Logger.debug( - `Cached connection status for ${connection.channelId}: ${connection.connected ? 'connected' : 'disconnected'}.`, + `Cached connection hubId for ${connection.connected ? 'connected' : 'disconnected'} channel ${connection.channelId}.`, ); }; -export const getConnection = async (channelId: string) => { +export const fetchConnection = async (channelId: string) => { const connection = await db.connectedList.findFirst({ where: { channelId } }); if (!connection) return null; - cacheConnectionStatus(connection); + + cacheConnectionHubId(connection); + if (connection.connected) syncHubConnCache(connection, 'modify'); return connection; }; @@ -96,7 +103,7 @@ export const getConnection = async (channelId: string) => { export const getConnectionHubId = async (channelId: string) => { const { data: hubId } = await getCachedData( `${RedisKeys.connectionHubId}:${channelId}`, - async () => (await getConnection(channelId))?.hubId, + async () => (await fetchConnection(channelId))?.hubId, ); return hubId; @@ -110,7 +117,7 @@ export const deleteConnection = async (where: whereUniuqeInput) => { export const createConnection = async (data: Prisma.connectedListCreateInput) => { const connection = await db.connectedList.create({ data }); - cacheConnectionStatus(connection); + cacheConnectionHubId(connection); syncHubConnCache(connection, 'create'); return connection; @@ -140,25 +147,22 @@ export const updateConnection = async (where: whereUniuqeInput, data: dataInput) const connection = await db.connectedList.update({ where, data }); // Update cache - await cacheConnectionStatus(connection); - await syncHubConnCache(connection, 'modify'); + await cacheConnectionHubId(connection); + await syncHubConnCache(connection, connection.connected ? 'modify' : 'delete'); return connection; }; export const updateConnections = async (where: whereInput, data: dataInput) => { - try { - // Update in database - const updated = await db.connectedList.updateMany({ where, data }); - return updated; - } - finally { - // repopulate cache - db.connectedList.findMany({ where }).then((connections) => { - connections.forEach(async (connection) => { - await cacheConnectionStatus(connection); - await syncHubConnCache(connection, 'modify'); - }); + // Update in database + const updated = await db.connectedList.updateMany({ where, data }); + + db.connectedList.findMany({ where }).then((connections) => { + connections.forEach(async (connection) => { + await cacheConnectionHubId(connection); + await syncHubConnCache(connection, connection.connected ? 'modify' : 'delete'); }); - } + }); + + return updated; }; diff --git a/src/utils/HubLogger/Default.ts b/src/utils/HubLogger/Default.ts index b0f94100a..3a34b0cc5 100644 --- a/src/utils/HubLogger/Default.ts +++ b/src/utils/HubLogger/Default.ts @@ -40,9 +40,9 @@ export const sendLog = async ( await client.cluster.broadcastEval( async (shardClient, ctx) => { const channel = await shardClient.channels.fetch(ctx.channelId).catch(() => null); - if (!channel?.isTextBased()) return; - - await channel.send({ content: ctx.content, embeds: [ctx.embed] }).catch(() => null); + if (shardClient.isGuildTextBasedChannel(channel)) { + await channel.send({ content: ctx.content, embeds: [ctx.embed] }).catch(() => null); + } }, { context: { channelId, embed, content } }, ); diff --git a/src/utils/RandomComponents.ts b/src/utils/RandomComponents.ts index 763fbf3ae..7b90079ad 100644 --- a/src/utils/RandomComponents.ts +++ b/src/utils/RandomComponents.ts @@ -5,15 +5,15 @@ import { checkBlacklists } from '#main/scripts/reaction/helpers.js'; import { stripIndents } from 'common-tags'; import { ActionRowBuilder, - AnySelectMenuInteraction, ButtonInteraction, EmbedBuilder, - Snowflake, StringSelectMenuBuilder, time, + type AnySelectMenuInteraction, + type Snowflake, } from 'discord.js'; import { HubSettingsBitField } from './BitFields.js'; -import { getConnection, updateConnection } from './ConnectedList.js'; +import { fetchConnection, updateConnection } from './ConnectedList.js'; import Constants, { emojis } from './Constants.js'; import { CustomID } from './CustomID.js'; import db from './Db.js'; @@ -204,7 +204,7 @@ export class RandomComponents { const customId = CustomID.parseCustomId(interaction.customId); const [channelId] = customId.args; - const connection = await getConnection(channelId); + const connection = await fetchConnection(channelId); if (!connection) { const locale = await interaction.client.userManager.getUserLocale(interaction.user.id); await interaction.reply({ From ab4ddc4497e66328da3b911e65d60f3fdb05a395 Mon Sep 17 00:00:00 2001 From: dev-737 <73829355+dev-737@users.noreply.github.com> Date: Thu, 12 Sep 2024 13:10:33 +0530 Subject: [PATCH 2/6] add "mode" property to message metadata --- prisma/schema.prisma | 2 + scripts/genLocaleTypes.js | 16 ++++++-- src/commands/context-menu/deleteMsg.ts | 2 +- src/commands/slash/Staff/purge.ts | 2 +- src/core/BaseCommand.ts | 2 +- src/events/messageCreate.ts | 43 ++++++++++++-------- src/modules/ServerBlacklistManager.ts | 4 +- src/modules/UserDbManager.ts | 4 +- src/scripts/network/Types.d.ts | 5 ++- src/scripts/network/helpers.ts | 28 ++++++++----- src/scripts/network/storeMessageData.ts | 54 ++++++++++++++++--------- src/typings/{en.d.ts => locale.d.ts} | 7 ++++ src/utils/Constants.ts | 6 +++ src/utils/LoadCommands.ts | 1 - src/utils/Locale.ts | 2 +- src/utils/cache/cacheUtils.ts | 22 ++++++---- 16 files changed, 136 insertions(+), 64 deletions(-) rename src/typings/{en.d.ts => locale.d.ts} (97%) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 378b9d920..76e0a7e83 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -117,6 +117,7 @@ model originalMessages { authorId String reactions Json? // eg. {"👎": ["9893820930928", "39283902803982"]} "emoji": userId[] basically createdAt DateTime + mode Int @default(0) broadcastMsgs broadcastedMessages[] // Ids of messages that were broadcasted to other hubs messageReference String? @db.String // id of the original message this message is replying to hub hubs? @relation(fields: [hubId], references: [id]) @@ -127,6 +128,7 @@ model broadcastedMessages { messageId String @id @map("_id") channelId String createdAt DateTime + mode Int @default(0) originalMsg originalMessages @relation(fields: [originalMsgId], references: [messageId]) originalMsgId String @db.String } diff --git a/scripts/genLocaleTypes.js b/scripts/genLocaleTypes.js index 78b35f2db..9b649cde8 100644 --- a/scripts/genLocaleTypes.js +++ b/scripts/genLocaleTypes.js @@ -2,9 +2,9 @@ import { readFileSync, writeFileSync } from 'fs'; import yaml from 'js-yaml'; import { dirname, resolve } from 'path'; +import prettier from 'prettier'; import { createPrinter, factory, NewLineKind, NodeFlags, SyntaxKind } from 'typescript'; import { fileURLToPath } from 'url'; -import prettier from 'prettier'; // Helper function to get the current directory name in ES modules const __filename = fileURLToPath(import.meta.url); @@ -86,9 +86,19 @@ const sourceFile = factory.createSourceFile( // Print the TypeScript code to a string const printer = createPrinter({ newLine: NewLineKind.LineFeed }); const formattedTypes = await formatWithPrettier(printer.printFile(sourceFile)); +const errorLocaleKeysExtra = 'export type ErrorLocaleKeys = Extract;' + +const output = `/* + WARNING: THIS IS AN AUTOGENERATED FILE. DO NOT EDIT IT DIRECTLY. + Update it through the script located in scripts/genLocaleTypings.js instead. +*/ + +${formattedTypes} +${errorLocaleKeysExtra} +` // Write the .d.ts file -const outputFilePath = resolve(__dirname, '..', 'src/typings/en.d.ts'); -writeFileSync(outputFilePath, formattedTypes); +const outputFilePath = resolve(__dirname, '..', 'src/typings/locale.d.ts'); +writeFileSync(outputFilePath, output); console.log(`Type definitions for locales written to ${outputFilePath}`); diff --git a/src/commands/context-menu/deleteMsg.ts b/src/commands/context-menu/deleteMsg.ts index 107b98f70..f03a35d69 100644 --- a/src/commands/context-menu/deleteMsg.ts +++ b/src/commands/context-menu/deleteMsg.ts @@ -21,7 +21,7 @@ export default class DeleteMessage extends BaseCommand { readonly cooldown = 10_000; async execute(interaction: MessageContextMenuCommandInteraction): Promise { - const isOnCooldown = await this.checkAndSetCooldown(interaction); + const isOnCooldown = await this.checkOrSetCooldown(interaction); if (isOnCooldown || !interaction.inCachedGuild()) return; await interaction.deferReply({ ephemeral: true }); diff --git a/src/commands/slash/Staff/purge.ts b/src/commands/slash/Staff/purge.ts index b517273fe..821694d9f 100644 --- a/src/commands/slash/Staff/purge.ts +++ b/src/commands/slash/Staff/purge.ts @@ -108,7 +108,7 @@ export default class Purge extends BaseCommand { }; async execute(interaction: ChatInputCommandInteraction): Promise { - const isOnCooldown = await this.checkAndSetCooldown(interaction); + const isOnCooldown = await this.checkOrSetCooldown(interaction); if (isOnCooldown) return; await interaction.deferReply({ fetchReply: true }); diff --git a/src/core/BaseCommand.ts b/src/core/BaseCommand.ts index 689947b71..c325b71c3 100644 --- a/src/core/BaseCommand.ts +++ b/src/core/BaseCommand.ts @@ -46,7 +46,7 @@ export default abstract class BaseCommand { async handleComponents?(interaction: MessageComponentInteraction): Promise; async handleModals?(interaction: ModalSubmitInteraction): Promise; - async checkAndSetCooldown(interaction: RepliableInteraction): Promise { + async checkOrSetCooldown(interaction: RepliableInteraction): Promise { const remainingCooldown = await this.getRemainingCooldown(interaction); if (remainingCooldown) { diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 65ff75962..51a624718 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -22,6 +22,7 @@ import { getEmbedMessageFormat, getReplyMention, } from '#main/scripts/network/messageFormatters.js'; +import { ConnectionMode } from '#main/utils/Constants.js'; export default class MessageCreate extends BaseEventListener<'messageCreate'> { readonly name = 'messageCreate'; @@ -62,7 +63,14 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> { }); // store the message in the db - await storeMessageData(message, sendResult, connection.hubId, referredMsgData.dbReferrence); + const mode = connection.compact ? ConnectionMode.Compact : ConnectionMode.Embed; + await storeMessageData( + message, + sendResult, + connection.hubId, + mode, + referredMsgData.dbReferrence, + ); } private async broadcastMessage( @@ -72,20 +80,16 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> { settings: HubSettingsBitField, opts: BroadcastOpts, ) { - const { referredMsgData } = opts; - - const referredContent = this.getReferredContent(referredMsgData); - const censoredContent = censor(message.content); const username = this.getUsername(settings, message); + const censoredContent = censor(message.content); + const referredContent = this.getReferredContent(opts.referredMsgData); + const { dbReferrence } = opts.referredMsgData; const results: NetworkWebhookSendResult[] = await Promise.all( hubConnections.map(async (connection) => { try { const author = { username, avatarURL: message.author.displayAvatarURL() }; - const reply = - referredMsgData.dbReferrence?.broadcastMsgs.find( - (m) => m.channelId === connection.channelId, - ) ?? referredMsgData.dbReferrence; + const reply = dbReferrence?.broadcastMsgs.get(connection.channelId) ?? dbReferrence; const jumpButton = reply ? [ @@ -124,10 +128,10 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> { messageFormat = getEmbedMessageFormat(connection, hub, { jumpButton, embeds }); } - const replyMention = getReplyMention(referredMsgData.dbReferredAuthor); - const { dbReferrence } = referredMsgData; + const replyMention = getReplyMention(opts.referredMsgData.dbReferredAuthor); - // NOTE: If multiple connections to same hub is possible in the future, checking for serverId only won't be enough + // NOTE: If multiple connections to same hub will be a feature in the future, + // checking for only serverId will not be enough if (replyMention && connection.serverId === dbReferrence?.serverId) { messageFormat.content = `${replyMention}, ${messageFormat.content ?? ''}`; messageFormat.allowedMentions = { @@ -137,7 +141,9 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> { } const messageRes = await this.sendMessage(connection.webhookURL, messageFormat); - return { messageRes, webhookURL: connection.webhookURL }; + const mode = connection.compact ? ConnectionMode.Compact : ConnectionMode.Embed; + + return { messageRes, webhookURL: connection.webhookURL, mode }; } catch (e) { return { error: e.message, webhookURL: connection.webhookURL }; @@ -153,9 +159,12 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> { } private getReferredContent(data: ReferredMsgData) { - return data?.referredMessage && data.dbReferrence - ? getReferredContent(data.referredMessage) - : undefined; + if (data.referredMessage && data.dbReferrence) { + const messagesRepliedTo = + data.dbReferrence.broadcastMsgs.get(data.referredMessage.channelId) ?? data.dbReferrence; + + return getReferredContent(data.referredMessage, messagesRepliedTo.mode); + } } private async getConnectionAndHubConnections(message: Message): Promise<{ @@ -173,7 +182,7 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> { hubConnections?.forEach((conn) => { if (conn.channelId === message.channelId) connection = conn; - else filteredHubConnections.push(conn); + else if (conn.connected) filteredHubConnections.push(conn); }); return { diff --git a/src/modules/ServerBlacklistManager.ts b/src/modules/ServerBlacklistManager.ts index 75c8756be..c68eb3a98 100644 --- a/src/modules/ServerBlacklistManager.ts +++ b/src/modules/ServerBlacklistManager.ts @@ -22,13 +22,13 @@ export default class ServerBlacklisManager extends BaseBlacklistManager await db.blacklistedServers.findFirst({ where: { id } }), ); if (blacklist?.blacklistedFrom.some((h) => h.hubId === hubId)) { - if (!cached) this.addToCache(blacklist); + if (!fromCache) this.addToCache(blacklist); return this.serializeBlacklist(blacklist); } return null; diff --git a/src/modules/UserDbManager.ts b/src/modules/UserDbManager.ts index 014ed0468..de28146db 100644 --- a/src/modules/UserDbManager.ts +++ b/src/modules/UserDbManager.ts @@ -28,7 +28,7 @@ export default class UserDbManager extends BaseBlacklistManager { ); if (!results.data) return null; - if (!results.cached) this.addToCache(results.data); + if (!results.fromCache) this.addToCache(results.data); return this.serializeBlacklists(results.data); } @@ -91,7 +91,7 @@ export default class UserDbManager extends BaseBlacklistManager { // if already blacklisted, override it const hubs = dbUser?.blacklistedFrom.filter((b) => b.hubId !== hubId) || []; - hubs?.push({ expires, reason, hubId, moderatorId }); + hubs.push({ expires, reason, hubId, moderatorId }); const updatedUser = await db.userData.upsert({ where: { id: user.id }, diff --git a/src/scripts/network/Types.d.ts b/src/scripts/network/Types.d.ts index 1ec2bf2ba..57e8c2e1e 100644 --- a/src/scripts/network/Types.d.ts +++ b/src/scripts/network/Types.d.ts @@ -6,10 +6,13 @@ import type { ActionRowBuilder, ButtonBuilder, EmbedBuilder, + Collection, } from 'discord.js'; export interface ReferredMsgData { - dbReferrence: (originalMessages & { broadcastMsgs: broadcastedMessages[] }) | null; + dbReferrence: + | (originalMessages & { broadcastMsgs: Collection }) + | null; referredAuthor: User | null; dbReferredAuthor: userData | null; referredMessage?: Message; diff --git a/src/scripts/network/helpers.ts b/src/scripts/network/helpers.ts index 3e702ec95..f1cc282de 100644 --- a/src/scripts/network/helpers.ts +++ b/src/scripts/network/helpers.ts @@ -1,5 +1,5 @@ import type { ReferredMsgData } from '#main/scripts/network/Types.d.ts'; -import Constants, { emojis } from '#main/utils/Constants.js'; +import Constants, { ConnectionMode, emojis } from '#main/utils/Constants.js'; import db from '#main/utils/Db.js'; import { supportedLocaleCodes, t } from '#main/utils/Locale.js'; import { censor } from '#main/utils/Profanity.js'; @@ -9,6 +9,7 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, + Collection, EmbedBuilder, } from 'discord.js'; @@ -17,10 +18,14 @@ import { * If the referred message has no content, returns a default message indicating that the original message contains an attachment. * If the referred message's content exceeds 1000 characters, truncates it and appends an ellipsis. * @param referredMessage The message being referred to. + * @param parseMode The mode in which the original message was sent in. * @returns The content of the referred message. */ -export const getReferredContent = (referredMessage: Message) => { - let referredContent = referredMessage.content || referredMessage.embeds[0]?.description; +export const getReferredContent = (referredMessage: Message, parseMode: ConnectionMode) => { + let referredContent = + parseMode === ConnectionMode.Compact + ? referredMessage.content + : referredMessage.embeds[0]?.description; if (!referredContent) { referredContent = '*Original message contains attachment <:attachment:1102464803647275028>*'; @@ -46,21 +51,21 @@ export const getReferredMsgData = async ( const { client } = referredMessage; // check if it was sent in the network - let dbReferrence = await db.originalMessages.findFirst({ + let dbReferrenceRaw = await db.originalMessages.findFirst({ where: { messageId: referredMessage.id }, include: { broadcastMsgs: true }, }); - if (!dbReferrence) { + if (!dbReferrenceRaw) { const broadcastedMsg = await db.broadcastedMessages.findFirst({ where: { messageId: referredMessage.id }, include: { originalMsg: { include: { broadcastMsgs: true } } }, }); - dbReferrence = broadcastedMsg?.originalMsg ?? null; + dbReferrenceRaw = broadcastedMsg?.originalMsg ?? null; } - if (!dbReferrence) { + if (!dbReferrenceRaw) { return { dbReferrence: null, referredAuthor: null, @@ -69,8 +74,13 @@ export const getReferredMsgData = async ( } // fetch the acttual user ("referredMessage" is a webhook message) - const referredAuthor = await client.users.fetch(dbReferrence.authorId).catch(() => null); - const dbReferredAuthor = await client.userManager.getUser(dbReferrence.authorId); + const referredAuthor = await client.users.fetch(dbReferrenceRaw.authorId).catch(() => null); + const dbReferredAuthor = await client.userManager.getUser(dbReferrenceRaw.authorId); + + const dbReferrence = { + ...dbReferrenceRaw, + broadcastMsgs: new Collection(dbReferrenceRaw.broadcastMsgs.map((m) => [m.channelId, m])), + }; return { dbReferrence, referredAuthor, dbReferredAuthor, referredMessage }; }; diff --git a/src/scripts/network/storeMessageData.ts b/src/scripts/network/storeMessageData.ts index 75ceda632..375dac846 100644 --- a/src/scripts/network/storeMessageData.ts +++ b/src/scripts/network/storeMessageData.ts @@ -1,17 +1,24 @@ import { updateConnections } from '#main/utils/ConnectedList.js'; -import { RedisKeys } from '#main/utils/Constants.js'; +import { ConnectionMode, RedisKeys } from '#main/utils/Constants.js'; import db from '#main/utils/Db.js'; import Logger from '#main/utils/Logger.js'; import cacheClient from '#main/utils/cache/cacheClient.js'; import { originalMessages } from '@prisma/client'; import { APIMessage, Message } from 'discord.js'; -export interface NetworkWebhookSendResult { - messageRes?: APIMessage; - error?: string; +interface ErrorResult { webhookURL: string; + error: string; } +interface SendResult { + messageRes: APIMessage; + mode: ConnectionMode; + webhookURL: string; +} + +export type NetworkWebhookSendResult = ErrorResult | SendResult; + /** * Stores message data in the database and updates the connectedList based on the webhook status. * @param channelAndMessageIds The result of sending the message to multiple channels. @@ -19,27 +26,37 @@ export interface NetworkWebhookSendResult { */ export default async ( message: Message, - channelAndMessageIds: NetworkWebhookSendResult[], + broadcastResults: NetworkWebhookSendResult[], hubId: string, + mode: ConnectionMode, dbReference?: originalMessages | null, ) => { - const messageDataObj: { channelId: string; messageId: string; createdAt: Date }[] = []; + const messageDataObj: { + channelId: string; + messageId: string; + createdAt: Date; + mode: ConnectionMode; + }[] = []; + const invalidWebhookURLs: string[] = []; const validErrors = ['Invalid Webhook Token', 'Unknown Webhook', 'Missing Permissions']; // loop through all results and extract message data and invalid webhook urls - channelAndMessageIds.forEach(({ messageRes, error, webhookURL }) => { - if (messageRes) { - messageDataObj.push({ - channelId: messageRes.channel_id, - messageId: messageRes.id, - createdAt: new Date(messageRes.timestamp), - }); - } - else if (error && validErrors.some((e) => error.includes(e))) { - Logger.info('%O', messageRes); // TODO Remove dis - invalidWebhookURLs.push(webhookURL); + broadcastResults.forEach((res) => { + if ('error' in res) { + if (!validErrors.some((e) => res.error.includes(e))) return; + + Logger.info('%O', res.error); // TODO Remove dis + invalidWebhookURLs.push(res.webhookURL); + return; } + + messageDataObj.push({ + channelId: res.messageRes.channel_id, + messageId: res.messageRes.id, + createdAt: new Date(res.messageRes.timestamp), + mode: res.mode, + }); }); if (hubId && messageDataObj.length > 0) { @@ -48,14 +65,15 @@ export default async ( // store message data in db await db.originalMessages.create({ data: { + mode, messageId: message.id, authorId: message.author.id, serverId: message.guildId, messageReference: dbReference?.messageId, createdAt: message.createdAt, + reactions: {}, broadcastMsgs: { createMany: { data: messageDataObj } }, hub: { connect: { id: hubId } }, - reactions: {}, }, }); } diff --git a/src/typings/en.d.ts b/src/typings/locale.d.ts similarity index 97% rename from src/typings/en.d.ts rename to src/typings/locale.d.ts index 6c8757093..97e6fc7c4 100644 --- a/src/typings/en.d.ts +++ b/src/typings/locale.d.ts @@ -1,3 +1,8 @@ +/* + WARNING: THIS IS AN AUTOGENERATED FILE. DO NOT EDIT IT DIRECTLY. + Update it through the script located in scripts/genLocaleTypings.js instead. +*/ + export type TranslationKeys = { rules: 'support_invite'; 'vote.description': never; @@ -216,3 +221,5 @@ export type TranslationKeys = { 'misc.loading': 'emoji'; 'misc.reportOptionMoved': 'emoji' | 'support_invite'; }; + +export type ErrorLocaleKeys = Extract; diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index 0943c53ba..ff55e05fd 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -21,6 +21,12 @@ export enum RedisKeys { cooldown = 'cooldown', blacklistedServers = 'blacklistedServers', channelQueue = 'channelQueue', + commandUsesLeft = 'commandUsesLeft', +} + +export enum ConnectionMode { + Compact = 0, + Embed = 1, } export default { diff --git a/src/utils/LoadCommands.ts b/src/utils/LoadCommands.ts index 084279bb7..3b316b9b0 100644 --- a/src/utils/LoadCommands.ts +++ b/src/utils/LoadCommands.ts @@ -32,7 +32,6 @@ const loadCommandInteractions = (command: BaseCommand) => { // @ts-expect-error The names of child class properties can be custom const method: InteractionFunction = command[methodName]; - // console.log(method, methodName, customId); interactionsMap.set(customId, method.bind(command)); }); diff --git a/src/utils/Locale.ts b/src/utils/Locale.ts index 4acc0112b..f212ca485 100644 --- a/src/utils/Locale.ts +++ b/src/utils/Locale.ts @@ -2,7 +2,7 @@ import Logger from './Logger.js'; import fs from 'fs'; import path from 'path'; import yaml from 'js-yaml'; -import type { TranslationKeys } from '#main/typings/en.js'; +import type { TranslationKeys } from '#main/typings/locale.js'; const localesMap = new Map(); diff --git a/src/utils/cache/cacheUtils.ts b/src/utils/cache/cacheUtils.ts index d3052b93d..912a15857 100644 --- a/src/utils/cache/cacheUtils.ts +++ b/src/utils/cache/cacheUtils.ts @@ -2,13 +2,20 @@ import cacheClient from '#main/utils/cache/cacheClient.js'; import { RedisKeys } from '#main/utils/Constants.js'; import { Prisma } from '@prisma/client'; import Logger from '../Logger.js'; +import { Awaitable } from 'discord.js'; // TODO: make this into a class -export const cacheData = async (key: string, value: string, expiry = 3600) => { - await cacheClient.set(key, value, 'EX', expiry).catch((e) => { +export const cacheData = async (key: string, value: string, expirySecs?: number) => { + try { + if (expirySecs) { + return await cacheClient.set(key, value, 'EX', expirySecs); + } + await cacheClient.set(key, value, 'KEEPTTL'); + } + catch (e) { Logger.error('Failed to set cache: ', e); - }); + } }; export const parseKey = (key: string) => { @@ -73,19 +80,20 @@ export const getAllDocuments = async (match: string) => { export const getCachedData = async ( key: `${RedisKeys}:${string}`, - fetchFunction: () => Promise, + fetchFunction: (() => Awaitable) | null, expiry?: number, -): Promise<{ data: ConvertDatesToString | null; cached: boolean }> => { +): Promise<{ data: ConvertDatesToString | null; fromCache: boolean }> => { // Check cache first let data = serializeCache(await cacheClient.get(key)); + const fromCache = data !== null; // If not in cache, fetch from database - if (!data) { + if (!fromCache && fetchFunction) { data = (await fetchFunction()) as ConvertDatesToString; // Store in cache with TTL if (data) await cacheData(key, JSON.stringify(data), expiry); } - return { data, cached: Boolean(data) }; + return { data, fromCache }; }; From 49c4eac81a1a09a6c826bc1075b757d0d9af210a Mon Sep 17 00:00:00 2001 From: dev-737 <73829355+dev-737@users.noreply.github.com> Date: Thu, 12 Sep 2024 13:13:06 +0530 Subject: [PATCH 3/6] make editMessage command compatible w/ reply mentions & introduce vote based limits --- src/commands/context-menu/editMsg.ts | 224 +++++++++++++++------------ src/modules/VoteBasedLimiter.ts | 82 ++++++++++ 2 files changed, 206 insertions(+), 100 deletions(-) create mode 100644 src/modules/VoteBasedLimiter.ts diff --git a/src/commands/context-menu/editMsg.ts b/src/commands/context-menu/editMsg.ts index 60fe0d8be..dcc9cd984 100644 --- a/src/commands/context-menu/editMsg.ts +++ b/src/commands/context-menu/editMsg.ts @@ -1,15 +1,19 @@ +import BaseCommand from '#main/core/BaseCommand.js'; +import { RegisterInteractionHandler } from '#main/decorators/Interaction.js'; +import VoteBasedLimiter from '#main/modules/VoteBasedLimiter.js'; import { HubSettingsBitField } from '#main/utils/BitFields.js'; -import { emojis } from '#main/utils/Constants.js'; +import Constants, { ConnectionMode, emojis } from '#main/utils/Constants.js'; import { CustomID } from '#main/utils/CustomID.js'; import db from '#main/utils/Db.js'; import { t } from '#main/utils/Locale.js'; import { censor } from '#main/utils/Profanity.js'; import { - checkIfStaff, containsInviteLinks, getAttachmentURL, + handleError, replaceLinks, } from '#main/utils/Utils.js'; +import { originalMessages } from '@prisma/client'; import { ActionRowBuilder, ApplicationCommandType, @@ -24,8 +28,11 @@ import { User, userMention, } from 'discord.js'; -import BaseCommand from '#main/core/BaseCommand.js'; -import { RegisterInteractionHandler } from '#main/decorators/Interaction.js'; + +interface ImageUrls { + oldURL?: string | null; + newURL?: string | null; +} export default class EditMessage extends BaseCommand { readonly data: RESTPostAPIApplicationCommandsJSONBody = { @@ -33,22 +40,21 @@ export default class EditMessage extends BaseCommand { name: 'Edit Message', dm_permission: false, }; + readonly cooldown = 10_000; async execute(interaction: MessageContextMenuCommandInteraction): Promise { - const isOnCooldown = await this.checkAndSetCooldown(interaction); + const isOnCooldown = await this.checkOrSetCooldown(interaction); if (isOnCooldown) return; const { userManager } = interaction.client; const target = interaction.targetMessage; const locale = await userManager.getUserLocale(interaction.user.id); + const voteLimiter = new VoteBasedLimiter('editMsg', interaction.user.id, userManager); - if ( - !checkIfStaff(interaction.user.id) && - !(await userManager.userVotedToday(interaction.user.id)) - ) { + if (await voteLimiter.hasExceededLimit()) { await interaction.reply({ - content: t({ phrase: 'errors.mustVote', locale }, { emoji: emojis.no }), + content: `${emojis.topggSparkles} You've hit your daily limit for message edits. [Vote for InterChat](${Constants.Links.Vote}) on top.gg to get unlimited edits!`, }); return; } @@ -119,21 +125,21 @@ export default class EditMessage extends BaseCommand { return; } - let originalMsg = await db.originalMessages.findFirst({ + let targetMsgData = await db.originalMessages.findFirst({ where: { messageId: target.id }, include: { hub: true, broadcastMsgs: true }, }); - if (!originalMsg) { + if (!targetMsgData) { const broadcastedMsg = await db.broadcastedMessages.findFirst({ where: { messageId: target.id }, include: { originalMsg: { include: { hub: true, broadcastMsgs: true } } }, }); - originalMsg = broadcastedMsg?.originalMsg ?? null; + targetMsgData = broadcastedMsg?.originalMsg ?? null; } - if (!originalMsg?.hub) { + if (!targetMsgData?.hub) { await interaction.editReply( t({ phrase: 'errors.unknownNetworkMessage', locale }, { emoji: emojis.no }), ); @@ -142,32 +148,34 @@ export default class EditMessage extends BaseCommand { // get the new message input by user const userInput = interaction.fields.getTextInputValue('newMessage'); - const hubSettings = new HubSettingsBitField(originalMsg.hub.settings); - const newMessage = hubSettings.has('HideLinks') ? replaceLinks(userInput) : userInput; - const { newEmbed, censoredEmbed, compactMsg, censoredCmpctMsg } = await this.fabricateNewMsg( - interaction.user, - target, - newMessage, - originalMsg.serverId, - ); - - if (hubSettings.has('BlockInvites') && containsInviteLinks(newMessage)) { + const hubSettings = new HubSettingsBitField(targetMsgData.hub.settings); + const messageToEdit = this.sanitizeMessage(userInput, hubSettings); + + if (hubSettings.has('BlockInvites') && containsInviteLinks(messageToEdit)) { await interaction.editReply( t({ phrase: 'errors.inviteLinks', locale }, { emoji: emojis.no }), ); return; } + const imageURLs = await this.getImageURLs(target, targetMsgData.mode, messageToEdit); + const newContents = this.getCompactContents(messageToEdit, imageURLs); + const newEmbeds = await this.buildEmbeds(target, targetMsgData, messageToEdit, { + serverId: targetMsgData.serverId, + user: interaction.user, + imageURLs, + }); + // find all the messages through the network const channelSettingsArr = await db.connectedList.findMany({ - where: { channelId: { in: originalMsg.broadcastMsgs.map((c) => c.channelId) } }, + where: { channelId: { in: targetMsgData.broadcastMsgs.map((c) => c.channelId) } }, }); - const results = originalMsg.broadcastMsgs.map(async (msg) => { - const settings = channelSettingsArr.find((c) => c.channelId === msg.channelId); - if (!settings) return false; + const results = targetMsgData.broadcastMsgs.map(async (msg) => { + const connection = channelSettingsArr.find((c) => c.channelId === msg.channelId); + if (!connection) return false; - const webhookURL = settings.webhookURL.split('/'); + const webhookURL = connection.webhookURL.split('/'); const webhook = await interaction.client .fetchWebhook(webhookURL[webhookURL.length - 2]) ?.catch(() => null); @@ -177,15 +185,19 @@ export default class EditMessage extends BaseCommand { let content; let embeds; - if (!settings.compact) embeds = settings.profFilter ? [censoredEmbed] : [newEmbed]; - else content = settings.profFilter ? censoredCmpctMsg : compactMsg; + if (msg.mode === ConnectionMode.Embed) { + embeds = connection.profFilter ? [newEmbeds.censored] : [newEmbeds.normal]; + } + else { + content = connection.profFilter ? newContents.censored : newContents.normal; + } // finally, edit the message return await webhook .editMessage(msg.messageId, { content, embeds, - threadId: settings.parentId ? settings.channelId : undefined, + threadId: connection.parentId ? connection.channelId : undefined, }) .then(() => true) .catch(() => false); @@ -193,89 +205,101 @@ export default class EditMessage extends BaseCommand { const resultsArray = await Promise.all(results); const edited = resultsArray.reduce((acc, cur) => acc + (cur ? 1 : 0), 0).toString(); - await interaction.editReply( - t( - { phrase: 'network.editSuccess', locale }, - { - edited, - total: resultsArray.length.toString(), - emoji: emojis.yes, - user: userMention(originalMsg.authorId), - }, - ), - ); - } - private async getImageUrls(target: Message, newMessage: string) { - // get image from embed - // get image from content - const oldImageUrl = target.content - ? await getAttachmentURL(target.content) - : target.embeds[0]?.image?.url; - const newImageUrl = await getAttachmentURL(newMessage); - return { oldImageUrl, newImageUrl }; + await interaction + .editReply( + t( + { phrase: 'network.editSuccess', locale }, + { + edited, + total: resultsArray.length.toString(), + emoji: emojis.yes, + user: userMention(targetMsgData.authorId), + }, + ), + ) + .catch(handleError); + + const voteLimiter = new VoteBasedLimiter('editMsg', interaction.user.id, userManager); + await voteLimiter.decrementUses(); } - private async buildNewEmbed( - user: User, + private async getImageURLs( target: Message, + mode: ConnectionMode, newMessage: string, - serverId: string, - opts?: { - oldImageUrl?: string | null; - newImageUrl?: string | null; - }, + ): Promise { + const oldURL = + mode === ConnectionMode.Compact + ? await getAttachmentURL(target.content) + : target.embeds[0]?.image?.url; + + const newURL = await getAttachmentURL(newMessage); + + return { oldURL, newURL }; + } + + private async buildEmbeds( + target: Message, + targetMsgData: originalMessages, + messageToEdit: string, + opts: { user: User; serverId: string; imageURLs?: ImageUrls }, ) { - const embedContent = - newMessage.replace(opts?.oldImageUrl ?? '', '').replace(opts?.newImageUrl ?? '', '') ?? null; - const embedImage = opts?.newImageUrl ?? opts?.oldImageUrl ?? null; + let embedContent = messageToEdit; + let embedImage = null; + + // This if check must come on top of the next one at all times + // because we want newImage Url to be given priority for the embedImage + if (opts.imageURLs?.newURL) { + embedContent = embedContent.replace(opts.imageURLs.newURL, ''); + embedImage = opts.imageURLs.newURL; + } + if (opts.imageURLs?.oldURL) { + embedContent = embedContent.replace(opts.imageURLs.oldURL, ''); + embedImage = opts.imageURLs.oldURL; + } + + let embed: EmbedBuilder; - if (!target.content) { + if (targetMsgData.mode === ConnectionMode.Embed) { // utilize the embed directly from the message - return EmbedBuilder.from(target.embeds[0]).setDescription(embedContent).setImage(embedImage); + embed = EmbedBuilder.from(target.embeds[0]).setDescription(embedContent).setImage(embedImage); + } + else { + const guild = await target.client.fetchGuild(opts.serverId); + + // create a new embed if the message being edited is in compact mode + embed = new EmbedBuilder() + .setAuthor({ name: opts.user.username, iconURL: opts.user.displayAvatarURL() }) + .setDescription(embedContent) + .setColor(Constants.Colors.invisible) + .setImage(embedImage) + .addFields( + target.embeds.at(0)?.fields.at(0) + ? [{ name: 'Replying-to', value: `${target.embeds[0].description}` }] + : [], + ) + .setFooter({ text: `Server: ${guild?.name}` }); } - const guild = await target.client.fetchGuild(serverId); - - // create a new embed if the message being edited is in compact mode - return new EmbedBuilder() - .setAuthor({ name: user.username, iconURL: user.displayAvatarURL() }) - .setDescription(embedContent) - .setColor('Random') - .setImage(embedImage) - .addFields( - target.embeds.at(0)?.fields.at(0) - ? [{ name: 'Replying-to', value: `${target.embeds[0].description}` }] - : [], - ) - .setFooter({ text: `Server: ${guild?.name}` }); - } + const censored = EmbedBuilder.from({ ...embed.data, description: censor(embedContent) }); - private async fabricateNewMsg(user: User, target: Message, newMessage: string, serverId: string) { - const { oldImageUrl, newImageUrl } = await this.getImageUrls(target, newMessage); - const newEmbed = await this.buildNewEmbed(user, target, newMessage, serverId, { - oldImageUrl, - newImageUrl, - }); + return { normal: embed, censored }; + } - // if the message being edited is in compact mode - // then we create a new embed with the new message and old reply - // else we just use the old embed and replace the description + private sanitizeMessage(content: string, settings: HubSettingsBitField) { + const newMessage = settings.has('HideLinks') ? replaceLinks(content) : content; + return newMessage; + } - const censoredEmbed = EmbedBuilder.from(newEmbed).setDescription( - censor(newEmbed.data.description ?? '') || null, - ); - let compactMsg = newMessage; + private getCompactContents(messageToEdit: string, imageUrls: ImageUrls) { + let compactMsg = messageToEdit; - if (oldImageUrl && newImageUrl) { - compactMsg = compactMsg.replace(oldImageUrl, newImageUrl); - } - else if (oldImageUrl && !newMessage.includes(oldImageUrl)) { - newEmbed.setImage(null); - censoredEmbed.setImage(null); + if (imageUrls.oldURL && imageUrls.newURL) { + // use the new url instead + compactMsg = compactMsg.replace(imageUrls.oldURL, imageUrls.newURL); } - const censoredCmpctMsg = censor(compactMsg); - return { newEmbed, censoredEmbed, compactMsg, censoredCmpctMsg }; + return { normal: compactMsg, censored: censor(compactMsg) }; } } diff --git a/src/modules/VoteBasedLimiter.ts b/src/modules/VoteBasedLimiter.ts new file mode 100644 index 000000000..59051bcd7 --- /dev/null +++ b/src/modules/VoteBasedLimiter.ts @@ -0,0 +1,82 @@ +import UserDbManager from '#main/modules/UserDbManager.js'; +import { cacheData, getCachedData } from '#main/utils/cache/cacheUtils.js'; +import { RedisKeys } from '#main/utils/Constants.js'; + +export default class VoteLimitManager { + private readonly userManager; + private readonly userId; + private readonly limitObjKey; + + private readonly MAX_USES_WITHOUT_VOTE; + private readonly CACHE_DURATION; // 12 hours + + constructor( + limitObjKey: string, + userId: string, + userManager: UserDbManager, + opts?: { maxUses?: number; cacheDuration?: number }, + ) { + this.limitObjKey = limitObjKey; + this.userId = userId; + this.userManager = userManager; + this.MAX_USES_WITHOUT_VOTE = opts?.maxUses ?? 3; + this.CACHE_DURATION = opts?.cacheDuration ?? 43200; // 12 hours + } + + public async getRemainingUses() { + const { data: usesLeft, fromCache } = await getCachedData( + `${RedisKeys.commandUsesLeft}:${this.limitObjKey}:${this.userId}`, + null, + ); + + return { usesLeft, fromCache }; + } + + public async setRemainingUses(remainingUses: number, expirySecs?: number) { + return await cacheData( + `${RedisKeys.commandUsesLeft}:${this.limitObjKey}:${this.userId}`, + remainingUses.toString(), + expirySecs, + ); + } + + public async decrementUses() { + const { usesLeft, fromCache } = await this.getRemainingUses(); + + // Default to max edits if there's no data + const newUsesCount = + usesLeft !== null && !isNaN(usesLeft) + ? Math.max(usesLeft - 1, 0) + : this.MAX_USES_WITHOUT_VOTE; + + // If from cache, don't overrite the duration + const expirySecs = !fromCache ? this.CACHE_DURATION : undefined; + await this.setRemainingUses(newUsesCount, expirySecs); + + return newUsesCount; + } + + public async hasExceededLimit() { + const { usesLeft, fromCache } = await this.getRemainingUses(); + + if (!fromCache) { + const dbUser = await this.userManager.getUser(this.userId); + + const voteExpirySecs = + dbUser?.lastVoted && dbUser.lastVoted.getTime() > Date.now() + ? Math.floor((dbUser.lastVoted.getTime() - Date.now()) / 1000) + : null; + + await this.setRemainingUses( + this.MAX_USES_WITHOUT_VOTE, + voteExpirySecs || this.CACHE_DURATION, + ); + } + else if (usesLeft === 0) { + const hasVoted = await this.userManager.userVotedToday(this.userId); + return !hasVoted; + } + + return false; + } +} From e659499658d26e482b512aa84b1686a5a587647c Mon Sep 17 00:00:00 2001 From: dev-737 <73829355+dev-737@users.noreply.github.com> Date: Thu, 12 Sep 2024 13:17:17 +0530 Subject: [PATCH 4/6] fix deepsource error --- src/utils/cache/cacheUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/cache/cacheUtils.ts b/src/utils/cache/cacheUtils.ts index 912a15857..66ba23e60 100644 --- a/src/utils/cache/cacheUtils.ts +++ b/src/utils/cache/cacheUtils.ts @@ -11,7 +11,7 @@ export const cacheData = async (key: string, value: string, expirySecs?: number) if (expirySecs) { return await cacheClient.set(key, value, 'EX', expirySecs); } - await cacheClient.set(key, value, 'KEEPTTL'); + return await cacheClient.set(key, value, 'KEEPTTL'); } catch (e) { Logger.error('Failed to set cache: ', e); From 125d45e8f412bfa419f768a1cefd2a17349e9c7a Mon Sep 17 00:00:00 2001 From: dev-737 <73829355+dev-737@users.noreply.github.com> Date: Thu, 12 Sep 2024 15:35:10 +0530 Subject: [PATCH 5/6] fix deepsource error2 --- src/commands/context-menu/deleteMsg.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/context-menu/deleteMsg.ts b/src/commands/context-menu/deleteMsg.ts index f03a35d69..d99899f02 100644 --- a/src/commands/context-menu/deleteMsg.ts +++ b/src/commands/context-menu/deleteMsg.ts @@ -107,7 +107,7 @@ export default class DeleteMessage extends BaseCommand { const { targetMessage } = interaction; const messageContent = - targetMessage.cleanContent ?? targetMessage.embeds.at(0)?.description?.replaceAll('`', '`'); + targetMessage.cleanContent ?? targetMessage.embeds.at(0)?.description?.replaceAll('`', '\`'); const imageUrl = targetMessage.embeds.at(0)?.image?.url ?? From e67f87a3066deb528ae8e5e4f49155e768145ec9 Mon Sep 17 00:00:00 2001 From: dev-737 <73829355+dev-737@users.noreply.github.com> Date: Thu, 12 Sep 2024 15:42:35 +0530 Subject: [PATCH 6/6] fix deepsource error2 (fr this time) --- src/commands/context-menu/deleteMsg.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/context-menu/deleteMsg.ts b/src/commands/context-menu/deleteMsg.ts index d99899f02..ed9386fce 100644 --- a/src/commands/context-menu/deleteMsg.ts +++ b/src/commands/context-menu/deleteMsg.ts @@ -107,7 +107,7 @@ export default class DeleteMessage extends BaseCommand { const { targetMessage } = interaction; const messageContent = - targetMessage.cleanContent ?? targetMessage.embeds.at(0)?.description?.replaceAll('`', '\`'); + targetMessage.cleanContent ?? targetMessage.embeds.at(0)?.description?.replaceAll('`', '\\`'); const imageUrl = targetMessage.embeds.at(0)?.image?.url ??