MAATRIX GAMES / Блог / Бот-модератор с автобаном по ключевым словам

Бот-модератор с автобаном по ключевым словам

MAATRIX GAMES

Ручная модерация чата держится, пока на сервере 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-5Timeout1 час
6-8Timeout24 часа
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 как ожидаемое следствие своих действий, а не как произвол админов.