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