all repos — telegram-bot-api @ 6da34a6ba5ef97891fcf581cf9275fe16503a5d8

Golang bindings for the Telegram Bot API

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}