Packages

WhatsApp Web API client for Elixir. Full-featured port of Baileys with end-to-end Signal Protocol encryption, multi-device support, groups, communities, media, newsletters, and native BEAM fault tolerance.

Current section

Files

Jump to
baileys_ex lib baileys_ex signal lid_mapping_store.ex
Raw

lib/baileys_ex/signal/lid_mapping_store.ex

defmodule BaileysEx.Signal.LIDMappingStore do
@moduledoc """
Store-backed PN<->LID mapping helpers aligned with Baileys' lookup rules.
User-level mappings are persisted in the `:"lid-mapping"` family using the
same forward/reverse keys Baileys writes (`pn_user` and `lid_user_reverse`).
Device-specific JIDs are derived at read time so Signal addressing preserves
per-device separation without duplicating stored rows.
Reverse PN lookup is intentionally limited to base `@lid` users, matching the
Baileys reference. Derived `@hosted.lid` device addresses are produced during
forward PN lookup, but are not treated as stored reverse-lookup identities.
"""
alias BaileysEx.Protocol.JID
alias BaileysEx.Signal.Store
@type mapping :: %{pn: String.t(), lid: String.t()}
@type lookup_fun :: ([String.t()] -> [mapping()] | nil)
@type error :: :invalid_mapping
@doc "Records active mapping correlations between Phone Numbers and Local Identifiers to the store engine."
@spec store_lid_pn_mappings(Store.t(), [mapping()]) :: :ok | {:error, error()}
def store_lid_pn_mappings(%Store{} = store, mappings) when is_list(mappings) do
entries =
Enum.reduce(mappings, %{}, fn mapping, acc ->
case normalize_mapping(mapping) do
{:ok, {pn_user, lid_user}} ->
acc
|> Map.put(forward_key(pn_user), lid_user)
|> Map.put(reverse_key(lid_user), pn_user)
:skip ->
acc
end
end)
case map_size(entries) do
0 ->
:ok
_count ->
Store.transaction(store, "lid-mapping", fn tx_store ->
Store.set(tx_store, %{:"lid-mapping" => entries})
end)
end
end
def store_lid_pn_mappings(%Store{}, _mappings), do: {:error, :invalid_mapping}
@doc "Retrieves the single valid LID value mapped to a distinct phone number identifier."
@spec get_lid_for_pn(Store.t(), String.t(), keyword()) :: {:ok, String.t() | nil}
def get_lid_for_pn(store, pn, opts \\ [])
def get_lid_for_pn(%Store{} = store, pn, opts) when is_binary(pn) do
with {:ok, mappings} <- get_lids_for_pns(store, [pn], opts) do
{:ok, first_mapping_value(mappings, :lid)}
end
end
def get_lid_for_pn(%Store{}, _pn, _opts), do: {:ok, nil}
@doc "Bulk-requests numerous LID identities tied to multiple phone numbers."
@spec get_lids_for_pns(Store.t(), [String.t()], keyword()) :: {:ok, [mapping()] | nil}
def get_lids_for_pns(store, pns, opts \\ [])
def get_lids_for_pns(%Store{} = store, pns, opts) when is_list(pns) do
lookup = Keyword.get(opts, :lookup)
mappings =
Store.transaction(store, "lid-mapping", fn tx_store ->
{resolved, pending} = resolve_pn_reads(tx_store, pns)
resolve_pending_pns(tx_store, resolved, pending, lookup)
end)
{:ok, mappings}
end
def get_lids_for_pns(%Store{}, _pns, _opts), do: {:ok, nil}
@doc "Obtains a Phone Number from an alias LID mapped node structure."
@spec get_pn_for_lid(Store.t(), String.t()) :: {:ok, String.t() | nil}
def get_pn_for_lid(%Store{} = store, lid) when is_binary(lid) do
with {:ok, mappings} <- get_pns_for_lids(store, [lid]) do
{:ok, first_mapping_value(mappings, :pn)}
end
end
def get_pn_for_lid(%Store{}, _lid), do: {:ok, nil}
@doc "Loads multiple source Phone Numbers from Local Identifiers collectively."
@spec get_pns_for_lids(Store.t(), [String.t()]) :: {:ok, [mapping()] | nil}
def get_pns_for_lids(%Store{} = store, lids) when is_list(lids) do
resolved =
Enum.reduce(lids, %{}, fn lid, acc ->
case parse_lid(lid) do
{:ok, parsed} -> merge_reverse_mapping(store, acc, lid, parsed)
:error -> acc
end
end)
{:ok, values_or_nil(resolved)}
end
def get_pns_for_lids(%Store{}, _lids), do: {:ok, nil}
defp resolve_pn_reads(store, pns) do
parsed =
Enum.reduce(pns, [], fn pn, acc ->
case parse_pn(pn) do
{:ok, parsed_pn} -> [parsed_pn | acc]
:error -> acc
end
end)
|> Enum.reverse()
forward_keys =
parsed
|> Enum.map(&forward_key(&1.user))
|> Enum.uniq()
entries = Store.get(store, :"lid-mapping", forward_keys)
Enum.reduce(parsed, {%{}, []}, fn parsed_pn, {resolved, pending} ->
case Map.get(entries, forward_key(parsed_pn.user)) do
nil ->
{resolved, [parsed_pn | pending]}
lid_user ->
mapping = %{pn: parsed_pn.original, lid: build_lid(parsed_pn, lid_user)}
{Map.put(resolved, parsed_pn.original, mapping), pending}
end
end)
end
defp resolve_pending_pns(_store, resolved, [], _lookup), do: values_or_nil(resolved)
defp resolve_pending_pns(%Store{}, resolved, _pending, nil) do
values_or_nil(resolved)
end
defp resolve_pending_pns(%Store{} = store, resolved, pending, lookup) do
lookup_input =
pending
|> Enum.map(&normalize_lookup_pn/1)
|> Enum.uniq()
fetched = lookup.(lookup_input) || []
:ok = store_lid_pn_mappings(store, fetched)
forward_entries =
pending
|> Enum.map(&forward_key(&1.user))
|> Enum.uniq()
|> then(&Store.get(store, :"lid-mapping", &1))
pending
|> Enum.reduce(resolved, fn parsed_pn, acc ->
case Map.get(forward_entries, forward_key(parsed_pn.user)) do
nil ->
acc
lid_user ->
Map.put(acc, parsed_pn.original, %{
pn: parsed_pn.original,
lid: build_lid(parsed_pn, lid_user)
})
end
end)
|> values_or_nil()
end
defp normalize_lookup_pn(%{user: user, server: "hosted"}), do: "#{user}@s.whatsapp.net"
defp normalize_lookup_pn(%{user: user}), do: "#{user}@s.whatsapp.net"
defp merge_reverse_mapping(store, resolved, lid, parsed) do
reverse_entries = Store.get(store, :"lid-mapping", [reverse_key(parsed.user)])
case Map.get(reverse_entries, reverse_key(parsed.user)) do
nil -> resolved
pn_user -> Map.put(resolved, lid, %{lid: lid, pn: build_pn(parsed, pn_user)})
end
end
defp normalize_mapping(%{lid: lid, pn: pn}) do
normalize_mapping_pair(lid, pn)
end
defp normalize_mapping(_mapping), do: :skip
defp normalize_mapping_pair(lid, pn) do
cond do
lid_entry?(lid) and pn_entry?(pn) -> mapping_tuple(lid, pn)
lid_entry?(pn) and pn_entry?(lid) -> mapping_tuple(pn, lid)
true -> :skip
end
end
defp mapping_tuple(lid, pn) do
with {:ok, lid_parsed} <- parse_lid(lid),
{:ok, pn_parsed} <- parse_pn(pn) do
{:ok, {pn_parsed.user, lid_parsed.user}}
else
:error -> :skip
end
end
defp forward_key(pn_user), do: pn_user
defp reverse_key(lid_user), do: "#{lid_user}_reverse"
defp parse_pn(jid) do
case JID.parse(jid) do
%BaileysEx.JID{user: user, server: server, device: device}
when is_binary(user) and server in ["s.whatsapp.net", "c.us", "hosted"] ->
{:ok, %{original: jid, user: user, server: server, device: device || 0}}
_other ->
:error
end
end
defp parse_lid(jid) do
case JID.parse(jid) do
%BaileysEx.JID{user: user, server: server, device: device}
when is_binary(user) and server == "lid" ->
{:ok, %{original: jid, user: user, server: server, device: device || 0}}
_other ->
:error
end
end
defp pn_entry?(jid), do: match?({:ok, _parsed}, parse_pn(jid))
defp lid_entry?(jid), do: match?({:ok, _parsed}, parse_lid(jid))
defp build_lid(%{device: device, server: server}, lid_user) do
lid_server = if server == "hosted", do: "hosted.lid", else: "lid"
JID.jid_encode(lid_user, lid_server, device)
end
defp build_pn(%{device: device, server: server}, pn_user) do
pn_server = if server == "hosted.lid", do: "hosted", else: "s.whatsapp.net"
"#{pn_user}:#{device}@#{pn_server}"
end
defp values_or_nil(values) when map_size(values) == 0, do: nil
defp values_or_nil(values), do: Map.values(values)
defp first_mapping_value(nil, _key), do: nil
defp first_mapping_value([mapping | _], key), do: Map.fetch!(mapping, key)
end