save shit

This commit is contained in:
Andrey Parhomenko 2024-05-30 13:43:10 +05:00
parent ffb7d8467f
commit 5a05904589
25 changed files with 741 additions and 501 deletions

2
.gitignore vendored
View file

@ -2,4 +2,4 @@
*.exe~ *.exe~
.env .env
secret.json secret.json
test /exe/

124
amocrm.go
View file

@ -1,132 +1,28 @@
package amo package amo
import ( import (
"fmt"
"time"
"errors"
"surdeus.su/core/amo/api" "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 { const (
GetUser(userId string) (*users.User, error) // Maximum entities for once.
GetLead(leadId string, query string) (*leads.Lead, error) // It is set to be the most effective
UpdateLead(lead *leads.Lead) error // but in fact can be greater.
GetCompany(companyId string, query string) (*companies.Company, error) MaxEnt = 50
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
type Client struct { type Client struct {
Api *api.Client API *api.Client
} }
type OauthTokenResponse = api.OauthTokenResponse func NewClient(secretPath string) (*Client, error) {
apiClient, err := api.NewAPI(secretPath)
func NewAmoClient(secretPath string) (*Client, error) {
apiClient, err := api.NewApi(secretPath)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Client{ return &Client{
Api: apiClient, API: apiClient,
}, nil }, 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)
}

View file

@ -1,16 +1,9 @@
package api package api
import ( import (
"encoding/json"
"io/ioutil"
"net/http"
"net/url" "net/url"
"errors"
"bytes"
"time" "time"
"fmt"
"log" "log"
"io"
"os" "os"
) )
@ -21,8 +14,8 @@ const (
) )
type ClientOptions struct { type ClientOptions struct {
Url string `json:"url"` URL string `json:"url"`
RedirectUrl string `json:"redirect_url"` RedirectURL string `json:"redirect_url"`
AuthCode string `json:"auth_code"` AuthCode string `json:"auth_code"`
@ -37,20 +30,13 @@ type ClientOptions struct {
type Client struct { type Client struct {
*log.Logger *log.Logger
options *ClientOptions options ClientOptions
BaseUrl *url.URL BaseURL *url.URL
secretStoreFilePath string secretStoreFilePath string
Debug bool Debug bool
} }
type requestOptions struct { type OAuthTokenResponse struct {
HttpMethod string
Body interface{}
Headers map[string]string
Abs bool
}
type OauthTokenResponse struct {
TokenType string `json:"token_type"` TokenType string `json:"token_type"`
ExpiresIn int `json:"expires_in"` ExpiresIn int `json:"expires_in"`
AccessToken string `json:"access_token"` AccessToken string `json:"access_token"`
@ -61,30 +47,34 @@ type TokenPair struct {
Access, Refresh string Access, Refresh string
} }
func NewApi(secretPath string) (*Client, error) { func NewAPI(secretPath string) (*Client, error) {
client := &Client{ 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, secretStoreFilePath: secretPath,
} }
options, err := client.readSecret() options, err := client.readSecret()
if err != nil { if err != nil {
return nil, NewApiError(err) return nil, NewErrorAPI(err)
} }
if client.Debug { if client.Debug {
client.Printf("ClientOptions: %v\n", options) client.Printf("ClientOptions: %v\n", options)
} }
if options.Url == "" || options.RedirectUrl == "" { if options.URL == "" || options.RedirectURL == "" {
return nil, NewApiError(InvalidUrlOptionsErr) return nil, NewErrorAPI(ErrInvalidOptionsURL)
} }
resolvedUrl, err := url.Parse(options.Url) parsedURL, err := url.Parse(options.URL)
if err != nil { if err != nil {
return nil, NewApiError(err) return nil, NewErrorAPI(err)
} }
client.BaseUrl = resolvedUrl client.BaseURL = parsedURL
client.options = options client.options = options
var ( var (
@ -92,293 +82,33 @@ func NewApi(secretPath string) (*Client, error) {
exchangeErr error exchangeErr error
exchanged bool exchanged bool
) )
if client.options.AccessToken == "" && client.options.RefreshToken == "" { if client.options.AccessToken == "" &&
client.options.RefreshToken == "" {
if client.options.ClientSecret == "" || if client.options.ClientSecret == "" ||
client.options.ClientId == "" || client.options.ClientId == "" ||
client.options.AuthCode == "" { client.options.AuthCode == "" {
return nil, NewApiError(InvalidExchangeAuthOptionsErr) return nil, NewErrorAPI(
ErrInvalidExchangeAuthOptions,
)
} }
_, exchangeErr = client.ExchangeAuth() _, exchangeErr = client.ExchangeAuth()
exchanged = true exchanged = true
} }
if (!exchanged || exchangeErr != nil) && options.RefreshToken != "" { if (!exchanged || exchangeErr != nil) &&
options.RefreshToken != "" {
// Refreshing token before the work. // Refreshing token before the work.
// Should think of how often should refresh // Should think of how often should refresh
// the token. (see the ExpiresIn) // the token. (see the ExpiresIn)
_, err = client.RefreshToken() _, err = client.RefreshToken()
if err != nil { if err != nil {
return nil, NewApiError(err) return nil, NewErrorAPI(err)
} }
} }
return client, nil 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
}

93
api/auth.go Normal file
View file

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

40
api/endpoint.go Normal file
View file

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

View file

@ -6,19 +6,21 @@ import (
) )
var ( var (
NoContentErr = errors.New("no content") ErrNoContent = errors.New("no content")
NoAuthErr = errors.New("not authorized") ErrNoAuth = errors.New("not authorized")
NoInternetErr = errors.New("no Internet provided") ErrUnreachable = errors.New("unreachable")
InvalidUrlOptionsErr = errors.New("invalid URL options") ErrInvalidOptionsURL = errors.New("invalid URL options")
InvalidExchangeAuthOptionsErr = errors.New("invalid ExchangeAuth options") ErrInvalidExchangeAuthOptions= errors.New(
UrlParsingErr = errors.New("URL parsing") "invalid ExchangeAuth options",
)
ErrUrlParsing= errors.New("URL parsing")
) )
func NewApiError(err error) error { func NewErrorAPI(err error) error {
return fmt.Errorf("NewApi: %w", err) return fmt.Errorf("NewAPI: %w", err)
} }
func RequestError(err error) error { func NewRequestError(err error) error {
return fmt.Errorf("RequestError: %w", err) return fmt.Errorf("RequestError: %w", err)
} }

165
api/request.go Normal file
View file

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

39
api/secret.go Normal file
View file

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

4
build.sh Executable file
View file

@ -0,0 +1,4 @@
#!/bin/sh
go build -o ./exe/ ./cmd/amocli/

56
cmd/amocli/getlead.go Normal file
View file

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

12
cmd/amocli/main.go Normal file
View file

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

33
companies.go Normal file
View file

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

31
contacts.go Normal file
View file

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

18
entity.go Normal file
View file

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

39
events.go Normal file
View file

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

View file

@ -1,3 +0,0 @@
package filters

3
go.mod
View file

@ -4,11 +4,12 @@ go 1.21.3
require ( require (
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
surdeus.su/core/ss v0.1.1 surdeus.su/core/ss v0.1.4
) )
require ( require (
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
surdeus.su/core/cli v0.0.2 // indirect
) )

8
go.sum
View file

@ -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/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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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 h1:CXYBVRk4fv+Z7Cu51Nw5wDs5CoaEf2VpjB7XmFqu3DQ=
surdeus.su/core/ss v0.1.1/go.mod h1:4gPfk0OjdJ1maKHyYiZwai7jIIwpVE5vgiBF2nubdrU= 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=

5
id.go
View file

@ -1,5 +0,0 @@
package amo
// Should use it later as an ID
// for type safety in compilation time.
type Id[V any] int64

62
leads.go Normal file
View file

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

25
leads/array.go Normal file
View file

@ -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"`
}

View file

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

View file

@ -1,6 +0,0 @@
version: 3
tasks:
btest:
cmds:
- go build ./cmd/test

18
users.go Normal file
View file

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