fix(rich-message): sync preserve referenced media for web clients
This commit is contained in:
parent
5156b17c1b
commit
3926db7762
3 changed files with 410 additions and 29 deletions
|
|
@ -50,6 +50,192 @@ func decodeRichBlocks(data []byte) ([]tg.PageBlockClass, error) {
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// richMessageMediaRefs is the media closure referenced by one PageBlock graph.
|
||||||
|
// IDs retain first-reference order so every projection is deterministic.
|
||||||
|
type richMessageMediaRefs struct {
|
||||||
|
photoIDs []int64
|
||||||
|
documentIDs []int64
|
||||||
|
photos map[int64]struct{}
|
||||||
|
documents map[int64]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectRichMessageMediaRefs(blocks []tg.PageBlockClass) (richMessageMediaRefs, error) {
|
||||||
|
refs := richMessageMediaRefs{
|
||||||
|
photos: make(map[int64]struct{}),
|
||||||
|
documents: make(map[int64]struct{}),
|
||||||
|
}
|
||||||
|
if err := refs.collectBlocks(blocks); err != nil {
|
||||||
|
return richMessageMediaRefs{}, err
|
||||||
|
}
|
||||||
|
return refs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *richMessageMediaRefs) addPhoto(id int64, required bool) error {
|
||||||
|
if id == 0 {
|
||||||
|
if required {
|
||||||
|
return photoInvalidErr()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, ok := r.photos[id]; ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r.photos[id] = struct{}{}
|
||||||
|
r.photoIDs = append(r.photoIDs, id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *richMessageMediaRefs) addDocument(id int64) error {
|
||||||
|
if id == 0 {
|
||||||
|
return mediaInvalidErr()
|
||||||
|
}
|
||||||
|
if _, ok := r.documents[id]; ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r.documents[id] = struct{}{}
|
||||||
|
r.documentIDs = append(r.documentIDs, id)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *richMessageMediaRefs) collectBlocks(blocks []tg.PageBlockClass) error {
|
||||||
|
for _, block := range blocks {
|
||||||
|
switch value := block.(type) {
|
||||||
|
case *tg.PageBlockPhoto:
|
||||||
|
if err := r.addPhoto(value.PhotoID, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case *tg.PageBlockVideo:
|
||||||
|
if err := r.addDocument(value.VideoID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case *tg.PageBlockAudio:
|
||||||
|
if err := r.addDocument(value.AudioID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case *tg.PageBlockEmbed:
|
||||||
|
if id, ok := value.GetPosterPhotoID(); ok {
|
||||||
|
if err := r.addPhoto(id, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case *tg.PageBlockEmbedPost:
|
||||||
|
if err := r.addPhoto(value.AuthorPhotoID, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := r.collectBlocks(value.Blocks); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case *tg.PageBlockRelatedArticles:
|
||||||
|
for i := range value.Articles {
|
||||||
|
if id, ok := value.Articles[i].GetPhotoID(); ok {
|
||||||
|
if err := r.addPhoto(id, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case *tg.PageBlockList:
|
||||||
|
for _, item := range value.Items {
|
||||||
|
if item, ok := item.(*tg.PageListItemBlocks); ok {
|
||||||
|
if err := r.collectBlocks(item.Blocks); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case *tg.PageBlockOrderedList:
|
||||||
|
for _, item := range value.Items {
|
||||||
|
if item, ok := item.(*tg.PageListOrderedItemBlocks); ok {
|
||||||
|
if err := r.collectBlocks(item.Blocks); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case *tg.PageBlockCover:
|
||||||
|
if err := r.collectBlocks([]tg.PageBlockClass{value.Cover}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case *tg.PageBlockCollage:
|
||||||
|
if err := r.collectBlocks(value.Items); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case *tg.PageBlockSlideshow:
|
||||||
|
if err := r.collectBlocks(value.Items); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case *tg.PageBlockDetails:
|
||||||
|
if err := r.collectBlocks(value.Blocks); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case *tg.PageBlockBlockquoteBlocks:
|
||||||
|
if err := r.collectBlocks(value.Blocks); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type richMessagePhotoBatchProvider interface {
|
||||||
|
GetPhotos(context.Context, []int64) ([]domain.Photo, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) resolveRichMessagePhotos(ctx context.Context, ids []int64) ([]domain.Photo, error) {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
resolved := make(map[int64]domain.Photo, len(ids))
|
||||||
|
if batch, ok := r.deps.Files.(richMessagePhotoBatchProvider); ok {
|
||||||
|
photos, err := batch.GetPhotos(ctx, ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, internalErr()
|
||||||
|
}
|
||||||
|
for _, photo := range photos {
|
||||||
|
resolved[photo.ID] = photo
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, id := range ids {
|
||||||
|
photo, found, err := r.deps.Files.GetPhoto(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, internalErr()
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
resolved[id] = photo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out := make([]domain.Photo, 0, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
photo, ok := resolved[id]
|
||||||
|
if !ok || photo.ID != id || len(photo.Sizes) == 0 {
|
||||||
|
return nil, photoInvalidErr()
|
||||||
|
}
|
||||||
|
out = append(out, photo)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Router) resolveRichMessageDocuments(ctx context.Context, ids []int64) ([]domain.Document, error) {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
documents, err := r.deps.Files.GetDocuments(ctx, ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, internalErr()
|
||||||
|
}
|
||||||
|
resolved := make(map[int64]domain.Document, len(documents))
|
||||||
|
for _, document := range documents {
|
||||||
|
resolved[document.ID] = document
|
||||||
|
}
|
||||||
|
out := make([]domain.Document, 0, len(ids))
|
||||||
|
for _, id := range ids {
|
||||||
|
document, ok := resolved[id]
|
||||||
|
if !ok || document.ID != id {
|
||||||
|
return nil, mediaInvalidErr()
|
||||||
|
}
|
||||||
|
out = append(out, document)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func normalizeRichBlocksForClients(blocks []tg.PageBlockClass) {
|
func normalizeRichBlocksForClients(blocks []tg.PageBlockClass) {
|
||||||
for _, block := range blocks {
|
for _, block := range blocks {
|
||||||
normalizeRichBlockForClients(block)
|
normalizeRichBlockForClients(block)
|
||||||
|
|
@ -172,7 +358,21 @@ func (r *Router) domainRichMessageFromInput(ctx context.Context, input tg.InputR
|
||||||
if err := validateRichMessageBlocks(in.Blocks); err != nil {
|
if err := validateRichMessageBlocks(in.Blocks); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if (len(in.Photos) > 0 || len(in.Documents) > 0) && r.deps.Files == nil {
|
for _, photo := range in.Photos {
|
||||||
|
if _, ok := inputPhotoID(photo); !ok {
|
||||||
|
return nil, photoInvalidErr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, document := range in.Documents {
|
||||||
|
if _, ok := inputDocumentID(document); !ok {
|
||||||
|
return nil, mediaInvalidErr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refs, err := collectRichMessageMediaRefs(in.Blocks)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if (len(refs.photoIDs) > 0 || len(refs.documentIDs) > 0) && r.deps.Files == nil {
|
||||||
return nil, notImplementedErr()
|
return nil, notImplementedErr()
|
||||||
}
|
}
|
||||||
normalizeRichBlocksForClients(in.Blocks)
|
normalizeRichBlocksForClients(in.Blocks)
|
||||||
|
|
@ -191,33 +391,13 @@ func (r *Router) domainRichMessageFromInput(ctx context.Context, input tg.InputR
|
||||||
if projectionErr == nil {
|
if projectionErr == nil {
|
||||||
rich.BotAPIProjection = projection
|
rich.BotAPIProjection = projection
|
||||||
}
|
}
|
||||||
for _, p := range in.Photos {
|
rich.Photos, err = r.resolveRichMessagePhotos(ctx, refs.photoIDs)
|
||||||
id, ok := inputPhotoID(p)
|
if err != nil {
|
||||||
if !ok {
|
return nil, err
|
||||||
return nil, photoInvalidErr()
|
|
||||||
}
|
|
||||||
photo, found, err := r.deps.Files.GetPhoto(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, internalErr()
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return nil, photoInvalidErr()
|
|
||||||
}
|
|
||||||
rich.Photos = append(rich.Photos, photo)
|
|
||||||
}
|
}
|
||||||
for _, d := range in.Documents {
|
rich.Documents, err = r.resolveRichMessageDocuments(ctx, refs.documentIDs)
|
||||||
id, ok := inputDocumentID(d)
|
if err != nil {
|
||||||
if !ok {
|
return nil, err
|
||||||
return nil, mediaInvalidErr()
|
|
||||||
}
|
|
||||||
doc, found, err := r.deps.Files.GetDocument(ctx, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, internalErr()
|
|
||||||
}
|
|
||||||
if !found {
|
|
||||||
return nil, mediaInvalidErr()
|
|
||||||
}
|
|
||||||
rich.Documents = append(rich.Documents, doc)
|
|
||||||
}
|
}
|
||||||
if rich.IsZero() {
|
if rich.IsZero() {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/iamxvbaba/td/bin"
|
||||||
"github.com/iamxvbaba/td/clock"
|
"github.com/iamxvbaba/td/clock"
|
||||||
"github.com/iamxvbaba/td/tg"
|
"github.com/iamxvbaba/td/tg"
|
||||||
"github.com/iamxvbaba/td/tgerr"
|
"github.com/iamxvbaba/td/tgerr"
|
||||||
|
"github.com/iamxvbaba/td/tlprofile"
|
||||||
"go.uber.org/zap/zaptest"
|
"go.uber.org/zap/zaptest"
|
||||||
|
|
||||||
appchannels "telesrv/internal/app/channels"
|
appchannels "telesrv/internal/app/channels"
|
||||||
|
|
@ -496,7 +498,17 @@ func TestRichMessageBlockFormatsEncodeDecode(t *testing.T) {
|
||||||
&tg.PageBlockUnsupported{},
|
&tg.PageBlockUnsupported{},
|
||||||
}
|
}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
r := &Router{}
|
r, _, _ := newMediaTestRouter(t)
|
||||||
|
files := r.deps.Files.(*fakeFiles)
|
||||||
|
for _, id := range []int64{1, 3, 4} {
|
||||||
|
files.photos[id] = domain.Photo{
|
||||||
|
ID: id, AccessHash: id + 100, DCID: 2,
|
||||||
|
Sizes: []domain.PhotoSize{{Kind: domain.PhotoSizeKindDefault, Type: "x", W: 32, H: 32, Size: 64}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, id := range []int64{2, 5, 6} {
|
||||||
|
files.docs[id] = domain.Document{ID: id, AccessHash: id + 100, DCID: 2, MimeType: "application/octet-stream", Size: 64}
|
||||||
|
}
|
||||||
rich, err := r.domainRichMessageFromInput(ctx, &tg.InputRichMessage{Blocks: blocks})
|
rich, err := r.domainRichMessageFromInput(ctx, &tg.InputRichMessage{Blocks: blocks})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("domain rich message: %v", err)
|
t.Fatalf("domain rich message: %v", err)
|
||||||
|
|
@ -789,7 +801,10 @@ func TestSendMessageRichMessageEmbeddedPhoto(t *testing.T) {
|
||||||
Message: "",
|
Message: "",
|
||||||
RandomID: 7003,
|
RandomID: 7003,
|
||||||
RichMessage: &tg.InputRichMessage{
|
RichMessage: &tg.InputRichMessage{
|
||||||
Blocks: []tg.PageBlockClass{&tg.PageBlockParagraph{Text: &tg.TextPlain{Text: "see photo"}}},
|
Blocks: []tg.PageBlockClass{
|
||||||
|
&tg.PageBlockParagraph{Text: &tg.TextPlain{Text: "see photo"}},
|
||||||
|
&tg.PageBlockPhoto{PhotoID: 889, Caption: richEmptyCaption()},
|
||||||
|
},
|
||||||
Photos: []tg.InputPhotoClass{&tg.InputPhoto{ID: 889, AccessHash: 42}},
|
Photos: []tg.InputPhotoClass{&tg.InputPhoto{ID: 889, AccessHash: 42}},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
@ -811,6 +826,179 @@ func TestSendMessageRichMessageEmbeddedPhoto(t *testing.T) {
|
||||||
if photo.ID != 889 {
|
if photo.ID != 889 {
|
||||||
t.Errorf("rich photo id = %d, want 889", photo.ID)
|
t.Errorf("rich photo id = %d, want 889", photo.ID)
|
||||||
}
|
}
|
||||||
|
block, ok := rich.Blocks[1].(*tg.PageBlockPhoto)
|
||||||
|
if !ok || block.PhotoID != photo.ID {
|
||||||
|
t.Fatalf("rich photo block = %#v, photo id = %d", rich.Blocks[1], photo.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRichMessageMediaClosureResolvesBlockReferences verifies the server builds the
|
||||||
|
// output resource tables from the PageBlock graph. The input resource vector is
|
||||||
|
// optional on the wire; its absence must not leave a dangling block that Web cannot
|
||||||
|
// render when the referenced upload already exists.
|
||||||
|
func TestRichMessageMediaClosureResolvesBlockReferences(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
r, _, _ := newMediaTestRouter(t)
|
||||||
|
files := r.deps.Files.(*fakeFiles)
|
||||||
|
for _, id := range []int64{889, 890, 891} {
|
||||||
|
files.photos[id] = domain.Photo{
|
||||||
|
ID: id, AccessHash: id + 100, DCID: 2,
|
||||||
|
Sizes: []domain.PhotoSize{{Kind: domain.PhotoSizeKindDefault, Type: "x", W: 800, H: 600, Size: 123}},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files.docs[990] = domain.Document{ID: 990, AccessHash: 1090, DCID: 2, MimeType: "video/mp4", Size: 456}
|
||||||
|
files.docs[991] = domain.Document{ID: 991, AccessHash: 1091, DCID: 2, MimeType: "audio/mpeg", Size: 789}
|
||||||
|
|
||||||
|
embed := &tg.PageBlockEmbed{Caption: richEmptyCaption()}
|
||||||
|
embed.SetPosterPhotoID(890)
|
||||||
|
article := tg.PageRelatedArticle{URL: "https://example.test/article", WebpageID: 1}
|
||||||
|
article.SetPhotoID(891)
|
||||||
|
blocks := []tg.PageBlockClass{
|
||||||
|
&tg.PageBlockParagraph{Text: &tg.TextPlain{Text: "media closure"}},
|
||||||
|
&tg.PageBlockCollage{
|
||||||
|
Items: []tg.PageBlockClass{
|
||||||
|
&tg.PageBlockPhoto{PhotoID: 889, Caption: richEmptyCaption()},
|
||||||
|
&tg.PageBlockVideo{VideoID: 990, Caption: richEmptyCaption()},
|
||||||
|
},
|
||||||
|
Caption: richEmptyCaption(),
|
||||||
|
},
|
||||||
|
&tg.PageBlockDetails{
|
||||||
|
Title: &tg.TextPlain{Text: "nested"},
|
||||||
|
Blocks: []tg.PageBlockClass{
|
||||||
|
&tg.PageBlockAudio{AudioID: 991, Caption: richEmptyCaption()},
|
||||||
|
embed,
|
||||||
|
&tg.PageBlockRelatedArticles{
|
||||||
|
Title: &tg.TextPlain{Text: "related"},
|
||||||
|
Articles: []tg.PageRelatedArticle{article},
|
||||||
|
},
|
||||||
|
&tg.PageBlockEmbedPost{
|
||||||
|
URL: "https://example.test/post",
|
||||||
|
WebpageID: 2,
|
||||||
|
AuthorPhotoID: 890,
|
||||||
|
Author: "author",
|
||||||
|
Blocks: []tg.PageBlockClass{
|
||||||
|
&tg.PageBlockPhoto{PhotoID: 889, Caption: richEmptyCaption()},
|
||||||
|
},
|
||||||
|
Caption: richEmptyCaption(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
rich, err := r.domainRichMessageFromInput(ctx, &tg.InputRichMessage{Blocks: blocks})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolve block media closure: %v", err)
|
||||||
|
}
|
||||||
|
if got := []int64{rich.Photos[0].ID, rich.Photos[1].ID, rich.Photos[2].ID}; !slicesEqual(got, []int64{889, 890, 891}) {
|
||||||
|
t.Fatalf("photo closure = %v, want [889 890 891]", got)
|
||||||
|
}
|
||||||
|
if got := []int64{rich.Documents[0].ID, rich.Documents[1].ID}; !slicesEqual(got, []int64{990, 991}) {
|
||||||
|
t.Fatalf("document closure = %v, want [990 991]", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRichMessageMediaClosureRejectsUnknownReference(t *testing.T) {
|
||||||
|
r, _, _ := newMediaTestRouter(t)
|
||||||
|
_, err := r.domainRichMessageFromInput(context.Background(), &tg.InputRichMessage{
|
||||||
|
Blocks: []tg.PageBlockClass{
|
||||||
|
&tg.PageBlockParagraph{Text: &tg.TextPlain{Text: "missing photo"}},
|
||||||
|
&tg.PageBlockPhoto{PhotoID: 999, Caption: richEmptyCaption()},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "PHOTO_INVALID") {
|
||||||
|
t.Fatalf("unknown rich photo error = %v, want PHOTO_INVALID", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChannelRichPhotoHistoryExactLayerRoundTrip(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
r, owner, channel := newRichChannelTestRouter(t)
|
||||||
|
files := r.deps.Files.(*fakeFiles)
|
||||||
|
files.photos[889] = domain.Photo{
|
||||||
|
ID: 889, AccessHash: 42, FileReference: []byte{1, 2, 3}, Date: 1700000000, DCID: 2,
|
||||||
|
Sizes: []domain.PhotoSize{{Kind: domain.PhotoSizeKindDefault, Type: "x", W: 800, H: 600, Size: 123}},
|
||||||
|
}
|
||||||
|
peer := &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
|
||||||
|
updates, err := r.onMessagesSendMessage(WithUserID(ctx, owner.ID), &tg.MessagesSendMessageRequest{
|
||||||
|
Peer: peer,
|
||||||
|
RandomID: 7202,
|
||||||
|
RichMessage: &tg.InputRichMessage{
|
||||||
|
Blocks: []tg.PageBlockClass{
|
||||||
|
&tg.PageBlockParagraph{Text: &tg.TextPlain{Text: "Android full-screen photo"}},
|
||||||
|
&tg.PageBlockPhoto{PhotoID: 889, Caption: richEmptyCaption()},
|
||||||
|
},
|
||||||
|
// Deliberately omit Photos: the PageBlock graph is the authoritative
|
||||||
|
// selection and the uploaded server object completes the output closure.
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("send channel rich photo: %v", err)
|
||||||
|
}
|
||||||
|
assertRichPhotoReference(t, "channel echo", newMessageFromUpdates(t, updates), 889)
|
||||||
|
|
||||||
|
historyList, err := r.deps.Channels.GetHistory(ctx, owner.ID, domain.ChannelHistoryFilter{
|
||||||
|
ChannelID: channel.ID,
|
||||||
|
Limit: 10,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("channel history: %v", err)
|
||||||
|
}
|
||||||
|
history := r.tgChannelHistoryMessages(WithUserID(ctx, owner.ID), owner.ID, historyList)
|
||||||
|
stored := singleChannelStoredMessage(t, history)
|
||||||
|
assertRichPhotoReference(t, "channel history", stored, 889)
|
||||||
|
|
||||||
|
for _, profile := range []tlprofile.Profile{tlprofile.Profile227, tlprofile.Profile228} {
|
||||||
|
var encoded bin.Buffer
|
||||||
|
if err := tlprofile.EncodeObject(profile, stored, &encoded); err != nil {
|
||||||
|
t.Fatalf("layer %d encode: %v", profile, err)
|
||||||
|
}
|
||||||
|
decoded, err := tlprofile.DecodeObject(profile, &bin.Buffer{Buf: encoded.Copy()}, tlprofile.Limits{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("layer %d decode: %v", profile, err)
|
||||||
|
}
|
||||||
|
message, ok := decoded.(*tg.Message)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("layer %d decoded %T, want *tg.Message", profile, decoded)
|
||||||
|
}
|
||||||
|
assertRichPhotoReference(t, "exact layer", message, 889)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertRichPhotoReference(t *testing.T, label string, message *tg.Message, photoID int64) {
|
||||||
|
t.Helper()
|
||||||
|
rich, ok := message.GetRichMessage()
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("%s: missing rich_message", label)
|
||||||
|
}
|
||||||
|
var referenced bool
|
||||||
|
for _, block := range rich.Blocks {
|
||||||
|
if photo, ok := block.(*tg.PageBlockPhoto); ok && photo.PhotoID == photoID {
|
||||||
|
referenced = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !referenced {
|
||||||
|
t.Fatalf("%s: missing pageBlockPhoto(%d)", label, photoID)
|
||||||
|
}
|
||||||
|
if len(rich.Photos) != 1 {
|
||||||
|
t.Fatalf("%s: photos = %d, want 1", label, len(rich.Photos))
|
||||||
|
}
|
||||||
|
photo, ok := rich.Photos[0].(*tg.Photo)
|
||||||
|
if !ok || photo.ID != photoID {
|
||||||
|
t.Fatalf("%s: photo = %#v, want id %d", label, rich.Photos[0], photoID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func slicesEqual(got, want []int64) bool {
|
||||||
|
if len(got) != len(want) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for i := range want {
|
||||||
|
if got[i] != want[i] {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// singleStoredMessage 从 messages.messages 取出唯一一条非空 *tg.Message。
|
// singleStoredMessage 从 messages.messages 取出唯一一条非空 *tg.Message。
|
||||||
|
|
|
||||||
|
|
@ -128,8 +128,21 @@ func collectRichMessageBlockMetrics(blocks []tg.PageBlockClass, depth int, metri
|
||||||
collectRichMessageBlockMetrics(value.Items, depth+1, metrics)
|
collectRichMessageBlockMetrics(value.Items, depth+1, metrics)
|
||||||
case *tg.PageBlockCover:
|
case *tg.PageBlockCover:
|
||||||
collectRichMessageBlockMetrics([]tg.PageBlockClass{value.Cover}, depth+1, metrics)
|
collectRichMessageBlockMetrics([]tg.PageBlockClass{value.Cover}, depth+1, metrics)
|
||||||
|
case *tg.PageBlockEmbed:
|
||||||
|
if id, ok := value.GetPosterPhotoID(); ok && id != 0 {
|
||||||
|
metrics.media++
|
||||||
|
}
|
||||||
case *tg.PageBlockEmbedPost:
|
case *tg.PageBlockEmbedPost:
|
||||||
|
if value.AuthorPhotoID != 0 {
|
||||||
|
metrics.media++
|
||||||
|
}
|
||||||
collectRichMessageBlockMetrics(value.Blocks, depth+1, metrics)
|
collectRichMessageBlockMetrics(value.Blocks, depth+1, metrics)
|
||||||
|
case *tg.PageBlockRelatedArticles:
|
||||||
|
for i := range value.Articles {
|
||||||
|
if id, ok := value.Articles[i].GetPhotoID(); ok && id != 0 {
|
||||||
|
metrics.media++
|
||||||
|
}
|
||||||
|
}
|
||||||
case *tg.PageBlockPhoto, *tg.PageBlockVideo, *tg.PageBlockAudio:
|
case *tg.PageBlockPhoto, *tg.PageBlockVideo, *tg.PageBlockAudio:
|
||||||
metrics.media++
|
metrics.media++
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue