From d5b441be0b3e41fa2b729c46a9679966b1540a98 Mon Sep 17 00:00:00 2001 From: Andrey Date: Thu, 15 Feb 2018 14:41:16 +0300 Subject: [PATCH] Custom http client as option. --- .travis.yml | 2 + bot.go | 110 +++++++++++++++---------- bot_test.go | 15 ++-- http/http.go => http.go | 47 +++++------ http/http_test.go | 174 ---------------------------------------- options.go | 20 +++-- types.go | 21 ++++- 7 files changed, 132 insertions(+), 257 deletions(-) rename http/http.go => http.go (60%) delete mode 100644 http/http_test.go diff --git a/.travis.yml b/.travis.yml index a2bc997..670a55a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,8 @@ language: go go: - 1.6 - 1.7 + - 1.8 + - 1.9 - tip before_install: diff --git a/bot.go b/bot.go index 135b587..a8431a0 100644 --- a/bot.go +++ b/bot.go @@ -5,9 +5,8 @@ import ( "encoding/json" "fmt" "io" + "net/http" "net/url" - - "github.com/onrik/micha/http" ) const ( @@ -22,21 +21,32 @@ type Response struct { Result json.RawMessage `json:"result"` } +// Bot telegram bot type Bot struct { + Options Me User token string updates chan Update stop bool offset uint64 - limit int - timeout int - logger Logger } // NewBot - create new bot instance func NewBot(token string, opts ...Option) (*Bot, error) { + options := Options{ + limit: 100, + timeout: 25, + logger: newLogger("micha"), + httpClient: http.DefaultClient, + } + + for _, opt := range opts { + opt(&options) + } + bot := Bot{ + Options: options, token: token, updates: make(chan Update), } @@ -46,20 +56,8 @@ func NewBot(token string, opts ...Option) (*Bot, error) { return nil, err } - options := &Options{ - Limit: 100, - Timeout: 25, - Logger: newLogger(me.Username), - } - - for _, opt := range opts { - opt(options) - } - bot.Me = *me - bot.limit = options.Limit - bot.timeout = options.Timeout - bot.logger = options.Logger + return &bot, nil } @@ -86,39 +84,67 @@ func (bot *Bot) decodeResponse(data []byte, target interface{}) error { if err := json.Unmarshal(response.Result, target); err != nil { return fmt.Errorf("Decode result error (%s)", err.Error()) - } else { - return nil } + + return nil } -// Make GET request to Telegram API +// Send GET request to Telegram API func (bot *Bot) get(method string, params url.Values, target interface{}) error { - response, err := http.Get(bot.buildURL(method) + "?" + params.Encode()) + request, err := newGetRequest(bot.buildURL(method), params) + if err != nil { + return err + } + + response, err := bot.httpClient.Do(request) if err != nil { return err - } else { - return bot.decodeResponse(response, target) } + + body, err := handleResponse(response) + if err != nil { + return err + } + + return bot.decodeResponse(body, target) } -// Make POST request to Telegram API +// Send POST request to Telegram API func (bot *Bot) post(method string, data, target interface{}) error { - response, err := http.Post(bot.buildURL(method), data) + request, err := newPostRequest(bot.buildURL(method), data) + if err != nil { + return err + } + response, err := bot.httpClient.Do(request) + if err != nil { + return err + } + + body, err := handleResponse(response) if err != nil { return err - } else { - return bot.decodeResponse(response, target) } + + return bot.decodeResponse(body, target) } -// Make POST request to Telegram API -func (bot *Bot) postMultipart(method string, file *http.File, params url.Values, target interface{}) error { - response, err := http.PostMultipart(bot.buildURL(method), file, params) +// Send POST multipart request to Telegram API +func (bot *Bot) postMultipart(method string, file *fileField, params url.Values, target interface{}) error { + request, err := newMultipartRequest(bot.buildURL(method), file, params) if err != nil { return err - } else { - return bot.decodeResponse(response, target) } + response, err := bot.httpClient.Do(request) + if err != nil { + return err + } + + body, err := handleResponse(response) + if err != nil { + return err + } + + return bot.decodeResponse(body, target) } // Use this method to receive incoming updates using long polling. @@ -177,7 +203,7 @@ func (bot *Bot) GetWebhookInfo() (*WebhookInfo, error) { } func (bot *Bot) SetWebhook(webhookURL string, options *SetWebhookOptions) error { - var file *http.File + var file *fileField params := url.Values{ "url": {webhookURL}, } @@ -189,7 +215,7 @@ func (bot *Bot) SetWebhook(webhookURL string, options *SetWebhookOptions) error params["allowed_updates"] = options.AllowedUpdates } if len(options.Certificate) > 0 { - file = &http.File{ + file = &fileField{ Source: bytes.NewBuffer(options.Certificate), Fieldname: "certificate", Filename: "certificate", @@ -247,7 +273,7 @@ func (bot *Bot) SendPhotoFile(chatID ChatID, file io.Reader, fileName string, op return nil, err } - f := &http.File{ + f := &fileField{ Source: file, Fieldname: "photo", Filename: fileName, @@ -277,7 +303,7 @@ func (bot *Bot) SendAudioFile(chatID ChatID, file io.Reader, fileName string, op return nil, err } - f := &http.File{ + f := &fileField{ Source: file, Fieldname: "audio", Filename: fileName, @@ -307,7 +333,7 @@ func (bot *Bot) SendDocumentFile(chatID ChatID, file io.Reader, fileName string, return nil, err } - f := &http.File{ + f := &fileField{ Source: file, Fieldname: "document", Filename: fileName, @@ -337,7 +363,7 @@ func (bot *Bot) SendStickerFile(chatID ChatID, file io.Reader, fileName string, return nil, err } - f := &http.File{ + f := &fileField{ Source: file, Fieldname: "sticker", Filename: fileName, @@ -367,7 +393,7 @@ func (bot *Bot) SendVideoFile(chatID ChatID, file io.Reader, fileName string, op return nil, err } - f := &http.File{ + f := &fileField{ Source: file, Fieldname: "video", Filename: fileName, @@ -399,7 +425,7 @@ func (bot *Bot) SendVoiceFile(chatID ChatID, file io.Reader, fileName string, op return nil, err } - f := &http.File{ + f := &fileField{ Source: file, Fieldname: "voice", Filename: fileName, @@ -429,7 +455,7 @@ func (bot *Bot) SendVideoNoteFile(chatID ChatID, file io.Reader, fileName string return nil, err } - f := &http.File{ + f := &fileField{ Source: file, Fieldname: "video_note", Filename: fileName, diff --git a/bot_test.go b/bot_test.go index 5ec27c7..386625c 100644 --- a/bot_test.go +++ b/bot_test.go @@ -27,10 +27,13 @@ func (s *BotTestSuite) SetupSuite() { s.bot = &Bot{ token: "111", - timeout: 25, - limit: 100, - logger: newLogger("micha"), updates: make(chan Update), + Options: Options{ + limit: 100, + timeout: 25, + logger: newLogger("micha"), + httpClient: http.DefaultClient, + }, } } @@ -128,16 +131,18 @@ func (s *BotTestSuite) TestNewBot() { s.Require().NotNil(bot) s.Require().Equal(25, bot.timeout) s.Require().Equal(100, bot.limit) - s.Require().Equal(newLogger("michabot"), bot.logger) + s.Require().Equal(newLogger("micha"), bot.logger) // With options logger := log.New(os.Stderr, "", log.LstdFlags) - bot, err = NewBot("111", WithLimit(50), WithTimeout(10), WithLogger(logger)) + httpClient := &http.Client{} + bot, err = NewBot("111", WithLimit(50), WithTimeout(10), WithLogger(logger), WithHttpClient(httpClient)) s.Require().Nil(err) s.Require().NotNil(bot) s.Require().Equal(10, bot.timeout) s.Require().Equal(50, bot.limit) s.Require().Equal(logger, bot.logger) + s.Require().Equal(httpClient, bot.httpClient) } func (s *BotTestSuite) TestErrorsHandle() { diff --git a/http/http.go b/http.go similarity index 60% rename from http/http.go rename to http.go index a2a67a2..17406b8 100644 --- a/http/http.go +++ b/http.go @@ -1,4 +1,4 @@ -package http +package micha import ( "bytes" @@ -11,7 +11,12 @@ import ( "net/url" ) -type File struct { +// HttpClient interface +type HttpClient interface { + Do(*http.Request) (*http.Response, error) +} + +type fileField struct { Source io.Reader Fieldname string Filename string @@ -20,22 +25,20 @@ type File struct { func handleResponse(response *http.Response) ([]byte, error) { defer response.Body.Close() if response.StatusCode > http.StatusBadRequest { - return nil, fmt.Errorf("Response status: %d", response.StatusCode) - } else { - return ioutil.ReadAll(response.Body) + return nil, fmt.Errorf("HTTP status: %d", response.StatusCode) } + + return ioutil.ReadAll(response.Body) } -func Get(url string) ([]byte, error) { - response, err := http.Get(url) - if err != nil { - return nil, err - } else { - return handleResponse(response) +func newGetRequest(url string, params url.Values) (*http.Request, error) { + if params != nil { + url += fmt.Sprintf("?%s", params.Encode()) } + return http.NewRequest(http.MethodGet, url, nil) } -func Post(url string, data interface{}) ([]byte, error) { +func newPostRequest(url string, data interface{}) (*http.Request, error) { body := new(bytes.Buffer) if data != nil { if err := json.NewEncoder(body).Encode(data); err != nil { @@ -43,22 +46,17 @@ func Post(url string, data interface{}) ([]byte, error) { } } - request, err := http.NewRequest("POST", url, body) + request, err := http.NewRequest(http.MethodPost, url, body) if err != nil { return nil, err } request.Header.Add("Content-Type", "application/json") - response, err := (&http.Client{}).Do(request) - if err != nil { - return nil, err - } else { - return handleResponse(response) - } + return request, nil } -func PostMultipart(url string, file *File, params url.Values) ([]byte, error) { +func newMultipartRequest(url string, file *fileField, params url.Values) (*http.Request, error) { body := new(bytes.Buffer) writer := multipart.NewWriter(body) @@ -85,17 +83,12 @@ func PostMultipart(url string, file *File, params url.Values) ([]byte, error) { return nil, err } - request, err := http.NewRequest("POST", url, body) + request, err := http.NewRequest(http.MethodPost, url, body) if err != nil { return nil, err } request.Header.Add("Content-Type", writer.FormDataContentType()) - response, err := (&http.Client{}).Do(request) - if err != nil { - return nil, err - } else { - return handleResponse(response) - } + return request, nil } diff --git a/http/http_test.go b/http/http_test.go deleted file mode 100644 index 9134ae1..0000000 --- a/http/http_test.go +++ /dev/null @@ -1,174 +0,0 @@ -package http - -import ( - "bytes" - "encoding/json" - "errors" - "io/ioutil" - "net/http" - "net/url" - "strings" - "testing" - - "github.com/jarcoal/httpmock" - "github.com/stretchr/testify/suite" -) - -type errorStruct struct{} - -func (e errorStruct) MarshalJSON() ([]byte, error) { - return nil, errors.New("marshal error") -} - -type HttpTestSuite struct { - suite.Suite -} - -func (s *HttpTestSuite) SetupSuite() { - httpmock.Activate() -} - -func (s *HttpTestSuite) TearDownSuite() { - httpmock.Deactivate() -} - -func (s *HttpTestSuite) TearDownTest() { - httpmock.Reset() -} - -func (s *HttpTestSuite) TestHandleResponse() { - data, err := handleResponse(httpmock.NewStringResponse(200, "ok")) - s.Equal(nil, err) - s.Equal([]byte("ok"), data) - - data, err = handleResponse(httpmock.NewStringResponse(400, "not ok")) - s.Equal(nil, err) - s.Equal([]byte("not ok"), data) - - data, err = handleResponse(httpmock.NewStringResponse(401, "not error")) - s.Equal("Response status: 401", err.Error()) - s.Equal(0, len(data)) -} - -func (s *HttpTestSuite) TestSuccessGet() { - url := "http://example.com" - - httpmock.RegisterResponder("GET", url, func(request *http.Request) (*http.Response, error) { - return httpmock.NewStringResponse(200, "111"), nil - }) - - response, err := Get(url) - s.Equal(nil, err) - s.Equal([]byte("111"), response) -} - -func (s *HttpTestSuite) TestErrorGet() { - url := "http://example.com" - httpmock.RegisterResponder("GET", url, func(request *http.Request) (*http.Response, error) { - return nil, errors.New("error") - }) - - response, err := Get(url) - s.NotEqual(nil, err) - s.Equal("Get http://example.com: error", err.Error()) - s.Equal(0, len(response)) -} - -func (s *HttpTestSuite) TestSuccessPost() { - url := "http://example.com" - data := map[string]string{ - "foo": "bar", - } - - httpmock.RegisterResponder("POST", url, func(request *http.Request) (*http.Response, error) { - requestData := map[string]string{} - defer request.Body.Close() - - err := json.NewDecoder(request.Body).Decode(&requestData) - s.Equal(nil, err) - s.Equal(data, requestData) - s.Equal("application/json", request.Header.Get("Content-Type")) - return httpmock.NewStringResponse(200, "ok"), nil - }) - - response, err := Post(url, data) - s.Equal(nil, err) - s.Equal([]byte("ok"), response) -} - -func (s *HttpTestSuite) TestErrorPost() { - url := "http://example.com" - httpmock.RegisterResponder("POST", url, func(request *http.Request) (*http.Response, error) { - return nil, errors.New("error") - }) - - data := errorStruct{} - response, err := Post(url, data) - s.NotEqual(nil, err) - s.True(strings.HasPrefix(err.Error(), "Encode data error")) - - response, err = Post(url, nil) - s.NotEqual(nil, err) - s.Equal("Post http://example.com: error", err.Error()) - s.Equal(0, len(response)) -} - -func (s *HttpTestSuite) TestSuccessPostMultipart() { - data := url.Values{ - "foo": {"bar"}, - } - url := "http://example.com" - - httpmock.RegisterResponder("POST", url, func(request *http.Request) (*http.Response, error) { - contentType := request.Header.Get("Content-Type") - s.True(strings.HasPrefix(contentType, "multipart/form-data; boundary=")) - - defer request.Body.Close() - err := request.ParseMultipartForm(1024) - s.Equal(nil, err) - s.Equal(request.MultipartForm.Value["foo"], []string{"bar"}) - - files := request.MultipartForm.File["file"] - s.Equal(1, len(files)) - s.Equal("somefile.ext", files[0].Filename) - file, err := files[0].Open() - s.Equal(nil, err) - - defer file.Close() - data, err := ioutil.ReadAll(file) - s.Equal(nil, err) - s.Equal([]byte("filedata"), data) - - return httpmock.NewStringResponse(200, "ok"), nil - }) - - file := &File{ - Source: bytes.NewBufferString("filedata"), - Fieldname: "file", - Filename: "somefile.ext", - } - response, err := PostMultipart(url, file, data) - s.Equal(nil, err) - s.Equal([]byte("ok"), response) -} - -func (s *HttpTestSuite) TestErrorPostMultipart() { - url := "http://example.com" - httpmock.RegisterResponder("POST", url, func(request *http.Request) (*http.Response, error) { - return nil, errors.New("error") - }) - - file := &File{ - Source: bytes.NewBufferString("filedata"), - Fieldname: "file", - Filename: "somefile.ext", - } - response, err := PostMultipart(url, file, nil) - s.NotEqual(nil, err) - s.Equal("Post http://example.com: error", err.Error()) - s.Equal(0, len(response)) -} - -func TestHttpTestSuite(t *testing.T) { - suite.Run(t, new(HttpTestSuite)) -} diff --git a/options.go b/options.go index f018dc0..57ca043 100644 --- a/options.go +++ b/options.go @@ -1,9 +1,10 @@ package micha type Options struct { - Limit int - Timeout int - Logger Logger + limit int + timeout int + logger Logger + httpClient HttpClient } type Option func(*Options) @@ -12,7 +13,7 @@ type Option func(*Options) // Values between 1—100 are accepted. Defaults to 100. func WithLimit(limit int) Option { return func(o *Options) { - o.Limit = limit + o.limit = limit } } @@ -20,14 +21,21 @@ func WithLimit(limit int) Option { // Defaults to 25 func WithTimeout(timeout int) Option { return func(o *Options) { - o.Timeout = timeout + o.timeout = timeout } } // WithLogger - set logger func WithLogger(logger Logger) Option { return func(o *Options) { - o.Logger = logger + o.logger = logger + } +} + +// WithHttpClient - set custom http client +func WithHttpClient(httpClient HttpClient) Option { + return func(o *Options) { + o.httpClient = httpClient } } diff --git a/types.go b/types.go index 7457655..9c0493d 100644 --- a/types.go +++ b/types.go @@ -314,9 +314,11 @@ type InlineKeyboardButton struct { Text string `json:"text,omitempty"` // Optional - URL string `json:"url,omitempty"` - CallbackData string `json:"callback_data,omitempty"` - SwitchInlineQuery string `json:"switch_inline_query,omitempty"` + URL string `json:"url,omitempty"` + CallbackData string `json:"callback_data,omitempty"` + SwitchInlineQuery string `json:"switch_inline_query,omitempty"` + SwitchInlineQueryCurrentChat string `json:"switch_inline_query_current_chat,omitempty"` + Pay bool `json:"pay,omitempty"` } // This object represents an inline keyboard that appears right next to the message it belongs to. @@ -351,7 +353,9 @@ type CallbackQuery struct { From User `json:"from"` Message *Message `json:"message"` InlineMessageID string `json:"inline_message_id"` + ChatInstance string `json:"chat_instance"` Data string `json:"data"` + GameShortName string `json:"game_short_name"` } // This object represents an incoming inline query. @@ -387,3 +391,14 @@ type WebhookInfo struct { MaxConnections int `json:"max_connections"` AllowedUpdates []string `json:"allowed_updates"` } + +type Invoice struct { + Title string + Description string + StartParameter string + Currency string + TotalAmount int +} + +type ShippingAddress struct { +}