Tady

Публичный Discord-бот для сообществ: 2500+ серверов и 260 000+ пользователей в пике. Собственный менеджер шардов на PHP, Redis для активного состояния, интерфейс на двух языках.

Период
август 2021 г. — н. в.
Статус
Активен
Моя роль
Единственный разработчик: замысел, архитектура, серверная логика, масштабирование и дальнейшее развитие.
Баннер Tady: мультиязычный Discord-бот — утилиты, розыгрыши, модерация

Стек

  • PHP
  • DiscordPHP
  • MySQL
  • Redis
  • JavaScript
  • discord.js
  • Node.js

Задача

Сообществам, в которых я общался, регулярно не хватало одних и тех же функций: делить участников на команды случайным образом, создавать каналы-считалки, проводить розыгрыши на русском языке с нужными условиями и удобнее модерировать сервер. Существующие боты закрывали только отдельные сценарии, а собрать весь нужный набор функций даже из нескольких решений не получалось. Поэтому Tady начинался как приватный бот для нескольких Discord-серверов.

Решение

Когда стало понятно, что те же сценарии нужны и другим сообществам, бот стал публичным. Сотню серверов он набрал быстро, и с этим порогом пришла обязательная верификация Discord. Отдельным процессом шло ревью на привилегированные интенты, без которых часть команд не работает. По мере роста я адаптировал архитектуру под масштаб: шардирование, Redis для активного состояния и мультиязычный интерфейс.

Архитектура

Шардирование и состояние TadyСам по себе шард — это отдельное соединение с Gateway: гильдии закрепляются за ним формулой от их ID, и один шард поддерживает максимум 2500 серверов. В Tady каждый шард запускается в отдельном PHP-процессе: DiscordPHP не предоставляет менеджер процессов, поэтому управление их запуском, контролем и перезапуском реализовано в проекте. Состояние при этом общее: Redis держит идущие розыгрыши и таймеры, MySQL остаётся источником истины и переживает перезапуск.у каждого — своё соединениеновый розыгрыш — сразу в обапри стартеDiscord Gatewayшард — это соединение с Gateway, а не процессгильдия → шард: (guild_id >> 22) % num_shardsодин шард — максимум 2500 серверов; при 2500+ шардирование обязательноTady — PHP / DiscordPHPСвой менеджер шардовзапуск, контроль, перезапускShard 0процессShard 1процесс…процессShard NпроцессОбщее состояниеRedisидущие розыгрыши и таймерыMySQLисточник истины, переживает перезапуск
Сам по себе шард — это отдельное соединение с Gateway: гильдии закрепляются за ним формулой от их ID, и один шард поддерживает максимум 2500 серверов. В Tady каждый шард запускается в отдельном PHP-процессе: DiscordPHP не предоставляет менеджер процессов, поэтому управление их запуском, контролем и перезапуском реализовано в проекте. Состояние при этом общее: Redis держит идущие розыгрыши и таймеры, MySQL остаётся источником истины и переживает перезапуск.

Ключевые технические решения

  • Менеджер шардов: подключение к Discord разделено на несколько Gateway-соединений, и каждое работает в отдельном PHP-процессе. DiscordPHP принимает номер шарда, но процессами не управляет, поэтому в проекте реализован собственный менеджер их запуска, контроля и перезапуска.
  • Redis как слой активного состояния, а база — как источник истины: при перезапуске бот поднимает состояние из базы, а новые розыгрыши пишет сразу в оба места.
  • Язык берётся из настроек Discord самого пользователя, английский — запасной вариант: сервер с русскоязычной аудиторией не заставляет никого переключаться вручную.
  • Обработка ошибок различает источник проблемы: не хватает прав у пользователя или у бота; название права показывается на языке того, кто вызвал команду.
  • Привилегированные интенты сокращены до необходимого: на верификации запрашивались все три, но Presence больше не нужен, и в актуальном ревью остаются только Members и Message Content.

Масштаб и шардирование

При росте до 2500+ серверов проект перешёл на обязательное для такого масштаба шардирование Discord.

Шард в Discord — это отдельное соединение с Gateway. Гильдии распределяются между шардами по формуле от их ID, при этом один шард поддерживает максимум 2500 серверов. DiscordPHP поддерживает параметры шардирования, но не предоставляет готового менеджера shard-процессов, поэтому для Tady был реализован собственный: каждый шард запускался в отдельном PHP-процессе, а запуск, контроль и перезапуск были автоматизированы. Состояние между процессами оставалось общим: Redis хранил активные розыгрыши и таймеры, а MySQL служил источником истины и сохранял постоянное состояние. При перезапуске shard-процесса активное состояние не терялось, а восстанавливалось из MySQL в Redis: розыгрыши, LFG, считалки и другие сохраняемые сценарии.

