all repos — telegram-bot-api @ 4610c561c6467a0884374769b29f907ec7e8d961

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	return message, nil
 93}
 94
 95// UploadFile makes a request to the API with a file.
 96//
 97// Requires the parameter to hold the file not be in the params.
 98// File should be a string to a file path, a FileBytes struct, or a FileReader struct.
 99func (bot *BotAPI) UploadFile(endpoint string, params map[string]string, fieldname string, file interface{}) (APIResponse, error) {
100	ms := multipartstreamer.New()
101	ms.WriteFields(params)
102
103	switch f := file.(type) {
104	case string:
105		fileHandle, err := os.Open(f)
106		if err != nil {
107			return APIResponse{}, err
108		}
109		defer fileHandle.Close()
110
111		fi, err := os.Stat(f)
112		if err != nil {
113			return APIResponse{}, err
114		}
115
116		ms.WriteReader(fieldname, fileHandle.Name(), fi.Size(), fileHandle)
117	case FileBytes:
118		buf := bytes.NewBuffer(f.Bytes)
119		ms.WriteReader(fieldname, f.Name, int64(len(f.Bytes)), buf)
120	case FileReader:
121		if f.Size == -1 {
122			data, err := ioutil.ReadAll(f.Reader)
123			if err != nil {
124				return APIResponse{}, err
125			}
126			buf := bytes.NewBuffer(data)
127
128			ms.WriteReader(fieldname, f.Name, int64(len(data)), buf)
129
130			break
131		}
132
133		ms.WriteReader(fieldname, f.Name, f.Size, f.Reader)
134	default:
135		return APIResponse{}, errors.New("bad file type")
136	}
137
138	req, err := http.NewRequest("POST", fmt.Sprintf(APIEndpoint, bot.Token, endpoint), nil)
139	ms.SetupRequest(req)
140	if err != nil {
141		return APIResponse{}, err
142	}
143
144	res, err := bot.Client.Do(req)
145	if err != nil {
146		return APIResponse{}, err
147	}
148	defer res.Body.Close()
149
150	bytes, err := ioutil.ReadAll(res.Body)
151	if err != nil {
152		return APIResponse{}, err
153	}
154
155	if bot.Debug {
156		log.Println(string(bytes[:]))
157	}
158
159	var apiResp APIResponse
160	json.Unmarshal(bytes, &apiResp)
161
162	if !apiResp.Ok {
163		return APIResponse{}, errors.New(apiResp.Description)
164	}
165
166	return apiResp, nil
167}
168
169// GetMe fetches the currently authenticated bot.
170//
171// There are no parameters for this method.
172func (bot *BotAPI) GetMe() (User, error) {
173	resp, err := bot.MakeRequest("getMe", nil)
174	if err != nil {
175		return User{}, err
176	}
177
178	var user User
179	json.Unmarshal(resp.Result, &user)
180
181	if bot.Debug {
182		log.Printf("getMe: %+v\n", user)
183	}
184
185	return user, nil
186}
187
188func (bot *BotAPI) Send(c Chattable) error {
189	return nil
190}
191
192// SendMessage sends a Message to a chat.
193//
194// Requires ChatID and Text.
195// DisableWebPagePreview, ReplyToMessageID, and ReplyMarkup are optional.
196func (bot *BotAPI) SendMessage(config MessageConfig) (Message, error) {
197	v, err := config.Values()
198
199	if err != nil {
200		return Message{}, err
201	}
202
203	message, err := bot.MakeMessageRequest("SendMessage", v)
204
205	if err != nil {
206		return Message{}, err
207	}
208
209	if bot.Debug {
210		log.Printf("SendMessage req : %+v\n", v)
211		log.Printf("SendMessage resp: %+v\n", message)
212	}
213
214	return message, nil
215}
216
217// ForwardMessage forwards a message from one chat to another.
218//
219// Requires ChatID (destination), FromChatID (source), and MessageID.
220func (bot *BotAPI) ForwardMessage(config ForwardConfig) (Message, error) {
221	v, _ := config.Values()
222
223	message, err := bot.MakeMessageRequest("forwardMessage", v)
224	if err != nil {
225		return Message{}, err
226	}
227
228	if bot.Debug {
229		log.Printf("forwardMessage req : %+v\n", v)
230		log.Printf("forwardMessage resp: %+v\n", message)
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		if bot.Debug {
255			log.Printf("SendPhoto req : %+v\n", v)
256			log.Printf("SendPhoto resp: %+v\n", message)
257		}
258
259		return message, nil
260	}
261
262	params := make(map[string]string)
263	if config.ChannelUsername != "" {
264		params["chat_id"] = config.ChannelUsername
265	} else {
266		params["chat_id"] = strconv.Itoa(config.ChatID)
267	}
268	if config.Caption != "" {
269		params["caption"] = config.Caption
270	}
271	if config.ReplyToMessageID != 0 {
272		params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
273	}
274	if config.ReplyMarkup != nil {
275		data, err := json.Marshal(config.ReplyMarkup)
276		if err != nil {
277			return Message{}, err
278		}
279
280		params["reply_markup"] = string(data)
281	}
282
283	var file interface{}
284	if config.FilePath == "" {
285		file = config.File
286	} else {
287		file = config.FilePath
288	}
289
290	resp, err := bot.UploadFile("SendPhoto", params, "photo", file)
291	if err != nil {
292		return Message{}, err
293	}
294
295	var message Message
296	json.Unmarshal(resp.Result, &message)
297
298	if bot.Debug {
299		log.Printf("SendPhoto resp: %+v\n", message)
300	}
301
302	return message, nil
303}
304
305// SendAudio sends or uploads an audio clip to a chat.
306// If using a file, the file must be in the .mp3 format.
307//
308// When the fields title and performer are both empty and
309// the mime-type of the file to be sent is not audio/mpeg,
310// the file must be an .ogg file encoded with OPUS.
311// You may use the tgutils.EncodeAudio func to assist you with this, if needed.
312//
313// Requires ChatID and FileID OR File.
314// ReplyToMessageID and ReplyMarkup are optional.
315// File should be either a string, FileBytes, or FileReader.
316func (bot *BotAPI) SendAudio(config AudioConfig) (Message, error) {
317	if config.UseExistingAudio {
318		v, err := config.Values()
319		if err != nil {
320			return Message{}, err
321		}
322
323		message, err := bot.MakeMessageRequest("sendAudio", v)
324		if err != nil {
325			return Message{}, err
326		}
327
328		if bot.Debug {
329			log.Printf("sendAudio req : %+v\n", v)
330			log.Printf("sendAudio resp: %+v\n", message)
331		}
332
333		return message, nil
334	}
335
336	params := make(map[string]string)
337
338	if config.ChannelUsername != "" {
339		params["chat_id"] = config.ChannelUsername
340	} else {
341		params["chat_id"] = strconv.Itoa(config.ChatID)
342	}
343	if config.ReplyToMessageID != 0 {
344		params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
345	}
346	if config.Duration != 0 {
347		params["duration"] = strconv.Itoa(config.Duration)
348	}
349	if config.ReplyMarkup != nil {
350		data, err := json.Marshal(config.ReplyMarkup)
351		if err != nil {
352			return Message{}, err
353		}
354
355		params["reply_markup"] = string(data)
356	}
357	if config.Performer != "" {
358		params["performer"] = config.Performer
359	}
360	if config.Title != "" {
361		params["title"] = config.Title
362	}
363
364	var file interface{}
365	if config.FilePath == "" {
366		file = config.File
367	} else {
368		file = config.FilePath
369	}
370
371	resp, err := bot.UploadFile("sendAudio", params, "audio", file)
372	if err != nil {
373		return Message{}, err
374	}
375
376	var message Message
377	json.Unmarshal(resp.Result, &message)
378
379	if bot.Debug {
380		log.Printf("sendAudio resp: %+v\n", message)
381	}
382
383	return message, nil
384}
385
386// SendDocument sends or uploads a document to a chat.
387//
388// Requires ChatID and FileID OR File.
389// ReplyToMessageID and ReplyMarkup are optional.
390// File should be either a string, FileBytes, or FileReader.
391func (bot *BotAPI) SendDocument(config DocumentConfig) (Message, error) {
392	if config.UseExistingDocument {
393		v, err := config.Values()
394		if err != nil {
395			return Message{}, err
396		}
397
398		message, err := bot.MakeMessageRequest("sendDocument", v)
399		if err != nil {
400			return Message{}, err
401		}
402
403		if bot.Debug {
404			log.Printf("sendDocument req : %+v\n", v)
405			log.Printf("sendDocument resp: %+v\n", message)
406		}
407
408		return message, nil
409	}
410
411	params := make(map[string]string)
412
413	if config.ChannelUsername != "" {
414		params["chat_id"] = config.ChannelUsername
415	} else {
416		params["chat_id"] = strconv.Itoa(config.ChatID)
417	}
418	if config.ReplyToMessageID != 0 {
419		params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
420	}
421	if config.ReplyMarkup != nil {
422		data, err := json.Marshal(config.ReplyMarkup)
423		if err != nil {
424			return Message{}, err
425		}
426
427		params["reply_markup"] = string(data)
428	}
429
430	var file interface{}
431	if config.FilePath == "" {
432		file = config.File
433	} else {
434		file = config.FilePath
435	}
436
437	resp, err := bot.UploadFile("sendDocument", params, "document", file)
438	if err != nil {
439		return Message{}, err
440	}
441
442	var message Message
443	json.Unmarshal(resp.Result, &message)
444
445	if bot.Debug {
446		log.Printf("sendDocument resp: %+v\n", message)
447	}
448
449	return message, nil
450}
451
452// SendVoice sends or uploads a playable voice to a chat.
453// If using a file, the file must be encoded as an .ogg with OPUS.
454// You may use the tgutils.EncodeAudio func to assist you with this, if needed.
455//
456// Requires ChatID and FileID OR File.
457// ReplyToMessageID and ReplyMarkup are optional.
458// File should be either a string, FileBytes, or FileReader.
459func (bot *BotAPI) SendVoice(config VoiceConfig) (Message, error) {
460	if config.UseExistingVoice {
461		v, err := config.Values()
462		if err != nil {
463			return Message{}, err
464		}
465
466		message, err := bot.MakeMessageRequest("sendVoice", v)
467		if err != nil {
468			return Message{}, err
469		}
470
471		if bot.Debug {
472			log.Printf("SendVoice req : %+v\n", v)
473			log.Printf("SendVoice resp: %+v\n", message)
474		}
475
476		return message, nil
477	}
478
479	params := make(map[string]string)
480
481	if config.ChannelUsername != "" {
482		params["chat_id"] = config.ChannelUsername
483	} else {
484		params["chat_id"] = strconv.Itoa(config.ChatID)
485	}
486	if config.ReplyToMessageID != 0 {
487		params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
488	}
489	if config.Duration != 0 {
490		params["duration"] = strconv.Itoa(config.Duration)
491	}
492	if config.ReplyMarkup != nil {
493		data, err := json.Marshal(config.ReplyMarkup)
494		if err != nil {
495			return Message{}, err
496		}
497
498		params["reply_markup"] = string(data)
499	}
500
501	var file interface{}
502	if config.FilePath == "" {
503		file = config.File
504	} else {
505		file = config.FilePath
506	}
507
508	resp, err := bot.UploadFile("SendVoice", params, "voice", file)
509	if err != nil {
510		return Message{}, err
511	}
512
513	var message Message
514	json.Unmarshal(resp.Result, &message)
515
516	if bot.Debug {
517		log.Printf("SendVoice resp: %+v\n", message)
518	}
519
520	return message, nil
521}
522
523// SendSticker sends or uploads a sticker to a chat.
524//
525// Requires ChatID and FileID OR File.
526// ReplyToMessageID and ReplyMarkup are optional.
527// File should be either a string, FileBytes, or FileReader.
528func (bot *BotAPI) SendSticker(config StickerConfig) (Message, error) {
529	if config.UseExistingSticker {
530		v, err := config.Values()
531		if err != nil {
532			return Message{}, err
533		}
534
535		message, err := bot.MakeMessageRequest("sendSticker", v)
536		if err != nil {
537			return Message{}, err
538		}
539
540		if bot.Debug {
541			log.Printf("sendSticker req : %+v\n", v)
542			log.Printf("sendSticker resp: %+v\n", message)
543		}
544
545		return message, nil
546	}
547
548	params := make(map[string]string)
549
550	if config.ChannelUsername != "" {
551		params["chat_id"] = config.ChannelUsername
552	} else {
553		params["chat_id"] = strconv.Itoa(config.ChatID)
554	}
555	if config.ReplyToMessageID != 0 {
556		params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
557	}
558	if config.ReplyMarkup != nil {
559		data, err := json.Marshal(config.ReplyMarkup)
560		if err != nil {
561			return Message{}, err
562		}
563
564		params["reply_markup"] = string(data)
565	}
566
567	var file interface{}
568	if config.FilePath == "" {
569		file = config.File
570	} else {
571		file = config.FilePath
572	}
573
574	resp, err := bot.UploadFile("sendSticker", params, "sticker", file)
575	if err != nil {
576		return Message{}, err
577	}
578
579	var message Message
580	json.Unmarshal(resp.Result, &message)
581
582	if bot.Debug {
583		log.Printf("sendSticker resp: %+v\n", message)
584	}
585
586	return message, nil
587}
588
589// SendVideo sends or uploads a video to a chat.
590//
591// Requires ChatID and FileID OR File.
592// ReplyToMessageID and ReplyMarkup are optional.
593// File should be either a string, FileBytes, or FileReader.
594func (bot *BotAPI) SendVideo(config VideoConfig) (Message, error) {
595	if config.UseExistingVideo {
596		v, err := config.Values()
597		if err != nil {
598			return Message{}, err
599		}
600
601		message, err := bot.MakeMessageRequest("sendVideo", v)
602		if err != nil {
603			return Message{}, err
604		}
605
606		if bot.Debug {
607			log.Printf("sendVideo req : %+v\n", v)
608			log.Printf("sendVideo resp: %+v\n", message)
609		}
610
611		return message, nil
612	}
613
614	params := make(map[string]string)
615
616	if config.ChannelUsername != "" {
617		params["chat_id"] = config.ChannelUsername
618	} else {
619		params["chat_id"] = strconv.Itoa(config.ChatID)
620	}
621	if config.ReplyToMessageID != 0 {
622		params["reply_to_message_id"] = strconv.Itoa(config.ReplyToMessageID)
623	}
624	if config.ReplyMarkup != nil {
625		data, err := json.Marshal(config.ReplyMarkup)
626		if err != nil {
627			return Message{}, err
628		}
629
630		params["reply_markup"] = string(data)
631	}
632
633	var file interface{}
634	if config.FilePath == "" {
635		file = config.File
636	} else {
637		file = config.FilePath
638	}
639
640	resp, err := bot.UploadFile("sendVideo", params, "video", file)
641	if err != nil {
642		return Message{}, err
643	}
644
645	var message Message
646	json.Unmarshal(resp.Result, &message)
647
648	if bot.Debug {
649		log.Printf("sendVideo resp: %+v\n", message)
650	}
651
652	return message, nil
653}
654
655// SendLocation sends a location to a chat.
656//
657// Requires ChatID, Latitude, and Longitude.
658// ReplyToMessageID and ReplyMarkup are optional.
659func (bot *BotAPI) SendLocation(config LocationConfig) (Message, error) {
660	v, err := config.Values()
661	if err != nil {
662		return Message{}, err
663	}
664
665	message, err := bot.MakeMessageRequest("sendLocation", v)
666	if err != nil {
667		return Message{}, err
668	}
669
670	if bot.Debug {
671		log.Printf("sendLocation req : %+v\n", v)
672		log.Printf("sendLocation resp: %+v\n", message)
673	}
674
675	return message, nil
676}
677
678// SendChatAction sets a current action in a chat.
679//
680// Requires ChatID and a valid Action (see Chat constants).
681func (bot *BotAPI) SendChatAction(config ChatActionConfig) error {
682	v, _ := config.Values()
683
684	_, err := bot.MakeRequest("sendChatAction", v)
685	if err != nil {
686		return err
687	}
688
689	return nil
690}
691
692// GetUserProfilePhotos gets a user's profile photos.
693//
694// Requires UserID.
695// Offset and Limit are optional.
696func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
697	v := url.Values{}
698	v.Add("user_id", strconv.Itoa(config.UserID))
699	if config.Offset != 0 {
700		v.Add("offset", strconv.Itoa(config.Offset))
701	}
702	if config.Limit != 0 {
703		v.Add("limit", strconv.Itoa(config.Limit))
704	}
705
706	resp, err := bot.MakeRequest("getUserProfilePhotos", v)
707	if err != nil {
708		return UserProfilePhotos{}, err
709	}
710
711	var profilePhotos UserProfilePhotos
712	json.Unmarshal(resp.Result, &profilePhotos)
713
714	if bot.Debug {
715		log.Printf("getUserProfilePhotos req : %+v\n", v)
716		log.Printf("getUserProfilePhotos resp: %+v\n", profilePhotos)
717	}
718
719	return profilePhotos, nil
720}
721
722// GetFile returns a file_id required to download a file.
723//
724// Requires FileID.
725func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
726	v := url.Values{}
727	v.Add("file_id", config.FileID)
728
729	resp, err := bot.MakeRequest("getFile", v)
730	if err != nil {
731		return File{}, err
732	}
733
734	var file File
735	json.Unmarshal(resp.Result, &file)
736
737	if bot.Debug {
738		log.Printf("getFile req : %+v\n", v)
739		log.Printf("getFile resp: %+v\n", file)
740	}
741
742	return file, nil
743}
744
745// GetUpdates fetches updates.
746// If a WebHook is set, this will not return any data!
747//
748// Offset, Limit, and Timeout are optional.
749// To not get old items, set Offset to one higher than the previous item.
750// Set Timeout to a large number to reduce requests and get responses instantly.
751func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
752	v := url.Values{}
753	if config.Offset > 0 {
754		v.Add("offset", strconv.Itoa(config.Offset))
755	}
756	if config.Limit > 0 {
757		v.Add("limit", strconv.Itoa(config.Limit))
758	}
759	if config.Timeout > 0 {
760		v.Add("timeout", strconv.Itoa(config.Timeout))
761	}
762
763	resp, err := bot.MakeRequest("getUpdates", v)
764	if err != nil {
765		return []Update{}, err
766	}
767
768	var updates []Update
769	json.Unmarshal(resp.Result, &updates)
770
771	if bot.Debug {
772		log.Printf("getUpdates: %+v\n", updates)
773	}
774
775	return updates, nil
776}
777
778// SetWebhook sets a webhook.
779// If this is set, GetUpdates will not get any data!
780//
781// Requires URL OR to set Clear to true.
782func (bot *BotAPI) SetWebhook(config WebhookConfig) (APIResponse, error) {
783	if config.Certificate == nil {
784		v := url.Values{}
785		if !config.Clear {
786			v.Add("url", config.URL.String())
787		}
788
789		return bot.MakeRequest("setWebhook", v)
790	}
791
792	params := make(map[string]string)
793	params["url"] = config.URL.String()
794
795	resp, err := bot.UploadFile("setWebhook", params, "certificate", config.Certificate)
796	if err != nil {
797		return APIResponse{}, err
798	}
799
800	var apiResp APIResponse
801	json.Unmarshal(resp.Result, &apiResp)
802
803	if bot.Debug {
804		log.Printf("setWebhook resp: %+v\n", apiResp)
805	}
806
807	return apiResp, nil
808}
809
810// UpdatesChan starts a channel for getting updates.
811func (bot *BotAPI) UpdatesChan(config UpdateConfig) error {
812	bot.Updates = make(chan Update, 100)
813
814	go func() {
815		for {
816			updates, err := bot.GetUpdates(config)
817			if err != nil {
818				log.Println(err)
819				log.Println("Failed to get updates, retrying in 3 seconds...")
820				time.Sleep(time.Second * 3)
821
822				continue
823			}
824
825			for _, update := range updates {
826				if update.UpdateID >= config.Offset {
827					config.Offset = update.UpdateID + 1
828					bot.Updates <- update
829				}
830			}
831		}
832	}()
833
834	return nil
835}
836
837// ListenForWebhook registers a http handler for a webhook.
838func (bot *BotAPI) ListenForWebhook(pattern string) {
839	bot.Updates = make(chan Update, 100)
840
841	http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
842		bytes, _ := ioutil.ReadAll(r.Body)
843
844		var update Update
845		json.Unmarshal(bytes, &update)
846
847		bot.Updates <- update
848	})
849}