feat: sync bot keyboards and callbacks

Sync telesrv b96f2dd (feat(bot): complete keyboards callbacks and durable delivery).

Skipped private docs and preserved public README files per sync rules; normalized the appearance seed log label for public naming.
This commit is contained in:
A 2026-07-19 20:38:48 +08:00
parent 0c99ae0a9d
commit bf965f610c
80 changed files with 7212 additions and 349 deletions

View file

@ -2,11 +2,16 @@ package rpc
import (
"context"
"errors"
"time"
"telesrv/internal/domain"
)
const botAPIGetUpdatesLimit = 100
const (
botAPIGetUpdatesLimit = 100
botAPIMaxNegativeOffset = 10000
)
type botAPIChannelBotMemberProvider interface {
ActiveBotMemberIDs(ctx context.Context, viewerUserID, channelID int64, limit int) ([]int64, error)
@ -17,24 +22,43 @@ func (r *Router) botAPIQueuedUpdates(ctx context.Context, botID int64, offset in
return nil, nil
}
fromID := int64(1)
if offset > 0 {
var items []domain.BotAPIUpdate
if offset < 0 {
if offset < -botAPIMaxNegativeOffset {
return nil, errors.New("OFFSET_INVALID")
}
var err error
items, err = r.deps.BotAPIUpdates.ListTailBotAPIUpdates(ctx, botID, int(-offset), botAPIGetUpdatesLimit)
if err != nil {
return nil, err
}
if len(items) > 0 && items[0].ID > 1 {
if err := r.deps.BotAPIUpdates.ConfirmBotAPIUpdates(ctx, botID, items[0].ID-1); err != nil {
return nil, err
}
}
} else if offset > 0 {
if err := r.deps.BotAPIUpdates.ConfirmBotAPIUpdates(ctx, botID, offset-1); err != nil {
return nil, err
}
fromID = offset
} else if confirmed, found, err := r.deps.BotAPIUpdates.ConfirmedBotAPIUpdateID(ctx, botID); err != nil {
return nil, err
} else if found {
fromID = confirmed + 1
}
items, err := r.deps.BotAPIUpdates.ListBotAPIUpdates(ctx, botID, fromID, botAPIGetUpdatesLimit)
if err != nil {
return nil, err
if offset >= 0 {
confirmed, found, err := r.deps.BotAPIUpdates.ConfirmedBotAPIUpdateID(ctx, botID)
if err != nil {
return nil, err
}
if found {
fromID = confirmed + 1
}
items, err = r.deps.BotAPIUpdates.ListBotAPIUpdates(ctx, botID, fromID, botAPIGetUpdatesLimit)
if err != nil {
return nil, err
}
}
if len(items) == 0 {
return nil, nil
}
events, leadingSkipped := r.botAPIQueuedUpdateEvents(ctx, botID, items)
events, leadingSkipped := r.botAPIQueuedUpdateEvents(ctx, botID, items, r.clock.Now())
if leadingSkipped > 0 {
if err := r.deps.BotAPIUpdates.ConfirmBotAPIUpdates(ctx, botID, leadingSkipped); err != nil {
return nil, err
@ -46,13 +70,16 @@ func (r *Router) botAPIQueuedUpdates(ctx context.Context, botID int64, offset in
return r.enrichUpdateEvents(ctx, botID, events), nil
}
func (r *Router) botAPIQueuedUpdateEvents(ctx context.Context, botID int64, items []domain.BotAPIUpdate) ([]domain.UpdateEvent, int64) {
func (r *Router) botAPIQueuedUpdateEvents(ctx context.Context, botID int64, items []domain.BotAPIUpdate, now time.Time) ([]domain.UpdateEvent, int64) {
privateIDs := make([]int, 0)
privateSeen := make(map[int]struct{})
channelIDs := make(map[int64][]int)
channelSeen := make(map[int64]map[int]struct{})
for _, item := range items {
if _, ok := botAPIQueuedUpdateKind(botID, item); !ok {
if _, ok := botAPIQueuedUpdateKind(botID, item, now); !ok {
continue
}
if item.Callback != nil && item.Callback.InlineMessage != nil {
continue
}
switch item.Peer.Type {
@ -79,7 +106,7 @@ func (r *Router) botAPIQueuedUpdateEvents(ctx context.Context, botID int64, item
events := make([]domain.UpdateEvent, 0, len(items))
leadingSkipped := int64(0)
for _, item := range items {
event, ok := botAPIQueuedUpdateEventFromMessages(botID, item, privateMessages, channelMessages)
event, ok := botAPIQueuedUpdateEventFromMessages(botID, item, privateMessages, channelMessages, now)
if !ok {
if len(events) == 0 {
leadingSkipped = item.ID
@ -101,7 +128,7 @@ func (r *Router) botAPIQueuedPrivateMessages(ctx context.Context, botID int64, i
}
out := make(map[int]domain.Message, len(list.Messages))
for _, msg := range list.Messages {
if msg.ID <= 0 || msg.Out || !botAPIMessageProjectable(msg) {
if msg.ID <= 0 || msg.OwnerUserID != botID {
continue
}
out[msg.ID] = msg
@ -127,10 +154,6 @@ func (r *Router) botAPIQueuedChannelMessages(ctx context.Context, botID int64, i
if msg.ID <= 0 || msg.Deleted || msg.Action != nil {
continue
}
projected := botAPIMessageFromChannel(botID, msg)
if projected.Out || !botAPIMessageProjectable(projected) {
continue
}
byID[msg.ID] = msg
}
if len(byID) > 0 {
@ -140,14 +163,37 @@ func (r *Router) botAPIQueuedChannelMessages(ctx context.Context, botID int64, i
return out
}
func botAPIQueuedUpdateKind(botID int64, item domain.BotAPIUpdate) (domain.UpdateEventType, bool) {
if item.ID <= 0 || item.BotUserID != botID || item.MessageID <= 0 {
func botAPIQueuedUpdateKind(botID int64, item domain.BotAPIUpdate, now time.Time) (domain.UpdateEventType, bool) {
if item.ID <= 0 || item.BotUserID != botID {
return "", false
}
eventType, ok := botAPIUpdateEventType(item.Kind)
if !ok {
return "", false
}
if item.Kind == domain.BotAPIUpdateCallbackQuery {
if item.Date <= 0 || !now.Before(time.Unix(int64(item.Date), 0).Add(botCallbackTimeout)) {
return "", false
}
cb := item.Callback
if cb == nil || cb.ID == 0 || cb.BotUserID != botID || cb.UserID <= 0 ||
cb.ChatInstance == 0 || len(cb.Data) > domain.MaxCallbackDataLen {
return "", false
}
if cb.InlineMessage != nil {
inline := cb.InlineMessage
if item.MessageID != 0 || item.Peer != (domain.Peer{}) || cb.MessageID != 0 || cb.Peer != (domain.Peer{}) ||
inline.DCID <= 0 || inline.OwnerID == 0 || inline.ID <= 0 || inline.AccessHash == 0 {
return "", false
}
return eventType, true
}
if item.MessageID <= 0 || cb.Peer != item.Peer || cb.MessageID != item.MessageID {
return "", false
}
} else if item.MessageID <= 0 {
return "", false
}
switch item.Peer.Type {
case domain.PeerTypeUser, domain.PeerTypeChannel:
if item.Peer.ID <= 0 {
@ -159,17 +205,48 @@ func botAPIQueuedUpdateKind(botID int64, item domain.BotAPIUpdate) (domain.Updat
return eventType, true
}
func botAPIQueuedUpdateEventFromMessages(botID int64, item domain.BotAPIUpdate, privateMessages map[int]domain.Message, channelMessages map[int64]map[int]domain.ChannelMessage) (domain.UpdateEvent, bool) {
eventType, ok := botAPIQueuedUpdateKind(botID, item)
func botAPIQueuedUpdateEventFromMessages(botID int64, item domain.BotAPIUpdate, privateMessages map[int]domain.Message, channelMessages map[int64]map[int]domain.ChannelMessage, now time.Time) (domain.UpdateEvent, bool) {
eventType, ok := botAPIQueuedUpdateKind(botID, item, now)
if !ok {
return domain.UpdateEvent{}, false
}
if eventType == domain.UpdateEventBotCallbackQuery && item.Callback.InlineMessage != nil {
callback := *item.Callback
callback.Data = append([]byte(nil), item.Callback.Data...)
inline := *item.Callback.InlineMessage
callback.InlineMessage = &inline
return domain.UpdateEvent{
UserID: botID,
Type: eventType,
Pts: int(item.ID),
PtsCount: 1,
Date: item.Date,
BotCallbackQuery: &callback,
}, true
}
switch item.Peer.Type {
case domain.PeerTypeUser:
msg, found := privateMessages[item.MessageID]
if !found {
return domain.UpdateEvent{}, false
}
if eventType == domain.UpdateEventBotCallbackQuery {
callback := *item.Callback
callback.Data = append([]byte(nil), item.Callback.Data...)
return domain.UpdateEvent{
UserID: botID,
Type: eventType,
Pts: int(item.ID),
PtsCount: 1,
Date: item.Date,
Peer: item.Peer,
Message: msg,
BotCallbackQuery: &callback,
}, true
}
if msg.Out || !botAPIMessageProjectable(msg) {
return domain.UpdateEvent{}, false
}
msg.Pts = int(item.ID)
return domain.UpdateEvent{
UserID: botID,
@ -186,6 +263,23 @@ func botAPIQueuedUpdateEventFromMessages(botID int64, item domain.BotAPIUpdate,
return domain.UpdateEvent{}, false
}
projected := botAPIMessageFromChannel(botID, msg)
if eventType == domain.UpdateEventBotCallbackQuery {
callback := *item.Callback
callback.Data = append([]byte(nil), item.Callback.Data...)
return domain.UpdateEvent{
UserID: botID,
Type: eventType,
Pts: int(item.ID),
PtsCount: 1,
Date: item.Date,
Peer: item.Peer,
Message: projected,
BotCallbackQuery: &callback,
}, true
}
if projected.Out || !botAPIMessageProjectable(projected) {
return domain.UpdateEvent{}, false
}
projected.Pts = int(item.ID)
return domain.UpdateEvent{
UserID: botID,
@ -207,6 +301,8 @@ func botAPIUpdateEventType(kind domain.BotAPIUpdateKind) (domain.UpdateEventType
return domain.UpdateEventNewMessage, true
case domain.BotAPIUpdateEditedMessage:
return domain.UpdateEventEditMessage, true
case domain.BotAPIUpdateCallbackQuery:
return domain.UpdateEventBotCallbackQuery, true
default:
return "", false
}
@ -410,7 +506,56 @@ func botAPIMessageMediaProjectable(media *domain.MessageMedia) bool {
return media.Photo != nil
case domain.MessageMediaKindDocument:
return media.Document != nil
case domain.MessageMediaKindContact:
return media.Contact != nil
case domain.MessageMediaKindGeo:
return media.Geo != nil
case domain.MessageMediaKindVenue:
return media.Venue != nil
case domain.MessageMediaKindPoll:
return media.Poll != nil
case domain.MessageMediaKindGeoLive:
return media.GeoLive != nil
case domain.MessageMediaKindService:
if media.ServiceAction == nil {
return false
}
switch media.ServiceAction.Kind {
case domain.MessageServiceActionWebViewDataSent:
return media.ServiceAction.WebViewData != nil
case domain.MessageServiceActionRequestedPeer:
return botAPIRequestedPeerProjectable(media.ServiceAction.RequestedPeer)
default:
return false
}
default:
return false
}
}
func botAPIRequestedPeerProjectable(action *domain.MessageRequestedPeerAction) bool {
if action == nil || action.ButtonID == 0 || len(action.Peers) == 0 || len(action.Peers) > domain.MaxBotRequestedPeerQuantity {
return false
}
details := make(map[domain.Peer]struct{}, len(action.Details))
for _, detail := range action.Details {
if detail.Peer.ID == 0 || (detail.Peer.Type != domain.PeerTypeUser && detail.Peer.Type != domain.PeerTypeChannel) {
return false
}
details[detail.Peer] = struct{}{}
}
requiresDetails := action.NameRequested || action.UsernameRequested || action.PhotoRequested
allUsers := true
for _, peer := range action.Peers {
if peer.ID == 0 || (peer.Type != domain.PeerTypeUser && peer.Type != domain.PeerTypeChannel) {
return false
}
if requiresDetails {
if _, ok := details[peer]; !ok {
return false
}
}
allUsers = allUsers && peer.Type == domain.PeerTypeUser
}
return allUsers || (len(action.Peers) == 1 && action.Peers[0].Type == domain.PeerTypeChannel)
}