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 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 data, err := ioutil.ReadAll(f.Reader)
126 if err != nil {
127 return APIResponse{}, err
128 }
129 buf := bytes.NewBuffer(data)
130
131 ms.WriteReader(fieldname, f.Name, int64(len(data)), buf)
132
133 break
134 }
135
136 ms.WriteReader(fieldname, f.Name, f.Size, f.Reader)
137 default:
138 return APIResponse{}, errors.New("bad file type")
139 }
140
141 req, err := http.NewRequest("POST", fmt.Sprintf(APIEndpoint, bot.Token, endpoint), nil)
142 ms.SetupRequest(req)
143 if err != nil {
144 return APIResponse{}, err
145 }
146
147 res, err := bot.Client.Do(req)
148 if err != nil {
149 return APIResponse{}, err
150 }
151 defer res.Body.Close()
152
153 bytes, err := ioutil.ReadAll(res.Body)
154 if err != nil {
155 return APIResponse{}, err
156 }
157
158 if bot.Debug {
159 log.Println(string(bytes[:]))
160 }
161
162 var apiResp APIResponse
163 json.Unmarshal(bytes, &apiResp)
164
165 if !apiResp.Ok {
166 return APIResponse{}, errors.New(apiResp.Description)
167 }
168
169 return apiResp, nil
170}
171
172func (this *BotAPI) GetFileDirectUrl(fileID string) (string, error) {
173 file, err := this.GetFile(FileConfig{fileID})
174
175 if err != nil {
176 return "", err
177 }
178
179 return file.Link(this.Token), nil
180}
181
182// GetMe fetches the currently authenticated bot.
183//
184// There are no parameters for this method.
185func (bot *BotAPI) GetMe() (User, error) {
186 resp, err := bot.MakeRequest("getMe", nil)
187 if err != nil {
188 return User{}, err
189 }
190
191 var user User
192 json.Unmarshal(resp.Result, &user)
193
194 if bot.Debug {
195 log.Printf("getMe: %+v\n", user)
196 }
197
198 return user, nil
199}
200
201func (bot *BotAPI) IsMessageToMe(message Message) (bool) {
202 return strings.Contains(message.Text, "@"+bot.Self.UserName)
203}
204
205func (bot *BotAPI) Send(c Chattable) (Message, error) {
206 switch c.(type) {
207 case Fileable:
208 return bot.sendFile(c.(Fileable))
209 default:
210 return bot.sendChattable(c)
211 }
212}
213
214func (bot *BotAPI) debugLog(context string, v url.Values, message interface{}) {
215 if bot.Debug {
216 log.Printf("%s req : %+v\n", context, v)
217 log.Printf("%s resp: %+v\n", context, message)
218 }
219}
220
221func (bot *BotAPI) sendExisting(method string, config Fileable) (Message, error) {
222 v, err := config.Values()
223
224 if err != nil {
225 return Message{}, err
226 }
227
228 message, err := bot.MakeMessageRequest(method, v)
229 if err != nil {
230 return Message{}, err
231 }
232
233 return message, nil
234}
235
236func (bot *BotAPI) uploadAndSend(method string, config Fileable) (Message, error) {
237 params, err := config.Params()
238 if err != nil {
239 return Message{}, err
240 }
241
242 file := config.GetFile()
243
244 resp, err := bot.UploadFile(method, params, config.Name(), file)
245 if err != nil {
246 return Message{}, err
247 }
248
249 var message Message
250 json.Unmarshal(resp.Result, &message)
251
252 if bot.Debug {
253 log.Printf("%s resp: %+v\n", method, message)
254 }
255
256 return message, nil
257}
258
259func (bot *BotAPI) sendFile(config Fileable) (Message, error) {
260 if config.UseExistingFile() {
261 return bot.sendExisting(config.Method(), config)
262 }
263
264 return bot.uploadAndSend(config.Method(), config)
265}
266
267func (bot *BotAPI) sendChattable(config Chattable) (Message, error) {
268 v, err := config.Values()
269 if err != nil {
270 return Message{}, err
271 }
272
273 message, err := bot.MakeMessageRequest(config.Method(), v)
274
275 if err != nil {
276 return Message{}, err
277 }
278
279 return message, nil
280}
281
282// GetUserProfilePhotos gets a user's profile photos.
283//
284// Requires UserID.
285// Offset and Limit are optional.
286func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
287 v := url.Values{}
288 v.Add("user_id", strconv.Itoa(config.UserID))
289 if config.Offset != 0 {
290 v.Add("offset", strconv.Itoa(config.Offset))
291 }
292 if config.Limit != 0 {
293 v.Add("limit", strconv.Itoa(config.Limit))
294 }
295
296 resp, err := bot.MakeRequest("getUserProfilePhotos", v)
297 if err != nil {
298 return UserProfilePhotos{}, err
299 }
300
301 var profilePhotos UserProfilePhotos
302 json.Unmarshal(resp.Result, &profilePhotos)
303
304 bot.debugLog("GetUserProfilePhoto", v, profilePhotos)
305
306 return profilePhotos, nil
307}
308
309// GetFile returns a file_id required to download a file.
310//
311// Requires FileID.
312func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
313 v := url.Values{}
314 v.Add("file_id", config.FileID)
315
316 resp, err := bot.MakeRequest("getFile", v)
317 if err != nil {
318 return File{}, err
319 }
320
321 var file File
322 json.Unmarshal(resp.Result, &file)
323
324 bot.debugLog("GetFile", v, file)
325
326 return file, nil
327}
328
329// GetUpdates fetches updates.
330// If a WebHook is set, this will not return any data!
331//
332// Offset, Limit, and Timeout are optional.
333// To not get old items, set Offset to one higher than the previous item.
334// Set Timeout to a large number to reduce requests and get responses instantly.
335func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
336 v := url.Values{}
337 if config.Offset > 0 {
338 v.Add("offset", strconv.Itoa(config.Offset))
339 }
340 if config.Limit > 0 {
341 v.Add("limit", strconv.Itoa(config.Limit))
342 }
343 if config.Timeout > 0 {
344 v.Add("timeout", strconv.Itoa(config.Timeout))
345 }
346
347 resp, err := bot.MakeRequest("getUpdates", v)
348 if err != nil {
349 return []Update{}, err
350 }
351
352 var updates []Update
353 json.Unmarshal(resp.Result, &updates)
354
355 if bot.Debug {
356 log.Printf("getUpdates: %+v\n", updates)
357 }
358
359 return updates, nil
360}
361
362func (bot *BotAPI) RemoveWebhook() (APIResponse, error) {
363 return bot.MakeRequest("setWebhook", url.Values{})
364}
365
366// SetWebhook sets a webhook.
367// If this is set, GetUpdates will not get any data!
368//
369// Requires URL OR to set Clear to true.
370func (bot *BotAPI) SetWebhook(config WebhookConfig) (APIResponse, error) {
371 if config.Certificate == nil {
372 v := url.Values{}
373 v.Add("url", config.URL.String())
374
375 return bot.MakeRequest("setWebhook", v)
376 }
377
378 params := make(map[string]string)
379 params["url"] = config.URL.String()
380
381 resp, err := bot.UploadFile("setWebhook", params, "certificate", config.Certificate)
382 if err != nil {
383 return APIResponse{}, err
384 }
385
386 var apiResp APIResponse
387 json.Unmarshal(resp.Result, &apiResp)
388
389 if bot.Debug {
390 log.Printf("setWebhook resp: %+v\n", apiResp)
391 }
392
393 return apiResp, nil
394}
395
396// UpdatesChan starts a channel for getting updates.
397func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (<-chan Update, error) {
398 updatesChan := make(chan Update, 100)
399
400 go func() {
401 for {
402 updates, err := bot.GetUpdates(config)
403 if err != nil {
404 log.Println(err)
405 log.Println("Failed to get updates, retrying in 3 seconds...")
406 time.Sleep(time.Second * 3)
407
408 continue
409 }
410
411 for _, update := range updates {
412 if update.UpdateID >= config.Offset {
413 config.Offset = update.UpdateID + 1
414 updatesChan <- update
415 }
416 }
417 }
418 }()
419
420 return updatesChan, nil
421}
422
423// ListenForWebhook registers a http handler for a webhook.
424func (bot *BotAPI) ListenForWebhook(pattern string) (<-chan Update, http.Handler) {
425 updatesChan := make(chan Update, 100)
426
427 handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
428 bytes, _ := ioutil.ReadAll(r.Body)
429
430 var update Update
431 json.Unmarshal(bytes, &update)
432
433 updatesChan <- update
434 })
435
436 http.HandleFunc(pattern, handler)
437
438 return updatesChan, handler
439}