В Pachca/Telegram текст сразу после открывающих тройных бэктиков трактуется как указатель языка code-блока и не отображается. Из-за этого пропадала первая строка уведомления — [время] METHOD host/URI, то есть путь и метод запроса не были видны. Добавлен \n после фенса; тест теперь проверяет перевод строки и наличие URI в выводе. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
162 lines
4.0 KiB
Go
162 lines
4.0 KiB
Go
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 "```\n" + formatPlain(d) + "```"
|
||
}
|
||
|
||
// formatHTML — обёртка в <pre> с экранированием для Email.
|
||
func formatHTML(d RequestData) string {
|
||
return "<pre>" + html.EscapeString(formatPlain(d)) + "</pre>"
|
||
}
|
||
|
||
// --- 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...)
|
||
}
|
||
|
||
// 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 := net.SplitHostPort(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
|
||
}
|