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