Node.jsでgoogleカレンダーから今日の予定を取得

##イントロダクション
nodejsでgoogle calendar APIを使ってイベント取得するまでの流れを解説します。
なお取得するカレンダーはマイカレンダーだけでなく、閲覧権限のある他のカレンダーの情報も同時に取得します。


## 環境準備
大まかな流れはほぼ公式ドキュメント通りです。
[公式ドキュメント](https://developers.google.com/calendar/quickstart/nodejs)

まずカレンダーAPIの有効化と、必要なキーを発行します。
※公式ドキュメント内の[ENABLE THE GOOGLE CALENDAR API]のボタンから簡単に発行できます。
credentials.jsonのファイルが生成されますので、実行ファイルと同じパスに保存しておきます。

次にgoogleカレンダーへ接続するためのクライアント用モジュールを追加します。

```
# npm install googleapis@27 --save
```

準備の手続きは以上になります。あとはソースを生成して実行するだけです。

## カレンダーデータの取得


（※以下すべてのソースはwebpackを使ってビルドしています。直接nodejsで実行するソースの場合はimport記述はエラーになるためrequireに置き換えた記述方法にしてください。）

```/googleCal.js
import fs from 'fs'
import readline from 'readline'
import {google} from 'googleapis'
import moment from 'moment'

export default class GoogleCal{
  constructor(config){
    this.config = Object.assign({},this.defaults(),config);
  }
  /**
   * 初期値
   */
  defaults(){
    return {
      scopes : ['https://www.googleapis.com/auth/calendar.readonly'],
      tokenPath : '/app/script/token.json',

    }
  }

  /**
   * google Calendar APIへの接続と認証
   */
  connect(){
    return new Promise((resolve,reject) => {
      fs.readFile('credentials.json', (err, content) => {
        if (err){
          console.log('Error loading client secret file:', err);
          reject();
        }
        this.authorize(JSON.parse(content))
        .then(auth => {
          this.config.auth = auth;
          resolve();
        });
      });
    });
  }

  /**
   * OAuth認証処理
   * given callback function.
   * @param {Object} credentials 認証データ
   * @return (promise) object oAuth2Client.
   */
  authorize(credentials) {
    const {client_secret, client_id, redirect_uris} = credentials.installed;
    const oAuth2Client = new google.auth.OAuth2(
        client_id, client_secret, redirect_uris[0]);

    return new Promise((resolve,reject) => {
      // 保存してあるトークン情報を読み取り、なければトークン生成
      fs.readFile(this.config.tokenPath, (err, token) => {
        if (err) {
          this.getAccessToken(oAuth2Client)
          .then(() => {
            resolve(oAuth2Client);
          })
        } else {
          // アクセストークンは1時間ほどしか期限がないため、毎回リフレッシュトークンから新たなトークンを生成する
          oAuth2Client.credentials = JSON.parse(token);
          oAuth2Client.refreshAccessToken((err, token) => {
            if (err) {
              console.log(err);
              reject();
            }
            oAuth2Client.setCredentials(token);
            fs.writeFile(this.config.tokenPath, JSON.stringify(token), (err) => {
              if (err){
                console.error(err);
                reject();
              }
              console.log('Token stored to', this.config.tokenPath);
            });
          });
          resolve(oAuth2Client);
        }
      });
    });
  }

  /**
   * OAuthのアクセストークンを取得
   * @param oAuth2Client {object} google.auth.OAuth2の戻り値
   * @return (promise) object 有効なtokenを含めたgoogle.auth.OAuth2
   */
  getAccessToken(oAuth2Client) {
    const authUrl = oAuth2Client.generateAuthUrl({
      access_type: 'offline',
      scope: this.config.scopes,
    });
    console.log('右記のURLをブラウザで開いてください:', authUrl);
    const rl = readline.createInterface({
      input: process.stdin,
      output: process.stdout,
    });
    return new Promise((resolve,reject) => {
      rl.question('表示されたコードを貼り付けてください: ', (code) => {
        rl.close();
        oAuth2Client.getToken(code, (err, token) => {
          if (err) return console.error('トークンを発行できませんでした', err);
          oAuth2Client.setCredentials(token);
          // トークンデータをファイルに書き出し
          fs.writeFile(this.config.tokenPath, JSON.stringify(token), (err) => {
            if (err) {
              console.error(err);
              reject();
            }
            console.log('トークンを保存しました', this.config.tokenPath);
          });
          resolve(oAuth2Client);
        });
      });
    });
  }

  /**
   * イベントを取得
   * @param params {object} 取得時のパラメータ
   * @return (promise) Array イベントデータ一覧
   */
  listEvents(params) {
    if(params.calendarId.match(/#/)){
      params.calendarId = encodeURIComponent(params.calendarId);
    }
    return new Promise((resolve,reject) => {
      const calendar = google.calendar({version: 'v3', auth: this.config.auth});
      calendar.events.list(params, (err, res) => { // events.listでイベント一覧を取得
        if (err) {
          console.log('The API returned an error: ' + err);
          reject();
        }
        resolve(res.data.items);
      });
    });
  }

  /**
   * カレンダー一覧を取得
   * @return (promise) Array カレンダーデータ一覧
   */
  calendarLists(){
    return new Promise((resolve,reject) => {
      const calendar = google.calendar({version: 'v3', auth : this.config.auth});
      calendar.calendarList.list({},(err, res) => { // calendarList.listでカレンダーの一覧を取得
        resolve(res.data.items);
      })
    });
  }
  
  /*
   * 本日のイベントを取得
   * @param filterFunc {function} イベントを取得するカレンダーリストの条件フィルター
   */
  getTodayEvents(filterFunc = null){
    // カレンダー毎に順次処理するためのタスク
    const task = (calName,params,result) => {
      return new Promise((resolve, reject) => {
        this.listEvents(params).then(resp => {
          const data = Object.assign([],result);
          data.push({
            calName,
            items : resp
          })
          resolve(data)
        });
      });
    }
    
    return new Promise((resolve,reject) => {
      this.calendarLists() // カレンダー一覧を取得
      .then(calList => {
        if(filterFunc){
          calList = calList.filter(filterFunc); // イベント取得するカレンダーを絞り込み
        }
        calList.reduce((prev, current) => {
          return prev.then(resp => {
            const today = moment(moment().format("YYYY-MM-DD")).utcOffset("+09:00"); // 本日の00:00(日本時間)
            const todayMax = moment(moment().format("YYYY-MM-DD")).add(1,'days').add(-1,'minutes').utcOffset("+09:00"); // 本日の23:59(日本時間)
            const params = {
              calendarId: current.id,
              timeMin: today.format(), // 本日の0:00
              timeMax: todayMax().format(), // 本日の23:59
              singleEvents: true,
              orderBy: 'startTime',
            }
            const summary = current.summaryOverride || current.summary; // カレンダー名
            return task(summary,params,resp)
          });
        }, Promise.resolve([]))
        .then(resp => {
          resolve(resp);
        })
        .catch(()=>{
          console.log("error!")
        })
      })
    });
  }
}
```

```/app.js
import GoogleCal from './googleCal'

const googleCal = new GoogleCal;
googleCal.connect()
.then(() => {
  return googleCal.getTodayEvents(_val => !_val.id.match(/#/)); // 誕生日や祝日のカレンダーは取得対象から除外
})
.then(val => {
  console.log(val);

});

```

## 実行

```
# node app.js
```

初回はtokenを持っていないため以下のような表示が出ますので、指示通りブラウザで指定のURLを開き、アカウント認証を行います。
アカウント認証が完了するとコードが表示されますので、それをコピペしてターミナルに入力します。

```
右記のURLをブラウザで開いてください:  https://accounts.google.com/o/oauth2/auth?access_type=offline&scope=****
表示されたコードを貼り付けてください:
```

認証に成功すると、カレンダーとそのタスクが表示されますので（/app.jsの最後のconsole.log(val）の部分）
あとはうまく加工して表示してください。
ちなみにデータの形式はこんな感じです。

```response.json
[
  {
    calName : xxx,
    items : [
      {
        kind: 'calendar#event',
         etag: '"1234567890"',
         id: '20frmklfmerkfrefmqw',
         status: 'confirmed',
         htmlLink: 'https://www.google.com/calendar/event?eid=xxxxxxxxxxxxxx',
         created: '2018-10-09T07:09:42.000Z',
         updated: '2018-10-09T07:09:42.778Z',
         summary: 'てすと',
      },
      … (item分繰り返し)

    ]
  }
  … (カレンダー分繰り返し)
]
```

## 所感
googleの公式ドキュメントが例文コード含め丁寧に解説されているため、わりと簡単に設置することができました。