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