Skip to content
This repository was archived by the owner on Oct 9, 2025. It is now read-only.

Commit 5dccb74

Browse files
committed
feat(hub join): check servername for swear words
1 parent c839aaa commit 5dccb74

File tree

2 files changed

+205
-58
lines changed

2 files changed

+205
-58
lines changed

src/services/HubJoinService.ts

Lines changed: 37 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -19,38 +19,37 @@ import BlacklistManager from '#src/managers/BlacklistManager.js';
1919
import HubManager from '#src/managers/HubManager.js';
2020
import { HubService } from '#src/services/HubService.js';
2121
import { type EmojiKeys, getEmoji } from '#src/utils/EmojiUtils.js';
22-
23-
import { stripIndents } from 'common-tags';
24-
import type {
25-
ChatInputCommandInteraction,
26-
GuildTextBasedChannel,
27-
MessageComponentInteraction,
28-
} from 'discord.js';
22+
import Context from '#src/core/CommandContext/Context.js';
2923
import type { TranslationKeys } from '#types/TranslationKeys.d.ts';
3024
import { createConnection } from '#utils/ConnectedListUtils.js';
3125
import db from '#utils/Db.js';
3226
import { type supportedLocaleCodes, t } from '#utils/Locale.js';
33-
import { check } from '#utils/ProfanityUtils.js';
3427
import { getOrCreateWebhook, getReplyMethod } from '#utils/Utils.js';
3528
import { logJoinToHub } from '#utils/hub/logger/JoinLeave.js';
3629
import { sendToHub } from '#utils/hub/utils.js';
37-
import Context from '#src/core/CommandContext/Context.js';
30+
import { stripIndents } from 'common-tags';
31+
import type {
32+
ChatInputCommandInteraction,
33+
GuildTextBasedChannel,
34+
MessageComponentInteraction,
35+
} from 'discord.js';
3836
// eslint-disable-next-line no-duplicate-imports
3937
import type { CachedContextType } from '#src/core/CommandContext/Context.js';
38+
import { checkRule } from '#src/utils/network/antiSwearChecks.js';
4039

