google-apps-scriptgoogle-app-maker

Is it possible to watch Directory API changes from Google App Maker/Google Apps Script?


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.


Solution

  • 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.