README
¶
zavtrachat_bot
Русский | English
Telegram-бот для одного конкретного чата на Go. Работает через Bot API (long polling или webhook, поддерживается self-hosted Bot API server), дополнительно использует MTProto-клиент для нескольких команд. Состояние — в PostgreSQL.
- 99 команд, зарегистрированных в
main.go(часть — алиасы одной функции) - 112 исходников
.go, 291 тест в 49 файлах - GitHub Actions: сборка + деплой на self-hosted раннере и публикация образа в
ghcr.io - Лицензия — MIT
- С 16.10.2026 разработку ведёт ИИ — модели локального стека Strata (
swift-1.5-iq3_xxs,qwen3.8-flash-next-iq3_xxsи др., подробности — в разделе Разработка)
Дни рождения
Даты хранятся в PostgreSQL, по Telegram ID пользователя. При первом запуске импортируются
90 записей из «Календаря Завтрачата» (assets/birthdays.json): только день и месяц,
без возраста; две завершенные серии и годовщина подкаста исключены. Запись 9 февраля
без RRULE также импортируется как день рождения. Удаленные даты не возвращаются при перезапуске.
Импортированные ники связываются с ID из базы пользователей; неизвестные сохраняются до
появления пользователя в базе. Привязанный ID сохраняется при смене ника.
/addbirthday ДД.ММ— добавить свой день рождения. Например,/addbirthday 23.06. Если дата уже записана, она не заменяется: сначала/delbirthday, затем добавление./delbirthday— удалить свой день рождения./whenyourbirthday @username— узнать день рождения участника./birthdays— десять ближайших дат, включая сегодняшние, с переходом через Новый год./testbirthday— публично поздравить @nakxwest в основном чате. Только админам Telegram, настроенным админам бота и системному администратору; не расходует ежегодное поздравление.
Ответы команд приватны для вызвавшего пользователя; они также работают в личке. Ежедневно с 10:00 МСК бот проверяет членство именинника в основном чате. Участники, администраторы и замьюченные участники получают открытку; вышедшие и забаненные — нет. Если бот перезапущен после 10:00, он догоняет только текущий день. Ошибка API повторяется на следующей минуте; неподтвержденное членство не позволяет отправить открытку. 29 февраля отмечается 28 февраля в невисокосные годы.
Карточка: один из десяти случайных праздничных фонов, текущая аватарка (или инициалы, если Telegram не отдает фото), имя профиля и «с днем рождения!», блестки. В подписи — кликабельный @ник или имя, если ника нет. Фоны встроены в бинарник, отдельный сервер изображений не нужен. Запись доставки и блокировка строки защищают от повторов после обычного перезапуска и при одновременной работе экземпляров. Редкий сбой строго между успешным ответом Telegram и фиксацией транзакции может привести к повтору: Bot API не поддерживает идемпотентную отправку сообщений.
Команды
Справка
/zbhelp— команды для всех участников с описаниями, примерами и алиасами./zbadminhelp— административные команды; доступ администраторам/владельцу основного чата Telegram, настроенным администраторам, модераторам и системному администратору.
В группе обе команды и их ответы эфемерные: справку и отказ в доступе видит только
вызвавший пользователь. В личке ответ обычный. При ошибке скрытой доставки публичного
ответа нет. /zbhelp находится в меню группы и лички, /zbadminhelp — в меню администраторов
основного чата (в личке и для настроенных модераторов доступна по имени).
Справка разделена по фактическим проверкам прав; команды редактирования/удаления своих
гетов доступны участникам. Права существующих команд не меняются.
Модерация
| Команда | Что делает |
|---|---|
/ban, /unban |
бан / разбан, опционально время в секундах (/ban @user 3600) |
/mute, /unmute |
заглушить / разглушить, опционально время в секундах |
/muted |
кто сейчас замьючен: причина, инициатор, остаток времени; скрытый ответ вызвавшему админу |
/kick |
кик (бан + разбан, чтобы можно было вернуться по ссылке) |
/warn, /mywarns |
предупреждение / сколько у меня предупреждений |
/remove |
удалить сообщение, ответом на которое послана команда |
/debug, /debuguser, /finduserinmessage |
JSON-дампы сообщения/юзера для отладки |
Доступ к модерационным командам — по спискам ZAVTRA_ADMINS / ZAVTRA_MODERS / ZAVTRA_SYSADMIN.
/muted вызывается в основном чате. Доступ: администраторы Telegram, владелец чата,
пользователи из ZAVTRA_ADMINS, ZAVTRA_MODERS и ZAVTRA_SYSADMIN. В меню команда
показывается администраторам чата; настроенные модераторы могут вызвать её по имени.
И команда из меню, и ответ эфемерные: отчёт видит только вызвавший пользователь.
Кнопки обновления и страниц редактируют его скрытое сообщение и заново проверяют права.
Время окончания указано по Москве.
Полный текущий список запрашивается через MTProto, включая ручные ограничения и старые муты.
При недоступном MTProto команда проверяет через Bot API известные боту активные ограничения
и явно предупреждает, что список неполон; ошибки проверки показываются отдельно.
Снятые, истёкшие ограничения, вышедшие участники и запреты только на медиа в список не входят.
Новая таблица mute_events хранит успешные муты/снятия, инициатора и источник: самоблесс,
рикошет, убийство, дуэль, массовый выстрел, административный мут или три предупреждения.
Ручные изменения отслеживаются через chat_member (бот должен быть администратором,
chat_member должен входить в ZAVTRA_ALLOWED_UPDATES; он включён по умолчанию).
Для ограничений без подходящей записи причина обозначается как неизвестная.
Имеющиеся игровые счётчики и правила ограничений не меняются.
Игры и «социальное»
| Команда | Что делает |
|---|---|
/duel, /russianroulette |
вызов на дуэль (русская рулетка), кнопки принятия/отказа |
/duelstats |
своя статистика дуэлей |
/kill и алиасы bite, kilobite, kilomlem, megabite, bless |
«пристрелить» юзера (временное ограничение) |
/revive, /resurrect, /redemption, /massrevive, /megarevive |
возродить одного / всех недавних |
/shotgun, gigabite, gigakill, ultrakill |
выстрел по чату |
/megamlem |
те же права и ограничения участников, что у /shotgun, с картинкой гигантского чёрного кота |
/isekai, suicide, blessing, /isekaitop, /blessingtop |
«устроиться в роскомнадзор» и топ чата |
/addbless, /addnope |
добавить причину блесса / текст отказа по кнопке |
/addsalvation |
добавить фразу спасения (до 200 символов): текстом после команды или ответом на сообщение |
/salvationlist |
управление отдельным набором фраз спасения в личке: страницы и удаление с подтверждением |
/salvationlistfull |
весь набор фраз спасения обычным текстом в личке |
/blesslist |
управление общими фразами блесса в личке: страницы, выбор и подтверждение удаления |
/pidor, /pidoreg, /pidorlist, /pidorme, /pidorstats, /pidorall, /pidorules, /pidordel, /pidorremovetoday |
игра «Пидор Дня!» |
/slap, /hug, /bonk, /marco, /shrug, /me |
стикеры/мем-действия и IRC-подобный /me |
/blesslist доступна в личке пользователям из ZAVTRA_ADMINS и ZAVTRA_MODERS.
В группе команда только предлагает перейти в личку. Фразы общие для /blessing, /kill,
/shotgun, /megamlem и их аналогов. Удаление требует отдельного подтверждения точного текста;
последнюю фразу удалить нельзя. Бот оставляет копию удалённого текста для восстановления через
неизменённую /addbless (до 200 символов; для длинных старых фраз выводится предупреждение).
Кнопки привязаны к пользователю и сообщению, действуют 30 минут;
после перезапуска или истечения срока откройте /blesslist заново. Статистика не меняется.
Медиа
| Команда | Что делает | Нужно |
|---|---|---|
/meow, /mlem, /woof |
гифка с котиком / не-котиком | MTProto |
/meme |
мем из канала | MTProto |
/quote |
карточка с цитатой из сообщения, на которое ответили | — |
/convert |
конвертация медиа: mp3, ogg, gif, audio, voice, animation |
ffmpeg, ZAVTRA_BOT_API_DATADIR |
/distort, /invert, /reverse, /loop |
«переебать»/инвертировать/залупить медиа | ffmpeg, ZAVTRA_BOT_API_DATADIR |
/mp3 |
скачать аудио по ссылке (YouTube и т. п.) | yt-dlp, ffmpeg |
/download, /wget |
скачать видео по ссылке | yt-dlp |
/savetopm, /giveme, /topm |
сохранить пост в личку (копирует в PM, бот должен быть разблокирован) | — |
Неполный фрагмент отмечается многоточиями со стороны пропущенного текста; технические статусы под карточками не публикуются. /quote создаёт публичную карточку ответом на сообщение или выделенный фрагмент Telegram. Обычные участники делят общий лимит 7 успешно отправленных карточек за календарный час по Москве, сохраняемый в базе после перезапуска. Под карточкой нет служебной подписи. При исчерпании лимита пользователь получает приватное уведомление; ошибки отправки возвращают место. После успешной отправки карточки бот удаляет сообщение с командой. Администраторы бота, системный администратор и администраторы основного чата цитируют без ограничений и без подписи о лимите. Сама команда отправляется обычным сообщением; ошибки, отказы и /quote debug остаются приватными. Диагностика доступна только администраторам. Защищённые авторы задаются закрытым секретом сборки или переменной ZAVTRA_QUOTE_PROTECTED_USERNAMES; список не публикуется в коде, справке и отказах. Их сообщения могут цитировать только администраторы. Проверяется исходный автор пересылки; пересылки со скрытым автором доступны только администраторам при включённой защите. ID распознанного защищённого автора сохраняется с хешем настройки для защиты после смены ника. Бот создаёт PNG-карточку
с градиентом, круглым аватаром, цитатой, именем автора и датой исходного сообщения
по Москве. У пересланных сообщений используются исходные автор и дата; для скрытого
автора сохраняется указанное Telegram имя. Аватар загружается из доступных размеров фото; при неудаче бот пробует текущую фотографию из карточки пользователя/чата. Для файла используется тот же сетевой клиент, что и для Bot API, включая его прокси/TLS. Поддерживаются пути общего каталога локального Bot API. Все попытки ограничены общими 8 секундами. Если аватар всё же недоступен, используются инициалы и личная подпись с причиной без токенов, URL и идентификаторов. Скрытый автор пересылки не ищется по имени.
Шрифты встроены в бинарник; обычные эмодзи рисуются монохромно. Для каждой карточки случайно выбирается один из четырёх сохранённых фонов в духе Telegram: светлый, ночной, сиреневый или мятный. Все исходные фоны — 1200×660 px. Текст белый на ночном фоне и тёмный на светлых; выбор не зависит от автора, повторы допустимы. Ширина карточки — 1200 px, высота адаптируется к тексту (от 660 px); фон заполняет её с сохранением пропорций. Аргументы команды не подменяют цитату. Длинные слова и абзацы переносятся,
текст не обрезается; если сообщение не помещается в карточку высотой до 6000 px, бот
предлагает выбрать более короткое. Подсказки видны только вызвавшему команду, сама
карточка видна только администратору, вызвавшему /quote. В группе используется персональное сообщение, в личке — обычный личный ответ. Ошибка персональной доставки не вызывает публичную отправку. Отрисовка использует общую ограниченную
очередь медиа. Новые настройки, внешние сервисы генерации и миграции не требуются.
В карточке — крупные кавычки, жирный текст, толстое подчёркивание под цитатой, аватар 160 px, подпись автора 40 px и дата 28 px. Короткие цитаты получают шрифт до 100 px; по мере увеличения текста он уменьшается до 36 px, без обрезки слов.
Одиночные слова до четырёх букв и варианты «пошёл на хуй» / «иди нахуй» отклоняются. Проверка учитывает регистр, ё/е, слитное/раздельное написание, распространённые опечатки, пунктуацию и повторяющиеся буквы. Проверяется выбранный фрагмент, если он задан, иначе всё сообщение. Отказ виден только вызвавшему администратору и содержит ровно: «Хехе, попа, а не цитата, выбери другое.»
Утилиты
| Команда | Что делает |
|---|---|
/get, /getall, /set, /del, /setgetowner |
«геты» — сохраняемые ответы бота (текст/медиа), инлайн-выбор через @botname |
/sed, /khaleesi, /say |
замена текста по образцу, мем-кхалиси, заставить бота сказать текст |
/stats |
топы чата: likes/reactions (лайки на сообщения), mutes/blessings (смерти), mutetime/blesstime (время в муте), messages, words; период day/week, без периода — за всё время |
/advice, /anekdot, /releases |
совет, анекдот, список релизов игр |
/cur |
курсы валют (нужен currency_key) |
/google |
ссылка на поиск по тексту |
/date, /ping, /getid, /testrandom, /tldr |
дата сервера, понг, ID юзера, проверка рандома, подсказка про кнопку TLDR |
/restart |
os.Exit(0) — перезапуск бота (нужен Restart=always в systemd) |
Ставки
/bet ДД.ММ.ГГГГ текст сохраняет прогноз на дату начиная с завтрашнего дня
по Москве. Записываются автор, текст, дата прогноза и время исходного сообщения.
/allbets показывает ставки начиная с сегодняшнего дня, /allbets all — также
прошедшие; в списке видна дата создания. /delbet ДД.ММ.ГГГГ текст удаляет свою
ставку и отменяет ещё не отправленное напоминание.
В день прогноза в 12:00 МСК бот публикует в основном чате (ZAVTRA_CHAT) отдельное
обычное сообщение для каждой ставки: «31.10.2026 @автор сказал: … Ну что, сбылось?».
Имя — кликабельное упоминание по Telegram ID; используется актуальный известный
боту ник, при его отсутствии — имя. Без записи пользователя остаётся сохранённое
при создании имя. Результат прогноза автоматически не определяется.
Проверка запускается при старте и на границе каждой минуты. После запуска между 12:00 и полуночью МСК бот отправляет пропущенные сегодняшние напоминания. Ставки прошедших дней не рассылаются. Успешная отправка отмечается в PostgreSQL; обычный перезапуск не повторяет её. Ошибки отправки повторяются на следующей проверке. Блокировки строк защищают от параллельной отправки несколькими экземплярами. Telegram и PostgreSQL не образуют общую транзакцию: при аварии после доставки, но до фиксации отметки, или неоднозначном сетевом тайм-ауте возможен повтор.
Миграция добавляет поля без изменения ключей и дат старых ставок. Историческая дата создания неизвестна: в списке это указано явно, напоминание начинается со слова «Ранее». Новые тексты ограничены 3500 единицами UTF-16 для запаса под оформление; длинные исторические ставки делятся на несколько сообщений.
Измерение производительности
/perf показывает отчёт в личке пользователям из ZAVTRA_ADMINS и ZAVTRA_MODERS.
В группе команда предлагает перейти в личку сообщением, видимым только вызвавшему;
остальным пользователям отчёт недоступен. /perf сохраняет отчёт с запуска и добавляет
краткие сводки за 15 и 60 минут. /perf 15 и /perf 60 показывают подробный отчёт
за выбранное окно. Окна состоят из текущей неполной минуты и 14/59 предыдущих минут;
вызовы относятся к минуте завершения. Хранятся 60 поминутных агрегатов, без истории событий.
Дополнительно показаны память Go (занятые объекты и память, полученная средой Go от ОС),
число горутин и текущие активные/ожидающие медиа-задачи с лимитами. Память Go не равна
RSS всего процесса и не включает память отдельных FFmpeg/yt-dlp. Счётчики медиа отражают
состояние на момент запроса, независимо от выбранного окна.
С момента запуска собираются число завершённых вызовов, возвращённых ошибок и медленных
вызовов (от 5 секунд), среднее и максимальное время. В отчёте — до 10 обработчиков
с наибольшим суммарным временем, команды-алиасы учитываются отдельно. Дополнительно
измеряются все запросы через основной GORM-клиент: количество, ошибки, среднее, максимум
и число запросов от 200 мс, включая служебные запросы запуска и фоновых задач.
record not found и сигналы продолжения/завершения диспетчера не считаются ошибками.
Хранятся только агрегаты по фиксированным именам обработчиков, без истории отдельных сообщений, аргументов и идентификаторов пользователей. Перезапуск обнуляет показатели; отдельная база и миграции не нужны. Время обработчика включает синхронные обращения к Telegram, но не работу горутин после его возврата. Обработанные внутри команды ошибки не видны как ошибки обработчика; SQL-ошибки учитываются отдельно. Это не измерение очереди обновлений или времени доставки ответа пользователю. BEGIN/COMMIT драйвера, MTProto-хранилища и сторонние SQL-клиенты в счётчик GORM не входят.
Медленные и завершившиеся ошибкой обработчики и SQL-запросы пишутся в существующий журнал
с длительностью и признаком ошибки. Новый SQL-логгер не раскрывает запросы, параметры
или текст ошибок. Старые диагностические сообщения других частей бота не меняются.
Владелец сервера может посмотреть эти события через
journalctl -u telegram-go-chatbot --since today | grep -E 'handler performance|database performance'.
Обработчики без команд
reserve_chat— удаляет системные сообщения о входе/выходе и добавляет юзеров в БД.channel— пересылает посты канала в чат; при заданномstream_channelдублирует туда.- Inline-режим —
@botname <текст>отдаёт сохранённые геты. - Реакции в основном чате.
- Глобальный middleware — debug-лог каждого апдейта + антиспам: больше
ZAVTRA_ANTISPAM_COUNTсообщений заZAVTRA_ANTISPAM_WINDOWсекунд → эфемерное предупреждениеZAVTRA_SYSADMINв основном чате.
Требования
| Зависимость | Где нужна |
|---|---|
| Go | 1.26+ (go.mod), в CI/Dockerfile база golang:1.25 + автозамена тулчейна |
| PostgreSQL | обязателен, таблицы создаются AutoMigrate при старте |
| ffmpeg, ffprobe | для /convert, /distort, /invert, /loop, /mp3 |
| yt-dlp | для /mp3, /download (в Docker-образе не установлен) |
| Self-hosted Bot API | опционально: ZAVTRA_BOT_API_URL (применяется ко всем API-вызовам); ZAVTRA_BOT_API_DATADIR — чтобы GetMedia читал файлы напрямую с диска (для /convert, /distort, /invert, /loop) |
| Прямой доступ к датацентрам Telegram | для MTProto-команд: TCP на 149.154.* / 91.108.*:443 + app_id/app_hash |
Конфигурация
Всё конфигурирование — переменные окружения ZAVTRA_*. Бот читает файл .env из рабочей директории
(ZAVTRA_ENV_FILE указывает на другой путь), а переменные окружения процесса его перекрывают: файл —
это настройки по умолчанию, окружение — точечные переопределения. Шаблон — .env.example, сам .env
в .gitignore: в нём токен бота и пароль базы, держите права 0600.
Docker: docker run --env-file .env, в compose env_file: .env, в systemd EnvironmentFile=. В образ
.env не попадает (.dockerignore).
| Переменная | Назначение |
|---|---|
ZAVTRA_TOKEN |
токен бота — обязательна |
ZAVTRA_CHAT |
ID основного чата — обязательна |
ZAVTRA_DSN |
PostgreSQL DSN |
ZAVTRA_BOT_API_URL |
базовый URL Bot API (по умолчанию https://api.telegram.org) |
ZAVTRA_BOT_API_DATADIR |
datadir self-hosted Bot API для прямого чтения файлов |
ZAVTRA_APP_ID, ZAVTRA_APP_HASH |
учётные данные MTProto-приложения для /meow, /woof, /meme |
ZAVTRA_ALLOWED_UPDATES |
какие апдейты запрашивать (по умолчанию message,channel_post,callback_query,chat_member) |
ZAVTRA_LISTEN, ZAVTRA_ENDPOINT_PUBLIC_URL, ZAVTRA_MAX_CONNECTIONS |
webhook-режим; если пусто — long polling |
ZAVTRA_RESERVE_CHAT |
чат, где удаляются сообщения о входе/выходе |
ZAVTRA_CHANNEL, ZAVTRA_STREAM_CHANNEL |
канал для пересылки постов и куда их дублировать |
ZAVTRA_ADMINS, ZAVTRA_MODERS, ZAVTRA_SYSADMIN |
ID с правами модерации (списки через запятую); ZAVTRA_SYSADMIN получает уведомления о старте, ошибках и антиспаме эфемерными сообщениями в основном чате (видит только он, Telegram удаляет их по настройке «эфемерные сообщения» чата; нужен self-hosted Bot API) |
ZAVTRA_CURRENCY_KEY |
CoinMarketCap API key для /cur |
ZAVTRA_RELEASES_URL |
календарь для /releases (по умолчанию — PS/PS5/Switch/Xbox PAL) |
ZAVTRA_PROXY |
HTTP-прокси для исходящих запросов: /advice, /anekdot, /releases, скачивание файлов (таймаут 10 с) |
ZAVTRA_ANTISPAM_WINDOW, ZAVTRA_ANTISPAM_COUNT |
окно в секундах и порог сообщений для антиспама |
ZAVTRA_AI_URL, ZAVTRA_AI_TOKEN, ZAVTRA_AI_MODEL, ZAVTRA_AI_SYSTEM, ZAVTRA_AI_ADMIN_ONLY_TOOLS |
AI-модуль (ai_*.go), в main.go пока выключен |
ZAVTRA_FONTPATH, ZAVTRA_COMMENT_CHAT |
объявлены в конфиге, в коде сейчас не используются |
ZAVTRA_ENV_FILE |
путь к .env (по умолчанию .env в рабочей директории) |
Списки — через запятую (пробел и ; тоже разделители). Значения с пробелами, $, # и переводами
строк берутся в двойные кавычки: ZAVTRA_AI_SYSTEM="первая строка\nвторая".
Права .env — 600: в файле токен бота и пароль базы.
Запуск
go build -o zavtrachat_bot
cp .env.example .env && chmod 600 .env # заполните токен и чат
./zavtrachat_bot # из каталога, где лежит .env
Docker:
docker build -t zavtrachat .
docker run -d --name zavtrachat --env-file .env zavtrachat
В образе WORKDIR /, ENTRYPOINT /goapp, установлен ffmpeg; конфигурация — --env-file .env
(или отдельные -e ZAVTRA_TOKEN=...). Секреты в образ не копируются (.dockerignore).
yt-dlp в образ не входит — /mp3 и /download в контейнере работать не будут.
CI (.github/workflows/):
-
deploy.yml— на изменение**.go,go.mod/go.sumи самого workflow:Testгоняетgo vetиgo test -race -count=1наubuntu-latestс отдельным сервисом PostgreSQL 18 (покрытие складывается в артефакт), затем наpushвmasterDeployсобираетtelegram-go-chatbotна self-hosted раннере, копирует его в/home/telegram/bot/и делаетsystemctl restart telegram-go-chatbot. На pull request тесты идут, прод не перезапускается; параллельные деплои блокируютсяconcurrency-группой;На сервере в рабочей директории сервиса должен лежать
.env(/home/telegram/bot/.env) — конфигурация читается только из окружения, без файла сервис не стартует. Права0600, владелец — пользователь сервиса; -
docker-publish_yml— сборка и публикация образа вghcr.io+ cosign-подпись. Файл назван с_ymlвместо.yml, поэтому GitHub Actions его не подхватывает (workflows ищут*.yml/*.yaml).
Тесты
go test -race -count=1 ./... # PostgreSQL-тесты опциональны локально
PROTO_PROBE=1 go test -count=1 -run TestMtprotoConnectProbe -v . # проверить доступ к датацентрам
В CI устанавливаются ffmpeg, ffprobe, ImageMagick (convert) и yt-dlp версии
2026.07.04. При ZAVTRA_REQUIRE_MEDIA_TESTS=1 отсутствие инструмента или невозможность
создать медиа-фикстуру — ошибка, а не пропуск. В итоговом отчёте видны пропущенные тесты;
разрешён только отдельный тест живого MTProto-соединения. Артефакт coverage содержит
профиль покрытия и test-results.jsonl. Локально медиа-проверки без инструментов остаются
опциональными.
Дуэли проверяются с управляемыми случайностью и паузами: повторные и параллельные кнопки, устаревшие приглашения, рикошет, ничья и ошибки Telegram. Рабочие вероятности и задержки анимации сохранены. Приглашение привязано к сообщению и участникам; завершённые кнопки не запускают игру повторно. При перезапуске незавершённые приглашения устаревают.
Получение котиков и мемов проверяется через подменённый MTProto-транспорт с настоящими кодированием ответов и загрузчиком файлов. Проверяются байты и тип отправленного медиа, пустые и ошибочные ответы, отсутствие имени файла и одновременный доступ к кэшу каналов. Учётные данные и соединение с живыми каналами этим тестам не нужны.
Проверки команд включают параллельные предупреждения и игровые смерти, ошибки записи,
отказ Telegram и сбой итогового сообщения. Смерти сохраняют прежний смысл попыток игры:
общий и дневной счётчики меняются одной транзакцией до обращения к Telegram; время мута
прибавляется только после подтверждённого ограничения. Ошибки счётчиков возвращаются
обработчиком. Успешный мут не откатывается из-за ошибки отправки сообщения. У /warn
неуспешное третье предупреждение откатывается, чтобы новый запрос мог повторить попытку;
уже достигнутый порог не вызывает повторного мута. Новые /kill и /blessing остаются
новыми игровыми действиями, а не повторами старого запроса. Общей гарантии exactly-once
для Bot API и БД нет.
routing_scenarios_test.go использует рабочие middleware и регистрацию обработчиков:
обычные/эфемерные команды, адресация боту, права, callbacks и chat_member.
remaining_postgres_test.go проверяет полную и повторную миграцию, переход со старой
схемы, чтение после переподключения и параллельные игровые команды в отдельных схемах.
Дополнительно проверяются все шесть медиа-типов гетов, целые подписи с форматированием,
права владельца, повреждённые данные, недельная граница предупреждений и полночь/DST
розыгрыша. FuzzSedPattern содержит начальный корпус; дополнительный прогон:
go test -run '^$' -fuzz '^FuzzSedPattern$' -fuzztime 20s ..
Инфраструктура тестов — testutils_test.go:
- фейковый Bot API-клиент, записывающий все вызовы бота (проверяется отправленный текст/действия);
хук
Observeдаёт заглянуть в файл, который обработчик закрывает и удаляет сразу после отправки; - in-memory SQLite (
github.com/glebarez/sqlite, чистый Go, CGO не нужен) вместо PostgreSQL; - хелперы
setupTestEnv,testMessage,testContext,testCallbackContext,seedUser,waitForCalls.
Сохранение альбомов в личку проверяется при одновременном поступлении сообщений и 128 запросах сохранения: каждая отправка сортирует свою копию списка, не меняет общий кэш и не блокирует его на время запроса к Telegram. Отдельно проверяются пустой кэш и пустой результат копирования.
Основной набор использует локальные заглушки и SQLite. TestStatsPostgresConcurrent дополнительно
проверяет на настоящей PostgreSQL одновременное создание слов и обновление счётчиков сообщений,
слов и времени мута: 32 параллельных обработчика, по 20 обновлений каждого счётчика.
В CI база предоставляется отдельным контейнером; локально тест запускается, если задан
ZAVTRA_TEST_POSTGRES_DSN для тестовой базы. Данные создаются в случайной отдельной схеме,
которая удаляется после теста. Миграции, история мутов и параллельные игровые команды
также проверяются на PostgreSQL; остальные сценарии используют SQLite.
TestMessageStatsQueryCount считает SQL-операторы в обработчике сообщения. После прогрева словаря
сообщение из 10 разных слов требует 1 оператор вместо 15, а из 10 повторений одного слова —
1 вместо 6, если данные отправителя также закэшированы. Служебные BEGIN/COMMIT в этот замер не входят. Все счётчики одного сообщения
записываются единым атомарным пакетом без дополнительной транзакции GORM; ошибка не оставляет
частично применённые счётчики. Ключи упорядочены для параллельных пакетов. Повторы и словоформы объединяются
по существующему ID слова; первые написания, периоды статистики и исторические строки сохраняются.
FIFO-кэш хранит не больше 4096 корней длиной до 128 байт, привязан к базе и объединяет одновременные
загрузки одного корня внутри процесса. Ошибки и нулевые ID не кэшируются. Изменение структуры базы
или объединение старых дубликатов слов не требуется.
Тяжёлые медиа-команды используют общую очередь: 2 выполняются, до 8 ждут не более 30 секунд.
Обработка ограничена 5 минутами с момента получения места; переполненная очередь возвращает
ошибку сразу. На Unix таймаут завершает группу процессов, включая дочерний FFmpeg у yt-dlp.
/distort дополнительно сохраняет ограничение в одну такую задачу. Обычные команды не ждут
эту очередь. Временные результаты удаляются и при ошибках; общий исходный файл Telegram
конвертации не удаляют. Лимит относится к задачам, а не к числу потоков внутри FFmpeg.
Данные пользователей сохраняются при изменении любого поля. Ограниченный FIFO-кэш на 4096
пользователей пропускает повторные записи, привязан к пулу базы и объединяет одновременные
записи одного пользователя. Через 5 минут после успешного сохранения выполняется проверочный
upsert: PostgreSQL обновляет строку только при отличии полей. Ошибки не кэшируются, удалённая
строка восстанавливается после истечения срока. Пустые имена и выключенные флаги тоже сохраняются.
Тот же путь используется для сообщений и событий chat_member.
Зависимости от окружения: четырнадцать тестов гоняют настоящие ffmpeg/ffprobe по сгенерированным
секундным файлам и пропускаются, если бинарников нет (TestFFmpegConvert,
TestFFmpegConvertConcurrentSameName, TestConvert, TestInvert, TestDistort,
TestLoopAnimation, TestDownloadFile, TestFFmpegRemoteMedia, TestGetImageForLLM,
TestGetImageForLLMRemote, TestGetImageForLLMConcurrentSameFile, TestAIWithPhoto,
TestMp3LocalSource, TestDownloadLocalSource); TestMtprotoConnectProbe пропускается без
PROTO_PROBE=1, TestYtdlpGetVer — без установленного yt-dlp.
Временные файлы конвертаций (/convert, /mp3, /download, картинка для /ai) и рабочие папки
/distort получают уникальные имена — os.CreateTemp, chat_id + message_id, DistortTempDir
для тестов. Поэтому несколько прогонов набора на одной машине не перетирают друг другу файлы, а
параллельные запросы на один и тот же файл не читают чужую копию; это проверяют
TestFFmpegConvertConcurrentSameName и TestGetImageForLLMConcurrentSameFile.
/mp3 и /download работают с реальным yt-dlp на локальных медиафикстурах: ytdlpFixtureServer
генерирует секундный m4a-файл и HLS-поток с отдельными видео- и аудиодорожками, отдаёт их по HTTP на
127.0.0.1, а TestMain выписывает loopback из прокси (NO_PROXY), чтобы yt-dlp шёл к фикстуре
напрямую. TestMp3LocalSource и TestDownloadLocalSource проверяют, что фильтр форматов выбранного
качества действительно находит фикстуру, ffmpeg склеивает/конвертирует дорожки, а отправленный файл
— это готовый mp3 и готовый mp4 с обеими дорожками.
Наружу тесты не ходят: весь внешний трафик уходит в локальные заглушки. mitm_test.go поднимает
MITM-прокси — TestMain генерирует одноразовый CA, пишет его в SSL_CERT_FILE и публикует прокси
через HTTP_PROXY/HTTPS_PROXY, поэтому в заглушку попадают даже клиенты с дефолтным транспортом
(клиент CoinMarketCap); сертификаты для хостов подписываются на лету. Два пути перехвата:
stubTLSProxy (прокси для httpClient()) и stubEnvRoute (маршрут поверх прокси процесса).
Локальные httptest-серверы: заглушка Bot API (TestBotInit — getMe, удаление вебхука, polling
и отчёт сисадмину; TestBotInitWebhook — сервер вебхука поднимается до setWebhook, запрос без
secret-токена получает 401), OpenAI-формат для /ai (TestAIStream, TestAIStreamError,
TestAIWithPhoto). Особые приёмы: TestRestart запускает процесс заново (os.Exit внутри
обработчика нельзя проверить из того же процесса), TestConfigInit работает в t.TempDir() и с чистым
окружением (clearZavtraEnv), чтобы не трогать .env чекаута. TestStartupReport проверяет отчёт о
старте на секреты: в нём нет токена, а из DSN, прокси, app hash и API-ключей вырезаны пароли.
Структура
main.go конфигурация обработчиков и запуск
utils_bot.go инициализация бота, polling/webhook, middleware+антиспам
utils_bot_proto.go MTProto-клиент: подключение, переподключение, таймауты
utils_config.go Configuration + переменные окружения ZAVTRA_* (.env)
utils_db.go модели gorm, AutoMigrate, текстовые пулы «да/нет»
utils_handlers.go обработчики апдейтов: апдейт юзера, альбом-кэш, проверка банов, реакции
utils_user.go имена и меншены юзеров, списки админов, поиск юзера в сообщении и в БД
utils_stats.go счётчики статистики сообщений, слов и времени в муте, начало суток
utils_reply.go Reply, ReplyAndRemove и SysAdminReply — чем команды отвечают пользователю и сисадмину
utils_startup.go отчёт о старте: сборка, хост, соединение, БД, чаты, версии инструментов (без кредов)
utils_common.go RandInt, StringInSlice, MarshalOmitEmptyAll
utils_http.go HTTP-клиент с прокси и таймаутом, DownloadFile
utils_forward.go пересылка постов канала
utils_files.go go:embed стикеров и картинки /megamlem (assets/*.png, assets/megamlem.jpg)
utils_plural.go русские числительные (ruPlural)
media.go медиа сообщения: тип, файл, где он лежит на диске
media_ffmpeg.go FFmpegConvert — конвертация для /convert, /distort, /invert, /loop
command_*.go команды
duel_*.go дуэль: request/accept/stats
pidor_*.go игра «Пидор Дня!»
bets_*.go ставки
ai_*.go AI-модуль (выключен)
*_test.go тесты + harness
assets/ стикеры, картинка /megamlem (go:embed) и CREDITS.md
MTProto-клиент
/meow, /woof и /meme работают через MTProto (gotgproto + gotd), а не через Bot API:
- клиент поднимается в фоновой горутине, подключение ограничено
protoConnectTimeout(45 с), при неудаче — повтор каждыеprotoReconnectDelay(30 с); в логе видноMTProto client connectedили причину (MTProto client is not connected (attempt=N, error=…)); - команды при неподключённом клиенте отвечают «MTProto-клиент ещё не подключился», а не падают;
- на каждый запрос свой дедлайн (RPC 60 с, скачивание 3 мин), read-only запросы ретраятся;
NoUpdates: true— бот не забирает апдейты у long polling Bot API, MTProto используется только для RPC;ZAVTRA_PROXYна MTProto не влияет: датацентры диалсятся прямым TCP, поэтому для этих команд нужен прямой доступ к*:443(или self-hosted MTProto-транзит).
Известные особенности
ZAVTRA_PROXYприменяется только к обычным HTTP-запросам бота (advice/anekdot/releases/скачивание файлов); вызовы Bot API и MTProto его не используют./restartделаетos.Exit(0)— перезапуск должен обеспечивать внешний супервизор (Restart=alwaysв systemd,restart: alwaysв compose).- Ошибки хендлеров диспетчер отправляет в чат («Ошибка: …» и контекст: текст команды, юзер, чат), токен в тексте подменяется на
TOKEN. - AI-команды (
/ai,/restartai,/setaisystem) закомментированы вmain.go.
Разработка
С 16 октября 2026 года разработкой занимается ИИ: правки пишет модель, человек ставит задачи,
принимает решения и проверяет результат. Каждый такой коммит помечен в сообщении
(Made with the … model.) и соавтором (Co-authored-by: … <noreply@opencode.ai>), поэтому вклад ИИ
виден прямо в истории git.
Модели локального стека Strata — конфигурации в ~/.strata/*.json, движок ~/.strata/engine/strata,
веса в ~/Strata-data/models:
Модель (model_name) |
Что это | В этом репозитории |
|---|---|---|
swift-1.5-iq3_xxs |
Swift-1.5 Qwen3.8-Flash-Next GSQ RCO, квант IQ3_XXS (ukisai/Swift-1.5-Qwen3.8-Flash-Next-GSQ-RCO-GGUF) |
текущая: тест-харнес, фиксы MTProto/числительных, тесты команд, фиксы старта бота и /ai, тесты меню команд, тесты /mp3//download на реальном yt-dlp, конфигурация через .env, README |
qwen3.8-flash-next-iq3_xxs |
Qwen3.8-Flash-Next GSQ RCO, квант IQ3_XXS (ISTA-DASLab/Qwen3.8-Flash-Next-GSQ-RCO-GGUF) |
e55e505 (go:embed), 30415bd (тесты хелперов + фикс RandInt) |
qwen3.8-flash-next |
она же, до отдельного кванта | 4ab27c5 — первый ИИ-коммит (bot_api_url для всех API-вызовов) |
orcarouter-qwen3.8-flash-next-uncensored-iq3_xxs |
Qwen3.8-Flash-Next Uncensored, IQ3_XXS | настроена в ~/.strata, к этому коду не применялась |
Рядом с ними в стеке: vision-проекции mmproj-Qwen3.8-Flash-Next-BF16.gguf и
mmproj-Swift-Qwen3.8-Flash-Next-BF16.gguf, черновик для спекулятивного декодирования
mtp-q2_0.gguf, а в ~/.strata/data/gguf_fingerprints.json описаны и другие кванты этих же моделей
(Q2_0, IQ2_XS, IQ3_XXS, IQ3_S, coder IQ1_M, unsloth UD-IQ4_XS / UD-Q4_K_XL).
Лицензия
MIT (см. LICENSE), © 2026 NexonSU. Лицензия покрывает исходники бота.
Что она не покрывает:
assets/bonk.pngиassets/hug.png— сторонние мемы, права на них не передаются; подробности вassets/CREDITS.md. При публичной публикации их стоит убрать или заменить на свои.ffmpegиyt-dlp— внешние GPL-программы, бот вызывает их отдельным процессом; лицензия бота на них не распространяется, условия их лицензий — на их стороне.- Почти все Go-зависимости — MIT / BSD-3 / Apache-2.0, они с MIT совместимы. Исключение —
github.com/celestix/gotgprotoпод GPL-3.0: он линкуется в бинарник, поэтому при распространении собранного бота условия GPL-3 действуют на работу целиком (придётся раскрыть исходники). Для запуска бота на своём сервере обязательств нет: GPL срабатывает при распространении, а не при использовании.
Команды фраз спасения доступны администраторам и модераторам (ZAVTRA_ADMINS, ZAVTRA_MODERS). Фразы хранятся отдельно от блессингов; управление ими не меняет вероятность блесса или статистику. Последнюю фразу удалить нельзя.
/blessing, /suicide и /isekai дают 33% шанс спасения: имя и случайная фраза из /salvationlist, затем «Живи. Пока что.». Спасение не добавляет смерть или время мьюта и не участвует в серии блессов. Админский рикошет всегда даёт блесс. При пустом наборе спасений работает обычный блесс. Спасения видны в /duelstats и /stats saves [day|week].
Source Files
¶
- ai_agent.go
- ai_init.go
- ai_restart.go
- ai_setsystem.go
- ai_utils.go
- bets_allbets.go
- bets_bet.go
- bets_delbet.go
- bets_reminders.go
- birthday_reminders.go
- birthday_render.go
- birthdays.go
- command_addbless.go
- command_addnope.go
- command_addsalvation.go
- command_advice.go
- command_anekdot.go
- command_ban.go
- command_birthday.go
- command_blessing.go
- command_blessingtop.go
- command_blesslist.go
- command_blesslistfull.go
- command_bonk.go
- command_convert.go
- command_cur.go
- command_date.go
- command_debug.go
- command_debuguser.go
- command_del.go
- command_distort.go
- command_download.go
- command_finduserinmessage.go
- command_get.go
- command_getall.go
- command_getid.go
- command_google.go
- command_hug.go
- command_invert.go
- command_khaleesi.go
- command_kick.go
- command_kill.go
- command_loop.go
- command_marco.go
- command_me.go
- command_meme.go
- command_meow.go
- command_mp3.go
- command_mute.go
- command_muted.go
- command_mywarns.go
- command_perf.go
- command_ping.go
- command_quote.go
- command_redemption.go
- command_releases.go
- command_remove.go
- command_restart.go
- command_revive.go
- command_salvationlist.go
- command_salvationlistfull.go
- command_savetopm.go
- command_say.go
- command_sed.go
- command_set.go
- command_setgetowner.go
- command_shotgun.go
- command_shrug.go
- command_slap.go
- command_stats.go
- command_testrandom.go
- command_tldr.go
- command_unban.go
- command_unmute.go
- command_warn.go
- command_woof.go
- command_zbhelp.go
- duel_accept.go
- duel_deny.go
- duel_request.go
- duel_stats.go
- main.go
- media.go
- media_ffmpeg.go
- media_input.go
- media_paths.go
- media_process_unix.go
- muted_telegram.go
- pidor_all.go
- pidor_del.go
- pidor_game.go
- pidor_list.go
- pidor_me.go
- pidor_reg.go
- pidor_remove_last.go
- pidor_rules.go
- pidor_stats.go
- quote_avatar.go
- quote_backgrounds.go
- quote_budget.go
- quote_protection.go
- quote_recovery.go
- quote_render.go
- quote_selection.go
- utils_bot.go
- utils_bot_proto.go
- utils_common.go
- utils_config.go
- utils_db.go
- utils_duel.go
- utils_files.go
- utils_forward.go
- utils_game_counters.go
- utils_handlers.go
- utils_http.go
- utils_media_tasks.go
- utils_mutes.go
- utils_performance.go
- utils_plural.go
- utils_proto_media.go
- utils_reply.go
- utils_startup.go
- utils_stats.go
- utils_stats_words.go
- utils_user.go
- utils_user_cache.go