Hubotを通してSlackとスプレッドシートを連携してルーレットを作る
Hubotを通してSlackとスプレッドシートを連携する
GMOペパボ Advent Calendar 2018の1日目の記事です。
Slackにてルーレットをして人にメンションを飛ばせるBotを作成しました。完成イメージは↓↓↓

こんな感じで、スプレッドシートに前回の当選者を残しながらランダムで人を指定するシステムです。2019年が間近に迫っているのに今更感が否めませんが、お付き合いください。

この記事の対象者
- Slackとスプレッドシートを連携したい人
- HubotもGoogleAppsScriptも触ってことがない人
目的
Slackとスプレッドシートを連携しようと思ったきっかけは、所属しているチームにて毎週行われるチームミーティングでは、KPTを用いた「ふりかえり」を行っていて、ファシリテーターを決める必要があります。ファシリテーターは毎回違う人を選びます。過去3、4回ぐらい前にファシリテーターになった人までは、ファシリテーターの候補から外していました。しかし、3、4週前のこととなると忘れていることが多く、覚えていない時もあります。したがって、過去3回までにファシリテーターを担当した人以外をランダムに選ぶようなルーレットを作りたいと思いました。チームミーティングの出席者が様々な事情で変動することがあり、エンジニア以外にも人数の編集しやすく、使いやすいスプレッドシートという媒体を使用して、Slackを通じてランダムでファシリテーターを指名したいと思いました。 この記事を書いた目的は、Hubotとスプレッドシートの連携の記事の多くは、完成したコードがバーンと公開されていて、仕組みを追いながら理解することができないからです。ただコピペして運用するぶんには問題ありませんが、一応エンジニアなので仕組みを理解しつつやっていきたいという気持ちがあります。ですので、初めから順を追って作成します。
要件
- SlackのBotに対して、
ルーレットとメンションしたら、あらかじめ指定した候補者の中から1名をランダムで選択して、その人に対してメンションする。 - 過去3回までのルーレットの当選者は、選ばれる候補者の中から外れる。
- 候補者のリストは、スプレッドシートで管理する。
この3つの要件を満たすように作っていきましよう。
開発順序
- SlackとHubotを連携させる。
- Hubotで、Slackからのリクエストを処理できるようにする。
- スプレッドシートから、Hubotに対して、候補者のリストから1名をランダムに選択して、レスポンスしてあげる。
- HubotをHerokuへデプロイする
Hubot編
Slack側の設定
Slack App ディレクトリでHubotと検索して、アプリをワークスペースに追加します。
アプリを Slack に追加する | アプリおよびインテグレーション | Slack App ディレクトリ
「設定を追加」を選択して、Hubotの名前を入力してHubotインテグレーションを使いします。ここでは名前を「roulette」とします。

次に追加したHubotのインテグレーションの設定を変更します。APIトークンはHubotのプロセスを立ち上げるときに必要です。

Hubtoの設定
Hubotのチュートリアルの通りやっていきましょう。
Getting Started With Hubot | HUBOT
まずHubotを動かすためのパッケージをインストールします。ここでは npm については触れません。必要なパッケージは yo と generator-hubot です。
$ npm install -g yo generator-hubot
次にHubotの雛形を作成します。このときメールアドレスやHubotの名前も要求されますが、全てEnterでも構いません。このタイミングでリポジトリを作りたい人は git init をするのを忘れずに。
$ mkdir hubot-roulette $ cd hubot-roulette $ yo hubot
bin/hubot Hubotのプロセスを立ち上げてローカルでHubotが動くことを確認します。 hubot-roulette ping でHubotから PONG とレスポンスがあれば疎通確認は完了です。
$ bin/hubot # 色々メッセージが出るけど動作するので無視する hubot-roulette> hubot-roulette ping hubot-roulette> PONG
次はHubotで簡単なスクリプトを書いて、Hello,Worldしてみます。 scripts/hello.coffee を作成します。JavaScriptでも可能ですが、今回はCoffeeScriptで書いてみます。これで、 hello と呼びかければ world と返ってきます。
$ touch scripts/hello.coffee
module.exports = (robot) ->
robot.respond /hello/i, (msg) ->
msg.send 'world'
$ bin/hubot hubot-roulette> hubot-roulette hello hubot-roulette> world
次にSlackとの連携をやってしまいます。 hubot-slack というSlackとHubotのアダプターのパッケージがあるのでインストールします。そして環境変数 HUBOT_SLACK_TOKEN にSlackの設定時に取得したAPIトークンを代入します。プロセスが起動したら、 roulette アカウントに対してDMを送信します。まずは ping を送信して疎通を確認します。次に hello と送信して world が返ってくるか確認します。
$ npm install hubot-slack # Slackのプロセスが起動 $ HUBOT_SLACK_TOKEN="Slackで取得したAPIトークン" ./bin/hubot --adapter slack

