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