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 ch <- update
415 }
416 }
417 }()
418
419 return ch, nil
420}
421
422// StopReceivingUpdates stops the go routine which receives updates
423func (bot *BotAPI) StopReceivingUpdates() {
424 if bot.Debug {
425 log.Println("Stopping the update receiver routine...")
426 }
427 close(bot.shutdownChannel)
428}
429
430// ListenForWebhook registers a http handler for a webhook.
431func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
432 ch := make(chan Update, bot.Buffer)
433
434 http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
435 bytes, _ := ioutil.ReadAll(r.Body)
436
437 var update Update
438 json.Unmarshal(bytes, &update)
439
440 ch <- update
441 })
442
443 return ch
444}
445
446// GetChat gets information about a chat.
447func (bot *BotAPI) GetChat(config ChatConfig) (Chat, error) {
448 v := url.Values{}
449
450 if config.SuperGroupUsername == "" {
451 v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
452 } else {
453 v.Add("chat_id", config.SuperGroupUsername)
454 }
455
456 resp, err := bot.MakeRequest("getChat", v)
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 ChatConfig) ([]ChatMember, error) {
472 v := url.Values{}
473
474 if config.SuperGroupUsername == "" {
475 v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
476 } else {
477 v.Add("chat_id", config.SuperGroupUsername)
478 }
479
480 resp, err := bot.MakeRequest("getChatAdministrators", v)
481 if err != nil {
482 return []ChatMember{}, err
483 }
484
485 var members []ChatMember
486 err = json.Unmarshal(resp.Result, &members)
487
488 return members, err
489}
490
491// GetChatMembersCount gets the number of users in a chat.
492func (bot *BotAPI) GetChatMembersCount(config ChatConfig) (int, error) {
493 v := url.Values{}
494
495 if config.SuperGroupUsername == "" {
496 v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
497 } else {
498 v.Add("chat_id", config.SuperGroupUsername)
499 }
500
501 resp, err := bot.MakeRequest("getChatMembersCount", v)
502 if err != nil {
503 return -1, err
504 }
505
506 var count int
507 err = json.Unmarshal(resp.Result, &count)
508
509 return count, err
510}
511
512// GetChatMember gets a specific chat member.
513func (bot *BotAPI) GetChatMember(config ChatConfigWithUser) (ChatMember, error) {
514 v := url.Values{}
515
516 if config.SuperGroupUsername == "" {
517 v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
518 } else {
519 v.Add("chat_id", config.SuperGroupUsername)
520 }
521 v.Add("user_id", strconv.Itoa(config.UserID))
522
523 resp, err := bot.MakeRequest("getChatMember", v)
524 if err != nil {
525 return ChatMember{}, err
526 }
527
528 var member ChatMember
529 err = json.Unmarshal(resp.Result, &member)
530
531 return member, err
532}
533
534// UnbanChatMember unbans a user from a chat. Note that this only will work
535// in supergroups and channels, and requires the bot to be an admin.
536func (bot *BotAPI) UnbanChatMember(config ChatMemberConfig) (APIResponse, error) {
537 v := url.Values{}
538
539 if config.SuperGroupUsername != "" {
540 v.Add("chat_id", config.SuperGroupUsername)
541 } else if config.ChannelUsername != "" {
542 v.Add("chat_id", config.ChannelUsername)
543 } else {
544 v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
545 }
546 v.Add("user_id", strconv.Itoa(config.UserID))
547
548 return bot.MakeRequest("unbanChatMember", v)
549}
550
551// RestrictChatMember to restrict a user in a supergroup. The bot must be an
552//administrator in the supergroup for this to work and must have the
553//appropriate admin rights. Pass True for all boolean parameters to lift
554//restrictions from a user. Returns True on success.
555func (bot *BotAPI) RestrictChatMember(config RestrictChatMemberConfig) (APIResponse, error) {
556 v := url.Values{}
557
558 if config.SuperGroupUsername != "" {
559 v.Add("chat_id", config.SuperGroupUsername)
560 } else if config.ChannelUsername != "" {
561 v.Add("chat_id", config.ChannelUsername)
562 } else {
563 v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
564 }
565 v.Add("user_id", strconv.Itoa(config.UserID))
566
567 if config.CanSendMessages != nil {
568 v.Add("can_send_messages", strconv.FormatBool(*config.CanSendMessages))
569 }
570 if config.CanSendMediaMessages != nil {
571 v.Add("can_send_media_messages", strconv.FormatBool(*config.CanSendMediaMessages))
572 }
573 if config.CanSendOtherMessages != nil {
574 v.Add("can_send_other_messages", strconv.FormatBool(*config.CanSendOtherMessages))
575 }
576 if config.CanAddWebPagePreviews != nil {
577 v.Add("can_add_web_page_previews", strconv.FormatBool(*config.CanAddWebPagePreviews))
578 }
579 if config.UntilDate != 0 {
580 v.Add("until_date", strconv.FormatInt(config.UntilDate, 10))
581 }
582
583 return bot.MakeRequest("restrictChatMember", v)
584}
585
586// PromoteChatMember add admin rights to user
587func (bot *BotAPI) PromoteChatMember(config PromoteChatMemberConfig) (APIResponse, error) {
588 v := url.Values{}
589
590 if config.SuperGroupUsername != "" {
591 v.Add("chat_id", config.SuperGroupUsername)
592 } else if config.ChannelUsername != "" {
593 v.Add("chat_id", config.ChannelUsername)
594 } else {
595 v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
596 }
597 v.Add("user_id", strconv.Itoa(config.UserID))
598
599 if config.CanChangeInfo != nil {
600 v.Add("can_change_info", strconv.FormatBool(*config.CanChangeInfo))
601 }
602 if config.CanPostMessages != nil {
603 v.Add("can_post_messages", strconv.FormatBool(*config.CanPostMessages))
604 }
605 if config.CanEditMessages != nil {
606 v.Add("can_edit_messages", strconv.FormatBool(*config.CanEditMessages))
607 }
608 if config.CanDeleteMessages != nil {
609 v.Add("can_delete_messages", strconv.FormatBool(*config.CanDeleteMessages))
610 }
611 if config.CanInviteUsers != nil {
612 v.Add("can_invite_users", strconv.FormatBool(*config.CanInviteUsers))
613 }
614 if config.CanRestrictMembers != nil {
615 v.Add("can_restrict_members", strconv.FormatBool(*config.CanRestrictMembers))
616 }
617 if config.CanPinMessages != nil {
618 v.Add("can_pin_messages", strconv.FormatBool(*config.CanPinMessages))
619 }
620 if config.CanPromoteMembers != nil {
621 v.Add("can_promote_members", strconv.FormatBool(*config.CanPromoteMembers))
622 }
623
624 return bot.MakeRequest("promoteChatMember", v)
625}
626
627// GetGameHighScores allows you to get the high scores for a game.
628func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
629 v, _ := config.values()
630
631 resp, err := bot.MakeRequest(config.method(), v)
632 if err != nil {
633 return []GameHighScore{}, err
634 }
635
636 var highScores []GameHighScore
637 err = json.Unmarshal(resp.Result, &highScores)
638
639 return highScores, err
640}
641
642// GetInviteLink get InviteLink for a chat
643func (bot *BotAPI) GetInviteLink(config ChatConfig) (string, error) {
644 v := url.Values{}
645
646 if config.SuperGroupUsername == "" {
647 v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
648 } else {
649 v.Add("chat_id", config.SuperGroupUsername)
650 }
651
652 resp, err := bot.MakeRequest("exportChatInviteLink", v)
653 if err != nil {
654 return "", err
655 }
656
657 var inviteLink string
658 err = json.Unmarshal(resp.Result, &inviteLink)
659
660 return inviteLink, err
661}
662
663// GetStickerSet returns a StickerSet.
664func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) {
665 v, err := config.values()
666 if err != nil {
667 return StickerSet{}, nil
668 }
669
670 resp, err := bot.MakeRequest(config.method(), v)
671 if err != nil {
672 return StickerSet{}, nil
673 }
674
675 var stickers StickerSet
676 err = json.Unmarshal(resp.Result, &stickers)
677
678 return stickers, err
679}