Exception: You do not have permission to call DriveApp.createFile - javascript

I created a custom script to be run from google sheets which I need to create a file. The script uses the following code excerpt:
/*
Custom function to call IEXAPI IEXkeystatearningsdate
#customfunction
*/
function IEXkeystatearningsdate(inputsymbol, stat, version) {
if (version == "Sandbox" || version == "sandbox")
var url="https://sandbox.iexapis.com/stable/stock/"+inputsymbol+"/stats/"+stat+"?token=xyz";
else
var url="https://cloud.iexapis.com/stable/stock/"+inputsymbol+"/stats/"+stat+"?token=xyz";
var response=UrlFetchApp.fetch(url); //Call REST API
var json=response.getContentText();
var data = JSON.parse(json); //Parse into JSON object
var d = new Date();
var n = d.toLocaleString();
var fn = "IEXkeystatearningsdate_" + n;
DriveApp.createFile(fn,inputsymbol, MimeType.CSV);
return (data);
}
However, I receive this message:
"Exception: You do not have permission to call DriveApp.createFile. Required permissions: https://www.googleapis.com/auth/drive (line 20)"
When I run this script directly from the script editor, I don't receive this message.
This is my manifest file:
{
"oauthScopes": [
"https://www.googleapis.com/auth/drive"
],
"timeZone": "America/Los_Angeles",
"dependencies": {
},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8"
}
I do not use G-Suite. I only use google/sheets for my personal use. The OAuth FAQ says this call should be allowed for personal use. Can someone help me with what I need to do to get this to work?

From your error description I can assume that you are running your script on a simple onEdit trigger.
Simple onEdit trigger cannot access services that require authorization.
See restrictions.
For you it means that you can perform to DriveApp on simple trigger.
Solution:
Use an Installable trigger instead.
To do so:
Rename the function onEdit() to something else that is not a key word
Go on Edit -> Current project's triggers
Click on + to create a new trigger
Specify the funciton to run
Set Select event type to On edit

Custom functions cannot be used to call services that access personal data:
Unlike most other types of Apps Scripts, custom functions never ask users to authorize access to personal data. Consequently, they can only call services that do not have access to personal data
That, of course, includes DriveApp. And it also includes things like getOAuthToken() (it returns null when called via custom functions), so calling Drive API directly through UrlFetch cannot work either.
You should either install an onEdit trigger, as ziganotschka suggested, or call this function through a custom menu instead of a custom function, so that you're asked for authorization when trying to run it.
Reference:
Using Apps Script services

Related

Using GIS (Google Identity Services) and API Subpackage (picker) without client package

I am currently replacing the gapi.oauth2 package, by using the TokenClient according to the guide and everything works fine.
global.google.accounts.oauth2
.initTokenClient({
client_id: CONFIG.google.clientId,
scope: 'https://www.googleapis.com/auth/drive.readonly',
ux_mode: 'popup',
callback(tokenResponse) {
if (tokenResponse && !tokenResponse.error) {
onSuccess(tokenResponse.access_token);
return;
}
onError(tokenResponse.error || 'google authentication failed');
},
})
.requestAccessToken({});
The only issue is that we are not using the gapi.client and would prefer avoiding loading that package as we are only using the token to show a picker by using google.picker.PickerBuilder.
Now after the initialization the GSI package tries to use gapi.client.setToken() which obviously fails as the package is not loaded.
[GSI_LOGGER-TOKEN_CLIENT]: Set token failed. Gapi.client.setToken undefined.
So now i could not find anything in the reference on how to prevent that call to happen, nor how to at least suppress the warning by not e.g hacking in a noop as a placeholder.
Does anyone know if there is any official way to deal with that ?
In my tests the access token is successfully returned to the callback handler.
It looks to be an incorrectly generated warning, annoying yes, but functionally you're okay. A later, official release should remove the warning with no action on your part required.
The new API of GIS (Google Identity Services) maybe remove the setToken of the gapi.
I had to fix it by the code.
gapi = {client: {setToken: function(){ return; }}};

Monitor google sheet change event externally [duplicate]

