diff --git a/config.go b/config.go
index 91c791c..6c6fcab 100644
--- a/config.go
+++ b/config.go
@@ -23,8 +23,16 @@ const (
// --- Telegram ---
var (
- cfgTelegramBotToken = ""
- cfgTelegramChatID = ""
+ cfgTelegramBotToken = ""
+ cfgTelegramChatID = ""
+ cfgTelegramSocks5Proxy = "" // Опционально: host:port
+ cfgTelegramSocks5User = "" // Логин прокси (опционально)
+ cfgTelegramSocks5Password = "" // Пароль прокси (опционально)
+)
+
+// --- Mattermost ---
+var (
+ cfgMattermostWebhookURL = "" // Incoming Webhook URL
)
// --- Email / SMTP ---
@@ -152,6 +160,15 @@ func applyConfig(env map[string]string) {
if v, ok := env["TELEGRAM_CHAT_ID"]; ok && v != "" {
cfgTelegramChatID = v
}
+ if v, ok := env["TELEGRAM_SOCKS5_PROXY"]; ok && v != "" {
+ cfgTelegramSocks5Proxy = v
+ }
+ if v, ok := env["TELEGRAM_SOCKS5_USER"]; ok && v != "" {
+ cfgTelegramSocks5User = v
+ }
+ if v, ok := env["TELEGRAM_SOCKS5_PASSWORD"]; ok && v != "" {
+ cfgTelegramSocks5Password = v
+ }
// Database
if v, ok := env["DB_USER"]; ok && v != "" {
@@ -276,6 +293,12 @@ func applyConfig(env map[string]string) {
if v, ok := env["EMAIL_FROM"]; ok && v != "" {
cfgEmailFrom = v
}
+ // Mattermost
+ if v, ok := env["MATTERMOST_WEBHOOK_URL"]; ok && v != "" {
+ cfgMattermostWebhookURL = v
+ }
+
+ // Email / SMTP
if v, ok := env["EMAIL_TO"]; ok && v != "" {
parts := strings.Split(v, ",")
recipients := make([]string, 0, len(parts))
@@ -300,6 +323,10 @@ func isTelegramConfigured() bool {
cfgTelegramChatID != "YOUR_CHAT_ID"
}
+func isMattermostConfigured() bool {
+ return cfgMattermostWebhookURL != ""
+}
+
func isEmailConfigured() bool {
return cfgEmailSMTPHost != "" &&
cfgEmailFrom != "" &&
@@ -312,7 +339,15 @@ func printConfigStatus() {
// Telegram
if isTelegramConfigured() {
- fmt.Println(" ✅ Telegram: настроен")
+ if cfgTelegramSocks5Proxy != "" {
+ if cfgTelegramSocks5User != "" {
+ fmt.Printf(" ✅ Telegram: настроен (прокси: %s, пользователь: %s)\n", cfgTelegramSocks5Proxy, cfgTelegramSocks5User)
+ } else {
+ fmt.Printf(" ✅ Telegram: настроен (прокси: %s, без авторизации)\n", cfgTelegramSocks5Proxy)
+ }
+ } else {
+ fmt.Println(" ✅ Telegram: настроен (прямое соединение)")
+ }
} else {
fmt.Println(" ⚠️ Telegram: не настроен")
}
@@ -324,6 +359,13 @@ func printConfigStatus() {
fmt.Println(" ⚠️ База данных: не настроена")
}
+ // Mattermost
+ if isMattermostConfigured() {
+ fmt.Printf(" ✅ Mattermost: настроен (webhook: %s)\n", cfgMattermostWebhookURL)
+ } else {
+ fmt.Println(" ⚠️ Mattermost: не настроен (MATTERMOST_WEBHOOK_URL)")
+ }
+
// Email
if isEmailConfigured() {
fmt.Printf(" ✅ Email: %s → %s (smtp: %s:%d)\n", cfgEmailFrom, strings.Join(cfgEmailTo, ", "), cfgEmailSMTPHost, cfgEmailSMTPPort)
diff --git a/go.mod b/go.mod
index 6c488e0..8d36114 100644
--- a/go.mod
+++ b/go.mod
@@ -7,6 +7,7 @@ require (
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/lipgloss v1.1.0
github.com/lib/pq v1.10.9
+ golang.org/x/net v0.38.0
)
require (
@@ -31,4 +32,4 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/text v0.3.8 // indirect
-)
+)
\ No newline at end of file
diff --git a/mattermost.go b/mattermost.go
new file mode 100644
index 0000000..7396d04
--- /dev/null
+++ b/mattermost.go
@@ -0,0 +1,91 @@
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "regexp"
+ "strings"
+ "time"
+)
+
+// mattermostSend отправляет сообщение в Mattermost через Incoming Webhook.
+// HTML-теги из Telegram автоматически конвертируются в Markdown.
+// Если вебхук не настроен — пишет в лог и не паникует.
+func mattermostSend(message string) error {
+ if !isMattermostConfigured() {
+ log.Printf("[Mattermost] Не настроен (нужен MATTERMOST_WEBHOOK_URL) — пропускаем")
+ return fmt.Errorf("mattermost не настроен")
+ }
+
+ mdMessage := mmHTMLToMarkdown(message)
+
+ payload := map[string]string{
+ "text": mdMessage,
+ }
+
+ jsonData, err := json.Marshal(payload)
+ if err != nil {
+ return fmt.Errorf("marshal error: %w", err)
+ }
+
+ log.Printf("[Mattermost] Отправка вебхук: %s", cfgMattermostWebhookURL)
+
+ client := &http.Client{Timeout: 15 * time.Second}
+ resp, err := client.Post(cfgMattermostWebhookURL, "application/json", bytes.NewBuffer(jsonData))
+ if err != nil {
+ return fmt.Errorf("ошибка отправки в Mattermost (webhook: %s): %w", cfgMattermostWebhookURL, err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ return fmt.Errorf("mattermost webhook error: status %d, body: %s", resp.StatusCode, string(body))
+ }
+
+ return nil
+}
+
+// mmHTMLToMarkdown конвертирует HTML-теги Telegram в Markdown для Mattermost.
+//
+// текст → **текст**
+// текст → _текст_
+// текст → `текст`
+//
текст→ ```текст``` +// ━━━ (разделители) → --- +// Остальные теги → удаляются +func mmHTMLToMarkdown(html string) string { + s := html + + // ... → **...** + s = regexp.MustCompile(`([\s\S]*?)`).ReplaceAllString(s, "**$1**") + + // ... → _..._ + s = regexp.MustCompile(`([\s\S]*?)`).ReplaceAllString(s, "_$1_") + + //
... → `...`
+ s = regexp.MustCompile(`([\s\S]*?)`).ReplaceAllString(s, "`$1`")
+
+ // ...→ ```...``` + s = regexp.MustCompile(`
([\s\S]*?)`).ReplaceAllString(s, "```$1```") + + // HTML-entities + s = strings.ReplaceAll(s, "<", "<") + s = strings.ReplaceAll(s, ">", ">") + s = strings.ReplaceAll(s, "&", "&") + s = strings.ReplaceAll(s, """, `"`) + + // Горизонтальные разделители (━━━...) → --- + s = regexp.MustCompile(`━+`).ReplaceAllString(s, "---") + + // Убираем оставшиеся HTML-теги + s = regexp.MustCompile(`<[^>]+>`).ReplaceAllString(s, "") + + // Убираем лишние пустые строки (более двух подряд) + s = regexp.MustCompile(`\n{3,}`).ReplaceAllString(s, "\n\n") + + return strings.TrimSpace(s) +} diff --git a/telegram.go b/telegram.go index e6feabf..283bb5e 100644 --- a/telegram.go +++ b/telegram.go @@ -2,14 +2,58 @@ package main import ( "bytes" + "context" "encoding/json" "fmt" "io" "log" + "net" "net/http" "net/url" + "time" + + "golang.org/x/net/proxy" ) +// telegramHTTPClient возвращает http.Client — обычный или через SOCKS5 прокси. +// Если TELEGRAM_SOCKS5_PROXY не задан — прямое соединение. +// Логин и пароль задаются отдельными параметрами TELEGRAM_SOCKS5_USER / TELEGRAM_SOCKS5_PASSWORD +// чтобы избежать проблем со спецсимволами при парсинге URL. +func telegramHTTPClient() (*http.Client, error) { + timeout := 15 * time.Second + + if cfgTelegramSocks5Proxy == "" { + // Прямое соединение + return &http.Client{Timeout: timeout}, nil + } + + var auth *proxy.Auth + if cfgTelegramSocks5User != "" { + auth = &proxy.Auth{ + User: cfgTelegramSocks5User, + Password: cfgTelegramSocks5Password, + } + } + + dialer, err := proxy.SOCKS5("tcp", cfgTelegramSocks5Proxy, auth, proxy.Direct) + if err != nil { + return nil, fmt.Errorf("ошибка создания SOCKS5 диалера: %w", err) + } + + transport := &http.Transport{ + DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { + return dialer.Dial(network, addr) + }, + } + + log.Printf("[Telegram] Используется SOCKS5 прокси: %s", cfgTelegramSocks5Proxy) + + return &http.Client{ + Transport: transport, + Timeout: timeout, + }, nil +} + // telegramSendHTML отправляет HTML-сообщение в Telegram func telegramSendHTML(message string) error { if !isTelegramConfigured() { @@ -17,6 +61,11 @@ func telegramSendHTML(message string) error { return fmt.Errorf("telegram не настроен") } + client, err := telegramHTTPClient() + if err != nil { + return fmt.Errorf("ошибка создания HTTP клиента: %w", err) + } + type tgMessage struct { ChatID string `json:"chat_id"` Text string `json:"text"` @@ -36,7 +85,7 @@ func telegramSendHTML(message string) error { apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", cfgTelegramBotToken) - resp, err := http.Post(apiURL, "application/json", bytes.NewBuffer(jsonData)) + resp, err := client.Post(apiURL, "application/json", bytes.NewBuffer(jsonData)) if err != nil { return fmt.Errorf("http error: %w", err) } @@ -57,13 +106,18 @@ func telegramSendPlainText(message string) error { return fmt.Errorf("telegram не настроен") } + client, err := telegramHTTPClient() + if err != nil { + return fmt.Errorf("ошибка создания HTTP клиента: %w", err) + } + apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", cfgTelegramBotToken) data := url.Values{} data.Set("chat_id", cfgTelegramChatID) data.Set("text", message) - resp, err := http.PostForm(apiURL, data) + resp, err := client.PostForm(apiURL, data) if err != nil { return fmt.Errorf("http error: %w", err) }