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 "strconv"
16 "strings"
17 "time"
18
19 "github.com/technoweenie/multipartstreamer"
20)
21
22// BotAPI allows you to interact with the Telegram Bot API.
23type BotAPI struct {
24 Token string `json:"token"`
25 Debug bool `json:"debug"`
26 Buffer int `json:"buffer"`
27
28 Self User `json:"-"`
29 Client *http.Client `json:"-"`
30 shutdownChannel chan interface{}
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
52 self, err := bot.GetMe()
53 if err != nil {
54 return nil, err
55 }
56
57 bot.Self = self
58
59 return bot, nil
60}
61
62// MakeRequest makes a request to a specific endpoint with our token.
63func (bot *BotAPI) MakeRequest(endpoint string, params url.Values) (APIResponse, error) {
64 if bot.Debug {
65 log.Printf("Endpoint: %s, values: %v\n", endpoint, params)
66 }
67
68 method := fmt.Sprintf(APIEndpoint, bot.Token, endpoint)
69
70 resp, err := bot.Client.PostForm(method, params)
71 if err != nil {
72 return APIResponse{}, err
73 }
74 defer resp.Body.Close()
75
76 var apiResp APIResponse
77 bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
78 if err != nil {
79 return apiResp, err
80 }
81
82 if bot.Debug {
83 log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
84 }
85
86 if !apiResp.Ok {
87 var parameters ResponseParameters
88
89 if apiResp.Parameters != nil {
90 parameters = *apiResp.Parameters
91 }
92
93 return apiResp, Error{
94 Message: apiResp.Description,
95 ResponseParameters: parameters,
96 }
97 }
98
99 return apiResp, nil
100}
101
102// decodeAPIResponse decode response and return slice of bytes if debug enabled.
103// If debug disabled, just decode http.Response.Body stream to APIResponse struct
104// for efficient memory usage
105func (bot *BotAPI) decodeAPIResponse(responseBody io.Reader, resp *APIResponse) (_ []byte, err error) {
106 if !bot.Debug {
107 dec := json.NewDecoder(responseBody)
108 err = dec.Decode(resp)
109 return
110 }
111
112 // if debug, read reponse body
113 data, err := ioutil.ReadAll(responseBody)
114 if err != nil {
115 return
116 }
117
118 err = json.Unmarshal(data, resp)
119 if err != nil {
120 return
121 }
122
123 return data, nil
124}
125
126// UploadFile makes a request to the API with a file.
127//
128// Requires the parameter to hold the file not be in the params.
129// File should be a string to a file path, a FileBytes struct,
130// a FileReader struct, or a url.URL.
131//
132// Note that if your FileReader has a size set to -1, it will read
133// the file into memory to calculate a size.
134func (bot *BotAPI) UploadFile(endpoint string, params map[string]string, fieldname string, file interface{}) (APIResponse, error) {
135 ms := multipartstreamer.New()
136
137 switch f := file.(type) {
138 case string:
139 ms.WriteFields(params)
140
141 fileHandle, err := os.Open(f)
142 if err != nil {
143 return APIResponse{}, err
144 }
145 defer fileHandle.Close()
146
147 fi, err := os.Stat(f)
148 if err != nil {
149 return APIResponse{}, err
150 }
151
152 ms.WriteReader(fieldname, fileHandle.Name(), fi.Size(), fileHandle)
153 case FileBytes:
154 ms.WriteFields(params)
155
156 buf := bytes.NewBuffer(f.Bytes)
157 ms.WriteReader(fieldname, f.Name, int64(len(f.Bytes)), buf)
158 case FileReader:
159 ms.WriteFields(params)
160
161 if f.Size != -1 {
162 ms.WriteReader(fieldname, f.Name, f.Size, f.Reader)
163
164 break
165 }
166
167 data, err := ioutil.ReadAll(f.Reader)
168 if err != nil {
169 return APIResponse{}, err
170 }
171
172 buf := bytes.NewBuffer(data)
173
174 ms.WriteReader(fieldname, f.Name, int64(len(data)), buf)
175 case url.URL:
176 params[fieldname] = f.String()
177
178 ms.WriteFields(params)
179 default:
180 return APIResponse{}, errors.New(ErrBadFileType)
181 }
182
183 if bot.Debug {
184 log.Printf("Endpoint: %s, fieldname: %s, params: %v, file: %T\n", endpoint, fieldname, params, file)
185 }
186
187 method := fmt.Sprintf(APIEndpoint, bot.Token, endpoint)
188
189 req, err := http.NewRequest("POST", method, nil)
190 if err != nil {
191 return APIResponse{}, err
192 }
193
194 ms.SetupRequest(req)
195
196 res, err := bot.Client.Do(req)
197 if err != nil {
198 return APIResponse{}, err
199 }
200 defer res.Body.Close()
201
202 bytes, err := ioutil.ReadAll(res.Body)
203 if err != nil {
204 return APIResponse{}, err
205 }
206
207 if bot.Debug {
208 log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
209 }
210
211 var apiResp APIResponse
212
213 err = json.Unmarshal(bytes, &apiResp)
214 if err != nil {
215 return APIResponse{}, err
216 }
217
218 if !apiResp.Ok {
219 return APIResponse{}, errors.New(apiResp.Description)
220 }
221
222 return apiResp, nil
223}
224
225// GetFileDirectURL returns direct URL to file
226//
227// It requires the FileID.
228func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) {
229 file, err := bot.GetFile(FileConfig{fileID})
230
231 if err != nil {
232 return "", err
233 }
234
235 return file.Link(bot.Token), nil
236}
237
238// GetMe fetches the currently authenticated bot.
239//
240// This method is called upon creation to validate the token,
241// and so you may get this data from BotAPI.Self without the need for
242// another request.
243func (bot *BotAPI) GetMe() (User, error) {
244 resp, err := bot.MakeRequest("getMe", nil)
245 if err != nil {
246 return User{}, err
247 }
248
249 var user User
250 err = json.Unmarshal(resp.Result, &user)
251
252 return user, err
253}
254
255// IsMessageToMe returns true if message directed to this bot.
256//
257// It requires the Message.
258func (bot *BotAPI) IsMessageToMe(message Message) bool {
259 return strings.Contains(message.Text, "@"+bot.Self.UserName)
260}
261
262// Request sends a Chattable to Telegram, and returns the APIResponse.
263func (bot *BotAPI) Request(c Chattable) (APIResponse, error) {
264 switch t := c.(type) {
265 case Fileable:
266 if t.useExistingFile() {
267 v, err := t.values()
268 if err != nil {
269 return APIResponse{}, err
270 }
271
272 return bot.MakeRequest(t.method(), v)
273 }
274
275 p, err := t.params()
276 if err != nil {
277 return APIResponse{}, err
278 }
279
280 return bot.UploadFile(t.method(), p, t.name(), t.getFile())
281 default:
282 v, err := c.values()
283 if err != nil {
284 return APIResponse{}, err
285 }
286
287 return bot.MakeRequest(c.method(), v)
288 }
289}
290
291// Send will send a Chattable item to Telegram and provides the
292// returned Message.
293func (bot *BotAPI) Send(c Chattable) (Message, error) {
294 resp, err := bot.Request(c)
295 if err != nil {
296 return Message{}, err
297 }
298
299 var message Message
300 err = json.Unmarshal(resp.Result, &message)
301
302 return message, err
303}
304
305// GetUserProfilePhotos gets a user's profile photos.
306//
307// It requires UserID.
308// Offset and Limit are optional.
309func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
310 v := url.Values{}
311 v.Add("user_id", strconv.Itoa(config.UserID))
312 if config.Offset != 0 {
313 v.Add("offset", strconv.Itoa(config.Offset))
314 }
315 if config.Limit != 0 {
316 v.Add("limit", strconv.Itoa(config.Limit))
317 }
318
319 resp, err := bot.MakeRequest("getUserProfilePhotos", v)
320 if err != nil {
321 return UserProfilePhotos{}, err
322 }
323
324 var profilePhotos UserProfilePhotos
325 err = json.Unmarshal(resp.Result, &profilePhotos)
326
327 return profilePhotos, err
328}
329
330// GetFile returns a File which can download a file from Telegram.
331//
332// Requires FileID.
333func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
334 v := url.Values{}
335 v.Add("file_id", config.FileID)
336
337 resp, err := bot.MakeRequest("getFile", v)
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 v := url.Values{}
357 if config.Offset != 0 {
358 v.Add("offset", strconv.Itoa(config.Offset))
359 }
360 if config.Limit > 0 {
361 v.Add("limit", strconv.Itoa(config.Limit))
362 }
363 if config.Timeout > 0 {
364 v.Add("timeout", strconv.Itoa(config.Timeout))
365 }
366
367 resp, err := bot.MakeRequest("getUpdates", v)
368 if err != nil {
369 return []Update{}, err
370 }
371
372 var updates []Update
373 err = json.Unmarshal(resp.Result, &updates)
374
375 return updates, err
376}
377
378// GetWebhookInfo allows you to fetch information about a webhook and if
379// one currently is set, along with pending update count and error messages.
380func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) {
381 resp, err := bot.MakeRequest("getWebhookInfo", url.Values{})
382 if err != nil {
383 return WebhookInfo{}, err
384 }
385
386 var info WebhookInfo
387 err = json.Unmarshal(resp.Result, &info)
388
389 return info, err
390}
391
392// GetUpdatesChan starts and returns a channel for getting updates.
393func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (UpdatesChannel, error) {
394 ch := make(chan Update, bot.Buffer)
395
396 go func() {
397 for {
398 select {
399 case <-bot.shutdownChannel:
400 return
401 default:
402 }
403
404 updates, err := bot.GetUpdates(config)
405 if err != nil {
406 log.Println(err)
407 log.Println("Failed to get updates, retrying in 3 seconds...")
408 time.Sleep(time.Second * 3)
409
410 continue
411 }
412
413 for _, update := range updates {
414 if update.UpdateID >= config.Offset {
415 config.Offset = update.UpdateID + 1
416 ch <- update
417 }
418 }
419 }
420 }()
421
422 return ch, nil
423}
424
425// StopReceivingUpdates stops the go routine which receives updates
426func (bot *BotAPI) StopReceivingUpdates() {
427 if bot.Debug {
428 log.Println("Stopping the update receiver routine...")
429 }
430 close(bot.shutdownChannel)
431}
432
433// ListenForWebhook registers a http handler for a webhook.
434func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
435 ch := make(chan Update, bot.Buffer)
436
437 http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
438 bytes, _ := ioutil.ReadAll(r.Body)
439
440 var update Update
441 json.Unmarshal(bytes, &update)
442
443 ch <- update
444 })
445
446 return ch
447}
448
449// GetChat gets information about a chat.
450func (bot *BotAPI) GetChat(config ChatConfig) (Chat, error) {
451 v := url.Values{}
452
453 if config.SuperGroupUsername == "" {
454 v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
455 } else {
456 v.Add("chat_id", config.SuperGroupUsername)
457 }
458
459 resp, err := bot.MakeRequest("getChat", v)
460 if err != nil {
461 return Chat{}, err
462 }
463
464 var chat Chat
465 err = json.Unmarshal(resp.Result, &chat)
466
467 return chat, err
468}
469
470// GetChatAdministrators gets a list of administrators in the chat.
471//
472// If none have been appointed, only the creator will be returned.
473// Bots are not shown, even if they are an administrator.
474func (bot *BotAPI) GetChatAdministrators(config ChatConfig) ([]ChatMember, error) {
475 v := url.Values{}
476
477 if config.SuperGroupUsername == "" {
478 v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
479 } else {
480 v.Add("chat_id", config.SuperGroupUsername)
481 }
482
483 resp, err := bot.MakeRequest("getChatAdministrators", v)
484 if err != nil {
485 return []ChatMember{}, err
486 }
487
488 var members []ChatMember
489 err = json.Unmarshal(resp.Result, &members)
490
491 return members, err
492}
493
494// GetChatMembersCount gets the number of users in a chat.
495func (bot *BotAPI) GetChatMembersCount(config ChatConfig) (int, error) {
496 v := url.Values{}
497
498 if config.SuperGroupUsername == "" {
499 v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
500 } else {
501 v.Add("chat_id", config.SuperGroupUsername)
502 }
503
504 resp, err := bot.MakeRequest("getChatMembersCount", v)
505 if err != nil {
506 return -1, err
507 }
508
509 var count int
510 err = json.Unmarshal(resp.Result, &count)
511
512 return count, err
513}
514
515// GetChatMember gets a specific chat member.
516func (bot *BotAPI) GetChatMember(config ChatConfigWithUser) (ChatMember, error) {
517 v := url.Values{}
518
519 if config.SuperGroupUsername == "" {
520 v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
521 } else {
522 v.Add("chat_id", config.SuperGroupUsername)
523 }
524 v.Add("user_id", strconv.Itoa(config.UserID))
525
526 resp, err := bot.MakeRequest("getChatMember", v)
527 if err != nil {
528 return ChatMember{}, err
529 }
530
531 var member ChatMember
532 err = json.Unmarshal(resp.Result, &member)
533
534 return member, err
535}
536
537// UnbanChatMember unbans a user from a chat. Note that this only will work
538// in supergroups and channels, and requires the bot to be an admin.
539func (bot *BotAPI) UnbanChatMember(config ChatMemberConfig) (APIResponse, error) {
540 v := url.Values{}
541
542 if config.SuperGroupUsername != "" {
543 v.Add("chat_id", config.SuperGroupUsername)
544 } else if config.ChannelUsername != "" {
545 v.Add("chat_id", config.ChannelUsername)
546 } else {
547 v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
548 }
549 v.Add("user_id", strconv.Itoa(config.UserID))
550
551 return bot.MakeRequest("unbanChatMember", v)
552}
553
554// RestrictChatMember to restrict a user in a supergroup. The bot must be an
555//administrator in the supergroup for this to work and must have the
556//appropriate admin rights. Pass True for all boolean parameters to lift
557//restrictions from a user. Returns True on success.
558func (bot *BotAPI) RestrictChatMember(config RestrictChatMemberConfig) (APIResponse, error) {
559 v := url.Values{}
560
561 if config.SuperGroupUsername != "" {
562 v.Add("chat_id", config.SuperGroupUsername)
563 } else if config.ChannelUsername != "" {
564 v.Add("chat_id", config.ChannelUsername)
565 } else {
566 v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
567 }
568 v.Add("user_id", strconv.Itoa(config.UserID))
569
570 if config.CanSendMessages != nil {
571 v.Add("can_send_messages", strconv.FormatBool(*config.CanSendMessages))
572 }
573 if config.CanSendMediaMessages != nil {
574 v.Add("can_send_media_messages", strconv.FormatBool(*config.CanSendMediaMessages))
575 }
576 if config.CanSendOtherMessages != nil {
577 v.Add("can_send_other_messages", strconv.FormatBool(*config.CanSendOtherMessages))
578 }
579 if config.CanAddWebPagePreviews != nil {
580 v.Add("can_add_web_page_previews", strconv.FormatBool(*config.CanAddWebPagePreviews))
581 }
582 if config.UntilDate != 0 {
583 v.Add("until_date", strconv.FormatInt(config.UntilDate, 10))
584 }
585
586 return bot.MakeRequest("restrictChatMember", v)
587}
588
589// PromoteChatMember add admin rights to user
590func (bot *BotAPI) PromoteChatMember(config PromoteChatMemberConfig) (APIResponse, error) {
591 v := url.Values{}
592
593 if config.SuperGroupUsername != "" {
594 v.Add("chat_id", config.SuperGroupUsername)
595 } else if config.ChannelUsername != "" {
596 v.Add("chat_id", config.ChannelUsername)
597 } else {
598 v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
599 }
600 v.Add("user_id", strconv.Itoa(config.UserID))
601
602 if config.CanChangeInfo != nil {
603 v.Add("can_change_info", strconv.FormatBool(*config.CanChangeInfo))
604 }
605 if config.CanPostMessages != nil {
606 v.Add("can_post_messages", strconv.FormatBool(*config.CanPostMessages))
607 }
608 if config.CanEditMessages != nil {
609 v.Add("can_edit_messages", strconv.FormatBool(*config.CanEditMessages))
610 }
611 if config.CanDeleteMessages != nil {
612 v.Add("can_delete_messages", strconv.FormatBool(*config.CanDeleteMessages))
613 }
614 if config.CanInviteUsers != nil {
615 v.Add("can_invite_users", strconv.FormatBool(*config.CanInviteUsers))
616 }
617 if config.CanRestrictMembers != nil {
618 v.Add("can_restrict_members", strconv.FormatBool(*config.CanRestrictMembers))
619 }
620 if config.CanPinMessages != nil {
621 v.Add("can_pin_messages", strconv.FormatBool(*config.CanPinMessages))
622 }
623 if config.CanPromoteMembers != nil {
624 v.Add("can_promote_members", strconv.FormatBool(*config.CanPromoteMembers))
625 }
626
627 return bot.MakeRequest("promoteChatMember", v)
628}
629
630// GetGameHighScores allows you to get the high scores for a game.
631func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
632 v, _ := config.values()
633
634 resp, err := bot.MakeRequest(config.method(), v)
635 if err != nil {
636 return []GameHighScore{}, err
637 }
638
639 var highScores []GameHighScore
640 err = json.Unmarshal(resp.Result, &highScores)
641
642 return highScores, err
643}
644
645// GetInviteLink get InviteLink for a chat
646func (bot *BotAPI) GetInviteLink(config ChatConfig) (string, error) {
647 v := url.Values{}
648
649 if config.SuperGroupUsername == "" {
650 v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
651 } else {
652 v.Add("chat_id", config.SuperGroupUsername)
653 }
654
655 resp, err := bot.MakeRequest("exportChatInviteLink", v)
656 if err != nil {
657 return "", err
658 }
659
660 var inviteLink string
661 err = json.Unmarshal(resp.Result, &inviteLink)
662
663 return inviteLink, err
664}
665
666// GetStickerSet returns a StickerSet.
667func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) {
668 v, err := config.values()
669 if err != nil {
670 return StickerSet{}, nil
671 }
672
673 resp, err := bot.MakeRequest(config.method(), v)
674 if err != nil {
675 return StickerSet{}, nil
676 }
677
678 var stickers StickerSet
679 err = json.Unmarshal(resp.Result, &stickers)
680
681 return stickers, err
682}