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 "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.
18type HTTPClient interface {
19 Do(req *http.Request) (*http.Response, error)
20}
21
22// BotAPI allows you to interact with the Telegram Bot API.
23type 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.
38func 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.
46func 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.
54func 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.
75func (bot *BotAPI) SetAPIEndpoint(apiEndpoint string) {
76 bot.apiEndpoint = apiEndpoint
77}
78
79func 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.
94func (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
145func (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.
167func (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.
268func (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.
283func (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.
298func (bot *BotAPI) IsMessageToMe(message Message) bool {
299 return strings.Contains(message.Text, "@"+bot.Self.UserName)
300}
301
302func 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.
313func (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.
340func (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.
353func (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.
369func (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.
384func (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.
403func (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.
417func (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.
430func (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
464func (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.
472func (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.
492func (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
514func (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.
535func 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.
556func (bot *BotAPI) GetChat(config ChatInfoConfig) (ChatFullInfo, error) {
557 resp, err := bot.Request(config)
558 if err != nil {
559 return ChatFullInfo{}, err
560 }
561
562 var chat ChatFullInfo
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.
572func (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.
585func (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.
598func (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.
611func (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
624func (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.
637func (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 stickerSet StickerSet
644 err = json.Unmarshal(resp.Result, &stickerSet)
645
646 return stickerSet, err
647}
648
649// GetCustomEmojiStickers returns a slice of Sticker objects.
650func (bot *BotAPI) GetCustomEmojiStickers(config GetCustomEmojiStickersConfig) ([]Sticker, error) {
651 resp, err := bot.Request(config)
652 if err != nil {
653 return []Sticker{}, err
654 }
655
656 var stickers []Sticker
657 err = json.Unmarshal(resp.Result, &stickers)
658
659 return stickers, err
660}
661
662// StopPoll stops a poll and returns the result.
663func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) {
664 resp, err := bot.Request(config)
665 if err != nil {
666 return Poll{}, err
667 }
668
669 var poll Poll
670 err = json.Unmarshal(resp.Result, &poll)
671
672 return poll, err
673}
674
675// GetMyCommands gets the currently registered commands.
676func (bot *BotAPI) GetMyCommands() ([]BotCommand, error) {
677 return bot.GetMyCommandsWithConfig(GetMyCommandsConfig{})
678}
679
680// GetMyCommandsWithConfig gets the currently registered commands with a config.
681func (bot *BotAPI) GetMyCommandsWithConfig(config GetMyCommandsConfig) ([]BotCommand, error) {
682 resp, err := bot.Request(config)
683 if err != nil {
684 return nil, err
685 }
686
687 var commands []BotCommand
688 err = json.Unmarshal(resp.Result, &commands)
689
690 return commands, err
691}
692
693// CopyMessage copy messages of any kind. The method is analogous to the method
694// forwardMessage, but the copied message doesn't have a link to the original
695// message. Returns the MessageID of the sent message on success.
696func (bot *BotAPI) CopyMessage(config CopyMessageConfig) (MessageID, error) {
697 resp, err := bot.Request(config)
698 if err != nil {
699 return MessageID{}, err
700 }
701
702 var messageID MessageID
703 err = json.Unmarshal(resp.Result, &messageID)
704
705 return messageID, err
706}
707
708// AnswerWebAppQuery sets the result of an interaction with a Web App and send a
709// corresponding message on behalf of the user to the chat from which the query originated.
710func (bot *BotAPI) AnswerWebAppQuery(config AnswerWebAppQueryConfig) (SentWebAppMessage, error) {
711 var sentWebAppMessage SentWebAppMessage
712
713 resp, err := bot.Request(config)
714 if err != nil {
715 return sentWebAppMessage, err
716 }
717
718 err = json.Unmarshal(resp.Result, &sentWebAppMessage)
719 return sentWebAppMessage, err
720}
721
722// GetMyDefaultAdministratorRights gets the current default administrator rights of the bot.
723func (bot *BotAPI) GetMyDefaultAdministratorRights(config GetMyDefaultAdministratorRightsConfig) (ChatAdministratorRights, error) {
724 var rights ChatAdministratorRights
725
726 resp, err := bot.Request(config)
727 if err != nil {
728 return rights, err
729 }
730
731 err = json.Unmarshal(resp.Result, &rights)
732 return rights, err
733}
734
735// EscapeText takes an input text and escape Telegram markup symbols.
736// In this way we can send a text without being afraid of having to escape the characters manually.
737// Note that you don't have to include the formatting style in the input text, or it will be escaped too.
738// If there is an error, an empty string will be returned.
739//
740// parseMode is the text formatting mode (ModeMarkdown, ModeMarkdownV2 or ModeHTML)
741// text is the input string that will be escaped
742func EscapeText(parseMode string, text string) string {
743 var replacer *strings.Replacer
744
745 if parseMode == ModeHTML {
746 replacer = strings.NewReplacer("<", "<", ">", ">", "&", "&")
747 } else if parseMode == ModeMarkdown {
748 replacer = strings.NewReplacer("_", "\\_", "*", "\\*", "`", "\\`", "[", "\\[")
749 } else if parseMode == ModeMarkdownV2 {
750 replacer = strings.NewReplacer(
751 "_", "\\_", "*", "\\*", "[", "\\[", "]", "\\]", "(",
752 "\\(", ")", "\\)", "~", "\\~", "`", "\\`", ">", "\\>",
753 "#", "\\#", "+", "\\+", "-", "\\-", "=", "\\=", "|",
754 "\\|", "{", "\\{", "}", "\\}", ".", "\\.", "!", "\\!",
755 )
756 } else {
757 return ""
758 }
759
760 return replacer.Replace(text)
761}