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 log.Println(err)
477 log.Println("Failed to get updates, retrying in 3 seconds...")
478 time.Sleep(time.Second * 3)
479
480 continue
481 }
482
483 for _, update := range updates {
484 if update.UpdateID >= config.Offset {
485 config.Offset = update.UpdateID + 1
486 ch <- update
487 }
488 }
489 }
490 }()
491
492 return ch
493}
494
495// StopReceivingUpdates stops the go routine which receives updates
496func (bot *BotAPI) StopReceivingUpdates() {
497 bot.mu.Lock()
498 defer bot.mu.Unlock()
499
500 if bot.Debug {
501 log.Println("Stopping the update receiver routine...")
502 }
503 for _, stopper := range bot.stoppers {
504 stopper()
505 }
506}
507
508// ListenForWebhook registers a http handler for a webhook.
509func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
510 ch := make(chan Update, bot.Buffer)
511
512 http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
513 update, err := bot.HandleUpdate(r)
514 if err != nil {
515 errMsg, _ := json.Marshal(map[string]string{"error": err.Error()})
516 w.WriteHeader(http.StatusBadRequest)
517 w.Header().Set("Content-Type", "application/json")
518 _, _ = w.Write(errMsg)
519 return
520 }
521
522 ch <- *update
523 })
524
525 return ch
526}
527
528// ListenForWebhookRespReqFormat registers a http handler for a single incoming webhook.
529func (bot *BotAPI) ListenForWebhookRespReqFormat(w http.ResponseWriter, r *http.Request) UpdatesChannel {
530 ch := make(chan Update, bot.Buffer)
531
532 func(w http.ResponseWriter, r *http.Request) {
533 defer close(ch)
534
535 update, err := bot.HandleUpdate(r)
536 if err != nil {
537 errMsg, _ := json.Marshal(map[string]string{"error": err.Error()})
538 w.WriteHeader(http.StatusBadRequest)
539 w.Header().Set("Content-Type", "application/json")
540 _, _ = w.Write(errMsg)
541 return
542 }
543
544 ch <- *update
545 }(w, r)
546
547 return ch
548}
549
550// HandleUpdate parses and returns update received via webhook
551func (bot *BotAPI) HandleUpdate(r *http.Request) (*Update, error) {
552 if r.Method != http.MethodPost {
553 err := errors.New("wrong HTTP method required POST")
554 return nil, err
555 }
556
557 var update Update
558 err := json.NewDecoder(r.Body).Decode(&update)
559 if err != nil {
560 return nil, err
561 }
562
563 return &update, nil
564}
565
566// WriteToHTTPResponse writes the request to the HTTP ResponseWriter.
567//
568// It doesn't support uploading files.
569//
570// See https://core.telegram.org/bots/api#making-requests-when-getting-updates
571// for details.
572func WriteToHTTPResponse(w http.ResponseWriter, c Chattable) error {
573 params, err := c.params()
574 if err != nil {
575 return err
576 }
577
578 if t, ok := c.(Fileable); ok {
579 if hasFilesNeedingUpload(t.files()) {
580 return errors.New("unable to use http response to upload files")
581 }
582 }
583
584 values := buildParams(params)
585 values.Set("method", c.method())
586
587 w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
588 _, err = w.Write([]byte(values.Encode()))
589 return err
590}
591
592// GetChat gets information about a chat.
593func (bot *BotAPI) GetChat(config ChatInfoConfig) (ChatFullInfo, error) {
594 resp, err := bot.Request(config)
595 if err != nil {
596 return ChatFullInfo{}, err
597 }
598
599 var chat ChatFullInfo
600 err = json.Unmarshal(resp.Result, &chat)
601
602 return chat, err
603}
604
605// GetChatAdministrators gets a list of administrators in the chat.
606//
607// If none have been appointed, only the creator will be returned.
608// Bots are not shown, even if they are an administrator.
609func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]ChatMember, error) {
610 resp, err := bot.Request(config)
611 if err != nil {
612 return []ChatMember{}, err
613 }
614
615 var members []ChatMember
616 err = json.Unmarshal(resp.Result, &members)
617
618 return members, err
619}
620
621// GetChatMembersCount gets the number of users in a chat.
622func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) {
623 resp, err := bot.Request(config)
624 if err != nil {
625 return -1, err
626 }
627
628 var count int
629 err = json.Unmarshal(resp.Result, &count)
630
631 return count, err
632}
633
634// GetChatMember gets a specific chat member.
635func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) {
636 resp, err := bot.Request(config)
637 if err != nil {
638 return ChatMember{}, err
639 }
640
641 var member ChatMember
642 err = json.Unmarshal(resp.Result, &member)
643
644 return member, err
645}
646
647// GetGameHighScores allows you to get the high scores for a game.
648func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
649 resp, err := bot.Request(config)
650 if err != nil {
651 return []GameHighScore{}, err
652 }
653
654 var highScores []GameHighScore
655 err = json.Unmarshal(resp.Result, &highScores)
656
657 return highScores, err
658}
659
660// GetInviteLink get InviteLink for a chat
661func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) {
662 resp, err := bot.Request(config)
663 if err != nil {
664 return "", err
665 }
666
667 var inviteLink string
668 err = json.Unmarshal(resp.Result, &inviteLink)
669
670 return inviteLink, err
671}
672
673// GetStickerSet returns a StickerSet.
674func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) {
675 resp, err := bot.Request(config)
676 if err != nil {
677 return StickerSet{}, err
678 }
679
680 var stickerSet StickerSet
681 err = json.Unmarshal(resp.Result, &stickerSet)
682
683 return stickerSet, err
684}
685
686// GetCustomEmojiStickers returns a slice of Sticker objects.
687func (bot *BotAPI) GetCustomEmojiStickers(config GetCustomEmojiStickersConfig) ([]Sticker, error) {
688 resp, err := bot.Request(config)
689 if err != nil {
690 return []Sticker{}, err
691 }
692
693 var stickers []Sticker
694 err = json.Unmarshal(resp.Result, &stickers)
695
696 return stickers, err
697}
698
699// StopPoll stops a poll and returns the result.
700func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) {
701 resp, err := bot.Request(config)
702 if err != nil {
703 return Poll{}, err
704 }
705
706 var poll Poll
707 err = json.Unmarshal(resp.Result, &poll)
708
709 return poll, err
710}
711
712// GetMyCommands gets the currently registered commands.
713func (bot *BotAPI) GetMyCommands() ([]BotCommand, error) {
714 return bot.GetMyCommandsWithConfig(GetMyCommandsConfig{})
715}
716
717// GetMyCommandsWithConfig gets the currently registered commands with a config.
718func (bot *BotAPI) GetMyCommandsWithConfig(config GetMyCommandsConfig) ([]BotCommand, error) {
719 resp, err := bot.Request(config)
720 if err != nil {
721 return nil, err
722 }
723
724 var commands []BotCommand
725 err = json.Unmarshal(resp.Result, &commands)
726
727 return commands, err
728}
729
730// CopyMessage copy messages of any kind. The method is analogous to the method
731// forwardMessage, but the copied message doesn't have a link to the original
732// message. Returns the MessageID of the sent message on success.
733func (bot *BotAPI) CopyMessage(config CopyMessageConfig) (MessageID, error) {
734 resp, err := bot.Request(config)
735 if err != nil {
736 return MessageID{}, err
737 }
738
739 var messageID MessageID
740 err = json.Unmarshal(resp.Result, &messageID)
741
742 return messageID, err
743}
744
745// AnswerWebAppQuery sets the result of an interaction with a Web App and send a
746// corresponding message on behalf of the user to the chat from which the query originated.
747func (bot *BotAPI) AnswerWebAppQuery(config AnswerWebAppQueryConfig) (SentWebAppMessage, error) {
748 var sentWebAppMessage SentWebAppMessage
749
750 resp, err := bot.Request(config)
751 if err != nil {
752 return sentWebAppMessage, err
753 }
754
755 err = json.Unmarshal(resp.Result, &sentWebAppMessage)
756 return sentWebAppMessage, err
757}
758
759// GetMyDefaultAdministratorRights gets the current default administrator rights of the bot.
760func (bot *BotAPI) GetMyDefaultAdministratorRights(config GetMyDefaultAdministratorRightsConfig) (ChatAdministratorRights, error) {
761 var rights ChatAdministratorRights
762
763 resp, err := bot.Request(config)
764 if err != nil {
765 return rights, err
766 }
767
768 err = json.Unmarshal(resp.Result, &rights)
769 return rights, err
770}
771
772// EscapeText takes an input text and escape Telegram markup symbols.
773// In this way we can send a text without being afraid of having to escape the characters manually.
774// Note that you don't have to include the formatting style in the input text, or it will be escaped too.
775// If there is an error, an empty string will be returned.
776//
777// parseMode is the text formatting mode (ModeMarkdown, ModeMarkdownV2 or ModeHTML)
778// text is the input string that will be escaped
779func EscapeText(parseMode string, text string) string {
780 var replacer *strings.Replacer
781
782 if parseMode == ModeHTML {
783 replacer = strings.NewReplacer("<", "<", ">", ">", "&", "&")
784 } else if parseMode == ModeMarkdown {
785 replacer = strings.NewReplacer("_", "\\_", "*", "\\*", "`", "\\`", "[", "\\[")
786 } else if parseMode == ModeMarkdownV2 {
787 replacer = strings.NewReplacer(
788 "_", "\\_", "*", "\\*", "[", "\\[", "]", "\\]", "(",
789 "\\(", ")", "\\)", "~", "\\~", "`", "\\`", ">", "\\>",
790 "#", "\\#", "+", "\\+", "-", "\\-", "=", "\\=", "|",
791 "\\|", "{", "\\{", "}", "\\}", ".", "\\.", "!", "\\!",
792 )
793 } else {
794 return ""
795 }
796
797 return replacer.Replace(text)
798}