├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── book.toml ├── bot.go ├── bot_test.go ├── configs.go ├── docs ├── SUMMARY.md ├── changelog.md ├── examples │ ├── README.md │ ├── command-handling.md │ ├── inline-keyboard.md │ └── keyboard.md ├── getting-started │ ├── README.md │ ├── files.md │ ├── important-notes.md │ └── library-structure.md └── internals │ ├── README.md │ ├── adding-endpoints.md │ └── uploading-files.md ├── go.mod ├── go.sum ├── helpers.go ├── helpers_test.go ├── log.go ├── params.go ├── params_test.go ├── passport.go ├── tests ├── audio.mp3 ├── cert.pem ├── image.jpg ├── key.pem ├── video.mp4 ├── videonote.mp4 └── voice.ogg ├── types.go └── types_test.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - develop 8 | pull_request: 9 | 10 | jobs: 11 | build: 12 | name: Test 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Set up Go 1.x 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: ^1.15 19 | id: go 20 | 21 | - name: Check out code into the Go module directory 22 | uses: actions/checkout@v2 23 | 24 | - name: Build 25 | run: go build -v . 26 | 27 | - name: Test 28 | run: go test -coverprofile=coverage.out -covermode=atomic -v . 29 | 30 | - name: Upload coverage report 31 | uses: codecov/codecov-action@v1 32 | with: 33 | file: ./coverage.out 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | coverage.out 3 | tmp/ 4 | book/ 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Syfaro 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang bindings for the Telegram Bot API 2 | 3 | [![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) 4 | [![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) 5 | 6 | All methods are fairly self-explanatory, and reading the [godoc](https://pkg.go.dev/github.com/go-telegram-bot-api/telegram-bot-api/v5) page should 7 | explain everything. If something isn't clear, open an issue or submit 8 | a pull request. 9 | 10 | There are more tutorials and high-level information on the website, [go-telegram-bot-api.dev](https://go-telegram-bot-api.dev). 11 | 12 | The scope of this project is just to provide a wrapper around the API 13 | without any additional features. There are other projects for creating 14 | something with plugins and command handlers without having to design 15 | all that yourself. 16 | 17 | Join [the development group](https://telegram.me/go_telegram_bot_api) if 18 | you want to ask questions or discuss development. 19 | 20 | ## Example 21 | 22 | First, ensure the library is installed and up to date by running 23 | `go get -u github.com/go-telegram-bot-api/telegram-bot-api/v5`. 24 | 25 | This is a very simple bot that just displays any gotten updates, 26 | then replies it to that chat. 27 | 28 | ```go 29 | package main 30 | 31 | import ( 32 | "log" 33 | 34 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 35 | ) 36 | 37 | func main() { 38 | bot, err := tgbotapi.NewBotAPI("MyAwesomeBotToken") 39 | if err != nil { 40 | log.Panic(err) 41 | } 42 | 43 | bot.Debug = true 44 | 45 | log.Printf("Authorized on account %s", bot.Self.UserName) 46 | 47 | u := tgbotapi.NewUpdate(0) 48 | u.Timeout = 60 49 | 50 | updates := bot.GetUpdatesChan(u) 51 | 52 | for update := range updates { 53 | if update.Message != nil { // If we got a message 54 | log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text) 55 | 56 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text) 57 | msg.ReplyToMessageID = update.Message.MessageID 58 | 59 | bot.Send(msg) 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | If you need to use webhooks (if you wish to run on Google App Engine), 66 | you may use a slightly different method. 67 | 68 | ```go 69 | package main 70 | 71 | import ( 72 | "log" 73 | "net/http" 74 | 75 | "github.com/go-telegram-bot-api/telegram-bot-api/v5" 76 | ) 77 | 78 | func main() { 79 | bot, err := tgbotapi.NewBotAPI("MyAwesomeBotToken") 80 | if err != nil { 81 | log.Fatal(err) 82 | } 83 | 84 | bot.Debug = true 85 | 86 | log.Printf("Authorized on account %s", bot.Self.UserName) 87 | 88 | wh, _ := tgbotapi.NewWebhookWithCert("https://www.example.com:8443/"+bot.Token, "cert.pem") 89 | 90 | _, err = bot.Request(wh) 91 | if err != nil { 92 | log.Fatal(err) 93 | } 94 | 95 | info, err := bot.GetWebhookInfo() 96 | if err != nil { 97 | log.Fatal(err) 98 | } 99 | 100 | if info.LastErrorDate != 0 { 101 | log.Printf("Telegram callback failed: %s", info.LastErrorMessage) 102 | } 103 | 104 | updates := bot.ListenForWebhook("/" + bot.Token) 105 | go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil) 106 | 107 | for update := range updates { 108 | log.Printf("%+v\n", update) 109 | } 110 | } 111 | ``` 112 | 113 | If you need, you may generate a self-signed certificate, as this requires 114 | HTTPS / TLS. The above example tells Telegram that this is your 115 | certificate and that it should be trusted, even though it is not 116 | properly signed. 117 | 118 | openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3560 -subj "//O=Org\CN=Test" -nodes 119 | 120 | Now that [Let's Encrypt](https://letsencrypt.org) is available, 121 | you may wish to generate your free TLS certificate there. 122 | -------------------------------------------------------------------------------- /book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Syfaro"] 3 | language = "en" 4 | multilingual = false 5 | src = "docs" 6 | title = "Go Telegram Bot API" 7 | 8 | [output.html] 9 | git-repository-url = "https://github.com/go-telegram-bot-api/telegram-bot-api" 10 | -------------------------------------------------------------------------------- /bot.go: -------------------------------------------------------------------------------- 1 | // Package tgbotapi has functions and types used for interacting with 2 | // the Telegram Bot API. 3 | package tgbotapi 4 | 5 | import ( 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "mime/multipart" 11 | "net/http" 12 | "net/url" 13 | "strings" 14 | "time" 15 | ) 16 | 17 | // HTTPClient is the type needed for the bot to perform HTTP requests. 18 | type HTTPClient interface { 19 | Do(req *http.Request) (*http.Response, error) 20 | } 21 | 22 | // BotAPI allows you to interact with the Telegram Bot API. 23 | type BotAPI struct { 24 | Token string `json:"token"` 25 | Debug bool `json:"debug"` 26 | Buffer int `json:"buffer"` 27 | 28 | Self User `json:"-"` 29 | Client HTTPClient `json:"-"` 30 | shutdownChannel chan interface{} 31 | 32 | apiEndpoint string 33 | } 34 | 35 | // NewBotAPI creates a new BotAPI instance. 36 | // 37 | // It requires a token, provided by @BotFather on Telegram. 38 | func NewBotAPI(token string) (*BotAPI, error) { 39 | return NewBotAPIWithClient(token, APIEndpoint, &http.Client{}) 40 | } 41 | 42 | // NewBotAPIWithAPIEndpoint creates a new BotAPI instance 43 | // and allows you to pass API endpoint. 44 | // 45 | // It requires a token, provided by @BotFather on Telegram and API endpoint. 46 | func NewBotAPIWithAPIEndpoint(token, apiEndpoint string) (*BotAPI, error) { 47 | return NewBotAPIWithClient(token, apiEndpoint, &http.Client{}) 48 | } 49 | 50 | // NewBotAPIWithClient creates a new BotAPI instance 51 | // and allows you to pass a http.Client. 52 | // 53 | // It requires a token, provided by @BotFather on Telegram and API endpoint. 54 | func NewBotAPIWithClient(token, apiEndpoint string, client HTTPClient) (*BotAPI, error) { 55 | bot := &BotAPI{ 56 | Token: token, 57 | Client: client, 58 | Buffer: 100, 59 | shutdownChannel: make(chan interface{}), 60 | 61 | apiEndpoint: apiEndpoint, 62 | } 63 | 64 | self, err := bot.GetMe() 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | bot.Self = self 70 | 71 | return bot, nil 72 | } 73 | 74 | // SetAPIEndpoint changes the Telegram Bot API endpoint used by the instance. 75 | func (bot *BotAPI) SetAPIEndpoint(apiEndpoint string) { 76 | bot.apiEndpoint = apiEndpoint 77 | } 78 | 79 | func buildParams(in Params) url.Values { 80 | if in == nil { 81 | return url.Values{} 82 | } 83 | 84 | out := url.Values{} 85 | 86 | for key, value := range in { 87 | out.Set(key, value) 88 | } 89 | 90 | return out 91 | } 92 | 93 | // MakeRequest makes a request to a specific endpoint with our token. 94 | func (bot *BotAPI) MakeRequest(endpoint string, params Params) (*APIResponse, error) { 95 | if bot.Debug { 96 | log.Printf("Endpoint: %s, params: %v\n", endpoint, params) 97 | } 98 | 99 | method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint) 100 | 101 | values := buildParams(params) 102 | 103 | req, err := http.NewRequest("POST", method, strings.NewReader(values.Encode())) 104 | if err != nil { 105 | return &APIResponse{}, err 106 | } 107 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 108 | 109 | resp, err := bot.Client.Do(req) 110 | if err != nil { 111 | return nil, err 112 | } 113 | defer resp.Body.Close() 114 | 115 | var apiResp APIResponse 116 | bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp) 117 | if err != nil { 118 | return &apiResp, err 119 | } 120 | 121 | if bot.Debug { 122 | log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes)) 123 | } 124 | 125 | if !apiResp.Ok { 126 | var parameters ResponseParameters 127 | 128 | if apiResp.Parameters != nil { 129 | parameters = *apiResp.Parameters 130 | } 131 | 132 | return &apiResp, &Error{ 133 | Code: apiResp.ErrorCode, 134 | Message: apiResp.Description, 135 | ResponseParameters: parameters, 136 | } 137 | } 138 | 139 | return &apiResp, nil 140 | } 141 | 142 | // decodeAPIResponse decode response and return slice of bytes if debug enabled. 143 | // If debug disabled, just decode http.Response.Body stream to APIResponse struct 144 | // for efficient memory usage 145 | func (bot *BotAPI) decodeAPIResponse(responseBody io.Reader, resp *APIResponse) ([]byte, error) { 146 | if !bot.Debug { 147 | dec := json.NewDecoder(responseBody) 148 | err := dec.Decode(resp) 149 | return nil, err 150 | } 151 | 152 | // if debug, read response body 153 | data, err := io.ReadAll(responseBody) 154 | if err != nil { 155 | return nil, err 156 | } 157 | 158 | err = json.Unmarshal(data, resp) 159 | if err != nil { 160 | return nil, err 161 | } 162 | 163 | return data, nil 164 | } 165 | 166 | // UploadFiles makes a request to the API with files. 167 | func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFile) (*APIResponse, error) { 168 | r, w := io.Pipe() 169 | m := multipart.NewWriter(w) 170 | 171 | // This code modified from the very helpful @HirbodBehnam 172 | // https://github.com/go-telegram-bot-api/telegram-bot-api/issues/354#issuecomment-663856473 173 | go func() { 174 | defer w.Close() 175 | defer m.Close() 176 | 177 | for field, value := range params { 178 | if err := m.WriteField(field, value); err != nil { 179 | w.CloseWithError(err) 180 | return 181 | } 182 | } 183 | 184 | for _, file := range files { 185 | if file.Data.NeedsUpload() { 186 | name, reader, err := file.Data.UploadData() 187 | if err != nil { 188 | w.CloseWithError(err) 189 | return 190 | } 191 | 192 | part, err := m.CreateFormFile(file.Name, name) 193 | if err != nil { 194 | w.CloseWithError(err) 195 | return 196 | } 197 | 198 | if _, err := io.Copy(part, reader); err != nil { 199 | w.CloseWithError(err) 200 | return 201 | } 202 | 203 | if closer, ok := reader.(io.ReadCloser); ok { 204 | if err = closer.Close(); err != nil { 205 | w.CloseWithError(err) 206 | return 207 | } 208 | } 209 | } else { 210 | value := file.Data.SendData() 211 | 212 | if err := m.WriteField(file.Name, value); err != nil { 213 | w.CloseWithError(err) 214 | return 215 | } 216 | } 217 | } 218 | }() 219 | 220 | if bot.Debug { 221 | log.Printf("Endpoint: %s, params: %v, with %d files\n", endpoint, params, len(files)) 222 | } 223 | 224 | method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint) 225 | 226 | req, err := http.NewRequest("POST", method, r) 227 | if err != nil { 228 | return nil, err 229 | } 230 | 231 | req.Header.Set("Content-Type", m.FormDataContentType()) 232 | 233 | resp, err := bot.Client.Do(req) 234 | if err != nil { 235 | return nil, err 236 | } 237 | defer resp.Body.Close() 238 | 239 | var apiResp APIResponse 240 | bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp) 241 | if err != nil { 242 | return &apiResp, err 243 | } 244 | 245 | if bot.Debug { 246 | log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes)) 247 | } 248 | 249 | if !apiResp.Ok { 250 | var parameters ResponseParameters 251 | 252 | if apiResp.Parameters != nil { 253 | parameters = *apiResp.Parameters 254 | } 255 | 256 | return &apiResp, &Error{ 257 | Message: apiResp.Description, 258 | ResponseParameters: parameters, 259 | } 260 | } 261 | 262 | return &apiResp, nil 263 | } 264 | 265 | // GetFileDirectURL returns direct URL to file 266 | // 267 | // It requires the FileID. 268 | func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) { 269 | file, err := bot.GetFile(FileConfig{fileID}) 270 | 271 | if err != nil { 272 | return "", err 273 | } 274 | 275 | return file.Link(bot.Token), nil 276 | } 277 | 278 | // GetMe fetches the currently authenticated bot. 279 | // 280 | // This method is called upon creation to validate the token, 281 | // and so you may get this data from BotAPI.Self without the need for 282 | // another request. 283 | func (bot *BotAPI) GetMe() (User, error) { 284 | resp, err := bot.MakeRequest("getMe", nil) 285 | if err != nil { 286 | return User{}, err 287 | } 288 | 289 | var user User 290 | err = json.Unmarshal(resp.Result, &user) 291 | 292 | return user, err 293 | } 294 | 295 | // IsMessageToMe returns true if message directed to this bot. 296 | // 297 | // It requires the Message. 298 | func (bot *BotAPI) IsMessageToMe(message Message) bool { 299 | return strings.Contains(message.Text, "@"+bot.Self.UserName) 300 | } 301 | 302 | func hasFilesNeedingUpload(files []RequestFile) bool { 303 | for _, file := range files { 304 | if file.Data.NeedsUpload() { 305 | return true 306 | } 307 | } 308 | 309 | return false 310 | } 311 | 312 | // Request sends a Chattable to Telegram, and returns the APIResponse. 313 | func (bot *BotAPI) Request(c Chattable) (*APIResponse, error) { 314 | params, err := c.params() 315 | if err != nil { 316 | return nil, err 317 | } 318 | 319 | if t, ok := c.(Fileable); ok { 320 | files := t.files() 321 | 322 | // If we have files that need to be uploaded, we should delegate the 323 | // request to UploadFile. 324 | if hasFilesNeedingUpload(files) { 325 | return bot.UploadFiles(t.method(), params, files) 326 | } 327 | 328 | // However, if there are no files to be uploaded, there's likely things 329 | // that need to be turned into params instead. 330 | for _, file := range files { 331 | params[file.Name] = file.Data.SendData() 332 | } 333 | } 334 | 335 | return bot.MakeRequest(c.method(), params) 336 | } 337 | 338 | // Send will send a Chattable item to Telegram and provides the 339 | // returned Message. 340 | func (bot *BotAPI) Send(c Chattable) (Message, error) { 341 | resp, err := bot.Request(c) 342 | if err != nil { 343 | return Message{}, err 344 | } 345 | 346 | var message Message 347 | err = json.Unmarshal(resp.Result, &message) 348 | 349 | return message, err 350 | } 351 | 352 | // SendMediaGroup sends a media group and returns the resulting messages. 353 | func (bot *BotAPI) SendMediaGroup(config MediaGroupConfig) ([]Message, error) { 354 | resp, err := bot.Request(config) 355 | if err != nil { 356 | return nil, err 357 | } 358 | 359 | var messages []Message 360 | err = json.Unmarshal(resp.Result, &messages) 361 | 362 | return messages, err 363 | } 364 | 365 | // GetUserProfilePhotos gets a user's profile photos. 366 | // 367 | // It requires UserID. 368 | // Offset and Limit are optional. 369 | func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) { 370 | resp, err := bot.Request(config) 371 | if err != nil { 372 | return UserProfilePhotos{}, err 373 | } 374 | 375 | var profilePhotos UserProfilePhotos 376 | err = json.Unmarshal(resp.Result, &profilePhotos) 377 | 378 | return profilePhotos, err 379 | } 380 | 381 | // GetFile returns a File which can download a file from Telegram. 382 | // 383 | // Requires FileID. 384 | func (bot *BotAPI) GetFile(config FileConfig) (File, error) { 385 | resp, err := bot.Request(config) 386 | if err != nil { 387 | return File{}, err 388 | } 389 | 390 | var file File 391 | err = json.Unmarshal(resp.Result, &file) 392 | 393 | return file, err 394 | } 395 | 396 | // GetUpdates fetches updates. 397 | // If a WebHook is set, this will not return any data! 398 | // 399 | // Offset, Limit, Timeout, and AllowedUpdates are optional. 400 | // To avoid stale items, set Offset to one higher than the previous item. 401 | // Set Timeout to a large number to reduce requests, so you can get updates 402 | // instantly instead of having to wait between requests. 403 | func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) { 404 | resp, err := bot.Request(config) 405 | if err != nil { 406 | return []Update{}, err 407 | } 408 | 409 | var updates []Update 410 | err = json.Unmarshal(resp.Result, &updates) 411 | 412 | return updates, err 413 | } 414 | 415 | // GetWebhookInfo allows you to fetch information about a webhook and if 416 | // one currently is set, along with pending update count and error messages. 417 | func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) { 418 | resp, err := bot.MakeRequest("getWebhookInfo", nil) 419 | if err != nil { 420 | return WebhookInfo{}, err 421 | } 422 | 423 | var info WebhookInfo 424 | err = json.Unmarshal(resp.Result, &info) 425 | 426 | return info, err 427 | } 428 | 429 | // GetUpdatesChan starts and returns a channel for getting updates. 430 | func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) UpdatesChannel { 431 | ch := make(chan Update, bot.Buffer) 432 | 433 | go func() { 434 | for { 435 | select { 436 | case <-bot.shutdownChannel: 437 | close(ch) 438 | return 439 | default: 440 | } 441 | 442 | updates, err := bot.GetUpdates(config) 443 | if err != nil { 444 | log.Println(err) 445 | log.Println("Failed to get updates, retrying in 3 seconds...") 446 | time.Sleep(time.Second * 3) 447 | 448 | continue 449 | } 450 | 451 | for _, update := range updates { 452 | if update.UpdateID >= config.Offset { 453 | config.Offset = update.UpdateID + 1 454 | ch <- update 455 | } 456 | } 457 | } 458 | }() 459 | 460 | return ch 461 | } 462 | 463 | // StopReceivingUpdates stops the go routine which receives updates 464 | func (bot *BotAPI) StopReceivingUpdates() { 465 | if bot.Debug { 466 | log.Println("Stopping the update receiver routine...") 467 | } 468 | close(bot.shutdownChannel) 469 | } 470 | 471 | // ListenForWebhook registers a http handler for a webhook. 472 | func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel { 473 | ch := make(chan Update, bot.Buffer) 474 | 475 | http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) { 476 | update, err := bot.HandleUpdate(r) 477 | if err != nil { 478 | errMsg, _ := json.Marshal(map[string]string{"error": err.Error()}) 479 | w.WriteHeader(http.StatusBadRequest) 480 | w.Header().Set("Content-Type", "application/json") 481 | _, _ = w.Write(errMsg) 482 | return 483 | } 484 | 485 | ch <- *update 486 | }) 487 | 488 | return ch 489 | } 490 | 491 | // ListenForWebhookRespReqFormat registers a http handler for a single incoming webhook. 492 | func (bot *BotAPI) ListenForWebhookRespReqFormat(w http.ResponseWriter, r *http.Request) UpdatesChannel { 493 | ch := make(chan Update, bot.Buffer) 494 | 495 | func(w http.ResponseWriter, r *http.Request) { 496 | defer close(ch) 497 | 498 | update, err := bot.HandleUpdate(r) 499 | if err != nil { 500 | errMsg, _ := json.Marshal(map[string]string{"error": err.Error()}) 501 | w.WriteHeader(http.StatusBadRequest) 502 | w.Header().Set("Content-Type", "application/json") 503 | _, _ = w.Write(errMsg) 504 | return 505 | } 506 | 507 | ch <- *update 508 | }(w, r) 509 | 510 | return ch 511 | } 512 | 513 | // HandleUpdate parses and returns update received via webhook 514 | func (bot *BotAPI) HandleUpdate(r *http.Request) (*Update, error) { 515 | if r.Method != http.MethodPost { 516 | err := errors.New("wrong HTTP method required POST") 517 | return nil, err 518 | } 519 | 520 | var update Update 521 | err := json.NewDecoder(r.Body).Decode(&update) 522 | if err != nil { 523 | return nil, err 524 | } 525 | 526 | return &update, nil 527 | } 528 | 529 | // WriteToHTTPResponse writes the request to the HTTP ResponseWriter. 530 | // 531 | // It doesn't support uploading files. 532 | // 533 | // See https://core.telegram.org/bots/api#making-requests-when-getting-updates 534 | // for details. 535 | func WriteToHTTPResponse(w http.ResponseWriter, c Chattable) error { 536 | params, err := c.params() 537 | if err != nil { 538 | return err 539 | } 540 | 541 | if t, ok := c.(Fileable); ok { 542 | if hasFilesNeedingUpload(t.files()) { 543 | return errors.New("unable to use http response to upload files") 544 | } 545 | } 546 | 547 | values := buildParams(params) 548 | values.Set("method", c.method()) 549 | 550 | w.Header().Set("Content-Type", "application/x-www-form-urlencoded") 551 | _, err = w.Write([]byte(values.Encode())) 552 | return err 553 | } 554 | 555 | // GetChat gets information about a chat. 556 | func (bot *BotAPI) GetChat(config ChatInfoConfig) (Chat, error) { 557 | resp, err := bot.Request(config) 558 | if err != nil { 559 | return Chat{}, err 560 | } 561 | 562 | var chat Chat 563 | err = json.Unmarshal(resp.Result, &chat) 564 | 565 | return chat, err 566 | } 567 | 568 | // GetChatAdministrators gets a list of administrators in the chat. 569 | // 570 | // If none have been appointed, only the creator will be returned. 571 | // Bots are not shown, even if they are an administrator. 572 | func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]ChatMember, error) { 573 | resp, err := bot.Request(config) 574 | if err != nil { 575 | return []ChatMember{}, err 576 | } 577 | 578 | var members []ChatMember 579 | err = json.Unmarshal(resp.Result, &members) 580 | 581 | return members, err 582 | } 583 | 584 | // GetChatMembersCount gets the number of users in a chat. 585 | func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) { 586 | resp, err := bot.Request(config) 587 | if err != nil { 588 | return -1, err 589 | } 590 | 591 | var count int 592 | err = json.Unmarshal(resp.Result, &count) 593 | 594 | return count, err 595 | } 596 | 597 | // GetChatMember gets a specific chat member. 598 | func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) { 599 | resp, err := bot.Request(config) 600 | if err != nil { 601 | return ChatMember{}, err 602 | } 603 | 604 | var member ChatMember 605 | err = json.Unmarshal(resp.Result, &member) 606 | 607 | return member, err 608 | } 609 | 610 | // GetGameHighScores allows you to get the high scores for a game. 611 | func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) { 612 | resp, err := bot.Request(config) 613 | if err != nil { 614 | return []GameHighScore{}, err 615 | } 616 | 617 | var highScores []GameHighScore 618 | err = json.Unmarshal(resp.Result, &highScores) 619 | 620 | return highScores, err 621 | } 622 | 623 | // GetInviteLink get InviteLink for a chat 624 | func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) { 625 | resp, err := bot.Request(config) 626 | if err != nil { 627 | return "", err 628 | } 629 | 630 | var inviteLink string 631 | err = json.Unmarshal(resp.Result, &inviteLink) 632 | 633 | return inviteLink, err 634 | } 635 | 636 | // GetStickerSet returns a StickerSet. 637 | func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) { 638 | resp, err := bot.Request(config) 639 | if err != nil { 640 | return StickerSet{}, err 641 | } 642 | 643 | var stickers StickerSet 644 | err = json.Unmarshal(resp.Result, &stickers) 645 | 646 | return stickers, err 647 | } 648 | 649 | // StopPoll stops a poll and returns the result. 650 | func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) { 651 | resp, err := bot.Request(config) 652 | if err != nil { 653 | return Poll{}, err 654 | } 655 | 656 | var poll Poll 657 | err = json.Unmarshal(resp.Result, &poll) 658 | 659 | return poll, err 660 | } 661 | 662 | // GetMyCommands gets the currently registered commands. 663 | func (bot *BotAPI) GetMyCommands() ([]BotCommand, error) { 664 | return bot.GetMyCommandsWithConfig(GetMyCommandsConfig{}) 665 | } 666 | 667 | // GetMyCommandsWithConfig gets the currently registered commands with a config. 668 | func (bot *BotAPI) GetMyCommandsWithConfig(config GetMyCommandsConfig) ([]BotCommand, error) { 669 | resp, err := bot.Request(config) 670 | if err != nil { 671 | return nil, err 672 | } 673 | 674 | var commands []BotCommand 675 | err = json.Unmarshal(resp.Result, &commands) 676 | 677 | return commands, err 678 | } 679 | 680 | // CopyMessage copy messages of any kind. The method is analogous to the method 681 | // forwardMessage, but the copied message doesn't have a link to the original 682 | // message. Returns the MessageID of the sent message on success. 683 | func (bot *BotAPI) CopyMessage(config CopyMessageConfig) (MessageID, error) { 684 | resp, err := bot.Request(config) 685 | if err != nil { 686 | return MessageID{}, err 687 | } 688 | 689 | var messageID MessageID 690 | err = json.Unmarshal(resp.Result, &messageID) 691 | 692 | return messageID, err 693 | } 694 | 695 | // AnswerWebAppQuery sets the result of an interaction with a Web App and send a 696 | // corresponding message on behalf of the user to the chat from which the query originated. 697 | func (bot *BotAPI) AnswerWebAppQuery(config AnswerWebAppQueryConfig) (SentWebAppMessage, error) { 698 | var sentWebAppMessage SentWebAppMessage 699 | 700 | resp, err := bot.Request(config) 701 | if err != nil { 702 | return sentWebAppMessage, err 703 | } 704 | 705 | err = json.Unmarshal(resp.Result, &sentWebAppMessage) 706 | return sentWebAppMessage, err 707 | } 708 | 709 | // GetMyDefaultAdministratorRights gets the current default administrator rights of the bot. 710 | func (bot *BotAPI) GetMyDefaultAdministratorRights(config GetMyDefaultAdministratorRightsConfig) (ChatAdministratorRights, error) { 711 | var rights ChatAdministratorRights 712 | 713 | resp, err := bot.Request(config) 714 | if err != nil { 715 | return rights, err 716 | } 717 | 718 | err = json.Unmarshal(resp.Result, &rights) 719 | return rights, err 720 | } 721 | 722 | // EscapeText takes an input text and escape Telegram markup symbols. 723 | // In this way we can send a text without being afraid of having to escape the characters manually. 724 | // Note that you don't have to include the formatting style in the input text, or it will be escaped too. 725 | // If there is an error, an empty string will be returned. 726 | // 727 | // parseMode is the text formatting mode (ModeMarkdown, ModeMarkdownV2 or ModeHTML) 728 | // text is the input string that will be escaped 729 | func EscapeText(parseMode string, text string) string { 730 | var replacer *strings.Replacer 731 | 732 | if parseMode == ModeHTML { 733 | replacer = strings.NewReplacer("<", "<", ">", ">", "&", "&") 734 | } else if parseMode == ModeMarkdown { 735 | replacer = strings.NewReplacer("_", "\\_", "*", "\\*", "`", "\\`", "[", "\\[") 736 | } else if parseMode == ModeMarkdownV2 { 737 | replacer = strings.NewReplacer( 738 | "_", "\\_", "*", "\\*", "[", "\\[", "]", "\\]", "(", 739 | "\\(", ")", "\\)", "~", "\\~", "`", "\\`", ">", "\\>", 740 | "#", "\\#", "+", "\\+", "-", "\\-", "=", "\\=", "|", 741 | "\\|", "{", "\\{", "}", "\\}", ".", "\\.", "!", "\\!", 742 | ) 743 | } else { 744 | return "" 745 | } 746 | 747 | return replacer.Replace(text) 748 | } 749 | -------------------------------------------------------------------------------- /bot_test.go: -------------------------------------------------------------------------------- 1 | package tgbotapi 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | const ( 11 | TestToken = "153667468:AAHlSHlMqSt1f_uFmVRJbm5gntu2HI4WW8I" 12 | ChatID = 76918703 13 | Channel = "@tgbotapitest" 14 | SupergroupChatID = -1001120141283 15 | ReplyToMessageID = 35 16 | ExistingPhotoFileID = "AgACAgIAAxkDAAEBFUZhIALQ9pZN4BUe8ZSzUU_2foSo1AACnrMxG0BucEhezsBWOgcikQEAAwIAA20AAyAE" 17 | ExistingDocumentFileID = "BQADAgADOQADjMcoCcioX1GrDvp3Ag" 18 | ExistingAudioFileID = "BQADAgADRgADjMcoCdXg3lSIN49lAg" 19 | ExistingVoiceFileID = "AwADAgADWQADjMcoCeul6r_q52IyAg" 20 | ExistingVideoFileID = "BAADAgADZgADjMcoCav432kYe0FRAg" 21 | ExistingVideoNoteFileID = "DQADAgADdQAD70cQSUK41dLsRMqfAg" 22 | ExistingStickerFileID = "BQADAgADcwADjMcoCbdl-6eB--YPAg" 23 | ) 24 | 25 | type testLogger struct { 26 | t *testing.T 27 | } 28 | 29 | func (t testLogger) Println(v ...interface{}) { 30 | t.t.Log(v...) 31 | } 32 | 33 | func (t testLogger) Printf(format string, v ...interface{}) { 34 | t.t.Logf(format, v...) 35 | } 36 | 37 | func getBot(t *testing.T) (*BotAPI, error) { 38 | bot, err := NewBotAPI(TestToken) 39 | bot.Debug = true 40 | 41 | logger := testLogger{t} 42 | SetLogger(logger) 43 | 44 | if err != nil { 45 | t.Error(err) 46 | } 47 | 48 | return bot, err 49 | } 50 | 51 | func TestNewBotAPI_notoken(t *testing.T) { 52 | _, err := NewBotAPI("") 53 | 54 | if err == nil { 55 | t.Error(err) 56 | } 57 | } 58 | 59 | func TestGetUpdates(t *testing.T) { 60 | bot, _ := getBot(t) 61 | 62 | u := NewUpdate(0) 63 | 64 | _, err := bot.GetUpdates(u) 65 | 66 | if err != nil { 67 | t.Error(err) 68 | } 69 | } 70 | 71 | func TestSendWithMessage(t *testing.T) { 72 | bot, _ := getBot(t) 73 | 74 | msg := NewMessage(ChatID, "A test message from the test library in telegram-bot-api") 75 | msg.ParseMode = ModeMarkdown 76 | _, err := bot.Send(msg) 77 | 78 | if err != nil { 79 | t.Error(err) 80 | } 81 | } 82 | 83 | func TestSendWithMessageReply(t *testing.T) { 84 | bot, _ := getBot(t) 85 | 86 | msg := NewMessage(ChatID, "A test message from the test library in telegram-bot-api") 87 | msg.ReplyToMessageID = ReplyToMessageID 88 | _, err := bot.Send(msg) 89 | 90 | if err != nil { 91 | t.Error(err) 92 | } 93 | } 94 | 95 | func TestSendWithMessageForward(t *testing.T) { 96 | bot, _ := getBot(t) 97 | 98 | msg := NewForward(ChatID, ChatID, ReplyToMessageID) 99 | _, err := bot.Send(msg) 100 | 101 | if err != nil { 102 | t.Error(err) 103 | } 104 | } 105 | 106 | func TestCopyMessage(t *testing.T) { 107 | bot, _ := getBot(t) 108 | 109 | msg := NewMessage(ChatID, "A test message from the test library in telegram-bot-api") 110 | message, err := bot.Send(msg) 111 | if err != nil { 112 | t.Error(err) 113 | } 114 | 115 | copyMessageConfig := NewCopyMessage(SupergroupChatID, message.Chat.ID, message.MessageID) 116 | messageID, err := bot.CopyMessage(copyMessageConfig) 117 | if err != nil { 118 | t.Error(err) 119 | } 120 | 121 | if messageID.MessageID == message.MessageID { 122 | t.Error("copied message ID was the same as original message") 123 | } 124 | } 125 | 126 | func TestSendWithNewPhoto(t *testing.T) { 127 | bot, _ := getBot(t) 128 | 129 | msg := NewPhoto(ChatID, FilePath("tests/image.jpg")) 130 | msg.Caption = "Test" 131 | _, err := bot.Send(msg) 132 | 133 | if err != nil { 134 | t.Error(err) 135 | } 136 | } 137 | 138 | func TestSendWithNewPhotoWithFileBytes(t *testing.T) { 139 | bot, _ := getBot(t) 140 | 141 | data, _ := os.ReadFile("tests/image.jpg") 142 | b := FileBytes{Name: "image.jpg", Bytes: data} 143 | 144 | msg := NewPhoto(ChatID, b) 145 | msg.Caption = "Test" 146 | _, err := bot.Send(msg) 147 | 148 | if err != nil { 149 | t.Error(err) 150 | } 151 | } 152 | 153 | func TestSendWithNewPhotoWithFileReader(t *testing.T) { 154 | bot, _ := getBot(t) 155 | 156 | f, _ := os.Open("tests/image.jpg") 157 | reader := FileReader{Name: "image.jpg", Reader: f} 158 | 159 | msg := NewPhoto(ChatID, reader) 160 | msg.Caption = "Test" 161 | _, err := bot.Send(msg) 162 | 163 | if err != nil { 164 | t.Error(err) 165 | } 166 | } 167 | 168 | func TestSendWithNewPhotoReply(t *testing.T) { 169 | bot, _ := getBot(t) 170 | 171 | msg := NewPhoto(ChatID, FilePath("tests/image.jpg")) 172 | msg.ReplyToMessageID = ReplyToMessageID 173 | 174 | _, err := bot.Send(msg) 175 | 176 | if err != nil { 177 | t.Error(err) 178 | } 179 | } 180 | 181 | func TestSendNewPhotoToChannel(t *testing.T) { 182 | bot, _ := getBot(t) 183 | 184 | msg := NewPhotoToChannel(Channel, FilePath("tests/image.jpg")) 185 | msg.Caption = "Test" 186 | _, err := bot.Send(msg) 187 | 188 | if err != nil { 189 | t.Error(err) 190 | t.Fail() 191 | } 192 | } 193 | 194 | func TestSendNewPhotoToChannelFileBytes(t *testing.T) { 195 | bot, _ := getBot(t) 196 | 197 | data, _ := os.ReadFile("tests/image.jpg") 198 | b := FileBytes{Name: "image.jpg", Bytes: data} 199 | 200 | msg := NewPhotoToChannel(Channel, b) 201 | msg.Caption = "Test" 202 | _, err := bot.Send(msg) 203 | 204 | if err != nil { 205 | t.Error(err) 206 | t.Fail() 207 | } 208 | } 209 | 210 | func TestSendNewPhotoToChannelFileReader(t *testing.T) { 211 | bot, _ := getBot(t) 212 | 213 | f, _ := os.Open("tests/image.jpg") 214 | reader := FileReader{Name: "image.jpg", Reader: f} 215 | 216 | msg := NewPhotoToChannel(Channel, reader) 217 | msg.Caption = "Test" 218 | _, err := bot.Send(msg) 219 | 220 | if err != nil { 221 | t.Error(err) 222 | t.Fail() 223 | } 224 | } 225 | 226 | func TestSendWithExistingPhoto(t *testing.T) { 227 | bot, _ := getBot(t) 228 | 229 | msg := NewPhoto(ChatID, FileID(ExistingPhotoFileID)) 230 | msg.Caption = "Test" 231 | _, err := bot.Send(msg) 232 | 233 | if err != nil { 234 | t.Error(err) 235 | } 236 | } 237 | 238 | func TestSendWithNewDocument(t *testing.T) { 239 | bot, _ := getBot(t) 240 | 241 | msg := NewDocument(ChatID, FilePath("tests/image.jpg")) 242 | _, err := bot.Send(msg) 243 | 244 | if err != nil { 245 | t.Error(err) 246 | } 247 | } 248 | 249 | func TestSendWithNewDocumentAndThumb(t *testing.T) { 250 | bot, _ := getBot(t) 251 | 252 | msg := NewDocument(ChatID, FilePath("tests/voice.ogg")) 253 | msg.Thumb = FilePath("tests/image.jpg") 254 | _, err := bot.Send(msg) 255 | 256 | if err != nil { 257 | t.Error(err) 258 | } 259 | } 260 | 261 | func TestSendWithExistingDocument(t *testing.T) { 262 | bot, _ := getBot(t) 263 | 264 | msg := NewDocument(ChatID, FileID(ExistingDocumentFileID)) 265 | _, err := bot.Send(msg) 266 | 267 | if err != nil { 268 | t.Error(err) 269 | } 270 | } 271 | 272 | func TestSendWithNewAudio(t *testing.T) { 273 | bot, _ := getBot(t) 274 | 275 | msg := NewAudio(ChatID, FilePath("tests/audio.mp3")) 276 | msg.Title = "TEST" 277 | msg.Duration = 10 278 | msg.Performer = "TEST" 279 | _, err := bot.Send(msg) 280 | 281 | if err != nil { 282 | t.Error(err) 283 | } 284 | } 285 | 286 | func TestSendWithExistingAudio(t *testing.T) { 287 | bot, _ := getBot(t) 288 | 289 | msg := NewAudio(ChatID, FileID(ExistingAudioFileID)) 290 | msg.Title = "TEST" 291 | msg.Duration = 10 292 | msg.Performer = "TEST" 293 | 294 | _, err := bot.Send(msg) 295 | 296 | if err != nil { 297 | t.Error(err) 298 | } 299 | } 300 | 301 | func TestSendWithNewVoice(t *testing.T) { 302 | bot, _ := getBot(t) 303 | 304 | msg := NewVoice(ChatID, FilePath("tests/voice.ogg")) 305 | msg.Duration = 10 306 | _, err := bot.Send(msg) 307 | 308 | if err != nil { 309 | t.Error(err) 310 | } 311 | } 312 | 313 | func TestSendWithExistingVoice(t *testing.T) { 314 | bot, _ := getBot(t) 315 | 316 | msg := NewVoice(ChatID, FileID(ExistingVoiceFileID)) 317 | msg.Duration = 10 318 | _, err := bot.Send(msg) 319 | 320 | if err != nil { 321 | t.Error(err) 322 | } 323 | } 324 | 325 | func TestSendWithContact(t *testing.T) { 326 | bot, _ := getBot(t) 327 | 328 | contact := NewContact(ChatID, "5551234567", "Test") 329 | 330 | if _, err := bot.Send(contact); err != nil { 331 | t.Error(err) 332 | } 333 | } 334 | 335 | func TestSendWithLocation(t *testing.T) { 336 | bot, _ := getBot(t) 337 | 338 | _, err := bot.Send(NewLocation(ChatID, 40, 40)) 339 | 340 | if err != nil { 341 | t.Error(err) 342 | } 343 | } 344 | 345 | func TestSendWithVenue(t *testing.T) { 346 | bot, _ := getBot(t) 347 | 348 | venue := NewVenue(ChatID, "A Test Location", "123 Test Street", 40, 40) 349 | 350 | if _, err := bot.Send(venue); err != nil { 351 | t.Error(err) 352 | } 353 | } 354 | 355 | func TestSendWithNewVideo(t *testing.T) { 356 | bot, _ := getBot(t) 357 | 358 | msg := NewVideo(ChatID, FilePath("tests/video.mp4")) 359 | msg.Duration = 10 360 | msg.Caption = "TEST" 361 | 362 | _, err := bot.Send(msg) 363 | 364 | if err != nil { 365 | t.Error(err) 366 | } 367 | } 368 | 369 | func TestSendWithExistingVideo(t *testing.T) { 370 | bot, _ := getBot(t) 371 | 372 | msg := NewVideo(ChatID, FileID(ExistingVideoFileID)) 373 | msg.Duration = 10 374 | msg.Caption = "TEST" 375 | 376 | _, err := bot.Send(msg) 377 | 378 | if err != nil { 379 | t.Error(err) 380 | } 381 | } 382 | 383 | func TestSendWithNewVideoNote(t *testing.T) { 384 | bot, _ := getBot(t) 385 | 386 | msg := NewVideoNote(ChatID, 240, FilePath("tests/videonote.mp4")) 387 | msg.Duration = 10 388 | 389 | _, err := bot.Send(msg) 390 | 391 | if err != nil { 392 | t.Error(err) 393 | } 394 | } 395 | 396 | func TestSendWithExistingVideoNote(t *testing.T) { 397 | bot, _ := getBot(t) 398 | 399 | msg := NewVideoNote(ChatID, 240, FileID(ExistingVideoNoteFileID)) 400 | msg.Duration = 10 401 | 402 | _, err := bot.Send(msg) 403 | 404 | if err != nil { 405 | t.Error(err) 406 | } 407 | } 408 | 409 | func TestSendWithNewSticker(t *testing.T) { 410 | bot, _ := getBot(t) 411 | 412 | msg := NewSticker(ChatID, FilePath("tests/image.jpg")) 413 | 414 | _, err := bot.Send(msg) 415 | 416 | if err != nil { 417 | t.Error(err) 418 | } 419 | } 420 | 421 | func TestSendWithExistingSticker(t *testing.T) { 422 | bot, _ := getBot(t) 423 | 424 | msg := NewSticker(ChatID, FileID(ExistingStickerFileID)) 425 | 426 | _, err := bot.Send(msg) 427 | 428 | if err != nil { 429 | t.Error(err) 430 | } 431 | } 432 | 433 | func TestSendWithNewStickerAndKeyboardHide(t *testing.T) { 434 | bot, _ := getBot(t) 435 | 436 | msg := NewSticker(ChatID, FilePath("tests/image.jpg")) 437 | msg.ReplyMarkup = ReplyKeyboardRemove{ 438 | RemoveKeyboard: true, 439 | Selective: false, 440 | } 441 | _, err := bot.Send(msg) 442 | 443 | if err != nil { 444 | t.Error(err) 445 | } 446 | } 447 | 448 | func TestSendWithExistingStickerAndKeyboardHide(t *testing.T) { 449 | bot, _ := getBot(t) 450 | 451 | msg := NewSticker(ChatID, FileID(ExistingStickerFileID)) 452 | msg.ReplyMarkup = ReplyKeyboardRemove{ 453 | RemoveKeyboard: true, 454 | Selective: false, 455 | } 456 | 457 | _, err := bot.Send(msg) 458 | 459 | if err != nil { 460 | t.Error(err) 461 | } 462 | } 463 | 464 | func TestSendWithDice(t *testing.T) { 465 | bot, _ := getBot(t) 466 | 467 | msg := NewDice(ChatID) 468 | _, err := bot.Send(msg) 469 | 470 | if err != nil { 471 | t.Error(err) 472 | t.Fail() 473 | } 474 | 475 | } 476 | 477 | func TestSendWithDiceWithEmoji(t *testing.T) { 478 | bot, _ := getBot(t) 479 | 480 | msg := NewDiceWithEmoji(ChatID, "🏀") 481 | _, err := bot.Send(msg) 482 | 483 | if err != nil { 484 | t.Error(err) 485 | t.Fail() 486 | } 487 | 488 | } 489 | 490 | func TestGetFile(t *testing.T) { 491 | bot, _ := getBot(t) 492 | 493 | file := FileConfig{ 494 | FileID: ExistingPhotoFileID, 495 | } 496 | 497 | _, err := bot.GetFile(file) 498 | 499 | if err != nil { 500 | t.Error(err) 501 | } 502 | } 503 | 504 | func TestSendChatConfig(t *testing.T) { 505 | bot, _ := getBot(t) 506 | 507 | _, err := bot.Request(NewChatAction(ChatID, ChatTyping)) 508 | 509 | if err != nil { 510 | t.Error(err) 511 | } 512 | } 513 | 514 | // TODO: identify why this isn't working 515 | // func TestSendEditMessage(t *testing.T) { 516 | // bot, _ := getBot(t) 517 | 518 | // msg, err := bot.Send(NewMessage(ChatID, "Testing editing.")) 519 | // if err != nil { 520 | // t.Error(err) 521 | // } 522 | 523 | // edit := EditMessageTextConfig{ 524 | // BaseEdit: BaseEdit{ 525 | // ChatID: ChatID, 526 | // MessageID: msg.MessageID, 527 | // }, 528 | // Text: "Updated text.", 529 | // } 530 | 531 | // _, err = bot.Send(edit) 532 | // if err != nil { 533 | // t.Error(err) 534 | // } 535 | // } 536 | 537 | func TestGetUserProfilePhotos(t *testing.T) { 538 | bot, _ := getBot(t) 539 | 540 | _, err := bot.GetUserProfilePhotos(NewUserProfilePhotos(ChatID)) 541 | if err != nil { 542 | t.Error(err) 543 | } 544 | } 545 | 546 | func TestSetWebhookWithCert(t *testing.T) { 547 | bot, _ := getBot(t) 548 | 549 | time.Sleep(time.Second * 2) 550 | 551 | bot.Request(DeleteWebhookConfig{}) 552 | 553 | wh, err := NewWebhookWithCert("https://example.com/tgbotapi-test/"+bot.Token, FilePath("tests/cert.pem")) 554 | 555 | if err != nil { 556 | t.Error(err) 557 | } 558 | _, err = bot.Request(wh) 559 | 560 | if err != nil { 561 | t.Error(err) 562 | } 563 | 564 | _, err = bot.GetWebhookInfo() 565 | 566 | if err != nil { 567 | t.Error(err) 568 | } 569 | 570 | bot.Request(DeleteWebhookConfig{}) 571 | } 572 | 573 | func TestSetWebhookWithoutCert(t *testing.T) { 574 | bot, _ := getBot(t) 575 | 576 | time.Sleep(time.Second * 2) 577 | 578 | bot.Request(DeleteWebhookConfig{}) 579 | 580 | wh, err := NewWebhook("https://example.com/tgbotapi-test/" + bot.Token) 581 | 582 | if err != nil { 583 | t.Error(err) 584 | } 585 | 586 | _, err = bot.Request(wh) 587 | 588 | if err != nil { 589 | t.Error(err) 590 | } 591 | 592 | info, err := bot.GetWebhookInfo() 593 | 594 | if err != nil { 595 | t.Error(err) 596 | } 597 | if info.MaxConnections == 0 { 598 | t.Errorf("Expected maximum connections to be greater than 0") 599 | } 600 | if info.LastErrorDate != 0 { 601 | t.Errorf("failed to set webhook: %s", info.LastErrorMessage) 602 | } 603 | 604 | bot.Request(DeleteWebhookConfig{}) 605 | } 606 | 607 | func TestSendWithMediaGroupPhotoVideo(t *testing.T) { 608 | bot, _ := getBot(t) 609 | 610 | cfg := NewMediaGroup(ChatID, []interface{}{ 611 | NewInputMediaPhoto(FileURL("https://github.com/go-telegram-bot-api/telegram-bot-api/raw/0a3a1c8716c4cd8d26a262af9f12dcbab7f3f28c/tests/image.jpg")), 612 | NewInputMediaPhoto(FilePath("tests/image.jpg")), 613 | NewInputMediaVideo(FilePath("tests/video.mp4")), 614 | }) 615 | 616 | messages, err := bot.SendMediaGroup(cfg) 617 | if err != nil { 618 | t.Error(err) 619 | } 620 | 621 | if messages == nil { 622 | t.Error("No received messages") 623 | } 624 | 625 | if len(messages) != len(cfg.Media) { 626 | t.Errorf("Different number of messages: %d", len(messages)) 627 | } 628 | } 629 | 630 | func TestSendWithMediaGroupDocument(t *testing.T) { 631 | bot, _ := getBot(t) 632 | 633 | cfg := NewMediaGroup(ChatID, []interface{}{ 634 | NewInputMediaDocument(FileURL("https://i.imgur.com/unQLJIb.jpg")), 635 | NewInputMediaDocument(FilePath("tests/image.jpg")), 636 | }) 637 | 638 | messages, err := bot.SendMediaGroup(cfg) 639 | if err != nil { 640 | t.Error(err) 641 | } 642 | 643 | if messages == nil { 644 | t.Error("No received messages") 645 | } 646 | 647 | if len(messages) != len(cfg.Media) { 648 | t.Errorf("Different number of messages: %d", len(messages)) 649 | } 650 | } 651 | 652 | func TestSendWithMediaGroupAudio(t *testing.T) { 653 | bot, _ := getBot(t) 654 | 655 | cfg := NewMediaGroup(ChatID, []interface{}{ 656 | NewInputMediaAudio(FilePath("tests/audio.mp3")), 657 | NewInputMediaAudio(FilePath("tests/audio.mp3")), 658 | }) 659 | 660 | messages, err := bot.SendMediaGroup(cfg) 661 | if err != nil { 662 | t.Error(err) 663 | } 664 | 665 | if messages == nil { 666 | t.Error("No received messages") 667 | } 668 | 669 | if len(messages) != len(cfg.Media) { 670 | t.Errorf("Different number of messages: %d", len(messages)) 671 | } 672 | } 673 | 674 | func ExampleNewBotAPI() { 675 | bot, err := NewBotAPI("MyAwesomeBotToken") 676 | if err != nil { 677 | panic(err) 678 | } 679 | 680 | bot.Debug = true 681 | 682 | log.Printf("Authorized on account %s", bot.Self.UserName) 683 | 684 | u := NewUpdate(0) 685 | u.Timeout = 60 686 | 687 | updates := bot.GetUpdatesChan(u) 688 | 689 | // Optional: wait for updates and clear them if you don't want to handle 690 | // a large backlog of old messages 691 | time.Sleep(time.Millisecond * 500) 692 | updates.Clear() 693 | 694 | for update := range updates { 695 | if update.Message == nil { 696 | continue 697 | } 698 | 699 | log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text) 700 | 701 | msg := NewMessage(update.Message.Chat.ID, update.Message.Text) 702 | msg.ReplyToMessageID = update.Message.MessageID 703 | 704 | bot.Send(msg) 705 | } 706 | } 707 | 708 | func ExampleNewWebhook() { 709 | bot, err := NewBotAPI("MyAwesomeBotToken") 710 | if err != nil { 711 | panic(err) 712 | } 713 | 714 | bot.Debug = true 715 | 716 | log.Printf("Authorized on account %s", bot.Self.UserName) 717 | 718 | wh, err := NewWebhookWithCert("https://www.google.com:8443/"+bot.Token, FilePath("cert.pem")) 719 | 720 | if err != nil { 721 | panic(err) 722 | } 723 | 724 | _, err = bot.Request(wh) 725 | 726 | if err != nil { 727 | panic(err) 728 | } 729 | 730 | info, err := bot.GetWebhookInfo() 731 | 732 | if err != nil { 733 | panic(err) 734 | } 735 | 736 | if info.LastErrorDate != 0 { 737 | log.Printf("failed to set webhook: %s", info.LastErrorMessage) 738 | } 739 | 740 | updates := bot.ListenForWebhook("/" + bot.Token) 741 | go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil) 742 | 743 | for update := range updates { 744 | log.Printf("%+v\n", update) 745 | } 746 | } 747 | 748 | func ExampleWebhookHandler() { 749 | bot, err := NewBotAPI("MyAwesomeBotToken") 750 | if err != nil { 751 | panic(err) 752 | } 753 | 754 | bot.Debug = true 755 | 756 | log.Printf("Authorized on account %s", bot.Self.UserName) 757 | 758 | wh, err := NewWebhookWithCert("https://www.google.com:8443/"+bot.Token, FilePath("cert.pem")) 759 | 760 | if err != nil { 761 | panic(err) 762 | } 763 | 764 | _, err = bot.Request(wh) 765 | if err != nil { 766 | panic(err) 767 | } 768 | info, err := bot.GetWebhookInfo() 769 | if err != nil { 770 | panic(err) 771 | } 772 | if info.LastErrorDate != 0 { 773 | log.Printf("[Telegram callback failed]%s", info.LastErrorMessage) 774 | } 775 | 776 | http.HandleFunc("/"+bot.Token, func(w http.ResponseWriter, r *http.Request) { 777 | update, err := bot.HandleUpdate(r) 778 | if err != nil { 779 | log.Printf("%+v\n", err.Error()) 780 | } else { 781 | log.Printf("%+v\n", *update) 782 | } 783 | }) 784 | 785 | go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil) 786 | } 787 | 788 | func ExampleInlineConfig() { 789 | bot, err := NewBotAPI("MyAwesomeBotToken") // create new bot 790 | if err != nil { 791 | panic(err) 792 | } 793 | 794 | log.Printf("Authorized on account %s", bot.Self.UserName) 795 | 796 | u := NewUpdate(0) 797 | u.Timeout = 60 798 | 799 | updates := bot.GetUpdatesChan(u) 800 | 801 | for update := range updates { 802 | if update.InlineQuery == nil { // if no inline query, ignore it 803 | continue 804 | } 805 | 806 | article := NewInlineQueryResultArticle(update.InlineQuery.ID, "Echo", update.InlineQuery.Query) 807 | article.Description = update.InlineQuery.Query 808 | 809 | inlineConf := InlineConfig{ 810 | InlineQueryID: update.InlineQuery.ID, 811 | IsPersonal: true, 812 | CacheTime: 0, 813 | Results: []interface{}{article}, 814 | } 815 | 816 | if _, err := bot.Request(inlineConf); err != nil { 817 | log.Println(err) 818 | } 819 | } 820 | } 821 | 822 | func TestDeleteMessage(t *testing.T) { 823 | bot, _ := getBot(t) 824 | 825 | msg := NewMessage(ChatID, "A test message from the test library in telegram-bot-api") 826 | msg.ParseMode = ModeMarkdown 827 | message, _ := bot.Send(msg) 828 | 829 | deleteMessageConfig := DeleteMessageConfig{ 830 | ChatID: message.Chat.ID, 831 | MessageID: message.MessageID, 832 | } 833 | _, err := bot.Request(deleteMessageConfig) 834 | 835 | if err != nil { 836 | t.Error(err) 837 | } 838 | } 839 | 840 | func TestPinChatMessage(t *testing.T) { 841 | bot, _ := getBot(t) 842 | 843 | msg := NewMessage(SupergroupChatID, "A test message from the test library in telegram-bot-api") 844 | msg.ParseMode = ModeMarkdown 845 | message, _ := bot.Send(msg) 846 | 847 | pinChatMessageConfig := PinChatMessageConfig{ 848 | ChatID: message.Chat.ID, 849 | MessageID: message.MessageID, 850 | DisableNotification: false, 851 | } 852 | _, err := bot.Request(pinChatMessageConfig) 853 | 854 | if err != nil { 855 | t.Error(err) 856 | } 857 | } 858 | 859 | func TestUnpinChatMessage(t *testing.T) { 860 | bot, _ := getBot(t) 861 | 862 | msg := NewMessage(SupergroupChatID, "A test message from the test library in telegram-bot-api") 863 | msg.ParseMode = ModeMarkdown 864 | message, _ := bot.Send(msg) 865 | 866 | // We need pin message to unpin something 867 | pinChatMessageConfig := PinChatMessageConfig{ 868 | ChatID: message.Chat.ID, 869 | MessageID: message.MessageID, 870 | DisableNotification: false, 871 | } 872 | 873 | if _, err := bot.Request(pinChatMessageConfig); err != nil { 874 | t.Error(err) 875 | } 876 | 877 | unpinChatMessageConfig := UnpinChatMessageConfig{ 878 | ChatID: message.Chat.ID, 879 | MessageID: message.MessageID, 880 | } 881 | 882 | if _, err := bot.Request(unpinChatMessageConfig); err != nil { 883 | t.Error(err) 884 | } 885 | } 886 | 887 | func TestUnpinAllChatMessages(t *testing.T) { 888 | bot, _ := getBot(t) 889 | 890 | msg := NewMessage(SupergroupChatID, "A test message from the test library in telegram-bot-api") 891 | msg.ParseMode = ModeMarkdown 892 | message, _ := bot.Send(msg) 893 | 894 | pinChatMessageConfig := PinChatMessageConfig{ 895 | ChatID: message.Chat.ID, 896 | MessageID: message.MessageID, 897 | DisableNotification: true, 898 | } 899 | 900 | if _, err := bot.Request(pinChatMessageConfig); err != nil { 901 | t.Error(err) 902 | } 903 | 904 | unpinAllChatMessagesConfig := UnpinAllChatMessagesConfig{ 905 | ChatID: message.Chat.ID, 906 | } 907 | 908 | if _, err := bot.Request(unpinAllChatMessagesConfig); err != nil { 909 | t.Error(err) 910 | } 911 | } 912 | 913 | func TestPolls(t *testing.T) { 914 | bot, _ := getBot(t) 915 | 916 | poll := NewPoll(SupergroupChatID, "Are polls working?", "Yes", "No") 917 | 918 | msg, err := bot.Send(poll) 919 | if err != nil { 920 | t.Error(err) 921 | } 922 | 923 | result, err := bot.StopPoll(NewStopPoll(SupergroupChatID, msg.MessageID)) 924 | if err != nil { 925 | t.Error(err) 926 | } 927 | 928 | if result.Question != "Are polls working?" { 929 | t.Error("Poll question did not match") 930 | } 931 | 932 | if !result.IsClosed { 933 | t.Error("Poll did not end") 934 | } 935 | 936 | if result.Options[0].Text != "Yes" || result.Options[0].VoterCount != 0 || result.Options[1].Text != "No" || result.Options[1].VoterCount != 0 { 937 | t.Error("Poll options were incorrect") 938 | } 939 | } 940 | 941 | func TestSendDice(t *testing.T) { 942 | bot, _ := getBot(t) 943 | 944 | dice := NewDice(ChatID) 945 | 946 | msg, err := bot.Send(dice) 947 | if err != nil { 948 | t.Error("Unable to send dice roll") 949 | } 950 | 951 | if msg.Dice == nil { 952 | t.Error("Dice roll was not received") 953 | } 954 | } 955 | 956 | func TestCommands(t *testing.T) { 957 | bot, _ := getBot(t) 958 | 959 | setCommands := NewSetMyCommands(BotCommand{ 960 | Command: "test", 961 | Description: "a test command", 962 | }) 963 | 964 | if _, err := bot.Request(setCommands); err != nil { 965 | t.Error("Unable to set commands") 966 | } 967 | 968 | commands, err := bot.GetMyCommands() 969 | if err != nil { 970 | t.Error("Unable to get commands") 971 | } 972 | 973 | if len(commands) != 1 { 974 | t.Error("Incorrect number of commands returned") 975 | } 976 | 977 | if commands[0].Command != "test" || commands[0].Description != "a test command" { 978 | t.Error("Commands were incorrectly set") 979 | } 980 | 981 | setCommands = NewSetMyCommandsWithScope(NewBotCommandScopeAllPrivateChats(), BotCommand{ 982 | Command: "private", 983 | Description: "a private command", 984 | }) 985 | 986 | if _, err := bot.Request(setCommands); err != nil { 987 | t.Error("Unable to set commands") 988 | } 989 | 990 | commands, err = bot.GetMyCommandsWithConfig(NewGetMyCommandsWithScope(NewBotCommandScopeAllPrivateChats())) 991 | if err != nil { 992 | t.Error("Unable to get commands") 993 | } 994 | 995 | if len(commands) != 1 { 996 | t.Error("Incorrect number of commands returned") 997 | } 998 | 999 | if commands[0].Command != "private" || commands[0].Description != "a private command" { 1000 | t.Error("Commands were incorrectly set") 1001 | } 1002 | } 1003 | 1004 | // TODO: figure out why test is failing 1005 | // 1006 | // func TestEditMessageMedia(t *testing.T) { 1007 | // bot, _ := getBot(t) 1008 | 1009 | // msg := NewPhoto(ChatID, "tests/image.jpg") 1010 | // msg.Caption = "Test" 1011 | // m, err := bot.Send(msg) 1012 | 1013 | // if err != nil { 1014 | // t.Error(err) 1015 | // } 1016 | 1017 | // edit := EditMessageMediaConfig{ 1018 | // BaseEdit: BaseEdit{ 1019 | // ChatID: ChatID, 1020 | // MessageID: m.MessageID, 1021 | // }, 1022 | // Media: NewInputMediaVideo(FilePath("tests/video.mp4")), 1023 | // } 1024 | 1025 | // _, err = bot.Request(edit) 1026 | // if err != nil { 1027 | // t.Error(err) 1028 | // } 1029 | // } 1030 | 1031 | func TestPrepareInputMediaForParams(t *testing.T) { 1032 | media := []interface{}{ 1033 | NewInputMediaPhoto(FilePath("tests/image.jpg")), 1034 | NewInputMediaVideo(FileID("test")), 1035 | } 1036 | 1037 | prepared := prepareInputMediaForParams(media) 1038 | 1039 | if media[0].(InputMediaPhoto).Media != FilePath("tests/image.jpg") { 1040 | t.Error("Original media was changed") 1041 | } 1042 | 1043 | if prepared[0].(InputMediaPhoto).Media != fileAttach("attach://file-0") { 1044 | t.Error("New media was not replaced") 1045 | } 1046 | 1047 | if prepared[1].(InputMediaVideo).Media != FileID("test") { 1048 | t.Error("Passthrough value was not the same") 1049 | } 1050 | } 1051 | -------------------------------------------------------------------------------- /docs/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Getting Started](./getting-started/README.md) 4 | - [Library Structure](./getting-started/library-structure.md) 5 | - [Files](./getting-started/files.md) 6 | - [Important Notes](./getting-started/important-notes.md) 7 | - [Examples](./examples/README.md) 8 | - [Command Handling](./examples/command-handling.md) 9 | - [Keyboard](./examples/keyboard.md) 10 | - [Inline Keyboard](./examples/inline-keyboard.md) 11 | - [Change Log](./changelog.md) 12 | 13 | # Contributing 14 | 15 | - [Internals](./internals/README.md) 16 | - [Adding Endpoints](./internals/adding-endpoints.md) 17 | - [Uploading Files](./internals/uploading-files.md) 18 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## v5.4.0 4 | 5 | - Remove all methods that return `(APIResponse, error)`. 6 | - Use the `Request` method instead. 7 | - For more information, see [Library Structure][library-structure]. 8 | - Remove all `New*Upload` and `New*Share` methods, replace with `New*`. 9 | - Use different [file types][files] to specify if upload or share. 10 | - Rename `UploadFile` to `UploadFiles`, accept `[]RequestFile` instead of a 11 | single fieldname and file. 12 | - Fix methods returning `APIResponse` and errors to always use pointers. 13 | - Update user IDs to `int64` because of Bot API changes. 14 | - Add missing Bot API features. 15 | 16 | [library-structure]: ./getting-started/library-structure.md#methods 17 | [files]: ./getting-started/files.md 18 | -------------------------------------------------------------------------------- /docs/examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | With a better understanding of how the library works, let's look at some more 4 | examples showing off some of Telegram's features. 5 | -------------------------------------------------------------------------------- /docs/examples/command-handling.md: -------------------------------------------------------------------------------- 1 | # Command Handling 2 | 3 | This is a simple example of changing behavior based on a provided command. 4 | 5 | ```go 6 | package main 7 | 8 | import ( 9 | "log" 10 | "os" 11 | 12 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 13 | ) 14 | 15 | func main() { 16 | bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN")) 17 | if err != nil { 18 | log.Panic(err) 19 | } 20 | 21 | bot.Debug = true 22 | 23 | log.Printf("Authorized on account %s", bot.Self.UserName) 24 | 25 | u := tgbotapi.NewUpdate(0) 26 | u.Timeout = 60 27 | 28 | updates := bot.GetUpdatesChan(u) 29 | 30 | for update := range updates { 31 | if update.Message == nil { // ignore any non-Message updates 32 | continue 33 | } 34 | 35 | if !update.Message.IsCommand() { // ignore any non-command Messages 36 | continue 37 | } 38 | 39 | // Create a new MessageConfig. We don't have text yet, 40 | // so we leave it empty. 41 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, "") 42 | 43 | // Extract the command from the Message. 44 | switch update.Message.Command() { 45 | case "help": 46 | msg.Text = "I understand /sayhi and /status." 47 | case "sayhi": 48 | msg.Text = "Hi :)" 49 | case "status": 50 | msg.Text = "I'm ok." 51 | default: 52 | msg.Text = "I don't know that command" 53 | } 54 | 55 | if _, err := bot.Send(msg); err != nil { 56 | log.Panic(err) 57 | } 58 | } 59 | } 60 | ``` 61 | -------------------------------------------------------------------------------- /docs/examples/inline-keyboard.md: -------------------------------------------------------------------------------- 1 | # Inline Keyboard 2 | 3 | This bot waits for you to send it the message "open" before sending you an 4 | inline keyboard containing a URL and some numbers. When a number is clicked, it 5 | sends you a message with your selected number. 6 | 7 | ```go 8 | package main 9 | 10 | import ( 11 | "log" 12 | "os" 13 | 14 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 15 | ) 16 | 17 | var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup( 18 | tgbotapi.NewInlineKeyboardRow( 19 | tgbotapi.NewInlineKeyboardButtonURL("1.com", "http://1.com"), 20 | tgbotapi.NewInlineKeyboardButtonData("2", "2"), 21 | tgbotapi.NewInlineKeyboardButtonData("3", "3"), 22 | ), 23 | tgbotapi.NewInlineKeyboardRow( 24 | tgbotapi.NewInlineKeyboardButtonData("4", "4"), 25 | tgbotapi.NewInlineKeyboardButtonData("5", "5"), 26 | tgbotapi.NewInlineKeyboardButtonData("6", "6"), 27 | ), 28 | ) 29 | 30 | func main() { 31 | bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN")) 32 | if err != nil { 33 | log.Panic(err) 34 | } 35 | 36 | bot.Debug = true 37 | 38 | log.Printf("Authorized on account %s", bot.Self.UserName) 39 | 40 | u := tgbotapi.NewUpdate(0) 41 | u.Timeout = 60 42 | 43 | updates := bot.GetUpdatesChan(u) 44 | 45 | // Loop through each update. 46 | for update := range updates { 47 | // Check if we've gotten a message update. 48 | if update.Message != nil { 49 | // Construct a new message from the given chat ID and containing 50 | // the text that we received. 51 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text) 52 | 53 | // If the message was open, add a copy of our numeric keyboard. 54 | switch update.Message.Text { 55 | case "open": 56 | msg.ReplyMarkup = numericKeyboard 57 | 58 | } 59 | 60 | // Send the message. 61 | if _, err = bot.Send(msg); err != nil { 62 | panic(err) 63 | } 64 | } else if update.CallbackQuery != nil { 65 | // Respond to the callback query, telling Telegram to show the user 66 | // a message with the data received. 67 | callback := tgbotapi.NewCallback(update.CallbackQuery.ID, update.CallbackQuery.Data) 68 | if _, err := bot.Request(callback); err != nil { 69 | panic(err) 70 | } 71 | 72 | // And finally, send a message containing the data received. 73 | msg := tgbotapi.NewMessage(update.CallbackQuery.Message.Chat.ID, update.CallbackQuery.Data) 74 | if _, err := bot.Send(msg); err != nil { 75 | panic(err) 76 | } 77 | } 78 | } 79 | } 80 | ``` 81 | -------------------------------------------------------------------------------- /docs/examples/keyboard.md: -------------------------------------------------------------------------------- 1 | # Keyboard 2 | 3 | This bot shows a numeric keyboard when you send a "open" message and hides it 4 | when you send "close" message. 5 | 6 | ```go 7 | package main 8 | 9 | import ( 10 | "log" 11 | "os" 12 | 13 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 14 | ) 15 | 16 | var numericKeyboard = tgbotapi.NewReplyKeyboard( 17 | tgbotapi.NewKeyboardButtonRow( 18 | tgbotapi.NewKeyboardButton("1"), 19 | tgbotapi.NewKeyboardButton("2"), 20 | tgbotapi.NewKeyboardButton("3"), 21 | ), 22 | tgbotapi.NewKeyboardButtonRow( 23 | tgbotapi.NewKeyboardButton("4"), 24 | tgbotapi.NewKeyboardButton("5"), 25 | tgbotapi.NewKeyboardButton("6"), 26 | ), 27 | ) 28 | 29 | func main() { 30 | bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN")) 31 | if err != nil { 32 | log.Panic(err) 33 | } 34 | 35 | bot.Debug = true 36 | 37 | log.Printf("Authorized on account %s", bot.Self.UserName) 38 | 39 | u := tgbotapi.NewUpdate(0) 40 | u.Timeout = 60 41 | 42 | updates := bot.GetUpdatesChan(u) 43 | 44 | for update := range updates { 45 | if update.Message == nil { // ignore non-Message updates 46 | continue 47 | } 48 | 49 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text) 50 | 51 | switch update.Message.Text { 52 | case "open": 53 | msg.ReplyMarkup = numericKeyboard 54 | case "close": 55 | msg.ReplyMarkup = tgbotapi.NewRemoveKeyboard(true) 56 | } 57 | 58 | if _, err := bot.Send(msg); err != nil { 59 | log.Panic(err) 60 | } 61 | } 62 | } 63 | ``` 64 | -------------------------------------------------------------------------------- /docs/getting-started/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | This library is designed as a simple wrapper around the Telegram Bot API. 4 | It's encouraged to read [Telegram's docs][telegram-docs] first to get an 5 | understanding of what Bots are capable of doing. They also provide some good 6 | approaches to solve common problems. 7 | 8 | [telegram-docs]: https://core.telegram.org/bots 9 | 10 | ## Installing 11 | 12 | ```bash 13 | go get -u github.com/go-telegram-bot-api/telegram-bot-api/v5 14 | ``` 15 | 16 | ## A Simple Bot 17 | 18 | To walk through the basics, let's create a simple echo bot that replies to your 19 | messages repeating what you said. Make sure you get an API token from 20 | [@Botfather][botfather] before continuing. 21 | 22 | Let's start by constructing a new [BotAPI][bot-api-docs]. 23 | 24 | [botfather]: https://t.me/Botfather 25 | [bot-api-docs]: https://pkg.go.dev/github.com/go-telegram-bot-api/telegram-bot-api/v5?tab=doc#BotAPI 26 | 27 | ```go 28 | package main 29 | 30 | import ( 31 | "os" 32 | 33 | tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" 34 | ) 35 | 36 | func main() { 37 | bot, err := tgbotapi.NewBotAPI(os.Getenv("TELEGRAM_APITOKEN")) 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | bot.Debug = true 43 | } 44 | ``` 45 | 46 | Instead of typing the API token directly into the file, we're using 47 | environment variables. This makes it easy to configure our Bot to use the right 48 | account and prevents us from leaking our real token into the world. Anyone with 49 | your token can send and receive messages from your Bot! 50 | 51 | We've also set `bot.Debug = true` in order to get more information about the 52 | requests being sent to Telegram. If you run the example above, you'll see 53 | information about a request to the [`getMe`][get-me] endpoint. The library 54 | automatically calls this to ensure your token is working as expected. It also 55 | fills in the `Self` field in your `BotAPI` struct with information about the 56 | Bot. 57 | 58 | Now that we've connected to Telegram, let's start getting updates and doing 59 | things. We can add this code in right after the line enabling debug mode. 60 | 61 | [get-me]: https://core.telegram.org/bots/api#getme 62 | 63 | ```go 64 | // Create a new UpdateConfig struct with an offset of 0. Offsets are used 65 | // to make sure Telegram knows we've handled previous values and we don't 66 | // need them repeated. 67 | updateConfig := tgbotapi.NewUpdate(0) 68 | 69 | // Tell Telegram we should wait up to 30 seconds on each request for an 70 | // update. This way we can get information just as quickly as making many 71 | // frequent requests without having to send nearly as many. 72 | updateConfig.Timeout = 30 73 | 74 | // Start polling Telegram for updates. 75 | updates := bot.GetUpdatesChan(updateConfig) 76 | 77 | // Let's go through each update that we're getting from Telegram. 78 | for update := range updates { 79 | // Telegram can send many types of updates depending on what your Bot 80 | // is up to. We only want to look at messages for now, so we can 81 | // discard any other updates. 82 | if update.Message == nil { 83 | continue 84 | } 85 | 86 | // Now that we know we've gotten a new message, we can construct a 87 | // reply! We'll take the Chat ID and Text from the incoming message 88 | // and use it to create a new message. 89 | msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text) 90 | // We'll also say that this message is a reply to the previous message. 91 | // For any other specifications than Chat ID or Text, you'll need to 92 | // set fields on the `MessageConfig`. 93 | msg.ReplyToMessageID = update.Message.MessageID 94 | 95 | // Okay, we're sending our message off! We don't care about the message 96 | // we just sent, so we'll discard it. 97 | if _, err := bot.Send(msg); err != nil { 98 | // Note that panics are a bad way to handle errors. Telegram can 99 | // have service outages or network errors, you should retry sending 100 | // messages or more gracefully handle failures. 101 | panic(err) 102 | } 103 | } 104 | ``` 105 | 106 | Congratulations! You've made your very own bot! 107 | 108 | Now that you've got some of the basics down, we can start talking about how the 109 | library is structured and more advanced features. 110 | -------------------------------------------------------------------------------- /docs/getting-started/files.md: -------------------------------------------------------------------------------- 1 | # Files 2 | 3 | Telegram supports specifying files in many different formats. In order to 4 | accommodate them all, there are multiple structs and type aliases required. 5 | 6 | All of these types implement the `RequestFileData` interface. 7 | 8 | | Type | Description | 9 | | ------------ | ------------------------------------------------------------------------- | 10 | | `FilePath` | A local path to a file | 11 | | `FileID` | Existing file ID on Telegram's servers | 12 | | `FileURL` | URL to file, must be served with expected MIME type | 13 | | `FileReader` | Use an `io.Reader` to provide a file. Lazily read to save memory. | 14 | | `FileBytes` | `[]byte` containing file data. Prefer to use `FileReader` to save memory. | 15 | 16 | ## `FilePath` 17 | 18 | A path to a local file. 19 | 20 | ```go 21 | file := tgbotapi.FilePath("tests/image.jpg") 22 | ``` 23 | 24 | ## `FileID` 25 | 26 | An ID previously uploaded to Telegram. IDs may only be reused by the same bot 27 | that received them. Additionally, thumbnail IDs cannot be reused. 28 | 29 | ```go 30 | file := tgbotapi.FileID("AgACAgIAAxkDAALesF8dCjAAAa_…") 31 | ``` 32 | 33 | ## `FileURL` 34 | 35 | A URL to an existing resource. It must be served with a correct MIME type to 36 | work as expected. 37 | 38 | ```go 39 | file := tgbotapi.FileURL("https://i.imgur.com/unQLJIb.jpg") 40 | ``` 41 | 42 | ## `FileReader` 43 | 44 | Use an `io.Reader` to provide file contents as needed. Requires a filename for 45 | the virtual file. 46 | 47 | ```go 48 | var reader io.Reader 49 | 50 | file := tgbotapi.FileReader{ 51 | Name: "image.jpg", 52 | Reader: reader, 53 | } 54 | ``` 55 | 56 | ## `FileBytes` 57 | 58 | Use a `[]byte` to provide file contents. Generally try to avoid this as it 59 | results in high memory usage. Also requires a filename for the virtual file. 60 | 61 | ```go 62 | var data []byte 63 | 64 | file := tgbotapi.FileBytes{ 65 | Name: "image.jpg", 66 | Bytes: data, 67 | } 68 | ``` 69 | -------------------------------------------------------------------------------- /docs/getting-started/important-notes.md: -------------------------------------------------------------------------------- 1 | # Important Notes 2 | 3 | The Telegram Bot API has a few potentially unanticipated behaviors. Here are a 4 | few of them. If any behavior was surprising to you, please feel free to open a 5 | pull request! 6 | 7 | ## Callback Queries 8 | 9 | - Every callback query must be answered, even if there is nothing to display to 10 | the user. Failure to do so will show a loading icon on the keyboard until the 11 | operation times out. 12 | 13 | ## ChatMemberUpdated 14 | 15 | - In order to receive `ChatMember` updates, you must explicitly add 16 | `UpdateTypeChatMember` to your `AllowedUpdates` when getting updates or 17 | setting your webhook. 18 | 19 | ## Entities use UTF16 20 | 21 | - When extracting text entities using offsets and lengths, characters can appear 22 | to be in incorrect positions. This is because Telegram uses UTF16 lengths 23 | while Golang uses UTF8. It's possible to convert between the two, see 24 | [issue #231][issue-231] for more details. 25 | 26 | [issue-231]: https://github.com/go-telegram-bot-api/telegram-bot-api/issues/231 27 | 28 | ## GetUpdatesChan 29 | 30 | - This method is very basic and likely unsuitable for production use. Consider 31 | creating your own implementation instead, as it's very simple to replicate. 32 | - This method only allows your bot to process one update at a time. You can 33 | spawn goroutines to handle updates concurrently or switch to webhooks instead. 34 | Webhooks are suggested for high traffic bots. 35 | 36 | ## Nil Updates 37 | 38 | - At most one of the fields in an `Update` will be set to a non-nil value. When 39 | evaluating updates, you must make sure you check that the field is not nil 40 | before trying to access any of it's fields. 41 | 42 | ## Privacy Mode 43 | 44 | - By default, bots only get updates directly addressed to them. If you need to 45 | get all messages, you must disable privacy mode with Botfather. Bots already 46 | added to groups will need to be removed and re-added for the changes to take 47 | effect. You can read more on the [Telegram Bot API docs][api-docs]. 48 | 49 | [api-docs]: https://core.telegram.org/bots/faq#what-messages-will-my-bot-get 50 | 51 | ## User and Chat ID size 52 | 53 | - These types require up to 52 significant bits to store correctly, making a 54 | 64-bit integer type required in most languages. They are already `int64` types 55 | in this library, but make sure you use correct types when saving them to a 56 | database or passing them to another language. 57 | -------------------------------------------------------------------------------- /docs/getting-started/library-structure.md: -------------------------------------------------------------------------------- 1 | # Library Structure 2 | 3 | This library is generally broken into three components you need to understand. 4 | 5 | ## Configs 6 | 7 | Configs are collections of fields related to a single request. For example, if 8 | one wanted to use the `sendMessage` endpoint, you could use the `MessageConfig` 9 | struct to configure the request. There is a one-to-one relationship between 10 | Telegram endpoints and configs. They generally have the naming pattern of 11 | removing the `send` prefix and they all end with the `Config` suffix. They 12 | generally implement the `Chattable` interface. If they can send files, they 13 | implement the `Fileable` interface. 14 | 15 | ## Helpers 16 | 17 | Helpers are easier ways of constructing common Configs. Instead of having to 18 | create a `MessageConfig` struct and remember to set the `ChatID` and `Text`, 19 | you can use the `NewMessage` helper method. It takes the two required parameters 20 | for the request to succeed. You can then set fields on the resulting 21 | `MessageConfig` after it's creation. They are generally named the same as 22 | method names except with `send` replaced with `New`. 23 | 24 | ## Methods 25 | 26 | Methods are used to send Configs after they are constructed. Generally, 27 | `Request` is the lowest level method you'll have to call. It accepts a 28 | `Chattable` parameter and knows how to upload files if needed. It returns an 29 | `APIResponse`, the most general return type from the Bot API. This method is 30 | called for any endpoint that doesn't have a more specific return type. For 31 | example, `setWebhook` only returns `true` or an error. Other methods may have 32 | more specific return types. The `getFile` endpoint returns a `File`. Almost 33 | every other method returns a `Message`, which you can use `Send` to obtain. 34 | 35 | There's lower level methods such as `MakeRequest` which require an endpoint and 36 | parameters instead of accepting configs. These are primarily used internally. 37 | If you find yourself having to use them, please open an issue. 38 | -------------------------------------------------------------------------------- /docs/internals/README.md: -------------------------------------------------------------------------------- 1 | # Internals 2 | 3 | If you want to contribute to the project, here's some more information about 4 | the internal structure of the library. 5 | -------------------------------------------------------------------------------- /docs/internals/adding-endpoints.md: -------------------------------------------------------------------------------- 1 | # Adding Endpoints 2 | 3 | This is mostly useful if you've managed to catch a new Telegram Bot API update 4 | before the library can get updated. It's also a great source of information 5 | about how the types work internally. 6 | 7 | ## Creating the Config 8 | 9 | The first step in adding a new endpoint is to create a new Config type for it. 10 | These belong in `configs.go`. 11 | 12 | Let's try and add the `deleteMessage` endpoint. We can see it requires two 13 | fields; `chat_id` and `message_id`. We can create a struct for these. 14 | 15 | ```go 16 | type DeleteMessageConfig struct { 17 | ChatID ??? 18 | MessageID int 19 | } 20 | ``` 21 | 22 | What type should `ChatID` be? Telegram allows specifying numeric chat IDs or 23 | channel usernames. Golang doesn't have union types, and interfaces are entirely 24 | untyped. This library solves this by adding two fields, a `ChatID` and a 25 | `ChannelUsername`. We can now write the struct as follows. 26 | 27 | ```go 28 | type DeleteMessageConfig struct { 29 | ChannelUsername string 30 | ChatID int64 31 | MessageID int 32 | } 33 | ``` 34 | 35 | Note that `ChatID` is an `int64`. Telegram chat IDs can be greater than 32 bits. 36 | 37 | Okay, we now have our struct. But we can't send it yet. It doesn't implement 38 | `Chattable` so it won't work with `Request` or `Send`. 39 | 40 | ### Making it `Chattable` 41 | 42 | We can see that `Chattable` only requires a few methods. 43 | 44 | ```go 45 | type Chattable interface { 46 | params() (Params, error) 47 | method() string 48 | } 49 | ``` 50 | 51 | `params` is the fields associated with the request. `method` is the endpoint 52 | that this Config is associated with. 53 | 54 | Implementing the `method` is easy, so let's start with that. 55 | 56 | ```go 57 | func (config DeleteMessageConfig) method() string { 58 | return "deleteMessage" 59 | } 60 | ``` 61 | 62 | Now we have to add the `params`. The `Params` type is an alias for 63 | `map[string]string`. Telegram expects only a single field for `chat_id`, so we 64 | have to determine what data to send. 65 | 66 | We could use an if statement to determine which field to get the value from. 67 | However, as this is a relatively common operation, there's helper methods for 68 | `Params`. We can use the `AddFirstValid` method to go through each possible 69 | value and stop when it discovers a valid one. Before writing your own Config, 70 | it's worth taking a look through `params.go` to see what other helpers exist. 71 | 72 | Now we can take a look at what a completed `params` method looks like. 73 | 74 | ```go 75 | func (config DeleteMessageConfig) params() (Params, error) { 76 | params := make(Params) 77 | 78 | params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) 79 | params.AddNonZero("message_id", config.MessageID) 80 | 81 | return params, nil 82 | } 83 | ``` 84 | 85 | ### Uploading Files 86 | 87 | Let's imagine that for some reason deleting a message requires a document to be 88 | uploaded and an optional thumbnail for that document. To add file upload 89 | support we need to implement `Fileable`. This only requires one additional 90 | method. 91 | 92 | ```go 93 | type Fileable interface { 94 | Chattable 95 | files() []RequestFile 96 | } 97 | ``` 98 | 99 | First, let's add some fields to store our files in. Most of the standard Configs 100 | have similar fields for their files. 101 | 102 | ```diff 103 | type DeleteMessageConfig struct { 104 | ChannelUsername string 105 | ChatID int64 106 | MessageID int 107 | + Delete RequestFileData 108 | + Thumb RequestFileData 109 | } 110 | ``` 111 | 112 | Adding another method is pretty simple. We'll always add a file named `delete` 113 | and add the `thumb` file if we have one. 114 | 115 | ```go 116 | func (config DeleteMessageConfig) files() []RequestFile { 117 | files := []RequestFile{{ 118 | Name: "delete", 119 | Data: config.Delete, 120 | }} 121 | 122 | if config.Thumb != nil { 123 | files = append(files, RequestFile{ 124 | Name: "thumb", 125 | Data: config.Thumb, 126 | }) 127 | } 128 | 129 | return files 130 | } 131 | ``` 132 | 133 | And now our files will upload! It will transparently handle uploads whether File 134 | is a `FilePath`, `FileURL`, `FileBytes`, `FileReader`, or `FileID`. 135 | 136 | ### Base Configs 137 | 138 | Certain Configs have repeated elements. For example, many of the items sent to a 139 | chat have `ChatID` or `ChannelUsername` fields, along with `ReplyToMessageID`, 140 | `ReplyMarkup`, and `DisableNotification`. Instead of implementing all of this 141 | code for each item, there's a `BaseChat` that handles it for your Config. 142 | Simply embed it in your struct to get all of those fields. 143 | 144 | There's only a few fields required for the `MessageConfig` struct after 145 | embedding the `BaseChat` struct. 146 | 147 | ```go 148 | type MessageConfig struct { 149 | BaseChat 150 | Text string 151 | ParseMode string 152 | DisableWebPagePreview bool 153 | } 154 | ``` 155 | 156 | It also inherits the `params` method from `BaseChat`. This allows you to call 157 | it, then you only have to add your new fields. 158 | 159 | ```go 160 | func (config MessageConfig) params() (Params, error) { 161 | params, err := config.BaseChat.params() 162 | if err != nil { 163 | return params, err 164 | } 165 | 166 | params.AddNonEmpty("text", config.Text) 167 | // Add your other fields 168 | 169 | return params, nil 170 | } 171 | ``` 172 | 173 | Similarly, there's a `BaseFile` struct for adding an associated file and 174 | `BaseEdit` struct for editing messages. 175 | 176 | ## Making it Friendly 177 | 178 | After we've got a Config type, we'll want to make it more user-friendly. We can 179 | do this by adding a new helper to `helpers.go`. These are functions that take 180 | in the required data for the request to succeed and populate a Config. 181 | 182 | Telegram only requires two fields to call `deleteMessage`, so this will be fast. 183 | 184 | ```go 185 | func NewDeleteMessage(chatID int64, messageID int) DeleteMessageConfig { 186 | return DeleteMessageConfig{ 187 | ChatID: chatID, 188 | MessageID: messageID, 189 | } 190 | } 191 | ``` 192 | 193 | Sometimes it makes sense to add more helpers if there's methods where you have 194 | to set exactly one field. You can also add helpers that accept a `username` 195 | string for channels if it's a common operation. 196 | 197 | And that's it! You've added a new method. 198 | -------------------------------------------------------------------------------- /docs/internals/uploading-files.md: -------------------------------------------------------------------------------- 1 | # Uploading Files 2 | 3 | To make files work as expected, there's a lot going on behind the scenes. Make 4 | sure to read through the [Files](../getting-started/files.md) section in 5 | Getting Started first as we'll be building on that information. 6 | 7 | This section only talks about file uploading. For non-uploaded files such as 8 | URLs and file IDs, you just need to pass a string. 9 | 10 | ## Fields 11 | 12 | Let's start by talking about how the library represents files as part of a 13 | Config. 14 | 15 | ### Static Fields 16 | 17 | Most endpoints use static file fields. For example, `sendPhoto` expects a single 18 | file named `photo`. All we have to do is set that single field with the correct 19 | value (either a string or multipart file). Methods like `sendDocument` take two 20 | file uploads, a `document` and a `thumb`. These are pretty straightforward. 21 | 22 | Remembering that the `Fileable` interface only requires one method, let's 23 | implement it for `DocumentConfig`. 24 | 25 | ```go 26 | func (config DocumentConfig) files() []RequestFile { 27 | // We can have multiple files, so we'll create an array. We also know that 28 | // there always is a document file, so initialize the array with that. 29 | files := []RequestFile{{ 30 | Name: "document", 31 | Data: config.File, 32 | }} 33 | 34 | // We'll only add a file if we have one. 35 | if config.Thumb != nil { 36 | files = append(files, RequestFile{ 37 | Name: "thumb", 38 | Data: config.Thumb, 39 | }) 40 | } 41 | 42 | return files 43 | } 44 | ``` 45 | 46 | Telegram also supports the `attach://` syntax (discussed more later) for 47 | thumbnails, but there's no reason to make things more complicated. 48 | 49 | ### Dynamic Fields 50 | 51 | Of course, not everything can be so simple. Methods like `sendMediaGroup` 52 | can accept many files, and each file can have custom markup. Using a static 53 | field isn't possible because we need to specify which field is attached to each 54 | item. Telegram introduced the `attach://` syntax for this. 55 | 56 | Let's follow through creating a new media group with string and file uploads. 57 | 58 | First, we start by creating some `InputMediaPhoto`. 59 | 60 | ```go 61 | photo := tgbotapi.NewInputMediaPhoto(tgbotapi.FilePath("tests/image.jpg")) 62 | url := tgbotapi.NewInputMediaPhoto(tgbotapi.FileURL("https://i.imgur.com/unQLJIb.jpg")) 63 | ``` 64 | 65 | This created a new `InputMediaPhoto` struct, with a type of `photo` and the 66 | media interface that we specified. 67 | 68 | We'll now create our media group with the photo and URL. 69 | 70 | ```go 71 | mediaGroup := NewMediaGroup(ChatID, []interface{}{ 72 | photo, 73 | url, 74 | }) 75 | ``` 76 | 77 | A `MediaGroupConfig` stores all the media in an array of interfaces. We now 78 | have all the data we need to upload, but how do we figure out field names for 79 | uploads? We didn't specify `attach://unique-file` anywhere. 80 | 81 | When the library goes to upload the files, it looks at the `params` and `files` 82 | for the Config. The params are generated by transforming the file into a value 83 | more suitable for uploading, file IDs and URLs are untouched but uploaded types 84 | are all changed into `attach://file-%d`. When collecting a list of files to 85 | upload, it names them the same way. This creates a nearly transparent way of 86 | handling multiple files in the background without the user having to consider 87 | what's going on. 88 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-telegram-bot-api/telegram-bot-api/v5 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-telegram-bot-api/telegram-bot-api/4126fa611266940425a9dfd37e0c92ba47881718/go.sum -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | package tgbotapi 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "net/url" 10 | "sort" 11 | "strings" 12 | ) 13 | 14 | // NewMessage creates a new Message. 15 | // 16 | // chatID is where to send it, text is the message text. 17 | func NewMessage(chatID int64, text string) MessageConfig { 18 | return MessageConfig{ 19 | BaseChat: BaseChat{ 20 | ChatID: chatID, 21 | ReplyToMessageID: 0, 22 | }, 23 | Text: text, 24 | DisableWebPagePreview: false, 25 | } 26 | } 27 | 28 | // NewDeleteMessage creates a request to delete a message. 29 | func NewDeleteMessage(chatID int64, messageID int) DeleteMessageConfig { 30 | return DeleteMessageConfig{ 31 | ChatID: chatID, 32 | MessageID: messageID, 33 | } 34 | } 35 | 36 | // NewMessageToChannel creates a new Message that is sent to a channel 37 | // by username. 38 | // 39 | // username is the username of the channel, text is the message text, 40 | // and the username should be in the form of `@username`. 41 | func NewMessageToChannel(username string, text string) MessageConfig { 42 | return MessageConfig{ 43 | BaseChat: BaseChat{ 44 | ChannelUsername: username, 45 | }, 46 | Text: text, 47 | } 48 | } 49 | 50 | // NewForward creates a new forward. 51 | // 52 | // chatID is where to send it, fromChatID is the source chat, 53 | // and messageID is the ID of the original message. 54 | func NewForward(chatID int64, fromChatID int64, messageID int) ForwardConfig { 55 | return ForwardConfig{ 56 | BaseChat: BaseChat{ChatID: chatID}, 57 | FromChatID: fromChatID, 58 | MessageID: messageID, 59 | } 60 | } 61 | 62 | // NewCopyMessage creates a new copy message. 63 | // 64 | // chatID is where to send it, fromChatID is the source chat, 65 | // and messageID is the ID of the original message. 66 | func NewCopyMessage(chatID int64, fromChatID int64, messageID int) CopyMessageConfig { 67 | return CopyMessageConfig{ 68 | BaseChat: BaseChat{ChatID: chatID}, 69 | FromChatID: fromChatID, 70 | MessageID: messageID, 71 | } 72 | } 73 | 74 | // NewPhoto creates a new sendPhoto request. 75 | // 76 | // chatID is where to send it, file is a string path to the file, 77 | // FileReader, or FileBytes. 78 | // 79 | // Note that you must send animated GIFs as a document. 80 | func NewPhoto(chatID int64, file RequestFileData) PhotoConfig { 81 | return PhotoConfig{ 82 | BaseFile: BaseFile{ 83 | BaseChat: BaseChat{ChatID: chatID}, 84 | File: file, 85 | }, 86 | } 87 | } 88 | 89 | // NewPhotoToChannel creates a new photo uploader to send a photo to a channel. 90 | // 91 | // Note that you must send animated GIFs as a document. 92 | func NewPhotoToChannel(username string, file RequestFileData) PhotoConfig { 93 | return PhotoConfig{ 94 | BaseFile: BaseFile{ 95 | BaseChat: BaseChat{ 96 | ChannelUsername: username, 97 | }, 98 | File: file, 99 | }, 100 | } 101 | } 102 | 103 | // NewAudio creates a new sendAudio request. 104 | func NewAudio(chatID int64, file RequestFileData) AudioConfig { 105 | return AudioConfig{ 106 | BaseFile: BaseFile{ 107 | BaseChat: BaseChat{ChatID: chatID}, 108 | File: file, 109 | }, 110 | } 111 | } 112 | 113 | // NewDocument creates a new sendDocument request. 114 | func NewDocument(chatID int64, file RequestFileData) DocumentConfig { 115 | return DocumentConfig{ 116 | BaseFile: BaseFile{ 117 | BaseChat: BaseChat{ChatID: chatID}, 118 | File: file, 119 | }, 120 | } 121 | } 122 | 123 | // NewSticker creates a new sendSticker request. 124 | func NewSticker(chatID int64, file RequestFileData) StickerConfig { 125 | return StickerConfig{ 126 | BaseFile: BaseFile{ 127 | BaseChat: BaseChat{ChatID: chatID}, 128 | File: file, 129 | }, 130 | } 131 | } 132 | 133 | // NewVideo creates a new sendVideo request. 134 | func NewVideo(chatID int64, file RequestFileData) VideoConfig { 135 | return VideoConfig{ 136 | BaseFile: BaseFile{ 137 | BaseChat: BaseChat{ChatID: chatID}, 138 | File: file, 139 | }, 140 | } 141 | } 142 | 143 | // NewAnimation creates a new sendAnimation request. 144 | func NewAnimation(chatID int64, file RequestFileData) AnimationConfig { 145 | return AnimationConfig{ 146 | BaseFile: BaseFile{ 147 | BaseChat: BaseChat{ChatID: chatID}, 148 | File: file, 149 | }, 150 | } 151 | } 152 | 153 | // NewVideoNote creates a new sendVideoNote request. 154 | // 155 | // chatID is where to send it, file is a string path to the file, 156 | // FileReader, or FileBytes. 157 | func NewVideoNote(chatID int64, length int, file RequestFileData) VideoNoteConfig { 158 | return VideoNoteConfig{ 159 | BaseFile: BaseFile{ 160 | BaseChat: BaseChat{ChatID: chatID}, 161 | File: file, 162 | }, 163 | Length: length, 164 | } 165 | } 166 | 167 | // NewVoice creates a new sendVoice request. 168 | func NewVoice(chatID int64, file RequestFileData) VoiceConfig { 169 | return VoiceConfig{ 170 | BaseFile: BaseFile{ 171 | BaseChat: BaseChat{ChatID: chatID}, 172 | File: file, 173 | }, 174 | } 175 | } 176 | 177 | // NewMediaGroup creates a new media group. Files should be an array of 178 | // two to ten InputMediaPhoto or InputMediaVideo. 179 | func NewMediaGroup(chatID int64, files []interface{}) MediaGroupConfig { 180 | return MediaGroupConfig{ 181 | ChatID: chatID, 182 | Media: files, 183 | } 184 | } 185 | 186 | // NewInputMediaPhoto creates a new InputMediaPhoto. 187 | func NewInputMediaPhoto(media RequestFileData) InputMediaPhoto { 188 | return InputMediaPhoto{ 189 | BaseInputMedia{ 190 | Type: "photo", 191 | Media: media, 192 | }, 193 | } 194 | } 195 | 196 | // NewInputMediaVideo creates a new InputMediaVideo. 197 | func NewInputMediaVideo(media RequestFileData) InputMediaVideo { 198 | return InputMediaVideo{ 199 | BaseInputMedia: BaseInputMedia{ 200 | Type: "video", 201 | Media: media, 202 | }, 203 | } 204 | } 205 | 206 | // NewInputMediaAnimation creates a new InputMediaAnimation. 207 | func NewInputMediaAnimation(media RequestFileData) InputMediaAnimation { 208 | return InputMediaAnimation{ 209 | BaseInputMedia: BaseInputMedia{ 210 | Type: "animation", 211 | Media: media, 212 | }, 213 | } 214 | } 215 | 216 | // NewInputMediaAudio creates a new InputMediaAudio. 217 | func NewInputMediaAudio(media RequestFileData) InputMediaAudio { 218 | return InputMediaAudio{ 219 | BaseInputMedia: BaseInputMedia{ 220 | Type: "audio", 221 | Media: media, 222 | }, 223 | } 224 | } 225 | 226 | // NewInputMediaDocument creates a new InputMediaDocument. 227 | func NewInputMediaDocument(media RequestFileData) InputMediaDocument { 228 | return InputMediaDocument{ 229 | BaseInputMedia: BaseInputMedia{ 230 | Type: "document", 231 | Media: media, 232 | }, 233 | } 234 | } 235 | 236 | // NewContact allows you to send a shared contact. 237 | func NewContact(chatID int64, phoneNumber, firstName string) ContactConfig { 238 | return ContactConfig{ 239 | BaseChat: BaseChat{ 240 | ChatID: chatID, 241 | }, 242 | PhoneNumber: phoneNumber, 243 | FirstName: firstName, 244 | } 245 | } 246 | 247 | // NewLocation shares your location. 248 | // 249 | // chatID is where to send it, latitude and longitude are coordinates. 250 | func NewLocation(chatID int64, latitude float64, longitude float64) LocationConfig { 251 | return LocationConfig{ 252 | BaseChat: BaseChat{ 253 | ChatID: chatID, 254 | }, 255 | Latitude: latitude, 256 | Longitude: longitude, 257 | } 258 | } 259 | 260 | // NewVenue allows you to send a venue and its location. 261 | func NewVenue(chatID int64, title, address string, latitude, longitude float64) VenueConfig { 262 | return VenueConfig{ 263 | BaseChat: BaseChat{ 264 | ChatID: chatID, 265 | }, 266 | Title: title, 267 | Address: address, 268 | Latitude: latitude, 269 | Longitude: longitude, 270 | } 271 | } 272 | 273 | // NewChatAction sets a chat action. 274 | // Actions last for 5 seconds, or until your next action. 275 | // 276 | // chatID is where to send it, action should be set via Chat constants. 277 | func NewChatAction(chatID int64, action string) ChatActionConfig { 278 | return ChatActionConfig{ 279 | BaseChat: BaseChat{ChatID: chatID}, 280 | Action: action, 281 | } 282 | } 283 | 284 | // NewUserProfilePhotos gets user profile photos. 285 | // 286 | // userID is the ID of the user you wish to get profile photos from. 287 | func NewUserProfilePhotos(userID int64) UserProfilePhotosConfig { 288 | return UserProfilePhotosConfig{ 289 | UserID: userID, 290 | Offset: 0, 291 | Limit: 0, 292 | } 293 | } 294 | 295 | // NewUpdate gets updates since the last Offset. 296 | // 297 | // offset is the last Update ID to include. 298 | // You likely want to set this to the last Update ID plus 1. 299 | func NewUpdate(offset int) UpdateConfig { 300 | return UpdateConfig{ 301 | Offset: offset, 302 | Limit: 0, 303 | Timeout: 0, 304 | } 305 | } 306 | 307 | // NewWebhook creates a new webhook. 308 | // 309 | // link is the url parsable link you wish to get the updates. 310 | func NewWebhook(link string) (WebhookConfig, error) { 311 | u, err := url.Parse(link) 312 | 313 | if err != nil { 314 | return WebhookConfig{}, err 315 | } 316 | 317 | return WebhookConfig{ 318 | URL: u, 319 | }, nil 320 | } 321 | 322 | // NewWebhookWithCert creates a new webhook with a certificate. 323 | // 324 | // link is the url you wish to get webhooks, 325 | // file contains a string to a file, FileReader, or FileBytes. 326 | func NewWebhookWithCert(link string, file RequestFileData) (WebhookConfig, error) { 327 | u, err := url.Parse(link) 328 | 329 | if err != nil { 330 | return WebhookConfig{}, err 331 | } 332 | 333 | return WebhookConfig{ 334 | URL: u, 335 | Certificate: file, 336 | }, nil 337 | } 338 | 339 | // NewInlineQueryResultArticle creates a new inline query article. 340 | func NewInlineQueryResultArticle(id, title, messageText string) InlineQueryResultArticle { 341 | return InlineQueryResultArticle{ 342 | Type: "article", 343 | ID: id, 344 | Title: title, 345 | InputMessageContent: InputTextMessageContent{ 346 | Text: messageText, 347 | }, 348 | } 349 | } 350 | 351 | // NewInlineQueryResultArticleMarkdown creates a new inline query article with Markdown parsing. 352 | func NewInlineQueryResultArticleMarkdown(id, title, messageText string) InlineQueryResultArticle { 353 | return InlineQueryResultArticle{ 354 | Type: "article", 355 | ID: id, 356 | Title: title, 357 | InputMessageContent: InputTextMessageContent{ 358 | Text: messageText, 359 | ParseMode: "Markdown", 360 | }, 361 | } 362 | } 363 | 364 | // NewInlineQueryResultArticleMarkdownV2 creates a new inline query article with MarkdownV2 parsing. 365 | func NewInlineQueryResultArticleMarkdownV2(id, title, messageText string) InlineQueryResultArticle { 366 | return InlineQueryResultArticle{ 367 | Type: "article", 368 | ID: id, 369 | Title: title, 370 | InputMessageContent: InputTextMessageContent{ 371 | Text: messageText, 372 | ParseMode: "MarkdownV2", 373 | }, 374 | } 375 | } 376 | 377 | // NewInlineQueryResultArticleHTML creates a new inline query article with HTML parsing. 378 | func NewInlineQueryResultArticleHTML(id, title, messageText string) InlineQueryResultArticle { 379 | return InlineQueryResultArticle{ 380 | Type: "article", 381 | ID: id, 382 | Title: title, 383 | InputMessageContent: InputTextMessageContent{ 384 | Text: messageText, 385 | ParseMode: "HTML", 386 | }, 387 | } 388 | } 389 | 390 | // NewInlineQueryResultGIF creates a new inline query GIF. 391 | func NewInlineQueryResultGIF(id, url string) InlineQueryResultGIF { 392 | return InlineQueryResultGIF{ 393 | Type: "gif", 394 | ID: id, 395 | URL: url, 396 | } 397 | } 398 | 399 | // NewInlineQueryResultCachedGIF create a new inline query with cached photo. 400 | func NewInlineQueryResultCachedGIF(id, gifID string) InlineQueryResultCachedGIF { 401 | return InlineQueryResultCachedGIF{ 402 | Type: "gif", 403 | ID: id, 404 | GIFID: gifID, 405 | } 406 | } 407 | 408 | // NewInlineQueryResultMPEG4GIF creates a new inline query MPEG4 GIF. 409 | func NewInlineQueryResultMPEG4GIF(id, url string) InlineQueryResultMPEG4GIF { 410 | return InlineQueryResultMPEG4GIF{ 411 | Type: "mpeg4_gif", 412 | ID: id, 413 | URL: url, 414 | } 415 | } 416 | 417 | // NewInlineQueryResultCachedMPEG4GIF create a new inline query with cached MPEG4 GIF. 418 | func NewInlineQueryResultCachedMPEG4GIF(id, MPEG4GIFID string) InlineQueryResultCachedMPEG4GIF { 419 | return InlineQueryResultCachedMPEG4GIF{ 420 | Type: "mpeg4_gif", 421 | ID: id, 422 | MPEG4FileID: MPEG4GIFID, 423 | } 424 | } 425 | 426 | // NewInlineQueryResultPhoto creates a new inline query photo. 427 | func NewInlineQueryResultPhoto(id, url string) InlineQueryResultPhoto { 428 | return InlineQueryResultPhoto{ 429 | Type: "photo", 430 | ID: id, 431 | URL: url, 432 | } 433 | } 434 | 435 | // NewInlineQueryResultPhotoWithThumb creates a new inline query photo. 436 | func NewInlineQueryResultPhotoWithThumb(id, url, thumb string) InlineQueryResultPhoto { 437 | return InlineQueryResultPhoto{ 438 | Type: "photo", 439 | ID: id, 440 | URL: url, 441 | ThumbURL: thumb, 442 | } 443 | } 444 | 445 | // NewInlineQueryResultCachedPhoto create a new inline query with cached photo. 446 | func NewInlineQueryResultCachedPhoto(id, photoID string) InlineQueryResultCachedPhoto { 447 | return InlineQueryResultCachedPhoto{ 448 | Type: "photo", 449 | ID: id, 450 | PhotoID: photoID, 451 | } 452 | } 453 | 454 | // NewInlineQueryResultVideo creates a new inline query video. 455 | func NewInlineQueryResultVideo(id, url string) InlineQueryResultVideo { 456 | return InlineQueryResultVideo{ 457 | Type: "video", 458 | ID: id, 459 | URL: url, 460 | } 461 | } 462 | 463 | // NewInlineQueryResultCachedVideo create a new inline query with cached video. 464 | func NewInlineQueryResultCachedVideo(id, videoID, title string) InlineQueryResultCachedVideo { 465 | return InlineQueryResultCachedVideo{ 466 | Type: "video", 467 | ID: id, 468 | VideoID: videoID, 469 | Title: title, 470 | } 471 | } 472 | 473 | // NewInlineQueryResultCachedSticker create a new inline query with cached sticker. 474 | func NewInlineQueryResultCachedSticker(id, stickerID, title string) InlineQueryResultCachedSticker { 475 | return InlineQueryResultCachedSticker{ 476 | Type: "sticker", 477 | ID: id, 478 | StickerID: stickerID, 479 | Title: title, 480 | } 481 | } 482 | 483 | // NewInlineQueryResultAudio creates a new inline query audio. 484 | func NewInlineQueryResultAudio(id, url, title string) InlineQueryResultAudio { 485 | return InlineQueryResultAudio{ 486 | Type: "audio", 487 | ID: id, 488 | URL: url, 489 | Title: title, 490 | } 491 | } 492 | 493 | // NewInlineQueryResultCachedAudio create a new inline query with cached photo. 494 | func NewInlineQueryResultCachedAudio(id, audioID string) InlineQueryResultCachedAudio { 495 | return InlineQueryResultCachedAudio{ 496 | Type: "audio", 497 | ID: id, 498 | AudioID: audioID, 499 | } 500 | } 501 | 502 | // NewInlineQueryResultVoice creates a new inline query voice. 503 | func NewInlineQueryResultVoice(id, url, title string) InlineQueryResultVoice { 504 | return InlineQueryResultVoice{ 505 | Type: "voice", 506 | ID: id, 507 | URL: url, 508 | Title: title, 509 | } 510 | } 511 | 512 | // NewInlineQueryResultCachedVoice create a new inline query with cached photo. 513 | func NewInlineQueryResultCachedVoice(id, voiceID, title string) InlineQueryResultCachedVoice { 514 | return InlineQueryResultCachedVoice{ 515 | Type: "voice", 516 | ID: id, 517 | VoiceID: voiceID, 518 | Title: title, 519 | } 520 | } 521 | 522 | // NewInlineQueryResultDocument creates a new inline query document. 523 | func NewInlineQueryResultDocument(id, url, title, mimeType string) InlineQueryResultDocument { 524 | return InlineQueryResultDocument{ 525 | Type: "document", 526 | ID: id, 527 | URL: url, 528 | Title: title, 529 | MimeType: mimeType, 530 | } 531 | } 532 | 533 | // NewInlineQueryResultCachedDocument create a new inline query with cached photo. 534 | func NewInlineQueryResultCachedDocument(id, documentID, title string) InlineQueryResultCachedDocument { 535 | return InlineQueryResultCachedDocument{ 536 | Type: "document", 537 | ID: id, 538 | DocumentID: documentID, 539 | Title: title, 540 | } 541 | } 542 | 543 | // NewInlineQueryResultLocation creates a new inline query location. 544 | func NewInlineQueryResultLocation(id, title string, latitude, longitude float64) InlineQueryResultLocation { 545 | return InlineQueryResultLocation{ 546 | Type: "location", 547 | ID: id, 548 | Title: title, 549 | Latitude: latitude, 550 | Longitude: longitude, 551 | } 552 | } 553 | 554 | // NewInlineQueryResultVenue creates a new inline query venue. 555 | func NewInlineQueryResultVenue(id, title, address string, latitude, longitude float64) InlineQueryResultVenue { 556 | return InlineQueryResultVenue{ 557 | Type: "venue", 558 | ID: id, 559 | Title: title, 560 | Address: address, 561 | Latitude: latitude, 562 | Longitude: longitude, 563 | } 564 | } 565 | 566 | // NewEditMessageText allows you to edit the text of a message. 567 | func NewEditMessageText(chatID int64, messageID int, text string) EditMessageTextConfig { 568 | return EditMessageTextConfig{ 569 | BaseEdit: BaseEdit{ 570 | ChatID: chatID, 571 | MessageID: messageID, 572 | }, 573 | Text: text, 574 | } 575 | } 576 | 577 | // NewEditMessageTextAndMarkup allows you to edit the text and reply markup of a message. 578 | func NewEditMessageTextAndMarkup(chatID int64, messageID int, text string, replyMarkup InlineKeyboardMarkup) EditMessageTextConfig { 579 | return EditMessageTextConfig{ 580 | BaseEdit: BaseEdit{ 581 | ChatID: chatID, 582 | MessageID: messageID, 583 | ReplyMarkup: &replyMarkup, 584 | }, 585 | Text: text, 586 | } 587 | } 588 | 589 | // NewEditMessageCaption allows you to edit the caption of a message. 590 | func NewEditMessageCaption(chatID int64, messageID int, caption string) EditMessageCaptionConfig { 591 | return EditMessageCaptionConfig{ 592 | BaseEdit: BaseEdit{ 593 | ChatID: chatID, 594 | MessageID: messageID, 595 | }, 596 | Caption: caption, 597 | } 598 | } 599 | 600 | // NewEditMessageReplyMarkup allows you to edit the inline 601 | // keyboard markup. 602 | func NewEditMessageReplyMarkup(chatID int64, messageID int, replyMarkup InlineKeyboardMarkup) EditMessageReplyMarkupConfig { 603 | return EditMessageReplyMarkupConfig{ 604 | BaseEdit: BaseEdit{ 605 | ChatID: chatID, 606 | MessageID: messageID, 607 | ReplyMarkup: &replyMarkup, 608 | }, 609 | } 610 | } 611 | 612 | // NewRemoveKeyboard hides the keyboard, with the option for being selective 613 | // or hiding for everyone. 614 | func NewRemoveKeyboard(selective bool) ReplyKeyboardRemove { 615 | return ReplyKeyboardRemove{ 616 | RemoveKeyboard: true, 617 | Selective: selective, 618 | } 619 | } 620 | 621 | // NewKeyboardButton creates a regular keyboard button. 622 | func NewKeyboardButton(text string) KeyboardButton { 623 | return KeyboardButton{ 624 | Text: text, 625 | } 626 | } 627 | 628 | // NewKeyboardButtonWebApp creates a keyboard button with text 629 | // which goes to a WebApp. 630 | func NewKeyboardButtonWebApp(text string, webapp WebAppInfo) KeyboardButton { 631 | return KeyboardButton{ 632 | Text: text, 633 | WebApp: &webapp, 634 | } 635 | } 636 | 637 | // NewKeyboardButtonContact creates a keyboard button that requests 638 | // user contact information upon click. 639 | func NewKeyboardButtonContact(text string) KeyboardButton { 640 | return KeyboardButton{ 641 | Text: text, 642 | RequestContact: true, 643 | } 644 | } 645 | 646 | // NewKeyboardButtonLocation creates a keyboard button that requests 647 | // user location information upon click. 648 | func NewKeyboardButtonLocation(text string) KeyboardButton { 649 | return KeyboardButton{ 650 | Text: text, 651 | RequestLocation: true, 652 | } 653 | } 654 | 655 | // NewKeyboardButtonRow creates a row of keyboard buttons. 656 | func NewKeyboardButtonRow(buttons ...KeyboardButton) []KeyboardButton { 657 | var row []KeyboardButton 658 | 659 | row = append(row, buttons...) 660 | 661 | return row 662 | } 663 | 664 | // NewReplyKeyboard creates a new regular keyboard with sane defaults. 665 | func NewReplyKeyboard(rows ...[]KeyboardButton) ReplyKeyboardMarkup { 666 | var keyboard [][]KeyboardButton 667 | 668 | keyboard = append(keyboard, rows...) 669 | 670 | return ReplyKeyboardMarkup{ 671 | ResizeKeyboard: true, 672 | Keyboard: keyboard, 673 | } 674 | } 675 | 676 | // NewOneTimeReplyKeyboard creates a new one time keyboard. 677 | func NewOneTimeReplyKeyboard(rows ...[]KeyboardButton) ReplyKeyboardMarkup { 678 | markup := NewReplyKeyboard(rows...) 679 | markup.OneTimeKeyboard = true 680 | return markup 681 | } 682 | 683 | // NewInlineKeyboardButtonData creates an inline keyboard button with text 684 | // and data for a callback. 685 | func NewInlineKeyboardButtonData(text, data string) InlineKeyboardButton { 686 | return InlineKeyboardButton{ 687 | Text: text, 688 | CallbackData: &data, 689 | } 690 | } 691 | 692 | // NewInlineKeyboardButtonWebApp creates an inline keyboard button with text 693 | // which goes to a WebApp. 694 | func NewInlineKeyboardButtonWebApp(text string, webapp WebAppInfo) InlineKeyboardButton { 695 | return InlineKeyboardButton{ 696 | Text: text, 697 | WebApp: &webapp, 698 | } 699 | } 700 | 701 | // NewInlineKeyboardButtonLoginURL creates an inline keyboard button with text 702 | // which goes to a LoginURL. 703 | func NewInlineKeyboardButtonLoginURL(text string, loginURL LoginURL) InlineKeyboardButton { 704 | return InlineKeyboardButton{ 705 | Text: text, 706 | LoginURL: &loginURL, 707 | } 708 | } 709 | 710 | // NewInlineKeyboardButtonURL creates an inline keyboard button with text 711 | // which goes to a URL. 712 | func NewInlineKeyboardButtonURL(text, url string) InlineKeyboardButton { 713 | return InlineKeyboardButton{ 714 | Text: text, 715 | URL: &url, 716 | } 717 | } 718 | 719 | // NewInlineKeyboardButtonSwitch creates an inline keyboard button with 720 | // text which allows the user to switch to a chat or return to a chat. 721 | func NewInlineKeyboardButtonSwitch(text, sw string) InlineKeyboardButton { 722 | return InlineKeyboardButton{ 723 | Text: text, 724 | SwitchInlineQuery: &sw, 725 | } 726 | } 727 | 728 | // NewInlineKeyboardRow creates an inline keyboard row with buttons. 729 | func NewInlineKeyboardRow(buttons ...InlineKeyboardButton) []InlineKeyboardButton { 730 | var row []InlineKeyboardButton 731 | 732 | row = append(row, buttons...) 733 | 734 | return row 735 | } 736 | 737 | // NewInlineKeyboardMarkup creates a new inline keyboard. 738 | func NewInlineKeyboardMarkup(rows ...[]InlineKeyboardButton) InlineKeyboardMarkup { 739 | var keyboard [][]InlineKeyboardButton 740 | 741 | keyboard = append(keyboard, rows...) 742 | 743 | return InlineKeyboardMarkup{ 744 | InlineKeyboard: keyboard, 745 | } 746 | } 747 | 748 | // NewCallback creates a new callback message. 749 | func NewCallback(id, text string) CallbackConfig { 750 | return CallbackConfig{ 751 | CallbackQueryID: id, 752 | Text: text, 753 | ShowAlert: false, 754 | } 755 | } 756 | 757 | // NewCallbackWithAlert creates a new callback message that alerts 758 | // the user. 759 | func NewCallbackWithAlert(id, text string) CallbackConfig { 760 | return CallbackConfig{ 761 | CallbackQueryID: id, 762 | Text: text, 763 | ShowAlert: true, 764 | } 765 | } 766 | 767 | // NewInvoice creates a new Invoice request to the user. 768 | func NewInvoice(chatID int64, title, description, payload, providerToken, startParameter, currency string, prices []LabeledPrice) InvoiceConfig { 769 | return InvoiceConfig{ 770 | BaseChat: BaseChat{ChatID: chatID}, 771 | Title: title, 772 | Description: description, 773 | Payload: payload, 774 | ProviderToken: providerToken, 775 | StartParameter: startParameter, 776 | Currency: currency, 777 | Prices: prices} 778 | } 779 | 780 | // NewChatTitle allows you to update the title of a chat. 781 | func NewChatTitle(chatID int64, title string) SetChatTitleConfig { 782 | return SetChatTitleConfig{ 783 | ChatID: chatID, 784 | Title: title, 785 | } 786 | } 787 | 788 | // NewChatDescription allows you to update the description of a chat. 789 | func NewChatDescription(chatID int64, description string) SetChatDescriptionConfig { 790 | return SetChatDescriptionConfig{ 791 | ChatID: chatID, 792 | Description: description, 793 | } 794 | } 795 | 796 | // NewChatPhoto allows you to update the photo for a chat. 797 | func NewChatPhoto(chatID int64, photo RequestFileData) SetChatPhotoConfig { 798 | return SetChatPhotoConfig{ 799 | BaseFile: BaseFile{ 800 | BaseChat: BaseChat{ 801 | ChatID: chatID, 802 | }, 803 | File: photo, 804 | }, 805 | } 806 | } 807 | 808 | // NewDeleteChatPhoto allows you to delete the photo for a chat. 809 | func NewDeleteChatPhoto(chatID int64) DeleteChatPhotoConfig { 810 | return DeleteChatPhotoConfig{ 811 | ChatID: chatID, 812 | } 813 | } 814 | 815 | // NewPoll allows you to create a new poll. 816 | func NewPoll(chatID int64, question string, options ...string) SendPollConfig { 817 | return SendPollConfig{ 818 | BaseChat: BaseChat{ 819 | ChatID: chatID, 820 | }, 821 | Question: question, 822 | Options: options, 823 | IsAnonymous: true, // This is Telegram's default. 824 | } 825 | } 826 | 827 | // NewStopPoll allows you to stop a poll. 828 | func NewStopPoll(chatID int64, messageID int) StopPollConfig { 829 | return StopPollConfig{ 830 | BaseEdit{ 831 | ChatID: chatID, 832 | MessageID: messageID, 833 | }, 834 | } 835 | } 836 | 837 | // NewDice allows you to send a random dice roll. 838 | func NewDice(chatID int64) DiceConfig { 839 | return DiceConfig{ 840 | BaseChat: BaseChat{ 841 | ChatID: chatID, 842 | }, 843 | } 844 | } 845 | 846 | // NewDiceWithEmoji allows you to send a random roll of one of many types. 847 | // 848 | // Emoji may be 🎲 (1-6), 🎯 (1-6), or 🏀 (1-5). 849 | func NewDiceWithEmoji(chatID int64, emoji string) DiceConfig { 850 | return DiceConfig{ 851 | BaseChat: BaseChat{ 852 | ChatID: chatID, 853 | }, 854 | Emoji: emoji, 855 | } 856 | } 857 | 858 | // NewBotCommandScopeDefault represents the default scope of bot commands. 859 | func NewBotCommandScopeDefault() BotCommandScope { 860 | return BotCommandScope{Type: "default"} 861 | } 862 | 863 | // NewBotCommandScopeAllPrivateChats represents the scope of bot commands, 864 | // covering all private chats. 865 | func NewBotCommandScopeAllPrivateChats() BotCommandScope { 866 | return BotCommandScope{Type: "all_private_chats"} 867 | } 868 | 869 | // NewBotCommandScopeAllGroupChats represents the scope of bot commands, 870 | // covering all group and supergroup chats. 871 | func NewBotCommandScopeAllGroupChats() BotCommandScope { 872 | return BotCommandScope{Type: "all_group_chats"} 873 | } 874 | 875 | // NewBotCommandScopeAllChatAdministrators represents the scope of bot commands, 876 | // covering all group and supergroup chat administrators. 877 | func NewBotCommandScopeAllChatAdministrators() BotCommandScope { 878 | return BotCommandScope{Type: "all_chat_administrators"} 879 | } 880 | 881 | // NewBotCommandScopeChat represents the scope of bot commands, covering a 882 | // specific chat. 883 | func NewBotCommandScopeChat(chatID int64) BotCommandScope { 884 | return BotCommandScope{ 885 | Type: "chat", 886 | ChatID: chatID, 887 | } 888 | } 889 | 890 | // NewBotCommandScopeChatAdministrators represents the scope of bot commands, 891 | // covering all administrators of a specific group or supergroup chat. 892 | func NewBotCommandScopeChatAdministrators(chatID int64) BotCommandScope { 893 | return BotCommandScope{ 894 | Type: "chat_administrators", 895 | ChatID: chatID, 896 | } 897 | } 898 | 899 | // NewBotCommandScopeChatMember represents the scope of bot commands, covering a 900 | // specific member of a group or supergroup chat. 901 | func NewBotCommandScopeChatMember(chatID, userID int64) BotCommandScope { 902 | return BotCommandScope{ 903 | Type: "chat_member", 904 | ChatID: chatID, 905 | UserID: userID, 906 | } 907 | } 908 | 909 | // NewGetMyCommandsWithScope allows you to set the registered commands for a 910 | // given scope. 911 | func NewGetMyCommandsWithScope(scope BotCommandScope) GetMyCommandsConfig { 912 | return GetMyCommandsConfig{Scope: &scope} 913 | } 914 | 915 | // NewGetMyCommandsWithScopeAndLanguage allows you to set the registered 916 | // commands for a given scope and language code. 917 | func NewGetMyCommandsWithScopeAndLanguage(scope BotCommandScope, languageCode string) GetMyCommandsConfig { 918 | return GetMyCommandsConfig{Scope: &scope, LanguageCode: languageCode} 919 | } 920 | 921 | // NewSetMyCommands allows you to set the registered commands. 922 | func NewSetMyCommands(commands ...BotCommand) SetMyCommandsConfig { 923 | return SetMyCommandsConfig{Commands: commands} 924 | } 925 | 926 | // NewSetMyCommandsWithScope allows you to set the registered commands for a given scope. 927 | func NewSetMyCommandsWithScope(scope BotCommandScope, commands ...BotCommand) SetMyCommandsConfig { 928 | return SetMyCommandsConfig{Commands: commands, Scope: &scope} 929 | } 930 | 931 | // NewSetMyCommandsWithScopeAndLanguage allows you to set the registered commands for a given scope 932 | // and language code. 933 | func NewSetMyCommandsWithScopeAndLanguage(scope BotCommandScope, languageCode string, commands ...BotCommand) SetMyCommandsConfig { 934 | return SetMyCommandsConfig{Commands: commands, Scope: &scope, LanguageCode: languageCode} 935 | } 936 | 937 | // NewDeleteMyCommands allows you to delete the registered commands. 938 | func NewDeleteMyCommands() DeleteMyCommandsConfig { 939 | return DeleteMyCommandsConfig{} 940 | } 941 | 942 | // NewDeleteMyCommandsWithScope allows you to delete the registered commands for a given 943 | // scope. 944 | func NewDeleteMyCommandsWithScope(scope BotCommandScope) DeleteMyCommandsConfig { 945 | return DeleteMyCommandsConfig{Scope: &scope} 946 | } 947 | 948 | // NewDeleteMyCommandsWithScopeAndLanguage allows you to delete the registered commands for a given 949 | // scope and language code. 950 | func NewDeleteMyCommandsWithScopeAndLanguage(scope BotCommandScope, languageCode string) DeleteMyCommandsConfig { 951 | return DeleteMyCommandsConfig{Scope: &scope, LanguageCode: languageCode} 952 | } 953 | 954 | // ValidateWebAppData validate data received via the Web App 955 | // https://core.telegram.org/bots/webapps#validating-data-received-via-the-web-app 956 | func ValidateWebAppData(token, telegramInitData string) (bool, error) { 957 | initData, err := url.ParseQuery(telegramInitData) 958 | if err != nil { 959 | return false, fmt.Errorf("error parsing data %w", err) 960 | } 961 | 962 | dataCheckString := make([]string, 0, len(initData)) 963 | for k, v := range initData { 964 | if k == "hash" { 965 | continue 966 | } 967 | if len(v) > 0 { 968 | dataCheckString = append(dataCheckString, fmt.Sprintf("%s=%s", k, v[0])) 969 | } 970 | } 971 | 972 | sort.Strings(dataCheckString) 973 | 974 | secret := hmac.New(sha256.New, []byte("WebAppData")) 975 | secret.Write([]byte(token)) 976 | 977 | hHash := hmac.New(sha256.New, secret.Sum(nil)) 978 | hHash.Write([]byte(strings.Join(dataCheckString, "\n"))) 979 | 980 | hash := hex.EncodeToString(hHash.Sum(nil)) 981 | 982 | if initData.Get("hash") != hash { 983 | return false, errors.New("hash not equal") 984 | } 985 | 986 | return true, nil 987 | } 988 | -------------------------------------------------------------------------------- /helpers_test.go: -------------------------------------------------------------------------------- 1 | package tgbotapi 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestNewWebhook(t *testing.T) { 8 | result, err := NewWebhook("https://example.com/token") 9 | 10 | if err != nil || 11 | result.URL.String() != "https://example.com/token" || 12 | result.Certificate != interface{}(nil) || 13 | result.MaxConnections != 0 || 14 | len(result.AllowedUpdates) != 0 { 15 | t.Fail() 16 | } 17 | } 18 | 19 | func TestNewWebhookWithCert(t *testing.T) { 20 | exampleFile := FileID("123") 21 | result, err := NewWebhookWithCert("https://example.com/token", exampleFile) 22 | 23 | if err != nil || 24 | result.URL.String() != "https://example.com/token" || 25 | result.Certificate != exampleFile || 26 | result.MaxConnections != 0 || 27 | len(result.AllowedUpdates) != 0 { 28 | t.Fail() 29 | } 30 | } 31 | 32 | func TestNewInlineQueryResultArticle(t *testing.T) { 33 | result := NewInlineQueryResultArticle("id", "title", "message") 34 | 35 | if result.Type != "article" || 36 | result.ID != "id" || 37 | result.Title != "title" || 38 | result.InputMessageContent.(InputTextMessageContent).Text != "message" { 39 | t.Fail() 40 | } 41 | } 42 | 43 | func TestNewInlineQueryResultArticleMarkdown(t *testing.T) { 44 | result := NewInlineQueryResultArticleMarkdown("id", "title", "*message*") 45 | 46 | if result.Type != "article" || 47 | result.ID != "id" || 48 | result.Title != "title" || 49 | result.InputMessageContent.(InputTextMessageContent).Text != "*message*" || 50 | result.InputMessageContent.(InputTextMessageContent).ParseMode != "Markdown" { 51 | t.Fail() 52 | } 53 | } 54 | 55 | func TestNewInlineQueryResultArticleHTML(t *testing.T) { 56 | result := NewInlineQueryResultArticleHTML("id", "title", "message") 57 | 58 | if result.Type != "article" || 59 | result.ID != "id" || 60 | result.Title != "title" || 61 | result.InputMessageContent.(InputTextMessageContent).Text != "message" || 62 | result.InputMessageContent.(InputTextMessageContent).ParseMode != "HTML" { 63 | t.Fail() 64 | } 65 | } 66 | 67 | func TestNewInlineQueryResultGIF(t *testing.T) { 68 | result := NewInlineQueryResultGIF("id", "google.com") 69 | 70 | if result.Type != "gif" || 71 | result.ID != "id" || 72 | result.URL != "google.com" { 73 | t.Fail() 74 | } 75 | } 76 | 77 | func TestNewInlineQueryResultMPEG4GIF(t *testing.T) { 78 | result := NewInlineQueryResultMPEG4GIF("id", "google.com") 79 | 80 | if result.Type != "mpeg4_gif" || 81 | result.ID != "id" || 82 | result.URL != "google.com" { 83 | t.Fail() 84 | } 85 | } 86 | 87 | func TestNewInlineQueryResultPhoto(t *testing.T) { 88 | result := NewInlineQueryResultPhoto("id", "google.com") 89 | 90 | if result.Type != "photo" || 91 | result.ID != "id" || 92 | result.URL != "google.com" { 93 | t.Fail() 94 | } 95 | } 96 | 97 | func TestNewInlineQueryResultPhotoWithThumb(t *testing.T) { 98 | result := NewInlineQueryResultPhotoWithThumb("id", "google.com", "thumb.com") 99 | 100 | if result.Type != "photo" || 101 | result.ID != "id" || 102 | result.URL != "google.com" || 103 | result.ThumbURL != "thumb.com" { 104 | t.Fail() 105 | } 106 | } 107 | 108 | func TestNewInlineQueryResultVideo(t *testing.T) { 109 | result := NewInlineQueryResultVideo("id", "google.com") 110 | 111 | if result.Type != "video" || 112 | result.ID != "id" || 113 | result.URL != "google.com" { 114 | t.Fail() 115 | } 116 | } 117 | 118 | func TestNewInlineQueryResultAudio(t *testing.T) { 119 | result := NewInlineQueryResultAudio("id", "google.com", "title") 120 | 121 | if result.Type != "audio" || 122 | result.ID != "id" || 123 | result.URL != "google.com" || 124 | result.Title != "title" { 125 | t.Fail() 126 | } 127 | } 128 | 129 | func TestNewInlineQueryResultVoice(t *testing.T) { 130 | result := NewInlineQueryResultVoice("id", "google.com", "title") 131 | 132 | if result.Type != "voice" || 133 | result.ID != "id" || 134 | result.URL != "google.com" || 135 | result.Title != "title" { 136 | t.Fail() 137 | } 138 | } 139 | 140 | func TestNewInlineQueryResultDocument(t *testing.T) { 141 | result := NewInlineQueryResultDocument("id", "google.com", "title", "mime/type") 142 | 143 | if result.Type != "document" || 144 | result.ID != "id" || 145 | result.URL != "google.com" || 146 | result.Title != "title" || 147 | result.MimeType != "mime/type" { 148 | t.Fail() 149 | } 150 | } 151 | 152 | func TestNewInlineQueryResultLocation(t *testing.T) { 153 | result := NewInlineQueryResultLocation("id", "name", 40, 50) 154 | 155 | if result.Type != "location" || 156 | result.ID != "id" || 157 | result.Title != "name" || 158 | result.Latitude != 40 || 159 | result.Longitude != 50 { 160 | t.Fail() 161 | } 162 | } 163 | 164 | func TestNewInlineKeyboardButtonLoginURL(t *testing.T) { 165 | result := NewInlineKeyboardButtonLoginURL("text", LoginURL{ 166 | URL: "url", 167 | ForwardText: "ForwardText", 168 | BotUsername: "username", 169 | RequestWriteAccess: false, 170 | }) 171 | 172 | if result.Text != "text" || 173 | result.LoginURL.URL != "url" || 174 | result.LoginURL.ForwardText != "ForwardText" || 175 | result.LoginURL.BotUsername != "username" || 176 | result.LoginURL.RequestWriteAccess != false { 177 | t.Fail() 178 | } 179 | } 180 | 181 | func TestNewEditMessageText(t *testing.T) { 182 | edit := NewEditMessageText(ChatID, ReplyToMessageID, "new text") 183 | 184 | if edit.Text != "new text" || 185 | edit.BaseEdit.ChatID != ChatID || 186 | edit.BaseEdit.MessageID != ReplyToMessageID { 187 | t.Fail() 188 | } 189 | } 190 | 191 | func TestNewEditMessageCaption(t *testing.T) { 192 | edit := NewEditMessageCaption(ChatID, ReplyToMessageID, "new caption") 193 | 194 | if edit.Caption != "new caption" || 195 | edit.BaseEdit.ChatID != ChatID || 196 | edit.BaseEdit.MessageID != ReplyToMessageID { 197 | t.Fail() 198 | } 199 | } 200 | 201 | func TestNewEditMessageReplyMarkup(t *testing.T) { 202 | markup := InlineKeyboardMarkup{ 203 | InlineKeyboard: [][]InlineKeyboardButton{ 204 | { 205 | {Text: "test"}, 206 | }, 207 | }, 208 | } 209 | 210 | edit := NewEditMessageReplyMarkup(ChatID, ReplyToMessageID, markup) 211 | 212 | if edit.ReplyMarkup.InlineKeyboard[0][0].Text != "test" || 213 | edit.BaseEdit.ChatID != ChatID || 214 | edit.BaseEdit.MessageID != ReplyToMessageID { 215 | t.Fail() 216 | } 217 | 218 | } 219 | 220 | func TestNewDice(t *testing.T) { 221 | dice := NewDice(42) 222 | 223 | if dice.ChatID != 42 || 224 | dice.Emoji != "" { 225 | t.Fail() 226 | } 227 | } 228 | 229 | func TestNewDiceWithEmoji(t *testing.T) { 230 | dice := NewDiceWithEmoji(42, "🏀") 231 | 232 | if dice.ChatID != 42 || 233 | dice.Emoji != "🏀" { 234 | t.Fail() 235 | } 236 | } 237 | 238 | func TestValidateWebAppData(t *testing.T) { 239 | t.Run("success", func(t *testing.T) { 240 | token := "5473903189:AAFnHnISQMP5UQQ5MEaoEWvxeiwNgz2CN2U" 241 | initData := "query_id=AAG1bpMJAAAAALVukwmZ_H2t&user=%7B%22id%22%3A160657077%2C%22first_name%22%3A%22Yury%20R%22%2C%22last_name%22%3A%22%22%2C%22username%22%3A%22crashiura%22%2C%22language_code%22%3A%22en%22%7D&auth_date=1656804462&hash=8d6960760a573d3212deb05e20d1a34959c83d24c1bc44bb26dde49a42aa9b34" 242 | result, err := ValidateWebAppData(token, initData) 243 | if err != nil { 244 | t.Fail() 245 | } 246 | if !result { 247 | t.Fail() 248 | } 249 | }) 250 | 251 | t.Run("error", func(t *testing.T) { 252 | token := "5473903189:AAFnHnISQMP5UQQ5MEaoEWvxeiwNgz2CN2U" 253 | initData := "asdfasdfasdfasdfasdf" 254 | result, err := ValidateWebAppData(token, initData) 255 | if err == nil { 256 | t.Fail() 257 | } 258 | if result { 259 | t.Fail() 260 | } 261 | }) 262 | } 263 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package tgbotapi 2 | 3 | import ( 4 | "errors" 5 | stdlog "log" 6 | "os" 7 | ) 8 | 9 | // BotLogger is an interface that represents the required methods to log data. 10 | // 11 | // Instead of requiring the standard logger, we can just specify the methods we 12 | // use and allow users to pass anything that implements these. 13 | type BotLogger interface { 14 | Println(v ...interface{}) 15 | Printf(format string, v ...interface{}) 16 | } 17 | 18 | var log BotLogger = stdlog.New(os.Stderr, "", stdlog.LstdFlags) 19 | 20 | // SetLogger specifies the logger that the package should use. 21 | func SetLogger(logger BotLogger) error { 22 | if logger == nil { 23 | return errors.New("logger is nil") 24 | } 25 | log = logger 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /params.go: -------------------------------------------------------------------------------- 1 | package tgbotapi 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "strconv" 7 | ) 8 | 9 | // Params represents a set of parameters that gets passed to a request. 10 | type Params map[string]string 11 | 12 | // AddNonEmpty adds a value if it not an empty string. 13 | func (p Params) AddNonEmpty(key, value string) { 14 | if value != "" { 15 | p[key] = value 16 | } 17 | } 18 | 19 | // AddNonZero adds a value if it is not zero. 20 | func (p Params) AddNonZero(key string, value int) { 21 | if value != 0 { 22 | p[key] = strconv.Itoa(value) 23 | } 24 | } 25 | 26 | // AddNonZero64 is the same as AddNonZero except uses an int64. 27 | func (p Params) AddNonZero64(key string, value int64) { 28 | if value != 0 { 29 | p[key] = strconv.FormatInt(value, 10) 30 | } 31 | } 32 | 33 | // AddBool adds a value of a bool if it is true. 34 | func (p Params) AddBool(key string, value bool) { 35 | if value { 36 | p[key] = strconv.FormatBool(value) 37 | } 38 | } 39 | 40 | // AddNonZeroFloat adds a floating point value that is not zero. 41 | func (p Params) AddNonZeroFloat(key string, value float64) { 42 | if value != 0 { 43 | p[key] = strconv.FormatFloat(value, 'f', 6, 64) 44 | } 45 | } 46 | 47 | // AddInterface adds an interface if it is not nil and can be JSON marshalled. 48 | func (p Params) AddInterface(key string, value interface{}) error { 49 | if value == nil || (reflect.ValueOf(value).Kind() == reflect.Ptr && reflect.ValueOf(value).IsNil()) { 50 | return nil 51 | } 52 | 53 | b, err := json.Marshal(value) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | p[key] = string(b) 59 | 60 | return nil 61 | } 62 | 63 | // AddFirstValid attempts to add the first item that is not a default value. 64 | // 65 | // For example, AddFirstValid(0, "", "test") would add "test". 66 | func (p Params) AddFirstValid(key string, args ...interface{}) error { 67 | for _, arg := range args { 68 | switch v := arg.(type) { 69 | case int: 70 | if v != 0 { 71 | p[key] = strconv.Itoa(v) 72 | return nil 73 | } 74 | case int64: 75 | if v != 0 { 76 | p[key] = strconv.FormatInt(v, 10) 77 | return nil 78 | } 79 | case string: 80 | if v != "" { 81 | p[key] = v 82 | return nil 83 | } 84 | case nil: 85 | default: 86 | b, err := json.Marshal(arg) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | p[key] = string(b) 92 | return nil 93 | } 94 | } 95 | 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /params_test.go: -------------------------------------------------------------------------------- 1 | package tgbotapi 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func assertLen(t *testing.T, params Params, l int) { 8 | actual := len(params) 9 | if actual != l { 10 | t.Fatalf("Incorrect number of params, expected %d but found %d\n", l, actual) 11 | } 12 | } 13 | 14 | func assertEq(t *testing.T, a interface{}, b interface{}) { 15 | if a != b { 16 | t.Fatalf("Values did not match, a: %v, b: %v\n", a, b) 17 | } 18 | } 19 | 20 | func TestAddNonEmpty(t *testing.T) { 21 | params := make(Params) 22 | params.AddNonEmpty("value", "value") 23 | assertLen(t, params, 1) 24 | assertEq(t, params["value"], "value") 25 | params.AddNonEmpty("test", "") 26 | assertLen(t, params, 1) 27 | assertEq(t, params["test"], "") 28 | } 29 | 30 | func TestAddNonZero(t *testing.T) { 31 | params := make(Params) 32 | params.AddNonZero("value", 1) 33 | assertLen(t, params, 1) 34 | assertEq(t, params["value"], "1") 35 | params.AddNonZero("test", 0) 36 | assertLen(t, params, 1) 37 | assertEq(t, params["test"], "") 38 | } 39 | 40 | func TestAddNonZero64(t *testing.T) { 41 | params := make(Params) 42 | params.AddNonZero64("value", 1) 43 | assertLen(t, params, 1) 44 | assertEq(t, params["value"], "1") 45 | params.AddNonZero64("test", 0) 46 | assertLen(t, params, 1) 47 | assertEq(t, params["test"], "") 48 | } 49 | 50 | func TestAddBool(t *testing.T) { 51 | params := make(Params) 52 | params.AddBool("value", true) 53 | assertLen(t, params, 1) 54 | assertEq(t, params["value"], "true") 55 | params.AddBool("test", false) 56 | assertLen(t, params, 1) 57 | assertEq(t, params["test"], "") 58 | } 59 | 60 | func TestAddNonZeroFloat(t *testing.T) { 61 | params := make(Params) 62 | params.AddNonZeroFloat("value", 1) 63 | assertLen(t, params, 1) 64 | assertEq(t, params["value"], "1.000000") 65 | params.AddNonZeroFloat("test", 0) 66 | assertLen(t, params, 1) 67 | assertEq(t, params["test"], "") 68 | } 69 | 70 | func TestAddInterface(t *testing.T) { 71 | params := make(Params) 72 | data := struct { 73 | Name string `json:"name"` 74 | }{ 75 | Name: "test", 76 | } 77 | params.AddInterface("value", data) 78 | assertLen(t, params, 1) 79 | assertEq(t, params["value"], `{"name":"test"}`) 80 | params.AddInterface("test", nil) 81 | assertLen(t, params, 1) 82 | assertEq(t, params["test"], "") 83 | } 84 | 85 | func TestAddFirstValid(t *testing.T) { 86 | params := make(Params) 87 | params.AddFirstValid("value", 0, "", "test") 88 | assertLen(t, params, 1) 89 | assertEq(t, params["value"], "test") 90 | params.AddFirstValid("value2", 3, "test") 91 | assertLen(t, params, 2) 92 | assertEq(t, params["value2"], "3") 93 | } 94 | -------------------------------------------------------------------------------- /passport.go: -------------------------------------------------------------------------------- 1 | package tgbotapi 2 | 3 | // PassportRequestInfoConfig allows you to request passport info 4 | type PassportRequestInfoConfig struct { 5 | BotID int `json:"bot_id"` 6 | Scope *PassportScope `json:"scope"` 7 | Nonce string `json:"nonce"` 8 | PublicKey string `json:"public_key"` 9 | } 10 | 11 | // PassportScopeElement supports using one or one of several elements. 12 | type PassportScopeElement interface { 13 | ScopeType() string 14 | } 15 | 16 | // PassportScope is the requested scopes of data. 17 | type PassportScope struct { 18 | V int `json:"v"` 19 | Data []PassportScopeElement `json:"data"` 20 | } 21 | 22 | // PassportScopeElementOneOfSeveral allows you to request any one of the 23 | // requested documents. 24 | type PassportScopeElementOneOfSeveral struct { 25 | } 26 | 27 | // ScopeType is the scope type. 28 | func (eo *PassportScopeElementOneOfSeveral) ScopeType() string { 29 | return "one_of" 30 | } 31 | 32 | // PassportScopeElementOne requires the specified element be provided. 33 | type PassportScopeElementOne struct { 34 | Type string `json:"type"` // One of “personal_details”, “passport”, “driver_license”, “identity_card”, “internal_passport”, “address”, “utility_bill”, “bank_statement”, “rental_agreement”, “passport_registration”, “temporary_registration”, “phone_number”, “email” 35 | Selfie bool `json:"selfie"` 36 | Translation bool `json:"translation"` 37 | NativeNames bool `json:"native_name"` 38 | } 39 | 40 | // ScopeType is the scope type. 41 | func (eo *PassportScopeElementOne) ScopeType() string { 42 | return "one" 43 | } 44 | 45 | type ( 46 | // PassportData contains information about Telegram Passport data shared with 47 | // the bot by the user. 48 | PassportData struct { 49 | // Array with information about documents and other Telegram Passport 50 | // elements that was shared with the bot 51 | Data []EncryptedPassportElement `json:"data"` 52 | 53 | // Encrypted credentials required to decrypt the data 54 | Credentials *EncryptedCredentials `json:"credentials"` 55 | } 56 | 57 | // PassportFile represents a file uploaded to Telegram Passport. Currently, all 58 | // Telegram Passport files are in JPEG format when decrypted and don't exceed 59 | // 10MB. 60 | PassportFile struct { 61 | // Unique identifier for this file 62 | FileID string `json:"file_id"` 63 | 64 | FileUniqueID string `json:"file_unique_id"` 65 | 66 | // File size 67 | FileSize int `json:"file_size"` 68 | 69 | // Unix time when the file was uploaded 70 | FileDate int64 `json:"file_date"` 71 | } 72 | 73 | // EncryptedPassportElement contains information about documents or other 74 | // Telegram Passport elements shared with the bot by the user. 75 | EncryptedPassportElement struct { 76 | // Element type. 77 | Type string `json:"type"` 78 | 79 | // Base64-encoded encrypted Telegram Passport element data provided by 80 | // the user, available for "personal_details", "passport", 81 | // "driver_license", "identity_card", "identity_passport" and "address" 82 | // types. Can be decrypted and verified using the accompanying 83 | // EncryptedCredentials. 84 | Data string `json:"data,omitempty"` 85 | 86 | // User's verified phone number, available only for "phone_number" type 87 | PhoneNumber string `json:"phone_number,omitempty"` 88 | 89 | // User's verified email address, available only for "email" type 90 | Email string `json:"email,omitempty"` 91 | 92 | // Array of encrypted files with documents provided by the user, 93 | // available for "utility_bill", "bank_statement", "rental_agreement", 94 | // "passport_registration" and "temporary_registration" types. Files can 95 | // be decrypted and verified using the accompanying EncryptedCredentials. 96 | Files []PassportFile `json:"files,omitempty"` 97 | 98 | // Encrypted file with the front side of the document, provided by the 99 | // user. Available for "passport", "driver_license", "identity_card" and 100 | // "internal_passport". The file can be decrypted and verified using the 101 | // accompanying EncryptedCredentials. 102 | FrontSide *PassportFile `json:"front_side,omitempty"` 103 | 104 | // Encrypted file with the reverse side of the document, provided by the 105 | // user. Available for "driver_license" and "identity_card". The file can 106 | // be decrypted and verified using the accompanying EncryptedCredentials. 107 | ReverseSide *PassportFile `json:"reverse_side,omitempty"` 108 | 109 | // Encrypted file with the selfie of the user holding a document, 110 | // provided by the user; available for "passport", "driver_license", 111 | // "identity_card" and "internal_passport". The file can be decrypted 112 | // and verified using the accompanying EncryptedCredentials. 113 | Selfie *PassportFile `json:"selfie,omitempty"` 114 | } 115 | 116 | // EncryptedCredentials contains data required for decrypting and 117 | // authenticating EncryptedPassportElement. See the Telegram Passport 118 | // Documentation for a complete description of the data decryption and 119 | // authentication processes. 120 | EncryptedCredentials struct { 121 | // Base64-encoded encrypted JSON-serialized data with unique user's 122 | // payload, data hashes and secrets required for EncryptedPassportElement 123 | // decryption and authentication 124 | Data string `json:"data"` 125 | 126 | // Base64-encoded data hash for data authentication 127 | Hash string `json:"hash"` 128 | 129 | // Base64-encoded secret, encrypted with the bot's public RSA key, 130 | // required for data decryption 131 | Secret string `json:"secret"` 132 | } 133 | 134 | // PassportElementError represents an error in the Telegram Passport element 135 | // which was submitted that should be resolved by the user. 136 | PassportElementError interface{} 137 | 138 | // PassportElementErrorDataField represents an issue in one of the data 139 | // fields that was provided by the user. The error is considered resolved 140 | // when the field's value changes. 141 | PassportElementErrorDataField struct { 142 | // Error source, must be data 143 | Source string `json:"source"` 144 | 145 | // The section of the user's Telegram Passport which has the error, one 146 | // of "personal_details", "passport", "driver_license", "identity_card", 147 | // "internal_passport", "address" 148 | Type string `json:"type"` 149 | 150 | // Name of the data field which has the error 151 | FieldName string `json:"field_name"` 152 | 153 | // Base64-encoded data hash 154 | DataHash string `json:"data_hash"` 155 | 156 | // Error message 157 | Message string `json:"message"` 158 | } 159 | 160 | // PassportElementErrorFrontSide represents an issue with the front side of 161 | // a document. The error is considered resolved when the file with the front 162 | // side of the document changes. 163 | PassportElementErrorFrontSide struct { 164 | // Error source, must be front_side 165 | Source string `json:"source"` 166 | 167 | // The section of the user's Telegram Passport which has the issue, one 168 | // of "passport", "driver_license", "identity_card", "internal_passport" 169 | Type string `json:"type"` 170 | 171 | // Base64-encoded hash of the file with the front side of the document 172 | FileHash string `json:"file_hash"` 173 | 174 | // Error message 175 | Message string `json:"message"` 176 | } 177 | 178 | // PassportElementErrorReverseSide represents an issue with the reverse side 179 | // of a document. The error is considered resolved when the file with reverse 180 | // side of the document changes. 181 | PassportElementErrorReverseSide struct { 182 | // Error source, must be reverse_side 183 | Source string `json:"source"` 184 | 185 | // The section of the user's Telegram Passport which has the issue, one 186 | // of "driver_license", "identity_card" 187 | Type string `json:"type"` 188 | 189 | // Base64-encoded hash of the file with the reverse side of the document 190 | FileHash string `json:"file_hash"` 191 | 192 | // Error message 193 | Message string `json:"message"` 194 | } 195 | 196 | // PassportElementErrorSelfie represents an issue with the selfie with a 197 | // document. The error is considered resolved when the file with the selfie 198 | // changes. 199 | PassportElementErrorSelfie struct { 200 | // Error source, must be selfie 201 | Source string `json:"source"` 202 | 203 | // The section of the user's Telegram Passport which has the issue, one 204 | // of "passport", "driver_license", "identity_card", "internal_passport" 205 | Type string `json:"type"` 206 | 207 | // Base64-encoded hash of the file with the selfie 208 | FileHash string `json:"file_hash"` 209 | 210 | // Error message 211 | Message string `json:"message"` 212 | } 213 | 214 | // PassportElementErrorFile represents an issue with a document scan. The 215 | // error is considered resolved when the file with the document scan changes. 216 | PassportElementErrorFile struct { 217 | // Error source, must be a file 218 | Source string `json:"source"` 219 | 220 | // The section of the user's Telegram Passport which has the issue, one 221 | // of "utility_bill", "bank_statement", "rental_agreement", 222 | // "passport_registration", "temporary_registration" 223 | Type string `json:"type"` 224 | 225 | // Base64-encoded file hash 226 | FileHash string `json:"file_hash"` 227 | 228 | // Error message 229 | Message string `json:"message"` 230 | } 231 | 232 | // PassportElementErrorFiles represents an issue with a list of scans. The 233 | // error is considered resolved when the list of files containing the scans 234 | // changes. 235 | PassportElementErrorFiles struct { 236 | // Error source, must be files 237 | Source string `json:"source"` 238 | 239 | // The section of the user's Telegram Passport which has the issue, one 240 | // of "utility_bill", "bank_statement", "rental_agreement", 241 | // "passport_registration", "temporary_registration" 242 | Type string `json:"type"` 243 | 244 | // List of base64-encoded file hashes 245 | FileHashes []string `json:"file_hashes"` 246 | 247 | // Error message 248 | Message string `json:"message"` 249 | } 250 | 251 | // Credentials contains encrypted data. 252 | Credentials struct { 253 | Data SecureData `json:"secure_data"` 254 | // Nonce the same nonce given in the request 255 | Nonce string `json:"nonce"` 256 | } 257 | 258 | // SecureData is a map of the fields and their encrypted values. 259 | SecureData map[string]*SecureValue 260 | // PersonalDetails *SecureValue `json:"personal_details"` 261 | // Passport *SecureValue `json:"passport"` 262 | // InternalPassport *SecureValue `json:"internal_passport"` 263 | // DriverLicense *SecureValue `json:"driver_license"` 264 | // IdentityCard *SecureValue `json:"identity_card"` 265 | // Address *SecureValue `json:"address"` 266 | // UtilityBill *SecureValue `json:"utility_bill"` 267 | // BankStatement *SecureValue `json:"bank_statement"` 268 | // RentalAgreement *SecureValue `json:"rental_agreement"` 269 | // PassportRegistration *SecureValue `json:"passport_registration"` 270 | // TemporaryRegistration *SecureValue `json:"temporary_registration"` 271 | 272 | // SecureValue contains encrypted values for a SecureData item. 273 | SecureValue struct { 274 | Data *DataCredentials `json:"data"` 275 | FrontSide *FileCredentials `json:"front_side"` 276 | ReverseSide *FileCredentials `json:"reverse_side"` 277 | Selfie *FileCredentials `json:"selfie"` 278 | Translation []*FileCredentials `json:"translation"` 279 | Files []*FileCredentials `json:"files"` 280 | } 281 | 282 | // DataCredentials contains information required to decrypt data. 283 | DataCredentials struct { 284 | // DataHash checksum of encrypted data 285 | DataHash string `json:"data_hash"` 286 | // Secret of encrypted data 287 | Secret string `json:"secret"` 288 | } 289 | 290 | // FileCredentials contains information required to decrypt files. 291 | FileCredentials struct { 292 | // FileHash checksum of encrypted data 293 | FileHash string `json:"file_hash"` 294 | // Secret of encrypted data 295 | Secret string `json:"secret"` 296 | } 297 | 298 | // PersonalDetails https://core.telegram.org/passport#personaldetails 299 | PersonalDetails struct { 300 | FirstName string `json:"first_name"` 301 | LastName string `json:"last_name"` 302 | MiddleName string `json:"middle_name"` 303 | BirthDate string `json:"birth_date"` 304 | Gender string `json:"gender"` 305 | CountryCode string `json:"country_code"` 306 | ResidenceCountryCode string `json:"residence_country_code"` 307 | FirstNameNative string `json:"first_name_native"` 308 | LastNameNative string `json:"last_name_native"` 309 | MiddleNameNative string `json:"middle_name_native"` 310 | } 311 | 312 | // IDDocumentData https://core.telegram.org/passport#iddocumentdata 313 | IDDocumentData struct { 314 | DocumentNumber string `json:"document_no"` 315 | ExpiryDate string `json:"expiry_date"` 316 | } 317 | ) 318 | -------------------------------------------------------------------------------- /tests/audio.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-telegram-bot-api/telegram-bot-api/4126fa611266940425a9dfd37e0c92ba47881718/tests/audio.mp3 -------------------------------------------------------------------------------- /tests/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIC0zCCAbugAwIBAgIJAPYfllX657axMA0GCSqGSIb3DQEBCwUAMAAwHhcNMTUx 3 | MTIxMTExMDQxWhcNMjUwODIwMTExMDQxWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOC 4 | AQ8AMIIBCgKCAQEAoMMSIIgYx8pT8Kz1O8Ukd/JVyqBQYRSo0enqEzo7295VROXq 5 | TUthbEbdi0OczUfl4IsAWppOSRrDwEguJZ0cJ/r6IxGsbrCdQr2MjgiomYtAXKKQ 6 | GAGL5Wls+AzcRNV0OszVJzkDNFYZzgNejyitGJSNEQMyU8r2gyPyIWP9MQKQst8y 7 | Mg91R/7l9jwf6AWwNxykZlYZurtsQ6XsBPZpF9YOFL7vZYPhKUFiNEm+74RpojC7 8 | Gt6nztYAUI2V/F+1uoXAr8nLpbj9SD0VSwyZLRG1uIVLBzhb0lfOIzAvJ45EKki9 9 | nejyoXfH1U5+iMzdSAdcy3MCBhpEZwJPqhDqeQIDAQABo1AwTjAdBgNVHQ4EFgQU 10 | JE0RLM+ohLnlDz0Qk0McCxtDK2MwHwYDVR0jBBgwFoAUJE0RLM+ohLnlDz0Qk0Mc 11 | CxtDK2MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAEmgME00JYuYZ 12 | 4wNaGrJskZ05ZnP+TXJusmBui9ToQ4UoykuyY5rsdGQ3SdzXPLdmd2nfMsw63iK2 13 | D7rjcH/rmn6fRccZqN0o0SXd/EuHeIoeW1Xnnivbt71b6mcOAeNg1UsMYxnMAVl0 14 | ywdkta8gURltagSfXoUbqlnSxn/zCwqaxxcQXA/CnunvRsFtQrwWjDBPg/BPULHX 15 | DEh2AactGtnGqEZ5iap/VCOVnmL6iPdJ1x5UIF/gS6U96wL+GHfcs1jCvPg+GEwR 16 | 3inh9oTXG9L21ge4lbGiPUIMBjtVcB3bXuQbOfec9Cr3ZhcQeZj680BIRxD/pNpA 17 | O/XeCfjfkw== 18 | -----END CERTIFICATE----- 19 | -------------------------------------------------------------------------------- /tests/image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-telegram-bot-api/telegram-bot-api/4126fa611266940425a9dfd37e0c92ba47881718/tests/image.jpg -------------------------------------------------------------------------------- /tests/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQCgwxIgiBjHylPw 3 | rPU7xSR38lXKoFBhFKjR6eoTOjvb3lVE5epNS2FsRt2LQ5zNR+XgiwBamk5JGsPA 4 | SC4lnRwn+vojEaxusJ1CvYyOCKiZi0BcopAYAYvlaWz4DNxE1XQ6zNUnOQM0VhnO 5 | A16PKK0YlI0RAzJTyvaDI/IhY/0xApCy3zIyD3VH/uX2PB/oBbA3HKRmVhm6u2xD 6 | pewE9mkX1g4Uvu9lg+EpQWI0Sb7vhGmiMLsa3qfO1gBQjZX8X7W6hcCvyculuP1I 7 | PRVLDJktEbW4hUsHOFvSV84jMC8njkQqSL2d6PKhd8fVTn6IzN1IB1zLcwIGGkRn 8 | Ak+qEOp5AgMBAAECggEBAJ/dPCJzlEjhL5XPONLmGXzZ1Gx5/VR86eBMv0O9jhb3 9 | wk2QYO3aPxggZGD/rGcKz1L6hzCR77WM0wpb/N/Um1I6pxHGmnU8VjYvLh10CM0f 10 | h7JWyfnFV+ubagxFJamhpkJuvKyTaldaI7EU8qxj47Xky18Wka53z6nbTgXcW8Sm 11 | V4CJy9OHNgKJQnylX6zOAaxVngSGde3xLslLjsYK4w9b2+OkCSUST2XXdo+ZLXxl 12 | cs0lEPFRM1Xh9/E6UrDrJMHHzio53L/W/+a8sIar1upgSY52pyD/tA7VSrAJ9nYC 13 | RezOU81VTLfMO+TYmgZzSUQJYh0cR4yqJe+wgl4U550CgYEA1EcS6Z+PO5Pr3u2+ 14 | XevawSAal6y9ONkkdOoASC977W37nn0E1wlQo41dR6DESCJfiSMeN0KbmXj5Wnc/ 15 | ADu+73iGwC90G9Qs9sjD7KAFBJvuj0V8hxvpWRdIBBbf7rlOj3CV0iXRYjkJbyJa 16 | cxuR0kiv4gTWmm5Cq+5ir8t1Oc8CgYEAwd+xOaDerNR481R+QmENFw+oR2EVMq3Q 17 | B/vinLK0PemQWrh32iBcd+vhSilOSQtUm1nko1jLK8C4s8X2vZYua4m5tcK9VqCt 18 | maCCq/ffxzsoW/GN8japnduz+qA+hKWJzW/aYR8tsOeqzjVqj4iIqPI4HuokrDi/ 19 | UD/QLgq5UTcCgYEAk2ZC0Kx15dXB7AtDq63xOTcUoAtXXRkSgohV58npEKXVGWkQ 20 | Kk0SjG7Fvc35XWlY0z3qZk6/AuOIqfOxcHUMEPatAtgwlH5RNo+T1EQNF/U6wotq 21 | e9q6vp026XgEyJwt29Y+giy2ZrDaRywgiFs1d0H3t0bKyXMUopQmPJFXdesCgYEA 22 | psCxXcDpZjxGX/zPsGZrbOdxtRtisTlg0k0rp93pO8tV90HtDHeDMT54g2ItzJPr 23 | TMev6XOpJNPZyf6+8GhpOuO2EQkT85u2VYoCeslz95gBabvFfIzZrUZYcnw76bm8 24 | YjAP5DN+CEfq2PyG0Df+W1ojPSvlKSCSJQMOG1vr81cCgYEAkjPY5WR99uJxYBNI 25 | OTFMSkETgDUbPXBu/E/h5Dtn79v8Moj9FvC7+q6sg9qXhrGhfK2xDev3/sTrbS/E 26 | Gcf8UNIne3AXsoAS8MtkOwJXHkYaTIboIYgDX4LlDmbGQlIRaWgyh2POI6VtjLBT 27 | ms6AdsdpIB6As9xNUBUwj/RnTZQ= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /tests/video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-telegram-bot-api/telegram-bot-api/4126fa611266940425a9dfd37e0c92ba47881718/tests/video.mp4 -------------------------------------------------------------------------------- /tests/videonote.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-telegram-bot-api/telegram-bot-api/4126fa611266940425a9dfd37e0c92ba47881718/tests/videonote.mp4 -------------------------------------------------------------------------------- /tests/voice.ogg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/go-telegram-bot-api/telegram-bot-api/4126fa611266940425a9dfd37e0c92ba47881718/tests/voice.ogg -------------------------------------------------------------------------------- /types_test.go: -------------------------------------------------------------------------------- 1 | package tgbotapi 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestUserStringWith(t *testing.T) { 9 | user := User{ 10 | ID: 0, 11 | FirstName: "Test", 12 | LastName: "Test", 13 | UserName: "", 14 | LanguageCode: "en", 15 | IsBot: false, 16 | } 17 | 18 | if user.String() != "Test Test" { 19 | t.Fail() 20 | } 21 | } 22 | 23 | func TestUserStringWithUserName(t *testing.T) { 24 | user := User{ 25 | ID: 0, 26 | FirstName: "Test", 27 | LastName: "Test", 28 | UserName: "@test", 29 | LanguageCode: "en", 30 | } 31 | 32 | if user.String() != "@test" { 33 | t.Fail() 34 | } 35 | } 36 | 37 | func TestMessageTime(t *testing.T) { 38 | message := Message{Date: 0} 39 | 40 | date := time.Unix(0, 0) 41 | if message.Time() != date { 42 | t.Fail() 43 | } 44 | } 45 | 46 | func TestMessageIsCommandWithCommand(t *testing.T) { 47 | message := Message{Text: "/command"} 48 | message.Entities = []MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}} 49 | 50 | if !message.IsCommand() { 51 | t.Fail() 52 | } 53 | } 54 | 55 | func TestIsCommandWithText(t *testing.T) { 56 | message := Message{Text: "some text"} 57 | 58 | if message.IsCommand() { 59 | t.Fail() 60 | } 61 | } 62 | 63 | func TestIsCommandWithEmptyText(t *testing.T) { 64 | message := Message{Text: ""} 65 | 66 | if message.IsCommand() { 67 | t.Fail() 68 | } 69 | } 70 | 71 | func TestCommandWithCommand(t *testing.T) { 72 | message := Message{Text: "/command"} 73 | message.Entities = []MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}} 74 | 75 | if message.Command() != "command" { 76 | t.Fail() 77 | } 78 | } 79 | 80 | func TestCommandWithEmptyText(t *testing.T) { 81 | message := Message{Text: ""} 82 | 83 | if message.Command() != "" { 84 | t.Fail() 85 | } 86 | } 87 | 88 | func TestCommandWithNonCommand(t *testing.T) { 89 | message := Message{Text: "test text"} 90 | 91 | if message.Command() != "" { 92 | t.Fail() 93 | } 94 | } 95 | 96 | func TestCommandWithBotName(t *testing.T) { 97 | message := Message{Text: "/command@testbot"} 98 | message.Entities = []MessageEntity{{Type: "bot_command", Offset: 0, Length: 16}} 99 | 100 | if message.Command() != "command" { 101 | t.Fail() 102 | } 103 | } 104 | 105 | func TestCommandWithAtWithBotName(t *testing.T) { 106 | message := Message{Text: "/command@testbot"} 107 | message.Entities = []MessageEntity{{Type: "bot_command", Offset: 0, Length: 16}} 108 | 109 | if message.CommandWithAt() != "command@testbot" { 110 | t.Fail() 111 | } 112 | } 113 | 114 | func TestMessageCommandArgumentsWithArguments(t *testing.T) { 115 | message := Message{Text: "/command with arguments"} 116 | message.Entities = []MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}} 117 | if message.CommandArguments() != "with arguments" { 118 | t.Fail() 119 | } 120 | } 121 | 122 | func TestMessageCommandArgumentsWithMalformedArguments(t *testing.T) { 123 | message := Message{Text: "/command-without argument space"} 124 | message.Entities = []MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}} 125 | if message.CommandArguments() != "without argument space" { 126 | t.Fail() 127 | } 128 | } 129 | 130 | func TestMessageCommandArgumentsWithoutArguments(t *testing.T) { 131 | message := Message{Text: "/command"} 132 | if message.CommandArguments() != "" { 133 | t.Fail() 134 | } 135 | } 136 | 137 | func TestMessageCommandArgumentsForNonCommand(t *testing.T) { 138 | message := Message{Text: "test text"} 139 | if message.CommandArguments() != "" { 140 | t.Fail() 141 | } 142 | } 143 | 144 | func TestMessageEntityParseURLGood(t *testing.T) { 145 | entity := MessageEntity{URL: "https://www.google.com"} 146 | 147 | if _, err := entity.ParseURL(); err != nil { 148 | t.Fail() 149 | } 150 | } 151 | 152 | func TestMessageEntityParseURLBad(t *testing.T) { 153 | entity := MessageEntity{URL: ""} 154 | 155 | if _, err := entity.ParseURL(); err == nil { 156 | t.Fail() 157 | } 158 | } 159 | 160 | func TestChatIsPrivate(t *testing.T) { 161 | chat := Chat{ID: 10, Type: "private"} 162 | 163 | if !chat.IsPrivate() { 164 | t.Fail() 165 | } 166 | } 167 | 168 | func TestChatIsGroup(t *testing.T) { 169 | chat := Chat{ID: 10, Type: "group"} 170 | 171 | if !chat.IsGroup() { 172 | t.Fail() 173 | } 174 | } 175 | 176 | func TestChatIsChannel(t *testing.T) { 177 | chat := Chat{ID: 10, Type: "channel"} 178 | 179 | if !chat.IsChannel() { 180 | t.Fail() 181 | } 182 | } 183 | 184 | func TestChatIsSuperGroup(t *testing.T) { 185 | chat := Chat{ID: 10, Type: "supergroup"} 186 | 187 | if !chat.IsSuperGroup() { 188 | t.Fail() 189 | } 190 | } 191 | 192 | func TestMessageEntityIsMention(t *testing.T) { 193 | entity := MessageEntity{Type: "mention"} 194 | 195 | if !entity.IsMention() { 196 | t.Fail() 197 | } 198 | } 199 | 200 | func TestMessageEntityIsHashtag(t *testing.T) { 201 | entity := MessageEntity{Type: "hashtag"} 202 | 203 | if !entity.IsHashtag() { 204 | t.Fail() 205 | } 206 | } 207 | 208 | func TestMessageEntityIsBotCommand(t *testing.T) { 209 | entity := MessageEntity{Type: "bot_command"} 210 | 211 | if !entity.IsCommand() { 212 | t.Fail() 213 | } 214 | } 215 | 216 | func TestMessageEntityIsUrl(t *testing.T) { 217 | entity := MessageEntity{Type: "url"} 218 | 219 | if !entity.IsURL() { 220 | t.Fail() 221 | } 222 | } 223 | 224 | func TestMessageEntityIsEmail(t *testing.T) { 225 | entity := MessageEntity{Type: "email"} 226 | 227 | if !entity.IsEmail() { 228 | t.Fail() 229 | } 230 | } 231 | 232 | func TestMessageEntityIsBold(t *testing.T) { 233 | entity := MessageEntity{Type: "bold"} 234 | 235 | if !entity.IsBold() { 236 | t.Fail() 237 | } 238 | } 239 | 240 | func TestMessageEntityIsItalic(t *testing.T) { 241 | entity := MessageEntity{Type: "italic"} 242 | 243 | if !entity.IsItalic() { 244 | t.Fail() 245 | } 246 | } 247 | 248 | func TestMessageEntityIsCode(t *testing.T) { 249 | entity := MessageEntity{Type: "code"} 250 | 251 | if !entity.IsCode() { 252 | t.Fail() 253 | } 254 | } 255 | 256 | func TestMessageEntityIsPre(t *testing.T) { 257 | entity := MessageEntity{Type: "pre"} 258 | 259 | if !entity.IsPre() { 260 | t.Fail() 261 | } 262 | } 263 | 264 | func TestMessageEntityIsTextLink(t *testing.T) { 265 | entity := MessageEntity{Type: "text_link"} 266 | 267 | if !entity.IsTextLink() { 268 | t.Fail() 269 | } 270 | } 271 | 272 | func TestFileLink(t *testing.T) { 273 | file := File{FilePath: "test/test.txt"} 274 | 275 | if file.Link("token") != "https://api.telegram.org/file/bottoken/test/test.txt" { 276 | t.Fail() 277 | } 278 | } 279 | 280 | // Ensure all configs are sendable 281 | var ( 282 | _ Chattable = AnimationConfig{} 283 | _ Chattable = AnswerWebAppQueryConfig{} 284 | _ Chattable = AudioConfig{} 285 | _ Chattable = BanChatMemberConfig{} 286 | _ Chattable = BanChatSenderChatConfig{} 287 | _ Chattable = CallbackConfig{} 288 | _ Chattable = ChatActionConfig{} 289 | _ Chattable = ChatAdministratorsConfig{} 290 | _ Chattable = ChatInfoConfig{} 291 | _ Chattable = ChatInviteLinkConfig{} 292 | _ Chattable = CloseConfig{} 293 | _ Chattable = ContactConfig{} 294 | _ Chattable = CopyMessageConfig{} 295 | _ Chattable = CreateChatInviteLinkConfig{} 296 | _ Chattable = DeleteChatPhotoConfig{} 297 | _ Chattable = DeleteChatStickerSetConfig{} 298 | _ Chattable = DeleteMessageConfig{} 299 | _ Chattable = DeleteMyCommandsConfig{} 300 | _ Chattable = DeleteWebhookConfig{} 301 | _ Chattable = DocumentConfig{} 302 | _ Chattable = EditChatInviteLinkConfig{} 303 | _ Chattable = EditMessageCaptionConfig{} 304 | _ Chattable = EditMessageLiveLocationConfig{} 305 | _ Chattable = EditMessageMediaConfig{} 306 | _ Chattable = EditMessageReplyMarkupConfig{} 307 | _ Chattable = EditMessageTextConfig{} 308 | _ Chattable = FileConfig{} 309 | _ Chattable = ForwardConfig{} 310 | _ Chattable = GameConfig{} 311 | _ Chattable = GetChatMemberConfig{} 312 | _ Chattable = GetChatMenuButtonConfig{} 313 | _ Chattable = GetGameHighScoresConfig{} 314 | _ Chattable = GetMyDefaultAdministratorRightsConfig{} 315 | _ Chattable = InlineConfig{} 316 | _ Chattable = InvoiceConfig{} 317 | _ Chattable = KickChatMemberConfig{} 318 | _ Chattable = LeaveChatConfig{} 319 | _ Chattable = LocationConfig{} 320 | _ Chattable = LogOutConfig{} 321 | _ Chattable = MediaGroupConfig{} 322 | _ Chattable = MessageConfig{} 323 | _ Chattable = PhotoConfig{} 324 | _ Chattable = PinChatMessageConfig{} 325 | _ Chattable = PreCheckoutConfig{} 326 | _ Chattable = PromoteChatMemberConfig{} 327 | _ Chattable = RestrictChatMemberConfig{} 328 | _ Chattable = RevokeChatInviteLinkConfig{} 329 | _ Chattable = SendPollConfig{} 330 | _ Chattable = SetChatDescriptionConfig{} 331 | _ Chattable = SetChatMenuButtonConfig{} 332 | _ Chattable = SetChatPhotoConfig{} 333 | _ Chattable = SetChatTitleConfig{} 334 | _ Chattable = SetGameScoreConfig{} 335 | _ Chattable = SetMyDefaultAdministratorRightsConfig{} 336 | _ Chattable = ShippingConfig{} 337 | _ Chattable = StickerConfig{} 338 | _ Chattable = StopMessageLiveLocationConfig{} 339 | _ Chattable = StopPollConfig{} 340 | _ Chattable = UnbanChatMemberConfig{} 341 | _ Chattable = UnbanChatSenderChatConfig{} 342 | _ Chattable = UnpinChatMessageConfig{} 343 | _ Chattable = UpdateConfig{} 344 | _ Chattable = UserProfilePhotosConfig{} 345 | _ Chattable = VenueConfig{} 346 | _ Chattable = VideoConfig{} 347 | _ Chattable = VideoNoteConfig{} 348 | _ Chattable = VoiceConfig{} 349 | _ Chattable = WebhookConfig{} 350 | ) 351 | 352 | // Ensure all Fileable types are correct. 353 | var ( 354 | _ Fileable = (*PhotoConfig)(nil) 355 | _ Fileable = (*AudioConfig)(nil) 356 | _ Fileable = (*DocumentConfig)(nil) 357 | _ Fileable = (*StickerConfig)(nil) 358 | _ Fileable = (*VideoConfig)(nil) 359 | _ Fileable = (*AnimationConfig)(nil) 360 | _ Fileable = (*VideoNoteConfig)(nil) 361 | _ Fileable = (*VoiceConfig)(nil) 362 | _ Fileable = (*SetChatPhotoConfig)(nil) 363 | _ Fileable = (*EditMessageMediaConfig)(nil) 364 | _ Fileable = (*SetChatPhotoConfig)(nil) 365 | _ Fileable = (*UploadStickerConfig)(nil) 366 | _ Fileable = (*NewStickerSetConfig)(nil) 367 | _ Fileable = (*AddStickerConfig)(nil) 368 | _ Fileable = (*MediaGroupConfig)(nil) 369 | _ Fileable = (*WebhookConfig)(nil) 370 | _ Fileable = (*SetStickerSetThumbConfig)(nil) 371 | ) 372 | 373 | // Ensure all RequestFileData types are correct. 374 | var ( 375 | _ RequestFileData = (*FilePath)(nil) 376 | _ RequestFileData = (*FileBytes)(nil) 377 | _ RequestFileData = (*FileReader)(nil) 378 | _ RequestFileData = (*FileURL)(nil) 379 | _ RequestFileData = (*FileID)(nil) 380 | _ RequestFileData = (*fileAttach)(nil) 381 | ) 382 | --------------------------------------------------------------------------------