2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

TypeScript + claspで Google Calendarの更新情報をGoogle Chatに通知するGoogle Apps Scriptを作成する

Last updated at Posted at 2022-01-28

TypeScript + clasp で Google Calender API を操作するGoogle Apps Script(以下、gas)を作成する。

TypeScript + clasp の環境構築については過去の記事を参照。

gasプロジェクトを作成する

まずは対象となるgasプロジェクトを作成及びTypeScript用の環境設定を行う

# 任意のディレクトリへ移動する
cd /path/to/project

# Google Calendar API を操作するプロジェクトを作成する
mkdir calendar-api-sample

# gasプロジェクトを作成する
clasp create --type standalone

# gas用のTypeScript定義をインストール
npm i -S @types/google-apps-script

ここまでのフォルダ構成

$ tree -a -L 1
.
├── .clasp.json
├── appsscript.json
├── node_modules
├── package-lock.json
└── package.json

プロジェクトにGoogle Calendar APIを追加する

GCPのプロジェクトIDを使わずにclasp上で追加する方法がわからなかったので、gasのコンソール上で追加した後、pullをする。

サービスの追加
スクリーンショット 2022-01-28 17.57.25.png

ローカルプロジェクトのアップデート(clasp pull)

clasp pull
Warning: files in subfolder are not accounted for unless you set a '/workspaces/gas-sample/calendar-api-sample/.claspignore' file.
Cloned 1 file.
└─ /workspaces/gas-sample/calendar-api-sample/appsscript.json

appscript.jsonにCalenderへのdependenciesが追加されていることを確認する。

スクリーンショット 2022-01-28 18.11.59.png

※つまりはappscript.jsonのdependnciesに以下の内容を手書きすればgasのコンソール上じゃなくてもできるようです。

appscript.json
  "dependencies": {
    "enabledAdvancedServices": [
      {
        "userSymbol": "Calendar",
        "version": "v3",
        "serviceId": "calendar"
      }
    ]
  }

Google Caldendar APIを使った実装を試してみる

Google Calendarの変更をトリガーにしたアプリケーションを作ってみます。
機能の流れとしては以下を想定しています。

  1. Google Calendarからの変更通知を受ける
  2. 変更されたカレンダーのイベントを取得する
  3. 変更されたカレンダーのイベントの内容をGoogle Chatに送信する

Google Calendarからの通知を受け取る

まずは変更を受け取るための関数を用意します。

Google Calendarからどういったイベントが通知されるのか、公式ドキュメントから確認します。

上記より、Google CalendarからEventが発生した場合、関数に渡されるEventObjectは以下のプロパティが設定されていることがわかります。

property name description
authMode ScriptApp.AuthMode の値。
例: FULL
calendarId イベントの更新が発生したカレンダーのID。
例: susan@example.com
triggerUid このイベントを生成したトリガーのID。
例:4034124084959907503

つまりEventからcalendarIdを取得したい場合は以下となります。

main.ts
interface CalendarUpdatedEvnet {
  authMode: GoogleAppsScript.Script.AuthMode;
  calendarId: string;
  triggerUid: string;
}

function onUpdatedEvent(e: CalendarUpdatedEvnet) {
  console.log(`calendarId: ${e.calendarId}`);
}

上記のソースをclasp pushでgasプロジェクトへUploadしましょう。

main.gs
// Compiled using undefined undefined (TypeScript 4.5.5)
"use strict";
function onUpdatedEvent(e) {
    const calendarId = e.calendarId;
    console.log(`calendarId: ${calendarId}`);
}

interfaceはjsへトランスコンパイルされると消えてしまう悲しいさだめをもったオブジェクトです)

トリガーを作成し、先ほど作ったonUpdatedEventを実行する関数として指定します。

スクリーンショット 2022-01-28 19.18.22.png

先ほどトリガーのソースとして指定したカレンダーを更新すると、無事トリガーが動作し、ログが表示されました。

スクリーンショット 2022-01-28 19.21.21.png

変更されたカレンダーの情報を取得する

公式ドキュメントを参考に変更されたカレンダーのイベントを取得するスクリプトを書いていきます。

main.ts
interface CalendarUpdatedEvnet {
  authMode: GoogleAppsScript.Script.AuthMode;
  calendarId: string;
  triggerUid: string;
}

function onUpdatedEvent(e: CalendarUpdatedEvnet) {
  console.log(`calendarId: ${e.calendarId}`);
  getUpdatedEvents(e.calendarId).forEach((event)=>{
    console.log(`title:${event.summary}`)
  })
}

