diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ca59d6b --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +# IDE's files +.idea +*.iml + +# Project ignores diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..87788d1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: go +go: + - '1.8' + - '1.9' + - '1.10' +before_install: + - go get -v github.com/google/go-querystring/query + - go get -v github.com/h2non/gock +script: go test -v ./... diff --git a/LICENSE b/LICENSE index c31da9c..99e0af0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2018 retailCRM - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +The MIT License (MIT) + +Copyright (c) 2015-2018 RetailDriver LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index 0b26a2e..e09cc86 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,50 @@ -# MG API Go client +[![Build Status](https://img.shields.io/travis/retailcrm/mg-transport-api-client-go/master.svg?style=flat-square)](https://travis-ci.org/retailcrm/mg-transport-api-client-go) +[![GitHub release](https://img.shields.io/github/release/retailcrm/mg-transport-api-client-go.svg?style=flat-square)](https://github.com/retailcrm/mg-transport-api-client-go/releases) +[![GoLang version](https://img.shields.io/badge/GoLang-1.8%2C%201.9%2C%201.10-blue.svg?style=flat-square)](https://golang.org/dl/) -Go client for MG + +# retailCRM Message Gateway Transport API Go client + +## Install + +```bash +go get -x github.com/retailcrm/mg-transport-api-client-go +``` + +## Usage + +```golang +package main + +import ( + "fmt" + "net/http" + + "github.com/retailcrm/mg-transport-api-client-go/v1" +) + +func main() { + var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d49bcba99be73bff503ea6") + ch := Channel{ + Type: "telegram", + Events: []string{ + "message_sent", + "message_updated", + "message_deleted", + "message_read", + }, + } + + data, status, err := c.ActivateTransportChannel(ch) + + if err != nil { + t.Errorf("%d %v", status, err) + } + + fmt.Printf("%v", data.ChannelID) +} +``` + +## Documentation + +* [GoDoc](https://godoc.org/github.com/retailcrm/mg-transport-api-client-go) diff --git a/v1/client.go b/v1/client.go new file mode 100644 index 0000000..07bad9e --- /dev/null +++ b/v1/client.go @@ -0,0 +1,272 @@ +package v1 + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "strconv" + "time" +) + +// New initialize client +func New(url string, token string) *MgClient { + return &MgClient{ + url, + token, + &http.Client{Timeout: 20 * time.Second}, + } +} + +// ActivateTransportChannel implement channel activation +// +// Example: +// +// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d49bcba99be73bff503ea6") +// +// request := ActivateRequest{ +// Type: "telegram", +// Events: [2]int{"message_sent", "message_sent"} +// } +// +// data, status, err := client.ActivateTransportChannel(request) +// +// if err != nil { +// fmt.Printf("%v", err) +// } +// +// fmt.Printf("%s\n", data.CreatedAt) +func (c *MgClient) ActivateTransportChannel(request Channel) (ActivateResponse, int, error) { + var resp ActivateResponse + outgoing, _ := json.Marshal(&request) + + data, status, err := c.PostRequest("/transport/channels", []byte(outgoing)) + if err != nil { + return resp, status, err + } + + if err := json.Unmarshal(data, &resp); err != nil { + return resp, status, err + } + + if status > http.StatusCreated || status < http.StatusOK { + return resp, status, c.Error(data) + } + + return resp, status, err +} + +// UpdateTransportChannel implement channel activation +// +// Example: +// +// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d49bcba99be73bff503ea6") +// +// request := ActivateRequest{ +// ID: 3053450384, +// Type: "telegram", +// Events: [2]int{"message_sent", "message_sent"} +// } +// +// data, status, err := client.UpdateTransportChannel(request) +// +// if err != nil { +// fmt.Printf("%v", err) +// } +// +// fmt.Printf("%s\n", data.UpdatedAt) +func (c *MgClient) UpdateTransportChannel(request Channel) (UpdateResponse, int, error) { + var resp UpdateResponse + outgoing, _ := json.Marshal(&request) + + data, status, err := c.PutRequest(fmt.Sprintf("/transport/channels/%d", request.ID), []byte(outgoing)) + if err != nil { + return resp, status, err + } + + if err := json.Unmarshal(data, &resp); err != nil { + return resp, status, err + } + + if status != http.StatusOK { + return resp, status, c.Error(data) + } + + return resp, status, err +} + +// DeactivateTransportChannel implement channel deactivation +// +// Example: +// +// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d49bcba99be73bff503ea6") +// +// data, status, err := client.DeactivateTransportChannel(3053450384) +// +// if err != nil { +// fmt.Printf("%v", err) +// } +// +// fmt.Printf("%s\n", data.DeactivatedAt) +func (c *MgClient) DeactivateTransportChannel(id uint64) (DeleteResponse, int, error) { + var resp DeleteResponse + + data, status, err := c.DeleteRequest(fmt.Sprintf("/transport/channels/%s", strconv.FormatUint(id, 10))) + if err != nil { + return resp, status, err + } + + if err := json.Unmarshal(data, &resp); err != nil { + return resp, status, err + } + + if status != http.StatusOK { + return resp, status, c.Error(data) + } + + return resp, status, err +} + +// Messages implement send message +// +// Example: +// +// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d49bcba99be73bff503ea6") +// snd := SendData{ +// SendMessage{ +// Message{ +// ExternalID: "23e23e23", +// Channel: channelId, +// Type: "text", +// Text: "hello!", +// }, +// time.Now(), +// }, +// User{ +// ExternalID: "8", +// Nickname: "@octopulus", +// Firstname: "Joe", +// }, +// channelId, +// } +// +// data, status, err := client.Messages(snd) +// +// if err != nil { +// fmt.Printf("%v", err) +// } +// +// fmt.Printf("%s\n", data.MessageID) +func (c *MgClient) Messages(request SendData) (MessagesResponse, int, error) { + var resp MessagesResponse + outgoing, _ := json.Marshal(&request) + + data, status, err := c.PostRequest("/transport/messages", []byte(outgoing)) + if err != nil { + return resp, status, err + } + + if err := json.Unmarshal(data, &resp); err != nil { + return resp, status, err + } + + if status != http.StatusOK { + return resp, status, c.Error(data) + } + + return resp, status, err +} + +// UpdateMessages implement edit message +// +// Example: +// +// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d49bcba99be73bff503ea6") +// snd := SendData{ +// SendMessage{ +// Message{ +// ExternalID: "23e23e23", +// Channel: channelId, +// Type: "text", +// Text: "hello!", +// }, +// time.Now(), +// }, +// User{ +// ExternalID: "8", +// Nickname: "@octopulus", +// Firstname: "Joe", +// }, +// channelId, +// } +// +// data, status, err := client.UpdateMessages(snd) +// +// if err != nil { +// fmt.Printf("%v", err) +// } +// +// fmt.Printf("%s\n", data.MessageID) +func (c *MgClient) UpdateMessages(request UpdateMessage) (MessagesResponse, int, error) { + var resp MessagesResponse + outgoing, _ := json.Marshal(&request) + + data, status, err := c.PutRequest("/transport/messages", []byte(outgoing)) + if err != nil { + return resp, status, err + } + + if err := json.Unmarshal(data, &resp); err != nil { + return resp, status, err + } + + if status != http.StatusOK { + return resp, status, c.Error(data) + } + + return resp, status, err +} + +// DeleteMessage implement delete message +// +// Example: +// +// var client = v1.New("https://token.url", "cb8ccf05e38a47543ad8477d49bcba99be73bff503ea6") +// +// data, status, err := client.DeleteMessage("3053450384") +// +// if err != nil { +// fmt.Printf("%v", err) +// } +// +// fmt.Printf("%s\n", data.MessageID) +func (c *MgClient) DeleteMessage(id string) (MessagesResponse, int, error) { + var resp MessagesResponse + + data, status, err := c.DeleteRequest(fmt.Sprintf("/transport/messages/%s", id)) + if err != nil { + return resp, status, err + } + + if err := json.Unmarshal(data, &resp); err != nil { + return resp, status, err + } + + if status != http.StatusOK { + return resp, status, c.Error(data) + } + + return resp, status, err +} + +func (c *MgClient) Error(info []byte) error { + var data map[string]interface{} + + if err := json.Unmarshal(info, &data); err != nil { + return err + } + + values := data["errors"].([]interface{}) + + return errors.New(values[0].(string)) +} diff --git a/v1/client_test.go b/v1/client_test.go new file mode 100644 index 0000000..afe6d16 --- /dev/null +++ b/v1/client_test.go @@ -0,0 +1,127 @@ +package v1 + +import ( + "net/http" + "os" + "strconv" + "testing" +) + +var ( + mgURL = os.Getenv("MG_URL") + mgToken = os.Getenv("MG_TOKEN") + channelId, _ = strconv.ParseUint(os.Getenv("MG_CHANNEL"), 10, 64) +) + +func client() *MgClient { + return New(mgURL, mgToken) +} + +func TestMgClient_ActivateTransportChannel(t *testing.T) { + c := client() + ch := Channel{ + ID: channelId, + Type: "telegram", + Events: []string{ + "message_sent", + "message_updated", + "message_deleted", + "message_read", + }, + } + + data, status, err := c.ActivateTransportChannel(ch) + + if err != nil { + t.Errorf("%d %v", status, err) + } + + t.Logf("%v", data.ChannelID) +} + +func TestMgClient_ActivateNewTransportChannel(t *testing.T) { + c := client() + ch := Channel{ + Type: "telegram", + Events: []string{ + "message_sent", + "message_updated", + "message_deleted", + "message_read", + }, + } + + data, status, err := c.ActivateTransportChannel(ch) + + if err != nil { + t.Errorf("%d %v", status, err) + } + + t.Logf("%v", data.ChannelID) +} + +func TestMgClient_UpdateTransportChannel(t *testing.T) { + c := client() + ch := Channel{ + ID: channelId, + Events: []string{ + "message_sent", + "message_read", + }, + } + + data, status, err := c.UpdateTransportChannel(ch) + + if status != http.StatusOK { + t.Errorf("%v", err) + } + + t.Logf("%v", data.ChannelID) +} + +func TestMgClient_DeactivateTransportChannel(t *testing.T) { + c := client() + deleteData, status, err := c.DeactivateTransportChannel(channelId) + + if err != nil { + t.Errorf("%d %v", status, err) + } + + if deleteData.DectivatedAt.String() == "" { + t.Errorf("%v", err) + } + + t.Logf("%v", deleteData.ChannelID) +} + +/*func TestMgClient_Messages(t *testing.T) { + c := client() + + snd := SendData{ + SendMessage{ + Message{ + ExternalID: "23e23e23", + Channel: channelId, + Type: "text", + Text: "hello!", + }, + time.Now(), + }, + User{ + ExternalID: "8", + Nickname: "@octopulus", + Firstname: "Joe", + }, + channelId, + } + + data, status, err := c.Messages(snd) + + if status != http.StatusOK { + t.Errorf("%v", err) + } + + if data.Time.String() == "" { + t.Errorf("%v", err) + } +}*/ diff --git a/v1/request.go b/v1/request.go new file mode 100644 index 0000000..21416ea --- /dev/null +++ b/v1/request.go @@ -0,0 +1,110 @@ +package v1 + +import ( + "bytes" + "fmt" + "io/ioutil" + "net/http" +) + +var prefix = "/api/v1" + +// GetRequest implements GET Request +func (c *MgClient) GetRequest(urlWithParameters string) ([]byte, int, error) { + var res []byte + + req, err := http.NewRequest("GET", fmt.Sprintf("%s%s%s", c.URL, prefix, urlWithParameters), nil) + if err != nil { + return res, 0, err + } + + req.Header.Set("X-Transport-Token", c.Token) + + resp, err := c.httpClient.Do(req) + if err != nil { + return res, 0, err + } + + if resp.StatusCode >= http.StatusInternalServerError { + err = fmt.Errorf("HTTP request error. Status code: %d.\n", resp.StatusCode) + return res, resp.StatusCode, err + } + + res, err = buildRawResponse(resp) + if err != nil { + return res, resp.StatusCode, err + } + + return res, resp.StatusCode, err +} + +// PostRequest implements POST Request +func (c *MgClient) PostRequest(url string, parameters []byte) ([]byte, int, error) { + return makeRequest( + "POST", + fmt.Sprintf("%s%s%s", c.URL, prefix, url), + bytes.NewBuffer(parameters), + c, + ) +} + +// PutRequest implements PUT Request +func (c *MgClient) PutRequest(url string, parameters []byte) ([]byte, int, error) { + return makeRequest( + "PUT", + fmt.Sprintf("%s%s%s", c.URL, prefix, url), + bytes.NewBuffer(parameters), + c, + ) +} + +// DeleteRequest implements DELETE Request +func (c *MgClient) DeleteRequest(url string) ([]byte, int, error) { + var buf []byte + + return makeRequest( + "DELETE", + fmt.Sprintf("%s%s%s", c.URL, prefix, url), + bytes.NewBuffer(buf), + c, + ) +} + +func makeRequest(reqType, url string, buf *bytes.Buffer, c *MgClient) ([]byte, int, error) { + var res []byte + req, err := http.NewRequest(reqType, url, buf) + if err != nil { + return res, 0, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Transport-Token", c.Token) + + resp, err := c.httpClient.Do(req) + if err != nil { + return res, 0, err + } + + if resp.StatusCode >= http.StatusInternalServerError { + err = fmt.Errorf("HTTP request error. Status code: %d.\n", resp.StatusCode) + return res, resp.StatusCode, err + } + + res, err = buildRawResponse(resp) + if err != nil { + return res, 0, err + } + + return res, resp.StatusCode, err +} + +func buildRawResponse(resp *http.Response) ([]byte, error) { + defer resp.Body.Close() + + res, err := ioutil.ReadAll(resp.Body) + if err != nil { + return res, err + } + + return res, nil +} diff --git a/v1/request_test.go b/v1/request_test.go new file mode 100644 index 0000000..b7b1f99 --- /dev/null +++ b/v1/request_test.go @@ -0,0 +1 @@ +package v1 diff --git a/v1/types.go b/v1/types.go new file mode 100644 index 0000000..7e93e16 --- /dev/null +++ b/v1/types.go @@ -0,0 +1,83 @@ +package v1 + +import ( + "net/http" + "time" +) + +// MgClient type +type MgClient struct { + URL string + Token string + httpClient *http.Client +} + +// Channel type +type Channel struct { + ID uint64 `url:"id,omitempty"` + Type string `url:"type,omitempty"` + Events []string `url:"events,omitempty,brackets"` +} + +// ActivateResponse channel activation response +type ActivateResponse struct { + ChannelID uint64 `json:"id"` + ActivatedAt time.Time `json:"activated_at"` +} + +// UpdateResponse channel update response +type UpdateResponse struct { + ChannelID uint64 `json:"id"` + UpdatedAt time.Time `json:"updated_at"` +} + +// DeleteResponse channel deactivation response +type DeleteResponse struct { + ChannelID uint64 `json:"id"` + DectivatedAt time.Time `json:"deactivated_at"` +} + +// User struct +type User struct { + ExternalID string `url:"external_id"` + Nickname string `url:"nickname"` + Firstname string `url:"first_name,omitempty"` + Lastname string `url:"last_name,omitempty"` + Avatar string `url:"avatar,omitempty"` + ProfileURL string `url:"profile_url,omitempty"` + Country string `url:"country,omitempty"` + Language string `url:"language,omitempty"` + Phone string `url:"phone,omitempty"` +} + +// Message struct +type Message struct { + ExternalID string `url:"external_id"` + Channel uint64 `url:"channel"` + Type string `url:"type"` + Text string `url:"text,omitempty"` +} + +// SendMessage struct +type SendMessage struct { + Message + SentAt time.Time `url:"sent_at,omitempty"` +} + +// UpdateMessage struct +type UpdateMessage struct { + Message + EditedAt time.Time `url:"edited_at,omitempty"` +} + +type SendData struct { + Message SendMessage `url:"message"` + User User `url:"user"` + Channel uint64 `url:"channel"` +} + +// MessagesResponse message event response +type MessagesResponse struct { + MessageID string `json:"message_id"` + Time time.Time `json:"time"` +}