├── .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 | [](https://pkg.go.dev/github.com/go-telegram-bot-api/telegram-bot-api/v5)
4 | [](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 |
--------------------------------------------------------------------------------