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