(この記事は「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」の17日目です)
昨日は@callmekoheiさんのBashScript を Elixir で書き直してみたっ(2倍速〜)でした!
はじめに
今回はGigalixirでSlackのBotを動かす方法(導入部)についてです。
自分は今年の8月頃にElixirを学び始めたのですが、そのきっかけがこのSlackのBotを作ることでした。
Elixirの魅力を伝えるにはあまりいい題材ではないかもしれないですが、ElixirでもPaaSを使って簡単にサービスを作れる点で取り組みやすい題材なので、記事にしてみました。
Gigalixirって何?という方はfukuoka.ex代表の@piacereさんの記事を見て頂くと非常にわかりやすいです。
https://qiita.com/piacere_ex/items/1a9cbcc740ca3707eaec6
(参考URLは記事の最後にまとめています。)
動作環境
- Elixir 1.5.1(Gigalixirのデフォルトのバージョン)
- Erlang 20.0(Gigalixirのデフォルトのバージョン)
- 他依存関係は下記の通り(mix.exsのdeps定義)
{:phoenix, "~> 1.3.0"},
{:phoenix_pubsub, "~> 1.0"},
{:phoenix_ecto, "~> 3.2"},
{:postgrex, ">= 0.0.0"},
{:phoenix_html, "~> 2.10"},
{:phoenix_live_reload, "~> 1.0", only: :dev},
{:libcluster, "~> 2.1"},
{:distillery, "~> 1.5", runtime: false},
{:gettext, "~> 0.11"},
{:cowboy, "~> 1.0"},
{:httpoison, "~> 1.0"},
{:poison, "~> 3.1"},
{:socket, "~> 0.3"}
実装からDeployまで
概要
今回はElixir/PhoenixのPaaSであるGigalixirでSlackのBotを作る導入として、SlackのRealTimeMessagingAPIを使った投稿機能の実装、GigalixirへのDeployまでを書いていきます。
1. 事前準備
事前準備として、Gigalixirの登録とSlackに投稿するためのtokenの取得を行います。
(手抜きですみませんが)ここは先人の良い記事があるので、Gigalixirの登録に関しては参考2の記事、Slackのtoken取得に関しては参考3の記事を参考にするとできます。
2. Gigalixirのテンプレートのclone
自分でPhoenix用のプロジェクトを一から作ってもよいのですが、公式のテンプレートをCloneすると自分で設定する内容を少なく作れるので、お試しで作ってみるには良いと思います。
テンプレートはPhoenixアプリケーションのプロジェクトで、そのままDeployするとGigalixir上でデフォルトのPhoenixアプリケーションが起動する様になっています。
こちらは参考4の記事に公式の手順がまとまっています。
3. Slackへの投稿処理の実装
2でCloneしたソースにSlackへの投稿処理を実装していくのですが、ここでは簡単に、RealTimeMessageを開始し、WebSocket通信を確立、Slackから最初に返されるhelloメッセージがきたら投稿する、という処理を実装します。(SlackのReal Time Message APIで必要な通信処理に関しては、参考5の公式ドキュメントを参照)
この部分は昨年のfukuoka.ex advent calenderの記事で非常に丁寧な解説があったため、そちらを元に下記の様に実装しました。
3.1. ライブラリの追加
HTTPの通信処理とReponseのParse用にhttpoison,poison, WebSocket通信用にsocketのライブラリを追加します。
2でcloneしたプロジェクトルート以下のmix.exsに以下を追加します。
defmodule GigalixirGettingStarted.Mixfile do
...
defp deps do
[
...(これより上はclone時点のライブラリそのまま)...
{:httpoison, "~> 1.0"},
{:poison, "~> 3.1"},
{:socket, "~> 0.3"}
]
end
...
追加したら、mix deps.get
を実行し、依存関係の解決ができるか確認します。
3.2. RealTimeMessageのSession開始からSlackへのPostまで
RealTimeMessageのSession開始, WebSocketへの接続, Slackからのメッセージ受け取り, Slackへの投稿までを下記の様に実装します。
(参考5の内容を参考にさせて頂いています。)
def SlackBot do
# 共通で使用するSlackのURLの基礎部分
@base_url "https://slack.com/api"
# 1で取得したtokenを使用してsession開始用のURL組み立て用の関数(他のURLが必要な場合も同様に実装)
def build_start_url(token) do
"#{@base_url}/rtm.start?token=#{token}"
end
def connect(api_token) do
fetch_body(api_token)
|> parse_url()
|> connect_web_socket()
|> loop(api_token)
end
def fetch_body(token) do
url = build_start_url(token)
# Real Time Message開始処理のレスポンスコードによって処理を振り分け
case HTTPoison.get! url do
%{status_code: 200, body: body} ->
Poison.Parser.parse!(body, keys: :atoms)
%{error: "account_inactive"} = error ->
raise "Failed to start real time message for slack!!"
end
end
def parse_url(%{url: url}) do
with %{"uri" => uri} <- Regex.named_captures(~r/wss:\/\/(?<uri>.*)/, url),
[domain| path] <- String.split(uri, "/") do
%{domain: domain, path: Enum.join(path, "/")}
end
end
def connect_web_socket(%{domain: domain, path: path}) do
Socket.Web.connect!(domain, secure: true, path: "/" <> path)
end
defp loop(socket, token) do
case socket |> Socket.Web.recv!() do
{:text, text} ->
Poison.Parser.parse!(text, keys: :atoms)
|> message(socket, token)
loop(socket, token)
{:ping, _} ->
t = DateTime.utc_now() |> DateTime.to_string()
socket |> Socket.Web.send!({:text, Poison.encode!(%{type: "ping", id: t})})
loop(socket, token)
end
end
# Hello Messageに対して返信
defp message(%{type: "hello"} = m, socket, token) do
socket |> Socket.Web.send!(
{:text, Poison.encode!(
%{token: token,
channel: "MyChannelID", # ここは投稿先のチャンネルIDを記載
text: "Hello! from Elixir",
type: "message"
})})
{:ok, 1}
end
# TODO: Hello Message以外でもエラーが発生しない様に一時的に空実装
defp message(_, socket, token) do
{:ok, 1}
end
end
今回はあまり関係のないので余談になりますが、WebsocketのResponseのurlは30秒以内にWebSocket通信を確立しないと向こうになってしまうため、30秒以上経過してしまった場合、再取得が必要になります。
3.3. Bot用のWokerの起動処理
Slackの処理が失敗しても他のプロセスに影響が出ない様、Gigalixirで起動するアプリケーション上でbot用の別プロセスで起動できる様にしておきます。
まず、上で作ったslack_bot.exについてサーバプロセスとして起動できる様にGenServer
を使用します。
ここでは、同時にconfig.exsにtokenの値を保存しておいてそこから呼び出すための処理も行なっています。
追記:
secretな値はソースコードに含まず、Gigalixirの環境変数から読み込むことを推奨しています。
下記のコマンドで設定し、config.exsで環境変数から読み込む様にします。
gigalixir config:set SLACK_API_TOKEN="<API TOKENの値>"
config :gigalixir_getting_started,
ecto_repos: [GigalixirGettingStarted.Repo],
api_key: Map.fetch!(System.get_env(), "SLACK_API_TOKEN") # 環境変数から読み込み
defmodule SlackBot do
use GenServer
# サーバープロセス起動時にSlackへの通信と投稿を行う
def start_link() do
GenServer.start_link(__MODULE__, [])
# API KEYのconfigからの取得
api_token = Application.get_env(:gigalixir_getting_started, :api_key)
connect(api_token)
end
...(以下の処理は同一)...
そして、このサーバプロセスをlib/gigalixir_getting_started/application.ex
(2でcloneしたソースをそのまま使用する場合)のプロセス起動処理に追加します。
defmodule GigalixirGettingStarted.Application do
...
def start(_type, _args) do
import Supervisor.Spec
# Define workers and child supervisors to be supervised
children = [
# Start the Ecto repository
supervisor(GigalixirGettingStarted.Repo, []),
# Start the endpoint when the application starts
supervisor(GigalixirGettingStartedWeb.Endpoint, []),
# Slack Botのworker起動処理を追加
worker(GigalixirGettingStarted.Slack.SlackBot, [])
]
...(以下の処理は同一)...
end
...(以下の処理は同一)...
ここまで来たらDeploy前に一度ローカルで動作チェックし、Slackに投稿されるか確認してみます。
mix phoenix.server
を実行するとSlackに投稿されることが確認できます。
4. Deploy
最後にGigalixirにデプロイするとPhoenixアプリケーションが起動してSlackに投稿されます。
デプロイ方法は参考9にある通り、ソースコードの差分をgitでadd, commit, pushするだけです。
git push gigalixir master
を実行・成功したことを確認し、しばらく待つと以下の様なメッセージが指定されたチャンネルに投稿されます。数分程度待つこともありますが、大体はすぐに投稿されることが確認できます。
これでSlackのbotをElixirで立てる導入段階まではできました。
後はOutgoingWebHookなどを使って返信機能の実装などを追加していけば、GASなどを使っていたBotを移植することもできます。
追記: 5. Projectの名前変更
プロジェクト名がそのままだと格好がつかないので、プロジェクト名も変更します。
この方法は参考10が参考になるのですが、rename用のファイルまで書き換えてしまわない様、ackコマンドの例外を下記の様に追記します。
#!/bin/bash
set -e
CURRENT_NAME="CurrentName"
CURRENT_OTP="current_name"
NEW_NAME="NewName"
NEW_OTP="new_name"
ack -l $CURRENT_NAME --ignore-file=is:rename_phoenix_project.sh | xargs sed -i '' -e "s/$CURRENT_NAME/$NEW_NAME/g"
ack -l $CURRENT_OTP --ignore-file=is:rename_phoenix_project.sh | xargs sed -i '' -e "s/$CURRENT_OTP/$NEW_OTP/g"
mv lib/$CURRENT_OTP lib/$NEW_OTP
mv lib/$CURRENT_OTP.ex lib/$NEW_OTP.ex
mv lib/${CURRENT_OTP}_web lib/${NEW_OTP}_web
mv lib/${CURRENT_OTP}_web.ex lib/${NEW_OTP}_web.ex
まとめと余談
今回はElixirでSlackのBotを実装し、それをElixir/Phoenix用のPaaSで動かす内容を書きました。
自前でサーバがなくGAS等でBotを立てていた方でも、Elixirで簡単にPaaSでBotを立てられることが伝わってくれれば(そしてElixirユーザが少しでも増えれば)と思っています。
また、今回プログラムを作成する上でfukuoka.exの@kobatakoさんの昨年のAdvent Calender記事をかなり参考にさせて頂きました。本当にありがとうございます。
余談ですが、自分がこのAdventCalenderに参加したのは、Elixirを始めたときからTweetを拾って丁寧に回答を返して頂いたり、分からないことがあった際記事を参考にさせて頂いたりとfukuoka.exの@piacereさんやコミュニティにお世話になり、自分でも記事を書いてみたいと思ったからでした。
fukuoka.exを始め、今各地でElixirのコミュニティが増えてきており非常に"熱い"言語だと思っていますが、自分もこの中で新しいことにチャレンジしてElixirの良さを伝えていけたらと思っています!
明日の「fukuoka.ex Elixir/Phoenix Advent Calendar 2018」は@twinbeeさんのgrpc-elixirでGoと通信してみる #2です! こちらもお楽しみに!
※まだElixirについて無知な部分が多く、内容に不備があれば指摘していただけると助かります。