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 "github.com/technoweenie/multipartstreamer"
11 "io/ioutil"
12 "log"
13 "net/http"
14 "net/url"
15 "os"
16 "strconv"
17 "strings"
18 "time"
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 Self User `json:"-"`
26 Client *http.Client `json:"-"`
27}
28
29// NewBotAPI creates a new BotAPI instance.
30//
31// It requires a token, provided by @BotFather on Telegram.
32func NewBotAPI(token string) (*BotAPI, error) {
33 return NewBotAPIWithClient(token, &http.Client{})
34}
35
36// NewBotAPIWithClient creates a new BotAPI instance
37// and allows you to pass a http.Client.
38//
39// It requires a token, provided by @BotFather on Telegram.
40func NewBotAPIWithClient(token string, client *http.Client) (*BotAPI, error) {
41 bot := &BotAPI{
42 Token: token,
43 Client: client,
44 }
45
46 self, err := bot.GetMe()
47 if err != nil {
48 return &BotAPI{}, err
49 }
50
51 bot.Self = self
52
53 return bot, nil
54}
55
56// MakeRequest makes a request to a specific endpoint with our token.
57func (bot *BotAPI) MakeRequest(endpoint string, params url.Values) (APIResponse, error) {
58 method := fmt.Sprintf(APIEndpoint, bot.Token, endpoint)
59
60 resp, err := bot.Client.PostForm(method, params)
61 if err != nil {
62 return APIResponse{}, err
63 }
64 defer resp.Body.Close()
65
66 if resp.StatusCode == http.StatusForbidden {
67 return APIResponse{}, errors.New(APIForbidden)
68 }
69
70 bytes, err := ioutil.ReadAll(resp.Body)
71 if err != nil {
72 return APIResponse{}, err
73 }
74
75 if bot.Debug {
76 log.Println(endpoint, string(bytes))
77 }
78
79 var apiResp APIResponse
80 json.Unmarshal(bytes, &apiResp)
81
82 if !apiResp.Ok {
83 return APIResponse{}, errors.New(apiResp.Description)
84 }
85
86 return apiResp, nil
87}
88
89// makeMessageRequest makes a request to a method that returns a Message.
90func (bot *BotAPI) makeMessageRequest(endpoint string, params url.Values) (Message, error) {
91 resp, err := bot.MakeRequest(endpoint, params)
92 if err != nil {
93 return Message{}, err
94 }
95
96 var message Message
97 json.Unmarshal(resp.Result, &message)
98
99 bot.debugLog(endpoint, params, message)
100
101 return message, nil
102}
103
104// UploadFile makes a request to the API with a file.
105//
106// Requires the parameter to hold the file not be in the params.
107// File should be a string to a file path, a FileBytes struct,
108// or a FileReader struct.
109//
110// Note that if your FileReader has a size set to -1, it will read
111// the file into memory to calculate a size.
112func (bot *BotAPI) UploadFile(endpoint string, params map[string]string, fieldname string, file interface{}) (APIResponse, error) {
113 ms := multipartstreamer.New()
114 ms.WriteFields(params)
115
116 switch f := file.(type) {
117 case string:
118 fileHandle, err := os.Open(f)
119 if err != nil {
120 return APIResponse{}, err
121 }
122 defer fileHandle.Close()
123
124 fi, err := os.Stat(f)
125 if err != nil {
126 return APIResponse{}, err
127 }
128
129 ms.WriteReader(fieldname, fileHandle.Name(), fi.Size(), fileHandle)
130 case FileBytes:
131 buf := bytes.NewBuffer(f.Bytes)
132 ms.WriteReader(fieldname, f.Name, int64(len(f.Bytes)), buf)
133 case FileReader:
134 if f.Size != -1 {
135 ms.WriteReader(fieldname, f.Name, f.Size, f.Reader)
136
137 break
138 }
139
140 data, err := ioutil.ReadAll(f.Reader)
141 if err != nil {
142 return APIResponse{}, err
143 }
144
145 buf := bytes.NewBuffer(data)
146
147 ms.WriteReader(fieldname, f.Name, int64(len(data)), buf)
148 default:
149 return APIResponse{}, errors.New("bad file type")
150 }
151
152 method := fmt.Sprintf(APIEndpoint, bot.Token, endpoint)
153
154 req, err := http.NewRequest("POST", method, nil)
155 if err != nil {
156 return APIResponse{}, err
157 }
158
159 ms.SetupRequest(req)
160
161 res, err := bot.Client.Do(req)
162 if err != nil {
163 return APIResponse{}, err
164 }
165 defer res.Body.Close()
166
167 bytes, err := ioutil.ReadAll(res.Body)
168 if err != nil {
169 return APIResponse{}, err
170 }
171
172 if bot.Debug {
173 log.Println(string(bytes))
174 }
175
176 var apiResp APIResponse
177 json.Unmarshal(bytes, &apiResp)
178
179 if !apiResp.Ok {
180 return APIResponse{}, errors.New(apiResp.Description)
181 }
182
183 return apiResp, nil
184}
185
186// GetFileDirectURL returns direct URL to file
187//
188// It requires the FileID.
189func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) {
190 file, err := bot.GetFile(FileConfig{fileID})
191
192 if err != nil {
193 return "", err
194 }
195
196 return file.Link(bot.Token), nil
197}
198
199// GetMe fetches the currently authenticated bot.
200//
201// This method is called upon creation to validate the token,
202// and so you may get this data from BotAPI.Self without the need for
203// another request.
204func (bot *BotAPI) GetMe() (User, error) {
205 resp, err := bot.MakeRequest("getMe", nil)
206 if err != nil {
207 return User{}, err
208 }
209
210 var user User
211 json.Unmarshal(resp.Result, &user)
212
213 bot.debugLog("getMe", nil, user)
214
215 return user, nil
216}
217
218// IsMessageToMe returns true if message directed to this bot.
219//
220// It requires the Message.
221func (bot *BotAPI) IsMessageToMe(message Message) bool {
222 return strings.Contains(message.Text, "@"+bot.Self.UserName)
223}
224
225// Send will send a Chattable item to Telegram.
226//
227// It requires the Chattable to send.
228func (bot *BotAPI) Send(c Chattable) (Message, error) {
229 switch c.(type) {
230 case Fileable:
231 return bot.sendFile(c.(Fileable))
232 default:
233 return bot.sendChattable(c)
234 }
235}
236
237// debugLog checks if the bot is currently running in debug mode, and if
238// so will display information about the request and response in the
239// debug log.
240func (bot *BotAPI) debugLog(context string, v url.Values, message interface{}) {
241 if bot.Debug {
242 log.Printf("%s req : %+v\n", context, v)
243 log.Printf("%s resp: %+v\n", context, message)
244 }
245}
246
247// sendExisting will send a Message with an existing file to Telegram.
248func (bot *BotAPI) sendExisting(method string, config Fileable) (Message, error) {
249 v, err := config.values()
250
251 if err != nil {
252 return Message{}, err
253 }
254
255 message, err := bot.makeMessageRequest(method, v)
256 if err != nil {
257 return Message{}, err
258 }
259
260 return message, nil
261}
262
263// uploadAndSend will send a Message with a new file to Telegram.
264func (bot *BotAPI) uploadAndSend(method string, config Fileable) (Message, error) {
265 params, err := config.params()
266 if err != nil {
267 return Message{}, err
268 }
269
270 file := config.getFile()
271
272 resp, err := bot.UploadFile(method, params, config.name(), file)
273 if err != nil {
274 return Message{}, err
275 }
276
277 var message Message
278 json.Unmarshal(resp.Result, &message)
279
280 bot.debugLog(method, nil, message)
281
282 return message, nil
283}
284
285// sendFile determines if the file is using an existing file or uploading
286// a new file, then sends it as needed.
287func (bot *BotAPI) sendFile(config Fileable) (Message, error) {
288 if config.useExistingFile() {
289 return bot.sendExisting(config.method(), config)
290 }
291
292 return bot.uploadAndSend(config.method(), config)
293}
294
295// sendChattable sends a Chattable.
296func (bot *BotAPI) sendChattable(config Chattable) (Message, error) {
297 v, err := config.values()
298 if err != nil {
299 return Message{}, err
300 }
301
302 message, err := bot.makeMessageRequest(config.method(), v)
303
304 if err != nil {
305 return Message{}, err
306 }
307
308 return message, nil
309}
310
311// GetUserProfilePhotos gets a user's profile photos.
312//
313// It requires UserID.
314// Offset and Limit are optional.
315func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
316 v := url.Values{}
317 v.Add("user_id", strconv.Itoa(config.UserID))
318 if config.Offset != 0 {
319 v.Add("offset", strconv.Itoa(config.Offset))
320 }
321 if config.Limit != 0 {
322 v.Add("limit", strconv.Itoa(config.Limit))
323 }
324
325 resp, err := bot.MakeRequest("getUserProfilePhotos", v)
326 if err != nil {
327 return UserProfilePhotos{}, err
328 }
329
330 var profilePhotos UserProfilePhotos
331 json.Unmarshal(resp.Result, &profilePhotos)
332
333 bot.debugLog("GetUserProfilePhoto", v, profilePhotos)
334
335 return profilePhotos, nil
336}
337
338// GetFile returns a File which can download a file from Telegram.
339//
340// Requires FileID.
341func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
342 v := url.Values{}
343 v.Add("file_id", config.FileID)
344
345 resp, err := bot.MakeRequest("getFile", v)
346 if err != nil {
347 return File{}, err
348 }
349
350 var file File
351 json.Unmarshal(resp.Result, &file)
352
353 bot.debugLog("GetFile", v, file)
354
355 return file, nil
356}
357
358// GetUpdates fetches updates.
359// If a WebHook is set, this will not return any data!
360//
361// Offset, Limit, and Timeout are optional.
362// To avoid stale items, set Offset to one higher than the previous item.
363// Set Timeout to a large number to reduce requests so you can get updates
364// instantly instead of having to wait between requests.
365func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
366 v := url.Values{}
367 if config.Offset > 0 {
368 v.Add("offset", strconv.Itoa(config.Offset))
369 }
370 if config.Limit > 0 {
371 v.Add("limit", strconv.Itoa(config.Limit))
372 }
373 if config.Timeout > 0 {
374 v.Add("timeout", strconv.Itoa(config.Timeout))
375 }
376
377 resp, err := bot.MakeRequest("getUpdates", v)
378 if err != nil {
379 return []Update{}, err
380 }
381
382 var updates []Update
383 json.Unmarshal(resp.Result, &updates)
384
385 bot.debugLog("getUpdates", v, updates)
386
387 return updates, nil
388}
389
390// RemoveWebhook unsets the webhook.
391func (bot *BotAPI) RemoveWebhook() (APIResponse, error) {
392 return bot.MakeRequest("setWebhook", url.Values{})
393}
394
395// SetWebhook sets a webhook.
396//
397// If this is set, GetUpdates will not get any data!
398//
399// If you do not have a legitmate TLS certificate, you need to include
400// your self signed certificate with the config.
401func (bot *BotAPI) SetWebhook(config WebhookConfig) (APIResponse, error) {
402 if config.Certificate == nil {
403 v := url.Values{}
404 v.Add("url", config.URL.String())
405
406 return bot.MakeRequest("setWebhook", v)
407 }
408
409 params := make(map[string]string)
410 params["url"] = config.URL.String()
411
412 resp, err := bot.UploadFile("setWebhook", params, "certificate", config.Certificate)
413 if err != nil {
414 return APIResponse{}, err
415 }
416
417 var apiResp APIResponse
418 json.Unmarshal(resp.Result, &apiResp)
419
420 if bot.Debug {
421 log.Printf("setWebhook resp: %+v\n", apiResp)
422 }
423
424 return apiResp, nil
425}
426
427// GetUpdatesChan starts and returns a channel for getting updates.
428func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (<-chan Update, error) {
429 updatesChan := make(chan Update, 100)
430
431 go func() {
432 for {
433 updates, err := bot.GetUpdates(config)
434 if err != nil {
435 log.Println(err)
436 log.Println("Failed to get updates, retrying in 3 seconds...")
437 time.Sleep(time.Second * 3)
438
439 continue
440 }
441
442 for _, update := range updates {
443 if update.UpdateID >= config.Offset {
444 config.Offset = update.UpdateID + 1
445 updatesChan <- update
446 }
447 }
448 }
449 }()
450
451 return updatesChan, nil
452}
453
454// ListenForWebhook registers a http handler for a webhook.
455func (bot *BotAPI) ListenForWebhook(pattern string) (<-chan Update, http.Handler) {
456 updatesChan := make(chan Update, 100)
457
458 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
459 bytes, _ := ioutil.ReadAll(r.Body)
460
461 var update Update
462 json.Unmarshal(bytes, &update)
463
464 updatesChan <- update
465 })
466
467 http.HandleFunc(pattern, handler)
468
469 return updatesChan, handler
470}
471
472// AnswerInlineQuery sends a response to an inline query.
473//
474// Note that you must respond to an inline query within 30 seconds.
475func (bot *BotAPI) AnswerInlineQuery(config InlineConfig) (APIResponse, error) {
476 v := url.Values{}
477
478 v.Add("inline_query_id", config.InlineQueryID)
479 v.Add("cache_time", strconv.Itoa(config.CacheTime))
480 v.Add("is_personal", strconv.FormatBool(config.IsPersonal))
481 v.Add("next_offset", config.NextOffset)
482 data, err := json.Marshal(config.Results)
483 if err != nil {
484 return APIResponse{}, err
485 }
486 v.Add("results", string(data))
487
488 return bot.MakeRequest("answerInlineQuery", v)
489}