GigalixirでSlackBotを動かしてみる

(この記事は[「fukuoka.ex Elixir／Phoenix Advent Calendar 2018」](https://qiita.com/advent-calendar/2018/fukuokaex)の17日目です)

昨日は@callmekoheiさんの[BashScript を Elixir で書き直してみたっ（２倍速〜）](https://qiita.com/callmekohei/items/0dee2d07d98726537943)でした！

## はじめに

今回はGigalixirでSlackのBotを動かす方法(導入部)についてです。
自分は今年の８月頃に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の記事](https://qiita.com/kobatako/items/586f82b2845f7d1583ce)で非常に丁寧な解説があったため、そちらを元に下記の様に実装しました。

#### 3.1. ライブラリの追加

HTTPの通信処理とReponseのParse用にhttpoison,poison, WebSocket通信用にsocketのライブラリを追加します。
2でcloneしたプロジェクトルート以下のmix.exsに以下を追加します。

```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](#参考)の内容を参考にさせて頂いています。)

```slack_bot.ex
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環境変数への設定
gigalixir config:set SLACK_API_TOKEN="<API TOKENの値>"
```

```config.exs
config :gigalixir_getting_started,
  ecto_repos: [GigalixirGettingStarted.Repo],
  api_key: Map.fetch!(System.get_env(), "SLACK_API_TOKEN") # 環境変数から読み込み
```

```slack_bot.ex
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したソースをそのまま使用する場合)のプロセス起動処理に追加します。

```application.ex
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`を実行・成功したことを確認し、しばらく待つと以下の様なメッセージが指定されたチャンネルに投稿されます。数分程度待つこともありますが、大体はすぐに投稿されることが確認できます。

![スクリーンショット 2018-12-16 22.52.12.png](https://qiita-image-store.s3.amazonaws.com/0/40655/f1be8cc8-2675-17fb-d35d-e3e2ef3870e6.png)

これでSlackのbotをElixirで立てる導入段階まではできました。
後はOutgoingWebHookなどを使って返信機能の実装などを追加していけば、GASなどを使っていたBotを移植することもできます。

### 追記: 5. Projectの名前変更

プロジェクト名がそのままだと格好がつかないので、プロジェクト名も変更します。
この方法は[参考10](#参考)が参考になるのですが、rename用のファイルまで書き換えてしまわない様、ackコマンドの例外を下記の様に追記します。

```rename_phoenix_project.sh
#!/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」](https://qiita.com/advent-calendar/2018/fukuokaex)は@twinbeeさんのgrpc-elixirでGoと通信してみる #2です！　こちらもお楽しみに！

※まだElixirについて無知な部分が多く、内容に不備があれば指摘していただけると助かります。

## 参考
1. [Gigalixir](https://www.gigalixir.com)
2. [【Gigalixir編①】Elixir／Phoenix本番リリース： 初期PJリリースまで](https://qiita.com/piacere_ex/items/1a9cbcc740ca3707eaec)
3. [SlackのWebhook URL取得手順](https://qiita.com/vmmhypervisor/items/18c99624a84df8b31008)
4. [Gigalixir テンプレートのClone](https://gigalixir.readthedocs.io/en/latest/main.html#prepare-your-app)
5. [ElixirでSlack Botを作った with Qiita API](https://qiita.com/kobatako/items/586f82b2845f7d1583ce)
6. [Elixirのhttpoisonライブラリ](https://hexdocs.pm/httpoison/readme.html)
7. [Elixirのpoisonライブラリ](https://hex.pm/packages/poison)
8. [Elixirのsocketライブラリ](https://hex.pm/packages/socket)
9. [Gigalixirへのデプロイ](https://gigalixir.readthedocs.io/en/latest/main.html#deploy)
10. [Phoenixのプロジェクト名変更](https://gist.github.com/kumekay/8f4c8c0b88152a83c989d45f1fab810a)