all repos — telegram-bot-api @ 2fa77043adf21ec8e5cf0cc2d12d5e351471ed5c

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	"mime/multipart"
 11	"net/http"
 12	"net/url"
 13	"strings"
 14	"time"
 15)
 16
 17// HTTPClient is the type needed for the bot to perform HTTP requests.
 18type HTTPClient interface {
 19	Do(req *http.Request) (*http.Response, error)
 20}
 21
 22// BotAPI allows you to interact with the Telegram Bot API.
 23type BotAPI struct {
 24	Token  string `json:"token"`
 25	Debug  bool   `json:"debug"`
 26	Buffer int    `json:"buffer"`
 27
 28	Self            User       `json:"-"`
 29	Client          HTTPClient `json:"-"`
 30	shutdownChannel chan interface{}
 31
 32	apiEndpoint string
 33}
 34
 35// NewBotAPI creates a new BotAPI instance.
 36//
 37// It requires a token, provided by @BotFather on Telegram.
 38func NewBotAPI(token string) (*BotAPI, error) {
 39	return NewBotAPIWithClient(token, APIEndpoint, &http.Client{})
 40}
 41
 42// NewBotAPIWithAPIEndpoint creates a new BotAPI instance
 43// and allows you to pass API endpoint.
 44//
 45// It requires a token, provided by @BotFather on Telegram and API endpoint.
 46func NewBotAPIWithAPIEndpoint(token, apiEndpoint string) (*BotAPI, error) {
 47	return NewBotAPIWithClient(token, apiEndpoint, &http.Client{})
 48}
 49
 50// NewBotAPIWithClient creates a new BotAPI instance
 51// and allows you to pass a http.Client.
 52//
 53// It requires a token, provided by @BotFather on Telegram and API endpoint.
 54func NewBotAPIWithClient(token, apiEndpoint string, client HTTPClient) (*BotAPI, error) {
 55	bot := &BotAPI{
 56		Token:           token,
 57		Client:          client,
 58		Buffer:          100,
 59		shutdownChannel: make(chan interface{}),
 60
 61		apiEndpoint: apiEndpoint,
 62	}
 63
 64	self, err := bot.GetMe()
 65	if err != nil {
 66		return nil, err
 67	}
 68
 69	bot.Self = self
 70
 71	return bot, nil
 72}
 73
 74// SetAPIEndpoint changes the Telegram Bot API endpoint used by the instance.
 75func (bot *BotAPI) SetAPIEndpoint(apiEndpoint string) {
 76	bot.apiEndpoint = apiEndpoint
 77}
 78
 79func buildParams(in Params) url.Values {
 80	if in == nil {
 81		return url.Values{}
 82	}
 83
 84	out := url.Values{}
 85
 86	for key, value := range in {
 87		out.Set(key, value)
 88	}
 89
 90	return out
 91}
 92
 93// MakeRequest makes a request to a specific endpoint with our token.
 94func (bot *BotAPI) MakeRequest(endpoint string, params Params) (*APIResponse, error) {
 95	if bot.Debug {
 96		log.Printf("Endpoint: %s, params: %v\n", endpoint, params)
 97	}
 98
 99	method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint)
