bot.go (view raw)
1// Package tgbotapi has bindings for interacting with the Telegram Bot API.
2package tgbotapi
3
4import (
5 "bytes"
6 "encoding/json"
7 "errors"
8 "fmt"
9 "github.com/technoweenie/multipartstreamer"
10 "io/ioutil"
11 "log"
12 "net/http"
13 "net/url"
14 "os"
15 "strconv"
16 "time"
17)
18
19// BotAPI has methods for interacting with all of Telegram's Bot API endpoints.
20type BotAPI struct {
21 Token string `json:"token"`
22 Debug bool `json:"debug"`
23 Self User `json:"-"`
24 Updates chan Update `json:"-"`
25 Client *http.Client `json:"-"`
26}
27
28// NewBotAPI creates a new BotAPI instance.
29// Requires a token, provided by @BotFather on Telegram
30func NewBotAPI(token string) (*BotAPI, error) {
31 return NewBotAPIWithClient(token, &http.Client{})
32}
33
34// NewBotAPIWithClient creates a new BotAPI instance passing an http.Client.
35// Requires a token, provided by @BotFather on Telegram
36func NewBotAPIWithClient(token string, client *http.Client) (*BotAPI, error) {
37 bot := &BotAPI{
38 Token: token,
39 Client: client,
40 }
41
42 self, err := bot.GetMe()
43 if err != nil {
44 return &BotAPI{}, err
45 }
46
47 bot.Self = self
48
49 return bot, nil
50}
51
52// MakeRequest makes a request to a specific endpoint with our token.
53// All requests are POSTs because Telegram doesn't care, and it's easier.
54func (bot *BotAPI) MakeRequest(endpoint string, params url.Values) (APIResponse, error) {
55 resp, err := bot.Client.PostForm(fmt.Sprintf(APIEndpoint, bot.Token, endpoint), params)
56 if err != nil {
57 return APIResponse{}, err
58 }
59 defer resp.Body.Close()
60
61 if resp.StatusCode == http.StatusForbidden {
62 return APIResponse{}, errors.New(APIForbidden)
63 }
64
65 bytes, err := ioutil.ReadAll(resp.Body)
66 if err != nil {
67 return APIResponse{}, err
68 }
69
70 if bot.Debug {
71 log.Println(endpoint, string(bytes))
72 }
73
74 var apiResp APIResponse
75 json.Unmarshal(bytes, &apiResp)
76
77 if !apiResp.Ok {
78 return APIResponse{}, errors.New(apiResp.Description)
79 }
80
81 return apiResp, nil
82}
83
84// UploadFile makes a request to the API with a file.
85//
86// Requires the parameter to hold the file not be in the params.
87// File should be a string to a file path, a FileBytes struct, or a FileReader struct.
88func (bot *BotAPI) UploadFile(endpoint string, params map[string]string, fieldname string, file interface{}) (APIResponse, error) {
89 ms := multipartstreamer.New()
90 ms.WriteFields(params)
91
92 switch f := file.(type) {
93 case string:
94 fileHandle, err := os.Open(f)
95 if err != nil {
96 return APIResponse{}, err
97 }
98 defer fileHandle.Close()
99
100 fi, err := os.Stat(f)
101 if err != nil {
102 return APIResponse{}, err
103 }
104
105 ms.WriteReader(fieldname, fileHandle.Name(), fi.Size(), fileHandle)
106 case FileBytes:
107 buf := bytes.NewBuffer(f.Bytes)
108 ms.WriteReader(fieldname, f.Name, int64(len(f.Bytes)), buf)
109 case FileReader:
110 if f.Size == -1 {
111 data, err := ioutil.ReadAll(f.Reader)
112 if err != nil {
113 return APIResponse{}, err
114 }
115 buf := bytes.NewBuffer(data)
116
117 ms.WriteReader(fieldname, f.Name, int64(len(data)), buf)
118
119 break
120 }
121
122 ms.WriteReader(fieldname, f.Name, f.Size, f.Reader)
123 default:
124 return APIResponse{}, errors.New("bad file type")
125 }
126
127 req, err := http.NewRequest("POST", fmt.Sprintf(APIEndpoint, bot.Token, endpoint), nil)
128 ms.SetupRequest(req)
129 if err != nil {
130 return APIResponse{}, err
131 }
132
133 res, err := bot.Client.Do(req)
134 if err != nil {
135 return APIResponse{}, err
136 }
137 defer res.Body.Close()
138
139 bytes, err := ioutil.ReadAll(res.Body)
140 if err != nil {
141 return APIResponse{}, err
142 }
143
144 if bot.Debug {
145 log.Println(string(bytes[:]))
146 }
147
148 var apiResp APIResponse
149 json.Unmarshal(bytes, &apiResp)
150
151 if !apiResp.Ok {
152 return APIResponse{}, errors.New(apiResp.Description)
153 }
154
155 return apiResp, nil
156}
157
158// GetMe fetches the currently authenticated bot.
159//
160// There are no parameters for this method.
161func (bot *BotAPI) GetMe() (User, error) {
162 resp, err := bot.MakeRequest("getMe", nil)
163 if err != nil {
164 return User{}, err
165 }
166
167 var user User
168 json.Unmarshal(resp.Result, &user)
169
170 if bot.Debug {
171 log.Printf("getMe: %+v\n", user)
172 }
173
174 return user, nil
175}
176
177func (bot *BotAPI) Send(c Chattable) error {
178 return nil
179}
180
181// SendMessage sends a Message to a chat.
182//
183// Requires ChatID and Text.
184// DisableWebPagePreview, ReplyToMessageID, and ReplyMarkup are optional.
185func (bot *BotAPI) SendMessage(config MessageConfig) (Message, error) {
186 v, err := config.Values()
187
188 if err != nil {
189 return Message{}, err
190 }
191
192 resp, err := bot.MakeRequest("SendMessage", v)
193 if err != nil {
194 return Message{}, err
195 }
196
197 var message Message
198 json.Unmarshal(resp.Result, &message)
199
200 if bot.Debug {
201 log.Printf("SendMessage req : %+v\n", v)
202 log.Printf("SendMessage resp: %+v\n", message)
203 }
204
205 return message, nil
206}
207
208// ForwardMessage forwards a message from one chat to another.
209//
210// Requires ChatID (destination), FromChatID (source), and MessageID.
211func (bot *BotAPI) ForwardMessage(config ForwardConfig) (Message, error) {
212 v, _:= config.Values()
213
214 resp, err := bot.MakeRequest("forwardMessage", v)
215 if err != nil {
216 return Message{}, err
217 }
218
219 var message Message
220 json.Unmarshal(resp.Result, &message)
221
222 if bot.Debug {
223 log.Printf("forwardMessage req : %+v\n", v)
224 log.Printf("forwardMessage resp: %+v\n", message)
225 }
226
227 return message, nil
228}
229
230// SendPhoto sends or uploads a photo to a chat.
231//
232// Requires ChatID and FileID OR File.
233// Caption, ReplyToMessageID, and ReplyMarkup are optional.
234// File should be either a string, FileBytes, or FileReader.
235func (bot *BotAPI) SendPhoto(config PhotoConfig) (Message, error) {
236 if config.UseExistingPhoto {
237 v, err := config.Values()
238
239 if err != nil {
240 return Message{}, err
241 }
242
243 resp, err := bot.MakeRequest("SendPhoto", v)
244 if err != nil {
245 return Message{}, err
246 }
247
248 var message Message
249 json.Unmarshal(resp.Result, &message)
250
251 if bot.Debug {
252 log.Printf("SendPhoto req : %+v\n", v)
253 log.Printf("SendPhoto resp: %+v\n", message)
254 }
255
256 return message, nil
257 }
258
259 params := make(map[string]string)
260 if config.ChannelUsername != "" {
261 params["chat_id"] = config.ChannelUsername
262 } else {
263 params["chat_id"] = strconv.Itoa(config.ChatID)
264 }
265 if config.Caption != "" {
266 params["caption"] = config.Caption
267 }
268 if config.ReplyToMessageID != 0 {
269 params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
270 }
271 if config.ReplyMarkup != nil {
272 data, err := json.Marshal(config.ReplyMarkup)
273 if err != nil {
274 return Message{}, err
275 }
276
277 params["reply_markup"] = string(data)
278 }
279
280 var file interface{}
281 if config.FilePath == "" {
282 file = config.File
283 } else {
284 file = config.FilePath
285 }
286
287 resp, err := bot.UploadFile("SendPhoto", params, "photo", file)
288 if err != nil {
289 return Message{}, err
290 }
291
292 var message Message
293 json.Unmarshal(resp.Result, &message)
294
295 if bot.Debug {
296 log.Printf("SendPhoto resp: %+v\n", message)
297 }
298
299 return message, nil
300}
301
302// SendAudio sends or uploads an audio clip to a chat.
303// If using a file, the file must be in the .mp3 format.
304//
305// When the fields title and performer are both empty and
306// the mime-type of the file to be sent is not audio/mpeg,
307// the file must be an .ogg file encoded with OPUS.
308// You may use the tgutils.EncodeAudio func to assist you with this, if needed.
309//
310// Requires ChatID and FileID OR File.
311// ReplyToMessageID and ReplyMarkup are optional.
312// File should be either a string, FileBytes, or FileReader.
313func (bot *BotAPI) SendAudio(config AudioConfig) (Message, error) {
314 if config.UseExistingAudio {
315 v, err := config.Values()
316 if err != nil {
317 return Message{}, err
318 }
319
320 resp, err := bot.MakeRequest("sendAudio", v)
321 if err != nil {
322 return Message{}, err
323 }
324
325 var message Message
326 json.Unmarshal(resp.Result, &message)
327
328 if bot.Debug {
329 log.Printf("sendAudio req : %+v\n", v)
330 log.Printf("sendAudio resp: %+v\n", message)
331 }
332
333 return message, nil
334 }
335
336 params := make(map[string]string)
337
338 if config.ChannelUsername != "" {
339 params["chat_id"] = config.ChannelUsername
340 } else {
341 params["chat_id"] = strconv.Itoa(config.ChatID)
342 }
343 if config.ReplyToMessageID != 0 {
344 params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
345 }
346 if config.Duration != 0 {
347 params["duration"] = strconv.Itoa(config.Duration)
348 }
349 if config.ReplyMarkup != nil {
350 data, err := json.Marshal(config.ReplyMarkup)
351 if err != nil {
352 return Message{}, err
353 }
354
355 params["reply_markup"] = string(data)
356 }
357 if config.Performer != "" {
358 params["performer"] = config.Performer
359 }
360 if config.Title != "" {
361 params["title"] = config.Title
362 }
363
364 var file interface{}
365 if config.FilePath == "" {
366 file = config.File
367 } else {
368 file = config.FilePath
369 }
370
371 resp, err := bot.UploadFile("sendAudio", params, "audio", file)
372 if err != nil {
373 return Message{}, err
374 }
375
376 var message Message
377 json.Unmarshal(resp.Result, &message)
378
379 if bot.Debug {
380 log.Printf("sendAudio resp: %+v\n", message)
381 }
382
383 return message, nil
384}
385
386// SendDocument sends or uploads a document to a chat.
387//
388// Requires ChatID and FileID OR File.
389// ReplyToMessageID and ReplyMarkup are optional.
390// File should be either a string, FileBytes, or FileReader.
391func (bot *BotAPI) SendDocument(config DocumentConfig) (Message, error) {
392 if config.UseExistingDocument {
393 v, err := config.Values()
394 if err != nil {
395 return Message{}, err
396 }
397
398 resp, err := bot.MakeRequest("sendDocument", v)
399 if err != nil {
400 return Message{}, err
401 }
402
403 var message Message
404 json.Unmarshal(resp.Result, &message)
405
406 if bot.Debug {
407 log.Printf("sendDocument req : %+v\n", v)
408 log.Printf("sendDocument resp: %+v\n", message)
409 }
410
411 return message, nil
412 }
413
414 params := make(map[string]string)
415
416 if config.ChannelUsername != "" {
417 params["chat_id"] = config.ChannelUsername
418 } else {
419 params["chat_id"] = strconv.Itoa(config.ChatID)
420 }
421 if config.ReplyToMessageID != 0 {
422 params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
423 }
424 if config.ReplyMarkup != nil {
425 data, err := json.Marshal(config.ReplyMarkup)
426 if err != nil {
427 return Message{}, err
428 }
429
430 params["reply_markup"] = string(data)
431 }
432
433 var file interface{}
434 if config.FilePath == "" {
435 file = config.File
436 } else {
437 file = config.FilePath
438 }
439
440 resp, err := bot.UploadFile("sendDocument", params, "document", file)
441 if err != nil {
442 return Message{}, err
443 }
444
445 var message Message
446 json.Unmarshal(resp.Result, &message)
447
448 if bot.Debug {
449 log.Printf("sendDocument resp: %+v\n", message)
450 }
451
452 return message, nil
453}
454
455// SendVoice sends or uploads a playable voice to a chat.
456// If using a file, the file must be encoded as an .ogg with OPUS.
457// You may use the tgutils.EncodeAudio func to assist you with this, if needed.
458//
459// Requires ChatID and FileID OR File.
460// ReplyToMessageID and ReplyMarkup are optional.
461// File should be either a string, FileBytes, or FileReader.
462func (bot *BotAPI) SendVoice(config VoiceConfig) (Message, error) {
463 if config.UseExistingVoice {
464 v, err := config.Values()
465 if err != nil {
466 return Message{}, err
467 }
468
469 resp, err := bot.MakeRequest("sendVoice", v)
470 if err != nil {
471 return Message{}, err
472 }
473
474 var message Message
475 json.Unmarshal(resp.Result, &message)
476
477 if bot.Debug {
478 log.Printf("SendVoice req : %+v\n", v)
479 log.Printf("SendVoice resp: %+v\n", message)
480 }
481
482 return message, nil
483 }
484
485 params := make(map[string]string)
486
487 if config.ChannelUsername != "" {
488 params["chat_id"] = config.ChannelUsername
489 } else {
490 params["chat_id"] = strconv.Itoa(config.ChatID)
491 }
492 if config.ReplyToMessageID != 0 {
493 params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
494 }
495 if config.Duration != 0 {
496 params["duration"] = strconv.Itoa(config.Duration)
497 }
498 if config.ReplyMarkup != nil {
499 data, err := json.Marshal(config.ReplyMarkup)
500 if err != nil {
501 return Message{}, err
502 }
503
504 params["reply_markup"] = string(data)
505 }
506
507 var file interface{}
508 if config.FilePath == "" {
509 file = config.File
510 } else {
511 file = config.FilePath
512 }
513
514 resp, err := bot.UploadFile("SendVoice", params, "voice", file)
515 if err != nil {
516 return Message{}, err
517 }
518
519 var message Message
520 json.Unmarshal(resp.Result, &message)
521
522 if bot.Debug {
523 log.Printf("SendVoice resp: %+v\n", message)
524 }
525
526 return message, nil
527}
528
529// SendSticker sends or uploads a sticker to a chat.
530//
531// Requires ChatID and FileID OR File.
532// ReplyToMessageID and ReplyMarkup are optional.
533// File should be either a string, FileBytes, or FileReader.
534func (bot *BotAPI) SendSticker(config StickerConfig) (Message, error) {
535 if config.UseExistingSticker {
536 v, err := config.Values()
537 if err != nil {
538 return Message{}, err
539 }
540
541 resp, err := bot.MakeRequest("sendSticker", v)
542 if err != nil {
543 return Message{}, err
544 }
545
546 var message Message
547 json.Unmarshal(resp.Result, &message)
548
549 if bot.Debug {
550 log.Printf("sendSticker req : %+v\n", v)
551 log.Printf("sendSticker resp: %+v\n", message)
552 }
553
554 return message, nil
555 }
556
557 params := make(map[string]string)
558
559 if config.ChannelUsername != "" {
560 params["chat_id"] = config.ChannelUsername
561 } else {
562 params["chat_id"] = strconv.Itoa(config.ChatID)
563 }
564 if config.ReplyToMessageID != 0 {
565 params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
566 }
567 if config.ReplyMarkup != nil {
568 data, err := json.Marshal(config.ReplyMarkup)
569 if err != nil {
570 return Message{}, err
571 }
572
573 params["reply_markup"] = string(data)
574 }
575
576 var file interface{}
577 if config.FilePath == "" {
578 file = config.File
579 } else {
580 file = config.FilePath
581 }
582
583 resp, err := bot.UploadFile("sendSticker", params, "sticker", file)
584 if err != nil {
585 return Message{}, err
586 }
587
588 var message Message
589 json.Unmarshal(resp.Result, &message)
590
591 if bot.Debug {
592 log.Printf("sendSticker resp: %+v\n", message)
593 }
594
595 return message, nil
596}
597
598// SendVideo sends or uploads a video to a chat.
599//
600// Requires ChatID and FileID OR File.
601// ReplyToMessageID and ReplyMarkup are optional.
602// File should be either a string, FileBytes, or FileReader.
603func (bot *BotAPI) SendVideo(config VideoConfig) (Message, error) {
604 if config.UseExistingVideo {
605 v, err := config.Values()
606 if err != nil {
607 return Message{}, err
608 }
609
610 resp, err := bot.MakeRequest("sendVideo", v)
611 if err != nil {
612 return Message{}, err
613 }
614
615 var message Message
616 json.Unmarshal(resp.Result, &message)
617
618 if bot.Debug {
619 log.Printf("sendVideo req : %+v\n", v)
620 log.Printf("sendVideo resp: %+v\n", message)
621 }
622
623 return message, nil
624 }
625
626 params := make(map[string]string)
627
628 if config.ChannelUsername != "" {
629 params["chat_id"] = config.ChannelUsername
630 } else {
631 params["chat_id"] = strconv.Itoa(config.ChatID)
632 }
633 if config.ReplyToMessageID != 0 {
634 params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
635 }
636 if config.ReplyMarkup != nil {
637 data, err := json.Marshal(config.ReplyMarkup)
638 if err != nil {
639 return Message{}, err
640 }
641
642 params["reply_markup"] = string(data)
643 }
644
645 var file interface{}
646 if config.FilePath == "" {
647 file = config.File
648 } else {
649 file = config.FilePath
650 }
651
652 resp, err := bot.UploadFile("sendVideo", params, "video", file)
653 if err != nil {
654 return Message{}, err
655 }
656
657 var message Message
658 json.Unmarshal(resp.Result, &message)
659
660 if bot.Debug {
661 log.Printf("sendVideo resp: %+v\n", message)
662 }
663
664 return message, nil
665}
666
667// SendLocation sends a location to a chat.
668//
669// Requires ChatID, Latitude, and Longitude.
670// ReplyToMessageID and ReplyMarkup are optional.
671func (bot *BotAPI) SendLocation(config LocationConfig) (Message, error) {
672 v, err := config.Values()
673 if err != nil {
674 return Message{}, err
675 }
676
677 resp, err := bot.MakeRequest("sendLocation", v)
678 if err != nil {
679 return Message{}, err
680 }
681
682 var message Message
683 json.Unmarshal(resp.Result, &message)
684
685 if bot.Debug {
686 log.Printf("sendLocation req : %+v\n", v)
687 log.Printf("sendLocation resp: %+v\n", message)
688 }
689
690 return message, nil
691}
692
693// SendChatAction sets a current action in a chat.
694//
695// Requires ChatID and a valid Action (see Chat constants).
696func (bot *BotAPI) SendChatAction(config ChatActionConfig) error {
697 v, _ := config.Values()
698
699 _, err := bot.MakeRequest("sendChatAction", v)
700 if err != nil {
701 return err
702 }
703
704 return nil
705}
706
707// GetUserProfilePhotos gets a user's profile photos.
708//
709// Requires UserID.
710// Offset and Limit are optional.
711func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
712 v := url.Values{}
713 v.Add("user_id", strconv.Itoa(config.UserID))
714 if config.Offset != 0 {
715 v.Add("offset", strconv.Itoa(config.Offset))
716 }
717 if config.Limit != 0 {
718 v.Add("limit", strconv.Itoa(config.Limit))
719 }
720
721 resp, err := bot.MakeRequest("getUserProfilePhotos", v)
722 if err != nil {
723 return UserProfilePhotos{}, err
724 }
725
726 var profilePhotos UserProfilePhotos
727 json.Unmarshal(resp.Result, &profilePhotos)
728
729 if bot.Debug {
730 log.Printf("getUserProfilePhotos req : %+v\n", v)
731 log.Printf("getUserProfilePhotos resp: %+v\n", profilePhotos)
732 }
733
734 return profilePhotos, nil
735}
736
737// GetFile returns a file_id required to download a file.
738//
739// Requires FileID.
740func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
741 v := url.Values{}
742 v.Add("file_id", config.FileID)
743
744 resp, err := bot.MakeRequest("getFile", v)
745 if err != nil {
746 return File{}, err
747 }
748
749 var file File
750 json.Unmarshal(resp.Result, &file)
751
752 if bot.Debug {
753 log.Printf("getFile req : %+v\n", v)
754 log.Printf("getFile resp: %+v\n", file)
755 }
756
757 return file, nil
758}
759
760// GetUpdates fetches updates.
761// If a WebHook is set, this will not return any data!
762//
763// Offset, Limit, and Timeout are optional.
764// To not get old items, set Offset to one higher than the previous item.
765// Set Timeout to a large number to reduce requests and get responses instantly.
766func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
767 v := url.Values{}
768 if config.Offset > 0 {
769 v.Add("offset", strconv.Itoa(config.Offset))
770 }
771 if config.Limit > 0 {
772 v.Add("limit", strconv.Itoa(config.Limit))
773 }
774 if config.Timeout > 0 {
775 v.Add("timeout", strconv.Itoa(config.Timeout))
776 }
777
778 resp, err := bot.MakeRequest("getUpdates", v)
779 if err != nil {
780 return []Update{}, err
781 }
782
783 var updates []Update
784 json.Unmarshal(resp.Result, &updates)
785
786 if bot.Debug {
787 log.Printf("getUpdates: %+v\n", updates)
788 }
789
790 return updates, nil
791}
792
793// SetWebhook sets a webhook.
794// If this is set, GetUpdates will not get any data!
795//
796// Requires URL OR to set Clear to true.
797func (bot *BotAPI) SetWebhook(config WebhookConfig) (APIResponse, error) {
798 if config.Certificate == nil {
799 v := url.Values{}
800 if !config.Clear {
801 v.Add("url", config.URL.String())
802 }
803
804 return bot.MakeRequest("setWebhook", v)
805 }
806
807 params := make(map[string]string)
808 params["url"] = config.URL.String()
809
810 resp, err := bot.UploadFile("setWebhook", params, "certificate", config.Certificate)
811 if err != nil {
812 return APIResponse{}, err
813 }
814
815 var apiResp APIResponse
816 json.Unmarshal(resp.Result, &apiResp)
817
818 if bot.Debug {
819 log.Printf("setWebhook resp: %+v\n", apiResp)
820 }
821
822 return apiResp, nil
823}
824
825// UpdatesChan starts a channel for getting updates.
826func (bot *BotAPI) UpdatesChan(config UpdateConfig) error {
827 bot.Updates = make(chan Update, 100)
828
829 go func() {
830 for {
831 updates, err := bot.GetUpdates(config)
832 if err != nil {
833 log.Println(err)
834 log.Println("Failed to get updates, retrying in 3 seconds...")
835 time.Sleep(time.Second * 3)
836
837 continue
838 }
839
840 for _, update := range updates {
841 if update.UpdateID >= config.Offset {
842 config.Offset = update.UpdateID + 1
843 bot.Updates <- update
844 }
845 }
846 }
847 }()
848
849 return nil
850}
851
852// ListenForWebhook registers a http handler for a webhook.
853func (bot *BotAPI) ListenForWebhook(pattern string) {
854 bot.Updates = make(chan Update, 100)
855
856 http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
857 bytes, _ := ioutil.ReadAll(r.Body)
858
859 var update Update
860 json.Unmarshal(bytes, &update)
861
862 bot.Updates <- update
863 })
864}