1021 lines
41 KiB
Go
1021 lines
41 KiB
Go
package rpc
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"hash/fnv"
|
|
"strings"
|
|
|
|
"github.com/iamxvbaba/td/tg"
|
|
"github.com/iamxvbaba/td/tgerr"
|
|
|
|
"telesrv/internal/domain"
|
|
)
|
|
|
|
func (r *Router) starGiftTransferPaymentForm(ctx context.Context, userID int64, inv *tg.InputInvoiceStarGiftTransfer) (tg.PaymentsPaymentFormClass, error) {
|
|
target, to, err := r.starGiftPaidTransferTarget(ctx, userID, inv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &tg.PaymentsPaymentFormStarGift{FormID: starGiftLifecycleFormID("transfer", userID,
|
|
target.ID, target.Owner.Type, target.Owner.ID, to.Type, to.ID, target.TransferStars, target.CanTransferAt),
|
|
Invoice: tg.Invoice{Currency: "XTR", Prices: []tg.LabeledPrice{{Label: "Collectible gift transfer", Amount: target.TransferStars}}}}, nil
|
|
}
|
|
|
|
func (r *Router) sendStarGiftTransferForm(ctx context.Context, userID, formID int64, inv *tg.InputInvoiceStarGiftTransfer) (tg.PaymentsPaymentResultClass, error) {
|
|
target, to, err := r.starGiftPaidTransferTarget(ctx, userID, inv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
wantFormID := starGiftLifecycleFormID("transfer", userID,
|
|
target.ID, target.Owner.Type, target.Owner.ID, to.Type, to.ID, target.TransferStars, target.CanTransferAt)
|
|
if formID == 0 || formID != wantFormID {
|
|
return nil, starsFormAmountMismatchErr()
|
|
}
|
|
if r.deps.Stars != nil {
|
|
if _, err := r.deps.Stars.GetBalance(ctx, userID); err != nil {
|
|
return nil, starsErr(err)
|
|
}
|
|
}
|
|
result, err := r.deps.Gifts.Transfer(ctx, domain.StarGiftTransferRequest{ActorUserID: userID,
|
|
Ref: domain.SavedStarGiftRef{Owner: target.Owner, MsgID: target.MsgID, SavedID: target.SavedID}, To: to,
|
|
ChargeStars: target.TransferStars, FormID: formID, CommandKey: fmt.Sprintf("paid-transfer:%d:%d", target.ID, formID),
|
|
Date: int(r.clock.Now().Unix()), OriginAuthKeyID: rawAuthKeyIDForOrigin(ctx), OriginSessionID: sessionIDOrZero(ctx)})
|
|
if err != nil {
|
|
return nil, starGiftLifecycleErr(err)
|
|
}
|
|
r.invalidateStarGiftOwner(target.Owner)
|
|
r.invalidateStarGiftOwner(to)
|
|
return &tg.PaymentsPaymentResult{Updates: r.starGiftTransferUpdates(ctx, userID, result, false)}, nil
|
|
}
|
|
|
|
func (r *Router) starGiftPaidTransferTarget(ctx context.Context, userID int64, inv *tg.InputInvoiceStarGiftTransfer) (domain.SavedStarGift, domain.Peer, error) {
|
|
if inv == nil || r.deps.Gifts == nil {
|
|
return domain.SavedStarGift{}, domain.Peer{}, starGiftInvalidErr()
|
|
}
|
|
ref, ok, err := r.starGiftRefFromInput(ctx, userID, inv.Stargift)
|
|
if err != nil || !ok {
|
|
return domain.SavedStarGift{}, domain.Peer{}, starGiftInvalidErr()
|
|
}
|
|
if err := r.checkStarGiftOwnerPermission(ctx, userID, ref.Owner); err != nil {
|
|
return domain.SavedStarGift{}, domain.Peer{}, err
|
|
}
|
|
to, err := r.checkedDomainPeerFromInputPeer(ctx, userID, inv.ToID)
|
|
if err != nil {
|
|
return domain.SavedStarGift{}, domain.Peer{}, err
|
|
}
|
|
saved, found, err := r.deps.Gifts.GetSaved(ctx, ref)
|
|
if err != nil {
|
|
return domain.SavedStarGift{}, domain.Peer{}, internalErr()
|
|
}
|
|
if !found || saved.UniqueGiftID == 0 || !saved.LifecycleStatus.Live() || saved.TransferStars <= 0 || saved.Owner == to {
|
|
return domain.SavedStarGift{}, domain.Peer{}, starGiftInvalidErr()
|
|
}
|
|
return saved, to, nil
|
|
}
|
|
|
|
func (r *Router) starGiftResalePaymentForm(ctx context.Context, userID int64, inv *tg.InputInvoiceStarGiftResale) (tg.PaymentsPaymentFormClass, error) {
|
|
gift, to, amount, err := r.starGiftResaleTarget(ctx, userID, inv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
currency := string(amount.Currency)
|
|
return &tg.PaymentsPaymentFormStarGift{FormID: starGiftLifecycleFormID("resale", userID,
|
|
gift.ID, gift.Owner.Type, gift.Owner.ID, to.Type, to.ID, amount.Currency, amount.Amount, gift.ResellVersion),
|
|
Invoice: tg.Invoice{Currency: currency, Prices: []tg.LabeledPrice{{Label: "Collectible gift resale", Amount: amount.Amount}}}}, nil
|
|
}
|
|
|
|
func (r *Router) sendStarGiftResaleForm(ctx context.Context, userID, formID int64, inv *tg.InputInvoiceStarGiftResale) (tg.PaymentsPaymentResultClass, error) {
|
|
gift, to, amount, err := r.starGiftResaleTarget(ctx, userID, inv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
wantFormID := starGiftLifecycleFormID("resale", userID,
|
|
gift.ID, gift.Owner.Type, gift.Owner.ID, to.Type, to.ID, amount.Currency, amount.Amount, gift.ResellVersion)
|
|
if formID == 0 || formID != wantFormID {
|
|
return nil, starsFormAmountMismatchErr()
|
|
}
|
|
if amount.Currency == domain.StarGiftCurrencyTON {
|
|
if _, err := r.deps.Gifts.TonBalance(ctx, userID); err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
} else if r.deps.Stars != nil {
|
|
if _, err := r.deps.Stars.GetBalance(ctx, userID); err != nil {
|
|
return nil, starsErr(err)
|
|
}
|
|
}
|
|
result, err := r.deps.Gifts.PurchaseResale(ctx, domain.StarGiftResalePurchaseRequest{BuyerUserID: userID,
|
|
Slug: gift.Slug, To: to, Amount: amount, FormID: formID, CommandKey: fmt.Sprintf("resale:%d:%d", gift.ID, formID),
|
|
Date: int(r.clock.Now().Unix()), OriginAuthKeyID: rawAuthKeyIDForOrigin(ctx), OriginSessionID: sessionIDOrZero(ctx)})
|
|
if err != nil {
|
|
return nil, starGiftLifecycleErr(err)
|
|
}
|
|
r.invalidateStarGiftOwner(gift.Owner)
|
|
r.invalidateStarGiftOwner(to)
|
|
return &tg.PaymentsPaymentResult{Updates: r.starGiftTransferUpdates(ctx, userID, result, amount.Currency == domain.StarGiftCurrencyTON)}, nil
|
|
}
|
|
|
|
func (r *Router) starGiftResaleTarget(ctx context.Context, userID int64, inv *tg.InputInvoiceStarGiftResale) (domain.UniqueStarGift, domain.Peer, domain.StarGiftAmount, error) {
|
|
if inv == nil || r.deps.Gifts == nil || strings.TrimSpace(inv.Slug) == "" {
|
|
return domain.UniqueStarGift{}, domain.Peer{}, domain.StarGiftAmount{}, starGiftInvalidErr()
|
|
}
|
|
gift, found, err := r.deps.Gifts.UniqueBySlug(ctx, inv.Slug)
|
|
if err != nil {
|
|
return domain.UniqueStarGift{}, domain.Peer{}, domain.StarGiftAmount{}, internalErr()
|
|
}
|
|
if !found || gift.ResellAmount == nil || gift.Burned || gift.OwnerAddress != "" {
|
|
return domain.UniqueStarGift{}, domain.Peer{}, domain.StarGiftAmount{}, starGiftInvalidErr()
|
|
}
|
|
to, err := r.checkedDomainPeerFromInputPeer(ctx, userID, inv.ToID)
|
|
if err != nil {
|
|
return domain.UniqueStarGift{}, domain.Peer{}, domain.StarGiftAmount{}, err
|
|
}
|
|
wantCurrency := domain.StarGiftCurrencyStars
|
|
if inv.Ton {
|
|
wantCurrency = domain.StarGiftCurrencyTON
|
|
}
|
|
if gift.ResellAmount.Currency != wantCurrency || gift.Owner == to {
|
|
return domain.UniqueStarGift{}, domain.Peer{}, domain.StarGiftAmount{}, starGiftInvalidErr()
|
|
}
|
|
return gift, to, *gift.ResellAmount, nil
|
|
}
|
|
|
|
func (r *Router) starGiftAuctionBidPaymentForm(ctx context.Context, userID int64, inv *tg.InputInvoiceStarGiftAuctionBid) (tg.PaymentsPaymentFormClass, error) {
|
|
state, peer, delta, err := r.starGiftAuctionBidTarget(ctx, userID, inv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &tg.PaymentsPaymentFormStars{FormID: starGiftLifecycleFormID("auction", userID,
|
|
state.Gift.ID, peer.Type, peer.ID, inv.BidAmount, state.Version),
|
|
BotID: domain.OfficialSystemUserID, Title: state.Gift.Title, Description: "Collectible gift auction bid",
|
|
Invoice: tg.Invoice{Currency: "XTR", Prices: []tg.LabeledPrice{{Label: "Auction bid", Amount: delta}}},
|
|
Users: tgUsersForViewer(userID, []domain.User{domain.OfficialSystemUser()})}, nil
|
|
}
|
|
|
|
func (r *Router) sendStarGiftAuctionBidForm(ctx context.Context, userID, formID int64, inv *tg.InputInvoiceStarGiftAuctionBid) (tg.PaymentsPaymentResultClass, error) {
|
|
state, peer, _, err := r.starGiftAuctionBidTarget(ctx, userID, inv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
wantFormID := starGiftLifecycleFormID("auction", userID,
|
|
state.Gift.ID, peer.Type, peer.ID, inv.BidAmount, state.Version)
|
|
if formID == 0 || formID != wantFormID {
|
|
return nil, starsFormAmountMismatchErr()
|
|
}
|
|
if r.deps.Stars != nil {
|
|
if _, err := r.deps.Stars.GetBalance(ctx, userID); err != nil {
|
|
return nil, starsErr(err)
|
|
}
|
|
}
|
|
message := ""
|
|
if text, ok := inv.GetMessage(); ok {
|
|
message = clampGiftMessage(text.Text)
|
|
}
|
|
newState, balance, err := r.deps.Gifts.BidAuction(ctx, domain.StarGiftAuctionBidRequest{UserID: userID,
|
|
GiftID: inv.GiftID, Peer: peer, BidAmount: inv.BidAmount, HideName: inv.HideName, Message: message,
|
|
UpdateBid: inv.UpdateBid, FormID: formID, Date: int(r.clock.Now().Unix())})
|
|
if err != nil {
|
|
return nil, starGiftLifecycleErr(err)
|
|
}
|
|
updates := emptyGiftUpdates(r.clock.Now().Unix())
|
|
updates.Updates = append(updates.Updates,
|
|
&tg.UpdateStarGiftAuctionState{GiftID: inv.GiftID, State: tgStarGiftAuctionState(newState)},
|
|
&tg.UpdateStarGiftAuctionUserState{GiftID: inv.GiftID, UserState: tgStarGiftAuctionUserState(newState.UserState)})
|
|
appendStarGiftBalanceUpdate(updates, domain.StarGiftCurrencyStars, balance.Balance)
|
|
return &tg.PaymentsPaymentResult{Updates: updates}, nil
|
|
}
|
|
|
|
func (r *Router) starGiftAuctionBidTarget(ctx context.Context, userID int64, inv *tg.InputInvoiceStarGiftAuctionBid) (domain.StarGiftAuction, domain.Peer, int64, error) {
|
|
if inv == nil || r.deps.Gifts == nil || inv.GiftID <= 0 || inv.BidAmount <= 0 {
|
|
return domain.StarGiftAuction{}, domain.Peer{}, 0, starGiftInvalidErr()
|
|
}
|
|
state, err := r.deps.Gifts.AuctionState(ctx, userID, inv.GiftID, "", int(r.clock.Now().Unix()))
|
|
if err != nil {
|
|
return domain.StarGiftAuction{}, domain.Peer{}, 0, starGiftLifecycleErr(err)
|
|
}
|
|
oldAmount := state.UserState.BidAmount
|
|
peer := domain.Peer{Type: domain.PeerTypeUser, ID: userID}
|
|
if inv.UpdateBid {
|
|
if oldAmount <= 0 || inv.HideName {
|
|
return domain.StarGiftAuction{}, domain.Peer{}, 0, starGiftInvalidErr()
|
|
}
|
|
if _, ok := inv.GetPeer(); ok {
|
|
return domain.StarGiftAuction{}, domain.Peer{}, 0, starGiftInvalidErr()
|
|
}
|
|
if _, ok := inv.GetMessage(); ok {
|
|
return domain.StarGiftAuction{}, domain.Peer{}, 0, starGiftInvalidErr()
|
|
}
|
|
peer = state.UserState.BidPeer
|
|
} else {
|
|
if oldAmount > 0 {
|
|
return domain.StarGiftAuction{}, domain.Peer{}, 0, starGiftInvalidErr()
|
|
}
|
|
if inputPeer, ok := inv.GetPeer(); ok {
|
|
peer, err = r.checkedDomainPeerFromInputPeer(ctx, userID, inputPeer)
|
|
if err != nil {
|
|
return domain.StarGiftAuction{}, domain.Peer{}, 0, err
|
|
}
|
|
}
|
|
}
|
|
if peer.Type == domain.PeerTypeChannel {
|
|
if err := r.checkStarGiftOwnerPermission(ctx, userID, peer); err != nil {
|
|
return domain.StarGiftAuction{}, domain.Peer{}, 0, err
|
|
}
|
|
}
|
|
minimum := state.MinBidAmount
|
|
if oldAmount > 0 {
|
|
minimum = state.UserState.MinBidAmount
|
|
}
|
|
if inv.BidAmount < minimum || inv.BidAmount <= oldAmount {
|
|
return domain.StarGiftAuction{}, domain.Peer{}, 0, starGiftInvalidErr()
|
|
}
|
|
return state, peer, inv.BidAmount - oldAmount, nil
|
|
}
|
|
|
|
func (r *Router) starGiftPrepaidUpgradePaymentForm(ctx context.Context, userID int64, inv *tg.InputInvoiceStarGiftPrepaidUpgrade) (tg.PaymentsPaymentFormClass, error) {
|
|
owner, target, price, err := r.starGiftPrepaidUpgradeTarget(ctx, userID, inv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
formID := starGiftLifecycleFormID("prepay-upgrade", userID, owner.Type, owner.ID, target.ID, inv.Hash, price)
|
|
return &tg.PaymentsPaymentFormStarGift{FormID: formID,
|
|
Invoice: tg.Invoice{Currency: "XTR", Prices: []tg.LabeledPrice{{Label: "Prepaid collectible gift upgrade", Amount: price}}}}, nil
|
|
}
|
|
|
|
func (r *Router) sendStarGiftPrepaidUpgradeForm(ctx context.Context, userID, formID int64, inv *tg.InputInvoiceStarGiftPrepaidUpgrade) (tg.PaymentsPaymentResultClass, error) {
|
|
if inv == nil || r.deps.Gifts == nil || formID == 0 {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
owner, target, price, targetErr := r.starGiftPrepaidUpgradeTarget(ctx, userID, inv)
|
|
commandKey := fmt.Sprintf("prepay-upgrade:%s:%d", strings.TrimSpace(inv.Hash), formID)
|
|
if targetErr == nil {
|
|
if formID != starGiftLifecycleFormID("prepay-upgrade", userID, owner.Type, owner.ID, target.ID, inv.Hash, price) {
|
|
return nil, starsFormAmountMismatchErr()
|
|
}
|
|
} else {
|
|
var err error
|
|
owner, err = r.checkedDomainPeerFromInputPeer(ctx, userID, inv.Peer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
price = 0 // accepted only by the store's exact replay path
|
|
}
|
|
if r.deps.Stars != nil {
|
|
if _, err := r.deps.Stars.GetBalance(ctx, userID); err != nil {
|
|
return nil, starsErr(err)
|
|
}
|
|
}
|
|
result, err := r.deps.Gifts.PrepayUpgrade(ctx, domain.StarGiftPrepaidUpgradeRequest{PayerUserID: userID,
|
|
Owner: owner, Hash: strings.TrimSpace(inv.Hash), ChargeStars: price, FormID: formID, CommandKey: commandKey,
|
|
Date: int(r.clock.Now().Unix()), OriginAuthKeyID: rawAuthKeyIDForOrigin(ctx), OriginSessionID: sessionIDOrZero(ctx)})
|
|
if err != nil {
|
|
return nil, starGiftLifecycleErr(err)
|
|
}
|
|
updates := r.starGiftSendUpdates(ctx, userID, result.Send)
|
|
appendStarGiftBalanceUpdate(updates, domain.StarGiftCurrencyStars, result.Balance.Balance)
|
|
r.invalidateStarGiftOwner(owner)
|
|
return &tg.PaymentsPaymentResult{Updates: updates}, nil
|
|
}
|
|
|
|
func (r *Router) starGiftPrepaidUpgradeTarget(ctx context.Context, userID int64, inv *tg.InputInvoiceStarGiftPrepaidUpgrade) (domain.Peer, domain.SavedStarGift, int64, error) {
|
|
if inv == nil || r.deps.Gifts == nil || strings.TrimSpace(inv.Hash) == "" {
|
|
return domain.Peer{}, domain.SavedStarGift{}, 0, starGiftInvalidErr()
|
|
}
|
|
owner, err := r.checkedDomainPeerFromInputPeer(ctx, userID, inv.Peer)
|
|
if err != nil {
|
|
return domain.Peer{}, domain.SavedStarGift{}, 0, err
|
|
}
|
|
target, price, err := r.deps.Gifts.PrepaidUpgradeTarget(ctx, owner, strings.TrimSpace(inv.Hash))
|
|
if err != nil {
|
|
return domain.Peer{}, domain.SavedStarGift{}, 0, starGiftLifecycleErr(err)
|
|
}
|
|
return owner, target, price, nil
|
|
}
|
|
|
|
func (r *Router) starGiftDropDetailsPaymentForm(ctx context.Context, userID int64, inv *tg.InputInvoiceStarGiftDropOriginalDetails) (tg.PaymentsPaymentFormClass, error) {
|
|
target, err := r.starGiftDropDetailsTarget(ctx, userID, inv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
formID := starGiftLifecycleFormID("drop-details", userID, target.ID, target.UniqueGiftID, target.DropOriginalDetailsStars)
|
|
return &tg.PaymentsPaymentFormStarGift{FormID: formID,
|
|
Invoice: tg.Invoice{Currency: "XTR", Prices: []tg.LabeledPrice{{Label: "Remove collectible gift original details", Amount: target.DropOriginalDetailsStars}}}}, nil
|
|
}
|
|
|
|
func (r *Router) sendStarGiftDropDetailsForm(ctx context.Context, userID, formID int64, inv *tg.InputInvoiceStarGiftDropOriginalDetails) (tg.PaymentsPaymentResultClass, error) {
|
|
if inv == nil || r.deps.Gifts == nil || formID == 0 {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
ref, ok, refErr := r.starGiftRefFromInput(ctx, userID, inv.Stargift)
|
|
if refErr != nil || !ok {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
target, targetErr := r.starGiftDropDetailsTarget(ctx, userID, inv)
|
|
charge := int64(0)
|
|
if targetErr == nil {
|
|
charge = target.DropOriginalDetailsStars
|
|
if formID != starGiftLifecycleFormID("drop-details", userID, target.ID, target.UniqueGiftID, charge) {
|
|
return nil, starsFormAmountMismatchErr()
|
|
}
|
|
}
|
|
if r.deps.Stars != nil {
|
|
if _, err := r.deps.Stars.GetBalance(ctx, userID); err != nil {
|
|
return nil, starsErr(err)
|
|
}
|
|
}
|
|
result, err := r.deps.Gifts.DropOriginalDetails(ctx, domain.StarGiftDropOriginalDetailsRequest{UserID: userID,
|
|
Ref: ref, ChargeStars: charge, FormID: formID, CommandKey: fmt.Sprintf("drop-details:%s:%s:%d", ref.Owner.Type, starGiftRefValue(ref), formID),
|
|
Date: int(r.clock.Now().Unix())})
|
|
if err != nil {
|
|
return nil, starGiftLifecycleErr(err)
|
|
}
|
|
updates := emptyGiftUpdates(r.clock.Now().Unix())
|
|
appendStarGiftBalanceUpdate(updates, domain.StarGiftCurrencyStars, result.Balance.Balance)
|
|
r.invalidateStarGiftOwner(ref.Owner)
|
|
return &tg.PaymentsPaymentResult{Updates: updates}, nil
|
|
}
|
|
|
|
func (r *Router) starGiftDropDetailsTarget(ctx context.Context, userID int64, inv *tg.InputInvoiceStarGiftDropOriginalDetails) (domain.SavedStarGift, error) {
|
|
if inv == nil || r.deps.Gifts == nil {
|
|
return domain.SavedStarGift{}, starGiftInvalidErr()
|
|
}
|
|
ref, ok, err := r.starGiftRefFromInput(ctx, userID, inv.Stargift)
|
|
if err != nil || !ok {
|
|
return domain.SavedStarGift{}, starGiftInvalidErr()
|
|
}
|
|
if err := r.checkStarGiftOwnerPermission(ctx, userID, ref.Owner); err != nil {
|
|
return domain.SavedStarGift{}, err
|
|
}
|
|
target, found, err := r.deps.Gifts.GetSaved(ctx, ref)
|
|
if err != nil {
|
|
return domain.SavedStarGift{}, internalErr()
|
|
}
|
|
if !found || !target.LifecycleStatus.Live() || target.UniqueGiftID <= 0 || target.DropOriginalDetailsStars <= 0 {
|
|
return domain.SavedStarGift{}, starGiftInvalidErr()
|
|
}
|
|
return target, nil
|
|
}
|
|
|
|
func starGiftLifecycleFormID(kind string, values ...any) int64 {
|
|
h := fnv.New64a()
|
|
_, _ = h.Write([]byte("telesrv:star-gift:" + kind + ":v1"))
|
|
for _, value := range values {
|
|
_, _ = fmt.Fprintf(h, ":%v", value)
|
|
}
|
|
id := int64(h.Sum64() & 0x7fffffffffffffff)
|
|
if id == 0 {
|
|
return 1
|
|
}
|
|
return id
|
|
}
|
|
|
|
func (r *Router) onPaymentsCheckCanSendGift(ctx context.Context, req *tg.PaymentsCheckCanSendGiftRequest) (tg.PaymentsCheckCanSendGiftResultClass, error) {
|
|
if req == nil || req.GiftID <= 0 || r.deps.Gifts == nil {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
gift, found, err := r.deps.Gifts.GiftByID(ctx, req.GiftID)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
if !found {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
now := int(r.clock.Now().Unix())
|
|
switch {
|
|
case gift.SoldOut || gift.Limited && gift.AvailabilityRemains <= 0:
|
|
return &tg.PaymentsCheckCanSendGiftResultFail{Reason: tg.TextWithEntities{Text: "This gift is sold out."}}, nil
|
|
case gift.LockedUntilDate > now:
|
|
return &tg.PaymentsCheckCanSendGiftResultFail{Reason: tg.TextWithEntities{Text: "This gift is not available yet."}}, nil
|
|
case gift.Auction:
|
|
return &tg.PaymentsCheckCanSendGiftResultFail{Reason: tg.TextWithEntities{Text: "This gift is distributed through an auction."}}, nil
|
|
default:
|
|
return &tg.PaymentsCheckCanSendGiftResultOk{}, nil
|
|
}
|
|
}
|
|
|
|
func (r *Router) onPaymentsGetUniqueStarGiftValueInfo(ctx context.Context, req *tg.PaymentsGetUniqueStarGiftValueInfoRequest) (*tg.PaymentsUniqueStarGiftValueInfo, error) {
|
|
if req == nil || strings.TrimSpace(req.Slug) == "" || r.deps.Gifts == nil {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
unique, found, err := r.deps.Gifts.UniqueBySlug(ctx, req.Slug)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
if !found {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
info, err := r.deps.Gifts.ValueInfo(ctx, unique.ID)
|
|
if err != nil {
|
|
return nil, starGiftLifecycleErr(err)
|
|
}
|
|
out := &tg.PaymentsUniqueStarGiftValueInfo{Currency: info.Currency, Value: info.Value,
|
|
InitialSaleDate: info.InitialSaleDate, InitialSaleStars: info.InitialSaleStars,
|
|
InitialSalePrice: info.InitialSalePrice}
|
|
if info.ValueIsAverage {
|
|
out.SetValueIsAverage(true)
|
|
}
|
|
if info.LastSaleDate > 0 {
|
|
out.SetLastSaleDate(info.LastSaleDate)
|
|
out.SetLastSalePrice(info.LastSalePrice)
|
|
}
|
|
if info.FloorPrice > 0 {
|
|
out.SetFloorPrice(info.FloorPrice)
|
|
}
|
|
if info.AveragePrice > 0 {
|
|
out.SetAveragePrice(info.AveragePrice)
|
|
}
|
|
out.SetListedCount(info.ListedCount)
|
|
return out, nil
|
|
}
|
|
|
|
func (r *Router) onPaymentsGetResaleStarGifts(ctx context.Context, req *tg.PaymentsGetResaleStarGiftsRequest) (*tg.PaymentsResaleStarGifts, error) {
|
|
if req == nil || r.deps.Gifts == nil {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
filter := domain.StarGiftResaleFilter{GiftID: req.GiftID, SortByPrice: req.SortByPrice, SortByNum: req.SortByNum,
|
|
ForCraft: req.ForCraft, StarsOnly: req.StarsOnly, Offset: req.Offset, Limit: req.Limit}
|
|
if filter.Limit <= 0 {
|
|
filter.Limit = domain.MaxSavedStarGiftsLimit
|
|
}
|
|
if attributes, ok := req.GetAttributes(); ok {
|
|
for _, attribute := range attributes {
|
|
switch value := attribute.(type) {
|
|
case *tg.StarGiftAttributeIDModel:
|
|
if value != nil && value.DocumentID > 0 {
|
|
filter.ModelIDs = append(filter.ModelIDs, value.DocumentID)
|
|
}
|
|
case *tg.StarGiftAttributeIDPattern:
|
|
if value != nil && value.DocumentID > 0 {
|
|
filter.PatternIDs = append(filter.PatternIDs, value.DocumentID)
|
|
}
|
|
case *tg.StarGiftAttributeIDBackdrop:
|
|
if value != nil && value.BackdropID > 0 {
|
|
filter.BackdropIDs = append(filter.BackdropIDs, int64(value.BackdropID))
|
|
}
|
|
default:
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
}
|
|
}
|
|
page, err := r.deps.Gifts.ListResale(ctx, filter)
|
|
if err != nil {
|
|
return nil, starGiftLifecycleErr(err)
|
|
}
|
|
out := &tg.PaymentsResaleStarGifts{Count: page.Count, Gifts: make([]tg.StarGiftClass, 0, len(page.Gifts)),
|
|
Users: []tg.UserClass{}, Chats: []tg.ChatClass{}}
|
|
userIDs, channelIDs := make([]int64, 0), make([]int64, 0)
|
|
for _, gift := range page.Gifts {
|
|
out.Gifts = append(out.Gifts, tgUniqueStarGift(gift))
|
|
if gift.Owner.Type == domain.PeerTypeUser {
|
|
userIDs = append(userIDs, gift.Owner.ID)
|
|
} else if gift.Owner.Type == domain.PeerTypeChannel {
|
|
channelIDs = append(channelIDs, gift.Owner.ID)
|
|
}
|
|
}
|
|
if page.NextOffset != "" {
|
|
out.SetNextOffset(page.NextOffset)
|
|
}
|
|
viewerID, _, _ := r.currentUserID(ctx)
|
|
out.Users = tgUsersForViewer(viewerID, r.domainUsersForIDs(ctx, viewerID, uniqueInt64(userIDs)))
|
|
out.Chats = r.tgChatsForChannelIDs(ctx, viewerID, uniqueInt64(channelIDs))
|
|
if attributesHash, requested := req.GetAttributesHash(); requested {
|
|
preview, found, previewErr := r.deps.Gifts.CollectiblePreview(ctx, req.GiftID)
|
|
if previewErr != nil {
|
|
return nil, internalErr()
|
|
}
|
|
if found {
|
|
hash := int64(preview.Revision)
|
|
out.SetAttributesHash(hash)
|
|
if attributesHash != hash {
|
|
attributes := make([]tg.StarGiftAttributeClass, 0, len(preview.Models)+len(preview.Patterns)+len(preview.Backdrops))
|
|
for _, attribute := range preview.Models {
|
|
attributes = append(attributes, tgStarGiftAttribute(attribute))
|
|
}
|
|
for _, attribute := range preview.Patterns {
|
|
attributes = append(attributes, tgStarGiftAttribute(attribute))
|
|
}
|
|
for _, attribute := range preview.Backdrops {
|
|
attributes = append(attributes, tgStarGiftAttribute(attribute))
|
|
}
|
|
out.SetAttributes(attributes)
|
|
}
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (r *Router) onPaymentsUpdateStarGiftPrice(ctx context.Context, req *tg.PaymentsUpdateStarGiftPriceRequest) (tg.UpdatesClass, error) {
|
|
if req == nil || r.deps.Gifts == nil {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
ref, ok, err := r.starGiftRefFromInput(ctx, userID, req.Stargift)
|
|
if err != nil || !ok {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
if err := r.checkStarGiftOwnerPermission(ctx, userID, ref.Owner); err != nil {
|
|
return nil, err
|
|
}
|
|
var amount *domain.StarGiftAmount
|
|
switch value := req.ResellAmount.(type) {
|
|
case *tg.StarsAmount:
|
|
if value == nil || value.Amount < 0 || value.Nanos != 0 {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
if value.Amount > 0 {
|
|
amount = &domain.StarGiftAmount{Currency: domain.StarGiftCurrencyStars, Amount: value.Amount}
|
|
}
|
|
case *tg.StarsTonAmount:
|
|
if value == nil || value.Amount < 0 {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
if value.Amount > 0 {
|
|
amount = &domain.StarGiftAmount{Currency: domain.StarGiftCurrencyTON, Amount: value.Amount}
|
|
}
|
|
default:
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
if _, err := r.deps.Gifts.SetListing(ctx, domain.StarGiftListingRequest{ActorUserID: userID, Ref: ref,
|
|
Amount: amount, Date: int(r.clock.Now().Unix())}); err != nil {
|
|
return nil, starGiftLifecycleErr(err)
|
|
}
|
|
r.invalidateStarGiftOwner(ref.Owner)
|
|
return emptyGiftUpdates(r.clock.Now().Unix()), nil
|
|
}
|
|
|
|
func (r *Router) onPaymentsTransferStarGift(ctx context.Context, req *tg.PaymentsTransferStarGiftRequest) (tg.UpdatesClass, error) {
|
|
if req == nil || r.deps.Gifts == nil {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
ref, ok, err := r.starGiftRefFromInput(ctx, userID, req.Stargift)
|
|
if err != nil || !ok {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
if err := r.checkStarGiftOwnerPermission(ctx, userID, ref.Owner); err != nil {
|
|
return nil, err
|
|
}
|
|
to, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.ToID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
now := int(r.clock.Now().Unix())
|
|
result, err := r.deps.Gifts.Transfer(ctx, domain.StarGiftTransferRequest{ActorUserID: userID, Ref: ref, To: to,
|
|
CommandKey: fmt.Sprintf("free:%s:%d:%s:%s:%d", ref.Owner.Type, ref.Owner.ID, starGiftRefValue(ref), to.Type, to.ID),
|
|
Date: now, OriginAuthKeyID: rawAuthKeyIDForOrigin(ctx), OriginSessionID: sessionIDOrZero(ctx)})
|
|
if err != nil {
|
|
return nil, starGiftLifecycleErr(err)
|
|
}
|
|
r.invalidateStarGiftOwner(ref.Owner)
|
|
r.invalidateStarGiftOwner(to)
|
|
return r.starGiftTransferUpdates(ctx, userID, result, false), nil
|
|
}
|
|
|
|
func (r *Router) onPaymentsGetStarGiftWithdrawalURL(ctx context.Context, req *tg.PaymentsGetStarGiftWithdrawalURLRequest) (*tg.PaymentsStarGiftWithdrawalURL, error) {
|
|
if req == nil || r.deps.Gifts == nil || r.deps.Account == nil {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
if err := r.deps.Account.CheckPassword(ctx, userID, domainPasswordCheck(req.Password)); err != nil {
|
|
return nil, passwordErr(err)
|
|
}
|
|
ref, ok, err := r.starGiftRefFromInput(ctx, userID, req.Stargift)
|
|
if err != nil || !ok || ref.Owner != (domain.Peer{Type: domain.PeerTypeUser, ID: userID}) {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
withdrawal, err := r.deps.Gifts.Withdraw(ctx, domain.StarGiftWithdrawalRequest{UserID: userID, Ref: ref, Date: int(r.clock.Now().Unix())})
|
|
if err != nil {
|
|
return nil, starGiftLifecycleErr(err)
|
|
}
|
|
return &tg.PaymentsStarGiftWithdrawalURL{URL: withdrawal.URL}, nil
|
|
}
|
|
|
|
func (r *Router) onPaymentsSendStarGiftOffer(ctx context.Context, req *tg.PaymentsSendStarGiftOfferRequest) (tg.UpdatesClass, error) {
|
|
if req == nil || r.deps.Gifts == nil {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
owner, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
price, ok := domainStarGiftAmount(req.Price)
|
|
if !ok {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
now := int(r.clock.Now().Unix())
|
|
result, err := r.deps.Gifts.SendOffer(ctx, domain.StarGiftOfferRequest{BuyerUserID: userID, Owner: owner,
|
|
Slug: req.Slug, Price: price, Duration: req.Duration, RandomID: req.RandomID, Date: now,
|
|
OriginAuthKeyID: rawAuthKeyIDForOrigin(ctx), OriginSessionID: sessionIDOrZero(ctx)})
|
|
if err != nil {
|
|
return nil, starGiftLifecycleErr(err)
|
|
}
|
|
updates := r.starGiftSendUpdates(ctx, userID, result.Send)
|
|
appendStarGiftBalanceUpdate(updates, price.Currency, result.Balance.Balance)
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onPaymentsResolveStarGiftOffer(ctx context.Context, req *tg.PaymentsResolveStarGiftOfferRequest) (tg.UpdatesClass, error) {
|
|
if req == nil || r.deps.Gifts == nil {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
now := int(r.clock.Now().Unix())
|
|
result, err := r.deps.Gifts.ResolveOffer(ctx, domain.StarGiftResolveOfferRequest{OwnerUserID: userID,
|
|
OfferMsgID: req.OfferMsgID, Decline: req.Decline, Date: now, OriginAuthKeyID: rawAuthKeyIDForOrigin(ctx),
|
|
OriginSessionID: sessionIDOrZero(ctx)})
|
|
if err != nil {
|
|
return nil, starGiftLifecycleErr(err)
|
|
}
|
|
updates := r.starGiftSendUpdates(ctx, userID, result.Send)
|
|
if !req.Decline {
|
|
if result.Offer.Price.Currency == domain.StarGiftCurrencyTON {
|
|
balance, _ := r.deps.Gifts.TonBalance(ctx, userID)
|
|
appendStarGiftBalanceUpdate(updates, domain.StarGiftCurrencyTON, balance)
|
|
} else if r.deps.Stars != nil {
|
|
balance, balanceErr := r.deps.Stars.GetBalance(ctx, userID)
|
|
if balanceErr == nil {
|
|
appendStarGiftBalanceUpdate(updates, domain.StarGiftCurrencyStars, balance.Balance)
|
|
}
|
|
}
|
|
}
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onPaymentsGetCraftStarGifts(ctx context.Context, req *tg.PaymentsGetCraftStarGiftsRequest) (*tg.PaymentsSavedStarGifts, error) {
|
|
if req == nil || r.deps.Gifts == nil {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
page, err := r.deps.Gifts.ListCraft(ctx, userID, req.GiftID, req.Offset, req.Limit)
|
|
if err != nil {
|
|
return nil, starGiftLifecycleErr(err)
|
|
}
|
|
return r.tgSavedStarGiftsResponse(ctx, userID, page.Gifts, page.Count, page.NextOffset)
|
|
}
|
|
|
|
func (r *Router) onPaymentsCraftStarGift(ctx context.Context, req *tg.PaymentsCraftStarGiftRequest) (tg.UpdatesClass, error) {
|
|
if req == nil || r.deps.Gifts == nil || len(req.Stargift) < 1 || len(req.Stargift) > 4 {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
refs := make([]domain.SavedStarGiftRef, 0, len(req.Stargift))
|
|
commandParts := make([]string, 0, len(req.Stargift))
|
|
seenSavedIDs := make(map[int64]struct{}, len(req.Stargift))
|
|
for _, input := range req.Stargift {
|
|
ref, ok, err := r.starGiftRefFromInput(ctx, userID, input)
|
|
if err != nil || !ok || ref.Owner != (domain.Peer{Type: domain.PeerTypeUser, ID: userID}) {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
saved, found, err := r.deps.Gifts.GetSaved(ctx, ref)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
if !found || saved.ID <= 0 || saved.Owner != ref.Owner {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
if _, duplicate := seenSavedIDs[saved.ID]; duplicate {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
seenSavedIDs[saved.ID] = struct{}{}
|
|
refs = append(refs, ref)
|
|
// Official wire identities (user msg id, channel saved id or collectible
|
|
// slug) identify one durable aggregate and therefore one idempotency key.
|
|
commandParts = append(commandParts, fmt.Sprint(saved.ID))
|
|
}
|
|
result, err := r.deps.Gifts.Craft(ctx, domain.StarGiftCraftRequest{UserID: userID, Refs: refs,
|
|
CommandKey: "rpc:" + strings.Join(commandParts, ","), Date: int(r.clock.Now().Unix()),
|
|
OriginAuthKeyID: rawAuthKeyIDForOrigin(ctx), OriginSessionID: sessionIDOrZero(ctx)})
|
|
if err != nil {
|
|
return nil, starGiftLifecycleErr(err)
|
|
}
|
|
r.invalidateRPCProjectionForUser(userID)
|
|
updates := emptyGiftUpdates(r.clock.Now().Unix())
|
|
if result.Success {
|
|
updates = r.starGiftSendUpdates(ctx, userID, result.Send)
|
|
}
|
|
sourceUpdates := make([]tg.UpdateClass, 0, len(result.SourceEdits))
|
|
for _, edit := range result.SourceEdits {
|
|
if edit.UserID != userID {
|
|
continue
|
|
}
|
|
if update := tgOtherUpdateFromEvent(edit.Event); update != nil {
|
|
sourceUpdates = append(sourceUpdates, update)
|
|
updates.Users = append(updates.Users, r.usersForMessageUpdate(ctx, userID, edit.Message)...)
|
|
updates.Chats = append(updates.Chats, r.chatsForMessageUpdate(ctx, userID, edit.Message)...)
|
|
if edit.Event.Date > updates.Date {
|
|
updates.Date = edit.Event.Date
|
|
}
|
|
}
|
|
}
|
|
// Craft source edits reserve pts before the crafted output message, so keep
|
|
// them first in the immediate response as well. Other sessions receive the
|
|
// same durable edit events through outbox/difference.
|
|
updates.Updates = append(sourceUpdates, updates.Updates...)
|
|
if !result.Success {
|
|
updates.Updates = append(updates.Updates, &tg.UpdateStarGiftCraftFail{})
|
|
}
|
|
return updates, nil
|
|
}
|
|
|
|
func (r *Router) onPaymentsGetStarGiftAuctionState(ctx context.Context, req *tg.PaymentsGetStarGiftAuctionStateRequest) (*tg.PaymentsStarGiftAuctionState, error) {
|
|
if req == nil || r.deps.Gifts == nil {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
var giftID int64
|
|
var slug string
|
|
switch value := req.Auction.(type) {
|
|
case *tg.InputStarGiftAuction:
|
|
if value != nil {
|
|
giftID = value.GiftID
|
|
}
|
|
case *tg.InputStarGiftAuctionSlug:
|
|
if value != nil {
|
|
slug = value.Slug
|
|
}
|
|
default:
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
state, err := r.deps.Gifts.AuctionState(ctx, userID, giftID, slug, int(r.clock.Now().Unix()))
|
|
if err != nil {
|
|
return nil, starGiftLifecycleErr(err)
|
|
}
|
|
stateClass := tgStarGiftAuctionState(state)
|
|
if !state.Finished && req.Version == state.Version {
|
|
stateClass = &tg.StarGiftAuctionStateNotModified{}
|
|
}
|
|
return &tg.PaymentsStarGiftAuctionState{Gift: tgStarGift(state.Gift), State: stateClass,
|
|
UserState: tgStarGiftAuctionUserState(state.UserState), Timeout: 30, Users: r.auctionUsers(ctx, userID, state), Chats: []tg.ChatClass{}}, nil
|
|
}
|
|
|
|
func (r *Router) onPaymentsGetStarGiftActiveAuctions(ctx context.Context, req *tg.PaymentsGetStarGiftActiveAuctionsRequest) (tg.PaymentsStarGiftActiveAuctionsClass, error) {
|
|
if req == nil {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
if r.deps.Gifts == nil {
|
|
return &tg.PaymentsStarGiftActiveAuctions{Auctions: []tg.StarGiftActiveAuctionState{}, Users: []tg.UserClass{}, Chats: []tg.ChatClass{}}, nil
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
states, err := r.deps.Gifts.ActiveAuctions(ctx, userID, int(r.clock.Now().Unix()))
|
|
if err != nil {
|
|
return nil, starGiftLifecycleErr(err)
|
|
}
|
|
out := &tg.PaymentsStarGiftActiveAuctions{Auctions: make([]tg.StarGiftActiveAuctionState, 0, len(states)), Users: []tg.UserClass{}, Chats: []tg.ChatClass{}}
|
|
userIDs := make([]int64, 0)
|
|
for _, state := range states {
|
|
out.Auctions = append(out.Auctions, tg.StarGiftActiveAuctionState{Gift: tgStarGift(state.Gift),
|
|
State: tgStarGiftAuctionState(state), UserState: tgStarGiftAuctionUserState(state.UserState)})
|
|
userIDs = append(userIDs, state.TopBidders...)
|
|
}
|
|
out.Users = tgUsersForViewer(userID, r.domainUsersForIDs(ctx, userID, uniqueInt64(userIDs)))
|
|
return out, nil
|
|
}
|
|
|
|
func (r *Router) onPaymentsGetStarGiftAuctionAcquiredGifts(ctx context.Context, req *tg.PaymentsGetStarGiftAuctionAcquiredGiftsRequest) (*tg.PaymentsStarGiftAuctionAcquiredGifts, error) {
|
|
if req == nil || r.deps.Gifts == nil {
|
|
return nil, starGiftInvalidErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return nil, internalErr()
|
|
}
|
|
items, err := r.deps.Gifts.AuctionAcquired(ctx, userID, req.GiftID)
|
|
if err != nil {
|
|
return nil, starGiftLifecycleErr(err)
|
|
}
|
|
out := &tg.PaymentsStarGiftAuctionAcquiredGifts{Gifts: make([]tg.StarGiftAuctionAcquiredGift, 0, len(items)),
|
|
Users: []tg.UserClass{}, Chats: []tg.ChatClass{}}
|
|
userIDs, channelIDs := make([]int64, 0), make([]int64, 0)
|
|
for _, item := range items {
|
|
gift := tg.StarGiftAuctionAcquiredGift{NameHidden: item.NameHidden, Peer: tgPeer(item.Peer), Date: item.Date,
|
|
BidAmount: item.BidAmount, Round: item.Round, Pos: item.Pos}
|
|
if item.Message != "" {
|
|
gift.SetMessage(tg.TextWithEntities{Text: item.Message})
|
|
}
|
|
if item.GiftNum > 0 {
|
|
gift.SetGiftNum(item.GiftNum)
|
|
}
|
|
out.Gifts = append(out.Gifts, gift)
|
|
if item.Peer.Type == domain.PeerTypeUser {
|
|
userIDs = append(userIDs, item.Peer.ID)
|
|
} else {
|
|
channelIDs = append(channelIDs, item.Peer.ID)
|
|
}
|
|
}
|
|
out.Users = tgUsersForViewer(userID, r.domainUsersForIDs(ctx, userID, uniqueInt64(userIDs)))
|
|
out.Chats = r.tgChatsForChannelIDs(ctx, userID, uniqueInt64(channelIDs))
|
|
return out, nil
|
|
}
|
|
|
|
func (r *Router) onPaymentsToggleChatStarGiftNotifications(ctx context.Context, req *tg.PaymentsToggleChatStarGiftNotificationsRequest) (bool, error) {
|
|
if req == nil || r.deps.Gifts == nil || r.deps.Channels == nil {
|
|
return false, starGiftInvalidErr()
|
|
}
|
|
userID, _, err := r.currentUserID(ctx)
|
|
if err != nil {
|
|
return false, internalErr()
|
|
}
|
|
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, req.Peer)
|
|
if err != nil || peer.Type != domain.PeerTypeChannel {
|
|
return false, peerIDInvalidErr()
|
|
}
|
|
if err := r.checkStarGiftOwnerPermission(ctx, userID, peer); err != nil {
|
|
return false, err
|
|
}
|
|
if err := r.deps.Gifts.SetNotifications(ctx, userID, peer.ID, req.Enabled); err != nil {
|
|
return false, starGiftLifecycleErr(err)
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func tgStarGiftAuctionState(state domain.StarGiftAuction) tg.StarGiftAuctionStateClass {
|
|
if state.Finished {
|
|
out := &tg.StarGiftAuctionStateFinished{StartDate: state.StartDate, EndDate: state.EndDate, AveragePrice: state.AveragePrice}
|
|
if state.ListedCount > 0 {
|
|
out.SetListedCount(state.ListedCount)
|
|
}
|
|
return out
|
|
}
|
|
levels := make([]tg.AuctionBidLevel, 0, len(state.BidLevels))
|
|
for _, level := range state.BidLevels {
|
|
levels = append(levels, tg.AuctionBidLevel{Pos: level.Pos, Amount: level.Amount, Date: level.Date})
|
|
}
|
|
return &tg.StarGiftAuctionState{Version: state.Version, StartDate: state.StartDate, EndDate: state.EndDate,
|
|
MinBidAmount: state.MinBidAmount, BidLevels: levels, TopBidders: state.TopBidders,
|
|
NextRoundAt: state.NextRoundAt, LastGiftNum: state.LastGiftNum, GiftsLeft: state.GiftsLeft,
|
|
CurrentRound: state.CurrentRound, TotalRounds: state.TotalRounds,
|
|
Rounds: []tg.StarGiftAuctionRoundClass{&tg.StarGiftAuctionRound{Num: 1, Duration: state.RoundDuration}}}
|
|
}
|
|
|
|
func tgStarGiftAuctionUserState(state domain.StarGiftAuctionUserState) tg.StarGiftAuctionUserState {
|
|
out := tg.StarGiftAuctionUserState{AcquiredCount: state.AcquiredCount}
|
|
if state.Returned {
|
|
out.SetReturned(true)
|
|
}
|
|
if state.BidAmount > 0 {
|
|
out.SetBidAmount(state.BidAmount)
|
|
out.SetBidDate(state.BidDate)
|
|
out.SetMinBidAmount(state.MinBidAmount)
|
|
out.SetBidPeer(tgPeer(state.BidPeer))
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (r *Router) auctionUsers(ctx context.Context, viewerID int64, state domain.StarGiftAuction) []tg.UserClass {
|
|
ids := append([]int64(nil), state.TopBidders...)
|
|
if state.UserState.BidPeer.Type == domain.PeerTypeUser {
|
|
ids = append(ids, state.UserState.BidPeer.ID)
|
|
}
|
|
return tgUsersForViewer(viewerID, r.domainUsersForIDs(ctx, viewerID, uniqueInt64(ids)))
|
|
}
|
|
|
|
func (r *Router) starGiftSendUpdates(ctx context.Context, viewerID int64, send domain.SendPrivateTextResult) *tg.Updates {
|
|
message, event := send.SenderMessage, send.SenderEvent
|
|
if send.RecipientMessage.OwnerUserID == viewerID {
|
|
message, event = send.RecipientMessage, send.RecipientEvent
|
|
} else if send.SenderMessage.OwnerUserID != viewerID {
|
|
return emptyGiftUpdates(r.clock.Now().Unix())
|
|
}
|
|
if message.ID <= 0 {
|
|
return emptyGiftUpdates(r.clock.Now().Unix())
|
|
}
|
|
users := r.usersForMessageUpdate(ctx, viewerID, message)
|
|
chats := r.chatsForMessageUpdate(ctx, viewerID, message)
|
|
return tgPrivateMessageUpdates(event, message, 0, false, users, chats)
|
|
}
|
|
|
|
func (r *Router) starGiftTransferUpdates(ctx context.Context, viewerID int64, result domain.StarGiftTransferResult, ton bool) *tg.Updates {
|
|
updates := r.starGiftSendUpdates(ctx, viewerID, result.Send)
|
|
currency := domain.StarGiftCurrencyStars
|
|
if ton {
|
|
currency = domain.StarGiftCurrencyTON
|
|
}
|
|
appendStarGiftBalanceUpdate(updates, currency, result.Balance.Balance)
|
|
return updates
|
|
}
|
|
|
|
func appendStarGiftBalanceUpdate(updates *tg.Updates, currency domain.StarGiftCurrency, balance int64) {
|
|
if updates == nil {
|
|
return
|
|
}
|
|
var amount tg.StarsAmountClass = &tg.StarsAmount{Amount: balance}
|
|
if currency == domain.StarGiftCurrencyTON {
|
|
amount = &tg.StarsTonAmount{Amount: balance}
|
|
}
|
|
updates.Updates = append(updates.Updates, &tg.UpdateStarsBalance{Balance: amount})
|
|
}
|
|
|
|
func emptyGiftUpdates(date int64) *tg.Updates {
|
|
return &tg.Updates{Updates: []tg.UpdateClass{}, Users: []tg.UserClass{}, Chats: []tg.ChatClass{}, Date: int(date)}
|
|
}
|
|
|
|
func (r *Router) checkStarGiftOwnerPermission(ctx context.Context, userID int64, owner domain.Peer) error {
|
|
if owner == (domain.Peer{Type: domain.PeerTypeUser, ID: userID}) {
|
|
return nil
|
|
}
|
|
if owner.Type != domain.PeerTypeChannel || r.deps.Channels == nil {
|
|
return peerIDInvalidErr()
|
|
}
|
|
view, err := r.deps.Channels.ResolveChannel(ctx, userID, owner.ID)
|
|
if err != nil {
|
|
return channelInvalidErr(err)
|
|
}
|
|
if view.Self.Role == domain.ChannelRoleCreator || view.Self.Role == domain.ChannelRoleAdmin && view.Self.AdminRights.PostMessages {
|
|
return nil
|
|
}
|
|
return tgerr.New(400, "CHAT_ADMIN_REQUIRED")
|
|
}
|
|
|
|
func (r *Router) invalidateStarGiftOwner(owner domain.Peer) {
|
|
if owner.Type == domain.PeerTypeUser {
|
|
r.invalidateRPCProjectionForUser(owner.ID)
|
|
} else if owner.Type == domain.PeerTypeChannel {
|
|
r.invalidateRPCProjectionForChannel(owner.ID)
|
|
}
|
|
}
|
|
|
|
func starGiftRefValue(ref domain.SavedStarGiftRef) string {
|
|
if ref.Slug != "" {
|
|
return "slug:" + strings.ToLower(strings.TrimSpace(ref.Slug))
|
|
}
|
|
if ref.Owner.Type == domain.PeerTypeChannel {
|
|
return fmt.Sprintf("saved:%d", ref.SavedID)
|
|
}
|
|
return fmt.Sprintf("msg:%d", ref.MsgID)
|
|
}
|
|
|
|
func uniqueInt64(values []int64) []int64 {
|
|
seen := make(map[int64]struct{}, len(values))
|
|
out := make([]int64, 0, len(values))
|
|
for _, value := range values {
|
|
if value <= 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[value]; ok {
|
|
continue
|
|
}
|
|
seen[value] = struct{}{}
|
|
out = append(out, value)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func starGiftLifecycleErr(err error) error {
|
|
switch {
|
|
case errors.Is(err, domain.ErrStarGiftFormExpired):
|
|
return formExpiredErr()
|
|
case errors.Is(err, domain.ErrStarGiftFormPurposeInvalid):
|
|
return purposeInvalidErr()
|
|
case errors.Is(err, domain.ErrStarGiftFormAmountMismatch):
|
|
return starsFormAmountMismatchErr()
|
|
case errors.Is(err, domain.ErrStarsInsufficient):
|
|
return tgerr.New(400, "BALANCE_TOO_LOW")
|
|
case errors.Is(err, domain.ErrPremiumRequired):
|
|
return tgerr.New(400, "PREMIUM_ACCOUNT_REQUIRED")
|
|
case errors.Is(err, domain.ErrStarGiftOfferExpired):
|
|
return tgerr.New(400, "STARGIFT_OFFER_EXPIRED")
|
|
case errors.Is(err, domain.ErrStarGiftOwnerInvalid):
|
|
return tgerr.New(400, "STARGIFT_OWNER_INVALID")
|
|
case errors.Is(err, domain.ErrStarGiftWithdrawalUnavailable):
|
|
return tgerr.New(400, "STARGIFT_WITHDRAWAL_UNAVAILABLE")
|
|
case errors.Is(err, domain.ErrStarGiftCraftUnavailable):
|
|
return tgerr.New(400, "STARGIFT_CRAFT_UNAVAILABLE")
|
|
case errors.Is(err, domain.ErrStarGiftNotFound), errors.Is(err, domain.ErrStarGiftResaleUnavailable),
|
|
errors.Is(err, domain.ErrStarGiftTransferUnavailable), errors.Is(err, domain.ErrStarGiftOfferInvalid),
|
|
errors.Is(err, domain.ErrStarGiftAuctionUnavailable),
|
|
errors.Is(err, domain.ErrStarGiftUnavailable), errors.Is(err, domain.ErrStarGiftInvalid),
|
|
errors.Is(err, domain.ErrStarGiftCollectibleUnavailable):
|
|
return starGiftInvalidErr()
|
|
default:
|
|
return internalErr()
|
|
}
|
|
}
|