Googleカレンダーの招待を自動化したら地味に人生が楽になった話

はじめに

ハニーとの予定をGoogleカレンダーで共有することにした。

ただ、予定を入れるときに共有を忘れることが多い。後から「あ、共有してなかった」と気づいて、慌てて招待する。これの繰り返し。というか直前に予定を行って怒られている。

「自動化しなければ」と思い立って、GASで毎日4時に自動でハニーを招待する仕組みを作った。派手な機能じゃないけど、日常のストレスは確実に減った。

何を作ったか

毎日4時に以下の処理を自動実行:

  • 今日以降の予定をチェック

  • 自分が作った予定だけを対象

  • タイトル末尾に [skip] がついていない予定に限定

  • 指定したメールアドレスをゲストとして追加

実装のポイント

### 1. isOwnedByMe() で自分の予定だけを対象

招待された予定には自動で招待を追加しない。これが重要。

if (!event.isOwnedByMe()) {
  continue;
}

### 2. [skip] で制御

タイトルに [skip] をつけるだけで除外。シンプルな運用ルール。

if (title.endsWith(skipSuffix)) {
  continue;
}

### 3. 既に招待済みならスキップ

同じ人を何度も招待しない。

const guestList = event.getGuestList().map(g => g.getEmail().toLowerCase());
if (guestList.includes(guestEmail.toLowerCase())) {
  continue;
}

### 4. 主催者の参加状態もセット

ゲストを追加する際、主催者(自分)のステータスも「参加(YES)」に変更する。 これをしないと、カレンダー上では「未承諾」のままになってしまう。

if (adminEmail) {
  event.addGuest(adminEmail);
  event.setMyStatus(CalendarApp.GuestStatus.YES)
}

実装全体

TypeScriptで書いて、claspでローカル管理。

function setConfig(): void {
  const props = PropertiesService.getScriptProperties();
  props.setProperties({
    ADMIN_EMAIL: "admin@sample.com",
    TARGET_GUEST_EMAIL: "info@sample.com",
    CALENDAR_ID: "primary",
    SKIP_SUFFIX: "[skip]"
  });
}

function inviteGuestToUpcomingEvents(): void {
  const props = PropertiesService.getScriptProperties();
  const guestEmail = props.getProperty("TARGET_GUEST_EMAIL");
  const adminEmail = props.getProperty("ADMIN_EMAIL");
  const calendarId = props.getProperty("CALENDAR_ID") || "primary";
  const skipSuffix = props.getProperty("SKIP_SUFFIX") || "[skip]";

  if (!guestEmail) {
    throw new Error("TARGET_GUEST_EMAIL が未設定です。setConfig() を実行してください。");
  }

  const calendar =
    calendarId === "primary"
      ? CalendarApp.getDefaultCalendar()
      : CalendarApp.getCalendarById(calendarId);

  if (!calendar) {
    throw new Error(`カレンダーが見つかりません: ${calendarId}`);
  }

  const now = new Date();
  const oneYearLater = new Date(now);
  oneYearLater.setFullYear(oneYearLater.getFullYear() + 1);

  const events = calendar.getEvents(now, oneYearLater);

  for (const event of events) {
    const title = (event.getTitle() || "").trim();

    if (title.endsWith(skipSuffix) || !event.isOwnedByMe()) {
      continue;
    }

    const guestList = event.getGuestList().map(g => g.getEmail().toLowerCase());
    if (guestList.includes(guestEmail.toLowerCase())) {
      continue;
    }

    try {
      if (adminEmail) {
        event.addGuest(adminEmail);
        event.setMyStatus(CalendarApp.GuestStatus.YES)
      }
      event.addGuest(guestEmail);
      console.log(`追加: ${title} -> ${guestEmail}`);
    } catch (e) {
      console.error(`追加失敗: ${title} / ${e}`);
    }
  }
}

function createDailyTrigger(): void {
  const functionName = "inviteGuestToUpcomingEvents";

  const triggers = ScriptApp.getProjectTriggers();
  for (const trigger of triggers) {
    if (trigger.getHandlerFunction() === functionName) {
      console.log("既存トリガーあり");
      return;
    }
  }

  ScriptApp.newTrigger(functionName)
    .timeBased()
    .everyDays(1)
    .atHour(4)
    .create();

  console.log("毎日4時のトリガーを作成しました");
}

function updateGuestEmail(newEmail: string): void {
  PropertiesService.getScriptProperties().setProperty("TARGET_GUEST_EMAIL", newEmail);
}

function updateAdminEmail(newEmail: string): void {
  PropertiesService.getScriptProperties().setProperty("ADMIN_EMAIL", newEmail);
}

セットアップ手順

### 1. clasp をインストール

npm install -g @google/clasp

### 2. ログイン

clasp login

### 3. 既存GASをpull(またはclone)

スクリプトIDはURLから取得:

clasp clone <SCRIPT_ID>

### 4. 初期設定を実行

Google Apps Script エディタで以下を実行:

setConfig()

### 5. トリガーを作成

createDailyTrigger()

失敗談

最初は全予定に招待を飛ばしていて、招待されたミーティングにも勝手に人が追加されるという事故が発生。

isOwnedByMe() チェックを入れてからは安定した。

運用ルール

  • タイトル末尾に [skip] をつけると除外される (家賃の支払い予定など)

  • 自分が作った予定だけが対象

  • 既に招待済みなら何もしない

  • メールアドレスは updateGuestEmail() で変更可能

ちょっとした工夫で生活が楽になる

こういう「ちょっとした面倒」を仕組みで消していくのは結構好きで、派手な機能じゃないけど、日常のストレスは確実に減る。

こういうのを積み上げると、生活の解像度が少し上がる気がする。

次のステップ

  • Slackへの通知追加

  • 複数メールアドレスへの対応

  • カレンダーごとの設定分岐

参考リンク