Merge branch 'master' of https://github.com/go-telegram-bot-api/telegram-bot-api into wehook-validation

bot-api-6.1
Dmitriy Kharchenko 2020-07-21 12:06:24 +03:00
commit 20b57111fc
10 changed files with 367 additions and 55 deletions

View File

@ -3,4 +3,6 @@ language: go
go:
- '1.10'
- '1.11'
- '1.12'
- '1.13'
- tip

View File

@ -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.

123
bot.go
View File

@ -19,6 +19,10 @@ import (
"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"`
@ -26,7 +30,7 @@ type BotAPI struct {
Buffer int `json:"buffer"`
Self User `json:"-"`
Client *http.Client `json:"-"`
Client HttpClient `json:"-"`
shutdownChannel chan interface{}
apiEndpoint string
@ -36,21 +40,29 @@ type BotAPI struct {
//
// 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 @@ func NewBotAPIWithClient(token string, client *http.Client) (*BotAPI, error) {
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 @@ func (bot *BotAPI) MakeRequest(endpoint string, params url.Values) (APIResponse,
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 @@ func (bot *BotAPI) UploadFile(endpoint string, params map[string]string, fieldna
}
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 @@ func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
// 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 @@ func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (UpdatesChannel, error) {
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)
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)
_, _ = 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 @@ func (bot *BotAPI) UnbanChatMember(config ChatMemberConfig) (APIResponse, error)
}
// 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 @@ func (bot *BotAPI) AnswerPreCheckoutQuery(config PreCheckoutConfig) (APIResponse
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 @@ func (bot *BotAPI) DeleteChatPhoto(config DeleteChatPhotoConfig) (APIResponse, e
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
}

View File

@ -8,7 +8,7 @@ import (
"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 @@ func TestSendWithExistingStickerAndKeyboardHide(t *testing.T) {
}
}
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 @@ func TestSetWebhookWithoutCert(t *testing.T) {
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)
}
@ -593,6 +622,40 @@ func ExampleNewWebhook() {
}
}
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() {
bot, err := tgbotapi.NewBotAPI("MyAwesomeBotToken") // create new bot
if err != nil {

View File

@ -37,6 +37,7 @@ const (
// Constant values for ParseMode in MessageConfig
const (
ModeMarkdown = "Markdown"
ModeMarkdownV2 = "MarkdownV2"
ModeHTML = "HTML"
)
@ -1138,6 +1139,7 @@ type PreCheckoutConfig struct {
// DeleteMessageConfig contains information of a message in a chat to delete.
type DeleteMessageConfig struct {
ChannelUsername string
ChatID int64
MessageID int
}
@ -1149,7 +1151,12 @@ func (config DeleteMessageConfig) method() string {
func (config DeleteMessageConfig) values() (url.Values, error) {
v := url.Values{}
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 @@ func (config DeleteChatPhotoConfig) values() (url.Values, error) {
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"
}

5
go.mod 100644
View File

@ -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

2
go.sum 100644
View File

@ -0,0 +1,2 @@
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=

View File

@ -18,6 +18,30 @@ func NewMessage(chatID int64, text string) MessageConfig {
}
}
// 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 @@ func NewDeleteMessage(chatID int64, messageID int) DeleteMessageConfig {
// 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 @@ func NewInlineQueryResultArticleMarkdown(id, title, messageText string) InlineQu
}
}
// 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 @@ func NewInlineQueryResultMPEG4GIF(id, url string) InlineQueryResultMPEG4GIF {
}
}
// 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",
@ -533,6 +571,16 @@ func NewInlineQueryResultCachedVideo(id, videoID, title string) InlineQueryResul
}
}
// 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,
}
}
// NewInlineQueryResultAudio creates a new inline query audio.
func NewInlineQueryResultAudio(id, url, title string) InlineQueryResultAudio {
return InlineQueryResultAudio{
@ -604,6 +652,18 @@ func NewInlineQueryResultLocation(id, title string, latitude, longitude float64)
}
}
// 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{
@ -615,6 +675,18 @@ func NewEditMessageText(chatID int64, messageID int, text string) EditMessageTex
}
}
// 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,
}
}
// NewEditMessageCaption allows you to edit the caption of a message.
func NewEditMessageCaption(chatID int64, messageID int, caption string) EditMessageCaptionConfig {
return EditMessageCaptionConfig{
@ -704,6 +776,13 @@ func NewReplyKeyboard(rows ...[]KeyboardButton) ReplyKeyboardMarkup {
}
}
// 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
// and data for a callback.
func NewInlineKeyboardButtonData(text, data string) InlineKeyboardButton {

View File

@ -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 @@ func TestNewEditMessageReplyMarkup(t *testing.T) {
}
}
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()
}
}

View File

@ -64,6 +64,9 @@ type User struct {
// 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,6 +341,7 @@ type Document struct {
// Sticker contains information about a sticker.
type Sticker struct {
FileUniqueID string `json:"file_unique_id"`
FileID string `json:"file_id"`
Width int `json:"width"`
Height int `json:"height"`
@ -345,6 +349,16 @@ type Sticker struct {
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 @@ type WebhookInfo struct {
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 @@ type InlineQueryResultPhoto struct {
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 @@ type InlineQueryResultCachedVideo struct {
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
@ -827,6 +854,23 @@ type InlineQueryResultLocation struct {
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"`
ThumbWidth int `json:"thumb_width"`
ThumbHeight int `json:"thumb_height"`
}
// InlineQueryResultGame is an inline query response game.
type InlineQueryResultGame struct {
Type string `json:"type"`