APIとの通信
スプレッドシートは、WebAPIを作成する機能があり、Hubotとのやり取りはhttpリクエストを通じて行います。まずは実際にスプレッドシートでAPIを作成するより、スタブサーバーを立ててHubot側の動作を確認することを優先しましょう。npmでjsonから簡単にスタブサーバーを立てることのできるパッケージ json-server をインストールします。
$ npm install -g json-server $ touch user.json
user.jsonの中身は以下の通りです。 GET "localhost:3000/users" のエンドポイントを作り、 "a" という文字列が返ってくることを期待します。
{
"users": [
"a"
]
}
# スタブサーバーを立ち上げる
$ json-server --watch user.json
\{^_^}/ hi!
Loading user.json
Done
Resources
http://localhost:3000/users
Home
http://localhost:3000
Type s + enter at any time to create a snapshot of the database
Watching...
# 期待通り"a"が返ってきた $ curl -X GET "http://localhost:3000/users" [ "a" ]%
Hubotのスクリプトから、このスタブサーバーのエンドポイントへリクエストします。JavaSriptのHTTPクライアントを使用します。
CoffeeScript Cookbook » Basic HTTP Client
http = require 'http'
module.exports = (robot) ->
robot.respond /hello/i, (msg) ->
# port番号3000を指定するのを忘れずに
http.get {host: 'localhost', port: 3000, path: '/users'}, (res) ->
if res.statusCode is 200
body = ''
res.setEncoding 'utf8'
res.on 'data', (chunk) ->
# レスポンスのデータをbodyにまとめる
body += chunk
res.on 'end', ->
# レスポンスのJSON形式のデータをパースする
obj = JSON.parse(body)
msg.send obj
else
console.log "error: #{res.statusCode}"
# きちんと"a"が取得できており、GETリクエストが成功したことがわかる $ bin/hubot hubot-roulette> hubot-roulette hello hubot-roulette> a
GoogleAppsScript編
次はスプレッドシートでAPIを作ります。新規のスプレッドシートを作成しましょう。 ツール>スクリプトエディタ でエディタを起動します。まずやることはスタブサーバーと同じで、単純なレスポンスを返せるようにすることです。 doGet 関数を定義して単純なJSONが返せるようにしましょう。
// GETリクエストに対する処理
function doGet(e) {
// API形式で出力できるように準備をする
var output = ContentService.createTextOutput();
// JSONで出力する
output.setMimeType(ContentService.MimeType.JSON);
// JSON 文字列に変換
payload = JSON.stringify({"user": "a"});
// ContentServiceインスタンスに出力するJSONをセットする
output.setContent(payload);
return output;
}
このあとAPIとして公開するようにします。 公開>ウェブアプリケーションとして導入 を選択して実行ユーザーを 全員(匿名ユーザーを含む) にします。URLを取得できるので curl にて確認します。リダイレクトに追従できるように -L を付けます。
$ curl -L https://script.google.com/macros/s/[APIのID]/exec ["a"]%
続いてはスプレッドシートから値をとってみます。A1にbと入力して、この値を返すAPIを作ります。 getValue 関数を新たに作成して、その中でA1の値を取得します。

// GETリクエストに対する処理
function doGet(e) {
// API形式で出力できるように準備をする
var output = ContentService.createTextOutput();
// JSONで出力する
output.setMimeType(ContentService.MimeType.JSON);
var value = getValue();
// JSON 文字列に変換
payload = JSON.stringify({"user": value});
// ContentServiceインスタンスに出力するJSONをセットする
output.setContent(payload);
return output;
}
function getValue() {
// スプレッドシートID
var id = 'シートのIDを入れる';
var ss = SpreadsheetApp.openById(id);
// シート名
var sheet = ss.getSheetByName("シート1");
// セルの値を取得する
// sheet.getRange({行番号},{列番号})でセルの範囲を指定して、.getValueで値を取得する
// この場合はA1を取得する
var value = sheet.getRange(1, 1).getValue();
return value;
}
# A1セルに入れていたものが返ってきた
$ curl -L https://script.google.com/macros/s/[APIのID]/exec
{"user":"b"}%
ここまでくればあと一息です。あとは書き込む方法さえ知ってしまえば、冒頭のルーレットを作る上で必要なことはだいたい網羅できます。 getValue 関数の中に以下のコードを追加します。そうすると実行時の時刻がB1のセルに書き込まれます。
// 実行した時刻をB1に書き込む sheet.getRange(1, 2).setValue(new Date());

続いてルーレットを作っていきます。ルーレットは、A2にカンマ区切りで候補者の名前を入れます。これはSlackのメンションするときに使うので、Slackのアカウント名です。A3からA5までは、それぞれ1~3回前のルーレットで当選した人が記録されています。この人たちは、集中して当選しないようにルーレットの抽選候補者から除外します。文字数が多くなってしまうので、個々の解説はできませんが、今までやってことを全て合わせた集合がルーレットという機能です。

