telegram: сервис поднимается без бота и работает одним входом

- Клиент бота собирается один раз и достаётся отправителю и транспорту;
  разрез прошёл по «ответил ли Telegram»: ответ «такого бота нет» роняет
  старт, недоступность даёт подъём без Telegram (ADR-2026-08-13). Ожидание
  при сборке ограничено сроком — иначе молчащий Telegram вешал подъём.
- Недоставленный ответ не роняет шаг: пишется с job_id и считается метрикой,
  уровень по причине — WARN для неподнятого входа, ERROR для неназванного
  адресата. Заведены transcriber_intake_up и transcriber_undelivered_reply_count.
- Закрыта утечка токена в журнал: отказ разбора адреса рождается раньше
  обращения к клиенту, то есть мимо чистки на его границе.
This commit is contained in:
av
2026-08-13 19:10:08 +03:00
parent 863ba3b42e
commit b733a84d6a
33 changed files with 1579 additions and 91 deletions
+67
View File
@@ -0,0 +1,67 @@
package main
import (
"errors"
"log/slog"
"git.vakhrushev.me/av/transcriber/internal/adapter/telegram"
"git.vakhrushev.me/av/transcriber/internal/contract"
"git.vakhrushev.me/av/transcriber/internal/metrics"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
// buildTelegram заводит вход Telegram при старте. Клиент собирается **один
// раз** и достаётся обоим — отправителю ответов и транспорту бота. Пока его
// строили порознь, два пути одного старта разошлись: один ронял процесс на
// негодном токене, другой терпел, и согласовывать их приходилось руками.
func buildTelegram(botToken string, logger *slog.Logger) (*tgbotapi.BotAPI, contract.TelegramMessageSender, error) {
bot, err := telegram.NewBot(botToken, logger)
return telegramFromBot(bot, err, logger)
}
// telegramFromBot решает, чем обернулась сборка клиента, и это решение —
// единственное содержательное здесь. Оно отделено от самого обращения к
// Telegram намеренно: обращение ходит в сеть и в проверке недоступно, а
// разрез проверять надо, иначе его молча вернут к прежнему виду.
//
// Разрез проходит по тому, **ответил ли Telegram**, и это решение владельца
// от 2026-08-13: недоступность Telegram на старт сервиса не влияет.
//
// - токен не задан — законный отказ от входа: сервис работает оставшимся;
// - Telegram ответил отказом — ошибка настройки: бота по этому токену не
// существует, ждать нечего, и старт роняется. Молча потерянный бот
// перестаёт отвечать отправителям, а узнать об этом было бы неоткуда;
// - до Telegram не дошли — недоступность: сеть, DNS, авария Bot API,
// истёкший срок ожидания. Сервис поднимается без Telegram, потому что
// основной вход у него другой, и класть его из-за чужой аварии нельзя.
//
// Ядро во всех трёх случаях получает непустого отправителя: необязательная
// зависимость, доехавшая до него нулём, уронила бы первую же задачу из
// Telegram.
func telegramFromBot(
bot *tgbotapi.BotAPI,
err error,
logger *slog.Logger,
) (*tgbotapi.BotAPI, contract.TelegramMessageSender, error) {
var apiErr *tgbotapi.Error
switch {
case errors.Is(err, telegram.ErrEmptyToken):
metrics.IntakeUpGauge.WithLabelValues("telegram").Set(0)
logger.Warn("Telegram bot is not started", "reason", "bot token is not set in configuration")
return nil, telegram.NewAbsentMessageSender(), nil
case errors.As(err, &apiErr):
// Отказ токена не несёт: его чистит единая точка `telegram.NewBot`.
return nil, nil, err
case err != nil:
metrics.IntakeUpGauge.WithLabelValues("telegram").Set(0)
logger.Warn("Telegram bot is not started", "reason", "telegram is unreachable", "error", err)
return nil, telegram.NewAbsentMessageSender(), nil
default:
metrics.IntakeUpGauge.WithLabelValues("telegram").Set(1)
return bot, telegram.NewTelegramMessageSender(bot, logger), nil
}
}