LoginSignup
4
1

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-10-04

概要

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

結果

無事できました

スクリーンショット 2018-10-04 22.28.56.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

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
4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1