Skip to content
This repository was archived by the owner on Oct 9, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
46 changes: 27 additions & 19 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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])
}
Expand Down Expand Up @@ -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])
}
Expand Down
82 changes: 69 additions & 13 deletions src/commands/slash/Main/hub/blockwords.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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':
Expand All @@ -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;
Expand Down Expand Up @@ -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 } });
Expand All @@ -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] });
}

Expand Down Expand Up @@ -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 },
);
}
Expand Down
18 changes: 8 additions & 10 deletions src/events/messageCreate.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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,
Expand All @@ -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;
Expand All @@ -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<true> {
return message.inGuild() && isHumanMessage(message);
}

private async getHub(hubId: string) {
return await db.hub.findFirst({
where: { id: hubId },
Expand All @@ -71,14 +68,17 @@ export default class MessageCreate extends BaseEventListener<'messageCreate'> {

private async processMessage(
message: Message<true>,
hub: Hub,
hub: Hub & { msgBlockList: MessageBlockList[] },
hubConnections: connectedList[],
settings: HubSettingsManager,
connection: connectedList,
attachmentURL: string | undefined,
) {
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);

Expand Down Expand Up @@ -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)}`;
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/managers/HubLogManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Omit<HubLogConfig, 'hubId'>, 'id'>;
export const logsWithRoleId = ['appeals', 'reports'];
export const logsWithRoleId = ['appeals', 'reports', 'networkAlerts'];

const channelMention = (channelId: string | null | undefined) =>
channelId ? `<#${channelId}>` : emojis.no;
Expand Down
3 changes: 3 additions & 0 deletions src/types/locale.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
31 changes: 31 additions & 0 deletions src/utils/HubLogger/BlockWordAlert.ts
Original file line number Diff line number Diff line change
@@ -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<true>, 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);
};
Loading