Slackで動く当番ボットをLuaで作る(2)


第一回目の記事はこちら

kiten.co を使うと、比較的簡単なLuaのコードでHubot風にSlackボットを作成することができます。

この記事ではタイマーで動作する通知ボットを題材にして、kiten上でどのようなコードを書くのかご紹介します。

基本的なプログラミングの経験がある人を対象読者と想定しています。
Luaの知識があるとより理解しやすいです。

第二回目の本記事では実際に当番決めをするところを作成し、それを発言するタイマーの設定をして最終版を作るまでを解説します。

kitenで提供されている機能のドキュメントについては https://docs.kiten.co/ から「リファレンス(Lua)」のメニューをたどってご参照ください。

コード

当番メッセージ生成

さて、前回休日判定ができるようになったので、当番を決めます。

日付を受け取って、「誰が当番か」というメッセージを作る関数を作成します。

朝当番は、予め列挙しておいた担当メンバーが一週間おきにローテーションするようにします。
2016年12月25日(日曜日)を基準日として、当番を判断したい日が基準日から見て何週間後かを計算します。

touban.lua
function message(target, now)
  local startDate = os.time({year=2016, month=12, day=25})
  local names = {"矢口", "赤坂", "パタースン", "市川"}

  local dateDiff = os.difftime(os.time(now), startDate) / (24 * 60 * 60)
  local weekDiff = math.floor(dateDiff / 7)
  return string.format(
    "%sの10時出社当番は%sです。",
    target,
    names[(weekDiff % #names) + 1]
  )
end

ついでに、今週分と来週分のメッセージを作る関数も作っておきます。

touban.lua
function messageForThisWeek(now)
  return message("今週", now)
end

function messageForNextWeek(now)
  return message("来週", os.date("*t", os.time(now) + 7 * 24 * 60 * 60))
end

キーワードに反応する部分

robot:respond()を使って、「今週」「来週」「当番」という文字列を含むメッセージに反応するようにしてみます。

「今週」と「来週」は1行で当番を発言させてみます。「当番」は今日の当番、今週の当番、来週の当番を続けて発言させることにします。

touban.lua
robot:respond("今週", function(res) res:send(messageForThisWeek(os.date("*t"))) end)
robot:respond("来週", function(res) res:send(messageForNextWeek(os.date("*t"))) end)
robot:respond("当番", function(res)
  local d = os.date("*t")
  res:send(message(os.date("%Y/%m/%d(%a)", os.time(d)), d))
  res:send(message("今週", d))
  res:send(message("来週", os.date("*t", os.time(d) + 7 * 24 * 60 * 60)))
end)

タイマーで起動する部分

newCornJobというメソッドで、タイマーで起動するジョブを登録します。

cronの時刻指定のところは、一般のcronの記法に追加して、秒を書くカラム(1項目目)があります。またday of month(4項目目)とday of week(6項目目)の両方を指定することはできないので、どちらかに?を書いて未指定にする必要があります。

チャネル名はbotに発言させたいチャネルを設定します。

週の初めの朝と週の終わりの夕方に発言させたいので、ジョブはこのようなつくりにします。

  • 毎朝9:30のタスクの中では、週の初めかどうかを確認し、初めであればその週の当番を発言する
  • 毎夕18:30のタスクの中では、週の終わりかどうかを確認し、終わりであれば次週の当番を発言する
touban.lua
robot.newCronJob("0 30 9 ? * 2-6", function(res)
  local d = os.date("*t")
  if isWeekStart(d) then
    robot.messageRoom("channel", messageForThisWeek(d))
  end
end, "Asia/Tokyo")

robot.newCronJob("0 30 18 ? * 2-6", function(res)
  local d = os.date("*t")
  if isWeekEnd(d) then
    robot.messageRoom("channel", messageForNextWeek(d))
  end
end, "Asia/Tokyo")

全体像

これですべての部品がそろいました。

まとめたコードはこのようになります。

touban.lua
function isHoliday(now)
  local holidays = {
    "20170109",
    "20170320",
    "20170503",
    "20170504",
    "20170505",
    "20170717",
    "20170811",
    "20170918",
    "20171009",
    "20171103",
    "20171123"
  }
  local additionalHolidays = {
    "20170102",
    "20170103"
  }

  local today = os.date("%Y%m%d", os.time(now))

  if now.wday == 1 or now.wday == 7 then
    return true
  end

  for _, h in ipairs(holidays) do
    if today == h then
      return true
    end
  end

  for _, h in ipairs(additionalHolidays) do
    if today == h then
      return true
    end
  end

  return false
end

function isWeekStart(now)
  for i = 1, (now.wday - 1) do
    local t = os.date("*t", os.time(now) - (now.wday - i) * 24 * 60 * 60)
    if not isHoliday(t) then
      return false;
    end
  end

  return not isHoliday(now)
end

function isWeekEnd(now)
  for i = (now.wday + 1), 7 do
    local t = os.date("*t", os.time(now) + (i - now.wday) * 24 * 60 * 60)
    if not isHoliday(t) then
      return false;
    end
  end

  return not isHoliday(now)
end


function message(target, now)
  local startDate = os.time({year=2016, month=12, day=25})
  local names =  {"矢口", "赤坂", "パタースン", "市川"}

  local dateDiff = os.difftime(os.time(now), startDate) / (24 * 60 * 60)
  local weekDiff = math.floor(dateDiff / 7)
  return string.format(
    "%sの10時出社当番は%sです。",
    target,
    names[(weekDiff % #names) + 1]
  )
end

function messageForThisWeek(now)
  return message("今週", now)
end

function messageForNextWeek(now)
  return message("来週", os.date("*t", os.time(now) + 7 * 24 * 60 * 60))
end

robot:respond("今週", function(res) res:send(messageForThisWeek(os.date("*t"))) end)
robot:respond("来週", function(res) res:send(messageForNextWeek(os.date("*t"))) end)
robot:respond("当番", function(res)
  local d = os.date("*t")
  res:send(message(os.date("%Y/%m/%d(%a)", os.time(d)), d))
  res:send(message("今週", d))
  res:send(message("来週", os.date("*t", os.time(d) + 7 * 24 * 60 * 60)))
end)

robot:respond("%d%d%d%d%d%d%d%d", function(res)
  local y, m, d = res.message.text:match("(%d%d%d%d)(%d%d)(%d%d)")
  d = {year = tonumber(y), month = tonumber(m), day = tonumber(d)}
  d = os.date("*t", os.time(d))
  local h = isHoliday(d)
  local s = isWeekStart(d)
  local e = isWeekEnd(d)
  res:send(string.format("isHoliday: %s,  isWeekStart: %s,  isWeekEnd: %s", tostring(h), tostring(s), tostring(e)))
end)

robot.newCronJob("0 30 9 ? * 2-6", function(res)
  local d = os.date("*t")
  if isWeekStart(d) then
    robot.messageRoom("channel", messageForThisWeek(d))
  end
end, "Asia/Tokyo")

robot.newCronJob("0 30 18 ? * 2-6", function(res)
  local d = os.date("*t")
  if isWeekEnd(d) then
    robot.messageRoom("channel", messageForNextWeek(d))
  end
end, "Asia/Tokyo")