123 lines
4.3 KiB
Go
123 lines
4.3 KiB
Go
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
|
||
}
|