4140
export class HubJoinService {
4241
private readonly interaction:
43-
| ChatInputCommandInteraction<'cached'>
44-
| MessageComponentInteraction<'cached'>
45-
| Context<CachedContextType>;
42+
| ChatInputCommandInteraction<'cached'>
43+
| MessageComponentInteraction<'cached'>
44+
| Context<CachedContextType>;
4645
private readonly locale: supportedLocaleCodes;
4746
private readonly hubService: HubService;
4847

4948
constructor(
5049
interaction:
51-
| ChatInputCommandInteraction<'cached'>
52-
| MessageComponentInteraction<'cached'>
53-
| Context<CachedContextType>,
50+
| ChatInputCommandInteraction<'cached'>
51+
| MessageComponentInteraction<'cached'>
52+
| Context<CachedContextType>,
5453
locale: supportedLocaleCodes,
5554
hubService: HubService = new HubService(),
5655
) {
@@ -74,19 +73,13 @@ export class HubJoinService {
7473
return await this.joinHub(channel, randomHub.name);
7574
}
7675

77-
async joinHub(
78-
channel: GuildTextBasedChannel,
79-
hubInviteOrName: string | undefined,
80-
) {
76+
async joinHub(channel: GuildTextBasedChannel, hubInviteOrName: string | undefined) {
8177
if (!this.interaction.deferred) {
8278
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
8379
// @ts-expect-error
8480
await this.interaction.deferReply({ flags: ['Ephemeral'] });
8581
}
8682

87-
const checksPassed = await this.runChecks(channel);
88-
if (!checksPassed) return false;
89-
9083
const hub = await this.fetchHub(hubInviteOrName);
9184
if (!hub) {
9285
await this.interaction.editReply({
@@ -97,10 +90,10 @@ export class HubJoinService {
9790
return false;
9891
}
9992

100-
if (
101-
(await this.isAlreadyInHub(channel, hub.id)) ||
102-
(await this.isBlacklisted(hub))
103-
) {
93+
const checksPassed = await this.runChecks(channel, hub);
94+
if (!checksPassed) return false;
95+
96+
if ((await this.isAlreadyInHub(channel, hub.id)) || (await this.isBlacklisted(hub))) {
10497
return false;
10598
}
10699

@@ -116,32 +109,27 @@ export class HubJoinService {
116109
hub: { connect: { id: hub.id } },
117110
connected: true,
118111
compact: true,
119-
profFilter: true,
120112
});
121113

122114
await this.sendSuccessMessages(hub, channel);
123115
return true;
124116
}
125117

126-
private async runChecks(channel: GuildTextBasedChannel) {
127-
if (
128-
!channel
129-
.permissionsFor(this.interaction.member)
130-
.has('ManageMessages', true)
131-
) {
118+
private async runChecks(channel: GuildTextBasedChannel, hub: HubManager) {
119+
if (!channel.permissionsFor(this.interaction.member).has('ManageMessages', true)) {
132120
await this.replyError('errors.missingPermissions', {
133121
permissions: 'Manage Messages',
134122
emoji: this.getEmoji('x_icon'),
135123
});
136124
return false;
137125
}
138126

139-
const { hasSlurs, hasProfanity } = check(this.interaction.guild.name);
140-
if (hasSlurs || hasProfanity) {
141-
await this.replyError('errors.serverNameInappropriate', {
142-
emoji: this.getEmoji('x_icon'),
143-
});
144-
return false;
127+
for (const rule of await hub.fetchAntiSwearRules()) {
128+
const match = checkRule(channel.guild.name, rule);
129+
if (match) {
130+
await this.replyError('errors.serverNameInappropriate', { emoji: this.getEmoji('x_icon') });
131+
return false;
132+
}
145133
}
146134

147135
return true;
@@ -184,14 +172,8 @@ export class HubJoinService {
184172
}
185173

186174
private async isBlacklisted(hub: HubManager) {
187-
const userBlManager = new BlacklistManager(
188-
'user',
189-
this.interaction.user.id,
190-
);
191-
const serverBlManager = new BlacklistManager(
192-
'server',
193-
this.interaction.guildId,
194-
);
175+
const userBlManager = new BlacklistManager('user', this.interaction.user.id);
176+
const serverBlManager = new BlacklistManager('server', this.interaction.guildId);
195177

196178
const userBlacklist = await userBlManager.fetchBlacklist(hub.id);
197179
const serverBlacklist = await serverBlManager.fetchBlacklist(hub.id);
@@ -219,10 +201,7 @@ export class HubJoinService {
219201
return webhook;
220202
}
221203

222-
private async sendSuccessMessages(
223-
hub: HubManager,
224-
channel: GuildTextBasedChannel,
225-
) {
204+
private async sendSuccessMessages(hub: HubManager, channel: GuildTextBasedChannel) {
226205
const replyData = {
227206
content: t('hub.join.success', this.locale, {
228207
channel: `${channel}`,
@@ -240,15 +219,15 @@ export class HubJoinService {
240219
await this.interaction[replyMethod](replyData);
241220

242221
const totalConnections =
243-
(await hub.connections.fetch())?.reduce(
244-
(total, c) => total + (c.data.connected ? 1 : 0),
245-
0,
246-
) ?? 0;
222+
(await hub.connections.fetch())?.reduce(
223+
(total, c) => total + (c.data.connected ? 1 : 0),
224+
0,
225+
) ?? 0;
247226

248227
const serverCountMessage =
249-
totalConnections === 0
250-
? 'There are no other servers connected to this hub yet. *cricket noises* 🦗'
251-
: `We now have ${totalConnections} servers in this hub! 🎉`;
228+
totalConnections === 0
229+
? 'There are no other servers connected to this hub yet. *cricket noises* 🦗'
230+
: `We now have ${totalConnections} servers in this hub! 🎉`;
252231

253232
// Announce to hub
254233
await sendToHub(hub.id, {
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/*
2+
* Copyright (C) 2025 InterChat
3+
*
4+
* InterChat is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU Affero General Public License as published
6+
* by the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* InterChat is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Affero General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Affero General Public License
15+
* along with InterChat. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
18+
import BlacklistManager from '#src/managers/BlacklistManager.js';
19+
20+
import { type BlockWord, BlockWordAction } from '@prisma/client';
21+
import type { ActionRowBuilder, Awaitable, ButtonBuilder, Message } from 'discord.js';
22+
import Logger from '#src/utils/Logger.js';
23+
import { logBlockwordAlert } from '#src/utils/hub/logger/BlockWordAlert.js';
24+
import { sendBlacklistNotif } from '#src/utils/moderation/blacklistUtils.js';
25+
import { createRegexFromWords } from '#utils/moderation/antiSwear.js';
26+
import type { CheckResult } from '#src/utils/network/runChecks.js';
27+
28+
// Interface for action handler results
29+
interface ActionResult {
30+
success: boolean;
31+
shouldBlock: boolean;
32+
components?: ActionRowBuilder<ButtonBuilder>[];
33+
message?: string;
34+
}
35+
36+
// Action handler type
37+
type ActionHandler = (
38+
message: Message<true>,
39+
rule: BlockWord,
40+
matches: string[],
41+
) => Awaitable<ActionResult>;
42+
43+
// Map of action handlers
44+
const actionHandlers: Record<BlockWordAction, ActionHandler> = {
45+
[BlockWordAction.BLOCK_MESSAGE]: () => ({
46+
success: true,
47+
shouldBlock: true,
48+
message: 'Message blocked due to containing prohibited words.',
49+
}),
50+
51+
[BlockWordAction.SEND_ALERT]: async (message, rule, matches) => {
52+
// Send alert to moderators
53+
await logBlockwordAlert(message, rule, matches);
54+
return { success: true, shouldBlock: false };
55+
},
56+
57+
[BlockWordAction.BLACKLIST]: async (message, rule) => {
58+
const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 minutes
59+
const reason = `Auto-blacklisted for using blocked words (Rule: ${rule.name})`;
60+
const target = message.author;
61+
const mod = message.client.user;
62+
63+
const blacklistManager = new BlacklistManager('user', target.id);
64+
await blacklistManager.addBlacklist({
65+
hubId: rule.hubId,
66+
reason,
67+
expiresAt,
68+
moderatorId: mod.id,
69+
});
70+
71+
await blacklistManager.log(rule.hubId, message.client, {
72+
mod,
73+
reason,
74+
expiresAt,
75+
});
76+
await sendBlacklistNotif('user', message.client, {
77+
target,
78+
hubId: rule.hubId,
79+
expiresAt,
80+
reason,
81+
}).catch(() => null);
82+
83+
return {
84+
success: true,
85+
shouldBlock: true,
86+
message: 'You have been blacklisted for using prohibited words.',
87+
};
88+
},
89+
};
90+
91+
interface ActionResult {
92+
success: boolean;
93+
shouldBlock: boolean;
94+
message?: string;
95+
}
96+
97+
interface BlockResult {
98+
shouldBlock: boolean;
99+
reason?: string;
100+
}
101+
102+
export async function checkBlockedWords(
103+
message: Message<true>,
104+
msgBlockList: BlockWord[],
105+
): Promise<CheckResult> {
106+
if (msgBlockList.length === 0) return { passed: true };
107+
108+
for (const rule of msgBlockList) {
109+
const { shouldBlock, reason } = await checkRuleAndExecuteAction(message, rule);
110+
if (shouldBlock) return { passed: false, reason };
111+
}
112+
113+
return { passed: true };
114+
}
115+
116+
async function executeAction(
117+
action: keyof typeof actionHandlers,
118+
message: Message<true>,
119+
rule: BlockWord,
120+
matches: RegExpMatchArray,
121+
): Promise<ActionResult> {
122+
const handler = actionHandlers[action];
123+
if (!handler) return { success: false, shouldBlock: false };
124+
125+
try {
126+
return await handler(message, rule, matches);
127+
}
128+
catch (error) {
129+
Logger.error(`Failed to execute action ${action}:`, error);
130+
return { success: false, shouldBlock: false };
131+
}
132+
}
133+
134+
async function processActions(
135+
message: Message<true>,
136+
triggeredRule: BlockWord,
137+
matches: RegExpMatchArray,
138+
): Promise<BlockResult> {
139+
if (!triggeredRule.actions.length) return { shouldBlock: false };
140+
141+
for (const actionToTake of triggeredRule.actions) {
142+
const result = await executeAction(actionToTake, message, triggeredRule, matches);
143+
if (result.success && result.shouldBlock) {
144+
return {
145+
shouldBlock: true,
146+
reason: result.message,
147+
};
148+
}
149+
}
150+
151+
return { shouldBlock: false };
152+
}
153+
154+
async function checkRuleAndExecuteAction(
155+
message: Message<true>,
156+
rule: BlockWord,
157+
): Promise<BlockResult> {
158+
const matches = checkRule(message.content, rule);
159+
if (!matches) return { shouldBlock: false };
160+
161+
return await processActions(message, rule, matches);
162+
}
163+
164+
export function checkRule(content: string, rule: BlockWord) {
165+
const regex = createRegexFromWords(rule.words);
166+
const matches = content.match(regex);
167+
return matches;
168+
}

0 commit comments

Comments
 (0)