【Google Calendar/Slack/Ruby】Ruby でシフトリマインドスクリプトを書いてみた。

作成日: 2017-11-06 /

前書き

イベントのシフトを忘れていた人がいたため、リマインドのために、Google Calendar で管理されているシフトを Slack で連絡するスクリプトを書いてみました。

好きな言語が Ruby なので、Ruby で Google Calendar の API と Slack の API を叩いてやってみました。学べることがあったので、思い切ってメモに残す。

1. 必要なgemのインストール

Gemfile
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

2. Google Cloud Platformでの設定

  1. Google Calendar API を有効し、無効になると表示されれば、OK
  2. 次に認証情報 -> 認証情報を作成 -> OAuth クライアント ID -> その他 -> 認証情報がある json を client_secret.json という名前で、ダウンロードし保存する。

3. 認証を行なっていく

下の authorize メソッドを動かし、Web ページに表示されるハッシュ値をコンソールに貼り付ければ、今後 API を叩くことができます。下のコードは、google の公式に quickstart のページにあるコードを参考にしました。

google_authentication.rb
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

4. アカウントに紐づいている全てのカレンダーを取得する

ここで、結構引っかかりました。上の google のサンプルコードは、アカウントがデフォルトで持っているカレンダーのイベント(青色のイベント)しか引っ張ってこないです。どうやって引っ張ってくるのかをソースコード(Google::Apis::CalendarV3::Service クラス) を実際に読んで、調べてみました。

service.rb
 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

getme などの言葉があり、calendarList とも記述されています。 試しにやってみたら、思った通り、複数のカレンダーのシフト(イベント)が取得されました。 gem 内のコードを読むのは、大事だな〜と新卒の始めの時期に改めて感じました。 ログインしているアカウントのデフォルトのカレンダーには、シフトがないので、 reject で、デフォルトのカレンダーの id を省きます。

calendar.rb
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

5. イベントを全取得して、次の日のシフトをフィルターを通して、取得する

google_calendar.rb
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

6. 取得したイベントのクラスを定義する

util/array_iterator.rb
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

7. シフトに入っている人のデータを管理するクラスを定義

1 つのインスタンスしか生成したくなかったので、シングルトンパターンで書きました。

mentor_registry.rb
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

8. シフトに入っている人のクラスを定義

mentor.rb
# 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

9. 最後にスラックに送信

slack_for_notifiacation.rb
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
slack_notifier.rb
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

10. crontab で、毎日通知がくるように

AWS EC2 インスタンスの crontab にスクリプトを登録しましょう。

main_script.rb
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
cron_script.bash
#!/usr/bin/env bash

export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"

bundle exec ruby main_script.rb
crontab
  ~ 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

11. こうなりました

まとめ

上にも書きましたが、gem 内のコードを読むことで、得られることがものすごく多いなと気づきました。 これで、シフトを忘れる人が出てきませんように願うばかりです。 最後まで読んでいただき、ありがとうございました。