Heroku(+Scheduler) + RailsでLineのリマインダーBotを作ったら世界に平和が訪れた

<h1>Ruby SDK, Push Message API, Scheduler</h1>
LineBot+Herokuの記事を見ると未だに頑張ってFixieでIPを固定しようとしていたり,SDKを使っていなかったり,オウム返しばかりでPush Message APIについての情報がなかったりで結構情報がアップデートされていない感じがあったため,LineBotの2016年12月版として書いて見ようと思います.

<h1>何を作ったのか?</h1>
現在シェアハウスに住んでいるのですが,ゴミ出しとか特にルールが無いと大体特定の人に偏ります.というわけでゴミ出し当番を決めてかつゴミの日をリマインドするBotを作ることにしました.

住民間のコミュニケーションは主にLineなので,必然的にLineBotを作ることに.サーバーはAWS使ってとかやり始めるとしんどそうだったのため,Heroku(+Scheduler)で行くことにしました.

Githubにソースコードは公開しています.
https://github.com/shuheiktgw/tombot

<h1>アカウント発行などの下準備</h1>
この辺を参考にしながら作業しました.

http://qiita.com/YoheiMiyamoto/items/f6851cdb40891edf0e57
http://qiita.com/pokochi/items/cc06e3f6edc9136446ca


上記に書いてない注意点としては以下のような感じです.

<h3>LineのアカウントタイプはDevelopper Trialを選択する</h3>
意外なハマりポイントとしてはLineのMessage APIのアカウントタイプに2種類ありまして,今回はリマインドメッセージをBot側から送りつける必要があるため必ずDevelopper Trialの方を選択してください(Developper TrialじゃないとPush Message API使えないらしい).

<h3>FixieとかLine側のIPのWhitelistの設定は不要</h3>
大体Heroku*LineBot系の記事を見るとFixieというHeorkuのAddOnを使ってIPを固定しようとしているのですが,Line側のIPのWhitelistの設定は今年の4月ぐらいから任意になっているためその辺の設定不要です(FixieがあるとアウトバウンドのProxyを指定しなくてはいけないのでSDKが使えなくなる).

参考: https://developers.line.me/news?ny=2016&nm=04

<h3>rails newは--database=postgresqlをつけて</h3>
Heorkuがpostgresqlで動いているため,newするときにこれをつけておくと楽です.その他のHeroku周りのやつは以下を参照してみてください.

参考: <a href='https://devcenter.heroku.com/articles/getting-started-with-rails5'>Getting Started with Rails 5.x on Heroku</a>
参考: <a href='https://devcenter.heroku.com/articles/getting-started-with-rails4'>Getting Started with Rails 4.x on Heroku</a>


<h1>LineのRuby SDKを使った場合の設定</h1>
大体ここに書いてあります.
https://github.com/line/line-bot-sdk-ruby

RailsだとGemfileに以下の一行を足します.
`gem 'line-bot-api'`

SDKをつかった場合lib/line_client.rbが以下のような感じになるかと思います.

```ruby:lib/line_client.rb

require 'line/bot'

class LineClient
  CHANNEL_SECRET = ENV['CHANNEL_SECRET']
  CHANNEL_ACCESS_TOKEN = ENV['CHANNEL_ACCESS_TOKEN']
  PUSH_TO_ID = ENV['PUSH_TO_ID']

  attr_reader :client

  def initialize
    @client ||= Line::Bot::Client.new { |config|
      config.channel_secret = CHANNEL_SECRET
      config.channel_token = CHANNEL_ACCESS_TOKEN
    }
  end

  def reply(reply_token, message)
    client.reply_message(reply_token, text_message(message))
  end

  def push(message)
    client.push_message(PUSH_TO_ID, text_message(message))
  end

  private

  def text_message(text)
    {
        "type" => "text",
        "text" => text
    }
  end
end

```

※1 line_client.rbってなんだという方はアカウント発行などの下準備のあたりで紹介したqiitaの記事を参照してください.

