diff --git a/.gitignore b/.gitignore index 66fd13c..d1baad6 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ # Dependency directories (remove the comment below to include it) # vendor/ +.idea \ No newline at end of file diff --git a/amocrm.go b/amocrm.go new file mode 100644 index 0000000..170cd87 --- /dev/null +++ b/amocrm.go @@ -0,0 +1,39 @@ +package go_amo + +import ( + "fmt" + "github.com/qdimka/go-amo/api" + "github.com/qdimka/go-amo/models" +) + +type AmoClient struct { + api *api.Client +} + +func NewAmoClient(options *api.ClientOptions) (*AmoClient, error) { + apiClient, err := api.NewClient(options) + if err != nil { + return nil, err + } + + return &AmoClient{ + api: apiClient, + }, nil +} + +func (client *AmoClient) GetLead(leadId string) (*models.Lead, error) { + deal := new(models.Lead) + err := client.api.Get(fmt.Sprintf("/api/v4/leads/%s", leadId), deal) + return deal, err +} + +func (client *AmoClient) GetUser(userId string) (*models.User, error) { + user := new(models.User) + err := client.api.Get(fmt.Sprintf("/api/v4/users/%s", userId), user) + return user, err +} + +func (client *AmoClient) UpdateLead(lead *models.Lead) error { + err := client.api.Patch(fmt.Sprintf("/api/v4/leads/%d", lead.ID), lead, nil) + return err +} diff --git a/api/api.go b/api/api.go new file mode 100644 index 0000000..048caa0 --- /dev/null +++ b/api/api.go @@ -0,0 +1,162 @@ +package api + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" +) + +type ClientOptions struct { + Url string + AccessToken string + RefreshToken string +} + +type Client struct { + options *ClientOptions + BaseUrl *url.URL +} + +type requestOptions struct { + HttpMethod string + Body interface{} + Headers map[string]string +} + +type OauthTokenResponse struct { + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + AccessToken string `json:"access_token"` + RefreshToken string `json:"refresh_token"` +} + +func NewClient(options *ClientOptions) (*Client, error) { + if len(options.AccessToken) == 0 || len(options.RefreshToken) == 0 || len(options.Url) == 0 { + return nil, errors.New("AmoCrm: Invalid options") + } + + resolvedUrl, err := url.Parse(options.Url) + if err != nil { + return nil, err + } + + return &Client{ + options: options, + BaseUrl: resolvedUrl, + }, nil +} + +func (api *Client) doRequest(resourceUrl string, requestParams requestOptions, result interface{}) error { + resolvedUrl, err := url.Parse(resourceUrl) + if err != nil { + return err + } + + requestUrl := api.BaseUrl.ResolveReference(resolvedUrl) + + requestBody := new(bytes.Buffer) + if requestParams.Body != nil { + encoder := json.NewEncoder(requestBody) + if err := encoder.Encode(requestParams.Body); err != nil { + return err + } + } + + request, err := http.NewRequest(requestParams.HttpMethod, requestUrl.String(), requestBody) + if err != nil { + return err + } + + if requestParams.Headers != nil { + for k, v := range requestParams.Headers { + request.Header.Set(k, v) + } + } + + response, err := http.DefaultClient.Do(request) + if err != nil { + return errors.New(fmt.Sprintf("Request error: %s %d %s %s", err.Error(), response.StatusCode, requestParams.HttpMethod, resourceUrl)) + } + defer response.Body.Close() + + if response.StatusCode >= 400 { + bodyBytes, _ := ioutil.ReadAll(response.Body) + return errors.New(fmt.Sprintf("%s %d %s %s", string(bodyBytes), response.StatusCode, requestParams.HttpMethod, resourceUrl)) + } + + if result != nil { + decoder := json.NewDecoder(response.Body) + err = decoder.Decode(result) + if err != nil { + return err + } + } + + return nil +} + +func (api *Client) RefreshToken() (*map[string]interface{}, error) { + result := new(map[string]interface{}) + request := map[string]string{ + "client_id": "7c08f8a9-c49d-4378-890c-a6dba79f88f9", + "client_secret": "SDqfNBxxk98zq6CRjLN7tHeyVHS0EAwlQkirAx0i71s81N9fXGPlTlkoxIOZd6Rg", + "grant_type": "refresh_token", + "refresh_token": api.options.RefreshToken, + "redirect_uri": "https://ddamosmartheadru.amocrm.ru", + } + + err := api.doRequest("/oauth2/access_token", requestOptions{ + HttpMethod: http.MethodPost, + Body: request, + Headers: map[string]string{ + "Accept": "application/json", + "Cache-Control": "no-cache", + "Content-Type": "application/json", + }, + }, result) + + return result, err +} + +func (api *Client) Get(resource string, result interface{}) error { + return api.doRequest(resource, requestOptions{ + HttpMethod: http.MethodGet, + Body: nil, + Headers: map[string]string{ + "Accept": "application/json", + "Cache-Control": "no-cache", + "Content-Type": "application/json", + "Authorization": fmt.Sprintf("Bearer %s", api.options.AccessToken), + }, + }, result) +} + +func (api *Client) Post(resource string, request interface{}, result interface{}) error { + return api.doRequest(resource, requestOptions{ + HttpMethod: http.MethodPost, + Body: request, + Headers: map[string]string{ + "Accept": "application/json", + "Cache-Control": "no-cache", + "Content-Type": "application/json", + "Authorization": fmt.Sprintf("Bearer %s", api.options.AccessToken), + }, + }, result) +} + +func (api *Client) Patch(resource string, request interface{}, result interface{}) error { + return api.doRequest(resource, requestOptions{ + HttpMethod: http.MethodPatch, + Body: request, + Headers: map[string]string{ + "Accept": "application/json", + "Cache-Control": "no-cache", + "Content-Type": "application/json", + "Authorization": fmt.Sprintf("Bearer %s", api.options.AccessToken), + }, + }, result) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..aa2d3b8 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/qdimka/go-amo + +go 1.15 + +require ( + github.com/gorilla/schema v1.2.0 + github.com/stretchr/testify v1.6.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..ba8b901 --- /dev/null +++ b/go.sum @@ -0,0 +1,13 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= +github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/models/leads.go b/models/leads.go new file mode 100644 index 0000000..6cc351b --- /dev/null +++ b/models/leads.go @@ -0,0 +1,87 @@ +package models + +type Lead struct { + ID int `json:"id"` + Name string `json:"name"` + Price int `json:"price"` + ResponsibleUserID int `json:"responsible_user_id"` + GroupID int `json:"group_id"` + StatusID int `json:"status_id"` + PipelineID int `json:"pipeline_id"` + LossReasonID int `json:"loss_reason_id,omitempty"` + SourceID interface{} `json:"source_id"` + CreatedBy int `json:"created_by"` + UpdatedBy int `json:"updated_by"` + CreatedAt int `json:"created_at"` + UpdatedAt int `json:"updated_at"` + ClosedAt int `json:"closed_at"` + ClosestTaskAt interface{} `json:"closest_task_at"` + IsDeleted bool `json:"is_deleted"` + CustomFieldsValues []*CustomFieldsValue `json:"custom_fields_values"` + Score interface{} `json:"score"` + AccountID int `json:"account_id"` + IsPriceModifiedByRobot bool `json:"is_price_modified_by_robot"` + Links Links `json:"_links"` + Embedded Embedded `json:"_embedded"` +} + +type Self struct { + Href string `json:"href"` +} + +type Links struct { + Self Self `json:"self"` +} + +type Tags struct { + ID int `json:"id"` + Name string `json:"name"` +} + +type Metadata struct { + Quantity int `json:"quantity"` + CatalogID int `json:"catalog_id"` +} + +type CatalogElements struct { + ID int `json:"id"` + Metadata Metadata `json:"metadata"` +} + +type LossReason struct { + ID int `json:"id"` + Name string `json:"name"` + Sort int `json:"sort"` + CreatedAt int `json:"created_at"` + UpdatedAt int `json:"updated_at"` + Links Links `json:"_links"` +} + +type Companies struct { + ID int `json:"id"` + Links Links `json:"_links"` +} + +type Contacts struct { + ID int `json:"id"` + IsMain bool `json:"is_main"` + Links Links `json:"_links"` +} + +type Embedded struct { + Tags []*Tags `json:"tags"` + CatalogElements []*CatalogElements `json:"catalog_elements"` + LossReason []*LossReason `json:"loss_reason"` + Companies []*Companies `json:"companies"` + Contacts []*Contacts `json:"contacts"` +} + +type Value struct { + Value interface{} `json:"value"` +} + +type CustomFieldsValue struct { + FieldID int `json:"field_id"` + FieldCode string `json:"field_code"` + Values []*Value `json:"values"` +} diff --git a/models/users.go b/models/users.go new file mode 100644 index 0000000..96e6987 --- /dev/null +++ b/models/users.go @@ -0,0 +1,10 @@ +package models + +type User struct { + ID int `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Lang string `json:"lang"` + Rights interface{} `json:"rights"` + Links interface{} `json:"_links"` +} diff --git a/webhooks/webhook.go b/webhooks/webhook.go new file mode 100644 index 0000000..5db6056 --- /dev/null +++ b/webhooks/webhook.go @@ -0,0 +1,62 @@ +package webhooks + +import ( + "github.com/gorilla/schema" + "log" + "net/url" + "strings" +) + +type WebhookRequest struct { + Leads Leads `schema:"leads"` + Account Account `schema:"account"` +} + +type Leads struct { + Status []Status `schema:"status"` + Add []Status `schema:"add"` +} + +type Status struct { + Id string `schema:"id"` + StatusId string `schema:"status_id"` + PipelineId string `schema:"pipeline_id"` + OldStatusId string `schema:"old_status_id"` + OldPipelineId string `schema:"old_pipeline_id"` +} + +type Account struct { + Id string `schema:"id"` + SubDomain string `schema:"subdomain"` +} + +func NewFromString(body string) (*WebhookRequest, error) { + decodedBody, err := url.QueryUnescape(body) + if err != nil { + log.Fatal(err) + return nil, err + } + + replacer := strings.NewReplacer("][", ".", "[", ".", "]", "") + decodedBody = replacer.Replace(decodedBody) + + bodyMap := make(map[string][]string) + + for _, value := range strings.Split(decodedBody, "&") { + parameter := strings.Split(value, "=") + bodyMap[parameter[0]] = []string{parameter[1]} + } + + webhook := new(WebhookRequest) + + err = schema.NewDecoder().Decode(webhook, bodyMap) + + return webhook, err +} + +func (hook *WebhookRequest) GetLeadId() string { + if hook.Leads.Status != nil { + return hook.Leads.Status[0].Id + } + return hook.Leads.Add[0].Id +} diff --git a/webhooks/webhook_test.go b/webhooks/webhook_test.go new file mode 100644 index 0000000..aae77ba --- /dev/null +++ b/webhooks/webhook_test.go @@ -0,0 +1,77 @@ +package webhooks + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_Webhook_MoveIntoStage(t *testing.T) { + requestString := "leads[status][0][id]=2050297&" + + "leads[status][0][status_id]=35573056&" + + "leads[status][0][pipeline_id]=3643927&" + + "leads[status][0][old_status_id]=35572897&" + + "leads[status][0][old_pipeline_id]=3643927&" + + "account[id]=29085955&" + + "account[subdomain]=domain" + + expected := &WebhookRequest{ + Leads: Leads{ + Status: []Status{ + { + Id: "2050297", + StatusId: "35573056", + PipelineId: "3643927", + OldStatusId: "35572897", + OldPipelineId: "3643927", + }, + }, + }, + Account: Account{ + Id: "29085955", + SubDomain: "domain", + }, + } + + webhook, err := NewFromString(requestString) + + if err != nil { + t.Fail() + } + + if assert.NotNil(t, webhook) { + assert.Equal(t, webhook, expected) + } +} + +func Test_Webhook_CreateIntoStage(t *testing.T) { + requestString := "leads[add][0][id]=2232929&" + + "leads[add][0][status_id]=35573056&" + + "leads[add][0][pipeline_id]=3643927&" + + "account[id]=29085955&account[subdomain]=domain" + + expected := &WebhookRequest{ + Leads: Leads{ + Add: []Status{ + { + Id: "2232929", + StatusId: "35573056", + PipelineId: "3643927", + }, + }, + }, + Account: Account{ + Id: "29085955", + SubDomain: "domain", + }, + } + + webhook, err := NewFromString(requestString) + + if err != nil { + t.Fail() + } + + if assert.NotNil(t, webhook) { + assert.Equal(t, webhook, expected) + } +}