正在同步日历会议更改

用户可以自由更新或删除其 Google 日历活动。如果用户在为活动创建会议后更新了活动,您的插件可能需要通过更新会议数据来响应此更改。如果您的第三方会议系统依赖于对活动数据进行跟踪,则未能在活动变更时更新会议可能会导致会议无法使用,进而导致用户体验不佳。

根据 Google 日历活动的变化及时更新会议数据的过程称为“同步”。您可以创建一个 Apps 脚本可安装触发器来同步活动更改,该触发器会在给定日历中的事件发生更改时触发。遗憾的是,该触发器不会报告哪些事件发生了更改,因此您无法将其限制为仅包含您创建的会议的事件。相反,您必须请求列出自上次同步以来对日历所做的所有更改,过滤事件列表,并相应地进行更新。

常规同步过程如下:

  1. 用户首次创建会议时,系统会初始化同步过程。
  2. 每当用户创建、更新或删除其中一个日历活动时,触发器都会在您的插件项目中执行触发器函数
  3. 触发器函数会检查自上次同步以来的事件更改集,并确定是否需要更新关联的第三方会议。
  4. 所有必要的更新均通过发出第三方 API 请求对会议进行。
  5. 系统会存储新的同步令牌,以便下次触发器执行只需要检查对日历的最新更改。

初始化同步

插件在第三方系统上成功创建会议后,应该创建一个可安装触发器,用于响应此日历中的事件更改(如果触发器尚不存在)。

创建触发器后,应通过创建初始同步令牌来完成初始化。具体方法是直接执行触发器函数。

创建日历触发器

如要进行同步,您的插件需要检测包含会议的日历活动何时更改。此操作通过创建 EventUpdated 可安装触发器来实现。您的插件只需为每个日历创建一个触发器,并且可以以编程方式创建它们。

创建触发器的最佳时机是用户创建第一场会议时,因为那时用户就会开始使用该插件。在创建会议并确认没有错误后,您的插件应检查此触发器是否存在(对于此用户),以及是否不存在。

实现同步触发器函数

当 Apps 脚本检测到导致触发器触发的条件时,系统会执行触发器函数。EventUpdated 日历触发器在用户创建、修改或删除指定日历中的任何活动时触发。

您必须实现插件使用的触发器函数。此触发器函数应执行以下操作:

  1. 使用 syncToken 进行 Google 日历高级服务 Calendar.Events.list() 调用,以检索自上次同步以来发生更改的活动列表。通过使用同步令牌,您可以减少插件必须检查的事件数量。

    如果在没有有效同步令牌的情况下执行触发器函数,则会退避到完全同步。完全同步只是尝试在规定的时间范围内检索所有事件,以生成新的有效的同步令牌。

  2. 系统会检查每个修改后的事件,以确定它是否具有关联的第三方会议。
  3. 如果活动包含会议,系统会检查该活动以查看更改的内容。 根据更改的情况,可能需要修改相关会议。例如,如果删除了一项活动,那么该插件可能也会删除相应会议。
  4. 对会议进行的任何必要的更改都可以通过对第三方系统进行 API 调用来进行。
  5. 完成所有必要的更改后,存储 Calendar.Events.list() 方法返回的 nextSyncToken。此同步令牌可在 Calendar.Events.list() 调用所返回结果的最后一页上找到。

更新 Google 日历活动

在某些情况下,您可能需要在执行同步时更新 Google 日历活动。如果您选择执行此操作,请通过相应的 Google 日历高级服务请求更新活动。请务必使用带有 If-Match 标头的条件更新。这可以防止您的更改意外覆盖用户在其他客户端中进行的并发更改。

示例

以下示例展示了如何为日历活动及其关联会议设置同步。

/**
 *  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' || eventHasConference(calEvent)) {
    // 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.

  }
}