3
2

More than 3 years have passed since last update.

Ruby(Rails)でGoogle Calendar API を本番環境(Heroku)で叩く

Posted at

概要

Google Calendar APIは認証まわりが非常に難しく魔境と言われています。
プログラミング初心者や公式ドキュメントを読むことが苦手な人であれば大半は挫折することでしょう。

実際のところ、私もトライしてみてかなり苦戦しました。
おそらく、次使う時は極力GAS(Google Apps Script)で実装すると思います。(認証&連携が楽なので)

しかし、意地でもRubyを使ってGoogle Calendarを叩きたいという人は少なからずいると思いますし、今後も同じところで躓くひとが多いと思うので備忘録として書き残しておきたいと思います。

前提

  • Ruby(2.2以上)
  • Railsを使用することを想定(オブジェクト指向で設計)
  • 本番環境:Heroku(無料でタスクスケジューラが使えるため)

公式ドキュメント

まずは公式ドキュメントをコピペしてじっくり眺めつつ、わからないところは仕様を調べましょう。

 quickstart.rb
require "google/apis/calendar_v3"
require "googleauth"
require "googleauth/stores/file_token_store"
require "date"
require "fileutils"

OOB_URI = "urn:ietf:wg:oauth:2.0:oob".freeze
APPLICATION_NAME = "Google Calendar API Ruby Quickstart".freeze
CREDENTIALS_PATH = "credentials.json".freeze
# The file token.yaml stores the user's access and refresh tokens, and is
# created automatically when the authorization flow completes for the first
# time.
TOKEN_PATH = "token.yaml".freeze
SCOPE = Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY

##
# Ensure valid credentials, either by restoring from the saved credentials
# files or intitiating an OAuth2 authorization. If authorization is required,
# the user's default browser will be launched to approve the request.
#
# @return [Google::Auth::UserRefreshCredentials] OAuth2 credentials
def authorize
  client_id = Google::Auth::ClientId.from_file CREDENTIALS_PATH
  token_store = Google::Auth::Stores::FileTokenStore.new file: TOKEN_PATH
  authorizer = Google::Auth::UserAuthorizer.new client_id, SCOPE, token_store
  user_id = "default"
  credentials = authorizer.get_credentials user_id
  if credentials.nil?
    url = authorizer.get_authorization_url base_url: OOB_URI
    puts "Open the following URL in the browser and enter the " \
         "resulting code after authorization:\n" + url
    code = gets
    credentials = authorizer.get_and_store_credentials_from_code(
      user_id: user_id, code: code, base_url: OOB_URI
    )
  end
  credentials
end

# Initialize the API
service = Google::Apis::CalendarV3::CalendarService.new
service.client_options.application_name = APPLICATION_NAME
service.authorization = authorize

# Fetch the next 10 events for the user
calendar_id = "primary"
response = service.list_events(calendar_id,
                               max_results:   10,
                               single_events: true,
                               order_by:      "startTime",
                               time_min:      DateTime.now.rfc3339)
puts "Upcoming events:"
puts "No upcoming events found" if response.items.empty?
response.items.each do |event|
  start = event.start.date || event.start.date_time
  puts "- #{event.summary} (#{start})"
end

無駄を省いてモデルに切り出す

calendar.rb

