富士通システムズウェブテクノロジー Advent Calendar 2019 13日目の記事です。
実は、私はパートナー会社の者です。
書かせていただけるなんて、懐が深くて感謝の限りでございます!
本当にありがとうございます!!!
タイトルの通りネタ記事です。
正直こんな記事でごめんなさい。
(先に謝っておくスタイル)
ただ、前日の投稿
「Google Cloud Vision APIを使って食べ物の写真を判定してみる - Qiita」
が良記事過ぎて、書くのがつらいっす。。。
とりあえず全力で頑張りますのでよろしくお願いいたします!
そしてお約束です。
お断り)記事は全て個人の見解です。会社・組織を代表するものではありません。
はじめに
当記事では、チャットをより楽しくするためのお遊び機能を作ったので紹介しています。
真面目に読むよりは息抜き程度にお読みくださると幸いです。
私の職場環境
私が働いている場所はチャットが大変普及しています。
素晴らしいことに心理的安全性の高いチャットが実現されており、本音のトークがバンバンやり取りされています。
ともすると、ポジティブな内容ばかりではなく、ネガティブな発言もあったりする訳です。
私自身、仕事がしんどい時は「眠い」「帰りたい」などの発言をしてしまうこともあります。
そんなとき、ふと思うのです。
つらいのは私だけなのだろうか…と
いやいや、そんなはずはない。
きっと同じように苦しんで頑張っている人もいるはず!
皆で分かち合えば、辛さも軽減されて頑張れるはず!
ならば作りましょう!同士を集めるブラックホールを!
ブラックホールを支えるツール達
- Mattermost v5.13.2
MattermostはOSSのチャットツールです。
Slackライクな使い心地でMarkdown形式が使えるのが強いところ。
個人的には、Slackより好きです。
詳しくはググってね
- Node-RED v0.18.7
Node-REDはブラウザ上でGUIベースでロジックをゴリゴリ書ける面白ツールです。
ノード
という形で様々な機能を持ったブロックをつなげることで、簡単にロジックが組めます。
APIとかも簡単に作れたりするので、Bot開発には最適です。
詳しくは(ry
ブラックホールの仕組み
「Mattermostのパブリックチャンネルで特定ワードを投稿する」(「眠い」「NMI」など)
「Node-REDのロジックで、強制的にブラックホールチャンネルに招待する」
補足:
ブラックホールチャンネルは予め用意しておくこと
なるべく、プライベートチャンネルにしましょう
(仕事中に眠い人が集まるので、公開処刑になる可能性が0ではない)
引き込んだら歓迎しましょう
実際の様子
こんな感じで大盛況です。眠そうなターゲットを引き込めたときの嬉しさはすごいです。
ブラックホールの作り方
ここからは真面目に技術のお話です。
事前準備
- Mattermostの管理者権限を取得
- ブラックホールチャンネルを作成
- Node-RED(Dockerなどを利用すればサクッと建てられます)
Mattemostで眠いを検知する機能を作る
眠いを検知する機能は、Mattermost統合機能の一つ外向きのウェブフックで実現しています。
Mattermostのメニューから、統合機能を作成します
(ここに表示されていない場合、管理者が禁止している可能性があります。その場合は管理者にネゴって機能を開放してもらう必要があります)
チャンネルを選択しない
(選択しないことで、全てのパブリックチャンネルが対象になります)
補足するために思いつく限りの眠そうなワードを登録します
(腕の見せ所です)
投稿の文頭にこのワードがついていれば補足できるようにします
(正確に一致するにすると、ワードの後ろがスペースでないと反応しません)
Node-REDのロジックを叩く部分です。
Node-REDのホスト名+/api/v1/nmi
という形で登録しておきます。
ここまでできたら外向きのウェブフック保存しましょう。
Mattemostで通知を受ける口を作る
今度はNode-REDからの通知を受け取る場所を、Mattermost統合機能の一つ内向きのウェブフックで実現します。
先程と同じ様に統合機能メニューを開き、今度は内向きのウェブフックを選択します
あとは、設定(名前、説明、デフォルト投稿チャンネル)を入力し、保存します。
(このチャンネルに固定する
のチェックを外しておけば、汎用的に使えます)
これで通知を受け取る口ができました。
Mattemostの設定はこれで以上です。
Node-REDで吸い込む機能を作る
全文書くと超大作になってしまうので、ある程度割愛します。。。っ!
(ロジックは後ほど全文載せます)
全体像を元に解説していきます。
MattermostからのWebhookを受け取るためにAPIを作成します。
Node-REDのホスト名/api/v1/nmi
という名前でAPIを定義しています。
存在確認
のノード(Node-REDはノードをつないでロジックを作る)でMattermostにAPIを投げています。
APIを実行するためにはアクセストークンが必要になるので、認証情報格納
のノードでアクセストークンを詰めています。
テキストセット
のノードで「誰」が「どこ」で「なんと言っているか」を整理して、http request
のノードでMattermostの内向きWebhookを叩いて、Mattermostに投稿します。
招待パラメータ設定
のノードでMattermost用のAPIのパラメータを詰めて、招待を実施しています。
招待後、「誰」が「どこ」で「なんと言っているか」を整理して、Mattermostに投稿します。
これで準備は完了です。
Node-REDロジック全文
上の図を見てもわからない箇所も多いと思うので、全文を載せておきます。
「ソースコードを読み込むませることもできます」
「図のところに貼り付ける」
Node-REDロジック全文
[
{
"id": "6136913a.b7dd8",
"type": "subflow",
"name": "認証情報格納",
"info": "またもすのアクセストークンを与えます",
"in": [
{
"x": 60,
"y": 60,
"wires": [
{
"id": "82b68cf6.84efd"
}
]
}
],
"out": [
{
"x": 500,
"y": 60,
"wires": [
{
"id": "82b68cf6.84efd",
"port": 0
}
]
}
]
},
{
"id": "82b68cf6.84efd",
"type": "function",
"z": "6136913a.b7dd8",
"name": "認証情報格納",
"func": "\nmsg.headers = {};\n\nmsg.headers['Authorization'] = 'Bearer {Mattermostのアクセストークン}';\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 280,
"y": 60,
"wires": [
[]
]
},
{
"id": "44865478.e5343c",
"type": "debug",
"z": "25e49fa5.4ce1e",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"x": 1290,
"y": 120,
"wires": []
},
{
"id": "7b3d351e.b0830c",
"type": "http request",
"z": "25e49fa5.4ce1e",
"name": "招待",
"method": "POST",
"ret": "txt",
"url": "",
"tls": "",
"x": 1250,
"y": 340,
"wires": [
[
"d50c7022.02391",
"4749d54e.c25dac"
]
]
},
{
"id": "958cbda1.fbce2",
"type": "http request",
"z": "25e49fa5.4ce1e",
"name": "存在確認",
"method": "GET",
"ret": "obj",
"url": "",
"tls": "",
"x": 1000,
"y": 120,
"wires": [
[
"a9e034e9.115308"
]
]
},
{
"id": "85eae35e.3f8b9",
"type": "subflow:6136913a.b7dd8",
"z": "25e49fa5.4ce1e",
"name": "",
"x": 820,
"y": 120,
"wires": [
[
"958cbda1.fbce2"
]
]
},
{
"id": "a9e034e9.115308",
"type": "function",
"z": "25e49fa5.4ce1e",
"name": "存在判定",
"func": "\nmsg.exist = false;\nfor (var key in msg.payload) {\n var user = msg.payload[key];\n if (user.user_id === msg.prePayload.user_id) {\n msg.exist = true;\n }\n}\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 1160,
"y": 120,
"wires": [
[
"30759ba3.5bdb24",
"44865478.e5343c"
]
]
},
{
"id": "30759ba3.5bdb24",
"type": "switch",
"z": "25e49fa5.4ce1e",
"name": "存在判定",
"property": "exist",
"propertyType": "msg",
"rules": [
{
"t": "true"
},
{
"t": "false"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 680,
"y": 300,
"wires": [
[
"76525710.751268"
],
[
"ff82c853.54b4c8"
]
]
},
{
"id": "ff82c853.54b4c8",
"type": "function",
"z": "25e49fa5.4ce1e",
"name": "招待パラメータ設定",
"func": "msg.payload = {\n user_id: msg.prePayload.user_id\n}\nmsg.url = \"http://{Mattermost_Host}/api/v4/channels/\" + msg.target_channel + \"/members\";\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 900,
"y": 340,
"wires": [
[
"33f97f1f.f2fc8"
]
]
},
{
"id": "33f97f1f.f2fc8",
"type": "subflow:6136913a.b7dd8",
"z": "25e49fa5.4ce1e",
"name": "",
"x": 1100,
"y": 340,
"wires": [
[
"7b3d351e.b0830c"
]
]
},
{
"id": "d50c7022.02391",
"type": "debug",
"z": "25e49fa5.4ce1e",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"x": 1410,
"y": 340,
"wires": []
},
{
"id": "4749d54e.c25dac",
"type": "function",
"z": "25e49fa5.4ce1e",
"name": "またもす返却設定",
"func": "msg.payload = {};\nmsg.payload.response_type = \"in_channel\";\nmsg.url = \"{Mattermost_Webhook}\";\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 870,
"y": 440,
"wires": [
[
"c1514d40.3717d"
]
]
},
{
"id": "c1514d40.3717d",
"type": "function",
"z": "25e49fa5.4ce1e",
"name": "テキストセット",
"func": "msg.payload.text = \"@\" + msg.prePayload.user_name + \"が ~\" + msg.prePayload.channel_name + \"で「\" + msg.prePayload.text + \"」と叫んだため、引き込まれました。\";\nmsg.payload.username = msg.target_icon_name;\nmsg.payload.icon_url = msg.target_icon_url;\n\nmsg.url = msg.webhook;\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 1060,
"y": 440,
"wires": [
[
"e18e06cf.4c2cf8"
]
]
},
{
"id": "e18e06cf.4c2cf8",
"type": "http request",
"z": "25e49fa5.4ce1e",
"name": "",
"method": "POST",
"ret": "txt",
"url": "",
"tls": "",
"x": 1230,
"y": 440,
"wires": [
[
"f71a1baf.9365f8"
]
]
},
{
"id": "f71a1baf.9365f8",
"type": "debug",
"z": "25e49fa5.4ce1e",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"x": 1370,
"y": 440,
"wires": []
},
{
"id": "76525710.751268",
"type": "function",
"z": "25e49fa5.4ce1e",
"name": "またもす返却設定",
"func": "msg.payload = {};\nmsg.payload.response_type = \"in_channel\";\nmsg.url = \"{Mattermost_Webhook}\";\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 890,
"y": 280,
"wires": [
[
"8a3f0824.242e08"
]
]
},
{
"id": "8a3f0824.242e08",
"type": "function",
"z": "25e49fa5.4ce1e",
"name": "テキストセット",
"func": "msg.payload.text = \"@\" + msg.prePayload.user_name + \"が ~\" + msg.prePayload.channel_name + \"で「\" + msg.prePayload.text + \"」と叫んでいます。\";\nmsg.payload.username = msg.target_icon_name;\nmsg.payload.icon_url = msg.target_icon_url;\n\nmsg.url = msg.webhook;\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 1100,
"y": 280,
"wires": [
[
"9dde8ac3.c97968"
]
]
},
{
"id": "9dde8ac3.c97968",
"type": "http request",
"z": "25e49fa5.4ce1e",
"name": "",
"method": "POST",
"ret": "txt",
"url": "",
"tls": "",
"x": 1270,
"y": 280,
"wires": [
[
"760308c4.4f00d8"
]
]
},
{
"id": "760308c4.4f00d8",
"type": "debug",
"z": "25e49fa5.4ce1e",
"name": "",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"x": 1410,
"y": 280,
"wires": []
},
{
"id": "bf6d4801.a6ac88",
"type": "function",
"z": "25e49fa5.4ce1e",
"name": "テキストセット",
"func": "msg.url = \"http://{Mattermost_Host}/api/v4/channels/\" + msg.target_channel + \"/members\";\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"x": 600,
"y": 120,
"wires": [
[
"85eae35e.3f8b9"
]
]
},
{
"id": "a1552db2.f9daa",
"type": "http in",
"z": "25e49fa5.4ce1e",
"name": "",
"url": "/api/v1/nmi",
"method": "post",
"upload": false,
"swaggerDoc": "",
"x": 140,
"y": 200,
"wires": [
[
"c1e67f2.6805a8"
]
]
},
{
"id": "c1e67f2.6805a8",
"type": "change",
"z": "25e49fa5.4ce1e",
"name": "眠いパラメータ",
"rules": [
{
"t": "set",
"p": "prePayload",
"pt": "msg",
"to": "payload",
"tot": "msg"
},
{
"t": "set",
"p": "target_channel",
"pt": "msg",
"to": "3p161y7tjfdh9yb4ckbg6zoe7c",
"tot": "str"
},
{
"t": "set",
"p": "target_channel_name",
"pt": "msg",
"to": "pjct_nmi",
"tot": "str"
},
{
"t": "set",
"p": "target_icon_url",
"pt": "msg",
"to": "/api/v4/emoji/8gt67j1p9pnq7xbk1gh6xuxtir/image",
"tot": "str"
},
{
"t": "set",
"p": "target_icon_name",
"pt": "msg",
"to": "NMI調査団",
"tot": "str"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 360,
"y": 200,
"wires": [
[
"bf6d4801.a6ac88"
]
]
}
]
「手動で変更が必要な場所」
赤丸でくくった場所にURLなりアクセストークンなりを記載してくれれば動作する(はず)です。
最後に
当初の想定と違い、吸い込むことが目的になりつつあったりもしますが、業務中の息抜きとしては最高です。
ぜひ、あなたのチャット環境にもブラックホールを導入しましょう!
そうすれば眠いときでも、仲間と共に楽しく頑張って仕事が出来るはずです。
Enjoy Working!!