all repos — telegram-bot-api @ 9d6e221a006b09fb3435a69e7c2a5bc05f96892e

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// a FileReader struct, or a url.URL.
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
115	switch f := file.(type) {
116	case string:
117		ms.WriteFields(params)
118
119		fileHandle, err := os.Open(f)
120		if err != nil {
121			return APIResponse{}, err
122		}
123		defer fileHandle.Close()
124
125		fi, err := os.Stat(f)
126		if err != nil {
127			return APIResponse{}, err
128		}
129
130		ms.WriteReader(fieldname, fileHandle.Name(), fi.Size(), fileHandle)
131	case FileBytes:
132		ms.WriteFields(params)
133
134		buf := bytes.NewBuffer(f.Bytes)
135		ms.WriteReader(fieldname, f.Name, int64(len(f.Bytes)), buf)
136	case FileReader:
137		ms.WriteFields(params)
138
139		if f.Size != -1 {
140			ms.WriteReader(fieldname, f.Name, f.Size, f.Reader)
141
142			break
143		}
144
145		data, err := ioutil.ReadAll(f.Reader)
146		if err != nil {
147			return APIResponse{}, err
148		}
149
150		buf := bytes.NewBuffer(data)
151
152		ms.WriteReader(fieldname, f.Name, int64(len(data)), buf)
153	case url.URL:
154		params[fieldname] = f.String()
155
156		ms.WriteFields(params)
157	default:
158		return APIResponse{}, errors.New(ErrBadFileType)
159	}
160
161	method := fmt.Sprintf(APIEndpoint, bot.Token, endpoint)
162
163	req, err := http.NewRequest("POST", method, nil)
164	if err != nil {
165		return APIResponse{}, err
166	}
167
168	ms.SetupRequest(req)
169
170	res, err := bot.Client.Do(req)
171	if err != nil {
172		return APIResponse{}, err
173	}
174	defer res.Body.Close()
175
176	bytes, err := ioutil.ReadAll(res.Body)
177	if err != nil {
178		return APIResponse{}, err
179	}
180
181	if bot.Debug {
182		log.Println(string(bytes))
183	}
184
185	var apiResp APIResponse
186	json.Unmarshal(bytes, &apiResp)
187
188	if !apiResp.Ok {
189		return APIResponse{}, errors.New(apiResp.Description)
190	}
191
192	return apiResp, nil
193}
194
195// GetFileDirectURL returns direct URL to file
196//
197// It requires the FileID.
198func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) {
199	file, err := bot.GetFile(FileConfig{fileID})
200
201	if err != nil {
202		return "", err
203	}
204
205	return file.Link(bot.Token), nil
206}
207
208// GetMe fetches the currently authenticated bot.
209//
210// This method is called upon creation to validate the token,
211// and so you may get this data from BotAPI.Self without the need for
212// another request.
213func (bot *BotAPI) GetMe() (User, error) {
214	resp, err := bot.MakeRequest("getMe", nil)
215	if err != nil {
216		return User{}, err
217	}
218
219	var user User
220	json.Unmarshal(resp.Result, &user)
221
222	bot.debugLog("getMe", nil, user)
223
224	return user, nil
225}
226
227// IsMessageToMe returns true if message directed to this bot.
228//
229// It requires the Message.
230func (bot *BotAPI) IsMessageToMe(message Message) bool {
231	return strings.Contains(message.Text, "@"+bot.Self.UserName)
232}
233
234// Send will send a Chattable item to Telegram.
235//
236// It requires the Chattable to send.
237func (bot *BotAPI) Send(c Chattable) (Message, error) {
238	switch c.(type) {
239	case Fileable:
240		return bot.sendFile(c.(Fileable))
241	default:
242		return bot.sendChattable(c)
243	}
244}
245
246// debugLog checks if the bot is currently running in debug mode, and if
247// so will display information about the request and response in the
248// debug log.
249func (bot *BotAPI) debugLog(context string, v url.Values, message interface{}) {
250	if bot.Debug {
251		log.Printf("%s req : %+v\n", context, v)
252		log.Printf("%s resp: %+v\n", context, message)
253	}
254}
255
256// sendExisting will send a Message with an existing file to Telegram.
257func (bot *BotAPI) sendExisting(method string, config Fileable) (Message, error) {
258	v, err := config.values()
259
260	if err != nil {
261		return Message{}, err
262	}
263
264	message, err := bot.makeMessageRequest(method, v)
265	if err != nil {
266		return Message{}, err
267	}
268
269	return message, nil
270}
271
272// uploadAndSend will send a Message with a new file to Telegram.
273func (bot *BotAPI) uploadAndSend(method string, config Fileable) (Message, error) {
274	params, err := config.params()
275	if err != nil {
276		return Message{}, err
277	}
278
279	file := config.getFile()
280
281	resp, err := bot.UploadFile(method, params, config.name(), file)
282	if err != nil {
283		return Message{}, err
284	}
285
286	var message Message
287	json.Unmarshal(resp.Result, &message)
288
289	bot.debugLog(method, nil, message)
290
291	return message, nil
292}
293
294// sendFile determines if the file is using an existing file or uploading
295// a new file, then sends it as needed.
296func (bot *BotAPI) sendFile(config Fileable) (Message, error) {
297	if config.useExistingFile() {
298		return bot.sendExisting(config.method(), config)
299	}
300
301	return bot.uploadAndSend(config.method(), config)
302}
303
304// sendChattable sends a Chattable.
305func (bot *BotAPI) sendChattable(config Chattable) (Message, error) {
306	v, err := config.values()
307	if err != nil {
308		return Message{}, err
309	}
310
311	message, err := bot.makeMessageRequest(config.method(), v)
312
313	if err != nil {
314		return Message{}, err
315	}
316
317	return message, nil
318}
319
320// GetUserProfilePhotos gets a user's profile photos.
321//
322// It requires UserID.
323// Offset and Limit are optional.
324func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
325	v := url.Values{}
326	v.Add("user_id", strconv.Itoa(config.UserID))
327	if config.Offset != 0 {
328		v.Add("offset", strconv.Itoa(config.Offset))
329	}
330	if config.Limit != 0 {
331		v.Add("limit", strconv.Itoa(config.Limit))
332	}
333
334	resp, err := bot.MakeRequest("getUserProfilePhotos", v)
335	if err != nil {
336		return UserProfilePhotos{}, err
337	}
338
339	var profilePhotos UserProfilePhotos
340	json.Unmarshal(resp.Result, &profilePhotos)
341
342	bot.debugLog("GetUserProfilePhoto", v, profilePhotos)
343
344	return profilePhotos, nil
345}
346
347// GetFile returns a File which can download a file from Telegram.
348//
349// Requires FileID.
350func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
351	v := url.Values{}
352	v.Add("file_id", config.FileID)
353
354	resp, err := bot.MakeRequest("getFile", v)
355	if err != nil {
356		return File{}, err
357	}
358
359	var file File
360	json.Unmarshal(resp.Result, &file)
361
362	bot.debugLog("GetFile", v, file)
363
364	return file, nil
365}
366
367// GetUpdates fetches updates.
368// If a WebHook is set, this will not return any data!
369//
370// Offset, Limit, and Timeout are optional.
371// To avoid stale items, set Offset to one higher than the previous item.
372// Set Timeout to a large number to reduce requests so you can get updates
373// instantly instead of having to wait between requests.
374func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
375	v := url.Values{}
376	if config.Offset != 0 {
377		v.Add("offset", strconv.Itoa(config.Offset))
378	}
379	if config.Limit > 0 {
380		v.Add("limit", strconv.Itoa(config.Limit))
381	}
382	if config.Timeout > 0 {
383		v.Add("timeout", strconv.Itoa(config.Timeout))
384	}
385
386	resp, err := bot.MakeRequest("getUpdates", v)
387	if err != nil {
388		return []Update{}, err
389	}
390
391	var updates []Update
392	json.Unmarshal(resp.Result, &updates)
393
394	bot.debugLog("getUpdates", v, updates)
395
396	return updates, nil
397}
398
399// RemoveWebhook unsets the webhook.
400func (bot *BotAPI) RemoveWebhook() (APIResponse, error) {
401	return bot.MakeRequest("setWebhook", url.Values{})
402}
403
404// SetWebhook sets a webhook.
405//
406// If this is set, GetUpdates will not get any data!
407//
408// If you do not have a legitimate TLS certificate, you need to include
409// your self signed certificate with the config.
410func (bot *BotAPI) SetWebhook(config WebhookConfig) (APIResponse, error) {
411	if config.Certificate == nil {
412		v := url.Values{}
413		v.Add("url", config.URL.String())
414
415		return bot.MakeRequest("setWebhook", v)
416	}
417
418	params := make(map[string]string)
419	params["url"] = config.URL.String()
420
421	resp, err := bot.UploadFile("setWebhook", params, "certificate", config.Certificate)
422	if err != nil {
423		return APIResponse{}, err
424	}
425
426	var apiResp APIResponse
427	json.Unmarshal(resp.Result, &apiResp)
428
429	if bot.Debug {
430		log.Printf("setWebhook resp: %+v\n", apiResp)
431	}
432
433	return apiResp, nil
434}
435
436// GetWebhookInfo allows you to fetch information about a webhook and if
437// one currently is set, along with pending update count and error messages.
438func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) {
439	resp, err := bot.MakeRequest("getWebhookInfo", url.Values{})
440	if err != nil {
441		return WebhookInfo{}, err
442	}
443
444	var info WebhookInfo
445	err = json.Unmarshal(resp.Result, &info)
446
447	return info, err
448}
449
450// GetUpdatesChan starts and returns a channel for getting updates.
451func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (<-chan Update, error) {
452	updatesChan := make(chan Update, 100)
453
454	go func() {
455		for {
456			updates, err := bot.GetUpdates(config)
457			if err != nil {
458				log.Println(err)
459				log.Println("Failed to get updates, retrying in 3 seconds...")
460				time.Sleep(time.Second * 3)
461
462				continue
463			}
464
465			for _, update := range updates {
466				if update.UpdateID >= config.Offset {
467					config.Offset = update.UpdateID + 1
468					updatesChan <- update
469				}
470			}
471		}
472	}()
473
474	return updatesChan, nil
475}
476
477// ListenForWebhook registers a http handler for a webhook.
478func (bot *BotAPI) ListenForWebhook(pattern string) <-chan Update {
479	updatesChan := make(chan Update, 100)
480
481	http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
482		bytes, _ := ioutil.ReadAll(r.Body)
483
484		var update Update
485		json.Unmarshal(bytes, &update)
486
487		updatesChan <- update
488	})
489
490	return updatesChan
491}
492
493// AnswerInlineQuery sends a response to an inline query.
494//
495// Note that you must respond to an inline query within 30 seconds.
496func (bot *BotAPI) AnswerInlineQuery(config InlineConfig) (APIResponse, error) {
497	v := url.Values{}
498
499	v.Add("inline_query_id", config.InlineQueryID)
500	v.Add("cache_time", strconv.Itoa(config.CacheTime))
501	v.Add("is_personal", strconv.FormatBool(config.IsPersonal))
502	v.Add("next_offset", config.NextOffset)
503	data, err := json.Marshal(config.Results)
504	if err != nil {
505		return APIResponse{}, err
506	}
507	v.Add("results", string(data))
508	v.Add("switch_pm_text", config.SwitchPMText)
509	v.Add("switch_pm_parameter", config.SwitchPMParameter)
510
511	bot.debugLog("answerInlineQuery", v, nil)
512
513	return bot.MakeRequest("answerInlineQuery", v)
514}
515
516// AnswerCallbackQuery sends a response to an inline query callback.
517func (bot *BotAPI) AnswerCallbackQuery(config CallbackConfig) (APIResponse, error) {
518	v := url.Values{}
519
520	v.Add("callback_query_id", config.CallbackQueryID)
521	if config.Text != "" {
522		v.Add("text", config.Text)
523	}
524	v.Add("show_alert", strconv.FormatBool(config.ShowAlert))
525	if config.URL != "" {
526		v.Add("url", config.URL)
527	}
528	v.Add("cache_time", strconv.Itoa(config.CacheTime))
529
530	bot.debugLog("answerCallbackQuery", v, nil)
531
532	return bot.MakeRequest("answerCallbackQuery", v)
533}
534
535// KickChatMember kicks a user from a chat. Note that this only will work
536// in supergroups, and requires the bot to be an admin. Also note they
537// will be unable to rejoin until they are unbanned.
538func (bot *BotAPI) KickChatMember(config ChatMemberConfig) (APIResponse, error) {
539	v := url.Values{}
540
541	if config.SuperGroupUsername == "" {
542		v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
543	} else {
544		v.Add("chat_id", config.SuperGroupUsername)
545	}
546	v.Add("user_id", strconv.Itoa(config.UserID))
547
548	bot.debugLog("kickChatMember", v, nil)
549
550	return bot.MakeRequest("kickChatMember", v)
551}
552
553// LeaveChat makes the bot leave the chat.
554func (bot *BotAPI) LeaveChat(config ChatConfig) (APIResponse, error) {
555	v := url.Values{}
556
557	if config.SuperGroupUsername == "" {
558		v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
559	} else {
560		v.Add("chat_id", config.SuperGroupUsername)
561	}
562
563	bot.debugLog("leaveChat", v, nil)
564
565	return bot.MakeRequest("leaveChat", v)
566}
567
568// GetChat gets information about a chat.
569func (bot *BotAPI) GetChat(config ChatConfig) (Chat, error) {
570	v := url.Values{}
571
572	if config.SuperGroupUsername == "" {
573		v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
574	} else {
575		v.Add("chat_id", config.SuperGroupUsername)
576	}
577
578	resp, err := bot.MakeRequest("getChat", v)
579	if err != nil {
580		return Chat{}, err
581	}
582
583	var chat Chat
584	err = json.Unmarshal(resp.Result, &chat)
585
586	bot.debugLog("getChat", v, chat)
587
588	return chat, err
589}
590
591// GetChatAdministrators gets a list of administrators in the chat.
592//
593// If none have been appointed, only the creator will be returned.
594// Bots are not shown, even if they are an administrator.
595func (bot *BotAPI) GetChatAdministrators(config ChatConfig) ([]ChatMember, error) {
596	v := url.Values{}
597
598	if config.SuperGroupUsername == "" {
599		v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
600	} else {
601		v.Add("chat_id", config.SuperGroupUsername)
602	}
603
604	resp, err := bot.MakeRequest("getChatAdministrators", v)
605	if err != nil {
606		return []ChatMember{}, err
607	}
608
609	var members []ChatMember
610	err = json.Unmarshal(resp.Result, &members)
611
612	bot.debugLog("getChatAdministrators", v, members)
613
614	return members, err
615}
616
617// GetChatMembersCount gets the number of users in a chat.
618func (bot *BotAPI) GetChatMembersCount(config ChatConfig) (int, error) {
619	v := url.Values{}
620
621	if config.SuperGroupUsername == "" {
622		v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
623	} else {
624		v.Add("chat_id", config.SuperGroupUsername)
625	}
626
627	resp, err := bot.MakeRequest("getChatMembersCount", v)
628	if err != nil {
629		return -1, err
630	}
631
632	var count int
633	err = json.Unmarshal(resp.Result, &count)
634
635	bot.debugLog("getChatMembersCount", v, count)
636
637	return count, err
638}
639
640// GetChatMember gets a specific chat member.
641func (bot *BotAPI) GetChatMember(config ChatConfigWithUser) (ChatMember, error) {
642	v := url.Values{}
643
644	if config.SuperGroupUsername == "" {
645		v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
646	} else {
647		v.Add("chat_id", config.SuperGroupUsername)
648	}
649	v.Add("user_id", strconv.Itoa(config.UserID))
650
651	resp, err := bot.MakeRequest("getChatMember", v)
652	if err != nil {
653		return ChatMember{}, err
654	}
655
656	var member ChatMember
657	err = json.Unmarshal(resp.Result, &member)
658
659	bot.debugLog("getChatMember", v, member)
660
661	return member, err
662}
663
664// UnbanChatMember unbans a user from a chat. Note that this only will work
665// in supergroups, and requires the bot to be an admin.
666func (bot *BotAPI) UnbanChatMember(config ChatMemberConfig) (APIResponse, error) {
667	v := url.Values{}
668
669	if config.SuperGroupUsername == "" {
670		v.Add("chat_id", strconv.FormatInt(config.ChatID, 10))
671	} else {
672		v.Add("chat_id", config.SuperGroupUsername)
673	}
674	v.Add("user_id", strconv.Itoa(config.UserID))
675
676	bot.debugLog("unbanChatMember", v, nil)
677
678	return bot.MakeRequest("unbanChatMember", v)
679}
680
681// GetGameHighScores allows you to get the high scores for a game.
682func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
683	v, _ := config.values()
684
685	resp, err := bot.MakeRequest(config.method(), v)
686	if err != nil {
687		return []GameHighScore{}, err
688	}
689
690	var highScores []GameHighScore
691	err = json.Unmarshal(resp.Result, &highScores)
692
693	return highScores, err
694}