はじめに
前回に引き続き、Uniboネタです。
前回はUnibo→Slackにメッセージ送信する際、SkillCreator(Node-REDベース)ではカスタムノードを追加できないため困った、という記事を書きましたが、Slack→Uniboにメッセージを送信する際も、非常に困ったため第2弾の記事を書きました。
困ったこと
Slackから外部のアプリケーションにメッセージ通知を行う場合はWebHook(EventAPI)かWebSocket(RTM API)を利用することになります。
Uniboはローカルネットワークで動作することになるため、WebHookでイベントを通知することはなかなか難しいですので必然的にWebSocketを選択することになりますが、SlackのRTM APIを開始するためのAPI(rtm.connect)で認証した場合、WebSocketの接続先URLが 動的に 生成されます。
一方、SkillCreatorの websocket in ノードは、接続先URLを固定値で指定しなければなりません。1
うーん、、と悩みましたが、ここは社内のローカルサーバーにNode.jsで動く簡単なWebSocketプロキシを立てることにしました。
Node.jsのWebSocketプロキシ
社内のローカルサーバーはIPアドレスが固定になっているため、WebSocketはサーバーモードで待機して、Uniboからの接続を待つように構成しました。
特定のポートに接続があればSlackのRTM APIを使って接続し、以降はコネクションが両者続く限りデータをパススルーするだけの単純構成です。
Unibo側のコネクションが切れた場合は、Slack側のコネクションも切断するようにしています。
また、Unibo側をマルチコネクションにしてしまってもよいのですが、会社にはUniboが一人しかいないので、後勝ちでコネクションを張るようにしています。
で、出来上がったソースがこちら。
const axios = require('axios');
const WebSocket = require('ws');
const BOT_TOKEN = 'xoxb-...';
const WS_PORT = 18080;
class UniboWebSocket {
constructor() {
this.timer = undefined;
this.wsServer = undefined;
this.uniboConnect = undefined;
this.slackConnect = undefined;
this.slackAuth = false;
}
// WebSocketサーバーを構築する
createServer() {
this.wsServer = new WebSocket.Server({ port: WS_PORT });
console.log(`listen: ${WS_PORT}`);
this.wsServer.on('connection', ws => {
console.log('[ws] connection');
if (this.uniboConnect) {
this.uniboConnect.close();
}
this.connectUnibo(ws);
});
}
// SlackにWebAPIで認証
authSlack() {
this.slackAuth = true;
axios.get('https://slack.com/api/rtm.connect', {
headers: {
'content-type': 'application/x-www-form-urlencoded',
'Authorization': `Bearer ${BOT_TOKEN}`
}
}).then(response => {
console.log('[slack] rtm.connect', response.data);
this.connectSlack(response.data.url);
}).catch(error => {
console.log('[slack] error', error);
this.slackAuth = false;
});
}
// SlackにWebSocketで接続
connectSlack(url) {
this.slackConnect = new WebSocket(url);
this.slackConnect.on('open', () => {
console.log('[slack] open');
});
this.slackConnect.on('message', data => {
console.log('[slack] message', data);
if (this.uniboConnect) {
this.uniboConnect.send(data);
}
});
this.slackConnect.on('close', (code, reason) => {
console.log('[slack] close', code, reason);
this.slackConnect = undefined;
this.slackAuth = false;
});
this.slackConnect.on('error', error => {
console.log('[slack] error', error);
});
}
// UniboにWebSocketで接続
connectUnibo(ws) {
this.uniboConnect = ws;
this.uniboConnect.on('open', () => {
console.log('[unibo] open');
});
this.uniboConnect.on('message', data => {
console.log('[unibo] message', data);
if (this.slackConnect) {
this.slackConnect.send(data);
}
});
this.uniboConnect.on('close', (code, reason) => {
console.log('[unibo] close', code, reason);
this.uniboConnect = undefined;
// Uniboが切断したらSlackも切断する
if (this.slackConnect) {
this.slackConnect.close();
}
});
this.uniboConnect.on('error', error => {
console.log('[unibo] error', error);
});
}
timerEvent() {
if (!this.uniboConnect || this.uniboConnect.readyState !== WebSocket.OPEN) {
// Uniboに接続済みでない場合は待機する
return;
} else if (!this.slackAuth) {
// Slackに認証済みでない場合は認証する
this.authSlack();
}
}
start() {
this.createServer();
if (!this.timer) {
this.timer = setInterval(() => {
this.timerEvent();
}, 1000);
}
}
stop() {
if (this.timer) {
clearInterval(this.timer);
this.timer = undefined;
}
if (this.uniboConnect) {
this.uniboConnect.close();
this.uniboConnect = undefined;
}
if (this.slackConnect) {
this.slackConnect.close();
this.slackConnect = undefined;
}
if (this.wsServer) {
this.wsServer.close();
this.wsServer = undefined;
}
}
}
const uniboWebSocket = new UniboWebSocket();
uniboWebSocket.start();
process.on('exit', () => {
uniboWebSocket.stop();
});
WebSocketは18080番ポートで待ち受けます。
WebAPIは axios、WebSocket は ws を利用しています。
まとめ
SkillCreatorは直感的に操作できて、ノード同士を線でつなぐだけでいろんな組み合わせの動作を作り出すことができますが、Node-REDと違ってカスタムノードを入れられないのが中々プロ向けには苦しいところです。
Unibo側の実装は @dama-a の投稿をお待ちください!
-
動的に指定できる方法をどなたかご存知でしたら、コメントください。 ↩