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 BaseChat) 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
202func (bot *BotAPI) sendExisting(method string, config Fileable) (Message, error) {
203 v, err := config.Values()
204
205 if err != nil {
206 return Message{}, err
207 }
208
209 message, err := bot.MakeMessageRequest(method, v)
210 if err != nil {
211 return Message{}, err
212 }
213
214 return message, nil
215}
216
217// SendMessage sends a Message to a chat.
218//
219// Requires ChatID and Text.
220// DisableWebPagePreview, ReplyToMessageID, and ReplyMarkup are optional.
221func (bot *BotAPI) SendMessage(config MessageConfig) (Message, error) {
222 v, err := config.Values()
223 if err != nil {
224 return Message{}, err
225 }
226
227 message, err := bot.MakeMessageRequest("SendMessage", v)
228
229 if err != nil {
230 return Message{}, err
231 }
232
233 return message, nil
234}
235
236// ForwardMessage forwards a message from one chat to another.
237//
238// Requires ChatID (destination), FromChatID (source), and MessageID.
239func (bot *BotAPI) ForwardMessage(config ForwardConfig) (Message, error) {
240 v, err := config.Values()
241 if err != nil {
242 return Message{}, err
243 }
244
245 message, err := bot.MakeMessageRequest("forwardMessage", v)
246 if err != nil {
247 return Message{}, err
248 }
249
250 return message, nil
251}
252
253// SendLocation sends a location to a chat.
254//
255// Requires ChatID, Latitude, and Longitude.
256// ReplyToMessageID and ReplyMarkup are optional.
257func (bot *BotAPI) SendLocation(config LocationConfig) (Message, error) {
258 v, err := config.Values()
259 if err != nil {
260 return Message{}, err
261 }
262
263 message, err := bot.MakeMessageRequest("sendLocation", v)
264 if err != nil {
265 return Message{}, err
266 }
267
268 return message, nil
269}
270
271// SendPhoto sends or uploads a photo to a chat.
272//
273// Requires ChatID and FileID OR File.
274// Caption, ReplyToMessageID, and ReplyMarkup are optional.
275// File should be either a string, FileBytes, or FileReader.
276func (bot *BotAPI) SendPhoto(config PhotoConfig) (Message, error) {
277 if config.UseExisting {
278 return bot.sendExisting("SendPhoto", config)
279 }
280
281 params, err := config.Params()
282 if err != nil {
283 return Message{}, err
284 }
285
286 file := config.GetFile()
287
288 resp, err := bot.UploadFile("SendPhoto", params, "photo", file)
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("SendPhoto resp: %+v\n", message)
298 }
299
300 return message, nil
301}
302
303// SendAudio sends or uploads an audio clip to a chat.
304// If using a file, the file must be in the .mp3 format.
305//
306// When the fields title and performer are both empty and
307// the mime-type of the file to be sent is not audio/mpeg,
308// the file must be an .ogg file encoded with OPUS.
309// You may use the tgutils.EncodeAudio func to assist you with this, if needed.
310//
311// Requires ChatID and FileID OR File.
312// ReplyToMessageID and ReplyMarkup are optional.
313// File should be either a string, FileBytes, or FileReader.
314func (bot *BotAPI) SendAudio(config AudioConfig) (Message, error) {
315 if config.UseExisting {
316 return bot.sendExisting("sendAudio", config)
317 }
318
319 params, err := config.Params()
320 if err != nil {
321 return Message{}, err
322 }
323
324 file := config.GetFile()
325
326 resp, err := bot.UploadFile("sendAudio", params, "audio", file)
327 if err != nil {
328 return Message{}, err
329 }
330
331 var message Message
332 json.Unmarshal(resp.Result, &message)
333
334 if bot.Debug {
335 log.Printf("sendAudio resp: %+v\n", message)
336 }
337
338 return message, nil
339}
340
341// SendDocument sends or uploads a document to a chat.
342//
343// Requires ChatID and FileID OR File.
344// ReplyToMessageID and ReplyMarkup are optional.
345// File should be either a string, FileBytes, or FileReader.
346func (bot *BotAPI) SendDocument(config DocumentConfig) (Message, error) {
347 if config.UseExisting {
348 return bot.sendExisting("sendDocument", config)
349 }
350
351 params, err := config.Params()
352 if err != nil {
353 return Message{}, err
354 }
355
356 file := config.GetFile()
357
358 resp, err := bot.UploadFile("sendDocument", params, "document", file)
359 if err != nil {
360 return Message{}, err
361 }
362
363 var message Message
364 json.Unmarshal(resp.Result, &message)
365
366 if bot.Debug {
367 log.Printf("sendDocument resp: %+v\n", message)
368 }
369
370 return message, nil
371}
372
373// SendVoice sends or uploads a playable voice to a chat.
374// If using a file, the file must be encoded as an .ogg with OPUS.
375// You may use the tgutils.EncodeAudio func to assist you with this, if needed.
376//
377// Requires ChatID and FileID OR File.
378// ReplyToMessageID and ReplyMarkup are optional.
379// File should be either a string, FileBytes, or FileReader.
380func (bot *BotAPI) SendVoice(config VoiceConfig) (Message, error) {
381 if config.UseExisting {
382 return bot.sendExisting("sendVoice", config)
383 }
384
385 params, err := config.Params()
386 if err != nil {
387 return Message{}, err
388 }
389
390 file := config.GetFile()
391
392 resp, err := bot.UploadFile("SendVoice", params, "voice", file)
393 if err != nil {
394 return Message{}, err
395 }
396
397 var message Message
398 json.Unmarshal(resp.Result, &message)
399
400 if bot.Debug {
401 log.Printf("SendVoice resp: %+v\n", message)
402 }
403
404 return message, nil
405}
406
407// SendSticker sends or uploads a sticker to a chat.
408//
409// Requires ChatID and FileID OR File.
410// ReplyToMessageID and ReplyMarkup are optional.
411// File should be either a string, FileBytes, or FileReader.
412func (bot *BotAPI) SendSticker(config StickerConfig) (Message, error) {
413 if config.UseExisting {
414 return bot.sendExisting("sendSticker", config)
415 }
416
417 params, err := config.Params()
418 if err != nil {
419 return Message{}, err
420 }
421
422 file := config.GetFile()
423
424 resp, err := bot.UploadFile("sendSticker", params, "sticker", file)
425 if err != nil {
426 return Message{}, err
427 }
428
429 var message Message
430 json.Unmarshal(resp.Result, &message)
431
432 if bot.Debug {
433 log.Printf("sendSticker resp: %+v\n", message)
434 }
435
436 return message, nil
437}
438
439// SendVideo sends or uploads a video to a chat.
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) SendVideo(config VideoConfig) (Message, error) {
445 if config.UseExisting {
446 return bot.sendExisting("sendVideo", config)
447 }
448
449 params, err := config.Params()
450 if err != nil {
451 return Message{}, err
452 }
453
454 file := config.GetFile()
455
456 resp, err := bot.UploadFile("sendVideo", params, "video", file)
457 if err != nil {
458 return Message{}, err
459 }
460
461 var message Message
462 json.Unmarshal(resp.Result, &message)
463
464 if bot.Debug {
465 log.Printf("sendVideo resp: %+v\n", message)
466 }
467
468 return message, nil
469}
470
471// SendChatAction sets a current action in a chat.
472//
473// Requires ChatID and a valid Action (see Chat constants).
474func (bot *BotAPI) SendChatAction(config ChatActionConfig) error {
475 v, err := config.Values()
476 if err != nil {
477 return err
478 }
479
480 _, err = bot.MakeRequest("sendChatAction", v)
481 if err != nil {
482 return err
483 }
484
485 return nil
486}
487
488// GetUserProfilePhotos gets a user's profile photos.
489//
490// Requires UserID.
491// Offset and Limit are optional.
492func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
493 v := url.Values{}
494 v.Add("user_id", strconv.Itoa(config.UserID))
495 if config.Offset != 0 {
496 v.Add("offset", strconv.Itoa(config.Offset))
497 }
498 if config.Limit != 0 {
499 v.Add("limit", strconv.Itoa(config.Limit))
500 }
501
502 resp, err := bot.MakeRequest("getUserProfilePhotos", v)
503 if err != nil {
504 return UserProfilePhotos{}, err
505 }
506
507 var profilePhotos UserProfilePhotos
508 json.Unmarshal(resp.Result, &profilePhotos)
509
510 bot.DebugLog("GetUserProfilePhoto", v, profilePhotos)
511
512 return profilePhotos, nil
513}
514
515// GetFile returns a file_id required to download a file.
516//
517// Requires FileID.
518func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
519 v := url.Values{}
520 v.Add("file_id", config.FileID)
521
522 resp, err := bot.MakeRequest("getFile", v)
523 if err != nil {
524 return File{}, err
525 }
526
527 var file File
528 json.Unmarshal(resp.Result, &file)
529
530 bot.DebugLog("GetFile", v, file)
531
532 return file, nil
533}
534
535// GetUpdates fetches updates.
536// If a WebHook is set, this will not return any data!
537//
538// Offset, Limit, and Timeout are optional.
539// To not get old items, set Offset to one higher than the previous item.
540// Set Timeout to a large number to reduce requests and get responses instantly.
541func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
542 v := url.Values{}
543 if config.Offset > 0 {
544 v.Add("offset", strconv.Itoa(config.Offset))
545 }
546 if config.Limit > 0 {
547 v.Add("limit", strconv.Itoa(config.Limit))
548 }
549 if config.Timeout > 0 {
550 v.Add("timeout", strconv.Itoa(config.Timeout))
551 }
552
553 resp, err := bot.MakeRequest("getUpdates", v)
554 if err != nil {
555 return []Update{}, err
556 }
557
558 var updates []Update
559 json.Unmarshal(resp.Result, &updates)
560
561 if bot.Debug {
562 log.Printf("getUpdates: %+v\n", updates)
563 }
564
565 return updates, nil
566}
567
568// SetWebhook sets a webhook.
569// If this is set, GetUpdates will not get any data!
570//
571// Requires URL OR to set Clear to true.
572func (bot *BotAPI) SetWebhook(config WebhookConfig) (APIResponse, error) {
573 if config.Certificate == nil {
574 v := url.Values{}
575 if !config.Clear {
576 v.Add("url", config.URL.String())
577 }
578
579 return bot.MakeRequest("setWebhook", v)
580 }
581
582 params := make(map[string]string)
583 params["url"] = config.URL.String()
584
585 resp, err := bot.UploadFile("setWebhook", params, "certificate", config.Certificate)
586 if err != nil {
587 return APIResponse{}, err
588 }
589
590 var apiResp APIResponse
591 json.Unmarshal(resp.Result, &apiResp)
592
593 if bot.Debug {
594 log.Printf("setWebhook resp: %+v\n", apiResp)
595 }
596
597 return apiResp, nil
598}
599
600// UpdatesChan starts a channel for getting updates.
601func (bot *BotAPI) UpdatesChan(config UpdateConfig) error {
602 bot.Updates = make(chan Update, 100)
603
604 go func() {
605 for {
606 updates, err := bot.GetUpdates(config)
607 if err != nil {
608 log.Println(err)
609 log.Println("Failed to get updates, retrying in 3 seconds...")
610 time.Sleep(time.Second * 3)
611
612 continue
613 }
614
615 for _, update := range updates {
616 if update.UpdateID >= config.Offset {
617 config.Offset = update.UpdateID + 1
618 bot.Updates <- update
619 }
620 }
621 }
622 }()
623
624 return nil
625}
626
627// ListenForWebhook registers a http handler for a webhook.
628func (bot *BotAPI) ListenForWebhook(pattern string) {
629 bot.Updates = make(chan Update, 100)
630
631 http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
632 bytes, _ := ioutil.ReadAll(r.Body)
633
634 var update Update
635 json.Unmarshal(bytes, &update)
636
637 bot.Updates <- update
638 })
639}