feat: implemented the way to acquire events in a Go structured way.
This commit is contained in:
parent
2e5805b97b
commit
4b088f5617
11 changed files with 361 additions and 57 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
*.exe
|
||||
*.exe~
|
||||
.env
|
||||
secret.json
|
||||
|
|
42
amocrm.go
42
amocrm.go
|
@ -7,6 +7,7 @@ import (
|
|||
"vultras.su/core/amo/contacts"
|
||||
"vultras.su/core/amo/leads"
|
||||
"vultras.su/core/amo/users"
|
||||
"vultras.su/core/amo/events"
|
||||
)
|
||||
|
||||
type IAmoClient interface {
|
||||
|
@ -28,15 +29,15 @@ type Client struct {
|
|||
|
||||
type OauthTokenResponse = api.OauthTokenResponse
|
||||
|
||||
func NewAmoClient(options *Options ) (*Client, *TokenPair, error) {
|
||||
apiClient, pair, err := api.NewClient(options)
|
||||
func NewAmoClient(secretPath string) (*Client, error) {
|
||||
apiClient, err := api.NewApi(secretPath)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &Client{
|
||||
Api: apiClient,
|
||||
}, pair, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (client *Client) updateEntity(url string, id int, body interface{}) error {
|
||||
|
@ -68,7 +69,7 @@ func (client *Client) UpdateLead(lead *leads.Lead) error {
|
|||
func (client *Client) GetCompany(companyId string, query string) (*companies.Company, error) {
|
||||
deal := new(companies.Company)
|
||||
resource := fmt.Sprintf("/api/v4/companies/%s", companyId)
|
||||
if len(query) != 0 {
|
||||
if query != "" {
|
||||
resource = resource + "?" + query
|
||||
}
|
||||
|
||||
|
@ -76,6 +77,37 @@ func (client *Client) GetCompany(companyId string, query string) (*companies.Com
|
|||
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 {
|
||||
if err == api.NoContentErr {
|
||||
return ret, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
ret = append(ret, resp.Embedded.Events...)
|
||||
if resp.Links.Next.Href == "" {
|
||||
break
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
|
162
api/api.go
162
api/api.go
|
@ -8,6 +8,9 @@ import (
|
|||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -17,27 +20,31 @@ const (
|
|||
)
|
||||
|
||||
type ClientOptions struct {
|
||||
Url string
|
||||
RedirectUrl string
|
||||
Url string `json:"url"`
|
||||
RedirectUrl string `json:"redirect_url"`
|
||||
|
||||
AuthCode string
|
||||
AuthCode string `json:"auth_code"`
|
||||
|
||||
ClientId string
|
||||
ClientSecret string
|
||||
ClientId string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
|
||||
AccessToken string
|
||||
RefreshToken string
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpirationDate time.Time `json:"access"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
options *ClientOptions
|
||||
BaseUrl *url.URL
|
||||
secretStoreFilePath string
|
||||
Debug bool `json`
|
||||
}
|
||||
|
||||
type requestOptions struct {
|
||||
HttpMethod string
|
||||
Body interface{}
|
||||
Headers map[string]string
|
||||
Abs bool
|
||||
}
|
||||
|
||||
type OauthTokenResponse struct {
|
||||
|
@ -51,42 +58,114 @@ type TokenPair struct {
|
|||
Access, Refresh string
|
||||
}
|
||||
|
||||
func NewClient(options *ClientOptions) (*Client, *TokenPair, error) {
|
||||
if options.Url == "" {
|
||||
return nil, nil, errors.New("AmoCrm: Invalid options: Url")
|
||||
func NewApi(secretPath string) (*Client, error) {
|
||||
ret := &Client{
|
||||
secretStoreFilePath: secretPath,
|
||||
}
|
||||
options, err := ret.readSecret()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if options.Url == "" || options.RedirectUrl == "" {
|
||||
return nil, errors.New("AmoCrm: Invalid options: Url")
|
||||
}
|
||||
resolvedUrl, err := url.Parse(options.Url)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := &Client{
|
||||
options: options,
|
||||
}
|
||||
ret.BaseUrl = resolvedUrl
|
||||
|
||||
var pair *TokenPair
|
||||
if options.AccessToken == "" || options.RefreshToken == "" {
|
||||
pair, err = ret.ExchangeAuth()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, err
|
||||
}
|
||||
ret.options = options
|
||||
|
||||
var (
|
||||
//pair *TokenPair
|
||||
exchangeErr error
|
||||
exchanged bool
|
||||
)
|
||||
if ret.options.AccessToken == "" || ret.options.RefreshToken == "" {
|
||||
if ret.options.ClientSecret == "" ||
|
||||
ret.options.ClientId == "" ||
|
||||
ret.options.AuthCode == "" {
|
||||
return nil, errors.New("AmoCrm: invalid options: ExchangeAuth")
|
||||
}
|
||||
_, exchangeErr = ret.ExchangeAuth()
|
||||
exchanged = true
|
||||
}
|
||||
|
||||
if !exchanged || exchangeErr != nil {
|
||||
// Refreshing token before the work.
|
||||
// Should think of how often should refresh
|
||||
// the token. (see the ExpiresIn)
|
||||
_, err = ret.RefreshToken()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &Client{
|
||||
options: options,
|
||||
BaseUrl: resolvedUrl,
|
||||
}, pair, nil
|
||||
}, 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) doRequest(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)
|
||||
requestUrl = api.BaseUrl.ResolveReference(resolvedUrl)
|
||||
} else {
|
||||
requestUrl, err = url.Parse(resourceUrl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
requestBody := new(bytes.Buffer)
|
||||
if requestParams.Body != nil {
|
||||
|
@ -108,6 +187,9 @@ func (api *Client) doRequest(resourceUrl string, requestParams requestOptions, r
|
|||
}
|
||||
|
||||
response, err := http.DefaultClient.Do(request)
|
||||
if api.Debug {
|
||||
fmt.Printf("\nAmo request: %+v\nAmo repsonse: %+v\n", request, response)
|
||||
}
|
||||
if err != nil {
|
||||
return errors.New(fmt.Sprintf(
|
||||
"Request error: %s %d %s %s",
|
||||
|
@ -118,8 +200,23 @@ func (api *Client) doRequest(resourceUrl string, requestParams requestOptions, r
|
|||
))
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode == 204 {
|
||||
return NoContentErr
|
||||
}
|
||||
|
||||
if response.StatusCode >= 400 {
|
||||
/*if response.StatusCode == 401 {
|
||||
_, err := api.RefreshToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return api.doRequest(
|
||||
resourceUrl,
|
||||
requestParams,
|
||||
result,
|
||||
)
|
||||
}*/
|
||||
|
||||
bodyBytes, _ := ioutil.ReadAll(response.Body)
|
||||
return errors.New(fmt.Sprintf(
|
||||
"%s %d %s %s",
|
||||
|
@ -168,6 +265,10 @@ func (api *Client) ExchangeAuth() (*TokenPair, error) {
|
|||
|
||||
api.options.AccessToken = result.AccessToken
|
||||
api.options.RefreshToken = result.RefreshToken
|
||||
err = api.writeSecret()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
|
@ -178,7 +279,7 @@ func (api *Client) RefreshToken() (*OauthTokenResponse, error) {
|
|||
"client_secret": api.options.ClientSecret,
|
||||
"grant_type": "refresh_token",
|
||||
"refresh_token": api.options.RefreshToken,
|
||||
"redirect_uri": api.options.Url,
|
||||
"redirect_uri": api.options.RedirectUrl,
|
||||
}
|
||||
|
||||
err := api.doRequest("/oauth2/access_token", requestOptions{
|
||||
|
@ -186,15 +287,30 @@ func (api *Client) RefreshToken() (*OauthTokenResponse, error) {
|
|||
Body: request,
|
||||
Headers: getHeaders(""),
|
||||
}, result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, err
|
||||
api.options.AccessToken = result.AccessToken
|
||||
api.options.RefreshToken = result.RefreshToken
|
||||
err = api.writeSecret()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (api *Client) Get(resource string, result interface{}) error {
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
9
api/errors.go
Normal file
9
api/errors.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
var (
|
||||
NoContentErr = errors.New("no content")
|
||||
)
|
|
@ -2,13 +2,16 @@ package main
|
|||
|
||||
import (
|
||||
"vultras.su/core/amo"
|
||||
"vultras.su/core/amo/api"
|
||||
"vultras.su/core/amo/webhooks"
|
||||
"vultras.su/core/amo/events"
|
||||
"vultras.su/core/bond"
|
||||
"vultras.su/core/bond/statuses"
|
||||
"os"
|
||||
//"os"
|
||||
"fmt"
|
||||
"io"
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Context = bond.Context
|
||||
|
@ -54,36 +57,46 @@ Def(
|
|||
))
|
||||
|
||||
func main() {
|
||||
|
||||
opts := &amo.Options{
|
||||
Url: os.Getenv("AMO_URL"),
|
||||
RedirectUrl: os.Getenv("AMO_REDIRECT_URL"),
|
||||
AccessToken: os.Getenv("AMO_ACCESS"),
|
||||
RefreshToken: os.Getenv("AMO_REFRESH"),
|
||||
ClientSecret: os.Getenv("AMO_CLIENT_SECRET"),
|
||||
ClientId: os.Getenv("AMO_CLIENT_ID"),
|
||||
AuthCode: os.Getenv("AMO_AUTH_CODE"),
|
||||
}
|
||||
fmt.Println(opts)
|
||||
client, pair, err := amo.NewAmoClient(opts)
|
||||
//fmt.Println(opts)
|
||||
client, err := amo.NewAmoClient("secret.json")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
client.Api.Debug = true
|
||||
gclient = client
|
||||
if pair != nil {
|
||||
fmt.Println("PAIR: %q", pair)
|
||||
}
|
||||
|
||||
/*company, err := client.GetCompany("80699047", "")
|
||||
if err != nil {
|
||||
company, err := client.GetCompany("80828925", "")
|
||||
if err != nil && err != api.NoContentErr {
|
||||
panic(err)
|
||||
}*/
|
||||
srv := bond.Server{
|
||||
Addr: ":15080",
|
||||
Handler: root,
|
||||
}
|
||||
err = srv.ListenAndServe()
|
||||
fmt.Printf("company: %+v\n", company)
|
||||
interval := time.Second * 10
|
||||
|
||||
now := time.Now()
|
||||
lastChanged := now
|
||||
for {
|
||||
time.Sleep(interval)
|
||||
req := events.EventsRequest{}
|
||||
req.Limit = 10
|
||||
req.Filter.Entity = []string{"company", "contact"}
|
||||
req.Filter.Type = events.CustomFieldValueChanged(
|
||||
2192301,
|
||||
2678095,
|
||||
)
|
||||
req.With = []string{"company_name"}
|
||||
req.Filter.CreatedAt.From = lastChanged
|
||||
events, err := client.GetEvents(req)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
fmt.Printf("Error: %s", err)
|
||||
continue
|
||||
}
|
||||
if len(events) == 0 {
|
||||
fmt.Printf("nothing changed")
|
||||
continue
|
||||
}
|
||||
lastChanged = time.Now()
|
||||
for i, event := range events {
|
||||
fmt.Printf("event %d: %v\n", i, event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
25
events/custom-field.go
Normal file
25
events/custom-field.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package events
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type Value struct {
|
||||
CustomFieldValue *CustomFieldValue `json:"custom_field_value,omitempty"`
|
||||
}
|
||||
|
||||
type CustomFieldValue struct {
|
||||
EnumId int `json:"enum_id,omitempty"`
|
||||
FieldId int `json:"field_id"`
|
||||
FieldType int `json:"field_type"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func CustomFieldValueChanged(ids ...int64) []string {
|
||||
ret := make([]string, len(ids))
|
||||
for i, id := range ids {
|
||||
ret[i] = fmt.Sprintf("custom_field_%d_value_changed", id)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
43
events/event.go
Normal file
43
events/event.go
Normal file
|
@ -0,0 +1,43 @@
|
|||
package events
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type EventsResponse struct {
|
||||
Page int `json:"_page"`
|
||||
Links struct {
|
||||
Self struct {
|
||||
Href string `json:"href"`
|
||||
} `json:"self"`
|
||||
Next struct {
|
||||
Href string `json:"href"`
|
||||
} `json:"next"`
|
||||
} `json:"_links"`
|
||||
|
||||
Embedded struct{
|
||||
Events []Event `json:"events"`
|
||||
} `json:"_embedded"`
|
||||
}
|
||||
|
||||
//type Events []Event
|
||||
|
||||
type Event struct {
|
||||
Id string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
EntityId int `json:"entity_id"`
|
||||
EntityType string `json:"entity_type"`
|
||||
CreatedBy int64 `json:"created_by"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
ValueAfter []Value `json:"value_after"`
|
||||
ValueBefore []Value `json:"value_before,omitempty"`
|
||||
AccountId int `json:"account_id"`
|
||||
Embedded struct {
|
||||
} `json:"_embedded"`
|
||||
}
|
||||
|
||||
func (e Event) String() string {
|
||||
bts, _ := json.MarshalIndent(e, "", "\t")
|
||||
return string(bts)
|
||||
}
|
||||
|
61
events/main.go
Normal file
61
events/main.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package events
|
||||
|
||||
import (
|
||||
"time"
|
||||
"strings"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type EventsRequest struct {
|
||||
Filter struct {
|
||||
CreatedAt struct {
|
||||
From, To time.Time
|
||||
}
|
||||
Type []string
|
||||
Entity []string
|
||||
}
|
||||
Page, Limit uint
|
||||
Ids, CreatedBy []int
|
||||
With []string
|
||||
}
|
||||
|
||||
func (req EventsRequest) Format() string {
|
||||
opts := []string{}
|
||||
|
||||
if len(req.With) > 0 {
|
||||
buf := fmt.Sprintf("with=%s", req.With[0])
|
||||
for _, with := range req.With[1:] {
|
||||
buf += ","+with
|
||||
}
|
||||
opts = append(opts, buf)
|
||||
}
|
||||
if req.Page > 0 {
|
||||
opts = append(opts, fmt.Sprintf("page=%d", req.Page))
|
||||
}
|
||||
if req.Limit > 0 {
|
||||
opts = append(opts, fmt.Sprintf("limit=%d", req.Limit))
|
||||
}
|
||||
if !req.Filter.CreatedAt.From.IsZero() && req.Filter.CreatedAt.To.IsZero() {
|
||||
opts = append(opts, fmt.Sprintf("filter[created_at]=%d",
|
||||
req.Filter.CreatedAt.From.Unix(),
|
||||
))
|
||||
} else if !req.Filter.CreatedAt.From.IsZero() && !req.Filter.CreatedAt.To.IsZero() {
|
||||
opts = append(
|
||||
opts,
|
||||
fmt.Sprintf("filter[created_at][from]=%d", req.Filter.CreatedAt.From.Unix()),
|
||||
fmt.Sprintf("filter[created_at][to]=%d", req.Filter.CreatedAt.To.Unix()),
|
||||
)
|
||||
}
|
||||
|
||||
for i, typ := range req.Filter.Type {
|
||||
opts = append(opts, fmt.Sprintf("filter[type][%d]=%s", i, typ))
|
||||
}
|
||||
|
||||
for i, ent := range req.Filter.Entity {
|
||||
opts = append(opts, fmt.Sprintf("filter[entity][%d]=%s", i, ent))
|
||||
}
|
||||
|
||||
ret := strings.Join(opts, "&")
|
||||
return ret
|
||||
}
|
||||
|
2
go.mod
2
go.mod
|
@ -4,7 +4,7 @@ go 1.21.3
|
|||
|
||||
require (
|
||||
github.com/stretchr/testify v1.6.1
|
||||
vultras.su/core/bond v0.0.0-20240114204709-a9c2c8810682
|
||||
vultras.su/core/bond v0.0.0-20240118183558-6fa4ef4cf402
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
2
go.sum
2
go.sum
|
@ -11,3 +11,5 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie
|
|||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
vultras.su/core/bond v0.0.0-20240114204709-a9c2c8810682 h1:NiT5kAwzjTO+C4/y2EyRI6N8DOl5YFLuyZEUqrYwfFE=
|
||||
vultras.su/core/bond v0.0.0-20240114204709-a9c2c8810682/go.mod h1:d8O5wwQlZrVAeoV7qIwxXabB9RuqgopP7wEyRl3++Tc=
|
||||
vultras.su/core/bond v0.0.0-20240118183558-6fa4ef4cf402 h1:XUEdQesLiMX8mK2ZQpJyfE0p+MfmgnOdM1Mt72F+FW4=
|
||||
vultras.su/core/bond v0.0.0-20240118183558-6fa4ef4cf402/go.mod h1:d8O5wwQlZrVAeoV7qIwxXabB9RuqgopP7wEyRl3++Tc=
|
||||
|
|
2
webhooks/handler.go
Normal file
2
webhooks/handler.go
Normal file
|
@ -0,0 +1,2 @@
|
|||
package webhooks
|
||||
|
Loading…
Reference in a new issue