1

I am trying to generate Google Slides from Google Sheets; have used Sheets script with no issues, but when I try to include Google Slides, after authenticating and getting Oauth permissions prompt, I am getting this error that I cannot find any reference for; I have made sure Google Slides API and Drive API are enabled in the Developers Console.

"Request failed for https://slides.googleapis.com/v1/presentations/... returned code 403. Truncated server response: { "error": { "code": 403, "message": "Google Slides API has not been used in project project-id-... before or it is disab... (use muteHttpExceptions option to examine full response) (line 93, file "Code")"

The code failing is as follows, the function that is failing was copied from How to download Google Slides as images?. Client ID and secret are defined, ommitted just for security

// from https://mashe.hawksey.info/2015/10/setting-up-oauth2-access-with-google-apps-script-blogger-api-example/

function getService() {
  // Create a new service with the given name. The name will be used when
  // persisting the authorized token, so ensure it is unique within the
  // scope of the property store.
  return OAuth2.createService('slidesOauth')

      // Set the endpoint URLs, which are the same for all Google services.
      .setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
      .setTokenUrl('https://accounts.google.com/o/oauth2/token')


      // Set the client ID and secret, from the Google Developers Console.
      .setClientId(CLIENT_ID)
      .setClientSecret(CLIENT_SECRET)

      // Set the name of the callback function in the script referenced
      // above that should be invoked to complete the OAuth flow.
      .setCallbackFunction('authCallback')

      // Set the property store where authorized tokens should be persisted.
      .setPropertyStore(PropertiesService.getUserProperties())

      // Set the scopes to request (space-separated for Google services).
      // this is blogger read only scope for write access is:
      // https://www.googleapis.com/auth/blogger
      .setScope('https://www.googleapis.com/auth/blogger.readonly')

      // Below are Google-specific OAuth2 parameters.

      // Sets the login hint, which will prevent the account chooser screen
      // from being shown to users logged in with multiple accounts.
      .setParam('login_hint', Session.getActiveUser().getEmail())

      // Requests offline access.
      .setParam('access_type', 'offline')

      // Forces the approval prompt every time. This is useful for testing,
      // but not desirable in a production application.
      .setParam('approval_prompt', 'force');
}

function authCallback(request) {
  var oauthService = getService();
  var isAuthorized = oauthService.handleCallback(request);
  if (isAuthorized) {
    return HtmlService.createHtmlOutput('Success! You can close this tab.');
  } else {
    return HtmlService.createHtmlOutput('Denied. You can close this tab');
  }
}

// from https://stackoverflow.com/questions/31662455/how-to-download-google-slides-as-images/40678925#40678925

function downloadPresentation(id) {
  var slideIds = getSlideIds(id); 

  for (var i = 0, slideId; slideId = slideIds[i]; i++) {
    downloadSlide('Slide ' + (i + 1), id, slideId);
  }
}
function downloadSlide(name, presentationId, slideId) {
  var url = 'https://docs.google.com/presentation/d/' + presentationId +
    '/export/png?id=' + presentationId + '&pageid=' + slideId; 
  var options = {
    headers: {
      Authorization: 'Bearer ' + getService().getAccessToken()
    }
  };
  var response = UrlFetchApp.fetch(url, options); // This is the failing line 93
  var image = response.getAs(MimeType.PNG);
  image.setName(name);
  DriveApp.createFile(image);
}
Community
  • 1
  • 1
Bruno Guardia
  • 453
  • 4
  • 14
  • Are the APIs enabled for the correct project ID? Select from the project dropdown in the Developer Console? (I mention this as I have had this error before from enabling APIs in the wrong project) – Bardy Dec 28 '16 at 21:33
  • Yes, it is the only project in this account, and took some time to figure out how to enable the APIs for the project. But thanks for the suggestion – Bruno Guardia Dec 28 '16 at 22:50

3 Answers3

1

EDIT: I got this working with this code snippet:

var CLIENT_ID = '...';
var CLIENT_SECRET = '...';
var PRESENTATION_ID = '...';

// from https://mashe.hawksey.info/2015/10/setting-up-oauth2-access-with-google-apps-script-blogger-api-example/

function getService() {
  // Create a new service with the given name. The name will be used when
  // persisting the authorized token, so ensure it is unique within the
  // scope of the property store.
  return OAuth2.createService('slidesOauth')

      // Set the endpoint URLs, which are the same for all Google services.
      .setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
      .setTokenUrl('https://accounts.google.com/o/oauth2/token')


      // Set the client ID and secret, from the Google Developers Console.
      .setClientId(CLIENT_ID)
      .setClientSecret(CLIENT_SECRET)

      // Set the name of the callback function in the script referenced
      // above that should be invoked to complete the OAuth flow.
      .setCallbackFunction('authCallback')

      // Set the property store where authorized tokens should be persisted.
      .setPropertyStore(PropertiesService.getUserProperties())

      // Set the scopes to request (space-separated for Google services).
      .setScope('https://www.googleapis.com/auth/drive')

      // Below are Google-specific OAuth2 parameters.

      // Sets the login hint, which will prevent the account chooser screen
      // from being shown to users logged in with multiple accounts.
      .setParam('login_hint', Session.getActiveUser().getEmail())

      // Requests offline access.
      .setParam('access_type', 'offline')

      // Forces the approval prompt every time. This is useful for testing,
      // but not desirable in a production application.
      .setParam('approval_prompt', 'force');
}

function authCallback(request) {
  var oauthService = getService();
  var isAuthorized = oauthService.handleCallback(request);
  if (isAuthorized) {
    return HtmlService.createHtmlOutput('Success! You can close this tab.');
  } else {
    return HtmlService.createHtmlOutput('Denied. You can close this tab');
  }
}

