Bluemix
Slack
node-red
ibmcloud
Node-REDDay 23

Node-REDでSlackにスタンプを。

こんにちは、水津です。
この記事は、Node-RED Advent Calendar 2017 23日目の投稿です。

はじめに

お仕事のチーム内チャットツールとしてSlackを使ってるのですが、チームメンバーから「スタンプが使いたい!」と 「けものフレンズ」の画像セット とともにDMが。。。画像を見るとたしかにスタンプが欲しい! せっかくなので、IBM Cloud(旧Bluemix)上のNode-REDで、Slackのスタンプ機能を作ってみました。
@Kiguchi1902さん、アイコンありがとうございます!)

目標

ぐぐってみると、みんな考えることは同じようでいろんな事例がありました。それらを参考に今回は以下を目標に作成します。

  • Slackの絵文字機能(:emoji:)でスタンプを選べるようにする
    • スタンプってやっぱり絵を見ないと選べないですよね。ブラウザ拡張とか色んな方法がありそうですが、チームメンバーの使い勝手を考え、特定のクライアントだけではなく標準クライアントでも絵を見ながら選べるようにしたいと思います。
  • スラッシュコマンドでスタンプ投稿する
    • 絵文字が使いたい時もあります。ですので、絵文字を強制的にスタンプにするのではなく、スタンプ投稿したい時だけスタンプになるようにしたいと思います。
  • DMやプライベートチャンネルでも使用可能にする
    • やっぱりパブリックだけではなく、個人間でも使用したいですよね。ですので、どんなチャンネルにも同じようにスタンプ投稿できるようにしたいと思います。

構成

今回は以下のような構成で進めます。
スクリーンショット 2017-12-23 16.08.29.png

  • イベントは、Slash Commandで。:emoji:を引数にEVENT CALL。
  • EVENTが来たら、そのユーザが認証済が確認。未認証の場合は認証画面へ。認証を行い、認証情報をCloudantに保管する。
  • 認証済の場合は、ユーザの認証情報を使って絵文字を検索し、絵文字のURLを取得する。
  • 絵文字のURLをattachementsのimage_urlに設定し、ユーザとしてSlackにPOSTする。

Node-REDで実装

SlackのSlash Command処理部分は、こんな感じのフローで実装しました。
スクリーンショット 2017-12-23 16.19.54.png

  • 処理フロー定義
