Alternate Runtimes for G Suite Add-ons is coming soon. Learn more.

Calendar conferencing add-on example

The following shows an example of a G Suite add-on that extends Calendar to provide syncing with a fictional web conferencing service called "My Web Conferencing". When installed, this add-on lets users see My Web Conferencing as a conferencing option when editing Google Calendar events.

The sample shows conference creation, event syncing, and hosts a simple add-on setting page by deploying the add-on script as a web app. The sample disables homepages.

Add-on Manifest

{
  "addOns": {
    "calendar": {
      "conferenceSolution": [{
        "id": 1,
        "name": "My Web Conference",
        "logoUrl": "https://lh3.googleusercontent.com/...",
        "onCreateFunction": "createConference"
      }],
      "createSettingsUrlFunction": "createSettingsUrl",
      "currentEventAccess": "READ_WRITE",
    },
    "common": {
      "homepageTrigger": {
        "enabled": false
      },
      "logoUrl": "https://lh3.googleusercontent.com/...",
      "name": "My Web Conferencing"
    },
  },
  "timeZone": "America/New_York",
  "dependencies": {
    "enabledAdvancedServices": [
    {
      "userSymbol": "Calendar",
      "serviceId": "calendar",
      "version": "v3"
    },
    ]
  },
  "webapp": {
    "access": "ANYONE",
    "executeAs": "USER_ACCESSING"
  },
  "exceptionLogging": "STACKDRIVER",
  "oauthScopes": [
    "https://www.googleapis.com/auth/calendar.addons.execute",
    "https://www.googleapis.com/auth/calendar.events.readonly",
    "https://www.googleapis.com/auth/calendar.addons.current.event.write",
    "https://www.googleapis.com/auth/script.external_request",
    "https://www.googleapis.com/auth/script.scriptapp"
  ],
  ...
}

CreateConf.gs

/**
 *  Creates a conference, then builds and returns a ConferenceData object
 *  with the corresponding conference information. This method is called
 *  when a user selects a conference solution defined by the add-on that
 *  uses this function as its 'onCreateFunction' in the add-on manifest.
 *
 *  @param {Object} arg The default argument passed to a 'onCreateFunction';
 *      it carries information about the Google Calendar event.
 *  @return {ConferenceData}
 */
function createConference(arg) {
  const eventData = arg.eventData;
  const calendarId = eventData.calendarId;
  const eventId = eventData.eventId;

  // Retrieve the Calendar event information using the Calendar
  // Advanced service.
  var calendarEvent;
  try {
    calendarEvent = Calendar.Events.get(calendarId, eventId);
  } catch (err) {
    // The calendar event does not exist just yet; just proceed with the
    // given event ID and allow the event details to sync later.
    console.log(err);
    calendarEvent = {
      id: eventId,
    };
  }

  // Create a conference on the third-party service and return the
  // conference data or errors in a custom JSON object.
  var conferenceInfo = create3rdPartyConference(calendarEvent);

  // Build and return a ConferenceData object, either with conference or
  // error information.
  var dataBuilder = ConferenceDataService.newConferenceDataBuilder();

  if (!conferenceInfo.error) {
    // No error, so build the ConferenceData object from the
    // returned conference info.

    var phoneEntryPoint = ConferenceDataService.newEntryPoint()
        .setEntryPointType(ConferenceDataService.EntryPointType.PHONE)
        .setUri('tel:+' + conferenceInfo.phoneNumber)
        .setPin(conferenceInfo.phonePin);

    var adminEmailParameter = ConferenceDataService.newConferenceParameter()
        .setKey('adminEmail')
        .setValue(conferenceInfo.adminEmail);

    dataBuilder.setConferenceId(conferenceInfo.id)
        .addEntryPoint(phoneEntryPoint)
        .addConferenceParameter(adminEmailParameter)
        .setNotes(conferenceInfo.conferenceLegalNotice);

    if (conferenceInfo.videoUri) {
      var videoEntryPoint = ConferenceDataService.newEntryPoint()
          .setEntryPointType(ConferenceDataService.EntryPointType.VIDEO)
          .setUri(conferenceInfo.videoUri)
          .setPasscode(conferenceInfo.videoPasscode);
      dataBuilder.addEntryPoint(videoEntryPoint);
    }

    // Since the conference creation request succeeded, make sure that
    // syncing has been enabled.
    initializeSyncing(calendarId, eventId, conferenceInfo.id);

  } else if (conferenceInfo.error === 'AUTH') {
    // Authenentication error. Implement a function to build the correct
    // authenication URL for the third-party conferencing system.
    var authenticationUrl = getAuthenticationUrl();
    var error = ConferenceDataService.newConferenceError()
        .setConferenceErrorType(
            ConferenceDataService.ConferenceErrorType.AUTHENTICATION)
        .setAuthenticationUrl(authenticationUrl);
    dataBuilder.setError(error);

  } else {
    // Other error type;
    var error = ConferenceDataService.newConferenceError()
        .setConferenceErrorType(
            ConferenceDataService.ConferenceErrorType.TEMPORARY);
    dataBuilder.setError(error);
  }

  // Don't forget to build the ConferenceData object.
  return dataBuilder.build();
}


