go-tg-screenshot-bot
726 строк · 17.6 Кб
1// Package tgbotapi has functions and types used for interacting with
2// the Telegram Bot API.
3package tgbotapi4
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 {20Do(req *http.Request) (*http.Response, error)21}
22
23// BotAPI allows you to interact with the Telegram Bot API.
24type BotAPI struct {25Token string `json:"token"`26Debug bool `json:"debug"`27Buffer int `json:"buffer"`28
29Self User `json:"-"`30Client HTTPClient `json:"-"`31shutdownChannel chan interface{}32
33apiEndpoint string34}
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) {40return 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) {48return 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) {56bot := &BotAPI{57Token: token,58Client: client,59Buffer: 100,60shutdownChannel: make(chan interface{}),61
62apiEndpoint: apiEndpoint,63}64
65self, err := bot.GetMe()66if err != nil {67return nil, err68}69
70bot.Self = self71
72return bot, nil73}
74
75// SetAPIEndpoint changes the Telegram Bot API endpoint used by the instance.
76func (bot *BotAPI) SetAPIEndpoint(apiEndpoint string) {77bot.apiEndpoint = apiEndpoint78}
79
80func buildParams(in Params) url.Values {81if in == nil {82return url.Values{}83}84
85out := url.Values{}86
87for key, value := range in {88out.Set(key, value)89}90
91return out92}
93
94// MakeRequest makes a request to a specific endpoint with our token.
95func (bot *BotAPI) MakeRequest(endpoint string, params Params) (*APIResponse, error) {96if bot.Debug {97log.Printf("Endpoint: %s, params: %v\n", endpoint, params)98}99
100method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint)101
102values := buildParams(params)103
104req, err := http.NewRequest("POST", method, strings.NewReader(values.Encode()))105if err != nil {106return &APIResponse{}, err107}108req.Header.Set("Content-Type", "application/x-www-form-urlencoded")109
110resp, err := bot.Client.Do(req)111if err != nil {112return nil, err113}114defer resp.Body.Close()115
116var apiResp APIResponse117bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)118if err != nil {119return &apiResp, err120}121
122if bot.Debug {123log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))124}125
126if !apiResp.Ok {127var parameters ResponseParameters128
129if apiResp.Parameters != nil {130parameters = *apiResp.Parameters131}132
133return &apiResp, &Error{134Code: apiResp.ErrorCode,135Message: apiResp.Description,136ResponseParameters: parameters,137}138}139
140return &apiResp, nil141}
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) {147if !bot.Debug {148dec := json.NewDecoder(responseBody)149err := dec.Decode(resp)150return nil, err151}152
153// if debug, read response body154data, err := ioutil.ReadAll(responseBody)155if err != nil {156return nil, err157}158
159err = json.Unmarshal(data, resp)160if err != nil {161return nil, err162}163
164return data, nil165}
166
167// UploadFiles makes a request to the API with files.
168func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFile) (*APIResponse, error) {169r, w := io.Pipe()170m := multipart.NewWriter(w)171
172// This code modified from the very helpful @HirbodBehnam173// https://github.com/go-telegram-bot-api/telegram-bot-api/issues/354#issuecomment-663856473174go func() {175defer w.Close()176defer m.Close()177
178for field, value := range params {179if err := m.WriteField(field, value); err != nil {180w.CloseWithError(err)181return182}183}184
185for _, file := range files {186if file.Data.NeedsUpload() {187name, reader, err := file.Data.UploadData()188if err != nil {189w.CloseWithError(err)190return191}192
193part, err := m.CreateFormFile(file.Name, name)194if err != nil {195w.CloseWithError(err)196return197}198
199if _, err := io.Copy(part, reader); err != nil {200w.CloseWithError(err)201return202}203
204if closer, ok := reader.(io.ReadCloser); ok {205if err = closer.Close(); err != nil {206w.CloseWithError(err)207return208}209}210} else {211value := file.Data.SendData()212
213if err := m.WriteField(file.Name, value); err != nil {214w.CloseWithError(err)215return216}217}218}219}()220
221if bot.Debug {222log.Printf("Endpoint: %s, params: %v, with %d files\n", endpoint, params, len(files))223}224
225method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint)226
227req, err := http.NewRequest("POST", method, r)228if err != nil {229return nil, err230}231
232req.Header.Set("Content-Type", m.FormDataContentType())233
234resp, err := bot.Client.Do(req)235if err != nil {236return nil, err237}238defer resp.Body.Close()239
240var apiResp APIResponse241bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)242if err != nil {243return &apiResp, err244}245
246if bot.Debug {247log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))248}249
250if !apiResp.Ok {251var parameters ResponseParameters252
253if apiResp.Parameters != nil {254parameters = *apiResp.Parameters255}256
257return &apiResp, &Error{258Message: apiResp.Description,259ResponseParameters: parameters,260}261}262
263return &apiResp, nil264}
265
266// GetFileDirectURL returns direct URL to file
267//
268// It requires the FileID.
269func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) {270file, err := bot.GetFile(FileConfig{fileID})271
272if err != nil {273return "", err274}275
276return file.Link(bot.Token), nil277}
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) {285resp, err := bot.MakeRequest("getMe", nil)286if err != nil {287return User{}, err288}289
290var user User291err = json.Unmarshal(resp.Result, &user)292
293return user, err294}
295
296// IsMessageToMe returns true if message directed to this bot.
297//
298// It requires the Message.
299func (bot *BotAPI) IsMessageToMe(message Message) bool {300return strings.Contains(message.Text, "@"+bot.Self.UserName)301}
302
303func hasFilesNeedingUpload(files []RequestFile) bool {304for _, file := range files {305if file.Data.NeedsUpload() {306return true307}308}309
310return false311}
312
313// Request sends a Chattable to Telegram, and returns the APIResponse.
314func (bot *BotAPI) Request(c Chattable) (*APIResponse, error) {315params, err := c.params()316if err != nil {317return nil, err318}319
320if t, ok := c.(Fileable); ok {321files := t.files()322
323// If we have files that need to be uploaded, we should delegate the324// request to UploadFile.325if hasFilesNeedingUpload(files) {326return bot.UploadFiles(t.method(), params, files)327}328
329// However, if there are no files to be uploaded, there's likely things330// that need to be turned into params instead.331for _, file := range files {332params[file.Name] = file.Data.SendData()333}334}335
336return 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) {342resp, err := bot.Request(c)343if err != nil {344return Message{}, err345}346
347var message Message348err = json.Unmarshal(resp.Result, &message)349
350return message, err351}
352
353// SendMediaGroup sends a media group and returns the resulting messages.
354func (bot *BotAPI) SendMediaGroup(config MediaGroupConfig) ([]Message, error) {355resp, err := bot.Request(config)356if err != nil {357return nil, err358}359
360var messages []Message361err = json.Unmarshal(resp.Result, &messages)362
363return messages, err364}
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) {371resp, err := bot.Request(config)372if err != nil {373return UserProfilePhotos{}, err374}375
376var profilePhotos UserProfilePhotos377err = json.Unmarshal(resp.Result, &profilePhotos)378
379return profilePhotos, err380}
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) {386resp, err := bot.Request(config)387if err != nil {388return File{}, err389}390
391var file File392err = json.Unmarshal(resp.Result, &file)393
394return file, err395}
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) {405resp, err := bot.Request(config)406if err != nil {407return []Update{}, err408}409
410var updates []Update411err = json.Unmarshal(resp.Result, &updates)412
413return updates, err414}
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) {419resp, err := bot.MakeRequest("getWebhookInfo", nil)420if err != nil {421return WebhookInfo{}, err422}423
424var info WebhookInfo425err = json.Unmarshal(resp.Result, &info)426
427return info, err428}
429
430// GetUpdatesChan starts and returns a channel for getting updates.
431func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) UpdatesChannel {432ch := make(chan Update, bot.Buffer)433
434go func() {435for {436select {437case <-bot.shutdownChannel:438close(ch)439return440default:441}442
443updates, err := bot.GetUpdates(config)444if err != nil {445log.Println(err)446log.Println("Failed to get updates, retrying in 3 seconds...")447time.Sleep(time.Second * 3)448
449continue450}451
452for _, update := range updates {453if update.UpdateID >= config.Offset {454config.Offset = update.UpdateID + 1455ch <- update456}457}458}459}()460
461return ch462}
463
464// StopReceivingUpdates stops the go routine which receives updates
465func (bot *BotAPI) StopReceivingUpdates() {466if bot.Debug {467log.Println("Stopping the update receiver routine...")468}469close(bot.shutdownChannel)470}
471
472// ListenForWebhook registers a http handler for a webhook.
473func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {474ch := make(chan Update, bot.Buffer)475
476http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {477update, err := bot.HandleUpdate(r)478if err != nil {479errMsg, _ := json.Marshal(map[string]string{"error": err.Error()})480w.WriteHeader(http.StatusBadRequest)481w.Header().Set("Content-Type", "application/json")482_, _ = w.Write(errMsg)483return484}485
486ch <- *update487})488
489return ch490}
491
492// ListenForWebhookRespReqFormat registers a http handler for a single incoming webhook.
493func (bot *BotAPI) ListenForWebhookRespReqFormat(w http.ResponseWriter, r *http.Request) UpdatesChannel {494ch := make(chan Update, bot.Buffer)495
496func(w http.ResponseWriter, r *http.Request) {497update, err := bot.HandleUpdate(r)498if err != nil {499errMsg, _ := json.Marshal(map[string]string{"error": err.Error()})500w.WriteHeader(http.StatusBadRequest)501w.Header().Set("Content-Type", "application/json")502_, _ = w.Write(errMsg)503return504}505
506ch <- *update507close(ch)508}(w, r)509
510return ch511}
512
513// HandleUpdate parses and returns update received via webhook
514func (bot *BotAPI) HandleUpdate(r *http.Request) (*Update, error) {515if r.Method != http.MethodPost {516err := errors.New("wrong HTTP method required POST")517return nil, err518}519
520var update Update521err := json.NewDecoder(r.Body).Decode(&update)522if err != nil {523return nil, err524}525
526return &update, nil527}
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 {536params, err := c.params()537if err != nil {538return err539}540
541if t, ok := c.(Fileable); ok {542if hasFilesNeedingUpload(t.files()) {543return errors.New("unable to use http response to upload files")544}545}546
547values := buildParams(params)548values.Set("method", c.method())549
550w.Header().Set("Content-Type", "application/x-www-form-urlencoded")551_, err = w.Write([]byte(values.Encode()))552return err553}
554
555// GetChat gets information about a chat.
556func (bot *BotAPI) GetChat(config ChatInfoConfig) (Chat, error) {557resp, err := bot.Request(config)558if err != nil {559return Chat{}, err560}561
562var chat Chat563err = json.Unmarshal(resp.Result, &chat)564
565return chat, err566}
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) {573resp, err := bot.Request(config)574if err != nil {575return []ChatMember{}, err576}577
578var members []ChatMember579err = json.Unmarshal(resp.Result, &members)580
581return members, err582}
583
584// GetChatMembersCount gets the number of users in a chat.
585func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) {586resp, err := bot.Request(config)587if err != nil {588return -1, err589}590
591var count int592err = json.Unmarshal(resp.Result, &count)593
594return count, err595}
596
597// GetChatMember gets a specific chat member.
598func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) {599resp, err := bot.Request(config)600if err != nil {601return ChatMember{}, err602}603
604var member ChatMember605err = json.Unmarshal(resp.Result, &member)606
607return member, err608}
609
610// GetGameHighScores allows you to get the high scores for a game.
611func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {612resp, err := bot.Request(config)613if err != nil {614return []GameHighScore{}, err615}616
617var highScores []GameHighScore618err = json.Unmarshal(resp.Result, &highScores)619
620return highScores, err621}
622
623// GetInviteLink get InviteLink for a chat
624func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) {625resp, err := bot.Request(config)626if err != nil {627return "", err628}629
630var inviteLink string631err = json.Unmarshal(resp.Result, &inviteLink)632
633return inviteLink, err634}
635
636// GetStickerSet returns a StickerSet.
637func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) {638resp, err := bot.Request(config)639if err != nil {640return StickerSet{}, err641}642
643var stickers StickerSet644err = json.Unmarshal(resp.Result, &stickers)645
646return stickers, err647}
648
649// StopPoll stops a poll and returns the result.
650func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) {651resp, err := bot.Request(config)652if err != nil {653return Poll{}, err654}655
656var poll Poll657err = json.Unmarshal(resp.Result, &poll)658
659return poll, err660}
661
662// GetMyCommands gets the currently registered commands.
663func (bot *BotAPI) GetMyCommands() ([]BotCommand, error) {664return bot.GetMyCommandsWithConfig(GetMyCommandsConfig{})665}
666
667// GetMyCommandsWithConfig gets the currently registered commands with a config.
668func (bot *BotAPI) GetMyCommandsWithConfig(config GetMyCommandsConfig) ([]BotCommand, error) {669resp, err := bot.Request(config)670if err != nil {671return nil, err672}673
674var commands []BotCommand675err = json.Unmarshal(resp.Result, &commands)676
677return commands, err678}
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) {684params, err := config.params()685if err != nil {686return MessageID{}, err687}688
689resp, err := bot.MakeRequest(config.method(), params)690if err != nil {691return MessageID{}, err692}693
694var messageID MessageID695err = json.Unmarshal(resp.Result, &messageID)696
697return messageID, err698}
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 {708var replacer *strings.Replacer709
710if parseMode == ModeHTML {711replacer = strings.NewReplacer("<", "<", ">", ">", "&", "&")712} else if parseMode == ModeMarkdown {713replacer = strings.NewReplacer("_", "\\_", "*", "\\*", "`", "\\`", "[", "\\[")714} else if parseMode == ModeMarkdownV2 {715replacer = strings.NewReplacer(716"_", "\\_", "*", "\\*", "[", "\\[", "]", "\\]", "(",717"\\(", ")", "\\)", "~", "\\~", "`", "\\`", ">", "\\>",718"#", "\\#", "+", "\\+", "-", "\\-", "=", "\\=", "|",719"\\|", "{", "\\{", "}", "\\}", ".", "\\.", "!", "\\!",720)721} else {722return ""723}724
725return replacer.Replace(text)726}
727