package rpc import ( "context" "errors" "sort" "strings" "testing" "github.com/iamxvbaba/td/clock" "github.com/iamxvbaba/td/tg" "github.com/iamxvbaba/td/tgerr" "go.uber.org/zap/zaptest" appmessages "telesrv/internal/app/messages" apppolls "telesrv/internal/app/polls" appprivacy "telesrv/internal/app/privacy" appstories "telesrv/internal/app/stories" appusers "telesrv/internal/app/users" "telesrv/internal/domain" "telesrv/internal/store/memory" ) // fakeFiles 是 FilesService 的最小测试替身:贴纸文档可解析,上传图片返回固定 Photo。 type fakeFiles struct { docs map[int64]domain.Document photos map[int64]domain.Photo profile map[fakeProfilePhotoKey]int64 reactions []domain.AvailableReaction effects []domain.AvailableEffect sets map[domain.StickerSetKind][]domain.StickerSet profilePhotos []domain.Photo profilePhotosTotal int lastProfileOffset int lastProfileLimit int lastProfileMaxID int64 resolveWebPageFn func(string) (domain.MessageWebPage, error) lookupWebPageFn func(string) (domain.MessageWebPage, bool) webPagePreviewOn bool getDocumentsCalls int createUploadCalls int getFileRequest domain.FileDownloadRequest getFileChunk domain.FileChunk getFileFound bool getFileCalls int } type fakeProfilePhotoKey struct { ownerType domain.PeerType ownerID int64 kind domain.ProfilePhotoKind } func (f *fakeFiles) putPhoto(photo domain.Photo) domain.Photo { if f.photos == nil { f.photos = map[int64]domain.Photo{} } f.photos[photo.ID] = photo return photo } func (f *fakeFiles) SaveFilePart(context.Context, int64, int64, int, []byte) (bool, error) { return true, nil } func (f *fakeFiles) SaveBigFilePart(context.Context, int64, int64, int, int, []byte) (bool, error) { return true, nil } func (f *fakeFiles) GetFile(_ context.Context, req domain.FileDownloadRequest) (domain.FileChunk, bool, error) { f.getFileCalls++ f.getFileRequest = req return f.getFileChunk, f.getFileFound, nil } func (f *fakeFiles) CreateEncryptedFileFromUpload(context.Context, domain.UploadedFileRef, int) (domain.EncryptedFileRef, error) { return domain.EncryptedFileRef{ID: 9001, AccessHash: 9002, Size: 16, DCID: 2, KeyFingerprint: 7}, nil } func (f *fakeFiles) DeleteEncryptedFileBlob(context.Context, string) error { return nil } func (f *fakeFiles) GeoMapTile(lat, long float64, w, h, zoom, scale int) ([]byte, string) { return []byte{0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A, 1, 2, 3, 4}, "image/png" } func (f *fakeFiles) ListAvailableReactions(context.Context) ([]domain.AvailableReaction, error) { return append([]domain.AvailableReaction(nil), f.reactions...), nil } func (f *fakeFiles) AvailableEffects(context.Context) ([]domain.AvailableEffect, int, error) { hash := 0 for _, e := range f.effects { hash = hash*31 + int(e.ID&0x7fffffff) } return append([]domain.AvailableEffect(nil), f.effects...), hash & 0x7fffffff, nil } func (f *fakeFiles) GetDocuments(_ context.Context, ids []int64) ([]domain.Document, error) { f.getDocumentsCalls++ out := make([]domain.Document, 0, len(ids)) for _, id := range ids { if d, ok := f.docs[id]; ok { out = append(out, d) } } return out, nil } func (f *fakeFiles) ResolveStickerSet(_ context.Context, ref domain.StickerSetRef) (domain.StickerSet, []domain.Document, bool, error) { for _, sets := range f.sets { for _, set := range sets { if set.Deleted { continue } match := false switch ref.Kind { case domain.StickerSetRefByID: match = set.ID == ref.ID case domain.StickerSetRefByShortName: match = set.ShortName == ref.ShortName case domain.StickerSetRefBySystem: match = set.SystemKey == ref.SystemKey } if !match { continue } docs := make([]domain.Document, 0, len(set.DocumentIDs)) for _, id := range set.DocumentIDs { if doc, ok := f.docs[id]; ok { docs = append(docs, doc) } } return set, docs, true, nil } } return domain.StickerSet{}, nil, false, nil } func (f *fakeFiles) ListStickerSets(_ context.Context, kind domain.StickerSetKind) ([]domain.StickerSet, error) { sets := f.sets[kind] return append([]domain.StickerSet(nil), sets...), nil } func (f *fakeFiles) CheckStickerSetShortName(_ context.Context, shortName string) (bool, error) { if !validTestStickerShortName(shortName) { return false, domain.ErrStickerSetShortNameInvalid } for _, sets := range f.sets { for _, set := range sets { if set.ShortName != "" && strings.EqualFold(set.ShortName, shortName) && !set.Deleted { return false, nil } } } return true, nil } func validTestStickerShortName(shortName string) bool { shortName = strings.ToLower(strings.TrimSpace(shortName)) if len(shortName) < domain.MinStickerSetShortNameLen || len(shortName) > domain.MaxStickerSetShortNameLen { return false } for i := 0; i < len(shortName); i++ { ch := shortName[i] if (ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9' && i > 0) || (ch == '_' && i > 0 && i < len(shortName)-1) { continue } return false } return true } func (f *fakeFiles) SuggestStickerSetShortName(ctx context.Context, title string, userID int64) (string, error) { base := strings.ToLower(strings.TrimSpace(title)) base = strings.ReplaceAll(base, " ", "_") if base == "" { base = "stickers" } if len(base) < domain.MinStickerSetShortNameLen { base += "_pack" } if len(base) > domain.MaxStickerSetShortNameLen { base = strings.Trim(base[:domain.MaxStickerSetShortNameLen], "_") } candidates := []string{base, base + "_pack", base + "_2"} for _, c := range candidates { if ok, err := f.CheckStickerSetShortName(ctx, c); err != nil { continue } else if ok { return c, nil } } return "", domain.ErrStickerSetShortNameOccupied } func (f *fakeFiles) CreateStickerSet(_ context.Context, req domain.CreateStickerSetRequest) (domain.StickerSet, []domain.Document, error) { if f.sets == nil { f.sets = map[domain.StickerSetKind][]domain.StickerSet{} } if f.docs == nil { f.docs = map[int64]domain.Document{} } if strings.TrimSpace(req.Title) == "" { return domain.StickerSet{}, nil, domain.ErrStickerSetTitleInvalid } if len(req.Items) == 0 { return domain.StickerSet{}, nil, domain.ErrStickerSetEmpty } shortName := strings.ToLower(strings.TrimSpace(req.ShortName)) if shortName == "" { shortName = "created_pack" } if ok, err := f.CheckStickerSetShortName(context.Background(), shortName); err != nil { return domain.StickerSet{}, nil, err } else if !ok { return domain.StickerSet{}, nil, domain.ErrStickerSetShortNameOccupied } kind := req.Kind if kind == "" { kind = domain.StickerSetKindStickers } docIDs := make([]int64, 0, len(req.Items)) packs := []domain.StickerPack{} keywords := []domain.StickerKeyword{} docs := make([]domain.Document, 0, len(req.Items)) for _, item := range req.Items { doc, ok := f.docs[item.DocumentID] if !ok || doc.AccessHash != item.DocumentAccessHash || !doc.IsStickerSetMaterial() { return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid } if strings.TrimSpace(item.Emoji) == "" { return domain.StickerSet{}, nil, domain.ErrStickerSetEmojiInvalid } docIDs = append(docIDs, item.DocumentID) packs = append(packs, domain.StickerPack{Emoticon: item.Emoji, DocumentIDs: []int64{item.DocumentID}}) if item.Keywords != "" { keywords = append(keywords, domain.StickerKeyword{DocumentID: item.DocumentID, Keywords: []string{strings.TrimSpace(item.Keywords)}}) } doc.Attributes = []domain.DocumentAttribute{{Kind: domain.DocAttrSticker, Alt: item.Emoji, StickerSetID: 9000, StickerSetAccessHash: 9001}} if kind == domain.StickerSetKindEmoji { doc.Attributes[0].Kind = domain.DocAttrCustomEmoji doc.Attributes[0].TextColor = req.TextColor } f.docs[item.DocumentID] = doc docs = append(docs, doc) } set := domain.StickerSet{ ID: 9000 + int64(len(f.sets[kind])), AccessHash: 9001 + int64(len(f.sets[kind])), ShortName: shortName, Title: req.Title, Kind: kind, Emojis: kind == domain.StickerSetKindEmoji, Masks: kind == domain.StickerSetKindMasks, TextColor: kind == domain.StickerSetKindEmoji && req.TextColor, Creator: true, CreatorUserID: req.CreatorUserID, Count: len(docIDs), Hash: 77 + len(f.sets[kind]), DocumentIDs: docIDs, Packs: packs, Keywords: keywords, } f.sets[kind] = append(f.sets[kind], set) return set, docs, nil } func (f *fakeFiles) ListCreatedStickerSets(_ context.Context, userID int64, offsetID int64, limit int) ([]domain.StickerSet, int, error) { var all []domain.StickerSet for _, sets := range f.sets { for _, set := range sets { if set.CreatorUserID == userID && !set.Deleted { set.Creator = true all = append(all, set) } } } sort.Slice(all, func(i, j int) bool { return all[i].ID > all[j].ID }) total := len(all) if offsetID != 0 { filtered := all[:0] for _, set := range all { if set.ID < offsetID { filtered = append(filtered, set) } } all = filtered } if limit > 0 && len(all) > limit { all = all[:limit] } return all, total, nil } func (f *fakeFiles) AddStickerToSet(_ context.Context, actorUserID int64, ref domain.StickerSetRef, item domain.StickerSetItemInput) (domain.StickerSet, []domain.Document, error) { kind, idx, ok := f.fakeStickerSetIndex(ref) if !ok { return domain.StickerSet{}, nil, domain.ErrStickerSetInvalid } set := f.sets[kind][idx] if set.CreatorUserID != actorUserID { return domain.StickerSet{}, nil, domain.ErrStickerSetNotOwned } doc, ok := f.docs[item.DocumentID] if !ok || doc.AccessHash != item.DocumentAccessHash || !doc.IsStickerSetMaterial() { return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid } if setID, _, ok := doc.StickerSetRef(); ok && setID != 0 && setID != set.ID { return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid } if fakeContainsInt64(set.DocumentIDs, doc.ID) { return set, f.fakeStickerSetDocs(set), nil } emoji := strings.TrimSpace(item.Emoji) if emoji == "" { return domain.StickerSet{}, nil, domain.ErrStickerSetEmojiInvalid } doc = fakeAttachStickerSet(doc, set, emoji) f.docs[doc.ID] = doc set.DocumentIDs = append(set.DocumentIDs, doc.ID) set.Count = len(set.DocumentIDs) set.Packs = fakeAddStickerPackDoc(set.Packs, emoji, doc.ID) if kw := strings.TrimSpace(item.Keywords); kw != "" { set.Keywords = fakeUpsertStickerKeyword(set.Keywords, domain.StickerKeyword{DocumentID: doc.ID, Keywords: []string{kw}}) } set.Hash++ f.sets[kind][idx] = set return set, f.fakeStickerSetDocs(set), nil } func (f *fakeFiles) RemoveStickerFromSet(_ context.Context, actorUserID int64, documentID int64, accessHash int64) (domain.StickerSet, []domain.Document, error) { doc, ok := f.docs[documentID] if !ok || doc.AccessHash != accessHash || !doc.IsStickerLike() { return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid } setID, setAccessHash, ok := doc.StickerSetRef() if !ok { return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid } kind, idx, ok := f.fakeStickerSetIndex(domain.StickerSetRef{Kind: domain.StickerSetRefByID, ID: setID, AccessHash: setAccessHash}) if !ok { return domain.StickerSet{}, nil, domain.ErrStickerSetInvalid } set := f.sets[kind][idx] if set.CreatorUserID != actorUserID { return domain.StickerSet{}, nil, domain.ErrStickerSetNotOwned } pos := fakeIndexInt64(set.DocumentIDs, documentID) if pos < 0 { return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid } set.DocumentIDs = append(append([]int64(nil), set.DocumentIDs[:pos]...), set.DocumentIDs[pos+1:]...) set.Count = len(set.DocumentIDs) set.Packs = fakeRemoveStickerPackDoc(set.Packs, documentID) set.Keywords = fakeRemoveStickerKeyword(set.Keywords, documentID) set.Hash++ doc = fakeDetachStickerSet(doc) f.docs[doc.ID] = doc f.sets[kind][idx] = set return set, f.fakeStickerSetDocs(set), nil } func (f *fakeFiles) ChangeStickerPosition(_ context.Context, actorUserID int64, documentID int64, accessHash int64, position int) (domain.StickerSet, []domain.Document, error) { doc, ok := f.docs[documentID] if !ok || doc.AccessHash != accessHash || !doc.IsStickerLike() { return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid } setID, setAccessHash, ok := doc.StickerSetRef() if !ok { return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid } kind, idx, ok := f.fakeStickerSetIndex(domain.StickerSetRef{Kind: domain.StickerSetRefByID, ID: setID, AccessHash: setAccessHash}) if !ok { return domain.StickerSet{}, nil, domain.ErrStickerSetInvalid } set := f.sets[kind][idx] if set.CreatorUserID != actorUserID { return domain.StickerSet{}, nil, domain.ErrStickerSetNotOwned } from := fakeIndexInt64(set.DocumentIDs, documentID) if from < 0 { return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid } if position < 0 || position >= len(set.DocumentIDs) { return domain.StickerSet{}, nil, domain.ErrStickerSetPositionInvalid } set.DocumentIDs = fakeMoveInt64(set.DocumentIDs, from, position) set.Hash++ f.sets[kind][idx] = set return set, f.fakeStickerSetDocs(set), nil } func (f *fakeFiles) RenameStickerSet(_ context.Context, actorUserID int64, ref domain.StickerSetRef, title string) (domain.StickerSet, []domain.Document, error) { kind, idx, ok := f.fakeStickerSetIndex(ref) if !ok { return domain.StickerSet{}, nil, domain.ErrStickerSetInvalid } set := f.sets[kind][idx] if set.CreatorUserID != actorUserID { return domain.StickerSet{}, nil, domain.ErrStickerSetNotOwned } title = strings.TrimSpace(title) if title == "" { return domain.StickerSet{}, nil, domain.ErrStickerSetTitleInvalid } set.Title = title set.Hash++ f.sets[kind][idx] = set return set, f.fakeStickerSetDocs(set), nil } func (f *fakeFiles) DeleteStickerSet(_ context.Context, actorUserID int64, ref domain.StickerSetRef) (domain.StickerSetKind, error) { kind, idx, ok := f.fakeStickerSetIndex(ref) if !ok { return "", domain.ErrStickerSetInvalid } set := f.sets[kind][idx] if set.CreatorUserID != actorUserID { return "", domain.ErrStickerSetNotOwned } set.Deleted = true f.sets[kind][idx] = set return kind, nil } func (f *fakeFiles) fakeStickerSetIndex(ref domain.StickerSetRef) (domain.StickerSetKind, int, bool) { for kind, sets := range f.sets { for idx, set := range sets { if set.Deleted { continue } switch ref.Kind { case domain.StickerSetRefByID: if set.ID == ref.ID && (ref.AccessHash == 0 || set.AccessHash == ref.AccessHash) { return kind, idx, true } case domain.StickerSetRefByShortName: if strings.EqualFold(set.ShortName, ref.ShortName) { return kind, idx, true } case domain.StickerSetRefBySystem: if set.SystemKey == ref.SystemKey { return kind, idx, true } } } } return "", 0, false } func (f *fakeFiles) fakeStickerSetDocs(set domain.StickerSet) []domain.Document { out := make([]domain.Document, 0, len(set.DocumentIDs)) for _, id := range set.DocumentIDs { if doc, ok := f.docs[id]; ok { out = append(out, doc) } } return out } func fakeAttachStickerSet(doc domain.Document, set domain.StickerSet, emoji string) domain.Document { want := domain.DocAttrSticker if set.Kind == domain.StickerSetKindEmoji || set.Emojis { want = domain.DocAttrCustomEmoji } attrs := append([]domain.DocumentAttribute(nil), doc.Attributes...) replaced := false for i := range attrs { if attrs[i].Kind != domain.DocAttrSticker && attrs[i].Kind != domain.DocAttrCustomEmoji { continue } attrs[i].Kind = want attrs[i].Alt = emoji attrs[i].StickerSetID = set.ID attrs[i].StickerSetAccessHash = set.AccessHash attrs[i].TextColor = set.TextColor replaced = true break } if !replaced { attrs = append(attrs, domain.DocumentAttribute{Kind: want, Alt: emoji, StickerSetID: set.ID, StickerSetAccessHash: set.AccessHash, TextColor: set.TextColor}) } doc.Attributes = attrs return doc } func fakeDetachStickerSet(doc domain.Document) domain.Document { attrs := append([]domain.DocumentAttribute(nil), doc.Attributes...) for i := range attrs { if attrs[i].Kind == domain.DocAttrSticker || attrs[i].Kind == domain.DocAttrCustomEmoji { attrs[i].StickerSetID = 0 attrs[i].StickerSetAccessHash = 0 attrs[i].TextColor = false break } } doc.Attributes = attrs return doc } func fakeAddStickerPackDoc(packs []domain.StickerPack, emoji string, documentID int64) []domain.StickerPack { out := append([]domain.StickerPack(nil), packs...) for i := range out { out[i].DocumentIDs = append([]int64(nil), out[i].DocumentIDs...) if out[i].Emoticon == emoji { if !fakeContainsInt64(out[i].DocumentIDs, documentID) { out[i].DocumentIDs = append(out[i].DocumentIDs, documentID) } return out } } return append(out, domain.StickerPack{Emoticon: emoji, DocumentIDs: []int64{documentID}}) } func fakeRemoveStickerPackDoc(packs []domain.StickerPack, documentID int64) []domain.StickerPack { out := make([]domain.StickerPack, 0, len(packs)) for _, pack := range packs { ids := make([]int64, 0, len(pack.DocumentIDs)) for _, id := range pack.DocumentIDs { if id != documentID { ids = append(ids, id) } } if len(ids) != 0 { out = append(out, domain.StickerPack{Emoticon: pack.Emoticon, DocumentIDs: ids}) } } return out } func fakeUpsertStickerKeyword(in []domain.StickerKeyword, keyword domain.StickerKeyword) []domain.StickerKeyword { out := fakeRemoveStickerKeyword(in, keyword.DocumentID) return append(out, keyword) } func fakeRemoveStickerKeyword(in []domain.StickerKeyword, documentID int64) []domain.StickerKeyword { out := make([]domain.StickerKeyword, 0, len(in)) for _, kw := range in { if kw.DocumentID != documentID { out = append(out, kw) } } return out } func fakeContainsInt64(in []int64, value int64) bool { return fakeIndexInt64(in, value) >= 0 } func fakeIndexInt64(in []int64, value int64) int { for i, v := range in { if v == value { return i } } return -1 } func fakeMoveInt64(in []int64, from, to int) []int64 { out := append([]int64(nil), in...) value := out[from] out = append(out[:from], out[from+1:]...) if to >= len(out) { return append(out, value) } out = append(out[:to], append([]int64{value}, out[to:]...)...) return out } func (f *fakeFiles) CreatePhotoFromUpload(_ context.Context, _ domain.UploadedFileRef) (domain.Photo, error) { photo := domain.Photo{ID: 777, AccessHash: 7, DCID: 2, Sizes: []domain.PhotoSize{{Kind: domain.PhotoSizeKindDefault, Type: "x", W: 800, H: 600}}} return f.putPhoto(photo), nil } func (f *fakeFiles) CreatePhotoFromBytes(_ context.Context, data []byte) (domain.Photo, error) { photo := domain.Photo{ ID: 8300 + int64(len(f.photos)), AccessHash: 83, DCID: 2, Sizes: []domain.PhotoSize{{Kind: domain.PhotoSizeKindDefault, Type: "x", W: 320, H: 200, Size: len(data)}}, } return f.putPhoto(photo), nil } func (f *fakeFiles) CreateAvatarFromUpload(_ context.Context, _ domain.UploadedFileRef) (domain.Photo, error) { photo := domain.Photo{ID: 778, AccessHash: 7, DCID: 2, Sizes: fakeAvatarStaticSizes()} return f.putPhoto(photo), nil } func (f *fakeFiles) CreateAvatarVideoFromUpload(_ context.Context, _ domain.UploadedFileRef, videoStartTs float64) (domain.Photo, error) { photo := domain.Photo{ID: 779, AccessHash: 7, DCID: 2, Sizes: append(fakeAvatarStaticSizes(), domain.PhotoSize{Kind: domain.PhotoSizeKindVideo, Type: "u", W: 640, H: 640, Size: 1024, VideoStartTs: videoStartTs})} return f.putPhoto(photo), nil } func (f *fakeFiles) CreateAvatarVideoMarkupFromUpload(_ context.Context, _ domain.UploadedFileRef, videoStartTs float64, markup domain.PhotoSize) (domain.Photo, error) { sizes := append(fakeAvatarStaticSizes(), domain.PhotoSize{Kind: domain.PhotoSizeKindVideo, Type: "u", W: 640, H: 640, Size: 1024, VideoStartTs: videoStartTs}) sizes = append(sizes, markup) photo := domain.Photo{ID: 781, AccessHash: 7, DCID: 2, Sizes: sizes} return f.putPhoto(photo), nil } func (f *fakeFiles) CreateAvatarMarkup(_ context.Context, size domain.PhotoSize) (domain.Photo, error) { photo := domain.Photo{ID: 780, AccessHash: 7, DCID: 2, Sizes: append(fakeAvatarStaticSizes(), size)} return f.putPhoto(photo), nil } func fakeAvatarStaticSizes() []domain.PhotoSize { return []domain.PhotoSize{ {Kind: domain.PhotoSizeKindDefault, Type: "s", W: 150, H: 150, Size: 900}, {Kind: domain.PhotoSizeKindDefault, Type: "a", W: 160, H: 160, Size: 1024}, {Kind: domain.PhotoSizeKindDefault, Type: "c", W: 640, H: 640, Size: 1024}, } } func (f *fakeFiles) CreateDocumentFromUpload(_ context.Context, _ domain.UploadedFileRef, spec domain.DocumentSpec) (domain.Document, error) { f.createUploadCalls++ return domain.Document{ID: 888, AccessHash: 8, DCID: 2, MimeType: spec.MimeType, Attributes: spec.Attributes}, nil } func (f *fakeFiles) CreateDocumentFromBytes(_ context.Context, data []byte, spec domain.DocumentSpec) (domain.Document, error) { doc := domain.Document{ ID: 8400 + int64(len(f.docs)), AccessHash: 84, DCID: 2, MimeType: spec.MimeType, Size: int64(len(data)), Attributes: append([]domain.DocumentAttribute(nil), spec.Attributes...), } if f.docs == nil { f.docs = map[int64]domain.Document{} } f.docs[doc.ID] = doc return doc, nil } func (f *fakeFiles) CreatePhotoFromURL(_ context.Context, rawURL string) (domain.Photo, error) { if rawURL == "" { return domain.Photo{}, domain.ErrPhotoInvalid } return f.putPhoto(domain.Photo{ID: 9100, AccessHash: 91, DCID: 2, Sizes: []domain.PhotoSize{{Kind: domain.PhotoSizeKindDefault, Type: "x", W: 320, H: 200}}}), nil } func (f *fakeFiles) ResolveWebPage(_ context.Context, rawURL string) (domain.MessageWebPage, error) { if f.resolveWebPageFn != nil { return f.resolveWebPageFn(rawURL) } return domain.MessageWebPage{}, errors.New("web page preview unavailable") } func (f *fakeFiles) WebPagePreviewEnabled() bool { return f.webPagePreviewOn } func (f *fakeFiles) LookupWebPage(_ context.Context, rawURL string) (domain.MessageWebPage, bool) { if f.lookupWebPageFn != nil { return f.lookupWebPageFn(rawURL) } return domain.MessageWebPage{}, false } func (f *fakeFiles) CreateDocumentFromURL(_ context.Context, rawURL string) (domain.Document, error) { if rawURL == "" { return domain.Document{}, domain.ErrDocumentInvalid } doc := domain.Document{ID: 9200, AccessHash: 92, DCID: 2, MimeType: "image/jpeg", Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrFilename, FileName: "ext.jpg"}}} if f.docs == nil { f.docs = map[int64]domain.Document{} } f.docs[doc.ID] = doc return doc, nil } func (f *fakeFiles) GetPhoto(_ context.Context, id int64) (domain.Photo, bool, error) { p, ok := f.photos[id] return p, ok, nil } func (f *fakeFiles) GetDocument(_ context.Context, id int64) (domain.Document, bool, error) { d, ok := f.docs[id] return d, ok, nil } func (f *fakeFiles) UploadProfilePhoto(ctx context.Context, ownerType domain.PeerType, ownerID int64, file domain.UploadedFileRef, date int) (domain.Photo, error) { return f.UploadProfilePhotoKind(ctx, ownerType, ownerID, domain.ProfilePhotoKindProfile, file, date) } func (f *fakeFiles) UploadProfilePhotoKind(_ context.Context, ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind, _ domain.UploadedFileRef, _ int) (domain.Photo, error) { photo, _ := f.CreateAvatarFromUpload(context.Background(), domain.UploadedFileRef{}) if f.profile == nil { f.profile = map[fakeProfilePhotoKey]int64{} } f.profile[fakeProfilePhotoKey{ownerType: ownerType, ownerID: ownerID, kind: kind}] = photo.ID return photo, nil } func (f *fakeFiles) SetCurrentProfilePhoto(ctx context.Context, ownerType domain.PeerType, ownerID, photoID int64, date int) (domain.Photo, bool, error) { return f.SetCurrentProfilePhotoKind(ctx, ownerType, ownerID, domain.ProfilePhotoKindProfile, photoID, date) } func (f *fakeFiles) SetCurrentProfilePhotoKind(_ context.Context, ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind, photoID int64, _ int) (domain.Photo, bool, error) { photo, ok := f.photos[photoID] if !ok { return domain.Photo{}, false, nil } if f.profile == nil { f.profile = map[fakeProfilePhotoKey]int64{} } f.profile[fakeProfilePhotoKey{ownerType: ownerType, ownerID: ownerID, kind: kind}] = photoID return photo, true, nil } func (f *fakeFiles) CurrentProfilePhoto(ctx context.Context, ownerType domain.PeerType, ownerID int64) (domain.Photo, bool, error) { return f.CurrentProfilePhotoKind(ctx, ownerType, ownerID, domain.ProfilePhotoKindProfile) } func (f *fakeFiles) CurrentProfilePhotoKind(_ context.Context, ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind) (domain.Photo, bool, error) { photoID := f.profile[fakeProfilePhotoKey{ownerType: ownerType, ownerID: ownerID, kind: kind}] if photoID == 0 { return domain.Photo{}, false, nil } photo, ok := f.photos[photoID] return photo, ok, nil } func (f *fakeFiles) CurrentProfilePhotos(ctx context.Context, ownerType domain.PeerType, ownerIDs []int64) (map[int64]domain.ProfilePhotoRef, error) { return f.CurrentProfilePhotosKind(ctx, ownerType, ownerIDs, domain.ProfilePhotoKindProfile) } func (f *fakeFiles) CurrentProfilePhotosKind(_ context.Context, ownerType domain.PeerType, ownerIDs []int64, kind domain.ProfilePhotoKind) (map[int64]domain.ProfilePhotoRef, error) { out := make(map[int64]domain.ProfilePhotoRef, len(ownerIDs)) for _, ownerID := range ownerIDs { photoID := f.profile[fakeProfilePhotoKey{ownerType: ownerType, ownerID: ownerID, kind: kind}] if photoID == 0 { continue } photo, ok := f.photos[photoID] if !ok { continue } out[ownerID] = domain.ProfilePhotoRef{ PhotoID: photo.ID, DCID: photo.DCID, Stripped: domain.StrippedFromSizes(photo.Sizes), HasVideo: domain.PhotoHasVideo(photo.Sizes), } } return out, nil } func (f *fakeFiles) GetProfilePhotos(_ context.Context, _ domain.PeerType, _ int64, offset, limit int, maxID int64) ([]domain.Photo, int, error) { f.lastProfileOffset = offset f.lastProfileLimit = limit f.lastProfileMaxID = maxID return append([]domain.Photo(nil), f.profilePhotos...), f.profilePhotosTotal, nil } func (f *fakeFiles) GetProfilePhotosKind(_ context.Context, _ domain.PeerType, _ int64, _ domain.ProfilePhotoKind, offset, limit int, maxID int64) ([]domain.Photo, int, error) { f.lastProfileOffset = offset f.lastProfileLimit = limit f.lastProfileMaxID = maxID return append([]domain.Photo(nil), f.profilePhotos...), f.profilePhotosTotal, nil } func (f *fakeFiles) DeleteProfilePhotos(ctx context.Context, ownerType domain.PeerType, ownerID int64, photoIDs []int64) (int, error) { return f.DeleteProfilePhotosKind(ctx, ownerType, ownerID, domain.ProfilePhotoKindProfile, photoIDs) } func (f *fakeFiles) DeleteProfilePhotosKind(_ context.Context, ownerType domain.PeerType, ownerID int64, kind domain.ProfilePhotoKind, photoIDs []int64) (int, error) { deleted := 0 key := fakeProfilePhotoKey{ownerType: ownerType, ownerID: ownerID, kind: kind} for _, id := range photoIDs { if _, ok := f.photos[id]; !ok { continue } deleted++ if f.profile[key] == id { delete(f.profile, key) } } return deleted, nil } func newMediaTestRouter(t *testing.T) (*Router, domain.User, domain.User) { t.Helper() ctx := context.Background() userStore := memory.NewUserStore() owner, _ := userStore.Create(ctx, domain.User{AccessHash: 11, Phone: "15550009001", FirstName: "Owner"}) friend, _ := userStore.Create(ctx, domain.User{AccessHash: 12, Phone: "15550009002", FirstName: "Friend"}) dialogStore := memory.NewDialogStore() messageStore := memory.NewMessageStore(dialogStore) pollStore := memory.NewPollStore() messageStore.AttachPollStore(pollStore) files := &fakeFiles{ docs: map[int64]domain.Document{ 555: { ID: 555, AccessHash: 5, DCID: 2, MimeType: "application/x-tgsticker", Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker, Alt: "\U0001f600", StickerSetID: 99, StickerSetAccessHash: 7}}, }, }, photos: map[int64]domain.Photo{}, } r := New(Config{DC: 2, IP: "127.0.0.1", Port: 2398}, Deps{ Users: appusers.NewService(userStore), Messages: appmessages.NewService(messageStore, dialogStore), Files: files, Polls: apppolls.NewService(pollStore), Sessions: &captureSessions{}, }, zaptest.NewLogger(t), clock.System) return r, owner, friend } func newMessageFromUpdates(t *testing.T, updates tg.UpdatesClass) *tg.Message { t.Helper() upd, ok := updates.(*tg.Updates) if !ok { t.Fatalf("expected *tg.Updates, got %T", updates) } for _, u := range upd.Updates { if nm, ok := u.(*tg.UpdateNewMessage); ok { msg, ok := nm.Message.(*tg.Message) if !ok { t.Fatalf("expected *tg.Message, got %T", nm.Message) } return msg } if nm, ok := u.(*tg.UpdateNewChannelMessage); ok { msg, ok := nm.Message.(*tg.Message) if !ok { t.Fatalf("expected channel *tg.Message, got %T", nm.Message) } return msg } } t.Fatal("no new message update found") return nil } func assertMessageMediaStory(t *testing.T, media tg.MessageMediaClass, wantUserID int64, wantStoryID int, wantEmbedded bool) { t.Helper() storyMedia, ok := media.(*tg.MessageMediaStory) if !ok { t.Fatalf("message media = %T, want *tg.MessageMediaStory", media) } peer, ok := storyMedia.Peer.(*tg.PeerUser) if !ok || peer.UserID != wantUserID { t.Fatalf("story media peer = %T %+v, want user %d", storyMedia.Peer, storyMedia.Peer, wantUserID) } if storyMedia.ID != wantStoryID { t.Fatalf("story media id = %d, want %d", storyMedia.ID, wantStoryID) } story, hasStory := storyMedia.GetStory() if hasStory != wantEmbedded { t.Fatalf("story media embedded = %v, want %v", hasStory, wantEmbedded) } if wantEmbedded { item, ok := story.(*tg.StoryItem) if !ok || item.ID != wantStoryID { t.Fatalf("embedded story = %T %+v, want story id %d", story, story, wantStoryID) } } } func TestSendMediaPrivateSticker(t *testing.T) { ctx := context.Background() r, owner, friend := newMediaTestRouter(t) updates, err := r.onMessagesSendMedia(WithUserID(ctx, owner.ID), &tg.MessagesSendMediaRequest{ Peer: &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash}, Media: &tg.InputMediaDocument{ID: &tg.InputDocument{ID: 555, AccessHash: 5}}, RandomID: 1001, }) if err != nil { t.Fatalf("sendMedia sticker: %v", err) } msg := newMessageFromUpdates(t, updates) media, ok := msg.Media.(*tg.MessageMediaDocument) 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) } if want := int64(555); doc.ID != want { t.Errorf("document id = %d, want %d", doc.ID, want) } if doc.DCID != 2 { t.Errorf("document dc_id = %d, want 2", doc.DCID) } hasSticker := false for _, a := range doc.Attributes { if _, ok := a.(*tg.DocumentAttributeSticker); ok { hasSticker = true } } if !hasSticker { t.Error("document missing sticker attribute") } } func TestSendMediaVoicePrivacyPreflightsBeforeUploadMaterialization(t *testing.T) { ctx := context.Background() r, owner, friend := newMediaTestRouter(t) files := r.deps.Files.(*fakeFiles) privacy := appprivacy.NewService(memory.NewPrivacyStore(), memory.NewContactStore()) if _, err := privacy.SetRules(ctx, friend.ID, domain.PrivacyKeyVoiceMessages, []domain.PrivacyRule{ {Kind: domain.PrivacyRuleDisallowAll}, }); err != nil { t.Fatalf("set voice privacy: %v", err) } r.deps.Privacy = privacy _, err := r.onMessagesSendMedia(WithUserID(ctx, owner.ID), &tg.MessagesSendMediaRequest{ Peer: &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash}, Media: &tg.InputMediaUploadedDocument{ File: &tg.InputFile{ID: 7001, Parts: 1, Name: "voice.ogg"}, MimeType: "audio/ogg", Attributes: []tg.DocumentAttributeClass{&tg.DocumentAttributeAudio{Voice: true, Duration: 1}}, }, RandomID: 7001, }) if err == nil || !tgerr.Is(err, "CHAT_SEND_VOICES_FORBIDDEN") { t.Fatalf("send uploaded voice err=%v, want CHAT_SEND_VOICES_FORBIDDEN", err) } if files.createUploadCalls != 0 { t.Fatalf("uploaded voice materialized %d documents before privacy rejection", files.createUploadCalls) } if files.getDocumentsCalls != 0 { t.Fatalf("uploaded voice unexpectedly loaded documents: calls=%d", files.getDocumentsCalls) } } func TestSendMediaVoicePrivacyBatchesReferencedDocumentPreflight(t *testing.T) { ctx := context.Background() r, owner, friend := newMediaTestRouter(t) files := r.deps.Files.(*fakeFiles) files.docs[7011] = domain.Document{ ID: 7011, AccessHash: 71, Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrVideo, RoundMessage: true}}, } privacy := appprivacy.NewService(memory.NewPrivacyStore(), memory.NewContactStore()) if _, err := privacy.SetRules(ctx, friend.ID, domain.PrivacyKeyVoiceMessages, []domain.PrivacyRule{ {Kind: domain.PrivacyRuleDisallowAll}, }); err != nil { t.Fatalf("set voice privacy: %v", err) } r.deps.Privacy = privacy _, err := r.onMessagesSendMedia(WithUserID(ctx, owner.ID), &tg.MessagesSendMediaRequest{ Peer: &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash}, Media: &tg.InputMediaDocument{ID: &tg.InputDocument{ ID: 7011, AccessHash: 71, }}, RandomID: 7011, }) if err == nil || !tgerr.Is(err, "CHAT_SEND_VOICES_FORBIDDEN") { t.Fatalf("send referenced round video err=%v, want CHAT_SEND_VOICES_FORBIDDEN", err) } if files.getDocumentsCalls != 1 { t.Fatalf("referenced document preflight loads=%d, want one bounded batch", files.getDocumentsCalls) } if files.createUploadCalls != 0 { t.Fatalf("referenced document path unexpectedly materialized upload: calls=%d", files.createUploadCalls) } } func TestSendMultiMediaPartialFailureSubsetRetryKeepsReservedGroupedID(t *testing.T) { ctx := context.Background() r, owner, friend := newMediaTestRouter(t) files := r.deps.Files.(*fakeFiles) first := tg.InputSingleMedia{ Media: &tg.InputMediaDocument{ID: &tg.InputDocument{ID: 555, AccessHash: 5}}, RandomID: 41001, Message: "first", } second := tg.InputSingleMedia{ // 首次请求时 556 尚不存在,使第一条已提交后第二条解析失败。 Media: &tg.InputMediaDocument{ID: &tg.InputDocument{ID: 556, AccessHash: 6}}, RandomID: 41002, Message: "second", } peer := &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash} if _, err := r.onMessagesSendMultiMedia(WithUserID(ctx, owner.ID), &tg.MessagesSendMultiMediaRequest{ Peer: peer, MultiMedia: []tg.InputSingleMedia{first, second}, }); err == nil || !tgerr.Is(err, "MEDIA_INVALID") { t.Fatalf("partial album err=%v, want MEDIA_INVALID after first item commit", err) } files.docs[556] = domain.Document{ID: 556, AccessHash: 6, DCID: 2, MimeType: "image/jpeg"} retry, err := r.onMessagesSendMultiMedia(WithUserID(ctx, owner.ID), &tg.MessagesSendMultiMediaRequest{ Peer: peer, MultiMedia: []tg.InputSingleMedia{second}, }) if err != nil { t.Fatalf("retry failed subset: %v", err) } retryMessage := newMessageFromUpdates(t, retry) retryGroup, ok := retryMessage.GetGroupedID() if !ok || retryGroup == 0 { t.Fatalf("retry grouped_id = %d present=%v, want non-zero reservation", retryGroup, ok) } history, err := r.deps.Messages.GetHistory(ctx, owner.ID, domain.MessageFilter{ HasPeer: true, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: friend.ID}, Limit: 10, }) if err != nil { t.Fatalf("album history: %v", err) } groups := make(map[int64]int64, 2) for _, message := range history.Messages { if message.RandomID == first.RandomID || message.RandomID == second.RandomID { groups[message.RandomID] = message.GroupedID } } if len(groups) != 2 || groups[first.RandomID] != retryGroup || groups[second.RandomID] != retryGroup { t.Fatalf("history album groups=%v, want both %d", groups, retryGroup) } changed := second changed.Message = "changed durable intent" if _, err := r.onMessagesSendMultiMedia(WithUserID(ctx, owner.ID), &tg.MessagesSendMultiMediaRequest{ Peer: peer, MultiMedia: []tg.InputSingleMedia{changed}, }); err == nil || !tgerr.Is(err, "RANDOM_ID_DUPLICATE") { t.Fatalf("changed reserved item err=%v, want RANDOM_ID_DUPLICATE", err) } } func TestSendMultiMediaChannelPartialFailureSubsetRetryKeepsReservedGroupedID(t *testing.T) { f := newRPCChannelFixture(t) owner := f.user(51, "15550009401", "AlbumOwner") member := f.user(52, "15550009402", "AlbumMember") channel := f.createLegacyMegagroup(owner, "Album Group", member) messageStore := memory.NewMessageStore() f.router.deps.Messages = appmessages.NewService(messageStore, nil) files := &fakeFiles{docs: map[int64]domain.Document{ 555: {ID: 555, AccessHash: 5, DCID: 2, MimeType: "image/jpeg"}, }, photos: map[int64]domain.Photo{}} f.router.deps.Files = files first := tg.InputSingleMedia{ Media: &tg.InputMediaDocument{ID: &tg.InputDocument{ID: 555, AccessHash: 5}}, RandomID: 42001, Message: "first", } second := tg.InputSingleMedia{ Media: &tg.InputMediaDocument{ID: &tg.InputDocument{ID: 556, AccessHash: 6}}, RandomID: 42002, Message: "second", } peer := inputPeerChannel(channel) if _, err := f.router.onMessagesSendMultiMedia(f.userCtx(owner), &tg.MessagesSendMultiMediaRequest{ Peer: peer, MultiMedia: []tg.InputSingleMedia{first, second}, }); err == nil || !tgerr.Is(err, "MEDIA_INVALID") { t.Fatalf("partial channel album err=%v, want MEDIA_INVALID", err) } files.docs[556] = domain.Document{ID: 556, AccessHash: 6, DCID: 2, MimeType: "image/jpeg"} retry, err := f.router.onMessagesSendMultiMedia(f.userCtx(owner), &tg.MessagesSendMultiMediaRequest{ Peer: peer, MultiMedia: []tg.InputSingleMedia{second}, }) if err != nil { t.Fatalf("retry channel subset: %v", err) } retryMessage := newMessageFromUpdates(t, retry) retryGroup, ok := retryMessage.GetGroupedID() if !ok || retryGroup == 0 { t.Fatalf("channel retry grouped_id=%d present=%v, want non-zero", retryGroup, ok) } history, err := f.router.deps.Channels.GetHistory(f.ctx, owner.ID, domain.ChannelHistoryFilter{ ChannelID: channel.ID, Limit: 10, }) if err != nil { t.Fatalf("channel album history: %v", err) } groups := make(map[int64]int64, 2) for _, message := range history.Messages { if message.RandomID == first.RandomID || message.RandomID == second.RandomID { groups[message.RandomID] = message.GroupedID } } if len(groups) != 2 || groups[first.RandomID] != retryGroup || groups[second.RandomID] != retryGroup { t.Fatalf("channel album groups=%v, want both %d", groups, retryGroup) } } 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) updates, err := r.onMessagesSendMedia(WithUserID(ctx, owner.ID), &tg.MessagesSendMediaRequest{ Peer: &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash}, Media: &tg.InputMediaUploadedPhoto{File: &tg.InputFile{ID: 42, Parts: 1, Name: "p.jpg"}}, Message: "caption", RandomID: 1002, }) if err != nil { t.Fatalf("sendMedia photo: %v", err) } msg := newMessageFromUpdates(t, updates) if msg.Message != "caption" { t.Errorf("caption = %q, want %q", msg.Message, "caption") } media, ok := msg.Media.(*tg.MessageMediaPhoto) if !ok { t.Fatalf("expected MessageMediaPhoto, got %T", msg.Media) } photo, ok := media.Photo.(*tg.Photo) if !ok { t.Fatalf("expected tg.Photo, got %T", media.Photo) } if photo.ID != 777 { t.Errorf("photo id = %d, want 777", photo.ID) } } func TestSendMediaInputMediaStoryStoresMessageMediaStory(t *testing.T) { ctx := context.Background() r, owner, friend := newMediaTestRouter(t) storyStore := memory.NewStoryStore() r.deps.Stories = appstories.NewService(storyStore) ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID} if _, err := storyStore.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{ Owner: ownerPeer, ID: 7, Date: 1700000001, ExpireDate: 1700003600, Public: true, Caption: "story source", Media: &domain.MessageMedia{Kind: domain.MessageMediaKindPhoto, Photo: &domain.Photo{ID: 771, AccessHash: 77, DCID: 2}}, }}); err != nil { t.Fatalf("upsert story: %v", err) } updates, err := r.onMessagesSendMedia(WithUserID(ctx, owner.ID), &tg.MessagesSendMediaRequest{ Peer: &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash}, Media: &tg.InputMediaStory{Peer: &tg.InputPeerSelf{}, ID: 7}, RandomID: 10021, }) if err != nil { t.Fatalf("sendMedia story: %v", err) } msg := newMessageFromUpdates(t, updates) assertMessageMediaStory(t, msg.Media, owner.ID, 7, true) got, err := r.onMessagesGetMessages(WithUserID(ctx, owner.ID), []tg.InputMessageClass{&tg.InputMessageID{ID: msg.ID}}) if err != nil { t.Fatalf("get story message: %v", err) } box, ok := got.(*tg.MessagesMessages) if !ok || len(box.Messages) != 1 { t.Fatalf("get story message = %T %+v, want one messages.messages", got, got) } stored, ok := box.Messages[0].(*tg.Message) if !ok { t.Fatalf("stored story message = %T, want *tg.Message", box.Messages[0]) } assertMessageMediaStory(t, stored.Media, owner.ID, 7, true) } func TestSendMediaInputMediaStoryRejectsNoForwardsSource(t *testing.T) { ctx := context.Background() r, owner, friend := newMediaTestRouter(t) storyStore := memory.NewStoryStore() r.deps.Stories = appstories.NewService(storyStore) ownerPeer := domain.Peer{Type: domain.PeerTypeUser, ID: owner.ID} if _, err := storyStore.UpsertStory(ctx, domain.UpsertStoryRequest{Story: domain.Story{ Owner: ownerPeer, ID: 8, Date: 1700000002, ExpireDate: 1700003600, Public: true, NoForwards: true, }}); err != nil { t.Fatalf("upsert noforwards story: %v", err) } _, err := r.onMessagesSendMedia(WithUserID(ctx, owner.ID), &tg.MessagesSendMediaRequest{ Peer: &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash}, Media: &tg.InputMediaStory{Peer: &tg.InputPeerSelf{}, ID: 8}, RandomID: 10022, }) if err == nil || !tgerr.Is(err, "CHAT_FORWARDS_RESTRICTED") { t.Fatalf("sendMedia noforwards story err = %v, want CHAT_FORWARDS_RESTRICTED", err) } } func TestSendMediaPrivateContact(t *testing.T) { ctx := context.Background() r, owner, friend := newMediaTestRouter(t) r.deps.Files = nil updates, err := r.onMessagesSendMedia(WithUserID(ctx, owner.ID), &tg.MessagesSendMediaRequest{ Peer: &tg.InputPeerUser{UserID: friend.ID, AccessHash: friend.AccessHash}, Media: &tg.InputMediaContact{ PhoneNumber: "+1 (555) 000-9002", FirstName: "Bob", LastName: "Shared", Vcard: "BEGIN:VCARD\nFN:Bob Shared\nEND:VCARD", }, RandomID: 1003, }) if err != nil { t.Fatalf("sendMedia contact: %v", err) } upd := updates.(*tg.Updates) msg := newMessageFromUpdates(t, updates) media, ok := msg.Media.(*tg.MessageMediaContact) if !ok { t.Fatalf("expected MessageMediaContact, got %T", msg.Media) } if media.PhoneNumber != "+1 (555) 000-9002" || media.FirstName != "Bob" || media.LastName != "Shared" || media.Vcard == "" { t.Fatalf("contact media = %+v, want preserved contact payload", media) } if media.UserID != friend.ID { t.Fatalf("contact user_id = %d, want %d", media.UserID, friend.ID) } foundFriend := false for _, u := range upd.Users { if got, ok := u.(*tg.User); ok && got.ID == friend.ID { foundFriend = true } } if !foundFriend { t.Fatalf("updates users = %#v, want shared contact user", upd.Users) } } func TestUploadMediaContactUnregistered(t *testing.T) { ctx := context.Background() r, owner, _ := newMediaTestRouter(t) media, err := r.onMessagesUploadMedia(WithUserID(ctx, owner.ID), &tg.MessagesUploadMediaRequest{ Peer: &tg.InputPeerEmpty{}, Media: &tg.InputMediaContact{ PhoneNumber: "+19990000000", FirstName: "External", LastName: "Contact", }, }) if err != nil { t.Fatalf("uploadMedia contact: %v", err) } contact, ok := media.(*tg.MessageMediaContact) if !ok { t.Fatalf("expected MessageMediaContact, got %T", media) } if contact.UserID != 0 { t.Fatalf("unregistered contact user_id = %d, want 0", contact.UserID) } if contact.FirstName != "External" || contact.LastName != "Contact" { t.Fatalf("contact media = %+v, want external contact", contact) } } func TestUploadMediaReturnsReusableMedia(t *testing.T) { ctx := context.Background() r, owner, _ := newMediaTestRouter(t) media, err := r.onMessagesUploadMedia(WithUserID(ctx, owner.ID), &tg.MessagesUploadMediaRequest{ Peer: &tg.InputPeerEmpty{}, Media: &tg.InputMediaDocument{ID: &tg.InputDocument{ID: 555, AccessHash: 5}}, }) if err != nil { t.Fatalf("uploadMedia: %v", err) } if _, ok := media.(*tg.MessageMediaDocument); !ok { t.Fatalf("expected MessageMediaDocument, got %T", media) } } func TestStickerSetDoesNotExposeUnserviceableDownloadThumb(t *testing.T) { set := tgStickerSet(domain.StickerSet{ ID: 99, AccessHash: 7, Title: "Set", ShortName: "set", ThumbDCID: 2, ThumbVersion: 123, Thumbs: []domain.PhotoSize{ {Kind: domain.PhotoSizeKindPath, Type: "j", Bytes: []byte{1, 2, 3}}, {Kind: domain.PhotoSizeKindDefault, Type: "a", W: 100, H: 100, Size: 4096}, }, }) thumbs, ok := set.GetThumbs() if !ok || len(thumbs) != 1 { t.Fatalf("thumbs = %#v, want only non-downloadable path thumb", thumbs) } if _, ok := thumbs[0].(*tg.PhotoPathSize); !ok { t.Fatalf("thumb[0] = %T, want PhotoPathSize", thumbs[0]) } }