all repos — telegram-bot-api @ 9361631c6d3a7a10da24e4172d471026560b6e97

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)
 18
 19// BotAPI has methods for interacting with all of Telegram's Bot API endpoints.
 20type BotAPI struct {
 21	Token   string       `json:"token"`
 22	Debug   bool         `json:"debug"`
 23	Self    User         `json:"-"`
 24	Updates chan Update  `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// GetMe fetches the currently authenticated bot.
173//
174// There are no parameters for this method.
175func (bot *BotAPI) GetMe() (User, error) {
176	resp, err := bot.MakeRequest("getMe", nil)
177	if err != nil {
178		return User{}, err
179	}
180
181	var user User
182	json.Unmarshal(resp.Result, &user)
183
184	if bot.Debug {
185		log.Printf("getMe: %+v\n", user)
186	}
187
188	return user, nil
189}
190
191func (bot *BotAPI) Send(c BaseChat) error {
192	return nil
193}
194
195func (bot *BotAPI) DebugLog(context string, v url.Values, message interface{}) {
196	if bot.Debug {
197		log.Printf("%s req : %+v\n", context, v)
198		log.Printf("%s resp: %+v\n", context, message)
199	}
200}
201
202func (bot *BotAPI) sendExisting(method string, config Fileable) (Message, error) {
203	v, err := config.Values()
204
205	if err != nil {
206		return Message{}, err
207	}
208
209	message, err := bot.MakeMessageRequest(method, v)
210	if err != nil {
211		return Message{}, err
212	}
213
214	return message, nil
215}
216
217// SendMessage sends a Message to a chat.
218//
219// Requires ChatID and Text.
220// DisableWebPagePreview, ReplyToMessageID, and ReplyMarkup are optional.
221func (bot *BotAPI) SendMessage(config MessageConfig) (Message, error) {
222	v, err := config.Values()
223	if err != nil {
224		return Message{}, err
225	}
226
227	message, err := bot.MakeMessageRequest("SendMessage", v)
228
229	if err != nil {
230		return Message{}, err
231	}
232
233	return message, nil
234}
235
236// ForwardMessage forwards a message from one chat to another.
237//
238// Requires ChatID (destination), FromChatID (source), and MessageID.
239func (bot *BotAPI) ForwardMessage(config ForwardConfig) (Message, error) {
240	v, err := config.Values()
241	if err != nil {
242		return Message{}, err
243	}
244
245	message, err := bot.MakeMessageRequest("forwardMessage", v)
246	if err != nil {
247		return Message{}, err
248	}
249
250	return message, nil
251}
252
253// SendLocation sends a location to a chat.
254//
255// Requires ChatID, Latitude, and Longitude.
256// ReplyToMessageID and ReplyMarkup are optional.
257func (bot *BotAPI) SendLocation(config LocationConfig) (Message, error) {
258	v, err := config.Values()
259	if err != nil {
260		return Message{}, err
261	}
262
263	message, err := bot.MakeMessageRequest("sendLocation", v)
264	if err != nil {
265		return Message{}, err
266	}
267
268	return message, nil
269}
270
271// SendPhoto sends or uploads a photo to a chat.
272//
273// Requires ChatID and FileID OR File.
274// Caption, ReplyToMessageID, and ReplyMarkup are optional.
275// File should be either a string, FileBytes, or FileReader.
276func (bot *BotAPI) SendPhoto(config PhotoConfig) (Message, error) {
277	if config.UseExisting {
278		return bot.sendExisting("SendPhoto", config)
279	}
280
281	params, err := config.Params()
282	if err != nil {
283		return Message{}, err
284	}
285
286	file := config.GetFile()
287
288	resp, err := bot.UploadFile("SendPhoto", params, "photo", file)
289	if err != nil {
290		return Message{}, err
291	}
292
293	var message Message
294	json.Unmarshal(resp.Result, &message)
295
296	if bot.Debug {
297		log.Printf("SendPhoto resp: %+v\n", message)
298	}
299
300	return message, nil
301}
302
303// SendAudio sends or uploads an audio clip to a chat.
304// If using a file, the file must be in the .mp3 format.
305//
306// When the fields title and performer are both empty and
307// the mime-type of the file to be sent is not audio/mpeg,
308// the file must be an .ogg file encoded with OPUS.
309// You may use the tgutils.EncodeAudio func to assist you with this, if needed.
310//
311// Requires ChatID and FileID OR File.
312// ReplyToMessageID and ReplyMarkup are optional.
313// File should be either a string, FileBytes, or FileReader.
314func (bot *BotAPI) SendAudio(config AudioConfig) (Message, error) {
315	if config.UseExisting {
316		return bot.sendExisting("sendAudio", config)
317	}
318
319	params, err := config.Params()
320	if err != nil {
321		return Message{}, err
322	}
323
324	file := config.GetFile()
325
326	resp, err := bot.UploadFile("sendAudio", params, "audio", file)
327	if err != nil {
328		return Message{}, err
329	}
330
331	var message Message
332	json.Unmarshal(resp.Result, &message)
333
334	if bot.Debug {
335		log.Printf("sendAudio resp: %+v\n", message)
336	}
337
338	return message, nil
339}
340
341// SendDocument sends or uploads a document to a chat.
342//
343// Requires ChatID and FileID OR File.
344// ReplyToMessageID and ReplyMarkup are optional.
345// File should be either a string, FileBytes, or FileReader.
346func (bot *BotAPI) SendDocument(config DocumentConfig) (Message, error) {
347	if config.UseExisting {
348		return bot.sendExisting("sendDocument", config)
349	}
350
351	params, err := config.Params()
352	if err != nil {
353		return Message{}, err
354	}
355
356	file := config.GetFile()
357
358	resp, err := bot.UploadFile("sendDocument", params, "document", file)
359	if err != nil {
360		return Message{}, err
361	}
362
363	var message Message
364	json.Unmarshal(resp.Result, &message)
365
366	if bot.Debug {
367		log.Printf("sendDocument resp: %+v\n", message)
368	}
369
370	return message, nil
371}
372
373// SendVoice sends or uploads a playable voice to a chat.
374// If using a file, the file must be encoded as an .ogg with OPUS.
375// You may use the tgutils.EncodeAudio func to assist you with this, if needed.
376//
377// Requires ChatID and FileID OR File.
378// ReplyToMessageID and ReplyMarkup are optional.
379// File should be either a string, FileBytes, or FileReader.
380func (bot *BotAPI) SendVoice(config VoiceConfig) (Message, error) {
381	if config.UseExisting {
382		return bot.sendExisting("sendVoice", config)
383	}
384
385	params, err := config.Params()
386	if err != nil {
387		return Message{}, err
388	}
389
390	file := config.GetFile()
391
392	resp, err := bot.UploadFile("SendVoice", params, "voice", file)
393	if err != nil {
394		return Message{}, err
395	}
396
397	var message Message
398	json.Unmarshal(resp.Result, &message)
399
400	if bot.Debug {
401		log.Printf("SendVoice resp: %+v\n", message)
402	}
403
404	return message, nil
405}
406
407// SendSticker sends or uploads a sticker to a chat.
408//
409// Requires ChatID and FileID OR File.
410// ReplyToMessageID and ReplyMarkup are optional.
411// File should be either a string, FileBytes, or FileReader.
412func (bot *BotAPI) SendSticker(config StickerConfig) (Message, error) {
413	if config.UseExisting {
414		return bot.sendExisting("sendSticker", config)
415	}
416
417	params, err := config.Params()
418	if err != nil {
419		return Message{}, err
420	}
421
422	file := config.GetFile()
423
424	resp, err := bot.UploadFile("sendSticker", params, "sticker", file)
425	if err != nil {
426		return Message{}, err
427	}
428
429	var message Message
430	json.Unmarshal(resp.Result, &message)
431
432	if bot.Debug {
433		log.Printf("sendSticker resp: %+v\n", message)
434	}
435
436	return message, nil
437}
438
439// SendVideo sends or uploads a video to a chat.
440//
441// Requires ChatID and FileID OR File.
442// ReplyToMessageID and ReplyMarkup are optional.
443// File should be either a string, FileBytes, or FileReader.
444func (bot *BotAPI) SendVideo(config VideoConfig) (Message, error) {
445	if config.UseExisting {
446		return bot.sendExisting("sendVideo", config)
447	}
448
449	params, err := config.Params()
450	if err != nil {
451		return Message{}, err
452	}
453
454	file := config.GetFile()
455
456	resp, err := bot.UploadFile("sendVideo", params, "video", file)
457	if err != nil {
458		return Message{}, err
459	}
460
461	var message Message
462	json.Unmarshal(resp.Result, &message)
463
464	if bot.Debug {
465		log.Printf("sendVideo resp: %+v\n", message)
466	}
467
468	return message, nil
469}
470
471// SendChatAction sets a current action in a chat.
472//
473// Requires ChatID and a valid Action (see Chat constants).
474func (bot *BotAPI) SendChatAction(config ChatActionConfig) error {
475	v, err := config.Values()
476	if err != nil {
477		return err
478	}
479
480	_, err = bot.MakeRequest("sendChatAction", v)
481	if err != nil {
482		return err
483	}
484
485	return nil
486}
487
488// GetUserProfilePhotos gets a user's profile photos.
489//
490// Requires UserID.
491// Offset and Limit are optional.
492func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
493	v := url.Values{}
494	v.Add("user_id", strconv.Itoa(config.UserID))
495	if config.Offset != 0 {
496		v.Add("offset", strconv.Itoa(config.Offset))
497	}
498	if config.Limit != 0 {
499		v.Add("limit", strconv.Itoa(config.Limit))
500	}
501
502	resp, err := bot.MakeRequest("getUserProfilePhotos", v)
503	if err != nil {
504		return UserProfilePhotos{}, err
505	}
506
507	var profilePhotos UserProfilePhotos
508	json.Unmarshal(resp.Result, &profilePhotos)
509
510	bot.DebugLog("GetUserProfilePhoto", v, profilePhotos)
511
512	return profilePhotos, nil
513}
514
515// GetFile returns a file_id required to download a file.
516//
517// Requires FileID.
518func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
519	v := url.Values{}
520	v.Add("file_id", config.FileID)
521
522	resp, err := bot.MakeRequest("getFile", v)
523	if err != nil {
524		return File{}, err
525	}
526
527	var file File
528	json.Unmarshal(resp.Result, &file)
529
530	bot.DebugLog("GetFile", v, file)
531
532	return file, nil
533}
534
535// GetUpdates fetches updates.
536// If a WebHook is set, this will not return any data!
537//
538// Offset, Limit, and Timeout are optional.
539// To not get old items, set Offset to one higher than the previous item.
540// Set Timeout to a large number to reduce requests and get responses instantly.
541func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
542	v := url.Values{}
543	if config.Offset > 0 {
544		v.Add("offset", strconv.Itoa(config.Offset))
545	}
546	if config.Limit > 0 {
547		v.Add("limit", strconv.Itoa(config.Limit))
548	}
549	if config.Timeout > 0 {
550		v.Add("timeout", strconv.Itoa(config.Timeout))
551	}
552
553	resp, err := bot.MakeRequest("getUpdates", v)
554	if err != nil {
555		return []Update{}, err
556	}
557
558	var updates []Update
559	json.Unmarshal(resp.Result, &updates)
560
561	if bot.Debug {
562		log.Printf("getUpdates: %+v\n", updates)
563	}
564
565	return updates, nil
566}
567
568// SetWebhook sets a webhook.
569// If this is set, GetUpdates will not get any data!
570//
571// Requires URL OR to set Clear to true.
572func (bot *BotAPI) SetWebhook(config WebhookConfig) (APIResponse, error) {
573	if config.Certificate == nil {
574		v := url.Values{}
575		if !config.Clear {
576			v.Add("url", config.URL.String())
577		}
578
579		return bot.MakeRequest("setWebhook", v)
580	}
581
582	params := make(map[string]string)
583	params["url"] = config.URL.String()
584
585	resp, err := bot.UploadFile("setWebhook", params, "certificate", config.Certificate)
586	if err != nil {
587		return APIResponse{}, err
588	}
589
590	var apiResp APIResponse
591	json.Unmarshal(resp.Result, &apiResp)
592
593	if bot.Debug {
594		log.Printf("setWebhook resp: %+v\n", apiResp)
595	}
596
597	return apiResp, nil
598}
599
600// UpdatesChan starts a channel for getting updates.
601func (bot *BotAPI) UpdatesChan(config UpdateConfig) error {
602	bot.Updates = make(chan Update, 100)
603
604	go func() {
605		for {
606			updates, err := bot.GetUpdates(config)
607			if err != nil {
608				log.Println(err)
609				log.Println("Failed to get updates, retrying in 3 seconds...")
610				time.Sleep(time.Second * 3)
611
612				continue
613			}
614
615			for _, update := range updates {
616				if update.UpdateID >= config.Offset {
617					config.Offset = update.UpdateID + 1
618					bot.Updates <- update
619				}
620			}
621		}
622	}()
623
624	return nil
625}
626
627// ListenForWebhook registers a http handler for a webhook.
628func (bot *BotAPI) ListenForWebhook(pattern string) {
629	bot.Updates = make(chan Update, 100)
630
631	http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
632		bytes, _ := ioutil.ReadAll(r.Body)
633
634		var update Update
635		json.Unmarshal(bytes, &update)
636
637		bot.Updates <- update
638	})
639}