Elixirでslack botを作る

hubotでSlack botを作っていたときに思っていたことがある. この正規表現はかなりめんどくさい.Elixirのパターンマッチで書いたらもっと楽になるのではないだろうか,と(そう簡単な話でもなかったのだが).

というわけでElixirでslack botを作ってみようと思ったのだが,そんなに整備された道がなかったのでメモ.

どのライブラリを使うか

ライブラリを使わないという手もあるのだが,一応awesome-elixirを見るといくつか使えそうなやつのがあるので使ってみる.

ここで,Qiita記事とかを検索してしまったのだが,

qiita.com

Slothはエラーが出て全然使えなかった.

というか,Slack側が少しbot周りを整理しており,以前とbotの作り方が変わっていたりするかもしれない.

elixirの流行り廃りなのか,たとえばslackerあたりは,2016年で更新が止まっている. こういうライブラリを使うと後々苦労するので,たとえ日本語での情報が多かったとしても,枯れているライブラリを使うべきではない.

そんなわけでElixir-Slackを使ってみた.

github.com

こいつは2018年現在でも活発に開発されている.

自分でプロセスを作らないといけない

ただ,Elixir-SlackはSlothのように,「ここにメッセージのハンドリングを書けば動く!」というほど簡単にはできていない. READMEには簡単そうに書いてあるけど…….

github.com

そもそもElixir-Slackは,Slackとの認証,websocket接続あたりまでを請け負うものなので,それを動かすプロセスは自分で作る必要がある.

というわけで,Supervisorの出番だ.

defmodule SampleBot do

  use Application

  def start(_type, _args) do
    import Supervisor.Spec, warn: false

    children = [
      worker(SampleBot.Bot, [[]]),
    ]

    opts = [strategy: :one_for_one, name: SampleBot.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

で,子プロセスを作る.

defmodule SampleBot.Bot do

  use Slack

  def start_link(initial_state) do
    Slack.Bot.start_link(__MODULE__, initial_state, Application.get_env(:release_bot, :slack_token))
  end

  def handle_connect(slack, state) do
    IO.puts "Connected as #{slack.me.id}"
    {:ok, state}
  end

  def handle_event(message = %{type: "message", text: text}, slack, state) do
    send_message('pong', message.channel, slack)
    {:ok, state}
  end
  def handle_event(_, _, state), do: {:ok, state}

  def handle_info({:message, text, channel}, slack, state) do
    IO.puts "Sending your message, captain!"

    send_message(text, channel, slack)

    {:ok, state}
  end
  def handle_info(_, _, state), do: {:ok, state}
end

こいつを--no-halt フラグをつけて実行すればslack botが起動する. とりあえず,すべてのメッセージにpong を返すだけ.

$ mix run --no-halt

パターンマッチを使うには

さて,せっかくelixirを使っているんだからメッセージのハンドリングはパターンマッチを使いたい.

しかし,いくらelixirといえど,単なるStringに対してできることといえば,正規表現マッチくらいなものだ. これだとhubotのときとあまり変わらなく,ありがたみが少ない.

だが,これはもとが単なる文字列だからだ.

文字列を何かしらのデータ構造に落とし込めばいいのでは?

つまり,たとえば String.split とかして,単語ごとの文字列に分割するというのはどうだろう.

hello worldであるなら,

"hello world"
|> String.split(" ")

# => ["hello", "world"]

とすれば良いのでは?

というわけで,リプライ時のみ反応する形にしてみる.

  def handle_event(message = %{type: "message", text: text}, slack, state) do
    text
    |> String.split(" ")
    |> SampleBot.Handler.handle_message(message.channel, slack, "<@#{slack.me.id}>")
    {:ok, state}
  end
defmodule SampleBot.Handler do
  use Slack

  def handle_message([user_id, "ping"] = _text, channel, slack, user_id) do
    send_message("pong", channel, slack)
  end

  def handle_message([user_id | mes] = _text, channel, slack, user_id) do
    send_message("Hello world", channel, slack)
  end

  def handle_message(_, _, _, _) do
    :ok
  end
end

これでパターンマッチをそれなりに使った形でbotが作れるぞー.

作ったソースはこちら.

github.com