feat: sync official star gift lifecycle tooling
Sync telesrv 4c0e2d9 (feat: complete official star gift lifecycle and admin tooling). Public adjustments: skipped private docs/deploy-nginx/README source changes, kept iamxvbaba/td public dependency, replaced local/private sample IP and orange seed label.
This commit is contained in:
parent
f2c2fd0236
commit
14bf7d1e20
92 changed files with 14768 additions and 727 deletions
|
|
@ -284,6 +284,8 @@ func tgChannelMessageAction(action domain.ChannelMessageAction) tg.MessageAction
|
|||
}
|
||||
case domain.ChannelActionStarGift:
|
||||
return tgMessageActionStarGift(action.StarGift)
|
||||
case domain.ChannelActionStarGiftUnique:
|
||||
return tgMessageActionStarGiftUnique(action.StarGiftUnique)
|
||||
case domain.ChannelActionSetChatWallpaper:
|
||||
if wallpaper := tgWallpaper(action.Wallpaper); wallpaper != nil {
|
||||
return &tg.MessageActionSetChatWallPaper{Wallpaper: wallpaper}
|
||||
|
|
|
|||
|
|
@ -221,29 +221,69 @@ func tgMessageServiceAction(msg domain.Message) tg.MessageActionClass {
|
|||
case domain.MessageServiceActionStarGift:
|
||||
return tgMessageActionStarGift(m.ServiceAction.StarGift)
|
||||
case domain.MessageServiceActionStarGiftUnique:
|
||||
action := m.ServiceAction.StarGiftUnique
|
||||
return tgMessageActionStarGiftUnique(m.ServiceAction.StarGiftUnique)
|
||||
case domain.MessageServiceActionStarGiftOffer:
|
||||
action := m.ServiceAction.StarGiftOffer
|
||||
if action == nil {
|
||||
return &tg.MessageActionEmpty{}
|
||||
}
|
||||
out := &tg.MessageActionStarGiftUnique{
|
||||
Upgrade: action.Upgrade, Saved: action.Saved, PrepaidUpgrade: action.PrepaidUpgrade,
|
||||
Gift: tgUniqueStarGift(action.Gift),
|
||||
return &tg.MessageActionStarGiftPurchaseOffer{Accepted: action.Accepted, Declined: action.Declined,
|
||||
Gift: tgUniqueStarGift(action.Gift), Price: tgStarGiftAmount(action.Price), ExpiresAt: action.ExpiresAt}
|
||||
case domain.MessageServiceActionStarGiftOfferDeclined:
|
||||
action := m.ServiceAction.StarGiftOfferDeclined
|
||||
if action == nil {
|
||||
return &tg.MessageActionEmpty{}
|
||||
}
|
||||
if action.FromUserID != 0 {
|
||||
out.SetFromID(&tg.PeerUser{UserID: action.FromUserID})
|
||||
}
|
||||
if peer := tgPeer(action.Peer); peer != nil {
|
||||
out.SetPeer(peer)
|
||||
}
|
||||
if action.SavedID != 0 {
|
||||
out.SetSavedID(action.SavedID)
|
||||
}
|
||||
return out
|
||||
return &tg.MessageActionStarGiftPurchaseOfferDeclined{Expired: action.Expired,
|
||||
Gift: tgUniqueStarGift(action.Gift), Price: tgStarGiftAmount(action.Price)}
|
||||
default:
|
||||
return &tg.MessageActionEmpty{}
|
||||
}
|
||||
}
|
||||
|
||||
func tgMessageActionStarGiftUnique(action *domain.MessageStarGiftUniqueAction) tg.MessageActionClass {
|
||||
if action == nil {
|
||||
return &tg.MessageActionEmpty{}
|
||||
}
|
||||
out := &tg.MessageActionStarGiftUnique{
|
||||
Upgrade: action.Upgrade, Saved: action.Saved, PrepaidUpgrade: action.PrepaidUpgrade,
|
||||
Transferred: action.Transferred, Refunded: action.Refunded, Assigned: action.Assigned,
|
||||
FromOffer: action.FromOffer, Craft: action.Craft,
|
||||
Gift: tgUniqueStarGift(action.Gift),
|
||||
}
|
||||
if action.CanExportAt > 0 {
|
||||
out.SetCanExportAt(action.CanExportAt)
|
||||
}
|
||||
if action.TransferStars > 0 {
|
||||
out.SetTransferStars(action.TransferStars)
|
||||
}
|
||||
if action.ResaleAmount != nil {
|
||||
out.SetResaleAmount(tgStarGiftAmount(*action.ResaleAmount))
|
||||
}
|
||||
if action.CanTransferAt > 0 {
|
||||
out.SetCanTransferAt(action.CanTransferAt)
|
||||
}
|
||||
if action.CanResellAt > 0 {
|
||||
out.SetCanResellAt(action.CanResellAt)
|
||||
}
|
||||
if action.DropOriginalDetailsStars > 0 {
|
||||
out.SetDropOriginalDetailsStars(action.DropOriginalDetailsStars)
|
||||
}
|
||||
if action.CanCraftAt > 0 {
|
||||
out.SetCanCraftAt(action.CanCraftAt)
|
||||
}
|
||||
if action.FromUserID != 0 {
|
||||
out.SetFromID(&tg.PeerUser{UserID: action.FromUserID})
|
||||
}
|
||||
if peer := tgPeer(action.Peer); peer != nil {
|
||||
out.SetPeer(peer)
|
||||
}
|
||||
if action.SavedID != 0 {
|
||||
out.SetSavedID(action.SavedID)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func tgPeerList(peers []domain.Peer) []tg.PeerClass {
|
||||
out := make([]tg.PeerClass, 0, len(peers))
|
||||
for _, peer := range peers {
|
||||
|
|
|
|||
|
|
@ -872,6 +872,7 @@ type GiftsService interface {
|
|||
UniqueByID(ctx context.Context, uniqueGiftID int64) (domain.UniqueStarGift, bool, error)
|
||||
UniqueByIDs(ctx context.Context, uniqueGiftIDs []int64) (map[int64]domain.UniqueStarGift, error)
|
||||
Upgrade(ctx context.Context, req domain.StarGiftUpgradeRequest) (domain.StarGiftUpgradeResult, error)
|
||||
UpgradeReceipt(ctx context.Context, userID int64, commandKey string) (domain.StarGiftUpgradeReceipt, bool, error)
|
||||
RecordSavedGift(ctx context.Context, gift domain.SavedStarGift) (int64, error)
|
||||
ListSaved(ctx context.Context, owner domain.Peer, excludeUnsaved bool, offset string, limit int) (domain.SavedStarGiftPage, error)
|
||||
ListSavedFiltered(ctx context.Context, filter domain.SavedStarGiftFilter) (domain.SavedStarGiftPage, error)
|
||||
|
|
@ -879,13 +880,36 @@ type GiftsService interface {
|
|||
ResolveSavedIDs(ctx context.Context, owner domain.Peer, refs []domain.SavedStarGiftRef) ([]int64, error)
|
||||
CountSaved(ctx context.Context, owner domain.Peer) (int, error)
|
||||
ToggleSaved(ctx context.Context, ref domain.SavedStarGiftRef, unsaved bool) (bool, error)
|
||||
Convert(ctx context.Context, ref domain.SavedStarGiftRef) (domain.SavedStarGift, error)
|
||||
ConvertAggregate(ctx context.Context, req domain.StarGiftConvertRequest) (domain.StarGiftConvertResult, error)
|
||||
ListCollections(ctx context.Context, owner domain.Peer) ([]domain.StarGiftCollection, error)
|
||||
CreateCollection(ctx context.Context, owner domain.Peer, title string, savedGiftIDs []int64) (domain.StarGiftCollection, error)
|
||||
UpdateCollection(ctx context.Context, owner domain.Peer, collectionID int, patch domain.StarGiftCollectionPatch) (domain.StarGiftCollection, error)
|
||||
DeleteCollection(ctx context.Context, owner domain.Peer, collectionID int) (bool, error)
|
||||
ReorderCollections(ctx context.Context, owner domain.Peer, collectionIDs []int) error
|
||||
SetPinned(ctx context.Context, owner domain.Peer, savedGiftIDs []int64) error
|
||||
ListResale(ctx context.Context, filter domain.StarGiftResaleFilter) (domain.StarGiftResalePage, error)
|
||||
ValueInfo(ctx context.Context, uniqueGiftID int64) (domain.StarGiftValueInfo, error)
|
||||
SetListing(ctx context.Context, req domain.StarGiftListingRequest) (domain.UniqueStarGift, error)
|
||||
Transfer(ctx context.Context, req domain.StarGiftTransferRequest) (domain.StarGiftTransferResult, error)
|
||||
PurchaseResale(ctx context.Context, req domain.StarGiftResalePurchaseRequest) (domain.StarGiftTransferResult, error)
|
||||
SendOffer(ctx context.Context, req domain.StarGiftOfferRequest) (domain.StarGiftOfferResult, error)
|
||||
ResolveOffer(ctx context.Context, req domain.StarGiftResolveOfferRequest) (domain.StarGiftOfferResult, error)
|
||||
ListCraft(ctx context.Context, userID, giftID int64, offset string, limit int) (domain.SavedStarGiftPage, error)
|
||||
Craft(ctx context.Context, req domain.StarGiftCraftRequest) (domain.StarGiftCraftResult, error)
|
||||
AuctionState(ctx context.Context, userID, giftID int64, slug string, now int) (domain.StarGiftAuction, error)
|
||||
ActiveAuctions(ctx context.Context, userID int64, now int) ([]domain.StarGiftAuction, error)
|
||||
AuctionAcquired(ctx context.Context, userID, giftID int64) ([]domain.StarGiftAuctionAcquired, error)
|
||||
BidAuction(ctx context.Context, req domain.StarGiftAuctionBidRequest) (domain.StarGiftAuction, domain.StarsBalance, error)
|
||||
PrepaidUpgradeTarget(ctx context.Context, owner domain.Peer, hash string) (domain.SavedStarGift, int64, error)
|
||||
PrepayUpgrade(ctx context.Context, req domain.StarGiftPrepaidUpgradeRequest) (domain.StarGiftPrepaidUpgradeResult, error)
|
||||
DropOriginalDetails(ctx context.Context, req domain.StarGiftDropOriginalDetailsRequest) (domain.StarGiftDropOriginalDetailsResult, error)
|
||||
SetNotifications(ctx context.Context, userID, channelID int64, enabled bool) error
|
||||
Withdraw(ctx context.Context, req domain.StarGiftWithdrawalRequest) (domain.StarGiftWithdrawal, error)
|
||||
TonBalance(ctx context.Context, userID int64) (int64, error)
|
||||
TonTransactions(ctx context.Context, userID int64, offset string, limit int) (domain.TonTransactionPage, error)
|
||||
IssuePurchaseForm(ctx context.Context, form domain.StarGiftPurchaseForm) (domain.StarGiftPurchaseForm, error)
|
||||
ValidatePurchaseForm(ctx context.Context, req domain.StarGiftPurchaseRequest) error
|
||||
Purchase(ctx context.Context, req domain.StarGiftPurchaseRequest) (domain.StarGiftPurchaseResult, error)
|
||||
}
|
||||
|
||||
// StarsService 抽象 Stars 本地账本(app/stars):余额查询、贷记/借记、流水分页。
|
||||
|
|
|
|||
|
|
@ -137,6 +137,10 @@ func starsFormAmountMismatchErr() error { return tgerr.New(406, "STARS_FORM_AMOU
|
|||
|
||||
func formIDEmptyErr() error { return tgerr.New(400, "FORM_ID_EMPTY") }
|
||||
|
||||
func formExpiredErr() error { return tgerr.New(400, "FORM_EXPIRED") }
|
||||
|
||||
func purposeInvalidErr() error { return tgerr.New(400, "PURPOSE_INVALID") }
|
||||
|
||||
func suggestedPostPeerInvalidErr() error { return tgerr.New(400, "SUGGESTED_POST_PEER_INVALID") }
|
||||
|
||||
func storyIDInvalidErr() error { return tgerr.New(400, "STORY_ID_INVALID") }
|
||||
|
|
|
|||
|
|
@ -33,12 +33,11 @@ func (r *Router) registerPayments(d *tlprofile.Dispatcher) {
|
|||
registerRPC[*tg.PaymentsGetStarsTransactionsRequest](d, tlprofile.SemanticMethodPaymentsGetStarsTransactions, func(ctx context.Context, layerRequest *tg.PaymentsGetStarsTransactionsRequest) (any, error) {
|
||||
return r.onPaymentsGetStarsTransactions(ctx, layerRequest)
|
||||
})
|
||||
registerRPC[*tg.PaymentsCheckCanSendGiftRequest](d, tlprofile.SemanticMethodPaymentsCheckCanSendGift, func(ctx context.Context, req *tg.PaymentsCheckCanSendGiftRequest) (any, error) {
|
||||
return r.onPaymentsCheckCanSendGift(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.PaymentsGetStarGiftActiveAuctionsRequest](d, tlprofile.SemanticMethodPaymentsGetStarGiftActiveAuctions, func(ctx context.Context, layerRequest *tg.PaymentsGetStarGiftActiveAuctionsRequest) (any, error) {
|
||||
hash := layerRequest.
|
||||
Hash
|
||||
_ = hash
|
||||
|
||||
return tdesktop.StarGiftActiveAuctions(), nil
|
||||
return r.onPaymentsGetStarGiftActiveAuctions(ctx, layerRequest)
|
||||
})
|
||||
registerRPC[*tg.PaymentsGetStarGiftsRequest](d, tlprofile.SemanticMethodPaymentsGetStarGifts, func(ctx context.Context, layerRequest *tg.PaymentsGetStarGiftsRequest) (any, error) {
|
||||
return r.onPaymentsGetStarGifts(ctx, layerRequest.
|
||||
|
|
@ -48,10 +47,19 @@ func (r *Router) registerPayments(d *tlprofile.Dispatcher) {
|
|||
return r.onPaymentsGetStarGiftUpgradePreview(ctx, layerRequest.
|
||||
GiftID)
|
||||
})
|
||||
registerRPC[*tg.PaymentsGetStarGiftUpgradeAttributesRequest](d, tlprofile.SemanticMethodPaymentsGetStarGiftUpgradeAttributes, func(ctx context.Context, layerRequest *tg.PaymentsGetStarGiftUpgradeAttributesRequest) (any, error) {
|
||||
return r.onPaymentsGetStarGiftUpgradeAttributes(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.PaymentsGetUniqueStarGiftValueInfoRequest](d, tlprofile.SemanticMethodPaymentsGetUniqueStarGiftValueInfo, func(ctx context.Context, req *tg.PaymentsGetUniqueStarGiftValueInfoRequest) (any, error) {
|
||||
return r.onPaymentsGetUniqueStarGiftValueInfo(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.PaymentsGetResaleStarGiftsRequest](d, tlprofile.SemanticMethodPaymentsGetResaleStarGifts, func(ctx context.Context, req *tg.PaymentsGetResaleStarGiftsRequest) (any, error) {
|
||||
return r.onPaymentsGetResaleStarGifts(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.PaymentsGetPaymentFormRequest](d, tlprofile.SemanticMethodPaymentsGetPaymentForm, func(ctx context.Context, layerRequest *tg.PaymentsGetPaymentFormRequest) (any, error) {
|
||||
return r.onPaymentsGetPaymentForm(ctx, layerRequest)
|
||||
})
|
||||
|
|
@ -75,6 +83,36 @@ func (r *Router) registerPayments(d *tlprofile.Dispatcher) {
|
|||
registerRPC[*tg.PaymentsUpgradeStarGiftRequest](d, tlprofile.SemanticMethodPaymentsUpgradeStarGift, func(ctx context.Context, layerRequest *tg.PaymentsUpgradeStarGiftRequest) (any, error) {
|
||||
return r.onPaymentsUpgradeStarGift(ctx, layerRequest)
|
||||
})
|
||||
registerRPC[*tg.PaymentsUpdateStarGiftPriceRequest](d, tlprofile.SemanticMethodPaymentsUpdateStarGiftPrice, func(ctx context.Context, req *tg.PaymentsUpdateStarGiftPriceRequest) (any, error) {
|
||||
return r.onPaymentsUpdateStarGiftPrice(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.PaymentsTransferStarGiftRequest](d, tlprofile.SemanticMethodPaymentsTransferStarGift, func(ctx context.Context, req *tg.PaymentsTransferStarGiftRequest) (any, error) {
|
||||
return r.onPaymentsTransferStarGift(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.PaymentsGetStarGiftWithdrawalURLRequest](d, tlprofile.SemanticMethodPaymentsGetStarGiftWithdrawalURL, func(ctx context.Context, req *tg.PaymentsGetStarGiftWithdrawalURLRequest) (any, error) {
|
||||
return r.onPaymentsGetStarGiftWithdrawalURL(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.PaymentsSendStarGiftOfferRequest](d, tlprofile.SemanticMethodPaymentsSendStarGiftOffer, func(ctx context.Context, req *tg.PaymentsSendStarGiftOfferRequest) (any, error) {
|
||||
return r.onPaymentsSendStarGiftOffer(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.PaymentsResolveStarGiftOfferRequest](d, tlprofile.SemanticMethodPaymentsResolveStarGiftOffer, func(ctx context.Context, req *tg.PaymentsResolveStarGiftOfferRequest) (any, error) {
|
||||
return r.onPaymentsResolveStarGiftOffer(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.PaymentsGetCraftStarGiftsRequest](d, tlprofile.SemanticMethodPaymentsGetCraftStarGifts, func(ctx context.Context, req *tg.PaymentsGetCraftStarGiftsRequest) (any, error) {
|
||||
return r.onPaymentsGetCraftStarGifts(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.PaymentsCraftStarGiftRequest](d, tlprofile.SemanticMethodPaymentsCraftStarGift, func(ctx context.Context, req *tg.PaymentsCraftStarGiftRequest) (any, error) {
|
||||
return r.onPaymentsCraftStarGift(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.PaymentsGetStarGiftAuctionStateRequest](d, tlprofile.SemanticMethodPaymentsGetStarGiftAuctionState, func(ctx context.Context, req *tg.PaymentsGetStarGiftAuctionStateRequest) (any, error) {
|
||||
return r.onPaymentsGetStarGiftAuctionState(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.PaymentsGetStarGiftAuctionAcquiredGiftsRequest](d, tlprofile.SemanticMethodPaymentsGetStarGiftAuctionAcquiredGifts, func(ctx context.Context, req *tg.PaymentsGetStarGiftAuctionAcquiredGiftsRequest) (any, error) {
|
||||
return r.onPaymentsGetStarGiftAuctionAcquiredGifts(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.PaymentsToggleChatStarGiftNotificationsRequest](d, tlprofile.SemanticMethodPaymentsToggleChatStarGiftNotifications, func(ctx context.Context, req *tg.PaymentsToggleChatStarGiftNotificationsRequest) (any, error) {
|
||||
return r.onPaymentsToggleChatStarGiftNotifications(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.PaymentsGetStarGiftCollectionsRequest](d, tlprofile.SemanticMethodPaymentsGetStarGiftCollections, func(ctx context.Context, layerRequest *tg.PaymentsGetStarGiftCollectionsRequest) (any, error) {
|
||||
return r.onPaymentsGetStarGiftCollections(ctx, layerRequest)
|
||||
})
|
||||
|
|
@ -108,36 +146,116 @@ func (r *Router) registerPayments(d *tlprofile.Dispatcher) {
|
|||
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
|
||||
return r.onPaymentsGetStarsRevenueStats(ctx, req)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
// onPaymentsGetStarsRevenueStats exposes real channel Star Gift proceeds from
|
||||
// the same peer-scoped ledger as getStarsStatus/getStarsTransactions. Personal
|
||||
// and bot revenue remain the bounded compatibility response because their
|
||||
// revenue bucket is distinct from the general Stars balance and is not modeled.
|
||||
func (r *Router) onPaymentsGetStarsRevenueStats(ctx context.Context, req *tg.PaymentsGetStarsRevenueStatsRequest) (*tg.PaymentsStarsRevenueStats, error) {
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if req == nil {
|
||||
return nil, peerIDInvalidErr()
|
||||
}
|
||||
owner, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ton := req.GetTon()
|
||||
if owner.Type != domain.PeerTypeChannel {
|
||||
return tdesktop.StarsRevenueStats(ton), nil
|
||||
}
|
||||
if err := r.checkStarGiftOwnerPermission(ctx, userID, owner); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ledger, ok := r.deps.Gifts.(channelGiftLedgerReader)
|
||||
if !ok {
|
||||
return tdesktop.StarsRevenueStats(ton), nil
|
||||
}
|
||||
var balance int64
|
||||
if ton {
|
||||
balance, err = ledger.ChannelTonBalance(ctx, owner.ID)
|
||||
} else {
|
||||
balance, err = ledger.ChannelStarsBalance(ctx, owner.ID)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
stats := tdesktop.StarsRevenueStats(ton)
|
||||
var amount tg.StarsAmountClass = &tg.StarsAmount{Amount: balance}
|
||||
if ton {
|
||||
amount = &tg.StarsTonAmount{Amount: balance}
|
||||
}
|
||||
// Channel ledgers currently only receive collectible conversion/marketplace
|
||||
// proceeds and have no withdrawal/debit path, so balance equals lifetime
|
||||
// revenue. Withdrawal stays disabled because no external payout exists.
|
||||
stats.Status.CurrentBalance = amount
|
||||
stats.Status.AvailableBalance = amount
|
||||
stats.Status.OverallRevenue = amount
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
type channelGiftLedgerReader interface {
|
||||
ChannelStarsBalance(ctx context.Context, channelID int64) (int64, error)
|
||||
ChannelStarsTransactions(ctx context.Context, channelID int64, offset string, limit int) (domain.StarsTransactionPage, error)
|
||||
ChannelTonBalance(ctx context.Context, channelID int64) (int64, error)
|
||||
ChannelTonTransactions(ctx context.Context, channelID int64, offset string, limit int) (domain.TonTransactionPage, error)
|
||||
}
|
||||
|
||||
// onPaymentsGetStarsStatus 返回请求 peer 的 Stars/本地 TON 余额。个人与频道账本
|
||||
// 严格隔离;频道读取要求 Star Gift 管理权限,不能把频道收益投影到执行 RPC 的管理员。
|
||||
// 响应必须是 payments.starsStatus(balance/chats/users 都是必填,空 vector 即可)——
|
||||
// 两端客户端无条件读取 balance(DrKLO StarsAmount 反序列化 / TDesktop vbalance())。
|
||||
func (r *Router) onPaymentsGetStarsStatus(ctx context.Context, req *tg.PaymentsGetStarsStatusRequest) (*tg.PaymentsStarsStatus, error) {
|
||||
userID, owner, err := r.starGiftLedgerOwner(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ton := req != nil && req.GetTon()
|
||||
if owner.Type == domain.PeerTypeChannel {
|
||||
ledger, ok := r.deps.Gifts.(channelGiftLedgerReader)
|
||||
if !ok {
|
||||
if ton {
|
||||
return emptyStarsStatus(&tg.StarsTonAmount{}), nil
|
||||
}
|
||||
return emptyStarsStatus(&tg.StarsAmount{}), nil
|
||||
}
|
||||
var balance int64
|
||||
if ton {
|
||||
balance, err = ledger.ChannelTonBalance(ctx, owner.ID)
|
||||
} else {
|
||||
balance, err = ledger.ChannelStarsBalance(ctx, owner.ID)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
var amount tg.StarsAmountClass = &tg.StarsAmount{Amount: balance}
|
||||
if ton {
|
||||
amount = &tg.StarsTonAmount{Amount: balance}
|
||||
}
|
||||
out := emptyStarsStatus(amount)
|
||||
out.Chats = r.tgChatsForChannelIDs(ctx, userID, []int64{owner.ID})
|
||||
return out, nil
|
||||
}
|
||||
if ton {
|
||||
if r.deps.Gifts == nil {
|
||||
return emptyStarsStatus(&tg.StarsTonAmount{}), nil
|
||||
}
|
||||
balance, err := r.deps.Gifts.TonBalance(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
return emptyStarsStatus(&tg.StarsTonAmount{Amount: balance}), nil
|
||||
}
|
||||
if r.deps.Stars == nil {
|
||||
return emptyStarsStatus(&tg.StarsAmount{}), nil
|
||||
}
|
||||
bal, err := r.deps.Stars.GetBalance(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, starsErr(err)
|
||||
|
|
@ -148,24 +266,82 @@ func (r *Router) onPaymentsGetStarsStatus(ctx context.Context, req *tg.PaymentsG
|
|||
// 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)
|
||||
userID, owner, err := r.starGiftTransactionLedgerOwner(ctx, req)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
return nil, err
|
||||
}
|
||||
offset := ""
|
||||
limit := domain.MaxStarsTransactionsLimit
|
||||
offset, limit := "", domain.MaxStarsTransactionsLimit
|
||||
if req != nil {
|
||||
offset = req.Offset
|
||||
if req.Limit > 0 {
|
||||
limit = req.Limit
|
||||
}
|
||||
}
|
||||
ton := req != nil && req.GetTon()
|
||||
if owner.Type == domain.PeerTypeChannel {
|
||||
ledger, ok := r.deps.Gifts.(channelGiftLedgerReader)
|
||||
if !ok {
|
||||
if ton {
|
||||
return emptyStarsStatus(&tg.StarsTonAmount{}), nil
|
||||
}
|
||||
return emptyStarsStatus(&tg.StarsAmount{}), nil
|
||||
}
|
||||
if ton {
|
||||
page, err := ledger.ChannelTonTransactions(ctx, owner.ID, offset, limit)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
out := emptyStarsStatus(&tg.StarsTonAmount{Amount: page.Balance})
|
||||
if txns := tgTonTransactions(page.Transactions); len(txns) > 0 {
|
||||
out.SetHistory(txns)
|
||||
}
|
||||
if page.NextOffset != "" {
|
||||
out.SetNextOffset(page.NextOffset)
|
||||
}
|
||||
r.enrichChannelTonLedgerStatus(ctx, userID, owner.ID, page.Transactions, out)
|
||||
return out, nil
|
||||
}
|
||||
page, err := ledger.ChannelStarsTransactions(ctx, owner.ID, offset, limit)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
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)
|
||||
}
|
||||
r.enrichChannelStarsLedgerStatus(ctx, userID, owner.ID, page.Transactions, out)
|
||||
return out, nil
|
||||
}
|
||||
if ton {
|
||||
if r.deps.Gifts == nil {
|
||||
return emptyStarsStatus(&tg.StarsTonAmount{}), nil
|
||||
}
|
||||
page, err := r.deps.Gifts.TonTransactions(ctx, userID, offset, limit)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
out := emptyStarsStatus(&tg.StarsTonAmount{Amount: page.Balance})
|
||||
if txns := tgTonTransactions(page.Transactions); len(txns) > 0 {
|
||||
out.SetHistory(txns)
|
||||
}
|
||||
if page.NextOffset != "" {
|
||||
out.SetNextOffset(page.NextOffset)
|
||||
}
|
||||
ids := make([]int64, 0)
|
||||
for _, txn := range page.Transactions {
|
||||
if txn.Peer.Type == domain.PeerTypeUser {
|
||||
ids = append(ids, txn.Peer.ID)
|
||||
}
|
||||
}
|
||||
out.Users = tgUsersForViewer(userID, r.domainUsersForIDs(ctx, userID, uniqueInt64(ids)))
|
||||
return out, nil
|
||||
}
|
||||
if r.deps.Stars == nil {
|
||||
return emptyStarsStatus(&tg.StarsAmount{}), nil
|
||||
}
|
||||
page, err := r.deps.Stars.ListTransactions(ctx, userID, offset, limit)
|
||||
if err != nil {
|
||||
return nil, starsErr(err)
|
||||
|
|
@ -184,6 +360,71 @@ func (r *Router) onPaymentsGetStarsTransactions(ctx context.Context, req *tg.Pay
|
|||
return out, nil
|
||||
}
|
||||
|
||||
func (r *Router) starGiftLedgerOwner(ctx context.Context, req *tg.PaymentsGetStarsStatusRequest) (int64, domain.Peer, error) {
|
||||
if req == nil {
|
||||
return 0, domain.Peer{}, peerIDInvalidErr()
|
||||
}
|
||||
return r.starGiftLedgerOwnerForPeer(ctx, req.Peer)
|
||||
}
|
||||
|
||||
func (r *Router) starGiftTransactionLedgerOwner(ctx context.Context, req *tg.PaymentsGetStarsTransactionsRequest) (int64, domain.Peer, error) {
|
||||
if req == nil {
|
||||
return 0, domain.Peer{}, peerIDInvalidErr()
|
||||
}
|
||||
return r.starGiftLedgerOwnerForPeer(ctx, req.Peer)
|
||||
}
|
||||
|
||||
func (r *Router) starGiftLedgerOwnerForPeer(ctx context.Context, input tg.InputPeerClass) (int64, domain.Peer, error) {
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return 0, domain.Peer{}, internalErr()
|
||||
}
|
||||
owner, err := r.checkedDomainPeerFromInputPeer(ctx, userID, input)
|
||||
if err != nil {
|
||||
return 0, domain.Peer{}, err
|
||||
}
|
||||
if owner.Type == domain.PeerTypeUser {
|
||||
if owner.ID != userID {
|
||||
return 0, domain.Peer{}, peerIDInvalidErr()
|
||||
}
|
||||
return userID, owner, nil
|
||||
}
|
||||
if err := r.checkStarGiftOwnerPermission(ctx, userID, owner); err != nil {
|
||||
return 0, domain.Peer{}, err
|
||||
}
|
||||
return userID, owner, nil
|
||||
}
|
||||
|
||||
func (r *Router) enrichChannelStarsLedgerStatus(ctx context.Context, viewerID, ownerChannelID int64, txns []domain.StarsTransaction, out *tg.PaymentsStarsStatus) {
|
||||
userIDs := make([]int64, 0, len(txns))
|
||||
channelIDs := []int64{ownerChannelID}
|
||||
for _, txn := range txns {
|
||||
switch txn.Peer.Type {
|
||||
case domain.PeerTypeUser:
|
||||
userIDs = append(userIDs, txn.Peer.ID)
|
||||
case domain.PeerTypeChannel:
|
||||
channelIDs = append(channelIDs, txn.Peer.ID)
|
||||
}
|
||||
}
|
||||
out.Users = tgUsersForViewer(viewerID, r.domainUsersForIDs(ctx, viewerID, uniqueInt64(userIDs)))
|
||||
out.Chats = r.tgChatsForChannelIDs(ctx, viewerID, uniqueInt64(channelIDs))
|
||||
}
|
||||
|
||||
func (r *Router) enrichChannelTonLedgerStatus(ctx context.Context, viewerID, ownerChannelID int64, txns []domain.TonTransaction, out *tg.PaymentsStarsStatus) {
|
||||
userIDs := make([]int64, 0, len(txns))
|
||||
channelIDs := []int64{ownerChannelID}
|
||||
for _, txn := range txns {
|
||||
switch txn.Peer.Type {
|
||||
case domain.PeerTypeUser:
|
||||
userIDs = append(userIDs, txn.Peer.ID)
|
||||
case domain.PeerTypeChannel:
|
||||
channelIDs = append(channelIDs, txn.Peer.ID)
|
||||
}
|
||||
}
|
||||
out.Users = tgUsersForViewer(viewerID, r.domainUsersForIDs(ctx, viewerID, uniqueInt64(userIDs)))
|
||||
out.Chats = r.tgChatsForChannelIDs(ctx, viewerID, uniqueInt64(channelIDs))
|
||||
}
|
||||
|
||||
// emptyStarsStatus 构造一个合法的最小 payments.starsStatus(chats/users 非空 vector 但可空)。
|
||||
func emptyStarsStatus(balance tg.StarsAmountClass) *tg.PaymentsStarsStatus {
|
||||
return &tg.PaymentsStarsStatus{
|
||||
|
|
@ -214,6 +455,49 @@ func tgStarsTransactions(in []domain.StarsTransaction) []tg.StarsTransaction {
|
|||
item.Reaction = true
|
||||
case domain.StarsReasonGift:
|
||||
item.Gift = true
|
||||
case domain.StarsReasonGiftUpgrade:
|
||||
item.StargiftUpgrade = true
|
||||
case domain.StarsReasonGiftResale:
|
||||
item.StargiftResale = true
|
||||
case domain.StarsReasonGiftPrepaid:
|
||||
item.StargiftPrepaidUpgrade = true
|
||||
case domain.StarsReasonGiftDrop:
|
||||
item.StargiftDropOriginalDetails = true
|
||||
case domain.StarsReasonGiftAuction:
|
||||
item.StargiftAuctionBid = true
|
||||
case domain.StarsReasonGiftOffer:
|
||||
item.Offer = true
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func tgTonTransactions(in []domain.TonTransaction) []tg.StarsTransaction {
|
||||
out := make([]tg.StarsTransaction, 0, len(in))
|
||||
for _, t := range in {
|
||||
item := tg.StarsTransaction{ID: strconv.FormatInt(t.ID, 10), Amount: &tg.StarsTonAmount{Amount: t.Amount},
|
||||
Date: t.Date, Peer: tgStarsTransactionPeer(domain.StarsTransaction{Peer: t.Peer, Reason: t.Reason})}
|
||||
if t.Amount > 0 {
|
||||
item.Refund = true
|
||||
}
|
||||
if t.Title != "" {
|
||||
item.SetTitle(t.Title)
|
||||
}
|
||||
if t.Description != "" {
|
||||
item.SetDescription(t.Description)
|
||||
}
|
||||
switch t.Reason {
|
||||
case domain.StarsReasonGiftResale:
|
||||
item.StargiftResale = true
|
||||
case domain.StarsReasonGiftOffer:
|
||||
item.Offer = true
|
||||
case domain.StarsReasonGiftAuction:
|
||||
item.StargiftAuctionBid = true
|
||||
case domain.StarsReasonGiftPrepaid:
|
||||
item.StargiftPrepaidUpgrade = true
|
||||
case domain.StarsReasonGiftDrop:
|
||||
item.StargiftDropOriginalDetails = true
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
|
|
|
|||
98
internal/rpc/payments_star_gift_catalog_projection_test.go
Normal file
98
internal/rpc/payments_star_gift_catalog_projection_test.go
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/iamxvbaba/td/bin"
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
"github.com/iamxvbaba/td/tlprofile"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func TestStarGiftCatalogProjectionKeepsSaleDatesBehindSoldOutFlag(t *testing.T) {
|
||||
base := domain.StarGift{
|
||||
ID: 8001,
|
||||
RevisionID: 9001,
|
||||
Stars: 100,
|
||||
ConvertStars: 85,
|
||||
Title: "Fresh Socks",
|
||||
FirstSaleDate: 100,
|
||||
LastSaleDate: 200,
|
||||
Sticker: domain.Document{
|
||||
ID: 700,
|
||||
AccessHash: 7,
|
||||
DCID: 2,
|
||||
MimeType: "application/x-tgsticker",
|
||||
Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
gift domain.StarGift
|
||||
wantSoldOut bool
|
||||
wantSaleDate bool
|
||||
}{
|
||||
{name: "unlimited live gift with operational sale history", gift: base},
|
||||
{name: "limited live gift", gift: func() domain.StarGift {
|
||||
gift := base
|
||||
gift.Limited = true
|
||||
gift.AvailabilityRemains = 9
|
||||
gift.AvailabilityTotal = 10
|
||||
return gift
|
||||
}()},
|
||||
{name: "sold out gift", gift: func() domain.StarGift {
|
||||
gift := base
|
||||
gift.Limited = true
|
||||
gift.SoldOut = true
|
||||
gift.AvailabilityTotal = 10
|
||||
return gift
|
||||
}(), wantSoldOut: true, wantSaleDate: true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
for _, profile := range []tlprofile.Profile{
|
||||
tlprofile.Profile225,
|
||||
tlprofile.Profile226,
|
||||
tlprofile.Profile227,
|
||||
tlprofile.Profile228,
|
||||
} {
|
||||
response := &tg.PaymentsStarGifts{
|
||||
Hash: 1,
|
||||
Gifts: []tg.StarGiftClass{tgStarGift(test.gift)},
|
||||
Chats: []tg.ChatClass{},
|
||||
Users: []tg.UserClass{},
|
||||
}
|
||||
wire := &bin.Buffer{}
|
||||
if err := tlprofile.EncodeObject(profile, response, wire); err != nil {
|
||||
t.Fatalf("encode Layer %d catalog: %v", profile, err)
|
||||
}
|
||||
decodedObject, err := tlprofile.DecodeObject(profile, &bin.Buffer{Buf: wire.Buf}, tlprofile.Limits{})
|
||||
if err != nil {
|
||||
t.Fatalf("decode Layer %d catalog: %v", profile, err)
|
||||
}
|
||||
decoded, ok := decodedObject.(*tg.PaymentsStarGifts)
|
||||
if !ok || len(decoded.Gifts) != 1 {
|
||||
t.Fatalf("decode Layer %d catalog = %T %#v", profile, decodedObject, decodedObject)
|
||||
}
|
||||
gift, ok := decoded.Gifts[0].(*tg.StarGift)
|
||||
if !ok {
|
||||
t.Fatalf("decode Layer %d gift = %T", profile, decoded.Gifts[0])
|
||||
}
|
||||
if gift.SoldOut != test.wantSoldOut {
|
||||
t.Fatalf("Layer %d sold_out = %v, want %v", profile, gift.SoldOut, test.wantSoldOut)
|
||||
}
|
||||
first, firstSet := gift.GetFirstSaleDate()
|
||||
last, lastSet := gift.GetLastSaleDate()
|
||||
if firstSet != test.wantSaleDate || lastSet != test.wantSaleDate {
|
||||
t.Fatalf("Layer %d sale date flags = (%v,%v), want %v", profile, firstSet, lastSet, test.wantSaleDate)
|
||||
}
|
||||
if test.wantSaleDate && (first != test.gift.FirstSaleDate || last != test.gift.LastSaleDate) {
|
||||
t.Fatalf("Layer %d sale dates = (%d,%d), want (%d,%d)", profile, first, last, test.gift.FirstSaleDate, test.gift.LastSaleDate)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
1019
internal/rpc/payments_star_gift_lifecycle.go
Normal file
1019
internal/rpc/payments_star_gift_lifecycle.go
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -26,18 +26,37 @@ func (r *Router) starGiftUpgradePaymentForm(ctx context.Context, userID int64, i
|
|||
}
|
||||
|
||||
func (r *Router) sendStarGiftUpgradeForm(ctx context.Context, userID, formID int64, inv *tg.InputInvoiceStarGiftUpgrade) (tg.PaymentsPaymentResultClass, error) {
|
||||
saved, preview, err := r.starGiftUpgradeTarget(ctx, userID, inv.Stargift)
|
||||
saved, err := r.starGiftUpgradeSavedTarget(ctx, userID, inv.Stargift)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wantFormID := starGiftUpgradeFormID(userID, saved.ID, preview.UpgradeStars, inv.KeepOriginalDetails)
|
||||
if formID == 0 || formID != wantFormID {
|
||||
return nil, starsFormAmountMismatchErr()
|
||||
commandKey := fmt.Sprintf("paid:%d:%d:%t", saved.ID, formID, inv.KeepOriginalDetails)
|
||||
receipt, replay, err := r.deps.Gifts.UpgradeReceipt(ctx, userID, commandKey)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
chargeStars := int64(0)
|
||||
if replay {
|
||||
if receipt.SourceSavedGiftID != saved.ID || receipt.FormID != formID || receipt.RequirePrepaid ||
|
||||
receipt.KeepOriginalDetails != inv.KeepOriginalDetails || receipt.ChargeStars <= 0 {
|
||||
return nil, starGiftInvalidErr()
|
||||
}
|
||||
chargeStars = receipt.ChargeStars
|
||||
} else {
|
||||
preview, err := r.starGiftUpgradePreviewForSaved(ctx, saved)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wantFormID := starGiftUpgradeFormID(userID, saved.ID, preview.UpgradeStars, inv.KeepOriginalDetails)
|
||||
if formID == 0 || formID != wantFormID {
|
||||
return nil, starsFormAmountMismatchErr()
|
||||
}
|
||||
chargeStars = preview.UpgradeStars
|
||||
}
|
||||
result, err := r.deps.Gifts.Upgrade(ctx, domain.StarGiftUpgradeRequest{
|
||||
UserID: userID, Ref: domain.SavedStarGiftRef{Owner: saved.Owner, MsgID: saved.MsgID},
|
||||
KeepOriginalDetails: inv.KeepOriginalDetails, ChargeStars: preview.UpgradeStars,
|
||||
FormID: formID, CommandKey: fmt.Sprintf("paid:%d:%d:%t", saved.ID, formID, inv.KeepOriginalDetails),
|
||||
UserID: userID, Ref: starGiftUpgradeSavedRef(saved),
|
||||
KeepOriginalDetails: inv.KeepOriginalDetails, ChargeStars: chargeStars,
|
||||
FormID: formID, CommandKey: commandKey,
|
||||
Date: int(r.clock.Now().Unix()), OriginAuthKeyID: rawAuthKeyIDForOrigin(ctx),
|
||||
OriginSessionID: sessionIDOrZero(ctx),
|
||||
})
|
||||
|
|
@ -57,17 +76,32 @@ func (r *Router) onPaymentsUpgradeStarGift(ctx context.Context, req *tg.Payments
|
|||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
saved, _, err := r.starGiftUpgradeTarget(ctx, userID, req.Stargift)
|
||||
saved, err := r.starGiftUpgradeSavedTarget(ctx, userID, req.Stargift)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if saved.PrepaidUpgradeStars <= 0 {
|
||||
return nil, starGiftInvalidErr()
|
||||
commandKey := fmt.Sprintf("prepaid:%d:%t", saved.ID, req.KeepOriginalDetails)
|
||||
receipt, replay, err := r.deps.Gifts.UpgradeReceipt(ctx, userID, commandKey)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if replay {
|
||||
if receipt.SourceSavedGiftID != saved.ID || receipt.FormID != 0 || !receipt.RequirePrepaid ||
|
||||
receipt.KeepOriginalDetails != req.KeepOriginalDetails || receipt.ChargeStars != 0 {
|
||||
return nil, starGiftInvalidErr()
|
||||
}
|
||||
} else {
|
||||
if _, err := r.starGiftUpgradePreviewForSaved(ctx, saved); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if saved.PrepaidUpgradeStars <= 0 {
|
||||
return nil, starGiftInvalidErr()
|
||||
}
|
||||
}
|
||||
result, err := r.deps.Gifts.Upgrade(ctx, domain.StarGiftUpgradeRequest{
|
||||
UserID: userID, Ref: domain.SavedStarGiftRef{Owner: saved.Owner, MsgID: saved.MsgID},
|
||||
UserID: userID, Ref: starGiftUpgradeSavedRef(saved),
|
||||
KeepOriginalDetails: req.KeepOriginalDetails, RequirePrepaid: true,
|
||||
CommandKey: fmt.Sprintf("prepaid:%d:%t", saved.ID, req.KeepOriginalDetails),
|
||||
CommandKey: commandKey,
|
||||
Date: int(r.clock.Now().Unix()), OriginAuthKeyID: rawAuthKeyIDForOrigin(ctx),
|
||||
OriginSessionID: sessionIDOrZero(ctx),
|
||||
})
|
||||
|
|
@ -79,33 +113,60 @@ func (r *Router) onPaymentsUpgradeStarGift(ctx context.Context, req *tg.Payments
|
|||
}
|
||||
|
||||
func (r *Router) starGiftUpgradeTarget(ctx context.Context, userID int64, input tg.InputSavedStarGiftClass) (domain.SavedStarGift, domain.StarGiftUpgradePreview, error) {
|
||||
if r.deps.Gifts == nil {
|
||||
return domain.SavedStarGift{}, domain.StarGiftUpgradePreview{}, notImplementedErr()
|
||||
}
|
||||
ref, ok, err := r.starGiftRefFromInput(ctx, userID, input)
|
||||
saved, err := r.starGiftUpgradeSavedTarget(ctx, userID, input)
|
||||
if err != nil {
|
||||
return domain.SavedStarGift{}, domain.StarGiftUpgradePreview{}, err
|
||||
}
|
||||
if !ok || ref.Owner.Type != domain.PeerTypeUser || ref.Owner.ID != userID {
|
||||
// Channel gift upgrades require a channel pts aggregate and are not silently
|
||||
// routed through the private-message transaction.
|
||||
return domain.SavedStarGift{}, domain.StarGiftUpgradePreview{}, starGiftInvalidErr()
|
||||
preview, err := r.starGiftUpgradePreviewForSaved(ctx, saved)
|
||||
return saved, preview, err
|
||||
}
|
||||
|
||||
func (r *Router) starGiftUpgradeSavedTarget(ctx context.Context, userID int64, input tg.InputSavedStarGiftClass) (domain.SavedStarGift, error) {
|
||||
if r.deps.Gifts == nil {
|
||||
return domain.SavedStarGift{}, notImplementedErr()
|
||||
}
|
||||
ref, ok, err := r.starGiftRefFromInput(ctx, userID, input)
|
||||
if err != nil {
|
||||
return domain.SavedStarGift{}, err
|
||||
}
|
||||
if !ok {
|
||||
return domain.SavedStarGift{}, starGiftInvalidErr()
|
||||
}
|
||||
if err := r.checkStarGiftOwnerPermission(ctx, userID, ref.Owner); err != nil {
|
||||
return domain.SavedStarGift{}, err
|
||||
}
|
||||
saved, found, err := r.deps.Gifts.GetSaved(ctx, ref)
|
||||
if err != nil {
|
||||
return domain.SavedStarGift{}, domain.StarGiftUpgradePreview{}, internalErr()
|
||||
return domain.SavedStarGift{}, internalErr()
|
||||
}
|
||||
if !found || saved.Converted || saved.UniqueGiftID != 0 {
|
||||
return domain.SavedStarGift{}, domain.StarGiftUpgradePreview{}, starGiftInvalidErr()
|
||||
if !found {
|
||||
return domain.SavedStarGift{}, starGiftInvalidErr()
|
||||
}
|
||||
return saved, nil
|
||||
}
|
||||
|
||||
func (r *Router) starGiftUpgradePreviewForSaved(ctx context.Context, saved domain.SavedStarGift) (domain.StarGiftUpgradePreview, error) {
|
||||
if saved.Converted || saved.UniqueGiftID != 0 {
|
||||
return domain.StarGiftUpgradePreview{}, starGiftInvalidErr()
|
||||
}
|
||||
preview, found, err := r.deps.Gifts.CollectiblePreview(ctx, saved.GiftID)
|
||||
if err != nil {
|
||||
return domain.SavedStarGift{}, domain.StarGiftUpgradePreview{}, internalErr()
|
||||
return domain.StarGiftUpgradePreview{}, internalErr()
|
||||
}
|
||||
if !found || preview.UpgradeStars <= 0 || preview.Issued >= preview.SupplyTotal {
|
||||
return domain.SavedStarGift{}, domain.StarGiftUpgradePreview{}, starGiftInvalidErr()
|
||||
return domain.StarGiftUpgradePreview{}, starGiftInvalidErr()
|
||||
}
|
||||
return saved, preview, nil
|
||||
return preview, nil
|
||||
}
|
||||
|
||||
func starGiftUpgradeSavedRef(saved domain.SavedStarGift) domain.SavedStarGiftRef {
|
||||
ref := domain.SavedStarGiftRef{Owner: saved.Owner}
|
||||
if saved.Owner.Type == domain.PeerTypeChannel {
|
||||
ref.SavedID = saved.SavedID
|
||||
} else {
|
||||
ref.MsgID = saved.MsgID
|
||||
}
|
||||
return ref
|
||||
}
|
||||
|
||||
func (r *Router) tgStarGiftUpgradeUpdates(ctx context.Context, ownerUserID int64, result domain.StarGiftUpgradeResult, includeBalance bool) *tg.Updates {
|
||||
|
|
@ -116,6 +177,17 @@ func (r *Router) tgStarGiftUpgradeUpdates(ctx context.Context, ownerUserID int64
|
|||
updates := tgPrivateMessageUpdates(event, message, 0, false,
|
||||
r.usersForMessageUpdate(ctx, ownerUserID, message),
|
||||
r.chatsForMessageUpdate(ctx, ownerUserID, message))
|
||||
for _, edit := range result.SourceEdits {
|
||||
if edit.UserID != ownerUserID {
|
||||
continue
|
||||
}
|
||||
if update := tgOtherUpdateFromEvent(edit.Event); update != nil {
|
||||
updates.Updates = append(updates.Updates, update)
|
||||
if edit.Event.Date > updates.Date {
|
||||
updates.Date = edit.Event.Date
|
||||
}
|
||||
}
|
||||
}
|
||||
if includeBalance {
|
||||
updates.Updates = append(updates.Updates, &tg.UpdateStarsBalance{Balance: &tg.StarsAmount{Amount: result.Balance.Balance}})
|
||||
}
|
||||
|
|
@ -175,6 +247,20 @@ func (r *Router) onPaymentsGetStarGiftUpgradePreview(ctx context.Context, giftID
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (r *Router) onPaymentsGetStarGiftUpgradeAttributes(ctx context.Context, giftID int64) (*tg.PaymentsStarGiftUpgradeAttributes, error) {
|
||||
if giftID <= 0 || r.deps.Gifts == nil {
|
||||
return nil, starGiftInvalidErr()
|
||||
}
|
||||
preview, found, err := r.deps.Gifts.CollectiblePreview(ctx, giftID)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if !found {
|
||||
return nil, starGiftInvalidErr()
|
||||
}
|
||||
return &tg.PaymentsStarGiftUpgradeAttributes{Attributes: tgAllStarGiftAttributes(preview)}, nil
|
||||
}
|
||||
|
||||
func (r *Router) onPaymentsGetUniqueStarGift(ctx context.Context, slug string) (*tg.PaymentsUniqueStarGift, error) {
|
||||
if r.deps.Gifts == nil || strings.TrimSpace(slug) == "" {
|
||||
return nil, starGiftInvalidErr()
|
||||
|
|
@ -211,6 +297,9 @@ func (r *Router) onPaymentsGetUniqueStarGift(ctx context.Context, slug string) (
|
|||
func tgStarGiftPreviewAttributes(preview domain.StarGiftUpgradePreview) []tg.StarGiftAttributeClass {
|
||||
out := make([]tg.StarGiftAttributeClass, 0, len(preview.Models)+len(preview.Patterns)+len(preview.Backdrops))
|
||||
for _, attribute := range preview.Models {
|
||||
if attribute.Crafted {
|
||||
continue
|
||||
}
|
||||
out = append(out, tgStarGiftAttribute(attribute))
|
||||
}
|
||||
for _, attribute := range preview.Patterns {
|
||||
|
|
@ -222,15 +311,25 @@ func tgStarGiftPreviewAttributes(preview domain.StarGiftUpgradePreview) []tg.Sta
|
|||
return out
|
||||
}
|
||||
|
||||
func tgAllStarGiftAttributes(preview domain.StarGiftUpgradePreview) []tg.StarGiftAttributeClass {
|
||||
out := make([]tg.StarGiftAttributeClass, 0, len(preview.Models)+len(preview.Patterns)+len(preview.Backdrops))
|
||||
for _, attributes := range [][]domain.StarGiftCollectibleAttribute{preview.Models, preview.Patterns, preview.Backdrops} {
|
||||
for _, attribute := range attributes {
|
||||
out = append(out, tgStarGiftAttribute(attribute))
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func tgStarGiftAttribute(attribute domain.StarGiftCollectibleAttribute) tg.StarGiftAttributeClass {
|
||||
rarity := &tg.StarGiftAttributeRarity{Permille: attribute.RarityPermille}
|
||||
rarity := tgStarGiftAttributeRarity(attribute)
|
||||
switch attribute.Kind {
|
||||
case domain.StarGiftCollectibleModel:
|
||||
document := tg.DocumentClass(&tg.DocumentEmpty{})
|
||||
if attribute.Document != nil {
|
||||
document = tgDocument(*attribute.Document)
|
||||
}
|
||||
return &tg.StarGiftAttributeModel{Name: attribute.Name, Document: document, Rarity: rarity}
|
||||
return &tg.StarGiftAttributeModel{Name: attribute.Name, Document: document, Rarity: rarity, Crafted: attribute.Crafted}
|
||||
case domain.StarGiftCollectiblePattern:
|
||||
document := tg.DocumentClass(&tg.DocumentEmpty{})
|
||||
if attribute.Document != nil {
|
||||
|
|
@ -248,6 +347,21 @@ func tgStarGiftAttribute(attribute domain.StarGiftCollectibleAttribute) tg.StarG
|
|||
}
|
||||
}
|
||||
|
||||
func tgStarGiftAttributeRarity(attribute domain.StarGiftCollectibleAttribute) tg.StarGiftAttributeRarityClass {
|
||||
switch attribute.RarityKind {
|
||||
case domain.StarGiftRarityUncommon:
|
||||
return &tg.StarGiftAttributeRarityUncommon{}
|
||||
case domain.StarGiftRarityRare:
|
||||
return &tg.StarGiftAttributeRarityRare{}
|
||||
case domain.StarGiftRarityEpic:
|
||||
return &tg.StarGiftAttributeRarityEpic{}
|
||||
case domain.StarGiftRarityLegendary:
|
||||
return &tg.StarGiftAttributeRarityLegendary{}
|
||||
default:
|
||||
return &tg.StarGiftAttributeRarity{Permille: attribute.RarityPermille}
|
||||
}
|
||||
}
|
||||
|
||||
func tgUniqueStarGift(unique domain.UniqueStarGift) *tg.StarGiftUnique {
|
||||
attributes := []tg.StarGiftAttributeClass{
|
||||
tgStarGiftAttribute(unique.Model),
|
||||
|
|
@ -268,11 +382,73 @@ func tgUniqueStarGift(unique domain.UniqueStarGift) *tg.StarGiftUnique {
|
|||
attributes = append(attributes, original)
|
||||
}
|
||||
out := &tg.StarGiftUnique{
|
||||
RequirePremium: unique.RequirePremium, ResaleTonOnly: unique.ResaleTonOnly,
|
||||
ThemeAvailable: unique.ThemeAvailable, Burned: unique.Burned, Crafted: unique.Crafted,
|
||||
ID: unique.ID, GiftID: unique.GiftID, Title: unique.Title, Slug: unique.Slug, Num: unique.Num,
|
||||
Attributes: attributes, AvailabilityIssued: unique.AvailabilityIssued, AvailabilityTotal: unique.AvailabilityTotal,
|
||||
}
|
||||
if owner := tgPeer(unique.Owner); owner != nil {
|
||||
if unique.OwnerAddress != "" {
|
||||
out.SetOwnerAddress(unique.OwnerAddress)
|
||||
} else if owner := tgPeer(unique.Owner); owner != nil {
|
||||
out.SetOwnerID(owner)
|
||||
} else if unique.OwnerName != "" {
|
||||
out.SetOwnerName(unique.OwnerName)
|
||||
}
|
||||
if unique.GiftAddress != "" {
|
||||
out.SetGiftAddress(unique.GiftAddress)
|
||||
}
|
||||
if unique.ResellAmount != nil {
|
||||
out.SetResellAmount([]tg.StarsAmountClass{tgStarGiftAmount(*unique.ResellAmount)})
|
||||
}
|
||||
if peer := tgPeer(unique.ReleasedBy); peer != nil {
|
||||
out.SetReleasedBy(peer)
|
||||
}
|
||||
if unique.ValueAmount > 0 {
|
||||
out.SetValueAmount(unique.ValueAmount)
|
||||
}
|
||||
if unique.ValueCurrency != "" {
|
||||
out.SetValueCurrency(unique.ValueCurrency)
|
||||
}
|
||||
if unique.ValueUSD > 0 {
|
||||
out.SetValueUsdAmount(unique.ValueUSD)
|
||||
}
|
||||
if peer := tgPeer(unique.ThemePeer); peer != nil {
|
||||
out.SetThemePeer(peer)
|
||||
}
|
||||
if peer := tgPeer(unique.Host); peer != nil {
|
||||
out.SetHostID(peer)
|
||||
}
|
||||
if unique.OfferMinStars > 0 && unique.Owner.Type == domain.PeerTypeUser {
|
||||
out.SetOfferMinStars(unique.OfferMinStars)
|
||||
}
|
||||
if unique.CraftChancePermille > 0 {
|
||||
out.SetCraftChancePermille(unique.CraftChancePermille)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func tgStarGiftAmount(amount domain.StarGiftAmount) tg.StarsAmountClass {
|
||||
if amount.Currency == domain.StarGiftCurrencyTON {
|
||||
return &tg.StarsTonAmount{Amount: amount.Amount}
|
||||
}
|
||||
return &tg.StarsAmount{Amount: amount.Amount, Nanos: amount.Nanos}
|
||||
}
|
||||
|
||||
func domainStarGiftAmount(amount tg.StarsAmountClass) (domain.StarGiftAmount, bool) {
|
||||
switch value := amount.(type) {
|
||||
case *tg.StarsAmount:
|
||||
if value == nil {
|
||||
return domain.StarGiftAmount{}, false
|
||||
}
|
||||
out := domain.StarGiftAmount{Currency: domain.StarGiftCurrencyStars, Amount: value.Amount, Nanos: value.Nanos}
|
||||
return out, out.Valid()
|
||||
case *tg.StarsTonAmount:
|
||||
if value == nil {
|
||||
return domain.StarGiftAmount{}, false
|
||||
}
|
||||
out := domain.StarGiftAmount{Currency: domain.StarGiftCurrencyTON, Amount: value.Amount}
|
||||
return out, out.Valid()
|
||||
default:
|
||||
return domain.StarGiftAmount{}, false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ package rpc
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
"github.com/iamxvbaba/td/tgerr"
|
||||
|
|
@ -80,6 +84,21 @@ func (r *Router) onPaymentsGetPaymentForm(ctx context.Context, req *tg.PaymentsG
|
|||
if inv, ok := req.Invoice.(*tg.InputInvoiceStarGiftUpgrade); ok {
|
||||
return r.starGiftUpgradePaymentForm(ctx, userID, inv)
|
||||
}
|
||||
if inv, ok := req.Invoice.(*tg.InputInvoiceStarGiftTransfer); ok {
|
||||
return r.starGiftTransferPaymentForm(ctx, userID, inv)
|
||||
}
|
||||
if inv, ok := req.Invoice.(*tg.InputInvoiceStarGiftResale); ok {
|
||||
return r.starGiftResalePaymentForm(ctx, userID, inv)
|
||||
}
|
||||
if inv, ok := req.Invoice.(*tg.InputInvoiceStarGiftAuctionBid); ok {
|
||||
return r.starGiftAuctionBidPaymentForm(ctx, userID, inv)
|
||||
}
|
||||
if inv, ok := req.Invoice.(*tg.InputInvoiceStarGiftPrepaidUpgrade); ok {
|
||||
return r.starGiftPrepaidUpgradePaymentForm(ctx, userID, inv)
|
||||
}
|
||||
if inv, ok := req.Invoice.(*tg.InputInvoiceStarGiftDropOriginalDetails); ok {
|
||||
return r.starGiftDropDetailsPaymentForm(ctx, userID, inv)
|
||||
}
|
||||
|
||||
inv, ok := req.Invoice.(*tg.InputInvoiceStarGift)
|
||||
if !ok {
|
||||
|
|
@ -92,16 +111,13 @@ func (r *Router) onPaymentsGetPaymentForm(ctx context.Context, req *tg.PaymentsG
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if inv.IncludeUpgrade && peer.Type != domain.PeerTypeUser {
|
||||
// Channel upgrades remain blocked until they can advance channel pts and
|
||||
// publish a durable channel update. Never collect a prepaid upgrade that
|
||||
// the recipient cannot consume.
|
||||
return nil, starGiftInvalidErr()
|
||||
}
|
||||
gift, err := r.starGiftFromCatalog(ctx, inv.GiftID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if gift.RequirePremium && !r.viewerPremium(ctx, userID) {
|
||||
return nil, tgerr400("PREMIUM_ACCOUNT_REQUIRED")
|
||||
}
|
||||
upgradeStars := int64(0)
|
||||
if inv.IncludeUpgrade {
|
||||
if gift.UpgradeStars <= 0 || gift.UpgradeIssued >= gift.UpgradeTotal {
|
||||
|
|
@ -109,8 +125,21 @@ func (r *Router) onPaymentsGetPaymentForm(ctx context.Context, req *tg.PaymentsG
|
|||
}
|
||||
upgradeStars = gift.UpgradeStars
|
||||
}
|
||||
giftMessage := ""
|
||||
if m, ok := inv.GetMessage(); ok {
|
||||
giftMessage = clampGiftMessage(m.Text)
|
||||
}
|
||||
now := int(r.clock.Now().Unix())
|
||||
form, err := r.deps.Gifts.IssuePurchaseForm(ctx, domain.StarGiftPurchaseForm{
|
||||
BuyerUserID: userID, To: peer, GiftID: gift.ID, RevisionID: gift.RevisionID,
|
||||
IncludeUpgrade: inv.IncludeUpgrade, HideName: inv.HideName, Message: giftMessage,
|
||||
ChargeStars: gift.Stars + upgradeStars, IssuedAt: now, ExpiresAt: now + 600,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, starGiftLifecycleErr(err)
|
||||
}
|
||||
return &tg.PaymentsPaymentFormStarGift{
|
||||
FormID: starGiftFormIDWithUpgrade(userID, peer, gift, inv.IncludeUpgrade),
|
||||
FormID: form.FormID,
|
||||
Invoice: tg.Invoice{
|
||||
Currency: "XTR",
|
||||
Prices: []tg.LabeledPrice{{Label: giftPriceLabel(gift), Amount: gift.Stars + upgradeStars}},
|
||||
|
|
@ -139,11 +168,29 @@ func (r *Router) onPaymentsSendStarsForm(ctx context.Context, req *tg.PaymentsSe
|
|||
if inv, ok := req.Invoice.(*tg.InputInvoiceStarGiftUpgrade); ok {
|
||||
return r.sendStarGiftUpgradeForm(ctx, userID, req.FormID, inv)
|
||||
}
|
||||
if inv, ok := req.Invoice.(*tg.InputInvoiceStarGiftTransfer); ok {
|
||||
return r.sendStarGiftTransferForm(ctx, userID, req.FormID, inv)
|
||||
}
|
||||
if inv, ok := req.Invoice.(*tg.InputInvoiceStarGiftResale); ok {
|
||||
return r.sendStarGiftResaleForm(ctx, userID, req.FormID, inv)
|
||||
}
|
||||
if inv, ok := req.Invoice.(*tg.InputInvoiceStarGiftAuctionBid); ok {
|
||||
return r.sendStarGiftAuctionBidForm(ctx, userID, req.FormID, inv)
|
||||
}
|
||||
if inv, ok := req.Invoice.(*tg.InputInvoiceStarGiftPrepaidUpgrade); ok {
|
||||
return r.sendStarGiftPrepaidUpgradeForm(ctx, userID, req.FormID, inv)
|
||||
}
|
||||
if inv, ok := req.Invoice.(*tg.InputInvoiceStarGiftDropOriginalDetails); ok {
|
||||
return r.sendStarGiftDropDetailsForm(ctx, userID, req.FormID, inv)
|
||||
}
|
||||
|
||||
inv, ok := req.Invoice.(*tg.InputInvoiceStarGift)
|
||||
if !ok {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
if req.FormID == 0 {
|
||||
return nil, formIDEmptyErr()
|
||||
}
|
||||
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, inv.Peer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -151,15 +198,9 @@ func (r *Router) onPaymentsSendStarsForm(ctx context.Context, req *tg.PaymentsSe
|
|||
if (peer.Type != domain.PeerTypeUser && peer.Type != domain.PeerTypeChannel) || peer.ID == 0 {
|
||||
return nil, peerIDInvalidErr()
|
||||
}
|
||||
if inv.IncludeUpgrade && peer.Type != domain.PeerTypeUser {
|
||||
return nil, starGiftInvalidErr()
|
||||
}
|
||||
if r.deps.Stars == nil || r.deps.Gifts == nil {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
if peer.Type == domain.PeerTypeUser && r.deps.Messages == nil {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
if peer.Type == domain.PeerTypeChannel && r.deps.Channels == nil {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
|
|
@ -167,6 +208,10 @@ func (r *Router) onPaymentsSendStarsForm(ctx context.Context, req *tg.PaymentsSe
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buyerPremium := r.viewerPremium(ctx, userID)
|
||||
if gift.RequirePremium && !buyerPremium {
|
||||
return nil, tgerr400("PREMIUM_ACCOUNT_REQUIRED")
|
||||
}
|
||||
upgradeStars := int64(0)
|
||||
if inv.IncludeUpgrade {
|
||||
if gift.UpgradeStars <= 0 || gift.UpgradeIssued >= gift.UpgradeTotal {
|
||||
|
|
@ -174,27 +219,58 @@ func (r *Router) onPaymentsSendStarsForm(ctx context.Context, req *tg.PaymentsSe
|
|||
}
|
||||
upgradeStars = gift.UpgradeStars
|
||||
}
|
||||
if req.FormID != starGiftFormIDWithUpgrade(userID, peer, gift, inv.IncludeUpgrade) {
|
||||
return nil, starsFormAmountMismatchErr()
|
||||
}
|
||||
giftMessage := ""
|
||||
if m, ok := inv.GetMessage(); ok {
|
||||
giftMessage = clampGiftMessage(m.Text)
|
||||
}
|
||||
now := int(r.clock.Now().Unix())
|
||||
purchaseReq := domain.StarGiftPurchaseRequest{BuyerUserID: userID, BuyerPremium: buyerPremium, To: peer,
|
||||
GiftID: gift.ID, RevisionID: gift.RevisionID, IncludeUpgrade: inv.IncludeUpgrade, HideName: inv.HideName, Message: giftMessage,
|
||||
ChargeStars: gift.Stars + upgradeStars, FormID: req.FormID, CommandKey: fmt.Sprintf("purchase:%d", req.FormID), Date: now,
|
||||
OriginAuthKeyID: rawAuthKeyIDForOrigin(ctx), OriginSessionID: sessionIDOrZero(ctx)}
|
||||
recipientBlocked := false
|
||||
if peer.Type == domain.PeerTypeUser {
|
||||
recipientBlocked, err = r.peerBlocksUser(ctx, userID, peer.ID)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
}
|
||||
if capability, ok := r.deps.Gifts.(interface{ AtomicPurchaseConfigured() bool }); ok && !capability.AtomicPurchaseConfigured() {
|
||||
if err := r.deps.Gifts.ValidatePurchaseForm(ctx, purchaseReq); err != nil {
|
||||
return nil, starGiftLifecycleErr(err)
|
||||
}
|
||||
if _, err := r.deps.Stars.GetBalance(ctx, userID); err != nil {
|
||||
return nil, starsErr(err)
|
||||
}
|
||||
return r.sendStarGiftMemoryPurchase(ctx, userID, peer, gift, inv, giftMessage, upgradeStars)
|
||||
}
|
||||
if _, err := r.deps.Stars.GetBalance(ctx, userID); err != nil {
|
||||
return nil, starsErr(err)
|
||||
}
|
||||
purchaseReq.RecipientBlocked = recipientBlocked
|
||||
result, err := r.deps.Gifts.Purchase(ctx, purchaseReq)
|
||||
if err != nil {
|
||||
return nil, starGiftLifecycleErr(err)
|
||||
}
|
||||
updates := r.starGiftSendUpdates(ctx, userID, result.Send)
|
||||
appendStarGiftBalanceUpdate(updates, domain.StarGiftCurrencyStars, result.Balance.Balance)
|
||||
r.invalidateStarGiftOwner(peer)
|
||||
return &tg.PaymentsPaymentResult{Updates: updates}, nil
|
||||
}
|
||||
|
||||
// 1. Debit 送礼人(不足→BALANCE_TOO_LOW)。
|
||||
func (r *Router) sendStarGiftMemoryPurchase(ctx context.Context, userID int64, peer domain.Peer, gift domain.StarGift,
|
||||
inv *tg.InputInvoiceStarGift, giftMessage string, upgradeStars int64) (tg.PaymentsPaymentResultClass, error) {
|
||||
purchaseStars := gift.Stars + upgradeStars
|
||||
balance, err := r.deps.Stars.Debit(ctx, userID, purchaseStars, domain.StarsReasonGift, peer, "Star gift", gift.Title)
|
||||
if err != nil {
|
||||
return nil, starsErr(err)
|
||||
}
|
||||
|
||||
var updates *tg.Updates
|
||||
switch peer.Type {
|
||||
case domain.PeerTypeUser:
|
||||
updates, err = r.sendStarGiftToUser(ctx, userID, peer.ID, gift, inv.HideName, giftMessage, upgradeStars)
|
||||
case domain.PeerTypeChannel:
|
||||
updates, err = r.sendStarGiftToChannel(ctx, userID, peer.ID, gift, inv.HideName, giftMessage)
|
||||
updates, err = r.sendStarGiftToChannel(ctx, userID, peer.ID, gift, inv.HideName, giftMessage, upgradeStars)
|
||||
default:
|
||||
err = domain.ErrStarGiftInvalid
|
||||
}
|
||||
|
|
@ -202,18 +278,10 @@ func (r *Router) onPaymentsSendStarsForm(ctx context.Context, req *tg.PaymentsSe
|
|||
r.refundStarGift(ctx, userID, peer, gift, purchaseStars)
|
||||
return nil, internalErr()
|
||||
}
|
||||
|
||||
// 4. 构建送礼人 Updates(服务消息 + updateStarsBalance)。
|
||||
if updates != nil {
|
||||
updates.Updates = append(updates.Updates, &tg.UpdateStarsBalance{Balance: &tg.StarsAmount{Amount: balance.Balance}})
|
||||
} else {
|
||||
updates = &tg.Updates{
|
||||
Updates: []tg.UpdateClass{&tg.UpdateStarsBalance{Balance: &tg.StarsAmount{Amount: balance.Balance}}},
|
||||
Users: []tg.UserClass{},
|
||||
Chats: []tg.ChatClass{},
|
||||
Date: int(r.clock.Now().Unix()),
|
||||
}
|
||||
if updates == nil {
|
||||
updates = emptyGiftUpdates(r.clock.Now().Unix())
|
||||
}
|
||||
appendStarGiftBalanceUpdate(updates, domain.StarGiftCurrencyStars, balance.Balance)
|
||||
return &tg.PaymentsPaymentResult{Updates: updates}, nil
|
||||
}
|
||||
|
||||
|
|
@ -295,8 +363,16 @@ func (r *Router) sendStarsTopupForm(ctx context.Context, userID, formID int64, i
|
|||
}
|
||||
|
||||
func (r *Router) sendStarGiftToUser(ctx context.Context, senderID, recipientID int64, gift domain.StarGift, hideName bool, message string, prepaidUpgradeStars int64) (*tg.Updates, error) {
|
||||
prepaidUpgradeHash := ""
|
||||
if prepaidUpgradeStars == 0 && gift.UpgradeStars > 0 && gift.UpgradeIssued < gift.UpgradeTotal {
|
||||
var token [32]byte
|
||||
if _, err := rand.Read(token[:]); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prepaidUpgradeHash = base64.RawURLEncoding.EncodeToString(token[:])
|
||||
}
|
||||
// 2. 投递礼物服务消息到收礼人私聊(双盒 + 推送)。
|
||||
send, err := r.deliverStarGift(ctx, senderID, recipientID, gift, hideName, message, prepaidUpgradeStars)
|
||||
send, err := r.deliverStarGift(ctx, senderID, recipientID, gift, hideName, message, prepaidUpgradeStars, prepaidUpgradeHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -312,6 +388,7 @@ func (r *Router) sendStarGiftToUser(ctx context.Context, senderID, recipientID i
|
|||
Unsaved: false,
|
||||
ConvertStars: gift.ConvertStars,
|
||||
PrepaidUpgradeStars: prepaidUpgradeStars,
|
||||
PrepaidUpgradeHash: prepaidUpgradeHash,
|
||||
Message: message,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -324,38 +401,40 @@ func (r *Router) sendStarGiftToUser(ctx context.Context, senderID, recipientID i
|
|||
return tgPrivateMessageUpdates(send.SenderEvent, send.SenderMessage, 0, false, users, chats), nil
|
||||
}
|
||||
|
||||
func (r *Router) sendStarGiftToChannel(ctx context.Context, senderID, channelID int64, gift domain.StarGift, hideName bool, message string) (*tg.Updates, error) {
|
||||
func (r *Router) sendStarGiftToChannel(ctx context.Context, senderID, channelID int64, gift domain.StarGift, hideName bool, message string, prepaidUpgradeStars int64) (*tg.Updates, error) {
|
||||
now := int(r.clock.Now().Unix())
|
||||
sticker := gift.Sticker
|
||||
action := domain.ChannelMessageAction{
|
||||
Type: domain.ChannelActionStarGift,
|
||||
StarGift: &domain.MessageStarGiftAction{
|
||||
GiftID: gift.ID,
|
||||
Stars: gift.Stars,
|
||||
ConvertStars: gift.ConvertStars,
|
||||
Title: gift.Title,
|
||||
Sticker: &sticker,
|
||||
Message: message,
|
||||
FromUserID: senderID,
|
||||
NameHidden: hideName,
|
||||
Saved: true,
|
||||
CanUpgrade: false,
|
||||
PrepaidUpgrade: false,
|
||||
UpgradeStars: 0,
|
||||
GiftID: gift.ID,
|
||||
Stars: gift.Stars,
|
||||
ConvertStars: gift.ConvertStars,
|
||||
Title: gift.Title,
|
||||
Sticker: &sticker,
|
||||
Message: message,
|
||||
FromUserID: senderID,
|
||||
NameHidden: hideName,
|
||||
Saved: true,
|
||||
CanUpgrade: gift.UpgradeStars > 0,
|
||||
PrepaidUpgrade: prepaidUpgradeStars > 0,
|
||||
UpgradePriceStars: gift.UpgradeStars,
|
||||
UpgradeStars: prepaidUpgradeStars,
|
||||
},
|
||||
}
|
||||
savedID, err := r.deps.Gifts.RecordSavedGift(ctx, domain.SavedStarGift{
|
||||
Owner: domain.Peer{Type: domain.PeerTypeChannel, ID: channelID},
|
||||
FromUserID: senderID,
|
||||
GiftID: gift.ID,
|
||||
RevisionID: gift.RevisionID,
|
||||
MsgID: 0,
|
||||
SavedID: 0,
|
||||
Date: now,
|
||||
NameHidden: hideName,
|
||||
Unsaved: false,
|
||||
ConvertStars: gift.ConvertStars,
|
||||
Message: message,
|
||||
Owner: domain.Peer{Type: domain.PeerTypeChannel, ID: channelID},
|
||||
FromUserID: senderID,
|
||||
GiftID: gift.ID,
|
||||
RevisionID: gift.RevisionID,
|
||||
MsgID: 0,
|
||||
SavedID: 0,
|
||||
Date: now,
|
||||
NameHidden: hideName,
|
||||
Unsaved: false,
|
||||
ConvertStars: gift.ConvertStars,
|
||||
PrepaidUpgradeStars: prepaidUpgradeStars,
|
||||
Message: message,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -375,7 +454,7 @@ func (r *Router) sendStarGiftToChannel(ctx context.Context, senderID, channelID
|
|||
}
|
||||
|
||||
// deliverStarGift 经 SendPrivateText 把 messageActionStarGift 服务消息投递到收礼人私聊。
|
||||
func (r *Router) deliverStarGift(ctx context.Context, senderID, recipientID int64, gift domain.StarGift, hideName bool, message string, prepaidUpgradeStars int64) (domain.SendPrivateTextResult, error) {
|
||||
func (r *Router) deliverStarGift(ctx context.Context, senderID, recipientID int64, gift domain.StarGift, hideName bool, message string, prepaidUpgradeStars int64, prepaidUpgradeHash string) (domain.SendPrivateTextResult, error) {
|
||||
recipientBlocked, err := r.peerBlocksUser(ctx, senderID, recipientID)
|
||||
if err != nil {
|
||||
return domain.SendPrivateTextResult{}, err
|
||||
|
|
@ -387,19 +466,21 @@ func (r *Router) deliverStarGift(ctx context.Context, senderID, recipientID int6
|
|||
ServiceAction: &domain.MessageServiceAction{
|
||||
Kind: domain.MessageServiceActionStarGift,
|
||||
StarGift: &domain.MessageStarGiftAction{
|
||||
GiftID: gift.ID,
|
||||
Stars: gift.Stars,
|
||||
ConvertStars: gift.ConvertStars,
|
||||
Title: gift.Title,
|
||||
Sticker: &sticker,
|
||||
Message: message,
|
||||
FromUserID: senderID,
|
||||
PeerUserID: recipientID,
|
||||
NameHidden: hideName,
|
||||
Saved: true,
|
||||
CanUpgrade: gift.UpgradeStars > 0,
|
||||
PrepaidUpgrade: prepaidUpgradeStars > 0,
|
||||
UpgradeStars: gift.UpgradeStars,
|
||||
GiftID: gift.ID,
|
||||
Stars: gift.Stars,
|
||||
ConvertStars: gift.ConvertStars,
|
||||
Title: gift.Title,
|
||||
Sticker: &sticker,
|
||||
Message: message,
|
||||
FromUserID: senderID,
|
||||
PeerUserID: recipientID,
|
||||
NameHidden: hideName,
|
||||
Saved: true,
|
||||
CanUpgrade: gift.UpgradeStars > 0,
|
||||
PrepaidUpgrade: prepaidUpgradeStars > 0,
|
||||
PrepaidUpgradeHash: prepaidUpgradeHash,
|
||||
UpgradePriceStars: gift.UpgradeStars,
|
||||
UpgradeStars: prepaidUpgradeStars,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -529,13 +610,15 @@ func (r *Router) onPaymentsSaveStarGift(ctx context.Context, req *tg.PaymentsSav
|
|||
return true, nil
|
||||
}
|
||||
|
||||
// onPaymentsConvertStarGift 把收到的礼物转换回 Stars(Credit + 标记 converted)。
|
||||
// onPaymentsConvertStarGift atomically destroys the regular gift and credits
|
||||
// the owner-scoped internal Stars ledger. Channel proceeds never leak to the
|
||||
// acting administrator's personal balance.
|
||||
func (r *Router) onPaymentsConvertStarGift(ctx context.Context, ref tg.InputSavedStarGiftClass) (bool, error) {
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
if r.deps.Gifts == nil || r.deps.Stars == nil {
|
||||
if r.deps.Gifts == nil {
|
||||
return false, notImplementedErr()
|
||||
}
|
||||
dref, ok, err := r.starGiftRefFromInput(ctx, userID, ref)
|
||||
|
|
@ -545,10 +628,42 @@ func (r *Router) onPaymentsConvertStarGift(ctx context.Context, ref tg.InputSave
|
|||
if !ok {
|
||||
return false, starGiftInvalidErr()
|
||||
}
|
||||
if dref.Owner.Type == domain.PeerTypeChannel {
|
||||
return false, notImplementedErr()
|
||||
if err := r.ensureCanManageStarGiftOwner(ctx, userID, dref.Owner); err != nil {
|
||||
return false, err
|
||||
}
|
||||
saved, err := r.deps.Gifts.Convert(ctx, dref)
|
||||
// The isolated memory RPC adapter intentionally has no aggregate store. Keep
|
||||
// its conversion primitive usable for tests, but never use this split write
|
||||
// path when the production lifecycle coordinator is configured. Channel
|
||||
// balances have no memory adapter because crediting an administrator would
|
||||
// violate owner-scoped accounting.
|
||||
if converter, ok := r.deps.Gifts.(interface {
|
||||
AtomicPurchaseConfigured() bool
|
||||
Convert(context.Context, domain.SavedStarGiftRef) (domain.SavedStarGift, error)
|
||||
}); ok && !converter.AtomicPurchaseConfigured() {
|
||||
if dref.Owner.Type != domain.PeerTypeUser || dref.Owner.ID != userID {
|
||||
return false, notImplementedErr()
|
||||
}
|
||||
updated, convertErr := converter.Convert(ctx, dref)
|
||||
if convertErr != nil {
|
||||
if errors.Is(convertErr, domain.ErrStarGiftNotFound) || errors.Is(convertErr, domain.ErrStarGiftAlreadyConverted) {
|
||||
return false, starGiftInvalidErr()
|
||||
}
|
||||
return false, internalErr()
|
||||
}
|
||||
if updated.ConvertStars > 0 {
|
||||
if _, creditErr := r.deps.Stars.Credit(ctx, userID, updated.ConvertStars, domain.StarsReasonGift,
|
||||
dref.Owner, "Star gift conversion", "Converted Star Gift"); creditErr != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
}
|
||||
r.invalidateStarGiftOwnerProjection(dref.Owner)
|
||||
return true, nil
|
||||
}
|
||||
result, err := r.deps.Gifts.ConvertAggregate(ctx, domain.StarGiftConvertRequest{
|
||||
ActorUserID: userID,
|
||||
Ref: dref,
|
||||
Date: int(r.clock.Now().Unix()),
|
||||
})
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrStarGiftNotFound):
|
||||
|
|
@ -559,15 +674,8 @@ func (r *Router) onPaymentsConvertStarGift(ctx context.Context, ref tg.InputSave
|
|||
return false, internalErr()
|
||||
}
|
||||
}
|
||||
if saved.ConvertStars > 0 {
|
||||
fromPeer := domain.Peer{Type: domain.PeerTypeUser, ID: saved.FromUserID}
|
||||
if _, err := r.deps.Stars.Credit(ctx, userID, saved.ConvertStars, domain.StarsReasonGift, fromPeer, "Star gift conversion", ""); err != nil {
|
||||
r.log.Error("star gift convert credit failed", zap.Int64("user_id", userID), zap.Int("msg_id", dref.MsgID), zap.Error(err))
|
||||
return false, internalErr()
|
||||
}
|
||||
}
|
||||
// 转换移除一份展示礼物 → 失效 owner full 投影。
|
||||
r.invalidateStarGiftOwnerProjection(dref.Owner)
|
||||
r.invalidateStarGiftOwnerProjection(result.Saved.Owner)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
|
@ -622,6 +730,24 @@ func (r *Router) starGiftRefFromInput(ctx context.Context, userID int64, ref tg.
|
|||
return domain.SavedStarGiftRef{}, false, peerIDInvalidErr()
|
||||
}
|
||||
return domain.SavedStarGiftRef{Owner: owner, SavedID: v.SavedID}, true, nil
|
||||
case *tg.InputSavedStarGiftSlug:
|
||||
if v == nil || r.deps.Gifts == nil {
|
||||
return domain.SavedStarGiftRef{}, false, nil
|
||||
}
|
||||
slug := strings.ToLower(strings.TrimSpace(v.Slug))
|
||||
if slug == "" || len(slug) > domain.MaxStarGiftSlugBytes {
|
||||
return domain.SavedStarGiftRef{}, false, nil
|
||||
}
|
||||
unique, found, err := r.deps.Gifts.UniqueBySlug(ctx, slug)
|
||||
if err != nil {
|
||||
return domain.SavedStarGiftRef{}, false, internalErr()
|
||||
}
|
||||
if !found || unique.Slug == "" || unique.Owner.ID == 0 ||
|
||||
(unique.Owner.Type != domain.PeerTypeUser && unique.Owner.Type != domain.PeerTypeChannel) {
|
||||
return domain.SavedStarGiftRef{}, false, nil
|
||||
}
|
||||
resolved := domain.SavedStarGiftRef{Owner: unique.Owner, Slug: strings.ToLower(strings.TrimSpace(unique.Slug))}
|
||||
return resolved, resolved.Valid(), nil
|
||||
default:
|
||||
return domain.SavedStarGiftRef{}, false, nil
|
||||
}
|
||||
|
|
@ -718,11 +844,6 @@ func (r *Router) resolveStarGiftCollectibleAvailability(ctx context.Context, gif
|
|||
if gift.UniqueGiftID != 0 {
|
||||
continue
|
||||
}
|
||||
if gift.Owner.Type != domain.PeerTypeUser {
|
||||
// Channel upgrade RPCs are deliberately blocked until the channel pts
|
||||
// aggregate exists, so do not advertise a dead-end action.
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[gift.GiftID]; ok {
|
||||
continue
|
||||
}
|
||||
|
|
@ -755,10 +876,24 @@ func tgStarGifts(catalog []domain.StarGift) []tg.StarGiftClass {
|
|||
// tgStarGift 把目录项投影为 tg.StarGift(Sticker 须为带 sticker 属性的有效 Document)。
|
||||
func tgStarGift(g domain.StarGift) *tg.StarGift {
|
||||
gift := &tg.StarGift{
|
||||
ID: g.ID,
|
||||
Sticker: tgDocument(g.Sticker),
|
||||
Stars: g.Stars,
|
||||
ConvertStars: g.ConvertStars,
|
||||
Limited: g.Limited, SoldOut: g.SoldOut, Birthday: g.Birthday,
|
||||
RequirePremium: g.RequirePremium, LimitedPerUser: g.LimitedPerUser,
|
||||
PeerColorAvailable: g.PeerColorAvailable, Auction: g.Auction,
|
||||
ID: g.ID, Sticker: tgDocument(g.Sticker), Stars: g.Stars, ConvertStars: g.ConvertStars,
|
||||
}
|
||||
if g.Limited {
|
||||
gift.SetAvailabilityRemains(g.AvailabilityRemains)
|
||||
gift.SetAvailabilityTotal(g.AvailabilityTotal)
|
||||
}
|
||||
if g.AvailabilityResale > 0 {
|
||||
gift.SetAvailabilityResale(g.AvailabilityResale)
|
||||
}
|
||||
// sold_out, first_sale_date and last_sale_date share TL flags.1. The store
|
||||
// retains sale timestamps for live gifts as operational facts, but exposing
|
||||
// either timestamp would make every client decode the gift as sold out.
|
||||
if g.SoldOut {
|
||||
gift.SetFirstSaleDate(g.FirstSaleDate)
|
||||
gift.SetLastSaleDate(g.LastSaleDate)
|
||||
}
|
||||
if g.Title != "" {
|
||||
gift.SetTitle(g.Title)
|
||||
|
|
@ -766,6 +901,31 @@ func tgStarGift(g domain.StarGift) *tg.StarGift {
|
|||
if g.UpgradeStars > 0 && g.UpgradeIssued < g.UpgradeTotal {
|
||||
gift.SetUpgradeStars(g.UpgradeStars)
|
||||
}
|
||||
if g.ResellMinStars > 0 {
|
||||
gift.SetResellMinStars(g.ResellMinStars)
|
||||
}
|
||||
if releasedBy := tgPeer(g.ReleasedBy); releasedBy != nil {
|
||||
gift.SetReleasedBy(releasedBy)
|
||||
}
|
||||
if g.LimitedPerUser {
|
||||
gift.SetPerUserTotal(g.PerUserTotal)
|
||||
gift.SetPerUserRemains(g.PerUserRemains)
|
||||
}
|
||||
if g.LockedUntilDate > 0 {
|
||||
gift.SetLockedUntilDate(g.LockedUntilDate)
|
||||
}
|
||||
if g.Auction {
|
||||
gift.SetAuctionSlug(g.AuctionSlug)
|
||||
gift.SetGiftsPerRound(g.GiftsPerRound)
|
||||
gift.SetAuctionStartDate(g.AuctionStartDate)
|
||||
}
|
||||
if g.UpgradeVariants > 0 {
|
||||
gift.SetUpgradeVariants(g.UpgradeVariants)
|
||||
}
|
||||
if g.Background != nil {
|
||||
gift.SetBackground(tg.StarGiftBackground{CenterColor: g.Background.CenterColor,
|
||||
EdgeColor: g.Background.EdgeColor, TextColor: g.Background.TextColor})
|
||||
}
|
||||
return gift
|
||||
}
|
||||
|
||||
|
|
@ -787,6 +947,9 @@ func tgMessageActionStarGift(in *domain.MessageStarGiftAction) tg.MessageActionC
|
|||
if in.Title != "" {
|
||||
gift.SetTitle(in.Title)
|
||||
}
|
||||
if in.UpgradePriceStars > 0 {
|
||||
gift.SetUpgradeStars(in.UpgradePriceStars)
|
||||
}
|
||||
action := &tg.MessageActionStarGift{Gift: gift}
|
||||
if in.NameHidden {
|
||||
action.NameHidden = true
|
||||
|
|
@ -799,12 +962,26 @@ func tgMessageActionStarGift(in *domain.MessageStarGiftAction) tg.MessageActionC
|
|||
}
|
||||
action.CanUpgrade = in.CanUpgrade
|
||||
action.PrepaidUpgrade = in.PrepaidUpgrade
|
||||
action.UpgradeSeparate = in.UpgradeSeparate
|
||||
action.AuctionAcquired = in.AuctionAcquired
|
||||
if in.UpgradeStars > 0 {
|
||||
action.SetUpgradeStars(in.UpgradeStars)
|
||||
}
|
||||
if in.UpgradeMsgID > 0 {
|
||||
action.SetUpgradeMsgID(in.UpgradeMsgID)
|
||||
}
|
||||
if in.PrepaidUpgradeHash != "" {
|
||||
action.SetPrepaidUpgradeHash(in.PrepaidUpgradeHash)
|
||||
}
|
||||
if in.GiftMsgID > 0 {
|
||||
action.SetGiftMsgID(in.GiftMsgID)
|
||||
}
|
||||
if in.GiftNum > 0 {
|
||||
action.SetGiftNum(in.GiftNum)
|
||||
}
|
||||
if to := tgPeer(in.To); to != nil {
|
||||
action.SetToID(to)
|
||||
}
|
||||
if in.ConvertStars > 0 {
|
||||
action.SetConvertStars(in.ConvertStars)
|
||||
}
|
||||
|
|
@ -889,6 +1066,9 @@ func tgSavedStarGifts(gifts []domain.SavedStarGift, catalog map[int64]domain.Sta
|
|||
item.SetUpgradeStars(g.PrepaidUpgradeStars)
|
||||
item.CanUpgrade = true
|
||||
}
|
||||
if g.PrepaidUpgradeHash != "" && g.PrepaidUpgradeStars == 0 && canIssue {
|
||||
item.SetPrepaidUpgradeHash(g.PrepaidUpgradeHash)
|
||||
}
|
||||
}
|
||||
if g.PinnedOrder > 0 {
|
||||
item.PinnedToTop = true
|
||||
|
|
@ -898,6 +1078,8 @@ func tgSavedStarGifts(gifts []domain.SavedStarGift, catalog map[int64]domain.Sta
|
|||
}
|
||||
if g.Unique != nil {
|
||||
item.SetGiftNum(g.Unique.Num)
|
||||
} else if g.GiftNum > 0 {
|
||||
item.SetGiftNum(g.GiftNum)
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
|
|
@ -944,24 +1126,6 @@ func savedStarGiftUserIDs(gifts []domain.SavedStarGift) []int64 {
|
|||
return ids
|
||||
}
|
||||
|
||||
func starGiftFormID(userID int64, peer domain.Peer, gift domain.StarGift) int64 {
|
||||
return starGiftFormIDWithUpgrade(userID, peer, gift, false)
|
||||
}
|
||||
|
||||
func starGiftFormIDWithUpgrade(userID int64, peer domain.Peer, gift domain.StarGift, includeUpgrade bool) int64 {
|
||||
id := userID*0x9e3779b1 ^ (gift.ID << 7) ^ (gift.RevisionID << 11) ^ (gift.Stars << 17) ^ (peer.ID << 23) ^ 0x5347494654
|
||||
if includeUpgrade {
|
||||
id ^= gift.UpgradeStars<<29 ^ 0x55504752414445
|
||||
}
|
||||
for _, ch := range string(peer.Type) {
|
||||
id = id*131 + int64(ch)
|
||||
}
|
||||
if id == 0 {
|
||||
id = 0x5347
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func starsTopupFormID(userID, stars int64, currency string, amount int64) int64 {
|
||||
id := userID*0x9e3779b1 ^ (stars << 7) ^ (amount << 13) ^ 0x5354415253
|
||||
for _, ch := range currency {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ import (
|
|||
)
|
||||
|
||||
func starGiftTestRouter(t *testing.T) (*Router, domain.User, domain.User, domain.StarGift) {
|
||||
return starGiftTestRouterWithPremium(t, false)
|
||||
}
|
||||
|
||||
func starGiftTestRouterWithPremium(t *testing.T, requirePremium bool) (*Router, domain.User, domain.User, domain.StarGift) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
users := memory.NewUserStore()
|
||||
|
|
@ -36,7 +40,7 @@ func starGiftTestRouter(t *testing.T) (*Router, domain.User, domain.User, domain
|
|||
t.Fatalf("create recipient: %v", err)
|
||||
}
|
||||
gift := domain.StarGift{
|
||||
ID: 8001, RevisionID: 9001, Stars: 50, ConvertStars: 50, Title: "Cake",
|
||||
ID: 8001, RevisionID: 9001, Stars: 50, ConvertStars: 50, Title: "Cake", RequirePremium: requirePremium,
|
||||
Sticker: domain.Document{ID: 700, AccessHash: 7, DCID: 2, MimeType: "application/x-tgsticker", Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}}},
|
||||
}
|
||||
giftStore := memory.NewStarGiftStore()
|
||||
|
|
@ -52,6 +56,33 @@ func starGiftTestRouter(t *testing.T) (*Router, domain.User, domain.User, domain
|
|||
return r, sender, recipient, gift
|
||||
}
|
||||
|
||||
func TestStarGiftPurchaseRequiresActivePremium(t *testing.T) {
|
||||
r, sender, recipient, gift := starGiftTestRouterWithPremium(t, true)
|
||||
ctx := WithUserID(context.Background(), sender.ID)
|
||||
inv := &tg.InputInvoiceStarGift{Peer: &tg.InputPeerUser{UserID: recipient.ID, AccessHash: recipient.AccessHash}, GiftID: gift.ID}
|
||||
if _, err := r.onPaymentsGetPaymentForm(ctx, &tg.PaymentsGetPaymentFormRequest{Invoice: inv}); !tgerr.Is(err, "PREMIUM_ACCOUNT_REQUIRED") {
|
||||
t.Fatalf("non-premium gift form err = %v, want PREMIUM_ACCOUNT_REQUIRED", err)
|
||||
}
|
||||
premium, ok := r.deps.Users.(UserPremiumService)
|
||||
if !ok {
|
||||
t.Fatalf("users service %T does not implement premium grants", r.deps.Users)
|
||||
}
|
||||
if _, err := premium.GrantPremium(context.Background(), sender.ID, 1); err != nil {
|
||||
t.Fatalf("grant premium: %v", err)
|
||||
}
|
||||
formRes, err := r.onPaymentsGetPaymentForm(ctx, &tg.PaymentsGetPaymentFormRequest{Invoice: inv})
|
||||
if err != nil {
|
||||
t.Fatalf("premium gift form: %v", err)
|
||||
}
|
||||
form, ok := formRes.(*tg.PaymentsPaymentFormStarGift)
|
||||
if !ok {
|
||||
t.Fatalf("premium gift form = %T", formRes)
|
||||
}
|
||||
if _, err := r.onPaymentsSendStarsForm(ctx, &tg.PaymentsSendStarsFormRequest{FormID: form.FormID, Invoice: inv}); err != nil {
|
||||
t.Fatalf("premium gift purchase: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type uniqueGiftRPCService struct {
|
||||
GiftsService
|
||||
unique domain.UniqueStarGift
|
||||
|
|
@ -61,8 +92,139 @@ func (s *uniqueGiftRPCService) UniqueBySlug(_ context.Context, slug string) (dom
|
|||
return s.unique, slug == s.unique.Slug, nil
|
||||
}
|
||||
|
||||
type craftStarGiftRPCService struct {
|
||||
GiftsService
|
||||
uniques map[string]domain.UniqueStarGift
|
||||
saved map[int64]domain.SavedStarGift
|
||||
result domain.StarGiftCraftResult
|
||||
craftReq domain.StarGiftCraftRequest
|
||||
craftCall int
|
||||
}
|
||||
|
||||
func (s *craftStarGiftRPCService) UniqueBySlug(_ context.Context, slug string) (domain.UniqueStarGift, bool, error) {
|
||||
unique, ok := s.uniques[slug]
|
||||
return unique, ok, nil
|
||||
}
|
||||
|
||||
func (s *craftStarGiftRPCService) GetSaved(_ context.Context, ref domain.SavedStarGiftRef) (domain.SavedStarGift, bool, error) {
|
||||
for _, saved := range s.saved {
|
||||
if saved.Owner != ref.Owner {
|
||||
continue
|
||||
}
|
||||
if ref.Slug != "" {
|
||||
unique, ok := s.uniques[ref.Slug]
|
||||
if ok && unique.ID == saved.UniqueGiftID {
|
||||
return saved, true, nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
if saved.MsgID == ref.MsgID {
|
||||
return saved, true, nil
|
||||
}
|
||||
}
|
||||
return domain.SavedStarGift{}, false, nil
|
||||
}
|
||||
|
||||
func (s *craftStarGiftRPCService) Craft(_ context.Context, req domain.StarGiftCraftRequest) (domain.StarGiftCraftResult, error) {
|
||||
s.craftCall++
|
||||
s.craftReq = req
|
||||
return s.result, nil
|
||||
}
|
||||
|
||||
func TestCraftStarGiftAcceptsOfficialSlugAndCanonicalizesAliases(t *testing.T) {
|
||||
owner := domain.Peer{Type: domain.PeerTypeUser, ID: 7102}
|
||||
service := &craftStarGiftRPCService{
|
||||
uniques: map[string]domain.UniqueStarGift{
|
||||
"official-8001-2": {ID: 902, Slug: "official-8001-2", Owner: owner, SourceSavedGiftID: 52},
|
||||
},
|
||||
saved: map[int64]domain.SavedStarGift{
|
||||
50: {ID: 50, Owner: owner, MsgID: 115, UniqueGiftID: 901, UpgradeMsgID: 116},
|
||||
52: {ID: 52, Owner: owner, MsgID: 111, UniqueGiftID: 902, UpgradeMsgID: 112},
|
||||
},
|
||||
result: domain.StarGiftCraftResult{Chance: 500, SourceEdits: []domain.EditedMessageForUser{{
|
||||
UserID: owner.ID,
|
||||
Message: domain.Message{ID: 116, OwnerUserID: owner.ID, Peer: owner, From: owner, Date: 100},
|
||||
Event: domain.UpdateEvent{UserID: owner.ID, Type: domain.UpdateEventEditMessage, Pts: 41, PtsCount: 1,
|
||||
Date: 100, Message: domain.Message{ID: 116, OwnerUserID: owner.ID, Peer: owner, From: owner, Date: 100}},
|
||||
}}},
|
||||
}
|
||||
r := New(Config{DC: 2}, Deps{Gifts: service}, zaptest.NewLogger(t), clock.System)
|
||||
ctx := WithUserID(context.Background(), owner.ID)
|
||||
updates, err := r.onPaymentsCraftStarGift(ctx, &tg.PaymentsCraftStarGiftRequest{Stargift: []tg.InputSavedStarGiftClass{
|
||||
&tg.InputSavedStarGiftUser{MsgID: 115},
|
||||
&tg.InputSavedStarGiftSlug{Slug: "OFFICIAL-8001-2"},
|
||||
}})
|
||||
if err != nil || updates == nil {
|
||||
t.Fatalf("craft mixed official refs: updates=%T err=%v", updates, err)
|
||||
}
|
||||
if service.craftCall != 1 || service.craftReq.CommandKey != "rpc:50,52" || len(service.craftReq.Refs) != 2 ||
|
||||
service.craftReq.Refs[1].Slug != "official-8001-2" {
|
||||
t.Fatalf("craft request = %+v calls=%d", service.craftReq, service.craftCall)
|
||||
}
|
||||
full, ok := updates.(*tg.Updates)
|
||||
if !ok || len(full.Updates) != 2 {
|
||||
t.Fatalf("craft failure updates = %T %#v", updates, updates)
|
||||
}
|
||||
if edit, ok := full.Updates[0].(*tg.UpdateEditMessage); !ok || edit.Pts != 41 || edit.PtsCount != 1 {
|
||||
t.Fatalf("craft failure source update = %T %#v", full.Updates[0], full.Updates[0])
|
||||
}
|
||||
if _, ok := full.Updates[1].(*tg.UpdateStarGiftCraftFail); !ok {
|
||||
t.Fatalf("craft terminal update = %T %#v", full.Updates[1], full.Updates[1])
|
||||
}
|
||||
|
||||
service.craftCall = 0
|
||||
_, err = r.onPaymentsCraftStarGift(ctx, &tg.PaymentsCraftStarGiftRequest{Stargift: []tg.InputSavedStarGiftClass{
|
||||
&tg.InputSavedStarGiftUser{MsgID: 111},
|
||||
&tg.InputSavedStarGiftSlug{Slug: "official-8001-2"},
|
||||
}})
|
||||
if !tgerr.Is(err, "STARGIFT_INVALID") || service.craftCall != 0 {
|
||||
t.Fatalf("duplicate aliases err=%v craft calls=%d", err, service.craftCall)
|
||||
}
|
||||
|
||||
_, err = r.onPaymentsCraftStarGift(ctx, &tg.PaymentsCraftStarGiftRequest{Stargift: []tg.InputSavedStarGiftClass{
|
||||
&tg.InputSavedStarGiftUser{MsgID: 116},
|
||||
}})
|
||||
if !tgerr.Is(err, "STARGIFT_INVALID") || service.craftCall != 0 {
|
||||
t.Fatalf("upgrade message id accepted as gift identity: err=%v craft calls=%d", err, service.craftCall)
|
||||
}
|
||||
}
|
||||
|
||||
type upgradeReplayRPCService struct {
|
||||
GiftsService
|
||||
saved domain.SavedStarGift
|
||||
receipt domain.StarGiftUpgradeReceipt
|
||||
result domain.StarGiftUpgradeResult
|
||||
upgradeCalls int
|
||||
previewCalls int
|
||||
lastRequest domain.StarGiftUpgradeRequest
|
||||
}
|
||||
|
||||
func (s *upgradeReplayRPCService) GetSaved(_ context.Context, _ domain.SavedStarGiftRef) (domain.SavedStarGift, bool, error) {
|
||||
return s.saved, true, nil
|
||||
}
|
||||
|
||||
func (s *upgradeReplayRPCService) UpgradeReceipt(_ context.Context, userID int64, _ string) (domain.StarGiftUpgradeReceipt, bool, error) {
|
||||
if userID != s.receipt.UserID {
|
||||
return domain.StarGiftUpgradeReceipt{}, false, nil
|
||||
}
|
||||
return s.receipt, true, nil
|
||||
}
|
||||
|
||||
func (s *upgradeReplayRPCService) CollectiblePreview(context.Context, int64) (domain.StarGiftUpgradePreview, bool, error) {
|
||||
s.previewCalls++
|
||||
return domain.StarGiftUpgradePreview{}, false, nil
|
||||
}
|
||||
|
||||
func (s *upgradeReplayRPCService) Upgrade(_ context.Context, req domain.StarGiftUpgradeRequest) (domain.StarGiftUpgradeResult, error) {
|
||||
s.upgradeCalls++
|
||||
s.lastRequest = req
|
||||
return s.result, nil
|
||||
}
|
||||
|
||||
func collectibleRPCAttribute(kind domain.StarGiftCollectibleAttributeKind, id int64, name string) domain.StarGiftCollectibleAttribute {
|
||||
attribute := domain.StarGiftCollectibleAttribute{Kind: kind, Name: name, RarityPermille: 1000}
|
||||
attribute := domain.StarGiftCollectibleAttribute{
|
||||
Kind: kind, Name: name, RarityKind: domain.StarGiftRarityPermille, RarityPermille: 1000,
|
||||
}
|
||||
if kind == domain.StarGiftCollectibleBackdrop {
|
||||
attribute.BackdropID = int(id)
|
||||
attribute.CenterColor = 0x112233
|
||||
|
|
@ -148,6 +310,120 @@ func TestSavedStarGiftProjectionCombinesHistoricalCatalogWithCurrentCollectibleA
|
|||
}
|
||||
}
|
||||
|
||||
func TestMessageStarGiftProjectionSeparatesPaidPriceFromPrepaidAmount(t *testing.T) {
|
||||
ordinary, ok := tgMessageActionStarGift(&domain.MessageStarGiftAction{
|
||||
GiftID: 8001, Stars: 50, ConvertStars: 25, CanUpgrade: true, UpgradePriceStars: 75,
|
||||
}).(*tg.MessageActionStarGift)
|
||||
if !ok {
|
||||
t.Fatalf("ordinary action = %T", ordinary)
|
||||
}
|
||||
ordinaryGift, ok := ordinary.Gift.(*tg.StarGift)
|
||||
if !ok {
|
||||
t.Fatalf("ordinary inner gift = %T", ordinary.Gift)
|
||||
}
|
||||
if price, set := ordinaryGift.GetUpgradeStars(); !set || price != 75 {
|
||||
t.Fatalf("ordinary inner upgrade_stars = %d set=%v, want paid price 75", price, set)
|
||||
}
|
||||
if amount, set := ordinary.GetUpgradeStars(); set || amount != 0 || ordinary.PrepaidUpgrade {
|
||||
t.Fatalf("ordinary outer upgrade_stars = %d set=%v prepaid=%v, want absent", amount, set, ordinary.PrepaidUpgrade)
|
||||
}
|
||||
|
||||
prepaid, ok := tgMessageActionStarGift(&domain.MessageStarGiftAction{
|
||||
GiftID: 8001, Stars: 50, ConvertStars: 25, CanUpgrade: true, PrepaidUpgrade: true,
|
||||
UpgradePriceStars: 75, UpgradeStars: 75,
|
||||
}).(*tg.MessageActionStarGift)
|
||||
if !ok {
|
||||
t.Fatalf("prepaid action = %T", prepaid)
|
||||
}
|
||||
if amount, set := prepaid.GetUpgradeStars(); !set || amount != 75 || !prepaid.PrepaidUpgrade {
|
||||
t.Fatalf("prepaid outer upgrade_stars = %d set=%v prepaid=%v, want 75", amount, set, prepaid.PrepaidUpgrade)
|
||||
}
|
||||
upgraded, ok := tgMessageActionStarGift(&domain.MessageStarGiftAction{
|
||||
GiftID: 8001, Stars: 50, ConvertStars: 25, UpgradeMsgID: 88,
|
||||
}).(*tg.MessageActionStarGift)
|
||||
if !ok {
|
||||
t.Fatalf("upgraded action = %T", upgraded)
|
||||
}
|
||||
if msgID, set := upgraded.GetUpgradeMsgID(); !set || msgID != 88 {
|
||||
t.Fatalf("upgrade_msg_id = %d set=%v, want 88", msgID, set)
|
||||
}
|
||||
for _, profile := range []tlprofile.Profile{tlprofile.Profile227, tlprofile.Profile228} {
|
||||
wire := &bin.Buffer{}
|
||||
if err := tlprofile.EncodeObject(profile, ordinary, wire); err != nil {
|
||||
t.Fatalf("encode Layer %d ordinary action: %v", profile, err)
|
||||
}
|
||||
decodedObject, err := tlprofile.DecodeObject(profile, &bin.Buffer{Buf: wire.Buf}, tlprofile.Limits{})
|
||||
if err != nil {
|
||||
t.Fatalf("decode Layer %d ordinary action: %v", profile, err)
|
||||
}
|
||||
decoded, ok := decodedObject.(*tg.MessageActionStarGift)
|
||||
if !ok {
|
||||
t.Fatalf("decode Layer %d action = %T", profile, decodedObject)
|
||||
}
|
||||
inner, ok := decoded.Gift.(*tg.StarGift)
|
||||
if !ok || inner.UpgradeStars != 75 || decoded.UpgradeStars != 0 || decoded.PrepaidUpgrade {
|
||||
t.Fatalf("Layer %d ordinary action lost paid/prepaid split: %#v", profile, decoded)
|
||||
}
|
||||
|
||||
upgradedWire := &bin.Buffer{}
|
||||
if err := tlprofile.EncodeObject(profile, upgraded, upgradedWire); err != nil {
|
||||
t.Fatalf("encode Layer %d upgraded action: %v", profile, err)
|
||||
}
|
||||
decodedUpgradedObject, err := tlprofile.DecodeObject(profile, &bin.Buffer{Buf: upgradedWire.Buf}, tlprofile.Limits{})
|
||||
if err != nil {
|
||||
t.Fatalf("decode Layer %d upgraded action: %v", profile, err)
|
||||
}
|
||||
decodedUpgraded, ok := decodedUpgradedObject.(*tg.MessageActionStarGift)
|
||||
if !ok || !decodedUpgraded.Upgraded || decodedUpgraded.UpgradeMsgID != 88 || decodedUpgraded.CanUpgrade {
|
||||
t.Fatalf("Layer %d upgraded action lost transition flags: %#v", profile, decodedUpgradedObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarGiftUpgradeRPCReplaysCommittedReceiptAfterTerminalTransition(t *testing.T) {
|
||||
r, sender, owner, gift := starGiftTestRouter(t)
|
||||
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
||||
saved := domain.SavedStarGift{
|
||||
ID: 47, Owner: ownerPeer, FromUserID: sender.ID, GiftID: gift.ID, RevisionID: gift.RevisionID,
|
||||
MsgID: 105, UniqueGiftID: 9200000000000004,
|
||||
}
|
||||
result := domain.StarGiftUpgradeResult{
|
||||
Saved: saved, Unique: domain.UniqueStarGift{ID: saved.UniqueGiftID, GiftID: gift.ID, Owner: ownerPeer},
|
||||
Balance: domain.StarsBalance{UserID: owner.ID, Balance: 1000}, Duplicate: true,
|
||||
Send: domain.SendPrivateTextResult{
|
||||
RecipientMessage: domain.Message{ID: 107, OwnerUserID: owner.ID, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: sender.ID}, From: domain.Peer{Type: domain.PeerTypeUser, ID: sender.ID}, Date: 1700000001},
|
||||
RecipientEvent: domain.UpdateEvent{UserID: owner.ID, Pts: 42, PtsCount: 1, Date: 1700000001},
|
||||
},
|
||||
}
|
||||
service := &upgradeReplayRPCService{GiftsService: r.deps.Gifts, saved: saved, result: result,
|
||||
receipt: domain.StarGiftUpgradeReceipt{UserID: owner.ID, SourceSavedGiftID: saved.ID,
|
||||
UniqueGiftID: saved.UniqueGiftID, RequirePrepaid: true, KeepOriginalDetails: true, BalanceAfter: 1000}}
|
||||
r.deps.Gifts = service
|
||||
ctx := WithUserID(context.Background(), owner.ID)
|
||||
if _, err := r.onPaymentsUpgradeStarGift(ctx, &tg.PaymentsUpgradeStarGiftRequest{
|
||||
KeepOriginalDetails: true, Stargift: &tg.InputSavedStarGiftUser{MsgID: saved.MsgID},
|
||||
}); err != nil {
|
||||
t.Fatalf("replay prepaid upgrade after terminal transition: %v", err)
|
||||
}
|
||||
if service.upgradeCalls != 1 || service.previewCalls != 0 || !service.lastRequest.RequirePrepaid || service.lastRequest.ChargeStars != 0 {
|
||||
t.Fatalf("prepaid replay calls=%d preview=%d req=%+v", service.upgradeCalls, service.previewCalls, service.lastRequest)
|
||||
}
|
||||
|
||||
const paidFormID int64 = -7611777087885039132
|
||||
service.receipt = domain.StarGiftUpgradeReceipt{UserID: owner.ID, SourceSavedGiftID: saved.ID,
|
||||
FormID: paidFormID, UniqueGiftID: saved.UniqueGiftID, ChargeStars: 25,
|
||||
KeepOriginalDetails: true, BalanceAfter: 975}
|
||||
service.upgradeCalls, service.previewCalls = 0, 0
|
||||
if _, err := r.sendStarGiftUpgradeForm(ctx, owner.ID, paidFormID, &tg.InputInvoiceStarGiftUpgrade{
|
||||
KeepOriginalDetails: true, Stargift: &tg.InputSavedStarGiftUser{MsgID: saved.MsgID},
|
||||
}); err != nil {
|
||||
t.Fatalf("replay paid upgrade after terminal transition: %v", err)
|
||||
}
|
||||
if service.upgradeCalls != 1 || service.previewCalls != 0 || service.lastRequest.ChargeStars != 25 || service.lastRequest.FormID != paidFormID {
|
||||
t.Fatalf("paid replay calls=%d preview=%d req=%+v", service.upgradeCalls, service.previewCalls, service.lastRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarGiftCollectiblePreviewUpgradeFormUniqueAndServiceProjection(t *testing.T) {
|
||||
r, sender, owner, gift := starGiftTestRouter(t)
|
||||
ctx := context.Background()
|
||||
|
|
@ -157,11 +433,15 @@ func TestStarGiftCollectiblePreviewUpgradeFormUniqueAndServiceProjection(t *test
|
|||
t.Fatalf("gift service = %T", r.deps.Gifts)
|
||||
}
|
||||
model := collectibleRPCAttribute(domain.StarGiftCollectibleModel, 8101, "Aurora")
|
||||
crafted := collectibleRPCAttribute(domain.StarGiftCollectibleModel, 8103, "Crafted Aurora")
|
||||
crafted.Crafted = true
|
||||
crafted.RarityKind = domain.StarGiftRarityLegendary
|
||||
crafted.RarityPermille = 0
|
||||
pattern := collectibleRPCAttribute(domain.StarGiftCollectiblePattern, 8102, "Orbit")
|
||||
backdrop := collectibleRPCAttribute(domain.StarGiftCollectibleBackdrop, 1, "Midnight")
|
||||
if _, err := giftService.PublishCollectibleRevision(ctx, domain.StarGiftCollectibleWrite{
|
||||
GiftID: gift.ID, UpgradeStars: 75, SupplyTotal: 500, SlugPrefix: "cake",
|
||||
Models: []domain.StarGiftCollectibleAttribute{model}, Patterns: []domain.StarGiftCollectibleAttribute{pattern},
|
||||
Models: []domain.StarGiftCollectibleAttribute{model, crafted}, Patterns: []domain.StarGiftCollectibleAttribute{pattern},
|
||||
Backdrops: []domain.StarGiftCollectibleAttribute{backdrop}, Actor: "test", CommandID: "collectible-rpc",
|
||||
}); err != nil {
|
||||
t.Fatalf("publish collectible pool: %v", err)
|
||||
|
|
@ -177,6 +457,17 @@ func TestStarGiftCollectiblePreviewUpgradeFormUniqueAndServiceProjection(t *test
|
|||
if err != nil || len(preview.SampleAttributes) != 3 {
|
||||
t.Fatalf("upgrade preview = %#v err %v", preview, err)
|
||||
}
|
||||
attributes, err := r.onPaymentsGetStarGiftUpgradeAttributes(ownerCtx, gift.ID)
|
||||
if err != nil || len(attributes.Attributes) != 4 {
|
||||
t.Fatalf("upgrade attributes = %#v err %v", attributes, err)
|
||||
}
|
||||
craftedTG, ok := attributes.Attributes[1].(*tg.StarGiftAttributeModel)
|
||||
if !ok || !craftedTG.Crafted {
|
||||
t.Fatalf("crafted attribute = %T %#v", attributes.Attributes[1], attributes.Attributes[1])
|
||||
}
|
||||
if _, ok := craftedTG.Rarity.(*tg.StarGiftAttributeRarityLegendary); !ok {
|
||||
t.Fatalf("crafted rarity = %T", craftedTG.Rarity)
|
||||
}
|
||||
invoice := &tg.InputInvoiceStarGiftUpgrade{Stargift: &tg.InputSavedStarGiftUser{MsgID: 444}}
|
||||
formClass, err := r.onPaymentsGetPaymentForm(ownerCtx, &tg.PaymentsGetPaymentFormRequest{Invoice: invoice})
|
||||
if err != nil {
|
||||
|
|
@ -208,7 +499,7 @@ func TestStarGiftCollectiblePreviewUpgradeFormUniqueAndServiceProjection(t *test
|
|||
message := domain.Message{Media: &domain.MessageMedia{Kind: domain.MessageMediaKindService, ServiceAction: &domain.MessageServiceAction{
|
||||
Kind: domain.MessageServiceActionStarGiftUnique,
|
||||
StarGiftUnique: &domain.MessageStarGiftUniqueAction{
|
||||
Gift: unique, FromUserID: sender.ID, Peer: unique.Owner, Upgrade: true, Saved: true,
|
||||
Gift: unique, FromUserID: sender.ID, Peer: unique.Owner, SavedID: 444, Upgrade: true, Saved: true,
|
||||
},
|
||||
}}}
|
||||
action, ok := tgMessageServiceAction(message).(*tg.MessageActionStarGiftUnique)
|
||||
|
|
@ -224,6 +515,9 @@ func TestStarGiftCollectiblePreviewUpgradeFormUniqueAndServiceProjection(t *test
|
|||
} else if user, ok := peer.(*tg.PeerUser); !ok || user.UserID != owner.ID {
|
||||
t.Fatalf("unique service action peer = %#v", peer)
|
||||
}
|
||||
if savedID, ok := action.GetSavedID(); !ok || savedID != 444 {
|
||||
t.Fatalf("unique service action saved_id = %d set=%v, want 444", savedID, ok)
|
||||
}
|
||||
for _, profile := range []tlprofile.Profile{tlprofile.Profile227, tlprofile.Profile228} {
|
||||
responseWire := &bin.Buffer{}
|
||||
if err := tlprofile.EncodeObject(profile, uniqueResponse, responseWire); err != nil {
|
||||
|
|
@ -254,7 +548,7 @@ func TestStarGiftCollectiblePreviewUpgradeFormUniqueAndServiceProjection(t *test
|
|||
if !ok {
|
||||
t.Fatalf("decode Layer %d unique action type = %T", profile, decodedActionObject)
|
||||
}
|
||||
if decodedActionGift, ok := decodedAction.Gift.(*tg.StarGiftUnique); !ok || !decodedAction.Upgrade || decodedActionGift.Slug != unique.Slug {
|
||||
if decodedActionGift, ok := decodedAction.Gift.(*tg.StarGiftUnique); !ok || !decodedAction.Upgrade || decodedAction.SavedID != 444 || decodedActionGift.Slug != unique.Slug {
|
||||
t.Fatalf("Layer %d unique action lost fields: %#v", profile, decodedAction)
|
||||
}
|
||||
}
|
||||
|
|
@ -556,10 +850,15 @@ func TestStarGiftChannelSaga(t *testing.T) {
|
|||
}); err != nil {
|
||||
t.Fatalf("publish channel collectible pool: %v", err)
|
||||
}
|
||||
if _, err := r.onPaymentsGetPaymentForm(senderCtx, &tg.PaymentsGetPaymentFormRequest{Invoice: &tg.InputInvoiceStarGift{
|
||||
upgradeFormRes, err := r.onPaymentsGetPaymentForm(senderCtx, &tg.PaymentsGetPaymentFormRequest{Invoice: &tg.InputInvoiceStarGift{
|
||||
Peer: channelPeer, GiftID: gift.ID, IncludeUpgrade: true,
|
||||
}}); err == nil {
|
||||
t.Fatal("channel include_upgrade must be rejected while channel upgrade is blocked")
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("getPaymentForm(channel include_upgrade): %v", err)
|
||||
}
|
||||
upgradeForm, ok := upgradeFormRes.(*tg.PaymentsPaymentFormStarGift)
|
||||
if !ok || len(upgradeForm.Invoice.Prices) != 1 || upgradeForm.Invoice.Prices[0].Amount != gift.Stars+75 {
|
||||
t.Fatalf("channel include_upgrade form = %T %+v, want total %d", upgradeFormRes, upgradeFormRes, gift.Stars+75)
|
||||
}
|
||||
inv := &tg.InputInvoiceStarGift{
|
||||
Peer: channelPeer,
|
||||
|
|
@ -612,8 +911,8 @@ func TestStarGiftChannelSaga(t *testing.T) {
|
|||
if savedRes.Count != 1 || len(savedRes.Gifts) != 1 {
|
||||
t.Fatalf("channel saved gifts = count %d len %d, want 1/1", savedRes.Count, len(savedRes.Gifts))
|
||||
}
|
||||
if savedRes.Gifts[0].CanUpgrade {
|
||||
t.Fatal("channel saved gift must not advertise upgrade while channel aggregate is blocked")
|
||||
if !savedRes.Gifts[0].CanUpgrade {
|
||||
t.Fatal("channel saved gift must advertise upgrade when a collectible pool is available")
|
||||
}
|
||||
savedID, ok := savedRes.Gifts[0].GetSavedID()
|
||||
if !ok || savedID <= 0 {
|
||||
|
|
@ -728,8 +1027,12 @@ func TestStarGiftInsufficientBalance(t *testing.T) {
|
|||
}, zaptest.NewLogger(t), clock.System)
|
||||
senderCtx := WithUserID(ctx, sender.ID)
|
||||
inv := &tg.InputInvoiceStarGift{Peer: &tg.InputPeerUser{UserID: recipient.ID, AccessHash: recipient.AccessHash}, GiftID: gift.ID}
|
||||
peer := domain.Peer{Type: domain.PeerTypeUser, ID: recipient.ID}
|
||||
if _, err := r.onPaymentsSendStarsForm(senderCtx, &tg.PaymentsSendStarsFormRequest{FormID: starGiftFormID(sender.ID, peer, gift), Invoice: inv}); err == nil {
|
||||
formRes, err := r.onPaymentsGetPaymentForm(senderCtx, &tg.PaymentsGetPaymentFormRequest{Invoice: inv})
|
||||
if err != nil {
|
||||
t.Fatalf("get expensive gift form: %v", err)
|
||||
}
|
||||
form := formRes.(*tg.PaymentsPaymentFormStarGift)
|
||||
if _, err := r.onPaymentsSendStarsForm(senderCtx, &tg.PaymentsSendStarsFormRequest{FormID: form.FormID, Invoice: inv}); !tgerr.Is(err, "BALANCE_TOO_LOW") {
|
||||
t.Fatalf("over-budget gift should error BALANCE_TOO_LOW")
|
||||
}
|
||||
// 余额未变。
|
||||
|
|
@ -738,22 +1041,60 @@ func TestStarGiftInsufficientBalance(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestStarGiftFormBindsCatalogRevisionAndPrice(t *testing.T) {
|
||||
func TestStarGiftPurchaseFormsAreFreshAndBindPurpose(t *testing.T) {
|
||||
r, sender, recipient, gift := starGiftTestRouter(t)
|
||||
ctx := WithUserID(context.Background(), sender.ID)
|
||||
peer := domain.Peer{Type: domain.PeerTypeUser, ID: recipient.ID}
|
||||
base := starGiftFormID(sender.ID, peer, gift)
|
||||
changedRevision := gift
|
||||
changedRevision.RevisionID++
|
||||
changedPrice := gift
|
||||
changedPrice.Stars++
|
||||
changedPeer := domain.Peer{Type: domain.PeerTypeUser, ID: recipient.ID + 1}
|
||||
if base == starGiftFormID(sender.ID, peer, changedRevision) || base == starGiftFormID(sender.ID, peer, changedPrice) || base == starGiftFormID(sender.ID, changedPeer, gift) {
|
||||
t.Fatal("star gift form id must bind revision, price and recipient")
|
||||
}
|
||||
inv := &tg.InputInvoiceStarGift{Peer: &tg.InputPeerUser{UserID: recipient.ID, AccessHash: recipient.AccessHash}, GiftID: gift.ID}
|
||||
if _, err := r.onPaymentsSendStarsForm(ctx, &tg.PaymentsSendStarsFormRequest{FormID: base + 1, Invoice: inv}); !tgerr.Is(err, "STARS_FORM_AMOUNT_MISMATCH") {
|
||||
t.Fatalf("bad form err=%v", err)
|
||||
firstRes, err := r.onPaymentsGetPaymentForm(ctx, &tg.PaymentsGetPaymentFormRequest{Invoice: inv})
|
||||
if err != nil {
|
||||
t.Fatalf("first form: %v", err)
|
||||
}
|
||||
secondRes, err := r.onPaymentsGetPaymentForm(ctx, &tg.PaymentsGetPaymentFormRequest{Invoice: inv})
|
||||
if err != nil {
|
||||
t.Fatalf("second form: %v", err)
|
||||
}
|
||||
first := firstRes.(*tg.PaymentsPaymentFormStarGift)
|
||||
second := secondRes.(*tg.PaymentsPaymentFormStarGift)
|
||||
if first.FormID == 0 || second.FormID == 0 || first.FormID == second.FormID {
|
||||
t.Fatalf("fresh form ids = %d/%d, want distinct non-zero TL longs", first.FormID, second.FormID)
|
||||
}
|
||||
if _, err := r.onPaymentsSendStarsForm(ctx, &tg.PaymentsSendStarsFormRequest{FormID: first.FormID + second.FormID, Invoice: inv}); !tgerr.Is(err, "FORM_EXPIRED") {
|
||||
t.Fatalf("unknown form err=%v, want FORM_EXPIRED", err)
|
||||
}
|
||||
tampered := *inv
|
||||
tampered.HideName = true
|
||||
if _, err := r.onPaymentsSendStarsForm(ctx, &tg.PaymentsSendStarsFormRequest{FormID: first.FormID, Invoice: &tampered}); !tgerr.Is(err, "PURPOSE_INVALID") {
|
||||
t.Fatalf("tampered form err=%v, want PURPOSE_INVALID", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarGiftCanPurchaseSameCatalogGiftTwice(t *testing.T) {
|
||||
r, sender, recipient, gift := starGiftTestRouter(t)
|
||||
ctx := WithUserID(context.Background(), sender.ID)
|
||||
inv := &tg.InputInvoiceStarGift{Peer: &tg.InputPeerUser{UserID: recipient.ID, AccessHash: recipient.AccessHash}, GiftID: gift.ID}
|
||||
var formIDs []int64
|
||||
for i := 0; i < 2; i++ {
|
||||
formRes, err := r.onPaymentsGetPaymentForm(ctx, &tg.PaymentsGetPaymentFormRequest{Invoice: inv})
|
||||
if err != nil {
|
||||
t.Fatalf("get form %d: %v", i, err)
|
||||
}
|
||||
form := formRes.(*tg.PaymentsPaymentFormStarGift)
|
||||
formIDs = append(formIDs, form.FormID)
|
||||
if _, err := r.onPaymentsSendStarsForm(ctx, &tg.PaymentsSendStarsFormRequest{FormID: form.FormID, Invoice: inv}); err != nil {
|
||||
t.Fatalf("purchase %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
if formIDs[0] == formIDs[1] {
|
||||
t.Fatalf("repeated purchase reused form id %d", formIDs[0])
|
||||
}
|
||||
saved, err := r.onPaymentsGetSavedStarGifts(WithUserID(context.Background(), recipient.ID), &tg.PaymentsGetSavedStarGiftsRequest{
|
||||
Peer: &tg.InputPeerSelf{}, Limit: 10,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("get recipient gifts: %v", err)
|
||||
}
|
||||
if saved.Count != 2 || len(saved.Gifts) != 2 {
|
||||
t.Fatalf("recipient gifts = count %d len %d, want two independent gifts", saved.Count, len(saved.Gifts))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -98,3 +98,113 @@ func TestOnPaymentsGetStarsStatusNilDeps(t *testing.T) {
|
|||
}
|
||||
_ = domain.DefaultStarsStartingGrant
|
||||
}
|
||||
|
||||
type channelLedgerGifts struct {
|
||||
GiftsService
|
||||
starsBalance int64
|
||||
tonBalance int64
|
||||
starsPage domain.StarsTransactionPage
|
||||
tonPage domain.TonTransactionPage
|
||||
}
|
||||
|
||||
func (s *channelLedgerGifts) ChannelStarsBalance(context.Context, int64) (int64, error) {
|
||||
return s.starsBalance, nil
|
||||
}
|
||||
|
||||
func (s *channelLedgerGifts) ChannelStarsTransactions(context.Context, int64, string, int) (domain.StarsTransactionPage, error) {
|
||||
return s.starsPage, nil
|
||||
}
|
||||
|
||||
func (s *channelLedgerGifts) ChannelTonBalance(context.Context, int64) (int64, error) {
|
||||
return s.tonBalance, nil
|
||||
}
|
||||
|
||||
func (s *channelLedgerGifts) ChannelTonTransactions(context.Context, int64, string, int) (domain.TonTransactionPage, error) {
|
||||
return s.tonPage, nil
|
||||
}
|
||||
|
||||
type channelLedgerChannels struct {
|
||||
ChannelsService
|
||||
view domain.ChannelView
|
||||
}
|
||||
|
||||
func (s *channelLedgerChannels) ResolveChannel(context.Context, int64, int64) (domain.ChannelView, error) {
|
||||
return s.view, nil
|
||||
}
|
||||
|
||||
func (s *channelLedgerChannels) GetChannels(context.Context, int64, []int64) ([]domain.ChannelView, error) {
|
||||
return []domain.ChannelView{s.view}, nil
|
||||
}
|
||||
|
||||
func TestPaymentsStarsLedgerUsesRequestedChannelOwner(t *testing.T) {
|
||||
const viewerID, channelID int64 = 1000000001, 2000000001
|
||||
view := domain.ChannelView{
|
||||
Channel: domain.Channel{ID: channelID, AccessHash: 9876, Title: "Gift Channel", Broadcast: true, CreatorUserID: viewerID},
|
||||
Self: domain.ChannelMember{ChannelID: channelID, UserID: viewerID, Role: domain.ChannelRoleCreator, Status: domain.ChannelMemberActive},
|
||||
}
|
||||
gifts := &channelLedgerGifts{
|
||||
starsBalance: 20,
|
||||
tonBalance: 900,
|
||||
starsPage: domain.StarsTransactionPage{Balance: 20, Transactions: []domain.StarsTransaction{{
|
||||
ID: 1, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 1000000002}, Amount: 20, Date: 10, Reason: domain.StarsReasonGift,
|
||||
}}},
|
||||
tonPage: domain.TonTransactionPage{Balance: 900, Transactions: []domain.TonTransaction{{
|
||||
ID: 2, Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: 2000000002}, GiftID: 9, Amount: 900, Date: 11, Reason: domain.StarsReasonGiftResale,
|
||||
}}},
|
||||
}
|
||||
r := New(Config{}, Deps{Gifts: gifts, Channels: &channelLedgerChannels{view: view}}, zaptest.NewLogger(t), clock.System)
|
||||
ctx := WithUserID(context.Background(), viewerID)
|
||||
peer := &tg.InputPeerChannel{ChannelID: channelID, AccessHash: view.Channel.AccessHash}
|
||||
|
||||
status, err := r.onPaymentsGetStarsStatus(ctx, &tg.PaymentsGetStarsStatusRequest{Peer: peer})
|
||||
if err != nil {
|
||||
t.Fatalf("get channel stars status: %v", err)
|
||||
}
|
||||
if amount, ok := status.Balance.(*tg.StarsAmount); !ok || amount.Amount != 20 || len(status.Chats) != 1 {
|
||||
t.Fatalf("channel stars status = %+v chats=%d", status.Balance, len(status.Chats))
|
||||
}
|
||||
revenue, err := r.onPaymentsGetStarsRevenueStats(ctx, &tg.PaymentsGetStarsRevenueStatsRequest{Peer: peer})
|
||||
if err != nil {
|
||||
t.Fatalf("get channel stars revenue: %v", err)
|
||||
}
|
||||
if current, ok := revenue.Status.CurrentBalance.(*tg.StarsAmount); !ok || current.Amount != 20 {
|
||||
t.Fatalf("channel stars revenue current = %+v", revenue.Status.CurrentBalance)
|
||||
}
|
||||
if overall, ok := revenue.Status.OverallRevenue.(*tg.StarsAmount); !ok || overall.Amount != 20 || revenue.Status.WithdrawalEnabled {
|
||||
t.Fatalf("channel stars revenue overall = %+v withdrawal=%v", revenue.Status.OverallRevenue, revenue.Status.WithdrawalEnabled)
|
||||
}
|
||||
|
||||
txnReq := &tg.PaymentsGetStarsTransactionsRequest{Peer: peer, Limit: 20}
|
||||
txnReq.SetTon(true)
|
||||
transactions, err := r.onPaymentsGetStarsTransactions(ctx, txnReq)
|
||||
if err != nil {
|
||||
t.Fatalf("get channel ton transactions: %v", err)
|
||||
}
|
||||
history, ok := transactions.GetHistory()
|
||||
if amount, amountOK := transactions.Balance.(*tg.StarsTonAmount); !amountOK || amount.Amount != 900 || !ok || len(history) != 1 || !history[0].StargiftResale {
|
||||
t.Fatalf("channel ton transactions = balance=%+v history=%+v", transactions.Balance, history)
|
||||
}
|
||||
revenueReq := &tg.PaymentsGetStarsRevenueStatsRequest{Peer: peer}
|
||||
revenueReq.SetTon(true)
|
||||
tonRevenue, err := r.onPaymentsGetStarsRevenueStats(ctx, revenueReq)
|
||||
if err != nil {
|
||||
t.Fatalf("get channel ton revenue: %v", err)
|
||||
}
|
||||
if current, ok := tonRevenue.Status.CurrentBalance.(*tg.StarsTonAmount); !ok || current.Amount != 900 {
|
||||
t.Fatalf("channel ton revenue current = %+v", tonRevenue.Status.CurrentBalance)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaymentsStarsLedgerRejectsNonAdminChannelReader(t *testing.T) {
|
||||
const viewerID, channelID int64 = 1000000001, 2000000001
|
||||
view := domain.ChannelView{
|
||||
Channel: domain.Channel{ID: channelID, AccessHash: 9876, Title: "Gift Channel", Broadcast: true},
|
||||
Self: domain.ChannelMember{ChannelID: channelID, UserID: viewerID, Role: domain.ChannelRoleMember, Status: domain.ChannelMemberActive},
|
||||
}
|
||||
r := New(Config{}, Deps{Gifts: &channelLedgerGifts{}, Channels: &channelLedgerChannels{view: view}}, zaptest.NewLogger(t), clock.System)
|
||||
ctx := WithUserID(context.Background(), viewerID)
|
||||
_, err := r.onPaymentsGetStarsStatus(ctx, &tg.PaymentsGetStarsStatusRequest{Peer: &tg.InputPeerChannel{ChannelID: channelID, AccessHash: view.Channel.AccessHash}})
|
||||
if err == nil {
|
||||
t.Fatal("non-admin channel ledger read unexpectedly succeeded")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue