diff --git a/docs/superpowers/specs/2026-06-14-notification-channels-design.md b/docs/superpowers/specs/2026-06-14-notification-channels-design.md new file mode 100644 index 0000000..f289a5d --- /dev/null +++ b/docs/superpowers/specs/2026-06-14-notification-channels-design.md @@ -0,0 +1,198 @@ +# Настраиваемые каналы уведомлений + +Дата: 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 «один запрос → все каналы».
+- Несколько экземпляров одного типа канала.
+- Именованные маршруты с произвольным набором каналов.
+- Вложения/фото (библиотека умеет, но текущая задача — текст запроса).