ElixirでSlackBotを簡単に作れるライブラリを作ったよ


SlackのBotを簡単に構築できるライブラリを作りました。

私が調べた限り、ElixirにはSlackのBotを作るライブラリとして、
[Elixir-Slack](https://github.com/BlakeWilliams/Elixir-Slack)、[slacker](https://github.com/koudelka/slacker)がありますが、求めているものとしてこれでは足りなかったためslackerの上に構築しました。

それがこちらです。
[Sloth](https://github.com/tamai/sloth)

使い方と構成、仕組みの解説をしたいと思います。

## HowTo

```
defmodule Echo do
  use Sloth.Plugin

  plugin ~r/^echo (.*)$/, :echo

  def echo(send_data, captures \\ []) do
    Sloth.Slacker.say(send_data["channel"], List.first(captures))
  end

end
```

上記sampleは、`echo ほげほげ`のような発言に対して、
`ほげほげ`と返すBotです。

`use Sloth.Plugin`を記述することで`plugin/2`マクロを使用できるようになります。
`plugin/2`マクロには、Botに反応させたいテキストの正規表現と、
その正規表現にマッチした際に実行するメソッドを渡します。
sampleでは`~r/^echo (.*)$/`と`:echo`となっています。

`echo`メソッドは`send_data`（slackでの発言の情報)と`captures`(正規表現のキャプチャ)を引数に受け取ります。
`send_data`内には発言されたチャンネル、発言したユーザーのID、発言のテキスト、発言の時間が含まれています。
それらを元に`Sloth.Slacker.say/2`メソッドでSlackに発言します。

Elixirを触ったことがない人にもElixirを触るきっかけとして使ってもらうために、
上記sampleが動く過程も解説します。内部構成と仕組みをさっさと説明しろ。という方はスクロールしてください。

Elixirのインストールについては、Qiitaの別記事、公式サイトをご確認ください。

elixirがインストールできたら先に進みます。

```
mix new echo
cd echo
```

mixコマンドを使用することでelixirのプロジェクトを作成できます。

```lib/echo.ex
ここに、先ほどのsampleを書く。
```

mixにはライブラリの依存を取得してくれる機能もあります。
RubyでいうBundlerです。

```mix.exs
def deps do
  [
    {:websocket_client, github: "jeremyong/websocket_client"},
    {:slacker,  "~> 0.0.1"},
    {:sloth, "~> 0.0.2"}
  ]
end
```

slothは、`websocket_clint`と`slacker`に依存しています。
`websocket_client`はerlangで記述されているものです。
elixirはerlangの上に構築されている言語のためerlangの資産を活用することができます。

```
mix deps.get
```

上記コマンドで依存ライブラリを取得します。

```mix.exs
def application do
  [applications: [:logger, :websocket_client, :slacker, :sloth]]
end
```

アプリケーションの起動時に読み込む（実行する）ライブラリを指定します。

```
export SLACK_TOKEN=xxxxxxxxxxxxxxxx
```

Slackのbotのトークンを環境変数に設定します。
Botの作成はこちらから行ってください。
https://my.slack.com/services/new/bot

bot作成後、botを常駐させたいチャンネル、グループにbotを招待してください。

```
ies -S mix

Echo.start_link
```

上記コマンドでアプリを立ち上げます。
これで、slackへ接続できた旨のメッセージが流れればOKです。
slack上で`echo ほげほげ`と発言してみてください。

# 内部構成と仕組み

![プロセス構成](https://qiita-image-store.s3.amazonaws.com/0/54939/b9d43c9a-4f14-1437-feb2-61ffc982e5b8.png "名称未設定_2.png")


Sloth.Supervisor 
-> Sloth.Slacker
-> Sloth.PlugManager
                 
1つの`supervisor`プロセスから、2つの`worker`を生成しています。
それぞれストラテジーとして`one_for_one`を指定しています。

Supervisorはプロセスの監視をしてくれるプロセスです。
workerプロセスが終了orクラッシュした際に検知し再起動などの動作を行います。
その動作が`one_for_one`です。
これは、workerがクラッシュしたらそのworkerのみ再起動するという動作です。
他にプロセスツリー全体を再起動などの動作があります。

## Sloth.Slacker

Sloth.Slackerはslackerを使用したもので、
slackのRTMAPIとの接続、RTMからのメッセージの受信、メッセージの送信、
取得したメッセージを正規表現解析して、マッチしたメソッドの実行をしています。
slackerはGenserverを実装しています。
RTMからメッセージ受信を`handle_cast`のcallbackメソッドで受け取ります。

```lib/sloth/slacker.ex
def handle_cast({:handle_incoming, type = "message", send_data}, state) do
  # 略
end
```

slothでは、RTMAPIのうち typeがmessageのもののみを扱うようにパターンマッチしていますが、
slackerを直接利用すれば他のイベントの受信に応じて処理を行うことも出来ます。
slothもそのうち拡張するかもしれません。

```lib/sloth/slacker.ex
def handle_cast({:handle_incoming, type = "message", send_data}, state) do
  Logger.debug "#{type} -> #{inspect send_data}"
  PlugManager.get_all
  |> Enum.each(fn({module, functions}) ->
    Enum.each(functions, fn({regex, func}) ->
      match = Regex.run(regex, send_data["text"])
      if match do
        [_text | captures] = match
        GenServer.cast(module, {:call_plugin, func, [send_data, captures]})
      end
    end)
  end)
  {:noreply, state}
end
```

PlugManagerの説明は後述しますが、
そこからpluginの一覧を取得して、ループしつつ、
受信したテキストにマッチするpluginを探します。
マッチしたらそのメソッドを呼び出しています。
pluginの実行を別プロセスにしたかったので、

```
GenServer.cast(module, {:call_plugin, func, [send_data, captures]})
```
GenServerのcastで処理を発火するようにしています。

## Sloth.PlugManager

名前の通り、pluginの管理をするモジュールです。

```lib/sloth/plug_manager.ex
defmodule Sloth.PlugManager do
  def start_link do
    Agent.start_link(fn -> %{} end, name: __MODULE__)
  end

  def add(module, plugin) do
    Agent.get_and_update(__MODULE__, fn(state) ->
      Map.get_and_update(state, module, fn(value) ->
        case value do
          nil -> {value, plugin}
          _ -> {value, value ++ plugin}
        end
      end)
    end)
  end

  def get(module) do
    Agent.get(__MODULE__, fn(state) -> Map.get(state, module) end)
  end

  def get_all do
    Agent.get(__MODULE__, fn(state) -> state end)
  end
end
```

pluginの管理方法として、Agentを使用しています。
やっていることは単純にAgentのラッパーです。
`start_link/0`でAgetnに初期値、空のMapを渡します。

`add/2`、`get/1`、`get_add/0`で
pluginの追加、取得を行います。

## Sloth.Plugin

最後は`plugin/2`マクロを提供する。Sloth.Pluginです。

```lib/sloth/plugin.ex
defmacro plugin(pattern, function) do
  quote do
    @plugin { unquote(pattern), unquote(function) }
  end
end
```

やっていることは単純で、plugin attributeに、パターンとメソッドを格納しています。
attributeは、配列になっていて、
`@plugin ~~~~`が複数あれば、配列の追加されていきます。

```lib/sloth/plugin.ex
defmacro __using__(_env) do
  quote do
    require Logger
    import Sloth.Plugin
    use GenServer

    @plugin []
    Module.register_attribute(__MODULE__, :plugin, accumulate: true)

    def terminate(reason, state) do
    end

    def handle_cast({:call_plugin, func, args}, state) do
      apply(__MODULE__, func, args)
      {:noreply, state}
    end

    def init(state) do
      {:ok, state}
    end

    @before_compile unquote(__MODULE__)
  end
end
```

`__using__/1`メソッドは、
`use Sloth.Plugin`により実行されます。

`import Sloth.Plugin`は、一見自身をimportしてどうするの？と思いますが、
この処理が実行されるコンテキストは、`use`マクロを使用しているモジュール側になるため、
sampleの場合だとEchoモジュールが`import Sloth.Plugin`を実行していることと同じです。
これで、`plugin/2`マクロを取り込んでいます。

ここで主にやっていることは、@plugin attributeの定義、
GenServerのcallbackメソッド、`handle_cast/2`、`terminate/2`、`init/1`の実装と、
`@before_compile`の定義です。

`terminate/2`については空のほうが、詳細がログ（コンソール）に出てたので、
空になってます・・・。よいやり方は正直わかってないです。

`handle_cast/2`は、Sloth.Slackerからのcastを受けているcallbackメソッドです。
引数に受けた、メソッドをそのまま呼んでいるだけです。

@before_compileは、compile前に最後？に呼ばれる処理の定義です。

```lib/sloth/plugin.ex
defmacro __before_compile__(env) do
  quote do
    def start_link do
      Sloth.PlugManager.add(__MODULE__, @plugin)
      GenServer.start_link(__MODULE__, [], name: __MODULE__)
    end
  end
end
```

@plugin attributeをそのままPlugManagerのAgentにぶっこんでます。
マップのkeyはモジュール名です。

![名称未設定_2.png](https://qiita-image-store.s3.amazonaws.com/0/54939/eac62f15-40a6-9ad8-d705-d75e8eddef15.png "名称未設定_2.png")

その後、Sloth.Slackerからのコールバックを受けるGenServerのプロセスを作成しています。

# まとめ
![名称未設定_2.png](https://qiita-image-store.s3.amazonaws.com/0/54939/fe93436c-24f6-01b2-a5fe-d493d95215f4.png "名称未設定_2.png")

pluginでメソッドを登録。
登録されたpluginの中でslackからのメッセージが正規表現にマッチするものを探す。
マッチしたメソッドを呼び出し。

やっていることは単純です。
単純ですが、ElixirのOTP周り、マクロのメタプログラミング。
この2つが受け持つ範囲、やれることを調べてこの構成にするまでに結構時間がかかりました。

現状はシンプルな機能ですが、
今後業務でのslackbotに使っていくので、
自分が欲しいと思った機能は追加されていくと思いますｗ

### 課題

Testがあんまりかけていないです。

# おまけ

[sloth_example](https://github.com/tamai/sloth_example)

slothを組み込んで、todoを作ったexampleです。
OTPのApplicationまで使っているので参考にしてもらえると幸いです。

issue,PullRequestも受け付けています！
よかったら使ってみてください:innocent: