all repos — telegram-bot-api @ 54b9c7e14b7d8b48ceae32f94d2cce514a02c525

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 Chattable) 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
202// SendMessage sends a Message to a chat.
203//
204// Requires ChatID and Text.
205// DisableWebPagePreview, ReplyToMessageID, and ReplyMarkup are optional.
206func (bot *BotAPI) SendMessage(config MessageConfig) (Message, error) {
207	v, err := config.Values()
208
209	if err != nil {
210		return Message{}, err
211	}
212
213	message, err := bot.MakeMessageRequest("SendMessage", v)
214
215	if err != nil {
216		return Message{}, err
217	}
218
219	return message, nil
220}
221
222// ForwardMessage forwards a message from one chat to another.
223//
224// Requires ChatID (destination), FromChatID (source), and MessageID.
225func (bot *BotAPI) ForwardMessage(config ForwardConfig) (Message, error) {
226	v, _ := config.Values()
227
228	message, err := bot.MakeMessageRequest("forwardMessage", v)
229	if err != nil {
230		return Message{}, err
231	}
232
233	return message, nil
234}
235
236// SendPhoto sends or uploads a photo to a chat.
237//
238// Requires ChatID and FileID OR File.
239// Caption, ReplyToMessageID, and ReplyMarkup are optional.
240// File should be either a string, FileBytes, or FileReader.
241func (bot *BotAPI) SendPhoto(config PhotoConfig) (Message, error) {
242	if config.UseExistingPhoto {
243		v, err := config.Values()
244
245		if err != nil {
246			return Message{}, err
247		}
248
249		message, err := bot.MakeMessageRequest("SendPhoto", v)
250		if err != nil {
251			return Message{}, err
252		}
253
254		return message, nil
255	}
256
257	params := make(map[string]string)
258	if config.ChannelUsername != "" {
259		params["chat_id"] = config.ChannelUsername
260	} else {
261		params["chat_id"] = strconv.Itoa(config.ChatID)
262	}
263	if config.Caption != "" {
264		params["caption"] = config.Caption
265	}
266	if config.ReplyToMessageID != 0 {
267		params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
268	}
269	if config.ReplyMarkup != nil {
270		data, err := json.Marshal(config.ReplyMarkup)
271		if err != nil {
272			return Message{}, err
273		}
274
275		params["reply_markup"] = string(data)
276	}
277
278	var file interface{}
279	if config.FilePath == "" {
280		file = config.File
281	} else {
282		file = config.FilePath
283	}
284
285	resp, err := bot.UploadFile("SendPhoto", params, "photo", file)
286	if err != nil {
287		return Message{}, err
288	}
289
290	var message Message
291	json.Unmarshal(resp.Result, &message)
292
293	if bot.Debug {
294		log.Printf("SendPhoto resp: %+v\n", message)
295	}
296
297	return message, nil
298}
299
300// SendAudio sends or uploads an audio clip to a chat.
301// If using a file, the file must be in the .mp3 format.
302//
303// When the fields title and performer are both empty and
304// the mime-type of the file to be sent is not audio/mpeg,
305// the file must be an .ogg file encoded with OPUS.
306// You may use the tgutils.EncodeAudio func to assist you with this, if needed.
307//
308// Requires ChatID and FileID OR File.
309// ReplyToMessageID and ReplyMarkup are optional.
310// File should be either a string, FileBytes, or FileReader.
311func (bot *BotAPI) SendAudio(config AudioConfig) (Message, error) {
312	if config.UseExistingAudio {
313		v, err := config.Values()
314		if err != nil {
315			return Message{}, err
316		}
317
318		message, err := bot.MakeMessageRequest("sendAudio", v)
319		if err != nil {
320			return Message{}, err
321		}
322
323		return message, nil
324	}
325
326	params := make(map[string]string)
327
328	if config.ChannelUsername != "" {
329		params["chat_id"] = config.ChannelUsername
330	} else {
331		params["chat_id"] = strconv.Itoa(config.ChatID)
332	}
333	if config.ReplyToMessageID != 0 {
334		params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
335	}
336	if config.Duration != 0 {
337		params["duration"] = strconv.Itoa(config.Duration)
338	}
339	if config.ReplyMarkup != nil {
340		data, err := json.Marshal(config.ReplyMarkup)
341		if err != nil {
342			return Message{}, err
343		}
344
345		params["reply_markup"] = string(data)
346	}
347	if config.Performer != "" {
348		params["performer"] = config.Performer
349	}
350	if config.Title != "" {
351		params["title"] = config.Title
352	}
353
354	var file interface{}
355	if config.FilePath == "" {
356		file = config.File
357	} else {
358		file = config.FilePath
359	}
360
361	resp, err := bot.UploadFile("sendAudio", params, "audio", file)
362	if err != nil {
363		return Message{}, err
364	}
365
366	var message Message
367	json.Unmarshal(resp.Result, &message)
368
369	if bot.Debug {
370		log.Printf("sendAudio resp: %+v\n", message)
371	}
372
373	return message, nil
374}
375
376// SendDocument sends or uploads a document to a chat.
377//
378// Requires ChatID and FileID OR File.
379// ReplyToMessageID and ReplyMarkup are optional.
380// File should be either a string, FileBytes, or FileReader.
381func (bot *BotAPI) SendDocument(config DocumentConfig) (Message, error) {
382	if config.UseExistingDocument {
383		v, err := config.Values()
384		if err != nil {
385			return Message{}, err
386		}
387
388		message, err := bot.MakeMessageRequest("sendDocument", v)
389		if err != nil {
390			return Message{}, err
391		}
392
393		return message, nil
394	}
395
396	params := make(map[string]string)
397
398	if config.ChannelUsername != "" {
399		params["chat_id"] = config.ChannelUsername
400	} else {
401		params["chat_id"] = strconv.Itoa(config.ChatID)
402	}
403	if config.ReplyToMessageID != 0 {
404		params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
405	}
406	if config.ReplyMarkup != nil {
407		data, err := json.Marshal(config.ReplyMarkup)
408		if err != nil {
409			return Message{}, err
410		}
411
412		params["reply_markup"] = string(data)
413	}
414
415	var file interface{}
416	if config.FilePath == "" {
417		file = config.File
418	} else {
419		file = config.FilePath
420	}
421
422	resp, err := bot.UploadFile("sendDocument", params, "document", file)
423	if err != nil {
424		return Message{}, err
425	}
426
427	var message Message
428	json.Unmarshal(resp.Result, &message)
429
430	if bot.Debug {
431		log.Printf("sendDocument resp: %+v\n", message)
432	}
433
434	return message, nil
435}
436
437// SendVoice sends or uploads a playable voice to a chat.
438// If using a file, the file must be encoded as an .ogg with OPUS.
439// You may use the tgutils.EncodeAudio func to assist you with this, if needed.
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) SendVoice(config VoiceConfig) (Message, error) {
445	if config.UseExistingVoice {
446		v, err := config.Values()
447		if err != nil {
448			return Message{}, err
449		}
450
451		message, err := bot.MakeMessageRequest("sendVoice", v)
452		if err != nil {
453			return Message{}, err
454		}
455
456		return message, nil
457	}
458
459	params := make(map[string]string)
460
461	if config.ChannelUsername != "" {
462		params["chat_id"] = config.ChannelUsername
463	} else {
464		params["chat_id"] = strconv.Itoa(config.ChatID)
465	}
466	if config.ReplyToMessageID != 0 {
467		params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
468	}
469	if config.Duration != 0 {
470		params["duration"] = strconv.Itoa(config.Duration)
471	}
472	if config.ReplyMarkup != nil {
473		data, err := json.Marshal(config.ReplyMarkup)
474		if err != nil {
475			return Message{}, err
476		}
477
478		params["reply_markup"] = string(data)
479	}
480
481	var file interface{}
482	if config.FilePath == "" {
483		file = config.File
484	} else {
485		file = config.FilePath
486	}
487
488	resp, err := bot.UploadFile("SendVoice", params, "voice", file)
489	if err != nil {
490		return Message{}, err
491	}
492
493	var message Message
494	json.Unmarshal(resp.Result, &message)
495
496	if bot.Debug {
497		log.Printf("SendVoice resp: %+v\n", message)
498	}
499
500	return message, nil
501}
502
503// SendSticker sends or uploads a sticker to a chat.
504//
505// Requires ChatID and FileID OR File.
506// ReplyToMessageID and ReplyMarkup are optional.
507// File should be either a string, FileBytes, or FileReader.
508func (bot *BotAPI) SendSticker(config StickerConfig) (Message, error) {
509	if config.UseExistingSticker {
510		v, err := config.Values()
511		if err != nil {
512			return Message{}, err
513		}
514
515		message, err := bot.MakeMessageRequest("sendSticker", v)
516		if err != nil {
517			return Message{}, err
518		}
519
520		return message, nil
521	}
522
523	params := make(map[string]string)
524
525	if config.ChannelUsername != "" {
526		params["chat_id"] = config.ChannelUsername
527	} else {
528		params["chat_id"] = strconv.Itoa(config.ChatID)
529	}
530	if config.ReplyToMessageID != 0 {
531		params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
532	}
533	if config.ReplyMarkup != nil {
534		data, err := json.Marshal(config.ReplyMarkup)
535		if err != nil {
536			return Message{}, err
537		}
538
539		params["reply_markup"] = string(data)
540	}
541
542	var file interface{}
543	if config.FilePath == "" {
544		file = config.File
545	} else {
546		file = config.FilePath
547	}
548
549	resp, err := bot.UploadFile("sendSticker", params, "sticker", file)
550	if err != nil {
551		return Message{}, err
552	}
553
554	var message Message
555	json.Unmarshal(resp.Result, &message)
556
557	if bot.Debug {
558		log.Printf("sendSticker resp: %+v\n", message)
559	}
560
561	return message, nil
562}
563
564// SendVideo sends or uploads a video to a chat.
565//
566// Requires ChatID and FileID OR File.
567// ReplyToMessageID and ReplyMarkup are optional.
568// File should be either a string, FileBytes, or FileReader.
569func (bot *BotAPI) SendVideo(config VideoConfig) (Message, error) {
570	if config.UseExistingVideo {
571		v, err := config.Values()
572		if err != nil {
573			return Message{}, err
574		}
575
576		message, err := bot.MakeMessageRequest("sendVideo", v)
577		if err != nil {
578			return Message{}, err
579		}
580
581		return message, nil
582	}
583
584	params := make(map[string]string)
585
586	if config.ChannelUsername != "" {
587		params["chat_id"] = config.ChannelUsername
588	} else {
589		params["chat_id"] = strconv.Itoa(config.ChatID)
590	}
591	if config.ReplyToMessageID != 0 {
592		params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
593	}
594	if config.ReplyMarkup != nil {
595		data, err := json.Marshal(config.ReplyMarkup)
596		if err != nil {
597			return Message{}, err
598		}
599
600		params["reply_markup"] = string(data)
601	}
602
603	var file interface{}
604	if config.FilePath == "" {
605		file = config.File
606	} else {
607		file = config.FilePath
608	}
609
610	resp, err := bot.UploadFile("sendVideo", params, "video", file)
611	if err != nil {
612		return Message{}, err
613	}
614
615	var message Message
616	json.Unmarshal(resp.Result, &message)
617
618	if bot.Debug {
619		log.Printf("sendVideo resp: %+v\n", message)
620	}
621
622	return message, nil
623}
624
625// SendLocation sends a location to a chat.
626//
627// Requires ChatID, Latitude, and Longitude.
628// ReplyToMessageID and ReplyMarkup are optional.
629func (bot *BotAPI) SendLocation(config LocationConfig) (Message, error) {
630	v, err := config.Values()
631	if err != nil {
632		return Message{}, err
633	}
634
635	message, err := bot.MakeMessageRequest("sendLocation", v)
636	if err != nil {
637		return Message{}, err
638	}
639
640	return message, nil
641}
642
643// SendChatAction sets a current action in a chat.
644//
645// Requires ChatID and a valid Action (see Chat constants).
646func (bot *BotAPI) SendChatAction(config ChatActionConfig) error {
647	v, _ := config.Values()
648
649	_, err := bot.MakeRequest("sendChatAction", v)
650	if err != nil {
651		return err
652	}
653
654	return nil
655}
656
657// GetUserProfilePhotos gets a user's profile photos.
658//
659// Requires UserID.
660// Offset and Limit are optional.
661func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
662	v := url.Values{}
663	v.Add("user_id", strconv.Itoa(config.UserID))
664	if config.Offset != 0 {
665		v.Add("offset", strconv.Itoa(config.Offset))
666	}
667	if config.Limit != 0 {
668		v.Add("limit", strconv.Itoa(config.Limit))
669	}
670
671	resp, err := bot.MakeRequest("getUserProfilePhotos", v)
672	if err != nil {
673		return UserProfilePhotos{}, err
674	}
675
676	var profilePhotos UserProfilePhotos
677	json.Unmarshal(resp.Result, &profilePhotos)
678
679	bot.DebugLog("GetUserProfilePhoto", v, profilePhotos)
680
681	return profilePhotos, nil
682}
683
684// GetFile returns a file_id required to download a file.
685//
686// Requires FileID.
687func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
688	v := url.Values{}
689	v.Add("file_id", config.FileID)
690
691	resp, err := bot.MakeRequest("getFile", v)
692	if err != nil {
693		return File{}, err
694	}
695
696	var file File
697	json.Unmarshal(resp.Result, &file)
698
699	bot.DebugLog("GetFile", v, file)
700
701	return file, nil
702}
703
704// GetUpdates fetches updates.
705// If a WebHook is set, this will not return any data!
706//
707// Offset, Limit, and Timeout are optional.
708// To not get old items, set Offset to one higher than the previous item.
709// Set Timeout to a large number to reduce requests and get responses instantly.
710func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
711	v := url.Values{}
712	if config.Offset > 0 {
713		v.Add("offset", strconv.Itoa(config.Offset))
714	}
715	if config.Limit > 0 {
716		v.Add("limit", strconv.Itoa(config.Limit))
717	}
718	if config.Timeout > 0 {
719		v.Add("timeout", strconv.Itoa(config.Timeout))
720	}
721
722	resp, err := bot.MakeRequest("getUpdates", v)
723	if err != nil {
724		return []Update{}, err
725	}
726
727	var updates []Update
728	json.Unmarshal(resp.Result, &updates)
729
730	if bot.Debug {
731		log.Printf("getUpdates: %+v\n", updates)
732	}
733
734	return updates, nil
735}
736
737// SetWebhook sets a webhook.
738// If this is set, GetUpdates will not get any data!
739//
740// Requires URL OR to set Clear to true.
741func (bot *BotAPI) SetWebhook(config WebhookConfig) (APIResponse, error) {
742	if config.Certificate == nil {
743		v := url.Values{}
744		if !config.Clear {
745			v.Add("url", config.URL.String())
746		}
747
748		return bot.MakeRequest("setWebhook", v)
749	}
750
751	params := make(map[string]string)
752	params["url"] = config.URL.String()
753
754	resp, err := bot.UploadFile("setWebhook", params, "certificate", config.Certificate)
755	if err != nil {
756		return APIResponse{}, err
757	}
758
759	var apiResp APIResponse
760	json.Unmarshal(resp.Result, &apiResp)
761
762	if bot.Debug {
763		log.Printf("setWebhook resp: %+v\n", apiResp)
764	}
765
766	return apiResp, nil
767}
768
769// UpdatesChan starts a channel for getting updates.
770func (bot *BotAPI) UpdatesChan(config UpdateConfig) error {
771	bot.Updates = make(chan Update, 100)
772
773	go func() {
774		for {
775			updates, err := bot.GetUpdates(config)
776			if err != nil {
777				log.Println(err)
778				log.Println("Failed to get updates, retrying in 3 seconds...")
779				time.Sleep(time.Second * 3)
780
781				continue
782			}
783
784			for _, update := range updates {
785				if update.UpdateID >= config.Offset {
786					config.Offset = update.UpdateID + 1
787					bot.Updates <- update
788				}
789			}
790		}
791	}()
792
793	return nil
794}
795
796// ListenForWebhook registers a http handler for a webhook.
797func (bot *BotAPI) ListenForWebhook(pattern string) {
798	bot.Updates = make(chan Update, 100)
799
800	http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
801		bytes, _ := ioutil.ReadAll(r.Body)
802
803		var update Update
804		json.Unmarshal(bytes, &update)
805
806		bot.Updates <- update
807	})
808}