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~
.env
secret.json
test
/exe/

124
amocrm.go
View file

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

View file

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

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

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 (
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
)

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

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

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

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
}