Edited at

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

More than 3 years have passed since last update.

まだ、導入編をやっていない方は【導入編】hubotでチームだけのお手軽オリジナルslackリマインダーを作るをやってください。


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


さて今日は、hubot企画の実装編です。

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


手順


  • 導入編で作成したhubotを持参してください_ø(●ʘ╻ʘ●)

  • railsで簡単なapiを作ります(※すでにチームのプロジェクトがある方はそれを使ったほうが楽です)


    • チームのプロジェクトを持っている方はrailsのslack-apiというgemを使って、チームのメンバーのidやe-mailを取得すると、連携やメンションが簡単にできます。



  • hubotからapiを叩いてデータを取得します

  • 定時にそのデータを元に投稿します


事前準備

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


データベース作成

# データベースは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


モデルの関連付け


user.rb

class User < ActiveRecord::Base

has_many :shift_users
has_many :shifts, through: :shift_users
end


shift.rb

class Shift < ActiveRecord::Base

has_many :users, through: :shift_users
has_many :shift_users
end


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に以下を追記します。


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:責任者


コントローラーを作成


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

$ rails g controller api/shifts/tomorrow


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


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


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をお使いの方は以下のサイトを参考に登録してください。


herokuにアップする

$ heroku login

# 登録したe-mailとパスワードを入力
$ git init
$ git add .
$ git commit -m 'initial commit'
$ heroku create hubot-api-app

左が作成されたアプリケーションのURL、右がgitのURLです。

スクリーンショット_2015-12-11_14_29_21.png


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

$ heroku addons:add cleardb

$ heroku config | grep CLEARDB_DATABASE_URL

出力: CLEARDB_DATABASE_URL: mysql://×××××××××××××××××××××

mysql2gemを使っているのでデータベースのURLもmysqlからmysql2に変更します。

$ heroku config:set DATABASE_URL=mysql2://×××××××××××××××××××××

$ heroku config
DATABASE_URL: mysql2://×××××××××××××××××××××

これは、以下のようになっています。

mysql2://ユーザー名:パスワード@ホスト名/データベース?reconnect=true

sequelProでデータベースにアクセスします。お気に入りに追加ボタンを押しておくと、次からアクセスするのにサイド入力する必要がなくなります。

スクリーンショット_2015-12-11_15_02_07.png

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


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

$ heroku run rake db:migrate

$ heroku run rake db:seed

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


apiをhubotに叩かせる

やっとこれから本題に入ります。

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


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

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


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

関数
仕様
結果

respond
呼ばれたら反応する

hear
チャンネル上のメッセージに反応する

enter
チャンネルに入室した際に反応する

leave
チャンネルを退出した際に反応する

topic
トピックが立てられたら反応する

send
メッセージを送信する

reply
発言者に対して返信する

random
引数の配列からランダムに取り出す

プロパティ
仕様
結果

match
respondやhearで正規表現でマッチしたものを取り出すときに使います

message
送られてきたメッセージに関する情報を取得できます

使用例集


respond

module.exports = (robot) ->

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


hear

module.exports = (robot) ->

robot.hear /休憩/i, (msg) ->
msg.send "お疲れ様!”


reply

module.exports = (robot) ->

robot.hear /誰か(.+)/i, (msg) ->
msg.reply "#{msg.match[1]}?どした?"


enter_and_leave

module.exports = (robot) ->

robot.enter (msg) ->
msg.send "いらっしゃい!#{msg.message.user.name}さん!"
robot.leave (msg) ->
msg.send "#{msg.message.user.name}さん ばいばい、、、"



topic

module.exports = (robot) ->

robot.topic (msg) ->
msg.reply "トピックが立ったぞい!"


enter

module.exports = (robot) ->

robot.enter (msg) ->
msg.send "いらっしゃい!#{msg.message.user.name}さん!"


random

module.exorts = (robot) ->

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


match

module.exports = (robot) ->

robot.respond /(.+)駅/, (msg) ->
msg.send "#{msg.match[0]}"
msg.send "#{msg.match[1]}"


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を叩く

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


reminder.coffee

module.exports = (robot) ->

robot.respond /hi/i, (msg) ->
msg.send 'Hi'

するともちろん以下のようになります。

Hubot> Hubot hi

Hubot> Hi

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


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メソッドを使えば、パラメーターも送ることができます。使用例は以下の通りです。


queryメソッド使用例

module.exports = (robot) ->

robot.respond /hi (.*)/i, (msg) ->
keyword = msg.match[1]
msg.http('https://xxxx/')
.query(q: keyword)
.get()

これで、変数requestにapiを叩いた結果のデータを格納することができました。ではこれからレスポンスとして返された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":"吉沢紗愛"}]
}
]


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のディレクトリで以下のコマンドを実行する。

$ npm install --save cron

$ npm install --save time

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


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の時間を設定します。全部で6つ指定します。左から、


  • 秒: 0 ~ 59

  • 分: 0 ~ 59

  • 時: 0 ~ 23

  • 日付: 1 ~ 31

  • 月: 0 ~ 11

  • 曜日: 0 ~ 6(0:日, 1:月, 2:火, 3:水, 4:木, 5:金, 6:土)

と設定できます。指定しない場合は * を指定します。

いくつか例を挙げます。

//毎秒実行する、うるさいので非推奨

new cronJob('* * * * * *', () ->

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

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

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

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

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

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を作ってみてください。