Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

cloudfunding-198x148.jpg

前回に引き続き、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が一人しかいないので、後勝ちでコネクションを張るようにしています。

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

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


  1. 動的に指定できる方法をどなたかご存知でしたら、コメントください。 

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away