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
Hubotとの会話
スプレッドシート は、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の値を取得します。
A1にbと入力してね
// 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":"山根"}%
B2に今回の当選者が入っている
ここまでできたら、実際に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秒くらいになったので煩わしさが解消されたかなーと思っています。ここまで書くのに疲れたのでもう休みます。
参考記事
hubotスクリプトの書き方とサンプル集 | mitc
Google Spreadsheet を簡易 Webサーバーとして動かして、手軽にWebHookを受け取る方法 - Qiita
JSON.stringify() - JavaScript | MDN
GASでGoogleスプレッドシートのセルの値、行数や列数を取得したり、セルに値を入力したりする基本 (1/2):Excel VBAプログラマーのためのGoogle Apps Script入門(2) - @IT
herokuでhubot立ててみたらカンタンだった - Qiita
HubotをHerokuでSlackに繋げるまで - Qiita
配列からランダムに値をとりだす。 - 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)