function getSlideIds(presentationId) {
  var url = 'https://slides.googleapis.com/v1/presentations/' + presentationId;
  var options = {
    headers: {
      Authorization: 'Bearer ' + getService().getAccessToken()
    }
  };
  var response = UrlFetchApp.fetch(url, options);

  var slideData = JSON.parse(response);
  return slideData.slides.map(function(slide) {
    return slide.objectId;
  });
}


// from http://stackoverflow.com/questions/31662455/how-to-download-google-slides-as-images/40678925#40678925

function downloadPresentation(id) {
  var slideIds = getSlideIds(id); 

  for (var i = 0, slideId; slideId = slideIds[i]; i++) {
    downloadSlide('Slide ' + (i + 1), id, slideId);
  }
}

function downloadSlide(name, presentationId, slideId) {
  var url = 'https://docs.google.com/presentation/d/' + presentationId +
    '/export/png?id=' + presentationId + '&pageid=' + slideId; 
  var options = {
    headers: {
      Authorization: 'Bearer ' + getService().getAccessToken()
    }
  };
  var response = UrlFetchApp.fetch(url, options); // This is the failing line 93
  var image = response.getAs(MimeType.PNG);
  image.setName(name);
  DriveApp.createFile(image);
}

function start() {
  var service = getService();
  var authorizationUrl = service.getAuthorizationUrl();
  Logger.log('Open the following URL and re-run the script: %s',
      authorizationUrl);

  if (service.hasAccess()) {
    downloadPresentation(PRESENTATION_ID);
  }
}

I'd guess that the client ID and secret don't come from project you think they come from. You can verify this by visiting your project's credentials page and seeing if there's a matching client ID listed under 'OAuth 2.0 client IDs'. The project containing that client ID needs to have the Slides API enabled.

Also note: the /export/png endpoint you're using isnt a documented/supported Google API so it may be renamed or break in the future. If you're interested in an official API for getting rendered PNGs of slides via the Slides API, follow along with this issue on the tracker.


Previous content:

Your code is also slightly different than the snippet you're copying from. It's using ScriptApp.getOAuthToken() to get the value of the authorization header, but you're calling a different getService().getAccessToken() function. That looks like you're using the apps-script-oauth2 library to generate your OAuth token. If that's the case, confirm that the Slides API is enabled on the Developer console project that generated the clientId and client secret you're passing in to OAuth2.createService, as its not necessarily the same project attached to your script. If switching to ScriptApp.getOAuthToken() is an option for you, that may work as well.

If that doesnt fix your issue, mind providing more of your code? The snippet you've pasted doesn't seem to match the error message, as your code appears to be making a request to docs.google.com, not slides.googleapis.com as mentioned in the error.

Maurice Codik
  • 598
  • 2
  • 6
  • Added more code; as you say, I substituted the ScriptApp call for a function that allowed to make sure I controlled the clientId and client secret. The snippet is correct, I am requesting from docs.google.com as the slides I want to use as template are at https://docs.google.com/presentation/d/1V7k5UTj... but seems this causes a redirect. – Bruno Guardia Dec 28 '16 at 22:57
  • @Bruno, mind adding the part that actually calls `downloadSlide`? I can try to reproduce with your whole script. I still suspect that the client ID / secret belong to a project you dont expect that doesnt have the API enabled. – Maurice Codik Dec 29 '16 at 14:12
  • You are right, missed the function that calls downloadSlide, while it does not quite relevant to the problem. The main function just calls downloadSlides with a valid ID (verified twice, last time just added the ID to https://docs.google.com/presentation/d/ and it works properly). I have also double checked the project, it is the only project associated to the account, and it reports both Google Drive and Google Slides API are enabled. I still think the key for the problem is the error message, on the side of "first time used", not disabled. – Bruno Guardia Dec 29 '16 at 18:34
  • @Bruno, I got this working with [this code](https://gist.github.com/anonymous/0d3b518028215d636291fe6e10e78706). The project my client ID and secret came from has the Slides API enabled. The only changes I made from your original code was filling in some boilerplate and changing the requested scope to drive (not blogger). The issue is definitely your client ID and client secret; I'd guess they dont come from the project you think they are coming from (check the project's [API credentials page](https://console.developers.google.com/apis/credentials) to confirm) – Maurice Codik Dec 29 '16 at 22:09
  • Thanks Maurice. Your code failed same as mine, but the logging for the Authorization URL allowed for finding the issue: Error: redirect_uri_mismatch The redirect URI in the request, https://script.google.com/macros/d/.../usercallback, does not match the ones authorized for the OAuth client. Visit https://console.developers.google.com/apis/credentials/oauthclient/....apps.googleusercontent.com?project=... to update the authorized redirect URIs. – Bruno Guardia Dec 30 '16 at 18:01
  • Correcting the RedirectURL made both your code and mine work seamlessly. The error message was misguiding what was the solution. Thanks a lot for the advice and the helpful logging! – Bruno Guardia Dec 30 '16 at 18:02
0

Short version of the solution: Thanks to Maurice Codik efforts I got both his code and mine working.

The issue was with the Authorized redirect URIs setup in the OAuth credentials, which had to be setup to

https://script.google.com/macros/d/[ScriptID]/usercallback

Bruno Guardia
  • 453
  • 4
  • 14
-1

This is not a direct answer to the OP question but does directly address the 1st part of their 1st sentence, which is, "I am trying to generate Google Slides from Google Sheets...." This is the exact use case I created a video (and accompanying blog post[s]) for. NOTE: the payload in the post is JSON, but the full example from the video is in Python, so non-Python devs can simply use it as pseudocode.)

wescpy
  • 10,689
  • 3
  • 54
  • 53