この記事は ミクシィグループ Advent Calendar 2019 の7日目の記事です。
初めましての方は初めまして、19新卒エンジニアの techno-tanoC です。
最近、趣味で Slack の slash command を作っているので、 elixir / phoenix による入力された文字列をそのまま返す slash command (とオマケの隠し機能)を作るチュートリアルを書いてみます。
Web app を作る
早速 Elixir / Phoenix のプロジェクトを作っていきましょう。
Slack の slash command は json を受け取って json を返すことを基本としているため、 HTML や Webpack は必要ありません。 --no-html
, --no-webpack
を付けます。また、今回は DB を扱わないため、 --no-ecto
オプションも付けます。これらのオプションは名前の通り、 HTML, Webpack, Ecto をプロジェクトに含めないオプションです。
mix phx
のバージョンは 1.4.11
を使いました。
$ mix phx.new echo --no-html --no-webpack --no-ecto
* creating echo/config/config.exs
* creating echo/config/dev.exs
* creating echo/config/prod.exs
* creating echo/config/prod.secret.exs
* creating echo/config/test.exs
* creating echo/lib/echo/application.ex
* creating echo/lib/echo.ex
* creating echo/lib/echo_web/channels/user_socket.ex
* creating echo/lib/echo_web/views/error_helpers.ex
* creating echo/lib/echo_web/views/error_view.ex
* creating echo/lib/echo_web/endpoint.ex
* creating echo/lib/echo_web/router.ex
* creating echo/lib/echo_web.ex
* creating echo/mix.exs
* creating echo/README.md
* creating echo/.formatter.exs
* creating echo/.gitignore
* creating echo/test/support/channel_case.ex
* creating echo/test/support/conn_case.ex
* creating echo/test/test_helper.exs
* creating echo/test/echo_web/views/error_view_test.exs
* creating echo/lib/echo_web/gettext.ex
* creating echo/priv/gettext/en/LC_MESSAGES/errors.po
* creating echo/priv/gettext/errors.pot
Fetch and install dependencies? [Yn] y
* running mix deps.get
* running mix deps.compile
We are almost there! The following steps are missing:
$ cd echo
Start your Phoenix app with:
$ mix phx.server
You can also run your app inside IEx (Interactive Elixir) as:
$ iex -S mix phx.server
Route を追加
/
を slash command のパスとします。
defmodule EchoWeb.Router do
use EchoWeb, :router
pipeline :api do
plug :accepts, ["json"]
end
- scope "/api", EchoWeb do
+ scope "/", EchoWeb do
pipe_through :api
+ post "/", SlackController, :index
end
end
Controller を追加
slash command では入力したコマンドのテキスト部分( /echo hello
なら hello
の部分) がそのまま POST されるため、それを取り出して送り返せば良いことになります。
defmodule EchoWeb.SlackController do
use EchoWeb, :controller
def index(conn, %{"text" => text}) do
render(conn, "index.json", %{text: text})
end
end
View を追加
View で Slack へ返却する json を組み立てます。Block Kit Builder を参考にしながら、どんなレスポンスを返すのか考えると良いでしょう。
defmodule EchoWeb.SlackView do
use EchoWeb, :view
def render("index.json", %{text: text}) do
%{
blocks: [
%{
type: "section",
text: %{
type: "plain_text",
text: text,
emoji: true
}
}
]
}
end
end
Slash Command を作成・インストールする
slash command の作成・追加
Slack API: Applications | Slack の「Create New App」で新しい Slack App を作ります。
「Development Slack Workspace」にはこの Slack App を管理する Slack Workspace を指定しましょう。
続いて Add features and functionality から slash commands を選択、 Create New Command を選択します。
「Request URL」には先程作った Web app が動いているホスト + Port を指定しましょう。
Phoenix のデフォルトのポート番号は 4000 のため特に設定していなければ、ホスト名が myhost
なら http://myhost:4000/
です。
あとはこの slach command を自分のワークスペースにインストールします。
試す
インストールした slash command をテストします。 slash command のインストールができていれば /echo
と打てば候補に出ると思います。 Phoenix サーバを起動しておき、メッセージを入力してみましょう。
$ mix phx.server
[info] Running EchoWeb.Endpoint with cowboy 2.7.0 at 0.0.0.0:4000 (http)
[info] Access EchoWeb.Endpoint at http://localhost:4000
トークンの検証をする
このままでは slack ではない第三者からも echo コマンドを実行できてしまいます。これを防ぐために slash command のリクエストに含まれているトークンを検証することで正しいリクエストかどうかをチェックしましょう。 Plug
という機能を使って実装します。
defmodule EchoWeb.Plug.VerifyToken do
import Plug.Conn
def init(opts), do: opts
def call(conn, _opts) do
token = Application.get_env(:echo, :slash_command_token)
unless conn.params["token"] == token do
conn
|> send_resp(403, "Invalid token")
|> halt()
end
conn
end
end
設定されたトークンとリクエストに入っているトークンが同じかどうかをチェックし、もし違えば 403
を返します。
あとは Router と Config に追記すれば OK です。
defmodule EchoWeb.Router do
use EchoWeb, :router
pipeline :api do
plug :accepts, ["json"]
+ plug EchoWeb.Plug.VerifyToken
end
scope "/", EchoWeb do
pipe_through :api
post "/", SlackController, :index
end
end
+ slash_command_token = System.get_env("SLASH_COMMAND_TOKEN") || raise "Set SLASH_COMMAND_TOKEN !!"
+ config :echo, :slash_command_token, slash_command_token
隠し機能を作る
/echo
コマンドですが、隠し機能として猫の画像を見る機能を付けてみましょう。 Cat as a service (CATAAS) を使えばランダムな猫の画像や、テキスト付きの猫の画像を見ることができます。素晴らしいサービスですね。
今回は /echo cat
でランダムな猫の画像、 /echo cat hello
で hello という文字の入った猫の画像を表示するという仕様で改造していきます。
defmodule EchoWeb.SlackController do
use EchoWeb, :controller
+ def index(conn, %{"text" => "cat"}) do
+ render(conn, "cat.json", %{message: nil})
+ end
+
+ def index(conn, %{"text" => "cat " <> message}) do
+ render(conn, "cat.json", %{message: message})
+ end
+
def index(conn, %{"text" => text}) do
render(conn, "index.json", %{text: text})
end
elixir の文字列のパターンマッチを使うことでとても簡単に実装することができました。 与えられた text
が "cat"
なら1つ目の節、 text
が "cat "
から始まるなら2つ目の節、それ以外ならば3つ目の節にマッチします。
あとは画像の URL を含むメッセージを返せば良いだけです。
defmodule EchoWeb.SlackView do
use EchoWeb, :view
+ def render("cat.json", %{message: nil}) do
+ %{
+ blocks: [
+ %{
+ type: "image",
+ title: %{
+ type: "plain_text",
+ text: "cat",
+ emoji: false
+ },
+ image_url: "https://cataas.com/cat?width=200",
+ alt_text: "cat image"
+ }
+ ]
+ }
+ end
+
+ def render("cat.json", %{message: message}) do
+ %{
+ blocks: [
+ %{
+ type: "image",
+ title: %{
+ type: "plain_text",
+ text: "message cat",
+ emoji: false
+ },
+ image_url: "https://cataas.com/cat/says/#{message}?width=200",
+ alt_text: "message cat image"
+ }
+ ]
+ }
+ end
+
def render("index.json", %{text: text}) do
%{
blocks: [
%{
type: "section",
text: %{
type: "plain_text",
text: text,
emoji: true
}
}
]
}
end
end
試しに /echo cat LGTM
を実行してみると良い感じの猫の画像が出ました。これでいつでも猫の画像を見ることができますね。
所感
- Elixir / Phoenix を使うことで簡単に slash command を作ることができた
- Elixir のパターンマッチのおかげで直観的に理解しやすい分岐を書くことができた
- phoenix の view を使うことで
Map
を返す関数を書けば json として返してくれて楽だった - Plug を使うことでシンプルにトークンの検証を行うことができた
ちなみにこの後コンテナ化して Cloud Run に乗せてみました。初回呼び出し時には slash command のタイムアウトである 3000ms にひっかかりましたが、それ以降はそれなりに安定してレスポンスを返えすことができていました。