From 13a8bd025c70a5f1d0700f41c13ea339e073d3d0 Mon Sep 17 00:00:00 2001 From: Gleb Sinyavsky Date: Fri, 20 Nov 2015 13:42:26 +0300 Subject: [PATCH] Refactorings --- .gitignore | 1 + bot.go | 978 ++++++++++++++++++++++++++++++++++++++++++++ bot_test.go | 5 +- configs.go | 189 +++++++++ helpers.go | 36 +- methods.go | 1125 --------------------------------------------------- updates.go | 33 -- webhook.go | 21 - 8 files changed, 1189 insertions(+), 1199 deletions(-) create mode 100644 .gitignore create mode 100644 configs.go delete mode 100644 methods.go delete mode 100644 updates.go delete mode 100644 webhook.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..62c8935 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea/ \ No newline at end of file diff --git a/bot.go b/bot.go index a41f51f..33c8ade 100644 --- a/bot.go +++ b/bot.go @@ -2,7 +2,18 @@ package tgbotapi import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "github.com/technoweenie/multipartstreamer" + "io/ioutil" + "log" "net/http" + "net/url" + "os" + "strconv" + "time" ) // BotAPI has methods for interacting with all of Telegram's Bot API endpoints. @@ -37,3 +48,970 @@ func NewBotAPIWithClient(token string, client *http.Client) (*BotAPI, error) { return bot, nil } + +// 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) { + resp, err := bot.Client.PostForm(fmt.Sprintf(APIEndpoint, bot.Token, endpoint), params) + if err != nil { + return APIResponse{}, err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusForbidden { + return APIResponse{}, errors.New(APIForbidden) + } + + bytes, err := ioutil.ReadAll(resp.Body) + if err != nil { + return APIResponse{}, err + } + + if bot.Debug { + log.Println(endpoint, string(bytes)) + } + + var apiResp APIResponse + json.Unmarshal(bytes, &apiResp) + + if !apiResp.Ok { + return APIResponse{}, errors.New(apiResp.Description) + } + + return apiResp, 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, 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) + + 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) + if err != nil { + return APIResponse{}, err + } + + res, err := bot.Client.Do(req) + if err != nil { + return APIResponse{}, err + } + defer res.Body.Close() + + bytes, err := ioutil.ReadAll(res.Body) + if err != nil { + return APIResponse{}, err + } + + if bot.Debug { + log.Println(string(bytes[:])) + } + + var apiResp APIResponse + json.Unmarshal(bytes, &apiResp) + + if !apiResp.Ok { + return APIResponse{}, errors.New(apiResp.Description) + } + + return apiResp, nil +} + +// GetMe fetches the currently authenticated bot. +// +// There are no parameters for this method. +func (bot *BotAPI) GetMe() (User, error) { + resp, err := bot.MakeRequest("getMe", nil) + if err != nil { + return User{}, err + } + + var user User + json.Unmarshal(resp.Result, &user) + + if bot.Debug { + log.Printf("getMe: %+v\n", user) + } + + return user, nil +} + +func (bot *BotAPI) Send(c Chattable) error { + return nil +} + +// SendMessage sends a Message to a chat. +// +// Requires ChatID and Text. +// DisableWebPagePreview, ReplyToMessageID, and ReplyMarkup are optional. +func (bot *BotAPI) SendMessage(config MessageConfig) (Message, error) { + v := url.Values{} + if config.ChannelUsername != "" { + v.Add("chat_id", config.ChannelUsername) + } else { + v.Add("chat_id", strconv.Itoa(config.ChatID)) + } + v.Add("text", config.Text) + v.Add("disable_web_page_preview", strconv.FormatBool(config.DisableWebPagePreview)) + if config.ParseMode != "" { + v.Add("parse_mode", config.ParseMode) + } + if config.ReplyToMessageID != 0 { + v.Add("reply_to_message_id", strconv.Itoa(config.ReplyToMessageID)) + } + if config.ReplyMarkup != nil { + data, err := json.Marshal(config.ReplyMarkup) + if err != nil { + return Message{}, err + } + + v.Add("reply_markup", string(data)) + } + + resp, err := bot.MakeRequest("SendMessage", v) + if err != nil { + return Message{}, err + } + + var message Message + json.Unmarshal(resp.Result, &message) + + if bot.Debug { + log.Printf("SendMessage req : %+v\n", v) + log.Printf("SendMessage resp: %+v\n", message) + } + + return message, nil +} + +// ForwardMessage forwards a message from one chat to another. +// +// Requires ChatID (destination), FromChatID (source), and MessageID. +func (bot *BotAPI) ForwardMessage(config ForwardConfig) (Message, error) { + v := url.Values{} + if config.ChannelUsername != "" { + v.Add("chat_id", config.ChannelUsername) + } else { + v.Add("chat_id", strconv.Itoa(config.ChatID)) + } + if config.FromChannelUsername != "" { + v.Add("chat_id", config.FromChannelUsername) + } else { + v.Add("chat_id", strconv.Itoa(config.FromChatID)) + } + v.Add("message_id", strconv.Itoa(config.MessageID)) + + resp, err := bot.MakeRequest("forwardMessage", v) + if err != nil { + return Message{}, err + } + + var message Message + json.Unmarshal(resp.Result, &message) + + if bot.Debug { + log.Printf("forwardMessage req : %+v\n", v) + log.Printf("forwardMessage resp: %+v\n", message) + } + + return message, nil +} + +// SendPhoto sends or uploads a photo to a chat. +// +// 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{} + if config.ChannelUsername != "" { + v.Add("chat_id", config.ChannelUsername) + } else { + v.Add("chat_id", strconv.Itoa(config.ChatID)) + } + v.Add("photo", config.FileID) + if config.Caption != "" { + v.Add("caption", config.Caption) + } + if config.ReplyToMessageID != 0 { + v.Add("reply_to_message_id", strconv.Itoa(config.ReplyToMessageID)) + } + if config.ReplyMarkup != nil { + data, err := json.Marshal(config.ReplyMarkup) + if err != nil { + return Message{}, err + } + + v.Add("reply_markup", string(data)) + } + + resp, err := bot.MakeRequest("SendPhoto", v) + if err != nil { + return Message{}, err + } + + var message Message + json.Unmarshal(resp.Result, &message) + + if bot.Debug { + log.Printf("SendPhoto req : %+v\n", v) + log.Printf("SendPhoto resp: %+v\n", message) + } + + return message, nil + } + + params := make(map[string]string) + if config.ChannelUsername != "" { + params["chat_id"] = config.ChannelUsername + } else { + params["chat_id"] = strconv.Itoa(config.ChatID) + } + if config.Caption != "" { + params["caption"] = config.Caption + } + if config.ReplyToMessageID != 0 { + params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID) + } + if config.ReplyMarkup != nil { + data, err := json.Marshal(config.ReplyMarkup) + if err != nil { + return Message{}, err + } + + params["reply_markup"] = string(data) + } + + 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 + } + + var message Message + json.Unmarshal(resp.Result, &message) + + if bot.Debug { + log.Printf("SendPhoto resp: %+v\n", message) + } + + return message, nil +} + +// 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 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 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{} + if config.ChannelUsername != "" { + v.Add("chat_id", config.ChannelUsername) + } else { + v.Add("chat_id", strconv.Itoa(config.ChatID)) + } + v.Add("audio", config.FileID) + if config.ReplyToMessageID != 0 { + v.Add("reply_to_message_id", strconv.Itoa(config.ReplyToMessageID)) + } + if config.Duration != 0 { + v.Add("duration", strconv.Itoa(config.Duration)) + } + if config.ReplyMarkup != nil { + data, err := json.Marshal(config.ReplyMarkup) + if err != nil { + return Message{}, err + } + + v.Add("reply_markup", string(data)) + } + if config.Performer != "" { + v.Add("performer", config.Performer) + } + if config.Title != "" { + v.Add("title", config.Title) + } + + resp, err := bot.MakeRequest("sendAudio", v) + if err != nil { + return Message{}, err + } + + var message Message + json.Unmarshal(resp.Result, &message) + + if bot.Debug { + log.Printf("sendAudio req : %+v\n", v) + log.Printf("sendAudio resp: %+v\n", message) + } + + return message, nil + } + + params := make(map[string]string) + + if config.ChannelUsername != "" { + params["chat_id"] = config.ChannelUsername + } else { + params["chat_id"] = strconv.Itoa(config.ChatID) + } + if config.ReplyToMessageID != 0 { + params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID) + } + if config.Duration != 0 { + params["duration"] = strconv.Itoa(config.Duration) + } + if config.ReplyMarkup != nil { + data, err := json.Marshal(config.ReplyMarkup) + if err != nil { + return Message{}, err + } + + params["reply_markup"] = string(data) + } + if config.Performer != "" { + params["performer"] = config.Performer + } + if config.Title != "" { + params["title"] = config.Title + } + + 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 + } + + var message Message + json.Unmarshal(resp.Result, &message) + + if bot.Debug { + log.Printf("sendAudio resp: %+v\n", message) + } + + return message, nil +} + +// SendDocument sends or uploads a document to a chat. +// +// 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{} + if config.ChannelUsername != "" { + v.Add("chat_id", config.ChannelUsername) + } else { + v.Add("chat_id", strconv.Itoa(config.ChatID)) + } + v.Add("document", config.FileID) + if config.ReplyToMessageID != 0 { + v.Add("reply_to_message_id", strconv.Itoa(config.ReplyToMessageID)) + } + if config.ReplyMarkup != nil { + data, err := json.Marshal(config.ReplyMarkup) + if err != nil { + return Message{}, err + } + + v.Add("reply_markup", string(data)) + } + + resp, err := bot.MakeRequest("sendDocument", v) + if err != nil { + return Message{}, err + } + + var message Message + json.Unmarshal(resp.Result, &message) + + if bot.Debug { + log.Printf("sendDocument req : %+v\n", v) + log.Printf("sendDocument resp: %+v\n", message) + } + + return message, nil + } + + params := make(map[string]string) + + if config.ChannelUsername != "" { + params["chat_id"] = config.ChannelUsername + } else { + params["chat_id"] = strconv.Itoa(config.ChatID) + } + if config.ReplyToMessageID != 0 { + params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID) + } + if config.ReplyMarkup != nil { + data, err := json.Marshal(config.ReplyMarkup) + if err != nil { + return Message{}, err + } + + params["reply_markup"] = string(data) + } + + 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 + } + + var message Message + json.Unmarshal(resp.Result, &message) + + if bot.Debug { + log.Printf("sendDocument resp: %+v\n", message) + } + + return message, nil +} + +// SendVoice sends or uploads a playable voice to a chat. +// 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 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{} + if config.ChannelUsername != "" { + v.Add("chat_id", config.ChannelUsername) + } else { + v.Add("chat_id", strconv.Itoa(config.ChatID)) + } + v.Add("voice", config.FileID) + if config.ReplyToMessageID != 0 { + v.Add("reply_to_message_id", strconv.Itoa(config.ReplyToMessageID)) + } + if config.Duration != 0 { + v.Add("duration", strconv.Itoa(config.Duration)) + } + if config.ReplyMarkup != nil { + data, err := json.Marshal(config.ReplyMarkup) + if err != nil { + return Message{}, err + } + + v.Add("reply_markup", string(data)) + } + + resp, err := bot.MakeRequest("sendVoice", v) + if err != nil { + return Message{}, err + } + + var message Message + json.Unmarshal(resp.Result, &message) + + if bot.Debug { + log.Printf("SendVoice req : %+v\n", v) + log.Printf("SendVoice resp: %+v\n", message) + } + + return message, nil + } + + params := make(map[string]string) + + if config.ChannelUsername != "" { + params["chat_id"] = config.ChannelUsername + } else { + params["chat_id"] = strconv.Itoa(config.ChatID) + } + if config.ReplyToMessageID != 0 { + params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID) + } + if config.Duration != 0 { + params["duration"] = strconv.Itoa(config.Duration) + } + if config.ReplyMarkup != nil { + data, err := json.Marshal(config.ReplyMarkup) + if err != nil { + return Message{}, err + } + + params["reply_markup"] = string(data) + } + + 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 + } + + var message Message + json.Unmarshal(resp.Result, &message) + + if bot.Debug { + log.Printf("SendVoice resp: %+v\n", message) + } + + return message, nil +} + +// SendSticker sends or uploads a sticker to a chat. +// +// 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{} + if config.ChannelUsername != "" { + v.Add("chat_id", config.ChannelUsername) + } else { + v.Add("chat_id", strconv.Itoa(config.ChatID)) + } + v.Add("sticker", config.FileID) + if config.ReplyToMessageID != 0 { + v.Add("reply_to_message_id", strconv.Itoa(config.ReplyToMessageID)) + } + if config.ReplyMarkup != nil { + data, err := json.Marshal(config.ReplyMarkup) + if err != nil { + return Message{}, err + } + + v.Add("reply_markup", string(data)) + } + + resp, err := bot.MakeRequest("sendSticker", v) + if err != nil { + return Message{}, err + } + + var message Message + json.Unmarshal(resp.Result, &message) + + if bot.Debug { + log.Printf("sendSticker req : %+v\n", v) + log.Printf("sendSticker resp: %+v\n", message) + } + + return message, nil + } + + params := make(map[string]string) + + if config.ChannelUsername != "" { + params["chat_id"] = config.ChannelUsername + } else { + params["chat_id"] = strconv.Itoa(config.ChatID) + } + if config.ReplyToMessageID != 0 { + params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID) + } + if config.ReplyMarkup != nil { + data, err := json.Marshal(config.ReplyMarkup) + if err != nil { + return Message{}, err + } + + params["reply_markup"] = string(data) + } + + 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 + } + + var message Message + json.Unmarshal(resp.Result, &message) + + if bot.Debug { + log.Printf("sendSticker resp: %+v\n", message) + } + + return message, nil +} + +// SendVideo sends or uploads a video to a chat. +// +// 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{} + if config.ChannelUsername != "" { + v.Add("chat_id", config.ChannelUsername) + } else { + v.Add("chat_id", strconv.Itoa(config.ChatID)) + } + v.Add("video", config.FileID) + if config.ReplyToMessageID != 0 { + v.Add("reply_to_message_id", strconv.Itoa(config.ReplyToMessageID)) + } + if config.Duration != 0 { + v.Add("duration", strconv.Itoa(config.Duration)) + } + if config.Caption != "" { + v.Add("caption", config.Caption) + } + if config.ReplyMarkup != nil { + data, err := json.Marshal(config.ReplyMarkup) + if err != nil { + return Message{}, err + } + + v.Add("reply_markup", string(data)) + } + + resp, err := bot.MakeRequest("sendVideo", v) + if err != nil { + return Message{}, err + } + + var message Message + json.Unmarshal(resp.Result, &message) + + if bot.Debug { + log.Printf("sendVideo req : %+v\n", v) + log.Printf("sendVideo resp: %+v\n", message) + } + + return message, nil + } + + params := make(map[string]string) + + if config.ChannelUsername != "" { + params["chat_id"] = config.ChannelUsername + } else { + params["chat_id"] = strconv.Itoa(config.ChatID) + } + if config.ReplyToMessageID != 0 { + params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID) + } + if config.ReplyMarkup != nil { + data, err := json.Marshal(config.ReplyMarkup) + if err != nil { + return Message{}, err + } + + params["reply_markup"] = string(data) + } + + 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 + } + + var message Message + json.Unmarshal(resp.Result, &message) + + if bot.Debug { + log.Printf("sendVideo resp: %+v\n", message) + } + + return message, nil +} + +// SendLocation sends a location to a chat. +// +// Requires ChatID, Latitude, and Longitude. +// ReplyToMessageID and ReplyMarkup are optional. +func (bot *BotAPI) SendLocation(config LocationConfig) (Message, error) { + v := url.Values{} + if config.ChannelUsername != "" { + v.Add("chat_id", config.ChannelUsername) + } else { + v.Add("chat_id", strconv.Itoa(config.ChatID)) + } + v.Add("latitude", strconv.FormatFloat(config.Latitude, 'f', 6, 64)) + v.Add("longitude", strconv.FormatFloat(config.Longitude, 'f', 6, 64)) + if config.ReplyToMessageID != 0 { + v.Add("reply_to_message_id", strconv.Itoa(config.ReplyToMessageID)) + } + if config.ReplyMarkup != nil { + data, err := json.Marshal(config.ReplyMarkup) + if err != nil { + return Message{}, err + } + + v.Add("reply_markup", string(data)) + } + + resp, err := bot.MakeRequest("sendLocation", v) + if err != nil { + return Message{}, err + } + + var message Message + json.Unmarshal(resp.Result, &message) + + if bot.Debug { + log.Printf("sendLocation req : %+v\n", v) + log.Printf("sendLocation resp: %+v\n", message) + } + + return message, nil +} + +// SendChatAction sets a current action in a chat. +// +// Requires ChatID and a valid Action (see Chat constants). +func (bot *BotAPI) SendChatAction(config ChatActionConfig) error { + v := url.Values{} + if config.ChannelUsername != "" { + v.Add("chat_id", config.ChannelUsername) + } else { + v.Add("chat_id", strconv.Itoa(config.ChatID)) + } + v.Add("action", config.Action) + + _, err := bot.MakeRequest("sendChatAction", v) + if err != nil { + return err + } + + return nil +} + +// GetUserProfilePhotos gets a user's profile photos. +// +// Requires UserID. +// Offset and Limit are optional. +func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) { + v := url.Values{} + v.Add("user_id", strconv.Itoa(config.UserID)) + if config.Offset != 0 { + v.Add("offset", strconv.Itoa(config.Offset)) + } + if config.Limit != 0 { + v.Add("limit", strconv.Itoa(config.Limit)) + } + + resp, err := bot.MakeRequest("getUserProfilePhotos", v) + if err != nil { + return UserProfilePhotos{}, err + } + + var profilePhotos UserProfilePhotos + json.Unmarshal(resp.Result, &profilePhotos) + + if bot.Debug { + log.Printf("getUserProfilePhotos req : %+v\n", v) + log.Printf("getUserProfilePhotos resp: %+v\n", profilePhotos) + } + + return profilePhotos, nil +} + +// GetFile returns a file_id required to download a file. +// +// Requires FileID. +func (bot *BotAPI) GetFile(config FileConfig) (File, error) { + v := url.Values{} + v.Add("file_id", config.FileID) + + resp, err := bot.MakeRequest("getFile", v) + if err != nil { + return File{}, err + } + + var file File + json.Unmarshal(resp.Result, &file) + + if bot.Debug { + log.Printf("getFile req : %+v\n", v) + log.Printf("getFile resp: %+v\n", file) + } + + return file, nil +} + +// GetUpdates fetches updates. +// If a WebHook is set, this will not return any data! +// +// Offset, Limit, and Timeout are optional. +// To not get old items, set Offset to one higher than the previous item. +// Set Timeout to a large number to reduce requests and get responses instantly. +func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) { + v := url.Values{} + if config.Offset > 0 { + v.Add("offset", strconv.Itoa(config.Offset)) + } + if config.Limit > 0 { + v.Add("limit", strconv.Itoa(config.Limit)) + } + if config.Timeout > 0 { + v.Add("timeout", strconv.Itoa(config.Timeout)) + } + + resp, err := bot.MakeRequest("getUpdates", v) + if err != nil { + return []Update{}, err + } + + var updates []Update + json.Unmarshal(resp.Result, &updates) + + if bot.Debug { + log.Printf("getUpdates: %+v\n", updates) + } + + return updates, nil +} + +// SetWebhook sets a webhook. +// If this is set, GetUpdates will not get any data! +// +// Requires URL OR to set Clear to true. +func (bot *BotAPI) SetWebhook(config WebhookConfig) (APIResponse, error) { + if config.Certificate == nil { + v := url.Values{} + if !config.Clear { + v.Add("url", config.URL.String()) + } + + return bot.MakeRequest("setWebhook", v) + } + + params := make(map[string]string) + params["url"] = config.URL.String() + + resp, err := bot.UploadFile("setWebhook", params, "certificate", config.Certificate) + if err != nil { + return APIResponse{}, err + } + + var apiResp APIResponse + json.Unmarshal(resp.Result, &apiResp) + + if bot.Debug { + log.Printf("setWebhook resp: %+v\n", apiResp) + } + + return apiResp, nil +} + +// UpdatesChan starts a channel for getting updates. +func (bot *BotAPI) UpdatesChan(config UpdateConfig) error { + bot.Updates = make(chan Update, 100) + + go func() { + for { + updates, err := bot.GetUpdates(config) + if err != nil { + log.Println(err) + log.Println("Failed to get updates, retrying in 3 seconds...") + time.Sleep(time.Second * 3) + + continue + } + + for _, update := range updates { + if update.UpdateID >= config.Offset { + config.Offset = update.UpdateID + 1 + bot.Updates <- update + } + } + } + }() + + return nil +} + +// ListenForWebhook registers a http handler for a webhook. +func (bot *BotAPI) ListenForWebhook(pattern string) { + bot.Updates = make(chan Update, 100) + + http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) { + bytes, _ := ioutil.ReadAll(r.Body) + + var update Update + json.Unmarshal(bytes, &update) + + bot.Updates <- update + }) +} diff --git a/bot_test.go b/bot_test.go index 6f56565..e62afee 100644 --- a/bot_test.go +++ b/bot_test.go @@ -1,7 +1,7 @@ package tgbotapi_test import ( - "github.com/syfaro/telegram-bot-api" + "github.com/zhulik/telegram-bot-api" "log" "os" "testing" @@ -20,7 +20,8 @@ func TestMain(m *testing.M) { func TestNewBotAPI_notoken(t *testing.T) { _, err := tgbotapi.NewBotAPI("") - if err.Error() != tgbotapi.APIForbidden { + if err == nil { + log.Println(err.Error()) t.Fail() } } diff --git a/configs.go b/configs.go new file mode 100644 index 0000000..7c5403e --- /dev/null +++ b/configs.go @@ -0,0 +1,189 @@ +package tgbotapi + +import ( + "io" + "net/url" +) + +// Telegram constants +const ( + // APIEndpoint is the endpoint for all API methods, with formatting for Sprintf + APIEndpoint = "https://api.telegram.org/bot%s/%s" + // FileEndpoint is the endpoint for downloading a file from Telegram + FileEndpoint = "https://api.telegram.org/file/bot%s/%s" +) + +// Constant values for ChatActions +const ( + ChatTyping = "typing" + ChatUploadPhoto = "upload_photo" + ChatRecordVideo = "record_video" + ChatUploadVideo = "upload_video" + ChatRecordAudio = "record_audio" + ChatUploadAudio = "upload_audio" + ChatUploadDocument = "upload_document" + ChatFindLocation = "find_location" +) + +// API errors +const ( + // APIForbidden happens when a token is bad + APIForbidden = "forbidden" +) + +// Constant values for ParseMode in MessageConfig +const ( + ModeMarkdown = "Markdown" +) + +// Base struct for all chat event(Message, Photo and so on) +type Chattable struct { + ChatID int + ChannelUsername string +} + +// MessageConfig contains information about a SendMessage request. +type MessageConfig struct { + Chattable + Text string + ParseMode string + DisableWebPagePreview bool + ReplyToMessageID int + ReplyMarkup interface{} +} + +// ForwardConfig contains information about a ForwardMessage request. +type ForwardConfig struct { + Chattable + FromChatID int + FromChannelUsername string + MessageID int +} + +// PhotoConfig contains information about a SendPhoto request. +type PhotoConfig struct { + Chattable + Caption string + ReplyToMessageID int + ReplyMarkup interface{} + UseExistingPhoto bool + FilePath string + File interface{} + FileID string +} + +// AudioConfig contains information about a SendAudio request. +type AudioConfig struct { + Chattable + Duration int + Performer string + Title string + ReplyToMessageID int + ReplyMarkup interface{} + UseExistingAudio bool + FilePath string + File interface{} + FileID string +} + +// DocumentConfig contains information about a SendDocument request. +type DocumentConfig struct { + Chattable + ReplyToMessageID int + ReplyMarkup interface{} + UseExistingDocument bool + FilePath string + File interface{} + FileID string +} + +// StickerConfig contains information about a SendSticker request. +type StickerConfig struct { + Chattable + ReplyToMessageID int + ReplyMarkup interface{} + UseExistingSticker bool + FilePath string + File interface{} + FileID string +} + +// VideoConfig contains information about a SendVideo request. +type VideoConfig struct { + Chattable + Duration int + Caption string + ReplyToMessageID int + ReplyMarkup interface{} + UseExistingVideo bool + FilePath string + File interface{} + FileID string +} + +// VoiceConfig contains information about a SendVoice request. +type VoiceConfig struct { + Chattable + Duration int + ReplyToMessageID int + ReplyMarkup interface{} + UseExistingVoice bool + FilePath string + File interface{} + FileID string +} + +// LocationConfig contains information about a SendLocation request. +type LocationConfig struct { + Chattable + Latitude float64 + Longitude float64 + ReplyToMessageID int + ReplyMarkup interface{} +} + +// ChatActionConfig contains information about a SendChatAction request. +type ChatActionConfig struct { + Chattable + Action string +} + +// UserProfilePhotosConfig contains information about a GetUserProfilePhotos request. +type UserProfilePhotosConfig struct { + UserID int + Offset int + Limit int +} + +// FileConfig has information about a file hosted on Telegram +type FileConfig struct { + FileID string +} + +// UpdateConfig contains information about a GetUpdates request. +type UpdateConfig struct { + Offset int + Limit int + Timeout int +} + +// WebhookConfig contains information about a SetWebhook request. +type WebhookConfig struct { + Clear bool + URL *url.URL + Certificate interface{} +} + +// 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 +} diff --git a/helpers.go b/helpers.go index 90cc312..50e2497 100644 --- a/helpers.go +++ b/helpers.go @@ -10,8 +10,8 @@ import ( // chatID is where to send it, text is the message text. func NewMessage(chatID int, text string) MessageConfig { return MessageConfig{ - ChatID: chatID, - Text: text, + Chattable: Chattable{ChatID: chatID}, + Text: text, DisableWebPagePreview: false, ReplyToMessageID: 0, } @@ -23,7 +23,7 @@ func NewMessage(chatID int, text string) MessageConfig { // and messageID is the ID of the original message. func NewForward(chatID int, fromChatID int, messageID int) ForwardConfig { return ForwardConfig{ - ChatID: chatID, + Chattable: Chattable{ChatID: chatID}, FromChatID: fromChatID, MessageID: messageID, } @@ -36,7 +36,7 @@ func NewForward(chatID int, fromChatID int, messageID int) ForwardConfig { // 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, + Chattable: Chattable{ChatID: chatID}, UseExistingPhoto: false, File: file, } @@ -48,7 +48,7 @@ func NewPhotoUpload(chatID int, file interface{}) PhotoConfig { // chatID is where to send it, fileID is the ID of the file already uploaded. func NewPhotoShare(chatID int, fileID string) PhotoConfig { return PhotoConfig{ - ChatID: chatID, + Chattable: Chattable{ChatID: chatID}, UseExistingPhoto: true, FileID: fileID, } @@ -61,7 +61,7 @@ func NewPhotoShare(chatID int, fileID string) PhotoConfig { // 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, + Chattable: Chattable{ChatID: chatID}, UseExistingAudio: false, File: file, } @@ -73,7 +73,7 @@ func NewAudioUpload(chatID int, file interface{}) AudioConfig { // chatID is where to send it, fileID is the ID of the audio already uploaded. func NewAudioShare(chatID int, fileID string) AudioConfig { return AudioConfig{ - ChatID: chatID, + Chattable: Chattable{ChatID: chatID}, UseExistingAudio: true, FileID: fileID, } @@ -86,7 +86,7 @@ func NewAudioShare(chatID int, fileID string) AudioConfig { // 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, + Chattable: Chattable{ChatID: chatID}, UseExistingDocument: false, File: file, } @@ -98,7 +98,7 @@ func NewDocumentUpload(chatID int, file interface{}) DocumentConfig { // chatID is where to send it, fileID is the ID of the document already uploaded. func NewDocumentShare(chatID int, fileID string) DocumentConfig { return DocumentConfig{ - ChatID: chatID, + Chattable: Chattable{ChatID: chatID}, UseExistingDocument: true, FileID: fileID, } @@ -110,7 +110,7 @@ func NewDocumentShare(chatID int, fileID string) DocumentConfig { // 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, + Chattable: Chattable{ChatID: chatID}, UseExistingSticker: false, File: file, } @@ -122,7 +122,7 @@ func NewStickerUpload(chatID int, file interface{}) StickerConfig { // chatID is where to send it, fileID is the ID of the sticker already uploaded. func NewStickerShare(chatID int, fileID string) StickerConfig { return StickerConfig{ - ChatID: chatID, + Chattable: Chattable{ChatID: chatID}, UseExistingSticker: true, FileID: fileID, } @@ -135,7 +135,7 @@ func NewStickerShare(chatID int, fileID string) StickerConfig { // 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, + Chattable: Chattable{ChatID: chatID}, UseExistingVideo: false, File: file, } @@ -147,7 +147,7 @@ func NewVideoUpload(chatID int, file interface{}) VideoConfig { // chatID is where to send it, fileID is the ID of the video already uploaded. func NewVideoShare(chatID int, fileID string) VideoConfig { return VideoConfig{ - ChatID: chatID, + Chattable: Chattable{ChatID: chatID}, UseExistingVideo: true, FileID: fileID, } @@ -160,7 +160,7 @@ func NewVideoShare(chatID int, fileID string) VideoConfig { // 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, + Chattable: Chattable{ChatID: chatID}, UseExistingVoice: false, File: file, } @@ -172,7 +172,7 @@ func NewVoiceUpload(chatID int, file interface{}) VoiceConfig { // chatID is where to send it, fileID is the ID of the video already uploaded. func NewVoiceShare(chatID int, fileID string) VoiceConfig { return VoiceConfig{ - ChatID: chatID, + Chattable: Chattable{ChatID: chatID}, UseExistingVoice: true, FileID: fileID, } @@ -184,7 +184,7 @@ func NewVoiceShare(chatID int, fileID string) VoiceConfig { // chatID is where to send it, latitude and longitude are coordinates. func NewLocation(chatID int, latitude float64, longitude float64) LocationConfig { return LocationConfig{ - ChatID: chatID, + Chattable: Chattable{ChatID: chatID}, Latitude: latitude, Longitude: longitude, ReplyToMessageID: 0, @@ -198,8 +198,8 @@ func NewLocation(chatID int, latitude float64, longitude float64) LocationConfig // chatID is where to send it, action should be set via CHAT constants. func NewChatAction(chatID int, action string) ChatActionConfig { return ChatActionConfig{ - ChatID: chatID, - Action: action, + Chattable: Chattable{ChatID: chatID}, + Action: action, } } diff --git a/methods.go b/methods.go deleted file mode 100644 index 555d08e..0000000 --- a/methods.go +++ /dev/null @@ -1,1125 +0,0 @@ -package tgbotapi - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "github.com/technoweenie/multipartstreamer" - "io" - "io/ioutil" - "log" - "net/http" - "net/url" - "os" - "strconv" -) - -// Telegram constants -const ( - // APIEndpoint is the endpoint for all API methods, with formatting for Sprintf - APIEndpoint = "https://api.telegram.org/bot%s/%s" - // FileEndpoint is the endpoint for downloading a file from Telegram - FileEndpoint = "https://api.telegram.org/file/bot%s/%s" -) - -// Constant values for ChatActions -const ( - ChatTyping = "typing" - ChatUploadPhoto = "upload_photo" - ChatRecordVideo = "record_video" - ChatUploadVideo = "upload_video" - ChatRecordAudio = "record_audio" - ChatUploadAudio = "upload_audio" - ChatUploadDocument = "upload_document" - ChatFindLocation = "find_location" -) - -// API errors -const ( - // APIForbidden happens when a token is bad - APIForbidden = "forbidden" -) - -// Constant values for ParseMode in MessageConfig -const ( - ModeMarkdown = "Markdown" -) - -// MessageConfig contains information about a SendMessage request. -type MessageConfig struct { - ChatID int - ChannelUsername string - Text string - ParseMode string - DisableWebPagePreview bool - ReplyToMessageID int - ReplyMarkup interface{} -} - -// ForwardConfig contains information about a ForwardMessage request. -type ForwardConfig struct { - ChatID int - ChannelUsername string - FromChatID int - FromChannelUsername string - MessageID int -} - -// PhotoConfig contains information about a SendPhoto request. -type PhotoConfig struct { - ChatID int - ChannelUsername string - Caption string - ReplyToMessageID int - ReplyMarkup interface{} - UseExistingPhoto bool - FilePath string - File interface{} - FileID string -} - -// AudioConfig contains information about a SendAudio request. -type AudioConfig struct { - ChatID int - ChannelUsername string - Duration int - Performer string - Title string - ReplyToMessageID int - ReplyMarkup interface{} - UseExistingAudio bool - FilePath string - File interface{} - FileID string -} - -// DocumentConfig contains information about a SendDocument request. -type DocumentConfig struct { - ChatID int - ChannelUsername string - ReplyToMessageID int - ReplyMarkup interface{} - UseExistingDocument bool - FilePath string - File interface{} - FileID string -} - -// StickerConfig contains information about a SendSticker request. -type StickerConfig struct { - ChatID int - ChannelUsername string - ReplyToMessageID int - ReplyMarkup interface{} - UseExistingSticker bool - FilePath string - File interface{} - FileID string -} - -// VideoConfig contains information about a SendVideo request. -type VideoConfig struct { - ChatID int - ChannelUsername string - Duration int - Caption string - ReplyToMessageID int - ReplyMarkup interface{} - UseExistingVideo bool - FilePath string - File interface{} - FileID string -} - -// VoiceConfig contains information about a SendVoice request. -type VoiceConfig struct { - ChatID int - ChannelUsername string - Duration int - ReplyToMessageID int - ReplyMarkup interface{} - UseExistingVoice bool - FilePath string - File interface{} - FileID string -} - -// LocationConfig contains information about a SendLocation request. -type LocationConfig struct { - ChatID int - ChannelUsername string - Latitude float64 - Longitude float64 - ReplyToMessageID int - ReplyMarkup interface{} -} - -// ChatActionConfig contains information about a SendChatAction request. -type ChatActionConfig struct { - ChatID int - ChannelUsername string - Action string -} - -// UserProfilePhotosConfig contains information about a GetUserProfilePhotos request. -type UserProfilePhotosConfig struct { - UserID int - Offset int - Limit int -} - -// FileConfig has information about a file hosted on Telegram -type FileConfig struct { - FileID string -} - -// UpdateConfig contains information about a GetUpdates request. -type UpdateConfig struct { - Offset int - Limit int - Timeout int -} - -// WebhookConfig contains information about a SetWebhook request. -type WebhookConfig struct { - Clear bool - URL *url.URL - Certificate interface{} -} - -// 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) { - resp, err := bot.Client.PostForm(fmt.Sprintf(APIEndpoint, bot.Token, endpoint), params) - if err != nil { - return APIResponse{}, err - } - defer resp.Body.Close() - - if resp.StatusCode == http.StatusForbidden { - return APIResponse{}, errors.New(APIForbidden) - } - - bytes, err := ioutil.ReadAll(resp.Body) - if err != nil { - return APIResponse{}, err - } - - if bot.Debug { - log.Println(endpoint, string(bytes)) - } - - var apiResp APIResponse - json.Unmarshal(bytes, &apiResp) - - if !apiResp.Ok { - return APIResponse{}, errors.New(apiResp.Description) - } - - return apiResp, 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, 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) - - 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) - if err != nil { - return APIResponse{}, err - } - - res, err := bot.Client.Do(req) - if err != nil { - return APIResponse{}, err - } - defer res.Body.Close() - - bytes, err := ioutil.ReadAll(res.Body) - if err != nil { - return APIResponse{}, err - } - - if bot.Debug { - log.Println(string(bytes[:])) - } - - var apiResp APIResponse - json.Unmarshal(bytes, &apiResp) - - if !apiResp.Ok { - return APIResponse{}, errors.New(apiResp.Description) - } - - return apiResp, nil -} - -// GetMe fetches the currently authenticated bot. -// -// There are no parameters for this method. -func (bot *BotAPI) GetMe() (User, error) { - resp, err := bot.MakeRequest("getMe", nil) - if err != nil { - return User{}, err - } - - var user User - json.Unmarshal(resp.Result, &user) - - if bot.Debug { - log.Printf("getMe: %+v\n", user) - } - - return user, nil -} - -// SendMessage sends a Message to a chat. -// -// Requires ChatID and Text. -// DisableWebPagePreview, ReplyToMessageID, and ReplyMarkup are optional. -func (bot *BotAPI) SendMessage(config MessageConfig) (Message, error) { - v := url.Values{} - if config.ChannelUsername != "" { - v.Add("chat_id", config.ChannelUsername) - } else { - v.Add("chat_id", strconv.Itoa(config.ChatID)) - } - v.Add("text", config.Text) - v.Add("disable_web_page_preview", strconv.FormatBool(config.DisableWebPagePreview)) - if config.ParseMode != "" { - v.Add("parse_mode", config.ParseMode) - } - if config.ReplyToMessageID != 0 { - v.Add("reply_to_message_id", strconv.Itoa(config.ReplyToMessageID)) - } - if config.ReplyMarkup != nil { - data, err := json.Marshal(config.ReplyMarkup) - if err != nil { - return Message{}, err - } - - v.Add("reply_markup", string(data)) - } - - resp, err := bot.MakeRequest("SendMessage", v) - if err != nil { - return Message{}, err - } - - var message Message - json.Unmarshal(resp.Result, &message) - - if bot.Debug { - log.Printf("SendMessage req : %+v\n", v) - log.Printf("SendMessage resp: %+v\n", message) - } - - return message, nil -} - -// ForwardMessage forwards a message from one chat to another. -// -// Requires ChatID (destination), FromChatID (source), and MessageID. -func (bot *BotAPI) ForwardMessage(config ForwardConfig) (Message, error) { - v := url.Values{} - if config.ChannelUsername != "" { - v.Add("chat_id", config.ChannelUsername) - } else { - v.Add("chat_id", strconv.Itoa(config.ChatID)) - } - if config.FromChannelUsername != "" { - v.Add("chat_id", config.FromChannelUsername) - } else { - v.Add("chat_id", strconv.Itoa(config.FromChatID)) - } - v.Add("message_id", strconv.Itoa(config.MessageID)) - - resp, err := bot.MakeRequest("forwardMessage", v) - if err != nil { - return Message{}, err - } - - var message Message - json.Unmarshal(resp.Result, &message) - - if bot.Debug { - log.Printf("forwardMessage req : %+v\n", v) - log.Printf("forwardMessage resp: %+v\n", message) - } - - return message, nil -} - -// SendPhoto sends or uploads a photo to a chat. -// -// 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{} - if config.ChannelUsername != "" { - v.Add("chat_id", config.ChannelUsername) - } else { - v.Add("chat_id", strconv.Itoa(config.ChatID)) - } - v.Add("photo", config.FileID) - if config.Caption != "" { - v.Add("caption", config.Caption) - } - if config.ReplyToMessageID != 0 { - v.Add("reply_to_message_id", strconv.Itoa(config.ReplyToMessageID)) - } - if config.ReplyMarkup != nil { - data, err := json.Marshal(config.ReplyMarkup) - if err != nil { - return Message{}, err - } - - v.Add("reply_markup", string(data)) - } - - resp, err := bot.MakeRequest("SendPhoto", v) - if err != nil { - return Message{}, err - } - - var message Message - json.Unmarshal(resp.Result, &message) - - if bot.Debug { - log.Printf("SendPhoto req : %+v\n", v) - log.Printf("SendPhoto resp: %+v\n", message) - } - - return message, nil - } - - params := make(map[string]string) - if config.ChannelUsername != "" { - params["chat_id"] = config.ChannelUsername - } else { - params["chat_id"] = strconv.Itoa(config.ChatID) - } - if config.Caption != "" { - params["caption"] = config.Caption - } - if config.ReplyToMessageID != 0 { - params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID) - } - if config.ReplyMarkup != nil { - data, err := json.Marshal(config.ReplyMarkup) - if err != nil { - return Message{}, err - } - - params["reply_markup"] = string(data) - } - - 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 - } - - var message Message - json.Unmarshal(resp.Result, &message) - - if bot.Debug { - log.Printf("SendPhoto resp: %+v\n", message) - } - - return message, nil -} - -// 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 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 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{} - if config.ChannelUsername != "" { - v.Add("chat_id", config.ChannelUsername) - } else { - v.Add("chat_id", strconv.Itoa(config.ChatID)) - } - v.Add("audio", config.FileID) - if config.ReplyToMessageID != 0 { - v.Add("reply_to_message_id", strconv.Itoa(config.ReplyToMessageID)) - } - if config.Duration != 0 { - v.Add("duration", strconv.Itoa(config.Duration)) - } - if config.ReplyMarkup != nil { - data, err := json.Marshal(config.ReplyMarkup) - if err != nil { - return Message{}, err - } - - v.Add("reply_markup", string(data)) - } - if config.Performer != "" { - v.Add("performer", config.Performer) - } - if config.Title != "" { - v.Add("title", config.Title) - } - - resp, err := bot.MakeRequest("sendAudio", v) - if err != nil { - return Message{}, err - } - - var message Message - json.Unmarshal(resp.Result, &message) - - if bot.Debug { - log.Printf("sendAudio req : %+v\n", v) - log.Printf("sendAudio resp: %+v\n", message) - } - - return message, nil - } - - params := make(map[string]string) - - if config.ChannelUsername != "" { - params["chat_id"] = config.ChannelUsername - } else { - params["chat_id"] = strconv.Itoa(config.ChatID) - } - if config.ReplyToMessageID != 0 { - params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID) - } - if config.Duration != 0 { - params["duration"] = strconv.Itoa(config.Duration) - } - if config.ReplyMarkup != nil { - data, err := json.Marshal(config.ReplyMarkup) - if err != nil { - return Message{}, err - } - - params["reply_markup"] = string(data) - } - if config.Performer != "" { - params["performer"] = config.Performer - } - if config.Title != "" { - params["title"] = config.Title - } - - 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 - } - - var message Message - json.Unmarshal(resp.Result, &message) - - if bot.Debug { - log.Printf("sendAudio resp: %+v\n", message) - } - - return message, nil -} - -// SendDocument sends or uploads a document to a chat. -// -// 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{} - if config.ChannelUsername != "" { - v.Add("chat_id", config.ChannelUsername) - } else { - v.Add("chat_id", strconv.Itoa(config.ChatID)) - } - v.Add("document", config.FileID) - if config.ReplyToMessageID != 0 { - v.Add("reply_to_message_id", strconv.Itoa(config.ReplyToMessageID)) - } - if config.ReplyMarkup != nil { - data, err := json.Marshal(config.ReplyMarkup) - if err != nil { - return Message{}, err - } - - v.Add("reply_markup", string(data)) - } - - resp, err := bot.MakeRequest("sendDocument", v) - if err != nil { - return Message{}, err - } - - var message Message - json.Unmarshal(resp.Result, &message) - - if bot.Debug { - log.Printf("sendDocument req : %+v\n", v) - log.Printf("sendDocument resp: %+v\n", message) - } - - return message, nil - } - - params := make(map[string]string) - - if config.ChannelUsername != "" { - params["chat_id"] = config.ChannelUsername - } else { - params["chat_id"] = strconv.Itoa(config.ChatID) - } - if config.ReplyToMessageID != 0 { - params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID) - } - if config.ReplyMarkup != nil { - data, err := json.Marshal(config.ReplyMarkup) - if err != nil { - return Message{}, err - } - - params["reply_markup"] = string(data) - } - - 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 - } - - var message Message - json.Unmarshal(resp.Result, &message) - - if bot.Debug { - log.Printf("sendDocument resp: %+v\n", message) - } - - return message, nil -} - -// SendVoice sends or uploads a playable voice to a chat. -// 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 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{} - if config.ChannelUsername != "" { - v.Add("chat_id", config.ChannelUsername) - } else { - v.Add("chat_id", strconv.Itoa(config.ChatID)) - } - v.Add("voice", config.FileID) - if config.ReplyToMessageID != 0 { - v.Add("reply_to_message_id", strconv.Itoa(config.ReplyToMessageID)) - } - if config.Duration != 0 { - v.Add("duration", strconv.Itoa(config.Duration)) - } - if config.ReplyMarkup != nil { - data, err := json.Marshal(config.ReplyMarkup) - if err != nil { - return Message{}, err - } - - v.Add("reply_markup", string(data)) - } - - resp, err := bot.MakeRequest("sendVoice", v) - if err != nil { - return Message{}, err - } - - var message Message - json.Unmarshal(resp.Result, &message) - - if bot.Debug { - log.Printf("SendVoice req : %+v\n", v) - log.Printf("SendVoice resp: %+v\n", message) - } - - return message, nil - } - - params := make(map[string]string) - - if config.ChannelUsername != "" { - params["chat_id"] = config.ChannelUsername - } else { - params["chat_id"] = strconv.Itoa(config.ChatID) - } - if config.ReplyToMessageID != 0 { - params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID) - } - if config.Duration != 0 { - params["duration"] = strconv.Itoa(config.Duration) - } - if config.ReplyMarkup != nil { - data, err := json.Marshal(config.ReplyMarkup) - if err != nil { - return Message{}, err - } - - params["reply_markup"] = string(data) - } - - 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 - } - - var message Message - json.Unmarshal(resp.Result, &message) - - if bot.Debug { - log.Printf("SendVoice resp: %+v\n", message) - } - - return message, nil -} - -// SendSticker sends or uploads a sticker to a chat. -// -// 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{} - if config.ChannelUsername != "" { - v.Add("chat_id", config.ChannelUsername) - } else { - v.Add("chat_id", strconv.Itoa(config.ChatID)) - } - v.Add("sticker", config.FileID) - if config.ReplyToMessageID != 0 { - v.Add("reply_to_message_id", strconv.Itoa(config.ReplyToMessageID)) - } - if config.ReplyMarkup != nil { - data, err := json.Marshal(config.ReplyMarkup) - if err != nil { - return Message{}, err - } - - v.Add("reply_markup", string(data)) - } - - resp, err := bot.MakeRequest("sendSticker", v) - if err != nil { - return Message{}, err - } - - var message Message - json.Unmarshal(resp.Result, &message) - - if bot.Debug { - log.Printf("sendSticker req : %+v\n", v) - log.Printf("sendSticker resp: %+v\n", message) - } - - return message, nil - } - - params := make(map[string]string) - - if config.ChannelUsername != "" { - params["chat_id"] = config.ChannelUsername - } else { - params["chat_id"] = strconv.Itoa(config.ChatID) - } - if config.ReplyToMessageID != 0 { - params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID) - } - if config.ReplyMarkup != nil { - data, err := json.Marshal(config.ReplyMarkup) - if err != nil { - return Message{}, err - } - - params["reply_markup"] = string(data) - } - - 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 - } - - var message Message - json.Unmarshal(resp.Result, &message) - - if bot.Debug { - log.Printf("sendSticker resp: %+v\n", message) - } - - return message, nil -} - -// SendVideo sends or uploads a video to a chat. -// -// 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{} - if config.ChannelUsername != "" { - v.Add("chat_id", config.ChannelUsername) - } else { - v.Add("chat_id", strconv.Itoa(config.ChatID)) - } - v.Add("video", config.FileID) - if config.ReplyToMessageID != 0 { - v.Add("reply_to_message_id", strconv.Itoa(config.ReplyToMessageID)) - } - if config.Duration != 0 { - v.Add("duration", strconv.Itoa(config.Duration)) - } - if config.Caption != "" { - v.Add("caption", config.Caption) - } - if config.ReplyMarkup != nil { - data, err := json.Marshal(config.ReplyMarkup) - if err != nil { - return Message{}, err - } - - v.Add("reply_markup", string(data)) - } - - resp, err := bot.MakeRequest("sendVideo", v) - if err != nil { - return Message{}, err - } - - var message Message - json.Unmarshal(resp.Result, &message) - - if bot.Debug { - log.Printf("sendVideo req : %+v\n", v) - log.Printf("sendVideo resp: %+v\n", message) - } - - return message, nil - } - - params := make(map[string]string) - - if config.ChannelUsername != "" { - params["chat_id"] = config.ChannelUsername - } else { - params["chat_id"] = strconv.Itoa(config.ChatID) - } - if config.ReplyToMessageID != 0 { - params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID) - } - if config.ReplyMarkup != nil { - data, err := json.Marshal(config.ReplyMarkup) - if err != nil { - return Message{}, err - } - - params["reply_markup"] = string(data) - } - - 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 - } - - var message Message - json.Unmarshal(resp.Result, &message) - - if bot.Debug { - log.Printf("sendVideo resp: %+v\n", message) - } - - return message, nil -} - -// SendLocation sends a location to a chat. -// -// Requires ChatID, Latitude, and Longitude. -// ReplyToMessageID and ReplyMarkup are optional. -func (bot *BotAPI) SendLocation(config LocationConfig) (Message, error) { - v := url.Values{} - if config.ChannelUsername != "" { - v.Add("chat_id", config.ChannelUsername) - } else { - v.Add("chat_id", strconv.Itoa(config.ChatID)) - } - v.Add("latitude", strconv.FormatFloat(config.Latitude, 'f', 6, 64)) - v.Add("longitude", strconv.FormatFloat(config.Longitude, 'f', 6, 64)) - if config.ReplyToMessageID != 0 { - v.Add("reply_to_message_id", strconv.Itoa(config.ReplyToMessageID)) - } - if config.ReplyMarkup != nil { - data, err := json.Marshal(config.ReplyMarkup) - if err != nil { - return Message{}, err - } - - v.Add("reply_markup", string(data)) - } - - resp, err := bot.MakeRequest("sendLocation", v) - if err != nil { - return Message{}, err - } - - var message Message - json.Unmarshal(resp.Result, &message) - - if bot.Debug { - log.Printf("sendLocation req : %+v\n", v) - log.Printf("sendLocation resp: %+v\n", message) - } - - return message, nil -} - -// SendChatAction sets a current action in a chat. -// -// Requires ChatID and a valid Action (see Chat constants). -func (bot *BotAPI) SendChatAction(config ChatActionConfig) error { - v := url.Values{} - if config.ChannelUsername != "" { - v.Add("chat_id", config.ChannelUsername) - } else { - v.Add("chat_id", strconv.Itoa(config.ChatID)) - } - v.Add("action", config.Action) - - _, err := bot.MakeRequest("sendChatAction", v) - if err != nil { - return err - } - - return nil -} - -// GetUserProfilePhotos gets a user's profile photos. -// -// Requires UserID. -// Offset and Limit are optional. -func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) { - v := url.Values{} - v.Add("user_id", strconv.Itoa(config.UserID)) - if config.Offset != 0 { - v.Add("offset", strconv.Itoa(config.Offset)) - } - if config.Limit != 0 { - v.Add("limit", strconv.Itoa(config.Limit)) - } - - resp, err := bot.MakeRequest("getUserProfilePhotos", v) - if err != nil { - return UserProfilePhotos{}, err - } - - var profilePhotos UserProfilePhotos - json.Unmarshal(resp.Result, &profilePhotos) - - if bot.Debug { - log.Printf("getUserProfilePhotos req : %+v\n", v) - log.Printf("getUserProfilePhotos resp: %+v\n", profilePhotos) - } - - return profilePhotos, nil -} - -// GetFile returns a file_id required to download a file. -// -// Requires FileID. -func (bot *BotAPI) GetFile(config FileConfig) (File, error) { - v := url.Values{} - v.Add("file_id", config.FileID) - - resp, err := bot.MakeRequest("getFile", v) - if err != nil { - return File{}, err - } - - var file File - json.Unmarshal(resp.Result, &file) - - if bot.Debug { - log.Printf("getFile req : %+v\n", v) - log.Printf("getFile resp: %+v\n", file) - } - - return file, nil -} - -// GetUpdates fetches updates. -// If a WebHook is set, this will not return any data! -// -// Offset, Limit, and Timeout are optional. -// To not get old items, set Offset to one higher than the previous item. -// Set Timeout to a large number to reduce requests and get responses instantly. -func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) { - v := url.Values{} - if config.Offset > 0 { - v.Add("offset", strconv.Itoa(config.Offset)) - } - if config.Limit > 0 { - v.Add("limit", strconv.Itoa(config.Limit)) - } - if config.Timeout > 0 { - v.Add("timeout", strconv.Itoa(config.Timeout)) - } - - resp, err := bot.MakeRequest("getUpdates", v) - if err != nil { - return []Update{}, err - } - - var updates []Update - json.Unmarshal(resp.Result, &updates) - - if bot.Debug { - log.Printf("getUpdates: %+v\n", updates) - } - - return updates, nil -} - -// SetWebhook sets a webhook. -// If this is set, GetUpdates will not get any data! -// -// Requires URL OR to set Clear to true. -func (bot *BotAPI) SetWebhook(config WebhookConfig) (APIResponse, error) { - if config.Certificate == nil { - v := url.Values{} - if !config.Clear { - v.Add("url", config.URL.String()) - } - - return bot.MakeRequest("setWebhook", v) - } - - params := make(map[string]string) - params["url"] = config.URL.String() - - resp, err := bot.UploadFile("setWebhook", params, "certificate", config.Certificate) - if err != nil { - return APIResponse{}, err - } - - var apiResp APIResponse - json.Unmarshal(resp.Result, &apiResp) - - if bot.Debug { - log.Printf("setWebhook resp: %+v\n", apiResp) - } - - return apiResp, nil -} diff --git a/updates.go b/updates.go deleted file mode 100644 index f790a88..0000000 --- a/updates.go +++ /dev/null @@ -1,33 +0,0 @@ -package tgbotapi - -import ( - "log" - "time" -) - -// UpdatesChan starts a channel for getting updates. -func (bot *BotAPI) UpdatesChan(config UpdateConfig) error { - bot.Updates = make(chan Update, 100) - - go func() { - for { - updates, err := bot.GetUpdates(config) - if err != nil { - log.Println(err) - log.Println("Failed to get updates, retrying in 3 seconds...") - time.Sleep(time.Second * 3) - - continue - } - - for _, update := range updates { - if update.UpdateID >= config.Offset { - config.Offset = update.UpdateID + 1 - bot.Updates <- update - } - } - } - }() - - return nil -} diff --git a/webhook.go b/webhook.go deleted file mode 100644 index 79060da..0000000 --- a/webhook.go +++ /dev/null @@ -1,21 +0,0 @@ -package tgbotapi - -import ( - "encoding/json" - "io/ioutil" - "net/http" -) - -// ListenForWebhook registers a http handler for a webhook. -func (bot *BotAPI) ListenForWebhook(pattern string) { - bot.Updates = make(chan Update, 100) - - http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) { - bytes, _ := ioutil.ReadAll(r.Body) - - var update Update - json.Unmarshal(bytes, &update) - - bot.Updates <- update - }) -}