bot.go (view raw)
1// Package tgbotapi has functions and types used for interacting with
2// the Telegram Bot API.
3package tgbotapi
4
5import (
6 "bytes"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "io"
11 "io/ioutil"
12 "net/http"
13 "net/url"
14 "os"
15 "strings"
16 "time"
17
18 "github.com/technoweenie/multipartstreamer"
19)
20
21// BotAPI allows you to interact with the Telegram Bot API.
22type BotAPI struct {
23 Token string `json:"token"`
24 Debug bool `json:"debug"`
25 Buffer int `json:"buffer"`
26
27 Self User `json:"-"`
28 Client *http.Client `json:"-"`
29 shutdownChannel chan interface{}
30
31 apiEndpoint string
32}
33
34// NewBotAPI creates a new BotAPI instance.
35//
36// It requires a token, provided by @BotFather on Telegram.
37func NewBotAPI(token string) (*BotAPI, error) {
38 return NewBotAPIWithClient(token, &http.Client{})
39}
40
41// NewBotAPIWithClient creates a new BotAPI instance
42// and allows you to pass a http.Client.
43//
44// It requires a token, provided by @BotFather on Telegram.
45func NewBotAPIWithClient(token string, client *http.Client) (*BotAPI, error) {
46 bot := &BotAPI{
47 Token: token,
48 Client: client,
49 Buffer: 100,
50 shutdownChannel: make(chan interface{}),
51
52 apiEndpoint: APIEndpoint,
53 }
54
55 self, err := bot.GetMe()
56 if err != nil {
57 return nil, err
58 }
59
60 bot.Self = self
61
62 return bot, nil
63}
64
65// SetAPIEndpoint changes the Telegram Bot API endpoint used by the instance.
66func (bot *BotAPI) SetAPIEndpoint(apiEndpoint string) {
67 bot.apiEndpoint = apiEndpoint
68}
69
70func buildParams(in Params) (out url.Values) {
71 if in == nil {
72 return url.Values{}
73 }
74
75 out = url.Values{}
76
77 for key, value := range in {
78 out.Set(key, value)
79 }
80
81 return
82}
83
84// MakeRequest makes a request to a specific endpoint with our token.
85func (bot *BotAPI) MakeRequest(endpoint string, params Params) (APIResponse, error) {
86 if bot.Debug {
87 log.Printf("Endpoint: %s, params: %v\n", endpoint, params)
88 }
89
90 method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint)
91
92 values := buildParams(params)
93
94 resp, err := bot.Client.PostForm(method, values)
95 if err != nil {
96 return APIResponse{}, err
97 }
98 defer resp.Body.Close()
99
100 var apiResp APIResponse
101 bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
102 if err != nil {
103 return apiResp, err
104 }
105
106 if bot.Debug {
107 log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
108 }
109
110 if !apiResp.Ok {
111 var parameters ResponseParameters
112
113 if apiResp.Parameters != nil {
114 parameters = *apiResp.Parameters
115 }
116
117 return apiResp, Error{
118 Code: apiResp.ErrorCode,
119 Message: apiResp.Description,
120 ResponseParameters: parameters,
121 }
122 }
123
124 return apiResp, nil
125}
126
127// decodeAPIResponse decode response and return slice of bytes if debug enabled.
128// If debug disabled, just decode http.Response.Body stream to APIResponse struct
129// for efficient memory usage
130func (bot *BotAPI) decodeAPIResponse(responseBody io.Reader, resp *APIResponse) (_ []byte, err error) {
131 if !bot.Debug {
132 dec := json.NewDecoder(responseBody)
133 err = dec.Decode(resp)
134 return
135 }
136
137 // if debug, read reponse body
138 data, err := ioutil.ReadAll(responseBody)
139 if err != nil {
140 return
141 }
142
143 err = json.Unmarshal(data, resp)
144 if err != nil {
145 return
146 }
147
148 return data, nil
149}
150
151// UploadFile makes a request to the API with a file.
152//
153// Requires the parameter to hold the file not be in the params.
154// File should be a string to a file path, a FileBytes struct,
155// a FileReader struct, or a url.URL.
156//
157// Note that if your FileReader has a size set to -1, it will read
158// the file into memory to calculate a size.
159func (bot *BotAPI) UploadFile(endpoint string, params Params, fieldname string, file interface{}) (APIResponse, error) {
160 ms := multipartstreamer.New()
161
162 switch f := file.(type) {
163 case string:
164 ms.WriteFields(params)
165
166 fileHandle, err := os.Open(f)
167 if err != nil {
168 return APIResponse{}, err
169 }
170 defer fileHandle.Close()
171
172 fi, err := os.Stat(f)
173 if err != nil {
174 return APIResponse{}, err
175 }
176
177 ms.WriteReader(fieldname, fileHandle.Name(), fi.Size(), fileHandle)
178 case FileBytes:
179 ms.WriteFields(params)
180
181 buf := bytes.NewBuffer(f.Bytes)
182 ms.WriteReader(fieldname, f.Name, int64(len(f.Bytes)), buf)
183 case FileReader:
184 ms.WriteFields(params)
185
186 if f.Size != -1 {
187 ms.WriteReader(fieldname, f.Name, f.Size, f.Reader)
188
189 break
190 }
191
192 data, err := ioutil.ReadAll(f.Reader)
193 if err != nil {
194 return APIResponse{}, err
195 }
196
197 buf := bytes.NewBuffer(data)
198
199 ms.WriteReader(fieldname, f.Name, int64(len(data)), buf)
200 case url.URL:
201 params[fieldname] = f.String()
202
203 ms.WriteFields(params)
204 default:
205 return APIResponse{}, errors.New(ErrBadFileType)
206 }
207
208 if bot.Debug {
209 log.Printf("Endpoint: %s, fieldname: %s, params: %v, file: %T\n", endpoint, fieldname, params, file)
210 }
211
212 method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint)
213
214 req, err := http.NewRequest("POST", method, nil)
215 if err != nil {
216 return APIResponse{}, err
217 }
218
219 ms.SetupRequest(req)
220
221 res, err := bot.Client.Do(req)
222 if err != nil {
223 return APIResponse{}, err
224 }
225 defer res.Body.Close()
226
227 bytes, err := ioutil.ReadAll(res.Body)
228 if err != nil {
229 return APIResponse{}, err
230 }
231
232 if bot.Debug {
233 log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
234 }
235
236 var apiResp APIResponse
237
238 err = json.Unmarshal(bytes, &apiResp)
239 if err != nil {
240 return APIResponse{}, err
241 }
242
243 if !apiResp.Ok {
244 return APIResponse{}, errors.New(apiResp.Description)
245 }
246
247 return apiResp, nil
248}
249
250// GetFileDirectURL returns direct URL to file
251//
252// It requires the FileID.
253func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) {
254 file, err := bot.GetFile(FileConfig{fileID})
255
256 if err != nil {
257 return "", err
258 }
259
260 return file.Link(bot.Token), nil
261}
262
263// GetMe fetches the currently authenticated bot.
264//
265// This method is called upon creation to validate the token,
266// and so you may get this data from BotAPI.Self without the need for
267// another request.
268func (bot *BotAPI) GetMe() (User, error) {
269 resp, err := bot.MakeRequest("getMe", nil)
270 if err != nil {
271 return User{}, err
272 }
273
274 var user User
275 err = json.Unmarshal(resp.Result, &user)
276
277 return user, err
278}
279
280// IsMessageToMe returns true if message directed to this bot.
281//
282// It requires the Message.
283func (bot *BotAPI) IsMessageToMe(message Message) bool {
284 return strings.Contains(message.Text, "@"+bot.Self.UserName)
285}
286
287// Request sends a Chattable to Telegram, and returns the APIResponse.
288func (bot *BotAPI) Request(c Chattable) (APIResponse, error) {
289 params, err := c.params()
290 if err != nil {
291 return APIResponse{}, err
292 }
293
294 switch t := c.(type) {
295 case Fileable:
296 if t.useExistingFile() {
297 return bot.MakeRequest(t.method(), params)
298 }
299
300 return bot.UploadFile(t.method(), params, t.name(), t.getFile())
301 default:
302 return bot.MakeRequest(c.method(), params)
303 }
304}
305
306// Send will send a Chattable item to Telegram and provides the
307// returned Message.
308func (bot *BotAPI) Send(c Chattable) (Message, error) {
309 resp, err := bot.Request(c)
310 if err != nil {
311 return Message{}, err
312 }
313
314 var message Message
315 err = json.Unmarshal(resp.Result, &message)
316
317 return message, err
318}
319
320// SendMediaGroup sends a media group and returns the resulting messages.
321func (bot *BotAPI) SendMediaGroup(config MediaGroupConfig) ([]Message, error) {
322 params, _ := config.params()
323
324 resp, err := bot.MakeRequest(config.method(), params)
325 if err != nil {
326 return nil, err
327 }
328
329 var messages []Message
330 err = json.Unmarshal(resp.Result, &messages)
331
332 return messages, err
333}
334
335// GetUserProfilePhotos gets a user's profile photos.
336//
337// It requires UserID.
338// Offset and Limit are optional.
339func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
340 params, _ := config.params()
341
342 resp, err := bot.MakeRequest(config.method(), params)
343 if err != nil {
344 return UserProfilePhotos{}, err
345 }
346
347 var profilePhotos UserProfilePhotos
348 err = json.Unmarshal(resp.Result, &profilePhotos)
349
350 return profilePhotos, err
351}
352
353// GetFile returns a File which can download a file from Telegram.
354//
355// Requires FileID.
356func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
357 params, _ := config.params()
358
359 resp, err := bot.MakeRequest(config.method(), params)
360 if err != nil {
361 return File{}, err
362 }
363
364 var file File
365 err = json.Unmarshal(resp.Result, &file)
366
367 return file, err
368}
369
370// GetUpdates fetches updates.
371// If a WebHook is set, this will not return any data!
372//
373// Offset, Limit, and Timeout are optional.
374// To avoid stale items, set Offset to one higher than the previous item.
375// Set Timeout to a large number to reduce requests so you can get updates
376// instantly instead of having to wait between requests.
377func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
378 params, _ := config.params()
379
380 resp, err := bot.MakeRequest(config.method(), params)
381 if err != nil {
382 return []Update{}, err
383 }
384
385 var updates []Update
386 err = json.Unmarshal(resp.Result, &updates)
387
388 return updates, err
389}
390
391// GetWebhookInfo allows you to fetch information about a webhook and if
392// one currently is set, along with pending update count and error messages.
393func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) {
394 resp, err := bot.MakeRequest("getWebhookInfo", nil)
395 if err != nil {
396 return WebhookInfo{}, err
397 }
398
399 var info WebhookInfo
400 err = json.Unmarshal(resp.Result, &info)
401
402 return info, err
403}
404
405// GetUpdatesChan starts and returns a channel for getting updates.
406func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) UpdatesChannel {
407 ch := make(chan Update, bot.Buffer)
408
409 go func() {
410 for {
411 select {
412 case <-bot.shutdownChannel:
413 return
414 default:
415 }
416
417 updates, err := bot.GetUpdates(config)
418 if err != nil {
419 log.Println(err)
420 log.Println("Failed to get updates, retrying in 3 seconds...")
421 time.Sleep(time.Second * 3)
422
423 continue
424 }
425
426 for _, update := range updates {
427 if update.UpdateID >= config.Offset {
428 config.Offset = update.UpdateID + 1
429 ch <- update
430 }
431 }
432 }
433 }()
434
435 return ch
436}
437
438// StopReceivingUpdates stops the go routine which receives updates
439func (bot *BotAPI) StopReceivingUpdates() {
440 if bot.Debug {
441 log.Println("Stopping the update receiver routine...")
442 }
443 close(bot.shutdownChannel)
444}
445
446// ListenForWebhook registers a http handler for a webhook.
447func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
448 ch := make(chan Update, bot.Buffer)
449
450 http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
451 bytes, _ := ioutil.ReadAll(r.Body)
452 r.Body.Close()
453
454 var update Update
455 json.Unmarshal(bytes, &update)
456
457 ch <- update
458 })
459
460 return ch
461}
462
463// WriteToHTTPResponse writes the request to the HTTP ResponseWriter.
464//
465// It doesn't support uploading files.
466//
467// See https://core.telegram.org/bots/api#making-requests-when-getting-updates
468// for details.
469func WriteToHTTPResponse(w http.ResponseWriter, c Chattable) error {
470 params, err := c.params()
471 if err != nil {
472 return err
473 }
474
475 if t, ok := c.(Fileable); ok {
476 if !t.useExistingFile() {
477 return errors.New("unable to use http response to upload files")
478 }
479 }
480
481 values := buildParams(params)
482 values.Set("method", c.method())
483
484 w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
485 _, err = w.Write([]byte(values.Encode()))
486 return err
487}
488
489// GetChat gets information about a chat.
490func (bot *BotAPI) GetChat(config ChatInfoConfig) (Chat, error) {
491 params, _ := config.params()
492
493 resp, err := bot.MakeRequest(config.method(), params)
494 if err != nil {
495 return Chat{}, err
496 }
497
498 var chat Chat
499 err = json.Unmarshal(resp.Result, &chat)
500
501 return chat, err
502}
503
504// GetChatAdministrators gets a list of administrators in the chat.
505//
506// If none have been appointed, only the creator will be returned.
507// Bots are not shown, even if they are an administrator.
508func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]ChatMember, error) {
509 params, _ := config.params()
510
511 resp, err := bot.MakeRequest(config.method(), params)
512 if err != nil {
513 return []ChatMember{}, err
514 }
515
516 var members []ChatMember
517 err = json.Unmarshal(resp.Result, &members)
518
519 return members, err
520}
521
522// GetChatMembersCount gets the number of users in a chat.
523func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) {
524 params, _ := config.params()
525
526 resp, err := bot.MakeRequest(config.method(), params)
527 if err != nil {
528 return -1, err
529 }
530
531 var count int
532 err = json.Unmarshal(resp.Result, &count)
533
534 return count, err
535}
536
537// GetChatMember gets a specific chat member.
538func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) {
539 params, _ := config.params()
540
541 resp, err := bot.MakeRequest(config.method(), params)
542 if err != nil {
543 return ChatMember{}, err
544 }
545
546 var member ChatMember
547 err = json.Unmarshal(resp.Result, &member)
548
549 return member, err
550}
551
552// GetGameHighScores allows you to get the high scores for a game.
553func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
554 params, _ := config.params()
555
556 resp, err := bot.MakeRequest(config.method(), params)
557 if err != nil {
558 return []GameHighScore{}, err
559 }
560
561 var highScores []GameHighScore
562 err = json.Unmarshal(resp.Result, &highScores)
563
564 return highScores, err
565}
566
567// GetInviteLink get InviteLink for a chat
568func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) {
569 params, _ := config.params()
570
571 resp, err := bot.MakeRequest(config.method(), params)
572 if err != nil {
573 return "", err
574 }
575
576 var inviteLink string
577 err = json.Unmarshal(resp.Result, &inviteLink)
578
579 return inviteLink, err
580}
581
582// GetStickerSet returns a StickerSet.
583func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) {
584 params, _ := config.params()
585
586 resp, err := bot.MakeRequest(config.method(), params)
587 if err != nil {
588 return StickerSet{}, err
589 }
590
591 var stickers StickerSet
592 err = json.Unmarshal(resp.Result, &stickers)
593
594 return stickers, err
595}
596
597// StopPoll stops a poll and returns the result.
598func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) {
599 params, err := config.params()
600 if err != nil {
601 return Poll{}, err
602 }
603
604 resp, err := bot.MakeRequest(config.method(), params)
605 if err != nil {
606 return Poll{}, err
607 }
608
609 var poll Poll
610 err = json.Unmarshal(resp.Result, &poll)
611
612 return poll, err
613}
614
615// GetMyCommands gets the currently registered commands.
616func (bot *BotAPI) GetMyCommands() ([]BotCommand, error) {
617 config := GetMyCommandsConfig{}
618
619 params, err := config.params()
620 if err != nil {
621 return nil, err
622 }
623
624 resp, err := bot.MakeRequest(config.method(), params)
625 if err != nil {
626 return nil, err
627 }
628
629 var commands []BotCommand
630 err = json.Unmarshal(resp.Result, &commands)
631
632 return commands, err
633}