389 lines
13 KiB
Go
389 lines
13 KiB
Go
package rpc
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
|
||
"github.com/iamxvbaba/td/tg"
|
||
|
||
"telesrv/internal/domain"
|
||
)
|
||
|
||
// registerBots 注册 bots.* RPC handler。
|
||
//
|
||
// 权限语义(官方访问矩阵):
|
||
// - setBotCommands/getBotCommands/resetBotCommands/setBotMenuButton/getBotMenuButton:
|
||
// 仅 bot 自己(调用者必须是 bot 账号)。
|
||
// - setBotInfo/getBotInfo:owner 经 bot:InputUser 代设,或 bot 自己(不带 bot 参数)。
|
||
//
|
||
// P2 范围:仅 default scope、单语言(非 default scope 与非空 lang_code 一律接受
|
||
// 但不存储,避免覆盖全局——见各 handler 的 isDefaultBotCommandScope/lang_code 闸门);
|
||
// menu button 为 per-bot 全局(per-user 维度记 todo)。元数据写入由 service 在事务内
|
||
// bump bot_info_version;命令变更后 bots_hooks.PushBotCommandsChanged 给在线相关用户
|
||
// 推 updateBotCommands(扇出封顶 100,无 pts),离线/超界用户靠 version bump 在下次
|
||
// getFullUser 重拉兜底。
|
||
func (r *Router) registerBots(d *tg.ServerDispatcher) {
|
||
d.OnBotsSendCustomRequest(r.onBotsSendCustomRequest)
|
||
d.OnBotsAnswerWebhookJSONQuery(r.onBotsAnswerWebhookJSONQuery)
|
||
d.OnBotsSetBotBroadcastDefaultAdminRights(r.onBotsSetBotBroadcastDefaultAdminRights)
|
||
d.OnBotsSetBotGroupDefaultAdminRights(r.onBotsSetBotGroupDefaultAdminRights)
|
||
d.OnBotsSetBotCommands(r.onBotsSetBotCommands)
|
||
d.OnBotsResetBotCommands(r.onBotsResetBotCommands)
|
||
d.OnBotsGetBotCommands(r.onBotsGetBotCommands)
|
||
d.OnBotsSetBotInfo(r.onBotsSetBotInfo)
|
||
d.OnBotsGetBotInfo(r.onBotsGetBotInfo)
|
||
d.OnBotsSetBotMenuButton(r.onBotsSetBotMenuButton)
|
||
d.OnBotsGetBotMenuButton(r.onBotsGetBotMenuButton)
|
||
d.OnBotsReorderUsernames(r.onBotsReorderUsernames)
|
||
d.OnBotsToggleUsername(r.onBotsToggleUsername)
|
||
d.OnBotsCanSendMessage(r.onBotsCanSendMessage)
|
||
d.OnBotsAllowSendMessage(r.onBotsAllowSendMessage)
|
||
d.OnBotsInvokeWebViewCustomMethod(r.onBotsInvokeWebViewCustomMethod)
|
||
d.OnBotsGetPopularAppBots(r.onBotsGetPopularAppBots)
|
||
d.OnBotsAddPreviewMedia(r.onBotsAddPreviewMedia)
|
||
d.OnBotsEditPreviewMedia(r.onBotsEditPreviewMedia)
|
||
d.OnBotsDeletePreviewMedia(r.onBotsDeletePreviewMedia)
|
||
d.OnBotsReorderPreviewMedias(r.onBotsReorderPreviewMedias)
|
||
d.OnBotsGetPreviewInfo(r.onBotsGetPreviewInfo)
|
||
d.OnBotsGetPreviewMedias(r.onBotsGetPreviewMedias)
|
||
d.OnBotsUpdateUserEmojiStatus(r.onBotsUpdateUserEmojiStatus)
|
||
d.OnBotsToggleUserEmojiStatusPermission(r.onBotsToggleUserEmojiStatusPermission)
|
||
d.OnBotsCheckDownloadFileParams(r.onBotsCheckDownloadFileParams)
|
||
d.OnBotsGetAdminedBots(r.onBotsGetAdminedBots)
|
||
d.OnBotsUpdateStarRefProgram(r.onBotsUpdateStarRefProgram)
|
||
d.OnBotsSetCustomVerification(r.onBotsSetCustomVerification)
|
||
d.OnBotsGetBotRecommendations(r.onBotsGetBotRecommendations)
|
||
d.OnBotsCheckUsername(r.onBotsCheckUsername)
|
||
d.OnBotsCreateBot(r.onBotsCreateBot)
|
||
d.OnBotsExportBotToken(r.onBotsExportBotToken)
|
||
d.OnBotsRequestWebViewButton(r.onBotsRequestWebViewButton)
|
||
d.OnBotsGetRequestedWebViewButton(r.onBotsGetRequestedWebViewButton)
|
||
d.OnBotsGetAccessSettings(r.onBotsGetAccessSettings)
|
||
d.OnBotsEditAccessSettings(r.onBotsEditAccessSettings)
|
||
// P3:startBot 深链 + inline callback 闭环。
|
||
d.OnMessagesStartBot(r.onMessagesStartBot)
|
||
d.OnMessagesGetBotCallbackAnswer(r.onMessagesGetBotCallbackAnswer)
|
||
d.OnMessagesSetBotCallbackAnswer(r.onMessagesSetBotCallbackAnswer)
|
||
d.OnMessagesGetInlineBotResults(r.onMessagesGetInlineBotResults)
|
||
d.OnMessagesSetInlineBotResults(r.onMessagesSetInlineBotResults)
|
||
d.OnMessagesSendInlineBotResult(r.onMessagesSendInlineBotResult)
|
||
d.OnMessagesSavePreparedInlineMessage(r.onMessagesSavePreparedInlineMessage)
|
||
d.OnMessagesEditInlineBotMessage(r.onMessagesEditInlineBotMessage)
|
||
d.OnMessagesSetBotShippingResults(r.onMessagesSetBotShippingResults)
|
||
d.OnMessagesSetBotPrecheckoutResults(r.onMessagesSetBotPrecheckoutResults)
|
||
}
|
||
|
||
// callerBotID 校验调用者本身是 bot 账号,返回其 user_id(bot-only RPC 用)。
|
||
func (r *Router) callerBotID(ctx context.Context) (int64, error) {
|
||
userID, _, err := r.currentUserID(ctx)
|
||
if err != nil {
|
||
return 0, internalErr()
|
||
}
|
||
if r.deps.Bots == nil || userID == 0 {
|
||
return 0, userBotRequiredErr()
|
||
}
|
||
if _, found, err := r.deps.Bots.BotInfo(ctx, userID); err != nil {
|
||
return 0, internalErr()
|
||
} else if !found {
|
||
return 0, userBotRequiredErr()
|
||
}
|
||
return userID, nil
|
||
}
|
||
|
||
func (r *Router) onBotsGetAdminedBots(ctx context.Context) ([]tg.UserClass, error) {
|
||
userID, _, err := r.currentUserID(ctx)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
if r.deps.Bots == nil {
|
||
return []tg.UserClass{}, nil
|
||
}
|
||
bots, err := r.deps.Bots.ListOwnedBots(ctx, userID)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
return r.tgUsersForViewer(userID, bots), nil
|
||
}
|
||
|
||
func (r *Router) onBotsCheckUsername(ctx context.Context, username string) (bool, error) {
|
||
userID, _, err := r.currentUserID(ctx)
|
||
if err != nil {
|
||
return false, internalErr()
|
||
}
|
||
if r.deps.Bots == nil {
|
||
return false, internalErr()
|
||
}
|
||
ok, err := r.deps.Bots.CheckUsername(ctx, userID, username)
|
||
if err != nil {
|
||
return false, botUsernameErr(err)
|
||
}
|
||
return ok, nil
|
||
}
|
||
|
||
func (r *Router) onBotsCreateBot(ctx context.Context, req *tg.BotsCreateBotRequest) (tg.UserClass, error) {
|
||
userID, _, err := r.currentUserID(ctx)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
if r.deps.Bots == nil {
|
||
return nil, internalErr()
|
||
}
|
||
if err := r.validateBotManager(ctx, userID, req.ManagerID); err != nil {
|
||
return nil, err
|
||
}
|
||
u, _, err := r.deps.Bots.CreateBot(ctx, userID, req.Name, req.Username)
|
||
if err != nil {
|
||
return nil, createBotErr(err)
|
||
}
|
||
return r.tgUser(u), nil
|
||
}
|
||
|
||
func (r *Router) validateBotManager(ctx context.Context, currentUserID int64, manager tg.InputUserClass) error {
|
||
if manager == nil || r.deps.Bots == nil {
|
||
return managerPermissionMissingErr()
|
||
}
|
||
u, found, err := r.userFromInput(ctx, currentUserID, manager)
|
||
if err != nil || !found {
|
||
return managerPermissionMissingErr()
|
||
}
|
||
if _, found, err := r.deps.Bots.BotInfo(ctx, u.ID); err != nil {
|
||
return internalErr()
|
||
} else if !found {
|
||
return managerPermissionMissingErr()
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (r *Router) onBotsExportBotToken(ctx context.Context, req *tg.BotsExportBotTokenRequest) (*tg.BotsExportedBotToken, error) {
|
||
userID, _, err := r.currentUserID(ctx)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
if r.deps.Bots == nil {
|
||
return nil, internalErr()
|
||
}
|
||
target, found, err := r.userFromInput(ctx, userID, req.Bot)
|
||
if err != nil || !found {
|
||
return nil, botInvalidErr()
|
||
}
|
||
token, err := r.deps.Bots.ExportBotToken(ctx, userID, target.ID, req.Revoke)
|
||
if err != nil {
|
||
if errors.Is(err, domain.ErrBotSessionsNotRevoked) && token != "" {
|
||
return &tg.BotsExportedBotToken{Token: token}, nil
|
||
}
|
||
return nil, exportBotTokenErr(err)
|
||
}
|
||
return &tg.BotsExportedBotToken{Token: token}, nil
|
||
}
|
||
|
||
// isDefaultCommandsTarget 报告请求是否落在 P2 唯一持久化的桶(default scope +
|
||
// 空 lang_code)。非 default scope 或非空 lang_code 一律接受但不存储——否则会
|
||
// 用某语言/某 scope 的命令覆盖唯一的全局 default 桶(reset 还会误清全局)。
|
||
func isDefaultCommandsTarget(scope tg.BotCommandScopeClass, langCode string) bool {
|
||
return isDefaultBotCommandScope(scope) && langCode == ""
|
||
}
|
||
|
||
func (r *Router) onBotsSetBotCommands(ctx context.Context, req *tg.BotsSetBotCommandsRequest) (bool, error) {
|
||
botID, err := r.callerBotID(ctx)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
// P2 仅持久化 default scope + 空 lang_code;其它接受但不存储(记 todo),避免
|
||
// 覆盖全局桶,也避免客户端因 error 重试。
|
||
if !isDefaultCommandsTarget(req.Scope, req.LangCode) {
|
||
return true, nil
|
||
}
|
||
if _, err := r.deps.Bots.SetBotCommands(ctx, botID, domainBotCommands(req.Commands)); err != nil {
|
||
return false, setBotCommandsErr(err)
|
||
}
|
||
r.invalidateChannelFullBotInfoCache()
|
||
r.invalidateRPCProjectionForUser(botID)
|
||
return true, nil
|
||
}
|
||
|
||
func (r *Router) onBotsResetBotCommands(ctx context.Context, req *tg.BotsResetBotCommandsRequest) (bool, error) {
|
||
botID, err := r.callerBotID(ctx)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
if !isDefaultCommandsTarget(req.Scope, req.LangCode) {
|
||
return true, nil
|
||
}
|
||
if _, err := r.deps.Bots.SetBotCommands(ctx, botID, nil); err != nil {
|
||
return false, setBotCommandsErr(err)
|
||
}
|
||
r.invalidateChannelFullBotInfoCache()
|
||
r.invalidateRPCProjectionForUser(botID)
|
||
return true, nil
|
||
}
|
||
|
||
func (r *Router) onBotsGetBotCommands(ctx context.Context, req *tg.BotsGetBotCommandsRequest) ([]tg.BotCommand, error) {
|
||
botID, err := r.callerBotID(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if !isDefaultCommandsTarget(req.Scope, req.LangCode) {
|
||
return []tg.BotCommand{}, nil
|
||
}
|
||
commands, err := r.deps.Bots.GetBotCommands(ctx, botID)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
return tgBotCommands(commands), nil
|
||
}
|
||
|
||
// resolveBotInfoTarget 解析 setBotInfo/getBotInfo 的目标 bot:带 bot 参数→owner 校验
|
||
// 该 bot;不带→调用者本身须为 bot。
|
||
func (r *Router) resolveBotInfoTarget(ctx context.Context, bot tg.InputUserClass, hasBot bool) (int64, error) {
|
||
userID, _, err := r.currentUserID(ctx)
|
||
if err != nil {
|
||
return 0, internalErr()
|
||
}
|
||
if r.deps.Bots == nil {
|
||
return 0, userBotInvalidErr()
|
||
}
|
||
if hasBot && bot != nil {
|
||
target, found, err := r.userFromInput(ctx, userID, bot)
|
||
if err != nil || !found {
|
||
return 0, botInvalidErr()
|
||
}
|
||
owns, err := r.deps.Bots.OwnsBot(ctx, userID, target.ID)
|
||
if err != nil {
|
||
return 0, internalErr()
|
||
}
|
||
if !owns {
|
||
return 0, botInvalidErr()
|
||
}
|
||
return target.ID, nil
|
||
}
|
||
// 无 bot 参数:调用者必须是 bot 自己。
|
||
if _, found, err := r.deps.Bots.BotInfo(ctx, userID); err != nil {
|
||
return 0, internalErr()
|
||
} else if !found {
|
||
return 0, userBotInvalidErr()
|
||
}
|
||
return userID, nil
|
||
}
|
||
|
||
func (r *Router) onBotsSetBotInfo(ctx context.Context, req *tg.BotsSetBotInfoRequest) (bool, error) {
|
||
bot, hasBot := req.GetBot()
|
||
botID, err := r.resolveBotInfoTarget(ctx, bot, hasBot)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
// 非空 lang_code:本地化 name/about/description 接受但不存储(否则写穿全局列)。
|
||
if req.LangCode != "" {
|
||
return true, nil
|
||
}
|
||
var upd domain.BotInfoUpdate
|
||
if name, ok := req.GetName(); ok {
|
||
upd.SetName, upd.Name = true, name
|
||
}
|
||
if about, ok := req.GetAbout(); ok {
|
||
upd.SetAbout, upd.About = true, about
|
||
}
|
||
if description, ok := req.GetDescription(); ok {
|
||
upd.SetDescription, upd.Description = true, description
|
||
}
|
||
if !upd.SetName && !upd.SetAbout && !upd.SetDescription {
|
||
return true, nil
|
||
}
|
||
if _, err := r.deps.Bots.SetBotInfo(ctx, botID, upd); err != nil {
|
||
return false, setBotInfoErr(err)
|
||
}
|
||
r.invalidateChannelFullBotInfoCache()
|
||
r.invalidateRPCProjectionForUser(botID)
|
||
return true, nil
|
||
}
|
||
|
||
func (r *Router) onBotsGetBotInfo(ctx context.Context, req *tg.BotsGetBotInfoRequest) (*tg.BotsBotInfo, error) {
|
||
bot, hasBot := req.GetBot()
|
||
botID, err := r.resolveBotInfoTarget(ctx, bot, hasBot)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
name, about, description, err := r.deps.Bots.GetBotInfo(ctx, botID)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
return &tg.BotsBotInfo{Name: name, About: about, Description: description}, nil
|
||
}
|
||
|
||
func (r *Router) onBotsSetBotMenuButton(ctx context.Context, req *tg.BotsSetBotMenuButtonRequest) (bool, error) {
|
||
botID, err := r.callerBotID(ctx)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
button, err := domainBotMenuButton(req.Button)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
if _, err := r.deps.Bots.SetBotMenuButton(ctx, botID, button); err != nil {
|
||
return false, setBotMenuButtonErr(err)
|
||
}
|
||
r.invalidateChannelFullBotInfoCache()
|
||
r.invalidateRPCProjectionForUser(botID)
|
||
return true, nil
|
||
}
|
||
|
||
func (r *Router) onBotsGetBotMenuButton(ctx context.Context, userid tg.InputUserClass) (tg.BotMenuButtonClass, error) {
|
||
botID, err := r.callerBotID(ctx)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
button, err := r.deps.Bots.GetBotMenuButton(ctx, botID)
|
||
if err != nil {
|
||
return nil, internalErr()
|
||
}
|
||
return tgBotMenuButton(button), nil
|
||
}
|
||
|
||
// --- tg ↔ domain 转换 ---
|
||
|
||
func isDefaultBotCommandScope(scope tg.BotCommandScopeClass) bool {
|
||
if scope == nil {
|
||
return true
|
||
}
|
||
_, ok := scope.(*tg.BotCommandScopeDefault)
|
||
return ok
|
||
}
|
||
|
||
func domainBotCommands(in []tg.BotCommand) []domain.BotCommand {
|
||
out := make([]domain.BotCommand, 0, len(in))
|
||
for _, c := range in {
|
||
out = append(out, domain.BotCommand{Command: c.Command, Description: c.Description})
|
||
}
|
||
return out
|
||
}
|
||
|
||
func tgBotCommands(in []domain.BotCommand) []tg.BotCommand {
|
||
out := make([]tg.BotCommand, 0, len(in))
|
||
for _, c := range in {
|
||
out = append(out, tg.BotCommand{Command: c.Command, Description: c.Description})
|
||
}
|
||
return out
|
||
}
|
||
|
||
func domainBotMenuButton(in tg.BotMenuButtonClass) (domain.BotMenuButton, error) {
|
||
switch v := in.(type) {
|
||
case *tg.BotMenuButtonDefault:
|
||
return domain.BotMenuButton{Type: domain.BotMenuButtonDefault}, nil
|
||
case *tg.BotMenuButtonCommands:
|
||
return domain.BotMenuButton{Type: domain.BotMenuButtonCommands}, nil
|
||
case *tg.BotMenuButton:
|
||
return domain.BotMenuButton{Type: domain.BotMenuButtonWebView, Text: v.Text, URL: v.URL}, nil
|
||
default:
|
||
return domain.BotMenuButton{}, botMenuButtonInvalidErr()
|
||
}
|
||
}
|
||
|
||
func tgBotMenuButton(b domain.BotMenuButton) tg.BotMenuButtonClass {
|
||
switch b.Type {
|
||
case domain.BotMenuButtonCommands:
|
||
return &tg.BotMenuButtonCommands{}
|
||
case domain.BotMenuButtonWebView:
|
||
return &tg.BotMenuButton{Text: b.Text, URL: b.URL}
|
||
default:
|
||
return &tg.BotMenuButtonDefault{}
|
||
}
|
||
}
|