lib/request_utils.ex

defmodule BlueskyEx.Client.RequestUtils do
  @moduledoc """
  A module to namespace HTTP-related functions.
  """

  alias BlueskyEx.Client.Session
  alias HTTPoison.Response

  @type headers :: [{String.t(), String.t()}, ...]
  @type method :: :get | :post
  @type request_opts :: [
          body: String.t() | nil,
          session: BlueskyEx.Client.Session.t() | nil
        ]

  defmodule URI.Builder do
    @moduledoc """
    A helper module to provide generator functions for RequestUtils.URI.
    """

    @spec build_uri(atom(), String.t(), list(atom())) :: any()
    defmacro build_uri(function_name, endpoint, params) do
      quote do
        case unquote(params) do
          [:pds, :query] ->
            @spec unquote(function_name)(pds :: String.t(), query :: query_params) :: uri
            def unquote(function_name)(pds, query) do
              build_base_uri(pds, unquote(endpoint)) <> query_obj_to_query_params(query)
            end

          _ ->
            @spec unquote(function_name)(pds :: String.t()) :: uri
            def unquote(function_name)(pds), do: build_base_uri(pds, unquote(endpoint))
        end
      end
    end
  end

  defmodule URI do
    @moduledoc """
    A module to namespace functions that generate an AT URI.
    """

    @type pds :: String.t()
    @type uri :: String.t()
    @type query_params :: %{atom() => integer() | String.t()}

    require URI.Builder
    import URI.Builder

    # GET
    build_uri(:get_account_invite_codes, "com.atproto.server.getAccountInviteCodes", [:pds])
    build_uri(:get_author_feed, "app.bsky.feed.getAuthorFeed", [:pds, :query])
    build_uri(:get_notifications, "app.bsky.notification.listNotifications", [:pds, :query])
    build_uri(:get_popular, "app.bsky.unspecced.getPopular", [:pds, :query])
    build_uri(:get_profile, "app.bsky.actor.getProfile", [:pds, :query])
    build_uri(:get_timeline, "app.bsky.feed.getTimeline", [:pds, :query])

    # POST
    build_uri(:create_record, "com.atproto.repo.createRecord", [:pds])
    build_uri(:create_session, "com.atproto.server.createSession", [:pds])

    # DELETE
    build_uri(:delete_record, "com.atproto.repo.deleteRecord", [:pds])

    @spec build_base_uri(pds, String.t()) :: uri
    defp build_base_uri(pds, endpoint), do: "#{pds}/xrpc/#{endpoint}"

    @spec query_obj_to_query_params(query_params) :: uri
    defp query_obj_to_query_params(query) do
      query
      |> Enum.reject(fn {_, value} -> value == nil or value == "" end)
      |> Enum.map_join("&", fn {key, value} -> "#{key}=#{value}" end)
      |> (&"?#{&1}").()
    end
  end

  @spec make_request(URI.uri(), request_opts) :: Response.t()
  def make_request(uri, opts \\ []) do
    body = Keyword.get(opts, :body)
    session = Keyword.get(opts, :session)
    method = if body, do: :post, else: :get
    headers = default_headers(session)

    {:ok, response} =
      case method do
        :get -> HTTPoison.get(uri, headers)
        :post -> HTTPoison.post(uri, body, headers)
      end

    response
  end

  @spec default_headers :: headers
  defp default_headers, do: [{"Content-Type", "application/json"}]

  @spec default_headers(Session.t() | nil) :: headers
  defp default_headers(session) when is_nil(session), do: default_headers()

  defp default_headers(%BlueskyEx.Client.Session{access_token: access_token}) do
    [{"Authorization", "Bearer #{access_token}"} | default_headers()]
  end
end