この記事は DSL Advent Calendar 2019 (Presented by 室蘭工業大学 データサイエンス研究室 - 岸上研) 18日目 の記事です。
日程調整を Slack を使ってもっとスマートにするためのツールを作ったので,使った技術とサービスを紹介します。
&toru (アンドとる) - 幹事も参加者も面倒な日程調整のお手伝いをします!
ちなみに日程調整の他,アンケートをとることもできます。
日程調整 | アンケート |
---|---|
使い方
ログイン
- &toru (アンドとる) - 幹事も参加者も面倒な日程調整のお手伝いをします! にアクセス
- 右上の「Login」ボタンをクリック
- 連携したい Slack アカウント(投稿したいワークスペースのアカウント)で Slack 上でログインして「Authorize」/「Allow」をクリック
- こんなページに飛びます
イベントの作成
- ログイン後のページ の上部にある「新規イベント作成」をクリック
- イベント作成のページが表示されるので,「イベント名」と「メッセージ」を入力してください。
メッセージにはイベントの概要や〆切などを記載すると良いと思います。
「候補」には実際にアンケートを取りたいことを入力します。
ドラッグしたり,横の矢印で順番が変えられます。
最後にアンケートを送信するチャンネルを選択したら,イベント作成ボタンをクリックしてください。
チャンネルは複数選択可能です。
- 実際に送信されるメッセージはこんな感じです。
アンケートに回答する
現在の回答状況を確認する
モチベーション
今まで,研究室内では 調整さん や Slack のスタンプで日時を決めたり,参加人数を把握していました。
しかし,調整さんでは他の人の名前が変えられたり,みんな自由に名前を付けれる都合上,誰なのか一瞬わからないことがありました。
あと,回答が少しめんどくさい。
また,Slack のスタンプでは回答は手軽ですが,参加なのか不参加なのかよくわからないスタンプだったり,1人で複数スタンプが押せるので純粋に足すだけでは参加人数を把握できませんでした(Python で API 叩けば,把握できます)。
実際に使っていた Python スクリプト
import requests
import json
import itertools
from pprint import pprint
token = "API Token"
channel = "Channel ID"
timestamp = 0.0
r = requests.get(
f'https://slack.com/api/users.list?token={token}&pretty=1'
)
users = {i['id']: f"{i['name']} ({i['profile']['real_name']})" for i in json.loads(r.text)['members']}
r = requests.get(
f'https://slack.com/api/reactions.get?token={token}&channel={channel}×tamp={timestamp}&pretty=1'
)
obj = json.loads(r.text)
reaction_users = [users.get(i) for i in set(itertools.chain.from_iterable([i['users'] for i in obj['message']['reactions']]))]
print(len(reaction_users))
pprint(reaction_users)
for i in obj['message']['reactions']:
print(f'{i["name"]}: {[users.get(j) for j in i["users"]]}')
適当にスタンプ押したり,複数押すのは楽しいですし,そういうのが Slack の random の文化だと思っているので,「 つけてください」とか「手あげてください」とか書いてない限りはそれで良いと思っています。
「悪いのはツール側なのだ!」と言うことで作り始めました。
要素技術など
このままだとただの宣伝記事になってしまうので,どのように実現したかなどを紹介します。
Heroku
Web サーバ (兼 API サーバ) は Heroku のアプリとして構築されています。
言語やフレームワークは以下の通り:
- 言語: Python3
- フレームワーク: responder
- ORM: peewee
- ライブラリ管理: pipenv
Heroku 向けのファイルは以下の2つです:
runtime.txt
Procfile
それぞれ以下の内容で書き込みます:
python-3.7.2
web: python app.py
あとは,app.py
上を中心に普段通りアプリケーションを開発します。
なお,後ほど Heroku のアプリ上にデータベースサーバを構築しますが,データベースへのアクセス情報は環境変数の DATABASE_URL
で取得できます。
次に Heroku のダッシュボードに移動して作業します。
まずは Heroku 上でデバッグ用と本番用にそれぞれ一つずつアプリを作成しました。
どちらのアプリにも Add-ons の「Heroku Postgres」の Free のものを追加しました。
また,Deploy 設定から,GitHub リポジトリと紐付けを行いました。
develop ブランチに関しては push されるとデベロップ環境にデプロイされ,master ブランチは Heroku の設定画面から「Deploy Branch」ボタンをクリックすることで本番環境にデプロイされる仕様にしました。
その後,Pipeline を作成してデバッグ用を Development に,本番用を Production に追加しました。
なお,Heroku の Dyno と呼ばれるサーバは 30 分間アクセスがないと,スリープする仕様になっています。
しかし,インターネット上にはスリープしないようにして運用している方々がいるので,私も今回は知恵を絞ってスリープしないようにしました。
正直,もう利用規約などは覚えておらず,規約違反かもしれませんが自己責任でお願いします。
スリープ回避
Google Apps Script (GAS) を用います。
まずは,自分のアプリにアクセスするスクリプトを作成します。
「新しいプロジェクト」をクリックして,以下を貼り付けてください。
function myFunction() {
var url = "http://and-toru.herokuapp.com/";
var response = UrlFetchApp.fetch(url);
console.log(response);
}
スクリプトを作成したら,その画面の「編集」に「現在のプロジェクトのトリガー」と言う項目があるので選択します。
すると,トリガーが設定できます。
このトリガーというのは,どのような条件の時にスクリプトのどの関数を実行するかを指定できます。
トリガーの設定の中で「時間主導型」と言う経過時間ごとに実行できるトリガーがあります。
このトリガーを 30分未満にし,myFunction
を実行するように設定することでスリープ回避を実現しました。
その他のスリープ回避方法は他のサイトを参照してください: Casual Developers Note: Herokuの無料dynoをスリープさせないで24時間稼働させる4つの方法
Slack Apps
次は Slack 側で Slack App として開発を進めます。
まずは slack api: Your Apps にアクセスして,「Create New App」をクリックします。
アプリ名と開発ワークスペースを選んだら作成します。
App の設定ページに移動したら,必要な機能をオンにします。
ボタン付きのリッチなメッセージやダイアログを使うため,「Interactive Components」をオンにしましょう。
「Request URL」はボタンなどが押された際のリクエストの送信先を選択します。
また,Slack アカウントを用いたログインとメッセージ投稿先一覧の表示のため,「OAuth & Permissions」の「Scope」では以下のように設定しました。
最後に,「User ID Translation」の「Translate Global IDs」はオンに設定しています。
Slack のメッセージのボタンはどうやって実現してるの?
この “回答する”,“結果を見る” は Interactive components の Button element を用いています。
その部分をソースコードから抜粋するとこんな感じです:
{
"type": "actions",
"elements": [
{
"type": "button",
"text": {
"type": "plain_text",
"text": "回答する",
"emoji": True
},
"action_id": f"request_answer_dialog/{str(event.id)}",
"value": "request_answer_dialog",
"style": "primary"
},
{
"type": "button",
"text": {
"type": "plain_text",
"text": "結果を見る",
"emoji": True
},
"url": f'{os.getenv("URL_SCHEME")}://{os.getenv("URL_HOST")}/events/{str(event.id)}'
}
]
}
button が押されると Interactive Components の Request URL にリクエストが送信されます。
その中に,action_id
と value
が含まれています。
今回は value
が要求されている内容で,action_id
が要求されている内容とイベント ID が渡されるようにしました。
その value
を元に分岐する部分のソースコードを抜粋します:
@api.route("~~")
async def slack_interactive(req, resp):
if req.method == "post":
media = await req.media()
payload = json.loads(media['payload'])
if payload['type'] == 'block_actions':
if payload['actions'][0]['value'] == 'request_answer_dialog':
pass # ここにダイアログを送る処理を書く
なお,url
が含まれている button はその URL をユーザのブラウザがロードしますが,同時に Interactive Components の Request URL にもリクエストが送信されるので注意が必要です。
どうやって Slack 内に Dialog を表示するの?
Interacting with users through dialogs に方法が書いてあります。
なお,Slack としては今後は dialog から modal へ移行を進めたいようなので (Slack: Upgrading outmoded dialogs to modal),新しく実装する場合は modal を用いるほうが良さそうです。
Slack: Using modals in Slack apps
Dialog のリクエストを抜粋して載せます。
ソースコード中の payload
は一つ前のソースコードの payload
です。
r = requests.post("https://slack.com/api/dialog.open", {
"token": token,
"trigger_id": payload['trigger_id'],
"dialog": json.dumps({
"callback_id": "submit_answer_dialog",
"title": event.name,
"submit_label": "送信",
"notify_on_cancel": False,
"state": payload['actions'][0]['action_id'].split('/')[1],
"elements": [
{
"type": "select",
"label": f"{q.label}",
"name": q.key,
"options": list(map(
lambda x: {"label": x[0],"value": x[1]},
[["○", "o"],["×", "x"],["△", "other"]]
))
}
for q in event.questions.order_by(Question.order)
] + [
{
"type": "text",
"label": "コメント",
"optional": True,
"name": "comment",
"placeholder": "連絡しておきたいことなど"
}
]
})
})
Dialog から送信されたデータはどうやって取り出すの?
ユーザが dialog の「送信」ボタンが押されるとまた Interactive Components の Request URL にリクエストが送られます。
そのリクエストが正規なものかどうかは署名をチェックすることで判断できます。
リクエストの受け取りから署名チェックの部分を抜粋して掲載します:
import hmac
import hashlib
@api.route("~~")
async def slack_interactive_callback(req, resp):
if req.method == "post":
media = await req.media()
payload = json.loads(media['payload'])
logger.debug(payload)
if payload['type'] == 'block_actions':
pass # ここにダイアログを送る処理を書く
elif payload['type'] == 'dialog_submission':
if payload['callback_id'] == 'submit_answer_dialog':
content = await req.content
sig_basestring = f'v0:{req.headers["X-Slack-Request-Timestamp"]}:{content.decode()}'
if hmac.compare_digest('v0=' + hmac.new(
os.getenv('SLACK_SIGNING_SECRET').encode('utf-8'),
sig_basestring.encode('utf-8'),
hashlib.sha256
).hexdigest(), req.headers['X-Slack-Signature']):
# 署名チェッククリア
pass
あとは,ユーザの投稿の中身は payload['submission']
に入っているので署名チェック後にいろいろしましょう。
参考
- Python responder 入門のために… 下調べ - Qiita
- responder: Deploying Responder
- Slack: Reference: Interactive components
- Slack: Interacting with users through dialogs
- Python 3.7.6rc1 ドキュメント: hmac - メッセージ認証のための鍵付きハッシュ化
あとがき
Slack の Dialog ですが,最近 Modal というものが登場して,移行が推奨されているようです。
上でも書きましたが,新規にやる場合は Modal のほうが良さそうです。
あと,&toru (アンドとる) - 幹事も参加者も面倒な日程調整のお手伝いをします! への要望などは Twitter: @YetAnother_yk までお願いします