defmodule WeChat.Component do
@moduledoc """
Use for WeChat official accounts third-party platform applications, `WeChat` module
is usually used for WeChat's functional APIs invoke directly, `WeChat.Component` is used to
call to refetch/refresh authorizer's access token internally.
"""
require Logger
alias WeChat.{Utils, Error}
defmacro __using__(opts \\ []) do
default_opts =
opts
|> Macro.prewalk(&Macro.expand(&1, __CALLER__))
|> Keyword.take([:adapter_storage, :appid, :authorizer_appid, :scenario])
quote do
def default_opts, do: unquote(default_opts)
@doc """
See WeChat.request/2 for more information.
"""
def request(method, options) do
options = Utils.merge_keyword(options, unquote(default_opts))
WeChat.component_request(method, options)
end
@doc """
The expire time (in seconds) to `access_token` and `ticket` temporary storage,
by default it is 7200 seconds
"""
defdelegate expires_in(), to: WeChat
defoverridable request: 2, expires_in: 0
end
end
@doc """
A function helper to fetch `component` application's access token.
When apply it to hub, if no available component access token from hub's storage, there will use `verify_ticket` to refetch a new one,
this function can be used for refresh function.
"""
def fetch_component_access_token(appid, adapter_storage) when is_atom(adapter_storage) do
fetch_component_access_token(appid, {adapter_storage, nil})
end
def fetch_component_access_token(appid, {adapter_storage, args}) do
case adapter_storage.fetch_component_access_token(appid, args) do
{:ok, %WeChat.Token{access_token: nil}} ->
component_access_token = remote_get_component_access_token(appid, adapter_storage, args)
Logger.info("get component_access_token from remote: #{inspect(component_access_token)}")
component_access_token
{:ok, %WeChat.Token{access_token: _}} = token ->
token
{:error, error} ->
{:error, error}
end
end
defp remote_get_component_access_token(appid, adapter_storage, args) do
case adapter_storage.fetch_component_verify_ticket(appid, args) do
{:ok, verify_ticket} when verify_ticket != nil ->
Logger.info(
">>> verify_ticket when remote_get_component_access_token: #{inspect(verify_ticket)}"
)
result =
WeChat.request(:post,
url: "/cgi-bin/component/api_component_token",
body: %{"component_verify_ticket" => verify_ticket},
appid: appid,
adapter_storage: {adapter_storage, args}
)
case result do
{:ok, response} ->
access_token = Map.get(response.body, "component_access_token")
expires_in = Map.get(response.body, "expires_in")
{
:ok,
%WeChat.Token{
access_token: access_token,
expires_in: expires_in,
timestamp: Utils.now_unix()
}
}
{:error, error} ->
Logger.error(
"remote call /cgi-bin/component/api_component_token for appid: #{inspect(appid)} occurs an error: #{
inspect(error)
}"
)
{:error, error}
end
{:error, error} ->
Logger.error(
"occur an error: #{inspect(error)} when get component access token for appid: #{
inspect(appid)
}"
)
raise %WeChat.Error{
message:
"verify_ticket is not existed for appid: #{inspect(appid)}, please try re-authorize",
reason: "verify_ticket_not_found"
}
end
end
@doc """
A function helper to fetch authorizer application's access token from `component` application.
When apply it to hub, if no available access token from hub's storage, there will use `refresh_token` to refetch a new one,
this function can be used for refresh function.
"""
def fetch_access_token(appid, authorizer_appid, adapter_storage)
when is_atom(adapter_storage) and adapter_storage != nil do
fetch_access_token(appid, authorizer_appid, {adapter_storage, nil})
end
def fetch_access_token(appid, authorizer_appid, {adapter_storage, args}) do
result = adapter_storage.fetch_access_token(appid, authorizer_appid, args)
case result do
{:ok, %WeChat.Token{access_token: access_token}} when access_token != nil ->
result
{:ok, %WeChat.Token{access_token: nil, refresh_token: refresh_token}}
when refresh_token != nil ->
refresh_or_refetch_token_to_refresh(
appid,
authorizer_appid,
refresh_token,
adapter_storage,
args
)
_ ->
find_and_refresh_access_token(appid, authorizer_appid, adapter_storage, args)
end
end
defp refresh_or_refetch_token_to_refresh(
appid,
authorizer_appid,
authorizer_refresh_token_str,
adapter_storage,
args
) do
refresh_result =
remote_refresh_authorizer_access_token(
appid,
authorizer_appid,
authorizer_refresh_token_str,
adapter_storage,
args
)
Logger.info(
">>>> remote refresh authorizer_access_token result: #{inspect(refresh_result)} <<<<"
)
case refresh_result do
nil ->
find_and_refresh_access_token(appid, authorizer_appid, adapter_storage, args)
{:ok, %WeChat.Token{access_token: _access_token}} ->
refresh_result
end
end
defp remote_refresh_authorizer_access_token(
appid,
authorizer_appid,
authorizer_refresh_token_str,
adapter_storage,
args
) do
data = %{
"authorizer_appid" => authorizer_appid,
"authorizer_refresh_token" => authorizer_refresh_token_str
}
request_result =
WeChat.request(:post,
url: "/cgi-bin/component/api_authorizer_token",
body: data,
appid: appid,
authorizer_appid: authorizer_appid,
adapter_storage: {adapter_storage, args}
)
Logger.info(
">>>> remote_refresh_authorizer_access_token request_result: #{inspect(request_result)} <<<<"
)
case request_result do
{:ok, response} ->
{
:ok,
%WeChat.Token{
access_token: Map.get(response.body, "authorizer_access_token"),
timestamp: Map.get(response.body, "timestamp"),
expires_in: Map.get(response.body, "expires_in")
}
}
{:error, error} ->
# errcode: 61023, invalid refresh_token
# try to refetch refresh_token, and then use it to refresh authorizer access_token
Logger.info(
"remote_refresh_authorizer_access_token occurs error: #{inspect(error)}, will try to refetch refresh_token and use it to refresh authorizer access_token"
)
nil
end
end
defp find_and_refresh_access_token(appid, authorizer_appid, adapter_storage, args) do
behaviours = adapter_storage.module_info[:attributes][:behaviour]
if WeChat.Storage.ComponentClient in behaviours do
# For client use case, send request to hub for refresh.
adapter_storage.refresh_access_token(appid, authorizer_appid, nil, args)
else
# For hub use case, the cached component refresh token is expired,
# rerun refresh token by authorizer list.
case remote_find_authorizer_refresh_token(appid, authorizer_appid, adapter_storage, args) do
{:ok, refresh_token_str} ->
Logger.info(
">>> refresh_token: #{inspect(refresh_token_str)} from remote_find_authorizer_refresh_token"
)
remote_refresh_authorizer_access_token(
appid,
authorizer_appid,
refresh_token_str,
adapter_storage,
args
)
error ->
error
end
end
end
defp remote_find_authorizer_refresh_token(
appid,
authorizer_appid,
adapter_storage,
args,
offset \\ 0,
count \\ 500
) do
request_result =
WeChat.request(:post,
appid: appid,
url: "/cgi-bin/component/api_get_authorizer_list",
body: %{"offset" => offset, "count" => count},
adapter_storage: {adapter_storage, args}
)
case request_result do
{:ok, response} ->
total_count = Map.get(response.body, "total_count")
Logger.info("remote get authorizer_list total_count: #{total_count}, offset: #{offset}")
list = Map.get(response.body, "list")
matched =
Enum.find(list, fn item ->
Map.get(item, "authorizer_appid") == authorizer_appid
end)
if matched != nil do
{:ok, Map.get(matched, "refresh_token")}
else
size_in_list = length(list)
if size_in_list == 0 or size_in_list == total_count do
Logger.error(
"not find matched authorizer_appid: #{authorizer_appid} in authorizer_list, please double check."
)
{:error, %Error{reason: "invalid_authorizer_appid"}}
else
if offset + 1 < total_count do
remote_find_authorizer_refresh_token(
appid,
authorizer_appid,
adapter_storage,
args,
offset + size_in_list,
count
)
else
Logger.error(
"not find matched authorizer_appid: #{authorizer_appid} in authorizer_list, please double check."
)
{:error, %Error{reason: "invalid_authorizer_appid"}}
end
end
end
{:error, error} ->
Logger.error("remote_find_authorizer_refresh_token error: #{inspect(error)}")
{:error, error}
end
end
end