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 resp, err := bot.Request(config)
381 if err != nil {
382 return nil, err
383 }
384
385 var messages []Message
386 err = json.Unmarshal(resp.Result, &messages)
387
388 return messages, err
389}
390
391// GetUserProfilePhotos gets a user's profile photos.
392//
393// It requires UserID.
394// Offset and Limit are optional.
395func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
396 resp, err := bot.Request(config)
397 if err != nil {
398 return UserProfilePhotos{}, err
399 }
400
401 var profilePhotos UserProfilePhotos
402 err = json.Unmarshal(resp.Result, &profilePhotos)
403
404 return profilePhotos, err
405}
406
407// GetFile returns a File which can download a file from Telegram.
408//
409// Requires FileID.
410func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
411 resp, err := bot.Request(config)
412 if err != nil {
413 return File{}, err
414 }
415
416 var file File
417 err = json.Unmarshal(resp.Result, &file)
418
419 return file, err
420}
421
422// GetUpdates fetches updates.
423// If a WebHook is set, this will not return any data!
424//
425// Offset, Limit, and Timeout are optional.
426// To avoid stale items, set Offset to one higher than the previous item.
427// Set Timeout to a large number to reduce requests so you can get updates
428// instantly instead of having to wait between requests.
429func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
430 resp, err := bot.Request(config)
431 if err != nil {
432 return []Update{}, err
433 }
434
435 var updates []Update
436 err = json.Unmarshal(resp.Result, &updates)
437
438 return updates, err
439}
440
441// GetWebhookInfo allows you to fetch information about a webhook and if
442// one currently is set, along with pending update count and error messages.
443func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) {
444 resp, err := bot.MakeRequest("getWebhookInfo", nil)
445 if err != nil {
446 return WebhookInfo{}, err
447 }
448
449 var info WebhookInfo
450 err = json.Unmarshal(resp.Result, &info)
451
452 return info, err
453}
454
455// GetUpdatesChan starts and returns a channel for getting updates.
456func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) UpdatesChannel {
457 ch := make(chan Update, bot.Buffer)
458
459 go func() {
460 for {
461 select {
462 case <-bot.shutdownChannel:
463 return
464 default:
465 }
466
467 updates, err := bot.GetUpdates(config)
468 if err != nil {
469 log.Println(err)
470 log.Println("Failed to get updates, retrying in 3 seconds...")
471 time.Sleep(time.Second * 3)
472
473 continue
474 }
475
476 for _, update := range updates {
477 if update.UpdateID >= config.Offset {
478 config.Offset = update.UpdateID + 1
479 ch <- update
480 }
481 }
482 }
483 }()
484
485 return ch
486}
487
488// StopReceivingUpdates stops the go routine which receives updates
489func (bot *BotAPI) StopReceivingUpdates() {
490 if bot.Debug {
491 log.Println("Stopping the update receiver routine...")
492 }
493 close(bot.shutdownChannel)
494}
495
496// ListenForWebhook registers a http handler for a webhook.
497func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
498 ch := make(chan Update, bot.Buffer)
499
500 http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
501 ch <- bot.HandleUpdate(w, r)
502 })
503
504 return ch
505}
506
507// HandleUpdate parses and returns update received via webhook
508func (bot *BotAPI) HandleUpdate(res http.ResponseWriter, req *http.Request) Update {
509 bytes, _ := ioutil.ReadAll(req.Body)
510 req.Body.Close()
511
512 var update Update
513 json.Unmarshal(bytes, &update)
514
515 return update
516}
517
518// WriteToHTTPResponse writes the request to the HTTP ResponseWriter.
519//
520// It doesn't support uploading files.
521//
522// See https://core.telegram.org/bots/api#making-requests-when-getting-updates
523// for details.
524func WriteToHTTPResponse(w http.ResponseWriter, c Chattable) error {
525 params, err := c.params()
526 if err != nil {
527 return err
528 }
529
530 if t, ok := c.(Fileable); ok {
531 if hasFilesNeedingUpload(t.files()) {
532 return errors.New("unable to use http response to upload files")
533 }
534 }
535
536 values := buildParams(params)
537 values.Set("method", c.method())
538
539 w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
540 _, err = w.Write([]byte(values.Encode()))
541 return err
542}
543
544// GetChat gets information about a chat.
545func (bot *BotAPI) GetChat(config ChatInfoConfig) (Chat, error) {
546 resp, err := bot.Request(config)
547 if err != nil {
548 return Chat{}, err
549 }
550
551 var chat Chat
552 err = json.Unmarshal(resp.Result, &chat)
553
554 return chat, err
555}
556
557// GetChatAdministrators gets a list of administrators in the chat.
558//
559// If none have been appointed, only the creator will be returned.
560// Bots are not shown, even if they are an administrator.
561func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]ChatMember, error) {
562 resp, err := bot.Request(config)
563 if err != nil {
564 return []ChatMember{}, err
565 }
566
567 var members []ChatMember
568 err = json.Unmarshal(resp.Result, &members)
569
570 return members, err
571}
572
573// GetChatMembersCount gets the number of users in a chat.
574func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) {
575 resp, err := bot.Request(config)
576 if err != nil {
577 return -1, err
578 }
579
580 var count int
581 err = json.Unmarshal(resp.Result, &count)
582
583 return count, err
584}
585
586// GetChatMember gets a specific chat member.
587func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) {
588 resp, err := bot.Request(config)
589 if err != nil {
590 return ChatMember{}, err
591 }
592
593 var member ChatMember
594 err = json.Unmarshal(resp.Result, &member)
595
596 return member, err
597}
598
599// GetGameHighScores allows you to get the high scores for a game.
600func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
601 resp, err := bot.Request(config)
602 if err != nil {
603 return []GameHighScore{}, err
604 }
605
606 var highScores []GameHighScore
607 err = json.Unmarshal(resp.Result, &highScores)
608
609 return highScores, err
610}
611
612// GetInviteLink get InviteLink for a chat
613func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) {
614 resp, err := bot.Request(config)
615 if err != nil {
616 return "", err
617 }
618
619 var inviteLink string
620 err = json.Unmarshal(resp.Result, &inviteLink)
621
622 return inviteLink, err
623}
624
625// GetStickerSet returns a StickerSet.
626func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) {
627 resp, err := bot.Request(config)
628 if err != nil {
629 return StickerSet{}, err
630 }
631
632 var stickers StickerSet
633 err = json.Unmarshal(resp.Result, &stickers)
634
635 return stickers, err
636}
637
638// StopPoll stops a poll and returns the result.
639func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) {
640 resp, err := bot.Request(config)
641 if err != nil {
642 return Poll{}, err
643 }
644
645 var poll Poll
646 err = json.Unmarshal(resp.Result, &poll)
647
648 return poll, err
649}
650
651// GetMyCommands gets the currently registered commands.
652func (bot *BotAPI) GetMyCommands() ([]BotCommand, error) {
653 config := GetMyCommandsConfig{}
654
655 resp, err := bot.Request(config)
656 if err != nil {
657 return nil, err
658 }
659
660 var commands []BotCommand
661 err = json.Unmarshal(resp.Result, &commands)
662
663 return commands, err
664}