/**
 *  Contact the third-party conferencing system to create a conference there,
 *  using the provided calendar event information. Collects and retuns the
 *  conference data returned by the third-party system in a custom JSON object
 *  with the following fields:
 *
 *    data.adminEmail - the conference administrator's email
 *    data.conferenceLegalNotice - the conference legal notice text
 *    data.error - Only present if there was an error during
 *         conference creation. Equal to 'AUTH' if the add-on user needs to
 *         authorize on the third-party system.
 *    data.id - the conference ID
 *    data.phoneNumber - the conference phone entry point phone number
 *    data.phonePin - the conference phone entry point PIN
 *    data.videoPasscode - the conference video entry point passcode
 *    data.videoUri - the conference video entry point URI
 *
 *  The above fields are specific to this example; which conference information
 *  you add-on needs is dependent on the third-party conferencing system
 *  requirements.
 *
 * @param {Object} calendarEvent A Calendar Event resource object returned by
 *     the Google Calendar API.
 * @return {Object}
 */
function create3rdPartyConference(calendarEvent) {
  var data = {};

  // Get the add-on settings information to pass to the third-party system.
  // Alternatively, store the add-on setting information on the third-party
  // system.
  var props = PropertiesService.getUserProperties();
  var disableVideo = props.getProperty('disableVideo') || "false";
  var namePrefix = props.getProperty('namePrefix')

  // Implementation details dependent on the third-party system API.
  // Typically one or more API calls are made to create the conference and
  // acquire its relevant data, which is then put in to the returned JSON
  // object.
  ...

  return data;
}

/**
 *  Return the URL used to authenticate the user with the third-party
 *  conferencing system.
 *
 *  @return {String}
 */
function getAuthenticationUrl() {
  var url;
  // Implementation details dependent on the third-party system.
  ...

  return url;
}

Syncing.gs

/**
 *  Initializes syncing of conference data by creating a sync trigger and
 *  sync token if either does not exist yet.
 *
 *  @param {String} calendarId The ID of the Google Calendar.
 */
function initializeSyncing(calendarId) {
  // Create a syncing trigger if it doesn't exist yet.
  createSyncTrigger(calendarId);

  // Perform an event sync to create the initial sync token.
  syncEvents({'calendarId': calendarId});
}

/**
 *  Creates a sync trigger if it does not exist yet.
 *
 *  @param {String} calendarId The ID of the Google Calendar.
 */
function createSyncTrigger(calendarId) {
  // Check to see if the trigger already exists; if does, return.
  var allTriggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < allTriggers.length; i++) {
    var trigger = allTriggers[i];
    if (trigger.getTriggerSourceId() == calendarId) {
      return;
    }
  }

  // Trigger does not exist, so create it. The trigger calls the
  // 'syncEvents()' trigger function when it fires.
  var trigger = ScriptApp.newTrigger('syncEvents')
      .forUserCalendar(calendarId)
      .onEventUpdated()
      .create();
}

