fix: sync sticker material reuse handling
This commit is contained in:
parent
bb07eb16d1
commit
20b388eeaa
5 changed files with 370 additions and 44 deletions
|
|
@ -102,10 +102,6 @@ func (s *Service) CreateStickerSet(ctx context.Context, req domain.CreateSticker
|
||||||
return domain.StickerSet{}, nil, err
|
return domain.StickerSet{}, nil, err
|
||||||
}
|
}
|
||||||
docByID := documentsByID(loaded)
|
docByID := documentsByID(loaded)
|
||||||
documentIDs := make([]int64, 0, len(req.Items))
|
|
||||||
packs := make([]domain.StickerPack, 0, len(req.Items))
|
|
||||||
packIndex := map[string]int{}
|
|
||||||
keywords := []domain.StickerKeyword{}
|
|
||||||
items := make([]domain.StickerSetItemInput, 0, len(req.Items))
|
items := make([]domain.StickerSetItemInput, 0, len(req.Items))
|
||||||
seenDocs := map[int64]struct{}{}
|
seenDocs := map[int64]struct{}{}
|
||||||
for _, item := range req.Items {
|
for _, item := range req.Items {
|
||||||
|
|
@ -122,40 +118,55 @@ func (s *Service) CreateStickerSet(ctx context.Context, req domain.CreateSticker
|
||||||
return domain.StickerSet{}, nil, err
|
return domain.StickerSet{}, nil, err
|
||||||
}
|
}
|
||||||
item.Emoji = emoji
|
item.Emoji = emoji
|
||||||
documentIDs = append(documentIDs, item.DocumentID)
|
|
||||||
if idx, ok := packIndex[emoji]; ok {
|
|
||||||
packs[idx].DocumentIDs = append(packs[idx].DocumentIDs, item.DocumentID)
|
|
||||||
} else {
|
|
||||||
packIndex[emoji] = len(packs)
|
|
||||||
packs = append(packs, domain.StickerPack{Emoticon: emoji, DocumentIDs: []int64{item.DocumentID}})
|
|
||||||
}
|
|
||||||
if kw := parseStickerKeywords(item.DocumentID, item.Keywords); len(kw.Keywords) > 0 {
|
|
||||||
keywords = append(keywords, kw)
|
|
||||||
}
|
|
||||||
items = append(items, item)
|
items = append(items, item)
|
||||||
}
|
}
|
||||||
|
if thumbID != 0 {
|
||||||
|
thumb, ok := docByID[thumbID]
|
||||||
|
if !ok || thumb.AccessHash != thumbAccess {
|
||||||
|
return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
set := domain.StickerSet{
|
set := domain.StickerSet{
|
||||||
ID: randomID(),
|
ID: randomID(),
|
||||||
AccessHash: randomID(),
|
AccessHash: randomID(),
|
||||||
ShortName: shortName,
|
ShortName: shortName,
|
||||||
Title: title,
|
Title: title,
|
||||||
Count: len(documentIDs),
|
|
||||||
Kind: kind,
|
Kind: kind,
|
||||||
Emojis: kind == domain.StickerSetKindEmoji,
|
Emojis: kind == domain.StickerSetKindEmoji,
|
||||||
Masks: kind == domain.StickerSetKindMasks,
|
Masks: kind == domain.StickerSetKindMasks,
|
||||||
TextColor: kind == domain.StickerSetKindEmoji && req.TextColor,
|
TextColor: kind == domain.StickerSetKindEmoji && req.TextColor,
|
||||||
Creator: true,
|
Creator: true,
|
||||||
CreatorUserID: req.CreatorUserID,
|
CreatorUserID: req.CreatorUserID,
|
||||||
DocumentIDs: documentIDs,
|
|
||||||
Packs: packs,
|
|
||||||
Keywords: keywords,
|
|
||||||
Software: strings.TrimSpace(req.Software),
|
Software: strings.TrimSpace(req.Software),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updatedDocs := make([]domain.Document, 0, len(items))
|
||||||
|
finalBySourceID := make(map[int64]domain.Document, len(items))
|
||||||
|
for _, item := range items {
|
||||||
|
doc := docByID[item.DocumentID]
|
||||||
|
doc, err = s.materialDocumentForStickerSet(ctx, doc, set.ID)
|
||||||
|
if err != nil {
|
||||||
|
return domain.StickerSet{}, nil, err
|
||||||
|
}
|
||||||
|
doc, err = s.prepareStickerSetDocument(ctx, doc, set, item.Emoji)
|
||||||
|
if err != nil {
|
||||||
|
return domain.StickerSet{}, nil, err
|
||||||
|
}
|
||||||
|
set.DocumentIDs = append(set.DocumentIDs, doc.ID)
|
||||||
|
set.Packs = addDocumentToStickerPacks(set.Packs, item.Emoji, doc.ID)
|
||||||
|
set.Keywords = upsertStickerKeywords(set.Keywords, parseStickerKeywords(doc.ID, item.Keywords))
|
||||||
|
finalBySourceID[item.DocumentID] = doc
|
||||||
|
updatedDocs = append(updatedDocs, doc)
|
||||||
|
}
|
||||||
|
set.Count = len(set.DocumentIDs)
|
||||||
if thumbID != 0 {
|
if thumbID != 0 {
|
||||||
thumb, ok := docByID[thumbID]
|
thumb, ok := finalBySourceID[thumbID]
|
||||||
if !ok || thumb.AccessHash != thumbAccess {
|
if !ok {
|
||||||
return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid
|
thumb, ok = docByID[thumbID]
|
||||||
|
if !ok || thumb.AccessHash != thumbAccess {
|
||||||
|
return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid
|
||||||
|
}
|
||||||
}
|
}
|
||||||
set.ThumbDocumentID = thumb.ID
|
set.ThumbDocumentID = thumb.ID
|
||||||
set.Thumbs = copyPhotoSizes(thumb.Thumbs)
|
set.Thumbs = copyPhotoSizes(thumb.Thumbs)
|
||||||
|
|
@ -165,17 +176,6 @@ func (s *Service) CreateStickerSet(ctx context.Context, req domain.CreateSticker
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
set.Hash = stickerSetHash(set)
|
set.Hash = stickerSetHash(set)
|
||||||
|
|
||||||
updatedDocs := make([]domain.Document, 0, len(items))
|
|
||||||
for _, item := range items {
|
|
||||||
doc := docByID[item.DocumentID]
|
|
||||||
doc, err = s.prepareStickerSetDocument(ctx, doc, set, item.Emoji)
|
|
||||||
if err != nil {
|
|
||||||
return domain.StickerSet{}, nil, err
|
|
||||||
}
|
|
||||||
docByID[item.DocumentID] = doc
|
|
||||||
updatedDocs = append(updatedDocs, doc)
|
|
||||||
}
|
|
||||||
if err := s.media.CreateStickerSet(ctx, set, updatedDocs); err != nil {
|
if err := s.media.CreateStickerSet(ctx, set, updatedDocs); err != nil {
|
||||||
if errors.Is(err, domain.ErrStickerSetShortNameOccupied) {
|
if errors.Is(err, domain.ErrStickerSetShortNameOccupied) {
|
||||||
return domain.StickerSet{}, nil, domain.ErrStickerSetShortNameOccupied
|
return domain.StickerSet{}, nil, domain.ErrStickerSetShortNameOccupied
|
||||||
|
|
@ -286,10 +286,7 @@ func (s *Service) prepareStickerSetDocument(ctx context.Context, doc domain.Docu
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) ensureStickerMaterialShape(ctx context.Context, doc domain.Document) (domain.Document, error) {
|
func (s *Service) ensureStickerMaterialShape(ctx context.Context, doc domain.Document) (domain.Document, error) {
|
||||||
if doc.IsStickerLike() {
|
mimeType := canonicalStickerMaterialMime(doc.StickerSetMaterialMime())
|
||||||
return doc, nil
|
|
||||||
}
|
|
||||||
mimeType := doc.StickerSetMaterialMime()
|
|
||||||
hasImageSize := false
|
hasImageSize := false
|
||||||
hasVideo := false
|
hasVideo := false
|
||||||
for _, attr := range doc.Attributes {
|
for _, attr := range doc.Attributes {
|
||||||
|
|
@ -301,7 +298,7 @@ func (s *Service) ensureStickerMaterialShape(ctx context.Context, doc domain.Doc
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch mimeType {
|
switch mimeType {
|
||||||
case "application/json":
|
case stickerMaterialMimeJSON:
|
||||||
data, ok := s.readStickerMaterialBlob(ctx, doc)
|
data, ok := s.readStickerMaterialBlob(ctx, doc)
|
||||||
if !ok {
|
if !ok {
|
||||||
return domain.Document{}, domain.ErrStickerSetFileInvalid
|
return domain.Document{}, domain.ErrStickerSetFileInvalid
|
||||||
|
|
@ -314,10 +311,10 @@ func (s *Service) ensureStickerMaterialShape(ctx context.Context, doc domain.Doc
|
||||||
if err != nil || int64(len(tgsData)) > domain.MaxStickerMaterialDocumentSize {
|
if err != nil || int64(len(tgsData)) > domain.MaxStickerMaterialDocumentSize {
|
||||||
return domain.Document{}, domain.ErrStickerSetFileInvalid
|
return domain.Document{}, domain.ErrStickerSetFileInvalid
|
||||||
}
|
}
|
||||||
if err := s.rewriteStickerMaterialBlob(ctx, doc.ID, tgsData, "application/x-tgsticker"); err != nil {
|
if err := s.rewriteStickerMaterialBlob(ctx, doc.ID, tgsData, stickerMaterialMimeTGS); err != nil {
|
||||||
return domain.Document{}, err
|
return domain.Document{}, err
|
||||||
}
|
}
|
||||||
doc.MimeType = "application/x-tgsticker"
|
doc.MimeType = stickerMaterialMimeTGS
|
||||||
doc.Size = int64(len(tgsData))
|
doc.Size = int64(len(tgsData))
|
||||||
doc.Attributes = replaceStickerMaterialFilename(doc.Attributes, "sticker.tgs")
|
doc.Attributes = replaceStickerMaterialFilename(doc.Attributes, "sticker.tgs")
|
||||||
if !hasImageSize {
|
if !hasImageSize {
|
||||||
|
|
@ -327,10 +324,15 @@ func (s *Service) ensureStickerMaterialShape(ctx context.Context, doc domain.Doc
|
||||||
H: 512,
|
H: 512,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case "application/x-tgsticker":
|
case stickerMaterialMimeTGS:
|
||||||
if data, ok := s.readStickerMaterialBlob(ctx, doc); ok && !validTGSStickerData(data) {
|
if data, ok := s.readStickerMaterialBlob(ctx, doc); ok && !validTGSStickerData(data) {
|
||||||
return domain.Document{}, domain.ErrStickerSetFileInvalid
|
return domain.Document{}, domain.ErrStickerSetFileInvalid
|
||||||
}
|
}
|
||||||
|
var err error
|
||||||
|
doc, err = s.ensureStickerMaterialMIME(ctx, doc, stickerMaterialMimeTGS)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Document{}, err
|
||||||
|
}
|
||||||
if !hasImageSize {
|
if !hasImageSize {
|
||||||
doc.Attributes = append(doc.Attributes, domain.DocumentAttribute{
|
doc.Attributes = append(doc.Attributes, domain.DocumentAttribute{
|
||||||
Kind: domain.DocAttrImageSize,
|
Kind: domain.DocAttrImageSize,
|
||||||
|
|
@ -338,7 +340,12 @@ func (s *Service) ensureStickerMaterialShape(ctx context.Context, doc domain.Doc
|
||||||
H: 512,
|
H: 512,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case "image/webp":
|
case stickerMaterialMimeWebP:
|
||||||
|
var err error
|
||||||
|
doc, err = s.ensureStickerMaterialMIME(ctx, doc, stickerMaterialMimeWebP)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Document{}, err
|
||||||
|
}
|
||||||
if !hasImageSize {
|
if !hasImageSize {
|
||||||
data, ok := s.readStickerMaterialBlob(ctx, doc)
|
data, ok := s.readStickerMaterialBlob(ctx, doc)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|
@ -354,11 +361,19 @@ func (s *Service) ensureStickerMaterialShape(ctx context.Context, doc domain.Doc
|
||||||
H: h,
|
H: h,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
case "video/webm", "video/mp4":
|
case stickerMaterialMimeWebM, stickerMaterialMimeMP4:
|
||||||
if !hasVideo {
|
if !hasVideo {
|
||||||
return domain.Document{}, domain.ErrStickerSetFileInvalid
|
return domain.Document{}, domain.ErrStickerSetFileInvalid
|
||||||
}
|
}
|
||||||
|
var err error
|
||||||
|
doc, err = s.ensureStickerMaterialMIME(ctx, doc, mimeType)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Document{}, err
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
|
if doc.IsStickerLike() {
|
||||||
|
return doc, nil
|
||||||
|
}
|
||||||
return domain.Document{}, domain.ErrStickerSetFileInvalid
|
return domain.Document{}, domain.ErrStickerSetFileInvalid
|
||||||
}
|
}
|
||||||
return doc, nil
|
return doc, nil
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,106 @@ func TestCreateStickerSetAcceptsUploadedStickerMaterial(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreateStickerSetNormalizesUploadedTGSMime(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
media := newFakeMediaStore()
|
||||||
|
media.docs[206] = domain.Document{
|
||||||
|
ID: 206,
|
||||||
|
AccessHash: 2006,
|
||||||
|
DCID: 2,
|
||||||
|
Size: 1024,
|
||||||
|
Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrFilename, FileName: "local.tgs"}},
|
||||||
|
}
|
||||||
|
media.blobs["doc:206"] = domain.FileBlob{
|
||||||
|
LocationKey: "doc:206",
|
||||||
|
Size: 1024,
|
||||||
|
}
|
||||||
|
svc := NewService(media, nil, 2)
|
||||||
|
|
||||||
|
_, docs, err := svc.CreateStickerSet(ctx, domain.CreateStickerSetRequest{
|
||||||
|
CreatorUserID: 1000000001,
|
||||||
|
Title: "Uploads",
|
||||||
|
ShortName: "tgs_uploads",
|
||||||
|
Items: []domain.StickerSetItemInput{{
|
||||||
|
DocumentID: 206,
|
||||||
|
DocumentAccessHash: 2006,
|
||||||
|
Emoji: "👋",
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create with uploaded tgs material: %v", err)
|
||||||
|
}
|
||||||
|
if len(docs) != 1 || docs[0].MimeType != stickerMaterialMimeTGS {
|
||||||
|
t.Fatalf("created docs = %+v, want normalized tgs mime", docs)
|
||||||
|
}
|
||||||
|
stored := media.docs[206]
|
||||||
|
if stored.MimeType != stickerMaterialMimeTGS {
|
||||||
|
t.Fatalf("stored document mime = %q, want %q", stored.MimeType, stickerMaterialMimeTGS)
|
||||||
|
}
|
||||||
|
blob := media.blobs["doc:206"]
|
||||||
|
if blob.MimeType != stickerMaterialMimeTGS {
|
||||||
|
t.Fatalf("stored blob mime = %q, want %q", blob.MimeType, stickerMaterialMimeTGS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateStickerSetClonesDocumentWhenReusedAcrossSets(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
media := &fakeMediaStore{
|
||||||
|
docs: map[int64]domain.Document{
|
||||||
|
301: {ID: 301, AccessHash: 3001, DCID: 2, Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}}},
|
||||||
|
},
|
||||||
|
sets: map[int64]domain.StickerSet{},
|
||||||
|
}
|
||||||
|
svc := NewService(media, nil, 2)
|
||||||
|
|
||||||
|
emojiSet, emojiDocs, err := svc.CreateStickerSet(ctx, domain.CreateStickerSetRequest{
|
||||||
|
CreatorUserID: 1000000001,
|
||||||
|
Title: "Emoji Pack",
|
||||||
|
ShortName: "emoji_pack",
|
||||||
|
Kind: domain.StickerSetKindEmoji,
|
||||||
|
Items: []domain.StickerSetItemInput{{
|
||||||
|
DocumentID: 301,
|
||||||
|
DocumentAccessHash: 3001,
|
||||||
|
Emoji: "🙂",
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create emoji set: %v", err)
|
||||||
|
}
|
||||||
|
stickerSet, stickerDocs, err := svc.CreateStickerSet(ctx, domain.CreateStickerSetRequest{
|
||||||
|
CreatorUserID: 1000000001,
|
||||||
|
Title: "Sticker Pack",
|
||||||
|
ShortName: "sticker_pack",
|
||||||
|
Items: []domain.StickerSetItemInput{{
|
||||||
|
DocumentID: 301,
|
||||||
|
DocumentAccessHash: 3001,
|
||||||
|
Emoji: "😄",
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create sticker set from existing emoji doc: %v", err)
|
||||||
|
}
|
||||||
|
if len(emojiDocs) != 1 || len(stickerDocs) != 1 {
|
||||||
|
t.Fatalf("created docs: emoji=%+v sticker=%+v, want one doc each", emojiDocs, stickerDocs)
|
||||||
|
}
|
||||||
|
if stickerDocs[0].ID == 301 || stickerSet.DocumentIDs[0] == 301 {
|
||||||
|
t.Fatalf("sticker set reused source doc id, set=%+v docs=%+v", stickerSet, stickerDocs)
|
||||||
|
}
|
||||||
|
source := media.docs[301]
|
||||||
|
if !source.IsCustomEmoji() {
|
||||||
|
t.Fatalf("source doc attrs = %+v, want custom emoji preserved", source.Attributes)
|
||||||
|
}
|
||||||
|
if id, _, ok := source.StickerSetRef(); !ok || id != emojiSet.ID {
|
||||||
|
t.Fatalf("source doc set ref = %d/%v, want emoji set %d", id, ok, emojiSet.ID)
|
||||||
|
}
|
||||||
|
if !stickerDocs[0].IsSticker() || stickerDocs[0].IsCustomEmoji() {
|
||||||
|
t.Fatalf("cloned doc attrs = %+v, want regular sticker", stickerDocs[0].Attributes)
|
||||||
|
}
|
||||||
|
if id, _, ok := stickerDocs[0].StickerSetRef(); !ok || id != stickerSet.ID {
|
||||||
|
t.Fatalf("cloned doc set ref = %d/%v, want sticker set %d", id, ok, stickerSet.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCreateStickerSetAcceptsWebPMaterialWithClientImageSize(t *testing.T) {
|
func TestCreateStickerSetAcceptsWebPMaterialWithClientImageSize(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
media := &fakeMediaStore{
|
media := &fakeMediaStore{
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,9 @@ func (s *Service) AddStickerToSet(ctx context.Context, actorUserID int64, ref do
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.StickerSet{}, nil, err
|
return domain.StickerSet{}, nil, err
|
||||||
}
|
}
|
||||||
if ownedSetID, _, ok := doc.StickerSetRef(); ok && ownedSetID != set.ID {
|
doc, err = s.materialDocumentForStickerSet(ctx, doc, set.ID)
|
||||||
return domain.StickerSet{}, nil, domain.ErrStickerSetFileInvalid
|
if err != nil {
|
||||||
|
return domain.StickerSet{}, nil, err
|
||||||
}
|
}
|
||||||
if containsInt64(set.DocumentIDs, doc.ID) {
|
if containsInt64(set.DocumentIDs, doc.ID) {
|
||||||
return set, docs, nil
|
return set, docs, nil
|
||||||
|
|
|
||||||
|
|
@ -178,3 +178,71 @@ func TestAddStickerToSetAcceptsUploadedMaterial(t *testing.T) {
|
||||||
t.Fatalf("added doc = %+v, want sticker-tagged video material", added)
|
t.Fatalf("added doc = %+v, want sticker-tagged video material", added)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAddStickerToSetClonesDocumentWhenReusedAcrossSets(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
media := &fakeMediaStore{
|
||||||
|
docs: map[int64]domain.Document{
|
||||||
|
401: {ID: 401, AccessHash: 4001, Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}}},
|
||||||
|
402: {ID: 402, AccessHash: 4002, Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}}},
|
||||||
|
},
|
||||||
|
sets: map[int64]domain.StickerSet{},
|
||||||
|
}
|
||||||
|
svc := NewService(media, nil, 2)
|
||||||
|
|
||||||
|
emojiSet, _, err := svc.CreateStickerSet(ctx, domain.CreateStickerSetRequest{
|
||||||
|
CreatorUserID: 1000000001,
|
||||||
|
Title: "Emoji Pack",
|
||||||
|
ShortName: "emoji_pack",
|
||||||
|
Kind: domain.StickerSetKindEmoji,
|
||||||
|
Items: []domain.StickerSetItemInput{{
|
||||||
|
DocumentID: 401,
|
||||||
|
DocumentAccessHash: 4001,
|
||||||
|
Emoji: "🙂",
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create emoji set: %v", err)
|
||||||
|
}
|
||||||
|
stickerSet, _, err := svc.CreateStickerSet(ctx, domain.CreateStickerSetRequest{
|
||||||
|
CreatorUserID: 1000000001,
|
||||||
|
Title: "Sticker Pack",
|
||||||
|
ShortName: "sticker_pack",
|
||||||
|
Items: []domain.StickerSetItemInput{{
|
||||||
|
DocumentID: 402,
|
||||||
|
DocumentAccessHash: 4002,
|
||||||
|
Emoji: "😄",
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create sticker set: %v", err)
|
||||||
|
}
|
||||||
|
stickerSet, docs, err := svc.AddStickerToSet(ctx, 1000000001, domain.StickerSetRef{Kind: domain.StickerSetRefByID, ID: stickerSet.ID, AccessHash: stickerSet.AccessHash}, domain.StickerSetItemInput{
|
||||||
|
DocumentID: 401,
|
||||||
|
DocumentAccessHash: 4001,
|
||||||
|
Emoji: "👋",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("add existing emoji doc to sticker set: %v", err)
|
||||||
|
}
|
||||||
|
if stickerSet.Count != 2 || len(docs) != 2 {
|
||||||
|
t.Fatalf("after add set=%+v docs=%d, want two docs", stickerSet, len(docs))
|
||||||
|
}
|
||||||
|
added := docs[1]
|
||||||
|
if added.ID == 401 || stickerSet.DocumentIDs[1] == 401 {
|
||||||
|
t.Fatalf("added doc reused source id, set=%+v docs=%+v", stickerSet, docs)
|
||||||
|
}
|
||||||
|
source := media.docs[401]
|
||||||
|
if !source.IsCustomEmoji() {
|
||||||
|
t.Fatalf("source doc attrs = %+v, want custom emoji preserved", source.Attributes)
|
||||||
|
}
|
||||||
|
if id, _, ok := source.StickerSetRef(); !ok || id != emojiSet.ID {
|
||||||
|
t.Fatalf("source doc set ref = %d/%v, want emoji set %d", id, ok, emojiSet.ID)
|
||||||
|
}
|
||||||
|
if !added.IsSticker() || added.IsCustomEmoji() {
|
||||||
|
t.Fatalf("added clone attrs = %+v, want regular sticker", added.Attributes)
|
||||||
|
}
|
||||||
|
if id, _, ok := added.StickerSetRef(); !ok || id != stickerSet.ID {
|
||||||
|
t.Fatalf("added clone set ref = %d/%v, want sticker set %d", id, ok, stickerSet.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
142
internal/app/files/sticker_material.go
Normal file
142
internal/app/files/sticker_material.go
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
package files
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"telesrv/internal/domain"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
stickerMaterialMimeTGS = "application/x-tgsticker"
|
||||||
|
stickerMaterialMimeWebP = "image/webp"
|
||||||
|
stickerMaterialMimeWebM = "video/webm"
|
||||||
|
stickerMaterialMimeMP4 = "video/mp4"
|
||||||
|
stickerMaterialMimeJSON = "application/json"
|
||||||
|
stickerMaterialMimeOctet = "application/octet-stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
func normalizeStickerMaterialDocumentMIME(doc domain.Document) (domain.Document, bool) {
|
||||||
|
mimeType := canonicalStickerMaterialMime(doc.StickerSetMaterialMime())
|
||||||
|
if mimeType == "" || mimeType == stickerMaterialMimeJSON {
|
||||||
|
return doc, false
|
||||||
|
}
|
||||||
|
if !shouldReplaceStickerMaterialMime(doc.MimeType, mimeType) {
|
||||||
|
return doc, false
|
||||||
|
}
|
||||||
|
doc.MimeType = mimeType
|
||||||
|
return doc, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) materialDocumentForStickerSet(ctx context.Context, doc domain.Document, targetSetID int64) (domain.Document, error) {
|
||||||
|
if ownedSetID, _, ok := doc.StickerSetRef(); ok && ownedSetID != 0 && ownedSetID != targetSetID {
|
||||||
|
return s.cloneStickerSetDocument(ctx, doc)
|
||||||
|
}
|
||||||
|
return doc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) cloneStickerSetDocument(ctx context.Context, source domain.Document) (domain.Document, error) {
|
||||||
|
copied := copyDocuments([]domain.Document{source})
|
||||||
|
if len(copied) == 0 {
|
||||||
|
return domain.Document{}, domain.ErrStickerSetFileInvalid
|
||||||
|
}
|
||||||
|
clone := copied[0]
|
||||||
|
oldID := clone.ID
|
||||||
|
clone.ID = randomID()
|
||||||
|
clone.AccessHash = randomID()
|
||||||
|
clone.FileReference = randomFileReference()
|
||||||
|
|
||||||
|
if source.Size > 0 {
|
||||||
|
blob, found, err := s.media.GetFileBlob(ctx, fmt.Sprintf("doc:%d", oldID))
|
||||||
|
if err != nil {
|
||||||
|
return domain.Document{}, err
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return domain.Document{}, domain.ErrStickerSetFileInvalid
|
||||||
|
}
|
||||||
|
blob.LocationKey = fmt.Sprintf("doc:%d", clone.ID)
|
||||||
|
if normalized, changed := normalizeStickerMaterialDocumentMIME(clone); changed {
|
||||||
|
clone = normalized
|
||||||
|
}
|
||||||
|
if mimeType := canonicalStickerMaterialMime(clone.StickerSetMaterialMime()); mimeType != "" && shouldReplaceStickerMaterialMime(blob.MimeType, mimeType) {
|
||||||
|
blob.MimeType = mimeType
|
||||||
|
}
|
||||||
|
if err := s.media.PutFileBlob(ctx, blob); err != nil {
|
||||||
|
return domain.Document{}, err
|
||||||
|
}
|
||||||
|
s.blobCache.put(blob.LocationKey, blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, thumb := range source.Thumbs {
|
||||||
|
if !thumb.Downloadable() || thumb.Type == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
oldKey := fmt.Sprintf("doc:%d:%s", oldID, thumb.Type)
|
||||||
|
blob, found, err := s.media.GetFileBlob(ctx, oldKey)
|
||||||
|
if err != nil {
|
||||||
|
return domain.Document{}, err
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
blob.LocationKey = fmt.Sprintf("doc:%d:%s", clone.ID, thumb.Type)
|
||||||
|
if err := s.media.PutFileBlob(ctx, blob); err != nil {
|
||||||
|
return domain.Document{}, err
|
||||||
|
}
|
||||||
|
s.blobCache.put(blob.LocationKey, blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
return clone, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ensureStickerMaterialMIME(ctx context.Context, doc domain.Document, mimeType string) (domain.Document, error) {
|
||||||
|
mimeType = canonicalStickerMaterialMime(mimeType)
|
||||||
|
if mimeType == "" || mimeType == stickerMaterialMimeJSON {
|
||||||
|
return doc, nil
|
||||||
|
}
|
||||||
|
if shouldReplaceStickerMaterialMime(doc.MimeType, mimeType) {
|
||||||
|
doc.MimeType = mimeType
|
||||||
|
}
|
||||||
|
if s == nil || s.media == nil || doc.ID == 0 {
|
||||||
|
return doc, nil
|
||||||
|
}
|
||||||
|
blob, found, err := s.media.GetFileBlob(ctx, fmt.Sprintf("doc:%d", doc.ID))
|
||||||
|
if err != nil {
|
||||||
|
return domain.Document{}, err
|
||||||
|
}
|
||||||
|
if found && shouldReplaceStickerMaterialMime(blob.MimeType, mimeType) {
|
||||||
|
blob.MimeType = mimeType
|
||||||
|
if err := s.media.PutFileBlob(ctx, blob); err != nil {
|
||||||
|
return domain.Document{}, err
|
||||||
|
}
|
||||||
|
s.blobCache.put(blob.LocationKey, blob)
|
||||||
|
}
|
||||||
|
return doc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalStickerMaterialMime(mimeType string) string {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(mimeType)) {
|
||||||
|
case stickerMaterialMimeTGS:
|
||||||
|
return stickerMaterialMimeTGS
|
||||||
|
case stickerMaterialMimeWebP:
|
||||||
|
return stickerMaterialMimeWebP
|
||||||
|
case stickerMaterialMimeWebM:
|
||||||
|
return stickerMaterialMimeWebM
|
||||||
|
case stickerMaterialMimeMP4:
|
||||||
|
return stickerMaterialMimeMP4
|
||||||
|
case stickerMaterialMimeJSON, "text/json", "application/lottie+json":
|
||||||
|
return stickerMaterialMimeJSON
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldReplaceStickerMaterialMime(current, inferred string) bool {
|
||||||
|
inferred = canonicalStickerMaterialMime(inferred)
|
||||||
|
if inferred == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
current = strings.ToLower(strings.TrimSpace(current))
|
||||||
|
return current == "" || current == stickerMaterialMimeOctet
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue