feat: sync monoforum and collectible emoji status
Sync telesrv bd15657 (feat(account): implement collectible emoji status). Skipped telesrv docs changes per public sync rules.
This commit is contained in:
parent
edb7057757
commit
0c99ae0a9d
91 changed files with 4061 additions and 693 deletions
|
|
@ -603,17 +603,21 @@ type ChannelMessage struct {
|
|||
From Peer
|
||||
SendAs *Peer
|
||||
// SavedPeer 是 monoforum 私信子会话分组键(按订阅者分组);普通频道消息为零值。
|
||||
SavedPeer Peer
|
||||
Date int
|
||||
EditDate int
|
||||
Post bool
|
||||
Silent bool
|
||||
NoForwards bool
|
||||
Body string
|
||||
Entities []MessageEntity
|
||||
ReplyTo *MessageReply
|
||||
Forward *MessageForward
|
||||
ViaBotID int64
|
||||
SavedPeer Peer
|
||||
// SuggestedPost 是频道私信建议投稿的不可变发送快照;普通频道消息为 nil。
|
||||
SuggestedPost *SuggestedPost
|
||||
// PaidMessageStars 是本条频道私信实际扣除的 Stars;管理员免费回复及普通频道消息为 0。
|
||||
PaidMessageStars int64
|
||||
Date int
|
||||
EditDate int
|
||||
Post bool
|
||||
Silent bool
|
||||
NoForwards bool
|
||||
Body string
|
||||
Entities []MessageEntity
|
||||
ReplyTo *MessageReply
|
||||
Forward *MessageForward
|
||||
ViaBotID int64
|
||||
// GroupedID 相册分组 id(sendMultiMedia 同组共享非零值,非相册恒 0)。
|
||||
GroupedID int64
|
||||
ReplyMarkup *MessageReplyMarkup
|
||||
|
|
@ -1459,7 +1463,15 @@ type SendMonoforumMessageRequest struct {
|
|||
IdempotencyPreflighted bool
|
||||
Message string
|
||||
Entities []MessageEntity
|
||||
Date int
|
||||
Media *MessageMedia
|
||||
ReplyTo *MessageReply
|
||||
Silent bool
|
||||
NoForwards bool
|
||||
SuggestedPost *SuggestedPost
|
||||
// AllowPaidStars 是客户端授权的最高可扣金额;实际扣款取频道当前价格,绝不按授权上限扣款。
|
||||
AllowPaidStars int64
|
||||
ClearDraft bool
|
||||
Date int
|
||||
}
|
||||
|
||||
// ChannelSendReplayRequest addresses either a regular channel send (SavedPeer is zero) or one
|
||||
|
|
@ -1579,6 +1591,8 @@ type SendChannelMessageResult struct {
|
|||
Event ChannelUpdateEvent
|
||||
Recipients []int64
|
||||
Duplicate bool
|
||||
// SenderStarsBalance 仅在实际发生 paid-message 借记时返回;RPC 只向发件人投影余额更新。
|
||||
SenderStarsBalance *StarsBalance
|
||||
// ReplayDeleteEvent is the existing durable channel delete event paired
|
||||
// with a deleted exact-random_id replay. It must be returned only to the
|
||||
// caller echo and must never be fanned out as a fresh event.
|
||||
|
|
|
|||
|
|
@ -6,37 +6,38 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
ErrChannelInvalid = errors.New("channel invalid")
|
||||
ErrChannelPrivate = errors.New("channel private")
|
||||
ErrChannelTitleInvalid = errors.New("channel title invalid")
|
||||
ErrChannelUserBanned = errors.New("user banned in channel")
|
||||
ErrChannelWriteForbidden = errors.New("chat write forbidden")
|
||||
ErrChannelAdminRequired = errors.New("chat admin required")
|
||||
ErrChannelNotModified = errors.New("chat not modified")
|
||||
ErrChannelForumMissing = errors.New("channel forum missing")
|
||||
ErrLinkNotModified = errors.New("discussion link not modified")
|
||||
ErrChatDiscussionUnallowed = errors.New("chat discussion unallowed")
|
||||
ErrBroadcastIDInvalid = errors.New("broadcast id invalid")
|
||||
ErrMegagroupIDInvalid = errors.New("megagroup id invalid")
|
||||
ErrMegagroupPrehistoryHidden = errors.New("megagroup prehistory hidden")
|
||||
ErrChatPublicRequired = errors.New("chat public required")
|
||||
ErrChannelUserCreator = errors.New("channel user creator")
|
||||
ErrChannelRightForbidden = errors.New("channel right forbidden")
|
||||
ErrPersistentTimestamp = errors.New("persistent timestamp invalid")
|
||||
ErrInviteHashEmpty = errors.New("invite hash empty")
|
||||
ErrInviteHashInvalid = errors.New("invite hash invalid")
|
||||
ErrInviteHashExpired = errors.New("invite hash expired")
|
||||
ErrInvitePermanent = errors.New("chat invite permanent")
|
||||
ErrInviteRevokedMissing = errors.New("invite revoked missing")
|
||||
ErrInviteRequestSent = errors.New("invite request sent")
|
||||
ErrHideRequesterMissing = errors.New("hide requester missing")
|
||||
ErrUsersTooMuch = errors.New("users too much")
|
||||
ErrUserAlreadyParticipant = errors.New("user already participant")
|
||||
ErrUserKicked = errors.New("user kicked")
|
||||
ErrUserNotParticipant = errors.New("user not participant")
|
||||
ErrBotGroupsBlocked = errors.New("bot groups blocked")
|
||||
ErrReactionInvalid = errors.New("reaction invalid")
|
||||
ErrReactionsTooMany = errors.New("reactions too many")
|
||||
ErrChannelInvalid = errors.New("channel invalid")
|
||||
ErrChannelPrivate = errors.New("channel private")
|
||||
ErrChannelTitleInvalid = errors.New("channel title invalid")
|
||||
ErrChannelUserBanned = errors.New("user banned in channel")
|
||||
ErrChannelWriteForbidden = errors.New("chat write forbidden")
|
||||
ErrChannelAdminRequired = errors.New("chat admin required")
|
||||
ErrChannelNotModified = errors.New("chat not modified")
|
||||
ErrChannelForumMissing = errors.New("channel forum missing")
|
||||
ErrChannelMonoforumUnsupported = errors.New("channel monoforum unsupported")
|
||||
ErrLinkNotModified = errors.New("discussion link not modified")
|
||||
ErrChatDiscussionUnallowed = errors.New("chat discussion unallowed")
|
||||
ErrBroadcastIDInvalid = errors.New("broadcast id invalid")
|
||||
ErrMegagroupIDInvalid = errors.New("megagroup id invalid")
|
||||
ErrMegagroupPrehistoryHidden = errors.New("megagroup prehistory hidden")
|
||||
ErrChatPublicRequired = errors.New("chat public required")
|
||||
ErrChannelUserCreator = errors.New("channel user creator")
|
||||
ErrChannelRightForbidden = errors.New("channel right forbidden")
|
||||
ErrPersistentTimestamp = errors.New("persistent timestamp invalid")
|
||||
ErrInviteHashEmpty = errors.New("invite hash empty")
|
||||
ErrInviteHashInvalid = errors.New("invite hash invalid")
|
||||
ErrInviteHashExpired = errors.New("invite hash expired")
|
||||
ErrInvitePermanent = errors.New("chat invite permanent")
|
||||
ErrInviteRevokedMissing = errors.New("invite revoked missing")
|
||||
ErrInviteRequestSent = errors.New("invite request sent")
|
||||
ErrHideRequesterMissing = errors.New("hide requester missing")
|
||||
ErrUsersTooMuch = errors.New("users too much")
|
||||
ErrUserAlreadyParticipant = errors.New("user already participant")
|
||||
ErrUserKicked = errors.New("user kicked")
|
||||
ErrUserNotParticipant = errors.New("user not participant")
|
||||
ErrBotGroupsBlocked = errors.New("bot groups blocked")
|
||||
ErrReactionInvalid = errors.New("reaction invalid")
|
||||
ErrReactionsTooMany = errors.New("reactions too many")
|
||||
)
|
||||
|
||||
// SlowModeWaitError carries the remaining wait seconds for a channel slow mode violation.
|
||||
|
|
|
|||
|
|
@ -101,19 +101,42 @@ type DialogDraftWebPage struct {
|
|||
Optional bool
|
||||
}
|
||||
|
||||
type SuggestedPostPriceKind string
|
||||
|
||||
const (
|
||||
SuggestedPostPriceStars SuggestedPostPriceKind = "stars"
|
||||
SuggestedPostPriceTON SuggestedPostPriceKind = "ton"
|
||||
)
|
||||
|
||||
// SuggestedPostPrice is either a decimal Stars amount or a nanotons amount.
|
||||
type SuggestedPostPrice struct {
|
||||
Kind SuggestedPostPriceKind
|
||||
Amount int64
|
||||
Nanos int
|
||||
}
|
||||
|
||||
// SuggestedPost is the domain-only snapshot shared by a monoforum message and its cloud draft.
|
||||
type SuggestedPost struct {
|
||||
Accepted bool
|
||||
Rejected bool
|
||||
Price *SuggestedPostPrice
|
||||
ScheduleDate int
|
||||
}
|
||||
|
||||
// DialogDraft is a cloud draft for one peer/topic, expressed only in domain types.
|
||||
type DialogDraft struct {
|
||||
Peer Peer
|
||||
TopMessageID int
|
||||
Date int
|
||||
NoWebpage bool
|
||||
InvertMedia bool
|
||||
Message string
|
||||
Entities []MessageEntity
|
||||
ReplyTo *MessageReply
|
||||
WebPage *DialogDraftWebPage
|
||||
Effect int64
|
||||
RichMessage *MessageRichMessage
|
||||
Peer Peer
|
||||
TopMessageID int
|
||||
Date int
|
||||
NoWebpage bool
|
||||
InvertMedia bool
|
||||
Message string
|
||||
Entities []MessageEntity
|
||||
ReplyTo *MessageReply
|
||||
WebPage *DialogDraftWebPage
|
||||
Effect int64
|
||||
SuggestedPost *SuggestedPost
|
||||
RichMessage *MessageRichMessage
|
||||
}
|
||||
|
||||
// Empty reports whether this draft should clear the cloud draft slot.
|
||||
|
|
@ -126,6 +149,7 @@ func (d DialogDraft) Empty() bool {
|
|||
(d.ReplyTo == nil || replyOnlyTopic) &&
|
||||
d.WebPage == nil &&
|
||||
d.Effect == 0 &&
|
||||
d.SuggestedPost == nil &&
|
||||
d.RichMessage.IsZero()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -238,6 +238,29 @@ type UniqueStarGift struct {
|
|||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// CollectibleEmojiStatus projects an immutable unique gift into the complete
|
||||
// status shape consumed by Telegram clients. Ownership/lifecycle validation
|
||||
// is intentionally performed by the caller because it depends on the actor;
|
||||
// this helper validates only the immutable renderable facts.
|
||||
func CollectibleEmojiStatus(g UniqueStarGift) (EmojiStatusCollectible, bool) {
|
||||
status := EmojiStatusCollectible{
|
||||
CollectibleID: g.ID,
|
||||
Title: g.Title,
|
||||
Slug: g.Slug,
|
||||
CenterColor: g.Backdrop.CenterColor,
|
||||
EdgeColor: g.Backdrop.EdgeColor,
|
||||
PatternColor: g.Backdrop.PatternColor,
|
||||
TextColor: g.Backdrop.TextColor,
|
||||
}
|
||||
if g.Model.Document != nil {
|
||||
status.DocumentID = g.Model.Document.ID
|
||||
}
|
||||
if g.Pattern.Document != nil {
|
||||
status.PatternDocumentID = g.Pattern.Document.ID
|
||||
}
|
||||
return status, status.Valid()
|
||||
}
|
||||
|
||||
type StarGiftCurrency string
|
||||
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package domain
|
|||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
|
|
@ -31,8 +32,9 @@ const (
|
|||
StarsReasonGiftAuction StarsTransactionReason = "gift_auction"
|
||||
StarsReasonGiftPrepaid StarsTransactionReason = "gift_prepaid_upgrade"
|
||||
StarsReasonGiftDrop StarsTransactionReason = "gift_drop_original_details"
|
||||
StarsReasonPaidMedia StarsTransactionReason = "paid_media" // 付费媒体解锁
|
||||
StarsReasonAdjust StarsTransactionReason = "adjust" // 兜底/人工调整
|
||||
StarsReasonPaidMedia StarsTransactionReason = "paid_media" // 付费媒体解锁
|
||||
StarsReasonPaidMessage StarsTransactionReason = "paid_message" // 频道 Direct Message 花费
|
||||
StarsReasonAdjust StarsTransactionReason = "adjust" // 兜底/人工调整
|
||||
)
|
||||
|
||||
// StarsTransaction 是一条账本流水。amount 带符号:贷记 > 0(含 refund/收取),借记 < 0。
|
||||
|
|
@ -98,6 +100,17 @@ var (
|
|||
ErrStarsInvalidAmount = errors.New("stars: invalid amount")
|
||||
)
|
||||
|
||||
// StarsPaymentRequiredError reports the minimum paid-message authorization the
|
||||
// sender must include in allow_paid_stars. The authorization is a ceiling; the
|
||||
// ledger debits only the channel's current configured price.
|
||||
type StarsPaymentRequiredError struct {
|
||||
Stars int64
|
||||
}
|
||||
|
||||
func (e *StarsPaymentRequiredError) Error() string {
|
||||
return fmt.Sprintf("stars: allow payment required: %d", e.Stars)
|
||||
}
|
||||
|
||||
// EncodeStarsCursor 把 keyset 游标(最后一条流水 id)编码为客户端不透明字符串。
|
||||
func EncodeStarsCursor(id int64) string {
|
||||
return base64.RawURLEncoding.EncodeToString([]byte(strconv.FormatInt(id, 10)))
|
||||
|
|
|
|||
|
|
@ -29,8 +29,11 @@ const (
|
|||
UpdateEventPeerStoryBlocked UpdateEventType = "peer_story_blocked"
|
||||
// UpdateEventUserPhone 映射 updateUserPhone。它是账号绝对状态更新,TL
|
||||
// 构造器不携 pts;事件仍占账号 pts,以便其它设备在线/离线保持同一水位。
|
||||
UpdateEventUserPhone UpdateEventType = "user_phone"
|
||||
UpdateEventDeleteMessages UpdateEventType = "delete_messages"
|
||||
UpdateEventUserPhone UpdateEventType = "user_phone"
|
||||
// UpdateEventUserEmojiStatus carries the exact immutable status snapshot.
|
||||
// It consumes account pts even though updateUserEmojiStatus has no pts.
|
||||
UpdateEventUserEmojiStatus UpdateEventType = "user_emoji_status"
|
||||
UpdateEventDeleteMessages UpdateEventType = "delete_messages"
|
||||
// UpdateEventPinnedMessages 映射 updatePinnedMessages(私聊置顶/取消
|
||||
// 置顶;MessageIDs 是该 owner 自己视角的 box id,Bool 为 pinned)。
|
||||
// TL 构造器自带账号 pts/pts_count,不属于 LacksWirePts。
|
||||
|
|
@ -81,6 +84,7 @@ type UpdateEvent struct {
|
|||
Peers []Peer
|
||||
Bool bool
|
||||
Phone string
|
||||
EmojiStatus UserEmojiStatus
|
||||
Settings PeerSettings
|
||||
MessageIDs []int
|
||||
MaxID int
|
||||
|
|
@ -127,6 +131,7 @@ func (e UpdateEvent) LacksWirePts() bool {
|
|||
UpdateEventPeerSettings,
|
||||
UpdateEventPeerStoryBlocked,
|
||||
UpdateEventUserPhone,
|
||||
UpdateEventUserEmojiStatus,
|
||||
UpdateEventDialogFilter,
|
||||
UpdateEventDialogFilterOrder,
|
||||
UpdateEventDialogFilters,
|
||||
|
|
|
|||
|
|
@ -16,6 +16,73 @@ type PeerColor struct {
|
|||
BackgroundEmojiID int64
|
||||
}
|
||||
|
||||
// EmojiStatusCollectible is the immutable projection needed to render a
|
||||
// collectible gift as an emoji status. The source of truth remains the owned
|
||||
// UniqueStarGift; users store an immutable snapshot so every user projection,
|
||||
// online update and offline difference observes the same shape without an
|
||||
// RPC-layer lookup.
|
||||
type EmojiStatusCollectible struct {
|
||||
CollectibleID int64 `json:"collectible_id"`
|
||||
DocumentID int64 `json:"document_id"`
|
||||
Title string `json:"title"`
|
||||
Slug string `json:"slug"`
|
||||
PatternDocumentID int64 `json:"pattern_document_id"`
|
||||
CenterColor int `json:"center_color"`
|
||||
EdgeColor int `json:"edge_color"`
|
||||
PatternColor int `json:"pattern_color"`
|
||||
TextColor int `json:"text_color"`
|
||||
}
|
||||
|
||||
// Empty reports whether no collectible status is present.
|
||||
func (s EmojiStatusCollectible) Empty() bool {
|
||||
return s == (EmojiStatusCollectible{})
|
||||
}
|
||||
|
||||
// Valid enforces the complete collectible status shape. Partial snapshots
|
||||
// are forbidden because clients would otherwise render a gradient without its
|
||||
// model/pattern or be unable to resolve the collectible link.
|
||||
func (s EmojiStatusCollectible) Valid() bool {
|
||||
if s.CollectibleID <= 0 || s.DocumentID <= 0 || s.PatternDocumentID <= 0 ||
|
||||
s.Title == "" || s.Slug == "" {
|
||||
return false
|
||||
}
|
||||
for _, color := range []int{s.CenterColor, s.EdgeColor, s.PatternColor, s.TextColor} {
|
||||
if color < 0 || color > 0xffffff {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// UserEmojiStatus is the protocol-neutral mutation value accepted by the user
|
||||
// service/store boundary. Exactly one of a normal document or a complete
|
||||
// collectible snapshot may be active; the zero value clears the status.
|
||||
type UserEmojiStatus struct {
|
||||
DocumentID int64 `json:"document_id"`
|
||||
Until int `json:"until,omitempty"`
|
||||
Collectible EmojiStatusCollectible `json:"collectible,omitempty"`
|
||||
}
|
||||
|
||||
func (s UserEmojiStatus) Empty() bool {
|
||||
return s.DocumentID == 0 && s.Collectible.Empty()
|
||||
}
|
||||
|
||||
func (s UserEmojiStatus) Valid() bool {
|
||||
if s.Until < 0 {
|
||||
return false
|
||||
}
|
||||
if s.Empty() {
|
||||
return s.Until == 0
|
||||
}
|
||||
if s.DocumentID <= 0 {
|
||||
return false
|
||||
}
|
||||
if s.Collectible.Empty() {
|
||||
return true
|
||||
}
|
||||
return s.Collectible.Valid() && s.DocumentID == s.Collectible.DocumentID
|
||||
}
|
||||
|
||||
// Empty reports whether no explicit color/profile color state is set.
|
||||
func (c PeerColor) Empty() bool {
|
||||
return !c.HasColor && c.BackgroundEmojiID == 0
|
||||
|
|
@ -47,9 +114,11 @@ type User struct {
|
|||
PremiumUntil int
|
||||
// EmojiStatusDocumentID / EmojiStatusUntil 是用户自定义 emoji status
|
||||
//(premium 专属,account.updateEmojiStatus)。DocumentID==0 表示未设置;
|
||||
// Until==0 表示永久。
|
||||
EmojiStatusDocumentID int64
|
||||
EmojiStatusUntil int
|
||||
// Until==0 表示永久。EmojiStatusCollectible 非零时 DocumentID 必须等于
|
||||
// collectible 的 model document id。
|
||||
EmojiStatusDocumentID int64
|
||||
EmojiStatusUntil int
|
||||
EmojiStatusCollectible EmojiStatusCollectible
|
||||
// Birthday 是用户公开生日(account.updateBirthday)。零值表示未设置。
|
||||
Birthday Birthday
|
||||
// PersonalChannelID 是资料页展示的「个人频道」(account.updatePersonalChannel);
|
||||
|
|
@ -86,12 +155,21 @@ func (u User) PremiumActiveAt(now int64) bool {
|
|||
// (已设置且未过期;Until==0 表示永久)。emoji status 是 premium 专属,到期
|
||||
// 降级后即便列仍有残值也不再下发。
|
||||
func (u User) EmojiStatusActiveAt(now int64) bool {
|
||||
if !u.PremiumActiveAt(now) || u.EmojiStatusDocumentID == 0 {
|
||||
if !u.PremiumActiveAt(now) || !u.EmojiStatus().Valid() || u.EmojiStatusDocumentID == 0 {
|
||||
return false
|
||||
}
|
||||
return u.EmojiStatusUntil == 0 || int64(u.EmojiStatusUntil) > now
|
||||
}
|
||||
|
||||
// EmojiStatus returns the complete status snapshot carried by this user.
|
||||
func (u User) EmojiStatus() UserEmojiStatus {
|
||||
return UserEmojiStatus{
|
||||
DocumentID: u.EmojiStatusDocumentID,
|
||||
Until: u.EmojiStatusUntil,
|
||||
Collectible: u.EmojiStatusCollectible,
|
||||
}
|
||||
}
|
||||
|
||||
// DeletedTombstone strips every viewer-dependent or personally identifying
|
||||
// field while preserving the immutable id and lifecycle audit facts.
|
||||
func (u User) DeletedTombstone() User {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue