ランダムで”いい感じ”にグループ分けしてSlack通知するbotをRubyで開発してみた

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110571/d735a4d1-0e93-8961-1f76-fdf6ad3c801c.png)
# はじめに
こちらは[Slack Advent Calendar 2019](https://qiita.com/advent-calendar/2019/slack)の21日目の記事となります。

今回は、仕事を通じて作成したグループ分けのbotについて詳しく書いていこうと思います。

当初は「**悪用厳禁！ Slack活用における情報収集 黒魔術**」というタイトルで、Slackを導入している企業や組織での情報収集の小ネタについて書こうと思いましたが、転職して日が浅いのでブラックなジョークは止めておこう（※違法ではないです）という保守的思考が働いたのと、どうせならエンジニアらしいことを優先して書きたいと考えたためお蔵入りとなりました。

そちらの方も、今後機会があればコラム的な感じで書いていきたいと思います（リアクション頂ければ書くためのモチベーションが高まります）。

# 背景・目的
## 作るに至った理由
元々は前職での”**社内のシャッフルランチのグループ分け**”を補助するために、個人的に開発したものです。

[グループ分けを簡単に行うことができるWebサービス](http://www.daiichi-g.co.jp/osusume/forfun/10_group/10.html)はもちろん存在していましたが、ポチポチする時間が長く工数が掛かったり、メンバーの抜け漏れが多いという課題がありました。

そこで、コマンド一発でグループ分けを実行するスクリプトを一度書いてしまって、業務負荷を減らせないかと考えたことが背景にあります。

## ”いい感じ”とは？
自前で実装した理由の一つに、「**剰余をグループにしない**」というものがありました。

例を挙げて少し詳しく説明します。全員で27人のチームがあり、ランチのためにチームを5つのグループに分けたいとしましょう。この場合「6人グループを2つと、5人グループを5つ」がいわゆる”いい感じ”に当たります。

しかし、様々なグループ分けのコードをググって探してみると、「6人グループが4つと、3人グループが1つ」というような、余りで構成されたグループが出来上がってしまうものが多くありました。

これでは各グループで均等にメンバーが割り振られず残念なランチになってしまうので、そのような”余りものグループを作らない”という実装を行いました。

# ソースコード
[<img width="620" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110571/4db08eae-e8d2-edda-3e06-d906c4924ceb.png">](https://github.com/f-teruhisa/quattro-bot)
>※社内ランチの社内呼称が”クアトロランチ”だったため、リポジトリ名もそれにあやかる形になっております

# 実装
## 依存関係・バージョン
- Ruby：2.5.5（そこまで気にしなくて大丈夫）
- [slack-ruby-client](https://github.com/slack-ruby/slack-ruby-client)
- [dotenv](https://github.com/bkeepers/dotenv)
- [csv](https://github.com/ruby/csv)

## スクリプト
以下のスクリプトをコマンドラインから実行することで、グループ分けを行います。

シャッフルをしている部分は、[27行目](https://github.com/f-teruhisa/quattro-bot/blob/master/script/slack.rb#L27)の`members.shuffle`メソッドの部分です。あと、個人的な嗜好によりメソッドを細かい責務で分けておりますので、読みづらい方も居るかと思います。

```script/slack.rb
require 'slack-ruby-client'
require 'dotenv'
require 'csv'

Dotenv.load

today = Date.today.strftime("%Y/%m/%d")
groups, members = [], []
numbers_of_group = ENV['NUMBERS_OF_GROUP'].to_i
comment = ENV['COMMENT'].to_s
channel = ENV['CHANNEL'].to_s

def setup_slack
  set_slack_api_token
  client = Slack::Web::Client.new
  return client
end

def set_slack_api_token
  Slack.configure do |conf|
    conf.token = ENV['SLACK_API_TOKEN']
  end
end

def load_members(members)
  read_csv(members)
  members.shuffle
end

def read_csv(members)
  CSV.foreach('data/members.csv') do |member|
    members << member
  end
end

def create_groups(numbers_of_group, groups, members)
  numbers_of_group.times { create_group(groups) }
  generate_csv_file(members, numbers_of_group, groups)
end

def create_group(groups)
  empty_group = Array.new
  groups << empty_group
end

def generate_csv_file(members, numbers_of_group, groups)
  grouped_members_csv = CSV.generate do |csv|
    split_members(members, numbers_of_group, groups, csv)
  end
  save_csv_file(grouped_members_csv)
end

def split_members(members, numbers_of_group, groups, csv)
  members.each_with_index do |member, i|
    number = i % numbers_of_group
    group = groups[number]
    assign_member_into_groups(group, number+1, member[0], csv)
  end
  push_groups_into_csv_file(csv, groups)
end

def assign_member_into_groups(group, number, member, csv)
  group << number unless group.include?(number)
  group << member
end

def push_groups_into_csv_file(csv, groups)
  groups.each do |group|
    csv << group
  end
  return csv
end

def save_csv_file(grouped_members_csv)
  File.open('data/grouped_members.csv', 'w') do |file|
    file.write(grouped_members_csv)
  end
end

def send_slack_api_with_csv(client, today, comment, channel)
  client.files_upload(
    channels: channel,
    as_user: true,
    file: Faraday::UploadIO.new('data/grouped_members.csv', 'text/csv'),
    title: "#{today} quattro groups",
    filename: 'grouped_members.csv',
    initial_comment: comment
  )
end

client = setup_slack
members = load_members(members)
create_groups(numbers_of_group, groups, members)
send_slack_api_with_csv(client, today, comment, channel)
```
コード：https://github.com/f-teruhisa/quattro-bot/blob/master/script/slack.rb

## .envファイル
以下の `.env` ファイルに設定情報を追記し、ルートディレクトリに格納しておきます。

```.env
SLACK_API_TOKEN= 
NUMBERS_OF_GROUP=
CHANNEL=
COMMENT=
```

設定する情報はそれぞれ以下の通りです。
envファイルのため、文字列をクォートで囲ったりする必要はありません。

```
SLACK_API_TOKEN=後述するSlackのAppを作成し取得するAPIトークン
NUMBERS_OF_GROUP=グループの数
CHANNEL=Slackのチャンネル（#は省略）
COMMENT=Slack通知する際に加えたい文章
```

ちなみに、`COMMENT=Slack通知する際に加えたい文章`に関しては、改行を含む場合のみシングルクォーテーションで囲むことで、改行を含んだメッセージを表現することができます。

## csv
このスクリプトを実行するにあたり、以下の2つのcsvファイルを活用します。

### members.csv
`data`フォルダに、元データとなるグループ分けする対象の`member.csv` ファイルを格納します。以下の[サンプルデータ](https://github.com/f-teruhisa/quattro-bot/blob/master/data/members.csv)の通り、１つのデータを1行1カラムで格納してください。

```members(sample).csv
Fukumoto
Tanaka
Yamamoto
Inoue
Aida
Ueda
Suzuki
Sato
Nakamura
Fujioka
Ito
Hara
Ohara
Yamasaki
Kawai
Kondo
```
>※元が社内ランチ用のプログラムなため`member`という名前になっています

### grouped_members.csv
こちらは、スクリプトを実行した後に自動生成されるcsvファイルです（よって最初は存在しないファイルです）。

元データをグループ分けした結果が、`grouped_members.csv`として吐き出されます。Slackには**このcsvファイルをメッセージに添付してPOSTする**という形を取っております。以下のサンプルデータは、先程の`member.csv`を5グループに分ける設定をし実行してみた結果です。

```grouped_member(sample).csv
1,Kawai,Yamamoto,Nakamura,Inoue
2,Tanaka,Yamasaki,Ueda
3,Fukumoto,Hara,Ohara
4,Aida,Fujioka,Ito
5,Sato,Suzuki,Kondo
```

便宜上”グループ○○(数字)”という指定ができ**コミュニケーションが円滑に行われるよう**に、各グループに番号を振っています。また、番号を振ることで、番号を先頭とした**序列**が各グループ内で暗黙的に作られるように設計しました。

>※参考：[行動経済学「ナッジ」は政策を変えるのか？](https://news.yahoo.co.jp/byline/takeuchikan/20190612-00129569/)

# 実行手順
基本的にはGithubに乗せています...が、英語なのとSlackの操作までは記載しておらず、若干不親切なのでこちらで詳細に記載します。

「**SLACK_API_TOKENの取得**」と「**ワークスペースへのアプリケーション導入**」の２つがゴールとなります。

## Custom IntegrationsからSlack botを作成
Slackで開発できるbotにはいくつか種類があり、設定が複雑なので以下より画面を追って説明していきます。

[<img width="626" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110571/3f34bf2e-323a-34b2-b05f-caac77c4ec54.png">](https://qiita.com/namutaka/items/233a83100c94af033575)

今回は「Custom Integrations」からbotを開発していきます。
ちなみに、以下のような画面です。

<img width="1679" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110571/44475500-6e7a-6255-1fe3-182de4da3be2.png">

上記の「Bots」をクリックすることで、Slackに登録するbotを設定していくことが可能です。

ちなみに、Slackのワークスペースが分かる場合は、以下のURLから直接遷移することができます。

`https://{Slackのワークスペース名}.slack.com/apps/manage/custom-integrations`

## SLACK_API_TOKENを取得
「Bots」をクリックすると、以下のような画面が出てきます。
「新規作成」と「編集」のどちらでも、SLACK_API_TOKENは取得できます。

<img width="1680" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110571/c014ab58-76db-9663-fe25-29d3461a61f3.png">

「新規作成」の場合はbotの名前を入力した後に、「編集」の場合は直接以下の画面に遷移するので、この画面でSLACK_API_TOKENを取得できます。後ほどenvファイルに書き込むので、コピーしておきましょう。

<img width="1680" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110571/858426d4-2616-67a3-4f36-c1236b18c5ee.png">

## リポジトリをcloneする
冒頭に紹介した[リポジトリ](https://github.com/f-teruhisa/quattro-bot)を、ローカル環境の任意のフォルダにcloneしてください。

```terminal
$ git clone https://github.com/f-teruhisa/quattro-bot.git
```

[<img width="620" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110571/4db08eae-e8d2-edda-3e06-d906c4924ceb.png">](https://github.com/f-teruhisa/quattro-bot)

## .envファイルに設定情報を記載
前半に紹介した`.env`ファイルにSLACK_API_TOKEN含む各設定情報を追記し保存してください。グループの数やメッセージの内容は任意のもので構いません。

## csvファイルに元データを格納
`data/members.csv`に、前半に紹介した通り、グループ分けしたいデータを一列に格納し保存してください。

## ワークスペースへのアプリケーション導入
作成したbotからSlackの任意のチャンネルに通知されるように、アプリケーションを導入しましょう。

通知を飛ばしたいSlackのチャンネルに入り画面の左上、チャンネル名をクリックするとメニューバーが現れます。
このメニューから「Add an app」をクリックし、作成したbotを導入します。

<img width="236" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110571/19fec300-2c4a-f98e-6665-a1fdfb5471b2.png">

「Add an app」をクリックすると以下のような画面が表示されるので、先程設定したbotの名前で検索すると表示されるはずです。選択して「Add」を選びましょう。

<img width="1616" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110571/02b30a6c-04a7-a589-3ad5-00e6e1f2c54e.png">

これで、Slack側でもbotから通知を受けるための準備が整いました。

## コマンド実行
さて、ここまでで準備は終わりです。
以下のコマンドを実行して、グループ分けとSlack通知を行いましょう。

```
$ ruby script/slack.rb
```

ちなみに、実行には依存gemが必要ですがGemfileにまとめておりますので、そちらを参考にしてgemのインストールも合わせて行ってください。以下のコマンドを一度に実行すれば、インストールが完了するかと思います。

```terminal
$ gem install dotenv\
      csv\
      slack-ruby-client 
```

## Slack通知
設定した情報が正しければ、以下のようにSlackに通知が飛んでくると思います。
一度設定してしまえば、あとはコマンドを実行するだけなので、グループ分けに悩まずに済みます。

<img width="784" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/110571/529b49ef-c09a-86c6-24cf-c99e44a40a1e.png">

> ※実際に、[仕事でアップデートを担当するgem](https://qiita.com/Terryy/items/a4bfafe51b455b6c5ad2)を分けた時に使った通知メッセージ

# 今後考えていること
それなりに使う機会に恵まれたbotですが、以下のような課題感もあるので、今後~~ヒマがあれば~~徐々に対処していきたいです。

### 定期運用に向いていない
今回のbotはインスタントなグループ分けには重宝するのですが、部署や名前などの属性は考慮していないため、普通に**ガンガン衝突します**。そのため、（偶然ですが）同じヒトやモノと何度も同じグループになることが多々あります。

衝突を防ぐためには、属性値の考慮や過去の結果を格納するDBを用意するなどの工夫が考えられます。

また、コマンドを叩かないとグループ分けやSlackへの通知が行われないため、実行者が忘れたり寝坊すると死にます。

これは、後にも説明するGoogle App Script(GAS)への移行やLamda化などで対応が可能であると考えておりmす

### GUIで実行できない
工数を考慮しGUIは実装しなかったのですが、ターミナル操作を行うため**非エンジニアのメンバーが利用するハードルは高い**です（一応セットアップと実行するコマンドを教えれば事足りますが）。

この辺は軽めのUIを作って、herokuにデプロイすることでブラウザでの操作を行う拡張ができるでしょう。

### csvの管理工数
社員情報など、マスタ情報がスプレッドシートやエクセルで管理され、氏名が縦に並んでいる（≒まんまコピペして`member.csv`に格納できる）ことを前提にcsvからデータを取り出す処理を書いていますが、「新入社員など新しいデータが必要な際に追加を忘れ、ナチュラルにハブってしまう」可能性があります。

これは、「（前職で）社内にRubyエンジニアが多くメンテナンスしやすいだろう」という安易な理由でRubyを選定したことがやや裏目に出てしまいました。

マスタデータをスプレッドシートで管理している会社も多い（前職はそうでした）ので、**GAS**で実装するほうがシンプルに保てたと感じます（Git管理しにくい、スプレッドシートに依存するというデメリットはありますが）。また、GASではサーバレスに定期実行を行うことも可能なので、先に上げた”定期運用に向いていない”という課題にも対処できます。

# さいごに
最後までお付き合い頂きありがとうございました。

筆者はこのbotスクリプトの開発が、人生初のいわゆる”**コーポレート・エンジニアリング**”だったのですが、自分の書いたスクリプトを何度も使ってもらい、実際に価値を生んでいる瞬間を何度も見ることができるのは、エンジニアとしても成功体験であったと振り返っています。

これからも「イケてないなあ...この作業」というプロセスを見かけたら、積極的にハックしていけるような精神を大事にエンジニアとして成長していきたいと思いました。

みなさんもSlackというすばらしいチャットツールを最大限に活用し、ときにはハックしてみると面白い経験ができるかもしれません。ぜひ試してみてください。