Drop permanent queue errors fast + log MAX bot add/remove events
Some checks failed
Build / build (push) Has been cancelled

Two queue items in send_queue could pile up indefinitely behind the
30-attempt limit and block per-destination ordering (hasPendingForChat):

  tg2max: "MAX attachment not ready after 10 retries", "must be at most"
  max2tg: "can't parse entities", "caption/message is too long"

These never become deliverable — drop them on the first failure so the
queue can drain and new messages for the same destination skip the
queued path.

Also: subscribe-listed bot_added / bot_removed updates weren't handled,
so when admins kicked the bot from a MAX chat the only signal was
silence in logs. listenMax now logs both events with chat id, actor user
id, username and display name; the DB pair is left intact in case the
bot is re-added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andrey Lugovskoy 2026-06-01 13:15:35 +04:00
parent 9f4c1dc36a
commit bb7bcfdb33
3 changed files with 33 additions and 3 deletions

1
.gitignore vendored
View file

@ -22,3 +22,4 @@ bridge.db.bak
broadcast.sh
broadcast_*.html
.broadcast_sent/
bridge_chats.csv

22
max.go
View file

@ -63,6 +63,28 @@ func (b *Bridge) listenMax(ctx context.Context) {
slog.Debug("MAX update", "type", fmt.Sprintf("%T", upd))
// Логируем выкидывание бота из чата — пара остаётся в БД (мб случайно
// выкинули, потом вернут), но видно кто/когда удалил.
if rm, isRm := upd.(*maxschemes.BotRemovedFromChatUpdate); isRm {
tgChatID, linked := b.repo.GetTgChat(rm.ChatId)
slog.Warn("MAX bot removed from chat",
"maxChat", rm.ChatId,
"byUser", rm.User.UserId,
"byUsername", rm.User.Username,
"byName", rm.User.Name,
"linkedTgChat", tgChatID,
"linked", linked)
continue
}
if add, isAdd := upd.(*maxschemes.BotAddedToChatUpdate); isAdd {
slog.Info("MAX bot added to chat",
"maxChat", add.ChatId,
"byUser", add.User.UserId,
"byUsername", add.User.Username,
"byName", add.User.Name)
continue
}
// Обработка удаления (только bridge, не crosspost)
if delUpd, isDel := upd.(*maxschemes.MessageRemovedUpdate); isDel {
tgChatID, tgMsgID, _, ok := b.repo.LookupTgMsgID(delUpd.MessageId)

View file

@ -127,8 +127,11 @@ func (b *Bridge) processQueueTg2Max(ctx context.Context, item QueueItem, now tim
mid, err := b.sendMaxDirectFormatted(ctx, item.DstChatID, item.Text, item.AttType, item.AttToken, item.ReplyTo, item.Format)
if err != nil {
errStr := err.Error()
// Permanent errors — дропаем
if strings.Contains(errStr, "403") || strings.Contains(errStr, "404") || strings.Contains(errStr, "chat.denied") {
// Permanent errors — дропаем сразу, чтобы не блокировать очередь HoL-ом.
if strings.Contains(errStr, "403") || strings.Contains(errStr, "404") ||
strings.Contains(errStr, "chat.denied") ||
strings.Contains(errStr, "attachment not ready after") ||
strings.Contains(errStr, "must be at most") {
slog.Warn("queue item dropped (permanent error)", "id", item.ID, "err", errStr)
b.repo.DeleteFromQueue(item.ID)
return
@ -182,7 +185,11 @@ func (b *Bridge) processQueueMax2Tg(ctx context.Context, item QueueItem, now tim
b.repo.IncrementAttempt(item.ID, now.Unix()) // retry immediately
return
}
if strings.Contains(errStr, "TOPIC_CLOSED") || strings.Contains(errStr, "403") || strings.Contains(errStr, "chat not found") {
if strings.Contains(errStr, "TOPIC_CLOSED") || strings.Contains(errStr, "403") || strings.Contains(errStr, "chat not found") ||
strings.Contains(errStr, "can't parse entities") ||
strings.Contains(errStr, "caption is too long") ||
strings.Contains(errStr, "message is too long") ||
strings.Contains(errStr, "MESSAGE_TOO_LONG") {
slog.Warn("queue item dropped (permanent error)", "id", item.ID, "dir", "max2tg", "err", errStr)
b.repo.DeleteFromQueue(item.ID)
return