193 lines
5.8 KiB
Go
193 lines
5.8 KiB
Go
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
|
||
}
|