lib/wechat.ex

defmodule WeChat do
  @moduledoc """
  WeChat SDK for Elixir

  ## 定义 Client 模块

  ### 公众号(默认)

      defmodule YourApp.WeChatAppCodeName do
        @moduledoc "CodeName"
        use WeChat,
          appid: "wx-appid",
          appsecret: "appsecret"
      end

  ### 小程序

      defmodule YourApp.WeChatAppCodeName do
        @moduledoc "CodeName"
        use WeChat,
          app_type: :mini_program,
          appid: "wx-appid",
          appsecret: "appsecret"
      end

  ### 第三方应用

      defmodule YourApp.WeChatAppCodeName do
        @moduledoc "CodeName"
        use WeChat,
          by_component?: true,
          app_type: :official_account | :mini_program, # 默认为 :official_account
          appid: "wx-appid",
          component_appid: "wx-third-appid", # 第三方 appid
      end

  ## 定义参数说明

  请看 `t:options/0`

  ## 接口调用

  支持两种方式调用:

  - 调用 `client` 方法:

    `YourApp.WeChatAppCodeName.Material.batch_get_material(:image, 2)`

  - 原生调用方法

    `WeChat.Material.batch_get_material(YourApp.WeChatAppCodeName, :image, 2)`

  ## 企业微信

  详情请看 `WeChat.Work`

  ## 微信支付

  详情请看 `WeChat.Pay`
  """
  import WeChat.Utils, only: [doc_link_prefix: 0]
  alias WeChat.{Refresher, HubClient, HubServer}
  alias WeChat.Work.Agent, as: WorkAgent

  @typedoc """
  OpenID 普通用户的标识,对当前公众号唯一

  加密后的微信号,每个用户对每个公众号的 `OpenID` 是唯一的。对于不同公众号,同一用户的 `OpenID` 不同

  [Docs Link](#{doc_link_prefix()}/doc/offiaccount/User_Management/Get_users_basic_information_UnionID.html){:target="_blank"}
  """
  @type openid :: String.t()
  @type openid_list :: [openid]

  @typedoc """
  UnionID 不同应用下的唯一ID

  同一用户,对同一个微信开放平台下的不同应用,`UnionID` 是相同的

  [Docs Link](#{doc_link_prefix()}/doc/offiaccount/User_Management/Get_users_basic_information_UnionID.html){:target="_blank"}
  """
  @type unionid :: String.t()

  @typedoc """
  服务器角色

  `:client`: 默认,主动刷新 `AccessToken`
  `:hub`: 中控服务器,主动刷新 `AccessToken`
  `:hub_client`: 逻辑服务器,从 `hub` 获取 `AccessToken`
  """
  @type server_role :: :client | :hub | :hub_client
  @typedoc "是否第三方平台开发"
  @type by_component? :: boolean
  @typedoc """
  `client` 的应用类型

  - `:official_account`: 公众号
  - `:mini_program`: 小程序
  """
  @type app_type :: :official_account | :mini_program
  @typedoc "公众号/小程序 应用id"
  @type appid :: String.t()
  @typedoc "公众号/小程序 应用代码"
  @type code_name :: String.t()
  @typedoc "应用秘钥"
  @type appsecret :: String.t()
  @typedoc "第三方平台应用id"
  @type component_appid :: String.t()
  @typedoc "第三方平台应用秘钥"
  @type component_appsecret :: String.t()
  @typedoc """
  服务器配置里的 `token` 值,在接收消息时用于校验签名
  """
  @type token :: String.t()
  @typedoc "错误码"
  @type err_code :: non_neg_integer
  @typedoc "错误信息"
  @type err_msg :: String.t()
  @type env_option :: :runtime_env | {:runtime_env, app} | :compile_env | {:compile_env, app}
  @typep app :: atom

  @typedoc """
  参数

  ## 参数说明

  - `appid`: `t:appid/0` - 必填
  - `app_type`: `t:app_type/0`
  - `code_name`: `t:code_name/0`, 如不指定,默认为模块名最后一个名称的全小写格式
  - `by_component?`: `t:by_component?/0`
  - `server_role`: `t:server_role/0`
  - `storage`: `t:WeChat.Storage.Adapter.t()`
  - `appsecret`: `t:appsecret/0` - 仅在 `by_component?` 设定为 `false` 时才有效
  - `component_appid`: `t:component_appid/0` - 仅在 `by_component?` 设定为 `true` 时才有效
  - `component_appsecret`: `t:component_appsecret/0` - 仅在 `by_component?` 设定为 `true` 时才有效
  - `encoding_aes_key`: `t:WeChat.ServerMessage.Encryptor.encoding_aes_key/0` - 在编译时会自动将 `encoding_aes_key` 转换为 `aes_key`
  - `token`: `t:token/0`
  - `requester`: 请求客户端 - `t:module/0`
  - `gen_sub_module?`: 是否生成子模块,默认生成
  - `sub_modules`: 指定生成子模块的列表

  ## 默认参数:

  - `server_role`: `:client`
  - `by_component?`: `false`
  - `app_type`: `:official_account`
  - `storage`: `WeChat.Storage.File`
  - `requester`: `WeChat.Requester`
  - `gen_sub_module?`: true
  """
  @type options :: [
          server_role: server_role | env_option,
          by_component?: by_component? | env_option,
          app_type: app_type | env_option,
          storage: WeChat.Storage.Adapter.t() | env_option,
          appid: appid,
          appsecret: appsecret | env_option,
          component_appid: component_appid,
          component_appsecret: component_appsecret | env_option,
          encoding_aes_key: WeChat.ServerMessage.Encryptor.encoding_aes_key() | env_option,
          token: token | env_option,
          requester: module
        ]
  @type client :: module
  @type requester :: module
  @type response :: Tesla.Env.result()
  @type start_options :: %{
          optional(:hub_springboard_url) => HubClient.hub_springboard_url(),
          optional(:oauth2_callbacks) => HubServer.oauth2_callbacks(),
          optional(:refresh_before_expired) => Refresher.Default.refresh_before_expired(),
          optional(:refresh_retry_interval) => Refresher.Default.refresh_retry_interval(),
          optional(:refresh_options) => Refresher.DefaultSettings.refresh_options()
        }

  @doc false
  defmacro __using__(options \\ []) do
    quote do
      use WeChat.Builder.OfficialAccount, unquote(options)
    end
  end

  @doc """
  通过 `appid` 或者 `code_name` 获取 `client`
  """
  @spec get_client(appid | code_name) :: nil | client
  defdelegate get_client(app_flag), to: WeChat.Storage.Cache, as: :search_client

  @spec get_client_agent(appid | code_name, WeChat.Work.agent() | String.t()) ::
          nil | {client, WorkAgent.t()}
  defdelegate get_client_agent(app_flag, agent_flag),
    to: WeChat.Storage.Cache,
    as: :search_client_agent

  @doc "动态构建 client"
  @spec build_client(client, options) :: {:ok, client}
  def build_client(client, options) do
    with {:module, module, _binary, _term} <-
           Module.create(
             client,
             quote do
               @moduledoc false
               use WeChat.Builder.OfficialAccount, unquote(Macro.escape(options))
             end,
             Macro.Env.location(__ENV__)
           ) do
      {:ok, module}
    end
  end

  @doc "动态启动 client"
  @spec start_client(client, start_options) :: :ok
  def start_client(client, options \\ %{}) do
    {options, refresher_setting} = Map.split(options, [:hub_springboard_url, :oauth2_callbacks])
    WeChat.Setup.setup_client(client, options)
    add_to_refresher(client, refresher_setting)
  end

  @doc """
  刷新器
    
  默认为 `WeChat.Refresher.Default`
  """
  @spec refresher :: module
  def refresher do
    Application.get_env(:wechat, :refresher, Refresher.Default)
  end

  @doc "将 client 添加到刷新器"
  @spec add_to_refresher(client, Refresher.Default.client_setting()) :: :ok
  def add_to_refresher(client, options \\ %{}) do
    module = refresher()
    module.add(client, options)
  end
end