Compare commits
No commits in common. "main" and "sonarqube" have entirely different histories.
57 changed files with 692 additions and 2424 deletions
33
.github/workflows/build.yml
vendored
Normal file
33
.github/workflows/build.yml
vendored
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
name: Testing&Coverage
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Compile and Test
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Clone Repository
|
||||||
|
uses: actions/checkout@master
|
||||||
|
- name: Setup go
|
||||||
|
uses: actions/setup-go@v1
|
||||||
|
with:
|
||||||
|
go-version: '1.16'
|
||||||
|
- run: |
|
||||||
|
go test -short -coverprofile=cov.out `go list ./... | grep -v vendor/`
|
||||||
|
go tool cover -func=cov.out
|
||||||
|
pwd
|
||||||
|
- name: Analyze with SonarCloud
|
||||||
|
uses: sonarsource/sonarcloud-github-action@master
|
||||||
|
with:
|
||||||
|
args: >
|
||||||
|
-Dsonar.organization=smarthead
|
||||||
|
-Dsonar.projectKey=smarthead_go-amo
|
||||||
|
-Dsonar.go.coverage.reportPaths=cov.out
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
|
|
19
.gitignore
vendored
19
.gitignore
vendored
|
@ -1,7 +1,16 @@
|
||||||
|
# Binaries for programs and plugins
|
||||||
*.exe
|
*.exe
|
||||||
*.exe~
|
*.exe~
|
||||||
.env
|
*.dll
|
||||||
secret.json
|
*.so
|
||||||
*.json
|
*.dylib
|
||||||
/exe/
|
|
||||||
/tmp/
|
# Test binary, built with `go test -c`
|
||||||
|
*.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
Normal file
201
LICENSE
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
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
README.md
Normal file
1
README.md
Normal file
|
@ -0,0 +1 @@
|
||||||
|
# go-amo
|
81
amo/amocrm.go
Normal file
81
amo/amocrm.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
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) updateEntity(url string, id int, body interface{}) error {
|
||||||
|
err := client.Api.Patch(fmt.Sprintf("%s/%d", url, id), body, nil)
|
||||||
|
return 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) 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) UpdateLead(lead *leads.Lead) error {
|
||||||
|
return client.updateEntity("/api/v4/leads", lead.ID, lead)
|
||||||
|
}
|
||||||
|
|
||||||
|
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) UpdateCompany(company *companies.Company) error {
|
||||||
|
return client.updateEntity("/api/v4/companies", company.ID, company)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (client *AmoClient) UpdateContact(contact *contacts.Contact) error {
|
||||||
|
return client.updateEntity("/api/v4/contacts", contact.ID, contact)
|
||||||
|
}
|
34
amocrm.go
34
amocrm.go
|
@ -1,34 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
227
api/api.go
227
api/api.go
|
@ -1,162 +1,165 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DefaultContentType = "application/json"
|
DefaultContentType = "application/json"
|
||||||
DefaultAccept = DefaultContentType
|
DefaultAccept = DefaultContentType
|
||||||
DefaultCacheControl = "no-cache"
|
DefaultCacheControl = "no-cache"
|
||||||
MaxEntitiesPerRequest = 250
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClientOptions struct {
|
type ClientOptions struct {
|
||||||
URL string `json:"url"`
|
Url string
|
||||||
RedirectURL string `json:"redirect_url"`
|
ClientId string
|
||||||
|
ClientSecret string
|
||||||
AuthCode string `json:"auth_code"`
|
AccessToken string
|
||||||
|
RefreshToken string
|
||||||
ClientId string `json:"client_id"`
|
|
||||||
ClientSecret string `json:"client_secret"`
|
|
||||||
|
|
||||||
AccessToken string `json:"access_token"`
|
|
||||||
|
|
||||||
ExpirationAt time.Time `json:"expiration_at,omitempty"`
|
|
||||||
RefreshToken string `json:"refresh_token"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
*log.Logger
|
options *ClientOptions
|
||||||
options ClientOptions
|
BaseUrl *url.URL
|
||||||
BaseURL *url.URL
|
|
||||||
secretStoreFilePath string
|
|
||||||
Debug bool
|
|
||||||
|
|
||||||
availableRequests int
|
|
||||||
mrps int
|
|
||||||
|
|
||||||
ticker *time.Ticker
|
|
||||||
req, reqNeed chan struct{}
|
|
||||||
|
|
||||||
requestsMade int64
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type OAuthTokenResponse struct {
|
type requestOptions struct {
|
||||||
|
HttpMethod string
|
||||||
|
Body interface{}
|
||||||
|
Headers map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type OauthTokenResponse struct {
|
||||||
TokenType string `json:"token_type"`
|
TokenType string `json:"token_type"`
|
||||||
ExpiresIn int `json:"expires_in"`
|
ExpiresIn int `json:"expires_in"`
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type TokenPair struct {
|
func NewClient(options *ClientOptions) (*Client, error) {
|
||||||
Access, Refresh string
|
if len(options.AccessToken) == 0 || len(options.RefreshToken) == 0 || len(options.Url) == 0 {
|
||||||
}
|
return nil, errors.New("AmoCrm: Invalid options")
|
||||||
|
|
||||||
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()
|
|
||||||
|
resolvedUrl, err := url.Parse(options.Url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorAPI(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if client.Debug {
|
return &Client{
|
||||||
client.Printf("ClientOptions: %v\n", options)
|
options: options,
|
||||||
}
|
BaseUrl: resolvedUrl,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
if options.URL == "" || options.RedirectURL == "" {
|
func (api *Client) doRequest(resourceUrl string, requestParams requestOptions, result interface{}) error {
|
||||||
return nil, NewErrorAPI(ErrInvalidOptionsURL)
|
resolvedUrl, err := url.Parse(resourceUrl)
|
||||||
}
|
|
||||||
|
|
||||||
parsedURL, err := url.Parse(options.URL)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorAPI(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
client.BaseURL = parsedURL
|
requestUrl := api.BaseUrl.ResolveReference(resolvedUrl)
|
||||||
client.options = options
|
|
||||||
|
|
||||||
var (
|
requestBody := new(bytes.Buffer)
|
||||||
//pair *TokenPair
|
if requestParams.Body != nil {
|
||||||
exchangeErr error
|
encoder := json.NewEncoder(requestBody)
|
||||||
exchanged bool
|
if err := encoder.Encode(requestParams.Body); err != nil {
|
||||||
)
|
return err
|
||||||
if client.options.AccessToken == "" &&
|
}
|
||||||
client.options.RefreshToken == "" {
|
|
||||||
|
|
||||||
if client.options.ClientSecret == "" ||
|
|
||||||
client.options.ClientId == "" ||
|
|
||||||
client.options.AuthCode == "" {
|
|
||||||
return nil, NewErrorAPI(
|
|
||||||
ErrInvalidExchangeAuthOptions,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, exchangeErr = client.ExchangeAuth()
|
request, err := http.NewRequest(requestParams.HttpMethod, requestUrl.String(), requestBody)
|
||||||
exchanged = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!exchanged || exchangeErr != nil) &&
|
|
||||||
options.RefreshToken != "" {
|
|
||||||
|
|
||||||
// Refreshing token before the work.
|
|
||||||
// Should think of how often should refresh
|
|
||||||
// the token. (see the ExpiresIn)
|
|
||||||
_, err = client.RefreshToken()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, NewErrorAPI(err)
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if requestParams.Headers != nil {
|
||||||
|
for k, v := range requestParams.Headers {
|
||||||
|
request.Header.Set(k, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return client, nil
|
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))
|
||||||
// Set maximum requests per second.
|
|
||||||
func (client *Client) SetMRPS(rps int) *Client {
|
|
||||||
client.mrps = rps
|
|
||||||
client.req = make(chan struct{})
|
|
||||||
client.reqNeed = make(chan struct{})
|
|
||||||
client.ticker = time.NewTicker(
|
|
||||||
time.Second/time.Duration(rps),
|
|
||||||
)
|
|
||||||
go func() {
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <- client.reqNeed :
|
|
||||||
client.req <- struct{}{}
|
|
||||||
}
|
}
|
||||||
<-client.ticker.C
|
defer response.Body.Close()
|
||||||
}
|
|
||||||
}()
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
func (client *Client) waitInQueue() bool {
|
if response.StatusCode >= 400 {
|
||||||
// Just leaving if MRPS is not set.
|
bodyBytes, _ := ioutil.ReadAll(response.Body)
|
||||||
if client.mrps == 0 {
|
return errors.New(fmt.Sprintf("%s %d %s %s", string(bodyBytes), response.StatusCode, requestParams.HttpMethod, resourceUrl))
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
client.reqNeed <- struct{}{}
|
if result != nil {
|
||||||
<- client.req
|
decoder := json.NewDecoder(response.Body)
|
||||||
|
err = decoder.Decode(result)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return true
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) finishRequest() {
|
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.Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := api.doRequest("/oauth2/access_token", requestOptions{
|
||||||
|
HttpMethod: http.MethodPost,
|
||||||
|
Body: request,
|
||||||
|
Headers: getHeaders(""),
|
||||||
|
}, result)
|
||||||
|
|
||||||
|
return result, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (client *Client) RequestsMade() int64 {
|
func (api *Client) Get(resource string, result interface{}) error {
|
||||||
return client.requestsMade
|
return api.doRequest(resource, requestOptions{
|
||||||
|
HttpMethod: http.MethodGet,
|
||||||
|
Body: nil,
|
||||||
|
Headers: getHeaders(api.options.AccessToken),
|
||||||
|
}, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *Client) Post(resource string, request interface{}, result interface{}) error {
|
||||||
|
return api.doRequest(resource, requestOptions{
|
||||||
|
HttpMethod: http.MethodPost,
|
||||||
|
Body: request,
|
||||||
|
Headers: getHeaders(api.options.AccessToken),
|
||||||
|
}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *Client) Patch(resource string, request interface{}, result interface{}) error {
|
||||||
|
return api.doRequest(resource, requestOptions{
|
||||||
|
HttpMethod: http.MethodPatch,
|
||||||
|
Body: request,
|
||||||
|
Headers: getHeaders(api.options.AccessToken),
|
||||||
|
}, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHeaders(token string) map[string]string {
|
||||||
|
headers := map[string]string{
|
||||||
|
"Accept": DefaultAccept,
|
||||||
|
"Cache-Control": DefaultCacheControl,
|
||||||
|
"Content-Type": DefaultContentType,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(token) > 0 {
|
||||||
|
headers["Authorization"] = fmt.Sprintf("Bearer %s", token)
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
|
}
|
||||||
|
|
93
api/auth.go
93
api/auth.go
|
@ -1,93 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
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
169
api/request.go
|
@ -1,169 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,39 +0,0 @@
|
||||||
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
3
btest.sh
|
@ -1,3 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
#
|
|
||||||
go build -o ./exe/ ./cmd/test
|
|
4
build.sh
4
build.sh
|
@ -1,4 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
go build -o ./exe/ ./cmd/amocli/
|
|
||||||
|
|
|
@ -1,202 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,34 +0,0 @@
|
||||||
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)
|
|
||||||
})
|
|
|
@ -1,26 +0,0 @@
|
||||||
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)
|
|
||||||
})
|
|
|
@ -1,29 +0,0 @@
|
||||||
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]",
|
|
||||||
)
|
|
|
@ -1,33 +0,0 @@
|
||||||
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]",
|
|
||||||
)
|
|
|
@ -1,188 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,32 +0,0 @@
|
||||||
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",
|
|
||||||
),
|
|
||||||
)
|
|
|
@ -1,30 +0,0 @@
|
||||||
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)
|
|
||||||
})
|
|
|
@ -1,76 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,59 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
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
92
companies.go
|
@ -1,92 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
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
72
contacts.go
|
@ -1,72 +0,0 @@
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,73 +0,0 @@
|
||||||
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
18
entity.go
|
@ -1,18 +0,0 @@
|
||||||
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
5
env.sh
|
@ -1,5 +0,0 @@
|
||||||
lines=$(cat .env)
|
|
||||||
for line in $lines ; do
|
|
||||||
echo $line
|
|
||||||
eval "export $line"
|
|
||||||
done
|
|
|
@ -1,7 +0,0 @@
|
||||||
package amo
|
|
||||||
|
|
||||||
import "errors"
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrTooManyEntities = errors.New("too many entities")
|
|
||||||
)
|
|
39
events.go
39
events.go
|
@ -1,39 +0,0 @@
|
||||||
package amo
|
|
||||||
|
|
||||||
import "surdeus.su/core/amo/events"
|
|
||||||
import "surdeus.su/core/amo/api"
|
|
||||||
import "surdeus.su/core/ss/urlenc"
|
|
||||||
import "errors"
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
// Returns the events from AmoCRM by specified request.
|
|
||||||
// If there are no such events returns an empty slice of events.
|
|
||||||
func (client *Client) GetEvents(
|
|
||||||
opts ...urlenc.Builder,
|
|
||||||
) ([]events.Event, error) {
|
|
||||||
res := fmt.Sprintf(
|
|
||||||
"/api/v4/events?%s",
|
|
||||||
urlenc.Join(opts...).Encode(),
|
|
||||||
)
|
|
||||||
|
|
||||||
ret := []events.Event{}
|
|
||||||
for {
|
|
||||||
resp := events.EventsResponse{}
|
|
||||||
err := client.API.Get(res, &resp)
|
|
||||||
if err != nil {
|
|
||||||
// Return empty if no content avialable.
|
|
||||||
if errors.Is(err, api.ErrNoContent) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
ret = append(ret, resp.Embedded.Events...)
|
|
||||||
if resp.Links.Next.Href == "" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
//time.Sleep(time.Millisecond * 300)
|
|
||||||
res = resp.Links.Next.Href
|
|
||||||
}
|
|
||||||
|
|
||||||
return ret, nil
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
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,15 +1,8 @@
|
||||||
module surdeus.su/core/amo
|
module github.com/qdimka/go-amo
|
||||||
|
|
||||||
go 1.21.3
|
go 1.15
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/stretchr/testify v1.9.0
|
github.com/gorilla/schema v1.2.0
|
||||||
surdeus.su/core/ss v0.1.4
|
github.com/stretchr/testify v1.6.1
|
||||||
)
|
|
||||||
|
|
||||||
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,20 +1,13 @@
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/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/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
|
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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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=
|
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
go install ./cmd/amocli/
|
|
143
lead-tuple.go
143
lead-tuple.go
|
@ -1,143 +0,0 @@
|
||||||
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
71
leads.go
|
@ -1,71 +0,0 @@
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
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"`
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,91 +0,0 @@
|
||||||
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
22
license.txt
|
@ -1,22 +0,0 @@
|
||||||
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.
|
|
||||||
|
|
14
models/common/common.go
Normal file
14
models/common/common.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package common
|
||||||
|
|
||||||
|
type Values struct {
|
||||||
|
Value interface{} `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 []Values `json:"values"`
|
||||||
|
}
|
36
models/companies/companies.go
Normal file
36
models/companies/companies.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package companies
|
||||||
|
|
||||||
|
import "github.com/qdimka/go-amo/models/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.CustomFieldsValue `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"`
|
||||||
|
}
|
57
models/contacts/contacts.go
Normal file
57
models/contacts/contacts.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package contacts
|
||||||
|
|
||||||
|
import "github.com/qdimka/go-amo/models/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.CustomFieldsValue `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 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"`
|
||||||
|
}
|
79
models/leads/leads.go
Normal file
79
models/leads/leads.go
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
package leads
|
||||||
|
|
||||||
|
import "github.com/qdimka/go-amo/models/common"
|
||||||
|
|
||||||
|
type Lead struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Price int `json:"price,omitempty"`
|
||||||
|
ResponsibleUserID int `json:"responsible_user_id,omitempty"`
|
||||||
|
GroupID int `json:"group_id,omitempty"`
|
||||||
|
StatusID int `json:"status_id,omitempty"`
|
||||||
|
PipelineID int `json:"pipeline_id,omitempty"`
|
||||||
|
LossReasonID int `json:"loss_reason_id,omitempty"`
|
||||||
|
SourceID interface{} `json:"source_id,omitempty"`
|
||||||
|
CreatedBy int `json:"created_by,omitempty"`
|
||||||
|
UpdatedBy int `json:"updated_by,omitempty"`
|
||||||
|
CreatedAt int `json:"created_at,omitempty"`
|
||||||
|
UpdatedAt int `json:"updated_at,omitempty"`
|
||||||
|
ClosedAt int `json:"closed_at,omitempty"`
|
||||||
|
ClosestTaskAt interface{} `json:"closest_task_at,omitempty"`
|
||||||
|
IsDeleted bool `json:"is_deleted,omitempty"`
|
||||||
|
CustomFieldsValues []*common.CustomFieldsValue `json:"custom_fields_values,omitempty"`
|
||||||
|
Score interface{} `json:"score,omitempty"`
|
||||||
|
AccountID int `json:"account_id,omitempty"`
|
||||||
|
IsPriceModifiedByRobot bool `json:"is_price_modified_by_robot,omitempty"`
|
||||||
|
Links Links `json:"_links,omitempty"`
|
||||||
|
Embedded Embedded `json:"_embedded,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
|
@ -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"`
|
13
next.go
13
next.go
|
@ -1,13 +0,0 @@
|
||||||
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,4 +0,0 @@
|
||||||
# amo
|
|
||||||
|
|
||||||
AmoCRM API implementation in Go.
|
|
||||||
|
|
|
@ -1,118 +0,0 @@
|
||||||
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))
|
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
|
|
||||||
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))
|
|
|
@ -1,15 +0,0 @@
|
||||||
|
|
||||||
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
18
users.go
|
@ -1,18 +0,0 @@
|
||||||
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,2 +0,0 @@
|
||||||
package webhooks
|
|
||||||
|
|
|
@ -1,104 +1,62 @@
|
||||||
package webhooks
|
package webhooks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
//"surdeus.su/core/ss/urlenc"
|
"github.com/gorilla/schema"
|
||||||
"surdeus.su/core/ss/jsons"
|
"log"
|
||||||
/*"log"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"*/
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CustomFields jsons.ArrayMap[CustomField]
|
type WebhookRequest struct {
|
||||||
type CustomField struct {
|
Leads Leads `schema:"leads"`
|
||||||
Id jsons.Int `json:"id"`
|
Account Account `schema:"account"`
|
||||||
Name string `json:"name"`
|
|
||||||
Values jsons.ArrayMap[any]
|
|
||||||
}
|
|
||||||
|
|
||||||
type Request struct {
|
|
||||||
Account Account `json:"account"`
|
|
||||||
Leads *Leads `json:"leads,omitempty"`
|
|
||||||
Contacts *Contacts `json:"contacts,omitempty"`
|
|
||||||
}
|
|
||||||
type Type int
|
|
||||||
const (
|
|
||||||
TypeNone Type = iota
|
|
||||||
TypeLeads
|
|
||||||
TypeContacts
|
|
||||||
)
|
|
||||||
|
|
||||||
func (r *Request) Type() Type {
|
|
||||||
if r.Leads != nil {
|
|
||||||
return TypeLeads
|
|
||||||
}
|
|
||||||
if r.Contacts != nil {
|
|
||||||
return TypeContacts
|
|
||||||
}
|
|
||||||
|
|
||||||
return TypeNone
|
|
||||||
}
|
|
||||||
|
|
||||||
type Account struct {
|
|
||||||
Id jsons.Int `json:"id"`
|
|
||||||
SubDomain string `json:"subdomain"`
|
|
||||||
Links struct {
|
|
||||||
Self string `json:"self"`
|
|
||||||
} `json:"_links"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Leads struct {
|
type Leads struct {
|
||||||
Status jsons.ArrayMap[LeadStatus] `json:"status"`
|
Status []Status `schema:"status"`
|
||||||
Add jsons.ArrayMap[LeadStatus] `json:"add"`
|
Add []Status `schema:"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 {
|
type Status struct {
|
||||||
Add jsons.ArrayMap[Contact] `json:"add"`
|
Id string `schema:"id"`
|
||||||
|
StatusId string `schema:"status_id"`
|
||||||
|
PipelineId string `schema:"pipeline_id"`
|
||||||
|
OldStatusId string `schema:"old_status_id"`
|
||||||
|
OldPipelineId string `schema:"old_pipeline_id"`
|
||||||
}
|
}
|
||||||
type Contact struct {
|
|
||||||
Type ContactType `json:"type"`
|
|
||||||
|
|
||||||
Id jsons.Int `json:"id"`
|
type Account struct {
|
||||||
Name string `json:"name"`
|
Id string `schema:"id"`
|
||||||
AccountId jsons.Int `json:"account_id"`
|
SubDomain string `schema:"subdomain"`
|
||||||
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) {
|
func NewFromString(body string) (*WebhookRequest, error) {
|
||||||
ret := &Request{}
|
decodedBody, err := url.QueryUnescape(body)
|
||||||
err := urlenc.Unmarsal(ret)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return ret, nil
|
|
||||||
}*/
|
|
||||||
|
|
||||||
func (hook *Request) GetLeadId() int {
|
replacer := strings.NewReplacer("][", ".", "[", ".", "]", "")
|
||||||
if hook.Leads.Status != nil {
|
decodedBody = replacer.Replace(decodedBody)
|
||||||
return int(hook.Leads.Status[0].Id)
|
|
||||||
|
bodyMap := make(map[string][]string)
|
||||||
|
|
||||||
|
for _, value := range strings.Split(decodedBody, "&") {
|
||||||
|
parameter := strings.Split(value, "=")
|
||||||
|
bodyMap[parameter[0]] = []string{parameter[1]}
|
||||||
}
|
}
|
||||||
return int(hook.Leads.Add[0].Id)
|
|
||||||
|
webhook := new(WebhookRequest)
|
||||||
|
|
||||||
|
err = schema.NewDecoder().Decode(webhook, bodyMap)
|
||||||
|
|
||||||
|
return webhook, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (hook *WebhookRequest) GetLeadId() string {
|
||||||
|
if hook.Leads.Status != nil {
|
||||||
|
return hook.Leads.Status[0].Id
|
||||||
|
}
|
||||||
|
return hook.Leads.Add[0].Id
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue