diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa7ac80 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.idea/ +coverage.out diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..8408fb7 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,5 @@ +language: go + +go: + - 1.4 + - tip \ No newline at end of file diff --git a/README.md b/README.md index 5023f53..c42e2a3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Golang bindings for the Telegram Bot API [![GoDoc](https://godoc.org/github.com/Syfaro/telegram-bot-api?status.svg)](http://godoc.org/github.com/Syfaro/telegram-bot-api) +[![Travis](https://travis-ci.org/Syfaro/telegram-bot-api.svg)](https://travis-ci.org/Syfaro/telegram-bot-api) All methods have been added, and all features should be available. If you want a feature that hasn't been added yet or something is broken, open an issue and I'll see what I can do. @@ -34,18 +35,15 @@ func main() { u := tgbotapi.NewUpdate(0) u.Timeout = 60 - err = bot.UpdatesChan(u) - if err != nil { - log.Panic(err) - } + updates, err := bot.GetUpdatesChan(u) - for update := range bot.Updates { + for update := range updates { log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text) msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text) msg.ReplyToMessageID = update.Message.MessageID - bot.SendMessage(msg) + bot.Send(msg) } } ``` @@ -76,10 +74,10 @@ func main() { log.Fatal(err) } - bot.ListenForWebhook("/"+bot.Token) + updates, _ := bot.ListenForWebhook("/" + bot.Token) go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil) - for update := range bot.Updates { + for update := range updates { log.Printf("%+v\n", update) } } @@ -87,4 +85,4 @@ func main() { If you need, you may generate a self signed certficate, as this requires HTTPS / TLS. The above example tells Telegram that this is your certificate and that it should be trusted, even though it is not properly signed. - openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3560 -subj -nodes + openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3560 -subj "//O=Org\CN=Test" -nodes diff --git a/bot.go b/bot.go index a41f51f..73f5ba1 100644 --- a/bot.go +++ b/bot.go @@ -2,16 +2,27 @@ package tgbotapi import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "github.com/technoweenie/multipartstreamer" + "io/ioutil" + "log" "net/http" + "net/url" + "os" + "strconv" + "strings" + "time" ) // BotAPI has methods for interacting with all of Telegram's Bot API endpoints. type BotAPI struct { - Token string `json:"token"` - Debug bool `json:"debug"` - Self User `json:"-"` - Updates chan Update `json:"-"` - Client *http.Client `json:"-"` + Token string `json:"token"` + Debug bool `json:"debug"` + Self User `json:"-"` + Client *http.Client `json:"-"` } // NewBotAPI creates a new BotAPI instance. @@ -37,3 +48,406 @@ 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 +} + +func (bot *BotAPI) makeMessageRequest(endpoint string, params url.Values) (Message, error) { + resp, err := bot.MakeRequest(endpoint, params) + if err != nil { + return Message{}, err + } + + var message Message + json.Unmarshal(resp.Result, &message) + + bot.debugLog(endpoint, params, message) + + return message, 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 +} + +// GetFileDirectURL returns direct URL to file +// +// Requires fileID +func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) { + file, err := bot.GetFile(FileConfig{fileID}) + + if err != nil { + return "", err + } + + return file.Link(bot.Token), 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 +} + +// IsMessageToMe returns true if message directed to this bot +// +// Requires message +func (bot *BotAPI) IsMessageToMe(message Message) bool { + return strings.Contains(message.Text, "@"+bot.Self.UserName) +} + +// Send will send event(Message, Photo, Audio, ChatAction, anything) to Telegram +// +// Requires Chattable +func (bot *BotAPI) Send(c Chattable) (Message, error) { + switch c.(type) { + case Fileable: + return bot.sendFile(c.(Fileable)) + default: + return bot.sendChattable(c) + } +} + +func (bot *BotAPI) debugLog(context string, v url.Values, message interface{}) { + if bot.Debug { + log.Printf("%s req : %+v\n", context, v) + log.Printf("%s resp: %+v\n", context, message) + } +} + +func (bot *BotAPI) sendExisting(method string, config Fileable) (Message, error) { + v, err := config.Values() + + if err != nil { + return Message{}, err + } + + message, err := bot.makeMessageRequest(method, v) + if err != nil { + return Message{}, err + } + + return message, nil +} + +func (bot *BotAPI) uploadAndSend(method string, config Fileable) (Message, error) { + params, err := config.Params() + if err != nil { + return Message{}, err + } + + file := config.GetFile() + + resp, err := bot.UploadFile(method, params, config.Name(), file) + if err != nil { + return Message{}, err + } + + var message Message + json.Unmarshal(resp.Result, &message) + + if bot.Debug { + log.Printf("%s resp: %+v\n", method, message) + } + + return message, nil +} + +func (bot *BotAPI) sendFile(config Fileable) (Message, error) { + if config.UseExistingFile() { + return bot.sendExisting(config.Method(), config) + } + + return bot.uploadAndSend(config.Method(), config) +} + +func (bot *BotAPI) sendChattable(config Chattable) (Message, error) { + v, err := config.Values() + if err != nil { + return Message{}, err + } + + message, err := bot.makeMessageRequest(config.Method(), v) + + if err != nil { + return Message{}, err + } + + return message, 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) + + bot.debugLog("GetUserProfilePhoto", v, 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) + + bot.debugLog("GetFile", v, 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 +} + +// RemoveWebhook removes webhook +// +// There are no parameters for this method. +func (bot *BotAPI) RemoveWebhook() (APIResponse, error) { + return bot.MakeRequest("setWebhook", url.Values{}) +} + +// 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{} + 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 +} + +// GetUpdatesChan starts and returns a channel for getting updates. +// +// Requires UpdateConfig +func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (<-chan Update, error) { + updatesChan := 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 + updatesChan <- update + } + } + } + }() + + return updatesChan, nil +} + +// ListenForWebhook registers a http handler for a webhook. +func (bot *BotAPI) ListenForWebhook(pattern string) (<-chan Update, http.Handler) { + updatesChan := make(chan Update, 100) + + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + bytes, _ := ioutil.ReadAll(r.Body) + + var update Update + json.Unmarshal(bytes, &update) + + updatesChan <- update + }) + + http.HandleFunc(pattern, handler) + + return updatesChan, handler +} diff --git a/bot_test.go b/bot_test.go index 6f56565..17cedac 100644 --- a/bot_test.go +++ b/bot_test.go @@ -1,63 +1,406 @@ package tgbotapi_test import ( - "github.com/syfaro/telegram-bot-api" + "github.com/Syfaro/telegram-bot-api" + "io/ioutil" "log" + "net/http" + "net/http/httptest" "os" + "strings" "testing" ) -func TestMain(m *testing.M) { - botToken := os.Getenv("TELEGRAM_API_TOKEN") +const ( + TestToken = "153667468:AAHlSHlMqSt1f_uFmVRJbm5gntu2HI4WW8I" + ChatID = 76918703 + ReplyToMessageID = 35 + ExistingPhotoFileID = "AgADAgADw6cxG4zHKAkr42N7RwEN3IFShCoABHQwXEtVks4EH2wBAAEC" + ExistingDocumentFileID = "BQADAgADOQADjMcoCcioX1GrDvp3Ag" + ExistingAudioFileID = "BQADAgADRgADjMcoCdXg3lSIN49lAg" + ExistingVoiceFileID = "AwADAgADWQADjMcoCeul6r_q52IyAg" + ExistingVideoFileID = "BAADAgADZgADjMcoCav432kYe0FRAg" + ExistingStickerFileID = "BQADAgADcwADjMcoCbdl-6eB--YPAg" +) - if botToken == "" { - log.Panic("You must provide a TELEGRAM_API_TOKEN env variable to test!") +func getBot(t *testing.T) (*tgbotapi.BotAPI, error) { + bot, err := tgbotapi.NewBotAPI(TestToken) + + if err != nil { + t.Fail() } - os.Exit(m.Run()) + return bot, err } func TestNewBotAPI_notoken(t *testing.T) { _, err := tgbotapi.NewBotAPI("") - if err.Error() != tgbotapi.APIForbidden { - t.Fail() - } -} - -func TestNewBotAPI_token(t *testing.T) { - _, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_API_TOKEN")) - - if err != nil { + if err == nil { t.Fail() } } func TestGetUpdates(t *testing.T) { - bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_API_TOKEN")) - - if err != nil { - t.Fail() - } + bot, _ := getBot(t) u := tgbotapi.NewUpdate(0) - _, err = bot.GetUpdates(u) + _, err := bot.GetUpdates(u) if err != nil { t.Fail() } } -func TestSendMessage(t *testing.T) { - bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_API_TOKEN")) +func TestSendWithMessage(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewMessage(ChatID, "A test message from the test library in telegram-bot-api") + msg.ParseMode = "markdown" + _, err := bot.Send(msg) if err != nil { t.Fail() } +} - msg := tgbotapi.NewMessage(36529758, "A test message from the test library in telegram-bot-api") - bot.SendMessage(msg) +func TestSendWithMessageReply(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewMessage(ChatID, "A test message from the test library in telegram-bot-api") + msg.ReplyToMessageID = ReplyToMessageID + _, err := bot.Send(msg) + + if err != nil { + t.Fail() + } +} + +func TestSendWithMessageForward(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewForward(ChatID, ChatID, ReplyToMessageID) + _, err := bot.Send(msg) + + if err != nil { + t.Fail() + } +} + +func TestSendWithNewPhoto(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewPhotoUpload(ChatID, "tests/image.jpg") + msg.Caption = "Test" + _, err := bot.Send(msg) + + if err != nil { + t.Fail() + } +} + +func TestSendWithNewPhotoWithFileBytes(t *testing.T) { + bot, _ := getBot(t) + + data, _ := ioutil.ReadFile("tests/image.jpg") + b := tgbotapi.FileBytes{Name: "image.jpg", Bytes: data} + + msg := tgbotapi.NewPhotoUpload(ChatID, b) + msg.Caption = "Test" + _, err := bot.Send(msg) + + if err != nil { + t.Fail() + } +} + +func TestSendWithNewPhotoWithFileReader(t *testing.T) { + bot, _ := getBot(t) + + f, _ := os.Open("tests/image.jpg") + reader := tgbotapi.FileReader{Name: "image.jpg", Reader: f, Size: -1} + + msg := tgbotapi.NewPhotoUpload(ChatID, reader) + msg.Caption = "Test" + _, err := bot.Send(msg) + + if err != nil { + t.Fail() + } +} + +func TestSendWithNewPhotoReply(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewPhotoUpload(ChatID, "tests/image.jpg") + msg.ReplyToMessageID = ReplyToMessageID + + _, err := bot.Send(msg) + + if err != nil { + t.Fail() + } +} + +func TestSendWithExistingPhoto(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewPhotoShare(ChatID, ExistingPhotoFileID) + msg.Caption = "Test" + _, err := bot.Send(msg) + + if err != nil { + t.Fail() + } +} + +func TestSendWithNewDocument(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewDocumentUpload(ChatID, "tests/image.jpg") + _, err := bot.Send(msg) + + if err != nil { + t.Fail() + } +} + +func TestSendWithExistingDocument(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewDocumentShare(ChatID, ExistingDocumentFileID) + _, err := bot.Send(msg) + + if err != nil { + t.Fail() + } +} + +func TestSendWithNewAudio(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewAudioUpload(ChatID, "tests/audio.mp3") + msg.Title = "TEST" + msg.Duration = 10 + msg.Performer = "TEST" + _, err := bot.Send(msg) + + if err != nil { + t.Fail() + } +} + +func TestSendWithExistingAudio(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewAudioShare(ChatID, ExistingAudioFileID) + msg.Title = "TEST" + msg.Duration = 10 + msg.Performer = "TEST" + + _, err := bot.Send(msg) + + if err != nil { + t.Fail() + } +} + +func TestSendWithNewVoice(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewVoiceUpload(ChatID, "tests/voice.ogg") + msg.Duration = 10 + _, err := bot.Send(msg) + + if err != nil { + t.Fail() + } +} + +func TestSendWithExistingVoice(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewVoiceShare(ChatID, ExistingVoiceFileID) + msg.Duration = 10 + _, err := bot.Send(msg) + + if err != nil { + t.Fail() + } +} + +func TestSendWithLocation(t *testing.T) { + bot, _ := getBot(t) + + _, err := bot.Send(tgbotapi.NewLocation(ChatID, 40, 40)) + + if err != nil { + t.Fail() + } +} + +func TestSendWithNewVideo(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewVideoUpload(ChatID, "tests/video.mp4") + msg.Duration = 10 + msg.Caption = "TEST" + + _, err := bot.Send(msg) + + if err != nil { + t.Fail() + } +} + +func TestSendWithExistingVideo(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewVideoShare(ChatID, ExistingVideoFileID) + msg.Duration = 10 + msg.Caption = "TEST" + + _, err := bot.Send(msg) + + if err != nil { + t.Fail() + } +} + +func TestSendWithNewSticker(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewStickerUpload(ChatID, "tests/image.jpg") + + _, err := bot.Send(msg) + + if err != nil { + t.Fail() + } +} + +func TestSendWithExistingSticker(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewStickerShare(ChatID, ExistingStickerFileID) + + _, err := bot.Send(msg) + + if err != nil { + t.Fail() + } +} + +func TestSendWithNewStickerAndKeyboardHide(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewStickerUpload(ChatID, "tests/image.jpg") + msg.ReplyMarkup = tgbotapi.ReplyKeyboardHide{true, false} + _, err := bot.Send(msg) + + if err != nil { + t.Fail() + } +} + +func TestSendWithExistingStickerAndKeyboardHide(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewStickerShare(ChatID, ExistingStickerFileID) + msg.ReplyMarkup = tgbotapi.ReplyKeyboardHide{true, false} + + _, err := bot.Send(msg) + + if err != nil { + + t.Fail() + } +} + +func TestGetFile(t *testing.T) { + bot, _ := getBot(t) + + file := tgbotapi.FileConfig{ExistingPhotoFileID} + + _, err := bot.GetFile(file) + + if err != nil { + t.Fail() + } +} + +func TestSendChatConfig(t *testing.T) { + bot, _ := getBot(t) + + _, err := bot.Send(tgbotapi.NewChatAction(ChatID, tgbotapi.ChatTyping)) + + if err != nil { + t.Fail() + } +} + +func TestGetUserProfilePhotos(t *testing.T) { + bot, _ := getBot(t) + + _, err := bot.GetUserProfilePhotos(tgbotapi.NewUserProfilePhotos(ChatID)) + if err != nil { + t.Fail() + } +} + +func TestListenForWebhook(t *testing.T) { + bot, _ := getBot(t) + + _, handler := bot.ListenForWebhook("/") + + req, _ := http.NewRequest("GET", "", strings.NewReader("{}")) + w := httptest.NewRecorder() + + handler.ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Errorf("Home page didn't return %v", http.StatusOK) + } +} + +func TestSetWebhookWithCert(t *testing.T) { + bot, _ := getBot(t) + + bot.RemoveWebhook() + + wh := tgbotapi.NewWebhookWithCert("https://example.com/tgbotapi-test/"+bot.Token, "tests/cert.pem") + _, err := bot.SetWebhook(wh) + if err != nil { + t.Fail() + } + + bot.RemoveWebhook() +} + +func TestSetWebhookWithoutCert(t *testing.T) { + bot, _ := getBot(t) + + bot.RemoveWebhook() + + wh := tgbotapi.NewWebhook("https://example.com/tgbotapi-test/" + bot.Token) + _, err := bot.SetWebhook(wh) + if err != nil { + t.Fail() + } + + bot.RemoveWebhook() +} + +func TestUpdatesChan(t *testing.T) { + bot, _ := getBot(t) + + var ucfg tgbotapi.UpdateConfig = tgbotapi.NewUpdate(0) + ucfg.Timeout = 60 + _, err := bot.GetUpdatesChan(ucfg) + + if err != nil { + t.Fail() + } } func ExampleNewBotAPI() { @@ -73,14 +416,37 @@ func ExampleNewBotAPI() { u := tgbotapi.NewUpdate(0) u.Timeout = 60 - err = bot.UpdatesChan(u) + updates, err := bot.GetUpdatesChan(u) - for update := range bot.Updates { + for update := range updates { log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text) msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text) msg.ReplyToMessageID = update.Message.MessageID - bot.SendMessage(msg) + bot.Send(msg) + } +} + +func ExampleNewWebhook() { + bot, err := tgbotapi.NewBotAPI("MyAwesomeBotToken") + if err != nil { + log.Fatal(err) + } + + bot.Debug = true + + log.Printf("Authorized on account %s", bot.Self.UserName) + + _, err = bot.SetWebhook(tgbotapi.NewWebhookWithCert("https://www.google.com:8443/"+bot.Token, "cert.pem")) + if err != nil { + log.Fatal(err) + } + + updates, _ := bot.ListenForWebhook("/" + bot.Token) + go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil) + + for update := range updates { + log.Printf("%+v\n", update) } } diff --git a/configs.go b/configs.go new file mode 100644 index 0000000..b687940 --- /dev/null +++ b/configs.go @@ -0,0 +1,498 @@ +package tgbotapi + +import ( + "encoding/json" + "io" + "net/url" + "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" +) + +//Chattable represents any event in chat(MessageConfig, PhotoConfig, ChatActionConfig and others) +type Chattable interface { + Values() (url.Values, error) + Method() string +} + +//Fileable represents any file event(PhotoConfig, DocumentConfig, AudioConfig, VoiceConfig, VideoConfig, StickerConfig) +type Fileable interface { + Chattable + Params() (map[string]string, error) + Name() string + GetFile() interface{} + UseExistingFile() bool +} + +// BaseChat is base struct for all chat event(Message, Photo and so on) +type BaseChat struct { + ChatID int + ChannelUsername string + ReplyToMessageID int + ReplyMarkup interface{} +} + +// Values returns url.Values representation of BaseChat +func (chat *BaseChat) Values() (url.Values, error) { + v := url.Values{} + if chat.ChannelUsername != "" { + v.Add("chat_id", chat.ChannelUsername) + } else { + v.Add("chat_id", strconv.Itoa(chat.ChatID)) + } + + if chat.ReplyToMessageID != 0 { + v.Add("reply_to_message_id", strconv.Itoa(chat.ReplyToMessageID)) + } + + if chat.ReplyMarkup != nil { + data, err := json.Marshal(chat.ReplyMarkup) + if err != nil { + return v, err + } + + v.Add("reply_markup", string(data)) + } + + return v, nil +} + +// BaseFile is base struct for all file events(PhotoConfig, DocumentConfig, AudioConfig, VoiceConfig, VideoConfig, StickerConfig) +type BaseFile struct { + BaseChat + FilePath string + File interface{} + FileID string + UseExisting bool +} + +// Params returns map[string]string representation of BaseFile +func (file BaseFile) Params() (map[string]string, error) { + params := make(map[string]string) + + if file.ChannelUsername != "" { + params["chat_id"] = file.ChannelUsername + } else { + params["chat_id"] = strconv.Itoa(file.ChatID) + } + + if file.ReplyToMessageID != 0 { + params["reply_to_message_id"] = strconv.Itoa(file.ReplyToMessageID) + } + + if file.ReplyMarkup != nil { + data, err := json.Marshal(file.ReplyMarkup) + if err != nil { + return params, err + } + + params["reply_markup"] = string(data) + } + + return params, nil +} + +// GetFile returns abstract representation of File inside BaseFile +func (file BaseFile) GetFile() interface{} { + var result interface{} + if file.FilePath == "" { + result = file.File + } else { + result = file.FilePath + } + + return result +} + +// UseExistingFile returns true if BaseFile contains already uploaded file by FileID +func (file BaseFile) UseExistingFile() bool { + return file.UseExisting +} + +// MessageConfig contains information about a SendMessage request. +type MessageConfig struct { + BaseChat + Text string + ParseMode string + DisableWebPagePreview bool + ReplyMarkup interface{} +} + +// Values returns url.Values representation of MessageConfig +func (config MessageConfig) Values() (url.Values, error) { + v, _ := config.BaseChat.Values() + v.Add("text", config.Text) + v.Add("disable_web_page_preview", strconv.FormatBool(config.DisableWebPagePreview)) + if config.ParseMode != "" { + v.Add("parse_mode", config.ParseMode) + } + + return v, nil +} + +// Method returns Telegram API method name for sending Message +func (config MessageConfig) Method() string { + return "SendMessage" +} + +// ForwardConfig contains information about a ForwardMessage request. +type ForwardConfig struct { + BaseChat + FromChatID int + FromChannelUsername string + MessageID int +} + +// Values returns url.Values representation of ForwardConfig +func (config ForwardConfig) Values() (url.Values, error) { + v, _ := config.BaseChat.Values() + v.Add("from_chat_id", strconv.Itoa(config.FromChatID)) + v.Add("message_id", strconv.Itoa(config.MessageID)) + return v, nil +} + +// Method returns Telegram API method name for sending Forward +func (config ForwardConfig) Method() string { + return "forwardMessage" +} + +// PhotoConfig contains information about a SendPhoto request. +type PhotoConfig struct { + BaseFile + Caption string +} + +// Params returns map[string]string representation of PhotoConfig +func (config PhotoConfig) Params() (map[string]string, error) { + params, _ := config.BaseFile.Params() + + if config.Caption != "" { + params["caption"] = config.Caption + } + + return params, nil +} + +// Values returns url.Values representation of PhotoConfig +func (config PhotoConfig) Values() (url.Values, error) { + v, _ := config.BaseChat.Values() + + v.Add(config.Name(), config.FileID) + if config.Caption != "" { + v.Add("caption", config.Caption) + } + return v, nil +} + +// Name return field name for uploading file +func (config PhotoConfig) Name() string { + return "photo" +} + +// Method returns Telegram API method name for sending Photo +func (config PhotoConfig) Method() string { + return "SendPhoto" +} + +// AudioConfig contains information about a SendAudio request. +type AudioConfig struct { + BaseFile + Duration int + Performer string + Title string +} + +// Values returns url.Values representation of AudioConfig +func (config AudioConfig) Values() (url.Values, error) { + v, _ := config.BaseChat.Values() + + v.Add(config.Name(), config.FileID) + if config.Duration != 0 { + v.Add("duration", strconv.Itoa(config.Duration)) + } + + if config.Performer != "" { + v.Add("performer", config.Performer) + } + if config.Title != "" { + v.Add("title", config.Title) + } + + return v, nil +} + +// Params returns map[string]string representation of AudioConfig +func (config AudioConfig) Params() (map[string]string, error) { + params, _ := config.BaseFile.Params() + + if config.Duration != 0 { + params["duration"] = strconv.Itoa(config.Duration) + } + + if config.Performer != "" { + params["performer"] = config.Performer + } + if config.Title != "" { + params["title"] = config.Title + } + + return params, nil +} + +// Name return field name for uploading file +func (config AudioConfig) Name() string { + return "audio" +} + +// Method returns Telegram API method name for sending Audio +func (config AudioConfig) Method() string { + return "SendAudio" +} + +// DocumentConfig contains information about a SendDocument request. +type DocumentConfig struct { + BaseFile +} + +// Values returns url.Values representation of DocumentConfig +func (config DocumentConfig) Values() (url.Values, error) { + v, _ := config.BaseChat.Values() + + v.Add(config.Name(), config.FileID) + + return v, nil +} + +// Params returns map[string]string representation of DocumentConfig +func (config DocumentConfig) Params() (map[string]string, error) { + params, _ := config.BaseFile.Params() + + return params, nil +} + +// Name return field name for uploading file +func (config DocumentConfig) Name() string { + return "document" +} + +// Method returns Telegram API method name for sending Document +func (config DocumentConfig) Method() string { + return "sendDocument" +} + +// StickerConfig contains information about a SendSticker request. +type StickerConfig struct { + BaseFile +} + +// Values returns url.Values representation of StickerConfig +func (config StickerConfig) Values() (url.Values, error) { + v, _ := config.BaseChat.Values() + + v.Add(config.Name(), config.FileID) + + return v, nil +} + +// Params returns map[string]string representation of StickerConfig +func (config StickerConfig) Params() (map[string]string, error) { + params, _ := config.BaseFile.Params() + + return params, nil +} + +// Name return field name for uploading file +func (config StickerConfig) Name() string { + return "sticker" +} + +// Method returns Telegram API method name for sending Sticker +func (config StickerConfig) Method() string { + return "sendSticker" +} + +// VideoConfig contains information about a SendVideo request. +type VideoConfig struct { + BaseFile + Duration int + Caption string +} + +// Values returns url.Values representation of VideoConfig +func (config VideoConfig) Values() (url.Values, error) { + v, _ := config.BaseChat.Values() + + v.Add(config.Name(), config.FileID) + if config.Duration != 0 { + v.Add("duration", strconv.Itoa(config.Duration)) + } + if config.Caption != "" { + v.Add("caption", config.Caption) + } + + return v, nil +} + +// Params returns map[string]string representation of VideoConfig +func (config VideoConfig) Params() (map[string]string, error) { + params, _ := config.BaseFile.Params() + + return params, nil +} + +// Name return field name for uploading file +func (config VideoConfig) Name() string { + return "video" +} + +// Method returns Telegram API method name for sending Video +func (config VideoConfig) Method() string { + return "sendVideo" +} + +// VoiceConfig contains information about a SendVoice request. +type VoiceConfig struct { + BaseFile + Duration int +} + +// Values returns url.Values representation of VoiceConfig +func (config VoiceConfig) Values() (url.Values, error) { + v, _ := config.BaseChat.Values() + + v.Add(config.Name(), config.FileID) + if config.Duration != 0 { + v.Add("duration", strconv.Itoa(config.Duration)) + } + + return v, nil +} + +// Params returns map[string]string representation of VoiceConfig +func (config VoiceConfig) Params() (map[string]string, error) { + params, _ := config.BaseFile.Params() + + if config.Duration != 0 { + params["duration"] = strconv.Itoa(config.Duration) + } + + return params, nil +} + +// Name return field name for uploading file +func (config VoiceConfig) Name() string { + return "voice" +} + +// Method returns Telegram API method name for sending Voice +func (config VoiceConfig) Method() string { + return "sendVoice" +} + +// LocationConfig contains information about a SendLocation request. +type LocationConfig struct { + BaseChat + Latitude float64 + Longitude float64 +} + +// Values returns url.Values representation of LocationConfig +func (config LocationConfig) Values() (url.Values, error) { + v, _ := config.BaseChat.Values() + + v.Add("latitude", strconv.FormatFloat(config.Latitude, 'f', 6, 64)) + v.Add("longitude", strconv.FormatFloat(config.Longitude, 'f', 6, 64)) + + return v, nil +} + +// Method returns Telegram API method name for sending Location +func (config LocationConfig) Method() string { + return "sendLocation" +} + +// ChatActionConfig contains information about a SendChatAction request. +type ChatActionConfig struct { + BaseChat + Action string +} + +// Values returns url.Values representation of ChatActionConfig +func (config ChatActionConfig) Values() (url.Values, error) { + v, _ := config.BaseChat.Values() + v.Add("action", config.Action) + return v, nil +} + +// Method returns Telegram API method name for sending ChatAction +func (config ChatActionConfig) Method() string { + return "sendChatAction" +} + +// 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 { + 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..6a5916b 100644 --- a/helpers.go +++ b/helpers.go @@ -10,10 +10,9 @@ 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, + BaseChat: BaseChat{ChatID: chatID, ReplyToMessageID: 0}, + Text: text, DisableWebPagePreview: false, - ReplyToMessageID: 0, } } @@ -23,7 +22,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, + BaseChat: BaseChat{ChatID: chatID}, FromChatID: fromChatID, MessageID: messageID, } @@ -36,9 +35,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, - UseExistingPhoto: false, - File: file, + BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, File: file, UseExisting: false}, } } @@ -48,9 +45,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, - UseExistingPhoto: true, - FileID: fileID, + BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, FileID: fileID, UseExisting: true}, } } @@ -61,9 +56,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, - UseExistingAudio: false, - File: file, + BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, File: file, UseExisting: false}, } } @@ -73,9 +66,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, - UseExistingAudio: true, - FileID: fileID, + BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, FileID: fileID, UseExisting: true}, } } @@ -86,9 +77,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, - UseExistingDocument: false, - File: file, + BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, File: file, UseExisting: false}, } } @@ -98,9 +87,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, - UseExistingDocument: true, - FileID: fileID, + BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, FileID: fileID, UseExisting: true}, } } @@ -110,9 +97,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, - UseExistingSticker: false, - File: file, + BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, File: file, UseExisting: false}, } } @@ -122,9 +107,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, - UseExistingSticker: true, - FileID: fileID, + BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, FileID: fileID, UseExisting: true}, } } @@ -135,9 +118,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, - UseExistingVideo: false, - File: file, + BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, File: file, UseExisting: false}, } } @@ -147,9 +128,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, - UseExistingVideo: true, - FileID: fileID, + BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, FileID: fileID, UseExisting: true}, } } @@ -160,9 +139,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, - UseExistingVoice: false, - File: file, + BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, File: file, UseExisting: false}, } } @@ -172,9 +149,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, - UseExistingVoice: true, - FileID: fileID, + BaseFile: BaseFile{BaseChat: BaseChat{ChatID: chatID}, FileID: fileID, UseExisting: true}, } } @@ -184,11 +159,9 @@ 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, - Latitude: latitude, - Longitude: longitude, - ReplyToMessageID: 0, - ReplyMarkup: nil, + BaseChat: BaseChat{ChatID: chatID, ReplyToMessageID: 0, ReplyMarkup: nil}, + Latitude: latitude, + Longitude: longitude, } } @@ -198,8 +171,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, + BaseChat: BaseChat{ChatID: chatID}, + Action: action, } } @@ -233,8 +206,7 @@ func NewWebhook(link string) WebhookConfig { u, _ := url.Parse(link) return WebhookConfig{ - URL: u, - Clear: false, + URL: u, } } @@ -247,7 +219,6 @@ func NewWebhookWithCert(link string, file interface{}) WebhookConfig { return WebhookConfig{ URL: u, - Clear: false, Certificate: file, } } 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/tests/audio.mp3 b/tests/audio.mp3 new file mode 100644 index 0000000..06b0284 Binary files /dev/null and b/tests/audio.mp3 differ diff --git a/tests/cert.pem b/tests/cert.pem new file mode 100644 index 0000000..aa2bb94 --- /dev/null +++ b/tests/cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC0zCCAbugAwIBAgIJAPYfllX657axMA0GCSqGSIb3DQEBCwUAMAAwHhcNMTUx +MTIxMTExMDQxWhcNMjUwODIwMTExMDQxWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAoMMSIIgYx8pT8Kz1O8Ukd/JVyqBQYRSo0enqEzo7295VROXq +TUthbEbdi0OczUfl4IsAWppOSRrDwEguJZ0cJ/r6IxGsbrCdQr2MjgiomYtAXKKQ +GAGL5Wls+AzcRNV0OszVJzkDNFYZzgNejyitGJSNEQMyU8r2gyPyIWP9MQKQst8y +Mg91R/7l9jwf6AWwNxykZlYZurtsQ6XsBPZpF9YOFL7vZYPhKUFiNEm+74RpojC7 +Gt6nztYAUI2V/F+1uoXAr8nLpbj9SD0VSwyZLRG1uIVLBzhb0lfOIzAvJ45EKki9 +nejyoXfH1U5+iMzdSAdcy3MCBhpEZwJPqhDqeQIDAQABo1AwTjAdBgNVHQ4EFgQU +JE0RLM+ohLnlDz0Qk0McCxtDK2MwHwYDVR0jBBgwFoAUJE0RLM+ohLnlDz0Qk0Mc +CxtDK2MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAEmgME00JYuYZ +4wNaGrJskZ05ZnP+TXJusmBui9ToQ4UoykuyY5rsdGQ3SdzXPLdmd2nfMsw63iK2 +D7rjcH/rmn6fRccZqN0o0SXd/EuHeIoeW1Xnnivbt71b6mcOAeNg1UsMYxnMAVl0 +ywdkta8gURltagSfXoUbqlnSxn/zCwqaxxcQXA/CnunvRsFtQrwWjDBPg/BPULHX +DEh2AactGtnGqEZ5iap/VCOVnmL6iPdJ1x5UIF/gS6U96wL+GHfcs1jCvPg+GEwR +3inh9oTXG9L21ge4lbGiPUIMBjtVcB3bXuQbOfec9Cr3ZhcQeZj680BIRxD/pNpA +O/XeCfjfkw== +-----END CERTIFICATE----- diff --git a/tests/image.jpg b/tests/image.jpg new file mode 100644 index 0000000..eddc186 Binary files /dev/null and b/tests/image.jpg differ diff --git a/tests/key.pem b/tests/key.pem new file mode 100644 index 0000000..034b703 --- /dev/null +++ b/tests/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQCgwxIgiBjHylPw +rPU7xSR38lXKoFBhFKjR6eoTOjvb3lVE5epNS2FsRt2LQ5zNR+XgiwBamk5JGsPA +SC4lnRwn+vojEaxusJ1CvYyOCKiZi0BcopAYAYvlaWz4DNxE1XQ6zNUnOQM0VhnO +A16PKK0YlI0RAzJTyvaDI/IhY/0xApCy3zIyD3VH/uX2PB/oBbA3HKRmVhm6u2xD +pewE9mkX1g4Uvu9lg+EpQWI0Sb7vhGmiMLsa3qfO1gBQjZX8X7W6hcCvyculuP1I +PRVLDJktEbW4hUsHOFvSV84jMC8njkQqSL2d6PKhd8fVTn6IzN1IB1zLcwIGGkRn +Ak+qEOp5AgMBAAECggEBAJ/dPCJzlEjhL5XPONLmGXzZ1Gx5/VR86eBMv0O9jhb3 +wk2QYO3aPxggZGD/rGcKz1L6hzCR77WM0wpb/N/Um1I6pxHGmnU8VjYvLh10CM0f +h7JWyfnFV+ubagxFJamhpkJuvKyTaldaI7EU8qxj47Xky18Wka53z6nbTgXcW8Sm +V4CJy9OHNgKJQnylX6zOAaxVngSGde3xLslLjsYK4w9b2+OkCSUST2XXdo+ZLXxl +cs0lEPFRM1Xh9/E6UrDrJMHHzio53L/W/+a8sIar1upgSY52pyD/tA7VSrAJ9nYC +RezOU81VTLfMO+TYmgZzSUQJYh0cR4yqJe+wgl4U550CgYEA1EcS6Z+PO5Pr3u2+ +XevawSAal6y9ONkkdOoASC977W37nn0E1wlQo41dR6DESCJfiSMeN0KbmXj5Wnc/ +ADu+73iGwC90G9Qs9sjD7KAFBJvuj0V8hxvpWRdIBBbf7rlOj3CV0iXRYjkJbyJa +cxuR0kiv4gTWmm5Cq+5ir8t1Oc8CgYEAwd+xOaDerNR481R+QmENFw+oR2EVMq3Q +B/vinLK0PemQWrh32iBcd+vhSilOSQtUm1nko1jLK8C4s8X2vZYua4m5tcK9VqCt +maCCq/ffxzsoW/GN8japnduz+qA+hKWJzW/aYR8tsOeqzjVqj4iIqPI4HuokrDi/ +UD/QLgq5UTcCgYEAk2ZC0Kx15dXB7AtDq63xOTcUoAtXXRkSgohV58npEKXVGWkQ +Kk0SjG7Fvc35XWlY0z3qZk6/AuOIqfOxcHUMEPatAtgwlH5RNo+T1EQNF/U6wotq +e9q6vp026XgEyJwt29Y+giy2ZrDaRywgiFs1d0H3t0bKyXMUopQmPJFXdesCgYEA +psCxXcDpZjxGX/zPsGZrbOdxtRtisTlg0k0rp93pO8tV90HtDHeDMT54g2ItzJPr +TMev6XOpJNPZyf6+8GhpOuO2EQkT85u2VYoCeslz95gBabvFfIzZrUZYcnw76bm8 +YjAP5DN+CEfq2PyG0Df+W1ojPSvlKSCSJQMOG1vr81cCgYEAkjPY5WR99uJxYBNI +OTFMSkETgDUbPXBu/E/h5Dtn79v8Moj9FvC7+q6sg9qXhrGhfK2xDev3/sTrbS/E +Gcf8UNIne3AXsoAS8MtkOwJXHkYaTIboIYgDX4LlDmbGQlIRaWgyh2POI6VtjLBT +ms6AdsdpIB6As9xNUBUwj/RnTZQ= +-----END PRIVATE KEY----- diff --git a/tests/video.mp4 b/tests/video.mp4 new file mode 100644 index 0000000..a203d0c Binary files /dev/null and b/tests/video.mp4 differ diff --git a/tests/voice.ogg b/tests/voice.ogg new file mode 100644 index 0000000..0d7f43e Binary files /dev/null and b/tests/voice.ogg differ diff --git a/tgutils/audio.go b/tgutils/audio.go deleted file mode 100644 index f7a0034..0000000 --- a/tgutils/audio.go +++ /dev/null @@ -1,92 +0,0 @@ -// Package tgutils provides extra functions to make certain tasks easier. -package tgutils - -import ( - "github.com/syfaro/telegram-bot-api" - "os" - "os/exec" - "path/filepath" - "strconv" - "sync" - "time" -) - -var rand uint32 -var randmu sync.Mutex - -func reseed() uint32 { - return uint32(time.Now().UnixNano() + int64(os.Getpid())) -} - -func nextSuffix() string { - randmu.Lock() - r := rand - if r == 0 { - r = reseed() - } - r = r*1664525 + 1013904223 // constants from Numerical Recipes - rand = r - randmu.Unlock() - return strconv.Itoa(int(1e9 + r%1e9))[1:] -} - -// this function ripped from ioutils.TempFile, except with a suffix, instead of prefix. -func tempFileWithSuffix(dir, suffix string) (f *os.File, err error) { - if dir == "" { - dir = os.TempDir() - } - - nconflict := 0 - for i := 0; i < 10000; i++ { - name := filepath.Join(dir, nextSuffix()+suffix) - f, err = os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600) - if os.IsExist(err) { - if nconflict++; nconflict > 10 { - randmu.Lock() - rand = reseed() - randmu.Unlock() - } - continue - } - break - } - return -} - -// EncodeAudio takes a file and attempts to convert it to a .ogg for Telegram. -// It then updates the path to the audio file in the AudioConfig. -// -// This function requires ffmpeg and opusenc to be installed on the system! -func EncodeAudio(audio *tgbotapi.AudioConfig) error { - f, err := tempFileWithSuffix(os.TempDir(), "_tgutils.ogg") - if err != nil { - return err - } - defer f.Close() - - ffmpegArgs := []string{ - "-i", - audio.FilePath, - "-f", - "wav", - "-", - } - - opusArgs := []string{ - "--bitrate", - "256", - "-", - f.Name(), - } - - c1 := exec.Command("ffmpeg", ffmpegArgs...) - c2 := exec.Command("opusenc", opusArgs...) - - c2.Stdin, _ = c1.StdoutPipe() - c2.Stdout = os.Stdout - c2.Start() - c1.Run() - c2.Wait() - - return nil -} diff --git a/types.go b/types.go index ee71508..26a950b 100644 --- a/types.go +++ b/types.go @@ -3,6 +3,7 @@ package tgbotapi import ( "encoding/json" "fmt" + "strings" "time" ) @@ -112,6 +113,16 @@ func (m *Message) IsGroup() bool { return m.From.ID != m.Chat.ID } +// IsCommand returns true if message starts from / +func (m *Message) IsCommand() bool { + return m.Text != "" && m.Text[0] == '/' +} + +// Command returns first word from message +func (m *Message) Command() string { + return strings.Split(m.Text, " ")[0] +} + // PhotoSize contains information about photos, including ID and Width and Height. type PhotoSize struct { FileID string `json:"file_id"` diff --git a/types_test.go b/types_test.go new file mode 100644 index 0000000..17ac99e --- /dev/null +++ b/types_test.go @@ -0,0 +1,74 @@ +package tgbotapi_test + +import ( + "github.com/Syfaro/telegram-bot-api" + "testing" + "time" +) + +func TestUserStringWith(t *testing.T) { + user := tgbotapi.User{0, "Test", "Test", ""} + + if user.String() != "Test Test" { + t.Fail() + } +} + +func TestUserStringWithUserName(t *testing.T) { + user := tgbotapi.User{0, "Test", "Test", "@test"} + + if user.String() != "@test" { + t.Fail() + } +} + +func TestMessageIsGroup(t *testing.T) { + from := tgbotapi.User{ID: 0} + chat := tgbotapi.Chat{ID: 10} + message := tgbotapi.Message{From: from, Chat: chat} + + if message.IsGroup() != true { + t.Fail() + } +} + +func TestMessageTime(t *testing.T) { + message := tgbotapi.Message{Date: 0} + + date := time.Unix(0, 0) + if message.Time() != date { + t.Fail() + } +} + +func TestChatIsPrivate(t *testing.T) { + chat := tgbotapi.Chat{ID: 10, Type: "private"} + + if chat.IsPrivate() != true { + t.Fail() + } +} + +func TestChatIsGroup(t *testing.T) { + chat := tgbotapi.Chat{ID: 10, Type: "group"} + + if chat.IsGroup() != true { + t.Fail() + } +} + +func TestChatIsChannel(t *testing.T) { + chat := tgbotapi.Chat{ID: 10, Type: "channel"} + + if chat.IsChannel() != true { + t.Fail() + } +} + +func TestFileLink(t *testing.T) { + file := tgbotapi.File{FilePath: "test/test.txt"} + + if file.Link("token") != "https://api.telegram.org/file/bottoken/test/test.txt" { + t.Fail() + } +} 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 - }) -}