【実装編】hubotでチームだけのお手軽オリジナルslackリマインダーを作る

まだ、導入編をやっていない方は[【導入編】hubotでチームだけのお手軽オリジナルslackリマインダーを作る](http://qiita.com/saino-katsutoshi/items/bafde772b445653e174f)をやってください。

>寒空の朝昔の女の夢を見た。その時、朝日が股間を照らしていた。

さて今日は、hubot企画の実装編です。
今回は前回の予告通り、 **cronを使って定期投稿** と **railsアプリケーションのapiを叩く** の二点を主題にして進めていきます。

# 手順

- 導入編で作成したhubotを持参してください＿ø(●ʘ╻ʘ●)
- railsで簡単なapiを作ります（※すでにチームのプロジェクトがある方はそれを使ったほうが楽です）
  - チームのプロジェクトを持っている方はrailsのslack-apiというgemを使って、チームのメンバーのidやe-mailを取得すると、連携やメンションが簡単にできます。
- hubotからapiを叩いてデータを取得します
- 定時にそのデータを元に投稿します

# 事前準備

まずは今回使用するrailsアプリケーションを作ります。ここでは手順だけ記述し、説明は割愛させていただきます。また、すでにアプリケーションを持っている方で、そこにapi作るよって方はこの章は飛ばしていただいて大丈夫です。

## データベース作成

```bash
# データベースはmysqlを使っているので、-dオプションでmysqlを指定
$ rails new hubot-api-app -d mysql
$ cd hubot-api-app
$ rake db:create
$ rails g model user family_name:string first_name:string
$ rails g model shift date:date start_time:integer
$ rails g model shift_user user:references shift:references status:integer
$ rake db:migrate
```

## モデルの関連付け

```rb:user.rb
class User < ActiveRecord::Base
  has_many :shift_users
  has_many :shifts, through: :shift_users
end
```

```rb:shift.rb
class Shift < ActiveRecord::Base
  has_many :users, through: :shift_users
  has_many :shift_users
end
```

```rb:shift_user.rb
class ShiftUser < ActiveRecord::Base
  belongs_to :user
  belongs_to :shift

  enum status: %i(not_in_charge in_charge manager)
end
```

shfit_user.rbに関してはgenerateの時にreferencesで定義をしてあるので、すでにuserとshiftに対する関連付けがされています。

### 必要なgemを追加

Gemfileに以下を追記します。

```rb:Gemfile
gem 'gimei'
```

`bundle install` コマンドを実行。

## データベースに初期値を設定

seeds.rbファイルをダウンロードし、データベースに初期データを入れます。

```
$ rm db/seeds.rb
$ wget db/ https://github.com/saino-katsutoshi/hubot-api-app/raw/master/db/seeds.rb
$ rake db:seed
```

データベース設計について軽く言及します。

テーブル名 | カラム名 | 役割
:-:|:-:|:-:
shifts | date | シフトの日付
　| start_time | シフト開始時刻
users | family_name | 姓
　| first_name | 名
shift_users | user_id | usersテーブルへの外部キー
　| shift_id | shiftsテーブルへの外部キー
　| status | 0:担当でない 1:担当 2:責任者

## コントローラーを作成

### 明日のシフトを取得するコントローラーを作成

```bash
$ rails g controller api/shifts/tomorrow
```

```rb:tomorrow_controller.rb
class Api::Shifts::TomorrowController < ApplicationController
  def index
    @users = []
    tomorrow_shifts = Shift.includes(:users).where(date: Date.tomorrow)
    i = 11
    tomorrow_shifts.each do |shift|
      @users << { start_time: i , in_charge: shift.users.where(shift_users:{status: 1}), manager: shift.users.where(shift_users:{status: 2}) }
      i += 2
    end
  end
end
```

```jbuilder:index.json.jbuilder
json.array! @users do |item|
  json.start_time item[:start_time]
  json.in_charge do
    json.array! item[:in_charge] do |in_charge|
      json.id in_charge.id
      json.full_name in_charge.family_name + in_charge.first_name
    end
  end
  json.manager do
    json.array! item[:manager] do |manager|
      json.id manager.id
      json.full_name manager.family_name + manager.first_name
    end
  end
end
```

```rb:route.rb
Rails.application.routes.draw do
  namespace :api, defaults: {format: :json}  do
    namespace :shifts do
      resources :tomorrow, only: :index
    end
  end
end
```

## heroku

これからapiをherokuにアップします。
この章ではherokuとsequelProを使います。
本記事ではmysqlを使います。sqLite3をお使いの方は以下のサイトを参考に登録してください。

- [Rails Girl Guide](http://railsgirls.jp/heroku/)

### herokuにアップする

```bash
$ heroku login
# 登録したe-mailとパスワードを入力
$ git init
$ git add .
$ git commit -m 'initial commit'
$ heroku create hubot-api-app
```
左が作成されたアプリケーションのURL、右がgitのURLです。
<img width="735" alt="スクリーンショット_2015-12-11_14_29_21.png" src="https://qiita-image-store.s3.amazonaws.com/0/86426/8ce4a399-40e5-ceed-0376-8c9b8d404867.png">

### データベースをmysqlに変更する

```bash
$ heroku addons:add cleardb
$ heroku config | grep CLEARDB_DATABASE_URL
```
出力: CLEARDB_DATABASE_URL: **mysql://×××××××××××××××××××××**
`mysql2gem`を使っているのでデータベースのURLもmysqlからmysql2に変更します。

```bash
$ heroku config:set DATABASE_URL=mysql2://×××××××××××××××××××××
$ heroku config
DATABASE_URL: mysql2://×××××××××××××××××××××
```
これは、以下のようになっています。
mysql2://ユーザー名:パスワード@ホスト名/データベース?reconnect=true
sequelProでデータベースにアクセスします。お気に入りに追加ボタンを押しておくと、次からアクセスするのにサイド入力する必要がなくなります。

<img width="524" alt="スクリーンショット_2015-12-11_15_02_07.png" src="https://qiita-image-store.s3.amazonaws.com/0/86426/e4de2ee7-9cf2-f0c4-f247-16dda69ac963.png">

これでデータベースへのアクセスができました。

### herokuのデータベースに初期値を入れる

```bash
$ heroku run rake db:migrate
$ heroku run rake db:seed
```

これでapiの準備が完了しました。これまででエラーなどでつまづいた方はコメントにて僕に連絡してください。


# apiをhubotに叩かせる

やっとこれから本題に入ります。
前章までに作ったapiをhubotから叩くことで、json型のデータを取得し、それを利用してリマインダーを作ります。また、次の章でそれを定期投稿できるようにします。

## hubotスクリプトの基礎を抑える

まず、hubotスクリプトで良く使うメソッドの一覧表と使用例をまとめておきます。

### hubotスクリプトの基本的な関数と使い方

関数 | 仕様 | 結果
:-:|:-:|:-:
respond | 呼ばれたら反応する | ![](https://i.gyazo.com/4d0bf2d96b47b6756b9066be36c4dd7c.png)
hear | チャンネル上のメッセージに反応する | ![](https://i.gyazo.com/241704044b79fd1003ca7a455546e968.png)
enter | チャンネルに入室した際に反応する | ![](https://i.gyazo.com/bfa192fc9065673d9f4888a1f1ba1aa3.png)
leave | チャンネルを退出した際に反応する | 
topic | トピックが立てられたら反応する | ![](https://i.gyazo.com/b6a9cec2358716af4fb26a765c2a0064.png)
send | メッセージを送信する | 
reply | 発言者に対して返信する | ![](https://i.gyazo.com/cd19782f126471335ee6243e28578817.png)
random | 引数の配列からランダムに取り出す | ![](https://i.gyazo.com/2ebce7ad76166183666ff61c68fb7494.png)

プロパティ | 仕様 | 結果
:-:|:-:|:-:
match | respondやhearで正規表現でマッチしたものを取り出すときに使います | ![](https://i.gyazo.com/adbcd860f36c715fb15bfbf9b25f6e54.png)
message | 送られてきたメッセージに関する情報を取得できます| 

**使用例集**

```coffeescript:respond
module.exports = (robot) ->
  robot.respond /疲れた/i, (msg) ->
    msg.send "あとちょっと、頑張って♡"
```

```coffeescript:hear
module.exports = (robot) ->
  robot.hear /休憩/i, (msg) ->
    msg.send "お疲れ様！”
```

```coffeescript:reply
module.exports = (robot) ->
  robot.hear /誰か(.+)/i, (msg) ->
    msg.reply "#{msg.match[1]}？どした？"
```

```coffeescript:enter_and_leave
module.exports = (robot) ->
  robot.enter (msg) ->
    msg.send "いらっしゃい！#{msg.message.user.name}さん！"
  robot.leave (msg) ->
    msg.send "#{msg.message.user.name}さん ばいばい、、、"

```

```coffeescript:topic
module.exports = (robot) ->
  robot.topic (msg) ->
    msg.reply "トピックが立ったぞい！"
```

```coffeescript:enter
module.exports = (robot) ->
  robot.enter (msg) ->
    msg.send "いらっしゃい！#{msg.message.user.name}さん！"
```

```coffeescript:random
module.exorts = (robot) ->
  words = [
    '大吉',
    '中吉',
    '吉',
    '大凶'
  ]
  robot.respond /おみくじ/i, (msg) ->
    msg.send msg.random words
```

```coffeescript:match
module.exports = (robot) ->
  robot.respond /(.+)駅/, (msg) ->
    msg.send "#{msg.match[0]}"
    msg.send "#{msg.match[1]}"
```

```coffeescript:message
module.exports = (robot) ->
  robot.respond /こんにちは/, (msg) ->
    msg.send "@#{msg.message.user.name}さん、こんにちは"
    msg.send "このチャンネルは ##{msg.message.room} です"

# messageによって得られるデータは以下のようになってます。
 message: {
      user: { id:'user', name:'saino', room:'#general' },
      text: 'こんにちは',
      id: undefined,
      done: false,
      room: '#general'
   },
```

### メンションのつけ方

メンションのつけ方はいたって簡単で、@channelとしたかったら **<!channel>** とするだけです。
以下表にまとめました。特定のユーザーにメンションを飛ばす場合は、単に文字列に **@id** とするだけでメンションがつきます。

入力 | 出力
:-:|:-:
<!channel> | @channel
<!here> | @here
<!everyone> | @everyone
@id | @id

## httpメソッドでhubotからapiを叩く

まずは、簡単にこちらからのメッセージに対して反応させるようにします。

```coffeescript:reminder.coffee
module.exports = (robot) ->
  robot.respond /hi/i, (msg) ->
    msg.send 'Hi'
```
するともちろん以下のようになります。

```bash
Hubot> Hubot hi
Hubot> Hi
```
これでとりあえず反応してくれるようになったので、次はapiを叩きます。

```coffeescript:remind.coffee
module.exports = (robot) ->
  robot.resopnd /hi/i, (msg) ->
    request = msg.http('http://hubot-api-app.herokuapp.com/api/shifts/tomorrow').get()
```

msgオブジェクトの **httpメソッド**は、引数のurlにHTTP通信をします。
**getメソッド**は、お察しの通りHTTPメソッドです。
**queryメソッド**を使えば、パラメーターも送ることができます。使用例は以下の通りです。

```coffeescript:queryメソッド使用例
module.exports = (robot) ->
  robot.respond /hi (.*)/i, (msg) ->
    keyword = msg.match[1]
    msg.http('https://xxxx/')
      .query(q: keyword)
      .get()
```

これで、変数requestにapiを叩いた結果のデータを格納することができました。ではこれからレスポンスとして返されたjsonデータから具体的なデータをメッセージとして送ります。

レスポンスは以下のような形になってます。（これらの人物はおそらく実在しません。）

```json
[
  {
    "start_time":11,
    "in_charge":[{"id":162,"full_name":"野口政美"},{"id":192,"full_name":"岡田春風"},{"id":92,"full_name":"丸山泰弘"}],
    "manager":[{"id":122,"full_name":"吉沢徳男"}]
  },
  {
    "start_time":13,
    "in_charge":[{"id":182,"full_name":"吉沢紗愛"},{"id":22,"full_name":"栗田実音"},{"id":242,"full_name":"榎本晴歌"}],
    "manager":[{"id":232,"full_name":"戸田倖"}]
  },
  {
    "start_time":15,
    "in_charge":[{"id":132,"full_name":"荻野采弓"},{"id":42,"full_name":"大山桃央"},{"id":72,"full_name":"佐々木清司"}],
    "manager":[{"id":52,"full_name":"中野紫稲"}]
  },
  {
    "start_time":17,
    "in_charge":[{"id":42,"full_name":"大山桃央"},{"id":2,"full_name":"大平朝水"},{"id":62,"full_name":"小山悠莉"}],
    "manager":[{"id":182,"full_name":"吉沢紗愛"}]
  }
]
```

```coffeescript:remind.coffee
weekDayJP = ['日', '月', '火', '水', '木', '金', '土']
module.exports = (robot) ->
  robot.respond /give me (.*) shift/i, (msg) ->
    nObj = new Date
    month = nObj.getMonth() + 1
    date = nObj.getDate() + 1
    day = weekDayJP[nObj.getDay()]
    tomorrow = "#{month}月#{date}日(#{day})"
    request = msg.http('http://hubot-api-app.herokuapp.com/api/shifts/' + msg.match[1])
                .get()
    request (err, res, body) ->
      data = JSON.parse body
      message = ''
      for value in data
        message += "【#{value.start_time.toString()}時】\n"
        message += "責任者："
        for v2 in value.manager
          message += "#{v2.full_name}  "
        message += "\n"
        for v1 in value.in_charge
          message += "#{v1.full_name}  "
        message += "\n"
      msg.send "#{tomorrow}のシフトは、、、、、\n#{message}"
```

**(.*)**の部分をキャプチャし、それをhttpメソッドのパスに渡すことでtomorrowやtodayに対応します。todayなどやりたい場合は、apiを追加しないといけません。
変数**request**にはレスポンスの情報が格納されています。
**body**はapiからのレスポンスがテキストの形になっています。`JSON.parse`の引数にこれを指定してあげると、テキストをjsonに解析してくれます。
それ以下は単にjsonからデータを取得しています。
このあたりを**slack-api**でチームのメンバーのidなどを取得することで、メンションなどつけることができます。今回は分量が多くなってしまうので、省略します。

これで、こちらのメッセージに反応して明日のシフトを送ってくれるようになりました。では最後にこれを定刻に投稿してくれるようにします。

# cronを使って定刻に投稿する

hubotのディレクトリで以下のコマンドを実行する。

```bash
$ npm install --save cron
$ npm install --save time
```

あとは、cronを設定するだけです。

```coffeescript:example.coffee
cronJob = require('cron').CronJob

module.exports = (robot) ->

  cronJob = new cronJob('* * * * * *', () ->
    envelope = room: "#general" #投稿するルーム指定
    robot.send envelope, "cron動いてるよ"  #投稿メッセージ
  )

  cronJob.start()  #jobを開始、startがないと永遠に実行されません。
  # cronJob.stop()  #jobを停止
```

## 時間を設定する

第一引数にcronの時間を設定します。全部で６つ指定します。左から、

- 秒: 0 ~ 59
- 分: 0 ~ 59
- 時: 0 ~ 23
- 日付: 1 ~ 31
- 月: 0 ~ 11
- 曜日: 0 ~ 6(0:日, 1:月, 2:火, 3:水, 4:木, 5:金, 6:土)

と設定できます。指定しない場合は `*` を指定します。
いくつか例を挙げます。

```coffeescript
//毎秒実行する、うるさいので非推奨
new cronJob('* * * * * *', () ->

//30秒毎に実行する、メンバーに不快感を与えますので非推奨
new cronJob('*/30 * * * * *', () ->

//毎分実行する
new cronJob('00 * * * * *', () ->

//月曜朝９時に実行する
new cronJob('00 00 09 * * 1', () ->

//平日の9時に実行する
new cronJob('00 00 09 * * 1-5', () ->
```


これを使って、reminder.coffeeを以下のように毎日19時に実行するようにします。

```coffeescript
cronJob = require('cron').CronJob
weekDayJP = ['日', '月', '火', '水', '木', '金', '土']

module.exports = (robot) ->
  postTomorrowShift = new cronJob('00 00 19 * * 0-6', () ->
    envelope = room: "#general"
    nObj = new Date
    month = nObj.getMonth() + 1
    date = nObj.getDate() + 1
    day = weekDayJP[nObj.getDay()]
    tomorrow = "#{month}月#{date}日(#{day})"
    request = robot.http('http://hubot-api-app.herokuapp.com/api/shifts/tomorrow')
                .get()
    request (err, res, body) ->
      data = JSON.parse body
      message = ''
      for value in data
        message += "【#{value.start_time.toString()}時】\n"
        message += "責任者："
        for v2 in value.manager
          message += "#{v2.full_name}  "
        message += "\n"
        for v1 in value.in_charge
          message += "#{v1.full_name}  "
        message += "\n"
      robot.send envelope, "#{tomorrow}のシフトは、、、、、\n#{message}"

postTomorrowShift.start()
```

robotがこちらに反応するのではなく、自らが行うので、robotオブジェクトの**httpメソッド**と**sendメソッド**に変更しました。結果は同じです。

これで以上です。

# 終わり

駆け足で作ってしまったので、リファクタリングや修正などありましたら是非コメントください。＿ø(●ʘ╻ʘ●)
また、今回はtomorrowだけでしたが、これをちょっといじれば結構応用効くと思いますので、是非日曜プログラミングでチームのhubotを作ってみてください。