owpengram-server/internal/rpc/bots_hooks.go

102 lines
4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rpc
import (
"context"
"time"
"github.com/gotd/td/tg"
"go.uber.org/zap"
"telesrv/internal/domain"
)
// 本文件实现 app/bots 的 RouterHooks 回调token revoke 后的 session 失效闭环,
// 以及命令变更后的 updateBotCommands 在线推送。Router 创建后经
// botsService.SetRouterHooks(router) 装配(见 cmd/telesrv/main.go
// maxBotCommandsPushPeers 限制单次命令变更的推送扇出bot 的最近 dialog peer 数)。
// 超出的离线/长尾用户靠 bot_info_version bump 在下次 getFullUser 时拿到新命令。
const maxBotCommandsPushPeers = 100
// RevokeBotSessions 撤销 bot 的全部已登录 session删除全部 authorization 行并
// 强制断开在线连接(与 account.resetAuthorization 被踢闭环同款顺序)。
func (r *Router) RevokeBotSessions(ctx context.Context, botUserID int64) error {
if r.deps.Auth == nil || botUserID == 0 {
return nil
}
deleted, err := r.deps.Auth.ResetAuthorizations(ctx, botUserID, [8]byte{})
if err != nil {
return err
}
for _, a := range deleted {
r.revokeAuthKeySessions(a.AuthKeyID)
if err := r.clearAuthKeyState(ctx, a.AuthKeyID); err != nil {
r.log.Warn("revoke bot sessions: clear auth key state",
zap.Int64("bot_user_id", botUserID), zap.Error(err))
}
}
if len(deleted) > 0 {
r.log.Info("revoked bot sessions", zap.Int64("bot_user_id", botUserID), zap.Int("count", len(deleted)))
}
return nil
}
// PushBotCommandsChanged 给「与该 bot 有私聊 dialog 且在线」的用户推
// updateBotCommandspeer = 该 bot 的 user peer对齐 TDesktop/DrKLO 消费语义)。
// updateBotCommands 无 pts/qts不进 getDifference——离线用户由随写库一起完成的
// bot_info_version bump 兜底(下次 getFullUser 重拉命令)。
//
// fire-and-forget在独立 goroutine 内执行(脱离已返回的 setBotCommands RPC ctx
// 否则 GetDialogs + 最多 maxBotCommandsPushPeers 次 best-effort 推送会把 RPC 响应
// 拖到拥塞超时之和、并跨用户占住 BotFather 条带锁。推送是纯通知,丢失靠 version
// bump 兜底,不需保证投达。扇出有界:只取 bot dialog 列表前 maxBotCommandsPushPeers
// 个 user peer再按在线快照过滤超界部分走版本兜底有意取舍
func (r *Router) PushBotCommandsChanged(ctx context.Context, botUserID int64, commands []domain.BotCommand) {
if r.deps.Dialogs == nil || r.deps.Sessions == nil || botUserID == 0 {
return
}
// 拷贝命令切片调用方service可能复用底层数组。
cmds := append([]domain.BotCommand(nil), commands...)
go r.pushBotCommandsChanged(context.WithoutCancel(ctx), botUserID, cmds)
}
func (r *Router) pushBotCommandsChanged(ctx context.Context, botUserID int64, commands []domain.BotCommand) {
defer func() {
if rec := recover(); rec != nil {
r.log.Error("push bot commands panicked", zap.Int64("bot_user_id", botUserID), zap.Any("panic", rec))
}
}()
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
list, err := r.deps.Dialogs.GetDialogs(ctx, botUserID, domain.DialogFilter{Limit: maxBotCommandsPushPeers})
if err != nil {
r.log.Warn("push bot commands: list bot dialogs", zap.Int64("bot_user_id", botUserID), zap.Error(err))
return
}
candidates := make([]int64, 0, len(list.Dialogs))
for _, dialog := range list.Dialogs {
if dialog.Peer.Type == domain.PeerTypeUser && dialog.Peer.ID != 0 && dialog.Peer.ID != botUserID {
candidates = append(candidates, dialog.Peer.ID)
}
}
if len(candidates) == 0 {
return
}
if provider, ok := r.deps.Sessions.(OnlineUserProvider); ok {
candidates = provider.OnlineUserIDsForCandidates(candidates, maxBotCommandsPushPeers)
}
if len(candidates) == 0 {
return
}
update := &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdateBotCommands{
Peer: &tg.PeerUser{UserID: botUserID},
BotID: botUserID,
Commands: tgBotCommands(commands),
}},
Date: int(r.clock.Now().Unix()),
}
for _, userID := range candidates {
r.pushUserMessage(ctx, userID, "push bot commands", update)
}
}