feat: sync collectible star gifts
This commit is contained in:
parent
47fcf0ea41
commit
5ecf4e912d
64 changed files with 7559 additions and 403 deletions
|
|
@ -220,6 +220,25 @@ func tgMessageServiceAction(msg domain.Message) tg.MessageActionClass {
|
|||
}
|
||||
case domain.MessageServiceActionStarGift:
|
||||
return tgMessageActionStarGift(m.ServiceAction.StarGift)
|
||||
case domain.MessageServiceActionStarGiftUnique:
|
||||
action := m.ServiceAction.StarGiftUnique
|
||||
if action == nil {
|
||||
return &tg.MessageActionEmpty{}
|
||||
}
|
||||
out := &tg.MessageActionStarGiftUnique{
|
||||
Upgrade: action.Upgrade, Saved: action.Saved, PrepaidUpgrade: action.PrepaidUpgrade,
|
||||
Gift: tgUniqueStarGift(action.Gift),
|
||||
}
|
||||
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
|
||||
default:
|
||||
return &tg.MessageActionEmpty{}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -864,12 +864,27 @@ type GiftsService interface {
|
|||
Catalog(ctx context.Context) ([]domain.StarGift, error)
|
||||
CatalogHash(ctx context.Context) (int, error)
|
||||
GiftByID(ctx context.Context, id int64) (domain.StarGift, bool, error)
|
||||
GiftRevisionByID(ctx context.Context, revisionID int64) (domain.StarGift, bool, error)
|
||||
CollectiblePreview(ctx context.Context, giftID int64) (domain.StarGiftUpgradePreview, bool, error)
|
||||
CollectibleAvailability(ctx context.Context, giftIDs []int64) (map[int64]domain.StarGiftCollectibleAvailability, error)
|
||||
UniqueBySlug(ctx context.Context, slug string) (domain.UniqueStarGift, bool, error)
|
||||
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)
|
||||
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)
|
||||
GetSaved(ctx context.Context, ref domain.SavedStarGiftRef) (domain.SavedStarGift, bool, error)
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
// StarsService 抽象 Stars 本地账本(app/stars):余额查询、贷记/借记、流水分页。
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ func TestAccountGetWallPapersReturnsDefaultCatalog(t *testing.T) {
|
|||
t.Fatalf("boxed response type = %T, want *tg.AccountWallPapers", box.WallPapers)
|
||||
}
|
||||
if wallpapers.Hash == 0 || len(wallpapers.Wallpapers) == 0 {
|
||||
t.Fatalf("wallpapers = %+v, want stable Default catalog", wallpapers)
|
||||
t.Fatalf("wallpapers = %+v, want stable default catalog", wallpapers)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -189,7 +189,7 @@ func TestAccountWallpaperSeedLookupAndAckRPCs(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestPaymentsGetStarGiftCollectionsReturnsEmptyAndValidatesPeer(t *testing.T) {
|
||||
func TestPaymentsGetStarGiftCollectionsNoServiceFallbackAndValidatesPeer(t *testing.T) {
|
||||
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{}, zaptest.NewLogger(t), clock.System)
|
||||
ctx := WithUserID(context.Background(), 1000000001)
|
||||
|
||||
|
|
|
|||
|
|
@ -30,25 +30,21 @@ func (r *Router) registerPayments(d *tg.ServerDispatcher) {
|
|||
return tdesktop.StarGiftActiveAuctions(), nil
|
||||
})
|
||||
d.OnPaymentsGetStarGifts(r.onPaymentsGetStarGifts)
|
||||
d.OnPaymentsGetStarGiftUpgradePreview(r.onPaymentsGetStarGiftUpgradePreview)
|
||||
d.OnPaymentsGetUniqueStarGift(r.onPaymentsGetUniqueStarGift)
|
||||
d.OnPaymentsGetPaymentForm(r.onPaymentsGetPaymentForm)
|
||||
d.OnPaymentsSendStarsForm(r.onPaymentsSendStarsForm)
|
||||
d.OnPaymentsGetSavedStarGifts(r.onPaymentsGetSavedStarGifts)
|
||||
d.OnPaymentsGetSavedStarGift(r.onPaymentsGetSavedStarGift)
|
||||
d.OnPaymentsSaveStarGift(r.onPaymentsSaveStarGift)
|
||||
d.OnPaymentsConvertStarGift(r.onPaymentsConvertStarGift)
|
||||
d.OnPaymentsGetStarGiftCollections(func(ctx context.Context, req *tg.PaymentsGetStarGiftCollectionsRequest) (tg.PaymentsStarGiftCollectionsClass, 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.StarGiftCollections(), nil
|
||||
})
|
||||
d.OnPaymentsUpgradeStarGift(r.onPaymentsUpgradeStarGift)
|
||||
d.OnPaymentsGetStarGiftCollections(r.onPaymentsGetStarGiftCollections)
|
||||
d.OnPaymentsCreateStarGiftCollection(r.onPaymentsCreateStarGiftCollection)
|
||||
d.OnPaymentsUpdateStarGiftCollection(r.onPaymentsUpdateStarGiftCollection)
|
||||
d.OnPaymentsDeleteStarGiftCollection(r.onPaymentsDeleteStarGiftCollection)
|
||||
d.OnPaymentsReorderStarGiftCollections(r.onPaymentsReorderStarGiftCollections)
|
||||
d.OnPaymentsToggleStarGiftsPinnedToTop(r.onPaymentsToggleStarGiftsPinnedToTop)
|
||||
d.OnPaymentsGetStarsRevenueAdsAccountURL(func(ctx context.Context, peer tg.InputPeerClass) (*tg.PaymentsStarsRevenueAdsAccountURL, error) {
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
|
|
|
|||
235
internal/rpc/payments_star_gift_collections.go
Normal file
235
internal/rpc/payments_star_gift_collections.go
Normal file
|
|
@ -0,0 +1,235 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func (r *Router) onPaymentsGetStarGiftCollections(ctx context.Context, req *tg.PaymentsGetStarGiftCollectionsRequest) (tg.PaymentsStarGiftCollectionsClass, error) {
|
||||
if req == nil {
|
||||
return nil, inputRequestInvalidErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
owner, err := r.starGiftOwnerPeer(ctx, userID, req.Peer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.deps.Gifts == nil {
|
||||
return &tg.PaymentsStarGiftCollections{Collections: []tg.StarGiftCollection{}}, nil
|
||||
}
|
||||
collections, err := r.deps.Gifts.ListCollections(ctx, owner)
|
||||
if err != nil {
|
||||
return nil, starGiftCollectionErr(err)
|
||||
}
|
||||
if req.Hash != 0 && req.Hash == domain.StarGiftCollectionsHash(collections) {
|
||||
return &tg.PaymentsStarGiftCollectionsNotModified{}, nil
|
||||
}
|
||||
return &tg.PaymentsStarGiftCollections{Collections: tgStarGiftCollections(collections)}, nil
|
||||
}
|
||||
|
||||
func (r *Router) onPaymentsCreateStarGiftCollection(ctx context.Context, req *tg.PaymentsCreateStarGiftCollectionRequest) (*tg.StarGiftCollection, error) {
|
||||
if req == nil || r.deps.Gifts == nil {
|
||||
return nil, inputRequestInvalidErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
owner, err := r.starGiftOwnerPeer(ctx, userID, req.Peer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := r.ensureCanManageStarGiftOwner(ctx, userID, owner); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids, err := r.resolveStarGiftCollectionRefs(ctx, userID, owner, req.Stargift)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
collection, err := r.deps.Gifts.CreateCollection(ctx, owner, strings.TrimSpace(req.Title), ids)
|
||||
if err != nil {
|
||||
return nil, starGiftCollectionErr(err)
|
||||
}
|
||||
r.invalidateStarGiftOwnerProjection(owner)
|
||||
out := tgStarGiftCollection(collection)
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (r *Router) onPaymentsUpdateStarGiftCollection(ctx context.Context, req *tg.PaymentsUpdateStarGiftCollectionRequest) (*tg.StarGiftCollection, error) {
|
||||
if req == nil || req.CollectionID <= 0 || r.deps.Gifts == nil {
|
||||
return nil, inputRequestInvalidErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
owner, err := r.starGiftOwnerPeer(ctx, userID, req.Peer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := r.ensureCanManageStarGiftOwner(ctx, userID, owner); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
patch := domain.StarGiftCollectionPatch{}
|
||||
if title, ok := req.GetTitle(); ok {
|
||||
title = strings.TrimSpace(title)
|
||||
patch.Title = &title
|
||||
}
|
||||
if refs, ok := req.GetDeleteStargift(); ok {
|
||||
patch.DeleteIDs, err = r.resolveStarGiftCollectionRefs(ctx, userID, owner, refs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if refs, ok := req.GetAddStargift(); ok {
|
||||
patch.AddIDs, err = r.resolveStarGiftCollectionRefs(ctx, userID, owner, refs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if refs, ok := req.GetOrder(); ok {
|
||||
patch.Order, err = r.resolveStarGiftCollectionRefs(ctx, userID, owner, refs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
collection, err := r.deps.Gifts.UpdateCollection(ctx, owner, req.CollectionID, patch)
|
||||
if err != nil {
|
||||
return nil, starGiftCollectionErr(err)
|
||||
}
|
||||
r.invalidateStarGiftOwnerProjection(owner)
|
||||
out := tgStarGiftCollection(collection)
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (r *Router) onPaymentsDeleteStarGiftCollection(ctx context.Context, req *tg.PaymentsDeleteStarGiftCollectionRequest) (bool, error) {
|
||||
if req == nil || req.CollectionID <= 0 || r.deps.Gifts == nil {
|
||||
return false, inputRequestInvalidErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
owner, err := r.starGiftOwnerPeer(ctx, userID, req.Peer)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := r.ensureCanManageStarGiftOwner(ctx, userID, owner); err != nil {
|
||||
return false, err
|
||||
}
|
||||
deleted, err := r.deps.Gifts.DeleteCollection(ctx, owner, req.CollectionID)
|
||||
if err != nil {
|
||||
return false, starGiftCollectionErr(err)
|
||||
}
|
||||
if !deleted {
|
||||
return false, starGiftCollectionErr(domain.ErrStarGiftCollectionNotFound)
|
||||
}
|
||||
r.invalidateStarGiftOwnerProjection(owner)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *Router) onPaymentsReorderStarGiftCollections(ctx context.Context, req *tg.PaymentsReorderStarGiftCollectionsRequest) (bool, error) {
|
||||
if req == nil || r.deps.Gifts == nil {
|
||||
return false, inputRequestInvalidErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
owner, err := r.starGiftOwnerPeer(ctx, userID, req.Peer)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := r.ensureCanManageStarGiftOwner(ctx, userID, owner); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := r.deps.Gifts.ReorderCollections(ctx, owner, req.Order); err != nil {
|
||||
return false, starGiftCollectionErr(err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *Router) onPaymentsToggleStarGiftsPinnedToTop(ctx context.Context, req *tg.PaymentsToggleStarGiftsPinnedToTopRequest) (bool, error) {
|
||||
if req == nil || r.deps.Gifts == nil {
|
||||
return false, inputRequestInvalidErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return false, internalErr()
|
||||
}
|
||||
owner, err := r.starGiftOwnerPeer(ctx, userID, req.Peer)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := r.ensureCanManageStarGiftOwner(ctx, userID, owner); err != nil {
|
||||
return false, err
|
||||
}
|
||||
ids, err := r.resolveStarGiftCollectionRefs(ctx, userID, owner, req.Stargift)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := r.deps.Gifts.SetPinned(ctx, owner, ids); err != nil {
|
||||
return false, starGiftCollectionErr(err)
|
||||
}
|
||||
r.invalidateStarGiftOwnerProjection(owner)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (r *Router) resolveStarGiftCollectionRefs(ctx context.Context, userID int64, owner domain.Peer, refs []tg.InputSavedStarGiftClass) ([]int64, error) {
|
||||
if len(refs) > domain.MaxStarGiftCollectionItems {
|
||||
return nil, inputRequestInvalidErr()
|
||||
}
|
||||
domainRefs := make([]domain.SavedStarGiftRef, 0, len(refs))
|
||||
for _, input := range refs {
|
||||
ref, ok, err := r.starGiftRefFromInput(ctx, userID, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok || ref.Owner != owner {
|
||||
return nil, starGiftInvalidErr()
|
||||
}
|
||||
domainRefs = append(domainRefs, ref)
|
||||
}
|
||||
ids, err := r.deps.Gifts.ResolveSavedIDs(ctx, owner, domainRefs)
|
||||
if err != nil {
|
||||
return nil, starGiftCollectionErr(err)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func tgStarGiftCollections(in []domain.StarGiftCollection) []tg.StarGiftCollection {
|
||||
out := make([]tg.StarGiftCollection, 0, len(in))
|
||||
for _, collection := range in {
|
||||
out = append(out, tgStarGiftCollection(collection))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func tgStarGiftCollection(in domain.StarGiftCollection) tg.StarGiftCollection {
|
||||
return tg.StarGiftCollection{
|
||||
CollectionID: in.CollectionID,
|
||||
Title: in.Title,
|
||||
GiftsCount: len(in.GiftIDs),
|
||||
Hash: in.Hash,
|
||||
}
|
||||
}
|
||||
|
||||
func starGiftCollectionErr(err error) error {
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrStarGiftNotFound),
|
||||
errors.Is(err, domain.ErrStarGiftCollectibleInvalid),
|
||||
errors.Is(err, domain.ErrStarGiftCollectionNotFound),
|
||||
errors.Is(err, domain.ErrStarGiftCollectionsFull):
|
||||
return inputRequestInvalidErr()
|
||||
default:
|
||||
return internalErr()
|
||||
}
|
||||
}
|
||||
278
internal/rpc/payments_star_gift_unique.go
Normal file
278
internal/rpc/payments_star_gift_unique.go
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func (r *Router) starGiftUpgradePaymentForm(ctx context.Context, userID int64, inv *tg.InputInvoiceStarGiftUpgrade) (tg.PaymentsPaymentFormClass, error) {
|
||||
saved, preview, err := r.starGiftUpgradeTarget(ctx, userID, inv.Stargift)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &tg.PaymentsPaymentFormStarGift{
|
||||
FormID: starGiftUpgradeFormID(userID, saved.ID, preview.UpgradeStars, inv.KeepOriginalDetails),
|
||||
Invoice: tg.Invoice{
|
||||
Currency: "XTR",
|
||||
Prices: []tg.LabeledPrice{{Label: "Star gift upgrade", Amount: preview.UpgradeStars}},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wantFormID := starGiftUpgradeFormID(userID, saved.ID, preview.UpgradeStars, inv.KeepOriginalDetails)
|
||||
if formID == 0 || formID != wantFormID {
|
||||
return nil, starsFormAmountMismatchErr()
|
||||
}
|
||||
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),
|
||||
Date: int(r.clock.Now().Unix()), OriginAuthKeyID: rawAuthKeyIDForOrigin(ctx),
|
||||
OriginSessionID: sessionIDOrZero(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, starGiftUpgradeErr(err)
|
||||
}
|
||||
r.invalidateStarGiftOwnerProjection(saved.Owner)
|
||||
updates := r.tgStarGiftUpgradeUpdates(ctx, userID, result, true)
|
||||
return &tg.PaymentsPaymentResult{Updates: updates}, nil
|
||||
}
|
||||
|
||||
func (r *Router) onPaymentsUpgradeStarGift(ctx context.Context, req *tg.PaymentsUpgradeStarGiftRequest) (tg.UpdatesClass, error) {
|
||||
if req == nil || r.deps.Gifts == nil {
|
||||
return nil, inputRequestInvalidErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
saved, _, err := r.starGiftUpgradeTarget(ctx, userID, req.Stargift)
|
||||
if 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},
|
||||
KeepOriginalDetails: req.KeepOriginalDetails, RequirePrepaid: true,
|
||||
CommandKey: fmt.Sprintf("prepaid:%d:%t", saved.ID, req.KeepOriginalDetails),
|
||||
Date: int(r.clock.Now().Unix()), OriginAuthKeyID: rawAuthKeyIDForOrigin(ctx),
|
||||
OriginSessionID: sessionIDOrZero(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, starGiftUpgradeErr(err)
|
||||
}
|
||||
r.invalidateStarGiftOwnerProjection(saved.Owner)
|
||||
return r.tgStarGiftUpgradeUpdates(ctx, userID, result, false), nil
|
||||
}
|
||||
|
||||
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)
|
||||
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()
|
||||
}
|
||||
saved, found, err := r.deps.Gifts.GetSaved(ctx, ref)
|
||||
if err != nil {
|
||||
return domain.SavedStarGift{}, domain.StarGiftUpgradePreview{}, internalErr()
|
||||
}
|
||||
if !found || saved.Converted || saved.UniqueGiftID != 0 {
|
||||
return domain.SavedStarGift{}, domain.StarGiftUpgradePreview{}, starGiftInvalidErr()
|
||||
}
|
||||
preview, found, err := r.deps.Gifts.CollectiblePreview(ctx, saved.GiftID)
|
||||
if err != nil {
|
||||
return domain.SavedStarGift{}, domain.StarGiftUpgradePreview{}, internalErr()
|
||||
}
|
||||
if !found || preview.UpgradeStars <= 0 || preview.Issued >= preview.SupplyTotal {
|
||||
return domain.SavedStarGift{}, domain.StarGiftUpgradePreview{}, starGiftInvalidErr()
|
||||
}
|
||||
return saved, preview, nil
|
||||
}
|
||||
|
||||
func (r *Router) tgStarGiftUpgradeUpdates(ctx context.Context, ownerUserID int64, result domain.StarGiftUpgradeResult, includeBalance bool) *tg.Updates {
|
||||
message, event := result.Send.RecipientMessage, result.Send.RecipientEvent
|
||||
if result.Send.SenderMessage.OwnerUserID == ownerUserID {
|
||||
message, event = result.Send.SenderMessage, result.Send.SenderEvent
|
||||
}
|
||||
updates := tgPrivateMessageUpdates(event, message, 0, false,
|
||||
r.usersForMessageUpdate(ctx, ownerUserID, message),
|
||||
r.chatsForMessageUpdate(ctx, ownerUserID, message))
|
||||
if includeBalance {
|
||||
updates.Updates = append(updates.Updates, &tg.UpdateStarsBalance{Balance: &tg.StarsAmount{Amount: result.Balance.Balance}})
|
||||
}
|
||||
return updates
|
||||
}
|
||||
|
||||
func starGiftUpgradeFormID(userID, savedGiftID, stars int64, keepOriginal bool) int64 {
|
||||
id := userID*0x9e3779b1 ^ savedGiftID<<11 ^ stars<<19 ^ 0x55504752414445
|
||||
if keepOriginal {
|
||||
id ^= 0x4b454550
|
||||
}
|
||||
if id < 0 {
|
||||
id = ^id
|
||||
}
|
||||
if id == 0 {
|
||||
id = 1
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func starGiftUpgradeErr(err error) error {
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrStarsInsufficient):
|
||||
return starsErr(err)
|
||||
case errors.Is(err, domain.ErrStarGiftNotFound),
|
||||
errors.Is(err, domain.ErrStarGiftAlreadyConverted),
|
||||
errors.Is(err, domain.ErrStarGiftAlreadyUpgraded),
|
||||
errors.Is(err, domain.ErrStarGiftCollectibleUnavailable),
|
||||
errors.Is(err, domain.ErrStarGiftCollectibleSoldOut),
|
||||
errors.Is(err, domain.ErrStarGiftCollectibleInvalid):
|
||||
return starGiftInvalidErr()
|
||||
default:
|
||||
return internalErr()
|
||||
}
|
||||
}
|
||||
|
||||
func sessionIDOrZero(ctx context.Context) int64 {
|
||||
sessionID, _ := SessionIDFrom(ctx)
|
||||
return sessionID
|
||||
}
|
||||
|
||||
func (r *Router) onPaymentsGetStarGiftUpgradePreview(ctx context.Context, giftID int64) (*tg.PaymentsStarGiftUpgradePreview, 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 || preview.Issued >= preview.SupplyTotal {
|
||||
return nil, starGiftInvalidErr()
|
||||
}
|
||||
return &tg.PaymentsStarGiftUpgradePreview{
|
||||
SampleAttributes: tgStarGiftPreviewAttributes(preview),
|
||||
Prices: []tg.StarGiftUpgradePrice{},
|
||||
NextPrices: []tg.StarGiftUpgradePrice{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Router) onPaymentsGetUniqueStarGift(ctx context.Context, slug string) (*tg.PaymentsUniqueStarGift, error) {
|
||||
if r.deps.Gifts == nil || strings.TrimSpace(slug) == "" {
|
||||
return nil, starGiftInvalidErr()
|
||||
}
|
||||
viewerUserID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
unique, found, err := r.deps.Gifts.UniqueBySlug(ctx, slug)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if !found {
|
||||
return nil, starGiftInvalidErr()
|
||||
}
|
||||
out := &tg.PaymentsUniqueStarGift{
|
||||
Gift: tgUniqueStarGift(unique),
|
||||
Chats: []tg.ChatClass{},
|
||||
Users: []tg.UserClass{},
|
||||
}
|
||||
switch unique.Owner.Type {
|
||||
case domain.PeerTypeUser:
|
||||
ids := []int64{unique.Owner.ID}
|
||||
if unique.KeepOriginalDetails && !unique.OriginalNameHidden && unique.OriginalFromUserID != 0 && unique.OriginalFromUserID != unique.Owner.ID {
|
||||
ids = append(ids, unique.OriginalFromUserID)
|
||||
}
|
||||
out.Users = tgUsersForViewer(viewerUserID, r.domainUsersForIDs(ctx, viewerUserID, ids))
|
||||
case domain.PeerTypeChannel:
|
||||
out.Chats = r.tgChatsForChannelIDs(ctx, viewerUserID, []int64{unique.Owner.ID})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
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 {
|
||||
out = append(out, tgStarGiftAttribute(attribute))
|
||||
}
|
||||
for _, attribute := range preview.Patterns {
|
||||
out = append(out, tgStarGiftAttribute(attribute))
|
||||
}
|
||||
for _, attribute := range preview.Backdrops {
|
||||
out = append(out, tgStarGiftAttribute(attribute))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func tgStarGiftAttribute(attribute domain.StarGiftCollectibleAttribute) tg.StarGiftAttributeClass {
|
||||
rarity := &tg.StarGiftAttributeRarity{Permille: attribute.RarityPermille}
|
||||
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}
|
||||
case domain.StarGiftCollectiblePattern:
|
||||
document := tg.DocumentClass(&tg.DocumentEmpty{})
|
||||
if attribute.Document != nil {
|
||||
document = tgDocument(*attribute.Document)
|
||||
}
|
||||
return &tg.StarGiftAttributePattern{Name: attribute.Name, Document: document, Rarity: rarity}
|
||||
case domain.StarGiftCollectibleBackdrop:
|
||||
return &tg.StarGiftAttributeBackdrop{
|
||||
Name: attribute.Name, BackdropID: attribute.BackdropID,
|
||||
CenterColor: attribute.CenterColor, EdgeColor: attribute.EdgeColor,
|
||||
PatternColor: attribute.PatternColor, TextColor: attribute.TextColor, Rarity: rarity,
|
||||
}
|
||||
default:
|
||||
return &tg.StarGiftAttributeBackdrop{Name: attribute.Name, Rarity: rarity}
|
||||
}
|
||||
}
|
||||
|
||||
func tgUniqueStarGift(unique domain.UniqueStarGift) *tg.StarGiftUnique {
|
||||
attributes := []tg.StarGiftAttributeClass{
|
||||
tgStarGiftAttribute(unique.Model),
|
||||
tgStarGiftAttribute(unique.Pattern),
|
||||
tgStarGiftAttribute(unique.Backdrop),
|
||||
}
|
||||
if unique.KeepOriginalDetails && unique.OriginalOwner.ID != 0 {
|
||||
original := &tg.StarGiftAttributeOriginalDetails{
|
||||
RecipientID: tgPeer(unique.OriginalOwner),
|
||||
Date: unique.OriginalDate,
|
||||
}
|
||||
if unique.OriginalFromUserID != 0 && !unique.OriginalNameHidden {
|
||||
original.SetSenderID(&tg.PeerUser{UserID: unique.OriginalFromUserID})
|
||||
}
|
||||
if unique.OriginalMessage != "" {
|
||||
original.SetMessage(tg.TextWithEntities{Text: unique.OriginalMessage})
|
||||
}
|
||||
attributes = append(attributes, original)
|
||||
}
|
||||
out := &tg.StarGiftUnique{
|
||||
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 {
|
||||
out.SetOwnerID(owner)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
|
@ -77,6 +77,9 @@ func (r *Router) onPaymentsGetPaymentForm(ctx context.Context, req *tg.PaymentsG
|
|||
}
|
||||
return r.starsTopupPaymentForm(userID, purpose), nil
|
||||
}
|
||||
if inv, ok := req.Invoice.(*tg.InputInvoiceStarGiftUpgrade); ok {
|
||||
return r.starGiftUpgradePaymentForm(ctx, userID, inv)
|
||||
}
|
||||
|
||||
inv, ok := req.Invoice.(*tg.InputInvoiceStarGift)
|
||||
if !ok {
|
||||
|
|
@ -85,18 +88,32 @@ func (r *Router) onPaymentsGetPaymentForm(ctx context.Context, req *tg.PaymentsG
|
|||
if r.deps.Gifts == nil {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, inv.Peer); err != nil {
|
||||
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, inv.Peer)
|
||||
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
|
||||
}
|
||||
upgradeStars := int64(0)
|
||||
if inv.IncludeUpgrade {
|
||||
if gift.UpgradeStars <= 0 || gift.UpgradeIssued >= gift.UpgradeTotal {
|
||||
return nil, starGiftInvalidErr()
|
||||
}
|
||||
upgradeStars = gift.UpgradeStars
|
||||
}
|
||||
return &tg.PaymentsPaymentFormStarGift{
|
||||
FormID: starGiftFormID(userID, inv.GiftID),
|
||||
FormID: starGiftFormIDWithUpgrade(userID, peer, gift, inv.IncludeUpgrade),
|
||||
Invoice: tg.Invoice{
|
||||
Currency: "XTR",
|
||||
Prices: []tg.LabeledPrice{{Label: giftPriceLabel(gift), Amount: gift.Stars}},
|
||||
Prices: []tg.LabeledPrice{{Label: giftPriceLabel(gift), Amount: gift.Stars + upgradeStars}},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -119,6 +136,9 @@ func (r *Router) onPaymentsSendStarsForm(ctx context.Context, req *tg.PaymentsSe
|
|||
if inv, ok := req.Invoice.(*tg.InputInvoiceStars); ok {
|
||||
return r.sendStarsTopupForm(ctx, userID, req.FormID, inv)
|
||||
}
|
||||
if inv, ok := req.Invoice.(*tg.InputInvoiceStarGiftUpgrade); ok {
|
||||
return r.sendStarGiftUpgradeForm(ctx, userID, req.FormID, inv)
|
||||
}
|
||||
|
||||
inv, ok := req.Invoice.(*tg.InputInvoiceStarGift)
|
||||
if !ok {
|
||||
|
|
@ -131,6 +151,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()
|
||||
}
|
||||
|
|
@ -144,13 +167,24 @@ func (r *Router) onPaymentsSendStarsForm(ctx context.Context, req *tg.PaymentsSe
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
upgradeStars := int64(0)
|
||||
if inv.IncludeUpgrade {
|
||||
if gift.UpgradeStars <= 0 || gift.UpgradeIssued >= gift.UpgradeTotal {
|
||||
return nil, starGiftInvalidErr()
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
// 1. Debit 送礼人(不足→BALANCE_TOO_LOW)。
|
||||
balance, err := r.deps.Stars.Debit(ctx, userID, gift.Stars, domain.StarsReasonGift, peer, "Star gift", gift.Title)
|
||||
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)
|
||||
}
|
||||
|
|
@ -158,14 +192,14 @@ func (r *Router) onPaymentsSendStarsForm(ctx context.Context, req *tg.PaymentsSe
|
|||
var updates *tg.Updates
|
||||
switch peer.Type {
|
||||
case domain.PeerTypeUser:
|
||||
updates, err = r.sendStarGiftToUser(ctx, userID, peer.ID, gift, inv.HideName, giftMessage)
|
||||
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)
|
||||
default:
|
||||
err = domain.ErrStarGiftInvalid
|
||||
}
|
||||
if err != nil {
|
||||
r.refundStarGift(ctx, userID, peer, gift)
|
||||
r.refundStarGift(ctx, userID, peer, gift, purchaseStars)
|
||||
return nil, internalErr()
|
||||
}
|
||||
|
||||
|
|
@ -260,23 +294,25 @@ func (r *Router) sendStarsTopupForm(ctx context.Context, userID, formID int64, i
|
|||
return &tg.PaymentsPaymentResult{Updates: starsBalanceUpdates(balance.Balance, r.clock.Now().Unix())}, nil
|
||||
}
|
||||
|
||||
func (r *Router) sendStarGiftToUser(ctx context.Context, senderID, recipientID int64, gift domain.StarGift, hideName bool, message string) (*tg.Updates, error) {
|
||||
func (r *Router) sendStarGiftToUser(ctx context.Context, senderID, recipientID int64, gift domain.StarGift, hideName bool, message string, prepaidUpgradeStars int64) (*tg.Updates, error) {
|
||||
// 2. 投递礼物服务消息到收礼人私聊(双盒 + 推送)。
|
||||
send, err := r.deliverStarGift(ctx, senderID, recipientID, gift, hideName, message)
|
||||
send, err := r.deliverStarGift(ctx, senderID, recipientID, gift, hideName, message, prepaidUpgradeStars)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 3. 记账:收礼人收到一份礼物实例(msg_id = 收礼人侧消息 id)。
|
||||
if _, err := r.deps.Gifts.RecordSavedGift(ctx, domain.SavedStarGift{
|
||||
Owner: domain.Peer{Type: domain.PeerTypeUser, ID: recipientID},
|
||||
FromUserID: senderID,
|
||||
GiftID: gift.ID,
|
||||
MsgID: send.RecipientMessage.ID,
|
||||
Date: send.RecipientMessage.Date,
|
||||
NameHidden: hideName,
|
||||
Unsaved: false,
|
||||
ConvertStars: gift.ConvertStars,
|
||||
Message: message,
|
||||
Owner: domain.Peer{Type: domain.PeerTypeUser, ID: recipientID},
|
||||
FromUserID: senderID,
|
||||
GiftID: gift.ID,
|
||||
RevisionID: gift.RevisionID,
|
||||
MsgID: send.RecipientMessage.ID,
|
||||
Date: send.RecipientMessage.Date,
|
||||
NameHidden: hideName,
|
||||
Unsaved: false,
|
||||
ConvertStars: gift.ConvertStars,
|
||||
PrepaidUpgradeStars: prepaidUpgradeStars,
|
||||
Message: message,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -294,21 +330,25 @@ func (r *Router) sendStarGiftToChannel(ctx context.Context, senderID, channelID
|
|||
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,
|
||||
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,
|
||||
},
|
||||
}
|
||||
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,
|
||||
|
|
@ -335,7 +375,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) (domain.SendPrivateTextResult, error) {
|
||||
func (r *Router) deliverStarGift(ctx context.Context, senderID, recipientID int64, gift domain.StarGift, hideName bool, message string, prepaidUpgradeStars int64) (domain.SendPrivateTextResult, error) {
|
||||
recipientBlocked, err := r.peerBlocksUser(ctx, senderID, recipientID)
|
||||
if err != nil {
|
||||
return domain.SendPrivateTextResult{}, err
|
||||
|
|
@ -347,16 +387,19 @@ 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,
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
@ -374,8 +417,8 @@ func (r *Router) deliverStarGift(ctx context.Context, senderID, recipientID int6
|
|||
}
|
||||
|
||||
// refundStarGift 补偿退款(投递/记账失败时把已 Debit 的星退回)。
|
||||
func (r *Router) refundStarGift(ctx context.Context, userID int64, peer domain.Peer, gift domain.StarGift) {
|
||||
if _, err := r.deps.Stars.Credit(ctx, userID, gift.Stars, domain.StarsReasonGift, peer, "Star gift refund", gift.Title); err != nil {
|
||||
func (r *Router) refundStarGift(ctx context.Context, userID int64, peer domain.Peer, gift domain.StarGift, amount int64) {
|
||||
if _, err := r.deps.Stars.Credit(ctx, userID, amount, domain.StarsReasonGift, peer, "Star gift refund", gift.Title); err != nil {
|
||||
r.log.Error("star gift refund failed", zap.Int64("user_id", userID), zap.Int64("gift_id", gift.ID), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
|
@ -396,11 +439,27 @@ func (r *Router) onPaymentsGetSavedStarGifts(ctx context.Context, req *tg.Paymen
|
|||
if r.deps.Gifts == nil {
|
||||
return emptySavedStarGifts(), nil
|
||||
}
|
||||
page, err := r.deps.Gifts.ListSaved(ctx, owner, req.ExcludeUnsaved, req.Offset, req.Limit)
|
||||
collectionID, _ := req.GetCollectionID()
|
||||
page, err := r.deps.Gifts.ListSavedFiltered(ctx, domain.SavedStarGiftFilter{
|
||||
Owner: owner,
|
||||
ExcludeUnsaved: req.ExcludeUnsaved,
|
||||
ExcludeSaved: req.ExcludeSaved,
|
||||
ExcludeUnlimited: req.ExcludeUnlimited,
|
||||
ExcludeUnique: req.ExcludeUnique,
|
||||
ExcludeUpgradable: req.ExcludeUpgradable,
|
||||
ExcludeUnupgradable: req.ExcludeUnupgradable,
|
||||
CollectionID: collectionID,
|
||||
Offset: req.Offset,
|
||||
Limit: req.Limit,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
return r.tgSavedStarGiftsResponse(ctx, userID, page.Gifts, page.Count, page.NextOffset), nil
|
||||
response, err := r.tgSavedStarGiftsResponse(ctx, userID, page.Gifts, page.Count, page.NextOffset)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// onPaymentsGetSavedStarGift 按 InputSavedStarGift 引用取指定礼物。
|
||||
|
|
@ -429,7 +488,11 @@ func (r *Router) onPaymentsGetSavedStarGift(ctx context.Context, refs []tg.Input
|
|||
gifts = append(gifts, g)
|
||||
}
|
||||
}
|
||||
return r.tgSavedStarGiftsResponse(ctx, userID, gifts, len(gifts), ""), nil
|
||||
response, err := r.tgSavedStarGiftsResponse(ctx, userID, gifts, len(gifts), "")
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// onPaymentsSaveStarGift 切换礼物在资料的展示(unsave=true 隐藏)。
|
||||
|
|
@ -565,9 +628,15 @@ func (r *Router) starGiftRefFromInput(ctx context.Context, userID int64, ref tg.
|
|||
}
|
||||
|
||||
func (r *Router) ensureCanManageStarGiftOwner(ctx context.Context, userID int64, owner domain.Peer) error {
|
||||
if owner.Type != domain.PeerTypeChannel {
|
||||
if owner.Type == domain.PeerTypeUser {
|
||||
if owner.ID != userID {
|
||||
return peerIDInvalidErr()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if owner.Type != domain.PeerTypeChannel {
|
||||
return peerIDInvalidErr()
|
||||
}
|
||||
if r.deps.Channels == nil {
|
||||
return notImplementedErr()
|
||||
}
|
||||
|
|
@ -590,11 +659,41 @@ func (r *Router) invalidateStarGiftOwnerProjection(owner domain.Peer) {
|
|||
}
|
||||
}
|
||||
|
||||
func (r *Router) tgSavedStarGiftsResponse(ctx context.Context, viewerUserID int64, gifts []domain.SavedStarGift, count int, nextOffset string) *tg.PaymentsSavedStarGifts {
|
||||
catalog := r.resolveStarGiftCatalog(ctx, gifts)
|
||||
func (r *Router) tgSavedStarGiftsResponse(ctx context.Context, viewerUserID int64, gifts []domain.SavedStarGift, count int, nextOffset string) (*tg.PaymentsSavedStarGifts, error) {
|
||||
uniqueIDs := make([]int64, 0)
|
||||
seenUnique := make(map[int64]struct{})
|
||||
for _, gift := range gifts {
|
||||
if gift.UniqueGiftID != 0 {
|
||||
if _, seen := seenUnique[gift.UniqueGiftID]; !seen {
|
||||
seenUnique[gift.UniqueGiftID] = struct{}{}
|
||||
uniqueIDs = append(uniqueIDs, gift.UniqueGiftID)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(uniqueIDs) > 0 {
|
||||
uniques, err := r.deps.Gifts.UniqueByIDs(ctx, uniqueIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := range gifts {
|
||||
if unique, ok := uniques[gifts[i].UniqueGiftID]; ok {
|
||||
copy := unique
|
||||
gifts[i].Unique = ©
|
||||
}
|
||||
}
|
||||
}
|
||||
catalog, err := r.resolveStarGiftCatalog(ctx, gifts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
availability, err := r.resolveStarGiftCollectibleAvailability(ctx, gifts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
projected := tgSavedStarGifts(gifts, catalog, availability)
|
||||
out := &tg.PaymentsSavedStarGifts{
|
||||
Count: count,
|
||||
Gifts: tgSavedStarGifts(gifts, catalog),
|
||||
Gifts: projected,
|
||||
Chats: []tg.ChatClass{},
|
||||
}
|
||||
if ids := savedStarGiftUserIDs(gifts); len(ids) > 0 {
|
||||
|
|
@ -605,7 +704,35 @@ func (r *Router) tgSavedStarGiftsResponse(ctx context.Context, viewerUserID int6
|
|||
if nextOffset != "" {
|
||||
out.SetNextOffset(nextOffset)
|
||||
}
|
||||
return out
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (r *Router) resolveStarGiftCollectibleAvailability(ctx context.Context, gifts []domain.SavedStarGift) (map[int64]domain.StarGiftCollectibleAvailability, error) {
|
||||
out := make(map[int64]domain.StarGiftCollectibleAvailability)
|
||||
if r.deps.Gifts == nil {
|
||||
return out, nil
|
||||
}
|
||||
ids := make([]int64, 0, len(gifts))
|
||||
seen := make(map[int64]struct{}, len(gifts))
|
||||
for _, gift := range gifts {
|
||||
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
|
||||
}
|
||||
seen[gift.GiftID] = struct{}{}
|
||||
ids = append(ids, gift.GiftID)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return out, nil
|
||||
}
|
||||
return r.deps.Gifts.CollectibleAvailability(ctx, ids)
|
||||
}
|
||||
|
||||
func emptySavedStarGifts() *tg.PaymentsSavedStarGifts {
|
||||
|
|
@ -636,6 +763,9 @@ func tgStarGift(g domain.StarGift) *tg.StarGift {
|
|||
if g.Title != "" {
|
||||
gift.SetTitle(g.Title)
|
||||
}
|
||||
if g.UpgradeStars > 0 && g.UpgradeIssued < g.UpgradeTotal {
|
||||
gift.SetUpgradeStars(g.UpgradeStars)
|
||||
}
|
||||
return gift
|
||||
}
|
||||
|
||||
|
|
@ -667,6 +797,14 @@ func tgMessageActionStarGift(in *domain.MessageStarGiftAction) tg.MessageActionC
|
|||
if in.Converted {
|
||||
action.Converted = true
|
||||
}
|
||||
action.CanUpgrade = in.CanUpgrade
|
||||
action.PrepaidUpgrade = in.PrepaidUpgrade
|
||||
if in.UpgradeStars > 0 {
|
||||
action.SetUpgradeStars(in.UpgradeStars)
|
||||
}
|
||||
if in.UpgradeMsgID > 0 {
|
||||
action.SetUpgradeMsgID(in.UpgradeMsgID)
|
||||
}
|
||||
if in.ConvertStars > 0 {
|
||||
action.SetConvertStars(in.ConvertStars)
|
||||
}
|
||||
|
|
@ -687,30 +825,38 @@ func tgMessageActionStarGift(in *domain.MessageStarGiftAction) tg.MessageActionC
|
|||
return action
|
||||
}
|
||||
|
||||
// resolveStarGiftCatalog 解析这批 saved gift 涉及的目录项(giftID → StarGift),供下发完整贴纸/星价。
|
||||
func (r *Router) resolveStarGiftCatalog(ctx context.Context, gifts []domain.SavedStarGift) map[int64]domain.StarGift {
|
||||
// resolveStarGiftCatalog 解析这批 saved gift 涉及的不可变目录版本(revisionID → StarGift)。
|
||||
func (r *Router) resolveStarGiftCatalog(ctx context.Context, gifts []domain.SavedStarGift) (map[int64]domain.StarGift, error) {
|
||||
out := make(map[int64]domain.StarGift, len(gifts))
|
||||
if r.deps.Gifts == nil {
|
||||
return out
|
||||
return out, nil
|
||||
}
|
||||
for _, g := range gifts {
|
||||
if _, ok := out[g.GiftID]; ok {
|
||||
if g.RevisionID == 0 {
|
||||
return nil, domain.ErrStarGiftInvalid
|
||||
}
|
||||
if _, ok := out[g.RevisionID]; ok {
|
||||
continue
|
||||
}
|
||||
if gift, found, err := r.deps.Gifts.GiftByID(ctx, g.GiftID); err == nil && found {
|
||||
out[g.GiftID] = gift
|
||||
gift, found, err := r.deps.Gifts.GiftRevisionByID(ctx, g.RevisionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !found {
|
||||
return nil, domain.ErrStarGiftInvalid
|
||||
}
|
||||
out[g.RevisionID] = gift
|
||||
}
|
||||
return out
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// tgSavedStarGifts 把已收到礼物实例投影为 []tg.SavedStarGift。
|
||||
func tgSavedStarGifts(gifts []domain.SavedStarGift, catalog map[int64]domain.StarGift) []tg.SavedStarGift {
|
||||
func tgSavedStarGifts(gifts []domain.SavedStarGift, catalog map[int64]domain.StarGift, availability map[int64]domain.StarGiftCollectibleAvailability) []tg.SavedStarGift {
|
||||
out := make([]tg.SavedStarGift, 0, len(gifts))
|
||||
for _, g := range gifts {
|
||||
item := tg.SavedStarGift{
|
||||
Date: g.Date,
|
||||
Gift: tgSavedStarGiftGift(g, catalog),
|
||||
Gift: tgSavedStarGiftGift(g, catalog, availability),
|
||||
}
|
||||
if g.NameHidden {
|
||||
item.NameHidden = true
|
||||
|
|
@ -733,19 +879,47 @@ func tgSavedStarGifts(gifts []domain.SavedStarGift, catalog map[int64]domain.Sta
|
|||
if g.Message != "" {
|
||||
item.SetMessage(tg.TextWithEntities{Text: g.Message})
|
||||
}
|
||||
if g.UniqueGiftID == 0 {
|
||||
current, available := availability[g.GiftID]
|
||||
canIssue := available && current.UpgradeStars > 0 && current.Issued < current.SupplyTotal
|
||||
if canIssue {
|
||||
item.CanUpgrade = true
|
||||
}
|
||||
if g.PrepaidUpgradeStars > 0 && canIssue {
|
||||
item.SetUpgradeStars(g.PrepaidUpgradeStars)
|
||||
item.CanUpgrade = true
|
||||
}
|
||||
}
|
||||
if g.PinnedOrder > 0 {
|
||||
item.PinnedToTop = true
|
||||
}
|
||||
if len(g.CollectionIDs) > 0 {
|
||||
item.SetCollectionID(append([]int(nil), g.CollectionIDs...))
|
||||
}
|
||||
if g.Unique != nil {
|
||||
item.SetGiftNum(g.Unique.Num)
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// tgSavedStarGiftGift 把 SavedStarGift 内嵌礼物投影为 tg.StarGift:优先用目录解析出完整贴纸/星价
|
||||
// (客户端据有效 sticker 渲染),目录缺失(礼物已下架)时兜底最小形态。convert_stars 用实例值。
|
||||
func tgSavedStarGiftGift(g domain.SavedStarGift, catalog map[int64]domain.StarGift) tg.StarGiftClass {
|
||||
if gift, ok := catalog[g.GiftID]; ok {
|
||||
// tgSavedStarGiftGift 按收到时的不可变 revision 投影,目录停用或后续改版不影响历史显示。
|
||||
func tgSavedStarGiftGift(g domain.SavedStarGift, catalog map[int64]domain.StarGift, availability map[int64]domain.StarGiftCollectibleAvailability) tg.StarGiftClass {
|
||||
if g.Unique != nil {
|
||||
return tgUniqueStarGift(*g.Unique)
|
||||
}
|
||||
if gift, ok := catalog[g.RevisionID]; ok {
|
||||
if current, ok := availability[g.GiftID]; ok {
|
||||
gift.UpgradeStars = current.UpgradeStars
|
||||
gift.UpgradeTotal = current.SupplyTotal
|
||||
gift.UpgradeIssued = current.Issued
|
||||
}
|
||||
out := tgStarGift(gift)
|
||||
out.ConvertStars = g.ConvertStars
|
||||
return out
|
||||
}
|
||||
// resolveStarGiftCatalog 在进入投影前保证每个 revision 都存在;该分支仅保留类型完备性。
|
||||
return &tg.StarGift{
|
||||
ID: g.GiftID,
|
||||
Sticker: &tg.DocumentEmpty{},
|
||||
|
|
@ -770,8 +944,18 @@ func savedStarGiftUserIDs(gifts []domain.SavedStarGift) []int64 {
|
|||
return ids
|
||||
}
|
||||
|
||||
func starGiftFormID(userID, giftID int64) int64 {
|
||||
id := userID*0x9e3779b1 ^ (giftID << 7) ^ 0x5347494654
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/iamxvbaba/td/bin"
|
||||
"github.com/iamxvbaba/td/clock"
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
"github.com/iamxvbaba/td/tgerr"
|
||||
|
|
@ -18,12 +19,6 @@ import (
|
|||
"telesrv/internal/store/memory"
|
||||
)
|
||||
|
||||
type stubGiftCatalog struct{ gifts []domain.StarGift }
|
||||
|
||||
func (s stubGiftCatalog) BuildStarGiftCatalog(_ context.Context) ([]domain.StarGift, error) {
|
||||
return s.gifts, nil
|
||||
}
|
||||
|
||||
func starGiftTestRouter(t *testing.T) (*Router, domain.User, domain.User, domain.StarGift) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
|
@ -40,10 +35,12 @@ func starGiftTestRouter(t *testing.T) (*Router, domain.User, domain.User, domain
|
|||
t.Fatalf("create recipient: %v", err)
|
||||
}
|
||||
gift := domain.StarGift{
|
||||
ID: 8001, Stars: 50, ConvertStars: 50, Title: "Cake",
|
||||
Sticker: domain.Document{ID: 700, AccessHash: 7, DCID: 2, MimeType: "image/webp"},
|
||||
ID: 8001, RevisionID: 9001, Stars: 50, ConvertStars: 50, Title: "Cake",
|
||||
Sticker: domain.Document{ID: 700, AccessHash: 7, DCID: 2, MimeType: "application/x-tgsticker", Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}}},
|
||||
}
|
||||
gifts := appstargifts.NewService(memory.NewStarGiftStore(), stubGiftCatalog{[]domain.StarGift{gift}})
|
||||
giftStore := memory.NewStarGiftStore()
|
||||
giftStore.SeedCatalog([]domain.StarGift{gift})
|
||||
gifts := appstargifts.NewService(giftStore, nil, 2)
|
||||
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
|
||||
Users: appusers.NewService(users),
|
||||
Messages: appmessages.NewService(msgStore, dialogs),
|
||||
|
|
@ -54,6 +51,328 @@ func starGiftTestRouter(t *testing.T) (*Router, domain.User, domain.User, domain
|
|||
return r, sender, recipient, gift
|
||||
}
|
||||
|
||||
type uniqueGiftRPCService struct {
|
||||
GiftsService
|
||||
unique domain.UniqueStarGift
|
||||
}
|
||||
|
||||
func (s *uniqueGiftRPCService) UniqueBySlug(_ context.Context, slug string) (domain.UniqueStarGift, bool, error) {
|
||||
return s.unique, slug == s.unique.Slug, nil
|
||||
}
|
||||
|
||||
func collectibleRPCAttribute(kind domain.StarGiftCollectibleAttributeKind, id int64, name string) domain.StarGiftCollectibleAttribute {
|
||||
attribute := domain.StarGiftCollectibleAttribute{Kind: kind, Name: name, RarityPermille: 1000}
|
||||
if kind == domain.StarGiftCollectibleBackdrop {
|
||||
attribute.BackdropID = int(id)
|
||||
attribute.CenterColor = 0x112233
|
||||
attribute.EdgeColor = 0x223344
|
||||
attribute.PatternColor = 0x334455
|
||||
attribute.TextColor = 0xffffff
|
||||
return attribute
|
||||
}
|
||||
attribute.Document = &domain.Document{
|
||||
ID: id, AccessHash: id + 1, FileReference: []byte("collectible-rpc"), Date: 1700000000,
|
||||
MimeType: "application/x-tgsticker", Size: 3, DCID: 2,
|
||||
Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}, {Kind: domain.DocAttrFilename, FileName: "gift.tgs"}},
|
||||
}
|
||||
attribute.Animation = &domain.StarGiftAnimation{
|
||||
SourceName: "gift.tgs", SourceFormat: domain.StarGiftAnimationTGS,
|
||||
JSON: []byte(`{"v":"5.7"}`), TGS: []byte("tgs"), SHA256: make([]byte, 32), Width: 512, Height: 512,
|
||||
}
|
||||
attribute.Blob = &domain.FileBlob{LocationKey: "doc:test", Backend: domain.MediaBackendLocalFS, ObjectKey: "test", Size: 3}
|
||||
return attribute
|
||||
}
|
||||
|
||||
func TestSavedStarGiftProjectionCombinesHistoricalCatalogWithCurrentCollectibleAvailability(t *testing.T) {
|
||||
historical := domain.StarGift{
|
||||
ID: 8001, RevisionID: 9001, Stars: 50, ConvertStars: 25, Title: "Historical Cake",
|
||||
Sticker: domain.Document{ID: 700, AccessHash: 7, DCID: 2, MimeType: "application/x-tgsticker"},
|
||||
}
|
||||
saved := domain.SavedStarGift{GiftID: historical.ID, RevisionID: historical.RevisionID, MsgID: 44, Date: 100, ConvertStars: historical.ConvertStars}
|
||||
availability := map[int64]domain.StarGiftCollectibleAvailability{
|
||||
historical.ID: {UpgradeStars: 75, SupplyTotal: 500, Issued: 12},
|
||||
}
|
||||
|
||||
projected := tgSavedStarGifts([]domain.SavedStarGift{saved}, map[int64]domain.StarGift{historical.RevisionID: historical}, availability)
|
||||
if len(projected) != 1 || !projected[0].CanUpgrade {
|
||||
t.Fatalf("saved gift = %#v, want current pool to make historical gift upgradable", projected)
|
||||
}
|
||||
gift, ok := projected[0].Gift.(*tg.StarGift)
|
||||
if !ok {
|
||||
t.Fatalf("saved gift inner = %T, want *tg.StarGift", projected[0].Gift)
|
||||
}
|
||||
if gift.Title != historical.Title || gift.Stars != historical.Stars || gift.ConvertStars != historical.ConvertStars {
|
||||
t.Fatalf("historical snapshot changed: %#v", gift)
|
||||
}
|
||||
if upgradeStars, ok := gift.GetUpgradeStars(); !ok || upgradeStars != 75 {
|
||||
t.Fatalf("upgrade_stars = %d ok=%v, want current price 75", upgradeStars, ok)
|
||||
}
|
||||
for _, profile := range []tg.LayerProfile{tg.LayerProfile227, tg.LayerProfile228} {
|
||||
wire := &tg.PaymentsSavedStarGifts{Count: 1, Gifts: projected, Chats: []tg.ChatClass{}, Users: []tg.UserClass{}}
|
||||
encoded := &bin.Buffer{}
|
||||
if err := tg.EncodeLayer(profile, tg.LayerConstructorPaymentsSavedStarGiftsType(), wire, encoded); err != nil {
|
||||
t.Fatalf("encode Layer %d saved gift: %v", profile, err)
|
||||
}
|
||||
decoded, err := tg.DecodeLayer(profile, tg.LayerConstructorPaymentsSavedStarGiftsType(), &bin.Buffer{Buf: encoded.Buf})
|
||||
if err != nil {
|
||||
t.Fatalf("decode Layer %d saved gift: %v", profile, err)
|
||||
}
|
||||
inner, ok := decoded.Gifts[0].Gift.(*tg.StarGift)
|
||||
if !ok || !decoded.Gifts[0].CanUpgrade || inner.UpgradeStars != 75 {
|
||||
t.Fatalf("Layer %d projection lost upgrade flags: %#v", profile, decoded.Gifts[0])
|
||||
}
|
||||
}
|
||||
|
||||
availability[historical.ID] = domain.StarGiftCollectibleAvailability{UpgradeStars: 75, SupplyTotal: 500, Issued: 500}
|
||||
soldOut := tgSavedStarGifts([]domain.SavedStarGift{saved}, map[int64]domain.StarGift{historical.RevisionID: historical}, availability)[0]
|
||||
if soldOut.CanUpgrade {
|
||||
t.Fatal("sold-out collectible pool must not advertise upgrade")
|
||||
}
|
||||
if gift, ok := soldOut.Gift.(*tg.StarGift); !ok {
|
||||
t.Fatalf("sold-out inner = %T, want *tg.StarGift", soldOut.Gift)
|
||||
} else if _, ok := gift.GetUpgradeStars(); ok {
|
||||
t.Fatal("sold-out catalog projection must not expose upgrade_stars")
|
||||
}
|
||||
saved.PrepaidUpgradeStars = 75
|
||||
soldOutPrepaid := tgSavedStarGifts([]domain.SavedStarGift{saved}, map[int64]domain.StarGift{historical.RevisionID: historical}, availability)[0]
|
||||
if soldOutPrepaid.CanUpgrade {
|
||||
t.Fatal("sold-out prepaid gift must not advertise an upgrade the aggregate will reject")
|
||||
}
|
||||
if _, ok := soldOutPrepaid.GetUpgradeStars(); ok {
|
||||
t.Fatal("sold-out prepaid gift must not expose stale prepaid upgrade_stars")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarGiftCollectiblePreviewUpgradeFormUniqueAndServiceProjection(t *testing.T) {
|
||||
r, sender, owner, gift := starGiftTestRouter(t)
|
||||
ctx := context.Background()
|
||||
ownerCtx := WithUserID(ctx, owner.ID)
|
||||
giftService, ok := r.deps.Gifts.(*appstargifts.Service)
|
||||
if !ok {
|
||||
t.Fatalf("gift service = %T", r.deps.Gifts)
|
||||
}
|
||||
model := collectibleRPCAttribute(domain.StarGiftCollectibleModel, 8101, "Aurora")
|
||||
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},
|
||||
Backdrops: []domain.StarGiftCollectibleAttribute{backdrop}, Actor: "test", CommandID: "collectible-rpc",
|
||||
}); err != nil {
|
||||
t.Fatalf("publish collectible pool: %v", err)
|
||||
}
|
||||
if _, err := r.deps.Gifts.RecordSavedGift(ctx, domain.SavedStarGift{
|
||||
Owner: domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}, FromUserID: sender.ID,
|
||||
GiftID: gift.ID, RevisionID: gift.RevisionID, MsgID: 444, Date: 1700000000, ConvertStars: gift.ConvertStars,
|
||||
}); err != nil {
|
||||
t.Fatalf("record upgrade target: %v", err)
|
||||
}
|
||||
|
||||
preview, err := r.onPaymentsGetStarGiftUpgradePreview(ownerCtx, gift.ID)
|
||||
if err != nil || len(preview.SampleAttributes) != 3 {
|
||||
t.Fatalf("upgrade preview = %#v err %v", preview, err)
|
||||
}
|
||||
invoice := &tg.InputInvoiceStarGiftUpgrade{Stargift: &tg.InputSavedStarGiftUser{MsgID: 444}}
|
||||
formClass, err := r.onPaymentsGetPaymentForm(ownerCtx, &tg.PaymentsGetPaymentFormRequest{Invoice: invoice})
|
||||
if err != nil {
|
||||
t.Fatalf("get upgrade payment form: %v", err)
|
||||
}
|
||||
form, ok := formClass.(*tg.PaymentsPaymentFormStarGift)
|
||||
if !ok || form.FormID == 0 || form.Invoice.Currency != "XTR" || len(form.Invoice.Prices) != 1 || form.Invoice.Prices[0].Amount != 75 {
|
||||
t.Fatalf("upgrade payment form = %T %#v", formClass, formClass)
|
||||
}
|
||||
|
||||
unique := domain.UniqueStarGift{
|
||||
ID: 9200000000000001, GiftID: gift.ID, Title: gift.Title, Slug: "cake-1", Num: 1,
|
||||
Owner: domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID},
|
||||
Model: model, Pattern: pattern, Backdrop: backdrop,
|
||||
AvailabilityIssued: 1, AvailabilityTotal: 500, KeepOriginalDetails: true,
|
||||
OriginalFromUserID: sender.ID, OriginalOwner: domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID},
|
||||
OriginalDate: 1700000000, OriginalMessage: "hello",
|
||||
}
|
||||
r.deps.Gifts = &uniqueGiftRPCService{GiftsService: r.deps.Gifts, unique: unique}
|
||||
uniqueResponse, err := r.onPaymentsGetUniqueStarGift(WithUserID(ctx, sender.ID), unique.Slug)
|
||||
if err != nil {
|
||||
t.Fatalf("get unique gift = %#v err %v", uniqueResponse, err)
|
||||
}
|
||||
uniqueGift, ok := uniqueResponse.Gift.(*tg.StarGiftUnique)
|
||||
if !ok || uniqueGift.Slug != unique.Slug || len(uniqueGift.Attributes) != 4 || len(uniqueResponse.Users) != 2 {
|
||||
t.Fatalf("get unique gift = %#v", uniqueResponse)
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
}}}
|
||||
action, ok := tgMessageServiceAction(message).(*tg.MessageActionStarGiftUnique)
|
||||
if !ok {
|
||||
t.Fatalf("unique service action type = %T", tgMessageServiceAction(message))
|
||||
}
|
||||
projectedGift, giftOK := action.Gift.(*tg.StarGiftUnique)
|
||||
if !giftOK || !action.Upgrade || !action.Saved || projectedGift.ID != unique.ID || projectedGift.Slug != unique.Slug {
|
||||
t.Fatalf("unique service action = %#v", tgMessageServiceAction(message))
|
||||
}
|
||||
if peer, ok := action.GetPeer(); !ok {
|
||||
t.Fatal("unique service action missing owner peer")
|
||||
} else if user, ok := peer.(*tg.PeerUser); !ok || user.UserID != owner.ID {
|
||||
t.Fatalf("unique service action peer = %#v", peer)
|
||||
}
|
||||
for _, profile := range []tg.LayerProfile{tg.LayerProfile227, tg.LayerProfile228} {
|
||||
responseWire := &bin.Buffer{}
|
||||
if err := tg.EncodeLayer(profile, tg.LayerConstructorPaymentsUniqueStarGiftType(), uniqueResponse, responseWire); err != nil {
|
||||
t.Fatalf("encode Layer %d unique response: %v", profile, err)
|
||||
}
|
||||
decodedResponse, err := tg.DecodeLayer(profile, tg.LayerConstructorPaymentsUniqueStarGiftType(), &bin.Buffer{Buf: responseWire.Buf})
|
||||
if err != nil {
|
||||
t.Fatalf("decode Layer %d unique response: %v", profile, err)
|
||||
}
|
||||
decodedGift, ok := decodedResponse.Gift.(*tg.StarGiftUnique)
|
||||
if !ok || decodedGift.Slug != unique.Slug || len(decodedGift.Attributes) != 4 {
|
||||
t.Fatalf("Layer %d unique response lost fields: %#v", profile, decodedResponse.Gift)
|
||||
}
|
||||
|
||||
actionWire := &bin.Buffer{}
|
||||
if err := tg.EncodeLayer(profile, tg.LayerConstructorMessageActionStarGiftUniqueType(), action, actionWire); err != nil {
|
||||
t.Fatalf("encode Layer %d unique action: %v", profile, err)
|
||||
}
|
||||
decodedAction, err := tg.DecodeLayer(profile, tg.LayerConstructorMessageActionStarGiftUniqueType(), &bin.Buffer{Buf: actionWire.Buf})
|
||||
if err != nil {
|
||||
t.Fatalf("decode Layer %d unique action: %v", profile, err)
|
||||
}
|
||||
if decodedActionGift, ok := decodedAction.Gift.(*tg.StarGiftUnique); !ok || !decodedAction.Upgrade || decodedActionGift.Slug != unique.Slug {
|
||||
t.Fatalf("Layer %d unique action lost fields: %#v", profile, decodedAction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarGiftCollectionsCRUDFilterOrderAndPin(t *testing.T) {
|
||||
r, _, owner, gift := starGiftTestRouter(t)
|
||||
ctx := WithUserID(context.Background(), owner.ID)
|
||||
ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID}
|
||||
for _, msgID := range []int{101, 102} {
|
||||
if _, err := r.deps.Gifts.RecordSavedGift(context.Background(), domain.SavedStarGift{
|
||||
Owner: ownerPeer, GiftID: gift.ID, RevisionID: gift.RevisionID,
|
||||
MsgID: msgID, Date: 1700000000 + msgID, ConvertStars: gift.ConvertStars,
|
||||
}); err != nil {
|
||||
t.Fatalf("record saved gift %d: %v", msgID, err)
|
||||
}
|
||||
}
|
||||
ref101 := tg.InputSavedStarGiftClass(&tg.InputSavedStarGiftUser{MsgID: 101})
|
||||
ref102 := tg.InputSavedStarGiftClass(&tg.InputSavedStarGiftUser{MsgID: 102})
|
||||
|
||||
first, err := r.onPaymentsCreateStarGiftCollection(ctx, &tg.PaymentsCreateStarGiftCollectionRequest{
|
||||
Peer: &tg.InputPeerSelf{}, Title: " Favorites ", Stargift: []tg.InputSavedStarGiftClass{ref101},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create first collection: %v", err)
|
||||
}
|
||||
second, err := r.onPaymentsCreateStarGiftCollection(ctx, &tg.PaymentsCreateStarGiftCollectionRequest{
|
||||
Peer: &tg.InputPeerSelf{}, Title: "Archive", Stargift: []tg.InputSavedStarGiftClass{ref102},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create second collection: %v", err)
|
||||
}
|
||||
if first.Title != "Favorites" || first.GiftsCount != 1 || second.GiftsCount != 1 {
|
||||
t.Fatalf("created collections = %#v / %#v", first, second)
|
||||
}
|
||||
|
||||
listedClass, err := r.onPaymentsGetStarGiftCollections(ctx, &tg.PaymentsGetStarGiftCollectionsRequest{Peer: &tg.InputPeerSelf{}})
|
||||
if err != nil {
|
||||
t.Fatalf("list collections: %v", err)
|
||||
}
|
||||
listed, ok := listedClass.(*tg.PaymentsStarGiftCollections)
|
||||
if !ok || len(listed.Collections) != 2 {
|
||||
t.Fatalf("list collections = %T %#v, want two", listedClass, listedClass)
|
||||
}
|
||||
domainCollections, err := r.deps.Gifts.ListCollections(context.Background(), ownerPeer)
|
||||
if err != nil {
|
||||
t.Fatalf("list domain collections: %v", err)
|
||||
}
|
||||
if notModified, err := r.onPaymentsGetStarGiftCollections(ctx, &tg.PaymentsGetStarGiftCollectionsRequest{
|
||||
Peer: &tg.InputPeerSelf{}, Hash: domain.StarGiftCollectionsHash(domainCollections),
|
||||
}); err != nil {
|
||||
t.Fatalf("hash list collections: %v", err)
|
||||
} else if _, ok := notModified.(*tg.PaymentsStarGiftCollectionsNotModified); !ok {
|
||||
t.Fatalf("hash response = %T, want not modified", notModified)
|
||||
}
|
||||
|
||||
update := &tg.PaymentsUpdateStarGiftCollectionRequest{Peer: &tg.InputPeerSelf{}, CollectionID: first.CollectionID}
|
||||
update.SetTitle("Best")
|
||||
update.SetAddStargift([]tg.InputSavedStarGiftClass{ref102})
|
||||
update.SetOrder([]tg.InputSavedStarGiftClass{ref102, ref101})
|
||||
updated, err := r.onPaymentsUpdateStarGiftCollection(ctx, update)
|
||||
if err != nil {
|
||||
t.Fatalf("update collection: %v", err)
|
||||
}
|
||||
if updated.Title != "Best" || updated.GiftsCount != 2 {
|
||||
t.Fatalf("updated collection = %#v", updated)
|
||||
}
|
||||
|
||||
filteredReq := &tg.PaymentsGetSavedStarGiftsRequest{Peer: &tg.InputPeerSelf{}, Limit: 10}
|
||||
filteredReq.SetCollectionID(first.CollectionID)
|
||||
filtered, err := r.onPaymentsGetSavedStarGifts(ctx, filteredReq)
|
||||
if err != nil {
|
||||
t.Fatalf("filter saved gifts by collection: %v", err)
|
||||
}
|
||||
if filtered.Count != 2 || len(filtered.Gifts) != 2 {
|
||||
t.Fatalf("filtered gifts count=%d len=%d, want 2/2", filtered.Count, len(filtered.Gifts))
|
||||
}
|
||||
for _, saved := range filtered.Gifts {
|
||||
ids, ok := saved.GetCollectionID()
|
||||
if !ok || len(ids) == 0 || ids[0] != first.CollectionID {
|
||||
t.Fatalf("saved gift collection projection = %#v", saved)
|
||||
}
|
||||
}
|
||||
|
||||
if ok, err := r.onPaymentsToggleStarGiftsPinnedToTop(ctx, &tg.PaymentsToggleStarGiftsPinnedToTopRequest{
|
||||
Peer: &tg.InputPeerSelf{}, Stargift: []tg.InputSavedStarGiftClass{ref101, ref102},
|
||||
}); err != nil || !ok {
|
||||
t.Fatalf("pin gifts = %v err %v", ok, err)
|
||||
}
|
||||
pinned, err := r.onPaymentsGetSavedStarGifts(ctx, &tg.PaymentsGetSavedStarGiftsRequest{Peer: &tg.InputPeerSelf{}, Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("list pinned gifts: %v", err)
|
||||
}
|
||||
if len(pinned.Gifts) != 2 || !pinned.Gifts[0].PinnedToTop || !pinned.Gifts[1].PinnedToTop {
|
||||
t.Fatalf("pinned projection = %#v", pinned.Gifts)
|
||||
}
|
||||
|
||||
if ok, err := r.onPaymentsReorderStarGiftCollections(ctx, &tg.PaymentsReorderStarGiftCollectionsRequest{
|
||||
Peer: &tg.InputPeerSelf{}, Order: []int{second.CollectionID, first.CollectionID},
|
||||
}); err != nil || !ok {
|
||||
t.Fatalf("reorder collections = %v err %v", ok, err)
|
||||
}
|
||||
reorderedClass, err := r.onPaymentsGetStarGiftCollections(ctx, &tg.PaymentsGetStarGiftCollectionsRequest{Peer: &tg.InputPeerSelf{}})
|
||||
if err != nil {
|
||||
t.Fatalf("list reordered collections: %v", err)
|
||||
}
|
||||
reordered := reorderedClass.(*tg.PaymentsStarGiftCollections)
|
||||
if reordered.Collections[0].CollectionID != second.CollectionID {
|
||||
t.Fatalf("reordered collections = %#v", reordered.Collections)
|
||||
}
|
||||
|
||||
if ok, err := r.onPaymentsDeleteStarGiftCollection(ctx, &tg.PaymentsDeleteStarGiftCollectionRequest{
|
||||
Peer: &tg.InputPeerSelf{}, CollectionID: first.CollectionID,
|
||||
}); err != nil || !ok {
|
||||
t.Fatalf("delete collection = %v err %v", ok, err)
|
||||
}
|
||||
afterDelete, err := r.onPaymentsGetSavedStarGifts(ctx, &tg.PaymentsGetSavedStarGiftsRequest{Peer: &tg.InputPeerSelf{}, Limit: 10})
|
||||
if err != nil {
|
||||
t.Fatalf("list gifts after collection delete: %v", err)
|
||||
}
|
||||
for _, saved := range afterDelete.Gifts {
|
||||
if ids, ok := saved.GetCollectionID(); ok {
|
||||
for _, id := range ids {
|
||||
if id == first.CollectionID {
|
||||
t.Fatalf("deleted collection %d leaked in saved gift %#v", id, saved)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 完整 star gift saga:catalog → getPaymentForm(paymentFormStarGift) → sendStarsForm(扣费+服务消息
|
||||
// +paymentResult) → 收礼人 getSavedStarGifts → save/convert。
|
||||
func TestStarGiftSaga(t *testing.T) {
|
||||
|
|
@ -214,6 +533,21 @@ func TestStarGiftChannelSaga(t *testing.T) {
|
|||
channel := created.Channel
|
||||
channelPeer := &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
|
||||
channelInput := &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
|
||||
giftService := r.deps.Gifts.(*appstargifts.Service)
|
||||
if _, err := giftService.PublishCollectibleRevision(ctx, domain.StarGiftCollectibleWrite{
|
||||
GiftID: gift.ID, UpgradeStars: 75, SupplyTotal: 10, SlugPrefix: "channel-cake",
|
||||
Models: []domain.StarGiftCollectibleAttribute{collectibleRPCAttribute(domain.StarGiftCollectibleModel, 8201, "Aurora")},
|
||||
Patterns: []domain.StarGiftCollectibleAttribute{collectibleRPCAttribute(domain.StarGiftCollectiblePattern, 8202, "Orbit")},
|
||||
Backdrops: []domain.StarGiftCollectibleAttribute{collectibleRPCAttribute(domain.StarGiftCollectibleBackdrop, 2, "Midnight")},
|
||||
Actor: "test", CommandID: "channel-collectible-rpc",
|
||||
}); err != nil {
|
||||
t.Fatalf("publish channel collectible pool: %v", err)
|
||||
}
|
||||
if _, 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")
|
||||
}
|
||||
inv := &tg.InputInvoiceStarGift{
|
||||
Peer: channelPeer,
|
||||
GiftID: gift.ID,
|
||||
|
|
@ -265,6 +599,9 @@ 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")
|
||||
}
|
||||
savedID, ok := savedRes.Gifts[0].GetSavedID()
|
||||
if !ok || savedID <= 0 {
|
||||
t.Fatalf("saved gift saved_id = %d ok %v, want positive", savedID, ok)
|
||||
|
|
@ -366,17 +703,20 @@ func TestStarGiftInsufficientBalance(t *testing.T) {
|
|||
msgStore := memory.NewMessageStore(dialogs)
|
||||
sender, _ := users.Create(ctx, domain.User{AccessHash: 7201, Phone: "15550007201", FirstName: "Poor"})
|
||||
recipient, _ := users.Create(ctx, domain.User{AccessHash: 7202, Phone: "15550007202", FirstName: "Rich"})
|
||||
gift := domain.StarGift{ID: 8002, Stars: 5000, ConvertStars: 5000, Title: "Expensive",
|
||||
Sticker: domain.Document{ID: 701, AccessHash: 7, DCID: 2, MimeType: "image/webp"}}
|
||||
gift := domain.StarGift{ID: 8002, RevisionID: 9002, Stars: 5000, ConvertStars: 5000, Title: "Expensive",
|
||||
Sticker: domain.Document{ID: 701, AccessHash: 7, DCID: 2, MimeType: "application/x-tgsticker", Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}}}}
|
||||
giftStore := memory.NewStarGiftStore()
|
||||
giftStore.SeedCatalog([]domain.StarGift{gift})
|
||||
r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{
|
||||
Users: appusers.NewService(users),
|
||||
Messages: appmessages.NewService(msgStore, dialogs),
|
||||
Stars: appstars.NewService(memory.NewStarsStore(), appstars.WithStartingGrant(1000)), // < 5000
|
||||
Gifts: appstargifts.NewService(memory.NewStarGiftStore(), stubGiftCatalog{[]domain.StarGift{gift}}),
|
||||
Gifts: appstargifts.NewService(giftStore, nil, 2),
|
||||
}, 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}
|
||||
if _, err := r.onPaymentsSendStarsForm(senderCtx, &tg.PaymentsSendStarsFormRequest{Invoice: inv}); err == nil {
|
||||
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 {
|
||||
t.Fatalf("over-budget gift should error BALANCE_TOO_LOW")
|
||||
}
|
||||
// 余额未变。
|
||||
|
|
@ -385,6 +725,25 @@ func TestStarGiftInsufficientBalance(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestStarGiftFormBindsCatalogRevisionAndPrice(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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarsTopupInvoiceFallbackCreditsBalance(t *testing.T) {
|
||||
r, sender, _, _ := starGiftTestRouter(t)
|
||||
ctx := context.Background()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue