Updated the ITelegramApi interface to include a method for deleting messages and modified SendMessageAsync to return the message ID. Enhanced the TelegramBotService to manage message visibility by replacing previous messages with new ones, ensuring a cleaner user experience. Introduced properties in TelegramSubscriber to track the latest menu and notice message IDs. Updated related tests to verify the new message management behavior and adjusted documentation to reflect these changes.
80 lines
8.7 KiB
Markdown
80 lines
8.7 KiB
Markdown
# Бот расписания в 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`) двигается на каждом тике, даже когда
|
|
подписчиков нет: иначе первый же подписавшийся получил бы пачку сообщений про всё, что успело
|
|
пройти. Догон ограничен пятнадцатью минутами — после долгого простоя бот не станет пересказывать
|
|
вчерашний эфир.
|
|
|
|
## Меню в чате
|
|
|
|
`/start` с кодом привязывает чат и открывает меню; `/start` без кода — открывает меню уже
|
|
привязанному. Заголовок канала в меню — это кнопка «программа передач»: ближайшие восемь блоков
|
|
в часовом поясе канала, идущее сейчас помечено. Подряд идущие серии одного шоу схлопываются
|
|
в один блок — зритель спрашивает «что дальше», а не «в каком порядке лежат файлы». Программа
|
|
приходит отдельным сообщением, меню остаётся на месте: иначе после просмотра пришлось бы звать
|
|
`/start`, чтобы вернуть кнопки. Меню живёт **одним сообщением**: нажатие кнопки переписывает его на месте, иначе
|
|
чат заполняется одинаковыми «ваши подписки» после каждого клика. Кнопка — тумблер: повторное
|
|
нажатие снимает подписку. `/stop` останавливает рассылку, не стирая подписки: вернувшись, зритель
|
|
получит свои каналы, а не пустой список.
|
|
|
|
**Бот убирает за собой.** В чате живут ровно два его сообщения: меню подписок и последнее
|
|
одноразовое — программа передач, подтверждение привязки или отказ. Новое меню снимает предыдущее
|
|
(две панели с разными галочками означают, что одна врёт), новая программа — предыдущую программу.
|
|
Идентификаторы обоих сообщений хранятся у подписчика. Команды зрителя остаются: в личном чате бот
|
|
вправе удалять только собственные сообщения, и права на чужие у него нет и не будет. Не удалившееся
|
|
сообщение (зритель убрал его сам, прошло больше 48 часов) уборку не срывает — это не та задача,
|
|
ради которой стоит рвать разговор.
|
|
|
|
Данные кнопок несут действие: `sub:канал:вид` переключает подписку, `epg:канал` показывает
|
|
программу. Действие в первом поле, а не «угадаем по числу частей»: кнопок со временем станет
|
|
больше, и разбор по длине строки сломался бы на первой же новой.
|
|
|
|
## Границы
|
|
|
|
- Бот не отвечает на произвольные вопросы и не показывает программу передач — только подписки.
|
|
- Групповые чаты не поддерживаются: подписка привязана к учётке, а в группе её владельца нет.
|
|
- Один чат — один подписчик; повторная привязка по новому коду меняет учётку, а не плодит копии.
|