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