// GETリクエストに対する処理
function doGet(e) {
// API形式で出力できるように準備をする
var output = ContentService.createTextOutput();
// JSONで出力する
output.setMimeType(ContentService.MimeType.JSON);
// スプレッドシートID
var id = 'シートのIDを入れる';
var ss = SpreadsheetApp.openById(id);
// シート名
var sheet = ss.getSheetByName("シート1");
// ルーレットの候補になるセルの値を取得する
var values = sheet.getRange(2, 1).getValues();
var resArray = values.toString().split(",");
// 過去3回まで選ばれたものは、再度選ばれないように候補から外す
var exclude = [];
for (i = 0; i < 3; i++) {
var x = i + 2;
if (sheet.getRange(2, x).getValue() != '') {
exclude[i] = sheet.getRange(2, x).getValue();
}
}
if (exclude.length < resArray.length) {
exclude.forEach(function(element) {
resArray = arrayDelete(resArray, element);
});
}
var target = resArray[Math.floor(Math.random() * resArray.length)];
sheet.getRange(2, 2).setValue(target);
if (exclude[0] !== undefined) {
sheet.getRange(2, 3).setValue(exclude[0]);
}
if (exclude[1] !== undefined) {
sheet.getRange(2, 4).setValue(exclude[1]);
}
var res = {};
res['user'] = target;
Logger.log(res);
Logger.log(target);
payload = JSON.stringify(res);
output.setContent(payload);
return output;
}
// valueの要素と一致したものを削除する
function arrayDelete(array, value) {
for(i = 0; i < array.length; i++){
if(array[i] === value){
//spliceメソッドで要素を削除
array.splice(i, 1);
}
}
return array;
}
# 予想した返しができている
$ curl -L https://script.google.com/macros/s/[APIのID]/exec
{"user":"山根"}%

ここまでできたら、実際にHubotのスクリプトでこのAPIを実行してみましょう。スプレッドシートで作成したAPIのURLは、リダイレクトしてGoogleAppsScriptを実行します。なのでリダイレクトしても追従できるように follow-redirects というパッケージをインストールします。package.jsonに追加するので -g オプションは付けません。 scripts/roulette.coffee にスクリプトを書きます。
$ npm install follow-redirects $ touch scripts/roulette.coffee
スプレッドシートのAPIのURLは漏れないに越したことはないので、環境変数から取得できるようにします。 API_URL という名前にします。
http = require 'follow-redirects'
# スプレッドシートはhttpsなので注意する
http = http.https
# 環境変数を取得
url = process.env.API_URL
module.exports = (robot) ->
robot.respond /ルーレット/i, (msg) ->
http.get
host: 'script.google.com'
path: url
, (res) ->
if res.statusCode is 200
body = ''
res.setEncoding 'utf8'
res.on 'data', (chunk) ->
body += chunk
res.on 'end', ->
obj = JSON.parse(body)
# APIからのレスポンスを@を先頭に付けてSlackへ返す
# レスポンスの例 {"user":"山根"}
msg.send '@' + obj.user.toString()
else
console.log "error: #{res.statusCode}"
$ HUBOT_SLACK_TOKEN="Slackで取得したAPIトークン" API_URL=/macros/s/[APIのID]/exec ./bin/hubot --adapter slack
これでSlackのrouletteに「ルーレット」と呟いてみます。するときちんと抽選結果が返ってきています。

このHubotをチャンネルに招待するために、 /invite @roulette を実行します。そして @roulette ルーレット とメンションを飛ばすと先ほどと同じ結果が得られます。

実運用
PCを常時起動している人だったら、ずっとHubotプロセスを実行して入ればいいのですが、そうはいかないのでHubotをHerokuへデプロイします。デプロイする前に、Hubotの雛形を作ったときのpackage.jsonのnode.jsのバージョン指定が 0.10.x になっているので適当なものに修正します。
"engines": {
"node": "10.10.0"
}
Herokuへデプロイするための準備も含めて行います。
$ heroku login $ heroku create [your-bot-name] # Hubotにはredisが必要 $ heroku addons:create redistogo:nano $ heroku config:set HUBOT_SLACK_TOKEN=[your-slack-token] --app [your-bot-name] $ heroku config:set API_URL=[your-api_url] --app [your-bot-name] $ heroku git:remote -a [your-app-name] $ git push heroku master
これでHerokuでHubotが動きます。
実際に運用してみての感想
まだ1回しか運用していませんが、誰が前回のファシレーテータかを思い出す必要がなく、ファシリテーター決めも30秒から1秒くらいになったので煩わしさが解消されたかなーと思っています。ここまで書くのに疲れたのでもう休みます。
参考記事
Google Spreadsheet を簡易 Webサーバーとして動かして、手軽にWebHookを受け取る方法 - Qiita
JSON.stringify() - JavaScript | MDN
herokuでhubot立ててみたらカンタンだった - Qiita
HubotをHerokuでSlackに繋げるまで - Qiita
CoffeeScript Cookbook » Basic HTTP Client
JSON Server使いこなし - モックサーバーの起動とリソース処理 | CodeGrid
typicode/json-server: Get a full fake REST API with zero coding in less than 30 seconds (seriously)