/**
 *  Sync events for the given calendar; this is the syncing trigger
 *  function. If a sync token already exists, this retrieves all events
 *  that have been modified since the last sync, then checks each to see
 *  if an associated conference needs to be updated and makes any required
 *  changes. If the sync token does not exist or is invalid, this
 *  retrieves future events modified in the last 24 hours instead. In
 *  either case, a new sync token is created and stored.
 *
 *  @param {Object} e If called by a event updated trigger, this object
 *      contains the Google Calendar ID, authorization mode, and
 *      calling trigger ID. Only the calendar ID is actually used here,
 *      however.
 */
function syncEvents(e) {
  var calendarId = e.calendarId;
  var properties = PropertiesService.getUserProperties();
  var syncToken = properties.getProperty('syncToken');

  var options;
  if (syncToken) {
    // There's an existing sync token, so configure the following event
    // retrieval request to only get events that have been modified
    // since the last sync.
    options = {
      syncToken: syncToken
    };
  } else {
    // No sync token, so configure to do a 'full' sync instead. In this
    // example only recently updated events are retrieved in a full sync.
    // A larger time window can be examined during a full sync, but this
    // slows down the script execution. Consider the trade-offs while
    // designing your add-on.
    var now = new Date();
    var yesterday = new Date();
    yesterday.setDate(now.getDate() - 1);
    options = {
      timeMin: now.toISOString(),          // Events that start after now...
      updatedMin: yesterday.toISOString(), // ...and were modified recently
      maxResults: 50,   // Max. number of results per page of responses
      orderBy: 'updated'
    }
  }

  // Examine the list of updated events since last sync (or all events
  // modified after yesterday if the sync token is missing or invalid), and
  // update any associated conferences as required.
  var events;
  var pageToken;
  do {
    try {
      options.pageToken = pageToken;
      events = Calendar.Events.list(calendarId, options);
    } catch (err) {
      // Check to see if the sync token was invalidated by the server;
      // if so, perform a full sync instead.
      if (err.message ===
            "Sync token is no longer valid, a full sync is required.") {
        properties.deleteProperty('syncToken');
        syncEvents(e);
        return;
      } else {
        throw new Error(err.message);
      }
    }

    // Read through the list of returned events looking for conferences
    // to update.
    if (events.items && events.items.length > 0) {
      for (var i = 0; i < events.items.length; i++) {
         var calEvent = events.items[i];
         // Check to see if there is a record of this event has a
         // conference that needs updating.
         if (eventHasConference(calEvent)) {
           updateConference(calEvent, calEvent.conferenceData.conferenceId);
         }
      }
    }

    pageToken = events.nextPageToken;
  } while (pageToken);

  // Record the new sync token.
  if (events.nextSyncToken) {
    properties.setProperty('syncToken', events.nextSyncToken);
  }
}

/**
 *  Returns true if the specified event has an associated conference
 *  of the type managed by this add-on; retuns false otherwise.
 *
 *  @param {Object} calEvent The Google Calendar event object, as defined by
 *      the Calendar API.
 *  @return {boolean}
 */
function eventHasConference(calEvent) {
  var name = calEvent.conferenceData.conferenceSolution.name || null;

  // This version checks if the conference data solution name matches the
  // one of the solution names used by the add-on. Alternatively you could
  // check the solution's entry point URIs or other solution-specific
  // information.
  if (name) {
    if (name === "My Web Conference" ||
        name === "My Recorded Web Conference") {
      return true;
    }
  }
  return false;
}

/**
 *  Update a conference based on new Google Calendar event information.
 *  The exact implementation of this function is highly dependant on the
 *  details of the third-party conferencing system, so only a rough outline
 *  is shown here.
 *
 *  @param {Object} calEvent The Google Calendar event object, as defined by
 *      the Calendar API.
 *  @param {String} conferenceId The ID used to identify the conference on
 *      the third-party conferencing system.
 */
function updateConference(calEvent, conferenceId) {
  // Check edge case: the event was cancelled
  if (calEvent.status === 'cancelled') {
    // Use the third-party API to delete the conference too.
    ...

  } else {
    // Extract any necessary information from the event object, then
    // make the appropriate third-party API requests to update the
    // conference with that information.
    ...
  }
}

Settings.gs

