Skip to main content

lib/amarula/protocol/profile/ops.ex

defmodule Amarula.Protocol.Profile.Ops do
  @moduledoc """
  Profile operations: build the IQs that read/change your (or a target's) profile
  picture and status, and parse their replies. Port of the profile-picture/status
  builders in Baileys `chats.ts` (`profilePictureUrl`, `updateProfilePicture`,
  `removeProfilePicture`, `updateProfileStatus`).

  Pure: builders return a `%Node{}`, parsers turn the reply node into a value. The
  IQ round-trip lives in `Amarula.Connection` (via `query_iq/3`). `target` is the
  jid being acted on, or `nil` for your own profile — Baileys omits the `target`
  attr entirely for self (sending it for your own jid makes the server never reply).
  """

  alias Amarula.Protocol.Binary.{Node, NodeUtils}
  alias Amarula.Protocol.Crypto.Constants

  @xmlns_picture "w:profile:picture"

  @type pic_type :: :preview | :image

  # --- IQ builders ---

  @doc """
  Fetch a profile picture URL. `target` is the jid to look up (a user or group jid);
  `type` is `:preview` (small) or `:image` (full). Reply parsed by `parse_url/1`.
  """
  @spec picture_url_query(String.t(), pic_type()) :: Node.t()
  def picture_url_query(target, type \\ :preview) when type in [:preview, :image] do
    picture = %Node{
      tag: "picture",
      attrs: %{"type" => Atom.to_string(type), "query" => "url"},
      content: nil
    }

    iq("get", @xmlns_picture, maybe_target(target), [picture])
  end

  @doc """
  Set a profile picture from already-encoded JPEG bytes. `target` is the jid being
  updated, or `nil` for your own profile. WhatsApp expects a small square JPEG; the
  caller is responsible for sizing it (Baileys resizes to 640px q50 before upload —
  Amarula does not resize, to avoid a native image dependency).
  """
  @spec set_picture(String.t() | nil, binary()) :: Node.t()
  def set_picture(target, jpeg_bytes) when is_binary(jpeg_bytes) do
    picture = %Node{tag: "picture", attrs: %{"type" => "image"}, content: jpeg_bytes}
    iq("set", @xmlns_picture, maybe_target(target), [picture])
  end

  @doc "Remove a profile picture. `target` is the jid, or `nil` for your own profile."
  @spec remove_picture(String.t() | nil) :: Node.t()
  def remove_picture(target) do
    iq("set", @xmlns_picture, maybe_target(target), nil)
  end

  @doc "Set your own profile status/bio text."
  @spec set_status(String.t()) :: Node.t()
  def set_status(status) when is_binary(status) do
    body = %Node{tag: "status", attrs: %{}, content: status}
    iq("set", "status", %{}, [body])
  end

  # --- reply parsing ---

  @doc "Pull the URL from a `w:profile:picture` reply (`<picture url=\"...\">`), or nil."
  @spec parse_url(Node.t()) :: String.t() | nil
  def parse_url(%Node{} = reply) do
    case NodeUtils.get_binary_node_child(reply, "picture") do
      %Node{} = picture -> NodeUtils.get_attr(picture, "url")
      _ -> nil
    end
  end

  # --- internals ---

  defp iq(type, xmlns, extra_attrs, content) do
    attrs =
      Map.merge(
        %{"to" => Constants.s_whatsapp_net(), "type" => type, "xmlns" => xmlns},
        extra_attrs
      )

    %Node{tag: "iq", attrs: attrs, content: content}
  end

  # Self ⇒ no `target` attr; a specific jid ⇒ target=jid.
  defp maybe_target(nil), do: %{}
  defp maybe_target(jid) when is_binary(jid), do: %{"target" => jid}
end