391 lines
9.6 KiB
Go
391 lines
9.6 KiB
Go
package amo
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"io"
|
||
"log"
|
||
"net/http"
|
||
"net/url"
|
||
"time"
|
||
|
||
"github.com/dgrijalva/jwt-go"
|
||
"github.com/pkg/errors"
|
||
"golang.org/x/oauth2"
|
||
"penahub.gitlab.yandexcloud.net/backend/templategen/tools"
|
||
)
|
||
|
||
const (
|
||
OauthURL = "https://www.amocrm.ru/oauth"
|
||
)
|
||
|
||
type Client struct {
|
||
App *ClientApp
|
||
Config *oauth2.Config // Индивидуальный конфиг клиента
|
||
HTTPClient *http.Client
|
||
Subdomain string // Субдомен с которым работаем
|
||
Token *oauth2.Token
|
||
}
|
||
|
||
type transport struct {
|
||
underlyingTransport http.RoundTripper
|
||
}
|
||
|
||
func (t *transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||
request.Header.Set("User-Agent", "amoCRM-oAuth-client/1.0")
|
||
request.Header.Set("Content-Type", "application/json")
|
||
|
||
return t.underlyingTransport.RoundTrip(request)
|
||
}
|
||
|
||
type ClientApp struct {
|
||
Config *oauth2.Config
|
||
}
|
||
|
||
/*
|
||
TODO: Вероятно стоит вынести аргументы в отдельную структуру типа Deps.
|
||
|
||
Аргументация Кирилла:
|
||
Я думаю лучше все аргументы для инициализации модуля выносить в структуру,
|
||
потому что количество аргументов может в дальнейшем увеличиваться.
|
||
И чтобы избежать большого количества аргументов в количестве +3 штук, лучше выносить в структуру.
|
||
*/
|
||
|
||
func NewClientApp(clientID, clientSecret string, redirectURI string) *ClientApp {
|
||
return &ClientApp{
|
||
Config: &oauth2.Config{
|
||
ClientID: clientID,
|
||
ClientSecret: clientSecret,
|
||
Endpoint: oauth2.Endpoint{
|
||
AuthURL: OauthURL,
|
||
TokenURL: OauthURL + "2/access_token",
|
||
},
|
||
RedirectURL: redirectURI,
|
||
Scopes: nil,
|
||
},
|
||
}
|
||
}
|
||
|
||
func (ca *ClientApp) GenerateOAuthURL(amoID, penaID, redirectURL string) (string, error) {
|
||
state, err := tools.EncryptTokenAES(tools.StateToken{
|
||
AmoID: amoID,
|
||
PenaID: penaID,
|
||
Service: "amo",
|
||
RedirectURL: redirectURL,
|
||
})
|
||
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
|
||
return ca.Config.AuthCodeURL(
|
||
state,
|
||
oauth2.SetAuthURLParam("mode", "popup"),
|
||
), nil
|
||
}
|
||
|
||
func (ca *ClientApp) DecodeJwt(r *http.Request) (*XAuthToken, error) {
|
||
tokenHeader := r.Header.Get("x-auth-token")
|
||
|
||
if tokenHeader == "" {
|
||
cookie, err := r.Cookie("x-auth-token")
|
||
if err != nil {
|
||
return nil, errors.New("empty jwt")
|
||
}
|
||
|
||
tokenHeader = cookie.Value
|
||
}
|
||
|
||
token, err := jwt.ParseWithClaims(tokenHeader, &XAuthToken{}, func(token *jwt.Token) (interface{}, error) {
|
||
return []byte(ca.Config.ClientSecret), nil
|
||
})
|
||
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
claims, ok := token.Claims.(*XAuthToken)
|
||
if !ok || !token.Valid {
|
||
fmt.Println("token:", token)
|
||
fmt.Println("claims:", claims)
|
||
fmt.Println("valid:", token.Valid)
|
||
return nil, errors.New("invalid token")
|
||
}
|
||
|
||
return claims, nil
|
||
}
|
||
|
||
func (ca *ClientApp) NewClient(ctx context.Context, referer string, token *oauth2.Token, code string) (*Client,
|
||
error) {
|
||
var err error
|
||
|
||
client := &Client{
|
||
App: ca,
|
||
Config: &oauth2.Config{
|
||
ClientID: ca.Config.ClientID,
|
||
ClientSecret: ca.Config.ClientSecret,
|
||
Endpoint: oauth2.Endpoint{
|
||
AuthURL: ca.Config.Endpoint.AuthURL,
|
||
TokenURL: "https://" + referer + "/oauth2/access_token",
|
||
},
|
||
RedirectURL: ca.Config.RedirectURL,
|
||
Scopes: ca.Config.Scopes,
|
||
},
|
||
HTTPClient: nil,
|
||
Subdomain: referer,
|
||
Token: token,
|
||
}
|
||
|
||
if code != "" {
|
||
token, err = client.Config.Exchange(ctx, code)
|
||
}
|
||
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
client.Token = token
|
||
client.HTTPClient = client.Config.Client(ctx, token)
|
||
client.HTTPClient.Transport = &transport{underlyingTransport: client.HTTPClient.Transport}
|
||
|
||
return client, nil
|
||
}
|
||
|
||
func (ca *ClientApp) RefreshToken(ctx context.Context, oldToken *oauth2.Token, referer string) (*oauth2.Token,
|
||
error) {
|
||
referer = "https://" + referer
|
||
|
||
config := &oauth2.Config{
|
||
ClientID: ca.Config.ClientID,
|
||
ClientSecret: ca.Config.ClientSecret,
|
||
Endpoint: oauth2.Endpoint{
|
||
AuthURL: ca.Config.Endpoint.AuthURL,
|
||
TokenURL: referer + "/oauth2/access_token",
|
||
},
|
||
RedirectURL: ca.Config.RedirectURL,
|
||
Scopes: ca.Config.Scopes,
|
||
}
|
||
|
||
token, err := config.TokenSource(ctx, oldToken).Token()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return token, nil
|
||
}
|
||
|
||
func (c *Client) GetAccount(ctx context.Context) (*Account, error) {
|
||
requestURL := url.URL{
|
||
Scheme: "https",
|
||
Host: c.Subdomain,
|
||
Path: "api/v4/account",
|
||
}
|
||
|
||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL.String(), http.NoBody)
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "GetAccount.NewRequest")
|
||
}
|
||
|
||
response, err := c.HTTPClient.Do(request)
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "GetAccount.DoRequest")
|
||
}
|
||
|
||
defer func() {
|
||
if err = response.Body.Close(); err != nil {
|
||
log.Println("ERROR", errors.Wrap(err, "GetAccount.BodyClose"))
|
||
}
|
||
}()
|
||
|
||
var (
|
||
result Account
|
||
bobo []byte
|
||
)
|
||
|
||
bobo, _ = io.ReadAll(response.Body)
|
||
|
||
fmt.Println("getarrr", string(bobo))
|
||
|
||
// TODO: Проверить в ssa сокращенную запись if err = operation(); err != nil {...}
|
||
err = json.Unmarshal(bobo, &result)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &result, nil
|
||
}
|
||
|
||
func (c *Client) GetLeadByID(ctx context.Context, id string) (*Lead, error) {
|
||
requestURLQueries := make(url.Values)
|
||
requestURLQueries.Add("with", "contacts,catalog_elements,is_price_modified_by_robot,loss_reason")
|
||
|
||
requestURL := url.URL{
|
||
Scheme: "https",
|
||
Host: c.Subdomain,
|
||
Path: fmt.Sprintf("api/v4/leads/%v", id),
|
||
RawQuery: requestURLQueries.Encode(),
|
||
}
|
||
|
||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL.String(), http.NoBody)
|
||
|
||
fmt.Println("URL:", requestURL.String())
|
||
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "GetLeadByID.NewRequest")
|
||
}
|
||
|
||
response, err := c.HTTPClient.Do(request)
|
||
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "GetLeadByID.DoRequest")
|
||
}
|
||
|
||
defer func() {
|
||
if err = response.Body.Close(); err != nil {
|
||
log.Println("ERROR", errors.Wrap(err, "GetLeadByID.BodyClose"))
|
||
}
|
||
}()
|
||
|
||
for response.StatusCode == http.StatusTooManyRequests {
|
||
time.Sleep(time.Second)
|
||
response, err = c.HTTPClient.Do(request)
|
||
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "GetLeadByID.DoRequest")
|
||
}
|
||
|
||
func() {
|
||
defer func() {
|
||
if err = response.Body.Close(); err != nil {
|
||
log.Println("ERROR", errors.Wrap(err, "GetLeadByID.BodyClose"))
|
||
}
|
||
}()
|
||
}()
|
||
}
|
||
|
||
var result Lead
|
||
responseData, err := io.ReadAll(response.Body)
|
||
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "GetLeadByID.ReadAll")
|
||
}
|
||
|
||
err = json.Unmarshal(responseData, &result)
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "GetLeadByID.Unmarshal")
|
||
}
|
||
|
||
return &result, nil
|
||
}
|
||
|
||
func (c *Client) GetContactByID(ctx context.Context, id string) (*Contact, error) {
|
||
requestURLQueries := make(url.Values)
|
||
requestURLQueries.Add("with", "catalog_elements,leads,customers")
|
||
|
||
requestURL := url.URL{
|
||
Scheme: "https",
|
||
Host: c.Subdomain,
|
||
Path: fmt.Sprintf("api/v4/contacts/%v", id),
|
||
RawQuery: requestURLQueries.Encode(),
|
||
}
|
||
|
||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL.String(), http.NoBody)
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "GetContactByID.NewRequest")
|
||
}
|
||
|
||
response, err := c.HTTPClient.Do(request)
|
||
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "GetContactByID.DoRequest")
|
||
}
|
||
|
||
defer func() {
|
||
if err = response.Body.Close(); err != nil {
|
||
log.Println("ERROR", errors.Wrap(err, "GetContactByID.BodyClose"))
|
||
}
|
||
}()
|
||
|
||
// TODO: переписать на отдельную сущность-клиент, которая сама внутри себя будет мониторить вопрос rate-limit
|
||
for response.StatusCode == http.StatusTooManyRequests {
|
||
time.Sleep(time.Second)
|
||
response, err = c.HTTPClient.Do(request)
|
||
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "GetContactByID.ForDoRequest")
|
||
}
|
||
|
||
func() {
|
||
defer func() {
|
||
if err = response.Body.Close(); err != nil {
|
||
log.Println("ERROR", errors.Wrap(err, "GetContactByID.BodyClose"))
|
||
}
|
||
}()
|
||
}()
|
||
}
|
||
|
||
var result Contact
|
||
err = json.NewDecoder(response.Body).Decode(&result)
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "GetContactByID.Decode")
|
||
}
|
||
|
||
return &result, nil
|
||
}
|
||
|
||
func (c *Client) GetCompanyByID(ctx context.Context, id string) (*Company, error) {
|
||
requestURLQueries := make(url.Values)
|
||
requestURLQueries.Add("with", "contacts,leads,catalog_elements,customers")
|
||
|
||
requestURL := url.URL{
|
||
Scheme: "https",
|
||
Host: c.Subdomain,
|
||
Path: fmt.Sprintf("api/v4/companies/%v", id),
|
||
RawQuery: requestURLQueries.Encode(),
|
||
}
|
||
|
||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL.String(), http.NoBody)
|
||
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "GetCompanyByID.NewRequest")
|
||
}
|
||
|
||
response, err := c.HTTPClient.Do(request)
|
||
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "GetCompanyByID.DoRequest")
|
||
}
|
||
|
||
defer func() {
|
||
if err = response.Body.Close(); err != nil {
|
||
log.Println("ERROR", errors.Wrap(err, "GetCompanyByID.BodyClose"))
|
||
}
|
||
}()
|
||
|
||
for response.StatusCode == http.StatusTooManyRequests {
|
||
time.Sleep(time.Second)
|
||
response, err = c.HTTPClient.Do(request)
|
||
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "GetCompanyByID.ForDoRequest")
|
||
}
|
||
|
||
func() {
|
||
defer func() {
|
||
if err = response.Body.Close(); err != nil {
|
||
log.Println("ERROR", errors.Wrap(err, "GetCompanyByID.BodyClose"))
|
||
}
|
||
}()
|
||
}()
|
||
}
|
||
|
||
var result Company
|
||
err = json.NewDecoder(response.Body).Decode(&result)
|
||
if err != nil {
|
||
return nil, errors.Wrap(err, "GetCompanyByID.Decode")
|
||
}
|
||
|
||
return &result, nil
|
||
}
|