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 resp, err := bot.Client.Do(req)
212 if err != nil {
213 return APIResponse{}, err
214 }
215 defer resp.Body.Close()
216
217 var apiResp APIResponse
218 bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
219 if err != nil {
220 return apiResp, err
221 }
222
223 if bot.Debug {
224 log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
225 }
226
227 if !apiResp.Ok {
228 var parameters ResponseParameters
229
230 if apiResp.Parameters != nil {
231 parameters = *apiResp.Parameters
232 }
233
234 return apiResp, Error{
235 Message: apiResp.Description,
236 ResponseParameters: parameters,
237 }
238 }
239
240 return apiResp, nil
241}
242
243// GetFileDirectURL returns direct URL to file
244//
245// It requires the FileID.
246func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) {
247 file, err := bot.GetFile(FileConfig{fileID})
248
249 if err != nil {
250 return "", err
251 }
252
253 return file.Link(bot.Token), nil
254}
255
256// GetMe fetches the currently authenticated bot.
257//
258// This method is called upon creation to validate the token,
259// and so you may get this data from BotAPI.Self without the need for
260// another request.
261func (bot *BotAPI) GetMe() (User, error) {
262 resp, err := bot.MakeRequest("getMe", nil)
263 if err != nil {
264 return User{}, err
265 }
266
267 var user User
268 err = json.Unmarshal(resp.Result, &user)
269
270 return user, err
271}
272
273// IsMessageToMe returns true if message directed to this bot.
274//
275// It requires the Message.
276func (bot *BotAPI) IsMessageToMe(message Message) bool {
277 return strings.Contains(message.Text, "@"+bot.Self.UserName)
278}
279
280// Request sends a Chattable to Telegram, and returns the APIResponse.
281func (bot *BotAPI) Request(c Chattable) (APIResponse, error) {
282 params, err := c.params()
283 if err != nil {
284 return APIResponse{}, err
285 }
286
287 switch t := c.(type) {
288 case Fileable:
289 if t.useExistingFile() {
290 return bot.MakeRequest(t.method(), params)
291 }
292
293 return bot.UploadFile(t.method(), params, t.name(), t.getFile())
294 default:
295 return bot.MakeRequest(c.method(), params)
296 }
297}
298
299// Send will send a Chattable item to Telegram and provides the
300// returned Message.
301func (bot *BotAPI) Send(c Chattable) (Message, error) {
302 resp, err := bot.Request(c)
303 if err != nil {
304 return Message{}, err
305 }
306
307 var message Message
308 err = json.Unmarshal(resp.Result, &message)
309
310 return message, err
311}
312
313// GetUserProfilePhotos gets a user's profile photos.
314//
315// It requires UserID.
316// Offset and Limit are optional.
317func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
318 params, _ := config.params()
319
320 resp, err := bot.MakeRequest(config.method(), params)
321 if err != nil {
322 return UserProfilePhotos{}, err
323 }
324
325 var profilePhotos UserProfilePhotos
326 err = json.Unmarshal(resp.Result, &profilePhotos)
327
328 return profilePhotos, err
329}
330
331// GetFile returns a File which can download a file from Telegram.
332//
333// Requires FileID.
334func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
335 params, _ := config.params()
336
337 resp, err := bot.MakeRequest(config.method(), params)
338 if err != nil {
339 return File{}, err
340 }
341
342 var file File
343 err = json.Unmarshal(resp.Result, &file)
344
345 return file, err
346}
347
348// GetUpdates fetches updates.
349// If a WebHook is set, this will not return any data!
350//
351// Offset, Limit, and Timeout are optional.
352// To avoid stale items, set Offset to one higher than the previous item.
353// Set Timeout to a large number to reduce requests so you can get updates
354// instantly instead of having to wait between requests.
355func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
356 params, _ := config.params()
357
358 resp, err := bot.MakeRequest(config.method(), params)
359 if err != nil {
360 return []Update{}, err
361 }
362
363 var updates []Update
364 err = json.Unmarshal(resp.Result, &updates)
365
366 return updates, err
367}
368
369// GetWebhookInfo allows you to fetch information about a webhook and if
370// one currently is set, along with pending update count and error messages.
371func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) {
372 resp, err := bot.MakeRequest("getWebhookInfo", nil)
373 if err != nil {
374 return WebhookInfo{}, err
375 }
376
377 var info WebhookInfo
378 err = json.Unmarshal(resp.Result, &info)
379
380 return info, err
381}
382
383// GetUpdatesChan starts and returns a channel for getting updates.
384func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) UpdatesChannel {
385 ch := make(chan Update, bot.Buffer)
386
387 go func() {
388 for {
389 select {
390 case <-bot.shutdownChannel:
391 return
392 default:
393 }
394
395 updates, err := bot.GetUpdates(config)
396 if err != nil {
397 log.Println(err)
398 log.Println("Failed to get updates, retrying in 3 seconds...")
399 time.Sleep(time.Second * 3)
400
401 continue
402 }
403
404 for _, update := range updates {
405 if update.UpdateID >= config.Offset {
406 config.Offset = update.UpdateID + 1
407 ch <- update
408 }
409 }
410 }
411 }()
412
413 return ch
414}
415
416// StopReceivingUpdates stops the go routine which receives updates
417func (bot *BotAPI) StopReceivingUpdates() {
418 if bot.Debug {
419 log.Println("Stopping the update receiver routine...")
420 }
421 close(bot.shutdownChannel)
422}
423
424// ListenForWebhook registers a http handler for a webhook.
425func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
426 ch := make(chan Update, bot.Buffer)
427
428 http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
429 bytes, _ := ioutil.ReadAll(r.Body)
430
431 var update Update
432 json.Unmarshal(bytes, &update)
433
434 ch <- update
435 })
436
437 return ch
438}
439
440// GetChat gets information about a chat.
441func (bot *BotAPI) GetChat(config ChatInfoConfig) (Chat, error) {
442 params, _ := config.params()
443
444 resp, err := bot.MakeRequest(config.method(), params)
445 if err != nil {
446 return Chat{}, err
447 }
448
449 var chat Chat
450 err = json.Unmarshal(resp.Result, &chat)
451
452 return chat, err
453}
454
455// GetChatAdministrators gets a list of administrators in the chat.
456//
457// If none have been appointed, only the creator will be returned.
458// Bots are not shown, even if they are an administrator.
459func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]ChatMember, error) {
460 params, _ := config.params()
461
462 resp, err := bot.MakeRequest(config.method(), params)
463 if err != nil {
464 return []ChatMember{}, err
465 }
466
467 var members []ChatMember
468 err = json.Unmarshal(resp.Result, &members)
469
470 return members, err
471}
472
473// GetChatMembersCount gets the number of users in a chat.
474func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) {
475 params, _ := config.params()
476
477 resp, err := bot.MakeRequest(config.method(), params)
478 if err != nil {
479 return -1, err
480 }
481
482 var count int
483 err = json.Unmarshal(resp.Result, &count)
484
485 return count, err
486}
487
488// GetChatMember gets a specific chat member.
489func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) {
490 params, _ := config.params()
491
492 resp, err := bot.MakeRequest(config.method(), params)
493 if err != nil {
494 return ChatMember{}, err
495 }
496
497 var member ChatMember
498 err = json.Unmarshal(resp.Result, &member)
499
500 return member, err
501}
502
503// GetGameHighScores allows you to get the high scores for a game.
504func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
505 params, _ := config.params()
506
507 resp, err := bot.MakeRequest(config.method(), params)
508 if err != nil {
509 return []GameHighScore{}, err
510 }
511
512 var highScores []GameHighScore
513 err = json.Unmarshal(resp.Result, &highScores)
514
515 return highScores, err
516}
517
518// GetInviteLink get InviteLink for a chat
519func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) {
520 params, _ := config.params()
521
522 resp, err := bot.MakeRequest(config.method(), params)
523 if err != nil {
524 return "", err
525 }
526
527 var inviteLink string
528 err = json.Unmarshal(resp.Result, &inviteLink)
529
530 return inviteLink, err
531}
532
533// GetStickerSet returns a StickerSet.
534func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) {
535 params, _ := config.params()
536
537 resp, err := bot.MakeRequest(config.method(), params)
538 if err != nil {
539 return StickerSet{}, nil
540 }
541
542 var stickers StickerSet
543 err = json.Unmarshal(resp.Result, &stickers)
544
545 return stickers, err
546}