概要
- Elixir/PhoenixでLINEのチャットボット(API)を作りたい
- SDKがない -> 作ってみる
結果
無事できました
実現したいこと
- LINEアプリを開く
- Elicチャンネル(ここではチャンネルをElicとしました-)に登録
- メッセージをチャンネルにつぶやく # ボット(Elixirで実装したWebhook API)にリクエスト送信される
- ボットから返信が届く
ボットの処理フロー(概要)
1. 署名を検証する
2. リクエストボディを取得
3. 応答APIを呼んでメッセージを送信する
4. レスポンスを返却
ボットの処理フロー(補足)
- 署名を検証する
- リクエストボディ(RAW)の取得
- HMAC-SHA256アルゴリズムを使用してリクエストボディダイジェストを取得/Base64エンコード
- X-Line-Signatureリクエストヘッダーを取得
- リクエストボディを取得
- LINEのWebhook APIの仕様から、以下のようなペイロード(events)がPOSTされる想定
{
"events": [
{
"type": "message",
"timestamp": 12345678,
"source": {
"type": "user",
"userId": "<送信元ユーザID>"
},
"replyToken": "<リプライトークン>",
"message": {
"type": "text",
"id": "<メッセージID>",
"text": "<メッセージのテキスト>"
}
}
]
}
- 応答メッセージを送信する (Webhookから送信されるHTTP POSTリクエストは、失敗しても再送されません。)
- APIを呼び出す
POST https://api.line.me/v2/bot/message/reply
- レスポンスを返却 (ボットアプリのサーバーにWebhookから送信されるHTTP POSTリクエストには、ステータスコード200を返す必要があります。)
ステータスコード: 200
ソースコード(抜粋)
- endpoint.ex
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
- コントローラ
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
- ビジネスロジック
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