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~
|
||||
.env
|
||||
secret.json
|
||||
test
|
||||
/exe/
|
||||
|
|
124
amocrm.go
124
amocrm.go
|
@ -1,132 +1,28 @@
|
|||
package amo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"errors"
|
||||
"surdeus.su/core/amo/api"
|
||||
"surdeus.su/core/amo/companies"
|
||||
"surdeus.su/core/amo/contacts"
|
||||
"surdeus.su/core/amo/leads"
|
||||
"surdeus.su/core/amo/users"
|
||||
"surdeus.su/core/amo/events"
|
||||
)
|
||||
|
||||
type IAmoClient interface {
|
||||
GetUser(userId string) (*users.User, error)
|
||||
GetLead(leadId string, query string) (*leads.Lead, error)
|
||||
UpdateLead(lead *leads.Lead) error
|
||||
GetCompany(companyId string, query string) (*companies.Company, error)
|
||||
UpdateCompany(company *companies.Company) error
|
||||
GetContact(contactId string, query string) (*contacts.Contact, error)
|
||||
UpdateContact(contact *contacts.Contact) error
|
||||
}
|
||||
|
||||
type TokenPair = api.TokenPair
|
||||
type Options = api.ClientOptions
|
||||
const (
|
||||
// Maximum entities for once.
|
||||
// It is set to be the most effective
|
||||
// but in fact can be greater.
|
||||
MaxEnt = 50
|
||||
)
|
||||
|
||||
type Client struct {
|
||||
Api *api.Client
|
||||
API *api.Client
|
||||
}
|
||||
|
||||
type OauthTokenResponse = api.OauthTokenResponse
|
||||
|
||||
func NewAmoClient(secretPath string) (*Client, error) {
|
||||
apiClient, err := api.NewApi(secretPath)
|
||||
func NewClient(secretPath string) (*Client, error) {
|
||||
apiClient, err := api.NewAPI(secretPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Client{
|
||||
Api: apiClient,
|
||||
API: apiClient,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (client *Client) updateEntity(url string, id int, body interface{}) error {
|
||||
err := client.Api.Patch(fmt.Sprintf("%s/%d", url, id), body, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
func (client *Client) GetUser(userId int) (*users.User, error) {
|
||||
user := new(users.User)
|
||||
err := client.Api.Get(fmt.Sprintf("/api/v4/users/%d", userId), user)
|
||||
return user, err
|
||||
}
|
||||
|
||||
func (client *Client) GetLead(leadId int, query string) (*leads.Lead, error) {
|
||||
deal := new(leads.Lead)
|
||||
resource := fmt.Sprintf("/api/v4/leads/%d", leadId)
|
||||
if query != "" {
|
||||
resource = resource + "?" + query
|
||||
}
|
||||
|
||||
err := client.Api.Get(resource, deal)
|
||||
return deal, err
|
||||
}
|
||||
|
||||
func (client *Client) UpdateLead(lead *leads.Lead) error {
|
||||
return client.updateEntity("/api/v4/leads", lead.Id, lead)
|
||||
}
|
||||
|
||||
func (client *Client) GetCompany(companyId int, query string) (*companies.Company, error) {
|
||||
deal := new(companies.Company)
|
||||
resource := fmt.Sprintf("/api/v4/companies/%d", companyId)
|
||||
if query != "" {
|
||||
resource = resource + "?" + query
|
||||
}
|
||||
|
||||
err := client.Api.Get(resource, deal)
|
||||
return deal, err
|
||||
}
|
||||
|
||||
// Returns the events from AmoCRM by specified request.
|
||||
// If there are no such events returns an empty slice of events.
|
||||
func (client *Client) GetEvents(req events.EventsRequest) ([]events.Event, error) {
|
||||
res := "/api/v4/events"
|
||||
format := req.Format()
|
||||
if format != "" {
|
||||
res += "?" + format
|
||||
}
|
||||
|
||||
var abs bool
|
||||
ret := []events.Event{}
|
||||
for {
|
||||
resp := events.EventsResponse{}
|
||||
err := client.Api.Get(res, &resp, abs)
|
||||
if err != nil {
|
||||
// Return empty if no content avialable.
|
||||
if errors.Is(err, api.NoContentErr) {
|
||||
return ret, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
ret = append(ret, resp.Embedded.Events...)
|
||||
if resp.Links.Next.Href == "" {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Millisecond * 300)
|
||||
abs = true
|
||||
res = resp.Links.Next.Href
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (client *Client) UpdateCompany(company *companies.Company) error {
|
||||
return client.updateEntity("/api/v4/companies", company.Id, company)
|
||||
}
|
||||
|
||||
func (client *Client) GetContact(contactId int, query string) (*contacts.Contact, error) {
|
||||
deal := new(contacts.Contact)
|
||||
resource := fmt.Sprintf("/api/v4/contacts/%d", contactId)
|
||||
if query != "" {
|
||||
resource = resource + "?" + query
|
||||
}
|
||||
|
||||
err := client.Api.Get(resource, deal)
|
||||
return deal, err
|
||||
}
|
||||
|
||||
func (client *Client) UpdateContact(contact *contacts.Contact) error {
|
||||
return client.updateEntity("/api/v4/contacts", contact.Id, contact)
|
||||
}
|
||||
|
|
340
api/api.go
340
api/api.go
|
@ -1,35 +1,28 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"errors"
|
||||
"bytes"
|
||||
"time"
|
||||
"fmt"
|
||||
"log"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultContentType = "application/json"
|
||||
DefaultAccept = DefaultContentType
|
||||
DefaultContentType = "application/json"
|
||||
DefaultAccept = DefaultContentType
|
||||
DefaultCacheControl = "no-cache"
|
||||
)
|
||||
|
||||
type ClientOptions struct {
|
||||
Url string `json:"url"`
|
||||
RedirectUrl string `json:"redirect_url"`
|
||||
URL string `json:"url"`
|
||||
RedirectURL string `json:"redirect_url"`
|
||||
|
||||
AuthCode string `json:"auth_code"`
|
||||
|
||||
ClientId string `json:"client_id"`
|
||||
ClientId string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
|
||||
AccessToken string `json:"access_token"`
|
||||
AccessToken string `json:"access_token"`
|
||||
|
||||
ExpirationAt time.Time `json:"expiration_at,omitempty"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
|
@ -37,23 +30,16 @@ type ClientOptions struct {
|
|||
|
||||
type Client struct {
|
||||
*log.Logger
|
||||
options *ClientOptions
|
||||
BaseUrl *url.URL
|
||||
options ClientOptions
|
||||
BaseURL *url.URL
|
||||
secretStoreFilePath string
|
||||
Debug bool
|
||||
}
|
||||
|
||||
type requestOptions struct {
|
||||
HttpMethod string
|
||||
Body interface{}
|
||||
Headers map[string]string
|
||||
Abs bool
|
||||
}
|
||||
|
||||
type OauthTokenResponse struct {
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
AccessToken string `json:"access_token"`
|
||||
type OAuthTokenResponse struct {
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
|
@ -61,30 +47,34 @@ type TokenPair struct {
|
|||
Access, Refresh string
|
||||
}
|
||||
|
||||
func NewApi(secretPath string) (*Client, error) {
|
||||
func NewAPI(secretPath string) (*Client, error) {
|
||||
client := &Client{
|
||||
Logger: log.New(os.Stdout, "AmoCRM client: ", log.Ldate | log.Ltime),
|
||||
Logger: log.New(
|
||||
os.Stdout,
|
||||
"AmoCRM client: ",
|
||||
log.Ldate | log.Ltime,
|
||||
),
|
||||
secretStoreFilePath: secretPath,
|
||||
}
|
||||
options, err := client.readSecret()
|
||||
if err != nil {
|
||||
return nil, NewApiError(err)
|
||||
return nil, NewErrorAPI(err)
|
||||
}
|
||||
|
||||
if client.Debug {
|
||||
client.Printf("ClientOptions: %v\n", options)
|
||||
}
|
||||
|
||||
if options.Url == "" || options.RedirectUrl == "" {
|
||||
return nil, NewApiError(InvalidUrlOptionsErr)
|
||||
if options.URL == "" || options.RedirectURL == "" {
|
||||
return nil, NewErrorAPI(ErrInvalidOptionsURL)
|
||||
}
|
||||
|
||||
resolvedUrl, err := url.Parse(options.Url)
|
||||
parsedURL, err := url.Parse(options.URL)
|
||||
if err != nil {
|
||||
return nil, NewApiError(err)
|
||||
return nil, NewErrorAPI(err)
|
||||
}
|
||||
|
||||
client.BaseUrl = resolvedUrl
|
||||
client.BaseURL = parsedURL
|
||||
client.options = options
|
||||
|
||||
var (
|
||||
|
@ -92,293 +82,33 @@ func NewApi(secretPath string) (*Client, error) {
|
|||
exchangeErr error
|
||||
exchanged bool
|
||||
)
|
||||
if client.options.AccessToken == "" && client.options.RefreshToken == "" {
|
||||
if client.options.AccessToken == "" &&
|
||||
client.options.RefreshToken == "" {
|
||||
|
||||
if client.options.ClientSecret == "" ||
|
||||
client.options.ClientId == "" ||
|
||||
client.options.AuthCode == "" {
|
||||
return nil, NewApiError(InvalidExchangeAuthOptionsErr)
|
||||
return nil, NewErrorAPI(
|
||||
ErrInvalidExchangeAuthOptions,
|
||||
)
|
||||
}
|
||||
|
||||
_, exchangeErr = client.ExchangeAuth()
|
||||
exchanged = true
|
||||
}
|
||||
|
||||
if (!exchanged || exchangeErr != nil) && options.RefreshToken != "" {
|
||||
if (!exchanged || exchangeErr != nil) &&
|
||||
options.RefreshToken != "" {
|
||||
|
||||
// Refreshing token before the work.
|
||||
// Should think of how often should refresh
|
||||
// the token. (see the ExpiresIn)
|
||||
_, err = client.RefreshToken()
|
||||
if err != nil {
|
||||
return nil, NewApiError(err)
|
||||
return nil, NewErrorAPI(err)
|
||||
}
|
||||
}
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (api *Client) readSecret() (*ClientOptions, error) {
|
||||
f, err := os.Open(api.secretStoreFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bts, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
ret := ClientOptions{}
|
||||
err = json.Unmarshal(bts, &ret)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ret, nil
|
||||
}
|
||||
|
||||
func (api *Client) writeSecret() error {
|
||||
bts, err := json.MarshalIndent(api.options, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = os.WriteFile(api.secretStoreFilePath, bts, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (api *Client) rdoRequest(resourceUrl string, requestParams requestOptions, result interface{}) error {
|
||||
var (
|
||||
requestUrl *url.URL
|
||||
err error
|
||||
)
|
||||
|
||||
if !requestParams.Abs {
|
||||
resolvedUrl, err := url.Parse(resourceUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
requestUrl = api.BaseUrl.ResolveReference(resolvedUrl)
|
||||
} else {
|
||||
requestUrl, err = url.Parse(resourceUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
requestBody := new(bytes.Buffer)
|
||||
if requestParams.Body != nil {
|
||||
encoder := json.NewEncoder(requestBody)
|
||||
if err := encoder.Encode(requestParams.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
request, err := http.NewRequest(requestParams.HttpMethod, requestUrl.String(), requestBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if requestParams.Headers != nil {
|
||||
for k, v := range requestParams.Headers {
|
||||
request.Header.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if api.Debug {
|
||||
api.Printf("Request: %+v\n\nAmo response: %+v\n\n", request, response)
|
||||
}
|
||||
if response == nil {
|
||||
return RequestError(NoInternetErr)
|
||||
}
|
||||
if err != nil {
|
||||
return RequestError(fmt.Errorf(
|
||||
"%w: %d %s %s",
|
||||
err,
|
||||
response.StatusCode,
|
||||
requestParams.HttpMethod,
|
||||
resourceUrl,
|
||||
))
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode == 204 {
|
||||
return RequestError(NoContentErr)
|
||||
}
|
||||
|
||||
if response.StatusCode >= 400 {
|
||||
var specErr error
|
||||
if response.StatusCode == 401 {
|
||||
specErr = NoAuthErr
|
||||
}
|
||||
|
||||
responseErrorInfo, _ := ioutil.ReadAll(response.Body)
|
||||
return RequestError(fmt.Errorf(
|
||||
"%w: %s %s %q %d",
|
||||
specErr,
|
||||
requestParams.HttpMethod,
|
||||
resourceUrl,
|
||||
string(responseErrorInfo),
|
||||
response.StatusCode,
|
||||
))
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
err = decoder.Decode(result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func (api *Client) doRequest(
|
||||
resourceUrl string, requestParams requestOptions, result any,
|
||||
) error {
|
||||
var err error
|
||||
err = api.rdoRequest(resourceUrl, requestParams, result)
|
||||
if errors.Is(err, NoAuthErr) && api.options.RefreshToken != "" {
|
||||
_, err = api.RefreshToken()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return api.rdoRequest(resourceUrl, requestParams, result)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *Client) ExchangeAuth() (*TokenPair, error) {
|
||||
result := &OauthTokenResponse{}
|
||||
request := map[string] string {
|
||||
"client_id": api.options.ClientId,
|
||||
"client_secret": api.options.ClientSecret,
|
||||
"grant_type": "authorization_code",
|
||||
"code": api.options.AuthCode,
|
||||
"redirect_uri": api.options.RedirectUrl,
|
||||
}
|
||||
|
||||
err := api.rdoRequest("/oauth2/access_token", requestOptions{
|
||||
HttpMethod: http.MethodPost,
|
||||
Body: request,
|
||||
Headers: getHeaders(""),
|
||||
}, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := &TokenPair{
|
||||
Access: result.AccessToken,
|
||||
Refresh: result.RefreshToken,
|
||||
}
|
||||
|
||||
api.options.AccessToken = result.AccessToken
|
||||
api.options.RefreshToken = result.RefreshToken
|
||||
now := time.Now()
|
||||
api.options.ExpirationAt = now.Add(
|
||||
time.Second*time.Duration(result.ExpiresIn),
|
||||
)
|
||||
err = api.writeSecret()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (api *Client) RefreshTokenIfExpired() error {
|
||||
if api.options.RefreshToken == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if now.After(api.options.ExpirationAt) || now.Equal(api.options.ExpirationAt){
|
||||
_, err := api.RefreshToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *Client) RefreshToken() (*OauthTokenResponse, error) {
|
||||
result := new(OauthTokenResponse)
|
||||
request := map[string]string{
|
||||
"client_id": api.options.ClientId,
|
||||
"client_secret": api.options.ClientSecret,
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": api.options.RefreshToken,
|
||||
"redirect_uri": api.options.RedirectUrl,
|
||||
}
|
||||
|
||||
err := api.rdoRequest("/oauth2/access_token", requestOptions{
|
||||
HttpMethod: http.MethodPost,
|
||||
Body: request,
|
||||
Headers: getHeaders(""),
|
||||
}, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
api.options.AccessToken = result.AccessToken
|
||||
api.options.RefreshToken = result.RefreshToken
|
||||
now := time.Now()
|
||||
api.options.ExpirationAt = now.Add(
|
||||
time.Second*time.Duration(result.ExpiresIn),
|
||||
)
|
||||
err = api.writeSecret()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (api *Client) Get(resource string, result interface{}, abs ...bool) error {
|
||||
var a bool
|
||||
if len(abs) > 0 {
|
||||
a = abs[0]
|
||||
}
|
||||
return api.doRequest(resource, requestOptions{
|
||||
HttpMethod: http.MethodGet,
|
||||
Body: nil,
|
||||
Headers: getHeaders(api.options.AccessToken),
|
||||
Abs: a,
|
||||
}, result)
|
||||
}
|
||||
|
||||
func (api *Client) Post(resource string, request interface{}, result interface{}) error {
|
||||
return api.doRequest(resource, requestOptions{
|
||||
HttpMethod: http.MethodPost,
|
||||
Body: request,
|
||||
Headers: getHeaders(api.options.AccessToken),
|
||||
}, result)
|
||||
}
|
||||
|
||||
func (api *Client) Patch(resource string, request interface{}, result interface{}) error {
|
||||
return api.doRequest(resource, requestOptions{
|
||||
HttpMethod: http.MethodPatch,
|
||||
Body: request,
|
||||
Headers: getHeaders(api.options.AccessToken),
|
||||
}, result)
|
||||
}
|
||||
|
||||
func getHeaders(token string) map[string]string {
|
||||
headers := map[string]string{
|
||||
"Accept": DefaultAccept,
|
||||
"Cache-Control": DefaultCacheControl,
|
||||
"Content-Type": DefaultContentType,
|
||||
}
|
||||
|
||||
if len(token) > 0 {
|
||||
headers["Authorization"] = fmt.Sprintf("Bearer %s", token)
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
|
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 (
|
||||
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
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 (
|
||||
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
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/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
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"
|
||||
|
||||
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"`
|
||||
}
|
||||
|
|
|
@ -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