This commit is contained in:
Andrey Parhomenko 2024-01-15 00:04:00 +03:00
commit 2c765d9bfd
14 changed files with 628 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
*.exe
*.exe~

92
amocrm.go Normal file
View file

@ -0,0 +1,92 @@
package amo
import (
"fmt"
"vultras.su/core/amo/api"
"vultras.su/core/amo/companies"
"vultras.su/core/amo/contacts"
"vultras.su/core/amo/leads"
"vultras.su/core/amo/users"
)
type IAmoClient interface {
GetUser(userId string) (*users.User, error)
GetLead(leadId string, query string) (*leads.Lead, error)
UpdateLead(lead *leads.Lead) error
GetCompany(companyId string, query string) (*companies.Company, error)
UpdateCompany(company *companies.Company) error
GetContact(contactId string, query string) (*contacts.Contact, error)
UpdateContact(contact *contacts.Contact) error
}
type Client struct {
Api *api.Client
}
//goland:noinspection GoUnusedExportedFunction
func NewAmoClient(options *api.ClientOptions) (*Client, error) {
apiClient, err := api.NewClient(options)
if err != nil {
return nil, err
}
return &Client{
Api: apiClient,
}, nil
}
func (client *Client) updateEntity(url string, id int, body interface{}) error {
err := client.Api.Patch(fmt.Sprintf("%s/%d", url, id), body, nil)
return err
}
func (client *Client) GetUser(userId string) (*users.User, error) {
user := new(users.User)
err := client.Api.Get(fmt.Sprintf("/api/v4/users/%s", userId), user)
return user, err
}
func (client *Client) GetLead(leadId string, query string) (*leads.Lead, error) {
deal := new(leads.Lead)
resource := fmt.Sprintf("/api/v4/leads/%s", leadId)
if len(query) != 0 {
resource = resource + "?" + query
}
err := client.Api.Get(resource, deal)
return deal, err
}
func (client *Client) UpdateLead(lead *leads.Lead) error {
return client.updateEntity("/api/v4/leads", lead.ID, lead)
}
func (client *Client) GetCompany(companyId string, query string) (*companies.Company, error) {
deal := new(companies.Company)
resource := fmt.Sprintf("/api/v4/companies/%s", companyId)
if len(query) != 0 {
resource = resource + "?" + query
}
err := client.Api.Get(resource, deal)
return deal, err
}
func (client *Client) UpdateCompany(company *companies.Company) error {
return client.updateEntity("/api/v4/companies", company.ID, company)
}
func (client *Client) GetContact(contactId string, query string) (*contacts.Contact, error) {
deal := new(contacts.Contact)
resource := fmt.Sprintf("/api/v4/contacts/%s", contactId)
if len(query) != 0 {
resource = resource + "?" + query
}
err := client.Api.Get(resource, deal)
return deal, err
}
func (client *Client) UpdateContact(contact *contacts.Contact) error {
return client.updateEntity("/api/v4/contacts", contact.ID, contact)
}

165
api/api.go Normal file
View file

