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 }