diff --git a/bot.go b/bot.go index 4590eab..2f257e1 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" ) // HTTPClient is the type needed for the bot to perform HTTP requests. @@ -96,7 +95,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) } @@ -107,14 +106,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 { @@ -128,14 +127,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. @@ -162,86 +161,100 @@ 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 { + w.CloseWithError(err) + return + } } - 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 { + w.CloseWithError(err) + return + } + 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 { + w.CloseWithError(err) + return + } - 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 { + w.CloseWithError(err) + return + } - break + 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 + } + + 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 + } } - - 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 { @@ -255,13 +268,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 @@ -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 @@ 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() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return nil, err } @@ -354,9 +396,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 } @@ -371,9 +411,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 } @@ -392,9 +430,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 } @@ -510,7 +546,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") } } @@ -525,9 +561,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 } @@ -543,9 +577,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 } @@ -558,9 +590,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 } @@ -573,9 +603,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 } @@ -588,9 +616,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 } @@ -603,9 +629,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 } @@ -618,9 +642,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 } @@ -633,12 +655,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 } @@ -653,12 +670,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 c465909..5ee738d 100644 --- a/bot_test.go +++ b/bot_test.go @@ -127,7 +127,7 @@ func TestCopyMessage(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) @@ -142,7 +142,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) @@ -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 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) @@ -182,7 +182,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) @@ -198,7 +198,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) @@ -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 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) @@ -239,7 +239,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.Thumb = "tests/image.jpg" _, err := bot.Send(msg) if err != nil { @@ -250,7 +262,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 { @@ -261,12 +273,10 @@ 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" - msg.MimeType = "audio/mpeg" - msg.FileSize = 688 _, err := bot.Send(msg) if err != nil { @@ -277,7 +287,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" @@ -292,7 +302,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) @@ -304,7 +314,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) @@ -346,7 +356,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" @@ -360,7 +370,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" @@ -374,7 +384,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) @@ -387,7 +397,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) @@ -400,7 +410,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) @@ -412,7 +422,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) @@ -424,7 +434,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, @@ -439,7 +449,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, @@ -583,12 +593,13 @@ func TestSetWebhookWithoutCert(t *testing.T) { bot.Request(DeleteWebhookConfig{}) } -func TestSendWithMediaGroup(t *testing.T) { +func TestSendWithMediaGroupPhotoVideo(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"), + 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) @@ -597,11 +608,55 @@ func TestSendWithMediaGroup(t *testing.T) { } 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 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)) } } @@ -899,3 +954,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 b08c3eb..595c1e5 100644 --- a/configs.go +++ b/configs.go @@ -1,6 +1,7 @@ package tgbotapi import ( + "fmt" "io" "net/url" "strconv" @@ -103,12 +104,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 } // LogOutConfig is a request to log out of the cloud Bot API server. @@ -164,28 +172,11 @@ 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 + 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 @@ func (config CopyMessageConfig) method() string { // PhotoConfig contains information about a SendPhoto request. type PhotoConfig struct { BaseFile + Thumb interface{} Caption string ParseMode string CaptionEntities []MessageEntity @@ -307,7 +299,6 @@ func (config PhotoConfig) params() (Params, error) { 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) @@ -315,17 +306,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 CaptionEntities []MessageEntity @@ -340,7 +344,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) @@ -351,17 +354,30 @@ func (config AudioConfig) params() (Params, error) { 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 @@ 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) params.AddBool("disable_content_type_detection", config.DisableContentTypeDetection) @@ -379,38 +394,50 @@ 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 } 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 @@ func (config VideoConfig) params() (Params, error) { 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 @@ 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 CaptionEntities []MessageEntity @@ -457,7 +496,6 @@ func (config AnimationConfig) params() (Params, error) { 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 @@ func (config AnimationConfig) params() (Params, error) { return params, err } -func (config AnimationConfig) name() string { - return "animation" -} - 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 @@ 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) 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 @@ func (config VoiceConfig) params() (Params, error) { 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,14 +585,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 @@ -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 (EditMessageMediaConfig) method() string { 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 @@ func (config WebhookConfig) params() (Params, error) { 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) files() []RequestFile { + if config.Certificate != nil { + return []RequestFile{{ + Name: "certificate", + File: config.Certificate, + }} + } -func (config WebhookConfig) getFile() interface{} { - return config.Certificate -} - -func (config WebhookConfig) useExistingFile() bool { - return config.URL != nil + return nil } // DeleteWebhookConfig is a helper to delete a webhook. @@ -1023,14 +1103,17 @@ 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. +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"` @@ -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 @@ 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. @@ -1756,12 +1827,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) @@ -1771,26 +1836,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. @@ -1814,29 +1871,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. @@ -1892,24 +1944,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) 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 @@ func (config SetMyCommandsConfig) params() (Params, error) { 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) + } -func (config DiceConfig) method() string { - return "sendDice" -} + return m + case InputMediaVideo: + switch m.Media.(type) { + case string, FileBytes, FileReader: + m.Media = fmt.Sprintf("attach://file-%d", idx) + } -func (config DiceConfig) params() (Params, error) { - params, err := config.BaseChat.params() - if err != nil { - return params, err + 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 } - params.AddNonEmpty("emoji", config.Emoji) - - return params, err + 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, + }) + } + 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 +} + +// 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/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 e98ae06..6684c1d 100644 --- a/helpers.go +++ b/helpers.go @@ -64,259 +64,105 @@ func NewCopyMessage(chatID int64, fromChatID int64, messageID int) CopyMessageCo } } -// 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, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } -// NewDocumentShare shares an existing document. -// You may use this to reshare an existing document without -// reuploading it. -// -// chatID is where to send it, fileID is the ID of the document -// already uploaded. -func NewDocumentShare(chatID int64, fileID string) DocumentConfig { - return DocumentConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, - }, - } -} - -// 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, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } -// 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 { +// NewVideo creates a new sendVideo request. +func NewVideo(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 { - return VideoConfig{ +// NewAnimation creates a new sendAnimation request. +func NewAnimation(chatID int64, file interface{}) AnimationConfig { + return AnimationConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } -// NewAnimationUpload creates a new animation 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 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 { +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 { +// NewVoice creates a new sendVoice request. +func NewVoice(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 { - return VoiceConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } @@ -331,7 +177,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", @@ -341,7 +187,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", @@ -351,7 +197,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", @@ -361,7 +207,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", @@ -371,7 +217,7 @@ func NewInputMediaAudio(media string) InputMediaAudio { } // NewInputMediaDocument creates a new InputMediaDocument. -func NewInputMediaDocument(media string) InputMediaDocument { +func NewInputMediaDocument(media interface{}) InputMediaDocument { return InputMediaDocument{ BaseInputMedia: BaseInputMedia{ Type: "document", @@ -889,37 +735,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{ diff --git a/types.go b/types.go index e6810ae..cb931d8 100644 --- a/types.go +++ b/types.go @@ -180,12 +180,6 @@ func (u *User) String() string { 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 @@ type BaseInputMedia struct { // pass an HTTP URL for Telegram to get a file from the Internet, // or pass β€œattach://” to upload a new one // using multipart/form-data under 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 @@ type InputMediaPhoto struct { // 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 @@ type InputMediaVideo struct { // 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 @@ type InputMediaAnimation struct { // 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 @@ type InputMediaAudio struct { // 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 diff --git a/types_test.go b/types_test.go index ba5f457..7a4b196 100644 --- a/types_test.go +++ b/types_test.go @@ -339,3 +339,24 @@ 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) + _ Fileable = (*MediaGroupConfig)(nil) + _ Fileable = (*WebhookConfig)(nil) + _ Fileable = (*SetStickerSetThumbConfig)(nil) +)