LoginSignup
1
2

More than 5 years have passed since last update.

Slackのオンラインユーザーにレビュー依頼するBot

Last updated at Posted at 2016-11-15

レビュワーを選ぶSlack用のBotをつくりました。
コピペでも多分動きますが、こちらが全ソースです
Github Oden

Oden

  • Slack APIを使います
  • レビューチャネルのオンラインのユーザーを、ランダムに2人選びます
  • データをRedis & JSONで保持します
    • これらのデータはSlack上のメッセージで操作できます
      1. User
        • オンラインユーザーリスト
      2. Skip
        • その日はレビューをスキップする人リスト
      3. Never
        • これから先レビューしない人リスト

コマンド

Odenにメンションを飛ばすことでコマンドが実行できます

Cmd Description Detail
pr レビュワーを選ぶ
users ユーザーを表示 1時間以内にオンラインskipsでもneversでもないユーザー
user+(.*) ユーザーを追加する カンマや空白区切りで複数可
user-(.*) ユーザーをスキップする その日だけレビューをスキップする
カンマや空白区切りで複数可
user-meで自分をスキップ
skips スキップされたユーザーを表示
nevers 削除されたユーザーを表示
never+(.*) ユーザーを削除 永遠にレビューをスキップする
カンマや空白区切りで複数可
user-meで自分を削除
never-(.*) ユーザーを復活 レビューを再開する
カンマや空白区切りで複数可
config botの設定を表示する

環境変数

Odenが使う環境変数

Config Variable Default value Description
CHANNEL "random" レビュワーのいるチャネル名
SELECT_NUM 2 レビューに必要な人数
FETCH_CRON 10分毎 ユーザーのオンラインをチェックする間隔
SKIP_CRON 毎日0時 スキップされたユーザーを復活させる間隔
CLEAR_CRON 1時間毎 オフラインユーザーをユーザーから外す間隔
JSON_PATH ./db.json 永続化用JSONファイルのパス

デフォルト値がはいってるので、そのままでも動きます

Hubotで使う環境変数

Config Variable Description
HUBOT_SLACK_BOTNAME Botの名前
HUBOT_SLACK_TEAM Slackのチーム名
HUBOT_HEROKU_KEEPALIVE_URL heroku open
HUBOT_SLACK_TOKEN Apps & Integrations (必須)
TZ Asia/Tokyo

Odenをローカルで動かす

% bin/hubot --adapter slack

You'll see some start up output and a prompt:

[Tue Nov 15 2016 11:48:00 GMT+0900 (JST)] INFO Logged in as post_twitter of ほげほげチーム
[Tue Nov 15 2016 11:48:01 GMT+0900 (JST)] INFO Slack client now connected
[Tue Nov 15 2016 11:48:01 GMT+0900 (JST)] INFO load
[Tue Nov 15 2016 11:48:01 GMT+0900 (JST)] INFO save
[Tue Nov 15 2016 11:48:01 GMT+0900 (JST)] INFO 
[ 'レビュワー: `2人`',
  'チャンネル: `cross-review`',
  'オンラインユーザー追加: `*/10 *   * * *`',
  'オフラインユーザー削除: `0    */1 * * *`',
  'レビュースキップの取消: `0    0   * * *`' ]
[Tue Nov 15 2016 11:48:01 GMT+0900 (JST)] INFO List:
[Tue Nov 15 2016 11:48:01 GMT+0900 (JST)] INFO hubot-redis-brain: Using default redis on localhost:6379

実装

oden.coffee
# Description
#  レビュー依頼Bot
#

request = require('request')
cronJob = require('cron').CronJob
fs      = require('fs')

select_num   = process.env.SELECT_NUM  || 2
channel_name = process.env.CHANNEL     || 'random'
super_user   = process.env.SUPER_USER  || 'admin'
fetch_cron   = process.env.FETCH_CRON  || '*/10 *   * * *'
clear_cron   = process.env.CLEAR_CRON  || '0    */1 * * *'
skip_cron    = process.env.SKIP_CRON   || '0    0   * * *'
path         = process.env.JSON_PATH   || './db.json'
token        = process.env.HUBOT_SLACK_TOKEN

