Merge remote-tracking branch 'upstream/main' into dev

This commit is contained in:
onysd 2026-07-18 09:19:27 +03:00
commit 6b29556ef8
836 changed files with 1598388 additions and 64684 deletions

View file

@ -26,9 +26,15 @@ type AdminCommand struct {
CompletedAt *time.Time
}
type AccountSendRestriction struct {
// AccountFreeze is the durable account-level read-only state advertised to
// Telegram clients through help.getAppConfig. Until is the appeal/deletion
// deadline; reaching it does not silently unfreeze the account.
type AccountFreeze struct {
UserID int64
Frozen bool
Since time.Time
Until time.Time
AppealURL string
Reason string
Actor string
CommandID string

View file

@ -5,9 +5,12 @@ import "time"
// Authorization 是一条设备授权:auth_key 与 user 的绑定 + initConnection 设备信息。
// auth_key 是协议产物、授权是业务产物,故独立于 store.AuthKeyData。
type Authorization struct {
AuthKeyID [8]byte // 协议原生 auth_key_id;store 边界按小端转 int64
UserID int64
Hash int64
AuthKeyID [8]byte // 协议原生 auth_key_id;store 边界按小端转 int64
UserID int64
Hash int64
// Layer is the last supported protocol profile explicitly observed for this
// auth key. It is a durable default for a new session, never an instruction
// to rewrite an already-active session's own profile.
Layer int
DeviceModel string
Platform string
@ -23,13 +26,18 @@ type Authorization struct {
}
// AuthKeyClientInfo 是未登录 auth_key 也需要保留的客户端协商元数据。
// 登录后的设备授权仍由 Authorization 表达;这里仅用于服务端重启后恢复
// pre-auth / setup 流程的 client type 与 layer。
// 登录后的设备授权仍由 Authorization 表达。Layer 保存最后一次受支持的显式
// wire profile,供服务端重启后为同一 auth key 的新 session 初始化默认值;活跃
// session 仍以自己的显式 invokeWithLayer 纠正值为准。
type AuthKeyClientInfo struct {
Layer int
DeviceModel string
Platform string
SystemVersion string
APIID int
AppVersion string
Layer int
// LayerObservationID is a read-only ordering token owned by the protocol
// store. Generic client metadata updates must never manufacture or advance
// it; only ordered invokeWithLayer evidence may do so.
LayerObservationID int64
DeviceModel string
Platform string
SystemVersion string
APIID int
AppVersion string
}

View file

@ -1,4 +1,4 @@
// Package domain 存放业务实体与值对象(User、Peer、Dialog、Message、MessageID 等)。
//
// 铁律:本包禁止依赖 gotd/td/tg 等协议层类型;TL 类型只允许出现在 RPC/MTProto 边界。
// 铁律:本包禁止依赖 iamxvbaba/td/tg 等协议层类型;TL 类型只允许出现在 RPC/MTProto 边界。
package domain

View file

@ -9,6 +9,55 @@ type LangPack struct {
Strings []LangPackString
}
// LangPackSeed 是一次启动扫描得到的完整语言包文件清单。
type LangPackSeed struct {
Catalog string
Scopes []string
Packs []LangPackSeedEntry
}
// LangPackSeedEntry 记录一个语言包文件、源文件 hash 与规范化内容 hash。
type LangPackSeedEntry struct {
Pack LangPack
SourceHash string
ContentHash string
StringsCount int
ContentLoaded bool
}
// LangPackSeedCatalog 是上次成功对账后可用于跳过未变文件解析的清单快照。
type LangPackSeedCatalog struct {
Catalog string `json:"catalog"`
Scopes []string `json:"scopes"`
Packs []LangPackSeedCatalogEntry `json:"packs"`
}
// LangPackSeedCatalogEntry 只保存判断源文件是否变化所需的有界元数据。
type LangPackSeedCatalogEntry struct {
LangPack string `json:"lang_pack"`
LangCode string `json:"lang_code"`
Version int `json:"version"`
SourceHash string `json:"source_hash"`
ContentHash string `json:"content_hash"`
StringsCount int `json:"strings_count"`
}
// LangPackLanguage 是 langpack.getLanguages/getLanguage 返回的语言元数据。
type LangPackLanguage struct {
LangPack string
LangCode string
Name string
NativeName string
BaseLangCode string
PluralCode string
Official bool
Rtl bool
Beta bool
StringsCount int
TranslatedCount int
TranslationsURL string
}
// LangPackString 是语言包中的一个普通或复数形式字符串。
type LangPackString struct {
Key string

View file

@ -561,6 +561,10 @@ const (
// MessageServiceActionStarGift 映射 messageActionStarGift:收到一份 Star 礼物。
// 礼物快照(贴纸/星价)内嵌在 action 里,收礼人无需额外拉取即可渲染气泡。
MessageServiceActionStarGift MessageServiceActionKind = "star_gift"
// 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"
)
// MessagePhoneCallAction 是 messageActionPhoneCall 的协议中立载荷。
@ -617,24 +621,42 @@ type MessageServiceAction struct {
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"`
}
// 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"`
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"`
}
// 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"`
}
// MessageMedia 是一条消息媒体载荷的业务表示(落库为消息行上的 JSONB 快照)。

View file

@ -246,19 +246,23 @@ type MessageFilter struct {
// SendPrivateTextRequest 是私聊文本/媒体发送命令。
type SendPrivateTextRequest struct {
SenderUserID int64
RecipientUserID int64
RandomID int64
Message string
Entities []MessageEntity
Media *MessageMedia
Silent bool
NoForwards bool
ReplyTo *MessageReply
Forward *MessageForward
Date int
OriginAuthKeyID [8]byte
OriginSessionID int64
SenderUserID int64
RecipientUserID int64
RandomID int64
Message string
Entities []MessageEntity
Media *MessageMedia
Silent bool
NoForwards bool
ReplyTo *MessageReply
Forward *MessageForward
Date int
OriginAuthKeyID [8]byte
OriginSessionID int64
// OriginUserID identifies the authenticated initiator when a server-generated
// service message is authored by another user. Zero preserves the ordinary
// send path where the sender is the initiator.
OriginUserID int64
RecipientBlocked bool
// IdempotencyFingerprint 是调用边界对原始、不可变发送请求计算的 SHA-256。
// RPC 层应优先填入原始 TL 请求指纹,避免链接预览、骰子结果、上传媒体
@ -285,6 +289,13 @@ type SendPrivateTextRequest struct {
RichMessage *MessageRichMessage
}
// HasContent reports whether the command contains a client-visible message payload.
// TDesktop rich-message posting intentionally leaves Message empty, so every send
// boundary must treat text, media, and rich_message as equivalent content sources.
func (r SendPrivateTextRequest) HasContent() bool {
return r.Message != "" || !r.Media.IsZero() || !r.RichMessage.IsZero()
}
// PrivateSendReplayRequest identifies one already-committed private send without carrying any
// mutable or resolver-derived message fields. The fingerprint is computed at the original
// request boundary and must be a complete SHA-256 value.

View file

@ -3,36 +3,255 @@ package domain
import (
"encoding/base64"
"errors"
"regexp"
"strconv"
"strings"
"time"
)
// Star gift(payments.sendStarsForm + inputInvoiceStarGift)领域模型。目录是从已 seed 的
// animated_emoji 合成的静态集合(StarGift);peer 收到的礼物实例落 peer_star_gifts(SavedStarGift)。
// Star gift(payments.sendStarsForm + inputInvoiceStarGift)领域模型。目录和不可变版本
// 持久化在 star_gift_catalog(_revisions);peer 收到的礼物实例落 peer_star_gifts。
// 与 Stars 账本配合:发礼 Debit、转换回 Stars 时 Credit。
// StarGift 是一个可购买礼物目录项(合成、非用户持有)。
// StarGift 是一个可购买礼物目录项。RevisionID 标识不可变的标题/价格/动画快照。
type StarGift struct {
ID int64
Stars int64 // 购买价(Stars)
ConvertStars int64 // 收礼人可转换回的 Stars(v1 = Stars,全额)
Title string // 可选标题
Sticker Document // 礼物贴纸快照(tg 投影必须是带 sticker 属性的有效 Document,否则客户端丢弃)
ID int64
RevisionID int64
Stars int64 // 购买价(Stars)
ConvertStars int64 // 收礼人可转换回的 Stars
UpgradeStars int64 // 升级为唯一礼物所需 Stars;0 表示当前不可升级
UpgradeTotal int // 当前已发布属性池允许发行的唯一礼物总量
UpgradeIssued int // 当前已发行数量
Title string // 可选标题
Sticker Document // 礼物贴纸快照(tg 投影必须是带 sticker 属性的有效 Document,否则客户端丢弃)
}
// 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
}
// StarGiftCollectibleAttributeKind 是唯一礼物三个必选属性槽位。
type StarGiftCollectibleAttributeKind string
const (
StarGiftCollectibleModel StarGiftCollectibleAttributeKind = "model"
StarGiftCollectiblePattern StarGiftCollectibleAttributeKind = "pattern"
StarGiftCollectibleBackdrop StarGiftCollectibleAttributeKind = "backdrop"
)
// StarGiftCollectibleAttribute 是已发布属性池的一项。RarityPermille 同时是客户端展示的
// 精确稀有度和升级抽取概率;同一 revision、同一 kind 的总和必须恰好为 1000。
type StarGiftCollectibleAttribute struct {
ID int64
CollectibleRevisionID int64
Kind StarGiftCollectibleAttributeKind
Name string
Document *Document
BackdropID int
CenterColor int
EdgeColor int
PatternColor int
TextColor int
RarityPermille int
SortOrder int
Animation *StarGiftAnimation
Blob *FileBlob
}
// StarGiftCollectibleRevision 是某普通礼物的一份不可变、可发布属性池。
type StarGiftCollectibleRevision struct {
ID int64
Owner Peer // 收礼 peer(user/channel)
FromUserID int64 // 送礼人(匿名也保留真实值供账本,下发时按 NameHidden 决定是否暴露)
GiftID int64 // → StarGift.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
Message string // 附言(可选)
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
}
// StarGiftCollectibleWrite 是后台创建/发布属性池的协议无关输入。
type StarGiftCollectibleWrite struct {
GiftID int64
UpgradeStars int64
SupplyTotal int
SlugPrefix string
Models []StarGiftCollectibleAttribute
Patterns []StarGiftCollectibleAttribute
Backdrops []StarGiftCollectibleAttribute
Actor string
CommandID string
}
// UniqueStarGift 是一份已经发行的唯一礼物。属性、编号与 slug 一经创建永久不变。
type UniqueStarGift struct {
ID int64
GiftID int64
CollectibleRevisionID int64
SourceSavedGiftID int64
Title string
Slug string
Num int
Owner Peer
Model StarGiftCollectibleAttribute
Pattern StarGiftCollectibleAttribute
Backdrop StarGiftCollectibleAttribute
AvailabilityIssued int
AvailabilityTotal int
KeepOriginalDetails bool
OriginalFromUserID int64
OriginalOwner Peer
OriginalDate int
OriginalMessage string
OriginalNameHidden bool
CreatedAt time.Time
}
// StarGiftUpgradePreview 是客户端升级弹窗所需的当前价格和属性样例。
type StarGiftUpgradePreview struct {
GiftID int64
Revision int
UpgradeStars int64
SupplyTotal int
Issued int
SlugPrefix string
Models []StarGiftCollectibleAttribute
Patterns []StarGiftCollectibleAttribute
Backdrops []StarGiftCollectibleAttribute
}
// StarGiftCollectibleAvailability is the lightweight current-pool projection used
// when rendering historical saved gifts. The saved gift keeps its immutable catalog
// revision for appearance and prices, while upgrade availability follows the pool
// currently published for the logical gift ID.
type StarGiftCollectibleAvailability struct {
UpgradeStars int64
SupplyTotal int
Issued int
}
// StarGiftUpgradeRequest is one idempotent user-owned upgrade command. Paid
// invoice upgrades set ChargeStars; the direct payments.upgradeStarGift path
// sets RequirePrepaid and charges zero at upgrade time.
type StarGiftUpgradeRequest struct {
UserID int64
Ref SavedStarGiftRef
KeepOriginalDetails bool
ChargeStars int64
RequirePrepaid bool
FormID int64
CommandKey string
Date int
OriginAuthKeyID [8]byte
OriginSessionID int64
}
type StarGiftUpgradeResult struct {
Saved SavedStarGift
Unique UniqueStarGift
Balance StarsBalance
Send SendPrivateTextResult
Duplicate bool
}
// StarGiftCollection 是 peer 资料页中的礼物集合;一份礼物可属于多个集合。
type StarGiftCollection struct {
Owner Peer
CollectionID int
Title string
GiftIDs []int64 // peer_star_gifts.id,按集合内顺序
Hash int64
SortOrder int
CreatedAt time.Time
UpdatedAt time.Time
}
// StarGiftCollectionPatch 描述 updateStarGiftCollection 的局部更新。
type StarGiftCollectionPatch struct {
Title *string
DeleteIDs []int64
AddIDs []int64
Order []int64
}
// StarGiftAnimationFormat 是后台导入源格式。服务端最终总是存储规范化 TGS。
type StarGiftAnimationFormat string
const (
StarGiftAnimationTGS StarGiftAnimationFormat = "tgs"
StarGiftAnimationLottie StarGiftAnimationFormat = "lottie"
)
// StarGiftAnimation 是已规范化并验证的动画。JSON 用于后台播放,TGS 用于客户端。
type StarGiftAnimation struct {
SourceName string
SourceFormat StarGiftAnimationFormat
JSON []byte
TGS []byte
SHA256 []byte
Width int
Height int
FrameRate float64
InPoint float64
OutPoint float64
}
// 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
}
// StarGiftCatalogEntry 是管理后台目录视图。
type StarGiftCatalogEntry struct {
Gift StarGift
Enabled bool
SortOrder int
Revision int
SourceName string
SourceFormat StarGiftAnimationFormat
AnimationSHA []byte
AnimationSize int64
Width int
Height int
FrameRate float64
ReceivedCount int64
CreatedBy string
UpdatedAt time.Time
}
// SavedStarGiftRef 是 payments.getSavedStarGift/saveStarGift/convertStarGift 的协议中立引用。
@ -62,6 +281,24 @@ type SavedStarGiftPage struct {
Count int // 总数(未转换、按 excludeUnsaved 过滤后)
}
// SavedStarGiftFilter describes the client-visible filters supported by
// payments.getSavedStarGifts. CollectionID is the collection membership filter;
// zero means all collections. The current catalog is used only to decide whether
// a regular gift remains upgradable, while its rendered gift snapshot still comes
// from RevisionID.
type SavedStarGiftFilter struct {
Owner Peer
ExcludeUnsaved bool
ExcludeSaved bool
ExcludeUnlimited bool
ExcludeUnique bool
ExcludeUpgradable bool
ExcludeUnupgradable bool
CollectionID int
Offset string
Limit int
}
// Star gift 边界常量。
const (
// MaxSavedStarGiftsLimit 是 getSavedStarGifts 单页上限。
@ -70,6 +307,20 @@ const (
MaxStarGiftMessageRunes = 255
// MaxStarGiftsOffsetBytes 是 keyset 游标字符串长度上限。
MaxStarGiftsOffsetBytes = 64
// MaxStarGiftTGSBytes 限制后台导入的压缩动画,避免管理面上传成为容量旁路。
MaxStarGiftTGSBytes int64 = 512 << 10
// MaxStarGiftLottieBytes 限制解压后的 Lottie JSON。
MaxStarGiftLottieBytes int64 = 4 << 20
// MaxStarGiftAnimationFrameRate / Seconds 限制管理后台播放器和客户端动画时间轴。
MaxStarGiftAnimationFrameRate = 120
MaxStarGiftAnimationSeconds = 30
// MaxStarGiftCatalogSize 是当前普通礼物目录的有界上限。
MaxStarGiftCatalogSize = 500
MaxStarGiftTitleRunes = 128
MaxStarGiftCollectibleAttributesPerKind = 256
MaxStarGiftCollectionTitleRunes = 12
MaxStarGiftCollectionsPerPeer = 100
MaxStarGiftCollectionItems = 1000
)
// Star gift 哨兵错误(rpc 层 errors.Is 映射为 tgerr)。
@ -79,20 +330,138 @@ var (
// ErrStarGiftNotFound 表示找不到该已收到礼物实例。
ErrStarGiftNotFound = errors.New("stargift: saved gift not found")
// ErrStarGiftAlreadyConverted 表示礼物已转换回 Stars(不可重复转换)。
ErrStarGiftAlreadyConverted = errors.New("stargift: already converted")
ErrStarGiftAlreadyConverted = errors.New("stargift: already converted")
ErrStarGiftFileInvalid = errors.New("stargift: invalid animation file")
ErrStarGiftCatalogFull = errors.New("stargift: catalog full")
ErrStarGiftCollectibleUnavailable = errors.New("stargift: collectible upgrade unavailable")
ErrStarGiftAlreadyUpgraded = errors.New("stargift: already upgraded")
ErrStarGiftCollectibleSoldOut = errors.New("stargift: collectible supply exhausted")
ErrStarGiftCollectibleInvalid = errors.New("stargift: invalid collectible definition")
ErrStarGiftCollectionNotFound = errors.New("stargift: collection not found")
ErrStarGiftCollectionsFull = errors.New("stargift: collections full")
)
// StarGiftCatalogHash 由目录的 (gift_id, stars) 折叠出稳定 hash,供 getStarGifts NotModified。
var starGiftCollectibleSlugPrefix = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,47}$`)
// ValidateStarGiftCollectibleDraft validates the operator-authored definition before animation
// blobs/documents are allocated. This is the validation boundary used by admin dry-runs.
func ValidateStarGiftCollectibleDraft(write StarGiftCollectibleWrite) error {
write.SlugPrefix = strings.TrimSpace(strings.ToLower(write.SlugPrefix))
if write.GiftID <= 0 || write.UpgradeStars <= 0 || write.SupplyTotal <= 0 ||
!starGiftCollectibleSlugPrefix.MatchString(write.SlugPrefix) || strings.TrimSpace(write.CommandID) == "" {
return ErrStarGiftCollectibleInvalid
}
if err := validateStarGiftAttributes(write.Models, StarGiftCollectibleModel, false); err != nil {
return err
}
if err := validateStarGiftAttributes(write.Patterns, StarGiftCollectiblePattern, false); err != nil {
return err
}
return validateStarGiftAttributes(write.Backdrops, StarGiftCollectibleBackdrop, false)
}
// ValidateStarGiftCollectibleWrite validates a complete publish command. Published pools are
// immutable, so partial definitions are rejected before any document/blob rows are written.
func ValidateStarGiftCollectibleWrite(write StarGiftCollectibleWrite) error {
if err := ValidateStarGiftCollectibleDraft(write); err != nil {
return err
}
if err := validateStarGiftAttributes(write.Models, StarGiftCollectibleModel, true); err != nil {
return err
}
if err := validateStarGiftAttributes(write.Patterns, StarGiftCollectiblePattern, true); err != nil {
return err
}
return validateStarGiftAttributes(write.Backdrops, StarGiftCollectibleBackdrop, true)
}
func validateStarGiftAttributes(attributes []StarGiftCollectibleAttribute, kind StarGiftCollectibleAttributeKind, requireStoredAsset bool) error {
if len(attributes) == 0 || len(attributes) > MaxStarGiftCollectibleAttributesPerKind {
return ErrStarGiftCollectibleInvalid
}
seen := make(map[string]struct{}, len(attributes))
total := 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 {
return ErrStarGiftCollectibleInvalid
}
key := strings.ToLower(name)
if _, ok := seen[key]; ok {
return ErrStarGiftCollectibleInvalid
}
seen[key] = struct{}{}
total += attribute.RarityPermille
switch kind {
case StarGiftCollectibleModel, StarGiftCollectiblePattern:
if attribute.Animation == nil || len(attribute.Animation.JSON) == 0 ||
len(attribute.Animation.TGS) == 0 || len(attribute.Animation.SHA256) != 32 {
return ErrStarGiftCollectibleInvalid
}
if requireStoredAsset && (attribute.Document == nil || !attribute.Document.IsSticker() ||
attribute.Document.MimeType != "application/x-tgsticker" || attribute.Blob == nil) {
return ErrStarGiftCollectibleInvalid
}
case StarGiftCollectibleBackdrop:
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 ||
attribute.TextColor < 0 || attribute.TextColor > 0xffffff {
return ErrStarGiftCollectibleInvalid
}
default:
return ErrStarGiftCollectibleInvalid
}
}
if total != 1000 {
return ErrStarGiftCollectibleInvalid
}
return nil
}
// StarGiftCatalogHash 由客户端可见目录字段折叠出稳定 hash,供 getStarGifts NotModified。
func StarGiftCatalogHash(catalog []StarGift) int {
var h uint64
for _, g := range catalog {
h ^= uint64(g.ID)
h = h*0x4f25 + uint64(g.ID)
h = h*0x4f25 + uint64(g.RevisionID)
h = h*0x4f25 + uint64(g.Stars)
h = h*0x4f25 + uint64(g.ConvertStars)
h = h*0x4f25 + uint64(g.UpgradeStars)
h = h*0x4f25 + uint64(g.UpgradeTotal)
h = h*0x4f25 + uint64(g.UpgradeIssued)
h = h*0x4f25 + uint64(g.Sticker.ID)
for _, r := range g.Title {
h = h*131 + uint64(r)
}
}
return int(h & 0x7fffffff)
}
// StarGiftCollectionsHash 按服务端返回顺序折叠每个集合自己的稳定 hash。
func StarGiftCollectionsHash(collections []StarGiftCollection) int64 {
var h uint64
for _, collection := range collections {
h = h*0x4f25 + uint64(collection.Hash)
}
return int64(h & 0x7fffffffffffffff)
}
// StarGiftCollectionHash returns the per-collection hash exposed by starGiftCollection.hash.
func StarGiftCollectionHash(title string, giftIDs []int64) int64 {
h := uint64(0x534743)
for _, r := range title {
h = h*131 + uint64(r)
}
for _, id := range giftIDs {
h = h*0x4f25 + uint64(id)
}
return int64(h & 0x7fffffffffffffff)
}
// EncodeStarGiftCursor / DecodeStarGiftCursor 是 saved gifts keyset 游标(最后一条实例 id)。
func EncodeStarGiftCursor(id int64) string {
return base64.RawURLEncoding.EncodeToString([]byte(strconv.FormatInt(id, 10)))

View file

@ -20,21 +20,22 @@ type StarsBalance struct {
type StarsTransactionReason string
const (
StarsReasonGrant StarsTransactionReason = "grant" // 起始余额自动授予
StarsReasonTopup StarsTransactionReason = "topup" // 充值(本地铸造)
StarsReasonReaction StarsTransactionReason = "reaction" // 付费 reaction 花费
StarsReasonGift StarsTransactionReason = "gift" // 星礼花费/收取
StarsReasonPaidMedia StarsTransactionReason = "paid_media" // 付费媒体解锁
StarsReasonAdjust StarsTransactionReason = "adjust" // 兜底/人工调整
StarsReasonGrant StarsTransactionReason = "grant" // 起始余额自动授予
StarsReasonTopup StarsTransactionReason = "topup" // 充值(本地铸造)
StarsReasonReaction StarsTransactionReason = "reaction" // 付费 reaction 花费
StarsReasonGift StarsTransactionReason = "gift" // 星礼花费/收取
StarsReasonGiftUpgrade StarsTransactionReason = "gift_upgrade" // 普通礼物升级为唯一礼物
StarsReasonPaidMedia StarsTransactionReason = "paid_media" // 付费媒体解锁
StarsReasonAdjust StarsTransactionReason = "adjust" // 兜底/人工调整
)
// StarsTransaction 是一条账本流水。amount 带符号:贷记 > 0(含 refund/收取),借记 < 0。
type StarsTransaction struct {
ID int64 // 单调递增账本 id(keyset 游标)
UserID int64 // 账本归属
Peer Peer // 对手方(grant/topup 等无对手时为零 Peer)
Amount int64 // 带符号金额
Date int // Unix 秒
ID int64 // 单调递增账本 id(keyset 游标)
UserID int64 // 账本归属
Peer Peer // 对手方(grant/topup 等无对手时为零 Peer)
Amount int64 // 带符号金额
Date int // Unix 秒
Reason StarsTransactionReason
Title string // 可选,投影到 tg.StarsTransaction.Title
Description string // 可选,投影到 tg.StarsTransaction.Description

View file

@ -8,3 +8,14 @@ type UpdateState struct {
Date int
Seq int
}
// UpdateStateCommitMode describes what a physically delivered update-state
// response proves. Every delivered baseline advances the device-local
// confirmed cursor; only an explicitly audited getState baseline also proves
// that retention may advance the client-observed cursor to the same point.
type UpdateStateCommitMode uint8
const (
UpdateStateCommitDeliveredOnly UpdateStateCommitMode = iota + 1
UpdateStateCommitDeliveredAndObservedBaseline
)

View file

@ -3,14 +3,15 @@ package domain
import "errors"
var (
ErrUsernameInvalid = errors.New("username invalid")
ErrUsernameOccupied = errors.New("username occupied")
ErrUsernameNotOccupied = errors.New("username not occupied")
ErrPhoneNotOccupied = errors.New("phone not occupied")
ErrFirstNameInvalid = errors.New("first name invalid")
ErrAboutTooLong = errors.New("about too long")
ErrUserNotFound = errors.New("user not found")
ErrUserSendRestricted = errors.New("user send restricted")
ErrUsernameInvalid = errors.New("username invalid")
ErrUsernameOccupied = errors.New("username occupied")
ErrUsernameNotOccupied = errors.New("username not occupied")
ErrPhoneNotOccupied = errors.New("phone not occupied")
ErrFirstNameInvalid = errors.New("first name invalid")
ErrAboutTooLong = errors.New("about too long")
ErrUserNotFound = errors.New("user not found")
ErrUserFrozen = errors.New("user account frozen")
ErrAuthenticatedScopeInvalid = errors.New("authenticated user scope invalid")
// ErrPremiumRequired 表示该操作仅限有效会员(PREMIUM_ACCOUNT_REQUIRED)。
ErrPremiumRequired = errors.New("premium account required")
// ErrPremiumBotUnsupported 表示 bot 账号不可被授予会员(官方语义)。