takanakahiko’s blog

多分三日坊主で辞めます。

Google Apps Script を用いてカレンダーへの自発的な参加登録ができるWebアプリを作成する

こんにちは。 takanakahiko です。

今回は、 Google Apps Script を用いてカレンダーへの自発的な参加登録ができるWebアプリを作成していきたいと思います。

このエントリーはAkatsuki Advent Calendar 2022の2日目の記事です。 adventar.org

昨日は tkmruさんの「脆弱性診断とiOSアプリの再署名」でした。 脆弱性診断に必要な再署名といった複雑な作業を簡単に行えるツールを公開されています。 hackerslab.aktsk.jp

前置き

今回紹介する実装例は Google Workspace ドメインに属している場合にのみ正しく動作します。 個人の Google アカウントでは動作しないためご了承ください。

また、 Google Apps Script に関する初歩的な説明は行いませんのでご了承ください。

モチベーション

私の業務において Google Calender で任意参加の予定を作成する機会がありました。 私が開発や運用を担当している基盤の利用者向けミーティングだったのですが、内容としては「参加したい人がいたらぜひ」といった温度感のものでした。 これ以外にも、例えば社内勉強会の予定なども同様のユースケースになると思います。

予定の参加希望者をカレンダーに招待する必要があるのですが、ざっくり考えると以下の方法があると思います。

  • 全社員向けに招待を飛ばす
  • 参加表明した人を Google Groups に招待し、その Group 向けに招待を飛ばす
  • 参加表明する人を一人ずつ招待する

しかしこれらの方法は、不必要な人への招待が発生してしまったり、また手動オペレーションが煩わしかったりでピンとくるものがありませんでした。

そこで、今回は Google Apps Script で軽い Web アプリケーションを作成し、それを利用することで解決したいと思いました。

Google Apps Script とは

Google Apps Script (以降は GAS の略称で表記)は Java Script スクリプトプラットフォームです。 GAS を用いると Google から提供されるさまざまなサービスを自動化することが可能です。

developers.google.com

また、GAS は

  • Spread Sheet のマクロにする
  • Web アプリケーションとして提供する

など、さまざまな呼出し方法があることも大きな特徴です。

実装

実際に実装する際のポイントを踏まえて説明していきます。

予定への招待

繰り返し予定への招待は以下のように行います。

var eventSeries = CalendarApp.getEventSeriesById(eventSeriesID);
eventSeries.addGuest(email);

ここで必要になるのが eventSeriesID を取得する方法です。 まず Google Calender で作った予定をダブルクリックします。 そうすると以下のような URL で編集画面が開くはずです。

ttps://calendar.google.com/calendar/u/0/r/eventedit/NTNnamxxxxxxxxxxxxxxxx

この中の NTNnamxxxxxxxxxxxxxxxx の部分は eid と呼ばれるものです。 これは base64 encode された文字列です。 これをデコードしてみると、以下のような文字列が取得できます。

53gjexxxxxxxx xxxxxx@xxxxx.xxx

ここにある 53gjexxxxxxxxeventSeriesID で、 xxxxxx@xxxxx.xxx が主催者のメールアドレスになります。 それを踏まえると、以下のような処理で eventSeriesID の取得も含めて招待まで行うことができることになります。

var eid = 'NTNnamxxxxxxxxxxxxxxxx'; // カレンダーの編集URLから取得
var eventSeriesID = Utilities.newBlob(Utilities.base64Decode(eid)).getDataAsString().split(' ')[0];
var eventSeries = CalendarApp.getEventSeriesById(eventSeriesID)
eventSeries.addGuest(email)

アクセスしたユーザの情報を取得する

上記の例にある email の取得方法について説明していきます。

GAS で実装した Web アプリにアクセスしたユーザのメールアドレスは以下のように取得できます。

var email = Session.getActiveUser();

しかし、 Session.getActiveUser() は基本的には Google Workspace 内でしか動作することができません。 この記事の冒頭にもありますが、今回の実装は Google Workspace 組織向けの実装となっています。 developers.google.com

ユーザがすでにイベントに招待されているかどうかは以下のように判断することができます。

if (eventSeries.getGuestByEmail(email)) {
  // 参加済み
}else{
  // 未参加
}

doGet から doPost へ POST するフォームを作成する

GAS では Web アプリケーションにアクセスしたユーザの HTTP メソッド(POSTやGET)によって呼ばれる関数が異なります。 以下のように HtmlService.createHtmlOutputContentService.createTextOutput を用いることでユーザに表示する内容を指定することができます。

function doGet(e) {
  // HTMLを表示する場合は以下のようにすると良い
  return HtmlService.createHtmlOutput(`<p>GETでアクセスされたよ</p>`);
}

