みなさんこんにちは。サーバーサイドエンジニアの柿本です。
新型コロナの影響でリモートワークをしている人が増えていると思いますが、みなさんはリモートワーク中に誰かとじゃんけんをする時はどうしていますか?
① Zoom ごしにする
=> 「あっ、ネットの調子が!!」とか言いながら後出しする人いますね?
② チャットで同時に「グー」「チョキ」「パー」とポストする
=> 後出しのオンパレードになりますね。というか誰も先に出しません。
③ WEB ツールを使う
=> じゃんけん丸 さんが便利です。
④ そもそも仕事中にじゃんけんしない
=> え、しないんですか?
Slack が仕事場と化した今、オフィスでじゃんけんをするように Slack でじゃんけんできたら便利ですよね?
ということで「じゃんけんボット」を作ることにしました!!
tl;dr
こちら に「Add to Slack」ボタンを用意しました!
クリックしてワークスペースに追加してください。
※今後サーバーを止める可能性があるのでお試しで動かしたあとは削除することをお勧めします。
技術スタック
- 言語/実行環境: Node.js
- フレームワーク: Bolt - Slack 社製のアプリフレームワーク
- データベース: Firestore
- インフラ: Google App Engine
- CI/CD: GitHub Actions
構想
誰かが「じゃんけんっ!」というようにトリガーを引くと、チャネルにいる人たちが自分の手を選択して参加できるようにします。
ユーザーストーリー
/jankenというスラッシュコマンドでじゃんけん開始- 「グー」「チョキ」「パー」のアクションボタンのいずれかをチャネルにいる参加者がクリック
- 「あいこ」もしくは「勝ち」が複数の場合は次のラウンド(2. アクションボタンを選択)へ進む
- 「勝ち」が1人になったら終了
制限仕様
- 最初に手を選択しなかった人は途中参加できない
- 各ラウンドの待ち時間は 10 秒とする
- 1回のじゃんけんにつき 10 ラウンドまでとする
最終形態

成果物
GitHub に公開しました!
実装のポイント
今回初めて Bolt を利用しましたが、 Slack 社公式のフレームワークだけあって非常に簡単に実装できます!
入門ガイド を一通りこなすだけで色々なものが作れそうな気がしてワクワクします!!
Bolt と Firestore とのつなぎこみや GAE のセットアップは以下のブログを参考にさせていただきました。
Bolt のカスタマイズについては Slack の中の人( @seratch さん )のこちらの Qiita 記事が参考になります。
工夫したところ
GitHub Actions で環境変数を置換する
上記ブログでは CI/CD に CircleCI を使っていますが、ソースコード管理との親和性を考えて GitHub Actions 経由で GAE に Deploy するようにしました。
GAE 用の Actions が公式に用意されているので、これを使えば簡単に実装できます。
また、 GAE で環境変数をセットするには app.yaml に値を保持して Deploy する必要があります。しかし app.yaml は構成ファイルなので Git 管理する必要もあるのです。
環境変数を Git 管理下のファイルに直書きするわけにはいかないので、 app.yaml に以下のような形でプレースホルダーを置いておいて、 GitHub Actions で Actions secrets と置換する処理を差し込みました。
// app.yaml env_variables: SLACK_SIGNING_SECRET: "$SLACK_SIGNING_SECRET" // <-- プレースホルダー ...
// .github/workflows/gae-deploy.yml
run: |
sed -i -e 's/$SLACK_SIGNING_SECRET/${{ secrets.SLACK_SIGNING_SECRET }}/g' app.yaml // <-- 置換処理
...
前回の Grafana on Heroku と同じ要領です!
installation の暗号化
Firestore は全データを暗号化していると 公式ドキュメントに記載 されております。
とはいえ GCP のコンソールから丸見えなので、念の為保存時に暗号化することにしました。
// src/initializers/bolt.ts
storeInstallation: async (installation) => {
...
return await installationsRef
.doc(installation.team.id)
.set({installation: encrypt(JSON.stringify(installation))}) // <-- ここで encrypt
...
},
fetchInstallation: async (installQuery) => {
...
const installationDoc = await installationsRef
.doc(installQuery.teamId)
.get()
const installationObj = installationDoc.data() as { installation: string }
return JSON.parse(decrypt(installationObj.installation)) // <-- ここで decrypt
...
},
ローカル開発用に /janken_dev コマンドを準備
チャットボットの開発をする時、 ngrok などのトンネリングツールを使うのが一般的だと思いますが、一度 GAE にデプロイしてしまうと、ローカルと GAE 両方で同じスラッシュコマンドを listen することになります。
複数のワークスペースを準備するという方法もありますが、ちょっとめんどくさかったので2つの Slack app に別のスラッシュコマンドを設定して、それぞれローカルと GAE のエンドポイントを呼び出すようにしました。
// src/commands/janken.ts
app.command('/janken', async (args) => { janken(args) })
app.command('/janken_dev', async (args) => { janken(args) }) // <-- `/janken_dev` でも呼び出せる
const janken = async ({ command, ack, say, client, context, respond }) => {
...
Slack app の設定
| Slack app | Slach command | Request URL |
|---|---|---|
| Janken Dev | /janken_dev |
( ngrok の URL )/slack/events |
| Janken | /janken |
( GAE の URL )/slack/events |
改善したいところ
ゲーム運びの UI
各ラウンドで参加者の出し手を全て表示すると誰が勝ったのか見づらいなと思い、結果(勝ち手が何であったか)のみ表示するようにしました。
そのためアクションボタンや参加状況をラウンドごとに消して結果を表示する、という処理を行った結果、UIがガチャついた感じになりました。
この辺りはセンスの問題なので、ご意見ある方は GitHuib で Issue をあげてください!
ロジックの設計
チャットアプリの適切なロジック設計がよくわからず、ただずらずらとスクリプトを書き連ねた感じになってしまいました。
MVC的なものが良いのか Xxx マネージャー的なものが良いのか考え中ですが、「こんな方法あるよ!」という方はぜひ Issue でご意見ください。
最後に
ここ数年で Slack のトークンの扱いがより厳密になったので自由度がなくなったなぁと思っていたのですが、適切なスコープ設定をすればむしろ安全なチャットアプリを作ることができます。
また Bolt の登場で簡単に実装できるようになったので、便利な Slack アプリをどんどん作っていけたらいいなと思いました!!
ユニファでは Slack でじゃんけんしたい 保育をハックするエンジニアを募集中です!