Google Calendar API を Ruby から使う: 認証と基本的なAPI

Google の API を触らざるを得ないときは毎回「いやだなーいやだなー」と思いながら調べるんですが、日本語だとあまり情報がなく、結局 google-api-ruby-client のソースを読みながら作業になるので、この際まとめることにしました。

包括的なもの、というよりは個人的に再利用できるように軽く書いておきます。

今回は Google Calendar だけじゃなく, ほとんどのAPI使う際に必要な認証部分 + 基本的なAPI操作ができるところ

まで書きます :+1:

前提

  • google-api-ruby-client 0.9 系

google-api-ruby-client は 0.8 系から 0.9 系に上がる際にインターフェースが変わってしまっており(マイナーアップデートじゃないだろ + はやくα版やめろ), 現在日本語のほとんどの記事は 0.8 以前を前提としてますが、今回は 0.9 系とさせていただきます。

OAuth 認証

今回, ログインしているユーザーごとの Google Calendar を取得して云々… ということをしたいため、認証はユーザーごとに OAuth2.0 で行うようにします。

Google の developer console認証情報を作成 を行い, client_secret_xxxx.json を取得しておいてください

この際, 環境毎に認証情報( client_secret.json ) を持つようにしてください。 redirect_uri が環境に応じて異なると思うので。

development だと localhost:3000/oauth2callback, production だと https://sampleapp.herokuapp.com/oauth2callback のような Redirect URI を設定するようになると思います。(自分で好きなように設定してください)

認証インターフェース色々

Ruby Quickstart / Google Calendar API とかを見ると, Google::Auth::Stores::FileTokenStore というクラスを使ってごちゃごちゃやってますが、要は

  • 認証した後の情報(Access Token, Refresh Token など)をファイルで保持
  • あればそれを使う。なければファイルを作成

という方法で、ユーザーごとにファイル作成すんの? となり現実的じゃない(なんかキモい)ため、 Google::APIClient::ClientSecrets というクラスを使うようにします。

従来の 0.8 系までの方法とほとんど同じ使い方なので、もし 0.8 系から移行する場合もこの方法ならほとんど書き換えることなくできるかと思います。

他に Google::Auth::Stores::RedisTokenStore というものもあり、名前から察するに redis に保持するんだろうという気がしますが、なるべくシンプルにしたかったので今回は使っていません。

google-auth-library-ruby を見れば詳しいことはわかります。

Google::APIClient::ClientSecrets

さきほど取得した client_secret_*.json というファイルを client_secret.json という名前にして、projectのどこかに置いて使え、っていうパターンで説明されてることが多いんですが、 github とかにそのまま上げちゃうのもどうなの?って感じのものなので、環境変数を用意しておき、サーバー毎にファイルを配置するようにします。

以下お見苦しいクラスが並びますが「気持ち」で読み取ってください :see_no_evil:

以下のようなクラスを作っておき, client_secret.json を読み込むメソッドに Google::APIClient::ClientSecrets.load(GoogleClientSecret.path) のようにして渡してあげます。

Rails 内で使ったので、 Rails 特有のメソッド等ありますが、よしなに読み替えていただけると…

class GoogleClientSecret
  # project の root に `client_secret.json` を配置する想定
  cattr_reader :default_path do
    Rails.root.join("client_secret.json")
  end

  GOOGLE_CLIENT_ID     = ENV.fetch("GOOGLE_CLIENT_ID")
  GOOGLE_CLIENT_SECRET = ENV.fetch("GOOGLE_CLIENT_SECRET")
  GOOGLE_REDIRECT_URI  = ENV.fetch("GOOGLE_REDIRECT_URI")

  def self.path
    return default_path if File.exists?(default_path)

    new.create
  end

  def create
    File.write default_path, json_content
    default_path
  end

  private

  def json_content
    # project_id はご自分の `client_secret.json` を見て設定してください
    {
      web: {
        client_id: GOOGLE_CLIENT_ID,
        project_id: "sampleapp",
        auth_uri: "https:\/\/accounts.google.com\/o\/oauth2\/auth",
        token_uri: "https:\/\/accounts.google.com\/o\/oauth2\/token",
        auth_provider_x509_cert_url: "https:\/\/www.googleapis.com\/oauth2\/v1\/certs",
        client_secret: GOOGLE_CLIENT_SECRET,
        redirect_uris: [GOOGLE_REDIRECT_URI]
      }
    }.to_json
  end
