From 0463e225435cc4c63832bd7ad7ce19769fbf038e Mon Sep 17 00:00:00 2001 From: Demian Date: Sun, 7 Nov 2021 18:28:21 +0200 Subject: [PATCH] telebot: rework media, implement Media and Inputtable interfaces --- api.go | 5 +- bot.go | 159 +++++++++++++--------------------------------------- bot_test.go | 9 ++- file.go | 11 ++-- media.go | 153 ++++++++++++++++++++++++++++++++++++++------------ message.go | 8 +++ util.go | 15 +---- 7 files changed, 182 insertions(+), 178 deletions(-) diff --git a/api.go b/api.go index 52a2f5f..a63fc47 100644 --- a/api.go +++ b/api.go @@ -83,11 +83,12 @@ func (b *Bot) sendFiles(method string, files map[string]File, params map[string] pipeReader, pipeWriter := io.Pipe() writer := multipart.NewWriter(pipeWriter) + go func() { defer pipeWriter.Close() for field, file := range rawFiles { - if err := addFileToWriter(writer, params["file_name"], field, file); err != nil { + if err := addFileToWriter(writer, files[field].fileName, field, file); err != nil { pipeWriter.CloseWithError(err) return } @@ -139,7 +140,7 @@ func addFileToWriter(writer *multipart.Writer, filename, field string, file inte defer f.Close() reader = f } else { - return errors.Errorf("telebot: file for field %v should be an io.ReadCloser or string", field) + return errors.Errorf("telebot: file for field %v should be io.ReadCloser or string", field) } part, err := writer.CreateFormFile(field, filename) diff --git a/bot.go b/bot.go index 71f1b8c..c0f13bf 100644 --- a/bot.go +++ b/bot.go @@ -276,8 +276,8 @@ func (b *Bot) ProcessUpdate(upd Update) { match := cmdRx.FindAllStringSubmatch(m.Text, -1) if match != nil { // Syntax: "@ " - command, botName := match[0][1], match[0][3] + if botName != "" && !strings.EqualFold(b.Me.Username, botName) { return } @@ -301,11 +301,30 @@ func (b *Bot) ProcessUpdate(upd Update) { return } + if m.Contact != nil { + b.handle(OnContact, c) + return + } + if m.Location != nil { + b.handle(OnLocation, c) + return + } + if m.Venue != nil { + b.handle(OnVenue, c) + return + } + if m.Game != nil { + b.handle(OnGame, c) + return + } + if m.Dice != nil { + b.handle(OnDice, c) + return + } if m.Invoice != nil { b.handle(OnInvoice, c) return } - if m.Payment != nil { b.handle(OnPayment, c) return @@ -526,16 +545,6 @@ func (b *Bot) handleMedia(c Context) bool { fired = b.handle(OnVideo, c) case m.VideoNote != nil: fired = b.handle(OnVideoNote, c) - case m.Contact != nil: - fired = b.handle(OnContact, c) - case m.Location != nil: - fired = b.handle(OnLocation, c) - case m.Venue != nil: - fired = b.handle(OnVenue, c) - case m.Game != nil: - fired = b.handle(OnGame, c) - case m.Dice != nil: - fired = b.handle(OnDice, c) default: return false } @@ -585,6 +594,7 @@ func (b *Bot) SendAlbum(to Recipient, a Album, opts ...interface{}) ([]Message, return nil, ErrBadRecipient } + sendOpts := extractOptions(opts) media := make([]string, len(a)) files := make(map[string]File) @@ -607,41 +617,11 @@ func (b *Bot) SendAlbum(to Recipient, a Album, opts ...interface{}) ([]Message, return nil, errors.Errorf("telebot: album entry #%d does not exist", i) } - switch y := x.(type) { - case *Photo: - data, _ = json.Marshal(struct { - Type string `json:"type"` - Media string `json:"media"` - Caption string `json:"caption,omitempty"` - ParseMode ParseMode `json:"parse_mode,omitempty"` - }{ - Type: "photo", - Media: repr, - Caption: y.Caption, - ParseMode: y.ParseMode, - }) - case *Video: - data, _ = json.Marshal(struct { - Type string `json:"type"` - Caption string `json:"caption"` - Media string `json:"media"` - Width int `json:"width,omitempty"` - Height int `json:"height,omitempty"` - Duration int `json:"duration,omitempty"` - SupportsStreaming bool `json:"supports_streaming,omitempty"` - }{ - Type: "video", - Caption: y.Caption, - Media: repr, - Width: y.Width, - Height: y.Height, - Duration: y.Duration, - SupportsStreaming: y.SupportsStreaming, - }) - default: - return nil, errors.Errorf("telebot: album entry #%d is not valid", i) - } + im := x.InputMedia() + im.Media = repr + im.ParseMode = sendOpts.ParseMode + data, _ = json.Marshal(im) media[i] = string(data) } @@ -649,8 +629,6 @@ func (b *Bot) SendAlbum(to Recipient, a Album, opts ...interface{}) ([]Message, "chat_id": to.Recipient(), "media": "[" + strings.Join(media, ",") + "]", } - - sendOpts := extractOptions(opts) b.embedSendOptions(params, sendOpts) data, err := b.sendFiles("sendMediaGroup", files, params) @@ -769,7 +747,7 @@ func (b *Bot) Edit(msg Editable, what interface{}, opts ...interface{}) (*Messag switch v := what.(type) { case *ReplyMarkup: return b.EditReplyMarkup(msg, v) - case InputMedia: + case Inputtable: return b.EditMedia(msg, v, opts...) case string: method = "editMessageText" @@ -889,14 +867,14 @@ func (b *Bot) EditCaption(msg Editable, caption string, opts ...interface{}) (*M // b.EditMedia(m, &tele.Photo{File: tele.FromDisk("chicken.jpg")}) // b.EditMedia(m, &tele.Video{File: tele.FromURL("http://video.mp4")}) // -func (b *Bot) EditMedia(msg Editable, media InputMedia, opts ...interface{}) (*Message, error) { +func (b *Bot) EditMedia(msg Editable, media Inputtable, opts ...interface{}) (*Message, error) { var ( repr string - thumb *Photo + file = media.MediaFile() + files = make(map[string]File) + thumb *Photo thumbName = "thumb" - file = media.MediaFile() - files = make(map[string]File) ) switch { @@ -915,76 +893,18 @@ func (b *Bot) EditMedia(msg Editable, media InputMedia, opts ...interface{}) (*M repr = "attach://" + s files[s] = *file default: - return nil, errors.Errorf("telebot: can't edit media, it does not exist") - } - - type FileJSON struct { - // All types. - Type string `json:"type"` - Caption string `json:"caption"` - Media string `json:"media"` - ParseMode ParseMode `json:"parse_mode,omitempty"` - - // Video. - Width int `json:"width,omitempty"` - Height int `json:"height,omitempty"` - SupportsStreaming bool `json:"supports_streaming,omitempty"` - - // Video and audio. - Duration int `json:"duration,omitempty"` - - // Document. - FileName string `json:"file_name"` - - // Document, video and audio. - Thumbnail string `json:"thumb,omitempty"` - MIME string `json:"mime_type,omitempty"` - - // Audio. - Title string `json:"title,omitempty"` - Performer string `json:"performer,omitempty"` + return nil, errors.Errorf("telebot: cannot edit media, it does not exist") } - result := &FileJSON{Media: repr} - switch m := media.(type) { - case *Photo: - result.Type = "photo" - result.Caption = m.Caption case *Video: - result.Type = "video" - result.Caption = m.Caption - result.Width = m.Width - result.Height = m.Height - result.Duration = m.Duration - result.SupportsStreaming = m.SupportsStreaming - result.MIME = m.MIME - thumb = m.Thumbnail - case *Document: - result.Type = "document" - result.Caption = m.Caption - result.FileName = m.FileName - result.MIME = m.MIME thumb = m.Thumbnail case *Audio: - result.Type = "audio" - result.Caption = m.Caption - result.Duration = m.Duration - result.MIME = m.MIME - result.Title = m.Title - result.Performer = m.Performer + thumb = m.Thumbnail + case *Document: thumb = m.Thumbnail case *Animation: - result.Type = "animation" - result.Caption = m.Caption - result.Width = m.Width - result.Height = m.Height - result.Duration = m.Duration - result.MIME = m.MIME - result.FileName = m.FileName thumb = m.Thumbnail - default: - return nil, errors.Errorf("telebot: media entry is not valid") } msgID, chatID := msg.MessageSig() @@ -993,15 +913,16 @@ func (b *Bot) EditMedia(msg Editable, media InputMedia, opts ...interface{}) (*M sendOpts := extractOptions(opts) b.embedSendOptions(params, sendOpts) - if sendOpts != nil { - result.ParseMode = params["parse_mode"] - } + im := media.InputMedia() + im.Media = repr + im.ParseMode = sendOpts.ParseMode + if thumb != nil { - result.Thumbnail = "attach://" + thumbName + im.Thumbnail = "attach://" + thumbName files[thumbName] = *thumb.MediaFile() } - data, _ := json.Marshal(result) + data, _ := json.Marshal(im) params["media"] = string(data) if chatID == 0 { // if inline message diff --git a/bot_test.go b/bot_test.go index 4599202..2c0ca60 100644 --- a/bot_test.go +++ b/bot_test.go @@ -405,7 +405,10 @@ func TestBot(t *testing.T) { _, err = b.SendAlbum(to, nil) assert.Error(t, err) - msgs, err := b.SendAlbum(to, Album{photo, photo}) + photo2 := *photo + photo2.Caption = "" + + msgs, err := b.SendAlbum(to, Album{photo, &photo2}, ModeHTML) require.NoError(t, err) assert.Len(t, msgs, 2) assert.NotEmpty(t, msgs[0].AlbumID) @@ -427,7 +430,7 @@ func TestBot(t *testing.T) { b.parseMode = ModeDefault }) - t.Run("Edit(what=InputMedia)", func(t *testing.T) { + t.Run("Edit(what=Media)", func(t *testing.T) { edited, err := b.Edit(msg, photo) require.NoError(t, err) assert.Equal(t, edited.Photo.UniqueID, photo.UniqueID) @@ -536,7 +539,7 @@ func TestBot(t *testing.T) { assert.NotNil(t, edited.Location) }) - // should be the last + // Should be after the Edit tests. t.Run("Delete()", func(t *testing.T) { require.NoError(t, b.Delete(msg)) }) diff --git a/file.go b/file.go index ea97ba5..707c432 100644 --- a/file.go +++ b/file.go @@ -9,20 +9,21 @@ import ( type File struct { FileID string `json:"file_id"` UniqueID string `json:"file_unique_id"` - FileName string `json:"file_name"` FileSize int `json:"file_size"` - // file on telegram server https://core.telegram.org/bots/api#file + // FilePath is used for files on Telegram server. FilePath string `json:"file_path"` - // file on local file system. + // FileLocal uis ed for files on local file system. FileLocal string `json:"file_local"` - // file on the internet + // FileURL is used for file on the internet. FileURL string `json:"file_url"` - // file backed with io.Reader + // FileReader is used for file backed with io.Reader. FileReader io.Reader `json:"-"` + + fileName string } // FromDisk constructs a new local (on-disk) file object. diff --git a/media.go b/media.go index 0632ca5..2ca4239 100644 --- a/media.go +++ b/media.go @@ -4,28 +4,50 @@ import ( "encoding/json" ) -// Album lets you group multiple media (so-called InputMedia) -// into a single message. -type Album []InputMedia +// Media is a generic type for all kinds of media that includes File. +type Media interface { + // MediaType returns string-represented media type. + MediaType() string -// InputMedia is a generic type for all kinds of media you -// can put into an album. -type InputMedia interface { - // As some files must be uploaded (instead of referencing) - // outer layers of Telebot require it. + // MediaFile returns a pointer to the media file. MediaFile() *File } +// InputMedia represents a composite InputMedia struct that is +// used by Telebot in sending and editing media methods. +type InputMedia struct { + Type string `json:"type"` + Media string `json:"media"` + ParseMode string `json:"parse_mode"` + Thumbnail string `json:"thumb"` + + *Photo + *Audio + *Video + *Document + *Animation +} + +// Inputtable is a generic type for all kinds of media you +// can put into an album. +type Inputtable interface { + Media + + // InputMedia returns already marshalled InputMedia type + // ready to be used in sending and editing media methods. + InputMedia() InputMedia +} + +// Album lets you group multiple media into a single message. +type Album []Inputtable + // Photo object represents a single photo file. type Photo struct { File - Width int `json:"width"` - Height int `json:"height"` - - // (Optional) - Caption string `json:"caption,omitempty"` - ParseMode ParseMode `json:"parse_mode,omitempty"` + Width int `json:"width"` + Height int `json:"height"` + Caption string `json:"caption,omitempty"` } type photoSize struct { @@ -36,27 +58,36 @@ type photoSize struct { Caption string `json:"caption,omitempty"` } -// MediaFile returns &Photo.File +func (p *Photo) MediaType() string { + return "photo" +} + func (p *Photo) MediaFile() *File { return &p.File } +func (p *Photo) InputMedia() InputMedia { + return InputMedia{ + Type: p.MediaType(), + Photo: p, + } +} + // UnmarshalJSON is custom unmarshaller required to abstract // away the hassle of treating different thumbnail sizes. // Instead, Telebot chooses the hi-res one and just sticks to it. // // I really do find it a beautiful solution. -func (p *Photo) UnmarshalJSON(jsonStr []byte) error { +func (p *Photo) UnmarshalJSON(data []byte) error { var hq photoSize - if jsonStr[0] == '{' { - if err := json.Unmarshal(jsonStr, &hq); err != nil { + if data[0] == '{' { + if err := json.Unmarshal(data, &hq); err != nil { return err } } else { var sizes []photoSize - - if err := json.Unmarshal(jsonStr, &sizes); err != nil { + if err := json.Unmarshal(data, &sizes); err != nil { return err } @@ -85,11 +116,22 @@ type Audio struct { FileName string `json:"file_name,omitempty"` } -// MediaFile returns &Audio.File +func (a *Audio) MediaType() string { + return "audio" +} + func (a *Audio) MediaFile() *File { + a.fileName = a.FileName return &a.File } +func (a *Audio) InputMedia() InputMedia { + return InputMedia{ + Type: a.MediaType(), + Audio: a, + } +} + // Document object represents a general file (as opposed to Photo or Audio). // Telegram users can send files of any type of up to 1.5 GB in size. type Document struct { @@ -102,11 +144,22 @@ type Document struct { FileName string `json:"file_name,omitempty"` } -// MediaFile returns &Document.File +func (d *Document) MediaType() string { + return "document" +} + func (d *Document) MediaFile() *File { + d.fileName = d.FileName return &d.File } +func (d *Document) InputMedia() InputMedia { + return InputMedia{ + Type: d.MediaType(), + Document: d, + } +} + // Video object represents a video file. type Video struct { File @@ -123,11 +176,22 @@ type Video struct { FileName string `json:"file_name,omitempty"` } -// MediaFile returns &Video.File +func (v *Video) MediaType() string { + return "video" +} + func (v *Video) MediaFile() *File { + v.fileName = v.FileName return &v.File } +func (v *Video) InputMedia() InputMedia { + return InputMedia{ + Type: v.MediaType(), + Video: v, + } +} + // Animation object represents a animation file. type Animation struct { File @@ -143,11 +207,22 @@ type Animation struct { FileName string `json:"file_name,omitempty"` } -// MediaFile returns &Animation.File +func (a *Animation) MediaType() string { + return "animation" +} + func (a *Animation) MediaFile() *File { + a.fileName = a.FileName return &a.File } +func (a *Animation) InputMedia() InputMedia { + return InputMedia{ + Type: a.MediaType(), + Animation: a, + } +} + // Voice object represents a voice note. type Voice struct { File @@ -159,10 +234,18 @@ type Voice struct { MIME string `json:"mime_type,omitempty"` } -// VideoNote represents a video message (available in Telegram apps -// as of v.4.0). +func (v *Voice) MediaType() string { + return "voice" +} + +func (v *Voice) MediaFile() *File { + return &v.File +} + +// VideoNote represents a video message. type VideoNote struct { File + Duration int `json:"duration"` // (Optional) @@ -170,7 +253,15 @@ type VideoNote struct { Length int `json:"length,omitempty"` } -// Contact object represents a contact to Telegram user +func (v *VideoNote) MediaType() string { + return "video_note" +} + +func (v *VideoNote) MediaFile() *File { + return &v.File +} + +// Contact object represents a contact to Telegram user. type Contact struct { PhoneNumber string `json:"phone_number"` FirstName string `json:"first_name"` @@ -193,14 +284,6 @@ type Location struct { LivePeriod int `json:"live_period,omitempty"` } -// ProximityAlert sent whenever -// a user in the chat triggers a proximity alert set by another user. -type ProximityAlert struct { - Traveler *User `json:"traveler,omitempty"` - Watcher *User `json:"watcher,omitempty"` - Distance int `json:"distance"` -} - // Venue object represents a venue location with name, address and // optional foursquare ID. type Venue struct { diff --git a/message.go b/message.go index 2506e08..c3bb707 100644 --- a/message.go +++ b/message.go @@ -270,6 +270,14 @@ type MessageEntity struct { // Entities is used to set message's text entities as a send option. type Entities []MessageEntity +// ProximityAlert sent whenever a user in the chat triggers +// a proximity alert set by another user. +type ProximityAlert struct { + Traveler *User `json:"traveler,omitempty"` + Watcher *User `json:"watcher,omitempty"` + Distance int `json:"distance"` +} + // AutoDeleteTimer represents a service message about a change in auto-delete timer settings. type AutoDeleteTimer struct { Unixtime int `json:"message_auto_delete_time"` diff --git a/util.go b/util.go index 8f2a492..b1f4021 100644 --- a/util.go +++ b/util.go @@ -132,24 +132,17 @@ func extractMessage(data []byte) (*Message, error) { } func extractOptions(how []interface{}) *SendOptions { - var opts *SendOptions + opts := &SendOptions{} for _, prop := range how { switch opt := prop.(type) { case *SendOptions: opts = opt.copy() case *ReplyMarkup: - if opts == nil { - opts = &SendOptions{} - } if opt != nil { opts.ReplyMarkup = opt.copy() } case Option: - if opts == nil { - opts = &SendOptions{} - } - switch opt { case NoPreview: opts.DisableWebPagePreview = true @@ -174,14 +167,8 @@ func extractOptions(how []interface{}) *SendOptions { panic("telebot: unsupported flag-option") } case ParseMode: - if opts == nil { - opts = &SendOptions{} - } opts.ParseMode = opt case Entities: - if opts == nil { - opts = &SendOptions{} - } opts.Entities = opt default: panic("telebot: unsupported send-option")