В ходе развития проекта была реализована отдельная версия на Node.js и discord.js. Позже развитие этой ветки было остановлено, а текущий production остался на PHP и DiscordPHP.

Мультиязычность

Бот поддерживает русский и английский языки. Ответы, подписи кнопок и справочные сообщения автоматически подстраиваются под язык, выбранный пользователем в интерфейсе Discord. Если выбранный язык не поддерживается, используется английский вариант по умолчанию.

Мультиязычность
Один и тот же /help на русском и английском: категории, список команд и счётчики следуют языку, выбранному пользователем в Discord.

Обработка ошибок

Бот различает причины отказа и отвечает на каждую по-своему. Если не хватает права, он проверяет, не хватает ли его пользователю или самому боту, и называет право на языке того, кто вызвал команду. Отдельно обрабатываются случаи, не связанные с правами: роль участника выше, у администратора иммунитет к муту. Каждый ответ называет причину и, где это возможно, следующий шаг, а если разобраться не вышло, из сообщения можно сразу перейти на сервер поддержки.

Обработка ошибок: четыре вида отказа
Четыре вида отказа: права не хватает пользователю; права не хватает боту — с подсказкой включить его и повторить; роль участника выше роли бота; и иммунитет администратора к муту. Ответ видит только тот, кто вызвал команду.

Формат длительности

Длительность задаётся текстом, без ручного перевода в секунды или минуты. Например, запись 1д 3ч 12м 10с корректно интерпретируется как 1 день, 3 часа, 12 минут и 10 секунд. Этот формат работает во всех командах, где нужно указать срок.

Формат длительности
Справка по /mute: синтаксис команды, формат длительности «1д 3ч 12м 10с» и два готовых примера. Ответ видит только тот, кто вызвал команду.

Розыгрыши

Команда /giveaway запускает розыгрыш, который сам завершается по таймеру. Продолжительность задаётся в том же формате, что и в других командах. После запуска бот отслеживает оставшееся время, автоматически подводит итоги и случайным образом выбирает одного или нескольких победителей из числа участников. Автор розыгрыша может просматривать свои активные розыгрыши, завершать их досрочно и при необходимости перевыбирать победителей.

Розыгрыши
Розыгрыш и его итоги: бот публикует приз, спонсора, число победителей и срок, а по таймеру подводит итог: четыре победителя и 3237 участников. Счётчик реакций показывает 3286, потому что кадр снят месяцем позже и реакции продолжали набегать. Итог бот фиксирует на момент подведения и со счётчика не читает: там всегда лишняя реакция его самого.

Помощь и подсказки

У каждой команды есть краткое описание, инструкция и примеры: посмотреть их можно через /help (список команд) или /help <название команды> (подробности по конкретной). Такой ответ виден только тому, кто его запросил, и не засоряет канал.

Помощь и подсказки
/help перечисляет команды по категориям, а /help <команда> раскрывает одну. Здесь giveaway с подкомандами create, list, delete, reroll и примерами вызова.

Сервер поддержки

У бота есть сервер поддержки, где пользователи получают помощь, находят ответы на частые вопросы и следят за новостями проекта. Бот никогда не делает рассылки в личных сообщениях или на серверах пользователей.

Сервер поддержки
Канал новостей сервера поддержки: каждое объявление выходит сразу на двух языках, а в боковой панели держатся живые счётчики серверов, пользователей и участников.

Утилиты и развлечения

Помимо основных сценариев у Tady есть небольшие команды для повседневной жизни сообщества: /teams делит участников на команды случайным образом, /translit переводит кириллицу в латиницу для никнеймов, /info и /avatar показывают данные аккаунта, а /counter создаёт каналы-считалки. Отдельно стоит /lfg, сбор на игру: бот публикует запись с местами и временем, участники присоединяются кнопкой, и когда группа набрана, он объявляет об этом сам.

Утилиты и развлечения
Пять команд: /teams делит участников на команды, /info показывает даты аккаунта и входа на сервер, /avatar достаёт аватарку, /lfg собирает группу на игру с местами, таймером и кнопками, /translit переводит кириллицу в латиницу.

Результаты

  • Прошёл верификацию Discord и ревью на привилегированные интенты, без которых команды не работают на большом числе серверов.
  • Вырос из приватного бота для нескольких сообществ в публичный проект, который работает и поддерживается до сих пор.
  • Шардирование и общее состояние позволили масштабировать бота без привязки всей нагрузки к одному Gateway-соединению или процессу.

Метрики

Discord-серверов в пике
2500+
пользователей в пике
260 000+

Факты

  • Из-за бага в системе тикетов Discord верификация заняла 4 месяца, и всё это время платформа не давала пользователям приглашать бота на свои серверы.