all repos — telegram-bot-api @ ab63d49cc925dfd42088efb03c558b99263c248c

Golang bindings for the Telegram Bot API

bot.go (view raw)

  1// Package tgbotapi has functions and types used for interacting with
  2// the Telegram Bot API.
  3package tgbotapi
  4
  5import (
  6	"bytes"
  7	"encoding/json"
  8	"errors"
  9	"fmt"
 10	"github.com/technoweenie/multipartstreamer"
 11	"io/ioutil"
 12	"log"
 13	"net/http"
 14	"net/url"
 15	"os"
 16	"strconv"
 17	"strings"
 18	"time"
 19)
 20
 21// BotAPI allows you to interact with the Telegram Bot API.
 22type BotAPI struct {
 23	Token  string       `json:"token"`
 24	Debug  bool         `json:"debug"`
 25	Self   User         `json:"-"`
 26	Client *http.Client `json:"-"`
 27}
 28
 29// NewBotAPI creates a new BotAPI instance.
 30//
 31// It requires a token, provided by @BotFather on Telegram.
 32func NewBotAPI(token string) (*BotAPI, error) {
 33	return NewBotAPIWithClient(token, &http.Client{})
 34}
 35
 36// NewBotAPIWithClient creates a new BotAPI instance
 37// and allows you to pass a http.Client.
 38//
 39// It requires a token, provided by @BotFather on Telegram.
 40func NewBotAPIWithClient(token string, client *http.Client) (*BotAPI, error) {
 41	bot := &BotAPI{
 42		Token:  token,
 43		Client: client,
 44	}
 45
 46	self, err := bot.GetMe()
 47	if err != nil {
 48		return &BotAPI{}, err
 49	}
 50
 51	bot.Self = self
 52
 53	return bot, nil
 54}
 55
 56// MakeRequest makes a request to a specific endpoint with our token.
 57func (bot *BotAPI) MakeRequest(endpoint string, params url.Values) (APIResponse, error) {
 58	method := fmt.Sprintf(APIEndpoint, bot.Token, endpoint)
 59
 60	resp, err := bot.Client.PostForm(method, params)
 61	if err != nil {
 62		return APIResponse{}, err
 63	}
 64	defer resp.Body.Close()
 65
 66	if resp.StatusCode == http.StatusForbidden {
 67		return APIResponse{}, errors.New(ErrAPIForbidden)
 68	}
 69
 70	bytes, err := ioutil.ReadAll(resp.Body)
 71	if err != nil {
 72		return APIResponse{}, err
 73	}
 74
 75	if bot.Debug {
 76		log.Println(endpoint, string(bytes))
 77	}
 78
 79	var apiResp APIResponse
 80	json.Unmarshal(bytes, &apiResp)
 81
 82	if !apiResp.Ok {
 83		return APIResponse{}, errors.New(apiResp.Description)
 84	}
 85
 86	return apiResp, nil
 87}
 88
 89// makeMessageRequest makes a request to a method that returns a Message.
 90func (bot *BotAPI) makeMessageRequest(endpoint string, params url.Values) (Message, error) {
 91	resp, err := bot.MakeRequest(endpoint, params)
 92	if err != nil {
 93		return Message{}, err
 94	}
 95
 96	var message Message
 97	json.Unmarshal(resp.Result, &message)
 98
 99	bot.debugLog(endpoint, params, message)
100
101	return message, nil
102}
103
104// UploadFile makes a request to the API with a file.
105//
106// Requires the parameter to hold the file not be in the params.
107// File should be a string to a file path, a FileBytes struct,
108// or a FileReader struct.
109//
110// Note that if your FileReader has a size set to -1, it will read
111// the file into memory to calculate a size.
112func (bot *BotAPI) UploadFile(endpoint string, params map[string]string, fieldname string, file interface{}) (APIResponse, error) {
113	ms := multipartstreamer.New()
114	ms.WriteFields(params)
115
116	switch f := file.(type) {
117	case string:
118		fileHandle, err := os.Open(f)
119		if err != nil {
120			return APIResponse{}, err
121		}
122		defer fileHandle.Close()
123
124		fi, err := os.Stat(f)
125		if err != nil {
126			return APIResponse{}, err
127		}
128
129		ms.WriteReader(fieldname, fileHandle.Name(), fi.Size(), fileHandle)
130	case FileBytes:
131		buf := bytes.NewBuffer(f.Bytes)
132		ms.WriteReader(fieldname, f.Name, int64(len(f.Bytes)), buf)
133	case FileReader:
134		if f.Size != -1 {
135			ms.WriteReader(fieldname, f.Name, f.Size, f.Reader)
136
137			break
138		}
139
140		data, err := ioutil.ReadAll(f.Reader)
141		if err != nil {
142			return APIResponse{}, err
143		}
144
145		buf := bytes.NewBuffer(data)
146
147		ms.WriteReader(fieldname, f.Name, int64(len(data)), buf)
148	default:
149		return APIResponse{}, errors.New(ErrBadFileType)
150	}
151
152	method := fmt.Sprintf(APIEndpoint, bot.Token, endpoint)
153
154	req, err := http.NewRequest("POST", method, nil)
155	if err != nil {
156		return APIResponse{}, err
157	}
158
159	ms.SetupRequest(req)
160
161	res, err := bot.Client.Do(req)
162	if err != nil {
163		return APIResponse{}, err
164	}
165	defer res.Body.Close()
166
167	bytes, err := ioutil.ReadAll(res.Body)
168	if err != nil {
169		return APIResponse{}, err
170	}
171
172	if bot.Debug {
173		log.Println(string(bytes))
174	}
175
176	var apiResp APIResponse
177	json.Unmarshal(bytes, &apiResp)
178
179	if !apiResp.Ok {
180		return APIResponse{}, errors.New(apiResp.Description)
181	}
182
183	return apiResp, nil
184}
185
186// GetFileDirectURL returns direct URL to file
187//
188// It requires the FileID.
189func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) {
190	file, err := bot.GetFile(FileConfig{fileID})
191
192	if err != nil {
193		return "", err
194	}
195
196	return file.Link(bot.Token), nil
197}
198
199// GetMe fetches the currently authenticated bot.
200//
201// This method is called upon creation to validate the token,
202// and so you may get this data from BotAPI.Self without the need for
203// another request.
204func (bot *BotAPI) GetMe() (User, error) {
205	resp, err := bot.MakeRequest("getMe", nil)
206	if err != nil {
207		return User{}, err
208	}
209
210	var user User
211	json.Unmarshal(resp.Result, &user)
212
213	bot.debugLog("getMe", nil, user)
214
215	return user, nil
216}
217
218// IsMessageToMe returns true if message directed to this bot.
219//
220// It requires the Message.
221func (bot *BotAPI) IsMessageToMe(message Message) bool {
222	return strings.Contains(message.Text, "@"+bot.Self.UserName)
223}
224
225// Send will send a Chattable item to Telegram.
226//
227// It requires the Chattable to send.
228func (bot *BotAPI) Send(c Chattable) (Message, error) {
229	switch c.(type) {
230	case Fileable:
231		return bot.sendFile(c.(Fileable))
232	default:
233		return bot.sendChattable(c)
234	}
235}
236
237// debugLog checks if the bot is currently running in debug mode, and if
238// so will display information about the request and response in the
239// debug log.
240func (bot *BotAPI) debugLog(context string, v url.Values, message interface{}) {
241	if bot.Debug {
242		log.Printf("%s req : %+v\n", context, v)
243		log.Printf("%s resp: %+v\n", context, message)
244	}
245}
246
247// sendExisting will send a Message with an existing file to Telegram.
248func (bot *BotAPI) sendExisting(method string, config Fileable) (Message, error) {
249	v, err := config.values()
250
251	if err != nil {
252		return Message{}, err
253	}
254
255	message, err := bot.makeMessageRequest(method, v)
256	if err != nil {
257		return Message{}, err
258	}
259
260	return message, nil
261}
262
263// uploadAndSend will send a Message with a new file to Telegram.
264func (bot *BotAPI) uploadAndSend(method string, config Fileable) (Message, error) {
265	params, err := config.params()
266	if err != nil {
267		return Message{}, err
268	}
269
270	file := config.getFile()
271
272	resp, err := bot.UploadFile(method, params, config.name(), file)
273	if err != nil {
274		return Message{}, err
275	}
276
277	var message Message
278	json.Unmarshal(resp.Result, &message)
279
280	bot.debugLog(method, nil, message)
281
282	return message, nil
283}
284
285// sendFile determines if the file is using an existing file or uploading
286// a new file, then sends it as needed.
287func (bot *BotAPI) sendFile(config Fileable) (Message, error) {
288	if config.useExistingFile() {
289		return bot.sendExisting(config.method(), config)
290	}
291
292	return bot.uploadAndSend(config.method(), config)
293}
294
295// sendChattable sends a Chattable.
296func (bot *BotAPI) sendChattable(config Chattable) (Message, error) {
297	v, err := config.values()
298	if err != nil {
299		return Message{}, err
300	}
301
302	message, err := bot.makeMessageRequest(config.method(), v)
303
304	if err != nil {
305		return Message{}, err
306	}
307
308	return message, nil
309}
310
311// GetUserProfilePhotos gets a user's profile photos.
312//
313// It requires UserID.
314// Offset and Limit are optional.
315func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
316	v := url.Values{}
317	v.Add("user_id", strconv.Itoa(config.UserID))
318	if config.Offset != 0 {
319		v.Add("offset", strconv.Itoa(config.Offset))
320	}
321	if config.Limit != 0 {
322		v.Add("limit", strconv.Itoa(config.Limit))
323	}
324
325	resp, err := bot.MakeRequest("getUserProfilePhotos", v)
326	if err != nil {
327		return UserProfilePhotos{}, err
328	}
329
330	var profilePhotos UserProfilePhotos
331	json.Unmarshal(resp.Result, &profilePhotos)
332
333	bot.debugLog("GetUserProfilePhoto", v, profilePhotos)
334
335	return profilePhotos, nil
336}
337
338// GetFile returns a File which can download a file from Telegram.
339//
340// Requires FileID.
341func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
342	v := url.Values{}
343	v.Add("file_id", config.FileID)
344
345	resp, err := bot.MakeRequest("getFile", v)
346	if err != nil {
347		return File{}, err
348	}
349
350	var file File
351	json.Unmarshal(resp.Result, &file)
352
353	bot.debugLog("GetFile", v, file)
354
355	return file, nil
356}
357
358// GetUpdates fetches updates.
359// If a WebHook is set, this will not return any data!
360//
361// Offset, Limit, and Timeout are optional.
362// To avoid stale items, set Offset to one higher than the previous item.
363// Set Timeout to a large number to reduce requests so you can get updates
364// instantly instead of having to wait between requests.
365func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
366	v := url.Values{}
367	if config.Offset > 0 {
368		v.Add("offset", strconv.Itoa(config.Offset))
369	}
370	if config.Limit > 0 {
371		v.Add("limit", strconv.Itoa(config.Limit))
372	}
373	if config.Timeout > 0 {
374		v.Add("timeout", strconv.Itoa(config.Timeout))
375	}
376
377	resp, err := bot.MakeRequest("getUpdates", v)
378	if err != nil {
379		return []Update{}, err
380	}
381
382	var updates []Update
383	json.Unmarshal(resp.Result, &updates)
384
385	bot.debugLog("getUpdates", v, updates)
386
387	return updates, nil
388}
389
390// RemoveWebhook unsets the webhook.
391func (bot *BotAPI) RemoveWebhook() (APIResponse, error) {
392	return bot.MakeRequest("setWebhook", url.Values{})
393}
394
395// SetWebhook sets a webhook.
396//
397// If this is set, GetUpdates will not get any data!
398//
399// If you do not have a legitmate TLS certificate, you need to include
400// your self signed certificate with the config.
401func (bot *BotAPI) SetWebhook(config WebhookConfig) (APIResponse, error) {
402	if config.Certificate == nil {
403		v := url.Values{}
404		v.Add("url", config.URL.String())
405
406		return bot.MakeRequest("setWebhook", v)
407	}
408
409	params := make(map[string]string)
410	params["url"] = config.URL.String()
411
412	resp, err := bot.UploadFile("setWebhook", params, "certificate", config.Certificate)
413	if err != nil {
414		return APIResponse{}, err
415	}
416
417	var apiResp APIResponse
418	json.Unmarshal(resp.Result, &apiResp)
419
420	if bot.Debug {
421		log.Printf("setWebhook resp: %+v\n", apiResp)
422	}
423
424	return apiResp, nil
425}
426
427// GetUpdatesChan starts and returns a channel for getting updates.
428func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (<-chan Update, error) {
429	updatesChan := make(chan Update, 100)
430
431	go func() {
432		for {
433			updates, err := bot.GetUpdates(config)
434			if err != nil {
435				log.Println(err)
436				log.Println("Failed to get updates, retrying in 3 seconds...")
437				time.Sleep(time.Second * 3)
438
439				continue
440			}
441
442			for _, update := range updates {
443				if update.UpdateID >= config.Offset {
444					config.Offset = update.UpdateID + 1
445					updatesChan <- update
446				}
447			}
448		}
449	}()
450
451	return updatesChan, nil
452}
453
454// ListenForWebhook registers a http handler for a webhook.
455func (bot *BotAPI) ListenForWebhook(pattern string) <-chan Update {
456	updatesChan := make(chan Update, 100)
457
458	http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
459		bytes, _ := ioutil.ReadAll(r.Body)
460
461		var update Update
462		json.Unmarshal(bytes, &update)
463
464		updatesChan <- update
465	})
466
467	return updatesChan
468}
469
470// AnswerInlineQuery sends a response to an inline query.
471//
472// Note that you must respond to an inline query within 30 seconds.
473func (bot *BotAPI) AnswerInlineQuery(config InlineConfig) (APIResponse, error) {
474	v := url.Values{}
475
476	v.Add("inline_query_id", config.InlineQueryID)
477	v.Add("cache_time", strconv.Itoa(config.CacheTime))
478	v.Add("is_personal", strconv.FormatBool(config.IsPersonal))
479	v.Add("next_offset", config.NextOffset)
480	data, err := json.Marshal(config.Results)
481	if err != nil {
482		return APIResponse{}, err
483	}
484	v.Add("results", string(data))
485	v.Add("switch_pm_text", config.SwitchPMText)
486	v.Add("switch_pm_parameter", config.SwitchPMParameter)
487
488	bot.debugLog("answerInlineQuery", v, nil)
489
490	return bot.MakeRequest("answerInlineQuery", v)
491}
492
493// AnswerCallbackQuery sends a response to an inline query callback.
494func (bot *BotAPI) AnswerCallbackQuery(config CallbackConfig) (APIResponse, error) {
495	v := url.Values{}
496
497	v.Add("callback_query_id", config.CallbackQueryID)
498	v.Add("text", config.Text)
499	v.Add("show_alert", strconv.FormatBool(config.ShowAlert))
500
501	bot.debugLog("answerCallbackQuery", v, nil)
502
503	return bot.MakeRequest("answerCallbackQuery", v)
504}
505
506// KickChatMember kicks a user from a chat. Note that this only will work
507// in supergroups, and requires the bot to be an admin. Also note they
508// will be unable to rejoin until they are unbanned.
509func (bot *BotAPI) KickChatMember(config ChatMemberConfig) (APIResponse, error) {
510	v := url.Values{}
511
512	if config.SuperGroupUsername == "" {
513		v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
514	} else {
515		v.Add("chat_id", config.SuperGroupUsername)
516	}
517	v.Add("user_id", strconv.Itoa(config.UserID))
518
519	bot.debugLog("kickChatMember", v, nil)
520
521	return bot.MakeRequest("kickChatMember", v)
522}
523
524// UnbanChatMember unbans a user from a chat. Note that this only will work
525// in supergroups, and requires the bot to be an admin.
526func (bot *BotAPI) UnbanChatMember(config ChatMemberConfig) (APIResponse, error) {
527	v := url.Values{}
528
529	if config.SuperGroupUsername == "" {
530		v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
531	} else {
532		v.Add("chat_id", config.SuperGroupUsername)
533	}
534	v.Add("user_id", strconv.Itoa(config.UserID))
535
536	bot.debugLog("unbanChatMember", v, nil)
537
538	return bot.MakeRequest("unbanChatMember", v)
539}