auspex/telegram.go
2026-06-09 17:12:18 +03:00

193 lines
5.8 KiB
Go
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net"
"net/http"
"net/url"
"time"
"golang.org/x/net/proxy"
)
// telegramHTTPClient возвращает цепочку http.Client для отправки запросов к Telegram API.
// Порядок попыток: HTTP прокси → SOCKS5 → прямое соединение.
// Если предыдущий вариант недоступен — автоматически пробуется следующий.
func telegramHTTPClients() []*http.Client {
timeout := 15 * time.Second
var clients []*http.Client
// 1. HTTP прокси
if cfgTelegramHTTPProxy != "" {
proxyURL, err := url.Parse(cfgTelegramHTTPProxy)
if err != nil {
log.Printf("[Telegram] ⚠️ Некорректный адрес HTTP прокси %q: %v", cfgTelegramHTTPProxy, err)
} else {
clients = append(clients, &http.Client{
Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)},
Timeout: timeout,
})
}
}
// 2. SOCKS5 прокси
if cfgTelegramSocks5Proxy != "" {
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 {
log.Printf("[Telegram] ⚠️ Ошибка создания SOCKS5 диалера: %v", err)
} else {
clients = append(clients, &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
return dialer.Dial(network, addr)
},
},
Timeout: timeout,
})
}
}
// 3. Прямое соединение — всегда последний fallback
clients = append(clients, &http.Client{Timeout: timeout})
return clients
}
// telegramDoPost выполняет POST запрос перебирая доступные клиенты по порядку.
// Возвращает первый успешный ответ или последнюю ошибку если все варианты исчерпаны.
func telegramDoPost(apiURL, contentType string, body []byte) (*http.Response, error) {
clients := telegramHTTPClients()
labels := telegramClientLabels()
var lastErr error
for i, client := range clients {
resp, err := client.Post(apiURL, contentType, bytes.NewBuffer(body))
if err != nil {
log.Printf("[Telegram] ⚠️ %s недоступен: %v", labels[i], err)
lastErr = err
continue
}
if i > 0 {
log.Printf("[Telegram] ✓ Использован fallback: %s", labels[i])
} else {
log.Printf("[Telegram] Отправка через: %s", labels[i])
}
return resp, nil
}
return nil, lastErr
}
// telegramDoPostForm выполняет POST form запрос с перебором клиентов.
func telegramDoPostForm(apiURL string, data url.Values) (*http.Response, error) {
clients := telegramHTTPClients()
labels := telegramClientLabels()
var lastErr error
for i, client := range clients {
resp, err := client.PostForm(apiURL, data)
if err != nil {
log.Printf("[Telegram] ⚠️ %s недоступен: %v", labels[i], err)
lastErr = err
continue
}
if i > 0 {
log.Printf("[Telegram] ✓ Использован fallback: %s", labels[i])
} else {
log.Printf("[Telegram] Отправка через: %s", labels[i])
}
return resp, nil
}
return nil, lastErr
}
// telegramClientLabels возвращает человекочитаемые названия клиентов в том же порядке.
func telegramClientLabels() []string {
var labels []string
if cfgTelegramHTTPProxy != "" {
labels = append(labels, fmt.Sprintf("HTTP прокси (%s)", cfgTelegramHTTPProxy))
}
if cfgTelegramSocks5Proxy != "" {
labels = append(labels, fmt.Sprintf("SOCKS5 (%s)", cfgTelegramSocks5Proxy))
}
labels = append(labels, "прямое соединение")
return labels
}
// telegramSendHTML отправляет HTML-сообщение в Telegram
func telegramSendHTML(message string) error {
if !isTelegramConfigured() {
log.Printf("[Telegram] Не настроен. Сообщение:\n%s\n", message)
return fmt.Errorf("telegram не настроен")
}
type tgMessage struct {
ChatID string `json:"chat_id"`
Text string `json:"text"`
ParseMode string `json:"parse_mode"`
}
msg := tgMessage{
ChatID: cfgTelegramChatID,
Text: message,
ParseMode: "HTML",
}
jsonData, err := json.Marshal(msg)
if err != nil {
return fmt.Errorf("marshal error: %w", err)
}
apiURL := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", cfgTelegramBotToken)
resp, err := telegramDoPost(apiURL, "application/json", jsonData)
if err != nil {
return fmt.Errorf("http error: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("telegram api error: status %d, body: %s", resp.StatusCode, string(body))
}
return nil
}
// telegramSendPlainText отправляет текстовое сообщение без форматирования
func telegramSendPlainText(message string) error {
if !isTelegramConfigured() {
log.Printf("[Telegram] Не настроен. Сообщение:\n%s\n", message)
return fmt.Errorf("telegram не настроен")
}
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 := telegramDoPostForm(apiURL, data)
if err != nil {
return fmt.Errorf("http error: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("telegram api error: status %d, body: %s", resp.StatusCode, string(body))
}
return nil
}