/**
 * 本機能で使用するOptionのみを抜粋している。
 * Optionの全定義についてはlinkを参照。
 * 
 * @link https://developers.google.com/calendar/api/v3/reference/events/list#parameters
 */
class CalendarQueryOptions {
  maxResults?: number;
  syncToken?: string;
  timeMin?: string;
}

/**
 * 指定されたCalendarIdに紐付くイベントを前回取得した時から更新があったイベントのみ取得する。
 * 過去に一度もイベントを取得していない場合、過去30日分を取得する。
 *
 * @param {string} calendarId カレンダーを一意に識別するID
 * @returns {GoogleAppsScript.Calendar.Schema.Event[]} イベントの一覧(最大100件)
 */
function getUpdatedEvents(
  calendarId: string,
): GoogleAppsScript.Calendar.Schema.Event[] {
  const properties = PropertiesService.getUserProperties();
  const key = `syncToken:${calendarId}`;
  const syncToken = properties.getProperty(key);

  let options: CalendarQueryOptions = { maxResults: 100 };
  if (syncToken) {
    options = { ...options, syncToken: syncToken };
  } else {
    options = { ...options, timeMin: getRelativeDate(-30, 0).toISOString() };
  }

  const events = Calendar.Events?.list(calendarId, options);

  if (events?.nextSyncToken) {
    properties.setProperty(key, events?.nextSyncToken);
  }

  return events?.items ? events.items : [];
}

/**
 * Helper function to get a new Date object relative to the current date.
 * @param {number} daysOffset The number of days in the future for the new date.
 * @param {number} hour The hour of the day for the new date, in the time zone
 *     of the script.
 * @return {Date} The new date.
 */
function getRelativeDate(daysOffset: number, hour: number) {
  var date = new Date();
  date.setDate(date.getDate() + daysOffset);
  date.setHours(hour);
  date.setMinutes(0);
  date.setSeconds(0);
  date.setMilliseconds(0);
  return date;
}

書き終わったら再度clasp pushをします。
以下は実際にアップロードされたソースです。

main.gs
// Compiled using undefined undefined (TypeScript 4.5.5)
"use strict";
function onUpdatedEvent(e) {
    console.log(`calendarId: ${e.calendarId}`);
    getUpdatedEvents(e.calendarId).forEach((event) => {
        console.log(`title:${event.summary}`);
    });
}
/**
 * 本機能で使用するOptionのみを抜粋している。
 * Optionの全定義についてはlinkを参照。
 *
 * @link https://developers.google.com/calendar/api/v3/reference/events/list#parameters
 */
class CalendarQueryOptions {
}
/**
 * 指定されたCalendarIdに紐付くイベントを前回取得した時から更新があったイベントのみ取得する。
 * 過去に一度もイベントを取得していない場合、過去30日分を取得する。
 *
 * @param {string} calendarId カレンダーを一意に識別するID
 * @returns {GoogleAppsScript.Calendar.Schema.Event[]} イベントの一覧(最大100件)
 */
function getUpdatedEvents(calendarId) {
    var _a;
    const properties = PropertiesService.getUserProperties();
    const key = `syncToken:${calendarId}`;
    const syncToken = properties.getProperty(key);
    let options = { maxResults: 100 };
    if (syncToken) {
        options = Object.assign(Object.assign({}, options), { syncToken: syncToken });
    }
    else {
        options = Object.assign(Object.assign({}, options), { timeMin: getRelativeDate(-30, 0).toISOString() });
    }
    const events = (_a = Calendar.Events) === null || _a === void 0 ? void 0 : _a.list(calendarId, options);
    if (events === null || events === void 0 ? void 0 : events.nextSyncToken) {
        properties.setProperty(key, events === null || events === void 0 ? void 0 : events.nextSyncToken);
    }
    return (events === null || events === void 0 ? void 0 : events.items) ? events.items : [];
}
/**
 * Helper function to get a new Date object relative to the current date.
 * @param {number} daysOffset The number of days in the future for the new date.
 * @param {number} hour The hour of the day for the new date, in the time zone
 *     of the script.
 * @return {Date} The new date.
 */
function getRelativeDate(daysOffset, hour) {
    var date = new Date();
    date.setDate(date.getDate() + daysOffset);
    date.setHours(hour);
    date.setMinutes(0);
    date.setSeconds(0);
    date.setMilliseconds(0);
    return date;
}

