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