telebot: rework media, implement Media and Inputtable interfaces

This commit is contained in:
Demian 2021-11-07 18:28:21 +02:00
parent 435b1e3b63
commit 0463e22543
7 changed files with 182 additions and 178 deletions

5
api.go
View File

@ -83,11 +83,12 @@ func (b *Bot) sendFiles(method string, files map[string]File, params map[string]
pipeReader, pipeWriter := io.Pipe() pipeReader, pipeWriter := io.Pipe()
writer := multipart.NewWriter(pipeWriter) writer := multipart.NewWriter(pipeWriter)
go func() { go func() {
defer pipeWriter.Close() defer pipeWriter.Close()
for field, file := range rawFiles { 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) pipeWriter.CloseWithError(err)
return return
} }
@ -139,7 +140,7 @@ func addFileToWriter(writer *multipart.Writer, filename, field string, file inte
defer f.Close() defer f.Close()
reader = f reader = f
} else { } 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) part, err := writer.CreateFormFile(field, filename)

159
bot.go
View File

@ -276,8 +276,8 @@ func (b *Bot) ProcessUpdate(upd Update) {
match := cmdRx.FindAllStringSubmatch(m.Text, -1) match := cmdRx.FindAllStringSubmatch(m.Text, -1)
if match != nil { if match != nil {
// Syntax: "</command>@<bot> <payload>" // Syntax: "</command>@<bot> <payload>"
command, botName := match[0][1], match[0][3] command, botName := match[0][1], match[0][3]
if botName != "" && !strings.EqualFold(b.Me.Username, botName) { if botName != "" && !strings.EqualFold(b.Me.Username, botName) {
return return
} }
@ -301,11 +301,30 @@ func (b *Bot) ProcessUpdate(upd Update) {
return 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 { if m.Invoice != nil {
b.handle(OnInvoice, c) b.handle(OnInvoice, c)
return return
} }
if m.Payment != nil { if m.Payment != nil {
b.handle(OnPayment, c) b.handle(OnPayment, c)
return return
@ -526,16 +545,6 @@ func (b *Bot) handleMedia(c Context) bool {
fired = b.handle(OnVideo, c) fired = b.handle(OnVideo, c)
case m.VideoNote != nil: case m.VideoNote != nil:
fired = b.handle(OnVideoNote, c) 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: default:
return false return false
} }
@ -585,6 +594,7 @@ func (b *Bot) SendAlbum(to Recipient, a Album, opts ...interface{}) ([]Message,
return nil, ErrBadRecipient return nil, ErrBadRecipient
} }
sendOpts := extractOptions(opts)
media := make([]string, len(a)) media := make([]string, len(a))
files := make(map[string]File) 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) return nil, errors.Errorf("telebot: album entry #%d does not exist", i)
} }
switch y := x.(type) { im := x.InputMedia()
case *Photo: im.Media = repr
data, _ = json.Marshal(struct { im.ParseMode = sendOpts.ParseMode
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)
}
data, _ = json.Marshal(im)
media[i] = string(data) media[i] = string(data)
} }
@ -649,8 +629,6 @@ func (b *Bot) SendAlbum(to Recipient, a Album, opts ...interface{}) ([]Message,
"chat_id": to.Recipient(), "chat_id": to.Recipient(),
"media": "[" + strings.Join(media, ",") + "]", "media": "[" + strings.Join(media, ",") + "]",
} }
sendOpts := extractOptions(opts)
b.embedSendOptions(params, sendOpts) b.embedSendOptions(params, sendOpts)
data, err := b.sendFiles("sendMediaGroup", files, params) 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) { switch v := what.(type) {
case *ReplyMarkup: case *ReplyMarkup:
return b.EditReplyMarkup(msg, v) return b.EditReplyMarkup(msg, v)
case InputMedia: case Inputtable:
return b.EditMedia(msg, v, opts...) return b.EditMedia(msg, v, opts...)
case string: case string:
method = "editMessageText" 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.Photo{File: tele.FromDisk("chicken.jpg")})
// b.EditMedia(m, &tele.Video{File: tele.FromURL("http://video.mp4")}) // 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 ( var (
repr string repr string
thumb *Photo
thumbName = "thumb"
file = media.MediaFile() file = media.MediaFile()
files = make(map[string]File) files = make(map[string]File)
thumb *Photo
thumbName = "thumb"
) )
switch { switch {
@ -915,76 +893,18 @@ func (b *Bot) EditMedia(msg Editable, media InputMedia, opts ...interface{}) (*M
repr = "attach://" + s repr = "attach://" + s
files[s] = *file files[s] = *file
default: default:
return nil, errors.Errorf("telebot: can't edit media, it does not exist") return nil, errors.Errorf("telebot: cannot 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"`
}
result := &FileJSON{Media: repr}
switch m := media.(type) { switch m := media.(type) {
case *Photo:
result.Type = "photo"
result.Caption = m.Caption
case *Video: 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 thumb = m.Thumbnail
case *Audio: case *Audio:
result.Type = "audio" thumb = m.Thumbnail
result.Caption = m.Caption case *Document:
result.Duration = m.Duration
result.MIME = m.MIME
result.Title = m.Title
result.Performer = m.Performer
thumb = m.Thumbnail thumb = m.Thumbnail
case *Animation: 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 thumb = m.Thumbnail
default:
return nil, errors.Errorf("telebot: media entry is not valid")
} }
msgID, chatID := msg.MessageSig() msgID, chatID := msg.MessageSig()
@ -993,15 +913,16 @@ func (b *Bot) EditMedia(msg Editable, media InputMedia, opts ...interface{}) (*M
sendOpts := extractOptions(opts) sendOpts := extractOptions(opts)
b.embedSendOptions(params, sendOpts) b.embedSendOptions(params, sendOpts)
if sendOpts != nil { im := media.InputMedia()
result.ParseMode = params["parse_mode"] im.Media = repr
} im.ParseMode = sendOpts.ParseMode
if thumb != nil { if thumb != nil {
result.Thumbnail = "attach://" + thumbName im.Thumbnail = "attach://" + thumbName
files[thumbName] = *thumb.MediaFile() files[thumbName] = *thumb.MediaFile()
} }
data, _ := json.Marshal(result) data, _ := json.Marshal(im)
params["media"] = string(data) params["media"] = string(data)
if chatID == 0 { // if inline message if chatID == 0 { // if inline message

View File

@ -405,7 +405,10 @@ func TestBot(t *testing.T) {
_, err = b.SendAlbum(to, nil) _, err = b.SendAlbum(to, nil)
assert.Error(t, err) 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) require.NoError(t, err)
assert.Len(t, msgs, 2) assert.Len(t, msgs, 2)
assert.NotEmpty(t, msgs[0].AlbumID) assert.NotEmpty(t, msgs[0].AlbumID)
@ -427,7 +430,7 @@ func TestBot(t *testing.T) {
b.parseMode = ModeDefault 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) edited, err := b.Edit(msg, photo)
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, edited.Photo.UniqueID, photo.UniqueID) assert.Equal(t, edited.Photo.UniqueID, photo.UniqueID)
@ -536,7 +539,7 @@ func TestBot(t *testing.T) {
assert.NotNil(t, edited.Location) assert.NotNil(t, edited.Location)
}) })
// should be the last // Should be after the Edit tests.
t.Run("Delete()", func(t *testing.T) { t.Run("Delete()", func(t *testing.T) {
require.NoError(t, b.Delete(msg)) require.NoError(t, b.Delete(msg))
}) })

11
file.go
View File

@ -9,20 +9,21 @@ import (
type File struct { type File struct {
FileID string `json:"file_id"` FileID string `json:"file_id"`
UniqueID string `json:"file_unique_id"` UniqueID string `json:"file_unique_id"`
FileName string `json:"file_name"`
FileSize int `json:"file_size"` 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"` FilePath string `json:"file_path"`
// file on local file system. // FileLocal uis ed for files on local file system.
FileLocal string `json:"file_local"` FileLocal string `json:"file_local"`
// file on the internet // FileURL is used for file on the internet.
FileURL string `json:"file_url"` FileURL string `json:"file_url"`
// file backed with io.Reader // FileReader is used for file backed with io.Reader.
FileReader io.Reader `json:"-"` FileReader io.Reader `json:"-"`
fileName string
} }
// FromDisk constructs a new local (on-disk) file object. // FromDisk constructs a new local (on-disk) file object.

147
media.go
View File

@ -4,28 +4,50 @@ import (
"encoding/json" "encoding/json"
) )
// Album lets you group multiple media (so-called InputMedia) // Media is a generic type for all kinds of media that includes File.
// into a single message. type Media interface {
type Album []InputMedia // MediaType returns string-represented media type.
MediaType() string
// InputMedia is a generic type for all kinds of media you // MediaFile returns a pointer to the media file.
// can put into an album.
type InputMedia interface {
// As some files must be uploaded (instead of referencing)
// outer layers of Telebot require it.
MediaFile() *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. // Photo object represents a single photo file.
type Photo struct { type Photo struct {
File File
Width int `json:"width"` Width int `json:"width"`
Height int `json:"height"` Height int `json:"height"`
// (Optional)
Caption string `json:"caption,omitempty"` Caption string `json:"caption,omitempty"`
ParseMode ParseMode `json:"parse_mode,omitempty"`
} }
type photoSize struct { type photoSize struct {
@ -36,27 +58,36 @@ type photoSize struct {
Caption string `json:"caption,omitempty"` Caption string `json:"caption,omitempty"`
} }
// MediaFile returns &Photo.File func (p *Photo) MediaType() string {
return "photo"
}
func (p *Photo) MediaFile() *File { func (p *Photo) MediaFile() *File {
return &p.File return &p.File
} }
func (p *Photo) InputMedia() InputMedia {
return InputMedia{
Type: p.MediaType(),
Photo: p,
}
}
// UnmarshalJSON is custom unmarshaller required to abstract // UnmarshalJSON is custom unmarshaller required to abstract
// away the hassle of treating different thumbnail sizes. // away the hassle of treating different thumbnail sizes.
// Instead, Telebot chooses the hi-res one and just sticks to it. // Instead, Telebot chooses the hi-res one and just sticks to it.
// //
// I really do find it a beautiful solution. // 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 var hq photoSize
if jsonStr[0] == '{' { if data[0] == '{' {
if err := json.Unmarshal(jsonStr, &hq); err != nil { if err := json.Unmarshal(data, &hq); err != nil {
return err return err
} }
} else { } else {
var sizes []photoSize var sizes []photoSize
if err := json.Unmarshal(data, &sizes); err != nil {
if err := json.Unmarshal(jsonStr, &sizes); err != nil {
return err return err
} }
@ -85,11 +116,22 @@ type Audio struct {
FileName string `json:"file_name,omitempty"` FileName string `json:"file_name,omitempty"`
} }
// MediaFile returns &Audio.File func (a *Audio) MediaType() string {
return "audio"
}
func (a *Audio) MediaFile() *File { func (a *Audio) MediaFile() *File {
a.fileName = a.FileName
return &a.File 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). // 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. // Telegram users can send files of any type of up to 1.5 GB in size.
type Document struct { type Document struct {
@ -102,11 +144,22 @@ type Document struct {
FileName string `json:"file_name,omitempty"` FileName string `json:"file_name,omitempty"`
} }
// MediaFile returns &Document.File func (d *Document) MediaType() string {
return "document"
}
func (d *Document) MediaFile() *File { func (d *Document) MediaFile() *File {
d.fileName = d.FileName
return &d.File return &d.File
} }
func (d *Document) InputMedia() InputMedia {
return InputMedia{
Type: d.MediaType(),
Document: d,
}
}
// Video object represents a video file. // Video object represents a video file.
type Video struct { type Video struct {
File File
@ -123,11 +176,22 @@ type Video struct {
FileName string `json:"file_name,omitempty"` FileName string `json:"file_name,omitempty"`
} }
// MediaFile returns &Video.File func (v *Video) MediaType() string {
return "video"
}
func (v *Video) MediaFile() *File { func (v *Video) MediaFile() *File {
v.fileName = v.FileName
return &v.File return &v.File
} }
func (v *Video) InputMedia() InputMedia {
return InputMedia{
Type: v.MediaType(),
Video: v,
}
}
// Animation object represents a animation file. // Animation object represents a animation file.
type Animation struct { type Animation struct {
File File
@ -143,11 +207,22 @@ type Animation struct {
FileName string `json:"file_name,omitempty"` FileName string `json:"file_name,omitempty"`
} }
// MediaFile returns &Animation.File func (a *Animation) MediaType() string {
return "animation"
}
func (a *Animation) MediaFile() *File { func (a *Animation) MediaFile() *File {
a.fileName = a.FileName
return &a.File return &a.File
} }
func (a *Animation) InputMedia() InputMedia {
return InputMedia{
Type: a.MediaType(),
Animation: a,
}
}
// Voice object represents a voice note. // Voice object represents a voice note.
type Voice struct { type Voice struct {
File File
@ -159,10 +234,18 @@ type Voice struct {
MIME string `json:"mime_type,omitempty"` MIME string `json:"mime_type,omitempty"`
} }
// VideoNote represents a video message (available in Telegram apps func (v *Voice) MediaType() string {
// as of v.4.0). return "voice"
}
func (v *Voice) MediaFile() *File {
return &v.File
}
// VideoNote represents a video message.
type VideoNote struct { type VideoNote struct {
File File
Duration int `json:"duration"` Duration int `json:"duration"`
// (Optional) // (Optional)
@ -170,7 +253,15 @@ type VideoNote struct {
Length int `json:"length,omitempty"` 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 { type Contact struct {
PhoneNumber string `json:"phone_number"` PhoneNumber string `json:"phone_number"`
FirstName string `json:"first_name"` FirstName string `json:"first_name"`
@ -193,14 +284,6 @@ type Location struct {
LivePeriod int `json:"live_period,omitempty"` 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 // Venue object represents a venue location with name, address and
// optional foursquare ID. // optional foursquare ID.
type Venue struct { type Venue struct {

View File

@ -270,6 +270,14 @@ type MessageEntity struct {
// Entities is used to set message's text entities as a send option. // Entities is used to set message's text entities as a send option.
type Entities []MessageEntity 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. // AutoDeleteTimer represents a service message about a change in auto-delete timer settings.
type AutoDeleteTimer struct { type AutoDeleteTimer struct {
Unixtime int `json:"message_auto_delete_time"` Unixtime int `json:"message_auto_delete_time"`

15
util.go
View File

@ -132,24 +132,17 @@ func extractMessage(data []byte) (*Message, error) {
} }
func extractOptions(how []interface{}) *SendOptions { func extractOptions(how []interface{}) *SendOptions {
var opts *SendOptions opts := &SendOptions{}
for _, prop := range how { for _, prop := range how {
switch opt := prop.(type) { switch opt := prop.(type) {
case *SendOptions: case *SendOptions:
opts = opt.copy() opts = opt.copy()
case *ReplyMarkup: case *ReplyMarkup:
if opts == nil {
opts = &SendOptions{}
}
if opt != nil { if opt != nil {
opts.ReplyMarkup = opt.copy() opts.ReplyMarkup = opt.copy()
} }
case Option: case Option:
if opts == nil {
opts = &SendOptions{}
}
switch opt { switch opt {
case NoPreview: case NoPreview:
opts.DisableWebPagePreview = true opts.DisableWebPagePreview = true
@ -174,14 +167,8 @@ func extractOptions(how []interface{}) *SendOptions {
panic("telebot: unsupported flag-option") panic("telebot: unsupported flag-option")
} }
case ParseMode: case ParseMode:
if opts == nil {
opts = &SendOptions{}
}
opts.ParseMode = opt opts.ParseMode = opt
case Entities: case Entities:
if opts == nil {
opts = &SendOptions{}
}
opts.Entities = opt opts.Entities = opt
default: default:
panic("telebot: unsupported send-option") panic("telebot: unsupported send-option")