Amarula.Connection (amarula v0.4.2)
View SourceThe per-connection process that owns the entire server conversation: the
WebSocketClient, the noise cipher (frame encode/decode + counters), IQ
correlation, login/handshake, send dispatch, and server-notification handling.
It is also the consumer's endpoint — the pid Amarula.connect/2 returns.
Consumer calls (connect, send_text, group_*, …) land here directly, and
consumer events go straight to the connection's parent_pid as
{:amarula, type, data} (no relay process, no subscriber registry).
Per-send work stays tiny: Connection only frames + writes + correlates (acks,
IQ replies). The heavy USync/bundle waits and Signal encrypt run on the
per-recipient ConversationSender, which hands back a ready stanza to relay.
Summary
Functions
Cast a request to force-refresh sessions for newly mapped LIDs.
Canonicalize jid to its phone-number identity. If jid is a LID
(<n>@lid) with a stored PN mapping, returns the equivalent
<pn>@s.whatsapp.net (preserving any device). Any other jid — already a PN,
a group, or a LID with no known mapping — is returned unchanged.
Connects to the WebSocket server.
Strip a decoded frame's 1-byte prefix, inflating when compressed. Bit 1 (0x02) of the prefix means the remainder is zlib-compressed (Baileys decompressingIfRequired); the server compresses larger frames. Public for testability.
Disconnects from the WebSocket server.
Ask the phone for older history of a chat (PEER_DATA_OPERATION on-demand).
Current auth creds (carries me.id/me.lid/account once logged in).
The connection's %Amarula.Conn{} — its storage scope + profile. Library code
that needs to read/write the Storage seam (sessions, LID mappings) from the
caller's process holds the pid; this hands it the %Conn{} those stores key
by. Internal: consumers use the higher-level facade calls.
Gets the current connection state.
Fetch one group's metadata. group is an Address or the @g.us jid string.
Run a group management op: send the IQ Groups.Ops.<builder> produced and run
transform on the reply. transform is fn {:ok, node} | {:error, node} -> result end. Used by the Amarula group_* API.
Fetch all groups we participate in.
Start a connection instance and return its pid — the consumer's handle. Starts
the per-connection supervision tree (Registry, caches, sender supervisor, this
Connection) and hands back the Connection child pid, so the public API
(connect/send_text/... on that pid) lands here directly.
Send a read receipt for message_ids in chat jid (optional participant).
Cast newly-learned LID↔PN pairs so the consumer gets a :lid_mapping_update
event. pairs is [{lid_jid, pn_jid}, ...].
Whether msg is a message in our own chat (the "Message Yourself" chat) — i.e.
it's from_me and addressed to our own account.
Subscribe to a contact's presence.
Send an IQ and block until the matching websocket reply arrives.
Frame and send a stanza over the websocket (fire-and-forget; no IQ reply
awaited). Used by the send path to relay the final <message>.
Request a link-code (phone-number) pairing code for phone (digits only,
E.164 without +).
Ask the phone to re-deliver a message by key (PEER_DATA_OPERATION resend).
Send a chat-state to jid (:composing/:recording/:paused).
Send a contact (display_name + vCard string) to jid.
Send multiple contacts: pairs is [{display_name, vcard}, ...].
Edit a previously-sent message, replacing its text with new_text.
Convenience: send_media(pid, jid, :image, ...).
Send a location to jid. opts: :name, :address, :url, :is_live.
Send a media message to jid. type is :image/:video/:audio/
:document/:sticker; data is the raw bytes. opts may carry :mimetype
plus per-type extras. Encrypts + uploads + sends. {:ok, msg_id} or {:error, _}.
Send a pre-built %Proto.Message{} to jid (1:1 or group). Used for
reactions, edits, deletes and media. Returns {:ok, msg_id}.
Send a poll to jid. Returns {:ok, msg_id, message_secret} — keep the secret
to tally incoming votes. opts: :selectable, :announcement, :message_secret.
React to a message with emoji (pass "" to remove the reaction). target_key
is the %Proto.MessageKey{} of the message being reacted to.
Delete a message for everyone (revoke). target_key is its %Proto.MessageKey{}.
Send a 1:1/group text message to jid. Encrypts and relays (fetching the
recipient's prekey bundle first if we have no session). Returns {:ok, msg_id}.
Re-point the consumer event sink to sink (a sink/0 — pid, registered name,
{:via, …}, {name, node}, or nil) on a live connection, without bouncing the
websocket. The old sink is demonitored and the new one monitored.
Send global presence (:available/:unavailable). Needs me.name.
Internal. Starts the bare Connection process only.
Stop the whole connection tree (this Connection, its caches + sender supervisor),
freeing the profile registration. Unlike disconnect/1 (which only closes the
websocket, leaving the supervised tree up to reconnect), this releases the profile
so it can be started again elsewhere. Returns :ok | {:error, :not_found}.
Updates authentication credentials.
Unlink this companion server-side, wipe ALL local storage, then disconnect. Destructive.
Types
The consumer event sink — where {:amarula, type, data} events are delivered.
A raw pid() is the simplest form, but it is not restart-safe: if the
consumer process restarts under a new pid, a pid sink points at a corpse (only
set_parent/2 recovers it). Any name GenServer.whereis/1 resolves — a
registered atom, a {:via, mod, term} tuple (e.g. Amarula.via(profile) of
another registry), or a {name, node} for a remote consumer — re-resolves to
the current holder on every event, so it survives the consumer's restart and
even this Connection's own restart automatically. nil drops events.
@type t() :: %Amarula.Connection{ auth_creds: Amarula.Protocol.Auth.AuthUtils.auth_creds() | nil, config: Amarula.Protocol.Socket.Types.socket_config(), conn: term(), connection_state: Amarula.Protocol.Socket.Types.connection_state(), connection_timeout_timer: reference() | nil, handshake_state: Amarula.Protocol.Socket.ConnectionValidator.handshake_state() | nil, instance_id: reference() | nil, keep_alive_timer: reference() | nil, last_error: term() | nil, last_recv_time: non_neg_integer() | nil, max_retries: non_neg_integer(), message_counter: non_neg_integer(), message_epoch: non_neg_integer(), message_tag_prefix: String.t(), msg_retry_counts: %{required(String.t()) => non_neg_integer()}, noise_state: Amarula.Protocol.Crypto.NoiseHandler.noise_state() | nil, parent_monitor: reference() | nil, parent_pid: sink(), pending_acks: %{ required(String.t()) => {GenServer.from(), (:ok -> term()), reference(), String.t()} }, pending_iqs: %{required(String.t()) => {atom(), reference()}}, pending_sends: %{ required(String.t()) => %{ msg_id: String.t(), text: String.t(), target_jid: String.t(), devices: [map()] } }, qr_refs: [String.t()], qr_timer: reference() | nil, retry_count: non_neg_integer(), retry_delay: non_neg_integer(), retry_timer: reference() | nil, sender_monitors: %{required(String.t()) => reference()}, server_response_timeout_timer: reference() | nil, waiting_for_server_response: boolean(), websocket_client: pid() | nil, websocket_monitor: reference() | nil }
Functions
@spec assert_lid_sessions(GenServer.server(), [String.t()]) :: :ok
Cast a request to force-refresh sessions for newly mapped LIDs.
@spec canonical_jid(GenServer.server(), String.t()) :: String.t()
Canonicalize jid to its phone-number identity. If jid is a LID
(<n>@lid) with a stored PN mapping, returns the equivalent
<pn>@s.whatsapp.net (preserving any device). Any other jid — already a PN,
a group, or a LID with no known mapping — is returned unchanged.
This is the public entry point to the LID↔PN mapping the library maintains
internally; consumers no longer need to reach into Protocol.Signal.*.
Connects to the WebSocket server.
Strip a decoded frame's 1-byte prefix, inflating when compressed. Bit 1 (0x02) of the prefix means the remainder is zlib-compressed (Baileys decompressingIfRequired); the server compresses larger frames. Public for testability.
Disconnects from the WebSocket server.
Ask the phone for older history of a chat (PEER_DATA_OPERATION on-demand).
Current auth creds (carries me.id/me.lid/account once logged in).
@spec get_conn(GenServer.server()) :: Amarula.Conn.t()
The connection's %Amarula.Conn{} — its storage scope + profile. Library code
that needs to read/write the Storage seam (sessions, LID mappings) from the
caller's process holds the pid; this hands it the %Conn{} those stores key
by. Internal: consumers use the higher-level facade calls.
Gets the current connection state.
@spec group_metadata(GenServer.server(), Amarula.jid()) :: {:ok, Amarula.Group.t()} | {:error, term()}
Fetch one group's metadata. group is an Address or the @g.us jid string.
@spec group_op(GenServer.server(), Amarula.Protocol.Binary.Node.t(), (term() -> term())) :: term()
Run a group management op: send the IQ Groups.Ops.<builder> produced and run
transform on the reply. transform is fn {:ok, node} | {:error, node} -> result end. Used by the Amarula group_* API.
@spec list_groups(GenServer.server()) :: {:ok, [Amarula.Group.t()]} | {:error, term()}
Fetch all groups we participate in.
@spec make_socket( Amarula.Conn.t(), keyword() ) :: {:ok, pid()} | {:error, {:already_running, pid()}} | {:error, term()}
Start a connection instance and return its pid — the consumer's handle. Starts
the per-connection supervision tree (Registry, caches, sender supervisor, this
Connection) and hands back the Connection child pid, so the public API
(connect/send_text/... on that pid) lands here directly.
opts may carry :parent_pid (events sink, default the caller).
@spec mark_read( GenServer.server(), Amarula.jid(), [String.t(), ...], Amarula.jid() | nil ) :: :ok | {:error, :not_connected}
Send a read receipt for message_ids in chat jid (optional participant).
@spec notify_lid_mappings(GenServer.server(), [{String.t(), String.t()}]) :: :ok
Cast newly-learned LID↔PN pairs so the consumer gets a :lid_mapping_update
event. pairs is [{lid_jid, pn_jid}, ...].
@spec own_chat?(GenServer.server(), Amarula.Msg.t()) :: boolean()
Whether msg is a message in our own chat (the "Message Yourself" chat) — i.e.
it's from_me and addressed to our own account.
This is the check a self-chat command channel needs (drive an agent by messaging
yourself), and it handles the LID/PN duality for you: WhatsApp may address the self
chat by either our PN (me.id, always present) or our LID (me.lid, present once the
server has sent it), so it matches msg.to against both of our own identities
(device ignored) rather than forcing a single normalized form.
On a single connection there is no feedback loop: a reply this connection sends to
the self chat is delivered to our other devices but not back to us (the send path
excludes our own sending device), so you don't need to filter your own sends. Only when
running two connections on the same account do their self-chat sends reach each
other — dedupe those cross-connection by the msg_id from the send.
@spec presence_subscribe(GenServer.server(), Amarula.jid()) :: :ok | {:error, :not_connected}
Subscribe to a contact's presence.
@spec query_iq(GenServer.server(), Amarula.Protocol.Binary.Node.t(), timeout()) :: {:ok, Amarula.Protocol.Binary.Node.t()} | {:error, Amarula.Protocol.Binary.Node.t() | :timeout | :not_connected}
Send an IQ and block until the matching websocket reply arrives.
Returns {:ok, node} on a type="result" reply, {:error, node} on an
error reply, or {:error, :timeout} if no reply comes within the IQ timeout.
The caller (a ConversationSender) blocks; Connection keeps owning the
socket and just routes the reply back. This is the only correlation primitive
the send path needs — no continuation logic lives here.
@spec relay_stanza(GenServer.server(), Amarula.Protocol.Binary.Node.t()) :: :ok | {:error, :not_connected}
Frame and send a stanza over the websocket (fire-and-forget; no IQ reply
awaited). Used by the send path to relay the final <message>.
@spec request_pairing_code(GenServer.server(), String.t(), keyword()) :: {:ok, String.t()} | {:error, term()}
Request a link-code (phone-number) pairing code for phone (digits only,
E.164 without +).
Call this during the QR window while unregistered (on the first
:connection_update carrying a qr). Returns {:ok, code} with an 8-char
code the user types into WhatsApp → Linked Devices → "Link with phone number".
The server later pushes a link_code_companion_reg notification, which we
finish internally; the usual 515 restart then logs in.
Pass custom_code: "ABCD2345" to use a fixed 8-char code instead of a random
one.
Ask the phone to re-deliver a message by key (PEER_DATA_OPERATION resend).
@spec send_chatstate( GenServer.server(), Amarula.jid(), :composing | :recording | :paused ) :: :ok | {:error, :not_connected}
Send a chat-state to jid (:composing/:recording/:paused).
Send a contact (display_name + vCard string) to jid.
Send multiple contacts: pairs is [{display_name, vcard}, ...].
Edit a previously-sent message, replacing its text with new_text.
Convenience: send_media(pid, jid, :image, ...).
Send a location to jid. opts: :name, :address, :url, :is_live.
Send a media message to jid. type is :image/:video/:audio/
:document/:sticker; data is the raw bytes. opts may carry :mimetype
plus per-type extras. Encrypts + uploads + sends. {:ok, msg_id} or {:error, _}.
Send a pre-built %Proto.Message{} to jid (1:1 or group). Used for
reactions, edits, deletes and media. Returns {:ok, msg_id}.
Send a poll to jid. Returns {:ok, msg_id, message_secret} — keep the secret
to tally incoming votes. opts: :selectable, :announcement, :message_secret.
React to a message with emoji (pass "" to remove the reaction). target_key
is the %Proto.MessageKey{} of the message being reacted to.
Delete a message for everyone (revoke). target_key is its %Proto.MessageKey{}.
Send a 1:1/group text message to jid. Encrypts and relays (fetching the
recipient's prekey bundle first if we have no session). Returns {:ok, msg_id}.
@spec set_parent(GenServer.server(), sink()) :: :ok
Re-point the consumer event sink to sink (a sink/0 — pid, registered name,
{:via, …}, {name, node}, or nil) on a live connection, without bouncing the
websocket. The old sink is demonitored and the new one monitored.
This is the recovery path when the process that called connect/2 restarts while
the connection survives: the new consumer re-attaches instead of forcing a
stop+reconnect. For a sink that should survive both the consumer's and this
connection's restarts, pass a name rather than a raw pid (see sink/0).
@spec set_presence(GenServer.server(), :available | :unavailable) :: :ok | {:error, term()}
Send global presence (:available/:unavailable). Needs me.name.
Internal. Starts the bare Connection process only.
This starts just this GenServer — not a working connection. A functional
connection needs the surrounding per-connection tree (its sender supervisor and
the InstanceRegistry wiring its siblings resolve through); without that, the
first send fails. ConnectionSupervisor calls this as the tree's child.
Consumers must not put {Amarula.Connection, …} in their own supervisor —
that yields a half-wired connection. The supported entry point is
Amarula.connect/2, which builds the whole tree (and owns the protocol-driven
restarts — e.g. the 515 reconnect after QR pairing — that the consumer should
not have to manage). To control naming/distribution, pass a :registry in the
connection config (e.g. Horde.Registry); see Amarula.ProfileRegistry.
opts:
:name— registered name (a:viatuple intoInstanceRegistry, passed byConnectionSupervisor). Omit to start an unnamed process.:parent_pid— process to receive{:amarula, type, data}events
@spec stop(pid()) :: :ok | {:error, :not_found}
Stop the whole connection tree (this Connection, its caches + sender supervisor),
freeing the profile registration. Unlike disconnect/1 (which only closes the
websocket, leaving the supervised tree up to reconnect), this releases the profile
so it can be started again elsewhere. Returns :ok | {:error, :not_found}.
Updates authentication credentials.
Called when credentials are updated (e.g., after successful pairing).
@spec wipe_credentials(GenServer.server()) :: :ok | {:error, term()}
Unlink this companion server-side, wipe ALL local storage, then disconnect. Destructive.