diff --git a/fileid/e2e_test.go b/fileid/e2e_test.go new file mode 100644 index 0000000000..9e06af6384 --- /dev/null +++ b/fileid/e2e_test.go @@ -0,0 +1,87 @@ +package fileid_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/go-faster/errors" + "go.uber.org/zap" + "go.uber.org/zap/zaptest" + + "github.com/gotd/td/fileid" + "github.com/gotd/td/internal/testutil" + "github.com/gotd/td/telegram" + "github.com/gotd/td/telegram/downloader" +) + +func runBot(ctx context.Context, token, fileID string, logger *zap.Logger) error { + bot := telegram.NewClient(telegram.TestAppID, telegram.TestAppHash, telegram.Options{ + Logger: logger, + }) + d := downloader.NewDownloader() + + return bot.Run(ctx, func(ctx context.Context) error { + auth, err := bot.Auth().Bot(ctx, token) + if err != nil { + return errors.Wrap(err, "auth bot") + } + user, ok := auth.User.AsNotEmpty() + if !ok { + return errors.Errorf("unexpected type %T", auth.User) + } + _ = user + + decoded, err := fileid.DecodeFileID(fileID) + if err != nil { + return errors.Wrap(err, "decode FileID") + } + + loc, ok := decoded.AsInputFileLocation() + if !ok { + return errors.Errorf("can't map %q", fileID) + } + + filename := "file.dat" + switch decoded.Type { + case fileid.Thumbnail, fileid.ProfilePhoto, fileid.Photo: + filename = "file.jpg" + case fileid.Video, + fileid.Animation, + fileid.VideoNote: + filename = "file.mp4" + case fileid.Audio: + filename = "file.mp3" + case fileid.Voice: + filename = "file.ogg" + case fileid.Sticker: + filename = "file.png" + } + + if _, err := d.Download(bot.API(), loc).ToPath(ctx, filename); err != nil { + return errors.Wrap(err, "download") + } + return nil + }) +} + +func TestExternalE2ECheckFileID(t *testing.T) { + testutil.SkipExternal(t) + token := os.Getenv("GOTD_E2E_BOT_TOKEN") + if token == "" { + t.Skip("Set GOTD_E2E_BOT_TOKEN env to run test.") + } + fileID := os.Getenv("GOTD_E2E_FILE_ID") + if fileID == "" { + t.Skip("Set GOTD_E2E_FILE_ID env to run test.") + } + logger := zaptest.NewLogger(t) + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + if err := runBot(ctx, token, fileID, logger.Named("bot")); err != nil { + t.Error(err) + } +} diff --git a/fileid/file_id.go b/fileid/file_id.go index c348cd5efe..b53da171ad 100644 --- a/fileid/file_id.go +++ b/fileid/file_id.go @@ -6,7 +6,6 @@ import ( "github.com/go-faster/errors" "github.com/gotd/td/bin" - "github.com/gotd/td/tg" ) // FileID represents parsed Telegram Bot API file_id. @@ -20,108 +19,6 @@ type FileID struct { PhotoSizeSource PhotoSizeSource } -// AsInputWebFileLocation converts file ID to tg.InputWebFileLocationClass. -func (f FileID) AsInputWebFileLocation() (tg.InputWebFileLocationClass, bool) { - if f.URL == "" { - return nil, false - } - - return &tg.InputWebFileLocation{ - URL: f.URL, - AccessHash: f.AccessHash, - }, true -} - -func (f FileID) asPhotoLocation() (tg.InputFileLocationClass, bool) { - switch src := f.PhotoSizeSource; src.Type { - case PhotoSizeSourceLegacy: - case PhotoSizeSourceThumbnail: - switch src.FileType { - case Photo, Thumbnail: - return &tg.InputPhotoFileLocation{ - ID: f.ID, - AccessHash: f.AccessHash, - FileReference: f.FileReference, - ThumbSize: string(f.PhotoSizeSource.ThumbnailType), - }, true - } - case PhotoSizeSourceDialogPhotoSmall, - PhotoSizeSourceDialogPhotoBig: - return &tg.InputPeerPhotoFileLocation{ - Big: src.Type == PhotoSizeSourceDialogPhotoBig, - Peer: src.dialogPeer(), - PhotoID: f.ID, - }, true - case PhotoSizeSourceStickerSetThumbnail: - case PhotoSizeSourceFullLegacy: - return &tg.InputPhotoLegacyFileLocation{ - ID: f.ID, - AccessHash: f.AccessHash, - FileReference: f.FileReference, - VolumeID: f.PhotoSizeSource.VolumeID, - LocalID: f.PhotoSizeSource.LocalID, - Secret: f.PhotoSizeSource.Secret, - }, true - case PhotoSizeSourceDialogPhotoSmallLegacy, - PhotoSizeSourceDialogPhotoBigLegacy: - return &tg.InputPeerPhotoFileLocationLegacy{ - Big: src.Type == PhotoSizeSourceDialogPhotoBigLegacy, - Peer: src.dialogPeer(), - VolumeID: f.PhotoSizeSource.VolumeID, - LocalID: f.PhotoSizeSource.LocalID, - }, true - case PhotoSizeSourceStickerSetThumbnailLegacy: - return &tg.InputStickerSetThumbLegacy{ - Stickerset: f.PhotoSizeSource.stickerSet(), - VolumeID: f.PhotoSizeSource.VolumeID, - LocalID: f.PhotoSizeSource.LocalID, - }, true - case PhotoSizeSourceStickerSetThumbnailVersion: - return &tg.InputStickerSetThumb{ - Stickerset: f.PhotoSizeSource.stickerSet(), - ThumbVersion: int(f.PhotoSizeSource.StickerVersion), - }, true - } - - return nil, false -} - -// AsInputFileLocation converts file ID to tg.InputFileLocationClass. -func (f FileID) AsInputFileLocation() (tg.InputFileLocationClass, bool) { - switch f.Type { - case Thumbnail, ProfilePhoto, Photo: - return f.asPhotoLocation() - case Encrypted: - return &tg.InputEncryptedFileLocation{ - ID: f.ID, - AccessHash: f.AccessHash, - }, true - case SecureRaw, - Secure: - return &tg.InputSecureFileLocation{ - ID: f.ID, - AccessHash: f.AccessHash, - }, true - case Video, - Voice, - Document, - Sticker, - Audio, - Animation, - VideoNote, - Background, - DocumentAsFile: - return &tg.InputDocumentFileLocation{ - ID: f.ID, - AccessHash: f.AccessHash, - FileReference: f.FileReference, - ThumbSize: "", // ? - }, true - } - - return nil, false -} - const ( webLocationFlag = 1 << 24 fileReferenceFlag = 1 << 25 diff --git a/fileid/from.go b/fileid/from.go new file mode 100644 index 0000000000..ad8feca1cd --- /dev/null +++ b/fileid/from.go @@ -0,0 +1,49 @@ +package fileid + +import "github.com/gotd/td/tg" + +// FromDocument creates FileID from tg.Document. +func FromDocument(doc *tg.Document) FileID { + fileID := FileID{ + Type: DocumentAsFile, + DC: doc.DCID, + ID: doc.ID, + AccessHash: doc.AccessHash, + FileReference: doc.FileReference, + } + for _, attr := range doc.Attributes { + switch attr := attr.(type) { + case *tg.DocumentAttributeAnimated: + fileID.Type = Animation + case *tg.DocumentAttributeSticker: + fileID.Type = Sticker + case *tg.DocumentAttributeVideo: + fileID.Type = Video + if attr.RoundMessage { + fileID.Type = VideoNote + } + case *tg.DocumentAttributeAudio: + fileID.Type = Audio + if attr.Voice { + fileID.Type = Voice + } + } + } + return fileID +} + +// FromPhoto creates FileID from tg.Photo. +func FromPhoto(photo *tg.Photo, thumbType rune) FileID { + return FileID{ + Type: Photo, + DC: photo.DCID, + ID: photo.ID, + AccessHash: photo.AccessHash, + FileReference: photo.FileReference, + PhotoSizeSource: PhotoSizeSource{ + Type: PhotoSizeSourceThumbnail, + FileType: Photo, + ThumbnailType: thumbType, + }, + } +} diff --git a/fileid/from_test.go b/fileid/from_test.go new file mode 100644 index 0000000000..1889b73e1a --- /dev/null +++ b/fileid/from_test.go @@ -0,0 +1,86 @@ +package fileid + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/gotd/td/tg" +) + +func TestFromDocument(t *testing.T) { + doc := func(attrs ...tg.DocumentAttributeClass) *tg.Document { + return &tg.Document{ + ID: 1, + AccessHash: 2, + FileReference: []byte{3}, + DCID: 4, + Attributes: attrs, + } + } + fileID := func(typ Type) FileID { + return FileID{ + Type: typ, + ID: 1, + AccessHash: 2, + FileReference: []byte{3}, + DC: 4, + } + } + + tests := []struct { + name string + doc *tg.Document + want FileID + }{ + {"File", doc(), fileID(DocumentAsFile)}, + {"Animation", doc(&tg.DocumentAttributeAnimated{}), fileID(Animation)}, + {"Sticker", doc(&tg.DocumentAttributeSticker{}), fileID(Sticker)}, + {"Video", doc(&tg.DocumentAttributeVideo{}), fileID(Video)}, + {"VideoNote", doc(&tg.DocumentAttributeVideo{RoundMessage: true}), fileID(VideoNote)}, + {"Audio", doc(&tg.DocumentAttributeAudio{}), fileID(Audio)}, + {"Voice", doc(&tg.DocumentAttributeAudio{Voice: true}), fileID(Voice)}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, FromDocument(tt.doc)) + }) + } +} + +func TestFromPhoto(t *testing.T) { + tests := []struct { + name string + photo *tg.Photo + size rune + want FileID + }{ + { + "Photo", + &tg.Photo{ + ID: 1, + AccessHash: 2, + FileReference: []byte{3}, + DCID: 4, + }, + 'x', + FileID{ + Type: Photo, + ID: 1, + AccessHash: 2, + FileReference: []byte{3}, + DC: 4, + PhotoSizeSource: PhotoSizeSource{ + Type: PhotoSizeSourceThumbnail, + FileType: Photo, + ThumbnailType: 'x', + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, FromPhoto(tt.photo, tt.size)) + }) + } +} diff --git a/fileid/into.go b/fileid/into.go new file mode 100644 index 0000000000..564ac714e9 --- /dev/null +++ b/fileid/into.go @@ -0,0 +1,105 @@ +package fileid + +import "github.com/gotd/td/tg" + +// AsInputWebFileLocation converts file ID to tg.InputWebFileLocationClass. +func (f FileID) AsInputWebFileLocation() (tg.InputWebFileLocationClass, bool) { + if f.URL == "" { + return nil, false + } + + return &tg.InputWebFileLocation{ + URL: f.URL, + AccessHash: f.AccessHash, + }, true +} + +func (f FileID) asPhotoLocation() (tg.InputFileLocationClass, bool) { + switch src := f.PhotoSizeSource; src.Type { + case PhotoSizeSourceLegacy: + case PhotoSizeSourceThumbnail: + switch src.FileType { + case Photo, Thumbnail: + return &tg.InputPhotoFileLocation{ + ID: f.ID, + AccessHash: f.AccessHash, + FileReference: f.FileReference, + ThumbSize: string(f.PhotoSizeSource.ThumbnailType), + }, true + } + case PhotoSizeSourceDialogPhotoSmall, + PhotoSizeSourceDialogPhotoBig: + return &tg.InputPeerPhotoFileLocation{ + Big: src.Type == PhotoSizeSourceDialogPhotoBig, + Peer: src.dialogPeer(), + PhotoID: f.ID, + }, true + case PhotoSizeSourceStickerSetThumbnail: + case PhotoSizeSourceFullLegacy: + return &tg.InputPhotoLegacyFileLocation{ + ID: f.ID, + AccessHash: f.AccessHash, + FileReference: f.FileReference, + VolumeID: f.PhotoSizeSource.VolumeID, + LocalID: f.PhotoSizeSource.LocalID, + Secret: f.PhotoSizeSource.Secret, + }, true + case PhotoSizeSourceDialogPhotoSmallLegacy, + PhotoSizeSourceDialogPhotoBigLegacy: + return &tg.InputPeerPhotoFileLocationLegacy{ + Big: src.Type == PhotoSizeSourceDialogPhotoBigLegacy, + Peer: src.dialogPeer(), + VolumeID: f.PhotoSizeSource.VolumeID, + LocalID: f.PhotoSizeSource.LocalID, + }, true + case PhotoSizeSourceStickerSetThumbnailLegacy: + return &tg.InputStickerSetThumbLegacy{ + Stickerset: f.PhotoSizeSource.stickerSet(), + VolumeID: f.PhotoSizeSource.VolumeID, + LocalID: f.PhotoSizeSource.LocalID, + }, true + case PhotoSizeSourceStickerSetThumbnailVersion: + return &tg.InputStickerSetThumb{ + Stickerset: f.PhotoSizeSource.stickerSet(), + ThumbVersion: int(f.PhotoSizeSource.StickerVersion), + }, true + } + + return nil, false +} + +// AsInputFileLocation converts file ID to tg.InputFileLocationClass. +func (f FileID) AsInputFileLocation() (tg.InputFileLocationClass, bool) { + switch f.Type { + case Thumbnail, ProfilePhoto, Photo: + return f.asPhotoLocation() + case Encrypted: + return &tg.InputEncryptedFileLocation{ + ID: f.ID, + AccessHash: f.AccessHash, + }, true + case SecureRaw, + Secure: + return &tg.InputSecureFileLocation{ + ID: f.ID, + AccessHash: f.AccessHash, + }, true + case Video, + Voice, + Document, + Sticker, + Audio, + Animation, + VideoNote, + Background, + DocumentAsFile: + return &tg.InputDocumentFileLocation{ + ID: f.ID, + AccessHash: f.AccessHash, + FileReference: f.FileReference, + ThumbSize: "", // ? + }, true + } + + return nil, false +} diff --git a/fileid/file_id_test.go b/fileid/into_test.go similarity index 100% rename from fileid/file_id_test.go rename to fileid/into_test.go