@ -0,0 +1,165 @@
package api
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
)
const (
DefaultContentType = "application/json"
DefaultAccept = DefaultContentType
DefaultCacheControl = "no-cache"
)
type ClientOptions struct {
Url string
ClientId string
ClientSecret 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() (*OauthTokenResponse, error) {
result := new(OauthTokenResponse)
request := map[string]string{
"client_id": api.options.ClientId,
"client_secret": api.options.ClientSecret,
"grant_type": "refresh_token",
"refresh_token": api.options.RefreshToken,
"redirect_uri": api.options.Url,
}
err := api.doRequest("/oauth2/access_token", requestOptions{
HttpMethod: http.MethodPost,
Body: request,
Headers: getHeaders(""),
}, result)
return result, err
}
func (api *Client) Get(resource string, result interface{}) error {
return api.doRequest(resource, requestOptions{
HttpMethod: http.MethodGet,
Body: nil,
Headers: getHeaders(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: getHeaders(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: getHeaders(api.options.AccessToken),
}, result)
}
func getHeaders(token string) map[string]string {
headers := map[string]string{
"Accept": DefaultAccept,
"Cache-Control": DefaultCacheControl,
"Content-Type": DefaultContentType,
}
if len(token) > 0 {
headers["Authorization"] = fmt.Sprintf("Bearer %s", token)
}
return headers
}

14
common/common.go Normal file
View file

@ -0,0 +1,14 @@
package common
type Values struct {
Value interface{} `json:"value,omitempty"`
EnumID int `json:"enum_id,omitempty"`
Enum string `json:"enum,omitempty"`
}
type CustomFieldsValue struct {
FieldID int `json:"field_id"`
FieldName string `json:"field_name,omitempty"`
FieldCode string `json:"field_code,omitempty"`
FieldType string `json:"field_type,omitempty"`
Values []Values `json:"values"`
}

36
companies/companies.go Normal file
View file

@ -0,0 +1,36 @@
package companies
import "vultras.su/core/amo/common"
type Company struct {
ID int `json:"id"`
Name string `json:"name,omitempty"`
ResponsibleUserID int `json:"responsible_user_id,omitempty"`
GroupID int `json:"group_id,omitempty"`
CreatedBy int `json:"created_by,omitempty"`
UpdatedBy int `json:"updated_by,omitempty"`
CreatedAt int `json:"created_at,omitempty"`
UpdatedAt int `json:"updated_at,omitempty"`
ClosestTaskAt interface{} `json:"closest_task_at,omitempty"`
CustomFieldsValues []common.CustomFieldsValue `json:"custom_fields_values,omitempty"`
AccountID int `json:"account_id,omitempty"`
Links Links `json:"_links,omitempty"`
Embedded Embedded `json:"_embedded,omitempty"`
}
type Self struct {
Href string `json:"href"`
}
type Links struct {
Self Self `json:"self"`
}
type Contacts struct {
ID int `json:"id"`
}
type Embedded struct {
Tags []interface{} `json:"tags"`
Contacts []*Contacts `json:"contacts"`
}

57
contacts/contacts.go Normal file
View file

@ -0,0 +1,57 @@
package contacts
import "vultras.su/core/amo/common"
type Contact struct {
ID int `json:"id"`
Name string `json:"name,omitempty"`
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
ResponsibleUserID int `json:"responsible_user_id,omitempty"`
GroupID int `json:"group_id,omitempty"`
CreatedBy int `json:"created_by,omitempty"`
UpdatedBy int `json:"updated_by,omitempty"`
CreatedAt int `json:"created_at,omitempty"`
UpdatedAt int `json:"updated_at,omitempty"`
ClosestTaskAt interface{} `json:"closest_task_at,omitempty"`
CustomFieldsValues []common.CustomFieldsValue `json:"custom_fields_values,omitempty"`
AccountID int `json:"account_id,omitempty"`
Links Links `json:"_links,omitempty"`
Embedded Embedded `json:"_embedded,omitempty"`
}
type Values struct {
Value string `json:"value"`
EnumID int `json:"enum_id"`
Enum string `json:"enum"`
}
type Self struct {
Href string `json:"href"`
}
type Links struct {
Self Self `json:"self"`
}
type Leads struct {
ID int `json:"id"`
Links Links `json:"_links"`
}
type Customers struct {
ID int `json:"id"`
Links Links `json:"_links"`
}
type Companies struct {
ID int `json:"id"`
Links Links `json:"_links"`
}
type Embedded struct {
Tags []interface{} `json:"tags"`
Leads []Leads `json:"leads"`
Customers []Customers `json:"customers"`
CatalogElements []interface{} `json:"catalog_elements"`
Companies []Companies `json:"companies"`
}

8
go.mod Normal file
View file

@ -0,0 +1,8 @@
module vultras.su/core/amo
go 1.15
require (
github.com/gorilla/schema v1.2.0
github.com/stretchr/testify v1.6.1
)

13
go.sum Normal file
View file

@ -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=

79
leads/leads.go Normal file
View file

@ -0,0 +1,79 @@
package leads
import "vultras.su/core/amo/common"
type Lead struct {
ID int `json:"id"`
Name string `json:"name,omitempty"`
Price int `json:"price,omitempty"`
ResponsibleUserID int `json:"responsible_user_id,omitempty"`
GroupID int `json:"group_id,omitempty"`
StatusID int `json:"status_id,omitempty"`
PipelineID int `json:"pipeline_id,omitempty"`
LossReasonID int `json:"loss_reason_id,omitempty"`
SourceID interface{} `json:"source_id,omitempty"`
CreatedBy int `json:"created_by,omitempty"`
UpdatedBy int `json:"updated_by,omitempty"`
CreatedAt int `json:"created_at,omitempty"`
UpdatedAt int `json:"updated_at,omitempty"`
ClosedAt int `json:"closed_at,omitempty"`
ClosestTaskAt interface{} `json:"closest_task_at,omitempty"`
IsDeleted bool `json:"is_deleted,omitempty"`
CustomFieldsValues []*common.CustomFieldsValue `json:"custom_fields_values,omitempty"`
Score interface{} `json:"score,omitempty"`
AccountID int `json:"account_id,omitempty"`
IsPriceModifiedByRobot bool `json:"is_price_modified_by_robot,omitempty"`
Links Links `json:"_links,omitempty"`
Embedded Embedded `json:"_embedded,omitempty"`
}
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"`
}

22
license.txt Normal file
View file

@ -0,0 +1,22 @@
Copyright 2024 (c) surdeus
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.

4
readme.md Normal file
View file

@ -0,0 +1,4 @@
# amo
AmoCRM API implementation in Go.

10
users/users.go Normal file
View file

@ -0,0 +1,10 @@
package users
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"`
}

48
webhooks/webhook.go Normal file
View file

@ -0,0 +1,48 @@
package webhooks
import (
"vultras.su/core/bond/urlenc"
"vultras.su/core/bond/jsons"
"log"
"net/url"
"strings"
)
type Request struct {
Leads Leads `schema:"leads"`
Account Account `schema:"account"`
}
type Leads struct {
Status jsons.ArrayMap[Status] `schema:"status"`
Add jsons.ArrayMap[Status] `schema:"add"`
}
type Status struct {
Id jsons.Int `schema:"id"`
StatusId jsons.Int `schema:"status_id"`
PipelineId jsons.Int `schema:"pipeline_id"`
OldStatusId jsons.Int `schema:"old_status_id"`
OldPipelineId jsons.Int `schema:"old_pipeline_id"`
}
type Account struct {
Id jsons.Int `schema:"id"`
SubDomain string `schema:"subdomain"`
}
func NewFromString(body string) (*WebhookRequest, error) {
ret := &WebhookRequest{}
err := urlenc.Unmarsal(ret)
if err != nil {
return nil, err
}
return ret, nil
}
func (hook *WebhookRequest) GetLeadId() string {
if hook.Leads.Status != nil {
return hook.Leads.Status[0].Id
}
return hook.Leads.Add[0].Id
}

77
webhooks/webhook_test.go Normal file
View file

@ -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)
}
}