function doPost(e) {
  // 文字を表示したいだけなら以下のようにしても良い
  return ContentService.createTextOutput("POSTでアクセスされたよ");
}

一般的なWebアプリケーションでは、GET によるアクセスで情報の閲覧を、 POST によるアクセスでユーザが何かしらの操作を行えるようにします。 言い換えると、 GET によるアクセスでいきなり操作(今回で言うところの予定の参加登録)が完了してしまったらアプリケーションとしては不自然です。

今回作るアプリケーションもそのように作っていきましょう。 GET でアクセスしたユーザには参加登録をするためのボタン(フォーム)を表示し、そのボタンを押したユーザーは POST でアクセスし直して参加登録がされる、といった流れにします。

ここで、問題があります。 GAS は HTML を iframe に包んで返却するといった点です。 iframe 内にある form で submit しても Forbidden Error 403 が返却されます。これは iframe 内での遷移になってしまうためです。 また、iframe の src が xxxx.googleusercontent.com に書き変わってしまうため、 <form action="." method="post" target="_top">> のよう action="."xxxx.googleusercontent.com を参照してしまい正常に動作しません。

それを踏まえて、以下のように action="${ScriptApp.getService().getUrl()}"target="_top" を設定することで iframe 外での post アクセスかつ、 POST 先の URL が正しく設定されているため正常に動作します。

function doGet(e) {
  return HtmlService.createHtmlOutput(`
    <form action="${ScriptApp.getService().getUrl()}" method="post" target="_top">
      <input type="submit" value="submit" />
    </form>
  `);
}

function doPost(e) {
  return ContentService.createTextOutput("POSTでアクセスされたよ");
}

実装の全体

これらの要点を踏まえ、今回は以下のように実装しました。

function getEventSeries() {
  var eid = 'xxxxxxxxxxxxxxx'; // カレンダーの編集URLから取得
  var eventSeriesID = Utilities.newBlob(Utilities.base64Decode(eid)).getDataAsString().split(' ')[0];
  return CalendarApp.getEventSeriesById(eventSeriesID);
}

function doGet(e) {
  var eventSeries = getEventSeries();
  return HtmlService.createHtmlOutput(`
    <form action="${ScriptApp.getService().getUrl()}" method="post" target="_top">
      <input type="submit" value="${eventSeries.getTitle()}に参加登録する" />
      <span>(登録解除はカレンダーからお願いします)</span>
    </form>
  `);
}

function doPost(e) {
  var email = Session.getActiveUser();
  var eventSeries = getEventSeries();
  if (eventSeries.getGuestByEmail(email)) {
    return ContentService.createTextOutput("既に参加しているようです(登録解除はカレンダーからお願いします)");
  }else{
    eventSeries.addGuest(email);
    return ContentService.createTextOutput("参加登録しました(登録解除はカレンダーからお願いします)");
  }
}

公開

次に、書いた GAS をデプロイし、 Web アプリケーションとして公開、 URL を取得する必要があります。

ここで注意点として、一度も実行していない GAS をいきなりWeb アプリケーションとして公開することはおすすめできません。 それは、この GAS プロジェクトにカレンダー取得等の権限が付与されていない可能性が高いためです。 今回は、保存後に getEventSeries を選択して一度実行しておきましょう。

getEventSeriesを実行する

では Web アプリケーションとして公開します。 デプロイ時に「次のユーザとして実行」を「自分」に、「アクセスできるユーザー」を「〇〇内の全員」にします。 前者はカレンダーの招待を行うユーザーが自分であるため、後者は今回のユースケースでは社内に向けた予定であるためですね。

デプロイボタンを押す

ウェブアプリを選択する

各種項目を設定する

動作確認

試しにこのイベントに招待するやつを作りましょう。

なんかいい感じのミーティングと題した予定

デプロイ時に表示されたURLにアクセスすると

フォームが表示されている

ポチッとすると

参加登録できた旨が表示される

無事に招待されています

自分が予定のゲストとして登録されている

まとめ

Google Apps Script を用いてカレンダーへの自発的な参加登録ができるWebアプリを作成しました。 GAS 特有の仕様による罠も多いですが、それでもこのようなアプリケーションがこんなに手軽に作成できるのは素晴らしいですね。

最後まで読んでいただきありがとうございました。

明日の Akatsuki Advent Calendar 2022 は Yoshitomo Yasuno さんの Rails の inverse_of の挙動についての内容です。かなりニッチそうで個人的に楽しみです。

最後に、アカツキでは一緒に働くエンジニアを募集しています。 カジュアル面談もやっていますので、気軽にご応募ください。

hrmos.co