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