owpengram-server/internal/rpc/send_media_test.go
2026-09-03 08:33:27 +03:00

1268 lines
46 KiB
Go

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])
}
}