telebot: rework media, implement Media and Inputtable interfaces

pull/477/head
Demian 3 years ago
parent 435b1e3b63
commit 0463e22543

@ -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)

159
bot.go

@ -276,8 +276,8 @@ func (b *Bot) ProcessUpdate(upd Update) {
match := cmdRx.FindAllStringSubmatch(m.Text, -1)
if match != nil {
// Syntax: "</command>@<bot> <payload>"
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

@ -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))
})

@ -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.

@ -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 {

@ -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"`

@ -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")

Loading…
Cancel
Save