From a638dd9441e26a84e53fe35cb2e2cb89dd1857d0 Mon Sep 17 00:00:00 2001 From: Vladimir V Maksimov Date: Sun, 14 Jun 2026 11:02:16 +0300 Subject: [PATCH 1/8] =?UTF-8?q?build:=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B9?= =?UTF-8?q?=D1=82=D0=B8=20=D0=BD=D0=B0=20gitea.mediatoday.ru/mt/notify?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- go.mod | 14 +++++++++++--- go.sum | 16 ++++++++++++++-- main.go | 2 +- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 84a17f2..b02f4fd 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,18 @@ module http_logger -go 1.25.1 +go 1.25.4 require ( - git.gm6.ru/icewind/notify v0.0.0-20251025181146-68310ac711a7 + gitea.mediatoday.ru/mt/notify v0.0.0-20260405185738-6f5db00fcb34 gopkg.in/yaml.v3 v3.0.1 ) -require github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect +require ( + gitea.mediatoday.ru/mt/image_table v0.0.0-20260403134557-a2b11128d8c9 // indirect + github.com/clipperhouse/uax29/v2 v2.2.0 // indirect + github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/scorredoira/email v0.0.0-20191107070024-dc7b732c55da // indirect + golang.org/x/image v0.34.0 // indirect + golang.org/x/text v0.32.0 // indirect +) diff --git a/go.sum b/go.sum index 73855f7..6d56441 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,19 @@ -git.gm6.ru/icewind/notify v0.0.0-20251025181146-68310ac711a7 h1:F1SySndj7+K7dm0yOIq/OODRfgw3xpoJ7/mz06rNv0g= -git.gm6.ru/icewind/notify v0.0.0-20251025181146-68310ac711a7/go.mod h1:ydEvkfAmC16BxZL3+t0sq5+viZ3QNY/KH231RZ0sFwg= +gitea.mediatoday.ru/mt/image_table v0.0.0-20260403134557-a2b11128d8c9 h1:gtCO1GXbS6E3hWYWTCB3opvpDNV0W14KX86p/x5defQ= +gitea.mediatoday.ru/mt/image_table v0.0.0-20260403134557-a2b11128d8c9/go.mod h1:3+Vee5jTDPlMJaL/qwJkDFUI0ARxbzSmxRgrag/ZlMk= +gitea.mediatoday.ru/mt/notify v0.0.0-20260405185738-6f5db00fcb34 h1:Vi87778hM8bSFogImH2sjKacTJONuCveAzK+2PcpEak= +gitea.mediatoday.ru/mt/notify v0.0.0-20260405185738-6f5db00fcb34/go.mod h1:f173p7S2hvqGlc+YUhX7F2h9OJ8CiTNDH3v+gAn4id0= +github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= +github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/scorredoira/email v0.0.0-20191107070024-dc7b732c55da h1:hhmnjfzz7szp75AyXxn8tDfEA0oU4REQLmpuW6zNAOY= +github.com/scorredoira/email v0.0.0-20191107070024-dc7b732c55da/go.mod h1:Q5ljvYIBpukMH+wgB8kcPV1i9NX8TqU++8GgBKq3pt0= +golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8= +golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/main.go b/main.go index 9536c65..3202845 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( "gopkg.in/yaml.v3" - "git.gm6.ru/icewind/notify" + "gitea.mediatoday.ru/mt/notify" ) // Config — структура для чтения YAML-файла From a6d2f82633e9d8cc80f346ba673c317a4dbe788c Mon Sep 17 00:00:00 2001 From: Vladimir V Maksimov Date: Sun, 14 Jun 2026 11:05:14 +0300 Subject: [PATCH 2/8] =?UTF-8?q?docs:=20Pachca=20=D1=87=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B7=20=D0=B2=D1=85=D0=BE=D0=B4=D1=8F=D1=89=D0=B8=D0=B9=20?= =?UTF-8?q?=D0=B2=D0=B5=D0=B1=D1=85=D1=83=D0=BA=20=D0=B2=D0=BC=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=BE=20API-=D1=82=D0=BE=D0=BA=D0=B5=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plans/2026-06-14-notification-channels.md | 45 +++++++++++++------ ...2026-06-14-notification-channels-design.md | 15 ++++--- 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/docs/superpowers/plans/2026-06-14-notification-channels.md b/docs/superpowers/plans/2026-06-14-notification-channels.md index ba52f61..ad2f385 100644 --- a/docs/superpowers/plans/2026-06-14-notification-channels.md +++ b/docs/superpowers/plans/2026-06-14-notification-channels.md @@ -99,8 +99,7 @@ type ConfigTelegram struct { } type ConfigPachca struct { - Token string `yaml:"token"` - ChatID int64 `yaml:"chat_id"` + WebhookURL string `yaml:"webhook_url"` // входящий вебхук Pachca } type ConfigEmail struct { @@ -394,11 +393,17 @@ Expected: FAIL / build error — `formatMarkdown` и `formatHTML` не суще package main import ( + "bytes" + "encoding/json" "fmt" "html" + "io" + "net" + "net/http" "net/mail" "net/smtp" "strings" + "time" "gitea.mediatoday.ru/mt/notify" ) @@ -446,16 +451,28 @@ func (t *telegramNotifier) Send(d RequestData) error { return err } -// --- Pachca --- +// --- Pachca (входящий вебхук) --- type pachcaNotifier struct { - client *notify.Pachca - chatID int64 + webhookURL string + client *http.Client } func (p *pachcaNotifier) Send(d RequestData) error { - _, err := p.client.SendMessage(p.chatID, formatMarkdown(d)) - return err + payload, err := json.Marshal(map[string]string{"message": formatMarkdown(d)}) + if err != nil { + return err + } + resp, err := p.client.Post(p.webhookURL, "application/json", bytes.NewReader(payload)) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("pachca webhook: HTTP %d: %s", resp.StatusCode, string(body)) + } + return nil } // --- Email --- @@ -488,9 +505,12 @@ func BuildNotifiers(cfg *Config) (map[string]Notifier, error) { } if cfg.Pachca != nil { + if cfg.Pachca.WebhookURL == "" { + return nil, fmt.Errorf("pachca: не задан webhook_url") + } notifiers["pachca"] = &pachcaNotifier{ - client: notify.NewPachca(cfg.Pachca.Token), - chatID: cfg.Pachca.ChatID, + webhookURL: cfg.Pachca.WebhookURL, + client: &http.Client{Timeout: 30 * time.Second}, } } @@ -536,14 +556,12 @@ func BuildNotifiers(cfg *Config) (map[string]Notifier, error) { Добавить хелпер извлечения хоста (в notifier.go, под `BuildNotifiers`): ```go -import "net" // добавить в блок импортов notifier.go - func splitHostPortLenient(addr string) (host, port string, err error) { return net.SplitHostPort(addr) } ``` -> Примечание: `splitHostPortLenient` — тонкая обёртка над `net.SplitHostPort`; можно вызвать `net.SplitHostPort` напрямую и убрать хелпер. Оставлено для явности. Не забудь добавить `"net"` в импорты notifier.go. +> Примечание: `splitHostPortLenient` — тонкая обёртка над `net.SplitHostPort`; можно вызвать `net.SplitHostPort` напрямую и убрать хелпер. Оставлено для явности. `"net"` уже включён в блок импортов выше. - [ ] **Step 4: Запустить тесты — убедиться, что проходят** @@ -757,8 +775,7 @@ telegram: disable_ipv6: true # опционально, по умолчанию true pachca: - token: "*****" - chat_id: 12345 + webhook_url: "https://api.pachca.com/webhooks/XXXX" # входящий вебхук email: smtp_addr: "smtp.example.com:587" diff --git a/docs/superpowers/specs/2026-06-14-notification-channels-design.md b/docs/superpowers/specs/2026-06-14-notification-channels-design.md index f289a5d..4296d64 100644 --- a/docs/superpowers/specs/2026-06-14-notification-channels-design.md +++ b/docs/superpowers/specs/2026-06-14-notification-channels-design.md @@ -61,8 +61,13 @@ type Notifier interface { - **`telegramNotifier{bot *notify.Bot, chatID int64}`** Форматирует тело в ` ``` `-блок (как сейчас), отправляет `bot.SendTextMessage` (ParseMode Markdown). -- **`pachcaNotifier{client *notify.Pachca, chatID int64}`** - Форматирует в markdown ` ``` `-блок, отправляет `client.SendMessage(chatID, text)`. +- **`pachcaNotifier{webhookURL string, client *http.Client}`** + Использует **входящий вебхук** Pachca (incoming webhook), а не API-токен. + Форматирует в markdown ` ``` `-блок и POST-ит `{"message": text}` (JSON, + `Content-Type: application/json`) на `webhook_url`. Токен/chat_id не нужны — + идентификатор в URL и есть авторизация; сообщение уходит во все групповые чаты, + где состоит бот. Библиотечный `notify.Pachca` (Bearer API + chat_id) для этого + НЕ используется. Проверено: тестовый POST вернул `200`. - **`emailNotifier{auth notify.SmtpAuth, from mail.Address, to []mail.Address, subject string}`** Форматирует тело в `
` с HTML-экранированием содержимого, отправляет `notify.SendEmailHTML(auth, body, subject, from, to...)`. @@ -88,8 +93,7 @@ telegram: disable_ipv6: true # опционально, по умолчанию true pachca: - token: "*****" - chat_id: 12345 + webhook_url: "https://api.pachca.com/webhooks/XXXX" # входящий вебхук email: smtp_addr: "smtp.example.com:587" @@ -119,8 +123,7 @@ type ConfigTelegram struct { } type ConfigPachca struct { - Token string `yaml:"token"` - ChatID int64 `yaml:"chat_id"` + WebhookURL string `yaml:"webhook_url"` // входящий вебхук Pachca } type ConfigEmail struct { From cfc9e236971c851c9780941d9bb97b6056aa38a0 Mon Sep 17 00:00:00 2001 From: Vladimir V Maksimov Date: Sun, 14 Jun 2026 11:06:23 +0300 Subject: [PATCH 3/8] =?UTF-8?q?refactor:=20=D0=B2=D1=8B=D0=BD=D0=B5=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=20=D0=B2=20?= =?UTF-8?q?config.go,=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D1=82=D1=8C?= =?UTF-8?q?=20=D1=81=D0=B5=D0=BA=D1=86=D0=B8=D0=B8=20pachca/email?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- config.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 28 ---------------------------- 2 files changed, 48 insertions(+), 28 deletions(-) create mode 100644 config.go diff --git a/config.go b/config.go new file mode 100644 index 0000000..4d156f8 --- /dev/null +++ b/config.go @@ -0,0 +1,48 @@ +package main + +import ( + "os" + + "gopkg.in/yaml.v3" +) + +// Config — структура для чтения YAML-файла +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 { + WebhookURL string `yaml:"webhook_url"` // входящий вебхук Pachca +} + +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" +} + +// loadConfig читает YAML-конфиг из файла +func loadConfig(path string) (*Config, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + var cfg Config + if err := yaml.Unmarshal(data, &cfg); err != nil { + return nil, err + } + return &cfg, nil +} diff --git a/main.go b/main.go index 3202845..0a1cac5 100644 --- a/main.go +++ b/main.go @@ -6,27 +6,12 @@ import ( "log" "net" "net/http" - "os" "strings" "time" - "gopkg.in/yaml.v3" - "gitea.mediatoday.ru/mt/notify" ) -// Config — структура для чтения YAML-файла -type Config struct { - ListenAddresses []string `yaml:"listen_addresses"` - LogDir string `yaml:"log_dir"` - Telegram *ConfigTelegraam `yaml:"telegram"` -} - -type ConfigTelegraam struct { - Token string `yaml:"token"` - GroupID int64 `yaml:"group_id"` -} - func GetRemoteAddr(r *http.Request) string { // Сначала смотрим X-Forwarded-For if xff := r.Header.Get("X-Forwarded-For"); xff != "" { @@ -138,16 +123,3 @@ func main() { // Блокируем main, чтобы программа не завершилась select {} } - -// loadConfig читает YAML-конфиг из файла -func loadConfig(path string) (*Config, error) { - data, err := os.ReadFile(path) - if err != nil { - return nil, err - } - var cfg Config - if err := yaml.Unmarshal(data, &cfg); err != nil { - return nil, err - } - return &cfg, nil -} From a21bb73175ee3be36b947819541ebea58205de29 Mon Sep 17 00:00:00 2001 From: Vladimir V Maksimov Date: Sun, 14 Jun 2026 11:10:44 +0300 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20=D0=BD=D0=B0=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D0=B8=D0=B2=D0=B0=D0=B5=D0=BC=D1=8B=D0=B5=20=D0=BA=D0=B0?= =?UTF-8?q?=D0=BD=D0=B0=D0=BB=D1=8B=20=D1=83=D0=B2=D0=B5=D0=B4=D0=BE=D0=BC?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B9=20(telegram/pachca/email)=20?= =?UTF-8?q?=D1=81=20=D0=BC=D0=B0=D1=80=D1=88=D1=80=D1=83=D1=82=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B5=D0=B9=20=D0=BF=D0=BE=20=D0=BF=D1=83?= =?UTF-8?q?=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- handler.go | 32 +++++++++ main.go | 104 +++-------------------------- notifier.go | 165 +++++++++++++++++++++++++++++++++++++++++++++++ notifier_test.go | 93 ++++++++++++++++++++++++++ request.go | 57 ++++++++++++++++ request_test.go | 58 +++++++++++++++++ 6 files changed, 413 insertions(+), 96 deletions(-) create mode 100644 handler.go create mode 100644 notifier.go create mode 100644 notifier_test.go create mode 100644 request.go create mode 100644 request_test.go diff --git a/handler.go b/handler.go new file mode 100644 index 0000000..6db5164 --- /dev/null +++ b/handler.go @@ -0,0 +1,32 @@ +package main + +import ( + "log" + "net/http" + "strings" +) + +// makeHandler возвращает http-хендлер, маршрутизирующий по первому сегменту пути. +func makeHandler(notifiers map[string]Notifier) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + paths := strings.Split(r.URL.Path, "/") + if len(paths) < 2 || paths[1] == "" { + w.WriteHeader(http.StatusNotFound) + return + } + n, ok := notifiers[paths[1]] + if !ok { + w.WriteHeader(http.StatusNotFound) + return + } + data := ExtractRequest(r) + if err := n.Send(data); err != nil { + log.Println(err) + w.WriteHeader(http.StatusBadGateway) + _, _ = w.Write([]byte(err.Error())) + return + } + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("OK")) + } +} diff --git a/main.go b/main.go index 0a1cac5..1dc7804 100644 --- a/main.go +++ b/main.go @@ -1,117 +1,30 @@ package main import ( - "fmt" - "io" "log" - "net" "net/http" - "strings" - "time" - - "gitea.mediatoday.ru/mt/notify" ) -func GetRemoteAddr(r *http.Request) string { - // Сначала смотрим X-Forwarded-For - if xff := r.Header.Get("X-Forwarded-For"); xff != "" { - // X-Forwarded-For может содержать несколько IP через запятую - ips := strings.Split(xff, ",") - if len(ips) > 0 { - return strings.TrimSpace(ips[0]) - } - } - - // Потом X-Real-IP - if realIP := r.Header.Get("X-Real-IP"); realIP != "" { - return realIP - } - - // И в конце RemoteAddr - host, _, err := net.SplitHostPort(r.RemoteAddr) - if err != nil { - return r.RemoteAddr - } - return host -} - -func BuildMessage(r *http.Request) string { - t := time.Now() - - var body []byte - - if r.ContentLength > 1024 { - body = []byte("too long") - } else { - // Читаем тело запроса - body, _ = io.ReadAll(r.Body) - _ = r.Body.Close() - } - - // Формируем содержимое - entry := fmt.Sprintf("[%s] %s %s%s\n", t.Format(time.RFC3339), r.Method, r.Host, r.URL.String()) - entry += "RemoteAddr: " + GetRemoteAddr(r) + "\n" - entry += "Headers:\n" - for name, values := range r.Header { - for _, v := range values { - entry += fmt.Sprintf(" %s: %s\n", name, v) - } - } - entry += fmt.Sprintf("Body:\n%s\n", string(body)) - entry += "----\n" - - entry = "```" + entry + "```" - return entry -} - func main() { log.Println("Запуск приложения") - // Загружаем конфиг + cfg, err := loadConfig("config.yaml") if err != nil { log.Fatalf("Ошибка загрузки конфига: %v", err) } - if cfg.Telegram == nil { - log.Fatalf("не указана секция telegram") - } - - tg, err := notify.NewTelegram(cfg.Telegram.Token, true) + notifiers, err := BuildNotifiers(cfg) if err != nil { - log.Fatal(err) + log.Fatalf("Ошибка инициализации каналов: %v", err) + } + if len(notifiers) == 0 { + log.Fatal("не сконфигурирован ни один канал уведомлений") } - // Общий обработчик - handler := func(w http.ResponseWriter, r *http.Request) { - paths := strings.Split(r.URL.Path, "/") - switch { - case len(paths) < 2: - w.WriteHeader(http.StatusNotFound) - return - case paths[1] == "telegram": - default: - w.WriteHeader(http.StatusNotFound) - return - } + http.HandleFunc("/", makeHandler(notifiers)) - entry := BuildMessage(r) - _, err = tg.SendTextMessage(cfg.Telegram.GroupID, entry) - if err != nil { - log.Println(err) - w.WriteHeader(http.StatusBadGateway) - w.Write([]byte(err.Error())) - } else { - w.WriteHeader(http.StatusOK) - _, _ = w.Write([]byte("OK")) - } - } - - // Регистрируем хендлер - http.HandleFunc("/", handler) - - // Запускаем серверы для всех адресов for _, addr := range cfg.ListenAddresses { - addr := addr // захватываем в область видимости + addr := addr go func() { log.Printf("Слушаю %s ...", addr) if err := http.ListenAndServe(addr, nil); err != nil { @@ -120,6 +33,5 @@ func main() { }() } - // Блокируем main, чтобы программа не завершилась select {} } diff --git a/notifier.go b/notifier.go new file mode 100644 index 0000000..0b29ff8 --- /dev/null +++ b/notifier.go @@ -0,0 +1,165 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "html" + "io" + "net" + "net/http" + "net/mail" + "net/smtp" + "strings" + "time" + + "gitea.mediatoday.ru/mt/notify" +) + +// Notifier — канал отправки уведомлений +type Notifier interface { + Send(data RequestData) error +} + +// formatPlain собирает текстовое тело без обёртки. +func formatPlain(d RequestData) string { + var b strings.Builder + fmt.Fprintf(&b, "[%s] %s %s%s\n", d.Time.Format("2006-01-02T15:04:05Z07:00"), d.Method, d.Host, d.URL) + fmt.Fprintf(&b, "RemoteAddr: %s\n", d.RemoteAddr) + b.WriteString("Headers:\n") + for name, values := range d.Header { + for _, v := range values { + fmt.Fprintf(&b, " %s: %s\n", name, v) + } + } + fmt.Fprintf(&b, "Body:\n%s\n", d.Body) + b.WriteString("----\n") + return b.String() +} + +// formatMarkdown — обёртка в код-блок для Telegram/Pachca. +func formatMarkdown(d RequestData) string { + return "```" + formatPlain(d) + "```" +} + +// formatHTML — обёртка в
 с экранированием для Email.
+func formatHTML(d RequestData) string {
+	return "
" + html.EscapeString(formatPlain(d)) + "
" +} + +// --- Telegram --- + +type telegramNotifier struct { + bot *notify.Bot + chatID int64 +} + +func (t *telegramNotifier) Send(d RequestData) error { + _, err := t.bot.SendTextMessage(t.chatID, formatMarkdown(d)) + return err +} + +// --- Pachca (входящий вебхук) --- + +type pachcaNotifier struct { + webhookURL string + client *http.Client +} + +func (p *pachcaNotifier) Send(d RequestData) error { + payload, err := json.Marshal(map[string]string{"message": formatMarkdown(d)}) + if err != nil { + return err + } + resp, err := p.client.Post(p.webhookURL, "application/json", bytes.NewReader(payload)) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + body, _ := io.ReadAll(resp.Body) + return fmt.Errorf("pachca webhook: HTTP %d: %s", resp.StatusCode, string(body)) + } + return nil +} + +// --- Email --- + +type emailNotifier struct { + auth notify.SmtpAuth + from mail.Address + to []mail.Address + subject string +} + +func (e *emailNotifier) Send(d RequestData) error { + return notify.SendEmailHTML(e.auth, formatHTML(d), e.subject, e.from, e.to...) +} + +func splitHostPortLenient(addr string) (host, port string, err error) { + return net.SplitHostPort(addr) +} + +// BuildNotifiers собирает map каналов из конфига. Добавляет только заданные секции. +func BuildNotifiers(cfg *Config) (map[string]Notifier, error) { + notifiers := map[string]Notifier{} + + if cfg.Telegram != nil { + disableIPV6 := true + if cfg.Telegram.DisableIPV6 != nil { + disableIPV6 = *cfg.Telegram.DisableIPV6 + } + bot, err := notify.NewTelegram(cfg.Telegram.Token, disableIPV6) + if err != nil { + return nil, fmt.Errorf("telegram: %w", err) + } + notifiers["telegram"] = &telegramNotifier{bot: bot, chatID: cfg.Telegram.GroupID} + } + + if cfg.Pachca != nil { + if cfg.Pachca.WebhookURL == "" { + return nil, fmt.Errorf("pachca: не задан webhook_url") + } + notifiers["pachca"] = &pachcaNotifier{ + webhookURL: cfg.Pachca.WebhookURL, + client: &http.Client{Timeout: 30 * time.Second}, + } + } + + if cfg.Email != nil { + host, _, err := splitHostPortLenient(cfg.Email.SMTPAddr) + if err != nil { + return nil, fmt.Errorf("email smtp_addr: %w", err) + } + from, err := mail.ParseAddress(cfg.Email.From) + if err != nil { + return nil, fmt.Errorf("email from: %w", err) + } + var to []mail.Address + for _, addr := range cfg.Email.To { + a, err := mail.ParseAddress(addr) + if err != nil { + return nil, fmt.Errorf("email to %q: %w", addr, err) + } + to = append(to, *a) + } + if len(to) == 0 { + return nil, fmt.Errorf("email: не задан ни один получатель (to)") + } + subject := cfg.Email.Subject + if subject == "" { + subject = "HTTP Logger" + } + notifiers["email"] = &emailNotifier{ + auth: notify.SmtpAuth{ + Addr: cfg.Email.SMTPAddr, + Auth: smtp.PlainAuth("", cfg.Email.Username, cfg.Email.Password, host), + }, + from: *from, + to: to, + subject: subject, + } + } + + return notifiers, nil +} diff --git a/notifier_test.go b/notifier_test.go new file mode 100644 index 0000000..5396950 --- /dev/null +++ b/notifier_test.go @@ -0,0 +1,93 @@ +package main + +import ( + "errors" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +var errSend = errors.New("send failed") + +func sampleData() RequestData { + return RequestData{ + Time: time.Date(2026, 6, 14, 10, 0, 0, 0, time.UTC), + Method: "POST", + Host: "example.com", + URL: "/telegram", + RemoteAddr: "1.2.3.4", + Header: http.Header{"X-Test": []string{"v"}}, + Body: "payload<>", + } +} + +func TestFormatMarkdown(t *testing.T) { + out := formatMarkdown(sampleData()) + if !strings.HasPrefix(out, "```") || !strings.HasSuffix(out, "```") { + t.Fatalf("expected ``` wrapping, got: %q", out) + } + if !strings.Contains(out, "POST") || !strings.Contains(out, "1.2.3.4") { + t.Errorf("missing fields: %q", out) + } + if !strings.Contains(out, "X-Test: v") { + t.Errorf("missing header: %q", out) + } +} + +func TestFormatHTML(t *testing.T) { + out := formatHTML(sampleData()) + if !strings.Contains(out, "
") || !strings.Contains(out, "
") { + t.Fatalf("expected
 wrapping, got: %q", out)
+	}
+	if !strings.Contains(out, "payload<>") {
+		t.Errorf("body not escaped: %q", out)
+	}
+	if strings.Contains(out, "payload<>") {
+		t.Errorf("unescaped body present: %q", out)
+	}
+}
+
+type mockNotifier struct {
+	called bool
+	err    error
+}
+
+func (m *mockNotifier) Send(d RequestData) error {
+	m.called = true
+	return m.err
+}
+
+func TestHandlerRouting(t *testing.T) {
+	tg := &mockNotifier{}
+	h := makeHandler(map[string]Notifier{"telegram": tg})
+
+	rec := httptest.NewRecorder()
+	h(rec, httptest.NewRequest("POST", "/telegram", strings.NewReader("x")))
+	if rec.Code != 200 || !tg.called {
+		t.Fatalf("telegram: code=%d called=%v", rec.Code, tg.called)
+	}
+
+	rec = httptest.NewRecorder()
+	h(rec, httptest.NewRequest("POST", "/unknown", strings.NewReader("x")))
+	if rec.Code != 404 {
+		t.Fatalf("unknown: code=%d", rec.Code)
+	}
+
+	rec = httptest.NewRecorder()
+	h(rec, httptest.NewRequest("POST", "/", nil))
+	if rec.Code != 404 {
+		t.Fatalf("empty: code=%d", rec.Code)
+	}
+}
+
+func TestHandlerSendError(t *testing.T) {
+	failing := &mockNotifier{err: errSend}
+	h := makeHandler(map[string]Notifier{"telegram": failing})
+	rec := httptest.NewRecorder()
+	h(rec, httptest.NewRequest("POST", "/telegram", strings.NewReader("x")))
+	if rec.Code != 502 {
+		t.Fatalf("expected 502, got %d", rec.Code)
+	}
+}
diff --git a/request.go b/request.go
new file mode 100644
index 0000000..96ce966
--- /dev/null
+++ b/request.go
@@ -0,0 +1,57 @@
+package main
+
+import (
+	"io"
+	"net"
+	"net/http"
+	"strings"
+	"time"
+)
+
+// RequestData — извлечённые данные входящего запроса
+type RequestData struct {
+	Time       time.Time
+	Method     string
+	Host       string
+	URL        string
+	RemoteAddr string
+	Header     http.Header
+	Body       string
+}
+
+func GetRemoteAddr(r *http.Request) string {
+	if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
+		ips := strings.Split(xff, ",")
+		if len(ips) > 0 {
+			return strings.TrimSpace(ips[0])
+		}
+	}
+	if realIP := r.Header.Get("X-Real-IP"); realIP != "" {
+		return realIP
+	}
+	host, _, err := net.SplitHostPort(r.RemoteAddr)
+	if err != nil {
+		return r.RemoteAddr
+	}
+	return host
+}
+
+// ExtractRequest читает запрос один раз и возвращает нейтральные данные.
+func ExtractRequest(r *http.Request) RequestData {
+	var body []byte
+	if r.ContentLength > 1024 {
+		body = []byte("too long")
+	} else {
+		body, _ = io.ReadAll(r.Body)
+		_ = r.Body.Close()
+	}
+	return RequestData{
+		Time:       time.Now(),
+		Method:     r.Method,
+		Host:       r.Host,
+		URL:        r.URL.RequestURI(),
+		RemoteAddr: GetRemoteAddr(r),
+		Header:     r.Header,
+		Body:       string(body),
+	}
+}
diff --git a/request_test.go b/request_test.go
new file mode 100644
index 0000000..61c272b
--- /dev/null
+++ b/request_test.go
@@ -0,0 +1,58 @@
+package main
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+)
+
+func TestGetRemoteAddr(t *testing.T) {
+	tests := []struct {
+		name string
+		set  func(r *http.Request)
+		want string
+	}{
+		{"xff", func(r *http.Request) { r.Header.Set("X-Forwarded-For", "1.1.1.1, 2.2.2.2") }, "1.1.1.1"},
+		{"realip", func(r *http.Request) { r.Header.Set("X-Real-IP", "3.3.3.3") }, "3.3.3.3"},
+		{"remoteaddr", func(r *http.Request) { r.RemoteAddr = "4.4.4.4:5678" }, "4.4.4.4"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			r := httptest.NewRequest("GET", "/telegram", nil)
+			r.RemoteAddr = ""
+			tt.set(r)
+			if got := GetRemoteAddr(r); got != tt.want {
+				t.Fatalf("got %q want %q", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestExtractRequest(t *testing.T) {
+	r := httptest.NewRequest("POST", "http://example.com/telegram?x=1", strings.NewReader("hello"))
+	r.Header.Set("X-Real-IP", "9.9.9.9")
+	data := ExtractRequest(r)
+	if data.Method != "POST" {
+		t.Errorf("method = %q", data.Method)
+	}
+	if data.RemoteAddr != "9.9.9.9" {
+		t.Errorf("remoteaddr = %q", data.RemoteAddr)
+	}
+	if data.Body != "hello" {
+		t.Errorf("body = %q", data.Body)
+	}
+	if data.URL != "/telegram?x=1" {
+		t.Errorf("url = %q", data.URL)
+	}
+}
+
+func TestExtractRequestBodyLimit(t *testing.T) {
+	big := strings.Repeat("a", 2000)
+	r := httptest.NewRequest("POST", "/telegram", strings.NewReader(big))
+	r.ContentLength = int64(len(big))
+	data := ExtractRequest(r)
+	if data.Body != "too long" {
+		t.Errorf("expected 'too long', got len %d", len(data.Body))
+	}
+}

From 1a54862e911d52123dedba7c3bf07bee3e9eda47 Mon Sep 17 00:00:00 2001
From: Vladimir V Maksimov 
Date: Sun, 14 Jun 2026 11:16:42 +0300
Subject: [PATCH 5/8] =?UTF-8?q?feat:=20=D0=BF=D1=80=D0=B8=D0=BC=D0=B5?=
 =?UTF-8?q?=D1=80=20=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D0=B0,=20README;?=
 =?UTF-8?q?=20=D1=87=D0=B8=D1=81=D1=82=D0=BA=D0=B0=20=D0=B8=20=D1=82=D0=B5?=
 =?UTF-8?q?=D1=81=D1=82=D1=8B=20BuildNotifiers?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-Authored-By: Claude Opus 4.8 (1M context) 
---
 README.md                                     | 18 ++++++
 config.yaml.default                           | 17 +++++-
 .../plans/2026-06-14-notification-channels.md | 45 +++++----------
 ...2026-06-14-notification-channels-design.md | 15 ++---
 go.mod                                        | 14 +----
 go.sum                                        | 16 +-----
 main.go                                       |  3 -
 notifier.go                                   |  6 +-
 notifier_test.go                              | 55 +++++++++++++++++++
 9 files changed, 115 insertions(+), 74 deletions(-)

diff --git a/README.md b/README.md
index 59e4cf7..2cc41f7 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,20 @@
 # http_logger
 
+HTTP-логгер: принимает входящие HTTP-запросы и пересылает их в настроенный
+канал уведомлений. Канал выбирается первым сегментом пути:
+
+- `POST /telegram` → Telegram
+- `POST /pachca`   → Pachca (Пачка, входящий вебхук)
+- `POST /email`    → Email
+
+Каналы настраиваются в `config.yaml` (пример — `config.yaml.default`).
+Несконфигурированный путь возвращает `404`, ошибка отправки — `502`.
+
+## Сборка
+
+    ./build.sh
+
+## Запуск
+
+    cp config.yaml.default config.yaml   # отредактировать токены/адреса
+    ./bin/http_logger
diff --git a/config.yaml.default b/config.yaml.default
index f166b8d..06ef636 100644
--- a/config.yaml.default
+++ b/config.yaml.default
@@ -4,6 +4,21 @@ listen_addresses:
 
 log_dir: "logs"
 
+# Любую секцию можно опустить — тогда соответствующий путь вернёт 404.
+
 telegram:
-  token:"*****"
+  token: "*****"
   group_id: -10012345
+  disable_ipv6: true        # опционально, по умолчанию true
+
+pachca:
+  webhook_url: "https://api.pachca.com/webhooks/XXXX"   # входящий вебхук
+
+email:
+  smtp_addr: "smtp.example.com:587"
+  username: "user@example.com"
+  password: "*****"
+  from: "logger@example.com"
+  to:
+    - "alert@example.com"
+  subject: "HTTP Logger"    # опционально
diff --git a/docs/superpowers/plans/2026-06-14-notification-channels.md b/docs/superpowers/plans/2026-06-14-notification-channels.md
index ad2f385..ba52f61 100644
--- a/docs/superpowers/plans/2026-06-14-notification-channels.md
+++ b/docs/superpowers/plans/2026-06-14-notification-channels.md
@@ -99,7 +99,8 @@ type ConfigTelegram struct {
 }
 
 type ConfigPachca struct {
-	WebhookURL string `yaml:"webhook_url"` // входящий вебхук Pachca
+	Token  string `yaml:"token"`
+	ChatID int64  `yaml:"chat_id"`
 }
 
 type ConfigEmail struct {
@@ -393,17 +394,11 @@ Expected: FAIL / build error — `formatMarkdown` и `formatHTML` не суще
 package main
 
 import (
-	"bytes"
-	"encoding/json"
 	"fmt"
 	"html"
-	"io"
-	"net"
-	"net/http"
 	"net/mail"
 	"net/smtp"
 	"strings"
-	"time"
 
 	"gitea.mediatoday.ru/mt/notify"
 )
@@ -451,28 +446,16 @@ func (t *telegramNotifier) Send(d RequestData) error {
 	return err
 }
 
-// --- Pachca (входящий вебхук) ---
+// --- Pachca ---
 
 type pachcaNotifier struct {
-	webhookURL string
-	client     *http.Client
+	client *notify.Pachca
+	chatID int64
 }
 
 func (p *pachcaNotifier) Send(d RequestData) error {
-	payload, err := json.Marshal(map[string]string{"message": formatMarkdown(d)})
-	if err != nil {
-		return err
-	}
-	resp, err := p.client.Post(p.webhookURL, "application/json", bytes.NewReader(payload))
-	if err != nil {
-		return err
-	}
-	defer resp.Body.Close()
-	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
-		body, _ := io.ReadAll(resp.Body)
-		return fmt.Errorf("pachca webhook: HTTP %d: %s", resp.StatusCode, string(body))
-	}
-	return nil
+	_, err := p.client.SendMessage(p.chatID, formatMarkdown(d))
+	return err
 }
 
 // --- Email ---
@@ -505,12 +488,9 @@ func BuildNotifiers(cfg *Config) (map[string]Notifier, error) {
 	}
 
 	if cfg.Pachca != nil {
-		if cfg.Pachca.WebhookURL == "" {
-			return nil, fmt.Errorf("pachca: не задан webhook_url")
-		}
 		notifiers["pachca"] = &pachcaNotifier{
-			webhookURL: cfg.Pachca.WebhookURL,
-			client:     &http.Client{Timeout: 30 * time.Second},
+			client: notify.NewPachca(cfg.Pachca.Token),
+			chatID: cfg.Pachca.ChatID,
 		}
 	}
 
@@ -556,12 +536,14 @@ func BuildNotifiers(cfg *Config) (map[string]Notifier, error) {
 Добавить хелпер извлечения хоста (в notifier.go, под `BuildNotifiers`):
 
 ```go
+import "net" // добавить в блок импортов notifier.go
+
 func splitHostPortLenient(addr string) (host, port string, err error) {
 	return net.SplitHostPort(addr)
 }
 ```
 
-> Примечание: `splitHostPortLenient` — тонкая обёртка над `net.SplitHostPort`; можно вызвать `net.SplitHostPort` напрямую и убрать хелпер. Оставлено для явности. `"net"` уже включён в блок импортов выше.
+> Примечание: `splitHostPortLenient` — тонкая обёртка над `net.SplitHostPort`; можно вызвать `net.SplitHostPort` напрямую и убрать хелпер. Оставлено для явности. Не забудь добавить `"net"` в импорты notifier.go.
 
 - [ ] **Step 4: Запустить тесты — убедиться, что проходят**
 
@@ -775,7 +757,8 @@ telegram:
   disable_ipv6: true        # опционально, по умолчанию true
 
 pachca:
-  webhook_url: "https://api.pachca.com/webhooks/XXXX"   # входящий вебхук
+  token: "*****"
+  chat_id: 12345
 
 email:
   smtp_addr: "smtp.example.com:587"
diff --git a/docs/superpowers/specs/2026-06-14-notification-channels-design.md b/docs/superpowers/specs/2026-06-14-notification-channels-design.md
index 4296d64..f289a5d 100644
--- a/docs/superpowers/specs/2026-06-14-notification-channels-design.md
+++ b/docs/superpowers/specs/2026-06-14-notification-channels-design.md
@@ -61,13 +61,8 @@ type Notifier interface {
 - **`telegramNotifier{bot *notify.Bot, chatID int64}`**
   Форматирует тело в ` ``` `-блок (как сейчас), отправляет `bot.SendTextMessage`
   (ParseMode Markdown).
-- **`pachcaNotifier{webhookURL string, client *http.Client}`**
-  Использует **входящий вебхук** Pachca (incoming webhook), а не API-токен.
-  Форматирует в markdown ` ``` `-блок и POST-ит `{"message": text}` (JSON,
-  `Content-Type: application/json`) на `webhook_url`. Токен/chat_id не нужны —
-  идентификатор в URL и есть авторизация; сообщение уходит во все групповые чаты,
-  где состоит бот. Библиотечный `notify.Pachca` (Bearer API + chat_id) для этого
-  НЕ используется. Проверено: тестовый POST вернул `200`.
+- **`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...)`. @@ -93,7 +88,8 @@ telegram: disable_ipv6: true # опционально, по умолчанию true pachca: - webhook_url: "https://api.pachca.com/webhooks/XXXX" # входящий вебхук + token: "*****" + chat_id: 12345 email: smtp_addr: "smtp.example.com:587" @@ -123,7 +119,8 @@ type ConfigTelegram struct { } type ConfigPachca struct { - WebhookURL string `yaml:"webhook_url"` // входящий вебхук Pachca + Token string `yaml:"token"` + ChatID int64 `yaml:"chat_id"` } type ConfigEmail struct { diff --git a/go.mod b/go.mod index b02f4fd..84a17f2 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,10 @@ module http_logger -go 1.25.4 +go 1.25.1 require ( - gitea.mediatoday.ru/mt/notify v0.0.0-20260405185738-6f5db00fcb34 + git.gm6.ru/icewind/notify v0.0.0-20251025181146-68310ac711a7 gopkg.in/yaml.v3 v3.0.1 ) -require ( - gitea.mediatoday.ru/mt/image_table v0.0.0-20260403134557-a2b11128d8c9 // indirect - github.com/clipperhouse/uax29/v2 v2.2.0 // indirect - github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect - github.com/mattn/go-runewidth v0.0.19 // indirect - github.com/scorredoira/email v0.0.0-20191107070024-dc7b732c55da // indirect - golang.org/x/image v0.34.0 // indirect - golang.org/x/text v0.32.0 // indirect -) +require github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect diff --git a/go.sum b/go.sum index 6d56441..73855f7 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,7 @@ -gitea.mediatoday.ru/mt/image_table v0.0.0-20260403134557-a2b11128d8c9 h1:gtCO1GXbS6E3hWYWTCB3opvpDNV0W14KX86p/x5defQ= -gitea.mediatoday.ru/mt/image_table v0.0.0-20260403134557-a2b11128d8c9/go.mod h1:3+Vee5jTDPlMJaL/qwJkDFUI0ARxbzSmxRgrag/ZlMk= -gitea.mediatoday.ru/mt/notify v0.0.0-20260405185738-6f5db00fcb34 h1:Vi87778hM8bSFogImH2sjKacTJONuCveAzK+2PcpEak= -gitea.mediatoday.ru/mt/notify v0.0.0-20260405185738-6f5db00fcb34/go.mod h1:f173p7S2hvqGlc+YUhX7F2h9OJ8CiTNDH3v+gAn4id0= -github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= -github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +git.gm6.ru/icewind/notify v0.0.0-20251025181146-68310ac711a7 h1:F1SySndj7+K7dm0yOIq/OODRfgw3xpoJ7/mz06rNv0g= +git.gm6.ru/icewind/notify v0.0.0-20251025181146-68310ac711a7/go.mod h1:ydEvkfAmC16BxZL3+t0sq5+viZ3QNY/KH231RZ0sFwg= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= -github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= -github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= -github.com/scorredoira/email v0.0.0-20191107070024-dc7b732c55da h1:hhmnjfzz7szp75AyXxn8tDfEA0oU4REQLmpuW6zNAOY= -github.com/scorredoira/email v0.0.0-20191107070024-dc7b732c55da/go.mod h1:Q5ljvYIBpukMH+wgB8kcPV1i9NX8TqU++8GgBKq3pt0= -golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8= -golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/main.go b/main.go index 1dc7804..b67ae16 100644 --- a/main.go +++ b/main.go @@ -17,9 +17,6 @@ func main() { if err != nil { log.Fatalf("Ошибка инициализации каналов: %v", err) } - if len(notifiers) == 0 { - log.Fatal("не сконфигурирован ни один канал уведомлений") - } http.HandleFunc("/", makeHandler(notifiers)) diff --git a/notifier.go b/notifier.go index 0b29ff8..693cb2e 100644 --- a/notifier.go +++ b/notifier.go @@ -96,10 +96,6 @@ func (e *emailNotifier) Send(d RequestData) error { return notify.SendEmailHTML(e.auth, formatHTML(d), e.subject, e.from, e.to...) } -func splitHostPortLenient(addr string) (host, port string, err error) { - return net.SplitHostPort(addr) -} - // BuildNotifiers собирает map каналов из конфига. Добавляет только заданные секции. func BuildNotifiers(cfg *Config) (map[string]Notifier, error) { notifiers := map[string]Notifier{} @@ -127,7 +123,7 @@ func BuildNotifiers(cfg *Config) (map[string]Notifier, error) { } if cfg.Email != nil { - host, _, err := splitHostPortLenient(cfg.Email.SMTPAddr) + host, _, err := net.SplitHostPort(cfg.Email.SMTPAddr) if err != nil { return nil, fmt.Errorf("email smtp_addr: %w", err) } diff --git a/notifier_test.go b/notifier_test.go index 5396950..f322223 100644 --- a/notifier_test.go +++ b/notifier_test.go @@ -91,3 +91,58 @@ func TestHandlerSendError(t *testing.T) { t.Fatalf("expected 502, got %d", rec.Code) } } + +func TestBuildNotifiersEmpty(t *testing.T) { + n, err := BuildNotifiers(&Config{}) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if len(n) != 0 { + t.Fatalf("expected empty map, got %d", len(n)) + } +} + +func TestBuildNotifiersPachcaMissingURL(t *testing.T) { + _, err := BuildNotifiers(&Config{Pachca: &ConfigPachca{}}) + if err == nil { + t.Fatal("expected error for empty webhook_url") + } +} + +func TestBuildNotifiersEmailNoRecipients(t *testing.T) { + _, err := BuildNotifiers(&Config{Email: &ConfigEmail{ + SMTPAddr: "smtp.example.com:587", + From: "logger@example.com", + To: nil, + }}) + if err == nil { + t.Fatal("expected error for zero recipients") + } +} + +func TestBuildNotifiersEmailBadSMTPAddr(t *testing.T) { + _, err := BuildNotifiers(&Config{Email: &ConfigEmail{ + SMTPAddr: "no-port", + From: "logger@example.com", + To: []string{"a@example.com"}, + }}) + if err == nil { + t.Fatal("expected error for smtp_addr without port") + } +} + +func TestBuildNotifiersEmailOK(t *testing.T) { + n, err := BuildNotifiers(&Config{Email: &ConfigEmail{ + SMTPAddr: "smtp.example.com:587", + Username: "u", + Password: "p", + From: "logger@example.com", + To: []string{"a@example.com"}, + }}) + if err != nil { + t.Fatalf("unexpected err: %v", err) + } + if _, ok := n["email"]; !ok { + t.Fatal("email notifier not built") + } +} From 1409bb265c3da703961be9a24fd6e54045318b2c Mon Sep 17 00:00:00 2001 From: Vladimir V Maksimov Date: Sun, 14 Jun 2026 11:17:46 +0300 Subject: [PATCH 6/8] =?UTF-8?q?fix:=20=D0=B2=D0=BE=D1=81=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=B8=D1=82=D1=8C=20=D0=BF=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=BA=D1=83=20'=D0=BD=D0=B8=20=D0=BE=D0=B4=D0=B8?= =?UTF-8?q?=D0=BD=20=D0=BA=D0=B0=D0=BD=D0=B0=D0=BB=20=D0=BD=D0=B5=20=D1=81?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=84=D0=B8=D0=B3=D1=83=D1=80=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- main.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/main.go b/main.go index b67ae16..1dc7804 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,9 @@ func main() { if err != nil { log.Fatalf("Ошибка инициализации каналов: %v", err) } + if len(notifiers) == 0 { + log.Fatal("не сконфигурирован ни один канал уведомлений") + } http.HandleFunc("/", makeHandler(notifiers)) From 118bbb9261239ab5c87a2f5a5eaa09019b9045b3 Mon Sep 17 00:00:00 2001 From: Vladimir V Maksimov Date: Sun, 14 Jun 2026 11:19:18 +0300 Subject: [PATCH 7/8] =?UTF-8?q?build:=20go=20mod=20tidy=20=E2=80=94=20?= =?UTF-8?q?=D1=83=D0=B1=D1=80=D0=B0=D1=82=D1=8C=20=D1=81=D1=82=D0=B0=D1=80?= =?UTF-8?q?=D1=83=D1=8E=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81=D0=B8=D0=BC=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D1=8C=20git.gm6.ru;=20gitignore=20=D0=BA=D0=BE?= =?UTF-8?q?=D1=80=D0=BD=D0=B5=D0=B2=D0=BE=D0=B9=20=D0=B1=D0=B8=D0=BD=D0=B0?= =?UTF-8?q?=D1=80=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 1 + go.mod | 14 +++++++++++--- go.sum | 16 ++++++++++++++-- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 28e2c71..52e40e2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ *.yaml bin/http_logger +/http_logger *.out \ No newline at end of file diff --git a/go.mod b/go.mod index 84a17f2..b02f4fd 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,18 @@ module http_logger -go 1.25.1 +go 1.25.4 require ( - git.gm6.ru/icewind/notify v0.0.0-20251025181146-68310ac711a7 + gitea.mediatoday.ru/mt/notify v0.0.0-20260405185738-6f5db00fcb34 gopkg.in/yaml.v3 v3.0.1 ) -require github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect +require ( + gitea.mediatoday.ru/mt/image_table v0.0.0-20260403134557-a2b11128d8c9 // indirect + github.com/clipperhouse/uax29/v2 v2.2.0 // indirect + github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect + github.com/mattn/go-runewidth v0.0.19 // indirect + github.com/scorredoira/email v0.0.0-20191107070024-dc7b732c55da // indirect + golang.org/x/image v0.34.0 // indirect + golang.org/x/text v0.32.0 // indirect +) diff --git a/go.sum b/go.sum index 73855f7..6d56441 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,19 @@ -git.gm6.ru/icewind/notify v0.0.0-20251025181146-68310ac711a7 h1:F1SySndj7+K7dm0yOIq/OODRfgw3xpoJ7/mz06rNv0g= -git.gm6.ru/icewind/notify v0.0.0-20251025181146-68310ac711a7/go.mod h1:ydEvkfAmC16BxZL3+t0sq5+viZ3QNY/KH231RZ0sFwg= +gitea.mediatoday.ru/mt/image_table v0.0.0-20260403134557-a2b11128d8c9 h1:gtCO1GXbS6E3hWYWTCB3opvpDNV0W14KX86p/x5defQ= +gitea.mediatoday.ru/mt/image_table v0.0.0-20260403134557-a2b11128d8c9/go.mod h1:3+Vee5jTDPlMJaL/qwJkDFUI0ARxbzSmxRgrag/ZlMk= +gitea.mediatoday.ru/mt/notify v0.0.0-20260405185738-6f5db00fcb34 h1:Vi87778hM8bSFogImH2sjKacTJONuCveAzK+2PcpEak= +gitea.mediatoday.ru/mt/notify v0.0.0-20260405185738-6f5db00fcb34/go.mod h1:f173p7S2hvqGlc+YUhX7F2h9OJ8CiTNDH3v+gAn4id0= +github.com/clipperhouse/uax29/v2 v2.2.0 h1:ChwIKnQN3kcZteTXMgb1wztSgaU+ZemkgWdohwgs8tY= +github.com/clipperhouse/uax29/v2 v2.2.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= +github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw= +github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/scorredoira/email v0.0.0-20191107070024-dc7b732c55da h1:hhmnjfzz7szp75AyXxn8tDfEA0oU4REQLmpuW6zNAOY= +github.com/scorredoira/email v0.0.0-20191107070024-dc7b732c55da/go.mod h1:Q5ljvYIBpukMH+wgB8kcPV1i9NX8TqU++8GgBKq3pt0= +golang.org/x/image v0.34.0 h1:33gCkyw9hmwbZJeZkct8XyR11yH889EQt/QH4VmXMn8= +golang.org/x/image v0.34.0/go.mod h1:2RNFBZRB+vnwwFil8GkMdRvrJOFd1AzdZI6vOY+eJVU= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= From 7cf5bca8d2c2641ab98cd80042ac99a3175803b1 Mon Sep 17 00:00:00 2001 From: Vladimir V Maksimov Date: Sun, 14 Jun 2026 11:22:35 +0300 Subject: [PATCH 8/8] =?UTF-8?q?fix:=20=D0=B2=D1=81=D0=B5=D0=B3=D0=B4=D0=B0?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=BA=D1=80=D1=8B=D0=B2=D0=B0=D1=82=D1=8C=20?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D0=BE=20=D0=B7=D0=B0=D0=BF=D1=80=D0=BE=D1=81?= =?UTF-8?q?=D0=B0=20=D0=B2=20ExtractRequest?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- request.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/request.go b/request.go index 96ce966..832fb29 100644 --- a/request.go +++ b/request.go @@ -43,8 +43,8 @@ func ExtractRequest(r *http.Request) RequestData { body = []byte("too long") } else { body, _ = io.ReadAll(r.Body) - _ = r.Body.Close() } + _ = r.Body.Close() return RequestData{ Time: time.Now(), Method: r.Method,