Skip to content

angee.messaging.backup_ingest ​

Shared batching + resume-watermark loop for device-backup message import.

A chat-backup import is the same three moves for every platform: read the store's messages in thread order, map each to the neutral :class:ParsedMessage, and flush through Message.objects.ingest in bounded batches. Because ingest is idempotent on (channel, external_id) the import is resumable — but a naive re-run re-reads the whole history (and re-fetches its media) every time. Per-thread resume watermarks let the store skip each thread's already-imported prefix in its own SQL, so an import interrupted by a task time limit advances on re-run instead of restarting.

The store (the platform-specific reader) owns the wire schema, media resolution, and the date encoding; this owner is neutral of all three. It reads landed Message rows grouped by thread.external_id — the chat:<channel-pk>:<thread-external-id> key, stripped back to the store's raw thread key — and hands the store each thread's newest already-imported instant as a plain datetime; the store converts those to its native date filter. That chat:<channel-pk>: key is derived inline here (_THREAD_KEY_PREFIX), mirroring how Message.objects.ingest composes the same key inline when it lands a chat thread (angee.messaging.managers); there is no shared manager method for it yet, so both sites spell the format out. Centralizing the derivation on a Thread/Message manager method — the single owner both the ingest and this resume path would then call — is a follow-up.

The WhatsApp addon predates this owner and still carries its own copy in :class:angee.messaging_integrate_whatsapp.backup.BackupImporter; migrating it onto this module (its _resume_watermarks normalizes the recovered key with bare_jid and pre-converts to CoreData seconds — both moves this owner leaves to the store) is a mechanical follow-up.

DEFAULT_MAX_BATCH_BYTES ​

Flush a batch once its buffered media reaches this many bytes.

The message-count default is fine for text, but a run of large videos would otherwise hold gigabytes of media bytes resident before the first ingest.

ContentKeyCounter ​

python
class ContentKeyCounter()

Derive stable ids only for export records that carry no native id.

Prefer the export's native id whenever one exists. For an id-less record, :meth:key returns <thread>:<timestamp_ms>:<sender>:<content-digest>:<occurrence>. occurrence counts previously seen identical tuples within the thread, preserving true duplicates while a fresh counter over the same deterministic export reproduces exactly the same ids.

key ​

python
def key(*, thread_key: str, timestamp_ms: int, sender_key: str,
        content: Any) -> str

Return the next deterministic external id for one id-less record.

thread_watermarks ​

python
def thread_watermarks(channel: Any, *, reason: str) -> dict[str, datetime]

Return each chat thread's newest already-imported instant, keyed by store key.

The map is empty for a first import. Keyed by the store's raw thread key — the thread's chat:<channel>:<key> external id with the manager-owned prefix stripped — so the store can skip the imported prefix per thread. A global since would instead drop older messages in threads a prior interrupted run never reached; per-thread watermarks converge on the last imported row and advance from there. The store owns turning each datetime into its native date filter (the reason this owner stays date-encoding neutral).

batch_ingest ​

python
def batch_ingest(channel: Any,
                 messages: Iterable[Any],
                 parsed_message: Callable[[Any], ParsedMessage],
                 *,
                 reason: str,
                 batch_size: int = 500,
                 max_batch_bytes: int = DEFAULT_MAX_BATCH_BYTES,
                 dry_run: bool = False,
                 on_batch: Callable[[int], None] | None = None) -> int

Drive a store's messages through the shared ingest path in batches; return the total.

messages yields the store's platform DTOs (each exposing media for byte accounting); parsed_message maps one DTO onto the neutral messaging seam. A batch flushes at batch_size messages or max_batch_bytes of buffered media, whichever comes first. Every chat backup lands under the CHAT kind with the email quotation graph off, so those are fixed here rather than re-decided per addon.

Released under the AGPL-3.0 License.