【Ruby】Googleカレンダーから予定を取得してSlack通知を飛ばしてみる

## この記事を読むとできるようになる事

![スクリーンショット 2021-02-06 12.36.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/2d074c74-63a7-ca5e-d86b-5cd1476beaf4.png)

こんな感じでその日の予定をGoogleカレンダーから取得してSlack通知を飛ばす事ができます。

## 目指す構成

![Untitled Diagram(6).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/42e16299-8888-3bf6-e844-9bd7cce2ce25.png)

① CloudWatch EventsでLambdaを起動。
② API経由でGoogleカレンダーから予定を取得。
③ Slackに通知を飛ばす。

## 使用技術

- Slack
- Google API
- AWS
  - Lambda
  - CloudWatch Events
- Ruby
  - 2.5系

※ Slack通知用のBotは各自あらかじめ作成しておいてください。
参照: [ワークスペースで利用するボットの作成](https://slack.com/intl/ja-jp/help/articles/115005265703-%E3%83%AF%E3%83%BC%E3%82%AF%E3%82%B9%E3%83%9A%E3%83%BC%E3%82%B9%E3%81%A7%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B%E3%83%9C%E3%83%83%E3%83%88%E3%81%AE%E4%BD%9C%E6%88%90)

## 完成形（Githubリポジトリ）

https://github.com/kazama1209/slack-notifier-for-google-calendar

一から作るのが面倒な人、作業を進める途中で詰まった人などは↑のリポジトリをcloneするなり比較して参考するなりしてください。

## 実装

では、作業開始です。

### 下準備

まず、API経由でGoogleカレンダーにアクセスするために必要な準備がいくつかあるので、そちらからやっていきましょう。

#### 秘密鍵（JSON）を作成

- Google Cloud Platformにログイン
- Google Calendar APIを有効化
- サービスアカウント（認証用）を作成
- 秘密鍵（JSON）をダウンロード

この辺に関しては私が以前に書いた記事（↓）に詳しく記載しているので、そちらをご覧ください。

参照: [PHP/Laravelを使ってGoogleドライブにファイルをアップロードしてみる](https://qiita.com/kazama1209/items/94db0ff9d10df249bf14#%E4%B8%8B%E6%BA%96%E5%82%99)

記事内ではGoogleドライブを使用していますが、基本的な流れは同じなのでそれぞれの単語を「Googleカレンダー」に適宜置き換えて操作していけば問題無いはず。

サービスアカウント作成後、秘密鍵などの情報が含まれたJSONファイルをダウンロードするところまで進めればOKです。

#### Googleカレンダーの設定

![スクリーンショット 2021-02-06 13.43.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/e2a844aa-e719-ae86-16b6-727b004ff719.png)

Googleカレンダーの右上にある歯車マークを押して「設定」へと進みます。

![スクリーンショット 2021-02-06 13.49.10_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/248a5ad2-f979-77e8-6140-2506ed2d9be7.jpeg)

左サイドバー内の「マイカレンダーの設定」→「特定のユーザーとの共有」から、先ほど作成したサービスアカウントのメールアドレスを追加しましょう。

![スクリーンショット 2021-02-06 13.53.05_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/a7a2b992-c8ff-2138-4135-5ed189808c50.jpeg)

権限に関してはお好みですが、今回はあくまで情報の「取得」しかしないので「予定の表示」で大丈夫です。

![スクリーンショット 2021-02-06 13.55.45_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/113ef354-7c64-17da-2036-127cdedf1e32.jpeg)

あとは「カレンダーの統合」内にある「カレンダーID」をメモに控えておいてください。こちらも後の実装で必要になってきます。

### コード

下準備が完了したので、ここから実際の処理を行うコードを書いていきます。

#### リポジトリを作成

```
$ mkdir slack-notifier-for-google-calendar && cd slack-notifier-for-google-calendar
```

#### Gitを設定

```
$ git init
$ touch .gitignore
```
```:./.gitignore
.bundle
.ruby-version
/vendor/bundle
credentials.json
.env
```

Gitで管理したくないものを記述。

#### Rubyのバージョンを指定

```
$ rbenv local 2.5.1 
```

2.5系か2.7系を推奨。（Lambdaで運用する事を考慮して）

#### 各種gemをインストール

```
$ bundle init
```

↑のコマンドでGemfileを生成し、以下のように編集します。

```ruby:./Gemfile
# frozen_string_literal: true

source 'https://rubygems.org'

gem 'async-websocket'
gem 'google-api-client'
gem 'dotenv'
gem 'slack-ruby-bot'
```

その後、必要なGemをインストール。

```
$ bundle install --path vendor/bundle
```

※ 後ほどgemも含めてzipファイルにパッケージングする必要があるため、グローバルではなくローカル（「vendor/bundle」以下）にインストールします。

#### サービスアカウントの秘密鍵（JSON）をルートディレクトリ直下に配置

![スクリーンショット 2021-02-06 14.13.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/7e4ba2fd-8e06-d6cb-4c2d-866669c470a2.png)

下準備のところで作成したサービスアカウントの秘密鍵（JSON）を「credentilas.json」にリネームし、ルートディレクトリ直下に配置します。

うっかりGitHubなどにアップしてしまうと大変な事になるので、ちゃんとgit管理から外れているか確認してください。

#### app.rbを作成

```
$ touch app.rb
```
```ruby:./app.rb
require 'bundler/setup'
require 'google/apis/calendar_v3'
require 'googleauth'
require 'date'
require 'dotenv'
require 'slack-ruby-client'

Dotenv.load

APPLICATION_NAME = 'Google Calendar × Slack'.freeze # そこまで重要ではないので適当な名前でOK。
CREDENTIALS_PATH = './credentials.json'.freeze # サービスアカウント作成時にDLしたJSONファイルをリネームしてルートディレクトリに配置。
CALENDER_ID = ENV['CALENDER_ID'].freeze # Googleカレンダー設定ページの「カレンダーの統合」という項目内に記載されている。

class GoogleCalendar
  def initialize
    @service = Google::Apis::CalendarV3::CalendarService.new
    @service.client_options.application_name = APPLICATION_NAME
    @service.authorization = authorize
    @calendar_id = CALENDER_ID
  end
  
  # 認証。
  def authorize
    authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
      json_key_io: File.open(CREDENTIALS_PATH),
      scope: Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY
    )
    authorizer.fetch_access_token!
    authorizer
  end
  
  # 引数に渡した日付をもとに予定一覧を取得。
  def fetch_events(date)
    @service.list_events(
      @calendar_id,
      max_results: 10, # 取得する予定の最大数。
      single_events: true,
      order_by: 'startTime',
      time_min: "#{date}T00:00:00Z", # 取得を開始するタイミング
      time_max: "#{date}T23:59:59Z"  # 取得を終了するタイミング
    )
  end
end

def create_slack_message
  google_calender = GoogleCalendar.new
  events = google_calender.fetch_events(Date.today)
  
  # その日の予定が何も無かった場合はここで処理終了。
  return '本日の予定はありません。' if events.items.empty?
  
  event_list = ''

  events.items.each_with_index do |event, index|
    start_time = event.start.date || event.start.date_time
    end_time = event.end.date || event.end.date_time
  
    event_details = "* #{event.summary} (#{start_time.strftime('%H:%M')} ~ #{end_time.strftime('%H:%M')})"
    event_details << " #{event.hangout_link}" if event.hangout_link # もしGoogleハングアウトのURLがある場合はそれも拾う。
    event_details << "\n\n" unless index == events.items.size - 1

    event_list << event_details
  end
  
  # ↑で取得した予定をメッセージの形に整形する。
  message = <<~EOS
    本日の予定です！
  
    ```
    #{event_list}
    ```
  EOS
end

# Slack通知を行うための初期設定。
Slack.configure do |conf|
  conf.token = ENV['SLACK_BOT_TOKEN']
end

def post_to_slack
  message = create_slack_message
  
  client = Slack::Web::Client.new
  client.chat_postMessage(
    channel: ENV['SLACK_CHANNEL_NAME'], # 通知を飛ばしたいSlackチャンネルを指定。
    text: message, 
    as_user: true
  )
end

post_to_slack
```

#### 環境変数をセット

```
$ touch .env
```
```:./.env
CALENDER_ID=hoge@example.com
SLACK_BOT_TOKEN=xoxb-*******************
SLACK_CHANNEL_NAME=#hoge-channel
```

それぞれの値をセットしてください。（人によって違います）

#### テスト実行

```
$ bundle exec ruby app.rb
```

![スクリーンショット 2021-02-06 14.23.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/6cec02a1-a1cf-fe22-ade2-75ce467776e5.png)

指定したチャンネルにメッセージが飛んでいれば成功です。

![スクリーンショット 2021-02-06 14.25.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/95b21bc3-23cc-db82-c017-015342896840.png)

ちゃんと正確に取得できていますね。

### デプロイ

正常な動作が確認できたら、いよいよデプロイしていきます。

#### app.rbを編集

「./app.rb」を次のように書き換えます。

```ruby:./app.rb（変更前）
......

def post_to_slack
  message = create_slack_message
  
  client = Slack::Web::Client.new
  client.chat_postMessage(
    channel: ENV['SLACK_CHANNEL_NAME'], # 通知を飛ばしたいSlackチャンネルを指定。
    text: message, 
    as_user: true
  )
end

post_to_slack

......
```

```ruby:./app.rb（変更後）
......

def post_to_slack(event:, context:)
  message = create_slack_message
  
  client = Slack::Web::Client.new
  client.chat_postMessage(
    channel: ENV['SLACK_CHANNEL_NAME'], 
    text: message, 
    as_user: true
  )
end

......
```

- 「post_to_slack」の引数に「event:」「context:」を渡す。
- 最後の行で「post_to_slack」を実行している部分を削除。

引数に「(event:, context:)」を渡さないとLambda上では動かないので注意してください。
参照記事: [AWS Lambda 関数ハンドラー](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-handler.html)

#### gemをパッケージングしてLambdaレイヤーを作成

Lambdaレイヤー: 複数のLambda関数で外部ライブラリやビジネスロジックを共有できる仕組み。
参照: [AWS Lambda Layersでライブラリを共通化](https://qiita.com/t_okkan/items/394a15577bd1aad46ec3)

今回のデプロイではLambdaレイヤーを利用します。

各種gemをLambda関数と切り離しておく事でコードサイズを小さく保つ事ができますし、今後同じようなLambda関数を作る事になった際には使い回しができるので便利です。

##### gemをパッケージング（zipファイル）

```
$ zip -r layers.zip ./vendor
```

gemが入っている「vendor」以下をzipファイルに閉じ込めます。

![スクリーンショット 2021-02-06 14.53.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/6fc768b4-80be-0c7e-e091-938d75536bc6.png)

上手くいくとこんな感じで「layers.zip」として出力されるはず。

##### Lambdaレイヤーを作成

![スクリーンショット 2021-02-06 14.56.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/6694d630-8e71-f81b-d119-eaa68830ce5a.png)

AWSコンソール画面から「Lambda」→「レイヤー」→「レイヤーの作成」へと進み、先ほど出力したzipファイルをアップロードします。

![スクリーンショット 2021-02-06 14.59.09_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/2fec6a10-4193-c032-f976-9d79d17366db.jpeg)

これで今後、Googleカレンダーから予定を取得してSlack通知を飛ばすのに必要なgemを複数のLambda関数で使い回す事ができるようになりました。

#### Lambda関数を作成

Lambdaレイヤーの準備ができたので、次はLambda関数の作成に入ります。

![スクリーンショット 2021-02-06 15.02.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/54ec6e02-0870-d5cd-95ea-a55594c0fe43.png)

AWSコンソール画面から「Lambda」→「関数」→「関数の作成」へと進み、必要な情報を入力してLambda関数を作成します。

#### 実行コード（app.rb）と秘密鍵をアップロード

```
$ zip -r codes.zip app.rb credentials.json
```

先ほどと同じ容量で「app.rb」と「credentials.json」をzipファイル化。

![スクリーンショット 2021-02-06 15.08.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/796bb23d-5fe2-693f-eacb-54370d15346f.png)

Lambda関数の管理画面からzipファイルをアップロードします。

![スクリーンショット 2021-02-06 15.10.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/2fb4281b-7ff8-e2fe-5096-933d60072ab5.png)

上手くいくとこんな感じで２つのファイルが反映されているはず。

#### 各種設定

あとは細かい設定がいくつかあるのでそれらを片付けていきます。

##### メモリ&タイムアウトを変更

![スクリーンショット 2021-02-06 15.19.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/bd9c58b0-76b9-68f9-50c3-2cd0aa169fb5.png)

- メモリ
  - 512MB
- タイムアウト
  - 30秒

この辺の数値はお好みで変えてください。

##### ハンドラ名を変更

![スクリーンショット 2021-02-06 15.14.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/037d7865-6c0d-3b95-1bc3-8babf2f0ff88.png)

「ハンドラ」を「lambda_function.lambda_handler」から「app.post_to_slack」に変更。

##### Lambdaレイヤーとの紐付け

![スクリーンショット 2021-02-06 15.16.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/8ed4cdbb-840d-9fab-0346-25cd29f773cf.png)

![スクリーンショット 2021-02-06 15.17.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/358d4342-2b8a-c95b-813c-9564db3d0f0d.png)

「レイヤーの追加」から先ほど作成したレイヤーとバージョンを指定して追加。

##### 環境変数をセット

![スクリーンショット 2021-02-06 15.25.03_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/108af241-f775-d474-5c19-51e9ecbd05fc.jpeg)

それぞれ適宜入力。

- GEM_PATH
  - /opt/vendor/bundle/ruby/2.5.0
- TZ
  - Asia/Tokyo

↑この2つは固定。


#### テスト実行

これで大体の設定は済んだので、実際に動くかどうかテストします。

![スクリーンショット 2021-02-06 15.28.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/0e93fcd2-48c1-ad1e-0d43-84c0a340488d.png)

 - テンプレート
   - hello-world
 - 名前
   - test
 - パラメータ
   - 空欄でOK

![スクリーンショット 2021-02-06 15.28.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/601baabb-b44e-338c-c69d-087d3a0d2ed2.png)

「呼び出し」を実行してSlackの通知が飛んでいれば成功です。

#### スケジュール実行

最後に、決められたスケジュールで定期実行がされるようにします。

![スクリーンショット 2021-02-06 15.37.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/3e8c5af9-163d-2466-e817-dd569af7b602.png)

「トリガーを追加」をクリック。

![スクリーンショット 2021-02-06 15.36.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/005e26a7-723f-59a5-f8ba-1eae9076d5d4.png)

- トリガー名: CloudWatch Events
- ルール: 新規ルールの作成
- ルール名: 任意
- ルールの説明: 任意
- ルールタイプ: スケジュール式
- スケジュール式: 任意

cron式の書き方の説明については今回省略。
参照: [クーロン(cron)をさわってみるお](https://qiita.com/katsukii/items/d5f90a6e4592d1414f99)

今回は平日の午前10時に通知してもらう事を想定して「cron(0 1 ? * MON-FRI *)」としました。（UTCだと日本時間と9時間ほど時差があるので注意。）

時間の経過を待ち、しっかりと定期実行されていれば成功です。

![スクリーンショット 2021-02-06 15.43.12_censored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688854/5e454aaa-f0c0-37b9-995c-cd58815c4faf.jpeg)

もし上手く行かなかった場合はCloudWatchのロググループ内にログが出力されているはずなので適宜デバッグしてください。

## あとがき

お疲れ様でした。もし記事通りに進めて上手く動作しない箇所があったらコメント蘭などで指摘していただけると幸いです。