43
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

TECH::CAMPAdvent Calendar 2015

Day 12

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

Last updated at Posted at 2015-12-12

まだ、導入編をやっていない方は【導入編】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としたかったら とするだけです。
以下表にまとめました。特定のユーザーにメンションを飛ばす場合は、単に文字列に @id とするだけでメンションがつきます。

入力 出力
@channel
@here
@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を作ってみてください。

43
51
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43
51

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?