What I wish to achieve:
Whenever a cell is changed in any google sheet on my shared drive (by
any user on the domain) I want to call an API endpoint and include
information about which cell was edited.
My approach:
I believe Google App Scripts Add-on is what I need. Installed for all users on the domain.
I see there are "bound" scripts and standalone scripts. For standalone scripts I am not able to create any other triggers than timer and calender based triggers. Bound scripts seem to be permanently bound to a single sheet and won't impact other sheets in any way.
What am I missing?
I find a few end-to-end tutorials on blogs for making bound scripts, but nothing for generic cross-domain stuff.
You can achieve all this through a standalone script. Create a standalone script and follow these steps:
Step 1: Get spreadsheet ids
First you would have to get the id of the different Spreadsheets in your shared drive. You can do it in Google Apps Script itself if you use the Advanced Drive Service (see Reference below). To activate this service, go to Resources > Advanced Google services... in your script editor and enable Drive API.
Then, write a function that will return an array of the spreadsheet ids in the shared drive. You will have to call Drive.Files.list for that. It could be something along the following lines (please write your shared driveId in the corresponding line):
function getFileIds() {
var params = {
corpora: "drive",
driveId: "your-shared-drive-id", // Please change this accordingly
includeItemsFromAllDrives: true,
q: "mimeType = 'application/vnd.google-apps.spreadsheet'",
supportsAllDrives: true
}
var files = Drive.Files.list(params)["items"];
var ids = files.map(function(file) {
return file["id"];
})
return ids;
}
Step 2: Create triggers for each spreadsheet
Install an onEdit trigger programmatically for each of the spreadsheets (an edit trigger fires a function every time the corresponding spreadsheet is edited, so I assume this is the trigger you want). For this, the ids retrieved in step 1 will be used. It could be something similar to this:
function createTriggers(ids) {
ids.forEach(function(id) {
var ss = SpreadsheetApp.openById(id);
createTrigger(ss);
})
}
function createTrigger(ss) {
ScriptApp.newTrigger('sendDataOnEdit')
.forSpreadsheet(ss)
.onEdit()
.create();
}
The function createTriggers gets an array of ids as a parameter and, for each id, creates an onEdit trigger: everytime any of these spreadsheets is edited, the function sendDataOnEdit will run, and that's where you will want to call your API endpoint with information about the edited cell.
Step 3: Call API endpoint
The function sendDataOnEdit has to get data from the edited cell and send it somewhere.
function sendDataOnEdit(e) {
// Please fill this up accordingly
var range = e.range;
var value = range.getValue();
UrlFetchApp.fetch(url, params) // Please fill this up accordingly
}
First, it can get information about the cell that was edited via the event object, passed to the function as the parameter e (you can get its column, its row, its value, the sheet and the spreadsheet where it is located, etc.). For example, to retrieve the value of the cell you can do e.range.getValue(). Check the link I provide in reference to get more details on this.
Second, when you have correctly retrieved the data you want to send, you can use UrlFetchApp.fetch(url, params) to make a request to your URL. In the link I provide below, you can see the parameters you can specify here (e.g., HTTP method, payload, etc.).
Please bear in mind that you might need to grant some authorization to access the API endpoint, if this is not public. Check the OAuth reference I attach below.
(You have to edit this function accordingly to retrieve and send exactly what you want. What I wrote is an example).
Summing this up:
In order to create the triggers you should run createTriggers once (if you run it more times, it will start creating duplicates). Run for example, this function, that first gets the file ids via Drive API and then creates the corresponding triggers:
function main() {
var ids = getFileIds();
createTriggers(ids);
}
Also, it would be useful to have a function that will delete all the triggers. Run this in case you want to start from fresh and make sure you don't have duplicates:
function deleteTriggers() {
var triggers = ScriptApp.getProjectTriggers();
triggers.forEach(function(trigger) {
ScriptApp.deleteTrigger(trigger);
})
}
Reference:
Advanced Drive Service
Drive.Files.list
onEdit trigger
Install trigger programmatically
onEdit event object
UrlFetchApp.fetch(url, params)
Connecting to external APIs
OAuth2 for Apps Script
ScriptApp.deleteTrigger(trigger)
I hope this is of any help.

RingCentral JavaScript SDK Handle Redirect URI points to local JavaScript Function

RingCentral JavaScript SDK handle redirect URI point to local JavaScript function
As-per Doc, they give option
RingCentral.SDK.handleLoginRedirect()
but dont know how to use that
var SDK = require('ringcentral');
rcsdk = new SDK({
server: SDK.server.sandbox,
appKey: '_app_key',
appSecret: 'app_password',
redirectUri: ''
})
function handleredirectURI(){
//handle redirections
}
We need points out our handleredirectURI function
Thanks in advance
Per the documentation. The context is 3-legged oAuth. Here is a demo: https://github.com/ringcentral/ringcentral-demos-oauth/tree/master/javascript
However the demo doesn't use the handleredirectURI method. Which means that the method is simply an utility method and it's not required to use it.
For the usage of handleredirectURI, I will come back and update my answer later.
Update
Here is the source code for handleredirectURI: https://github.com/ringcentral/ringcentral-js/blob/669b7d06254d3620c5a5f24c94b401aa862be948/src/SDK.js#L115-L124
You can see that the method parses win.location to get some useful data and postMessage back to its opener.
Unit tests for handleredirectURI: https://github.com/ringcentral/ringcentral-js/blob/669b7d06254d3620c5a5f24c94b401aa862be948/src/SDK-spec.js#L27-L63
Update 2
I read handleredirectURI' source code, unit tests, sample code again and I think its usage is just like what is written in its documentation:
Popup Setup
This setup is good when your app is rendered as a widget on a third-party sites.
If you would like to simply open RingCentral login pages in a popup, you may use the following short-hand in your app's login page:
var platform = rcsdk.platform();
var loginUrl = platform.loginUrl({implicit: true}); // implicit parameter is optional, default false
platform
.loginWindow({url: loginUrl}) // this method also allows to supply more options to control window position
.then(function (loginOptions){
return platform.login(loginOptions);
})
.then(...)
.catch(...);
In this case your landing page (the one to which Redirect URI points) need to call the following code:
RingCentral.SDK.handleLoginRedirect();
Explained:
Run the first code snippet to open the login popup.
In the redirected to page, run the second code snippet (that one line of code) to get everything else done.
After that the authorization part is done and you can invoke other APIs.
Let me know if you have more questions.

