defmodule WeChat.Http.Middleware.Component do
@moduledoc false
@behaviour Tesla.Middleware
alias WeChat.Http.Middleware.Common
alias WeChat.{Error, Request, Utils}
require Logger
def call(env, next, request) do
execute(env, next, request)
end
defp execute(env, next, request) do
case prepare_request(env, request) do
{:error, error} ->
{:error, error}
prepared_env ->
prepared_env
|> Tesla.run(next)
|> decode_response(env, next, request)
end
end
defp prepare_request(env, request) do
env
|> populate_component_access_token(request)
|> Common.encode_request()
end
# https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Official_Accounts/official_account_website_authorization.html
defp populate_component_access_token(
env,
%Request{
uri: %URI{path: "/sns/oauth2/component/access_token"},
authorizer_appid: authorizer_appid,
appid: appid
} = request
) do
case append_component_access_token(env, request) do
{:error, error} ->
{:error, error}
env ->
query = Keyword.merge([appid: authorizer_appid, component_appid: appid], env.query)
{
Map.put(env, :query, query),
request
}
end
end
# https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/Mini_Programs/WeChat_login.html
defp populate_component_access_token(
env,
%Request{
uri: %URI{path: "/sns/component/jscode2session"},
authorizer_appid: authorizer_appid,
appid: appid
} = request
) do
case append_component_access_token(env, request) do
{:error, error} ->
{:error, error}
env ->
query = Keyword.merge([appid: authorizer_appid, component_appid: appid], env.query)
{
Map.put(env, :query, query),
request
}
end
end
defp populate_component_access_token(
env,
%Request{
uri: %URI{path: "/cgi-bin/component/api_component_token"},
appid: appid,
adapter_storage: {adapter_storage, args}
} = request
) do
case adapter_storage.fetch_secret_key(appid, args) do
{:ok, secret} ->
body =
env.body
|> populate_required_into_body([:component_verify_ticket])
|> Map.put(:component_appid, request.appid)
|> Map.put(:component_appsecret, secret)
{
Map.put(env, :body, body),
request
}
nil ->
Logger.error("not found secret_key for appid: #{inspect(appid)}")
raise %Error{
reason: "invalid_config",
message: "not found secret_key for appid: #{inspect(appid)}"
}
end
end
defp populate_component_access_token(
env,
%Request{uri: %URI{path: "/cgi-bin/component/api_create_preauthcode"}, appid: appid} =
request
) do
case append_component_access_token(env, request) do
{:error, error} ->
{:error, error}
env ->
body = populate_required_into_body(env.body, [], %{component_appid: appid})
{
Map.put(env, :body, body),
request
}
end
end
defp populate_component_access_token(
env,
%Request{uri: %URI{path: "/cgi-bin/component/api_query_auth"}, appid: appid} = request
) do
case append_component_access_token(env, request) do
{:error, error} ->
{:error, error}
env ->
body =
populate_required_into_body(env.body, [:authorization_code], %{component_appid: appid})
{
Map.put(env, :body, body),
request
}
end
end
defp populate_component_access_token(
env,
%Request{uri: %URI{path: "/cgi-bin/component/api_authorizer_token"}, appid: appid} =
request
) do
case append_component_access_token(env, request) do
{:error, error} ->
{:error, error}
env ->
body =
populate_required_into_body(env.body, [:authorizer_appid, :authorizer_refresh_token], %{
component_appid: appid
})
{
Map.put(env, :body, body),
request
}
end
end
defp populate_component_access_token(
env,
%Request{uri: %URI{path: "/cgi-bin/component/api_get_authorizer_info"}, appid: appid} =
request
) do
case append_component_access_token(env, request) do
{:error, error} ->
{:error, error}
env ->
body =
populate_required_into_body(env.body, [:authorizer_appid], %{component_appid: appid})
{
Map.put(env, :body, body),
request
}
end
end
defp populate_component_access_token(
env,
%Request{uri: %URI{path: "/cgi-bin/component/api_get_authorizer_option"}, appid: appid} =
request
) do
case append_component_access_token(env, request) do
{:error, error} ->
{:error, error}
env ->
body =
populate_required_into_body(env.body, [:authorizer_appid, :option_name], %{
component_appid: appid
})
{
Map.put(env, :body, body),
request
}
end
end
defp populate_component_access_token(
env,
%Request{uri: %URI{path: "/cgi-bin/component/api_set_authorizer_option"}, appid: appid} =
request
) do
case append_component_access_token(env, request) do
{:error, error} ->
{:error, error}
env ->
body =
populate_required_into_body(
env.body,
[:authorizer_appid, :option_name, :option_value],
%{
component_appid: appid
}
)
{
Map.put(env, :body, body),
request
}
end
end
defp populate_component_access_token(
env,
%Request{uri: %URI{path: "/cgi-bin/component/api_get_authorizer_list"}, appid: appid} =
request
) do
case append_component_access_token(env, request) do
{:error, error} ->
{:error, error}
env ->
body = populate_required_into_body(env.body, [:offset, :count], %{component_appid: appid})
Logger.info("after processing api_get_authorizer_list the body: #{inspect(body)}")
{
Map.put(env, :body, body),
request
}
end
end
defp populate_component_access_token(env, request) do
case append_component_access_token(env, request) do
{:error, error} ->
{:error, error}
env ->
{env, request}
end
end
defp append_component_access_token(env, %Request{access_token: component_access_token})
when component_access_token != nil do
Logger.info(fn ->
"auto append component_access_token using the latest re-freshed component_access_token: #{inspect(component_access_token)}"
end)
Map.update!(
env,
:query,
&Keyword.put(&1 || [], :component_access_token, component_access_token)
)
end
defp append_component_access_token(env, %Request{adapter_storage: adapter_storage, appid: appid}) do
result = WeChat.Component.fetch_component_access_token(appid, adapter_storage)
case result do
{:error, error} ->
{:error, error}
{:ok, %WeChat.Token{access_token: component_access_token}}
when is_bitstring(component_access_token) ->
Map.update!(
env,
:query,
&Keyword.put(&1 || [], :component_access_token, component_access_token)
)
end
end
defp decode_response({:ok, %{body: body} = response}, env, next, request)
when body != "" and body != nil do
case rerun_when_token_expire(env, next, request, response) do
{:no_retry, json_resp_body} ->
json_resp_body = sync_to_storage_cache(json_resp_body, request)
{
:ok,
%{
status: response.status,
headers: response.headers,
body: json_resp_body
}
}
{retry_result, _} ->
retry_result
end
end
defp decode_response({:ok, %{body: body} = response}, _env, _next, _request)
when body == "" or body == nil do
{:error,
%Error{reason: "unknown", message: "response body is empty", http_status: response.status}}
end
defp decode_response({:error, reason}, _env, _next, _request) do
Logger.error("occurs error when decode response with reason: #{inspect(reason)}")
{:error, %Error{reason: "#{reason}"}}
end
defp sync_to_storage_cache(
%{"authorizer_access_token" => access_token, "expires_in" => expires_in} = response,
%Request{
uri: %URI{path: "/cgi-bin/component/api_query_auth"},
adapter_storage: {adapter_storage, args},
appid: appid,
authorizer_appid: authorizer_appid
}
)
when access_token != nil and expires_in != nil do
authorizer_refresh_token = Map.get(response, "authorizer_refresh_token")
{:ok, token} =
adapter_storage.save_access_token(
appid,
authorizer_appid,
access_token,
authorizer_refresh_token,
args
)
response |> Map.put("expires_in", token.expires_in) |> Map.put("timestamp", token.timestamp)
end
defp sync_to_storage_cache(
%{"authorizer_access_token" => access_token, "expires_in" => expires_in} = response,
%Request{
uri: %URI{path: "/cgi-bin/component/api_authorizer_token"},
adapter_storage: {adapter_storage, args},
appid: appid,
authorizer_appid: authorizer_appid
}
)
when access_token != nil and expires_in != nil do
authorizer_refresh_token = Map.get(response, "authorizer_refresh_token")
{:ok, token} =
adapter_storage.save_access_token(
appid,
authorizer_appid,
access_token,
authorizer_refresh_token,
args
)
response |> Map.put("expires_in", token.expires_in) |> Map.put("timestamp", token.timestamp)
end
defp sync_to_storage_cache(
%{"component_access_token" => component_access_token, "expires_in" => expires_in} =
response,
%Request{
uri: %URI{path: "/cgi-bin/component/api_component_token"},
adapter_storage: {adapter_storage, args},
appid: appid
}
)
when component_access_token != nil and expires_in != nil do
adapter_storage.save_component_access_token(appid, component_access_token, args)
response
end
defp sync_to_storage_cache(response, _request) do
response
end
defp populate_required_into_body(body, fields, prepared \\ %{})
defp populate_required_into_body(body, [], prepared)
when is_map(prepared) and is_bitstring(body) do
prepared
end
defp populate_required_into_body(body, [], prepared) when is_map(prepared) and is_map(body) do
prepared
end
defp populate_required_into_body(nil, [], prepared) when is_map(prepared) do
prepared
end
defp populate_required_into_body(body, [], prepared) when is_map(prepared) do
raise %Error{
reason: "invalid_request",
message: "http body: #{inspect(body)} is invalid"
}
end
defp populate_required_into_body(body, [current_field | rest_fields], prepared)
when is_map(prepared) and is_map(body) and is_bitstring(current_field) do
current_field_atom = String.to_atom(current_field)
value =
Map.get_lazy(body, current_field, fn ->
Map.get(body, current_field_atom)
end)
updated = Map.put(prepared, current_field_atom, value)
populate_required_into_body(body, rest_fields, updated)
end
defp populate_required_into_body(body, [current_field | rest_fields], prepared)
when is_map(prepared) and is_map(body) and is_atom(current_field) do
value =
Map.get_lazy(body, current_field, fn ->
Map.get(body, to_string(current_field))
end)
updated = Map.put(prepared, current_field, value)
populate_required_into_body(body, rest_fields, updated)
end
defp populate_required_into_body(body, [current_field | _rest_fields], prepared)
when is_map(prepared) and is_bitstring(body) and is_bitstring(current_field) do
updated = Map.put(prepared, String.to_atom(current_field), body)
populate_required_into_body(body, [], updated)
end
defp populate_required_into_body(body, [current_field | _rest_fields], prepared)
when is_map(prepared) and is_bitstring(body) and is_atom(current_field) do
updated = Map.put(prepared, current_field, body)
populate_required_into_body(body, [], updated)
end
defp populate_required_into_body(body, [current_field | _rest_fields], prepared)
when is_map(prepared) and is_map(body)
when is_map(prepared) and is_bitstring(body) do
raise %Error{
reason: "invalid_request",
message: "invalid field: #{inspect(current_field)} from request body"
}
end
defp populate_required_into_body(nil, fields, prepared)
when is_map(prepared) and is_list(fields) and fields != [] do
prepared
end
defp populate_required_into_body(body, fields, prepared)
when is_map(prepared) and is_list(fields) and fields != [] do
raise %Error{
reason: "invalid_request",
message: "invalid request body: #{inspect(body)}"
}
end
defp rerun_when_token_expire(env, next, request, %{body: body} = response) do
json_resp_body = Utils.json_decode(body)
result = rerun_when_token_expire(env, next, request, json_resp_body, response.query)
{result, json_resp_body}
end
defp rerun_when_token_expire(
env,
next,
%Request{appid: appid, adapter_storage: {adapter_storage, args}} = request,
%{"errcode" => errcode},
request_query
)
when errcode == 40001
when errcode == 42001 do
Logger.info(
"when invoke wechat component apis occurs expired component_access_token for wechat appid: #{appid}, will refresh to fetch a new component_access_token"
)
refresh_result =
adapter_storage.refresh_component_access_token(
appid,
request_query[:component_access_token],
args
)
case refresh_result do
{:ok, %WeChat.Token{access_token: new_component_access_token}}
when new_component_access_token != nil ->
request = Map.put(request, :access_token, new_component_access_token)
execute(env, next, request)
{:error, error} ->
{:error, error}
end
end
defp rerun_when_token_expire(
_env,
_next,
%Request{appid: appid},
%{"errcode" => errcode} = json_resp_body,
_request_query
)
when errcode == 61005
when errcode == 61006 do
Logger.error(
"component_verify_ticket of appid: #{appid} is expired or invalid (errcode: #{errcode}), please re-auth to update verify_ticket"
)
{:error,
%Error{
reason: "invalid_component_verify_ticket",
errcode: errcode,
message: Map.get(json_resp_body, "errmsg")
}}
end
defp rerun_when_token_expire(
env,
_next,
%Request{appid: appid},
%{"errcode" => errcode} = json_resp_body,
_request_query
)
when errcode == 61023 do
Logger.info(
"refresh_token of appid: #{appid} is invalid, will retry fetch, env: #{inspect(env)}"
)
{:error,
%Error{
reason: "invalid_component_refresh_token",
errcode: errcode,
message: Map.get(json_resp_body, "errmsg")
}}
end
defp rerun_when_token_expire(
_env,
_next,
%Request{appid: appid},
%{"errcode" => errcode} = json_resp_body,
_request_query
)
when errcode == 61004 do
Logger.error("clientip is not in whitelist of appid: #{appid}")
{:error,
%Error{
reason: "invalid_clientip",
errcode: errcode,
message: Map.get(json_resp_body, "errmsg")
}}
end
defp rerun_when_token_expire(_env, _next, _request, _json_resp_body, _request_query) do
:no_retry
end
end