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 resp, err := bot.Client.Do(req)
222 if err != nil {
223 return APIResponse{}, err
224 }
225 defer resp.Body.Close()
226
227 var apiResp APIResponse
228 bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
229 if err != nil {
230 return apiResp, err
231 }
232
233 if bot.Debug {
234 log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
235 }
236
237 if !apiResp.Ok {
238 var parameters ResponseParameters
239
240 if apiResp.Parameters != nil {
241 parameters = *apiResp.Parameters
242 }
243
244 return apiResp, Error{
245 Message: apiResp.Description,
246 ResponseParameters: parameters,
247 }
248 }
249
250 return apiResp, nil
251}
252
253// GetFileDirectURL returns direct URL to file
254//
255// It requires the FileID.
256func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) {
257 file, err := bot.GetFile(FileConfig{fileID})
258
259 if err != nil {
260 return "", err
261 }
262
263 return file.Link(bot.Token), nil
264}
265
266// GetMe fetches the currently authenticated bot.
267//
268// This method is called upon creation to validate the token,
269// and so you may get this data from BotAPI.Self without the need for
270// another request.
271func (bot *BotAPI) GetMe() (User, error) {
272 resp, err := bot.MakeRequest("getMe", nil)
273 if err != nil {
274 return User{}, err
275 }
276
277 var user User
278 err = json.Unmarshal(resp.Result, &user)
279
280 return user, err
281}
282
283// IsMessageToMe returns true if message directed to this bot.
284//
285// It requires the Message.
286func (bot *BotAPI) IsMessageToMe(message Message) bool {
287 return strings.Contains(message.Text, "@"+bot.Self.UserName)
288}
289
290// Request sends a Chattable to Telegram, and returns the APIResponse.
291func (bot *BotAPI) Request(c Chattable) (APIResponse, error) {
292 params, err := c.params()
293 if err != nil {
294 return APIResponse{}, err
295 }
296
297 switch t := c.(type) {
298 case Fileable:
299 if t.useExistingFile() {
300 return bot.MakeRequest(t.method(), params)
301 }
302
303 return bot.UploadFile(t.method(), params, t.name(), t.getFile())
304 default:
305 return bot.MakeRequest(c.method(), params)
306 }
307}
308
309// Send will send a Chattable item to Telegram and provides the
310// returned Message.
311func (bot *BotAPI) Send(c Chattable) (Message, error) {
312 resp, err := bot.Request(c)
313 if err != nil {
314 return Message{}, err
315 }
316
317 var message Message
318 err = json.Unmarshal(resp.Result, &message)
319
320 return message, err
321}
322
323// SendMediaGroup sends a media group and returns the resulting messages.
324func (bot *BotAPI) SendMediaGroup(config MediaGroupConfig) ([]Message, error) {
325 params, _ := config.params()
326
327 resp, err := bot.MakeRequest(config.method(), params)
328 if err != nil {
329 return nil, err
330 }
331
332 var messages []Message
333 err = json.Unmarshal(resp.Result, &messages)
334
335 return messages, err
336}
337
338// GetUserProfilePhotos gets a user's profile photos.
339//
340// It requires UserID.
341// Offset and Limit are optional.
342func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
343 params, _ := config.params()
344
345 resp, err := bot.MakeRequest(config.method(), params)
346 if err != nil {
347 return UserProfilePhotos{}, err
348 }
349
350 var profilePhotos UserProfilePhotos
351 err = json.Unmarshal(resp.Result, &profilePhotos)
352
353 return profilePhotos, err
354}
355
356// GetFile returns a File which can download a file from Telegram.
357//
358// Requires FileID.
359func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
360 params, _ := config.params()
361
362 resp, err := bot.MakeRequest(config.method(), params)
363 if err != nil {
364 return File{}, err
365 }
366
367 var file File
368 err = json.Unmarshal(resp.Result, &file)
369
370 return file, err
371}
372
373// GetUpdates fetches updates.
374// If a WebHook is set, this will not return any data!
375//
376// Offset, Limit, and Timeout are optional.
377// To avoid stale items, set Offset to one higher than the previous item.
378// Set Timeout to a large number to reduce requests so you can get updates
379// instantly instead of having to wait between requests.
380func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
381 params, _ := config.params()
382
383 resp, err := bot.MakeRequest(config.method(), params)
384 if err != nil {
385 return []Update{}, err
386 }
387
388 var updates []Update
389 err = json.Unmarshal(resp.Result, &updates)
390
391 return updates, err
392}
393
394// GetWebhookInfo allows you to fetch information about a webhook and if
395// one currently is set, along with pending update count and error messages.
396func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) {
397 resp, err := bot.MakeRequest("getWebhookInfo", nil)
398 if err != nil {
399 return WebhookInfo{}, err
400 }
401
402 var info WebhookInfo
403 err = json.Unmarshal(resp.Result, &info)
404
405 return info, err
406}
407
408// GetUpdatesChan starts and returns a channel for getting updates.
409func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) UpdatesChannel {
410 ch := make(chan Update, bot.Buffer)
411
412 go func() {
413 for {
414 select {
415 case <-bot.shutdownChannel:
416 return
417 default:
418 }
419
420 updates, err := bot.GetUpdates(config)
421 if err != nil {
422 log.Println(err)
423 log.Println("Failed to get updates, retrying in 3 seconds...")
424 time.Sleep(time.Second * 3)
425
426 continue
427 }
428
429 for _, update := range updates {
430 if update.UpdateID >= config.Offset {
431 config.Offset = update.UpdateID + 1
432 ch <- update
433 }
434 }
435 }
436 }()
437
438 return ch
439}
440
441// StopReceivingUpdates stops the go routine which receives updates
442func (bot *BotAPI) StopReceivingUpdates() {
443 if bot.Debug {
444 log.Println("Stopping the update receiver routine...")
445 }
446 close(bot.shutdownChannel)
447}
448
449// ListenForWebhook registers a http handler for a webhook.
450func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
451 ch := make(chan Update, bot.Buffer)
452
453 http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
454 ch <- bot.HandleUpdate(w, r)
455 })
456
457 return ch
458}
459
460// HandleUpdate parses and returns update received via webhook
461func (bot *BotAPI) HandleUpdate(res http.ResponseWriter, req *http.Request) Update {
462 bytes, _ := ioutil.ReadAll(req.Body)
463 req.Body.Close()
464
465 var update Update
466 json.Unmarshal(bytes, &update)
467
468 return update
469}
470
471// WriteToHTTPResponse writes the request to the HTTP ResponseWriter.
472//
473// It doesn't support uploading files.
474//
475// See https://core.telegram.org/bots/api#making-requests-when-getting-updates
476// for details.
477func WriteToHTTPResponse(w http.ResponseWriter, c Chattable) error {
478 params, err := c.params()
479 if err != nil {
480 return err
481 }
482
483 if t, ok := c.(Fileable); ok {
484 if !t.useExistingFile() {
485 return errors.New("unable to use http response to upload files")
486 }
487 }
488
489 values := buildParams(params)
490 values.Set("method", c.method())
491
492 w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
493 _, err = w.Write([]byte(values.Encode()))
494 return err
495}
496
497// GetChat gets information about a chat.
498func (bot *BotAPI) GetChat(config ChatInfoConfig) (Chat, error) {
499 params, _ := config.params()
500
501 resp, err := bot.MakeRequest(config.method(), params)
502 if err != nil {
503 return Chat{}, err
504 }
505
506 var chat Chat
507 err = json.Unmarshal(resp.Result, &chat)
508
509 return chat, err
510}
511
512// GetChatAdministrators gets a list of administrators in the chat.
513//
514// If none have been appointed, only the creator will be returned.
515// Bots are not shown, even if they are an administrator.
516func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]ChatMember, error) {
517 params, _ := config.params()
518
519 resp, err := bot.MakeRequest(config.method(), params)
520 if err != nil {
521 return []ChatMember{}, err
522 }
523
524 var members []ChatMember
525 err = json.Unmarshal(resp.Result, &members)
526
527 return members, err
528}
529
530// GetChatMembersCount gets the number of users in a chat.
531func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) {
532 params, _ := config.params()
533
534 resp, err := bot.MakeRequest(config.method(), params)
535 if err != nil {
536 return -1, err
537 }
538
539 var count int
540 err = json.Unmarshal(resp.Result, &count)
541
542 return count, err
543}
544
545// GetChatMember gets a specific chat member.
546func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) {
547 params, _ := config.params()
548
549 resp, err := bot.MakeRequest(config.method(), params)
550 if err != nil {
551 return ChatMember{}, err
552 }
553
554 var member ChatMember
555 err = json.Unmarshal(resp.Result, &member)
556
557 return member, err
558}
559
560// GetGameHighScores allows you to get the high scores for a game.
561func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
562 params, _ := config.params()
563
564 resp, err := bot.MakeRequest(config.method(), params)
565 if err != nil {
566 return []GameHighScore{}, err
567 }
568
569 var highScores []GameHighScore
570 err = json.Unmarshal(resp.Result, &highScores)
571
572 return highScores, err
573}
574
575// GetInviteLink get InviteLink for a chat
576func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) {
577 params, _ := config.params()
578
579 resp, err := bot.MakeRequest(config.method(), params)
580 if err != nil {
581 return "", err
582 }
583
584 var inviteLink string
585 err = json.Unmarshal(resp.Result, &inviteLink)
586
587 return inviteLink, err
588}
589
590// GetStickerSet returns a StickerSet.
591func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) {
592 params, _ := config.params()
593
594 resp, err := bot.MakeRequest(config.method(), params)
595 if err != nil {
596 return StickerSet{}, err
597 }
598
599 var stickers StickerSet
600 err = json.Unmarshal(resp.Result, &stickers)
601
602 return stickers, err
603}
604
605// StopPoll stops a poll and returns the result.
606func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) {
607 params, err := config.params()
608 if err != nil {
609 return Poll{}, err
610 }
611
612 resp, err := bot.MakeRequest(config.method(), params)
613 if err != nil {
614 return Poll{}, err
615 }
616
617 var poll Poll
618 err = json.Unmarshal(resp.Result, &poll)
619
620 return poll, err
621}
622
623// GetMyCommands gets the currently registered commands.
624func (bot *BotAPI) GetMyCommands() ([]BotCommand, error) {
625 config := GetMyCommandsConfig{}
626
627 params, err := config.params()
628 if err != nil {
629 return nil, err
630 }
631
632 resp, err := bot.MakeRequest(config.method(), params)
633 if err != nil {
634 return nil, err
635 }
636
637 var commands []BotCommand
638 err = json.Unmarshal(resp.Result, &commands)
639
640 return commands, err
641}