Skip to content

Commit 9da5764

Browse files
committed
feat: SendLivePhoto
messages.sendMedia with InputMediaUploadedPhoto{LivePhoto:true, Video:...}: the still photo is an uploaded file, the live_photo video is finalized to an input document (upload via uploadMedia, or a file_id). Reuses the message.Sender media path for caption/reply/markup. Factors out a mimeVideoMP4 constant. Mapping confirmed against telegram-bot-api 10.1. Hermetic tests; lint clean.
1 parent d794f2c commit 9da5764

6 files changed

Lines changed: 145 additions & 5 deletions

File tree

edit_media.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,10 @@ func (b *Bot) inputMediaToTg(ctx context.Context, m InputMedia) (tg.InputMediaCl
8686
media, err := b.photoInputMedia(ctx, m.Media)
8787
return media, captionData{m.Caption, m.ParseMode, m.CaptionEntities}, err
8888
case *InputMediaVideo:
89-
media, err := b.documentInputMedia(ctx, m.Media, "video/mp4")
89+
media, err := b.documentInputMedia(ctx, m.Media, mimeVideoMP4)
9090
return media, captionData{m.Caption, m.ParseMode, m.CaptionEntities}, err
9191
case *InputMediaAnimation:
92-
media, err := b.documentInputMedia(ctx, m.Media, "video/mp4")
92+
media, err := b.documentInputMedia(ctx, m.Media, mimeVideoMP4)
9393
return media, captionData{m.Caption, m.ParseMode, m.CaptionEntities}, err
9494
case *InputMediaAudio:
9595
media, err := b.documentInputMedia(ctx, m.Media, "audio/mpeg")

inline_query_result.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ import (
99
// mimeImageJPEG is the default thumbnail MIME type for inline results.
1010
const mimeImageJPEG = "image/jpeg"
1111

12+
// mimeVideoMP4 is the MIME type used for uploaded video documents.
13+
const mimeVideoMP4 = "video/mp4"
14+
1215
// errNilInlineResult is returned when a method is given a nil InlineQueryResult.
1316
func errNilInlineResult() *Error {
1417
return &Error{Code: 400, Description: "Bad Request: inline query result is nil"}
@@ -349,7 +352,7 @@ func (r *InlineQueryResultGif) toTg(ctx context.Context, b *Bot) (tg.InputBotInl
349352

350353
func (r *InlineQueryResultMpeg4Gif) toTg(ctx context.Context, b *Bot) (tg.InputBotInlineResultClass, error) {
351354
return b.webResult(ctx, r.ID, "gif", r.Title, "",
352-
r.Mpeg4URL, "video/mp4", r.ThumbnailURL, 0, 0,
355+
r.Mpeg4URL, mimeVideoMP4, r.ThumbnailURL, 0, 0,
353356
r.InputMessageContent, r.captioned, r.ReplyMarkup)
354357
}
355358

live_photo.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
6+
"github.com/gotd/td/telegram/message"
7+
"github.com/gotd/td/telegram/message/styling"
8+
"github.com/gotd/td/tg"
9+
)
10+
11+
// liveVideoDocument resolves the video component of a live photo into an MTProto
12+
// input document. An upload is finalized through messages.uploadMedia; a file_id
13+
// references an existing document. URLs are not supported.
14+
func (b *Bot) liveVideoDocument(ctx context.Context, file InputFile) (tg.InputDocumentClass, error) {
15+
switch f := file.(type) {
16+
case InputFileID:
17+
return inputDocumentFromFileID(string(f))
18+
case *InputFileUpload:
19+
uploaded, err := b.uploadInputFile(ctx, f)
20+
if err != nil {
21+
return nil, err
22+
}
23+
24+
media, err := b.uploadMediaRef(ctx, &tg.InputMediaUploadedDocument{
25+
File: uploaded,
26+
MimeType: mimeVideoMP4,
27+
Attributes: []tg.DocumentAttributeClass{&tg.DocumentAttributeVideo{}},
28+
})
29+
if err != nil {
30+
return nil, err
31+
}
32+
33+
doc, ok := media.(*tg.InputMediaDocument)
34+
if !ok {
35+
return nil, &Error{Code: 500, Description: "Internal Server Error: unexpected live photo video media"}
36+
}
37+
38+
return doc.ID, nil
39+
default:
40+
return nil, &Error{Code: 400, Description: "Bad Request: live photo video must be an uploaded file or file_id"}
41+
}
42+
}
43+
44+
// SendLivePhoto sends a live photo (an Apple-style still image paired with a short
45+
// video). The still photo must be an uploaded file; livePhoto is its video, an
46+
// uploaded file or a file_id.
47+
func (b *Bot) SendLivePhoto(
48+
ctx context.Context, chat ChatID, photo, livePhoto InputFile, caption string, opts ...SendOption,
49+
) (*Message, error) {
50+
up, ok := photo.(*InputFileUpload)
51+
if !ok {
52+
return nil, &Error{Code: 400, Description: "Bad Request: live photo still must be an uploaded file"}
53+
}
54+
55+
return b.sendResolvedMedia(ctx, chat, caption, func(ctx context.Context, c []styling.StyledTextOption) (message.MediaOption, error) {
56+
photoFile, err := b.uploadInputFile(ctx, up)
57+
if err != nil {
58+
return nil, err
59+
}
60+
61+
video, err := b.liveVideoDocument(ctx, livePhoto)
62+
if err != nil {
63+
return nil, err
64+
}
65+
66+
media := &tg.InputMediaUploadedPhoto{
67+
File: photoFile,
68+
Video: video,
69+
LivePhoto: true,
70+
}
71+
72+
return message.Media(media, c...), nil
73+
}, opts...)
74+
}

live_photo_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/gotd/td/tg"
8+
)
9+
10+
func TestSendLivePhoto(t *testing.T) {
11+
inv := newMockInvoker()
12+
inv.reply(tg.MessagesUploadMediaRequestTypeID, &tg.MessageMediaDocument{
13+
Document: &tg.Document{
14+
ID: 5, AccessHash: 6, FileReference: []byte{0}, MimeType: "video/mp4", Size: 2000, DCID: 2,
15+
Attributes: []tg.DocumentAttributeClass{&tg.DocumentAttributeVideo{}},
16+
},
17+
})
18+
inv.reply(tg.MessagesSendMediaRequestTypeID, sendMediaOK())
19+
20+
b := newMockBot(inv)
21+
22+
_, err := b.SendLivePhoto(context.Background(), userRef(10, 20),
23+
FileFromBytes("still.jpg", []byte("img")),
24+
FileFromBytes("live.mp4", []byte("vid")),
25+
"caption")
26+
if err != nil {
27+
t.Fatalf("SendLivePhoto: %v", err)
28+
}
29+
30+
if !inv.called(tg.MessagesUploadMediaRequestTypeID) {
31+
t.Fatal("live photo video should be finalized via uploadMedia")
32+
}
33+
34+
var req tg.MessagesSendMediaRequest
35+
36+
inv.decode(t, tg.MessagesSendMediaRequestTypeID, &req)
37+
38+
media, ok := req.Media.(*tg.InputMediaUploadedPhoto)
39+
if !ok {
40+
t.Fatalf("media = %#v, want uploaded photo", req.Media)
41+
}
42+
43+
if !media.LivePhoto {
44+
t.Fatal("LivePhoto flag should be set")
45+
}
46+
47+
if _, ok := media.GetVideo(); !ok {
48+
t.Fatal("live photo should attach a video document")
49+
}
50+
}
51+
52+
func TestSendLivePhotoStillMustUpload(t *testing.T) {
53+
inv := newMockInvoker()
54+
55+
if _, err := newMockBot(inv).SendLivePhoto(context.Background(), userRef(10, 20),
56+
FileID("somefileid"), FileFromBytes("live.mp4", []byte("vid")), ""); err == nil {
57+
t.Fatal("expected error when still photo is not an upload")
58+
}
59+
60+
if inv.count() != 0 {
61+
t.Fatalf("made %d RPC calls, want 0", inv.count())
62+
}
63+
}

paid_media.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ func (b *Bot) paidUploadedVideo(ctx context.Context, f *InputFileUpload, v Input
155155

156156
doc := &tg.InputMediaUploadedDocument{
157157
File: uploaded,
158-
MimeType: "video/mp4",
158+
MimeType: mimeVideoMP4,
159159
Attributes: []tg.DocumentAttributeClass{video},
160160
}
161161
if f.Name != "" {

stories.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ func (b *Bot) storyVideoMedia(ctx context.Context, content InputStoryContentVide
280280

281281
return &tg.InputMediaUploadedDocument{
282282
File: upFile,
283-
MimeType: "video/mp4",
283+
MimeType: mimeVideoMP4,
284284
NosoundVideo: content.IsAnimation,
285285
Attributes: []tg.DocumentAttributeClass{&video},
286286
}, nil

0 commit comments

Comments
 (0)