【GAS】複数のGoogleカレンダーの予定を一覧で把握したい！

## はじめに
仕事で使ってるGoogleカレンダーをチームで管理して、カレンダー表示+一覧化してほしいとのことで、今回やったことの備忘録を残します。

:::note info
そのため今回はスプレッドシートに予定を出力するまでとなります。
:::


## 準備するもの
- Googleアカウント
- Googleスプレッドシート
- GASエディタ
- Googleカレンダー

## 簡単な流れ
1. Googleカレンダーで設定変更
    1. 各ユーザーのカレンダーIDを取得
    1. Googleカレンダーで他のユーザーの閲覧を許可
1. スクリプトを書いていく
1. 挙動確認

## 実際にやってみた
:::note info
今回はユーザー①のカレンダーにユーザー②の予定を追加して、ユーザー①のスプレッドシートで予定を管理していきます。
:::

1. Googleカレンダーでの設定
    1. カレンダーIDを取得(人数分)
        1. Googleカレンダーを開く
        1. 右上の歯車マーク→「設定」を押下
            <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2918071/699bfa9c-d4da-110f-8a82-46e1219e3f19.png" width=50%>
        1. 左下のマイカレンダーの設定の設定したいユーザー→「カレンダーの統合」を押下
            <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2918071/bf144c1f-01ec-4003-b784-ce3bad03e9c3.png" width=50%>
        1. 「カレンダーID」と記載されているものがIDです。後ほど使いますので、控えておいてください
    1. 他のユーザーの閲覧を許可する
        1. 招待する側（ユーザー②）のGoogleカレンダーを表示する
        1. 上記3で開いたようにマイカレンダーの設定の「予定のアクセス権限」を押下
            <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2918071/891f52be-c47f-6615-96ce-c0d590e37c18.png" width=50%>
        1. 「特定のユーザーまたはグループと共有する」欄の「ユーザーやグループを追加」と記載されているプラスボタンを押下
            <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2918071/2d50145e-e9f8-7a3c-ce84-1da99c844d3f.png" width=70%>
        1. 共有したいユーザー（ユーザー①）のメールアドレスを入力
            <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2918071/315ee18e-443b-b6dd-839e-a8319d04fe0b.png" width=70%>

            :::note info
            今回は予定を参照できればOKなので、権限は「予定の表示（すべての予定の詳細）」にしますが、必要最低限の権限にすることをお勧めします。
                :::

        1. 招待されたユーザー（ユーザー①）のGmailを開き、招待メールを確認
        1. メール内の「このカレンダーを追加」を押下
        1. 自動でGoogleカレンダーに移動して、「カレンダーを追加」とポップアップが出るので、「追加」を押下

