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