lib/lib_wechat.ex

defmodule LibWechat do
  @moduledoc "README.md"
             |> File.read!()
             |> String.split("<!-- MDOC !-->")
             |> Enum.fetch!(1)

  alias LibWechat.Client

  @external_resource "README.md"
  @options_schema [
    name: [
      type: :atom,
      doc: "name of this process",
      default: :wechat
    ],
    client_module: [
      type: :atom,
      doc: "module that implements `LibWechat.Client` behavior",
      default: LibWechat.Client.Finch
    ],
    client: [
      type: :any,
      doc: "client instance",
      default: LibWechat.Client.Finch.new()
    ],
    appid: [
      type: :string,
      required: true,
      doc: "第三方用户唯一凭证"
    ],
    secret: [
      type: :string,
      required: true,
      doc: "第三方用户唯一凭证密钥,即appsecret"
    ],
    json_module: [
      type: :atom,
      doc: "module that implements json's encode and decode behavior like Jason",
      default: Jason
    ]
  ]

  # types
  @type t :: %__MODULE__{
          name: GenServer.name(),
          client_module: module(),
          client: Client.t(),
          appid: bitstring(),
          secret: bitstring(),
          json_module: module()
        }
  @type options_t :: keyword(unquote(NimbleOptions.option_typespec(@options_schema)))
  @type json_t :: %{bitstring() => any()}
  @type ok_t(ret) :: {:ok, ret}
  @type err_t :: {:error, LibWechat.Client.Error.t()}

  @enforce_keys ~w(name client_module client appid secret json_module)a

  defstruct @enforce_keys

  @doc """
  create an instance of LibWechat
  ## options
  #{NimbleOptions.docs(@options_schema)}
  """
  @spec new(options_t()) :: t()
  def new(opts \\ []) do
    opts =
      NimbleOptions.validate!(opts, @options_schema)

    struct(__MODULE__, opts)
  end

  def child_spec(opts) do
    wechat = Keyword.fetch!(opts, :wechat)
    %{id: {__MODULE__, wechat.name}, start: {__MODULE__, :start_link, [opts]}}
  end

  @spec start_link(keyword()) :: GenServer.on_start()
  def start_link(opts) do
    {wechat, _opts} = Keyword.pop!(opts, :wechat)
    wechat.client_module.start_link(client: wechat.client)
  end

  @doc """
  获取access_token
  https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html


  ## Examples

      {:ok, %{"access_token"=>"xxx"}} = LibWechat.get_access_token(wechat)
  """
  @spec get_access_token(t()) :: ok_t(json_t()) | err_t()
  def get_access_token(wechat) do
    params = %{
      "appid" => wechat.appid,
      "secret" => wechat.secret,
      "grant_type" => "client_credential"
    }

    with {:ok, body} <- Client.do_request(wechat.client, :get, "/cgi-bin/token", nil, params) do
      {:ok, wechat.json_module.decode!(body)}
    end
  end

  @doc """
  https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html
  """
  @spec jscode_to_session(t(), String.t()) :: ok_t(json_t()) | err_t()
  def jscode_to_session(wechat, code) do
    params = %{
      "appid" => wechat.appid,
      "secret" => wechat.secret,
      "js_code" => code,
      "grant_type" => "authorization_code"
    }

    with {:ok, body} <- Client.do_request(wechat.client, :get, "/sns/jscode2session", nil, params) do
      {:ok, wechat.json_module.decode!(body)}
    end
  end

  @doc """
  https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/qr-code/wxacode.getUnlimited.html

  ## Examples

      {:ok, <<255, 216, ...>>} = LibWechat.get_unlimited_wxacode(wechat, token,
        %{"scene" => "foo=bar",
          "page" => "pages/index/index",
          "width" => 430,
          "auto_color" => false,
          "line_color" => %{"r" => 0, "g" => 0, "b" => 0},
          "is_hyaline" => false
        }
      )
  """
  @spec get_unlimited_wxacode(t(), String.t(), json_t()) :: ok_t(binary()) | err_t()
  def get_unlimited_wxacode(wechat, token, body) do
    Client.do_request(wechat.client, :post, "/wxa/getwxacodeunlimit", body, %{
      "access_token" => token
    })
  end

  @doc """
  https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-link/urllink.generate.html

  ## Examples

      {:ok, %{
        "errcode" => 0,
        "errmsg" => "ok",
        "url_link" => "https://wxaurl.cn/bz2LB4RMDVqq"
      }} = LibWechat.get_urllink(wechat, token,
        %{
          "path" => "pages/index/index",
          "query" => "foo=bar",
          "is_expire" => false,
          "expire_type" => 0,
          "expire_time" => 0
        }
      )
  """
  @spec get_urllink(t(), String.t(), json_t()) :: ok_t(json_t()) | err_t()
  def get_urllink(wechat, token, body) do
    with {:ok, ret} <-
           Client.do_request(wechat.client, :post, "/wxa/generate_urllink", body, %{
             "access_token" => token
           }) do
      {:ok, wechat.json_module.decode!(ret)}
    end
  end

  @doc """
  https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/url-scheme/urlscheme.generate.html

  ## Examples

      {:ok, %{
        "errcode" => 0,
        "errmsg" => "ok",
        "openlink" => "weixin://dl/business/?t=Akeatr890b"
      }} = LibWechat.generate_scheme(wechat, token,
        %{
          "jump_wxa" => %{
            "path" => "pages/index/index",
            "query" => "foo=bar"
          },
          "is_expire" => false,
          "expire_type" => 0,
          "expire_time" => 0
        }
      )

  """
  @spec generate_scheme(t(), String.t(), json_t()) ::
          ok_t(json_t()) | err_t()
  def generate_scheme(wechat, token, body) do
    with {:ok, ret} <-
           Client.do_request(wechat.client, :post, "/wxa/generatescheme", body, %{
             "access_token" => token
           }) do
      {:ok, wechat.json_module.decode!(ret)}
    end
  end

  @doc """
  https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html

  ## Examples

      {:ok, %{
        "errcode" => 0,
        "errmsg" => "ok",
        "msgid" => 294402298110051942
        }} = LibWechat.subscribe_send(wechat, token,
        %{
          "touser" => "OPENID",
          "template_id" => "TEMPLATE_ID",
          "page" => "index",
          "miniprogram_state" => "developer",
          "lang" => "zh_CN",
          "data" => %{
            "number01" => %{"value" => "339208499"},
            "date01" => %{"value" => "2015年01月05日"},
            "site01" => %{"value" => "TIT创意园"},
            "site02" => %{"value" => "广州市新港中路397号"}
          }
        }
      )

  """
  @spec subscribe_send(t(), String.t(), json_t()) :: ok_t(json_t()) | err_t()
  def subscribe_send(wechat, token, body) do
    with {:ok, ret} <-
           Client.do_request(wechat.client, :post, "/cgi-bin/message/subscribe/send", body, %{
             "access_token" => token
           }) do
      {:ok, wechat.json_module.decode!(ret)}
    end
  end

  @doc """
  https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/uniform-message/uniformMessage.send.html

  ## Examples

      {:ok, %{"errcode" => 0, "errmsg" => "ok"}} = LibWechat.uniform_send(wechat, token,
        %{
          "touser" => "OPENID",
          "weapp_template_msg" => %{
            "template_id" => "TEMPLATE_ID",
            "page" => "index",
            "form_id" => "FORMID",
            "data" => %{
              "keyword1" => %{"value" => "339208499"},
              "keyword2" => %{"value" => "2015年01月05日"},
              "keyword3" => %{"value" => "粤海喜来登酒店"},
              "keyword4" => %{"value" => "广州市天河区天河路208号"}
            },
            "emphasis_keyword" => "keyword1.DATA"
          },
          "mp_template_msg" => %{
            "appid" => "APPID ",
            "template_id" => "TEMPLATE_ID",
            "url" => "http://weixin.qq.com/download",
            "miniprogram" => %{
              "appid" => "xiaochengxuappid12345",
              "pagepath" => "index?foo=bar"
            },
            "data" => %{
              "first" => %{"value" => "恭喜你购买成功!", "color" => "#173177"},
              "keyword1" => %{"value" => "巧克力", "color" => "#173177"},
              "keyword2" => %{"value" => "39.8元", "color" => "#173177"},
              "keyword3" => %{"value" => "2014年9月22日", "color" => "#173177"},
              "remark" => %{"value" => "欢迎再次购买!", "color" => "#173177"}
            }
          }
        })

  """
  @spec uniform_send(t(), String.t(), json_t()) :: ok_t(json_t()) | err_t()
  def uniform_send(wechat, token, body) do
    with {:ok, ret} <-
           Client.do_request(
             wechat.client,
             :post,
             "/cgi-bin/message/wxopen/template/uniform_send",
             body,
             %{
               "access_token" => token
             }
           ) do
      {:ok, wechat.json_module.decode!(ret)}
    end
  end

  @doc """
  https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/phonenumber/phonenumber.getPhoneNumber.html

  ## Examples

      {:ok,
        %{
            "errcode":0,
            "errmsg":"ok",
            "phone_info": {
              "phoneNumber":"xxxxxx",
              "purePhoneNumber": "xxxxxx",
              "countryCode": 86,
              "watermark": {
                  "timestamp": 1637744274,
                  "appid": "xxxx"
              }
            }
          }
        } = get_phone_number(wechat, token, code)
  """
  @spec get_phone_number(t(), String.t(), String.t()) ::
          ok_t(json_t()) | err_t()
  def get_phone_number(wechat, token, code) do
    with {:ok, ret} <-
           Client.do_request(
             wechat.client,
             :post,
             "/wxa/business/getuserphonenumber",
             %{"code" => code},
             %{
               "access_token" => token
             }
           ) do
      {:ok, wechat.json_module.decode!(ret)}
    end
  end
end