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
193func (bot *BotAPI) DebugLog(context string, v url.Values, message interface{}) {
194 if bot.Debug {
195 log.Printf("%s req : %+v\n", context, v)
196 log.Printf("%s resp: %+v\n", context, message)
197 }
198}
199
200// SendMessage sends a Message to a chat.
201//
202// Requires ChatID and Text.
203// DisableWebPagePreview, ReplyToMessageID, and ReplyMarkup are optional.
204func (bot *BotAPI) SendMessage(config MessageConfig) (Message, error) {
205 v, err := config.Values()
206
207 if err != nil {
208 return Message{}, err
209 }
210
211 message, err := bot.MakeMessageRequest("SendMessage", v)
212
213 if err != nil {
214 return Message{}, err
215 }
216
217 bot.DebugLog("SendMessage", v, message)
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 bot.DebugLog("ForwardMessage", v, message)
234
235 return message, nil
236}
237
238// SendPhoto sends or uploads a photo to a chat.
239//
240// Requires ChatID and FileID OR File.
241// Caption, ReplyToMessageID, and ReplyMarkup are optional.
242// File should be either a string, FileBytes, or FileReader.
243func (bot *BotAPI) SendPhoto(config PhotoConfig) (Message, error) {
244 if config.UseExistingPhoto {
245 v, err := config.Values()
246
247 if err != nil {
248 return Message{}, err
249 }
250
251 message, err := bot.MakeMessageRequest("SendPhoto", v)
252 if err != nil {
253 return Message{}, err
254 }
255
256 bot.DebugLog("SendPhoto", v, message)
257
258 return message, nil
259 }
260
261 params := make(map[string]string)
262 if config.ChannelUsername != "" {
263 params["chat_id"] = config.ChannelUsername
264 } else {
265 params["chat_id"] = strconv.Itoa(config.ChatID)
266 }
267 if config.Caption != "" {
268 params["caption"] = config.Caption
269 }
270 if config.ReplyToMessageID != 0 {
271 params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
272 }
273 if config.ReplyMarkup != nil {
274 data, err := json.Marshal(config.ReplyMarkup)
275 if err != nil {
276 return Message{}, err
277 }
278
279 params["reply_markup"] = string(data)
280 }
281
282 var file interface{}
283 if config.FilePath == "" {
284 file = config.File
285 } else {
286 file = config.FilePath
287 }
288
289 resp, err := bot.UploadFile("SendPhoto", params, "photo", file)
290 if err != nil {
291 return Message{}, err
292 }
293
294 var message Message
295 json.Unmarshal(resp.Result, &message)
296
297 if bot.Debug {
298 log.Printf("SendPhoto resp: %+v\n", message)
299 }
300
301 return message, nil
302}
303
304// SendAudio sends or uploads an audio clip to a chat.
305// If using a file, the file must be in the .mp3 format.
306//
307// When the fields title and performer are both empty and
308// the mime-type of the file to be sent is not audio/mpeg,
309// the file must be an .ogg file encoded with OPUS.
310// You may use the tgutils.EncodeAudio func to assist you with this, if needed.
311//
312// Requires ChatID and FileID OR File.
313// ReplyToMessageID and ReplyMarkup are optional.
314// File should be either a string, FileBytes, or FileReader.
315func (bot *BotAPI) SendAudio(config AudioConfig) (Message, error) {
316 if config.UseExistingAudio {
317 v, err := config.Values()
318 if err != nil {
319 return Message{}, err
320 }
321
322 message, err := bot.MakeMessageRequest("sendAudio", v)
323 if err != nil {
324 return Message{}, err
325 }
326
327
328 bot.DebugLog("SendAudio", v, message)
329
330 return message, nil
331 }
332
333 params := make(map[string]string)
334
335 if config.ChannelUsername != "" {
336 params["chat_id"] = config.ChannelUsername
337 } else {
338 params["chat_id"] = strconv.Itoa(config.ChatID)
339 }
340 if config.ReplyToMessageID != 0 {
341 params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
342 }
343 if config.Duration != 0 {
344 params["duration"] = strconv.Itoa(config.Duration)
345 }
346 if config.ReplyMarkup != nil {
347 data, err := json.Marshal(config.ReplyMarkup)
348 if err != nil {
349 return Message{}, err
350 }
351
352 params["reply_markup"] = string(data)
353 }
354 if config.Performer != "" {
355 params["performer"] = config.Performer
356 }
357 if config.Title != "" {
358 params["title"] = config.Title
359 }
360
361 var file interface{}
362 if config.FilePath == "" {
363 file = config.File
364 } else {
365 file = config.FilePath
366 }
367
368 resp, err := bot.UploadFile("sendAudio", params, "audio", file)
369 if err != nil {
370 return Message{}, err
371 }
372
373 var message Message
374 json.Unmarshal(resp.Result, &message)
375
376 if bot.Debug {
377 log.Printf("sendAudio resp: %+v\n", message)
378 }
379
380 return message, nil
381}
382
383// SendDocument sends or uploads a document to a chat.
384//
385// Requires ChatID and FileID OR File.
386// ReplyToMessageID and ReplyMarkup are optional.
387// File should be either a string, FileBytes, or FileReader.
388func (bot *BotAPI) SendDocument(config DocumentConfig) (Message, error) {
389 if config.UseExistingDocument {
390 v, err := config.Values()
391 if err != nil {
392 return Message{}, err
393 }
394
395 message, err := bot.MakeMessageRequest("sendDocument", v)
396 if err != nil {
397 return Message{}, err
398 }
399
400 bot.DebugLog("SendDocument", v, message)
401
402 return message, nil
403 }
404
405 params := make(map[string]string)
406
407 if config.ChannelUsername != "" {
408 params["chat_id"] = config.ChannelUsername
409 } else {
410 params["chat_id"] = strconv.Itoa(config.ChatID)
411 }
412 if config.ReplyToMessageID != 0 {
413 params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
414 }
415 if config.ReplyMarkup != nil {
416 data, err := json.Marshal(config.ReplyMarkup)
417 if err != nil {
418 return Message{}, err
419 }
420
421 params["reply_markup"] = string(data)
422 }
423
424 var file interface{}
425 if config.FilePath == "" {
426 file = config.File
427 } else {
428 file = config.FilePath
429 }
430
431 resp, err := bot.UploadFile("sendDocument", params, "document", file)
432 if err != nil {
433 return Message{}, err
434 }
435
436 var message Message
437 json.Unmarshal(resp.Result, &message)
438
439 if bot.Debug {
440 log.Printf("sendDocument resp: %+v\n", message)
441 }
442
443 return message, nil
444}
445
446// SendVoice sends or uploads a playable voice to a chat.
447// If using a file, the file must be encoded as an .ogg with OPUS.
448// You may use the tgutils.EncodeAudio func to assist you with this, if needed.
449//
450// Requires ChatID and FileID OR File.
451// ReplyToMessageID and ReplyMarkup are optional.
452// File should be either a string, FileBytes, or FileReader.
453func (bot *BotAPI) SendVoice(config VoiceConfig) (Message, error) {
454 if config.UseExistingVoice {
455 v, err := config.Values()
456 if err != nil {
457 return Message{}, err
458 }
459
460 message, err := bot.MakeMessageRequest("sendVoice", v)
461 if err != nil {
462 return Message{}, err
463 }
464
465 bot.DebugLog("SendVoice", v, message)
466
467 return message, nil
468 }
469
470 params := make(map[string]string)
471
472 if config.ChannelUsername != "" {
473 params["chat_id"] = config.ChannelUsername
474 } else {
475 params["chat_id"] = strconv.Itoa(config.ChatID)
476 }
477 if config.ReplyToMessageID != 0 {
478 params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
479 }
480 if config.Duration != 0 {
481 params["duration"] = strconv.Itoa(config.Duration)
482 }
483 if config.ReplyMarkup != nil {
484 data, err := json.Marshal(config.ReplyMarkup)
485 if err != nil {
486 return Message{}, err
487 }
488
489 params["reply_markup"] = string(data)
490 }
491
492 var file interface{}
493 if config.FilePath == "" {
494 file = config.File
495 } else {
496 file = config.FilePath
497 }
498
499 resp, err := bot.UploadFile("SendVoice", params, "voice", file)
500 if err != nil {
501 return Message{}, err
502 }
503
504 var message Message
505 json.Unmarshal(resp.Result, &message)
506
507 if bot.Debug {
508 log.Printf("SendVoice resp: %+v\n", message)
509 }
510
511 return message, nil
512}
513
514// SendSticker sends or uploads a sticker to a chat.
515//
516// Requires ChatID and FileID OR File.
517// ReplyToMessageID and ReplyMarkup are optional.
518// File should be either a string, FileBytes, or FileReader.
519func (bot *BotAPI) SendSticker(config StickerConfig) (Message, error) {
520 if config.UseExistingSticker {
521 v, err := config.Values()
522 if err != nil {
523 return Message{}, err
524 }
525
526 message, err := bot.MakeMessageRequest("sendSticker", v)
527 if err != nil {
528 return Message{}, err
529 }
530
531 bot.DebugLog("SendSticker", v, message)
532
533 return message, nil
534 }
535
536 params := make(map[string]string)
537
538 if config.ChannelUsername != "" {
539 params["chat_id"] = config.ChannelUsername
540 } else {
541 params["chat_id"] = strconv.Itoa(config.ChatID)
542 }
543 if config.ReplyToMessageID != 0 {
544 params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
545 }
546 if config.ReplyMarkup != nil {
547 data, err := json.Marshal(config.ReplyMarkup)
548 if err != nil {
549 return Message{}, err
550 }
551
552 params["reply_markup"] = string(data)
553 }
554
555 var file interface{}
556 if config.FilePath == "" {
557 file = config.File
558 } else {
559 file = config.FilePath
560 }
561
562 resp, err := bot.UploadFile("sendSticker", params, "sticker", file)
563 if err != nil {
564 return Message{}, err
565 }
566
567 var message Message
568 json.Unmarshal(resp.Result, &message)
569
570 if bot.Debug {
571 log.Printf("sendSticker resp: %+v\n", message)
572 }
573
574 return message, nil
575}
576
577// SendVideo sends or uploads a video to a chat.
578//
579// Requires ChatID and FileID OR File.
580// ReplyToMessageID and ReplyMarkup are optional.
581// File should be either a string, FileBytes, or FileReader.
582func (bot *BotAPI) SendVideo(config VideoConfig) (Message, error) {
583 if config.UseExistingVideo {
584 v, err := config.Values()
585 if err != nil {
586 return Message{}, err
587 }
588
589 message, err := bot.MakeMessageRequest("sendVideo", v)
590 if err != nil {
591 return Message{}, err
592 }
593
594 bot.DebugLog("SendVideo", v, message)
595
596 return message, nil
597 }
598
599 params := make(map[string]string)
600
601 if config.ChannelUsername != "" {
602 params["chat_id"] = config.ChannelUsername
603 } else {
604 params["chat_id"] = strconv.Itoa(config.ChatID)
605 }
606 if config.ReplyToMessageID != 0 {
607 params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
608 }
609 if config.ReplyMarkup != nil {
610 data, err := json.Marshal(config.ReplyMarkup)
611 if err != nil {
612 return Message{}, err
613 }
614
615 params["reply_markup"] = string(data)
616 }
617
618 var file interface{}
619 if config.FilePath == "" {
620 file = config.File
621 } else {
622 file = config.FilePath
623 }
624
625 resp, err := bot.UploadFile("sendVideo", params, "video", file)
626 if err != nil {
627 return Message{}, err
628 }
629
630 var message Message
631 json.Unmarshal(resp.Result, &message)
632
633 if bot.Debug {
634 log.Printf("sendVideo resp: %+v\n", message)
635 }
636
637 return message, nil
638}
639
640// SendLocation sends a location to a chat.
641//
642// Requires ChatID, Latitude, and Longitude.
643// ReplyToMessageID and ReplyMarkup are optional.
644func (bot *BotAPI) SendLocation(config LocationConfig) (Message, error) {
645 v, err := config.Values()
646 if err != nil {
647 return Message{}, err
648 }
649
650 message, err := bot.MakeMessageRequest("sendLocation", v)
651 if err != nil {
652 return Message{}, err
653 }
654
655 bot.DebugLog("SendLocation", v, message)
656
657 return message, nil
658}
659
660// SendChatAction sets a current action in a chat.
661//
662// Requires ChatID and a valid Action (see Chat constants).
663func (bot *BotAPI) SendChatAction(config ChatActionConfig) error {
664 v, _ := config.Values()
665
666 _, err := bot.MakeRequest("sendChatAction", v)
667 if err != nil {
668 return err
669 }
670
671 return nil
672}
673
674// GetUserProfilePhotos gets a user's profile photos.
675//
676// Requires UserID.
677// Offset and Limit are optional.
678func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
679 v := url.Values{}
680 v.Add("user_id", strconv.Itoa(config.UserID))
681 if config.Offset != 0 {
682 v.Add("offset", strconv.Itoa(config.Offset))
683 }
684 if config.Limit != 0 {
685 v.Add("limit", strconv.Itoa(config.Limit))
686 }
687
688 resp, err := bot.MakeRequest("getUserProfilePhotos", v)
689 if err != nil {
690 return UserProfilePhotos{}, err
691 }
692
693 var profilePhotos UserProfilePhotos
694 json.Unmarshal(resp.Result, &profilePhotos)
695
696 bot.DebugLog("GetUserProfilePhoto", v, profilePhotos)
697
698 return profilePhotos, nil
699}
700
701// GetFile returns a file_id required to download a file.
702//
703// Requires FileID.
704func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
705 v := url.Values{}
706 v.Add("file_id", config.FileID)
707
708 resp, err := bot.MakeRequest("getFile", v)
709 if err != nil {
710 return File{}, err
711 }
712
713 var file File
714 json.Unmarshal(resp.Result, &file)
715
716 bot.DebugLog("GetFile", v, file)
717
718 return file, nil
719}
720
721// GetUpdates fetches updates.
722// If a WebHook is set, this will not return any data!
723//
724// Offset, Limit, and Timeout are optional.
725// To not get old items, set Offset to one higher than the previous item.
726// Set Timeout to a large number to reduce requests and get responses instantly.
727func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
728 v := url.Values{}
729 if config.Offset > 0 {
730 v.Add("offset", strconv.Itoa(config.Offset))
731 }
732 if config.Limit > 0 {
733 v.Add("limit", strconv.Itoa(config.Limit))
734 }
735 if config.Timeout > 0 {
736 v.Add("timeout", strconv.Itoa(config.Timeout))
737 }
738
739 resp, err := bot.MakeRequest("getUpdates", v)
740 if err != nil {
741 return []Update{}, err
742 }
743
744 var updates []Update
745 json.Unmarshal(resp.Result, &updates)
746
747 if bot.Debug {
748 log.Printf("getUpdates: %+v\n", updates)
749 }
750
751 return updates, nil
752}
753
754// SetWebhook sets a webhook.
755// If this is set, GetUpdates will not get any data!
756//
757// Requires URL OR to set Clear to true.
758func (bot *BotAPI) SetWebhook(config WebhookConfig) (APIResponse, error) {
759 if config.Certificate == nil {
760 v := url.Values{}
761 if !config.Clear {
762 v.Add("url", config.URL.String())
763 }
764
765 return bot.MakeRequest("setWebhook", v)
766 }
767
768 params := make(map[string]string)
769 params["url"] = config.URL.String()
770
771 resp, err := bot.UploadFile("setWebhook", params, "certificate", config.Certificate)
772 if err != nil {
773 return APIResponse{}, err
774 }
775
776 var apiResp APIResponse
777 json.Unmarshal(resp.Result, &apiResp)
778
779 if bot.Debug {
780 log.Printf("setWebhook resp: %+v\n", apiResp)
781 }
782
783 return apiResp, nil
784}
785
786// UpdatesChan starts a channel for getting updates.
787func (bot *BotAPI) UpdatesChan(config UpdateConfig) error {
788 bot.Updates = make(chan Update, 100)
789
790 go func() {
791 for {
792 updates, err := bot.GetUpdates(config)
793 if err != nil {
794 log.Println(err)
795 log.Println("Failed to get updates, retrying in 3 seconds...")
796 time.Sleep(time.Second * 3)
797
798 continue
799 }
800
801 for _, update := range updates {
802 if update.UpdateID >= config.Offset {
803 config.Offset = update.UpdateID + 1
804 bot.Updates <- update
805 }
806 }
807 }
808 }()
809
810 return nil
811}
812
813// ListenForWebhook registers a http handler for a webhook.
814func (bot *BotAPI) ListenForWebhook(pattern string) {
815 bot.Updates = make(chan Update, 100)
816
817 http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
818 bytes, _ := ioutil.ReadAll(r.Body)
819
820 var update Update
821 json.Unmarshal(bytes, &update)
822
823 bot.Updates <- update
824 })
825}