/**
 *  Builds and returns the URL that leads to the settings page for this
 *  add-on.
 *
 *  @return {String}
 */
function createSettingsUrl() {
  // Returns the URL of this script's web app deployment. You
  // can optionally add URL parameters here if desired.
  return ScriptApp.getService().getUrl();
}


/**
 * Serves HTML of the add-on setting page.
 *
 * @param {Object} e event parameter that can contain information
 *     about any URL parameters provided.
 * @return {Object}
 */
function doGet(e) {
  var html = HtmlService.createHtmlOutputFromFile('Settings');
  return html.setTitle('My Web Conferencing Add-on Settings');
}

/**
 * Extracts and returns add-on settings from the Apps Script Properties service.
 * Alternatively, setttings can be stored on the third-party conferencing
 * system, in which case this method should make an appropriate API call to
 * retrieve them.
 *
 * @return {Object}
 */
function getAddonSettings() {
  var props = PropertiesService.getUserProperties();
  var settings = {
    disableVideo: props.getProperty('disableVideo') || 'false';
    namePrefix: props.getProperty('namePrefix') || '';
  }
  return settings;
}

/**
 * Saves the specified add-on settings to the Apps Script Properties service.
 * Alternatively, setttings can be stored on the third-party conferencing
 * system, in which case this method should make an appropriate API call to
 * store them.
 *
 * @param {Object} settings A collection of setting values to store.
 */
function saveAddonSettings(settings) {
  var props = PropertiesService.getUserProperties();
  props.setProperty('disableVideo', settings.disableVideo);
  props.setProperty('namePrefix', settings.namePrefix);
}

Settings.html

<!DOCTYPE html>
<html>
  <head>
    <base target="_top">
    <!-- This CSS package applies Google styling. -->
    <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons.css">
    <style>
      .error {
        color: #FF0000;
      }
      .hidden {
        display: none;
      }
    </style>
  </head>

  <body>
    <h1 id="main-heading">Loading...</h1>
    <div class="block" id="results">
      <form name="settings-form" id="settings-form">
        <div>
          <label for="name-prefix">Default meeting name prefix: </label>
          <input type="text" id="name-prefix" name="name-prefix">
        </div>
        <div>
          <input type="checkbox" id="disable-video">
          <label for="disable-video">
            Disable all video conferencing entry points</label>
        </div>
        <input type="submit" name="save" id="save-button" value="Save Settings"/>
      </form>
    </div>

    <script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
    <script>
      var headingText = "My Web Conference Add-on Settings";

      /**
       * Run initializations on web app load.
       */
      $(function() {
        $('#settings-form').bind('submit', onSettingsSave);

        // Call the server here to retrieve any information needed to
        // build the page.
        google.script.run
           .withSuccessHandler(function(settings) {
                // Update the setting page values with the retrieved results.
                updateDisplay(settings);
              })
           .withFailureHandler(function(msg) {
                // Report failures in the settings page; any thrown messages are
                // passed here as 'msg'.
                $('#main-heading').text(
                    "Error retrieving setting information: " + msg);
                $('#main-heading').addClass("error");
              })
           .getAddonSettings();
      });

      /**
       * Updates display of setting information.
       *
       * @param {Object} settings Setting information returned by the server.
       */
      function updateDisplay(settings) {
        $('#main-heading').text(headingText);
        $('#disable-video').prop('checked', settings.disableVideo === 'true');
        $('#name-prefix').val(settings.namePrefix);
      }

      function onSettingsSave() {
        $('#main-heading').text('Saving...');
        var settings = {
          disableVideo: $('#disable-video').prop('checked');
          namePrefix: $('#name-prefix').val();
        };

        // Call the server here to save settings.
        google.script.run
           .withSuccessHandler(function() {
                // Respond to success conditions here.
                $('#main-heading').text(headingText);
              })
           .withFailureHandler(function(msg) {
                // Report failures in the settings page; any thrown messages are
                // passed here as 'msg'.
                $('#main-heading').text(
                    "Error saving setting information: " + msg);
                $('#main-heading').addClass("error");
              })
           .saveAddonSettings(settings);
        return false;
      }
    </script>
  </body>
</html>