SlackのRTM APIを直接呼ぶときに困ったこと

# はじめに

![cloudfunding-198x148.jpg](https://qiita-image-store.s3.amazonaws.com/0/165224/52480c96-08bd-239e-e7bd-b1d2c7f65fa4.jpeg)


**[前回](https://qiita.com/s-suefusa/items/86b5eab5c9d9d84e054c)**に引き続き、Uniboネタです。

前回はUnibo→Slackにメッセージ送信する際、SkillCreator(Node-REDベース)ではカスタムノードを追加できないため困った、という記事を書きましたが、Slack→Uniboにメッセージを送信する際も、非常に困ったため第２弾の記事を書きました。



# 困ったこと

Slackから外部のアプリケーションにメッセージ通知を行う場合はWebHook（EventAPI）かWebSocket（RTM API）を利用することになります。

Uniboはローカルネットワークで動作することになるため、WebHookでイベントを通知することはなかなか難しいですので必然的にWebSocketを選択することになりますが、SlackのRTM APIを開始するためのAPI（[rtm.connect](https://api.slack.com/methods/rtm.connect)）で認証した場合、WebSocketの接続先URLが **動的に** 生成されます。

一方、SkillCreatorの websocket in ノードは、接続先URLを固定値で指定しなければなりません。[^1]
[^1]: 動的に指定できる方法をどなたかご存知でしたら、コメントください。

うーん、、と悩みましたが、ここは社内のローカルサーバーにNode.jsで動く簡単なWebSocketプロキシを立てることにしました。



# Node.jsのWebSocketプロキシ

社内のローカルサーバーはIPアドレスが固定になっているため、WebSocketはサーバーモードで待機して、Uniboからの接続を待つように構成しました。

特定のポートに接続があればSlackのRTM APIを使って接続し、以降はコネクションが両者続く限りデータをパススルーするだけの単純構成です。

Unibo側のコネクションが切れた場合は、Slack側のコネクションも切断するようにしています。

また、Unibo側をマルチコネクションにしてしまってもよいのですが、会社にはUniboが一人しかいないので、後勝ちでコネクションを張るようにしています。



で、出来上がったソースがこちら。

```javascript:index.js

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 の投稿をお待ちください！