Extracts WhatsApp history sync payloads embedded in protocol messages.
Mirrors the flow used in reference clients:
Message.protocolMessage.historySyncNotificationinitialHistBootstrapInlinePayload(zlib-compressed)HistorySync.conversations
Decoding uses the generated WAWebProtobufsHistorySync modules; this module
only flattens the structs into the internal map contract.
Inline chunks versus blob chunks
WhatsApp delivers the bootstrap chunk inline (initialHistBootstrapInlinePayload),
and the larger RECENT/FULL chunks as encrypted media the client is expected to
download. Only the inline path is implemented: the bootstrap gives roughly the
latest message per chat, which is enough for a chat list but not for reading
conversation history.
Blob chunks are not silently discarded — extract_from_message/1 returns
{:pending_blob, blob_ref()} with everything a download needs, so the gap is a
missing consumer rather than lost data. To close it:
- teach
ExWapp.Media.Cryptothe:historytype — HKDF context string"WhatsApp History Keys"(whatsmeowdownload.go), and allow it invalidate_ref/1; - hand the
blob_ref()to the existingExWapp.Media.HTTP.download/3, which already resolvesmedia_connfromdirect_pathand verifies the HMAC; - zlib-inflate the plaintext and feed it to
parse_history_payload/2below — the parsing and flattening already work, they are what the inline path uses.
Run the download off the inbound message path (a chunk can be megabytes); whatsmeow uses a dedicated worker with an idle timeout for exactly this reason.
Summary
Types
Everything needed to download and decrypt a history-sync chunk delivered as media instead of inline. Kept verbatim from the notification so a future downloader needs no re-parse.
Functions
Flattens an already-inflated HistorySync payload into the internal contract.
Types
@type blob_ref() :: %{ sync_type: atom() | non_neg_integer() | nil, chunk_order: non_neg_integer() | nil, progress: non_neg_integer() | nil, oldest_msg_timestamp: non_neg_integer() | nil, media_key: binary() | nil, direct_path: String.t() | nil, enc_handle: String.t() | nil, file_sha256: binary() | nil, file_enc_sha256: binary() | nil, file_length: non_neg_integer() | nil }
Everything needed to download and decrypt a history-sync chunk delivered as media instead of inline. Kept verbatim from the notification so a future downloader needs no re-parse.
@type conversation() :: %{ jid: String.t(), name: String.t() | nil, unread_count: non_neg_integer() | nil, last_message_timestamp: non_neg_integer() | nil, pinned: boolean() | nil, archived: boolean() | nil, muted_until: non_neg_integer() | nil, is_group: boolean(), pn_jid: String.t() | nil, lid_jid: String.t() | nil, tc_token: binary() | nil, tc_token_timestamp: non_neg_integer() | nil, tc_token_sender_timestamp: non_neg_integer() | nil, messages: [conversation_message()] }
@type conversation_message() :: %{ id: String.t() | nil, from_me: boolean(), timestamp: non_neg_integer() | nil, text: String.t() | nil, participant: String.t() | nil, remote_jid: String.t() | nil, order_id: non_neg_integer() | nil }
@type t() :: %{ sync_type: atom() | non_neg_integer() | nil, conversations: [conversation()], lid_mappings: [{String.t(), String.t()}], call_log_records: [ExWapp.Call.t()] }
Functions
Flattens an already-inflated HistorySync payload into the internal contract.
The inline path calls this after inflating the bootstrap payload; a future blob downloader can call it with the decrypted-and-inflated chunk instead.