|
@@ -272,41 +272,78 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
|
|
return nil
|
|
return nil
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// NewBot creates a new Telegram bot instance with optional proxy and API server settings.
|
|
|
|
|
-func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) {
|
|
|
|
|
- if proxyUrl == "" && apiServerUrl == "" {
|
|
|
|
|
- return telego.NewBot(token)
|
|
|
|
|
|
|
+// createRobustFastHTTPClient creates a fasthttp.Client with proper connection handling
|
|
|
|
|
+func (t *Tgbot) createRobustFastHTTPClient(proxyUrl string) *fasthttp.Client {
|
|
|
|
|
+ client := &fasthttp.Client{
|
|
|
|
|
+ // Connection timeouts
|
|
|
|
|
+ ReadTimeout: 30 * time.Second,
|
|
|
|
|
+ WriteTimeout: 30 * time.Second,
|
|
|
|
|
+ MaxIdleConnDuration: 60 * time.Second,
|
|
|
|
|
+ MaxConnDuration: 0, // unlimited, but controlled by MaxIdleConnDuration
|
|
|
|
|
+ MaxIdemponentCallAttempts: 3,
|
|
|
|
|
+ ReadBufferSize: 4096,
|
|
|
|
|
+ WriteBufferSize: 4096,
|
|
|
|
|
+ MaxConnsPerHost: 100,
|
|
|
|
|
+ MaxConnWaitTimeout: 10 * time.Second,
|
|
|
|
|
+ DisableHeaderNamesNormalizing: false,
|
|
|
|
|
+ DisablePathNormalizing: false,
|
|
|
|
|
+ // Retry on connection errors
|
|
|
|
|
+ RetryIf: func(request *fasthttp.Request) bool {
|
|
|
|
|
+ // Retry on connection errors for GET requests
|
|
|
|
|
+ return string(request.Header.Method()) == "GET" || string(request.Header.Method()) == "POST"
|
|
|
|
|
+ },
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Set proxy if provided
|
|
|
|
|
+ if proxyUrl != "" {
|
|
|
|
|
+ client.Dial = fasthttpproxy.FasthttpSocksDialer(proxyUrl)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ return client
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// NewBot creates a new Telegram bot instance with optional proxy and API server settings.
|
|
|
|
|
+func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) {
|
|
|
|
|
+ // Validate proxy URL if provided
|
|
|
if proxyUrl != "" {
|
|
if proxyUrl != "" {
|
|
|
if !strings.HasPrefix(proxyUrl, "socks5://") {
|
|
if !strings.HasPrefix(proxyUrl, "socks5://") {
|
|
|
- logger.Warning("Invalid socks5 URL, using default")
|
|
|
|
|
- return telego.NewBot(token)
|
|
|
|
|
|
|
+ logger.Warning("Invalid socks5 URL, ignoring proxy")
|
|
|
|
|
+ proxyUrl = "" // Clear invalid proxy
|
|
|
|
|
+ } else {
|
|
|
|
|
+ _, err := url.Parse(proxyUrl)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ logger.Warningf("Can't parse proxy URL, ignoring proxy: %v", err)
|
|
|
|
|
+ proxyUrl = ""
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- _, err := url.Parse(proxyUrl)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- logger.Warningf("Can't parse proxy URL, using default instance for tgbot: %v", err)
|
|
|
|
|
- return telego.NewBot(token)
|
|
|
|
|
|
|
+ // Validate API server URL if provided
|
|
|
|
|
+ if apiServerUrl != "" {
|
|
|
|
|
+ if !strings.HasPrefix(apiServerUrl, "http") {
|
|
|
|
|
+ logger.Warning("Invalid http(s) URL for API server, using default")
|
|
|
|
|
+ apiServerUrl = ""
|
|
|
|
|
+ } else {
|
|
|
|
|
+ _, err := url.Parse(apiServerUrl)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ logger.Warningf("Can't parse API server URL, using default: %v", err)
|
|
|
|
|
+ apiServerUrl = ""
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- return telego.NewBot(token, telego.WithFastHTTPClient(&fasthttp.Client{
|
|
|
|
|
- Dial: fasthttpproxy.FasthttpSocksDialer(proxyUrl),
|
|
|
|
|
- }))
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if !strings.HasPrefix(apiServerUrl, "http") {
|
|
|
|
|
- logger.Warning("Invalid http(s) URL, using default")
|
|
|
|
|
- return telego.NewBot(token)
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // Create robust fasthttp client
|
|
|
|
|
+ client := t.createRobustFastHTTPClient(proxyUrl)
|
|
|
|
|
|
|
|
- _, err := url.Parse(apiServerUrl)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- logger.Warningf("Can't parse API server URL, using default instance for tgbot: %v", err)
|
|
|
|
|
- return telego.NewBot(token)
|
|
|
|
|
|
|
+ // Build bot options
|
|
|
|
|
+ var options []telego.BotOption
|
|
|
|
|
+ options = append(options, telego.WithFastHTTPClient(client))
|
|
|
|
|
+
|
|
|
|
|
+ if apiServerUrl != "" {
|
|
|
|
|
+ options = append(options, telego.WithAPIServer(apiServerUrl))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- return telego.NewBot(token, telego.WithAPIServer(apiServerUrl))
|
|
|
|
|
|
|
+ return telego.NewBot(token, options...)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// IsRunning checks if the Telegram bot is currently running.
|
|
// IsRunning checks if the Telegram bot is currently running.
|
|
@@ -390,7 +427,7 @@ func (t *Tgbot) decodeQuery(query string) (string, error) {
|
|
|
// OnReceive starts the message receiving loop for the Telegram bot.
|
|
// OnReceive starts the message receiving loop for the Telegram bot.
|
|
|
func (t *Tgbot) OnReceive() {
|
|
func (t *Tgbot) OnReceive() {
|
|
|
params := telego.GetUpdatesParams{
|
|
params := telego.GetUpdatesParams{
|
|
|
- Timeout: 30, // Increased timeout to reduce API calls
|
|
|
|
|
|
|
+ Timeout: 20, // Reduced timeout to detect connection issues faster
|
|
|
}
|
|
}
|
|
|
// Strict singleton: never start a second long-polling loop.
|
|
// Strict singleton: never start a second long-polling loop.
|
|
|
tgBotMutex.Lock()
|
|
tgBotMutex.Lock()
|
|
@@ -408,7 +445,7 @@ func (t *Tgbot) OnReceive() {
|
|
|
botWG.Add(1)
|
|
botWG.Add(1)
|
|
|
tgBotMutex.Unlock()
|
|
tgBotMutex.Unlock()
|
|
|
|
|
|
|
|
- // Get updates channel using the context.
|
|
|
|
|
|
|
+ // Get updates channel using the context with shorter timeout for better error recovery
|
|
|
updates, _ := bot.UpdatesViaLongPolling(ctx, ¶ms)
|
|
updates, _ := bot.UpdatesViaLongPolling(ctx, ¶ms)
|
|
|
go func() {
|
|
go func() {
|
|
|
defer botWG.Done()
|
|
defer botWG.Done()
|
|
@@ -2247,10 +2284,36 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
|
|
|
if len(replyMarkup) > 0 && n == (len(allMessages)-1) {
|
|
if len(replyMarkup) > 0 && n == (len(allMessages)-1) {
|
|
|
params.ReplyMarkup = replyMarkup[0]
|
|
params.ReplyMarkup = replyMarkup[0]
|
|
|
}
|
|
}
|
|
|
- _, err := bot.SendMessage(context.Background(), ¶ms)
|
|
|
|
|
- if err != nil {
|
|
|
|
|
- logger.Warning("Error sending telegram message :", err)
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // Retry logic with exponential backoff for connection errors
|
|
|
|
|
+ maxRetries := 3
|
|
|
|
|
+ for attempt := range maxRetries {
|
|
|
|
|
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
|
|
|
+ _, err := bot.SendMessage(ctx, ¶ms)
|
|
|
|
|
+ cancel()
|
|
|
|
|
+
|
|
|
|
|
+ if err == nil {
|
|
|
|
|
+ break // Success
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if error is a connection error
|
|
|
|
|
+ errStr := err.Error()
|
|
|
|
|
+ isConnectionError := strings.Contains(errStr, "connection") ||
|
|
|
|
|
+ strings.Contains(errStr, "timeout") ||
|
|
|
|
|
+ strings.Contains(errStr, "closed")
|
|
|
|
|
+
|
|
|
|
|
+ if isConnectionError && attempt < maxRetries-1 {
|
|
|
|
|
+ // Exponential backoff: 1s, 2s, 4s
|
|
|
|
|
+ backoff := time.Duration(1<<uint(attempt)) * time.Second
|
|
|
|
|
+ logger.Warningf("Connection error sending telegram message (attempt %d/%d), retrying in %v: %v",
|
|
|
|
|
+ attempt+1, maxRetries, backoff, err)
|
|
|
|
|
+ time.Sleep(backoff)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ logger.Warning("Error sending telegram message:", err)
|
|
|
|
|
+ break
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
// Reduced delay to improve performance (only needed for rate limiting)
|
|
// Reduced delay to improve performance (only needed for rate limiting)
|
|
|
if n < len(allMessages)-1 { // Only delay between messages, not after the last one
|
|
if n < len(allMessages)-1 { // Only delay between messages, not after the last one
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
time.Sleep(100 * time.Millisecond)
|
|
@@ -2585,8 +2648,12 @@ func (t *Tgbot) SendBackupToAdmins() {
|
|
|
if !t.IsRunning() {
|
|
if !t.IsRunning() {
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- for _, adminId := range adminIds {
|
|
|
|
|
|
|
+ for i, adminId := range adminIds {
|
|
|
t.sendBackup(int64(adminId))
|
|
t.sendBackup(int64(adminId))
|
|
|
|
|
+ // Add delay between sends to avoid Telegram rate limits
|
|
|
|
|
+ if i < len(adminIds)-1 {
|
|
|
|
|
+ time.Sleep(1 * time.Second)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -3596,13 +3663,17 @@ func (t *Tgbot) sendBackup(chatId int64) {
|
|
|
logger.Error("Error in trigger a checkpoint operation: ", err)
|
|
logger.Error("Error in trigger a checkpoint operation: ", err)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // Send database backup
|
|
|
file, err := os.Open(config.GetDBPath())
|
|
file, err := os.Open(config.GetDBPath())
|
|
|
if err == nil {
|
|
if err == nil {
|
|
|
|
|
+ defer file.Close()
|
|
|
|
|
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
|
|
|
+ defer cancel()
|
|
|
document := tu.Document(
|
|
document := tu.Document(
|
|
|
tu.ID(chatId),
|
|
tu.ID(chatId),
|
|
|
tu.File(file),
|
|
tu.File(file),
|
|
|
)
|
|
)
|
|
|
- _, err = bot.SendDocument(context.Background(), document)
|
|
|
|
|
|
|
+ _, err = bot.SendDocument(ctx, document)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
logger.Error("Error in uploading backup: ", err)
|
|
logger.Error("Error in uploading backup: ", err)
|
|
|
}
|
|
}
|
|
@@ -3610,13 +3681,20 @@ func (t *Tgbot) sendBackup(chatId int64) {
|
|
|
logger.Error("Error in opening db file for backup: ", err)
|
|
logger.Error("Error in opening db file for backup: ", err)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // Small delay between file sends
|
|
|
|
|
+ time.Sleep(500 * time.Millisecond)
|
|
|
|
|
+
|
|
|
|
|
+ // Send config.json backup
|
|
|
file, err = os.Open(xray.GetConfigPath())
|
|
file, err = os.Open(xray.GetConfigPath())
|
|
|
if err == nil {
|
|
if err == nil {
|
|
|
|
|
+ defer file.Close()
|
|
|
|
|
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
|
|
|
+ defer cancel()
|
|
|
document := tu.Document(
|
|
document := tu.Document(
|
|
|
tu.ID(chatId),
|
|
tu.ID(chatId),
|
|
|
tu.File(file),
|
|
tu.File(file),
|
|
|
)
|
|
)
|
|
|
- _, err = bot.SendDocument(context.Background(), document)
|
|
|
|
|
|
|
+ _, err = bot.SendDocument(ctx, document)
|
|
|
if err != nil {
|
|
if err != nil {
|
|
|
logger.Error("Error in uploading config.json: ", err)
|
|
logger.Error("Error in uploading config.json: ", err)
|
|
|
}
|
|
}
|