トリガーが設定されているCalendarを更新し、イベントがロギングされることを確認します。

スクリーンショット 2022-01-28 20.30.37.png

無事カレンダーのタイトルが表示されました。

変更されたカレンダーのイベントの内容をGoogle Chatに送信する

最後は、完全に蛇足ですが、取得したイベントをGoogle Chatに送信します。
Google ChatはGoogle Workspacesがないと使用できない(はず)です。

公式サイトを参考にコードを書いていきます。
gasにおけるHTTPリクエストについてはこちらを使用するようです。

main.ts
interface CalendarUpdatedEvnet {
  authMode: GoogleAppsScript.Script.AuthMode;
  calendarId: string;
  triggerUid: string;
}

const WEBHOOK_URL = "<WEBHOOK_URL>"

function onUpdatedEvent(e: CalendarUpdatedEvnet) {
  console.log(`calendarId: ${e.calendarId}`);
  getUpdatedEvents(e.calendarId).forEach((event) => {
    let message: string;
    if (event.status === "cancelled") {
      message = `
      イベントが削除されました。
      `
    } else {
      message = `
      イベントが更新されました。\n
      *${event.summary}*
      ==================================================\n
      開始日時:${event.start?.dateTime}\n
      終了日時:${event.end?.dateTime}\n
      説明  :${event.description}\n
      <${event.htmlLink}|LINK>
      `
    }
    sendMessageToChat(WEBHOOK_URL, message)
  });
}

/**
 * 本機能で使用するOptionのみを抜粋している。
 * Optionの全定義についてはlinkを参照。
 *
 * @link https://developers.google.com/calendar/api/v3/reference/events/list#parameters
 */
class CalendarQueryOptions {
  maxResults?: number;
  syncToken?: string;
  timeMin?: string;
}

/**
 * 指定されたCalendarIdに紐付くイベントを前回取得した時から更新があったイベントのみ取得する。
 * 過去に一度もイベントを取得していない場合、過去30日分を取得する。
 * 
 * @link https://developers.google.com/calendar/api/v3/reference/events#resource
 * 
 * @param {string} calendarId カレンダーを一意に識別するID
 * @returns {GoogleAppsScript.Calendar.Schema.Event[]} イベントの一覧(最大100件)
 */
function getUpdatedEvents(
  calendarId: string
): GoogleAppsScript.Calendar.Schema.Event[] {
  const properties = PropertiesService.getUserProperties();
  const key = `syncToken:${calendarId}`;
  const syncToken = properties.getProperty(key);

  let options: CalendarQueryOptions = { maxResults: 100 };
  if (syncToken) {
    options = { ...options, syncToken: syncToken };
  } else {
    options = { ...options, timeMin: getRelativeDate(-30, 0).toISOString() };
  }

  const events = Calendar.Events?.list(calendarId, options);

  if (events?.nextSyncToken) {
    properties.setProperty(key, events?.nextSyncToken);
  }

  return events?.items ? events.items : [];
}

/**
 * Helper function to get a new Date object relative to the current date.
 * @param {number} daysOffset The number of days in the future for the new date.
 * @param {number} hour The hour of the day for the new date, in the time zone
 *     of the script.
 * @return {Date} The new date.
 */
function getRelativeDate(daysOffset: number, hour: number) {
  var date = new Date();
  date.setDate(date.getDate() + daysOffset);
  date.setHours(hour);
  date.setMinutes(0);
  date.setSeconds(0);
  date.setMilliseconds(0);
  return date;
}

/**
 * 引数として与えられたWebhookURLを元にGoogle Chatへメッセージを送付する
 * 
 * @link https://developers.google.com/chat/api/guides/message-formats/basic
 * @param webHookURL Web Hook URL
 * @param message チャットに送付するメッセージ
 */
function sendMessageToChat(webHookURL: string, message: string): void {
  const params: GoogleAppsScript.URL_Fetch.URLFetchRequestOptions = {
    payload: JSON.stringify({
      text: message
    }),
    method: "post",
    contentType: "application/json",
  };
  const response = UrlFetchApp.fetch(webHookURL, params);
  console.log(`repsonse:${response}`);
}

こちらもclasp pushした後、トリガーが紐づくカレンダーを更新することで、Google Chatに通知されるようになります。

スクリーンショット 2022-01-28 21.22.22.png

以上です。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?