all repos — telegram-bot-api @ 4fe428c77a68a9903ab5bfcaecb978003ce0424b

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	"encoding/json"
  7	"errors"
  8	"fmt"
  9	"io"
 10	"io/ioutil"
 11	"mime/multipart"
 12	"net/http"
 13	"net/url"
 14	"strings"
 15	"time"
 16)
 17
 18// HTTPClient is the type needed for the bot to perform HTTP requests.
 19type HTTPClient interface {
 20	Do(req *http.Request) (*http.Response, error)
 21}
 22
 23// BotAPI allows you to interact with the Telegram Bot API.
 24type BotAPI struct {
 25	Token  string `json:"token"`
 26	Debug  bool   `json:"debug"`
 27	Buffer int    `json:"buffer"`
 28
 29	Self            User       `json:"-"`
 30	Client          HTTPClient `json:"-"`
 31	shutdownChannel chan interface{}
 32
 33	apiEndpoint string
 34}
 35
 36// NewBotAPI creates a new BotAPI instance.
 37//
 38// It requires a token, provided by @BotFather on Telegram.
 39func NewBotAPI(token string) (*BotAPI, error) {
 40	return NewBotAPIWithClient(token, APIEndpoint, &http.Client{})
 41}
 42
 43// NewBotAPIWithAPIEndpoint creates a new BotAPI instance
 44// and allows you to pass API endpoint.
 45//
 46// It requires a token, provided by @BotFather on Telegram and API endpoint.
 47func NewBotAPIWithAPIEndpoint(token, apiEndpoint string) (*BotAPI, error) {
 48	return NewBotAPIWithClient(token, apiEndpoint, &http.Client{})
 49}
 50
 51// NewBotAPIWithClient creates a new BotAPI instance
 52// and allows you to pass a http.Client.
 53//
 54// It requires a token, provided by @BotFather on Telegram and API endpoint.
 55func NewBotAPIWithClient(token, apiEndpoint string, client HTTPClient) (*BotAPI, error) {
 56	bot := &BotAPI{
 57		Token:           token,
 58		Client:          client,
 59		Buffer:          100,
 60		shutdownChannel: make(chan interface{}),
 61
 62		apiEndpoint: apiEndpoint,
 63	}
 64
 65	self, err := bot.GetMe()
 66	if err != nil {
 67		return nil, err
 68	}
 69
 70	bot.Self = self
 71
 72	return bot, nil
 73}
 74
 75// SetAPIEndpoint changes the Telegram Bot API endpoint used by the instance.
 76func (bot *BotAPI) SetAPIEndpoint(apiEndpoint string) {
 77	bot.apiEndpoint = apiEndpoint
 78}
 79
 80func buildParams(in Params) url.Values {
 81	if in == nil {
 82		return url.Values{}
 83	}
 84
 85	out := url.Values{}
 86
 87	for key, value := range in {
 88		out.Set(key, value)
 89	}
 90
 91	return out
 92}
 93
 94// MakeRequest makes a request to a specific endpoint with our token.
 95func (bot *BotAPI) MakeRequest(endpoint string, params Params) (*APIResponse, error) {
 96	if bot.Debug {
 97		log.Printf("Endpoint: %s, params: %v\n", endpoint, params)
 98	}
 99
100	method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint)
101
102	values := buildParams(params)
103
104	req, err := http.NewRequest("POST", method, strings.NewReader(values.Encode()))
105	if err != nil {
106		return &APIResponse{}, err
107	}
108	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
109
110	resp, err := bot.Client.Do(req)
111	if err != nil {
112		return nil, err
113	}
114	defer resp.Body.Close()
115
116	var apiResp APIResponse
117	bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
118	if err != nil {
119		return &apiResp, err
120	}
121
122	if bot.Debug {
123		log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
124	}
125
126	if !apiResp.Ok {
127		var parameters ResponseParameters
128
129		if apiResp.Parameters != nil {
130			parameters = *apiResp.Parameters
131		}
132
133		return &apiResp, &Error{
134			Code:               apiResp.ErrorCode,
135			Message:            apiResp.Description,
136			ResponseParameters: parameters,
137		}
138	}
139
140	return &apiResp, nil
141}
142
143// decodeAPIResponse decode response and return slice of bytes if debug enabled.
144// If debug disabled, just decode http.Response.Body stream to APIResponse struct
145// for efficient memory usage
146func (bot *BotAPI) decodeAPIResponse(responseBody io.Reader, resp *APIResponse) ([]byte, error) {
147	if !bot.Debug {
148		dec := json.NewDecoder(responseBody)
149		err := dec.Decode(resp)
150		return nil, err
151	}
152
153	// if debug, read response body
154	data, err := ioutil.ReadAll(responseBody)
155	if err != nil {
156		return nil, err
157	}
158
159	err = json.Unmarshal(data, resp)
160	if err != nil {
161		return nil, err
162	}
163
164	return data, nil
165}
166
167// UploadFiles makes a request to the API with files.
168func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFile) (*APIResponse, error) {
169	r, w := io.Pipe()
170	m := multipart.NewWriter(w)
171
172	// This code modified from the very helpful @HirbodBehnam
173	// https://github.com/go-telegram-bot-api/telegram-bot-api/issues/354#issuecomment-663856473
174	go func() {
175		defer w.Close()
176		defer m.Close()
177
178		for field, value := range params {
179			if err := m.WriteField(field, value); err != nil {
180				w.CloseWithError(err)
181				return
182			}
183		}
184
185		for _, file := range files {
186			if file.Data.NeedsUpload() {
187				name, reader, err := file.Data.UploadData()
188				if err != nil {
189					w.CloseWithError(err)
190					return
191				}
192
193				part, err := m.CreateFormFile(file.Name, name)
194				if err != nil {
195					w.CloseWithError(err)
196					return
197				}
198
199				if _, err := io.Copy(part, reader); err != nil {
200					w.CloseWithError(err)
201					return
202				}
203
204				if closer, ok := reader.(io.ReadCloser); ok {
205					if err = closer.Close(); err != nil {
206						w.CloseWithError(err)
207						return
208					}
209				}
210			} else {
211				value := file.Data.SendData()
212
213				if err := m.WriteField(file.Name, value); err != nil {
214					w.CloseWithError(err)
215					return
216				}
217			}
218		}
219	}()
220
221	if bot.Debug {
222		log.Printf("Endpoint: %s, params: %v, with %d files\n", endpoint, params, len(files))
223	}
224
225	method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint)
226
227	req, err := http.NewRequest("POST", method, r)
228	if err != nil {
229		return nil, err
230	}
231
232	req.Header.Set("Content-Type", m.FormDataContentType())
233
234	resp, err := bot.Client.Do(req)
235	if err != nil {
236		return nil, err
237	}
238	defer resp.Body.Close()
239
240	var apiResp APIResponse
241	bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
242	if err != nil {
243		return &apiResp, err
244	}
245
246	if bot.Debug {
247		log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
248	}
249
250	if !apiResp.Ok {
251		var parameters ResponseParameters
252
253		if apiResp.Parameters != nil {
254			parameters = *apiResp.Parameters
255		}
256
257		return &apiResp, &Error{
258			Message:            apiResp.Description,
259			ResponseParameters: parameters,
260		}
261	}
262
263	return &apiResp, nil
264}
265
266// GetFileDirectURL returns direct URL to file
267//
268// It requires the FileID.
269func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) {
270	file, err := bot.GetFile(FileConfig{fileID})
271
272	if err != nil {
273		return "", err
274	}
275
276	return file.Link(bot.Token), nil
277}
278
279// GetMe fetches the currently authenticated bot.
280//
281// This method is called upon creation to validate the token,
282// and so you may get this data from BotAPI.Self without the need for
283// another request.
284func (bot *BotAPI) GetMe() (User, error) {
285	resp, err := bot.MakeRequest("getMe", nil)
286	if err != nil {
287		return User{}, err
288	}
289
290	var user User
291	err = json.Unmarshal(resp.Result, &user)
292
293	return user, err
294}
295
296// IsMessageToMe returns true if message directed to this bot.
297//
298// It requires the Message.
299func (bot *BotAPI) IsMessageToMe(message Message) bool {
300	return strings.Contains(message.Text, "@"+bot.Self.UserName)
301}
302
303func hasFilesNeedingUpload(files []RequestFile) bool {
304	for _, file := range files {
305		if file.Data.NeedsUpload() {
306			return true
307		}
308	}
309
310	return false
311}
312
313// Request sends a Chattable to Telegram, and returns the APIResponse.
314func (bot *BotAPI) Request(c Chattable) (*APIResponse, error) {
315	params, err := c.params()
316	if err != nil {
317		return nil, err
318	}
319
320	if t, ok := c.(Fileable); ok {
321		files := t.files()
322
323		// If we have files that need to be uploaded, we should delegate the
324		// request to UploadFile.
325		if hasFilesNeedingUpload(files) {
326			return bot.UploadFiles(t.method(), params, files)
327		}
328
329		// However, if there are no files to be uploaded, there's likely things
330		// that need to be turned into params instead.
331		for _, file := range files {
332			params[file.Name] = file.Data.SendData()
333		}
334	}
335
336	return bot.MakeRequest(c.method(), params)
337}
338
339// Send will send a Chattable item to Telegram and provides the
340// returned Message.
341func (bot *BotAPI) Send(c Chattable) (Message, error) {
342	resp, err := bot.Request(c)
343	if err != nil {
344		return Message{}, err
345	}
346
347	var message Message
348	err = json.Unmarshal(resp.Result, &message)
349
350	return message, err
351}
352
353// SendMediaGroup sends a media group and returns the resulting messages.
354func (bot *BotAPI) SendMediaGroup(config MediaGroupConfig) ([]Message, error) {
355	resp, err := bot.Request(config)
356	if err != nil {
357		return nil, err
358	}
359
360	var messages []Message
361	err = json.Unmarshal(resp.Result, &messages)
362
363	return messages, err
364}
365
366// GetUserProfilePhotos gets a user's profile photos.
367//
368// It requires UserID.
369// Offset and Limit are optional.
370func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
371	resp, err := bot.Request(config)
372	if err != nil {
373		return UserProfilePhotos{}, err
374	}
375
376	var profilePhotos UserProfilePhotos
377	err = json.Unmarshal(resp.Result, &profilePhotos)
378
379	return profilePhotos, err
380}
381
382// GetFile returns a File which can download a file from Telegram.
383//
384// Requires FileID.
385func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
386	resp, err := bot.Request(config)
387	if err != nil {
388		return File{}, err
389	}
390
391	var file File
392	err = json.Unmarshal(resp.Result, &file)
393
394	return file, err
395}
396
397// GetUpdates fetches updates.
398// If a WebHook is set, this will not return any data!
399//
400// Offset, Limit, Timeout, and AllowedUpdates are optional.
401// To avoid stale items, set Offset to one higher than the previous item.
402// Set Timeout to a large number to reduce requests, so you can get updates
403// instantly instead of having to wait between requests.
404func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
405	resp, err := bot.Request(config)
406	if err != nil {
407		return []Update{}, err
408	}
409
410	var updates []Update
411	err = json.Unmarshal(resp.Result, &updates)
412
413	return updates, err
414}
415
416// GetWebhookInfo allows you to fetch information about a webhook and if
417// one currently is set, along with pending update count and error messages.
418func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) {
419	resp, err := bot.MakeRequest("getWebhookInfo", nil)
420	if err != nil {
421		return WebhookInfo{}, err
422	}
423
424	var info WebhookInfo
425	err = json.Unmarshal(resp.Result, &info)
426
427	return info, err
428}
429
430// GetUpdatesChan starts and returns a channel for getting updates.
431func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) UpdatesChannel {
432	ch := make(chan Update, bot.Buffer)
433
434	go func() {
435		for {
436			select {
437			case <-bot.shutdownChannel:
438				close(ch)
439				return
440			default:
441			}
442
443			updates, err := bot.GetUpdates(config)
444			if err != nil {
445				log.Println(err)
446				log.Println("Failed to get updates, retrying in 3 seconds...")
447				time.Sleep(time.Second * 3)
448
449				continue
450			}
451
452			for _, update := range updates {
453				if update.UpdateID >= config.Offset {
454					config.Offset = update.UpdateID + 1
455					ch <- update
456				}
457			}
458		}
459	}()
460
461	return ch
462}
463
464// StopReceivingUpdates stops the go routine which receives updates
465func (bot *BotAPI) StopReceivingUpdates() {
466	if bot.Debug {
467		log.Println("Stopping the update receiver routine...")
468	}
469	close(bot.shutdownChannel)
470}
471
472// ListenForWebhook registers a http handler for a webhook.
473func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
474	ch := make(chan Update, bot.Buffer)
475
476	http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
477		update, err := bot.HandleUpdate(r)
478		if err != nil {
479			errMsg, _ := json.Marshal(map[string]string{"error": err.Error()})
480			w.WriteHeader(http.StatusBadRequest)
481			w.Header().Set("Content-Type", "application/json")
482			_, _ = w.Write(errMsg)
483			return
484		}
485
486		ch <- *update
487	})
488
489	return ch
490}
491
492// ListenForWebhookRespReqFormat registers a http handler for a single incoming webhook.
493func (bot *BotAPI) ListenForWebhookRespReqFormat(w http.ResponseWriter, r *http.Request) UpdatesChannel {
494	ch := make(chan Update, bot.Buffer)
495
496	func(w http.ResponseWriter, r *http.Request) {
497		update, err := bot.HandleUpdate(r)
498		if err != nil {
499			errMsg, _ := json.Marshal(map[string]string{"error": err.Error()})
500			w.WriteHeader(http.StatusBadRequest)
501			w.Header().Set("Content-Type", "application/json")
502			_, _ = w.Write(errMsg)
503			return
504		}
505
506		ch <- *update
507		close(ch)
508	}(w, r)
509
510	return ch
511}
512
513// HandleUpdate parses and returns update received via webhook
514func (bot *BotAPI) HandleUpdate(r *http.Request) (*Update, error) {
515	if r.Method != http.MethodPost {
516		err := errors.New("wrong HTTP method required POST")
517		return nil, err
518	}
519
520	var update Update
521	err := json.NewDecoder(r.Body).Decode(&update)
522	if err != nil {
523		return nil, err
524	}
525
526	return &update, nil
527}
528
529// WriteToHTTPResponse writes the request to the HTTP ResponseWriter.
530//
531// It doesn't support uploading files.
532//
533// See https://core.telegram.org/bots/api#making-requests-when-getting-updates
534// for details.
535func WriteToHTTPResponse(w http.ResponseWriter, c Chattable) error {
536	params, err := c.params()
537	if err != nil {
538		return err
539	}
540
541	if t, ok := c.(Fileable); ok {
542		if hasFilesNeedingUpload(t.files()) {
543			return errors.New("unable to use http response to upload files")
544		}
545	}
546
547	values := buildParams(params)
548	values.Set("method", c.method())
549
550	w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
551	_, err = w.Write([]byte(values.Encode()))
552	return err
553}
554
555// GetChat gets information about a chat.
556func (bot *BotAPI) GetChat(config ChatInfoConfig) (Chat, error) {
557	resp, err := bot.Request(config)
558	if err != nil {
559		return Chat{}, err
560	}
561
562	var chat Chat
563	err = json.Unmarshal(resp.Result, &chat)
564
565	return chat, err
566}
567
568// GetChatAdministrators gets a list of administrators in the chat.
569//
570// If none have been appointed, only the creator will be returned.
571// Bots are not shown, even if they are an administrator.
572func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]ChatMember, error) {
573	resp, err := bot.Request(config)
574	if err != nil {
575		return []ChatMember{}, err
576	}
577
578	var members []ChatMember
579	err = json.Unmarshal(resp.Result, &members)
580
581	return members, err
582}
583
584// GetChatMembersCount gets the number of users in a chat.
585func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) {
586	resp, err := bot.Request(config)
587	if err != nil {
588		return -1, err
589	}
590
591	var count int
592	err = json.Unmarshal(resp.Result, &count)
593
594	return count, err
595}
596
597// GetChatMember gets a specific chat member.
598func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) {
599	resp, err := bot.Request(config)
600	if err != nil {
601		return ChatMember{}, err
602	}
603
604	var member ChatMember
605	err = json.Unmarshal(resp.Result, &member)
606
607	return member, err
608}
609
610// GetGameHighScores allows you to get the high scores for a game.
611func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
612	resp, err := bot.Request(config)
613	if err != nil {
614		return []GameHighScore{}, err
615	}
616
617	var highScores []GameHighScore
618	err = json.Unmarshal(resp.Result, &highScores)
619
620	return highScores, err
621}
622
623// GetInviteLink get InviteLink for a chat
624func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) {
625	resp, err := bot.Request(config)
626	if err != nil {
627		return "", err
628	}
629
630	var inviteLink string
631	err = json.Unmarshal(resp.Result, &inviteLink)
632
633	return inviteLink, err
634}
635
636// GetStickerSet returns a StickerSet.
637func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) {
638	resp, err := bot.Request(config)
639	if err != nil {
640		return StickerSet{}, err
641	}
642
643	var stickers StickerSet
644	err = json.Unmarshal(resp.Result, &stickers)
645
646	return stickers, err
647}
648
649// StopPoll stops a poll and returns the result.
650func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) {
651	resp, err := bot.Request(config)
652	if err != nil {
653		return Poll{}, err
654	}
655
656	var poll Poll
657	err = json.Unmarshal(resp.Result, &poll)
658
659	return poll, err
660}
661
662// GetMyCommands gets the currently registered commands.
663func (bot *BotAPI) GetMyCommands() ([]BotCommand, error) {
664	return bot.GetMyCommandsWithConfig(GetMyCommandsConfig{})
665}
666
667// GetMyCommandsWithConfig gets the currently registered commands with a config.
668func (bot *BotAPI) GetMyCommandsWithConfig(config GetMyCommandsConfig) ([]BotCommand, error) {
669	resp, err := bot.Request(config)
670	if err != nil {
671		return nil, err
672	}
673
674	var commands []BotCommand
675	err = json.Unmarshal(resp.Result, &commands)
676
677	return commands, err
678}
679
680// CopyMessage copy messages of any kind. The method is analogous to the method
681// forwardMessage, but the copied message doesn't have a link to the original
682// message. Returns the MessageID of the sent message on success.
683func (bot *BotAPI) CopyMessage(config CopyMessageConfig) (MessageID, error) {
684	params, err := config.params()
685	if err != nil {
686		return MessageID{}, err
687	}
688
689	resp, err := bot.MakeRequest(config.method(), params)
690	if err != nil {
691		return MessageID{}, err
692	}
693
694	var messageID MessageID
695	err = json.Unmarshal(resp.Result, &messageID)
696
697	return messageID, err
698}
699
700// EscapeText takes an input text and escape Telegram markup symbols.
701// In this way we can send a text without being afraid of having to escape the characters manually.
702// Note that you don't have to include the formatting style in the input text, or it will be escaped too.
703// If there is an error, an empty string will be returned.
704//
705// parseMode is the text formatting mode (ModeMarkdown, ModeMarkdownV2 or ModeHTML)
706// text is the input string that will be escaped
707func EscapeText(parseMode string, text string) string {
708	var replacer *strings.Replacer
709
710	if parseMode == ModeHTML {
711		replacer = strings.NewReplacer("<", "&lt;", ">", "&gt;", "&", "&amp;")
712	} else if parseMode == ModeMarkdown {
713		replacer = strings.NewReplacer("_", "\\_", "*", "\\*", "`", "\\`", "[", "\\[")
714	} else if parseMode == ModeMarkdownV2 {
715		replacer = strings.NewReplacer(
716			"_", "\\_", "*", "\\*", "[", "\\[", "]", "\\]", "(",
717			"\\(", ")", "\\)", "~", "\\~", "`", "\\`", ">", "\\>",
718			"#", "\\#", "+", "\\+", "-", "\\-", "=", "\\=", "|",
719			"\\|", "{", "\\{", "}", "\\}", ".", "\\.", "!", "\\!",
720		)
721	} else {
722		return ""
723	}
724
725	return replacer.Replace(text)
726}