lib/wechat/work/work.ex

defmodule WeChat.Work do
  @moduledoc """
  企业微信

  ** 注意 ** 未支持企业微信服务商

  定义 `Client` 模块

      defmodule YourApp.WeChatAppCodeName do
        @moduledoc "CodeName"
        use WeChat.Work,
          corp_id: "corp_id",
          agents: [
            contacts_agent(secret: "contacts_secret"),
            customer_agent(secret: "customer_secret"),
            kf_agent(secret: "customer_secret"),
            agent(10000, name: :agent_name, secret: "agent_secret"),
            ...
          ]
      end

  定义参数说明请看 `t:options/0`
  """
  import WeChat.Work.Agent, only: [agent2id: 2]
  alias WeChat.{Utils, Work.Agent}

  @doc_link Utils.work_doc_link_prefix()

  @type client :: module()
  @type js_api_ticket :: String.t()
  @type url :: String.t()

  @typedoc """
  每个企业都拥有唯一的 corpid -
  [官方文档](#{@doc_link}/90665#corpid)

  获取此信息可在管理后台“我的企业”-“企业信息”下查看“企业ID”(需要有管理员权限)

  """
  @type corp_id :: String.t()

  @typedoc """
  每个应用都有唯一的 agentid -
  [官方文档](#{@doc_link}/90665#agentid)

  在管理后台->“应用与小程序”->“应用”,点进某个应用,即可看到 agentid
  """
  @type agent_id :: Agent.agent_id()
  @type agent_name :: Agent.agent_name()
  @type agent :: agent_name | agent_id
  @type agents :: [Agent.t(), ...]

  @typedoc """
  secret 是企业应用里面用于保障数据安全的“钥匙” -
  [官方文档](#{@doc_link}/90665#secret)

  每一个应用都有一个独立的访问密钥,为了保证数据的安全,secret务必不能泄漏。
  目前 `secret` 有:

  - 自建应用 `secret`
    在管理后台->“应用与小程序”->“应用”->“自建”,点进某个应用,即可看到。
  - 基础应用 `secret`
    某些基础应用(如“审批”“打卡”应用),支持通过API进行操作。在管理后台->“应用与小程序”->“应用->”“基础”,点进某个应用,点开“API”小按钮,即可看到。
  - 通讯录管理 `secret`
    在“管理工具”-“通讯录同步”里面查看(需开启“API接口同步”);
  - 外部联系人管理 `secret`
    在“客户联系”栏,点开“API”小按钮,即可看到。
  """
  @type secret :: Agent.secret()

  @typedoc """
  参数

  ## 参数说明

  - `corp_id`: `t:corp_id/0` - 必填
  - `agents`: 应用列表 - `t:agents/0` - 必填 & 至少一个
  - `server_role`: `t:WeChat.server_role/0`
  - `storage`: `t:WeChat.Storage.Adapter.t/0`
  - `requester`: 请求客户端 - `t:module/0`

  ## 默认参数:

  - `server_role`: `:client`
  - `storage`: `WeChat.Storage.File`
  - `requester`: `WeChat.WorkRequester`
  - 其余参数皆为可选
  """
  @type options :: [
          corp_id: corp_id,
          agents: agents | WeChat.env_option(),
          server_role: WeChat.server_role() | WeChat.env_option(),
          storage: WeChat.Storage.Adapter.t() | WeChat.env_option(),
          requester: module
        ]

  @typedoc """
  access_token 是企业后台去企业微信的后台获取信息时的重要票据 -
  [官方文档](#{@doc_link}/90665#access_token)

  由 `corpid` 和 `secret` 产生。所有接口在通信时都需要携带此信息用于验证接口的访问权限
  """
  @type access_token :: String.t()

  @doc false
  defmacro __using__(options \\ []) do
    quote do
      import WeChat.Work.Agent,
        only: [
          agent: 1,
          agent: 2,
          contacts_agent: 1,
          customer_agent: 1,
          kf_agent: 1
        ]

      use WeChat.Builder.Work, unquote(options)
    end
  end

  @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.Work, unquote(Macro.escape(options))
             end,
             Macro.Env.location(__ENV__)
           ) do
      {:ok, module}
    end
  end

  @doc "动态启动 client"
  @spec start_client(client, WeChat.start_options()) :: :ok
  defdelegate start_client(client, options \\ %{}), to: WeChat

  @doc "动态启动 agent"
  @spec start_agent(client, Agent.t(), WeChat.Setup.options()) ::
          :ok | :client_not_in | {atom, term()}
  def start_agent(client, agent, options \\ %{}) do
    case Agent.append_agent(client, agent) do
      :ok ->
        WeChat.Setup.setup_work_agent(client, agent, options)
        refresher = WeChat.refresher()
        refresher.append_work_agent(client, agent)

      {:error, error} ->
        error
    end
  end

  @doc """
  获取 access_token - [官方文档](#{@doc_link}/91039){:target="_blank"}
  """
  @spec get_access_token(client, agent) :: WeChat.response()
  def get_access_token(client, agent) do
    agent = Agent.fetch_agent!(client, agent)

    client.get("/cgi-bin/gettoken",
      query: [corpid: client.appid(), corpsecret: agent.secret]
    )
  end

  @doc """
  获取 jsapi_ticket

  - [企业](#{@doc_link}/90506#获取企业的jsapi_ticket){:target="_blank"}
  - [应用](#{@doc_link}/90506#获取应用的jsapi_ticket){:target="_blank"}
  """
  @spec get_jsapi_ticket(client, agent, is_agent :: boolean) :: WeChat.response()
  def get_jsapi_ticket(client, agent, is_agent \\ false) do
    if is_agent do
      client.get("/cgi-bin/ticket/get",
        query: [type: "agent_config", access_token: client.get_access_token(agent)]
      )
    else
      client.get("/cgi-bin/get_jsapi_ticket",
        query: [access_token: client.get_access_token(agent)]
      )
    end
  end

  @doc false
  def get_cache(client, agent, key) do
    Agent.fetch_agent_cache_id!(client, agent)
    |> WeChat.Storage.Cache.get_cache(key)
  end

  @doc """
  生成JS-SDK配置 -
  [官方文档](#{@doc_link}/90514){:target="_blank"}
  """
  @spec js_sdk_config(client, agent, url) :: map
  def js_sdk_config(client, agent, url) do
    get_cache(client, agent, :js_api_ticket)
    |> sign_js_sdk(url, client.appid())
  end

  @doc """
  生成agentConfig配置 -
  [官方文档](#{@doc_link}/94313){:target="_blank"}
  """
  @spec js_sdk_agent_config(client, agent, url) :: map
  def js_sdk_agent_config(client, agent, url) do
    {corp_id, config} =
      get_cache(client, agent, :agent_js_api_ticket)
      |> sign_js_sdk(url, client.appid())
      |> Map.pop!(:appId)

    Map.merge(config, %{corpid: corp_id, agentid: agent2id(client, agent)})
  end

  @doc """
  生成JS-SDK配置(by ticket)

  - [签名算法](#{@doc_link}/90506#签名算法){:target="_blank"}
  """
  @spec sign_js_sdk(js_api_ticket, url, corp_id) :: map
  def sign_js_sdk(jsapi_ticket, url, corp_id) do
    url = String.replace(url, ~r/\#.*/, "")
    nonce_str = Utils.random_string(16)
    timestamp = Utils.now_unix()

    signature =
      Utils.sha1(
        "jsapi_ticket=#{jsapi_ticket}&noncestr=#{nonce_str}&timestamp=#{timestamp}&url=#{url}"
      )

    %{appId: corp_id, signature: signature, timestamp: timestamp, nonceStr: nonce_str}
  end
end