イベントのシフトを忘れていた人がいたため、リマインドのために、Google Calendar で管理されているシフトを Slack で連絡するスクリプトを書いてみました。
好きな言語が Ruby なので、Ruby で Google Calendar の API と Slack の API を叩いてやってみました。学べることがあったので、思い切ってメモに残す。
source 'https://rubygems.org'
git_source(:github) do |repo_name|
repo_name = "#{repo_name}/#{repo_name}" unless repo_name.include?("/")
"https://github.com/#{repo_name}.git"
end
# DEBUG
gem 'pry'
gem 'pry-byebug'
# FUNDAMENTAL
gem 'dotenv'
# GOOGLE API CLIENT
gem 'google-api-client'
# SLACK NOTIFICATION
gem 'slack-notifier'
# ENUM
gem 'ruby-enum'
$ bundle install
client_secret.json
という名前で、ダウンロードし保存する。下の authorize
メソッドを動かし、Web ページに表示されるハッシュ値をコンソールに貼り付ければ、今後 API を叩くことができます。下のコードは、google の公式に quickstart のページにあるコードを参考にしました。
require 'fileutils'
require 'google/apis/calendar_v3'
require 'googleauth'
require 'googleauth/stores/file_token_store'
class GoogleAuthentication
SCOPE = Google::Apis::CalendarV3::AUTH_CALENDAR_READONLY
OOB_URI = 'urn:ietf:wg:oauth:2.0:oob'
CLIENT_SECRETS_PATH = 'client_secret.json'
CREDENTIALS_PATH = File.join(Dir.pwd, '.credentials', 'calendar_ruby_sample_quickstart.yaml')
def self.access_to_calendar_service
Google::Apis::CalendarV3::CalendarService.new
end
def self.authorize
FileUtils.mkdir_p(File.dirname(CREDENTIALS_PATH))
client_id = Google::Auth::ClientId.from_file(CLIENT_SECRETS_PATH)
token_store = Google::Auth::Stores::FileTokenStore.new(file: CREDENTIALS_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"
puts url
code = gets
credentials = authorizer.get_and_store_credentials_from_code(
user_id: user_id, code: code, base_url: OOB_URI)
end
credentials
end
end
ここで、結構引っかかりました。上の google のサンプルコードは、アカウントがデフォルトで持っているカレンダーのイベント(青色のイベント)しか引っ張ってこないです。どうやって引っ張ってくるのかをソースコード(Google::Apis::CalendarV3::Service
クラス) を実際に読んで、調べてみました。
def list_calendar_lists(max_results: nil, min_access_role: nil, page_token: nil, show_deleted: nil, show_hidden: nil, sync_token: nil, fields: nil, quota_user: nil, user_ip: nil, options: nil, &block)
command = make_simple_command(:get, 'users/me/calendarList', options)
...
end
get
や me
などの言葉があり、calendarList
とも記述されています。
試しにやってみたら、思った通り、複数のカレンダーのシフト(イベント)が取得されました。
gem 内のコードを読むのは、大事だな〜と新卒の始めの時期に改めて感じました。
ログインしているアカウントのデフォルトのカレンダーには、シフトがないので、 reject
で、デフォルトのカレンダーの id を省きます。
require 'rubygems'
require 'dotenv'
require 'pry'
require './util/google_authentication'
require '../v2/calendar_item'
Dotenv.overload
class Calendar
@@calendar = Util::GoogleAuthentication.access_to_calendar_service
def ids
@@calendar.list_calendar_lists.items.reject{ |calendar| calendar.id == ENV['GMAIL_ACCOUNT'] }.map(&:id)
end
end
require 'rubygems'
require 'dotenv'
require 'pry'
require './util/google_authentication'
require '../v2/calendar_item'
class Calendar
# Get items
# @return GoogleCalendarItem[]
class << self
def shifts_tomorrow
tomorrow_shifts = []
ids.each { |calendar_id|
@@calendar.list_events(calendar_id).items.each { |event|
calendar_item = CalendarItem.new( { calendar_name: event.summary, start_time: event.start.date_time } )
tomorrow_shifts.push(calendar_item) if calendar_item.tomorrow_shift?
}
}
tomorrow_shifts
end
private
def ids
@@calendar.list_calendar_lists.items.reject{ |calendar| calendar.id == ENV['GMAIL_ACCOUNT'] }.map(&:id)
end
end
end
require '../v2/mentor_registry'
# Data object to wrap and carry an item originally from Google Calendar.
class CalendarItem
# Constructor
# @return CalendarItem
def initialize(calendar_item)
@calendar_item = calendar_item
end
# Return a mentor who is a participant of this calendar item.
# @return Mentor
def mentor
MentorRegistry.instance.find_by_name(calendar_name)
end
# Return a calendar_name assigned to the item.
# In this application, the calendar_name is always a name for a mentor.
# @return String
def calendar_name
@calendar_item[:calendar_name]
end
# Return when the item starts.
# @return String
def start_time
@calendar_item[:start_time].strftime('%m/%d %H:%M')
end
# Checks if calendar_item is a tomorrow_shift
# @return Boolean
def tomorrow_shift?
start_time_nil? && between_today_and_tomorrow?
end
private
# Checks if the calendar item object does not have any start_time in it.
# @return Boolean
def start_time_nil?
!@calendar_item[:start_time].nil?
end
# Checks if the calendar item is from today to tomorrow.
# @return Boolean
def between_today_and_tomorrow?
current_time = DateTime.now
@calendar_item[:start_time].between?(current_time, current_time + 1)
end
end
1 つのインスタンスしか生成したくなかったので、シングルトンパターンで書きました。
require 'yaml'
require 'pry'
require 'singleton'
require '../v2/mentor'
# List of Mentors
class MentorRegistry
include Singleton
MENTOR_CONFIG_FILENAME = 'mentors.yml'
# Factory, setup registry
# @return MentorRegistry
def initialize
@settings = YAML.load_file(MENTOR_CONFIG_FILENAME)
@list = {}
end
# Find mentors who has a specific name
# @return Mentor
def find_by_name(name)
@list[name] ||= Mentor.new(@settings[name])
end
end
# Mentor
class Mentor
# Constructor
# @param [Hash] data The hash variable that has mentor info in it.
# @return Mentor
def initialize(data)
@data = data
end
# Return the mention on slack in data instance variable.
# @return String
def mention
@data['mention']
end
# Check if his/her birth day is a day before the given date.
# @return boolean
def day_before_birthday?
@data['birthday'] == (Date.today+1).strftime('%m/%d')
end
end
require 'rubygems'
require 'pry'
require '../v2/util/slack_notifier'
require '../v2/calendar_item'
# Slack
class SlackForNotification
STARTER_NOTIFICATION = '明日のシフトはこちら!'
HOPING_REACTION_STATEMENT = '`こちらにメンションがついている方は今日23時までに必ず本通知にリアクション` をお願いします!'
NO_SHIFT_STATEMENT = '明日のシフトはありません!'
BIRTHDAY_STATEMENT = 'そして、なんと明日誕生日のメンターが!!! そのメンターは...!!!'
CELERATION_ENCOURAGEMENT_STATEMENT = '明日会ったときに、「誕生日おめでとう」と言おう!'
@notifier = SlackNotifier.notifier
class << self
def sends_starter_notification
@notifier.post(text: STARTER_NOTIFICATION)
@notifier.post(text: HOPING_REACTION_STATEMENT)
end
def sends_shift_notification(mention:, calendar_name:, start_time:)
notification = "<#{mention}> : #{calendar_name} : #{start_time}"
@notifier.post(text: notification)
end
def sends_birthday_notification(mention:, calendar_name:)
@notifier.post(text: "#{BIRTHDAY_STATEMENT}")
@notifier.post(text: "<#{mention}>#{calendar_name} :tada: :tada:")
@notifier.post(text: CELERATION_ENCOURAGEMENT_STATEMENT)
end
def sends_no_shift_notification
@notifier.post(text: NO_SHIFT_STATEMENT)
end
end
end
require 'slack-notifier'
# Slack Notifier
class SlackNotifier
@notifier = nil
def self.notifier
@notifier ||= Slack::Notifier.new(ENV['TIMES_JIO_SLACK_WEBHOOK_URL'], username: 'TECH::CAMP WASEDA SHIFT REMINDER')
end
def self.post(text:)
@notifier.post(text: text)
end
end
AWS EC2 インスタンスの crontab にスクリプトを登録しましょう。
require 'rubygems'
require 'yaml'
require 'pry'
require 'dotenv'
require '../v2/mentor_registry'
require '../v2/calendar'
require '../v2/calendar_item'
require '../v2/slack_for_notification'
shifts_tomorrow = Calendar.shifts_tomorrow
if shifts_tomorrow.empty?
SlackForNotification.sends_no_shift_notification
else
SlackForNotification.sends_starter_notification
shifts_tomorrow.each do |calendar_item|
SlackForNotification.sends_shift_notification(
mention: calendar_item.mentor.mention,
calendar_name: calendar_item.calendar_name,
start_time: calendar_item.start_time
)
end
end
#!/usr/bin/env bash
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"
bundle exec ruby main_script.rb
➜ ~ crontab -l
CRON_TZ=Asia/Tokyo
30 22 * * * cd /home/user_name/RubyAlgorithm/notification_on_slack && sh ./cron_script.sh >> /home/user_name/RubyAlgorithm/notification_on_slack/text.txt 2>&1
上にも書きましたが、gem 内のコードを読むことで、得られることがものすごく多いなと気づきました。 これで、シフトを忘れる人が出てきませんように願うばかりです。 最後まで読んでいただき、ありがとうございました。