end

認証用のクラス

google-api-client が変なディレクトリ構成しているため、使いたいクラスを都度 require してあげてください。

Gemfile 側に書いておいても大丈夫です。

認証をする人のモデルをここでは User モデルとしており, refresh_token:string カラムを持たせておいてください。

require 'google/apis/calendar_v3'
require 'google/api_client/client_secrets'

class GoogleAuthorizer
  CLIENT_SECRET_PATH = GoogleClientSecret.path
  attr_reader :auth_client

  def initialize
    @auth_client = load_auth_client
  end

  def authorize_uri
    auth_client.authorization_uri.to_s
  end

  def update_token!(user)
    auth_client.update!(refresh_token: user.refresh_token)
    auth_client.fetch_access_token!
  end

  private

  def load_auth_client
    client_secrets = Google::APIClient::ClientSecrets.load(CLIENT_SECRET_PATH)
    client = client_secrets.to_authorization
    # もし書き込み権限を与えたくないなら, Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY という定数を使ってください
    client.update!(scope: Google::Apis::CalendarV3::AUTH_CALENDAR)
    client
  end
end

UI側に カレンダーを連携する ボタンみたいなのを設置しておいて、それが押された際に refresh_token が無いようであれば GoogleAuthorizer#authorize_uri に飛ばしてあげて、認証を済ませてもらいます。

Calendar Handling

実際に Google Calendar 自体を操作するクラスです

authorizer = GoogleAuthorizer.new
authorizer.update_token!(user)

cal = GoogleCalendar.new(user, authorizer.auth_client)
cal.calendar_list

みたいな感じで使います。設計がイマイチですが、ほんとすいません。

毎回 GoogleAuthorizer#update_token! を呼んで, Access Token を取得し直してますが、以前に取得した access_token を保存しておき、使うようにしても、「 code がないょ」と怒られてしまうので、このような実装になってます。

イケてない。

なので DB には access token を保存しないような作りになってます :weary:

以下最初にやってることとしては, #load_serviceservice.authorization に先の認証objectを渡しているところぐらいでしょうか。

あとは ここ にある、 service.rb ( と場合によっては classes.rb ) を見て、都度使いたいメソッドを見てこんな感じのクラスに生やせばいいと思います。

require 'google/apis/calendar_v3'

class GoogleCalendar
  attr_reader :service, :user

  def initialize(user, auth_client)
    @user = user
    @service = load_service(auth_client)
  end

  # カレンダー一覧を取得
  # default では writer or owner 権限のあるものだけ取るようにしてある
  def calendar_list(options = { min_access_role: 'writer' })
    service.list_calendar_lists(options)
  end

  # カレンダーの event を取得
  # default のカレンダーは google account の mail アドレスが calendar id になっている
  def events(calendar_id = user.email, options = {})
    service.list_events(calendar_id, options)
  end

  def watch_events(calendar_id = user.email, options = {})
    channel_id = SecureRandom.uuid
    my_channel = Google::Apis::CalendarV3::Channel.new(
      id: channel_id,
      address: "https://sampleapp.herokuapp.com/google_calendars/callback",
      type: 'web_hook',
    )
    channel = service.watch_event(calendar_id, my_channel, options)
    Rails.logger.info channel.to_h
    channel
  end

  def stop_channel(channel_id, resource_id)
    channel = Google::Apis::CalendarV3::Channel.new(
      id: channel_id,
      resource_id: resource_id
    )
    service.stop_channel(channel)
  end

  private

  def load_service(auth_client)
    s = Google::Apis::CalendarV3::CalendarService.new
    # なんでもいいです
    s.client_options.application_name = 'kaeritai'
    s.authorization = auth_client
    s
  end
end

ここにある #watch_events, #stop_channel は Google Calendar に変更があった場合に通知を行いたい、という要望があったら使うためのものです。

Push Notification / Google Calendar API や, Google Calendar上の予定の変更を監視する / Qiita などがよくまとまってておすすめです。

ここらへんがすごいめんどくさいので、別のPostにまとめますmm

今回自分で wrapper 用のクラスを適当に書いてますが、なんとなく使い方がわかれば幸いですmm

続く

Contents