Бот-модератор с автобаном по ключевым словам
Ручная модерация чата держится, пока на сервере 30-50 человек — дальше поток сообщений растёт быстрее, чем ваша готовность сидеть в Discord круглыми сутками. Ключевые слова — самый прямой способ автоматизировать первую линию защиты: бот сам вычищает мат, расистские оскорбления и типовые триггеры конфликтов, а вам остаётся разбирать спорные случаи и жалобы на несправедливый бан. Разберём, как собрать список стоп-слов, довести его до автоматического бана по накоплению нарушений и не отстрелить себе ногу ложными срабатываниями.
Содержание
Двухуровневая система: встроенный AutoMod плюс счётчик нарушений
Прежде чем писать код, стоит понять, что Discord уже умеет часть работы бесплатно. В Server Settings → Safety Setup → AutoMod можно завести правило Custom Keyword — до нескольких десятков правил на сервер, в каждом до 1000 слов и фраз, с поддержкой wildcard (*ident* поймает и «identifier», и «coincidence»). Реакция мгновенная, без задержки на ответ бота, и не расходует ничей аптайм — это первый рубеж.
Проблема встроенного AutoMod в другом: он реагирует на каждое сообщение изолированно. Правило «заблокировать + timeout на 10 минут» одинаково срабатывает и на первое нарушение новичка, который не в курсе правил, и на пятое нарушение тролля, который специально проверяет границы. Автобан за накопление — это уже не про фильтр текста, а про счётчик, и его AutoMod из коробки не делает.
Поэтому рабочая связка выглядит так: AutoMod ловит явный мусор мгновенно на уровне Discord, а кастомный бот держит свою базу нарушений по каждому пользователю и эскалирует — warn → mute → kick → ban — когда счётчик переваливает пороги. Ниже — как собрать вторую часть, раз первую Discord уже сделал за вас.
Список ключевых слов: категории и уровни строгости
Единый список «плохих слов» — плохая идея с первого дня: мат и откровенные оскорбления заслуживают мгновенного жёсткого действия, а спорные слова («нуб», «даун» как жаргонное оскорбление, ссылки на сторонние Discord-серверы) — просто предупреждения. Разделите список на категории с разным весом:
{
"severe": {
"weight": 3,
"action": "timeout",
"words": ["слово1", "слово2", "расистский_термин"]
},
"medium": {
"weight": 2,
"action": "delete",
"words": ["оскорбление1", "оскорбление2"]
},
"mild": {
"weight": 1,
"action": "warn",
"words": ["спам-фраза1", "реф-ссылка-паттерн"]
}
}
Веса складываются в общий счётчик нарушений пользователя, а не работают как отдельные независимые правила — это и даёт автобан «по накоплению», а не по одному слову. Практика, которая экономит нервы: заведите список в отдельном приватном канале или гугл-таблице, доступной только старшим модераторам, а не прямо в коде бота — тогда добавить слово может любой модератор без деплоя, а не только тот, кто трогает репозиторий.
Отдельно вынесите whitelist-исключения — фразы, которые формально содержат стоп-слово, но контекстно безобидны (например, часть игрового термина или ник, который у кого-то реально такой). О самой частой ловушке здесь — ниже, в разделе про ложные срабатывания.
Нужен игровой сервер?
MAATRIX GAMES — 45+ игр, деплой за минуты, DDoS-защита, автобэкапы. Тарифы от 399 ₽/мес.
Создать серверЛогика эскалации: от warn до автобана
Ядро системы — таблица с историей нарушений и функция, которая решает действие по накопленному весу за скользящее окно времени (обычно 24-72 часа, чтобы старые нарушения не топили пользователя вечно). На discord.js с SQLite (better-sqlite3) это выглядит примерно так:
const db = require('better-sqlite3')('moderation.db');
db.exec(`CREATE TABLE IF NOT EXISTS violations (
user_id TEXT, guild_id TEXT, weight INTEGER, ts INTEGER
)`);
function addViolation(userId, guildId, weight) {
db.prepare('INSERT INTO violations VALUES (?, ?, ?, ?)')
.run(userId, guildId, weight, Date.now());
const windowMs = 48 * 60 * 60 * 1000;
const total = db.prepare(
`SELECT SUM(weight) as s FROM violations
WHERE user_id = ? AND guild_id = ? AND ts > ?`
).get(userId, guildId, Date.now() - windowMs);
return total.s || 0;
}
Пороги эскалации задаются отдельно от весов слов — так проще их подкручивать без пересборки списка:
| Накопленный вес | Действие | Длительность |
|---|---|---|
| 1-2 | Предупреждение в ЛС + запись в лог | — |
| 3-5 | Timeout | 1 час |
| 6-8 | Timeout | 24 часа |
| 9+ | Kick | — |
| Повторно после kick в течение 30 дней | Ban | — |
Автобан сразу «в лоб» на первое сообщение — почти всегда ошибка: даже точный список слов время от времени ловит то, что не должен был. Пусть до реального ban доходит только пользователь, который уже получил timeout и продолжил — это естественный человеческий предохранитель против единичного ложного срабатывания.
if (total >= 9) {
await member.kick('Автомодерация: накоплено нарушений — ' + total);
} else if (total >= 6) {
await member.timeout(24 * 60 * 60 * 1000, 'Автомодерация: повторные нарушения');
} else if (total >= 3) {
await member.timeout(60 * 60 * 1000, 'Автомодерация: нарушение правил чата');
} else {
await member.send('Предупреждение: сообщение нарушает правила сервера.');
}
Если у вас уже есть бот, который держит связь с игровым сервером через RCON — например, для бана нарушителя одновременно в Discord и в игре — логичнее не городить второго бота, а дописать этот модуль в него же, как в статье про RCON-бота для управления сервером.
Нормализация текста: как ловят обход фильтра
Голый message.content.includes(word) пробивается за пять минут — игроки быстро находят обходы. Прежде чем сравнивать текст со списком, его стоит нормализовать:
function normalize(text) {
return text
.toLowerCase()
.normalize('NFKD') // разложить юникод-варианты
.replace(/[̀-ͯ]/g, '') // убрать диакритику
.replace(/[0з3]/g, 'о') // типовые leetspeak-замены
.replace(/[1|!]/g, 'и')
.replace(/[^\p{L}\p{N}\s]/gu, '') // убрать спецсимволы-разделители
.replace(/(.)\1{2,}/g, '$1$1') // сжать повторы: "сууукааа" → "суука"
.replace(/\s+/g, ' ');
}
Даже эта простая нормализация закрывает большинство бытовых обходов: расстановку точек и звёздочек между буквами, задвоение символов, кириллицу вперемешку с похожей латиницей (омоглифы вроде «а» латинской вместо «а» кириллической — частый трюк, который на глаз незаметен). Полностью непробиваемого фильтра не бывает — упорный тролль всегда найдёт способ написать слово так, что человек поймёт, а regex нет, — но нормализация закрывает 90% случаев без фанатизма в правилах.
Для самого сравнения используйте границы слова, а не голый substring — иначе ловите классическую проблему Сканторпа (Scunthorpe problem): английский город с этим названием годами блокировался фильтрами мата из-за подстроки внутри. С русским текстом это тоже актуально: без \b фильтр на «хер» заблокирует «херсон» и «эксперимент».
function containsBadWord(text, word) {
const escaped = word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const re = new RegExp(`(?<![а-яa-z])${escaped}(?![а-яa-z])`, 'iu');
return re.test(text);
}
Ложные срабатывания: как не забанить невиновного
Три типовых источника ложных банов, которые стоит продумать заранее, а не ловить постфактум на жалобе игрока:
- Substring-совпадения без границ слова — разобрано выше, но стоит проверять список вручную: прогоните каждое стоп-слово через поиск по словарю частотных русских слов и убедитесь, что оно не является частью безобидного слова.
- Никнеймы и игровые термины — если в списке есть общеупотребимое в вашей игре слово (например, название фракции или предмета, которое случайно совпадает с чем-то грубым на другом языке), заведите под него явное исключение вместо того, чтобы убирать слово из списка целиком.
- Цитирование и пересказ — игрок описывает, что ему написал тролль в личку, дословно цитируя оскорбление, чтобы пожаловаться модератору — фильтр банит жалобщика. Разумный компромисс: в канале жалоб/тикетов автомодерацию по ключевым словам отключать вовсе, полагаясь на ручной разбор — про сам процесс работы с жалобами есть отдельная статья про работу с жалобами игроков.
- Контекст естественного языка — слово «убить» в фразе «эти гриферы меня чуть не убили» и в реальной угрозе — разный уровень серьёзности, который простой список слов не различает. Для таких пограничных слов ставьте только
warn, никогда сразуtimeoutилиkick.
Практический тест перед тем как включать фильтр на прод: прогоните список последних 500-1000 сообщений из логов чата (если есть) через новый набор правил в dry-run режиме — бот считает, что бы он забанил, но не банит, — и вручную просмотрите совпадения. Это полчаса работы, которые экономят десятки испорченных отношений с реальными игроками.
Лог-канал, аудит и апелляции
Каждое автоматическое действие обязано падать в отдельный лог-канал, видимый только модераторам — без этого разбор жалоб «меня забанили ни за что» превращается в гадание:
await logChannel.send({
embeds: [{
title: 'Автомодерация: действие применено',
fields: [
{ name: 'Пользователь', value: `<@${userId}>`, inline: true },
{ name: 'Действие', value: action, inline: true },
{ name: 'Накоплено', value: String(total), inline: true },
{ name: 'Сообщение', value: originalContent.slice(0, 500) },
{ name: 'Совпавшее слово', value: matchedWord }
],
timestamp: new Date().toISOString()
}]
});
Обязательно логируйте не только факт бана, но и что именно сработало и исходный текст сообщения — без этого модератор не сможет быстро отличить обоснованное действие от ложного срабатывания при разборе апелляции. Заведите отдельную команду или тикет-форму для оспаривания автобана — часть жалоб будет закрываться за минуту простым «да, это баг фильтра, разбаньте», и это нормальная цена автоматизации, если процесс апелляции быстрый. Если вы уже выстраиваете команду модераторов под растущий поток жалоб, разделение прав и обязанностей разобрано в статье про набор команды администраторов.
Нужен игровой сервер?
MAATRIX GAMES — 45+ игр, деплой за минуты, DDoS-защита, автобэкапы. Тарифы от 399 ₽/мес.
Создать серверЧастые вопросы
Сколько слов должно быть в списке, чтобы фильтр реально работал?
Универсального числа нет — рабочие списки для русскоязычных комьюнити обычно начинаются от полусотни слов и фраз с учётом словоформ и вырастают до нескольких сотен по мере обнаружения новых обходов. Важнее не количество, а регулярный пересмотр: раз в 1-2 недели просматривайте лог сработавших правил и добавляйте то, что реально всплывает.
Стоит ли банить сразу за одно тяжёлое слово без эскалации?
Для явных откровенных оскорблений и разжигания розни — да, разумно давать сразу timeout или kick без прогрева через warn, это и заложено в веса severe-категории выше. Но полный бан с первого раза лучше не выдавать автоматически вообще — оставьте его для повторных нарушителей или для ручного решения модератора.
Можно ли обойтись только встроенным AutoMod без своего бота?
Для небольшого сервера — вполне: заведите Custom Keyword правило с списком слов и timeout-действием, этого хватает на первое время. Кастомный счётчик нарушений нужен, когда вы хотите разную реакцию на первое и пятое нарушение, а не одинаковый timeout каждый раз — сравнение других готовых вариантов (MEE6, Dyno) и встроенного AutoMod подробнее разобрано в статье про Discord-бота для модерации чата.
Как часто игроки находят новые способы обойти фильтр слов?
Регулярно, особенно на живом активном сервере — это нормальная динамика, а не признак плохого списка. Держите канал для репортов от модераторов «вот это прошло, а не должно было» и обновляйте список короткими итерациями, а не раз в полгода одним большим патчем.
Нужно ли уведомлять игроков о том, что чат мониторится автомодерацией?
Да, и это не только вежливость — упомяните это в правилах сервера явно. Прозрачность снижает число «за что» в тикетах: игрок, который знает про автобан по ключевым словам, воспринимает timeout как ожидаемое следствие своих действий, а не как произвол админов.