※2 LineClient#pushはPush Message APIを使用する際に使います.PUSH_TO_IDは後でもう少し詳しく説明します.

<h1>ControllerとかServiceとかその辺の設定</h1>
あとでHerokuのSchedulerというrakeタスクを定期実行するAddOnを使用してリマインダーを設定するのですがその前にControllerからServiceまでの設定を一気に整えてしまいましょう.リマインダーを設定する際はHeroku側のTZ環境変数をAsia/Tokyoに変更しておくこともお忘れなく.

<Strong>この辺は我が家の設定なので適宜読み飛ばしてください.
重要なのは次のSchedulerの設定とPush先のIDの取得方法です.</Strong>

以下で家賃のリマインド,掃除のリマインド,ゴミ出しのリマインドを設定しています.

```ruby:app/controllers/webhook_controller.rb

class WebhookController < ApplicationController
  protect_from_forgery with: :null_session

  def reminder
    reminders = get_reminders
    logger.info(reminders)

    reminders.each do |reminder|
      res = line_client.push(reminder)
      logger.info(res.body)
    end
  end

  private

  def get_reminders
    reminder_service.execute
  end

  def reminder_service
    ReminderService.new
  end

  def line_client
    @line_client ||= LineClient.new
  end
end

```



```ruby:app/services/reminder_service.rb

require 'date'
require_relative '../../config/initializers/constants'

class ReminderService
  def initialize(today = Date::today, garbage_service = GarbageService.new, cleaning_date_service = CleaningDateService.new)
    @today = today
    @garbage_service = garbage_service
    @cleaning_date_service = cleaning_date_service
  end

  def execute
    check_reminders
  end

  def check_reminders
    reminders = []

    garbage_reminder       = check_garbage_reminder
    payment_reminder       = check_payment_reminder
    cleaning_date_reminder = check_cleaning_date_reminder

    reminders << garbage_reminder       if garbage_reminder.present?
    reminders << payment_reminder       if payment_reminder.present?
    reminders << cleaning_date_reminder if cleaning_date_reminder.present?

    reminders
  end

  def check_garbage_reminder
    @garbage_service.reminder(@today)
  end

  def check_payment_reminder
    if @today.day == 25
      "今日は25日です. 家賃の振り込みをおねシャッス!口座は以下です\n#{Constants::ACCOUNT}"
    elsif @today.day == 28
      '今日は28日です. まさかまだ家賃振り込んでない人なんていないよね...'
    else
      ''
    end
  end

  def check_cleaning_date_reminder
    @cleaning_date_service.reminder(@today)
  end
end

```

```ruby:app/services/garbage_service.rb

require 'date'
require_relative '../models/garbage'
require_relative '../../config/initializers/constants'

class GarbageService
  def initialize(garbage_model = Garbage)
    @garbage_model = garbage_model
  end

  def reminder(today)
    # 0 = 日曜日, 6 = 土曜日
    day_of_week   = today.wday
    week_of_month = get_week_of_month(today)

    case day_of_week
      when 0
        set_person_in_charge
        "明日からのゴミ出し大臣は#{get_person_in_charge}さんです! よろしくお願いします!"
      when 1
        if week_of_month == 2 || week_of_month == 4
          "今日は不燃ゴミの日です. ゴミ出し大臣の#{get_person_in_charge}さんは不燃ゴミをお願いします!"
        end
      when 2, 5
        "今日は燃えるゴミの日です. ゴミ出し大臣の#{get_person_in_charge}さんは燃えるゴミをお願いします!"
      when 6
        "今日は資源ごみの日です, ゴミ出し大臣の#{get_person_in_charge}さんはカン･ビン･ペットボトル･ダンボールのゴミ出しをお願いします!"
      else
        ''
    end
  end

  def get_person_in_charge
    @garbage_model.last.name
  end

  def set_person_in_charge_manually(name)
    if Constants::MEMBER_LIST.include?(name)
      @garbage_model.create(name: name)
      "今週のゴミ出し大臣を#{name}さんに設定しました"
    else
      "名前が不正です. セットすることができる名前はこちら#{Constants::MEMBER_LIST.join(', ')}"
    end
  end

  private
  def set_person_in_charge
    list = Constants::MEMBER_LIST
    previous = get_person_in_charge

    if previous == list.last
      @garbage_model.create(name: list.first)
    else
      next_index = list.index(previous) + 1
      @garbage_model.create(name: list[next_index])
    end
  end

  def get_week_of_month(today)
    (today.day + 6) / 7
  end
end

```

