mirror of
https://github.com/HugeFrog24/go-telegram-bot.git
synced 2026-06-29 22:07:12 +00:00
118 lines
4.3 KiB
Go
118 lines
4.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/go-telegram/bot/models"
|
|
)
|
|
|
|
// albumFlushWindow is the debounce delay before a buffered Telegram media_group
|
|
// is flushed as a single coalesced user turn. 1s matches the de-facto community
|
|
// standard across the dominant third-party album plugins (aiogram-media-group,
|
|
// DieTime/telegraf-media-group) and sits above the sub-100ms values documented
|
|
// as lossy under network jitter (openclaw#1811). Telegram has no official
|
|
// "album complete" signal, so timeout-based flush is the only option.
|
|
const albumFlushWindow = 1 * time.Second
|
|
|
|
// pendingAlbum holds a Telegram media_group as its items arrive, plus the
|
|
// per-user metadata captured from the first item. All items in an album share
|
|
// the same chat/user, so we record metadata once and reuse it at flush time.
|
|
type pendingAlbum struct {
|
|
items []*models.Message
|
|
// Metadata captured from the first arriving item. Albums always come from
|
|
// the same user/chat, so these are stable across the buffering window.
|
|
chatID, userID int64
|
|
username, firstName, lastName, languageCode string
|
|
isPremium bool
|
|
messageTime int
|
|
businessConnectionID string
|
|
// timer flushes the album after albumFlushWindow with no further arrivals.
|
|
// Each new arrival stops the previous timer (best-effort) and installs a
|
|
// fresh one — the standard debounce pattern.
|
|
timer *time.Timer
|
|
}
|
|
|
|
// bufferAlbumItem appends an incoming Telegram album item to the per-MediaGroupID
|
|
// buffer. On first arrival it captures the user/chat metadata and starts the
|
|
// flush timer; on subsequent arrivals it appends the item and extends the timer.
|
|
// The 1s debounce gives the rest of the album time to arrive over the network.
|
|
func (b *Bot) bufferAlbumItem(
|
|
ctx context.Context,
|
|
msg *models.Message,
|
|
chatID, userID int64,
|
|
username, firstName, lastName string,
|
|
isPremium bool,
|
|
languageCode string,
|
|
messageTime int,
|
|
businessConnectionID string,
|
|
) {
|
|
b.albumBuffersMu.Lock()
|
|
defer b.albumBuffersMu.Unlock()
|
|
|
|
album, exists := b.albumBuffers[msg.MediaGroupID]
|
|
if !exists {
|
|
album = &pendingAlbum{
|
|
chatID: chatID,
|
|
userID: userID,
|
|
username: username,
|
|
firstName: firstName,
|
|
lastName: lastName,
|
|
isPremium: isPremium,
|
|
languageCode: languageCode,
|
|
messageTime: messageTime,
|
|
businessConnectionID: businessConnectionID,
|
|
}
|
|
b.albumBuffers[msg.MediaGroupID] = album
|
|
}
|
|
album.items = append(album.items, msg)
|
|
|
|
// Stop the previous timer best-effort; even if it already fired the race
|
|
// is benign because flushAlbum removes the map entry under the lock — a
|
|
// late arrival would simply seed a fresh album.
|
|
if album.timer != nil {
|
|
album.timer.Stop()
|
|
}
|
|
mediaGroupID := msg.MediaGroupID
|
|
album.timer = time.AfterFunc(albumFlushWindow, func() {
|
|
b.flushAlbum(ctx, mediaGroupID)
|
|
})
|
|
}
|
|
|
|
// flushAlbum is called by the flush timer (or by code that needs to force-flush
|
|
// during shutdown). It removes the album from the buffer, sorts items by
|
|
// message_id (Telegram does not guarantee in-order arrival), runs the rate-limit
|
|
// check once, and dispatches to handlePhotoMessage.
|
|
func (b *Bot) flushAlbum(ctx context.Context, mediaGroupID string) {
|
|
b.albumBuffersMu.Lock()
|
|
album, exists := b.albumBuffers[mediaGroupID]
|
|
if !exists {
|
|
b.albumBuffersMu.Unlock()
|
|
return
|
|
}
|
|
delete(b.albumBuffers, mediaGroupID)
|
|
items := album.items
|
|
captured := *album // copy fields for use after unlock
|
|
b.albumBuffersMu.Unlock()
|
|
|
|
// Sort by Telegram message_id: items in an album arrive as separate Updates
|
|
// over the network and may interleave. Sorting restores the user's intended
|
|
// order before we hand them to Claude.
|
|
sort.Slice(items, func(i, j int) bool { return items[i].ID < items[j].ID })
|
|
|
|
// Rate-limit fires once per coalesced album, not once per item.
|
|
if !b.checkRateLimits(captured.userID) {
|
|
b.sendRateLimitExceededMessage(ctx, captured.chatID, captured.businessConnectionID)
|
|
return
|
|
}
|
|
|
|
b.handlePhotoMessage(
|
|
ctx, items,
|
|
captured.chatID, captured.userID,
|
|
captured.username, captured.firstName, captured.lastName,
|
|
captured.isPremium, captured.languageCode, captured.messageTime,
|
|
captured.businessConnectionID,
|
|
)
|
|
}
|