feat: sync GIFv and saved GIF support
This commit is contained in:
parent
c0088f1160
commit
5f7c0b9804
21 changed files with 641 additions and 50 deletions
|
|
@ -459,13 +459,18 @@ func tgDocumentAttributes(mimeType string, attrs []domain.DocumentAttribute) []t
|
|||
Stickerset: tgInputStickerSetFromIDs(a.StickerSetID, a.StickerSetAccessHash),
|
||||
})
|
||||
case domain.DocAttrVideo:
|
||||
out = append(out, &tg.DocumentAttributeVideo{
|
||||
video := &tg.DocumentAttributeVideo{
|
||||
RoundMessage: a.RoundMessage,
|
||||
SupportsStreaming: a.SupportsStreaming,
|
||||
Nosound: a.NoSound,
|
||||
Duration: a.Duration,
|
||||
W: a.W,
|
||||
H: a.H,
|
||||
})
|
||||
}
|
||||
if a.VideoCodec != "" {
|
||||
video.SetVideoCodec(a.VideoCodec)
|
||||
}
|
||||
out = append(out, video)
|
||||
case domain.DocAttrAudio:
|
||||
attr := &tg.DocumentAttributeAudio{
|
||||
Voice: a.Voice,
|
||||
|
|
|
|||
|
|
@ -100,6 +100,9 @@ func stickersetInvalidErr() error { return tgerr.New(406, "STICKERSET_INVALID")
|
|||
// stickerInvalidErr 表示输入文档不是合法贴纸/GIF(faveSticker/saveRecentSticker/saveGif)。
|
||||
func stickerInvalidErr() error { return tgerr.New(400, "STICKER_DOCUMENT_INVALID") }
|
||||
|
||||
// gifIDInvalidErr 表示 messages.saveGif 引用的文档不存在或不是规范 GIFv。
|
||||
func gifIDInvalidErr() error { return tgerr.New(400, "GIF_ID_INVALID") }
|
||||
|
||||
func mediaCaptionTooLongErr() error { return tgerr.New(400, "MEDIA_CAPTION_TOO_LONG") }
|
||||
|
||||
func replyMarkupInvalidErr() error { return tgerr.New(400, "REPLY_MARKUP_INVALID") }
|
||||
|
|
|
|||
|
|
@ -246,7 +246,11 @@ func TestMessagesGetSearchCountersUsesMediaCategoryCounts(t *testing.T) {
|
|||
send(4, "music", docMedia(4, domain.DocumentAttribute{Kind: domain.DocAttrAudio, Title: "song"}), nil)
|
||||
send(5, "voice", docMedia(5, domain.DocumentAttribute{Kind: domain.DocAttrAudio, Voice: true}), nil)
|
||||
send(6, "round", docMedia(6, domain.DocumentAttribute{Kind: domain.DocAttrVideo, RoundMessage: true}), nil)
|
||||
send(7, "gif", docMedia(7, domain.DocumentAttribute{Kind: domain.DocAttrAnimated}), nil)
|
||||
gif := docMedia(7,
|
||||
domain.DocumentAttribute{Kind: domain.DocAttrAnimated},
|
||||
domain.DocumentAttribute{Kind: domain.DocAttrVideo, W: 320, H: 240, Duration: 1})
|
||||
gif.Document.MimeType = "video/mp4"
|
||||
send(7, "gif", gif, nil)
|
||||
send(8, "poll", &domain.MessageMedia{Kind: domain.MessageMediaKindPoll}, nil)
|
||||
|
||||
filters := []tg.MessagesFilterClass{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/gotd/td/tg"
|
||||
|
||||
|
|
@ -27,6 +29,9 @@ func (r *Router) stickerCollectionSvc() (stickerCollectionService, bool) {
|
|||
func (r *Router) stickerDocumentFromInput(ctx context.Context, input tg.InputDocumentClass, requireGif bool) (domain.Document, error) {
|
||||
in, ok := input.(*tg.InputDocument)
|
||||
if !ok || in.ID == 0 || r.deps.Files == nil {
|
||||
if requireGif {
|
||||
return domain.Document{}, gifIDInvalidErr()
|
||||
}
|
||||
return domain.Document{}, stickerInvalidErr()
|
||||
}
|
||||
doc, found, err := r.deps.Files.GetDocument(ctx, in.ID)
|
||||
|
|
@ -34,6 +39,9 @@ func (r *Router) stickerDocumentFromInput(ctx context.Context, input tg.InputDoc
|
|||
return domain.Document{}, internalErr()
|
||||
}
|
||||
ok = found && doc.AccessHash == in.AccessHash
|
||||
if ok && len(in.FileReference) > 0 && !bytes.Equal(in.FileReference, doc.FileReference) {
|
||||
ok = false
|
||||
}
|
||||
if ok {
|
||||
if requireGif {
|
||||
ok = doc.IsGif()
|
||||
|
|
@ -42,6 +50,9 @@ func (r *Router) stickerDocumentFromInput(ctx context.Context, input tg.InputDoc
|
|||
}
|
||||
}
|
||||
if !ok {
|
||||
if requireGif {
|
||||
return domain.Document{}, gifIDInvalidErr()
|
||||
}
|
||||
return domain.Document{}, stickerInvalidErr()
|
||||
}
|
||||
return doc, nil
|
||||
|
|
@ -95,7 +106,7 @@ func (r *Router) onMessagesSaveRecentSticker(ctx context.Context, req *tg.Messag
|
|||
|
||||
func (r *Router) onMessagesSaveGif(ctx context.Context, req *tg.MessagesSaveGifRequest) (bool, error) {
|
||||
if req == nil {
|
||||
return false, stickerInvalidErr()
|
||||
return false, gifIDInvalidErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
|
|
@ -137,7 +148,10 @@ func (r *Router) onMessagesGetFavedStickers(ctx context.Context, hash int64) (tg
|
|||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
docs := r.stickerCollectionDocuments(ctx, userID, domain.StickerCollectionFaved, nil)
|
||||
docs, err := r.stickerCollectionDocuments(ctx, userID, domain.StickerCollectionFaved, nil)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
catalogHash := stickerDocumentsHash(docs)
|
||||
if hash != 0 && hash == catalogHash {
|
||||
return &tg.MessagesFavedStickersNotModified{}, nil
|
||||
|
|
@ -159,7 +173,10 @@ func (r *Router) onMessagesGetRecentStickers(ctx context.Context, req *tg.Messag
|
|||
kind = domain.StickerCollectionRecentAttached
|
||||
}
|
||||
var dates []int
|
||||
docs := r.stickerCollectionDocuments(ctx, userID, kind, &dates)
|
||||
docs, err := r.stickerCollectionDocuments(ctx, userID, kind, &dates)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
catalogHash := stickerDocumentsHash(docs)
|
||||
if req != nil && req.Hash != 0 && req.Hash == catalogHash {
|
||||
return &tg.MessagesRecentStickersNotModified{}, nil
|
||||
|
|
@ -177,7 +194,10 @@ func (r *Router) onMessagesGetSavedGifs(ctx context.Context, hash int64) (tg.Mes
|
|||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
docs := r.stickerCollectionDocuments(ctx, userID, domain.StickerCollectionGif, nil)
|
||||
docs, err := r.stickerCollectionDocuments(ctx, userID, domain.StickerCollectionGif, nil)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
catalogHash := stickerDocumentsHash(docs)
|
||||
if hash != 0 && hash == catalogHash {
|
||||
return &tg.MessagesSavedGifsNotModified{}, nil
|
||||
|
|
@ -186,21 +206,24 @@ func (r *Router) onMessagesGetSavedGifs(ctx context.Context, hash int64) (tg.Mes
|
|||
}
|
||||
|
||||
// stickerCollectionDocuments 取某集合并解析为完整文档(最新在前,顺序与集合一致)。
|
||||
// 解析不到的文档(已删/不可用)跳过。若 datesOut 非 nil,按相同顺序填充 used_at。
|
||||
func (r *Router) stickerCollectionDocuments(ctx context.Context, userID int64, kind domain.StickerCollectionKind, datesOut *[]int) []domain.Document {
|
||||
// 集合引用缺失或类型错误属于坏数据并 fail-fast;若 datesOut 非 nil,按相同顺序填充 used_at。
|
||||
func (r *Router) stickerCollectionDocuments(ctx context.Context, userID int64, kind domain.StickerCollectionKind, datesOut *[]int) ([]domain.Document, error) {
|
||||
svc, ok := r.stickerCollectionSvc()
|
||||
if !ok || r.deps.Files == nil {
|
||||
if datesOut != nil {
|
||||
*datesOut = []int{}
|
||||
}
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
items, err := svc.ListStickerCollection(ctx, userID, kind, domain.MaxStickerCollectionItems(kind))
|
||||
if err != nil || len(items) == 0 {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(items) == 0 {
|
||||
if datesOut != nil {
|
||||
*datesOut = []int{}
|
||||
}
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
ids := make([]int64, 0, len(items))
|
||||
dateByID := make(map[int64]int, len(items))
|
||||
|
|
@ -210,10 +233,7 @@ func (r *Router) stickerCollectionDocuments(ctx context.Context, userID int64, k
|
|||
}
|
||||
resolved, err := r.deps.Files.GetDocuments(ctx, ids)
|
||||
if err != nil {
|
||||
if datesOut != nil {
|
||||
*datesOut = []int{}
|
||||
}
|
||||
return nil
|
||||
return nil, err
|
||||
}
|
||||
byID := documentsByID(resolved)
|
||||
docs := make([]domain.Document, 0, len(items))
|
||||
|
|
@ -221,7 +241,13 @@ func (r *Router) stickerCollectionDocuments(ctx context.Context, userID int64, k
|
|||
for _, id := range ids { // 保持集合顺序(最新在前)
|
||||
doc, ok := byID[id]
|
||||
if !ok {
|
||||
continue
|
||||
return nil, fmt.Errorf("sticker collection %s references missing document %d", kind, id)
|
||||
}
|
||||
if kind == domain.StickerCollectionGif && !doc.IsGif() {
|
||||
return nil, fmt.Errorf("saved gif collection references non-GIFv document %d", id)
|
||||
}
|
||||
if kind != domain.StickerCollectionGif && !doc.IsSticker() {
|
||||
return nil, fmt.Errorf("sticker collection %s references non-sticker document %d", kind, id)
|
||||
}
|
||||
docs = append(docs, doc)
|
||||
dates = append(dates, dateByID[id])
|
||||
|
|
@ -229,7 +255,7 @@ func (r *Router) stickerCollectionDocuments(ctx context.Context, userID int64, k
|
|||
if datesOut != nil {
|
||||
*datesOut = dates
|
||||
}
|
||||
return docs
|
||||
return docs, nil
|
||||
}
|
||||
|
||||
func stickerDocumentsHash(docs []domain.Document) int64 {
|
||||
|
|
|
|||
|
|
@ -14,20 +14,28 @@ import (
|
|||
"telesrv/internal/store/memory"
|
||||
)
|
||||
|
||||
func stickerCollectionRouter(t *testing.T) *Router {
|
||||
func stickerCollectionRouter(t *testing.T) (*Router, *captureSessions) {
|
||||
t.Helper()
|
||||
files := &fakeFiles{docs: map[int64]domain.Document{
|
||||
101: {ID: 101, AccessHash: 11, Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}}},
|
||||
102: {ID: 102, AccessHash: 12, Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrSticker}}},
|
||||
201: {ID: 201, AccessHash: 21, Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrAnimated}}},
|
||||
201: {ID: 201, AccessHash: 21, MimeType: "video/mp4", Attributes: []domain.DocumentAttribute{
|
||||
{Kind: domain.DocAttrAnimated},
|
||||
{Kind: domain.DocAttrVideo, W: 320, H: 240, Duration: 1},
|
||||
}},
|
||||
202: {ID: 202, AccessHash: 22, MimeType: "video/mp4", Attributes: []domain.DocumentAttribute{
|
||||
{Kind: domain.DocAttrAnimated},
|
||||
{Kind: domain.DocAttrVideo, W: 640, H: 360, Duration: 2},
|
||||
}},
|
||||
301: {ID: 301, AccessHash: 31, Attributes: []domain.DocumentAttribute{{Kind: domain.DocAttrAudio}}},
|
||||
}}
|
||||
passwordStore := memory.NewPasswordStore()
|
||||
sessions := &captureSessions{}
|
||||
return New(Config{}, Deps{
|
||||
Account: appaccount.NewService(passwordStore, appaccount.WithStickerCollections(passwordStore)),
|
||||
Files: files,
|
||||
Sessions: &captureSessions{},
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
Sessions: sessions,
|
||||
}, zaptest.NewLogger(t), clock.System), sessions
|
||||
}
|
||||
|
||||
func inputDoc(id, accessHash int64) *tg.InputDocument {
|
||||
|
|
@ -36,7 +44,7 @@ func inputDoc(id, accessHash int64) *tg.InputDocument {
|
|||
|
||||
// TestFavedStickersRoundTrip 回归:faveSticker/getFavedStickers 此前未注册/返空。
|
||||
func TestFavedStickersRoundTrip(t *testing.T) {
|
||||
r := stickerCollectionRouter(t)
|
||||
r, _ := stickerCollectionRouter(t)
|
||||
ctx := WithUserID(context.Background(), 1000000001)
|
||||
|
||||
// 非贴纸文档拒绝。
|
||||
|
|
@ -80,7 +88,7 @@ func TestFavedStickersRoundTrip(t *testing.T) {
|
|||
|
||||
// TestRecentStickersRoundTrip 验证 saveRecentSticker/getRecentStickers + dates + clear。
|
||||
func TestRecentStickersRoundTrip(t *testing.T) {
|
||||
r := stickerCollectionRouter(t)
|
||||
r, _ := stickerCollectionRouter(t)
|
||||
ctx := WithUserID(context.Background(), 1000000001)
|
||||
|
||||
if ok, err := r.onMessagesSaveRecentSticker(ctx, &tg.MessagesSaveRecentStickerRequest{ID: inputDoc(101, 11)}); err != nil || !ok {
|
||||
|
|
@ -110,22 +118,48 @@ func TestRecentStickersRoundTrip(t *testing.T) {
|
|||
|
||||
// TestSavedGifsRoundTrip 验证 saveGif/getSavedGifs + 非 GIF 拒绝。
|
||||
func TestSavedGifsRoundTrip(t *testing.T) {
|
||||
r := stickerCollectionRouter(t)
|
||||
r, sessions := stickerCollectionRouter(t)
|
||||
ctx := WithUserID(context.Background(), 1000000001)
|
||||
|
||||
// 非 GIF(贴纸)拒绝。
|
||||
if ok, err := r.onMessagesSaveGif(ctx, &tg.MessagesSaveGifRequest{ID: inputDoc(101, 11)}); ok || !tgerr.Is(err, "STICKER_DOCUMENT_INVALID") {
|
||||
t.Fatalf("save non-gif = ok %v err %v, want STICKER_DOCUMENT_INVALID", ok, err)
|
||||
if ok, err := r.onMessagesSaveGif(ctx, &tg.MessagesSaveGifRequest{ID: inputDoc(101, 11)}); ok || !tgerr.Is(err, "GIF_ID_INVALID") {
|
||||
t.Fatalf("save non-gif = ok %v err %v, want GIF_ID_INVALID", ok, err)
|
||||
}
|
||||
if ok, err := r.onMessagesSaveGif(ctx, &tg.MessagesSaveGifRequest{ID: inputDoc(201, 21)}); err != nil || !ok {
|
||||
t.Fatalf("save gif = ok %v err %v", ok, err)
|
||||
}
|
||||
if pushed, ok := sessions.lastUserPush().(*tg.Updates); !ok || len(pushed.Updates) != 1 {
|
||||
t.Fatalf("save gif push = %T %+v, want updateSavedGifs", sessions.lastUserPush(), pushed)
|
||||
} else if _, ok := pushed.Updates[0].(*tg.UpdateSavedGifs); !ok {
|
||||
t.Fatalf("save gif update = %T, want *tg.UpdateSavedGifs", pushed.Updates[0])
|
||||
}
|
||||
if ok, err := r.onMessagesSaveGif(ctx, &tg.MessagesSaveGifRequest{ID: inputDoc(202, 22)}); err != nil || !ok {
|
||||
t.Fatalf("save second gif = ok %v err %v", ok, err)
|
||||
}
|
||||
out, err := r.onMessagesGetSavedGifs(ctx, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("get saved gifs: %v", err)
|
||||
}
|
||||
if got := out.(*tg.MessagesSavedGifs); len(got.Gifs) != 1 {
|
||||
t.Fatalf("saved gifs = %d, want 1", len(got.Gifs))
|
||||
full := out.(*tg.MessagesSavedGifs)
|
||||
if len(full.Gifs) != 2 || full.Gifs[0].(*tg.Document).ID != 202 {
|
||||
t.Fatalf("saved gifs = %+v, want newest 202 first", full.Gifs)
|
||||
}
|
||||
again, err := r.onMessagesGetSavedGifs(ctx, full.Hash)
|
||||
if err != nil {
|
||||
t.Fatalf("get saved gifs by hash: %v", err)
|
||||
}
|
||||
if _, ok := again.(*tg.MessagesSavedGifsNotModified); !ok {
|
||||
t.Fatalf("get saved gifs by hash = %T, want NotModified", again)
|
||||
}
|
||||
if ok, err := r.onMessagesSaveGif(ctx, &tg.MessagesSaveGifRequest{ID: inputDoc(201, 21), Unsave: true}); err != nil || !ok {
|
||||
t.Fatalf("unsave gif = ok %v err %v", ok, err)
|
||||
}
|
||||
out, err = r.onMessagesGetSavedGifs(ctx, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("get after unsave: %v", err)
|
||||
}
|
||||
if got := out.(*tg.MessagesSavedGifs); len(got.Gifs) != 1 || got.Gifs[0].(*tg.Document).ID != 202 {
|
||||
t.Fatalf("saved gifs after unsave = %+v, want [202]", got.Gifs)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -530,9 +530,10 @@ func (r *Router) resolveInputMedia(ctx context.Context, userID int64, input tg.I
|
|||
return nil, fileReferenceInvalidErr()
|
||||
}
|
||||
spec := domain.DocumentSpec{
|
||||
MimeType: in.MimeType,
|
||||
Attributes: domainDocumentAttributes(in.Attributes),
|
||||
ForceFile: in.ForceFile,
|
||||
MimeType: in.MimeType,
|
||||
Attributes: domainDocumentAttributes(in.Attributes),
|
||||
ForceFile: in.ForceFile,
|
||||
NosoundVideo: in.NosoundVideo,
|
||||
}
|
||||
if thumb, ok := in.GetThumb(); ok {
|
||||
if tref, ok := uploadedFileRef(userID, thumb); ok {
|
||||
|
|
@ -951,7 +952,7 @@ func domainDocumentAttributes(attrs []tg.DocumentAttributeClass) []domain.Docume
|
|||
}
|
||||
out = append(out, attr)
|
||||
case *tg.DocumentAttributeVideo:
|
||||
out = append(out, domain.DocumentAttribute{Kind: domain.DocAttrVideo, W: v.W, H: v.H, Duration: v.Duration, RoundMessage: v.RoundMessage, SupportsStreaming: v.SupportsStreaming})
|
||||
out = append(out, domain.DocumentAttribute{Kind: domain.DocAttrVideo, W: v.W, H: v.H, Duration: v.Duration, RoundMessage: v.RoundMessage, SupportsStreaming: v.SupportsStreaming, NoSound: v.Nosound, VideoCodec: v.VideoCodec})
|
||||
case *tg.DocumentAttributeAudio:
|
||||
out = append(out, domain.DocumentAttribute{Kind: domain.DocAttrAudio, AudioDuration: v.Duration, Voice: v.Voice, Title: v.Title, Performer: v.Performer, Waveform: v.Waveform})
|
||||
case *tg.DocumentAttributeFilename:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue