Add support for uploading multiple files.
@@ -9,13 +9,12 @@ "errors"
"fmt" "io" "io/ioutil" + "mime/multipart" "net/http" "net/url" "os" "strings" "time" - - "github.com/technoweenie/multipartstreamer" ) // BotAPI allows you to interact with the Telegram Bot API.@@ -82,7 +81,7 @@ return
} // MakeRequest makes a request to a specific endpoint with our token. -func (bot *BotAPI) MakeRequest(endpoint string, params Params) (APIResponse, error) { +func (bot *BotAPI) MakeRequest(endpoint string, params Params) (*APIResponse, error) { if bot.Debug { log.Printf("Endpoint: %s, params: %v\n", endpoint, params) }@@ -93,14 +92,14 @@ values := buildParams(params)
resp, err := bot.Client.PostForm(method, values) if err != nil { - return APIResponse{}, err + return nil, err } defer resp.Body.Close() var apiResp APIResponse bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp) if err != nil { - return apiResp, err + return &apiResp, err } if bot.Debug {@@ -114,14 +113,14 @@ if apiResp.Parameters != nil {
parameters = *apiResp.Parameters } - return apiResp, Error{ + return &apiResp, &Error{ Code: apiResp.ErrorCode, Message: apiResp.Description, ResponseParameters: parameters, } } - return apiResp, nil + return &apiResp, nil } // decodeAPIResponse decode response and return slice of bytes if debug enabled.@@ -148,86 +147,102 @@
return data, nil } -// UploadFile makes a request to the API with a file. -// -// Requires the parameter to hold the file not be in the params. -// File should be a string to a file path, a FileBytes struct, -// a FileReader struct, or a url.URL. -// -// Note that if your FileReader has a size set to -1, it will read -// the file into memory to calculate a size. -func (bot *BotAPI) UploadFile(endpoint string, params Params, fieldname string, file interface{}) (APIResponse, error) { - ms := multipartstreamer.New() +// UploadFiles makes a request to the API with files. +func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFile) (*APIResponse, error) { + r, w := io.Pipe() + m := multipart.NewWriter(w) - switch f := file.(type) { - case string: - ms.WriteFields(params) + // This code modified from the very helpful @HirbodBehnam + // https://github.com/go-telegram-bot-api/telegram-bot-api/issues/354#issuecomment-663856473 + go func() { + defer w.Close() + defer m.Close() - fileHandle, err := os.Open(f) - if err != nil { - return APIResponse{}, err + for field, value := range params { + if err := m.WriteField(field, value); err != nil { + panic(err) + } } - defer fileHandle.Close() - fi, err := os.Stat(f) - if err != nil { - return APIResponse{}, err - } + for _, file := range files { + switch f := file.File.(type) { + case string: + fileHandle, err := os.Open(f) + if err != nil { + panic(err) + } + defer fileHandle.Close() - ms.WriteReader(fieldname, fileHandle.Name(), fi.Size(), fileHandle) - case FileBytes: - ms.WriteFields(params) + part, err := m.CreateFormFile(file.Name, fileHandle.Name()) + if err != nil { + panic(err) + } - buf := bytes.NewBuffer(f.Bytes) - ms.WriteReader(fieldname, f.Name, int64(len(f.Bytes)), buf) - case FileReader: - ms.WriteFields(params) + io.Copy(part, fileHandle) + case FileBytes: + part, err := m.CreateFormFile(file.Name, f.Name) + if err != nil { + panic(err) + } - if f.Size != -1 { - ms.WriteReader(fieldname, f.Name, f.Size, f.Reader) + buf := bytes.NewBuffer(f.Bytes) + io.Copy(part, buf) + case FileReader: + part, err := m.CreateFormFile(file.Name, f.Name) + if err != nil { + panic(err) + } - break - } + if f.Size != -1 { + io.Copy(part, f.Reader) + } else { + data, err := ioutil.ReadAll(f.Reader) + if err != nil { + panic(err) + } - data, err := ioutil.ReadAll(f.Reader) - if err != nil { - return APIResponse{}, err + buf := bytes.NewBuffer(data) + io.Copy(part, buf) + } + case FileURL: + val := string(f) + if err := m.WriteField(file.Name, val); err != nil { + panic(err) + } + case FileID: + val := string(f) + if err := m.WriteField(file.Name, val); err != nil { + panic(err) + } + default: + panic(errors.New(ErrBadFileType)) + } } - - buf := bytes.NewBuffer(data) - - ms.WriteReader(fieldname, f.Name, int64(len(data)), buf) - case url.URL: - params[fieldname] = f.String() - - ms.WriteFields(params) - default: - return APIResponse{}, errors.New(ErrBadFileType) - } + }() if bot.Debug { - log.Printf("Endpoint: %s, fieldname: %s, params: %v, file: %T\n", endpoint, fieldname, params, file) + log.Printf("Endpoint: %s, params: %v, with %d files\n", endpoint, params, len(files)) } method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint) - req, err := http.NewRequest("POST", method, nil) + req, err := http.NewRequest("POST", method, r) if err != nil { - return APIResponse{}, err + return nil, err } - ms.SetupRequest(req) + req.Header.Set("Content-Type", m.FormDataContentType()) resp, err := bot.Client.Do(req) if err != nil { - return APIResponse{}, err + return nil, err } defer resp.Body.Close() var apiResp APIResponse bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp) if err != nil { - return apiResp, err + return &apiResp, err } if bot.Debug {@@ -241,13 +256,13 @@ if apiResp.Parameters != nil {
parameters = *apiResp.Parameters } - return apiResp, Error{ + return &apiResp, &Error{ Message: apiResp.Description, ResponseParameters: parameters, } } - return apiResp, nil + return &apiResp, nil } // GetFileDirectURL returns direct URL to file@@ -287,23 +302,54 @@ func (bot *BotAPI) IsMessageToMe(message Message) bool {
return strings.Contains(message.Text, "@"+bot.Self.UserName) } +func hasFilesNeedingUpload(files []RequestFile) bool { + for _, file := range files { + switch file.File.(type) { + case string, FileBytes, FileReader: + return true + } + } + + return false +} + // Request sends a Chattable to Telegram, and returns the APIResponse. -func (bot *BotAPI) Request(c Chattable) (APIResponse, error) { +func (bot *BotAPI) Request(c Chattable) (*APIResponse, error) { params, err := c.params() if err != nil { - return APIResponse{}, err + return nil, err } - switch t := c.(type) { - case Fileable: - if t.useExistingFile() { - return bot.MakeRequest(t.method(), params) + if t, ok := c.(Fileable); ok { + files := t.files() + + // If we have files that need to be uploaded, we should delegate the + // request to UploadFile. + if hasFilesNeedingUpload(files) { + return bot.UploadFiles(t.method(), params, files) } - return bot.UploadFile(t.method(), params, t.name(), t.getFile()) - default: - return bot.MakeRequest(c.method(), params) + // 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 + } } + + return bot.MakeRequest(c.method(), params) } // Send will send a Chattable item to Telegram and provides the@@ -322,9 +368,51 @@ }
// SendMediaGroup sends a media group and returns the resulting messages. func (bot *BotAPI) SendMediaGroup(config MediaGroupConfig) ([]Message, error) { - params, _ := config.params() + filesToUpload := []RequestFile{} - resp, err := bot.MakeRequest(config.method(), params) + newMedia := []interface{}{} + + for idx, media := range config.Media { + switch m := media.(type) { + case InputMediaPhoto: + switch f := m.Media.(type) { + case string, FileBytes, FileReader: + m.Media = fmt.Sprintf("attach://file-%d", idx) + newMedia = append(newMedia, m) + + filesToUpload = append(filesToUpload, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + File: f, + }) + default: + newMedia = append(newMedia, m) + } + case InputMediaVideo: + switch f := m.Media.(type) { + case string, FileBytes, FileReader: + m.Media = fmt.Sprintf("attach://file-%d", idx) + newMedia = append(newMedia, m) + + filesToUpload = append(filesToUpload, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + File: f, + }) + default: + newMedia = append(newMedia, m) + } + default: + return nil, errors.New(ErrBadFileType) + } + } + + params, err := config.params() + if err != nil { + return nil, err + } + + params.AddInterface("media", newMedia) + + resp, err := bot.UploadFiles(config.method(), params, filesToUpload) if err != nil { return nil, err }@@ -340,9 +428,7 @@ //
// It requires UserID. // Offset and Limit are optional. func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) { - params, _ := config.params() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return UserProfilePhotos{}, err }@@ -357,9 +443,7 @@ // GetFile returns a File which can download a file from Telegram.
// // Requires FileID. func (bot *BotAPI) GetFile(config FileConfig) (File, error) { - params, _ := config.params() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return File{}, err }@@ -378,9 +462,7 @@ // To avoid stale items, set Offset to one higher than the previous item.
// Set Timeout to a large number to reduce requests so you can get updates // instantly instead of having to wait between requests. func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) { - params, _ := config.params() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return []Update{}, err }@@ -481,7 +563,7 @@ return err
} if t, ok := c.(Fileable); ok { - if !t.useExistingFile() { + if hasFilesNeedingUpload(t.files()) { return errors.New("unable to use http response to upload files") } }@@ -496,9 +578,7 @@ }
// GetChat gets information about a chat. func (bot *BotAPI) GetChat(config ChatInfoConfig) (Chat, error) { - params, _ := config.params() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return Chat{}, err }@@ -514,9 +594,7 @@ //
// If none have been appointed, only the creator will be returned. // Bots are not shown, even if they are an administrator. func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]ChatMember, error) { - params, _ := config.params() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return []ChatMember{}, err }@@ -529,9 +607,7 @@ }
// GetChatMembersCount gets the number of users in a chat. func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) { - params, _ := config.params() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return -1, err }@@ -544,9 +620,7 @@ }
// GetChatMember gets a specific chat member. func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) { - params, _ := config.params() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return ChatMember{}, err }@@ -559,9 +633,7 @@ }
// GetGameHighScores allows you to get the high scores for a game. func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) { - params, _ := config.params() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return []GameHighScore{}, err }@@ -574,9 +646,7 @@ }
// GetInviteLink get InviteLink for a chat func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) { - params, _ := config.params() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return "", err }@@ -589,9 +659,7 @@ }
// GetStickerSet returns a StickerSet. func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) { - params, _ := config.params() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return StickerSet{}, err }@@ -604,12 +672,7 @@ }
// StopPoll stops a poll and returns the result. func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) { - params, err := config.params() - if err != nil { - return Poll{}, err - } - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return Poll{}, err }@@ -624,12 +687,7 @@ // GetMyCommands gets the currently registered commands.
func (bot *BotAPI) GetMyCommands() ([]BotCommand, error) { config := GetMyCommandsConfig{} - params, err := config.params() - if err != nil { - return nil, err - } - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return nil, err }
@@ -92,7 +92,7 @@
func TestSendWithNewPhoto(t *testing.T) { bot, _ := getBot(t) - msg := NewPhotoUpload(ChatID, "tests/image.jpg") + msg := NewPhoto(ChatID, "tests/image.jpg") msg.Caption = "Test" _, err := bot.Send(msg)@@ -107,7 +107,7 @@
data, _ := ioutil.ReadFile("tests/image.jpg") b := FileBytes{Name: "image.jpg", Bytes: data} - msg := NewPhotoUpload(ChatID, b) + msg := NewPhoto(ChatID, b) msg.Caption = "Test" _, err := bot.Send(msg)@@ -122,7 +122,7 @@
f, _ := os.Open("tests/image.jpg") reader := FileReader{Name: "image.jpg", Reader: f, Size: -1} - msg := NewPhotoUpload(ChatID, reader) + msg := NewPhoto(ChatID, reader) msg.Caption = "Test" _, err := bot.Send(msg)@@ -134,7 +134,7 @@
func TestSendWithNewPhotoReply(t *testing.T) { bot, _ := getBot(t) - msg := NewPhotoUpload(ChatID, "tests/image.jpg") + msg := NewPhoto(ChatID, "tests/image.jpg") msg.ReplyToMessageID = ReplyToMessageID _, err := bot.Send(msg)@@ -147,7 +147,7 @@
func TestSendNewPhotoToChannel(t *testing.T) { bot, _ := getBot(t) - msg := NewPhotoUploadToChannel(Channel, "tests/image.jpg") + msg := NewPhotoToChannel(Channel, "tests/image.jpg") msg.Caption = "Test" _, err := bot.Send(msg)@@ -163,7 +163,7 @@
data, _ := ioutil.ReadFile("tests/image.jpg") b := FileBytes{Name: "image.jpg", Bytes: data} - msg := NewPhotoUploadToChannel(Channel, b) + msg := NewPhotoToChannel(Channel, b) msg.Caption = "Test" _, err := bot.Send(msg)@@ -179,7 +179,7 @@
f, _ := os.Open("tests/image.jpg") reader := FileReader{Name: "image.jpg", Reader: f, Size: -1} - msg := NewPhotoUploadToChannel(Channel, reader) + msg := NewPhotoToChannel(Channel, reader) msg.Caption = "Test" _, err := bot.Send(msg)@@ -192,7 +192,7 @@
func TestSendWithExistingPhoto(t *testing.T) { bot, _ := getBot(t) - msg := NewPhotoShare(ChatID, ExistingPhotoFileID) + msg := NewPhoto(ChatID, FileID(ExistingPhotoFileID)) msg.Caption = "Test" _, err := bot.Send(msg)@@ -204,7 +204,19 @@
func TestSendWithNewDocument(t *testing.T) { bot, _ := getBot(t) - msg := NewDocumentUpload(ChatID, "tests/image.jpg") + msg := NewDocument(ChatID, "tests/image.jpg") + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + } +} + +func TestSendWithNewDocumentAndThumb(t *testing.T) { + bot, _ := getBot(t) + + msg := NewDocument(ChatID, "tests/voice.ogg") + msg.AddFile("thumb", "tests/image.jpg") _, err := bot.Send(msg) if err != nil {@@ -215,7 +227,7 @@
func TestSendWithExistingDocument(t *testing.T) { bot, _ := getBot(t) - msg := NewDocumentShare(ChatID, ExistingDocumentFileID) + msg := NewDocument(ChatID, FileID(ExistingDocumentFileID)) _, err := bot.Send(msg) if err != nil {@@ -226,7 +238,7 @@
func TestSendWithNewAudio(t *testing.T) { bot, _ := getBot(t) - msg := NewAudioUpload(ChatID, "tests/audio.mp3") + msg := NewAudio(ChatID, "tests/audio.mp3") msg.Title = "TEST" msg.Duration = 10 msg.Performer = "TEST"@@ -242,7 +254,7 @@
func TestSendWithExistingAudio(t *testing.T) { bot, _ := getBot(t) - msg := NewAudioShare(ChatID, ExistingAudioFileID) + msg := NewAudio(ChatID, FileID(ExistingAudioFileID)) msg.Title = "TEST" msg.Duration = 10 msg.Performer = "TEST"@@ -257,7 +269,7 @@
func TestSendWithNewVoice(t *testing.T) { bot, _ := getBot(t) - msg := NewVoiceUpload(ChatID, "tests/voice.ogg") + msg := NewVoice(ChatID, "tests/voice.ogg") msg.Duration = 10 _, err := bot.Send(msg)@@ -269,7 +281,7 @@
func TestSendWithExistingVoice(t *testing.T) { bot, _ := getBot(t) - msg := NewVoiceShare(ChatID, ExistingVoiceFileID) + msg := NewVoice(ChatID, FileID(ExistingVoiceFileID)) msg.Duration = 10 _, err := bot.Send(msg)@@ -311,7 +323,7 @@
func TestSendWithNewVideo(t *testing.T) { bot, _ := getBot(t) - msg := NewVideoUpload(ChatID, "tests/video.mp4") + msg := NewVideo(ChatID, "tests/video.mp4") msg.Duration = 10 msg.Caption = "TEST"@@ -325,7 +337,7 @@
func TestSendWithExistingVideo(t *testing.T) { bot, _ := getBot(t) - msg := NewVideoShare(ChatID, ExistingVideoFileID) + msg := NewVideo(ChatID, FileID(ExistingVideoFileID)) msg.Duration = 10 msg.Caption = "TEST"@@ -339,7 +351,7 @@
func TestSendWithNewVideoNote(t *testing.T) { bot, _ := getBot(t) - msg := NewVideoNoteUpload(ChatID, 240, "tests/videonote.mp4") + msg := NewVideoNote(ChatID, 240, "tests/videonote.mp4") msg.Duration = 10 _, err := bot.Send(msg)@@ -352,7 +364,7 @@
func TestSendWithExistingVideoNote(t *testing.T) { bot, _ := getBot(t) - msg := NewVideoNoteShare(ChatID, 240, ExistingVideoNoteFileID) + msg := NewVideoNote(ChatID, 240, FileID(ExistingVideoNoteFileID)) msg.Duration = 10 _, err := bot.Send(msg)@@ -365,7 +377,7 @@
func TestSendWithNewSticker(t *testing.T) { bot, _ := getBot(t) - msg := NewStickerUpload(ChatID, "tests/image.jpg") + msg := NewSticker(ChatID, "tests/image.jpg") _, err := bot.Send(msg)@@ -377,7 +389,7 @@
func TestSendWithExistingSticker(t *testing.T) { bot, _ := getBot(t) - msg := NewStickerShare(ChatID, ExistingStickerFileID) + msg := NewSticker(ChatID, FileID(ExistingStickerFileID)) _, err := bot.Send(msg)@@ -389,7 +401,7 @@
func TestSendWithNewStickerAndKeyboardHide(t *testing.T) { bot, _ := getBot(t) - msg := NewStickerUpload(ChatID, "tests/image.jpg") + msg := NewSticker(ChatID, "tests/image.jpg") msg.ReplyMarkup = ReplyKeyboardRemove{ RemoveKeyboard: true, Selective: false,@@ -404,7 +416,7 @@
func TestSendWithExistingStickerAndKeyboardHide(t *testing.T) { bot, _ := getBot(t) - msg := NewStickerShare(ChatID, ExistingStickerFileID) + msg := NewSticker(ChatID, FileID(ExistingStickerFileID)) msg.ReplyMarkup = ReplyKeyboardRemove{ RemoveKeyboard: true, Selective: false,@@ -526,9 +538,9 @@ func TestSendWithMediaGroup(t *testing.T) {
bot, _ := getBot(t) cfg := NewMediaGroup(ChatID, []interface{}{ - NewInputMediaPhoto("https://i.imgur.com/unQLJIb.jpg"), - NewInputMediaPhoto("https://i.imgur.com/J5qweNZ.jpg"), - NewInputMediaVideo("https://i.imgur.com/F6RmI24.mp4"), + NewInputMediaPhoto(FileURL("https://i.imgur.com/unQLJIb.jpg")), + NewInputMediaPhoto("tests/image.jpg"), + NewInputMediaVideo("tests/video.mp4"), }) messages, err := bot.SendMediaGroup(cfg)@@ -537,11 +549,11 @@ t.Error(err)
} if messages == nil { - t.Error() + t.Error("No received messages") } - if len(messages) != 3 { - t.Error() + if len(messages) != len(cfg.Media) { + t.Errorf("Different number of messages: %d", len(messages)) } }
@@ -55,12 +55,19 @@ params() (Params, error)
method() string } +// RequestFile represents a file associated with a request. May involve +// uploading a file, or passing an existing ID. +type RequestFile struct { + // The multipart upload field name. + Name string + // The file to upload. + File interface{} +} + // Fileable is any config type that can be sent that includes a file. type Fileable interface { Chattable - name() string - getFile() interface{} - useExistingFile() bool + files() []RequestFile } // BaseChat is base type for all chat config types.@@ -87,11 +94,21 @@
// BaseFile is a base type for all file config types. type BaseFile struct { BaseChat - File interface{} - FileID string - UseExisting bool - MimeType string - FileSize int + Files []RequestFile + MimeType string + FileSize int +} + +// AddFile specifies a file for a Telegram request. +func (file *BaseFile) AddFile(name string, f interface{}) { + if file.Files == nil { + file.Files = make([]RequestFile, 0, 1) + } + + file.Files = append(file.Files, RequestFile{ + Name: name, + File: f, + }) } func (file BaseFile) params() (Params, error) {@@ -103,12 +120,8 @@
return params, err } -func (file BaseFile) getFile() interface{} { - return file.File -} - -func (file BaseFile) useExistingFile() bool { - return file.UseExisting +func (file BaseFile) files() []RequestFile { + return file.Files } // BaseEdit is base type of all chat edits.@@ -194,7 +207,6 @@
func (config PhotoConfig) params() (Params, error) { params, err := config.BaseFile.params() - params.AddNonEmpty(config.name(), config.FileID) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode)@@ -225,7 +237,6 @@ if err != nil {
return params, err } - params.AddNonEmpty(config.name(), config.FileID) params.AddNonZero("duration", config.Duration) params.AddNonEmpty("performer", config.Performer) params.AddNonEmpty("title", config.Title)@@ -253,7 +264,6 @@
func (config DocumentConfig) params() (Params, error) { params, err := config.BaseFile.params() - params.AddNonEmpty(config.name(), config.FileID) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode)@@ -274,11 +284,7 @@ BaseFile
} func (config StickerConfig) params() (Params, error) { - params, err := config.BaseChat.params() - - params.AddNonEmpty(config.name(), config.FileID) - - return params, err + return config.BaseChat.params() } func (config StickerConfig) name() string {@@ -301,7 +307,6 @@
func (config VideoConfig) params() (Params, error) { params, err := config.BaseChat.params() - params.AddNonEmpty(config.name(), config.FileID) params.AddNonZero("duration", config.Duration) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode)@@ -329,7 +334,6 @@
func (config AnimationConfig) params() (Params, error) { params, err := config.BaseChat.params() - params.AddNonEmpty(config.name(), config.FileID) params.AddNonZero("duration", config.Duration) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode)@@ -355,7 +359,6 @@
func (config VideoNoteConfig) params() (Params, error) { params, err := config.BaseChat.params() - params.AddNonEmpty(config.name(), config.FileID) params.AddNonZero("duration", config.Duration) params.AddNonZero("length", config.Length)@@ -381,7 +384,6 @@
func (config VoiceConfig) params() (Params, error) { params, err := config.BaseChat.params() - params.AddNonEmpty(config.name(), config.FileID) params.AddNonZero("duration", config.Duration) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode)@@ -683,23 +685,28 @@ func (config EditMessageCaptionConfig) method() string {
return "editMessageCaption" } -// EditMessageMediaConfig contains information about editing a message's media. +// EditMessageMediaConfig allows you to make an editMessageMedia request. type EditMessageMediaConfig struct { BaseEdit Media interface{} } +func (config EditMessageMediaConfig) files() []RequestFile { + return []RequestFile{ + { + Name: "media", + File: config.Media, + }, + } +} + func (EditMessageMediaConfig) method() string { return "editMessageMedia" } func (config EditMessageMediaConfig) params() (Params, error) { - params, err := config.BaseEdit.params() - - params.AddInterface("media", config.Media) - - return params, err + return config.BaseEdit.params() } // EditMessageReplyMarkupConfig allows you to modify the reply markup@@ -818,14 +825,6 @@ func (config WebhookConfig) name() string {
return "certificate" } -func (config WebhookConfig) getFile() interface{} { - return config.Certificate -} - -func (config WebhookConfig) useExistingFile() bool { - return config.URL != nil -} - // RemoveWebhookConfig is a helper to remove a webhook. type RemoveWebhookConfig struct { }@@ -853,6 +852,12 @@ Name string
Reader io.Reader Size int64 } + +// 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 {@@ -1310,14 +1315,6 @@ }
func (config SetChatPhotoConfig) name() string { return "photo" -} - -func (config SetChatPhotoConfig) getFile() interface{} { - return config.File -} - -func (config SetChatPhotoConfig) useExistingFile() bool { - return config.UseExisting } // DeleteChatPhotoConfig allows you to delete a group, supergroup, or channel's photo.@@ -1415,18 +1412,13 @@
return params, nil } -func (config UploadStickerConfig) name() string { - return "png_sticker" -} - -func (config UploadStickerConfig) getFile() interface{} { - return config.PNGSticker -} - -func (config UploadStickerConfig) useExistingFile() bool { - _, ok := config.PNGSticker.(string) - - return ok +func (config UploadStickerConfig) files() []RequestFile { + return []RequestFile{ + { + Name: "png_sticker", + File: config.PNGSticker, + }, + } } // NewStickerSetConfig allows creating a new sticker set.@@ -1454,12 +1446,6 @@ params.AddNonZero64("user_id", config.UserID)
params["name"] = config.Name params["title"] = config.Title - if sticker, ok := config.PNGSticker.(string); ok { - params[config.name()] = sticker - } else if sticker, ok := config.TGSSticker.(string); ok { - params[config.name()] = sticker - } - params["emojis"] = config.Emojis params.AddBool("contains_masks", config.ContainsMasks)@@ -1469,26 +1455,18 @@
return params, err } -func (config NewStickerSetConfig) getFile() interface{} { - return config.PNGSticker -} - -func (config NewStickerSetConfig) name() string { - return "png_sticker" -} - -func (config NewStickerSetConfig) useExistingFile() bool { +func (config NewStickerSetConfig) files() []RequestFile { if config.PNGSticker != nil { - _, ok := config.PNGSticker.(string) - return ok + return []RequestFile{{ + Name: "png_sticker", + File: config.PNGSticker, + }} } - if config.TGSSticker != nil { - _, ok := config.TGSSticker.(string) - return ok - } - - panic("NewStickerSetConfig had nil PNGSticker and TGSSticker") + return []RequestFile{{ + Name: "tgs_sticker", + File: config.TGSSticker, + }} } // AddStickerConfig allows you to add a sticker to a set.@@ -1512,29 +1490,24 @@ params.AddNonZero64("user_id", config.UserID)
params["name"] = config.Name params["emojis"] = config.Emojis - if sticker, ok := config.PNGSticker.(string); ok { - params[config.name()] = sticker - } else if sticker, ok := config.TGSSticker.(string); ok { - params[config.name()] = sticker - } - err := params.AddInterface("mask_position", config.MaskPosition) return params, err } -func (config AddStickerConfig) name() string { - return "png_sticker" -} - -func (config AddStickerConfig) getFile() interface{} { - return config.PNGSticker -} +func (config AddStickerConfig) files() []RequestFile { + if config.PNGSticker != nil { + return []RequestFile{{ + Name: "png_sticker", + File: config.PNGSticker, + }} + } -func (config AddStickerConfig) useExistingFile() bool { - _, ok := config.PNGSticker.(string) + return []RequestFile{{ + Name: "tgs_sticker", + File: config.TGSSticker, + }} - return ok } // SetStickerPositionConfig allows you to change the position of a sticker in a set.@@ -1601,15 +1574,6 @@ func (config SetStickerSetThumbConfig) name() string {
return "thumb" } -func (config SetStickerSetThumbConfig) getFile() interface{} { - return config.Thumb -} - -func (config SetStickerSetThumbConfig) useExistingFile() bool { - _, ok := config.Thumb.(string) - return ok -} - // SetChatStickerSetConfig allows you to set the sticker set for a supergroup. type SetChatStickerSetConfig struct { ChatID int64@@ -1652,6 +1616,9 @@
// MediaGroupConfig allows you to send a group of media. // // Media consist of InputMedia items (InputMediaPhoto, InputMediaVideo). +// +// Due to additional processing required, this config is not Chattable or +// Fileable. It must be uploaded with SendMediaGroup. type MediaGroupConfig struct { ChatID int64 ChannelUsername string@@ -1669,9 +1636,6 @@ func (config MediaGroupConfig) params() (Params, error) {
params := make(Params) params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) - if err := params.AddInterface("media", config.Media); err != nil { - return params, nil - } params.AddBool("disable_notification", config.DisableNotification) params.AddNonZero("reply_to_message_id", config.ReplyToMessageID)
@@ -51,261 +51,131 @@ MessageID: messageID,
} } -// NewPhotoUpload creates a new photo uploader. -// -// chatID is where to send it, file is a string path to the file, -// FileReader, or FileBytes. +// NewPhoto creates a new sendPhoto request. // // Note that you must send animated GIFs as a document. -func NewPhotoUpload(chatID int64, file interface{}) PhotoConfig { - return PhotoConfig{ +func NewPhoto(chatID int64, file interface{}) PhotoConfig { + config := PhotoConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, + BaseChat: BaseChat{ChatID: chatID}, }, } + + config.AddFile(config.name(), file) + + return config } -// NewPhotoUploadToChannel creates a new photo uploader to send a photo to a channel. -// -// username is the username of the channel, file is a string path to the file, -// FileReader, or FileBytes. +// NewPhotoToChannel creates a new photo uploader to send a photo to a channel. // // Note that you must send animated GIFs as a document. -func NewPhotoUploadToChannel(username string, file interface{}) PhotoConfig { - return PhotoConfig{ +func NewPhotoToChannel(username string, file interface{}) PhotoConfig { + config := PhotoConfig{ BaseFile: BaseFile{ BaseChat: BaseChat{ ChannelUsername: username, }, - File: file, - UseExisting: false, }, } -} -// NewPhotoShare shares an existing photo. -// You may use this to reshare an existing photo without reuploading it. -// -// chatID is where to send it, fileID is the ID of the file -// already uploaded. -func NewPhotoShare(chatID int64, fileID string) PhotoConfig { - return PhotoConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, - }, - } -} + config.AddFile(config.name(), file) -// NewAudioUpload creates a new audio uploader. -// -// chatID is where to send it, file is a string path to the file, -// FileReader, or FileBytes. -func NewAudioUpload(chatID int64, file interface{}) AudioConfig { - return AudioConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, - }, - } + return config } -// NewAudioShare shares an existing audio file. -// You may use this to reshare an existing audio file without -// reuploading it. -// -// chatID is where to send it, fileID is the ID of the audio -// already uploaded. -func NewAudioShare(chatID int64, fileID string) AudioConfig { - return AudioConfig{ +// NewAudio creates a new sendAudio request. +func NewAudio(chatID int64, file interface{}) AudioConfig { + config := AudioConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, + BaseChat: BaseChat{ChatID: chatID}, }, } -} -// NewDocumentUpload creates a new document uploader. -// -// chatID is where to send it, file is a string path to the file, -// FileReader, or FileBytes. -func NewDocumentUpload(chatID int64, file interface{}) DocumentConfig { - return DocumentConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, - }, - } -} + config.AddFile(config.name(), file) -// NewDocumentShare shares an existing document. -// You may use this to reshare an existing document without -// reuploading it. -// -// chatID is where to send it, fileID is the ID of the document -// already uploaded. -func NewDocumentShare(chatID int64, fileID string) DocumentConfig { - return DocumentConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, - }, - } + return config } -// NewStickerUpload creates a new sticker uploader. -// -// chatID is where to send it, file is a string path to the file, -// FileReader, or FileBytes. -func NewStickerUpload(chatID int64, file interface{}) StickerConfig { - return StickerConfig{ +// NewDocument creates a new sendDocument request. +func NewDocument(chatID int64, file interface{}) DocumentConfig { + config := DocumentConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, + BaseChat: BaseChat{ChatID: chatID}, }, } -} -// NewStickerShare shares an existing sticker. -// You may use this to reshare an existing sticker without -// reuploading it. -// -// chatID is where to send it, fileID is the ID of the sticker -// already uploaded. -func NewStickerShare(chatID int64, fileID string) StickerConfig { - return StickerConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, - }, - } -} + config.AddFile(config.name(), file) -// NewVideoUpload creates a new video uploader. -// -// chatID is where to send it, file is a string path to the file, -// FileReader, or FileBytes. -func NewVideoUpload(chatID int64, file interface{}) VideoConfig { - return VideoConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, - }, - } + return config } -// NewVideoShare shares an existing video. -// You may use this to reshare an existing video without reuploading it. -// -// chatID is where to send it, fileID is the ID of the video -// already uploaded. -func NewVideoShare(chatID int64, fileID string) VideoConfig { - return VideoConfig{ +// NewSticker creates a new sendSticker request. +func NewSticker(chatID int64, file interface{}) StickerConfig { + config := StickerConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, + BaseChat: BaseChat{ChatID: chatID}, }, } + + config.AddFile(config.name(), file) + + return config } -// NewAnimationUpload creates a new animation uploader. -// -// chatID is where to send it, file is a string path to the file, -// FileReader, or FileBytes. -func NewAnimationUpload(chatID int64, file interface{}) AnimationConfig { - return AnimationConfig{ +// NewVideo creates a new sendVideo request. +func NewVideo(chatID int64, file interface{}) VideoConfig { + config := VideoConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, + BaseChat: BaseChat{ChatID: chatID}, }, } + + config.AddFile(config.name(), file) + + return config } -// NewAnimationShare shares an existing animation. -// You may use this to reshare an existing animation without reuploading it. -// -// chatID is where to send it, fileID is the ID of the animation -// already uploaded. -func NewAnimationShare(chatID int64, fileID string) AnimationConfig { - return AnimationConfig{ +// NewAnimation creates a new sendAnimation request. +func NewAnimation(chatID int64, file interface{}) AnimationConfig { + config := AnimationConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, + BaseChat: BaseChat{ChatID: chatID}, }, } + + config.AddFile(config.name(), file) + + return config } -// NewVideoNoteUpload creates a new video note uploader. +// NewVideoNote creates a new sendVideoNote request. // // chatID is where to send it, file is a string path to the file, // FileReader, or FileBytes. -func NewVideoNoteUpload(chatID int64, length int, file interface{}) VideoNoteConfig { - return VideoNoteConfig{ +func NewVideoNote(chatID int64, length int, file interface{}) VideoNoteConfig { + config := VideoNoteConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, + BaseChat: BaseChat{ChatID: chatID}, }, Length: length, } -} -// NewVideoNoteShare shares an existing video. -// You may use this to reshare an existing video without reuploading it. -// -// chatID is where to send it, fileID is the ID of the video -// already uploaded. -func NewVideoNoteShare(chatID int64, length int, fileID string) VideoNoteConfig { - return VideoNoteConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, - }, - Length: length, - } -} + config.AddFile(config.name(), file) -// NewVoiceUpload creates a new voice uploader. -// -// chatID is where to send it, file is a string path to the file, -// FileReader, or FileBytes. -func NewVoiceUpload(chatID int64, file interface{}) VoiceConfig { - return VoiceConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, - }, - } + return config } -// NewVoiceShare shares an existing voice. -// You may use this to reshare an existing voice without reuploading it. -// -// chatID is where to send it, fileID is the ID of the video -// already uploaded. -func NewVoiceShare(chatID int64, fileID string) VoiceConfig { - return VoiceConfig{ +// NewVoice creates a new sendVoice request. +func NewVoice(chatID int64, file interface{}) VoiceConfig { + config := VoiceConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, + BaseChat: BaseChat{ChatID: chatID}, }, } + + config.AddFile(config.name(), file) + + return config } // NewMediaGroup creates a new media group. Files should be an array of@@ -318,7 +188,7 @@ }
} // NewInputMediaPhoto creates a new InputMediaPhoto. -func NewInputMediaPhoto(media string) InputMediaPhoto { +func NewInputMediaPhoto(media interface{}) InputMediaPhoto { return InputMediaPhoto{ BaseInputMedia{ Type: "photo",@@ -328,7 +198,7 @@ }
} // NewInputMediaVideo creates a new InputMediaVideo. -func NewInputMediaVideo(media string) InputMediaVideo { +func NewInputMediaVideo(media interface{}) InputMediaVideo { return InputMediaVideo{ BaseInputMedia: BaseInputMedia{ Type: "video",@@ -338,7 +208,7 @@ }
} // NewInputMediaAnimation creates a new InputMediaAnimation. -func NewInputMediaAnimation(media string) InputMediaAnimation { +func NewInputMediaAnimation(media interface{}) InputMediaAnimation { return InputMediaAnimation{ BaseInputMedia: BaseInputMedia{ Type: "animation",@@ -348,7 +218,7 @@ }
} // NewInputMediaAudio creates a new InputMediaAudio. -func NewInputMediaAudio(media string) InputMediaAudio { +func NewInputMediaAudio(media interface{}) InputMediaAudio { return InputMediaAudio{ BaseInputMedia: BaseInputMedia{ Type: "audio",@@ -875,37 +745,6 @@ Currency: currency,
Prices: prices} } -// NewSetChatPhotoUpload creates a new chat photo uploader. -// -// 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 NewSetChatPhotoUpload(chatID int64, file interface{}) SetChatPhotoConfig { - return SetChatPhotoConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, - }, - } -} - -// NewSetChatPhotoShare shares an existing photo. -// You may use this to reshare an existing photo without reuploading it. -// -// chatID is where to send it, fileID is the ID of the file -// already uploaded. -func NewSetChatPhotoShare(chatID int64, fileID string) SetChatPhotoConfig { - return SetChatPhotoConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, - }, - } -} - // NewChatTitle allows you to update the title of a chat. func NewChatTitle(chatID int64, title string) SetChatTitleConfig { return SetChatTitleConfig{@@ -924,14 +763,17 @@ }
// NewChatPhoto allows you to update the photo for a chat. func NewChatPhoto(chatID int64, photo interface{}) SetChatPhotoConfig { - return SetChatPhotoConfig{ + config := SetChatPhotoConfig{ BaseFile: BaseFile{ BaseChat: BaseChat{ ChatID: chatID, }, - File: photo, }, } + + config.AddFile(config.name(), photo) + + return config } // NewDeleteChatPhoto allows you to delete the photo for a chat.
@@ -1112,10 +1112,10 @@ }
// BaseInputMedia is a base type for the InputMedia types. type BaseInputMedia struct { - Type string `json:"type"` - Media string `json:"media"` - Caption string `json:"caption"` - ParseMode string `json:"parse_mode"` + Type string `json:"type"` + Media interface{} `json:"media"` + Caption string `json:"caption,omitempty"` + ParseMode string `json:"parse_mode,omitempty"` } // InputMediaPhoto is a photo to send as part of a media group.@@ -1126,10 +1126,10 @@
// InputMediaVideo is a video to send as part of a media group. type InputMediaVideo struct { BaseInputMedia - Width int `json:"width"` - Height int `json:"height"` - Duration int `json:"duration"` - SupportsStreaming bool `json:"supports_streaming"` + Width int `json:"width,omitempty"` + Height int `json:"height,omitempty"` + Duration int `json:"duration,omitempty"` + SupportsStreaming bool `json:"supports_streaming,omitempty"` } // InputMediaAnimation is an animation to send as part of a media group.
@@ -331,3 +331,21 @@ _ Chattable = VideoNoteConfig{}
_ Chattable = VoiceConfig{} _ Chattable = WebhookConfig{} ) + +// Ensure all Fileable types are correct. +var ( + _ Fileable = (*PhotoConfig)(nil) + _ Fileable = (*AudioConfig)(nil) + _ Fileable = (*DocumentConfig)(nil) + _ Fileable = (*StickerConfig)(nil) + _ Fileable = (*VideoConfig)(nil) + _ Fileable = (*AnimationConfig)(nil) + _ Fileable = (*VideoNoteConfig)(nil) + _ Fileable = (*VoiceConfig)(nil) + _ Fileable = (*SetChatPhotoConfig)(nil) + _ Fileable = (*EditMessageMediaConfig)(nil) + _ Fileable = (*SetChatPhotoConfig)(nil) + _ Fileable = (*UploadStickerConfig)(nil) + _ Fileable = (*NewStickerSetConfig)(nil) + _ Fileable = (*AddStickerConfig)(nil) +)