mirror of
https://github.com/ente-io/ente.git
synced 2025-06-26 03:40:35 +00:00
156 lines
4.5 KiB
Go
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
|
|
}
|