From e48e6416e7805cf7a96c33c6eb958b94131585e1 Mon Sep 17 00:00:00 2001 From: Syfaro Date: Mon, 7 Sep 2015 11:20:43 -0500 Subject: [PATCH] allow passing a []byte or io.Reader to upload functions, closes #20 --- bot.go | 4 +- helpers.go | 36 ++++++------- methods.go | 153 ++++++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 144 insertions(+), 49 deletions(-) diff --git a/bot.go b/bot.go index ef8c00f..a41f51f 100644 --- a/bot.go +++ b/bot.go @@ -1,7 +1,9 @@ // Package tgbotapi has bindings for interacting with the Telegram Bot API. package tgbotapi -import "net/http" +import ( + "net/http" +) // BotAPI has methods for interacting with all of Telegram's Bot API endpoints. type BotAPI struct { diff --git a/helpers.go b/helpers.go index d21230a..f3e5cd3 100644 --- a/helpers.go +++ b/helpers.go @@ -33,12 +33,12 @@ func NewForward(chatID int, fromChatID int, messageID int) ForwardConfig { // This requires a file on the local filesystem to upload to Telegram. // Perhaps set a ChatAction of ChatUploadPhoto while processing. // -// chatID is where to send it, filename is the path to the file. -func NewPhotoUpload(chatID int, filename string) PhotoConfig { +// chatID is where to send it, file is a string path to the file, or FileReader or FileBytes. +func NewPhotoUpload(chatID int, file interface{}) PhotoConfig { return PhotoConfig{ ChatID: chatID, UseExistingPhoto: false, - FilePath: filename, + File: file, } } @@ -58,12 +58,12 @@ func NewPhotoShare(chatID int, fileID string) PhotoConfig { // This requires a file on the local filesystem to upload to Telegram. // Perhaps set a ChatAction of ChatRecordAudio or ChatUploadAudio while processing. // -// chatID is where to send it, filename is the path to the file. -func NewAudioUpload(chatID int, filename string) AudioConfig { +// chatID is where to send it, file is a string path to the file, or FileReader or FileBytes. +func NewAudioUpload(chatID int, file interface{}) AudioConfig { return AudioConfig{ ChatID: chatID, UseExistingAudio: false, - FilePath: filename, + File: file, } } @@ -83,12 +83,12 @@ func NewAudioShare(chatID int, fileID string) AudioConfig { // This requires a file on the local filesystem to upload to Telegram. // Perhaps set a ChatAction of ChatUploadDocument while processing. // -// chatID is where to send it, filename is the path to the file. -func NewDocumentUpload(chatID int, filename string) DocumentConfig { +// chatID is where to send it, file is a string path to the file, or FileReader or FileBytes. +func NewDocumentUpload(chatID int, file interface{}) DocumentConfig { return DocumentConfig{ ChatID: chatID, UseExistingDocument: false, - FilePath: filename, + File: file, } } @@ -107,12 +107,12 @@ func NewDocumentShare(chatID int, fileID string) DocumentConfig { // NewStickerUpload creates a new sticker uploader. // This requires a file on the local filesystem to upload to Telegram. // -// chatID is where to send it, filename is the path to the file. -func NewStickerUpload(chatID int, filename string) StickerConfig { +// chatID is where to send it, file is a string path to the file, or FileReader or FileBytes. +func NewStickerUpload(chatID int, file interface{}) StickerConfig { return StickerConfig{ ChatID: chatID, UseExistingSticker: false, - FilePath: filename, + File: file, } } @@ -132,12 +132,12 @@ func NewStickerShare(chatID int, fileID string) StickerConfig { // This requires a file on the local filesystem to upload to Telegram. // Perhaps set a ChatAction of ChatRecordVideo or ChatUploadVideo while processing. // -// chatID is where to send it, filename is the path to the file. -func NewVideoUpload(chatID int, filename string) VideoConfig { +// chatID is where to send it, file is a string path to the file, or FileReader or FileBytes. +func NewVideoUpload(chatID int, file interface{}) VideoConfig { return VideoConfig{ ChatID: chatID, UseExistingVideo: false, - FilePath: filename, + File: file, } } @@ -157,12 +157,12 @@ func NewVideoShare(chatID int, fileID string) VideoConfig { // This requires a file on the local filesystem to upload to Telegram. // Perhaps set a ChatAction of ChatRecordVideo or ChatUploadVideo while processing. // -// chatID is where to send it, filename is the path to the file. -func NewVoiceUpload(chatID int, filename string) VoiceConfig { +// chatID is where to send it, file is a string path to the file, or FileReader or FileBytes. +func NewVoiceUpload(chatID int, file interface{}) VoiceConfig { return VoiceConfig{ ChatID: chatID, UseExistingVoice: false, - FilePath: filename, + File: file, } } diff --git a/methods.go b/methods.go index 40b82e2..7d88264 100644 --- a/methods.go +++ b/methods.go @@ -1,17 +1,18 @@ package tgbotapi import ( + "bytes" "encoding/json" "errors" "fmt" + "github.com/technoweenie/multipartstreamer" + "io" "io/ioutil" "log" "net/http" "net/url" "os" "strconv" - - "github.com/technoweenie/multipartstreamer" ) // Telegram constants @@ -62,6 +63,7 @@ type PhotoConfig struct { ReplyMarkup interface{} UseExistingPhoto bool FilePath string + File interface{} FileID string } @@ -75,6 +77,7 @@ type AudioConfig struct { ReplyMarkup interface{} UseExistingAudio bool FilePath string + File interface{} FileID string } @@ -85,6 +88,7 @@ type DocumentConfig struct { ReplyMarkup interface{} UseExistingDocument bool FilePath string + File interface{} FileID string } @@ -95,6 +99,7 @@ type StickerConfig struct { ReplyMarkup interface{} UseExistingSticker bool FilePath string + File interface{} FileID string } @@ -107,6 +112,7 @@ type VideoConfig struct { ReplyMarkup interface{} UseExistingVideo bool FilePath string + File interface{} FileID string } @@ -118,6 +124,7 @@ type VoiceConfig struct { ReplyMarkup interface{} UseExistingVoice bool FilePath string + File interface{} FileID string } @@ -156,6 +163,20 @@ type WebhookConfig struct { URL *url.URL } +// FileBytes contains information about a set of bytes to upload as a File. +type FileBytes struct { + Name string + Bytes []byte +} + +// FileReader contains information about a reader to upload as a File. +// 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 +} + // MakeRequest makes a request to a specific endpoint with our token. // All requests are POSTs because Telegram doesn't care, and it's easier. func (bot *BotAPI) MakeRequest(endpoint string, params url.Values) (APIResponse, error) { @@ -191,21 +212,45 @@ func (bot *BotAPI) MakeRequest(endpoint string, params url.Values) (APIResponse, // UploadFile makes a request to the API with a file. // // Requires the parameter to hold the file not be in the params. -func (bot *BotAPI) UploadFile(endpoint string, params map[string]string, fieldname string, filename string) (APIResponse, error) { - f, err := os.Open(filename) - if err != nil { - return APIResponse{}, err - } - defer f.Close() - - fi, err := os.Stat(filename) - if err != nil { - return APIResponse{}, err - } - +// File should be a string to a file path, a FileBytes struct, or a FileReader struct. +func (bot *BotAPI) UploadFile(endpoint string, params map[string]string, fieldname string, file interface{}) (APIResponse, error) { ms := multipartstreamer.New() ms.WriteFields(params) - ms.WriteReader(fieldname, f.Name(), fi.Size(), f) + + switch f := file.(type) { + case string: + 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 + } + + ms.WriteReader(fieldname, fileHandle.Name(), fi.Size(), fileHandle) + case FileBytes: + buf := bytes.NewBuffer(f.Bytes) + ms.WriteReader(fieldname, f.Name, int64(len(f.Bytes)), buf) + case FileReader: + if f.Size == -1 { + 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) + + break + } + + ms.WriteReader(fieldname, f.Name, f.Size, f.Reader) + default: + return APIResponse{}, errors.New("bad file type") + } req, err := http.NewRequest("POST", fmt.Sprintf(APIEndpoint, bot.Token, endpoint), nil) ms.SetupRequest(req) @@ -317,8 +362,9 @@ func (bot *BotAPI) ForwardMessage(config ForwardConfig) (Message, error) { // SendPhoto sends or uploads a photo to a chat. // -// Requires ChatID and FileID OR FilePath. +// Requires ChatID and FileID OR File. // Caption, ReplyToMessageID, and ReplyMarkup are optional. +// File should be either a string, FileBytes, or FileReader. func (bot *BotAPI) SendPhoto(config PhotoConfig) (Message, error) { if config.UseExistingPhoto { v := url.Values{} @@ -372,7 +418,14 @@ func (bot *BotAPI) SendPhoto(config PhotoConfig) (Message, error) { params["reply_markup"] = string(data) } - resp, err := bot.UploadFile("SendPhoto", params, "photo", config.FilePath) + var file interface{} + if config.FilePath == "" { + file = config.File + } else { + file = config.FilePath + } + + resp, err := bot.UploadFile("SendPhoto", params, "photo", file) if err != nil { return Message{}, err } @@ -390,13 +443,14 @@ func (bot *BotAPI) SendPhoto(config PhotoConfig) (Message, error) { // SendAudio sends or uploads an audio clip to a chat. // If using a file, the file must be in the .mp3 format. // -// when the fields title and performer are both empty -// and the mime-type of the file to be sent is not audio/mpeg, -// the file must be in an .ogg file encoded with OPUS. +// When the fields title and performer are both empty and +// the mime-type of the file to be sent is not audio/mpeg, +// the file must be an .ogg file encoded with OPUS. // You may use the tgutils.EncodeAudio func to assist you with this, if needed. // -// Requires ChatID and FileID OR FilePath. +// Requires ChatID and FileID OR File. // ReplyToMessageID and ReplyMarkup are optional. +// File should be either a string, FileBytes, or FileReader. func (bot *BotAPI) SendAudio(config AudioConfig) (Message, error) { if config.UseExistingAudio { v := url.Values{} @@ -463,7 +517,14 @@ func (bot *BotAPI) SendAudio(config AudioConfig) (Message, error) { params["title"] = config.Title } - resp, err := bot.UploadFile("sendAudio", params, "audio", config.FilePath) + var file interface{} + if config.FilePath == "" { + file = config.File + } else { + file = config.FilePath + } + + resp, err := bot.UploadFile("sendAudio", params, "audio", file) if err != nil { return Message{}, err } @@ -480,8 +541,9 @@ func (bot *BotAPI) SendAudio(config AudioConfig) (Message, error) { // SendDocument sends or uploads a document to a chat. // -// Requires ChatID and FileID OR FilePath. +// Requires ChatID and FileID OR File. // ReplyToMessageID and ReplyMarkup are optional. +// File should be either a string, FileBytes, or FileReader. func (bot *BotAPI) SendDocument(config DocumentConfig) (Message, error) { if config.UseExistingDocument { v := url.Values{} @@ -530,7 +592,14 @@ func (bot *BotAPI) SendDocument(config DocumentConfig) (Message, error) { params["reply_markup"] = string(data) } - resp, err := bot.UploadFile("sendDocument", params, "document", config.FilePath) + var file interface{} + if config.FilePath == "" { + file = config.File + } else { + file = config.FilePath + } + + resp, err := bot.UploadFile("sendDocument", params, "document", file) if err != nil { return Message{}, err } @@ -549,8 +618,9 @@ func (bot *BotAPI) SendDocument(config DocumentConfig) (Message, error) { // If using a file, the file must be encoded as an .ogg with OPUS. // You may use the tgutils.EncodeAudio func to assist you with this, if needed. // -// Requires ChatID and FileID OR FilePath. +// Requires ChatID and FileID OR File. // ReplyToMessageID and ReplyMarkup are optional. +// File should be either a string, FileBytes, or FileReader. func (bot *BotAPI) SendVoice(config VoiceConfig) (Message, error) { if config.UseExistingVoice { v := url.Values{} @@ -605,7 +675,14 @@ func (bot *BotAPI) SendVoice(config VoiceConfig) (Message, error) { params["reply_markup"] = string(data) } - resp, err := bot.UploadFile("SendVoice", params, "voice", config.FilePath) + var file interface{} + if config.FilePath == "" { + file = config.File + } else { + file = config.FilePath + } + + resp, err := bot.UploadFile("SendVoice", params, "voice", file) if err != nil { return Message{}, err } @@ -622,8 +699,9 @@ func (bot *BotAPI) SendVoice(config VoiceConfig) (Message, error) { // SendSticker sends or uploads a sticker to a chat. // -// Requires ChatID and FileID OR FilePath. +// Requires ChatID and FileID OR File. // ReplyToMessageID and ReplyMarkup are optional. +// File should be either a string, FileBytes, or FileReader. func (bot *BotAPI) SendSticker(config StickerConfig) (Message, error) { if config.UseExistingSticker { v := url.Values{} @@ -672,7 +750,14 @@ func (bot *BotAPI) SendSticker(config StickerConfig) (Message, error) { params["reply_markup"] = string(data) } - resp, err := bot.UploadFile("sendSticker", params, "sticker", config.FilePath) + var file interface{} + if config.FilePath == "" { + file = config.File + } else { + file = config.FilePath + } + + resp, err := bot.UploadFile("sendSticker", params, "sticker", file) if err != nil { return Message{}, err } @@ -689,8 +774,9 @@ func (bot *BotAPI) SendSticker(config StickerConfig) (Message, error) { // SendVideo sends or uploads a video to a chat. // -// Requires ChatID and FileID OR FilePath. +// Requires ChatID and FileID OR File. // ReplyToMessageID and ReplyMarkup are optional. +// File should be either a string, FileBytes, or FileReader. func (bot *BotAPI) SendVideo(config VideoConfig) (Message, error) { if config.UseExistingVideo { v := url.Values{} @@ -745,7 +831,14 @@ func (bot *BotAPI) SendVideo(config VideoConfig) (Message, error) { params["reply_markup"] = string(data) } - resp, err := bot.UploadFile("sendVideo", params, "video", config.FilePath) + var file interface{} + if config.FilePath == "" { + file = config.File + } else { + file = config.FilePath + } + + resp, err := bot.UploadFile("sendVideo", params, "video", file) if err != nil { return Message{}, err }