diff --git a/locales/en.yml b/locales/en.yml index 665bb862..c942ee28 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -165,15 +165,15 @@ hub: invalidName: '{emoji} Invalid hub name. It must not contain `discord`, `clyde` or \`\`\` . Please choose another name.' nameTaken: '{emoji} This hub name is already taken. Please choose another name.' success: | - ### Hub Created! - - Congratulations! Your private hub, **{name}**, has been successfully created. + ### Your __private__ hub, **{name}**, has been successfully created. To join, create an invite using `/hub invite create` and share the generated code. Then join using `/hub join`. + - **Edit hub:** `/hub edit` - **Generate invite:** `/hub invite create` - - **Make private:** `/hub visibility` + - **Make public:** `/hub visibility` - **Join hub:** `/hub join` - **Edit hub:** `/hub edit` + - **Set report channel:** `/hub logging set_channe;` - **Add moderators:** `/hub moderator add` Learn more about hubs in our [guide]({docs_link}). Join the [support server]({support_invite}) for help. @@ -211,7 +211,7 @@ hub: list: title: '**Invite Codes:**' noInvites: '{emoji} This hub has no invites yet. Use `/hub invite create` to create one.' - notPrivate: '{emoji} Only private hubs can have invites. Use `/hub manage` to make this hub private.' + notPrivate: '{emoji} Only private hubs can have invites. Use `/hub edit` to make this hub private.' joined: noJoinedHubs: '{emoji} This server has not joined any hubs yet. Use `/hub browse` to view a list of hubs.' joinedHubs: This server is a part of **{total}** hub(s). Use `/hub leave` to leave a hub. @@ -331,7 +331,7 @@ errors: **Error ID:** ```{errorId}``` mustVote: Please [vote](https://top.gg/bot/769921109209907241/vote) for InterChat to use this command, your support is very much appreciated! - inviteLinks: '{emoji} You may not send invite links to this hub. Set an invite in `/connection` instead! Hub mods can configure this using `/hub manage settings`' + inviteLinks: '{emoji} You may not send invite links to this hub. Set an invite in `/connection` instead! Hub mods can configure this using `/hub edit settings`' invalidLangCode: '{emoji} Invalid language code. Please make sure you have entered a correct [language code](https://cloud.google.com/translate/docs/languages).' unknownServer: '{emoji} Unknown server. Please make sure you have entered the correct **Server ID**.' unknownNetworkMessage: '{emoji} Unknown Message. If it has been sent in the past minute, please wait few more seconds and try again.' diff --git a/src/commands/context-menu/editMsg.ts b/src/commands/context-menu/editMsg.ts index f2281f5e..c72fa908 100644 --- a/src/commands/context-menu/editMsg.ts +++ b/src/commands/context-menu/editMsg.ts @@ -1,17 +1,17 @@ /* eslint-disable complexity */ -import Constants, { ConnectionMode, emojis } from '#utils/Constants.js'; import BaseCommand from '#main/core/BaseCommand.js'; import { RegisterInteractionHandler } from '#main/decorators/RegisterInteractionHandler.js'; import HubSettingsManager from '#main/managers/HubSettingsManager.js'; import { SerializedHubSettings } from '#main/modules/BitFields.js'; import VoteBasedLimiter from '#main/modules/VoteBasedLimiter.js'; -import { fetchHub } from '#main/utils/hub/utils.js'; +import { HubService } from '#main/services/HubService.js'; import { findOriginalMessage, getBroadcast, getBroadcasts, getOriginalMessage, } from '#main/utils/network/messageUtils.js'; +import Constants, { ConnectionMode, emojis } from '#utils/Constants.js'; import { CustomID } from '#utils/CustomID.js'; import db from '#utils/Db.js'; import { getAttachmentURL } from '#utils/ImageUtils.js'; @@ -132,7 +132,8 @@ export default class EditMessage extends BaseCommand { } // Fetch the hub information - const hub = await fetchHub(originalMsgData.hubId); + const hubService = new HubService(db); + const hub = await hubService.fetchHub(originalMsgData.hubId); if (!hub) { await interaction.editReply( t('errors.unknownNetworkMessage', await this.getLocale(interaction), { diff --git a/src/commands/context-menu/messageInfo.ts b/src/commands/context-menu/messageInfo.ts index a164f604..d9674728 100644 --- a/src/commands/context-menu/messageInfo.ts +++ b/src/commands/context-menu/messageInfo.ts @@ -2,6 +2,7 @@ import BaseCommand from '#main/core/BaseCommand.js'; import { RegisterInteractionHandler } from '#main/decorators/RegisterInteractionHandler.js'; import { modPanelButton } from '#main/interactions/ShowModPanel.js'; import HubLogManager from '#main/managers/HubLogManager.js'; +import { HubService } from '#main/services/HubService.js'; import { findOriginalMessage, getOriginalMessage } from '#main/utils/network/messageUtils.js'; import type { RemoveMethods } from '#types/CustomClientProps.d.ts'; import { greyOutButton, greyOutButtons } from '#utils/ComponentUtils.js'; @@ -11,7 +12,7 @@ import { CustomID } from '#utils/CustomID.js'; import db from '#utils/Db.js'; import { InfoEmbed } from '#utils/EmbedUtils.js'; import { sendHubReport } from '#utils/hub/logger/Report.js'; -import { fetchHub, isStaffOrHubMod } from '#utils/hub/utils.js'; +import { isStaffOrHubMod } from '#utils/hub/utils.js'; import { supportedLocaleCodes, t } from '#utils/Locale.js'; import type { connectedList, Hub } from '@prisma/client'; import { @@ -336,7 +337,8 @@ export default class MessageInfo extends BaseCommand { // utils private async fetchHub(hubId: string | undefined) { - return hubId ? await fetchHub(hubId) : null; + const hubService = new HubService(db); + return hubId ? await hubService.fetchHub(hubId) : null; } private async getMessageInfo(interaction: MessageContextMenuCommandInteraction) { diff --git a/src/commands/context-menu/modActions.ts b/src/commands/context-menu/modActions.ts index f179620d..f9699ff5 100644 --- a/src/commands/context-menu/modActions.ts +++ b/src/commands/context-menu/modActions.ts @@ -1,11 +1,13 @@ import BaseCommand from '#main/core/BaseCommand.js'; import { buildModPanel } from '#main/interactions/ModPanel.js'; +import { HubService } from '#main/services/HubService.js'; +import db from '#main/utils/Db.js'; import { findOriginalMessage, getOriginalMessage, OriginalMessage, } from '#main/utils/network/messageUtils.js'; -import { fetchHub, isStaffOrHubMod } from '#utils/hub/utils.js'; +import { isStaffOrHubMod } from '#utils/hub/utils.js'; import { t, type supportedLocaleCodes } from '#utils/Locale.js'; import { ApplicationCommandType, @@ -52,7 +54,8 @@ export default class BlacklistCtxMenu extends BaseCommand { originalMsg: OriginalMessage, locale: supportedLocaleCodes, ) { - const hub = await fetchHub(originalMsg.hubId); + const hubService = new HubService(db); + const hub = await hubService.fetchHub(originalMsg.hubId); if (!hub || !isStaffOrHubMod(interaction.user.id, hub)) { await this.replyEmbed(interaction, t('errors.messageNotSentOrExpired', locale), { ephemeral: true, diff --git a/src/commands/prefix/deleteMsg.ts b/src/commands/prefix/deleteMsg.ts index 477962a7..4a9aa326 100644 --- a/src/commands/prefix/deleteMsg.ts +++ b/src/commands/prefix/deleteMsg.ts @@ -1,6 +1,6 @@ import { emojis } from '#utils/Constants.js'; import BasePrefixCommand, { CommandData } from '#main/core/BasePrefixCommand.js'; -import { fetchHub, isStaffOrHubMod } from '#main/utils/hub/utils.js'; +import { isStaffOrHubMod } from '#main/utils/hub/utils.js'; import { deleteMessageFromHub } from '#main/utils/moderation/deleteMessage.js'; import { findOriginalMessage, @@ -8,7 +8,9 @@ import { getMessageIdFromStr, getOriginalMessage, } from '#main/utils/network/messageUtils.js'; -import { Message } from 'discord.js'; +import { EmbedBuilder, Message } from 'discord.js'; +import { HubService } from '#main/services/HubService.js'; +import db from '#main/utils/Db.js'; export default class DeleteMsgCommand extends BasePrefixCommand { public readonly data: CommandData = { @@ -34,13 +36,17 @@ export default class DeleteMsgCommand extends BasePrefixCommand { return; } - const hub = await fetchHub(originalMsg.hubId); + const hubService = new HubService(db); + const hub = await hubService.fetchHub(originalMsg.hubId); if ( - !hub || - !isStaffOrHubMod(message.author.id, hub) || - originalMsg.authorId !== message.author.id + !hub || // Check if the hub exists + !isStaffOrHubMod(message.author.id, hub) || // Check if the user is a staff or hub mod + originalMsg.authorId !== message.author.id // Only then check if the user is the author of the message ) { - await message.channel.send('You do not have permission to use this command on that message.'); + const embed = new EmbedBuilder() + .setColor('Red') + .setDescription(`${emojis.no} You do not have permission to use this command.`); + await message.reply({ embeds: [embed] }); return; } diff --git a/src/commands/prefix/modpanel.ts b/src/commands/prefix/modpanel.ts index 36a91ded..86d92f98 100644 --- a/src/commands/prefix/modpanel.ts +++ b/src/commands/prefix/modpanel.ts @@ -1,23 +1,26 @@ import BasePrefixCommand, { CommandData } from '#main/core/BasePrefixCommand.js'; import { buildModPanel } from '#main/interactions/ModPanel.js'; -import { fetchHub, isStaffOrHubMod } from '#main/utils/hub/utils.js'; +import { HubService } from '#main/services/HubService.js'; +import { emojis } from '#main/utils/Constants.js'; +import db from '#main/utils/Db.js'; +import { isStaffOrHubMod } from '#main/utils/hub/utils.js'; import { findOriginalMessage, getMessageIdFromStr, getOriginalMessage, } from '#main/utils/network/messageUtils.js'; -import { Message } from 'discord.js'; +import { EmbedBuilder, Message } from 'discord.js'; export default class BlacklistPrefixCommand extends BasePrefixCommand { public readonly data: CommandData = { name: 'modpanel', description: 'Blacklist a user or server from using the bot', category: 'Moderation', - usage: 'modpanel user ID or server ID `', + usage: 'modpanel ` messageId | messageLink `', examples: ['mp 123456789012345678', 'mod 123456789012345678'], aliases: ['bl', 'mp', 'moderate', 'modactions', 'modpanel', 'mod'], dbPermission: false, - requiredArgs: 1, + requiredArgs: 0, }; protected async run(message: Message, args: string[]) { @@ -27,13 +30,17 @@ export default class BlacklistPrefixCommand extends BasePrefixCommand { : null; if (!originalMessage) { - await message.channel.send('Please provide a valid message ID or link.'); + await message.reply('Please provide a valid message ID or link.'); return; } - const hub = await fetchHub(originalMessage.hubId); + const hubService = new HubService(db); + const hub = await hubService.fetchHub(originalMessage.hubId); if (!hub || !isStaffOrHubMod(message.author.id, hub)) { - await message.channel.send('You do not have permission to use this command.'); + const embed = new EmbedBuilder() + .setColor('Red') + .setDescription(`${emojis.no} You do not have permission to use this command.`); + await message.reply({ embeds: [embed] }); return; } diff --git a/src/commands/prefix/report.ts b/src/commands/prefix/report.ts index 7edb48f6..f77f7e57 100644 --- a/src/commands/prefix/report.ts +++ b/src/commands/prefix/report.ts @@ -15,7 +15,7 @@ export default class ReportPrefixCommand extends BasePrefixCommand { name: 'report', description: 'Report a message', category: 'Utility', - usage: 'report ` [message ID or link] ` ` reason ` ', + usage: 'report ` [messageId | messageLink] ` ` reason ` ', examples: [ 'report 123456789012345678', 'report https://discord.com/channels/123456789012345678/123456789012345678/123456789012345678', diff --git a/src/commands/slash/Main/hub/announce.ts b/src/commands/slash/Main/hub/announce.ts index e0b85e81..c3b9463e 100644 --- a/src/commands/slash/Main/hub/announce.ts +++ b/src/commands/slash/Main/hub/announce.ts @@ -1,6 +1,6 @@ import { emojis } from '#utils/Constants.js'; import db from '#main/utils/Db.js'; -import { fetchHub, isHubManager, sendToHub } from '#main/utils/hub/utils.js'; +import { isHubManager, sendToHub } from '#main/utils/hub/utils.js'; import { ActionRowBuilder, ChatInputCommandInteraction, @@ -13,6 +13,7 @@ import { import HubCommand from './index.js'; import { CustomID } from '#main/utils/CustomID.js'; import { RegisterInteractionHandler } from '#main/decorators/RegisterInteractionHandler.js'; +import { HubService } from '#main/services/HubService.js'; export default class AnnounceCommand extends HubCommand { readonly cooldown = 1 * 60 * 1000; @@ -51,7 +52,8 @@ export default class AnnounceCommand extends HubCommand { await interaction.reply(`${emojis.loading} Sending announcement to all connected servers...`); const [hubId] = CustomID.parseCustomId(interaction.customId).args; const announcement = interaction.fields.getTextInputValue('announcement'); - const hub = await fetchHub(hubId); + const hubService = new HubService(db); + const hub = await hubService.fetchHub(hubId); await sendToHub(hubId, { avatarURL: hub?.iconUrl, diff --git a/src/commands/slash/Main/hub/browse.ts b/src/commands/slash/Main/hub/browse.ts index 946bfaf6..1d79ae22 100644 --- a/src/commands/slash/Main/hub/browse.ts +++ b/src/commands/slash/Main/hub/browse.ts @@ -7,7 +7,6 @@ import { getHubConnections } from '#main/utils/ConnectedListUtils.js'; import { CustomID } from '#main/utils/CustomID.js'; import db from '#main/utils/Db.js'; import { InfoEmbed } from '#main/utils/EmbedUtils.js'; -import { fetchHub } from '#main/utils/hub/utils.js'; import { calculateRating, getStars } from '#main/utils/Utils.js'; import { connectedList, Hub } from '@prisma/client'; import { stripIndents } from 'common-tags'; @@ -21,6 +20,7 @@ import { EmbedField, time, } from 'discord.js'; +import { HubService } from '#main/services/HubService.js'; export default class BrowseCommand extends HubCommand { async execute(interaction: ChatInputCommandInteraction) { @@ -71,14 +71,15 @@ export default class BrowseCommand extends HubCommand { const customId = CustomID.parseCustomId(interaction.customId); const [hubId] = customId.args; - if (!interaction.memberPermissions.has('ManageMessages')) { + if (!interaction.memberPermissions.has('ManageMessages', true)) { await interaction.deferUpdate(); return; } await interaction.deferReply(); - const hub = await fetchHub(hubId); + const hubService = new HubService(db); + const hub = await hubService.fetchHub(hubId); if (!hub) { await interaction.reply({ content: 'Hub not found.', ephemeral: true }); return; diff --git a/src/commands/slash/Main/hub/create.ts b/src/commands/slash/Main/hub/create.ts index 2e24ed0a..e5b99de8 100644 --- a/src/commands/slash/Main/hub/create.ts +++ b/src/commands/slash/Main/hub/create.ts @@ -1,9 +1,11 @@ -import Constants, { emojis } from '#utils/Constants.js'; import { RegisterInteractionHandler } from '#main/decorators/RegisterInteractionHandler.js'; -import { HubSettingsBits } from '#main/modules/BitFields.js'; -import { CustomID } from '#utils/CustomID.js'; +import { HubValidator } from '#main/modules/HubValidator.js'; +import { HubCreationData, HubService } from '#main/services/HubService.js'; +import { CustomID } from '#main/utils/CustomID.js'; +import { handleError } from '#main/utils/Utils.js'; +import Constants from '#utils/Constants.js'; import db from '#utils/Db.js'; -import { t } from '#utils/Locale.js'; +import { supportedLocaleCodes, t } from '#utils/Locale.js'; import { ActionRowBuilder, CacheType, @@ -17,17 +19,19 @@ import { import HubCommand from './index.js'; export default class Create extends HubCommand { + private readonly hubService: HubService; readonly cooldown = 10 * 60 * 1000; // 10 mins + constructor() { + super(); + this.hubService = new HubService(db); + } + async execute(interaction: ChatInputCommandInteraction) { const { userManager } = interaction.client; const locale = await userManager.getUserLocale(interaction.user.id); - const isOnCooldown = await this.getRemainingCooldown(interaction); - if (isOnCooldown) { - await this.sendCooldownError(interaction, isOnCooldown, locale); - return; - } + if (await this.isOnCooldown(interaction, locale)) return; const modal = new ModalBuilder() .setTitle(t('hub.create.modal.title', locale)) @@ -80,93 +84,93 @@ export default class Create extends HubCommand { await interaction.showModal(modal); } + private async isOnCooldown( + interaction: ChatInputCommandInteraction, + locale: supportedLocaleCodes, + ): Promise { + const remainingCooldown = await this.getRemainingCooldown(interaction); + if (remainingCooldown) { + await this.sendCooldownError(interaction, remainingCooldown, locale); + return true; + } + return false; + } + @RegisterInteractionHandler('hub_create_modal') async handleModals(interaction: ModalSubmitInteraction): Promise { await interaction.deferReply({ ephemeral: true }); - const name = interaction.fields.getTextInputValue('name'); - const description = interaction.fields.getTextInputValue('description'); - const icon = interaction.fields.getTextInputValue('icon'); - const banner = interaction.fields.getTextInputValue('banner'); const { userManager } = interaction.client; const locale = await userManager.getUserLocale(interaction.user.id); - // if hubName contains "discord", "clyde" "```" then return - if (Constants.Regex.BannedWebhookWords.test(name)) { - await interaction.followUp({ - content: t('hub.create.invalidName', locale, { emoji: emojis.no }), - ephemeral: true, - }); - return; + try { + const hubData = this.extractHubData(interaction); + await this.processHubCreation(interaction, hubData, locale); } - - const hubs = await db.hub.findMany({ - where: { OR: [{ ownerId: interaction.user.id }, { name }] }, - }); - - if (hubs.find((hub) => hub.name === name)) { - await interaction.followUp({ - content: t('hub.create.nameTaken', locale, { emoji: emojis.no }), - ephemeral: true, - }); - return; - } - else if ( - hubs.reduce((acc, hub) => (hub.ownerId === interaction.user.id ? acc + 1 : acc), 0) >= 3 - ) { - await interaction.followUp({ - content: t('hub.create.maxHubs', locale, { emoji: emojis.no }), - ephemeral: true, - }); - return; + catch (error) { + handleError(error, interaction); } + } - const imgurRegex = Constants.Regex.ImgurImage; - const iconUrl = icon.length > 0 ? icon.match(imgurRegex)?.[0] : false; - const bannerUrl = banner.length > 0 ? banner.match(imgurRegex)?.[0] : false; + private extractHubData(interaction: ModalSubmitInteraction): HubCreationData { + return { + name: interaction.fields.getTextInputValue('name'), + description: interaction.fields.getTextInputValue('description'), + iconUrl: interaction.fields.getTextInputValue('icon'), + bannerUrl: interaction.fields.getTextInputValue('banner'), + ownerId: interaction.user.id, + }; + } - // TODO: create a gif showing how to get imgur links - if (iconUrl === false || bannerUrl === false) { + private async processHubCreation( + interaction: ModalSubmitInteraction, + hubData: HubCreationData, + locale: supportedLocaleCodes, + ): Promise { + const validator = new HubValidator(locale); + const existingHubs = await this.hubService.getExistingHubs(hubData.ownerId, hubData.name); + + const validationResult = await validator.validateNewHub(hubData, existingHubs); + if (!validationResult.isValid) { await interaction.followUp({ - content: t('hub.invalidImgurUrl', locale, { emoji: emojis.no }), + content: validationResult.error, ephemeral: true, }); return; } - await db.hub.create({ - data: { - name, - description, - private: true, - ownerId: interaction.user.id, - iconUrl: iconUrl ?? Constants.Links.EasterAvatar, - bannerUrl, - settings: - HubSettingsBits.SpamFilter | HubSettingsBits.Reactions | HubSettingsBits.BlockNSFW, - }, - }); - - // set cooldown after creating a hub (because a failed hub creation should not trigger the cooldown) - interaction.client.commandCooldowns.setCooldown( - `${interaction.user.id}-hub-create`, - 60 * 60 * 1000, - ); // 1 hour + await this.hubService.createHub(hubData); + await this.handleSuccessfulCreation(interaction, hubData.name, locale); + } + + private async handleSuccessfulCreation( + interaction: ModalSubmitInteraction, + hubName: string, + locale: supportedLocaleCodes, + ): Promise { + this.setCooldowns(interaction); const successEmbed = new EmbedBuilder() .setColor('Green') .setDescription( t('hub.create.success', locale, { - name, + name: hubName, support_invite: Constants.Links.SupportInvite, docs_link: Constants.Links.Docs, }), ) .setTimestamp(); + await interaction.editReply({ embeds: [successEmbed] }); + } + + private setCooldowns(interaction: ModalSubmitInteraction): void { + interaction.client.commandCooldowns.setCooldown( + `${interaction.user.id}-hub-create`, + 60 * 60 * 1000, // 1 hour + ); + const command = HubCommand.subcommands.get('create'); command?.setUserCooldown(interaction); - - await interaction.editReply({ embeds: [successEmbed] }); } } diff --git a/src/commands/slash/Main/hub/delete.ts b/src/commands/slash/Main/hub/delete.ts index 955ab694..7b01bd31 100644 --- a/src/commands/slash/Main/hub/delete.ts +++ b/src/commands/slash/Main/hub/delete.ts @@ -1,10 +1,10 @@ -import { emojis } from '#utils/Constants.js'; import { RegisterInteractionHandler } from '#main/decorators/RegisterInteractionHandler.js'; +import { HubService } from '#main/services/HubService.js'; import { setComponentExpiry } from '#utils/ComponentUtils.js'; +import { emojis } from '#utils/Constants.js'; import { CustomID } from '#utils/CustomID.js'; import db from '#utils/Db.js'; import { InfoEmbed } from '#utils/EmbedUtils.js'; -import { deleteHubs } from '#utils/hub/utils.js'; import { t } from '#utils/Locale.js'; import { ActionRowBuilder, @@ -96,8 +96,11 @@ export default class Delete extends HubCommand { await interaction.update({ embeds: [embed], components: [] }); - const hubInDb = await db.hub.findFirst({ where: { id: hubId, ownerId: interaction.user.id } }); - if (!hubInDb) { + const hubService = new HubService(db); + const hubInDb = await hubService.fetchHub(hubId); + + // only the owner can delete the hub + if (hubInDb?.ownerId !== interaction.user.id) { const infoEmbed = new InfoEmbed().setDescription( t('hub.notFound', locale, { emoji: emojis.no }), ); @@ -106,10 +109,12 @@ export default class Delete extends HubCommand { return; } - await deleteHubs([hubInDb.id]); + // Delete the hub and all related data + await hubService.deleteHub(hubInDb.id); await interaction.editReply({ content: t('hub.delete.success', locale, { emoji: emojis.tick, hub: hubInDb.name }), + embeds: [], }); } } diff --git a/src/commands/slash/Main/hub/logging.ts b/src/commands/slash/Main/hub/logging.ts index 9886f9c1..4fe2a153 100644 --- a/src/commands/slash/Main/hub/logging.ts +++ b/src/commands/slash/Main/hub/logging.ts @@ -1,29 +1,71 @@ -import { ChatInputCommandInteraction } from 'discord.js'; -import HubCommand from './index.js'; -import db from '#utils/Db.js'; import HubLogManager, { LogConfigTypes, RoleIdLogConfigs } from '#main/managers/HubLogManager.js'; +import { HubService } from '#main/services/HubService.js'; +import { isGuildTextBasedChannel } from '#main/utils/ChannelUtls.js'; +import { emojis } from '#main/utils/Constants.js'; +import db from '#utils/Db.js'; import { Hub } from '@prisma/client'; +import { + Channel, + ChatInputCommandInteraction, + GuildMember, + GuildTextBasedChannel, + Role, +} from 'discord.js'; +import HubCommand from './index.js'; + +interface SetLogOptions { + hubId: string; + logType: LogConfigTypes; + target: GuildTextBasedChannel | Role | null; + member: GuildMember; +} export default class LoggingCommand extends HubCommand { async execute(interaction: ChatInputCommandInteraction) { - const subcommand = interaction.options.getSubcommand(); + if (!interaction.inCachedGuild()) return; - const hub = await this.runHubChecks(interaction); + const hub = await this.getHubForUser(interaction); if (!hub) return; - switch (subcommand) { - case 'view': - await this.handleView(interaction, hub); - break; - case 'set_channel': - await this.handleSet(interaction, hub.id, 'channel'); - break; - case 'set_role': - await this.handleSet(interaction, hub.id, 'role'); - break; - default: - break; + const handlers = { + view: () => this.handleView(interaction, hub), + set_channel: () => this.handleSetChannel(interaction, hub.id), + set_role: () => this.handleSetRole(interaction, hub.id), + }; + + const subcommand = interaction.options.getSubcommand() as keyof typeof handlers; + await handlers[subcommand]?.(); + } + + private async getHubForUser(interaction: ChatInputCommandInteraction): Promise { + const hubService = new HubService(db); + const hubName = interaction.options.getString('hub'); + const hubs = await hubService.getHubsForUser(interaction.user.id); + + if (hubs.length === 0) { + await this.replyEmbed(interaction, 'You do not have access to any hubs.', { + ephemeral: true, + }); + return null; } + + if (hubName) { + const hub = hubs.find((h) => h.name === hubName); + if (!hub) { + await this.replyEmbed(interaction, 'Hub not found.', { ephemeral: true }); + return null; + } + return hub; + } + + if (hubs.length === 1) return hubs[0]; + + await this.replyEmbed( + interaction, + 'You must provide a hub in the `hub` option of the command.', + { ephemeral: true }, + ); + return null; } private async handleView(interaction: ChatInputCommandInteraction, hub: Hub) { @@ -32,85 +74,96 @@ export default class LoggingCommand extends HubCommand { await interaction.reply({ embeds: [embed] }); } - private async handleSet( - interaction: ChatInputCommandInteraction, + private async handleSetChannel( + interaction: ChatInputCommandInteraction<'cached'>, hubId: string, - setType: 'channel' | 'role', ) { - const id = - setType === 'channel' - ? interaction.options.getChannel('channel')?.id - : interaction.options.getRole('role')?.id; - + const channel = interaction.options.getChannel('channel') as GuildTextBasedChannel | null; const logType = interaction.options.getString('log_type', true) as LogConfigTypes; + + if (!this.validateChannelPermissions(channel, interaction)) return; + + await this.handleSetLogConfig({ + hubId, + logType, + target: channel, + member: interaction.member, + setType: 'channel', + }); + + await this.sendSetConfirmation(interaction, logType, channel, 'channel'); + } + + private async handleSetRole(interaction: ChatInputCommandInteraction<'cached'>, hubId: string) { + const role = interaction.options.getRole('role'); + const logType = interaction.options.getString('log_type', true) as RoleIdLogConfigs; + + await this.handleSetLogConfig({ + hubId, + logType, + target: role, + member: interaction.member, + setType: 'role', + }); + + await this.sendSetConfirmation(interaction, logType, role, 'role'); + } + + private async handleSetLogConfig({ + hubId, + logType, + target, + setType, + }: SetLogOptions & { setType: 'channel' | 'role' }) { const hubLogManager = await HubLogManager.create(hubId); - if (!id) { + if (!target?.id) { if (setType === 'channel') await hubLogManager.resetLog(logType); else await hubLogManager.removeRoleId(logType as RoleIdLogConfigs); - - await this.replyEmbed( - interaction, - `Successfully reset logging ${setType} for type \`${logType}\`.`, - { - ephemeral: true, - }, - ); return; } if (setType === 'channel') { - await hubLogManager.setLogChannel(logType, id); - await this.replyEmbed( - interaction, - `Successfully set \`${logType}\` logging channel to <#${id}>.`, - { ephemeral: true }, - ); + await hubLogManager.setLogChannel(logType, target.id); } - else if (setType === 'role' && hubLogManager.config.appeals?.channelId) { - await hubLogManager.setRoleId(logType as RoleIdLogConfigs, id); - await this.replyEmbed( - interaction, - `Successfully set \`${logType}\` mention role to <@&${id}>.`, - { ephemeral: true }, - ); + else if (hubLogManager.config.appeals?.channelId) { + await hubLogManager.setRoleId(logType as RoleIdLogConfigs, target.id); } else { - await this.replyEmbed( - interaction, - 'You must set the logging channel before setting the role ID.', - { ephemeral: true }, - ); + throw new Error('Appeals channel must be set before setting role ID'); } } - private async runHubChecks(interaction: ChatInputCommandInteraction) { - const hubName = interaction.options.getString('hub') as string | undefined; - const hubs = await db.hub.findMany({ - where: { - OR: [ - { ownerId: interaction.user.id }, - { moderators: { some: { userId: interaction.user.id, position: 'manager' } } }, - ], - }, - }); + private validateChannelPermissions( + channel: GuildTextBasedChannel | null, + interaction: ChatInputCommandInteraction<'cached'>, + ): boolean { + if (!channel) return true; - let hub; - if (hubName) { - hub = hubs.find((h) => h.name.toLowerCase() === hubName.toLowerCase()); - } - else if (hubs.length === 1) { - hub = hubs[0]; - } - else if (hubs.length > 1 || !hub) { - await this.replyEmbed( - interaction, - 'You must provide a hub in the `hub` option of the command.', - { ephemeral: true }, - ); - return null; + const hasPermissions = + isGuildTextBasedChannel(channel as Channel) && + channel.permissionsFor(interaction.member).has('ManageMessages', true); + + if (!hasPermissions) { + this.replyEmbed(interaction, 'errors.missingPermissions', { + t: { emoji: emojis.no, permissions: 'Manage Messages' }, + }); + return false; } - return hub; + return true; + } + + private async sendSetConfirmation( + interaction: ChatInputCommandInteraction, + logType: string, + target: GuildTextBasedChannel | Role | null, + type: 'channel' | 'role', + ) { + const message = target + ? `Successfully set \`${logType}\` ${type} to <#${target.id}>.` + : `Successfully reset logging ${type} for type \`${logType}\`.`; + + await this.replyEmbed(interaction, message, { ephemeral: true }); } } diff --git a/src/commands/slash/Main/setup.ts b/src/commands/slash/Main/setup.ts index 0bf198b4..bc137dd5 100644 --- a/src/commands/slash/Main/setup.ts +++ b/src/commands/slash/Main/setup.ts @@ -150,7 +150,7 @@ export default class SetupCommand extends BaseCommand { return; } - if (!interaction.member.permissionsIn(channel).has('ManageMessages')) { + if (!interaction.member.permissionsIn(channel).has('ManageMessages', true)) { await interaction.reply({ content: 'You cannot setup the bot in a channel where you do not have `Manage Messages` permission.', diff --git a/src/events/messageReactionAdd.ts b/src/events/messageReactionAdd.ts index d639e2d4..1cee8d87 100644 --- a/src/events/messageReactionAdd.ts +++ b/src/events/messageReactionAdd.ts @@ -1,6 +1,7 @@ import BaseEventListener from '#main/core/BaseEventListener.js'; import { HubSettingsBitField } from '#main/modules/BitFields.js'; -import { fetchHub } from '#main/utils/hub/utils.js'; +import { HubService } from '#main/services/HubService.js'; +import db from '#main/utils/Db.js'; import { findOriginalMessage, getOriginalMessage, @@ -27,7 +28,9 @@ export default class ReadctionAdd extends BaseEventListener<'messageReactionAdd' const originalMsg = (await getOriginalMessage(reaction.message.id)) ?? (await findOriginalMessage(reaction.message.id)); - const hub = originalMsg ? await fetchHub(originalMsg?.hubId) : null; + + const hubService = new HubService(db); + const hub = originalMsg ? await hubService.fetchHub(originalMsg?.hubId) : null; if (!originalMsg || !hub || !new HubSettingsBitField(hub.settings).has('Reactions')) { return; diff --git a/src/interactions/BlacklistAppeal.ts b/src/interactions/BlacklistAppeal.ts index 222eb7ed..6b2747bd 100644 --- a/src/interactions/BlacklistAppeal.ts +++ b/src/interactions/BlacklistAppeal.ts @@ -3,10 +3,11 @@ import BlacklistManager from '#main/managers/BlacklistManager.js'; import HubLogManager from '#main/managers/HubLogManager.js'; import ServerInfractionManager from '#main/managers/InfractionManager/ServerInfractionManager.js'; import UserInfractionManager from '#main/managers/InfractionManager/UserInfractionManager.js'; +import { HubService } from '#main/services/HubService.js'; +import db from '#main/utils/Db.js'; import { CustomID } from '#utils/CustomID.js'; import { ErrorEmbed, InfoEmbed } from '#utils/EmbedUtils.js'; import logAppeals from '#utils/hub/logger/Appeals.js'; -import { fetchHub } from '#utils/hub/utils.js'; import Logger from '#utils/Logger.js'; import { buildAppealSubmitModal } from '#utils/moderation/blacklistUtils.js'; import { getReplyMethod, msToReadable } from '#utils/Utils.js'; @@ -144,7 +145,8 @@ export default class AppealInteraction { if (!blacklist) return; if (customId.suffix === 'approve') await blacklistManager.removeBlacklist(hubId); - const hub = await fetchHub(hubId); + const hubService = new HubService(db); + const hub = await hubService.fetchHub(hubId); let appealer; let appealTarget; @@ -204,7 +206,8 @@ export default class AppealInteraction { const blacklistManager = new BlacklistManager(infractionManager); - const hub = await fetchHub(hubId); + const hubService = new HubService(db); + const hub = await hubService.fetchHub(hubId); const allInfractions = await infractionManager.getHubInfractions(hubId, { type: 'BLACKLIST' }); const sevenDays = 60 * 60 * 24 * 7 * 1000; diff --git a/src/interactions/ModPanel.ts b/src/interactions/ModPanel.ts index 7bd42005..e711a360 100644 --- a/src/interactions/ModPanel.ts +++ b/src/interactions/ModPanel.ts @@ -2,9 +2,11 @@ import { RegisterInteractionHandler } from '#main/decorators/RegisterInteraction import BlacklistManager from '#main/managers/BlacklistManager.js'; import ServerInfractionManager from '#main/managers/InfractionManager/ServerInfractionManager.js'; import UserInfractionManager from '#main/managers/InfractionManager/UserInfractionManager.js'; +import { HubService } from '#main/services/HubService.js'; import { CustomID } from '#main/utils/CustomID.js'; +import db from '#main/utils/Db.js'; import { InfoEmbed } from '#main/utils/EmbedUtils.js'; -import { fetchHub, isStaffOrHubMod } from '#main/utils/hub/utils.js'; +import { isStaffOrHubMod } from '#main/utils/hub/utils.js'; import { supportedLocaleCodes, t } from '#main/utils/Locale.js'; import { isDeleteInProgress } from '#main/utils/moderation/deleteMessage.js'; import { @@ -102,7 +104,8 @@ export default class ModPanelHandler { originalMsg: OriginalMessage, locale: supportedLocaleCodes, ) { - const hub = await fetchHub(originalMsg.hubId); + const hubService = new HubService(db); + const hub = await hubService.fetchHub(originalMsg.hubId); if (!hub || !isStaffOrHubMod(interaction.user.id, hub)) { const embed = new InfoEmbed().setDescription(t('errors.messageNotSentOrExpired', locale)); await interaction.editReply({ embeds: [embed] }); diff --git a/src/interactions/NetworkReaction.ts b/src/interactions/NetworkReaction.ts index 5021e2a7..22b1902b 100644 --- a/src/interactions/NetworkReaction.ts +++ b/src/interactions/NetworkReaction.ts @@ -2,7 +2,6 @@ import Constants, { emojis } from '#utils/Constants.js'; import { RegisterInteractionHandler } from '#main/decorators/RegisterInteractionHandler.js'; import HubSettingsManager from '#main/managers/HubSettingsManager.js'; import { HubSettingsBitField } from '#main/modules/BitFields.js'; -import { fetchHub } from '#main/utils/hub/utils.js'; import { findOriginalMessage, getOriginalMessage, @@ -26,6 +25,8 @@ import { StringSelectMenuBuilder, time, } from 'discord.js'; +import db from '#main/utils/Db.js'; +import { HubService } from '#main/services/HubService.js'; export default class NetworkReactionInteraction { @RegisterInteractionHandler('reaction_') @@ -38,7 +39,9 @@ export default class NetworkReactionInteraction { const { customId, messageId } = this.getInteractionDetails(interaction); const originalMessage = (await getOriginalMessage(messageId)) ?? (await findOriginalMessage(messageId)); - const hub = originalMessage ? await fetchHub(originalMessage?.hubId) : null; + + const hubService = new HubService(db); + const hub = originalMessage ? await hubService.fetchHub(originalMessage?.hubId) : null; if (!originalMessage || !this.isReactionAllowed(hub)) return; diff --git a/src/interactions/ShowModPanel.ts b/src/interactions/ShowModPanel.ts index 45881f9f..84f30564 100644 --- a/src/interactions/ShowModPanel.ts +++ b/src/interactions/ShowModPanel.ts @@ -2,10 +2,12 @@ import { emojis } from '#utils/Constants.js'; import { RegisterInteractionHandler } from '#main/decorators/RegisterInteractionHandler.js'; import { CustomID } from '#main/utils/CustomID.js'; import { InfoEmbed } from '#main/utils/EmbedUtils.js'; -import { fetchHub, isStaffOrHubMod } from '#main/utils/hub/utils.js'; +import { isStaffOrHubMod } from '#main/utils/hub/utils.js'; import { findOriginalMessage, getOriginalMessage } from '#main/utils/network/messageUtils.js'; import { ButtonBuilder, ButtonInteraction, ButtonStyle } from 'discord.js'; import { buildModPanel } from '#main/interactions/ModPanel.js'; +import { HubService } from '#main/services/HubService.js'; +import db from '#main/utils/Db.js'; export const modPanelButton = (targetMsgId: string, opts?: { label?: string; emoji?: string }) => new ButtonBuilder() @@ -24,7 +26,9 @@ export default class ModActionsButton { const originalMessage = (await getOriginalMessage(messageId)) ?? (await findOriginalMessage(messageId)); - const hub = originalMessage ? await fetchHub(originalMessage?.hubId) : null; + + const hubService = new HubService(db); + const hub = originalMessage ? await hubService.fetchHub(originalMessage?.hubId) : null; if (!originalMessage || !hub || !isStaffOrHubMod(interaction.user.id, hub)) { await interaction.editReply({ components: [] }); diff --git a/src/modules/HubValidator.ts b/src/modules/HubValidator.ts new file mode 100644 index 00000000..d6f91f98 --- /dev/null +++ b/src/modules/HubValidator.ts @@ -0,0 +1,84 @@ +import type { HubCreationData } from '#main/services/HubService.js'; +import Constants, { emojis } from '#main/utils/Constants.js'; +import db from '#main/utils/Db.js'; +import { supportedLocaleCodes, t } from '#main/utils/Locale.js'; +import { Hub } from '@prisma/client'; + +export interface ValidationResult { + isValid: boolean; + error?: string; +} + +export class HubValidator { + private readonly locale: supportedLocaleCodes; + + constructor(locale: supportedLocaleCodes) { + this.locale = locale; + } + + private static readonly MAX_HUBS_PER_USER = 3; + + async validateNewHub(data: HubCreationData, existingHubs: Hub[]): Promise { + const nameValidation = this.validateHubName(data.name); + if (!nameValidation.isValid) return nameValidation; + + const uniqueNameValidation = await this.validateUniqueName(data.name); + if (!uniqueNameValidation.isValid) return uniqueNameValidation; + + const hubLimitValidation = this.validateHubLimit(data.ownerId, existingHubs); + if (!hubLimitValidation.isValid) return hubLimitValidation; + + const imageValidation = this.validateImages(data.iconUrl, data.bannerUrl); + if (!imageValidation.isValid) return imageValidation; + + return { isValid: true }; + } + + private validateHubName(name: string): ValidationResult { + if (Constants.Regex.BannedWebhookWords.test(name)) { + return { + isValid: false, + error: t('hub.create.invalidName', this.locale, { emoji: emojis.no }), + }; + } + return { isValid: true }; + } + + private async validateUniqueName(name: string): Promise { + const existingHub = await db.hub.findFirst({ where: { name } }); + if (existingHub) { + return { + isValid: false, + error: t('hub.create.nameTaken', this.locale, { emoji: emojis.no }), + }; + } + return { isValid: true }; + } + + private validateHubLimit(ownerId: string, existingHubs: Hub[]): ValidationResult { + const userHubCount = existingHubs.reduce( + (acc, hub) => (hub.ownerId === ownerId ? acc + 1 : acc), + 0, + ); + + if (userHubCount >= HubValidator.MAX_HUBS_PER_USER) { + return { + isValid: false, + error: t('hub.create.maxHubs', this.locale, { emoji: emojis.no }), + }; + } + return { isValid: true }; + } + + private validateImages(iconUrl?: string, bannerUrl?: string): ValidationResult { + const imgurRegex = Constants.Regex.ImgurImage; + + if ((iconUrl && !imgurRegex.test(iconUrl)) || (bannerUrl && !imgurRegex.test(bannerUrl))) { + return { + isValid: false, + error: t('hub.invalidImgurUrl', this.locale, { emoji: emojis.no }), + }; + } + return { isValid: true }; + } +} diff --git a/src/services/HubJoinService.ts b/src/services/HubJoinService.ts index 8f428c51..a4d4ff42 100644 --- a/src/services/HubJoinService.ts +++ b/src/services/HubJoinService.ts @@ -82,7 +82,7 @@ export class HubJoinService { } private async runChecks(channel: GuildTextBasedChannel) { - if (!channel.permissionsFor(this.interaction.member).has('ManageMessages')) { + if (!channel.permissionsFor(this.interaction.member).has('ManageMessages', true)) { await this.replyError('errors.missingPermissions', { permissions: 'Manage Messages', emoji: emojis.no, diff --git a/src/services/HubService.ts b/src/services/HubService.ts new file mode 100644 index 00000000..5167688f --- /dev/null +++ b/src/services/HubService.ts @@ -0,0 +1,67 @@ +import { HubSettingsBits } from '#main/modules/BitFields.js'; +import { deleteConnections } from '#main/utils/ConnectedListUtils.js'; +import Constants from '#main/utils/Constants.js'; +import { PrismaClient } from '@prisma/client'; + +export interface HubCreationData { + name: string; + description: string; + iconUrl?: string; + bannerUrl?: string; + ownerId: string; +} + +export class HubService { + private readonly db: PrismaClient; + constructor(db: PrismaClient) { + this.db = db; + } + + async fetchHub(id: string) { + return await this.db.hub.findFirst({ where: { id } }); + } + + async createHub(data: HubCreationData): Promise { + await this.db.hub.create({ + data: { + ...data, + private: true, + iconUrl: data.iconUrl ?? Constants.Links.EasterAvatar, + bannerUrl: data.bannerUrl ?? null, + settings: + HubSettingsBits.SpamFilter | HubSettingsBits.Reactions | HubSettingsBits.BlockNSFW, + }, + }); + } + + async deleteHub(hubId: string): Promise { + // delete all relations first and then delete the hub + await deleteConnections({ hubId }); + await this.db.$transaction([ + this.db.hubInvite.deleteMany({ where: { hubId } }), + this.db.hubLogConfig.deleteMany({ where: { hubId } }), + this.db.messageBlockList.deleteMany({ where: { hubId } }), + this.db.userInfraction.deleteMany({ where: { hubId } }), + this.db.serverInfraction.deleteMany({ where: { hubId } }), + ]); + + // finally, delete the hub + await this.db.hub.deleteMany({ where: { id: hubId } }); + } + + async getHubsForUser(userId: string) { + return await this.db.hub.findMany({ where: { ownerId: userId } }); + } + + async getHubByName(name: string) { + return await this.db.hub.findFirst({ where: { name } }); + } + + async getExistingHubs(ownerId: string, hubName: string) { + return await this.db.hub.findMany({ + where: { + OR: [{ ownerId }, { name: hubName }], + }, + }); + } +} diff --git a/src/utils/Constants.ts b/src/utils/Constants.ts index 8f2d9351..41f19e3d 100644 --- a/src/utils/Constants.ts +++ b/src/utils/Constants.ts @@ -87,7 +87,7 @@ export default { Hexcode: /^#[0-9A-F]{6}$/i, ChannelMention: /<#|!|>/g, ImgurImage: /https?:\/\/i\.imgur\.com\/[a-zA-Z0-9]+\.((jpg)|(jpeg)|(png)|(gif))/g, - MessageLink: /https:\/\/discord.com\/channels\/(\d{17,19})\/(\d{17,19})\/(\d{17,19})/g, + MessageLink: /https:\/\/(?:canary\.|ptb\.)?discord\.com\/channels\/(\d+)\/(\d+)\/(\d+)/, SimpleRegexEscape: /[.*+?^${}()|[\]\\]/g, RegexChars: /[-[\]{}()*+?.,\\^$|#\s]/g, }, diff --git a/src/utils/hub/logger/BlockWordAlert.ts b/src/utils/hub/logger/BlockWordAlert.ts index 066f4c61..ed75b894 100644 --- a/src/utils/hub/logger/BlockWordAlert.ts +++ b/src/utils/hub/logger/BlockWordAlert.ts @@ -1,27 +1,37 @@ import { emojis } from '#utils/Constants.js'; import HubLogManager from '#main/managers/HubLogManager.js'; import { sendLog } from '#main/utils/hub/logger/Default.js'; -import { ACTION_LABELS } from '#main/utils/moderation/blockWords.js'; +import { ACTION_LABELS, createRegexFromWords } from '#main/utils/moderation/blockWords.js'; import { MessageBlockList } from '@prisma/client'; import { stripIndents } from 'common-tags'; import { EmbedBuilder, Message } from 'discord.js'; -export const logBlockwordAlert = async (message: Message, rule: MessageBlockList) => { +const boldANSIText = (text: string) => `\u001b[1;2m${text}\u001b[0m`; + +export const logBlockwordAlert = async ( + message: Message, + rule: MessageBlockList, + matches: string[], +) => { const logManager = await HubLogManager.create(rule.hubId); if (!logManager.config.networkAlerts) return; + const content = message.content.replace(createRegexFromWords(matches), boldANSIText); const embed = new EmbedBuilder() .setColor('Yellow') .setTitle(`${emojis.exclamation} Blocked Word Alert`) .setDescription( stripIndents` A message containing blocked words was detected: + **Rule Triggered:** ${rule.name} **Author:** ${message.author.tag} (${message.author.id}) - **Server:** ${message.guild.name} (${message.guild.id}) - **Message Content:** - \`\`\`${message.content}\`\`\` + **Server:** ${message.guild.name} (${message.guild.id}) + ### Message Content: + \`\`\`ansi + ${content} + \`\`\` -# Actions Taken: **${rule.actions.map((a) => ACTION_LABELS[a]).join(', ')}** `, ) diff --git a/src/utils/hub/utils.ts b/src/utils/hub/utils.ts index bb8e07ff..722a1de2 100644 --- a/src/utils/hub/utils.ts +++ b/src/utils/hub/utils.ts @@ -1,10 +1,8 @@ import Constants from '#main/utils/Constants.js'; import { deleteConnection, - deleteConnections, getHubConnections, } from '#utils/ConnectedListUtils.js'; -import db from '#utils/Db.js'; import Logger from '#utils/Logger.js'; import { checkIfStaff } from '#utils/Utils.js'; import type { Hub } from '@prisma/client'; @@ -50,16 +48,6 @@ export const sendToHub = async ( } }; -export const deleteHubs = async (ids: string[]) => { - // delete all relations first and then delete the hub - await deleteConnections({ hubId: { in: ids } }); - await db.hubInvite.deleteMany({ where: { hubId: { in: ids } } }); - await db.hubLogConfig.deleteMany({ where: { hubId: { in: ids } } }); - - // finally, delete the hub - await db.hub.deleteMany({ where: { id: { in: ids } } }); -}; -export const fetchHub = async (id: string) => await db.hub.findFirst({ where: { id } }); export const isHubMod = (userId: string, hub: Hub) => Boolean(hub.ownerId === userId || hub.moderators.find((mod) => mod.userId === userId)); diff --git a/src/utils/network/blockwordsRunner.ts b/src/utils/network/blockwordsRunner.ts index 22d84eb7..f416424a 100644 --- a/src/utils/network/blockwordsRunner.ts +++ b/src/utils/network/blockwordsRunner.ts @@ -16,7 +16,11 @@ interface ActionResult { } // Action handler type -type ActionHandler = (message: Message, rule: MessageBlockList) => Awaitable; +type ActionHandler = ( + message: Message, + rule: MessageBlockList, + matches: string[], +) => Awaitable; // Map of action handlers const actionHandlers: Record = { @@ -26,9 +30,9 @@ const actionHandlers: Record = { message: 'Message blocked due to containing prohibited words.', }), - [BlockWordAction.SEND_ALERT]: async (message, rule) => { + [BlockWordAction.SEND_ALERT]: async (message, rule, matches) => { // Send alert to moderators - await logBlockwordAlert(message, rule); + await logBlockwordAlert(message, rule, matches); return { success: true, shouldBlock: false }; }, @@ -67,7 +71,9 @@ export async function checkBlockedWords(message: Message, msgBlockList: Me for (const rule of msgBlockList) { const regex = createRegexFromWords(rule.words); - if (regex.test(message.content)) { + const matches = message.content.match(regex); + + if (matches) { let shouldBlock = false; let blockReason: string | undefined; @@ -76,7 +82,7 @@ export async function checkBlockedWords(message: Message, msgBlockList: Me const handler = actionHandlers[action]; if (handler) { try { - const result = await handler(message, rule); + const result = await handler(message, rule, matches); if (result.success && result.shouldBlock) { shouldBlock = true; blockReason = result.message;