Google Calendar APIを叩く

学科で #eeic_botthon という「Rubyを使って1日でツイッターのbotを作ろう!」イベントがあった。そこで僕もメイドさんbotを作りたくてGoogle Calendarと連携した予定管理ができるようにしたかったわけだ。

Rubyには一応 google_calendar というgemがあるのだが、これがいかんせん使いづらい。しかも、何故か学科PCの画面を閉じると動かなくなる。意味不明だ。そこで、google-api-clientという公式gemを使って実装しようということに。

これが全然資料がなくて、なにをやっていいかさっぱりわからず。結局仕組みを解読してなんとかがんばった。

まずはアプリケーションの登録。いろんな方法があるが、ここでは以下の様な方法を取る。

  1. Google Developer Consoleでプロジェクトを登録(すでにあればそれでもよい)。プロジェクト名はなんでも良い
  2. 左の「APIと認証」→「API」からGoogle CalendarのAPIを有効にする
  3. 左の「APIと認証」→「認証情報」から、「新しいクライアントIDを作成」→「インストールされているアプリケーション(その他)」
  4. 「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をにらめっこしながら実装すればよい。


コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です