学科で #eeic_botthon という「Rubyを使って1日でツイッターのbotを作ろう!」イベントがあった。そこで僕もメイドさんbotを作りたくてGoogle Calendarと連携した予定管理ができるようにしたかったわけだ。
Rubyには一応 google_calendar というgemがあるのだが、これがいかんせん使いづらい。しかも、何故か学科PCの画面を閉じると動かなくなる。意味不明だ。そこで、google-api-clientという公式gemを使って実装しようということに。
これが全然資料がなくて、なにをやっていいかさっぱりわからず。結局仕組みを解読してなんとかがんばった。
まずはアプリケーションの登録。いろんな方法があるが、ここでは以下の様な方法を取る。
- Google Developer Consoleでプロジェクトを登録(すでにあればそれでもよい)。プロジェクト名はなんでも良い
- 左の「APIと認証」→「API」からGoogle CalendarのAPIを有効にする
- 左の「APIと認証」→「認証情報」から、「新しいクライアントIDを作成」→「インストールされているアプリケーション(その他)」
- 「JSONをダウンロード」を押してこれを client_secrets.json という名前で保存
これで事前準備は完了。このjsonはアプリケーション情報が入るので漏れないように。
次にリフレッシュトークンを得る。OAuthにはアクセストークンとリフレッシュトークンと認証コードの3種類があって、認証コードを通すとアクセストークンとリフレッシュトークンが得られて、リフレッシュトークンによってもアクセストークンが得られるという仕組み(たぶん)。アクセストークンは一時的(3600秒しかもたない)ので、リフレッシュトークンを覚えこませてアクセストークンを何度も発行して使う。
以下のRubyを、client_secrets.json と同じフォルダに適当なファイル名で保存する。redirect_uriは上で登録したアプリケーションと同じものを用いるが、おそらく上のコードと同じなはず。Google Calendar API以外のものを使いたいなら、scopeのところをいじると良い。
require 'google/api_client' require 'launchy' client_secrets = Google::APIClient::ClientSecrets.load auth_client = client_secrets.to_authorization auth_client.update!(:scope => 'https://www.googleapis.com/auth/calendar', :redirect_uri => 'urn:ietf:wg:oauth:2.0:oob') auth_uri = auth_client.authorization_uri.to_s puts auth_uri Launchy.open(auth_uri) auth_client.code = gets auth_client.fetch_access_token! puts auth_client.refresh_token
実行するとブラウザが開いて認証を求められる(ブラウザが開かなかったら標準出力されたURLをコピペして開く)。認証するとコードが与えられるので、それを端末にコピペしてEnter。するとリフレッシュトークンが吐出される仕組み。
なおリフレッシュトークンが漏れると何でもかんでもアクセスできるようになるので注意して管理すること。
あとはこれを使ってクライアントを作ってぽいぽいして〜という流れになるが、面倒くさいのでライブラリっぽいものを書いた。かなり適当なので、運用する際はちょいちょいいじりながら使うとよい。
require 'google/api_client' require 'time' class CalendarClient def initialize(application_name:, client_id:, client_secret:, refresh_token:, redirect_uri: 'urn:ietf:wg:oauth:2.0:oob') @client = Google::APIClient.new(:application_name => application_name) @client.authorization = Signet::OAuth2::Client.new( token_credential_uri: 'https://accounts.google.com/o/oauth2/token', audience: 'https://accounts.google.com/o/oauth2/token', scope: ['https://www.googleapis.com/auth/calendar'], client_id: client_id, client_secret: client_secret, refresh_token: refresh_token ) @client.authorization.refresh! @service = @client.discovered_api('calendar', 'v3') end end class Event attr_accessor :id, :summary, :description, :location, :start_time, :end_time, :all_day def initialize(event) @summary = event.summary @id = event.id @description = event.description @location = event.location @all_day = false if event.start.date_time @start_time = Time.parse(event.start.date_time.to_s) elsif event.start.date @start_time = Time.parse(event.start.date.to_s) @all_day = true end if event.end.date_time @end_time = Time.parse(event.end.date_time.to_s) elsif event.end.date @end_time = Time.parse(event.end.date.to_s) end end def all_day? @all_day end end class CalendarClient def get_all_events(calendar_id: 'primary', maxResults: nil, timeMin: nil, timeMax: nil) @client.authorization.refresh! parameters = { :calendarId => calendar_id } if maxResults parameters[:maxResults] = maxResults end if timeMin parameters[:timeMin] = timeMin.strftime('%Y-%m-%dT%H:%M:%S%:z') end if timeMax parameters[:timeMax] = timeMax.strftime('%Y-%m-%dT%H:%M:%S%:z') end result = @client.execute!(:api_method => @service.events.list, :parameters => parameters) arr = [] result.data.items.each do |e| arr += [Event.new(e)] end arr.sort {|a, b| a.start_time <=> b.start_time } end def insert_event(summary:, description: nil, location: nil, start_time:, end_time:, all_day:, calendar_id: 'primary') @client.authorization.refresh! request = { 'summary' => summary } if all_day request['start'] = {'date' => start_time.strftime('%Y-%m-%d')} request['end'] = {'date' => end_time.strftime('%Y-%m-%d')} else request['start'] = {'dateTime' => start_time.strftime('%Y-%m-%dT%H:%M:%S%:z')} request['end'] = {'dateTime' => end_time.strftime('%Y-%m-%dT%H:%M:%S%:z')} end if description request['description'] = description end if location request['location'] = location end results = @client.execute!(:api_method => @service.events.insert, :parameters => { :calendarId => calendar_id }, :body_object => request) Event.new(results.data) end def delete_event(calendar_id: 'primary', event_id:) @client.authorization.refresh! @client.execute(:api_method => @service.events.delete, :parameters => {'calendarId' => calendar_id, 'eventId' => event_id}) end end
クライアントの初期化は、CalendarClientのinitializeで行っている。application_nameに何を使えばいいのかずっと悩んでいたのだが、どうやらなんでもよいらしい。refresh_tokenにはリフレッシュトークンを、client_idとclient_secretにはそれぞれクライアントIDとシークレットコードを入れる。これはクライアント一覧画面にも表示されているし、client_secrets.jsonにも書いてある。
Eventはイベントのラッパー的なものになっている。もとのEventの時刻定義が意味不明で、こんなかんじの実装をしてあげないと単純に扱えない。
あとはCalendarClientのメソッドを叩くだけ。get_all_eventsは引数なしでも動くので一回叩いてみるとよいだろう。時刻を表現する文字列も結構特殊なので、Timeをぶちこんだら自動で変換してくれるような処理を各所で入れている。あと、吐き出される順番がむちゃくちゃなので、これもクライアントでソートした。
使うときは以下のようにする。これは実際に用いているコードだ。
@client = CalendarClient.new(application_name: "Ishotihadus's Maid Bot", client_id: "****", client_secret: "****", refresh_token: "****") def today now = Time.now Time.new(now.year, now.month, now.day, 0, 0, 0, now.strftime("%:z")) end def days(n = 1) 60*60*24*n end def get_day_event(n) @client.get_all_events(timeMin: today + days(n), timeMax: today + days(n + 1)) end def get_day_event_str(n, str) arr = get_day_event(n) if arr.length == 0 return str + 'は特に予定はないわ' end str = str + 'は' day_event = arr.select{|item| item.all_day} not_day_event = arr.select{|item| !(item.all_day)} day_event.each do |event| str += '、' + event.summary end not_day_event.each do |event| str += '、' + event.start_time.strftime("%H:%M") + 'から' + event.summary end str += 'があるわ!' str end get_day_event_str(0, '今日')
ここまで来たら読者もだいたい理解したと思うので、後は公式のAPIリストとgoogle-api-clientのGitHubをにらめっこしながら実装すればよい。