feat: sync official star gift lifecycle tooling

Sync telesrv 4c0e2d9 (feat: complete official star gift lifecycle and admin tooling).

Public adjustments: skipped private docs/deploy-nginx/README source changes, kept iamxvbaba/td public dependency, replaced local/private sample IP and orange seed label.
This commit is contained in:
A 2026-07-19 03:11:35 +08:00
parent f2c2fd0236
commit 14bf7d1e20
92 changed files with 14768 additions and 727 deletions

View file

@ -550,6 +550,9 @@ const (
ChannelActionPaidMessagesPrice ChannelMessageActionType = "paid_messages_price"
// ChannelActionStarGift 映射 messageActionStarGift:频道礼物的 admin-log 快照。
ChannelActionStarGift ChannelMessageActionType = "star_gift"
// ChannelActionStarGiftUnique 映射 messageActionStarGiftUnique:collectible
// 升级、转赠等所有权变更只进入 Recent Actions,不伪造频道历史/pts。
ChannelActionStarGiftUnique ChannelMessageActionType = "star_gift_unique"
// ChannelActionSetChatWallpaper 映射 messageActionSetChatWallPaper:频道外观页设置 wallpaper。
ChannelActionSetChatWallpaper ChannelMessageActionType = "set_chat_wallpaper"
)
@ -583,7 +586,8 @@ type ChannelMessageAction struct {
Incompleted []int
TodoItems []MessageTodoItem
// StarGift 仅 star_gift 服务消息使用。
StarGift *MessageStarGiftAction
StarGift *MessageStarGiftAction
StarGiftUnique *MessageStarGiftUniqueAction
// Wallpaper 仅 set_chat_wallpaper 服务消息使用。
Wallpaper *Wallpaper
// Photo 仅 chat_edit_photo 服务消息使用。

View file

@ -564,7 +564,9 @@ const (
// MessageServiceActionStarGiftUnique maps messageActionStarGiftUnique. The
// immutable collectible snapshot is carried by the service message so an
// exact replay/difference never depends on mutable catalog state.
MessageServiceActionStarGiftUnique MessageServiceActionKind = "star_gift_unique"
MessageServiceActionStarGiftUnique MessageServiceActionKind = "star_gift_unique"
MessageServiceActionStarGiftOffer MessageServiceActionKind = "star_gift_offer"
MessageServiceActionStarGiftOfferDeclined MessageServiceActionKind = "star_gift_offer_declined"
)
// MessagePhoneCallAction 是 messageActionPhoneCall 的协议中立载荷。
@ -612,51 +614,87 @@ type MessageRequestedPeerAction struct {
// MessageServiceAction 是私聊服务消息动作的协议中立表示。
type MessageServiceAction struct {
Kind MessageServiceActionKind `json:"kind"`
Photo *Photo `json:"photo,omitempty"`
Call *MessagePhoneCallAction `json:"call,omitempty"`
ConferenceCall *MessageConferenceCallAction `json:"conference_call,omitempty"`
BotAllowed *MessageBotAllowedAction `json:"bot_allowed,omitempty"`
WebViewData *MessageWebViewDataAction `json:"web_view_data,omitempty"`
RequestedPeer *MessageRequestedPeerAction `json:"requested_peer,omitempty"`
ChatThemeEmoticon string `json:"chat_theme_emoticon,omitempty"`
StarGift *MessageStarGiftAction `json:"star_gift,omitempty"`
StarGiftUnique *MessageStarGiftUniqueAction `json:"star_gift_unique,omitempty"`
Kind MessageServiceActionKind `json:"kind"`
Photo *Photo `json:"photo,omitempty"`
Call *MessagePhoneCallAction `json:"call,omitempty"`
ConferenceCall *MessageConferenceCallAction `json:"conference_call,omitempty"`
BotAllowed *MessageBotAllowedAction `json:"bot_allowed,omitempty"`
WebViewData *MessageWebViewDataAction `json:"web_view_data,omitempty"`
RequestedPeer *MessageRequestedPeerAction `json:"requested_peer,omitempty"`
ChatThemeEmoticon string `json:"chat_theme_emoticon,omitempty"`
StarGift *MessageStarGiftAction `json:"star_gift,omitempty"`
StarGiftUnique *MessageStarGiftUniqueAction `json:"star_gift_unique,omitempty"`
StarGiftOffer *MessageStarGiftOfferAction `json:"star_gift_offer,omitempty"`
StarGiftOfferDeclined *MessageStarGiftOfferDeclinedAction `json:"star_gift_offer_declined,omitempty"`
}
// MessageStarGiftAction 是 messageActionStarGift 的协议中立载荷:内嵌礼物快照(贴纸/星价)
// 使收礼人无需额外拉取即可渲染。PeerUserID/PeerChannelID 为收礼方;NameHidden 时下发不暴露 from。
type MessageStarGiftAction struct {
GiftID int64 `json:"gift_id"`
Stars int64 `json:"stars"`
ConvertStars int64 `json:"convert_stars,omitempty"`
Title string `json:"title,omitempty"`
Sticker *Document `json:"sticker,omitempty"`
Message string `json:"message,omitempty"`
FromUserID int64 `json:"from_user_id,omitempty"`
PeerUserID int64 `json:"peer_user_id,omitempty"`
PeerChannelID int64 `json:"peer_channel_id,omitempty"`
SavedID int64 `json:"saved_id,omitempty"`
NameHidden bool `json:"name_hidden,omitempty"`
Saved bool `json:"saved,omitempty"`
Converted bool `json:"converted,omitempty"`
CanUpgrade bool `json:"can_upgrade,omitempty"`
PrepaidUpgrade bool `json:"prepaid_upgrade,omitempty"`
UpgradeStars int64 `json:"upgrade_stars,omitempty"`
UpgradeMsgID int `json:"upgrade_msg_id,omitempty"`
GiftID int64 `json:"gift_id"`
Stars int64 `json:"stars"`
ConvertStars int64 `json:"convert_stars,omitempty"`
Title string `json:"title,omitempty"`
Sticker *Document `json:"sticker,omitempty"`
Message string `json:"message,omitempty"`
FromUserID int64 `json:"from_user_id,omitempty"`
PeerUserID int64 `json:"peer_user_id,omitempty"`
PeerChannelID int64 `json:"peer_channel_id,omitempty"`
SavedID int64 `json:"saved_id,omitempty"`
NameHidden bool `json:"name_hidden,omitempty"`
Saved bool `json:"saved,omitempty"`
Converted bool `json:"converted,omitempty"`
CanUpgrade bool `json:"can_upgrade,omitempty"`
PrepaidUpgrade bool `json:"prepaid_upgrade,omitempty"`
PrepaidUpgradeHash string `json:"prepaid_upgrade_hash,omitempty"`
UpgradeSeparate bool `json:"upgrade_separate,omitempty"`
// UpgradePriceStars belongs to the inner StarGift.upgrade_stars field and
// is the price of a normal paid upgrade. UpgradeStars below belongs to the
// outer messageActionStarGift and is only the amount already prepaid by the
// sender. TDesktop uses these two fields to choose the paid vs free flow.
UpgradePriceStars int64 `json:"upgrade_price_stars,omitempty"`
UpgradeStars int64 `json:"upgrade_stars,omitempty"`
UpgradeMsgID int `json:"upgrade_msg_id,omitempty"`
GiftMsgID int `json:"gift_msg_id,omitempty"`
GiftNum int `json:"gift_num,omitempty"`
AuctionAcquired bool `json:"auction_acquired,omitempty"`
To Peer `json:"to,omitempty"`
}
// MessageStarGiftUniqueAction is the protocol-neutral payload of an upgrade
// service message. Commercial transfer/resale/export fields are intentionally
// absent from the collectibles mainline.
type MessageStarGiftUniqueAction struct {
Gift UniqueStarGift `json:"gift"`
FromUserID int64 `json:"from_user_id,omitempty"`
Peer Peer `json:"peer"`
SavedID int64 `json:"saved_id,omitempty"`
Upgrade bool `json:"upgrade,omitempty"`
Saved bool `json:"saved,omitempty"`
PrepaidUpgrade bool `json:"prepaid_upgrade,omitempty"`
Gift UniqueStarGift `json:"gift"`
FromUserID int64 `json:"from_user_id,omitempty"`
Peer Peer `json:"peer"`
SavedID int64 `json:"saved_id,omitempty"`
Upgrade bool `json:"upgrade,omitempty"`
Saved bool `json:"saved,omitempty"`
PrepaidUpgrade bool `json:"prepaid_upgrade,omitempty"`
Transferred bool `json:"transferred,omitempty"`
Refunded bool `json:"refunded,omitempty"`
Assigned bool `json:"assigned,omitempty"`
FromOffer bool `json:"from_offer,omitempty"`
Craft bool `json:"craft,omitempty"`
CanExportAt int `json:"can_export_at,omitempty"`
TransferStars int64 `json:"transfer_stars,omitempty"`
ResaleAmount *StarGiftAmount `json:"resale_amount,omitempty"`
CanTransferAt int `json:"can_transfer_at,omitempty"`
CanResellAt int `json:"can_resell_at,omitempty"`
DropOriginalDetailsStars int64 `json:"drop_original_details_stars,omitempty"`
CanCraftAt int `json:"can_craft_at,omitempty"`
}
type MessageStarGiftOfferAction struct {
Gift UniqueStarGift `json:"gift"`
Price StarGiftAmount `json:"price"`
ExpiresAt int `json:"expires_at"`
Accepted bool `json:"accepted,omitempty"`
Declined bool `json:"declined,omitempty"`
}
type MessageStarGiftOfferDeclinedAction struct {
Gift UniqueStarGift `json:"gift"`
Price StarGiftAmount `json:"price"`
Expired bool `json:"expired,omitempty"`
}
// MessageMedia 是一条消息媒体载荷的业务表示(落库为消息行上的 JSONB 快照)。

View file

@ -24,29 +24,85 @@ type StarGift struct {
UpgradeIssued int // 当前已发行数量
Title string // 可选标题
Sticker Document // 礼物贴纸快照(tg 投影必须是带 sticker 属性的有效 Document,否则客户端丢弃)
// Layer 228 regular-gift shape. Static release facts live in the immutable
// catalog revision; AvailabilityRemains/AvailabilityResale are the current
// inventory projection maintained on the catalog aggregate.
Limited bool
SoldOut bool
Birthday bool
RequirePremium bool
LimitedPerUser bool
PeerColorAvailable bool
Auction bool
AvailabilityRemains int
AvailabilityTotal int
AvailabilityResale int64
FirstSaleDate int
LastSaleDate int
ResellMinStars int64
ReleasedBy Peer
PerUserTotal int
PerUserRemains int
LockedUntilDate int
AuctionSlug string
GiftsPerRound int
AuctionStartDate int
UpgradeVariants int
Background *StarGiftBackground
}
// StarGiftBackground is the release-level palette used by auction cards and
// gift previews before a collectible backdrop is selected.
type StarGiftBackground struct {
CenterColor int
EdgeColor int
TextColor int
}
// SavedStarGift 是一条已收到的礼物实例(peer_star_gifts 一行)。
type SavedStarGift struct {
ID int64
Owner Peer // 收礼 peer(user/channel)
FromUserID int64 // 送礼人(匿名也保留真实值供账本,下发时按 NameHidden 决定是否暴露)
GiftID int64 // → StarGift.ID
RevisionID int64 // → star_gift_catalog_revisions.id,历史查询必须按此版本投影
MsgID int // 用户礼物的私聊 msg_id;频道礼物不进历史,固定为 0
SavedID int64 // 频道礼物 inputSavedStarGiftChat.saved_id;用户礼物为 0
Date int // 收到时刻 Unix 秒
NameHidden bool // 送礼人请求隐藏姓名
Unsaved bool // 未展示在个人资料(saveStarGift 切换)
Converted bool // 已转换回 Stars(终态,从列表排除)
ConvertStars int64 // 转换可退回的 Stars
PrepaidUpgradeStars int64 // 送礼人随礼物预付的唯一礼物升级额
Message string // 附言(可选)
UniqueGiftID int64 // 非 0 表示已升级为唯一礼物;与 Converted 互斥
UpgradeMsgID int // messageActionStarGiftUnique 的 owner 侧消息 id
PinnedOrder int // >0 表示资料页置顶顺序
CollectionIDs []int // 当前所属集合;按集合顺序稳定返回
Unique *UniqueStarGift
ID int64
Owner Peer // 收礼 peer(user/channel)
FromUserID int64 // 送礼人(匿名也保留真实值供账本,下发时按 NameHidden 决定是否暴露)
GiftID int64 // → StarGift.ID
RevisionID int64 // → star_gift_catalog_revisions.id,历史查询必须按此版本投影
MsgID int // 用户礼物的私聊 msg_id;频道礼物不进历史,固定为 0
SavedID int64 // 频道礼物 inputSavedStarGiftChat.saved_id;用户礼物为 0
Date int // 收到时刻 Unix 秒
NameHidden bool // 送礼人请求隐藏姓名
Unsaved bool // 未展示在个人资料(saveStarGift 切换)
Converted bool // 已转换回 Stars(终态,从列表排除)
LifecycleStatus StarGiftLifecycleStatus
ConvertStars int64 // 转换可退回的 Stars
PrepaidUpgradeStars int64 // 送礼人随礼物预付的唯一礼物升级额
PrepaidUpgradeHash string // 第三方单独代付升级的一次性 entitlement
GiftNum int // auction-acquired release number for regular gifts
Message string // 附言(可选)
UniqueGiftID int64 // 非 0 表示已升级为唯一礼物;与 Converted 互斥
TransferStars int64
CanExportAt int
CanTransferAt int
CanResellAt int
DropOriginalDetailsStars int64
CanCraftAt int
UpgradeMsgID int // 当前 owner 侧承载 messageActionStarGiftUnique 的消息 id;所有权转移时随新消息更新
PinnedOrder int // >0 表示资料页置顶顺序
CollectionIDs []int // 当前所属集合;按集合顺序稳定返回
Unique *UniqueStarGift
}
type StarGiftLifecycleStatus string
const (
StarGiftLifecycleActive StarGiftLifecycleStatus = "active"
StarGiftLifecycleConverted StarGiftLifecycleStatus = "converted"
StarGiftLifecycleBurned StarGiftLifecycleStatus = "burned"
StarGiftLifecycleExported StarGiftLifecycleStatus = "exported"
)
func (s StarGiftLifecycleStatus) Live() bool {
return s == StarGiftLifecycleActive
}
// StarGiftCollectibleAttributeKind 是唯一礼物三个必选属性槽位。
@ -58,8 +114,31 @@ const (
StarGiftCollectibleBackdrop StarGiftCollectibleAttributeKind = "backdrop"
)
// StarGiftCollectibleAttribute 是已发布属性池的一项。RarityPermille 同时是客户端展示的
// 精确稀有度和升级抽取概率;同一 revision、同一 kind 的总和必须恰好为 1000。
// StarGiftAttributeRarityKind mirrors the Layer 228 rarity union. Permille is the only
// kind eligible for a regular upgrade draw; named rarities are currently used by
// craft-only models and must still be preserved in the published attribute directory.
type StarGiftAttributeRarityKind string
const (
StarGiftRarityPermille StarGiftAttributeRarityKind = "permille"
StarGiftRarityUncommon StarGiftAttributeRarityKind = "uncommon"
StarGiftRarityRare StarGiftAttributeRarityKind = "rare"
StarGiftRarityEpic StarGiftAttributeRarityKind = "epic"
StarGiftRarityLegendary StarGiftAttributeRarityKind = "legendary"
)
func (k StarGiftAttributeRarityKind) Valid() bool {
switch k {
case StarGiftRarityPermille, StarGiftRarityUncommon, StarGiftRarityRare,
StarGiftRarityEpic, StarGiftRarityLegendary:
return true
default:
return false
}
}
// StarGiftCollectibleAttribute 是已发布属性池的一项。RarityKind/RarityPermille
// 是客户端展示事实;普通升级把非 crafted 的 permille 值当相对权重,不要求合计为 1000。
type StarGiftCollectibleAttribute struct {
ID int64
CollectibleRevisionID int64
@ -71,7 +150,10 @@ type StarGiftCollectibleAttribute struct {
EdgeColor int
PatternColor int
TextColor int
RarityKind StarGiftAttributeRarityKind
RarityPermille int
Crafted bool
OfficialDocumentID int64
SortOrder int
Animation *StarGiftAnimation
Blob *FileBlob
@ -79,33 +161,37 @@ type StarGiftCollectibleAttribute struct {
// StarGiftCollectibleRevision 是某普通礼物的一份不可变、可发布属性池。
type StarGiftCollectibleRevision struct {
ID int64
GiftID int64
Revision int
UpgradeStars int64
SupplyTotal int
Issued int
SlugPrefix string
Published bool
Models []StarGiftCollectibleAttribute
Patterns []StarGiftCollectibleAttribute
Backdrops []StarGiftCollectibleAttribute
CreatedBy string
CreatedAt time.Time
PublishedAt time.Time
ID int64
GiftID int64
Revision int
UpgradeStars int64
SupplyTotal int
Issued int
SlugPrefix string
Published bool
Models []StarGiftCollectibleAttribute
Patterns []StarGiftCollectibleAttribute
Backdrops []StarGiftCollectibleAttribute
CreatedBy string
CreatedAt time.Time
PublishedAt time.Time
OfficialGiftID int64
SourceManifestSHA256 []byte
}
// StarGiftCollectibleWrite 是后台创建/发布属性池的协议无关输入。
type StarGiftCollectibleWrite struct {
GiftID int64
UpgradeStars int64
SupplyTotal int
SlugPrefix string
Models []StarGiftCollectibleAttribute
Patterns []StarGiftCollectibleAttribute
Backdrops []StarGiftCollectibleAttribute
Actor string
CommandID string
GiftID int64
UpgradeStars int64
SupplyTotal int
SlugPrefix string
Models []StarGiftCollectibleAttribute
Patterns []StarGiftCollectibleAttribute
Backdrops []StarGiftCollectibleAttribute
Actor string
CommandID string
OfficialGiftID int64
SourceManifestSHA256 []byte
}
// UniqueStarGift 是一份已经发行的唯一礼物。属性、编号与 slug 一经创建永久不变。
@ -118,6 +204,26 @@ type UniqueStarGift struct {
Slug string
Num int
Owner Peer
RequirePremium bool
ResaleTonOnly bool
ThemeAvailable bool
Burned bool
Crafted bool
OwnerName string
OwnerAddress string
GiftAddress string
ResellAmount *StarGiftAmount
ResellVersion int64
ReleasedBy Peer
ValueAmount int64
ValueCurrency string
ValueUSD int64
ThemePeer Peer
Host Peer
OfferMinStars int
CraftChancePermille int
LastSaleDate int
LastSaleAmount *StarGiftAmount
Model StarGiftCollectibleAttribute
Pattern StarGiftCollectibleAttribute
Backdrop StarGiftCollectibleAttribute
@ -132,6 +238,33 @@ type UniqueStarGift struct {
CreatedAt time.Time
}
type StarGiftCurrency string
const (
StarGiftCurrencyStars StarGiftCurrency = "XTR"
StarGiftCurrencyTON StarGiftCurrency = "TON"
)
type StarGiftAmount struct {
Currency StarGiftCurrency
Amount int64
Nanos int
}
func (a StarGiftAmount) Valid() bool {
if a.Amount <= 0 {
return false
}
switch a.Currency {
case StarGiftCurrencyStars:
return a.Nanos >= -999999999 && a.Nanos <= 999999999
case StarGiftCurrencyTON:
return a.Nanos == 0
default:
return false
}
}
// StarGiftUpgradePreview 是客户端升级弹窗所需的当前价格和属性样例。
type StarGiftUpgradePreview struct {
GiftID int64
@ -171,7 +304,205 @@ type StarGiftUpgradeRequest struct {
OriginSessionID int64
}
type StarGiftPurchaseRequest struct {
BuyerUserID int64
BuyerPremium bool
To Peer
GiftID int64
RevisionID int64
IncludeUpgrade bool
HideName bool
Message string
ChargeStars int64
FormID int64
CommandKey string
Date int
RecipientBlocked bool
OriginAuthKeyID [8]byte
OriginSessionID int64
}
// StarGiftPurchaseForm is the server-issued, short-lived payment intent that
// binds payments.getPaymentForm to one later payments.sendStarsForm call. A
// fresh form represents a fresh purchase even when every invoice field is the
// same; retrying one form represents the same purchase command.
type StarGiftPurchaseForm struct {
FormID int64
BuyerUserID int64
To Peer
GiftID int64
RevisionID int64
IncludeUpgrade bool
HideName bool
Message string
ChargeStars int64
IssuedAt int
ExpiresAt int
}
type StarGiftPurchaseResult struct {
Gift StarGift
Saved SavedStarGift
Balance StarsBalance
Send SendPrivateTextResult
Duplicate bool
}
// StarGiftConvertRequest identifies one owner-scoped regular gift conversion.
// ActorUserID is the authenticated user who owns the user gift or administers
// the channel gift; authorization is checked again at the RPC boundary.
type StarGiftConvertRequest struct {
ActorUserID int64
Ref SavedStarGiftRef
Date int
}
// StarGiftConvertResult exposes the committed aggregate state. OwnerBalance is
// the post-credit balance of either the user or the channel internal Stars
// ledger selected by Saved.Owner.
type StarGiftConvertResult struct {
Saved SavedStarGift
OwnerBalance int64
}
type StarGiftUpgradeResult struct {
Saved SavedStarGift
Unique UniqueStarGift
Balance StarsBalance
Send SendPrivateTextResult
SourceEdits []EditedMessageForUser
Duplicate bool
}
// StarGiftUpgradeReceipt is the immutable command envelope needed to replay a
// committed upgrade after the saved gift has entered its unique terminal state.
// In particular, a paid replay must not be rebound to a later catalog price.
type StarGiftUpgradeReceipt struct {
UserID int64
SourceSavedGiftID int64
FormID int64
UniqueGiftID int64
ChargeStars int64
BalanceAfter int64
SourceEditPts int
RequirePrepaid bool
KeepOriginalDetails bool
}
type StarGiftPrepaidUpgradeRequest struct {
PayerUserID int64
Owner Peer
Hash string
ChargeStars int64
FormID int64
CommandKey string
Date int
OriginAuthKeyID [8]byte
OriginSessionID int64
}
type StarGiftPrepaidUpgradeResult struct {
Saved SavedStarGift
Balance StarsBalance
Send SendPrivateTextResult
Duplicate bool
}
type StarGiftDropOriginalDetailsRequest struct {
UserID int64
Ref SavedStarGiftRef
ChargeStars int64
FormID int64
CommandKey string
Date int
}
type StarGiftDropOriginalDetailsResult struct {
Saved SavedStarGift
Unique UniqueStarGift
Balance StarsBalance
Duplicate bool
}
// StarGiftLifecyclePolicy is the server-owned policy snapshotted when a regular
// gift becomes collectible. It deliberately contains no wallet/node/provider
// configuration: TON remains only a currency unit in the local ledger.
type StarGiftLifecyclePolicy struct {
TransferStars int64
DropOriginalDetailsStars int64
OfferMinStars int
ExportDelaySeconds int
TransferDelaySeconds int
ResellDelaySeconds int
CraftDelaySeconds int
CraftChancePermille int
}
func (p StarGiftLifecyclePolicy) Valid() bool {
return p.TransferStars >= 0 && p.DropOriginalDetailsStars >= 0 && p.OfferMinStars >= 0 &&
p.ExportDelaySeconds >= 0 && p.TransferDelaySeconds >= 0 && p.ResellDelaySeconds >= 0 &&
p.CraftDelaySeconds >= 0 && p.CraftChancePermille >= 0 && p.CraftChancePermille <= 1000
}
// StarGiftMarketPolicy is snapshotted in the running aggregate coordinator.
// Proceeds permille is the seller share; the remainder is recorded as platform
// commission. TON is still only a unit in the local ledger.
type StarGiftMarketPolicy struct {
StarsProceedsPermille int
TONProceedsPermille int
}
func (p StarGiftMarketPolicy) Valid() bool {
return p.StarsProceedsPermille >= 0 && p.StarsProceedsPermille <= 1000 &&
p.TONProceedsPermille >= 0 && p.TONProceedsPermille <= 1000
}
type StarGiftResaleFilter struct {
GiftID int64
SortByPrice bool
SortByNum bool
ForCraft bool
StarsOnly bool
ModelIDs []int64
PatternIDs []int64
BackdropIDs []int64
Offset string
Limit int
}
type StarGiftResalePage struct {
Gifts []UniqueStarGift
Count int
NextOffset string
}
type StarGiftValueInfo struct {
Currency string
Value int64
ValueIsAverage bool
InitialSaleDate int
InitialSaleStars int64
InitialSalePrice int64
LastSaleDate int
LastSalePrice int64
FloorPrice int64
AveragePrice int64
ListedCount int
}
type StarGiftTransferRequest struct {
ActorUserID int64
Ref SavedStarGiftRef
To Peer
ChargeStars int64
FormID int64
CommandKey string
Date int
OriginAuthKeyID [8]byte
OriginSessionID int64
}
type StarGiftTransferResult struct {
Saved SavedStarGift
Unique UniqueStarGift
Balance StarsBalance
@ -179,6 +510,161 @@ type StarGiftUpgradeResult struct {
Duplicate bool
}
type StarGiftListingRequest struct {
ActorUserID int64
Ref SavedStarGiftRef
Amount *StarGiftAmount
Date int
}
type StarGiftResalePurchaseRequest struct {
BuyerUserID int64
Slug string
To Peer
Amount StarGiftAmount
FormID int64
CommandKey string
Date int
OriginAuthKeyID [8]byte
OriginSessionID int64
}
type StarGiftOfferRequest struct {
BuyerUserID int64
Owner Peer
Slug string
Price StarGiftAmount
Duration int
RandomID int64
Date int
OriginAuthKeyID [8]byte
OriginSessionID int64
}
type StarGiftOffer struct {
ID int64
BuyerUserID int64
Owner Peer
UniqueGiftID int64
Price StarGiftAmount
RandomID int64
OfferMsgID int
BuyerMsgID int
Status string
CreatedAt int
ExpiresAt int
ResolvedAt int
Gift UniqueStarGift
}
type StarGiftOfferResult struct {
Offer StarGiftOffer
Saved SavedStarGift
Unique UniqueStarGift
Balance StarsBalance
Send SendPrivateTextResult
Duplicate bool
}
type StarGiftResolveOfferRequest struct {
OwnerUserID int64
OfferMsgID int
Decline bool
Date int
OriginAuthKeyID [8]byte
OriginSessionID int64
}
type StarGiftCraftRequest struct {
UserID int64
Refs []SavedStarGiftRef
CommandKey string
Date int
OriginAuthKeyID [8]byte
OriginSessionID int64
}
type StarGiftCraftResult struct {
Success bool
Chance int
Gift *UniqueStarGift
Send SendPrivateTextResult
SourceEdits []EditedMessageForUser
Duplicate bool
}
type StarGiftAuction struct {
Gift StarGift
Version int
StartDate int
EndDate int
MinBidAmount int64
NextRoundAt int
LastGiftNum int
GiftsLeft int
CurrentRound int
TotalRounds int
RoundDuration int
BidLevels []StarGiftAuctionBidLevel
TopBidders []int64
UserState StarGiftAuctionUserState
Finished bool
AveragePrice int64
ListedCount int
}
type StarGiftAuctionBidLevel struct {
Pos int
Amount int64
Date int
}
type StarGiftAuctionUserState struct {
Returned bool
BidAmount int64
BidDate int
MinBidAmount int64
BidPeer Peer
AcquiredCount int
}
type StarGiftAuctionBidRequest struct {
UserID int64
GiftID int64
Peer Peer
BidAmount int64
HideName bool
Message string
UpdateBid bool
FormID int64
Date int
}
type StarGiftAuctionAcquired struct {
Peer Peer
Date int
BidAmount int64
Round int
Pos int
Message string
GiftNum int
NameHidden bool
}
type StarGiftWithdrawalRequest struct {
UserID int64
Ref SavedStarGiftRef
Date int
}
type StarGiftWithdrawal struct {
ProviderRequestID string
URL string
ExpiresAt int
Status string
Gift UniqueStarGift
}
// StarGiftCollection 是 peer 资料页中的礼物集合;一份礼物可属于多个集合。
type StarGiftCollection struct {
Owner Peer
@ -223,17 +709,53 @@ type StarGiftAnimation struct {
// StarGiftCatalogWrite 是 store 原子创建目录版本所需的协议无关数据。
type StarGiftCatalogWrite struct {
GiftID int64 // 0 创建新礼物;非 0 为该礼物创建新 revision
Title string
Stars int64
ConvertStars int64
Enabled bool
SortOrder int
Document Document
Blob FileBlob
Animation StarGiftAnimation
Actor string
CommandID string
GiftID int64 // 0 创建新礼物;非 0 为该礼物创建新 revision
Title string
Stars int64
ConvertStars int64
Enabled bool
SortOrder int
Document Document
Blob FileBlob
Animation StarGiftAnimation
Actor string
CommandID string
OfficialGiftID int64
SourceManifestSHA256 []byte
OfficialSourceJSON []byte
Limited bool
SoldOut bool
Birthday bool
RequirePremium bool
LimitedPerUser bool
PeerColorAvailable bool
Auction bool
AvailabilityRemains int
AvailabilityTotal int
AvailabilityResale int64
FirstSaleDate int
LastSaleDate int
ResellMinStars int64
ReleasedBy Peer
PerUserTotal int
LockedUntilDate int
AuctionSlug string
GiftsPerRound int
AuctionStartDate int
UpgradeVariants int
Background *StarGiftBackground
}
// StarGiftCatalogBundleWrite atomically publishes one catalog revision and its optional
// complete collectible pool. Collectible.GiftID is filled with the allocated local gift ID.
type StarGiftCatalogBundleWrite struct {
Catalog StarGiftCatalogWrite
Collectible *StarGiftCollectibleWrite
}
type StarGiftCatalogBundleResult struct {
Catalog StarGiftCatalogEntry
Collectible *StarGiftCollectibleRevision
}
// StarGiftCatalogEntry 是管理后台目录视图。
@ -255,20 +777,24 @@ type StarGiftCatalogEntry struct {
}
// SavedStarGiftRef 是 payments.getSavedStarGift/saveStarGift/convertStarGift 的协议中立引用。
// 用户礼物使用 inputSavedStarGiftUser.msg_id;频道礼物使用 inputSavedStarGiftChat.peer + saved_id。
// 用户礼物使用 inputSavedStarGiftUser.msg_id;频道礼物使用 inputSavedStarGiftChat.peer + saved_id;
// 已升级的唯一礼物也可使用官方 inputSavedStarGiftSlug.slug。三种身份必须互斥。
type SavedStarGiftRef struct {
Owner Peer
MsgID int
SavedID int64
Slug string
}
// Valid reports whether the reference has the identity required by its owner kind.
func (r SavedStarGiftRef) Valid() bool {
slug := strings.TrimSpace(r.Slug)
validSlug := slug != "" && slug == r.Slug && len(slug) <= MaxStarGiftSlugBytes && r.MsgID == 0 && r.SavedID == 0
switch r.Owner.Type {
case PeerTypeUser:
return r.Owner.ID != 0 && r.MsgID > 0
return r.Owner.ID != 0 && (validSlug || r.MsgID > 0 && r.SavedID == 0 && slug == "")
case PeerTypeChannel:
return r.Owner.ID != 0 && r.SavedID > 0
return r.Owner.ID != 0 && (validSlug || r.SavedID > 0 && r.MsgID == 0 && slug == "")
default:
return false
}
@ -317,7 +843,8 @@ const (
// MaxStarGiftCatalogSize 是当前普通礼物目录的有界上限。
MaxStarGiftCatalogSize = 500
MaxStarGiftTitleRunes = 128
MaxStarGiftCollectibleAttributesPerKind = 256
MaxStarGiftSlugBytes = 255
MaxStarGiftCollectibleAttributesPerKind = 512
MaxStarGiftCollectionTitleRunes = 12
MaxStarGiftCollectionsPerPeer = 100
MaxStarGiftCollectionItems = 1000
@ -339,6 +866,18 @@ var (
ErrStarGiftCollectibleInvalid = errors.New("stargift: invalid collectible definition")
ErrStarGiftCollectionNotFound = errors.New("stargift: collection not found")
ErrStarGiftCollectionsFull = errors.New("stargift: collections full")
ErrStarGiftUnavailable = errors.New("stargift: unavailable")
ErrStarGiftOwnerInvalid = errors.New("stargift: owner invalid")
ErrStarGiftTransferUnavailable = errors.New("stargift: transfer unavailable")
ErrStarGiftResaleUnavailable = errors.New("stargift: resale unavailable")
ErrStarGiftOfferInvalid = errors.New("stargift: offer invalid")
ErrStarGiftOfferExpired = errors.New("stargift: offer expired")
ErrStarGiftCraftUnavailable = errors.New("stargift: craft unavailable")
ErrStarGiftAuctionUnavailable = errors.New("stargift: auction unavailable")
ErrStarGiftWithdrawalUnavailable = errors.New("stargift: withdrawal provider unavailable")
ErrStarGiftFormExpired = errors.New("stargift: payment form expired")
ErrStarGiftFormPurposeInvalid = errors.New("stargift: payment form purpose invalid")
ErrStarGiftFormAmountMismatch = errors.New("stargift: payment form amount mismatch")
)
var starGiftCollectibleSlugPrefix = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,47}$`)
@ -351,6 +890,11 @@ func ValidateStarGiftCollectibleDraft(write StarGiftCollectibleWrite) error {
!starGiftCollectibleSlugPrefix.MatchString(write.SlugPrefix) || strings.TrimSpace(write.CommandID) == "" {
return ErrStarGiftCollectibleInvalid
}
if write.OfficialGiftID < 0 ||
(write.OfficialGiftID == 0 && len(write.SourceManifestSHA256) != 0) ||
(write.OfficialGiftID > 0 && len(write.SourceManifestSHA256) != 32) {
return ErrStarGiftCollectibleInvalid
}
if err := validateStarGiftAttributes(write.Models, StarGiftCollectibleModel, false); err != nil {
return err
}
@ -380,11 +924,19 @@ func validateStarGiftAttributes(attributes []StarGiftCollectibleAttribute, kind
return ErrStarGiftCollectibleInvalid
}
seen := make(map[string]struct{}, len(attributes))
total := 0
selectable := 0
for _, attribute := range attributes {
name := strings.TrimSpace(attribute.Name)
if attribute.Kind != kind || name == "" || len([]rune(name)) > MaxStarGiftTitleRunes ||
attribute.RarityPermille <= 0 || attribute.RarityPermille > 1000 {
rarityKind := attribute.RarityKind
if attribute.Kind != kind || name == "" || len([]rune(name)) > MaxStarGiftTitleRunes || !rarityKind.Valid() {
return ErrStarGiftCollectibleInvalid
}
if rarityKind == StarGiftRarityPermille {
if attribute.RarityPermille <= 0 || attribute.RarityPermille > 1000 || attribute.Crafted {
return ErrStarGiftCollectibleInvalid
}
selectable++
} else if attribute.RarityPermille != 0 || !attribute.Crafted || kind != StarGiftCollectibleModel {
return ErrStarGiftCollectibleInvalid
}
key := strings.ToLower(name)
@ -392,7 +944,6 @@ func validateStarGiftAttributes(attributes []StarGiftCollectibleAttribute, kind
return ErrStarGiftCollectibleInvalid
}
seen[key] = struct{}{}
total += attribute.RarityPermille
switch kind {
case StarGiftCollectibleModel, StarGiftCollectiblePattern:
if attribute.Animation == nil || len(attribute.Animation.JSON) == 0 ||
@ -404,7 +955,7 @@ func validateStarGiftAttributes(attributes []StarGiftCollectibleAttribute, kind
return ErrStarGiftCollectibleInvalid
}
case StarGiftCollectibleBackdrop:
if attribute.BackdropID <= 0 || attribute.Document != nil ||
if attribute.BackdropID < 0 || attribute.Document != nil ||
attribute.CenterColor < 0 || attribute.CenterColor > 0xffffff ||
attribute.EdgeColor < 0 || attribute.EdgeColor > 0xffffff ||
attribute.PatternColor < 0 || attribute.PatternColor > 0xffffff ||
@ -415,7 +966,7 @@ func validateStarGiftAttributes(attributes []StarGiftCollectibleAttribute, kind
return ErrStarGiftCollectibleInvalid
}
}
if total != 1000 {
if selectable == 0 {
return ErrStarGiftCollectibleInvalid
}
return nil

View file

@ -0,0 +1,95 @@
package domain
import (
"crypto/sha256"
"errors"
"strings"
"testing"
)
func TestSavedStarGiftRefRequiresOneOfficialIdentity(t *testing.T) {
user := Peer{Type: PeerTypeUser, ID: 42}
channel := Peer{Type: PeerTypeChannel, ID: 84}
tests := []struct {
name string
ref SavedStarGiftRef
want bool
}{
{name: "user message", ref: SavedStarGiftRef{Owner: user, MsgID: 10}, want: true},
{name: "channel saved id", ref: SavedStarGiftRef{Owner: channel, SavedID: 20}, want: true},
{name: "user collectible slug", ref: SavedStarGiftRef{Owner: user, Slug: "official-42-1"}, want: true},
{name: "channel collectible slug", ref: SavedStarGiftRef{Owner: channel, Slug: "official-84-1"}, want: true},
{name: "message and slug", ref: SavedStarGiftRef{Owner: user, MsgID: 10, Slug: "official-42-1"}},
{name: "saved id and slug", ref: SavedStarGiftRef{Owner: channel, SavedID: 20, Slug: "official-84-1"}},
{name: "whitespace slug", ref: SavedStarGiftRef{Owner: user, Slug: " official-42-1"}},
{name: "oversized slug", ref: SavedStarGiftRef{Owner: user, Slug: strings.Repeat("x", MaxStarGiftSlugBytes+1)}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.ref.Valid(); got != tt.want {
t.Fatalf("Valid() = %v, want %v", got, tt.want)
}
})
}
}
func TestStarGiftLifecycleStatusRequiresExplicitActive(t *testing.T) {
if StarGiftLifecycleStatus("").Live() {
t.Fatal("empty lifecycle status must not be treated as active")
}
if !StarGiftLifecycleActive.Live() {
t.Fatal("active lifecycle status must be live")
}
}
func validCollectibleDraft() StarGiftCollectibleWrite {
animation := &StarGiftAnimation{JSON: []byte(`{}`), TGS: []byte{1}, SHA256: make([]byte, sha256.Size)}
return StarGiftCollectibleWrite{
GiftID: 1, UpgradeStars: 25, SupplyTotal: 100, SlugPrefix: "official-1", CommandID: "test",
Models: []StarGiftCollectibleAttribute{
{Kind: StarGiftCollectibleModel, Name: "Regular", RarityKind: StarGiftRarityPermille, RarityPermille: 922, Animation: animation},
{Kind: StarGiftCollectibleModel, Name: "Crafted", RarityKind: StarGiftRarityLegendary, Crafted: true, Animation: animation},
},
Patterns: []StarGiftCollectibleAttribute{
{Kind: StarGiftCollectiblePattern, Name: "Pattern", RarityKind: StarGiftRarityPermille, RarityPermille: 989, Animation: animation},
},
Backdrops: []StarGiftCollectibleAttribute{
{Kind: StarGiftCollectibleBackdrop, Name: "Backdrop", BackdropID: 0, RarityKind: StarGiftRarityPermille, RarityPermille: 999},
},
}
}
func TestValidateStarGiftCollectibleDraftOfficialProvenance(t *testing.T) {
write := validCollectibleDraft()
write.OfficialGiftID = 10
write.SourceManifestSHA256 = make([]byte, sha256.Size)
if err := ValidateStarGiftCollectibleDraft(write); err != nil {
t.Fatalf("valid official draft: %v", err)
}
tests := map[string]StarGiftCollectibleWrite{}
withoutHash := write
withoutHash.SourceManifestSHA256 = nil
tests["official ID without hash"] = withoutHash
withoutID := write
withoutID.OfficialGiftID = 0
tests["hash without official ID"] = withoutID
negativeID := write
negativeID.OfficialGiftID = -1
tests["negative official ID"] = negativeID
for name, invalid := range tests {
t.Run(name, func(t *testing.T) {
if err := ValidateStarGiftCollectibleDraft(invalid); !errors.Is(err, ErrStarGiftCollectibleInvalid) {
t.Fatalf("err=%v, want ErrStarGiftCollectibleInvalid", err)
}
})
}
}
func TestValidateStarGiftCollectibleDraftRejectsImplicitRarity(t *testing.T) {
write := validCollectibleDraft()
write.Models[0].RarityKind = ""
if err := ValidateStarGiftCollectibleDraft(write); !errors.Is(err, ErrStarGiftCollectibleInvalid) {
t.Fatalf("err=%v, want ErrStarGiftCollectibleInvalid", err)
}
}

View file

@ -20,13 +20,19 @@ type StarsBalance struct {
type StarsTransactionReason string
const (
StarsReasonGrant StarsTransactionReason = "grant" // 起始余额自动授予
StarsReasonTopup StarsTransactionReason = "topup" // 充值(本地铸造)
StarsReasonReaction StarsTransactionReason = "reaction" // 付费 reaction 花费
StarsReasonGift StarsTransactionReason = "gift" // 星礼花费/收取
StarsReasonGiftUpgrade StarsTransactionReason = "gift_upgrade" // 普通礼物升级为唯一礼物
StarsReasonPaidMedia StarsTransactionReason = "paid_media" // 付费媒体解锁
StarsReasonAdjust StarsTransactionReason = "adjust" // 兜底/人工调整
StarsReasonGrant StarsTransactionReason = "grant" // 起始余额自动授予
StarsReasonTopup StarsTransactionReason = "topup" // 充值(本地铸造)
StarsReasonReaction StarsTransactionReason = "reaction" // 付费 reaction 花费
StarsReasonGift StarsTransactionReason = "gift" // 星礼花费/收取
StarsReasonGiftUpgrade StarsTransactionReason = "gift_upgrade" // 普通礼物升级为唯一礼物
StarsReasonGiftTransfer StarsTransactionReason = "gift_transfer"
StarsReasonGiftResale StarsTransactionReason = "gift_resale"
StarsReasonGiftOffer StarsTransactionReason = "gift_offer"
StarsReasonGiftAuction StarsTransactionReason = "gift_auction"
StarsReasonGiftPrepaid StarsTransactionReason = "gift_prepaid_upgrade"
StarsReasonGiftDrop StarsTransactionReason = "gift_drop_original_details"
StarsReasonPaidMedia StarsTransactionReason = "paid_media" // 付费媒体解锁
StarsReasonAdjust StarsTransactionReason = "adjust" // 兜底/人工调整
)
// StarsTransaction 是一条账本流水。amount 带符号:贷记 > 0(含 refund/收取),借记 < 0。
@ -52,6 +58,28 @@ type StarsTransactionPage struct {
Users []User // History 中提到的对手方用户,供 tg Users 富化
}
// TonTransaction is an entry in telesrv's internal nanoton ledger. It models
// the Telegram TON-denominated gift UI without contacting a wallet, Fragment,
// a TON node, or any blockchain service.
type TonTransaction struct {
ID int64
UserID int64
Peer Peer
GiftID int64
Amount int64 // signed nanoton amount
Date int
Reason StarsTransactionReason
Title string
Description string
}
type TonTransactionPage struct {
Balance int64
Transactions []TonTransaction
NextOffset string
Users []User
}
// Stars 账本边界常量。
const (
// DefaultStarsStartingGrant 是惰性首读授予的起始 Stars 余额(本地测试用)。