all repos — telegram-bot-api @ 0d6825ebcc23bb24c6c80948ef095d9080184b8c

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