chore: refresh gramsrv public release

This commit is contained in:
A 2026-06-30 14:37:43 +08:00
parent 75cebe8dbf
commit 70b6820474
1274 changed files with 378751 additions and 59919 deletions

View file

@ -0,0 +1,684 @@
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
}