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

こんにちは、水津です。
この記事は、[Node-RED Advent Calendar 2017](https://qiita.com/advent-calendar/2017/nodered) 23日目の投稿です。

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

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

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

# 構成
今回は以下のような構成で進めます。
<img width="655" alt="スクリーンショット 2017-12-23 16.08.29.png" src="https://qiita-image-store.s3.amazonaws.com/0/142110/b8750562-4911-ca6b-d3d4-aa4add43720f.png">

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


# Node-REDで実装
SlackのSlash Command処理部分は、こんな感じのフローで実装しました。
<img width="890" alt="スクリーンショット 2017-12-23 16.19.54.png" src="https://qiita-image-store.s3.amazonaws.com/0/142110/f7bbec6c-b801-8e75-1b6e-b82f62d5220a.png">

- 処理フロー定義

```json
[
    {
        "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": []
    }
]
```


ユーザ認証部分は、こんな感じでフロー実装してます。
<img width="1005" alt="スクリーンショット 2017-12-23 16.29.16.png" src="https://qiita-image-store.s3.amazonaws.com/0/142110/4bd569fa-fae0-3893-1bd1-b9c72ff5b003.png">

Slackの認証キー設定部分はSlackの以下の値を設定ください。
<img width="1132" alt="スクリーンショット 2017-12-23 16.45.14.png" src="https://qiita-image-store.s3.amazonaws.com/0/142110/2978d69e-26b9-eeb0-9ca9-7886c00eb14b.png">


- 受信部分のフロー定義

```json
[
    {
        "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": []
    }
]
```

- 処理部分のフロー定義

```json
[
    {
        "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
<img width="850" alt="スクリーンショット 2017-12-23 17.12.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/142110/845954a8-4736-e93b-4dbd-d35dcda6ae75.png">

- OAuth & Permissions
<img width="1036" alt="スクリーンショット 2017-12-23 17.12.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/142110/cde7a150-03b7-35fd-91c1-70d09ce17dc9.png">



# 使ってみる
使用感はこんな感じです。
![Untitled3.gif](https://qiita-image-store.s3.amazonaws.com/0/142110/63fbbd9b-92c4-35f5-6439-c6b43873a17c.gif)
これで絵文字をスタンプとして使うことが出来るようになりました。何かあったら、「たーのしー！」とアピールできます！

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