diff --git a/bot.go b/bot.go index c7e473f..fb6deea 100644 --- a/bot.go +++ b/bot.go @@ -105,16 +105,17 @@ func (bot *BotAPI) makeMessageRequest(endpoint string, params url.Values) (Messa // // 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. +// a FileReader struct, or a url.URL. // // Note that if your FileReader has a size set to -1, it will read // the file into memory to calculate a size. 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: + ms.WriteFields(params) + fileHandle, err := os.Open(f) if err != nil { return APIResponse{}, err @@ -128,9 +129,13 @@ func (bot *BotAPI) UploadFile(endpoint string, params map[string]string, fieldna ms.WriteReader(fieldname, fileHandle.Name(), fi.Size(), fileHandle) case FileBytes: + ms.WriteFields(params) + buf := bytes.NewBuffer(f.Bytes) ms.WriteReader(fieldname, f.Name, int64(len(f.Bytes)), buf) case FileReader: + ms.WriteFields(params) + if f.Size != -1 { ms.WriteReader(fieldname, f.Name, f.Size, f.Reader) @@ -145,6 +150,10 @@ func (bot *BotAPI) UploadFile(endpoint string, params map[string]string, fieldna buf := bytes.NewBuffer(data) ms.WriteReader(fieldname, f.Name, int64(len(data)), buf) + case url.URL: + params[fieldname] = f.String() + + ms.WriteFields(params) default: return APIResponse{}, errors.New(ErrBadFileType) } @@ -424,6 +433,20 @@ func (bot *BotAPI) SetWebhook(config WebhookConfig) (APIResponse, error) { return apiResp, nil } +// GetWebhookInfo allows you to fetch information about a webhook and if +// one currently is set, along with pending update count and error messages. +func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) { + resp, err := bot.MakeRequest("getWebhookInfo", url.Values{}) + if err != nil { + return WebhookInfo{}, err + } + + var info WebhookInfo + err = json.Unmarshal(resp.Result, &info) + + return info, err +} + // GetUpdatesChan starts and returns a channel for getting updates. func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (<-chan Update, error) { updatesChan := make(chan Update, 100) @@ -495,8 +518,13 @@ func (bot *BotAPI) AnswerCallbackQuery(config CallbackConfig) (APIResponse, erro v := url.Values{} v.Add("callback_query_id", config.CallbackQueryID) - v.Add("text", config.Text) + if config.Text != "" { + v.Add("text", config.Text) + } v.Add("show_alert", strconv.FormatBool(config.ShowAlert)) + if config.URL != "" { + v.Add("url", config.URL) + } bot.debugLog("answerCallbackQuery", v, nil) @@ -648,3 +676,18 @@ func (bot *BotAPI) UnbanChatMember(config ChatMemberConfig) (APIResponse, error) return bot.MakeRequest("unbanChatMember", v) } + +// GetGameHighScores allows you to get the high scores for a game. +func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) { + v, _ := config.values() + + resp, err := bot.MakeRequest(config.method(), v) + if err != nil { + return []GameHighScore{}, err + } + + var highScores []GameHighScore + err = json.Unmarshal(resp.Result, &highScores) + + return highScores, err +} diff --git a/configs.go b/configs.go index b91d070..6d09b22 100644 --- a/configs.go +++ b/configs.go @@ -275,6 +275,7 @@ func (config PhotoConfig) method() string { // AudioConfig contains information about a SendAudio request. type AudioConfig struct { BaseFile + Caption string Duration int Performer string Title string @@ -431,6 +432,7 @@ func (config VideoConfig) method() string { // VoiceConfig contains information about a SendVoice request. type VoiceConfig struct { BaseFile + Caption string Duration int } @@ -539,6 +541,90 @@ func (config ContactConfig) method() string { return "sendContact" } +// GameConfig allows you to send a game. +type GameConfig struct { + BaseChat + GameShortName string +} + +func (config GameConfig) values() (url.Values, error) { + v, _ := config.BaseChat.values() + + v.Add("game_short_name", config.GameShortName) + + return v, nil +} + +func (config GameConfig) method() string { + return "sendGame" +} + +// SetGameScoreConfig allows you to update the game score in a chat. +type SetGameScoreConfig struct { + UserID int + Score int + ChatID int + ChannelUsername string + MessageID int + InlineMessageID string + EditMessage bool +} + +func (config SetGameScoreConfig) values() (url.Values, error) { + v := url.Values{} + + v.Add("user_id", strconv.Itoa(config.UserID)) + v.Add("score", strconv.Itoa(config.Score)) + if config.InlineMessageID == "" { + if config.ChannelUsername == "" { + v.Add("chat_id", strconv.Itoa(config.ChatID)) + } else { + v.Add("chat_id", config.ChannelUsername) + } + v.Add("message_id", strconv.Itoa(config.MessageID)) + } else { + v.Add("inline_message_id", config.InlineMessageID) + } + v.Add("edit_message", strconv.FormatBool(config.EditMessage)) + + return v, nil +} + +func (config SetGameScoreConfig) method() string { + return "setGameScore" +} + +// GetGameHighScoresConfig allows you to fetch the high scores for a game. +type GetGameHighScoresConfig struct { + UserID int + ChatID int + ChannelUsername string + MessageID int + InlineMessageID string +} + +func (config GetGameHighScoresConfig) values() (url.Values, error) { + v := url.Values{} + + v.Add("user_id", strconv.Itoa(config.UserID)) + if config.InlineMessageID == "" { + if config.ChannelUsername == "" { + v.Add("chat_id", strconv.Itoa(config.ChatID)) + } else { + v.Add("chat_id", config.ChannelUsername) + } + v.Add("message_id", strconv.Itoa(config.MessageID)) + } else { + v.Add("inline_message_id", config.InlineMessageID) + } + + return v, nil +} + +func (config GetGameHighScoresConfig) method() string { + return "getGameHighScores" +} + // ChatActionConfig contains information about a SendChatAction request. type ChatActionConfig struct { BaseChat @@ -669,6 +755,7 @@ type CallbackConfig struct { CallbackQueryID string `json:"callback_query_id"` Text string `json:"text"` ShowAlert bool `json:"show_alert"` + URL string `json:"url"` } // ChatMemberConfig contains information about a user in a chat for use diff --git a/types.go b/types.go index 1c7b1b0..327bbef 100644 --- a/types.go +++ b/types.go @@ -12,10 +12,17 @@ import ( // APIResponse is a response from the Telegram API with the result // stored raw. type APIResponse struct { - Ok bool `json:"ok"` - Result json.RawMessage `json:"result"` - ErrorCode int `json:"error_code"` - Description string `json:"description"` + Ok bool `json:"ok"` + Result json.RawMessage `json:"result"` + ErrorCode int `json:"error_code"` + Description string `json:"description"` + Parameters *ResponseParameters `json:"parameters"` +} + +// ResponseParameters are various errors that can be returned in APIResponse. +type ResponseParameters struct { + MigrateToChatID int `json:"migrate_to_chat_id"` // optional + RetryAfter int `json:"retry_after"` // optional } // Update is an update response, from GetUpdates. @@ -61,12 +68,13 @@ type GroupChat struct { // Chat contains information about the place a message was sent. type Chat struct { - ID int64 `json:"id"` - Type string `json:"type"` - Title string `json:"title"` // optional - UserName string `json:"username"` // optional - FirstName string `json:"first_name"` // optional - LastName string `json:"last_name"` // optional + ID int64 `json:"id"` + Type string `json:"type"` + Title string `json:"title"` // optional + UserName string `json:"username"` // optional + FirstName string `json:"first_name"` // optional + LastName string `json:"last_name"` // optional + AllMembersAreAdmins bool `json:"all_members_are_administrators"` // optional } // IsPrivate returns if the Chat is a private conversation. @@ -110,6 +118,7 @@ type Message struct { Entities *[]MessageEntity `json:"entities"` // optional Audio *Audio `json:"audio"` // optional Document *Document `json:"document"` // optional + Game *Game `json:"game"` // optional Photo *[]PhotoSize `json:"photo"` // optional Sticker *Sticker `json:"sticker"` // optional Video *Video `json:"video"` // optional @@ -324,11 +333,15 @@ type InlineKeyboardMarkup struct { // // Note that some values are references as even an empty string // will change behavior. +// +// CallbackGame, if set, MUST be first button in first row. type InlineKeyboardButton struct { - Text string `json:"text"` - URL *string `json:"url,omitempty"` // optional - CallbackData *string `json:"callback_data,omitempty"` // optional - SwitchInlineQuery *string `json:"switch_inline_query,omitempty"` // optional + Text string `json:"text"` + URL *string `json:"url,omitempty"` // optional + CallbackData *string `json:"callback_data,omitempty"` // optional + SwitchInlineQuery *string `json:"switch_inline_query,omitempty"` // optional + SwitchInlineQueryCurrentChat *string `json:"switch_inline_query_current_chat"` // optional + CallbackGame *CallbackGame `json:"callback_game"` // optional } // CallbackQuery is data sent when a keyboard button with callback data @@ -338,7 +351,9 @@ type CallbackQuery struct { From *User `json:"from"` Message *Message `json:"message"` // optional InlineMessageID string `json:"inline_message_id"` // optional - Data string `json:"data"` // optional + ChatInstance string `json:"chat_instance"` + Data string `json:"data"` // optional + GameShortName string `json:"game_short_name"` // optional } // ForceReply allows the Bot to have users directly reply to it without @@ -369,6 +384,49 @@ func (chat ChatMember) HasLeft() bool { return chat.Status == "left" } // WasKicked returns if the ChatMember was kicked from the chat. func (chat ChatMember) WasKicked() bool { return chat.Status == "kicked" } +// Game is a game within Telegram. +type Game struct { + Title string `json:"title"` + Description string `json:"description"` + Photo []PhotoSize `json:"photo"` + Text string `json:"text"` + TextEntities []MessageEntity `json:"text_entities"` + Animation Animation `json:"animation"` +} + +// Animation is a GIF animation demonstrating the game. +type Animation struct { + FileID string `json:"file_id"` + Thumb PhotoSize `json:"thumb"` + FileName string `json:"file_name"` + MimeType string `json:"mime_type"` + FileSize int `json:"file_size"` +} + +// GameHighScore is a user's score and position on the leaderboard. +type GameHighScore struct { + Position int `json:"position"` + User User `json:"user"` + Score int `json:"score"` +} + +// CallbackGame is for starting a game in an inline keyboard button. +type CallbackGame struct{} + +// WebhookInfo is information about a currently set webhook. +type WebhookInfo struct { + URL string `json:"url"` + 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 +} + +// IsSet returns true if a webhook is currently set. +func (info WebhookInfo) IsSet() bool { + return info.URL != "" +} + // InlineQuery is a Query from Telegram for an inline request. type InlineQuery struct { ID string `json:"id"` @@ -460,6 +518,7 @@ type InlineQueryResultAudio struct { ID string `json:"id"` // required URL string `json:"audio_url"` // required Title string `json:"title"` // required + Caption string `json:"caption"` Performer string `json:"performer"` Duration int `json:"audio_duration"` ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` @@ -472,6 +531,7 @@ type InlineQueryResultVoice struct { ID string `json:"id"` // required URL string `json:"voice_url"` // required Title string `json:"title"` // required + Caption string `json:"caption"` Duration int `json:"voice_duration"` ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` InputMessageContent interface{} `json:"input_message_content,omitempty"` @@ -507,6 +567,14 @@ type InlineQueryResultLocation struct { ThumbHeight int `json:"thumb_height"` } +// InlineQueryResultGame is an inline query response game. +type InlineQueryResultGame struct { + Type string `json:"type"` + ID string `json:"id"` + GameShortName string `json:"game_short_name"` + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup"` +} + // ChosenInlineResult is an inline query result chosen by a User type ChosenInlineResult struct { ResultID string `json:"result_id"`