package api import ( "bytes" "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "net/url" "io" "os" "time" ) 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 { options *ClientOptions BaseUrl *url.URL secretStoreFilePath string Debug bool `json` } 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) { ret := &Client{ secretStoreFilePath: secretPath, } options, err := ret.readSecret() if err != nil { return nil, err } fmt.Println("opts:", options) if options.Url == "" || options.RedirectUrl == "" { return nil, errors.New("AmoCrm: Invalid options: Url") } resolvedUrl, err := url.Parse(options.Url) if err != nil { return nil, err } ret.BaseUrl = resolvedUrl if err != nil { return nil, err } ret.options = options var ( //pair *TokenPair exchangeErr error exchanged bool ) if ret.options.AccessToken == "" && ret.options.RefreshToken == "" { if ret.options.ClientSecret == "" || ret.options.ClientId == "" || ret.options.AuthCode == "" { return nil, errors.New("AmoCrm: invalid options: ExchangeAuth") } _, exchangeErr = ret.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 = ret.RefreshToken() if err != nil { return nil, err } } return &Client{ options: options, BaseUrl: resolvedUrl, }, 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 { fmt.Printf("\nAmo request: %+v\nAmo repsonse: %+v\n", request, response) } if response == nil { return NoInternetErr } if err != nil { return errors.New(fmt.Sprintf( "Request error: %s %d %s %s", err.Error(), response.StatusCode, requestParams.HttpMethod, resourceUrl, )) } defer response.Body.Close() if response.StatusCode == 204 { return NoContentErr } if response.StatusCode >= 400 { if response.StatusCode == 401 { return NoAuthErr } bodyBytes, _ := ioutil.ReadAll(response.Body) return errors.New(fmt.Sprintf( "%s %d %s %s", string(bodyBytes), response.StatusCode, requestParams.HttpMethod, resourceUrl, )) } if result != nil { decoder := json.NewDecoder(response.Body) err = decoder.Decode(result) if err != nil { 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 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 }