save shit
This commit is contained in:
parent
ffb7d8467f
commit
5a05904589
25 changed files with 741 additions and 501 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -2,4 +2,4 @@
|
||||||
*.exe~
|
*.exe~
|
||||||
.env
|
.env
|
||||||
secret.json
|
secret.json
|
||||||
test
|
/exe/
|
||||||
|
|
124
amocrm.go
124
amocrm.go
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
340
api/api.go
340
api/api.go
|
@ -1,35 +1,28 @@
|
||||||
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DefaultContentType = "application/json"
|
DefaultContentType = "application/json"
|
||||||
DefaultAccept = DefaultContentType
|
DefaultAccept = DefaultContentType
|
||||||
DefaultCacheControl = "no-cache"
|
DefaultCacheControl = "no-cache"
|
||||||
)
|
)
|
||||||
|
|
||||||
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"`
|
||||||
|
|
||||||
ClientId string `json:"client_id"`
|
ClientId string `json:"client_id"`
|
||||||
ClientSecret string `json:"client_secret"`
|
ClientSecret string `json:"client_secret"`
|
||||||
|
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
|
|
||||||
ExpirationAt time.Time `json:"expiration_at,omitempty"`
|
ExpirationAt time.Time `json:"expiration_at,omitempty"`
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
@ -37,23 +30,16 @@ 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
|
TokenType string `json:"token_type"`
|
||||||
Body interface{}
|
ExpiresIn int `json:"expires_in"`
|
||||||
Headers map[string]string
|
AccessToken string `json:"access_token"`
|
||||||
Abs bool
|
|
||||||
}
|
|
||||||
|
|
||||||
type OauthTokenResponse struct {
|
|
||||||
TokenType string `json:"token_type"`
|
|
||||||
ExpiresIn int `json:"expires_in"`
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_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
93
api/auth.go
Normal 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
40
api/endpoint.go
Normal 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)
|
||||||
|
}
|
|
@ -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
165
api/request.go
Normal 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
39
api/secret.go
Normal 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
4
build.sh
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
go build -o ./exe/ ./cmd/amocli/
|
||||||
|
|
56
cmd/amocli/getlead.go
Normal file
56
cmd/amocli/getlead.go
Normal 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
12
cmd/amocli/main.go
Normal 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
33
companies.go
Normal 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
31
contacts.go
Normal 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
18
entity.go
Normal 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
39
events.go
Normal 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
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
package filters
|
|
||||||
|
|
||||||
|
|
3
go.mod
3
go.mod
|
@ -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
8
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/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
5
id.go
|
@ -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
62
leads.go
Normal 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
25
leads/array.go
Normal 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"`
|
||||||
|
}
|
||||||
|
|
|
@ -3,28 +3,28 @@ package leads
|
||||||
import "surdeus.su/core/amo/common"
|
import "surdeus.su/core/amo/common"
|
||||||
|
|
||||||
type Lead struct {
|
type Lead struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Price int `json:"price,omitempty"`
|
Price int `json:"price,omitempty"`
|
||||||
ResponsibleUserId int `json:"responsible_user_id,omitempty"`
|
ResponsibleUserId int `json:"responsible_user_id,omitempty"`
|
||||||
GroupId int `json:"group_id,omitempty"`
|
GroupId int `json:"group_id,omitempty"`
|
||||||
StatusId int `json:"status_id,omitempty"`
|
StatusId int `json:"status_id,omitempty"`
|
||||||
PipelineId int `json:"pipeline_id,omitempty"`
|
PipelineId int `json:"pipeline_id,omitempty"`
|
||||||
LossReasonId int `json:"loss_reason_id,omitempty"`
|
LossReasonId int `json:"loss_reason_id,omitempty"`
|
||||||
SourceId interface{} `json:"source_id,omitempty"`
|
SourceId interface{} `json:"source_id,omitempty"`
|
||||||
CreatedBy int `json:"created_by,omitempty"`
|
CreatedBy int `json:"created_by,omitempty"`
|
||||||
UpdatedBy int `json:"updated_by,omitempty"`
|
UpdatedBy int `json:"updated_by,omitempty"`
|
||||||
CreatedAt int `json:"created_at,omitempty"`
|
CreatedAt int `json:"created_at,omitempty"`
|
||||||
UpdatedAt int `json:"updated_at,omitempty"`
|
UpdatedAt int `json:"updated_at,omitempty"`
|
||||||
ClosedAt int `json:"closed_at,omitempty"`
|
ClosedAt int `json:"closed_at,omitempty"`
|
||||||
ClosestTaskAt interface{} `json:"closest_task_at,omitempty"`
|
ClosestTaskAt interface{} `json:"closest_task_at,omitempty"`
|
||||||
IsDeleted bool `json:"is_deleted,omitempty"`
|
IsDeleted bool `json:"is_deleted,omitempty"`
|
||||||
CustomFieldsValues []common.CustomFieldsValue `json:"custom_fields_values,omitempty"`
|
CustomFieldsValues []common.CustomFieldsValue `json:"custom_fields_values,omitempty"`
|
||||||
Score interface{} `json:"score,omitempty"`
|
Score interface{} `json:"score,omitempty"`
|
||||||
AccountId int `json:"account_id,omitempty"`
|
AccountId int `json:"account_id,omitempty"`
|
||||||
IsPriceModifiedByRobot bool `json:"is_price_modified_by_robot,omitempty"`
|
IsPriceModifiedByRobot bool `json:"is_price_modified_by_robot,omitempty"`
|
||||||
Links Links `json:"_links,omitempty"`
|
Links Links `json:"_links,omitempty"`
|
||||||
Embedded Embedded `json:"_embedded,omitempty"`
|
Embedded Embedded `json:"_embedded,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Self struct {
|
type Self struct {
|
||||||
|
@ -36,44 +36,44 @@ type Links struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type Tags struct {
|
type Tags struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Metadata struct {
|
type Metadata struct {
|
||||||
Quantity int `json:"quantity"`
|
Quantity int `json:"quantity"`
|
||||||
CatalogId int `json:"catalog_id"`
|
CatalogId int `json:"catalog_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CatalogElements struct {
|
type CatalogElements struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id"`
|
||||||
Metadata Metadata `json:"metadata"`
|
Metadata Metadata `json:"metadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LossReason struct {
|
type LossReason struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Sort int `json:"sort"`
|
Sort int `json:"sort"`
|
||||||
CreatedAt int `json:"created_at"`
|
CreatedAt int `json:"created_at"`
|
||||||
UpdatedAt int `json:"updated_at"`
|
UpdatedAt int `json:"updated_at"`
|
||||||
Links Links `json:"_links"`
|
Links Links `json:"_links"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Companies struct {
|
type Companies struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id"`
|
||||||
Links Links `json:"_links"`
|
Links Links `json:"_links"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Contacts struct {
|
type Contacts struct {
|
||||||
Id int `json:"id"`
|
Id int `json:"id"`
|
||||||
IsMain bool `json:"is_main"`
|
IsMain bool `json:"is_main"`
|
||||||
Links Links `json:"_links"`
|
Links Links `json:"_links"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Embedded struct {
|
type Embedded struct {
|
||||||
Tags []*Tags `json:"tags"`
|
Tags []*Tags `json:"tags"`
|
||||||
CatalogElements []*CatalogElements `json:"catalog_elements"`
|
CatalogElements []*CatalogElements `json:"catalog_elements"`
|
||||||
LossReason []*LossReason `json:"loss_reason"`
|
LossReason []*LossReason `json:"loss_reason"`
|
||||||
Companies []*Companies `json:"companies"`
|
Companies []*Companies `json:"companies"`
|
||||||
Contacts []*Contacts `json:"contacts"`
|
Contacts []*Contacts `json:"contacts"`
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,6 +0,0 @@
|
||||||
version: 3
|
|
||||||
|
|
||||||
tasks:
|
|
||||||
btest:
|
|
||||||
cmds:
|
|
||||||
- go build ./cmd/test
|
|
18
users.go
Normal file
18
users.go
Normal 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
|
||||||
|
}
|
Loading…
Reference in a new issue