diff --git a/.gitignore b/.gitignore index d12b833..e5ddf76 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ *.exe~ .env secret.json -test +/exe/ diff --git a/amocrm.go b/amocrm.go index fd1a0e8..e081944 100644 --- a/amocrm.go +++ b/amocrm.go @@ -1,132 +1,28 @@ package amo import ( - "fmt" - "time" - "errors" "surdeus.su/core/amo/api" - "surdeus.su/core/amo/companies" - "surdeus.su/core/amo/contacts" - "surdeus.su/core/amo/leads" - "surdeus.su/core/amo/users" - "surdeus.su/core/amo/events" ) -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 TokenPair = api.TokenPair -type Options = api.ClientOptions +const ( + // Maximum entities for once. + // It is set to be the most effective + // but in fact can be greater. + MaxEnt = 50 +) type Client struct { - Api *api.Client + API *api.Client } -type OauthTokenResponse = api.OauthTokenResponse - -func NewAmoClient(secretPath string) (*Client, error) { - apiClient, err := api.NewApi(secretPath) +func NewClient(secretPath string) (*Client, error) { + apiClient, err := api.NewAPI(secretPath) if err != nil { return nil, err } return &Client{ - Api: apiClient, + 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 int) (*users.User, error) { - user := new(users.User) - err := client.Api.Get(fmt.Sprintf("/api/v4/users/%d", userId), user) - return user, err -} - -func (client *Client) GetLead(leadId int, query string) (*leads.Lead, error) { - deal := new(leads.Lead) - resource := fmt.Sprintf("/api/v4/leads/%d", leadId) - if query != "" { - 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 int, query string) (*companies.Company, error) { - deal := new(companies.Company) - resource := fmt.Sprintf("/api/v4/companies/%d", companyId) - if query != "" { - resource = resource + "?" + query - } - - err := client.Api.Get(resource, deal) - return deal, err -} - -// Returns the events from AmoCRM by specified request. -// If there are no such events returns an empty slice of events. -func (client *Client) GetEvents(req events.EventsRequest) ([]events.Event, error) { - res := "/api/v4/events" - format := req.Format() - if format != "" { - res += "?" + format - } - - var abs bool - ret := []events.Event{} - for { - resp := events.EventsResponse{} - err := client.Api.Get(res, &resp, abs) - if err != nil { - // Return empty if no content avialable. - if errors.Is(err, api.NoContentErr) { - return ret, nil - } - return nil, err - } - ret = append(ret, resp.Embedded.Events...) - if resp.Links.Next.Href == "" { - break - } - time.Sleep(time.Millisecond * 300) - abs = true - res = resp.Links.Next.Href - } - - return ret, nil -} - -func (client *Client) UpdateCompany(company *companies.Company) error { - return client.updateEntity("/api/v4/companies", company.Id, company) -} - -func (client *Client) GetContact(contactId int, query string) (*contacts.Contact, error) { - deal := new(contacts.Contact) - resource := fmt.Sprintf("/api/v4/contacts/%d", contactId) - if query != "" { - 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) -} diff --git a/api/api.go b/api/api.go index 80fb1f8..905ba81 100644 --- a/api/api.go +++ b/api/api.go @@ -1,35 +1,28 @@ package api import ( - "encoding/json" - "io/ioutil" - "net/http" "net/url" - "errors" - "bytes" "time" - "fmt" "log" - "io" "os" ) const ( - DefaultContentType = "application/json" - DefaultAccept = DefaultContentType + DefaultContentType = "application/json" + DefaultAccept = DefaultContentType DefaultCacheControl = "no-cache" ) type ClientOptions struct { - Url string `json:"url"` - RedirectUrl string `json:"redirect_url"` + URL string `json:"url"` + RedirectURL string `json:"redirect_url"` AuthCode string `json:"auth_code"` - ClientId string `json:"client_id"` + ClientId string `json:"client_id"` ClientSecret string `json:"client_secret"` - AccessToken string `json:"access_token"` + AccessToken string `json:"access_token"` ExpirationAt time.Time `json:"expiration_at,omitempty"` RefreshToken string `json:"refresh_token"` @@ -37,23 +30,16 @@ type ClientOptions struct { type Client struct { *log.Logger - options *ClientOptions - BaseUrl *url.URL + options ClientOptions + BaseURL *url.URL secretStoreFilePath string Debug bool } -type requestOptions struct { - HttpMethod string - Body interface{} - Headers map[string]string - Abs bool -} - -type OauthTokenResponse struct { - TokenType string `json:"token_type"` - ExpiresIn int `json:"expires_in"` - AccessToken string `json:"access_token"` +type OAuthTokenResponse struct { + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + AccessToken string `json:"access_token"` RefreshToken string `json:"refresh_token"` } @@ -61,30 +47,34 @@ type TokenPair struct { Access, Refresh string } -func NewApi(secretPath string) (*Client, error) { +func NewAPI(secretPath string) (*Client, error) { client := &Client{ - Logger: log.New(os.Stdout, "AmoCRM client: ", log.Ldate | log.Ltime), + Logger: log.New( + os.Stdout, + "AmoCRM client: ", + log.Ldate | log.Ltime, + ), secretStoreFilePath: secretPath, } options, err := client.readSecret() if err != nil { - return nil, NewApiError(err) + return nil, NewErrorAPI(err) } if client.Debug { client.Printf("ClientOptions: %v\n", options) } - if options.Url == "" || options.RedirectUrl == "" { - return nil, NewApiError(InvalidUrlOptionsErr) + if options.URL == "" || options.RedirectURL == "" { + return nil, NewErrorAPI(ErrInvalidOptionsURL) } - resolvedUrl, err := url.Parse(options.Url) + parsedURL, err := url.Parse(options.URL) if err != nil { - return nil, NewApiError(err) + return nil, NewErrorAPI(err) } - client.BaseUrl = resolvedUrl + client.BaseURL = parsedURL client.options = options var ( @@ -92,293 +82,33 @@ func NewApi(secretPath string) (*Client, error) { exchangeErr error exchanged bool ) - if client.options.AccessToken == "" && client.options.RefreshToken == "" { + if client.options.AccessToken == "" && + client.options.RefreshToken == "" { + if client.options.ClientSecret == "" || client.options.ClientId == "" || client.options.AuthCode == "" { - return nil, NewApiError(InvalidExchangeAuthOptionsErr) + return nil, NewErrorAPI( + ErrInvalidExchangeAuthOptions, + ) } + _, exchangeErr = client.ExchangeAuth() exchanged = true } - if (!exchanged || exchangeErr != nil) && options.RefreshToken != "" { + if (!exchanged || exchangeErr != nil) && + options.RefreshToken != "" { + // Refreshing token before the work. // Should think of how often should refresh // the token. (see the ExpiresIn) _, err = client.RefreshToken() if err != nil { - return nil, NewApiError(err) + return nil, NewErrorAPI(err) } } return client, nil } -func (api *Client) readSecret() (*ClientOptions, error) { - f, err := os.Open(api.secretStoreFilePath) - if err != nil { - return nil, err - } - - bts, err := io.ReadAll(f) - if err != nil { - return nil, err - } - - - ret := ClientOptions{} - err = json.Unmarshal(bts, &ret) - if err != nil { - return nil, err - } - - return &ret, nil -} - -func (api *Client) writeSecret() error { - bts, err := json.MarshalIndent(api.options, "", "\t") - if err != nil { - return err - } - - err = os.WriteFile(api.secretStoreFilePath, bts, 0644) - if err != nil { - return err - } - return err -} - -func (api *Client) rdoRequest(resourceUrl string, requestParams requestOptions, result interface{}) error { - var ( - requestUrl *url.URL - err error - ) - - if !requestParams.Abs { - resolvedUrl, err := url.Parse(resourceUrl) - if err != nil { - return err - } - - requestUrl = api.BaseUrl.ResolveReference(resolvedUrl) - } else { - requestUrl, err = url.Parse(resourceUrl) - if err != nil { - return err - } - } - - 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 api.Debug { - api.Printf("Request: %+v\n\nAmo response: %+v\n\n", request, response) - } - if response == nil { - return RequestError(NoInternetErr) - } - if err != nil { - return RequestError(fmt.Errorf( - "%w: %d %s %s", - err, - response.StatusCode, - requestParams.HttpMethod, - resourceUrl, - )) - } - defer response.Body.Close() - - if response.StatusCode == 204 { - return RequestError(NoContentErr) - } - - if response.StatusCode >= 400 { - var specErr error - if response.StatusCode == 401 { - specErr = NoAuthErr - } - - responseErrorInfo, _ := ioutil.ReadAll(response.Body) - return RequestError(fmt.Errorf( - "%w: %s %s %q %d", - specErr, - requestParams.HttpMethod, - resourceUrl, - string(responseErrorInfo), - response.StatusCode, - )) - } - - if result != nil { - decoder := json.NewDecoder(response.Body) - err = decoder.Decode(result) - if err != nil { - return err - } - } - - return nil -} -func (api *Client) doRequest( - resourceUrl string, requestParams requestOptions, result any, -) error { - var err error - err = api.rdoRequest(resourceUrl, requestParams, result) - if errors.Is(err, NoAuthErr) && api.options.RefreshToken != "" { - _, err = api.RefreshToken() - if err != nil { - return nil - } - return api.rdoRequest(resourceUrl, requestParams, result) - } else if err != nil { - return err - } - - return nil -} - -func (api *Client) ExchangeAuth() (*TokenPair, error) { - result := &OauthTokenResponse{} - request := map[string] string { - "client_id": api.options.ClientId, - "client_secret": api.options.ClientSecret, - "grant_type": "authorization_code", - "code": api.options.AuthCode, - "redirect_uri": api.options.RedirectUrl, - } - - err := api.rdoRequest("/oauth2/access_token", requestOptions{ - HttpMethod: http.MethodPost, - Body: request, - Headers: getHeaders(""), - }, result) - if err != nil { - return nil, err - } - - ret := &TokenPair{ - Access: result.AccessToken, - Refresh: result.RefreshToken, - } - - api.options.AccessToken = result.AccessToken - api.options.RefreshToken = result.RefreshToken - now := time.Now() - api.options.ExpirationAt = now.Add( - time.Second*time.Duration(result.ExpiresIn), - ) - err = api.writeSecret() - if err != nil { - return nil, err - } - return ret, nil -} - -func (api *Client) RefreshTokenIfExpired() error { - if api.options.RefreshToken == "" { - return nil - } - - now := time.Now() - if now.After(api.options.ExpirationAt) || now.Equal(api.options.ExpirationAt){ - _, err := api.RefreshToken() - 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.RedirectUrl, - } - - err := api.rdoRequest("/oauth2/access_token", requestOptions{ - HttpMethod: http.MethodPost, - Body: request, - Headers: getHeaders(""), - }, result) - if err != nil { - return nil, err - } - - api.options.AccessToken = result.AccessToken - api.options.RefreshToken = result.RefreshToken - now := time.Now() - api.options.ExpirationAt = now.Add( - time.Second*time.Duration(result.ExpiresIn), - ) - err = api.writeSecret() - if err != nil { - return nil, err - } - - return result, nil -} - -func (api *Client) Get(resource string, result interface{}, abs ...bool) error { - var a bool - if len(abs) > 0 { - a = abs[0] - } - return api.doRequest(resource, requestOptions{ - HttpMethod: http.MethodGet, - Body: nil, - Headers: getHeaders(api.options.AccessToken), - Abs: a, - }, 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 -} diff --git a/api/auth.go b/api/auth.go new file mode 100644 index 0000000..dbc9eab --- /dev/null +++ b/api/auth.go @@ -0,0 +1,93 @@ +package api + +import "time" +import "net/http" + +func (api *Client) ExchangeAuth() (*TokenPair, error) { + result := &OAuthTokenResponse{} + request := map[string] string { + "client_id": api.options.ClientId, + "client_secret": api.options.ClientSecret, + "grant_type": "authorization_code", + "code": api.options.AuthCode, + "redirect_uri": api.options.RedirectURL, + } + + err := api.doRequest("/oauth2/access_token", RequestOptions{ + Method: http.MethodPost, + Body: request, + Headers: makeHeaders(""), + }, result) + if err != nil { + return nil, err + } + + ret := &TokenPair{ + Access: result.AccessToken, + Refresh: result.RefreshToken, + } + + api.options.AccessToken = result.AccessToken + api.options.RefreshToken = result.RefreshToken + now := time.Now() + api.options.ExpirationAt = now.Add( + time.Second*time.Duration(result.ExpiresIn), + ) + err = api.writeSecret() + if err != nil { + return nil, err + } + return ret, nil +} + +func (api *Client) RefreshTokenIfExpired() error { + if api.options.RefreshToken == "" { + return nil + } + + now := time.Now() + if now.After(api.options.ExpirationAt) || now.Equal(api.options.ExpirationAt){ + _, err := api.RefreshToken() + 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.RedirectURL, + } + + err := api.doRequest( + "/oauth2/access_token", + RequestOptions{ + Method: http.MethodPost, + Body: request, + Headers: makeHeaders(""), + }, + result, + ) + if err != nil { + return nil, err + } + + api.options.AccessToken = result.AccessToken + api.options.RefreshToken = result.RefreshToken + now := time.Now() + api.options.ExpirationAt = now.Add( + time.Second*time.Duration(result.ExpiresIn), + ) + err = api.writeSecret() + if err != nil { + return nil, err + } + + return result, nil +} diff --git a/api/endpoint.go b/api/endpoint.go new file mode 100644 index 0000000..a44bf07 --- /dev/null +++ b/api/endpoint.go @@ -0,0 +1,40 @@ +package api + +import "net/http" + +func (api *Client) Get( + u string, + result interface{}, +) error { + params := RequestOptions{ + Method: http.MethodGet, + Headers: makeHeaders(api.options.AccessToken), + } + return api.doRequest(u, params, result) +} + +func (api *Client) Post( + u string, + request any, + result any, +) error { + params := RequestOptions{ + Method: http.MethodPost, + Body: request, + Headers: makeHeaders(api.options.AccessToken), + } + return api.doRequest(u, params, result) +} + +func (api *Client) Patch( + u string, + request any, + result any, +) error { + params := RequestOptions{ + Method: http.MethodPatch, + Body: request, + Headers: makeHeaders(api.options.AccessToken), + } + return api.doRequest(u, params, result) +} diff --git a/api/errors.go b/api/errors.go index d5dfc32..96b249a 100644 --- a/api/errors.go +++ b/api/errors.go @@ -6,19 +6,21 @@ import ( ) var ( - NoContentErr = errors.New("no content") - NoAuthErr = errors.New("not authorized") - NoInternetErr = errors.New("no Internet provided") - InvalidUrlOptionsErr = errors.New("invalid URL options") - InvalidExchangeAuthOptionsErr = errors.New("invalid ExchangeAuth options") - UrlParsingErr = errors.New("URL parsing") + ErrNoContent = errors.New("no content") + ErrNoAuth = errors.New("not authorized") + ErrUnreachable = errors.New("unreachable") + ErrInvalidOptionsURL = errors.New("invalid URL options") + ErrInvalidExchangeAuthOptions= errors.New( + "invalid ExchangeAuth options", + ) + ErrUrlParsing= errors.New("URL parsing") ) -func NewApiError(err error) error { - return fmt.Errorf("NewApi: %w", err) +func NewErrorAPI(err error) error { + return fmt.Errorf("NewAPI: %w", err) } -func RequestError(err error) error { +func NewRequestError(err error) error { return fmt.Errorf("RequestError: %w", err) } diff --git a/api/request.go b/api/request.go new file mode 100644 index 0000000..f17096f --- /dev/null +++ b/api/request.go @@ -0,0 +1,165 @@ +package api + +import "encoding/json" +import "net/http" +import "net/url" +import "errors" +import "bytes" +import "fmt" +import "io" + +type RequestOptions struct { + Method string + Body interface{} + Headers map[string]string +} + +func (client *Client) doRawRequest( + u *url.URL, + params RequestOptions, + result interface{}, +) error { + var ( + err error + ) + + reqBody := new(bytes.Buffer) + if params.Body != nil { + encoder := json.NewEncoder(reqBody) + err := encoder.Encode(params.Body) + if err != nil { + return err + } + } + + request, err := http.NewRequest( + params.Method, + u.String(), + reqBody, + ) + if err != nil { + return err + } + + if params.Headers != nil { + for k, v := range params.Headers { + request.Header.Set(k, v) + } + } + + res, err := http.DefaultClient.Do(request) + if client.Debug { + client.Printf( + "Request: %+v\n\nAmo response: %+v\n\n", + request, + res, + ) + } + if res == nil { + if err != nil { + return NewRequestError(err) + } + return NewRequestError(ErrUnreachable) + } + if err != nil { + return NewRequestError(fmt.Errorf( + "%w: %d %s %s", + err, + res.StatusCode, + params.Method, + u, + )) + } + defer res.Body.Close() + + if res.StatusCode == 204 { + return NewRequestError(ErrNoContent) + } + + if res.StatusCode >= 400 { + var specErr error + if res.StatusCode == 401 { + specErr = ErrNoAuth + } + + resErrInfo, _ := io.ReadAll(res.Body) + return NewRequestError(fmt.Errorf( + "%w: %s %s %q %d", + specErr, + params.Method, + u, + string(resErrInfo), + res.StatusCode, + )) + } + + if result != nil { + decoder := json.NewDecoder(res.Body) + err = decoder.Decode(result) + if err != nil { + return err + } + } + + return nil +} + +func (client *Client) doRequest( + u string, + params RequestOptions, + result any, +) error { + var ( + err error + parsedURL *url.URL + ) + + parsedURL, err = url.Parse(u) + if err != nil { + return err + } + reqURL := client.BaseURL.ResolveReference(parsedURL) + + err = client.doRawRequest( + reqURL, + params, + result, + ) + if errors.Is(err, ErrNoAuth) && + client.options.RefreshToken != "" { + + _, err = client.RefreshToken() + if err != nil { + return nil + } + + return client.doRawRequest( + reqURL, + params, + result, + ) + + } else if err != nil { + return err + } + + return nil +} + +func makeHeaders( + 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 +} + diff --git a/api/secret.go b/api/secret.go new file mode 100644 index 0000000..a3476f3 --- /dev/null +++ b/api/secret.go @@ -0,0 +1,39 @@ +package api + +import "encoding/json" +import "os" +import "io" + +func (api *Client) readSecret() (ClientOptions, error) { + f, err := os.Open(api.secretStoreFilePath) + if err != nil { + return ClientOptions{}, err + } + + bts, err := io.ReadAll(f) + if err != nil { + return ClientOptions{}, err + } + + + ret := ClientOptions{} + err = json.Unmarshal(bts, &ret) + if err != nil { + return ClientOptions{}, err + } + + return ret, nil +} + +func (api *Client) writeSecret() error { + bts, err := json.MarshalIndent(api.options, "", "\t") + if err != nil { + return err + } + + err = os.WriteFile(api.secretStoreFilePath, bts, 0644) + if err != nil { + return err + } + return err +} diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..5fe203d --- /dev/null +++ b/build.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +go build -o ./exe/ ./cmd/amocli/ + diff --git a/cmd/amocli/getlead.go b/cmd/amocli/getlead.go new file mode 100644 index 0000000..a7d5a6d --- /dev/null +++ b/cmd/amocli/getlead.go @@ -0,0 +1,56 @@ +package main + + +import "surdeus.su/core/amo" +import "surdeus.su/core/ss/urlenc" +import "surdeus.su/core/cli/mtool" +import "encoding/json" +import "strconv" +import "log" +import "fmt" +//import "os" + +var getLead = mtool.T("get-leads").Func(func(flags *mtool.Flags){ + var ( + secretPath string + ) + + flags.StringVar( + &secretPath, + "secret", + "", + "path to JSON file with AMO CRM secrets", + "AMO_SECRET", + ) + idStrs := flags.Parse() + ids := make([]int, len(idStrs)) + for i, idStr := range idStrs { + var err error + ids[i], err = strconv.Atoi(idStr) + if err != nil { + log.Printf("Error: Atoi(%q): %s\n", err) + return + } + } + + c, err := amo.NewClient(secretPath) + if err != nil { + log.Fatalf("NewAmoClient(...): %s\n", err) + } + + leads, err := c.GetLeads( + urlenc.Array[int]{ + "id", + ids, + }, + ) + if err != nil { + log.Fatalf("GetLeadsByIDs(...): %s\n", err) + } + + bts, err := json.MarshalIndent(leads, "", " ") + if err != nil { + log.Fatalf("json.Marshal(...): %s\n", err) + } + fmt.Printf("%s\n", bts) +}) diff --git a/cmd/amocli/main.go b/cmd/amocli/main.go new file mode 100644 index 0000000..1512b61 --- /dev/null +++ b/cmd/amocli/main.go @@ -0,0 +1,12 @@ +package main + +import "surdeus.su/core/cli/mtool" +import "os" + +func main() { + tool.Run(os.Args[1:]) +} + +var tool = mtool.T("amocli").Subs( + getLead, +) diff --git a/companies.go b/companies.go new file mode 100644 index 0000000..bc1b2ae --- /dev/null +++ b/companies.go @@ -0,0 +1,33 @@ +package amo + +import "surdeus.su/core/ss/urlenc" +import "surdeus.su/core/amo/companies" +import "fmt" + +func (client *Client) GetCompany( + companyId int, + opts ...urlenc.Builder, +) (*companies.Company, error) { + deal := new(companies.Company) + resource := fmt.Sprintf( + "/api/v4/companies/%d?%s", + companyId, + urlenc.Join(opts...), + ) + + err := client.API.Get(resource, deal) + if err != nil { + return nil, err + } + return deal, nil +} + +func (client *Client) UpdateCompany( + company *companies.Company, +) error { + return client.updateEntity( + "/api/v4/companies", + company.Id, + company, + ) +} diff --git a/contacts.go b/contacts.go new file mode 100644 index 0000000..662c276 --- /dev/null +++ b/contacts.go @@ -0,0 +1,31 @@ +package amo + +import "surdeus.su/core/amo/contacts" +import "surdeus.su/core/ss/urlenc" +import "fmt" + +func (client *Client) GetContact( + contactId int, + opts ...urlenc.Builder, +) (*contacts.Contact, error) { + deal := new(contacts.Contact) + res := fmt.Sprintf( + "/api/v4/contacts/%d?%s", + contactId, + urlenc.Join(opts...).Encode(), + ) + + err := client.API.Get(res, deal) + if err != nil { + return nil, err + } + return deal, nil +} + +func (client *Client) UpdateContact(contact *contacts.Contact) error { + return client.updateEntity( + "/api/v4/contacts", + contact.Id, + contact, + ) +} diff --git a/entity.go b/entity.go new file mode 100644 index 0000000..6366e5c --- /dev/null +++ b/entity.go @@ -0,0 +1,18 @@ +package amo + +import "fmt" + +func (client *Client) updateEntity( + u string, + id int, + body any, +) error { + err := client.API.Patch( + fmt.Sprintf( + "%s/%d", + u, id, + ), + body, nil, + ) + return err +} diff --git a/events.go b/events.go new file mode 100644 index 0000000..da317eb --- /dev/null +++ b/events.go @@ -0,0 +1,39 @@ +package amo + +import "surdeus.su/core/amo/events" +import "surdeus.su/core/amo/api" +import "surdeus.su/core/ss/urlenc" +import "errors" +import "fmt" + +// Returns the events from AmoCRM by specified request. +// If there are no such events returns an empty slice of events. +func (client *Client) GetEvents( + opts ...urlenc.Builder, +) ([]events.Event, error) { + res := fmt.Sprintf( + "/api/v4/events?%s", + urlenc.Join(opts...).Encode(), + ) + + ret := []events.Event{} + for { + resp := events.EventsResponse{} + err := client.API.Get(res, &resp) + if err != nil { + // Return empty if no content avialable. + if errors.Is(err, api.ErrNoContent) { + break + } + return nil, err + } + ret = append(ret, resp.Embedded.Events...) + if resp.Links.Next.Href == "" { + break + } + //time.Sleep(time.Millisecond * 300) + res = resp.Links.Next.Href + } + + return ret, nil +} diff --git a/filters/main.go b/filters/main.go deleted file mode 100644 index 679da88..0000000 --- a/filters/main.go +++ /dev/null @@ -1,3 +0,0 @@ -package filters - - diff --git a/go.mod b/go.mod index af90f77..3c712b3 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,12 @@ go 1.21.3 require ( github.com/stretchr/testify v1.9.0 - surdeus.su/core/ss v0.1.1 + surdeus.su/core/ss v0.1.4 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + surdeus.su/core/cli v0.0.2 // indirect ) diff --git a/go.sum b/go.sum index 62a5240..aa4b27c 100644 --- a/go.sum +++ b/go.sum @@ -8,5 +8,13 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +surdeus.su/core/cli v0.0.2 h1:RdHHk3/Fhwxz9PjaE+vTlCuF9KmhrmNUb5y4oqulrYI= +surdeus.su/core/cli v0.0.2/go.mod h1:UKwCmcSX+x7XX9aF3gOaaAaJcJA3gtUmL4vdnM43+fM= surdeus.su/core/ss v0.1.1 h1:CXYBVRk4fv+Z7Cu51Nw5wDs5CoaEf2VpjB7XmFqu3DQ= surdeus.su/core/ss v0.1.1/go.mod h1:4gPfk0OjdJ1maKHyYiZwai7jIIwpVE5vgiBF2nubdrU= +surdeus.su/core/ss v0.1.2 h1:gCVeAypwD1pO6mFZg1rOV5oYQMkIERrKxEmpGq6PGBI= +surdeus.su/core/ss v0.1.2/go.mod h1:4gPfk0OjdJ1maKHyYiZwai7jIIwpVE5vgiBF2nubdrU= +surdeus.su/core/ss v0.1.3 h1:/oUi9427THM4NqSzZZCXTmu8OJKWHVOb8P2kSvlq3Uw= +surdeus.su/core/ss v0.1.3/go.mod h1:4gPfk0OjdJ1maKHyYiZwai7jIIwpVE5vgiBF2nubdrU= +surdeus.su/core/ss v0.1.4 h1:rswGlNbjlxbYSYUcTCPTzJr1d13YkDtQw9JvFNmQsxQ= +surdeus.su/core/ss v0.1.4/go.mod h1:4gPfk0OjdJ1maKHyYiZwai7jIIwpVE5vgiBF2nubdrU= diff --git a/id.go b/id.go deleted file mode 100644 index eb81c3f..0000000 --- a/id.go +++ /dev/null @@ -1,5 +0,0 @@ -package amo - -// Should use it later as an ID -// for type safety in compilation time. -type Id[V any] int64 diff --git a/leads.go b/leads.go new file mode 100644 index 0000000..d258eb1 --- /dev/null +++ b/leads.go @@ -0,0 +1,62 @@ +package amo + +import "surdeus.su/core/amo/api" +import "surdeus.su/core/amo/leads" +import "surdeus.su/core/ss/urlenc" +import "errors" +import "fmt" + +// Get list of leads. +func (client *Client) GetLeads( + opts ...urlenc.Builder, +) ([]leads.Lead, error) { + res := fmt.Sprintf( + "/api/v4/leads?%s", + urlenc.Join(opts...).Encode(), + ) + + ret := []leads.Lead{} + + for { + var lds leads.Leads + err := client.API.Get(res, &lds) + if err != nil { + if errors.Is(err, api.ErrNoContent) { + break + } + return nil, err + } + + ret = append(ret, lds.Embedded.Leads...) + + if lds.Links.Next.Href == "" { + break + } + res = lds.Links.Next.Href + + } + return ret, nil +} + +// Get lead with the specified ID. +func (client *Client) GetLead( + leadId int, + opts ...urlenc.Builder, +) (*leads.Lead, error) { + deal := new(leads.Lead) + resource := fmt.Sprintf( + "/api/v4/leads/%d?%s", + leadId, + urlenc.Join(opts...).Encode(), + ) + + 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) +} + diff --git a/leads/array.go b/leads/array.go new file mode 100644 index 0000000..4d536d9 --- /dev/null +++ b/leads/array.go @@ -0,0 +1,25 @@ +package leads + +// The structure represents +// response on the */leads . +type Leads struct { + Page int `json:"_page"` + Links struct { + Self struct { + Href string `json:"href"` + } `json:"self"` + Next struct { + Href string `json:"href"` + } `json:"next"` + First struct { + Href string `json:"first"` + } `json:"first"` + Prev struct { + Href string `json:"href"` + } `json:"prev"` + } `json:"_links"` + Embedded struct { + Leads []Lead `json:"leads"` + } `json:"_embedded"` +} + diff --git a/leads/leads.go b/leads/leads.go index d51c8b7..f30d10a 100644 --- a/leads/leads.go +++ b/leads/leads.go @@ -3,28 +3,28 @@ package leads import "surdeus.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"` + 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 { @@ -36,44 +36,44 @@ type Links struct { } type Tags struct { - Id int `json:"id"` + Id int `json:"id"` Name string `json:"name"` } type Metadata struct { - Quantity int `json:"quantity"` + Quantity int `json:"quantity"` CatalogId int `json:"catalog_id"` } type CatalogElements struct { - Id int `json:"id"` + 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"` + 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"` + Id int `json:"id"` Links Links `json:"_links"` } type Contacts struct { - Id int `json:"id"` - IsMain bool `json:"is_main"` - Links Links `json:"_links"` + Id int `json:"id"` + IsMain bool `json:"is_main"` + Links Links `json:"_links"` } type Embedded struct { - Tags []*Tags `json:"tags"` + Tags []*Tags `json:"tags"` CatalogElements []*CatalogElements `json:"catalog_elements"` - LossReason []*LossReason `json:"loss_reason"` - Companies []*Companies `json:"companies"` - Contacts []*Contacts `json:"contacts"` + LossReason []*LossReason `json:"loss_reason"` + Companies []*Companies `json:"companies"` + Contacts []*Contacts `json:"contacts"` } diff --git a/options/main.go b/options/main.go deleted file mode 100644 index 5f721e6..0000000 --- a/options/main.go +++ /dev/null @@ -1,18 +0,0 @@ -package options - -type Option interface { - GetOption() string -} - -type Array[V any] struct { - Name string - Values []V -} - -func (arr Array) GetOption() string { - -} - -type Filter struct { -} - diff --git a/taskfile.yml b/taskfile.yml deleted file mode 100644 index 307c96c..0000000 --- a/taskfile.yml +++ /dev/null @@ -1,6 +0,0 @@ -version: 3 - -tasks: - btest: - cmds: - - go build ./cmd/test diff --git a/users.go b/users.go new file mode 100644 index 0000000..da3f566 --- /dev/null +++ b/users.go @@ -0,0 +1,18 @@ +package amo + +import "surdeus.su/core/ss/urlenc" +import "surdeus.su/core/amo/users" +import "fmt" + +func (client *Client) GetUser( + userId int, + opts ...urlenc.Builder, +) (*users.User, error) { + user := new(users.User) + err := client.API.Get(fmt.Sprintf( + "/api/v4/users/%d?%s", + userId, + urlenc.Join(opts...).Encode(), + ), user) + return user, err +}