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