require "google/apis/calendar_v3"
require "googleauth"
require "googleauth/stores/file_token_store"
require "date"
require "fileutils"
class Calendar 



  # The file token.yaml stores the user's access and refresh tokens, and is
  # created automatically when the authorization flow completes for the first
  # time.



  ##
  # Ensure valid credentials, either by restoring from the saved credentials
  # files or intitiating an OAuth2 authorization. If authorization is required,
  # the user's default browser will be launched to approve the request.
  #
  # @return [Google::Auth::UserRefreshCredentials] OAuth2 credentials


  def authorize
    # 環境変数の定義
    uri = ENV["OOB_URI"]
    user_id = ENV["MAIL"]

      secret_hash = {
        "web" => {
          "client_id"     => ENV["CLIENT_ID"],
          "project_id" => ENV["PROJECT_ID"],
          "auth_uri" => ENV["AUTH_URI"],
          "token_uri" => ENV["TOKEN_URI"],
          "auth_provider_x509_cert_url" => ENV["PROVIDER_URI"],
          "client_secret" => ENV["CLIENT_SECRET"],
          "redirect_uris" => [ENV["REDIRECT_URIS"]],
          "javascript_origins" => [ENV["JAVASCRIPT_ORIGINS"]]
        }
      }
      # herokuの環境的に環境変数から読み込んだほうが良い
      client_id = Google::Auth::ClientId.from_hash secret_hash   
      token_store = Google::Auth::Stores::FileTokenStore.new file: "token.yaml"
      authorizer = Google::Auth::UserAuthorizer.new client_id, Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY, token_store

      credentials = authorizer.get_credentials user_id

      if !credentials
          url = authorizer.get_authorization_url base_url: uri
          puts "Open the following URL in the browser and enter the " \
              "resulting code after authorization:\n" + url
          code = ENV["CODE"]

          credentials = authorizer.get_and_store_credentials_from_code(
          user_id: user_id, code: code, base_url: uri
          )



      end
      credentials
  end
  # Initialize the API
  def initialize
      @service = Google::Apis::CalendarV3::CalendarService.new
      @service.client_options.application_name = ENV["APPLICATION_NAME"]
      @service.authorization = authorize
  end

  def fetch_events
      calendar_id = ENV["CALENDAR_ID"]
      now = DateTime.now + 1
      response = @service.list_events(calendar_id,
                                  max_results:   5,
                                  single_events: true,
                                  order_by:      "startTime",
                                  time_min:      DateTime.new(now.year,now.month,now.day,0,0,0),
                                  time_max:      DateTime.new(now.year,now.month,now.day,23,59,59) )
  end
end

フレームワークを使わずにRubyだけで書くくらいの小規模なコードであれば個人的にGASを使うことを推奨しているので、今回は大規模なシステムにGoogle Calendarを導入することを想定してRailsを使っていると想定しています。

余談ですが、GASでGoogle Calendarから引っ張った情報をRailsに送るという手法もあります。(こっちのほうが楽な人は楽かも)

ではひとつずつ見ていきましょう。

initializeメソッド(準備)

calendar.rb
  # Initialize the API
  def initialize
      @service = Google::Apis::CalendarV3::CalendarService.new
      @service.client_options.application_name = ENV["APPLICATION_NAME"]
      @service.authorization = authorize
  end

calendarクラスでインスタンスが生成されるとまず、initializeメソッドが呼び出されます。

3行目でauthorizeメソッドが呼び出され認証がはじまります。

変数は全て環境変数(ENV)に格納し、githubにプッシュして漏れるリスクをなくしています。

dotenv-railsというgemをインストールすれば.envファイルで一括管理できるのでおすすめです。
間違えて大事なキーをgithubにプッシュしないように気をつけましょう。

dotenv-railsの参考記事

続いて鬼門である認証まわりを見ていきましょう。

authorizeメソッド(認証)

 calendar.rb
def authorize
    # 環境変数の定義
    uri = ENV["OOB_URI"]
    user_id = ENV["MAIL"]

      secret_hash = {
        "web" => {
          "client_id"     => ENV["CLIENT_ID"],
          "project_id" => ENV["PROJECT_ID"],
          "auth_uri" => ENV["AUTH_URI"],
          "token_uri" => ENV["TOKEN_URI"],
          "auth_provider_x509_cert_url" => ENV["PROVIDER_URI"],
          "client_secret" => ENV["CLIENT_SECRET"],
          "redirect_uris" => [ENV["REDIRECT_URIS"]],
          "javascript_origins" => [ENV["JAVASCRIPT_ORIGINS"]]
        }
      }
      # herokuの環境的に環境変数から読み込んだほうが良い
      client_id = Google::Auth::ClientId.from_hash secret_hash   
      token_store = Google::Auth::Stores::FileTokenStore.new file: "token.yaml"
      authorizer = Google::Auth::UserAuthorizer.new client_id, Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY, token_store

      credentials = authorizer.get_credentials user_id

      if !credentials
          url = authorizer.get_authorization_url base_url: uri
          puts "Open the following URL in the browser and enter the " \
              "resulting code after authorization:\n" + url
          code = ENV["CODE"]

          credentials = authorizer.get_and_store_credentials_from_code(
          user_id: user_id, code: code, base_url: uri
          )

      end
      credentials
  end

