26
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

富士通システムズウェブテクノロジーAdvent Calendar 2019

Day 13

眠い人を吸い込むブラックホールチャンネルを作った話

Last updated at Posted at 2019-12-12

富士通システムズウェブテクノロジー Advent Calendar 2019 13日目の記事です。
実は、私はパートナー会社の者です。

書かせていただけるなんて、懐が深くて感謝の限りでございます!
本当にありがとうございます!!! :bow_tone1:

タイトルの通りネタ記事です。
正直こんな記事でごめんなさい。
(先に謝っておくスタイル)

ただ、前日の投稿
Google Cloud Vision APIを使って食べ物の写真を判定してみる - Qiita
が良記事過ぎて、書くのがつらいっす。。。

とりあえず全力で頑張りますのでよろしくお願いいたします!

そしてお約束です。
お断り)記事は全て個人の見解です。会社・組織を代表するものではありません。

はじめに

当記事では、チャットをより楽しくするためのお遊び機能を作ったので紹介しています。
真面目に読むよりは息抜き程度にお読みくださると幸いです。

私の職場環境

私が働いている場所はチャットが大変普及しています。
素晴らしいことに心理的安全性の高いチャットが実現されており、本音のトークがバンバンやり取りされています。
ともすると、ポジティブな内容ばかりではなく、ネガティブな発言もあったりする訳です。

私自身、仕事がしんどい時は「眠い」「帰りたい」などの発言をしてしまうこともあります。
そんなとき、ふと思うのです。

つらいのは私だけなのだろうか…と

いやいや、そんなはずはない。
きっと同じように苦しんで頑張っている人もいるはず!
皆で分かち合えば、辛さも軽減されて頑張れるはず!

ならば作りましょう!同士を集めるブラックホールを!

ブラックホールを支えるツール達

MattermostはOSSのチャットツールです。
Slackライクな使い心地でMarkdown形式が使えるのが強いところ。
個人的には、Slackより好きです。
詳しくはググってね :heart:

Node-REDはブラウザ上でGUIベースでロジックをゴリゴリ書ける面白ツールです。
ノードという形で様々な機能を持ったブロックをつなげることで、簡単にロジックが組めます。
APIとかも簡単に作れたりするので、Bot開発には最適です。
詳しくは(ry

ブラックホールの仕組み

「Mattermostのパブリックチャンネルで特定ワードを投稿する」(「眠い」「NMI」など)
仕組み-1.png

「ワードを検知したらNode-REDのロジック発火」
仕組み-2.png

「Node-REDのロジックで、強制的にブラックホールチャンネルに招待する」
仕組み-3.png

補足:
ブラックホールチャンネルは予め用意しておくこと
なるべく、プライベートチャンネルにしましょう
(仕事中に眠い人が集まるので、公開処刑になる可能性が0ではない)
引き込んだら歓迎しましょう

実際の様子

「夕方はNMI」
夕方はNMI

「昼もNMI」
昼もNMI

「仲間をふやそう」
仲間をふやそう

こんな感じで大盛況です。眠そうなターゲットを引き込めたときの嬉しさはすごいです。

ブラックホールの作り方

ここからは真面目に技術のお話です。

事前準備

  • Mattermostの管理者権限を取得
  • ブラックホールチャンネルを作成
  • Node-RED(Dockerなどを利用すればサクッと建てられます)

Mattemostで眠いを検知する機能を作る

眠いを検知する機能は、Mattermost統合機能の一つ外向きのウェブフックで実現しています。

Mattermostのメニューから、統合機能を作成します
image.png
(ここに表示されていない場合、管理者が禁止している可能性があります。その場合は管理者にネゴって機能を開放してもらう必要があります)

外向きのウェブフックを作成します
image.png
image.png

任意の名前、説明を記載します
image.png

チャンネルを選択しない
(選択しないことで、全てのパブリックチャンネルが対象になります)
image.png

補足するために思いつく限りの眠そうなワードを登録します
(腕の見せ所です)
image.png

投稿の文頭にこのワードがついていれば補足できるようにします
(正確に一致するにすると、ワードの後ろがスペースでないと反応しません)
image.png

Node-REDのロジックを叩く部分です。
Node-REDのホスト名+/api/v1/nmiという形で登録しておきます。
image.png

ここまでできたら外向きのウェブフック保存しましょう。

Mattemostで通知を受ける口を作る

今度はNode-REDからの通知を受け取る場所を、Mattermost統合機能の一つ内向きのウェブフックで実現します。

先程と同じ様に統合機能メニューを開き、今度は内向きのウェブフックを選択します
image.png
image.png

あとは、設定(名前、説明、デフォルト投稿チャンネル)を入力し、保存します。
このチャンネルに固定するのチェックを外しておけば、汎用的に使えます)
image.png

これで通知を受け取る口ができました。
Mattemostの設定はこれで以上です。

Node-REDで吸い込む機能を作る

全文書くと超大作になってしまうので、ある程度割愛します。。。っ!
(ロジックは後ほど全文載せます)

全体像を元に解説していきます。

「全体像」
image.png

MattermostからのWebhookを受け取るためにAPIを作成します。
Node-REDのホスト名/api/v1/nmiという名前でAPIを定義しています。

「眠い人がすでに眠いチャンネルに存在しているか判定する」
Node-RED全体像_判定.png

存在確認のノード(Node-REDはノードをつないでロジックを作る)でMattermostにAPIを投げています。
APIを実行するためにはアクセストークンが必要になるので、認証情報格納のノードでアクセストークンを詰めています。

「存在している場合は、通知するだけ」
Node-RED全体像_存在有.png

テキストセットのノードで「誰」が「どこ」で「なんと言っているか」を整理して、http requestのノードでMattermostの内向きWebhookを叩いて、Mattermostに投稿します。

「存在していない場合は、招待してから通知する」
Node-RED全体像_存在無.png

招待パラメータ設定のノードでMattermost用のAPIのパラメータを詰めて、招待を実施しています。
招待後、「誰」が「どこ」で「なんと言っているか」を整理して、Mattermostに投稿します。

これで準備は完了です。

Node-REDロジック全文

上の図を見てもわからない箇所も多いと思うので、全文を載せておきます。

「ソースコードを読み込むませることもできます」
image.png
「図のところに貼り付ける」
image.png

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なりアクセストークンなりを記載してくれれば動作する(はず)です。
image.png

最後に

当初の想定と違い、吸い込むことが目的になりつつあったりもしますが、業務中の息抜きとしては最高です。
ぜひ、あなたのチャット環境にもブラックホールを導入しましょう!

そうすれば眠いときでも、仲間と共に楽しく頑張って仕事が出来るはずです。
Enjoy Working!!

image.png
26
11
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?