From ce4fc988c916518bf64e8d02be6e19d89d745928 Mon Sep 17 00:00:00 2001 From: Syfaro Date: Sat, 25 Jul 2020 19:29:40 -0500 Subject: [PATCH 1/7] Add support for uploading multiple files. --- bot.go | 292 ++++++++++++++++++++++++----------------- bot_test.go | 68 ++++++---- configs.go | 186 +++++++++++--------------- go.mod | 2 - go.sum | 2 - helpers.go | 354 ++++++++++++++------------------------------------ types.go | 16 +-- types_test.go | 18 +++ 8 files changed, 414 insertions(+), 524 deletions(-) diff --git a/bot.go b/bot.go index b028f9d..c7ded98 100644 --- a/bot.go +++ b/bot.go @@ -9,13 +9,12 @@ import ( "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 @@ func buildParams(in Params) (out url.Values) { } // 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 @@ func (bot *BotAPI) MakeRequest(endpoint string, params Params) (APIResponse, err 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 @@ func (bot *BotAPI) MakeRequest(endpoint string, params Params) (APIResponse, err 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 @@ func (bot *BotAPI) decodeAPIResponse(responseBody io.Reader, resp *APIResponse) 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 - } - defer fileHandle.Close() - - fi, err := os.Stat(f) - if err != nil { - return APIResponse{}, err + for field, value := range params { + if err := m.WriteField(field, value); err != nil { + panic(err) + } } - ms.WriteReader(fieldname, fileHandle.Name(), fi.Size(), fileHandle) - case FileBytes: - ms.WriteFields(params) + for _, file := range files { + switch f := file.File.(type) { + case string: + fileHandle, err := os.Open(f) + if err != nil { + panic(err) + } + defer fileHandle.Close() - buf := bytes.NewBuffer(f.Bytes) - ms.WriteReader(fieldname, f.Name, int64(len(f.Bytes)), buf) - case FileReader: - ms.WriteFields(params) + part, err := m.CreateFormFile(file.Name, fileHandle.Name()) + if err != nil { + panic(err) + } - if f.Size != -1 { - ms.WriteReader(fieldname, f.Name, f.Size, f.Reader) + io.Copy(part, fileHandle) + case FileBytes: + part, err := m.CreateFormFile(file.Name, f.Name) + if err != nil { + panic(err) + } - break + buf := bytes.NewBuffer(f.Bytes) + io.Copy(part, buf) + case FileReader: + part, err := m.CreateFormFile(file.Name, f.Name) + if err != nil { + panic(err) + } + + if f.Size != -1 { + io.Copy(part, f.Reader) + } else { + data, err := ioutil.ReadAll(f.Reader) + if err != nil { + panic(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)) + } } - - data, err := ioutil.ReadAll(f.Reader) - if err != nil { - return APIResponse{}, err - } - - 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 @@ func (bot *BotAPI) UploadFile(endpoint string, params Params, fieldname string, 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 @@ func (bot *BotAPI) Send(c Chattable) (Message, error) { // 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 @@ func (bot *BotAPI) SendMediaGroup(config MediaGroupConfig) ([]Message, error) { // 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 @@ func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserPro // // 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 @@ func (bot *BotAPI) GetFile(config FileConfig) (File, error) { // 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 @@ func WriteToHTTPResponse(w http.ResponseWriter, c Chattable) error { } 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 @@ func WriteToHTTPResponse(w http.ResponseWriter, c Chattable) error { // 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 @@ func (bot *BotAPI) GetChat(config ChatInfoConfig) (Chat, error) { // 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 @@ func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]Cha // 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 @@ func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error // 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 @@ func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) // 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 @@ func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHigh // 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 @@ func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) { // 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 @@ func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) // 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 @@ func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) { 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 } diff --git a/bot_test.go b/bot_test.go index 3229a8e..cb4ee42 100644 --- a/bot_test.go +++ b/bot_test.go @@ -92,7 +92,7 @@ func TestSendWithMessageForward(t *testing.T) { 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 @@ func TestSendWithNewPhotoWithFileBytes(t *testing.T) { 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 @@ func TestSendWithNewPhotoWithFileReader(t *testing.T) { 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 TestSendWithNewPhotoWithFileReader(t *testing.T) { 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 TestSendWithNewPhotoReply(t *testing.T) { 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 @@ func TestSendNewPhotoToChannelFileBytes(t *testing.T) { 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 @@ func TestSendNewPhotoToChannelFileReader(t *testing.T) { 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 TestSendNewPhotoToChannelFileReader(t *testing.T) { 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 TestSendWithExistingPhoto(t *testing.T) { 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 TestSendWithNewDocument(t *testing.T) { 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 TestSendWithExistingDocument(t *testing.T) { 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 TestSendWithNewAudio(t *testing.T) { 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 TestSendWithExistingAudio(t *testing.T) { 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 TestSendWithNewVoice(t *testing.T) { 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 TestSendWithVenue(t *testing.T) { 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 TestSendWithNewVideo(t *testing.T) { 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 TestSendWithExistingVideo(t *testing.T) { 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 TestSendWithNewVideoNote(t *testing.T) { 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 TestSendWithExistingVideoNote(t *testing.T) { 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 TestSendWithNewSticker(t *testing.T) { 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 TestSendWithExistingSticker(t *testing.T) { 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 TestSendWithNewStickerAndKeyboardHide(t *testing.T) { 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 @@ func TestSendWithMediaGroup(t *testing.T) { } 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)) } } diff --git a/configs.go b/configs.go index 939522b..2dfc777 100644 --- a/configs.go +++ b/configs.go @@ -55,12 +55,19 @@ type Chattable interface { 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 @@ func (chat *BaseChat) params() (Params, error) { // 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 @@ func (file BaseFile) params() (Params, error) { 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 @@ type PhotoConfig struct { 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 @@ func (config AudioConfig) params() (Params, error) { 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 @@ type DocumentConfig struct { 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 @@ type StickerConfig struct { } 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 @@ type VideoConfig struct { 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 @@ type AnimationConfig struct { 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 @@ type VideoNoteConfig struct { 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 @@ type VoiceConfig struct { 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 { } @@ -854,6 +853,12 @@ type FileReader struct { 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 { InlineQueryID string `json:"inline_query_id"` @@ -1312,14 +1317,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. type DeleteChatPhotoConfig struct { ChatID int64 @@ -1415,18 +1412,13 @@ func (config UploadStickerConfig) params() (Params, error) { 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 @@ func (config NewStickerSetConfig) params() (Params, error) { 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 @@ func (config NewStickerSetConfig) params() (Params, error) { 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 @@ func (config AddStickerConfig) params() (Params, error) { 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) files() []RequestFile { + if config.PNGSticker != nil { + return []RequestFile{{ + Name: "png_sticker", + File: config.PNGSticker, + }} + } -func (config AddStickerConfig) getFile() interface{} { - return config.PNGSticker -} + return []RequestFile{{ + Name: "tgs_sticker", + File: config.TGSSticker, + }} -func (config AddStickerConfig) useExistingFile() bool { - _, ok := config.PNGSticker.(string) - - 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 @@ func (config DeleteChatStickerSetConfig) params() (Params, error) { // 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) diff --git a/go.mod b/go.mod index 42f56cf..9e7f65c 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,3 @@ module github.com/go-telegram-bot-api/telegram-bot-api/v5 -require github.com/technoweenie/multipartstreamer v1.0.1 - go 1.13 diff --git a/go.sum b/go.sum index 8660600..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +0,0 @@ -github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= -github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= diff --git a/helpers.go b/helpers.go index 1e95367..5684c64 100644 --- a/helpers.go +++ b/helpers.go @@ -51,261 +51,131 @@ func NewForward(chatID int64, fromChatID int64, messageID int) ForwardConfig { } } -// 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, }, } + + config.AddFile(config.name(), file) + + return config } -// 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{ +// 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}, }, } + + config.AddFile(config.name(), file) + + return config } -// NewAudioUpload creates a new audio uploader. +// NewDocument creates a new sendDocument request. +func NewDocument(chatID int64, file interface{}) DocumentConfig { + config := DocumentConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + }, + } + + config.AddFile(config.name(), file) + + return config +} + +// NewSticker creates a new sendSticker request. +func NewSticker(chatID int64, file interface{}) StickerConfig { + config := StickerConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + }, + } + + config.AddFile(config.name(), file) + + return config +} + +// NewVideo creates a new sendVideo request. +func NewVideo(chatID int64, file interface{}) VideoConfig { + config := VideoConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + }, + } + + config.AddFile(config.name(), file) + + return config +} + +// NewAnimation creates a new sendAnimation request. +func NewAnimation(chatID int64, file interface{}) AnimationConfig { + config := AnimationConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + }, + } + + config.AddFile(config.name(), file) + + return config +} + +// NewVideoNote creates a new sendVideoNote request. // // 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{ +func NewVideoNote(chatID int64, length int, file interface{}) VideoNoteConfig { + config := VideoNoteConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, - }, - } -} - -// 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 { - 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, - }, - } -} - -// 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{ - 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, - }, - } -} - -// 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{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, - }, - } -} - -// 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{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, - }, - } -} - -// 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. -// -// 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{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, + BaseChat: BaseChat{ChatID: chatID}, }, Length: length, } + + config.AddFile(config.name(), file) + + return config } -// 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{ +// 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}, }, - 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, - }, - } -} + config.AddFile(config.name(), file) -// 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{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, - }, - } + return config } // NewMediaGroup creates a new media group. Files should be an array of @@ -318,7 +188,7 @@ func NewMediaGroup(chatID int64, files []interface{}) MediaGroupConfig { } // NewInputMediaPhoto creates a new InputMediaPhoto. -func NewInputMediaPhoto(media string) InputMediaPhoto { +func NewInputMediaPhoto(media interface{}) InputMediaPhoto { return InputMediaPhoto{ BaseInputMedia{ Type: "photo", @@ -328,7 +198,7 @@ func NewInputMediaPhoto(media string) InputMediaPhoto { } // 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 @@ func NewInputMediaVideo(media string) InputMediaVideo { } // 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 @@ func NewInputMediaAnimation(media string) InputMediaAnimation { } // 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 @@ func NewInvoice(chatID int64, title, description, payload, providerToken, startP 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 @@ func NewChatDescription(chatID int64, description string) SetChatDescriptionConf // 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. diff --git a/types.go b/types.go index 95a871d..5e3159b 100644 --- a/types.go +++ b/types.go @@ -1112,10 +1112,10 @@ type BotCommand struct { // 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 @@ type InputMediaPhoto struct { // 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. diff --git a/types_test.go b/types_test.go index 401cb6a..46ec2d1 100644 --- a/types_test.go +++ b/types_test.go @@ -331,3 +331,21 @@ var ( _ 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) +) From 99b74b8efaa519636cf7f56afed97b65ecafb512 Mon Sep 17 00:00:00 2001 From: Syfaro Date: Sat, 25 Jul 2020 21:20:05 -0500 Subject: [PATCH 2/7] Improve usability of new files for configs. --- bot_test.go | 4 +- configs.go | 188 +++++++++++++++++++++++++++++++++++++--------------- helpers.go | 70 ++++++------------- 3 files changed, 154 insertions(+), 108 deletions(-) diff --git a/bot_test.go b/bot_test.go index cb4ee42..9e3f822 100644 --- a/bot_test.go +++ b/bot_test.go @@ -216,7 +216,7 @@ func TestSendWithNewDocumentAndThumb(t *testing.T) { bot, _ := getBot(t) msg := NewDocument(ChatID, "tests/voice.ogg") - msg.AddFile("thumb", "tests/image.jpg") + msg.Thumb = "tests/image.jpg" _, err := bot.Send(msg) if err != nil { @@ -242,8 +242,6 @@ func TestSendWithNewAudio(t *testing.T) { msg.Title = "TEST" msg.Duration = 10 msg.Performer = "TEST" - msg.MimeType = "audio/mpeg" - msg.FileSize = 688 _, err := bot.Send(msg) if err != nil { diff --git a/configs.go b/configs.go index 2dfc777..495f9ac 100644 --- a/configs.go +++ b/configs.go @@ -94,34 +94,11 @@ func (chat *BaseChat) params() (Params, error) { // BaseFile is a base type for all file config types. type BaseFile struct { BaseChat - 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, - }) + 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) files() []RequestFile { - return file.Files + return file.BaseChat.params() } // BaseEdit is base type of all chat edits. @@ -200,6 +177,7 @@ func (config ForwardConfig) method() string { // PhotoConfig contains information about a SendPhoto request. type PhotoConfig struct { BaseFile + Thumb interface{} Caption string ParseMode string } @@ -213,17 +191,30 @@ func (config PhotoConfig) params() (Params, error) { 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 Duration int @@ -246,17 +237,30 @@ func (config AudioConfig) params() (Params, error) { return params, nil } -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 } @@ -270,14 +274,26 @@ func (config DocumentConfig) params() (Params, error) { 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 @@ -287,17 +303,21 @@ func (config StickerConfig) params() (Params, error) { return config.BaseChat.params() } -func (config StickerConfig) name() string { - return "sticker" -} - 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 @@ -315,18 +335,31 @@ func (config VideoConfig) params() (Params, error) { 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 } @@ -349,9 +382,26 @@ 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 } @@ -365,17 +415,30 @@ func (config VideoNoteConfig) params() (Params, error) { 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 Duration int @@ -391,14 +454,26 @@ func (config VoiceConfig) params() (Params, error) { return params, err } -func (config VoiceConfig) name() string { - return "voice" -} - 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. type LocationConfig struct { BaseChat @@ -1313,8 +1388,11 @@ func (config SetChatPhotoConfig) method() string { return "setChatPhoto" } -func (config SetChatPhotoConfig) name() string { - return "photo" +func (config SetChatPhotoConfig) files() []RequestFile { + return []RequestFile{{ + Name: "photo", + File: config.File, + }} } // DeleteChatPhotoConfig allows you to delete a group, supergroup, or channel's photo. diff --git a/helpers.go b/helpers.go index 5684c64..8557970 100644 --- a/helpers.go +++ b/helpers.go @@ -55,97 +55,76 @@ func NewForward(chatID int64, fromChatID int64, messageID int) ForwardConfig { // // Note that you must send animated GIFs as a document. func NewPhoto(chatID int64, file interface{}) PhotoConfig { - config := PhotoConfig{ + return PhotoConfig{ BaseFile: BaseFile{ BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } - - config.AddFile(config.name(), file) - - return config } // NewPhotoToChannel creates a new photo uploader to send a photo to a channel. // // Note that you must send animated GIFs as a document. func NewPhotoToChannel(username string, file interface{}) PhotoConfig { - config := PhotoConfig{ + return PhotoConfig{ BaseFile: BaseFile{ BaseChat: BaseChat{ ChannelUsername: username, }, + File: file, }, } - - config.AddFile(config.name(), file) - - return config } // NewAudio creates a new sendAudio request. func NewAudio(chatID int64, file interface{}) AudioConfig { - config := AudioConfig{ + return AudioConfig{ BaseFile: BaseFile{ BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } - - config.AddFile(config.name(), file) - - return config } // NewDocument creates a new sendDocument request. func NewDocument(chatID int64, file interface{}) DocumentConfig { - config := DocumentConfig{ + return DocumentConfig{ BaseFile: BaseFile{ BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } - - config.AddFile(config.name(), file) - - return config } // NewSticker creates a new sendSticker request. func NewSticker(chatID int64, file interface{}) StickerConfig { - config := StickerConfig{ + return StickerConfig{ BaseFile: BaseFile{ BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } - - config.AddFile(config.name(), file) - - return config } // NewVideo creates a new sendVideo request. func NewVideo(chatID int64, file interface{}) VideoConfig { - config := VideoConfig{ + return VideoConfig{ BaseFile: BaseFile{ BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } - - config.AddFile(config.name(), file) - - return config } // NewAnimation creates a new sendAnimation request. func NewAnimation(chatID int64, file interface{}) AnimationConfig { - config := AnimationConfig{ + return AnimationConfig{ BaseFile: BaseFile{ BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } - - config.AddFile(config.name(), file) - - return config } // NewVideoNote creates a new sendVideoNote request. @@ -153,29 +132,23 @@ func NewAnimation(chatID int64, file interface{}) AnimationConfig { // chatID is where to send it, file is a string path to the file, // FileReader, or FileBytes. func NewVideoNote(chatID int64, length int, file interface{}) VideoNoteConfig { - config := VideoNoteConfig{ + return VideoNoteConfig{ BaseFile: BaseFile{ BaseChat: BaseChat{ChatID: chatID}, + File: file, }, Length: length, } - - config.AddFile(config.name(), file) - - return config } // NewVoice creates a new sendVoice request. func NewVoice(chatID int64, file interface{}) VoiceConfig { - config := VoiceConfig{ + return VoiceConfig{ BaseFile: BaseFile{ BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } - - config.AddFile(config.name(), file) - - return config } // NewMediaGroup creates a new media group. Files should be an array of @@ -763,17 +736,14 @@ func NewChatDescription(chatID int64, description string) SetChatDescriptionConf // NewChatPhoto allows you to update the photo for a chat. func NewChatPhoto(chatID int64, photo interface{}) SetChatPhotoConfig { - config := SetChatPhotoConfig{ + return 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. From c6bf64c67d2d1002b7fbec45608fb914d176616c Mon Sep 17 00:00:00 2001 From: Syfaro Date: Sat, 25 Jul 2020 23:36:31 -0500 Subject: [PATCH 3/7] Replace panic with CloseWithError. --- bot.go | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/bot.go b/bot.go index c7ded98..4bd2fba 100644 --- a/bot.go +++ b/bot.go @@ -160,7 +160,8 @@ func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFi for field, value := range params { if err := m.WriteField(field, value); err != nil { - panic(err) + w.CloseWithError(err) + return } } @@ -169,20 +170,23 @@ func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFi case string: fileHandle, err := os.Open(f) if err != nil { - panic(err) + w.CloseWithError(err) + return } defer fileHandle.Close() part, err := m.CreateFormFile(file.Name, fileHandle.Name()) if err != nil { - panic(err) + w.CloseWithError(err) + return } io.Copy(part, fileHandle) case FileBytes: part, err := m.CreateFormFile(file.Name, f.Name) if err != nil { - panic(err) + w.CloseWithError(err) + return } buf := bytes.NewBuffer(f.Bytes) @@ -190,7 +194,8 @@ func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFi case FileReader: part, err := m.CreateFormFile(file.Name, f.Name) if err != nil { - panic(err) + w.CloseWithError(err) + return } if f.Size != -1 { @@ -198,7 +203,8 @@ func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFi } else { data, err := ioutil.ReadAll(f.Reader) if err != nil { - panic(err) + w.CloseWithError(err) + return } buf := bytes.NewBuffer(data) @@ -207,15 +213,18 @@ func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFi case FileURL: val := string(f) if err := m.WriteField(file.Name, val); err != nil { - panic(err) + w.CloseWithError(err) + return } case FileID: val := string(f) if err := m.WriteField(file.Name, val); err != nil { - panic(err) + w.CloseWithError(err) + return } default: - panic(errors.New(ErrBadFileType)) + w.CloseWithError(errors.New(ErrBadFileType)) + return } } }() From 8d14bd7a5608c3a9fb7459c37893dc977d968f4f Mon Sep 17 00:00:00 2001 From: Syfaro Date: Sun, 26 Jul 2020 14:40:12 -0500 Subject: [PATCH 4/7] Make MediaGroupConfig Chattable and Fileable. --- bot.go | 46 +------------------------------------------- configs.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++++--- types_test.go | 1 + 3 files changed, 52 insertions(+), 48 deletions(-) diff --git a/bot.go b/bot.go index 4bd2fba..d44ae54 100644 --- a/bot.go +++ b/bot.go @@ -377,51 +377,7 @@ func (bot *BotAPI) Send(c Chattable) (Message, error) { // SendMediaGroup sends a media group and returns the resulting messages. func (bot *BotAPI) SendMediaGroup(config MediaGroupConfig) ([]Message, error) { - filesToUpload := []RequestFile{} - - 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) + resp, err := bot.Request(config) if err != nil { return nil, err } diff --git a/configs.go b/configs.go index 495f9ac..36e5ad7 100644 --- a/configs.go +++ b/configs.go @@ -1,6 +1,7 @@ package tgbotapi import ( + "fmt" "io" "net/url" "strconv" @@ -1694,9 +1695,6 @@ func (config DeleteChatStickerSetConfig) params() (Params, error) { // 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 @@ -1717,9 +1715,58 @@ func (config MediaGroupConfig) params() (Params, error) { params.AddBool("disable_notification", config.DisableNotification) params.AddNonZero("reply_to_message_id", config.ReplyToMessageID) + newMedia := make([]interface{}, len(config.Media)) + copy(newMedia, config.Media) + + for idx, media := range config.Media { + switch m := media.(type) { + case InputMediaPhoto: + switch m.Media.(type) { + case string, FileBytes, FileReader: + m.Media = fmt.Sprintf("attach://file-%d", idx) + newMedia[idx] = m + } + case InputMediaVideo: + switch m.Media.(type) { + case string, FileBytes, FileReader: + m.Media = fmt.Sprintf("attach://file-%d", idx) + newMedia[idx] = m + } + } + } + + params.AddInterface("media", newMedia) + return params, nil } +func (config MediaGroupConfig) files() []RequestFile { + files := []RequestFile{} + + for idx, media := range config.Media { + switch m := media.(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, + }) + } + } + } + + return files +} + // DiceConfig allows you to send a random dice roll to Telegram. // // Emoji may be one of the following: 🎲 (1-6), 🎯 (1-6), 🏀 (1-5). diff --git a/types_test.go b/types_test.go index 46ec2d1..3b6c5be 100644 --- a/types_test.go +++ b/types_test.go @@ -348,4 +348,5 @@ var ( _ Fileable = (*UploadStickerConfig)(nil) _ Fileable = (*NewStickerSetConfig)(nil) _ Fileable = (*AddStickerConfig)(nil) + _ Fileable = (*MediaGroupConfig)(nil) ) From a45216f441cd4adc8bda63ea430a24ffa778ab6f Mon Sep 17 00:00:00 2001 From: Syfaro Date: Sun, 26 Jul 2020 15:51:33 -0500 Subject: [PATCH 5/7] Generalize InputMedia, fix editMessageMedia. --- bot_test.go | 46 ++++++++++++ configs.go | 201 +++++++++++++++++++++++++++++++++++----------------- types.go | 1 + 3 files changed, 182 insertions(+), 66 deletions(-) diff --git a/bot_test.go b/bot_test.go index 9e3f822..9645efa 100644 --- a/bot_test.go +++ b/bot_test.go @@ -817,3 +817,49 @@ func TestSetCommands(t *testing.T) { 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") + } +} diff --git a/configs.go b/configs.go index 36e5ad7..2b15c16 100644 --- a/configs.go +++ b/configs.go @@ -768,21 +768,23 @@ type EditMessageMediaConfig struct { 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) { - return config.BaseEdit.params() + params, err := config.BaseEdit.params() + if err != nil { + return params, err + } + + 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 @@ -892,9 +894,9 @@ func (config WebhookConfig) params() (Params, error) { } params.AddNonZero("max_connections", config.MaxConnections) - params.AddInterface("allowed_updates", config.AllowedUpdates) + err := params.AddInterface("allowed_updates", config.AllowedUpdates) - return params, nil + return params, err } func (config WebhookConfig) name() string { @@ -1177,9 +1179,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. @@ -1492,12 +1494,10 @@ func (config UploadStickerConfig) params() (Params, error) { } func (config UploadStickerConfig) files() []RequestFile { - return []RequestFile{ - { - Name: "png_sticker", - File: config.PNGSticker, - }, - } + return []RequestFile{{ + Name: "png_sticker", + File: config.PNGSticker, + }} } // NewStickerSetConfig allows creating a new sticker set. @@ -1715,56 +1715,13 @@ func (config MediaGroupConfig) params() (Params, error) { params.AddBool("disable_notification", config.DisableNotification) params.AddNonZero("reply_to_message_id", config.ReplyToMessageID) - newMedia := make([]interface{}, len(config.Media)) - copy(newMedia, config.Media) + err := params.AddInterface("media", prepareInputMediaForParams(config.Media)) - for idx, media := range config.Media { - switch m := media.(type) { - case InputMediaPhoto: - switch m.Media.(type) { - case string, FileBytes, FileReader: - m.Media = fmt.Sprintf("attach://file-%d", idx) - newMedia[idx] = m - } - case InputMediaVideo: - switch m.Media.(type) { - case string, FileBytes, FileReader: - m.Media = fmt.Sprintf("attach://file-%d", idx) - newMedia[idx] = m - } - } - } - - params.AddInterface("media", newMedia) - - return params, nil + return params, err } func (config MediaGroupConfig) files() []RequestFile { - files := []RequestFile{} - - for idx, media := range config.Media { - switch m := media.(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, - }) - } - } - } - - return files + return prepareInputMediaForFiles(config.Media) } // DiceConfig allows you to send a random dice roll to Telegram. @@ -1818,3 +1775,115 @@ func (config SetMyCommandsConfig) params() (Params, error) { return params, err } + +// 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 + } + + return nil +} + +// 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, + }) + } + } + + return files +} + +// 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 + } + } + + 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 files +} diff --git a/types.go b/types.go index 5e3159b..2eb3ad5 100644 --- a/types.go +++ b/types.go @@ -1126,6 +1126,7 @@ type InputMediaPhoto struct { // InputMediaVideo is a video to send as part of a media group. type InputMediaVideo struct { BaseInputMedia + Thumb interface{} Width int `json:"width,omitempty"` Height int `json:"height,omitempty"` Duration int `json:"duration,omitempty"` From f2cd95670dcea2b0416090fcffab9e4940cce341 Mon Sep 17 00:00:00 2001 From: Syfaro Date: Sun, 26 Jul 2020 16:06:22 -0500 Subject: [PATCH 6/7] Update some forgotten items. --- bot.go | 13 +------------ bot_test.go | 4 ++-- configs.go | 29 ++++++++++++++--------------- types_test.go | 2 ++ 4 files changed, 19 insertions(+), 29 deletions(-) diff --git a/bot.go b/bot.go index d44ae54..2fc190c 100644 --- a/bot.go +++ b/bot.go @@ -198,18 +198,7 @@ func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFi return } - if f.Size != -1 { - io.Copy(part, f.Reader) - } else { - data, err := ioutil.ReadAll(f.Reader) - if err != nil { - w.CloseWithError(err) - return - } - - buf := bytes.NewBuffer(data) - io.Copy(part, buf) - } + io.Copy(part, f.Reader) case FileURL: val := string(f) if err := m.WriteField(file.Name, val); err != nil { diff --git a/bot_test.go b/bot_test.go index 9645efa..537dcc3 100644 --- a/bot_test.go +++ b/bot_test.go @@ -120,7 +120,7 @@ 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 := NewPhoto(ChatID, reader) msg.Caption = "Test" @@ -177,7 +177,7 @@ 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 := NewPhotoToChannel(Channel, reader) msg.Caption = "Test" diff --git a/configs.go b/configs.go index 2b15c16..925766f 100644 --- a/configs.go +++ b/configs.go @@ -375,10 +375,6 @@ func (config AnimationConfig) params() (Params, error) { return params, err } -func (config AnimationConfig) name() string { - return "animation" -} - func (config AnimationConfig) method() string { return "sendAnimation" } @@ -899,8 +895,15 @@ func (config WebhookConfig) params() (Params, error) { return params, err } -func (config WebhookConfig) name() string { - return "certificate" +func (config WebhookConfig) files() []RequestFile { + if config.Certificate != nil { + return []RequestFile{{ + Name: "certificate", + File: config.Certificate, + }} + } + + return nil } // RemoveWebhookConfig is a helper to remove a webhook. @@ -923,12 +926,9 @@ type FileBytes struct { } // 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. @@ -1642,15 +1642,14 @@ func (config SetStickerSetThumbConfig) params() (Params, error) { 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) files() []RequestFile { + return []RequestFile{{ + Name: "thumb", + File: config.Thumb, + }} } // SetChatStickerSetConfig allows you to set the sticker set for a supergroup. diff --git a/types_test.go b/types_test.go index 3b6c5be..354625c 100644 --- a/types_test.go +++ b/types_test.go @@ -349,4 +349,6 @@ var ( _ Fileable = (*NewStickerSetConfig)(nil) _ Fileable = (*AddStickerConfig)(nil) _ Fileable = (*MediaGroupConfig)(nil) + _ Fileable = (*WebhookConfig)(nil) + _ Fileable = (*SetStickerSetThumbConfig)(nil) ) From b163052f82f70d259ba53759bd3470286201b479 Mon Sep 17 00:00:00 2001 From: Syfaro Date: Sat, 20 Feb 2021 13:49:00 -0500 Subject: [PATCH 7/7] Handle InputMedia{Document,Audio} in media groups. --- bot_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++- configs.go | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++++ helpers.go | 2 +- types.go | 12 ++++++++---- 4 files changed, 110 insertions(+), 6 deletions(-) diff --git a/bot_test.go b/bot_test.go index 537dcc3..af1e237 100644 --- a/bot_test.go +++ b/bot_test.go @@ -532,7 +532,7 @@ func TestSetWebhookWithoutCert(t *testing.T) { bot.Request(RemoveWebhookConfig{}) } -func TestSendWithMediaGroup(t *testing.T) { +func TestSendWithMediaGroupPhotoVideo(t *testing.T) { bot, _ := getBot(t) cfg := NewMediaGroup(ChatID, []interface{}{ @@ -555,6 +555,50 @@ func TestSendWithMediaGroup(t *testing.T) { } } +func TestSendWithMediaGroupDocument(t *testing.T) { + bot, _ := getBot(t) + + cfg := NewMediaGroup(ChatID, []interface{}{ + NewInputMediaDocument(FileURL("https://i.imgur.com/unQLJIb.jpg")), + NewInputMediaDocument("tests/image.jpg"), + }) + + 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 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)) + } +} + func ExampleNewBotAPI() { bot, err := NewBotAPI("MyAwesomeBotToken") if err != nil { diff --git a/configs.go b/configs.go index 925766f..3231407 100644 --- a/configs.go +++ b/configs.go @@ -1805,6 +1805,30 @@ func prepareInputMediaParam(inputMedia interface{}, idx int) interface{} { 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 } @@ -1847,6 +1871,38 @@ func prepareInputMediaFile(inputMedia interface{}, idx int) []RequestFile { 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 diff --git a/helpers.go b/helpers.go index 8557970..87b5d4a 100644 --- a/helpers.go +++ b/helpers.go @@ -201,7 +201,7 @@ func NewInputMediaAudio(media interface{}) InputMediaAudio { } // NewInputMediaDocument creates a new InputMediaDocument. -func NewInputMediaDocument(media string) InputMediaDocument { +func NewInputMediaDocument(media interface{}) InputMediaDocument { return InputMediaDocument{ BaseInputMedia: BaseInputMedia{ Type: "document", diff --git a/types.go b/types.go index 2eb3ad5..bba539d 100644 --- a/types.go +++ b/types.go @@ -1112,10 +1112,11 @@ type BotCommand struct { // BaseInputMedia is a base type for the InputMedia types. type BaseInputMedia struct { - Type string `json:"type"` - Media interface{} `json:"media"` - Caption string `json:"caption,omitempty"` - ParseMode string `json:"parse_mode,omitempty"` + Type string `json:"type"` + Media interface{} `json:"media"` + Caption string `json:"caption,omitempty"` + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + ParseMode string `json:"parse_mode,omitempty"` } // InputMediaPhoto is a photo to send as part of a media group. @@ -1144,6 +1145,7 @@ type InputMediaAnimation struct { // InputMediaAudio is a audio to send as part of a media group. type InputMediaAudio struct { BaseInputMedia + Thumb interface{} Duration int `json:"duration"` Performer string `json:"performer"` Title string `json:"title"` @@ -1152,6 +1154,8 @@ type InputMediaAudio struct { // InputMediaDocument is a audio to send as part of a media group. type InputMediaDocument struct { BaseInputMedia + Thumb interface{} + DisableContentTypeDetection bool `json:"disable_content_type_detection,omitempty"` } // Error is an error containing extra information returned by the Telegram API.