- в секции [telegram] заведён обязательный ключ enabled: умолчания у него нет, файл без него негоден; bot_token стал только ключом доступа и при enabled = false не читается вовсе, а пустой при enabled = true роняет старт - выключенный вход даёт подъём одним входом без единого обращения к Telegram и записью INFO вместо прежнего WARN: это выбор владельца, а не отклонение - отказ разбора файла настроек больше не пересказывает toml — её ParseError несёт в тексте разбираемое значение, и оборванная строка секретного ключа уносила его в журнал; теперь называются путь, строка, столбец и последний ключ
101 lines
6.4 KiB
Go
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
|
|
}
|
|
}
|