From b90a6d12ea5b135e55b583464308caf4d0910c34 Mon Sep 17 00:00:00 2001 From: Badgerbees Date: Wed, 1 Apr 2026 03:13:34 +0700 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFfix(telegram):=20refine=20duplicate-me?= =?UTF-8?q?ssage=20protection=20with=20narrow=20error=20classification?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses reviewer concerns regarding silent message loss by narrowing the error swallowing logic in EditMessage: - Excludes context.DeadlineExceeded and context.Canceled from being swallowed, ensuring local timeouts before transmission still trigger a fallback send. - Adds an explicit check for the 'message is not modified' error to safely identify edits that have already landed on Telegram's servers. - Narrowly targets confirmed post-connect dropouts (e.g., connection reset) instead of broad network-ish string matching. - Fixes the missing isPostConnectError definition and required errors import. --- pkg/channels/telegram/telegram.go | 59 +++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/pkg/channels/telegram/telegram.go b/pkg/channels/telegram/telegram.go index 831eb43cc..c1097bf04 100644 --- a/pkg/channels/telegram/telegram.go +++ b/pkg/channels/telegram/telegram.go @@ -4,6 +4,7 @@ import ( "context" "crypto/rand" "encoding/binary" + "errors" "fmt" "io" "net/http" @@ -377,8 +378,38 @@ func (c *TelegramChannel) EditMessage(ctx context.Context, chatID string, messag } _, err = c.bot.EditMessageText(ctx, editMsg) if err != nil { - logParseFailed(err, useMarkdownV2) - _, err = c.bot.EditMessageText(ctx, tu.EditMessageText(tu.ID(cid), mid, content)) + // If it failed because it was already modified (likely from a previous + // attempt that timed out on our end but landed on Telegram), we treat + // it as success to prevent the Manager from sending a duplicate message. + if strings.Contains(err.Error(), "message is not modified") { + return nil + } + + // Only fallback to plain text if the error looks like a parsing failure (Bad Request). + // Network errors or timeouts should NOT trigger a retry with different content. + if strings.Contains(err.Error(), "Bad Request") { + logParseFailed(err, useMarkdownV2) + _, err = c.bot.EditMessageText(ctx, tu.EditMessageText(tu.ID(cid), mid, content)) + } + } + + if err != nil { + if strings.Contains(err.Error(), "message is not modified") { + return nil + } + + if isPostConnectError(err) { + logger.WarnCF( + "telegram", + "EditMessage likely landed but result is unknown; swallowing error to prevent duplicate", + map[string]any{ + "chat_id": chatID, + "mid": mid, + "error": err.Error(), + }, + ) + return nil // Swallow to prevent Manager fallback to a new SendMessage + } } return err @@ -1133,3 +1164,27 @@ func cryptoRandInt() int { _, _ = rand.Read(b[:]) return int(binary.BigEndian.Uint32(b[:])) | 1 // ensure non-zero } + +// isPostConnectError identifies network errors that likely occurred after +// the request was transmitted to Telegram (e.g. dropped connection while +// waiting for response). Swallowing these for edits prevents duplicate +// fallbacks, at the small risk of leaving a stale placeholder if the +// edit never actually reached the server. +func isPostConnectError(err error) bool { + if err == nil { + return false + } + + // Context errors (timeout/canceled) are too broad; they can be triggered + // locally before any data is sent. Never swallow them. + if errors.Is(err, context.DeadlineExceeded) || errors.Is(err, context.Canceled) { + return false + } + + msg := strings.ToLower(err.Error()) + // Narrowly target connection dropouts where the request likely landed. + return strings.Contains(msg, "connection reset by peer") || + strings.Contains(msg, "unexpected eof") || + strings.Contains(msg, "connection closed by foreign host") || + strings.Contains(msg, "broken pipe") +}