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