package main import ( "errors" "log/slog" "git.vakhrushev.me/av/transcriber/internal/adapter/telegram" "git.vakhrushev.me/av/transcriber/internal/config" "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(cfg config.TelegramConfig, logger *slog.Logger) (*tgbotapi.BotAPI, contract.TelegramMessageSender, error) { return telegramFromConfig(cfg, telegram.NewBot, logger) } // telegramFromConfig решает, поднимать ли вход вообще, и делает это **до** // всякого обращения к Telegram. Намерение объявляет признак включения; ключ // доступа при выключенном входе не смотрится вовсе. // // Сборка клиента приходит параметром, и это не украшение. Главное утверждение // выключенного входа — «обращения не уходит ни одного», — иначе непроверяемо: // адрес Bot API живёт внутри `telegram.NewBot`, и проверка, судящая по исходу, // осталась бы зелёной и тогда, когда ветка выключенного входа встала **после** // обращения. Прогон с заполненным ключом ушёл бы в живой Telegram боевым // токеном, а заметить это было бы нечем. // // Шов — подпись, а не интерфейс: реализация у него одна, и заводить тип ради // неё значит заводить понятие там, где хватает функции. func telegramFromConfig( cfg config.TelegramConfig, newBot func(string, *slog.Logger) (*tgbotapi.BotAPI, error), logger *slog.Logger, ) (*tgbotapi.BotAPI, contract.TelegramMessageSender, error) { if !cfg.Enabled { metrics.IntakeUpGauge.WithLabelValues("telegram").Set(0) // Уровень «к сведению», а не «может стать проблемой»: это выбор // владельца, а не отклонение. Предупреждение остаётся за тем, чего // владелец не выбирал, — недоступностью Telegram. logger.Info("Telegram bot is not started", "reason", "telegram intake is disabled in configuration") return nil, telegram.NewAbsentMessageSender(), nil } bot, err := newBot(cfg.BotToken, logger) return telegramFromBot(bot, err, logger) } // telegramFromBot решает, чем обернулась сборка клиента, и это решение — // единственное содержательное здесь. Оно отделено от самого обращения к // Telegram намеренно: обращение ходит в сеть и в проверке недоступно, а // разрез проверять надо, иначе его молча вернут к прежнему виду. // // Разрез проходит по тому, **ответил ли Telegram**, и это решение владельца // от 2026-08-13: недоступность Telegram на старт сервиса не влияет. Сюда // доходит только включённый вход: выключенный отсеян выше, до обращения. // // - Telegram ответил отказом либо ключ доступа пуст — ошибка настройки: бота // по такому ключу не существует, ждать нечего, и старт роняется. Молча // потерянный бот перестаёт отвечать отправителям, а узнать об этом было бы // неоткуда; // - до Telegram не дошли — недоступность: сеть, DNS, авария Bot API, // истёкший срок ожидания. Сервис поднимается без Telegram, потому что // основной вход у него другой, и класть его из-за чужой аварии нельзя. // // Ветка пустого ключа по построению недостижима — его ловит проверка настроек // раньше, — но исход у неё **тот же**, что у проверки, и потому она оставлена. // Убрав её, мы отправили бы пустой ключ в общий случай `err != nil`, то есть в // «недоступность»: обход проверки настроек дал бы тихий подъём без бота — ровно // то, против чего написано это изменение. // // Ядро в обоих мягких случаях получает непустого отправителя: необязательная // зависимость, доехавшая до него нулём, уронила бы первую же задачу из // 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), 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 } }