owpengram-server/internal/rpc/payments_star_gifts.go

684 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package rpc
import (
"context"
"errors"
"github.com/gotd/td/tg"
"github.com/gotd/td/tgerr"
"go.uber.org/zap"
"telesrv/internal/domain"
)
// Star gift(payments.* 礼物 RPC):目录 / 购买表单 / 发送 / 收礼列表 / 展示切换 / 转换回 Stars。
// 扣费经 r.deps.Stars 账本;用户礼物走私聊服务消息,频道礼物只落 saved gifts + admin log。
func starGiftInvalidErr() error { return tgerr.New(400, "STARGIFT_INVALID") }
// onPaymentsGetStarGifts 返回可购买礼物目录(hash 命中返回 NotModified)。
func (r *Router) onPaymentsGetStarGifts(ctx context.Context, hash int) (tg.PaymentsStarGiftsClass, error) {
if r.deps.Gifts == nil {
return &tg.PaymentsStarGifts{Gifts: []tg.StarGiftClass{}, Chats: []tg.ChatClass{}, Users: []tg.UserClass{}}, nil
}
catalogHash, err := r.deps.Gifts.CatalogHash(ctx)
if err != nil {
return nil, internalErr()
}
catalog, err := r.deps.Gifts.Catalog(ctx)
if err != nil {
return nil, internalErr()
}
// 刻意不返回 starGiftsNotModified:目录只有少量静态礼物,而 DrKLO 在 force-stop/重装后
// 会保留 catalog hash 但丢失礼物缓存——一旦命中 hash 返回 NotModified,送礼选择器就永远空。
// 始终回完整目录(带宽可忽略),保证客户端无论缓存状态都能渲染礼物网格。
_ = catalogHash
return &tg.PaymentsStarGifts{
Hash: catalogHash,
Gifts: tgStarGifts(catalog),
Chats: []tg.ChatClass{},
Users: []tg.UserClass{},
}, nil
}
// onPaymentsGetPaymentForm 仅处理 inputInvoiceStarGift:返回 paymentFormStarGift。
// 崩溃约束:star gift invoice 必须返 paymentFormStarGift#b425cfe1(TDesktop 单分支 match),
// Invoice.Prices 必须非空(DrKLO/TDesktop 读 prices.front())。
func (r *Router) onPaymentsGetPaymentForm(ctx context.Context, req *tg.PaymentsGetPaymentFormRequest) (tg.PaymentsPaymentFormClass, error) {
if req == nil {
return nil, inputRequestInvalidErr()
}
inv, ok := req.Invoice.(*tg.InputInvoiceStarGift)
if !ok {
return nil, notImplementedErr()
}
if r.deps.Gifts == nil {
return nil, notImplementedErr()
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if _, err := r.checkedDomainPeerFromInputPeer(ctx, userID, inv.Peer); err != nil {
return nil, err
}
gift, err := r.starGiftFromCatalog(ctx, inv.GiftID)
if err != nil {
return nil, err
}
return &tg.PaymentsPaymentFormStarGift{
FormID: starGiftFormID(userID, inv.GiftID),
Invoice: tg.Invoice{
Currency: "XTR",
Prices: []tg.LabeledPrice{{Label: giftPriceLabel(gift), Amount: gift.Stars}},
},
}, nil
}
// onPaymentsSendStarsForm 仅处理 inputInvoiceStarGift:Debit→投递/记账,
// 返回 paymentResult{updates}(含 updateStarsBalance;用户礼物还含私聊服务消息)。失败补偿退款。
// 崩溃约束:必须返回合法 paymentResult{非空 Updates}(DrKLO 强转)。
func (r *Router) onPaymentsSendStarsForm(ctx context.Context, req *tg.PaymentsSendStarsFormRequest) (tg.PaymentsPaymentResultClass, error) {
if req == nil {
return nil, inputRequestInvalidErr()
}
inv, ok := req.Invoice.(*tg.InputInvoiceStarGift)
if !ok {
return nil, notImplementedErr()
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
peer, err := r.checkedDomainPeerFromInputPeer(ctx, userID, inv.Peer)
if err != nil {
return nil, err
}
if (peer.Type != domain.PeerTypeUser && peer.Type != domain.PeerTypeChannel) || peer.ID == 0 {
return nil, peerIDInvalidErr()
}
if r.deps.Stars == nil || r.deps.Gifts == nil {
return nil, notImplementedErr()
}
if peer.Type == domain.PeerTypeUser && r.deps.Messages == nil {
return nil, notImplementedErr()
}
if peer.Type == domain.PeerTypeChannel && r.deps.Channels == nil {
return nil, notImplementedErr()
}
gift, err := r.starGiftFromCatalog(ctx, inv.GiftID)
if err != nil {
return nil, err
}
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)
if err != nil {
return nil, starsErr(err)
}
var updates *tg.Updates
switch peer.Type {
case domain.PeerTypeUser:
updates, err = r.sendStarGiftToUser(ctx, userID, peer.ID, gift, inv.HideName, giftMessage)
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)
return nil, internalErr()
}
// 4. 构建送礼人 Updates(服务消息 + updateStarsBalance)。
if updates != nil {
updates.Updates = append(updates.Updates, &tg.UpdateStarsBalance{Balance: &tg.StarsAmount{Amount: balance.Balance}})
} else {
updates = &tg.Updates{
Updates: []tg.UpdateClass{&tg.UpdateStarsBalance{Balance: &tg.StarsAmount{Amount: balance.Balance}}},
Users: []tg.UserClass{},
Chats: []tg.ChatClass{},
Date: int(r.clock.Now().Unix()),
}
}
return &tg.PaymentsPaymentResult{Updates: updates}, nil
}
func (r *Router) sendStarGiftToUser(ctx context.Context, senderID, recipientID int64, gift domain.StarGift, hideName bool, message string) (*tg.Updates, error) {
// 2. 投递礼物服务消息到收礼人私聊(双盒 + 推送)。
send, err := r.deliverStarGift(ctx, senderID, recipientID, gift, hideName, message)
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,
}); err != nil {
return nil, err
}
// 收礼人 stargifts_count 变化 → 失效其 userFull 投影,资料页 Gifts 区段才会出现。
r.invalidateRPCProjectionForUser(recipientID)
users := r.usersForMessageUpdate(ctx, senderID, send.SenderMessage)
chats := r.chatsForMessageUpdate(ctx, senderID, send.SenderMessage)
return tgPrivateMessageUpdates(send.SenderEvent, send.SenderMessage, 0, false, users, chats), nil
}
func (r *Router) sendStarGiftToChannel(ctx context.Context, senderID, channelID int64, gift domain.StarGift, hideName bool, message string) (*tg.Updates, error) {
now := int(r.clock.Now().Unix())
sticker := gift.Sticker
action := domain.ChannelMessageAction{
Type: domain.ChannelActionStarGift,
StarGift: &domain.MessageStarGiftAction{
GiftID: gift.ID,
Stars: gift.Stars,
ConvertStars: gift.ConvertStars,
Title: gift.Title,
Sticker: &sticker,
Message: message,
FromUserID: senderID,
NameHidden: hideName,
Saved: true,
},
}
savedID, err := r.deps.Gifts.RecordSavedGift(ctx, domain.SavedStarGift{
Owner: domain.Peer{Type: domain.PeerTypeChannel, ID: channelID},
FromUserID: senderID,
GiftID: gift.ID,
MsgID: 0,
SavedID: 0,
Date: now,
NameHidden: hideName,
Unsaved: false,
ConvertStars: gift.ConvertStars,
Message: message,
})
if err != nil {
return nil, err
}
action.StarGift.PeerChannelID = channelID
action.StarGift.SavedID = savedID
if err := r.deps.Channels.AppendStarGiftAdminLog(ctx, channelID, senderID, savedID, now, action); err != nil {
r.log.Warn("channel_star_gift_admin_log_failed",
zap.Int64("sender_id", senderID),
zap.Int64("channel_id", channelID),
zap.Int64("saved_id", savedID),
zap.Error(err),
)
}
r.invalidateRPCProjectionForChannel(channelID)
return nil, nil
}
// deliverStarGift 经 SendPrivateText 把 messageActionStarGift 服务消息投递到收礼人私聊。
func (r *Router) deliverStarGift(ctx context.Context, senderID, recipientID int64, gift domain.StarGift, hideName bool, message string) (domain.SendPrivateTextResult, error) {
recipientBlocked, err := r.peerBlocksUser(ctx, senderID, recipientID)
if err != nil {
return domain.SendPrivateTextResult{}, err
}
authKeyID, _ := AuthKeyIDFrom(ctx)
sessionID, _ := SessionIDFrom(ctx)
sticker := gift.Sticker
media := &domain.MessageMedia{
Kind: domain.MessageMediaKindService,
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,
},
},
}
return r.deps.Messages.SendPrivateText(ctx, senderID, domain.SendPrivateTextRequest{
SenderUserID: senderID,
RecipientUserID: recipientID,
RandomID: randomNonZeroInt64(),
Media: media,
Silent: false,
Date: int(r.clock.Now().Unix()),
OriginAuthKeyID: authKeyID,
OriginSessionID: sessionID,
RecipientBlocked: recipientBlocked,
})
}
// 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 {
r.log.Error("star gift refund failed", zap.Int64("user_id", userID), zap.Int64("gift_id", gift.ID), zap.Error(err))
}
}
// onPaymentsGetSavedStarGifts 返回某 peer 收到的礼物列表(末页省略 next_offset)。
func (r *Router) onPaymentsGetSavedStarGifts(ctx context.Context, req *tg.PaymentsGetSavedStarGiftsRequest) (*tg.PaymentsSavedStarGifts, 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 emptySavedStarGifts(), nil
}
page, err := r.deps.Gifts.ListSaved(ctx, owner, req.ExcludeUnsaved, req.Offset, req.Limit)
if err != nil {
return nil, internalErr()
}
return r.tgSavedStarGiftsResponse(ctx, userID, page.Gifts, page.Count, page.NextOffset), nil
}
// onPaymentsGetSavedStarGift 按 InputSavedStarGift 引用取指定礼物。
func (r *Router) onPaymentsGetSavedStarGift(ctx context.Context, refs []tg.InputSavedStarGiftClass) (*tg.PaymentsSavedStarGifts, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return nil, internalErr()
}
if r.deps.Gifts == nil {
return emptySavedStarGifts(), nil
}
gifts := make([]domain.SavedStarGift, 0, len(refs))
for _, ref := range refs {
dref, ok, err := r.starGiftRefFromInput(ctx, userID, ref)
if err != nil {
return nil, err
}
if !ok {
continue
}
g, found, err := r.deps.Gifts.GetSaved(ctx, dref)
if err != nil {
return nil, internalErr()
}
if found && !g.Converted {
gifts = append(gifts, g)
}
}
return r.tgSavedStarGiftsResponse(ctx, userID, gifts, len(gifts), ""), nil
}
// onPaymentsSaveStarGift 切换礼物在资料的展示(unsave=true 隐藏)。
func (r *Router) onPaymentsSaveStarGift(ctx context.Context, req *tg.PaymentsSaveStarGiftRequest) (bool, error) {
if req == nil {
return false, inputRequestInvalidErr()
}
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if r.deps.Gifts == nil {
return false, notImplementedErr()
}
ref, ok, err := r.starGiftRefFromInput(ctx, userID, req.Stargift)
if err != nil {
return false, err
}
if !ok {
return false, starGiftInvalidErr()
}
if err := r.ensureCanManageStarGiftOwner(ctx, userID, ref.Owner); err != nil {
return false, err
}
updated, err := r.deps.Gifts.ToggleSaved(ctx, ref, req.Unsave)
if err != nil {
return false, internalErr()
}
if !updated {
return false, starGiftInvalidErr()
}
// 隐藏/展示切换改变展示礼物数 → 失效 owner full 投影。
r.invalidateStarGiftOwnerProjection(ref.Owner)
return true, nil
}
// onPaymentsConvertStarGift 把收到的礼物转换回 Stars(Credit + 标记 converted)。
func (r *Router) onPaymentsConvertStarGift(ctx context.Context, ref tg.InputSavedStarGiftClass) (bool, error) {
userID, _, err := r.currentUserID(ctx)
if err != nil {
return false, internalErr()
}
if r.deps.Gifts == nil || r.deps.Stars == nil {
return false, notImplementedErr()
}
dref, ok, err := r.starGiftRefFromInput(ctx, userID, ref)
if err != nil {
return false, err
}
if !ok {
return false, starGiftInvalidErr()
}
if dref.Owner.Type == domain.PeerTypeChannel {
return false, notImplementedErr()
}
saved, err := r.deps.Gifts.Convert(ctx, dref)
if err != nil {
switch {
case errors.Is(err, domain.ErrStarGiftNotFound):
return false, starGiftInvalidErr()
case errors.Is(err, domain.ErrStarGiftAlreadyConverted):
return false, starGiftInvalidErr()
default:
return false, internalErr()
}
}
if saved.ConvertStars > 0 {
fromPeer := domain.Peer{Type: domain.PeerTypeUser, ID: saved.FromUserID}
if _, err := r.deps.Stars.Credit(ctx, userID, saved.ConvertStars, domain.StarsReasonGift, fromPeer, "Star gift conversion", ""); err != nil {
r.log.Error("star gift convert credit failed", zap.Int64("user_id", userID), zap.Int("msg_id", dref.MsgID), zap.Error(err))
return false, internalErr()
}
}
// 转换移除一份展示礼物 → 失效 owner full 投影。
r.invalidateStarGiftOwnerProjection(dref.Owner)
return true, nil
}
// ---- helpers ----
func (r *Router) starGiftFromCatalog(ctx context.Context, giftID int64) (domain.StarGift, error) {
gift, ok, err := r.deps.Gifts.GiftByID(ctx, giftID)
if err != nil {
return domain.StarGift{}, internalErr()
}
if !ok {
return domain.StarGift{}, starGiftInvalidErr()
}
return gift, nil
}
// starGiftOwnerPeer 解析 getSavedStarGifts 的 owner:inputPeerSelf/空 → 自己,否则解析 user/channel peer。
func (r *Router) starGiftOwnerPeer(ctx context.Context, userID int64, peer tg.InputPeerClass) (domain.Peer, error) {
switch peer.(type) {
case nil, *tg.InputPeerSelf:
return domain.Peer{Type: domain.PeerTypeUser, ID: userID}, nil
}
resolved, err := r.checkedDomainPeerFromInputPeer(ctx, userID, peer)
if err != nil {
return domain.Peer{}, err
}
if (resolved.Type != domain.PeerTypeUser && resolved.Type != domain.PeerTypeChannel) || resolved.ID == 0 {
return domain.Peer{}, peerIDInvalidErr()
}
return resolved, nil
}
func (r *Router) starGiftRefFromInput(ctx context.Context, userID int64, ref tg.InputSavedStarGiftClass) (domain.SavedStarGiftRef, bool, error) {
switch v := ref.(type) {
case *tg.InputSavedStarGiftUser:
if v == nil || v.MsgID <= 0 {
return domain.SavedStarGiftRef{}, false, nil
}
return domain.SavedStarGiftRef{
Owner: domain.Peer{Type: domain.PeerTypeUser, ID: userID},
MsgID: v.MsgID,
}, true, nil
case *tg.InputSavedStarGiftChat:
if v == nil || v.SavedID <= 0 {
return domain.SavedStarGiftRef{}, false, nil
}
owner, err := r.checkedDomainPeerFromInputPeer(ctx, userID, v.Peer)
if err != nil {
return domain.SavedStarGiftRef{}, false, err
}
if owner.Type != domain.PeerTypeChannel || owner.ID == 0 {
return domain.SavedStarGiftRef{}, false, peerIDInvalidErr()
}
return domain.SavedStarGiftRef{Owner: owner, SavedID: v.SavedID}, true, nil
default:
return domain.SavedStarGiftRef{}, false, nil
}
}
func (r *Router) ensureCanManageStarGiftOwner(ctx context.Context, userID int64, owner domain.Peer) error {
if owner.Type != domain.PeerTypeChannel {
return nil
}
if r.deps.Channels == nil {
return notImplementedErr()
}
view, err := r.deps.Channels.ResolveChannel(ctx, userID, owner.ID)
if err != nil {
return channelInvalidErr(err)
}
if !channelMemberIsAdmin(view.Self) {
return channelInvalidErr(domain.ErrChannelAdminRequired)
}
return nil
}
func (r *Router) invalidateStarGiftOwnerProjection(owner domain.Peer) {
switch owner.Type {
case domain.PeerTypeUser:
r.invalidateRPCProjectionForUser(owner.ID)
case domain.PeerTypeChannel:
r.invalidateRPCProjectionForChannel(owner.ID)
}
}
func (r *Router) tgSavedStarGiftsResponse(ctx context.Context, viewerUserID int64, gifts []domain.SavedStarGift, count int, nextOffset string) *tg.PaymentsSavedStarGifts {
catalog := r.resolveStarGiftCatalog(ctx, gifts)
out := &tg.PaymentsSavedStarGifts{
Count: count,
Gifts: tgSavedStarGifts(gifts, catalog),
Chats: []tg.ChatClass{},
}
if ids := savedStarGiftUserIDs(gifts); len(ids) > 0 {
out.Users = tgUsersForViewer(viewerUserID, r.domainUsersForIDs(ctx, viewerUserID, ids))
} else {
out.Users = []tg.UserClass{}
}
if nextOffset != "" {
out.SetNextOffset(nextOffset)
}
return out
}
func emptySavedStarGifts() *tg.PaymentsSavedStarGifts {
return &tg.PaymentsSavedStarGifts{
Gifts: []tg.SavedStarGift{},
Chats: []tg.ChatClass{},
Users: []tg.UserClass{},
}
}
// tgStarGifts 把目录投影为 []tg.StarGiftClass。
func tgStarGifts(catalog []domain.StarGift) []tg.StarGiftClass {
out := make([]tg.StarGiftClass, 0, len(catalog))
for _, g := range catalog {
out = append(out, tgStarGift(g))
}
return out
}
// tgStarGift 把目录项投影为 tg.StarGift(Sticker 须为带 sticker 属性的有效 Document)。
func tgStarGift(g domain.StarGift) *tg.StarGift {
gift := &tg.StarGift{
ID: g.ID,
Sticker: tgDocument(g.Sticker),
Stars: g.Stars,
ConvertStars: g.ConvertStars,
}
if g.Title != "" {
gift.SetTitle(g.Title)
}
return gift
}
// tgMessageActionStarGift 把礼物服务消息载荷投影为 messageActionStarGift。
func tgMessageActionStarGift(in *domain.MessageStarGiftAction) tg.MessageActionClass {
if in == nil {
return &tg.MessageActionEmpty{}
}
gift := &tg.StarGift{
ID: in.GiftID,
Stars: in.Stars,
ConvertStars: in.ConvertStars,
}
if in.Sticker != nil {
gift.Sticker = tgDocument(*in.Sticker)
} else {
gift.Sticker = &tg.DocumentEmpty{}
}
if in.Title != "" {
gift.SetTitle(in.Title)
}
action := &tg.MessageActionStarGift{Gift: gift}
if in.NameHidden {
action.NameHidden = true
}
if in.Saved {
action.Saved = true
}
if in.Converted {
action.Converted = true
}
if in.ConvertStars > 0 {
action.SetConvertStars(in.ConvertStars)
}
if in.Message != "" {
action.SetMessage(tg.TextWithEntities{Text: in.Message})
}
if in.FromUserID != 0 && !in.NameHidden {
action.SetFromID(&tg.PeerUser{UserID: in.FromUserID})
}
if in.PeerUserID != 0 {
action.SetPeer(&tg.PeerUser{UserID: in.PeerUserID})
} else if in.PeerChannelID != 0 {
action.SetPeer(&tg.PeerChannel{ChannelID: in.PeerChannelID})
}
if in.SavedID != 0 {
action.SetSavedID(in.SavedID)
}
return action
}
// resolveStarGiftCatalog 解析这批 saved gift 涉及的目录项(giftID → StarGift),供下发完整贴纸/星价。
func (r *Router) resolveStarGiftCatalog(ctx context.Context, gifts []domain.SavedStarGift) map[int64]domain.StarGift {
out := make(map[int64]domain.StarGift, len(gifts))
if r.deps.Gifts == nil {
return out
}
for _, g := range gifts {
if _, ok := out[g.GiftID]; ok {
continue
}
if gift, found, err := r.deps.Gifts.GiftByID(ctx, g.GiftID); err == nil && found {
out[g.GiftID] = gift
}
}
return out
}
// tgSavedStarGifts 把已收到礼物实例投影为 []tg.SavedStarGift。
func tgSavedStarGifts(gifts []domain.SavedStarGift, catalog map[int64]domain.StarGift) []tg.SavedStarGift {
out := make([]tg.SavedStarGift, 0, len(gifts))
for _, g := range gifts {
item := tg.SavedStarGift{
Date: g.Date,
Gift: tgSavedStarGiftGift(g, catalog),
}
if g.NameHidden {
item.NameHidden = true
}
if g.Unsaved {
item.Unsaved = true
}
if g.FromUserID != 0 && !g.NameHidden {
item.SetFromID(&tg.PeerUser{UserID: g.FromUserID})
}
if g.Owner.Type == domain.PeerTypeUser && g.MsgID > 0 {
item.SetMsgID(g.MsgID)
}
if g.Owner.Type == domain.PeerTypeChannel && g.SavedID > 0 {
item.SetSavedID(g.SavedID)
}
if g.ConvertStars > 0 {
item.SetConvertStars(g.ConvertStars)
}
if g.Message != "" {
item.SetMessage(tg.TextWithEntities{Text: g.Message})
}
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 {
out := tgStarGift(gift)
out.ConvertStars = g.ConvertStars
return out
}
return &tg.StarGift{
ID: g.GiftID,
Sticker: &tg.DocumentEmpty{},
Stars: 0,
ConvertStars: g.ConvertStars,
}
}
func savedStarGiftUserIDs(gifts []domain.SavedStarGift) []int64 {
seen := make(map[int64]struct{}, len(gifts))
ids := make([]int64, 0, len(gifts))
for _, g := range gifts {
if g.FromUserID == 0 || g.NameHidden {
continue
}
if _, ok := seen[g.FromUserID]; ok {
continue
}
seen[g.FromUserID] = struct{}{}
ids = append(ids, g.FromUserID)
}
return ids
}
func starGiftFormID(userID, giftID int64) int64 {
id := userID*0x9e3779b1 ^ (giftID << 7) ^ 0x5347494654
if id == 0 {
id = 0x5347
}
return id
}
func giftPriceLabel(g domain.StarGift) string {
if g.Title != "" {
return g.Title
}
return "Gift"
}
func clampGiftMessage(s string) string {
runes := []rune(s)
if len(runes) > domain.MaxStarGiftMessageRunes {
return string(runes[:domain.MaxStarGiftMessageRunes])
}
return s
}