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