lib/record_manager.ex

defmodule BlueskyEx.Client.RecordManager do
  @moduledoc """
  A module to namespace functions that interact with a Bluesky feed.
  """

  alias BlueskyEx.Client.{RequestUtils, Session}
  alias HTTPoison.Response

  @type feed_query_opts :: [
          limit: non_neg_integer(),
          algorithm: String.t()
        ]

  @spec get_account_invite_codes(Session.t()) :: Response.t()
  def get_account_invite_codes(session),
    do: fetch_data(:get_account_invite_codes, session)

  @spec get_notifications(Session.t(), limit: integer() | nil, algorithm: String.t() | nil) ::
          Response.t()
  def get_notifications(session, opts \\ []),
    do: fetch_data(:get_notifications, session, query: build_feed_query(opts))

  @spec get_popular(Session.t(), limit: integer() | nil, algorithm: String.t() | nil) ::
          Response.t()
  def get_popular(session, opts \\ []),
    do: fetch_data(:get_popular, session, query: build_feed_query(opts))

  @spec get_timeline(Session.t(), limit: integer() | nil, algorithm: String.t() | nil) ::
          Response.t()
  def get_timeline(session, opts \\ []),
    do: fetch_data(:get_timeline, session, query: build_feed_query(opts))

  @spec get_author_feed(Session.t(),
          actor: String.t(),
          limit: integer() | nil,
          algorithm: String.t() | nil
        ) :: Response.t()
  def get_author_feed(session, opts \\ []),
    do:
      fetch_data(:get_author_feed, session,
        query: Map.merge(build_feed_query(opts), build_actor_query(session))
      )

  @spec get_profile(Session.t(), actor: String.t() | nil) :: Response.t()
  def get_profile(session, opts \\ []),
    do: fetch_data(:get_profile, session, query: build_actor_query(session, opts))

  @spec create_post(Session.t(), text: String.t()) :: Response.t()
  def create_post(session, text: text),
    do:
      fetch_data(:create_record, session,
        body: build_create_body(session, "app.bsky.feed.post", %{text: text})
      )

  @spec delete_post(Session.t(), String.t()) :: Response.t()
  def delete_post(session, rkey),
    do:
      fetch_data(:delete_record, session,
        body: build_delete_body(session, "app.bsky.feed.post", rkey)
      )

  @spec create_like(Session.t(), uri: String.t(), cid: String.t()) :: Response.t()
  def create_like(session, uri: uri, cid: cid),
    do:
      fetch_data(:create_record, session,
        body: build_create_body(session, "app.bsky.feed.like", %{subject: %{uri: uri, cid: cid}})
      )

  @spec delete_like(Session.t(), String.t()) :: Response.t()
  def delete_like(session, rkey),
    do:
      fetch_data(:delete_record, session,
        body: build_delete_body(session, "app.bsky.feed.like", rkey)
      )

  @spec create_repost(Session.t(), uri: String.t(), cid: String.t()) :: Response.t()
  def create_repost(session, uri: uri, cid: cid),
    do:
      fetch_data(:create_record, session,
        body:
          build_create_body(session, "app.bsky.feed.repost", %{subject: %{uri: uri, cid: cid}})
      )

  @spec delete_repost(Session.t(), String.t()) :: Response.t()
  def delete_repost(session, rkey),
    do:
      fetch_data(:delete_record, session,
        body: build_delete_body(session, "app.bsky.feed.repost", rkey)
      )

  @typep fetch_options :: [
           {:body, String.t()}
           | {:query, RequestUtils.URI.query_params()}
         ]
  @spec fetch_data(atom(), Session.t(), fetch_options) :: Response.t()
  defp fetch_data(request_type, %Session{pds: pds} = session, options \\ []) do
    query = options[:query]
    body = options[:body]

    args =
      case query do
        nil -> [pds]
        _ -> [pds, query]
      end

    uri = apply(RequestUtils.URI, request_type, args)
    RequestUtils.make_request(uri, body: body, session: session)
  end

  @spec build_create_body(Session.t(), String.t(), map()) :: String.t()
  defp build_create_body(session, type, fields) do
    Jason.encode!(%{
      collection: type,
      repo: session.did,
      record:
        Map.merge(
          %{
            "$type": type,
            createdAt: timestamp_now()
          },
          fields
        )
    })
  end

  @spec build_delete_body(Session.t(), String.t(), String.t()) :: String.t()
  defp build_delete_body(session, type, rkey) do
    Jason.encode!(%{
      collection: type,
      repo: session.did,
      rkey: rkey
    })
  end

  @spec timestamp_now :: String.t()
  defp timestamp_now do
    DateTime.utc_now() |> DateTime.to_iso8601()
  end

  @spec build_feed_query(Keyword.t()) :: RequestUtils.URI.query_params()
  defp build_feed_query(opts) do
    algorithm = Keyword.get(opts, :algorithm, "reverse-chronological")
    limit = Keyword.get(opts, :limit, 30)

    %{limit: limit, algorithm: algorithm}
  end

  @spec build_actor_query(Session.t(), actor: String.t() | nil) :: RequestUtils.URI.query_params()
  defp build_actor_query(session, opts \\ []) do
    %{actor: opts[:actor] || session.did}
  end
end