package rpc import ( "context" "errors" "strconv" "github.com/iamxvbaba/td/tg" "github.com/iamxvbaba/td/tlprofile" "telesrv/internal/compat/tdesktop" "telesrv/internal/domain" ) // registerPayments 注册 payments.* RPC:Stars 本地账本(余额/流水真实化)+ 其余 // gift/auction/revenue 第一阶段兼容桩。 func (r *Router) registerPayments(d *tlprofile.Dispatcher) { registerRPC[*tg.PaymentsGetStarsTopupOptionsRequest](d, tlprofile.SemanticMethodPaymentsGetStarsTopupOptions, func(ctx context.Context, layerRequest *tg.PaymentsGetStarsTopupOptionsRequest) (any, // premium 订阅赠送 telesrv 不实现(无支付流),返回空选项。关键作用:TDesktop 送礼框 // ShowStarGiftBox 的 ready() 门控要求 getPremiumGiftCodeOptions 成功返回(on_next)才置 // premiumGiftsReady=true,否则整框不弹出——此前返 NOT_IMPLEMENTED 导致点生日礼物无反应。 // 空列表即解门,星礼物正常发送;premium 区段另由 userFull.disallow_premium_gifts=true 隐藏。 error) { return devStarsTopupOptions(), nil }) registerRPC[*tg.PaymentsGetPremiumGiftCodeOptionsRequest](d, tlprofile.SemanticMethodPaymentsGetPremiumGiftCodeOptions, func(ctx context.Context, req *tg.PaymentsGetPremiumGiftCodeOptionsRequest) (any, error) { return []tg.PremiumGiftCodeOption{}, nil }) registerRPC[*tg.PaymentsGetStarsStatusRequest](d, tlprofile.SemanticMethodPaymentsGetStarsStatus, func(ctx context.Context, layerRequest *tg.PaymentsGetStarsStatusRequest) (any, error) { return r.onPaymentsGetStarsStatus(ctx, layerRequest) }) registerRPC[*tg.PaymentsGetStarsTransactionsRequest](d, tlprofile.SemanticMethodPaymentsGetStarsTransactions, func(ctx context.Context, layerRequest *tg.PaymentsGetStarsTransactionsRequest) (any, error) { return r.onPaymentsGetStarsTransactions(ctx, layerRequest) }) registerRPC[*tg.PaymentsGetStarGiftActiveAuctionsRequest](d, tlprofile.SemanticMethodPaymentsGetStarGiftActiveAuctions, func(ctx context.Context, layerRequest *tg.PaymentsGetStarGiftActiveAuctionsRequest) (any, error) { hash := layerRequest. Hash _ = hash return tdesktop.StarGiftActiveAuctions(), nil }) registerRPC[*tg.PaymentsGetStarGiftsRequest](d, tlprofile.SemanticMethodPaymentsGetStarGifts, func(ctx context.Context, layerRequest *tg.PaymentsGetStarGiftsRequest) (any, error) { return r.onPaymentsGetStarGifts(ctx, layerRequest. Hash) }) registerRPC[*tg.PaymentsGetStarGiftUpgradePreviewRequest](d, tlprofile.SemanticMethodPaymentsGetStarGiftUpgradePreview, func(ctx context.Context, layerRequest *tg.PaymentsGetStarGiftUpgradePreviewRequest) (any, error) { return r.onPaymentsGetStarGiftUpgradePreview(ctx, layerRequest. GiftID) }) registerRPC[*tg.PaymentsGetUniqueStarGiftRequest](d, tlprofile.SemanticMethodPaymentsGetUniqueStarGift, func(ctx context.Context, layerRequest *tg.PaymentsGetUniqueStarGiftRequest) (any, error) { return r.onPaymentsGetUniqueStarGift(ctx, layerRequest. Slug) }) registerRPC[*tg.PaymentsGetPaymentFormRequest](d, tlprofile.SemanticMethodPaymentsGetPaymentForm, func(ctx context.Context, layerRequest *tg.PaymentsGetPaymentFormRequest) (any, error) { return r.onPaymentsGetPaymentForm(ctx, layerRequest) }) registerRPC[*tg.PaymentsSendStarsFormRequest](d, tlprofile.SemanticMethodPaymentsSendStarsForm, func(ctx context.Context, layerRequest *tg.PaymentsSendStarsFormRequest) (any, error) { return r.onPaymentsSendStarsForm(ctx, layerRequest) }) registerRPC[*tg.PaymentsGetSavedStarGiftsRequest](d, tlprofile.SemanticMethodPaymentsGetSavedStarGifts, func(ctx context.Context, layerRequest *tg.PaymentsGetSavedStarGiftsRequest) (any, error) { return r.onPaymentsGetSavedStarGifts(ctx, layerRequest) }) registerRPC[*tg.PaymentsGetSavedStarGiftRequest](d, tlprofile.SemanticMethodPaymentsGetSavedStarGift, func(ctx context.Context, layerRequest *tg.PaymentsGetSavedStarGiftRequest) (any, error) { return r.onPaymentsGetSavedStarGift(ctx, layerRequest. Stargift) }) registerRPC[*tg.PaymentsSaveStarGiftRequest](d, tlprofile.SemanticMethodPaymentsSaveStarGift, func(ctx context.Context, layerRequest *tg.PaymentsSaveStarGiftRequest) (any, error) { return r.onPaymentsSaveStarGift(ctx, layerRequest) }) registerRPC[*tg.PaymentsConvertStarGiftRequest](d, tlprofile.SemanticMethodPaymentsConvertStarGift, func(ctx context.Context, layerRequest *tg.PaymentsConvertStarGiftRequest) (any, error) { return r.onPaymentsConvertStarGift(ctx, layerRequest. Stargift) }) registerRPC[*tg.PaymentsUpgradeStarGiftRequest](d, tlprofile.SemanticMethodPaymentsUpgradeStarGift, func(ctx context.Context, layerRequest *tg.PaymentsUpgradeStarGiftRequest) (any, error) { return r.onPaymentsUpgradeStarGift(ctx, layerRequest) }) registerRPC[*tg.PaymentsGetStarGiftCollectionsRequest](d, tlprofile.SemanticMethodPaymentsGetStarGiftCollections, func(ctx context.Context, layerRequest *tg.PaymentsGetStarGiftCollectionsRequest) (any, error) { return r.onPaymentsGetStarGiftCollections(ctx, layerRequest) }) registerRPC[*tg.PaymentsCreateStarGiftCollectionRequest](d, tlprofile.SemanticMethodPaymentsCreateStarGiftCollection, func(ctx context.Context, layerRequest *tg.PaymentsCreateStarGiftCollectionRequest) (any, error) { return r.onPaymentsCreateStarGiftCollection(ctx, layerRequest) }) registerRPC[*tg.PaymentsUpdateStarGiftCollectionRequest](d, tlprofile.SemanticMethodPaymentsUpdateStarGiftCollection, func(ctx context.Context, layerRequest *tg.PaymentsUpdateStarGiftCollectionRequest) (any, error) { return r.onPaymentsUpdateStarGiftCollection(ctx, layerRequest) }) registerRPC[*tg.PaymentsDeleteStarGiftCollectionRequest](d, tlprofile.SemanticMethodPaymentsDeleteStarGiftCollection, func(ctx context.Context, layerRequest *tg.PaymentsDeleteStarGiftCollectionRequest) (any, error) { return r.onPaymentsDeleteStarGiftCollection(ctx, layerRequest) }) registerRPC[*tg.PaymentsReorderStarGiftCollectionsRequest](d, tlprofile.SemanticMethodPaymentsReorderStarGiftCollections, func(ctx context.Context, layerRequest *tg.PaymentsReorderStarGiftCollectionsRequest) (any, error) { return r.onPaymentsReorderStarGiftCollections(ctx, layerRequest) }) registerRPC[*tg.PaymentsToggleStarGiftsPinnedToTopRequest](d, tlprofile.SemanticMethodPaymentsToggleStarGiftsPinnedToTop, func(ctx context.Context, layerRequest *tg.PaymentsToggleStarGiftsPinnedToTopRequest) (any, error) { return r.onPaymentsToggleStarGiftsPinnedToTop(ctx, layerRequest) }) registerRPC[*tg.PaymentsGetStarsRevenueAdsAccountURLRequest](d, tlprofile.SemanticMethodPaymentsGetStarsRevenueAdsAccountURL, func(ctx context.Context, layerRequest *tg.PaymentsGetStarsRevenueAdsAccountURLRequest) (any, error) { peer := layerRequest. Peer _ = peer userID, _, err := r.currentUserID(ctx) if err != nil { return nil, internalErr() } if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, peer); err != nil { return nil, err } return &tg.PaymentsStarsRevenueAdsAccountURL{URL: "https://ads.telegram.org/"}, nil }) registerRPC[*tg.PaymentsGetStarsRevenueStatsRequest](d, tlprofile.SemanticMethodPaymentsGetStarsRevenueStats, func(ctx context.Context, req *tg.PaymentsGetStarsRevenueStatsRequest) (any, error) { userID, _, err := r.currentUserID(ctx) if err != nil { return nil, internalErr() } if req == nil { return nil, peerIDInvalidErr() } if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer); err != nil { return nil, err } return tdesktop.StarsRevenueStats(req.GetTon()), nil }) } // onPaymentsGetStarsStatus 返回当前账号的 Stars 余额(首读时惰性授予起始余额)。 // 响应必须是 payments.starsStatus(balance/chats/users 都是必填,空 vector 即可)—— // 两端客户端无条件读取 balance(DrKLO StarsAmount 反序列化 / TDesktop vbalance())。 func (r *Router) onPaymentsGetStarsStatus(ctx context.Context, req *tg.PaymentsGetStarsStatusRequest) (*tg.PaymentsStarsStatus, error) { if req != nil && req.GetTon() { // TON 余额未建模:返回 0 nanoton 的合法响应。 return emptyStarsStatus(&tg.StarsTonAmount{}), nil } if r.deps.Stars == nil { return emptyStarsStatus(&tg.StarsAmount{}), nil } userID, _, err := r.currentUserID(ctx) if err != nil { return nil, internalErr() } bal, err := r.deps.Stars.GetBalance(ctx, userID) if err != nil { return nil, starsErr(err) } return emptyStarsStatus(&tg.StarsAmount{Amount: bal.Balance}), nil } // onPaymentsGetStarsTransactions 返回 keyset 分页的 Stars 流水(同 starsStatus 信封)。 // 末页必须省略 next_offset(flag 不置),否则 DrKLO 会无限翻页。 func (r *Router) onPaymentsGetStarsTransactions(ctx context.Context, req *tg.PaymentsGetStarsTransactionsRequest) (*tg.PaymentsStarsStatus, error) { if req != nil && req.GetTon() { return emptyStarsStatus(&tg.StarsTonAmount{}), nil } if r.deps.Stars == nil { return emptyStarsStatus(&tg.StarsAmount{}), nil } userID, _, err := r.currentUserID(ctx) if err != nil { return nil, internalErr() } offset := "" limit := domain.MaxStarsTransactionsLimit if req != nil { offset = req.Offset if req.Limit > 0 { limit = req.Limit } } page, err := r.deps.Stars.ListTransactions(ctx, userID, offset, limit) if err != nil { return nil, starsErr(err) } out := emptyStarsStatus(&tg.StarsAmount{Amount: page.Balance}) if txns := tgStarsTransactions(page.Transactions); len(txns) > 0 { out.SetHistory(txns) } if page.NextOffset != "" { out.SetNextOffset(page.NextOffset) } // 富化流水中提到的用户对手方(频道对手方进 Chats 留待 paid reaction 阶段)。 if ids := starsTransactionUserIDs(page.Transactions); len(ids) > 0 { out.Users = tgUsersForViewer(userID, r.domainUsersForIDs(ctx, userID, ids)) } return out, nil } // emptyStarsStatus 构造一个合法的最小 payments.starsStatus(chats/users 非空 vector 但可空)。 func emptyStarsStatus(balance tg.StarsAmountClass) *tg.PaymentsStarsStatus { return &tg.PaymentsStarsStatus{ Balance: balance, Chats: []tg.ChatClass{}, Users: []tg.UserClass{}, } } // tgStarsTransactions 把账本流水投影为 tg.StarsTransaction(amount 带符号:借记为负)。 func tgStarsTransactions(in []domain.StarsTransaction) []tg.StarsTransaction { out := make([]tg.StarsTransaction, 0, len(in)) for _, t := range in { item := tg.StarsTransaction{ ID: strconv.FormatInt(t.ID, 10), Amount: &tg.StarsAmount{Amount: t.Amount}, Date: t.Date, Peer: tgStarsTransactionPeer(t), } if t.Title != "" { item.SetTitle(t.Title) } if t.Description != "" { item.SetDescription(t.Description) } switch t.Reason { case domain.StarsReasonReaction: item.Reaction = true case domain.StarsReasonGift: item.Gift = true } out = append(out, item) } return out } // tgStarsTransactionPeer 选择对手方构造器:grant/topup 走 Fragment(站外充值轨), // 真实 peer 走 starsTransactionPeer,其余兜底 Unsupported(Peer 字段必填,不可为 nil)。 func tgStarsTransactionPeer(t domain.StarsTransaction) tg.StarsTransactionPeerClass { switch t.Reason { case domain.StarsReasonGrant, domain.StarsReasonTopup: return &tg.StarsTransactionPeerFragment{} } if t.Peer.Type != "" && t.Peer.ID != 0 { if p := tgPeer(t.Peer); p != nil { return &tg.StarsTransactionPeer{Peer: p} } } return &tg.StarsTransactionPeerUnsupported{} } // starsTransactionUserIDs 收集流水中去重的用户类对手方 id。 func starsTransactionUserIDs(in []domain.StarsTransaction) []int64 { seen := make(map[int64]struct{}, len(in)) ids := make([]int64, 0, len(in)) for _, t := range in { if t.Peer.Type != domain.PeerTypeUser || t.Peer.ID == 0 { continue } if _, ok := seen[t.Peer.ID]; ok { continue } seen[t.Peer.ID] = struct{}{} ids = append(ids, t.Peer.ID) } return ids } // starsErr 把 Stars 账本领域错误映射为客户端可识别的 tgerr(仿 premiumBoostErr)。 func starsErr(err error) error { switch { case errors.Is(err, domain.ErrStarsInsufficient): return balanceTooLowErr() case errors.Is(err, domain.ErrStarsInvalidAmount): return starsAmountInvalidErr() default: return internalErr() } }