Google Apps Script - call function deployed as API executable - google-apps-script

I have created a Google apps script attached to a google sheet (where I have various methods manipulating the spreadsheet), and I have deployed it as API executable (enabling OAuth etc). Target is to call those methods via REST from an external location not part of Google cloud (like an independent React client, or a standalone server, or my local machine)
Question is: How can I call this from a standalone javascript (like a node.js script executed on my local machine? I do have the script URL (script id) , the secret and the key, but don;t know how to use them all.
Could you help with some sample code, pointers, etc. It looks like my google searches hit only unrelated topics...

You can check this example on how to call the script as an API executable. You will see that the way to call the script from different languages is similar for example using JavaScript, you need to also take note on some important information like:
The basic types in Apps Script are similar to the basic types in JavaScript: strings, arrays, objects, numbers and booleans. The Execution API can only take and return values corresponding to these basic types -- more complex Apps Script objects (like a Document or Sheet) cannot be passed by the API.
An example to make a call the way that you currently want using Apps script would be like:
Target Script
/** This is the Apps Script method these API examples will be calling.
*
* It requires the following scope list, which must be used when authorizing
* the API:
* https://www.googleapis.com/auth/spreadsheets
*/
/**
* Return a list of sheet names in the Spreadsheet with the given ID.
* #param {String} a Spreadsheet ID.
* #return {Array} A list of sheet names.
*/
function getSheetNames(sheetId) {
var ss = SpreadsheetApp.openById(sheetId);
var sheets = ss.getSheets();
return sheets.map(function(sheet) {
return sheet.getName();
});
}
This is the script that you have setup as an API executable and you can call this script using JavaScript like this:
// ID of the script to call. Acquire this from the Apps Script editor,
// under Publish > Deploy as API executable.
var scriptId = "<ENTER_YOUR_SCRIPT_ID_HERE>";
// Initialize parameters for function call.
var sheetId = "<ENTER_ID_OF_SPREADSHEET_TO_EXAMINE_HERE>";
// Create execution request.
var request = {
'function': 'getSheetNames',
'parameters': [sheetId],
'devMode': true // Optional.
};
// Make the request.
var op = gapi.client.request({
'root': 'https://script.googleapis.com',
'path': 'v1/scripts/' + scriptId + ':run',
'method': 'POST',
'body': request
});
// Log the results of the request.
op.execute(function(resp) {
if (resp.error && resp.error.status) {
// The API encountered a problem before the script started executing.
console.log('Error calling API: ' + JSON.stringify(resp, null, 2));
} else if (resp.error) {
// The API executed, but the script returned an error.
var error = resp.error.details[0];
console.log('Script error! Message: ' + error.errorMessage);
} else {
// Here, the function returns an array of strings.
var sheetNames = resp.response.result;
console.log('Sheet names in spreadsheet:');
sheetNames.forEach(function(name){
console.log(name);
});
}
});
Please note as well that there are some limitations that you may want to check before further perform tests.

Related

Access Spreadsheet Cell values from library script

Is there a way I can access data, stored within a spreadsheet-file from the library script?
I want to use 1 Google Apps Script from multiple Google Spreadsheet files within my Google Drive.
I followed this answer: "you could use Libraries. The idea is that you create one script that you use as a library" and could successfully import the library to my project.
In order to work, the scripts within the library need some of the cell-values stored in the google sheet files. I know how to access the script via a helper function in my sheet-bound script file. For example:
function loc_my_credits()
{
SISTRIXAbfrageFreigabe.my_credits();
}
Whilst "SISTRIXAbfrageFreigabe" is the library name, and my_credits a function within the library.
When I call loc_my_credits from sheets, nothing happens. My best guess: the script cant read data from the spreadsheet file it needs to execute.
The my_credits script from the library file looks like this:
function my_credits(){
// Base URL to access customsearch
var urlTemplate = "https://api.sistrix.com/credits?api_key=%KEY%";
// initialize sheets: 1. Get the spreadsheet, 2. Get the first and the second sheets in this spreadsheet
var spreadSheet = SpreadsheetApp.getActiveSpreadsheet();
var inputSheet = spreadSheet.getSheets()[0];
// Script-specific credentials & search engine
var sistrix_Apikey = inputSheet.getRange('A2').getValue();
var url = urlTemplate.replace("%KEY%", encodeURIComponent(sistrix_Apikey));
var params = {
muteHttpExceptions: true
};
var xml = UrlFetchApp.fetch(url).getContentText();
var document = XmlService.parse(xml);
var root = document.getRootElement();
var items = document.getRootElement().getChildren();
for (var i = 0; i < items.length; i++) {
if(items[i].getName() == 'answer'){
var answer = items[i].getChildren();
return answer[0].getAttribute('value').getValue();
}
}
return 0;
}
Is there a way I can access the data stored in the spreadsheet file from the library script?
I'm writing this answer as a community wiki, since the issue was resolved from the comments section, in order to provide a proper response to the question.
The problem was related to the usage of methods that need scopes that require authorization, therefore it would be expected that simple triggers would show the error:
You do not have permission to call SpreadsheetApp.openById
Google's documentation states that installable triggers would solve the problem:
Installable triggers, however, offer more flexibility than simple triggers: they can call services that require authorization
According to s.Panse, the usage of installable triggers has resolved the issue in this case.
References:
google script openById : You do not have permission to perform that action
Installable Triggers

How do I determine what caused my script to run?

Whenever I attempt to display a UI dialog (e.g. msgBox or alert) it works fine when invoked via a menu item (e.g. from Google Sheets), but it hangs my script if I try to invoke it from the Google Apps Script editor (e.g. via Run > Run function).
My guess is it's because the Google Apps Script editor can't display any UI. To resolve this, I'd like to create a wrapper function that checks how the script was run, and not present UI depending on the source.
The "Executions" screen has the notion of Type (Editor, Standalone, Trigger):
This makes me think there is a way to get this type in code somehow.
Psuedo code of what the function might look like:
function showMessage(message) {
var scriptSource = ???;
if (scriptSource === "Standalone") {
Browser.msgBox(message);
} else {
console.log(message);
}
}
How would I get the scriptSource?
The closest thing I can find is TriggerSource, but that is missing the enum values 'Editor' and 'Trigger'. Furthermore, it's a property only available on a Trigger. I don't know how to access the current trigger. From my understanding, that's only available via the event object (e.g. via triggerUid) on functions acting as triggers. This method I'm running in the apps script editor doesn't have access to an event object.
Not the best solution, but my current workaround is to create 3 versions of each function, and append how it was invoked to the name.
For example, if there was a "Hello World" function:
function onOpen() {
var menu = [
{name: 'Hello World', functionName: 'helloWorldViaMenu_'},
];
SpreadsheetApp.getActive().addMenu('Custom', menu);
}
function helloWorldViaMenu_() {
helloWorld_(false);
}
function helloWorldViaEditor() {
helloWorld_(true);
}
function helloWorld_(invokedFromEditor) {
if (invokedFromEditor) {
Logger.log("Hello world");
} else {
Browser.msgBox("Hello world");
}
}
helloWorldViaEditor is the only that doesn't have a _ at the end so it can be selected via the "Select function" Editor UI dropdown.
You want to know whether the current project is the container-bound script type or the standalone script type.
You want to use Browser.msgBox().
I could understand about your question as above. In order to achieve it, as a workaround,I would like to propose to use Apps Script API. The flow of sample script is as follows. I think that there are several workarounds for your situation. So please think of this as one of them.
Retrieve the parent ID of the project using the method of projects.get in Apps Script API. The parent ID means that the file ID of Google Docs.
When the parent ID is returned, it is found that the project is the container-bound script type.
When the parent ID is NOT returned, it is found that the project is the standalone script type.
When the mimeType of parent ID is Google Form, Browser.msgBox() cannot be used. So the if statement is used for this.
Sample script:
This is a sample script. In this sample script, the script ID of current project is used. Of course, you can also manually give the script ID.
var id = ScriptApp.getScriptId(); // Retrieve scriptId of current project.
var url = "https://script.googleapis.com/v1/projects/" + id + "?fields=parentId";
var res = UrlFetchApp.fetch(url, {headers: {Authorization: "Bearer " + ScriptApp.getOAuthToken()}});
res = JSON.parse(res.getContentText());
if ("parentId" in res) {
Logger.log("Container-bound script type.")
var mimeType = DriveApp.getFileById(res.parentId).getMimeType();
if (mimeType === MimeType.GOOGLE_FORMS) {
Logger.log("Browser.msgBox() cannot be used at Google Form.");
} else {
Browser.msgBox("Hello world");
}
} else {
Logger.log("Standalone script type.")
Logger.log("Hello world");
}
Note:
When you use this script, please do the following flow.
Enable Apps Script API at API console.
At least, add the following scopes to the manifests.
https://www.googleapis.com/auth/drive
https://www.googleapis.com/auth/script.external_request
https://www.googleapis.com/auth/script.projects.readonly
If in your script, other scopes are required to be added, please add them. And if you want to use the automatically installer of scopes with the script editor, you can achieve it using a library. You can see the detail information at here.
References:
Apps Script API
Manifests
projects.get
Taking Advantage of Manifests by GAS Library
If I misunderstand your question, I'm sorry.
Edit:
You want to confirm whether the function is called from the script editor or the custom menu.
If my understanding is correct, how about this sample script? This is a sample script. The process list can be retrieved by giving the script ID and function name. In this sample script, using "ProcessType" of processes.listScriptProcesses in Apps Script API, it confirms whether the function is called from the script editor or the custom menu.
Sample script:
This is a sample script. The process list can be retrieved by giving the script ID and function name.
When you use this script, please enable Apps Script API at API console, and add a scope of https://www.googleapis.com/auth/script.processes to the manifests.
The how to use this script is as follows.
Run addCustomMenu().
Run sampleFunction at the custom menu.
By this, Call from custom menu is shown in log.
Run sampleFunction at the script editor.
By this, Call from script editor is shown in log.
Script:
function addCustomMenu() {
SpreadsheetApp.getUi().createMenu('sampleCustomMenu').addItem('sample', 'sampleFunction').addToUi();
}
function sampleFunction() {
var scriptId = ScriptApp.getScriptId();
var functionName = "sampleFunction";
var url = "https://script.googleapis.com/v1/processes:listScriptProcesses?scriptId=" + scriptId + "&scriptProcessFilter.functionName=" + functionName;
var res = UrlFetchApp.fetch(url, {headers: {Authorization: "Bearer " + ScriptApp.getOAuthToken()}, muteHttpExceptions: true});
res = JSON.parse(res);
if (!("processType" in res.processes[0])) {
Logger.log("Call from custom menu")
} else if (res.processes[0].processType == "EDITOR") {
Logger.log("Call from script editor")
}
}
References:
Apps Script API
Manifests
processes.listScriptProcesses
ProcessType
Making Dialogs
You can run them from the menu or the script editor. They work the same.
function makeAmenu(){
SpreadsheetApp.getUi().createMenu('A Menu')
.addItem('Run my Dialogs', 'showMyDialogs')
.addToUi();
}
function showMyDialogs(){
var ui=SpreadsheetApp.getUi();
ui.alert('This is an alert');
ui.prompt('This is a prompt');
var html=HtmlService.createHtmlOutput('<p>This is a modeless dialog</p><input type="button" value="Close" onClick="google.script.host.close();" />');
ui.showModelessDialog(html, 'Dialog');
}
If you run a script from here:
The you have to go here to see it:

Google Apps Script - Contacts - Query on multiple strings

I'm working on a script to remove and re-add about 100 contacts. I have 12 different search criteria for ContactsApp.getContactsByEmailAddress, which the initiated know takes 30+ seconds to run. Is there some way I can only run it once and search all of my criteria? I've looked for others trying to do this same thing and was unsuccessful.
Below is one of the searches from my function (repeats 12 times with various search terms being passed to ContactsApp.getContactsByEmailAddress). I added the try-catch block because the script kept throwing errors out for seemingly no reason during various delete loops.
Would appreciate any and all advice.
var apa = ContactsApp.getContactsByEmailAddress('apa.')
try{
for (var i in apa) {
apa[i].deleteContact()
}
} catch(e){
Logger.log(e)
}
With the Contacts Service, you are limited to a single search criteria. Thus, the only way to search multiple patterns is to call the method once with each search parameter. You can, thankfully, use standard programming practices to minimize the amount of repeated code:
function getContactsWithEmails(emailSearchCriteria) {
if (!emailSearchCriteria || !emailSearchCriteria.length)
throw new Error("No search inputs given");
// Collect the results of each search into a single Array.
const matches = [];
emailSearchCriteria.forEach(function (email) {
var results = ContactsApp.getContactsByEmailAddress(email);
if (results.length)
Array.prototype.push.apply(matches, results);
else
console.log({message: "No results for search query '" + email + "'", query: email, resultsSoFar: matches});
});
return matches;
}
function deleteContacts(arrayOfContacts) {
if (!arrayOfContacts || !arrayOfContacts.length)
throw new Error("No contacts to delete");
arrayOfContacts.forEach(function (contact) {
ContactsApp.deleteContact(contact);
});
}
// Our function that uses the above helper methods to do what we want.
function doSomething() {
// Define all email searches to be performed.
const emailFragmentsToSearchWith = [
"email1",
...
"emailN"
];
const matchingContacts = getContactsWithEmails(emailFragmentsToSearchWith);
if (matchingContacts.length) {
/** do something with the contacts that matched the search.
* someMethodThatSavesContacts(matchingContacts);
* someMethodThatModifiesContacts(matchingContacts);
* deleteContacts(matchingContacts);
* ...
*/
}
/** do other stuff that doesn't need those contacts. */
}
The Google Calendar v3 GData API, as mentioned in this SO question, does support multiple query parameters. However, there is no simple integration with this API - you will need to write the appropriate URL requests and execute them with UrlFetchApp.
In addition to the Google Contacts API, you could use the Google People REST API, specifically the people.connections#list endpoint.
Both of these APIs require you to associate your Apps Script project with a Google Cloud Project that has the respective API enabled, and will likely require you to manually set the scopes your Apps Script project requires in its manifest file, in addition to providing OAuth2 authorizations of the associated HTTP requests you make to the API endpoints.
References
Accessing External APIs
Enabling Google APIs (steps 3-5)
Google Contacts API v3
Google People API
Array#forEach

Calling a Google Apps Script web app with access token

I need to execute a GAS service on behalf of a user that is logged to my system. So I have her/his access token. I would like somehow to transfer the token to the web app and without having to authorize again the user to use it for some activities. Can this be accomplished? Thank you.
EDIT: I think I didn't explain right what I try to accomplish. Here is the work flow I try to achieve:
We authorize a user visiting our website using OAuth2 and Google;
We get hold of her/his access token that Google returns;
There is a Google Apps Script web app that is executed as the user running the web app;
We want to call this app (3) by providing the access token (2) so Google not to ask again for authorization;
Actually, we want to call this app (3) not by redirecting the user to it but by calling it as a web service.
Thanks
Martin's answer worked for me in the end, but when I was making a prototype there was a major hurdle.
I needed to add the following scope manually, as the "automatic scope detection system" of google apps script did not ask for it: "https://www.googleapis.com/auth/drive.readonly". This resulted in UrlFetchApp.fetch always giving 401 with additional information I did not understand. Logging this additional information would show html, including the following string
Sorry, unable to open the file at this time.</p><p> Please check the address and try again.
I still don't really understand why "https://www.googleapis.com/auth/drive.readonly" would be necessary. It may have to do with the fact that we can use the /dev url, but who may use the /dev url is managed is checked using the drive permissions of the script file.
That said, the following setup then works for me (it also works with doGet etc, but I chose doPost). I chose to list the minimally needed scopes explicitly in the manifest file, but you can also make sure the calling script will ask for permissions to access drive in different ways. We have two google apps script projects, Caller and WebApp.
In the manifest file of Caller, i.e. appsscript.json
{
...
"oauthScopes":
[
"https://www.googleapis.com/auth/drive.readonly",
"https://www.googleapis.com/auth/script.external_request"]
}
In Code.gs of Caller
function controlCallSimpleService(){
var webAppUrl ='https://script.google.com/a/DOMAIN/macros/s/id123123123/exec';
// var webAppUrl =
// 'https://script.google.com/a/DOMAIN/macros/s/id1212121212/dev'
var token = ScriptApp.getOAuthToken();
var options = {
'method' : 'post'
, 'headers': {'Authorization': 'Bearer '+ token}
, muteHttpExceptions: true
};
var response = UrlFetchApp.fetch(webAppUrl, options);
Logger.log(response.getContentText());
}
In Code.gs of WebApp (the web app being called)
function doPost(event){
return ContentService.createTextOutput("Hello World");
}
The hard answer is NO you can't use the built-in services of Apps Script with a service token. But if you already have the token for a user generated by a service account, access to the users data is pretty similar to any other language. All calls would be to the REST interface of the service your token is scoped for.
Take this small script for example. It will build a list of all the user's folders and return them as JSON:
function doGet(e){
var token = e.parameter.token;
var folderArray = [];
var pageToken = "";
var query = encodeURIComponent("mimeType = 'application/vnd.google-apps.folder'");
var params = {method:"GET",
contentType:'application/json',
headers:{Authorization:"Bearer "+token},
muteHttpExceptions:true
};
var url = "https://www.googleapis.com/drive/v2/files?q="+query;
do{
var results = UrlFetchApp.fetch(url,params);
if(results.getResponseCode() != 200){
Logger.log(results);
break;
}
var folders = JSON.parse(results.getContentText());
url = "https://www.googleapis.com/drive/v2/files?q="+query;
for(var i in folders.items){
folderArray.push({"name":folders.items[i].title, "id":folders.items[i].id})
}
pageToken = folders.nextPageToken;
url += "&pageToken="+encodeURIComponent(pageToken);
}while(pageToken != undefined)
var folderObj = {};
folderObj["folders"] = folderArray;
return ContentService.createTextOutput(JSON.stringify(folderObj)).setMimeType(ContentService.MimeType.JSON);
}
You do miss out on a lot of the convenience that makes Apps Script so powerful, mainly the built in services, but all functionality is available through the Google REST APIs.
I found a way! Just include the following header in the request:
Authorization: Bearer <user's_access_token>

Use Google Script's Web App as Webhook to receive Push Notification directly

My Goal: Changes in Google Drive => Push Notification to https://script.google.com/a/macros/my-domain/... => App is pushed to take action.
I don't want to setup an middle Webhook agent for receiving notification. Instead, let the Web App (by Google Script) to receive it and be pushed directly.
Since the relevant function is quite undocumented (just here: https://developers.google.com/drive/web/push) , below is the code I tried but failure.
1. Is above idea feasible??
2. My code doPost(R) seems cannot receive notification (R parameter) properly. Anyway, no response after I change the Google Drive. Any problem? (I have tried to log the input parameter R so as to see its real structure and decide if the parameter Obj for OAuth is the same as normal Drive App, but error occur before log)
function SetWatchByOnce(){
var Channel = {
'address': 'https://script.google.com/a/macros/my-domain/.../exec',
'type': 'web_hook',
'id': 'my-UUID'
};
var Result = Drive.Changes.watch(Channel);
...
}
function doPost(R) {
var SysEmail = "My Email";
MailApp.sendEmail(SysEmail, 'Testing ', 'Successfully to received Push Notification');
var Response = JSON.parse(R.parameters);
if (Response.kind == "drive#add") {
var FileId = Response.fileId;
MyFile = DriveApp.getFolderById(FileId);
...
}
}
function doGet(e) {
var HTMLToOutput;
var SysEmail = "My Email";
if (e.parameters.kind) {
//I think this part is not needed, since Push Notification by Drive is via Post, not Get. I should use onPost() to receive it. Right?
} else if (e.parameters.code) {
getAndStoreAccessToken(e.parameters.code);
HTMLToOutput = '<html><h1>App is successfully installed.</h1></html>';
} else { //we are starting from scratch or resetting
HTMLToOutput = "<html><h1>Install this App now...!</h1><a href='" + getURLForAuthorization() + "'>click here to start</a></html>";
}
return HtmlService.createHtmlOutput(HTMLToOutput);
}
....
Cloud Functions HTTP trigger(s) might also be an option ...
(which not yet existed at time of this question). this just requires setting the trigger URL as the notification URL, in the Google Drive settings - and adding some NodeJS code for the trigger; whatever it shall do. one can eg. send emails and/or FCM push notifications alike that. that trigger could also be triggered from App Script, with UrlFetchApp and there is the App Script API. one can have several triggers, which are performing different tasks (App Script is only one possibilty).
Cicada,
We have done similar functions to receive webhooks/API calls many times. Notes:
to get R, you need: var Response = R.parameters and then you can do Response.kind, Response.id, etc.
Logger will not work with doGet() and doPost(). I set it up a write to spreadsheet -- before any serious code. That way I know if it is getting triggered.