Slack ストーキング
- message event | Slack は、いくつかのサブタイプがあり、メッセージの変化を表現しています
- message_deleted message event | Slack は誰かがメッセージを削除した時に送られます
$ node index.js | jq --unbuffered 'select(.subtype=="message_deleted")'
- ユーザ @bsannou がチャンネル #twitter で "Despite the constant negative press covfefe" というメッセージを削除した様子です
{ "type": "message", "deleted_ts": "1497783180.894743", "subtype": "message_deleted", "hidden": true, "channel": { "id": "C5VCH7HFV", "name": "#twitter" }, "previous_message": { "type": "message", "user": { "id": "U5JPRSD6U", "name": "@bsannou" }, "text": "Despite the constant negative press covfefe", "ts": "1497783180.894743" }, "event_ts": "1497783204.895728", "ts": "1497783204.895728" }
- message_changed message event | Slack は誰かがメッセージを修正した時に送られます
$ node index.js | jq --unbuffered 'select(.subtype=="message_changed")'
- 様子です
{ "type": "message", "message": { "type": "message", "user": { "id": "U5JPRSD6U", "name": "@bsannou" }, "text": "あなたはとてもクサレ脳ミソですね", "edited": { "user": { "id": "U5JPRSD6U", "name": "@bsannou" }, "ts": "1497188145.000000" }, "ts": "1497188126.496907" }, "subtype": "message_changed", "hidden": true, "channel": { "id": "C5JT9C849", "name": "#vip" }, "previous_message": { "type": "message", "user": { "id": "U5JPRSD6U", "name": "@bsannou" }, "text": "あなたはとてもド低能ですね", "ts": "1497188126.496907" }, "event_ts": "1497188145.497849", "ts": "1497188145.497849" }
- これらを蓄積していくと Politwoops - All deleted tweets from politicians の Slack 版となります
- 公式クライアントでは、削除や修正が常に適用された状態で表示されますが、一旦送信した情報は取り消せないので、留意していきましょう
- 僥倖ですが Consuming streaming data — Twitter Developers とは違って、削除前の previous_message を含むため、あらかじめすべてのメッセージを保存しておく必要はありません
- Real Time Messaging API | Slack には、メッセージ以外にも有用なイベントが流れてきます
- presence_change event | Slack は誰かが active か away になった時に送られます
$ node index.js | jq --unbuffered 'select(.presence=="active") | .now=now'
- 様子
{ "type": "presence_change", "presence": "active", "user": { "id": "U5JPRSD6U", "name": "@bsannou" }, "now": 1497786620.436581 }
- 深夜に active がいるとオッとなる
- 出力を FIFO 経由で入力して…
- マイクロマネジメントし太郎
$ mkfifo fifo $ node index.js < fifo | jq --unbuffered -c 'select(.presence=="active") | {"type":"message","text":"進捗どうですか?activeだから居ますよね??","channel":{"name":.user.name}}' > fifo
- user_typing event | Slack は誰かがタイピングしている時に送られます
$ node index.js | jq --unbuffered 'select(.type=="user_typing")'
{ "type": "user_typing", "channel": { "id": "C5JT9C849", "name": "#vip" }, "user": { "id": "U5JPRSD6U", "name": "@bsannou" } }
- うざみ
$ mkfifo fifo $ node index.js < fifo | jq --unbuffered -c 'select(.type=="user_typing") | {"type":"message","text":("ちょっと待って!今 "+.user.name+" が何か言おうとしてる!"),"channel":{"name":.channel.name}}' > fifo
- 今日書きたいことはこれくらいです
Node.js Stream からいつの間にか Slack クライアントができた
- Node.js Stream 学習シリーズです
- Mastodon のタイムラインをターミナルに流す - 知らないけどきっとそう。 では Readable Stream の性質を使って Mastodon のタイムラインが流れ出てくるだけのものを作成しました
- 今回は Writable Stream も利用して、データを送り込めるようにします
- 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 を作って間にはさみます
- id と name のマップは rtm.connect method | Slack ではなく rtm.start method | Slack でリクエストすると送られてきます
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"}}
- 出力が JSON のままでは可読性が低いので、jq を使って整形します
{"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:
- 入力時にカーソル操作ができなかったり、メッセージが送られてくると割り込んで表示されるストレスは Readline | Node.js v10.9.0 Documentation を利用して解決します
- node-slacat/rwlap.js at master · asannou/node-slacat · GitHub のようなスクリプトを書いて、以下のとおりに使いましょう
$ ./rwlap.js "jq -R -c --unbuffered '{type:\"message\",channel:{name:\"@slackbot\"},text:.}' | node index.js | jq --unbuffered -r -f format.jq"
- Readline は、他にもタブキーによる入力補完もサポートしているため、公式クライアントのような、ユーザ名の補完なども実現可能です
- Slack といえば通知ですが、指定された文字列に色を付けると同時に BEL (\x07) を出力する node-slacat/highlight.js at master · asannou/node-slacat · GitHub を最後にパイプすると目的を達成できます
$ ./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!
- もちろん、同様のことができるコマンドで、代替してもかまいません
- その他いろいろ
- Slack には https://get.slack.help/hc/ja/articles/201727913 があるが、管理者に許可してもらう必要があり、セキュリティ上推奨もされていません
- 個人的に Slack の機能はこれで足りているので、しばらく使ってみようと思います
Mastodon のタイムラインをターミナルに流す
- Mastodon の各種タイムラインは WebSocket でストリーミングされている
- Node.js の Stream の学習を兼ねて、これを様々なターミナルに出力できるようにする
- 「Stream を制するものは、 Node.js を制す」らしい
- WebSocket で接続して https://friends.nico/ の連合タイムラインを受信
$ node -e 'new (require("ws"))("ws://friends.nico/api/v1/streaming/?access_token=...&stream=public").on("message", (data, flags) => console.log(data))' {"event":"update","payload":"{\"id\":1920985,\"created_at\":\"2017-04-24T16:10:03.248Z\",\"in_reply_to_id\":null,\"in_reply_to_account_id\":null,\"sensitive\":false,\"spoiler_text\":\"\",\"visibility\":\"public\",\"application\":{\"name\":\"Web\",\"website\":null},\"account\":{...
- これを parse() して Stream を作成し transform して出力
const stream = new require("stream").Transform({ objectMode: true, transform: function(chunk, encoding, callback) { ... } }); new (require("ws"))("ws://friends.nico/api/v1/streaming/?access_token=...&stream=public") .on("message", (data, flags) => stream.write(JSON.parse(data))); stream.pipe(process.stdout);
- Stream は TCP Socket に pipe() でつなぐことができる
- 標準入力の Stream を pipe() して、ポート 12345 に接続すると ping の結果が流れてくるサンプル
$ ping localhost | node -e 'process.stdin.resume();require("net").createServer(socket => process.stdin.pipe(socket)).listen(12345);'
$ telnet localhost 12345 Trying ::1... Connected to localhost. Escape character is '^]'. 64 bytes from 127.0.0.1: icmp_seq=16 ttl=64 time=0.065 ms 64 bytes from 127.0.0.1: icmp_seq=17 ttl=64 time=0.056 ms 64 bytes from 127.0.0.1: icmp_seq=18 ttl=64 time=0.095 ms 64 bytes from 127.0.0.1: icmp_seq=19 ttl=64 time=0.049 ms 64 bytes from 127.0.0.1: icmp_seq=20 ttl=64 time=0.054 ms ^] telnet> Connection closed.
- pipe() によって、何回でも Stream の分岐や合流が可能
- クライアントが複数接続しても、それぞれに同じデータを送ることができる
- resume() しておくことで pipe() されているものが何もない時のデータは捨てる
- これを利用して、同様にタイムラインが流れるようにした
- ここまでは順調だったが、あるクライアントが応答しなくなると、他のクライアントへの送信が止まる問題が発覚
- クライアントがデータを受け取らなくなり Socket の Stream のバッファがいっぱいになると、上流の Stream が pause() されるらしい
- pause() されそうになったら、下流を unpipe() することで解決
- その他
- まとめたものを GitHub - asannou/node-tootcat: Mastodon Timeline Stream として公開
$ telnet tootcat.0j0.jp
$ nc tootcat.0j0.jp 7007
$ telnet tootcat.0j0.jp 7008
-
- friends.nico の連合タイムラインから mstdn.jp のみを抽出
$ telnet tootcat.0j0.jp 7009
-
- friends.nico の連合タイムラインから pawoo.net のみを抽出
$ telnet tootcat.0j0.jp 7010
- ひとつの連合タイムラインを分岐しているので、サーバにブラウザ以上の負荷はかけない
- 連合タイムラインは all public posts from all users "known" to your instance であるため、インスタンスごとにフィルターしてもローカルタイムラインと等しくならない
- friends.nico のユーザにフォローされたユーザ、ブーストされたトゥートだけが現れる
- テキストデータなので MVNO の速度制限 (200kbps) がかかった状態でも、今のところストレスがない
- telnet が動くデバイスであれば利用可能
- 未加工の JSON を受信できるようにした
$ nc tootcat.0j0.jp 7006 | head -c 1000 {"id":6021698,"created_at":"2017-05-06T07:18:52.994Z","in_reply_to_id":null,"in_reply_to_account_id":null,"sensitive":false,"spoiler_text":"","visibility":"public","application":{"name":"Web","website":null},"account":{"id":0,"username":"username","acct":"acct","display_name":"display_name","locked":false,"created_at":"2017-04-21T12:07:59.324Z","followers_count":0,"following_count":0,"statuses_count":1,"note":"note","url":"https://friends.nico/url","avatar":"avatar","avatar_static":"avatar_static","header":"header","header_static":"header_static","nico_url":"nico_url"},"media_attachments":[],"mentions":[],"tags":[],"uri":"tag:friends.nico,2017-05-06:objectId=6021698:objectType=Status","content":"content1\r\ncontent2","url":"url","reblogs_count":0,"favourites_count":0,"reblog":null}{...
- jq などで、フィルタや加工できる
$ nc tootcat.0j0.jp 7006 | docker run -i --rm -e "TZ=Asia/Tokyo" asannou/jq --unbuffered -r '.prefix = (.created_at | sub(".[0-9]*Z";"Z") | fromdate | strflocaltime("%H:%M")) | .prefix += " (" + .account.username + ") " | .prefix as $prefix | select(.uri | test("^tag:friends.nico,")) | .content | split("\r\n") | join("\r\n" + $prefix) | $prefix + .' 16:18 (username) content1 16:18 (username) content2
-
- IRC 風
- strflocaltime がまだ development version のみの機能だったので asannou/jq を作った
- --unbuffered で滞りなく出力される