bot.go (view raw)
1// Package tgbotapi has functions and types used for interacting with
2// the Telegram Bot API.
3package tgbotapi
4
5import (
6 "encoding/json"
7 "errors"
8 "fmt"
9 "io"
10 "io/ioutil"
11 "mime/multipart"
12 "net/http"
13 "net/url"
14 "strings"
15 "time"
16)
17
18// HTTPClient is the type needed for the bot to perform HTTP requests.
19type HTTPClient interface {
20 Do(req *http.Request) (*http.Response, error)
21}
22
23// BotAPI allows you to interact with the Telegram Bot API.
24type BotAPI struct {
25 Token string `json:"token"`
26 Debug bool `json:"debug"`
27 Buffer int `json:"buffer"`
28
29 Self User `json:"-"`
30 Client HTTPClient `json:"-"`
31 shutdownChannel chan interface{}
32
33 apiEndpoint string
34}
35
36// NewBotAPI creates a new BotAPI instance.
37//
38// It requires a token, provided by @BotFather on Telegram.
39func NewBotAPI(token string) (*BotAPI, error) {
40 return NewBotAPIWithClient(token, APIEndpoint, &http.Client{})
41}
42
43// NewBotAPIWithAPIEndpoint creates a new BotAPI instance
44// and allows you to pass API endpoint.
45//
46// It requires a token, provided by @BotFather on Telegram and API endpoint.
47func NewBotAPIWithAPIEndpoint(token, apiEndpoint string) (*BotAPI, error) {
48 return NewBotAPIWithClient(token, apiEndpoint, &http.Client{})
49}
50
51// NewBotAPIWithClient creates a new BotAPI instance
52// and allows you to pass a http.Client.
53//
54// It requires a token, provided by @BotFather on Telegram and API endpoint.
55func NewBotAPIWithClient(token, apiEndpoint string, client HTTPClient) (*BotAPI, error) {
56 bot := &BotAPI{
57 Token: token,
58 Client: client,
59 Buffer: 100,
60 shutdownChannel: make(chan interface{}),
61
62 apiEndpoint: apiEndpoint,
63 }
64
65 self, err := bot.GetMe()
66 if err != nil {
67 return nil, err
68 }
69
70 bot.Self = self
71
72 return bot, nil
73}
74
75// SetAPIEndpoint changes the Telegram Bot API endpoint used by the instance.
76func (bot *BotAPI) SetAPIEndpoint(apiEndpoint string) {
77 bot.apiEndpoint = apiEndpoint
78}
79
80func buildParams(in Params) url.Values {
81 if in == nil {
82 return url.Values{}
83 }
84
85 out := url.Values{}
86
87 for key, value := range in {
88 out.Set(key, value)
89 }
90
91 return out
92}
93
94// MakeRequest makes a request to a specific endpoint with our token.
95func (bot *BotAPI) MakeRequest(endpoint string, params Params) (*APIResponse, error) {
96 if bot.Debug {
97 log.Printf("Endpoint: %s, params: %v\n", endpoint, params)
98 }
99
100 method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint)
101
102 values := buildParams(params)
103
104 req, err := http.NewRequest("POST", method, strings.NewReader(values.Encode()))
105 if err != nil {
106 return &APIResponse{}, err
107 }
108 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
109
110 resp, err := bot.Client.Do(req)
111 if err != nil {
112 return nil, err
113 }
114 defer resp.Body.Close()
115
116 var apiResp APIResponse
117 bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
118 if err != nil {
119 return &apiResp, err
120 }
121
122 if bot.Debug {
123 log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
124 }
125
126 if !apiResp.Ok {
127 var parameters ResponseParameters
128
129 if apiResp.Parameters != nil {
130 parameters = *apiResp.Parameters
131 }
132
133 return &apiResp, &Error{
134 Code: apiResp.ErrorCode,
135 Message: apiResp.Description,
136 ResponseParameters: parameters,
137 }
138 }
139
140 return &apiResp, nil
141}
142
143// decodeAPIResponse decode response and return slice of bytes if debug enabled.
144// If debug disabled, just decode http.Response.Body stream to APIResponse struct
145// for efficient memory usage
146func (bot *BotAPI) decodeAPIResponse(responseBody io.Reader, resp *APIResponse) ([]byte, error) {
147 if !bot.Debug {
148 dec := json.NewDecoder(responseBody)
149 err := dec.Decode(resp)
150 return nil, err
151 }
152
153 // if debug, read response body
154 data, err := ioutil.ReadAll(responseBody)
155 if err != nil {
156 return nil, err
157 }
158
159 err = json.Unmarshal(data, resp)
160 if err != nil {
161 return nil, err
162 }
163
164 return data, nil
165}
166
167// UploadFiles makes a request to the API with files.
168func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFile) (*APIResponse, error) {
169 r, w := io.Pipe()
170 m := multipart.NewWriter(w)
171
172 // This code modified from the very helpful @HirbodBehnam
173 // https://github.com/go-telegram-bot-api/telegram-bot-api/issues/354#issuecomment-663856473
174 go func() {
175 defer w.Close()
176 defer m.Close()
177
178 for field, value := range params {
179 if err := m.WriteField(field, value); err != nil {
180 w.CloseWithError(err)
181 return
182 }
183 }
184
185 for _, file := range files {
186 if file.Data.NeedsUpload() {
187 name, reader, err := file.Data.UploadData()
188 if err != nil {
189 w.CloseWithError(err)
190 return
191 }
192
193 part, err := m.CreateFormFile(file.Name, name)
194 if err != nil {
195 w.CloseWithError(err)
196 return
197 }
198
199 if _, err := io.Copy(part, reader); err != nil {
200 w.CloseWithError(err)
201 return
202 }
203
204 if closer, ok := reader.(io.ReadCloser); ok {
205 if err = closer.Close(); err != nil {
206 w.CloseWithError(err)
207 return
208 }
209 }
210 } else {
211 value := file.Data.SendData()
212
213 if err := m.WriteField(file.Name, value); err != nil {
214 w.CloseWithError(err)
215 return
216 }
217 }
218 }
219 }()
220
221 if bot.Debug {
222 log.Printf("Endpoint: %s, params: %v, with %d files\n", endpoint, params, len(files))
223 }
224
225 method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint)
226
227 req, err := http.NewRequest("POST", method, r)
228 if err != nil {
229 return nil, err
230 }
231
232 req.Header.Set("Content-Type", m.FormDataContentType())
233
234 resp, err := bot.Client.Do(req)
235 if err != nil {
236 return nil, err
237 }
238 defer resp.Body.Close()
239
240 var apiResp APIResponse
241 bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
242 if err != nil {
243 return &apiResp, err
244 }
245
246 if bot.Debug {
247 log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
248 }
249
250 if !apiResp.Ok {
251 var parameters ResponseParameters
252
253 if apiResp.Parameters != nil {
254 parameters = *apiResp.Parameters
255 }
256
257 return &apiResp, &Error{
258 Message: apiResp.Description,
259 ResponseParameters: parameters,
260 }
261 }
262
263 return &apiResp, nil
264}
265
266// GetFileDirectURL returns direct URL to file
267//
268// It requires the FileID.
269func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) {
270 file, err := bot.GetFile(FileConfig{fileID})
271
272 if err != nil {
273 return "", err
274 }
275
276 return file.Link(bot.Token), nil
277}
278
279// GetMe fetches the currently authenticated bot.
280//
281// This method is called upon creation to validate the token,
282// and so you may get this data from BotAPI.Self without the need for
283// another request.
284func (bot *BotAPI) GetMe() (User, error) {
285 resp, err := bot.MakeRequest("getMe", nil)
286 if err != nil {
287 return User{}, err
288 }
289
290 var user User
291 err = json.Unmarshal(resp.Result, &user)
292
293 return user, err
294}
295
296// IsMessageToMe returns true if message directed to this bot.
297//
298// It requires the Message.
299func (bot *BotAPI) IsMessageToMe(message Message) bool {
300 return strings.Contains(message.Text, "@"+bot.Self.UserName)
301}
302
303func hasFilesNeedingUpload(files []RequestFile) bool {
304 for _, file := range files {
305 if file.Data.NeedsUpload() {
306 return true
307 }
308 }
309
310 return false
311}
312
313// Request sends a Chattable to Telegram, and returns the APIResponse.
314func (bot *BotAPI) Request(c Chattable) (*APIResponse, error) {
315 params, err := c.params()
316 if err != nil {
317 return nil, err
318 }
319
320 if t, ok := c.(Fileable); ok {
321 files := t.files()
322
323 // If we have files that need to be uploaded, we should delegate the
324 // request to UploadFile.
325 if hasFilesNeedingUpload(files) {
326 return bot.UploadFiles(t.method(), params, files)
327 }
328
329 // However, if there are no files to be uploaded, there's likely things
330 // that need to be turned into params instead.
331 for _, file := range files {
332 params[file.Name] = file.Data.SendData()
333 }
334 }
335
336 return bot.MakeRequest(c.method(), params)
337}
338
339// Send will send a Chattable item to Telegram and provides the
340// returned Message.
341func (bot *BotAPI) Send(c Chattable) (Message, error) {
342 resp, err := bot.Request(c)
343 if err != nil {
344 return Message{}, err
345 }
346
347 var message Message
348 err = json.Unmarshal(resp.Result, &message)
349
350 return message, err
351}
352
353// SendMediaGroup sends a media group and returns the resulting messages.
354func (bot *BotAPI) SendMediaGroup(config MediaGroupConfig) ([]Message, error) {
355 resp, err := bot.Request(config)
356 if err != nil {
357 return nil, err
358 }
359
360 var messages []Message
361 err = json.Unmarshal(resp.Result, &messages)
362
363 return messages, err
364}
365
366// GetUserProfilePhotos gets a user's profile photos.
367//
368// It requires UserID.
369// Offset and Limit are optional.
370func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
371 resp, err := bot.Request(config)
372 if err != nil {
373 return UserProfilePhotos{}, err
374 }
375
376 var profilePhotos UserProfilePhotos
377 err = json.Unmarshal(resp.Result, &profilePhotos)
378
379 return profilePhotos, err
380}
381
382// GetFile returns a File which can download a file from Telegram.
383//
384// Requires FileID.
385func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
386 resp, err := bot.Request(config)
387 if err != nil {
388 return File{}, err
389 }
390
391 var file File
392 err = json.Unmarshal(resp.Result, &file)
393
394 return file, err
395}
396
397// GetUpdates fetches updates.
398// If a WebHook is set, this will not return any data!
399//
400// Offset, Limit, Timeout, and AllowedUpdates are optional.
401// To avoid stale items, set Offset to one higher than the previous item.
402// Set Timeout to a large number to reduce requests, so you can get updates
403// instantly instead of having to wait between requests.
404func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
405 resp, err := bot.Request(config)
406 if err != nil {
407 return []Update{}, err
408 }
409
410 var updates []Update
411 err = json.Unmarshal(resp.Result, &updates)
412
413 return updates, err
414}
415
416// GetWebhookInfo allows you to fetch information about a webhook and if
417// one currently is set, along with pending update count and error messages.
418func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) {
419 resp, err := bot.MakeRequest("getWebhookInfo", nil)
420 if err != nil {
421 return WebhookInfo{}, err
422 }
423
424 var info WebhookInfo
425 err = json.Unmarshal(resp.Result, &info)
426
427 return info, err
428}
429
430// GetUpdatesChan starts and returns a channel for getting updates.
431func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) UpdatesChannel {
432 ch := make(chan Update, bot.Buffer)
433
434 go func() {
435 for {
436 select {
437 case <-bot.shutdownChannel:
438 close(ch)
439 return
440 default:
441 }
442
443 updates, err := bot.GetUpdates(config)
444 if err != nil {
445 log.Println(err)
446 log.Println("Failed to get updates, retrying in 3 seconds...")
447 time.Sleep(time.Second * 3)
448
449 continue
450 }
451
452 for _, update := range updates {
453 if update.UpdateID >= config.Offset {
454 config.Offset = update.UpdateID + 1
455 ch <- update
456 }
457 }
458 }
459 }()
460
461 return ch
462}
463
464// StopReceivingUpdates stops the go routine which receives updates
465func (bot *BotAPI) StopReceivingUpdates() {
466 if bot.Debug {
467 log.Println("Stopping the update receiver routine...")
468 }
469 close(bot.shutdownChannel)
470}
471
472// ListenForWebhook registers a http handler for a webhook.
473func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
474 ch := make(chan Update, bot.Buffer)
475
476 http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
477 update, err := bot.HandleUpdate(r)
478 if err != nil {
479 errMsg, _ := json.Marshal(map[string]string{"error": err.Error()})
480 w.WriteHeader(http.StatusBadRequest)
481 w.Header().Set("Content-Type", "application/json")
482 _, _ = w.Write(errMsg)
483 return
484 }
485
486 ch <- *update
487 close(ch)
488 })
489
490 return ch
491}
492
493// ListenForWebhookRespReqFormat registers a http handler for a webhook.
494func (bot *BotAPI) ListenForWebhookRespReqFormat(w http.ResponseWriter, r *http.Request) UpdatesChannel {
495 ch := make(chan Update, bot.Buffer)
496
497 func(w http.ResponseWriter, r *http.Request) {
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 close(ch)
509 }(w, r)
510
511 return ch
512}
513
514// HandleUpdate parses and returns update received via webhook
515func (bot *BotAPI) HandleUpdate(r *http.Request) (*Update, error) {
516 if r.Method != http.MethodPost {
517 err := errors.New("wrong HTTP method required POST")
518 return nil, err
519 }
520
521 var update Update
522 err := json.NewDecoder(r.Body).Decode(&update)
523 if err != nil {
524 return nil, err
525 }
526
527 return &update, nil
528}
529
530// WriteToHTTPResponse writes the request to the HTTP ResponseWriter.
531//
532// It doesn't support uploading files.
533//
534// See https://core.telegram.org/bots/api#making-requests-when-getting-updates
535// for details.
536func WriteToHTTPResponse(w http.ResponseWriter, c Chattable) error {
537 params, err := c.params()
538 if err != nil {
539 return err
540 }
541
542 if t, ok := c.(Fileable); ok {
543 if hasFilesNeedingUpload(t.files()) {
544 return errors.New("unable to use http response to upload files")
545 }
546 }
547
548 values := buildParams(params)
549 values.Set("method", c.method())
550
551 w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
552 _, err = w.Write([]byte(values.Encode()))
553 return err
554}
555
556// GetChat gets information about a chat.
557func (bot *BotAPI) GetChat(config ChatInfoConfig) (Chat, error) {
558 resp, err := bot.Request(config)
559 if err != nil {
560 return Chat{}, err
561 }
562
563 var chat Chat
564 err = json.Unmarshal(resp.Result, &chat)
565
566 return chat, err
567}
568
569// GetChatAdministrators gets a list of administrators in the chat.
570//
571// If none have been appointed, only the creator will be returned.
572// Bots are not shown, even if they are an administrator.
573func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]ChatMember, error) {
574 resp, err := bot.Request(config)
575 if err != nil {
576 return []ChatMember{}, err
577 }
578
579 var members []ChatMember
580 err = json.Unmarshal(resp.Result, &members)
581
582 return members, err
583}
584
585// GetChatMembersCount gets the number of users in a chat.
586func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) {
587 resp, err := bot.Request(config)
588 if err != nil {
589 return -1, err
590 }
591
592 var count int
593 err = json.Unmarshal(resp.Result, &count)
594
595 return count, err
596}
597
598// GetChatMember gets a specific chat member.
599func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) {
600 resp, err := bot.Request(config)
601 if err != nil {
602 return ChatMember{}, err
603 }
604
605 var member ChatMember
606 err = json.Unmarshal(resp.Result, &member)
607
608 return member, err
609}
610
611// GetGameHighScores allows you to get the high scores for a game.
612func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
613 resp, err := bot.Request(config)
614 if err != nil {
615 return []GameHighScore{}, err
616 }
617
618 var highScores []GameHighScore
619 err = json.Unmarshal(resp.Result, &highScores)
620
621 return highScores, err
622}
623
624// GetInviteLink get InviteLink for a chat
625func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) {
626 resp, err := bot.Request(config)
627 if err != nil {
628 return "", err
629 }
630
631 var inviteLink string
632 err = json.Unmarshal(resp.Result, &inviteLink)
633
634 return inviteLink, err
635}
636
637// GetStickerSet returns a StickerSet.
638func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) {
639 resp, err := bot.Request(config)
640 if err != nil {
641 return StickerSet{}, err
642 }
643
644 var stickers StickerSet
645 err = json.Unmarshal(resp.Result, &stickers)
646
647 return stickers, err
648}
649
650// StopPoll stops a poll and returns the result.
651func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) {
652 resp, err := bot.Request(config)
653 if err != nil {
654 return Poll{}, err
655 }
656
657 var poll Poll
658 err = json.Unmarshal(resp.Result, &poll)
659
660 return poll, err
661}
662
663// GetMyCommands gets the currently registered commands.
664func (bot *BotAPI) GetMyCommands() ([]BotCommand, error) {
665 return bot.GetMyCommandsWithConfig(GetMyCommandsConfig{})
666}
667
668// GetMyCommandsWithConfig gets the currently registered commands with a config.
669func (bot *BotAPI) GetMyCommandsWithConfig(config GetMyCommandsConfig) ([]BotCommand, error) {
670 resp, err := bot.Request(config)
671 if err != nil {
672 return nil, err
673 }
674
675 var commands []BotCommand
676 err = json.Unmarshal(resp.Result, &commands)
677
678 return commands, err
679}
680
681// CopyMessage copy messages of any kind. The method is analogous to the method
682// forwardMessage, but the copied message doesn't have a link to the original
683// message. Returns the MessageID of the sent message on success.
684func (bot *BotAPI) CopyMessage(config CopyMessageConfig) (MessageID, error) {
685 params, err := config.params()
686 if err != nil {
687 return MessageID{}, err
688 }
689
690 resp, err := bot.MakeRequest(config.method(), params)
691 if err != nil {
692 return MessageID{}, err
693 }
694
695 var messageID MessageID
696 err = json.Unmarshal(resp.Result, &messageID)
697
698 return messageID, err
699}
700
701// EscapeText takes an input text and escape Telegram markup symbols.
702// In this way we can send a text without being afraid of having to escape the characters manually.
703// Note that you don't have to include the formatting style in the input text, or it will be escaped too.
704// If there is an error, an empty string will be returned.
705//
706// parseMode is the text formatting mode (ModeMarkdown, ModeMarkdownV2 or ModeHTML)
707// text is the input string that will be escaped
708func EscapeText(parseMode string, text string) string {
709 var replacer *strings.Replacer
710
711 if parseMode == ModeHTML {
712 replacer = strings.NewReplacer("<", "<", ">", ">", "&", "&")
713 } else if parseMode == ModeMarkdown {
714 replacer = strings.NewReplacer("_", "\\_", "*", "\\*", "`", "\\`", "[", "\\[")
715 } else if parseMode == ModeMarkdownV2 {
716 replacer = strings.NewReplacer(
717 "_", "\\_", "*", "\\*", "[", "\\[", "]", "\\]", "(",
718 "\\(", ")", "\\)", "~", "\\~", "`", "\\`", ">", "\\>",
719 "#", "\\#", "+", "\\+", "-", "\\-", "=", "\\=", "|",
720 "\\|", "{", "\\{", "}", "\\}", ".", "\\.", "!", "\\!",
721 )
722 } else {
723 return ""
724 }
725
726 return replacer.Replace(text)
727}