384 lines
8.1 KiB
Go
384 lines
8.1 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"errors"
|
|
"bytes"
|
|
"time"
|
|
"fmt"
|
|
"log"
|
|
"io"
|
|
"os"
|
|
)
|
|
|
|
const (
|
|
DefaultContentType = "application/json"
|
|
DefaultAccept = DefaultContentType
|
|
DefaultCacheControl = "no-cache"
|
|
)
|
|
|
|
type ClientOptions struct {
|
|
Url string `json:"url"`
|
|
RedirectUrl string `json:"redirect_url"`
|
|
|
|
AuthCode string `json:"auth_code"`
|
|
|
|
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 {
|
|
*log.Logger
|
|
options *ClientOptions
|
|
BaseUrl *url.URL
|
|
secretStoreFilePath string
|
|
Debug bool
|
|
}
|
|
|
|
type requestOptions struct {
|
|
HttpMethod string
|
|
Body interface{}
|
|
Headers map[string]string
|
|
Abs bool
|
|
}
|
|
|
|
type OauthTokenResponse struct {
|
|
TokenType string `json:"token_type"`
|
|
ExpiresIn int `json:"expires_in"`
|
|
AccessToken string `json:"access_token"`
|
|
RefreshToken string `json:"refresh_token"`
|
|
}
|
|
|
|
type TokenPair struct {
|
|
Access, Refresh string
|
|
}
|
|
|
|
func NewApi(secretPath string) (*Client, error) {
|
|
client := &Client{
|
|
Logger: log.New(os.Stdout, "AmoCRM client: ", log.Ldate | log.Ltime),
|
|
secretStoreFilePath: secretPath,
|
|
}
|
|
options, err := client.readSecret()
|
|
if err != nil {
|
|
return nil, NewApiError(err)
|
|
}
|
|
|
|
if client.Debug {
|
|
client.Printf("ClientOptions: %v\n", options)
|
|
}
|
|
|
|
if options.Url == "" || options.RedirectUrl == "" {
|
|
return nil, NewApiError(InvalidUrlOptionsErr)
|
|
}
|
|
|
|
resolvedUrl, err := url.Parse(options.Url)
|
|
if err != nil {
|
|
return nil, NewApiError(err)
|
|
}
|
|
|
|
client.BaseUrl = resolvedUrl
|
|
client.options = options
|
|
|
|
var (
|
|
//pair *TokenPair
|
|
exchangeErr error
|
|
exchanged bool
|
|
)
|
|
if client.options.AccessToken == "" && client.options.RefreshToken == "" {
|
|
if client.options.ClientSecret == "" ||
|
|
client.options.ClientId == "" ||
|
|
client.options.AuthCode == "" {
|
|
return nil, NewApiError(InvalidExchangeAuthOptionsErr)
|
|
}
|
|
_, exchangeErr = client.ExchangeAuth()
|
|
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 {
|
|
return nil, NewApiError(err)
|
|
}
|
|
}
|
|
|
|
return client, nil
|
|
}
|
|
|
|
func (api *Client) readSecret() (*ClientOptions, error) {
|
|
f, err := os.Open(api.secretStoreFilePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
bts, err := io.ReadAll(f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
|
|
ret := ClientOptions{}
|
|
err = json.Unmarshal(bts, &ret)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ret, nil
|
|
}
|
|
|
|
func (api *Client) writeSecret() error {
|
|
bts, err := json.MarshalIndent(api.options, "", "\t")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = os.WriteFile(api.secretStoreFilePath, bts, 0644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (api *Client) rdoRequest(resourceUrl string, requestParams requestOptions, result interface{}) error {
|
|
var (
|
|
requestUrl *url.URL
|
|
err error
|
|
)
|
|
|
|
if !requestParams.Abs {
|
|
resolvedUrl, err := url.Parse(resourceUrl)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
requestUrl = api.BaseUrl.ResolveReference(resolvedUrl)
|
|
} else {
|
|
requestUrl, err = url.Parse(resourceUrl)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
requestBody := new(bytes.Buffer)
|
|
if requestParams.Body != nil {
|
|
encoder := json.NewEncoder(requestBody)
|
|
if err := encoder.Encode(requestParams.Body); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
request, err := http.NewRequest(requestParams.HttpMethod, requestUrl.String(), requestBody)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if requestParams.Headers != nil {
|
|
for k, v := range requestParams.Headers {
|
|
request.Header.Set(k, v)
|
|
}
|
|
}
|
|
|
|
response, err := http.DefaultClient.Do(request)
|
|
if api.Debug {
|
|
api.Printf("Request: %+v\n\nAmo response: %+v\n\n", request, response)
|
|
}
|
|
if response == nil {
|
|
return RequestError(NoInternetErr)
|
|
}
|
|
if err != nil {
|
|
return RequestError(fmt.Errorf(
|
|
"%w: %d %s %s",
|
|
err,
|
|
response.StatusCode,
|
|
requestParams.HttpMethod,
|
|
resourceUrl,
|
|
))
|
|
}
|
|
defer response.Body.Close()
|
|
|
|
if response.StatusCode == 204 {
|
|
return RequestError(NoContentErr)
|
|
}
|
|
|
|
if response.StatusCode >= 400 {
|
|
var specErr error
|
|
if response.StatusCode == 401 {
|
|
specErr = NoAuthErr
|
|
}
|
|
|
|
responseErrorInfo, _ := ioutil.ReadAll(response.Body)
|
|
return RequestError(fmt.Errorf(
|
|
"%w: %s %s %q %d",
|
|
specErr,
|
|
requestParams.HttpMethod,
|
|
resourceUrl,
|
|
string(responseErrorInfo),
|
|
response.StatusCode,
|
|
))
|
|
}
|
|
|
|
if result != nil {
|
|
decoder := json.NewDecoder(response.Body)
|
|
err = decoder.Decode(result)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
func (api *Client) doRequest(
|
|
resourceUrl string, requestParams requestOptions, result any,
|
|
) error {
|
|
var err error
|
|
err = api.rdoRequest(resourceUrl, requestParams, result)
|
|
if errors.Is(err, NoAuthErr) && api.options.RefreshToken != "" {
|
|
_, err = api.RefreshToken()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
return api.rdoRequest(resourceUrl, requestParams, result)
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
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.rdoRequest("/oauth2/access_token", requestOptions{
|
|
HttpMethod: http.MethodPost,
|
|
Body: request,
|
|
Headers: getHeaders(""),
|
|
}, 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.rdoRequest("/oauth2/access_token", requestOptions{
|
|
HttpMethod: http.MethodPost,
|
|
Body: request,
|
|
Headers: getHeaders(""),
|
|
}, 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
|
|
}
|
|
|
|
func (api *Client) Get(resource string, result interface{}, abs ...bool) error {
|
|
var a bool
|
|
if len(abs) > 0 {
|
|
a = abs[0]
|
|
}
|
|
return api.doRequest(resource, requestOptions{
|
|
HttpMethod: http.MethodGet,
|
|
Body: nil,
|
|
Headers: getHeaders(api.options.AccessToken),
|
|
Abs: a,
|
|
}, result)
|
|
}
|
|
|
|
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
|
|
}
|