methods.go (view raw)
1package tgbotapi
2
3import (
4 "bytes"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io"
9 "io/ioutil"
10 "log"
11 "mime/multipart"
12 "net/http"
13 "net/url"
14 "os"
15 "strconv"
16)
17
18// Telegram constants
19const (
20 // APIEndpoint is the endpoint for all API methods, with formatting for Sprintf
21 APIEndpoint = "https://api.telegram.org/bot%s/%s"
22)
23
24// Constant values for ChatActions
25const (
26 ChatTyping = "typing"
27 ChatUploadPhoto = "upload_photo"
28 ChatRecordVideo = "record_video"
29 ChatUploadVideo = "upload_video"
30 ChatRecordAudio = "record_audio"
31 ChatUploadAudio = "upload_audio"
32 ChatUploadDocument = "upload_document"
33 ChatFindLocation = "find_location"
34)
35
36// API errors
37const (
38 // APIForbidden happens when a token is bad
39 APIForbidden = "forbidden"
40)
41
42// MessageConfig contains information about a SendMessage request.
43type MessageConfig struct {
44 ChatID int
45 Text string
46 DisableWebPagePreview bool
47 ReplyToMessageID int
48 ReplyMarkup interface{}
49}
50
51// ForwardConfig contains information about a ForwardMessage request.
52type ForwardConfig struct {
53 ChatID int
54 FromChatID int
55 MessageID int
56}
57
58// PhotoConfig contains information about a SendPhoto request.
59type PhotoConfig struct {
60 ChatID int
61 Caption string
62 ReplyToMessageID int
63 ReplyMarkup interface{}
64 UseExistingPhoto bool
65 FilePath string
66 FileID string
67}
68
69// AudioConfig contains information about a SendAudio request.
70type AudioConfig struct {
71 ChatID int
72 ReplyToMessageID int
73 ReplyMarkup interface{}
74 UseExistingAudio bool
75 FilePath string
76 FileID string
77}
78
79// DocumentConfig contains information about a SendDocument request.
80type DocumentConfig struct {
81 ChatID int
82 ReplyToMessageID int
83 ReplyMarkup interface{}
84 UseExistingDocument bool
85 FilePath string
86 FileID string
87}
88
89// StickerConfig contains information about a SendSticker request.
90type StickerConfig struct {
91 ChatID int
92 ReplyToMessageID int
93 ReplyMarkup interface{}
94 UseExistingSticker bool
95 FilePath string
96 FileID string
97}
98
99// VideoConfig contains information about a SendVideo request.
100type VideoConfig struct {
101 ChatID int
102 ReplyToMessageID int
103 ReplyMarkup interface{}
104 UseExistingVideo bool
105 FilePath string
106 FileID string
107}
108
109// LocationConfig contains information about a SendLocation request.
110type LocationConfig struct {
111 ChatID int
112 Latitude float64
113 Longitude float64
114 ReplyToMessageID int
115 ReplyMarkup interface{}
116}
117
118// ChatActionConfig contains information about a SendChatAction request.
119type ChatActionConfig struct {
120 ChatID int
121 Action string
122}
123
124// UserProfilePhotosConfig contains information about a GetUserProfilePhotos request.
125type UserProfilePhotosConfig struct {
126 UserID int
127 Offset int
128 Limit int
129}
130
131// UpdateConfig contains information about a GetUpdates request.
132type UpdateConfig struct {
133 Offset int
134 Limit int
135 Timeout int
136}
137
138// WebhookConfig contains information about a SetWebhook request.
139type WebhookConfig struct {
140 Clear bool
141 URL *url.URL
142}
143
144// MakeRequest makes a request to a specific endpoint with our token.
145// All requests are POSTs because Telegram doesn't care, and it's easier.
146func (bot *BotAPI) MakeRequest(endpoint string, params url.Values) (APIResponse, error) {
147 resp, err := bot.Client.PostForm(fmt.Sprintf(APIEndpoint, bot.Token, endpoint), params)
148 if err != nil {
149 return APIResponse{}, err
150 }
151 defer resp.Body.Close()
152
153 if resp.StatusCode == http.StatusForbidden {
154 return APIResponse{}, errors.New(APIForbidden)
155 }
156
157 bytes, err := ioutil.ReadAll(resp.Body)
158 if err != nil {
159 return APIResponse{}, err
160 }
161
162 if bot.Debug {
163 log.Println(endpoint, string(bytes))
164 }
165
166 var apiResp APIResponse
167 json.Unmarshal(bytes, &apiResp)
168
169 if !apiResp.Ok {
170 return APIResponse{}, errors.New(apiResp.Description)
171 }
172
173 return apiResp, nil
174}
175
176// UploadFile makes a request to the API with a file.
177//
178// Requires the parameter to hold the file not be in the params.
179func (bot *BotAPI) UploadFile(endpoint string, params map[string]string, fieldname string, filename string) (APIResponse, error) {
180 var b bytes.Buffer
181 w := multipart.NewWriter(&b)
182
183 f, err := os.Open(filename)
184 if err != nil {
185 return APIResponse{}, err
186 }
187
188 fw, err := w.CreateFormFile(fieldname, filename)
189 if err != nil {
190 return APIResponse{}, err
191 }
192
193 if _, err = io.Copy(fw, f); err != nil {
194 return APIResponse{}, err
195 }
196
197 for key, val := range params {
198 if fw, err = w.CreateFormField(key); err != nil {
199 return APIResponse{}, err
200 }
201
202 if _, err = fw.Write([]byte(val)); err != nil {
203 return APIResponse{}, err
204 }
205 }
206
207 w.Close()
208
209 req, err := http.NewRequest("POST", fmt.Sprintf(APIEndpoint, bot.Token, endpoint), &b)
210 if err != nil {
211 return APIResponse{}, err
212 }
213
214 req.Header.Set("Content-Type", w.FormDataContentType())
215
216 res, err := bot.Client.Do(req)
217 if err != nil {
218 return APIResponse{}, err
219 }
220
221 bytes, err := ioutil.ReadAll(res.Body)
222 if err != nil {
223 return APIResponse{}, err
224 }
225
226 if bot.Debug {
227 log.Println(string(bytes[:]))
228 }
229
230 var apiResp APIResponse
231 json.Unmarshal(bytes, &apiResp)
232
233 return apiResp, nil
234}
235
236// GetMe fetches the currently authenticated bot.
237//
238// There are no parameters for this method.
239func (bot *BotAPI) GetMe() (User, error) {
240 resp, err := bot.MakeRequest("getMe", nil)
241 if err != nil {
242 return User{}, err
243 }
244
245 var user User
246 json.Unmarshal(resp.Result, &user)
247
248 if bot.Debug {
249 log.Printf("getMe: %+v\n", user)
250 }
251
252 return user, nil
253}
254
255// SendMessage sends a Message to a chat.
256//
257// Requires ChatID and Text.
258// DisableWebPagePreview, ReplyToMessageID, and ReplyMarkup are optional.
259func (bot *BotAPI) SendMessage(config MessageConfig) (Message, error) {
260 v := url.Values{}
261 v.Add("chat_id", strconv.Itoa(config.ChatID))
262 v.Add("text", config.Text)
263 v.Add("disable_web_page_preview", strconv.FormatBool(config.DisableWebPagePreview))
264 if config.ReplyToMessageID != 0 {
265 v.Add("reply_to_message_id", strconv.Itoa(config.ReplyToMessageID))
266 }
267 if config.ReplyMarkup != nil {
268 data, err := json.Marshal(config.ReplyMarkup)
269 if err != nil {
270 return Message{}, err
271 }
272
273 v.Add("reply_markup", string(data))
274 }
275
276 resp, err := bot.MakeRequest("SendMessage", v)
277 if err != nil {
278 return Message{}, err
279 }
280
281 var message Message
282 json.Unmarshal(resp.Result, &message)
283
284 if bot.Debug {
285 log.Printf("SendMessage req : %+v\n", v)
286 log.Printf("SendMessage resp: %+v\n", message)
287 }
288
289 return message, nil
290}
291
292// ForwardMessage forwards a message from one chat to another.
293//
294// Requires ChatID (destination), FromChatID (source), and MessageID.
295func (bot *BotAPI) ForwardMessage(config ForwardConfig) (Message, error) {
296 v := url.Values{}
297 v.Add("chat_id", strconv.Itoa(config.ChatID))
298 v.Add("from_chat_id", strconv.Itoa(config.FromChatID))
299 v.Add("message_id", strconv.Itoa(config.MessageID))
300
301 resp, err := bot.MakeRequest("forwardMessage", v)
302 if err != nil {
303 return Message{}, err
304 }
305
306 var message Message
307 json.Unmarshal(resp.Result, &message)
308
309 if bot.Debug {
310 log.Printf("forwardMessage req : %+v\n", v)
311 log.Printf("forwardMessage resp: %+v\n", message)
312 }
313
314 return message, nil
315}
316
317// SendPhoto sends or uploads a photo to a chat.
318//
319// Requires ChatID and FileID OR FilePath.
320// Caption, ReplyToMessageID, and ReplyMarkup are optional.
321func (bot *BotAPI) SendPhoto(config PhotoConfig) (Message, error) {
322 if config.UseExistingPhoto {
323 v := url.Values{}
324 v.Add("chat_id", strconv.Itoa(config.ChatID))
325 v.Add("photo", config.FileID)
326 if config.Caption != "" {
327 v.Add("caption", config.Caption)
328 }
329 if config.ReplyToMessageID != 0 {
330 v.Add("reply_to_message_id", strconv.Itoa(config.ChatID))
331 }
332 if config.ReplyMarkup != nil {
333 data, err := json.Marshal(config.ReplyMarkup)
334 if err != nil {
335 return Message{}, err
336 }
337
338 v.Add("reply_markup", string(data))
339 }
340
341 resp, err := bot.MakeRequest("SendPhoto", v)
342 if err != nil {
343 return Message{}, err
344 }
345
346 var message Message
347 json.Unmarshal(resp.Result, &message)
348
349 if bot.Debug {
350 log.Printf("SendPhoto req : %+v\n", v)
351 log.Printf("SendPhoto resp: %+v\n", message)
352 }
353
354 return message, nil
355 }
356
357 params := make(map[string]string)
358 params["chat_id"] = strconv.Itoa(config.ChatID)
359 if config.Caption != "" {
360 params["caption"] = config.Caption
361 }
362 if config.ReplyToMessageID != 0 {
363 params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
364 }
365 if config.ReplyMarkup != nil {
366 data, err := json.Marshal(config.ReplyMarkup)
367 if err != nil {
368 return Message{}, err
369 }
370
371 params["reply_markup"] = string(data)
372 }
373
374 resp, err := bot.UploadFile("SendPhoto", params, "photo", config.FilePath)
375 if err != nil {
376 return Message{}, err
377 }
378
379 var message Message
380 json.Unmarshal(resp.Result, &message)
381
382 if bot.Debug {
383 log.Printf("SendPhoto resp: %+v\n", message)
384 }
385
386 return message, nil
387}
388
389// SendAudio sends or uploads an audio clip to a chat.
390// If using a file, the file must be encoded as an .ogg with OPUS.
391//
392// Requires ChatID and FileID OR FilePath.
393// ReplyToMessageID and ReplyMarkup are optional.
394func (bot *BotAPI) SendAudio(config AudioConfig) (Message, error) {
395 if config.UseExistingAudio {
396 v := url.Values{}
397 v.Add("chat_id", strconv.Itoa(config.ChatID))
398 v.Add("audio", config.FileID)
399 if config.ReplyToMessageID != 0 {
400 v.Add("reply_to_message_id", strconv.Itoa(config.ReplyToMessageID))
401 }
402 if config.ReplyMarkup != nil {
403 data, err := json.Marshal(config.ReplyMarkup)
404 if err != nil {
405 return Message{}, err
406 }
407
408 v.Add("reply_markup", string(data))
409 }
410
411 resp, err := bot.MakeRequest("sendAudio", v)
412 if err != nil {
413 return Message{}, err
414 }
415
416 var message Message
417 json.Unmarshal(resp.Result, &message)
418
419 if bot.Debug {
420 log.Printf("sendAudio req : %+v\n", v)
421 log.Printf("sendAudio resp: %+v\n", message)
422 }
423
424 return message, nil
425 }
426
427 params := make(map[string]string)
428
429 params["chat_id"] = strconv.Itoa(config.ChatID)
430 if config.ReplyToMessageID != 0 {
431 params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
432 }
433 if config.ReplyMarkup != nil {
434 data, err := json.Marshal(config.ReplyMarkup)
435 if err != nil {
436 return Message{}, err
437 }
438
439 params["reply_markup"] = string(data)
440 }
441
442 resp, err := bot.UploadFile("sendAudio", params, "audio", config.FilePath)
443 if err != nil {
444 return Message{}, err
445 }
446
447 var message Message
448 json.Unmarshal(resp.Result, &message)
449
450 if bot.Debug {
451 log.Printf("sendAudio resp: %+v\n", message)
452 }
453
454 return message, nil
455}
456
457// SendDocument sends or uploads a document to a chat.
458//
459// Requires ChatID and FileID OR FilePath.
460// ReplyToMessageID and ReplyMarkup are optional.
461func (bot *BotAPI) SendDocument(config DocumentConfig) (Message, error) {
462 if config.UseExistingDocument {
463 v := url.Values{}
464 v.Add("chat_id", strconv.Itoa(config.ChatID))
465 v.Add("document", config.FileID)
466 if config.ReplyToMessageID != 0 {
467 v.Add("reply_to_message_id", strconv.Itoa(config.ReplyToMessageID))
468 }
469 if config.ReplyMarkup != nil {
470 data, err := json.Marshal(config.ReplyMarkup)
471 if err != nil {
472 return Message{}, err
473 }
474
475 v.Add("reply_markup", string(data))
476 }
477
478 resp, err := bot.MakeRequest("sendDocument", v)
479 if err != nil {
480 return Message{}, err
481 }
482
483 var message Message
484 json.Unmarshal(resp.Result, &message)
485
486 if bot.Debug {
487 log.Printf("sendDocument req : %+v\n", v)
488 log.Printf("sendDocument resp: %+v\n", message)
489 }
490
491 return message, nil
492 }
493
494 params := make(map[string]string)
495
496 params["chat_id"] = strconv.Itoa(config.ChatID)
497 if config.ReplyToMessageID != 0 {
498 params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
499 }
500 if config.ReplyMarkup != nil {
501 data, err := json.Marshal(config.ReplyMarkup)
502 if err != nil {
503 return Message{}, err
504 }
505
506 params["reply_markup"] = string(data)
507 }
508
509 resp, err := bot.UploadFile("sendDocument", params, "document", config.FilePath)
510 if err != nil {
511 return Message{}, err
512 }
513
514 var message Message
515 json.Unmarshal(resp.Result, &message)
516
517 if bot.Debug {
518 log.Printf("sendDocument resp: %+v\n", message)
519 }
520
521 return message, nil
522}
523
524// SendSticker sends or uploads a sticker to a chat.
525//
526// Requires ChatID and FileID OR FilePath.
527// ReplyToMessageID and ReplyMarkup are optional.
528func (bot *BotAPI) SendSticker(config StickerConfig) (Message, error) {
529 if config.UseExistingSticker {
530 v := url.Values{}
531 v.Add("chat_id", strconv.Itoa(config.ChatID))
532 v.Add("sticker", config.FileID)
533 if config.ReplyToMessageID != 0 {
534 v.Add("reply_to_message_id", strconv.Itoa(config.ReplyToMessageID))
535 }
536 if config.ReplyMarkup != nil {
537 data, err := json.Marshal(config.ReplyMarkup)
538 if err != nil {
539 return Message{}, err
540 }
541
542 v.Add("reply_markup", string(data))
543 }
544
545 resp, err := bot.MakeRequest("sendSticker", v)
546 if err != nil {
547 return Message{}, err
548 }
549
550 var message Message
551 json.Unmarshal(resp.Result, &message)
552
553 if bot.Debug {
554 log.Printf("sendSticker req : %+v\n", v)
555 log.Printf("sendSticker resp: %+v\n", message)
556 }
557
558 return message, nil
559 }
560
561 params := make(map[string]string)
562
563 params["chat_id"] = strconv.Itoa(config.ChatID)
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 resp, err := bot.UploadFile("sendSticker", params, "sticker", config.FilePath)
577 if err != nil {
578 return Message{}, err
579 }
580
581 var message Message
582 json.Unmarshal(resp.Result, &message)
583
584 if bot.Debug {
585 log.Printf("sendSticker resp: %+v\n", message)
586 }
587
588 return message, nil
589}
590
591// SendVideo sends or uploads a video to a chat.
592//
593// Requires ChatID and FileID OR FilePath.
594// ReplyToMessageID and ReplyMarkup are optional.
595func (bot *BotAPI) SendVideo(config VideoConfig) (Message, error) {
596 if config.UseExistingVideo {
597 v := url.Values{}
598 v.Add("chat_id", strconv.Itoa(config.ChatID))
599 v.Add("video", config.FileID)
600 if config.ReplyToMessageID != 0 {
601 v.Add("reply_to_message_id", strconv.Itoa(config.ReplyToMessageID))
602 }
603 if config.ReplyMarkup != nil {
604 data, err := json.Marshal(config.ReplyMarkup)
605 if err != nil {
606 return Message{}, err
607 }
608
609 v.Add("reply_markup", string(data))
610 }
611
612 resp, err := bot.MakeRequest("sendVideo", v)
613 if err != nil {
614 return Message{}, err
615 }
616
617 var message Message
618 json.Unmarshal(resp.Result, &message)
619
620 if bot.Debug {
621 log.Printf("sendVideo req : %+v\n", v)
622 log.Printf("sendVideo resp: %+v\n", message)
623 }
624
625 return message, nil
626 }
627
628 params := make(map[string]string)
629
630 params["chat_id"] = strconv.Itoa(config.ChatID)
631 if config.ReplyToMessageID != 0 {
632 params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
633 }
634 if config.ReplyMarkup != nil {
635 data, err := json.Marshal(config.ReplyMarkup)
636 if err != nil {
637 return Message{}, err
638 }
639
640 params["reply_markup"] = string(data)
641 }
642
643 resp, err := bot.UploadFile("sendVideo", params, "video", config.FilePath)
644 if err != nil {
645 return Message{}, err
646 }
647
648 var message Message
649 json.Unmarshal(resp.Result, &message)
650
651 if bot.Debug {
652 log.Printf("sendVideo resp: %+v\n", message)
653 }
654
655 return message, nil
656}
657
658// SendLocation sends a location to a chat.
659//
660// Requires ChatID, Latitude, and Longitude.
661// ReplyToMessageID and ReplyMarkup are optional.
662func (bot *BotAPI) SendLocation(config LocationConfig) (Message, error) {
663 v := url.Values{}
664 v.Add("chat_id", strconv.Itoa(config.ChatID))
665 v.Add("latitude", strconv.FormatFloat(config.Latitude, 'f', 6, 64))
666 v.Add("longitude", strconv.FormatFloat(config.Longitude, 'f', 6, 64))
667 if config.ReplyToMessageID != 0 {
668 v.Add("reply_to_message_id", strconv.Itoa(config.ReplyToMessageID))
669 }
670 if config.ReplyMarkup != nil {
671 data, err := json.Marshal(config.ReplyMarkup)
672 if err != nil {
673 return Message{}, err
674 }
675
676 v.Add("reply_markup", string(data))
677 }
678
679 resp, err := bot.MakeRequest("sendLocation", v)
680 if err != nil {
681 return Message{}, err
682 }
683
684 var message Message
685 json.Unmarshal(resp.Result, &message)
686
687 if bot.Debug {
688 log.Printf("sendLocation req : %+v\n", v)
689 log.Printf("sendLocation resp: %+v\n", message)
690 }
691
692 return message, nil
693}
694
695// SendChatAction sets a current action in a chat.
696//
697// Requires ChatID and a valid Action (see Chat constants).
698func (bot *BotAPI) SendChatAction(config ChatActionConfig) error {
699 v := url.Values{}
700 v.Add("chat_id", strconv.Itoa(config.ChatID))
701 v.Add("action", config.Action)
702
703 _, err := bot.MakeRequest("sendChatAction", v)
704 if err != nil {
705 return err
706 }
707
708 return nil
709}
710
711// GetUserProfilePhotos gets a user's profile photos.
712//
713// Requires UserID.
714// Offset and Limit are optional.
715func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
716 v := url.Values{}
717 v.Add("user_id", strconv.Itoa(config.UserID))
718 if config.Offset != 0 {
719 v.Add("offset", strconv.Itoa(config.Offset))
720 }
721 if config.Limit != 0 {
722 v.Add("limit", strconv.Itoa(config.Limit))
723 }
724
725 resp, err := bot.MakeRequest("getUserProfilePhotos", v)
726 if err != nil {
727 return UserProfilePhotos{}, err
728 }
729
730 var profilePhotos UserProfilePhotos
731 json.Unmarshal(resp.Result, &profilePhotos)
732
733 if bot.Debug {
734 log.Printf("getUserProfilePhotos req : %+v\n", v)
735 log.Printf("getUserProfilePhotos resp: %+v\n", profilePhotos)
736 }
737
738 return profilePhotos, nil
739}
740
741// GetUpdates fetches updates.
742// If a WebHook is set, this will not return any data!
743//
744// Offset, Limit, and Timeout are optional.
745// To not get old items, set Offset to one higher than the previous item.
746// Set Timeout to a large number to reduce requests and get responses instantly.
747func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
748 v := url.Values{}
749 if config.Offset > 0 {
750 v.Add("offset", strconv.Itoa(config.Offset))
751 }
752 if config.Limit > 0 {
753 v.Add("limit", strconv.Itoa(config.Limit))
754 }
755 if config.Timeout > 0 {
756 v.Add("timeout", strconv.Itoa(config.Timeout))
757 }
758
759 resp, err := bot.MakeRequest("getUpdates", v)
760 if err != nil {
761 return []Update{}, err
762 }
763
764 var updates []Update
765 json.Unmarshal(resp.Result, &updates)
766
767 if bot.Debug {
768 log.Printf("getUpdates: %+v\n", updates)
769 }
770
771 return updates, nil
772}
773
774// SetWebhook sets a webhook.
775// If this is set, GetUpdates will not get any data!
776//
777// Requires Url OR to set Clear to true.
778func (bot *BotAPI) SetWebhook(config WebhookConfig) error {
779 v := url.Values{}
780 if !config.Clear {
781 v.Add("url", config.URL.String())
782 }
783
784 _, err := bot.MakeRequest("setWebhook", v)
785
786 return err
787}