123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748 |
- // Package tgbotapi has functions and types used for interacting with
- // the Telegram Bot API.
- package tgbotapi
- import (
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "mime/multipart"
- "net/http"
- "net/url"
- "strings"
- "time"
- )
- // HTTPClient is the type needed for the bot to perform HTTP requests.
- type HTTPClient interface {
- Do(req *http.Request) (*http.Response, error)
- }
- // BotAPI allows you to interact with the Telegram Bot API.
- type BotAPI struct {
- Token string `json:"token"`
- Debug bool `json:"debug"`
- Buffer int `json:"buffer"`
- Self User `json:"-"`
- Client HTTPClient `json:"-"`
- shutdownChannel chan interface{}
- apiEndpoint string
- }
- // NewBotAPI creates a new BotAPI instance.
- //
- // It requires a token, provided by @BotFather on Telegram.
- func NewBotAPI(token string) (*BotAPI, error) {
- return NewBotAPIWithClient(token, APIEndpoint, &http.Client{})
- }
- // NewBotAPIWithAPIEndpoint creates a new BotAPI instance
- // and allows you to pass API endpoint.
- //
- // It requires a token, provided by @BotFather on Telegram and API endpoint.
- func NewBotAPIWithAPIEndpoint(token, apiEndpoint string) (*BotAPI, error) {
- return NewBotAPIWithClient(token, apiEndpoint, &http.Client{})
- }
- // NewBotAPIWithClient creates a new BotAPI instance
- // and allows you to pass a http.Client.
- //
- // It requires a token, provided by @BotFather on Telegram and API endpoint.
- func NewBotAPIWithClient(token, apiEndpoint string, client HTTPClient) (*BotAPI, error) {
- bot := &BotAPI{
- Token: token,
- Client: client,
- Buffer: 100,
- shutdownChannel: make(chan interface{}),
- apiEndpoint: apiEndpoint,
- }
- self, err := bot.GetMe()
- if err != nil {
- return nil, err
- }
- bot.Self = self
- return bot, nil
- }
- // SetAPIEndpoint changes the Telegram Bot API endpoint used by the instance.
- func (bot *BotAPI) SetAPIEndpoint(apiEndpoint string) {
- bot.apiEndpoint = apiEndpoint
- }
- func buildParams(in Params) url.Values {
- if in == nil {
- return url.Values{}
- }
- out := url.Values{}
- for key, value := range in {
- out.Set(key, value)
- }
- return out
- }
- // MakeRequest makes a request to a specific endpoint with our token.
- func (bot *BotAPI) MakeRequest(endpoint string, params Params) (*APIResponse, error) {
- if bot.Debug {
- log.Printf("Endpoint: %s, params: %v\n", endpoint, params)
- }
- method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint)
- values := buildParams(params)
- req, err := http.NewRequest("POST", method, strings.NewReader(values.Encode()))
- if err != nil {
- return &APIResponse{}, err
- }
- req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
- resp, err := bot.Client.Do(req)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- var apiResp APIResponse
- bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
- if err != nil {
- return &apiResp, err
- }
- if bot.Debug {
- log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
- }
- if !apiResp.Ok {
- var parameters ResponseParameters
- if apiResp.Parameters != nil {
- parameters = *apiResp.Parameters
- }
- return &apiResp, &Error{
- Code: apiResp.ErrorCode,
- Message: apiResp.Description,
- ResponseParameters: parameters,
- }
- }
- return &apiResp, nil
- }
- // decodeAPIResponse decode response and return slice of bytes if debug enabled.
- // If debug disabled, just decode http.Response.Body stream to APIResponse struct
- // for efficient memory usage
- func (bot *BotAPI) decodeAPIResponse(responseBody io.Reader, resp *APIResponse) ([]byte, error) {
- if !bot.Debug {
- dec := json.NewDecoder(responseBody)
- err := dec.Decode(resp)
- return nil, err
- }
- // if debug, read response body
- data, err := io.ReadAll(responseBody)
- if err != nil {
- return nil, err
- }
- err = json.Unmarshal(data, resp)
- if err != nil {
- return nil, err
- }
- return data, nil
- }
- // UploadFiles makes a request to the API with files.
- func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFile) (*APIResponse, error) {
- r, w := io.Pipe()
- m := multipart.NewWriter(w)
- // This code modified from the very helpful @HirbodBehnam
- // https://github.com/go-telegram-bot-api/telegram-bot-api/issues/354#issuecomment-663856473
- go func() {
- defer w.Close()
- defer m.Close()
- for field, value := range params {
- if err := m.WriteField(field, value); err != nil {
- w.CloseWithError(err)
- return
- }
- }
- for _, file := range files {
- if file.Data.NeedsUpload() {
- name, reader, err := file.Data.UploadData()
- if err != nil {
- w.CloseWithError(err)
- return
- }
- part, err := m.CreateFormFile(file.Name, name)
- if err != nil {
- w.CloseWithError(err)
- return
- }
- if _, err := io.Copy(part, reader); err != nil {
- w.CloseWithError(err)
- return
- }
- if closer, ok := reader.(io.ReadCloser); ok {
- if err = closer.Close(); err != nil {
- w.CloseWithError(err)
- return
- }
- }
- } else {
- value := file.Data.SendData()
- if err := m.WriteField(file.Name, value); err != nil {
- w.CloseWithError(err)
- return
- }
- }
- }
- }()
- if bot.Debug {
- log.Printf("Endpoint: %s, params: %v, with %d files\n", endpoint, params, len(files))
- }
- method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint)
- req, err := http.NewRequest("POST", method, r)
- if err != nil {
- return nil, err
- }
- req.Header.Set("Content-Type", m.FormDataContentType())
- resp, err := bot.Client.Do(req)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- var apiResp APIResponse
- bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
- if err != nil {
- return &apiResp, err
- }
- if bot.Debug {
- log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
- }
- if !apiResp.Ok {
- var parameters ResponseParameters
- if apiResp.Parameters != nil {
- parameters = *apiResp.Parameters
- }
- return &apiResp, &Error{
- Message: apiResp.Description,
- ResponseParameters: parameters,
- }
- }
- return &apiResp, nil
- }
- // GetFileDirectURL returns direct URL to file
- //
- // It requires the FileID.
- func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) {
- file, err := bot.GetFile(FileConfig{fileID})
- if err != nil {
- return "", err
- }
- return file.Link(bot.Token), nil
- }
- // GetMe fetches the currently authenticated bot.
- //
- // This method is called upon creation to validate the token,
- // and so you may get this data from BotAPI.Self without the need for
- // another request.
- func (bot *BotAPI) GetMe() (User, error) {
- resp, err := bot.MakeRequest("getMe", nil)
- if err != nil {
- return User{}, err
- }
- var user User
- err = json.Unmarshal(resp.Result, &user)
- return user, err
- }
- // IsMessageToMe returns true if message directed to this bot.
- //
- // It requires the Message.
- func (bot *BotAPI) IsMessageToMe(message Message) bool {
- return strings.Contains(message.Text, "@"+bot.Self.UserName)
- }
- func hasFilesNeedingUpload(files []RequestFile) bool {
- for _, file := range files {
- if file.Data.NeedsUpload() {
- return true
- }
- }
- return false
- }
- // Request sends a Chattable to Telegram, and returns the APIResponse.
- func (bot *BotAPI) Request(c Chattable) (*APIResponse, error) {
- params, err := c.params()
- if err != nil {
- return nil, err
- }
- if t, ok := c.(Fileable); ok {
- files := t.files()
- // If we have files that need to be uploaded, we should delegate the
- // request to UploadFile.
- if hasFilesNeedingUpload(files) {
- return bot.UploadFiles(t.method(), params, files)
- }
- // However, if there are no files to be uploaded, there's likely things
- // that need to be turned into params instead.
- for _, file := range files {
- params[file.Name] = file.Data.SendData()
- }
- }
- return bot.MakeRequest(c.method(), params)
- }
- // Send will send a Chattable item to Telegram and provides the
- // returned Message.
- func (bot *BotAPI) Send(c Chattable) (Message, error) {
- resp, err := bot.Request(c)
- if err != nil {
- return Message{}, err
- }
- var message Message
- err = json.Unmarshal(resp.Result, &message)
- return message, err
- }
- // SendMediaGroup sends a media group and returns the resulting messages.
- func (bot *BotAPI) SendMediaGroup(config MediaGroupConfig) ([]Message, error) {
- resp, err := bot.Request(config)
- if err != nil {
- return nil, err
- }
- var messages []Message
- err = json.Unmarshal(resp.Result, &messages)
- return messages, err
- }
- // GetUserProfilePhotos gets a user's profile photos.
- //
- // It requires UserID.
- // Offset and Limit are optional.
- func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
- resp, err := bot.Request(config)
- if err != nil {
- return UserProfilePhotos{}, err
- }
- var profilePhotos UserProfilePhotos
- err = json.Unmarshal(resp.Result, &profilePhotos)
- return profilePhotos, err
- }
- // GetFile returns a File which can download a file from Telegram.
- //
- // Requires FileID.
- func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
- resp, err := bot.Request(config)
- if err != nil {
- return File{}, err
- }
- var file File
- err = json.Unmarshal(resp.Result, &file)
- return file, err
- }
- // GetUpdates fetches updates.
- // If a WebHook is set, this will not return any data!
- //
- // Offset, Limit, Timeout, and AllowedUpdates are optional.
- // To avoid stale items, set Offset to one higher than the previous item.
- // Set Timeout to a large number to reduce requests, so you can get updates
- // instantly instead of having to wait between requests.
- func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
- resp, err := bot.Request(config)
- if err != nil {
- return []Update{}, err
- }
- var updates []Update
- err = json.Unmarshal(resp.Result, &updates)
- return updates, err
- }
- // GetWebhookInfo allows you to fetch information about a webhook and if
- // one currently is set, along with pending update count and error messages.
- func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) {
- resp, err := bot.MakeRequest("getWebhookInfo", nil)
- if err != nil {
- return WebhookInfo{}, err
- }
- var info WebhookInfo
- err = json.Unmarshal(resp.Result, &info)
- return info, err
- }
- // GetUpdatesChan starts and returns a channel for getting updates.
- func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) UpdatesChannel {
- ch := make(chan Update, bot.Buffer)
- go func() {
- for {
- select {
- case <-bot.shutdownChannel:
- close(ch)
- return
- default:
- }
- updates, err := bot.GetUpdates(config)
- if err != nil {
- log.Println(err)
- log.Println("Failed to get updates, retrying in 3 seconds...")
- time.Sleep(time.Second * 3)
- continue
- }
- for _, update := range updates {
- if update.UpdateID >= config.Offset {
- config.Offset = update.UpdateID + 1
- ch <- update
- }
- }
- }
- }()
- return ch
- }
- // StopReceivingUpdates stops the go routine which receives updates
- func (bot *BotAPI) StopReceivingUpdates() {
- if bot.Debug {
- log.Println("Stopping the update receiver routine...")
- }
- close(bot.shutdownChannel)
- }
- // ListenForWebhook registers a http handler for a webhook.
- func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
- ch := make(chan Update, bot.Buffer)
- http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
- update, err := bot.HandleUpdate(r)
- if err != nil {
- errMsg, _ := json.Marshal(map[string]string{"error": err.Error()})
- w.WriteHeader(http.StatusBadRequest)
- w.Header().Set("Content-Type", "application/json")
- _, _ = w.Write(errMsg)
- return
- }
- ch <- *update
- })
- return ch
- }
- // ListenForWebhookRespReqFormat registers a http handler for a single incoming webhook.
- func (bot *BotAPI) ListenForWebhookRespReqFormat(w http.ResponseWriter, r *http.Request) UpdatesChannel {
- ch := make(chan Update, bot.Buffer)
- func(w http.ResponseWriter, r *http.Request) {
- defer close(ch)
- update, err := bot.HandleUpdate(r)
- if err != nil {
- errMsg, _ := json.Marshal(map[string]string{"error": err.Error()})
- w.WriteHeader(http.StatusBadRequest)
- w.Header().Set("Content-Type", "application/json")
- _, _ = w.Write(errMsg)
- return
- }
- ch <- *update
- }(w, r)
- return ch
- }
- // HandleUpdate parses and returns update received via webhook
- func (bot *BotAPI) HandleUpdate(r *http.Request) (*Update, error) {
- if r.Method != http.MethodPost {
- err := errors.New("wrong HTTP method required POST")
- return nil, err
- }
- var update Update
- err := json.NewDecoder(r.Body).Decode(&update)
- if err != nil {
- return nil, err
- }
- return &update, nil
- }
- // WriteToHTTPResponse writes the request to the HTTP ResponseWriter.
- //
- // It doesn't support uploading files.
- //
- // See https://core.telegram.org/bots/api#making-requests-when-getting-updates
- // for details.
- func WriteToHTTPResponse(w http.ResponseWriter, c Chattable) error {
- params, err := c.params()
- if err != nil {
- return err
- }
- if t, ok := c.(Fileable); ok {
- if hasFilesNeedingUpload(t.files()) {
- return errors.New("unable to use http response to upload files")
- }
- }
- values := buildParams(params)
- values.Set("method", c.method())
- w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
- _, err = w.Write([]byte(values.Encode()))
- return err
- }
- // GetChat gets information about a chat.
- func (bot *BotAPI) GetChat(config ChatInfoConfig) (Chat, error) {
- resp, err := bot.Request(config)
- if err != nil {
- return Chat{}, err
- }
- var chat Chat
- err = json.Unmarshal(resp.Result, &chat)
- return chat, err
- }
- // GetChatAdministrators gets a list of administrators in the chat.
- //
- // If none have been appointed, only the creator will be returned.
- // Bots are not shown, even if they are an administrator.
- func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]ChatMember, error) {
- resp, err := bot.Request(config)
- if err != nil {
- return []ChatMember{}, err
- }
- var members []ChatMember
- err = json.Unmarshal(resp.Result, &members)
- return members, err
- }
- // GetChatMembersCount gets the number of users in a chat.
- func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) {
- resp, err := bot.Request(config)
- if err != nil {
- return -1, err
- }
- var count int
- err = json.Unmarshal(resp.Result, &count)
- return count, err
- }
- // GetChatMember gets a specific chat member.
- func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) {
- resp, err := bot.Request(config)
- if err != nil {
- return ChatMember{}, err
- }
- var member ChatMember
- err = json.Unmarshal(resp.Result, &member)
- return member, err
- }
- // GetGameHighScores allows you to get the high scores for a game.
- func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
- resp, err := bot.Request(config)
- if err != nil {
- return []GameHighScore{}, err
- }
- var highScores []GameHighScore
- err = json.Unmarshal(resp.Result, &highScores)
- return highScores, err
- }
- // GetInviteLink get InviteLink for a chat
- func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) {
- resp, err := bot.Request(config)
- if err != nil {
- return "", err
- }
- var inviteLink string
- err = json.Unmarshal(resp.Result, &inviteLink)
- return inviteLink, err
- }
- // GetStickerSet returns a StickerSet.
- func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) {
- resp, err := bot.Request(config)
- if err != nil {
- return StickerSet{}, err
- }
- var stickers StickerSet
- err = json.Unmarshal(resp.Result, &stickers)
- return stickers, err
- }
- // StopPoll stops a poll and returns the result.
- func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) {
- resp, err := bot.Request(config)
- if err != nil {
- return Poll{}, err
- }
- var poll Poll
- err = json.Unmarshal(resp.Result, &poll)
- return poll, err
- }
- // GetMyCommands gets the currently registered commands.
- func (bot *BotAPI) GetMyCommands() ([]BotCommand, error) {
- return bot.GetMyCommandsWithConfig(GetMyCommandsConfig{})
- }
- // GetMyCommandsWithConfig gets the currently registered commands with a config.
- func (bot *BotAPI) GetMyCommandsWithConfig(config GetMyCommandsConfig) ([]BotCommand, error) {
- resp, err := bot.Request(config)
- if err != nil {
- return nil, err
- }
- var commands []BotCommand
- err = json.Unmarshal(resp.Result, &commands)
- return commands, err
- }
- // CopyMessage copy messages of any kind. The method is analogous to the method
- // forwardMessage, but the copied message doesn't have a link to the original
- // message. Returns the MessageID of the sent message on success.
- func (bot *BotAPI) CopyMessage(config CopyMessageConfig) (MessageID, error) {
- resp, err := bot.Request(config)
- if err != nil {
- return MessageID{}, err
- }
- var messageID MessageID
- err = json.Unmarshal(resp.Result, &messageID)
- return messageID, err
- }
- // AnswerWebAppQuery sets the result of an interaction with a Web App and send a
- // corresponding message on behalf of the user to the chat from which the query originated.
- func (bot *BotAPI) AnswerWebAppQuery(config AnswerWebAppQueryConfig) (SentWebAppMessage, error) {
- var sentWebAppMessage SentWebAppMessage
- resp, err := bot.Request(config)
- if err != nil {
- return sentWebAppMessage, err
- }
- err = json.Unmarshal(resp.Result, &sentWebAppMessage)
- return sentWebAppMessage, err
- }
- // GetMyDefaultAdministratorRights gets the current default administrator rights of the bot.
- func (bot *BotAPI) GetMyDefaultAdministratorRights(config GetMyDefaultAdministratorRightsConfig) (ChatAdministratorRights, error) {
- var rights ChatAdministratorRights
- resp, err := bot.Request(config)
- if err != nil {
- return rights, err
- }
- err = json.Unmarshal(resp.Result, &rights)
- return rights, err
- }
- // EscapeText takes an input text and escape Telegram markup symbols.
- // In this way we can send a text without being afraid of having to escape the characters manually.
- // Note that you don't have to include the formatting style in the input text, or it will be escaped too.
- // If there is an error, an empty string will be returned.
- //
- // parseMode is the text formatting mode (ModeMarkdown, ModeMarkdownV2 or ModeHTML)
- // text is the input string that will be escaped
- func EscapeText(parseMode string, text string) string {
- var replacer *strings.Replacer
- if parseMode == ModeHTML {
- replacer = strings.NewReplacer("<", "<", ">", ">", "&", "&")
- } else if parseMode == ModeMarkdown {
- replacer = strings.NewReplacer("_", "\\_", "*", "\\*", "`", "\\`", "[", "\\[")
- } else if parseMode == ModeMarkdownV2 {
- replacer = strings.NewReplacer(
- "_", "\\_", "*", "\\*", "[", "\\[", "]", "\\]", "(",
- "\\(", ")", "\\)", "~", "\\~", "`", "\\`", ">", "\\>",
- "#", "\\#", "+", "\\+", "-", "\\-", "=", "\\=", "|",
- "\\|", "{", "\\{", "}", "\\}", ".", "\\.", "!", "\\!",
- )
- } else {
- return ""
- }
- return replacer.Replace(text)
- }
|