Enhance AutoMod functionality with default word list and improved filtering

- Introduced a default blocked words list for the WORD_FILTER, including common profanity in both English and German.
- Updated the word matching logic to utilize Unicode word boundaries, reducing false positives.
- Implemented a button in the WebUI to load the default word list, enhancing user experience.
- Added functionality to seed default word filters only when necessary, preventing overwriting user-defined lists.
- Improved localization for new features related to the word list in both English and German.
- Documented changes in phase tracking to reflect the new AutoMod enhancements.
This commit is contained in:
TheOnlyMace
2026-07-25 17:42:06 +02:00
parent 2058713e03
commit 03bb406d82
9 changed files with 305 additions and 48 deletions

View File

@@ -1,6 +1,7 @@
import type { Redis } from 'ioredis';
import {
capsRatio,
contentMatchesBlockedWord,
countEmojis,
extractUrls,
hasZalgo,
@@ -124,15 +125,14 @@ function checkExternalLink(content: string, threshold: AutoModThreshold): Filter
}
function checkWordFilter(content: string, wordList: AutoModWordList): FilterResult | null {
const lower = content.toLowerCase();
for (const word of wordList.words) {
if (word.length > 0 && lower.includes(word.toLowerCase())) {
if (contentMatchesBlockedWord(content, word)) {
return { matched: true, ruleType: 'WORD_FILTER', reason: 'Blocked word' };
}
}
for (const pattern of wordList.regexPatterns) {
try {
const regex = new RegExp(pattern, 'i');
const regex = new RegExp(pattern, 'iu');
if (regex.test(content)) {
return { matched: true, ruleType: 'WORD_FILTER', reason: 'Blocked pattern' };
}

View File

@@ -1,6 +1,8 @@
import type { PrismaClient } from '@prisma/client';
import type { Prisma, PrismaClient } from '@prisma/client';
import {
DEFAULT_AUTOMOD_RULES,
defaultAutoModWordList,
shouldSeedDefaultBlockedWords,
type AutoModExceptions,
type AutoModThreshold,
type AutoModWordList
@@ -16,6 +18,26 @@ export async function getAutoModConfig(prisma: PrismaClient, guildId: string) {
return config;
}
async function seedDefaultWordFilterIfNeeded(prisma: PrismaClient, guildId: string) {
const rule = await prisma.autoModRule.findUnique({
where: { guildId_ruleType: { guildId, ruleType: 'WORD_FILTER' } },
select: { id: true, wordList: true }
});
if (!rule || !shouldSeedDefaultBlockedWords(rule.wordList)) {
return;
}
const existing = parseWordList(rule.wordList);
await prisma.autoModRule.update({
where: { id: rule.id },
data: {
wordList: {
...defaultAutoModWordList(),
regexPatterns: existing.regexPatterns
} as Prisma.InputJsonValue
}
});
}
export async function ensureDefaultAutoModRules(prisma: PrismaClient, guildId: string) {
await ensureGuild(prisma, guildId);
const existing = await prisma.autoModRule.findMany({
@@ -24,19 +46,19 @@ export async function ensureDefaultAutoModRules(prisma: PrismaClient, guildId: s
});
const have = new Set(existing.map((row) => row.ruleType));
const missing = DEFAULT_AUTOMOD_RULES.filter((rule) => !have.has(rule.ruleType));
if (missing.length === 0) {
return;
if (missing.length > 0) {
await prisma.autoModRule.createMany({
data: missing.map((rule) => ({
guildId,
ruleType: rule.ruleType,
action: rule.action,
threshold: rule.threshold ?? undefined,
wordList: rule.wordList ?? undefined,
enabled: ['SPAM', 'MASS_MENTION', 'INVITE_LINK', 'PHISHING'].includes(rule.ruleType)
}))
});
}
await prisma.autoModRule.createMany({
data: missing.map((rule) => ({
guildId,
ruleType: rule.ruleType,
action: rule.action,
threshold: rule.threshold ?? undefined,
wordList: rule.wordList ?? undefined,
enabled: ['SPAM', 'MASS_MENTION', 'INVITE_LINK', 'PHISHING'].includes(rule.ruleType)
}))
});
await seedDefaultWordFilterIfNeeded(prisma, guildId);
}
export async function getEnabledAutoModRules(prisma: PrismaClient, guildId: string) {
@@ -71,7 +93,8 @@ export function parseWordList(raw: unknown): AutoModWordList {
const obj = raw as Record<string, unknown>;
return {
words: Array.isArray(obj.words) ? obj.words.map(String) : [],
regexPatterns: Array.isArray(obj.regexPatterns) ? obj.regexPatterns.map(String) : []
regexPatterns: Array.isArray(obj.regexPatterns) ? obj.regexPatterns.map(String) : [],
...(obj.userCleared === true ? { userCleared: true } : {})
};
}