1. GASを書いていく

    :::note warn
    2025/02/01に追記しています。完成系のコードはこちらへ
    [完成したコードはこちら](#完成したコードの全容)
        :::

    ```javascript
    function getCalender() {
      // 上記で指定したidを指定
      const idA = 'カレンダーID①';
      const idB = 'カレンダーID②';
      const user = [idA, idB]; // idを配列に格納
      var eventDetails = []; // 配列を用意
      // 各ユーザーの予定を取得（二次元配列）
      for (let i=0; i<user.length; i++) {
        eventDetails[i] = getSchedule(user[i]); 
      }
      eventDetails = eventDetails.flat(); // 配列を平坦化
      eventDetails = sort(eventDetails); // 予定を開始時刻で昇順
      write(eventDetails); // スプレッドシートに記入
    }
    
    function getSchedule(id) {
      const calendar = CalendarApp.getCalendarById(id); // 指定したIDのカレンダーを取得
      const events = calendar.getEventsForDay(new Date()); // 今日のイベントを取得(取得したい日時によって修正してください)
      var result = []; // 配列を用意
      const keys = ['title', 'user', 'startTime', 'endTime'];
    
      for (let i=0; i<events.length; i++) {
        var event = events[i];
        // 各項目を変数に代入
        var title = event.getTitle(); // タイトルを取得
        var email = event.getCreators().toString(); // メールアドレスを取得
        var startTime = changeDateFormat(event.getStartTime()); // 開始時刻を日本時間表記に変換
        var endTime = changeDateFormat(event.getEndTime()); // 終了時刻を日本時間表記に変換
        var values = [title, email, startTime, endTime];
        
        var obj = {};
        for (let j=0; j<keys.length; j++) {
          obj[keys[j]] = values[j];
        }
        result.push(obj);
      }
      return result;
    }
    
    function changeDateFormat(date) {
      var newDate = Utilities.formatDate(date, 'JST', 'yyyy/MM/dd HH:mm:ss');
      return newDate;
    }
    
    function sort(eventDetails) {
      eventDetails.sort((a, b) => new Date(a.startTime) - new Date(b.startTime));
      return eventDetails;
    }
    
    function write(eventDetails) {
      // シートを指定して取得
      const ss = SpreadsheetApp.getActiveSpreadsheet();
      const sheet = ss.getSheetByName('シート1'); // ここはシート名に合わせて修正してください
      sheet.clear(); // シートをリセット
      const headers = ['予定', 'メールアドレス', '開始時刻', '終了時刻'];
      sheet.appendRow(headers); // ヘッダーを入力
      // 取得したデータを入力
      for (let i=0; i<eventDetails.length; i++) {
        sheet.getRange('A'+(i+2)).setValue(eventDetails[i].title);
        sheet.getRange('B'+(i+2)).setValue(eventDetails[i].user);
        sheet.getRange('C'+(i+2)).setValue(eventDetails[i].startTime);
        sheet.getRange('D'+(i+2)).setValue(eventDetails[i].endTime);
      }
    }
    
    ```

1. 挙動確認
    1. GASエディタの「実行ボタン」を押下
    1. 初回は「認証が必要です」みたいなポップアップが表示されるので、「続行」→アカウントを選択→「詳細」→「安全ではないページに移動」→「許可」と押下していく
    1. 再度「実行」ボタンを押下
    1. スプレッドシートに戻って、予定が表示されればOK

## 今後のアイディア
- トリガー設定をしてこの処理をバッチ化、取得した予定をLINEやGmail、Slackなどに毎朝送るようにすればスプレッドシートを開かずに１日の予定を確認できる
- ↑をやるならその日に入った予定の扱いをどうするか。。そこまで気にしないか、予定が入ったらリアルタイムで知らせるとか。

## まとめ
今回はGoogleカレンダーでしたが、そのほかのカレンダーアプリ(TimeTreeとか)との連携とかも今度やってみたいと思います。
こうした方がシンプルにできる、ここがわかりづらい等ありましたらコメントください！
よければいいねもお願いします！

## 2025/02/01追記
1. ID取得を柔軟に対応できるようにしゅうせ
    上記のコードではユーザの追加、削除等に対応しにくいと思いますので、少しメソッドを追加していきます。
    作戦としては指定したシートにID、メールアドレス、ユーザ名を入力して、それを取得するメソッドを追加します。
    1. ユーザ情報シートを作成
        <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2918071/309c6e13-5f7a-665a-313c-88823fa667d6.png" width=70%>
    1. 以下メソッドを追加、その他コードを修正
        - 追加メソッド
            ```javascript
            function getUserList() {
              // シートを指定して取得
              const ss = SpreadsheetApp.getActiveSpreadsheet();
              const sheet = ss.getSheetByName('ユーザ情報');
              
              var lastRow = sheet.getLastRow(); // 情報が入力されている最終行を取得
              var range = sheet.getRange(2,1,lastRow - 1,3); // A2:C列最終行まで取得
              var userList = range.getValues();
            
              return userList;
            }
            ```
        - 修正メソッド
            ```diff_javascript
            function getCalender() {
            -   // 上記で指定したidを指定
            -    const idA = 'カレンダーID①';
            -    const idB = 'カレンダーID②';
            -    const user = [idA, idB]; // idを配列に格納
            -    var eventDetails = []; // 配列を用意
            +    // idを取得
            +    var userList = getUserList();
            +    var idList = []; // 配列を用意
            +    for (let i = 0; i < userList.length; i++) {
            +      idList[i] = userList[i][0] // idを配列に格納
            +    }
            
              var eventDetails = []; // 配列を用意
              // 各ユーザーの予定を取得（二次元配列）
            -    for (let i=0; i<user.length; i++) {
            -      eventDetails[i] = getSchedule(user[i]); 
            -    }
            +    for (var i = 0; i < idList.length; i++) {
            +      eventDetails[i] = getSchedule(idList[i]); 
            +    }
            
              eventDetails = eventDetails.flat(); // 配列を平坦化
              eventDetails = sort(eventDetails); // 予定を開始時刻で昇順
              write(eventDetails); // スプレッドシートに記入
            }
            ```
1. メールアドレス→ユーザ名に変換
    以前はメールアドレスを表示していましたが、(次の記事では)LINEに送信となるとメッセージの中にメールアドレスがあると見づらいなと。。
    そのためメールアドレスからユーザー名に変換する処理を追加します。
といっても上記で追加したメソッドを流用します。
    1. 修正したコード
        ```diff_javascript
            function getSchedule(id) {
              const calendar = CalendarApp.getCalendarById(id); // 指定したIDのカレンダーを取得
              const events = calendar.getEventsForDay(new Date()); // 今日のイベントを取得(取得したい日時によって修正してください)
              var result = []; // 配列を用意
              const keys = ['title', 'user', 'startTime', 'endTime'];
            
              for (let i=0; i<events.length; i++) {
                var event = events[i];
                // 各項目を変数に代入
                var title = event.getTitle(); // タイトルを取得
                var email = event.getCreators().toString(); // メールアドレスを取得
                var startTime = changeDateFormat(event.getStartTime()); // 開始時刻を日本時間表記に変換
                var endTime = changeDateFormat(event.getEndTime()); // 終了時刻を日本時間表記に変換
       +        // 「ユーザ情報」からメールアドレスとユーザ名を取得
       +         var userList = getUserList();
       +         // メールアドレスをユーザ名に変換
       +         var user = '';
       +         for (let i = 0; i < userList.length; i++) {
       +           if (userList[i][1] == email) {
       +             user = userList[i][2];
       +           }
       +         }

       -        var values = [title, email, startTime, endTime];
       +        var values = [title, user, startTime, endTime];
                
                var obj = {};
                for (let j=0; j<keys.length; j++) {
                  obj[keys[j]] = values[j];
                }
                result.push(obj);
              }
              return result;
            }
        ```
## 完成したコードの全容
```javascript
function getCalender() {
  // メールアドレスを取得
  var userList = getUserList();
  var idList = []; // 配列を用意
  for (let i = 0; i < userList.length; i++) {
    idList[i] = userList[i][0] // idを配列に格納
  }

  var eventDetails = []; // 配列を用意
  // 各ユーザーの予定を取得（二次元配列）
  for (var i = 0; i < idList.length; i++) {
    eventDetails[i] = getSchedule(idList[i]); 
  }

  eventDetails = eventDetails.flat(); // 配列を平坦化
  eventDetails = sort(eventDetails); // 予定を開始時刻で昇順
  write(eventDetails); // スプレッドシートに記入
}

function getSchedule(id) {
  var calendar = CalendarApp.getCalendarById(id); // 指定したIDのカレンダーを取得
  var events = calendar.getEventsForDay(new Date()); // 今日のイベントを取得
  var result = [];
  var keys = ['title', 'user', 'startTime', 'endTime'];

  for (var i = 0; i < events.length; i++) {
    var event = events[i];
    var title = event.getTitle(); // タイトルを取得
    var email = event.getCreators().toString(); // メールアドレスを取得
    var startTime = changeDate(event.getStartTime()); // 開始時刻を日本時間表記に変換
    var endTime = changeDate(event.getEndTime()); // 終了時刻を日本時間表記に変換

    // 「ユーザ情報」からメールアドレスとユーザ名を取得
    var userList = getUserList();
    // メールアドレスをユーザ名に変換
    var user = '';
    for (let i = 0; i < userList.length; i++) {
      if (userList[i][1] == email) {
        user = userList[i][2];
      }
    }
    
    var values = [title, user, startTime, endTime];
    var obj = {};

    for (var j = 0; j < keys.length; j++) {
      obj[keys[j]] = values[j];
    }
    result.push(obj);
  }
  return result;
}

function changeDate(date) {
  const newDate = Utilities.formatDate(date, 'JST', 'yyyy/MM/dd HH:mm:ss');
  return newDate;
}

function sort(eventDetails) {
  eventDetails.sort((a, b) => new Date(a.startTime) - new Date(b.startTime));
  return eventDetails;
}

function write(eventDetails) {
  // シートを指定して取得
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('シート1');
  sheet.clear(); // シートをリセット
  const headers = ['予定', 'ユーザー', '開始時刻', '終了時刻'];
  sheet.appendRow(headers); // ヘッダーを入力

  // 取得したデータを入力
  for (var i = 0; i < eventDetails.length; i++) {
    sheet.getRange('A'+(i+2)).setValue(eventDetails[i].title);
    sheet.getRange('B'+(i+2)).setValue(eventDetails[i].user);
    sheet.getRange('C'+(i+2)).setValue(eventDetails[i].startTime);
    sheet.getRange('D'+(i+2)).setValue(eventDetails[i].endTime);
  }
}

function getUserList() {
  // シートを指定して取得
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName('ユーザ情報');
  
  var lastRow = sheet.getLastRow();
  var range = sheet.getRange(2,1,lastRow - 1,3);
  var userList = range.getValues();

  return userList;
}
```
追記したせいで、だいぶ記事が長くなってしまった。。すみません。。

## 参考文献
以下のサイトを参考にさせていただきました。ありがとうございます。

https://qiita.com/mikeneko_t98/items/60e264941492d0b44fe5

https://data-x.jp/blog/google%E3%82%AB%E3%83%AC%E3%83%B3%E3%83%80%E3%83%BC%E3%81%A8%E9%80%A3%E6%90%BA%E3%81%99%E3%82%8Bgas%E3%81%AE%E9%96%A2%E6%95%B0%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9/

https://zenn.dev/namioto/articles/8e900d563bba06