├── logger.go ├── go.mod ├── types_test.go ├── .gitignore ├── examples └── echobot │ └── main.go ├── types_payments.go ├── options_test.go ├── helpers_test.go ├── http_test.go ├── types_games.go ├── .github └── workflows │ └── tests.yml ├── LICENSE ├── go.sum ├── types_stickers.go ├── validity_test.go ├── types_inline_test.go ├── validity.go ├── README.md ├── http.go ├── helpers.go ├── options.go ├── types.go ├── types_inline.go ├── bot.go └── bot_test.go /logger.go: -------------------------------------------------------------------------------- 1 | package micha 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // Logger interface 8 | type Logger interface { 9 | ErrorContext(ctx context.Context, msg string, args ...any) 10 | } 11 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/onrik/micha 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/jarcoal/httpmock v1.3.1 7 | github.com/stretchr/testify v1.9.0 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/pmezard/go-difflib v1.0.0 // indirect 13 | gopkg.in/yaml.v3 v3.0.1 // indirect 14 | ) 15 | -------------------------------------------------------------------------------- /types_test.go: -------------------------------------------------------------------------------- 1 | package micha 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestReplyMarkup(t *testing.T) { 8 | (ReplyKeyboardMarkup{}).itsReplyMarkup() 9 | (ReplyKeyboardRemove{}).itsReplyMarkup() 10 | (InlineKeyboardMarkup{}).itsReplyMarkup() 11 | (ForceReply{}).itsReplyMarkup() 12 | (ForceReply{}).itsReplyMarkup() 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | /vendor 27 | -------------------------------------------------------------------------------- /examples/echobot/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/onrik/micha" 7 | ) 8 | 9 | func main() { 10 | bot, err := micha.NewBot("") 11 | if err != nil { 12 | log.Fatal(err) 13 | } 14 | 15 | go bot.Start() 16 | 17 | for update := range bot.Updates() { 18 | if update.Message != nil { 19 | bot.SendMessage(update.Message.Chat.ID, update.Message.Text, nil) 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /types_payments.go: -------------------------------------------------------------------------------- 1 | package micha 2 | 3 | type Invoice struct { 4 | Title string 5 | Description string 6 | StartParameter string 7 | Currency string 8 | TotalAmount int 9 | } 10 | 11 | type SuccessfulPayment struct { 12 | // TODO 13 | } 14 | 15 | type ShippingAddress struct { 16 | // TODO 17 | } 18 | 19 | type PassportData struct { 20 | // TODO 21 | } 22 | 23 | type ShippingQuery struct { 24 | // TODO 25 | } 26 | 27 | type PreCheckoutQuery struct { 28 | // TODO 29 | } 30 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package micha 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestWithAPIServer(t *testing.T) { 11 | options := &Options{} 12 | WithAPIServer("http://127.0.0.1/")(options) 13 | 14 | require.Equal(t, "http://127.0.0.1", options.apiServer) 15 | } 16 | 17 | func TestWithCtx(t *testing.T) { 18 | options := &Options{} 19 | ctx := context.WithValue(context.Background(), "foo", "bar") 20 | WithCtx(ctx)(options) 21 | 22 | require.Equal(t, ctx, options.ctx) 23 | } 24 | -------------------------------------------------------------------------------- /helpers_test.go: -------------------------------------------------------------------------------- 1 | package micha 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | type testStruct struct { 11 | ID int64 `json:"id"` 12 | Name string `json:"name"` 13 | } 14 | 15 | func TestStructToValues(t *testing.T) { 16 | _, err := structToValues([]string{"1"}) 17 | require.NotNil(t, err) 18 | 19 | s := testStruct{ 20 | ID: 3904834, 21 | Name: `"Rock & Roll"`, 22 | } 23 | values, err := structToValues(s) 24 | require.Nil(t, err) 25 | require.Equal(t, url.Values{ 26 | "id": {"3904834"}, 27 | "name": {`"Rock & Roll"`}, 28 | }, values) 29 | } 30 | -------------------------------------------------------------------------------- /http_test.go: -------------------------------------------------------------------------------- 1 | package micha 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestHTTPError(t *testing.T) { 13 | var err error = HTTPError{ 14 | StatusCode: http.StatusNotFound, 15 | } 16 | 17 | require.Equal(t, "http status 404 (Not Found)", err.Error()) 18 | } 19 | 20 | func TestHandleResponse(t *testing.T) { 21 | response := &http.Response{ 22 | Body: io.NopCloser(bytes.NewBuffer(nil)), 23 | StatusCode: http.StatusForbidden, 24 | } 25 | 26 | body, err := handleResponse(response) 27 | require.NotNil(t, err) 28 | require.Nil(t, body) 29 | 30 | require.Equal(t, "http status 403 (Forbidden)", err.Error()) 31 | } 32 | -------------------------------------------------------------------------------- /types_games.go: -------------------------------------------------------------------------------- 1 | package micha 2 | 3 | // Game object represents a game. 4 | // Use BotFather to create and edit games, their short names will act as unique identifiers. 5 | type Game struct { 6 | Title string `json:"title"` 7 | Description string `json:"description"` 8 | Photo []PhotoSize `json:"photo"` 9 | 10 | // Optional 11 | Text string `json:"text"` 12 | TextEntities []MessageEntity `json:"text_entities"` 13 | Animation *Animation `json:"animation"` 14 | } 15 | 16 | // GameHighScore object represents one row of the high scores table for a game. 17 | type GameHighScore struct { 18 | Position int `json:"position"` 19 | User User `json:"user"` 20 | Score int `json:"score"` 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | test: 13 | strategy: 14 | matrix: 15 | go-version: [1.22.x] 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Install Go 21 | uses: actions/setup-go@v3 22 | with: 23 | go-version: ${{ matrix.go-version }} 24 | 25 | - name: Checkout code 26 | uses: actions/checkout@v3 27 | 28 | - name: Test 29 | run: go test -covermode atomic -coverprofile=profile.cov ./... 30 | 31 | - name: Coveralls 32 | env: 33 | COVERALLS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | run: | 35 | go install github.com/mattn/goveralls@latest 36 | goveralls -coverprofile=profile.cov -service=github -ignore=examples/*/* 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Andrey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww= 4 | github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= 5 | github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= 6 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 7 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 8 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 9 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 12 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 13 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 14 | -------------------------------------------------------------------------------- /types_stickers.go: -------------------------------------------------------------------------------- 1 | package micha 2 | 3 | // Sticker object represents a WebP image, so-called sticker. 4 | type Sticker struct { 5 | FileID string `json:"file_id"` 6 | Width int `json:"width"` 7 | Height int `json:"height"` 8 | IsAnimated bool `json:"is_animated,omitempty"` 9 | 10 | // Optional 11 | Thumb *PhotoSize `json:"thumb,omitempty"` 12 | Emoji string `json:"emoji,omitempty"` 13 | SetName string `json:"set_name,omitempty"` 14 | MaskPosition *MaskPosition `json:"mask_position,omitempty"` 15 | FileSize uint64 `json:"file_size,omitempty"` 16 | } 17 | 18 | // StickerSet object represents a sticker set. 19 | type StickerSet struct { 20 | Name string `json:"name"` 21 | Title string `json:"title"` 22 | IsAnimated bool `json:"is_animated"` 23 | ContainsMasks bool `json:"contains_masks"` 24 | Stickers []Sticker `json:"stickers"` 25 | } 26 | 27 | // MaskPosition object describes the position on faces where a mask should be placed by default. 28 | type MaskPosition struct { 29 | Point string `json:"point"` 30 | XShift float64 `json:"x_shift"` 31 | YShift float64 `json:"y_shift"` 32 | Scale float64 `json:"scale"` 33 | } 34 | -------------------------------------------------------------------------------- /validity_test.go: -------------------------------------------------------------------------------- 1 | package micha 2 | 3 | import ( 4 | "net/url" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestBuildCheckString(t *testing.T) { 11 | values := url.Values{ 12 | "id": {"12807202"}, 13 | "first_name": {"John"}, 14 | "username": {"doe"}, 15 | "photo_url": {"https://t.me/i/userpic/320/a4f15041-e6a3-4cf4-9363-b0cba2f66720.jpg"}, 16 | "auth_date": {"1722489598"}, 17 | "hash": {"a19016198a35deb3469a2af3c5aa1f40aa71941cf14cc20e7be8c6507b147061"}, 18 | } 19 | 20 | require.Equal(t, 21 | "auth_date=1722489598\nfirst_name=John\nid=12807202\nphoto_url=https://t.me/i/userpic/320/a4f15041-e6a3-4cf4-9363-b0cba2f66720.jpg\nusername=doe", 22 | buildCheckString(values), 23 | ) 24 | } 25 | 26 | func TestValidateHash(t *testing.T) { 27 | values := url.Values{ 28 | "id": {"12807202"}, 29 | "first_name": {"John"}, 30 | "username": {"doe"}, 31 | "photo_url": {"https://t.me/i/userpic/320/a4f15041-e6a3-4cf4-9363-b0cba2f66720.jpg"}, 32 | "auth_date": {"1722489598"}, 33 | "hash": {"aba23cb08c508bd952abb2a9b3c2e9b3d6a2c8726145ccea53bd660d7995b586"}, 34 | } 35 | 36 | // Test valid 37 | err := validateHash(values, []byte("111")) 38 | require.Nil(t, err) 39 | 40 | // Test invalid 41 | err = validateHash(values, []byte("222")) 42 | require.NotNil(t, err) 43 | } 44 | -------------------------------------------------------------------------------- /types_inline_test.go: -------------------------------------------------------------------------------- 1 | package micha 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestInlineQueryResult(t *testing.T) { 8 | (InlineQueryResultArticle{}).itsInlineQueryResult() 9 | (InlineQueryResultPhoto{}).itsInlineQueryResult() 10 | (InlineQueryResultCachedPhoto{}).itsInlineQueryResult() 11 | (InlineQueryResultGif{}).itsInlineQueryResult() 12 | (InlineQueryResultCachedGif{}).itsInlineQueryResult() 13 | (InlineQueryResultMpeg4Gif{}).itsInlineQueryResult() 14 | (InlineQueryResultCachedMpeg4Gif{}).itsInlineQueryResult() 15 | (InlineQueryResultVideo{}).itsInlineQueryResult() 16 | (InlineQueryResultCachedVideo{}).itsInlineQueryResult() 17 | (InlineQueryResultAudio{}).itsInlineQueryResult() 18 | (InlineQueryResultCachedAudio{}).itsInlineQueryResult() 19 | (InlineQueryResultVoice{}).itsInlineQueryResult() 20 | (InlineQueryResultCachedVoice{}).itsInlineQueryResult() 21 | (InlineQueryResultDocument{}).itsInlineQueryResult() 22 | (InlineQueryResultCachedDocument{}).itsInlineQueryResult() 23 | (InlineQueryResultLocation{}).itsInlineQueryResult() 24 | (InlineQueryResultVenue{}).itsInlineQueryResult() 25 | (InlineQueryResultCachedSticker{}).itsInlineQueryResult() 26 | (InlineQueryResultContact{}).itsInlineQueryResult() 27 | (InlineQueryResultGame{}).itsInlineQueryResult() 28 | } 29 | 30 | func TestInputMessageContent(t *testing.T) { 31 | (InputTextMessageContent{}).itsInputMessageContent() 32 | (InputLocationMessageContent{}).itsInputMessageContent() 33 | (InputVenueMessageContent{}).itsInputMessageContent() 34 | (inputMessageContentImplementation{}).itsInputMessageContent() 35 | } 36 | -------------------------------------------------------------------------------- /validity.go: -------------------------------------------------------------------------------- 1 | package micha 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | "errors" 7 | "fmt" 8 | "net/url" 9 | "sort" 10 | "strings" 11 | ) 12 | 13 | var ( 14 | ErrInvalidHash = errors.New("invalid hash") 15 | ) 16 | 17 | // ValidateAuthCallback - https://core.telegram.org/widgets/login#checking-authorization 18 | func ValidateAuthCallback(values url.Values, botToken string) error { 19 | secret := sha256.Sum256([]byte(botToken)) 20 | 21 | return validateHash(values, secret[:]) 22 | } 23 | 24 | // ValidateWabAppData - https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app 25 | func ValidateWabAppData(values url.Values, botToken string) error { 26 | hm := hmac.New(sha256.New, []byte("WebAppData")) 27 | _, err := hm.Write([]byte(botToken)) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | secret := hm.Sum(nil) 33 | 34 | return validateHash(values, secret) 35 | } 36 | 37 | func validateHash(values url.Values, secret []byte) error { 38 | hm := hmac.New(sha256.New, secret) 39 | _, err := hm.Write([]byte(buildCheckString(values))) 40 | if err != nil { 41 | return err 42 | } 43 | 44 | if fmt.Sprintf("%x", hm.Sum(nil)) != values.Get("hash") { 45 | return ErrInvalidHash 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func buildCheckString(values url.Values) string { 52 | keys := []string{} 53 | for key := range values { 54 | if key == "hash" { 55 | continue 56 | } 57 | keys = append(keys, key) 58 | } 59 | 60 | sort.Strings(keys) 61 | parts := []string{} 62 | for _, key := range keys { 63 | parts = append(parts, fmt.Sprintf("%s=%s", key, values.Get(key))) 64 | } 65 | 66 | return strings.Join(parts, "\n") 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Micha 2 | 3 | [![Tests](https://github.com/onrik/micha/workflows/Tests/badge.svg)](https://github.com/onrik/micha/actions) 4 | [![Coverage Status](https://coveralls.io/repos/github/onrik/micha/badge.svg?branch=master)](https://coveralls.io/github/onrik/micha?branch=master) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/onrik/micha)](https://goreportcard.com/report/github.com/onrik/micha) 6 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/onrik/micha)](https://pkg.go.dev/github.com/onrik/micha) 7 | 8 | Client lib for [Telegram bot api](https://core.telegram.org/bots/api). 9 | 10 | ### Simple echo bot 11 | ```go 12 | package main 13 | 14 | import ( 15 | "log" 16 | 17 | "github.com/onrik/micha" 18 | ) 19 | 20 | func main() { 21 | bot, err := micha.NewBot("") 22 | if err != nil { 23 | log.Println(err) 24 | return 25 | } 26 | 27 | go bot.Start() 28 | 29 | for update := range bot.Updates() { 30 | if update.Message != nil { 31 | bot.SendMessage(update.Message.Chat.ID, update.Message.Text, nil) 32 | } 33 | } 34 | } 35 | 36 | ``` 37 | 38 | 39 | ### Custom [Telegram Bot API](https://github.com/tdlib/telegram-bot-api) 40 | ```go 41 | package main 42 | 43 | import ( 44 | "log" 45 | 46 | "github.com/onrik/micha" 47 | ) 48 | 49 | func main() { 50 | bot, err := micha.NewBot( 51 | "", 52 | micha.WithAPIServer("http://127.0.0.1:8081"), 53 | ) 54 | if err != nil { 55 | log.Println(err) 56 | return 57 | } 58 | 59 | err = bot.Logout() 60 | if err != nil { 61 | log.Println(err) 62 | return 63 | } 64 | 65 | 66 | go bot.Start() 67 | 68 | for update := range bot.Updates() { 69 | if update.Message != nil { 70 | bot.SendMessage(update.Message.Chat.ID, update.Message.Text, nil) 71 | } 72 | } 73 | } 74 | 75 | ``` 76 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package micha 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "io" 9 | "mime/multipart" 10 | "net/http" 11 | "net/url" 12 | ) 13 | 14 | // HttpClient interface 15 | type HttpClient interface { 16 | Do(*http.Request) (*http.Response, error) 17 | } 18 | 19 | type HTTPError struct { 20 | StatusCode int 21 | } 22 | 23 | func (e HTTPError) Error() string { 24 | return fmt.Sprintf("http status %d (%s)", e.StatusCode, http.StatusText(e.StatusCode)) 25 | } 26 | 27 | type fileField struct { 28 | Source io.Reader 29 | Fieldname string 30 | Filename string 31 | } 32 | 33 | func handleResponse(response *http.Response) ([]byte, error) { 34 | defer response.Body.Close() 35 | if response.StatusCode > http.StatusBadRequest { 36 | return nil, HTTPError{response.StatusCode} 37 | } 38 | 39 | return io.ReadAll(response.Body) 40 | } 41 | 42 | func newGetRequest(ctx context.Context, url string, params url.Values) (*http.Request, error) { 43 | if params != nil { 44 | url += fmt.Sprintf("?%s", params.Encode()) 45 | } 46 | return http.NewRequestWithContext(ctx, http.MethodGet, url, nil) 47 | } 48 | 49 | func newPostRequest(ctx context.Context, url string, data interface{}) (*http.Request, error) { 50 | body := new(bytes.Buffer) 51 | if data != nil { 52 | if err := json.NewEncoder(body).Encode(data); err != nil { 53 | return nil, fmt.Errorf("encode data error: %w", err) 54 | } 55 | } 56 | 57 | request, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | request.Header.Add("Content-Type", "application/json") 63 | 64 | return request, nil 65 | } 66 | 67 | func newMultipartRequest(ctx context.Context, url string, file *fileField, params url.Values) (*http.Request, error) { 68 | body := new(bytes.Buffer) 69 | writer := multipart.NewWriter(body) 70 | 71 | if file != nil { 72 | part, err := writer.CreateFormFile(file.Fieldname, file.Filename) 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | if _, err := io.Copy(part, file.Source); err != nil { 78 | return nil, err 79 | } 80 | } 81 | 82 | for field, values := range params { 83 | for i := range values { 84 | if err := writer.WriteField(field, values[i]); err != nil { 85 | return nil, err 86 | } 87 | } 88 | } 89 | 90 | if err := writer.Close(); err != nil { 91 | return nil, err 92 | } 93 | 94 | request, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | request.Header.Add("Content-Type", writer.FormDataContentType()) 100 | 101 | return request, nil 102 | } 103 | -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | package micha 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "net/url" 7 | ) 8 | 9 | // Convert struct to url values map 10 | // TODO: temp implementation 11 | func structToValues(s any) (url.Values, error) { 12 | buff := bytes.Buffer{} 13 | encoder := json.NewEncoder(&buff) 14 | encoder.SetEscapeHTML(false) 15 | err := encoder.Encode(s) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | rawMap := map[string]json.RawMessage{} 21 | if err := json.Unmarshal(buff.Bytes(), &rawMap); err != nil { 22 | return nil, err 23 | } 24 | 25 | values := url.Values{} 26 | for key, value := range rawMap { 27 | if value[0] != '"' { 28 | values.Set(key, string(value)) 29 | continue 30 | } 31 | 32 | var str string 33 | err = json.Unmarshal(value, &str) 34 | if err != nil { 35 | return nil, err 36 | } 37 | values.Set(key, str) 38 | } 39 | 40 | return values, nil 41 | } 42 | 43 | type sendMessageParams struct { 44 | SendMessageOptions 45 | ChatID ChatID `json:"chat_id"` 46 | Text string `json:"text"` 47 | } 48 | 49 | type sendPhotoParams struct { 50 | ChatID ChatID `json:"chat_id"` 51 | Photo string `json:"photo,omitempty"` 52 | SendPhotoOptions 53 | } 54 | 55 | func newSendPhotoParams(chatID ChatID, photo string, options *SendPhotoOptions) *sendPhotoParams { 56 | params := &sendPhotoParams{ 57 | ChatID: chatID, 58 | Photo: photo, 59 | } 60 | 61 | if options != nil { 62 | params.SendPhotoOptions = *options 63 | } 64 | 65 | return params 66 | } 67 | 68 | type sendAudioParams struct { 69 | ChatID ChatID `json:"chat_id"` 70 | Audio string `json:"audio,omitempty"` 71 | SendAudioOptions 72 | } 73 | 74 | func newSendAudioParams(chatID ChatID, audio string, options *SendAudioOptions) *sendAudioParams { 75 | params := &sendAudioParams{ 76 | ChatID: chatID, 77 | Audio: audio, 78 | } 79 | 80 | if options != nil { 81 | params.SendAudioOptions = *options 82 | } 83 | 84 | return params 85 | } 86 | 87 | type sendDocumentParams struct { 88 | ChatID ChatID `json:"chat_id"` 89 | Document string `json:"document,omitempty"` 90 | SendDocumentOptions 91 | } 92 | 93 | func newSendDocumentParams(chatID ChatID, document string, options *SendDocumentOptions) *sendDocumentParams { 94 | params := &sendDocumentParams{ 95 | ChatID: chatID, 96 | Document: document, 97 | } 98 | 99 | if options != nil { 100 | params.SendDocumentOptions = *options 101 | } 102 | 103 | return params 104 | } 105 | 106 | type sendStickerParams struct { 107 | ChatID ChatID `json:"chat_id"` 108 | Sticker string `json:"sticker,omitempty"` 109 | SendStickerOptions 110 | } 111 | 112 | func newSendStickerParams(chatID ChatID, sticker string, options *SendStickerOptions) *sendStickerParams { 113 | params := &sendStickerParams{ 114 | ChatID: chatID, 115 | Sticker: sticker, 116 | } 117 | 118 | if options != nil { 119 | params.SendStickerOptions = *options 120 | } 121 | 122 | return params 123 | } 124 | 125 | type sendVideoParams struct { 126 | ChatID ChatID `json:"chat_id"` 127 | Video string `json:"video,omitempty"` 128 | SendVideoOptions 129 | } 130 | 131 | func newSendVideoParams(chatID ChatID, video string, options *SendVideoOptions) *sendVideoParams { 132 | params := &sendVideoParams{ 133 | ChatID: chatID, 134 | Video: video, 135 | } 136 | 137 | if options != nil { 138 | params.SendVideoOptions = *options 139 | } 140 | 141 | return params 142 | } 143 | 144 | type sendVoiceParams struct { 145 | ChatID ChatID `json:"chat_id"` 146 | Voice string `json:"voice,omitempty"` 147 | SendVoiceOptions 148 | } 149 | 150 | func newSendVoiceParams(chatID ChatID, voice string, options *SendVoiceOptions) *sendVoiceParams { 151 | params := &sendVoiceParams{ 152 | ChatID: chatID, 153 | Voice: voice, 154 | } 155 | 156 | if options != nil { 157 | params.SendVoiceOptions = *options 158 | } 159 | 160 | return params 161 | } 162 | 163 | type sendVideoNoteParams struct { 164 | ChatID ChatID `json:"chat_id"` 165 | VideoNote string `json:"video_note,omitempty"` 166 | SendVideoNoteOptions 167 | } 168 | 169 | func newSendVideoNoteParams(chatID ChatID, videoNote string, options *SendVideoNoteOptions) *sendVideoNoteParams { 170 | params := &sendVideoNoteParams{ 171 | ChatID: chatID, 172 | VideoNote: videoNote, 173 | } 174 | 175 | if options != nil { 176 | params.SendVideoNoteOptions = *options 177 | } 178 | 179 | return params 180 | } 181 | 182 | type sendLocationParams struct { 183 | ChatID ChatID `json:"chat_id"` 184 | Latitude float64 `json:"latitude,omitempty"` 185 | Longitude float64 `json:"longitude,omitempty"` 186 | SendLocationOptions 187 | } 188 | 189 | func newSendLocationParams(chatID ChatID, latitude, longitude float64, options *SendLocationOptions) *sendLocationParams { 190 | params := &sendLocationParams{ 191 | ChatID: chatID, 192 | Latitude: latitude, 193 | Longitude: longitude, 194 | } 195 | 196 | if options != nil { 197 | params.SendLocationOptions = *options 198 | } 199 | 200 | return params 201 | } 202 | 203 | type sendVenueParams struct { 204 | ChatID ChatID `json:"chat_id"` 205 | Latitude float64 `json:"latitude,omitempty"` 206 | Longitude float64 `json:"longitude,omitempty"` 207 | Title string `json:"title,omitempty"` 208 | Address string `json:"address,omitempty"` 209 | SendVenueOptions 210 | } 211 | 212 | func newSendVenueParams(chatID ChatID, latitude, longitude float64, title, address string, options *SendVenueOptions) *sendVenueParams { 213 | params := &sendVenueParams{ 214 | ChatID: chatID, 215 | Latitude: latitude, 216 | Longitude: longitude, 217 | Title: title, 218 | Address: address, 219 | } 220 | 221 | if options != nil { 222 | params.SendVenueOptions = *options 223 | } 224 | 225 | return params 226 | } 227 | 228 | type sendContactParams struct { 229 | ChatID ChatID `json:"chat_id"` 230 | PhoneNumber string `json:"phone_number"` 231 | FirstName string `json:"first_name"` 232 | LastName string `json:"last_name,omitempty"` 233 | SendContactOptions 234 | } 235 | 236 | func newSendContactParams(chatID ChatID, phoneNumber, firstName, lastName string, options *SendContactOptions) *sendContactParams { 237 | params := &sendContactParams{ 238 | ChatID: chatID, 239 | PhoneNumber: phoneNumber, 240 | FirstName: firstName, 241 | LastName: lastName, 242 | } 243 | 244 | if options != nil { 245 | params.SendContactOptions = *options 246 | } 247 | 248 | return params 249 | } 250 | 251 | type sendGameParams struct { 252 | ChatID ChatID `json:"chat_id"` 253 | GameShortName string `json:"game_short_name"` 254 | SendGameOptions 255 | } 256 | 257 | type setGameScoreParams struct { 258 | UserID int64 `json:"user_id"` 259 | Score int `json:"score"` 260 | SetGameScoreOptions 261 | } 262 | 263 | type answerCallbackQueryParams struct { 264 | CallbackQueryID string `json:"callback_query_id"` 265 | AnswerCallbackQueryOptions 266 | } 267 | 268 | func newAnswerCallbackQueryParams(callbackQueryID string, options *AnswerCallbackQueryOptions) *answerCallbackQueryParams { 269 | params := &answerCallbackQueryParams{ 270 | CallbackQueryID: callbackQueryID, 271 | } 272 | 273 | if options != nil { 274 | params.AnswerCallbackQueryOptions = *options 275 | } 276 | 277 | return params 278 | } 279 | 280 | type editMessageTextParams struct { 281 | ChatID ChatID `json:"chat_id,omitempty"` 282 | MessageID int64 `json:"message_id,omitempty"` 283 | InlineMessageID string `json:"inline_message_id,omitempty"` 284 | Text string `json:"text"` 285 | EditMessageTextOptions 286 | } 287 | 288 | type editMessageCationParams struct { 289 | ChatID ChatID `json:"chat_id,omitempty"` 290 | MessageID int64 `json:"message_id,omitempty"` 291 | InlineMessageID string `json:"inline_message_id,omitempty"` 292 | EditMessageCationOptions 293 | } 294 | 295 | type editMessageReplyMarkupParams struct { 296 | ChatID ChatID `json:"chat_id,omitempty"` 297 | MessageID int64 `json:"message_id,omitempty"` 298 | InlineMessageID string `json:"inline_message_id,omitempty"` 299 | ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` 300 | } 301 | 302 | type answerInlineQueryParams struct { 303 | InlineQueryID string `json:"inline_query_id"` 304 | Results InlineQueryResults `json:"results"` 305 | AnswerInlineQueryOptions 306 | } 307 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package micha 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | ) 7 | 8 | type Options struct { 9 | limit int 10 | timeout int 11 | logger Logger 12 | apiServer string 13 | httpClient HttpClient 14 | ctx context.Context 15 | } 16 | 17 | type Option func(*Options) 18 | 19 | // WithLimit - set getUpdates limit 20 | // Values between 1—100 are accepted. Defaults to 100. 21 | func WithLimit(limit int) Option { 22 | return func(o *Options) { 23 | o.limit = limit 24 | } 25 | } 26 | 27 | // WithTimeout - set timeout in seconds for getUpdates long polling 28 | // Defaults to 25 29 | func WithTimeout(timeout int) Option { 30 | return func(o *Options) { 31 | o.timeout = timeout 32 | } 33 | } 34 | 35 | // WithLogger - set logger 36 | func WithLogger(logger Logger) Option { 37 | return func(o *Options) { 38 | o.logger = logger 39 | } 40 | } 41 | 42 | // WithHttpClient - set custom http client 43 | func WithHttpClient(httpClient HttpClient) Option { 44 | return func(o *Options) { 45 | o.httpClient = httpClient 46 | } 47 | } 48 | 49 | // WithAPIServer - set custom api server (https://github.com/tdlib/telegram-bot-api) 50 | func WithAPIServer(url string) Option { 51 | return func(o *Options) { 52 | o.apiServer = strings.TrimSuffix(url, "/") 53 | } 54 | } 55 | 56 | // WithAPIServer - set custom context 57 | func WithCtx(ctx context.Context) Option { 58 | return func(o *Options) { 59 | o.ctx = ctx 60 | } 61 | } 62 | 63 | // SendMessageOptions optional params SendMessage method 64 | type SendMessageOptions struct { 65 | ParseMode ParseMode `json:"parse_mode,omitempty"` 66 | DisableWebPagePreview bool `json:"disable_web_page_preview,omitempty"` 67 | DisableNotification bool `json:"disable_notification,omitempty"` 68 | ReplyToMessageID int64 `json:"reply_to_message_id,omitempty"` 69 | ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` 70 | } 71 | 72 | // SendPhotoOptions optional params SendPhoto method 73 | type SendPhotoOptions struct { 74 | Caption string `json:"caption,omitempty"` 75 | ParseMode ParseMode `json:"parse_mode,omitempty"` 76 | DisableNotification bool `json:"disable_notification,omitempty"` 77 | ReplyToMessageID int64 `json:"reply_to_message_id,omitempty"` 78 | ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` 79 | } 80 | 81 | // SendAudioOptions optional params SendAudio method 82 | type SendAudioOptions struct { 83 | Caption string `json:"caption,omitempty"` 84 | ParseMode ParseMode `json:"parse_mode,omitempty"` 85 | Duration int `json:"duration,omitempty"` 86 | Performer string `json:"performer,omitempty"` 87 | Title string `json:"title,omitempty"` 88 | Thumb string `json:"thumb,omitempty"` // TODO add thumb as file 89 | DisableNotification bool `json:"disable_notification,omitempty"` 90 | ProtectContent bool `json:"protect_content,omitempty"` 91 | ReplyToMessageID int64 `json:"reply_to_message_id,omitempty"` 92 | ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` 93 | } 94 | 95 | // SendDocumentOptions optional params SendDocument method 96 | type SendDocumentOptions struct { 97 | Thumb string `json:"thumb,omitempty"` // TODO add thumb as file 98 | Caption string `json:"caption,omitempty"` 99 | ParseMode ParseMode `json:"parse_mode,omitempty"` 100 | DisableNotification bool `json:"disable_notification,omitempty"` 101 | ReplyToMessageID int64 `json:"reply_to_message_id,omitempty"` 102 | ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` 103 | } 104 | 105 | // Send sticker optional params 106 | type SendStickerOptions struct { 107 | DisableNotification bool `json:"disable_notification,omitempty"` 108 | ReplyToMessageID int64 `json:"reply_to_message_id,omitempty"` 109 | ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` 110 | } 111 | 112 | // SendVideoOptions video optional params SendVideo method 113 | type SendVideoOptions struct { 114 | Duration int `json:"duration,omitempty"` 115 | Width int `json:"width,omitempty"` 116 | Height int `json:"height,omitempty"` 117 | Thumb string `json:"thumb,omitempty"` // TODO add thumb as file 118 | Caption string `json:"caption,omitempty"` 119 | ParseMode ParseMode `json:"parse_mode,omitempty"` 120 | SupportsStreaming bool `json:"supports_streaming,omitempty"` 121 | DisableNotification bool `json:"disable_notification,omitempty"` 122 | ReplyToMessageID int64 `json:"reply_to_message_id,omitempty"` 123 | ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` 124 | } 125 | 126 | // SendVoiceOptions optional params for SendVoice method 127 | type SendVoiceOptions struct { 128 | Caption string `json:"caption,omitempty"` 129 | ParseMode ParseMode `json:"parse_mode,omitempty"` 130 | Duration int `json:"duration,omitempty"` 131 | DisableNotification bool `json:"disable_notification,omitempty"` 132 | ReplyToMessageID int64 `json:"reply_to_message_id,omitempty"` 133 | ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` 134 | } 135 | 136 | // SendVideoNoteOptions optional params for SendVideoNote method 137 | type SendVideoNoteOptions struct { 138 | Duration int `json:"duration,omitempty"` 139 | Length int `json:"length,omitempty"` 140 | Thumb string `json:"thumb,omitempty"` // TODO add thumb as file 141 | DisableNotification bool `json:"disable_notification,omitempty"` 142 | ReplyToMessageID int64 `json:"reply_to_message_id,omitempty"` 143 | ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` 144 | } 145 | 146 | // SendLocationOptions optional params for SendLocation method 147 | type SendLocationOptions struct { 148 | LivePeriod int `json:"live_period,omitempty"` 149 | DisableNotification bool `json:"disable_notification,omitempty"` 150 | ReplyToMessageID int64 `json:"reply_to_message_id,omitempty"` 151 | ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` 152 | } 153 | 154 | // SendVenueOptions optional params for SendVenue method 155 | type SendVenueOptions struct { 156 | FoursquareID string `json:"foursquare_id,omitempty"` 157 | FoursquareType string `json:"foursquare_type,omitempty"` 158 | DisableNotification bool `json:"disable_notification,omitempty"` 159 | ReplyToMessageID int64 `json:"reply_to_message_id,omitempty"` 160 | ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` 161 | } 162 | 163 | // SendContactOptions optional params for SendContact method 164 | type SendContactOptions struct { 165 | VCard string `json:"vcard,omitempty"` 166 | DisableNotification bool `json:"disable_notification,omitempty"` 167 | ReplyToMessageID int64 `json:"reply_to_message_id,omitempty"` 168 | ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` 169 | } 170 | 171 | // SendGameOptions optional params for SendGame method 172 | type SendGameOptions struct { 173 | DisableNotification bool `json:"disable_notification,omitempty"` 174 | ReplyToMessageID int64 `json:"reply_to_message_id,omitempty"` 175 | ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` 176 | } 177 | 178 | // Set game score optional params 179 | type SetGameScoreOptions struct { 180 | ChatID ChatID `json:"chat_id,omitempty"` 181 | MessageID int64 `json:"message_id,omitempty"` 182 | InlineMessageID string `json:"inline_message_id,omitempty"` 183 | DisableEditMessage bool `json:"disable_edit_message,omitempty"` 184 | Force bool `json:"force,omitempty"` 185 | } 186 | 187 | // Get game high scopres optional params 188 | type GetGameHighScoresOptions struct { 189 | ChatID ChatID `json:"chat_id,omitempty"` 190 | MessageID int64 `json:"message_id,omitempty"` 191 | InlineMessageID string `json:"inline_message_id,omitempty"` 192 | } 193 | 194 | // Edit message text optional params 195 | type EditMessageTextOptions struct { 196 | ParseMode ParseMode `json:"parse_mode,omitempty"` 197 | DisableWebPagePreview bool `json:"disable_web_page_preview,omitempty"` 198 | ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` 199 | } 200 | 201 | // Edit message caption optional params 202 | type EditMessageCationOptions struct { 203 | Caption string `json:"caption,omitempty"` 204 | ReplyMarkup ReplyMarkup `json:"reply_markup,omitempty"` 205 | } 206 | 207 | // Answer callback query optional params 208 | type AnswerCallbackQueryOptions struct { 209 | Text string `json:"text,omitempty"` 210 | ShowAlert bool `json:"show_alert,omitempty"` 211 | URL string `json:"url,omitempty"` 212 | CacheTime int `json:"cache_time,omitempty"` 213 | } 214 | 215 | // Answer inline query optional params 216 | type AnswerInlineQueryOptions struct { 217 | CacheTime int `json:"cache_time,omitempty"` 218 | IsPersonal bool `json:"is_personal,omitempty"` 219 | NextOffset string `json:"next_offset,omitempty"` 220 | SwitchPmText string `json:"switch_pm_text,omitempty"` 221 | SwitchPmParameter string `json:"switch_pm_parameter,omitempty"` 222 | } 223 | 224 | // Set webhook query optional params 225 | type SetWebhookOptions struct { 226 | Certificate []byte `json:"certificate,omitempty"` 227 | MaxConnections int `json:"max_connections,omitempty"` 228 | AllowedUpdates []string `json:"allowed_updates,omitempty"` 229 | } 230 | -------------------------------------------------------------------------------- /types.go: -------------------------------------------------------------------------------- 1 | package micha 2 | 3 | const ( 4 | PARSE_MODE_DEFAULT ParseMode = "" 5 | PARSE_MODE_HTML ParseMode = "HTML" 6 | PARSE_MODE_MARKDOWN ParseMode = "Markdown" 7 | 8 | CHAT_TYPE_PRIVATE ChatType = "private" 9 | CHAT_TYPE_GROUP ChatType = "group" 10 | CHAT_TYPE_SUPERGROUP ChatType = "supergroup" 11 | CHAT_TYPE_CHANNEL ChatType = "channel" 12 | 13 | CHAT_ACTION_TYPING ChatAction = "typing" 14 | CHAT_ACTION_UPLOAD_PHOTO ChatAction = "upload_photo" 15 | CHAT_ACTION_RECORD_VIDEO ChatAction = "record_video" 16 | CHAT_ACTION_UPLOAD_VIDEO ChatAction = "upload_video" 17 | CHAT_ACTION_RECORD_AUDIO ChatAction = "record_audio" 18 | CHAT_ACTION_UPLOAD_AUDIO ChatAction = "upload_audio" 19 | CHAT_ACTION_UPLOAD_DOCUMENT ChatAction = "upload_document" 20 | CHAT_ACTION_FIND_LOCATION ChatAction = "find_location" 21 | CHAT_ACTION_RECORD_VIDEO_NOTE ChatAction = "record_video_note" 22 | CHAT_ACTION_UPLOAD_VIDEO_NOTE ChatAction = "upload_video_note" 23 | 24 | MEMBER_STATUS_CREATOR MemberStatus = "creator" 25 | MEMBER_STATUS_ADMINISTRATOR MemberStatus = "administrator" 26 | MEMBER_STATUS_MEMBER MemberStatus = "member" 27 | MEMBER_STATUS_LEFT MemberStatus = "left" 28 | MEMBER_STATUS_KICKED MemberStatus = "kicked" 29 | 30 | MESSAGE_ENTITY_MENTION MessageEntityType = "mention" 31 | MESSAGE_ENTITY_HASHTAG MessageEntityType = "hashtag" 32 | MESSAGE_ENTITY_BOT_COMMAND MessageEntityType = "bot_command" 33 | MESSAGE_ENTITY_URL MessageEntityType = "url" 34 | MESSAGE_ENTITY_EMAIL MessageEntityType = "email" 35 | MESSAGE_ENTITY_BOLD MessageEntityType = "bold" 36 | MESSAGE_ENTITY_ITALIC MessageEntityType = "italic" 37 | MESSAGE_ENTITY_CODE MessageEntityType = "code" 38 | MESSAGE_ENTITY_PRE MessageEntityType = "pre" 39 | MESSAGE_ENTITY_TEXT_LINK MessageEntityType = "text_link" 40 | MESSAGE_ENTITY_TEXT_MENTION MessageEntityType = "text_mention" 41 | ) 42 | 43 | type ParseMode string 44 | type ChatType string 45 | type ChatAction string 46 | type MemberStatus string 47 | type MessageEntityType string 48 | 49 | // User object represents a Telegram user, bot 50 | type User struct { 51 | ID int64 `json:"id"` 52 | IsBot bool `json:"is_bot"` 53 | FirstName string `json:"first_name"` 54 | LastName string `json:"last_name"` 55 | Username string `json:"username"` 56 | LanguageCode string `json:"language_code"` 57 | } 58 | 59 | type ChatID string 60 | 61 | func (chatID *ChatID) UnmarshalJSON(value []byte) error { 62 | *chatID = ChatID(value) 63 | return nil 64 | } 65 | 66 | // Chat object represents a chat. 67 | type Chat struct { 68 | ID ChatID `json:"id"` 69 | Type ChatType `json:"type"` 70 | 71 | // Optional 72 | Title string `json:"title,omitempty"` 73 | Username string `json:"username,omitempty"` 74 | FirstName string `json:"first_name,omitempty"` 75 | LastName string `json:"last_name,omitempty"` 76 | Photo *ChatPhoto `json:"photo,omitempty"` 77 | Description string `json:"description,omitempty"` 78 | InviteLink string `json:"invite_link,omitempty"` 79 | PinnedMessage *Message `json:"pinned_message,omitempty"` 80 | Permissions *ChatPermissions `json:"permissions,omitempty"` 81 | StickerSetName string `json:"sticker_set_name,omitempty"` 82 | CanSetStickerSet bool `json:"can_set_sticker_set,omitempty"` 83 | } 84 | 85 | // Message object represents a message. 86 | type Message struct { 87 | MessageID int64 `json:"message_id"` 88 | From User `json:"from"` 89 | Date uint64 `json:"date"` 90 | Chat Chat `json:"chat"` 91 | 92 | // Optional 93 | ForwardFrom *User `json:"forward_from,omitempty"` 94 | ForwardFromChat *Chat `json:"forward_from_chat,omitempty"` 95 | ForwardFromMessageID int64 `json:"forward_from_message_id,omitempty"` 96 | ForwardSignature string `json:"forward_signature,omitempty"` 97 | ForwardSenderName string `json:"forward_sender_name,omitempty"` 98 | ForwardDate uint64 `json:"forward_date,omitempty"` 99 | ReplyToMessage *Message `json:"reply_to_message,omitempty"` 100 | EditDate uint64 `json:"edit_date,omitempty"` 101 | MediaGroupID string `json:"media_group_id,omitempty"` 102 | AuthorSignature string `json:"author_signature,omitempty"` 103 | Text string `json:"text,omitempty"` 104 | Entities []MessageEntity `json:"entities,omitempty"` 105 | CaptionEntities []MessageEntity `json:"caption_entities,omitempty"` 106 | Audio *Audio `json:"audio,omitempty"` 107 | Document *Document `json:"document,omitempty"` 108 | Animation *Animation `json:"animation,omitempty"` 109 | Game *Game `json:"game,omitempty"` 110 | Photo []PhotoSize `json:"photo,omitempty"` 111 | Sticker *Sticker `json:"sticker,omitempty"` 112 | Video *Video `json:"video,omitempty"` 113 | Voice *Voice `json:"voice,omitempty"` 114 | VideoNote *VideoNote `json:"video_note,omitempty"` 115 | Caption string `json:"caption,omitempty"` 116 | Contact *Contact `json:"contact,omitempty"` 117 | Location *Location `json:"location,omitempty"` 118 | Venue *Venue `json:"venue,omitempty"` 119 | Poll *Poll `json:"poll,omitempty"` 120 | NewChatMembers []User `json:"new_chat_members,omitempty"` 121 | LeftChatMember *User `json:"left_chat_member,omitempty"` 122 | NewChatTitle string `json:"new_chat_title,omitempty"` 123 | NewChatPhoto []PhotoSize `json:"new_chat_photo,omitempty"` 124 | DeleteChatPhoto bool `json:"delete_chat_photo,omitempty"` 125 | GroupChatCreated bool `json:"group_chat_created,omitempty"` 126 | SupergroupChatCreated bool `json:"supergroup_chat_created,omitempty"` 127 | ChannelChatCreated bool `json:"channel_chat_created,omitempty"` 128 | MigrateToChatID ChatID `json:"migrate_to_chat_id,omitempty"` 129 | MigrateFromChatID ChatID `json:"migrate_from_chat_id,omitempty"` 130 | PinnedMessage *Message `json:"pinned_message,omitempty"` 131 | Invoice *Invoice `json:"invoice,omitempty"` 132 | SuccessfulPayment *SuccessfulPayment `json:"successful_payment,omitempty"` 133 | ConnectedWebsite string `json:"connected_website,omitempty"` 134 | PassportData *PassportData `json:"passport_data,omitempty"` 135 | ReplyMarkup InlineKeyboardMarkup `json:"reply_markup,omitempty"` 136 | } 137 | 138 | // MessageEntity object represents one special entity in a text message. For example, hashtags, usernames, URLs, etc. 139 | type MessageEntity struct { 140 | Type MessageEntityType `json:"type"` 141 | Offset int `json:"offset"` 142 | Limit int `json:"limit"` 143 | 144 | // Optional 145 | URL string `json:"url,omitempty"` // For “text_link” only, url that will be opened after user taps on the text 146 | User *User `json:"user,omitempty"` // For “text_mention” only, the mentioned user 147 | } 148 | 149 | // PhotoSize object represents an image/sticker of a particular size. 150 | type PhotoSize struct { 151 | FileID string `json:"file_id"` 152 | Width int `json:"width"` 153 | Height int `json:"height"` 154 | 155 | // Optional 156 | FileSize uint64 `json:"file_size,omitempty"` 157 | } 158 | 159 | // Audio object represents an audio file (voice note). 160 | type Audio struct { 161 | FileID string `json:"file_id"` 162 | Duration int `json:"duration"` 163 | 164 | // Optional 165 | Performer string `json:"performer,omitempty"` 166 | Title string `json:"title,omitempty"` 167 | MimeType string `json:"mime_type,omitempty"` 168 | Thumb *PhotoSize `json:"thumb,omitempty"` 169 | FileSize uint64 `json:"file_size,omitempty"` 170 | } 171 | 172 | // Document object represents a general file (as opposed to Photo or Audio). 173 | // Telegram users can send files of any type of up to 1.5 GB in size. 174 | type Document struct { 175 | FileID string `json:"file_id"` 176 | 177 | // Optional 178 | Thumb *PhotoSize `json:"thumb,omitempty"` 179 | FileName string `json:"file_name,omitempty"` 180 | MimeType string `json:"mime_type,omitempty"` 181 | FileSize uint64 `json:"file_size,omitempty"` 182 | } 183 | 184 | // Video object represents an MP4-encoded video. 185 | type Video struct { 186 | FileID string `json:"file_id"` 187 | Width int `json:"width"` 188 | Height int `json:"height"` 189 | Duration int `json:"duration"` 190 | 191 | // Optional 192 | Thumb *PhotoSize `json:"thumb,omitempty"` 193 | MimeType string `json:"mime_type,omitempty"` 194 | FileSize uint64 `json:"file_size,omitempty"` 195 | } 196 | 197 | // Animation object represents an animation file (GIF or H.264/MPEG-4 AVC video without sound). 198 | type Animation struct { 199 | FileID string `json:"file_id"` 200 | Width int `json:"width"` 201 | Height int `json:"height"` 202 | Duration int `json:"duration"` 203 | 204 | // Optional 205 | Thumb *PhotoSize `json:"thumb,omitempty"` 206 | FileName string `json:"file_name,omitempty"` 207 | MimeType string `json:"mime_type,omitempty"` 208 | FileSize *int `json:"file_size,omitempty"` 209 | } 210 | 211 | // Voice object represents a voice note. 212 | type Voice struct { 213 | FileID string `json:"file_id"` 214 | Duration int `json:"duration"` 215 | 216 | // Optional 217 | MimeType string `json:"mime_type,omitempty"` 218 | FileSize int `json:"file_size,omitempty"` 219 | } 220 | 221 | // VideoNote object represents a video message. 222 | type VideoNote struct { 223 | FileID string `json:"file_id"` 224 | Length int `json:"length"` 225 | Duration int `json:"duration"` 226 | 227 | // Optional 228 | Thumb *PhotoSize `json:"thumb,omitempty"` 229 | FileSize int `json:"file_size,omitempty"` 230 | } 231 | 232 | // Contact object represents a contact to Telegram user 233 | type Contact struct { 234 | PhoneNumber string `json:"phone_number"` 235 | FirstName string `json:"first_name"` 236 | 237 | // Optional 238 | LastName string `json:"last_name,omitempty"` 239 | UserID int64 `json:"user_id,omitempty"` 240 | VCard string `json:"vcard,omitempty"` 241 | } 242 | 243 | // Location object represents geographic position. 244 | type Location struct { 245 | Longitude float64 `json:"longitude"` 246 | Latitude float64 `json:"latitude"` 247 | } 248 | 249 | // Venue object represents a venue. 250 | type Venue struct { 251 | Location Location `json:"location"` 252 | Title string `json:"title"` 253 | Address string `json:"address"` 254 | 255 | // Optional 256 | FoursquareID string `json:"foursquare_id,omitempty"` 257 | FoursquareType string `json:"foursquare_type,omitempty"` 258 | } 259 | 260 | // PollOption object contains information about one answer option in a poll. 261 | type PollOption struct { 262 | Text string `json:"text"` 263 | VoterCount int `json:"voter_count"` 264 | } 265 | 266 | // Poll object contains information about a poll. 267 | type Poll struct { 268 | ID string `json:"id"` 269 | Question string `json:"question"` 270 | Options []PollOption `json:"options"` 271 | IsClosed bool `json:"is_closed"` 272 | } 273 | 274 | // UserProfilePhotos object represent a user's profile pictures. 275 | type UserProfilePhotos struct { 276 | TotalCount int `json:"total_count"` 277 | Photos [][]PhotoSize `json:"photos"` 278 | } 279 | 280 | // File object represents a file ready to be downloaded. 281 | // The file can be downloaded via the link https://api.telegram.org/file/bot/. 282 | // It is guaranteed that the link will be valid for at least 1 hour. 283 | // When the link expires, a new one can be requested by calling getFile. 284 | type File struct { 285 | FileID string `json:"file_id"` 286 | 287 | // Optional 288 | FileSize uint64 `json:"file_size,omitempty"` 289 | FilePath string `json:"file_path,omitempty"` 290 | } 291 | 292 | type ReplyMarkup interface { 293 | itsReplyMarkup() 294 | } 295 | 296 | type replyMarkupImplementation struct{} 297 | 298 | func (r replyMarkupImplementation) itsReplyMarkup() {} 299 | 300 | // KeyboardButton object represents one button of the reply keyboard. 301 | // For simple text buttons String can be used instead of this object to specify text of the button. 302 | // Optional fields are mutually exclusive. 303 | type KeyboardButton struct { 304 | Text string `json:"text"` 305 | 306 | // Optional 307 | RequestContact bool `json:"request_contact,omitempty"` 308 | RequestLocation bool `json:"request_location,omitempty"` 309 | } 310 | 311 | // ReplyKeyboardMarkup object represents a custom keyboard with reply options 312 | type ReplyKeyboardMarkup struct { 313 | replyMarkupImplementation 314 | Keyboard [][]KeyboardButton `json:"keyboard"` 315 | ResizeKeyboard bool `json:"resize_keyboard,omitempty"` 316 | OneTimeKeyboard bool `json:"one_time_keyboard,omitempty"` 317 | Selective bool `json:"selective,omitempty"` 318 | } 319 | 320 | // ReplyKeyboardRemove object 321 | // Upon receiving a message with this object, Telegram clients will remove the current custom keyboard and display the default letter-keyboard. 322 | // By default, custom keyboards are displayed until a new keyboard is sent by a bot. 323 | // An exception is made for one-time keyboards that are hidden immediately after the user presses a button 324 | type ReplyKeyboardRemove struct { 325 | replyMarkupImplementation 326 | RemoveKeyboard bool `json:"remove_keyboard,omitempty"` 327 | Selective bool `json:"selective,omitempty"` 328 | } 329 | 330 | // InlineKeyboardButton object represents one button of an inline keyboard. You must use exactly one of the optional fields. 331 | type InlineKeyboardButton struct { 332 | Text string `json:"text,omitempty"` 333 | 334 | // Optional 335 | URL string `json:"url,omitempty"` 336 | LoginURL *LoginURL `json:"login_url,omitempty"` 337 | CallbackData string `json:"callback_data,omitempty"` 338 | SwitchInlineQuery string `json:"switch_inline_query,omitempty"` 339 | SwitchInlineQueryCurrentChat string `json:"switch_inline_query_current_chat,omitempty"` 340 | Pay bool `json:"pay,omitempty"` 341 | } 342 | 343 | // LoginURL object represents a parameter of the inline keyboard button used to automatically authorize a user. 344 | // Serves as a great replacement for the Telegram Login Widget when the user is coming from Telegram. 345 | // All the user needs to do is tap/click a button and confirm that they want to log in: 346 | type LoginURL struct { 347 | URL string `json:"url"` 348 | 349 | // Optional 350 | ForwardText string `json:"forward_text,omitempty"` 351 | BotUsername string `json:"bot_username,omitempty"` 352 | RequestWriteAccess bool `json:"request_write_access,omitempty"` 353 | } 354 | 355 | // InlineKeyboardMarkup object represents an inline keyboard that appears right next to the message it belongs to. 356 | type InlineKeyboardMarkup struct { 357 | replyMarkupImplementation 358 | InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard"` 359 | } 360 | 361 | // ChosenInlineResult object represents a result of an inline query that was chosen by the user and sent to their chat partner. 362 | type ChosenInlineResult struct { 363 | ResultID string `json:"result_id"` 364 | From User `json:"from"` 365 | Location *Location `json:"location"` 366 | InlineMessageID string `json:"inline_message_id"` 367 | Query string `json:"query"` 368 | } 369 | 370 | // CallbackQuery object represents an incoming callback query from a callback button in an inline keyboard. 371 | // If the button that originated the query was attached to a message sent by the bot, the field message will be presented. 372 | // If the button was attached to a message sent via the bot (in inline mode), the field inline_message_id will be presented. 373 | type CallbackQuery struct { 374 | ID string `json:"id"` 375 | From User `json:"from"` 376 | 377 | // Optional 378 | Message *Message `json:"message,omitempty"` 379 | InlineMessageID string `json:"inline_message_id,omitempty"` 380 | ChatInstance string `json:"chat_instance,omitempty"` 381 | Data string `json:"data,omitempty"` 382 | GameShortName string `json:"game_short_name,omitempty"` 383 | } 384 | 385 | // ForceReply object 386 | // Upon receiving a message with this object, 387 | // Telegram clients will display a reply interface to the user. 388 | // This can be extremely useful if you want to create user-friendly step-by-step interfaces without having to sacrifice privacy mode. 389 | type ForceReply struct { 390 | replyMarkupImplementation 391 | ForceReply bool `json:"force_reply"` 392 | Selective bool `json:"selective,omitempty"` 393 | } 394 | 395 | // ChatPhoto object represents a chat photo. 396 | type ChatPhoto struct { 397 | SmallFileID string `json:"small_file_id"` 398 | BigFileID string `json:"big_file_id"` 399 | } 400 | 401 | // ChatMember object contains information about one member of a chat. 402 | type ChatMember struct { 403 | User User `json:"user"` 404 | Status MemberStatus `json:"status"` 405 | 406 | // Optional 407 | UntilDate int64 `json:"until_date,omitempty"` 408 | CanBeEdited bool `json:"can_be_edited,omitempty"` 409 | CanPostMessages bool `json:"can_post_messages,omitempty"` 410 | CanEditMessages bool `json:"can_edit_messages,omitempty"` 411 | CanDeleteMessages bool `json:"can_delete_messages,omitempty"` 412 | CanRestrictMembers bool `json:"can_restrict_members,omitempty"` 413 | CanPromoteMembers bool `json:"can_promote_members,omitempty"` 414 | CanChangeInfo bool `json:"can_change_info,omitempty"` 415 | CanInviteUsers bool `json:"can_invite_users,omitempty"` 416 | CanPinMessages bool `json:"can_pin_messages,omitempty"` 417 | IsMember bool `json:"is_member,omitempty"` 418 | CanSendMessages bool `json:"can_send_messages,omitempty"` 419 | CanSendMediaMessages bool `json:"can_send_media_messages,omitempty"` 420 | CanSendPolls bool `json:"can_send_polls,omitempty"` 421 | CanSendOtherMessages bool `json:"can_send_other_messages,omitempty"` 422 | CanAddWebPagePreviews bool `json:"can_add_web_page_previews,omitempty"` 423 | } 424 | 425 | // ChatPermissions describes actions that a non-administrator user is allowed to take in a chat. 426 | type ChatPermissions struct { 427 | CanSendMessages bool `json:"can_send_messages,omitempty"` 428 | CanSendMediaMessages bool `json:"can_send_media_messages,omitempty"` 429 | CanSendPolls bool `json:"can_send_polls,omitempty"` 430 | CanSendOtherMessages bool `json:"can_send_other_messages,omitempty"` 431 | CanAddWebPagePreviews bool `json:"can_add_web_page_previews,omitempty"` 432 | CanChangeInfo bool `json:"can_change_info,omitempty"` 433 | CanInviteUsers bool `json:"can_invite_users,omitempty"` 434 | CanPinMessages bool `json:"can_pin_messages,omitempty"` 435 | } 436 | 437 | // ResponseParameters contains information about why a request was unsuccessful. 438 | type ResponseParameters struct { 439 | MigrateToChatID ChatID `json:"migrate_to_chat_id,omitempty"` 440 | RetryAfter int64 `json:"retry_after,omitempty"` 441 | } 442 | 443 | // Update object represents an incoming update. 444 | // Only one of the optional parameters can be present in any given update. 445 | type Update struct { 446 | UpdateID uint64 `json:"update_id"` 447 | 448 | // Optional 449 | Message *Message `json:"message,omitempty"` 450 | EditedMessage *Message `json:"edited_message,omitempty"` 451 | ChannelPost *Message `json:"channel_post,omitempty"` 452 | EditedChannelPost *Message `json:"edited_channel_post,omitempty"` 453 | InlineQuery *InlineQuery `json:"inline_query,omitempty"` 454 | ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result,omitempty"` 455 | CallbackQuery *CallbackQuery `json:"callback_query,omitempty"` 456 | ShippingQuery *ShippingQuery `json:"shipping_query,omitempty"` 457 | PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query,omitempty"` 458 | Poll *Poll `json:"poll,omitempty"` 459 | } 460 | 461 | // WebhookInfo contains information about the current status of a webhook. 462 | type WebhookInfo struct { 463 | URL string `json:"url"` 464 | HasCustomCertificate bool `json:"has_custom_certificate"` 465 | PendingUpdateCount int `json:"pending_update_count"` 466 | LastErrorDate uint64 `json:"last_error_date,omitempty"` 467 | LastErrorMessage string `json:"last_error_message,omitempty"` 468 | MaxConnections int `json:"max_connections,omitempty"` 469 | AllowedUpdates []string `json:"allowed_updates,omitempty"` 470 | } 471 | -------------------------------------------------------------------------------- /types_inline.go: -------------------------------------------------------------------------------- 1 | package micha 2 | 3 | const ( 4 | INLINE_TYPE_RESULT_ARTICLE InlineResultType = "article" 5 | INLINE_TYPE_RESULT_PHOTO InlineResultType = "photo" 6 | INLINE_TYPE_RESULT_GIF InlineResultType = "gif" 7 | INLINE_TYPE_RESULT_MPEG4_GIF InlineResultType = "mpeg4_gif" 8 | INLINE_TYPE_RESULT_VIDEO InlineResultType = "video" 9 | INLINE_TYPE_RESULT_AUDIO InlineResultType = "audio" 10 | INLINE_TYPE_RESULT_VOICE InlineResultType = "voice" 11 | INLINE_TYPE_RESULT_DOCUMENT InlineResultType = "document" 12 | INLINE_TYPE_RESULT_LOCATION InlineResultType = "location" 13 | INLINE_TYPE_RESULT_VENUE InlineResultType = "venue" 14 | INLINE_TYPE_RESULT_CONTACT InlineResultType = "contact" 15 | INLINE_TYPE_RESULT_STICKER InlineResultType = "sticker" 16 | INLINE_TYPE_RESULT_GAME InlineResultType = "game" 17 | ) 18 | 19 | // InlineQuery object represents an incoming inline query. 20 | // When the user sends an empty query, your bot could return some default or trending results. 21 | type InlineQuery struct { 22 | ID string `json:"id"` 23 | From User `json:"from"` 24 | Location *Location `json:"location,omitempty"` 25 | Query string `json:"query"` 26 | Offset string `json:"offset"` 27 | } 28 | 29 | type InlineResultType string 30 | 31 | type InlineQueryResults []InlineQueryResult 32 | 33 | type InlineQueryResult interface { 34 | itsInlineQueryResult() 35 | } 36 | 37 | type inlineQueryResultImplementation struct{} 38 | 39 | func (i inlineQueryResultImplementation) itsInlineQueryResult() {} 40 | 41 | // Represents a link to an article or web page. 42 | type InlineQueryResultArticle struct { 43 | inlineQueryResultImplementation 44 | Type InlineResultType `json:"type"` 45 | ID string `json:"id"` 46 | Title string `json:"title"` 47 | 48 | // Optional 49 | URL string `json:"url,omitempty"` 50 | HideURL bool `json:"hide_url,omitempty"` 51 | Description string `json:"description,omitempty"` 52 | ThumbURL string `json:"thumb_url,omitempty"` 53 | ThumbWidth int `json:"thumb_width,omitempty"` 54 | ThumbHeight int `json:"thumb_height,omitempty"` 55 | ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` 56 | InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` 57 | } 58 | 59 | // Represents a link to a photo. 60 | // By default, this photo will be sent by the user with optional caption. 61 | // Alternatively, you can use input_message_content to send a message with the specified content instead of the photo. 62 | type InlineQueryResultPhoto struct { 63 | inlineQueryResultImplementation 64 | Type InlineResultType `json:"type"` 65 | ID string `json:"id"` 66 | PhotoURL string `json:"photo_url"` 67 | 68 | // Optional 69 | MimeType string `json:"mime_type,omitempty"` 70 | PhotoWidth int `json:"photo_width,omitempty"` 71 | PhotoHeight int `json:"photo_height,omitempty"` 72 | ThumbURL string `json:"thumb_url,omitempty"` 73 | Title string `json:"title,omitempty"` 74 | Description string `json:"description,omitempty"` 75 | Caption string `json:"caption,omitempty"` 76 | ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` 77 | InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` 78 | } 79 | 80 | // Represents a link to a photo stored on the Telegram servers. 81 | // By default, this photo will be sent by the user with an optional caption. 82 | // Alternatively, you can use input_message_content to send a message with the specified content instead of the photo. 83 | type InlineQueryResultCachedPhoto struct { 84 | inlineQueryResultImplementation 85 | Type InlineResultType `json:"type"` 86 | ID string `json:"id"` 87 | PhotoFileID string `json:"photo_file_id"` 88 | 89 | // Optional 90 | Title string `json:"title,omitempty"` 91 | Description string `json:"description,omitempty"` 92 | Caption string `json:"caption,omitempty"` 93 | ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` 94 | InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` 95 | } 96 | 97 | // Represents a link to an animated GIF file. 98 | // By default, this animated GIF file will be sent by the user with optional caption. 99 | // Alternatively, you can use input_message_content to send a message with the specified content instead of the animation. 100 | type InlineQueryResultGif struct { 101 | inlineQueryResultImplementation 102 | Type InlineResultType `json:"type"` 103 | ID string `json:"id"` 104 | GifURL string `json:"gif_url"` 105 | 106 | // Optional 107 | GifWidth int `json:"gif_width,omitempty"` 108 | GifHeight int `json:"gif_height,omitempty"` 109 | GifDuration int `json:"gif_duration"` 110 | ThumbURL string `json:"thumb_url,omitempty"` 111 | Title string `json:"title,omitempty"` 112 | Caption string `json:"caption,omitempty"` 113 | ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` 114 | InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` 115 | } 116 | 117 | // Represents a link to an animated GIF file stored on the Telegram servers. 118 | // By default, this animated GIF file will be sent by the user with an optional caption. 119 | // Alternatively, you can use input_message_content to send a message with specified content instead of the animation. 120 | type InlineQueryResultCachedGif struct { 121 | inlineQueryResultImplementation 122 | Type InlineResultType `json:"type"` 123 | ID string `json:"id"` 124 | GifFileID string `json:"gif_file_id"` 125 | 126 | // Optional 127 | Title string `json:"title,omitempty"` 128 | Caption string `json:"caption,omitempty"` 129 | ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` 130 | InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` 131 | } 132 | 133 | // Represents a link to a video animation (H.264/MPEG-4 AVC video without sound). 134 | // By default, this animated MPEG-4 file will be sent by the user with optional caption. 135 | // Alternatively, you can use input_message_content to send a message with the specified content instead of the animation. 136 | type InlineQueryResultMpeg4Gif struct { 137 | inlineQueryResultImplementation 138 | Type InlineResultType `json:"type"` 139 | ID string `json:"id"` 140 | Mpeg4URL string `json:"mpeg4_url"` 141 | 142 | // Optional 143 | Mpeg4Width int `json:"mpeg4_width,omitempty"` 144 | Mpeg4Height int `json:"mpeg4_height,omitempty"` 145 | Mpeg4Duration int `json:"mpeg4_duration,omitempty"` 146 | ThumbURL string `json:"thumb_url,omitempty"` 147 | Title string `json:"title,omitempty"` 148 | Caption string `json:"caption,omitempty"` 149 | ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` 150 | InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` 151 | } 152 | 153 | // Represents a link to a video animation (H.264/MPEG-4 AVC video without sound) stored on the Telegram servers. 154 | // By default, this animated MPEG-4 file will be sent by the user with an optional caption. 155 | // Alternatively, you can use input_message_content to send a message with the specified content instead of the animation. 156 | type InlineQueryResultCachedMpeg4Gif struct { 157 | inlineQueryResultImplementation 158 | Type InlineResultType `json:"type"` 159 | ID string `json:"id"` 160 | Mpeg4FileID string `json:"mpeg4_file_id"` 161 | 162 | // Optional 163 | Title string `json:"title,omitempty"` 164 | Caption string `json:"caption,omitempty"` 165 | ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` 166 | InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` 167 | } 168 | 169 | // Represents a link to a page containing an embedded video player or a video file. 170 | // By default, this video file will be sent by the user with an optional caption. 171 | // Alternatively, you can use input_message_content to send a message with the specified content instead of the video. 172 | type InlineQueryResultVideo struct { 173 | inlineQueryResultImplementation 174 | Type InlineResultType `json:"type"` 175 | ID string `json:"id"` 176 | VideoURL string `json:"video_url"` 177 | MimeType string `json:"mime_type"` 178 | 179 | // Optional 180 | ThumbURL string `json:"thumb_url,omitempty"` 181 | Title string `json:"title,omitempty"` 182 | Caption string `json:"caption,omitempty"` 183 | VideoWidth int `json:"video_width,omitempty"` 184 | VideoHeight int `json:"video_height,omitempty"` 185 | VideoDuration int `json:"video_duration,omitempty"` 186 | Description string `json:"description,omitempty"` 187 | ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` 188 | InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` 189 | } 190 | 191 | // Represents a link to a video file stored on the Telegram servers. 192 | // By default, this video file will be sent by the user with an optional caption. 193 | // Alternatively, you can use input_message_content to send a message with the specified content instead of the video. 194 | type InlineQueryResultCachedVideo struct { 195 | inlineQueryResultImplementation 196 | Type InlineResultType `json:"type"` 197 | ID string `json:"id"` 198 | VideoFileID string `json:"video_file_id"` 199 | 200 | // Optional 201 | Title string `json:"title,omitempty"` 202 | Description string `json:"description,omitempty"` 203 | Caption string `json:"caption,omitempty"` 204 | ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` 205 | InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` 206 | } 207 | 208 | // Represents a link to an mp3 audio file. 209 | // By default, this audio file will be sent by the user. 210 | // Alternatively, you can use input_message_content to send a message with the specified content instead of the audio. 211 | type InlineQueryResultAudio struct { 212 | inlineQueryResultImplementation 213 | Type InlineResultType `json:"type"` 214 | ID string `json:"id"` 215 | AudioURL string `json:"audio_url"` 216 | Title string `json:"title"` 217 | 218 | // Optional 219 | Performer string `json:"performer,omitempty"` 220 | AudioDuration int `json:"audio_duration,omitempty"` 221 | ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` 222 | InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` 223 | } 224 | 225 | // Represents a link to an mp3 audio file stored on the Telegram servers. 226 | // By default, this audio file will be sent by the user. 227 | // Alternatively, you can use input_message_content to send a message with the specified content instead of the audio. 228 | type InlineQueryResultCachedAudio struct { 229 | inlineQueryResultImplementation 230 | Type InlineResultType `json:"type"` 231 | ID string `json:"id"` 232 | AudioFileID string `json:"audio_file_id"` 233 | 234 | // Optional 235 | ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` 236 | InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` 237 | } 238 | 239 | // Represents a link to a voice recording in an .ogg container encoded with OPUS. 240 | // By default, this voice recording will be sent by the user. 241 | // Alternatively, you can use input_message_content to send a message with the specified content instead of the the voice message. 242 | type InlineQueryResultVoice struct { 243 | inlineQueryResultImplementation 244 | Type InlineResultType `json:"type"` 245 | ID string `json:"id"` 246 | VoiceURL string `json:"voice_url"` 247 | Title string `json:"title"` 248 | 249 | // Optional 250 | VoiceDuration int `json:"voice_duration,omitempty"` 251 | ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` 252 | InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` 253 | } 254 | 255 | // Represents a link to a voice message stored on the Telegram servers. 256 | // By default, this voice message will be sent by the user. 257 | // Alternatively, you can use input_message_content to send a message with the specified content instead of the voice message. 258 | type InlineQueryResultCachedVoice struct { 259 | inlineQueryResultImplementation 260 | Type InlineResultType `json:"type"` 261 | ID string `json:"id"` 262 | VoiceFileID string `json:"voice_file_id"` 263 | 264 | // Optional 265 | Title string `json:"title"` 266 | ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` 267 | InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` 268 | } 269 | 270 | // Represents a link to a file. 271 | // By default, this file will be sent by the user with an optional caption. 272 | // Alternatively, you can use input_message_content to send a message with the specified content instead of the file. Currently, only .PDF and .ZIP files can be sent using this method. 273 | type InlineQueryResultDocument struct { 274 | inlineQueryResultImplementation 275 | Type InlineResultType `json:"type"` 276 | ID string `json:"id"` 277 | Title string `json:"title"` 278 | DocumentURL string `json:"document_url"` 279 | MimeType string `json:"mime_type"` 280 | 281 | // Optional 282 | Caption string `json:"caption,omitempty"` 283 | Description string `json:"description,omitempty"` 284 | ThumbURL string `json:"thumb_url,omitempty"` 285 | ThumbWidth int `json:"thumb_width,omitempty"` 286 | ThumbHeight int `json:"thumb_height,omitempty"` 287 | ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` 288 | InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` 289 | } 290 | 291 | // Represents a link to a file stored on the Telegram servers. 292 | // By default, this file will be sent by the user with an optional caption. 293 | // Alternatively, you can use input_message_content to send a message with the specified content instead of the file. 294 | // Currently, only pdf-files and zip archives can be sent using this method. 295 | type InlineQueryResultCachedDocument struct { 296 | inlineQueryResultImplementation 297 | Type InlineResultType `json:"type"` 298 | ID string `json:"id"` 299 | Title string `json:"title"` 300 | DocumentFileID string `json:"document_file_id"` 301 | 302 | // Optional 303 | Description string `json:"description,omitempty"` 304 | Caption string `json:"caption,omitempty"` 305 | ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` 306 | InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` 307 | } 308 | 309 | // Represents a location on a map. 310 | // By default, the location will be sent by the user. 311 | // Alternatively, you can use input_message_content to send a message with the specified content instead of the location. 312 | type InlineQueryResultLocation struct { 313 | inlineQueryResultImplementation 314 | Type InlineResultType `json:"type"` 315 | ID string `json:"id"` 316 | Latitude float64 `json:"latitude"` 317 | Longitude float64 `json:"longitude"` 318 | Title string `json:"title"` 319 | 320 | // Optional 321 | ThumbURL string `json:"thumb_url,omitempty"` 322 | ThumbWidth int `json:"thumb_width,omitempty"` 323 | ThumbHeight int `json:"thumb_height,omitempty"` 324 | ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` 325 | InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` 326 | } 327 | 328 | // Represents a venue. 329 | // By default, the venue will be sent by the user. 330 | // Alternatively, you can use input_message_content to send a message with the specified content instead of the venue. 331 | type InlineQueryResultVenue struct { 332 | inlineQueryResultImplementation 333 | Type InlineResultType `json:"type"` 334 | ID string `json:"id"` 335 | Latitude float64 `json:"latitude"` 336 | Longitude float64 `json:"longitude"` 337 | Title string `json:"title"` 338 | Address string `json:"address"` 339 | 340 | // Optional 341 | FoursquareID string `json:"foursquare_id,omitempty"` 342 | ThumbURL string `json:"thumb_url,omitempty"` 343 | ThumbWidth int `json:"thumb_width,omitempty"` 344 | ThumbHeight int `json:"thumb_height,omitempty"` 345 | ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` 346 | InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` 347 | } 348 | 349 | // Represents a link to a sticker stored on the Telegram servers. 350 | // By default, this sticker will be sent by the user. 351 | // Alternatively, you can use input_message_content to send a message with the specified content instead of the sticker. 352 | type InlineQueryResultCachedSticker struct { 353 | inlineQueryResultImplementation 354 | Type InlineResultType `json:"type"` 355 | ID string `json:"id"` 356 | StickerFileID string `json:"sticker_file_id"` 357 | 358 | // Optional 359 | ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` 360 | InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` 361 | } 362 | 363 | // Represents a contact with a phone number. 364 | // By default, this contact will be sent by the user. 365 | // Alternatively, you can use input_message_content to send a message with the specified content instead of the contact. 366 | type InlineQueryResultContact struct { 367 | inlineQueryResultImplementation 368 | Type InlineResultType `json:"type"` 369 | ID string `json:"id"` 370 | PhoneNumber string `json:"phone_number"` 371 | FirstName string `json:"first_name"` 372 | 373 | // Optional 374 | LastName string `json:"last_name,omitempty"` 375 | ThumbURL string `json:"thumb_url,omitempty"` 376 | ThumbWidth int `json:"thumb_width,omitempty"` 377 | ThumbHeight int `json:"thumb_height,omitempty"` 378 | ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` 379 | InputMessageContent InputMessageContent `json:"input_message_content,omitempty"` 380 | } 381 | 382 | // Represents a Game. 383 | type InlineQueryResultGame struct { 384 | inlineQueryResultImplementation 385 | Type InlineResultType `json:"type"` 386 | ID string `json:"id"` 387 | GameShortName string `json:"game_short_name"` 388 | 389 | // Optional 390 | ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` 391 | } 392 | 393 | type InputMessageContent interface { 394 | itsInputMessageContent() 395 | } 396 | 397 | type inputMessageContentImplementation struct{} 398 | 399 | func (i inputMessageContentImplementation) itsInputMessageContent() {} 400 | 401 | // InputTextMessageContent contains text for displaying as an inline query result. 402 | type InputTextMessageContent struct { 403 | inputMessageContentImplementation 404 | MessageText string `json:"message_text"` 405 | ParseMode ParseMode `json:"parse_mode"` 406 | DisableWebPagePreview bool `json:"disable_web_page_preview"` 407 | } 408 | 409 | // InputLocationMessageContent contains a location for displaying as an inline query result. 410 | type InputLocationMessageContent struct { 411 | inputMessageContentImplementation 412 | Latitude float64 `json:"latitude"` 413 | Longitude float64 `json:"longitude"` 414 | } 415 | 416 | // InputVenueMessageContent contains a venue for displaying an inline query result. 417 | type InputVenueMessageContent struct { 418 | inputMessageContentImplementation 419 | Latitude float64 `json:"latitude"` 420 | Longitude float64 `json:"longitude"` 421 | Title string `json:"title"` 422 | Address string `json:"address"` 423 | FoursquareID string `json:"foursquare_id"` 424 | } 425 | 426 | // InputContactMessageContent contains a contact for displaying as an inline query result. 427 | type InputContactMessageContent struct { 428 | inputMessageContentImplementation 429 | PhoneNumber string `json:"phone_number"` 430 | FirstName string `json:"first_name"` 431 | LastName string `json:"last_name"` 432 | } 433 | -------------------------------------------------------------------------------- /bot.go: -------------------------------------------------------------------------------- 1 | package micha 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "log/slog" 11 | "net/http" 12 | "net/url" 13 | ) 14 | 15 | const ( 16 | defaultAPIServer = "https://api.telegram.org" 17 | ) 18 | 19 | type Response struct { 20 | Ok bool `json:"ok"` 21 | ErrorCode int `json:"error_code"` 22 | Description string `json:"description"` 23 | Result json.RawMessage `json:"result"` 24 | } 25 | 26 | // Bot telegram bot 27 | type Bot struct { 28 | Options 29 | Me User 30 | 31 | token string 32 | updates chan Update 33 | offset uint64 34 | cancelFunc context.CancelFunc 35 | } 36 | 37 | // NewBot - create new bot instance 38 | func NewBot(token string, opts ...Option) (*Bot, error) { 39 | options := Options{ 40 | limit: 100, 41 | timeout: 25, 42 | logger: slog.Default(), 43 | apiServer: defaultAPIServer, 44 | httpClient: http.DefaultClient, 45 | ctx: context.Background(), 46 | } 47 | 48 | for _, opt := range opts { 49 | opt(&options) 50 | } 51 | 52 | bot := Bot{ 53 | Options: options, 54 | token: token, 55 | updates: make(chan Update), 56 | } 57 | bot.ctx, bot.cancelFunc = context.WithCancel(options.ctx) 58 | 59 | me, err := bot.GetMe() 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | bot.Me = *me 65 | 66 | return &bot, nil 67 | } 68 | 69 | // Build url for API method 70 | func (bot *Bot) buildURL(method string) string { 71 | return bot.Options.apiServer + fmt.Sprintf("/bot%s/%s", bot.token, method) 72 | } 73 | 74 | // Decode response result to target object 75 | func (bot *Bot) decodeResponse(data []byte, target interface{}) error { 76 | response := new(Response) 77 | if err := json.Unmarshal(data, response); err != nil { 78 | return fmt.Errorf("decode response error: %w", err) 79 | } 80 | 81 | if !response.Ok { 82 | return fmt.Errorf("Error %d (%s)", response.ErrorCode, response.Description) 83 | } 84 | 85 | if target == nil { 86 | // Don't need to decode result 87 | return nil 88 | } 89 | 90 | if err := json.Unmarshal(response.Result, target); err != nil { 91 | return fmt.Errorf("decode result error: %w", err) 92 | } 93 | 94 | return nil 95 | } 96 | 97 | // Send GET request to Telegram API 98 | func (bot *Bot) get(method string, params url.Values, target interface{}) error { 99 | request, err := newGetRequest(bot.ctx, bot.buildURL(method), params) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | response, err := bot.httpClient.Do(request) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | body, err := handleResponse(response) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | return bot.decodeResponse(body, target) 115 | } 116 | 117 | // Send POST request to Telegram API 118 | func (bot *Bot) post(method string, data, target interface{}) error { 119 | request, err := newPostRequest(bot.ctx, bot.buildURL(method), data) 120 | if err != nil { 121 | return err 122 | } 123 | response, err := bot.httpClient.Do(request) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | body, err := handleResponse(response) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | return bot.decodeResponse(body, target) 134 | } 135 | 136 | // Send POST multipart request to Telegram API 137 | func (bot *Bot) postMultipart(method string, file *fileField, params url.Values, target interface{}) error { 138 | request, err := newMultipartRequest(bot.ctx, bot.buildURL(method), file, params) 139 | if err != nil { 140 | return err 141 | } 142 | response, err := bot.httpClient.Do(request) 143 | if err != nil { 144 | return err 145 | } 146 | 147 | body, err := handleResponse(response) 148 | if err != nil { 149 | return err 150 | } 151 | 152 | return bot.decodeResponse(body, target) 153 | } 154 | 155 | // Use this method to receive incoming updates using long polling. 156 | // An Array of Update objects is returned. 157 | func (bot *Bot) getUpdates(offset uint64, allowedUpdates ...string) ([]Update, error) { 158 | params := url.Values{ 159 | "limit": {fmt.Sprintf("%d", bot.limit)}, 160 | "offset": {fmt.Sprintf("%d", offset)}, 161 | "timeout": {fmt.Sprintf("%d", bot.timeout)}, 162 | } 163 | 164 | if len(allowedUpdates) > 0 { 165 | params["allowed_updates"] = allowedUpdates 166 | } 167 | 168 | updates := []Update{} 169 | err := bot.get("getUpdates", params, &updates) 170 | 171 | return updates, err 172 | } 173 | 174 | // Start getting updates 175 | func (bot *Bot) Start(allowedUpdates ...string) { 176 | for { 177 | updates, err := bot.getUpdates(bot.offset+1, allowedUpdates...) 178 | if err != nil { 179 | bot.logger.ErrorContext(bot.ctx, "Get updates error", "error", err) 180 | httpErr := HTTPError{} 181 | if errors.As(err, &httpErr) && httpErr.StatusCode == http.StatusConflict { 182 | bot.cancelFunc() 183 | } 184 | } 185 | 186 | for _, update := range updates { 187 | bot.updates <- update 188 | bot.offset = update.UpdateID 189 | } 190 | 191 | select { 192 | case <-bot.ctx.Done(): 193 | close(bot.updates) 194 | return 195 | default: 196 | } 197 | } 198 | } 199 | 200 | // Stop getting updates 201 | func (bot *Bot) Stop() { 202 | bot.cancelFunc() 203 | } 204 | 205 | // Updates channel 206 | func (bot *Bot) Updates() <-chan Update { 207 | return bot.updates 208 | } 209 | 210 | func (bot *Bot) GetWebhookInfo() (*WebhookInfo, error) { 211 | webhookInfo := new(WebhookInfo) 212 | err := bot.get("getWebhookInfo", url.Values{}, webhookInfo) 213 | 214 | return webhookInfo, err 215 | } 216 | 217 | func (bot *Bot) SetWebhook(webhookURL string, options *SetWebhookOptions) error { 218 | var file *fileField 219 | params := url.Values{ 220 | "url": {webhookURL}, 221 | } 222 | if options != nil { 223 | if options.MaxConnections > 0 { 224 | params.Set("max_connections", fmt.Sprintf("%d", options.MaxConnections)) 225 | } 226 | if len(options.AllowedUpdates) > 0 { 227 | params["allowed_updates"] = options.AllowedUpdates 228 | } 229 | if len(options.Certificate) > 0 { 230 | file = &fileField{ 231 | Source: bytes.NewBuffer(options.Certificate), 232 | Fieldname: "certificate", 233 | Filename: "certificate", 234 | } 235 | } 236 | } 237 | 238 | return bot.postMultipart("setWebhook", file, params, nil) 239 | } 240 | 241 | func (bot *Bot) DeleteWebhook() error { 242 | return bot.post("deleteWebhook", nil, nil) 243 | } 244 | 245 | // Logout 246 | // Use this method to log out from the cloud Bot API server before launching the bot locally. 247 | // You must log out the bot before running it locally, 248 | // otherwise there is no guarantee that the bot will receive updates. 249 | // After a successful call, you can immediately log in on a local server, 250 | // but will not be able to log in back to the cloud Bot API server for 10 minutes. 251 | func (bot *Bot) Logout() error { 252 | url := defaultAPIServer + fmt.Sprintf("/bot%s/logOut", bot.token) 253 | request, err := newGetRequest(bot.ctx, url, nil) 254 | if err != nil { 255 | return err 256 | } 257 | 258 | response, err := bot.httpClient.Do(request) 259 | if err != nil { 260 | return err 261 | } 262 | 263 | _, err = handleResponse(response) 264 | return err 265 | } 266 | 267 | // A simple method for testing your bot's auth token. 268 | // Returns basic information about the bot in form of a User object. 269 | func (bot *Bot) GetMe() (*User, error) { 270 | me := new(User) 271 | err := bot.get("getMe", nil, me) 272 | 273 | return me, err 274 | } 275 | 276 | // Raw - send any method and return raw response 277 | func (bot *Bot) Raw(method string, data any) ([]byte, error) { 278 | request, err := newPostRequest(bot.ctx, bot.buildURL(method), data) 279 | if err != nil { 280 | return nil, err 281 | } 282 | 283 | response, err := bot.httpClient.Do(request) 284 | if err != nil { 285 | return nil, err 286 | } 287 | 288 | return handleResponse(response) 289 | } 290 | 291 | // Use this method to send text messages. 292 | func (bot *Bot) SendMessage(chatID ChatID, text string, options *SendMessageOptions) (*Message, error) { 293 | params := sendMessageParams{ 294 | ChatID: chatID, 295 | Text: text, 296 | } 297 | if options != nil { 298 | params.SendMessageOptions = *options 299 | } 300 | 301 | message := new(Message) 302 | err := bot.post("sendMessage", params, message) 303 | 304 | return message, err 305 | } 306 | 307 | // Send exists photo by file_id 308 | func (bot *Bot) SendPhoto(chatID ChatID, photoID string, options *SendPhotoOptions) (*Message, error) { 309 | params := newSendPhotoParams(chatID, photoID, options) 310 | 311 | message := new(Message) 312 | err := bot.post("sendPhoto", params, message) 313 | 314 | return message, err 315 | } 316 | 317 | // Send photo file 318 | func (bot *Bot) SendPhotoFile(chatID ChatID, file io.Reader, fileName string, options *SendPhotoOptions) (*Message, error) { 319 | params := newSendPhotoParams(chatID, "", options) 320 | values, err := structToValues(params) 321 | if err != nil { 322 | return nil, err 323 | } 324 | 325 | f := &fileField{ 326 | Source: file, 327 | Fieldname: "photo", 328 | Filename: fileName, 329 | } 330 | 331 | message := new(Message) 332 | err = bot.postMultipart("sendPhoto", f, values, message) 333 | 334 | return message, err 335 | } 336 | 337 | // Send exists audio by file_id 338 | func (bot *Bot) SendAudio(chatID ChatID, audioID string, options *SendAudioOptions) (*Message, error) { 339 | params := newSendAudioParams(chatID, audioID, options) 340 | 341 | message := new(Message) 342 | err := bot.post("sendAudio", params, message) 343 | 344 | return message, err 345 | } 346 | 347 | // Send audio file 348 | func (bot *Bot) SendAudioFile(chatID ChatID, file io.Reader, fileName string, options *SendAudioOptions) (*Message, error) { 349 | params := newSendAudioParams(chatID, "", options) 350 | values, err := structToValues(params) 351 | if err != nil { 352 | return nil, err 353 | } 354 | 355 | f := &fileField{ 356 | Source: file, 357 | Fieldname: "audio", 358 | Filename: fileName, 359 | } 360 | 361 | message := new(Message) 362 | err = bot.postMultipart("sendAudio", f, values, message) 363 | 364 | return message, err 365 | } 366 | 367 | // Send exists document by file_id 368 | func (bot *Bot) SendDocument(chatID ChatID, documentID string, options *SendDocumentOptions) (*Message, error) { 369 | params := newSendDocumentParams(chatID, documentID, options) 370 | 371 | message := new(Message) 372 | err := bot.post("sendDocument", params, message) 373 | 374 | return message, err 375 | } 376 | 377 | // Send file 378 | func (bot *Bot) SendDocumentFile(chatID ChatID, file io.Reader, fileName string, options *SendDocumentOptions) (*Message, error) { 379 | params := newSendDocumentParams(chatID, "", options) 380 | values, err := structToValues(params) 381 | if err != nil { 382 | return nil, err 383 | } 384 | 385 | f := &fileField{ 386 | Source: file, 387 | Fieldname: "document", 388 | Filename: fileName, 389 | } 390 | 391 | message := new(Message) 392 | err = bot.postMultipart("sendDocument", f, values, message) 393 | 394 | return message, err 395 | } 396 | 397 | // Send exists sticker by file_id 398 | func (bot *Bot) SendSticker(chatID ChatID, stickerID string, options *SendStickerOptions) (*Message, error) { 399 | params := newSendStickerParams(chatID, stickerID, options) 400 | 401 | message := new(Message) 402 | err := bot.post("sendSticker", params, message) 403 | 404 | return message, err 405 | } 406 | 407 | // Send .webp sticker file 408 | func (bot *Bot) SendStickerFile(chatID ChatID, file io.Reader, fileName string, options *SendStickerOptions) (*Message, error) { 409 | params := newSendStickerParams(chatID, "", options) 410 | values, err := structToValues(params) 411 | if err != nil { 412 | return nil, err 413 | } 414 | 415 | f := &fileField{ 416 | Source: file, 417 | Fieldname: "sticker", 418 | Filename: fileName, 419 | } 420 | 421 | message := new(Message) 422 | err = bot.postMultipart("sendSticker", f, values, message) 423 | 424 | return message, err 425 | } 426 | 427 | // Send exists video by file_id 428 | func (bot *Bot) SendVideo(chatID ChatID, videoID string, options *SendVideoOptions) (*Message, error) { 429 | params := newSendVideoParams(chatID, videoID, options) 430 | 431 | message := new(Message) 432 | err := bot.post("sendVideo", params, message) 433 | 434 | return message, err 435 | } 436 | 437 | // Use this method to send video files, Telegram clients support mp4 videos (other formats may be sent as Document). 438 | func (bot *Bot) SendVideoFile(chatID ChatID, file io.Reader, fileName string, options *SendVideoOptions) (*Message, error) { 439 | params := newSendVideoParams(chatID, "", options) 440 | values, err := structToValues(params) 441 | if err != nil { 442 | return nil, err 443 | } 444 | 445 | f := &fileField{ 446 | Source: file, 447 | Fieldname: "video", 448 | Filename: fileName, 449 | } 450 | 451 | message := new(Message) 452 | err = bot.postMultipart("sendVideo", f, values, message) 453 | 454 | return message, err 455 | } 456 | 457 | // Send exists voice by file_id 458 | func (bot *Bot) SendVoice(chatID ChatID, voiceID string, options *SendVoiceOptions) (*Message, error) { 459 | params := newSendVoiceParams(chatID, voiceID, options) 460 | 461 | message := new(Message) 462 | err := bot.post("sendVoice", params, message) 463 | 464 | return message, err 465 | } 466 | 467 | // Use this method to send audio files, 468 | // if you want Telegram clients to display the file as a playable voice message. 469 | // For this to work, your audio must be in an .ogg file encoded with OPUS (other formats may be sent as Audio or Document). 470 | func (bot *Bot) SendVoiceFile(chatID ChatID, file io.Reader, fileName string, options *SendVoiceOptions) (*Message, error) { 471 | params := newSendVoiceParams(chatID, "", options) 472 | values, err := structToValues(params) 473 | if err != nil { 474 | return nil, err 475 | } 476 | 477 | f := &fileField{ 478 | Source: file, 479 | Fieldname: "voice", 480 | Filename: fileName, 481 | } 482 | 483 | message := new(Message) 484 | err = bot.postMultipart("sendVoice", f, values, message) 485 | 486 | return message, err 487 | } 488 | 489 | // Send exists video note by file_id 490 | func (bot *Bot) SendVideoNote(chatID ChatID, videoNoteID string, options *SendVideoNoteOptions) (*Message, error) { 491 | params := newSendVideoNoteParams(chatID, videoNoteID, options) 492 | 493 | message := new(Message) 494 | err := bot.post("sendVideoNote", params, message) 495 | 496 | return message, err 497 | } 498 | 499 | // Use this method to send video messages 500 | func (bot *Bot) SendVideoNoteFile(chatID ChatID, file io.Reader, fileName string, options *SendVideoNoteOptions) (*Message, error) { 501 | params := newSendVideoNoteParams(chatID, "", options) 502 | values, err := structToValues(params) 503 | if err != nil { 504 | return nil, err 505 | } 506 | 507 | f := &fileField{ 508 | Source: file, 509 | Fieldname: "video_note", 510 | Filename: fileName, 511 | } 512 | 513 | message := new(Message) 514 | err = bot.postMultipart("sendVideoNote", f, values, message) 515 | 516 | return message, err 517 | } 518 | 519 | // Use this method to send point on the map 520 | func (bot *Bot) SendLocation(chatID ChatID, latitude, longitude float64, options *SendLocationOptions) (*Message, error) { 521 | params := newSendLocationParams(chatID, latitude, longitude, options) 522 | 523 | message := new(Message) 524 | err := bot.post("sendLocation", params, message) 525 | 526 | return message, err 527 | } 528 | 529 | // Use this method to send information about a venue 530 | func (bot *Bot) SendVenue(chatID ChatID, latitude, longitude float64, title, address string, options *SendVenueOptions) (*Message, error) { 531 | params := newSendVenueParams(chatID, latitude, longitude, title, address, options) 532 | 533 | message := new(Message) 534 | err := bot.post("sendVenue", params, message) 535 | 536 | return message, err 537 | } 538 | 539 | // Use this method to send phone contacts 540 | func (bot *Bot) SendContact(chatID ChatID, phoneNumber, firstName, lastName string, options *SendContactOptions) (*Message, error) { 541 | params := newSendContactParams(chatID, phoneNumber, firstName, lastName, options) 542 | 543 | message := new(Message) 544 | err := bot.post("sendContact", params, message) 545 | 546 | return message, err 547 | } 548 | 549 | // Use this method to forward messages of any kind. 550 | func (bot *Bot) ForwardMessage(chatID, fromChatID ChatID, messageID int64, disableNotification bool) (*Message, error) { 551 | params := map[string]interface{}{ 552 | "chat_id": chatID, 553 | "from_chat_id": fromChatID, 554 | "message_id": messageID, 555 | "disable_notification": disableNotification, 556 | } 557 | 558 | message := new(Message) 559 | err := bot.post("forwardMessage", params, message) 560 | 561 | return message, err 562 | } 563 | 564 | // Use this method when you need to tell the user that something is happening on the bot's side. 565 | // The status is set for 5 seconds or less (when a message arrives from your bot, Telegram clients clear its typing status). 566 | func (bot *Bot) SendChatAction(chatID ChatID, action ChatAction) error { 567 | params := map[string]interface{}{ 568 | "chat_id": chatID, 569 | "action": action, 570 | } 571 | 572 | return bot.post("sendChatAction", params, nil) 573 | } 574 | 575 | // Use this method to get a list of profile pictures for a user. 576 | func (bot *Bot) GetUserProfilePhotos(userID int64, offset, limit *int) (*UserProfilePhotos, error) { 577 | params := url.Values{ 578 | "user_id": {fmt.Sprintf("%d", userID)}, 579 | } 580 | 581 | if offset != nil { 582 | params["offset"] = []string{fmt.Sprintf("%d", *offset)} 583 | } 584 | if limit != nil { 585 | params["limit"] = []string{fmt.Sprintf("%d", *limit)} 586 | } 587 | 588 | profilePhotos := new(UserProfilePhotos) 589 | err := bot.get("getUserProfilePhotos", params, profilePhotos) 590 | 591 | return profilePhotos, err 592 | } 593 | 594 | // Use this method to get basic info about a file and prepare it for downloading. 595 | // It is guaranteed that the link will be valid for at least 1 hour. 596 | // When the link expires, a new one can be requested by calling getFile again. 597 | func (bot *Bot) GetFile(fileID string) (*File, error) { 598 | params := url.Values{ 599 | "file_id": {fileID}, 600 | } 601 | 602 | file := new(File) 603 | err := bot.get("getFile", params, file) 604 | 605 | return file, err 606 | } 607 | 608 | // Return absolute url for file downloading by file path 609 | func (bot *Bot) DownloadFileURL(filePath string) string { 610 | return bot.Options.apiServer + fmt.Sprintf("/file/bot%s/%s", bot.token, filePath) 611 | } 612 | 613 | // Use this method to edit text messages sent by the bot or via the bot (for inline bots). 614 | func (bot *Bot) EditMessageText(chatID ChatID, messageID int64, inlineMessageID, text string, options *EditMessageTextOptions) (*Message, error) { 615 | params := editMessageTextParams{ 616 | ChatID: chatID, 617 | MessageID: messageID, 618 | InlineMessageID: inlineMessageID, 619 | Text: text, 620 | } 621 | if options != nil { 622 | params.EditMessageTextOptions = *options 623 | } 624 | 625 | message := new(Message) 626 | err := bot.post("editMessageText", params, message) 627 | 628 | return message, err 629 | } 630 | 631 | // Use this method to edit captions of messages sent by the bot or via the bot (for inline bots). 632 | func (bot *Bot) EditMessageCaption(chatID ChatID, messageID int64, inlineMessageID string, options *EditMessageCationOptions) (*Message, error) { 633 | params := editMessageCationParams{ 634 | ChatID: chatID, 635 | MessageID: messageID, 636 | InlineMessageID: inlineMessageID, 637 | } 638 | if options != nil { 639 | params.EditMessageCationOptions = *options 640 | } 641 | 642 | message := new(Message) 643 | err := bot.post("editMessageCaption", params, message) 644 | 645 | return message, err 646 | } 647 | 648 | // Use this method to edit only the reply markup of messages sent by the bot or via the bot (for inline bots). 649 | func (bot *Bot) EditMessageReplyMarkup(chatID ChatID, messageID int64, inlineMessageID string, replyMarkup ReplyMarkup) (*Message, error) { 650 | params := editMessageReplyMarkupParams{ 651 | ChatID: chatID, 652 | MessageID: messageID, 653 | InlineMessageID: inlineMessageID, 654 | ReplyMarkup: replyMarkup, 655 | } 656 | 657 | message := new(Message) 658 | err := bot.post("editMessageReplyMarkup", params, message) 659 | 660 | return message, err 661 | } 662 | 663 | // Use this method to delete a message. 664 | // A message can only be deleted if it was sent less than 48 hours ago. 665 | // Any such recently sent outgoing message may be deleted. 666 | // Additionally, if the bot is an administrator in a group chat, it can delete any message. 667 | // If the bot is an administrator in a supergroup, it can delete messages from any other user and service messages about people joining or leaving the group (other types of service messages may only be removed by the group creator). In channels, bots can only remove their own messages. 668 | func (bot *Bot) DeleteMessage(chatID ChatID, messageID int64) (bool, error) { 669 | params := map[string]interface{}{ 670 | "chat_id": chatID, 671 | "message_id": messageID, 672 | } 673 | 674 | var success bool 675 | err := bot.post("deleteMessage", params, &success) 676 | 677 | return success, err 678 | } 679 | 680 | // Use this method to send answers to an inline query. 681 | // No more than 50 results per query are allowed. 682 | func (bot *Bot) AnswerInlineQuery(inlineQueryID string, results InlineQueryResults, options *AnswerInlineQueryOptions) error { 683 | params := answerInlineQueryParams{ 684 | InlineQueryID: inlineQueryID, 685 | Results: results, 686 | } 687 | if options != nil { 688 | params.AnswerInlineQueryOptions = *options 689 | } 690 | 691 | return bot.post("answerInlineQuery", params, nil) 692 | } 693 | 694 | // Use this method to kick a user from a group or a supergroup. 695 | // In the case of supergroups, the user will not be able to return to the group on their own using invite links, etc., unless unbanned first. 696 | // The bot must be an administrator in the group for this to work. 697 | func (bot *Bot) KickChatMember(chatID ChatID, userID int64) error { 698 | params := map[string]interface{}{ 699 | "chat_id": chatID, 700 | "user_id": userID, 701 | } 702 | 703 | return bot.post("kickChatMember", params, nil) 704 | } 705 | 706 | // Use this method for your bot to leave a group, supergroup or channel 707 | func (bot *Bot) LeaveChat(chatID ChatID) error { 708 | params := map[string]interface{}{ 709 | "chat_id": chatID, 710 | } 711 | 712 | return bot.post("leaveChat", params, nil) 713 | } 714 | 715 | // Use this method to unban a previously kicked user in a supergroup. 716 | // The user will not return to the group automatically, but will be able to join via link, etc. 717 | // The bot must be an administrator in the group for this to work. 718 | func (bot *Bot) UnbanChatMember(chatID ChatID, userID int64) error { 719 | params := map[string]interface{}{ 720 | "chat_id": chatID, 721 | "user_id": userID, 722 | } 723 | 724 | return bot.post("unbanChatMember", params, nil) 725 | } 726 | 727 | // Use this method to get up to date information about the chat (current name of the user for one-on-one conversations, current username of a user, group or channel, etc.). 728 | func (bot *Bot) GetChat(chatID ChatID) (*Chat, error) { 729 | params := url.Values{ 730 | "chat_id": {string(chatID)}, 731 | } 732 | 733 | chat := new(Chat) 734 | err := bot.get("getChat", params, chat) 735 | 736 | return chat, err 737 | } 738 | 739 | // Use this method to get a list of administrators in a chat. 740 | // If the chat is a group or a supergroup and no administrators were appointed, only the creator will be returned. 741 | func (bot *Bot) GetChatAdministrators(chatID ChatID) ([]ChatMember, error) { 742 | params := url.Values{ 743 | "chat_id": {string(chatID)}, 744 | } 745 | 746 | administrators := []ChatMember{} 747 | err := bot.get("getChatAdministrators", params, &administrators) 748 | 749 | return administrators, err 750 | } 751 | 752 | // Use this method to get the number of members in a chat. 753 | func (bot *Bot) GetChatMembersCount(chatID ChatID) (int, error) { 754 | params := url.Values{ 755 | "chat_id": {string(chatID)}, 756 | } 757 | 758 | count := 0 759 | err := bot.get("getChatMembersCount", params, &count) 760 | 761 | return count, err 762 | } 763 | 764 | // Use this method to get information about a member of a chat. 765 | func (bot *Bot) GetChatMember(chatID ChatID, userID int64) (*ChatMember, error) { 766 | params := url.Values{ 767 | "chat_id": {string(chatID)}, 768 | "user_id": {fmt.Sprintf("%d", userID)}, 769 | } 770 | 771 | chatMember := new(ChatMember) 772 | err := bot.get("getChatMember", params, chatMember) 773 | 774 | return chatMember, err 775 | } 776 | 777 | // Use this method to send answers to callback queries sent from inline keyboards. 778 | // The answer will be displayed to the user as a notification at the top of the chat screen or as an alert. 779 | func (bot *Bot) AnswerCallbackQuery(callbackQueryID string, options *AnswerCallbackQueryOptions) error { 780 | params := newAnswerCallbackQueryParams(callbackQueryID, options) 781 | 782 | return bot.post("answerCallbackQuery", params, nil) 783 | } 784 | 785 | // Use this method to send a game. 786 | func (bot *Bot) SendGame(chatID ChatID, gameShortName string, options *SendGameOptions) (*Message, error) { 787 | params := sendGameParams{ 788 | ChatID: chatID, 789 | GameShortName: gameShortName, 790 | } 791 | 792 | if options != nil { 793 | params.SendGameOptions = *options 794 | } 795 | 796 | message := new(Message) 797 | err := bot.post("sendGame", params, message) 798 | 799 | return message, err 800 | } 801 | 802 | // Use this method to set the score of the specified user in a game. 803 | func (bot *Bot) SetGameScore(userID int64, score int, options *SetGameScoreOptions) (*Message, error) { 804 | params := setGameScoreParams{ 805 | UserID: userID, 806 | Score: score, 807 | } 808 | 809 | if options != nil { 810 | params.SetGameScoreOptions = *options 811 | } 812 | 813 | message := new(Message) 814 | err := bot.post("setGameScore", params, message) 815 | 816 | return message, err 817 | } 818 | 819 | // Use this method to get data for high score tables. 820 | // Will return the score of the specified user and several of his neighbors in a game. 821 | func (bot *Bot) GetGameHighScores(userID int64, options *GetGameHighScoresOptions) ([]GameHighScore, error) { 822 | params, err := structToValues(options) 823 | if err != nil { 824 | return nil, err 825 | } 826 | 827 | params.Set("user_id", fmt.Sprintf("%d", userID)) 828 | 829 | scores := []GameHighScore{} 830 | err = bot.get("getGameHighScores", params, &scores) 831 | 832 | return scores, err 833 | } 834 | -------------------------------------------------------------------------------- /bot_test.go: -------------------------------------------------------------------------------- 1 | package micha 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "log/slog" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "strings" 13 | "testing" 14 | 15 | "github.com/jarcoal/httpmock" 16 | "github.com/stretchr/testify/suite" 17 | ) 18 | 19 | type BotTestSuite struct { 20 | suite.Suite 21 | bot *Bot 22 | } 23 | 24 | func (s *BotTestSuite) SetupSuite() { 25 | httpmock.Activate() 26 | 27 | s.bot = &Bot{ 28 | token: "111", 29 | updates: make(chan Update), 30 | Options: Options{ 31 | limit: 100, 32 | timeout: 25, 33 | logger: slog.Default(), 34 | apiServer: defaultAPIServer, 35 | httpClient: http.DefaultClient, 36 | }, 37 | } 38 | s.bot.ctx, s.bot.cancelFunc = context.WithCancel(context.Background()) 39 | } 40 | 41 | func (s *BotTestSuite) TearDownSuite() { 42 | httpmock.Deactivate() 43 | } 44 | 45 | func (s *BotTestSuite) TearDownTest() { 46 | httpmock.Reset() 47 | } 48 | 49 | func (s *BotTestSuite) registerResponse(method string, params url.Values, response string) { 50 | url := s.bot.buildURL(method) 51 | if params != nil { 52 | url += fmt.Sprintf("?%s", params.Encode()) 53 | } 54 | 55 | httpmock.RegisterResponder("GET", url, httpmock.NewStringResponder(200, response)) 56 | } 57 | 58 | func (s *BotTestSuite) registerRequestCheck(method string, exceptedRequest string) { 59 | s.registerResultWithRequestCheck(method, `{}`, exceptedRequest) 60 | } 61 | 62 | func (s *BotTestSuite) registerResultWithRequestCheck(method, result, exceptedRequest string) { 63 | url := s.bot.buildURL(method) 64 | 65 | httpmock.RegisterResponder("POST", url, func(request *http.Request) (*http.Response, error) { 66 | defer request.Body.Close() 67 | body, err := io.ReadAll(request.Body) 68 | if err != nil { 69 | return nil, err 70 | } 71 | if exceptedRequest == "" { 72 | s.Require().Equal(exceptedRequest, strings.TrimSpace(string(body))) 73 | } else { 74 | s.JSONEq(exceptedRequest, string(body)) 75 | } 76 | return httpmock.NewStringResponse(200, fmt.Sprintf(`{"ok":true, "result": %s}`, result)), nil 77 | }) 78 | } 79 | 80 | func (s *BotTestSuite) registeMultipartrRequestCheck(method string, exceptedValues url.Values, exceptedFile fileField) { 81 | url := s.bot.buildURL(method) 82 | 83 | httpmock.RegisterResponder("POST", url, func(request *http.Request) (*http.Response, error) { 84 | err := request.ParseMultipartForm(1024) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | form := request.MultipartForm 90 | for field, value := range exceptedValues { 91 | s.Require().Equal(value, form.Value[field]) 92 | } 93 | 94 | files := form.File[exceptedFile.Fieldname] 95 | s.Require().Equal(1, len(files)) 96 | 97 | file, err := files[0].Open() 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | defer file.Close() 103 | data, err := io.ReadAll(file) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | exceptedData, err := io.ReadAll(exceptedFile.Source) 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | s.Require().Equal(exceptedData, data) 114 | 115 | return httpmock.NewStringResponse(200, `{"ok":true, "result": {}}`), nil 116 | }) 117 | } 118 | 119 | func (s *BotTestSuite) TestNewBot() { 120 | s.registerResponse("getMe", nil, `{ 121 | "ok":true, 122 | "result": { 123 | "id":1, 124 | "first_name": "Micha", 125 | "username": "michabot" 126 | } 127 | }`) 128 | 129 | // Without options 130 | bot, err := NewBot("111") 131 | s.Require().Nil(err) 132 | s.Require().NotNil(bot) 133 | s.Require().Equal(25, bot.timeout) 134 | s.Require().Equal(100, bot.limit) 135 | s.Require().Equal(slog.Default(), bot.logger) 136 | 137 | // With options 138 | logger := slog.New(slog.NewTextHandler(os.Stderr, nil)) 139 | httpClient := &http.Client{} 140 | bot, err = NewBot("111", WithLimit(50), WithTimeout(10), WithLogger(logger), WithHttpClient(httpClient)) 141 | s.Require().Nil(err) 142 | s.Require().NotNil(bot) 143 | s.Require().Equal(10, bot.timeout) 144 | s.Require().Equal(50, bot.limit) 145 | s.Require().Equal(logger, bot.logger) 146 | s.Require().Equal(httpClient, bot.httpClient) 147 | } 148 | 149 | func (s *BotTestSuite) TestErrorsHandle() { 150 | s.registerResponse("method", nil, `{dsfkdf`) 151 | 152 | err := s.bot.get("method", nil, nil) 153 | s.Require().NotNil(err) 154 | s.True(strings.Contains(err.Error(), "decode response error")) 155 | 156 | httpmock.Reset() 157 | s.registerResponse("method", nil, `{"ok":false, "error_code": 111}`) 158 | err = s.bot.get("method", nil, nil) 159 | s.Require().NotNil(err) 160 | s.True(strings.Contains(err.Error(), "Error 111")) 161 | 162 | httpmock.Reset() 163 | s.registerResponse("method", nil, `{"ok":true, "result": "dssdd"}`) 164 | var result int 165 | err = s.bot.get("method", nil, &result) 166 | s.Require().NotNil(err) 167 | s.True(strings.Contains(err.Error(), "decode result error")) 168 | } 169 | 170 | func (s *BotTestSuite) TestBuildUrl() { 171 | url := s.bot.buildURL("someMethod") 172 | s.Require().Equal(url, "https://api.telegram.org/bot111/someMethod") 173 | } 174 | 175 | func (s *BotTestSuite) TestGetUpdates() { 176 | values := url.Values{ 177 | "offset": {"1"}, 178 | "timeout": {"25"}, 179 | "limit": {"100"}, 180 | "allowed_updates": {"message", "callback_query"}, 181 | } 182 | s.registerResponse("getUpdates", values, `{ 183 | "ok": true, 184 | "result": [{ 185 | "update_id": 463249624 186 | }] 187 | }`) 188 | 189 | go s.bot.Start("message", "callback_query") 190 | 191 | update, ok := <-s.bot.Updates() 192 | s.Require().True(ok) 193 | s.Require().Equal(uint64(463249624), update.UpdateID) 194 | s.Require().Equal(uint64(463249624), s.bot.offset) 195 | 196 | s.bot.Stop() 197 | update, ok = <-s.bot.Updates() 198 | s.Require().False(ok) 199 | s.bot.ctx, s.bot.cancelFunc = context.WithCancel(context.Background()) 200 | } 201 | 202 | func (s *BotTestSuite) TestGetMe() { 203 | s.registerResponse("getMe", nil, `{ 204 | "ok":true, 205 | "result": { 206 | "id": 143, 207 | "first_name": "John", 208 | "last_name": "Doe", 209 | "username": "jdbot" 210 | } 211 | }`) 212 | 213 | me, err := s.bot.GetMe() 214 | s.Require().Nil(err) 215 | s.Require().NotNil(me) 216 | s.Require().Equal(int64(143), me.ID) 217 | s.Require().Equal("John", me.FirstName) 218 | s.Require().Equal("Doe", me.LastName) 219 | s.Require().Equal("jdbot", me.Username) 220 | } 221 | 222 | func (s *BotTestSuite) TestGetChat() { 223 | s.registerResponse("getChat", url.Values{"chat_id": {"123"}}, `{ 224 | "ok": true, 225 | "result": { 226 | "id": 123, 227 | "type": "group", 228 | "title": "ChatTitle", 229 | "first_name": "fn", 230 | "last_name": "ln", 231 | "username": "un" 232 | } 233 | }`) 234 | 235 | chat, err := s.bot.GetChat("123") 236 | s.Require().Nil(err) 237 | s.Require().Equal(chat.ID, ChatID("123")) 238 | s.Require().Equal(chat.Type, CHAT_TYPE_GROUP) 239 | s.Require().Equal(chat.Title, "ChatTitle") 240 | s.Require().Equal(chat.FirstName, "fn") 241 | s.Require().Equal(chat.LastName, "ln") 242 | s.Require().Equal(chat.Username, "un") 243 | } 244 | 245 | func (s *BotTestSuite) TestGetWebhookInfo() { 246 | s.registerResponse("getWebhookInfo", nil, `{ 247 | "ok": true, 248 | "result": { 249 | "url": "someurl", 250 | "has_custom_certificate": true, 251 | "pending_update_count": 33, 252 | "last_error_date": 1480190406, 253 | "last_error_message": "No way", 254 | "max_connections": 4, 255 | "allowed_updates": ["message", "callback_query"] 256 | } 257 | }`) 258 | webhookInfo, err := s.bot.GetWebhookInfo() 259 | s.Nil(err) 260 | s.NotNil(webhookInfo) 261 | s.Require().Equal("someurl", webhookInfo.URL) 262 | s.True(webhookInfo.HasCustomCertificate) 263 | s.Require().Equal(33, webhookInfo.PendingUpdateCount) 264 | s.Require().Equal(uint64(1480190406), webhookInfo.LastErrorDate) 265 | s.Require().Equal("No way", webhookInfo.LastErrorMessage) 266 | s.Require().Equal(4, webhookInfo.MaxConnections) 267 | s.Require().Equal([]string{"message", "callback_query"}, webhookInfo.AllowedUpdates) 268 | } 269 | 270 | func (s *BotTestSuite) TestSetWebhook() { 271 | params := url.Values{ 272 | "url": {"hookurl"}, 273 | "max_connections": {"9"}, 274 | "allowed_updates": {"message", "callback_query"}, 275 | } 276 | data := "92839727433" 277 | options := &SetWebhookOptions{ 278 | Certificate: []byte(data), 279 | MaxConnections: 9, 280 | AllowedUpdates: []string{"message", "callback_query"}, 281 | } 282 | 283 | file := fileField{ 284 | Source: bytes.NewBufferString(data), 285 | Fieldname: "certificate", 286 | Filename: "certificate", 287 | } 288 | s.registeMultipartrRequestCheck("setWebhook", params, file) 289 | 290 | err := s.bot.SetWebhook("hookurl", options) 291 | 292 | s.Nil(err) 293 | } 294 | 295 | func (s *BotTestSuite) TestDeleteWebhook() { 296 | s.registerRequestCheck("deleteWebhook", "") 297 | 298 | err := s.bot.DeleteWebhook() 299 | s.Nil(err) 300 | } 301 | 302 | func (s *BotTestSuite) TestGetChatAdministrators() { 303 | s.registerResponse("getChatAdministrators", url.Values{"chat_id": {"123"}}, `{ 304 | "ok": true, 305 | "result": [ 306 | { 307 | "status": "administrator", 308 | "user": { 309 | "id": 456, 310 | "first_name": "John", 311 | "last_name": "Doe", 312 | "username": "john_doe" 313 | } 314 | }, 315 | { 316 | "status": "administrator", 317 | "user": { 318 | "id": 789, 319 | "first_name": "Mohammad", 320 | "last_name": "Li", 321 | "username": "mli" 322 | } 323 | } 324 | ] 325 | }`) 326 | 327 | administrators, err := s.bot.GetChatAdministrators("123") 328 | s.Require().Nil(err) 329 | s.Require().Equal(len(administrators), 2) 330 | s.Require().Equal(administrators[0].User.ID, int64(456)) 331 | s.Require().Equal(administrators[0].User.FirstName, "John") 332 | s.Require().Equal(administrators[0].User.LastName, "Doe") 333 | s.Require().Equal(administrators[0].User.Username, "john_doe") 334 | s.Require().Equal(administrators[0].Status, MEMBER_STATUS_ADMINISTRATOR) 335 | 336 | s.Require().Equal(administrators[1].User.ID, int64(789)) 337 | s.Require().Equal(administrators[1].User.FirstName, "Mohammad") 338 | s.Require().Equal(administrators[1].User.LastName, "Li") 339 | s.Require().Equal(administrators[1].User.Username, "mli") 340 | s.Require().Equal(administrators[1].Status, MEMBER_STATUS_ADMINISTRATOR) 341 | } 342 | 343 | func (s *BotTestSuite) TestGetChatMember() { 344 | s.registerResponse("getChatMember", url.Values{"chat_id": {"123"}, "user_id": {"456"}}, `{ 345 | "ok": true, 346 | "result": { 347 | "status": "creator", 348 | "user": { 349 | "id": 456, 350 | "first_name": "John", 351 | "last_name": "Doe", 352 | "username": "john_doe" 353 | } 354 | } 355 | }`) 356 | 357 | chatMember, err := s.bot.GetChatMember("123", 456) 358 | s.Require().Nil(err) 359 | s.Require().Equal(chatMember.User.ID, int64(456)) 360 | s.Require().Equal(chatMember.User.FirstName, "John") 361 | s.Require().Equal(chatMember.User.LastName, "Doe") 362 | s.Require().Equal(chatMember.User.Username, "john_doe") 363 | s.Require().Equal(chatMember.Status, MEMBER_STATUS_CREATOR) 364 | 365 | } 366 | 367 | func (s *BotTestSuite) TestGetChatMembersCount() { 368 | s.registerResponse("getChatMembersCount", url.Values{"chat_id": {"123"}}, `{"ok":true, "result": 25}`) 369 | 370 | count, err := s.bot.GetChatMembersCount("123") 371 | s.Require().Nil(err) 372 | s.Require().Equal(count, 25) 373 | } 374 | 375 | func (s *BotTestSuite) TestGetFile() { 376 | s.registerResponse("getFile", url.Values{"file_id": {"222"}}, `{"ok":true,"result":{"file_id":"222","file_size":5,"file_path":"document/file_3.txt"}}`) 377 | 378 | file, err := s.bot.GetFile("222") 379 | s.Require().Nil(err) 380 | s.Require().Equal(file.FileID, "222") 381 | s.Require().Equal(file.FileSize, uint64(5)) 382 | s.Require().Equal(file.FilePath, "document/file_3.txt") 383 | } 384 | 385 | func (s *BotTestSuite) TestDownloadFileURL() { 386 | url := s.bot.DownloadFileURL("file.mp3") 387 | s.Require().Equal(url, "https://api.telegram.org/file/bot111/file.mp3") 388 | } 389 | 390 | func (s *BotTestSuite) TestSendPhoto() { 391 | request := `{"chat_id":"111","photo":"35f9f497a879436fbb6e682f6dd75986","caption":"test caption","reply_to_message_id":143}` 392 | s.registerRequestCheck("sendPhoto", request) 393 | 394 | message, err := s.bot.SendPhoto("111", "35f9f497a879436fbb6e682f6dd75986", &SendPhotoOptions{ 395 | Caption: "test caption", 396 | ReplyToMessageID: 143, 397 | }) 398 | 399 | s.Require().Nil(err) 400 | s.Require().NotNil(message) 401 | } 402 | 403 | func (s *BotTestSuite) TestSendPhotoFile() { 404 | params := url.Values{ 405 | "chat_id": {"112"}, 406 | "caption": {"capt"}, 407 | } 408 | data := bytes.NewBufferString("sadkf") 409 | file := fileField{ 410 | Source: bytes.NewBufferString("sadkf"), 411 | Fieldname: "photo", 412 | Filename: "photo.png", 413 | } 414 | s.registeMultipartrRequestCheck("sendPhoto", params, file) 415 | 416 | message, err := s.bot.SendPhotoFile("112", data, "photo.png", &SendPhotoOptions{ 417 | Caption: "capt", 418 | }) 419 | 420 | s.Require().Nil(err) 421 | s.Require().NotNil(message) 422 | } 423 | 424 | func (s *BotTestSuite) TestSendAudio() { 425 | request := `{"chat_id":"123","audio":"061c2810391f44f6beffa3ee8a7e5af4","duration":36,"performer":"John Doe","title":"Single","reply_to_message_id":143}` 426 | s.registerRequestCheck("sendAudio", request) 427 | 428 | message, err := s.bot.SendAudio("123", "061c2810391f44f6beffa3ee8a7e5af4", &SendAudioOptions{ 429 | Duration: 36, 430 | Performer: "John Doe", 431 | Title: "Single", 432 | ReplyToMessageID: 143, 433 | }) 434 | 435 | s.Require().Nil(err) 436 | s.Require().NotNil(message) 437 | } 438 | 439 | func (s *BotTestSuite) TestSendAudioFile() { 440 | params := url.Values{ 441 | "chat_id": {"522"}, 442 | "duration": {"133"}, 443 | "performer": {"perf"}, 444 | "title": {"Hit"}, 445 | } 446 | data := bytes.NewBufferString("audio data") 447 | file := fileField{ 448 | Source: bytes.NewBufferString("audio data"), 449 | Fieldname: "audio", 450 | Filename: "song.mp3", 451 | } 452 | s.registeMultipartrRequestCheck("sendAudio", params, file) 453 | 454 | message, err := s.bot.SendAudioFile("522", data, "song.mp3", &SendAudioOptions{ 455 | Duration: 133, 456 | Performer: "perf", 457 | Title: "Hit", 458 | }) 459 | 460 | s.Require().Nil(err) 461 | s.Require().NotNil(message) 462 | } 463 | 464 | func (s *BotTestSuite) TestSendDocument() { 465 | request := `{"chat_id":"124","document":"efd8d08958894a6781873b9830634483","caption":"document caption","reply_to_message_id":144}` 466 | s.registerRequestCheck("sendDocument", request) 467 | 468 | message, err := s.bot.SendDocument("124", "efd8d08958894a6781873b9830634483", &SendDocumentOptions{ 469 | Caption: "document caption", 470 | ReplyToMessageID: 144, 471 | }) 472 | 473 | s.Require().Nil(err) 474 | s.Require().NotNil(message) 475 | } 476 | 477 | func (s *BotTestSuite) TestSendDocumentFile() { 478 | params := url.Values{ 479 | "chat_id": {"89"}, 480 | "caption": {"top secret"}, 481 | } 482 | data := bytes.NewBufferString("...") 483 | file := fileField{ 484 | Source: bytes.NewBufferString("..."), 485 | Fieldname: "document", 486 | Filename: "x-files.txt", 487 | } 488 | s.registeMultipartrRequestCheck("sendDocument", params, file) 489 | 490 | message, err := s.bot.SendDocumentFile("89", data, "x-files.txt", &SendDocumentOptions{ 491 | Caption: "top secret", 492 | }) 493 | 494 | s.Require().Nil(err) 495 | s.Require().NotNil(message) 496 | } 497 | 498 | func (s *BotTestSuite) TestSendSticker() { 499 | request := `{"chat_id":"125","sticker":"070114a7fa964322acb3d65e6e36eb2b","reply_to_message_id":145}` 500 | s.registerRequestCheck("sendSticker", request) 501 | 502 | message, err := s.bot.SendSticker("125", "070114a7fa964322acb3d65e6e36eb2b", &SendStickerOptions{ 503 | ReplyToMessageID: 145, 504 | }) 505 | 506 | s.Require().Nil(err) 507 | s.Require().NotNil(message) 508 | } 509 | 510 | func (s *BotTestSuite) TestSendStickerFile() { 511 | params := url.Values{ 512 | "chat_id": {"100"}, 513 | } 514 | data := bytes.NewBufferString("sticker data") 515 | file := fileField{ 516 | Source: bytes.NewBufferString("sticker data"), 517 | Fieldname: "sticker", 518 | Filename: "sticker.webp", 519 | } 520 | s.registeMultipartrRequestCheck("sendSticker", params, file) 521 | 522 | message, err := s.bot.SendStickerFile("100", data, "sticker.webp", nil) 523 | 524 | s.Require().Nil(err) 525 | s.Require().NotNil(message) 526 | } 527 | 528 | func (s *BotTestSuite) TestSendVideo() { 529 | request := `{"chat_id":"126","video":"b169f647c020405b8c9035cf3f315ff0","duration":22,"width":320,"height":240,"caption":"video caption","reply_to_message_id":146}` 530 | s.registerRequestCheck("sendVideo", request) 531 | 532 | message, err := s.bot.SendVideo("126", "b169f647c020405b8c9035cf3f315ff0", &SendVideoOptions{ 533 | Duration: 22, 534 | Width: 320, 535 | Height: 240, 536 | Caption: "video caption", 537 | ReplyToMessageID: 146, 538 | }) 539 | 540 | s.Require().Nil(err) 541 | s.Require().NotNil(message) 542 | } 543 | 544 | func (s *BotTestSuite) TestSendVideoFile() { 545 | params := url.Values{ 546 | "chat_id": {"789"}, 547 | "duration": {"61"}, 548 | "width": {"1280"}, 549 | "height": {"720"}, 550 | "caption": {"funny cats"}, 551 | } 552 | data := bytes.NewBufferString("video data") 553 | file := fileField{ 554 | Source: bytes.NewBufferString("video data"), 555 | Fieldname: "video", 556 | Filename: "cats.mp4", 557 | } 558 | s.registeMultipartrRequestCheck("sendVideo", params, file) 559 | 560 | message, err := s.bot.SendVideoFile("789", data, "cats.mp4", &SendVideoOptions{ 561 | Duration: 61, 562 | Width: 1280, 563 | Height: 720, 564 | Caption: "funny cats", 565 | }) 566 | 567 | s.Require().Nil(err) 568 | s.Require().NotNil(message) 569 | } 570 | 571 | func (s *BotTestSuite) TestSendVoice() { 572 | request := `{"chat_id":"127","voice":"75ac50947bc34a3ea2efdca5000d9ad5","duration":56,"reply_to_message_id":147}` 573 | s.registerRequestCheck("sendVoice", request) 574 | 575 | message, err := s.bot.SendVoice("127", "75ac50947bc34a3ea2efdca5000d9ad5", &SendVoiceOptions{ 576 | Duration: 56, 577 | ReplyToMessageID: 147, 578 | }) 579 | 580 | s.Require().Nil(err) 581 | s.Require().NotNil(message) 582 | } 583 | 584 | func (s *BotTestSuite) TestSendVoiceFile() { 585 | params := url.Values{ 586 | "chat_id": {"101"}, 587 | "duration": {"15"}, 588 | } 589 | data := bytes.NewBufferString("voice data") 590 | file := fileField{ 591 | Source: bytes.NewBufferString("voice data"), 592 | Fieldname: "voice", 593 | Filename: "voice.ogg", 594 | } 595 | s.registeMultipartrRequestCheck("sendVoice", params, file) 596 | 597 | message, err := s.bot.SendVoiceFile("101", data, "voice.ogg", &SendVoiceOptions{ 598 | Duration: 15, 599 | }) 600 | 601 | s.Require().Nil(err) 602 | s.Require().NotNil(message) 603 | } 604 | 605 | func (s *BotTestSuite) TestSendVideoNote() { 606 | // Test without options 607 | s.registerResultWithRequestCheck("sendVideoNote", "{}", `{ 608 | "chat_id": "123", 609 | "video_note": "837y7w6gdf6sd" 610 | }`) 611 | 612 | message, err := s.bot.SendVideoNote("123", "837y7w6gdf6sd", nil) 613 | s.Require().Nil(err) 614 | s.Require().NotNil(message) 615 | 616 | httpmock.Reset() 617 | 618 | // Test with options 619 | s.registerResultWithRequestCheck("sendVideoNote", "{}", `{ 620 | "chat_id": "123", 621 | "video_note": "837y7w6gdf6sd", 622 | "duration": 22, 623 | "length": 133, 624 | "disable_notification": true, 625 | "reply_to_message_id": 39047324 626 | }`) 627 | message, err = s.bot.SendVideoNote("123", "837y7w6gdf6sd", &SendVideoNoteOptions{ 628 | Duration: 22, 629 | Length: 133, 630 | DisableNotification: true, 631 | ReplyToMessageID: 39047324, 632 | }) 633 | s.Require().Nil(err) 634 | s.Require().NotNil(message) 635 | } 636 | 637 | func (s *BotTestSuite) TestSendVideoNoteFile() { 638 | params := url.Values{ 639 | "chat_id": {"522"}, 640 | "duration": {"347"}, 641 | "length": {"3847"}, 642 | "reply_to_message_id": {"3904834"}, 643 | } 644 | data := bytes.NewBufferString("video note data") 645 | file := fileField{ 646 | Source: bytes.NewBufferString("video note data"), 647 | Fieldname: "video_note", 648 | Filename: "aaa.mp4", 649 | } 650 | s.registeMultipartrRequestCheck("sendVideoNote", params, file) 651 | 652 | message, err := s.bot.SendVideoNoteFile("522", data, "aaa.mp4", &SendVideoNoteOptions{ 653 | Duration: 347, 654 | Length: 3847, 655 | ReplyToMessageID: 3904834, 656 | }) 657 | s.Require().Nil(err) 658 | s.Require().NotNil(message) 659 | } 660 | 661 | func (s *BotTestSuite) TestSendLocation() { 662 | request := `{"chat_id":"128","latitude":22.532434,"longitude":-44.8243324,"reply_to_message_id":148}` 663 | s.registerRequestCheck("sendLocation", request) 664 | 665 | message, err := s.bot.SendLocation("128", 22.532434, -44.8243324, &SendLocationOptions{ 666 | ReplyToMessageID: 148, 667 | }) 668 | 669 | s.Require().Nil(err) 670 | s.Require().NotNil(message) 671 | } 672 | 673 | func (s *BotTestSuite) TestSendVenue() { 674 | request := `{"chat_id":"129","latitude":22.532434,"longitude":-44.8243324,"title":"Kremlin","address":"Red Square 1","foursquare_id":"1","reply_to_message_id":149}` 675 | s.registerRequestCheck("sendVenue", request) 676 | 677 | message, err := s.bot.SendVenue("129", 22.532434, -44.8243324, "Kremlin", "Red Square 1", &SendVenueOptions{ 678 | FoursquareID: "1", 679 | ReplyToMessageID: 149, 680 | }) 681 | 682 | s.Require().Nil(err, err) 683 | s.Require().NotNil(message) 684 | } 685 | 686 | func (s *BotTestSuite) TestSendContact() { 687 | request := `{"chat_id":"130","phone_number":"+79998887766","first_name":"John","last_name":"Doe","reply_to_message_id":150}` 688 | s.registerRequestCheck("sendContact", request) 689 | 690 | message, err := s.bot.SendContact("130", "+79998887766", "John", "Doe", &SendContactOptions{ 691 | ReplyToMessageID: 150, 692 | }) 693 | 694 | s.Require().Nil(err) 695 | s.Require().NotNil(message) 696 | } 697 | 698 | func (s *BotTestSuite) TestForwardMessage() { 699 | request := `{"chat_id":"131","disable_notification":true,"from_chat_id":"99","message_id":543}` 700 | s.registerRequestCheck("forwardMessage", request) 701 | 702 | message, err := s.bot.ForwardMessage("131", "99", 543, true) 703 | 704 | s.Require().Nil(err) 705 | s.Require().NotNil(message) 706 | } 707 | 708 | func (s *BotTestSuite) TestSendChatAction() { 709 | request := `{"action":"typing","chat_id":"132"}` 710 | s.registerRequestCheck("sendChatAction", request) 711 | 712 | err := s.bot.SendChatAction("132", CHAT_ACTION_TYPING) 713 | s.Require().Nil(err) 714 | } 715 | 716 | func (s *BotTestSuite) TestAnswerCallbackQuery() { 717 | request := `{"callback_query_id":"66b04f35ec624974a78f72710a3dc09d","text":"foo","show_alert":true}` 718 | s.registerRequestCheck("answerCallbackQuery", request) 719 | 720 | err := s.bot.AnswerCallbackQuery("66b04f35ec624974a78f72710a3dc09d", &AnswerCallbackQueryOptions{ 721 | Text: "foo", 722 | ShowAlert: true, 723 | }) 724 | s.Require().Nil(err) 725 | } 726 | 727 | func (s *BotTestSuite) TestKickChatMember() { 728 | request := `{"chat_id":"1","user_id":2}` 729 | s.registerRequestCheck("kickChatMember", request) 730 | 731 | err := s.bot.KickChatMember("1", 2) 732 | s.Require().Nil(err) 733 | } 734 | 735 | func (s *BotTestSuite) TestLeaveChat() { 736 | request := `{"chat_id":"143"}` 737 | s.registerRequestCheck("leaveChat", request) 738 | 739 | err := s.bot.LeaveChat("143") 740 | s.Require().Nil(err) 741 | } 742 | 743 | func (s *BotTestSuite) TestUnbanChatMember() { 744 | request := `{"chat_id":"22","user_id":33}` 745 | s.registerRequestCheck("unbanChatMember", request) 746 | 747 | err := s.bot.UnbanChatMember("22", 33) 748 | s.Require().Nil(err) 749 | } 750 | 751 | func (s *BotTestSuite) TestGetUserProfilePhotos() { 752 | params := url.Values{ 753 | "user_id": {"55"}, 754 | "limit": {"1"}, 755 | "offset": {"22"}, 756 | } 757 | s.registerResponse("getUserProfilePhotos", params, `{ 758 | "ok": true, 759 | "result": { 760 | "total_count": 1, 761 | "photos": [[{ 762 | "file_id": "111", 763 | "width": 320, 764 | "height": 240, 765 | "file_size": 15320 766 | }]] 767 | } 768 | }`) 769 | 770 | offset := 22 771 | limit := 1 772 | userPhotos, err := s.bot.GetUserProfilePhotos(55, &offset, &limit) 773 | s.Require().Nil(err) 774 | s.Require().Equal(userPhotos.TotalCount, 1) 775 | s.Require().Equal(userPhotos.Photos[0][0].FileID, "111") 776 | s.Require().Equal(userPhotos.Photos[0][0].FileSize, uint64(15320)) 777 | s.Require().Equal(userPhotos.Photos[0][0].Width, 320) 778 | s.Require().Equal(userPhotos.Photos[0][0].Height, 240) 779 | 780 | } 781 | 782 | func (s *BotTestSuite) TestSendMessage() { 783 | request := `{"reply_to_message_id":89,"parse_mode":"HTML","chat_id":"3434","text":"mss"}` 784 | s.registerRequestCheck("sendMessage", request) 785 | 786 | _, err := s.bot.SendMessage("3434", "mss", &SendMessageOptions{ 787 | ReplyToMessageID: 89, 788 | ParseMode: PARSE_MODE_HTML, 789 | }) 790 | s.Require().Nil(err) 791 | } 792 | 793 | func (s *BotTestSuite) TestSendGame() { 794 | request := `{"chat_id":"298","game_short_name":"ggg","reply_to_message_id":892}` 795 | s.registerRequestCheck("sendGame", request) 796 | 797 | _, err := s.bot.SendGame("298", "ggg", &SendGameOptions{ 798 | ReplyToMessageID: 892, 799 | }) 800 | s.Require().Nil(err) 801 | } 802 | 803 | func (s *BotTestSuite) TestSetGameScore() { 804 | request := `{"user_id":1,"score":777,"chat_id":"552","message_id":892,"inline_message_id":"stf","disable_edit_message":true}` 805 | s.registerRequestCheck("setGameScore", request) 806 | 807 | _, err := s.bot.SetGameScore(1, 777, &SetGameScoreOptions{ 808 | ChatID: "552", 809 | MessageID: int64(892), 810 | InlineMessageID: "stf", 811 | DisableEditMessage: true, 812 | }) 813 | s.Require().Nil(err) 814 | } 815 | 816 | func (s *BotTestSuite) TestGetGameHighScorese() { 817 | s.registerResponse("getGameHighScores", url.Values{ 818 | "user_id": {"91247"}, 819 | "chat_id": {"123"}, 820 | "message_id": {"892"}, 821 | }, `{ 822 | "ok": true, 823 | "result": [ 824 | { 825 | "position": 1, 826 | "score": 22, 827 | "user": { 828 | "id": 456, 829 | "first_name": "John", 830 | "last_name": "Doe", 831 | "username": "john_doe" 832 | } 833 | }, 834 | { 835 | "position": 2, 836 | "score": 11, 837 | "user": { 838 | "id": 789, 839 | "first_name": "Mohammad", 840 | "last_name": "Li", 841 | "username": "mli" 842 | } 843 | } 844 | ] 845 | }`) 846 | 847 | scores, err := s.bot.GetGameHighScores(91247, &GetGameHighScoresOptions{ 848 | ChatID: "123", 849 | MessageID: int64(892), 850 | }) 851 | s.Require().Nil(err) 852 | s.Require().Equal(len(scores), 2) 853 | s.Require().Equal(scores[0].Position, 1) 854 | s.Require().Equal(scores[0].Score, 22) 855 | s.Require().Equal(scores[0].User, User{ 856 | ID: 456, 857 | FirstName: "John", 858 | LastName: "Doe", 859 | Username: "john_doe", 860 | }) 861 | 862 | s.Require().Equal(scores[1].Position, 2) 863 | s.Require().Equal(scores[1].Score, 11) 864 | s.Require().Equal(scores[1].User, User{ 865 | ID: 789, 866 | FirstName: "Mohammad", 867 | LastName: "Li", 868 | Username: "mli", 869 | }) 870 | 871 | } 872 | 873 | func (s *BotTestSuite) TestEditMessageText() { 874 | request := `{"chat_id":"143","message_id":67,"inline_message_id":"gyt","text":"new text","parse_mode":"Markdown"}` 875 | s.registerRequestCheck("editMessageText", request) 876 | 877 | _, err := s.bot.EditMessageText("143", 67, "gyt", "new text", &EditMessageTextOptions{ 878 | ParseMode: PARSE_MODE_MARKDOWN, 879 | }) 880 | 881 | s.Require().Nil(err) 882 | } 883 | 884 | func (s *BotTestSuite) TestEditMessageCaption() { 885 | request := `{"chat_id":"490","message_id":87,"inline_message_id":"ubl","caption":"ca"}` 886 | s.registerRequestCheck("editMessageCaption", request) 887 | 888 | _, err := s.bot.EditMessageCaption("490", 87, "ubl", &EditMessageCationOptions{ 889 | Caption: "ca", 890 | }) 891 | 892 | s.Require().Nil(err) 893 | } 894 | 895 | func (s *BotTestSuite) TestEditMessageReplyMarkup() { 896 | request := `{"chat_id":"781","message_id":32,"inline_message_id":"zzt","reply_markup":{"force_reply":true,"selective":true}}` 897 | s.registerRequestCheck("editMessageReplyMarkup", request) 898 | 899 | _, err := s.bot.EditMessageReplyMarkup("781", 32, "zzt", ForceReply{ 900 | ForceReply: true, 901 | Selective: true, 902 | }) 903 | 904 | s.Require().Nil(err) 905 | } 906 | 907 | func (s *BotTestSuite) TestDeleteMessage() { 908 | s.registerResultWithRequestCheck("deleteMessage", "true", `{ 909 | "chat_id": "111", 910 | "message_id": 124 911 | }`) 912 | 913 | success, err := s.bot.DeleteMessage("111", 124) 914 | s.Require().Nil(err) 915 | s.Require().True(success) 916 | 917 | httpmock.Reset() 918 | s.registerResultWithRequestCheck("deleteMessage", "false", `{ 919 | "chat_id": "222", 920 | "message_id": 431 921 | }`) 922 | 923 | success, err = s.bot.DeleteMessage("222", 431) 924 | s.Require().Nil(err) 925 | s.Require().False(success) 926 | } 927 | 928 | func (s *BotTestSuite) TestAnswerInlineQuery() { 929 | request := `{"inline_query_id":"aaa","results":[{"type":"article","id":"124","title":"Article"}],"cache_time":42,"is_personal":true,"next_offset":"2","switch_pm_text":"yes","switch_pm_parameter":"no"}` 930 | s.registerRequestCheck("answerInlineQuery", request) 931 | 932 | results := InlineQueryResults{} 933 | results = append(results, InlineQueryResultArticle{ 934 | Type: INLINE_TYPE_RESULT_ARTICLE, 935 | ID: "124", 936 | Title: "Article", 937 | }) 938 | err := s.bot.AnswerInlineQuery("aaa", results, &AnswerInlineQueryOptions{ 939 | CacheTime: 42, 940 | IsPersonal: true, 941 | NextOffset: "2", 942 | SwitchPmText: "yes", 943 | SwitchPmParameter: "no", 944 | }) 945 | s.Require().Nil(err) 946 | } 947 | 948 | func TestBotTestSuite(t *testing.T) { 949 | suite.Run(t, new(BotTestSuite)) 950 | } 951 | --------------------------------------------------------------------------------