LoginSignup
55
53

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-08-20
  • やってみたら結構簡単にできたのでメモ。Elixir-Slackを使いました。このページに書いてある通りに進めます。
  • 2015/09/28 大幅にコード書き換えたので記事も合わせて修正しておきます。

準備

  • Slackは事前にbotを作ってAPIキーを用意してください。
  • とりあえずElixirのアプリケーションを作成。今回はElixirなんでFFっぽい名前でマジックボットで作ってます。
> mix new magic_bot
  • cd してmix.exsのapplicationとdepsを編集。終わったらmix deps.getしとく。
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キーの情報を追加
config/config.exs
use Mix.Config
config  :MagicBot,
  api_key: "BotのAPIキー"

スクリプト作成

Slackのコネクションを管理するモジュール

  • slackのコネクションをstateでもち回し続けるプロセス。メッセージを受け取ったらBotAction.Supervisorの下にメッセージに対応したスクリプトを実行するプロセスを新しく作成する。
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 になり、動的に追加できる。
  • クラッシュ時は子プロセスは再起動されない
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が動くようになっています。
  • パターンマッチで合うものがあれば動いて、なければ何もせずに終わります。
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

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を開始する。
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

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

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

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

Herokuにデプロイしてプロセスを永続化する

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

Herokuにアプリケーションを作成

> 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が更新されるようになります。

今回のコード

55
53
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
55
53