owpengram-server/internal/rpc/bots_callback.go

123 lines
4.3 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"
"unicode/utf8"
"github.com/gotd/td/tg"
"github.com/gotd/td/tgerr"
"telesrv/internal/domain"
)
// botCallbackTimeout 是 getBotCallbackAnswer 的挂起上限:bot 未在窗口内
// setBotCallbackAnswer 即回 BOT_RESPONSE_TIMEOUT(不快速失败,给 bot 上线追答的窗口)。
const botCallbackTimeout = 25 * time.Second
func botResponseTimeoutErr() error { return tgerr.New(502, "BOT_RESPONSE_TIMEOUT") }
func dataInvalidErr() error { return tgerr.New(400, "DATA_INVALID") }
// onMessagesGetBotCallbackAnswer 处理 inline callback 按钮点击:把 updateBotCallbackQuery
// 推给 bot,挂起等待 bot 的 setBotCallbackAnswer,或超时回 BOT_RESPONSE_TIMEOUT。
func (r *Router) onMessagesGetBotCallbackAnswer(ctx context.Context, req *tg.MessagesGetBotCallbackAnswerRequest) (*tg.MessagesBotCallbackAnswer, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if userID == 0 {
return nil, peerIDInvalidErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
if err != nil {
return nil, err
}
// callback 按钮只存在于 bot 的私聊消息。peer 必须是 bot 用户。
if peer.Type != domain.PeerTypeUser || !r.userIsBot(ctx, peer.ID) {
return nil, dataInvalidErr()
}
botUserID := peer.ID
// game 按钮(getBotCallbackAnswer.game)P3 不支持:返回空答案(客户端不弹任何东西),
// 不挂起、不推送(避免给 bot 投递无法处理的 game query)。
if req.Game {
return &tg.MessagesBotCallbackAnswer{}, nil
}
data, hasData := req.GetData()
if !hasData {
return nil, dataInvalidErr()
}
// 校验目标消息存在于请求者自己的盒、且对端正是该 bot。
if req.MsgID <= 0 || req.MsgID > domain.MaxMessageBoxID {
return nil, messageIDInvalidErr()
}
msg, ok, err := r.lookupOwnerMessage(ctx, userID, req.MsgID)
if err != nil {
return nil, internalErr()
}
if !ok || msg.Peer != peer {
return nil, messageIDInvalidErr()
}
queryID, pending := r.callbacks.register(botUserID, userID)
defer r.callbacks.deregister(queryID)
// updateBotCallbackQuery 是 ephemeral(无 pts/qts,不进 getDifference):仅在线推给
// bot;bot 离线则投递 0,但仍走超时窗口(I5,给 bot 上线追答机会)。
// MsgID 透传请求者侧的 box id(P3 不做 bot 侧 box id 翻译——bot 侧消息编辑后移,记 todo)。
update := &tg.UpdateBotCallbackQuery{
QueryID: queryID,
UserID: userID,
Peer: &tg.PeerUser{UserID: userID},
MsgID: req.MsgID,
ChatInstance: chatInstanceFor(botUserID, userID),
}
update.SetData(data)
r.pushUserMessage(ctx, botUserID, "push bot callback query", &tg.Updates{
Updates: []tg.UpdateClass{update},
Date: int(r.clock.Now().Unix()),
})
waitCtx, cancel := context.WithTimeout(ctx, botCallbackTimeout)
defer cancel()
select {
case ans := <-pending.ch:
return tgBotCallbackAnswer(ans), nil
case <-waitCtx.Done():
return nil, botResponseTimeoutErr()
}
}
// onMessagesSetBotCallbackAnswer 是 bot 对一次 callback query 的应答:解挂等待中的
// getBotCallbackAnswer。仅属主 bot 可解挂(callerBotID==pending.botUserID,I6)。
func (r *Router) onMessagesSetBotCallbackAnswer(ctx context.Context, req *tg.MessagesSetBotCallbackAnswerRequest) (bool, error) {
botID, err := r.callerBotID(ctx)
if err != nil {
return false, err
}
ans := domain.BotCallbackAnswer{Alert: req.Alert, CacheTime: req.CacheTime}
if msg, ok := req.GetMessage(); ok {
if utf8.RuneCountInString(msg) > domain.MaxBotCallbackAnswerLen {
return false, messageTooLongErr()
}
ans.Message = msg
}
if url, ok := req.GetURL(); ok {
ans.URL = url
}
// resolve 返回是否投递成功;未注册/超时/非属主一律 false。对 bot 而言答案是否
// 被等待者接收无关紧要(官方恒返回 true),但非属主必须拒绝投递(防钓鱼弹窗)。
r.callbacks.resolve(botID, req.QueryID, ans)
return true, nil
}
func tgBotCallbackAnswer(ans domain.BotCallbackAnswer) *tg.MessagesBotCallbackAnswer {
out := &tg.MessagesBotCallbackAnswer{Alert: ans.Alert, CacheTime: ans.CacheTime}
if ans.Message != "" {
out.SetMessage(ans.Message)
}
if ans.URL != "" {
out.SetURL(ans.URL)
out.HasURL = true
}
return out
}