[
    {
        "id": "98e56880.114228",
        "type": "http in",
        "z": "4d7ee28a.f42664",
        "name": "",
        "url": "/stamp",
        "method": "post",
        "upload": false,
        "swaggerDoc": "",
        "x": 150,
        "y": 140,
        "wires": [
            [
                "457f8d1b.b45ffc",
                "a837be7.019124"
            ]
        ]
    },
    {
        "id": "457f8d1b.b45ffc",
        "type": "debug",
        "z": "4d7ee28a.f42664",
        "name": "",
        "active": false,
        "console": "false",
        "complete": "true",
        "x": 290,
        "y": 100,
        "wires": []
    },
    {
        "id": "a837be7.019124",
        "type": "function",
        "z": "4d7ee28a.f42664",
        "name": "変数等の設定",
        "func": "var emoji = msg.payload.text.replace(/:([^:]+):/, '$1');\n\nmsg.process =  {\n    \"token\" : \"\",\n    \"inputpayload\" :  msg.payload,\n    \"emoji\" : emoji,\n    \"emojiurl\" : \"\",\n    \"authurl\" : \"https://\" + msg.req.headers.host + \"/\" + msg.payload.team_domain + \"/auth\"\n};\n\nvar payloadMessage = {\n    '_id' : msg.payload.team_id + msg.payload.user_id\n};\n\nmsg.payload = payloadMessage;\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 320,
        "y": 140,
        "wires": [
            [
                "f5dcbca2.17d98"
            ]
        ]
    },
    {
        "id": "76dc7450.8a2bbc",
        "type": "http request",
        "z": "4d7ee28a.f42664",
        "name": "絵文字リスト取得",
        "method": "POST",
        "ret": "obj",
        "url": "https://slack.com/api/emoji.list",
        "tls": "",
        "x": 490,
        "y": 260,
        "wires": [
            [
                "75db5ce2.d61d4c"
            ]
        ]
    },
    {
        "id": "b3c026e1.a12118",
        "type": "function",
        "z": "4d7ee28a.f42664",
        "name": "絵文字API CALL MSG作成",
        "func": "msg.headers = {\n \"content-type\":\"application/x-www-form-urlencoded\"\n};\n\nmsg.process.token = msg.payload.userinfo.access_token;\n\nvar payload = {\n    \"token\" : msg.process.token\n};\n\nmsg.payload = payload;\n\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 260,
        "y": 260,
        "wires": [
            [
                "76dc7450.8a2bbc"
            ]
        ]
    },
    {
        "id": "75db5ce2.d61d4c",
        "type": "function",
        "z": "4d7ee28a.f42664",
        "name": "Slack POSTMSG作成",
        "func": "msg.headers = {\n \"content-type\":\"application/json\",\n \"Authorization\" : \"Bearer \" + msg.process.token\n};\n\nmsg.process.emojiurl = msg.payload.emoji[msg.process.emoji];\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 320,
        "y": 380,
        "wires": [
            [
                "c1491da8.46e84"
            ]
        ]
    },
    {
        "id": "7e43bfe1.b628e8",
        "type": "change",
        "z": "4d7ee28a.f42664",
        "name": "payload削除",
        "rules": [
            {
                "t": "delete",
                "p": "payload",
                "pt": "msg"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 830,
        "y": 160,
        "wires": [
            [
                "d3e6e223.92082"
            ]
        ]
    },
    {
        "id": "d3e6e223.92082",
        "type": "http response",
        "z": "4d7ee28a.f42664",
        "name": "",
        "statusCode": "",
        "headers": {},
        "x": 1010,
        "y": 140,
        "wires": []
    },
    {
        "id": "4517e326.db357c",
        "type": "http request",
        "z": "4d7ee28a.f42664",
        "name": "PostMSG",
        "method": "POST",
        "ret": "obj",
        "url": "https://slack.com/api/chat.postMessage",
        "tls": "",
        "x": 800,
        "y": 400,
        "wires": [
            [
                "638a7b13.a42cb4"
            ]
        ]
    },
    {
        "id": "638a7b13.a42cb4",
        "type": "debug",
        "z": "4d7ee28a.f42664",
        "name": "",
        "active": false,
        "console": "false",
        "complete": "false",
        "x": 950,
        "y": 400,
        "wires": []
    },
    {
        "id": "592845de.08a384",
        "type": "comment",
        "z": "4d7ee28a.f42664",
        "name": "SlackStampメインフロー",
        "info": "",
        "x": 150,
        "y": 60,
        "wires": []
    },
    {
        "id": "f5dcbca2.17d98",
        "type": "cloudant in",
        "z": "4d7ee28a.f42664",
        "name": "認証KEY取得",
        "cloudant": "",
        "database": "slackstamp",
        "service": "SlackStamp-cloudantNoSQLDB",
        "search": "_id_",
        "design": "",
        "index": "",
        "x": 490,
        "y": 140,
        "wires": [
            [
                "942a7d31.506d",
                "8d6150c.f56eeb"
            ]
        ]
    },
    {
        "id": "942a7d31.506d",
        "type": "debug",
        "z": "4d7ee28a.f42664",
        "name": "",
        "active": false,
        "console": "false",
        "complete": "payload",
        "x": 650,
        "y": 100,
        "wires": []
    },
    {
        "id": "8d6150c.f56eeb",
        "type": "switch",
        "z": "4d7ee28a.f42664",
        "name": "",
        "property": "payload",
        "propertyType": "msg",
        "rules": [
            {
                "t": "null"
            },
            {
                "t": "else"
            }
        ],
        "checkall": "true",
        "outputs": 2,
        "x": 630,
        "y": 140,
        "wires": [
            [
                "2b7f3e38.699b62"
            ],
            [
                "b3c026e1.a12118",
                "7e43bfe1.b628e8"
            ]
        ]
    },
    {
        "id": "2b7f3e38.699b62",
        "type": "function",
        "z": "4d7ee28a.f42664",
        "name": "初回MSG",
        "func": "msg.payload = msg.process.inputpayload;\nmsg.payload.text = \"[初回利用] SlackStampの認証をお願いします。 \" + msg.process.authurl;\n\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 820,
        "y": 120,
        "wires": [
            [
                "d3e6e223.92082"
            ]
        ]
    },
    {
        "id": "a6f32691.8f4a98",
        "type": "function",
        "z": "4d7ee28a.f42664",
        "name": "スタンプ",
        "func": "var postMessage = {\n    'token' : msg.process.token,\n    'channel' : msg.process.inputpayload.channel_id,\n    'as_user' : true,\n    'text' : '',\n    'attachments' : [{\n        'fallback' : \"スタンプを送信しました\",\n        'color': '#fff',\n        'text' : '',\n        'image_url': msg.process.emojiurl,\n    }]\n};\n\nmsg.payload = postMessage;\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 640,
        "y": 400,
        "wires": [
            [
                "4517e326.db357c"
            ]
        ]
    },
    {
        "id": "c1491da8.46e84",
        "type": "switch",
        "z": "4d7ee28a.f42664",
        "name": "",
        "property": "process.emojiurl",
        "propertyType": "msg",
        "rules": [
            {
                "t": "null"
            },
            {
                "t": "else"
            }
        ],
        "checkall": "true",
        "outputs": 2,
        "x": 490,
        "y": 380,
        "wires": [
            [
                "15b18302.19d1ed"
            ],
            [
                "a6f32691.8f4a98"
            ]
        ]
    },
    {
        "id": "15b18302.19d1ed",
        "type": "function",
        "z": "4d7ee28a.f42664",
        "name": "絵文字",
        "func": "var postMessage = {\n    'token' : msg.process.token,\n    'channel' : msg.process.inputpayload.channel_id,\n    'as_user' : true,\n    'text' : msg.process.inputpayload.text\n};\n\nmsg.payload = postMessage;\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 630,
        "y": 360,
        "wires": [
            [
                "4517e326.db357c"
            ]
        ]
    },
    {
        "id": "e71ec857.3696f8",
        "type": "comment",
        "z": "4d7ee28a.f42664",
        "name": "KEY取得できなかった場合",
        "info": "",
        "x": 870,
        "y": 80,
        "wires": []
    },
    {
        "id": "20ca5eed.fa0baa",
        "type": "comment",
        "z": "4d7ee28a.f42664",
        "name": "KEY取得できた場合",
        "info": "",
        "x": 850,
        "y": 200,
        "wires": []
    },
    {
        "id": "8b1a4764.40de68",
        "type": "comment",
        "z": "4d7ee28a.f42664",
        "name": "絵文字リストになかった場合",
        "info": "",
        "x": 700,
        "y": 320,
        "wires": []
    },
    {
        "id": "1a7f4b25.5c3fdd",
        "type": "comment",
        "z": "4d7ee28a.f42664",
        "name": "絵文字リストにあった場合",
        "info": "",
        "x": 690,
        "y": 440,
        "wires": []
    },
    {
        "id": "cf2a7262.a83f7",
        "type": "comment",
        "z": "4d7ee28a.f42664",
        "name": "SlackにMSGをPOST",
        "info": "",
        "x": 840,
        "y": 360,
        "wires": []
    },
    {
        "id": "a26249c9.be0c9",
        "type": "comment",
        "z": "4d7ee28a.f42664",
        "name": "Slack Slashコマンド受信",
        "info": "",
        "x": 190,
        "y": 180,
        "wires": []
    },
    {
        "id": "584517a8.0516b",
        "type": "comment",
        "z": "4d7ee28a.f42664",
        "name": "KEY取得できた場合",
        "info": "",
        "x": 230,
        "y": 300,
        "wires": []
    }
]

ユーザ認証部分は、こんな感じでフロー実装してます。
スクリーンショット 2017-12-23 16.29.16.png

Slackの認証キー設定部分はSlackの以下の値を設定ください。
スクリーンショット 2017-12-23 16.45.14.png

  • 受信部分のフロー定義
[
    {
        "id": "adf0765d.0394c8",
        "type": "comment",
        "z": "c800f3f2.664c2",
        "name": "認証 and CALLBACK",
        "info": "",
        "x": 140,
        "y": 60,
        "wires": []
    },
    {
        "id": "d952053d.29eee",
        "type": "http in",
        "z": "c800f3f2.664c2",
        "name": "",
        "url": "/xxxxxx/auth",
        "method": "get",
        "upload": false,
        "swaggerDoc": "",
        "x": 190,
        "y": 140,
        "wires": [
            [
                "ea2e07cc.96c32"
            ]
        ]
    },
    {
        "id": "ea2e07cc.96c32",
        "type": "change",
        "z": "c800f3f2.664c2",
        "name": "xxxxxx環境",
        "rules": [
            {
                "t": "set",
                "p": "process.client_id",
                "pt": "msg",
                "to": "111111111111.111111111111",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 370,
        "y": 140,
        "wires": [
            [
                "b4683e98.2024c"
            ]
        ]
    },
    {
        "id": "b4683e98.2024c",
        "type": "link out",
        "z": "c800f3f2.664c2",
        "name": "Stellvia認証画面",
        "links": [
            "3e01498f.c93fe6"
        ],
        "x": 475,
        "y": 140,
        "wires": []
    },
    {
        "id": "a61cbc82.432d3",
        "type": "http in",
        "z": "c800f3f2.664c2",
        "name": "",
        "url": "/xxxxxx/auth2",
        "method": "get",
        "upload": false,
        "swaggerDoc": "",
        "x": 190,
        "y": 180,
        "wires": [
            [
                "3d7f681c.ac032"
            ]
        ]
    },
    {
        "id": "3d7f681c.ac032",
        "type": "change",
        "z": "c800f3f2.664c2",
        "name": "xxxxxx環境",
        "rules": [
            {
                "t": "set",
                "p": "process.client_id",
                "pt": "msg",
                "to": "111111111111.111111111111",
                "tot": "str"
            },
            {
                "t": "set",
                "p": "process.client_secret",
                "pt": "msg",
                "to": "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 370,
        "y": 180,
        "wires": [
            [
                "bd2e5027.4ef288"
            ]
        ]
    },
    {
        "id": "bd2e5027.4ef288",
        "type": "link out",
        "z": "c800f3f2.664c2",
        "name": "Stellvia認証CB",
        "links": [
            "d96a6ba1.e083b8",
            "43050a3b.f12f64"
        ],
        "x": 475,
        "y": 180,
        "wires": []
    },
    {
        "id": "52d8e6cd.792608",
        "type": "comment",
        "z": "c800f3f2.664c2",
        "name": "xxxxxx",
        "info": "",
        "x": 130,
        "y": 100,
        "wires": []
    }
]
  • 処理部分のフロー定義
[
    {
        "id": "e9cbc54d.a5993",
        "type": "template",
        "z": "4d7ee28a.f42664",
        "name": "認証",
        "field": "payload",
        "fieldType": "msg",
        "format": "handlebars",
        "syntax": "mustache",
        "template": "<h2>SlackStamp認証</h2>\n\n<a href=\"https://slack.com/oauth/authorize?&client_id={{process.client_id}}&scope=chat:write:user,emoji:read,commands\">\n    <img alt=\"Add to Slack\" src=\"https://platform.slack-edge.com/img/add_to_slack.png\"/>\n</a>",
        "output": "str",
        "x": 210,
        "y": 580,
        "wires": [
            [
                "55985f1c.1f44b8",
                "3376116b.2c80be"
            ]
        ]
    },
    {
        "id": "55985f1c.1f44b8",
        "type": "http response",
        "z": "4d7ee28a.f42664",
        "name": "",
        "statusCode": "",
        "headers": {},
        "x": 350,
        "y": 580,
        "wires": []
    },
    {
        "id": "3376116b.2c80be",
        "type": "debug",
        "z": "4d7ee28a.f42664",
        "name": "",
        "active": false,
        "console": "false",
        "complete": "false",
        "x": 370,
        "y": 620,
        "wires": []
    },
    {
        "id": "ca03d033.294ca8",
        "type": "comment",
        "z": "4d7ee28a.f42664",
        "name": "SlackStamp認証画面",
        "info": "",
        "x": 140,
        "y": 520,
        "wires": []
    },
    {
        "id": "3e01498f.c93fe6",
        "type": "link in",
        "z": "4d7ee28a.f42664",
        "name": "認証画面Main",
        "links": [
            "74e2d59b.b494fc",
            "34f6e10.cd7cf2",
            "a70132ac.9346b",
            "916c5142.ddb6f8",
            "3d87be37.c756c2",
            "b4683e98.2024c"
        ],
        "x": 115,
        "y": 580,
        "wires": [
            [
                "e9cbc54d.a5993"
            ]
        ]
    },
    {
        "id": "c192089d.71dab8",
        "type": "comment",
        "z": "4d7ee28a.f42664",
        "name": "SlackStamp認証CALLBACK",
        "info": "",
        "x": 160,
        "y": 680,
        "wires": []
    },
    {
        "id": "34f94996.bae216",
        "type": "function",
        "z": "4d7ee28a.f42664",
        "name": "Slack 認証用MSG作成",
        "func": "msg.headers = {\n \"content-type\":\"application/x-www-form-urlencoded\"\n};\n\nvar postMessage = {\n    'client_id' : msg.process.client_id,\n    'client_secret' : msg.process.client_secret,\n    'code' : msg.payload.code,\n    'redirect_uri' : \"https://slackstamp.mybluemix.net\" + msg.req.route.path\n};\n\nmsg.payload = postMessage;\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 400,
        "y": 780,
        "wires": [
            [
                "a2592738.241d48",
                "22964517.421972"
            ]
        ]
    },
    {
        "id": "a2592738.241d48",
        "type": "http request",
        "z": "4d7ee28a.f42664",
        "name": "",
        "method": "POST",
        "ret": "obj",
        "url": "https://slack.com/api/oauth.access",
        "tls": "",
        "x": 590,
        "y": 780,
        "wires": [
            [
                "59f4086a.c69a2",
                "bba90c0f.57d8b8"
            ]
        ]
    },
    {
        "id": "59f4086a.c69a2",
        "type": "debug",
        "z": "4d7ee28a.f42664",
        "name": "",
        "active": false,
        "console": "false",
        "complete": "false",
        "x": 770,
        "y": 820,
        "wires": []
    },
    {
        "id": "1a719f9e.b56e78",
        "type": "cloudant out",
        "z": "4d7ee28a.f42664",
        "name": "認証KEY保存",
        "cloudant": "",
        "database": "slackstamp",
        "service": "SlackStamp-cloudantNoSQLDB",
        "payonly": true,
        "operation": "insert",
        "x": 990,
        "y": 780,
        "wires": []
    },
    {
        "id": "bba90c0f.57d8b8",
        "type": "function",
        "z": "4d7ee28a.f42664",
        "name": "認証KEY保存MSG作成",
        "func": "var payloadMessage = {\n    '_id' : msg.payload.team_id + msg.payload.user_id,\n    'userinfo' : msg.payload\n};\n\nmsg.payload = payloadMessage;\n\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 800,
        "y": 780,
        "wires": [
            [
                "1a719f9e.b56e78",
                "45584c57.4cbb14"
            ]
        ]
    },
    {
        "id": "45584c57.4cbb14",
        "type": "debug",
        "z": "4d7ee28a.f42664",
        "name": "",
        "active": false,
        "console": "false",
        "complete": "false",
        "x": 990,
        "y": 740,
        "wires": []
    },
    {
        "id": "a13414ff.e90568",
        "type": "debug",
        "z": "4d7ee28a.f42664",
        "name": "",
        "active": false,
        "console": "false",
        "complete": "payload",
        "x": 370,
        "y": 700,
        "wires": []
    },
    {
        "id": "4fda031c.5883bc",
        "type": "http response",
        "z": "4d7ee28a.f42664",
        "name": "",
        "statusCode": "",
        "headers": {},
        "x": 350,
        "y": 740,
        "wires": []
    },
    {
        "id": "ff8025dc.cb0458",
        "type": "function",
        "z": "4d7ee28a.f42664",
        "name": "整理用",
        "func": "\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "x": 210,
        "y": 740,
        "wires": [
            [
                "34f94996.bae216",
                "4fda031c.5883bc",
                "a13414ff.e90568"
            ]
        ]
    },
    {
        "id": "22964517.421972",
        "type": "debug",
        "z": "4d7ee28a.f42664",
        "name": "",
        "active": false,
        "console": "false",
        "complete": "false",
        "x": 590,
        "y": 740,
        "wires": []
    },
    {
        "id": "43050a3b.f12f64",
        "type": "link in",
        "z": "4d7ee28a.f42664",
        "name": "認証CBMain",
        "links": [
            "74ad72f8.db0abc",
            "d41b9cec.c285b8",
            "cb84cfc9.76e38",
            "b994c1ef.ac2a08",
            "b4a0bb69.d51938",
            "a866718e.bbf87",
            "bd2e5027.4ef288"
        ],
        "x": 115,
        "y": 740,
        "wires": [
            [
                "ff8025dc.cb0458"
            ]
        ]
    }
]

Slack側の設定

Slackの方は、新しいアプリをビルドします。名称はテキトウに「SlackStamp」等でもつけて下さい。機能は以下を設定下さい。設定後、チームにインストールください。

  • Slash Command
    スクリーンショット 2017-12-23 17.12.28.png

  • OAuth & Permissions
    スクリーンショット 2017-12-23 17.12.44.png

使ってみる

使用感はこんな感じです。
Untitled3.gif
これで絵文字をスタンプとして使うことが出来るようになりました。何かあったら、「たーのしー!」とアピールできます!

まとめ

「けものフレンズ」のアイコンをスタンプとして使いたいというメンバー要望がスタートでしたが、IBM Cloud上のNode-REDを使うことで、こんなにもカンタンに:emoji:をスタンプとして使うことができるようになりました。Slackでのコミュニケーションが賑やかになり、今まであまり投稿してくれなかったメンバーも使ってくれるようになりました。
今は、けものフレンズの他に、エンジニアを褒めるネコのアイコンも追加し、よりいっそう活発なチームになってます!(スタンプラッシュの副作用も出てますが。。。)
@kobaka7さん、アイコンありがとうございます!)