Канал — для всех. Бот — для участников. Это не маркетинговое разделение, а техническое: у них разные задачи, разные права и разные точки отказа.
Почему не один бот для всего
Сначала хотелось, чтобы бот был «лицом» проекта: новости, проекты, обратная связь. Но чем больше функций, тем больше путаницы.
Посетителю нужен канал: читать новости, смотреть статьи, писать в почту или нажимать «Сообщить о проблеме». Ему не нужен бот, который просит привязать аккаунт.
Участнику нужен бот: уведомления о входе, 2FA, сессии, события, репорты. Ему не нужен публичный канал как единственный интерфейс.
В итоге:
@iniproject_ru— публичный канал. Новости, статьи, анонсы.@iniproject_site_bot— внутренний бот. Только для участников.- Сайт — основная точка входа: кнопка 🐛, почта, формы.
Гостю бот не предлагается. Если он пишет боту /report, бот отвечает: «Это внутренний бот команды, напиши в канал или на почту». Не потому что жалко, а потому что репорты гостей должны попадать в общий раздел, а не в личку бота.
Очередь вместо прямых сообщений
Сайт не отправляет сообщения в Telegram напрямую. Он кладёт их в таблицу tg_queue:
- заголовок;
- текст;
- ссылка;
- фото;
- кнопка;
- «без звука»;
- статус:
pending,sent,error; - получатель;
- время;
- ошибка.
Админка показывает очередь и позволяет повторить, удалить или посмотреть полный текст. Это удобно: если Telegram недоступен, сообщение не теряется — оно ждёт.
Дальше работает pull-модель: бот на отдельном VPS раз в 20 секунд дергает endpoint сайта:
GET /api/tg-pull.php
X-TG-Key: ...
Сайт отдаёт очередное сообщение, бот отправляет его в канал или участнику и помечает статус.
Почему pull, а не webhook:
- не нужен публичный endpoint для Telegram;
- не надо хранить токен бота на сайте;
- проще firewall;
- бот может жить на другом сервере;
- если бот упал, сообщения не теряются.
Минус — задержка до 20 секунд. Для новостей это нормально.
Где живёт бот
Бот — отдельный процесс на отдельном VPS, в /opt/iniproject-bot, под systemd. Токен — только там, в файле с правами 600. На сайте токена нет.
Это важное разделение: если сайт взломают, токен бота не утечёт. Если утечёт токен бота, сайт всё равно не получит доступ к базе.
Код бота лежит в репозитории бэкапов, но не в публичной части сайта.
Привязка участника
Участник заходит в профиль, нажимает «Привязать Telegram», получает код на 10 минут. Потом пишет боту:
/start <код>
Бот связывает chat_id с пользователем. Дальше:
/status— статус привязки;/stop— отвязать;- уведомления о входе;
- 2FA-подтверждения;
- события: статус репорта, обновление проекта, бейдж, публикация.
Если 2FA включена, отвязка Telegram запрещена: иначе можно потерять второй фактор.
Урок: привязка — это не «подписка на новости». Это связь аккаунта с каналом доставки. Её надо делать явно и с подтверждением.
Уведомления о входе
При входе бот присылает:
- время (МСК);
- IP;
- устройство;
- кнопку «Сменить пароль».
Это не блокирует вход. Это даёт человеку шанс заметить чужой вход.
Если человек нажимает «Это не я» — сессия отзывается, пароль можно сменить. Если «Подтвердить» — вход продолжается.
Важно: уведомление не должно быть спамом. Только вход, только при привязке, только по делу.
2FA через Telegram
Сценарий:
- Пользователь вводит пароль.
- Сайт создаёт
login_confirmationsс токеном. - Бот присылает сообщение с кнопками.
- Пользователь нажимает «Подтвердить».
- Сайт завершает вход.
Если Telegram недоступен — есть резервные коды. Если потерян телефон — админ может сбросить TOTP/2FA.
Урок: второй фактор через мессенджер удобен, но зависим от мессенджера. Всегда нужен план Б.
Что видит гость
Гость в боте видит:
- «Сайт»;
- «Канал»;
- «Привязка».
И всё. /news, /projects, /report закрыты. Он не может отправить репорт через бота, потому что для этого нужна привязка. Это не баг, а решение: репорты должны быть связаны с человеком, иначе с ними невозможно работать.
Гостю на сайте доступны:
- кнопка 🐛 «Сообщить о проблеме»;
- почта;
- Telegram-канал.
Этого достаточно.
Где были грабли
Меню для непривязанных
Сначала бот показывал полное меню всем. Гость нажимал «Новости» и получал либо ошибку, либо пустоту. Пришлось сделать отдельное меню для непривязанных и закрыть команды.
Урок: если функция требует привязки, не показывай её до привязки. Или объясняй, что нужно.
Fallback на plain text
Telegram не всегда принимает HTML-разметку. Если сообщение не отправляется, бот повторяет его простым текстом. Это лучше, чем молчаливая ошибка.
Полный текст в очереди
Сначала в очереди была только строка. Потом понадобилось смотреть, что именно уйдёт. Добавили модалку с полным текстом, получателем и ошибкой.
Урок: очередь — это не «лог отправки». Это рабочий инструмент. В ней должно быть видно, что произойдёт.
Дубли
Если бот упал после отправки, но до отметки «sent», сообщение может уйти повторно. Полной идемпотентности нет. Пока спасает то, что очередь небольшая и админ видит статус.
Урок: retry без идемпотентности — это дубли. Для важных сообщений нужен ключ.
Вместо вывода
Разделение «канал для всех, бот для участников» оказалось не ограничением, а упрощением. У каждого канала своя аудитория и свои ожидания.
Публичный канал не должен знать, кто ты. Бот должен знать, кто ты, иначе он не сможет отправить уведомление.
Если смешать эти роли, получится бот, который рекламирует внутренние функции гостям, и канал, который пытается быть службой поддержки. Ни то, ни другое не работает.
