diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..48b2859 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,33 @@ +name: Test + +on: + push: + branches: + - master + - develop + pull_request: + +jobs: + build: + name: Test + runs-on: ubuntu-latest + steps: + - name: Set up Go 1.x + uses: actions/setup-go@v2 + with: + go-version: ^1.15 + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + + - name: Build + run: go build -v . + + - name: Test + run: go test -coverprofile=coverage.out -covermode=atomic -v . + + - name: Upload coverage report + uses: codecov/codecov-action@v1 + with: + file: ./coverage.out diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5769aa1..0000000 --- a/.travis.yml +++ /dev/null @@ -1,6 +0,0 @@ -language: go - -go: - - '1.10' - - '1.11' - - tip diff --git a/README.md b/README.md index 02cab2d..536077f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![GoDoc](https://godoc.org/github.com/go-telegram-bot-api/telegram-bot-api?status.svg)](http://godoc.org/github.com/go-telegram-bot-api/telegram-bot-api) [![Travis](https://travis-ci.org/go-telegram-bot-api/telegram-bot-api.svg)](https://travis-ci.org/go-telegram-bot-api/telegram-bot-api) -All methods are fairly self explanatory, and reading the godoc page should +All methods are fairly self explanatory, and reading the [godoc](http://godoc.org/github.com/go-telegram-bot-api/telegram-bot-api) page should explain everything. If something isn't clear, open an issue or submit a pull request. diff --git a/bot.go b/bot.go index c6ca084..2f257e1 100644 --- a/bot.go +++ b/bot.go @@ -9,43 +9,60 @@ import ( "fmt" "io" "io/ioutil" + "mime/multipart" "net/http" "net/url" "os" "strings" "time" - - "github.com/technoweenie/multipartstreamer" ) +// HTTPClient is the type needed for the bot to perform HTTP requests. +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) + PostForm(url string, data url.Values) (*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 *http.Client `json:"-"` + 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, &http.Client{}) + 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. -func NewBotAPIWithClient(token string, client *http.Client) (*BotAPI, error) { +// 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() @@ -58,6 +75,11 @@ func NewBotAPIWithClient(token string, client *http.Client) (*BotAPI, error) { 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) (out url.Values) { if in == nil { return url.Values{} @@ -73,25 +95,25 @@ func buildParams(in Params) (out url.Values) { } // MakeRequest makes a request to a specific endpoint with our token. -func (bot *BotAPI) MakeRequest(endpoint string, params Params) (APIResponse, error) { +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(APIEndpoint, bot.Token, endpoint) + method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint) values := buildParams(params) resp, err := bot.Client.PostForm(method, values) if err != nil { - return APIResponse{}, err + return nil, err } defer resp.Body.Close() var apiResp APIResponse bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp) if err != nil { - return apiResp, err + return &apiResp, err } if bot.Debug { @@ -105,13 +127,14 @@ func (bot *BotAPI) MakeRequest(endpoint string, params Params) (APIResponse, err parameters = *apiResp.Parameters } - return apiResp, Error{ + return &apiResp, &Error{ + Code: apiResp.ErrorCode, Message: apiResp.Description, ResponseParameters: parameters, } } - return apiResp, nil + return &apiResp, nil } // decodeAPIResponse decode response and return slice of bytes if debug enabled. @@ -138,103 +161,120 @@ func (bot *BotAPI) decodeAPIResponse(responseBody io.Reader, resp *APIResponse) return data, nil } -// UploadFile makes a request to the API with a file. -// -// Requires the parameter to hold the file not be in the params. -// File should be a string to a file path, a FileBytes struct, -// a FileReader struct, or a url.URL. -// -// Note that if your FileReader has a size set to -1, it will read -// the file into memory to calculate a size. -func (bot *BotAPI) UploadFile(endpoint string, params Params, fieldname string, file interface{}) (APIResponse, error) { - ms := multipartstreamer.New() +// 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) - switch f := file.(type) { - case string: - ms.WriteFields(params) + // 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() - fileHandle, err := os.Open(f) - if err != nil { - return APIResponse{}, err - } - defer fileHandle.Close() - - fi, err := os.Stat(f) - if err != nil { - return APIResponse{}, err + for field, value := range params { + if err := m.WriteField(field, value); err != nil { + w.CloseWithError(err) + return + } } - ms.WriteReader(fieldname, fileHandle.Name(), fi.Size(), fileHandle) - case FileBytes: - ms.WriteFields(params) + for _, file := range files { + switch f := file.File.(type) { + case string: + fileHandle, err := os.Open(f) + if err != nil { + w.CloseWithError(err) + return + } + defer fileHandle.Close() - buf := bytes.NewBuffer(f.Bytes) - ms.WriteReader(fieldname, f.Name, int64(len(f.Bytes)), buf) - case FileReader: - ms.WriteFields(params) + part, err := m.CreateFormFile(file.Name, fileHandle.Name()) + if err != nil { + w.CloseWithError(err) + return + } - if f.Size != -1 { - ms.WriteReader(fieldname, f.Name, f.Size, f.Reader) + io.Copy(part, fileHandle) + case FileBytes: + part, err := m.CreateFormFile(file.Name, f.Name) + if err != nil { + w.CloseWithError(err) + return + } - break + buf := bytes.NewBuffer(f.Bytes) + io.Copy(part, buf) + case FileReader: + part, err := m.CreateFormFile(file.Name, f.Name) + if err != nil { + w.CloseWithError(err) + return + } + + io.Copy(part, f.Reader) + case FileURL: + val := string(f) + if err := m.WriteField(file.Name, val); err != nil { + w.CloseWithError(err) + return + } + case FileID: + val := string(f) + if err := m.WriteField(file.Name, val); err != nil { + w.CloseWithError(err) + return + } + default: + w.CloseWithError(errors.New(ErrBadFileType)) + return + } } - - data, err := ioutil.ReadAll(f.Reader) - if err != nil { - return APIResponse{}, err - } - - buf := bytes.NewBuffer(data) - - ms.WriteReader(fieldname, f.Name, int64(len(data)), buf) - case url.URL: - params[fieldname] = f.String() - - ms.WriteFields(params) - default: - return APIResponse{}, errors.New(ErrBadFileType) - } + }() if bot.Debug { - log.Printf("Endpoint: %s, fieldname: %s, params: %v, file: %T\n", endpoint, fieldname, params, file) + log.Printf("Endpoint: %s, params: %v, with %d files\n", endpoint, params, len(files)) } - method := fmt.Sprintf(APIEndpoint, bot.Token, endpoint) + method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint) - req, err := http.NewRequest("POST", method, nil) + req, err := http.NewRequest("POST", method, r) if err != nil { - return APIResponse{}, err + return nil, err } - ms.SetupRequest(req) + req.Header.Set("Content-Type", m.FormDataContentType()) - res, err := bot.Client.Do(req) + resp, err := bot.Client.Do(req) if err != nil { - return APIResponse{}, err + return nil, err } - defer res.Body.Close() + defer resp.Body.Close() - bytes, err := ioutil.ReadAll(res.Body) + var apiResp APIResponse + bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp) if err != nil { - return APIResponse{}, err + return &apiResp, err } if bot.Debug { log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes)) } - var apiResp APIResponse - - err = json.Unmarshal(bytes, &apiResp) - if err != nil { - return APIResponse{}, err - } - if !apiResp.Ok { - return APIResponse{}, errors.New(apiResp.Description) + var parameters ResponseParameters + + if apiResp.Parameters != nil { + parameters = *apiResp.Parameters + } + + return &apiResp, &Error{ + Message: apiResp.Description, + ResponseParameters: parameters, + } } - return apiResp, nil + return &apiResp, nil } // GetFileDirectURL returns direct URL to file @@ -274,23 +314,54 @@ func (bot *BotAPI) IsMessageToMe(message Message) bool { return strings.Contains(message.Text, "@"+bot.Self.UserName) } +func hasFilesNeedingUpload(files []RequestFile) bool { + for _, file := range files { + switch file.File.(type) { + case string, FileBytes, FileReader: + return true + } + } + + return false +} + // Request sends a Chattable to Telegram, and returns the APIResponse. -func (bot *BotAPI) Request(c Chattable) (APIResponse, error) { +func (bot *BotAPI) Request(c Chattable) (*APIResponse, error) { params, err := c.params() if err != nil { - return APIResponse{}, err + return nil, err } - switch t := c.(type) { - case Fileable: - if t.useExistingFile() { - return bot.MakeRequest(t.method(), params) + 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) } - return bot.UploadFile(t.method(), params, t.name(), t.getFile()) - default: - return bot.MakeRequest(c.method(), params) + // 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 { + var s string + + switch f := file.File.(type) { + case string: + s = f + case FileID: + s = string(f) + case FileURL: + s = string(f) + default: + return nil, errors.New(ErrBadFileType) + } + + params[file.Name] = s + } } + + return bot.MakeRequest(c.method(), params) } // Send will send a Chattable item to Telegram and provides the @@ -309,9 +380,7 @@ func (bot *BotAPI) Send(c Chattable) (Message, error) { // SendMediaGroup sends a media group and returns the resulting messages. func (bot *BotAPI) SendMediaGroup(config MediaGroupConfig) ([]Message, error) { - params, _ := config.params() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return nil, err } @@ -327,9 +396,7 @@ func (bot *BotAPI) SendMediaGroup(config MediaGroupConfig) ([]Message, error) { // It requires UserID. // Offset and Limit are optional. func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) { - params, _ := config.params() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return UserProfilePhotos{}, err } @@ -344,9 +411,7 @@ func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserPro // // Requires FileID. func (bot *BotAPI) GetFile(config FileConfig) (File, error) { - params, _ := config.params() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return File{}, err } @@ -360,14 +425,12 @@ func (bot *BotAPI) GetFile(config FileConfig) (File, error) { // GetUpdates fetches updates. // If a WebHook is set, this will not return any data! // -// Offset, Limit, and Timeout are optional. +// 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) { - params, _ := config.params() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return []Update{}, err } @@ -400,6 +463,7 @@ func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) UpdatesChannel { for { select { case <-bot.shutdownChannel: + close(ch) return default: } @@ -438,22 +502,66 @@ func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel { ch := make(chan Update, bot.Buffer) http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) { - bytes, _ := ioutil.ReadAll(r.Body) + 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 + } - var update Update - json.Unmarshal(bytes, &update) - - ch <- update + ch <- *update }) 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) { - params, _ := config.params() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return Chat{}, err } @@ -469,9 +577,7 @@ func (bot *BotAPI) GetChat(config ChatInfoConfig) (Chat, error) { // 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) { - params, _ := config.params() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return []ChatMember{}, err } @@ -484,9 +590,7 @@ func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]Cha // GetChatMembersCount gets the number of users in a chat. func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) { - params, _ := config.params() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return -1, err } @@ -499,9 +603,7 @@ func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error // GetChatMember gets a specific chat member. func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) { - params, _ := config.params() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return ChatMember{}, err } @@ -514,9 +616,7 @@ func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) // GetGameHighScores allows you to get the high scores for a game. func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) { - params, _ := config.params() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return []GameHighScore{}, err } @@ -529,9 +629,7 @@ func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHigh // GetInviteLink get InviteLink for a chat func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) { - params, _ := config.params() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return "", err } @@ -544,9 +642,7 @@ func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) { // GetStickerSet returns a StickerSet. func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) { - params, _ := config.params() - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return StickerSet{}, err } @@ -559,12 +655,7 @@ func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) // StopPoll stops a poll and returns the result. func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) { - params, err := config.params() - if err != nil { - return Poll{}, err - } - - resp, err := bot.MakeRequest(config.method(), params) + resp, err := bot.Request(config) if err != nil { return Poll{}, err } @@ -574,3 +665,38 @@ func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) { return poll, err } + +// GetMyCommands gets the currently registered commands. +func (bot *BotAPI) GetMyCommands() ([]BotCommand, error) { + config := GetMyCommandsConfig{} + + 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) { + params, err := config.params() + if err != nil { + return MessageID{}, err + } + + resp, err := bot.MakeRequest(config.method(), params) + if err != nil { + return MessageID{}, err + } + + var messageID MessageID + err = json.Unmarshal(resp.Result, &messageID) + + return messageID, err +} diff --git a/bot_test.go b/bot_test.go index fe1fd55..82bf6d4 100644 --- a/bot_test.go +++ b/bot_test.go @@ -11,6 +11,7 @@ import ( const ( TestToken = "153667468:AAHlSHlMqSt1f_uFmVRJbm5gntu2HI4WW8I" ChatID = 76918703 + Channel = "@tgbotapitest" SupergroupChatID = -1001120141283 ReplyToMessageID = 35 ExistingPhotoFileID = "AgADAgADw6cxG4zHKAkr42N7RwEN3IFShCoABHQwXEtVks4EH2wBAAEC" @@ -22,13 +23,27 @@ const ( ExistingStickerFileID = "BQADAgADcwADjMcoCbdl-6eB--YPAg" ) +type testLogger struct { + t *testing.T +} + +func (t testLogger) Println(v ...interface{}) { + t.t.Log(v...) +} + +func (t testLogger) Printf(format string, v ...interface{}) { + t.t.Logf(format, v...) +} + func getBot(t *testing.T) (*BotAPI, error) { bot, err := NewBotAPI(TestToken) bot.Debug = true + logger := testLogger{t} + SetLogger(logger) + if err != nil { t.Error(err) - t.Fail() } return bot, err @@ -39,7 +54,6 @@ func TestNewBotAPI_notoken(t *testing.T) { if err == nil { t.Error(err) - t.Fail() } } @@ -52,7 +66,6 @@ func TestGetUpdates(t *testing.T) { if err != nil { t.Error(err) - t.Fail() } } @@ -60,12 +73,11 @@ func TestSendWithMessage(t *testing.T) { bot, _ := getBot(t) msg := NewMessage(ChatID, "A test message from the test library in telegram-bot-api") - msg.ParseMode = "markdown" + msg.ParseMode = ModeMarkdown _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } @@ -78,7 +90,6 @@ func TestSendWithMessageReply(t *testing.T) { if err != nil { t.Error(err) - t.Fail() } } @@ -90,20 +101,38 @@ func TestSendWithMessageForward(t *testing.T) { if err != nil { t.Error(err) - t.Fail() + } +} + +func TestCopyMessage(t *testing.T) { + bot, _ := getBot(t) + + msg := NewMessage(ChatID, "A test message from the test library in telegram-bot-api") + message, err := bot.Send(msg) + if err != nil { + t.Error(err) + } + + copyMessageConfig := NewCopyMessage(SupergroupChatID, message.Chat.ID, message.MessageID) + messageID, err := bot.CopyMessage(copyMessageConfig) + if err != nil { + t.Error(err) + } + + if messageID.MessageID == message.MessageID { + t.Error("copied message ID was the same as original message") } } func TestSendWithNewPhoto(t *testing.T) { bot, _ := getBot(t) - msg := NewPhotoUpload(ChatID, "tests/image.jpg") + msg := NewPhoto(ChatID, "tests/image.jpg") msg.Caption = "Test" _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } @@ -113,13 +142,12 @@ func TestSendWithNewPhotoWithFileBytes(t *testing.T) { data, _ := ioutil.ReadFile("tests/image.jpg") b := FileBytes{Name: "image.jpg", Bytes: data} - msg := NewPhotoUpload(ChatID, b) + msg := NewPhoto(ChatID, b) msg.Caption = "Test" _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } @@ -127,9 +155,34 @@ func TestSendWithNewPhotoWithFileReader(t *testing.T) { bot, _ := getBot(t) f, _ := os.Open("tests/image.jpg") - reader := FileReader{Name: "image.jpg", Reader: f, Size: -1} + reader := FileReader{Name: "image.jpg", Reader: f} - msg := NewPhotoUpload(ChatID, reader) + msg := NewPhoto(ChatID, reader) + msg.Caption = "Test" + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + } +} + +func TestSendWithNewPhotoReply(t *testing.T) { + bot, _ := getBot(t) + + msg := NewPhoto(ChatID, "tests/image.jpg") + msg.ReplyToMessageID = ReplyToMessageID + + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + } +} + +func TestSendNewPhotoToChannel(t *testing.T) { + bot, _ := getBot(t) + + msg := NewPhotoToChannel(Channel, "tests/image.jpg") msg.Caption = "Test" _, err := bot.Send(msg) @@ -139,12 +192,30 @@ func TestSendWithNewPhotoWithFileReader(t *testing.T) { } } -func TestSendWithNewPhotoReply(t *testing.T) { +func TestSendNewPhotoToChannelFileBytes(t *testing.T) { bot, _ := getBot(t) - msg := NewPhotoUpload(ChatID, "tests/image.jpg") - msg.ReplyToMessageID = ReplyToMessageID + data, _ := ioutil.ReadFile("tests/image.jpg") + b := FileBytes{Name: "image.jpg", Bytes: data} + msg := NewPhotoToChannel(Channel, b) + msg.Caption = "Test" + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendNewPhotoToChannelFileReader(t *testing.T) { + bot, _ := getBot(t) + + f, _ := os.Open("tests/image.jpg") + reader := FileReader{Name: "image.jpg", Reader: f} + + msg := NewPhotoToChannel(Channel, reader) + msg.Caption = "Test" _, err := bot.Send(msg) if err != nil { @@ -156,61 +227,67 @@ func TestSendWithNewPhotoReply(t *testing.T) { func TestSendWithExistingPhoto(t *testing.T) { bot, _ := getBot(t) - msg := NewPhotoShare(ChatID, ExistingPhotoFileID) + msg := NewPhoto(ChatID, FileID(ExistingPhotoFileID)) msg.Caption = "Test" _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } func TestSendWithNewDocument(t *testing.T) { bot, _ := getBot(t) - msg := NewDocumentUpload(ChatID, "tests/image.jpg") + msg := NewDocument(ChatID, "tests/image.jpg") + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + } +} + +func TestSendWithNewDocumentAndThumb(t *testing.T) { + bot, _ := getBot(t) + + msg := NewDocument(ChatID, "tests/voice.ogg") + msg.Thumb = "tests/image.jpg" _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } func TestSendWithExistingDocument(t *testing.T) { bot, _ := getBot(t) - msg := NewDocumentShare(ChatID, ExistingDocumentFileID) + msg := NewDocument(ChatID, FileID(ExistingDocumentFileID)) _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } func TestSendWithNewAudio(t *testing.T) { bot, _ := getBot(t) - msg := NewAudioUpload(ChatID, "tests/audio.mp3") + msg := NewAudio(ChatID, "tests/audio.mp3") msg.Title = "TEST" msg.Duration = 10 msg.Performer = "TEST" - msg.MimeType = "audio/mpeg" - msg.FileSize = 688 _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } func TestSendWithExistingAudio(t *testing.T) { bot, _ := getBot(t) - msg := NewAudioShare(ChatID, ExistingAudioFileID) + msg := NewAudio(ChatID, FileID(ExistingAudioFileID)) msg.Title = "TEST" msg.Duration = 10 msg.Performer = "TEST" @@ -219,33 +296,30 @@ func TestSendWithExistingAudio(t *testing.T) { if err != nil { t.Error(err) - t.Fail() } } func TestSendWithNewVoice(t *testing.T) { bot, _ := getBot(t) - msg := NewVoiceUpload(ChatID, "tests/voice.ogg") + msg := NewVoice(ChatID, "tests/voice.ogg") msg.Duration = 10 _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } func TestSendWithExistingVoice(t *testing.T) { bot, _ := getBot(t) - msg := NewVoiceShare(ChatID, ExistingVoiceFileID) + msg := NewVoice(ChatID, FileID(ExistingVoiceFileID)) msg.Duration = 10 _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } @@ -256,7 +330,6 @@ func TestSendWithContact(t *testing.T) { if _, err := bot.Send(contact); err != nil { t.Error(err) - t.Fail() } } @@ -267,7 +340,6 @@ func TestSendWithLocation(t *testing.T) { if err != nil { t.Error(err) - t.Fail() } } @@ -278,14 +350,13 @@ func TestSendWithVenue(t *testing.T) { if _, err := bot.Send(venue); err != nil { t.Error(err) - t.Fail() } } func TestSendWithNewVideo(t *testing.T) { bot, _ := getBot(t) - msg := NewVideoUpload(ChatID, "tests/video.mp4") + msg := NewVideo(ChatID, "tests/video.mp4") msg.Duration = 10 msg.Caption = "TEST" @@ -293,14 +364,13 @@ func TestSendWithNewVideo(t *testing.T) { if err != nil { t.Error(err) - t.Fail() } } func TestSendWithExistingVideo(t *testing.T) { bot, _ := getBot(t) - msg := NewVideoShare(ChatID, ExistingVideoFileID) + msg := NewVideo(ChatID, FileID(ExistingVideoFileID)) msg.Duration = 10 msg.Caption = "TEST" @@ -308,68 +378,63 @@ func TestSendWithExistingVideo(t *testing.T) { if err != nil { t.Error(err) - t.Fail() } } func TestSendWithNewVideoNote(t *testing.T) { bot, _ := getBot(t) - msg := NewVideoNoteUpload(ChatID, 240, "tests/videonote.mp4") + msg := NewVideoNote(ChatID, 240, "tests/videonote.mp4") msg.Duration = 10 _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } func TestSendWithExistingVideoNote(t *testing.T) { bot, _ := getBot(t) - msg := NewVideoNoteShare(ChatID, 240, ExistingVideoNoteFileID) + msg := NewVideoNote(ChatID, 240, FileID(ExistingVideoNoteFileID)) msg.Duration = 10 _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } func TestSendWithNewSticker(t *testing.T) { bot, _ := getBot(t) - msg := NewStickerUpload(ChatID, "tests/image.jpg") + msg := NewSticker(ChatID, "tests/image.jpg") _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } func TestSendWithExistingSticker(t *testing.T) { bot, _ := getBot(t) - msg := NewStickerShare(ChatID, ExistingStickerFileID) + msg := NewSticker(ChatID, FileID(ExistingStickerFileID)) _, err := bot.Send(msg) if err != nil { t.Error(err) - t.Fail() } } func TestSendWithNewStickerAndKeyboardHide(t *testing.T) { bot, _ := getBot(t) - msg := NewStickerUpload(ChatID, "tests/image.jpg") + msg := NewSticker(ChatID, "tests/image.jpg") msg.ReplyMarkup = ReplyKeyboardRemove{ RemoveKeyboard: true, Selective: false, @@ -378,14 +443,13 @@ func TestSendWithNewStickerAndKeyboardHide(t *testing.T) { if err != nil { t.Error(err) - t.Fail() } } func TestSendWithExistingStickerAndKeyboardHide(t *testing.T) { bot, _ := getBot(t) - msg := NewStickerShare(ChatID, ExistingStickerFileID) + msg := NewSticker(ChatID, FileID(ExistingStickerFileID)) msg.ReplyMarkup = ReplyKeyboardRemove{ RemoveKeyboard: true, Selective: false, @@ -393,10 +457,35 @@ func TestSendWithExistingStickerAndKeyboardHide(t *testing.T) { _, err := bot.Send(msg) + if err != nil { + t.Error(err) + } +} + +func TestSendWithDice(t *testing.T) { + bot, _ := getBot(t) + + msg := NewDice(ChatID) + _, err := bot.Send(msg) + if err != nil { t.Error(err) t.Fail() } + +} + +func TestSendWithDiceWithEmoji(t *testing.T) { + bot, _ := getBot(t) + + msg := NewDiceWithEmoji(ChatID, "🏀") + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } + } func TestGetFile(t *testing.T) { @@ -410,7 +499,6 @@ func TestGetFile(t *testing.T) { if err != nil { t.Error(err) - t.Fail() } } @@ -421,7 +509,6 @@ func TestSendChatConfig(t *testing.T) { if err != nil { t.Error(err) - t.Fail() } } @@ -431,7 +518,6 @@ func TestSendEditMessage(t *testing.T) { msg, err := bot.Send(NewMessage(ChatID, "Testing editing.")) if err != nil { t.Error(err) - t.Fail() } edit := EditMessageTextConfig{ @@ -445,7 +531,6 @@ func TestSendEditMessage(t *testing.T) { _, err = bot.Send(edit) if err != nil { t.Error(err) - t.Fail() } } @@ -455,7 +540,6 @@ func TestGetUserProfilePhotos(t *testing.T) { _, err := bot.GetUserProfilePhotos(NewUserProfilePhotos(ChatID)) if err != nil { t.Error(err) - t.Fail() } } @@ -464,13 +548,17 @@ func TestSetWebhookWithCert(t *testing.T) { time.Sleep(time.Second * 2) - bot.Request(RemoveWebhookConfig{}) + bot.Request(DeleteWebhookConfig{}) + + wh, err := NewWebhookWithCert("https://example.com/tgbotapi-test/"+bot.Token, "tests/cert.pem") + + if err != nil { + t.Error(err) + } + _, err = bot.Request(wh) - wh := NewWebhookWithCert("https://example.com/tgbotapi-test/"+bot.Token, "tests/cert.pem") - _, err := bot.Request(wh) if err != nil { t.Error(err) - t.Fail() } _, err = bot.GetWebhookInfo() @@ -479,7 +567,7 @@ func TestSetWebhookWithCert(t *testing.T) { t.Error(err) } - bot.Request(RemoveWebhookConfig{}) + bot.Request(DeleteWebhookConfig{}) } func TestSetWebhookWithoutCert(t *testing.T) { @@ -487,13 +575,18 @@ func TestSetWebhookWithoutCert(t *testing.T) { time.Sleep(time.Second * 2) - bot.Request(RemoveWebhookConfig{}) + bot.Request(DeleteWebhookConfig{}) + + wh, err := NewWebhook("https://example.com/tgbotapi-test/" + bot.Token) + + if err != nil { + t.Error(err) + } + + _, err = bot.Request(wh) - wh := NewWebhook("https://example.com/tgbotapi-test/" + bot.Token) - _, err := bot.Request(wh) if err != nil { t.Error(err) - t.Fail() } info, err := bot.GetWebhookInfo() @@ -501,21 +594,23 @@ func TestSetWebhookWithoutCert(t *testing.T) { if err != nil { t.Error(err) } - + if info.MaxConnections == 0 { + t.Errorf("Expected maximum connections to be greater than 0") + } if info.LastErrorDate != 0 { t.Errorf("failed to set webhook: %s", info.LastErrorMessage) } - bot.Request(RemoveWebhookConfig{}) + bot.Request(DeleteWebhookConfig{}) } -func TestSendWithMediaGroup(t *testing.T) { +func TestSendWithMediaGroupPhotoVideo(t *testing.T) { bot, _ := getBot(t) cfg := NewMediaGroup(ChatID, []interface{}{ - NewInputMediaPhoto("https://i.imgur.com/unQLJIb.jpg"), - NewInputMediaPhoto("https://i.imgur.com/J5qweNZ.jpg"), - NewInputMediaVideo("https://i.imgur.com/F6RmI24.mp4"), + NewInputMediaPhoto(FileURL("https://github.com/go-telegram-bot-api/telegram-bot-api/raw/0a3a1c8716c4cd8d26a262af9f12dcbab7f3f28c/tests/image.jpg")), + NewInputMediaPhoto("tests/image.jpg"), + NewInputMediaVideo("tests/video.mp4"), }) messages, err := bot.SendMediaGroup(cfg) @@ -524,11 +619,55 @@ func TestSendWithMediaGroup(t *testing.T) { } if messages == nil { - t.Error() + t.Error("No received messages") } - if len(messages) != 3 { - t.Error() + if len(messages) != len(cfg.Media) { + t.Errorf("Different number of messages: %d", len(messages)) + } +} + +func TestSendWithMediaGroupDocument(t *testing.T) { + bot, _ := getBot(t) + + cfg := NewMediaGroup(ChatID, []interface{}{ + NewInputMediaDocument(FileURL("https://i.imgur.com/unQLJIb.jpg")), + NewInputMediaDocument("tests/image.jpg"), + }) + + messages, err := bot.SendMediaGroup(cfg) + if err != nil { + t.Error(err) + } + + if messages == nil { + t.Error("No received messages") + } + + if len(messages) != len(cfg.Media) { + t.Errorf("Different number of messages: %d", len(messages)) + } +} + +func TestSendWithMediaGroupAudio(t *testing.T) { + bot, _ := getBot(t) + + cfg := NewMediaGroup(ChatID, []interface{}{ + NewInputMediaAudio("tests/audio.mp3"), + NewInputMediaAudio("tests/audio.mp3"), + }) + + messages, err := bot.SendMediaGroup(cfg) + if err != nil { + t.Error(err) + } + + if messages == nil { + t.Error("No received messages") + } + + if len(messages) != len(cfg.Media) { + t.Errorf("Different number of messages: %d", len(messages)) } } @@ -576,7 +715,14 @@ func ExampleNewWebhook() { log.Printf("Authorized on account %s", bot.Self.UserName) - _, err = bot.Request(NewWebhookWithCert("https://www.google.com:8443/"+bot.Token, "cert.pem")) + wh, err := NewWebhookWithCert("https://www.google.com:8443/"+bot.Token, "cert.pem") + + if err != nil { + panic(err) + } + + _, err = bot.Request(wh) + if err != nil { panic(err) } @@ -599,6 +745,46 @@ func ExampleNewWebhook() { } } +func ExampleWebhookHandler() { + bot, err := NewBotAPI("MyAwesomeBotToken") + if err != nil { + panic(err) + } + + bot.Debug = true + + log.Printf("Authorized on account %s", bot.Self.UserName) + + wh, err := NewWebhookWithCert("https://www.google.com:8443/"+bot.Token, "cert.pem") + + if err != nil { + panic(err) + } + + _, err = bot.Request(wh) + if err != nil { + panic(err) + } + info, err := bot.GetWebhookInfo() + if err != nil { + panic(err) + } + if info.LastErrorDate != 0 { + log.Printf("[Telegram callback failed]%s", info.LastErrorMessage) + } + + http.HandleFunc("/"+bot.Token, func(w http.ResponseWriter, r *http.Request) { + update, err := bot.HandleUpdate(r) + if err != nil { + log.Printf("%+v\n", err.Error()) + } else { + log.Printf("%+v\n", *update) + } + }) + + go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil) +} + func ExampleInlineConfig() { bot, err := NewBotAPI("MyAwesomeBotToken") // create new bot if err != nil { @@ -637,7 +823,7 @@ func TestDeleteMessage(t *testing.T) { bot, _ := getBot(t) msg := NewMessage(ChatID, "A test message from the test library in telegram-bot-api") - msg.ParseMode = "markdown" + msg.ParseMode = ModeMarkdown message, _ := bot.Send(msg) deleteMessageConfig := DeleteMessageConfig{ @@ -648,7 +834,6 @@ func TestDeleteMessage(t *testing.T) { if err != nil { t.Error(err) - t.Fail() } } @@ -656,7 +841,7 @@ func TestPinChatMessage(t *testing.T) { bot, _ := getBot(t) msg := NewMessage(SupergroupChatID, "A test message from the test library in telegram-bot-api") - msg.ParseMode = "markdown" + msg.ParseMode = ModeMarkdown message, _ := bot.Send(msg) pinChatMessageConfig := PinChatMessageConfig{ @@ -668,7 +853,6 @@ func TestPinChatMessage(t *testing.T) { if err != nil { t.Error(err) - t.Fail() } } @@ -676,7 +860,7 @@ func TestUnpinChatMessage(t *testing.T) { bot, _ := getBot(t) msg := NewMessage(SupergroupChatID, "A test message from the test library in telegram-bot-api") - msg.ParseMode = "markdown" + msg.ParseMode = ModeMarkdown message, _ := bot.Send(msg) // We need pin message to unpin something @@ -688,16 +872,41 @@ func TestUnpinChatMessage(t *testing.T) { if _, err := bot.Request(pinChatMessageConfig); err != nil { t.Error(err) - t.Fail() } unpinChatMessageConfig := UnpinChatMessageConfig{ - ChatID: message.Chat.ID, + ChatID: message.Chat.ID, + MessageID: message.MessageID, } if _, err := bot.Request(unpinChatMessageConfig); err != nil { t.Error(err) - t.Fail() + } +} + +func TestUnpinAllChatMessages(t *testing.T) { + bot, _ := getBot(t) + + msg := NewMessage(SupergroupChatID, "A test message from the test library in telegram-bot-api") + msg.ParseMode = ModeMarkdown + message, _ := bot.Send(msg) + + pinChatMessageConfig := PinChatMessageConfig{ + ChatID: message.Chat.ID, + MessageID: message.MessageID, + DisableNotification: true, + } + + if _, err := bot.Request(pinChatMessageConfig); err != nil { + t.Error(err) + } + + unpinAllChatMessagesConfig := UnpinAllChatMessagesConfig{ + ChatID: message.Chat.ID, + } + + if _, err := bot.Request(unpinAllChatMessagesConfig); err != nil { + t.Error(err) } } @@ -709,27 +918,109 @@ func TestPolls(t *testing.T) { msg, err := bot.Send(poll) if err != nil { t.Error(err) - t.Fail() } result, err := bot.StopPoll(NewStopPoll(SupergroupChatID, msg.MessageID)) if err != nil { t.Error(err) - t.Fail() } if result.Question != "Are polls working?" { t.Error("Poll question did not match") - t.Fail() } if !result.IsClosed { t.Error("Poll did not end") - t.Fail() } if result.Options[0].Text != "Yes" || result.Options[0].VoterCount != 0 || result.Options[1].Text != "No" || result.Options[1].VoterCount != 0 { t.Error("Poll options were incorrect") - t.Fail() + } +} + +func TestSendDice(t *testing.T) { + bot, _ := getBot(t) + + dice := NewSendDice(ChatID) + + msg, err := bot.Send(dice) + if err != nil { + t.Error("Unable to send dice roll") + } + + if msg.Dice == nil { + t.Error("Dice roll was not received") + } +} + +func TestSetCommands(t *testing.T) { + bot, _ := getBot(t) + + setCommands := NewSetMyCommands(BotCommand{ + Command: "test", + Description: "a test command", + }) + + if _, err := bot.Request(setCommands); err != nil { + t.Error("Unable to set commands") + } + + commands, err := bot.GetMyCommands() + if err != nil { + t.Error("Unable to get commands") + } + + if len(commands) != 1 { + t.Error("Incorrect number of commands returned") + } + + if commands[0].Command != "test" || commands[0].Description != "a test command" { + t.Error("Commands were incorrectly set") + } +} + +func TestEditMessageMedia(t *testing.T) { + bot, _ := getBot(t) + + msg := NewPhoto(ChatID, "tests/image.jpg") + msg.Caption = "Test" + m, err := bot.Send(msg) + + if err != nil { + t.Error(err) + } + + edit := EditMessageMediaConfig{ + BaseEdit: BaseEdit{ + ChatID: ChatID, + MessageID: m.MessageID, + }, + Media: NewInputMediaVideo("tests/video.mp4"), + } + + _, err = bot.Request(edit) + if err != nil { + t.Error(err) + } +} + +func TestPrepareInputMediaForParams(t *testing.T) { + media := []interface{}{ + NewInputMediaPhoto("tests/image.jpg"), + NewInputMediaVideo(FileID("test")), + } + + prepared := prepareInputMediaForParams(media) + + if media[0].(InputMediaPhoto).Media != "tests/image.jpg" { + t.Error("Original media was changed") + } + + if prepared[0].(InputMediaPhoto).Media != "attach://file-0" { + t.Error("New media was not replaced") + } + + if prepared[1].(InputMediaVideo).Media != FileID("test") { + t.Error("Passthrough value was not the same") } } diff --git a/configs.go b/configs.go index 0258d74..31f934d 100644 --- a/configs.go +++ b/configs.go @@ -1,8 +1,10 @@ package tgbotapi import ( + "fmt" "io" "net/url" + "strconv" ) // Telegram constants @@ -16,14 +18,16 @@ const ( // Constant values for ChatActions const ( - ChatTyping = "typing" - ChatUploadPhoto = "upload_photo" - ChatRecordVideo = "record_video" - ChatUploadVideo = "upload_video" - ChatRecordAudio = "record_audio" - ChatUploadAudio = "upload_audio" - ChatUploadDocument = "upload_document" - ChatFindLocation = "find_location" + ChatTyping = "typing" + ChatUploadPhoto = "upload_photo" + ChatRecordVideo = "record_video" + ChatUploadVideo = "upload_video" + ChatRecordAudio = "record_audio" + ChatUploadAudio = "upload_audio" + ChatUploadDocument = "upload_document" + ChatFindLocation = "find_location" + ChatRecordVideoNote = "record_video_note" + ChatUploadVideoNote = "upload_video_note" ) // API errors @@ -34,8 +38,57 @@ const ( // Constant values for ParseMode in MessageConfig const ( - ModeMarkdown = "Markdown" - ModeHTML = "HTML" + ModeMarkdown = "Markdown" + ModeMarkdownV2 = "MarkdownV2" + ModeHTML = "HTML" +) + +// Constant values for update types +const ( + // New incoming message of any kind — text, photo, sticker, etc. + UpdateTypeMessage = "message" + + // New version of a message that is known to the bot and was edited + UpdateTypeEditedMessage = "edited_message" + + // New incoming channel post of any kind — text, photo, sticker, etc. + UpdateTypeChannelPost = "channel_post" + + // New version of a channel post that is known to the bot and was edited + UpdateTypeEditedChannelPost = "edited_channel_post" + + // New incoming inline query + UpdateTypeInlineQuery = "inline_query" + + // The result of an inline query that was chosen by a user and sent to their + // chat partner. Please see the documentation on the feedback collecting for + // details on how to enable these updates for your bot. + UpdateTypeChosenInlineResult = "chosen_inline_result" + + // New incoming callback query + UpdateTypeCallbackQuery = "callback_query" + + // New incoming shipping query. Only for invoices with flexible price + UpdateTypeShippingQuery = "shipping_query" + + // New incoming pre-checkout query. Contains full information about checkout + UpdateTypePreCheckoutQuery = "pre_checkout_query" + + // New poll state. Bots receive only updates about stopped polls and polls + // which are sent by the bot + UpdateTypePoll = "poll" + + // A user changed their answer in a non-anonymous poll. Bots receive new votes + // only in polls that were sent by the bot itself. + UpdateTypePollAnswer = "poll_answer" + + // The bot's chat member status was updated in a chat. For private chats, this + // update is received only when the bot is blocked or unblocked by the user. + UpdateTypeMyChatMember = "my_chat_member" + + // The bot must be an administrator in the chat and must explicitly specify + // this update in the list of allowed_updates to receive these updates. + UpdateTypeChatMember = "chat_member" ) // Library errors @@ -51,21 +104,56 @@ type Chattable interface { method() string } +// RequestFile represents a file associated with a request. May involve +// uploading a file, or passing an existing ID. +type RequestFile struct { + // The multipart upload field name. + Name string + // The file to upload. + File interface{} +} + // Fileable is any config type that can be sent that includes a file. type Fileable interface { Chattable - name() string - getFile() interface{} - useExistingFile() bool + files() []RequestFile +} + +// LogOutConfig is a request to log out of the cloud Bot API server. +// +// Note that you may not log back in for at least 10 minutes. +type LogOutConfig struct{} + +func (LogOutConfig) method() string { + return "logOut" +} + +func (LogOutConfig) params() (Params, error) { + return nil, nil +} + +// CloseConfig is a request to close the bot instance on a local server. +// +// Note that you may not close an instance for the first 10 minutes after the +// bot has started. +type CloseConfig struct{} + +func (CloseConfig) method() string { + return "close" +} + +func (CloseConfig) params() (Params, error) { + return nil, nil } // BaseChat is base type for all chat config types. type BaseChat struct { - ChatID int64 // required - ChannelUsername string - ReplyToMessageID int - ReplyMarkup interface{} - DisableNotification bool + ChatID int64 // required + ChannelUsername string + ReplyToMessageID int + ReplyMarkup interface{} + DisableNotification bool + AllowSendingWithoutReply bool } func (chat *BaseChat) params() (Params, error) { @@ -74,6 +162,7 @@ func (chat *BaseChat) params() (Params, error) { params.AddFirstValid("chat_id", chat.ChatID, chat.ChannelUsername) params.AddNonZero("reply_to_message_id", chat.ReplyToMessageID) params.AddBool("disable_notification", chat.DisableNotification) + params.AddBool("allow_sending_without_reply", chat.AllowSendingWithoutReply) err := params.AddInterface("reply_markup", chat.ReplyMarkup) @@ -83,28 +172,11 @@ func (chat *BaseChat) params() (Params, error) { // BaseFile is a base type for all file config types. type BaseFile struct { BaseChat - File interface{} - FileID string - UseExisting bool - MimeType string - FileSize int + File interface{} } func (file BaseFile) params() (Params, error) { - params, err := file.BaseChat.params() - - params.AddNonEmpty("mime_type", file.MimeType) - params.AddNonZero("file_size", file.FileSize) - - return params, err -} - -func (file BaseFile) getFile() interface{} { - return file.File -} - -func (file BaseFile) useExistingFile() bool { - return file.UseExisting + return file.BaseChat.params() } // BaseEdit is base type of all chat edits. @@ -136,6 +208,7 @@ type MessageConfig struct { BaseChat Text string ParseMode string + Entities []MessageEntity DisableWebPagePreview bool } @@ -148,8 +221,9 @@ func (config MessageConfig) params() (Params, error) { params.AddNonEmpty("text", config.Text) params.AddBool("disable_web_page_preview", config.DisableWebPagePreview) params.AddNonEmpty("parse_mode", config.ParseMode) + err = params.AddInterface("entities", config.Entities) - return params, nil + return params, err } func (config MessageConfig) method() string { @@ -180,39 +254,88 @@ func (config ForwardConfig) method() string { return "forwardMessage" } -// PhotoConfig contains information about a SendPhoto request. -type PhotoConfig struct { - BaseFile - Caption string - ParseMode string +// CopyMessageConfig contains information about a copyMessage request. +type CopyMessageConfig struct { + BaseChat + FromChatID int64 + FromChannelUsername string + MessageID int + Caption string + ParseMode string + CaptionEntities []MessageEntity } -func (config PhotoConfig) params() (Params, error) { - params, err := config.BaseFile.params() +func (config CopyMessageConfig) params() (Params, error) { + params, err := config.BaseChat.params() + if err != nil { + return params, err + } - params.AddNonEmpty(config.name(), config.FileID) + params.AddFirstValid("from_chat_id", config.FromChatID, config.FromChannelUsername) + params.AddNonZero("message_id", config.MessageID) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) + err = params.AddInterface("caption_entities", config.CaptionEntities) return params, err } -func (config PhotoConfig) name() string { - return "photo" +func (config CopyMessageConfig) method() string { + return "copyMessage" +} + +// PhotoConfig contains information about a SendPhoto request. +type PhotoConfig struct { + BaseFile + Thumb interface{} + Caption string + ParseMode string + CaptionEntities []MessageEntity +} + +func (config PhotoConfig) params() (Params, error) { + params, err := config.BaseFile.params() + if err != nil { + return params, err + } + + params.AddNonEmpty("caption", config.Caption) + params.AddNonEmpty("parse_mode", config.ParseMode) + err = params.AddInterface("caption_entities", config.CaptionEntities) + + return params, err } func (config PhotoConfig) method() string { return "sendPhoto" } +func (config PhotoConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "photo", + File: config.File, + }} + + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + File: config.Thumb, + }) + } + + return files +} + // AudioConfig contains information about a SendAudio request. type AudioConfig struct { BaseFile - Caption string - ParseMode string - Duration int - Performer string - Title string + Thumb interface{} + Caption string + ParseMode string + CaptionEntities []MessageEntity + Duration int + Performer string + Title string } func (config AudioConfig) params() (Params, error) { @@ -221,129 +344,190 @@ func (config AudioConfig) params() (Params, error) { return params, err } - params.AddNonEmpty(config.name(), config.FileID) params.AddNonZero("duration", config.Duration) params.AddNonEmpty("performer", config.Performer) params.AddNonEmpty("title", config.Title) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) + err = params.AddInterface("caption_entities", config.CaptionEntities) - return params, nil -} - -func (config AudioConfig) name() string { - return "audio" + return params, err } func (config AudioConfig) method() string { return "sendAudio" } +func (config AudioConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "audio", + File: config.File, + }} + + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + File: config.Thumb, + }) + } + + return files +} + // DocumentConfig contains information about a SendDocument request. type DocumentConfig struct { BaseFile - Caption string - ParseMode string + Thumb interface{} + Caption string + ParseMode string + CaptionEntities []MessageEntity + DisableContentTypeDetection bool } func (config DocumentConfig) params() (Params, error) { params, err := config.BaseFile.params() - params.AddNonEmpty(config.name(), config.FileID) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) + params.AddBool("disable_content_type_detection", config.DisableContentTypeDetection) return params, err } -func (config DocumentConfig) name() string { - return "document" -} - func (config DocumentConfig) method() string { return "sendDocument" } +func (config DocumentConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "document", + File: config.File, + }} + + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + File: config.Thumb, + }) + } + + return files +} + // StickerConfig contains information about a SendSticker request. type StickerConfig struct { BaseFile } func (config StickerConfig) params() (Params, error) { - params, err := config.BaseChat.params() - - params.AddNonEmpty(config.name(), config.FileID) - - return params, err -} - -func (config StickerConfig) name() string { - return "sticker" + return config.BaseChat.params() } func (config StickerConfig) method() string { return "sendSticker" } +func (config StickerConfig) files() []RequestFile { + return []RequestFile{{ + Name: "sticker", + File: config.File, + }} +} + // VideoConfig contains information about a SendVideo request. type VideoConfig struct { BaseFile + Thumb interface{} Duration int Caption string ParseMode string + CaptionEntities []MessageEntity SupportsStreaming bool } func (config VideoConfig) params() (Params, error) { params, err := config.BaseChat.params() + if err != nil { + return params, err + } - params.AddNonEmpty(config.name(), config.FileID) params.AddNonZero("duration", config.Duration) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) params.AddBool("supports_streaming", config.SupportsStreaming) + err = params.AddInterface("caption_entities", config.CaptionEntities) return params, err } -func (config VideoConfig) name() string { - return "video" -} - func (config VideoConfig) method() string { return "sendVideo" } +func (config VideoConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "video", + File: config.File, + }} + + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + File: config.Thumb, + }) + } + + return files +} + // AnimationConfig contains information about a SendAnimation request. type AnimationConfig struct { BaseFile - Duration int - Caption string - ParseMode string + Duration int + Thumb interface{} + Caption string + ParseMode string + CaptionEntities []MessageEntity } func (config AnimationConfig) params() (Params, error) { params, err := config.BaseChat.params() + if err != nil { + return params, err + } - params.AddNonEmpty(config.name(), config.FileID) params.AddNonZero("duration", config.Duration) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) + err = params.AddInterface("caption_entities", config.CaptionEntities) return params, err } -func (config AnimationConfig) name() string { - return "animation" -} - func (config AnimationConfig) method() string { return "sendAnimation" } +func (config AnimationConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "animation", + File: config.File, + }} + + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + File: config.Thumb, + }) + } + + return files +} + // VideoNoteConfig contains information about a SendVideoNote request. type VideoNoteConfig struct { BaseFile + Thumb interface{} Duration int Length int } @@ -351,54 +535,85 @@ type VideoNoteConfig struct { func (config VideoNoteConfig) params() (Params, error) { params, err := config.BaseChat.params() - params.AddNonEmpty(config.name(), config.FileID) params.AddNonZero("duration", config.Duration) params.AddNonZero("length", config.Length) return params, err } -func (config VideoNoteConfig) name() string { - return "video_note" -} - func (config VideoNoteConfig) method() string { return "sendVideoNote" } +func (config VideoNoteConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "video_note", + File: config.File, + }} + + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + File: config.Thumb, + }) + } + + return files +} + // VoiceConfig contains information about a SendVoice request. type VoiceConfig struct { BaseFile - Caption string - ParseMode string - Duration int + Thumb interface{} + Caption string + ParseMode string + CaptionEntities []MessageEntity + Duration int } func (config VoiceConfig) params() (Params, error) { params, err := config.BaseChat.params() + if err != nil { + return params, err + } - params.AddNonEmpty(config.name(), config.FileID) params.AddNonZero("duration", config.Duration) params.AddNonEmpty("caption", config.Caption) params.AddNonEmpty("parse_mode", config.ParseMode) + err = params.AddInterface("caption_entities", config.CaptionEntities) return params, err } -func (config VoiceConfig) name() string { - return "voice" -} - func (config VoiceConfig) method() string { return "sendVoice" } +func (config VoiceConfig) files() []RequestFile { + files := []RequestFile{{ + Name: "voice", + File: config.File, + }} + + if config.Thumb != nil { + files = append(files, RequestFile{ + Name: "thumb", + File: config.Thumb, + }) + } + + return files +} + // LocationConfig contains information about a SendLocation request. type LocationConfig struct { BaseChat - Latitude float64 // required - Longitude float64 // required - LivePeriod int // optional + Latitude float64 // required + Longitude float64 // required + HorizontalAccuracy float64 // optional + LivePeriod int // optional + Heading int // optional + ProximityAlertRadius int // optional } func (config LocationConfig) params() (Params, error) { @@ -406,7 +621,10 @@ func (config LocationConfig) params() (Params, error) { params.AddNonZeroFloat("latitude", config.Latitude) params.AddNonZeroFloat("longitude", config.Longitude) + params.AddNonZeroFloat("horizontal_accuracy", config.HorizontalAccuracy) params.AddNonZero("live_period", config.LivePeriod) + params.AddNonZero("heading", config.Heading) + params.AddNonZero("proximity_alert_radius", config.ProximityAlertRadius) return params, err } @@ -418,8 +636,11 @@ func (config LocationConfig) method() string { // EditMessageLiveLocationConfig allows you to update a live location. type EditMessageLiveLocationConfig struct { BaseEdit - Latitude float64 // required - Longitude float64 // required + Latitude float64 // required + Longitude float64 // required + HorizontalAccuracy float64 // optional + Heading int // optional + ProximityAlertRadius int // optional } func (config EditMessageLiveLocationConfig) params() (Params, error) { @@ -427,6 +648,9 @@ func (config EditMessageLiveLocationConfig) params() (Params, error) { params.AddNonZeroFloat("latitude", config.Latitude) params.AddNonZeroFloat("longitude", config.Longitude) + params.AddNonZeroFloat("horizontal_accuracy", config.HorizontalAccuracy) + params.AddNonZero("heading", config.Heading) + params.AddNonZero("proximity_alert_radius", config.ProximityAlertRadius) return params, err } @@ -451,11 +675,14 @@ func (config StopMessageLiveLocationConfig) method() string { // VenueConfig contains information about a SendVenue request. type VenueConfig struct { BaseChat - Latitude float64 // required - Longitude float64 // required - Title string // required - Address string // required - FoursquareID string + Latitude float64 // required + Longitude float64 // required + Title string // required + Address string // required + FoursquareID string + FoursquareType string + GooglePlaceID string + GooglePlaceType string } func (config VenueConfig) params() (Params, error) { @@ -466,6 +693,9 @@ func (config VenueConfig) params() (Params, error) { params["title"] = config.Title params["address"] = config.Address params.AddNonEmpty("foursquare_id", config.FoursquareID) + params.AddNonEmpty("foursquare_type", config.FoursquareType) + params.AddNonEmpty("google_place_id", config.GooglePlaceID) + params.AddNonEmpty("google_place_type", config.GooglePlaceType) return params, err } @@ -502,8 +732,18 @@ func (config ContactConfig) method() string { // SendPollConfig allows you to send a poll. type SendPollConfig struct { BaseChat - Question string - Options []string + Question string + Options []string + IsAnonymous bool + Type string + AllowsMultipleAnswers bool + CorrectOptionID int64 + Explanation string + ExplanationParseMode string + ExplanationEntities []MessageEntity + OpenPeriod int + CloseDate int + IsClosed bool } func (config SendPollConfig) params() (Params, error) { @@ -513,7 +753,19 @@ func (config SendPollConfig) params() (Params, error) { } params["question"] = config.Question - err = params.AddInterface("options", config.Options) + if err = params.AddInterface("options", config.Options); err != nil { + return params, err + } + params["is_anonymous"] = strconv.FormatBool(config.IsAnonymous) + params.AddNonEmpty("type", config.Type) + params["allows_multiple_answers"] = strconv.FormatBool(config.AllowsMultipleAnswers) + params["correct_option_id"] = strconv.FormatInt(config.CorrectOptionID, 10) + params.AddBool("is_closed", config.IsClosed) + params.AddNonEmpty("explanation", config.Explanation) + params.AddNonEmpty("explanation_parse_mode", config.ExplanationParseMode) + params.AddNonZero("open_period", config.OpenPeriod) + params.AddNonZero("close_date", config.CloseDate) + err = params.AddInterface("explanation_entities", config.ExplanationEntities) return params, err } @@ -542,7 +794,7 @@ func (config GameConfig) method() string { // SetGameScoreConfig allows you to update the game score in a chat. type SetGameScoreConfig struct { - UserID int + UserID int64 Score int Force bool DisableEditMessage bool @@ -555,7 +807,7 @@ type SetGameScoreConfig struct { func (config SetGameScoreConfig) params() (Params, error) { params := make(Params) - params.AddNonZero("user_id", config.UserID) + params.AddNonZero64("user_id", config.UserID) params.AddNonZero("scrore", config.Score) params.AddBool("disable_edit_message", config.DisableEditMessage) @@ -575,8 +827,8 @@ func (config SetGameScoreConfig) method() string { // GetGameHighScoresConfig allows you to fetch the high scores for a game. type GetGameHighScoresConfig struct { - UserID int - ChatID int + UserID int64 + ChatID int64 ChannelUsername string MessageID int InlineMessageID string @@ -585,7 +837,7 @@ type GetGameHighScoresConfig struct { func (config GetGameHighScoresConfig) params() (Params, error) { params := make(Params) - params.AddNonZero("user_id", config.UserID) + params.AddNonZero64("user_id", config.UserID) if config.InlineMessageID != "" { params["inline_message_id"] = config.InlineMessageID @@ -624,15 +876,20 @@ type EditMessageTextConfig struct { BaseEdit Text string ParseMode string + Entities []MessageEntity DisableWebPagePreview bool } func (config EditMessageTextConfig) params() (Params, error) { params, err := config.BaseEdit.params() + if err != nil { + return params, err + } params["text"] = config.Text params.AddNonEmpty("parse_mode", config.ParseMode) params.AddBool("disable_web_page_preview", config.DisableWebPagePreview) + err = params.AddInterface("entities", config.Entities) return params, err } @@ -644,15 +901,20 @@ func (config EditMessageTextConfig) method() string { // EditMessageCaptionConfig allows you to modify the caption of a message. type EditMessageCaptionConfig struct { BaseEdit - Caption string - ParseMode string + Caption string + ParseMode string + CaptionEntities []MessageEntity } func (config EditMessageCaptionConfig) params() (Params, error) { params, err := config.BaseEdit.params() + if err != nil { + return params, err + } params["caption"] = config.Caption params.AddNonEmpty("parse_mode", config.ParseMode) + err = params.AddInterface("caption_entities", config.CaptionEntities) return params, err } @@ -661,7 +923,7 @@ func (config EditMessageCaptionConfig) method() string { return "editMessageCaption" } -// EditMessageMediaConfig contains information about editing a message's media. +// EditMessageMediaConfig allows you to make an editMessageMedia request. type EditMessageMediaConfig struct { BaseEdit @@ -674,12 +936,19 @@ func (EditMessageMediaConfig) method() string { func (config EditMessageMediaConfig) params() (Params, error) { params, err := config.BaseEdit.params() + if err != nil { + return params, err + } - params.AddInterface("media", config.Media) + err = params.AddInterface("media", prepareInputMediaParam(config.Media, 0)) return params, err } +func (config EditMessageMediaConfig) files() []RequestFile { + return prepareInputMediaFile(config.Media, 0) +} + // EditMessageReplyMarkupConfig allows you to modify the reply markup // of a message. type EditMessageReplyMarkupConfig struct { @@ -710,7 +979,7 @@ func (StopPollConfig) method() string { // UserProfilePhotosConfig contains information about a // GetUserProfilePhotos request. type UserProfilePhotosConfig struct { - UserID int + UserID int64 Offset int Limit int } @@ -722,7 +991,7 @@ func (UserProfilePhotosConfig) method() string { func (config UserProfilePhotosConfig) params() (Params, error) { params := make(Params) - params.AddNonZero("user_id", config.UserID) + params.AddNonZero64("user_id", config.UserID) params.AddNonZero("offset", config.Offset) params.AddNonZero("limit", config.Limit) @@ -748,9 +1017,10 @@ func (config FileConfig) params() (Params, error) { // UpdateConfig contains information about a GetUpdates request. type UpdateConfig struct { - Offset int - Limit int - Timeout int + Offset int + Limit int + Timeout int + AllowedUpdates []string } func (UpdateConfig) method() string { @@ -763,16 +1033,19 @@ func (config UpdateConfig) params() (Params, error) { params.AddNonZero("offset", config.Offset) params.AddNonZero("limit", config.Limit) params.AddNonZero("timeout", config.Timeout) + params.AddInterface("allowed_updates", config.AllowedUpdates) return params, nil } // WebhookConfig contains information about a SetWebhook request. type WebhookConfig struct { - URL *url.URL - Certificate interface{} - MaxConnections int - AllowedUpdates []string + URL *url.URL + Certificate interface{} + IPAddress string + MaxConnections int + AllowedUpdates []string + DropPendingUpdates bool } func (config WebhookConfig) method() string { @@ -786,36 +1059,42 @@ func (config WebhookConfig) params() (Params, error) { params["url"] = config.URL.String() } + params.AddNonEmpty("ip_address", config.IPAddress) params.AddNonZero("max_connections", config.MaxConnections) - params.AddInterface("allowed_updates", config.AllowedUpdates) + err := params.AddInterface("allowed_updates", config.AllowedUpdates) + params.AddBool("drop_pending_updates", config.DropPendingUpdates) + + return params, err +} + +func (config WebhookConfig) files() []RequestFile { + if config.Certificate != nil { + return []RequestFile{{ + Name: "certificate", + File: config.Certificate, + }} + } + + return nil +} + +// DeleteWebhookConfig is a helper to delete a webhook. +type DeleteWebhookConfig struct { + DropPendingUpdates bool +} + +func (config DeleteWebhookConfig) method() string { + return "deleteWebhook" +} + +func (config DeleteWebhookConfig) params() (Params, error) { + params := make(Params) + + params.AddBool("drop_pending_updates", config.DropPendingUpdates) return params, nil } -func (config WebhookConfig) name() string { - return "certificate" -} - -func (config WebhookConfig) getFile() interface{} { - return config.Certificate -} - -func (config WebhookConfig) useExistingFile() bool { - return config.URL != nil -} - -// RemoveWebhookConfig is a helper to remove a webhook. -type RemoveWebhookConfig struct { -} - -func (config RemoveWebhookConfig) method() string { - return "setWebhook" -} - -func (config RemoveWebhookConfig) params() (Params, error) { - return nil, nil -} - // FileBytes contains information about a set of bytes to upload // as a File. type FileBytes struct { @@ -824,14 +1103,17 @@ type FileBytes struct { } // FileReader contains information about a reader to upload as a File. -// If Size is -1, it will read the entire Reader into memory to -// calculate a Size. type FileReader struct { Name string Reader io.Reader - Size int64 } +// FileURL is a URL to use as a file for a request. +type FileURL string + +// FileID is an ID of a file already uploaded to Telegram. +type FileID string + // InlineConfig contains information on making an InlineQuery response. type InlineConfig struct { InlineQueryID string `json:"inline_query_id"` @@ -856,12 +1138,9 @@ func (config InlineConfig) params() (Params, error) { params.AddNonEmpty("next_offset", config.NextOffset) params.AddNonEmpty("switch_pm_text", config.SwitchPMText) params.AddNonEmpty("switch_pm_parameter", config.SwitchPMParameter) + err := params.AddInterface("results", config.Results) - if err := params.AddInterface("results", config.Results); err != nil { - return params, err - } - - return params, nil + return params, err } // CallbackConfig contains information on making a CallbackQuery response. @@ -895,12 +1174,13 @@ type ChatMemberConfig struct { ChatID int64 SuperGroupUsername string ChannelUsername string - UserID int + UserID int64 } // UnbanChatMemberConfig allows you to unban a user. type UnbanChatMemberConfig struct { ChatMemberConfig + OnlyIfBanned bool } func (config UnbanChatMemberConfig) method() string { @@ -911,7 +1191,8 @@ func (config UnbanChatMemberConfig) params() (Params, error) { params := make(Params) params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername) - params.AddNonZero("user_id", config.UserID) + params.AddNonZero64("user_id", config.UserID) + params.AddBool("only_if_banned", config.OnlyIfBanned) return params, nil } @@ -919,7 +1200,8 @@ func (config UnbanChatMemberConfig) params() (Params, error) { // KickChatMemberConfig contains extra fields to kick user type KickChatMemberConfig struct { ChatMemberConfig - UntilDate int64 + UntilDate int64 + RevokeMessages bool } func (config KickChatMemberConfig) method() string { @@ -930,8 +1212,9 @@ func (config KickChatMemberConfig) params() (Params, error) { params := make(Params) params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) - params.AddNonZero("user_id", config.UserID) + params.AddNonZero64("user_id", config.UserID) params.AddNonZero64("until_date", config.UntilDate) + params.AddBool("revoke_messages", config.RevokeMessages) return params, nil } @@ -939,11 +1222,8 @@ func (config KickChatMemberConfig) params() (Params, error) { // RestrictChatMemberConfig contains fields to restrict members of chat type RestrictChatMemberConfig struct { ChatMemberConfig - UntilDate int64 - CanSendMessages *bool - CanSendMediaMessages *bool - CanSendOtherMessages *bool - CanAddWebPagePreviews *bool + UntilDate int64 + Permissions *ChatPermissions } func (config RestrictChatMemberConfig) method() string { @@ -954,28 +1234,28 @@ func (config RestrictChatMemberConfig) params() (Params, error) { params := make(Params) params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername) - params.AddNonZero("user_id", config.UserID) + params.AddNonZero64("user_id", config.UserID) - params.AddNonNilBool("can_send_messages", config.CanSendMessages) - params.AddNonNilBool("can_send_media_messages", config.CanSendMediaMessages) - params.AddNonNilBool("can_send_other_messages", config.CanSendOtherMessages) - params.AddNonNilBool("can_add_web_page_previews", config.CanAddWebPagePreviews) + err := params.AddInterface("permissions", config.Permissions) params.AddNonZero64("until_date", config.UntilDate) - return params, nil + return params, err } // PromoteChatMemberConfig contains fields to promote members of chat type PromoteChatMemberConfig struct { ChatMemberConfig - CanChangeInfo *bool - CanPostMessages *bool - CanEditMessages *bool - CanDeleteMessages *bool - CanInviteUsers *bool - CanRestrictMembers *bool - CanPinMessages *bool - CanPromoteMembers *bool + IsAnonymous bool + CanManageChat bool + CanChangeInfo bool + CanPostMessages bool + CanEditMessages bool + CanDeleteMessages bool + CanManageVoiceChats bool + CanInviteUsers bool + CanRestrictMembers bool + CanPinMessages bool + CanPromoteMembers bool } func (config PromoteChatMemberConfig) method() string { @@ -986,16 +1266,40 @@ func (config PromoteChatMemberConfig) params() (Params, error) { params := make(Params) params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername) - params.AddNonZero("user_id", config.UserID) + params.AddNonZero64("user_id", config.UserID) - params.AddNonNilBool("can_change_info", config.CanChangeInfo) - params.AddNonNilBool("can_post_messages", config.CanPostMessages) - params.AddNonNilBool("can_edit_messages", config.CanEditMessages) - params.AddNonNilBool("can_delete_messages", config.CanDeleteMessages) - params.AddNonNilBool("can_invite_users", config.CanInviteUsers) - params.AddNonNilBool("can_restrict_members", config.CanRestrictMembers) - params.AddNonNilBool("can_pin_messages", config.CanPinMessages) - params.AddNonNilBool("can_promote_members", config.CanPromoteMembers) + params.AddBool("is_anonymous", config.IsAnonymous) + params.AddBool("can_manage_chat", config.CanManageChat) + params.AddBool("can_change_info", config.CanChangeInfo) + params.AddBool("can_post_messages", config.CanPostMessages) + params.AddBool("can_edit_messages", config.CanEditMessages) + params.AddBool("can_delete_messages", config.CanDeleteMessages) + params.AddBool("can_manage_voice_chats", config.CanManageVoiceChats) + params.AddBool("can_invite_users", config.CanInviteUsers) + params.AddBool("can_restrict_members", config.CanRestrictMembers) + params.AddBool("can_pin_messages", config.CanPinMessages) + params.AddBool("can_promote_members", config.CanPromoteMembers) + + return params, nil +} + +// SetChatAdministratorCustomTitle sets the title of an administrative user +// promoted by the bot for a chat. +type SetChatAdministratorCustomTitle struct { + ChatMemberConfig + CustomTitle string +} + +func (SetChatAdministratorCustomTitle) method() string { + return "setChatAdministratorCustomTitle" +} + +func (config SetChatAdministratorCustomTitle) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername, config.ChannelUsername) + params.AddNonZero64("user_id", config.UserID) + params.AddNonEmpty("custom_title", config.CustomTitle) return params, nil } @@ -1041,6 +1345,27 @@ func (ChatAdministratorsConfig) method() string { return "getChatAdministrators" } +// SetChatPermissionsConfig allows you to set default permissions for the +// members in a group. The bot must be an administrator and have rights to +// restrict members. +type SetChatPermissionsConfig struct { + ChatConfig + Permissions *ChatPermissions +} + +func (SetChatPermissionsConfig) method() string { + return "setChatPermissions" +} + +func (config SetChatPermissionsConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + err := params.AddInterface("permissions", config.Permissions) + + return params, err +} + // ChatInviteLinkConfig contains information about getting a chat link. // // Note that generating a new link will revoke any previous links. @@ -1060,6 +1385,77 @@ func (config ChatInviteLinkConfig) params() (Params, error) { return params, nil } +// CreateChatInviteLinkConfig allows you to create an additional invite link for +// a chat. The bot must be an administrator in the chat for this to work and +// must have the appropriate admin rights. The link can be revoked using the +// RevokeChatInviteLinkConfig. +type CreateChatInviteLinkConfig struct { + ChatConfig + ExpireDate int + MemberLimit int +} + +func (CreateChatInviteLinkConfig) method() string { + return "createChatInviteLink" +} + +func (config CreateChatInviteLinkConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + params.AddNonZero("expire_date", config.ExpireDate) + params.AddNonZero("member_limit", config.MemberLimit) + + return params, nil +} + +// EditChatInviteLinkConfig allows you to edit a non-primary invite link created +// by the bot. The bot must be an administrator in the chat for this to work and +// must have the appropriate admin rights. +type EditChatInviteLinkConfig struct { + ChatConfig + InviteLink string + ExpireDate int + MemberLimit int +} + +func (EditChatInviteLinkConfig) method() string { + return "editChatInviteLink" +} + +func (config EditChatInviteLinkConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + params["invite_link"] = config.InviteLink + params.AddNonZero("expire_date", config.ExpireDate) + params.AddNonZero("member_limit", config.MemberLimit) + + return params, nil +} + +// RevokeChatInviteLinkConfig allows you to revoke an invite link created by the +// bot. If the primary link is revoked, a new link is automatically generated. +// The bot must be an administrator in the chat for this to work and must have +// the appropriate admin rights. +type RevokeChatInviteLinkConfig struct { + ChatConfig + InviteLink string +} + +func (RevokeChatInviteLinkConfig) method() string { + return "revokeChatInviteLink" +} + +func (config RevokeChatInviteLinkConfig) params() (Params, error) { + params := make(Params) + + params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) + params["invite_link"] = config.InviteLink + + return params, nil +} + // LeaveChatConfig allows you to leave a chat. type LeaveChatConfig struct { ChatID int64 @@ -1082,14 +1478,14 @@ func (config LeaveChatConfig) params() (Params, error) { type ChatConfigWithUser struct { ChatID int64 SuperGroupUsername string - UserID int + UserID int64 } func (config ChatConfigWithUser) params() (Params, error) { params := make(Params) params.AddFirstValid("chat_id", config.ChatID, config.SuperGroupUsername) - params.AddNonZero("user_id", config.UserID) + params.AddNonZero64("user_id", config.UserID) return params, nil } @@ -1140,10 +1536,7 @@ func (config InvoiceConfig) params() (Params, error) { params["start_parameter"] = config.StartParameter params["currency"] = config.Currency - if err = params.AddInterface("prices", config.Prices); err != nil { - return params, err - } - + err = params.AddInterface("prices", config.Prices) params.AddNonEmpty("provider_data", config.ProviderData) params.AddNonEmpty("photo_url", config.PhotoURL) params.AddNonZero("photo_size", config.PhotoSize) @@ -1157,7 +1550,7 @@ func (config InvoiceConfig) params() (Params, error) { params.AddBool("send_phone_number_to_provider", config.SendPhoneNumberToProvider) params.AddBool("send_email_to_provider", config.SendEmailToProvider) - return params, nil + return params, err } func (config InvoiceConfig) method() string { @@ -1172,6 +1565,21 @@ type ShippingConfig struct { ErrorMessage string } +func (config ShippingConfig) method() string { + return "answerShippingQuery" +} + +func (config ShippingConfig) params() (Params, error) { + params := make(Params) + + params["shipping_query_id"] = config.ShippingQueryID + params.AddBool("ok", config.OK) + err := params.AddInterface("shipping_options", config.ShippingOptions) + params.AddNonEmpty("error_message", config.ErrorMessage) + + return params, err +} + // PreCheckoutConfig conatins information for answerPreCheckoutQuery request. type PreCheckoutConfig struct { PreCheckoutQueryID string // required @@ -1179,10 +1587,25 @@ type PreCheckoutConfig struct { ErrorMessage string } +func (config PreCheckoutConfig) method() string { + return "answerPreCheckoutQuery" +} + +func (config PreCheckoutConfig) params() (Params, error) { + params := make(Params) + + params["pre_checkout_query_id"] = config.PreCheckoutQueryID + params.AddBool("ok", config.OK) + params.AddNonEmpty("error_message", config.ErrorMessage) + + return params, nil +} + // DeleteMessageConfig contains information of a message in a chat to delete. type DeleteMessageConfig struct { - ChatID int64 - MessageID int + ChannelUsername string + ChatID int64 + MessageID int } func (config DeleteMessageConfig) method() string { @@ -1192,7 +1615,7 @@ func (config DeleteMessageConfig) method() string { func (config DeleteMessageConfig) params() (Params, error) { params := make(Params) - params.AddNonZero64("chat_id", config.ChatID) + params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) params.AddNonZero("message_id", config.MessageID) return params, nil @@ -1220,10 +1643,13 @@ func (config PinChatMessageConfig) params() (Params, error) { return params, nil } -// UnpinChatMessageConfig contains information of chat to unpin. +// UnpinChatMessageConfig contains information of a chat message to unpin. +// +// If MessageID is not specified, it will unpin the most recent pin. type UnpinChatMessageConfig struct { ChatID int64 ChannelUsername string + MessageID int } func (config UnpinChatMessageConfig) method() string { @@ -1233,6 +1659,26 @@ func (config UnpinChatMessageConfig) method() string { func (config UnpinChatMessageConfig) params() (Params, error) { params := make(Params) + params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) + params.AddNonZero("message_id", config.MessageID) + + return params, nil +} + +// UnpinAllChatMessagesConfig contains information of all messages to unpin in +// a chat. +type UnpinAllChatMessagesConfig struct { + ChatID int64 + ChannelUsername string +} + +func (config UnpinAllChatMessagesConfig) method() string { + return "unpinAllChatMessages" +} + +func (config UnpinAllChatMessagesConfig) params() (Params, error) { + params := make(Params) + params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) return params, nil @@ -1247,16 +1693,11 @@ func (config SetChatPhotoConfig) method() string { return "setChatPhoto" } -func (config SetChatPhotoConfig) name() string { - return "photo" -} - -func (config SetChatPhotoConfig) getFile() interface{} { - return config.File -} - -func (config SetChatPhotoConfig) useExistingFile() bool { - return config.UseExisting +func (config SetChatPhotoConfig) files() []RequestFile { + return []RequestFile{{ + Name: "photo", + File: config.File, + }} } // DeleteChatPhotoConfig allows you to delete a group, supergroup, or channel's photo. @@ -1354,24 +1795,22 @@ func (config UploadStickerConfig) params() (Params, error) { return params, nil } -func (config UploadStickerConfig) name() string { - return "png_sticker" -} - -func (config UploadStickerConfig) getFile() interface{} { - return config.PNGSticker -} - -func (config UploadStickerConfig) useExistingFile() bool { - return false +func (config UploadStickerConfig) files() []RequestFile { + return []RequestFile{{ + Name: "png_sticker", + File: config.PNGSticker, + }} } // NewStickerSetConfig allows creating a new sticker set. +// +// You must set either PNGSticker or TGSSticker. type NewStickerSetConfig struct { UserID int64 Name string Title string PNGSticker interface{} + TGSSticker interface{} Emojis string ContainsMasks bool MaskPosition *MaskPosition @@ -1388,10 +1827,6 @@ func (config NewStickerSetConfig) params() (Params, error) { params["name"] = config.Name params["title"] = config.Title - if sticker, ok := config.PNGSticker.(string); ok { - params[config.name()] = sticker - } - params["emojis"] = config.Emojis params.AddBool("contains_masks", config.ContainsMasks) @@ -1401,18 +1836,18 @@ func (config NewStickerSetConfig) params() (Params, error) { return params, err } -func (config NewStickerSetConfig) getFile() interface{} { - return config.PNGSticker -} +func (config NewStickerSetConfig) files() []RequestFile { + if config.PNGSticker != nil { + return []RequestFile{{ + Name: "png_sticker", + File: config.PNGSticker, + }} + } -func (config NewStickerSetConfig) name() string { - return "png_sticker" -} - -func (config NewStickerSetConfig) useExistingFile() bool { - _, ok := config.PNGSticker.(string) - - return ok + return []RequestFile{{ + Name: "tgs_sticker", + File: config.TGSSticker, + }} } // AddStickerConfig allows you to add a sticker to a set. @@ -1420,6 +1855,7 @@ type AddStickerConfig struct { UserID int64 Name string PNGSticker interface{} + TGSSticker interface{} Emojis string MaskPosition *MaskPosition } @@ -1435,25 +1871,24 @@ func (config AddStickerConfig) params() (Params, error) { params["name"] = config.Name params["emojis"] = config.Emojis - if sticker, ok := config.PNGSticker.(string); ok { - params[config.name()] = sticker - } - err := params.AddInterface("mask_position", config.MaskPosition) return params, err } -func (config AddStickerConfig) name() string { - return "png_sticker" -} +func (config AddStickerConfig) files() []RequestFile { + if config.PNGSticker != nil { + return []RequestFile{{ + Name: "png_sticker", + File: config.PNGSticker, + }} + } -func (config AddStickerConfig) getFile() interface{} { - return config.PNGSticker -} + return []RequestFile{{ + Name: "tgs_sticker", + File: config.TGSSticker, + }} -func (config AddStickerConfig) useExistingFile() bool { - return false } // SetStickerPositionConfig allows you to change the position of a sticker in a set. @@ -1492,6 +1927,33 @@ func (config DeleteStickerConfig) params() (Params, error) { return params, nil } +// SetStickerSetThumbConfig allows you to set the thumbnail for a sticker set. +type SetStickerSetThumbConfig struct { + Name string + UserID int64 + Thumb interface{} +} + +func (config SetStickerSetThumbConfig) method() string { + return "setStickerSetThumb" +} + +func (config SetStickerSetThumbConfig) params() (Params, error) { + params := make(Params) + + params["name"] = config.Name + params.AddNonZero64("user_id", config.UserID) + + return params, nil +} + +func (config SetStickerSetThumbConfig) files() []RequestFile { + return []RequestFile{{ + Name: "thumb", + File: config.Thumb, + }} +} + // SetChatStickerSetConfig allows you to set the sticker set for a supergroup. type SetChatStickerSetConfig struct { ChatID int64 @@ -1551,11 +2013,236 @@ func (config MediaGroupConfig) params() (Params, error) { params := make(Params) params.AddFirstValid("chat_id", config.ChatID, config.ChannelUsername) - if err := params.AddInterface("media", config.Media); err != nil { - return params, nil - } params.AddBool("disable_notification", config.DisableNotification) params.AddNonZero("reply_to_message_id", config.ReplyToMessageID) - return params, nil + err := params.AddInterface("media", prepareInputMediaForParams(config.Media)) + + return params, err +} + +func (config MediaGroupConfig) files() []RequestFile { + return prepareInputMediaForFiles(config.Media) +} + +// DiceConfig contains information about a sendDice request. +type DiceConfig struct { + BaseChat + // Emoji on which the dice throw animation is based. + // Currently, must be one of 🎲, 🎯, 🏀, ⚽, 🎳, or 🎰. + // Dice can have values 1-6 for 🎲, 🎯, and 🎳, values 1-5 for 🏀 and ⚽, + // and values 1-64 for 🎰. + // Defaults to “🎲” + Emoji string +} + +func (config DiceConfig) method() string { + return "sendDice" +} + +func (config DiceConfig) params() (Params, error) { + params, err := config.BaseChat.params() + if err != nil { + return params, err + } + + params.AddNonEmpty("emoji", config.Emoji) + + return params, err +} + +// GetMyCommandsConfig gets a list of the currently registered commands. +type GetMyCommandsConfig struct{} + +func (config GetMyCommandsConfig) method() string { + return "getMyCommands" +} + +func (config GetMyCommandsConfig) params() (Params, error) { + return nil, nil +} + +// SetMyCommandsConfig sets a list of commands the bot understands. +type SetMyCommandsConfig struct { + commands []BotCommand +} + +func (config SetMyCommandsConfig) method() string { + return "setMyCommands" +} + +func (config SetMyCommandsConfig) params() (Params, error) { + params := make(Params) + + err := params.AddInterface("commands", config.commands) + + return params, err +} + +// prepareInputMediaParam evaluates a single InputMedia and determines if it +// needs to be modified for a successful upload. If it returns nil, then the +// value does not need to be included in the params. Otherwise, it will return +// the same type as was originally provided. +// +// The idx is used to calculate the file field name. If you only have a single +// file, 0 may be used. It is formatted into "attach://file-%d" for the primary +// media and "attach://file-%d-thumb" for thumbnails. +// +// It is expected to be used in conjunction with prepareInputMediaFile. +func prepareInputMediaParam(inputMedia interface{}, idx int) interface{} { + switch m := inputMedia.(type) { + case InputMediaPhoto: + switch m.Media.(type) { + case string, FileBytes, FileReader: + m.Media = fmt.Sprintf("attach://file-%d", idx) + } + + return m + case InputMediaVideo: + switch m.Media.(type) { + case string, FileBytes, FileReader: + m.Media = fmt.Sprintf("attach://file-%d", idx) + } + + switch m.Thumb.(type) { + case string, FileBytes, FileReader: + m.Thumb = fmt.Sprintf("attach://file-%d-thumb", idx) + } + + return m + case InputMediaAudio: + switch m.Media.(type) { + case string, FileBytes, FileReader: + m.Media = fmt.Sprintf("attach://file-%d", idx) + } + + switch m.Thumb.(type) { + case string, FileBytes, FileReader: + m.Thumb = fmt.Sprintf("attach://file-%d-thumb", idx) + } + + return m + case InputMediaDocument: + switch m.Media.(type) { + case string, FileBytes, FileReader: + m.Media = fmt.Sprintf("attach://file-%d", idx) + } + + switch m.Thumb.(type) { + case string, FileBytes, FileReader: + m.Thumb = fmt.Sprintf("attach://file-%d-thumb", idx) + } + + return m + } + + return nil +} + +// prepareInputMediaFile generates an array of RequestFile to provide for +// Fileable's files method. It returns an array as a single InputMedia may have +// multiple files, for the primary media and a thumbnail. +// +// The idx parameter is used to generate file field names. It uses the names +// "file-%d" for the main file and "file-%d-thumb" for the thumbnail. +// +// It is expected to be used in conjunction with prepareInputMediaParam. +func prepareInputMediaFile(inputMedia interface{}, idx int) []RequestFile { + files := []RequestFile{} + + switch m := inputMedia.(type) { + case InputMediaPhoto: + switch f := m.Media.(type) { + case string, FileBytes, FileReader: + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + File: f, + }) + } + case InputMediaVideo: + switch f := m.Media.(type) { + case string, FileBytes, FileReader: + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + File: f, + }) + } + + switch f := m.Thumb.(type) { + case string, FileBytes, FileReader: + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d-thumb", idx), + File: f, + }) + } + case InputMediaDocument: + switch f := m.Media.(type) { + case string, FileBytes, FileReader: + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + File: f, + }) + } + + switch f := m.Thumb.(type) { + case string, FileBytes, FileReader: + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + File: f, + }) + } + case InputMediaAudio: + switch f := m.Media.(type) { + case string, FileBytes, FileReader: + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + File: f, + }) + } + + switch f := m.Thumb.(type) { + case string, FileBytes, FileReader: + files = append(files, RequestFile{ + Name: fmt.Sprintf("file-%d", idx), + File: f, + }) + } + } + + return files +} + +// prepareInputMediaForParams calls prepareInputMediaParam for each item +// provided and returns a new array with the correct params for a request. +// +// It is expected that files will get data from the associated function, +// prepareInputMediaForFiles. +func prepareInputMediaForParams(inputMedia []interface{}) []interface{} { + newMedia := make([]interface{}, len(inputMedia)) + copy(newMedia, inputMedia) + + for idx, media := range inputMedia { + if param := prepareInputMediaParam(media, idx); param != nil { + newMedia[idx] = param + } + } + + return newMedia +} + +// prepareInputMediaForFiles calls prepareInputMediaFile for each item +// provided and returns a new array with the correct files for a request. +// +// It is expected that params will get data from the associated function, +// prepareInputMediaForParams. +func prepareInputMediaForFiles(inputMedia []interface{}) []RequestFile { + files := []RequestFile{} + + for idx, media := range inputMedia { + if file := prepareInputMediaFile(media, idx); file != nil { + files = append(files, file...) + } + } + + return files } diff --git a/go.mod b/go.mod index b63227e..9e7f65c 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/go-telegram-bot-api/telegram-bot-api/v5 -require github.com/technoweenie/multipartstreamer v1.0.1 +go 1.13 diff --git a/go.sum b/go.sum index 8660600..e69de29 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +0,0 @@ -github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM= -github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= diff --git a/helpers.go b/helpers.go index 3fafe57..5167d86 100644 --- a/helpers.go +++ b/helpers.go @@ -29,7 +29,8 @@ func NewDeleteMessage(chatID int64, messageID int) DeleteMessageConfig { // NewMessageToChannel creates a new Message that is sent to a channel // by username. // -// username is the username of the channel, text is the message text. +// username is the username of the channel, text is the message text, +// and the username should be in the form of `@username`. func NewMessageToChannel(username string, text string) MessageConfig { return MessageConfig{ BaseChat: BaseChat{ @@ -51,241 +52,117 @@ func NewForward(chatID int64, fromChatID int64, messageID int) ForwardConfig { } } -// NewPhotoUpload creates a new photo uploader. +// NewCopyMessage creates a new copy message. +// +// chatID is where to send it, fromChatID is the source chat, +// and messageID is the ID of the original message. +func NewCopyMessage(chatID int64, fromChatID int64, messageID int) CopyMessageConfig { + return CopyMessageConfig{ + BaseChat: BaseChat{ChatID: chatID}, + FromChatID: fromChatID, + MessageID: messageID, + } +} + +// NewPhoto creates a new sendPhoto request. // // chatID is where to send it, file is a string path to the file, // FileReader, or FileBytes. // // Note that you must send animated GIFs as a document. -func NewPhotoUpload(chatID int64, file interface{}) PhotoConfig { +func NewPhoto(chatID int64, file interface{}) PhotoConfig { return PhotoConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } -// NewPhotoShare shares an existing photo. -// You may use this to reshare an existing photo without reuploading it. +// NewPhotoToChannel creates a new photo uploader to send a photo to a channel. // -// chatID is where to send it, fileID is the ID of the file -// already uploaded. -func NewPhotoShare(chatID int64, fileID string) PhotoConfig { +// Note that you must send animated GIFs as a document. +func NewPhotoToChannel(username string, file interface{}) PhotoConfig { return PhotoConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, + BaseChat: BaseChat{ + ChannelUsername: username, + }, + File: file, }, } } -// NewAudioUpload creates a new audio uploader. -// -// chatID is where to send it, file is a string path to the file, -// FileReader, or FileBytes. -func NewAudioUpload(chatID int64, file interface{}) AudioConfig { +// NewAudio creates a new sendAudio request. +func NewAudio(chatID int64, file interface{}) AudioConfig { return AudioConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } -// NewAudioShare shares an existing audio file. -// You may use this to reshare an existing audio file without -// reuploading it. -// -// chatID is where to send it, fileID is the ID of the audio -// already uploaded. -func NewAudioShare(chatID int64, fileID string) AudioConfig { - return AudioConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, - }, - } -} - -// NewDocumentUpload creates a new document uploader. -// -// chatID is where to send it, file is a string path to the file, -// FileReader, or FileBytes. -func NewDocumentUpload(chatID int64, file interface{}) DocumentConfig { +// NewDocument creates a new sendDocument request. +func NewDocument(chatID int64, file interface{}) DocumentConfig { return DocumentConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } -// NewDocumentShare shares an existing document. -// You may use this to reshare an existing document without -// reuploading it. -// -// chatID is where to send it, fileID is the ID of the document -// already uploaded. -func NewDocumentShare(chatID int64, fileID string) DocumentConfig { - return DocumentConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, - }, - } -} - -// NewStickerUpload creates a new sticker uploader. -// -// chatID is where to send it, file is a string path to the file, -// FileReader, or FileBytes. -func NewStickerUpload(chatID int64, file interface{}) StickerConfig { +// NewSticker creates a new sendSticker request. +func NewSticker(chatID int64, file interface{}) StickerConfig { return StickerConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } -// NewStickerShare shares an existing sticker. -// You may use this to reshare an existing sticker without -// reuploading it. -// -// chatID is where to send it, fileID is the ID of the sticker -// already uploaded. -func NewStickerShare(chatID int64, fileID string) StickerConfig { - return StickerConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, - }, - } -} - -// NewVideoUpload creates a new video uploader. -// -// chatID is where to send it, file is a string path to the file, -// FileReader, or FileBytes. -func NewVideoUpload(chatID int64, file interface{}) VideoConfig { +// NewVideo creates a new sendVideo request. +func NewVideo(chatID int64, file interface{}) VideoConfig { return VideoConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } -// NewVideoShare shares an existing video. -// You may use this to reshare an existing video without reuploading it. -// -// chatID is where to send it, fileID is the ID of the video -// already uploaded. -func NewVideoShare(chatID int64, fileID string) VideoConfig { - return VideoConfig{ +// NewAnimation creates a new sendAnimation request. +func NewAnimation(chatID int64, file interface{}) AnimationConfig { + return AnimationConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } -// NewAnimationUpload creates a new animation uploader. +// NewVideoNote creates a new sendVideoNote request. // // chatID is where to send it, file is a string path to the file, // FileReader, or FileBytes. -func NewAnimationUpload(chatID int64, file interface{}) AnimationConfig { - return AnimationConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, - }, - } -} - -// NewAnimationShare shares an existing animation. -// You may use this to reshare an existing animation without reuploading it. -// -// chatID is where to send it, fileID is the ID of the animation -// already uploaded. -func NewAnimationShare(chatID int64, fileID string) AnimationConfig { - return AnimationConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, - }, - } -} - -// NewVideoNoteUpload creates a new video note uploader. -// -// chatID is where to send it, file is a string path to the file, -// FileReader, or FileBytes. -func NewVideoNoteUpload(chatID int64, length int, file interface{}) VideoNoteConfig { +func NewVideoNote(chatID int64, length int, file interface{}) VideoNoteConfig { return VideoNoteConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, Length: length, } } -// NewVideoNoteShare shares an existing video. -// You may use this to reshare an existing video without reuploading it. -// -// chatID is where to send it, fileID is the ID of the video -// already uploaded. -func NewVideoNoteShare(chatID int64, length int, fileID string) VideoNoteConfig { - return VideoNoteConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, - }, - Length: length, - } -} - -// NewVoiceUpload creates a new voice uploader. -// -// chatID is where to send it, file is a string path to the file, -// FileReader, or FileBytes. -func NewVoiceUpload(chatID int64, file interface{}) VoiceConfig { +// NewVoice creates a new sendVoice request. +func NewVoice(chatID int64, file interface{}) VoiceConfig { return VoiceConfig{ BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, - }, - } -} - -// NewVoiceShare shares an existing voice. -// You may use this to reshare an existing voice without reuploading it. -// -// chatID is where to send it, fileID is the ID of the video -// already uploaded. -func NewVoiceShare(chatID int64, fileID string) VoiceConfig { - return VoiceConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, + BaseChat: BaseChat{ChatID: chatID}, + File: file, }, } } @@ -300,7 +177,7 @@ func NewMediaGroup(chatID int64, files []interface{}) MediaGroupConfig { } // NewInputMediaPhoto creates a new InputMediaPhoto. -func NewInputMediaPhoto(media string) InputMediaPhoto { +func NewInputMediaPhoto(media interface{}) InputMediaPhoto { return InputMediaPhoto{ BaseInputMedia{ Type: "photo", @@ -310,7 +187,7 @@ func NewInputMediaPhoto(media string) InputMediaPhoto { } // NewInputMediaVideo creates a new InputMediaVideo. -func NewInputMediaVideo(media string) InputMediaVideo { +func NewInputMediaVideo(media interface{}) InputMediaVideo { return InputMediaVideo{ BaseInputMedia: BaseInputMedia{ Type: "video", @@ -320,7 +197,7 @@ func NewInputMediaVideo(media string) InputMediaVideo { } // NewInputMediaAnimation creates a new InputMediaAnimation. -func NewInputMediaAnimation(media string) InputMediaAnimation { +func NewInputMediaAnimation(media interface{}) InputMediaAnimation { return InputMediaAnimation{ BaseInputMedia: BaseInputMedia{ Type: "animation", @@ -330,7 +207,7 @@ func NewInputMediaAnimation(media string) InputMediaAnimation { } // NewInputMediaAudio creates a new InputMediaAudio. -func NewInputMediaAudio(media string) InputMediaAudio { +func NewInputMediaAudio(media interface{}) InputMediaAudio { return InputMediaAudio{ BaseInputMedia: BaseInputMedia{ Type: "audio", @@ -340,7 +217,7 @@ func NewInputMediaAudio(media string) InputMediaAudio { } // NewInputMediaDocument creates a new InputMediaDocument. -func NewInputMediaDocument(media string) InputMediaDocument { +func NewInputMediaDocument(media interface{}) InputMediaDocument { return InputMediaDocument{ BaseInputMedia: BaseInputMedia{ Type: "document", @@ -400,7 +277,7 @@ func NewChatAction(chatID int64, action string) ChatActionConfig { // NewUserProfilePhotos gets user profile photos. // // userID is the ID of the user you wish to get profile photos from. -func NewUserProfilePhotos(userID int) UserProfilePhotosConfig { +func NewUserProfilePhotos(userID int64) UserProfilePhotosConfig { return UserProfilePhotosConfig{ UserID: userID, Offset: 0, @@ -423,25 +300,33 @@ func NewUpdate(offset int) UpdateConfig { // NewWebhook creates a new webhook. // // link is the url parsable link you wish to get the updates. -func NewWebhook(link string) WebhookConfig { - u, _ := url.Parse(link) +func NewWebhook(link string) (WebhookConfig, error) { + u, err := url.Parse(link) + + if err != nil { + return WebhookConfig{}, err + } return WebhookConfig{ URL: u, - } + }, nil } // NewWebhookWithCert creates a new webhook with a certificate. // // link is the url you wish to get webhooks, // file contains a string to a file, FileReader, or FileBytes. -func NewWebhookWithCert(link string, file interface{}) WebhookConfig { - u, _ := url.Parse(link) +func NewWebhookWithCert(link string, file interface{}) (WebhookConfig, error) { + u, err := url.Parse(link) + + if err != nil { + return WebhookConfig{}, err + } return WebhookConfig{ URL: u, Certificate: file, - } + }, nil } // NewInlineQueryResultArticle creates a new inline query article. @@ -469,6 +354,19 @@ func NewInlineQueryResultArticleMarkdown(id, title, messageText string) InlineQu } } +// NewInlineQueryResultArticleMarkdownV2 creates a new inline query article with MarkdownV2 parsing. +func NewInlineQueryResultArticleMarkdownV2(id, title, messageText string) InlineQueryResultArticle { + return InlineQueryResultArticle{ + Type: "article", + ID: id, + Title: title, + InputMessageContent: InputTextMessageContent{ + Text: messageText, + ParseMode: "MarkdownV2", + }, + } +} + // NewInlineQueryResultArticleHTML creates a new inline query article with HTML parsing. func NewInlineQueryResultArticleHTML(id, title, messageText string) InlineQueryResultArticle { return InlineQueryResultArticle{ @@ -491,6 +389,15 @@ func NewInlineQueryResultGIF(id, url string) InlineQueryResultGIF { } } +// NewInlineQueryResultCachedGIF create a new inline query with cached photo. +func NewInlineQueryResultCachedGIF(id, gifID string) InlineQueryResultCachedGIF { + return InlineQueryResultCachedGIF{ + Type: "gif", + ID: id, + GIFID: gifID, + } +} + // NewInlineQueryResultMPEG4GIF creates a new inline query MPEG4 GIF. func NewInlineQueryResultMPEG4GIF(id, url string) InlineQueryResultMPEG4GIF { return InlineQueryResultMPEG4GIF{ @@ -500,6 +407,15 @@ func NewInlineQueryResultMPEG4GIF(id, url string) InlineQueryResultMPEG4GIF { } } +// NewInlineQueryResultCachedMPEG4GIF create a new inline query with cached MPEG4 GIF. +func NewInlineQueryResultCachedMPEG4GIF(id, MPEG4GIFID string) InlineQueryResultCachedMPEG4GIF { + return InlineQueryResultCachedMPEG4GIF{ + Type: "mpeg4_gif", + ID: id, + MPEG4FileID: MPEG4GIFID, + } +} + // NewInlineQueryResultPhoto creates a new inline query photo. func NewInlineQueryResultPhoto(id, url string) InlineQueryResultPhoto { return InlineQueryResultPhoto{ @@ -519,6 +435,15 @@ func NewInlineQueryResultPhotoWithThumb(id, url, thumb string) InlineQueryResult } } +// NewInlineQueryResultCachedPhoto create a new inline query with cached photo. +func NewInlineQueryResultCachedPhoto(id, photoID string) InlineQueryResultCachedPhoto { + return InlineQueryResultCachedPhoto{ + Type: "photo", + ID: id, + PhotoID: photoID, + } +} + // NewInlineQueryResultVideo creates a new inline query video. func NewInlineQueryResultVideo(id, url string) InlineQueryResultVideo { return InlineQueryResultVideo{ @@ -528,6 +453,26 @@ func NewInlineQueryResultVideo(id, url string) InlineQueryResultVideo { } } +// NewInlineQueryResultCachedVideo create a new inline query with cached video. +func NewInlineQueryResultCachedVideo(id, videoID, title string) InlineQueryResultCachedVideo { + return InlineQueryResultCachedVideo{ + Type: "video", + ID: id, + VideoID: videoID, + Title: title, + } +} + +// NewInlineQueryResultCachedSticker create a new inline query with cached sticker. +func NewInlineQueryResultCachedSticker(id, stickerID, title string) InlineQueryResultCachedSticker { + return InlineQueryResultCachedSticker{ + Type: "sticker", + ID: id, + StickerID: stickerID, + Title: title, + } +} + // NewInlineQueryResultAudio creates a new inline query audio. func NewInlineQueryResultAudio(id, url, title string) InlineQueryResultAudio { return InlineQueryResultAudio{ @@ -538,6 +483,15 @@ func NewInlineQueryResultAudio(id, url, title string) InlineQueryResultAudio { } } +// NewInlineQueryResultCachedAudio create a new inline query with cached photo. +func NewInlineQueryResultCachedAudio(id, audioID string) InlineQueryResultCachedAudio { + return InlineQueryResultCachedAudio{ + Type: "audio", + ID: id, + AudioID: audioID, + } +} + // NewInlineQueryResultVoice creates a new inline query voice. func NewInlineQueryResultVoice(id, url, title string) InlineQueryResultVoice { return InlineQueryResultVoice{ @@ -548,6 +502,16 @@ func NewInlineQueryResultVoice(id, url, title string) InlineQueryResultVoice { } } +// NewInlineQueryResultCachedVoice create a new inline query with cached photo. +func NewInlineQueryResultCachedVoice(id, voiceID, title string) InlineQueryResultCachedVoice { + return InlineQueryResultCachedVoice{ + Type: "voice", + ID: id, + VoiceID: voiceID, + Title: title, + } +} + // NewInlineQueryResultDocument creates a new inline query document. func NewInlineQueryResultDocument(id, url, title, mimeType string) InlineQueryResultDocument { return InlineQueryResultDocument{ @@ -559,6 +523,16 @@ func NewInlineQueryResultDocument(id, url, title, mimeType string) InlineQueryRe } } +// NewInlineQueryResultCachedDocument create a new inline query with cached photo. +func NewInlineQueryResultCachedDocument(id, documentID, title string) InlineQueryResultCachedDocument { + return InlineQueryResultCachedDocument{ + Type: "document", + ID: id, + DocumentID: documentID, + Title: title, + } +} + // NewInlineQueryResultLocation creates a new inline query location. func NewInlineQueryResultLocation(id, title string, latitude, longitude float64) InlineQueryResultLocation { return InlineQueryResultLocation{ @@ -570,6 +544,18 @@ func NewInlineQueryResultLocation(id, title string, latitude, longitude float64) } } +// NewInlineQueryResultVenue creates a new inline query venue. +func NewInlineQueryResultVenue(id, title, address string, latitude, longitude float64) InlineQueryResultVenue { + return InlineQueryResultVenue{ + Type: "venue", + ID: id, + Title: title, + Address: address, + Latitude: latitude, + Longitude: longitude, + } +} + // NewEditMessageText allows you to edit the text of a message. func NewEditMessageText(chatID int64, messageID int, text string) EditMessageTextConfig { return EditMessageTextConfig{ @@ -581,6 +567,18 @@ func NewEditMessageText(chatID int64, messageID int, text string) EditMessageTex } } +// NewEditMessageTextAndMarkup allows you to edit the text and replymarkup of a message. +func NewEditMessageTextAndMarkup(chatID int64, messageID int, text string, replyMarkup InlineKeyboardMarkup) EditMessageTextConfig { + return EditMessageTextConfig{ + BaseEdit: BaseEdit{ + ChatID: chatID, + MessageID: messageID, + ReplyMarkup: &replyMarkup, + }, + Text: text, + } +} + // NewEditMessageCaption allows you to edit the caption of a message. func NewEditMessageCaption(chatID int64, messageID int, caption string) EditMessageCaptionConfig { return EditMessageCaptionConfig{ @@ -604,17 +602,6 @@ func NewEditMessageReplyMarkup(chatID int64, messageID int, replyMarkup InlineKe } } -// NewHideKeyboard hides the keyboard, with the option for being selective -// or hiding for everyone. -func NewHideKeyboard(selective bool) ReplyKeyboardHide { - log.Println("NewHideKeyboard is deprecated, please use NewRemoveKeyboard") - - return ReplyKeyboardHide{ - HideKeyboard: true, - Selective: selective, - } -} - // NewRemoveKeyboard hides the keyboard, with the option for being selective // or hiding for everyone. func NewRemoveKeyboard(selective bool) ReplyKeyboardRemove { @@ -670,6 +657,13 @@ func NewReplyKeyboard(rows ...[]KeyboardButton) ReplyKeyboardMarkup { } } +// NewOneTimeReplyKeyboard creates a new one time keyboard. +func NewOneTimeReplyKeyboard(rows ...[]KeyboardButton) ReplyKeyboardMarkup { + markup := NewReplyKeyboard(rows...) + markup.OneTimeKeyboard = true + return markup +} + // NewInlineKeyboardButtonData creates an inline keyboard button with text // and data for a callback. func NewInlineKeyboardButtonData(text, data string) InlineKeyboardButton { @@ -758,37 +752,6 @@ func NewInvoice(chatID int64, title, description, payload, providerToken, startP Prices: prices} } -// NewSetChatPhotoUpload creates a new chat photo uploader. -// -// chatID is where to send it, file is a string path to the file, -// FileReader, or FileBytes. -// -// Note that you must send animated GIFs as a document. -func NewSetChatPhotoUpload(chatID int64, file interface{}) SetChatPhotoConfig { - return SetChatPhotoConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - File: file, - UseExisting: false, - }, - } -} - -// NewSetChatPhotoShare shares an existing photo. -// You may use this to reshare an existing photo without reuploading it. -// -// chatID is where to send it, fileID is the ID of the file -// already uploaded. -func NewSetChatPhotoShare(chatID int64, fileID string) SetChatPhotoConfig { - return SetChatPhotoConfig{ - BaseFile: BaseFile{ - BaseChat: BaseChat{ChatID: chatID}, - FileID: fileID, - UseExisting: true, - }, - } -} - // NewChatTitle allows you to update the title of a chat. func NewChatTitle(chatID int64, title string) SetChatTitleConfig { return SetChatTitleConfig{ @@ -830,8 +793,9 @@ func NewPoll(chatID int64, question string, options ...string) SendPollConfig { BaseChat: BaseChat{ ChatID: chatID, }, - Question: question, - Options: options, + Question: question, + Options: options, + IsAnonymous: true, // This is Telegram's default. } } @@ -844,3 +808,36 @@ func NewStopPoll(chatID int64, messageID int) StopPollConfig { }, } } + +// NewSendDice allows you to send a random dice roll. +// +// Deprecated: Use NewDice instead. +func NewSendDice(chatID int64) DiceConfig { + return NewDice(chatID) +} + +// NewDice allows you to send a random dice roll. +func NewDice(chatID int64) DiceConfig { + return DiceConfig{ + BaseChat: BaseChat{ + ChatID: chatID, + }, + } +} + +// NewDiceWithEmoji allows you to send a random roll of one of many types. +// +// Emoji may be 🎲 (1-6), 🎯 (1-6), or 🏀 (1-5). +func NewDiceWithEmoji(chatID int64, emoji string) DiceConfig { + return DiceConfig{ + BaseChat: BaseChat{ + ChatID: chatID, + }, + Emoji: emoji, + } +} + +// NewSetMyCommands allows you to set the registered commands. +func NewSetMyCommands(commands ...BotCommand) SetMyCommandsConfig { + return SetMyCommandsConfig{commands: commands} +} diff --git a/helpers_test.go b/helpers_test.go index 47c5d98..3739892 100644 --- a/helpers_test.go +++ b/helpers_test.go @@ -4,6 +4,31 @@ import ( "testing" ) +func TestNewWebhook(t *testing.T) { + result, err := NewWebhook("https://example.com/token") + + if err != nil || + result.URL.String() != "https://example.com/token" || + result.Certificate != interface{}(nil) || + result.MaxConnections != 0 || + len(result.AllowedUpdates) != 0 { + t.Fail() + } +} + +func TestNewWebhookWithCert(t *testing.T) { + exampleFile := File{FileID: "123"} + result, err := NewWebhookWithCert("https://example.com/token", exampleFile) + + if err != nil || + result.URL.String() != "https://example.com/token" || + result.Certificate != exampleFile || + result.MaxConnections != 0 || + len(result.AllowedUpdates) != 0 { + t.Fail() + } +} + func TestNewInlineQueryResultArticle(t *testing.T) { result := NewInlineQueryResultArticle("id", "title", "message") @@ -176,8 +201,8 @@ func TestNewEditMessageCaption(t *testing.T) { func TestNewEditMessageReplyMarkup(t *testing.T) { markup := InlineKeyboardMarkup{ InlineKeyboard: [][]InlineKeyboardButton{ - []InlineKeyboardButton{ - InlineKeyboardButton{Text: "test"}, + { + {Text: "test"}, }, }, } @@ -191,3 +216,21 @@ func TestNewEditMessageReplyMarkup(t *testing.T) { } } + +func TestNewDice(t *testing.T) { + dice := NewDice(42) + + if dice.ChatID != 42 || + dice.Emoji != "" { + t.Fail() + } +} + +func TestNewDiceWithEmoji(t *testing.T) { + dice := NewDiceWithEmoji(42, "🏀") + + if dice.ChatID != 42 || + dice.Emoji != "🏀" { + t.Fail() + } +} diff --git a/params.go b/params.go index 599c8eb..289f7e3 100644 --- a/params.go +++ b/params.go @@ -37,13 +37,6 @@ func (p Params) AddBool(key string, value bool) { } } -// AddNonNilBool adds the value of a bool pointer if not nil. -func (p Params) AddNonNilBool(key string, value *bool) { - if value != nil { - p[key] = strconv.FormatBool(*value) - } -} - // AddNonZeroFloat adds a floating point value that is not zero. func (p Params) AddNonZeroFloat(key string, value float64) { if value != 0 { @@ -76,14 +69,17 @@ func (p Params) AddFirstValid(key string, args ...interface{}) error { case int: if v != 0 { p[key] = strconv.Itoa(v) + return nil } case int64: if v != 0 { p[key] = strconv.FormatInt(v, 10) + return nil } case string: if v != "" { p[key] = v + return nil } case nil: default: @@ -93,6 +89,7 @@ func (p Params) AddFirstValid(key string, args ...interface{}) error { } p[key] = string(b) + return nil } } diff --git a/params_test.go b/params_test.go new file mode 100644 index 0000000..75eb060 --- /dev/null +++ b/params_test.go @@ -0,0 +1,93 @@ +package tgbotapi + +import ( + "testing" +) + +func assertLen(t *testing.T, params Params, l int) { + actual := len(params) + if actual != l { + t.Fatalf("Incorrect number of params, expected %d but found %d\n", l, actual) + } +} + +func assertEq(t *testing.T, a interface{}, b interface{}) { + if a != b { + t.Fatalf("Values did not match, a: %v, b: %v\n", a, b) + } +} + +func TestAddNonEmpty(t *testing.T) { + params := make(Params) + params.AddNonEmpty("value", "value") + assertLen(t, params, 1) + assertEq(t, params["value"], "value") + params.AddNonEmpty("test", "") + assertLen(t, params, 1) + assertEq(t, params["test"], "") +} + +func TestAddNonZero(t *testing.T) { + params := make(Params) + params.AddNonZero("value", 1) + assertLen(t, params, 1) + assertEq(t, params["value"], "1") + params.AddNonZero("test", 0) + assertLen(t, params, 1) + assertEq(t, params["test"], "") +} + +func TestAddNonZero64(t *testing.T) { + params := make(Params) + params.AddNonZero64("value", 1) + assertLen(t, params, 1) + assertEq(t, params["value"], "1") + params.AddNonZero64("test", 0) + assertLen(t, params, 1) + assertEq(t, params["test"], "") +} + +func TestAddBool(t *testing.T) { + params := make(Params) + params.AddBool("value", true) + assertLen(t, params, 1) + assertEq(t, params["value"], "true") + params.AddBool("test", false) + assertLen(t, params, 1) + assertEq(t, params["test"], "") +} + +func TestAddNonZeroFloat(t *testing.T) { + params := make(Params) + params.AddNonZeroFloat("value", 1) + assertLen(t, params, 1) + assertEq(t, params["value"], "1.000000") + params.AddNonZeroFloat("test", 0) + assertLen(t, params, 1) + assertEq(t, params["test"], "") +} + +func TestAddInterface(t *testing.T) { + params := make(Params) + data := struct { + Name string `json:"name"` + }{ + Name: "test", + } + params.AddInterface("value", data) + assertLen(t, params, 1) + assertEq(t, params["value"], `{"name":"test"}`) + params.AddInterface("test", nil) + assertLen(t, params, 1) + assertEq(t, params["test"], "") +} + +func TestAddFirstValid(t *testing.T) { + params := make(Params) + params.AddFirstValid("value", 0, "", "test") + assertLen(t, params, 1) + assertEq(t, params["value"], "test") + params.AddFirstValid("value2", 3, "test") + assertLen(t, params, 2) + assertEq(t, params["value2"], "3") +} diff --git a/passport.go b/passport.go index 5f55006..8b0177e 100644 --- a/passport.go +++ b/passport.go @@ -61,6 +61,8 @@ type ( // Unique identifier for this file FileID string `json:"file_id"` + FileUniqueID string `json:"file_unique_id"` + // File size FileSize int `json:"file_size"` diff --git a/types.go b/types.go index 347d4b8..3b7d315 100644 --- a/types.go +++ b/types.go @@ -13,31 +13,101 @@ import ( // stored raw. type APIResponse struct { Ok bool `json:"ok"` - Result json.RawMessage `json:"result"` - ErrorCode int `json:"error_code"` - Description string `json:"description"` - Parameters *ResponseParameters `json:"parameters"` + Result json.RawMessage `json:"result,omitempty"` + ErrorCode int `json:"error_code,omitempty"` + Description string `json:"description,omitempty"` + Parameters *ResponseParameters `json:"parameters,omitempty"` } -// ResponseParameters are various errors that can be returned in APIResponse. -type ResponseParameters struct { - MigrateToChatID int64 `json:"migrate_to_chat_id"` // optional - RetryAfter int `json:"retry_after"` // optional +// Error is an error containing extra information returned by the Telegram API. +type Error struct { + Code int + Message string + ResponseParameters +} + +// Error message string. +func (e Error) Error() string { + return e.Message } // Update is an update response, from GetUpdates. type Update struct { - UpdateID int `json:"update_id"` - Message *Message `json:"message"` - EditedMessage *Message `json:"edited_message"` - ChannelPost *Message `json:"channel_post"` - EditedChannelPost *Message `json:"edited_channel_post"` - InlineQuery *InlineQuery `json:"inline_query"` - ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result"` - CallbackQuery *CallbackQuery `json:"callback_query"` - ShippingQuery *ShippingQuery `json:"shipping_query"` - PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query"` - Poll *Poll `json:"poll"` + // UpdateID is the update's unique identifier. + // Update identifiers start from a certain positive number and increase + // sequentially. + // This ID becomes especially handy if you're using Webhooks, + // since it allows you to ignore repeated updates or to restore + // the correct update sequence, should they get out of order. + // If there are no new updates for at least a week, then identifier + // of the next update will be chosen randomly instead of sequentially. + UpdateID int `json:"update_id"` + // Message new incoming message of any kind — text, photo, sticker, etc. + // + // optional + Message *Message `json:"message,omitempty"` + // EditedMessage new version of a message that is known to the bot and was + // edited + // + // optional + EditedMessage *Message `json:"edited_message,omitempty"` + // ChannelPost new version of a message that is known to the bot and was + // edited + // + // optional + ChannelPost *Message `json:"channel_post,omitempty"` + // EditedChannelPost new incoming channel post of any kind — text, photo, + // sticker, etc. + // + // optional + EditedChannelPost *Message `json:"edited_channel_post,omitempty"` + // InlineQuery new incoming inline query + // + // optional + InlineQuery *InlineQuery `json:"inline_query,omitempty"` + // ChosenInlineResult is the result of an inline query + // that was chosen by a user and sent to their chat partner. + // Please see our documentation on the feedback collecting + // for details on how to enable these updates for your bot. + // + // optional + ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result,omitempty"` + // CallbackQuery new incoming callback query + // + // optional + CallbackQuery *CallbackQuery `json:"callback_query,omitempty"` + // ShippingQuery new incoming shipping query. Only for invoices with + // flexible price + // + // optional + ShippingQuery *ShippingQuery `json:"shipping_query,omitempty"` + // PreCheckoutQuery new incoming pre-checkout query. Contains full + // information about checkout + // + // optional + PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query,omitempty"` + // Pool new poll state. Bots receive only updates about stopped polls and + // polls, which are sent by the bot + // + // optional + Poll *Poll `json:"poll,omitempty"` + // PollAnswer user changed their answer in a non-anonymous poll. Bots + // receive new votes only in polls that were sent by the bot itself. + // + // optional + PollAnswer *PollAnswer `json:"poll_answer,omitempty"` + // MyChatMember is the bot's chat member status was updated in a chat. For + // private chats, this update is received only when the bot is blocked or + // unblocked by the user. + // + // optional + MyChatMember *ChatMemberUpdated `json:"my_chat_member"` + // ChatMember is a chat member's status was updated in a chat. The bot must + // be an administrator in the chat and must explicitly specify "chat_member" + // in the list of allowed_updates to receive these updates. + // + // optional + ChatMember *ChatMemberUpdated `json:"chat_member"` } // UpdatesChannel is the channel for getting updates. @@ -50,14 +120,44 @@ func (ch UpdatesChannel) Clear() { } } -// User is a user on Telegram. +// User represents a Telegram user or bot. type User struct { - ID int `json:"id"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` // optional - UserName string `json:"username"` // optional - LanguageCode string `json:"language_code"` // optional - IsBot bool `json:"is_bot"` // optional + // ID is a unique identifier for this user or bot + ID int64 `json:"id"` + // IsBot true, if this user is a bot + // + // optional + IsBot bool `json:"is_bot,omitempty"` + // FirstName user's or bot's first name + FirstName string `json:"first_name"` + // LastName user's or bot's last name + // + // optional + LastName string `json:"last_name,omitempty"` + // UserName user's or bot's username + // + // optional + UserName string `json:"username,omitempty"` + // LanguageCode IETF language tag of the user's language + // more info: https://en.wikipedia.org/wiki/IETF_language_tag + // + // optional + LanguageCode string `json:"language_code,omitempty"` + // CanJoinGroups is true, if the bot can be invited to groups. + // Returned only in getMe. + // + // optional + CanJoinGroups bool `json:"can_join_groups,omitempty"` + // CanReadAllGroupMessages is true, if privacy mode is disabled for the bot. + // Returned only in getMe. + // + // optional + CanReadAllGroupMessages bool `json:"can_read_all_group_messages,omitempty"` + // SupportsInlineQueries is true, if the bot supports inline queries. + // Returned only in getMe. + // + // optional + SupportsInlineQueries bool `json:"supports_inline_queries,omitempty"` } // String displays a simple text version of a user. @@ -65,6 +165,9 @@ type User struct { // It is normally a user's username, but falls back to a first/last // name as available. func (u *User) String() string { + if u == nil { + return "" + } if u.UserName != "" { return u.UserName } @@ -77,33 +180,81 @@ func (u *User) String() string { return name } -// GroupChat is a group chat. -type GroupChat struct { - ID int `json:"id"` - Title string `json:"title"` -} - -// ChatPhoto represents a chat photo. -type ChatPhoto struct { - SmallFileID string `json:"small_file_id"` - BigFileID string `json:"big_file_id"` -} - -// Chat contains information about the place a message was sent. +// Chat represents a chat. type Chat struct { - ID int64 `json:"id"` - Type string `json:"type"` - Title string `json:"title"` // optional - UserName string `json:"username"` // optional - FirstName string `json:"first_name"` // optional - LastName string `json:"last_name"` // optional - AllMembersAreAdmins bool `json:"all_members_are_administrators"` // optional - Photo *ChatPhoto `json:"photo"` // optional - Description string `json:"description,omitempty"` // optional - InviteLink string `json:"invite_link,omitempty"` // optional - PinnedMessage *Message `json:"pinned_message"` // optional - StickerSetName string `json:"sticker_set_name"` // optional - CanSetStickerSet bool `json:"can_set_sticker_set"` // optional + // ID is a unique identifier for this chat + ID int64 `json:"id"` + // Type of chat, can be either “private”, “group”, “supergroup” or “channel” + Type string `json:"type"` + // Title for supergroups, channels and group chats + // + // optional + Title string `json:"title,omitempty"` + // UserName for private chats, supergroups and channels if available + // + // optional + UserName string `json:"username,omitempty"` + // FirstName of the other party in a private chat + // + // optional + FirstName string `json:"first_name,omitempty"` + // LastName of the other party in a private chat + // + // optional + LastName string `json:"last_name,omitempty"` + // Photo is a chat photo + Photo *ChatPhoto `json:"photo"` + // Bio is the bio of the other party in a private chat. Returned only in + // getChat + // + // optional + Bio string `json:"bio,omitempty"` + // Description for groups, supergroups and channel chats + // + // optional + Description string `json:"description,omitempty"` + // InviteLink is a chat invite link, for groups, supergroups and channel chats. + // Each administrator in a chat generates their own invite links, + // so the bot must first generate the link using exportChatInviteLink + // + // optional + InviteLink string `json:"invite_link,omitempty"` + // PinnedMessage is the pinned message, for groups, supergroups and channels + // + // optional + PinnedMessage *Message `json:"pinned_message,omitempty"` + // Permissions is default chat member permissions, for groups and + // supergroups. Returned only in getChat. + // + // optional + Permissions *ChatPermissions `json:"permissions,omitempty"` + // SlowModeDelay is for supergroups, the minimum allowed delay between + // consecutive messages sent by each unpriviledged user. Returned only in + // getChat. + // + // optional + SlowModeDelay int `json:"slow_mode_delay,omitempty"` + // StickerSetName is for supergroups, name of group sticker set.Returned + // only in getChat. + // + // optional + StickerSetName string `json:"sticker_set_name,omitempty"` + // CanSetStickerSet is true, if the bot can change the group sticker set. + // Returned only in getChat. + // + // optional + CanSetStickerSet bool `json:"can_set_sticker_set,omitempty"` + // LinkedChatID is a unique identifier for the linked chat, i.e. the + // discussion group identifier for a channel and vice versa; for supergroups + // and channel chats. + // + // optional + LinkedChatID int64 `json:"linked_chat_id,omitempty"` + // Location is for supergroups, the location to which the supergroup is + // connected. Returned only in getChat. + // + // optional + Location *ChatLocation `json:"location"` } // IsPrivate returns if the Chat is a private conversation. @@ -131,56 +282,261 @@ func (c Chat) ChatConfig() ChatConfig { return ChatConfig{ChatID: c.ID} } -// Message is returned by almost every request, and contains data about -// almost anything. +// Message represents a message. type Message struct { - MessageID int `json:"message_id"` - From *User `json:"from"` // optional - Date int `json:"date"` - Chat *Chat `json:"chat"` - ForwardFrom *User `json:"forward_from"` // optional - ForwardFromChat *Chat `json:"forward_from_chat"` // optional - ForwardFromMessageID int `json:"forward_from_message_id"` // optional - ForwardSignature string `json:"forward_signature"` // optional - ForwardSenderName string `json:"forward_sender_name"` // optional - ForwardDate int `json:"forward_date"` // optional - ReplyToMessage *Message `json:"reply_to_message"` // optional - EditDate int `json:"edit_date"` // optional - MediaGroupID string `json:"media_group_id"` // optional - AuthorSignature string `json:"author_signature"` // optional - Text string `json:"text"` // optional - Entities []MessageEntity `json:"entities"` // optional - CaptionEntities []MessageEntity `json:"caption_entities"` // optional - Audio *Audio `json:"audio"` // optional - Document *Document `json:"document"` // optional - Animation *ChatAnimation `json:"animation"` // optional - Game *Game `json:"game"` // optional - Photo []PhotoSize `json:"photo"` // optional - Sticker *Sticker `json:"sticker"` // optional - Video *Video `json:"video"` // optional - VideoNote *VideoNote `json:"video_note"` // optional - Voice *Voice `json:"voice"` // optional - Caption string `json:"caption"` // optional - Contact *Contact `json:"contact"` // optional - Location *Location `json:"location"` // optional - Venue *Venue `json:"venue"` // optional - Poll *Poll `json:"poll"` // optional - NewChatMembers []User `json:"new_chat_members"` // optional - LeftChatMember *User `json:"left_chat_member"` // optional - NewChatTitle string `json:"new_chat_title"` // optional - NewChatPhoto []PhotoSize `json:"new_chat_photo"` // optional - DeleteChatPhoto bool `json:"delete_chat_photo"` // optional - GroupChatCreated bool `json:"group_chat_created"` // optional - SuperGroupChatCreated bool `json:"supergroup_chat_created"` // optional - ChannelChatCreated bool `json:"channel_chat_created"` // optional - MigrateToChatID int64 `json:"migrate_to_chat_id"` // optional - MigrateFromChatID int64 `json:"migrate_from_chat_id"` // optional - PinnedMessage *Message `json:"pinned_message"` // optional - Invoice *Invoice `json:"invoice"` // optional - SuccessfulPayment *SuccessfulPayment `json:"successful_payment"` // optional - ConnectedWebsite string `json:"connected_website"` // optional - PassportData *PassportData `json:"passport_data,omitempty"` // optional - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup"` // optional + // MessageID is a unique message identifier inside this chat + MessageID int `json:"message_id"` + // From is a sender, empty for messages sent to channels; + // + // optional + From *User `json:"from,omitempty"` + // SenderChat is the sender of the message, sent on behalf of a chat. The + // channel itself for channel messages. The supergroup itself for messages + // from anonymous group administrators. The linked channel for messages + // automatically forwarded to the discussion group + // + // optional + SenderChat *Chat `json:"sender_chat,omitempty"` + // Date of the message was sent in Unix time + Date int `json:"date"` + // Chat is the conversation the message belongs to + Chat *Chat `json:"chat"` + // ForwardFrom for forwarded messages, sender of the original message; + // + // optional + ForwardFrom *User `json:"forward_from,omitempty"` + // ForwardFromChat for messages forwarded from channels, + // information about the original channel; + // + // optional + ForwardFromChat *Chat `json:"forward_from_chat,omitempty"` + // ForwardFromMessageID for messages forwarded from channels, + // identifier of the original message in the channel; + // + // optional + ForwardFromMessageID int `json:"forward_from_message_id,omitempty"` + // ForwardSignature for messages forwarded from channels, signature of the + // post author if present + // + // optional + ForwardSignature string `json:"forward_signature,omitempty"` + // ForwardSenderName is the sender's name for messages forwarded from users + // who disallow adding a link to their account in forwarded messages + // + // optional + ForwardSenderName string `json:"forward_sender_name,omitempty"` + // ForwardDate for forwarded messages, date the original message was sent in Unix time; + // + // optional + ForwardDate int `json:"forward_date,omitempty"` + // ReplyToMessage for replies, the original message. + // Note that the Message object in this field will not contain further ReplyToMessage fields + // even if it itself is a reply; + // + // optional + ReplyToMessage *Message `json:"reply_to_message,omitempty"` + // ViaBot through which the message was sent; + // + // optional + ViaBot *User `json:"via_bot,omitempty"` + // EditDate of the message was last edited in Unix time; + // + // optional + EditDate int `json:"edit_date,omitempty"` + // MediaGroupID is the unique identifier of a media message group this message belongs to; + // + // optional + MediaGroupID string `json:"media_group_id,omitempty"` + // AuthorSignature is the signature of the post author for messages in channels; + // + // optional + AuthorSignature string `json:"author_signature,omitempty"` + // Text is for text messages, the actual UTF-8 text of the message, 0-4096 characters; + // + // optional + Text string `json:"text,omitempty"` + // Entities is for text messages, special entities like usernames, + // URLs, bot commands, etc. that appear in the text; + // + // optional + Entities []MessageEntity `json:"entities,omitempty"` + // Animation message is an animation, information about the animation. + // For backward compatibility, when this field is set, the document field will also be set; + // + // optional + Animation *Animation `json:"animation,omitempty"` + // Audio message is an audio file, information about the file; + // + // optional + Audio *Audio `json:"audio,omitempty"` + // Document message is a general file, information about the file; + // + // optional + Document *Document `json:"document,omitempty"` + // Photo message is a photo, available sizes of the photo; + // + // optional + Photo []PhotoSize `json:"photo,omitempty"` + // Sticker message is a sticker, information about the sticker; + // + // optional + Sticker *Sticker `json:"sticker,omitempty"` + // Video message is a video, information about the video; + // + // optional + Video *Video `json:"video,omitempty"` + // VideoNote message is a video note, information about the video message; + // + // optional + VideoNote *VideoNote `json:"video_note,omitempty"` + // Voice message is a voice message, information about the file; + // + // optional + Voice *Voice `json:"voice,omitempty"` + // Caption for the animation, audio, document, photo, video or voice, 0-1024 characters; + // + // optional + Caption string `json:"caption,omitempty"` + // CaptionEntities; + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` + // Contact message is a shared contact, information about the contact; + // + // optional + Contact *Contact `json:"contact,omitempty"` + // Dice is a dice with random value; + // + // optional + Dice *Dice `json:"dice,omitempty"` + // Game message is a game, information about the game; + // + // optional + Game *Game `json:"game,omitempty"` + // Poll is a native poll, information about the poll; + // + // optional + Poll *Poll `json:"poll,omitempty"` + // Venue message is a venue, information about the venue. + // For backward compatibility, when this field is set, the location field + // will also be set; + // + // optional + Venue *Venue `json:"venue,omitempty"` + // Location message is a shared location, information about the location; + // + // optional + Location *Location `json:"location,omitempty"` + // NewChatMembers that were added to the group or supergroup + // and information about them (the bot itself may be one of these members); + // + // optional + NewChatMembers []User `json:"new_chat_members,omitempty"` + // LeftChatMember is a member was removed from the group, + // information about them (this member may be the bot itself); + // + // optional + LeftChatMember *User `json:"left_chat_member,omitempty"` + // NewChatTitle is a chat title was changed to this value; + // + // optional + NewChatTitle string `json:"new_chat_title,omitempty"` + // NewChatPhoto is a chat photo was change to this value; + // + // optional + NewChatPhoto []PhotoSize `json:"new_chat_photo,omitempty"` + // DeleteChatPhoto is a service message: the chat photo was deleted; + // + // optional + DeleteChatPhoto bool `json:"delete_chat_photo,omitempty"` + // GroupChatCreated is a service message: the group has been created; + // + // optional + GroupChatCreated bool `json:"group_chat_created,omitempty"` + // SuperGroupChatCreated is a service message: the supergroup has been created. + // This field can't be received in a message coming through updates, + // because bot can't be a member of a supergroup when it is created. + // It can only be found in ReplyToMessage if someone replies to a very first message + // in a directly created supergroup; + // + // optional + SuperGroupChatCreated bool `json:"supergroup_chat_created,omitempty"` + // ChannelChatCreated is a service message: the channel has been created. + // This field can't be received in a message coming through updates, + // because bot can't be a member of a channel when it is created. + // It can only be found in ReplyToMessage + // if someone replies to a very first message in a channel; + // + // optional + ChannelChatCreated bool `json:"channel_chat_created,omitempty"` + // MessageAutoDeleteTimerChanged is a service message: auto-delete timer + // settings changed in the chat. + // + // optional + MessageAutoDeleteTimerChanged *MessageAutoDeleteTimerChanged `json:"message_auto_delete_timer_changed"` + // MigrateToChatID is the group has been migrated to a supergroup with the specified identifier. + // This number may be greater than 32 bits and some programming languages + // may have difficulty/silent defects in interpreting it. + // But it is smaller than 52 bits, so a signed 64 bit integer + // or double-precision float type are safe for storing this identifier; + // + // optional + MigrateToChatID int64 `json:"migrate_to_chat_id,omitempty"` + // MigrateFromChatID is the supergroup has been migrated from a group with the specified identifier. + // This number may be greater than 32 bits and some programming languages + // may have difficulty/silent defects in interpreting it. + // But it is smaller than 52 bits, so a signed 64 bit integer + // or double-precision float type are safe for storing this identifier; + // + // optional + MigrateFromChatID int64 `json:"migrate_from_chat_id,omitempty"` + // PinnedMessage is a specified message was pinned. + // Note that the Message object in this field will not contain further ReplyToMessage + // fields even if it is itself a reply; + // + // optional + PinnedMessage *Message `json:"pinned_message,omitempty"` + // Invoice message is an invoice for a payment; + // + // optional + Invoice *Invoice `json:"invoice,omitempty"` + // SuccessfulPayment message is a service message about a successful payment, + // information about the payment; + // + // optional + SuccessfulPayment *SuccessfulPayment `json:"successful_payment,omitempty"` + // ConnectedWebsite is Tthe domain name of the website on which the user has + // logged in; + // + // optional + ConnectedWebsite string `json:"connected_website,omitempty"` + // PassportData is a Telegram Passport data; + // + // optional + PassportData *PassportData `json:"passport_data,omitempty"` + // ProximityAlertTriggered is a service message. A user in the chat + // triggered another user's proximity alert while sharing Live Location + // + // optional + ProximityAlertTriggered *ProximityAlertTriggered `json:"proximity_alert_triggered"` + // VoiceChatStarted is a service message: voice chat started. + // + // optional + VoiceChatStarted *VoiceChatStarted `json:"voice_chat_started"` + // VoiceChatEnded is a service message: voice chat ended. + // + // optional + VoiceChatEnded *VoiceChatEnded `json:"voice_chat_ended"` + // VoiceChatParticipantsInvited is a service message: new participants + // invited to a voice chat. + // + // optional + VoiceChatParticipantsInvited *VoiceChatParticipantsInvited `json:"voice_chat_participants_invited"` + // ReplyMarkup is the Inline keyboard attached to the message. + // login_url buttons are represented as ordinary url buttons. + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` } // Time converts the message timestamp into a Time. @@ -195,7 +551,7 @@ func (m *Message) IsCommand() bool { } entity := m.Entities[0] - return entity.Offset == 0 && entity.Type == "bot_command" + return entity.Offset == 0 && entity.IsCommand() } // Command checks if the message was a command and if it was, returns the @@ -252,273 +608,926 @@ func (m *Message) CommandArguments() string { return m.Text[entity.Length+1:] } -// MessageEntity contains information about data in a Message. +// MessageID represents a unique message identifier. +type MessageID struct { + MessageID int `json:"message_id"` +} + +// MessageEntity represents one special entity in a text message. type MessageEntity struct { - Type string `json:"type"` - Offset int `json:"offset"` - Length int `json:"length"` - URL string `json:"url"` // optional - User *User `json:"user"` // optional + // Type of the entity. + // Can be: + // “mention” (@username), + // “hashtag” (#hashtag), + // “cashtag” ($USD), + // “bot_command” (/start@jobs_bot), + // “url” (https://telegram.org), + // “email” (do-not-reply@telegram.org), + // “phone_number” (+1-212-555-0123), + // “bold” (bold text), + // “italic” (italic text), + // “underline” (underlined text), + // “strikethrough” (strikethrough text), + // “code” (monowidth string), + // “pre” (monowidth block), + // “text_link” (for clickable text URLs), + // “text_mention” (for users without usernames) + Type string `json:"type"` + // Offset in UTF-16 code units to the start of the entity + Offset int `json:"offset"` + // Length + Length int `json:"length"` + // URL for “text_link” only, url that will be opened after user taps on the text + // + // optional + URL string `json:"url,omitempty"` + // User for “text_mention” only, the mentioned user + // + // optional + User *User `json:"user,omitempty"` + // Language for “pre” only, the programming language of the entity text + // + // optional + Language string `json:"language,omitempty"` } // ParseURL attempts to parse a URL contained within a MessageEntity. -func (entity MessageEntity) ParseURL() (*url.URL, error) { - if entity.URL == "" { +func (e MessageEntity) ParseURL() (*url.URL, error) { + if e.URL == "" { return nil, errors.New(ErrBadURL) } - return url.Parse(entity.URL) + return url.Parse(e.URL) } -// PhotoSize contains information about photos. +// IsMention returns true if the type of the message entity is "mention" (@username). +func (e MessageEntity) IsMention() bool { + return e.Type == "mention" +} + +// IsHashtag returns true if the type of the message entity is "hashtag". +func (e MessageEntity) IsHashtag() bool { + return e.Type == "hashtag" +} + +// IsCommand returns true if the type of the message entity is "bot_command". +func (e MessageEntity) IsCommand() bool { + return e.Type == "bot_command" +} + +// IsURL returns true if the type of the message entity is "url". +func (e MessageEntity) IsURL() bool { + return e.Type == "url" +} + +// IsEmail returns true if the type of the message entity is "email". +func (e MessageEntity) IsEmail() bool { + return e.Type == "email" +} + +// IsBold returns true if the type of the message entity is "bold" (bold text). +func (e MessageEntity) IsBold() bool { + return e.Type == "bold" +} + +// IsItalic returns true if the type of the message entity is "italic" (italic text). +func (e MessageEntity) IsItalic() bool { + return e.Type == "italic" +} + +// IsCode returns true if the type of the message entity is "code" (monowidth string). +func (e MessageEntity) IsCode() bool { + return e.Type == "code" +} + +// IsPre returns true if the type of the message entity is "pre" (monowidth block). +func (e MessageEntity) IsPre() bool { + return e.Type == "pre" +} + +// IsTextLink returns true if the type of the message entity is "text_link" (clickable text URL). +func (e MessageEntity) IsTextLink() bool { + return e.Type == "text_link" +} + +// PhotoSize represents one size of a photo or a file / sticker thumbnail. type PhotoSize struct { - FileID string `json:"file_id"` - Width int `json:"width"` - Height int `json:"height"` - FileSize int `json:"file_size"` // optional + // FileID identifier for this file, which can be used to download or reuse + // the file + FileID string `json:"file_id"` + // FileUniqueID is the unique identifier for this file, which is supposed to + // be the same over time and for different bots. Can't be used to download + // or reuse the file. + FileUniqueID string `json:"file_unique_id"` + // Width photo width + Width int `json:"width"` + // Height photo height + Height int `json:"height"` + // FileSize file size + // + // optional + FileSize int `json:"file_size,omitempty"` } -// Audio contains information about audio. +// Animation represents an animation file. +type Animation struct { + // FileID odentifier for this file, which can be used to download or reuse + // the file + FileID string `json:"file_id"` + // FileUniqueID is the unique identifier for this file, which is supposed to + // be the same over time and for different bots. Can't be used to download + // or reuse the file. + FileUniqueID string `json:"file_unique_id"` + // Width video width as defined by sender + Width int `json:"width"` + // Height video height as defined by sender + Height int `json:"height"` + // Duration of the video in seconds as defined by sender + Duration int `json:"duration"` + // Thumbnail animation thumbnail as defined by sender + // + // optional + Thumbnail *PhotoSize `json:"thumb,omitempty"` + // FileName original animation filename as defined by sender + // + // optional + FileName string `json:"file_name,omitempty"` + // MimeType of the file as defined by sender + // + // optional + MimeType string `json:"mime_type,omitempty"` + // FileSize file size + // + // optional + FileSize int `json:"file_size,omitempty"` +} + +// Audio represents an audio file to be treated as music by the Telegram clients. type Audio struct { - FileID string `json:"file_id"` - Duration int `json:"duration"` - Performer string `json:"performer"` // optional - Title string `json:"title"` // optional - MimeType string `json:"mime_type"` // optional - FileSize int `json:"file_size"` // optional + // FileID is an identifier for this file, which can be used to download or + // reuse the file + FileID string `json:"file_id"` + // FileUniqueID is the unique identifier for this file, which is supposed to + // be the same over time and for different bots. Can't be used to download + // or reuse the file. + FileUniqueID string `json:"file_unique_id"` + // Duration of the audio in seconds as defined by sender + Duration int `json:"duration"` + // Performer of the audio as defined by sender or by audio tags + // + // optional + Performer string `json:"performer,omitempty"` + // Title of the audio as defined by sender or by audio tags + // + // optional + Title string `json:"title,omitempty"` + // FileName is the original filename as defined by sender + // + // optional + FileName string `json:"file_name,omitempty"` + // MimeType of the file as defined by sender + // + // optional + MimeType string `json:"mime_type,omitempty"` + // FileSize file size + // + // optional + FileSize int `json:"file_size,omitempty"` + // Thumbnail is the album cover to which the music file belongs + // + // optional + Thumbnail *PhotoSize `json:"thumb,omitempty"` } -// Document contains information about a document. +// Document represents a general file. type Document struct { - FileID string `json:"file_id"` - Thumbnail *PhotoSize `json:"thumb"` // optional - FileName string `json:"file_name"` // optional - MimeType string `json:"mime_type"` // optional - FileSize int `json:"file_size"` // optional + // FileID is a identifier for this file, which can be used to download or + // reuse the file + FileID string `json:"file_id"` + // FileUniqueID is the unique identifier for this file, which is supposed to + // be the same over time and for different bots. Can't be used to download + // or reuse the file. + FileUniqueID string `json:"file_unique_id"` + // Thumbnail document thumbnail as defined by sender + // + // optional + Thumbnail *PhotoSize `json:"thumb,omitempty"` + // FileName original filename as defined by sender + // + // optional + FileName string `json:"file_name,omitempty"` + // MimeType of the file as defined by sender + // + // optional + MimeType string `json:"mime_type,omitempty"` + // FileSize file size + // + // optional + FileSize int `json:"file_size,omitempty"` } -// Sticker contains information about a sticker. -type Sticker struct { - FileID string `json:"file_id"` - Width int `json:"width"` - Height int `json:"height"` - Thumbnail *PhotoSize `json:"thumb"` // optional - Emoji string `json:"emoji"` // optional - SetName string `json:"set_name"` // optional - MaskPosition MaskPosition `json:"mask_position"` //optional - FileSize int `json:"file_size"` // optional -} - -// MaskPosition is the position of a mask. -type MaskPosition struct { - Point string `json:"point"` - XShift float32 `json:"x_shift"` - YShift float32 `json:"y_shift"` - Scale float32 `json:"scale"` - FileID string `json:"file_id"` - Width int `json:"width"` - Height int `json:"height"` - Thumbnail *PhotoSize `json:"thumb"` // optional - Emoji string `json:"emoji"` // optional - FileSize int `json:"file_size"` // optional - SetName string `json:"set_name"` // optional -} - -// ChatAnimation contains information about an animation. -type ChatAnimation struct { - FileID string `json:"file_id"` - Width int `json:"width"` - Height int `json:"height"` - Duration int `json:"duration"` - Thumbnail *PhotoSize `json:"thumb"` // optional - FileName string `json:"file_name"` // optional - MimeType string `json:"mime_type"` // optional - FileSize int `json:"file_size"` // optional -} - -// Video contains information about a video. +// Video represents a video file. type Video struct { - FileID string `json:"file_id"` - Width int `json:"width"` - Height int `json:"height"` - Duration int `json:"duration"` - Thumbnail *PhotoSize `json:"thumb"` // optional - MimeType string `json:"mime_type"` // optional - FileSize int `json:"file_size"` // optional + // FileID identifier for this file, which can be used to download or reuse + // the file + FileID string `json:"file_id"` + // FileUniqueID is the unique identifier for this file, which is supposed to + // be the same over time and for different bots. Can't be used to download + // or reuse the file. + FileUniqueID string `json:"file_unique_id"` + // Width video width as defined by sender + Width int `json:"width"` + // Height video height as defined by sender + Height int `json:"height"` + // Duration of the video in seconds as defined by sender + Duration int `json:"duration"` + // Thumbnail video thumbnail + // + // optional + Thumbnail *PhotoSize `json:"thumb,omitempty"` + // FileName is the original filename as defined by sender + // + // optional + FileName string `json:"file_name,omitempty"` + // MimeType of a file as defined by sender + // + // optional + MimeType string `json:"mime_type,omitempty"` + // FileSize file size + // + // optional + FileSize int `json:"file_size,omitempty"` } -// VideoNote contains information about a video. +// VideoNote object represents a video message. type VideoNote struct { - FileID string `json:"file_id"` - Length int `json:"length"` - Duration int `json:"duration"` - Thumbnail *PhotoSize `json:"thumb"` // optional - FileSize int `json:"file_size"` // optional + // FileID identifier for this file, which can be used to download or reuse the file + FileID string `json:"file_id"` + // FileUniqueID is the unique identifier for this file, which is supposed to + // be the same over time and for different bots. Can't be used to download + // or reuse the file. + FileUniqueID string `json:"file_unique_id"` + // Length video width and height (diameter of the video message) as defined by sender + Length int `json:"length"` + // Duration of the video in seconds as defined by sender + Duration int `json:"duration"` + // Thumbnail video thumbnail + // + // optional + Thumbnail *PhotoSize `json:"thumb,omitempty"` + // FileSize file size + // + // optional + FileSize int `json:"file_size,omitempty"` } -// Voice contains information about a voice. +// Voice represents a voice note. type Voice struct { - FileID string `json:"file_id"` - Duration int `json:"duration"` - MimeType string `json:"mime_type"` // optional - FileSize int `json:"file_size"` // optional + // FileID identifier for this file, which can be used to download or reuse the file + FileID string `json:"file_id"` + // FileUniqueID is the unique identifier for this file, which is supposed to + // be the same over time and for different bots. Can't be used to download + // or reuse the file. + FileUniqueID string `json:"file_unique_id"` + // Duration of the audio in seconds as defined by sender + Duration int `json:"duration"` + // MimeType of the file as defined by sender + // + // optional + MimeType string `json:"mime_type,omitempty"` + // FileSize file size + // + // optional + FileSize int `json:"file_size,omitempty"` } -// Contact contains information about a contact. +// Contact represents a phone contact. // // Note that LastName and UserID may be empty. type Contact struct { + // PhoneNumber contact's phone number PhoneNumber string `json:"phone_number"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` // optional - UserID int `json:"user_id"` // optional - VCard string `json:"vcard"` // optional + // FirstName contact's first name + FirstName string `json:"first_name"` + // LastName contact's last name + // + // optional + LastName string `json:"last_name,omitempty"` + // UserID contact's user identifier in Telegram + // + // optional + UserID int64 `json:"user_id,omitempty"` + // VCard is additional data about the contact in the form of a vCard. + // + // optional + VCard string `json:"vcard,omitempty"` } -// Location contains information about a place. -type Location struct { - Longitude float64 `json:"longitude"` - Latitude float64 `json:"latitude"` -} - -// Venue contains information about a venue, including its Location. -type Venue struct { - Location Location `json:"location"` - Title string `json:"title"` - Address string `json:"address"` - FoursquareID string `json:"foursquare_id"` // optional +// Dice represents an animated emoji that displays a random value. +type Dice struct { + // Emoji on which the dice throw animation is based + Emoji string `json:"emoji"` + // Value of the dice + Value int `json:"value"` } // PollOption contains information about one answer option in a poll. type PollOption struct { - Text string `json:"text"` - VoterCount int `json:"voter_count"` + // Text is the option text, 1-100 characters + Text string `json:"text"` + // VoterCount is the number of users that voted for this option + VoterCount int `json:"voter_count"` +} + +// PollAnswer represents an answer of a user in a non-anonymous poll. +type PollAnswer struct { + // PollID is the unique poll identifier + PollID string `json:"poll_id"` + // User who changed the answer to the poll + User User `json:"user"` + // OptionIDs is the 0-based identifiers of poll options chosen by the user. + // May be empty if user retracted vote. + OptionIDs []int `json:"option_ids"` } // Poll contains information about a poll. type Poll struct { - ID string `json:"id"` - Question string `json:"question"` - Options []PollOption `json:"options"` - IsClosed bool `json:"is_closed"` + // ID is the unique poll identifier + ID string `json:"id"` + // Question is the poll question, 1-255 characters + Question string `json:"question"` + // Options is the list of poll options + Options []PollOption `json:"options"` + // TotalVoterCount is the total numbers of users who voted in the poll + TotalVoterCount int `json:"total_voter_count"` + // IsClosed is if the poll is closed + IsClosed bool `json:"is_closed"` + // IsAnonymous is if the poll is anonymous + IsAnonymous bool `json:"is_anonymous"` + // Type is the poll type, currently can be "regular" or "quiz" + Type string `json:"type"` + // AllowsMultipleAnswers is true, if the poll allows multiple answers + AllowsMultipleAnswers bool `json:"allows_multiple_answers"` + // CorrectOptionID is the 0-based identifier of the correct answer option. + // Available only for polls in quiz mode, which are closed, or was sent (not + // forwarded) by the bot or to the private chat with the bot. + // + // optional + CorrectOptionID int `json:"correct_option_id,omitempty"` + // Explanation is text that is shown when a user chooses an incorrect answer + // or taps on the lamp icon in a quiz-style poll, 0-200 characters + // + // optional + Explanation string `json:"explanation,omitempty"` + // ExplainationEntities are special entities like usernames, URLs, bot + // commands, etc. that appear in the explanation + // + // optional + ExplanationEntities []MessageEntity `json:"explanation_entities,omitempty"` + // OpenPeriod is the amount of time in seconds the poll will be active + // after creation + // + // optional + OpenPeriod int `json:"open_period,omitempty"` + // Closedate is the point in time (unix timestamp) when the poll will be + // automatically closed + // + // optional + CloseDate int `json:"close_date,omitempty"` +} + +// Location represents a point on the map. +type Location struct { + // Longitude as defined by sender + Longitude float64 `json:"longitude"` + // Latitude as defined by sender + Latitude float64 `json:"latitude"` + // HorizontalAccuracy is the radius of uncertainty for the location, + // measured in meters; 0-1500 + // + // optional + HorizontalAccuracy float64 `json:"horizontal_accuracy,omitempty"` + // LivePeriod is time relative to the message sending date, during which the + // location can be updated, in seconds. For active live locations only. + // + // optional + LivePeriod int `json:"live_period,omitempty"` + // Heading is the direction in which user is moving, in degrees; 1-360. For + // active live locations only. + // + // optional + Heading int `json:"heading,omitempty"` + // ProximityAlertRadius is the maximum distance for proximity alerts about + // approaching another chat member, in meters. For sent live locations only. + // + // optional + ProximityAlertRadius int `json:"proximity_alert_radius,omitempty"` +} + +// Venue represents a venue. +type Venue struct { + // Location is the venue location + Location Location `json:"location"` + // Title is the name of the venue + Title string `json:"title"` + // Address of the venue + Address string `json:"address"` + // FoursquareID is the foursquare identifier of the venue + // + // optional + FoursquareID string `json:"foursquare_id,omitempty"` + // FoursquareType is the foursquare type of the venue + // + // optional + FoursquareType string `json:"foursquare_type,omitempty"` + // GooglePlaceID is the Google Places identifier of the venue + // + // optional + GooglePlaceID string `json:"google_place_id,omitempty"` + // GooglePlaceType is the Google Places type of the venue + // + // optional + GooglePlaceType string `json:"google_place_type,omitempty"` +} + +// ProximityAlertTriggered represents a service message sent when a user in the +// chat triggers a proximity alert sent by another user. +type ProximityAlertTriggered struct { + // Traveler is the user that triggered the alert + Traveler User `json:"traveler"` + // Watcher is the user that set the alert + Watcher User `json:"watcher"` + // Distance is the distance between the users + Distance int `json:"distance"` +} + +// MessageAutoDeleteTimerChanged represents a service message about a change in +// auto-delete timer settings. +type MessageAutoDeleteTimerChanged struct { + // New auto-delete time for messages in the chat. + MessageAutoDeleteTime int `json:"message_auto_delete_time"` +} + +// VoiceChatStarted represents a service message about a voice chat started in +// the chat. +type VoiceChatStarted struct{} + +// VoiceChatEnded represents a service message about a voice chat ended in the +// chat. +type VoiceChatEnded struct { + // Voice chat duration; in seconds. + Duration int `json:"duration"` +} + +// VoiceChatParticipantsInvited represents a service message about new members +// invited to a voice chat. +type VoiceChatParticipantsInvited struct { + // New members that were invited to the voice chat. + // + // optional + Users []User `json:"users"` } // UserProfilePhotos contains a set of user profile photos. type UserProfilePhotos struct { - TotalCount int `json:"total_count"` - Photos [][]PhotoSize `json:"photos"` + // TotalCount total number of profile pictures the target user has + TotalCount int `json:"total_count"` + // Photos requested profile pictures (in up to 4 sizes each) + Photos [][]PhotoSize `json:"photos"` } // File contains information about a file to download from Telegram. type File struct { - FileID string `json:"file_id"` - FileSize int `json:"file_size"` // optional - FilePath string `json:"file_path"` // optional + // FileID identifier for this file, which can be used to download or reuse + // the file + FileID string `json:"file_id"` + // FileUniqueID is the unique identifier for this file, which is supposed to + // be the same over time and for different bots. Can't be used to download + // or reuse the file. + FileUniqueID string `json:"file_unique_id"` + // FileSize file size, if known + // + // optional + FileSize int `json:"file_size,omitempty"` + // FilePath file path + // + // optional + FilePath string `json:"file_path,omitempty"` } // Link returns a full path to the download URL for a File. // -// It requires the Bot Token to create the link. +// It requires the Bot token to create the link. func (f *File) Link(token string) string { return fmt.Sprintf(FileEndpoint, token, f.FilePath) } -// ReplyKeyboardMarkup allows the Bot to set a custom keyboard. +// ReplyKeyboardMarkup represents a custom keyboard with reply options. type ReplyKeyboardMarkup struct { - Keyboard [][]KeyboardButton `json:"keyboard"` - ResizeKeyboard bool `json:"resize_keyboard"` // optional - OneTimeKeyboard bool `json:"one_time_keyboard"` // optional - Selective bool `json:"selective"` // optional + // Keyboard is an array of button rows, each represented by an Array of KeyboardButton objects + Keyboard [][]KeyboardButton `json:"keyboard"` + // ResizeKeyboard requests clients to resize the keyboard vertically for optimal fit + // (e.g., make the keyboard smaller if there are just two rows of buttons). + // Defaults to false, in which case the custom keyboard + // is always of the same height as the app's standard keyboard. + // + // optional + ResizeKeyboard bool `json:"resize_keyboard,omitempty"` + // OneTimeKeyboard requests clients to hide the keyboard as soon as it's been used. + // The keyboard will still be available, but clients will automatically display + // the usual letter-keyboard in the chat – the user can press a special button + // in the input field to see the custom keyboard again. + // Defaults to false. + // + // optional + OneTimeKeyboard bool `json:"one_time_keyboard,omitempty"` + // Selective use this parameter if you want to show the keyboard to specific users only. + // Targets: + // 1) users that are @mentioned in the text of the Message object; + // 2) if the bot's message is a reply (has Message.ReplyToMessage not nil), sender of the original message. + // + // Example: A user requests to change the bot's language, + // bot replies to the request with a keyboard to select the new language. + // Other users in the group don't see the keyboard. + // + // optional + Selective bool `json:"selective,omitempty"` } -// KeyboardButton is a button within a custom keyboard. +// KeyboardButton represents one button of the reply keyboard. For simple text +// buttons String can be used instead of this object to specify text of the +// button. Optional fields request_contact, request_location, and request_poll +// are mutually exclusive. type KeyboardButton struct { - Text string `json:"text"` - RequestContact bool `json:"request_contact"` - RequestLocation bool `json:"request_location"` + // Text of the button. If none of the optional fields are used, + // it will be sent as a message when the button is pressed. + Text string `json:"text"` + // RequestContact if True, the user's phone number will be sent + // as a contact when the button is pressed. + // Available in private chats only. + // + // optional + RequestContact bool `json:"request_contact,omitempty"` + // RequestLocation if True, the user's current location will be sent when + // the button is pressed. + // Available in private chats only. + // + // optional + RequestLocation bool `json:"request_location,omitempty"` + // RequestPoll if True, the user will be asked to create a poll and send it + // to the bot when the button is pressed. Available in private chats only + // + // optional + RequestPoll *KeyboardButtonPollType `json:"request_poll,omitempty"` } -// ReplyKeyboardHide allows the Bot to hide a custom keyboard. -type ReplyKeyboardHide struct { - HideKeyboard bool `json:"hide_keyboard"` - Selective bool `json:"selective"` // optional +// KeyboardButtonPollType represents type of a poll, which is allowed to +// be created and sent when the corresponding button is pressed. +type KeyboardButtonPollType struct { + // Type is if quiz is passed, the user will be allowed to create only polls + // in the quiz mode. If regular is passed, only regular polls will be + // allowed. Otherwise, the user will be allowed to create a poll of any type. + Type string `json:"type"` } -// ReplyKeyboardRemove allows the Bot to hide a custom keyboard. +// ReplyKeyboardRemove Upon receiving a message with this object, Telegram +// clients will remove the current custom keyboard and display the default +// letter-keyboard. By default, custom keyboards are displayed until a new +// keyboard is sent by a bot. An exception is made for one-time keyboards +// that are hidden immediately after the user presses a button. type ReplyKeyboardRemove struct { + // RemoveKeyboard requests clients to remove the custom keyboard + // (user will not be able to summon this keyboard; + // if you want to hide the keyboard from sight but keep it accessible, + // use one_time_keyboard in ReplyKeyboardMarkup). RemoveKeyboard bool `json:"remove_keyboard"` - Selective bool `json:"selective"` + // Selective use this parameter if you want to remove the keyboard for specific users only. + // Targets: + // 1) users that are @mentioned in the text of the Message object; + // 2) if the bot's message is a reply (has Message.ReplyToMessage not nil), sender of the original message. + // + // Example: A user votes in a poll, bot returns confirmation message + // in reply to the vote and removes the keyboard for that user, + // while still showing the keyboard with poll options to users who haven't voted yet. + // + // optional + Selective bool `json:"selective,omitempty"` } -// InlineKeyboardMarkup is a custom keyboard presented for an inline bot. +// InlineKeyboardMarkup represents an inline keyboard that appears right next to +// the message it belongs to. type InlineKeyboardMarkup struct { + // InlineKeyboard array of button rows, each represented by an Array of + // InlineKeyboardButton objects InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard"` } -// InlineKeyboardButton is a button within a custom keyboard for -// inline query responses. +// InlineKeyboardButton represents one button of an inline keyboard. You must +// use exactly one of the optional fields. // // Note that some values are references as even an empty string // will change behavior. // // CallbackGame, if set, MUST be first button in first row. type InlineKeyboardButton struct { - Text string `json:"text"` - URL *string `json:"url,omitempty"` // optional - LoginURL *LoginURL `json:"login_url,omitempty"` // optional - CallbackData *string `json:"callback_data,omitempty"` // optional - SwitchInlineQuery *string `json:"switch_inline_query,omitempty"` // optional - SwitchInlineQueryCurrentChat *string `json:"switch_inline_query_current_chat,omitempty"` // optional - CallbackGame *CallbackGame `json:"callback_game,omitempty"` // optional - Pay bool `json:"pay,omitempty"` // optional + // Text label text on the button + Text string `json:"text"` + // URL HTTP or tg:// url to be opened when button is pressed. + // + // optional + URL *string `json:"url,omitempty"` + // LoginURL is an HTTP URL used to automatically authorize the user. Can be + // used as a replacement for the Telegram Login Widget + // + // optional + LoginURL *LoginURL `json:"login_url,omitempty"` + // CallbackData data to be sent in a callback query to the bot when button is pressed, 1-64 bytes. + // + // optional + CallbackData *string `json:"callback_data,omitempty"` + // SwitchInlineQuery if set, pressing the button will prompt the user to select one of their chats, + // open that chat and insert the bot's username and the specified inline query in the input field. + // Can be empty, in which case just the bot's username will be inserted. + // + // This offers an easy way for users to start using your bot + // in inline mode when they are currently in a private chat with it. + // Especially useful when combined with switch_pm… actions – in this case + // the user will be automatically returned to the chat they switched from, + // skipping the chat selection screen. + // + // optional + SwitchInlineQuery *string `json:"switch_inline_query,omitempty"` + // SwitchInlineQueryCurrentChat if set, pressing the button will insert the bot's username + // and the specified inline query in the current chat's input field. + // Can be empty, in which case only the bot's username will be inserted. + // + // This offers a quick way for the user to open your bot in inline mode + // in the same chat – good for selecting something from multiple options. + // + // optional + SwitchInlineQueryCurrentChat *string `json:"switch_inline_query_current_chat,omitempty"` + // CallbackGame description of the game that will be launched when the user presses the button. + // + // optional + CallbackGame *CallbackGame `json:"callback_game,omitempty"` + // Pay specify True, to send a Pay button. + // + // NOTE: This type of button must always be the first button in the first row. + // + // optional + Pay bool `json:"pay,omitempty"` } -// LoginURL is the parameters for the login inline keyboard button type. +// LoginURL represents a parameter of the inline keyboard button used to +// automatically authorize a user. Serves as a great replacement for the +// Telegram Login Widget when the user is coming from Telegram. All the user +// needs to do is tap/click a button and confirm that they want to log in. type LoginURL struct { - URL string `json:"url"` - ForwardText string `json:"forward_text,omitempty"` - BotUsername string `json:"bot_username,omitempty"` - RequestWriteAccess bool `json:"request_write_access,omitempty"` + // URL is an HTTP URL to be opened with user authorization data added to the + // query string when the button is pressed. If the user refuses to provide + // authorization data, the original URL without information about the user + // will be opened. The data added is the same as described in Receiving + // authorization data. + // + // NOTE: You must always check the hash of the received data to verify the + // authentication and the integrity of the data as described in Checking + // authorization. + URL string `json:"url"` + // ForwardText is the new text of the button in forwarded messages + // + // optional + ForwardText string `json:"forward_text,omitempty"` + // BotUsername is the username of a bot, which will be used for user + // authorization. See Setting up a bot for more details. If not specified, + // the current bot's username will be assumed. The url's domain must be the + // same as the domain linked with the bot. See Linking your domain to the + // bot for more details. + // + // optional + BotUsername string `json:"bot_username,omitempty"` + // RequestWriteAccess if true requests permission for your bot to send + // messages to the user + // + // optional + RequestWriteAccess bool `json:"request_write_access,omitempty"` } -// CallbackQuery is data sent when a keyboard button with callback data -// is clicked. +// CallbackQuery represents an incoming callback query from a callback button in +// an inline keyboard. If the button that originated the query was attached to a +// message sent by the bot, the field message will be present. If the button was +// attached to a message sent via the bot (in inline mode), the field +// inline_message_id will be present. Exactly one of the fields data or +// game_short_name will be present. type CallbackQuery struct { - ID string `json:"id"` - From *User `json:"from"` - Message *Message `json:"message"` // optional - InlineMessageID string `json:"inline_message_id"` // optional - ChatInstance string `json:"chat_instance"` - Data string `json:"data"` // optional - GameShortName string `json:"game_short_name"` // optional + // ID unique identifier for this query + ID string `json:"id"` + // From sender + From *User `json:"from"` + // Message with the callback button that originated the query. + // Note that message content and message date will not be available if the + // message is too old. + // + // optional + Message *Message `json:"message,omitempty"` + // InlineMessageID identifier of the message sent via the bot in inline + // mode, that originated the query. + // + // optional + InlineMessageID string `json:"inline_message_id,omitempty"` + // ChatInstance global identifier, uniquely corresponding to the chat to + // which the message with the callback button was sent. Useful for high + // scores in games. + ChatInstance string `json:"chat_instance"` + // Data associated with the callback button. Be aware that + // a bad client can send arbitrary data in this field. + // + // optional + Data string `json:"data,omitempty"` + // GameShortName short name of a Game to be returned, serves as the unique identifier for the game. + // + // optional + GameShortName string `json:"game_short_name,omitempty"` } -// ForceReply allows the Bot to have users directly reply to it without -// additional interaction. +// ForceReply when receiving a message with this object, Telegram clients will +// display a reply interface to the user (act as if the user has selected the +// bot's message and tapped 'Reply'). This can be extremely useful if you want +// to create user-friendly step-by-step interfaces without having to sacrifice +// privacy mode. type ForceReply struct { + // ForceReply shows reply interface to the user, + // as if they manually selected the bot's message and tapped 'Reply'. ForceReply bool `json:"force_reply"` - Selective bool `json:"selective"` // optional + // Selective use this parameter if you want to force reply from specific users only. + // Targets: + // 1) users that are @mentioned in the text of the Message object; + // 2) if the bot's message is a reply (has Message.ReplyToMessage not nil), sender of the original message. + // + // optional + Selective bool `json:"selective,omitempty"` } -// ChatMember is information about a member in a chat. +// ChatPhoto represents a chat photo. +type ChatPhoto struct { + // SmallFileID is a file identifier of small (160x160) chat photo. + // This file_id can be used only for photo download and + // only for as long as the photo is not changed. + SmallFileID string `json:"small_file_id"` + // SmallFileUniqueID is a unique file identifier of small (160x160) chat + // photo, which is supposed to be the same over time and for different bots. + // Can't be used to download or reuse the file. + SmallFileUniqueID string `json:"small_file_unique_id"` + // BigFileID is a file identifier of big (640x640) chat photo. + // This file_id can be used only for photo download and + // only for as long as the photo is not changed. + BigFileID string `json:"big_file_id"` + // BigFileUniqueID is a file identifier of big (640x640) chat photo, which + // is supposed to be the same over time and for different bots. Can't be + // used to download or reuse the file. + BigFileUniqueID string `json:"big_file_unique_id"` +} + +// ChatInviteLink represents an invite link for a chat. +type ChatInviteLink struct { + // InviteLink is the invite link. If the link was created by another chat + // administrator, then the second part of the link will be replaced with “…”. + InviteLink string `json:"invite_link"` + // Creator of the link. + Creator User `json:"creator"` + // IsPrimary is true, if the link is primary. + IsPrimary bool `json:"is_primary"` + // IsRevoked is true, if the link is revoked. + IsRevoked bool `json:"is_revoked"` + // ExpireDate is the point in time (Unix timestamp) when the link will + // expire or has been expired. + // + // optional + ExpireDate int `json:"expire_date"` + // MemberLimit is the maximum number of users that can be members of the + // chat simultaneously after joining the chat via this invite link; 1-99999. + // + // optional + MemberLimit int `json:"member_limit"` +} + +// ChatMember contains information about one member of a chat. type ChatMember struct { - User *User `json:"user"` - Status string `json:"status"` - UntilDate int64 `json:"until_date,omitempty"` // optional - CanBeEdited bool `json:"can_be_edited,omitempty"` // optional - CanChangeInfo bool `json:"can_change_info,omitempty"` // optional - CanPostMessages bool `json:"can_post_messages,omitempty"` // optional - CanEditMessages bool `json:"can_edit_messages,omitempty"` // optional - CanDeleteMessages bool `json:"can_delete_messages,omitempty"` // optional - CanInviteUsers bool `json:"can_invite_users,omitempty"` // optional - CanRestrictMembers bool `json:"can_restrict_members,omitempty"` // optional - CanPinMessages bool `json:"can_pin_messages,omitempty"` // optional - CanPromoteMembers bool `json:"can_promote_members,omitempty"` // optional - IsChatMember bool `json:"is_member"` // optional - CanSendMessages bool `json:"can_send_messages,omitempty"` // optional - CanSendMediaMessages bool `json:"can_send_media_messages,omitempty"` // optional - CanSendOtherMessages bool `json:"can_send_other_messages,omitempty"` // optional - CanAddWebPagePreviews bool `json:"can_add_web_page_previews,omitempty"` // optional + // User information about the user + User *User `json:"user"` + // Status the member's status in the chat. + // Can be + // “creator”, + // “administrator”, + // “member”, + // “restricted”, + // “left” or + // “kicked” + Status string `json:"status"` + // CustomTitle owner and administrators only. Custom title for this user + // + // optional + CustomTitle string `json:"custom_title,omitempty"` + // IsAnonymous owner and administrators only. True, if the user's presence + // in the chat is hidden + // + // optional + IsAnonymous bool `json:"is_anonymous"` + // UntilDate restricted and kicked only. + // Date when restrictions will be lifted for this user; + // unix time. + // + // optional + UntilDate int64 `json:"until_date,omitempty"` + // CanBeEdited administrators only. + // True, if the bot is allowed to edit administrator privileges of that user. + // + // optional + CanBeEdited bool `json:"can_be_edited,omitempty"` + // CanManageChat administrators only. + // True, if the administrator can access the chat event log, chat + // statistics, message statistics in channels, see channel members, see + // anonymous administrators in supergoups and ignore slow mode. Implied by + // any other administrator privilege. + // + // optional + CanManageChat bool `json:"can_manage_chat"` + // CanPostMessages administrators only. + // True, if the administrator can post in the channel; + // channels only. + // + // optional + CanPostMessages bool `json:"can_post_messages,omitempty"` + // CanEditMessages administrators only. + // True, if the administrator can edit messages of other users and can pin messages; + // channels only. + // + // optional + CanEditMessages bool `json:"can_edit_messages,omitempty"` + // CanDeleteMessages administrators only. + // True, if the administrator can delete messages of other users. + // + // optional + CanDeleteMessages bool `json:"can_delete_messages,omitempty"` + // CanManageVoiceChats administrators only. + // True, if the administrator can manage voice chats. + // + // optional + CanManageVoiceChats bool `json:"can_manage_voice_chats"` + // CanRestrictMembers administrators only. + // True, if the administrator can restrict, ban or unban chat members. + // + // optional + CanRestrictMembers bool `json:"can_restrict_members,omitempty"` + // CanPromoteMembers administrators only. + // True, if the administrator can add new administrators + // with a subset of their own privileges or demote administrators that he has promoted, + // directly or indirectly (promoted by administrators that were appointed by the user). + // + // optional + CanPromoteMembers bool `json:"can_promote_members,omitempty"` + // CanChangeInfo administrators and restricted only. + // True, if the user is allowed to change the chat title, photo and other settings. + // + // optional + CanChangeInfo bool `json:"can_change_info,omitempty"` + // CanInviteUsers administrators and restricted only. + // True, if the user is allowed to invite new users to the chat. + // + // optional + CanInviteUsers bool `json:"can_invite_users,omitempty"` + // CanPinMessages administrators and restricted only. + // True, if the user is allowed to pin messages; groups and supergroups only + // + // optional + CanPinMessages bool `json:"can_pin_messages,omitempty"` + // IsMember is true, if the user is a member of the chat at the moment of + // the request + IsMember bool `json:"is_member"` + // CanSendMessages + // + // optional + CanSendMessages bool `json:"can_send_messages,omitempty"` + // CanSendMediaMessages restricted only. + // True, if the user is allowed to send text messages, contacts, locations and venues + // + // optional + CanSendMediaMessages bool `json:"can_send_media_messages,omitempty"` + // CanSendPolls restricted only. + // True, if the user is allowed to send polls + // + // optional + CanSendPolls bool `json:"can_send_polls,omitempty"` + // CanSendOtherMessages restricted only. + // True, if the user is allowed to send audios, documents, + // photos, videos, video notes and voice notes. + // + // optional + CanSendOtherMessages bool `json:"can_send_other_messages,omitempty"` + // CanAddWebPagePreviews restricted only. + // True, if the user is allowed to add web page previews to their messages. + // + // optional + CanAddWebPagePreviews bool `json:"can_add_web_page_previews,omitempty"` } // IsCreator returns if the ChatMember was the creator of the chat. @@ -527,39 +1536,337 @@ func (chat ChatMember) IsCreator() bool { return chat.Status == "creator" } // IsAdministrator returns if the ChatMember is a chat administrator. func (chat ChatMember) IsAdministrator() bool { return chat.Status == "administrator" } -// IsMember returns if the ChatMember is a current member of the chat. -func (chat ChatMember) IsMember() bool { return chat.Status == "member" } - // HasLeft returns if the ChatMember left the chat. func (chat ChatMember) HasLeft() bool { return chat.Status == "left" } // WasKicked returns if the ChatMember was kicked from the chat. func (chat ChatMember) WasKicked() bool { return chat.Status == "kicked" } -// Game is a game within Telegram. -type Game struct { - Title string `json:"title"` - Description string `json:"description"` - Photo []PhotoSize `json:"photo"` - Text string `json:"text"` - TextEntities []MessageEntity `json:"text_entities"` - Animation Animation `json:"animation"` +// ChatMemberUpdated represents changes in the status of a chat member. +type ChatMemberUpdated struct { + // Chat the user belongs to. + Chat Chat `json:"chat"` + // From is the performer of the action, which resulted in the change. + From User `json:"from"` + // Date the change was done in Unix time. + Date int `json:"date"` + // Previous information about the chat member. + OldChatMember ChatMember `json:"old_chat_member"` + // New information about the chat member. + NewChatMember ChatMember `json:"new_chat_member"` + // InviteLink is the link which was used by the user to join the chat; + // for joining by invite link events only. + // + // optional + InviteLink *ChatInviteLink `json:"invite_link"` } -// Animation is a GIF animation demonstrating the game. -type Animation struct { - FileID string `json:"file_id"` - Thumb PhotoSize `json:"thumb"` - FileName string `json:"file_name"` - MimeType string `json:"mime_type"` - FileSize int `json:"file_size"` +// ChatPermissions describes actions that a non-administrator user is +// allowed to take in a chat. All fields are optional. +type ChatPermissions struct { + // CanSendMessages is true, if the user is allowed to send text messages, + // contacts, locations and venues + // + // optional + CanSendMessages bool `json:"can_send_messages,omitempty"` + // CanSendMediaMessages is true, if the user is allowed to send audios, + // documents, photos, videos, video notes and voice notes, implies + // can_send_messages + // + // optional + CanSendMediaMessages bool `json:"can_send_media_messages,omitempty"` + // CanSendPolls is true, if the user is allowed to send polls, implies + // can_send_messages + // + // optional + CanSendPolls bool `json:"can_send_polls,omitempty"` + // CanSendOtherMessages is true, if the user is allowed to send animations, + // games, stickers and use inline bots, implies can_send_media_messages + // + // optional + CanSendOtherMessages bool `json:"can_send_other_messages,omitempty"` + // CanAddWebPagePreviews is true, if the user is allowed to add web page + // previews to their messages, implies can_send_media_messages + // + // optional + CanAddWebPagePreviews bool `json:"can_add_web_page_previews,omitempty"` + // CanChangeInfo is true, if the user is allowed to change the chat title, + // photo and other settings. Ignored in public supergroups + // + // optional + CanChangeInfo bool `json:"can_change_info,omitempty"` + // CanInviteUsers is true, if the user is allowed to invite new users to the + // chat + // + // optional + CanInviteUsers bool `json:"can_invite_users,omitempty"` + // CanPinMessages is true, if the user is allowed to pin messages. Ignored + // in public supergroups + // + // optional + CanPinMessages bool `json:"can_pin_messages,omitempty"` +} + +// ChatLocation represents a location to which a chat is connected. +type ChatLocation struct { + // Location is the location to which the supergroup is connected. Can't be a + // live location. + Location Location `json:"location"` + // Address is the location address; 1-64 characters, as defined by the chat + // owner + Address string `json:"address"` +} + +// BotCommand represents a bot command. +type BotCommand struct { + // Command text of the command, 1-32 characters. + // Can contain only lowercase English letters, digits and underscores. + Command string `json:"command"` + // Description of the command, 3-256 characters. + Description string `json:"description"` +} + +// ResponseParameters are various errors that can be returned in APIResponse. +type ResponseParameters struct { + // The group has been migrated to a supergroup with the specified identifier. + // + // optional + MigrateToChatID int64 `json:"migrate_to_chat_id,omitempty"` + // In case of exceeding flood control, the number of seconds left to wait + // before the request can be repeated. + // + // optional + RetryAfter int `json:"retry_after,omitempty"` +} + +// BaseInputMedia is a base type for the InputMedia types. +type BaseInputMedia struct { + // Type of the result. + Type string `json:"type"` + // Media file to send. Pass a file_id to send a file + // that exists on the Telegram servers (recommended), + // pass an HTTP URL for Telegram to get a file from the Internet, + // or pass “attach://” to upload a new one + // using multipart/form-data under name. + Media interface{} `json:"media"` + // thumb intentionally missing as it is not currently compatible + + // Caption of the video to be sent, 0-1024 characters after entities parsing. + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities"` +} + +// InputMediaPhoto is a photo to send as part of a media group. +type InputMediaPhoto struct { + BaseInputMedia +} + +// InputMediaVideo is a video to send as part of a media group. +type InputMediaVideo struct { + BaseInputMedia + // Thumbnail of the file sent; can be ignored if thumbnail generation for + // the file is supported server-side. + // + // optional + Thumb interface{} `json:"thumb"` + // Width video width + // + // optional + Width int `json:"width,omitempty"` + // Height video height + // + // optional + Height int `json:"height,omitempty"` + // Duration video duration + // + // optional + Duration int `json:"duration,omitempty"` + // SupportsStreaming pass True, if the uploaded video is suitable for streaming. + // + // optional + SupportsStreaming bool `json:"supports_streaming,omitempty"` +} + +// InputMediaAnimation is an animation to send as part of a media group. +type InputMediaAnimation struct { + BaseInputMedia + // Thumbnail of the file sent; can be ignored if thumbnail generation for + // the file is supported server-side. + // + // optional + Thumb interface{} `json:"thumb"` + // Width video width + // + // optional + Width int `json:"width,omitempty"` + // Height video height + // + // optional + Height int `json:"height,omitempty"` + // Duration video duration + // + // optional + Duration int `json:"duration,omitempty"` +} + +// InputMediaAudio is a audio to send as part of a media group. +type InputMediaAudio struct { + BaseInputMedia + // Thumbnail of the file sent; can be ignored if thumbnail generation for + // the file is supported server-side. + // + // optional + Thumb interface{} `json:"thumb"` + // Duration of the audio in seconds + // + // optional + Duration int `json:"duration,omitempty"` + // Performer of the audio + // + // optional + Performer string `json:"performer,omitempty"` + // Title of the audio + // + // optional + Title string `json:"title,omitempty"` +} + +// InputMediaDocument is a general file to send as part of a media group. +type InputMediaDocument struct { + BaseInputMedia + // Thumbnail of the file sent; can be ignored if thumbnail generation for + // the file is supported server-side. + // + // optional + Thumb interface{} `json:"thumb"` + // DisableContentTypeDetection disables automatic server-side content type + // detection for files uploaded using multipart/form-data. Always true, if + // the document is sent as part of an album + // + // optional + DisableContentTypeDetection bool `json:"disable_content_type_detection,omitempty"` +} + +// Sticker represents a sticker. +type Sticker struct { + // FileID is an identifier for this file, which can be used to download or + // reuse the file + FileID string `json:"file_id"` + // FileUniqueID is an unique identifier for this file, + // which is supposed to be the same over time and for different bots. + // Can't be used to download or reuse the file. + FileUniqueID string `json:"file_unique_id"` + // Width sticker width + Width int `json:"width"` + // Height sticker height + Height int `json:"height"` + // IsAnimated true, if the sticker is animated + // + // optional + IsAnimated bool `json:"is_animated,omitempty"` + // Thumbnail sticker thumbnail in the .WEBP or .JPG format + // + // optional + Thumbnail *PhotoSize `json:"thumb,omitempty"` + // Emoji associated with the sticker + // + // optional + Emoji string `json:"emoji,omitempty"` + // SetName of the sticker set to which the sticker belongs + // + // optional + SetName string `json:"set_name,omitempty"` + // MaskPosition is for mask stickers, the position where the mask should be + // placed + // + // optional + MaskPosition *MaskPosition `json:"mask_position,omitempty"` + // FileSize + // + // optional + FileSize int `json:"file_size,omitempty"` +} + +// StickerSet represents a sticker set. +type StickerSet struct { + // Name sticker set name + Name string `json:"name"` + // Title sticker set title + Title string `json:"title"` + // IsAnimated true, if the sticker set contains animated stickers + IsAnimated bool `json:"is_animated"` + // ContainsMasks true, if the sticker set contains masks + ContainsMasks bool `json:"contains_masks"` + // Stickers list of all set stickers + Stickers []Sticker `json:"stickers"` + // Thumb is the sticker set thumbnail in the .WEBP or .TGS format + Thumbnail *PhotoSize `json:"thumb"` +} + +// MaskPosition describes the position on faces where a mask should be placed +// by default. +type MaskPosition struct { + // The part of the face relative to which the mask should be placed. + // One of “forehead”, “eyes”, “mouth”, or “chin”. + Point string `json:"point"` + // Shift by X-axis measured in widths of the mask scaled to the face size, + // from left to right. For example, choosing -1.0 will place mask just to + // the left of the default mask position. + XShift float64 `json:"x_shift"` + // Shift by Y-axis measured in heights of the mask scaled to the face size, + // from top to bottom. For example, 1.0 will place the mask just below the + // default mask position. + YShift float64 `json:"y_shift"` + // Mask scaling coefficient. For example, 2.0 means double size. + Scale float64 `json:"scale"` +} + +// Game represents a game. Use BotFather to create and edit games, their short +// names will act as unique identifiers. +type Game struct { + // Title of the game + Title string `json:"title"` + // Description of the game + Description string `json:"description"` + // Photo that will be displayed in the game message in chats. + Photo []PhotoSize `json:"photo"` + // Text a brief description of the game or high scores included in the game message. + // Can be automatically edited to include current high scores for the game + // when the bot calls setGameScore, or manually edited using editMessageText. 0-4096 characters. + // + // optional + Text string `json:"text,omitempty"` + // TextEntities special entities that appear in text, such as usernames, URLs, bot commands, etc. + // + // optional + TextEntities []MessageEntity `json:"text_entities,omitempty"` + // Animation animation that will be displayed in the game message in chats. + // Upload via BotFather (https://t.me/botfather). + // + // optional + Animation Animation `json:"animation,omitempty"` } // GameHighScore is a user's score and position on the leaderboard. type GameHighScore struct { - Position int `json:"position"` - User User `json:"user"` - Score int `json:"score"` + // Position in high score table for the game + Position int `json:"position"` + // User user + User User `json:"user"` + // Score score + Score int `json:"score"` } // CallbackGame is for starting a game in an inline keyboard button. @@ -567,11 +1874,36 @@ type CallbackGame struct{} // WebhookInfo is information about a currently set webhook. type WebhookInfo struct { - URL string `json:"url"` - HasCustomCertificate bool `json:"has_custom_certificate"` - PendingUpdateCount int `json:"pending_update_count"` - LastErrorDate int `json:"last_error_date"` // optional - LastErrorMessage string `json:"last_error_message"` // optional + // URL webhook URL, may be empty if webhook is not set up. + URL string `json:"url"` + // HasCustomCertificate true, if a custom certificate was provided for webhook certificate checks. + HasCustomCertificate bool `json:"has_custom_certificate"` + // PendingUpdateCount number of updates awaiting delivery. + PendingUpdateCount int `json:"pending_update_count"` + // IPAddress is the currently used webhook IP address + // + // optional + IPAddress string `json:"ip_address,omitempty"` + // LastErrorDate unix time for the most recent error + // that happened when trying to deliver an update via webhook. + // + // optional + LastErrorDate int `json:"last_error_date,omitempty"` + // LastErrorMessage error message in human-readable format for the most recent error + // that happened when trying to deliver an update via webhook. + // + // optional + LastErrorMessage string `json:"last_error_message,omitempty"` + // MaxConnections maximum allowed number of simultaneous + // HTTPS connections to the webhook for update delivery. + // + // optional + MaxConnections int `json:"max_connections,omitempty"` + // AllowedUpdates is a list of update types the bot is subscribed to. + // Defaults to all update types + // + // optional + AllowedUpdates []string `json:"allowed_updates,omitempty"` } // IsSet returns true if a webhook is currently set. @@ -581,145 +1913,386 @@ func (info WebhookInfo) IsSet() bool { // InlineQuery is a Query from Telegram for an inline request. type InlineQuery struct { - ID string `json:"id"` - From *User `json:"from"` - Location *Location `json:"location"` // optional - Query string `json:"query"` - Offset string `json:"offset"` + // ID unique identifier for this query + ID string `json:"id"` + // From sender + From *User `json:"from"` + // Location sender location, only for bots that request user location. + // + // optional + Location *Location `json:"location,omitempty"` + // Query text of the query (up to 256 characters). + Query string `json:"query"` + // Offset of the results to be returned, can be controlled by the bot. + Offset string `json:"offset"` } -// InlineQueryResultArticle is an inline query response article. +// InlineQueryResultCachedAudio is an inline query response with cached audio. +type InlineQueryResultCachedAudio struct { + // Type of the result, must be audio + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // AudioID a valid file identifier for the audio file + AudioID string `json:"audio_file_id"` + // Caption 0-1024 characters after entities parsing + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the audio + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultCachedDocument is an inline query response with cached document. +type InlineQueryResultCachedDocument struct { + // Type of the result, must be document + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // DocumentID a valid file identifier for the file + DocumentID string `json:"document_file_id"` + // Title for the result + // + // optional + Title string `json:"title,omitempty"` + // Caption of the document to be sent, 0-1024 characters after entities parsing + // + // optional + Caption string `json:"caption,omitempty"` + // Description short description of the result + // + // optional + Description string `json:"description,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // // See formatting options for more details + // // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the file + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultCachedGIF is an inline query response with cached gif. +type InlineQueryResultCachedGIF struct { + // Type of the result, must be gif. + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes. + ID string `json:"id"` + // GifID a valid file identifier for the GIF file. + GIFID string `json:"gif_file_id"` + // Title for the result + // + // optional + Title string `json:"title,omitempty"` + // Caption of the GIF file to be sent, 0-1024 characters after entities parsing. + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities"` + // ReplyMarkup inline keyboard attached to the message. + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the GIF animation. + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultCachedMPEG4GIF is an inline query response with cached +// H.264/MPEG-4 AVC video without sound gif. +type InlineQueryResultCachedMPEG4GIF struct { + // Type of the result, must be mpeg4_gif + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // MPEG4FileID a valid file identifier for the MP4 file + MPEG4FileID string `json:"mpeg4_file_id"` + // Title for the result + // + // optional + Title string `json:"title,omitempty"` + // Caption of the MPEG-4 file to be sent, 0-1024 characters after entities parsing. + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities"` + // ReplyMarkup inline keyboard attached to the message. + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the video animation. + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultCachedPhoto is an inline query response with cached photo. +type InlineQueryResultCachedPhoto struct { + // Type of the result, must be photo. + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes. + ID string `json:"id"` + // PhotoID a valid file identifier of the photo. + PhotoID string `json:"photo_file_id"` + // Title for the result. + // + // optional + Title string `json:"title,omitempty"` + // Description short description of the result. + // + // optional + Description string `json:"description,omitempty"` + // Caption of the photo to be sent, 0-1024 characters after entities parsing. + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the photo caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities"` + // ReplyMarkup inline keyboard attached to the message. + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the photo. + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultCachedSticker is an inline query response with cached sticker. +type InlineQueryResultCachedSticker struct { + // Type of the result, must be sticker + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // StickerID a valid file identifier of the sticker + StickerID string `json:"sticker_file_id"` + // Title is a title + Title string `json:"title"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the sticker + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultCachedVideo is an inline query response with cached video. +type InlineQueryResultCachedVideo struct { + // Type of the result, must be video + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // VideoID a valid file identifier for the video file + VideoID string `json:"video_file_id"` + // Title for the result + Title string `json:"title"` + // Description short description of the result + // + // optional + Description string `json:"description,omitempty"` + // Caption of the video to be sent, 0-1024 characters after entities parsing + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the video + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultCachedVoice is an inline query response with cached voice. +type InlineQueryResultCachedVoice struct { + // Type of the result, must be voice + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // VoiceID a valid file identifier for the voice message + VoiceID string `json:"voice_file_id"` + // Title voice message title + Title string `json:"title"` + // Caption 0-1024 characters after entities parsing + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the voice message + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultArticle represents a link to an article or web page. type InlineQueryResultArticle struct { - Type string `json:"type"` // required - ID string `json:"id"` // required - Title string `json:"title"` // required - InputMessageContent interface{} `json:"input_message_content,omitempty"` // required - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - URL string `json:"url"` - HideURL bool `json:"hide_url"` - Description string `json:"description"` - ThumbURL string `json:"thumb_url"` - ThumbWidth int `json:"thumb_width"` - ThumbHeight int `json:"thumb_height"` -} - -// InlineQueryResultPhoto is an inline query response photo. -type InlineQueryResultPhoto struct { - Type string `json:"type"` // required - ID string `json:"id"` // required - URL string `json:"photo_url"` // required - MimeType string `json:"mime_type"` - Width int `json:"photo_width"` - Height int `json:"photo_height"` - ThumbURL string `json:"thumb_url"` - Title string `json:"title"` - Description string `json:"description"` - Caption string `json:"caption"` - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - InputMessageContent interface{} `json:"input_message_content,omitempty"` -} - -// InlineQueryResultGIF is an inline query response GIF. -type InlineQueryResultGIF struct { - Type string `json:"type"` // required - ID string `json:"id"` // required - URL string `json:"gif_url"` // required - Width int `json:"gif_width"` - Height int `json:"gif_height"` - Duration int `json:"gif_duration"` - ThumbURL string `json:"thumb_url"` - Title string `json:"title"` - Caption string `json:"caption"` - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - InputMessageContent interface{} `json:"input_message_content,omitempty"` -} - -// InlineQueryResultMPEG4GIF is an inline query response MPEG4 GIF. -type InlineQueryResultMPEG4GIF struct { - Type string `json:"type"` // required - ID string `json:"id"` // required - URL string `json:"mpeg4_url"` // required - Width int `json:"mpeg4_width"` - Height int `json:"mpeg4_height"` - Duration int `json:"mpeg4_duration"` - ThumbURL string `json:"thumb_url"` - Title string `json:"title"` - Caption string `json:"caption"` - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - InputMessageContent interface{} `json:"input_message_content,omitempty"` -} - -// InlineQueryResultVideo is an inline query response video. -type InlineQueryResultVideo struct { - Type string `json:"type"` // required - ID string `json:"id"` // required - URL string `json:"video_url"` // required - MimeType string `json:"mime_type"` // required - ThumbURL string `json:"thumb_url"` - Title string `json:"title"` - Caption string `json:"caption"` - Width int `json:"video_width"` - Height int `json:"video_height"` - Duration int `json:"video_duration"` - Description string `json:"description"` - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - InputMessageContent interface{} `json:"input_message_content,omitempty"` + // Type of the result, must be article. + Type string `json:"type"` + // ID unique identifier for this result, 1-64 Bytes. + ID string `json:"id"` + // Title of the result + Title string `json:"title"` + // InputMessageContent content of the message to be sent. + InputMessageContent interface{} `json:"input_message_content,omitempty"` + // ReplyMarkup Inline keyboard attached to the message. + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // URL of the result. + // + // optional + URL string `json:"url,omitempty"` + // HideURL pass True, if you don't want the URL to be shown in the message. + // + // optional + HideURL bool `json:"hide_url,omitempty"` + // Description short description of the result. + // + // optional + Description string `json:"description,omitempty"` + // ThumbURL url of the thumbnail for the result + // + // optional + ThumbURL string `json:"thumb_url,omitempty"` + // ThumbWidth thumbnail width + // + // optional + ThumbWidth int `json:"thumb_width,omitempty"` + // ThumbHeight thumbnail height + // + // optional + ThumbHeight int `json:"thumb_height,omitempty"` } // InlineQueryResultAudio is an inline query response audio. type InlineQueryResultAudio struct { - Type string `json:"type"` // required - ID string `json:"id"` // required - URL string `json:"audio_url"` // required - Title string `json:"title"` // required - Caption string `json:"caption"` - Performer string `json:"performer"` - Duration int `json:"audio_duration"` - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - InputMessageContent interface{} `json:"input_message_content,omitempty"` -} - -// InlineQueryResultVoice is an inline query response voice. -type InlineQueryResultVoice struct { - Type string `json:"type"` // required - ID string `json:"id"` // required - URL string `json:"voice_url"` // required - Title string `json:"title"` // required - Caption string `json:"caption"` - Duration int `json:"voice_duration"` - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - InputMessageContent interface{} `json:"input_message_content,omitempty"` -} - -// InlineQueryResultDocument is an inline query response document. -type InlineQueryResultDocument struct { - Type string `json:"type"` // required - ID string `json:"id"` // required - Title string `json:"title"` // required - Caption string `json:"caption"` - URL string `json:"document_url"` // required - MimeType string `json:"mime_type"` // required - Description string `json:"description"` - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - InputMessageContent interface{} `json:"input_message_content,omitempty"` - ThumbURL string `json:"thumb_url"` - ThumbWidth int `json:"thumb_width"` - ThumbHeight int `json:"thumb_height"` -} - -// InlineQueryResultLocation is an inline query response location. -type InlineQueryResultLocation struct { - Type string `json:"type"` // required - ID string `json:"id"` // required - Latitude float64 `json:"latitude"` // required - Longitude float64 `json:"longitude"` // required - LivePeriod int `json:"live_period"` // optional - Title string `json:"title"` // required - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` - InputMessageContent interface{} `json:"input_message_content,omitempty"` - ThumbURL string `json:"thumb_url"` - ThumbWidth int `json:"thumb_width"` - ThumbHeight int `json:"thumb_height"` + // Type of the result, must be audio + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // URL a valid url for the audio file + URL string `json:"audio_url"` + // Title is a title + Title string `json:"title"` + // Caption 0-1024 characters after entities parsing + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities"` + // Performer is a performer + // + // optional + Performer string `json:"performer,omitempty"` + // Duration audio duration in seconds + // + // optional + Duration int `json:"audio_duration,omitempty"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the audio + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` } // InlineQueryResultContact is an inline query response contact. @@ -739,183 +2312,674 @@ type InlineQueryResultContact struct { // InlineQueryResultGame is an inline query response game. type InlineQueryResultGame struct { - Type string `json:"type"` - ID string `json:"id"` - GameShortName string `json:"game_short_name"` - ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // Type of the result, must be game + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // GameShortName short name of the game + GameShortName string `json:"game_short_name"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` +} + +// InlineQueryResultDocument is an inline query response document. +type InlineQueryResultDocument struct { + // Type of the result, must be document + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // Title for the result + Title string `json:"title"` + // Caption of the document to be sent, 0-1024 characters after entities parsing + // + // optional + Caption string `json:"caption,omitempty"` + // URL a valid url for the file + URL string `json:"document_url"` + // MimeType of the content of the file, either “application/pdf” or “application/zip” + MimeType string `json:"mime_type"` + // Description short description of the result + // + // optional + Description string `json:"description,omitempty"` + // ReplyMarkup nline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the file + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` + // ThumbURL url of the thumbnail (jpeg only) for the file + // + // optional + ThumbURL string `json:"thumb_url,omitempty"` + // ThumbWidth thumbnail width + // + // optional + ThumbWidth int `json:"thumb_width,omitempty"` + // ThumbHeight thumbnail height + // + // optional + ThumbHeight int `json:"thumb_height,omitempty"` +} + +// InlineQueryResultGIF is an inline query response GIF. +type InlineQueryResultGIF struct { + // Type of the result, must be gif. + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes. + ID string `json:"id"` + // URL a valid URL for the GIF file. File size must not exceed 1MB. + URL string `json:"gif_url"` + // ThumbURL url of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result. + ThumbURL string `json:"thumb_url"` + // Width of the GIF + // + // optional + Width int `json:"gif_width,omitempty"` + // Height of the GIF + // + // optional + Height int `json:"gif_height,omitempty"` + // Duration of the GIF + // + // optional + Duration int `json:"gif_duration,omitempty"` + // Title for the result + // + // optional + Title string `json:"title,omitempty"` + // Caption of the GIF file to be sent, 0-1024 characters after entities parsing. + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the GIF animation. + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultLocation is an inline query response location. +type InlineQueryResultLocation struct { + // Type of the result, must be location + Type string `json:"type"` + // ID unique identifier for this result, 1-64 Bytes + ID string `json:"id"` + // Latitude of the location in degrees + Latitude float64 `json:"latitude"` + // Longitude of the location in degrees + Longitude float64 `json:"longitude"` + // Title of the location + Title string `json:"title"` + // HorizontalAccuracy is the radius of uncertainty for the location, + // measured in meters; 0-1500 + // + // optional + HorizontalAccuracy float64 `json:"horizontal_accuracy"` + // LivePeriod is the period in seconds for which the location can be + // updated, should be between 60 and 86400. + // + // optional + LivePeriod int `json:"live_period"` + // Heading is for live locations, a direction in which the user is moving, + // in degrees. Must be between 1 and 360 if specified. + // + // optional + Heading int `json:"heading"` + // ProximityAlertRadius is for live locations, a maximum distance for + // proximity alerts about approaching another chat member, in meters. Must + // be between 1 and 100000 if specified. + // + // optional + ProximityAlertRadius int `json:"proximity_alert_radius"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the location + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` + // ThumbURL url of the thumbnail for the result + // + // optional + ThumbURL string `json:"thumb_url,omitempty"` + // ThumbWidth thumbnail width + // + // optional + ThumbWidth int `json:"thumb_width,omitempty"` + // ThumbHeight thumbnail height + // + // optional + ThumbHeight int `json:"thumb_height,omitempty"` +} + +// InlineQueryResultMPEG4GIF is an inline query response MPEG4 GIF. +type InlineQueryResultMPEG4GIF struct { + // Type of the result, must be mpeg4_gif + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // URL a valid URL for the MP4 file. File size must not exceed 1MB + URL string `json:"mpeg4_url"` + // Width video width + // + // optional + Width int `json:"mpeg4_width"` + // Height vVideo height + // + // optional + Height int `json:"mpeg4_height"` + // Duration video duration + // + // optional + Duration int `json:"mpeg4_duration"` + // ThumbURL url of the static (JPEG or GIF) or animated (MPEG4) thumbnail for the result. + ThumbURL string `json:"thumb_url"` + // Title for the result + // + // optional + Title string `json:"title,omitempty"` + // Caption of the MPEG-4 file to be sent, 0-1024 characters after entities parsing. + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the video animation + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultPhoto is an inline query response photo. +type InlineQueryResultPhoto struct { + // Type of the result, must be article. + Type string `json:"type"` + // ID unique identifier for this result, 1-64 Bytes. + ID string `json:"id"` + // URL a valid URL of the photo. Photo must be in jpeg format. + // Photo size must not exceed 5MB. + URL string `json:"photo_url"` + // MimeType + MimeType string `json:"mime_type"` + // Width of the photo + // + // optional + Width int `json:"photo_width,omitempty"` + // Height of the photo + // + // optional + Height int `json:"photo_height,omitempty"` + // ThumbURL url of the thumbnail for the photo. + // + // optional + ThumbURL string `json:"thumb_url,omitempty"` + // Title for the result + // + // optional + Title string `json:"title,omitempty"` + // Description short description of the result + // + // optional + Description string `json:"description,omitempty"` + // Caption of the photo to be sent, 0-1024 characters after entities parsing. + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the photo caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // ReplyMarkup inline keyboard attached to the message. + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities"` + // InputMessageContent content of the message to be sent instead of the photo. + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultVenue is an inline query response venue. +type InlineQueryResultVenue struct { + // Type of the result, must be venue + Type string `json:"type"` + // ID unique identifier for this result, 1-64 Bytes + ID string `json:"id"` + // Latitude of the venue location in degrees + Latitude float64 `json:"latitude"` + // Longitude of the venue location in degrees + Longitude float64 `json:"longitude"` + // Title of the venue + Title string `json:"title"` + // Address of the venue + Address string `json:"address"` + // FoursquareID foursquare identifier of the venue if known + // + // optional + FoursquareID string `json:"foursquare_id,omitempty"` + // FoursquareType foursquare type of the venue, if known. + // (For example, “arts_entertainment/default”, “arts_entertainment/aquarium” or “food/icecream”.) + // + // optional + FoursquareType string `json:"foursquare_type,omitempty"` + // GooglePlaceID is the Google Places identifier of the venue + // + // optional + GooglePlaceID string `json:"google_place_id,omitempty"` + // GooglePlaceType is the Google Places type of the venue + // + // optional + GooglePlaceType string `json:"google_place_type,omitempty"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the venue + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` + // ThumbURL url of the thumbnail for the result + // + // optional + ThumbURL string `json:"thumb_url,omitempty"` + // ThumbWidth thumbnail width + // + // optional + ThumbWidth int `json:"thumb_width,omitempty"` + // ThumbHeight thumbnail height + // + // optional + ThumbHeight int `json:"thumb_height,omitempty"` +} + +// InlineQueryResultVideo is an inline query response video. +type InlineQueryResultVideo struct { + // Type of the result, must be video + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // URL a valid url for the embedded video player or video file + URL string `json:"video_url"` + // MimeType of the content of video url, “text/html” or “video/mp4” + MimeType string `json:"mime_type"` + // + // ThumbURL url of the thumbnail (jpeg only) for the video + // optional + ThumbURL string `json:"thumb_url,omitempty"` + // Title for the result + Title string `json:"title"` + // Caption of the video to be sent, 0-1024 characters after entities parsing + // + // optional + Caption string `json:"caption,omitempty"` + // Width video width + // + // optional + Width int `json:"video_width,omitempty"` + // Height video height + // + // optional + Height int `json:"video_height,omitempty"` + // Duration video duration in seconds + // + // optional + Duration int `json:"video_duration,omitempty"` + // Description short description of the result + // + // optional + Description string `json:"description,omitempty"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the video. + // This field is required if InlineQueryResultVideo is used to send + // an HTML-page as a result (e.g., a YouTube video). + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultVoice is an inline query response voice. +type InlineQueryResultVoice struct { + // Type of the result, must be voice + Type string `json:"type"` + // ID unique identifier for this result, 1-64 bytes + ID string `json:"id"` + // URL a valid URL for the voice recording + URL string `json:"voice_url"` + // Title recording title + Title string `json:"title"` + // Caption 0-1024 characters after entities parsing + // + // optional + Caption string `json:"caption,omitempty"` + // ParseMode mode for parsing entities in the video caption. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // CaptionEntities is a list of special entities that appear in the caption, + // which can be specified instead of parse_mode + // + // optional + CaptionEntities []MessageEntity `json:"caption_entities"` + // Duration recording duration in seconds + // + // optional + Duration int `json:"voice_duration,omitempty"` + // ReplyMarkup inline keyboard attached to the message + // + // optional + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + // InputMessageContent content of the message to be sent instead of the voice recording + // + // optional + InputMessageContent interface{} `json:"input_message_content,omitempty"` } // ChosenInlineResult is an inline query result chosen by a User type ChosenInlineResult struct { - ResultID string `json:"result_id"` - From *User `json:"from"` - Location *Location `json:"location"` - InlineMessageID string `json:"inline_message_id"` - Query string `json:"query"` + // ResultID the unique identifier for the result that was chosen + ResultID string `json:"result_id"` + // From the user that chose the result + From *User `json:"from"` + // Location sender location, only for bots that require user location + // + // optional + Location *Location `json:"location,omitempty"` + // InlineMessageID identifier of the sent inline message. + // Available only if there is an inline keyboard attached to the message. + // Will be also received in callback queries and can be used to edit the message. + // + // optional + InlineMessageID string `json:"inline_message_id,omitempty"` + // Query the query that was used to obtain the result + Query string `json:"query"` } // InputTextMessageContent contains text for displaying // as an inline query result. type InputTextMessageContent struct { - Text string `json:"message_text"` - ParseMode string `json:"parse_mode"` - DisableWebPagePreview bool `json:"disable_web_page_preview"` + // Text of the message to be sent, 1-4096 characters + Text string `json:"message_text"` + // ParseMode mode for parsing entities in the message text. + // See formatting options for more details + // (https://core.telegram.org/bots/api#formatting-options). + // + // optional + ParseMode string `json:"parse_mode,omitempty"` + // Entities is a list of special entities that appear in message text, which + // can be specified instead of parse_mode + // + // optional + Entities []MessageEntity `json:"entities,omitempty"` + // DisableWebPagePreview disables link previews for links in the sent message + // + // optional + DisableWebPagePreview bool `json:"disable_web_page_preview,omitempty"` } // InputLocationMessageContent contains a location for displaying // as an inline query result. type InputLocationMessageContent struct { - Latitude float64 `json:"latitude"` + // Latitude of the location in degrees + Latitude float64 `json:"latitude"` + // Longitude of the location in degrees Longitude float64 `json:"longitude"` + // HorizontalAccuracy is the radius of uncertainty for the location, + // measured in meters; 0-1500 + // + // optional + HorizontalAccuracy float64 `json:"horizontal_accuracy"` + // LivePeriod is the period in seconds for which the location can be + // updated, should be between 60 and 86400 + // + // optional + LivePeriod int `json:"live_period,omitempty"` + // Heading is for live locations, a direction in which the user is moving, + // in degrees. Must be between 1 and 360 if specified. + // + // optional + Heading int `json:"heading"` + // ProximityAlertRadius is for live locations, a maximum distance for + // proximity alerts about approaching another chat member, in meters. Must + // be between 1 and 100000 if specified. + // + // optional + ProximityAlertRadius int `json:"proximity_alert_radius"` } // InputVenueMessageContent contains a venue for displaying // as an inline query result. type InputVenueMessageContent struct { - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` - Title string `json:"title"` - Address string `json:"address"` - FoursquareID string `json:"foursquare_id"` + // Latitude of the venue in degrees + Latitude float64 `json:"latitude"` + // Longitude of the venue in degrees + Longitude float64 `json:"longitude"` + // Title name of the venue + Title string `json:"title"` + // Address of the venue + Address string `json:"address"` + // FoursquareID foursquare identifier of the venue, if known + // + // optional + FoursquareID string `json:"foursquare_id,omitempty"` + // FoursquareType Foursquare type of the venue, if known + // + // optional + FoursquareType string `json:"foursquare_type,omitempty"` + // GooglePlaceID is the Google Places identifier of the venue + // + // optional + GooglePlaceID string `json:"google_place_id"` + // GooglePlaceType is the Google Places type of the venue + // + // optional + GooglePlaceType string `json:"google_place_type"` } // InputContactMessageContent contains a contact for displaying // as an inline query result. type InputContactMessageContent struct { + // PhoneNumber contact's phone number PhoneNumber string `json:"phone_number"` - FirstName string `json:"first_name"` - LastName string `json:"last_name"` - VCard string `json:"vcard"` -} - -// Invoice contains basic information about an invoice. -type Invoice struct { - Title string `json:"title"` - Description string `json:"description"` - StartParameter string `json:"start_parameter"` - Currency string `json:"currency"` - TotalAmount int `json:"total_amount"` + // FirstName contact's first name + FirstName string `json:"first_name"` + // LastName contact's last name + // + // optional + LastName string `json:"last_name,omitempty"` + // Additional data about the contact in the form of a vCard + // + // optional + VCard string `json:"vcard,omitempty"` } // LabeledPrice represents a portion of the price for goods or services. type LabeledPrice struct { - Label string `json:"label"` - Amount int `json:"amount"` + // Label portion label + Label string `json:"label"` + // Amount price of the product in the smallest units of the currency (integer, not float/double). + // For example, for a price of US$ 1.45 pass amount = 145. + // See the exp parameter in currencies.json + // (https://core.telegram.org/bots/payments/currencies.json), + // it shows the number of digits past the decimal point + // for each currency (2 for the majority of currencies). + Amount int `json:"amount"` +} + +// Invoice contains basic information about an invoice. +type Invoice struct { + // Title product name + Title string `json:"title"` + // Description product description + Description string `json:"description"` + // StartParameter unique bot deep-linking parameter that can be used to generate this invoice + StartParameter string `json:"start_parameter"` + // Currency three-letter ISO 4217 currency code + // (see https://core.telegram.org/bots/payments#supported-currencies) + Currency string `json:"currency"` + // TotalAmount total price in the smallest units of the currency (integer, not float/double). + // For example, for a price of US$ 1.45 pass amount = 145. + // See the exp parameter in currencies.json + // (https://core.telegram.org/bots/payments/currencies.json), + // it shows the number of digits past the decimal point + // for each currency (2 for the majority of currencies). + TotalAmount int `json:"total_amount"` } // ShippingAddress represents a shipping address. type ShippingAddress struct { + // CountryCode ISO 3166-1 alpha-2 country code CountryCode string `json:"country_code"` - State string `json:"state"` - City string `json:"city"` + // State if applicable + State string `json:"state"` + // City city + City string `json:"city"` + // StreetLine1 first line for the address StreetLine1 string `json:"street_line1"` + // StreetLine2 second line for the address StreetLine2 string `json:"street_line2"` - PostCode string `json:"post_code"` + // PostCode address post code + PostCode string `json:"post_code"` } // OrderInfo represents information about an order. type OrderInfo struct { - Name string `json:"name,omitempty"` - PhoneNumber string `json:"phone_number,omitempty"` - Email string `json:"email,omitempty"` + // Name user name + // + // optional + Name string `json:"name,omitempty"` + // PhoneNumber user's phone number + // + // optional + PhoneNumber string `json:"phone_number,omitempty"` + // Email user email + // + // optional + Email string `json:"email,omitempty"` + // ShippingAddress user shipping address + // + // optional ShippingAddress *ShippingAddress `json:"shipping_address,omitempty"` } // ShippingOption represents one shipping option. type ShippingOption struct { - ID string `json:"id"` - Title string `json:"title"` + // ID shipping option identifier + ID string `json:"id"` + // Title option title + Title string `json:"title"` + // Prices list of price portions Prices []LabeledPrice `json:"prices"` } // SuccessfulPayment contains basic information about a successful payment. type SuccessfulPayment struct { - Currency string `json:"currency"` - TotalAmount int `json:"total_amount"` - InvoicePayload string `json:"invoice_payload"` - ShippingOptionID string `json:"shipping_option_id,omitempty"` - OrderInfo *OrderInfo `json:"order_info,omitempty"` - TelegramPaymentChargeID string `json:"telegram_payment_charge_id"` - ProviderPaymentChargeID string `json:"provider_payment_charge_id"` + // Currency three-letter ISO 4217 currency code + // (see https://core.telegram.org/bots/payments#supported-currencies) + Currency string `json:"currency"` + // TotalAmount total price in the smallest units of the currency (integer, not float/double). + // For example, for a price of US$ 1.45 pass amount = 145. + // See the exp parameter in currencies.json, + // (https://core.telegram.org/bots/payments/currencies.json) + // it shows the number of digits past the decimal point + // for each currency (2 for the majority of currencies). + TotalAmount int `json:"total_amount"` + // InvoicePayload bot specified invoice payload + InvoicePayload string `json:"invoice_payload"` + // ShippingOptionID identifier of the shipping option chosen by the user + // + // optional + ShippingOptionID string `json:"shipping_option_id,omitempty"` + // OrderInfo order info provided by the user + // + // optional + OrderInfo *OrderInfo `json:"order_info,omitempty"` + // TelegramPaymentChargeID telegram payment identifier + TelegramPaymentChargeID string `json:"telegram_payment_charge_id"` + // ProviderPaymentChargeID provider payment identifier + ProviderPaymentChargeID string `json:"provider_payment_charge_id"` } // ShippingQuery contains information about an incoming shipping query. type ShippingQuery struct { - ID string `json:"id"` - From *User `json:"from"` - InvoicePayload string `json:"invoice_payload"` + // ID unique query identifier + ID string `json:"id"` + // From user who sent the query + From *User `json:"from"` + // InvoicePayload bot specified invoice payload + InvoicePayload string `json:"invoice_payload"` + // ShippingAddress user specified shipping address ShippingAddress *ShippingAddress `json:"shipping_address"` } // PreCheckoutQuery contains information about an incoming pre-checkout query. type PreCheckoutQuery struct { - ID string `json:"id"` - From *User `json:"from"` - Currency string `json:"currency"` - TotalAmount int `json:"total_amount"` - InvoicePayload string `json:"invoice_payload"` - ShippingOptionID string `json:"shipping_option_id,omitempty"` - OrderInfo *OrderInfo `json:"order_info,omitempty"` -} - -// StickerSet is a collection of stickers. -type StickerSet struct { - Name string `json:"name"` - Title string `json:"title"` - ContainsMasks bool `json:"contains_masks"` - Stickers []Sticker `json:"stickers"` -} - -// BaseInputMedia is a base type for the InputMedia types. -type BaseInputMedia struct { - Type string `json:"type"` - Media string `json:"media"` - Caption string `json:"caption"` - ParseMode string `json:"parse_mode"` -} - -// InputMediaPhoto is a photo to send as part of a media group. -type InputMediaPhoto struct { - BaseInputMedia -} - -// InputMediaVideo is a video to send as part of a media group. -type InputMediaVideo struct { - BaseInputMedia - Width int `json:"width"` - Height int `json:"height"` - Duration int `json:"duration"` - SupportsStreaming bool `json:"supports_streaming"` -} - -// InputMediaAnimation is an animation to send as part of a media group. -type InputMediaAnimation struct { - BaseInputMedia - Width int `json:"width"` - Height int `json:"height"` - Duration int `json:"duration"` -} - -// InputMediaAudio is a audio to send as part of a media group. -type InputMediaAudio struct { - BaseInputMedia - Duration int `json:"duration"` - Performer string `json:"performer"` - Title string `json:"title"` -} - -// InputMediaDocument is a audio to send as part of a media group. -type InputMediaDocument struct { - BaseInputMedia -} - -// Error is an error containing extra information returned by the Telegram API. -type Error struct { - Message string - ResponseParameters -} - -// Error message string. -func (e Error) Error() string { - return e.Message + // ID unique query identifier + ID string `json:"id"` + // From user who sent the query + From *User `json:"from"` + // Currency three-letter ISO 4217 currency code + // // (see https://core.telegram.org/bots/payments#supported-currencies) + Currency string `json:"currency"` + // TotalAmount total price in the smallest units of the currency (integer, not float/double). + // // For example, for a price of US$ 1.45 pass amount = 145. + // // See the exp parameter in currencies.json, + // // (https://core.telegram.org/bots/payments/currencies.json) + // // it shows the number of digits past the decimal point + // // for each currency (2 for the majority of currencies). + TotalAmount int `json:"total_amount"` + // InvoicePayload bot specified invoice payload + InvoicePayload string `json:"invoice_payload"` + // ShippingOptionID identifier of the shipping option chosen by the user + // + // optional + ShippingOptionID string `json:"shipping_option_id,omitempty"` + // OrderInfo order info provided by the user + // + // optional + OrderInfo *OrderInfo `json:"order_info,omitempty"` } diff --git a/types_test.go b/types_test.go index b418874..7a4b196 100644 --- a/types_test.go +++ b/types_test.go @@ -189,6 +189,86 @@ func TestChatIsSuperGroup(t *testing.T) { } } +func TestMessageEntityIsMention(t *testing.T) { + entity := MessageEntity{Type: "mention"} + + if !entity.IsMention() { + t.Fail() + } +} + +func TestMessageEntityIsHashtag(t *testing.T) { + entity := MessageEntity{Type: "hashtag"} + + if !entity.IsHashtag() { + t.Fail() + } +} + +func TestMessageEntityIsBotCommand(t *testing.T) { + entity := MessageEntity{Type: "bot_command"} + + if !entity.IsCommand() { + t.Fail() + } +} + +func TestMessageEntityIsUrl(t *testing.T) { + entity := MessageEntity{Type: "url"} + + if !entity.IsURL() { + t.Fail() + } +} + +func TestMessageEntityIsEmail(t *testing.T) { + entity := MessageEntity{Type: "email"} + + if !entity.IsEmail() { + t.Fail() + } +} + +func TestMessageEntityIsBold(t *testing.T) { + entity := MessageEntity{Type: "bold"} + + if !entity.IsBold() { + t.Fail() + } +} + +func TestMessageEntityIsItalic(t *testing.T) { + entity := MessageEntity{Type: "italic"} + + if !entity.IsItalic() { + t.Fail() + } +} + +func TestMessageEntityIsCode(t *testing.T) { + entity := MessageEntity{Type: "code"} + + if !entity.IsCode() { + t.Fail() + } +} + +func TestMessageEntityIsPre(t *testing.T) { + entity := MessageEntity{Type: "pre"} + + if !entity.IsPre() { + t.Fail() + } +} + +func TestMessageEntityIsTextLink(t *testing.T) { + entity := MessageEntity{Type: "text_link"} + + if !entity.IsTextLink() { + t.Fail() + } +} + func TestFileLink(t *testing.T) { file := File{FilePath: "test/test.txt"} @@ -203,31 +283,53 @@ var ( _ Chattable = AudioConfig{} _ Chattable = CallbackConfig{} _ Chattable = ChatActionConfig{} + _ Chattable = ChatAdministratorsConfig{} + _ Chattable = ChatInfoConfig{} + _ Chattable = ChatInviteLinkConfig{} + _ Chattable = CloseConfig{} _ Chattable = ContactConfig{} + _ Chattable = CopyMessageConfig{} + _ Chattable = CreateChatInviteLinkConfig{} _ Chattable = DeleteChatPhotoConfig{} _ Chattable = DeleteChatStickerSetConfig{} _ Chattable = DeleteMessageConfig{} + _ Chattable = DeleteWebhookConfig{} _ Chattable = DocumentConfig{} + _ Chattable = EditChatInviteLinkConfig{} _ Chattable = EditMessageCaptionConfig{} _ Chattable = EditMessageLiveLocationConfig{} + _ Chattable = EditMessageMediaConfig{} _ Chattable = EditMessageReplyMarkupConfig{} _ Chattable = EditMessageTextConfig{} + _ Chattable = FileConfig{} _ Chattable = ForwardConfig{} _ Chattable = GameConfig{} + _ Chattable = GetChatMemberConfig{} _ Chattable = GetGameHighScoresConfig{} _ Chattable = InlineConfig{} _ Chattable = InvoiceConfig{} _ Chattable = KickChatMemberConfig{} + _ Chattable = LeaveChatConfig{} _ Chattable = LocationConfig{} + _ Chattable = LogOutConfig{} _ Chattable = MediaGroupConfig{} _ Chattable = MessageConfig{} _ Chattable = PhotoConfig{} _ Chattable = PinChatMessageConfig{} + _ Chattable = PreCheckoutConfig{} + _ Chattable = PromoteChatMemberConfig{} + _ Chattable = RestrictChatMemberConfig{} + _ Chattable = RevokeChatInviteLinkConfig{} + _ Chattable = SendPollConfig{} _ Chattable = SetChatDescriptionConfig{} _ Chattable = SetChatPhotoConfig{} _ Chattable = SetChatTitleConfig{} _ Chattable = SetGameScoreConfig{} + _ Chattable = ShippingConfig{} _ Chattable = StickerConfig{} + _ Chattable = StopMessageLiveLocationConfig{} + _ Chattable = StopPollConfig{} + _ Chattable = UnbanChatMemberConfig{} _ Chattable = UnpinChatMessageConfig{} _ Chattable = UpdateConfig{} _ Chattable = UserProfilePhotosConfig{} @@ -237,3 +339,24 @@ var ( _ Chattable = VoiceConfig{} _ Chattable = WebhookConfig{} ) + +// Ensure all Fileable types are correct. +var ( + _ Fileable = (*PhotoConfig)(nil) + _ Fileable = (*AudioConfig)(nil) + _ Fileable = (*DocumentConfig)(nil) + _ Fileable = (*StickerConfig)(nil) + _ Fileable = (*VideoConfig)(nil) + _ Fileable = (*AnimationConfig)(nil) + _ Fileable = (*VideoNoteConfig)(nil) + _ Fileable = (*VoiceConfig)(nil) + _ Fileable = (*SetChatPhotoConfig)(nil) + _ Fileable = (*EditMessageMediaConfig)(nil) + _ Fileable = (*SetChatPhotoConfig)(nil) + _ Fileable = (*UploadStickerConfig)(nil) + _ Fileable = (*NewStickerSetConfig)(nil) + _ Fileable = (*AddStickerConfig)(nil) + _ Fileable = (*MediaGroupConfig)(nil) + _ Fileable = (*WebhookConfig)(nil) + _ Fileable = (*SetStickerSetThumbConfig)(nil) +)