概要
Google Calendar APIは認証まわりが非常に難しく魔境と言われています。
プログラミング初心者や公式ドキュメントを読むことが苦手な人であれば大半は挫折することでしょう。
実際のところ、私もトライしてみてかなり苦戦しました。
おそらく、次使う時は極力GAS(Google Apps Script)で実装すると思います。(認証&連携が楽なので)
しかし、意地でもRubyを使ってGoogle Calendarを叩きたいという人は少なからずいると思いますし、今後も同じところで躓くひとが多いと思うので備忘録として書き残しておきたいと思います。
前提
- Ruby(2.2以上)
- Railsを使用することを想定(オブジェクト指向で設計)
- 本番環境:Heroku(無料でタスクスケジューラが使えるため)
公式ドキュメント
まずは公式ドキュメントをコピペしてじっくり眺めつつ、わからないところは仕様を調べましょう。
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
無駄を省いてモデルに切り出す
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メソッド(準備)
# 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にプッシュしないように気をつけましょう。
続いて鬼門である認証まわりを見ていきましょう。
authorizeメソッド(認証)
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メソッド(イベント情報の取得)
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)
場所が設定されてない時や時刻が終日のときはしっかり例外処理をすることをおすすめします。
データの取得、整形
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のソースコード、英語記事を読む力が問われてきます。
私自身かなり良い訓練になりました。
この記事が少しでも皆さんのお役に立てれば幸いです。