```ruby:app/services/cleaning_date_service.rb

require 'date'
require_relative '../models/cleaning_date'

class CleaningDateService

  def initialize(cleaning_date_model = CleaningDate)
    @cleaning_date_model = cleaning_date_model
  end

  def reminder(today)
    if scheduled_cleaning_date - today == 3
      return "次の掃除は#{scheduled_cleaning_date.strftime("%m/%d")}です.忘れないようにしましょう.参加できない場合は代わりに1000円ですよ~"
    elsif scheduled_cleaning_date == today
      return '今日は掃除の日です.参加できない場合は代わりに1000円ですよ~'
    end

    if scheduled_cleaning_date.month != today.month && scheduled_cleaning_date.month != today.month + 1
      case today.day
        when 1
          '月初なので, 今月の掃除日を決めましょう!'
        when 5, 10, 20
          'まだ今月の掃除日が決まっていないみたいです... 今日中に必ず決めましょう!'
        when 29
          '今月掃除してないです... おこですよ!!'
        else
          ''
      end
    else
      ''
    end
  end

  def scheduled_cleaning_date
    @cleaning_date_model.last.cleaning_date
  end

  def scheduled_cleaning_date=(date)
    @cleaning_date_model.create(cleaning_date: date)
  end
end

```

<h1>Heroku Schedulerの設定</h1>
上記のリマインダーを定期実行するためのrakeタスク化してします.さらにそれをHerokuのSchedulerというAddOnで毎朝定期実行していきます.

<h3>1. rakeタスクの登録</h3>
lib/tasks以下に以下のように,WebhookController#reminderを実行するrakeタスクを作成します.

```ruby:lib/tasks/reminder.rake

task :reminder_task => :environment do
  webhook_controller = WebhookController.new
  webhook_controller.reminder
end

```

<h3>2. Heroku Schedulerの設定</h3>
Heroku SchedulerというAddOnをHerokuに追加します.追加したら以下のようにreminder_taskを登録します.

時間はUTCでしか設定できないようです.自分は毎朝日本時間の朝6時半にリマインドしてほしかったので21:30 UTCに時間を設定しています.

![スクリーンショット 2016-12-12 9.13.55.png](https://qiita-image-store.s3.amazonaws.com/0/107471/6baa3e42-b614-7306-f172-e773f1763f2e.png)

<h1>Push先のIDの取得</h1>

ふう..これでやっと最後です.

line_client.rbの中に`PUSH_TO_ID`というのがあったと思いますがこれを指定することで,毎朝決まったグループ/個人(我が家の場合は住人が入っているグループのID)にメッセージを送信します.

このIDの取得方法のベストプラクティスがまだわからないのですが,基本的にはLineAPIからのリクエストの中に入っている`source: {groupId: xxxxx}`(個人の場合はuserId)を使用すれば問題ないようです.リクエスト自体はオウム返し系の記事で書いてあるようにcallbackの口をコントローラーに作って上げれば受け取れるかと思います.


<h1>最後に</h1>

ここでは本題からずれるのであまり書きませんでしたが,実際にボットを使っていると結構手動で直したいときがありました(今週のゴミ出し担当を手動で設定したいとか).なので実はそのへんの口もちゃんと用意してあります.

ソースコードは以下に公開しているので,気になる方は確認して見てください.
https://github.com/shuheiktgw/tombot