Merge pull request #356 from go-telegram-bot-api/multiple-uploads 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" ) // HTTPClient is the type needed for the bot to perform HTTP requests.@@ -96,7 +95,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) }@@ -107,14 +106,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 {@@ -128,14 +127,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.@@ -162,86 +161,100 @@
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 { + w.CloseWithError(err) + return + } } - 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 { + w.CloseWithError(err) + return + } + 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 { + w.CloseWithError(err) + return + } - 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 { + w.CloseWithError(err) + return + } - 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 { + w.CloseWithError(err) + return + } - break - } - - data, err := ioutil.ReadAll(f.Reader) - if err != nil { - return APIResponse{}, err + io.Copy(part, f.Reader) + case FileURL: + val := string(f) + if err := m.WriteField(file.Name, val); 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 + } } - - 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 {@@ -255,13 +268,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@@ -301,23 +314,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@@ -336,9 +380,7 @@ }
// SendMediaGroup sends a media group and returns the resulting messages. func (bot *BotAPI) SendMediaGroup(config MediaGroupConfig) ([]Message, error) { - params, _ := config.params() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return nil, err }@@ -354,9 +396,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 }@@ -371,9 +411,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 }@@ -392,9 +430,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 }@@ -510,7 +546,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") } }@@ -525,9 +561,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 }@@ -543,9 +577,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 }@@ -558,9 +590,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 }@@ -573,9 +603,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 }@@ -588,9 +616,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 }@@ -603,9 +629,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 }@@ -618,9 +642,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 }@@ -633,12 +655,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 }@@ -653,12 +670,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 }
@@ -127,7 +127,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)@@ -142,7 +142,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)@@ -155,9 +155,9 @@ func TestSendWithNewPhotoWithFileReader(t *testing.T) {
bot, _ := getBot(t) f, _ := os.Open("tests/image.jpg") - reader := FileReader{Name: "image.jpg", Reader: f, Size: -1} + reader := FileReader{Name: "image.jpg", Reader: f} - msg := NewPhotoUpload(ChatID, reader) + msg := NewPhoto(ChatID, reader) msg.Caption = "Test" _, err := bot.Send(msg)@@ -169,7 +169,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)@@ -182,7 +182,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)@@ -198,7 +198,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)@@ -212,9 +212,9 @@ func TestSendNewPhotoToChannelFileReader(t *testing.T) {
bot, _ := getBot(t) f, _ := os.Open("tests/image.jpg") - reader := FileReader{Name: "image.jpg", Reader: f, Size: -1} + reader := FileReader{Name: "image.jpg", Reader: f} - msg := NewPhotoUploadToChannel(Channel, reader) + msg := NewPhotoToChannel(Channel, reader) msg.Caption = "Test" _, err := bot.Send(msg)@@ -227,7 +227,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)@@ -239,7 +239,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.Thumb = "tests/image.jpg" _, err := bot.Send(msg) if err != nil {@@ -250,7 +262,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 {@@ -261,12 +273,10 @@
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" - msg.MimeType = "audio/mpeg" - msg.FileSize = 688 _, err := bot.Send(msg) if err != nil {@@ -277,7 +287,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"@@ -292,7 +302,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)@@ -304,7 +314,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)@@ -346,7 +356,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"@@ -360,7 +370,7 @@
func TestSendWithExistingVideo(t *testing.T) { bot, _ := getBot(t) - msg := NewVideoShare(ChatID, ExistingVideoFileID) + msg := NewVideo(ChatID, FileID(ExistingVideoFileID)) msg.Duration = 10 msg.Caption = "TEST"@@ -374,7 +384,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)@@ -387,7 +397,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)@@ -400,7 +410,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)@@ -412,7 +422,7 @@
func TestSendWithExistingSticker(t *testing.T) { bot, _ := getBot(t) - msg := NewStickerShare(ChatID, ExistingStickerFileID) + msg := NewSticker(ChatID, FileID(ExistingStickerFileID)) _, err := bot.Send(msg)@@ -424,7 +434,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,@@ -439,7 +449,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,@@ -583,12 +593,35 @@
bot.Request(DeleteWebhookConfig{}) } -func TestSendWithMediaGroup(t *testing.T) { +func TestSendWithMediaGroupPhotoVideo(t *testing.T) { + 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"), + }) + + messages, err := bot.SendMediaGroup(cfg) + if err != nil { + t.Error(err) + } + + if messages == nil { + t.Error("No received messages") + } + + if len(messages) != len(cfg.Media) { + t.Errorf("Different number of messages: %d", len(messages)) + } +} + +func TestSendWithMediaGroupDocument(t *testing.T) { bot, _ := getBot(t) cfg := NewMediaGroup(ChatID, []interface{}{ - NewInputMediaPhoto("https://github.com/go-telegram-bot-api/telegram-bot-api/raw/0a3a1c8716c4cd8d26a262af9f12dcbab7f3f28c/tests/image.jpg"), - NewInputMediaVideo("https://github.com/go-telegram-bot-api/telegram-bot-api/raw/0a3a1c8716c4cd8d26a262af9f12dcbab7f3f28c/tests/video.mp4"), + NewInputMediaDocument(FileURL("https://i.imgur.com/unQLJIb.jpg")), + NewInputMediaDocument("tests/image.jpg"), }) messages, err := bot.SendMediaGroup(cfg)@@ -597,11 +630,33 @@ t.Error(err)
} if messages == nil { - t.Error() + t.Error("No received messages") } - if len(messages) != 2 { - t.Error() + if len(messages) != len(cfg.Media) { + t.Errorf("Different number of messages: %d", len(messages)) + } +} + +func TestSendWithMediaGroupAudio(t *testing.T) { + bot, _ := getBot(t) + + cfg := NewMediaGroup(ChatID, []interface{}{ + NewInputMediaAudio("tests/audio.mp3"), + NewInputMediaAudio("tests/audio.mp3"), + }) + + messages, err := bot.SendMediaGroup(cfg) + if err != nil { + t.Error(err) + } + + if messages == nil { + t.Error("No received messages") + } + + if len(messages) != len(cfg.Media) { + t.Errorf("Different number of messages: %d", len(messages)) } }@@ -899,3 +954,49 @@ if commands[0].Command != "test" || commands[0].Description != "a test command" {
t.Error("Commands were incorrectly set") } } + +func TestEditMessageMedia(t *testing.T) { + bot, _ := getBot(t) + + msg := NewPhoto(ChatID, "tests/image.jpg") + msg.Caption = "Test" + m, err := bot.Send(msg) + + if err != nil { + t.Error(err) + } + + edit := EditMessageMediaConfig{ + BaseEdit: BaseEdit{ + ChatID: ChatID, + MessageID: m.MessageID, + }, + Media: NewInputMediaVideo("tests/video.mp4"), + } + + _, err = bot.Request(edit) + if err != nil { + t.Error(err) + } +} + +func TestPrepareInputMediaForParams(t *testing.T) { + media := []interface{}{ + NewInputMediaPhoto("tests/image.jpg"), + NewInputMediaVideo(FileID("test")), + } + + prepared := prepareInputMediaForParams(media) + + if media[0].(InputMediaPhoto).Media != "tests/image.jpg" { + t.Error("Original media was changed") + } + + if prepared[0].(InputMediaPhoto).Media != "attach://file-0" { + t.Error("New media was not replaced") + } + + if prepared[1].(InputMediaVideo).Media != FileID("test") { + t.Error("Passthrough value was not the same") + } +}
@@ -1,6 +1,7 @@
package tgbotapi import ( + "fmt" "io" "net/url" "strconv"@@ -103,12 +104,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 } // LogOutConfig is a request to log out of the cloud Bot API server.@@ -164,28 +172,11 @@
// BaseFile is a base type for all file config types. type BaseFile struct { BaseChat - File interface{} - FileID string - UseExisting bool - MimeType string - FileSize int + File interface{} } func (file BaseFile) params() (Params, error) { - params, err := file.BaseChat.params() - - params.AddNonEmpty("mime_type", file.MimeType) - params.AddNonZero("file_size", file.FileSize) - - return params, err -} - -func (file BaseFile) getFile() interface{} { - return file.File -} - -func (file BaseFile) useExistingFile() bool { - return file.UseExisting + return file.BaseChat.params() } // BaseEdit is base type of all chat edits.@@ -296,6 +287,7 @@
// PhotoConfig contains information about a SendPhoto request. type PhotoConfig struct { BaseFile + Thumb interface{} Caption string ParseMode string CaptionEntities []MessageEntity@@ -307,25 +299,37 @@ if err != nil {
return params, err } - params.AddNonEmpty(config.name(), config.FileID) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) err = params.AddInterface("caption_entities", config.CaptionEntities) return params, err -} - -func (config PhotoConfig) name() string { - return "photo" } func (config PhotoConfig) method() string { return "sendPhoto" } +func (config PhotoConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "photo", + File: config.File, + }} + + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + File: config.Thumb, + }) + } + + return files +} + // AudioConfig contains information about a SendAudio request. type AudioConfig struct { BaseFile + Thumb interface{} Caption string ParseMode string CaptionEntities []MessageEntity@@ -340,7 +344,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)@@ -349,19 +352,32 @@ params.AddNonEmpty("parse_mode", config.ParseMode)
err = params.AddInterface("caption_entities", config.CaptionEntities) return params, err -} - -func (config AudioConfig) name() string { - return "audio" } func (config AudioConfig) method() string { return "sendAudio" } +func (config AudioConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "audio", + File: config.File, + }} + + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + File: config.Thumb, + }) + } + + return files +} + // DocumentConfig contains information about a SendDocument request. type DocumentConfig struct { BaseFile + Thumb interface{} Caption string ParseMode string CaptionEntities []MessageEntity@@ -371,7 +387,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) params.AddBool("disable_content_type_detection", config.DisableContentTypeDetection)@@ -379,38 +394,50 @@
return params, err } -func (config DocumentConfig) name() string { - return "document" -} - func (config DocumentConfig) method() string { return "sendDocument" } +func (config DocumentConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "document", + File: config.File, + }} + + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + File: config.Thumb, + }) + } + + return files +} + // StickerConfig contains information about a SendSticker request. type StickerConfig struct { BaseFile } func (config StickerConfig) params() (Params, error) { - params, err := config.BaseChat.params() - - params.AddNonEmpty(config.name(), config.FileID) - - return params, err -} - -func (config StickerConfig) name() string { - return "sticker" + return config.BaseChat.params() } func (config StickerConfig) method() string { return "sendSticker" } +func (config StickerConfig) files() []RequestFile { + return []RequestFile{{ + Name: "sticker", + File: config.File, + }} +} + // VideoConfig contains information about a SendVideo request. type VideoConfig struct { BaseFile + Thumb interface{} Duration int Caption string ParseMode string@@ -424,7 +451,6 @@ if err != nil {
return params, err } - params.AddNonEmpty(config.name(), config.FileID) params.AddNonZero("duration", config.Duration) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode)@@ -434,18 +460,31 @@
return params, err } -func (config VideoConfig) name() string { - return "video" -} - func (config VideoConfig) method() string { return "sendVideo" } +func (config VideoConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "video", + File: config.File, + }} + + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + File: config.Thumb, + }) + } + + return files +} + // AnimationConfig contains information about a SendAnimation request. type AnimationConfig struct { BaseFile Duration int + Thumb interface{} Caption string ParseMode string CaptionEntities []MessageEntity@@ -457,7 +496,6 @@ if err != nil {
return params, err } - params.AddNonEmpty(config.name(), config.FileID) params.AddNonZero("duration", config.Duration) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode)@@ -466,17 +504,30 @@
return params, err } -func (config AnimationConfig) name() string { - return "animation" +func (config AnimationConfig) method() string { + return "sendAnimation" } -func (config AnimationConfig) method() string { - return "sendAnimation" +func (config AnimationConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "animation", + File: config.File, + }} + + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + File: config.Thumb, + }) + } + + return files } // VideoNoteConfig contains information about a SendVideoNote request. type VideoNoteConfig struct { BaseFile + Thumb interface{} Duration int Length int }@@ -484,24 +535,36 @@
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) return params, err -} - -func (config VideoNoteConfig) name() string { - return "video_note" } func (config VideoNoteConfig) method() string { return "sendVideoNote" } +func (config VideoNoteConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "video_note", + File: config.File, + }} + + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + File: config.Thumb, + }) + } + + return files +} + // VoiceConfig contains information about a SendVoice request. type VoiceConfig struct { BaseFile + Thumb interface{} Caption string ParseMode string CaptionEntities []MessageEntity@@ -514,7 +577,6 @@ if err != nil {
return params, err } - params.AddNonEmpty(config.name(), config.FileID) params.AddNonZero("duration", config.Duration) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode)@@ -523,12 +585,24 @@
return params, err } -func (config VoiceConfig) name() string { - return "voice" +func (config VoiceConfig) method() string { + return "sendVoice" } -func (config VoiceConfig) method() string { - return "sendVoice" +func (config VoiceConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "voice", + File: config.File, + }} + + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + File: config.Thumb, + }) + } + + return files } // LocationConfig contains information about a SendLocation request.@@ -849,7 +923,7 @@ 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@@ -862,12 +936,19 @@ }
func (config EditMessageMediaConfig) params() (Params, error) { params, err := config.BaseEdit.params() + if err != nil { + return params, err + } - params.AddInterface("media", config.Media) + err = params.AddInterface("media", prepareInputMediaParam(config.Media, 0)) return params, err } +func (config EditMessageMediaConfig) files() []RequestFile { + return prepareInputMediaFile(config.Media, 0) +} + // EditMessageReplyMarkupConfig allows you to modify the reply markup // of a message. type EditMessageReplyMarkupConfig struct {@@ -980,22 +1061,21 @@ }
params.AddNonEmpty("ip_address", config.IPAddress) params.AddNonZero("max_connections", config.MaxConnections) - params.AddInterface("allowed_updates", config.AllowedUpdates) + err := params.AddInterface("allowed_updates", config.AllowedUpdates) params.AddBool("drop_pending_updates", config.DropPendingUpdates) - return params, nil + return params, err } -func (config WebhookConfig) name() string { - return "certificate" -} - -func (config WebhookConfig) getFile() interface{} { - return config.Certificate -} +func (config WebhookConfig) files() []RequestFile { + if config.Certificate != nil { + return []RequestFile{{ + Name: "certificate", + File: config.Certificate, + }} + } -func (config WebhookConfig) useExistingFile() bool { - return config.URL != nil + return nil } // DeleteWebhookConfig is a helper to delete a webhook.@@ -1023,13 +1103,16 @@ Bytes []byte
} // FileReader contains information about a reader to upload as a File. -// If Size is -1, it will read the entire Reader into memory to -// calculate a Size. type FileReader struct { 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 {@@ -1278,9 +1361,9 @@ func (config SetChatPermissionsConfig) params() (Params, error) {
params := make(Params) params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) - params.AddInterface("permissions", config.Permissions) + err := params.AddInterface("permissions", config.Permissions) - return params, nil + return params, err } // ChatInviteLinkConfig contains information about getting a chat link.@@ -1610,16 +1693,11 @@ func (config SetChatPhotoConfig) method() string {
return "setChatPhoto" } -func (config SetChatPhotoConfig) name() string { - return "photo" -} - -func (config SetChatPhotoConfig) getFile() interface{} { - return config.File -} - -func (config SetChatPhotoConfig) useExistingFile() bool { - return config.UseExisting +func (config SetChatPhotoConfig) files() []RequestFile { + return []RequestFile{{ + Name: "photo", + File: config.File, + }} } // DeleteChatPhotoConfig allows you to delete a group, supergroup, or channel's photo.@@ -1717,18 +1795,11 @@
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.@@ -1756,12 +1827,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)@@ -1771,26 +1836,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 - } - - if config.TGSSticker != nil { - _, ok := config.TGSSticker.(string) - return ok + return []RequestFile{{ + Name: "png_sticker", + File: config.PNGSticker, + }} } - 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.@@ -1814,29 +1871,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.@@ -1892,24 +1944,14 @@
params["name"] = config.Name params.AddNonZero("user_id", config.UserID) - if thumb, ok := config.Thumb.(string); ok { - params["thumb"] = thumb - } - return params, nil } -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 +func (config SetStickerSetThumbConfig) files() []RequestFile { + return []RequestFile{{ + Name: "thumb", + File: config.Thumb, + }} } // SetChatStickerSetConfig allows you to set the sticker set for a supergroup.@@ -1971,13 +2013,42 @@ 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) - return params, nil + err := params.AddInterface("media", prepareInputMediaForParams(config.Media)) + + return params, err +} + +func (config MediaGroupConfig) files() []RequestFile { + return prepareInputMediaForFiles(config.Media) +} + +// DiceConfig contains information about a sendDice request. +type DiceConfig struct { + BaseChat + // Emoji on which the dice throw animation is based. + // Currently, must be one of π², π―, π, β½, π³, or π°. + // Dice can have values 1-6 for π², π―, and π³, values 1-5 for π and β½, + // and values 1-64 for π°. + // Defaults to βπ²β + Emoji string +} + +func (config DiceConfig) method() string { + return "sendDice" +} + +func (config DiceConfig) params() (Params, error) { + params, err := config.BaseChat.params() + if err != nil { + return params, err + } + + params.AddNonEmpty("emoji", config.Emoji) + + return params, err } // GetMyCommandsConfig gets a list of the currently registered commands.@@ -2008,28 +2079,170 @@
return params, err } -// DiceConfig contains information about a sendDice request. -type DiceConfig struct { - BaseChat - // Emoji on which the dice throw animation is based. - // Currently, must be one of π², π―, π, β½, π³, or π°. - // Dice can have values 1-6 for π², π―, and π³, values 1-5 for π and β½, - // and values 1-64 for π°. - // Defaults to βπ²β - Emoji string +// prepareInputMediaParam evaluates a single InputMedia and determines if it +// needs to be modified for a successful upload. If it returns nil, then the +// value does not need to be included in the params. Otherwise, it will return +// the same type as was originally provided. +// +// The idx is used to calculate the file field name. If you only have a single +// file, 0 may be used. It is formatted into "attach://file-%d" for the primary +// media and "attach://file-%d-thumb" for thumbnails. +// +// 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) + } + + return m + case InputMediaVideo: + switch m.Media.(type) { + case string, FileBytes, FileReader: + m.Media = fmt.Sprintf("attach://file-%d", idx) + } + + switch m.Thumb.(type) { + case string, FileBytes, FileReader: + m.Thumb = 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) + } + + switch m.Thumb.(type) { + case string, FileBytes, FileReader: + m.Thumb = 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) + } + + switch m.Thumb.(type) { + case string, FileBytes, FileReader: + m.Thumb = fmt.Sprintf("attach://file-%d-thumb", idx) + } + + return m + } + + return nil } -func (config DiceConfig) method() string { - return "sendDice" +// prepareInputMediaFile generates an array of RequestFile to provide for +// Fileable's files method. It returns an array as a single InputMedia may have +// multiple files, for the primary media and a thumbnail. +// +// The idx parameter is used to generate file field names. It uses the names +// "file-%d" for the main file and "file-%d-thumb" for the thumbnail. +// +// It is expected to be used in conjunction with prepareInputMediaParam. +func prepareInputMediaFile(inputMedia interface{}, idx int) []RequestFile { + files := []RequestFile{} + + switch m := inputMedia.(type) { + case InputMediaPhoto: + switch f := m.Media.(type) { + case string, FileBytes, FileReader: + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + File: f, + }) + } + case InputMediaVideo: + switch f := m.Media.(type) { + case string, FileBytes, FileReader: + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + File: f, + }) + } + + switch f := m.Thumb.(type) { + case string, FileBytes, FileReader: + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d-thumb", idx), + File: f, + }) + } + case InputMediaDocument: + switch f := m.Media.(type) { + case string, FileBytes, FileReader: + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + File: f, + }) + } + + switch f := m.Thumb.(type) { + case string, FileBytes, FileReader: + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + File: f, + }) + } + case InputMediaAudio: + switch f := m.Media.(type) { + case string, FileBytes, FileReader: + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + File: f, + }) + } + + switch f := m.Thumb.(type) { + case string, FileBytes, FileReader: + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + File: f, + }) + } + } + + return files } -func (config DiceConfig) params() (Params, error) { - params, err := config.BaseChat.params() - if err != nil { - return params, err +// prepareInputMediaForParams calls prepareInputMediaParam for each item +// provided and returns a new array with the correct params for a request. +// +// It is expected that files will get data from the associated function, +// prepareInputMediaForFiles. +func prepareInputMediaForParams(inputMedia []interface{}) []interface{} { + newMedia := make([]interface{}, len(inputMedia)) + copy(newMedia, inputMedia) + + for idx, media := range inputMedia { + if param := prepareInputMediaParam(media, idx); param != nil { + newMedia[idx] = param + } } - params.AddNonEmpty("emoji", config.Emoji) + return newMedia +} + +// prepareInputMediaForFiles calls prepareInputMediaFile for each item +// provided and returns a new array with the correct files for a request. +// +// It is expected that params will get data from the associated function, +// prepareInputMediaForParams. +func prepareInputMediaForFiles(inputMedia []interface{}) []RequestFile { + files := []RequestFile{} + + for idx, media := range inputMedia { + if file := prepareInputMediaFile(media, idx); file != nil { + files = append(files, file...) + } + } - return params, err + return files }
@@ -64,259 +64,105 @@ MessageID: messageID,
} } -// NewPhotoUpload creates a new photo uploader. +// NewPhoto creates a new sendPhoto request. // // 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 NewPhotoUpload(chatID int64, file interface{}) PhotoConfig { +func NewPhoto(chatID int64, file interface{}) PhotoConfig { return PhotoConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } -// 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 { +func NewPhotoToChannel(username string, file interface{}) PhotoConfig { return PhotoConfig{ BaseFile: BaseFile{ BaseChat: BaseChat{ ChannelUsername: username, }, - File: file, - UseExisting: false, + File: file, }, } } -// 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, - }, - } -} - -// 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 { +// NewAudio creates a new sendAudio request. +func NewAudio(chatID int64, file interface{}) AudioConfig { return AudioConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } -// 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{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, - }, - } -} - -// 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 { +// NewDocument creates a new sendDocument request. +func NewDocument(chatID int64, file interface{}) DocumentConfig { return DocumentConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, - }, - } -} - -// 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, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } -// 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 { +// NewSticker creates a new sendSticker request. +func NewSticker(chatID int64, file interface{}) StickerConfig { return StickerConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, - }, - } -} - -// 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, - }, - } -} - -// 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, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } -// 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 { +// NewVideo creates a new sendVideo request. +func NewVideo(chatID int64, file interface{}) VideoConfig { return VideoConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } -// 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 { +// NewAnimation creates a new sendAnimation request. +func NewAnimation(chatID int64, file interface{}) AnimationConfig { return AnimationConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } -// 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{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, - }, - } -} - -// 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 { +func NewVideoNote(chatID int64, length int, file interface{}) VideoNoteConfig { return VideoNoteConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, 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, - } -} - -// 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, - }, - } -} - -// 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 { +// NewVoice creates a new sendVoice request. +func NewVoice(chatID int64, file interface{}) VoiceConfig { return VoiceConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } }@@ -331,7 +177,7 @@ }
} // NewInputMediaPhoto creates a new InputMediaPhoto. -func NewInputMediaPhoto(media string) InputMediaPhoto { +func NewInputMediaPhoto(media interface{}) InputMediaPhoto { return InputMediaPhoto{ BaseInputMedia{ Type: "photo",@@ -341,7 +187,7 @@ }
} // NewInputMediaVideo creates a new InputMediaVideo. -func NewInputMediaVideo(media string) InputMediaVideo { +func NewInputMediaVideo(media interface{}) InputMediaVideo { return InputMediaVideo{ BaseInputMedia: BaseInputMedia{ Type: "video",@@ -351,7 +197,7 @@ }
} // NewInputMediaAnimation creates a new InputMediaAnimation. -func NewInputMediaAnimation(media string) InputMediaAnimation { +func NewInputMediaAnimation(media interface{}) InputMediaAnimation { return InputMediaAnimation{ BaseInputMedia: BaseInputMedia{ Type: "animation",@@ -361,7 +207,7 @@ }
} // NewInputMediaAudio creates a new InputMediaAudio. -func NewInputMediaAudio(media string) InputMediaAudio { +func NewInputMediaAudio(media interface{}) InputMediaAudio { return InputMediaAudio{ BaseInputMedia: BaseInputMedia{ Type: "audio",@@ -371,7 +217,7 @@ }
} // NewInputMediaDocument creates a new InputMediaDocument. -func NewInputMediaDocument(media string) InputMediaDocument { +func NewInputMediaDocument(media interface{}) InputMediaDocument { return InputMediaDocument{ BaseInputMedia: BaseInputMedia{ Type: "document",@@ -887,37 +733,6 @@ ProviderToken: providerToken,
StartParameter: startParameter, 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.
@@ -180,12 +180,6 @@
return name } -// GroupChat is a group chat. -type GroupChat struct { - ID int `json:"id"` - Title string `json:"title"` -} - // Chat represents a chat. type Chat struct { // ID is a unique identifier for this chat@@ -1654,7 +1648,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 string `json:"media"` + Media interface{} `json:"media"` // thumb intentionally missing as it is not currently compatible // Caption of the video to be sent, 0-1024 characters after entities parsing.@@ -1682,6 +1676,11 @@
// InputMediaVideo is a video to send as part of a media group. type InputMediaVideo struct { BaseInputMedia + // Thumbnail of the file sent; can be ignored if thumbnail generation for + // the file is supported server-side. + // + // optional + Thumb interface{} `json:"thumb"` // Width video width // // optional@@ -1703,6 +1702,11 @@
// InputMediaAnimation is an animation to send as part of a media group. type InputMediaAnimation struct { BaseInputMedia + // Thumbnail of the file sent; can be ignored if thumbnail generation for + // the file is supported server-side. + // + // optional + Thumb interface{} `json:"thumb"` // Width video width // // optional@@ -1720,6 +1724,11 @@
// InputMediaAudio is a audio to send as part of a media group. type InputMediaAudio struct { BaseInputMedia + // Thumbnail of the file sent; can be ignored if thumbnail generation for + // the file is supported server-side. + // + // optional + Thumb interface{} `json:"thumb"` // Duration of the audio in seconds // // optional@@ -1737,6 +1746,11 @@
// InputMediaDocument is a general file to send as part of a media group. type InputMediaDocument struct { BaseInputMedia + // Thumbnail of the file sent; can be ignored if thumbnail generation for + // the file is supported server-side. + // + // optional + Thumb interface{} `json:"thumb"` // 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
@@ -339,3 +339,24 @@ _ 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) + _ Fileable = (*MediaGroupConfig)(nil) + _ Fileable = (*WebhookConfig)(nil) + _ Fileable = (*SetStickerSetThumbConfig)(nil) +)