# Бот расписания в Telegram Оповещения зрителю: «на канале началось шоу» и «скоро начнётся». Настраивается в админке (вкладка «Телеграм»), подписки собирает сам зритель кнопками в чате. ## Что решено и почему **Подписка только по одноразовому коду.** Кнопка «Открыть чат с ботом» на странице эфира выдаёт ссылку `t.me/бот?start=КОД`; код живёт 15 минут, сгорает при первом использовании и привязывает чат к учётной записи TeleWave. Анонимный бот («кто нашёл, тот и подписался») означал бы рассылку неизвестно кому и невозможность отписать чат вместе с блокировкой пользователя. Ссылка выдаётся по нажатию, а не лежит в разметке: в ней пропуск к чужим оповещениям. **Два транспорта, переключателем.** `Polling` — сервер сам ходит за обновлениями: работает из-за NAT и через прокси, и это единственный вариант там, где публичного адреса нет. `Webhook` — Telegram стучится на наш HTTPS-эндпоинт: дешевле и быстрее, но требует доступного извне домена, и прокси здесь не поможет — соединение устанавливает Telegram. Разговор с ботом при этом один и тот же: оба транспорта приносят одинаковое обновление и разбираются общим кодом. **Прокси — часть настроек, а не окружения.** HTTP или SOCKS5 с логином и паролем; клиент собирается на каждый вызов, поэтому смена прокси не требует перезапуска контейнера. Кривой адрес — ошибка, а не повод молча пойти напрямую: «работает в обход прокси» на канале, ради которого прокси и заводили, хуже отказа. **Секреты в базе, наружу не отдаются.** Токен и пароль прокси лежат открытым текстом: шифровать их нечем — Data Protection в проекте сознательно не заводили (см. CLAUDE.md). Наружу уходит только признак «задан», а пустое поле в форме означает «оставить как есть». Секрет вебхука генерируется сам и проверяется заголовком `X-Telegram-Bot-Api-Secret-Token`: адрес рано или поздно окажется в чужих логах, и без секрета туда постучится кто угодно. **Проверка связи — на сохранении.** Настройки сразу спрашивают у Telegram `getMe`: неверный токен или мёртвый прокси иначе всплыли бы через сутки молчания бота. Оттуда же приходит имя бота — без него не собрать ссылку на чат, поэтому кнопка у зрителя появляется только после успешного контакта. Вебхук ставится и снимается тем же сохранением: пока он стоит, Telegram не отдаёт обновления опросом, и переключение режима без этого оставило бы бота немым. ## Оповещения Считаются по материализованной ленте, а не по факту раздачи: лента и есть эфир, а ждать подтверждения от плеера не от кого — зрителей может не быть вовсе. - **Начало шоу** — на смену шоу, а не на каждую запись: четыре серии подряд это одна программа в глазах зрителя, и четыре сообщения о ней читаются как спам. - **Скоро в эфире** — за пять минут до следующей программы. Оба называют канал: подписок у зрителя несколько, и «Сейчас в эфире» без названия не отвечает на вопрос «где» — по такому сообщению непонятно, куда переключаться. Граница разобранного (`TelegramSettings.NotifiedUntil`) двигается на каждом тике, даже когда подписчиков нет: иначе первый же подписавшийся получил бы пачку сообщений про всё, что успело пройти. Догон ограничен пятнадцатью минутами — после долгого простоя бот не станет пересказывать вчерашний эфир. В чате живёт только последнее оповещение: новое снимает предыдущее (`TelegramSubscriber.AlertMessageId`). Оповещение отвечает на вопрос «что идёт сейчас», а вчерашнее «сейчас в эфире» на этот вопрос отвечает неправдой — за сутки таких набирается несколько десятков, и найти среди них актуальное нельзя. Счётчик отдельный от одноразовых ответов бота: рассылка приходит сама и не должна сносить программу передач, которую зритель только что запросил. ## Меню в чате `/start` с кодом привязывает чат и открывает меню; `/start` без кода — открывает меню уже привязанному. Заголовок канала в меню — это кнопка «программа передач»: ближайшие восемь блоков в часовом поясе канала, идущее сейчас помечено. Подряд идущие серии одного шоу схлопываются в один блок — зритель спрашивает «что дальше», а не «в каком порядке лежат файлы». Программа приходит отдельным сообщением, меню остаётся на месте: иначе после просмотра пришлось бы звать `/start`, чтобы вернуть кнопки. Меню живёт **одним сообщением**: нажатие кнопки переписывает его на месте, иначе чат заполняется одинаковыми «ваши подписки» после каждого клика. Кнопка — тумблер: повторное нажатие снимает подписку. `/stop` останавливает рассылку, не стирая подписки: вернувшись, зритель получит свои каналы, а не пустой список. **Бот убирает за собой.** В чате живут ровно три его сообщения: меню подписок, последнее одноразовое (программа передач, подтверждение привязки или отказ) и последнее оповещение рассылки. Новое меню снимает предыдущее (две панели с разными галочками означают, что одна врёт), новая программа — предыдущую программу, новое оповещение — предыдущее оповещение. Идентификаторы всех трёх хранятся у подписчика. Команды зрителя остаются: в личном чате бот вправе удалять только собственные сообщения, и права на чужие у него нет и не будет. Не удалившееся сообщение (зритель убрал его сам, прошло больше 48 часов) уборку не срывает — это не та задача, ради которой стоит рвать разговор. Данные кнопок несут действие: `sub:канал:вид` переключает подписку, `epg:канал` показывает программу. Действие в первом поле, а не «угадаем по числу частей»: кнопок со временем станет больше, и разбор по длине строки сломался бы на первой же новой. ## Границы - Бот не отвечает на произвольные вопросы и не показывает программу передач — только подписки. - Групповые чаты не поддерживаются: подписка привязана к учётке, а в группе её владельца нет. - Один чат — один подписчик; повторная привязка по новому коду меняет учётку, а не плодит копии.