diff --git a/locales/en.yml b/locales/en.yml index c3672fcba..1ccf19a14 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -296,6 +296,9 @@ hub: appeals: label: Appeals description: Recieve appeals from blacklisted users/servers. + networkAlerts: + label: Network Alerts + description: Recieve alerts about automatically blocked messages. report: modal: title: Report Details diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 1a50f3762..840858e8e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -54,6 +54,12 @@ enum InfractionStatus { APPEALED } +enum BlockWordAction { + BLOCK_MESSAGE + BLACKLIST + SEND_ALERT +} + model UserInfraction { id String @id @default(nanoid(10)) @map("_id") userId String @db.String @@ -91,8 +97,8 @@ model ServerInfraction { // TODO: major refactor needed for mode and profFilter thing model connectedList { - id String @id @default(auto()) @map("_id") @db.ObjectId - channelId String @unique // channel can be thread, or a normal channel + id String @id @default(auto()) @map("_id") @db.ObjectId + channelId String @unique // channel can be thread, or a normal channel parentId String? // ID of the parent channel, if it's a thread @map("parentChannelId") serverId String connected Boolean @@ -101,10 +107,10 @@ model connectedList { profFilter Boolean embedColor String? webhookURL String - lastActive DateTime? @default(now()) - date DateTime @default(now()) - hub Hub? @relation(fields: [hubId], references: [id]) - hubId String @db.ObjectId + lastActive DateTime @default(now()) + date DateTime @default(now()) + hub Hub? @relation(fields: [hubId], references: [id]) + hubId String @db.ObjectId @@index(fields: [channelId, serverId]) } @@ -135,27 +141,29 @@ model Hub { } model MessageBlockList { - id String @id @default(auto()) @map("_id") @db.ObjectId + id String @id @default(auto()) @map("_id") @db.ObjectId name String words String createdBy String - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - hub Hub @relation(fields: [hubId], references: [id]) - hubId String @db.ObjectId + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + actions BlockWordAction[] @default([]) + hub Hub @relation(fields: [hubId], references: [id]) + hubId String @db.ObjectId @@index([id, words]) } model HubLogConfig { - id String @id @default(auto()) @map("_id") @db.ObjectId - modLogs String? - joinLeaves String? - profanity String? - appeals hubLogChannelAndRole? - reports hubLogChannelAndRole? - hub Hub @relation(fields: [hubId], references: [id]) - hubId String @unique @db.ObjectId + id String @id @default(auto()) @map("_id") @db.ObjectId + modLogs String? + joinLeaves String? + profanity String? + appeals hubLogChannelAndRole? + reports hubLogChannelAndRole? + networkAlerts hubLogChannelAndRole? + hub Hub @relation(fields: [hubId], references: [id]) + hubId String @unique @db.ObjectId @@index([id, hubId]) } diff --git a/src/commands/slash/Main/hub/blockwords.ts b/src/commands/slash/Main/hub/blockwords.ts index 6fbcfc8e9..205a71370 100644 --- a/src/commands/slash/Main/hub/blockwords.ts +++ b/src/commands/slash/Main/hub/blockwords.ts @@ -1,19 +1,22 @@ import HubCommand from '#main/commands/slash/Main/hub/index.js'; import { emojis } from '#main/config/Constants.js'; import { RegisterInteractionHandler } from '#main/decorators/Interaction.js'; +import { ACTION_LABELS, buildBlockWordsListEmbed } from '#main/utils/moderation/blockWords.js'; import { CustomID } from '#utils/CustomID.js'; import db from '#utils/Db.js'; import { isStaffOrHubMod } from '#utils/hub/utils.js'; import { t } from '#utils/Locale.js'; import { - buildBlockWordsListEmbed, + buildBlockWordsActionsSelect, + buildBWRuleEmbed, buildBlockWordsModal, - buildEditBlockedWordsBtn, + buildBlockedWordsBtns, sanitizeWords, -} from '#utils/moderation/blockedWords.js'; -import { Hub, MessageBlockList } from '@prisma/client'; +} from '#utils/moderation/blockWords.js'; +import { BlockWordAction, Hub, MessageBlockList } from '@prisma/client'; import { RepliableInteraction, + StringSelectMenuInteraction, type ButtonInteraction, type ChatInputCommandInteraction, type ModalSubmitInteraction, @@ -34,7 +37,6 @@ export default class BlockWordCommand extends HubCommand { switch (interaction.options.getSubcommand()) { case 'edit': - // TODO: add actions lul await this.handleEditSubcommand(interaction, hub); break; case 'list': @@ -48,7 +50,7 @@ export default class BlockWordCommand extends HubCommand { } } - @RegisterInteractionHandler('blockwordsButton', 'edit') + @RegisterInteractionHandler('blockwordsButton', 'editWords') async handleEditButtons(interaction: ButtonInteraction) { const customId = CustomID.parseCustomId(interaction.customId); const [hubId, ruleId] = customId.args; @@ -96,10 +98,17 @@ export default class BlockWordCommand extends HubCommand { return; } - await db.messageBlockList.create({ + const rule = await db.messageBlockList.create({ data: { hubId, name, createdBy: interaction.user.id, words: newWords }, }); - await interaction.editReply(`${emojis.yes} Rule added.`); + + const embed = buildBWRuleEmbed(rule); + const buttons = buildBlockedWordsBtns(hub.id, rule.id); + await interaction.editReply({ + content: `${emojis.yes} Rule added.`, + embeds: [embed], + components: [buttons], + }); } else if (newWords.length === 0) { await db.messageBlockList.delete({ where: { id: ruleId } }); @@ -111,19 +120,66 @@ export default class BlockWordCommand extends HubCommand { } } + @RegisterInteractionHandler('blockwordsButton', 'configActions') + async handleConfigureActions(interaction: ButtonInteraction) { + const customId = CustomID.parseCustomId(interaction.customId); + const [hubId, ruleId] = customId.args; + + const hub = await this.fetchHub({ id: hubId }); + if (!hub || !isStaffOrHubMod(interaction.user.id, hub)) { + const locale = await this.getLocale(interaction); + await this.replyEmbed(interaction, t('hub.notFound_mod', locale, { emoji: emojis.no }), { + ephemeral: true, + }); + return; + } + + const rule = hub.msgBlockList.find((r) => r.id === ruleId); + if (!rule) { + await interaction.reply({ content: 'Rule not found', ephemeral: true }); + return; + } + + const selectMenu = buildBlockWordsActionsSelect(hubId, ruleId, rule.actions || []); + await interaction.reply({ + content: `Configure actions for rule: ${rule.name}`, + components: [selectMenu], + ephemeral: true, + }); + } + + @RegisterInteractionHandler('blockwordsSelect', 'actions') + async handleActionSelection(interaction: StringSelectMenuInteraction) { + const customId = CustomID.parseCustomId(interaction.customId); + const ruleId = customId.args[1]; + const selectedActions = interaction.values as BlockWordAction[]; + + await db.messageBlockList.update({ + where: { id: ruleId }, + data: { actions: selectedActions }, + }); + + const actionLabels = selectedActions.map((action) => ACTION_LABELS[action]).join(', '); + await interaction.update({ + content: `✅ Actions updated for rule: ${actionLabels}`, + components: [], + }); + } + private async handleEditSubcommand( interaction: ChatInputCommandInteraction, hub: Hub & { msgBlockList: MessageBlockList[] }, ) { - const blockWords = hub.msgBlockList; + const ruleName = interaction.options.getString('rule', true); + const rule = hub.msgBlockList.find((r) => r.name === ruleName); - if (!blockWords.length) { + if (!rule) { await this.replyWithNotFound(interaction); return; } - const embed = buildBlockWordsListEmbed(blockWords); - const buttons = buildEditBlockedWordsBtn(hub.id, blockWords); + const embed = buildBWRuleEmbed(rule); + const buttons = buildBlockedWordsBtns(hub.id, rule.id); await interaction.reply({ embeds: [embed], components: [buttons] }); } @@ -152,7 +208,7 @@ export default class BlockWordCommand extends HubCommand { private async replyWithNotFound(interaction: RepliableInteraction) { await this.replyEmbed( interaction, - 'No block word rules are in this hub yet. Use `/hub blockwords add` to add some.', + 'No block word rules are in this hub yet or selected rule name is invalid. Use `/hub blockwords add` to add some or `/hub blockwords list` to list all created rules.', { ephemeral: true }, ); } diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index d4ea1e775..4b2e9d18e 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -1,6 +1,7 @@ import { ConnectionMode } from '#main/config/Constants.js'; import BaseEventListener from '#main/core/BaseEventListener.js'; import HubSettingsManager from '#main/managers/HubSettingsManager.js'; +import { checkBlockedWords } from '#main/utils/network/blockwordsRunner.js'; import { generateJumpButton as getJumpButton } from '#utils/ComponentUtils.js'; import { getConnectionHubId, getHubConnections } from '#utils/ConnectedListUtils.js'; import db from '#utils/Db.js'; @@ -20,7 +21,7 @@ import storeMessageData, { NetworkWebhookSendResult } from '#utils/network/store import type { BroadcastOpts, ReferredMsgData } from '#utils/network/Types.js'; import { censor } from '#utils/ProfanityUtils.js'; import { isHumanMessage, trimAndCensorBannedWebhookWords } from '#utils/Utils.js'; -import { connectedList, Hub } from '@prisma/client'; +import { connectedList, Hub, MessageBlockList } from '@prisma/client'; import { ActionRowBuilder, ButtonBuilder, @@ -34,7 +35,7 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> { readonly name = 'messageCreate'; async execute(message: Message) { - if (!this.isValidMessage(message)) return; + if (!message.inGuild() || !isHumanMessage(message)) return; const { connection, hubConnections } = await this.getConnectionAndHubConnections(message); if (!connection?.connected || !hubConnections) return; @@ -58,10 +59,6 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> { await this.processMessage(message, hub, hubConnections, settings, connection, attachmentURL); } - private isValidMessage(message: Message): message is Message { - return message.inGuild() && isHumanMessage(message); - } - private async getHub(hubId: string) { return await db.hub.findFirst({ where: { id: hubId }, @@ -71,7 +68,7 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> { private async processMessage( message: Message, - hub: Hub, + hub: Hub & { msgBlockList: MessageBlockList[] }, hubConnections: connectedList[], settings: HubSettingsManager, connection: connectedList, @@ -79,6 +76,9 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> { ) { message.channel.sendTyping().catch(() => null); + const { passed } = await checkBlockedWords(message, hub.msgBlockList); + if (!passed) return; + const referredMessage = await this.fetchReferredMessage(message); const referredMsgData = await getReferredMsgData(referredMessage); @@ -240,7 +240,6 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> { connection: connectedList, referredMsgData?: ReferredMsgData, ): WebhookMessageCreateOptions { - if (referredMsgData && connection.serverId === referredMsgData.dbReferrence?.guildId) { const { dbReferredAuthor, dbReferrence } = referredMsgData; const replyMention = `${getReplyMention(dbReferredAuthor)}`; @@ -277,10 +276,9 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> { const connectionHubId = await getConnectionHubId(message.channelId); if (!connectionHubId) return { connection: null, hubConnections: null }; - const hubConnections = await getHubConnections(connectionHubId); - let connection: connectedList | null = null; const filteredHubConnections: connectedList[] = []; + const hubConnections = await getHubConnections(connectionHubId); hubConnections?.forEach((conn) => { if (conn.channelId === message.channelId) connection = conn; diff --git a/src/managers/HubLogManager.ts b/src/managers/HubLogManager.ts index 7aa0d479d..a42a76ad1 100644 --- a/src/managers/HubLogManager.ts +++ b/src/managers/HubLogManager.ts @@ -8,9 +8,9 @@ import { HubLogConfig, Prisma } from '@prisma/client'; import { stripIndents } from 'common-tags'; import { ActionRowBuilder, roleMention, Snowflake, StringSelectMenuBuilder } from 'discord.js'; -export type RoleIdLogConfigs = 'appeals' | 'reports'; +export type RoleIdLogConfigs = 'appeals' | 'reports' | 'networkAlerts'; export type LogConfigTypes = keyof Omit, 'id'>; -export const logsWithRoleId = ['appeals', 'reports']; +export const logsWithRoleId = ['appeals', 'reports', 'networkAlerts']; const channelMention = (channelId: string | null | undefined) => channelId ? `<#${channelId}>` : emojis.no; diff --git a/src/types/locale.d.ts b/src/types/locale.d.ts index 3fc36e507..5b0cd955b 100644 --- a/src/types/locale.d.ts +++ b/src/types/locale.d.ts @@ -155,6 +155,8 @@ export type TranslationKeys = { 'hub.manage.logs.joinLeaves.description': never; 'hub.manage.logs.appeals.label': never; 'hub.manage.logs.appeals.description': never; + 'hub.manage.logs.networkAlerts.label': never; + 'hub.manage.logs.networkAlerts.description': never; 'report.modal.title': never; 'report.modal.other.label': never; 'report.modal.other.placeholder': never; @@ -187,6 +189,7 @@ export type TranslationKeys = { 'errors.unknown': 'emoji' | 'support_invite'; 'errors.notUsable': 'emoji'; 'errors.cooldown': 'emoji' | 'time'; + 'errors.serverNameInappropriate': 'emoji'; 'errors.banned': 'emoji' | 'reason' | 'support_invite'; 'misc.webhookNoLongerExists': 'emoji'; 'misc.noReason': never; diff --git a/src/utils/HubLogger/BlockWordAlert.ts b/src/utils/HubLogger/BlockWordAlert.ts new file mode 100644 index 000000000..b6e05fe8d --- /dev/null +++ b/src/utils/HubLogger/BlockWordAlert.ts @@ -0,0 +1,31 @@ +import { emojis } from '#main/config/Constants.js'; +import HubLogManager from '#main/managers/HubLogManager.js'; +import { sendLog } from '#main/utils/HubLogger/Default.js'; +import { ACTION_LABELS } 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 logManager = await HubLogManager.create(rule.hubId); + if (!logManager.config.networkAlerts) return; + + 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}\`\`\` + + -# Actions Taken: **${rule.actions.map((a) => ACTION_LABELS[a]).join(', ')}** + `, + ) + .setTimestamp(); + + await sendLog(message.client.cluster, logManager.config.networkAlerts.channelId, embed); +}; diff --git a/src/utils/moderation/blockedWords.ts b/src/utils/moderation/blockWords.ts similarity index 58% rename from src/utils/moderation/blockedWords.ts rename to src/utils/moderation/blockWords.ts index 26aa376a9..04d67e89f 100644 --- a/src/utils/moderation/blockedWords.ts +++ b/src/utils/moderation/blockWords.ts @@ -1,7 +1,8 @@ +// utils/moderation/blockedWords.ts import { emojis, numberEmojis } from '#main/config/Constants.js'; import { CustomID } from '#utils/CustomID.js'; import { InfoEmbed } from '#utils/EmbedUtils.js'; -import { MessageBlockList } from '@prisma/client'; +import { BlockWordAction, MessageBlockList } from '@prisma/client'; import { stripIndents } from 'common-tags'; import { codeBlock, @@ -11,8 +12,15 @@ import { ModalBuilder, TextInputBuilder, TextInputStyle, + StringSelectMenuBuilder, } from 'discord.js'; +export const ACTION_LABELS = { + [BlockWordAction.BLOCK_MESSAGE]: '🚫 Block Message', + [BlockWordAction.SEND_ALERT]: '🔔 Send Alert', + [BlockWordAction.BLACKLIST]: '⛔ Blacklist User/Server', +} as const; + export function createRegexFromWords(words: string | string[]) { if (Array.isArray(words)) return createRegexFromWords(words.join(',')); @@ -44,18 +52,31 @@ export const buildBlockWordsListEmbed = (rules: MessageBlockList[]) => name: `${numberEmojis[index + 1]}: ${name}`, value: codeBlock(words.replace(/\.\*/g, '*')), })), - ) - .setFooter({ text: 'Click the button below to add more words' }); + ); -export const buildEditBlockedWordsBtn = (hubId: string, rules: MessageBlockList[]) => +export const buildBWRuleEmbed = (rule: MessageBlockList) => { + const actions = rule.actions.map((a) => ACTION_LABELS[a]).join(', '); + return new InfoEmbed() + .removeTitle() + .setDescription( + stripIndents` + ### ${emojis.exclamation} Editing Rule: ${rule.name} + ${rule.words ? `**Blocked Words:**\n${codeBlock(rule.words.replace(/\.\*/g, '*'))}` : ''} + -# Configured Actions: **${actions.length > 0 ? actions : 'None. Configure using the button below.'}** + `, + ) + .setFooter({ text: 'Click the button below to edit' }); +}; +export const buildBlockedWordsBtns = (hubId: string, ruleId: string) => new ActionRowBuilder().addComponents( - rules.map(({ id, name }, index) => - new ButtonBuilder() - .setCustomId(new CustomID('blockwordsButton:edit', [hubId, id]).toString()) - .setLabel(`Edit ${name}`) - .setEmoji(numberEmojis[index + 1]) - .setStyle(ButtonStyle.Secondary), - ), + new ButtonBuilder() + .setCustomId(new CustomID('blockwordsButton:configActions', [hubId, ruleId]).toString()) + .setLabel('Configure Actions') + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId(new CustomID('blockwordsButton:editWords', [hubId, ruleId]).toString()) + .setLabel('Edit Words') + .setStyle(ButtonStyle.Secondary), ); export const buildBlockWordsModal = (hubId: string, opts?: { presetRule: MessageBlockList }) => { @@ -96,3 +117,23 @@ export const buildBlockWordsModal = (hubId: string, opts?: { presetRule: Message return modal; }; + +export const buildBlockWordsActionsSelect = ( + hubId: string, + ruleId: string, + currentActions: BlockWordAction[], +) => + new ActionRowBuilder().addComponents( + new StringSelectMenuBuilder() + .setCustomId(new CustomID('blockwordsSelect:actions', [hubId, ruleId]).toString()) + .setPlaceholder('Select actions for this rule') + .setMinValues(1) + .setMaxValues(Object.keys(BlockWordAction).length) + .setOptions( + Object.entries(ACTION_LABELS).map(([value, label]) => ({ + label, + value, + default: currentActions.includes(value as BlockWordAction), + })), + ), + ); diff --git a/src/utils/network/blockwordsRunner.ts b/src/utils/network/blockwordsRunner.ts new file mode 100644 index 000000000..6261a1abb --- /dev/null +++ b/src/utils/network/blockwordsRunner.ts @@ -0,0 +1,108 @@ +import BlacklistManager from '#main/managers/BlacklistManager.js'; +import UserInfractionManager from '#main/managers/InfractionManager/UserInfractionManager.js'; +import { logBlockwordAlert } from '#main/utils/HubLogger/BlockWordAlert.js'; +import Logger from '#main/utils/Logger.js'; +import { sendBlacklistNotif } from '#main/utils/moderation/blacklistUtils.js'; +import { createRegexFromWords } from '#main/utils/moderation/blockWords.js'; +import { BlockWordAction, MessageBlockList } from '@prisma/client'; +import { ActionRowBuilder, Awaitable, ButtonBuilder, Message } from 'discord.js'; + +// Interface for action handler results +interface ActionResult { + success: boolean; + shouldBlock: boolean; + components?: ActionRowBuilder[]; + message?: string; +} + +// Action handler type +type ActionHandler = (message: Message, rule: MessageBlockList) => Awaitable; + +// Map of action handlers +const actionHandlers: Record = { + [BlockWordAction.BLOCK_MESSAGE]: () => ({ + success: true, + shouldBlock: true, + message: 'Message blocked due to containing prohibited words.', + }), + + [BlockWordAction.SEND_ALERT]: async (message, rule) => { + // Send alert to moderators + await logBlockwordAlert(message, rule); + return { success: true, shouldBlock: false }; + }, + + [BlockWordAction.BLACKLIST]: async (message, rule) => { + const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24 hour blacklist + const reason = `Auto-blacklisted for using blocked words (Rule: ${rule.name})`; + const target = message.author; + const mod = message.client.user; + + const blacklistManager = new BlacklistManager(new UserInfractionManager(target.id)); + await blacklistManager.addBlacklist({ + hubId: rule.hubId, + reason, + expiresAt, + moderatorId: mod.id, + }); + + await blacklistManager.log(rule.hubId, message.client, { mod, reason, expiresAt }); + await sendBlacklistNotif('user', message.client, { + target, + hubId: rule.hubId, + expiresAt, + reason, + }).catch(() => null); + + return { + success: true, + shouldBlock: true, + message: 'You have been blacklisted for using prohibited words.', + }; + }, +}; + +export async function checkBlockedWords(message: Message, msgBlockList: MessageBlockList[]) { + if (msgBlockList.length === 0) return Promise.resolve({ passed: true }); + + for (const rule of msgBlockList) { + const regex = createRegexFromWords(rule.words); + if (regex.test(message.content)) { + let shouldBlock = false; + let blockReason: string | undefined; + + // Execute all configured actions for this rule + for (const action of rule.actions || []) { + const handler = actionHandlers[action]; + if (handler) { + try { + const result = await handler(message, rule); + if (result.success && result.shouldBlock) { + shouldBlock = true; + blockReason = result.message; + } + } + catch (error) { + Logger.error(`Failed to execute action ${action}:`, error); + } + } + } + + // If no specific blocking actions were taken but actions were configured, + // still block the message by default + if (rule.actions?.length && !shouldBlock) { + shouldBlock = true; + blockReason = `Your message contains blocked words from the rule: ${rule.name}`; + } + + if (shouldBlock) { + return { + passed: false, + reason: blockReason, + }; + } + } + } + + return { passed: true }; +} diff --git a/src/utils/network/runChecks.ts b/src/utils/network/runChecks.ts index f077fefa6..1541d45bf 100644 --- a/src/utils/network/runChecks.ts +++ b/src/utils/network/runChecks.ts @@ -8,7 +8,6 @@ import db from '#utils/Db.js'; import { isHubMod } from '#utils/hub/utils.js'; import logProfanity from '#utils/HubLogger/Profanity.js'; import { supportedLocaleCodes, t } from '#utils/Locale.js'; -import { createRegexFromWords } from '#utils/moderation/blockedWords.js'; import { sendWelcomeMsg } from '#utils/network/helpers.js'; import { check as checkProfanity } from '#utils/ProfanityUtils.js'; import { containsInviteLinks, replaceLinks } from '#utils/Utils.js'; @@ -43,7 +42,6 @@ const checks: CheckFunction[] = [ checkAttachments, checkNSFW, checkLinks, - checkBlockedWords, ]; const replyToMsg = async ( @@ -181,20 +179,6 @@ function checkProfanityAndSlurs(message: Message, { hub }: CheckFunctionOp return { passed: true }; } -function checkBlockedWords(message: Message, { hub }: CheckFunctionOpts): CheckResult { - if (hub.msgBlockList.length === 0) return { passed: true }; - const regex = createRegexFromWords(hub.msgBlockList.map((r) => r.words)); - if (regex.test(message.content)) { - logProfanity(hub.id, message.content, message.author, message.guild); - return { - passed: false, - reason: 'Your message contains blocked words.', - }; - } - - return { passed: true }; -} - function checkNewUser(message: Message, opts: CheckFunctionOpts): CheckResult { const sevenDaysAgo = Date.now() - 1000 * 60 * 60 * 24 * 7; if (message.author.createdTimestamp > sevenDaysAgo) {