Current section

Files

Jump to
amarula lib amarula protocol messages history_sync.ex
Raw

lib/amarula/protocol/messages/history_sync.ex

defmodule Amarula.Protocol.Messages.HistorySync do
@moduledoc """
Download + decode the history-sync blob a `HISTORY_SYNC_NOTIFICATION` points to,
ported from Baileys `downloadHistory`/`processHistoryMessage` (`Utils/history.ts`).
On first link (and incrementally), the primary device pushes the chat history as
an external, encrypted, zlib-deflated blob referenced by the notification. We
download it (media crypto, `:history` keys), inflate, and decode a
`Proto.HistorySync` — yielding the conversations (chats), contacts, and their
messages. This is what populates the chat list, and acking/consuming it is what
moves the phone from "Paused" to "active".
"""
alias Amarula.{Address, Chat, Contact}
alias Amarula.Protocol.Messages.Media
alias Amarula.Protocol.Proto
@type result :: %{
sync_type: atom(),
chats: [Chat.t()],
contacts: [Contact.t()],
push_names: [{String.t(), String.t()}]
}
@doc """
Download + decode the blob for a `%HistorySyncNotification{}`. Returns
`{:ok, %{sync_type, chats, contacts}}` or `{:error, reason}`.
"""
@spec fetch(struct()) :: {:ok, result()} | {:error, term()}
def fetch(notification) do
with {:ok, deflated} <- raw_blob(notification),
{:ok, raw} <- inflate(deflated) do
sync = Proto.HistorySync.decode(raw)
{:ok, to_result(sync)}
end
end
# A HISTORY_SYNC_NOTIFICATION is delivered one of two ways:
# * inline — `initialHistBootstrapInlinePayload` carries the (still-deflated)
# bytes directly (e.g. PUSH_NAME / small chunks); no download.
# * external — `directPath`+`mediaKey` reference an encrypted blob to download.
defp raw_blob(%{initialHistBootstrapInlinePayload: inline}) when is_binary(inline),
do: {:ok, inline}
defp raw_blob(%{directPath: dp, mediaKey: mk} = n) when is_binary(dp) and is_binary(mk) do
# `n` is a raw proto node (camelCase); build the canonical snake_case descriptor.
Media.download(%{direct_path: dp, url: Map.get(n, :url), media_key: mk}, :history)
end
defp raw_blob(_notification), do: {:error, :no_history_payload}
# The blob is raw zlib (deflate); inflate to the HistorySync protobuf bytes.
defp inflate(deflated) do
z = :zlib.open()
try do
:zlib.inflateInit(z)
raw = :zlib.inflate(z, deflated) |> IO.iodata_to_binary()
:zlib.inflateEnd(z)
{:ok, raw}
rescue
e -> {:error, {:inflate_failed, e}}
after
:zlib.close(z)
end
end
defp to_result(sync) do
convos = sync.conversations || []
%{
sync_type: sync.syncType,
chats: Enum.map(convos, &chat/1),
contacts: Enum.flat_map(convos, &contact/1),
# push names keyed by jid (incl. our own — used to learn me.name)
push_names: for(p <- sync.pushnames || [], p.pushname, do: {p.id, p.pushname})
}
end
defp chat(convo) do
%Chat{
address: Address.parse(convo.id),
archived: convo.archived,
pinned: pinned?(convo),
mute_end: convo.muteEndTime,
unread: convo.unreadCount
}
end
defp contact(convo) do
name = convo.displayName || convo.name
if is_binary(name) and name != "" do
[%Contact{address: Address.parse(convo.id), full_name: name}]
else
[]
end
end
defp pinned?(%{pinned: p}) when is_integer(p), do: p > 0
defp pinned?(_), do: nil
end