defmodule WeChat.Refresher.DefaultSettings do
@moduledoc """
刷新 `AccessToken` 的方法
"""
require Logger
alias WeChat.{Account, WebPage, Component, MiniProgram, Work, Utils, Storage.Cache}
@type key_name :: atom
@type token :: String.t()
@type expires_in :: non_neg_integer
@type token_list :: [{key_name, token, expires_in}]
@type refresh_fun_result ::
{:ok, token, expires_in} | {:ok, token_list, expires_in} | {:error, any}
@type refresh_fun :: (WeChat.client() -> refresh_fun_result)
@type refresh_option :: {WeChat.Storage.Adapter.store_id(), key_name, refresh_fun}
@type refresh_options :: [refresh_option]
@doc """
根据不同的 `client` 的 `app_type` & `by_component?` 输出不同的 `refresh_options`
rules:
- `by_component?` == `true`: `component_refresh_options/1`
- `official_account`: `official_account_refresh_options/1`
- `mini_program`: `mini_program_refresh_options/1`
"""
@spec get_refresh_options_by_client(WeChat.client()) :: refresh_options
def get_refresh_options_by_client(client) do
if client.by_component?() do
component_refresh_options(client)
else
if match?(:hub_client, client.server_role()) and match?(nil, client.storage()) do
raise RuntimeError,
"Not accept storage: nil when server_role: :hub_client, please set a module for :storage when defining #{inspect(client)}."
end
case client.app_type() do
:official_account ->
check_secret(client, :appsecret)
official_account_refresh_options(client)
:mini_program ->
check_secret(client, :appsecret)
mini_program_refresh_options(client)
:work ->
work_refresh_options(client)
end
end
end
defp check_secret(client, fun) do
if client.server_role() != :hub_client do
unless function_exported?(client, fun, 0) do
raise RuntimeError, "Please set :appsecret when defining #{inspect(client)}."
end
end
end
@doc """
输出[公众号]的 `refresh_options`
刷新如下 `AccessToken`:
- `access_token`
- `js_api_ticket`
- `wx_card_ticket`
"""
@spec official_account_refresh_options(WeChat.client()) :: refresh_options
def official_account_refresh_options(client) do
appid = client.appid()
[
{appid, :access_token, &__MODULE__.refresh_access_token/1},
{appid, :js_api_ticket, &__MODULE__.refresh_ticket("jsapi", &1)},
{appid, :wx_card_ticket, &__MODULE__.refresh_ticket("wx_card", &1)}
]
end
@doc """
输出[第三方平台]的 `refresh_options`
刷新如下 `AccessToken`:
- `component_access_token`
- AuthorizerRefreshOptions (get by client.app_type())
"""
@spec component_refresh_options(WeChat.client()) :: refresh_options
def component_refresh_options(client) do
component_appid = client.component_appid()
appid = client.appid()
case client.app_type() do
:official_account ->
check_secret(client, :component_appsecret)
[
{component_appid, :component_access_token,
&__MODULE__.refresh_component_access_token/1},
{appid, :access_token, &__MODULE__.refresh_authorizer_access_token/1},
{appid, :js_api_ticket, &__MODULE__.refresh_ticket("jsapi", &1)},
{appid, :wx_card_ticket, &__MODULE__.refresh_ticket("wx_card", &1)}
]
:mini_program ->
check_secret(client, :component_appsecret)
[
{component_appid, :component_access_token,
&__MODULE__.refresh_component_access_token/1},
{appid, :access_token, &__MODULE__.refresh_authorizer_access_token/1}
]
# TODO
# :work ->
# [
# {component_appid, :component_access_token,
# &__MODULE__.refresh_component_access_token/1},
# {appid, :access_token, &__MODULE__.refresh_authorizer_access_token/1}
# ]
end
end
@doc """
输出[小程序]的 `refresh_options`
刷新如下 `AccessToken`:
- `access_token`
"""
@spec mini_program_refresh_options(Work.client()) :: refresh_options
def mini_program_refresh_options(client),
do: [
{client.appid(), :access_token, &__MODULE__.refresh_mini_program_access_token/1}
]
@doc """
输出[企业微信]的 `refresh_options`
刷新如下 `AccessToken`:
- `access_token`
"""
@spec work_refresh_options(WeChat.client()) :: refresh_options
def work_refresh_options(client) do
Enum.flat_map(client.agents(), &work_refresh_options(client, &1))
end
@spec work_refresh_options(WeChat.client(), Work.Agent.t()) :: refresh_options
def work_refresh_options(client, %Work.Agent{
id: agent_id,
cache_id: cache_id,
secret: secret,
refresh_list: refresh_list
}) do
unless secret do
raise RuntimeError,
"Please set :secret for agent:#{agent_id} when defining #{inspect(client)}."
end
list =
refresh_list
|> List.wrap()
|> Enum.map(fn
store_key = :js_api_ticket ->
{cache_id, store_key,
&__MODULE__.refresh_work_jsapi_ticket(&1, cache_id, agent_id, store_key, false)}
store_key = :agent_js_api_ticket ->
{cache_id, store_key,
&__MODULE__.refresh_work_jsapi_ticket(&1, cache_id, agent_id, store_key, true)}
end)
[
{cache_id, :access_token, &__MODULE__.refresh_work_access_token(&1, cache_id, agent_id)}
| list
]
end
@spec refresh_access_token(WeChat.client()) :: refresh_fun_result
def refresh_access_token(client) do
with :ignore <- get_token_for_hub_client(client, :access_token),
{:ok, %{status: 200, body: data}} <- Account.get_access_token(client),
%{"access_token" => access_token, "expires_in" => expires_in} <- data do
{:ok, access_token, expires_in}
end
end
defp get_store_key_by_ticket_type("jsapi"), do: :js_api_ticket
defp get_store_key_by_ticket_type("wx_card"), do: :wx_card_ticket
@spec refresh_ticket(WeChat.WebPage.js_api_ticket_type(), WeChat.client()) :: refresh_fun_result
def refresh_ticket(ticket_type, client) do
with store_key <- get_store_key_by_ticket_type(ticket_type),
:ignore <- get_token_for_hub_client(client, store_key),
{:ok, %{status: 200, body: data}} <- WebPage.get_ticket(client, ticket_type),
%{"ticket" => ticket, "expires_in" => expires_in} <- data do
{:ok, ticket, expires_in}
end
end
@spec refresh_component_access_token(WeChat.client()) :: refresh_fun_result
def refresh_component_access_token(client) do
component_appid = client.component_appid()
with :ignore <- get_token_for_hub_client(client, component_appid, :component_access_token),
ticket when ticket != nil <- ensure_component_verify_ticket(client),
{:ok, %{status: 200, body: data}} <- Component.get_component_token(client, ticket),
%{"component_access_token" => component_access_token, "expires_in" => expires_in} <- data do
{:ok, component_access_token, expires_in}
end
end
defp ensure_component_verify_ticket(client) do
store_id = client.component_appid()
store_key = :component_verify_ticket
case Cache.get_cache(store_id, store_key) do
nil ->
with storage when storage != nil <- client.storage(),
{:ok, %{"value" => ticket, "expired_time" => expires} = store_map} <-
storage.restore(store_id, store_key),
diff <- expires - Utils.now_unix(),
true <- diff > 0 do
Cache.put_cache(store_id, store_key, ticket)
Cache.put_cache({:store_map, store_id}, store_key, store_map)
Logger.info(
"Call #{inspect(storage)}.restore(#{store_id}, #{store_key}) succeed, the expires_in is: #{diff}s."
)
ticket
else
_ -> nil
end
ticket ->
ticket
end
end
@spec refresh_authorizer_access_token(WeChat.client()) :: refresh_fun_result
def refresh_authorizer_access_token(client) do
with :ignore <- get_token_for_hub_client(client, :access_token),
{:ok, %{status: 200, body: data}} <- Component.authorizer_token(client),
%{
"authorizer_access_token" => authorizer_access_token,
"authorizer_refresh_token" => authorizer_refresh_token,
"expires_in" => expires_in
} <- data do
list = [
{:access_token, authorizer_access_token, expires_in},
# 官网并未说明有效期是多少,暂定30天有效期
{:authorizer_refresh_token, authorizer_refresh_token, 30 * 24 * 60 * 60}
]
{:ok, list, expires_in}
end
end
@spec refresh_mini_program_access_token(WeChat.client()) :: refresh_fun_result
def refresh_mini_program_access_token(client) do
with :ignore <- get_token_for_hub_client(client, :access_token),
{:ok, %{status: 200, body: data}} <- MiniProgram.Auth.get_access_token(client),
%{"access_token" => access_token, "expires_in" => expires_in} <- data do
{:ok, access_token, expires_in}
end
end
@spec refresh_work_access_token(Work.client(), Cache.cache_id(), Work.agent_id()) ::
refresh_fun_result
def refresh_work_access_token(client, cache_id, agent_id) do
with :ignore <- get_token_for_hub_client(client, cache_id, :access_token),
{:ok, %{status: 200, body: data}} <- Work.get_access_token(client, agent_id),
%{"access_token" => access_token, "expires_in" => expires_in} <- data do
{:ok, access_token, expires_in}
end
end
@spec refresh_work_jsapi_ticket(
Work.client(),
Cache.cache_id(),
Work.agent_id(),
Cache.cache_sub_key(),
is_agent :: boolean
) :: refresh_fun_result
def refresh_work_jsapi_ticket(client, cache_id, agent_id, store_key, is_agent) do
with :ignore <- get_token_for_hub_client(client, cache_id, store_key),
{:ok, %{status: 200, body: data}} <- Work.get_jsapi_ticket(client, agent_id, is_agent),
%{"ticket" => ticket, "expires_in" => expires_in} <- data do
{:ok, ticket, expires_in}
end
end
# return token for hub_client role
defp get_token_for_hub_client(client, store_id \\ nil, store_key) do
store_id = store_id || client.appid()
if match?(:hub_client, client.server_role()) do
with storage when storage != nil <- client.storage(),
{:ok, %{"value" => access_token, "expired_time" => expired_time}} <-
storage.restore(store_id, store_key) do
expires_in = expired_time - Utils.now_unix()
{:ok, access_token, expires_in}
end
else
:ignore
end
end
end