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 bytes, _ := ioutil.ReadAll(r.Body)
455 r.Body.Close()
456
457 var update Update
458 json.Unmarshal(bytes, &update)
459
460 ch <- update
461 })
462
463 return ch
464}
465
466// WriteToHTTPResponse writes the request to the HTTP ResponseWriter.
467//
468// It doesn't support uploading files.
469//
470// See https://core.telegram.org/bots/api#making-requests-when-getting-updates
471// for details.
472func WriteToHTTPResponse(w http.ResponseWriter, c Chattable) error {
473 params, err := c.params()
474 if err != nil {
475 return err
476 }
477
478 if t, ok := c.(Fileable); ok {
479 if !t.useExistingFile() {
480 return errors.New("unable to use http response to upload files")
481 }
482 }
483
484 values := buildParams(params)
485 values.Set("method", c.method())
486
487 w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
488 _, err = w.Write([]byte(values.Encode()))
489 return err
490}
491
492// GetChat gets information about a chat.
493func (bot *BotAPI) GetChat(config ChatInfoConfig) (Chat, error) {
494 params, _ := config.params()
495
496 resp, err := bot.MakeRequest(config.method(), params)
497 if err != nil {
498 return Chat{}, err
499 }
500
501 var chat Chat
502 err = json.Unmarshal(resp.Result, &chat)
503
504 return chat, err
505}
506
507// GetChatAdministrators gets a list of administrators in the chat.
508//
509// If none have been appointed, only the creator will be returned.
510// Bots are not shown, even if they are an administrator.
511func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]ChatMember, error) {
512 params, _ := config.params()
513
514 resp, err := bot.MakeRequest(config.method(), params)
515 if err != nil {
516 return []ChatMember{}, err
517 }
518
519 var members []ChatMember
520 err = json.Unmarshal(resp.Result, &members)
521
522 return members, err
523}
524
525// GetChatMembersCount gets the number of users in a chat.
526func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) {
527 params, _ := config.params()
528
529 resp, err := bot.MakeRequest(config.method(), params)
530 if err != nil {
531 return -1, err
532 }
533
534 var count int
535 err = json.Unmarshal(resp.Result, &count)
536
537 return count, err
538}
539
540// GetChatMember gets a specific chat member.
541func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) {
542 params, _ := config.params()
543
544 resp, err := bot.MakeRequest(config.method(), params)
545 if err != nil {
546 return ChatMember{}, err
547 }
548
549 var member ChatMember
550 err = json.Unmarshal(resp.Result, &member)
551
552 return member, err
553}
554
555// GetGameHighScores allows you to get the high scores for a game.
556func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
557 params, _ := config.params()
558
559 resp, err := bot.MakeRequest(config.method(), params)
560 if err != nil {
561 return []GameHighScore{}, err
562 }
563
564 var highScores []GameHighScore
565 err = json.Unmarshal(resp.Result, &highScores)
566
567 return highScores, err
568}
569
570// GetInviteLink get InviteLink for a chat
571func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) {
572 params, _ := config.params()
573
574 resp, err := bot.MakeRequest(config.method(), params)
575 if err != nil {
576 return "", err
577 }
578
579 var inviteLink string
580 err = json.Unmarshal(resp.Result, &inviteLink)
581
582 return inviteLink, err
583}
584
585// GetStickerSet returns a StickerSet.
586func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) {
587 params, _ := config.params()
588
589 resp, err := bot.MakeRequest(config.method(), params)
590 if err != nil {
591 return StickerSet{}, err
592 }
593
594 var stickers StickerSet
595 err = json.Unmarshal(resp.Result, &stickers)
596
597 return stickers, err
598}
599
600// StopPoll stops a poll and returns the result.
601func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) {
602 params, err := config.params()
603 if err != nil {
604 return Poll{}, err
605 }
606
607 resp, err := bot.MakeRequest(config.method(), params)
608 if err != nil {
609 return Poll{}, err
610 }
611
612 var poll Poll
613 err = json.Unmarshal(resp.Result, &poll)
614
615 return poll, err
616}
617
618// GetMyCommands gets the currently registered commands.
619func (bot *BotAPI) GetMyCommands() ([]BotCommand, error) {
620 config := GetMyCommandsConfig{}
621
622 params, err := config.params()
623 if err != nil {
624 return nil, err
625 }
626
627 resp, err := bot.MakeRequest(config.method(), params)
628 if err != nil {
629 return nil, err
630 }
631
632 var commands []BotCommand
633 err = json.Unmarshal(resp.Result, &commands)
634
635 return commands, err
636}