mirror of
https://github.com/mvt-project/mvt.git
synced 2026-09-03 08:30:51 +02:00
Add timeline events for all WhatsApp contact timestamps
Extract ZABOUTEXPIRATIONTIMESTAMP and emit a timeline event for each timestamp stored on a WhatsApp contact record: disappearing messages timer changes, "about" text changes and scheduled expiry, and contact record updates. ContactsV2.sqlite stores no other date attributes in any released schema version.
This commit is contained in:
+1
-1
@@ -429,7 +429,7 @@ If indicators are provided through the command-line, they are checked against th
|
||||
Backup: :material-check:
|
||||
Full filesystem dump: :material-check:
|
||||
|
||||
This JSON file is created by mvt-ios' `WhatsappContacts` module. The module extracts WhatsApp contact records from the SQLite database located at *private/var/mobile/Containers/Shared/AppGroup/\*/ContactsV2.sqlite*, including each contact's phone number, WhatsApp and LID identifiers, and the per-contact disappearing messages timer, which is not recorded in *ChatStorage.sqlite*. Contacts with a disappearing messages timer set produce a `disappearing_mode_set` event in the timeline.
|
||||
This JSON file is created by mvt-ios' `WhatsappContacts` module. The module extracts WhatsApp contact records from the SQLite database located at *private/var/mobile/Containers/Shared/AppGroup/\*/ContactsV2.sqlite*, including each contact's phone number, WhatsApp and LID identifiers, and the per-contact disappearing messages timer, which is not recorded in *ChatStorage.sqlite*. Each timestamp stored on a contact record produces a timeline event: `disappearing_mode_set` (when the disappearing messages timer was last changed), `about_changed` (when the contact last changed their "about" text), `about_expiration` (when a timed "about" is scheduled to expire) and `contact_last_updated` (when the contact record was last updated).
|
||||
|
||||
This database is often missing from incremental backups. When it cannot be found, the module logs a warning and produces no results, in which case the disappearing messages state of chats cannot be determined from the backup.
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ COLUMN_CANDIDATES = {
|
||||
"disappearing_mode_duration": ["ZDISAPPEARINGMODEDURATION"],
|
||||
"disappearing_mode_timestamp": ["ZDISAPPEARINGMODETIMESTAMP"],
|
||||
"about_timestamp": ["ZABOUTTIMESTAMP"],
|
||||
"about_expiration_timestamp": ["ZABOUTEXPIRATIONTIMESTAMP"],
|
||||
"last_updated": ["ZLASTUPDATED"],
|
||||
"phone_status": ["ZPHONESTATUS", "ZPHONENUMBERSTATUS"],
|
||||
"sync_policy": ["ZSYNCPOLICY"],
|
||||
@@ -76,6 +77,7 @@ STRING_FIELDS = [
|
||||
DATE_FIELDS = [
|
||||
"disappearing_mode_timestamp",
|
||||
"about_timestamp",
|
||||
"about_expiration_timestamp",
|
||||
"last_updated",
|
||||
]
|
||||
|
||||
@@ -98,6 +100,19 @@ def _label_duration(duration) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _describe_contact(record: ModuleAtomicResult) -> str:
|
||||
contact = (
|
||||
record.get("whatsapp_id")
|
||||
or record.get("lid")
|
||||
or record.get("phone_number")
|
||||
or "unknown"
|
||||
)
|
||||
full_name = record.get("full_name")
|
||||
if full_name:
|
||||
contact = f"{contact} ({full_name})"
|
||||
return contact
|
||||
|
||||
|
||||
class WhatsappContacts(IOSExtraction):
|
||||
"""This module extracts WhatsApp contact records and per-contact
|
||||
disappearing-messages settings from ContactsV2.sqlite.
|
||||
@@ -127,30 +142,61 @@ class WhatsappContacts(IOSExtraction):
|
||||
)
|
||||
|
||||
def serialize(self, record: ModuleAtomicResult) -> ModuleSerializedResult:
|
||||
timestamp = record.get("disappearing_mode_timestamp")
|
||||
if not timestamp:
|
||||
return {}
|
||||
records = []
|
||||
contact = _describe_contact(record)
|
||||
|
||||
contact = (
|
||||
record.get("whatsapp_id")
|
||||
or record.get("lid")
|
||||
or record.get("phone_number")
|
||||
or "unknown"
|
||||
)
|
||||
data = (
|
||||
f"WhatsApp disappearing messages timer set to "
|
||||
f"'{record.get('disappearing_mode_label')}' for {contact}"
|
||||
)
|
||||
full_name = record.get("full_name")
|
||||
if full_name:
|
||||
data += f" ({full_name})"
|
||||
if record.get("disappearing_mode_timestamp"):
|
||||
records.append(
|
||||
{
|
||||
"timestamp": record["disappearing_mode_timestamp"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": "disappearing_mode_set",
|
||||
"data": (
|
||||
f"WhatsApp disappearing messages timer set to "
|
||||
f"'{record.get('disappearing_mode_label')}' "
|
||||
f"for {contact}"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"timestamp": timestamp,
|
||||
"module": self.__class__.__name__,
|
||||
"event": "disappearing_mode_set",
|
||||
"data": data,
|
||||
}
|
||||
if record.get("about_timestamp"):
|
||||
data = f"WhatsApp about text of {contact} changed"
|
||||
about_text = record.get("about_text")
|
||||
if about_text:
|
||||
data += f' to "{about_text}"'
|
||||
records.append(
|
||||
{
|
||||
"timestamp": record["about_timestamp"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": "about_changed",
|
||||
"data": data,
|
||||
}
|
||||
)
|
||||
|
||||
if record.get("about_expiration_timestamp"):
|
||||
records.append(
|
||||
{
|
||||
"timestamp": record["about_expiration_timestamp"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": "about_expiration",
|
||||
"data": (
|
||||
f"WhatsApp about text of {contact} scheduled "
|
||||
f"to expire"
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
if record.get("last_updated"):
|
||||
records.append(
|
||||
{
|
||||
"timestamp": record["last_updated"],
|
||||
"module": self.__class__.__name__,
|
||||
"event": "contact_last_updated",
|
||||
"data": f"WhatsApp contact record for {contact} updated",
|
||||
}
|
||||
)
|
||||
|
||||
return records
|
||||
|
||||
def run(self) -> None:
|
||||
try:
|
||||
|
||||
Binary file not shown.
@@ -25,6 +25,8 @@ class TestWhatsappContactsModule:
|
||||
assert alice["disappearing_mode_is_on"] is True
|
||||
assert alice["disappearing_mode_label"] == "24 hours"
|
||||
assert alice["disappearing_mode_timestamp"] == "2025-07-23 21:46:40.000000"
|
||||
assert alice["about_timestamp"] == "2025-07-12 08:00:00.000000"
|
||||
assert alice["about_expiration_timestamp"] == "2025-08-16 01:20:00.000000"
|
||||
assert alice["last_updated"] == "2025-08-04 11:33:20.000000"
|
||||
|
||||
bob = next(r for r in m.results if r["given_name"] == "Bob")
|
||||
@@ -34,11 +36,43 @@ class TestWhatsappContactsModule:
|
||||
assert bob["disappearing_mode_label"] == "off"
|
||||
assert bob["disappearing_mode_timestamp"] is None
|
||||
|
||||
assert len(m.timeline) == 1
|
||||
assert m.timeline[0]["event"] == "disappearing_mode_set"
|
||||
assert m.timeline[0]["timestamp"] == "2025-07-23 21:46:40.000000"
|
||||
assert "24 hours" in m.timeline[0]["data"]
|
||||
assert "14155550100@s.whatsapp.net" in m.timeline[0]["data"]
|
||||
# Alice: disappearing_mode_set, about_changed, about_expiration and
|
||||
# contact_last_updated. Bob: contact_last_updated only.
|
||||
assert len(m.timeline) == 5
|
||||
|
||||
events = {
|
||||
(entry["event"], entry["timestamp"]): entry["data"]
|
||||
for entry in m.timeline
|
||||
}
|
||||
assert (
|
||||
"24 hours"
|
||||
in events[("disappearing_mode_set", "2025-07-23 21:46:40.000000")]
|
||||
)
|
||||
assert (
|
||||
"14155550100@s.whatsapp.net (Alice Example)"
|
||||
in events[("disappearing_mode_set", "2025-07-23 21:46:40.000000")]
|
||||
)
|
||||
assert (
|
||||
'changed to "Hey there! I am using WhatsApp."'
|
||||
in events[("about_changed", "2025-07-12 08:00:00.000000")]
|
||||
)
|
||||
assert (
|
||||
"scheduled to expire"
|
||||
in events[("about_expiration", "2025-08-16 01:20:00.000000")]
|
||||
)
|
||||
|
||||
updated = [
|
||||
entry["data"]
|
||||
for entry in m.timeline
|
||||
if entry["event"] == "contact_last_updated"
|
||||
]
|
||||
assert len(updated) == 2
|
||||
assert all(
|
||||
entry["timestamp"] == "2025-08-04 11:33:20.000000"
|
||||
for entry in m.timeline
|
||||
if entry["event"] == "contact_last_updated"
|
||||
)
|
||||
assert any("14155550101@s.whatsapp.net (Bob Example)" in d for d in updated)
|
||||
|
||||
assert len(m.alertstore.alerts) == 0
|
||||
|
||||
|
||||
Reference in New Issue
Block a user