Merge pull request #469 from go-telegram-bot-api/files Create interface for file data
@@ -3,7 +3,6 @@ // the Telegram Bot API.
package tgbotapi import ( - "bytes" "encoding/json" "errors" "fmt"@@ -12,7 +11,6 @@ "io/ioutil"
"mime/multipart" "net/http" "net/url" - "os" "strings" "time" )@@ -185,54 +183,37 @@ }
} for _, file := range files { - switch f := file.File.(type) { - case string: - fileHandle, err := os.Open(f) + if file.Data.NeedsUpload() { + name, reader, err := file.Data.UploadData() if err != nil { w.CloseWithError(err) return } - defer fileHandle.Close() - part, err := m.CreateFormFile(file.Name, fileHandle.Name()) + part, err := m.CreateFormFile(file.Name, name) if err != nil { w.CloseWithError(err) return } - io.Copy(part, fileHandle) - case FileBytes: - part, err := m.CreateFormFile(file.Name, f.Name) - if err != nil { + if _, err := io.Copy(part, reader); err != nil { w.CloseWithError(err) return } - buf := bytes.NewBuffer(f.Bytes) - io.Copy(part, buf) - case FileReader: - part, err := m.CreateFormFile(file.Name, f.Name) - if err != nil { - w.CloseWithError(err) - return + if closer, ok := reader.(io.ReadCloser); ok { + if err = closer.Close(); err != nil { + w.CloseWithError(err) + return + } } + } else { + value := file.Data.SendData() - io.Copy(part, f.Reader) - case FileURL: - val := string(f) - if err := m.WriteField(file.Name, val); err != nil { + if err := m.WriteField(file.Name, value); err != nil { w.CloseWithError(err) return } - case FileID: - val := string(f) - if err := m.WriteField(file.Name, val); err != nil { - w.CloseWithError(err) - return - } - default: - w.CloseWithError(errors.New(ErrBadFileType)) - return } } }()@@ -321,8 +302,7 @@ }
func hasFilesNeedingUpload(files []RequestFile) bool { for _, file := range files { - switch file.File.(type) { - case string, FileBytes, FileReader: + if file.Data.NeedsUpload() { return true } }@@ -349,20 +329,7 @@
// However, if there are no files to be uploaded, there's likely things // that need to be turned into params instead. for _, file := range files { - var s string - - switch f := file.File.(type) { - case string: - s = f - case FileID: - s = string(f) - case FileURL: - s = string(f) - default: - return nil, errors.New(ErrBadFileType) - } - - params[file.Name] = s + params[file.Name] = file.Data.SendData() } }
@@ -127,7 +127,7 @@
func TestSendWithNewPhoto(t *testing.T) { bot, _ := getBot(t) - msg := NewPhoto(ChatID, "tests/image.jpg") + msg := NewPhoto(ChatID, FilePath("tests/image.jpg")) msg.Caption = "Test" _, err := bot.Send(msg)@@ -169,7 +169,7 @@
func TestSendWithNewPhotoReply(t *testing.T) { bot, _ := getBot(t) - msg := NewPhoto(ChatID, "tests/image.jpg") + msg := NewPhoto(ChatID, FilePath("tests/image.jpg")) msg.ReplyToMessageID = ReplyToMessageID _, err := bot.Send(msg)@@ -182,7 +182,7 @@
func TestSendNewPhotoToChannel(t *testing.T) { bot, _ := getBot(t) - msg := NewPhotoToChannel(Channel, "tests/image.jpg") + msg := NewPhotoToChannel(Channel, FilePath("tests/image.jpg")) msg.Caption = "Test" _, err := bot.Send(msg)@@ -239,7 +239,7 @@
func TestSendWithNewDocument(t *testing.T) { bot, _ := getBot(t) - msg := NewDocument(ChatID, "tests/image.jpg") + msg := NewDocument(ChatID, FilePath("tests/image.jpg")) _, err := bot.Send(msg) if err != nil {@@ -250,8 +250,8 @@
func TestSendWithNewDocumentAndThumb(t *testing.T) { bot, _ := getBot(t) - msg := NewDocument(ChatID, "tests/voice.ogg") - msg.Thumb = "tests/image.jpg" + msg := NewDocument(ChatID, FilePath("tests/voice.ogg")) + msg.Thumb = FilePath("tests/image.jpg") _, err := bot.Send(msg) if err != nil {@@ -273,7 +273,7 @@
func TestSendWithNewAudio(t *testing.T) { bot, _ := getBot(t) - msg := NewAudio(ChatID, "tests/audio.mp3") + msg := NewAudio(ChatID, FilePath("tests/audio.mp3")) msg.Title = "TEST" msg.Duration = 10 msg.Performer = "TEST"@@ -302,7 +302,7 @@
func TestSendWithNewVoice(t *testing.T) { bot, _ := getBot(t) - msg := NewVoice(ChatID, "tests/voice.ogg") + msg := NewVoice(ChatID, FilePath("tests/voice.ogg")) msg.Duration = 10 _, err := bot.Send(msg)@@ -356,7 +356,7 @@
func TestSendWithNewVideo(t *testing.T) { bot, _ := getBot(t) - msg := NewVideo(ChatID, "tests/video.mp4") + msg := NewVideo(ChatID, FilePath("tests/video.mp4")) msg.Duration = 10 msg.Caption = "TEST"@@ -384,7 +384,7 @@
func TestSendWithNewVideoNote(t *testing.T) { bot, _ := getBot(t) - msg := NewVideoNote(ChatID, 240, "tests/videonote.mp4") + msg := NewVideoNote(ChatID, 240, FilePath("tests/videonote.mp4")) msg.Duration = 10 _, err := bot.Send(msg)@@ -410,7 +410,7 @@
func TestSendWithNewSticker(t *testing.T) { bot, _ := getBot(t) - msg := NewSticker(ChatID, "tests/image.jpg") + msg := NewSticker(ChatID, FilePath("tests/image.jpg")) _, err := bot.Send(msg)@@ -434,7 +434,7 @@
func TestSendWithNewStickerAndKeyboardHide(t *testing.T) { bot, _ := getBot(t) - msg := NewSticker(ChatID, "tests/image.jpg") + msg := NewSticker(ChatID, FilePath("tests/image.jpg")) msg.ReplyMarkup = ReplyKeyboardRemove{ RemoveKeyboard: true, Selective: false,@@ -550,7 +550,7 @@ time.Sleep(time.Second * 2)
bot.Request(DeleteWebhookConfig{}) - wh, err := NewWebhookWithCert("https://example.com/tgbotapi-test/"+bot.Token, "tests/cert.pem") + wh, err := NewWebhookWithCert("https://example.com/tgbotapi-test/"+bot.Token, FilePath("tests/cert.pem")) if err != nil { t.Error(err)@@ -609,8 +609,8 @@ bot, _ := getBot(t)
cfg := NewMediaGroup(ChatID, []interface{}{ NewInputMediaPhoto(FileURL("https://github.com/go-telegram-bot-api/telegram-bot-api/raw/0a3a1c8716c4cd8d26a262af9f12dcbab7f3f28c/tests/image.jpg")), - NewInputMediaPhoto("tests/image.jpg"), - NewInputMediaVideo("tests/video.mp4"), + NewInputMediaPhoto(FilePath("tests/image.jpg")), + NewInputMediaVideo(FilePath("tests/video.mp4")), }) messages, err := bot.SendMediaGroup(cfg)@@ -632,7 +632,7 @@ bot, _ := getBot(t)
cfg := NewMediaGroup(ChatID, []interface{}{ NewInputMediaDocument(FileURL("https://i.imgur.com/unQLJIb.jpg")), - NewInputMediaDocument("tests/image.jpg"), + NewInputMediaDocument(FilePath("tests/image.jpg")), }) messages, err := bot.SendMediaGroup(cfg)@@ -653,8 +653,8 @@ func TestSendWithMediaGroupAudio(t *testing.T) {
bot, _ := getBot(t) cfg := NewMediaGroup(ChatID, []interface{}{ - NewInputMediaAudio("tests/audio.mp3"), - NewInputMediaAudio("tests/audio.mp3"), + NewInputMediaAudio(FilePath("tests/audio.mp3")), + NewInputMediaAudio(FilePath("tests/audio.mp3")), }) messages, err := bot.SendMediaGroup(cfg)@@ -715,7 +715,7 @@ bot.Debug = true
log.Printf("Authorized on account %s", bot.Self.UserName) - wh, err := NewWebhookWithCert("https://www.google.com:8443/"+bot.Token, "cert.pem") + wh, err := NewWebhookWithCert("https://www.google.com:8443/"+bot.Token, FilePath("cert.pem")) if err != nil { panic(err)@@ -755,7 +755,7 @@ bot.Debug = true
log.Printf("Authorized on account %s", bot.Self.UserName) - wh, err := NewWebhookWithCert("https://www.google.com:8443/"+bot.Token, "cert.pem") + wh, err := NewWebhookWithCert("https://www.google.com:8443/"+bot.Token, FilePath("cert.pem")) if err != nil { panic(err)@@ -1019,7 +1019,7 @@ // BaseEdit: BaseEdit{
// ChatID: ChatID, // MessageID: m.MessageID, // }, -// Media: NewInputMediaVideo("tests/video.mp4"), +// Media: NewInputMediaVideo(FilePath("tests/video.mp4")), // } // _, err = bot.Request(edit)@@ -1030,17 +1030,17 @@ // }
func TestPrepareInputMediaForParams(t *testing.T) { media := []interface{}{ - NewInputMediaPhoto("tests/image.jpg"), + NewInputMediaPhoto(FilePath("tests/image.jpg")), NewInputMediaVideo(FileID("test")), } prepared := prepareInputMediaForParams(media) - if media[0].(InputMediaPhoto).Media != "tests/image.jpg" { + if media[0].(InputMediaPhoto).Media != FilePath("tests/image.jpg") { t.Error("Original media was changed") } - if prepared[0].(InputMediaPhoto).Media != "attach://file-0" { + if prepared[0].(InputMediaPhoto).Media != fileAttach("attach://file-0") { t.Error("New media was not replaced") }
@@ -1,9 +1,11 @@
package tgbotapi import ( + "bytes" "fmt" "io" "net/url" + "os" "strconv" )@@ -98,9 +100,7 @@ )
// Library errors const ( - // ErrBadFileType happens when you pass an unknown type - ErrBadFileType = "bad file type" - ErrBadURL = "bad or empty url" + ErrBadURL = "bad or empty url" ) // Chattable is any config type that can be sent.@@ -109,19 +109,134 @@ params() (Params, error)
method() string } -// RequestFile represents a file associated with a request. May involve -// uploading a file, or passing an existing ID. +// Fileable is any config type that can be sent that includes a file. +type Fileable interface { + Chattable + files() []RequestFile +} + +// RequestFile represents a file associated with a field name. type RequestFile struct { - // The multipart upload field name. + // The file field name. Name string - // The file to upload. - File interface{} + // The file data to include. + Data RequestFileData +} + +// RequestFileData represents the data to be used for a file. +type RequestFileData interface { + // If the file needs to be uploaded. + NeedsUpload() bool + + // Get the file name and an `io.Reader` for the file to be uploaded. This + // must only be called when the file needs to be uploaded. + UploadData() (string, io.Reader, error) + // Get the file data to send when a file does not need to be uploaded. This + // must only be called when the file does not need to be uploaded. + SendData() string +} + +// FileBytes contains information about a set of bytes to upload +// as a File. +type FileBytes struct { + Name string + Bytes []byte +} + +func (fb FileBytes) NeedsUpload() bool { + return true +} + +func (fb FileBytes) UploadData() (string, io.Reader, error) { + return fb.Name, bytes.NewReader(fb.Bytes), nil +} + +func (fb FileBytes) SendData() string { + panic("FileBytes must be uploaded") +} + +// FileReader contains information about a reader to upload as a File. +type FileReader struct { + Name string + Reader io.Reader +} + +func (fr FileReader) NeedsUpload() bool { + return true +} + +func (fr FileReader) UploadData() (string, io.Reader, error) { + return fr.Name, fr.Reader, nil +} + +func (fr FileReader) SendData() string { + panic("FileReader must be uploaded") +} + +// FilePath is a path to a local file. +type FilePath string + +func (fp FilePath) NeedsUpload() bool { + return true +} + +func (fp FilePath) UploadData() (string, io.Reader, error) { + fileHandle, err := os.Open(string(fp)) + if err != nil { + return "", nil, err + } + + name := fileHandle.Name() + return name, fileHandle, err } -// Fileable is any config type that can be sent that includes a file. -type Fileable interface { - Chattable - files() []RequestFile +func (fp FilePath) SendData() string { + panic("FilePath must be uploaded") +} + +// FileURL is a URL to use as a file for a request. +type FileURL string + +func (fu FileURL) NeedsUpload() bool { + return false +} + +func (fu FileURL) UploadData() (string, io.Reader, error) { + panic("FileURL cannot be uploaded") +} + +func (fu FileURL) SendData() string { + return string(fu) +} + +// FileID is an ID of a file already uploaded to Telegram. +type FileID string + +func (fi FileID) NeedsUpload() bool { + return false +} + +func (fi FileID) UploadData() (string, io.Reader, error) { + panic("FileID cannot be uploaded") +} + +func (fi FileID) SendData() string { + return string(fi) +} + +// fileAttach is a internal file type used for processed media groups. +type fileAttach string + +func (fa fileAttach) NeedsUpload() bool { + return false +} + +func (fa fileAttach) UploadData() (string, io.Reader, error) { + panic("fileAttach cannot be uploaded") +} + +func (fa fileAttach) SendData() string { + return string(fa) } // LogOutConfig is a request to log out of the cloud Bot API server.@@ -177,7 +292,7 @@
// BaseFile is a base type for all file config types. type BaseFile struct { BaseChat - File interface{} + File RequestFileData } func (file BaseFile) params() (Params, error) {@@ -292,7 +407,7 @@
// PhotoConfig contains information about a SendPhoto request. type PhotoConfig struct { BaseFile - Thumb interface{} + Thumb RequestFileData Caption string ParseMode string CaptionEntities []MessageEntity@@ -318,13 +433,13 @@
func (config PhotoConfig) files() []RequestFile { files := []RequestFile{{ Name: "photo", - File: config.File, + Data: config.File, }} if config.Thumb != nil { files = append(files, RequestFile{ Name: "thumb", - File: config.Thumb, + Data: config.Thumb, }) }@@ -334,7 +449,7 @@
// AudioConfig contains information about a SendAudio request. type AudioConfig struct { BaseFile - Thumb interface{} + Thumb RequestFileData Caption string ParseMode string CaptionEntities []MessageEntity@@ -366,13 +481,13 @@
func (config AudioConfig) files() []RequestFile { files := []RequestFile{{ Name: "audio", - File: config.File, + Data: config.File, }} if config.Thumb != nil { files = append(files, RequestFile{ Name: "thumb", - File: config.Thumb, + Data: config.Thumb, }) }@@ -382,7 +497,7 @@
// DocumentConfig contains information about a SendDocument request. type DocumentConfig struct { BaseFile - Thumb interface{} + Thumb RequestFileData Caption string ParseMode string CaptionEntities []MessageEntity@@ -406,13 +521,13 @@
func (config DocumentConfig) files() []RequestFile { files := []RequestFile{{ Name: "document", - File: config.File, + Data: config.File, }} if config.Thumb != nil { files = append(files, RequestFile{ Name: "thumb", - File: config.Thumb, + Data: config.Thumb, }) }@@ -435,14 +550,14 @@
func (config StickerConfig) files() []RequestFile { return []RequestFile{{ Name: "sticker", - File: config.File, + Data: config.File, }} } // VideoConfig contains information about a SendVideo request. type VideoConfig struct { BaseFile - Thumb interface{} + Thumb RequestFileData Duration int Caption string ParseMode string@@ -472,13 +587,13 @@
func (config VideoConfig) files() []RequestFile { files := []RequestFile{{ Name: "video", - File: config.File, + Data: config.File, }} if config.Thumb != nil { files = append(files, RequestFile{ Name: "thumb", - File: config.Thumb, + Data: config.Thumb, }) }@@ -489,7 +604,7 @@ // AnimationConfig contains information about a SendAnimation request.
type AnimationConfig struct { BaseFile Duration int - Thumb interface{} + Thumb RequestFileData Caption string ParseMode string CaptionEntities []MessageEntity@@ -516,13 +631,13 @@
func (config AnimationConfig) files() []RequestFile { files := []RequestFile{{ Name: "animation", - File: config.File, + Data: config.File, }} if config.Thumb != nil { files = append(files, RequestFile{ Name: "thumb", - File: config.Thumb, + Data: config.Thumb, }) }@@ -532,7 +647,7 @@
// VideoNoteConfig contains information about a SendVideoNote request. type VideoNoteConfig struct { BaseFile - Thumb interface{} + Thumb RequestFileData Duration int Length int }@@ -553,13 +668,13 @@
func (config VideoNoteConfig) files() []RequestFile { files := []RequestFile{{ Name: "video_note", - File: config.File, + Data: config.File, }} if config.Thumb != nil { files = append(files, RequestFile{ Name: "thumb", - File: config.Thumb, + Data: config.Thumb, }) }@@ -569,7 +684,7 @@
// VoiceConfig contains information about a SendVoice request. type VoiceConfig struct { BaseFile - Thumb interface{} + Thumb RequestFileData Caption string ParseMode string CaptionEntities []MessageEntity@@ -597,13 +712,13 @@
func (config VoiceConfig) files() []RequestFile { files := []RequestFile{{ Name: "voice", - File: config.File, + Data: config.File, }} if config.Thumb != nil { files = append(files, RequestFile{ Name: "thumb", - File: config.Thumb, + Data: config.Thumb, }) }@@ -1046,7 +1161,7 @@
// WebhookConfig contains information about a SetWebhook request. type WebhookConfig struct { URL *url.URL - Certificate interface{} + Certificate RequestFileData IPAddress string MaxConnections int AllowedUpdates []string@@ -1076,7 +1191,7 @@ func (config WebhookConfig) files() []RequestFile {
if config.Certificate != nil { return []RequestFile{{ Name: "certificate", - File: config.Certificate, + Data: config.Certificate, }} }@@ -1100,25 +1215,6 @@
return params, nil } -// FileBytes contains information about a set of bytes to upload -// as a File. -type FileBytes struct { - Name string - Bytes []byte -} - -// FileReader contains information about a reader to upload as a File. -type FileReader struct { - Name string - Reader io.Reader -} - -// FileURL is a URL to use as a file for a request. -type FileURL string - -// FileID is an ID of a file already uploaded to Telegram. -type FileID string - // InlineConfig contains information on making an InlineQuery response. type InlineConfig struct { InlineQueryID string `json:"inline_query_id"`@@ -1753,7 +1849,7 @@
func (config SetChatPhotoConfig) files() []RequestFile { return []RequestFile{{ Name: "photo", - File: config.File, + Data: config.File, }} }@@ -1837,7 +1933,7 @@
// UploadStickerConfig allows you to upload a sticker for use in a set later. type UploadStickerConfig struct { UserID int64 - PNGSticker interface{} + PNGSticker RequestFileData } func (config UploadStickerConfig) method() string {@@ -1855,7 +1951,7 @@
func (config UploadStickerConfig) files() []RequestFile { return []RequestFile{{ Name: "png_sticker", - File: config.PNGSticker, + Data: config.PNGSticker, }} }@@ -1866,8 +1962,8 @@ type NewStickerSetConfig struct {
UserID int64 Name string Title string - PNGSticker interface{} - TGSSticker interface{} + PNGSticker RequestFileData + TGSSticker RequestFileData Emojis string ContainsMasks bool MaskPosition *MaskPosition@@ -1897,13 +1993,13 @@ func (config NewStickerSetConfig) files() []RequestFile {
if config.PNGSticker != nil { return []RequestFile{{ Name: "png_sticker", - File: config.PNGSticker, + Data: config.PNGSticker, }} } return []RequestFile{{ Name: "tgs_sticker", - File: config.TGSSticker, + Data: config.TGSSticker, }} }@@ -1911,8 +2007,8 @@ // AddStickerConfig allows you to add a sticker to a set.
type AddStickerConfig struct { UserID int64 Name string - PNGSticker interface{} - TGSSticker interface{} + PNGSticker RequestFileData + TGSSticker RequestFileData Emojis string MaskPosition *MaskPosition }@@ -1937,13 +2033,13 @@ func (config AddStickerConfig) files() []RequestFile {
if config.PNGSticker != nil { return []RequestFile{{ Name: "png_sticker", - File: config.PNGSticker, + Data: config.PNGSticker, }} } return []RequestFile{{ Name: "tgs_sticker", - File: config.TGSSticker, + Data: config.TGSSticker, }} }@@ -1988,7 +2084,7 @@ // SetStickerSetThumbConfig allows you to set the thumbnail for a sticker set.
type SetStickerSetThumbConfig struct { Name string UserID int64 - Thumb interface{} + Thumb RequestFileData } func (config SetStickerSetThumbConfig) method() string {@@ -2007,7 +2103,7 @@
func (config SetStickerSetThumbConfig) files() []RequestFile { return []RequestFile{{ Name: "thumb", - File: config.Thumb, + Data: config.Thumb, }} }@@ -2181,45 +2277,38 @@ // It is expected to be used in conjunction with prepareInputMediaFile.
func prepareInputMediaParam(inputMedia interface{}, idx int) interface{} { switch m := inputMedia.(type) { case InputMediaPhoto: - switch m.Media.(type) { - case string, FileBytes, FileReader: - m.Media = fmt.Sprintf("attach://file-%d", idx) + if m.Media.NeedsUpload() { + m.Media = fileAttach(fmt.Sprintf("attach://file-%d", idx)) } return m case InputMediaVideo: - switch m.Media.(type) { - case string, FileBytes, FileReader: - m.Media = fmt.Sprintf("attach://file-%d", idx) + if m.Media.NeedsUpload() { + m.Media = fileAttach(fmt.Sprintf("attach://file-%d", idx)) } - switch m.Thumb.(type) { - case string, FileBytes, FileReader: - m.Thumb = fmt.Sprintf("attach://file-%d-thumb", idx) + if m.Thumb != nil && m.Thumb.NeedsUpload() { + m.Thumb = fileAttach(fmt.Sprintf("attach://file-%d-thumb", idx)) } return m case InputMediaAudio: - switch m.Media.(type) { - case string, FileBytes, FileReader: - m.Media = fmt.Sprintf("attach://file-%d", idx) + if m.Media.NeedsUpload() { + m.Media = fileAttach(fmt.Sprintf("attach://file-%d", idx)) } - switch m.Thumb.(type) { - case string, FileBytes, FileReader: - m.Thumb = fmt.Sprintf("attach://file-%d-thumb", idx) + if m.Thumb != nil && m.Thumb.NeedsUpload() { + m.Thumb = fileAttach(fmt.Sprintf("attach://file-%d-thumb", idx)) } return m case InputMediaDocument: - switch m.Media.(type) { - case string, FileBytes, FileReader: - m.Media = fmt.Sprintf("attach://file-%d", idx) + if m.Media.NeedsUpload() { + m.Media = fileAttach(fmt.Sprintf("attach://file-%d", idx)) } - switch m.Thumb.(type) { - case string, FileBytes, FileReader: - m.Thumb = fmt.Sprintf("attach://file-%d-thumb", idx) + if m.Thumb != nil && m.Thumb.NeedsUpload() { + m.Thumb = fileAttach(fmt.Sprintf("attach://file-%d-thumb", idx)) } return m@@ -2241,59 +2330,52 @@ files := []RequestFile{}
switch m := inputMedia.(type) { case InputMediaPhoto: - switch f := m.Media.(type) { - case string, FileBytes, FileReader: + if m.Media.NeedsUpload() { files = append(files, RequestFile{ Name: fmt.Sprintf("file-%d", idx), - File: f, + Data: m.Media, }) } case InputMediaVideo: - switch f := m.Media.(type) { - case string, FileBytes, FileReader: + if m.Media.NeedsUpload() { files = append(files, RequestFile{ Name: fmt.Sprintf("file-%d", idx), - File: f, + Data: m.Media, }) } - switch f := m.Thumb.(type) { - case string, FileBytes, FileReader: + if m.Thumb != nil && m.Thumb.NeedsUpload() { files = append(files, RequestFile{ - Name: fmt.Sprintf("file-%d-thumb", idx), - File: f, + Name: fmt.Sprintf("file-%d", idx), + Data: m.Thumb, }) } case InputMediaDocument: - switch f := m.Media.(type) { - case string, FileBytes, FileReader: + if m.Media.NeedsUpload() { files = append(files, RequestFile{ Name: fmt.Sprintf("file-%d", idx), - File: f, + Data: m.Media, }) } - switch f := m.Thumb.(type) { - case string, FileBytes, FileReader: + if m.Thumb != nil && m.Thumb.NeedsUpload() { files = append(files, RequestFile{ Name: fmt.Sprintf("file-%d", idx), - File: f, + Data: m.Thumb, }) } case InputMediaAudio: - switch f := m.Media.(type) { - case string, FileBytes, FileReader: + if m.Media.NeedsUpload() { files = append(files, RequestFile{ Name: fmt.Sprintf("file-%d", idx), - File: f, + Data: m.Media, }) } - switch f := m.Thumb.(type) { - case string, FileBytes, FileReader: + if m.Thumb != nil && m.Thumb.NeedsUpload() { files = append(files, RequestFile{ Name: fmt.Sprintf("file-%d", idx), - File: f, + Data: m.Thumb, }) } }
@@ -3,20 +3,22 @@
Telegram supports specifying files in many different formats. In order to accommodate them all, there are multiple structs and type aliases required. +All of these types implement the `RequestFileData` interface. + | Type | Description | | ------------ | ------------------------------------------------------------------------- | -| `string` | Used as a local path to a file | +| `FilePath` | A local path to a file | | `FileID` | Existing file ID on Telegram's servers | | `FileURL` | URL to file, must be served with expected MIME type | | `FileReader` | Use an `io.Reader` to provide a file. Lazily read to save memory. | | `FileBytes` | `[]byte` containing file data. Prefer to use `FileReader` to save memory. | -## `string` +## `FilePath` A path to a local file. ```go -file := "tests/image.jpg" +file := tgbotapi.FilePath("tests/image.jpg") ``` ## `FileID`
@@ -19,7 +19,8 @@ MessageID int
} ``` -What type should `ChatID` be? Telegram allows specifying numeric chat IDs or channel usernames. Golang doesn't have union types, and interfaces are entirely +What type should `ChatID` be? Telegram allows specifying numeric chat IDs or +channel usernames. Golang doesn't have union types, and interfaces are entirely untyped. This library solves this by adding two fields, a `ChatID` and a `ChannelUsername`. We can now write the struct as follows.@@ -103,8 +104,8 @@ type DeleteMessageConfig struct {
ChannelUsername string ChatID int64 MessageID int -+ Delete interface{} -+ Thumb interface{} ++ Delete RequestFileData ++ Thumb RequestFileData } ```@@ -115,13 +116,13 @@ ```go
func (config DeleteMessageConfig) files() []RequestFile { files := []RequestFile{{ Name: "delete", - File: config.Delete, + Data: config.Delete, }} if config.Thumb != nil { files = append(files, RequestFile{ Name: "thumb", - File: config.Thumb, + Data: config.Thumb, }) }@@ -129,7 +130,8 @@ return files
} ``` -And now our files will upload! It will transparently handle uploads whether File is a string with a path to a file, `FileURL`, `FileBytes`, `FileReader`, or `FileID`. +And now our files will upload! It will transparently handle uploads whether File +is a `FilePath`, `FileURL`, `FileBytes`, `FileReader`, or `FileID`. ### Base Configs
@@ -28,14 +28,14 @@ // We can have multiple files, so we'll create an array. We also know that
// there always is a document file, so initialize the array with that. files := []RequestFile{{ Name: "document", - File: config.File, + Data: config.File, }} // We'll only add a file if we have one. if config.Thumb != nil { files = append(files, RequestFile{ Name: "thumb", - File: config.Thumb, + Data: config.Thumb, }) }@@ -58,7 +58,7 @@
First, we start by creating some `InputMediaPhoto`. ```go -photo := tgbotapi.NewInputMediaPhoto("tests/image.jpg") +photo := tgbotapi.NewInputMediaPhoto(tgbotapi.FilePath("tests/image.jpg")) url := tgbotapi.NewInputMediaPhoto(tgbotapi.FileURL("https://i.imgur.com/unQLJIb.jpg")) ```@@ -85,24 +85,3 @@ are all changed into `attach://file-%d`. When collecting a list of files to
upload, it names them the same way. This creates a nearly transparent way of handling multiple files in the background without the user having to consider what's going on. - -## Library Processing - -If at some point in the future new upload types are required, let's talk about -where the current types are used. - -Upload types are defined in `configs.go`. Where possible, type aliases are -preferred. Structs can be used when multiple fields are required. - -The main usage of the upload types happens in `UploadFiles`. It switches on each -file's type in order to determine how to upload it. Files that aren't uploaded -(file IDs, URLs) are converted back into strings and passed through as strings -into the correct field. Uploaded types are processed as needed (opening files, -etc.) and written into the form using a copy approach in a goroutine to reduce -memory usage. - -In addition to `UploadFiles`, there's more processing of upload types in the -`prepareInputMediaParam` and `prepareInputMediaFile` functions. These look at -the `InputMedia` types to determine which files are uploaded and which are -passed through as strings. They only need to be aware of which files need to be -replaced with `attach://` fields.
@@ -70,7 +70,7 @@ // chatID is where to send it, file is a string path to the file,
// FileReader, or FileBytes. // // Note that you must send animated GIFs as a document. -func NewPhoto(chatID int64, file interface{}) PhotoConfig { +func NewPhoto(chatID int64, file RequestFileData) PhotoConfig { return PhotoConfig{ BaseFile: BaseFile{ BaseChat: BaseChat{ChatID: chatID},@@ -82,7 +82,7 @@
// NewPhotoToChannel creates a new photo uploader to send a photo to a channel. // // Note that you must send animated GIFs as a document. -func NewPhotoToChannel(username string, file interface{}) PhotoConfig { +func NewPhotoToChannel(username string, file RequestFileData) PhotoConfig { return PhotoConfig{ BaseFile: BaseFile{ BaseChat: BaseChat{@@ -94,7 +94,7 @@ }
} // NewAudio creates a new sendAudio request. -func NewAudio(chatID int64, file interface{}) AudioConfig { +func NewAudio(chatID int64, file RequestFileData) AudioConfig { return AudioConfig{ BaseFile: BaseFile{ BaseChat: BaseChat{ChatID: chatID},@@ -104,7 +104,7 @@ }
} // NewDocument creates a new sendDocument request. -func NewDocument(chatID int64, file interface{}) DocumentConfig { +func NewDocument(chatID int64, file RequestFileData) DocumentConfig { return DocumentConfig{ BaseFile: BaseFile{ BaseChat: BaseChat{ChatID: chatID},@@ -114,7 +114,7 @@ }
} // NewSticker creates a new sendSticker request. -func NewSticker(chatID int64, file interface{}) StickerConfig { +func NewSticker(chatID int64, file RequestFileData) StickerConfig { return StickerConfig{ BaseFile: BaseFile{ BaseChat: BaseChat{ChatID: chatID},@@ -124,7 +124,7 @@ }
} // NewVideo creates a new sendVideo request. -func NewVideo(chatID int64, file interface{}) VideoConfig { +func NewVideo(chatID int64, file RequestFileData) VideoConfig { return VideoConfig{ BaseFile: BaseFile{ BaseChat: BaseChat{ChatID: chatID},@@ -134,7 +134,7 @@ }
} // NewAnimation creates a new sendAnimation request. -func NewAnimation(chatID int64, file interface{}) AnimationConfig { +func NewAnimation(chatID int64, file RequestFileData) AnimationConfig { return AnimationConfig{ BaseFile: BaseFile{ BaseChat: BaseChat{ChatID: chatID},@@ -147,7 +147,7 @@ // NewVideoNote creates a new sendVideoNote request.
// // chatID is where to send it, file is a string path to the file, // FileReader, or FileBytes. -func NewVideoNote(chatID int64, length int, file interface{}) VideoNoteConfig { +func NewVideoNote(chatID int64, length int, file RequestFileData) VideoNoteConfig { return VideoNoteConfig{ BaseFile: BaseFile{ BaseChat: BaseChat{ChatID: chatID},@@ -158,7 +158,7 @@ }
} // NewVoice creates a new sendVoice request. -func NewVoice(chatID int64, file interface{}) VoiceConfig { +func NewVoice(chatID int64, file RequestFileData) VoiceConfig { return VoiceConfig{ BaseFile: BaseFile{ BaseChat: BaseChat{ChatID: chatID},@@ -177,7 +177,7 @@ }
} // NewInputMediaPhoto creates a new InputMediaPhoto. -func NewInputMediaPhoto(media interface{}) InputMediaPhoto { +func NewInputMediaPhoto(media RequestFileData) InputMediaPhoto { return InputMediaPhoto{ BaseInputMedia{ Type: "photo",@@ -187,7 +187,7 @@ }
} // NewInputMediaVideo creates a new InputMediaVideo. -func NewInputMediaVideo(media interface{}) InputMediaVideo { +func NewInputMediaVideo(media RequestFileData) InputMediaVideo { return InputMediaVideo{ BaseInputMedia: BaseInputMedia{ Type: "video",@@ -197,7 +197,7 @@ }
} // NewInputMediaAnimation creates a new InputMediaAnimation. -func NewInputMediaAnimation(media interface{}) InputMediaAnimation { +func NewInputMediaAnimation(media RequestFileData) InputMediaAnimation { return InputMediaAnimation{ BaseInputMedia: BaseInputMedia{ Type: "animation",@@ -207,7 +207,7 @@ }
} // NewInputMediaAudio creates a new InputMediaAudio. -func NewInputMediaAudio(media interface{}) InputMediaAudio { +func NewInputMediaAudio(media RequestFileData) InputMediaAudio { return InputMediaAudio{ BaseInputMedia: BaseInputMedia{ Type: "audio",@@ -217,7 +217,7 @@ }
} // NewInputMediaDocument creates a new InputMediaDocument. -func NewInputMediaDocument(media interface{}) InputMediaDocument { +func NewInputMediaDocument(media RequestFileData) InputMediaDocument { return InputMediaDocument{ BaseInputMedia: BaseInputMedia{ Type: "document",@@ -316,7 +316,7 @@ // NewWebhookWithCert creates a new webhook with a certificate.
// // link is the url you wish to get webhooks, // file contains a string to a file, FileReader, or FileBytes. -func NewWebhookWithCert(link string, file interface{}) (WebhookConfig, error) { +func NewWebhookWithCert(link string, file RequestFileData) (WebhookConfig, error) { u, err := url.Parse(link) if err != nil {@@ -769,7 +769,7 @@ }
} // NewChatPhoto allows you to update the photo for a chat. -func NewChatPhoto(chatID int64, photo interface{}) SetChatPhotoConfig { +func NewChatPhoto(chatID int64, photo RequestFileData) SetChatPhotoConfig { return SetChatPhotoConfig{ BaseFile: BaseFile{ BaseChat: BaseChat{@@ -781,7 +781,7 @@ }
} // NewDeleteChatPhoto allows you to delete the photo for a chat. -func NewDeleteChatPhoto(chatID int64, photo interface{}) DeleteChatPhotoConfig { +func NewDeleteChatPhoto(chatID int64) DeleteChatPhotoConfig { return DeleteChatPhotoConfig{ ChatID: chatID, }
@@ -17,7 +17,7 @@ }
} func TestNewWebhookWithCert(t *testing.T) { - exampleFile := File{FileID: "123"} + exampleFile := FileID("123") result, err := NewWebhookWithCert("https://example.com/token", exampleFile) if err != nil ||
@@ -1723,7 +1723,7 @@ // that exists on the Telegram servers (recommended),
// pass an HTTP URL for Telegram to get a file from the Internet, // or pass “attach://<file_attach_name>” to upload a new one // using multipart/form-data under <file_attach_name> name. - Media interface{} `json:"media"` + Media RequestFileData `json:"media"` // thumb intentionally missing as it is not currently compatible // Caption of the video to be sent, 0-1024 characters after entities parsing.@@ -1755,7 +1755,7 @@ // Thumbnail of the file sent; can be ignored if thumbnail generation for
// the file is supported server-side. // // optional - Thumb interface{} `json:"thumb,omitempty"` + Thumb RequestFileData `json:"thumb,omitempty"` // Width video width // // optional@@ -1781,7 +1781,7 @@ // Thumbnail of the file sent; can be ignored if thumbnail generation for
// the file is supported server-side. // // optional - Thumb interface{} `json:"thumb,omitempty"` + Thumb RequestFileData `json:"thumb,omitempty"` // Width video width // // optional@@ -1803,7 +1803,7 @@ // Thumbnail of the file sent; can be ignored if thumbnail generation for
// the file is supported server-side. // // optional - Thumb interface{} `json:"thumb,omitempty"` + Thumb RequestFileData `json:"thumb,omitempty"` // Duration of the audio in seconds // // optional@@ -1825,7 +1825,7 @@ // Thumbnail of the file sent; can be ignored if thumbnail generation for
// the file is supported server-side. // // optional - Thumb interface{} `json:"thumb,omitempty"` + Thumb RequestFileData `json:"thumb,omitempty"` // DisableContentTypeDetection disables automatic server-side content type // detection for files uploaded using multipart/form-data. Always true, if // the document is sent as part of an album
@@ -361,3 +361,13 @@ _ Fileable = (*MediaGroupConfig)(nil)
_ Fileable = (*WebhookConfig)(nil) _ Fileable = (*SetStickerSetThumbConfig)(nil) ) + +// Ensure all RequestFileData types are correct. +var ( + _ RequestFileData = (*FilePath)(nil) + _ RequestFileData = (*FileBytes)(nil) + _ RequestFileData = (*FileReader)(nil) + _ RequestFileData = (*FileURL)(nil) + _ RequestFileData = (*FileID)(nil) + _ RequestFileData = (*fileAttach)(nil) +)