fix: harden channel rights and sticker compatibility
Co-authored-by: HSgram <3013954224@qq.com>
This commit is contained in:
parent
6867d201ed
commit
599453a3c4
33 changed files with 1520 additions and 130 deletions
|
|
@ -20,27 +20,27 @@ const (
|
|||
botInlineCacheMaxEntries = 256
|
||||
)
|
||||
|
||||
func botInlineDisabledErr() error { return tgerr.New(400, "BOT_INLINE_DISABLED") }
|
||||
func botInlineGeoNotAllowedErr() error { return tgerr.New(400, "BOT_INLINE_GEO_NOT_ALLOWED") }
|
||||
func botWebviewDisabledErr() error { return tgerr.New(400, "BOT_WEBVIEW_DISABLED") }
|
||||
func queryIDInvalidErr() error { return tgerr.New(400, "QUERY_ID_INVALID") }
|
||||
func queryIDEmptyErr() error { return tgerr.New(400, "QUERY_ID_EMPTY") }
|
||||
func resultIDEmptyErr() error { return tgerr.New(400, "RESULT_ID_EMPTY") }
|
||||
func resultIDInvalidErr() error { return tgerr.New(400, "RESULT_ID_INVALID") }
|
||||
func resultIDDuplicateErr() error { return tgerr.New(400, "RESULT_ID_DUPLICATE") }
|
||||
func resultTypeInvalidErr() error { return tgerr.New(400, "RESULT_TYPE_INVALID") }
|
||||
func resultsTooMuchErr() error { return tgerr.New(400, "RESULTS_TOO_MUCH") }
|
||||
func sendMessageTypeInvalidErr() error { return tgerr.New(400, "SEND_MESSAGE_TYPE_INVALID") }
|
||||
func nextOffsetInvalidErr() error { return tgerr.New(400, "NEXT_OFFSET_INVALID") }
|
||||
func startParamEmptyErr() error { return tgerr.New(400, "START_PARAM_EMPTY") }
|
||||
func switchPmTextEmptyErr() error { return tgerr.New(400, "SWITCH_PM_TEXT_EMPTY") }
|
||||
func switchWebviewInvalidErr() error { return tgerr.New(400, "SWITCH_WEBVIEW_URL_INVALID") }
|
||||
func inlineResultExpiredErr() error { return tgerr.New(400, "INLINE_RESULT_EXPIRED") }
|
||||
func webDocumentInvalidErr() error { return tgerr.New(400, "WEBDOCUMENT_INVALID") }
|
||||
func webDocumentMimeInvalidErr() error { return tgerr.New(400, "WEBDOCUMENT_MIME_INVALID") }
|
||||
func webDocumentSizeTooBigErr() error { return tgerr.New(400, "WEBDOCUMENT_SIZE_TOO_BIG") }
|
||||
func webDocumentURLEmptyErr() error { return tgerr.New(400, "WEBDOCUMENT_URL_EMPTY") }
|
||||
func webDocumentURLInvalidErr() error { return tgerr.New(400, "WEBDOCUMENT_URL_INVALID") }
|
||||
func botInlineDisabledErr() error { return tgerr.New(400, "BOT_INLINE_DISABLED") }
|
||||
func botInlineGeoNotAllowedErr() error { return tgerr.New(400, "BOT_INLINE_GEO_NOT_ALLOWED") }
|
||||
func botWebviewDisabledErr() error { return tgerr.New(400, "BOT_WEBVIEW_DISABLED") }
|
||||
func queryIDInvalidErr() error { return tgerr.New(400, "QUERY_ID_INVALID") }
|
||||
func queryIDEmptyErr() error { return tgerr.New(400, "QUERY_ID_EMPTY") }
|
||||
func resultIDEmptyErr() error { return tgerr.New(400, "RESULT_ID_EMPTY") }
|
||||
func resultIDInvalidErr() error { return tgerr.New(400, "RESULT_ID_INVALID") }
|
||||
func resultIDDuplicateErr() error { return tgerr.New(400, "RESULT_ID_DUPLICATE") }
|
||||
func resultTypeInvalidErr() error { return tgerr.New(400, "RESULT_TYPE_INVALID") }
|
||||
func resultsTooMuchErr() error { return tgerr.New(400, "RESULTS_TOO_MUCH") }
|
||||
func sendMessageTypeInvalidErr() error { return tgerr.New(400, "SEND_MESSAGE_TYPE_INVALID") }
|
||||
func nextOffsetInvalidErr() error { return tgerr.New(400, "NEXT_OFFSET_INVALID") }
|
||||
func startParamEmptyErr() error { return tgerr.New(400, "START_PARAM_EMPTY") }
|
||||
func switchPmTextEmptyErr() error { return tgerr.New(400, "SWITCH_PM_TEXT_EMPTY") }
|
||||
func switchWebviewInvalidErr() error { return tgerr.New(400, "SWITCH_WEBVIEW_URL_INVALID") }
|
||||
func inlineResultExpiredErr() error { return tgerr.New(400, "INLINE_RESULT_EXPIRED") }
|
||||
func webDocumentInvalidErr() error { return tgerr.New(400, "WEBDOCUMENT_INVALID") }
|
||||
func webDocumentMimeInvalidErr() error { return tgerr.New(400, "WEBDOCUMENT_MIME_INVALID") }
|
||||
func webDocumentSizeTooBigErr() error { return tgerr.New(400, "WEBDOCUMENT_SIZE_TOO_BIG") }
|
||||
func webDocumentURLEmptyErr() error { return tgerr.New(400, "WEBDOCUMENT_URL_EMPTY") }
|
||||
func webDocumentURLInvalidErr() error { return tgerr.New(400, "WEBDOCUMENT_URL_INVALID") }
|
||||
|
||||
func (r *Router) onMessagesGetInlineBotResults(ctx context.Context, req *tg.MessagesGetInlineBotResultsRequest) (*tg.MessagesBotResults, error) {
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
|
|
@ -1132,7 +1132,7 @@ func tgInlineWebDocument(in domain.BotInlineWebDocument) tg.WebDocumentClass {
|
|||
AccessHash: in.AccessHash,
|
||||
Size: in.Size,
|
||||
MimeType: in.MimeType,
|
||||
Attributes: tgDocumentAttributes(in.Attributes),
|
||||
Attributes: tgDocumentAttributes(in.MimeType, in.Attributes),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -808,20 +808,29 @@ func domainChannelAdminRights(rights tg.ChatAdminRights) domain.ChannelAdminRigh
|
|||
|
||||
func tgChatBannedRights(rights domain.ChannelBannedRights) tg.ChatBannedRights {
|
||||
return tg.ChatBannedRights{
|
||||
ViewMessages: rights.ViewMessages,
|
||||
SendMessages: rights.SendMessages,
|
||||
SendMedia: rights.SendMedia,
|
||||
SendStickers: rights.SendStickers,
|
||||
SendGifs: rights.SendGifs,
|
||||
SendGames: rights.SendGames,
|
||||
SendInline: rights.SendInline,
|
||||
EmbedLinks: rights.EmbedLinks,
|
||||
SendPolls: rights.SendPolls,
|
||||
ChangeInfo: rights.ChangeInfo,
|
||||
InviteUsers: rights.InviteUsers,
|
||||
PinMessages: rights.PinMessages,
|
||||
EditRank: rights.EditRank,
|
||||
UntilDate: rights.UntilDate,
|
||||
ViewMessages: rights.ViewMessages,
|
||||
SendMessages: rights.SendMessages,
|
||||
SendMedia: rights.SendMedia,
|
||||
SendStickers: rights.SendStickers,
|
||||
SendGifs: rights.SendGifs,
|
||||
SendGames: rights.SendGames,
|
||||
SendInline: rights.SendInline,
|
||||
EmbedLinks: rights.EmbedLinks,
|
||||
SendPolls: rights.SendPolls,
|
||||
ChangeInfo: rights.ChangeInfo,
|
||||
InviteUsers: rights.InviteUsers,
|
||||
PinMessages: rights.PinMessages,
|
||||
ManageTopics: rights.ManageTopics,
|
||||
SendPhotos: rights.SendPhotos,
|
||||
SendVideos: rights.SendVideos,
|
||||
SendRoundvideos: rights.SendRoundvideos,
|
||||
SendAudios: rights.SendAudios,
|
||||
SendVoices: rights.SendVoices,
|
||||
SendDocs: rights.SendDocs,
|
||||
SendPlain: rights.SendPlain,
|
||||
EditRank: rights.EditRank,
|
||||
SendReactions: rights.SendReactions,
|
||||
UntilDate: rights.UntilDate,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -835,20 +844,29 @@ func tgDefaultChatBannedRights(rights domain.ChannelBannedRights) tg.ChatBannedR
|
|||
|
||||
func domainChannelBannedRights(rights tg.ChatBannedRights) domain.ChannelBannedRights {
|
||||
return domain.ChannelBannedRights{
|
||||
ViewMessages: rights.ViewMessages,
|
||||
SendMessages: rights.SendMessages,
|
||||
SendMedia: rights.SendMedia,
|
||||
SendStickers: rights.SendStickers,
|
||||
SendGifs: rights.SendGifs,
|
||||
SendGames: rights.SendGames,
|
||||
SendInline: rights.SendInline,
|
||||
EmbedLinks: rights.EmbedLinks,
|
||||
SendPolls: rights.SendPolls,
|
||||
ChangeInfo: rights.ChangeInfo,
|
||||
InviteUsers: rights.InviteUsers,
|
||||
PinMessages: rights.PinMessages,
|
||||
EditRank: rights.EditRank,
|
||||
UntilDate: rights.UntilDate,
|
||||
ViewMessages: rights.ViewMessages,
|
||||
SendMessages: rights.SendMessages,
|
||||
SendMedia: rights.SendMedia,
|
||||
SendStickers: rights.SendStickers,
|
||||
SendGifs: rights.SendGifs,
|
||||
SendGames: rights.SendGames,
|
||||
SendInline: rights.SendInline,
|
||||
EmbedLinks: rights.EmbedLinks,
|
||||
SendPolls: rights.SendPolls,
|
||||
ChangeInfo: rights.ChangeInfo,
|
||||
InviteUsers: rights.InviteUsers,
|
||||
PinMessages: rights.PinMessages,
|
||||
ManageTopics: rights.ManageTopics,
|
||||
SendPhotos: rights.SendPhotos,
|
||||
SendVideos: rights.SendVideos,
|
||||
SendRoundvideos: rights.SendRoundvideos,
|
||||
SendAudios: rights.SendAudios,
|
||||
SendVoices: rights.SendVoices,
|
||||
SendDocs: rights.SendDocs,
|
||||
SendPlain: rights.SendPlain,
|
||||
EditRank: rights.EditRank,
|
||||
SendReactions: rights.SendReactions,
|
||||
UntilDate: rights.UntilDate,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -67,3 +67,37 @@ func TestTGChannelFullIncludesExportedInvite(t *testing.T) {
|
|||
t.Fatalf("channelFull.exported_invite = %#v, want active permanent invite", invite)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChannelBannedRightsRoundTripModernFields(t *testing.T) {
|
||||
in := tg.ChatBannedRights{
|
||||
ViewMessages: true,
|
||||
SendMessages: true,
|
||||
SendMedia: true,
|
||||
SendStickers: true,
|
||||
SendGifs: true,
|
||||
SendGames: true,
|
||||
SendInline: true,
|
||||
EmbedLinks: true,
|
||||
SendPolls: true,
|
||||
ChangeInfo: true,
|
||||
InviteUsers: true,
|
||||
PinMessages: true,
|
||||
ManageTopics: true,
|
||||
SendPhotos: true,
|
||||
SendVideos: true,
|
||||
SendRoundvideos: true,
|
||||
SendAudios: true,
|
||||
SendVoices: true,
|
||||
SendDocs: true,
|
||||
SendPlain: true,
|
||||
EditRank: true,
|
||||
SendReactions: true,
|
||||
UntilDate: 12345,
|
||||
}
|
||||
domainRights := domainChannelBannedRights(in)
|
||||
out := tgChatBannedRights(domainRights)
|
||||
|
||||
if out != in {
|
||||
t.Fatalf("banned rights round-trip = %+v, want %+v", out, in)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import (
|
|||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
const mimeApplicationXTGSticker = "application/x-tgsticker"
|
||||
|
||||
// 本文件集中 domain media 值对象 → tg.* 的转换;tg.* 只在 rpc 层出现。
|
||||
// 供 reaction / sticker 资源 RPC 与消息 media 共用。
|
||||
|
||||
|
|
@ -27,9 +29,13 @@ func tgMessageMedia(m *domain.MessageMedia) tg.MessageMediaClass {
|
|||
}
|
||||
return out
|
||||
case domain.MessageMediaKindDocument:
|
||||
nopremium := m.Nopremium
|
||||
if m.Document != nil && m.Document.IsSticker() {
|
||||
nopremium = true
|
||||
}
|
||||
out := &tg.MessageMediaDocument{
|
||||
Spoiler: m.Spoiler,
|
||||
Nopremium: m.Nopremium,
|
||||
Nopremium: nopremium,
|
||||
Voice: m.Voice,
|
||||
Round: m.Round,
|
||||
Video: m.Video,
|
||||
|
|
@ -267,9 +273,9 @@ func tgDocument(d domain.Document) tg.DocumentClass {
|
|||
Date: d.Date,
|
||||
MimeType: d.MimeType,
|
||||
Size: d.Size,
|
||||
Thumbs: tgDocumentThumbs(d.Thumbs),
|
||||
Thumbs: tgDocumentThumbs(d.MimeType, d.Thumbs),
|
||||
DCID: d.DCID,
|
||||
Attributes: tgDocumentAttributes(d.Attributes),
|
||||
Attributes: tgDocumentAttributes(d.MimeType, d.Attributes),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -281,12 +287,15 @@ func tgDocuments(docs []domain.Document) []tg.DocumentClass {
|
|||
return out
|
||||
}
|
||||
|
||||
func tgDocumentThumbs(sizes []domain.PhotoSize) []tg.PhotoSizeClass {
|
||||
func tgDocumentThumbs(mimeType string, sizes []domain.PhotoSize) []tg.PhotoSizeClass {
|
||||
if len(sizes) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]tg.PhotoSizeClass, 0, len(sizes))
|
||||
for _, s := range sizes {
|
||||
if isSeedSyntheticTGStickerPreviewThumb(mimeType, s) {
|
||||
continue
|
||||
}
|
||||
if s.Kind == domain.PhotoSizeKindCached && len(s.Bytes) > 0 {
|
||||
size := s.Size
|
||||
if size == 0 {
|
||||
|
|
@ -302,6 +311,17 @@ func tgDocumentThumbs(sizes []domain.PhotoSize) []tg.PhotoSizeClass {
|
|||
return compactPhotoSizeClasses(out)
|
||||
}
|
||||
|
||||
func isSeedSyntheticTGStickerPreviewThumb(mimeType string, s domain.PhotoSize) bool {
|
||||
// Older seed imports gave TGS documents without thumbnails a 1x1 transparent
|
||||
// "m" PNG. Clients can prefer that unusable preview and render blank stickers.
|
||||
return mimeType == mimeApplicationXTGSticker &&
|
||||
s.Kind == domain.PhotoSizeKindCached &&
|
||||
s.Type == "m" &&
|
||||
s.W <= 1 &&
|
||||
s.H <= 1 &&
|
||||
len(s.Bytes) > 0
|
||||
}
|
||||
|
||||
func tgPhotoSizes(sizes []domain.PhotoSize) []tg.PhotoSizeClass {
|
||||
if len(sizes) == 0 {
|
||||
return nil
|
||||
|
|
@ -408,15 +428,19 @@ func compactPhotoSizeClasses(in []tg.PhotoSizeClass) []tg.PhotoSizeClass {
|
|||
return out
|
||||
}
|
||||
|
||||
func tgDocumentAttributes(attrs []domain.DocumentAttribute) []tg.DocumentAttributeClass {
|
||||
out := make([]tg.DocumentAttributeClass, 0, len(attrs))
|
||||
func tgDocumentAttributes(mimeType string, attrs []domain.DocumentAttribute) []tg.DocumentAttributeClass {
|
||||
out := make([]tg.DocumentAttributeClass, 0, len(attrs)+1)
|
||||
hasAnimated := false
|
||||
hasStickerLike := false
|
||||
for _, a := range attrs {
|
||||
switch a.Kind {
|
||||
case domain.DocAttrImageSize:
|
||||
out = append(out, &tg.DocumentAttributeImageSize{W: a.W, H: a.H})
|
||||
case domain.DocAttrAnimated:
|
||||
hasAnimated = true
|
||||
out = append(out, &tg.DocumentAttributeAnimated{})
|
||||
case domain.DocAttrSticker:
|
||||
hasStickerLike = true
|
||||
out = append(out, &tg.DocumentAttributeSticker{
|
||||
Mask: a.Mask,
|
||||
Alt: a.Alt,
|
||||
|
|
@ -444,6 +468,7 @@ func tgDocumentAttributes(attrs []domain.DocumentAttribute) []tg.DocumentAttribu
|
|||
case domain.DocAttrFilename:
|
||||
out = append(out, &tg.DocumentAttributeFilename{FileName: a.FileName})
|
||||
case domain.DocAttrCustomEmoji:
|
||||
hasStickerLike = true
|
||||
out = append(out, &tg.DocumentAttributeCustomEmoji{
|
||||
Free: a.Free,
|
||||
TextColor: a.TextColor,
|
||||
|
|
@ -452,6 +477,9 @@ func tgDocumentAttributes(attrs []domain.DocumentAttribute) []tg.DocumentAttribu
|
|||
})
|
||||
}
|
||||
}
|
||||
if mimeType == mimeApplicationXTGSticker && hasStickerLike && !hasAnimated {
|
||||
out = append(out, &tg.DocumentAttributeAnimated{})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
|
|
@ -671,6 +699,12 @@ func stickerSetRefFromInput(input tg.InputStickerSetClass) (domain.StickerSetRef
|
|||
return domain.StickerSetRef{Kind: domain.StickerSetRefBySystem, SystemKey: "emoji_generic_animations"}, true
|
||||
case *tg.InputStickerSetEmojiDefaultStatuses:
|
||||
return domain.StickerSetRef{Kind: domain.StickerSetRefBySystem, SystemKey: domain.StickerSetSystemKeyEmojiDefaultStatuses}, true
|
||||
case *tg.InputStickerSetEmojiChannelDefaultStatuses:
|
||||
return domain.StickerSetRef{Kind: domain.StickerSetRefBySystem, SystemKey: domain.StickerSetSystemKeyEmojiDefaultStatuses}, true
|
||||
case *tg.InputStickerSetEmojiDefaultTopicIcons:
|
||||
return domain.StickerSetRef{Kind: domain.StickerSetRefBySystem, SystemKey: domain.StickerSetSystemKeyEmojiDefaultTopicIcons}, true
|
||||
case *tg.InputStickerSetPremiumGifts:
|
||||
return domain.StickerSetRef{Kind: domain.StickerSetRefBySystem, SystemKey: domain.StickerSetSystemKeyPremiumGifts}, true
|
||||
case *tg.InputStickerSetDice:
|
||||
return domain.StickerSetRef{Kind: domain.StickerSetRefBySystem, SystemKey: "dice:" + in.Emoticon}, true
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package rpc
|
|||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
|
@ -51,6 +53,20 @@ func (r *Router) onMessagesSendReaction(ctx context.Context, req *tg.MessagesSen
|
|||
return updates, nil
|
||||
}
|
||||
if peer.Type == domain.PeerTypeUser && r.deps.Messages != nil {
|
||||
if len(reactions) == 0 && r.shouldSuppressTransientPrivateReactionClear(userID, peer, req.MsgID, date) {
|
||||
res, err := r.deps.Messages.GetMessageReactions(ctx, userID, domain.PrivateMessageReactionsRequest{
|
||||
OwnerUserID: userID,
|
||||
Peer: peer,
|
||||
IDs: []int{req.MsgID},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, messageReactionErr(err)
|
||||
}
|
||||
return r.privateMessagesReactionsUpdates(ctx, userID, peer, res, []int{req.MsgID}), nil
|
||||
}
|
||||
if req.Big && len(reactions) > 0 {
|
||||
r.rememberTransientPrivateBigReaction(userID, peer, req.MsgID, date)
|
||||
}
|
||||
res, err := r.deps.Messages.SetMessageReactions(ctx, userID, domain.SetPrivateMessageReactionsRequest{
|
||||
UserID: userID,
|
||||
Peer: peer,
|
||||
|
|
@ -64,6 +80,9 @@ func (r *Router) onMessagesSendReaction(ctx context.Context, req *tg.MessagesSen
|
|||
if err != nil {
|
||||
return nil, messageReactionErr(err)
|
||||
}
|
||||
if len(reactions) == 0 {
|
||||
r.forgetTransientPrivateBigReaction(userID, peer, req.MsgID)
|
||||
}
|
||||
if err := r.recordMessageReactionUse(ctx, userID, reactions, req.GetAddToRecent(), date); err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
|
|
@ -94,6 +113,104 @@ func (r *Router) onMessagesSendReaction(ctx context.Context, req *tg.MessagesSen
|
|||
return tgEmptyUpdates(int(r.clock.Now().Unix())), nil
|
||||
}
|
||||
|
||||
const (
|
||||
transientPrivateBigReactionClearWindowSeconds = 3
|
||||
transientPrivateBigReactionMaxEntries = 4096
|
||||
)
|
||||
|
||||
type transientPrivateBigReactionKey struct {
|
||||
UserID int64
|
||||
PeerID int64
|
||||
MessageID int
|
||||
}
|
||||
|
||||
type transientPrivateBigReactionEntry struct {
|
||||
ExpiresAt int
|
||||
}
|
||||
|
||||
type transientPrivateBigReactionCache struct {
|
||||
mu sync.Mutex
|
||||
entries map[transientPrivateBigReactionKey]transientPrivateBigReactionEntry
|
||||
}
|
||||
|
||||
func transientPrivateBigReactionMapKey(userID int64, peer domain.Peer, messageID int) transientPrivateBigReactionKey {
|
||||
return transientPrivateBigReactionKey{
|
||||
UserID: userID,
|
||||
PeerID: peer.ID,
|
||||
MessageID: messageID,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) rememberTransientPrivateBigReaction(userID int64, peer domain.Peer, messageID int, date int) {
|
||||
if peer.Type != domain.PeerTypeUser || userID == 0 || peer.ID == 0 || messageID <= 0 || date <= 0 {
|
||||
return
|
||||
}
|
||||
r.transientPrivateBigReactions.remember(transientPrivateBigReactionMapKey(userID, peer, messageID), date+transientPrivateBigReactionClearWindowSeconds, date)
|
||||
}
|
||||
|
||||
func (r *Router) shouldSuppressTransientPrivateReactionClear(userID int64, peer domain.Peer, messageID int, date int) bool {
|
||||
return r.transientPrivateBigReactions.shouldSuppress(transientPrivateBigReactionMapKey(userID, peer, messageID), date)
|
||||
}
|
||||
|
||||
func (r *Router) forgetTransientPrivateBigReaction(userID int64, peer domain.Peer, messageID int) {
|
||||
r.transientPrivateBigReactions.forget(transientPrivateBigReactionMapKey(userID, peer, messageID))
|
||||
}
|
||||
|
||||
func (c *transientPrivateBigReactionCache) remember(key transientPrivateBigReactionKey, expiresAt int, now int) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.entries == nil {
|
||||
c.entries = make(map[transientPrivateBigReactionKey]transientPrivateBigReactionEntry)
|
||||
}
|
||||
if len(c.entries) >= transientPrivateBigReactionMaxEntries {
|
||||
c.pruneLocked(now)
|
||||
}
|
||||
if len(c.entries) >= transientPrivateBigReactionMaxEntries {
|
||||
c.dropOneLocked()
|
||||
}
|
||||
c.entries[key] = transientPrivateBigReactionEntry{ExpiresAt: expiresAt}
|
||||
}
|
||||
|
||||
func (c *transientPrivateBigReactionCache) shouldSuppress(key transientPrivateBigReactionKey, now int) bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
entry, ok := c.entries[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if now > entry.ExpiresAt {
|
||||
delete(c.entries, key)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *transientPrivateBigReactionCache) forget(key transientPrivateBigReactionKey) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
delete(c.entries, key)
|
||||
}
|
||||
|
||||
func (c *transientPrivateBigReactionCache) pruneLocked(now int) {
|
||||
for key, entry := range c.entries {
|
||||
if now > entry.ExpiresAt {
|
||||
delete(c.entries, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *transientPrivateBigReactionCache) dropOneLocked() {
|
||||
var oldestKey transientPrivateBigReactionKey
|
||||
oldestExpiresAt := int(^uint(0) >> 1)
|
||||
for key, entry := range c.entries {
|
||||
if entry.ExpiresAt < oldestExpiresAt {
|
||||
oldestKey = key
|
||||
oldestExpiresAt = entry.ExpiresAt
|
||||
}
|
||||
}
|
||||
delete(c.entries, oldestKey)
|
||||
}
|
||||
|
||||
func (r *Router) recordMessageReactionUse(ctx context.Context, userID int64, reactions []domain.MessageReaction, addToRecent bool, date int) error {
|
||||
if len(reactions) == 0 || r.deps.Channels == nil {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -294,3 +294,30 @@ func TestMessagesSendReactionPrivatePushesViewerLocalMessageID(t *testing.T) {
|
|||
t.Fatalf("pushed recent reactions = %+v set=%v, want one unread non-my reaction", recent, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransientPrivateBigReactionCacheIsBoundedAndExpires(t *testing.T) {
|
||||
r := &Router{}
|
||||
peer := domain.Peer{Type: domain.PeerTypeUser, ID: 2002}
|
||||
r.rememberTransientPrivateBigReaction(1001, peer, 1, 10)
|
||||
if !r.shouldSuppressTransientPrivateReactionClear(1001, peer, 1, 12) {
|
||||
t.Fatalf("transient big reaction clear should be suppressed inside window")
|
||||
}
|
||||
if r.shouldSuppressTransientPrivateReactionClear(1001, peer, 1, 14) {
|
||||
t.Fatalf("transient big reaction clear should not be suppressed after expiry")
|
||||
}
|
||||
|
||||
var cache transientPrivateBigReactionCache
|
||||
for i := 0; i < transientPrivateBigReactionMaxEntries+100; i++ {
|
||||
cache.remember(transientPrivateBigReactionKey{
|
||||
UserID: 1001,
|
||||
PeerID: int64(2000 + i),
|
||||
MessageID: i + 1,
|
||||
}, 100+i, 1)
|
||||
}
|
||||
cache.mu.Lock()
|
||||
got := len(cache.entries)
|
||||
cache.mu.Unlock()
|
||||
if got > transientPrivateBigReactionMaxEntries {
|
||||
t.Fatalf("transient cache entries = %d, want <= %d", got, transientPrivateBigReactionMaxEntries)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ func (r *Router) registerMessages(d *tg.ServerDispatcher) {
|
|||
d.OnMessagesGetMaskStickers(r.onMessagesGetMaskStickers)
|
||||
d.OnMessagesGetFeaturedStickers(r.onMessagesGetFeaturedStickers)
|
||||
d.OnMessagesGetFeaturedEmojiStickers(r.onMessagesGetFeaturedEmojiStickers)
|
||||
d.OnMessagesGetOldFeaturedStickers(r.onMessagesGetOldFeaturedStickers)
|
||||
d.OnMessagesGetRecentStickers(r.onMessagesGetRecentStickers)
|
||||
d.OnMessagesGetFavedStickers(r.onMessagesGetFavedStickers)
|
||||
d.OnMessagesGetSavedGifs(r.onMessagesGetSavedGifs)
|
||||
|
|
@ -109,10 +110,7 @@ func (r *Router) registerMessages(d *tg.ServerDispatcher) {
|
|||
d.OnMessagesGetEmojiStatusGroups(func(ctx context.Context, hash int) (tg.MessagesEmojiGroupsClass, error) {
|
||||
return tdesktop.EmojiStatusGroups(), nil
|
||||
})
|
||||
d.OnMessagesGetEmojiStickerGroups(func(ctx context.Context, hash int) (tg.MessagesEmojiGroupsClass, error) {
|
||||
// 自定义 emoji 贴纸的分类(Premium);telesrv 未 seed custom-emoji 集,保持空。
|
||||
return &tg.MessagesEmojiGroupsNotModified{}, nil
|
||||
})
|
||||
d.OnMessagesGetEmojiStickerGroups(r.onMessagesGetEmojiStickerGroups)
|
||||
d.OnMessagesGetEmojiProfilePhotoGroups(func(ctx context.Context, hash int) (tg.MessagesEmojiGroupsClass, error) {
|
||||
return tdesktop.EmojiProfilePhotoGroups(), nil
|
||||
})
|
||||
|
|
|
|||
|
|
@ -117,18 +117,19 @@ type Router struct {
|
|||
// updateStatus 高频续期时数秒内只落一次 DB。
|
||||
lastSeenPersist sync.Map // userID(int64) -> int64(unix)
|
||||
// tempKeyResolveCache 缓存 rawTempKeyID -> resolved perm(带过期),容量有界。
|
||||
tempKeyResolveCache *tempKeyResolveCache
|
||||
storyProjectionCache *storyProjectionCache
|
||||
storyPinnedCache *storyPinnedAvailableCache
|
||||
storyPinnedListCache *storyPinnedStoriesCache
|
||||
channelFullBotCache *channelFullBotInfoCache
|
||||
userFullProjectionCache *userFullProjectionCache
|
||||
peerSettingsProjectionCache *peerSettingsProjectionCache
|
||||
channelFullProjectionCache *channelFullProjectionCache
|
||||
emojiStickers *emojiStickerIndex
|
||||
notifySettings *notifySettingsCache
|
||||
stickerCatalog *stickerCatalogCache
|
||||
accountSettings *accountSettingsCache
|
||||
tempKeyResolveCache *tempKeyResolveCache
|
||||
storyProjectionCache *storyProjectionCache
|
||||
storyPinnedCache *storyPinnedAvailableCache
|
||||
storyPinnedListCache *storyPinnedStoriesCache
|
||||
channelFullBotCache *channelFullBotInfoCache
|
||||
userFullProjectionCache *userFullProjectionCache
|
||||
peerSettingsProjectionCache *peerSettingsProjectionCache
|
||||
channelFullProjectionCache *channelFullProjectionCache
|
||||
emojiStickers *emojiStickerIndex
|
||||
notifySettings *notifySettingsCache
|
||||
stickerCatalog *stickerCatalogCache
|
||||
transientPrivateBigReactions transientPrivateBigReactionCache
|
||||
accountSettings *accountSettingsCache
|
||||
// webPageResolveSem 是链接预览异步解析的并发信号量(有界):发送后把 pending 占位
|
||||
// 解析为卡片并就地替换。满则丢弃任务(消息留 pending)。nil=未启用(测试可直接调
|
||||
// resolvePendingWebPage 同步验证)。
|
||||
|
|
|
|||
|
|
@ -819,6 +819,9 @@ func (r *Router) messageContactUserID(ctx context.Context, userID int64, phone s
|
|||
// messageMediaFromDocument 由 Document 构造 MessageMedia,并从属性推导 Video/Round/Voice 标志。
|
||||
func messageMediaFromDocument(doc domain.Document, spoiler bool, ttl int) *domain.MessageMedia {
|
||||
media := &domain.MessageMedia{Kind: domain.MessageMediaKindDocument, Document: &doc, Spoiler: spoiler, TTLSeconds: ttl}
|
||||
if doc.IsSticker() {
|
||||
media.Nopremium = true
|
||||
}
|
||||
for _, attr := range doc.Attributes {
|
||||
switch attr.Kind {
|
||||
case domain.DocAttrVideo:
|
||||
|
|
|
|||
|
|
@ -801,6 +801,9 @@ func TestSendMediaPrivateSticker(t *testing.T) {
|
|||
if !ok {
|
||||
t.Fatalf("expected MessageMediaDocument, got %T", msg.Media)
|
||||
}
|
||||
if !media.Nopremium {
|
||||
t.Fatal("sticker message media missing nopremium flag")
|
||||
}
|
||||
doc, ok := media.Document.(*tg.Document)
|
||||
if !ok {
|
||||
t.Fatalf("expected tg.Document, got %T", media.Document)
|
||||
|
|
@ -822,6 +825,28 @@ func TestSendMediaPrivateSticker(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestTGMessageMediaDocumentMarksHistoricalStickerNopremium(t *testing.T) {
|
||||
media := tgMessageMedia(&domain.MessageMedia{
|
||||
Kind: domain.MessageMediaKindDocument,
|
||||
Document: &domain.Document{
|
||||
ID: 555,
|
||||
AccessHash: 5,
|
||||
MimeType: "application/x-tgsticker",
|
||||
Attributes: []domain.DocumentAttribute{
|
||||
{Kind: domain.DocAttrImageSize, W: 512, H: 512},
|
||||
{Kind: domain.DocAttrSticker, Alt: "🙂", StickerSetID: 10, StickerSetAccessHash: 20},
|
||||
},
|
||||
},
|
||||
})
|
||||
docMedia, ok := media.(*tg.MessageMediaDocument)
|
||||
if !ok {
|
||||
t.Fatalf("media = %T, want *tg.MessageMediaDocument", media)
|
||||
}
|
||||
if !docMedia.Nopremium {
|
||||
t.Fatal("historical sticker message media missing nopremium flag")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendMediaPrivateUploadedPhoto(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
r, owner, friend := newMediaTestRouter(t)
|
||||
|
|
|
|||
|
|
@ -102,17 +102,11 @@ func (r *Router) onMessagesGetStickerSet(ctx context.Context, req *tg.MessagesGe
|
|||
zap.Int("documents", len(fallbackDocs)),
|
||||
)
|
||||
}
|
||||
if req.Hash != 0 && req.Hash == fallbackSet.Hash {
|
||||
return &tg.MessagesStickerSetNotModified{}, nil
|
||||
}
|
||||
return tgMessagesStickerSet(fallbackSet, fallbackDocs), nil
|
||||
}
|
||||
// 未 seed 的系统集 / 未知短名:回退兼容 stub,避免破坏客户端。
|
||||
return tdesktop.StickerSet(req), nil
|
||||
}
|
||||
if req.Hash != 0 && req.Hash == set.Hash {
|
||||
return &tg.MessagesStickerSetNotModified{}, nil
|
||||
}
|
||||
set, err = r.stickerSetWithViewerInstallState(ctx, set)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -191,6 +185,43 @@ func (r *Router) onMessagesGetEmojiStickers(ctx context.Context, hash int64) (tg
|
|||
return r.allStickersForKind(ctx, hash, domain.StickerSetKindEmoji)
|
||||
}
|
||||
|
||||
func (r *Router) onMessagesGetEmojiStickerGroups(ctx context.Context, hash int) (tg.MessagesEmojiGroupsClass, error) {
|
||||
empty := func() tg.MessagesEmojiGroupsClass {
|
||||
return &tg.MessagesEmojiGroups{Hash: 0, Groups: []tg.EmojiGroupClass{}}
|
||||
}
|
||||
if r.deps.Files == nil {
|
||||
return empty(), nil
|
||||
}
|
||||
sets := r.stickerCatalogSets(ctx, domain.StickerSetKindEmoji)
|
||||
visible := make([]domain.StickerSet, 0, len(sets))
|
||||
for _, set := range sets {
|
||||
if set.ID == 0 || set.Archived {
|
||||
continue
|
||||
}
|
||||
visible = append(visible, set)
|
||||
}
|
||||
if len(visible) == 0 {
|
||||
return empty(), nil
|
||||
}
|
||||
catalogHash := emojiStickerGroupsHash(visible)
|
||||
if hash != 0 && hash == catalogHash {
|
||||
return &tg.MessagesEmojiGroupsNotModified{}, nil
|
||||
}
|
||||
iconEmojiID := emojiStickerGroupIconID(visible)
|
||||
if iconEmojiID == 0 {
|
||||
return empty(), nil
|
||||
}
|
||||
return &tg.MessagesEmojiGroups{
|
||||
Hash: catalogHash,
|
||||
Groups: []tg.EmojiGroupClass{
|
||||
&tg.EmojiGroupPremium{
|
||||
Title: "Premium",
|
||||
IconEmojiID: iconEmojiID,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Router) onMessagesGetMaskStickers(ctx context.Context, hash int64) (tg.MessagesAllStickersClass, error) {
|
||||
return r.allStickersForKind(ctx, hash, domain.StickerSetKindMasks)
|
||||
}
|
||||
|
|
@ -329,6 +360,13 @@ func (r *Router) onMessagesGetFeaturedEmojiStickers(ctx context.Context, hash in
|
|||
return r.featuredStickersForKind(ctx, hash, domain.StickerSetKindEmoji)
|
||||
}
|
||||
|
||||
func (r *Router) onMessagesGetOldFeaturedStickers(ctx context.Context, req *tg.MessagesGetOldFeaturedStickersRequest) (tg.MessagesFeaturedStickersClass, error) {
|
||||
if req == nil {
|
||||
return r.onMessagesGetFeaturedStickers(ctx, 0)
|
||||
}
|
||||
return r.onMessagesGetFeaturedStickers(ctx, req.Hash)
|
||||
}
|
||||
|
||||
// featuredStickersForKind 把已 seed 的(未归档)贴纸/emoji 集作为 trending 呈现。
|
||||
// 性能:先用集目录 hash 比对,命中即返回 *NotModified——封面文档解析只在 cache-miss
|
||||
// 时发生(一次批量 GetDocuments),避免每次请求都解析封面。
|
||||
|
|
@ -467,6 +505,31 @@ func featuredStickerSetsHash(sets []domain.StickerSet) int64 {
|
|||
return int64(tdesktopCountHash(values))
|
||||
}
|
||||
|
||||
func emojiStickerGroupsHash(sets []domain.StickerSet) int {
|
||||
values := make([]int64, 0, len(sets)*2)
|
||||
for _, set := range sets {
|
||||
if set.ID == 0 || set.Archived {
|
||||
continue
|
||||
}
|
||||
values = append(values, set.ID, int64(set.Hash))
|
||||
}
|
||||
return int(tdesktopCountHash(values) & 0x7fffffff)
|
||||
}
|
||||
|
||||
func emojiStickerGroupIconID(sets []domain.StickerSet) int64 {
|
||||
for _, set := range sets {
|
||||
if set.ThumbDocumentID != 0 {
|
||||
return set.ThumbDocumentID
|
||||
}
|
||||
for _, id := range set.DocumentIDs {
|
||||
if id != 0 {
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func boolHashValue(v bool) int64 {
|
||||
if v {
|
||||
return 1
|
||||
|
|
|
|||
|
|
@ -218,6 +218,45 @@ func TestMessagesGetStickerSetAndroidPlaceholderUsesSeededSet(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMessagesGetStickerSetReturnsFullOnMatchingHash(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
files := &fakeFiles{
|
||||
docs: map[int64]domain.Document{
|
||||
201: {ID: 201, AccessHash: 21, DCID: 2},
|
||||
},
|
||||
sets: map[domain.StickerSetKind][]domain.StickerSet{
|
||||
domain.StickerSetKindStickers: {
|
||||
{
|
||||
ID: 10,
|
||||
AccessHash: 100,
|
||||
ShortName: "one",
|
||||
Title: "One",
|
||||
Kind: domain.StickerSetKindStickers,
|
||||
Count: 1,
|
||||
Hash: 123,
|
||||
DocumentIDs: []int64{201},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
r := &Router{deps: Deps{Files: files}}
|
||||
|
||||
res, err := r.onMessagesGetStickerSet(ctx, &tg.MessagesGetStickerSetRequest{
|
||||
Stickerset: &tg.InputStickerSetID{ID: 10, AccessHash: 100},
|
||||
Hash: 123,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("getStickerSet matching hash: %v", err)
|
||||
}
|
||||
full, ok := res.(*tg.MessagesStickerSet)
|
||||
if !ok {
|
||||
t.Fatalf("getStickerSet matching hash = %T, want *tg.MessagesStickerSet", res)
|
||||
}
|
||||
if full.Set.ID != 10 || len(full.Documents) != 1 {
|
||||
t.Fatalf("getStickerSet matching hash returned set %d docs %d, want set 10 with one doc", full.Set.ID, len(full.Documents))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessagesGetStickerSetAndroidPlaceholderFallsBackToEmptyWithoutSeed(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
r := &Router{deps: Deps{Files: &fakeFiles{}}}
|
||||
|
|
@ -236,6 +275,30 @@ func TestMessagesGetStickerSetAndroidPlaceholderFallsBackToEmptyWithoutSeed(t *t
|
|||
}
|
||||
}
|
||||
|
||||
func TestStickerSetRefFromSystemInputs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in tg.InputStickerSetClass
|
||||
want string
|
||||
}{
|
||||
{"emoji default statuses", &tg.InputStickerSetEmojiDefaultStatuses{}, domain.StickerSetSystemKeyEmojiDefaultStatuses},
|
||||
{"emoji channel default statuses", &tg.InputStickerSetEmojiChannelDefaultStatuses{}, domain.StickerSetSystemKeyEmojiDefaultStatuses},
|
||||
{"emoji default topic icons", &tg.InputStickerSetEmojiDefaultTopicIcons{}, domain.StickerSetSystemKeyEmojiDefaultTopicIcons},
|
||||
{"premium gifts", &tg.InputStickerSetPremiumGifts{}, domain.StickerSetSystemKeyPremiumGifts},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ref, ok := stickerSetRefFromInput(tt.in)
|
||||
if !ok {
|
||||
t.Fatalf("stickerSetRefFromInput(%T) not handled", tt.in)
|
||||
}
|
||||
if ref.Kind != domain.StickerSetRefBySystem || ref.SystemKey != tt.want {
|
||||
t.Fatalf("ref = %+v, want system key %q", ref, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessagesGetMaskStickersUsesMaskCatalog(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
files := &fakeFiles{
|
||||
|
|
@ -333,6 +396,86 @@ func TestMessagesGetFeaturedStickersSurfacesSeededSets(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMessagesGetOldFeaturedStickersUsesFeaturedCatalog(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
files := &fakeFiles{
|
||||
docs: map[int64]domain.Document{
|
||||
201: {ID: 201, AccessHash: 21, DCID: 2},
|
||||
},
|
||||
sets: map[domain.StickerSetKind][]domain.StickerSet{
|
||||
domain.StickerSetKindStickers: {
|
||||
{
|
||||
ID: 10,
|
||||
AccessHash: 100,
|
||||
ShortName: "one",
|
||||
Title: "One",
|
||||
Kind: domain.StickerSetKindStickers,
|
||||
Count: 1,
|
||||
Hash: 123,
|
||||
DocumentIDs: []int64{201},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
r := &Router{deps: Deps{Files: files}}
|
||||
res, err := r.onMessagesGetOldFeaturedStickers(ctx, &tg.MessagesGetOldFeaturedStickersRequest{Limit: 20})
|
||||
if err != nil {
|
||||
t.Fatalf("getOldFeaturedStickers: %v", err)
|
||||
}
|
||||
full, ok := res.(*tg.MessagesFeaturedStickers)
|
||||
if !ok {
|
||||
t.Fatalf("getOldFeaturedStickers = %T, want *tg.MessagesFeaturedStickers", res)
|
||||
}
|
||||
if len(full.Sets) != 1 {
|
||||
t.Fatalf("old featured sets = %d, want one", len(full.Sets))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessagesGetEmojiStickerGroupsUsesSeededEmojiCatalog(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
files := &fakeFiles{
|
||||
sets: map[domain.StickerSetKind][]domain.StickerSet{
|
||||
domain.StickerSetKindEmoji: {
|
||||
{
|
||||
ID: 20,
|
||||
AccessHash: 200,
|
||||
ShortName: "emoji",
|
||||
Title: "Emoji",
|
||||
Kind: domain.StickerSetKindEmoji,
|
||||
Count: 1,
|
||||
Hash: 999,
|
||||
Emojis: true,
|
||||
ThumbDocumentID: 555,
|
||||
DocumentIDs: []int64{201},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
r := &Router{deps: Deps{Files: files}}
|
||||
first, err := r.onMessagesGetEmojiStickerGroups(ctx, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("getEmojiStickerGroups: %v", err)
|
||||
}
|
||||
full, ok := first.(*tg.MessagesEmojiGroups)
|
||||
if !ok {
|
||||
t.Fatalf("getEmojiStickerGroups = %T, want *tg.MessagesEmojiGroups", first)
|
||||
}
|
||||
if len(full.Groups) != 1 {
|
||||
t.Fatalf("emoji sticker groups = %d, want one", len(full.Groups))
|
||||
}
|
||||
group, ok := full.Groups[0].(*tg.EmojiGroupPremium)
|
||||
if !ok || group.IconEmojiID != 555 {
|
||||
t.Fatalf("emoji sticker group = %T %+v, want premium icon 555", full.Groups[0], full.Groups[0])
|
||||
}
|
||||
second, err := r.onMessagesGetEmojiStickerGroups(ctx, full.Hash)
|
||||
if err != nil {
|
||||
t.Fatalf("getEmojiStickerGroups cached: %v", err)
|
||||
}
|
||||
if _, ok := second.(*tg.MessagesEmojiGroupsNotModified); !ok {
|
||||
t.Fatalf("cached getEmojiStickerGroups = %T, want notModified", second)
|
||||
}
|
||||
}
|
||||
|
||||
// countingStickerFiles 包 *fakeFiles 计数 ListStickerSets,验证目录缓存短路。
|
||||
type countingStickerFiles struct {
|
||||
*fakeFiles
|
||||
|
|
@ -447,6 +590,59 @@ func TestTGDocumentCompactsCachedThumbToDownloadableSize(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestTGDocumentDropsSeedSyntheticTGSPreviewThumb(t *testing.T) {
|
||||
doc := tgDocument(domain.Document{
|
||||
ID: 100,
|
||||
AccessHash: 1,
|
||||
DCID: 2,
|
||||
MimeType: "application/x-tgsticker",
|
||||
Thumbs: []domain.PhotoSize{
|
||||
{Kind: domain.PhotoSizeKindCached, Type: "m", W: 1, H: 1, Bytes: []byte("png")},
|
||||
},
|
||||
})
|
||||
full, ok := doc.(*tg.Document)
|
||||
if !ok {
|
||||
t.Fatalf("tgDocument = %T, want *tg.Document", doc)
|
||||
}
|
||||
if len(full.Thumbs) != 0 {
|
||||
t.Fatalf("thumbs = %#v, want no synthetic TGS preview thumb", full.Thumbs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTGDocumentAddsAnimatedAttributeForTGSSticker(t *testing.T) {
|
||||
doc := tgDocument(domain.Document{
|
||||
ID: 100,
|
||||
AccessHash: 1,
|
||||
DCID: 2,
|
||||
MimeType: "application/x-tgsticker",
|
||||
Attributes: []domain.DocumentAttribute{
|
||||
{Kind: domain.DocAttrImageSize, W: 512, H: 512},
|
||||
{Kind: domain.DocAttrSticker, Alt: "🙂", StickerSetID: 10, StickerSetAccessHash: 20},
|
||||
{Kind: domain.DocAttrFilename, FileName: "AnimatedSticker.tgs"},
|
||||
},
|
||||
})
|
||||
full, ok := doc.(*tg.Document)
|
||||
if !ok {
|
||||
t.Fatalf("tgDocument = %T, want *tg.Document", doc)
|
||||
}
|
||||
hasSticker := false
|
||||
hasAnimated := false
|
||||
for _, attr := range full.Attributes {
|
||||
switch attr.(type) {
|
||||
case *tg.DocumentAttributeSticker:
|
||||
hasSticker = true
|
||||
case *tg.DocumentAttributeAnimated:
|
||||
hasAnimated = true
|
||||
}
|
||||
}
|
||||
if !hasSticker {
|
||||
t.Fatal("TGS sticker document missing sticker attribute")
|
||||
}
|
||||
if !hasAnimated {
|
||||
t.Fatal("TGS sticker document missing synthesized animated attribute")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTGDocumentUsesDomainDocumentID(t *testing.T) {
|
||||
const documentID int64 = 1382305375846410902
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue