How to Use Google Cloud APIs with Apps Script – Sample Application

The Google Cloud Vision API helps you identify text, objects and places inside pictures. The images may be hosted on a public website, you could store them inside a Google Cloud Storage bucket or you can encode the images to a base64 string.

This sample applications will help you understand how to interact with the Google Cloud Vision API using Google Apps Script. To get started, create a new Google Script. Go to Resources > Cloud Platform Project > View API Console and enable the Google Cloud Vision API.

Also see: Dummies Guide to Google OAuth 2
Inside the Google APIs dashboard, go to Credentials > Create Credentials > OAuth Client ID and choose Web Application as the type of application. Put https://script.google.com under Authorized JavaScript Origins.

For the Authorized Redirect URIs, go to the Script, run getGoogleCallbackUrl and you will find the URL inside the logs section.

// 1. Use this Callback Url with your Google Project
function getGoogleCallbackURL(silent) {
  var url = ScriptApp.getService().getUrl();
  var callbackUrl = (url.indexOf('/exec') >= 0 ? url.slice(0, -4) : url.slice(0, -3)) + 'usercallback';
  if (!silent) Logger.log(callbackUrl);
  return callbackUrl;
}

Save the Oauth2 Client and make a note of the Google Client Id and the Client Secret. Put them in the storeGoogleCredentials() function, run the function to save the credentials in the property store and then remove the values from the script.

// 2. Store the Client ID and Client Secret in the Property Store
function storeGoogleCredentials() {    
  resetSettings_();
  getPropertyStore_().setProperties({
    "client_id": "123.apps.googleusercontent.com",
    "client_secret": "googleClientSecret"
  });
}

Publish the script as a web app and open the app URL in a new tab. It will require authorization once and then store the refresh token in the property store.

// 3. Get the Oauth URL to authorize the app
function doGet(e) {
  
  var propertyStore = getPropertyStore_();
  
  if (!propertyStore.getProperty('refresh_token')) {
    
    var stateToken = ScriptApp
    .newStateToken()
    .withMethod('googleCallback')
    .withArgument('name', 'value')
    .withTimeout(2000)
    .createToken();
    
    var params = {
      state: stateToken,
      scope: [
        "https://www.googleapis.com/auth/cloud-platform", 
        "https://www.googleapis.com/auth/cloud-vision"
      ].join(" "),
      client_id: propertyStore.getProperty('client_id'),
      redirect_uri: getGoogleCallbackURL(true),
      response_type: 'code',
      access_type: 'offline',
      approval_prompt: 'force'
    };
    var queryString = Object.keys(params).map(function (e) {
      return e + '=' + encodeURIComponent(params[e]);
    }).join("&");
    
    var url = 'https://accounts.google.com/o/oauth2/auth?' + queryString;
    return HtmlService.createHtmlOutput("<a href='URL' target='_blank'>Click here to authorize</a>".replace("URL", url));
  } else {
    return HtmlService.createHtmlOutput("ctrlq.org app is authorized");
  }
}

// Exchange Authorization code with Access Token
function googleCallback(e) {
  
  var propertyStore = getPropertyStore_();
  var props = propertyStore.getProperties();
  
  var credentials = makeHttpPostRequest_(
    "https://accounts.google.com/o/oauth2/token", {
      code: e.parameter.code,
      redirect_uri: getGoogleCallbackURL(true),
      client_id: props.client_id,
      client_secret: props.client_secret,
      grant_type: "authorization_code"
    });
  
  if (!credentials.error) {
    cacheAccessToken_(credentials.access_token);
    propertyStore.setProperty('refresh_token', credentials.refresh_token);
    return HtmlService.createHtmlOutput("OK");
  }
  
  return HtmlService.createHtmlOutput(credentials.error);
}

If you get an invalid_scope error saying “You don’t have permission to access some scopes. Your project is trying to access scopes that need to go through the verification process.” – you’ll have to submit a request using our OAuth Developer Verification form.

The access token is stored in the cache as it is valid for 3600 seconds and a new token can be requested using the refresh token.

// The access token is in cache and can be requested using the refresh token
function getAccessToken_() {
  var accessToken = getCacheStore_().get("access_token");
  if (!accessToken) {
    accessToken = refreshAccessToken_();
  }
  return accessToken;
}

function cacheAccessToken_(accessToken) {
  // Cache for 55 minutes, token otherwise valid for 60 minutes
  getCacheStore_().put("access_token", accessToken, 3300);
}

function refreshAccessToken_() {
  
  var props = getPropertyStore_().getProperties();
  var response = makeHttpPostRequest_(
    "https://accounts.google.com/o/oauth2/token", {
      client_id: props.client_id,
      client_secret: props.client_secret,
      refresh_token: props.refresh_token,
      grant_type: "refresh_token"
    });
  
  if (response.hasOwnProperty("access_token")) {
    cacheAccessToken_(json.access_token);
    return json.access_token;
  }
  
  return null;  
}

Now that our basic setup is in place, we can make a call to the Cloud Vision API with a simple HTTP POST request. The authorization headers should include the bearer access token.

function CloudVisionAPI(imageUrl) {
  var imageBytes = UrlFetchApp.fetch(imageUrl).getContent();
  var payload = JSON.stringify({
    requests: [{
      image: {
        content: Utilities.base64Encode(imageBytes)
      },
      features: [{
          type: "LABEL_DETECTION", 
          maxResults: 3
      }]
    }]
  });
  
  var requestUrl = 'https://vision.googleapis.com/v1/images:annotate';
  var response = UrlFetchApp.fetch(requestUrl, {
    method: 'POST',
    headers: {
      authorization: 'Bearer ' + getAccessToken_()
    },
    contentType: 'application/json',
    payload: payload,
    muteHttpExceptions: true
  }).getContentText();

  Logger.log(JSON.parse(response));

}

The refresh token will remain valid until access hasn’t been revoked by the user.

function revokeAccess() {
  var propertyStore = getPropertyStore_();
  var accessToken = getAccessToken_();
  if (accessToken !== null) {
    var url = "https://accounts.google.com/o/oauth2/revoke?token=" + accessToken;
    var res = UrlFetchApp.fetch(url, {
      muteHttpExceptions: true
    });
  }
  resetSettings_();
}

And here are a couple of helper utility functions for accessing the cache and property store.


function getCacheStore_() {
  return CacheService.getScriptCache();
}

function getPropertyStore_() {
  return PropertiesService.getScriptProperties();
}

function resetSettings_() {
  getPropertyStore_().deleteAllProperties();
  getCacheStore_().remove("access_token")
}

function makeHttpPostRequest_(url, payload) {
  try {
    var response = UrlFetchApp.fetch(url, {
      method: "POST",
      payload: payload,
      muteHttpExceptions: true
    }).getContentText();  
    return JSON.parse(response);
  } catch (f) {
    Logger.log(f.toString());
  }
  return {};
}

The access tokens expire every 60 minutes. You can also make an HTTPS POST or GET request to the tokeninfo endpoint to know about the validity, scope and expiry of the token.

googleapis.com/oauth2/v3/tokeninfo?access_token=ACCESSTOKEN