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