公式ドキュメントと違うところはsecret_hashという変数に全て格納し、Google::Auth::ClientId.from_hash secret_hash でclient_idを入手している点です。
公式ではGoogleDeveloperからダウンロードしたjsonファイルを読み込む仕様でしたがherokuでjsonファイルを読み込むのは少々面倒なので環境変数で読み込むことは出来ないか検討し、公式のソースコードを読み漁り、hashから読み込む関数があったので書き換えました。

secret_hashの中身はすべてダウンロードしたjsonファイルの中に書いてある変数です。
uriはGoogleDeveloperにリダイレクトURLを登録したものにします。

jsonファイルから読み込む仕様で問題ない場合はこのような書き換えは必要ないです。

実際にこの関数(authorize)を実施しようとするとcredentialsがnilなのでターミナルに認証のURLが表示され、そこのURLにcodeが格納されているためコピペして環境変数(CODE)に格納します。

jsonファイルのダウンロードやredirect_uriについて詳しく知りたい方は↓におすすめの記事を載せておきます。
おすすめ記事

fetch_eventsメソッド(イベント情報の取得)

calendar.rb
  def fetch_events
      calendar_id = ENV["CALENDAR_ID"]
      now = DateTime.now + 1
      response = @service.list_events(calendar_id,
                                  max_results:   5,
                                  single_events: true,
                                  order_by:      "startTime",
                                  time_min:      DateTime.new(now.year,now.month,now.day,0,0,0),
                                  time_max:      DateTime.new(now.year,now.month,now.day,23,59,59) )
  end

認証がうまく行けばあとはイベント情報を取得するだけです。
こちらもほとんど公式ドキュメントのまんまですが公式の説明が適当すぎて分かりにくかったので軽く解説します。
calendar_idはGoogle Calendarの設定画面から取得します。

時刻は基本USタイムゾーンで返却されるので返却された時刻をJST(日本時間)に変換する必要があります。
DateTimeはrailsのゾーン設定に依存するので極力TimeWithZoneを使うのをおすすめします。

時間の扱い方にあまり慣れていない人は伊藤さんの素晴らしい記事があるので一読することをおすすめします。

RubyとRailsにおけるTime, Date, DateTime, TimeWithZoneの違い

  • max_result : 取得したい最大のイベント数
  • order_by : 取得するイベントの並び順
  • time_min : 取得したいイベントの開始時刻の最小値(最も近い時間帯)
  • time_max : 取得したいイベントの終了時刻の最大値(最も遠い時間帯)
  • single_events : これはよくわかんないけどtrueにしたほうが良い(みんなそうしてる)

データが取得できたら欲しいデータだけ変数に格納し、データを整形してあげましょう。
私自身ここから先のコードは別のモデルに切り出しました。

responseの値をデバック(ターミナルに吐き出して)ひとつひとつどんな情報が格納されているか観察するのをおすすめします。(response.itemsでイベントの配列が取得)

多くの人が使うであろう値をここに列挙しておきます

  • 題名、タイトル(summary)
  • 開始時刻(start)
  • 終了時刻(end)
  • 場所(location)

場所が設定されてない時や時刻が終日のときはしっかり例外処理をすることをおすすめします。

データの取得、整形

message.rb
class Message
    def organize_from_calendar
        club_calendar = Calendar.new
        response = club_calendar.fetch_events
        if response.items.empty?
            result = '予定無し'
        else
            event =response.items.first
            start_time = event.start.date_time.in_time_zone('Tokyo').strftime("%H:%M")
            end_time = event.end.date_time.in_time_zone('Tokyo').strftime("%H:%M")
            location = event.location
            title = event.summary
            #要件は満たせるけど可読性が微妙
            #locationがnilではない場合は○○での「で」を追加
            location += "で" if location
            result = "明日は#{start_time}から#{end_time}まで#{location}#{title}があります。\n欠席or遅刻者は背番号+(スペース)遅刻or欠席+(スペース)理由の形式でご回答ください。\n(例)21番 欠席 授業があるため"
        end
    end
end

せっかくなので私が実際に書いたコードを載せておきます。
例外処理はbegin rescueを使って書いても問題ないです。

最後に

公式ドキュメントにはquickstart.rbとか書いてあるのに全然すぐにスタート出来ない&公式ドキュメントは不親切で笑えてきます。

こういうときこそ、公式のドキュメントやgithubのソースコード、英語記事を読む力が問われてきます。

私自身かなり良い訓練になりました。

この記事が少しでも皆さんのお役に立てれば幸いです。

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2