Current section

Files

Jump to
amarula lib amarula profile.ex
Raw

lib/amarula/profile.ex

defmodule Amarula.Profile do
@moduledoc """
Profile reads and writes: fetch a profile-picture URL, set/remove your (or a
group's) picture, and set your status/bio. The consumer-facing half of Baileys'
`profilePictureUrl` / `updateProfilePicture` / `removeProfilePicture` /
`updateProfileStatus`.
Each function builds an IQ via `Amarula.Protocol.Profile.Ops`, sends it through
`Amarula.Connection.query_iq/3`, and parses the reply. Call them on this module:
`picture_url/3`, `update_picture/3`, `remove_picture/2`, `update_status/2`.
> #### Picture URL privacy token {: .info}
> `picture_url/3` implements the common path. WhatsApp can require a per-contact
> privacy token (`tctoken`) for *other* users' pictures; that path is not yet
> implemented, so fetching some contacts' pictures may return `nil`. Your own and
> group pictures are unaffected.
"""
alias Amarula.Address
alias Amarula.Connection
alias Amarula.Protocol.Profile.Ops
@type conn :: GenServer.server()
@doc """
Fetch the profile-picture URL for `jid` (a user or group). `type` is `:preview`
(small, default) or `:image` (full). Returns `{:ok, url}`, `{:ok, nil}` when there
is no picture (or it is not visible to you), or `{:error, reason}`.
"""
@spec picture_url(conn(), String.t() | Address.t(), Ops.pic_type()) ::
{:ok, String.t() | nil} | {:error, term()}
def picture_url(conn, jid, type \\ :preview) do
target = Address.to_jid!(jid)
with {:ok, reply} <- Connection.query_iq(conn, Ops.picture_url_query(target, type)) do
{:ok, Ops.parse_url(reply)}
end
end
@doc """
Set your own profile picture (or a group's, if `jid` is a group you administer)
from already-encoded JPEG bytes. WhatsApp expects a small square JPEG; the caller
must size it (Amarula does not resize — see the module note). Returns `:ok` or
`{:error, reason}`.
"""
@spec update_picture(conn(), String.t() | Address.t(), binary()) :: :ok | {:error, term()}
def update_picture(conn, jid, jpeg_bytes) do
target = target_for(conn, jid)
with {:ok, _reply} <- Connection.query_iq(conn, Ops.set_picture(target, jpeg_bytes)), do: :ok
end
@doc "Remove your own (or a group's) profile picture. Returns `:ok` or `{:error, reason}`."
@spec remove_picture(conn(), String.t() | Address.t()) :: :ok | {:error, term()}
def remove_picture(conn, jid) do
target = target_for(conn, jid)
with {:ok, _reply} <- Connection.query_iq(conn, Ops.remove_picture(target)), do: :ok
end
@doc "Set your own profile status/bio text. Returns `:ok` or `{:error, reason}`."
@spec update_status(conn(), String.t()) :: :ok | {:error, term()}
def update_status(conn, status) when is_binary(status) do
with {:ok, _reply} <- Connection.query_iq(conn, Ops.set_status(status)), do: :ok
end
# Resolve the `target` attr for a picture write: `nil` when `jid` is our own
# account (Baileys omits target for self), else the wire jid. Falls back to the
# wire jid if creds aren't available yet (e.g. not logged in).
defp target_for(conn, jid) do
addr = Address.parse!(jid)
if own_account?(conn, addr), do: nil, else: Address.to_jid!(addr)
end
# `get_auth_creds` returns a map; `me` defaults to `%{}` before login, so no
# address matches and we fall back to a targeted IQ — no special-casing needed.
defp own_account?(conn, %Address{} = addr) do
me = conn |> Connection.get_auth_creds() |> Map.get(:me, %{})
[Map.get(me, :id), Map.get(me, :lid)]
|> Enum.reject(&is_nil/1)
|> Enum.map(&Address.parse/1)
|> Enum.any?(fn mine -> mine && Address.same_account?(mine, addr) end)
end
end