Googleカレンダーと連携した日報生成ツールをRubyで作る

この記事は、[UUUM Advent Calendar 2018] 17日目用の記事です

会社では、日報を `本日の業務` 、 `明日の予定` 、 `一言` といった項目のMarkDown形式で、Web上のフォームに入力する運用をしています。

作業やミーティングに関しては、社内ではGoogleカレンダーで共有・管理しているので、Googleカレンダーからタイトルをコピペして日報を作っていたりして、なんとなく悲しい思いをしていました。

今回は、以下のようなフォーマットの日報を出力するツールをRubyで作ってみることにしました。

```markdown
# 本日の業務

* [MTG] 朝会
* [MTG] 開発定例
* [MTG] 1 on 1 （aさん）
* 機能Aのテストコード

# 明日の予定

* [MTG] 朝会
* [MTG] 委員会
* 機能B実装

# 一言
弱い者ほど相手を許すことができない。許すということは、強さの証だ。
```

# Googleカレンダーヘのアクセス

日報内の `本日の業務` と `明日の予定` は、Googleカレンダーに登録しているイベントをもとに作成します。なので、自分のGoogleカレンダーにAPIでアクセスすることにしました。

Googleの各種API向けに、Ruby向けのクライアントの実装がGemとして配置されています。おまけに、公式ドキュメントにGemの利用例まで書いてありますので、これに従ってイベント取得まで行っていくようにします。

## クライアントのインストール

