lib/wechat/plug/work_event_handler.ex

if Code.ensure_loaded?(Plug) do
  defmodule WeChat.Plug.WorkEventHandler do
    @moduledoc """
    企业微信推送消息处理器

    ## Usage

    请将入口路径设置为如下格式 `/*xxx/:app/:agent` 并将代码加到 `router` 里面:

        scope "/wx/event/:app/:agent" do
          forward "/", #{inspect(__MODULE__)}, event_handler: &YourModule.handle_event/4
        end

    ## Options

    - `event_handler`: 必填, [定义](`t:event_handler/0`)
    - `event_parser`: 可选, [定义](`t:event_parser/0`),
      默认值: [&ServerMessage.EventHelper.parse_work_xml_event/4](`WeChat.ServerMessage.EventHelper.parse_work_xml_event/4`)
    """

    import Plug.Conn
    require Logger
    alias WeChat.Work.Agent
    alias WeChat.Plug.EventHandler
    alias WeChat.ServerMessage.{EventHelper, Encryptor}
    @behaviour Plug

    @typedoc "事件处理回调函数"
    @type event_handler ::
            (Plug.Conn.t(), WeChat.client(), Agent.t(), message :: map ->
               EventHandler.event_handler_return())
    @typedoc "事件解析函数"
    @type event_parser ::
            (params :: map, body :: String.t() | map, WeChat.client(), Agent.t() ->
               {:ok, EventHelper.data_type(), EventHelper.xml() | EventHelper.json()}
               | {:error, String.t()})

    @doc false
    def init(opts) do
      opts = Map.new(opts)

      event_handler =
        with {:ok, handler} <- Map.fetch(opts, :event_handler),
             true <- is_function(handler, 4) do
          handler
        else
          :error ->
            raise ArgumentError, "please set :event_handler when using #{inspect(__MODULE__)}"

          false ->
            raise ArgumentError,
                  "the :event_handler must arg 4 function when using #{inspect(__MODULE__)}"
        end

      event_parser =
        with {:ok, parser} <- Map.fetch(opts, :event_parser),
             true <- is_function(parser, 4) do
          parser
        else
          :error ->
            &EventHelper.parse_work_xml_event/4

          false ->
            raise ArgumentError,
                  "the :event_parser must arg 4 function when using #{inspect(__MODULE__)}"
        end

      %{event_handler: event_handler, event_parser: event_parser}
    end

    def call(%{method: "GET", path_params: path_params} = conn, _opts) do
      with %{"app" => app, "agent" => agent_str} <- path_params,
           {client, agent} <- WeChat.get_client_agent(app, agent_str) do
        validate_encrypted_request(conn, client.appid(), agent.token, agent.aes_key)
      else
        _ -> send_resp(conn, 400, "Bad Request") |> halt()
      end
    end

    def call(%{method: "POST", path_params: path_params} = conn, opts) do
      with %{"app" => app, "agent" => agent_str} <- path_params,
           {client, agent} <- WeChat.get_client_agent(app, agent_str) do
        handle_event_request(conn, client, agent, opts.event_parser, opts.event_handler)
      else
        _ -> send_resp(conn, 400, "Bad Request") |> halt()
      end
    end

    def call(conn, _opts) do
      send_resp(conn, 404, "Invalid Method") |> halt()
    end

    @doc """
    验证消息的确来自微信服务器
    """
    @spec validate_encrypted_request(
            Plug.Conn.t(),
            id :: String.t(),
            WeChat.token(),
            Encryptor.aes_key()
          ) :: Plug.Conn.t()
    def validate_encrypted_request(conn = %{query_params: query_params}, id, token, aes_key) do
      with echo_str when echo_str != nil <- query_params["echostr"],
           true <- EventHelper.check_msg_signature?(echo_str, query_params, token),
           {^id, message} <- Encryptor.decrypt(echo_str, aes_key) do
        send_resp(conn, 200, message)
      else
        _ -> send_resp(conn, 400, "Bad Request")
      end
      |> halt()
    end

    @doc "接受事件推送"
    @spec handle_event_request(
            Plug.Conn.t(),
            WeChat.client(),
            Agent.t(),
            event_parser,
            event_handler
          ) :: Plug.Conn.t()
    def handle_event_request(
          %{query_params: query_params} = conn,
          client,
          agent,
          event_parser,
          event_handler
        ) do
      with {:ok, body, conn} <- check_and_read_body(conn),
           {:ok, reply_type, message} <- event_parser.(query_params, body, client, agent) do
        try do
          event_handler.(conn, client, agent, message)
        rescue
          error ->
            Logger.error(
              "call #{inspect(event_handler)}.(#{inspect(client)}, #{inspect(message)}) get error: #{inspect(error)}"
            )

            send_resp(conn, 500, "Internal Server Error")
        else
          # 被动回复推送消息
          {:reply, xml_string, timestamp} ->
            body = EventHelper.reply_msg(reply_type, xml_string, timestamp, client, agent)
            send_resp(conn, 200, body)

          {:reply, body} ->
            send_resp(conn, 200, body)

          :retry ->
            send_resp(conn, 500, "please retry")

          :error ->
            send_resp(conn, 500, "error, please retry")

          {:error, _} ->
            send_resp(conn, 500, "error, please retry")

          :ok ->
            send_resp(conn, 200, "success")

          :ignore ->
            send_resp(conn, 200, "success")

          conn ->
            conn
        end
      else
        _ -> send_resp(conn, 400, "Bad Request")
      end
      |> halt()
    end

    defp check_and_read_body(%{body_params: body_params} = conn) when is_struct(body_params),
      do: read_body(conn)

    defp check_and_read_body(conn), do: {:ok, conn.body_params, conn}
  end
end