chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

389
internal/rpc/bots.go Normal file
View file

@ -0,0 +1,389 @@
package rpc
import (
"context"
"errors"
"github.com/gotd/td/tg"
"telesrv/internal/domain"
)
// registerBots 注册 bots.* RPC handler。
//
// 权限语义(官方访问矩阵):
// - setBotCommands/getBotCommands/resetBotCommands/setBotMenuButton/getBotMenuButton
// 仅 bot 自己(调用者必须是 bot 账号)。
// - setBotInfo/getBotInfoowner 经 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)
// P3startBot 深链 + 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_idbot-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{}
}
}