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