Merge branch 'master' into fix-closing-update-channel-add-serverless-method
jump to
@@ -0,0 +1,33 @@
+name: Test + +on: + push: + branches: + - master + - develop + pull_request: + +jobs: + build: + name: Test + runs-on: ubuntu-latest + steps: + - name: Set up Go 1.x + uses: actions/setup-go@v2 + with: + go-version: ^1.15 + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + + - name: Build + run: go build -v . + + - name: Test + run: go test -coverprofile=coverage.out -covermode=atomic -v . + + - name: Upload coverage report + uses: codecov/codecov-action@v1 + with: + file: ./coverage.out
@@ -1,7 +1,7 @@
# Golang bindings for the Telegram Bot API -[![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) +[![Go Reference](https://pkg.go.dev/badge/github.com/go-telegram-bot-api/telegram-bot-api/v5.svg)](https://pkg.go.dev/github.com/go-telegram-bot-api/telegram-bot-api/v5) +[![Test](https://github.com/go-telegram-bot-api/telegram-bot-api/actions/workflows/test.yml/badge.svg)](https://github.com/go-telegram-bot-api/telegram-bot-api/actions/workflows/test.yml) 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@@ -18,7 +18,7 @@
## Example First, ensure the library is installed and up to date by running -`go get -u github.com/go-telegram-bot-api/telegram-bot-api`. +`go get -u github.com/go-telegram-bot-api/telegram-bot-api/v5`. This is a very simple bot that just displays any gotten updates, then replies it to that chat.@@ -29,7 +29,7 @@
import ( "log" - "github.com/go-telegram-bot-api/telegram-bot-api" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) func main() {@@ -62,7 +62,7 @@ }
} ``` -There are more examples on the [wiki](https://github.com/go-telegram-bot-api/telegram-bot-api/wiki) +There are more examples on the [site](https://go-telegram-bot-api.dev/) with detailed information on how to do many different kinds of things. It's a great place to get started on using keyboards, commands, or other kinds of reply markup.
@@ -0,0 +1,9 @@
+[book] +authors = ["Syfaro"] +language = "en" +multilingual = false +src = "docs" +title = "Go Telegram Bot API" + +[output.html] +git-repository-url = "https://github.com/go-telegram-bot-api/telegram-bot-api"
@@ -3,23 +3,20 @@ // the Telegram Bot API.
package tgbotapi import ( - "bytes" "encoding/json" "errors" "fmt" "io" "io/ioutil" + "mime/multipart" "net/http" "net/url" - "os" - "strconv" "strings" "time" - - "github.com/technoweenie/multipartstreamer" ) -type HttpClient interface { +// HTTPClient is the type needed for the bot to perform HTTP requests. +type HTTPClient interface { Do(req *http.Request) (*http.Response, error) }@@ -30,7 +27,7 @@ Debug bool `json:"debug"`
Buffer int `json:"buffer"` Self User `json:"-"` - Client HttpClient `json:"-"` + Client HTTPClient `json:"-"` shutdownChannel chan interface{} apiEndpoint string@@ -55,7 +52,7 @@ // NewBotAPIWithClient creates a new BotAPI instance
// and allows you to pass a http.Client. // // It requires a token, provided by @BotFather on Telegram and API endpoint. -func NewBotAPIWithClient(token, apiEndpoint string, client HttpClient) (*BotAPI, error) { +func NewBotAPIWithClient(token, apiEndpoint string, client HTTPClient) (*BotAPI, error) { bot := &BotAPI{ Token: token, Client: client,@@ -75,46 +72,72 @@
return bot, nil } -// SetAPIEndpoint add telegram apiEndpont to Bot +// SetAPIEndpoint changes the Telegram Bot API endpoint used by the instance. func (bot *BotAPI) SetAPIEndpoint(apiEndpoint string) { bot.apiEndpoint = apiEndpoint } +func buildParams(in Params) url.Values { + if in == nil { + return url.Values{} + } + + out := url.Values{} + + for key, value := range in { + out.Set(key, value) + } + + return out +} + // MakeRequest makes a request to a specific endpoint with our token. -func (bot *BotAPI) MakeRequest(endpoint string, params url.Values) (APIResponse, error) { +func (bot *BotAPI) MakeRequest(endpoint string, params Params) (*APIResponse, error) { + if bot.Debug { + log.Printf("Endpoint: %s, params: %v\n", endpoint, params) + } + method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint) - req, err := http.NewRequest("POST", method, strings.NewReader(params.Encode())) + values := buildParams(params) + + req, err := http.NewRequest("POST", method, strings.NewReader(values.Encode())) if err != nil { - return APIResponse{}, err + 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 + return nil, err } defer resp.Body.Close() var apiResp APIResponse bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp) if err != nil { - return apiResp, err + return &apiResp, err } if bot.Debug { - log.Printf("%s resp: %s", endpoint, bytes) + log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes)) } if !apiResp.Ok { - parameters := ResponseParameters{} + var 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 + return &apiResp, nil } // decodeAPIResponse decode response and return slice of bytes if debug enabled.@@ -141,118 +164,103 @@
return data, nil } -// makeMessageRequest makes a request to a method that returns a Message. -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, -// 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() +// UploadFiles makes a request to the API with files. +func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFile) (*APIResponse, error) { + r, w := io.Pipe() + m := multipart.NewWriter(w) - switch f := file.(type) { - case string: - ms.WriteFields(params) + // This code modified from the very helpful @HirbodBehnam + // https://github.com/go-telegram-bot-api/telegram-bot-api/issues/354#issuecomment-663856473 + go func() { + defer w.Close() + defer m.Close() - fileHandle, err := os.Open(f) - if err != nil { - return APIResponse{}, err + for field, value := range params { + if err := m.WriteField(field, value); err != nil { + w.CloseWithError(err) + return + } } - defer fileHandle.Close() - fi, err := os.Stat(f) - if err != nil { - return APIResponse{}, err - } - - ms.WriteReader(fieldname, fileHandle.Name(), fi.Size(), fileHandle) - case FileBytes: - ms.WriteFields(params) + for _, file := range files { + if file.Data.NeedsUpload() { + name, reader, err := file.Data.UploadData() + if err != nil { + w.CloseWithError(err) + return + } - buf := bytes.NewBuffer(f.Bytes) - ms.WriteReader(fieldname, f.Name, int64(len(f.Bytes)), buf) - case FileReader: - ms.WriteFields(params) + part, err := m.CreateFormFile(file.Name, name) + if err != nil { + w.CloseWithError(err) + return + } - if f.Size != -1 { - ms.WriteReader(fieldname, f.Name, f.Size, f.Reader) + if _, err := io.Copy(part, reader); err != nil { + w.CloseWithError(err) + return + } - break - } + if closer, ok := reader.(io.ReadCloser); ok { + if err = closer.Close(); err != nil { + w.CloseWithError(err) + return + } + } + } else { + value := file.Data.SendData() - data, err := ioutil.ReadAll(f.Reader) - if err != nil { - return APIResponse{}, err + if err := m.WriteField(file.Name, value); err != nil { + w.CloseWithError(err) + return + } + } } + }() - 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) + if bot.Debug { + log.Printf("Endpoint: %s, params: %v, with %d files\n", endpoint, params, len(files)) } method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint) - req, err := http.NewRequest("POST", method, nil) + req, err := http.NewRequest("POST", method, r) if err != nil { - return APIResponse{}, err + return nil, err } - ms.SetupRequest(req) + req.Header.Set("Content-Type", m.FormDataContentType()) - res, err := bot.Client.Do(req) + resp, err := bot.Client.Do(req) if err != nil { - return APIResponse{}, err + return nil, err } - defer res.Body.Close() + defer resp.Body.Close() - bytes, err := ioutil.ReadAll(res.Body) + var apiResp APIResponse + bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp) if err != nil { - return APIResponse{}, err + return &apiResp, err } if bot.Debug { - log.Println(string(bytes)) - } - - var apiResp APIResponse - - err = json.Unmarshal(bytes, &apiResp) - if err != nil { - return APIResponse{}, err + log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes)) } if !apiResp.Ok { - parameters := ResponseParameters{} + var parameters ResponseParameters + if apiResp.Parameters != nil { parameters = *apiResp.Parameters } - return apiResp, Error{Code: apiResp.ErrorCode, Message: apiResp.Description, ResponseParameters: parameters} + + return &apiResp, &Error{ + Message: apiResp.Description, + ResponseParameters: parameters, + } } - return apiResp, nil + return &apiResp, nil } // GetFileDirectURL returns direct URL to file@@ -280,11 +288,9 @@ return User{}, err
} var user User - json.Unmarshal(resp.Result, &user) + err = json.Unmarshal(resp.Result, &user) - bot.debugLog("getMe", nil, user) - - return user, nil + return user, err } // IsMessageToMe returns true if message directed to this bot.@@ -294,90 +300,67 @@ func (bot *BotAPI) IsMessageToMe(message Message) bool {
return strings.Contains(message.Text, "@"+bot.Self.UserName) } -// Send will send a Chattable item to Telegram. -// -// It requires the Chattable to send. -func (bot *BotAPI) Send(c Chattable) (Message, error) { - switch c.(type) { - case Fileable: - return bot.sendFile(c.(Fileable)) - default: - return bot.sendChattable(c) +func hasFilesNeedingUpload(files []RequestFile) bool { + for _, file := range files { + if file.Data.NeedsUpload() { + return true + } } -} -// debugLog checks if the bot is currently running in debug mode, and if -// so will display information about the request and response in the -// debug log. -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) - } + return false } -// sendExisting will send a Message with an existing file to Telegram. -func (bot *BotAPI) sendExisting(method string, config Fileable) (Message, error) { - v, err := config.values() - +// Request sends a Chattable to Telegram, and returns the APIResponse. +func (bot *BotAPI) Request(c Chattable) (*APIResponse, error) { + params, err := c.params() if err != nil { - return Message{}, err + return nil, err } - message, err := bot.makeMessageRequest(method, v) - if err != nil { - return Message{}, err - } + if t, ok := c.(Fileable); ok { + files := t.files() - return message, nil -} + // If we have files that need to be uploaded, we should delegate the + // request to UploadFile. + if hasFilesNeedingUpload(files) { + return bot.UploadFiles(t.method(), params, files) + } -// uploadAndSend will send a Message with a new file to Telegram. -func (bot *BotAPI) uploadAndSend(method string, config Fileable) (Message, error) { - params, err := config.params() - if err != nil { - return Message{}, err + // However, if there are no files to be uploaded, there's likely things + // that need to be turned into params instead. + for _, file := range files { + params[file.Name] = file.Data.SendData() + } } - file := config.getFile() + return bot.MakeRequest(c.method(), params) +} - resp, err := bot.UploadFile(method, params, config.name(), file) +// Send will send a Chattable item to Telegram and provides the +// returned Message. +func (bot *BotAPI) Send(c Chattable) (Message, error) { + resp, err := bot.Request(c) if err != nil { return Message{}, err } var message Message - json.Unmarshal(resp.Result, &message) - - bot.debugLog(method, nil, message) - - return message, nil -} - -// sendFile determines if the file is using an existing file or uploading -// a new file, then sends it as needed. -func (bot *BotAPI) sendFile(config Fileable) (Message, error) { - if config.useExistingFile() { - return bot.sendExisting(config.method(), config) - } + err = json.Unmarshal(resp.Result, &message) - return bot.uploadAndSend(config.method(), config) + return message, err } -// sendChattable sends a Chattable. -func (bot *BotAPI) sendChattable(config Chattable) (Message, error) { - v, err := config.values() +// SendMediaGroup sends a media group and returns the resulting messages. +func (bot *BotAPI) SendMediaGroup(config MediaGroupConfig) ([]Message, error) { + resp, err := bot.Request(config) if err != nil { - return Message{}, err + return nil, err } - message, err := bot.makeMessageRequest(config.method(), v) - - if err != nil { - return Message{}, err - } + var messages []Message + err = json.Unmarshal(resp.Result, &messages) - return message, nil + return messages, err } // GetUserProfilePhotos gets a user's profile photos.@@ -385,121 +368,55 @@ //
// It 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) + resp, err := bot.Request(config) if err != nil { return UserProfilePhotos{}, err } var profilePhotos UserProfilePhotos - json.Unmarshal(resp.Result, &profilePhotos) + err = json.Unmarshal(resp.Result, &profilePhotos) - bot.debugLog("GetUserProfilePhoto", v, profilePhotos) - - return profilePhotos, nil + return profilePhotos, err } // GetFile returns a File which can download a file from Telegram. // // 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) + resp, err := bot.Request(config) if err != nil { return File{}, err } var file File - json.Unmarshal(resp.Result, &file) + err = json.Unmarshal(resp.Result, &file) - bot.debugLog("GetFile", v, file) - - return file, nil + return file, err } // GetUpdates fetches updates. // If a WebHook is set, this will not return any data! // -// Offset, Limit, and Timeout are optional. +// Offset, Limit, Timeout, and AllowedUpdates are optional. // To avoid stale items, set Offset to one higher than the previous item. // Set Timeout to a large number to reduce requests so you can get updates // instantly instead of having to wait between requests. 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) + resp, err := bot.Request(config) if err != nil { return []Update{}, err } var updates []Update - json.Unmarshal(resp.Result, &updates) - - bot.debugLog("getUpdates", v, updates) + err = json.Unmarshal(resp.Result, &updates) - return updates, nil -} - -// RemoveWebhook unsets the webhook. -func (bot *BotAPI) RemoveWebhook() (APIResponse, error) { - return bot.MakeRequest("deleteWebhook", url.Values{}) -} - -// SetWebhook sets a webhook. -// -// If this is set, GetUpdates will not get any data! -// -// If you do not have a legitimate TLS certificate, you need to include -// your self signed certificate with the config. -func (bot *BotAPI) SetWebhook(config WebhookConfig) (APIResponse, error) { - - if config.Certificate == nil { - v := url.Values{} - v.Add("url", config.URL.String()) - if config.MaxConnections != 0 { - v.Add("max_connections", strconv.Itoa(config.MaxConnections)) - } - - return bot.MakeRequest("setWebhook", v) - } - - params := make(map[string]string) - params["url"] = config.URL.String() - if config.MaxConnections != 0 { - params["max_connections"] = strconv.Itoa(config.MaxConnections) - } - - resp, err := bot.UploadFile("setWebhook", params, "certificate", config.Certificate) - if err != nil { - return APIResponse{}, err - } - - return resp, nil + return updates, err } // 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{}) + resp, err := bot.MakeRequest("getWebhookInfo", nil) if err != nil { return WebhookInfo{}, err }@@ -511,7 +428,7 @@ return info, err
} // GetUpdatesChan starts and returns a channel for getting updates. -func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (UpdatesChannel, error) { +func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) UpdatesChannel { ch := make(chan Update, bot.Buffer) go func() {@@ -541,7 +458,7 @@ }
} }() - return ch, nil + return ch } // StopReceivingUpdates stops the go routine which receives updates@@ -610,96 +527,35 @@
return &update, nil } -// AnswerInlineQuery sends a response to an inline query. +// WriteToHTTPResponse writes the request to the HTTP ResponseWriter. +// +// It doesn't support uploading files. // -// Note that you must respond to an inline query within 30 seconds. -func (bot *BotAPI) AnswerInlineQuery(config InlineConfig) (APIResponse, error) { - v := url.Values{} - - v.Add("inline_query_id", config.InlineQueryID) - v.Add("cache_time", strconv.Itoa(config.CacheTime)) - v.Add("is_personal", strconv.FormatBool(config.IsPersonal)) - v.Add("next_offset", config.NextOffset) - data, err := json.Marshal(config.Results) +// See https://core.telegram.org/bots/api#making-requests-when-getting-updates +// for details. +func WriteToHTTPResponse(w http.ResponseWriter, c Chattable) error { + params, err := c.params() if err != nil { - return APIResponse{}, err - } - v.Add("results", string(data)) - v.Add("switch_pm_text", config.SwitchPMText) - v.Add("switch_pm_parameter", config.SwitchPMParameter) - - bot.debugLog("answerInlineQuery", v, nil) - - return bot.MakeRequest("answerInlineQuery", v) -} - -// AnswerCallbackQuery sends a response to an inline query callback. -func (bot *BotAPI) AnswerCallbackQuery(config CallbackConfig) (APIResponse, error) { - v := url.Values{} - - v.Add("callback_query_id", config.CallbackQueryID) - if config.Text != "" { - v.Add("text", config.Text) - } - v.Add("show_alert", strconv.FormatBool(config.ShowAlert)) - if config.URL != "" { - v.Add("url", config.URL) - } - v.Add("cache_time", strconv.Itoa(config.CacheTime)) - - bot.debugLog("answerCallbackQuery", v, nil) - - return bot.MakeRequest("answerCallbackQuery", v) -} - -// KickChatMember kicks a user from a chat. Note that this only will work -// in supergroups, and requires the bot to be an admin. Also note they -// will be unable to rejoin until they are unbanned. -func (bot *BotAPI) KickChatMember(config KickChatMemberConfig) (APIResponse, error) { - v := url.Values{} - - if config.SuperGroupUsername == "" { - v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) - } else { - v.Add("chat_id", config.SuperGroupUsername) - } - v.Add("user_id", strconv.Itoa(config.UserID)) - - if config.UntilDate != 0 { - v.Add("until_date", strconv.FormatInt(config.UntilDate, 10)) + return err } - bot.debugLog("kickChatMember", v, nil) - - return bot.MakeRequest("kickChatMember", v) -} - -// LeaveChat makes the bot leave the chat. -func (bot *BotAPI) LeaveChat(config ChatConfig) (APIResponse, error) { - v := url.Values{} - - if config.SuperGroupUsername == "" { - v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) - } else { - v.Add("chat_id", config.SuperGroupUsername) + if t, ok := c.(Fileable); ok { + if hasFilesNeedingUpload(t.files()) { + return errors.New("unable to use http response to upload files") + } } - bot.debugLog("leaveChat", v, nil) + values := buildParams(params) + values.Set("method", c.method()) - return bot.MakeRequest("leaveChat", v) + w.Header().Set("Content-Type", "application/x-www-form-urlencoded") + _, err = w.Write([]byte(values.Encode())) + return err } // GetChat gets information about a chat. -func (bot *BotAPI) GetChat(config ChatConfig) (Chat, error) { - v := url.Values{} - - if config.SuperGroupUsername == "" { - v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) - } else { - v.Add("chat_id", config.SuperGroupUsername) - } - - resp, err := bot.MakeRequest("getChat", v) +func (bot *BotAPI) GetChat(config ChatInfoConfig) (Chat, error) { + resp, err := bot.Request(config) if err != nil { return Chat{}, err }@@ -707,8 +563,6 @@
var chat Chat err = json.Unmarshal(resp.Result, &chat) - bot.debugLog("getChat", v, chat) - return chat, err }@@ -716,39 +570,21 @@ // GetChatAdministrators gets a list of administrators in the chat.
// // If none have been appointed, only the creator will be returned. // Bots are not shown, even if they are an administrator. -func (bot *BotAPI) GetChatAdministrators(config ChatConfig) ([]ChatMember, error) { - v := url.Values{} - - if config.SuperGroupUsername == "" { - v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) - } else { - v.Add("chat_id", config.SuperGroupUsername) - } - - resp, err := bot.MakeRequest("getChatAdministrators", v) +func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]ChatMember, error) { + resp, err := bot.Request(config) if err != nil { return []ChatMember{}, err } var members []ChatMember err = json.Unmarshal(resp.Result, &members) - - bot.debugLog("getChatAdministrators", v, members) return members, err } // GetChatMembersCount gets the number of users in a chat. -func (bot *BotAPI) GetChatMembersCount(config ChatConfig) (int, error) { - v := url.Values{} - - if config.SuperGroupUsername == "" { - v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) - } else { - v.Add("chat_id", config.SuperGroupUsername) - } - - resp, err := bot.MakeRequest("getChatMembersCount", v) +func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) { + resp, err := bot.Request(config) if err != nil { return -1, err }@@ -756,139 +592,25 @@
var count int err = json.Unmarshal(resp.Result, &count) - bot.debugLog("getChatMembersCount", v, count) - return count, err } // GetChatMember gets a specific chat member. -func (bot *BotAPI) GetChatMember(config ChatConfigWithUser) (ChatMember, error) { - v := url.Values{} - - if config.SuperGroupUsername == "" { - v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) - } else { - v.Add("chat_id", config.SuperGroupUsername) - } - v.Add("user_id", strconv.Itoa(config.UserID)) - - resp, err := bot.MakeRequest("getChatMember", v) +func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) { + resp, err := bot.Request(config) if err != nil { return ChatMember{}, err } var member ChatMember err = json.Unmarshal(resp.Result, &member) - - bot.debugLog("getChatMember", v, member) return member, err } -// UnbanChatMember unbans a user from a chat. Note that this only will work -// in supergroups and channels, and requires the bot to be an admin. -func (bot *BotAPI) UnbanChatMember(config ChatMemberConfig) (APIResponse, error) { - v := url.Values{} - - if config.SuperGroupUsername != "" { - v.Add("chat_id", config.SuperGroupUsername) - } else if config.ChannelUsername != "" { - v.Add("chat_id", config.ChannelUsername) - } else { - v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) - } - v.Add("user_id", strconv.Itoa(config.UserID)) - - bot.debugLog("unbanChatMember", v, nil) - - 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. -func (bot *BotAPI) RestrictChatMember(config RestrictChatMemberConfig) (APIResponse, error) { - v := url.Values{} - - if config.SuperGroupUsername != "" { - v.Add("chat_id", config.SuperGroupUsername) - } else if config.ChannelUsername != "" { - v.Add("chat_id", config.ChannelUsername) - } else { - v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) - } - v.Add("user_id", strconv.Itoa(config.UserID)) - - if config.CanSendMessages != nil { - v.Add("can_send_messages", strconv.FormatBool(*config.CanSendMessages)) - } - if config.CanSendMediaMessages != nil { - v.Add("can_send_media_messages", strconv.FormatBool(*config.CanSendMediaMessages)) - } - if config.CanSendOtherMessages != nil { - v.Add("can_send_other_messages", strconv.FormatBool(*config.CanSendOtherMessages)) - } - if config.CanAddWebPagePreviews != nil { - v.Add("can_add_web_page_previews", strconv.FormatBool(*config.CanAddWebPagePreviews)) - } - if config.UntilDate != 0 { - v.Add("until_date", strconv.FormatInt(config.UntilDate, 10)) - } - - bot.debugLog("restrictChatMember", v, nil) - - return bot.MakeRequest("restrictChatMember", v) -} - -// PromoteChatMember add admin rights to user -func (bot *BotAPI) PromoteChatMember(config PromoteChatMemberConfig) (APIResponse, error) { - v := url.Values{} - - if config.SuperGroupUsername != "" { - v.Add("chat_id", config.SuperGroupUsername) - } else if config.ChannelUsername != "" { - v.Add("chat_id", config.ChannelUsername) - } else { - v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) - } - v.Add("user_id", strconv.Itoa(config.UserID)) - - if config.CanChangeInfo != nil { - v.Add("can_change_info", strconv.FormatBool(*config.CanChangeInfo)) - } - if config.CanPostMessages != nil { - v.Add("can_post_messages", strconv.FormatBool(*config.CanPostMessages)) - } - if config.CanEditMessages != nil { - v.Add("can_edit_messages", strconv.FormatBool(*config.CanEditMessages)) - } - if config.CanDeleteMessages != nil { - v.Add("can_delete_messages", strconv.FormatBool(*config.CanDeleteMessages)) - } - if config.CanInviteUsers != nil { - v.Add("can_invite_users", strconv.FormatBool(*config.CanInviteUsers)) - } - if config.CanRestrictMembers != nil { - v.Add("can_restrict_members", strconv.FormatBool(*config.CanRestrictMembers)) - } - if config.CanPinMessages != nil { - v.Add("can_pin_messages", strconv.FormatBool(*config.CanPinMessages)) - } - if config.CanPromoteMembers != nil { - v.Add("can_promote_members", strconv.FormatBool(*config.CanPromoteMembers)) - } - - bot.debugLog("promoteChatMember", v, nil) - - return bot.MakeRequest("promoteChatMember", 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) + resp, err := bot.Request(config) if err != nil { return []GameHighScore{}, err }@@ -899,65 +621,9 @@
return highScores, err } -// AnswerShippingQuery allows you to reply to Update with shipping_query parameter. -func (bot *BotAPI) AnswerShippingQuery(config ShippingConfig) (APIResponse, error) { - v := url.Values{} - - v.Add("shipping_query_id", config.ShippingQueryID) - v.Add("ok", strconv.FormatBool(config.OK)) - if config.OK == true { - data, err := json.Marshal(config.ShippingOptions) - if err != nil { - return APIResponse{}, err - } - v.Add("shipping_options", string(data)) - } else { - v.Add("error_message", config.ErrorMessage) - } - - bot.debugLog("answerShippingQuery", v, nil) - - return bot.MakeRequest("answerShippingQuery", v) -} - -// AnswerPreCheckoutQuery allows you to reply to Update with pre_checkout_query. -func (bot *BotAPI) AnswerPreCheckoutQuery(config PreCheckoutConfig) (APIResponse, error) { - v := url.Values{} - - v.Add("pre_checkout_query_id", config.PreCheckoutQueryID) - v.Add("ok", strconv.FormatBool(config.OK)) - if config.OK != true { - v.Add("error_message", config.ErrorMessage) - } - - bot.debugLog("answerPreCheckoutQuery", v, nil) - - return bot.MakeRequest("answerPreCheckoutQuery", v) -} - -// DeleteMessage deletes a message in a chat -func (bot *BotAPI) DeleteMessage(config DeleteMessageConfig) (APIResponse, error) { - v, err := config.values() - if err != nil { - return APIResponse{}, err - } - - bot.debugLog(config.method(), v, nil) - - return bot.MakeRequest(config.method(), v) -} - // GetInviteLink get InviteLink for a chat -func (bot *BotAPI) GetInviteLink(config ChatConfig) (string, error) { - v := url.Values{} - - if config.SuperGroupUsername == "" { - v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) - } else { - v.Add("chat_id", config.SuperGroupUsername) - } - - resp, err := bot.MakeRequest("exportChatInviteLink", v) +func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) { + resp, err := bot.Request(config) if err != nil { return "", err }@@ -968,124 +634,68 @@
return inviteLink, err } -// PinChatMessage pin message in supergroup -func (bot *BotAPI) PinChatMessage(config PinChatMessageConfig) (APIResponse, error) { - v, err := config.values() +// GetStickerSet returns a StickerSet. +func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) { + resp, err := bot.Request(config) if err != nil { - return APIResponse{}, err + return StickerSet{}, err } - bot.debugLog(config.method(), v, nil) + var stickers StickerSet + err = json.Unmarshal(resp.Result, &stickers) - return bot.MakeRequest(config.method(), v) + return stickers, err } -// UnpinChatMessage unpin message in supergroup -func (bot *BotAPI) UnpinChatMessage(config UnpinChatMessageConfig) (APIResponse, error) { - v, err := config.values() +// StopPoll stops a poll and returns the result. +func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) { + resp, err := bot.Request(config) if err != nil { - return APIResponse{}, err + return Poll{}, err } - bot.debugLog(config.method(), v, nil) + var poll Poll + err = json.Unmarshal(resp.Result, &poll) - return bot.MakeRequest(config.method(), v) + return poll, err } -// SetChatTitle change title of chat. -func (bot *BotAPI) SetChatTitle(config SetChatTitleConfig) (APIResponse, error) { - v, err := config.values() - if err != nil { - return APIResponse{}, err - } - - bot.debugLog(config.method(), v, nil) - - return bot.MakeRequest(config.method(), v) +// GetMyCommands gets the currently registered commands. +func (bot *BotAPI) GetMyCommands() ([]BotCommand, error) { + return bot.GetMyCommandsWithConfig(GetMyCommandsConfig{}) } -// SetChatDescription change description of chat. -func (bot *BotAPI) SetChatDescription(config SetChatDescriptionConfig) (APIResponse, error) { - v, err := config.values() +// GetMyCommandsWithConfig gets the currently registered commands with a config. +func (bot *BotAPI) GetMyCommandsWithConfig(config GetMyCommandsConfig) ([]BotCommand, error) { + resp, err := bot.Request(config) if err != nil { - return APIResponse{}, err + return nil, err } - bot.debugLog(config.method(), v, nil) + var commands []BotCommand + err = json.Unmarshal(resp.Result, &commands) - return bot.MakeRequest(config.method(), v) + return commands, err } -// SetChatPhoto change photo of chat. -func (bot *BotAPI) SetChatPhoto(config SetChatPhotoConfig) (APIResponse, error) { +// CopyMessage copy messages of any kind. The method is analogous to the method +// forwardMessage, but the copied message doesn't have a link to the original +// message. Returns the MessageID of the sent message on success. +func (bot *BotAPI) CopyMessage(config CopyMessageConfig) (MessageID, error) { params, err := config.params() if err != nil { - return APIResponse{}, err + return MessageID{}, err } - file := config.getFile() - - return bot.UploadFile(config.method(), params, config.name(), file) -} - -// DeleteChatPhoto delete photo of chat. -func (bot *BotAPI) DeleteChatPhoto(config DeleteChatPhotoConfig) (APIResponse, error) { - v, err := config.values() + resp, err := bot.MakeRequest(config.method(), params) if err != nil { - return APIResponse{}, err + return MessageID{}, err } - bot.debugLog(config.method(), v, nil) + var messageID MessageID + err = json.Unmarshal(resp.Result, &messageID) - 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 -} - -// GetMyCommands gets the current list of the bot's commands. -func (bot *BotAPI) GetMyCommands() ([]BotCommand, error) { - res, err := bot.MakeRequest("getMyCommands", nil) - if err != nil { - return nil, err - } - var commands []BotCommand - err = json.Unmarshal(res.Result, &commands) - if err != nil { - return nil, err - } - return commands, nil -} - -// SetMyCommands changes the list of the bot's commands. -func (bot *BotAPI) SetMyCommands(commands []BotCommand) error { - v := url.Values{} - data, err := json.Marshal(commands) - if err != nil { - return err - } - v.Add("commands", string(data)) - _, err = bot.MakeRequest("setMyCommands", v) - if err != nil { - return err - } - return nil + return messageID, err } // EscapeText takes an input text and escape Telegram markup symbols.
@@ -1,22 +1,20 @@
-package tgbotapi_test +package tgbotapi import ( "io/ioutil" - "log" "net/http" "os" "testing" "time" - - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" ) const ( TestToken = "153667468:AAHlSHlMqSt1f_uFmVRJbm5gntu2HI4WW8I" ChatID = 76918703 + Channel = "@tgbotapitest" SupergroupChatID = -1001120141283 ReplyToMessageID = 35 - ExistingPhotoFileID = "AgADAgADw6cxG4zHKAkr42N7RwEN3IFShCoABHQwXEtVks4EH2wBAAEC" + ExistingPhotoFileID = "AgACAgIAAxkDAAEBFUZhIALQ9pZN4BUe8ZSzUU_2foSo1AACnrMxG0BucEhezsBWOgcikQEAAwIAA20AAyAE" ExistingDocumentFileID = "BQADAgADOQADjMcoCcioX1GrDvp3Ag" ExistingAudioFileID = "BQADAgADRgADjMcoCdXg3lSIN49lAg" ExistingVoiceFileID = "AwADAgADWQADjMcoCeul6r_q52IyAg"@@ -25,88 +23,116 @@ ExistingVideoNoteFileID = "DQADAgADdQAD70cQSUK41dLsRMqfAg"
ExistingStickerFileID = "BQADAgADcwADjMcoCbdl-6eB--YPAg" ) -func getBot(t *testing.T) (*tgbotapi.BotAPI, error) { - bot, err := tgbotapi.NewBotAPI(TestToken) +type testLogger struct { + t *testing.T +} + +func (t testLogger) Println(v ...interface{}) { + t.t.Log(v...) +} + +func (t testLogger) Printf(format string, v ...interface{}) { + t.t.Logf(format, v...) +} + +func getBot(t *testing.T) (*BotAPI, error) { + bot, err := NewBotAPI(TestToken) bot.Debug = true + logger := testLogger{t} + SetLogger(logger) + if err != nil { t.Error(err) - t.Fail() } return bot, err } func TestNewBotAPI_notoken(t *testing.T) { - _, err := tgbotapi.NewBotAPI("") + _, err := NewBotAPI("") if err == nil { t.Error(err) - t.Fail() } } func TestGetUpdates(t *testing.T) { bot, _ := getBot(t) - u := tgbotapi.NewUpdate(0) + u := NewUpdate(0) _, err := bot.GetUpdates(u) if err != nil { t.Error(err) - t.Fail() } } 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" + msg := NewMessage(ChatID, "A test message from the test library in telegram-bot-api") + msg.ParseMode = ModeMarkdown _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } func TestSendWithMessageReply(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewMessage(ChatID, "A test message from the test library in telegram-bot-api") + msg := NewMessage(ChatID, "A test message from the test library in telegram-bot-api") msg.ReplyToMessageID = ReplyToMessageID _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } func TestSendWithMessageForward(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewForward(ChatID, ChatID, ReplyToMessageID) + msg := NewForward(ChatID, ChatID, ReplyToMessageID) _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() + } +} + +func TestCopyMessage(t *testing.T) { + bot, _ := getBot(t) + + msg := NewMessage(ChatID, "A test message from the test library in telegram-bot-api") + message, err := bot.Send(msg) + if err != nil { + t.Error(err) + } + + copyMessageConfig := NewCopyMessage(SupergroupChatID, message.Chat.ID, message.MessageID) + messageID, err := bot.CopyMessage(copyMessageConfig) + if err != nil { + t.Error(err) + } + + if messageID.MessageID == message.MessageID { + t.Error("copied message ID was the same as original message") } } func TestSendWithNewPhoto(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewPhotoUpload(ChatID, "tests/image.jpg") + msg := NewPhoto(ChatID, FilePath("tests/image.jpg")) msg.Caption = "Test" _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } }@@ -114,15 +140,14 @@ func TestSendWithNewPhotoWithFileBytes(t *testing.T) {
bot, _ := getBot(t) data, _ := ioutil.ReadFile("tests/image.jpg") - b := tgbotapi.FileBytes{Name: "image.jpg", Bytes: data} + b := FileBytes{Name: "image.jpg", Bytes: data} - msg := tgbotapi.NewPhotoUpload(ChatID, b) + msg := NewPhoto(ChatID, b) msg.Caption = "Test" _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } }@@ -130,24 +155,67 @@ func TestSendWithNewPhotoWithFileReader(t *testing.T) {
bot, _ := getBot(t) f, _ := os.Open("tests/image.jpg") - reader := tgbotapi.FileReader{Name: "image.jpg", Reader: f, Size: -1} + reader := FileReader{Name: "image.jpg", Reader: f} - msg := tgbotapi.NewPhotoUpload(ChatID, reader) + msg := NewPhoto(ChatID, reader) msg.Caption = "Test" _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } func TestSendWithNewPhotoReply(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewPhotoUpload(ChatID, "tests/image.jpg") + msg := NewPhoto(ChatID, FilePath("tests/image.jpg")) msg.ReplyToMessageID = ReplyToMessageID + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + } +} + +func TestSendNewPhotoToChannel(t *testing.T) { + bot, _ := getBot(t) + + msg := NewPhotoToChannel(Channel, FilePath("tests/image.jpg")) + msg.Caption = "Test" + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendNewPhotoToChannelFileBytes(t *testing.T) { + bot, _ := getBot(t) + + data, _ := ioutil.ReadFile("tests/image.jpg") + b := FileBytes{Name: "image.jpg", Bytes: data} + + msg := NewPhotoToChannel(Channel, b) + msg.Caption = "Test" + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendNewPhotoToChannelFileReader(t *testing.T) { + bot, _ := getBot(t) + + f, _ := os.Open("tests/image.jpg") + reader := FileReader{Name: "image.jpg", Reader: f} + + msg := NewPhotoToChannel(Channel, reader) + msg.Caption = "Test" _, err := bot.Send(msg) if err != nil {@@ -159,61 +227,67 @@
func TestSendWithExistingPhoto(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewPhotoShare(ChatID, ExistingPhotoFileID) + msg := NewPhoto(ChatID, FileID(ExistingPhotoFileID)) msg.Caption = "Test" _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } func TestSendWithNewDocument(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewDocumentUpload(ChatID, "tests/image.jpg") + msg := NewDocument(ChatID, FilePath("tests/image.jpg")) _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() + } +} + +func TestSendWithNewDocumentAndThumb(t *testing.T) { + bot, _ := getBot(t) + + msg := NewDocument(ChatID, FilePath("tests/voice.ogg")) + msg.Thumb = FilePath("tests/image.jpg") + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) } } func TestSendWithExistingDocument(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewDocumentShare(ChatID, ExistingDocumentFileID) + msg := NewDocument(ChatID, FileID(ExistingDocumentFileID)) _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } func TestSendWithNewAudio(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewAudioUpload(ChatID, "tests/audio.mp3") + msg := NewAudio(ChatID, FilePath("tests/audio.mp3")) msg.Title = "TEST" msg.Duration = 10 msg.Performer = "TEST" - msg.MimeType = "audio/mpeg" - msg.FileSize = 688 _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } func TestSendWithExistingAudio(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewAudioShare(ChatID, ExistingAudioFileID) + msg := NewAudio(ChatID, FileID(ExistingAudioFileID)) msg.Title = "TEST" msg.Duration = 10 msg.Performer = "TEST"@@ -222,73 +296,67 @@ _, err := bot.Send(msg)
if err != nil { t.Error(err) - t.Fail() } } func TestSendWithNewVoice(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewVoiceUpload(ChatID, "tests/voice.ogg") + msg := NewVoice(ChatID, FilePath("tests/voice.ogg")) msg.Duration = 10 _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } func TestSendWithExistingVoice(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewVoiceShare(ChatID, ExistingVoiceFileID) + msg := NewVoice(ChatID, FileID(ExistingVoiceFileID)) msg.Duration = 10 _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } func TestSendWithContact(t *testing.T) { bot, _ := getBot(t) - contact := tgbotapi.NewContact(ChatID, "5551234567", "Test") + contact := NewContact(ChatID, "5551234567", "Test") if _, err := bot.Send(contact); err != nil { t.Error(err) - t.Fail() } } func TestSendWithLocation(t *testing.T) { bot, _ := getBot(t) - _, err := bot.Send(tgbotapi.NewLocation(ChatID, 40, 40)) + _, err := bot.Send(NewLocation(ChatID, 40, 40)) if err != nil { t.Error(err) - t.Fail() } } func TestSendWithVenue(t *testing.T) { bot, _ := getBot(t) - venue := tgbotapi.NewVenue(ChatID, "A Test Location", "123 Test Street", 40, 40) + venue := NewVenue(ChatID, "A Test Location", "123 Test Street", 40, 40) if _, err := bot.Send(venue); err != nil { t.Error(err) - t.Fail() } } func TestSendWithNewVideo(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewVideoUpload(ChatID, "tests/video.mp4") + msg := NewVideo(ChatID, FilePath("tests/video.mp4")) msg.Duration = 10 msg.Caption = "TEST"@@ -296,14 +364,13 @@ _, err := bot.Send(msg)
if err != nil { t.Error(err) - t.Fail() } } func TestSendWithExistingVideo(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewVideoShare(ChatID, ExistingVideoFileID) + msg := NewVideo(ChatID, FileID(ExistingVideoFileID)) msg.Duration = 10 msg.Caption = "TEST"@@ -311,69 +378,64 @@ _, err := bot.Send(msg)
if err != nil { t.Error(err) - t.Fail() } } func TestSendWithNewVideoNote(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewVideoNoteUpload(ChatID, 240, "tests/videonote.mp4") + msg := NewVideoNote(ChatID, 240, FilePath("tests/videonote.mp4")) msg.Duration = 10 _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } func TestSendWithExistingVideoNote(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewVideoNoteShare(ChatID, 240, ExistingVideoNoteFileID) + msg := NewVideoNote(ChatID, 240, FileID(ExistingVideoNoteFileID)) msg.Duration = 10 _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } func TestSendWithNewSticker(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewStickerUpload(ChatID, "tests/image.jpg") + msg := NewSticker(ChatID, FilePath("tests/image.jpg")) _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } func TestSendWithExistingSticker(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewStickerShare(ChatID, ExistingStickerFileID) + msg := NewSticker(ChatID, FileID(ExistingStickerFileID)) _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } func TestSendWithNewStickerAndKeyboardHide(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewStickerUpload(ChatID, "tests/image.jpg") - msg.ReplyMarkup = tgbotapi.ReplyKeyboardRemove{ + msg := NewSticker(ChatID, FilePath("tests/image.jpg")) + msg.ReplyMarkup = ReplyKeyboardRemove{ RemoveKeyboard: true, Selective: false, }@@ -381,15 +443,14 @@ _, err := bot.Send(msg)
if err != nil { t.Error(err) - t.Fail() } } func TestSendWithExistingStickerAndKeyboardHide(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewStickerShare(ChatID, ExistingStickerFileID) - msg.ReplyMarkup = tgbotapi.ReplyKeyboardRemove{ + msg := NewSticker(ChatID, FileID(ExistingStickerFileID)) + msg.ReplyMarkup = ReplyKeyboardRemove{ RemoveKeyboard: true, Selective: false, }@@ -398,14 +459,13 @@ _, err := bot.Send(msg)
if err != nil { t.Error(err) - t.Fail() } } func TestSendWithDice(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewDice(ChatID) + msg := NewDice(ChatID) _, err := bot.Send(msg) if err != nil {@@ -418,7 +478,7 @@
func TestSendWithDiceWithEmoji(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewDiceWithEmoji(ChatID, "🏀") + msg := NewDiceWithEmoji(ChatID, "🏀") _, err := bot.Send(msg) if err != nil {@@ -431,38 +491,37 @@
func TestGetFile(t *testing.T) { bot, _ := getBot(t) - file := tgbotapi.FileConfig{FileID: ExistingPhotoFileID} + file := FileConfig{ + FileID: ExistingPhotoFileID, + } _, err := bot.GetFile(file) if err != nil { t.Error(err) - t.Fail() } } func TestSendChatConfig(t *testing.T) { bot, _ := getBot(t) - _, err := bot.Send(tgbotapi.NewChatAction(ChatID, tgbotapi.ChatTyping)) + _, err := bot.Request(NewChatAction(ChatID, ChatTyping)) if err != nil { t.Error(err) - t.Fail() } } func TestSendEditMessage(t *testing.T) { bot, _ := getBot(t) - msg, err := bot.Send(tgbotapi.NewMessage(ChatID, "Testing editing.")) + msg, err := bot.Send(NewMessage(ChatID, "Testing editing.")) if err != nil { t.Error(err) - t.Fail() } - edit := tgbotapi.EditMessageTextConfig{ - BaseEdit: tgbotapi.BaseEdit{ + edit := EditMessageTextConfig{ + BaseEdit: BaseEdit{ ChatID: ChatID, MessageID: msg.MessageID, },@@ -472,17 +531,15 @@
_, err = bot.Send(edit) if err != nil { t.Error(err) - t.Fail() } } func TestGetUserProfilePhotos(t *testing.T) { bot, _ := getBot(t) - _, err := bot.GetUserProfilePhotos(tgbotapi.NewUserProfilePhotos(ChatID)) + _, err := bot.GetUserProfilePhotos(NewUserProfilePhotos(ChatID)) if err != nil { t.Error(err) - t.Fail() } }@@ -491,19 +548,26 @@ bot, _ := getBot(t)
time.Sleep(time.Second * 2) - bot.RemoveWebhook() + bot.Request(DeleteWebhookConfig{}) + + wh, err := NewWebhookWithCert("https://example.com/tgbotapi-test/"+bot.Token, FilePath("tests/cert.pem")) - wh := tgbotapi.NewWebhookWithCert("https://example.com/tgbotapi-test/"+bot.Token, "tests/cert.pem") - _, err := bot.SetWebhook(wh) if err != nil { t.Error(err) - t.Fail() } + _, err = bot.Request(wh) + + if err != nil { + t.Error(err) + } + _, err = bot.GetWebhookInfo() + if err != nil { t.Error(err) } - bot.RemoveWebhook() + + bot.Request(DeleteWebhookConfig{}) } func TestSetWebhookWithoutCert(t *testing.T) {@@ -511,15 +575,22 @@ bot, _ := getBot(t)
time.Sleep(time.Second * 2) - bot.RemoveWebhook() + bot.Request(DeleteWebhookConfig{}) + + wh, err := NewWebhook("https://example.com/tgbotapi-test/" + bot.Token) - wh := tgbotapi.NewWebhook("https://example.com/tgbotapi-test/" + bot.Token) - _, err := bot.SetWebhook(wh) if err != nil { t.Error(err) - t.Fail() } + + _, err = bot.Request(wh) + + if err != nil { + t.Error(err) + } + info, err := bot.GetWebhookInfo() + if err != nil { t.Error(err) }@@ -527,51 +598,93 @@ 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) + t.Errorf("failed to set webhook: %s", info.LastErrorMessage) } - bot.RemoveWebhook() + + bot.Request(DeleteWebhookConfig{}) } -func TestUpdatesChan(t *testing.T) { +func TestSendWithMediaGroupPhotoVideo(t *testing.T) { bot, _ := getBot(t) - var ucfg tgbotapi.UpdateConfig = tgbotapi.NewUpdate(0) - ucfg.Timeout = 60 - _, err := bot.GetUpdatesChan(ucfg) + cfg := NewMediaGroup(ChatID, []interface{}{ + NewInputMediaPhoto(FileURL("https://github.com/go-telegram-bot-api/telegram-bot-api/raw/0a3a1c8716c4cd8d26a262af9f12dcbab7f3f28c/tests/image.jpg")), + NewInputMediaPhoto(FilePath("tests/image.jpg")), + NewInputMediaVideo(FilePath("tests/video.mp4")), + }) + messages, err := bot.SendMediaGroup(cfg) if err != nil { t.Error(err) - t.Fail() + } + + if messages == nil { + t.Error("No received messages") + } + + if len(messages) != len(cfg.Media) { + t.Errorf("Different number of messages: %d", len(messages)) + } +} + +func TestSendWithMediaGroupDocument(t *testing.T) { + bot, _ := getBot(t) + + cfg := NewMediaGroup(ChatID, []interface{}{ + NewInputMediaDocument(FileURL("https://i.imgur.com/unQLJIb.jpg")), + NewInputMediaDocument(FilePath("tests/image.jpg")), + }) + + messages, err := bot.SendMediaGroup(cfg) + if err != nil { + t.Error(err) + } + + if messages == nil { + t.Error("No received messages") + } + + if len(messages) != len(cfg.Media) { + t.Errorf("Different number of messages: %d", len(messages)) } } -func TestSendWithMediaGroup(t *testing.T) { +func TestSendWithMediaGroupAudio(t *testing.T) { bot, _ := getBot(t) - cfg := tgbotapi.NewMediaGroup(ChatID, []interface{}{ - tgbotapi.NewInputMediaPhoto("https://github.com/go-telegram-bot-api/telegram-bot-api/raw/0a3a1c8716c4cd8d26a262af9f12dcbab7f3f28c/tests/image.jpg"), - tgbotapi.NewInputMediaVideo("https://github.com/go-telegram-bot-api/telegram-bot-api/raw/0a3a1c8716c4cd8d26a262af9f12dcbab7f3f28c/tests/video.mp4"), + cfg := NewMediaGroup(ChatID, []interface{}{ + NewInputMediaAudio(FilePath("tests/audio.mp3")), + NewInputMediaAudio(FilePath("tests/audio.mp3")), }) - _, err := bot.Send(cfg) + + messages, err := bot.SendMediaGroup(cfg) if err != nil { t.Error(err) } + + if messages == nil { + t.Error("No received messages") + } + + if len(messages) != len(cfg.Media) { + t.Errorf("Different number of messages: %d", len(messages)) + } } func ExampleNewBotAPI() { - bot, err := tgbotapi.NewBotAPI("MyAwesomeBotToken") + bot, err := NewBotAPI("MyAwesomeBotToken") if err != nil { - log.Panic(err) + panic(err) } bot.Debug = true log.Printf("Authorized on account %s", bot.Self.UserName) - u := tgbotapi.NewUpdate(0) + u := NewUpdate(0) u.Timeout = 60 - updates, err := bot.GetUpdatesChan(u) + updates := bot.GetUpdatesChan(u) // Optional: wait for updates and clear them if you don't want to handle // a large backlog of old messages@@ -585,7 +698,7 @@ }
log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text) - msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text) + msg := NewMessage(update.Message.Chat.ID, update.Message.Text) msg.ReplyToMessageID = update.Message.MessageID bot.Send(msg)@@ -593,26 +706,37 @@ }
} func ExampleNewWebhook() { - bot, err := tgbotapi.NewBotAPI("MyAwesomeBotToken") + bot, err := NewBotAPI("MyAwesomeBotToken") if err != nil { - log.Fatal(err) + panic(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")) + wh, err := NewWebhookWithCert("https://www.google.com:8443/"+bot.Token, FilePath("cert.pem")) + if err != nil { - log.Fatal(err) + panic(err) } + + _, err = bot.Request(wh) + + if err != nil { + panic(err) + } + info, err := bot.GetWebhookInfo() + if err != nil { - log.Fatal(err) + panic(err) } + if info.LastErrorDate != 0 { - log.Printf("[Telegram callback failed]%s", info.LastErrorMessage) + log.Printf("failed to set webhook: %s", info.LastErrorMessage) } + updates := bot.ListenForWebhook("/" + bot.Token) go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil)@@ -622,22 +746,28 @@ }
} func ExampleWebhookHandler() { - bot, err := tgbotapi.NewBotAPI("MyAwesomeBotToken") + bot, err := NewBotAPI("MyAwesomeBotToken") if err != nil { - log.Fatal(err) + panic(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")) + wh, err := NewWebhookWithCert("https://www.google.com:8443/"+bot.Token, FilePath("cert.pem")) + if err != nil { - log.Fatal(err) + panic(err) + } + + _, err = bot.Request(wh) + if err != nil { + panic(err) } info, err := bot.GetWebhookInfo() if err != nil { - log.Fatal(err) + panic(err) } if info.LastErrorDate != 0 { log.Printf("[Telegram callback failed]%s", info.LastErrorMessage)@@ -655,35 +785,35 @@
go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil) } -func ExampleAnswerInlineQuery() { - bot, err := tgbotapi.NewBotAPI("MyAwesomeBotToken") // create new bot +func ExampleInlineConfig() { + bot, err := NewBotAPI("MyAwesomeBotToken") // create new bot if err != nil { - log.Panic(err) + panic(err) } log.Printf("Authorized on account %s", bot.Self.UserName) - u := tgbotapi.NewUpdate(0) + u := NewUpdate(0) u.Timeout = 60 - updates, err := bot.GetUpdatesChan(u) + updates := bot.GetUpdatesChan(u) for update := range updates { if update.InlineQuery == nil { // if no inline query, ignore it continue } - article := tgbotapi.NewInlineQueryResultArticle(update.InlineQuery.ID, "Echo", update.InlineQuery.Query) + article := NewInlineQueryResultArticle(update.InlineQuery.ID, "Echo", update.InlineQuery.Query) article.Description = update.InlineQuery.Query - inlineConf := tgbotapi.InlineConfig{ + inlineConf := InlineConfig{ InlineQueryID: update.InlineQuery.ID, IsPersonal: true, CacheTime: 0, Results: []interface{}{article}, } - if _, err := bot.AnswerInlineQuery(inlineConf); err != nil { + if _, err := bot.Request(inlineConf); err != nil { log.Println(err) } }@@ -692,64 +822,229 @@
func TestDeleteMessage(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewMessage(ChatID, "A test message from the test library in telegram-bot-api") - msg.ParseMode = "markdown" + msg := NewMessage(ChatID, "A test message from the test library in telegram-bot-api") + msg.ParseMode = ModeMarkdown message, _ := bot.Send(msg) - deleteMessageConfig := tgbotapi.DeleteMessageConfig{ + deleteMessageConfig := DeleteMessageConfig{ ChatID: message.Chat.ID, MessageID: message.MessageID, } - _, err := bot.DeleteMessage(deleteMessageConfig) + _, err := bot.Request(deleteMessageConfig) if err != nil { t.Error(err) - t.Fail() } } func TestPinChatMessage(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewMessage(SupergroupChatID, "A test message from the test library in telegram-bot-api") - msg.ParseMode = "markdown" + msg := NewMessage(SupergroupChatID, "A test message from the test library in telegram-bot-api") + msg.ParseMode = ModeMarkdown message, _ := bot.Send(msg) - pinChatMessageConfig := tgbotapi.PinChatMessageConfig{ + pinChatMessageConfig := PinChatMessageConfig{ ChatID: message.Chat.ID, MessageID: message.MessageID, DisableNotification: false, } - _, err := bot.PinChatMessage(pinChatMessageConfig) + _, err := bot.Request(pinChatMessageConfig) if err != nil { t.Error(err) - t.Fail() } } func TestUnpinChatMessage(t *testing.T) { bot, _ := getBot(t) - msg := tgbotapi.NewMessage(SupergroupChatID, "A test message from the test library in telegram-bot-api") - msg.ParseMode = "markdown" + msg := NewMessage(SupergroupChatID, "A test message from the test library in telegram-bot-api") + msg.ParseMode = ModeMarkdown message, _ := bot.Send(msg) // We need pin message to unpin something - pinChatMessageConfig := tgbotapi.PinChatMessageConfig{ + pinChatMessageConfig := PinChatMessageConfig{ ChatID: message.Chat.ID, MessageID: message.MessageID, DisableNotification: false, } - _, err := bot.PinChatMessage(pinChatMessageConfig) - unpinChatMessageConfig := tgbotapi.UnpinChatMessageConfig{ + if _, err := bot.Request(pinChatMessageConfig); err != nil { + t.Error(err) + } + + unpinChatMessageConfig := UnpinChatMessageConfig{ + ChatID: message.Chat.ID, + MessageID: message.MessageID, + } + + if _, err := bot.Request(unpinChatMessageConfig); err != nil { + t.Error(err) + } +} + +func TestUnpinAllChatMessages(t *testing.T) { + bot, _ := getBot(t) + + msg := NewMessage(SupergroupChatID, "A test message from the test library in telegram-bot-api") + msg.ParseMode = ModeMarkdown + message, _ := bot.Send(msg) + + pinChatMessageConfig := PinChatMessageConfig{ + ChatID: message.Chat.ID, + MessageID: message.MessageID, + DisableNotification: true, + } + + if _, err := bot.Request(pinChatMessageConfig); err != nil { + t.Error(err) + } + + unpinAllChatMessagesConfig := UnpinAllChatMessagesConfig{ ChatID: message.Chat.ID, } - _, err = bot.UnpinChatMessage(unpinChatMessageConfig) + + if _, err := bot.Request(unpinAllChatMessagesConfig); err != nil { + t.Error(err) + } +} + +func TestPolls(t *testing.T) { + bot, _ := getBot(t) + + poll := NewPoll(SupergroupChatID, "Are polls working?", "Yes", "No") + + msg, err := bot.Send(poll) + if err != nil { + t.Error(err) + } + result, err := bot.StopPoll(NewStopPoll(SupergroupChatID, msg.MessageID)) if err != nil { t.Error(err) - t.Fail() + } + + if result.Question != "Are polls working?" { + t.Error("Poll question did not match") + } + + if !result.IsClosed { + t.Error("Poll did not end") + } + + if result.Options[0].Text != "Yes" || result.Options[0].VoterCount != 0 || result.Options[1].Text != "No" || result.Options[1].VoterCount != 0 { + t.Error("Poll options were incorrect") + } +} + +func TestSendDice(t *testing.T) { + bot, _ := getBot(t) + + dice := NewSendDice(ChatID) + + msg, err := bot.Send(dice) + if err != nil { + t.Error("Unable to send dice roll") + } + + if msg.Dice == nil { + t.Error("Dice roll was not received") + } +} + +func TestCommands(t *testing.T) { + bot, _ := getBot(t) + + setCommands := NewSetMyCommands(BotCommand{ + Command: "test", + Description: "a test command", + }) + + if _, err := bot.Request(setCommands); err != nil { + t.Error("Unable to set commands") + } + + commands, err := bot.GetMyCommands() + if err != nil { + t.Error("Unable to get commands") + } + + if len(commands) != 1 { + t.Error("Incorrect number of commands returned") + } + + if commands[0].Command != "test" || commands[0].Description != "a test command" { + t.Error("Commands were incorrectly set") + } + + setCommands = NewSetMyCommandsWithScope(NewBotCommandScopeAllPrivateChats(), BotCommand{ + Command: "private", + Description: "a private command", + }) + + if _, err := bot.Request(setCommands); err != nil { + t.Error("Unable to set commands") + } + + commands, err = bot.GetMyCommandsWithConfig(NewGetMyCommandsWithScope(NewBotCommandScopeAllPrivateChats())) + if err != nil { + t.Error("Unable to get commands") + } + + if len(commands) != 1 { + t.Error("Incorrect number of commands returned") + } + + if commands[0].Command != "private" || commands[0].Description != "a private command" { + t.Error("Commands were incorrectly set") + } +} + +// TODO: figure out why test is failing +// +// func TestEditMessageMedia(t *testing.T) { +// bot, _ := getBot(t) + +// msg := NewPhoto(ChatID, "tests/image.jpg") +// msg.Caption = "Test" +// m, err := bot.Send(msg) + +// if err != nil { +// t.Error(err) +// } + +// edit := EditMessageMediaConfig{ +// BaseEdit: BaseEdit{ +// ChatID: ChatID, +// MessageID: m.MessageID, +// }, +// Media: NewInputMediaVideo(FilePath("tests/video.mp4")), +// } + +// _, err = bot.Request(edit) +// if err != nil { +// t.Error(err) +// } +// } + +func TestPrepareInputMediaForParams(t *testing.T) { + media := []interface{}{ + NewInputMediaPhoto(FilePath("tests/image.jpg")), + NewInputMediaVideo(FileID("test")), + } + + prepared := prepareInputMediaForParams(media) + + if media[0].(InputMediaPhoto).Media != FilePath("tests/image.jpg") { + t.Error("Original media was changed") + } + + if prepared[0].(InputMediaPhoto).Media != fileAttach("attach://file-0") { + t.Error("New media was not replaced") + } + + if prepared[1].(InputMediaVideo).Media != FileID("test") { + t.Error("Passthrough value was not the same") } }
@@ -1,9 +1,11 @@
package tgbotapi import ( - "encoding/json" + "bytes" + "fmt" "io" "net/url" + "os" "strconv" )@@ -18,14 +20,21 @@ )
// 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" + ChatTyping = "typing" + ChatUploadPhoto = "upload_photo" + ChatRecordVideo = "record_video" + ChatUploadVideo = "upload_video" + ChatRecordVoice = "record_voice" + ChatUploadVoice = "upload_voice" + // Deprecated: use ChatRecordVoice instead. + ChatRecordAudio = "record_audio" + // Deprecated: use ChatUploadVoice instead. + ChatUploadAudio = "upload_audio" + ChatUploadDocument = "upload_document" + ChatChooseSticker = "choose_sticker" + ChatFindLocation = "find_location" + ChatRecordVideoNote = "record_video_note" + ChatUploadVideoNote = "upload_video_note" ) // API errors@@ -41,130 +50,253 @@ ModeMarkdownV2 = "MarkdownV2"
ModeHTML = "HTML" ) +// Constant values for update types +const ( + // New incoming message of any kind — text, photo, sticker, etc. + UpdateTypeMessage = "message" + + // New version of a message that is known to the bot and was edited + UpdateTypeEditedMessage = "edited_message" + + // New incoming channel post of any kind — text, photo, sticker, etc. + UpdateTypeChannelPost = "channel_post" + + // New version of a channel post that is known to the bot and was edited + UpdateTypeEditedChannelPost = "edited_channel_post" + + // New incoming inline query + UpdateTypeInlineQuery = "inline_query" + + // The result of an inline query that was chosen by a user and sent to their + // chat partner. Please see the documentation on the feedback collecting for + // details on how to enable these updates for your bot. + UpdateTypeChosenInlineResult = "chosen_inline_result" + + // New incoming callback query + UpdateTypeCallbackQuery = "callback_query" + + // New incoming shipping query. Only for invoices with flexible price + UpdateTypeShippingQuery = "shipping_query" + + // New incoming pre-checkout query. Contains full information about checkout + UpdateTypePreCheckoutQuery = "pre_checkout_query" + + // New poll state. Bots receive only updates about stopped polls and polls + // which are sent by the bot + UpdateTypePoll = "poll" + + // A user changed their answer in a non-anonymous poll. Bots receive new votes + // only in polls that were sent by the bot itself. + UpdateTypePollAnswer = "poll_answer" + + // The bot's chat member status was updated in a chat. For private chats, this + // update is received only when the bot is blocked or unblocked by the user. + UpdateTypeMyChatMember = "my_chat_member" + + // The bot must be an administrator in the chat and must explicitly specify + // this update in the list of allowed_updates to receive these updates. + UpdateTypeChatMember = "chat_member" +) + // Library errors const ( - // ErrBadFileType happens when you pass an unknown type - ErrBadFileType = "bad file type" - ErrBadURL = "bad or empty url" + ErrBadURL = "bad or empty url" ) // Chattable is any config type that can be sent. type Chattable interface { - values() (url.Values, error) + params() (Params, error) method() string } // Fileable is any config type that can be sent that includes a file. type Fileable interface { Chattable - params() (map[string]string, error) - name() string - getFile() interface{} - useExistingFile() bool + files() []RequestFile } -// BaseChat is base type for all chat config types. -type BaseChat struct { - ChatID int64 // required - ChannelUsername string - ReplyToMessageID int - ReplyMarkup interface{} - DisableNotification bool +// RequestFile represents a file associated with a field name. +type RequestFile struct { + // The file field name. + Name string + // The file data to include. + Data RequestFileData } -func (chat *BaseChat) params() (Params, error) { - params := make(Params) +// RequestFileData represents the data to be used for a file. +type RequestFileData interface { + // If the file needs to be uploaded. + NeedsUpload() bool - params.AddFirstValid("chat_id", chat.ChatID, chat.ChannelUsername) - params.AddNonZero("reply_to_message_id", chat.ReplyToMessageID) - params.AddBool("disable_notification", chat.DisableNotification) + // Get the file name and an `io.Reader` for the file to be uploaded. This + // must only be called when the file needs to be uploaded. + UploadData() (string, io.Reader, error) + // Get the file data to send when a file does not need to be uploaded. This + // must only be called when the file does not need to be uploaded. + SendData() string +} - err := params.AddInterface("reply_markup", chat.ReplyMarkup) +// FileBytes contains information about a set of bytes to upload +// as a File. +type FileBytes struct { + Name string + Bytes []byte +} - return params, err +func (fb FileBytes) NeedsUpload() bool { + return true +} + +func (fb FileBytes) UploadData() (string, io.Reader, error) { + return fb.Name, bytes.NewReader(fb.Bytes), nil +} + +func (fb FileBytes) SendData() string { + panic("FileBytes must be uploaded") +} + +// FileReader contains information about a reader to upload as a File. +type FileReader struct { + Name string + Reader io.Reader +} + +func (fr FileReader) NeedsUpload() bool { + return true +} + +func (fr FileReader) UploadData() (string, io.Reader, error) { + return fr.Name, fr.Reader, nil +} + +func (fr FileReader) SendData() string { + panic("FileReader must be uploaded") +} + +// FilePath is a path to a local file. +type FilePath string + +func (fp FilePath) NeedsUpload() bool { + return true } -// 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.FormatInt(chat.ChatID, 10)) +func (fp FilePath) UploadData() (string, io.Reader, error) { + fileHandle, err := os.Open(string(fp)) + if err != nil { + return "", nil, err } - if chat.ReplyToMessageID != 0 { - v.Add("reply_to_message_id", strconv.Itoa(chat.ReplyToMessageID)) - } + name := fileHandle.Name() + return name, fileHandle, err +} - if chat.ReplyMarkup != nil { - data, err := json.Marshal(chat.ReplyMarkup) - if err != nil { - return v, err - } +func (fp FilePath) SendData() string { + panic("FilePath must be uploaded") +} - v.Add("reply_markup", string(data)) - } +// FileURL is a URL to use as a file for a request. +type FileURL string - v.Add("disable_notification", strconv.FormatBool(chat.DisableNotification)) +func (fu FileURL) NeedsUpload() bool { + return false +} - return v, nil +func (fu FileURL) UploadData() (string, io.Reader, error) { + panic("FileURL cannot be uploaded") } -// BaseFile is a base type for all file config types. -type BaseFile struct { - BaseChat - File interface{} - FileID string - UseExisting bool - MimeType string - FileSize int +func (fu FileURL) SendData() string { + return string(fu) +} + +// FileID is an ID of a file already uploaded to Telegram. +type FileID string + +func (fi FileID) NeedsUpload() bool { + return false +} + +func (fi FileID) UploadData() (string, io.Reader, error) { + panic("FileID cannot be uploaded") +} + +func (fi FileID) SendData() string { + return string(fi) +} + +// fileAttach is a internal file type used for processed media groups. +type fileAttach string + +func (fa fileAttach) NeedsUpload() bool { + return false } -// params returns a map[string]string representation of BaseFile. -func (file BaseFile) params() (map[string]string, error) { - params := make(map[string]string) +func (fa fileAttach) UploadData() (string, io.Reader, error) { + panic("fileAttach cannot be uploaded") +} - if file.ChannelUsername != "" { - params["chat_id"] = file.ChannelUsername - } else { - params["chat_id"] = strconv.FormatInt(file.ChatID, 10) - } +func (fa fileAttach) SendData() string { + return string(fa) +} - if file.ReplyToMessageID != 0 { - params["reply_to_message_id"] = strconv.Itoa(file.ReplyToMessageID) - } +// LogOutConfig is a request to log out of the cloud Bot API server. +// +// Note that you may not log back in for at least 10 minutes. +type LogOutConfig struct{} - if file.ReplyMarkup != nil { - data, err := json.Marshal(file.ReplyMarkup) - if err != nil { - return params, err - } +func (LogOutConfig) method() string { + return "logOut" +} - params["reply_markup"] = string(data) - } +func (LogOutConfig) params() (Params, error) { + return nil, nil +} - if file.MimeType != "" { - params["mime_type"] = file.MimeType - } +// CloseConfig is a request to close the bot instance on a local server. +// +// Note that you may not close an instance for the first 10 minutes after the +// bot has started. +type CloseConfig struct{} - if file.FileSize > 0 { - params["file_size"] = strconv.Itoa(file.FileSize) - } +func (CloseConfig) method() string { + return "close" +} - params["disable_notification"] = strconv.FormatBool(file.DisableNotification) +func (CloseConfig) params() (Params, error) { + return nil, nil +} - return params, nil +// BaseChat is base type for all chat config types. +type BaseChat struct { + ChatID int64 // required + ChannelUsername string + ReplyToMessageID int + ReplyMarkup interface{} + DisableNotification bool + AllowSendingWithoutReply bool } -// getFile returns the file. -func (file BaseFile) getFile() interface{} { - return file.File +func (chat *BaseChat) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", chat.ChatID, chat.ChannelUsername) + params.AddNonZero("reply_to_message_id", chat.ReplyToMessageID) + params.AddBool("disable_notification", chat.DisableNotification) + params.AddBool("allow_sending_without_reply", chat.AllowSendingWithoutReply) + + err := params.AddInterface("reply_markup", chat.ReplyMarkup) + + return params, err } -// useExistingFile returns if the BaseFile has already been uploaded. -func (file BaseFile) useExistingFile() bool { - return file.UseExisting +// BaseFile is a base type for all file config types. +type BaseFile struct { + BaseChat + File RequestFileData +} + +func (file BaseFile) params() (Params, error) { + return file.BaseChat.params() } // BaseEdit is base type of all chat edits.@@ -176,29 +308,19 @@ InlineMessageID string
ReplyMarkup *InlineKeyboardMarkup } -func (edit BaseEdit) values() (url.Values, error) { - v := url.Values{} +func (edit BaseEdit) params() (Params, error) { + params := make(Params) - if edit.InlineMessageID == "" { - if edit.ChannelUsername != "" { - v.Add("chat_id", edit.ChannelUsername) - } else { - v.Add("chat_id", strconv.FormatInt(edit.ChatID, 10)) - } - v.Add("message_id", strconv.Itoa(edit.MessageID)) + if edit.InlineMessageID != "" { + params["inline_message_id"] = edit.InlineMessageID } else { - v.Add("inline_message_id", edit.InlineMessageID) + params.AddFirstValid("chat_id", edit.ChatID, edit.ChannelUsername) + params.AddNonZero("message_id", edit.MessageID) } - if edit.ReplyMarkup != nil { - data, err := json.Marshal(edit.ReplyMarkup) - if err != nil { - return v, err - } - v.Add("reply_markup", string(data)) - } + err := params.AddInterface("reply_markup", edit.ReplyMarkup) - return v, nil + return params, err } // MessageConfig contains information about a SendMessage request.@@ -206,25 +328,24 @@ type MessageConfig struct {
BaseChat Text string ParseMode string + Entities []MessageEntity DisableWebPagePreview bool } -// values returns a url.Values representation of MessageConfig. -func (config MessageConfig) values() (url.Values, error) { - v, err := config.BaseChat.values() +func (config MessageConfig) params() (Params, error) { + params, err := config.BaseChat.params() if err != nil { - return v, err - } - 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 params, err } - return v, nil + params.AddNonEmpty("text", config.Text) + params.AddBool("disable_web_page_preview", config.DisableWebPagePreview) + params.AddNonEmpty("parse_mode", config.ParseMode) + err = params.AddInterface("entities", config.Entities) + + return params, err } -// method returns Telegram API method name for sending Message. func (config MessageConfig) method() string { return "sendMessage" }@@ -237,515 +358,466 @@ FromChannelUsername string
MessageID int // required } -// values returns a url.Values representation of ForwardConfig. -func (config ForwardConfig) values() (url.Values, error) { - v, err := config.BaseChat.values() +func (config ForwardConfig) params() (Params, error) { + params, err := config.BaseChat.params() if err != nil { - return v, err + return params, err } - v.Add("from_chat_id", strconv.FormatInt(config.FromChatID, 10)) - v.Add("message_id", strconv.Itoa(config.MessageID)) - return v, nil + + params.AddNonZero64("from_chat_id", config.FromChatID) + params.AddNonZero("message_id", config.MessageID) + + return params, 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 - ParseMode string +// CopyMessageConfig contains information about a copyMessage request. +type CopyMessageConfig struct { + BaseChat + FromChatID int64 + FromChannelUsername string + MessageID int + Caption string + ParseMode string + CaptionEntities []MessageEntity } -// Params returns a 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 - if config.ParseMode != "" { - params["parse_mode"] = config.ParseMode - } +func (config CopyMessageConfig) params() (Params, error) { + params, err := config.BaseChat.params() + if err != nil { + return params, err } - return params, nil + params.AddFirstValid("from_chat_id", config.FromChatID, config.FromChannelUsername) + params.AddNonZero("message_id", config.MessageID) + params.AddNonEmpty("caption", config.Caption) + params.AddNonEmpty("parse_mode", config.ParseMode) + err = params.AddInterface("caption_entities", config.CaptionEntities) + + return params, err } -// Values returns a url.Values representation of PhotoConfig. -func (config PhotoConfig) values() (url.Values, error) { - v, err := config.BaseChat.values() - if err != nil { - return v, err - } +func (config CopyMessageConfig) method() string { + return "copyMessage" +} - v.Add(config.name(), config.FileID) - if config.Caption != "" { - v.Add("caption", config.Caption) - if config.ParseMode != "" { - v.Add("parse_mode", config.ParseMode) - } +// PhotoConfig contains information about a SendPhoto request. +type PhotoConfig struct { + BaseFile + Thumb RequestFileData + Caption string + ParseMode string + CaptionEntities []MessageEntity +} + +func (config PhotoConfig) params() (Params, error) { + params, err := config.BaseFile.params() + if err != nil { + return params, err } - return v, nil -} + params.AddNonEmpty("caption", config.Caption) + params.AddNonEmpty("parse_mode", config.ParseMode) + err = params.AddInterface("caption_entities", config.CaptionEntities) -// name returns the field name for the Photo. -func (config PhotoConfig) name() string { - return "photo" + return params, err } -// method returns Telegram API method name for sending Photo. func (config PhotoConfig) method() string { return "sendPhoto" } +func (config PhotoConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "photo", + Data: config.File, + }} + + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + Data: config.Thumb, + }) + } + + return files +} + // AudioConfig contains information about a SendAudio request. type AudioConfig struct { BaseFile - Caption string - ParseMode string - Duration int - Performer string - Title string + Thumb RequestFileData + Caption string + ParseMode string + CaptionEntities []MessageEntity + Duration int + Performer string + Title string } -// values returns a url.Values representation of AudioConfig. -func (config AudioConfig) values() (url.Values, error) { - v, err := config.BaseChat.values() +func (config AudioConfig) params() (Params, error) { + params, err := config.BaseChat.params() if err != nil { - return v, err + return params, err } - 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) - } - if config.Caption != "" { - v.Add("caption", config.Caption) - if config.ParseMode != "" { - v.Add("parse_mode", config.ParseMode) - } - } + params.AddNonZero("duration", config.Duration) + params.AddNonEmpty("performer", config.Performer) + params.AddNonEmpty("title", config.Title) + params.AddNonEmpty("caption", config.Caption) + params.AddNonEmpty("parse_mode", config.ParseMode) + err = params.AddInterface("caption_entities", config.CaptionEntities) - return v, nil + return params, err } -// params returns a map[string]string representation of AudioConfig. -func (config AudioConfig) params() (map[string]string, error) { - params, _ := config.BaseFile.params() +func (config AudioConfig) method() string { + return "sendAudio" +} - if config.Duration != 0 { - params["duration"] = strconv.Itoa(config.Duration) - } +func (config AudioConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "audio", + Data: config.File, + }} - if config.Performer != "" { - params["performer"] = config.Performer - } - if config.Title != "" { - params["title"] = config.Title - } - if config.Caption != "" { - params["caption"] = config.Caption - if config.ParseMode != "" { - params["parse_mode"] = config.ParseMode - } + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + Data: config.Thumb, + }) } - return params, nil -} - -// name returns the field name for the Audio. -func (config AudioConfig) name() string { - return "audio" -} - -// method returns Telegram API method name for sending Audio. -func (config AudioConfig) method() string { - return "sendAudio" + return files } // DocumentConfig contains information about a SendDocument request. type DocumentConfig struct { BaseFile - Caption string - ParseMode string + Thumb RequestFileData + Caption string + ParseMode string + CaptionEntities []MessageEntity + DisableContentTypeDetection bool } -// values returns a url.Values representation of DocumentConfig. -func (config DocumentConfig) values() (url.Values, error) { - v, err := config.BaseChat.values() - if err != nil { - return v, err - } +func (config DocumentConfig) params() (Params, error) { + params, err := config.BaseFile.params() - v.Add(config.name(), config.FileID) - if config.Caption != "" { - v.Add("caption", config.Caption) - if config.ParseMode != "" { - v.Add("parse_mode", config.ParseMode) - } - } + params.AddNonEmpty("caption", config.Caption) + params.AddNonEmpty("parse_mode", config.ParseMode) + params.AddBool("disable_content_type_detection", config.DisableContentTypeDetection) - return v, nil + return params, err } -// params returns a map[string]string representation of DocumentConfig. -func (config DocumentConfig) params() (map[string]string, error) { - params, _ := config.BaseFile.params() - - if config.Caption != "" { - params["caption"] = config.Caption - if config.ParseMode != "" { - params["parse_mode"] = config.ParseMode - } - } - - return params, nil -} - -// name returns the field name for the Document. -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 -} +func (config DocumentConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "document", + Data: config.File, + }} -// values returns a url.Values representation of StickerConfig. -func (config StickerConfig) values() (url.Values, error) { - v, err := config.BaseChat.values() - if err != nil { - return v, err + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + Data: config.Thumb, + }) } - v.Add(config.name(), config.FileID) - - return v, nil + return files } -// params returns a map[string]string representation of StickerConfig. -func (config StickerConfig) params() (map[string]string, error) { - params, _ := config.BaseFile.params() - - return params, nil +// StickerConfig contains information about a SendSticker request. +type StickerConfig struct { + BaseFile } -// name returns the field name for the Sticker. -func (config StickerConfig) name() string { - return "sticker" +func (config StickerConfig) params() (Params, error) { + return config.BaseChat.params() } -// method returns Telegram API method name for sending Sticker. func (config StickerConfig) method() string { return "sendSticker" } +func (config StickerConfig) files() []RequestFile { + return []RequestFile{{ + Name: "sticker", + Data: config.File, + }} +} + // VideoConfig contains information about a SendVideo request. type VideoConfig struct { BaseFile - Duration int - Caption string - ParseMode string + Thumb RequestFileData + Duration int + Caption string + ParseMode string + CaptionEntities []MessageEntity + SupportsStreaming bool } -// values returns a url.Values representation of VideoConfig. -func (config VideoConfig) values() (url.Values, error) { - v, err := config.BaseChat.values() +func (config VideoConfig) params() (Params, error) { + params, err := config.BaseChat.params() if err != nil { - return v, err + return params, err } - 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) - if config.ParseMode != "" { - v.Add("parse_mode", config.ParseMode) - } - } + params.AddNonZero("duration", config.Duration) + params.AddNonEmpty("caption", config.Caption) + params.AddNonEmpty("parse_mode", config.ParseMode) + params.AddBool("supports_streaming", config.SupportsStreaming) + err = params.AddInterface("caption_entities", config.CaptionEntities) - return v, nil + return params, err } -// params returns a map[string]string representation of VideoConfig. -func (config VideoConfig) params() (map[string]string, error) { - params, _ := config.BaseFile.params() - - if config.Caption != "" { - params["caption"] = config.Caption - if config.ParseMode != "" { - params["parse_mode"] = config.ParseMode - } - } - - return params, nil +func (config VideoConfig) method() string { + return "sendVideo" } -// name returns the field name for the Video. -func (config VideoConfig) name() string { - return "video" -} +func (config VideoConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "video", + Data: config.File, + }} -// method returns Telegram API method name for sending Video. -func (config VideoConfig) method() string { - return "sendVideo" + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + Data: config.Thumb, + }) + } + + return files } // AnimationConfig contains information about a SendAnimation request. type AnimationConfig struct { BaseFile - Duration int - Caption string - ParseMode string + Duration int + Thumb RequestFileData + Caption string + ParseMode string + CaptionEntities []MessageEntity } -// values returns a url.Values representation of AnimationConfig. -func (config AnimationConfig) values() (url.Values, error) { - v, err := config.BaseChat.values() +func (config AnimationConfig) params() (Params, error) { + params, err := config.BaseChat.params() if err != nil { - return v, err + return params, err } - 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) - if config.ParseMode != "" { - v.Add("parse_mode", config.ParseMode) - } - } + params.AddNonZero("duration", config.Duration) + params.AddNonEmpty("caption", config.Caption) + params.AddNonEmpty("parse_mode", config.ParseMode) + err = params.AddInterface("caption_entities", config.CaptionEntities) - return v, nil + return params, err } -// params returns a map[string]string representation of AnimationConfig. -func (config AnimationConfig) params() (map[string]string, error) { - params, _ := config.BaseFile.params() +func (config AnimationConfig) method() string { + return "sendAnimation" +} - if config.Caption != "" { - params["caption"] = config.Caption - if config.ParseMode != "" { - params["parse_mode"] = config.ParseMode - } +func (config AnimationConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "animation", + Data: config.File, + }} + + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + Data: config.Thumb, + }) } - return params, nil -} - -// name returns the field name for the Animation. -func (config AnimationConfig) name() string { - return "animation" -} - -// method returns Telegram API method name for sending Animation. -func (config AnimationConfig) method() string { - return "sendAnimation" + return files } // VideoNoteConfig contains information about a SendVideoNote request. type VideoNoteConfig struct { BaseFile + Thumb RequestFileData Duration int Length int } -// values returns a url.Values representation of VideoNoteConfig. -func (config VideoNoteConfig) values() (url.Values, error) { - v, err := config.BaseChat.values() - if err != nil { - return v, err - } +func (config VideoNoteConfig) params() (Params, error) { + params, err := config.BaseChat.params() - v.Add(config.name(), config.FileID) - if config.Duration != 0 { - v.Add("duration", strconv.Itoa(config.Duration)) - } + params.AddNonZero("duration", config.Duration) + params.AddNonZero("length", config.Length) - // Telegram API seems to have a bug, if no length is provided or it is 0, it will send an error response - if config.Length != 0 { - v.Add("length", strconv.Itoa(config.Length)) - } + return params, err +} - return v, nil +func (config VideoNoteConfig) method() string { + return "sendVideoNote" } -// params returns a map[string]string representation of VideoNoteConfig. -func (config VideoNoteConfig) params() (map[string]string, error) { - params, _ := config.BaseFile.params() +func (config VideoNoteConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "video_note", + Data: config.File, + }} - if config.Length != 0 { - params["length"] = strconv.Itoa(config.Length) - } - if config.Duration != 0 { - params["duration"] = strconv.Itoa(config.Duration) + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + Data: config.Thumb, + }) } - return params, nil -} - -// name returns the field name for the VideoNote. -func (config VideoNoteConfig) name() string { - return "video_note" -} - -// method returns Telegram API method name for sending VideoNote. -func (config VideoNoteConfig) method() string { - return "sendVideoNote" + return files } // VoiceConfig contains information about a SendVoice request. type VoiceConfig struct { BaseFile - Caption string - ParseMode string - Duration int + Thumb RequestFileData + Caption string + ParseMode string + CaptionEntities []MessageEntity + Duration int } -// values returns a url.Values representation of VoiceConfig. -func (config VoiceConfig) values() (url.Values, error) { - v, err := config.BaseChat.values() +func (config VoiceConfig) params() (Params, error) { + params, err := config.BaseChat.params() if err != nil { - return v, err + return params, err } - 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) - if config.ParseMode != "" { - v.Add("parse_mode", config.ParseMode) - } - } + params.AddNonZero("duration", config.Duration) + params.AddNonEmpty("caption", config.Caption) + params.AddNonEmpty("parse_mode", config.ParseMode) + err = params.AddInterface("caption_entities", config.CaptionEntities) + + return params, err +} - return v, nil +func (config VoiceConfig) method() string { + return "sendVoice" } -// params returns a map[string]string representation of VoiceConfig. -func (config VoiceConfig) params() (map[string]string, error) { - params, _ := config.BaseFile.params() +func (config VoiceConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "voice", + Data: config.File, + }} - if config.Duration != 0 { - params["duration"] = strconv.Itoa(config.Duration) - } - if config.Caption != "" { - params["caption"] = config.Caption - if config.ParseMode != "" { - params["parse_mode"] = config.ParseMode - } + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + Data: config.Thumb, + }) } - return params, nil + return files } -// name returns the field name for the Voice. -func (config VoiceConfig) name() string { - return "voice" +// LocationConfig contains information about a SendLocation request. +type LocationConfig struct { + BaseChat + Latitude float64 // required + Longitude float64 // required + HorizontalAccuracy float64 // optional + LivePeriod int // optional + Heading int // optional + ProximityAlertRadius int // optional } -// method returns Telegram API method name for sending Voice. -func (config VoiceConfig) method() string { - return "sendVoice" +func (config LocationConfig) params() (Params, error) { + params, err := config.BaseChat.params() + + params.AddNonZeroFloat("latitude", config.Latitude) + params.AddNonZeroFloat("longitude", config.Longitude) + params.AddNonZeroFloat("horizontal_accuracy", config.HorizontalAccuracy) + params.AddNonZero("live_period", config.LivePeriod) + params.AddNonZero("heading", config.Heading) + params.AddNonZero("proximity_alert_radius", config.ProximityAlertRadius) + + return params, err } -// MediaGroupConfig contains information about a sendMediaGroup request. -type MediaGroupConfig struct { - BaseChat - InputMedia []interface{} +func (config LocationConfig) method() string { + return "sendLocation" } -func (config MediaGroupConfig) values() (url.Values, error) { - v, err := config.BaseChat.values() - if err != nil { - return v, err - } +// EditMessageLiveLocationConfig allows you to update a live location. +type EditMessageLiveLocationConfig struct { + BaseEdit + Latitude float64 // required + Longitude float64 // required + HorizontalAccuracy float64 // optional + Heading int // optional + ProximityAlertRadius int // optional +} - data, err := json.Marshal(config.InputMedia) - if err != nil { - return v, err - } +func (config EditMessageLiveLocationConfig) params() (Params, error) { + params, err := config.BaseEdit.params() - v.Add("media", string(data)) + params.AddNonZeroFloat("latitude", config.Latitude) + params.AddNonZeroFloat("longitude", config.Longitude) + params.AddNonZeroFloat("horizontal_accuracy", config.HorizontalAccuracy) + params.AddNonZero("heading", config.Heading) + params.AddNonZero("proximity_alert_radius", config.ProximityAlertRadius) - return v, nil + return params, err } -func (config MediaGroupConfig) method() string { - return "sendMediaGroup" +func (config EditMessageLiveLocationConfig) method() string { + return "editMessageLiveLocation" } -// LocationConfig contains information about a SendLocation request. -type LocationConfig struct { - BaseChat - Latitude float64 // required - Longitude float64 // required +// StopMessageLiveLocationConfig stops updating a live location. +type StopMessageLiveLocationConfig struct { + BaseEdit } -// values returns a url.Values representation of LocationConfig. -func (config LocationConfig) values() (url.Values, error) { - v, err := config.BaseChat.values() - if err != nil { - return v, err - } - - v.Add("latitude", strconv.FormatFloat(config.Latitude, 'f', 6, 64)) - v.Add("longitude", strconv.FormatFloat(config.Longitude, 'f', 6, 64)) - - return v, nil +func (config StopMessageLiveLocationConfig) params() (Params, error) { + return config.BaseEdit.params() } -// method returns Telegram API method name for sending Location. -func (config LocationConfig) method() string { - return "sendLocation" +func (config StopMessageLiveLocationConfig) method() string { + return "stopMessageLiveLocation" } // VenueConfig contains information about a SendVenue request. type VenueConfig struct { BaseChat - Latitude float64 // required - Longitude float64 // required - Title string // required - Address string // required - FoursquareID string + Latitude float64 // required + Longitude float64 // required + Title string // required + Address string // required + FoursquareID string + FoursquareType string + GooglePlaceID string + GooglePlaceType string } -func (config VenueConfig) values() (url.Values, error) { - v, err := config.BaseChat.values() - if err != nil { - return v, err - } +func (config VenueConfig) params() (Params, error) { + params, err := config.BaseChat.params() - v.Add("latitude", strconv.FormatFloat(config.Latitude, 'f', 6, 64)) - v.Add("longitude", strconv.FormatFloat(config.Longitude, 'f', 6, 64)) - v.Add("title", config.Title) - v.Add("address", config.Address) - if config.FoursquareID != "" { - v.Add("foursquare_id", config.FoursquareID) - } + params.AddNonZeroFloat("latitude", config.Latitude) + params.AddNonZeroFloat("longitude", config.Longitude) + params["title"] = config.Title + params["address"] = config.Address + params.AddNonEmpty("foursquare_id", config.FoursquareID) + params.AddNonEmpty("foursquare_type", config.FoursquareType) + params.AddNonEmpty("google_place_id", config.GooglePlaceID) + params.AddNonEmpty("google_place_type", config.GooglePlaceType) - return v, nil + return params, err } func (config VenueConfig) method() string {@@ -758,19 +830,19 @@ BaseChat
PhoneNumber string FirstName string LastName string + VCard string } -func (config ContactConfig) values() (url.Values, error) { - v, err := config.BaseChat.values() - if err != nil { - return v, err - } +func (config ContactConfig) params() (Params, error) { + params, err := config.BaseChat.params() - v.Add("phone_number", config.PhoneNumber) - v.Add("first_name", config.FirstName) - v.Add("last_name", config.LastName) + params["phone_number"] = config.PhoneNumber + params["first_name"] = config.FirstName + + params.AddNonEmpty("last_name", config.LastName) + params.AddNonEmpty("vcard", config.VCard) - return v, nil + return params, err } func (config ContactConfig) method() string {@@ -788,19 +860,22 @@ AllowsMultipleAnswers bool
CorrectOptionID int64 Explanation string ExplanationParseMode string + ExplanationEntities []MessageEntity OpenPeriod int CloseDate int IsClosed bool } -func (config SendPollConfig) values() (url.Values, error) { +func (config SendPollConfig) params() (Params, error) { params, err := config.BaseChat.params() if err != nil { - return params.toValues(), err + return params, err } params["question"] = config.Question - err = params.AddInterface("options", config.Options) + if err = params.AddInterface("options", config.Options); err != nil { + return params, err + } params["is_anonymous"] = strconv.FormatBool(config.IsAnonymous) params.AddNonEmpty("type", config.Type) params["allows_multiple_answers"] = strconv.FormatBool(config.AllowsMultipleAnswers)@@ -810,8 +885,9 @@ params.AddNonEmpty("explanation", config.Explanation)
params.AddNonEmpty("explanation_parse_mode", config.ExplanationParseMode) params.AddNonZero("open_period", config.OpenPeriod) params.AddNonZero("close_date", config.CloseDate) + err = params.AddInterface("explanation_entities", config.ExplanationEntities) - return params.toValues(), err + return params, err } func (SendPollConfig) method() string {@@ -824,15 +900,12 @@ BaseChat
GameShortName string } -func (config GameConfig) values() (url.Values, error) { - v, err := config.BaseChat.values() - if err != nil { - return v, err - } +func (config GameConfig) params() (Params, error) { + params, err := config.BaseChat.params() - v.Add("game_short_name", config.GameShortName) + params["game_short_name"] = config.GameShortName - return v, nil + return params, err } func (config GameConfig) method() string {@@ -841,7 +914,7 @@ }
// SetGameScoreConfig allows you to update the game score in a chat. type SetGameScoreConfig struct { - UserID int + UserID int64 Score int Force bool DisableEditMessage bool@@ -851,24 +924,21 @@ MessageID int
InlineMessageID string } -func (config SetGameScoreConfig) values() (url.Values, error) { - v := url.Values{} +func (config SetGameScoreConfig) params() (Params, error) { + params := make(Params) - 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.FormatInt(config.ChatID, 10)) - } else { - v.Add("chat_id", config.ChannelUsername) - } - v.Add("message_id", strconv.Itoa(config.MessageID)) + params.AddNonZero64("user_id", config.UserID) + params.AddNonZero("scrore", config.Score) + params.AddBool("disable_edit_message", config.DisableEditMessage) + + if config.InlineMessageID != "" { + params["inline_message_id"] = config.InlineMessageID } else { - v.Add("inline_message_id", config.InlineMessageID) + params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + params.AddNonZero("message_id", config.MessageID) } - v.Add("disable_edit_message", strconv.FormatBool(config.DisableEditMessage)) - return v, nil + return params, nil } func (config SetGameScoreConfig) method() string {@@ -877,29 +947,26 @@ }
// GetGameHighScoresConfig allows you to fetch the high scores for a game. type GetGameHighScoresConfig struct { - UserID int - ChatID int + UserID int64 + ChatID int64 ChannelUsername string MessageID int InlineMessageID string } -func (config GetGameHighScoresConfig) values() (url.Values, error) { - v := url.Values{} +func (config GetGameHighScoresConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) - 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)) + if config.InlineMessageID != "" { + params["inline_message_id"] = config.InlineMessageID } else { - v.Add("inline_message_id", config.InlineMessageID) + params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + params.AddNonZero("message_id", config.MessageID) } - return v, nil + return params, nil } func (config GetGameHighScoresConfig) method() string {@@ -912,17 +979,14 @@ BaseChat
Action string // required } -// values returns a url.Values representation of ChatActionConfig. -func (config ChatActionConfig) values() (url.Values, error) { - v, err := config.BaseChat.values() - if err != nil { - return v, err - } - v.Add("action", config.Action) - return v, nil +func (config ChatActionConfig) params() (Params, error) { + params, err := config.BaseChat.params() + + params["action"] = config.Action + + return params, err } -// method returns Telegram API method name for sending ChatAction. func (config ChatActionConfig) method() string { return "sendChatAction" }@@ -932,20 +996,22 @@ type EditMessageTextConfig struct {
BaseEdit Text string ParseMode string + Entities []MessageEntity DisableWebPagePreview bool } -func (config EditMessageTextConfig) values() (url.Values, error) { - v, err := config.BaseEdit.values() +func (config EditMessageTextConfig) params() (Params, error) { + params, err := config.BaseEdit.params() if err != nil { - return v, err + return params, err } - v.Add("text", config.Text) - v.Add("parse_mode", config.ParseMode) - v.Add("disable_web_page_preview", strconv.FormatBool(config.DisableWebPagePreview)) + params["text"] = config.Text + params.AddNonEmpty("parse_mode", config.ParseMode) + params.AddBool("disable_web_page_preview", config.DisableWebPagePreview) + err = params.AddInterface("entities", config.Entities) - return v, nil + return params, err } func (config EditMessageTextConfig) method() string {@@ -955,80 +1021,198 @@
// EditMessageCaptionConfig allows you to modify the caption of a message. type EditMessageCaptionConfig struct { BaseEdit - Caption string - ParseMode string + Caption string + ParseMode string + CaptionEntities []MessageEntity } -func (config EditMessageCaptionConfig) values() (url.Values, error) { - v, _ := config.BaseEdit.values() +func (config EditMessageCaptionConfig) params() (Params, error) { + params, err := config.BaseEdit.params() + if err != nil { + return params, err + } - v.Add("caption", config.Caption) - if config.ParseMode != "" { - v.Add("parse_mode", config.ParseMode) - } + params["caption"] = config.Caption + params.AddNonEmpty("parse_mode", config.ParseMode) + err = params.AddInterface("caption_entities", config.CaptionEntities) - return v, nil + return params, err } func (config EditMessageCaptionConfig) method() string { return "editMessageCaption" } +// EditMessageMediaConfig allows you to make an editMessageMedia request. +type EditMessageMediaConfig struct { + BaseEdit + + Media interface{} +} + +func (EditMessageMediaConfig) method() string { + return "editMessageMedia" +} + +func (config EditMessageMediaConfig) params() (Params, error) { + params, err := config.BaseEdit.params() + if err != nil { + return params, err + } + + err = params.AddInterface("media", prepareInputMediaParam(config.Media, 0)) + + return params, err +} + +func (config EditMessageMediaConfig) files() []RequestFile { + return prepareInputMediaFile(config.Media, 0) +} + // EditMessageReplyMarkupConfig allows you to modify the reply markup // of a message. type EditMessageReplyMarkupConfig struct { BaseEdit } -func (config EditMessageReplyMarkupConfig) values() (url.Values, error) { - return config.BaseEdit.values() +func (config EditMessageReplyMarkupConfig) params() (Params, error) { + return config.BaseEdit.params() } func (config EditMessageReplyMarkupConfig) method() string { return "editMessageReplyMarkup" } +// StopPollConfig allows you to stop a poll sent by the bot. +type StopPollConfig struct { + BaseEdit +} + +func (config StopPollConfig) params() (Params, error) { + return config.BaseEdit.params() +} + +func (StopPollConfig) method() string { + return "stopPoll" +} + // UserProfilePhotosConfig contains information about a // GetUserProfilePhotos request. type UserProfilePhotosConfig struct { - UserID int + UserID int64 Offset int Limit int } +func (UserProfilePhotosConfig) method() string { + return "getUserProfilePhotos" +} + +func (config UserProfilePhotosConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + params.AddNonZero("offset", config.Offset) + params.AddNonZero("limit", config.Limit) + + return params, nil +} + // FileConfig has information about a file hosted on Telegram. type FileConfig struct { FileID string } +func (FileConfig) method() string { + return "getFile" +} + +func (config FileConfig) params() (Params, error) { + params := make(Params) + + params["file_id"] = config.FileID + + return params, nil +} + // UpdateConfig contains information about a GetUpdates request. type UpdateConfig struct { - Offset int - Limit int - Timeout int + Offset int + Limit int + Timeout int + AllowedUpdates []string +} + +func (UpdateConfig) method() string { + return "getUpdates" +} + +func (config UpdateConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero("offset", config.Offset) + params.AddNonZero("limit", config.Limit) + params.AddNonZero("timeout", config.Timeout) + params.AddInterface("allowed_updates", config.AllowedUpdates) + + return params, nil } // WebhookConfig contains information about a SetWebhook request. type WebhookConfig struct { - URL *url.URL - Certificate interface{} - MaxConnections int + URL *url.URL + Certificate RequestFileData + IPAddress string + MaxConnections int + AllowedUpdates []string + DropPendingUpdates bool } -// FileBytes contains information about a set of bytes to upload -// as a File. -type FileBytes struct { - Name string - Bytes []byte +func (config WebhookConfig) method() string { + return "setWebhook" } -// 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 +func (config WebhookConfig) params() (Params, error) { + params := make(Params) + + if config.URL != nil { + params["url"] = config.URL.String() + } + + params.AddNonEmpty("ip_address", config.IPAddress) + params.AddNonZero("max_connections", config.MaxConnections) + err := params.AddInterface("allowed_updates", config.AllowedUpdates) + params.AddBool("drop_pending_updates", config.DropPendingUpdates) + + return params, err +} + +func (config WebhookConfig) files() []RequestFile { + if config.Certificate != nil { + return []RequestFile{{ + Name: "certificate", + Data: config.Certificate, + }} + } + + return nil +} + +// DeleteWebhookConfig is a helper to delete a webhook. +type DeleteWebhookConfig struct { + DropPendingUpdates bool +} + +func (config DeleteWebhookConfig) method() string { + return "deleteWebhook" +} + +func (config DeleteWebhookConfig) params() (Params, error) { + params := make(Params) + + params.AddBool("drop_pending_updates", config.DropPendingUpdates) + + return params, nil } // InlineConfig contains information on making an InlineQuery response.@@ -1042,6 +1226,24 @@ SwitchPMText string `json:"switch_pm_text"`
SwitchPMParameter string `json:"switch_pm_parameter"` } +func (config InlineConfig) method() string { + return "answerInlineQuery" +} + +func (config InlineConfig) params() (Params, error) { + params := make(Params) + + params["inline_query_id"] = config.InlineQueryID + params.AddNonZero("cache_time", config.CacheTime) + params.AddBool("is_personal", config.IsPersonal) + params.AddNonEmpty("next_offset", config.NextOffset) + params.AddNonEmpty("switch_pm_text", config.SwitchPMText) + params.AddNonEmpty("switch_pm_parameter", config.SwitchPMParameter) + err := params.AddInterface("results", config.Results) + + return params, err +} + // CallbackConfig contains information on making a CallbackQuery response. type CallbackConfig struct { CallbackQueryID string `json:"callback_query_id"`@@ -1051,42 +1253,156 @@ URL string `json:"url"`
CacheTime int `json:"cache_time"` } +func (config CallbackConfig) method() string { + return "answerCallbackQuery" +} + +func (config CallbackConfig) params() (Params, error) { + params := make(Params) + + params["callback_query_id"] = config.CallbackQueryID + params.AddNonEmpty("text", config.Text) + params.AddBool("show_alert", config.ShowAlert) + params.AddNonEmpty("url", config.URL) + params.AddNonZero("cache_time", config.CacheTime) + + return params, nil +} + // ChatMemberConfig contains information about a user in a chat for use // with administrative functions such as kicking or unbanning a user. type ChatMemberConfig struct { ChatID int64 SuperGroupUsername string ChannelUsername string - UserID int + UserID int64 +} + +// UnbanChatMemberConfig allows you to unban a user. +type UnbanChatMemberConfig struct { + ChatMemberConfig + OnlyIfBanned bool +} + +func (config UnbanChatMemberConfig) method() string { + return "unbanChatMember" +} + +func (config UnbanChatMemberConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername) + params.AddNonZero64("user_id", config.UserID) + params.AddBool("only_if_banned", config.OnlyIfBanned) + + return params, nil } // KickChatMemberConfig contains extra fields to kick user type KickChatMemberConfig struct { ChatMemberConfig - UntilDate int64 + UntilDate int64 + RevokeMessages bool +} + +func (config KickChatMemberConfig) method() string { + return "kickChatMember" +} + +func (config KickChatMemberConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + params.AddNonZero64("user_id", config.UserID) + params.AddNonZero64("until_date", config.UntilDate) + params.AddBool("revoke_messages", config.RevokeMessages) + + return params, nil } // RestrictChatMemberConfig contains fields to restrict members of chat type RestrictChatMemberConfig struct { ChatMemberConfig - UntilDate int64 - CanSendMessages *bool - CanSendMediaMessages *bool - CanSendOtherMessages *bool - CanAddWebPagePreviews *bool + UntilDate int64 + Permissions *ChatPermissions +} + +func (config RestrictChatMemberConfig) method() string { + return "restrictChatMember" +} + +func (config RestrictChatMemberConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername) + params.AddNonZero64("user_id", config.UserID) + + err := params.AddInterface("permissions", config.Permissions) + params.AddNonZero64("until_date", config.UntilDate) + + return params, err } // PromoteChatMemberConfig contains fields to promote members of chat type PromoteChatMemberConfig struct { ChatMemberConfig - CanChangeInfo *bool - CanPostMessages *bool - CanEditMessages *bool - CanDeleteMessages *bool - CanInviteUsers *bool - CanRestrictMembers *bool - CanPinMessages *bool - CanPromoteMembers *bool + IsAnonymous bool + CanManageChat bool + CanChangeInfo bool + CanPostMessages bool + CanEditMessages bool + CanDeleteMessages bool + CanManageVoiceChats bool + CanInviteUsers bool + CanRestrictMembers bool + CanPinMessages bool + CanPromoteMembers bool +} + +func (config PromoteChatMemberConfig) method() string { + return "promoteChatMember" +} + +func (config PromoteChatMemberConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername) + params.AddNonZero64("user_id", config.UserID) + + params.AddBool("is_anonymous", config.IsAnonymous) + params.AddBool("can_manage_chat", config.CanManageChat) + params.AddBool("can_change_info", config.CanChangeInfo) + params.AddBool("can_post_messages", config.CanPostMessages) + params.AddBool("can_edit_messages", config.CanEditMessages) + params.AddBool("can_delete_messages", config.CanDeleteMessages) + params.AddBool("can_manage_voice_chats", config.CanManageVoiceChats) + params.AddBool("can_invite_users", config.CanInviteUsers) + params.AddBool("can_restrict_members", config.CanRestrictMembers) + params.AddBool("can_pin_messages", config.CanPinMessages) + params.AddBool("can_promote_members", config.CanPromoteMembers) + + return params, nil +} + +// SetChatAdministratorCustomTitle sets the title of an administrative user +// promoted by the bot for a chat. +type SetChatAdministratorCustomTitle struct { + ChatMemberConfig + CustomTitle string +} + +func (SetChatAdministratorCustomTitle) method() string { + return "setChatAdministratorCustomTitle" +} + +func (config SetChatAdministratorCustomTitle) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername) + params.AddNonZero64("user_id", config.UserID) + params.AddNonEmpty("custom_title", config.CustomTitle) + + return params, nil } // ChatConfig contains information about getting information on a chat.@@ -1095,80 +1411,299 @@ ChatID int64
SuperGroupUsername string } -// ChatConfigWithUser contains information about getting information on -// a specific user within a chat. +func (config ChatConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + + return params, nil +} + +// ChatInfoConfig contains information about getting chat information. +type ChatInfoConfig struct { + ChatConfig +} + +func (ChatInfoConfig) method() string { + return "getChat" +} + +// ChatMemberCountConfig contains information about getting the number of users in a chat. +type ChatMemberCountConfig struct { + ChatConfig +} + +func (ChatMemberCountConfig) method() string { + return "getChatMembersCount" +} + +// ChatAdministratorsConfig contains information about getting chat administrators. +type ChatAdministratorsConfig struct { + ChatConfig +} + +func (ChatAdministratorsConfig) method() string { + return "getChatAdministrators" +} + +// SetChatPermissionsConfig allows you to set default permissions for the +// members in a group. The bot must be an administrator and have rights to +// restrict members. +type SetChatPermissionsConfig struct { + ChatConfig + Permissions *ChatPermissions +} + +func (SetChatPermissionsConfig) method() string { + return "setChatPermissions" +} + +func (config SetChatPermissionsConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + err := params.AddInterface("permissions", config.Permissions) + + return params, err +} + +// ChatInviteLinkConfig contains information about getting a chat link. +// +// Note that generating a new link will revoke any previous links. +type ChatInviteLinkConfig struct { + ChatConfig +} + +func (ChatInviteLinkConfig) method() string { + return "exportChatInviteLink" +} + +func (config ChatInviteLinkConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + + return params, nil +} + +// CreateChatInviteLinkConfig allows you to create an additional invite link for +// a chat. The bot must be an administrator in the chat for this to work and +// must have the appropriate admin rights. The link can be revoked using the +// RevokeChatInviteLinkConfig. +type CreateChatInviteLinkConfig struct { + ChatConfig + Name string + ExpireDate int + MemberLimit int + CreatesJoinRequest bool +} + +func (CreateChatInviteLinkConfig) method() string { + return "createChatInviteLink" +} + +func (config CreateChatInviteLinkConfig) params() (Params, error) { + params := make(Params) + + params.AddNonEmpty("name", config.Name) + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + params.AddNonZero("expire_date", config.ExpireDate) + params.AddNonZero("member_limit", config.MemberLimit) + params.AddBool("creates_join_request", config.CreatesJoinRequest) + + return params, nil +} + +// EditChatInviteLinkConfig allows you to edit a non-primary invite link created +// by the bot. The bot must be an administrator in the chat for this to work and +// must have the appropriate admin rights. +type EditChatInviteLinkConfig struct { + ChatConfig + InviteLink string + Name string + ExpireDate int + MemberLimit int + CreatesJoinRequest bool +} + +func (EditChatInviteLinkConfig) method() string { + return "editChatInviteLink" +} + +func (config EditChatInviteLinkConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + params.AddNonEmpty("name", config.Name) + params["invite_link"] = config.InviteLink + params.AddNonZero("expire_date", config.ExpireDate) + params.AddNonZero("member_limit", config.MemberLimit) + params.AddBool("creates_join_request", config.CreatesJoinRequest) + + return params, nil +} + +// RevokeChatInviteLinkConfig allows you to revoke an invite link created by the +// bot. If the primary link is revoked, a new link is automatically generated. +// The bot must be an administrator in the chat for this to work and must have +// the appropriate admin rights. +type RevokeChatInviteLinkConfig struct { + ChatConfig + InviteLink string +} + +func (RevokeChatInviteLinkConfig) method() string { + return "revokeChatInviteLink" +} + +func (config RevokeChatInviteLinkConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + params["invite_link"] = config.InviteLink + + return params, nil +} + +// ApproveChatJoinRequestConfig allows you to approve a chat join request. +type ApproveChatJoinRequestConfig struct { + ChatConfig + UserID int64 +} + +func (ApproveChatJoinRequestConfig) method() string { + return "approveChatJoinRequest" +} + +func (config ApproveChatJoinRequestConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + params.AddNonZero("user_id", int(config.UserID)) + + return params, nil +} + +// DeclineChatJoinRequest allows you to decline a chat join request. +type DeclineChatJoinRequest struct { + ChatConfig + UserID int64 +} + +func (DeclineChatJoinRequest) method() string { + return "declineChatJoinRequest" +} + +func (config DeclineChatJoinRequest) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + params.AddNonZero("user_id", int(config.UserID)) + + return params, nil +} + +// LeaveChatConfig allows you to leave a chat. +type LeaveChatConfig struct { + ChatID int64 + ChannelUsername string +} + +func (config LeaveChatConfig) method() string { + return "leaveChat" +} + +func (config LeaveChatConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + + return params, nil +} + +// ChatConfigWithUser contains information about a chat and a user. type ChatConfigWithUser struct { ChatID int64 SuperGroupUsername string - UserID int + UserID int64 +} + +func (config ChatConfigWithUser) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + params.AddNonZero64("user_id", config.UserID) + + return params, nil +} + +// GetChatMemberConfig is information about getting a specific member in a chat. +type GetChatMemberConfig struct { + ChatConfigWithUser +} + +func (GetChatMemberConfig) method() string { + return "getChatMember" } // InvoiceConfig contains information for sendInvoice request. type InvoiceConfig struct { BaseChat - Title string // required - Description string // required - Payload string // required - ProviderToken string // required - StartParameter string // required - Currency string // required - Prices *[]LabeledPrice // required - PhotoURL string - PhotoSize int - PhotoWidth int - PhotoHeight int - NeedName bool - NeedPhoneNumber bool - NeedEmail bool - NeedShippingAddress bool - IsFlexible bool + Title string // required + Description string // required + Payload string // required + ProviderToken string // required + Currency string // required + Prices []LabeledPrice // required + MaxTipAmount int + SuggestedTipAmounts []int + StartParameter string + ProviderData string + PhotoURL string + PhotoSize int + PhotoWidth int + PhotoHeight int + NeedName bool + NeedPhoneNumber bool + NeedEmail bool + NeedShippingAddress bool + SendPhoneNumberToProvider bool + SendEmailToProvider bool + IsFlexible bool } -func (config InvoiceConfig) values() (url.Values, error) { - v, err := config.BaseChat.values() - if err != nil { - return v, err - } - v.Add("title", config.Title) - v.Add("description", config.Description) - v.Add("payload", config.Payload) - v.Add("provider_token", config.ProviderToken) - v.Add("start_parameter", config.StartParameter) - v.Add("currency", config.Currency) - data, err := json.Marshal(config.Prices) +func (config InvoiceConfig) params() (Params, error) { + params, err := config.BaseChat.params() if err != nil { - return v, err + return params, err } - v.Add("prices", string(data)) - if config.PhotoURL != "" { - v.Add("photo_url", config.PhotoURL) + + params["title"] = config.Title + params["description"] = config.Description + params["payload"] = config.Payload + params["provider_token"] = config.ProviderToken + params["currency"] = config.Currency + if err = params.AddInterface("prices", config.Prices); err != nil { + return params, err } - if config.PhotoSize != 0 { - v.Add("photo_size", strconv.Itoa(config.PhotoSize)) - } - if config.PhotoWidth != 0 { - v.Add("photo_width", strconv.Itoa(config.PhotoWidth)) - } - if config.PhotoHeight != 0 { - v.Add("photo_height", strconv.Itoa(config.PhotoHeight)) - } - if config.NeedName != false { - v.Add("need_name", strconv.FormatBool(config.NeedName)) - } - if config.NeedPhoneNumber != false { - v.Add("need_phone_number", strconv.FormatBool(config.NeedPhoneNumber)) - } - if config.NeedEmail != false { - v.Add("need_email", strconv.FormatBool(config.NeedEmail)) - } - if config.NeedShippingAddress != false { - v.Add("need_shipping_address", strconv.FormatBool(config.NeedShippingAddress)) - } - if config.IsFlexible != false { - v.Add("is_flexible", strconv.FormatBool(config.IsFlexible)) - } + + params.AddNonZero("max_tip_amount", config.MaxTipAmount) + err = params.AddInterface("suggested_tip_amounts", config.SuggestedTipAmounts) + params.AddNonEmpty("start_parameter", config.StartParameter) + params.AddNonEmpty("provider_data", config.ProviderData) + params.AddNonEmpty("photo_url", config.PhotoURL) + params.AddNonZero("photo_size", config.PhotoSize) + params.AddNonZero("photo_width", config.PhotoWidth) + params.AddNonZero("photo_height", config.PhotoHeight) + params.AddBool("need_name", config.NeedName) + params.AddBool("need_phone_number", config.NeedPhoneNumber) + params.AddBool("need_email", config.NeedEmail) + params.AddBool("need_shipping_address", config.NeedShippingAddress) + params.AddBool("is_flexible", config.IsFlexible) + params.AddBool("send_phone_number_to_provider", config.SendPhoneNumberToProvider) + params.AddBool("send_email_to_provider", config.SendEmailToProvider) - return v, nil + return params, err } func (config InvoiceConfig) method() string {@@ -1179,10 +1714,25 @@ // ShippingConfig contains information for answerShippingQuery request.
type ShippingConfig struct { ShippingQueryID string // required OK bool // required - ShippingOptions *[]ShippingOption + ShippingOptions []ShippingOption ErrorMessage string } +func (config ShippingConfig) method() string { + return "answerShippingQuery" +} + +func (config ShippingConfig) params() (Params, error) { + params := make(Params) + + params["shipping_query_id"] = config.ShippingQueryID + params.AddBool("ok", config.OK) + err := params.AddInterface("shipping_options", config.ShippingOptions) + params.AddNonEmpty("error_message", config.ErrorMessage) + + return params, err +} + // PreCheckoutConfig conatins information for answerPreCheckoutQuery request. type PreCheckoutConfig struct { PreCheckoutQueryID string // required@@ -1190,6 +1740,20 @@ OK bool // required
ErrorMessage string } +func (config PreCheckoutConfig) method() string { + return "answerPreCheckoutQuery" +} + +func (config PreCheckoutConfig) params() (Params, error) { + params := make(Params) + + params["pre_checkout_query_id"] = config.PreCheckoutQueryID + params.AddBool("ok", config.OK) + params.AddNonEmpty("error_message", config.ErrorMessage) + + return params, nil +} + // DeleteMessageConfig contains information of a message in a chat to delete. type DeleteMessageConfig struct { ChannelUsername string@@ -1201,23 +1765,19 @@ func (config DeleteMessageConfig) method() string {
return "deleteMessage" } -func (config DeleteMessageConfig) values() (url.Values, error) { - v := url.Values{} +func (config DeleteMessageConfig) params() (Params, error) { + params := make(Params) - if config.ChannelUsername == "" { - v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) - } else { - v.Add("chat_id", config.ChannelUsername) - } + params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + params.AddNonZero("message_id", config.MessageID) - v.Add("message_id", strconv.Itoa(config.MessageID)) - - return v, nil + return params, nil } // PinChatMessageConfig contains information of a message in a chat to pin. type PinChatMessageConfig struct { ChatID int64 + ChannelUsername string MessageID int DisableNotification bool }@@ -1226,55 +1786,117 @@ func (config PinChatMessageConfig) method() string {
return "pinChatMessage" } -func (config PinChatMessageConfig) values() (url.Values, error) { - v := url.Values{} +func (config PinChatMessageConfig) params() (Params, error) { + params := make(Params) - v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) - v.Add("message_id", strconv.Itoa(config.MessageID)) - v.Add("disable_notification", strconv.FormatBool(config.DisableNotification)) + params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + params.AddNonZero("message_id", config.MessageID) + params.AddBool("disable_notification", config.DisableNotification) - return v, nil + return params, nil } -// UnpinChatMessageConfig contains information of chat to unpin. +// UnpinChatMessageConfig contains information of a chat message to unpin. +// +// If MessageID is not specified, it will unpin the most recent pin. type UnpinChatMessageConfig struct { - ChatID int64 + ChatID int64 + ChannelUsername string + MessageID int } func (config UnpinChatMessageConfig) method() string { return "unpinChatMessage" } -func (config UnpinChatMessageConfig) values() (url.Values, error) { - v := url.Values{} +func (config UnpinChatMessageConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + params.AddNonZero("message_id", config.MessageID) - v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + return params, nil +} + +// UnpinAllChatMessagesConfig contains information of all messages to unpin in +// a chat. +type UnpinAllChatMessagesConfig struct { + ChatID int64 + ChannelUsername string +} - return v, nil +func (config UnpinAllChatMessagesConfig) method() string { + return "unpinAllChatMessages" } -// SetChatTitleConfig contains information for change chat title. +func (config UnpinAllChatMessagesConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + + return params, nil +} + +// SetChatPhotoConfig allows you to set a group, supergroup, or channel's photo. +type SetChatPhotoConfig struct { + BaseFile +} + +func (config SetChatPhotoConfig) method() string { + return "setChatPhoto" +} + +func (config SetChatPhotoConfig) files() []RequestFile { + return []RequestFile{{ + Name: "photo", + Data: config.File, + }} +} + +// DeleteChatPhotoConfig allows you to delete a group, supergroup, or channel's photo. +type DeleteChatPhotoConfig struct { + ChatID int64 + ChannelUsername string +} + +func (config DeleteChatPhotoConfig) method() string { + return "deleteChatPhoto" +} + +func (config DeleteChatPhotoConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + + return params, nil +} + +// SetChatTitleConfig allows you to set the title of something other than a private chat. type SetChatTitleConfig struct { - ChatID int64 - Title string + ChatID int64 + ChannelUsername string + + Title string } func (config SetChatTitleConfig) method() string { return "setChatTitle" } -func (config SetChatTitleConfig) values() (url.Values, error) { - v := url.Values{} +func (config SetChatTitleConfig) params() (Params, error) { + params := make(Params) - v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) - v.Add("title", config.Title) + params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + params["title"] = config.Title - return v, nil + return params, nil } -// SetChatDescriptionConfig contains information for change chat description. +// SetChatDescriptionConfig allows you to set the description of a supergroup or channel. type SetChatDescriptionConfig struct { - ChatID int64 + ChatID int64 + ChannelUsername string + Description string }@@ -1282,85 +1904,516 @@ func (config SetChatDescriptionConfig) method() string {
return "setChatDescription" } -func (config SetChatDescriptionConfig) values() (url.Values, error) { - v := url.Values{} +func (config SetChatDescriptionConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + params["description"] = config.Description + + return params, nil +} + +// GetStickerSetConfig allows you to get the stickers in a set. +type GetStickerSetConfig struct { + Name string +} + +func (config GetStickerSetConfig) method() string { + return "getStickerSet" +} + +func (config GetStickerSetConfig) params() (Params, error) { + params := make(Params) + + params["name"] = config.Name + + return params, nil +} + +// UploadStickerConfig allows you to upload a sticker for use in a set later. +type UploadStickerConfig struct { + UserID int64 + PNGSticker RequestFileData +} + +func (config UploadStickerConfig) method() string { + return "uploadStickerFile" +} + +func (config UploadStickerConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + + return params, nil +} + +func (config UploadStickerConfig) files() []RequestFile { + return []RequestFile{{ + Name: "png_sticker", + Data: config.PNGSticker, + }} +} + +// NewStickerSetConfig allows creating a new sticker set. +// +// You must set either PNGSticker or TGSSticker. +type NewStickerSetConfig struct { + UserID int64 + Name string + Title string + PNGSticker RequestFileData + TGSSticker RequestFileData + Emojis string + ContainsMasks bool + MaskPosition *MaskPosition +} + +func (config NewStickerSetConfig) method() string { + return "createNewStickerSet" +} + +func (config NewStickerSetConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + params["name"] = config.Name + params["title"] = config.Title + + params["emojis"] = config.Emojis + + params.AddBool("contains_masks", config.ContainsMasks) + + err := params.AddInterface("mask_position", config.MaskPosition) + + return params, err +} + +func (config NewStickerSetConfig) files() []RequestFile { + if config.PNGSticker != nil { + return []RequestFile{{ + Name: "png_sticker", + Data: config.PNGSticker, + }} + } + + return []RequestFile{{ + Name: "tgs_sticker", + Data: config.TGSSticker, + }} +} + +// AddStickerConfig allows you to add a sticker to a set. +type AddStickerConfig struct { + UserID int64 + Name string + PNGSticker RequestFileData + TGSSticker RequestFileData + Emojis string + MaskPosition *MaskPosition +} + +func (config AddStickerConfig) method() string { + return "addStickerToSet" +} + +func (config AddStickerConfig) params() (Params, error) { + params := make(Params) + + params.AddNonZero64("user_id", config.UserID) + params["name"] = config.Name + params["emojis"] = config.Emojis + + err := params.AddInterface("mask_position", config.MaskPosition) + + return params, err +} + +func (config AddStickerConfig) files() []RequestFile { + if config.PNGSticker != nil { + return []RequestFile{{ + Name: "png_sticker", + Data: config.PNGSticker, + }} + } + + return []RequestFile{{ + Name: "tgs_sticker", + Data: config.TGSSticker, + }} + +} + +// SetStickerPositionConfig allows you to change the position of a sticker in a set. +type SetStickerPositionConfig struct { + Sticker string + Position int +} + +func (config SetStickerPositionConfig) method() string { + return "setStickerPositionInSet" +} + +func (config SetStickerPositionConfig) params() (Params, error) { + params := make(Params) + + params["sticker"] = config.Sticker + params.AddNonZero("position", config.Position) + + return params, nil +} + +// DeleteStickerConfig allows you to delete a sticker from a set. +type DeleteStickerConfig struct { + Sticker string +} + +func (config DeleteStickerConfig) method() string { + return "deleteStickerFromSet" +} + +func (config DeleteStickerConfig) params() (Params, error) { + params := make(Params) + + params["sticker"] = config.Sticker - v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) - v.Add("description", config.Description) + return params, nil +} + +// SetStickerSetThumbConfig allows you to set the thumbnail for a sticker set. +type SetStickerSetThumbConfig struct { + Name string + UserID int64 + Thumb RequestFileData +} - return v, nil +func (config SetStickerSetThumbConfig) method() string { + return "setStickerSetThumb" } -// SetChatPhotoConfig contains information for change chat photo -type SetChatPhotoConfig struct { - BaseFile +func (config SetStickerSetThumbConfig) params() (Params, error) { + params := make(Params) + + params["name"] = config.Name + params.AddNonZero64("user_id", config.UserID) + + return params, nil +} + +func (config SetStickerSetThumbConfig) files() []RequestFile { + return []RequestFile{{ + Name: "thumb", + Data: config.Thumb, + }} +} + +// SetChatStickerSetConfig allows you to set the sticker set for a supergroup. +type SetChatStickerSetConfig struct { + ChatID int64 + SuperGroupUsername string + + StickerSetName string } -// name returns the field name for the Photo. -func (config SetChatPhotoConfig) name() string { - return "photo" +func (config SetChatStickerSetConfig) method() string { + return "setChatStickerSet" } -// method returns Telegram API method name for sending Photo. -func (config SetChatPhotoConfig) method() string { - return "setChatPhoto" +func (config SetChatStickerSetConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + params["sticker_set_name"] = config.StickerSetName + + return params, nil } -// DeleteChatPhotoConfig contains information for delete chat photo. -type DeleteChatPhotoConfig struct { - ChatID int64 +// DeleteChatStickerSetConfig allows you to remove a supergroup's sticker set. +type DeleteChatStickerSetConfig struct { + ChatID int64 + SuperGroupUsername string } -func (config DeleteChatPhotoConfig) method() string { - return "deleteChatPhoto" +func (config DeleteChatStickerSetConfig) method() string { + return "deleteChatStickerSet" } -func (config DeleteChatPhotoConfig) values() (url.Values, error) { - v := url.Values{} +func (config DeleteChatStickerSetConfig) params() (Params, error) { + params := make(Params) - v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) - return v, nil + return params, nil } -// GetStickerSetConfig contains information for get sticker set. -type GetStickerSetConfig struct { - Name string +// MediaGroupConfig allows you to send a group of media. +// +// Media consist of InputMedia items (InputMediaPhoto, InputMediaVideo). +type MediaGroupConfig struct { + ChatID int64 + ChannelUsername string + + Media []interface{} + DisableNotification bool + ReplyToMessageID int } -func (config GetStickerSetConfig) method() string { - return "getStickerSet" +func (config MediaGroupConfig) method() string { + return "sendMediaGroup" +} + +func (config MediaGroupConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + params.AddBool("disable_notification", config.DisableNotification) + params.AddNonZero("reply_to_message_id", config.ReplyToMessageID) + + err := params.AddInterface("media", prepareInputMediaForParams(config.Media)) + + return params, err } -func (config GetStickerSetConfig) values() (url.Values, error) { - v := url.Values{} - v.Add("name", config.Name) - return v, nil +func (config MediaGroupConfig) files() []RequestFile { + return prepareInputMediaForFiles(config.Media) } // 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 “🏀”. + // Currently, must be one of 🎲, 🎯, 🏀, ⚽, 🎳, or 🎰. + // Dice can have values 1-6 for 🎲, 🎯, and 🎳, values 1-5 for 🏀 and ⚽, + // and values 1-64 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() +func (config DiceConfig) method() string { + return "sendDice" +} + +func (config DiceConfig) params() (Params, error) { + params, err := config.BaseChat.params() if err != nil { - return v, err + return params, err } - if config.Emoji != "" { - v.Add("emoji", config.Emoji) + + params.AddNonEmpty("emoji", config.Emoji) + + return params, err +} + +// GetMyCommandsConfig gets a list of the currently registered commands. +type GetMyCommandsConfig struct { + Scope *BotCommandScope + LanguageCode string +} + +func (config GetMyCommandsConfig) method() string { + return "getMyCommands" +} + +func (config GetMyCommandsConfig) params() (Params, error) { + params := make(Params) + + err := params.AddInterface("scope", config.Scope) + params.AddNonEmpty("language_code", config.LanguageCode) + + return params, err +} + +// SetMyCommandsConfig sets a list of commands the bot understands. +type SetMyCommandsConfig struct { + Commands []BotCommand + Scope *BotCommandScope + LanguageCode string +} + +func (config SetMyCommandsConfig) method() string { + return "setMyCommands" +} + +func (config SetMyCommandsConfig) params() (Params, error) { + params := make(Params) + + if err := params.AddInterface("commands", config.Commands); err != nil { + return params, err } - return v, nil + err := params.AddInterface("scope", config.Scope) + params.AddNonEmpty("language_code", config.LanguageCode) + + return params, err +} + +type DeleteMyCommandsConfig struct { + Scope *BotCommandScope + LanguageCode string +} + +func (config DeleteMyCommandsConfig) method() string { + return "deleteMyCommands" } -// method returns Telegram API method name for sending Dice. -func (config DiceConfig) method() string { - return "sendDice" +func (config DeleteMyCommandsConfig) params() (Params, error) { + params := make(Params) + + err := params.AddInterface("scope", config.Scope) + params.AddNonEmpty("language_code", config.LanguageCode) + + return params, err +} + +// prepareInputMediaParam evaluates a single InputMedia and determines if it +// needs to be modified for a successful upload. If it returns nil, then the +// value does not need to be included in the params. Otherwise, it will return +// the same type as was originally provided. +// +// The idx is used to calculate the file field name. If you only have a single +// file, 0 may be used. It is formatted into "attach://file-%d" for the primary +// media and "attach://file-%d-thumb" for thumbnails. +// +// It is expected to be used in conjunction with prepareInputMediaFile. +func prepareInputMediaParam(inputMedia interface{}, idx int) interface{} { + switch m := inputMedia.(type) { + case InputMediaPhoto: + if m.Media.NeedsUpload() { + m.Media = fileAttach(fmt.Sprintf("attach://file-%d", idx)) + } + + return m + case InputMediaVideo: + if m.Media.NeedsUpload() { + m.Media = fileAttach(fmt.Sprintf("attach://file-%d", idx)) + } + + if m.Thumb != nil && m.Thumb.NeedsUpload() { + m.Thumb = fileAttach(fmt.Sprintf("attach://file-%d-thumb", idx)) + } + + return m + case InputMediaAudio: + if m.Media.NeedsUpload() { + m.Media = fileAttach(fmt.Sprintf("attach://file-%d", idx)) + } + + if m.Thumb != nil && m.Thumb.NeedsUpload() { + m.Thumb = fileAttach(fmt.Sprintf("attach://file-%d-thumb", idx)) + } + + return m + case InputMediaDocument: + if m.Media.NeedsUpload() { + m.Media = fileAttach(fmt.Sprintf("attach://file-%d", idx)) + } + + if m.Thumb != nil && m.Thumb.NeedsUpload() { + m.Thumb = fileAttach(fmt.Sprintf("attach://file-%d-thumb", idx)) + } + + return m + } + + return nil +} + +// prepareInputMediaFile generates an array of RequestFile to provide for +// Fileable's files method. It returns an array as a single InputMedia may have +// multiple files, for the primary media and a thumbnail. +// +// The idx parameter is used to generate file field names. It uses the names +// "file-%d" for the main file and "file-%d-thumb" for the thumbnail. +// +// It is expected to be used in conjunction with prepareInputMediaParam. +func prepareInputMediaFile(inputMedia interface{}, idx int) []RequestFile { + files := []RequestFile{} + + switch m := inputMedia.(type) { + case InputMediaPhoto: + if m.Media.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + Data: m.Media, + }) + } + case InputMediaVideo: + if m.Media.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + Data: m.Media, + }) + } + + if m.Thumb != nil && m.Thumb.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + Data: m.Thumb, + }) + } + case InputMediaDocument: + if m.Media.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + Data: m.Media, + }) + } + + if m.Thumb != nil && m.Thumb.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + Data: m.Thumb, + }) + } + case InputMediaAudio: + if m.Media.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + Data: m.Media, + }) + } + + if m.Thumb != nil && m.Thumb.NeedsUpload() { + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + Data: m.Thumb, + }) + } + } + + return files +} + +// prepareInputMediaForParams calls prepareInputMediaParam for each item +// provided and returns a new array with the correct params for a request. +// +// It is expected that files will get data from the associated function, +// prepareInputMediaForFiles. +func prepareInputMediaForParams(inputMedia []interface{}) []interface{} { + newMedia := make([]interface{}, len(inputMedia)) + copy(newMedia, inputMedia) + + for idx, media := range inputMedia { + if param := prepareInputMediaParam(media, idx); param != nil { + newMedia[idx] = param + } + } + + return newMedia +} + +// prepareInputMediaForFiles calls prepareInputMediaFile for each item +// provided and returns a new array with the correct files for a request. +// +// It is expected that params will get data from the associated function, +// prepareInputMediaForParams. +func prepareInputMediaForFiles(inputMedia []interface{}) []RequestFile { + files := []RequestFile{} + + for idx, media := range inputMedia { + if file := prepareInputMediaFile(media, idx); file != nil { + files = append(files, file...) + } + } + + return files }
@@ -0,0 +1,17 @@
+# Summary + +- [Getting Started](./getting-started/README.md) + - [Library Structure](./getting-started/library-structure.md) + - [Files](./getting-started/files.md) + - [Important Notes](./getting-started/important-notes.md) +- [Examples](./examples/README.md) + - [Command Handling](./examples/command-handling.md) + - [Keyboard](./examples/keyboard.md) + - [Inline Keyboard](./examples/inline-keyboard.md) +- [Change Log](./changelog.md) + +# Contributing + +- [Internals](./internals/README.md) + - [Adding Endpoints](./internals/adding-endpoints.md) + - [Uploading Files](./internals/uploading-files.md)
@@ -0,0 +1,19 @@
+# Change Log + +## v5 + +**Work In Progress** + +- Remove all methods that return `(APIResponse, error)`. + - Use the `Request` method instead. + - For more information, see [Library Structure][library-structure]. +- Remove all `New*Upload` and `New*Share` methods, replace with `New*`. + - Use different [file types][files] to specify if upload or share. +- Rename `UploadFile` to `UploadFiles`, accept `[]RequestFile` instead of a + single fieldname and file. +- Fix methods returning `APIResponse` and errors to always use pointers. +- Update user IDs to `int64` because of Bot API changes. +- Add missing Bot API features. + +[library-structure]: ./getting-started/library-structure.md#methods +[files]: ./getting-started/files.md
@@ -0,0 +1,4 @@
+# Examples + +With a better understanding of how the library works, let's look at some more +examples showing off some of Telegram's features.
@@ -0,0 +1,60 @@
+# Command Handling + +This is a simple example of changing behavior based on a provided command. + +```go +package main + +import ( + "log" + "os" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +func main() { + bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN")) + if err != nil { + log.Panic(err) + } + + bot.Debug = true + + log.Printf("Authorized on account %s", bot.Self.UserName) + + u := tgbotapi.NewUpdate(0) + u.Timeout = 60 + + updates := bot.GetUpdatesChan(u) + + for update := range updates { + if update.Message == nil { // ignore any non-Message updates + continue + } + + if !update.Message.IsCommand() { // ignore any non-command Messages + continue + } + + // Create a new MessageConfig. We don't have text yet, + // so we leave it empty. + msg := tgbotapi.NewMessage(update.Message.Chat.ID, "") + + // Extract the command from the Message. + switch update.Message.Command() { + case "help": + msg.Text = "I understand /sayhi and /status." + case "sayhi": + msg.Text = "Hi :)" + case "status": + msg.Text = "I'm ok." + default: + msg.Text = "I don't know that command" + } + + if _, err := bot.Send(msg); err != nil { + log.Panic(err) + } + } +} +```
@@ -0,0 +1,80 @@
+# Inline Keyboard + +This bot waits for you to send it the message "open" before sending you an +inline keyboard containing a URL and some numbers. When a number is clicked, it +sends you a message with your selected number. + +```go +package main + +import ( + "log" + "os" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup( + tgbotapi.NewInlineKeyboardRow( + tgbotapi.NewInlineKeyboardButtonURL("1.com", "http://1.com"), + tgbotapi.NewInlineKeyboardButtonData("2", "2"), + tgbotapi.NewInlineKeyboardButtonData("3", "3"), + ), + tgbotapi.NewInlineKeyboardRow( + tgbotapi.NewInlineKeyboardButtonData("4", "4"), + tgbotapi.NewInlineKeyboardButtonData("5", "5"), + tgbotapi.NewInlineKeyboardButtonData("6", "6"), + ), +) + +func main() { + bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN")) + if err != nil { + log.Panic(err) + } + + bot.Debug = true + + log.Printf("Authorized on account %s", bot.Self.UserName) + + u := tgbotapi.NewUpdate(0) + u.Timeout = 60 + + updates := bot.GetUpdatesChan(u) + + // Loop through each update. + for update := range updates { + // Check if we've gotten a message update. + if update.Message != nil { + // Construct a new message from the given chat ID and containing + // the text that we received. + msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text) + + // If the message was open, add a copy of our numeric keyboard. + switch update.Message.Text { + case "open": + msg.ReplyMarkup = numericKeyboard + + } + + // Send the message. + if _, err = bot.Send(msg); err != nil { + panic(err) + } + } else if update.CallbackQuery != nil { + // Respond to the callback query, telling Telegram to show the user + // a message with the data received. + callback := tgbotapi.NewCallback(update.CallbackQuery.ID, update.CallbackQuery.Data) + if _, err := bot.Request(callback); err != nil { + panic(err) + } + + // And finally, send a message containing the data received. + msg := tgbotapi.NewMessage(update.CallbackQuery.Message.Chat.ID, update.CallbackQuery.Data) + if _, err := bot.Send(msg); err != nil { + panic(err) + } + } + } +} +```
@@ -0,0 +1,63 @@
+# Keyboard + +This bot shows a numeric keyboard when you send a "open" message and hides it +when you send "close" message. + +```go +package main + +import ( + "log" + "os" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +var numericKeyboard = tgbotapi.NewReplyKeyboard( + tgbotapi.NewKeyboardButtonRow( + tgbotapi.NewKeyboardButton("1"), + tgbotapi.NewKeyboardButton("2"), + tgbotapi.NewKeyboardButton("3"), + ), + tgbotapi.NewKeyboardButtonRow( + tgbotapi.NewKeyboardButton("4"), + tgbotapi.NewKeyboardButton("5"), + tgbotapi.NewKeyboardButton("6"), + ), +) + +func main() { + bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN")) + if err != nil { + log.Panic(err) + } + + bot.Debug = true + + log.Printf("Authorized on account %s", bot.Self.UserName) + + u := tgbotapi.NewUpdate(0) + u.Timeout = 60 + + updates := bot.GetUpdatesChan(u) + + for update := range updates { + if update.Message == nil { // ignore non-Message updates + continue + } + + msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text) + + switch update.Message.Text { + case "open": + msg.ReplyMarkup = numericKeyboard + case "close": + msg.ReplyMarkup = tgbotapi.NewRemoveKeyboard(true) + } + + if _, err := bot.Send(msg); err != nil { + log.Panic(err) + } + } +} +```
@@ -0,0 +1,109 @@
+# Getting Started + +This library is designed as a simple wrapper around the Telegram Bot API. +It's encouraged to read [Telegram's docs][telegram-docs] first to get an +understanding of what Bots are capable of doing. They also provide some good +approaches to solve common problems. + +[telegram-docs]: https://core.telegram.org/bots + +## Installing + +```bash +go get -u github.com/go-telegram-bot-api/telegram-bot-api/v5 +``` + +## A Simple Bot + +To walk through the basics, let's create a simple echo bot that replies to your +messages repeating what you said. Make sure you get an API token from +[@Botfather][botfather] before continuing. + +Let's start by constructing a new [BotAPI][bot-api-docs]. + +[botfather]: https://t.me/Botfather +[bot-api-docs]: https://pkg.go.dev/github.com/go-telegram-bot-api/telegram-bot-api/v5?tab=doc#BotAPI + +```go +package main + +import ( + "os" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +func main() { + bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN")) + if err != nil { + panic(err) + } + + bot.Debug = true +} +``` + +Instead of typing the API token directly into the file, we're using +environment variables. This makes it easy to configure our Bot to use the right +account and prevents us from leaking our real token into the world. Anyone with +your token can send and receive messages from your Bot! + +We've also set `bot.Debug = true` in order to get more information about the +requests being sent to Telegram. If you run the example above, you'll see +information about a request to the [`getMe`][get-me] endpoint. The library +automatically calls this to ensure your token is working as expected. It also +fills in the `Self` field in your `BotAPI` struct with information about the +Bot. + +Now that we've connected to Telegram, let's start getting updates and doing +things. We can add this code in right after the line enabling debug mode. + +[get-me]: https://core.telegram.org/bots/api#getme + +```go + // Create a new UpdateConfig struct with an offset of 0. Offsets are used + // to make sure Telegram knows we've handled previous values and we don't + // need them repeated. + updateConfig := tgbotapi.NewUpdate(0) + + // Tell Telegram we should wait up to 30 seconds on each request for an + // update. This way we can get information just as quickly as making many + // frequent requests without having to send nearly as many. + updateConfig.Timeout = 30 + + // Start polling Telegram for updates. + updates := bot.GetUpdatesChan(updateConfig) + + // Let's go through each update that we're getting from Telegram. + for update := range updates { + // Telegram can send many types of updates depending on what your Bot + // is up to. We only want to look at messages for now, so we can + // discard any other updates. + if update.Message == nil { + continue + } + + // Now that we know we've gotten a new message, we can construct a + // reply! We'll take the Chat ID and Text from the incoming message + // and use it to create a new message. + msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text) + // We'll also say that this message is a reply to the previous message. + // For any other specifications than Chat ID or Text, you'll need to + // set fields on the `MessageConfig`. + msg.ReplyToMessageID = update.Message.MessageID + + // Okay, we're sending our message off! We don't care about the message + // we just sent, so we'll discard it. + if _, err := bot.Send(msg); err != nil { + // Note that panics are a bad way to handle errors. Telegram can + // have service outages or network errors, you should retry sending + // messages or more gracefully handle failures. + panic(err) + } + } +``` + +Congradulations! You've made your very own bot! + +Now that you've got some of the basics down, we can start talking about how the +library is structured and more advanced features.
@@ -0,0 +1,68 @@
+# Files + +Telegram supports specifying files in many different formats. In order to +accommodate them all, there are multiple structs and type aliases required. + +All of these types implement the `RequestFileData` interface. + +| Type | Description | +| ------------ | ------------------------------------------------------------------------- | +| `FilePath` | A local path to a file | +| `FileID` | Existing file ID on Telegram's servers | +| `FileURL` | URL to file, must be served with expected MIME type | +| `FileReader` | Use an `io.Reader` to provide a file. Lazily read to save memory. | +| `FileBytes` | `[]byte` containing file data. Prefer to use `FileReader` to save memory. | + +## `FilePath` + +A path to a local file. + +```go +file := tgbotapi.FilePath("tests/image.jpg") +``` + +## `FileID` + +An ID previously uploaded to Telegram. IDs may only be reused by the same bot +that received them. Additionally, thumbnail IDs cannot be reused. + +```go +file := tgbotapi.FileID("AgACAgIAAxkDAALesF8dCjAAAa_…") +``` + +## `FileURL` + +A URL to an existing resource. It must be served with a correct MIME type to +work as expected. + +```go +file := tgbotapi.FileURL("https://i.imgur.com/unQLJIb.jpg") +``` + +## `FileReader` + +Use an `io.Reader` to provide file contents as needed. Requires a filename for +the virtual file. + +```go +var reader io.Reader + +file := tgbotapi.FileReader{ + Name: "image.jpg", + Reader: reader, +} +``` + +## `FileBytes` + +Use a `[]byte` to provide file contents. Generally try to avoid this as it +results in high memory usage. Also requires a filename for the virtual file. + +```go +var data []byte + +file := tgbotapi.FileBytes{ + Name: "image.jpg", + Bytes: data, +} +```
@@ -0,0 +1,56 @@
+# Important Notes + +The Telegram Bot API has a few potentially unanticipated behaviors. Here are a +few of them. If any behavior was surprising to you, please feel free to open a +pull request! + +## Callback Queries + +- Every callback query must be answered, even if there is nothing to display to + the user. Failure to do so will show a loading icon on the keyboard until the + operation times out. + +## ChatMemberUpdated + +- In order to receive `ChatMember` updates, you must explicitly add + `UpdateTypeChatMember` to your `AllowedUpdates` when getting updates or + setting your webhook. + +## Entities use UTF16 + +- When extracting text entities using offsets and lengths, characters can appear + to be in incorrect positions. This is because Telegram uses UTF16 lengths + while Golang uses UTF8. It's possible to convert between the two, see + [issue #231][issue-231] for more details. + +[issue-231]: https://github.com/go-telegram-bot-api/telegram-bot-api/issues/231 + +## GetUpdatesChan + +- This method is very basic and likely unsuitable for production use. Consider + creating your own implementation instead, as it's very simple to replicate. +- This method only allows your bot to process one update at a time. You can + spawn goroutines to handle updates concurrently or switch to webhooks instead. + Webhooks are suggested for high traffic bots. + +## Nil Updates + +- At most one of the fields in an `Update` will be set to a non-nil value. When + evaluating updates, you must make sure you check that the field is not nil + before trying to access any of it's fields. + +## Privacy Mode + +- By default, bots only get updates directly addressed to them. If you need to + get all messages, you must disable privacy mode with Botfather. Bots already + added to groups will need to be removed and readded for the changes to take + effect. You can read more on the [Telegram Bot API docs][api-docs]. + +[api-docs]: https://core.telegram.org/bots/faq#what-messages-will-my-bot-get + +## User and Chat ID size + +- These types require up to 52 significant bits to store correctly, making a + 64-bit integer type required in most languages. They are already `int64` types + in this library, but make sure you use correct types when saving them to a + database or passing them to another language.
@@ -0,0 +1,37 @@
+# Library Structure + +This library is generally broken into three components you need to understand. + +## Configs + +Configs are collections of fields related to a single request. For example, if +one wanted to use the `sendMessage` endpoint, you could use the `MessageConfig` +struct to configure the request. There is a one-to-one relationship between +Telegram endpoints and configs. They generally have the naming pattern of +removing the `send` prefix and they all end with the `Config` suffix. They +generally implement the `Chattable` interface. If they can send files, they +implement the `Fileable` interface. + +## Helpers + +Helpers are easier ways of constructing common Configs. Instead of having to +create a `MessageConfig` struct and remember to set the `ChatID` and `Text`, +you can use the `NewMessage` helper method. It takes the two required parameters +for the request to succeed. You can then set fields on the resulting +`MessageConfig` after it's creation. They are generally named the same as +method names except with `send` replaced with `New`. + +## Methods + +Methods are used to send Configs after they are constructed. Generally, +`Request` is the lowest level method you'll have to call. It accepts a +`Chattable` parameter and knows how to upload files if needed. It returns an +`APIResponse`, the most general return type from the Bot API. This method is +called for any endpoint that doesn't have a more specific return type. For +example, `setWebhook` only returns `true` or an error. Other methods may have +more specific return types. The `getFile` endpoint returns a `File`. Almost +every other method returns a `Message`, which you can use `Send` to obtain. + +There's lower level methods such as `MakeRequest` which require an endpoint and +parameters instead of accepting configs. These are primarily used internally. +If you find yourself having to use them, please open an issue.
@@ -0,0 +1,4 @@
+# Internals + +If you want to contribute to the project, here's some more information about +the internal structure of the library.
@@ -0,0 +1,197 @@
+# Adding Endpoints + +This is mostly useful if you've managed to catch a new Telegram Bot API update +before the library can get updated. It's also a great source of information +about how the types work internally. + +## Creating the Config + +The first step in adding a new endpoint is to create a new Config type for it. +These belong in `configs.go`. + +Let's try and add the `deleteMessage` endpoint. We can see it requires two +fields; `chat_id` and `message_id`. We can create a struct for these. + +```go +type DeleteMessageConfig struct { + ChatID ??? + MessageID int +} +``` + +What type should `ChatID` be? Telegram allows specifying numeric chat IDs or +channel usernames. Golang doesn't have union types, and interfaces are entirely +untyped. This library solves this by adding two fields, a `ChatID` and a +`ChannelUsername`. We can now write the struct as follows. + +```go +type DeleteMessageConfig struct { + ChannelUsername string + ChatID int64 + MessageID int +} +``` + +Note that `ChatID` is an `int64`. Telegram chat IDs can be greater than 32 bits. + +Okay, we now have our struct. But we can't send it yet. It doesn't implement +`Chattable` so it won't work with `Request` or `Send`. + +### Making it `Chattable` + +We can see that `Chattable` only requires a few methods. + +```go +type Chattable interface { + params() (Params, error) + method() string +} +``` + +`params` is the fields associated with the request. `method` is the endpoint +that this Config is associated with. + +Implementing the `method` is easy, so let's start with that. + +```go +func (config DeleteMessageConfig) method() string { + return "deleteMessage" +} +``` + +Now we have to add the `params`. The `Params` type is an alias for +`map[string]string`. Telegram expects only a single field for `chat_id`, so we +have to determine what data to send. + +We could use an if statement to determine which field to get the value from. +However, as this is a relatively common operation, there's helper methods for +`Params`. We can use the `AddFirstValid` method to go through each possible +value and stop when it discovers a valid one. Before writing your own Config, +it's worth taking a look through `params.go` to see what other helpers exist. + +Now we can take a look at what a completed `params` method looks like. + +```go +func (config DeleteMessageConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + params.AddNonZero("message_id", config.MessageID) + + return params, nil +} +``` + +### Uploading Files + +Let's imagine that for some reason deleting a message requires a document to be +uploaded and an optional thumbnail for that document. To add file upload +support we need to implement `Fileable`. This only requires one additional +method. + +```go +type Fileable interface { + Chattable + files() []RequestFile +} +``` + +First, let's add some fields to store our files in. Most of the standard Configs +have similar fields for their files. + +```diff + type DeleteMessageConfig struct { + ChannelUsername string + ChatID int64 + MessageID int ++ Delete RequestFileData ++ Thumb RequestFileData + } +``` + +Adding another method is pretty simple. We'll always add a file named `delete` +and add the `thumb` file if we have one. + +```go +func (config DeleteMessageConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "delete", + Data: config.Delete, + }} + + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + Data: config.Thumb, + }) + } + + return files +} +``` + +And now our files will upload! It will transparently handle uploads whether File +is a `FilePath`, `FileURL`, `FileBytes`, `FileReader`, or `FileID`. + +### Base Configs + +Certain Configs have repeated elements. For example, many of the items sent to a +chat have `ChatID` or `ChannelUsername` fields, along with `ReplyToMessageID`, +`ReplyMarkup`, and `DisableNotification`. Instead of implementing all of this +code for each item, there's a `BaseChat` that handles it for your Config. +Simply embed it in your struct to get all of those fields. + +There's only a few fields required for the `MessageConfig` struct after +embedding the `BaseChat` struct. + +```go +type MessageConfig struct { + BaseChat + Text string + ParseMode string + DisableWebPagePreview bool +} +``` + +It also inherits the `params` method from `BaseChat`. This allows you to call +it, then you only have to add your new fields. + +```go +func (config MessageConfig) params() (Params, error) { + params, err := config.BaseChat.params() + if err != nil { + return params, err + } + + params.AddNonEmpty("text", config.Text) + // Add your other fields + + return params, nil +} +``` + +Similarly, there's a `BaseFile` struct for adding an associated file and +`BaseEdit` struct for editing messages. + +## Making it Friendly + +After we've got a Config type, we'll want to make it more user-friendly. We can +do this by adding a new helper to `helpers.go`. These are functions that take +in the required data for the request to succeed and populate a Config. + +Telegram only requires two fields to call `deleteMessage`, so this will be fast. + +```go +func NewDeleteMessage(chatID int64, messageID int) DeleteMessageConfig { + return DeleteMessageConfig{ + ChatID: chatID, + MessageID: messageID, + } +} +``` + +Sometimes it makes sense to add more helpers if there's methods where you have +to set exactly one field. You can also add helpers that accept a `username` +string for channels if it's a common operation. + +And that's it! You've added a new method.
@@ -0,0 +1,87 @@
+# Uploading Files + +To make files work as expected, there's a lot going on behind the scenes. Make +sure to read through the [Files](../getting-started/files.md) section in +Getting Started first as we'll be building on that information. + +This section only talks about file uploading. For non-uploaded files such as +URLs and file IDs, you just need to pass a string. + +## Fields + +Let's start by talking about how the library represents files as part of a +Config. + +### Static Fields + +Most endpoints use static file fields. For example, `sendPhoto` expects a single +file named `photo`. All we have to do is set that single field with the correct +value (either a string or multipart file). Methods like `sendDocument` take two +file uploads, a `document` and a `thumb`. These are pretty straightforward. + +Remembering that the `Fileable` interface only requires one method, let's +implement it for `DocumentConfig`. + +```go +func (config DocumentConfig) files() []RequestFile { + // We can have multiple files, so we'll create an array. We also know that + // there always is a document file, so initialize the array with that. + files := []RequestFile{{ + Name: "document", + Data: config.File, + }} + + // We'll only add a file if we have one. + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + Data: config.Thumb, + }) + } + + return files +} +``` + +Telegram also supports the `attach://` syntax (discussed more later) for +thumbnails, but there's no reason to make things more complicated. + +### Dynamic Fields + +Of course, not everything can be so simple. Methods like `sendMediaGroup` +can accept many files, and each file can have custom markup. Using a static +field isn't possible because we need to specify which field is attached to each +item. Telegram introduced the `attach://` syntax for this. + +Let's follow through creating a new media group with string and file uploads. + +First, we start by creating some `InputMediaPhoto`. + +```go +photo := tgbotapi.NewInputMediaPhoto(tgbotapi.FilePath("tests/image.jpg")) +url := tgbotapi.NewInputMediaPhoto(tgbotapi.FileURL("https://i.imgur.com/unQLJIb.jpg")) +``` + +This created a new `InputMediaPhoto` struct, with a type of `photo` and the +media interface that we specified. + +We'll now create our media group with the photo and URL. + +```go +mediaGroup := NewMediaGroup(ChatID, []interface{}{ + photo, + url, +}) +``` + +A `MediaGroupConfig` stores all of the media in an array of interfaces. We now +have all of the data we need to upload, but how do we figure out field names for +uploads? We didn't specify `attach://unique-file` anywhere. + +When the library goes to upload the files, it looks at the `params` and `files` +for the Config. The params are generated by transforming the file into a value +more suitable for uploading, file IDs and URLs are untouched but uploaded types +are all changed into `attach://file-%d`. When collecting a list of files to +upload, it names them the same way. This creates a nearly transparent way of +handling multiple files in the background without the user having to consider +what's going on.
@@ -18,30 +18,6 @@ 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{@@ -76,269 +52,177 @@ MessageID: messageID,
} } -// NewPhotoUpload creates a new photo uploader. +// NewCopyMessage creates a new copy message. +// +// chatID is where to send it, fromChatID is the source chat, +// and messageID is the ID of the original message. +func NewCopyMessage(chatID int64, fromChatID int64, messageID int) CopyMessageConfig { + return CopyMessageConfig{ + BaseChat: BaseChat{ChatID: chatID}, + FromChatID: fromChatID, + MessageID: messageID, + } +} + +// NewPhoto creates a new sendPhoto request. // // chatID is where to send it, file is a string path to the file, // FileReader, or FileBytes. // // Note that you must send animated GIFs as a document. -func NewPhotoUpload(chatID int64, file interface{}) PhotoConfig { +func NewPhoto(chatID int64, file RequestFileData) PhotoConfig { return PhotoConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } -// NewPhotoShare shares an existing photo. -// You may use this to reshare an existing photo without reuploading it. +// NewPhotoToChannel creates a new photo uploader to send a photo to a channel. // -// chatID is where to send it, fileID is the ID of the file -// already uploaded. -func NewPhotoShare(chatID int64, fileID string) PhotoConfig { +// Note that you must send animated GIFs as a document. +func NewPhotoToChannel(username string, file RequestFileData) PhotoConfig { return PhotoConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, + BaseChat: BaseChat{ + ChannelUsername: username, + }, + File: file, }, } } -// NewAudioUpload creates a new audio uploader. -// -// chatID is where to send it, file is a string path to the file, -// FileReader, or FileBytes. -func NewAudioUpload(chatID int64, file interface{}) AudioConfig { +// NewAudio creates a new sendAudio request. +func NewAudio(chatID int64, file RequestFileData) AudioConfig { return AudioConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } -// NewAudioShare shares an existing audio file. -// You may use this to reshare an existing audio file without -// reuploading it. -// -// chatID is where to send it, fileID is the ID of the audio -// already uploaded. -func NewAudioShare(chatID int64, fileID string) AudioConfig { - return AudioConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, - }, - } -} - -// NewDocumentUpload creates a new document uploader. -// -// chatID is where to send it, file is a string path to the file, -// FileReader, or FileBytes. -func NewDocumentUpload(chatID int64, file interface{}) DocumentConfig { - return DocumentConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, - }, - } -} - -// NewDocumentShare shares an existing document. -// You may use this to reshare an existing document without -// reuploading it. -// -// chatID is where to send it, fileID is the ID of the document -// already uploaded. -func NewDocumentShare(chatID int64, fileID string) DocumentConfig { +// NewDocument creates a new sendDocument request. +func NewDocument(chatID int64, file RequestFileData) DocumentConfig { return DocumentConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } -// NewStickerUpload creates a new sticker uploader. -// -// chatID is where to send it, file is a string path to the file, -// FileReader, or FileBytes. -func NewStickerUpload(chatID int64, file interface{}) StickerConfig { +// NewSticker creates a new sendSticker request. +func NewSticker(chatID int64, file RequestFileData) StickerConfig { return StickerConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } -// NewStickerShare shares an existing sticker. -// You may use this to reshare an existing sticker without -// reuploading it. -// -// chatID is where to send it, fileID is the ID of the sticker -// already uploaded. -func NewStickerShare(chatID int64, fileID string) StickerConfig { - return StickerConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, - }, - } -} - -// NewVideoUpload creates a new video uploader. -// -// chatID is where to send it, file is a string path to the file, -// FileReader, or FileBytes. -func NewVideoUpload(chatID int64, file interface{}) VideoConfig { +// NewVideo creates a new sendVideo request. +func NewVideo(chatID int64, file RequestFileData) VideoConfig { return VideoConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } -// NewVideoShare shares an existing video. -// You may use this to reshare an existing video without reuploading it. -// -// chatID is where to send it, fileID is the ID of the video -// already uploaded. -func NewVideoShare(chatID int64, fileID string) VideoConfig { - return VideoConfig{ +// NewAnimation creates a new sendAnimation request. +func NewAnimation(chatID int64, file RequestFileData) AnimationConfig { + return AnimationConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } -// NewAnimationUpload creates a new animation uploader. +// NewVideoNote creates a new sendVideoNote request. // // chatID is where to send it, file is a string path to the file, // FileReader, or FileBytes. -func NewAnimationUpload(chatID int64, file interface{}) AnimationConfig { - return AnimationConfig{ +func NewVideoNote(chatID int64, length int, file RequestFileData) VideoNoteConfig { + return VideoNoteConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, + Length: length, } } -// NewAnimationShare shares an existing animation. -// You may use this to reshare an existing animation without reuploading it. -// -// chatID is where to send it, fileID is the ID of the animation -// already uploaded. -func NewAnimationShare(chatID int64, fileID string) AnimationConfig { - return AnimationConfig{ +// NewVoice creates a new sendVoice request. +func NewVoice(chatID int64, file RequestFileData) VoiceConfig { + return VoiceConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } -// NewVideoNoteUpload creates a new video note uploader. -// -// chatID is where to send it, file is a string path to the file, -// FileReader, or FileBytes. -func NewVideoNoteUpload(chatID int64, length int, file interface{}) VideoNoteConfig { - return VideoNoteConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, - }, - Length: length, +// NewMediaGroup creates a new media group. Files should be an array of +// two to ten InputMediaPhoto or InputMediaVideo. +func NewMediaGroup(chatID int64, files []interface{}) MediaGroupConfig { + return MediaGroupConfig{ + ChatID: chatID, + Media: files, } } -// NewVideoNoteShare shares an existing video. -// You may use this to reshare an existing video without reuploading it. -// -// chatID is where to send it, fileID is the ID of the video -// already uploaded. -func NewVideoNoteShare(chatID int64, length int, fileID string) VideoNoteConfig { - return VideoNoteConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, +// NewInputMediaPhoto creates a new InputMediaPhoto. +func NewInputMediaPhoto(media RequestFileData) InputMediaPhoto { + return InputMediaPhoto{ + BaseInputMedia{ + Type: "photo", + Media: media, }, - Length: length, } } -// NewVoiceUpload creates a new voice uploader. -// -// chatID is where to send it, file is a string path to the file, -// FileReader, or FileBytes. -func NewVoiceUpload(chatID int64, file interface{}) VoiceConfig { - return VoiceConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, +// NewInputMediaVideo creates a new InputMediaVideo. +func NewInputMediaVideo(media RequestFileData) InputMediaVideo { + return InputMediaVideo{ + BaseInputMedia: BaseInputMedia{ + Type: "video", + Media: media, }, } } -// NewVoiceShare shares an existing voice. -// You may use this to reshare an existing voice without reuploading it. -// -// chatID is where to send it, fileID is the ID of the video -// already uploaded. -func NewVoiceShare(chatID int64, fileID string) VoiceConfig { - return VoiceConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, +// NewInputMediaAnimation creates a new InputMediaAnimation. +func NewInputMediaAnimation(media RequestFileData) InputMediaAnimation { + return InputMediaAnimation{ + BaseInputMedia: BaseInputMedia{ + Type: "animation", + Media: media, }, } } -// NewMediaGroup creates a new media group. Files should be an array of -// two to ten InputMediaPhoto or InputMediaVideo. -func NewMediaGroup(chatID int64, files []interface{}) MediaGroupConfig { - return MediaGroupConfig{ - BaseChat: BaseChat{ - ChatID: chatID, +// NewInputMediaAudio creates a new InputMediaAudio. +func NewInputMediaAudio(media RequestFileData) InputMediaAudio { + return InputMediaAudio{ + BaseInputMedia: BaseInputMedia{ + Type: "audio", + Media: media, }, - InputMedia: files, } } -// NewInputMediaPhoto creates a new InputMediaPhoto. -func NewInputMediaPhoto(media string) InputMediaPhoto { - return InputMediaPhoto{ - Type: "photo", - Media: media, - } -} - -// NewInputMediaVideo creates a new InputMediaVideo. -func NewInputMediaVideo(media string) InputMediaVideo { - return InputMediaVideo{ - Type: "video", - Media: media, +// NewInputMediaDocument creates a new InputMediaDocument. +func NewInputMediaDocument(media RequestFileData) InputMediaDocument { + return InputMediaDocument{ + BaseInputMedia: BaseInputMedia{ + Type: "document", + Media: media, + }, } }@@ -393,7 +277,7 @@
// NewUserProfilePhotos gets user profile photos. // // userID is the ID of the user you wish to get profile photos from. -func NewUserProfilePhotos(userID int) UserProfilePhotosConfig { +func NewUserProfilePhotos(userID int64) UserProfilePhotosConfig { return UserProfilePhotosConfig{ UserID: userID, Offset: 0,@@ -416,25 +300,33 @@
// NewWebhook creates a new webhook. // // link is the url parsable link you wish to get the updates. -func NewWebhook(link string) WebhookConfig { - u, _ := url.Parse(link) +func NewWebhook(link string) (WebhookConfig, error) { + u, err := url.Parse(link) + + if err != nil { + return WebhookConfig{}, err + } return WebhookConfig{ URL: u, - } + }, nil } // NewWebhookWithCert creates a new webhook with a certificate. // // link is the url you wish to get webhooks, // file contains a string to a file, FileReader, or FileBytes. -func NewWebhookWithCert(link string, file interface{}) WebhookConfig { - u, _ := url.Parse(link) +func NewWebhookWithCert(link string, file RequestFileData) (WebhookConfig, error) { + u, err := url.Parse(link) + + if err != nil { + return WebhookConfig{}, err + } return WebhookConfig{ URL: u, Certificate: file, - } + }, nil } // NewInlineQueryResultArticle creates a new inline query article.@@ -502,7 +394,7 @@ func NewInlineQueryResultCachedGIF(id, gifID string) InlineQueryResultCachedGIF {
return InlineQueryResultCachedGIF{ Type: "gif", ID: id, - GifID: gifID, + GIFID: gifID, } }@@ -516,11 +408,11 @@ }
} // NewInlineQueryResultCachedMPEG4GIF create a new inline query with cached MPEG4 GIF. -func NewInlineQueryResultCachedMPEG4GIF(id, MPEG4GifID string) InlineQueryResultCachedMpeg4Gif { - return InlineQueryResultCachedMpeg4Gif{ - Type: "mpeg4_gif", - ID: id, - MGifID: MPEG4GifID, +func NewInlineQueryResultCachedMPEG4GIF(id, MPEG4GIFID string) InlineQueryResultCachedMPEG4GIF { + return InlineQueryResultCachedMPEG4GIF{ + Type: "mpeg4_gif", + ID: id, + MPEG4FileID: MPEG4GIFID, } }@@ -710,17 +602,6 @@ },
} } -// NewHideKeyboard hides the keyboard, with the option for being selective -// or hiding for everyone. -func NewHideKeyboard(selective bool) ReplyKeyboardHide { - log.Println("NewHideKeyboard is deprecated, please use NewRemoveKeyboard") - - return ReplyKeyboardHide{ - HideKeyboard: true, - Selective: selective, - } -} - // NewRemoveKeyboard hides the keyboard, with the option for being selective // or hiding for everyone. func NewRemoveKeyboard(selective bool) ReplyKeyboardRemove {@@ -792,6 +673,15 @@ CallbackData: &data,
} } +// NewInlineKeyboardButtonLoginURL creates an inline keyboard button with text +// which goes to a LoginURL. +func NewInlineKeyboardButtonLoginURL(text string, loginURL LoginURL) InlineKeyboardButton { + return InlineKeyboardButton{ + Text: text, + LoginURL: &loginURL, + } +} + // NewInlineKeyboardButtonURL creates an inline keyboard button with text // which goes to a URL. func NewInlineKeyboardButtonURL(text, url string) InlineKeyboardButton {@@ -850,7 +740,7 @@ }
} // NewInvoice creates a new Invoice request to the user. -func NewInvoice(chatID int64, title, description, payload, providerToken, startParameter, currency string, prices *[]LabeledPrice) InvoiceConfig { +func NewInvoice(chatID int64, title, description, payload, providerToken, startParameter, currency string, prices []LabeledPrice) InvoiceConfig { return InvoiceConfig{ BaseChat: BaseChat{ChatID: chatID}, Title: title,@@ -862,33 +752,183 @@ Currency: currency,
Prices: prices} } -// NewSetChatPhotoUpload creates a new chat photo uploader. -// -// chatID is where to send it, file is a string path to the file, -// FileReader, or FileBytes. -// -// Note that you must send animated GIFs as a document. -func NewSetChatPhotoUpload(chatID int64, file interface{}) SetChatPhotoConfig { +// NewChatTitle allows you to update the title of a chat. +func NewChatTitle(chatID int64, title string) SetChatTitleConfig { + return SetChatTitleConfig{ + ChatID: chatID, + Title: title, + } +} + +// NewChatDescription allows you to update the description of a chat. +func NewChatDescription(chatID int64, description string) SetChatDescriptionConfig { + return SetChatDescriptionConfig{ + ChatID: chatID, + Description: description, + } +} + +// NewChatPhoto allows you to update the photo for a chat. +func NewChatPhoto(chatID int64, photo RequestFileData) SetChatPhotoConfig { return SetChatPhotoConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, + BaseChat: BaseChat{ + ChatID: chatID, + }, + File: photo, + }, + } +} + +// NewDeleteChatPhoto allows you to delete the photo for a chat. +func NewDeleteChatPhoto(chatID int64) DeleteChatPhotoConfig { + return DeleteChatPhotoConfig{ + ChatID: chatID, + } +} + +// NewPoll allows you to create a new poll. +func NewPoll(chatID int64, question string, options ...string) SendPollConfig { + return SendPollConfig{ + BaseChat: BaseChat{ + ChatID: chatID, + }, + Question: question, + Options: options, + IsAnonymous: true, // This is Telegram's default. + } +} + +// NewStopPoll allows you to stop a poll. +func NewStopPoll(chatID int64, messageID int) StopPollConfig { + return StopPollConfig{ + BaseEdit{ + ChatID: chatID, + MessageID: messageID, }, } } -// NewSetChatPhotoShare shares an existing photo. -// You may use this to reshare an existing photo without reuploading it. +// NewSendDice allows you to send a random dice roll. // -// chatID is where to send it, fileID is the ID of the file -// already uploaded. -func NewSetChatPhotoShare(chatID int64, fileID string) SetChatPhotoConfig { - return SetChatPhotoConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, +// Deprecated: Use NewDice instead. +func NewSendDice(chatID int64) DiceConfig { + return NewDice(chatID) +} + +// NewDice allows you to send a random dice roll. +func NewDice(chatID int64) DiceConfig { + return DiceConfig{ + BaseChat: BaseChat{ + ChatID: chatID, }, } } + +// NewDiceWithEmoji allows you to send a random roll of one of many types. +// +// Emoji may be 🎲 (1-6), 🎯 (1-6), or 🏀 (1-5). +func NewDiceWithEmoji(chatID int64, emoji string) DiceConfig { + return DiceConfig{ + BaseChat: BaseChat{ + ChatID: chatID, + }, + Emoji: emoji, + } +} + +// NewBotCommandScopeDefault represents the default scope of bot commands. +func NewBotCommandScopeDefault() BotCommandScope { + return BotCommandScope{Type: "default"} +} + +// NewBotCommandScopeAllPrivateChats represents the scope of bot commands, +// covering all private chats. +func NewBotCommandScopeAllPrivateChats() BotCommandScope { + return BotCommandScope{Type: "all_private_chats"} +} + +// NewBotCommandScopeAllGroupChats represents the scope of bot commands, +// covering all group and supergroup chats. +func NewBotCommandScopeAllGroupChats() BotCommandScope { + return BotCommandScope{Type: "all_group_chats"} +} + +// NewBotCommandScopeAllChatAdministrators represents the scope of bot commands, +// covering all group and supergroup chat administrators. +func NewBotCommandScopeAllChatAdministrators() BotCommandScope { + return BotCommandScope{Type: "all_chat_administrators"} +} + +// NewBotCommandScopeChat represents the scope of bot commands, covering a +// specific chat. +func NewBotCommandScopeChat(chatID int64) BotCommandScope { + return BotCommandScope{ + Type: "chat", + ChatID: chatID, + } +} + +// NewBotCommandScopeChatAdministrators represents the scope of bot commands, +// covering all administrators of a specific group or supergroup chat. +func NewBotCommandScopeChatAdministrators(chatID int64) BotCommandScope { + return BotCommandScope{ + Type: "chat_administrators", + ChatID: chatID, + } +} + +// NewBotCommandScopeChatMember represents the scope of bot commands, covering a +// specific member of a group or supergroup chat. +func NewBotCommandScopeChatMember(chatID, userID int64) BotCommandScope { + return BotCommandScope{ + Type: "chat_member", + ChatID: chatID, + UserID: userID, + } +} + +// NewGetMyCommandsWithScope allows you to set the registered commands for a +// given scope. +func NewGetMyCommandsWithScope(scope BotCommandScope) GetMyCommandsConfig { + return GetMyCommandsConfig{Scope: &scope} +} + +// NewGetMyCommandsWithScopeAndLanguage allows you to set the registered +// commands for a given scope and language code. +func NewGetMyCommandsWithScopeAndLanguage(scope BotCommandScope, languageCode string) GetMyCommandsConfig { + return GetMyCommandsConfig{Scope: &scope, LanguageCode: languageCode} +} + +// NewSetMyCommands allows you to set the registered commands. +func NewSetMyCommands(commands ...BotCommand) SetMyCommandsConfig { + return SetMyCommandsConfig{Commands: commands} +} + +// NewSetMyCommands allows you to set the registered commands for a given scope. +func NewSetMyCommandsWithScope(scope BotCommandScope, commands ...BotCommand) SetMyCommandsConfig { + return SetMyCommandsConfig{Commands: commands, Scope: &scope} +} + +// NewSetMyCommands allows you to set the registered commands for a given scope +// and language code. +func NewSetMyCommandsWithScopeAndLanguage(scope BotCommandScope, languageCode string, commands ...BotCommand) SetMyCommandsConfig { + return SetMyCommandsConfig{Commands: commands, Scope: &scope, LanguageCode: languageCode} +} + +// NewDeleteMyCommands allows you to delete the registered commands. +func NewDeleteMyCommands() DeleteMyCommandsConfig { + return DeleteMyCommandsConfig{} +} + +// NewDeleteMyCommands allows you to delete the registered commands for a given +// scope. +func NewDeleteMyCommandsWithScope(scope BotCommandScope) DeleteMyCommandsConfig { + return DeleteMyCommandsConfig{Scope: &scope} +} + +// NewDeleteMyCommands allows you to delete the registered commands for a given +// scope and language code. +func NewDeleteMyCommandsWithScopeAndLanguage(scope BotCommandScope, languageCode string) DeleteMyCommandsConfig { + return DeleteMyCommandsConfig{Scope: &scope, LanguageCode: languageCode} +}
@@ -1,48 +1,71 @@
-package tgbotapi_test +package tgbotapi import ( "testing" +) - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" -) +func TestNewWebhook(t *testing.T) { + result, err := NewWebhook("https://example.com/token") + + if err != nil || + result.URL.String() != "https://example.com/token" || + result.Certificate != interface{}(nil) || + result.MaxConnections != 0 || + len(result.AllowedUpdates) != 0 { + t.Fail() + } +} + +func TestNewWebhookWithCert(t *testing.T) { + exampleFile := FileID("123") + result, err := NewWebhookWithCert("https://example.com/token", exampleFile) + + if err != nil || + result.URL.String() != "https://example.com/token" || + result.Certificate != exampleFile || + result.MaxConnections != 0 || + len(result.AllowedUpdates) != 0 { + t.Fail() + } +} func TestNewInlineQueryResultArticle(t *testing.T) { - result := tgbotapi.NewInlineQueryResultArticle("id", "title", "message") + result := NewInlineQueryResultArticle("id", "title", "message") if result.Type != "article" || result.ID != "id" || result.Title != "title" || - result.InputMessageContent.(tgbotapi.InputTextMessageContent).Text != "message" { + result.InputMessageContent.(InputTextMessageContent).Text != "message" { t.Fail() } } func TestNewInlineQueryResultArticleMarkdown(t *testing.T) { - result := tgbotapi.NewInlineQueryResultArticleMarkdown("id", "title", "*message*") + result := NewInlineQueryResultArticleMarkdown("id", "title", "*message*") if result.Type != "article" || result.ID != "id" || result.Title != "title" || - result.InputMessageContent.(tgbotapi.InputTextMessageContent).Text != "*message*" || - result.InputMessageContent.(tgbotapi.InputTextMessageContent).ParseMode != "Markdown" { + result.InputMessageContent.(InputTextMessageContent).Text != "*message*" || + result.InputMessageContent.(InputTextMessageContent).ParseMode != "Markdown" { t.Fail() } } func TestNewInlineQueryResultArticleHTML(t *testing.T) { - result := tgbotapi.NewInlineQueryResultArticleHTML("id", "title", "<b>message</b>") + result := NewInlineQueryResultArticleHTML("id", "title", "<b>message</b>") if result.Type != "article" || result.ID != "id" || result.Title != "title" || - result.InputMessageContent.(tgbotapi.InputTextMessageContent).Text != "<b>message</b>" || - result.InputMessageContent.(tgbotapi.InputTextMessageContent).ParseMode != "HTML" { + result.InputMessageContent.(InputTextMessageContent).Text != "<b>message</b>" || + result.InputMessageContent.(InputTextMessageContent).ParseMode != "HTML" { t.Fail() } } func TestNewInlineQueryResultGIF(t *testing.T) { - result := tgbotapi.NewInlineQueryResultGIF("id", "google.com") + result := NewInlineQueryResultGIF("id", "google.com") if result.Type != "gif" || result.ID != "id" ||@@ -52,7 +75,7 @@ }
} func TestNewInlineQueryResultMPEG4GIF(t *testing.T) { - result := tgbotapi.NewInlineQueryResultMPEG4GIF("id", "google.com") + result := NewInlineQueryResultMPEG4GIF("id", "google.com") if result.Type != "mpeg4_gif" || result.ID != "id" ||@@ -62,7 +85,7 @@ }
} func TestNewInlineQueryResultPhoto(t *testing.T) { - result := tgbotapi.NewInlineQueryResultPhoto("id", "google.com") + result := NewInlineQueryResultPhoto("id", "google.com") if result.Type != "photo" || result.ID != "id" ||@@ -72,7 +95,7 @@ }
} func TestNewInlineQueryResultPhotoWithThumb(t *testing.T) { - result := tgbotapi.NewInlineQueryResultPhotoWithThumb("id", "google.com", "thumb.com") + result := NewInlineQueryResultPhotoWithThumb("id", "google.com", "thumb.com") if result.Type != "photo" || result.ID != "id" ||@@ -83,7 +106,7 @@ }
} func TestNewInlineQueryResultVideo(t *testing.T) { - result := tgbotapi.NewInlineQueryResultVideo("id", "google.com") + result := NewInlineQueryResultVideo("id", "google.com") if result.Type != "video" || result.ID != "id" ||@@ -93,7 +116,7 @@ }
} func TestNewInlineQueryResultAudio(t *testing.T) { - result := tgbotapi.NewInlineQueryResultAudio("id", "google.com", "title") + result := NewInlineQueryResultAudio("id", "google.com", "title") if result.Type != "audio" || result.ID != "id" ||@@ -104,7 +127,7 @@ }
} func TestNewInlineQueryResultVoice(t *testing.T) { - result := tgbotapi.NewInlineQueryResultVoice("id", "google.com", "title") + result := NewInlineQueryResultVoice("id", "google.com", "title") if result.Type != "voice" || result.ID != "id" ||@@ -115,7 +138,7 @@ }
} func TestNewInlineQueryResultDocument(t *testing.T) { - result := tgbotapi.NewInlineQueryResultDocument("id", "google.com", "title", "mime/type") + result := NewInlineQueryResultDocument("id", "google.com", "title", "mime/type") if result.Type != "document" || result.ID != "id" ||@@ -127,7 +150,7 @@ }
} func TestNewInlineQueryResultLocation(t *testing.T) { - result := tgbotapi.NewInlineQueryResultLocation("id", "name", 40, 50) + result := NewInlineQueryResultLocation("id", "name", 40, 50) if result.Type != "location" || result.ID != "id" ||@@ -138,8 +161,25 @@ t.Fail()
} } +func TestNewInlineKeyboardButtonLoginURL(t *testing.T) { + result := NewInlineKeyboardButtonLoginURL("text", LoginURL{ + URL: "url", + ForwardText: "ForwardText", + BotUsername: "username", + RequestWriteAccess: false, + }) + + if result.Text != "text" || + result.LoginURL.URL != "url" || + result.LoginURL.ForwardText != "ForwardText" || + result.LoginURL.BotUsername != "username" || + result.LoginURL.RequestWriteAccess != false { + t.Fail() + } +} + func TestNewEditMessageText(t *testing.T) { - edit := tgbotapi.NewEditMessageText(ChatID, ReplyToMessageID, "new text") + edit := NewEditMessageText(ChatID, ReplyToMessageID, "new text") if edit.Text != "new text" || edit.BaseEdit.ChatID != ChatID ||@@ -149,7 +189,7 @@ }
} func TestNewEditMessageCaption(t *testing.T) { - edit := tgbotapi.NewEditMessageCaption(ChatID, ReplyToMessageID, "new caption") + edit := NewEditMessageCaption(ChatID, ReplyToMessageID, "new caption") if edit.Caption != "new caption" || edit.BaseEdit.ChatID != ChatID ||@@ -159,15 +199,15 @@ }
} func TestNewEditMessageReplyMarkup(t *testing.T) { - markup := tgbotapi.InlineKeyboardMarkup{ - InlineKeyboard: [][]tgbotapi.InlineKeyboardButton{ - []tgbotapi.InlineKeyboardButton{ - tgbotapi.InlineKeyboardButton{Text: "test"}, + markup := InlineKeyboardMarkup{ + InlineKeyboard: [][]InlineKeyboardButton{ + { + {Text: "test"}, }, }, } - edit := tgbotapi.NewEditMessageReplyMarkup(ChatID, ReplyToMessageID, markup) + edit := NewEditMessageReplyMarkup(ChatID, ReplyToMessageID, markup) if edit.ReplyMarkup.InlineKeyboard[0][0].Text != "test" || edit.BaseEdit.ChatID != ChatID ||@@ -178,7 +218,7 @@
} func TestNewDice(t *testing.T) { - dice := tgbotapi.NewDice(42) + dice := NewDice(42) if dice.ChatID != 42 || dice.Emoji != "" {@@ -187,7 +227,7 @@ }
} func TestNewDiceWithEmoji(t *testing.T) { - dice := tgbotapi.NewDiceWithEmoji(42, "🏀") + dice := NewDiceWithEmoji(42, "🏀") if dice.ChatID != 42 || dice.Emoji != "🏀" {
@@ -2,31 +2,12 @@ package tgbotapi
import ( "encoding/json" - "net/url" "reflect" "strconv" ) // Params represents a set of parameters that gets passed to a request. type Params map[string]string - -func newParams(values url.Values) Params { - params := Params{} - for k, v := range values { - if len(v) > 0 { - params[k] = v[0] - } - } - return params -} - -func (p Params) toValues() url.Values { - values := url.Values{} - for k, v := range p { - values[k] = []string{v} - } - return values -} // AddNonEmpty adds a value if it not an empty string. func (p Params) AddNonEmpty(key, value string) {
@@ -0,0 +1,93 @@
+package tgbotapi + +import ( + "testing" +) + +func assertLen(t *testing.T, params Params, l int) { + actual := len(params) + if actual != l { + t.Fatalf("Incorrect number of params, expected %d but found %d\n", l, actual) + } +} + +func assertEq(t *testing.T, a interface{}, b interface{}) { + if a != b { + t.Fatalf("Values did not match, a: %v, b: %v\n", a, b) + } +} + +func TestAddNonEmpty(t *testing.T) { + params := make(Params) + params.AddNonEmpty("value", "value") + assertLen(t, params, 1) + assertEq(t, params["value"], "value") + params.AddNonEmpty("test", "") + assertLen(t, params, 1) + assertEq(t, params["test"], "") +} + +func TestAddNonZero(t *testing.T) { + params := make(Params) + params.AddNonZero("value", 1) + assertLen(t, params, 1) + assertEq(t, params["value"], "1") + params.AddNonZero("test", 0) + assertLen(t, params, 1) + assertEq(t, params["test"], "") +} + +func TestAddNonZero64(t *testing.T) { + params := make(Params) + params.AddNonZero64("value", 1) + assertLen(t, params, 1) + assertEq(t, params["value"], "1") + params.AddNonZero64("test", 0) + assertLen(t, params, 1) + assertEq(t, params["test"], "") +} + +func TestAddBool(t *testing.T) { + params := make(Params) + params.AddBool("value", true) + assertLen(t, params, 1) + assertEq(t, params["value"], "true") + params.AddBool("test", false) + assertLen(t, params, 1) + assertEq(t, params["test"], "") +} + +func TestAddNonZeroFloat(t *testing.T) { + params := make(Params) + params.AddNonZeroFloat("value", 1) + assertLen(t, params, 1) + assertEq(t, params["value"], "1.000000") + params.AddNonZeroFloat("test", 0) + assertLen(t, params, 1) + assertEq(t, params["test"], "") +} + +func TestAddInterface(t *testing.T) { + params := make(Params) + data := struct { + Name string `json:"name"` + }{ + Name: "test", + } + params.AddInterface("value", data) + assertLen(t, params, 1) + assertEq(t, params["value"], `{"name":"test"}`) + params.AddInterface("test", nil) + assertLen(t, params, 1) + assertEq(t, params["test"], "") +} + +func TestAddFirstValid(t *testing.T) { + params := make(Params) + params.AddFirstValid("value", 0, "", "test") + assertLen(t, params, 1) + assertEq(t, params["value"], "test") + params.AddFirstValid("value2", 3, "test") + assertLen(t, params, 2) + assertEq(t, params["value2"], "3") +}
@@ -61,6 +61,8 @@ PassportFile struct {
// Unique identifier for this file FileID string `json:"file_id"` + FileUniqueID string `json:"file_unique_id"` + // File size FileSize int `json:"file_size"`
@@ -13,22 +13,29 @@ // 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"` - Parameters *ResponseParameters `json:"parameters"` + Result json.RawMessage `json:"result,omitempty"` + ErrorCode int `json:"error_code,omitempty"` + Description string `json:"description,omitempty"` + Parameters *ResponseParameters `json:"parameters,omitempty"` } -// ResponseParameters are various errors that can be returned in APIResponse. -type ResponseParameters struct { - MigrateToChatID int64 `json:"migrate_to_chat_id"` // optional - RetryAfter int `json:"retry_after"` // optional +// Error is an error containing extra information returned by the Telegram API. +type Error struct { + Code int + Message string + ResponseParameters +} + +// Error message string. +func (e Error) Error() string { + return e.Message } // Update is an update response, from GetUpdates. type Update struct { // UpdateID is the update's unique identifier. - // Update identifiers start from a certain positive number and increase sequentially. + // Update identifiers start from a certain positive number and increase + // sequentially. // This ID becomes especially handy if you're using Webhooks, // since it allows you to ignore repeated updates or to restore // the correct update sequence, should they get out of order.@@ -38,42 +45,124 @@ UpdateID int `json:"update_id"`
// Message new incoming message of any kind — text, photo, sticker, etc. // // optional - Message *Message `json:"message"` - // EditedMessage + Message *Message `json:"message,omitempty"` + // EditedMessage new version of a message that is known to the bot and was + // edited // // optional - EditedMessage *Message `json:"edited_message"` - // ChannelPost new version of a message that is known to the bot and was edited + EditedMessage *Message `json:"edited_message,omitempty"` + // ChannelPost new version of a message that is known to the bot and was + // edited // // optional - ChannelPost *Message `json:"channel_post"` - // EditedChannelPost new incoming channel post of any kind — text, photo, sticker, etc. + ChannelPost *Message `json:"channel_post,omitempty"` + // EditedChannelPost new incoming channel post of any kind — text, photo, + // sticker, etc. // // optional - EditedChannelPost *Message `json:"edited_channel_post"` + EditedChannelPost *Message `json:"edited_channel_post,omitempty"` // InlineQuery new incoming inline query // // optional - InlineQuery *InlineQuery `json:"inline_query"` + InlineQuery *InlineQuery `json:"inline_query,omitempty"` // ChosenInlineResult is the result of an inline query // that was chosen by a user and sent to their chat partner. // Please see our documentation on the feedback collecting // for details on how to enable these updates for your bot. // // optional - ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result"` + ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result,omitempty"` // CallbackQuery new incoming callback query // // optional - CallbackQuery *CallbackQuery `json:"callback_query"` - // ShippingQuery new incoming shipping query. Only for invoices with flexible price + CallbackQuery *CallbackQuery `json:"callback_query,omitempty"` + // ShippingQuery new incoming shipping query. Only for invoices with + // flexible price + // + // optional + ShippingQuery *ShippingQuery `json:"shipping_query,omitempty"` + // PreCheckoutQuery new incoming pre-checkout query. Contains full + // information about checkout + // + // optional + PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query,omitempty"` + // Pool new poll state. Bots receive only updates about stopped polls and + // polls, which are sent by the bot + // + // optional + Poll *Poll `json:"poll,omitempty"` + // PollAnswer user changed their answer in a non-anonymous poll. Bots + // receive new votes only in polls that were sent by the bot itself. + // + // optional + PollAnswer *PollAnswer `json:"poll_answer,omitempty"` + // MyChatMember is the bot's chat member status was updated in a chat. For + // private chats, this update is received only when the bot is blocked or + // unblocked by the user. + // + // optional + MyChatMember *ChatMemberUpdated `json:"my_chat_member"` + // ChatMember is a chat member's status was updated in a chat. The bot must + // be an administrator in the chat and must explicitly specify "chat_member" + // in the list of allowed_updates to receive these updates. // // optional - ShippingQuery *ShippingQuery `json:"shipping_query"` - // PreCheckoutQuery new incoming pre-checkout query. Contains full information about checkout + ChatMember *ChatMemberUpdated `json:"chat_member"` + // ChatJoinRequest is a request to join the chat has been sent. The bot must + // have the can_invite_users administrator right in the chat to receive + // these updates. // // optional - PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query"` + ChatJoinRequest *ChatJoinRequest `json:"chat_join_request"` +} + +// SentFrom returns the user who sent an update. Can be nil, if Telegram did not provide information +// about the user in the update object. +func (u *Update) SentFrom() *User { + switch { + case u.Message != nil: + return u.Message.From + case u.EditedMessage != nil: + return u.EditedMessage.From + case u.InlineQuery != nil: + return u.InlineQuery.From + case u.ChosenInlineResult != nil: + return u.ChosenInlineResult.From + case u.CallbackQuery != nil: + return u.CallbackQuery.From + case u.ShippingQuery != nil: + return u.ShippingQuery.From + case u.PreCheckoutQuery != nil: + return u.PreCheckoutQuery.From + default: + return nil + } +} + +// CallbackData returns the callback query data, if it exists. +func (u *Update) CallbackData() string { + if u.CallbackQuery != nil { + return u.CallbackQuery.Data + } + return "" +} + +// FromChat returns the chat where an update occured. +func (u *Update) FromChat() *Chat { + switch { + case u.Message != nil: + return u.Message.Chat + case u.EditedMessage != nil: + return u.EditedMessage.Chat + case u.ChannelPost != nil: + return u.ChannelPost.Chat + case u.EditedChannelPost != nil: + return u.EditedChannelPost.Chat + case u.CallbackQuery != nil: + return u.CallbackQuery.Message.Chat + default: + return nil + } } // UpdatesChannel is the channel for getting updates.@@ -89,26 +178,41 @@
// User represents a Telegram user or bot. type User struct { // ID is a unique identifier for this user or bot - ID int `json:"id"` + ID int64 `json:"id"` + // IsBot true, if this user is a bot + // + // optional + IsBot bool `json:"is_bot,omitempty"` // FirstName user's or bot's first name FirstName string `json:"first_name"` // LastName user's or bot's last name // // optional - LastName string `json:"last_name"` + LastName string `json:"last_name,omitempty"` // UserName user's or bot's username // // optional - UserName string `json:"username"` + UserName string `json:"username,omitempty"` // LanguageCode IETF language tag of the user's language // more info: https://en.wikipedia.org/wiki/IETF_language_tag // // optional - LanguageCode string `json:"language_code"` - // IsBot true, if this user is a bot + LanguageCode string `json:"language_code,omitempty"` + // CanJoinGroups is true, if the bot can be invited to groups. + // Returned only in getMe. + // + // optional + CanJoinGroups bool `json:"can_join_groups,omitempty"` + // CanReadAllGroupMessages is true, if privacy mode is disabled for the bot. + // Returned only in getMe. + // + // optional + CanReadAllGroupMessages bool `json:"can_read_all_group_messages,omitempty"` + // SupportsInlineQueries is true, if the bot supports inline queries. + // Returned only in getMe. // // optional - IsBot bool `json:"is_bot"` + SupportsInlineQueries bool `json:"supports_inline_queries,omitempty"` } // String displays a simple text version of a user.@@ -131,25 +235,7 @@
return name } -// GroupChat is a group chat. -type GroupChat struct { - ID int `json:"id"` - Title string `json:"title"` -} - -// ChatPhoto represents a chat photo. -type ChatPhoto struct { - // SmallFileID is a file identifier of small (160x160) chat photo. - // This file_id can be used only for photo download and - // only for as long as the photo is not changed. - SmallFileID string `json:"small_file_id"` - // BigFileID is a file identifier of big (640x640) chat photo. - // This file_id can be used only for photo download and - // only for as long as the photo is not changed. - BigFileID string `json:"big_file_id"` -} - -// Chat contains information about the place a message was sent. +// Chat represents a chat. type Chat struct { // ID is a unique identifier for this chat ID int64 `json:"id"`@@ -158,25 +244,26 @@ Type string `json:"type"`
// Title for supergroups, channels and group chats // // optional - Title string `json:"title"` + Title string `json:"title,omitempty"` // UserName for private chats, supergroups and channels if available // // optional - UserName string `json:"username"` + UserName string `json:"username,omitempty"` // FirstName of the other party in a private chat // // optional - FirstName string `json:"first_name"` + FirstName string `json:"first_name,omitempty"` // LastName of the other party in a private chat // // optional - LastName string `json:"last_name"` - // AllMembersAreAdmins - // - // optional - AllMembersAreAdmins bool `json:"all_members_are_administrators"` + LastName string `json:"last_name,omitempty"` // Photo is a chat photo Photo *ChatPhoto `json:"photo"` + // Bio is the bio of the other party in a private chat. Returned only in + // getChat + // + // optional + Bio string `json:"bio,omitempty"` // Description for groups, supergroups and channel chats // // optional@@ -187,10 +274,42 @@ // so the bot must first generate the link using exportChatInviteLink
// // optional InviteLink string `json:"invite_link,omitempty"` - // PinnedMessage Pinned message, for groups, supergroups and channels + // PinnedMessage is the pinned message, for groups, supergroups and channels + // + // optional + PinnedMessage *Message `json:"pinned_message,omitempty"` + // Permissions is default chat member permissions, for groups and + // supergroups. Returned only in getChat. + // + // optional + Permissions *ChatPermissions `json:"permissions,omitempty"` + // SlowModeDelay is for supergroups, the minimum allowed delay between + // consecutive messages sent by each unpriviledged user. Returned only in + // getChat. + // + // optional + SlowModeDelay int `json:"slow_mode_delay,omitempty"` + // StickerSetName is for supergroups, name of group sticker set.Returned + // only in getChat. + // + // optional + StickerSetName string `json:"sticker_set_name,omitempty"` + // CanSetStickerSet is true, if the bot can change the group sticker set. + // Returned only in getChat. + // + // optional + CanSetStickerSet bool `json:"can_set_sticker_set,omitempty"` + // LinkedChatID is a unique identifier for the linked chat, i.e. the + // discussion group identifier for a channel and vice versa; for supergroups + // and channel chats. + // + // optional + LinkedChatID int64 `json:"linked_chat_id,omitempty"` + // Location is for supergroups, the location to which the supergroup is + // connected. Returned only in getChat. // // optional - PinnedMessage *Message `json:"pinned_message"` + Location *ChatLocation `json:"location"` } // IsPrivate returns if the Chat is a private conversation.@@ -218,15 +337,21 @@ func (c Chat) ChatConfig() ChatConfig {
return ChatConfig{ChatID: c.ID} } -// Message is returned by almost every request, and contains data about -// almost anything. +// Message represents a message. type Message struct { // MessageID is a unique message identifier inside this chat MessageID int `json:"message_id"` // From is a sender, empty for messages sent to channels; // // optional - From *User `json:"from"` + From *User `json:"from,omitempty"` + // SenderChat is the sender of the message, sent on behalf of a chat. The + // channel itself for channel messages. The supergroup itself for messages + // from anonymous group administrators. The linked channel for messages + // automatically forwarded to the discussion group + // + // optional + SenderChat *Chat `json:"sender_chat,omitempty"` // Date of the message was sent in Unix time Date int `json:"date"` // Chat is the conversation the message belongs to@@ -234,136 +359,155 @@ Chat *Chat `json:"chat"`
// ForwardFrom for forwarded messages, sender of the original message; // // optional - ForwardFrom *User `json:"forward_from"` + ForwardFrom *User `json:"forward_from,omitempty"` // ForwardFromChat for messages forwarded from channels, // information about the original channel; // // optional - ForwardFromChat *Chat `json:"forward_from_chat"` + ForwardFromChat *Chat `json:"forward_from_chat,omitempty"` // ForwardFromMessageID for messages forwarded from channels, // identifier of the original message in the channel; // // optional - ForwardFromMessageID int `json:"forward_from_message_id"` + ForwardFromMessageID int `json:"forward_from_message_id,omitempty"` + // ForwardSignature for messages forwarded from channels, signature of the + // post author if present + // + // optional + ForwardSignature string `json:"forward_signature,omitempty"` + // ForwardSenderName is the sender's name for messages forwarded from users + // who disallow adding a link to their account in forwarded messages + // + // optional + ForwardSenderName string `json:"forward_sender_name,omitempty"` // ForwardDate for forwarded messages, date the original message was sent in Unix time; // // optional - ForwardDate int `json:"forward_date"` + ForwardDate int `json:"forward_date,omitempty"` // ReplyToMessage for replies, the original message. // Note that the Message object in this field will not contain further ReplyToMessage fields // even if it itself is a reply; // // optional - ReplyToMessage *Message `json:"reply_to_message"` + ReplyToMessage *Message `json:"reply_to_message,omitempty"` // ViaBot through which the message was sent; // // optional - ViaBot *User `json:"via_bot"` + ViaBot *User `json:"via_bot,omitempty"` // EditDate of the message was last edited in Unix time; // // optional - EditDate int `json:"edit_date"` + EditDate int `json:"edit_date,omitempty"` // MediaGroupID is the unique identifier of a media message group this message belongs to; // // optional - MediaGroupID string `json:"media_group_id"` + MediaGroupID string `json:"media_group_id,omitempty"` // AuthorSignature is the signature of the post author for messages in channels; // // optional - AuthorSignature string `json:"author_signature"` + AuthorSignature string `json:"author_signature,omitempty"` // Text is for text messages, the actual UTF-8 text of the message, 0-4096 characters; // // optional - Text string `json:"text"` + Text string `json:"text,omitempty"` // Entities is for text messages, special entities like usernames, // URLs, bot commands, etc. that appear in the text; // // optional - Entities *[]MessageEntity `json:"entities"` - // CaptionEntities; + Entities []MessageEntity `json:"entities,omitempty"` + // Animation message is an animation, information about the animation. + // For backward compatibility, when this field is set, the document field will also be set; // // optional - CaptionEntities *[]MessageEntity `json:"caption_entities"` + Animation *Animation `json:"animation,omitempty"` // Audio message is an audio file, information about the file; // // optional - Audio *Audio `json:"audio"` + Audio *Audio `json:"audio,omitempty"` // Document message is a general file, information about the file; // // optional - Document *Document `json:"document"` - // Animation message is an animation, information about the animation. - // For backward compatibility, when this field is set, the document field will also be set; - // - // optional - Animation *ChatAnimation `json:"animation"` - // Game message is a game, information about the game; - // - // optional - Game *Game `json:"game"` + Document *Document `json:"document,omitempty"` // Photo message is a photo, available sizes of the photo; // // optional - Photo *[]PhotoSize `json:"photo"` + Photo []PhotoSize `json:"photo,omitempty"` // Sticker message is a sticker, information about the sticker; // // optional - Sticker *Sticker `json:"sticker"` + Sticker *Sticker `json:"sticker,omitempty"` // Video message is a video, information about the video; // // optional - Video *Video `json:"video"` + Video *Video `json:"video,omitempty"` // VideoNote message is a video note, information about the video message; // // optional - VideoNote *VideoNote `json:"video_note"` + VideoNote *VideoNote `json:"video_note,omitempty"` // Voice message is a voice message, information about the file; // // optional - Voice *Voice `json:"voice"` + Voice *Voice `json:"voice,omitempty"` // Caption for the animation, audio, document, photo, video or voice, 0-1024 characters; // // optional - Caption string `json:"caption"` + Caption string `json:"caption,omitempty"` + // CaptionEntities; + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` // Contact message is a shared contact, information about the contact; // // optional - Contact *Contact `json:"contact"` - // Location message is a shared location, information about the location; + Contact *Contact `json:"contact,omitempty"` + // Dice is a dice with random value; // // optional - Location *Location `json:"location"` + Dice *Dice `json:"dice,omitempty"` + // Game message is a game, information about the game; + // + // optional + Game *Game `json:"game,omitempty"` + // Poll is a native poll, information about the poll; + // + // optional + Poll *Poll `json:"poll,omitempty"` // Venue message is a venue, information about the venue. - // For backward compatibility, when this field is set, the location field will also be set; + // For backward compatibility, when this field is set, the location field + // will also be set; + // + // optional + Venue *Venue `json:"venue,omitempty"` + // Location message is a shared location, information about the location; // // optional - Venue *Venue `json:"venue"` + Location *Location `json:"location,omitempty"` // NewChatMembers that were added to the group or supergroup // and information about them (the bot itself may be one of these members); // // optional - NewChatMembers *[]User `json:"new_chat_members"` + NewChatMembers []User `json:"new_chat_members,omitempty"` // LeftChatMember is a member was removed from the group, // information about them (this member may be the bot itself); // // optional - LeftChatMember *User `json:"left_chat_member"` + LeftChatMember *User `json:"left_chat_member,omitempty"` // NewChatTitle is a chat title was changed to this value; // // optional - NewChatTitle string `json:"new_chat_title"` + NewChatTitle string `json:"new_chat_title,omitempty"` // NewChatPhoto is a chat photo was change to this value; // // optional - NewChatPhoto *[]PhotoSize `json:"new_chat_photo"` + NewChatPhoto []PhotoSize `json:"new_chat_photo,omitempty"` // DeleteChatPhoto is a service message: the chat photo was deleted; // // optional - DeleteChatPhoto bool `json:"delete_chat_photo"` + DeleteChatPhoto bool `json:"delete_chat_photo,omitempty"` // GroupChatCreated is a service message: the group has been created; // // optional - GroupChatCreated bool `json:"group_chat_created"` + GroupChatCreated bool `json:"group_chat_created,omitempty"` // SuperGroupChatCreated is a service message: the supergroup has been created. // This field can't be received in a message coming through updates, // because bot can't be a member of a supergroup when it is created.@@ -371,7 +515,7 @@ // It can only be found in ReplyToMessage if someone replies to a very first message
// in a directly created supergroup; // // optional - SuperGroupChatCreated bool `json:"supergroup_chat_created"` + SuperGroupChatCreated bool `json:"supergroup_chat_created,omitempty"` // ChannelChatCreated is a service message: the channel has been created. // This field can't be received in a message coming through updates, // because bot can't be a member of a channel when it is created.@@ -379,7 +523,12 @@ // It can only be found in ReplyToMessage
// if someone replies to a very first message in a channel; // // optional - ChannelChatCreated bool `json:"channel_chat_created"` + ChannelChatCreated bool `json:"channel_chat_created,omitempty"` + // MessageAutoDeleteTimerChanged is a service message: auto-delete timer + // settings changed in the chat. + // + // optional + MessageAutoDeleteTimerChanged *MessageAutoDeleteTimerChanged `json:"message_auto_delete_timer_changed"` // MigrateToChatID is the group has been migrated to a supergroup with the specified identifier. // This number may be greater than 32 bits and some programming languages // may have difficulty/silent defects in interpreting it.@@ -387,7 +536,7 @@ // But it is smaller than 52 bits, so a signed 64 bit integer
// or double-precision float type are safe for storing this identifier; // // optional - MigrateToChatID int64 `json:"migrate_to_chat_id"` + MigrateToChatID int64 `json:"migrate_to_chat_id,omitempty"` // MigrateFromChatID is the supergroup has been migrated from a group with the specified identifier. // This number may be greater than 32 bits and some programming languages // may have difficulty/silent defects in interpreting it.@@ -395,26 +544,58 @@ // But it is smaller than 52 bits, so a signed 64 bit integer
// or double-precision float type are safe for storing this identifier; // // optional - MigrateFromChatID int64 `json:"migrate_from_chat_id"` + MigrateFromChatID int64 `json:"migrate_from_chat_id,omitempty"` // PinnedMessage is a specified message was pinned. // Note that the Message object in this field will not contain further ReplyToMessage // fields even if it is itself a reply; // // optional - PinnedMessage *Message `json:"pinned_message"` + PinnedMessage *Message `json:"pinned_message,omitempty"` // Invoice message is an invoice for a payment; // // optional - Invoice *Invoice `json:"invoice"` + Invoice *Invoice `json:"invoice,omitempty"` // SuccessfulPayment message is a service message about a successful payment, // information about the payment; // // optional - SuccessfulPayment *SuccessfulPayment `json:"successful_payment"` + SuccessfulPayment *SuccessfulPayment `json:"successful_payment,omitempty"` + // ConnectedWebsite is Tthe domain name of the website on which the user has + // logged in; + // + // optional + ConnectedWebsite string `json:"connected_website,omitempty"` // PassportData is a Telegram Passport data; // // optional PassportData *PassportData `json:"passport_data,omitempty"` + // ProximityAlertTriggered is a service message. A user in the chat + // triggered another user's proximity alert while sharing Live Location + // + // optional + ProximityAlertTriggered *ProximityAlertTriggered `json:"proximity_alert_triggered"` + // VoiceChatScheduled is a service message: voice chat scheduled. + // + // optional + VoiceChatScheduled *VoiceChatScheduled `json:"voice_chat_scheduled"` + // VoiceChatStarted is a service message: voice chat started. + // + // optional + VoiceChatStarted *VoiceChatStarted `json:"voice_chat_started"` + // VoiceChatEnded is a service message: voice chat ended. + // + // optional + VoiceChatEnded *VoiceChatEnded `json:"voice_chat_ended"` + // VoiceChatParticipantsInvited is a service message: new participants + // invited to a voice chat. + // + // optional + VoiceChatParticipantsInvited *VoiceChatParticipantsInvited `json:"voice_chat_participants_invited"` + // ReplyMarkup is the Inline keyboard attached to the message. + // login_url buttons are represented as ordinary url buttons. + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` } // Time converts the message timestamp into a Time.@@ -424,11 +605,11 @@ }
// IsCommand returns true if message starts with a "bot_command" entity. func (m *Message) IsCommand() bool { - if m.Entities == nil || len(*m.Entities) == 0 { + if m.Entities == nil || len(m.Entities) == 0 { return false } - entity := (*m.Entities)[0] + entity := m.Entities[0] return entity.Offset == 0 && entity.IsCommand() }@@ -458,7 +639,7 @@ return ""
} // IsCommand() checks that the message begins with a bot_command entity - entity := (*m.Entities)[0] + entity := m.Entities[0] return m.Text[1:entity.Length] }@@ -477,7 +658,8 @@ return ""
} // IsCommand() checks that the message begins with a bot_command entity - entity := (*m.Entities)[0] + entity := m.Entities[0] + if len(m.Text) == entity.Length { return "" // The command makes up the whole message }@@ -485,7 +667,12 @@
return m.Text[entity.Length+1:] } -// MessageEntity contains information about data in a Message. +// MessageID represents a unique message identifier. +type MessageID struct { + MessageID int `json:"message_id"` +} + +// MessageEntity represents one special entity in a text message. type MessageEntity struct { // Type of the entity. // Can be:@@ -512,11 +699,15 @@ Length int `json:"length"`
// URL for “text_link” only, url that will be opened after user taps on the text // // optional - URL string `json:"url"` + URL string `json:"url,omitempty"` // User for “text_mention” only, the mentioned user // // optional - User *User `json:"user"` + User *User `json:"user,omitempty"` + // Language for “pre” only, the programming language of the entity text + // + // optional + Language string `json:"language,omitempty"` } // ParseURL attempts to parse a URL contained within a MessageEntity.@@ -543,8 +734,8 @@ func (e MessageEntity) IsCommand() bool {
return e.Type == "bot_command" } -// IsUrl returns true if the type of the message entity is "url". -func (e MessageEntity) IsUrl() bool { +// IsURL returns true if the type of the message entity is "url". +func (e MessageEntity) IsURL() bool { return e.Type == "url" }@@ -578,10 +769,15 @@ func (e MessageEntity) IsTextLink() bool {
return e.Type == "text_link" } -// PhotoSize contains information about photos. +// PhotoSize represents one size of a photo or a file / sticker thumbnail. type PhotoSize struct { - // FileID identifier for this file, which can be used to download or reuse the file + // FileID identifier for this file, which can be used to download or reuse + // the file FileID string `json:"file_id"` + // FileUniqueID is the unique identifier for this file, which is supposed to + // be the same over time and for different bots. Can't be used to download + // or reuse the file. + FileUniqueID string `json:"file_unique_id"` // Width photo width Width int `json:"width"` // Height photo height@@ -589,135 +785,115 @@ Height int `json:"height"`
// FileSize file size // // optional - FileSize int `json:"file_size"` + FileSize int `json:"file_size,omitempty"` } -// Audio contains information about audio. -type Audio struct { - // FileID is an identifier for this file, which can be used to download or reuse the file +// Animation represents an animation file. +type Animation struct { + // FileID odentifier for this file, which can be used to download or reuse + // the file FileID string `json:"file_id"` - // Duration of the audio in seconds as defined by sender + // FileUniqueID is the unique identifier for this file, which is supposed to + // be the same over time and for different bots. Can't be used to download + // or reuse the file. + FileUniqueID string `json:"file_unique_id"` + // Width video width as defined by sender + Width int `json:"width"` + // Height video height as defined by sender + Height int `json:"height"` + // Duration of the video in seconds as defined by sender Duration int `json:"duration"` - // Performer of the audio as defined by sender or by audio tags + // Thumbnail animation thumbnail as defined by sender // // optional - Performer string `json:"performer"` - // Title of the audio as defined by sender or by audio tags + Thumbnail *PhotoSize `json:"thumb,omitempty"` + // FileName original animation filename as defined by sender // // optional - Title string `json:"title"` + FileName string `json:"file_name,omitempty"` // MimeType of the file as defined by sender // // optional - MimeType string `json:"mime_type"` + MimeType string `json:"mime_type,omitempty"` // FileSize file size // // optional - FileSize int `json:"file_size"` + FileSize int `json:"file_size,omitempty"` } -// Document contains information about a document. -type Document struct { - // FileID is a identifier for this file, which can be used to download or reuse the file +// Audio represents an audio file to be treated as music by the Telegram clients. +type Audio struct { + // FileID is an identifier for this file, which can be used to download or + // reuse the file FileID string `json:"file_id"` - // Thumbnail document thumbnail as defined by sender - // - // optional - Thumbnail *PhotoSize `json:"thumb"` - // FileName original filename as defined by sender + // FileUniqueID is the unique identifier for this file, which is supposed to + // be the same over time and for different bots. Can't be used to download + // or reuse the file. + FileUniqueID string `json:"file_unique_id"` + // Duration of the audio in seconds as defined by sender + Duration int `json:"duration"` + // Performer of the audio as defined by sender or by audio tags // // optional - FileName string `json:"file_name"` - // MimeType of the file as defined by sender - // - // optional - MimeType string `json:"mime_type"` - // FileSize file size - // - // optional - FileSize int `json:"file_size"` -} - -// Sticker contains information about a sticker. -type Sticker struct { - // FileUniqueID is an unique identifier for this file, - // which is supposed to be the same over time and for different bots. - // Can't be used to download or reuse the file. - FileUniqueID string `json:"file_unique_id"` - // FileID is an identifier for this file, which can be used to download or reuse the file - FileID string `json:"file_id"` - // Width sticker width - Width int `json:"width"` - // Height sticker height - Height int `json:"height"` - // Thumbnail sticker thumbnail in the .WEBP or .JPG format + Performer string `json:"performer,omitempty"` + // Title of the audio as defined by sender or by audio tags // // optional - Thumbnail *PhotoSize `json:"thumb"` - // Emoji associated with the sticker + Title string `json:"title,omitempty"` + // FileName is the original filename as defined by sender // // optional - Emoji string `json:"emoji"` - // FileSize + FileName string `json:"file_name,omitempty"` + // MimeType of the file as defined by sender // // optional - FileSize int `json:"file_size"` - // SetName of the sticker set to which the sticker belongs + MimeType string `json:"mime_type,omitempty"` + // FileSize file size // // optional - SetName string `json:"set_name"` - // IsAnimated true, if the sticker is animated + FileSize int `json:"file_size,omitempty"` + // Thumbnail is the album cover to which the music file belongs // // optional - IsAnimated bool `json:"is_animated"` + Thumbnail *PhotoSize `json:"thumb,omitempty"` } -// StickerSet contains information about an sticker set. -type StickerSet struct { - // Name sticker set name - Name string `json:"name"` - // Title sticker set title - Title string `json:"title"` - // IsAnimated true, if the sticker set contains animated stickers - IsAnimated bool `json:"is_animated"` - // ContainsMasks true, if the sticker set contains masks - ContainsMasks bool `json:"contains_masks"` - // Stickers list of all set stickers - Stickers []Sticker `json:"stickers"` -} - -// ChatAnimation contains information about an animation. -type ChatAnimation struct { - // FileID odentifier for this file, which can be used to download or reuse the file +// Document represents a general file. +type Document struct { + // FileID is a identifier for this file, which can be used to download or + // reuse the file FileID string `json:"file_id"` - // Width video width as defined by sender - Width int `json:"width"` - // Height video height as defined by sender - Height int `json:"height"` - // Duration of the video in seconds as defined by sender - Duration int `json:"duration"` - // Thumbnail animation thumbnail as defined by sender + // FileUniqueID is the unique identifier for this file, which is supposed to + // be the same over time and for different bots. Can't be used to download + // or reuse the file. + FileUniqueID string `json:"file_unique_id"` + // Thumbnail document thumbnail as defined by sender // // optional - Thumbnail *PhotoSize `json:"thumb"` - // FileName original animation filename as defined by sender + Thumbnail *PhotoSize `json:"thumb,omitempty"` + // FileName original filename as defined by sender // // optional - FileName string `json:"file_name"` - // MimeType of the file as defined by sender + FileName string `json:"file_name,omitempty"` + // MimeType of the file as defined by sender // // optional - MimeType string `json:"mime_type"` + MimeType string `json:"mime_type,omitempty"` // FileSize file size // // optional - FileSize int `json:"file_size"` + FileSize int `json:"file_size,omitempty"` } -// Video contains information about a video. +// Video represents a video file. type Video struct { - // FileID identifier for this file, which can be used to download or reuse the file + // FileID identifier for this file, which can be used to download or reuse + // the file FileID string `json:"file_id"` + // FileUniqueID is the unique identifier for this file, which is supposed to + // be the same over time and for different bots. Can't be used to download + // or reuse the file. + FileUniqueID string `json:"file_unique_id"` // Width video width as defined by sender Width int `json:"width"` // Height video height as defined by sender@@ -727,21 +903,29 @@ Duration int `json:"duration"`
// Thumbnail video thumbnail // // optional - Thumbnail *PhotoSize `json:"thumb"` + Thumbnail *PhotoSize `json:"thumb,omitempty"` + // FileName is the original filename as defined by sender + // + // optional + FileName string `json:"file_name,omitempty"` // MimeType of a file as defined by sender // // optional - MimeType string `json:"mime_type"` + MimeType string `json:"mime_type,omitempty"` // FileSize file size // // optional - FileSize int `json:"file_size"` + FileSize int `json:"file_size,omitempty"` } -// VideoNote contains information about a video. +// VideoNote object represents a video message. type VideoNote struct { // FileID identifier for this file, which can be used to download or reuse the file FileID string `json:"file_id"` + // FileUniqueID is the unique identifier for this file, which is supposed to + // be the same over time and for different bots. Can't be used to download + // or reuse the file. + FileUniqueID string `json:"file_unique_id"` // Length video width and height (diameter of the video message) as defined by sender Length int `json:"length"` // Duration of the video in seconds as defined by sender@@ -749,30 +933,34 @@ Duration int `json:"duration"`
// Thumbnail video thumbnail // // optional - Thumbnail *PhotoSize `json:"thumb"` + Thumbnail *PhotoSize `json:"thumb,omitempty"` // FileSize file size // // optional - FileSize int `json:"file_size"` + FileSize int `json:"file_size,omitempty"` } -// Voice contains information about a voice. +// Voice represents a voice note. type Voice struct { // FileID identifier for this file, which can be used to download or reuse the file FileID string `json:"file_id"` + // FileUniqueID is the unique identifier for this file, which is supposed to + // be the same over time and for different bots. Can't be used to download + // or reuse the file. + FileUniqueID string `json:"file_unique_id"` // Duration of the audio in seconds as defined by sender Duration int `json:"duration"` // MimeType of the file as defined by sender // // optional - MimeType string `json:"mime_type"` + MimeType string `json:"mime_type,omitempty"` // FileSize file size // // optional - FileSize int `json:"file_size"` + FileSize int `json:"file_size,omitempty"` } -// Contact contains information about a contact. +// Contact represents a phone contact. // // Note that LastName and UserID may be empty. type Contact struct {@@ -783,33 +971,193 @@ FirstName string `json:"first_name"`
// LastName contact's last name // // optional - LastName string `json:"last_name"` + LastName string `json:"last_name,omitempty"` // UserID contact's user identifier in Telegram // // optional - UserID int `json:"user_id"` + UserID int64 `json:"user_id,omitempty"` + // VCard is additional data about the contact in the form of a vCard. + // + // optional + VCard string `json:"vcard,omitempty"` } -// Location contains information about a place. +// Dice represents an animated emoji that displays a random value. +type Dice struct { + // Emoji on which the dice throw animation is based + Emoji string `json:"emoji"` + // Value of the dice + Value int `json:"value"` +} + +// PollOption contains information about one answer option in a poll. +type PollOption struct { + // Text is the option text, 1-100 characters + Text string `json:"text"` + // VoterCount is the number of users that voted for this option + VoterCount int `json:"voter_count"` +} + +// PollAnswer represents an answer of a user in a non-anonymous poll. +type PollAnswer struct { + // PollID is the unique poll identifier + PollID string `json:"poll_id"` + // User who changed the answer to the poll + User User `json:"user"` + // OptionIDs is the 0-based identifiers of poll options chosen by the user. + // May be empty if user retracted vote. + OptionIDs []int `json:"option_ids"` +} + +// Poll contains information about a poll. +type Poll struct { + // ID is the unique poll identifier + ID string `json:"id"` + // Question is the poll question, 1-255 characters + Question string `json:"question"` + // Options is the list of poll options + Options []PollOption `json:"options"` + // TotalVoterCount is the total numbers of users who voted in the poll + TotalVoterCount int `json:"total_voter_count"` + // IsClosed is if the poll is closed + IsClosed bool `json:"is_closed"` + // IsAnonymous is if the poll is anonymous + IsAnonymous bool `json:"is_anonymous"` + // Type is the poll type, currently can be "regular" or "quiz" + Type string `json:"type"` + // AllowsMultipleAnswers is true, if the poll allows multiple answers + AllowsMultipleAnswers bool `json:"allows_multiple_answers"` + // CorrectOptionID is the 0-based identifier of the correct answer option. + // Available only for polls in quiz mode, which are closed, or was sent (not + // forwarded) by the bot or to the private chat with the bot. + // + // optional + CorrectOptionID int `json:"correct_option_id,omitempty"` + // Explanation is text that is shown when a user chooses an incorrect answer + // or taps on the lamp icon in a quiz-style poll, 0-200 characters + // + // optional + Explanation string `json:"explanation,omitempty"` + // ExplainationEntities are special entities like usernames, URLs, bot + // commands, etc. that appear in the explanation + // + // optional + ExplanationEntities []MessageEntity `json:"explanation_entities,omitempty"` + // OpenPeriod is the amount of time in seconds the poll will be active + // after creation + // + // optional + OpenPeriod int `json:"open_period,omitempty"` + // Closedate is the point in time (unix timestamp) when the poll will be + // automatically closed + // + // optional + CloseDate int `json:"close_date,omitempty"` +} + +// Location represents a point on the map. type Location struct { // Longitude as defined by sender Longitude float64 `json:"longitude"` // Latitude as defined by sender Latitude float64 `json:"latitude"` + // HorizontalAccuracy is the radius of uncertainty for the location, + // measured in meters; 0-1500 + // + // optional + HorizontalAccuracy float64 `json:"horizontal_accuracy,omitempty"` + // LivePeriod is time relative to the message sending date, during which the + // location can be updated, in seconds. For active live locations only. + // + // optional + LivePeriod int `json:"live_period,omitempty"` + // Heading is the direction in which user is moving, in degrees; 1-360. For + // active live locations only. + // + // optional + Heading int `json:"heading,omitempty"` + // ProximityAlertRadius is the maximum distance for proximity alerts about + // approaching another chat member, in meters. For sent live locations only. + // + // optional + ProximityAlertRadius int `json:"proximity_alert_radius,omitempty"` } -// Venue contains information about a venue, including its Location. +// Venue represents a venue. type Venue struct { - // Location venue location + // Location is the venue location Location Location `json:"location"` - // Title name of the venue + // Title is the name of the venue Title string `json:"title"` // Address of the venue Address string `json:"address"` - // FoursquareID foursquare identifier of the venue + // FoursquareID is the foursquare identifier of the venue + // + // optional + FoursquareID string `json:"foursquare_id,omitempty"` + // FoursquareType is the foursquare type of the venue + // + // optional + FoursquareType string `json:"foursquare_type,omitempty"` + // GooglePlaceID is the Google Places identifier of the venue + // + // optional + GooglePlaceID string `json:"google_place_id,omitempty"` + // GooglePlaceType is the Google Places type of the venue + // + // optional + GooglePlaceType string `json:"google_place_type,omitempty"` +} + +// ProximityAlertTriggered represents a service message sent when a user in the +// chat triggers a proximity alert sent by another user. +type ProximityAlertTriggered struct { + // Traveler is the user that triggered the alert + Traveler User `json:"traveler"` + // Watcher is the user that set the alert + Watcher User `json:"watcher"` + // Distance is the distance between the users + Distance int `json:"distance"` +} + +// MessageAutoDeleteTimerChanged represents a service message about a change in +// auto-delete timer settings. +type MessageAutoDeleteTimerChanged struct { + // New auto-delete time for messages in the chat. + MessageAutoDeleteTime int `json:"message_auto_delete_time"` +} + +// VoiceChatScheduled represents a service message about a voice chat scheduled +// in the chat. +type VoiceChatScheduled struct { + // Point in time (Unix timestamp) when the voice chat is supposed to be + // started by a chat administrator + StartDate int `json:"start_date"` +} + +// Time converts the scheduled start date into a Time. +func (m *VoiceChatScheduled) Time() time.Time { + return time.Unix(int64(m.StartDate), 0) +} + +// VoiceChatStarted represents a service message about a voice chat started in +// the chat. +type VoiceChatStarted struct{} + +// VoiceChatEnded represents a service message about a voice chat ended in the +// chat. +type VoiceChatEnded struct { + // Voice chat duration; in seconds. + Duration int `json:"duration"` +} + +// VoiceChatParticipantsInvited represents a service message about new members +// invited to a voice chat. +type VoiceChatParticipantsInvited struct { + // New members that were invited to the voice chat. // // optional - FoursquareID string `json:"foursquare_id"` + Users []User `json:"users"` } // UserProfilePhotos contains a set of user profile photos.@@ -822,26 +1170,31 @@ }
// File contains information about a file to download from Telegram. type File struct { - // FileID identifier for this file, which can be used to download or reuse the file + // FileID identifier for this file, which can be used to download or reuse + // the file FileID string `json:"file_id"` + // FileUniqueID is the unique identifier for this file, which is supposed to + // be the same over time and for different bots. Can't be used to download + // or reuse the file. + FileUniqueID string `json:"file_unique_id"` // FileSize file size, if known // // optional - FileSize int `json:"file_size"` + FileSize int `json:"file_size,omitempty"` // FilePath file path // // optional - FilePath string `json:"file_path"` + FilePath string `json:"file_path,omitempty"` } // Link returns a full path to the download URL for a File. // -// It requires the Bot Token to create the link. +// It requires the Bot token to create the link. func (f *File) Link(token string) string { return fmt.Sprintf(FileEndpoint, token, f.FilePath) } -// ReplyKeyboardMarkup allows the Bot to set a custom keyboard. +// ReplyKeyboardMarkup represents a custom keyboard with reply options. type ReplyKeyboardMarkup struct { // Keyboard is an array of button rows, each represented by an Array of KeyboardButton objects Keyboard [][]KeyboardButton `json:"keyboard"`@@ -851,7 +1204,7 @@ // Defaults to false, in which case the custom keyboard
// is always of the same height as the app's standard keyboard. // // optional - ResizeKeyboard bool `json:"resize_keyboard"` + ResizeKeyboard bool `json:"resize_keyboard,omitempty"` // OneTimeKeyboard requests clients to hide the keyboard as soon as it's been used. // The keyboard will still be available, but clients will automatically display // the usual letter-keyboard in the chat – the user can press a special button@@ -859,7 +1212,12 @@ // in the input field to see the custom keyboard again.
// Defaults to false. // // optional - OneTimeKeyboard bool `json:"one_time_keyboard"` + OneTimeKeyboard bool `json:"one_time_keyboard,omitempty"` + // InputFieldPlaceholder is the placeholder to be shown in the input field when + // the keyboard is active; 1-64 characters. + // + // optional + InputFieldPlaceholder string `json:"input_field_placeholder,omitempty"` // Selective use this parameter if you want to show the keyboard to specific users only. // Targets: // 1) users that are @mentioned in the text of the Message object;@@ -870,10 +1228,13 @@ // bot replies to the request with a keyboard to select the new language.
// Other users in the group don't see the keyboard. // // optional - Selective bool `json:"selective"` + Selective bool `json:"selective,omitempty"` } -// KeyboardButton is a button within a custom keyboard. +// KeyboardButton represents one button of the reply keyboard. For simple text +// buttons String can be used instead of this object to specify text of the +// button. Optional fields request_contact, request_location, and request_poll +// are mutually exclusive. type KeyboardButton struct { // Text of the button. If none of the optional fields are used, // it will be sent as a message when the button is pressed.@@ -883,21 +1244,34 @@ // as a contact when the button is pressed.
// Available in private chats only. // // optional - RequestContact bool `json:"request_contact"` - // RequestLocation if True, the user's current location will be sent when the button is pressed. + RequestContact bool `json:"request_contact,omitempty"` + // RequestLocation if True, the user's current location will be sent when + // the button is pressed. // Available in private chats only. // // optional - RequestLocation bool `json:"request_location"` + RequestLocation bool `json:"request_location,omitempty"` + // RequestPoll if True, the user will be asked to create a poll and send it + // to the bot when the button is pressed. Available in private chats only + // + // optional + RequestPoll *KeyboardButtonPollType `json:"request_poll,omitempty"` } -// ReplyKeyboardHide allows the Bot to hide a custom keyboard. -type ReplyKeyboardHide struct { - HideKeyboard bool `json:"hide_keyboard"` - Selective bool `json:"selective"` // optional +// KeyboardButtonPollType represents type of a poll, which is allowed to +// be created and sent when the corresponding button is pressed. +type KeyboardButtonPollType struct { + // Type is if quiz is passed, the user will be allowed to create only polls + // in the quiz mode. If regular is passed, only regular polls will be + // allowed. Otherwise, the user will be allowed to create a poll of any type. + Type string `json:"type"` } -// ReplyKeyboardRemove allows the Bot to hide a custom keyboard. +// ReplyKeyboardRemove Upon receiving a message with this object, Telegram +// clients will remove the current custom keyboard and display the default +// letter-keyboard. By default, custom keyboards are displayed until a new +// keyboard is sent by a bot. An exception is made for one-time keyboards +// that are hidden immediately after the user presses a button. type ReplyKeyboardRemove struct { // RemoveKeyboard requests clients to remove the custom keyboard // (user will not be able to summon this keyboard;@@ -914,17 +1288,19 @@ // in reply to the vote and removes the keyboard for that user,
// while still showing the keyboard with poll options to users who haven't voted yet. // // optional - Selective bool `json:"selective"` + Selective bool `json:"selective,omitempty"` } -// InlineKeyboardMarkup is a custom keyboard presented for an inline bot. +// InlineKeyboardMarkup represents an inline keyboard that appears right next to +// the message it belongs to. type InlineKeyboardMarkup struct { - // InlineKeyboard array of button rows, each represented by an Array of InlineKeyboardButton objects + // InlineKeyboard array of button rows, each represented by an Array of + // InlineKeyboardButton objects InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard"` } -// InlineKeyboardButton is a button within a custom keyboard for -// inline query responses. +// InlineKeyboardButton represents one button of an inline keyboard. You must +// use exactly one of the optional fields. // // Note that some values are references as even an empty string // will change behavior.@@ -937,6 +1313,11 @@ // URL HTTP or tg:// url to be opened when button is pressed.
// // optional URL *string `json:"url,omitempty"` + // LoginURL is an HTTP URL used to automatically authorize the user. Can be + // used as a replacement for the Telegram Login Widget + // + // optional + LoginURL *LoginURL `json:"login_url,omitempty"` // CallbackData data to be sent in a callback query to the bot when button is pressed, 1-64 bytes. // // optional@@ -974,54 +1355,158 @@ // optional
Pay bool `json:"pay,omitempty"` } -// CallbackQuery is data sent when a keyboard button with callback data -// is clicked. +// LoginURL represents a parameter of the inline keyboard button used to +// automatically authorize a user. Serves as a great replacement for the +// Telegram Login Widget when the user is coming from Telegram. All the user +// needs to do is tap/click a button and confirm that they want to log in. +type LoginURL struct { + // URL is an HTTP URL to be opened with user authorization data added to the + // query string when the button is pressed. If the user refuses to provide + // authorization data, the original URL without information about the user + // will be opened. The data added is the same as described in Receiving + // authorization data. + // + // NOTE: You must always check the hash of the received data to verify the + // authentication and the integrity of the data as described in Checking + // authorization. + URL string `json:"url"` + // ForwardText is the new text of the button in forwarded messages + // + // optional + ForwardText string `json:"forward_text,omitempty"` + // BotUsername is the username of a bot, which will be used for user + // authorization. See Setting up a bot for more details. If not specified, + // the current bot's username will be assumed. The url's domain must be the + // same as the domain linked with the bot. See Linking your domain to the + // bot for more details. + // + // optional + BotUsername string `json:"bot_username,omitempty"` + // RequestWriteAccess if true requests permission for your bot to send + // messages to the user + // + // optional + RequestWriteAccess bool `json:"request_write_access,omitempty"` +} + +// CallbackQuery represents an incoming callback query from a callback button in +// an inline keyboard. If the button that originated the query was attached to a +// message sent by the bot, the field message will be present. If the button was +// attached to a message sent via the bot (in inline mode), the field +// inline_message_id will be present. Exactly one of the fields data or +// game_short_name will be present. type CallbackQuery struct { // ID unique identifier for this query ID string `json:"id"` // From sender From *User `json:"from"` // Message with the callback button that originated the query. - // Note that message content and message date will not be available if the message is too old. + // Note that message content and message date will not be available if the + // message is too old. // // optional - Message *Message `json:"message"` - // InlineMessageID identifier of the message sent via the bot in inline mode, that originated the query. + Message *Message `json:"message,omitempty"` + // InlineMessageID identifier of the message sent via the bot in inline + // mode, that originated the query. // // optional - // - InlineMessageID string `json:"inline_message_id"` - // ChatInstance global identifier, uniquely corresponding to the chat to which - // the message with the callback button was sent. Useful for high scores in games. - // + InlineMessageID string `json:"inline_message_id,omitempty"` + // ChatInstance global identifier, uniquely corresponding to the chat to + // which the message with the callback button was sent. Useful for high + // scores in games. ChatInstance string `json:"chat_instance"` // Data associated with the callback button. Be aware that // a bad client can send arbitrary data in this field. // // optional - Data string `json:"data"` + Data string `json:"data,omitempty"` // GameShortName short name of a Game to be returned, serves as the unique identifier for the game. // // optional - GameShortName string `json:"game_short_name"` + GameShortName string `json:"game_short_name,omitempty"` } -// ForceReply allows the Bot to have users directly reply to it without -// additional interaction. +// ForceReply when receiving a message with this object, Telegram clients will +// display a reply interface to the user (act as if the user has selected the +// bot's message and tapped 'Reply'). This can be extremely useful if you want +// to create user-friendly step-by-step interfaces without having to sacrifice +// privacy mode. type ForceReply struct { // ForceReply shows reply interface to the user, // as if they manually selected the bot's message and tapped 'Reply'. ForceReply bool `json:"force_reply"` + // InputFieldPlaceholder is the placeholder to be shown in the input field when + // the reply is active; 1-64 characters. + // + // optional + InputFieldPlaceholder string `json:"input_field_placeholder,omitempty"` // Selective use this parameter if you want to force reply from specific users only. // Targets: // 1) users that are @mentioned in the text of the Message object; // 2) if the bot's message is a reply (has Message.ReplyToMessage not nil), sender of the original message. // // optional - Selective bool `json:"selective"` + Selective bool `json:"selective,omitempty"` +} + +// ChatPhoto represents a chat photo. +type ChatPhoto struct { + // SmallFileID is a file identifier of small (160x160) chat photo. + // This file_id can be used only for photo download and + // only for as long as the photo is not changed. + SmallFileID string `json:"small_file_id"` + // SmallFileUniqueID is a unique file identifier of small (160x160) chat + // photo, which is supposed to be the same over time and for different bots. + // Can't be used to download or reuse the file. + SmallFileUniqueID string `json:"small_file_unique_id"` + // BigFileID is a file identifier of big (640x640) chat photo. + // This file_id can be used only for photo download and + // only for as long as the photo is not changed. + BigFileID string `json:"big_file_id"` + // BigFileUniqueID is a file identifier of big (640x640) chat photo, which + // is supposed to be the same over time and for different bots. Can't be + // used to download or reuse the file. + BigFileUniqueID string `json:"big_file_unique_id"` } -// ChatMember is information about a member in a chat. +// ChatInviteLink represents an invite link for a chat. +type ChatInviteLink struct { + // InviteLink is the invite link. If the link was created by another chat + // administrator, then the second part of the link will be replaced with “…”. + InviteLink string `json:"invite_link"` + // Creator of the link. + Creator User `json:"creator"` + // CreatesJoinRequest is true if users joining the chat via the link need to + // be approved by chat administrators. + // + // optional + CreatesJoinRequest bool `json:"creates_join_request"` + // IsPrimary is true, if the link is primary. + IsPrimary bool `json:"is_primary"` + // IsRevoked is true, if the link is revoked. + IsRevoked bool `json:"is_revoked"` + // Name is the name of the invite link. + // + // optional + Name string `json:"name"` + // ExpireDate is the point in time (Unix timestamp) when the link will + // expire or has been expired. + // + // optional + ExpireDate int `json:"expire_date"` + // MemberLimit is the maximum number of users that can be members of the + // chat simultaneously after joining the chat via this invite link; 1-99999. + // + // optional + MemberLimit int `json:"member_limit"` + // PendingJoinRequestCount is the number of pending join requests created + // using this link. + // + // optional + PendingJoinRequestCount int `json:"pending_join_request_count"` +} + +// ChatMember contains information about one member of a chat. type ChatMember struct { // User information about the user User *User `json:"user"`@@ -1038,6 +1523,11 @@ // CustomTitle owner and administrators only. Custom title for this user
// // optional CustomTitle string `json:"custom_title,omitempty"` + // IsAnonymous owner and administrators only. True, if the user's presence + // in the chat is hidden + // + // optional + IsAnonymous bool `json:"is_anonymous"` // UntilDate restricted and kicked only. // Date when restrictions will be lifted for this user; // unix time.@@ -1049,12 +1539,15 @@ // True, if the bot is allowed to edit administrator privileges of that user.
// // optional CanBeEdited bool `json:"can_be_edited,omitempty"` - // CanChangeInfo administrators and restricted only. - // True, if the user is allowed to change the chat title, photo and other settings. + // CanManageChat administrators only. + // True, if the administrator can access the chat event log, chat + // statistics, message statistics in channels, see channel members, see + // anonymous administrators in supergoups and ignore slow mode. Implied by + // any other administrator privilege. // // optional - CanChangeInfo bool `json:"can_change_info,omitempty"` - // CanChangeInfo administrators only. + CanManageChat bool `json:"can_manage_chat"` + // CanPostMessages administrators only. // True, if the administrator can post in the channel; // channels only. //@@ -1071,20 +1564,16 @@ // True, if the administrator can delete messages of other users.
// // optional CanDeleteMessages bool `json:"can_delete_messages,omitempty"` - // CanInviteUsers administrators and restricted only. - // True, if the user is allowed to invite new users to the chat. + // CanManageVoiceChats administrators only. + // True, if the administrator can manage voice chats. // // optional - CanInviteUsers bool `json:"can_invite_users,omitempty"` + CanManageVoiceChats bool `json:"can_manage_voice_chats"` // CanRestrictMembers administrators only. // True, if the administrator can restrict, ban or unban chat members. // // optional CanRestrictMembers bool `json:"can_restrict_members,omitempty"` - // CanPinMessages - // - // optional - CanPinMessages bool `json:"can_pin_messages,omitempty"` // CanPromoteMembers administrators only. // True, if the administrator can add new administrators // with a subset of their own privileges or demote administrators that he has promoted,@@ -1092,6 +1581,24 @@ // directly or indirectly (promoted by administrators that were appointed by the user).
// // optional CanPromoteMembers bool `json:"can_promote_members,omitempty"` + // CanChangeInfo administrators and restricted only. + // True, if the user is allowed to change the chat title, photo and other settings. + // + // optional + CanChangeInfo bool `json:"can_change_info,omitempty"` + // CanInviteUsers administrators and restricted only. + // True, if the user is allowed to invite new users to the chat. + // + // optional + CanInviteUsers bool `json:"can_invite_users,omitempty"` + // CanPinMessages administrators and restricted only. + // True, if the user is allowed to pin messages; groups and supergroups only + // + // optional + CanPinMessages bool `json:"can_pin_messages,omitempty"` + // IsMember is true, if the user is a member of the chat at the moment of + // the request + IsMember bool `json:"is_member"` // CanSendMessages // // optional@@ -1101,6 +1608,11 @@ // True, if the user is allowed to send text messages, contacts, locations and venues
// // optional CanSendMediaMessages bool `json:"can_send_media_messages,omitempty"` + // CanSendPolls restricted only. + // True, if the user is allowed to send polls + // + // optional + CanSendPolls bool `json:"can_send_polls,omitempty"` // CanSendOtherMessages restricted only. // True, if the user is allowed to send audios, documents, // photos, videos, video notes and voice notes.@@ -1120,371 +1632,502 @@
// IsAdministrator returns if the ChatMember is a chat administrator. func (chat ChatMember) IsAdministrator() bool { return chat.Status == "administrator" } -// IsMember returns if the ChatMember is a current member of the chat. -func (chat ChatMember) IsMember() bool { return chat.Status == "member" } - // HasLeft returns if the ChatMember left the chat. 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 of the game - Title string `json:"title"` - // Description of the game - Description string `json:"description"` - // Photo that will be displayed in the game message in chats. - Photo []PhotoSize `json:"photo"` - // Text a brief description of the game or high scores included in the game message. - // Can be automatically edited to include current high scores for the game - // when the bot calls setGameScore, or manually edited using editMessageText. 0-4096 characters. +// ChatMemberUpdated represents changes in the status of a chat member. +type ChatMemberUpdated struct { + // Chat the user belongs to. + Chat Chat `json:"chat"` + // From is the performer of the action, which resulted in the change. + From User `json:"from"` + // Date the change was done in Unix time. + Date int `json:"date"` + // Previous information about the chat member. + OldChatMember ChatMember `json:"old_chat_member"` + // New information about the chat member. + NewChatMember ChatMember `json:"new_chat_member"` + // InviteLink is the link which was used by the user to join the chat; + // for joining by invite link events only. // // optional - Text string `json:"text"` - // TextEntities special entities that appear in text, such as usernames, URLs, bot commands, etc. + InviteLink *ChatInviteLink `json:"invite_link"` +} + +// ChatJoinRequest represents a join request sent to a chat. +type ChatJoinRequest struct { + // Chat to which the request was sent. + Chat Chat `json:"chat"` + // User that sent the join request. + From User `json:"user"` + // Date the request was sent in Unix time. + Date int `json:"date"` + // Bio of the user. // // optional - TextEntities []MessageEntity `json:"text_entities"` - // Animation animation that will be displayed in the game message in chats. - // Upload via BotFather (https://t.me/botfather). + Bio string `json:"bio"` + // InviteLink is the link that was used by the user to send the join request. // // optional - Animation Animation `json:"animation"` + InviteLink *ChatInviteLink `json:"invite_link"` } -// Animation is a GIF animation demonstrating the game. -type Animation struct { - // FileID identifier for this file, which can be used to download or reuse the file. - FileID string `json:"file_id"` - // Thumb animation thumbnail as defined by sender. +// ChatPermissions describes actions that a non-administrator user is +// allowed to take in a chat. All fields are optional. +type ChatPermissions struct { + // CanSendMessages is true, if the user is allowed to send text messages, + // contacts, locations and venues // // optional - Thumb PhotoSize `json:"thumb"` - // FileName original animation filename as defined by sender. + CanSendMessages bool `json:"can_send_messages,omitempty"` + // CanSendMediaMessages is true, if the user is allowed to send audios, + // documents, photos, videos, video notes and voice notes, implies + // can_send_messages // // optional - FileName string `json:"file_name"` - // MimeType of the file as defined by sender. + CanSendMediaMessages bool `json:"can_send_media_messages,omitempty"` + // CanSendPolls is true, if the user is allowed to send polls, implies + // can_send_messages // // optional - MimeType string `json:"mime_type"` - // FileSize ile size + CanSendPolls bool `json:"can_send_polls,omitempty"` + // CanSendOtherMessages is true, if the user is allowed to send animations, + // games, stickers and use inline bots, implies can_send_media_messages // // optional - FileSize int `json:"file_size"` -} - -// GameHighScore is a user's score and position on the leaderboard. -type GameHighScore struct { - // Position in high score table for the game - Position int `json:"position"` - // User user - User User `json:"user"` - // Score score - 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 webhook URL, may be empty if webhook is not set up. - URL string `json:"url"` - // HasCustomCertificate true, if a custom certificate was provided for webhook certificate checks. - HasCustomCertificate bool `json:"has_custom_certificate"` - // PendingUpdateCount number of updates awaiting delivery. - PendingUpdateCount int `json:"pending_update_count"` - // LastErrorDate unix time for the most recent error - // that happened when trying to deliver an update via webhook. + CanSendOtherMessages bool `json:"can_send_other_messages,omitempty"` + // CanAddWebPagePreviews is true, if the user is allowed to add web page + // previews to their messages, implies can_send_media_messages // // optional - LastErrorDate int `json:"last_error_date"` - // LastErrorMessage error message in human-readable format for the most recent error - // that happened when trying to deliver an update via webhook. + CanAddWebPagePreviews bool `json:"can_add_web_page_previews,omitempty"` + // CanChangeInfo is true, if the user is allowed to change the chat title, + // photo and other settings. Ignored in public supergroups // // optional - LastErrorMessage string `json:"last_error_message"` - // MaxConnections maximum allowed number of simultaneous - // HTTPS connections to the webhook for update delivery. + CanChangeInfo bool `json:"can_change_info,omitempty"` + // CanInviteUsers is true, if the user is allowed to invite new users to the + // chat // // optional - MaxConnections int `json:"max_connections"` + CanInviteUsers bool `json:"can_invite_users,omitempty"` + // CanPinMessages is true, if the user is allowed to pin messages. Ignored + // in public supergroups + // + // optional + CanPinMessages bool `json:"can_pin_messages,omitempty"` } -// IsSet returns true if a webhook is currently set. -func (info WebhookInfo) IsSet() bool { - return info.URL != "" +// ChatLocation represents a location to which a chat is connected. +type ChatLocation struct { + // Location is the location to which the supergroup is connected. Can't be a + // live location. + Location Location `json:"location"` + // Address is the location address; 1-64 characters, as defined by the chat + // owner + Address string `json:"address"` +} + +// BotCommand represents a bot command. +type BotCommand struct { + // Command text of the command, 1-32 characters. + // Can contain only lowercase English letters, digits and underscores. + Command string `json:"command"` + // Description of the command, 3-256 characters. + Description string `json:"description"` } -// InputMediaPhoto contains a photo for displaying as part of a media group. -type InputMediaPhoto struct { - // Type of the result, must be photo. - Type string `json:"type"` - // Media file to send. Pass a file_id to send a file that - // exists on the Telegram servers (recommended), - // pass an HTTP URL for Telegram to get a file from the Internet, - // or pass “attach://<file_attach_name>” to upload a new one - // using multipart/form-data under <file_attach_name> name. - Media string `json:"media"` - // Caption of the photo to be sent, 0-1024 characters after entities parsing. +// BotCommandScope represents the scope to which bot commands are applied. +// +// It contains the fields for all types of scopes, different types only support +// specific (or no) fields. +type BotCommandScope struct { + Type string `json:"type"` + ChatID int64 `json:"chat_id,omitempty"` + UserID int64 `json:"user_id,omitempty"` +} + +// ResponseParameters are various errors that can be returned in APIResponse. +type ResponseParameters struct { + // The group has been migrated to a supergroup with the specified identifier. // // optional - Caption string `json:"caption"` - // ParseMode mode for parsing entities in the photo caption. - // See formatting options for more details - // (https://core.telegram.org/bots/api#formatting-options). + MigrateToChatID int64 `json:"migrate_to_chat_id,omitempty"` + // In case of exceeding flood control, the number of seconds left to wait + // before the request can be repeated. // // optional - ParseMode string `json:"parse_mode"` + RetryAfter int `json:"retry_after,omitempty"` } -// InputMediaVideo contains a video for displaying as part of a media group. -type InputMediaVideo struct { - // Type of the result, must be video. +// BaseInputMedia is a base type for the InputMedia types. +type BaseInputMedia struct { + // Type of the result. Type string `json:"type"` // Media file to send. Pass a file_id to send a file // that exists on the Telegram servers (recommended), // pass an HTTP URL for Telegram to get a file from the Internet, // or pass “attach://<file_attach_name>” to upload a new one // using multipart/form-data under <file_attach_name> name. - Media string `json:"media"` + Media RequestFileData `json:"media"` // thumb intentionally missing as it is not currently compatible // Caption of the video to be sent, 0-1024 characters after entities parsing. // // optional - Caption string `json:"caption"` + Caption string `json:"caption,omitempty"` // ParseMode mode for parsing entities in the video caption. // See formatting options for more details // (https://core.telegram.org/bots/api#formatting-options). // // optional - ParseMode string `json:"parse_mode"` + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities"` +} + +// InputMediaPhoto is a photo to send as part of a media group. +type InputMediaPhoto struct { + BaseInputMedia +} + +// InputMediaVideo is a video to send as part of a media group. +type InputMediaVideo struct { + BaseInputMedia + // Thumbnail of the file sent; can be ignored if thumbnail generation for + // the file is supported server-side. + // + // optional + Thumb RequestFileData `json:"thumb,omitempty"` // Width video width // // optional - Width int `json:"width"` + Width int `json:"width,omitempty"` // Height video height // // optional - Height int `json:"height"` + Height int `json:"height,omitempty"` // Duration video duration // // optional - Duration int `json:"duration"` + Duration int `json:"duration,omitempty"` // SupportsStreaming pass True, if the uploaded video is suitable for streaming. // // optional - SupportsStreaming bool `json:"supports_streaming"` + SupportsStreaming bool `json:"supports_streaming,omitempty"` } -// InlineQuery is a Query from Telegram for an inline request. -type InlineQuery struct { - // ID unique identifier for this query - ID string `json:"id"` - // From sender - From *User `json:"from"` - // Location sender location, only for bots that request user location. +// InputMediaAnimation is an animation to send as part of a media group. +type InputMediaAnimation struct { + BaseInputMedia + // Thumbnail of the file sent; can be ignored if thumbnail generation for + // the file is supported server-side. // // optional - Location *Location `json:"location"` - // Query text of the query (up to 256 characters). - Query string `json:"query"` - // Offset of the results to be returned, can be controlled by the bot. - Offset string `json:"offset"` -} - -// InlineQueryResultArticle is an inline query response article. -type InlineQueryResultArticle struct { - // Type of the result, must be article. + Thumb RequestFileData `json:"thumb,omitempty"` + // Width video width // - // required - Type string `json:"type"` - // ID unique identifier for this result, 1-64 Bytes. - // - // required - ID string `json:"id"` - // Title of the result + // optional + Width int `json:"width,omitempty"` + // Height video height // - // required - Title string `json:"title"` - // InputMessageContent content of the message to be sent. - // - // required - InputMessageContent interface{} `json:"input_message_content,omitempty"` - // ReplyMarkup Inline keyboard attached to the message. + // optional + Height int `json:"height,omitempty"` + // Duration video duration // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // URL of the result. + Duration int `json:"duration,omitempty"` +} + +// InputMediaAudio is a audio to send as part of a media group. +type InputMediaAudio struct { + BaseInputMedia + // Thumbnail of the file sent; can be ignored if thumbnail generation for + // the file is supported server-side. // // optional - URL string `json:"url"` - // HideURL pass True, if you don't want the URL to be shown in the message. + Thumb RequestFileData `json:"thumb,omitempty"` + // Duration of the audio in seconds // // optional - HideURL bool `json:"hide_url"` - // Description short description of the result. + Duration int `json:"duration,omitempty"` + // Performer of the audio // // optional - Description string `json:"description"` - // ThumbURL url of the thumbnail for the result + Performer string `json:"performer,omitempty"` + // Title of the audio // // optional - ThumbURL string `json:"thumb_url"` - // ThumbWidth thumbnail width + Title string `json:"title,omitempty"` +} + +// InputMediaDocument is a general file to send as part of a media group. +type InputMediaDocument struct { + BaseInputMedia + // Thumbnail of the file sent; can be ignored if thumbnail generation for + // the file is supported server-side. // // optional - ThumbWidth int `json:"thumb_width"` - // ThumbHeight thumbnail height + Thumb RequestFileData `json:"thumb,omitempty"` + // DisableContentTypeDetection disables automatic server-side content type + // detection for files uploaded using multipart/form-data. Always true, if + // the document is sent as part of an album // // optional - ThumbHeight int `json:"thumb_height"` + DisableContentTypeDetection bool `json:"disable_content_type_detection,omitempty"` } -// InlineQueryResultPhoto is an inline query response photo. -type InlineQueryResultPhoto struct { - // Type of the result, must be article. +// Sticker represents a sticker. +type Sticker struct { + // FileID is an identifier for this file, which can be used to download or + // reuse the file + FileID string `json:"file_id"` + // FileUniqueID is an unique identifier for this file, + // which is supposed to be the same over time and for different bots. + // Can't be used to download or reuse the file. + FileUniqueID string `json:"file_unique_id"` + // Width sticker width + Width int `json:"width"` + // Height sticker height + Height int `json:"height"` + // IsAnimated true, if the sticker is animated // - // required - Type string `json:"type"` - // ID unique identifier for this result, 1-64 Bytes. + // optional + IsAnimated bool `json:"is_animated,omitempty"` + // Thumbnail sticker thumbnail in the .WEBP or .JPG format // - // required - ID string `json:"id"` - // URL a valid URL of the photo. Photo must be in jpeg format. - // Photo size must not exceed 5MB. - URL string `json:"photo_url"` - // MimeType - MimeType string `json:"mime_type"` - // Width of the photo + // optional + Thumbnail *PhotoSize `json:"thumb,omitempty"` + // Emoji associated with the sticker // // optional - Width int `json:"photo_width"` - // Height of the photo + Emoji string `json:"emoji,omitempty"` + // SetName of the sticker set to which the sticker belongs // // optional - Height int `json:"photo_height"` - // ThumbURL url of the thumbnail for the photo. + SetName string `json:"set_name,omitempty"` + // MaskPosition is for mask stickers, the position where the mask should be + // placed // // optional - ThumbURL string `json:"thumb_url"` - // Title for the result + MaskPosition *MaskPosition `json:"mask_position,omitempty"` + // FileSize // // optional + FileSize int `json:"file_size,omitempty"` +} + +// StickerSet represents a sticker set. +type StickerSet struct { + // Name sticker set name + Name string `json:"name"` + // Title sticker set title Title string `json:"title"` - // Description short description of the result + // IsAnimated true, if the sticker set contains animated stickers + IsAnimated bool `json:"is_animated"` + // ContainsMasks true, if the sticker set contains masks + ContainsMasks bool `json:"contains_masks"` + // Stickers list of all set stickers + Stickers []Sticker `json:"stickers"` + // Thumb is the sticker set thumbnail in the .WEBP or .TGS format + Thumbnail *PhotoSize `json:"thumb"` +} + +// MaskPosition describes the position on faces where a mask should be placed +// by default. +type MaskPosition struct { + // The part of the face relative to which the mask should be placed. + // One of “forehead”, “eyes”, “mouth”, or “chin”. + Point string `json:"point"` + // Shift by X-axis measured in widths of the mask scaled to the face size, + // from left to right. For example, choosing -1.0 will place mask just to + // the left of the default mask position. + XShift float64 `json:"x_shift"` + // Shift by Y-axis measured in heights of the mask scaled to the face size, + // from top to bottom. For example, 1.0 will place the mask just below the + // default mask position. + YShift float64 `json:"y_shift"` + // Mask scaling coefficient. For example, 2.0 means double size. + Scale float64 `json:"scale"` +} + +// Game represents a game. Use BotFather to create and edit games, their short +// names will act as unique identifiers. +type Game struct { + // Title of the game + Title string `json:"title"` + // Description of the game + Description string `json:"description"` + // Photo that will be displayed in the game message in chats. + Photo []PhotoSize `json:"photo"` + // Text a brief description of the game or high scores included in the game message. + // Can be automatically edited to include current high scores for the game + // when the bot calls setGameScore, or manually edited using editMessageText. 0-4096 characters. // // optional - Description string `json:"description"` - // Caption of the photo to be sent, 0-1024 characters after entities parsing. + Text string `json:"text,omitempty"` + // TextEntities special entities that appear in text, such as usernames, URLs, bot commands, etc. // // optional - Caption string `json:"caption"` - // ParseMode mode for parsing entities in the photo caption. - // See formatting options for more details - // (https://core.telegram.org/bots/api#formatting-options). + TextEntities []MessageEntity `json:"text_entities,omitempty"` + // Animation animation that will be displayed in the game message in chats. + // Upload via BotFather (https://t.me/botfather). // // optional - ParseMode string `json:"parse_mode"` - // ReplyMarkup inline keyboard attached to the message. + Animation Animation `json:"animation,omitempty"` +} + +// GameHighScore is a user's score and position on the leaderboard. +type GameHighScore struct { + // Position in high score table for the game + Position int `json:"position"` + // User user + User User `json:"user"` + // Score score + 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 webhook URL, may be empty if webhook is not set up. + URL string `json:"url"` + // HasCustomCertificate true, if a custom certificate was provided for webhook certificate checks. + HasCustomCertificate bool `json:"has_custom_certificate"` + // PendingUpdateCount number of updates awaiting delivery. + PendingUpdateCount int `json:"pending_update_count"` + // IPAddress is the currently used webhook IP address // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the photo. + IPAddress string `json:"ip_address,omitempty"` + // LastErrorDate unix time for the most recent error + // that happened when trying to deliver an update via webhook. // // optional - InputMessageContent interface{} `json:"input_message_content,omitempty"` -} - -// InlineQueryResultCachedPhoto is an inline query response with cached photo. -type InlineQueryResultCachedPhoto struct { - // Type of the result, must be photo. + LastErrorDate int `json:"last_error_date,omitempty"` + // LastErrorMessage error message in human-readable format for the most recent error + // that happened when trying to deliver an update via webhook. // - // required - Type string `json:"type"` - // ID unique identifier for this result, 1-64 bytes. + // optional + LastErrorMessage string `json:"last_error_message,omitempty"` + // MaxConnections maximum allowed number of simultaneous + // HTTPS connections to the webhook for update delivery. // - // required - ID string `json:"id"` - // PhotoID a valid file identifier of the photo. + // optional + MaxConnections int `json:"max_connections,omitempty"` + // AllowedUpdates is a list of update types the bot is subscribed to. + // Defaults to all update types // - // required - PhotoID string `json:"photo_file_id"` - // Title for the result. + // optional + AllowedUpdates []string `json:"allowed_updates,omitempty"` +} + +// 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 unique identifier for this query + ID string `json:"id"` + // From sender + From *User `json:"from"` + // Query text of the query (up to 256 characters). + Query string `json:"query"` + // Offset of the results to be returned, can be controlled by the bot. + Offset string `json:"offset"` + // Type of the chat, from which the inline query was sent. Can be either + // “sender” for a private chat with the inline query sender, “private”, + // “group”, “supergroup”, or “channel”. The chat type should be always known + // for requests sent from official clients and most third-party clients, + // unless the request was sent from a secret chat // // optional - Title string `json:"title"` - // Description short description of the result. + ChatType string `json:"chat_type"` + // Location sender location, only for bots that request user location. // // optional - Description string `json:"description"` - // Caption of the photo to be sent, 0-1024 characters after entities parsing. + Location *Location `json:"location,omitempty"` +} + +// InlineQueryResultCachedAudio is an inline query response with cached audio. +type InlineQueryResultCachedAudio struct { + // Type of the result, must be audio + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // AudioID a valid file identifier for the audio file + AudioID string `json:"audio_file_id"` + // Caption 0-1024 characters after entities parsing // // optional - Caption string `json:"caption"` - // ParseMode mode for parsing entities in the photo caption. + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the video caption. // See formatting options for more details // (https://core.telegram.org/bots/api#formatting-options). // // optional - ParseMode string `json:"parse_mode"` - // ReplyMarkup inline keyboard attached to the message. + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ReplyMarkup inline keyboard attached to the message // // optional ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the photo. + // InputMessageContent content of the message to be sent instead of the audio // // optional InputMessageContent interface{} `json:"input_message_content,omitempty"` } -// InlineQueryResultGIF is an inline query response GIF. -type InlineQueryResultGIF struct { - // Type of the result, must be gif. - // - // required +// InlineQueryResultCachedDocument is an inline query response with cached document. +type InlineQueryResultCachedDocument struct { + // Type of the result, must be document Type string `json:"type"` - // ID unique identifier for this result, 1-64 bytes. - // - // required + // ID unique identifier for this result, 1-64 bytes ID string `json:"id"` - // URL a valid URL for the GIF file. File size must not exceed 1MB. - // - // required - URL string `json:"gif_url"` - // ThumbURL url of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result. - // - // required - ThumbURL string `json:"thumb_url"` - // Width of the GIF + // DocumentID a valid file identifier for the file + DocumentID string `json:"document_file_id"` + // Title for the result // // optional - Width int `json:"gif_width,omitempty"` - // Height of the GIF + Title string `json:"title,omitempty"` + // Caption of the document to be sent, 0-1024 characters after entities parsing // // optional - Height int `json:"gif_height,omitempty"` - // Duration of the GIF + Caption string `json:"caption,omitempty"` + // Description short description of the result // // optional - Duration int `json:"gif_duration,omitempty"` - // Title for the result + Description string `json:"description,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // // See formatting options for more details + // // (https://core.telegram.org/bots/api#formatting-options). // // optional - Title string `json:"title,omitempty"` - // Caption of the GIF file to be sent, 0-1024 characters after entities parsing. + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode // // optional - Caption string `json:"caption,omitempty"` + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` // ReplyMarkup inline keyboard attached to the message // // optional ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the GIF animation. + // InputMessageContent content of the message to be sent instead of the file // // optional InputMessageContent interface{} `json:"input_message_content,omitempty"`@@ -1493,31 +2136,30 @@
// InlineQueryResultCachedGIF is an inline query response with cached gif. type InlineQueryResultCachedGIF struct { // Type of the result, must be gif. - // - // required Type string `json:"type"` // ID unique identifier for this result, 1-64 bytes. - // - // required ID string `json:"id"` // GifID a valid file identifier for the GIF file. - // - // required - GifID string `json:"gif_file_id"` + GIFID string `json:"gif_file_id"` // Title for the result // // optional - Title string `json:"title"` + Title string `json:"title,omitempty"` // Caption of the GIF file to be sent, 0-1024 characters after entities parsing. // // optional - Caption string `json:"caption"` + Caption string `json:"caption,omitempty"` // ParseMode mode for parsing entities in the caption. // See formatting options for more details // (https://core.telegram.org/bots/api#formatting-options). // // optional - ParseMode string `json:"parse_mode"` + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` // ReplyMarkup inline keyboard attached to the message. // // optional@@ -1528,144 +2170,101 @@ // optional
InputMessageContent interface{} `json:"input_message_content,omitempty"` } -// InlineQueryResultMPEG4GIF is an inline query response MPEG4 GIF. -type InlineQueryResultMPEG4GIF struct { +// InlineQueryResultCachedMPEG4GIF is an inline query response with cached +// H.264/MPEG-4 AVC video without sound gif. +type InlineQueryResultCachedMPEG4GIF struct { // Type of the result, must be mpeg4_gif - // - // required Type string `json:"type"` // ID unique identifier for this result, 1-64 bytes - // - // required ID string `json:"id"` - // URL a valid URL for the MP4 file. File size must not exceed 1MB - // - // required - URL string `json:"mpeg4_url"` - // Width video width + // MPEG4FileID a valid file identifier for the MP4 file + MPEG4FileID string `json:"mpeg4_file_id"` + // Title for the result // // optional - Width int `json:"mpeg4_width"` - // Height vVideo height + Title string `json:"title,omitempty"` + // Caption of the MPEG-4 file to be sent, 0-1024 characters after entities parsing. // // optional - Height int `json:"mpeg4_height"` - // Duration video duration + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). // // optional - Duration int `json:"mpeg4_duration"` - // ThumbURL url of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result. - ThumbURL string `json:"thumb_url"` - // Title for the result - // - // optional - Title string `json:"title"` - // Caption of the MPEG-4 file to be sent, 0-1024 characters after entities parsing. + ParseMode string `json:"parse_mode,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). // // optional - Caption string `json:"caption"` - // ReplyMarkup inline keyboard attached to the message + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ReplyMarkup inline keyboard attached to the message. // // optional ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the video animation + // InputMessageContent content of the message to be sent instead of the video animation. // // optional InputMessageContent interface{} `json:"input_message_content,omitempty"` } -// InlineQueryResultCachedMpeg4Gif is an inline query response with cached -// H.264/MPEG-4 AVC video without sound gif. -type InlineQueryResultCachedMpeg4Gif struct { - // Type of the result, must be mpeg4_gif - // - // required +// InlineQueryResultCachedPhoto is an inline query response with cached photo. +type InlineQueryResultCachedPhoto struct { + // Type of the result, must be photo. Type string `json:"type"` - // ID unique identifier for this result, 1-64 bytes - // - // required + // ID unique identifier for this result, 1-64 bytes. ID string `json:"id"` - // MGifID a valid file identifier for the MP4 file + // PhotoID a valid file identifier of the photo. + PhotoID string `json:"photo_file_id"` + // Title for the result. // - // required - MGifID string `json:"mpeg4_file_id"` - // Title for the result + // optional + Title string `json:"title,omitempty"` + // Description short description of the result. // // optional - Title string `json:"title"` - // Caption of the MPEG-4 file to be sent, 0-1024 characters after entities parsing. + Description string `json:"description,omitempty"` + // Caption of the photo to be sent, 0-1024 characters after entities parsing. // // optional - Caption string `json:"caption"` - // ParseMode mode for parsing entities in the caption. + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the photo caption. // See formatting options for more details // (https://core.telegram.org/bots/api#formatting-options). // // optional - ParseMode string `json:"parse_mode"` + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` // ReplyMarkup inline keyboard attached to the message. // // optional ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the video animation. + // InputMessageContent content of the message to be sent instead of the photo. // // optional InputMessageContent interface{} `json:"input_message_content,omitempty"` } -// InlineQueryResultVideo is an inline query response video. -type InlineQueryResultVideo struct { - // Type of the result, must be video - // - // required +// InlineQueryResultCachedSticker is an inline query response with cached sticker. +type InlineQueryResultCachedSticker struct { + // Type of the result, must be sticker Type string `json:"type"` // ID unique identifier for this result, 1-64 bytes - // - // required ID string `json:"id"` - // URL a valid url for the embedded video player or video file - // - // required - URL string `json:"video_url"` - // MimeType of the content of video url, “text/html” or “video/mp4” - // - // required - MimeType string `json:"mime_type"` - // - // ThumbURL url of the thumbnail (jpeg only) for the video - // optional - ThumbURL string `json:"thumb_url"` - // Title for the result - // - // required + // StickerID a valid file identifier of the sticker + StickerID string `json:"sticker_file_id"` + // Title is a title Title string `json:"title"` - // Caption of the video to be sent, 0-1024 characters after entities parsing - // - // optional - Caption string `json:"caption"` - // Width video width - // - // optional - Width int `json:"video_width"` - // Height video height - // - // optional - Height int `json:"video_height"` - // Duration video duration in seconds - // - // optional - Duration int `json:"video_duration"` - // Description short description of the result - // - // optional - Description string `json:"description"` // ReplyMarkup inline keyboard attached to the message // // optional ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the video. - // This field is required if InlineQueryResultVideo is used to send - // an HTML-page as a result (e.g., a YouTube video). + // InputMessageContent content of the message to be sent instead of the sticker // // optional InputMessageContent interface{} `json:"input_message_content,omitempty"`@@ -1674,35 +2273,32 @@
// InlineQueryResultCachedVideo is an inline query response with cached video. type InlineQueryResultCachedVideo struct { // Type of the result, must be video - // - // required Type string `json:"type"` // ID unique identifier for this result, 1-64 bytes - // - // required ID string `json:"id"` // VideoID a valid file identifier for the video file - // - // required VideoID string `json:"video_file_id"` // Title for the result - // - // required Title string `json:"title"` // Description short description of the result // // optional - Description string `json:"description"` + Description string `json:"description,omitempty"` // Caption of the video to be sent, 0-1024 characters after entities parsing // // optional - Caption string `json:"caption"` + Caption string `json:"caption,omitempty"` // ParseMode mode for parsing entities in the video caption. // See formatting options for more details // (https://core.telegram.org/bots/api#formatting-options). // // optional - ParseMode string `json:"parse_mode"` + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` // ReplyMarkup inline keyboard attached to the message // // optional@@ -1713,216 +2309,173 @@ // optional
InputMessageContent interface{} `json:"input_message_content,omitempty"` } -// InlineQueryResultCachedSticker is an inline query response with cached sticker. -type InlineQueryResultCachedSticker struct { - // Type of the result, must be sticker - // - // required +// InlineQueryResultCachedVoice is an inline query response with cached voice. +type InlineQueryResultCachedVoice struct { + // Type of the result, must be voice Type string `json:"type"` // ID unique identifier for this result, 1-64 bytes - // - // required ID string `json:"id"` - // StickerID a valid file identifier of the sticker + // VoiceID a valid file identifier for the voice message + VoiceID string `json:"voice_file_id"` + // Title voice message title + Title string `json:"title"` + // Caption 0-1024 characters after entities parsing // - // required - StickerID string `json:"sticker_file_id"` - // Title is a title - Title string `json:"title"` + // optional + Caption string `json:"caption,omitempty"` // ParseMode mode for parsing entities in the video caption. // See formatting options for more details // (https://core.telegram.org/bots/api#formatting-options). // // optional - ParseMode string `json:"parse_mode"` + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` // ReplyMarkup inline keyboard attached to the message // // optional ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the sticker + // InputMessageContent content of the message to be sent instead of the voice message // // optional InputMessageContent interface{} `json:"input_message_content,omitempty"` } -// InlineQueryResultAudio is an inline query response audio. -type InlineQueryResultAudio struct { - // Type of the result, must be audio - // - // required +// InlineQueryResultArticle represents a link to an article or web page. +type InlineQueryResultArticle struct { + // Type of the result, must be article. Type string `json:"type"` - // ID unique identifier for this result, 1-64 bytes - // - // required + // ID unique identifier for this result, 1-64 Bytes. ID string `json:"id"` - // URL a valid url for the audio file + // Title of the result + Title string `json:"title"` + // InputMessageContent content of the message to be sent. + InputMessageContent interface{} `json:"input_message_content,omitempty"` + // ReplyMarkup Inline keyboard attached to the message. // - // required - URL string `json:"audio_url"` - // Title is a title + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // URL of the result. // - // required - Title string `json:"title"` - // Caption 0-1024 characters after entities parsing + // optional + URL string `json:"url,omitempty"` + // HideURL pass True, if you don't want the URL to be shown in the message. // // optional - Caption string `json:"caption"` - // Performer is a performer + HideURL bool `json:"hide_url,omitempty"` + // Description short description of the result. // // optional - Performer string `json:"performer"` - // Duration audio duration in seconds + Description string `json:"description,omitempty"` + // ThumbURL url of the thumbnail for the result // // optional - Duration int `json:"audio_duration"` - // ReplyMarkup inline keyboard attached to the message + ThumbURL string `json:"thumb_url,omitempty"` + // ThumbWidth thumbnail width // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the audio + ThumbWidth int `json:"thumb_width,omitempty"` + // ThumbHeight thumbnail height // // optional - InputMessageContent interface{} `json:"input_message_content,omitempty"` + ThumbHeight int `json:"thumb_height,omitempty"` } -// InlineQueryResultCachedAudio is an inline query response with cached audio. -type InlineQueryResultCachedAudio struct { +// InlineQueryResultAudio is an inline query response audio. +type InlineQueryResultAudio struct { // Type of the result, must be audio - // - // required Type string `json:"type"` // ID unique identifier for this result, 1-64 bytes - // - // required ID string `json:"id"` - // AudioID a valid file identifier for the audio file - // - // required - AudioID string `json:"audio_file_id"` + // URL a valid url for the audio file + URL string `json:"audio_url"` + // Title is a title + Title string `json:"title"` // Caption 0-1024 characters after entities parsing // // optional - Caption string `json:"caption"` + Caption string `json:"caption,omitempty"` // ParseMode mode for parsing entities in the video caption. // See formatting options for more details // (https://core.telegram.org/bots/api#formatting-options). // // optional - ParseMode string `json:"parse_mode"` - // ReplyMarkup inline keyboard attached to the message + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode // // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the audio + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // Performer is a performer // // optional - InputMessageContent interface{} `json:"input_message_content,omitempty"` -} - -// InlineQueryResultVoice is an inline query response voice. -type InlineQueryResultVoice struct { - // Type of the result, must be voice - // - // required - Type string `json:"type"` - // ID unique identifier for this result, 1-64 bytes - // - // required - ID string `json:"id"` - // URL a valid URL for the voice recording - // - // required - URL string `json:"voice_url"` - // Title recording title - // - // required - Title string `json:"title"` - // Caption 0-1024 characters after entities parsing + Performer string `json:"performer,omitempty"` + // Duration audio duration in seconds // // optional - Caption string `json:"caption"` - // Duration recording duration in seconds - // - // optional - Duration int `json:"voice_duration"` + Duration int `json:"audio_duration,omitempty"` // ReplyMarkup inline keyboard attached to the message // // optional ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the voice recording + // InputMessageContent content of the message to be sent instead of the audio // // optional InputMessageContent interface{} `json:"input_message_content,omitempty"` } -// InlineQueryResultCachedVoice is an inline query response with cached voice. -type InlineQueryResultCachedVoice struct { - // Type of the result, must be voice - // - // required +// InlineQueryResultContact is an inline query response contact. +type InlineQueryResultContact struct { + Type string `json:"type"` // required + ID string `json:"id"` // required + PhoneNumber string `json:"phone_number"` // required + FirstName string `json:"first_name"` // required + LastName string `json:"last_name"` + VCard string `json:"vcard"` + 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 of the result, must be game Type string `json:"type"` // ID unique identifier for this result, 1-64 bytes - // - // required ID string `json:"id"` - // VoiceID a valid file identifier for the voice message - // - // required - VoiceID string `json:"voice_file_id"` - // Title voice message title - // - // required - Title string `json:"title"` - // Caption 0-1024 characters after entities parsing - // - // optional - Caption string `json:"caption"` - // ParseMode mode for parsing entities in the video caption. - // See formatting options for more details - // (https://core.telegram.org/bots/api#formatting-options). - // - // optional - ParseMode string `json:"parse_mode"` + // GameShortName short name of the game + GameShortName string `json:"game_short_name"` // ReplyMarkup inline keyboard attached to the message // // optional ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the voice message - // - // optional - InputMessageContent interface{} `json:"input_message_content,omitempty"` } // InlineQueryResultDocument is an inline query response document. type InlineQueryResultDocument struct { // Type of the result, must be document - // - // required Type string `json:"type"` // ID unique identifier for this result, 1-64 bytes - // - // required ID string `json:"id"` // Title for the result - // - // required Title string `json:"title"` // Caption of the document to be sent, 0-1024 characters after entities parsing // // optional - Caption string `json:"caption"` + Caption string `json:"caption,omitempty"` // URL a valid url for the file - // - // required URL string `json:"document_url"` // MimeType of the content of the file, either “application/pdf” or “application/zip” - // - // required MimeType string `json:"mime_type"` // Description short description of the result // // optional - Description string `json:"description"` + Description string `json:"description,omitempty"` // ReplyMarkup nline keyboard attached to the message // // optional@@ -1934,54 +2487,63 @@ InputMessageContent interface{} `json:"input_message_content,omitempty"`
// ThumbURL url of the thumbnail (jpeg only) for the file // // optional - ThumbURL string `json:"thumb_url"` + ThumbURL string `json:"thumb_url,omitempty"` // ThumbWidth thumbnail width // // optional - ThumbWidth int `json:"thumb_width"` + ThumbWidth int `json:"thumb_width,omitempty"` // ThumbHeight thumbnail height // // optional - ThumbHeight int `json:"thumb_height"` + ThumbHeight int `json:"thumb_height,omitempty"` } -// InlineQueryResultCachedDocument is an inline query response with cached document. -type InlineQueryResultCachedDocument struct { - // Type of the result, must be document +// InlineQueryResultGIF is an inline query response GIF. +type InlineQueryResultGIF struct { + // Type of the result, must be gif. + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes. + ID string `json:"id"` + // URL a valid URL for the GIF file. File size must not exceed 1MB. + URL string `json:"gif_url"` + // ThumbURL url of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result. + ThumbURL string `json:"thumb_url"` + // Width of the GIF // - // required - Type string `json:"type"` - // ID unique identifier for this result, 1-64 bytes + // optional + Width int `json:"gif_width,omitempty"` + // Height of the GIF // - // required - ID string `json:"id"` - // DocumentID a valid file identifier for the file + // optional + Height int `json:"gif_height,omitempty"` + // Duration of the GIF // - // required - DocumentID string `json:"document_file_id"` + // optional + Duration int `json:"gif_duration,omitempty"` // Title for the result // // optional - Title string `json:"title"` // required - // Caption of the document to be sent, 0-1024 characters after entities parsing + Title string `json:"title,omitempty"` + // Caption of the GIF file to be sent, 0-1024 characters after entities parsing. // // optional - Caption string `json:"caption"` - // Description short description of the result + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). // // optional - Description string `json:"description"` - // ParseMode mode for parsing entities in the video caption. - // // See formatting options for more details - // // (https://core.telegram.org/bots/api#formatting-options). + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode // // optional - ParseMode string `json:"parse_mode"` + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` // ReplyMarkup inline keyboard attached to the message // // optional ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - // InputMessageContent content of the message to be sent instead of the file + // InputMessageContent content of the message to be sent instead of the GIF animation. // // optional InputMessageContent interface{} `json:"input_message_content,omitempty"`@@ -1990,25 +2552,36 @@
// InlineQueryResultLocation is an inline query response location. type InlineQueryResultLocation struct { // Type of the result, must be location - // - // required Type string `json:"type"` // ID unique identifier for this result, 1-64 Bytes - // - // required ID string `json:"id"` // Latitude of the location in degrees - // - // required Latitude float64 `json:"latitude"` // Longitude of the location in degrees - // - // required Longitude float64 `json:"longitude"` // Title of the location + Title string `json:"title"` + // HorizontalAccuracy is the radius of uncertainty for the location, + // measured in meters; 0-1500 // - // required - Title string `json:"title"` + // optional + HorizontalAccuracy float64 `json:"horizontal_accuracy"` + // LivePeriod is the period in seconds for which the location can be + // updated, should be between 60 and 86400. + // + // optional + LivePeriod int `json:"live_period"` + // Heading is for live locations, a direction in which the user is moving, + // in degrees. Must be between 1 and 360 if specified. + // + // optional + Heading int `json:"heading"` + // ProximityAlertRadius is for live locations, a maximum distance for + // proximity alerts about approaching another chat member, in meters. Must + // be between 1 and 100000 if specified. + // + // optional + ProximityAlertRadius int `json:"proximity_alert_radius"` // ReplyMarkup inline keyboard attached to the message // // optional@@ -2020,52 +2593,155 @@ InputMessageContent interface{} `json:"input_message_content,omitempty"`
// ThumbURL url of the thumbnail for the result // // optional - ThumbURL string `json:"thumb_url"` + ThumbURL string `json:"thumb_url,omitempty"` // ThumbWidth thumbnail width // // optional - ThumbWidth int `json:"thumb_width"` + ThumbWidth int `json:"thumb_width,omitempty"` // ThumbHeight thumbnail height // // optional - ThumbHeight int `json:"thumb_height"` + ThumbHeight int `json:"thumb_height,omitempty"` +} + +// InlineQueryResultMPEG4GIF is an inline query response MPEG4 GIF. +type InlineQueryResultMPEG4GIF struct { + // Type of the result, must be mpeg4_gif + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // URL a valid URL for the MP4 file. File size must not exceed 1MB + URL string `json:"mpeg4_url"` + // Width video width + // + // optional + Width int `json:"mpeg4_width"` + // Height vVideo height + // + // optional + Height int `json:"mpeg4_height"` + // Duration video duration + // + // optional + Duration int `json:"mpeg4_duration"` + // ThumbURL url of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result. + ThumbURL string `json:"thumb_url"` + // Title for the result + // + // optional + Title string `json:"title,omitempty"` + // Caption of the MPEG-4 file to be sent, 0-1024 characters after entities parsing. + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the video animation + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultPhoto is an inline query response photo. +type InlineQueryResultPhoto struct { + // Type of the result, must be article. + Type string `json:"type"` + // ID unique identifier for this result, 1-64 Bytes. + ID string `json:"id"` + // URL a valid URL of the photo. Photo must be in jpeg format. + // Photo size must not exceed 5MB. + URL string `json:"photo_url"` + // MimeType + MimeType string `json:"mime_type"` + // Width of the photo + // + // optional + Width int `json:"photo_width,omitempty"` + // Height of the photo + // + // optional + Height int `json:"photo_height,omitempty"` + // ThumbURL url of the thumbnail for the photo. + // + // optional + ThumbURL string `json:"thumb_url,omitempty"` + // Title for the result + // + // optional + Title string `json:"title,omitempty"` + // Description short description of the result + // + // optional + Description string `json:"description,omitempty"` + // Caption of the photo to be sent, 0-1024 characters after entities parsing. + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the photo caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // ReplyMarkup inline keyboard attached to the message. + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // InputMessageContent content of the message to be sent instead of the photo. + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` } // InlineQueryResultVenue is an inline query response venue. type InlineQueryResultVenue struct { // Type of the result, must be venue - // - // required Type string `json:"type"` // ID unique identifier for this result, 1-64 Bytes - // - // required ID string `json:"id"` // Latitude of the venue location in degrees - // - // required Latitude float64 `json:"latitude"` // Longitude of the venue location in degrees - // - // required Longitude float64 `json:"longitude"` // Title of the venue - // - // required Title string `json:"title"` // Address of the venue - // - // required Address string `json:"address"` // FoursquareID foursquare identifier of the venue if known // // optional - FoursquareID string `json:"foursquare_id"` + FoursquareID string `json:"foursquare_id,omitempty"` // FoursquareType foursquare type of the venue, if known. // (For example, “arts_entertainment/default”, “arts_entertainment/aquarium” or “food/icecream”.) // // optional - FoursquareType string `json:"foursquare_type"` + FoursquareType string `json:"foursquare_type,omitempty"` + // GooglePlaceID is the Google Places identifier of the venue + // + // optional + GooglePlaceID string `json:"google_place_id,omitempty"` + // GooglePlaceType is the Google Places type of the venue + // + // optional + GooglePlaceType string `json:"google_place_type,omitempty"` // ReplyMarkup inline keyboard attached to the message // // optional@@ -2077,35 +2753,102 @@ InputMessageContent interface{} `json:"input_message_content,omitempty"`
// ThumbURL url of the thumbnail for the result // // optional - ThumbURL string `json:"thumb_url"` + ThumbURL string `json:"thumb_url,omitempty"` // ThumbWidth thumbnail width // // optional - ThumbWidth int `json:"thumb_width"` + ThumbWidth int `json:"thumb_width,omitempty"` // ThumbHeight thumbnail height // // optional - ThumbHeight int `json:"thumb_height"` + ThumbHeight int `json:"thumb_height,omitempty"` } -// InlineQueryResultGame is an inline query response game. -type InlineQueryResultGame struct { - // Type of the result, must be game - // - // required +// InlineQueryResultVideo is an inline query response video. +type InlineQueryResultVideo struct { + // Type of the result, must be video Type string `json:"type"` // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // URL a valid url for the embedded video player or video file + URL string `json:"video_url"` + // MimeType of the content of video url, “text/html” or “video/mp4” + MimeType string `json:"mime_type"` // - // required + // ThumbURL url of the thumbnail (jpeg only) for the video + // optional + ThumbURL string `json:"thumb_url,omitempty"` + // Title for the result + Title string `json:"title"` + // Caption of the video to be sent, 0-1024 characters after entities parsing + // + // optional + Caption string `json:"caption,omitempty"` + // Width video width + // + // optional + Width int `json:"video_width,omitempty"` + // Height video height + // + // optional + Height int `json:"video_height,omitempty"` + // Duration video duration in seconds + // + // optional + Duration int `json:"video_duration,omitempty"` + // Description short description of the result + // + // optional + Description string `json:"description,omitempty"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the video. + // This field is required if InlineQueryResultVideo is used to send + // an HTML-page as a result (e.g., a YouTube video). + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultVoice is an inline query response voice. +type InlineQueryResultVoice struct { + // Type of the result, must be voice + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes ID string `json:"id"` - // GameShortName short name of the game + // URL a valid URL for the voice recording + URL string `json:"voice_url"` + // Title recording title + Title string `json:"title"` + // Caption 0-1024 characters after entities parsing + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode // - // required - GameShortName string `json:"game_short_name"` + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // Duration recording duration in seconds + // + // optional + Duration int `json:"voice_duration,omitempty"` // ReplyMarkup inline keyboard attached to the message // // optional ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the voice recording + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` } // ChosenInlineResult is an inline query result chosen by a User@@ -2117,13 +2860,13 @@ From *User `json:"from"`
// Location sender location, only for bots that require user location // // optional - Location *Location `json:"location"` + Location *Location `json:"location,omitempty"` // InlineMessageID identifier of the sent inline message. // Available only if there is an inline keyboard attached to the message. // Will be also received in callback queries and can be used to edit the message. // // optional - InlineMessageID string `json:"inline_message_id"` + InlineMessageID string `json:"inline_message_id,omitempty"` // Query the query that was used to obtain the result Query string `json:"query"` }@@ -2138,11 +2881,16 @@ // See formatting options for more details
// (https://core.telegram.org/bots/api#formatting-options). // // optional - ParseMode string `json:"parse_mode"` + ParseMode string `json:"parse_mode,omitempty"` + // Entities is a list of special entities that appear in message text, which + // can be specified instead of parse_mode + // + // optional + Entities []MessageEntity `json:"entities,omitempty"` // DisableWebPagePreview disables link previews for links in the sent message // // optional - DisableWebPagePreview bool `json:"disable_web_page_preview"` + DisableWebPagePreview bool `json:"disable_web_page_preview,omitempty"` } // InputLocationMessageContent contains a location for displaying@@ -2152,6 +2900,27 @@ // Latitude of the location in degrees
Latitude float64 `json:"latitude"` // Longitude of the location in degrees Longitude float64 `json:"longitude"` + // HorizontalAccuracy is the radius of uncertainty for the location, + // measured in meters; 0-1500 + // + // optional + HorizontalAccuracy float64 `json:"horizontal_accuracy"` + // LivePeriod is the period in seconds for which the location can be + // updated, should be between 60 and 86400 + // + // optional + LivePeriod int `json:"live_period,omitempty"` + // Heading is for live locations, a direction in which the user is moving, + // in degrees. Must be between 1 and 360 if specified. + // + // optional + Heading int `json:"heading"` + // ProximityAlertRadius is for live locations, a maximum distance for + // proximity alerts about approaching another chat member, in meters. Must + // be between 1 and 100000 if specified. + // + // optional + ProximityAlertRadius int `json:"proximity_alert_radius"` } // InputVenueMessageContent contains a venue for displaying@@ -2168,7 +2937,19 @@ Address string `json:"address"`
// FoursquareID foursquare identifier of the venue, if known // // optional - FoursquareID string `json:"foursquare_id"` + FoursquareID string `json:"foursquare_id,omitempty"` + // FoursquareType Foursquare type of the venue, if known + // + // optional + FoursquareType string `json:"foursquare_type,omitempty"` + // GooglePlaceID is the Google Places identifier of the venue + // + // optional + GooglePlaceID string `json:"google_place_id"` + // GooglePlaceType is the Google Places type of the venue + // + // optional + GooglePlaceType string `json:"google_place_type"` } // InputContactMessageContent contains a contact for displaying@@ -2181,7 +2962,107 @@ FirstName string `json:"first_name"`
// LastName contact's last name // // optional - LastName string `json:"last_name"` + LastName string `json:"last_name,omitempty"` + // Additional data about the contact in the form of a vCard + // + // optional + VCard string `json:"vcard,omitempty"` +} + +// InputInvoiceMessageContent represents the content of an invoice message to be +// sent as the result of an inline query. +type InputInvoiceMessageContent struct { + // Product name, 1-32 characters + Title string `json:"title"` + // Product description, 1-255 characters + Description string `json:"description"` + // Bot-defined invoice payload, 1-128 bytes. This will not be displayed to + // the user, use for your internal processes. + Payload string `json:"payload"` + // Payment provider token, obtained via Botfather + ProviderToken string `json:"provider_token"` + // Three-letter ISO 4217 currency code + Currency string `json:"currency"` + // Price breakdown, a JSON-serialized list of components (e.g. product + // price, tax, discount, delivery cost, delivery tax, bonus, etc.) + Prices []LabeledPrice `json:"prices"` + // The maximum accepted amount for tips in the smallest units of the + // currency (integer, not float/double). + // + // optional + MaxTipAmount int `json:"max_tip_amount,omitempty"` + // An array of suggested amounts of tip in the smallest units of the + // currency (integer, not float/double). At most 4 suggested tip amounts can + // be specified. The suggested tip amounts must be positive, passed in a + // strictly increased order and must not exceed max_tip_amount. + // + // optional + SuggestedTipAmounts []int `json:"suggested_tip_amounts,omitempty"` + // A JSON-serialized object for data about the invoice, which will be shared + // with the payment provider. A detailed description of the required fields + // should be provided by the payment provider. + // + // optional + ProviderData string `json:"provider_data,omitempty"` + // URL of the product photo for the invoice. Can be a photo of the goods or + // a marketing image for a service. People like it better when they see what + // they are paying for. + // + // optional + PhotoURL string `json:"photo_url,omitempty"` + // Photo size + // + // optional + PhotoSize int `json:"photo_size,omitempty"` + // Photo width + // + // optional + PhotoWidth int `json:"photo_width,omitempty"` + // Photo height + // + // optional + PhotoHeight int `json:"photo_height,omitempty"` + // Pass True, if you require the user's full name to complete the order + // + // optional + NeedName bool `json:"need_name,omitempty"` + // Pass True, if you require the user's phone number to complete the order + // + // optional + NeedPhoneNumber bool `json:"need_phone_number,omitempty"` + // Pass True, if you require the user's email address to complete the order + // + // optional + NeedEmail bool `json:"need_email,omitempty"` + // Pass True, if you require the user's shipping address to complete the order + // + // optional + NeedShippingAddress bool `json:"need_shipping_address,omitempty"` + // Pass True, if user's phone number should be sent to provider + // + // optional + SendPhoneNumberToProvider bool `json:"send_phone_number_to_provider,omitempty"` + // Pass True, if user's email address should be sent to provider + // + // optional + SendEmailToProvider bool `json:"send_email_to_provider,omitempty"` + // Pass True, if the final price depends on the shipping method + // + // optional + IsFlexible bool `json:"is_flexible,omitempty"` +} + +// LabeledPrice represents a portion of the price for goods or services. +type LabeledPrice struct { + // Label portion label + Label string `json:"label"` + // Amount price of the product in the smallest units of the currency (integer, not float/double). + // For example, for a price of US$ 1.45 pass amount = 145. + // See the exp parameter in currencies.json + // (https://core.telegram.org/bots/payments/currencies.json), + // it shows the number of digits past the decimal point + // for each currency (2 for the majority of currencies). + Amount int `json:"amount"` } // Invoice contains basic information about an invoice.@@ -2204,19 +3085,6 @@ // for each currency (2 for the majority of currencies).
TotalAmount int `json:"total_amount"` } -// LabeledPrice represents a portion of the price for goods or services. -type LabeledPrice struct { - // Label portion label - Label string `json:"label"` - // Amount price of the product in the smallest units of the currency (integer, not float/double). - // For example, for a price of US$ 1.45 pass amount = 145. - // See the exp parameter in currencies.json - // (https://core.telegram.org/bots/payments/currencies.json), - // it shows the number of digits past the decimal point - // for each currency (2 for the majority of currencies). - Amount int `json:"amount"` -} - // ShippingAddress represents a shipping address. type ShippingAddress struct { // CountryCode ISO 3166-1 alpha-2 country code@@ -2260,7 +3128,7 @@ ID string `json:"id"`
// Title option title Title string `json:"title"` // Prices list of price portions - Prices *[]LabeledPrice `json:"prices"` + Prices []LabeledPrice `json:"prices"` } // SuccessfulPayment contains basic information about a successful payment.@@ -2330,23 +3198,3 @@ //
// optional OrderInfo *OrderInfo `json:"order_info,omitempty"` } - -// Error is an error containing extra information returned by the Telegram API. -type Error struct { - Code int - Message string - ResponseParameters -} - -func (e Error) Error() string { - return e.Message -} - -// BotCommand represents a bot command. -type BotCommand struct { - // Command text of the command, 1-32 characters. - // Can contain only lowercase English letters, digits and underscores. - Command string `json:"command"` - // Description of the command, 3-256 characters. - Description string `json:"description"` -}
@@ -1,14 +1,12 @@
-package tgbotapi_test +package tgbotapi import ( "testing" "time" - - "github.com/go-telegram-bot-api/telegram-bot-api" ) func TestUserStringWith(t *testing.T) { - user := tgbotapi.User{ + user := User{ ID: 0, FirstName: "Test", LastName: "Test",@@ -23,7 +21,7 @@ }
} func TestUserStringWithUserName(t *testing.T) { - user := tgbotapi.User{ + user := User{ ID: 0, FirstName: "Test", LastName: "Test",@@ -37,7 +35,7 @@ }
} func TestMessageTime(t *testing.T) { - message := tgbotapi.Message{Date: 0} + message := Message{Date: 0} date := time.Unix(0, 0) if message.Time() != date {@@ -46,33 +44,33 @@ }
} func TestMessageIsCommandWithCommand(t *testing.T) { - message := tgbotapi.Message{Text: "/command"} - message.Entities = &[]tgbotapi.MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}} + message := Message{Text: "/command"} + message.Entities = []MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}} - if message.IsCommand() != true { + if !message.IsCommand() { t.Fail() } } func TestIsCommandWithText(t *testing.T) { - message := tgbotapi.Message{Text: "some text"} + message := Message{Text: "some text"} - if message.IsCommand() != false { + if message.IsCommand() { t.Fail() } } func TestIsCommandWithEmptyText(t *testing.T) { - message := tgbotapi.Message{Text: ""} + message := Message{Text: ""} - if message.IsCommand() != false { + if message.IsCommand() { t.Fail() } } func TestCommandWithCommand(t *testing.T) { - message := tgbotapi.Message{Text: "/command"} - message.Entities = &[]tgbotapi.MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}} + message := Message{Text: "/command"} + message.Entities = []MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}} if message.Command() != "command" { t.Fail()@@ -80,7 +78,7 @@ }
} func TestCommandWithEmptyText(t *testing.T) { - message := tgbotapi.Message{Text: ""} + message := Message{Text: ""} if message.Command() != "" { t.Fail()@@ -88,7 +86,7 @@ }
} func TestCommandWithNonCommand(t *testing.T) { - message := tgbotapi.Message{Text: "test text"} + message := Message{Text: "test text"} if message.Command() != "" { t.Fail()@@ -96,8 +94,8 @@ }
} func TestCommandWithBotName(t *testing.T) { - message := tgbotapi.Message{Text: "/command@testbot"} - message.Entities = &[]tgbotapi.MessageEntity{{Type: "bot_command", Offset: 0, Length: 16}} + message := Message{Text: "/command@testbot"} + message.Entities = []MessageEntity{{Type: "bot_command", Offset: 0, Length: 16}} if message.Command() != "command" { t.Fail()@@ -105,8 +103,8 @@ }
} func TestCommandWithAtWithBotName(t *testing.T) { - message := tgbotapi.Message{Text: "/command@testbot"} - message.Entities = &[]tgbotapi.MessageEntity{{Type: "bot_command", Offset: 0, Length: 16}} + message := Message{Text: "/command@testbot"} + message.Entities = []MessageEntity{{Type: "bot_command", Offset: 0, Length: 16}} if message.CommandWithAt() != "command@testbot" { t.Fail()@@ -114,37 +112,37 @@ }
} func TestMessageCommandArgumentsWithArguments(t *testing.T) { - message := tgbotapi.Message{Text: "/command with arguments"} - message.Entities = &[]tgbotapi.MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}} + message := Message{Text: "/command with arguments"} + message.Entities = []MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}} if message.CommandArguments() != "with arguments" { t.Fail() } } func TestMessageCommandArgumentsWithMalformedArguments(t *testing.T) { - message := tgbotapi.Message{Text: "/command-without argument space"} - message.Entities = &[]tgbotapi.MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}} + message := Message{Text: "/command-without argument space"} + message.Entities = []MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}} if message.CommandArguments() != "without argument space" { t.Fail() } } func TestMessageCommandArgumentsWithoutArguments(t *testing.T) { - message := tgbotapi.Message{Text: "/command"} + message := Message{Text: "/command"} if message.CommandArguments() != "" { t.Fail() } } func TestMessageCommandArgumentsForNonCommand(t *testing.T) { - message := tgbotapi.Message{Text: "test text"} + message := Message{Text: "test text"} if message.CommandArguments() != "" { t.Fail() } } func TestMessageEntityParseURLGood(t *testing.T) { - entity := tgbotapi.MessageEntity{URL: "https://www.google.com"} + entity := MessageEntity{URL: "https://www.google.com"} if _, err := entity.ParseURL(); err != nil { t.Fail()@@ -152,7 +150,7 @@ }
} func TestMessageEntityParseURLBad(t *testing.T) { - entity := tgbotapi.MessageEntity{URL: ""} + entity := MessageEntity{URL: ""} if _, err := entity.ParseURL(); err == nil { t.Fail()@@ -160,31 +158,31 @@ }
} func TestChatIsPrivate(t *testing.T) { - chat := tgbotapi.Chat{ID: 10, Type: "private"} + chat := Chat{ID: 10, Type: "private"} - if chat.IsPrivate() != true { + if !chat.IsPrivate() { t.Fail() } } func TestChatIsGroup(t *testing.T) { - chat := tgbotapi.Chat{ID: 10, Type: "group"} + chat := Chat{ID: 10, Type: "group"} - if chat.IsGroup() != true { + if !chat.IsGroup() { t.Fail() } } func TestChatIsChannel(t *testing.T) { - chat := tgbotapi.Chat{ID: 10, Type: "channel"} + chat := Chat{ID: 10, Type: "channel"} - if chat.IsChannel() != true { + if !chat.IsChannel() { t.Fail() } } func TestChatIsSuperGroup(t *testing.T) { - chat := tgbotapi.Chat{ID: 10, Type: "supergroup"} + chat := Chat{ID: 10, Type: "supergroup"} if !chat.IsSuperGroup() { t.Fail()@@ -192,7 +190,7 @@ }
} func TestMessageEntityIsMention(t *testing.T) { - entity := tgbotapi.MessageEntity{Type: "mention"} + entity := MessageEntity{Type: "mention"} if !entity.IsMention() { t.Fail()@@ -200,7 +198,7 @@ }
} func TestMessageEntityIsHashtag(t *testing.T) { - entity := tgbotapi.MessageEntity{Type: "hashtag"} + entity := MessageEntity{Type: "hashtag"} if !entity.IsHashtag() { t.Fail()@@ -208,7 +206,7 @@ }
} func TestMessageEntityIsBotCommand(t *testing.T) { - entity := tgbotapi.MessageEntity{Type: "bot_command"} + entity := MessageEntity{Type: "bot_command"} if !entity.IsCommand() { t.Fail()@@ -216,15 +214,15 @@ }
} func TestMessageEntityIsUrl(t *testing.T) { - entity := tgbotapi.MessageEntity{Type: "url"} + entity := MessageEntity{Type: "url"} - if !entity.IsUrl() { + if !entity.IsURL() { t.Fail() } } func TestMessageEntityIsEmail(t *testing.T) { - entity := tgbotapi.MessageEntity{Type: "email"} + entity := MessageEntity{Type: "email"} if !entity.IsEmail() { t.Fail()@@ -232,7 +230,7 @@ }
} func TestMessageEntityIsBold(t *testing.T) { - entity := tgbotapi.MessageEntity{Type: "bold"} + entity := MessageEntity{Type: "bold"} if !entity.IsBold() { t.Fail()@@ -240,7 +238,7 @@ }
} func TestMessageEntityIsItalic(t *testing.T) { - entity := tgbotapi.MessageEntity{Type: "italic"} + entity := MessageEntity{Type: "italic"} if !entity.IsItalic() { t.Fail()@@ -248,7 +246,7 @@ }
} func TestMessageEntityIsCode(t *testing.T) { - entity := tgbotapi.MessageEntity{Type: "code"} + entity := MessageEntity{Type: "code"} if !entity.IsCode() { t.Fail()@@ -256,7 +254,7 @@ }
} func TestMessageEntityIsPre(t *testing.T) { - entity := tgbotapi.MessageEntity{Type: "pre"} + entity := MessageEntity{Type: "pre"} if !entity.IsPre() { t.Fail()@@ -264,7 +262,7 @@ }
} func TestMessageEntityIsTextLink(t *testing.T) { - entity := tgbotapi.MessageEntity{Type: "text_link"} + entity := MessageEntity{Type: "text_link"} if !entity.IsTextLink() { t.Fail()@@ -272,9 +270,104 @@ }
} func TestFileLink(t *testing.T) { - file := tgbotapi.File{FilePath: "test/test.txt"} + file := File{FilePath: "test/test.txt"} if file.Link("token") != "https://api.telegram.org/file/bottoken/test/test.txt" { t.Fail() } } + +// Ensure all configs are sendable +var ( + _ Chattable = AnimationConfig{} + _ Chattable = AudioConfig{} + _ Chattable = CallbackConfig{} + _ Chattable = ChatActionConfig{} + _ Chattable = ChatAdministratorsConfig{} + _ Chattable = ChatInfoConfig{} + _ Chattable = ChatInviteLinkConfig{} + _ Chattable = CloseConfig{} + _ Chattable = ContactConfig{} + _ Chattable = CopyMessageConfig{} + _ Chattable = CreateChatInviteLinkConfig{} + _ Chattable = DeleteChatPhotoConfig{} + _ Chattable = DeleteChatStickerSetConfig{} + _ Chattable = DeleteMessageConfig{} + _ Chattable = DeleteMyCommandsConfig{} + _ Chattable = DeleteWebhookConfig{} + _ Chattable = DocumentConfig{} + _ Chattable = EditChatInviteLinkConfig{} + _ Chattable = EditMessageCaptionConfig{} + _ Chattable = EditMessageLiveLocationConfig{} + _ Chattable = EditMessageMediaConfig{} + _ Chattable = EditMessageReplyMarkupConfig{} + _ Chattable = EditMessageTextConfig{} + _ Chattable = FileConfig{} + _ Chattable = ForwardConfig{} + _ Chattable = GameConfig{} + _ Chattable = GetChatMemberConfig{} + _ Chattable = GetGameHighScoresConfig{} + _ Chattable = InlineConfig{} + _ Chattable = InvoiceConfig{} + _ Chattable = KickChatMemberConfig{} + _ Chattable = LeaveChatConfig{} + _ Chattable = LocationConfig{} + _ Chattable = LogOutConfig{} + _ Chattable = MediaGroupConfig{} + _ Chattable = MessageConfig{} + _ Chattable = PhotoConfig{} + _ Chattable = PinChatMessageConfig{} + _ Chattable = PreCheckoutConfig{} + _ Chattable = PromoteChatMemberConfig{} + _ Chattable = RestrictChatMemberConfig{} + _ Chattable = RevokeChatInviteLinkConfig{} + _ Chattable = SendPollConfig{} + _ Chattable = SetChatDescriptionConfig{} + _ Chattable = SetChatPhotoConfig{} + _ Chattable = SetChatTitleConfig{} + _ Chattable = SetGameScoreConfig{} + _ Chattable = ShippingConfig{} + _ Chattable = StickerConfig{} + _ Chattable = StopMessageLiveLocationConfig{} + _ Chattable = StopPollConfig{} + _ Chattable = UnbanChatMemberConfig{} + _ Chattable = UnpinChatMessageConfig{} + _ Chattable = UpdateConfig{} + _ Chattable = UserProfilePhotosConfig{} + _ Chattable = VenueConfig{} + _ Chattable = VideoConfig{} + _ Chattable = VideoNoteConfig{} + _ Chattable = VoiceConfig{} + _ Chattable = WebhookConfig{} +) + +// Ensure all Fileable types are correct. +var ( + _ Fileable = (*PhotoConfig)(nil) + _ Fileable = (*AudioConfig)(nil) + _ Fileable = (*DocumentConfig)(nil) + _ Fileable = (*StickerConfig)(nil) + _ Fileable = (*VideoConfig)(nil) + _ Fileable = (*AnimationConfig)(nil) + _ Fileable = (*VideoNoteConfig)(nil) + _ Fileable = (*VoiceConfig)(nil) + _ Fileable = (*SetChatPhotoConfig)(nil) + _ Fileable = (*EditMessageMediaConfig)(nil) + _ Fileable = (*SetChatPhotoConfig)(nil) + _ Fileable = (*UploadStickerConfig)(nil) + _ Fileable = (*NewStickerSetConfig)(nil) + _ Fileable = (*AddStickerConfig)(nil) + _ Fileable = (*MediaGroupConfig)(nil) + _ Fileable = (*WebhookConfig)(nil) + _ Fileable = (*SetStickerSetThumbConfig)(nil) +) + +// Ensure all RequestFileData types are correct. +var ( + _ RequestFileData = (*FilePath)(nil) + _ RequestFileData = (*FileBytes)(nil) + _ RequestFileData = (*FileReader)(nil) + _ RequestFileData = (*FileURL)(nil) + _ RequestFileData = (*FileID)(nil) + _ RequestFileData = (*fileAttach)(nil) +)