diff --git a/.travis.yml b/.travis.yml index 5769aa1..51865c8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,4 +3,6 @@ language: go go: - '1.10' - '1.11' + - '1.12' + - '1.13' - tip diff --git a/README.md b/README.md index bf341d2..43b33ed 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/bot.go b/bot.go index 3bdb49a..5a13ec7 100644 --- a/bot.go +++ b/bot.go @@ -19,14 +19,18 @@ 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"` 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 @@ 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 +} diff --git a/bot_test.go b/bot_test.go index 60f3e65..1118a08 100644 --- a/bot_test.go +++ b/bot_test.go @@ -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 { diff --git a/configs.go b/configs.go index 181d4e4..3989774 100644 --- a/configs.go +++ b/configs.go @@ -36,8 +36,9 @@ const ( // Constant values for ParseMode in MessageConfig const ( - ModeMarkdown = "Markdown" - ModeHTML = "HTML" + ModeMarkdown = "Markdown" + ModeMarkdownV2 = "MarkdownV2" + ModeHTML = "HTML" ) // Library errors @@ -1138,8 +1139,9 @@ type PreCheckoutConfig struct { // 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) method() string { 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 @@ 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" +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7df46f4 --- /dev/null +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8660600 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= +github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= diff --git a/helpers.go b/helpers.go index 70180bc..e116224 100644 --- a/helpers.go +++ b/helpers.go @@ -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 { diff --git a/helpers_test.go b/helpers_test.go index 9542f02..ec15a4e 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -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() + } +} diff --git a/types.go b/types.go index 52cb36c..44f1a58 100644 --- a/types.go +++ b/types.go @@ -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,13 +341,24 @@ type Document struct { // 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 @@ 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"`