Node.js Stream からいつの間にか Slack クライアントができた

  • Mastodon は観察専門なので、使用頻度の高い Slack を題材とします
  • 最初は Mastodon と同じく Slack のリアルタイムデータが流れ出てくる Stream を作ります
    • Legacy tokens | Slack から Authentication token を入手
    • rtm.connect method | Slack で token を渡して Real Time Messaging API の WebSocket エンドポイントを得る
    • WebSocket から受け取った message を stream に書き込み
    • stream から stdout に出力
let ws;
const url = "https://slack.com/api/rtm.connect?token=xoxp-0000000000-0000000000-000000000000-0123456789abcdef0123456789abcdef";
const stream = new require("stream").PassThrough();
require("https").get(url, res => {
  res.on("data", chunk => {
    const obj = JSON.parse(chunk);
    ws = new (require("ws"))(obj.url);
    ws.on("message", data => stream.write(data + "\n"));
  });
});
stream.pipe(process.stdout);
$ node index.js
{"type":"hello"}
{"type":"reconnect_url","url":"wss://mpmulti-0a1g.slack-msgs.com/websocket/ZvwORwLc1uFhgMHJAZva-04tZg2Lp5iJEclZ8vnOCctMTnJC64LQvb7MZvgJNifnscAGXvki2UAEcJpnlwFwCY031HGGe-NizIM5cZZKfdMQcO9kbXBhb8LkUKQnhR3bx-Z5hxefvQgsVQVgZwSFO9huhgXo3cBvjCc44YTZklY="}
{"type":"presence_change","presence":"active","user":"U5HCFCV5W"}
...
  • ここに、書き込むと WebSocket にデータを送る Writable Stream のコードを追加して stdin と繋ぎます
const writable = new require("stream").Writable({
  write: (chunk, encoding, callback) => {
    ws.send(chunk.toString());
  }
});
process.stdin.pipe(writable);
  • stdin へチャンネルにメッセージを投稿するための JSON を入力します
$ node index.js
...
{"type":"message","channel":"C5JT9C849","text":"test"}
  • 直後に成功を表す JSON が送られてきたら、ブラウザ等から確認すると反映されていることがわかります
{"ok":true,"reply_to":0,"ts":"1495991511.232093","text":"test"}
  • ここまでで、最もシンプルな Slack クライアントが完成しました
  • 送られてくる JSON は channel や user の情報が "C5JT9C849" のような id となっていて扱いづらく、よって name に変換する Transform Stream を作って間にはさみます
const url = "https://slack.com/api/rtm.start?token=xoxp-0000000000-0000000000-000000000000-0123456789abcdef0123456789abcdef";
const stream = new require("stream").PassThrough();
require("https").get(url, res => { ... });
const transform = new require("stream").Transform({
  transform: function(chunk, encoding, callback) { ... }
});
stream.pipe(transform).pipe(process.stdout);
{"type":"message","user":{"id":"U5HCFCV5W","name":"@asannou"},"text":"test","ts":"1496083043.865560","channel":{"id":"C5JT9C849","name":"#vip"}}
{"type":"message","user":{"id":"USLACKBOT","name":"@slackbot"},"text":"Hello, I’m Slackbot. I try to be helpful. (But I’m still just a bot. Sorry!) *Type something* and hit _enter_ to send your message.","ts":"1495732144.332015","channel":{"id":"D5J44CUDS","name":"@slackbot"}}
{"type":"message","user":{"id":"USLACKBOT","name":"@slackbot"},"text":"Pleasure to meet you. Let me show you a couple things about Slack.","ts":"1495732157.337778","channel":{"id":"D5J44CUDS","name":"@slackbot"}}
170525170904.332015 [@slackbot] (@slackbot) Hello, I’m Slackbot. I try to be helpful. (But I’m still just a bot. Sorry!) *Type something* and hit _enter_ to send your message.
170525170917.337778 [@slackbot] (@slackbot) Pleasure to meet you. Let me show you a couple things about Slack.
  • タイムスタンプ部分 "%y%m%d%H%M%S.%6N" が苦しいですが、生 JSON よりはマシでしょう
    • 出力のすべては JSON であるがゆえに、特定のチャンネルをフィルタリングするなど、jq の能力が発揮されます
$ node index.js | jq -c --unbuffered 'select(.channel.name != "#general")'
  • 入力についても、手打ち JSON は死ぬので jq に頼ります
$ echo "test" | jq -R -c --unbuffered '{type:"message",channel:{name:"@slackbot"},text:.}'
{"type":"message","channel":{"name":"@slackbot"},"text":"test"}
  • 入力と出力を jq で挟むことにより、ちょっとだけまともな Slack クライアントになりました
$ jq -R -c --unbuffered '{type:"message",channel:{name:"@slackbot"},text:.}' | node index.js | jq --unbuffered -r -f format.jq
help!
170528175842.970929 [@slackbot] (@asannou) help!
170528175842.970933 [@slackbot] (@slackbot) I can help by answering simple questions about how Slack works. I'm just a bot, though! If you need more help, try our <https://get.slack.help/hc/en-us/|Help Center> for loads of useful information about Slack ― it's easy to search! Or simply type */feedback* followed by your question or comment, and a human person will get back to you. :smile:
$ ./rwlap.js "jq -R -c --unbuffered '{type:\"message\",channel:{name:\"@slackbot\"},text:.}' | node index.js | jq --unbuffered -r -f format.jq"
  • Readline は、他にもタブキーによる入力補完もサポートしているため、公式クライアントのような、ユーザ名の補完なども実現可能です
$ ./rwlap.js "jq -R -c --unbuffered '{type:\"message\",channel:{name:\"@slackbot\"},text:.}' | node index.js | jq --unbuffered -r -f format.jq | ./highlight.js asannou"

170528175842.970929 [@slackbot] (@asannou) help!

  • もちろん、同様のことができるコマンドで、代替してもかまいません
  • その他いろいろ
    • チャンネルをステートフルに指定する
    • スレッドの対応
    • WebSocket で受け付けてもらえないリクエストを HTTPS で送って、結果だけ stream に流す
    • パイプで繋いだ各コマンドでバッファされないよう、無効にするか、ラインバッファのオプションを指定する
    • stdout からのデータは JSON として処理されてしまうので、補助的なデータは stderr から出力


  • 個人的に Slack の機能はこれで足りているので、しばらく使ってみようと思います