[公式ドキュメント](https://developers.google.com/calendar/quickstart/ruby)では、 `gem install google-api-client` コマンドでインストールしていましたが、これだと作ったツールを動かすまでの手順が複雑化してしまうかもしれません。今回は Bundler を使ってインストールできるようにします。

```ruby:Gemfile
source 'https://rubygems.org'

gem 'google-api-client'
```

インストール実行時、インストール先をローカルディレクトリにして、他のツールに影響を少なくなるようにします。

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

普段は生のRubyではなくRailsを書くことが多いので忘れかけているのですが、こういった形でGemを追加した場合、利用するには対象のコードを明示的に `require` する必要があります。 Bundler でインストールしたGemの場合、以下のように行うと一気に `require` を行うことができます。

```ruby
require "bundler/setup"
Bundler.require
```

この辺りの事情に関しては、以前以下の記事に書いたのでぜひ！

[Railsの風が吹いたら確認したい、Rubyのモジュールシステム - UUUM攻殻機動隊(エンジニアブログ)](http://system.blog.uuum.jp/entry/2016/08/10/110000)

## APIトークン取得 & Googleカレンダーイベント取得

APIトークン取得は実はまあまあ煩雑なのですが、基本的に[公式ドキュメント](https://developers.google.com/calendar/quickstart/ruby)に沿って行いました。

* Google API ConsoleでGoogle Calendar APIを有効化
* Google API Consoleで認証情報（OAuth クライアントID）を作成し、認証情報をダウンロード

を行っておき、[公式ドキュメントのサンプルコード](https://github.com/gsuitedevs/ruby-samples/blob/master/calendar/quickstart/quickstart.rb) を動かすとトークン取得して直近のイベントを取れるところまでが簡単にできます。

が、私の場合、トークン取得は正しく動作したものの、期待したイベントが取れませんでした。。

Googleカレンダーは、自分以外のカレンダーを「マイカレンダー」として登録できるのですが、APIでイベントを取得する際に「マイカレンダー」内のどのカレンダーからイベントを取得したいのか、 `calendar_id` として正しく指定しないとうまくいかなくなります。

`calendar_id` として指定できる文字列をどのように取得すればいいのかというと、一旦以下のようなコードを書いて動かすことで、現在自分の「マイカレンダー」に登録してある全てのカレンダーの `calendar_id` が取得できます。

```ruby
# Initialize the API
service = Google::Apis::CalendarV3::CalendarService.new
service.client_options.application_name = APPLICATION_NAME
service.authorization = authorize
# ⇧ 上記のようにして、API呼び出し用のサービスをセットアップ
# 公式ドキュメントのサンプルからコピペです

# マイカレンダー内の calendar_id を列挙
service.list_calendar_lists.items.each do |item|
  puts item.summary
end
```

これで取れる文字列、GoogleカレンダーのUIで表示されるカレンダーと一致するものもあればそうでないものもあってややこしいのですが、この手順で取れた文字列を `calendar_id` として使えば間違いなさそうです。

## 日時の指定

さて、カレンダーから「本日分」のイベントを取得する際、APIに対して日時を指定する必要があります。日報なので基本的には本当に「本日」とその「翌日」を指定すればいいのですが、もしかすると日報作成時点で日付をまたいでしまう（出し忘れて後日出す）とか、「翌日」が休日で次の営業日から予定を取得したいパターンがありえます。

このツールはCLIで使うので、コマンドライン引数で受け付けられるようにします。

当然のことながら、Rubyは標準でコマンドライン引数を受け付けられる機能はあるのですが、今回は OptionParser というライブラリを使って、もう少し便利に引数を受け取れるようにします。

以下のコードを追加することで、コマンドライン引数に `-s` と `-e` という名前付きで、2つの日付を渡せるようにできます。

```ruby
require "optparse"

option = {}
OptionParser.new do |opt|
  opt.on('-s [mm/dd]', '今日の日付（省略可）') {|v| option[:start_date] = v}
  opt.on('-e [mm/dd]', '明日の日付（省略可）') {|v| option[:tommorow_date] = v}

  opt.parse!(ARGV)
end

# このようにして受け取ったパラメータを取得できるようになる
p option[:start_date]
```

引数を受け取った場合はそれを採用、受け取らなかった場合はシステム的に「本日」とその「翌日」を採用すれば使いやすいので、以下のようにしました。

```ruby
# コマンドライン引数で受け取った "12/15" のような文字列から Time オブジェクトを作成
# 年部分については引数を手で入力するのも面倒なので、システム的に「今年」を使う
def create_date_from_param(param_date)
  date_array = param_date.split("/")
  Time.parse("#{Time.now.year}/#{date_array[0]}/#{date_array[1]}")
end

# 「本日」をコマンドライン引数を受け取っていたら採用、そうでなければシステム的な「本日」
if option[:start_date]
  start_date = create_date_from_param(option[:start_date])
else
  start_date = Time.now
end

# 「翌日」をコマンドライン引数を受け取っていたら採用、そうでなければ「本日」の翌日
if option[:tommorow_date]
  tommorow_date = create_date_from_param(option[:tommorow_date])
else
  tommorow_date = start_date.tomorrow
end
```

APIにわたす日時には、その日の `00:00:00` と `23:59:59` を渡したいです。この手の時刻の取得はRailsだとデフォルトで `beginning_of_day` と `end_of_day` を使えば簡単に取れます。今回は Rails内のユーティリティである ActiveSupport という Gemをインストールして使ってみることにしました。

```ruby:Gemfile
gem 'activesupport'
```

```ruby
# 利用するには、Bundler.require だけだとだめで、この require が明示的に必要
require 'active_support/time'

# beginning_of_day などを利用して、その日全てのイベントを取得
today_items = service.list_events(
  calendar_id,
  single_events: true,
  time_min: start_date.beginning_of_day.iso8601,
  time_max: start_date.end_of_day.iso8601,
  order_by: 'startTime').items
```

# 文字列として出力

CLIなので、日報の文字列は標準出力に書き出すことにします。

日報は以下のようなフォーマットです。

```markdown
# 本日の業務

* [MTG] 朝会
* [MTG] 開発定例
* [MTG] 1 on 1 （aさん）
* 機能Aのテストコード

# 明日の予定

* [MTG] 朝会
* [MTG] 委員会
* 機能B実装

# 一言
弱い者ほど相手を許すことができない。許すということは、強さの証だ。
```

全ての行を文字列の連結やヒアドキュメントを使って構築してもいいのですが、Rubyの場合は標準ライブラリに ERB という便利な機能がついてくるので利用することにします。

RailsではデフォルトでHTMLのテンプレートエンジンとして ERB を使っていますが、今回も似たような形で使えました。

以下のようなファイルを用意します。[^1]

[^1]: ファイル名に `.md.erb` といったRailsのアセットビルド風な拡張子を採用しているのは特に意味はなく、単に `.erb` でも動作すると思います。


```erb:nippo.md.erb
# 本日の業務

<% today_items.each do |item| -%>
* <%= item.summary %>
<% end -%>

# 明日の予定

<% tomorrow_items.each do |item| -%>
* <%= item.summary %>
<% end -%>

# 一言
<%= hitokoto %>
```

このようにしておくことで、いちいち文字列連結を String クラスの機能で頑張らなくても、 `today_items` と `tomorrow_items` というオブジェクトを用意しておくだけで一気に目的の文字列が作成できます。 （ `hitokoto` 部分に関しては後述します）

普段RailsでERBを使う場合にはあまり意識しなくてよいのですが、素朴にERBを使うと改行が余計に入ってしまって意図したフォーマットにならなくなります。 `<% tomorrow_items.each do |item| %>` といった制御構文に含まれる最後の改行まで出力されてしまうのが原因です。

その場合、前述のコード内で行っているように `%>` を `-%>` に変え、ERB利用時に `trim_mode` として `"-"` を指定することでいい感じに改行を扱うことができるようになります。

```ruby
require 'erb'

# trim_modeに "-" を指定
nippo_erb = ERB.new(File.read("nippo.md.erb"), nil, "-")

# runメソッドで標準出力まで行ってくれて便利
nippo_erb.run
```

この辺りのERBの使い方に関しては、Rubist Magazine（るびま）の以下の記事が詳しいです。

[標準添付ライブラリ紹介 【第 10 回】 ERB](https://magazine.rubyist.net/articles/0017/0017-BundledLibraries.html)

# 一言

日報のフォーマットでは、以下のように「一言」という欄があって、その日に感じたことを軽く書くことになっています。

```markdown
# 一言
テストを書くのに明け暮れた一日でした
```

本当に業務のことを書いてもいいですが、「ラーメン食べたい」とか「お金がありあまっているので沼に投げ入れたい」とか心の声が漏れちゃった感じの内容でもいいことになっています。

さすがにここをRubyで生成するのは難しいのですが、とりあえず過去の偉人の名言でも入れておくと格好がつくかもしれないので入れてみます。

[名言集.com というサイトにランダムで名言を表示してくれるページ](http://www.meigensyu.com/quotations/view/1968.html)があります。このページ内の「名言」をスクレイピングさせてもらい、内容を取ってくるようにしました。

Rubyのスクレイピングでは `Nokogiri` が便利で速いので入れてみます。

```ruby:Gemfile
gem 'nokogiri'
```

以下のように実装することで、該当のページからの名言取得が簡単にできます。

```ruby
require "open-uri"
require "nokogiri"

# 名言集.com のランダムページをスクレイピングさせてもらう
def fetch_hitokoto
  charset = nil
  url = "http://www.meigensyu.com/quotations/view/random"
  html = open(url) do |f|
    charset = f.charset
    f.read
  end

  path = '//*[@id="contents_box"]/div[2]/div[1]'
  doc = Nokogiri::HTML.parse(html, nil, charset)

  doc.xpath(path).inject("") { |result, node| node.inner_text }
end
```

ここまで作ったので、ERBのテンプレートを呼び出す前に

```ruby
hitokoto = fetch_hitokoto
```

としておき、

ERB内で、

```erb:nippo.md.erb
# 一言
<%= hitokoto %>
```

とすることで、一言に「名言」が入るようになります。

[スクレイピングさせていただいているページ](http://www.meigensyu.com/quotations/view/1968.html)にアクセスしてみるとわかるのですが、日報内に入れる「一言」っぽくない文言が取れることも多いのですw

今アクセスしたところ、

> 彼が夕食に遅れるときは、 浮気しているか死んで道端に転がっているかのどっちかなのよ。 道端でくたばっているほうがいい、といつも思ったわ。

という名言が取れたのですが、これ日報に入れていいのだろうか。。

# まとめ

以上でRuby製の日報作成ツールができました。成果物は以下のリポジトリに入れました。

[nakahashi/nippo: 日報生成ツール](https://github.com/nakahashi/nippo)

実行イメージは以下のような感じです。

```
$ ruby nippo.rb
# 本日の業務

* [MTG] 朝会
* [MTG] 開発定例
* [MTG] 1 on 1 （aさん）
* 機能Aのテストコード

# 明日の予定

* [MTG] 朝会
* [MTG] 委員会
* 機能B実装

# 一言
友情とは、誰かに小さな親切をしてやり、 お返しに大きな親切を期待する契約である。
```

普段Railsを書くことが多いのですが、生のRubyを書くのも面白いと思いました。