ElixirでSlackのbotを作ってHerokuで動かしてみる

- やってみたら結構簡単にできたのでメモ。[Elixir-Slack](https://libraries.io/hex/slack)を使いました。このページに書いてある通りに進めます。
- 2015/09/28 大幅にコード書き換えたので記事も合わせて修正しておきます。

## 準備
- Slackは事前にbotを作ってAPIキーを用意してください。
- とりあえずElixirのアプリケーションを作成。今回はElixirなんでFFっぽい名前でマジックボットで作ってます。

```
> mix new magic_bot
```

- cd してmix.exsのapplicationとdepsを編集。終わったらmix deps.getしとく。

```ex:mix.exs
  def application do
    [applications: [:logger, :slack],
    mod: {MagicBot, []}]
  end

  defp deps do
    [
      {:websocket_client, github: "jeremyong/websocket_client"},
      {:slack, "~> 0.2.0"}
    ]
  end
```

- configにAPIキーの情報を追加

```ex:config/config.exs
use Mix.Config
config  :MagicBot,
  api_key: "BotのAPIキー"
```

## スクリプト作成
### Slackのコネクションを管理するモジュール
- slackのコネクションをstateでもち回し続けるプロセス。メッセージを受け取ったらBotAction.Supervisorの下にメッセージに対応したスクリプトを実行するプロセスを新しく作成する。

```ex:lib/magic_bot/bot.ex
defmodule MagicBot.Bot do

  use Slack

  def handle_connect(slack, state) do
    # 接続に成功したら呼ばれるコールバック
    IO.puts "Connected as #{slack.me.name}"
    {:ok, state}
  end

  def handle_message(message = %{type: "message", text: _}, slack, state) do
    # メッセージを受け取ったら呼ばれるコールバック
    trigger = String.split(message.text, ~r{ |　})
    
    case String.starts_with?(message.text, "<@#{slack.me.id}>: ") do
      # botにメンション飛ばしたらrespondでプロセス作成
      true -> BotAction.Supervisor.start_action(state[:sup_action], :respond, Enum.fetch!(trigger, 1), message, slack)
      # それ以外はhearでプロセス作成
      false -> BotAction.Supervisor.start_action(state[:sup_action], :hear, hd(trigger), message, slack)
    end
    {:ok, state}
  end

  def handle_message(_message, _slack, state) do
    {:ok, state}
  end
end
```

### Slackから受け取ったbotのスクリプトのプロセスを管理するsupervisor
- Slackのプロセスから`start_action/1`が呼ばれ、このTask.Supervisor配下に新しいプロセスを作成する。
- Task.Supervisorの子プロセスの戦略は `simple_one_for_one` になり、動的に追加できる。
- クラッシュ時は子プロセスは再起動されない

```ex:lib/bot_action/supervisor.ex
defmodule BotAction.Supervisor do

  def start_link(opts \\ []) do
    Task.Supervisor.start_link(opts)
  end

  def start_action(supervisor, command, trigger, message, slack) do
    Task.Supervisor.start_child(supervisor, fn ->
      case command do
        :respond -> BotAction.Action.respond(trigger, message, slack)
        :hear -> BotAction.Action.hear(trigger, message, slack)
      end
    end)
  end

end
```

### 実際にBotに何かさせたいスクリプトを書く部分
- hubotのhearとrespondっぽい感じで実装してます。@bot名 hogeで来たらrespond/3をそれ以外はhear/3が動くようになっています。
- パターンマッチで合うものがあれば動いて、なければ何もせずに終わります。

```ex:lib/bot_action/action.ex
defmodule BotAction.Action do

  def hear("hear?", message, slack) do
    send_message("I can hear", message.channel, slack)
  end

  # Don't remove. This is default pattern.
  def hear(_, _, _) do
  end

  def respond("respond?", message, slack) do
     send_message("I can respond", message.channel, slack)
  end

  # Don't remove. This is default pattern.
  def respond(_, _, _) do
  end
end
```

### 全体を管理するSupervisor

```ex:lib/magic_bot/bot.ex
defmodule MagicBot.Supervisor do

  use Supervisor

  def start_link(opts \\ []) do
    Supervisor.start_link(__MODULE__, :ok, opts)
  end

  @bot_name MagicBot.Bot
  @action_sup_name BotAction.Supervisor

  def init(:ok) do

    # API キーを取得。環境変数から取れなければconfigから取得。
    api_key = case System.get_env("MAGICBOT_API_KEY") do
      nil -> Application.get_env(:MagicBot, :api_key)
      s -> s
    end

    # アプリケーションのSupervisorで管理する子プロセスを作成
    children = [
      supervisor(BotAction.Supervisor, [[name: @action_sup_name]]),
      worker(MagicBot.Bot, [api_key, [name: @bot_name, sup_action: @action_sup_name]])
    ]

    # 戦略は `one_for_one`でSlackとアクションのsupervisorを起動
    supervise(children, strategy: :one_for_one)
  end

end
```

### 最後に起動用のモジュール
- `mix.exs` で指定したモジュール。MagicBotのSupervisorを開始する。

```ex:lib/magic_bot.ex
defmodule MagicBot do

  use Application

  def start(_type, _args) do
    MagicBot.Supervisor.start_link
  end

end
```

### 起動
-no-haltオプションを付けることでプロセスが回り続けるらしい。

```
mix run --no-halt
```

### 話しかけてみる

![スクリーンショット 2015-08-20 23.37.04.png](https://qiita-image-store.s3.amazonaws.com/0/25841/2e6d1ada-d00b-0add-b465-347c524535cd.png)

### 何かbotの動作を追加してみる

lgtmとつぶやいたら lgtm.in から lgtm用画像を取ってくるScriptを追加してみます。

```ex:lib/bot_action/action.ex
  require HTTPoison
  require Floki

  def hear("lgtm", message, slack) do
    HTTPoison.start
    case URI.encode("http://www.lgtm.in/g") |> HTTPoison.get do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> body
        |> Floki.find("#imageUrl")
        |> Floki.attribute("value")
        |> hd
        |> send_message(message.channel, slack)
      {_, _} -> nil
    end
  end
```

動かしてみる

![スクリーンショット 2015-08-21 19.16.39.png](https://qiita-image-store.s3.amazonaws.com/0/25841/3c0afcc7-fe48-899e-3f40-9267c089b854.png)

## Herokuにデプロイしてプロセスを永続化する
- 今はMacの中で動かしていますが、これだとMacを閉じたらBotは眠りについてしまうのでHerokuにデプロイしてElixirのプロセスを回してみます。
- GitHubと連携する体で進めるので、コードはGitHubにあげます。
- Herokuはあまり詳しくないので間違っていたら指摘ください。

### Herokuにアプリケーションを作成
- Herokuのビルドパックの一覧に[heroku-buildpack-elixir](https://github.com/HashNuke/heroku-buildpack-elixir)があるのでこれを使います。

```
> heroku create magic-bot --buildpack "https://github.com/HashNuke/heroku-buildpack-elixir.git"
```

### Herokuにデプロイする
- buildpackの説明に従って以下のファイルを作成
- データベースはいまのとこ使わないのでコメントアウト

```:elixir_buildpack.config
# Erlang version
erlang_version=17.5

# Elixir version
elixir_version=1.0.4

# Always rebuild from scratch on every deploy?
always_rebuild=false

# Export heroku config vars
# config_vars_to_export=(DATABASE_URL)
```

- Heroku用のBootファイル

```:Procfile
botworker: mix run --no-halt
```

- Heroku側の設定でMAGICBOT_API_KEYにConfigに書いているキーを設定。configはignoreに追加してcommit対象外にしとく。
- ここまでできたらCommitしてGithubにpush
- あとはHeroku側でGithubとの接続を有効にしてManualDeployして完了です。
- Automatic deploysを有効にすれば、以降はBotを編集してGithubにPushすれば自動でBotが更新されるようになります。

# 

## 今回のコード
https://github.com/rei-m/magic_bot