Elixir/PhoenixでLINE Messaging API HMAC署名検証と応答

## 概要

* Elixir/PhoenixでLINEのチャットボット（API）を作りたい
* SDKがない -> 作ってみる

## 結果

無事できました

<img width="505" alt="スクリーンショット 2018-10-04 22.28.56.png" src="https://qiita-image-store.s3.amazonaws.com/0/79576/67b3b689-1fec-abfa-3a7b-08940d2ff3c5.png">

## 実現したいこと

1. LINEアプリを開く
2. Elicチャンネル(ここではチャンネルをElicとしました-)に登録
3. メッセージをチャンネルにつぶやく # ボット（Elixirで実装したWebhook API）にリクエスト送信される
4. ボットから返信が届く


## ボットの処理フロー(概要)

```
1. 署名を検証する
2. リクエストボディを取得
3. 応答APIを呼んでメッセージを送信する
4. レスポンスを返却
```

## ボットの処理フロー(補足)

1. 署名を検証する
  * リクエストボディ(RAW)の取得
  * HMAC-SHA256アルゴリズムを使用してリクエストボディダイジェストを取得/Base64エンコード
  * X-Line-Signatureリクエストヘッダーを取得 
2. リクエストボディを取得
  * LINEのWebhook APIの仕様から、以下のようなペイロード(events)がPOSTされる想定
```
{
    "events": [
        {
            "type": "message",
            "timestamp": 12345678,
            "source": {
                "type": "user",
                "userId": "<送信元ユーザID>"
            },
            "replyToken": "<リプライトークン>",
            "message": {
                "type": "text",
                "id": "<メッセージID>",
                "text":	"<メッセージのテキスト>"
            }
        }
    ]
}
```
3. 応答メッセージを送信する (Webhookから送信されるHTTP POSTリクエストは、失敗しても再送されません。)
    * APIを呼び出す
```
POST https://api.line.me/v2/bot/message/reply
```
4. レスポンスを返却 (ボットアプリのサーバーにWebhookから送信されるHTTP POSTリクエストには、ステータスコード200を返す必要があります。)
```
ステータスコード: 200
```

## ソースコード（抜粋）
* endpoint.ex

```elixir

defmodule PortionWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :portion

  socket "/socket", PortionWeb.UserSocket

# ...

  plug :copy_req_raw_body

  plug Plug.Parsers,
    parsers: [:urlencoded, :multipart, :json],
    pass: ["*/*"],
    json_decoder: Poison

# ...

  defp copy_req_raw_body(conn, _) do
    {:ok, body, _} = Plug.Conn.read_body(conn)
    Plug.Conn.put_private(conn, :raw_body, body)
  end
```

* コントローラ

```elixir

  def create(conn, %{"events" => events}) do
    envs = Application.get_env(:portion, PortionWeb.Endpoint)[:line_envs]

    {"x-line-signature", signature} = Enum.find(conn.req_headers, &elem(&1, 0) == "x-line-signature")
      body = conn.private[:raw_body]

      LineWebhookService.validate_signature(
        signature,
        body,
        envs[:channel_secret]
      )
      LineWebhookService.process_bot(
        events,
        envs[:access_token]
      )

      # 200応答を返却
      conn |> put_status(:ok) |> render("response.json", %{"result": "OK!"})
  end
```

* ビジネスロジック

```elixir

defmodule Portion.Lesson.LineWebhookService do
  @moduledoc false

  def validate_signature(signature, body, secret) do

    expected = :crypto.hmac(:sha256, secret, body) |> Base.encode64

    cond do
      expected === signature -> {:ok, expected}
      true -> raise "Invalid Signature"
    end

  end

  def process_bot(events, access_token) do
    events
     |> filter_repliable_events 
     |> Enum.each( &(build_reply_message(&1) 
     |> reply_message(&1, access_token)) )
  end

  defp reply_message(reply_message, event, access_token) do
    headers = [
      {"Content-Type", "application/json"},
      {"Authorization", "Bearer #{access_token}"},
    ]
    request_body = %{replyToken: event["replyToken"], messages: [ %{type: "text", text: reply_message} ]}
    response = HTTPoison.post!("https://api.line.me/v2/bot/message/reply", Poison.encode!(request_body), headers)
    case response do
      %HTTPoison.Response{status_code: status} -> IO.puts("Success #{status}")
      %HTTPoison.Error{reason: reason} -> reason |> Kernel.inspect |> IO.puts
    end
  end

  defp build_reply_message(event) do
    # TODO: 応答メッセージを変える
    "Thank you for #{event["message"]["text"]}"
  end

  defp filter_repliable_events(events) do
    events |> Enum.filter( &( is_repliable(&1) ) )
  end

  defp is_repliable(event) do
    event["type"] === "message" && event["replyToken"] !== nil
  end
end
```