Access UserProperties in customfunction? - google-apps-script

We want to get data from our API into Google Sheets. Our API requires authentication. Our tentative architecture has the user entering their credentials in a ModalDialog, then we hit our authentication endpoint which returns a session token which is saved. Subsequently the user makes customfunction calls in which we make calls to our API data end-point and include the authentication token.
My question -
How do I get the authentication token into the scope of the customfunction? It seems like PropertiesService.getUserProperties should do this but the values we set in the function that gets the credential token are not visible in the customfunction.
function saveCredentials(action){
var username = action['username'],
password = action['password'];
var response = UrFetchApp.fetch('https://www.foo.com/account/logon/, {method: 'post', payload: {email: action, password: password}});
var token = response.getAllHeaders()['Set-Cookie'];
PropertiesService.getUserProperties().setProperty('token', token);}
/**
* #param {parameter1}
* #param {parameter2}
* #customfunction
*/
function getDataFromFooAPI(parameter1, parameter2){
var token = PropertiesService.getUserProperties().getProperty('token') // expecting this to be the value set in saveCredentials but it is not.
var url = 'https://www.foo.com/api/data/' + parameter1 + '/' + parameter2;
var response = UrlFetchApp.fetch(url, {'headers' : {'token' : token}};
}
Does the context in which the customfunction executes references a different PropertiesService than the rest of the script? Are we trying to do the impossible?

There are some documented limitations with custom functions, notably that they don't have access to personal information. They can access script and document properties, but not user properties. Custom functions should behave consistently for all users who have access to the sheet and operate without authorization. Relying on user properties would make that virtually impossible.
If you can use document properties instead that should work.

Related

Google Apps Script - call function deployed as API executable

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.

Add Redirect URI To Automatically Generated Google OAuth 2.0 Client ID

I'm trying to make a Google sheet that integrates with the YouTube Data and Analytics API. However, when trying to implement this, I came into a known issue with regards to allowing brand YouTube accounts/channels to be authenticated with a Google app which is explained here. https://issuetracker.google.com/issues/36764531
To get around this, the document mentions instructions from this link which I am now trying to implement myself https://mashe.hawksey.info/2017/09/identity-crisis-using-the-youtube-api-with-google-apps-script-and-scheduling-live-broadcasts-from-google-sheets/
From the instructions, I have:
Imported the necessary libraries into the script
Added the necessary Google Apps Script code which is at the bottom
Created my own OAuth 2.0 Client ID credentials in the Google Cloud Console
However, in the first link, a comment had also been added to say that while the instructions still worked, the script project now has to be associated with a cloud project and so that's what I did. As part of this, it created its own OAuth 2.0 Client ID which I believe it's now using rather than the credentials I had already generated myself. I have added a picture below to illustrate what I mean. I also then can't edit these new credentials meaning that I can't add any redirect URI.
Is there a way I can add the redirect URI to the automatically generated credentials? The problem now is that if I then run the setup function from the script as per the instructions, when I then try to open this link it gives me, I then get given the following message
Error 400: redirect_uri_mismatch
The redirect URI in the request, https://script.google.com/macros/d/12u2laknmO_9-zgxBbAX6wG9gJDUOvgJmYm5UquJsamShus9s5McrGBar/usercallback, does not match the ones authorized for the OAuth client. To update the authorized redirect URIs, visit: https://console.developers.google.com/apis/credentials/oauthclient/${your_client_id}?project=${your_project_number}
/**
* Authorizes and makes a request to the YouTube Data API.
*/
function setup() {
var service = getYouTubeService();
YouTube.setTokenService(function(){ return service.getAccessToken(); });
if (service.hasAccess()) {
var result = YouTube.channelsList("snippet", {mine:true});
Logger.log(JSON.stringify(result, null, 2));
throw "Open View > Logs to see result";
} else {
var authorizationUrl = service.getAuthorizationUrl();
Logger.log('Open the following URL and re-run the script: %s',
authorizationUrl);
throw "Open View > Logs to get authentication url";
}
}
/**
* Configures the service.
*/
function getYouTubeService() {
return OAuth2.createService('YouTube')
// Set the endpoint URLs.
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
// Set the client ID and secret.
.setClientId(getStaticScriptProperty_('client_id'))
.setClientSecret(getStaticScriptProperty_('client_secret'))
// Set the name of the callback function that should be invoked to complete
// the OAuth flow.
.setCallbackFunction('authCallback')
// Set the property store where authorized tokens should be persisted
// you might want to switch to Script Properties if sharing access
.setPropertyStore(PropertiesService.getUserProperties())
// Set the scope and additional Google-specific parameters.
.setScope(["https://www.googleapis.com/auth/youtube",
"https://www.googleapis.com/auth/youtube.force-ssl",
"https://www.googleapis.com/auth/youtube.readonly",
"https://www.googleapis.com/auth/youtubepartner",
"https://www.googleapis.com/auth/youtubepartner-channel-audit"])
.setParam('access_type', 'offline');
}
/**
* Handles the OAuth callback.
*/
function authCallback(request) {
var service = getYouTubeService();
var authorized = service.handleCallback(request);
if (authorized) {
return HtmlService.createHtmlOutput('Success!');
} else {
return HtmlService.createHtmlOutput('Denied');
}
}
/**
* Logs the redirect URI to register in the Google Developers Console.
*/
function logRedirectUri() {
var service = getYouTubeService();
Logger.log(service.getRedirectUri());
throw "Open View > Logs to get redirect url";
}
/**
* Reset the authorization state, so that it can be re-tested.
*/
function reset() {
var service = getYouTubeService();
service.reset();
}
/**
* Gets a static script property, using long term caching.
* #param {string} key The property key.
* #returns {string} The property value.
*/
function getStaticScriptProperty_(key) {
var value = CacheService.getScriptCache().get(key);
if (!value) {
value = PropertiesService.getScriptProperties().getProperty(key);
CacheService.getScriptCache().put(key, value, 21600);
}
return value;
}
I hope this is clear but if not then I can answer any further questions. Or if there is a better way of being able to authenticate a brand YouTube account with the API, then please let me know.
In the end I had to make new credentials in the console. Once I had done this and bound it with the script, it then seemed to work as expected.

Getting Invalid redirect_uri using OAuth2 for Google Sheets

I am not sure what to use as my Redirect URI. Bitbucket has a space for "Callback URL" which I am assuming is where it is entered. Anyone else have this problem/know how to use Redirect URI in this situation? I am setting up my service object below.
function getBitbucketServiceOAuth2()
{
// 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('bitbucket')
// Set the endpoint URLs, which are the same for all Google services.
.setAuthorizationBaseUrl('https://bitbucket.org/site/oauth2/authorize')
.setTokenUrl('https://bitbucket.org/site/oauth2/access_token')
// Set the client ID and secret, from the Google Developers Console.
.setClientId('WL6MQbGku7axd5kqun')
.setClientSecret('jTRmqm5ug9fEUMAxeCQRx95uQz8LCevW')
// 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');
}
And then getting the Authorize url below.
var authorizationUrl = service.getAuthorizationUrl();
var template = HtmlService.createTemplate(
'Authorize. ' +
'Reopen the sidebar when the authorization is complete.');
template.authorizationUrl = authorizationUrl;
var page = template.evaluate();
SpreadsheetApp.getUi().showSidebar(page);
Any help would be great!
When you get the error 403 (or equivalent), you will see "read more" link. Click on that and copy paste the expected link it gives.
Now, use this link to generate new credentials in redirect uri.
This flow worked for me. Hope it works for you too.

Using the UrlShortener API in a custom Spreadsheet function

I would like to use an API (url shortener) with a public google Add-on.
For the moment my code returns:
Daily Limit for Unauthenticated Use Exceeded. Continued use requires signup.
Is this possible?
If yes, Do I need an authentification Token?
If yes, what key type should I choose?
How can I implement the authorisation for this kind of use?
Do I need to pay for it?
If no, How could other add-ons use external APIs
Thanks a lot for your answers,
EDIT: The OP pointed out in the comments that this is a custom function. Custom function run with limited authorization. A complete list of what is available is at:
https://developers.google.com/apps-script/guides/sheets/functions#using_apps_script_services
Below uses the REST API to get the shortened url. This will work with custom functions. You will just need to enable the URL Shortener API and generate a Server API Key. Use the IPs at the following link for your server api key:
https://developers.google.com/apps-script/guides/jdbc#setup_for_google_cloud_sql
/**
* Returns a shortened URL of the input.
*
* #param {string} longUrl The long URL to shorten.
* #return The shortened url.
* #customfunction
*/
function getShortUrl(longUrl) {
var payLoad = {"longUrl": longUrl};
var apiKey = PropertiesService.getScriptProperties().getProperty("ServerApiKey");
var url = "https://www.googleapis.com/urlshortener/v1/url?key="+ apiKey;
var options = { method:"POST",
contentType:"application/json",
payload:JSON.stringify(payLoad),
muteHttpExceptions:true};
var response = UrlFetchApp.fetch(url, options);
if(response.getResponseCode() != 200){throw new Error("Unable to shorten url");}
return JSON.parse(response).id;
}
Original Post
Here is a quick primer on using the UrlShortener advanced service. You will need to turn on the service and activate the Url Shortener api in the developers console. This will give you a quota of 1,000,000 requests per day for your add-on.
function myFunction() {
var url = UrlShortener.newUrl();
url.longUrl = "http://www.example.org";
var short = UrlShortener.Url.insert(url);
Logger.log(short);
//list all users shortened urls
Logger.log(UrlShortener.Url.list());
}

How do I getGivenName() of getActiveUser()

Each user on the domain initiates a simple script we run for leave entitlements but we want the welcome message to be "Hi First Name," however the script doesn't seem to be able to fetch getGivenName() from getActiveUser() for a standard user.
Is there a way?
As noted in comments, and in Documentation, the UserManager Service is only accessible by Domain Administrators.
Here's an alternative. Domain Users may have themselves in their own contacts, so how about a best-effort attempt at finding themselves there?
/**
* Get current user's name, by accessing their contacts.
*
* #returns {String} First name (GivenName) if available,
* else FullName, or login ID (userName)
* if record not found in contacts.
*/
function getOwnName(){
var email = Session.getEffectiveUser().getEmail();
var self = ContactsApp.getContact(email);
// If user has themselves in their contacts, return their name
if (self) {
// Prefer given name, if that's available
var name = self.getGivenName();
// But we will settle for the full name
if (!name) name = self.getFullName();
return name;
}
// If they don't have themselves in Contacts, return the bald userName.
else {
var userName = Session.getEffectiveUser().getUsername();
return userName;
}
}
In Apps Script, I was able to get this information using the About REST API: https://developers.google.com/drive/v2/reference/about/get
var aboutData = DriveApp.About.get();
var userEmail = aboutData["user"]["emailAddress"];
var userDisplayName = aboutData["user"]["displayName"];
You can get a user name but first you have to create a domain user using the provisioning api. You can enable the API by logging in to your admin account, and select Domain settings and the User settings tab to select the checkbox enabling the Provisioning API. Read more about it here
You can then use
user = user.getgivenName()
Since the UserManager Service is only available to a Domain Administrator, you could publish a service as the administrator, that serves user's Given Names, and invoke that from the user-run script using the UrlFetchApp.
The UserName Service
Refer to the Content Service Documentation for the background information this is based upon.
The service accepts a parameter, userName, which it uses to perform a lookup as the administrator.
Paste the following code into a script, then deploy the script as a web service. This must be done by a Domain Administrator, as the service access the UserManager Service, but the script must be made accessible by all users in the domain. (Since I'm not an admin in my domain, I cannot access the UserManager, so I've included a domain-user-invokable line for testing, calling the getOwnName() function I described in my first answer.)
Remember to invoke doGet() from the debugger to go through the authorization before accessing the published service.
/**
* When invoked as a Web Service running as Domain Administrator,
* returns the GivenName of the requested user.
*
* #param {String} userName= Should be set to Session.getEffectiveUser().getUsername().
*/
function doGet(request) {
//return ContentService.createTextOutput(getOwnName()); // for testing by non-admin user
var userName = request.parameters.userName;
var givenName = UserManager.getUser(userName).getGivenName();
return ContentService.createTextOutput(givenName);
}
Invoke service using UrlFetch
Refer to Using External APIs for an explanation of how to make use of the service written in the previous section. I'll show how to access the service from another script, but remember that you can also do this from web pages within your domain.
We will use UrlFetchApp.fetch() to get our service to return the user's first name as a String.
The service was written to accept one parameter, userName, and we append this to the url, in the form userName=<string>.
With the URL built, we fetch(), then retrieve the name from the response. While this example returns just the name, you may choose to change the service to return the complete "Hello User" string.
function testService() {
var domain = "my-google-domain.com";
var scriptId = "Script ID of service";
var url = "https://script.google.com/a/macros/"+domain+"/s/"+scriptId+"/exec?"
+ "userName="+Session.getEffectiveUser().getUsername();
var response = UrlFetchApp.fetch(url);
var myName = response.getContentText();
debugger; // pause in debugger
}
Another potential way of getting the display name on a gmail account is to find a Draft email in the GmailApp, and get the From header, which may have the full name. Some drafts might be setup with no display name in gmail, in which case the From header will only be the email address, but typically the From header is in the format:
Firstname Lastname <email#domain.com>
This code should get you the string above from the first gmail Draft: (note this will probably throw an exception if there are no drafts, so check that first.)
GmailApp.getDrafts()[0].getMessage().getHeader("From")
Ref: https://developers.google.com/apps-script/reference/gmail/gmail-message#getHeader(String)
Ref: https://www.ietf.org/rfc/rfc2822.txt