I need to send Google form data to external api, this api uses api-key to authenticate every requests being made. Hence I collect the api-key and save it in the property store.
Since multipl calls to property stores must be avoided, I thought using cache service is a google choice when there are large number of incoming form submissions. On reading the official documentation I am not sure I have understood well.
I have the following code that gives the api-key when called., I need to introduce cache service within it. Any pointers are greatly appreciated.
function getApiKey() {
var apiKey = PropertiesService.getUserProperties().getProperty("API_KEY")
if (apiKey)
return apiKey;
else
return "INVALID";
}
An equivalent of PropertiesService.getUserProperties().getProperty("API_KEY") with CacheService:
CacheService.getUserCache().get("API_KEY")
To write values to Cache use put(key, value, expirationInSeconds).
You can find a good documentation including samples under Class Cache.
EDIT 2 - I have now provided my own answer to the issue - any further thoughts or inputs would still be appreciated
EDIT 1 - POTENTIAL RELEVANT INFORMATION:
I've found this footnote at the bottom of the Content Service documentation page that says:
For security reasons, content returned by the Content service isn't served from script.google.com, but instead redirected to a one-time URL at script.googleusercontent.com. This means that if you use the Content service to return data to another application, you must ensure that the HTTP client is configured to follow redirects. For example, in the cURL command line utility, add the flag -L. Check the documentation for your HTTP client for more information on how to enable this behavior.
This seems relevant as I am using ContentService to serve data to Strava's GET request - would this not mean that the response it receives comes from a different URL and hence not from the specified callback URL?
I've been trying to use Google Apps Script to create a webhook subscription to Strava and I feel like I'm extremely close to figuring it out, but I've encounter one last hurdle that I can't seem to pass.
The documentation for creating a webhook subscription to Strava is listed here and I've reached the stage at which I'm making a POST to the Strava API requesting a subscription. Strava then sends an HTTP GET request to my callback_url that I've specified which contains some parameters - most important of which is the hub.challenge parameter. This param must then be sent back within 2 seconds by my callback address in order for the link to be validated. This is where I'm having some grief.
function doGet(e) {
var hubChal = e.parameter["hub.challenge"];
var result = {
"hub.challenge": hubChal
};
return ContentService.createTextOutput(JSON.stringify(result))
.setMimeType(ContentService.MimeType.JSON);
}
Above is my current function that is dealing with incoming GET requests.
In the documentation it states:
Your callback address must respond within two seconds to the GET request from Strava’s subscription service. The response should indicate status code 200 and should echo the hub.challenge field in the response body as application/json content type: { “hub.challenge”:”15f7d1a91c1f40f8a748fd134752feb3” }
However, when I send the POST to the webhook subscription API I receive this response on Postman:
{
"message": "Bad Request",
"errors": [
{
"resource": "PushSubscription",
"field": "callback url",
"code": "GET to callback URL does not return 200"
}
]
}
I checked through the troubleshooting advice that is offered on the doc page, an element of which gives a sample GET request for you to send yourself to see what your callback address offers in return:
Check that the response to the above request shows a 200 status and correctly echos the hub.challenge in the JSON body. The response body to the above sample curl request should look like { “hub.challenge”:”15f7d1a91c1f40f8a748fd134752feb3” }
With the following dummy GET request:
{your-callback-url}?hub.verify_token=test&hub.challenge=15f7d1a91c1f40f8a748fd134752feb3&hub.mode=subscribe
Plugging in my callback url and sending a GET request via Postman returns the following to me:
{
"hub.challenge": "15f7d1a91c1f40f8a748fd134752feb3"
}
Along with showing a 200 OK Status code and in well under 2 seconds.
I really can't see what I've done incorrectly here, as it seems like I'm fulfilling the criteria laid out to set up a subscription. It's worth mentioning that I'm not very familiar with Google Apps Script, so it's entirely possible and even likely that I'm missing something basic, but I can't see it for the life of me.
I've gone through all of the troubleshooting advice and haven't been able to find an answer online despite an entire afternoon and evening of searching yesterday. Any help would be enormously appreciated - thanks.
I believe I've finally figured out my issue and unfortunately it seems it is not possible to set up a webhook subscription to Strava using Google Apps Script - at least not whilst using ContentService to serve data to Strava GET requests.
At the end of the ContentService documentation it says:
For security reasons, content returned by the Content service isn't served from script.google.com, but instead redirected to a one-time URL at script.googleusercontent.com. This means that if you use the Content service to return data to another application, you must ensure that the HTTP client is configured to follow redirects. For example, in the cURL command line utility, add the flag -L. Check the documentation for your HTTP client for more information on how to enable this behavior.
The problem with this is that Strava expects and will only accept a 200 status code attached to the response to its GET request, however ContentService sends a 302 redirect to this one-time URL. I was able to verify this in Postman by turning off "Automatically follow redirects" in the request settings of my dummy GET request:
https://i.stack.imgur.com/vAvIm.png
Since I have auto redirect enabled by default on Postman, I wasn't seeing the 302 at any point when sending my own requests, leading to everything appearing to be in order.
If there is a way around this ContentService issue then please do inform me, as the outcome of this is that I'm now going to have to poll Strava for new data (booo) which will make my finished product significantly more clunky.
Hopefully this post will now stand to help anyone else who finds themselves with my issue in the future and will save them a few days of confusion!
I am not familiar with Goole Apps Script (very cool, by the way!), but it sounds like you have Strava webhooks well understood.
It sounds like your doGet method is returning something other than 200. Do you have a logging feature that can output what the doGet method is being sent, what it's sending back, etc?
If you haven't already, you may want to check out the Strava tutorial on subscribing to webhooks using Node, Express and Ngrok. A few extra steps, but fairly easy to follow and debug.
I'm working on an performance evaluation app in Google App Maker. One of the challenges we have with our current tool is that it doesn't sync with our G Suite directory when a person's manager changes or when a person has a name change -- their existing evaluations are linked to the person's old name and we have to change manually.
In my new app, I have an Employees datasource that includes a relation to the evaluation itself that was initially populated via the Directory API. Reading the documentation here, it seems as though I should be able to set up a watch on the Users resource to look for user updates and parse through them to make the appropriate name and manager changes in my Employees datasource. What I can't figure out, though, is what the receiving URL should be for the watch request.
If anyone has done this successfully within Google App Maker, or even solely within a Google Apps Script, I'd love to know how you did it.
EDITED TO ADD:
I created a silly little GAS test function to see if I can get #dimu-designs solution below to work. Unfortunately, I just get a Bad Request error. Here's what I have:
function setUserWatch() {
var optionalArgs = {
"event": "update"
};
var resource = {
"id": "10ff4786-4363-4681-abc8-28166022425b",
"type": "web_hook",
"address": "https://script.google.com/a/.../...hXlw/exec"
};
AdminDirectory.Users.watch(resource);
}
Address is the current web app URL.
EDITED TO ADD MORE:
The (in)ability to use GAS to receive web hooks has been an active issue/feature request since Sep 2014 -- https://issuetracker.google.com/issues/36761910 -- which has been #dimu-designs on top of for some time.
This is a more comprehensive answer.
Google supports push notifications across many of their APIs. However there are many subtle (and not so subtle) differences between them. Some that leverage webhooks send their data payloads primarily as HTTP headers; for example Drive API and Calendar API. Others mix their payloads across HTTP headers and a POST body(ex: AdminDirectory API). And its gets even crazier, with some APIs utilizing different mechanisms altogether (ex: GMail API leverages Cloud PubSub).
There are nuances to each but your goal is to leverage AdminDirectory push notifications in a GAS app. To do that you need a GAS Web App whose URL can serve as a web-hook endpoint.
STEP 1 - Deploy A Stand-Alone Script As A Web App
Let's start with the following template script and deploy it as a Web App from the Apps Script Editor menu Publish > Deploy As Web App:
/** HTTP GET request handler */
function doGet(e) {
return ContentService.createTextOutput("GET message");
}
/** HTTP POST request handler */
function doPost(e) {
return ContentService.createTextOutput("POST message");
}
STEP 2 - Verify/Validate Domain Ownership And Add/Register Domain
NOTE: As of August 2019, GAS Web App URLs can no longer be verified using this method. Google Cloud Functions may be a viable
alternative.
With the web app deployed you now have to verify and register the domain of the receiving url, which in this case is also the web app url. This url takes the following form:
https://script.google.com/macros/s/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/exec
Technically you cannot own a GAS web app url's domain. Thankfully the App Script Gods at Google do provide a mechanism to verify and register a GAS web app url.
From the Apps Script Editor menu select Publish > Register in Chrome Web Store. Registering a published web app with the Chrome Web Store also validates the URL's domain (no need to fiddle with the search console).
Once validated you need to add the "domain" via the Domain verification page in the API Console. The "domain" is everything in the url sans the 'exec', so you'll add a string that looks like this:
https://script.google.com/macros/s/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/
STEP 3 - Make a watch request
For this step the AdminSDK/Directory API service should be enabled both for your App Script project and in the API Console.
Create a function that generates a watch request (this can be retooled for other event types):
function startUpdateWatch() {
var channel = AdminDirectory.newChannel(),
receivingURL = "https://script.google.com/macros/s/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/exec",
gSuiteDomain = "[business-name].com",
event = "update";
channel.id = Utilities.getUuid();
channel.type = "web_hook";
channel.address = receivingURL + "?domain=" + gSuiteDomain + "&event=" + event;
channel.expiration = Date.now() + 21600000; // max of 6 hours in the future; Note: watch must be renew before expiration to keep sending notifications
AdminDirectory.Users.watch(
channel,
{
"domain":gSuiteDomain,
"event":event
}
);
}
Note that Directory API push notifications have an expiration, the max being 6 hours from starting the watch so it must be renewed periodically to ensure notifications are sent to the endpoint URL. Typically you can use a time-based trigger to call this function every 5 hours or so.
STEP 4 - Update doPost(e) trigger to handle incoming notifications
Unlike the push mechanisms of other APIs, the Directory API sends a POST body along with its notifications, so the doPost(e) method is guaranteed to be triggered when a notification is sent. Tailor the doPost(e) trigger to handle incoming events and re-deploy the web app:
function doPost(e) {
switch(e.parameter.event) {
case "update":
// do update stuff
break;
case "add":
break;
case "delete":
break;
}
return ContentService.createTextOutput("POST message");
}
There is one caveat to keep in mind. Push notifications for update events only tell you that the user's data was updated, it won't tell you exactly what was changed. But that's a problem for another question.
Note that there are a ton of details I left out but this should be enough to get you up and running.
You can do this with GAS and the Admin SDK. The Directory API supports Notifications (Note this is scheduled to be deprecated so not sure what is replacing this functionality). You can then set up a GMAIL script to do what you need to do with the notification.
UPDATE: There are also PUSH notifications from the Directory API.
I was able to set up push notifications for a local resource (a spreadsheet) using Heroku-based Node.js app as an intermediary API. The Node app captures the custom request headers and builds the payload to be consumed by the doPost(e) function of the GAS web app.
The code for constructing a watch request is simple
//get the unique id
var channelId = Utilities.getUuid();
//build the resource object
var resource = {
"id": channelId,
"type": "web_hook",
"address": "https://yourapp.herokuapp.com/drivesub
}
//watch the resource
Drive.Files.watch(resource, fileId);
The challenge is to get that domain address verified. There are ways to verify the standalone (not file-bound!) GAS web app, however, as previous posters have mentioned, the Apps Script web app can't access custom headers.
After you've enabled the Pub/Sub API and created the topic & subscription, go to APIs & Services -> Credentials -> Domain verification. It gives you a few options of verifying your domain, including serving the html file. Download the file generated by Google. Thankfully, Heroku makes it very easy to deploy a Node app
https://devcenter.heroku.com/articles/getting-started-with-nodejs
After your domain is verified you can make your subscription push data to the endpoint URL on Heroku.
I simply created the js file for route handlers and created one specifically for domain verification
handlers.verifyDomain = function(callback){
//Synchronously read from the static html file. Async method fs.readFile() doesn't make the file available to the router callback
var file = fs.readFileSync('./google/google_file.html');
callback(200, file);
}
Then include the handler in your router object like so:
var router = {
"google_file.html": handlers.verifyDomain
}
Finally, in your server starting function, get the path from the URL (there are multiple ways of doing that), and execute the handler;
var routeHandler = router(path);
routerHandler(function(statusCode, file){
//do something
});
Go back to domain verification tool and load the HTML page to verify the URL. After it's verified, the only remaining step is creating the GAS web app and posting to it.
Back to Node app. Note that my endpoint is https://yourapp.herokuapp.com/drivesub
//The code for posting data to GAS web app. Of course, you need to
// update your router with router['driveSub'] = handlers.driveSub
handlers.driveSub = function(data, callback){
var headers = data.headers;
var options = {
method:"POST",
uri: your_gas_app_url,
json:{"headers":headers}
};
//Use NPM to install the request module
request.post(options, function(err, httpResponse, body){
console.log(err, body);
});
callback(200, data);
}
Apps Script app - don't forget to publish it.
function doPost(req) {
var postData = req.postData["contents"];
var headers = JSON.parse(postData)["headers"];
//take action
return 200;
}
Unfortunately you cannot, at least not solely using Apps Script.
Admin Directory push notifications require a web-hook URL endpoint to receive notifications. You might think deploying a GAS web app and using its URL as an endpoint would be sufficient. But the thing with Admin Directory Push notifications is that its data payload resides in custom HTTP headers which cannot be accessed from a GAS Web App. (This also holds true for push notifications across other APIs including the Drive and Calendar APIs)
You can however leverage Google Cloud Functions (a GCP service) in tandem with GAS, but you'll have to know your way around Node.js.
EDIT
After giving this some thought, and reviewing your requirements I believe there is a way to pull this off just using GAS.
You can setup a unique push notification channel for a given event per user/domain (the 'update' event in your use case) by setting the event parameter when initializing the watch. Thus the GAS web app will only be triggered if an update event occurs; you don't really need to rely on the HTTP header to determine the event type.
If you want to track multiple events, you simply create a unique channel per event and use the same GAS Web app endpoint for each one. You differentiate between events by checking the event parameter sent in the POST request. This should remove the need for middle-man services such as Heroku or Google Cloud Functions.
I'd like to use the /watch (doc) API endpoint for subscribing a remote url to receive informations about changes in Google Drive.
The webhook received doesn't contain any body as specified in the documentation but contains headers which should help to understand what happened on the user's account.
Problem is that the headers doesn't contain anything useful and contains incorrect values too.
I'm saving a pageToken that I'm using to retrieve changes but when receiving multiple concurrent requests there's an high risk to receive the same change twice.
Is there a safe way to understand what happened hook by hook?
I would like to receive metadata or a summary of changes in my push notifications from the Google Drive API.
It would appear from the documentation that setting payload to true in the request body would accomplish this. As far as I can see, both file and change notifications do not include a request body even when payload is set to true when the notification channel is created.
The documentation states that payload is "A Boolean value to indicate whether payload is wanted. Optional."
Does anyone have any idea what this field does (if anything) and whether there is a way to include additional information?
If we check with other Google APIs with similar payload request usage, it looks like a flag to determine if the message body should be included in the notifications.
Take a look at the Google Admin SDK Push Notification, its almost similar with Drive API push notification is doing.