all repos — telegram-bot-api @ 1a3e995b376de8e2cb9d01565ced80d8b103896b

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	"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			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
172// GetFileDirectURL returns direct URL to file
173//
174// Requires fileID
175func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) {
176	file, err := bot.GetFile(FileConfig{fileID})
177
178	if err != nil {
179		return "", err
180	}
181
182	return file.Link(bot.Token), nil
183}
184
185// GetMe fetches the currently authenticated bot.
186//
187// There are no parameters for this method.
188func (bot *BotAPI) GetMe() (User, error) {
189	resp, err := bot.MakeRequest("getMe", nil)
190	if err != nil {
191		return User{}, err
192	}
193
194	var user User
195	json.Unmarshal(resp.Result, &user)
196
197	if bot.Debug {
198		log.Printf("getMe: %+v\n", user)
199	}
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
223// QuickSend will send message to selected chat
224func (bot *BotAPI) QuickSend(chatID int, message string) (Message, error) {
225	msg := NewMessage(chatID, message)
226	return bot.Send(msg)
227}
228
229func (bot *BotAPI) debugLog(context string, v url.Values, message interface{}) {
230	if bot.Debug {
231		log.Printf("%s req : %+v\n", context, v)
232		log.Printf("%s resp: %+v\n", context, message)
233	}
234}
235
236func (bot *BotAPI) sendExisting(method string, config Fileable) (Message, error) {
237	v, err := config.Values()
238
239	if err != nil {
240		return Message{}, err
241	}
242
243	message, err := bot.makeMessageRequest(method, v)
244	if err != nil {
245		return Message{}, err
246	}
247
248	return message, nil
249}
250
251func (bot *BotAPI) uploadAndSend(method string, config Fileable) (Message, error) {
252	params, err := config.Params()
253	if err != nil {
254		return Message{}, err
255	}
256
257	file := config.GetFile()
258
259	resp, err := bot.UploadFile(method, params, config.Name(), file)
260	if err != nil {
261		return Message{}, err
262	}
263
264	var message Message
265	json.Unmarshal(resp.Result, &message)
266
267	if bot.Debug {
268		log.Printf("%s resp: %+v\n", method, message)
269	}
270
271	return message, nil
272}
273
274func (bot *BotAPI) sendFile(config Fileable) (Message, error) {
275	if config.UseExistingFile() {
276		return bot.sendExisting(config.Method(), config)
277	}
278
279	return bot.uploadAndSend(config.Method(), config)
280}
281
282func (bot *BotAPI) sendChattable(config Chattable) (Message, error) {
283	v, err := config.Values()
284	if err != nil {
285		return Message{}, err
286	}
287
288	message, err := bot.makeMessageRequest(config.Method(), v)
289
290	if err != nil {
291		return Message{}, err
292	}
293
294	return message, nil
295}
296
297// GetUserProfilePhotos gets a user's profile photos.
298//
299// Requires UserID.
300// Offset and Limit are optional.
301func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
302	v := url.Values{}
303	v.Add("user_id", strconv.Itoa(config.UserID))
304	if config.Offset != 0 {
305		v.Add("offset", strconv.Itoa(config.Offset))
306	}
307	if config.Limit != 0 {
308		v.Add("limit", strconv.Itoa(config.Limit))
309	}
310
311	resp, err := bot.MakeRequest("getUserProfilePhotos", v)
312	if err != nil {
313		return UserProfilePhotos{}, err
314	}
315
316	var profilePhotos UserProfilePhotos
317	json.Unmarshal(resp.Result, &profilePhotos)
318
319	bot.debugLog("GetUserProfilePhoto", v, profilePhotos)
320
321	return profilePhotos, nil
322}
323
324// GetFile returns a file_id required to download a file.
325//
326// Requires FileID.
327func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
328	v := url.Values{}
329	v.Add("file_id", config.FileID)
330
331	resp, err := bot.MakeRequest("getFile", v)
332	if err != nil {
333		return File{}, err
334	}
335
336	var file File
337	json.Unmarshal(resp.Result, &file)
338
339	bot.debugLog("GetFile", v, file)
340
341	return file, nil
342}
343
344// GetUpdates fetches updates.
345// If a WebHook is set, this will not return any data!
346//
347// Offset, Limit, and Timeout are optional.
348// To not get old items, set Offset to one higher than the previous item.
349// Set Timeout to a large number to reduce requests and get responses instantly.
350func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
351	v := url.Values{}
352	if config.Offset > 0 {
353		v.Add("offset", strconv.Itoa(config.Offset))
354	}
355	if config.Limit > 0 {
356		v.Add("limit", strconv.Itoa(config.Limit))
357	}
358	if config.Timeout > 0 {
359		v.Add("timeout", strconv.Itoa(config.Timeout))
360	}
361
362	resp, err := bot.MakeRequest("getUpdates", v)
363	if err != nil {
364		return []Update{}, err
365	}
366
367	var updates []Update
368	json.Unmarshal(resp.Result, &updates)
369
370	if bot.Debug {
371		log.Printf("getUpdates: %+v\n", updates)
372	}
373
374	return updates, nil
375}
376
377// RemoveWebhook removes webhook
378//
379// There are no parameters for this method.
380func (bot *BotAPI) RemoveWebhook() (APIResponse, error) {
381	return bot.MakeRequest("setWebhook", url.Values{})
382}
383
384// SetWebhook sets a webhook.
385// If this is set, GetUpdates will not get any data!
386//
387// Requires URL OR to set Clear to true.
388func (bot *BotAPI) SetWebhook(config WebhookConfig) (APIResponse, error) {
389	if config.Certificate == nil {
390		v := url.Values{}
391		v.Add("url", config.URL.String())
392
393		return bot.MakeRequest("setWebhook", v)
394	}
395
396	params := make(map[string]string)
397	params["url"] = config.URL.String()
398
399	resp, err := bot.UploadFile("setWebhook", params, "certificate", config.Certificate)
400	if err != nil {
401		return APIResponse{}, err
402	}
403
404	var apiResp APIResponse
405	json.Unmarshal(resp.Result, &apiResp)
406
407	if bot.Debug {
408		log.Printf("setWebhook resp: %+v\n", apiResp)
409	}
410
411	return apiResp, nil
412}
413
414// GetUpdatesChan starts and returns a channel for getting updates.
415//
416// Requires UpdateConfig
417func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (<-chan Update, error) {
418	updatesChan := make(chan Update, 100)
419
420	go func() {
421		for {
422			updates, err := bot.GetUpdates(config)
423			if err != nil {
424				log.Println(err)
425				log.Println("Failed to get updates, retrying in 3 seconds...")
426				time.Sleep(time.Second * 3)
427
428				continue
429			}
430
431			for _, update := range updates {
432				if update.UpdateID >= config.Offset {
433					config.Offset = update.UpdateID + 1
434					updatesChan <- update
435				}
436			}
437		}
438	}()
439
440	return updatesChan, nil
441}
442
443// ListenForWebhook registers a http handler for a webhook.
444func (bot *BotAPI) ListenForWebhook(pattern string) (<-chan Update, http.Handler) {
445	updatesChan := make(chan Update, 100)
446
447	handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
448		bytes, _ := ioutil.ReadAll(r.Body)
449
450		var update Update
451		json.Unmarshal(bytes, &update)
452
453		updatesChan <- update
454	})
455
456	http.HandleFunc(pattern, handler)
457
458	return updatesChan, handler
459}