Current section

Files

Jump to
amarula lib amarula protocol usync devices.ex
Raw

lib/amarula/protocol/usync/devices.ex

defmodule Amarula.Protocol.USync.Devices do
@moduledoc """
Turn a parsed USync result into the concrete list of device JIDs to encrypt
for. Port of Baileys `extractDeviceJids` (`src/Utils/signal.ts`).
Each USync result entry carries the user's `id` (jid) and a `"devices"` map
with a `:device_list`. We expand that into one JID per device, applying the
same three filters Baileys uses:
1. drop device 0 when `exclude_zero_devices?` is set
2. drop our own sending device (same user *and* same device)
3. drop non-zero devices that lack a `key_index` (server rejects them)
"""
alias Amarula.Protocol.Binary.JID
@type device_jid :: %{
user: String.t(),
device: non_neg_integer(),
server: String.t(),
jid: String.t()
}
@doc """
Expand `result_list` (the `:list` from `USync.parse_result/2`) into device JIDs.
`my_jid` / `my_lid` identify our own account so we can skip our sending
device. `exclude_zero_devices?` mirrors Baileys' `excludeZeroDevices` flag.
"""
@spec extract([map()], String.t(), String.t() | nil, boolean()) :: [device_jid()]
def extract(result_list, my_jid, my_lid, exclude_zero_devices?) do
%{user: my_user, device: my_device} = decode_self(my_jid)
my_lid_user = my_lid && Map.get(JID.decode(my_lid) || %{}, :user)
Enum.flat_map(result_list, fn entry ->
with %{user: user, server: server} <- JID.decode(entry.id),
device_list when is_list(device_list) <- device_list(entry) do
Enum.flat_map(device_list, fn device ->
expand_device(device, %{
user: user,
server: server,
my_user: my_user,
my_lid_user: my_lid_user,
my_device: my_device,
exclude_zero?: exclude_zero_devices?
})
end)
else
_ -> []
end
end)
end
defp expand_device(%{id: device, key_index: key_index}, ctx) do
if keep_device?(device, key_index, ctx) do
jid = JID.encode(%{user: ctx.user, server: ctx.server, device: device})
[%{user: ctx.user, device: device, server: ctx.server, jid: jid}]
else
[]
end
end
# Three-part filter, matching Baileys' condition exactly.
defp keep_device?(device, key_index, ctx) do
not_excluded_zero?(device, ctx.exclude_zero?) and
not_own_device?(device, ctx) and
addressable?(device, key_index)
end
defp not_excluded_zero?(0, true), do: false
defp not_excluded_zero?(_device, _exclude?), do: true
# Either a different user, or — if it is us — a different device than ours.
defp not_own_device?(device, %{
user: user,
my_user: my_user,
my_lid_user: my_lid_user,
my_device: my_device
}) do
user not in [my_user, my_lid_user] or device != my_device
end
defp addressable?(0, _key_index), do: true
defp addressable?(_device, key_index), do: not is_nil(key_index) and key_index != 0
defp device_list(%{"devices" => %{device_list: list}}), do: list
defp device_list(_), do: nil
# our own jid may or may not carry a device suffix; default to device 0.
defp decode_self(my_jid) do
case JID.decode(my_jid) do
%{user: user} = decoded -> %{user: user, device: Map.get(decoded, :device, 0) || 0}
_ -> %{user: nil, device: 0}
end
end
end