WEBエンジニアの石田です。
さて、僕は前回もSlackネタでしたが、今回もSlackネタです。
弊社では、お掃除部という部活(?)がありまして、拭き掃除・ゴミ出し・換気などのオフィス内の掃除、あと朝一のコーヒー作りを有志が毎日行っています。
当番とかは決まっておらず、できるメンバーでやろう!というスタンスなのですが、部員の大半を占めるエンジニア達はフレックス制で出社時間がバラバラ…
となると、チェックリストは欲しいですよね。
紙とかホワイトボードでチェック!となるのがまあ普通だと思いますが、弊社はIT企業。そして社内のコミュニケーションにSlackを使ってるんだから、せっかくならオンラインで済ませてしまいたい。という欲が僕の中で沸々と湧き上がってきたので、SlackAPIを使ってアプリ作りました(・ω・)b
まずは出来上がったものをご紹介します。
ボタンをクリックすると名前が登録され、同じボタンを押すと登録が解除されます
仕組み
- AM9:00になるとタスク一覧と、項目ごとの絵文字のボタンが並んでいるメッセージが自動投稿される。
- cronで平日毎日9:00にpostするAPIを叩く
- タスクの絵文字ボタンをクリックすると、クリックしたユーザーのIDがタスク一覧の下に表示される。
- 既にボタンをクリック済みのユーザーが、もう一度同じ絵文字のボタンをクリックするとタスク一覧の下に表示されていた名前が消える。
- 絵文字のボタンを押すとSlack から指定したURLにリクエストが送られ、リクエストに応じて処理し、メッセージを書き換えるAPIを叩く
今回つかったもの
- PHP
- MySQL
- Slack API
- Interactive Components
やってみる
1. Slack APIでAppを作成
- Interactive Componentsメッセージにボタンやメニューなどを付与し、ユーザーからのアクションに応じて応答できる機能です。GitHubやBitBucketのAppをはじめ、最近とても頻繁に使われているので多分見たことあるのではないでしょうか。
https://api.slack.com/apps?new_app=1
AppNameは後から変更も可能でした。
上記のURLにアクセスすると、SlackAppの作成画面が出るので AppName (アプリ名) と Development Slack Workspace (使用するSlackのワークスペース) を指定し、 Create App します。
SlackでAPIを作成すると、FeaturesにInteractive Componentsという項目があり、Onにすると設定が可能になります。
設定は色々とありそうですが、今回は Request URLに任意のURL( example.com/api/hogehoge.php など)を貼ればOK。
あと、投稿用にSlackのトークンを取得しておきます。
OAuth & Permissions からScopesを絞って Install App。 Permission Scopeはchat:write:botを選択でOKです。
権限はchatだけでok
Permissionを設定したらInstall App
これでSlack上の設定は完了。
2. サーバー側の設定
とりあえずmysqlでDBを用意。型は適当につけてます
1CREATE TABLE polls
2(
3id INT AUTO_INCREMENT PRIMARY KEY,
4message_ts VARCHAR(255) NOT NULL,
5channel_id VARCHAR(255) NOT NULL,
6text TEXT NOT NULL,
7answers TEXT NOT NULL,
8attachments TEXT NOT NULL,
9created_at TIMESTAMP NULL
10) DEFAULT CHARSET = utf8 AUTO_INCREMENT = 1;
11CREATE TABLE votes
12(
13id INT AUTO_INCREMENT PRIMARY KEY,
14channel_id VARCHAR(255) NOT NULL,
15message_ts VARCHAR(255) NOT NULL,
16user_name VARCHAR(255) NOT NULL,
17user_id VARCHAR(255) NOT NULL,
18action_name VARCHAR(255) NOT NULL,
19action_value VARCHAR(255) NOT NULL,
20created_at TIMESTAMP NOT NULL
21) DEFAULT CHARSET = utf8 AUTO_INCREMENT = 1;
22
貼り付けた任意のURLではなく、まずpublicの外にでも投稿用のファイルを作成します。
postの第一引数の連想配列が選択肢です。nameに表示する文字を入れて、 iconはslackのアイコンの名前を入力(::はとる)でOK。なくてもOK。
1<?php
2post([
3['name' => '換気', 'icon' => 'wind_blowing_face'],
4['name' => 'ゴミ出し', 'icon' => 'wastebasket'],
5['name' => '拭き掃除', 'icon' => 'sparkles'],
6['name' => 'コーヒー', 'icon' => 'coffee'],
7] , '今日のTODO');
8function post($questions, $description) {
9$attachments = [];
10$buttons = [];
11$text = $description . "\n";
12// answersが表示テキスト、buttonsが実施ボタンになる
13$answers = [];
14foreach ($questions as $key => $button) {
15$answers[] = ':' . $button['icon'] . ': ' . $button['name'];
16$buttons[] = [
17'name' => $button['icon'] ?? ($key + 1),
18'text' => ':' . ($button['icon'] ?? ($key + 1)) . ':',
19'type' => 'button',
20'value' => $button['name']
21];
22}
23// ボタンは5つを超えるとattachmentがスマホで表示できなくなるためを分割しておく
24$block = array_chunk($buttons, 5);
25// タスクごとに改行する
26$text .= implode("\n", $answers);
27// ボタンのブロックごとにattachmentを作成する
28foreach ($block as $key => $action) {
29$attachments[] =
30[
31'fallback' => 'daily' . $key,
32"callback_id" => "daily",
33"color" => "#3AA3E3",
34"attachment_type" => "default",
35'actions' => $action
36];
37}
38// Slackに投稿を行う
39$curl = curl_init();
40curl_setopt_array($curl, [
41CURLOPT_RETURNTRANSFER => true,
42CURLOPT_POST => true,
43CURLOPT_URL => 'https://slack.com/api/chat.postMessage',
44CURLOPT_HTTPHEADER => [
45'Content-Type: application/json; charset=utf-8',
46'Authorization: Bearer ' . '【SLACKのトークン】'
47],
48CURLOPT_POSTFIELDS => json_encode([
49'channel' => '【投稿チャンネル】',
50'text' => $text,
51'attachments' => $attachments
52])
53]);
54$response = curl_exec($curl);
55$response = json_decode($response, true) ?? null;
56if ( !$response ) {
57return false;
58}
59//投稿情報をMySQLに保存しておく
60$db = new PDO(
61'mysql:host=' . '【接続するホスト】' .';dbname=' . '【DB名】' . ';charset=utf8',
62'【DBユーザー名】', '【DBパスワード】', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
63);
64$query = $db->prepare('INSERT INTO polls (message_ts, channel_id, text, answers, attachments, created_at) VALUES (:message_ts, :channel_id, :text, :answers, :attachments, :created_at)');
65$query->execute([
66':message_ts' => $response['ts'],
67':channel_id' => $response['channel'],
68':text' => $description,
69':answers' => json_encode($answers),
70':attachments' => json_encode($response['message']['attachments']),
71':created_at' => date('Y-m-d H:i:s')
72]);
73return $response;
74}
75
上記のPHPファイルを実行すると指定したチャンネルに下記のようなフォームみたいなものが投稿されます。
各ボタンをクリックすると、先程のURLのhoge.phpにPOSTが走るので、下記を配置しておきます。
1<?php
2if (!empty($_POST['payload'])) {
3$vote = json_decode($_POST['payload'], true);
4$message = $vote['message_ts'];
5$channel = $vote['channel']['id'];
6$user_id = $vote['user']['id'];
7$username = $vote['user']['name'];
8$db = new PDO(
9'mysql:host=' . '【接続するホスト】' .';dbname=' . '【DB名】' . ';charset=utf8',
10'【DBユーザー名】', '【DBパスワード】', [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
11);
12$query = $db->prepare('SELECT * FROM polls WHERE message_ts = :message_ts AND channel_id = :channel_id');
13$query->execute([
14':message_ts' => $message,
15':channel_id' => $channel
16]);
17$data = current($query->fetchAll());
18if ($data) {
19$attachments = json_decode($data['attachments'], true);
20//既に存在する実施TODO・実施者IDとボタン押した人の実施TODO・IDが一致したら存在フラグをたてる
21$votes = selectVotes($db, $message, $channel);
22$exist = false;
23foreach ($votes as $answer) {
24if ($answer['user_id'] === $user_id && $answer['action_value'] === $vote['actions'][0]['value']) {
25$exist = $answer['id'];
26}
27}
28//存在フラグが立ってる場合は削除・ない場合は新規挿入
29if ($exist) {
30$db->prepare('DELETE FROM votes WHERE id=:id')->execute([':id' => $exist]);
31} else {
32$db->prepare('
33INSERT INTO
34votes
35(channel_id, message_ts, user_name, user_id, action_name, action_value, created_at)
36VALUES
37(:channel_id, :message_ts, :user_name, :user_id, :action_name, :action_value, :created_at)')
38->execute([
39':channel_id' => $channel,
40':message_ts' => $message,
41':user_name' => $username,
42':user_id' => $user_id,
43':action_name' => $vote['actions'][0]['name'],
44':action_value' => $vote['actions'][0]['value'],
45':created_at' => date('Y-m-d H:i:s')
46]);
47}
48// 投稿したTODOの現在の実施者一覧を出す
49$votes = selectVotes($db, $message, $channel);
50$result = [];
51$answers = json_decode($data['answers']);
52foreach ($answers as $answer) {
53$result[$answer] = '';
54foreach ($votes as $vote) {
55if ($answer === ':' . $vote['action_name'] . ': ' . $vote['action_value']) {
56$result[$answer] .= ' <@' . $vote['user_id'] . '>';
57}
58}
59}
60// 実施者一覧をメッセージに反映する
61$text = $data['text'];
62foreach ($result as $answer => $voters) {
63$text .= "\n" . $answer . "\n";
64$text .= empty($voters) ? '' : $voters . "\n";
65}
66$update = [
67'channel' => $data['channel_id'],
68'ts' => $data['message_ts'],
69'text' => $text,
70'attachments' => $attachments
71];
72// Slackのメッセージを更新する
73$url = 'https://slack.com/api/chat.update';
74$curl = curl_init();
75curl_setopt_array($curl, [
76CURLOPT_RETURNTRANSFER => true,
77CURLOPT_POST => true,
78CURLOPT_URL => $url,
79CURLOPT_HTTPHEADER => [
80'Content-Type: application/json; charset=utf-8',
81'Authorization: Bearer ' . '【SLACKのトークン】'
82],
83CURLOPT_POSTFIELDS => json_encode($update)
84]);
85return curl_exec($curl);
86}
87}
88function selectVotes(PDO $db, string $message_ts, string $channel_id)
89{
90$query = $db->prepare('
91SELECT
92*
93FROM
94votes
95WHERE
96message_ts = :message_ts AND
97channel_id = :channel_id
98ORDER BY
99action_value
100');
101$query->execute([
102':message_ts' => $message_ts,
103':channel_id' => $channel_id
104]);
105return $query->fetchAll();
106}
送られてきたpostの中にユーザーID・ボタンを押したmessageの情報・押したボタンの情報があるので、それを照合し、あれば削除(MySQLから既存レコードをDELETE)・なければ作成(MySQLにINSERT)し、元のメッセージを状況に合わせて更新することで投票を実現しています。
所感
プログラムの部分で長くはなってしまいましたが、これでTODOアプリは完成です。シーズでは毎日の日次タスクと、月曜日に週次タスクを投稿する形で運用してますが、今のところ問題なく稼働しています。
こういったTODOリストの共有やリマインダ、GitHub/BitBucketの連携など、SlackAPIを応用することで、手軽に様々な機能を日々のコミュニケーションの中に自力で追加できるのはとても魅力的だと感じました。
アイデア次第ではもっと面白いことができそうなSlackAPI、もっと色々な活用方法を見出して社内に還元していきたいなあ…と目論んでおります!