ExWapp.Proto.HistorySync (ExWapp v0.1.5)

Copy Markdown View Source

Extracts WhatsApp history sync payloads embedded in protocol messages.

Mirrors the flow used in reference clients:

  • Message.protocolMessage.historySyncNotification
  • initialHistBootstrapInlinePayload (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. extract_from_message/1 returns inline chunks immediately and returns {:pending_blob, blob_ref()} for media-delivered chunks. The session downloads those chunks in supervised background tasks and feeds the decrypted compressed bytes to parse_history_payload/2, then uses the same projection as inline history. This does not request additional history from the phone.

Summary

Types

Everything needed to download and decrypt a history-sync chunk delivered as media instead of inline. Kept verbatim from the notification so the background downloader needs no re-parse.

t()

Functions

Flattens a HistorySync payload into the internal contract.

Types

blob_ref()

@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 the background downloader needs no re-parse.

conversation()

@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()]
}

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
}

t()

@type t() :: %{
  sync_type: atom() | non_neg_integer() | nil,
  conversations: [conversation()],
  lid_mappings: [{String.t(), String.t()}],
  call_log_records: [ExWapp.Call.t()]
}

Functions

extract_from_message(message_payload)

@spec extract_from_message(binary()) ::
  {:ok, t()}
  | {:pending_blob, blob_ref()}
  | :not_history_sync
  | {:error, term()}

parse_history_payload(payload, sync_type)

@spec parse_history_payload(
  {:ok, binary()} | {:compressed, binary()} | binary(),
  term()
) ::
  {:ok, t()} | {:error, term()}

Flattens a HistorySync payload into the internal contract.

The inline path passes already-inflated bytes. Downloaded chunks use {:compressed, bytes} to validate zlib compression before decoding protobuf.