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