Merge branch 'master' of https://github.com/go-telegram-bot-api/telegram-bot-api into wehook-validation
@@ -3,7 +3,7 @@
[![GoDoc](https://godoc.org/github.com/go-telegram-bot-api/telegram-bot-api?status.svg)](http://godoc.org/github.com/go-telegram-bot-api/telegram-bot-api) [![Travis](https://travis-ci.org/go-telegram-bot-api/telegram-bot-api.svg)](https://travis-ci.org/go-telegram-bot-api/telegram-bot-api) -All methods are fairly self explanatory, and reading the godoc page should +All methods are fairly self explanatory, and reading the [godoc](http://godoc.org/github.com/go-telegram-bot-api/telegram-bot-api) page should explain everything. If something isn't clear, open an issue or submit a pull request.
@@ -19,14 +19,18 @@
"github.com/technoweenie/multipartstreamer" ) +type HttpClient interface { + Do(req *http.Request) (*http.Response, error) +} + // BotAPI allows you to interact with the Telegram Bot API. type BotAPI struct { Token string `json:"token"` Debug bool `json:"debug"` Buffer int `json:"buffer"` - Self User `json:"-"` - Client *http.Client `json:"-"` + Self User `json:"-"` + Client HttpClient `json:"-"` shutdownChannel chan interface{} apiEndpoint string@@ -36,21 +40,29 @@ // NewBotAPI creates a new BotAPI instance.
// // It requires a token, provided by @BotFather on Telegram. func NewBotAPI(token string) (*BotAPI, error) { - return NewBotAPIWithClient(token, &http.Client{}) + return NewBotAPIWithClient(token, APIEndpoint, &http.Client{}) +} + +// NewBotAPIWithAPIEndpoint creates a new BotAPI instance +// and allows you to pass API endpoint. +// +// It requires a token, provided by @BotFather on Telegram and API endpoint. +func NewBotAPIWithAPIEndpoint(token, apiEndpoint string) (*BotAPI, error) { + return NewBotAPIWithClient(token, apiEndpoint, &http.Client{}) } // NewBotAPIWithClient creates a new BotAPI instance // and allows you to pass a http.Client. // -// It requires a token, provided by @BotFather on Telegram. -func NewBotAPIWithClient(token string, client *http.Client) (*BotAPI, error) { +// It requires a token, provided by @BotFather on Telegram and API endpoint. +func NewBotAPIWithClient(token, apiEndpoint string, client HttpClient) (*BotAPI, error) { bot := &BotAPI{ Token: token, Client: client, Buffer: 100, shutdownChannel: make(chan interface{}), - apiEndpoint: APIEndpoint, + apiEndpoint: apiEndpoint, } self, err := bot.GetMe()@@ -63,15 +75,22 @@
return bot, nil } -func (b *BotAPI) SetAPIEndpoint(apiEndpoint string) { - b.apiEndpoint = apiEndpoint +// SetAPIEndpoint add telegram apiEndpont to Bot +func (bot *BotAPI) SetAPIEndpoint(apiEndpoint string) { + bot.apiEndpoint = apiEndpoint } // MakeRequest makes a request to a specific endpoint with our token. func (bot *BotAPI) MakeRequest(endpoint string, params url.Values) (APIResponse, error) { method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint) - resp, err := bot.Client.PostForm(method, params) + req, err := http.NewRequest("POST", method, strings.NewReader(params.Encode())) + if err != nil { + return APIResponse{}, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := bot.Client.Do(req) if err != nil { return APIResponse{}, err }@@ -92,7 +111,7 @@ parameters := ResponseParameters{}
if apiResp.Parameters != nil { parameters = *apiResp.Parameters } - return apiResp, Error{Code: apiResp.ErrorCode, Message: apiResp.Description, ResponseParameters: parameters} + return apiResp, &Error{Code: apiResp.ErrorCode, Message: apiResp.Description, ResponseParameters: parameters} } return apiResp, nil@@ -226,7 +245,11 @@ return APIResponse{}, err
} if !apiResp.Ok { - return APIResponse{}, errors.New(apiResp.Description) + parameters := ResponseParameters{} + if apiResp.Parameters != nil { + parameters = *apiResp.Parameters + } + return apiResp, Error{Code: apiResp.ErrorCode, Message: apiResp.Description, ResponseParameters: parameters} } return apiResp, nil@@ -438,7 +461,7 @@ }
// RemoveWebhook unsets the webhook. func (bot *BotAPI) RemoveWebhook() (APIResponse, error) { - return bot.MakeRequest("setWebhook", url.Values{}) + return bot.MakeRequest("deleteWebhook", url.Values{}) } // SetWebhook sets a webhook.@@ -495,6 +518,7 @@ go func() {
for { select { case <-bot.shutdownChannel: + close(ch) return default: }@@ -533,40 +557,46 @@ func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
ch := make(chan Update, bot.Buffer) http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodPost { - errMsg, _ := json.Marshal(map[string]string{"error": "Wrong HTTP method, required POST"}) - w.WriteHeader(http.StatusMethodNotAllowed) - w.Header().Set("Content-Type", "application/json") - w.Write(errMsg) - return - } - - bytes, err := ioutil.ReadAll(r.Body) + update, err := bot.HandleUpdate(r) if err != nil { errMsg, _ := json.Marshal(map[string]string{"error": err.Error()}) w.WriteHeader(http.StatusBadRequest) w.Header().Set("Content-Type", "application/json") - w.Write(errMsg) + _, _ = w.Write(errMsg) return } - r.Body.Close() - var update Update - err = json.Unmarshal(bytes, &update) - if err != nil { - errMsg, _ := json.Marshal(map[string]string{"error": err.Error()}) - w.WriteHeader(http.StatusBadRequest) - w.Header().Set("Content-Type", "application/json") - w.Write(errMsg) - return - } - - ch <- update + ch <- *update }) return ch } +// HandleUpdate parses and returns update received via webhook +func (bot *BotAPI) HandleUpdate(r *http.Request) (*Update, error) { + if r.Method != http.MethodPost { + err := errors.New("wrong HTTP method required POST") + return nil, err + } + + payload, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, err + } + + if err := r.Body.Close(); err != nil { + return nil, err + } + + var update Update + err = json.Unmarshal(payload, &update) + if err != nil { + return nil, err + } + + return &update, nil +} + // AnswerInlineQuery sends a response to an inline query. // // Note that you must respond to an inline query within 30 seconds.@@ -762,9 +792,9 @@ return bot.MakeRequest("unbanChatMember", v)
} // RestrictChatMember to restrict a user in a supergroup. The bot must be an -//administrator in the supergroup for this to work and must have the -//appropriate admin rights. Pass True for all boolean parameters to lift -//restrictions from a user. Returns True on success. +// administrator in the supergroup for this to work and must have the +// appropriate admin rights. Pass True for all boolean parameters to lift +// restrictions from a user. Returns True on success. func (bot *BotAPI) RestrictChatMember(config RestrictChatMemberConfig) (APIResponse, error) { v := url.Values{}@@ -884,7 +914,7 @@
v.Add("pre_checkout_query_id", config.PreCheckoutQueryID) v.Add("ok", strconv.FormatBool(config.OK)) if config.OK != true { - v.Add("error", config.ErrorMessage) + v.Add("error_message", config.ErrorMessage) } bot.debugLog("answerPreCheckoutQuery", v, nil)@@ -996,3 +1026,22 @@ bot.debugLog(config.method(), v, nil)
return bot.MakeRequest(config.method(), v) } + +// GetStickerSet get a sticker set. +func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) { + v, err := config.values() + if err != nil { + return StickerSet{}, err + } + bot.debugLog(config.method(), v, nil) + res, err := bot.MakeRequest(config.method(), v) + if err != nil { + return StickerSet{}, err + } + stickerSet := StickerSet{} + err = json.Unmarshal(res.Result, &stickerSet) + if err != nil { + return StickerSet{}, err + } + return stickerSet, nil +}
@@ -8,7 +8,7 @@ "os"
"testing" "time" - "github.com/go-telegram-bot-api/telegram-bot-api" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" ) const (@@ -402,6 +402,32 @@ t.Fail()
} } +func TestSendWithDice(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewDice(ChatID) + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } + +} + +func TestSendWithDiceWithEmoji(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewDiceWithEmoji(ChatID, "🏀") + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } + +} + func TestGetFile(t *testing.T) { bot, _ := getBot(t)@@ -497,6 +523,9 @@ info, err := bot.GetWebhookInfo()
if err != nil { t.Error(err) } + if info.MaxConnections == 0 { + t.Errorf("Expected maximum connections to be greater than 0") + } if info.LastErrorDate != 0 { t.Errorf("[Telegram callback failed]%s", info.LastErrorMessage) }@@ -591,6 +620,40 @@
for update := range updates { log.Printf("%+v\n", update) } +} + +func ExampleWebhookHandler() { + 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) + } + info, err := bot.GetWebhookInfo() + if err != nil { + log.Fatal(err) + } + if info.LastErrorDate != 0 { + log.Printf("[Telegram callback failed]%s", info.LastErrorMessage) + } + + http.HandleFunc("/"+bot.Token, func(w http.ResponseWriter, r *http.Request) { + update, err := bot.HandleUpdate(r) + if err != nil { + log.Printf("%+v\n", err.Error()) + } else { + log.Printf("%+v\n", *update) + } + }) + + go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil) } func ExampleAnswerInlineQuery() {
@@ -36,8 +36,9 @@ )
// Constant values for ParseMode in MessageConfig const ( - ModeMarkdown = "Markdown" - ModeHTML = "HTML" + ModeMarkdown = "Markdown" + ModeMarkdownV2 = "MarkdownV2" + ModeHTML = "HTML" ) // Library errors@@ -1138,8 +1139,9 @@ }
// DeleteMessageConfig contains information of a message in a chat to delete. type DeleteMessageConfig struct { - ChatID int64 - MessageID int + ChannelUsername string + ChatID int64 + MessageID int } func (config DeleteMessageConfig) method() string {@@ -1149,7 +1151,12 @@
func (config DeleteMessageConfig) values() (url.Values, error) { v := url.Values{} - v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + if config.ChannelUsername == "" { + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + } else { + v.Add("chat_id", config.ChannelUsername) + } + v.Add("message_id", strconv.Itoa(config.MessageID)) return v, nil@@ -1262,3 +1269,45 @@ v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
return v, nil } + +// GetStickerSetConfig contains information for get sticker set. +type GetStickerSetConfig struct { + Name string +} + +func (config GetStickerSetConfig) method() string { + return "getStickerSet" +} + +func (config GetStickerSetConfig) values() (url.Values, error) { + v := url.Values{} + v.Add("name", config.Name) + return v, nil +} + +// DiceConfig contains information about a sendDice request. +type DiceConfig struct { + BaseChat + // Emoji on which the dice throw animation is based. + // Currently, must be one of “🎲”, “🎯”, or “🏀”. + // Dice can have values 1-6 for “🎲” and “🎯”, and values 1-5 for “🏀”. + // Defaults to “🎲” + Emoji string +} + +// values returns a url.Values representation of DiceConfig. +func (config DiceConfig) values() (url.Values, error) { + v, err := config.BaseChat.values() + if err != nil { + return v, err + } + if config.Emoji != "" { + v.Add("emoji", config.Emoji) + } + return v, nil +} + +// method returns Telegram API method name for sending Dice. +func (config DiceConfig) method() string { + return "sendDice" +}
@@ -0,0 +1,5 @@
+module github.com/go-telegram-bot-api/telegram-bot-api + +go 1.12 + +require github.com/technoweenie/multipartstreamer v1.0.1
@@ -0,0 +1,2 @@
+github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= +github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
@@ -18,6 +18,30 @@ DisableWebPagePreview: false,
} } +// NewDice creates a new DiceConfig. +// +// chatID is where to send it +func NewDice(chatID int64) DiceConfig { + return DiceConfig{ + BaseChat: BaseChat{ + ChatID: chatID, + }, + } +} + +// NewDiceWithEmoji creates a new DiceConfig. +// +// chatID is where to send it +// emoji is type of the Dice +func NewDiceWithEmoji(chatID int64, emoji string) DiceConfig { + return DiceConfig{ + BaseChat: BaseChat{ + ChatID: chatID, + }, + Emoji: emoji, + } +} + // NewDeleteMessage creates a request to delete a message. func NewDeleteMessage(chatID int64, messageID int) DeleteMessageConfig { return DeleteMessageConfig{@@ -29,7 +53,8 @@
// NewMessageToChannel creates a new Message that is sent to a channel // by username. // -// username is the username of the channel, text is the message text. +// username is the username of the channel, text is the message text, +// and the username should be in the form of `@username`. func NewMessageToChannel(username string, text string) MessageConfig { return MessageConfig{ BaseChat: BaseChat{@@ -437,6 +462,19 @@ },
} } +// NewInlineQueryResultArticleMarkdownV2 creates a new inline query article with MarkdownV2 parsing. +func NewInlineQueryResultArticleMarkdownV2(id, title, messageText string) InlineQueryResultArticle { + return InlineQueryResultArticle{ + Type: "article", + ID: id, + Title: title, + InputMessageContent: InputTextMessageContent{ + Text: messageText, + ParseMode: "MarkdownV2", + }, + } +} + // NewInlineQueryResultArticleHTML creates a new inline query article with HTML parsing. func NewInlineQueryResultArticleHTML(id, title, messageText string) InlineQueryResultArticle { return InlineQueryResultArticle{@@ -477,7 +515,7 @@ URL: url,
} } -// NewInlineQueryResultCachedPhoto create a new inline query with cached photo. +// NewInlineQueryResultCachedMPEG4GIF create a new inline query with cached MPEG4 GIF. func NewInlineQueryResultCachedMPEG4GIF(id, MPEG4GifID string) InlineQueryResultCachedMpeg4Gif { return InlineQueryResultCachedMpeg4Gif{ Type: "mpeg4_gif",@@ -530,6 +568,16 @@ Type: "video",
ID: id, VideoID: videoID, Title: title, + } +} + +// NewInlineQueryResultCachedSticker create a new inline query with cached sticker. +func NewInlineQueryResultCachedSticker(id, stickerID, title string) InlineQueryResultCachedSticker { + return InlineQueryResultCachedSticker{ + Type: "sticker", + ID: id, + StickerID: stickerID, + Title: title, } }@@ -604,12 +652,36 @@ Longitude: longitude,
} } +// NewInlineQueryResultVenue creates a new inline query venue. +func NewInlineQueryResultVenue(id, title, address string, latitude, longitude float64) InlineQueryResultVenue { + return InlineQueryResultVenue{ + Type: "venue", + ID: id, + Title: title, + Address: address, + Latitude: latitude, + Longitude: longitude, + } +} + // NewEditMessageText allows you to edit the text of a message. func NewEditMessageText(chatID int64, messageID int, text string) EditMessageTextConfig { return EditMessageTextConfig{ BaseEdit: BaseEdit{ ChatID: chatID, MessageID: messageID, + }, + Text: text, + } +} + +// NewEditMessageTextAndMarkup allows you to edit the text and replymarkup of a message. +func NewEditMessageTextAndMarkup(chatID int64, messageID int, text string, replyMarkup InlineKeyboardMarkup) EditMessageTextConfig { + return EditMessageTextConfig{ + BaseEdit: BaseEdit{ + ChatID: chatID, + MessageID: messageID, + ReplyMarkup: &replyMarkup, }, Text: text, }@@ -702,6 +774,13 @@ return ReplyKeyboardMarkup{
ResizeKeyboard: true, Keyboard: keyboard, } +} + +// NewOneTimeReplyKeyboard creates a new one time keyboard. +func NewOneTimeReplyKeyboard(rows ...[]KeyboardButton) ReplyKeyboardMarkup { + markup := NewReplyKeyboard(rows...) + markup.OneTimeKeyboard = true + return markup } // NewInlineKeyboardButtonData creates an inline keyboard button with text
@@ -1,8 +1,9 @@
package tgbotapi_test import ( - "github.com/go-telegram-bot-api/telegram-bot-api" "testing" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" ) func TestNewInlineQueryResultArticle(t *testing.T) {@@ -175,3 +176,21 @@ t.Fail()
} } + +func TestNewDice(t *testing.T) { + dice := tgbotapi.NewDice(42) + + if dice.ChatID != 42 || + dice.Emoji != "" { + t.Fail() + } +} + +func TestNewDiceWithEmoji(t *testing.T) { + dice := tgbotapi.NewDiceWithEmoji(42, "🏀") + + if dice.ChatID != 42 || + dice.Emoji != "🏀" { + t.Fail() + } +}
@@ -64,6 +64,9 @@ //
// It is normally a user's username, but falls back to a first/last // name as available. func (u *User) String() string { + if u == nil { + return "" + } if u.UserName != "" { return u.UserName }@@ -338,13 +341,24 @@ }
// Sticker contains information about a sticker. type Sticker struct { - FileID string `json:"file_id"` - Width int `json:"width"` - Height int `json:"height"` - Thumbnail *PhotoSize `json:"thumb"` // optional - Emoji string `json:"emoji"` // optional - FileSize int `json:"file_size"` // optional - SetName string `json:"set_name"` // optional + FileUniqueID string `json:"file_unique_id"` + FileID string `json:"file_id"` + Width int `json:"width"` + Height int `json:"height"` + Thumbnail *PhotoSize `json:"thumb"` // optional + Emoji string `json:"emoji"` // optional + FileSize int `json:"file_size"` // optional + SetName string `json:"set_name"` // optional + IsAnimated bool `json:"is_animated"` // optional +} + +// StickerSet contains information about an sticker set. +type StickerSet struct { + Name string `json:"name"` + Title string `json:"title"` + IsAnimated bool `json:"is_animated"` + ContainsMasks bool `json:"contains_masks"` + Stickers []Sticker `json:"stickers"` } // ChatAnimation contains information about an animation.@@ -570,6 +584,7 @@ HasCustomCertificate bool `json:"has_custom_certificate"`
PendingUpdateCount int `json:"pending_update_count"` LastErrorDate int `json:"last_error_date"` // optional LastErrorMessage string `json:"last_error_message"` // optional + MaxConnections int `json:"max_connections"` // optional } // IsSet returns true if a webhook is currently set.@@ -634,6 +649,7 @@ ThumbURL string `json:"thumb_url"`
Title string `json:"title"` Description string `json:"description"` Caption string `json:"caption"` + ParseMode string `json:"parse_mode"` ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` InputMessageContent interface{} `json:"input_message_content,omitempty"` }@@ -736,6 +752,17 @@ ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"` } +// InlineQueryResultCachedSticker is an inline query response with cached sticker. +type InlineQueryResultCachedSticker struct { + Type string `json:"type"` // required + ID string `json:"id"` // required + StickerID string `json:"sticker_file_id"` // required + Title string `json:"title"` // required + ParseMode string `json:"parse_mode"` + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + // InlineQueryResultAudio is an inline query response audio. type InlineQueryResultAudio struct { Type string `json:"type"` // required@@ -820,6 +847,23 @@ ID string `json:"id"` // required
Latitude float64 `json:"latitude"` // required Longitude float64 `json:"longitude"` // required Title string `json:"title"` // required + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + InputMessageContent interface{} `json:"input_message_content,omitempty"` + ThumbURL string `json:"thumb_url"` + ThumbWidth int `json:"thumb_width"` + ThumbHeight int `json:"thumb_height"` +} + +// InlineQueryResultVenue is an inline query response venue. +type InlineQueryResultVenue struct { + Type string `json:"type"` // required + ID string `json:"id"` // required + Latitude float64 `json:"latitude"` // required + Longitude float64 `json:"longitude"` // required + Title string `json:"title"` // required + Address string `json:"address"` // required + FoursquareID string `json:"foursquare_id"` + FoursquareType string `json:"foursquare_type"` ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` InputMessageContent interface{} `json:"input_message_content,omitempty"` ThumbURL string `json:"thumb_url"`