Compare commits
No commits in common. "leads-query" and "main" have entirely different histories.
leads-quer
...
main
55 changed files with 2427 additions and 644 deletions
19
.gitignore
vendored
19
.gitignore
vendored
|
@ -1,16 +1,7 @@
|
||||||
# Binaries for programs and plugins
|
|
||||||
*.exe
|
*.exe
|
||||||
*.exe~
|
*.exe~
|
||||||
*.dll
|
.env
|
||||||
*.so
|
secret.json
|
||||||
*.dylib
|
*.json
|
||||||
|
/exe/
|
||||||
# Test binary, built with `go test -c`
|
/tmp/
|
||||||
*.test
|
|
||||||
|
|
||||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
|
||||||
*.out
|
|
||||||
|
|
||||||
# Dependency directories (remove the comment below to include it)
|
|
||||||
# vendor/
|
|
||||||
.idea
|
|
||||||
|
|
201
LICENSE
201
LICENSE
|
@ -1,201 +0,0 @@
|
||||||
Apache License
|
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
||||||
|
|
||||||
1. Definitions.
|
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
|
||||||
the copyright owner that is granting the License.
|
|
||||||
|
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
|
||||||
other entities that control, are controlled by, or are under common
|
|
||||||
control with that entity. For the purposes of this definition,
|
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
|
||||||
direction or management of such entity, whether by contract or
|
|
||||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
||||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
||||||
|
|
||||||
"You" (or "Your") shall mean an individual or Legal Entity
|
|
||||||
exercising permissions granted by this License.
|
|
||||||
|
|
||||||
"Source" form shall mean the preferred form for making modifications,
|
|
||||||
including but not limited to software source code, documentation
|
|
||||||
source, and configuration files.
|
|
||||||
|
|
||||||
"Object" form shall mean any form resulting from mechanical
|
|
||||||
transformation or translation of a Source form, including but
|
|
||||||
not limited to compiled object code, generated documentation,
|
|
||||||
and conversions to other media types.
|
|
||||||
|
|
||||||
"Work" shall mean the work of authorship, whether in Source or
|
|
||||||
Object form, made available under the License, as indicated by a
|
|
||||||
copyright notice that is included in or attached to the work
|
|
||||||
(an example is provided in the Appendix below).
|
|
||||||
|
|
||||||
"Derivative Works" shall mean any work, whether in Source or Object
|
|
||||||
form, that is based on (or derived from) the Work and for which the
|
|
||||||
editorial revisions, annotations, elaborations, or other modifications
|
|
||||||
represent, as a whole, an original work of authorship. For the purposes
|
|
||||||
of this License, Derivative Works shall not include works that remain
|
|
||||||
separable from, or merely link (or bind by name) to the interfaces of,
|
|
||||||
the Work and Derivative Works thereof.
|
|
||||||
|
|
||||||
"Contribution" shall mean any work of authorship, including
|
|
||||||
the original version of the Work and any modifications or additions
|
|
||||||
to that Work or Derivative Works thereof, that is intentionally
|
|
||||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
||||||
or by an individual or Legal Entity authorized to submit on behalf of
|
|
||||||
the copyright owner. For the purposes of this definition, "submitted"
|
|
||||||
means any form of electronic, verbal, or written communication sent
|
|
||||||
to the Licensor or its representatives, including but not limited to
|
|
||||||
communication on electronic mailing lists, source code control systems,
|
|
||||||
and issue tracking systems that are managed by, or on behalf of, the
|
|
||||||
Licensor for the purpose of discussing and improving the Work, but
|
|
||||||
excluding communication that is conspicuously marked or otherwise
|
|
||||||
designated in writing by the copyright owner as "Not a Contribution."
|
|
||||||
|
|
||||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
||||||
on behalf of whom a Contribution has been received by Licensor and
|
|
||||||
subsequently incorporated within the Work.
|
|
||||||
|
|
||||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
copyright license to reproduce, prepare Derivative Works of,
|
|
||||||
publicly display, publicly perform, sublicense, and distribute the
|
|
||||||
Work and such Derivative Works in Source or Object form.
|
|
||||||
|
|
||||||
3. Grant of Patent License. Subject to the terms and conditions of
|
|
||||||
this License, each Contributor hereby grants to You a perpetual,
|
|
||||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
||||||
(except as stated in this section) patent license to make, have made,
|
|
||||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
||||||
where such license applies only to those patent claims licensable
|
|
||||||
by such Contributor that are necessarily infringed by their
|
|
||||||
Contribution(s) alone or by combination of their Contribution(s)
|
|
||||||
with the Work to which such Contribution(s) was submitted. If You
|
|
||||||
institute patent litigation against any entity (including a
|
|
||||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
||||||
or a Contribution incorporated within the Work constitutes direct
|
|
||||||
or contributory patent infringement, then any patent licenses
|
|
||||||
granted to You under this License for that Work shall terminate
|
|
||||||
as of the date such litigation is filed.
|
|
||||||
|
|
||||||
4. Redistribution. You may reproduce and distribute copies of the
|
|
||||||
Work or Derivative Works thereof in any medium, with or without
|
|
||||||
modifications, and in Source or Object form, provided that You
|
|
||||||
meet the following conditions:
|
|
||||||
|
|
||||||
(a) You must give any other recipients of the Work or
|
|
||||||
Derivative Works a copy of this License; and
|
|
||||||
|
|
||||||
(b) You must cause any modified files to carry prominent notices
|
|
||||||
stating that You changed the files; and
|
|
||||||
|
|
||||||
(c) You must retain, in the Source form of any Derivative Works
|
|
||||||
that You distribute, all copyright, patent, trademark, and
|
|
||||||
attribution notices from the Source form of the Work,
|
|
||||||
excluding those notices that do not pertain to any part of
|
|
||||||
the Derivative Works; and
|
|
||||||
|
|
||||||
(d) If the Work includes a "NOTICE" text file as part of its
|
|
||||||
distribution, then any Derivative Works that You distribute must
|
|
||||||
include a readable copy of the attribution notices contained
|
|
||||||
within such NOTICE file, excluding those notices that do not
|
|
||||||
pertain to any part of the Derivative Works, in at least one
|
|
||||||
of the following places: within a NOTICE text file distributed
|
|
||||||
as part of the Derivative Works; within the Source form or
|
|
||||||
documentation, if provided along with the Derivative Works; or,
|
|
||||||
within a display generated by the Derivative Works, if and
|
|
||||||
wherever such third-party notices normally appear. The contents
|
|
||||||
of the NOTICE file are for informational purposes only and
|
|
||||||
do not modify the License. You may add Your own attribution
|
|
||||||
notices within Derivative Works that You distribute, alongside
|
|
||||||
or as an addendum to the NOTICE text from the Work, provided
|
|
||||||
that such additional attribution notices cannot be construed
|
|
||||||
as modifying the License.
|
|
||||||
|
|
||||||
You may add Your own copyright statement to Your modifications and
|
|
||||||
may provide additional or different license terms and conditions
|
|
||||||
for use, reproduction, or distribution of Your modifications, or
|
|
||||||
for any such Derivative Works as a whole, provided Your use,
|
|
||||||
reproduction, and distribution of the Work otherwise complies with
|
|
||||||
the conditions stated in this License.
|
|
||||||
|
|
||||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
||||||
any Contribution intentionally submitted for inclusion in the Work
|
|
||||||
by You to the Licensor shall be under the terms and conditions of
|
|
||||||
this License, without any additional terms or conditions.
|
|
||||||
Notwithstanding the above, nothing herein shall supersede or modify
|
|
||||||
the terms of any separate license agreement you may have executed
|
|
||||||
with Licensor regarding such Contributions.
|
|
||||||
|
|
||||||
6. Trademarks. This License does not grant permission to use the trade
|
|
||||||
names, trademarks, service marks, or product names of the Licensor,
|
|
||||||
except as required for reasonable and customary use in describing the
|
|
||||||
origin of the Work and reproducing the content of the NOTICE file.
|
|
||||||
|
|
||||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
||||||
agreed to in writing, Licensor provides the Work (and each
|
|
||||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
||||||
implied, including, without limitation, any warranties or conditions
|
|
||||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
||||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
||||||
appropriateness of using or redistributing the Work and assume any
|
|
||||||
risks associated with Your exercise of permissions under this License.
|
|
||||||
|
|
||||||
8. Limitation of Liability. In no event and under no legal theory,
|
|
||||||
whether in tort (including negligence), contract, or otherwise,
|
|
||||||
unless required by applicable law (such as deliberate and grossly
|
|
||||||
negligent acts) or agreed to in writing, shall any Contributor be
|
|
||||||
liable to You for damages, including any direct, indirect, special,
|
|
||||||
incidental, or consequential damages of any character arising as a
|
|
||||||
result of this License or out of the use or inability to use the
|
|
||||||
Work (including but not limited to damages for loss of goodwill,
|
|
||||||
work stoppage, computer failure or malfunction, or any and all
|
|
||||||
other commercial damages or losses), even if such Contributor
|
|
||||||
has been advised of the possibility of such damages.
|
|
||||||
|
|
||||||
9. Accepting Warranty or Additional Liability. While redistributing
|
|
||||||
the Work or Derivative Works thereof, You may choose to offer,
|
|
||||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
||||||
or other liability obligations and/or rights consistent with this
|
|
||||||
License. However, in accepting such obligations, You may act only
|
|
||||||
on Your own behalf and on Your sole responsibility, not on behalf
|
|
||||||
of any other Contributor, and only if You agree to indemnify,
|
|
||||||
defend, and hold each Contributor harmless for any liability
|
|
||||||
incurred by, or claims asserted against, such Contributor by reason
|
|
||||||
of your accepting any such warranty or additional liability.
|
|
||||||
|
|
||||||
END OF TERMS AND CONDITIONS
|
|
||||||
|
|
||||||
APPENDIX: How to apply the Apache License to your work.
|
|
||||||
|
|
||||||
To apply the Apache License to your work, attach the following
|
|
||||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
||||||
replaced with your own identifying information. (Don't include
|
|
||||||
the brackets!) The text should be enclosed in the appropriate
|
|
||||||
comment syntax for the file format. We also recommend that a
|
|
||||||
file or class name and description of purpose be included on the
|
|
||||||
same "printed page" as the copyright notice for easier
|
|
||||||
identification within third-party archives.
|
|
||||||
|
|
||||||
Copyright [yyyy] [name of copyright owner]
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
|
@ -1 +0,0 @@
|
||||||
# go-amo
|
|
|
@ -1,69 +0,0 @@
|
||||||
package amo
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"github.com/qdimka/go-amo/api"
|
|
||||||
"github.com/qdimka/go-amo/models/companies"
|
|
||||||
"github.com/qdimka/go-amo/models/contacts"
|
|
||||||
"github.com/qdimka/go-amo/models/leads"
|
|
||||||
"github.com/qdimka/go-amo/models/users"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AmoClient struct {
|
|
||||||
api *api.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAmoClient(options *api.ClientOptions) (*AmoClient, error) {
|
|
||||||
apiClient, err := api.NewClient(options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &AmoClient{
|
|
||||||
api: apiClient,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *AmoClient) GetLead(leadId string, query string) (*leads.Lead, error) {
|
|
||||||
deal := new(leads.Lead)
|
|
||||||
resource := fmt.Sprintf("/api/v4/leads/%s", leadId)
|
|
||||||
if len(query) != 0 {
|
|
||||||
resource = resource + "?" + query
|
|
||||||
}
|
|
||||||
|
|
||||||
err := client.api.Get(resource, deal)
|
|
||||||
return deal, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *AmoClient) GetUser(userId string) (*users.User, error) {
|
|
||||||
user := new(users.User)
|
|
||||||
err := client.api.Get(fmt.Sprintf("/api/v4/users/%s", userId), user)
|
|
||||||
return user, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *AmoClient) UpdateLead(lead *leads.Lead) error {
|
|
||||||
err := client.api.Patch(fmt.Sprintf("/api/v4/leads/%d", lead.ID), lead, nil)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *AmoClient) GetCompany(companyId string, query string) (*companies.Company, error) {
|
|
||||||
deal := new(companies.Company)
|
|
||||||
resource := fmt.Sprintf("/api/v4/companies/%s", companyId)
|
|
||||||
if len(query) != 0 {
|
|
||||||
resource = resource + "?" + query
|
|
||||||
}
|
|
||||||
|
|
||||||
err := client.api.Get(resource, deal)
|
|
||||||
return deal, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *AmoClient) GetContact(contactId string, query string) (*contacts.Contact, error) {
|
|
||||||
deal := new(contacts.Contact)
|
|
||||||
resource := fmt.Sprintf("/api/v4/contacts/%s", contactId)
|
|
||||||
if len(query) != 0 {
|
|
||||||
resource = resource + "?" + query
|
|
||||||
}
|
|
||||||
|
|
||||||
err := client.api.Get(resource, deal)
|
|
||||||
return deal, err
|
|
||||||
}
|
|
34
amocrm.go
Normal file
34
amocrm.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package amo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"surdeus.su/core/amo/api"
|
||||||
|
"surdeus.su/core/amo/leads"
|
||||||
|
"surdeus.su/core/amo/companies"
|
||||||
|
"surdeus.su/core/amo/contacts"
|
||||||
|
"surdeus.su/core/amo/events"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Lead = leads.Lead
|
||||||
|
type Company = companies.Company
|
||||||
|
type Contact = contacts.Contact
|
||||||
|
type Event = events.Event
|
||||||
|
|
||||||
|
const (
|
||||||
|
MEPR = api.MaxEntitiesPerRequest
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
API *api.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(secretPath string) (*Client, error) {
|
||||||
|
apiClient, err := api.NewAPI(secretPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Client{
|
||||||
|
API: apiClient,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
246
api/api.go
246
api/api.go
|
@ -1,164 +1,162 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"time"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
DefaultContentType = "application/json"
|
||||||
|
DefaultAccept = DefaultContentType
|
||||||
|
DefaultCacheControl = "no-cache"
|
||||||
|
MaxEntitiesPerRequest = 250
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClientOptions struct {
|
type ClientOptions struct {
|
||||||
Url string
|
URL string `json:"url"`
|
||||||
ClientId string
|
RedirectURL string `json:"redirect_url"`
|
||||||
ClientSecret string
|
|
||||||
AccessToken string
|
|
||||||
RefreshToken string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Client struct {
|
AuthCode string `json:"auth_code"`
|
||||||
options *ClientOptions
|
|
||||||
BaseUrl *url.URL
|
|
||||||
}
|
|
||||||
|
|
||||||
type requestOptions struct {
|
ClientId string `json:"client_id"`
|
||||||
HttpMethod string
|
ClientSecret string `json:"client_secret"`
|
||||||
Body interface{}
|
|
||||||
Headers map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
type OauthTokenResponse struct {
|
AccessToken string `json:"access_token"`
|
||||||
TokenType string `json:"token_type"`
|
|
||||||
ExpiresIn int `json:"expires_in"`
|
ExpirationAt time.Time `json:"expiration_at,omitempty"`
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewClient(options *ClientOptions) (*Client, error) {
|
type Client struct {
|
||||||
if len(options.AccessToken) == 0 || len(options.RefreshToken) == 0 || len(options.Url) == 0 {
|
*log.Logger
|
||||||
return nil, errors.New("AmoCrm: Invalid options")
|
options ClientOptions
|
||||||
}
|
BaseURL *url.URL
|
||||||
|
secretStoreFilePath string
|
||||||
|
Debug bool
|
||||||
|
|
||||||
resolvedUrl, err := url.Parse(options.Url)
|
availableRequests int
|
||||||
if err != nil {
|
mrps int
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Client{
|
ticker *time.Ticker
|
||||||
options: options,
|
req, reqNeed chan struct{}
|
||||||
BaseUrl: resolvedUrl,
|
|
||||||
}, nil
|
requestsMade int64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *Client) doRequest(resourceUrl string, requestParams requestOptions, result interface{}) error {
|
type OAuthTokenResponse struct {
|
||||||
resolvedUrl, err := url.Parse(resourceUrl)
|
TokenType string `json:"token_type"`
|
||||||
|
ExpiresIn int `json:"expires_in"`
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
RefreshToken string `json:"refresh_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TokenPair struct {
|
||||||
|
Access, Refresh string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPI(secretPath string) (*Client, error) {
|
||||||
|
client := &Client{
|
||||||
|
Logger: log.New(
|
||||||
|
os.Stdout,
|
||||||
|
"AmoCRM client: ",
|
||||||
|
log.Ldate | log.Ltime,
|
||||||
|
),
|
||||||
|
secretStoreFilePath: secretPath,
|
||||||
|
}
|
||||||
|
options, err := client.readSecret()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, NewErrorAPI(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
requestUrl := api.BaseUrl.ResolveReference(resolvedUrl)
|
if client.Debug {
|
||||||
|
client.Printf("ClientOptions: %v\n", options)
|
||||||
|
}
|
||||||
|
|
||||||
requestBody := new(bytes.Buffer)
|
if options.URL == "" || options.RedirectURL == "" {
|
||||||
if requestParams.Body != nil {
|
return nil, NewErrorAPI(ErrInvalidOptionsURL)
|
||||||
encoder := json.NewEncoder(requestBody)
|
}
|
||||||
if err := encoder.Encode(requestParams.Body); err != nil {
|
|
||||||
return err
|
parsedURL, err := url.Parse(options.URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, NewErrorAPI(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client.BaseURL = parsedURL
|
||||||
|
client.options = options
|
||||||
|
|
||||||
|
var (
|
||||||
|
//pair *TokenPair
|
||||||
|
exchangeErr error
|
||||||
|
exchanged bool
|
||||||
|
)
|
||||||
|
if client.options.AccessToken == "" &&
|
||||||
|
client.options.RefreshToken == "" {
|
||||||
|
|
||||||
|
if client.options.ClientSecret == "" ||
|
||||||
|
client.options.ClientId == "" ||
|
||||||
|
client.options.AuthCode == "" {
|
||||||
|
return nil, NewErrorAPI(
|
||||||
|
ErrInvalidExchangeAuthOptions,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_, exchangeErr = client.ExchangeAuth()
|
||||||
|
exchanged = true
|
||||||
}
|
}
|
||||||
|
|
||||||
request, err := http.NewRequest(requestParams.HttpMethod, requestUrl.String(), requestBody)
|
if (!exchanged || exchangeErr != nil) &&
|
||||||
if err != nil {
|
options.RefreshToken != "" {
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if requestParams.Headers != nil {
|
// Refreshing token before the work.
|
||||||
for k, v := range requestParams.Headers {
|
// Should think of how often should refresh
|
||||||
request.Header.Set(k, v)
|
// the token. (see the ExpiresIn)
|
||||||
}
|
_, err = client.RefreshToken()
|
||||||
}
|
|
||||||
|
|
||||||
response, err := http.DefaultClient.Do(request)
|
|
||||||
if err != nil {
|
|
||||||
return errors.New(fmt.Sprintf("Request error: %s %d %s %s", err.Error(), response.StatusCode, requestParams.HttpMethod, resourceUrl))
|
|
||||||
}
|
|
||||||
defer response.Body.Close()
|
|
||||||
|
|
||||||
if response.StatusCode >= 400 {
|
|
||||||
bodyBytes, _ := ioutil.ReadAll(response.Body)
|
|
||||||
return errors.New(fmt.Sprintf("%s %d %s %s", string(bodyBytes), response.StatusCode, requestParams.HttpMethod, resourceUrl))
|
|
||||||
}
|
|
||||||
|
|
||||||
if result != nil {
|
|
||||||
decoder := json.NewDecoder(response.Body)
|
|
||||||
err = decoder.Decode(result)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, NewErrorAPI(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *Client) RefreshToken() (*OauthTokenResponse, error) {
|
// Set maximum requests per second.
|
||||||
result := new(OauthTokenResponse)
|
func (client *Client) SetMRPS(rps int) *Client {
|
||||||
request := map[string]string{
|
client.mrps = rps
|
||||||
"client_id": api.options.ClientId,
|
client.req = make(chan struct{})
|
||||||
"client_secret": api.options.ClientSecret,
|
client.reqNeed = make(chan struct{})
|
||||||
"grant_type": "refresh_token",
|
client.ticker = time.NewTicker(
|
||||||
"refresh_token": api.options.RefreshToken,
|
time.Second/time.Duration(rps),
|
||||||
"redirect_uri": api.options.Url,
|
)
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <- client.reqNeed :
|
||||||
|
client.req <- struct{}{}
|
||||||
|
}
|
||||||
|
<-client.ticker.C
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) waitInQueue() bool {
|
||||||
|
// Just leaving if MRPS is not set.
|
||||||
|
if client.mrps == 0 {
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
err := api.doRequest("/oauth2/access_token", requestOptions{
|
client.reqNeed <- struct{}{}
|
||||||
HttpMethod: http.MethodPost,
|
<- client.req
|
||||||
Body: request,
|
|
||||||
Headers: map[string]string{
|
|
||||||
"Accept": "application/json",
|
|
||||||
"Cache-Control": "no-cache",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}, result)
|
|
||||||
|
|
||||||
return result, err
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *Client) Get(resource string, result interface{}) error {
|
func (client *Client) finishRequest() {
|
||||||
return api.doRequest(resource, requestOptions{
|
|
||||||
HttpMethod: http.MethodGet,
|
|
||||||
Body: nil,
|
|
||||||
Headers: map[string]string{
|
|
||||||
"Accept": "application/json",
|
|
||||||
"Cache-Control": "no-cache",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": fmt.Sprintf("Bearer %s", api.options.AccessToken),
|
|
||||||
},
|
|
||||||
}, result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *Client) Post(resource string, request interface{}, result interface{}) error {
|
func (client *Client) RequestsMade() int64 {
|
||||||
return api.doRequest(resource, requestOptions{
|
return client.requestsMade
|
||||||
HttpMethod: http.MethodPost,
|
|
||||||
Body: request,
|
|
||||||
Headers: map[string]string{
|
|
||||||
"Accept": "application/json",
|
|
||||||
"Cache-Control": "no-cache",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": fmt.Sprintf("Bearer %s", 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: map[string]string{
|
|
||||||
"Accept": "application/json",
|
|
||||||
"Cache-Control": "no-cache",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"Authorization": fmt.Sprintf("Bearer %s", api.options.AccessToken),
|
|
||||||
},
|
|
||||||
}, result)
|
|
||||||
}
|
|
||||||
|
|
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)
|
||||||
|
}
|
26
api/errors.go
Normal file
26
api/errors.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
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 NewErrorAPI(err error) error {
|
||||||
|
return fmt.Errorf("NewAPI: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRequestError(err error) error {
|
||||||
|
return fmt.Errorf("RequestError: %w", err)
|
||||||
|
}
|
||||||
|
|
169
api/request.go
Normal file
169
api/request.go
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
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
|
||||||
|
)
|
||||||
|
client.requestsMade++
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
if client.waitInQueue() {
|
||||||
|
defer client.finishRequest()
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
3
btest.sh
Executable file
3
btest.sh
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
#
|
||||||
|
go build -o ./exe/ ./cmd/test
|
4
build.sh
Executable file
4
build.sh
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
go build -o ./exe/ ./cmd/amocli/
|
||||||
|
|
202
cmd/amocli/common.go
Normal file
202
cmd/amocli/common.go
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "surdeus.su/core/cli/mtool"
|
||||||
|
import "surdeus.su/core/amo"
|
||||||
|
import "sync"
|
||||||
|
import "os"
|
||||||
|
import "strconv"
|
||||||
|
import "log"
|
||||||
|
import "bufio"
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
type DefaultFlags struct {
|
||||||
|
SecretPath string
|
||||||
|
Threads int
|
||||||
|
Verbose bool
|
||||||
|
MRPS int
|
||||||
|
MEPR int
|
||||||
|
|
||||||
|
All bool
|
||||||
|
StartPage int
|
||||||
|
EndPage int
|
||||||
|
|
||||||
|
Indent bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeDefaultFlags(opts *DefaultFlags, flags *mtool.Flags) {
|
||||||
|
flags.StringVar(
|
||||||
|
&opts.SecretPath,
|
||||||
|
"secret",
|
||||||
|
"secret.json",
|
||||||
|
"path to JSON file with AMO CRM secrets",
|
||||||
|
"AMO_SECRET",
|
||||||
|
)
|
||||||
|
flags.IntVar(
|
||||||
|
&opts.MRPS,
|
||||||
|
"mrps",
|
||||||
|
9,
|
||||||
|
"maximum requests per second",
|
||||||
|
)
|
||||||
|
flags.IntVar(
|
||||||
|
&opts.Threads,
|
||||||
|
"threads",
|
||||||
|
5,
|
||||||
|
"amount of threads to run the requests",
|
||||||
|
)
|
||||||
|
flags.BoolVar(
|
||||||
|
&opts.Verbose,
|
||||||
|
"no-verbose",
|
||||||
|
true,
|
||||||
|
"disable verbose mode",
|
||||||
|
)
|
||||||
|
flags.IntVar(
|
||||||
|
&opts.MEPR,
|
||||||
|
"mepr",
|
||||||
|
amo.MEPR,
|
||||||
|
"max entities per request",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func MakeGetterFlags(
|
||||||
|
opts *DefaultFlags,
|
||||||
|
flags *mtool.Flags,
|
||||||
|
) {
|
||||||
|
flags.BoolVar(
|
||||||
|
&opts.All,
|
||||||
|
"all",
|
||||||
|
false,
|
||||||
|
"get all entities",
|
||||||
|
)
|
||||||
|
flags.IntVar(
|
||||||
|
&opts.StartPage,
|
||||||
|
"startpage",
|
||||||
|
1,
|
||||||
|
"the page to start at (works only with -all)",
|
||||||
|
)
|
||||||
|
flags.IntVar(
|
||||||
|
&opts.EndPage,
|
||||||
|
"endpage",
|
||||||
|
math.MaxInt,
|
||||||
|
"the page to end the requests",
|
||||||
|
)
|
||||||
|
flags.BoolVar(
|
||||||
|
&opts.Indent,
|
||||||
|
"no-indent",
|
||||||
|
true,
|
||||||
|
"disable indenting for JSON output",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run function for slice's parts in different threads.
|
||||||
|
// Returns a channel that ticks that the threads finished.
|
||||||
|
func RunForSliceInThreads[V any](
|
||||||
|
threadN, mepr int, // Thread amount and MERP.
|
||||||
|
slice []V,
|
||||||
|
// The function that takes
|
||||||
|
// the thread number and the slice of slice.
|
||||||
|
fn func(int, []V),
|
||||||
|
) (chan struct{}) {
|
||||||
|
ret := make(chan struct{})
|
||||||
|
|
||||||
|
// No need for threads if
|
||||||
|
// there are so few entities.
|
||||||
|
if len(slice) <= mepr {
|
||||||
|
go func() {
|
||||||
|
fn(1, slice)
|
||||||
|
ret <- struct{}{}
|
||||||
|
}()
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
runFn :=func(thread int, s []V) {
|
||||||
|
defer wg.Done()
|
||||||
|
iterN := (len(s) / mepr) + 1
|
||||||
|
for j := 0 ; j<iterN ; j++ {
|
||||||
|
start := j*mepr
|
||||||
|
end := start + mepr
|
||||||
|
if end > len(s) {
|
||||||
|
end = len(s)
|
||||||
|
}
|
||||||
|
if len(s[start:end]) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fn(thread, s[start:end])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Maximizing speed on small data.
|
||||||
|
|
||||||
|
|
||||||
|
//threadSize := len(slice) / threadN
|
||||||
|
threadMeprRest := len(slice) % mepr
|
||||||
|
//threadSize := (len(slice)-threadMeprRest)/threadN
|
||||||
|
preThreadSize :=
|
||||||
|
((len(slice)-threadMeprRest)/threadN)
|
||||||
|
threadSize := (preThreadSize/mepr+1)*mepr
|
||||||
|
|
||||||
|
if threadSize < mepr {
|
||||||
|
threadSize = mepr
|
||||||
|
threadN = len(slice) / mepr
|
||||||
|
runFn = func(thread int, s []V) {
|
||||||
|
defer wg.Done()
|
||||||
|
fn(thread, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0 ; i<threadN ; i++ {
|
||||||
|
first := i * threadSize
|
||||||
|
last := first + threadSize
|
||||||
|
if last > len(slice) {
|
||||||
|
last = len(slice)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Got an empty slice.
|
||||||
|
if len(slice[first:last]) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Add(1)
|
||||||
|
go runFn(i+1, slice[first:last])
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
ret <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadIDs(idStrs []string) []int {
|
||||||
|
var ids []int
|
||||||
|
if len(idStrs) > 0 {
|
||||||
|
ids = make([]int, 0, len(idStrs))
|
||||||
|
for _, idStr := range idStrs {
|
||||||
|
id, err := strconv.Atoi(idStr)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error: Atoi(%q): %s\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ids = make([]int, 0)
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
for scanner.Scan() {
|
||||||
|
txt := scanner.Text()
|
||||||
|
|
||||||
|
id, err := strconv.Atoi(txt)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(
|
||||||
|
"strconv.Atoi(%q): %s\n",
|
||||||
|
txt, err,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
34
cmd/amocli/getcom.go
Normal file
34
cmd/amocli/getcom.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
type CompanyGetter struct{}
|
||||||
|
|
||||||
|
func (l CompanyGetter) GetValues(
|
||||||
|
c *amo.Client,
|
||||||
|
opts ...urlenc.Builder,
|
||||||
|
) ([]amo.Company, amo.NextFunc[[]amo.Company], error) {
|
||||||
|
return c.GetCompanies(opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l CompanyGetter) GetNameMul() string {
|
||||||
|
return "companies"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l CompanyGetter) GetFuncName() string {
|
||||||
|
return "amo.GetCompanies"
|
||||||
|
}
|
||||||
|
|
||||||
|
var getComs = mtool.T("get-companies").Func(func(
|
||||||
|
flags *mtool.Flags,
|
||||||
|
){
|
||||||
|
RunGetter(CompanyGetter{}, flags)
|
||||||
|
})
|
26
cmd/amocli/getcontact.go
Normal file
26
cmd/amocli/getcontact.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "surdeus.su/core/amo"
|
||||||
|
import "surdeus.su/core/ss/urlenc"
|
||||||
|
import "surdeus.su/core/cli/mtool"
|
||||||
|
|
||||||
|
type ContactGetter struct{}
|
||||||
|
func (l ContactGetter) GetValues(
|
||||||
|
c *amo.Client,
|
||||||
|
opts ...urlenc.Builder,
|
||||||
|
) ([]amo.Contact, amo.NextFunc[[]amo.Contact], error) {
|
||||||
|
return c.GetContacts(opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l ContactGetter) GetNameMul() string {
|
||||||
|
return "companies"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l ContactGetter) GetFuncName() string {
|
||||||
|
return "amo.GetCompanies"
|
||||||
|
}
|
||||||
|
|
||||||
|
var getContacts = mtool.T("get-contacts").
|
||||||
|
Func(func(flags *mtool.Flags){
|
||||||
|
RunGetter(ContactGetter{}, flags)
|
||||||
|
})
|
29
cmd/amocli/getlead.go
Normal file
29
cmd/amocli/getlead.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "surdeus.su/core/cli/mtool"
|
||||||
|
import "surdeus.su/core/amo"
|
||||||
|
import "surdeus.su/core/ss/urlenc"
|
||||||
|
|
||||||
|
type LeadGetter struct {}
|
||||||
|
|
||||||
|
func (l LeadGetter) GetValues(
|
||||||
|
c *amo.Client,
|
||||||
|
opts ...urlenc.Builder,
|
||||||
|
) ([]amo.Lead, amo.NextFunc[[]amo.Lead], error) {
|
||||||
|
return c.GetLeads(opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LeadGetter) GetNameMul() string {
|
||||||
|
return "leads"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LeadGetter) GetFuncName() string {
|
||||||
|
return "amo.GetLeads"
|
||||||
|
}
|
||||||
|
var getLead = mtool.T("get-leads").Func(func(
|
||||||
|
flags *mtool.Flags,
|
||||||
|
){
|
||||||
|
RunGetter(LeadGetter{}, flags)
|
||||||
|
}).Usage(
|
||||||
|
"[id1 id2 ... idN]",
|
||||||
|
)
|
33
cmd/amocli/getleadtup.go
Normal file
33
cmd/amocli/getleadtup.go
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "surdeus.su/core/cli/mtool"
|
||||||
|
import "surdeus.su/core/amo"
|
||||||
|
import "surdeus.su/core/ss/urlenc"
|
||||||
|
|
||||||
|
type LeadTupleGetter struct{}
|
||||||
|
func (l LeadTupleGetter) GetValues(
|
||||||
|
c *amo.Client,
|
||||||
|
opts ...urlenc.Builder,
|
||||||
|
) (
|
||||||
|
[]amo.LeadTuple,
|
||||||
|
amo.NextFunc[[]amo.LeadTuple],
|
||||||
|
error,
|
||||||
|
) {
|
||||||
|
return c.GetLeadTuples(opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LeadTupleGetter) GetNameMul() string {
|
||||||
|
return "lead tuples"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l LeadTupleGetter) GetFuncName() string {
|
||||||
|
return "amo.GetLeadTuples"
|
||||||
|
}
|
||||||
|
|
||||||
|
var getLeadTuple = mtool.T("get-lead-tuples").Func(func(
|
||||||
|
flags *mtool.Flags,
|
||||||
|
){
|
||||||
|
RunGetter(LeadTupleGetter{}, flags)
|
||||||
|
}).Usage(
|
||||||
|
"[id1 id2 ... idN]",
|
||||||
|
)
|
188
cmd/amocli/getter.go
Normal file
188
cmd/amocli/getter.go
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "surdeus.su/core/amo"
|
||||||
|
import "surdeus.su/core/ss/urlenc"
|
||||||
|
|
||||||
|
import "surdeus.su/core/cli/mtool"
|
||||||
|
|
||||||
|
//import "fmt"
|
||||||
|
import "log"
|
||||||
|
import "time"
|
||||||
|
import "os"
|
||||||
|
import "os/signal"
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
type Getter[V any] interface {
|
||||||
|
GetValues(*amo.Client, ...urlenc.Builder) ([]V, amo.NextFunc[[]V], error)
|
||||||
|
GetNameMul() string
|
||||||
|
GetFuncName() string
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunGetter[V any, G Getter[V]](g G, flags *mtool.Flags) {
|
||||||
|
var (
|
||||||
|
opts DefaultFlags
|
||||||
|
)
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
MakeDefaultFlags(&opts, flags)
|
||||||
|
MakeGetterFlags(&opts, flags)
|
||||||
|
|
||||||
|
idStrs := flags.Parse()
|
||||||
|
c, err := amo.NewClient(opts.SecretPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("NewAmoClient(...): %s\n", err)
|
||||||
|
}
|
||||||
|
c.API.SetMRPS(opts.MRPS)
|
||||||
|
enc := json.NewEncoder(os.Stdout)
|
||||||
|
if opts.Indent {
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
inter := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(inter, os.Interrupt)
|
||||||
|
go func() {
|
||||||
|
<-inter
|
||||||
|
os.Stdout.Sync()
|
||||||
|
os.Exit(0)
|
||||||
|
}()
|
||||||
|
|
||||||
|
finalNum := 0
|
||||||
|
if opts.All {
|
||||||
|
page := opts.StartPage
|
||||||
|
values, next, err := g.GetValues(
|
||||||
|
c,
|
||||||
|
urlenc.Value[int]{"page", page},
|
||||||
|
urlenc.Value[int]{"limit", opts.MEPR},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf(
|
||||||
|
"%s(...): %s\n", g.GetFuncName(), err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
finalNum += len(values)
|
||||||
|
|
||||||
|
if opts.Verbose {
|
||||||
|
log.Printf("Got %d %s (%d, page %d)\n",
|
||||||
|
len(values), g.GetNameMul(), finalNum, page)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, value := range values {
|
||||||
|
err := enc.Encode(value)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("json.Encode(...): %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
page++
|
||||||
|
for page <= opts.EndPage && next != nil {
|
||||||
|
values, next, err = next()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf(
|
||||||
|
"%s(...): %s\n",
|
||||||
|
g.GetFuncName(), err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
finalNum += len(values)
|
||||||
|
if opts.Verbose {
|
||||||
|
log.Printf("Got %d %s (%d, page %d)\n",
|
||||||
|
len(values), g.GetNameMul(),
|
||||||
|
finalNum, page)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, value := range values {
|
||||||
|
err = enc.Encode(value)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("json.Encode(...): %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
page++
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ids := ReadIDs(idStrs)
|
||||||
|
if len(ids) == 0 {
|
||||||
|
log.Fatalf("Got no IDs to read %s", g.GetNameMul())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
valueChan := make(chan []V)
|
||||||
|
finish := RunForSliceInThreads[int](
|
||||||
|
opts.Threads, opts.MEPR,
|
||||||
|
ids, func(thread int, s []int){
|
||||||
|
values, _, err := g.GetValues(
|
||||||
|
c,
|
||||||
|
urlenc.Array[int]{
|
||||||
|
"id",
|
||||||
|
s,
|
||||||
|
},
|
||||||
|
urlenc.Value[string]{
|
||||||
|
"with",
|
||||||
|
"contacts",
|
||||||
|
},
|
||||||
|
urlenc.Value[int]{
|
||||||
|
"limit",
|
||||||
|
opts.MEPR,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("thread(%d): %s(...): %s\n",
|
||||||
|
thread, g.GetFuncName(), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
valueChan <- values
|
||||||
|
if opts.Verbose {
|
||||||
|
log.Printf(
|
||||||
|
"thread(%d): Got %d %s\n",
|
||||||
|
thread, len(values),
|
||||||
|
g.GetNameMul(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
//var wg sync.WaitGroup
|
||||||
|
go func(){
|
||||||
|
// Waiting for appending so we do not lose data.
|
||||||
|
<-finish
|
||||||
|
for len(valueChan) > 0 {}
|
||||||
|
close(valueChan)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for values := range valueChan {
|
||||||
|
finalNum += len(values)
|
||||||
|
for _, value := range values {
|
||||||
|
err := enc.Encode(value)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("json.Encode(...): %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.Verbose {
|
||||||
|
log.Printf(
|
||||||
|
"thtread(main) %.2f%%, sument=%d",
|
||||||
|
float32(finalNum)/float32(len(ids))*100.,
|
||||||
|
finalNum,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.Verbose {
|
||||||
|
rm := c.API.RequestsMade()
|
||||||
|
log.Printf(
|
||||||
|
"Summarized got %d %s\n",
|
||||||
|
finalNum, g.GetNameMul(),
|
||||||
|
)
|
||||||
|
log.Printf(
|
||||||
|
"Made %d requests in process\n",
|
||||||
|
rm,
|
||||||
|
)
|
||||||
|
took := time.Since(now).Seconds()
|
||||||
|
log.Printf(
|
||||||
|
"Took %f seconds\n",
|
||||||
|
took,
|
||||||
|
)
|
||||||
|
log.Printf("RPS = %f\n", float64(rm)/took)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
32
cmd/amocli/main.go
Normal file
32
cmd/amocli/main.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "surdeus.su/core/cli/mtool"
|
||||||
|
import "runtime/debug"
|
||||||
|
import "os"
|
||||||
|
import "log"
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
tool.Run(os.Args[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
var tool = mtool.T("amocli").Subs(
|
||||||
|
getLead,
|
||||||
|
|
||||||
|
getComs,
|
||||||
|
updateComs,
|
||||||
|
|
||||||
|
getContacts,
|
||||||
|
getLeadTuple,
|
||||||
|
|
||||||
|
mtool.T("version").Func(func(flags *mtool.Flags){
|
||||||
|
bi, ok := debug.ReadBuildInfo()
|
||||||
|
if !ok {
|
||||||
|
log.Fatalf("could not read build info\n")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(bi.Main.Version)
|
||||||
|
}).Desc(
|
||||||
|
"print program version",
|
||||||
|
),
|
||||||
|
)
|
30
cmd/amocli/updatecom.go
Normal file
30
cmd/amocli/updatecom.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "surdeus.su/core/amo"
|
||||||
|
//import "surdeus.su/core/amo/api"
|
||||||
|
import "surdeus.su/core/cli/mtool"
|
||||||
|
//import "surdeus.su/core/amo/companies"
|
||||||
|
//import "encoding/json"
|
||||||
|
//import "log"
|
||||||
|
//import "os"
|
||||||
|
|
||||||
|
type CompanyUpdater struct{}
|
||||||
|
|
||||||
|
func (u CompanyUpdater) UpdateValues(
|
||||||
|
c *amo.Client, cs []amo.Company,
|
||||||
|
) ([]amo.Company, error) {
|
||||||
|
return c.UpdateCompanies(cs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u CompanyUpdater) GetNameMul() string {
|
||||||
|
return "companies"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u CompanyUpdater) GetFuncName() string {
|
||||||
|
return "amo.UpdateCompanies"
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateComs =
|
||||||
|
mtool.T("update-companies").Func(func(flags *mtool.Flags){
|
||||||
|
RunUpdater(CompanyUpdater{}, flags)
|
||||||
|
})
|
76
cmd/amocli/updater.go
Normal file
76
cmd/amocli/updater.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
//import "surdeus.su/core/ss/u"
|
||||||
|
import "surdeus.su/core/amo"
|
||||||
|
import "surdeus.su/core/cli/mtool"
|
||||||
|
import "encoding/json"
|
||||||
|
import "os"
|
||||||
|
import "log"
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Updater[V, VR any] interface {
|
||||||
|
UpdateValues(*amo.Client, []V) (VR, error)
|
||||||
|
GetNameMul() string
|
||||||
|
GetFuncName() string
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunUpdater[V, VR any, U Updater[V, VR]](
|
||||||
|
u U, flags *mtool.Flags,
|
||||||
|
) {
|
||||||
|
now := time.Now()
|
||||||
|
var (
|
||||||
|
opts DefaultFlags
|
||||||
|
)
|
||||||
|
MakeDefaultFlags(&opts, flags)
|
||||||
|
flags.Parse()
|
||||||
|
|
||||||
|
client, err := amo.NewClient(opts.SecretPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("NewAmoClient(...): %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
values := []V{}
|
||||||
|
dec := json.NewDecoder(os.Stdin)
|
||||||
|
err = dec.Decode(&values)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("json.Decode(...): %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
finish := RunForSliceInThreads[V](
|
||||||
|
opts.Threads, opts.MEPR,
|
||||||
|
values, func(thread int, s []V){
|
||||||
|
_, err := u.UpdateValues(client, s)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(
|
||||||
|
"thread(%d): %s(...): %s\n",
|
||||||
|
thread, u.GetFuncName(), err,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if opts.Verbose {
|
||||||
|
log.Printf(
|
||||||
|
"thread(%d): Updated %d %s\n",
|
||||||
|
thread, len(s), u.GetNameMul(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
<-finish
|
||||||
|
if opts.Verbose {
|
||||||
|
rm := client.API.RequestsMade()
|
||||||
|
/*log.Printf(
|
||||||
|
"Summarized got %d %s\n",
|
||||||
|
len(finalValues), g.GetNameMul(),
|
||||||
|
)*/
|
||||||
|
log.Printf(
|
||||||
|
"Made %d requests in process\n",
|
||||||
|
rm,
|
||||||
|
)
|
||||||
|
took := time.Since(now).Seconds()
|
||||||
|
log.Printf(
|
||||||
|
"Took %f seconds\n",
|
||||||
|
took,
|
||||||
|
)
|
||||||
|
log.Printf("RPS = %f\n", float64(rm)/took)
|
||||||
|
}
|
||||||
|
}
|
59
cmd/test/main.go
Normal file
59
cmd/test/main.go
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"surdeus.su/core/amo"
|
||||||
|
"surdeus.su/core/amo/api"
|
||||||
|
//"surdeus.su/core/amo/webhooks"
|
||||||
|
"surdeus.su/core/amo/events"
|
||||||
|
//"surdeus.su/core/ss"
|
||||||
|
//"surdeus.su/core/ss/statuses"
|
||||||
|
//"os"
|
||||||
|
"fmt"
|
||||||
|
//"io"
|
||||||
|
//"encoding/json"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
//fmt.Println(opts)
|
||||||
|
client, err := amo.NewAmoClient("secret.json")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
client.Api.Debug = true
|
||||||
|
|
||||||
|
company, err := client.GetCompany(80828925, "")
|
||||||
|
if err != nil && err != api.NoContentErr {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
45
common/common.go
Normal file
45
common/common.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"regexp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Value struct {
|
||||||
|
Value any `json:"value,omitempty"`
|
||||||
|
EnumID int `json:"enum_id,omitempty"`
|
||||||
|
Enum string `json:"enum,omitempty"`
|
||||||
|
}
|
||||||
|
type CustomFieldsValue struct {
|
||||||
|
FieldID int `json:"field_id"`
|
||||||
|
FieldName string `json:"field_name,omitempty"`
|
||||||
|
FieldCode string `json:"field_code,omitempty"`
|
||||||
|
FieldType string `json:"field_type,omitempty"`
|
||||||
|
Values []Value `json:"values"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomFieldsValues []CustomFieldsValue
|
||||||
|
|
||||||
|
func (vs CustomFieldsValues) GetByID(
|
||||||
|
id int,
|
||||||
|
) (CustomFieldsValue, bool) {
|
||||||
|
for _, v := range vs {
|
||||||
|
if v.FieldID == id {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CustomFieldsValue{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fields CustomFieldsValues) FilterByNameRegex(
|
||||||
|
re *regexp.Regexp,
|
||||||
|
) CustomFieldsValues {
|
||||||
|
ret := CustomFieldsValues{}
|
||||||
|
for _, field := range fields {
|
||||||
|
if re.MatchString(field.FieldName) {
|
||||||
|
ret = append(ret, field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
|
92
companies.go
Normal file
92
companies.go
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
package amo
|
||||||
|
|
||||||
|
import "surdeus.su/core/ss/urlenc"
|
||||||
|
import "surdeus.su/core/amo/companies"
|
||||||
|
import "surdeus.su/core/amo/api"
|
||||||
|
import "errors"
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func (client *Client) GetCompany(
|
||||||
|
companyID int,
|
||||||
|
opts ...urlenc.Builder,
|
||||||
|
) (*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
|
||||||
|
}
|
||||||
|
// Get list of leads.
|
||||||
|
func (client *Client) GetCompanies(
|
||||||
|
opts ...urlenc.Builder,
|
||||||
|
) ([]Company, NextFunc[[]Company], error) {
|
||||||
|
res := fmt.Sprintf(
|
||||||
|
"/api/v4/companies?%s",
|
||||||
|
urlenc.Join(opts...).Encode(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return client.GetCompaniesByURL(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) GetCompaniesByURL(
|
||||||
|
u string,
|
||||||
|
) ([]Company, NextFunc[[]Company], error) {
|
||||||
|
var fn NextFunc[[]Company]
|
||||||
|
|
||||||
|
coms := companies.Companies{}
|
||||||
|
err := client.API.Get(u, &coms)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, api.ErrNoContent) {
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//ret = append(ret, coms.Embedded.Companies...)
|
||||||
|
|
||||||
|
nextHref := coms.Links.Next.Href
|
||||||
|
if nextHref != "" {
|
||||||
|
fn = MakeNextFunc(
|
||||||
|
nextHref,
|
||||||
|
client.GetCompaniesByURL,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return coms.Embedded.Companies, fn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) UpdateCompany(
|
||||||
|
company *companies.Company,
|
||||||
|
) error {
|
||||||
|
return client.updateEntity(
|
||||||
|
"/api/v4/companies",
|
||||||
|
company.ID,
|
||||||
|
company,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) UpdateCompanies(
|
||||||
|
cs []companies.Company,
|
||||||
|
) ([]Company, error) {
|
||||||
|
if len(cs) > MEPR {
|
||||||
|
return nil, ErrTooManyEntities
|
||||||
|
}
|
||||||
|
|
||||||
|
//ret := []Company{}
|
||||||
|
err := client.API.Patch(
|
||||||
|
"/api/v4/companies",
|
||||||
|
cs,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
51
companies/companies.go
Normal file
51
companies/companies.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package companies
|
||||||
|
|
||||||
|
import "surdeus.su/core/amo/common"
|
||||||
|
|
||||||
|
type Company struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
ResponsibleUserID int `json:"responsible_user_id,omitempty"`
|
||||||
|
GroupID int `json:"group_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"`
|
||||||
|
ClosestTaskAt interface{} `json:"closest_task_at,omitempty"`
|
||||||
|
CustomFieldsValues common.CustomFieldsValues `json:"custom_fields_values,omitempty"`
|
||||||
|
AccountID int `json:"account_id,omitempty"`
|
||||||
|
Links Links `json:"_links,omitempty"`
|
||||||
|
Embedded Embedded `json:"_embedded,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Self struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Links struct {
|
||||||
|
Self Self `json:"self"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Contacts struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Embedded struct {
|
||||||
|
Tags []interface{} `json:"tags"`
|
||||||
|
Contacts []*Contacts `json:"contacts"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Companies 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 {
|
||||||
|
Companies []Company `json:"companies"`
|
||||||
|
} `json:"_embedded"`
|
||||||
|
}
|
72
contacts.go
Normal file
72
contacts.go
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
package amo
|
||||||
|
|
||||||
|
import "surdeus.su/core/amo/contacts"
|
||||||
|
import "surdeus.su/core/ss/urlenc"
|
||||||
|
import "errors"
|
||||||
|
import "surdeus.su/core/amo/api"
|
||||||
|
import "fmt"
|
||||||
|
//import "log"
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
// Get list of contacts.
|
||||||
|
func (client *Client) GetContacts(
|
||||||
|
opts ...urlenc.Builder,
|
||||||
|
) ([]contacts.Contact, NextFunc[[]Contact], error) {
|
||||||
|
res := fmt.Sprintf(
|
||||||
|
"/api/v4/contacts?%s",
|
||||||
|
urlenc.Join(opts...).Encode(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return client.GetContactsByURL(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) GetContactsByURL(
|
||||||
|
u string,
|
||||||
|
) ([]Contact, NextFunc[[]Contact], error) {
|
||||||
|
var fn NextFunc[[]Contact]
|
||||||
|
|
||||||
|
contacts := contacts.Contacts{}
|
||||||
|
err := client.API.Get(u, &contacts)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, api.ErrNoContent) {
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
//ret = append(ret, coms.Embedded.Companies...)
|
||||||
|
|
||||||
|
nextHref := contacts.Links.Next.Href
|
||||||
|
if nextHref != "" {
|
||||||
|
fn = MakeNextFunc(
|
||||||
|
nextHref,
|
||||||
|
client.GetContactsByURL,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return contacts.Embedded.Contacts, fn, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) UpdateContact(contact *contacts.Contact) error {
|
||||||
|
return client.updateEntity(
|
||||||
|
"/api/v4/contacts",
|
||||||
|
contact.ID,
|
||||||
|
contact,
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
73
contacts/contacts.go
Normal file
73
contacts/contacts.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
package contacts
|
||||||
|
|
||||||
|
import "surdeus.su/core/amo/common"
|
||||||
|
|
||||||
|
type Contact struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
FirstName string `json:"first_name,omitempty"`
|
||||||
|
LastName string `json:"last_name,omitempty"`
|
||||||
|
ResponsibleUserID int `json:"responsible_user_id,omitempty"`
|
||||||
|
GroupID int `json:"group_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"`
|
||||||
|
ClosestTaskAt interface{} `json:"closest_task_at,omitempty"`
|
||||||
|
CustomFieldsValues common.CustomFieldsValues `json:"custom_fields_values,omitempty"`
|
||||||
|
AccountID int `json:"account_id,omitempty"`
|
||||||
|
Links Links `json:"_links,omitempty"`
|
||||||
|
Embedded Embedded `json:"_embedded,omitempty"`
|
||||||
|
}
|
||||||
|
type Values struct {
|
||||||
|
Value string `json:"value"`
|
||||||
|
EnumID int `json:"enum_id"`
|
||||||
|
Enum string `json:"enum"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Self struct {
|
||||||
|
Href string `json:"href"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Links struct {
|
||||||
|
Self Self `json:"self"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Leads struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Links Links `json:"_links"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Customers struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Links Links `json:"_links"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Company struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Links Links `json:"_links"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Embedded struct {
|
||||||
|
Tags []interface{} `json:"tags"`
|
||||||
|
Leads []Leads `json:"leads"`
|
||||||
|
Customers []Customers `json:"customers"`
|
||||||
|
CatalogElements []interface{} `json:"catalog_elements"`
|
||||||
|
Companies []Company `json:"companies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Contacts 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 {
|
||||||
|
Contacts []Contact `json:"contacts"`
|
||||||
|
} `json:"_embedded"`
|
||||||
|
}
|
||||||
|
|
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
|
||||||
|
}
|
5
env.sh
Normal file
5
env.sh
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
lines=$(cat .env)
|
||||||
|
for line in $lines ; do
|
||||||
|
echo $line
|
||||||
|
eval "export $line"
|
||||||
|
done
|
7
errors.go
Normal file
7
errors.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package amo
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrTooManyEntities = errors.New("too many entities")
|
||||||
|
)
|
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
|
||||||
|
}
|
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
|
||||||
|
}
|
||||||
|
|
15
go.mod
15
go.mod
|
@ -1,8 +1,15 @@
|
||||||
module github.com/qdimka/go-amo
|
module surdeus.su/core/amo
|
||||||
|
|
||||||
go 1.15
|
go 1.21.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gorilla/schema v1.2.0
|
github.com/stretchr/testify v1.9.0
|
||||||
github.com/stretchr/testify v1.6.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
|
||||||
)
|
)
|
||||||
|
|
25
go.sum
25
go.sum
|
@ -1,13 +1,20 @@
|
||||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
|
|
||||||
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
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.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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/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=
|
||||||
|
|
3
install.sh
Executable file
3
install.sh
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
go install ./cmd/amocli/
|
143
lead-tuple.go
Normal file
143
lead-tuple.go
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
package amo
|
||||||
|
|
||||||
|
import "surdeus.su/core/ss/urlenc"
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// The type describes tuple
|
||||||
|
// contaning everything related
|
||||||
|
// to THE lead.
|
||||||
|
type LeadTuple struct {
|
||||||
|
Lead *Lead `json:"lead,omitempty"`
|
||||||
|
MainContact *Contact `json:"main_contact,omitempty"`
|
||||||
|
Company *Company `json:"company,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) GetLeadTuples(
|
||||||
|
opts ...urlenc.Builder,
|
||||||
|
) ([]LeadTuple, NextFunc[[]LeadTuple], error) {
|
||||||
|
opts = append(
|
||||||
|
opts, urlenc.Value[string]{"with", "contacts"},
|
||||||
|
)
|
||||||
|
res := fmt.Sprintf(
|
||||||
|
"/api/v4/leads?%s",
|
||||||
|
urlenc.Join(opts...).Encode(),
|
||||||
|
)
|
||||||
|
return client.GetLeadTuplesByURL(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) GetLeadTuplesByURL(
|
||||||
|
u string,
|
||||||
|
) ([]LeadTuple, NextFunc[[]LeadTuple], error) {
|
||||||
|
return client.getLeadTuplesByFuncURL(
|
||||||
|
func() ([]Lead, NextFunc[[]Lead], error){
|
||||||
|
return client.GetLeadsByURL(u)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) getLeadTuplesByFuncURL(
|
||||||
|
// The function describes way of getting leads.
|
||||||
|
callback func() (
|
||||||
|
[]Lead,
|
||||||
|
NextFunc[[]Lead],
|
||||||
|
error,
|
||||||
|
),
|
||||||
|
) ([]LeadTuple, NextFunc[[]LeadTuple], error) {
|
||||||
|
var fn NextFunc[[]LeadTuple]
|
||||||
|
leads, next, err := callback()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("amo.GetLeads(...): %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
comIDs := []int{}
|
||||||
|
contactIDs := []int{}
|
||||||
|
leadToCom := map[int]int{}
|
||||||
|
leadToContact := map[int]int{}
|
||||||
|
|
||||||
|
for _, lead := range leads {
|
||||||
|
coms := lead.Embedded.Companies
|
||||||
|
if len(coms) > 0 {
|
||||||
|
com := coms[0]
|
||||||
|
leadToCom[lead.ID] = com.ID
|
||||||
|
comIDs = append(comIDs, com.ID)
|
||||||
|
}
|
||||||
|
mainContact, hasMain := lead.Embedded.
|
||||||
|
Contacts.GetMain()
|
||||||
|
if hasMain {
|
||||||
|
leadToContact[lead.ID] = mainContact.ID
|
||||||
|
contactIDs = append(contactIDs, mainContact.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
comMap := map[int] *Company{}
|
||||||
|
var coms []Company
|
||||||
|
if len(comIDs) > 0 {
|
||||||
|
coms, _, err = client.GetCompanies(
|
||||||
|
urlenc.Array[int]{
|
||||||
|
"id",
|
||||||
|
comIDs,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf(
|
||||||
|
"amo.GetCompanies(...): %s\n", err)
|
||||||
|
}
|
||||||
|
for i := range coms {
|
||||||
|
comMap[coms[i].ID] = &coms[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
contactMap := map[int] *Contact{}
|
||||||
|
var contacts []Contact
|
||||||
|
if len(contactIDs) > 0 {
|
||||||
|
contacts, _, err = client.GetContacts(
|
||||||
|
urlenc.Array[int]{
|
||||||
|
"id",
|
||||||
|
contactIDs,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf(
|
||||||
|
"client.GetContacts(...): %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range contacts {
|
||||||
|
contactMap[contacts[i].ID] = &contacts[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := []LeadTuple{}
|
||||||
|
for i := range leads {
|
||||||
|
leadID := leads[i].ID
|
||||||
|
tup := LeadTuple{
|
||||||
|
Lead: &leads[i],
|
||||||
|
}
|
||||||
|
contactID, ok := leadToContact[leadID]
|
||||||
|
if ok {
|
||||||
|
contact, ok := contactMap[contactID]
|
||||||
|
if ok {
|
||||||
|
tup.MainContact = contact
|
||||||
|
}
|
||||||
|
}
|
||||||
|
companyID, ok := leadToCom[leadID]
|
||||||
|
if ok {
|
||||||
|
company, ok := comMap[companyID]
|
||||||
|
if ok {
|
||||||
|
tup.Company = company
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret = append(ret, tup)
|
||||||
|
}
|
||||||
|
|
||||||
|
if next != nil {
|
||||||
|
fn = NextFunc[[]LeadTuple](
|
||||||
|
func() ([]LeadTuple, NextFunc[[]LeadTuple], error) {
|
||||||
|
return client.getLeadTuplesByFuncURL(
|
||||||
|
next,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, fn, nil
|
||||||
|
|
||||||
|
}
|
71
leads.go
Normal file
71
leads.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package amo
|
||||||
|
|
||||||
|
import "surdeus.su/core/amo/api"
|
||||||
|
import "surdeus.su/core/amo/leads"
|
||||||
|
import "surdeus.su/core/ss/urlenc"
|
||||||
|
import "errors"
|
||||||
|
import "fmt"
|
||||||
|
//import "log"
|
||||||
|
|
||||||
|
// Get list of leads.
|
||||||
|
func (client *Client) GetLeads(
|
||||||
|
opts ...urlenc.Builder,
|
||||||
|
) ([]Lead, NextFunc[[]Lead], error) {
|
||||||
|
res := fmt.Sprintf(
|
||||||
|
"/api/v4/leads?%s",
|
||||||
|
urlenc.Join(opts...).Encode(),
|
||||||
|
)
|
||||||
|
|
||||||
|
return client.GetLeadsByURL(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *Client) GetLeadsByURL(
|
||||||
|
u string,
|
||||||
|
) ([]Lead, NextFunc[[]Lead], error) {
|
||||||
|
var fn NextFunc[[]Lead]
|
||||||
|
|
||||||
|
lds := leads.Leads{}
|
||||||
|
err := client.API.Get(u, &lds)
|
||||||
|
if err != nil {
|
||||||
|
// Check for empty.
|
||||||
|
if errors.Is(err, api.ErrNoContent) {
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
// Some other error.
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nextHref := lds.Links.Next.Href
|
||||||
|
if nextHref != "" {
|
||||||
|
fn = MakeNextFunc(
|
||||||
|
nextHref,
|
||||||
|
client.GetLeadsByURL,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lds.Embedded.Leads, fn, 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"`
|
||||||
|
}
|
||||||
|
|
91
leads/leads.go
Normal file
91
leads/leads.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
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.CustomFieldsValues `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 {
|
||||||
|
Href string `json:"href"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Links struct {
|
||||||
|
Self Self `json:"self"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tags struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Metadata struct {
|
||||||
|
Quantity int `json:"quantity"`
|
||||||
|
CatalogID int `json:"catalog_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CatalogElements struct {
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Companies struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Links Links `json:"_links"`
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
type Contact struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
IsMain bool `json:"is_main"`
|
||||||
|
Links Links `json:"_links"`
|
||||||
|
}
|
||||||
|
type Contacts []Contact
|
||||||
|
|
||||||
|
func (cs Contacts) GetMain() (Contact, bool) {
|
||||||
|
for _, contact := range cs {
|
||||||
|
if contact.IsMain {
|
||||||
|
return contact, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Contact{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
type Embedded struct {
|
||||||
|
Tags []*Tags `json:"tags"`
|
||||||
|
CatalogElements []*CatalogElements `json:"catalog_elements"`
|
||||||
|
LossReason []*LossReason `json:"loss_reason"`
|
||||||
|
Companies []*Companies `json:"companies"`
|
||||||
|
Contacts Contacts `json:"contacts"`
|
||||||
|
}
|
||||||
|
|
22
license.txt
Normal file
22
license.txt
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
Copyright 2024 (c) surdeus
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge,
|
||||||
|
to any person obtaining a copy of this software and associated documentation files (the “Software”),
|
||||||
|
to deal in the Software without restriction,
|
||||||
|
including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute,
|
||||||
|
sublicense, and/or sell copies of the Software,
|
||||||
|
and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall
|
||||||
|
be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||||
|
TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
|
@ -1,38 +0,0 @@
|
||||||
package companies
|
|
||||||
|
|
||||||
type Company struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
ResponsibleUserID int `json:"responsible_user_id"`
|
|
||||||
GroupID int `json:"group_id"`
|
|
||||||
CreatedBy int `json:"created_by"`
|
|
||||||
UpdatedBy int `json:"updated_by"`
|
|
||||||
CreatedAt int `json:"created_at"`
|
|
||||||
UpdatedAt int `json:"updated_at"`
|
|
||||||
ClosestTaskAt interface{} `json:"closest_task_at"`
|
|
||||||
CustomFieldsValues []CustomFieldsValues `json:"custom_fields_values"`
|
|
||||||
AccountID int `json:"account_id"`
|
|
||||||
Links Links `json:"_links"`
|
|
||||||
Embedded Embedded `json:"_embedded"`
|
|
||||||
}
|
|
||||||
type Values struct {
|
|
||||||
Value string `json:"value"`
|
|
||||||
EnumID int `json:"enum_id"`
|
|
||||||
Enum string `json:"enum"`
|
|
||||||
}
|
|
||||||
type CustomFieldsValues struct {
|
|
||||||
FieldID int `json:"field_id"`
|
|
||||||
FieldName string `json:"field_name"`
|
|
||||||
FieldCode string `json:"field_code"`
|
|
||||||
FieldType string `json:"field_type"`
|
|
||||||
Values []Values `json:"values"`
|
|
||||||
}
|
|
||||||
type Self struct {
|
|
||||||
Href string `json:"href"`
|
|
||||||
}
|
|
||||||
type Links struct {
|
|
||||||
Self Self `json:"self"`
|
|
||||||
}
|
|
||||||
type Embedded struct {
|
|
||||||
Tags []interface{} `json:"tags"`
|
|
||||||
}
|
|
|
@ -1,56 +0,0 @@
|
||||||
package contacts
|
|
||||||
|
|
||||||
type Contact struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
FirstName string `json:"first_name"`
|
|
||||||
LastName string `json:"last_name"`
|
|
||||||
ResponsibleUserID int `json:"responsible_user_id"`
|
|
||||||
GroupID int `json:"group_id"`
|
|
||||||
CreatedBy int `json:"created_by"`
|
|
||||||
UpdatedBy int `json:"updated_by"`
|
|
||||||
CreatedAt int `json:"created_at"`
|
|
||||||
UpdatedAt int `json:"updated_at"`
|
|
||||||
ClosestTaskAt interface{} `json:"closest_task_at"`
|
|
||||||
CustomFieldsValues []CustomFieldsValues `json:"custom_fields_values"`
|
|
||||||
AccountID int `json:"account_id"`
|
|
||||||
Links Links `json:"_links"`
|
|
||||||
Embedded Embedded `json:"_embedded"`
|
|
||||||
}
|
|
||||||
type Values struct {
|
|
||||||
Value string `json:"value"`
|
|
||||||
EnumID int `json:"enum_id"`
|
|
||||||
Enum string `json:"enum"`
|
|
||||||
}
|
|
||||||
type CustomFieldsValues struct {
|
|
||||||
FieldID int `json:"field_id"`
|
|
||||||
FieldName string `json:"field_name"`
|
|
||||||
FieldCode string `json:"field_code"`
|
|
||||||
FieldType string `json:"field_type"`
|
|
||||||
Values []Values `json:"values"`
|
|
||||||
}
|
|
||||||
type Self struct {
|
|
||||||
Href string `json:"href"`
|
|
||||||
}
|
|
||||||
type Links struct {
|
|
||||||
Self Self `json:"self"`
|
|
||||||
}
|
|
||||||
type Leads struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Links Links `json:"_links"`
|
|
||||||
}
|
|
||||||
type Customers struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Links Links `json:"_links"`
|
|
||||||
}
|
|
||||||
type Companies struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Links Links `json:"_links"`
|
|
||||||
}
|
|
||||||
type Embedded struct {
|
|
||||||
Tags []interface{} `json:"tags"`
|
|
||||||
Leads []Leads `json:"leads"`
|
|
||||||
Customers []Customers `json:"customers"`
|
|
||||||
CatalogElements []interface{} `json:"catalog_elements"`
|
|
||||||
Companies []Companies `json:"companies"`
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
package leads
|
|
||||||
|
|
||||||
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 []*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 {
|
|
||||||
Href string `json:"href"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Links struct {
|
|
||||||
Self Self `json:"self"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Tags struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Metadata struct {
|
|
||||||
Quantity int `json:"quantity"`
|
|
||||||
CatalogID int `json:"catalog_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CatalogElements struct {
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Companies struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Links Links `json:"_links"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Contacts struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
IsMain bool `json:"is_main"`
|
|
||||||
Links Links `json:"_links"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Embedded struct {
|
|
||||||
Tags []*Tags `json:"tags"`
|
|
||||||
CatalogElements []*CatalogElements `json:"catalog_elements"`
|
|
||||||
LossReason []*LossReason `json:"loss_reason"`
|
|
||||||
Companies []*Companies `json:"companies"`
|
|
||||||
Contacts []*Contacts `json:"contacts"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Value struct {
|
|
||||||
Value interface{} `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CustomFieldsValue struct {
|
|
||||||
FieldID int `json:"field_id"`
|
|
||||||
FieldCode string `json:"field_code,omitempty"`
|
|
||||||
Values []*Value `json:"values"`
|
|
||||||
}
|
|
13
next.go
Normal file
13
next.go
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
package amo
|
||||||
|
|
||||||
|
type NextFunc[V any] func() (V, NextFunc[V], error)
|
||||||
|
|
||||||
|
func MakeNextFunc[V any](
|
||||||
|
href string,
|
||||||
|
fn func(string) (V, NextFunc[V], error),
|
||||||
|
) NextFunc[V] {
|
||||||
|
return NextFunc[V](func() (V, NextFunc[V], error){
|
||||||
|
return fn(href)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
4
readme.md
Normal file
4
readme.md
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
# amo
|
||||||
|
|
||||||
|
AmoCRM API implementation in Go.
|
||||||
|
|
118
scripts/inn-check.xgo
Normal file
118
scripts/inn-check.xgo
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
os := import("os")
|
||||||
|
fmt := import("fmt")
|
||||||
|
json := import("json")
|
||||||
|
|
||||||
|
args := os.args()[2:]
|
||||||
|
file_path := args[0]
|
||||||
|
|
||||||
|
contact_field_id := 231711
|
||||||
|
company_field_id := 1300614
|
||||||
|
lead_field_id := 686597
|
||||||
|
|
||||||
|
holders := json.decode(os.read_file(file_path))
|
||||||
|
//fmt.println(len(holders))
|
||||||
|
//os.exit(0)
|
||||||
|
patch := []
|
||||||
|
|
||||||
|
find_by_field_id := func(id, fields){
|
||||||
|
for _, field in fields {
|
||||||
|
if field.field_id == id {
|
||||||
|
return field
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is_field_correct := func(field){
|
||||||
|
if !int(field.values[0].value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
str := string(field.values[0].value)
|
||||||
|
if len(str) <9 || len(str) > 12 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
//holders = holders[:2]
|
||||||
|
for i, h in holders {
|
||||||
|
company := {}
|
||||||
|
if !h.company_id {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
company.id = h.company_id
|
||||||
|
|
||||||
|
company_inn := find_by_field_id(company_field_id, h.company_inns)
|
||||||
|
//fmt.println(i, " reachd", company_inn)
|
||||||
|
if company_inn {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
contact_inn := find_by_field_id(contact_field_id, h.contact_inns)
|
||||||
|
lead_inn := find_by_field_id(lead_field_id, h.lead_inns)
|
||||||
|
|
||||||
|
if !contact_inn {
|
||||||
|
if !lead_inn || !is_field_correct(lead_inn) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
company.__reason = "Взято из лида (в контакте не было)"
|
||||||
|
company.custom_fields_values = [{
|
||||||
|
field_id: company_field_id,
|
||||||
|
values: [{
|
||||||
|
value: string(lead_inn.values[0].value)
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
patch += [company]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !lead_inn {
|
||||||
|
if !is_field_correct(contact_inn) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
company.__reason = "Взято из контакта (в лиде не было)"
|
||||||
|
company.custom_fields_values =[{
|
||||||
|
field_id: company_field_id,
|
||||||
|
values: [{
|
||||||
|
value: string(contact_inn.values[0].value)
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
} else {
|
||||||
|
if int(contact_inn.values[0].value) != int(lead_inn.values[0].value) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !is_field_correct(contact_inn) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
company.__reason = "Взято из лида и контакта"
|
||||||
|
company.custom_fields_values =[{
|
||||||
|
field_id: company_field_id,
|
||||||
|
values: [{
|
||||||
|
value: string(contact_inn.values[0].value)
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
patch += [company]
|
||||||
|
|
||||||
|
/*if h.company_inns {
|
||||||
|
for _, field in h.company_inns {
|
||||||
|
company.custom_fields_values += [{
|
||||||
|
field_id: field.field_id,
|
||||||
|
values: [{
|
||||||
|
value: field.values[0].value
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
companies += [company]*/
|
||||||
|
}
|
||||||
|
|
||||||
|
//patch = patch[:500]
|
||||||
|
//fmt.println(len(patch))
|
||||||
|
|
||||||
|
bts := json.indent(json.encode(patch), "", " ")
|
||||||
|
fmt.println(string(bts))
|
||||||
|
|
17
scripts/len-uniq-tuple-leads.xgo
Normal file
17
scripts/len-uniq-tuple-leads.xgo
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
|
||||||
|
os := import("os")
|
||||||
|
fmt := import("fmt")
|
||||||
|
json := import("json")
|
||||||
|
|
||||||
|
args := os.args()[2:]
|
||||||
|
file_path := args[0]
|
||||||
|
|
||||||
|
arr := json.decode(os.read_file(file_path))
|
||||||
|
ret := {}
|
||||||
|
for _, v in arr {
|
||||||
|
if !v.lead || !v.lead.id{
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ret[string(v.lead.id)] = 1
|
||||||
|
}
|
||||||
|
fmt.println(len(ret))
|
15
scripts/len.xgo
Normal file
15
scripts/len.xgo
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
os := import("os")
|
||||||
|
fmt := import("fmt")
|
||||||
|
sjson := import("cjson")
|
||||||
|
|
||||||
|
dec := cjson.new_decoder("<stdin>")
|
||||||
|
//file_path := args[0]
|
||||||
|
n := 0
|
||||||
|
for {
|
||||||
|
v := dec.decode()
|
||||||
|
if !v {break}
|
||||||
|
n++
|
||||||
|
fmt.println(n)
|
||||||
|
}
|
||||||
|
fmt.println(n)
|
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
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
package users
|
package users
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int `json:"id"`
|
Id int `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Lang string `json:"lang"`
|
Lang string `json:"lang"`
|
2
webhooks/handler.go
Normal file
2
webhooks/handler.go
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
package webhooks
|
||||||
|
|
|
@ -1,62 +1,104 @@
|
||||||
package webhooks
|
package webhooks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gorilla/schema"
|
//"surdeus.su/core/ss/urlenc"
|
||||||
"log"
|
"surdeus.su/core/ss/jsons"
|
||||||
|
/*"log"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"*/
|
||||||
)
|
)
|
||||||
|
|
||||||
type WebhookRequest struct {
|
type CustomFields jsons.ArrayMap[CustomField]
|
||||||
Leads Leads `schema:"leads"`
|
type CustomField struct {
|
||||||
Account Account `schema:"account"`
|
Id jsons.Int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Values jsons.ArrayMap[any]
|
||||||
}
|
}
|
||||||
|
|
||||||
type Leads struct {
|
type Request struct {
|
||||||
Status []Status `schema:"status"`
|
Account Account `json:"account"`
|
||||||
Add []Status `schema:"add"`
|
Leads *Leads `json:"leads,omitempty"`
|
||||||
|
Contacts *Contacts `json:"contacts,omitempty"`
|
||||||
}
|
}
|
||||||
|
type Type int
|
||||||
|
const (
|
||||||
|
TypeNone Type = iota
|
||||||
|
TypeLeads
|
||||||
|
TypeContacts
|
||||||
|
)
|
||||||
|
|
||||||
type Status struct {
|
func (r *Request) Type() Type {
|
||||||
Id string `schema:"id"`
|
if r.Leads != nil {
|
||||||
StatusId string `schema:"status_id"`
|
return TypeLeads
|
||||||
PipelineId string `schema:"pipeline_id"`
|
}
|
||||||
OldStatusId string `schema:"old_status_id"`
|
if r.Contacts != nil {
|
||||||
OldPipelineId string `schema:"old_pipeline_id"`
|
return TypeContacts
|
||||||
|
}
|
||||||
|
|
||||||
|
return TypeNone
|
||||||
}
|
}
|
||||||
|
|
||||||
type Account struct {
|
type Account struct {
|
||||||
Id string `schema:"id"`
|
Id jsons.Int `json:"id"`
|
||||||
SubDomain string `schema:"subdomain"`
|
SubDomain string `json:"subdomain"`
|
||||||
|
Links struct {
|
||||||
|
Self string `json:"self"`
|
||||||
|
} `json:"_links"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFromString(body string) (*WebhookRequest, error) {
|
type Leads struct {
|
||||||
decodedBody, err := url.QueryUnescape(body)
|
Status jsons.ArrayMap[LeadStatus] `json:"status"`
|
||||||
|
Add jsons.ArrayMap[LeadStatus] `json:"add"`
|
||||||
|
}
|
||||||
|
type LeadStatus struct {
|
||||||
|
Id jsons.Int `json:"id"`
|
||||||
|
StatusId jsons.Int `json:"status_id"`
|
||||||
|
|
||||||
|
PipelineId jsons.Int `json:"pipeline_id"`
|
||||||
|
|
||||||
|
OldStatusId jsons.Int `json:"old_status_id"`
|
||||||
|
OldPipelineId jsons.Int `json:"old_pipeline_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Contacts struct {
|
||||||
|
Add jsons.ArrayMap[Contact] `json:"add"`
|
||||||
|
}
|
||||||
|
type Contact struct {
|
||||||
|
Type ContactType `json:"type"`
|
||||||
|
|
||||||
|
Id jsons.Int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
AccountId jsons.Int `json:"account_id"`
|
||||||
|
CreatedUserId jsons.Int `json:"created_user_id"`
|
||||||
|
ModifiedUserId jsons.Int `json:"modified_user_id"`
|
||||||
|
ResponsibleUserId jsons.Int `json:"responsible_user_id"`
|
||||||
|
|
||||||
|
CreatedAt jsons.Int `json:"created_at"`
|
||||||
|
UpdatedAt jsons.Int `json:"updated_at"`
|
||||||
|
LastModified jsons.Int `json:"last_modified"`
|
||||||
|
DateCreate jsons.Int `json:"date_create"`
|
||||||
|
|
||||||
|
CustomFields CustomFields`json:"custom_fields,omitempty"`
|
||||||
|
}
|
||||||
|
type ContactType string
|
||||||
|
const (
|
||||||
|
NoContactType = ""
|
||||||
|
ContactContactType = "contacts"
|
||||||
|
CompanyContactType = "company"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*func NewFromString(body string) (*Request, error) {
|
||||||
|
ret := &Request{}
|
||||||
|
err := urlenc.Unmarsal(ret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return ret, nil
|
||||||
|
}*/
|
||||||
|
|
||||||
replacer := strings.NewReplacer("][", ".", "[", ".", "]", "")
|
func (hook *Request) GetLeadId() int {
|
||||||
decodedBody = replacer.Replace(decodedBody)
|
|
||||||
|
|
||||||
bodyMap := make(map[string][]string)
|
|
||||||
|
|
||||||
for _, value := range strings.Split(decodedBody, "&") {
|
|
||||||
parameter := strings.Split(value, "=")
|
|
||||||
bodyMap[parameter[0]] = []string{parameter[1]}
|
|
||||||
}
|
|
||||||
|
|
||||||
webhook := new(WebhookRequest)
|
|
||||||
|
|
||||||
err = schema.NewDecoder().Decode(webhook, bodyMap)
|
|
||||||
|
|
||||||
return webhook, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (hook *WebhookRequest) GetLeadId() string {
|
|
||||||
if hook.Leads.Status != nil {
|
if hook.Leads.Status != nil {
|
||||||
return hook.Leads.Status[0].Id
|
return int(hook.Leads.Status[0].Id)
|
||||||
}
|
}
|
||||||
return hook.Leads.Add[0].Id
|
return int(hook.Leads.Add[0].Id)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue