# Настраиваемые каналы уведомлений Дата: 2026-06-14 ## Цель Сейчас `http_logger` умеет отправлять входящие HTTP-запросы только в Telegram — канал жёстко зашит в `main.go`. Нужно дать возможность настраивать каналы получения уведомлений через конфиг и добавить два новых: **Pachca (Пачка)** и **Email**, используя библиотеку `gitea.mediatoday.ru/mt/notify`. ## Решения (согласованы с пользователем) 1. **Путь = канал.** Первый сегмент пути URL выбирает канал: `/telegram`, `/pachca`, `/email`. Каждый путь шлёт ровно в свой канал. Сохраняет текущую логику маршрутизации. 2. **Один экземпляр на тип.** В конфиге по одной секции на каждый тип канала. 3. **Форматирование под каждый канал.** Извлечение данных запроса — общее, форматирование — на стороне канала (Telegram — markdown-блок, Email — HTML). 4. **Канал не сконфигурирован → `404`.** 5. Поднять модуль `notify` на актуальную версию по новому пути `gitea.mediatoday.ru/mt/notify` (в старой версии нет Pachca/Email). ## Архитектура ### Структура данных запроса `BuildMessage` разделяется на две части: извлечение данных и форматирование. ```go // RequestData — извлечённые данные входящего запроса type RequestData struct { Time time.Time Method string Host string URL string RemoteAddr string Header http.Header Body string } // ExtractRequest читает запрос один раз (тело с лимитом 1024 байт, как сейчас) func ExtractRequest(r *http.Request) RequestData ``` Логика `GetRemoteAddr` и лимит тела (`> 1024` → `"too long"`) сохраняются. ### Интерфейс канала ```go // Notifier — канал отправки уведомлений type Notifier interface { Send(data RequestData) error } ``` ### Реализации Каждая реализация сама форматирует `RequestData` в свой формат. - **`telegramNotifier{bot *notify.Bot, chatID int64}`** Форматирует тело в ` ``` `-блок (как сейчас), отправляет `bot.SendTextMessage` (ParseMode Markdown). - **`pachcaNotifier{client *notify.Pachca, chatID int64}`** Форматирует в markdown ` ``` `-блок, отправляет `client.SendMessage(chatID, text)`. - **`emailNotifier{auth notify.SmtpAuth, from mail.Address, to []mail.Address, subject string}`** Форматирует тело в `
` с HTML-экранированием содержимого, отправляет `notify.SendEmailHTML(auth, body, subject, from, to...)`. Каждая реализация выносится в отдельный файл (`telegram.go`, `pachca.go`, `email.go`) либо группируется в `notifiers.go` — детали на этапе плана. Цель — не раздувать `main.go`. ### Конфиг Любую секцию можно опустить — тогда соответствующий путь даёт `404`. ```yaml listen_addresses: - ":8080" - "127.0.0.1:9090" log_dir: "logs" telegram: token: "*****" group_id: -10012345 disable_ipv6: true # опционально, по умолчанию true pachca: token: "*****" chat_id: 12345 email: smtp_addr: "smtp.example.com:587" username: "user@example.com" password: "*****" from: "logger@example.com" to: - "alert@example.com" subject: "HTTP Logger" # опционально, по умолчанию "HTTP Logger" ``` Структуры Go: ```go type Config struct { ListenAddresses []string `yaml:"listen_addresses"` LogDir string `yaml:"log_dir"` Telegram *ConfigTelegram `yaml:"telegram"` Pachca *ConfigPachca `yaml:"pachca"` Email *ConfigEmail `yaml:"email"` } type ConfigTelegram struct { Token string `yaml:"token"` GroupID int64 `yaml:"group_id"` DisableIPV6 *bool `yaml:"disable_ipv6"` // nil → true } type ConfigPachca struct { Token string `yaml:"token"` ChatID int64 `yaml:"chat_id"` } type ConfigEmail struct { SMTPAddr string `yaml:"smtp_addr"` Username string `yaml:"username"` Password string `yaml:"password"` From string `yaml:"from"` To []string `yaml:"to"` Subject string `yaml:"subject"` // "" → "HTTP Logger" } ``` Замечание: исходный конфиг содержал опечатку `token:"*****"` (без пробела) и имя типа `ConfigTelegraam` — исправляются в рамках работы. ### Сборка каналов на старте ```go notifiers := map[string]Notifier{} ``` - Для каждой заданной секции конфига строим соответствующий `Notifier` и кладём в map под ключом-именем пути (`"telegram"`, `"pachca"`, `"email"`). - Для email: хост для `smtp.PlainAuth` извлекается из `smtp_addr` через `net.SplitHostPort`; `auth = smtp.PlainAuth("", username, password, host)`. - Если ни одна секция не задана → `log.Fatal("не сконфигурирован ни один канал")`. - Ошибка инициализации конкретного канала (например, `NewTelegram`) → `log.Fatal`. ### Маршрутизация в хендлере ```go paths := strings.Split(r.URL.Path, "/") // "/telegram" → ["", "telegram"] if len(paths) < 2 { 404 } n, ok := notifiers[paths[1]] if !ok { 404 } // неизвестный или несконфигурированный канал data := ExtractRequest(r) if err := n.Send(data); err != nil { log.Println(err); 502 + err.Error() } else { 200 + "OK" } ``` Поведение кодов ответа (`404` / `502` / `200`) сохраняется как в текущей версии. ## Обработка ошибок - Несконфигурированный/неизвестный путь → `404`. - Ошибка отправки в канал → `502` + текст ошибки в теле (как сейчас). - Ошибки конфигурации/инициализации каналов → `log.Fatal` на старте. ## Зависимости - `go.mod`: заменить `git.gm6.ru/icewind/notify` на `gitea.mediatoday.ru/mt/notify v0.0.0-20260405185738-6f5db00fcb34` (доступна в module cache). Обновить `go.sum` через `go mod tidy`. - Новые импорты: `net/mail`, `net/smtp`, `html` (для экранирования HTML email). ## Тестирование - `ExtractRequest` — модульный тест: корректный разбор метода/хоста/URL/заголовков, `GetRemoteAddr` (X-Forwarded-For → X-Real-IP → RemoteAddr), лимит тела `> 1024`. - Форматирование каждого канала — тест, проверяющий обёртку (` ``` `-блок для Telegram/Pachca, `
` + экранирование для Email).
- Маршрутизация хендлера — тест через `httptest`: неизвестный путь → `404`,
  известный канал с моком `Notifier` → `200`, ошибка `Send` → `502`. Интерфейс
  `Notifier` позволяет подменить реальные каналы моком.
- Реальная отправка в Telegram/Pachca/Email не тестируется (внешние сервисы).

## Вне рамок (YAGNI)

- Broadcast «один запрос → все каналы».
- Несколько экземпляров одного типа канала.
- Именованные маршруты с произвольным набором каналов.
- Вложения/фото (библиотека умеет, но текущая задача — текст запроса).