Google spreadsheet get parameters from URL

I have an app, that links one service with Google Sheets by OAuth 1.0.
I click "login" in addon menu, send signature and callback domain (current sheet). Then in service I click on button, get request token and it returns me to the specified domain with parameters.
function onOpen(e)
{
Logger.log( SpreadsheetApp.getActiveSpreadsheet().getUrl() );
Logger.log( e.source.getUrl() );
}
But .getUrl() doesn't contain them.
According to >this< I can't use doGet(e) in Sheets and, because of OAuth, I can't use Web App, because I still need to pass these parameters to Sheets.
I tried to get it on client-side by window.top.location.href, but had cross-domain errors.
Question: Can I get it? Or is it impossible?
So, a partial answer to my question:
We need 2 different scripts. The first as Sheets web addon, the second as Web App. Next, we need to implement all the steps presented in the screenshot:
1) After steps 0, 1, 2 Service Provider should redirect us to Helper script (Web App) with the following code:
function doGet(e)
{
var token = e.parameter.oauth_token,
code = e.parameter.oauth_verifier,
response;
if(token && code && Root.setVerificationCode(token, code))
response = "Success";
else
response = 'Error';
return HtmlService.createHtmlOutput(response);
}
Here we retrieving GET parameters and send them to Sheets web addon (Root).
Click Resources -> Libraries and add your addon as library. Root is just an identifier.
2) Sheets addon code:
function setVerificationCode(token, code)
{
var oauth = new OAuth();
if(oauth.getAccessToken(code))
{
// change menu here
return true;
}
else
return false;
}
These are full steps you need to implement OAuth 1.0.
P.S. Now I have problems with changing menu, but it'll be my next question.

Retrieving an object/resource from a URL

I recently started using Google Apps Script to automate some Google Analytics reporting tasks. Many objects returned by Google Analytics Services have 'get' functions that return a URL in the form of a string. Actually, many Google Apps Script objects have functions that return these resource URLs, not just Google's Analytics Services. The indication is that I can somehow use these URLs to reference Google Apps resources and get objects in return, but I don't know how.
I tried simply loading one of these URLs in a browser expecting JSON or something else I could use, but received a 404 error instead. The same happened when I tried requesting the URL using a Google Apps Script UrlFetchApp.
Here's a simple example:
var accountId = '0123456789'; // Pretend this is a valid account number
// Get the first WebProperty
var webProp = Analytics.Management.Webproperties.list(accountId).getItems()[0];
// ...getHref() is along the lines of "https://www.googleapis.com/analytics/v3/management/accounts/0123456789"
var parentHref = webProp.getParetLink().getHref();
The question is, what do I do with parentHref to get an Analytics Account object back? I feel like I'm missing something that should be fairly basic...
Resources:
Google Apps Script Reference for Analytics Services
Read Using OAuth 2.0 to Access Google APIs, will help you get what you need.
I add a very basic code that can serve as a guide.
/* CODE FOR DEMONSTRATION PURPOSES */
function authorizeGAS() {
var result = false;
var oauthConfig = UrlFetchApp.addOAuthService("google");
oauthConfig.setAccessTokenUrl("https://www.google.com/accounts/OAuthGetAccessToken");
oauthConfig.setRequestTokenUrl("https://www.google.com/accounts/OAuthGetRequestToken?" +
"scope=https://www.googleapis.com/auth/analytics.readonly");
oauthConfig.setAuthorizationUrl("https://www.google.com/accounts/OAuthAuthorizeToken");
oauthConfig.setConsumerKey(ScriptProperties.getProperty("YOUR_ANALYTICS_CONSUMER_KEY"));
oauthConfig.setConsumerSecret(ScriptProperties.getProperty("YOUR_ANALYTICS_CONSUMER_SECRET"));
var requestData = {
"method": "GET",
"oAuthServiceName": "google",
"oAuthUseToken": "always"
};
var URL = "https://www.googleapis.com/analytics/v3/management/accounts/~all/webproperties/~all/profiles";
try {
result = JSON.parse(UrlFetchApp.fetch(URL, requestData).getContentText());
} catch (e) {
if (e.message) Logger.log(e.message);
}
Logger.log(result);
return result;
}
It's not about activating the API. It's obviously active since he is already getting analytics data.
You can't just urlfetch those URLs, because you are missing the OAuth-related code that goes with calling a URL that requires authentication and permission.

Categories