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