2024-03-01 13:37:01 +05:30

156 lines
4.5 KiB
Go

// The zoho package contains wrappers for the (generic) Zoho API.
//
// These are stateless functions that wrap over the HTTP calls that need to be
// made to obtain data from the Zoho HTTP API. In particular, they contain the
// code for dealing with access tokens and their renewal.
package zoho
import (
"encoding/json"
"fmt"
"io"
"net/http"
"github.com/ente-io/stacktrace"
log "github.com/sirupsen/logrus"
)
// The minimum credentials we need to obtain valid access tokens.
//
// To generate these credentials:
//
// Create new client in https://api-console.zoho.com/. Use client type
// "Self-client". This gives us the client id and client secret.
//
// Generate an (authorization) code with scope "ZohoCampaigns.contact.WRITE" and
// portal "Campaigns".
//
// Use this authorization code to obtain a refresh/access token pair. Note that
// we don't have a redirect_uri, so we just use the dummy one that is given in
// their documentation examples elsewhere.
// (https://www.zoho.com/accounts/protocol/oauth/web-apps/access-token.html)
//
// curl -X POST \
// 'https://accounts.zoho.com/oauth/v2/token? \
// client_id=xxx&grant_type=authorization_code&client_secret=yyy \
// &redirect_uri=https://www.zylker.com/oauthredirect&code=zzz'
//
// Save the refresh token. We can later use it to regenerate the access token
// (Zoho access tokens have a short, 1 hour validity anyway).
type Credentials struct {
ClientID string
ClientSecret string
RefreshToken string
}
// Do an HTTP `method` request to `url` using the given accessToken.
//
// If the accessToken has expired, use the given credentials to renew it.
//
// Return the accessToken (renewed or original) that gets used, and any errors
// that occurred. If the API returns `status` "success", then error will be nil.
func DoRequest(method string, url string, accessToken string, credentials Credentials) (string, error) {
ar, err := doRequestNoRetry(method, url, accessToken, credentials)
if err != nil {
return accessToken, stacktrace.Propagate(err, "")
}
// Code 1007 indicates that the access token has expired
// ("message":"Unauthorized request.")
if ar.Status == "error" && ar.Code == "1007" {
accessToken, err = renewAccessToken(credentials)
if err != nil {
return accessToken, stacktrace.Propagate(err, "")
}
// Try again
ar, err = doRequestNoRetry(method, url, accessToken, credentials)
if err != nil {
return accessToken, stacktrace.Propagate(err, "")
}
}
if ar.Status == "success" {
return accessToken, nil
}
// Something else went wrong
return accessToken, stacktrace.NewError(
"Zoho API returned an non-success status %s (code %s: %s)",
ar.Status, ar.Code, ar.Message)
}
// The basic generic fields that we expect in a response from Zoho APIs
type genericAPIResponse struct {
Status string `json:"status"`
Code string `json:"Code"`
Message string `json:"message"`
}
func doRequestNoRetry(method string, url string, accessToken string, credentials Credentials) (genericAPIResponse, error) {
var ar genericAPIResponse
client := &http.Client{}
req, err := http.NewRequest(method, url, nil)
if err != nil {
return ar, stacktrace.Propagate(err, "")
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", accessToken))
res, err := client.Do(req)
if err != nil {
return ar, stacktrace.Propagate(err, "")
}
if res.Body != nil {
defer res.Body.Close()
}
body, err := io.ReadAll(res.Body)
if err != nil {
return ar, stacktrace.Propagate(err, "")
}
log.Infof("Zoho %s %s response: %s", method, url, body)
err = json.Unmarshal(body, &ar)
return ar, stacktrace.Propagate(err, "")
}
// Obtain a new access token using the given credentials
func renewAccessToken(credentials Credentials) (string, error) {
// https://www.zoho.com/crm/developer/docs/api/v3/refresh.html
url := fmt.Sprintf(
"https://accounts.zoho.com/oauth/v2/token?refresh_token=%s&client_id=%s&client_secret=%s&grant_type=refresh_token",
credentials.RefreshToken, credentials.ClientID, credentials.ClientSecret)
type jsonResponse struct {
AccessToken string `json:"access_token"`
}
res, err := http.Post(url, "application/json", nil)
if err != nil {
return "", stacktrace.Propagate(err, "")
}
if res.Body != nil {
defer res.Body.Close()
}
body, err := io.ReadAll(res.Body)
if err != nil {
return "", stacktrace.Propagate(err, "")
}
var jr jsonResponse
err = json.Unmarshal(body, &jr)
if err != nil {
return "", stacktrace.Propagate(err, "")
}
log.Infof("Renewed Zoho access token")
return jr.AccessToken, nil
}