SlackBotをGUIでポチポチしていい感じにするツールを作ってる

年末年始の休みぐらいから、SlackBotをGUIでポチポチするだけでいい感じに作れるようなツールを作ってる。

画面でポチポチ作ると

f:id:mugi1:20180128144304p:plain

こんな感じで動く

f:id:mugi1:20180128144705p:plain

とりあえずある程度動くようになったのでリポジトリ公開しといた

github.com

動機

SlackBotについて考えてみたときに、以下のようなことだけ簡単に実現できて自由に組み合わせることが出来れば、全部じゃなくとも80%くらいのやりたいことは実現できるんじゃね?とある日思った。

  • 実行のトリガー
    • 何らかのキーワードで実行(@bot 次の予定 みたいな)
    • スケジュールして一定時間ごとに実行
    • 外部からのWebRequestで処理を実行
  • トリガーで処理すること
    • どっかにWebRequestを投げて結果をもらう
  • 最後にやること
    • どっかにWebRequestを投げる(投げ捨てる)
    • 何か結果をチャンネルに返す

すでにBotを構築するツールはいっぱいあると思うけど、GUIでポチポチやって簡単に作れたら楽だな〜というのと、なんでもいいのでNuxt.jsを使ってなんか作りたかった、というのがあって、作ってみることにした。

使ったもの

  • Nuxt.js
  • express : APIサーバ兼、Nuxt.jsの動作用
  • mongo : データ保存用。雑にmongooseでアクセスしてる
  • element-ui : 見た目はこいつに任せてる
  • axios : 外部へのWebRequest発行用
  • node-schedule : スケジュール実行用

でだいたい動いてる。

深い意味や理由はまったくないが、全部jsで書いてみようと思ったのでexpressとかを使ってる。

概念的な話

以下のような名前をつけて取り扱ってる

  • Flow : 一連の処理の流れ
  • Trigger : Flowを起動するためのきっかけとなるもの。Flowにつき1つ。
  • Action : Triggerによって実行される一連の処理。Flowにつき任意数。
  • Finisher : Flowの最後にやりたい処理。Flowにつき1つ。

ActionやFinisherは1つまえに実行されたものから何らかのパラメータを受け取って自由に扱う。

たとえば上に貼った画像の例ではToyama.rbのイベント情報をチャンネルに返してるが、これはActionで行ったaxiosのレスポンスが次のFinisherに渡されてるので可能になってる。

(ちなみにFinisherではlodash#template形式で雑にテンプレートを書けるようにしてみた)

Slackへの依存をいかに切り出すか

Slack以外のツールにも対応させたいな〜みたいな思いがあったので、Slackに依存する部分とのやりとりはすべて events 経由で間接的に行ってる。

具体的には、Botの動作全体を管理するクラスでは

module.exports = class Bot {
  constructor (client) {
    this.client = client;
  }

  start (configuration) {
    this.client.start(configuration);
  }

  stop () {
    this.client.stop();
  }

  bindMessageTriggerFlow (flow) {
    this.client.on('messageReceived', (data) => {
      if (!data.actionName || data.actionName !== flow.trigger.data.word) return;

      this.executeAllActions(flow, data);
    });
  }


  ...

みたいにして、client がSlackかどうかは関係なく、特定のinterfaceを持ってるかどうかだけ気にするようにしてみた。

client自体は

const EventEmitter = require('events').EventEmitter;

module.exports = class BaseClient extends EventEmitter {
  start (state) {
    throw new Error('not implements error');
  }

  stop () {
    throw new Error('not implements error');
  }

  emitMessageReceived (message = {}) {
    this.emit('messageReceived', message);
  }

  sendMessage () {
    throw new Error('not implements error');
  }
};

みたいなBaseクラスを継承させていて、Chatサービスに依存する実装はサブクラスに丸投げしてる。

定期実行の方法

cron形式で定期実行できるようにしたかったんだけど、その実現に node-schedule を利用させてもらった。

github.com

さくっと動いて便利だった。

気をつけないといけなかったのが、当たり前だけど、Botを止めてもScheduleは止まらない。そのあたりの実装忘れてて、テスト用チャンネルが地獄みたいなことになってた。

f:id:mugi1:20180128152458p:plain

これから作ろうと思ってるもの

  • Herokuのデプロイボタン置く
  • Finisherの概念って実はいらないのでActionに統合する
    • 冷静に考えると最後のActionをFinisherにすればそれでいいので、Finisherなんていらんかったんや、ってこのエントリ書いてて気づいた。
  • ランダムなURLを発行して、それをTriggerにする
  • 認証
    • URLさえ知ってればokみたいになってるので、超簡易的な認証をつけときたい
  • 他のChatツールに対応したい
    • なんとかなると信じてる

まだ途中だけど、作ってる感想としてはNuxt.jsがとても楽でいい感じ。 router書かなくて良いのは最高です。