3

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.

Lynn Hoffman
  • 200
  • 11

4 Answers4

6

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.

TheAddonDepot
  • 8,408
  • 2
  • 20
  • 30
  • ...And finally, all of the pieces come together! Thank you *so* much for this very clear assembly of so many bits from so many different places. – Lynn Hoffman Aug 22 '18 at 23:53
  • I followed all theses steps and still, I can see in the logs that my doPost function is never reached.. How can I be sure that the watch request has really worked ? – Benbb96 Sep 12 '19 at 15:24
  • @Benbb96 Did you redeploy your web app to a new version after making changes to the code? If not, you'll need to redeploy your web app to a new version to ensure that any changes take hold (clicking just the update button is not sufficient). Its a known quirk with GAS Web Apps... – TheAddonDepot Sep 12 '19 at 15:33
  • @DimuDesigns Yes I've done that everytime I made a change in the doPost(). I also checked if the URL changed but it doesn't seem to. – Benbb96 Sep 12 '19 at 15:36
  • @Benbb96 Which Logging method are you using `Logger.log()` or `console.log()`? You can't use `Logger.log` with `doPost()`, you'll need to use StackDriver's `console.log()` method. Logs generated by `console.log()` should show up in your [Apps Script Developer Dashboard/Hub](https://script.google.com/home) under the `My Executions` section (find the name of your project in the list and then click the expand icon to the far right). – TheAddonDepot Sep 12 '19 at 15:56
  • @DimuDesigns Yes I've noticed that too, so I put both to be sure. I can correctly see the logs on StackDriver's when I access my script through GET method. But when I add a user in the Google admin or when I use the AdminSDK to create a new user within a script, the doPost function is never triggered. I think I will redo all the process and if it still does not work, I will open my own question here. – Benbb96 Sep 13 '19 at 08:08
  • So I restarted from the beginning and I did all the steps correctly (I think). First time I didn't manage to "Register in Chrome Web Store" because I didn't enable and accept the conditions of the Chrome Web Store. But even if I've done it the second time (I published the app for the users in my GSuite), it still doesn't work at the end, the post function is never executed by doing an action in the admin or through an API request with the Admin SDK... Maybe it has to do with the fact that I am working in a GSuite domain I don't know. In the end, I've found another way to do the thing I wanted. – Benbb96 Sep 19 '19 at 06:21
1

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.

Jon Pellant
  • 388
  • 3
  • 13
  • My understanding is that the watch method for a given resource type is what's supposed to replace Notifications. For users, that documentation is [here](https://developers.google.com/admin-sdk/directory/v1/reference/users/watch). It's just not clear to me how to implement that within GAS without a receiving URL for the push notification. – Lynn Hoffman Aug 22 '18 at 13:03
  • @dimu-designs 's answer suggests that won't work, either. But thanks for the suggestion. – Lynn Hoffman Aug 22 '18 at 13:18
  • @LynnHoffman Yes, but for your needs the `email` and `kind` files might be good enough. Then just go fetch the details. { "kind": "admin#directory#user", "id": "111220860655841818702", "etag": "\"Mf8RAmnABsVfQ47MMT_18MHAdRE/evLIDlz2Fd9zbAqwvIp7Pzq8UAw\"", "primaryEmail": "user@mydomain.com" } – Jon Pellant Aug 22 '18 at 13:26
  • @JonPellant That won't be sufficient. To do anything worthwhile the OP will need to know the event that triggered the notification and that's stored in the `X-Goog-Resource-State` HTTP header. – TheAddonDepot Aug 22 '18 at 13:36
  • @JonPellant But your approach may still be applicable...see my updated response – TheAddonDepot Aug 22 '18 at 14:06
1

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.

TheAddonDepot
  • 8,408
  • 2
  • 20
  • 30
  • Sadly, that's not the answer I was looking for. But it definitely answers my question. Thanks, @Dimu! – Lynn Hoffman Aug 22 '18 at 13:17
  • @LynnHoffman well, you can do it using Heroku as an in-between service. Please see my answer – Anton Dementiev Aug 22 '18 at 13:45
  • I don't know if I'm just misunderstanding, but when I try to create the channel, I keep getting a Bad Request error. I'll post my test function as an update to my question. – Lynn Hoffman Aug 22 '18 at 16:58
  • The ability to use GAS to receive web hooks has been an active issue/feature request since Sep 2014: [https://issuetracker.google.com/issues/36761910](https://issuetracker.google.com/issues/36761910). I'll look at the Google Cloud Function as an alternative. – Lynn Hoffman Aug 22 '18 at 17:24
  • @LynnHoffman Drive API Push Notifications are a special case since the `X-Goog-Resource-State` HTTP header is the only way to track changes to drive files. Push Notifications for the Admin Directory API are a different thing however, and it does offer a bit more leeway as you're able to track events via url parameters. So you should still be able to do this using GAS Web Apps. I'm going to run a few experiments and write something up. – TheAddonDepot Aug 22 '18 at 19:56
  • And since I really am just looking for update events, I'm guessing that makes it more straightforward? – Lynn Hoffman Aug 22 '18 at 20:30
1

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;

}
Anton Dementiev
  • 5,451
  • 4
  • 20
  • 32