同步處理日曆會議變更

使用者可以自由更新或刪除 Google 日曆活動。如果使用者在建立會議後更新活動,外掛程式可能需要更新會議資料以回應變更。如果第三方會議系統仰賴追蹤事件資料,無法在活動變更時更新會議,可能會導致會議無法使用,並導致使用者體驗不佳。

根據 Google 日曆活動的變更,隨時更新會議資料的程序稱為「同步處理」。如要同步處理事件變更,您可以建立 Apps Script 可安裝觸發條件,在特定日曆中的事件發生變更時觸發。遺憾的是,觸發條件不會回報哪些事件發生變化,且您無法限制系統只會顯示您已建立的會議活動。相反地,您必須要求一份日曆自上次同步處理以來對日曆所做的所有變更,然後篩選事件清單,並據此進行更新。

一般同步處理程序如下:

  1. 使用者首次建立會議時,系統會初始化同步處理程序。
  2. 每當使用者建立、更新或刪除任一日曆活動時,該觸發條件會在外掛程式專案中執行觸發條件函式
  3. 觸發條件函式會檢查自上次同步處理以來的事件變更組合,判斷是否需要更新相關聯的第三方會議。
  4. 如要對會議進行任何必要的更新,您必須發出第三方 API 要求。
  5. 系統會儲存新的同步處理權杖,因此下一個觸發條件執行作業只需要檢查日曆的最新變更。

初始化同步處理

外掛程式在第三方系統上成功建立會議後,應建立可安裝的觸發條件,如果觸發條件尚未存在,就會回應這個日曆中的事件變更

建立觸發條件後,初始化作業應透過建立初始同步處理權杖來完成。方法是直接執行觸發條件。

建立日曆觸發條件

如要同步,外掛程式需要偵測含有會議附加會議的日曆活動何時變更。方法是建立 EventUpdated 可安裝的觸發條件。外掛程式只需要為每個日曆設定一個觸發條件,即可以程式輔助方式建立。

建立觸發條件的最佳時機就是使用者建立第一場會議,因為使用者會開始使用外掛程式。建立會議並確認沒有任何錯誤後,您的外掛程式應檢查觸發條件是否適用於此使用者,且如果沒有建立。

實作同步處理觸發條件函式

Apps Script 偵測到導致觸發條件觸發的條件時,就會執行觸發條件。EventUpdated 日曆觸發條件會在使用者建立、修改或刪除指定日曆中任何活動時觸發。

您必須導入外掛程式使用的觸發條件函式。這個觸發條件函式應執行以下操作:

  1. 使用 syncToken 發出日曆進階服務 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.

  }
}