Files
transcriber/telegram_build.go
T
av cd57b68215 config: включение Telegram разведено с ключом доступа
- в секции [telegram] заведён обязательный ключ enabled: умолчания у него нет,
  файл без него негоден; bot_token стал только ключом доступа и при
  enabled = false не читается вовсе, а пустой при enabled = true роняет старт
- выключенный вход даёт подъём одним входом без единого обращения к Telegram и
  записью INFO вместо прежнего WARN: это выбор владельца, а не отклонение
- отказ разбора файла настроек больше не пересказывает toml — её ParseError
  несёт в тексте разбираемое значение, и оборванная строка секретного ключа
  уносила его в журнал; теперь называются путь, строка, столбец и последний ключ
2026-08-13 21:46:14 +03:00

101 lines
6.4 KiB
Go

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
}
}