all repos — telegram-bot-api @ 92ce2aad9403b2ee7918c92f4ed17db403ba9552

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