100
101	values := buildParams(params)
102
103	req, err := http.NewRequest("POST", method, strings.NewReader(values.Encode()))
104	if err != nil {
105		return &APIResponse{}, err
106	}
107	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
108
109	resp, err := bot.Client.Do(req)
110	if err != nil {
111		return nil, err
112	}
113	defer resp.Body.Close()
114
115	var apiResp APIResponse
116	bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
117	if err != nil {
118		return &apiResp, err
119	}
120
121	if bot.Debug {
122		log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
123	}
124
125	if !apiResp.Ok {
126		var parameters ResponseParameters
127
128		if apiResp.Parameters != nil {
129			parameters = *apiResp.Parameters
130		}
131
132		return &apiResp, &Error{
133			Code:               apiResp.ErrorCode,
134			Message:            apiResp.Description,
135			ResponseParameters: parameters,
136		}
137	}
138
139	return &apiResp, nil
140}
141
142// decodeAPIResponse decode response and return slice of bytes if debug enabled.
143// If debug disabled, just decode http.Response.Body stream to APIResponse struct
144// for efficient memory usage
145func (bot *BotAPI) decodeAPIResponse(responseBody io.Reader, resp *APIResponse) ([]byte, error) {
146	if !bot.Debug {
147		dec := json.NewDecoder(responseBody)
148		err := dec.Decode(resp)
149		return nil, err
150	}
151
152	// if debug, read response body
153	data, err := io.ReadAll(responseBody)
154	if err != nil {
155		return nil, err
156	}
157
158	err = json.Unmarshal(data, resp)
159	if err != nil {
160		return nil, err
161	}
162
163	return data, nil
164}
165
166// UploadFiles makes a request to the API with files.
167func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFile) (*APIResponse, error) {
168	r, w := io.Pipe()
169	m := multipart.NewWriter(w)
170
171	// This code modified from the very helpful @HirbodBehnam
172	// https://github.com/go-telegram-bot-api/telegram-bot-api/issues/354#issuecomment-663856473
173	go func() {
174		defer w.Close()
175		defer m.Close()
176
177		for field, value := range params {
178			if err := m.WriteField(field, value); err != nil {
179				w.CloseWithError(err)
180				return
181			}
182		}
183
184		for _, file := range files {
185			if file.Data.NeedsUpload() {
186				name, reader, err := file.Data.UploadData()
187				if err != nil {
188					w.CloseWithError(err)
189					return
190				}
191
192				part, err := m.CreateFormFile(file.Name, name)
193				if err != nil {
194					w.CloseWithError(err)
195					return
196				}
197
198				if _, err := io.Copy(part, reader); err != nil {
199					w.CloseWithError(err)
200					return
201				}
202
203				if closer, ok := reader.(io.ReadCloser); ok {
204					if err = closer.Close(); err != nil {
205						w.CloseWithError(err)
206						return
207					}
208				}
209			} else {
210				value := file.Data.SendData()
211
212				if err := m.WriteField(file.Name, value); err != nil {
213					w.CloseWithError(err)
214					return
215				}
216			}
217		}
218	}()
219
220	if bot.Debug {
221		log.Printf("Endpoint: %s, params: %v, with %d files\n", endpoint, params, len(files))
222	}
223
224	method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint)
225
226	req, err := http.NewRequest("POST", method, r)
227	if err != nil {
228		return nil, err
229	}
230
231	req.Header.Set("Content-Type", m.FormDataContentType())
232
233	resp, err := bot.Client.Do(req)
234	if err != nil {
235		return nil, err
236	}
237	defer resp.Body.Close()
238
239	var apiResp APIResponse
240	bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
241	if err != nil {
242		return &apiResp, err
243	}
244
245	if bot.Debug {
246		log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
247	}
248
249	if !apiResp.Ok {
250		var parameters ResponseParameters
251
252		if apiResp.Parameters != nil {
253			parameters = *apiResp.Parameters
254		}
255
256		return &apiResp, &Error{
257			Message:            apiResp.Description,
258			ResponseParameters: parameters,
259		}
260	}
261
262	return &apiResp, nil
263}
264
265// GetFileDirectURL returns direct URL to file
266//
267// It requires the FileID.
268func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) {
269	file, err := bot.GetFile(FileConfig{fileID})
270
271	if err != nil {
272		return "", err
273	}
274
275	return file.Link(bot.Token), nil
276}
277
278// GetMe fetches the currently authenticated bot.
279//
280// This method is called upon creation to validate the token,
281// and so you may get this data from BotAPI.Self without the need for
282// another request.
283func (bot *BotAPI) GetMe() (User, error) {
284	resp, err := bot.MakeRequest("getMe", nil)
285	if err != nil {
286		return User{}, err
287	}
288
289	var user User
290	err = json.Unmarshal(resp.Result, &user)
291
292	return user, err
293}
294
295// IsMessageToMe returns true if message directed to this bot.
296//
297// It requires the Message.
298func (bot *BotAPI) IsMessageToMe(message Message) bool {
299	return strings.Contains(message.Text, "@"+bot.Self.UserName)
300}
301
302func hasFilesNeedingUpload(files []RequestFile) bool {
303	for _, file := range files {
304		if file.Data.NeedsUpload() {
305			return true
306		}
307	}
308
309	return false
310}
311
312// Request sends a Chattable to Telegram, and returns the APIResponse.
313func (bot *BotAPI) Request(c Chattable) (*APIResponse, error) {
314	params, err := c.params()
315	if err != nil {
316		return nil, err
317	}
318
319	if t, ok := c.(Fileable); ok {
320		files := t.files()
321
322		// If we have files that need to be uploaded, we should delegate the
323		// request to UploadFile.
324		if hasFilesNeedingUpload(files) {
325			return bot.UploadFiles(t.method(), params, files)
326		}
327
328		// However, if there are no files to be uploaded, there's likely things
329		// that need to be turned into params instead.
330		for _, file := range files {
331			params[file.Name] = file.Data.SendData()
332		}
333	}
334
335	return bot.MakeRequest(c.method(), params)
336}
337
338// Send will send a Chattable item to Telegram and provides the
339// returned Message.
340func (bot *BotAPI) Send(c Chattable) (Message, error) {
341	resp, err := bot.Request(c)
342	if err != nil {
343		return Message{}, err
344	}
345
346	var message Message
347	err = json.Unmarshal(resp.Result, &message)
348
349	return message, err
350}
351
352// SendMediaGroup sends a media group and returns the resulting messages.
353func (bot *BotAPI) SendMediaGroup(config MediaGroupConfig) ([]Message, error) {
354	resp, err := bot.Request(config)
355	if err != nil {
356		return nil, err
357	}
358
359	var messages []Message
360	err = json.Unmarshal(resp.Result, &messages)
361
362	return messages, err
363}
364
365// GetUserProfilePhotos gets a user's profile photos.
366//
367// It requires UserID.
368// Offset and Limit are optional.
369func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
370	resp, err := bot.Request(config)
371	if err != nil {
372		return UserProfilePhotos{}, err
373	}
374
375	var profilePhotos UserProfilePhotos
376	err = json.Unmarshal(resp.Result, &profilePhotos)
377
378	return profilePhotos, err
379}
380
381// GetFile returns a File which can download a file from Telegram.
382//
383// Requires FileID.
384func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
385	resp, err := bot.Request(config)
386	if err != nil {
387		return File{}, err
388	}
389
390	var file File
391	err = json.Unmarshal(resp.Result, &file)
392
393	return file, err
394}
395
396// GetUpdates fetches updates.
397// If a WebHook is set, this will not return any data!
398//
399// Offset, Limit, Timeout, and AllowedUpdates are optional.
400// To avoid stale items, set Offset to one higher than the previous item.
401// Set Timeout to a large number to reduce requests, so you can get updates
402// instantly instead of having to wait between requests.
403func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
404	resp, err := bot.Request(config)
405	if err != nil {
406		return []Update{}, err
407	}
408
409	var updates []Update
410	err = json.Unmarshal(resp.Result, &updates)
411
412	return updates, err
413}
414
415// GetWebhookInfo allows you to fetch information about a webhook and if
416// one currently is set, along with pending update count and error messages.
417func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) {
418	resp, err := bot.MakeRequest("getWebhookInfo", nil)
419	if err != nil {
420		return WebhookInfo{}, err
421	}
422
423	var info WebhookInfo
424	err = json.Unmarshal(resp.Result, &info)
425
426	return info, err
427}
428
429// GetUpdatesChan starts and returns a channel for getting updates.
430func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) UpdatesChannel {
431	ch := make(chan Update, bot.Buffer)
432
433	go func() {
434		for {
435			select {
436			case <-bot.shutdownChannel:
437				close(ch)
438				return
439			default:
440			}
441
442			updates, err := bot.GetUpdates(config)
443			if err != nil {
444				log.Println(err)
445				log.Println("Failed to get updates, retrying in 3 seconds...")
446				time.Sleep(time.Second * 3)
447
448				continue
449			}
450
451			for _, update := range updates {
452				if update.UpdateID >= config.Offset {
453					config.Offset = update.UpdateID + 1
454					ch <- update
455				}
456			}
457		}
458	}()
459
460	return ch
461}
462
463// StopReceivingUpdates stops the go routine which receives updates
464func (bot *BotAPI) StopReceivingUpdates() {
465	if bot.Debug {
466		log.Println("Stopping the update receiver routine...")
467	}
468	close(bot.shutdownChannel)
469}
470
471// ListenForWebhook registers a http handler for a webhook.
472func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
473	ch := make(chan Update, bot.Buffer)
474
475	http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
476		update, err := bot.HandleUpdate(r)
477		if err != nil {
478			errMsg, _ := json.Marshal(map[string]string{"error": err.Error()})
479			w.WriteHeader(http.StatusBadRequest)
480			w.Header().Set("Content-Type", "application/json")
481			_, _ = w.Write(errMsg)
482			return
483		}
484
485		ch <- *update
486	})
487
488	return ch
489}
490
491// ListenForWebhookRespReqFormat registers a http handler for a single incoming webhook.
492func (bot *BotAPI) ListenForWebhookRespReqFormat(w http.ResponseWriter, r *http.Request) UpdatesChannel {
493	ch := make(chan Update, bot.Buffer)
494
495	func(w http.ResponseWriter, r *http.Request) {
496		defer close(ch)
497
498		update, err := bot.HandleUpdate(r)
499		if err != nil {
500			errMsg, _ := json.Marshal(map[string]string{"error": err.Error()})
501			w.WriteHeader(http.StatusBadRequest)
502			w.Header().Set("Content-Type", "application/json")
503			_, _ = w.Write(errMsg)
504			return
505		}
506
507		ch <- *update
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	resp, err := bot.Request(config)
685	if err != nil {
686		return MessageID{}, err
687	}
688
689	var messageID MessageID
690	err = json.Unmarshal(resp.Result, &messageID)
691
692	return messageID, err
693}
694
695// AnswerWebAppQuery sets the result of an interaction with a Web App and send a
696// corresponding message on behalf of the user to the chat from which the query originated.
697func (bot *BotAPI) AnswerWebAppQuery(config AnswerWebAppQueryConfig) (SentWebAppMessage, error) {
698	var sentWebAppMessage SentWebAppMessage
699
700	resp, err := bot.Request(config)
701	if err != nil {
702		return sentWebAppMessage, err
703	}
704
705	err = json.Unmarshal(resp.Result, &sentWebAppMessage)
706	return sentWebAppMessage, err
707}
708
709// GetMyDefaultAdministratorRights gets the current default administrator rights of the bot.
710func (bot *BotAPI) GetMyDefaultAdministratorRights(config GetMyDefaultAdministratorRightsConfig) (ChatAdministratorRights, error) {
711	var rights ChatAdministratorRights
712
713	resp, err := bot.Request(config)
714	if err != nil {
715		return rights, err
716	}
717
718	err = json.Unmarshal(resp.Result, &rights)
719	return rights, err
720}
721
722// EscapeText takes an input text and escape Telegram markup symbols.
723// In this way we can send a text without being afraid of having to escape the characters manually.
724// Note that you don't have to include the formatting style in the input text, or it will be escaped too.
725// If there is an error, an empty string will be returned.
726//
727// parseMode is the text formatting mode (ModeMarkdown, ModeMarkdownV2 or ModeHTML)
728// text is the input string that will be escaped
729func EscapeText(parseMode string, text string) string {
730	var replacer *strings.Replacer
731
732	if parseMode == ModeHTML {
733		replacer = strings.NewReplacer("<", "&lt;", ">", "&gt;", "&", "&amp;")
734	} else if parseMode == ModeMarkdown {
735		replacer = strings.NewReplacer("_", "\\_", "*", "\\*", "`", "\\`", "[", "\\[")
736	} else if parseMode == ModeMarkdownV2 {
737		replacer = strings.NewReplacer(
738			"_", "\\_", "*", "\\*", "[", "\\[", "]", "\\]", "(",
739			"\\(", ")", "\\)", "~", "\\~", "`", "\\`", ">", "\\>",
740			"#", "\\#", "+", "\\+", "-", "\\-", "=", "\\=", "|",
741			"\\|", "{", "\\{", "}", "\\}", ".", "\\.", "!", "\\!",
742		)
743	} else {
744		return ""
745	}
746
747	return replacer.Replace(text)
748}