module.exports = (robot) ->
  robot.brain.setAutoSave false

  load = ->
    robot.logger.info "load"
    data = JSON.parse fs.readFileSync path, encoding: 'utf-8'
    robot.brain.mergeData data
    robot.brain.setAutoSave true

  save = (data) ->
    robot.logger.info "save"
    fs.writeFileSync path, JSON.stringify data

  robot.brain.on 'loaded', save
  load()

  robot.logger.info config()
  fetch_users(robot)

  # 設定を表示する
  robot.respond /config/, (msg) =>
    msg.send "\n#{config().join('\n')}"

  # レビュワーを選ぶ
  robot.respond /pr/, (msg) =>
    users = get(robot, 'users')
    never_users = get(robot, 'never_users')
    skip_users = get(robot, 'skip_users')
    my_name = msg.message.user.name
    for name in [super_user, my_name] + never_users + skip_users
      skip_idx = users.indexOf(name)
      users.splice(skip_idx, 1) if skip_idx != -1
    if users.length < select_num
      msg.send("アサインできるレビュワーが #{users.length} 名です\n")
      return
    users = random_fetch(users, select_num)
    msg.send("@#{users.join(' @')}")

  # ユーザーをスキップ
  robot.respond /user-(.*)/, (msg) =>
    user = msg.match[1]
    user = msg.message.user.name if /me/.test(user)
    add(robot, 'skip_users', user)
    rm(robot, 'users', user)

  # ユーザーを追加
  robot.respond /user\+(.*)/, (msg) =>
    user = msg.match[1]
    user = msg.message.user.name if /me/.test(user)
    add(robot, 'users', user)
    rm(robot, 'skip_users', user)

  # ユーザーを表示
  robot.respond /users/, (msg) =>
    users = get(robot, 'users')
    msg.send("レビュー可能なユーザー: \n#{users.join(', ')}")

  # スキップリストを表示
  robot.respond /skips/, (msg) =>
    skip_users = get(robot, 'skip_users')
    msg.send("スキップしたユーザー: \n#{skip_users.join(', ')}")

  # ユーザーを復活
  robot.respond /never-(.*)/, (msg) =>
    user = msg.match[1]
    user = msg.message.user.name if /me/.test(user)
    add(robot, 'users', user)
    rm(robot, 'never_users', user)

  # ユーザーを削除
  robot.respond /never\+(.*)/, (msg) =>
    user = msg.match[1]
    user = msg.message.user.name if /me/.test(user)
    add(robot, 'never_users', user)
    rm(robot, 'users', user)

  # 削除されたユーザーを表示
  robot.respond /nevers/, (msg) =>
    never_users = get(robot, 'never_users')
    msg.send("削除されたユーザー: \n#{never_users.join(', ')}")

  # スキップリストをリセット
  new cronJob(skip_cron, () ->
    robot.brain.set('skip_users', [])
    robot.logger.info "skip"
  ).start()

  # ユーザーリストをリセット
  new cronJob(clear_cron, () ->
    robot.brain.set('users', [])
    robot.logger.info "clear"
  ).start()

  # ユーザーリストを更新
  new cronJob(fetch_cron, () ->
    fetch_users(robot)
    robot.logger.info "fetch"
  ).start()

  # 生存確認
  robot.router.get '/', (req, res) ->
    res.send 'pong'

##################################################

# 設定を配列で返す
config = () ->
  ["レビュワー: `#{select_num}人`",
   "チャンネル: `#{channel_name}`",
   "オンラインユーザー追加: `#{fetch_cron}`",
   "オフラインユーザー削除: `#{clear_cron}`",
   "レビュースキップの取消: `#{skip_cron}`",]

# ユーザーを更新する
fetch_users = (robot) ->
  users = get(robot, 'users')
  robot.logger.info "List: #{users.join(', ')}"
  groups_list = "https://slack.com/api/groups.list?token=#{token}&pretty=1"
  request.get groups_list, (error, response, body) =>
    data = JSON.parse(body)
    target_channel = null
    for group in data.groups
      target_channel = group if group.name == channel_name
    if target_channel
      user_ids = target_channel.members.sort -> Math.random()
      for user_id in user_ids
        check_online(robot, user_id)
    else
      channels_list = "https://slack.com/api/channels.list?token=#{token}&pretty=1"
      request.get channels_list, (error, response, body) =>
        data = JSON.parse(body)
        for channel in data.channels
          target_channel = channel if channel.name == channel_name
        user_ids = target_channel.members.sort -> Math.random()
        for user_id in user_ids
            check_online(robot, user_id)

# ユーザーを追加する
check_online = (robot, user_id) ->
  do (user_id) ->
    users_getPresence = "https://slack.com/api/users.getPresence?token=#{token}&user=#{user_id}&pretty=1"
    request.get users_getPresence, (error, response, body) =>
      data = JSON.parse(body)
      if (data.presence == "active")
        users_info = "https://slack.com/api/users.info?token=#{token}&user=#{user_id}&pretty=1"
        request.get users_info, (error, response, body) =>
          data = JSON.parse(body)
          return if data.user.is_bot
          user_name = data.user.name
          never_users = get(robot, 'never_users')
          skip_users = get(robot, 'skip_users')
          return if (never_users + skip_users).indexOf(user_name) != -1
          users = get(robot, 'users')
          robot.logger.info "Add:  #{user_name}" if users.indexOf(user_name) == -1
          users.push(user_name)
          robot.brain.set('users', uniq(users))

get = (robot, key) ->
    return (robot.brain.get(key) || []).slice(0)

rm = (robot, key, value) ->
    values = value.split(/[ ・\s,、@]+/)
    arr = get(robot, key)
    for value in values
      skip_idx = arr.indexOf(value)
      arr.splice(skip_idx, 1) if skip_idx != -1
    robot.brain.set(key, arr)
    console.log "#{key} #{robot.brain.get(key, arr)}"
    return arr

add = (robot, key, value) ->
    values = value.split(/[ ・\s,、@]+/)
    arr = get(robot, key)
    arr = arr.concat(values)
    arr = uniq(arr)
    arr.splice arr.indexOf(''), 1 if arr.indexOf('') != -1
    robot.brain.set(key, arr)
    console.log "#{key} #{robot.brain.get(key, arr)}"
    return arr

uniq = (ar) ->
  if ar.length == 0
    return []
  res = {}
  res[ar[key]] = ar[key] for key in [0..ar.length-1]
  value for key, value of res

random_fetch = (array, num) ->
  a = array
  t = []
  r = []
  l = a.length
  n = if num < l then num else l
  while n-- > 0
    i = Math.random() * l | 0
    r[n] = t[i] or a[i]
    --l
    t[i] = t[l] or a[l]
  r
1
2
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
1
2