@agentcomms/whatsapp 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +258 -0
- package/THIRD_PARTY_LICENSES +924 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +3584 -0
- package/dist/coerce-BscjcmYU.mjs +34 -0
- package/dist/dist-COPefvtq-D67MoOnB.mjs +9880 -0
- package/dist/index.d.mts +11605 -0
- package/dist/index.mjs +12 -0
- package/dist/mcp-DXXb3Vv3-Co5BzjYe.mjs +16220 -0
- package/dist/schemas-DiGKNDNq.mjs +5885 -0
- package/dist/server-DJZWYUNK.mjs +215 -0
- package/dist/src-D_zzAWoS-BhK0GFdq-XAvBzdGB.mjs +7806 -0
- package/dist/stdio-BR8R87El.mjs +102 -0
- package/dist/stdio-CHZ2WRcx-5iYMXTdw.mjs +607 -0
- package/dist/stdio-entry-m_AQ55DZ.mjs +21 -0
- package/dist/sync-BUT0jSeG.mjs +9360 -0
- package/package.json +91 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Criss Moldovan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# @agentcomms/whatsapp
|
|
2
|
+
|
|
3
|
+
WhatsApp for coding agents, **read-only**. An agent can list, read and search the chats WhatsApp for Mac already
|
|
4
|
+
keeps on your Mac, and draft a reply — which comes back as a link that opens WhatsApp with the text filled in.
|
|
5
|
+
**You press send.** The package has no network client, no WhatsApp session and no way to send anything.
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install -g @agentcomms/whatsapp # or run it with npx -y @agentcomms/whatsapp <command>
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
It needs **macOS with WhatsApp for Mac** installed and signed in, and **Node 22.16 or newer** (it reads with Node's
|
|
12
|
+
own SQLite, which is complete from 22.16; an older Node is refused with what to install).
|
|
13
|
+
|
|
14
|
+
## What it does, and what it never does, in plain words
|
|
15
|
+
|
|
16
|
+
- **It reads only local files.** Nothing it does to read, search, draft or keep its lists connects to WhatsApp, or
|
|
17
|
+
to anything: no socket, no HTTP, no DNS. Its manifest declares no host (`"hosts": []`). A test builds the published
|
|
18
|
+
bundle and checks that no network module is anywhere in it; another runs those commands and every tool with the
|
|
19
|
+
network cut off, and `mcp install` only with `--print`. Installing is the exception, and it reaches npm, never
|
|
20
|
+
WhatsApp: `npm install`, and `mcp install`, which by default installs this package from npm into agentcomms' own
|
|
21
|
+
runtime (with `--launcher npx`, the client fetches it from npm each time it starts the server).
|
|
22
|
+
- **It never sends, marks as read, reacts, or shows you as online or typing.** There is no code that could. A draft
|
|
23
|
+
is a `whatsapp://send` or `https://wa.me/` link; WhatsApp fills in the message and waits for you.
|
|
24
|
+
- **It never writes to WhatsApp's files.** It copies the message store and its write-ahead log, reads the copy, and
|
|
25
|
+
deletes it. A test checks WhatsApp's folder is byte-for-byte and mtime-for-mtime unchanged after a sync.
|
|
26
|
+
- **It reads one file.** Only `ChatStorage.sqlite` and its log are opened, by exact name. The same folder holds the
|
|
27
|
+
encryption keys that make the Mac a linked device (`Axolotl.sqlite`), contacts and media; none is ever read.
|
|
28
|
+
- **Media is described, never opened**: type, size and file name.
|
|
29
|
+
|
|
30
|
+
**WhatsApp's terms.** WhatsApp forbids unofficial clients and automation, and bans numbers it catches — including
|
|
31
|
+
low-volume, reply-only use. The usual way to build this, a library that logs in as a linked device, is exactly that
|
|
32
|
+
kind of client (and a malicious copy of one, `lotusbail`, stole the sessions of 56,000 installs in December 2025). So
|
|
33
|
+
this does not do it: it reads a file the official app has already written to your Mac, and nothing it does reaches
|
|
34
|
+
WhatsApp's servers. That is a design choice, not legal advice. **Do not pair it with anything that sends** — another
|
|
35
|
+
WhatsApp MCP server, a script that types into WhatsApp — or the protection is gone; `mcp install` warns about any
|
|
36
|
+
other WhatsApp server it finds registered.
|
|
37
|
+
|
|
38
|
+
**What you give up, stated plainly.**
|
|
39
|
+
|
|
40
|
+
- **Your messages are copied into a local index**, in plain text, so an agent can search them. It is owner-only
|
|
41
|
+
(0600 in a 0700 folder, put back if anything loosens it), holds only the chats your lists let agents see, and
|
|
42
|
+
`remove` deletes it. It is **not encrypted**, and it does not have the protection WhatsApp's own folder has:
|
|
43
|
+
macOS asks before an app reads WhatsApp's folder, and it does not ask before one reads this index. Anything that
|
|
44
|
+
runs as you can read it. (Why not encrypted: Node's SQLite cannot open an encrypted database or load one from
|
|
45
|
+
memory, so the index would have to be decrypted to disk for every read — a plaintext copy anyway — and the key
|
|
46
|
+
would sit in a store every program running as you can read.)
|
|
47
|
+
- **macOS will ask for permission to read WhatsApp's data**, for the app this runs in — below. If you grant **Full
|
|
48
|
+
Disk Access** instead, you grant it to your whole terminal or MCP client, which is far broader than WhatsApp.
|
|
49
|
+
- **An agent reads other people's messages to you.** Hide chats with `deny`, or allow only some with `allow`.
|
|
50
|
+
Groups are visible unless you hide them, and a person you have denied is still visible in a group you have not:
|
|
51
|
+
to keep a group from agents, deny the group.
|
|
52
|
+
- **Messages are untrusted.** Anyone with your number can send text meant for the agent. Every message, name and
|
|
53
|
+
file name reaches it inside the untrusted-content envelope, with invisible and bidirectional characters removed
|
|
54
|
+
and counted; the residual risk is a model following an instruction anyway — which is why sending stays with you.
|
|
55
|
+
|
|
56
|
+
## Getting started
|
|
57
|
+
|
|
58
|
+
In a terminal — these are yours to run, and each refuses an agent:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
agent-whatsapp add personal/whatsapp # names WhatsApp for Mac's store; macOS may ask: choose Allow
|
|
62
|
+
agent-whatsapp sync --account personal/whatsapp # copy, check, index, delete the copy
|
|
63
|
+
agent-whatsapp status
|
|
64
|
+
agent-whatsapp deny +15555550102 --account personal/whatsapp # optional: a chat agents never see
|
|
65
|
+
agent-whatsapp mcp install --client claude-code --account personal/whatsapp
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`mcp install` registers the server with your client, pinned to that account, as a change you approve: at a terminal
|
|
69
|
+
you type `yes` to what it shows; run by an agent it exits `10` with a preview and an approval id, and the same command
|
|
70
|
+
with `--approval <id>` registers it after your yes. Under the `confirm` change policy you approve with
|
|
71
|
+
`agent-whatsapp approve <id>` and a code instead. Restart the client afterwards. From a chat, the core server's
|
|
72
|
+
`comms_server_install` with `channel: "whatsapp"` and `account` is the same change.
|
|
73
|
+
|
|
74
|
+
For WhatsApp Business, add its store with
|
|
75
|
+
`--source ~/Library/Group\ Containers/group.net.whatsapp.WhatsAppSMB.shared/ChatStorage.sqlite`.
|
|
76
|
+
|
|
77
|
+
## macOS permission
|
|
78
|
+
|
|
79
|
+
macOS protects other apps' data. On recent versions (reported for group containers from macOS 15.2), the first time a
|
|
80
|
+
process reads WhatsApp's folder macOS asks **"<app> would like to access data from other apps"** — where `<app>` is the
|
|
81
|
+
one responsible for the process: the terminal you run the command in, or the MCP client that started the server —
|
|
82
|
+
never Node itself. Until someone answers, the read waits. Older versions may not ask at all, and either allow the read or
|
|
83
|
+
refuse it.
|
|
84
|
+
|
|
85
|
+
- **Allow** fixes it for that app's session. A background MCP server cannot click it, and the answer does not always
|
|
86
|
+
persist for background processes — so run `add` and the first `sync` in a terminal.
|
|
87
|
+
- **Full Disk Access** makes it stick: System Settings → Privacy & Security → Full Disk Access, add the terminal (or the
|
|
88
|
+
MCP client), then quit and reopen it. It is much broader than WhatsApp; revoke it when you no longer need it.
|
|
89
|
+
|
|
90
|
+
The package says which: a refusal comes back as exit `77` / `AUTH_REQUIRED` with the steps and, when the environment
|
|
91
|
+
says which app it is, its name; a dialog nobody answers fails after 12 seconds as exit `75` with "look for the dialog"
|
|
92
|
+
instead of hanging; a missing store says WhatsApp for Mac may not be installed.
|
|
93
|
+
|
|
94
|
+
Only `add` and `sync` (and `status`, unless `--no-check`) touch WhatsApp's folder. `chats`, `read` and `search` open
|
|
95
|
+
only the index — so they cannot raise a dialog, and they keep working, on what was last synced, if WhatsApp is closed,
|
|
96
|
+
updating or gone.
|
|
97
|
+
|
|
98
|
+
## Commands and tools
|
|
99
|
+
|
|
100
|
+
| Command | MCP tool | What it does |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `add <org/whatsapp> [--source]` | — | a person names the store; the first read, when macOS asks |
|
|
103
|
+
| `remove <org/whatsapp>` | — | forget it, its chat lists and its index; WhatsApp's own store is not touched |
|
|
104
|
+
| `allow <chat> --account` | — | let agents see this chat; once any is allowed, only allowed chats are visible |
|
|
105
|
+
| `deny <chat> --account` | — | hide this chat from agents entirely |
|
|
106
|
+
| `clear [chat] --account` | — | take a chat off both lists, or with no chat empty them |
|
|
107
|
+
| `status [--account] [--no-check]` | `whatsapp_status` | what is set up, whether it can be read, what the index holds |
|
|
108
|
+
| `sync --account` | `whatsapp_sync` | copy, check, index, delete the copy |
|
|
109
|
+
| `chats --account [--kind] [--limit]` | `whatsapp_chats` | chats, newest first; status updates only with `--kind status` |
|
|
110
|
+
| `read <chat> --account [--before] [--limit]` | `whatsapp_read` | one chat, newest first |
|
|
111
|
+
| `search <words> --account [--chat] [--sender] [--kind] [--limit]` | `whatsapp_search` | full-text: text, captions, file names, sender and chat names |
|
|
112
|
+
| `draft <to> <text> [--account] [--open]` | `whatsapp_draft` | a `whatsapp://send` and a `https://wa.me/` link; the person sends |
|
|
113
|
+
| `approve <id>` | — | approve a change — registering or pruning this server — at a terminal |
|
|
114
|
+
| `mcp [--account]` | — | the MCP server on stdio, pinned to one account when `--account` is given |
|
|
115
|
+
| `mcp install`, `mcp prune` | `comms_server_install`, `comms_server_prune` (core) | register the server with a client; remove old runtimes |
|
|
116
|
+
|
|
117
|
+
Each command and its tool run the same operation, and `capabilities.json` holds them to it. The commands with no
|
|
118
|
+
tool are the person's on purpose: which file on the Mac an agent reads, and which chats in it, are not an agent's to
|
|
119
|
+
decide, in either direction — `add`, `remove`, `allow`, `deny` and `clear` also refuse an agent at the command line.
|
|
120
|
+
`draft --open` is refused to an agent too: a filled-in message box landing on the screen of someone typing elsewhere
|
|
121
|
+
is one Enter away from sent.
|
|
122
|
+
|
|
123
|
+
`draft` takes a phone number or any id `chats` and `read` print. A group has no number, so its draft comes back as text
|
|
124
|
+
to paste, with the reason; so does a chat with someone who hides their number (`@lid`), a broadcast list and a channel.
|
|
125
|
+
A status update is not a chat anyone writes to, and is refused.
|
|
126
|
+
|
|
127
|
+
A draft cannot be used to find out which chats you hid. While your lists hide anything, a draft goes only to a chat
|
|
128
|
+
an agent could read — in the index, and visible — and a hidden one is refused with the same `NOT_FOUND` as a chat that
|
|
129
|
+
is not there; while they hide nothing, any number is drafted to. With no account named, every account's lists apply.
|
|
130
|
+
And every account's **deny** list applies to every draft, named account or not: a draft is a link to a number, not to
|
|
131
|
+
an account, so a number you denied on one is not drafted to by naming another. A pinned server consults its own
|
|
132
|
+
account's lists only.
|
|
133
|
+
|
|
134
|
+
**A pinned server** (`agent-whatsapp mcp --account personal/whatsapp`, which `mcp install --account` writes) acts on
|
|
135
|
+
that account whether or not a call names it, refuses any other, and names no other account — not in its greeting,
|
|
136
|
+
not in `whatsapp_status`. The pin follows the account through a rename.
|
|
137
|
+
|
|
138
|
+
**Kinds of chat.** From its id: `direct` (`…@s.whatsapp.net`), `hidden-number` (`…@lid`), `group` (`…@g.us`),
|
|
139
|
+
`channel` (`…@newsletter`), `broadcast` (`…@broadcast`) and `status` (WhatsApp's status feed, and one session per
|
|
140
|
+
contact's posts). Status updates are indexed, but `chats` and `search` leave them out unless asked for with
|
|
141
|
+
`--kind status`; one named by its id is read like any other chat. An id none of these match is `unknown`.
|
|
142
|
+
|
|
143
|
+
## Accounts, and the chats agents may see
|
|
144
|
+
|
|
145
|
+
An account is a record in agentcomms' own `config.json`, beside Gmail's mailboxes and Slack's workspaces, named
|
|
146
|
+
`organisation/whatsapp`. It is read-only by construction (`mode: "read"`, the only mode this channel has), names the
|
|
147
|
+
store by a stable id (`group.net.whatsapp.WhatsApp.shared` for WhatsApp for Mac), and holds no secret: its secret
|
|
148
|
+
reference is `whatsapp:none:<id>`, which names nothing, and `agentcomms secrets migrate` moves nothing for it. Because
|
|
149
|
+
it is core's record, a renamed account's old name is answered with its new one, and `comms_server_install` checks a
|
|
150
|
+
pin against it.
|
|
151
|
+
|
|
152
|
+
Each account can carry two lists, by chat id or phone number. A number on a list needs its country code, written with
|
|
153
|
+
`+` or `00` (`+1 555 555 0102`, `00 1 555 555 0102`): without one, `(555) 555-0102` could be a number in any country,
|
|
154
|
+
and an entry that matched no chat would hide nothing while saying it did, so it is refused. Each entry is looked up in
|
|
155
|
+
the index and the chat it names is shown; one that names no chat there is kept — you may hide a number before it
|
|
156
|
+
writes — and said to match none. (Anywhere a number is taken, `00` means `+`, and a number starting with a single `0`
|
|
157
|
+
is refused as national.)
|
|
158
|
+
|
|
159
|
+
- **deny** — chats an agent must never see. Not listed, searched, read, drafted to or counted; asking for one by id
|
|
160
|
+
gets the same `NOT_FOUND`, word for word, as a chat that does not exist.
|
|
161
|
+
- **allow** — once anything is on it, the only chats an agent sees. Denied wins over allowed.
|
|
162
|
+
|
|
163
|
+
A phone number names a person rather than one chat: their one-to-one chat, their own status posts and, in the status
|
|
164
|
+
feed, the posts they wrote. A status post is checked against its author, so it needs one: a post WhatsApp recorded no
|
|
165
|
+
author for could be anyone's, including someone you denied, so while either list has anything on it, such a post is
|
|
166
|
+
hidden, and `status` says how many (`unattributedStatus`). Your own posts are always shown. An author WhatsApp recorded
|
|
167
|
+
under a hidden-number id (`…@lid`) is matched by that id, not by their number: deny the id as well.
|
|
168
|
+
|
|
169
|
+
A group is a chat, allowed or denied whole: denying someone does not take what they wrote out of a group an agent may
|
|
170
|
+
see, whether or not WhatsApp recorded them as the author. That is a decision, not an oversight. Hiding one person's
|
|
171
|
+
lines would leave the others' replies and quotes describing them; a member can appear under a hidden-number id their
|
|
172
|
+
number does not match; and a group on the allow list would lose everyone who is not on it too. To keep a group from
|
|
173
|
+
agents, deny the group.
|
|
174
|
+
|
|
175
|
+
The lists apply at once to every read on both surfaces, and `sync` applies them too, leaving what they hide out of
|
|
176
|
+
the index — as they are when it finishes: a list changed while a sync runs is read again just before the new index
|
|
177
|
+
replaces the old, and the index is built again if it changed. `status` reports how many chats each list holds, never which.
|
|
178
|
+
|
|
179
|
+
The lists are kept in `whatsapp-chats.json` beside `config.json`, by account id — not in `config.json`. Everything in
|
|
180
|
+
that file is kept through every write, but only a few settings are *judged* when a write loosens something, and a
|
|
181
|
+
deny list there could be emptied by any program that writes it with nobody asked. In a file of its own, written only
|
|
182
|
+
by the person's three commands, and read so that a file that cannot be read shows no chat rather than every chat,
|
|
183
|
+
they stay the person's.
|
|
184
|
+
|
|
185
|
+
## Moving from the spike
|
|
186
|
+
|
|
187
|
+
If you ran the unpublished spike, its accounts are in `whatsapp-spike.json`. The first command or server start of this
|
|
188
|
+
release moves them into `config.json`, once: the same account id, so the index the spike built is read as it is with no
|
|
189
|
+
new sync (each chat's kind taken from its id, so the spike's `unknown` status sessions read as status updates); the
|
|
190
|
+
chat lists moved before the account appears; a name that is already taken left unmoved and said so, with where its
|
|
191
|
+
index — a plaintext copy of its messages that nothing reads now — was left for you to delete; the
|
|
192
|
+
old file kept as `whatsapp-spike.json.migrated-<time>`; one line in the audit log (`agentcomms audit tail`) and one on
|
|
193
|
+
stderr. It reads neither WhatsApp's store nor the spike's index to do it. A configuration still on the old flat names
|
|
194
|
+
waits until `agentcomms names migrate` has run.
|
|
195
|
+
|
|
196
|
+
## How the store is read
|
|
197
|
+
|
|
198
|
+
WhatsApp for Mac keeps the store at `~/Library/Group Containers/group.net.whatsapp.WhatsApp.shared/ChatStorage.sqlite`,
|
|
199
|
+
unencrypted on disk, and holds it open in SQLite's WAL mode while it runs: recent messages sit in
|
|
200
|
+
`ChatStorage.sqlite-wal` until the app folds them into the main file.
|
|
201
|
+
|
|
202
|
+
1. **Copy, don't open.** The store and its log are copied, byte for byte, into a private folder under agentcomms'
|
|
203
|
+
state directory, owner-only. Nothing opens them with SQLite, takes a lock on them, or touches the app's `-shm`
|
|
204
|
+
file. SQLite's `mode=ro` was rejected because a read-only connection still takes locks and writes read-marks into
|
|
205
|
+
the app's `-shm` file; `immutable=1` because it ignores the log, missing the newest messages, and can read torn
|
|
206
|
+
pages while the app writes.
|
|
207
|
+
2. **Open once, never through a link.** Each file is opened once, read-only, refusing a symbolic link, and every byte
|
|
208
|
+
is copied from that open file — its name is never opened again, so a name swapped for a link to the key store
|
|
209
|
+
between a check and the copy changes nothing. A file with a second name (a hard link, which could be the key
|
|
210
|
+
store's) or that is not a regular file is refused. The cost: Node's copy, which clones on APFS, takes a name and
|
|
211
|
+
would open it again, so the bytes are copied instead — the store's size on disk until the sync deletes it.
|
|
212
|
+
3. **Consistent, or not at all.** Each open file is fingerprinted (device, inode, links, size, nanosecond mtime)
|
|
213
|
+
before and after the copy, and each name is looked at again for a file that came, went or was replaced; if
|
|
214
|
+
WhatsApp wrote in between, the copy is discarded and taken again, up to five times, then refused. SQLite then
|
|
215
|
+
checks the copy (`quick_check`).
|
|
216
|
+
4. **Check the layout before reading.** A missing required table or column refuses the whole sync by name (exit `65`),
|
|
217
|
+
and the previous index is kept as it was. A missing optional part turns off one named feature and is reported.
|
|
218
|
+
5. **Index, then delete the copy.** The index is rebuilt in a new file and renamed into place, owner-only. The copy is
|
|
219
|
+
deleted whatever happens; one a crash left behind is removed by the next sync. `remove` holds the same lock as
|
|
220
|
+
`sync`: it waits for a sync that is running (up to a minute), then deletes the index that sync wrote with the rest,
|
|
221
|
+
and a sync that was waiting behind it finds the account gone and writes nothing.
|
|
222
|
+
|
|
223
|
+
WhatsApp can change this layout without notice. The reader refuses rather than guess when a required part is gone;
|
|
224
|
+
the risk it cannot catch is a column that keeps its name and changes its meaning, so check a handful of chats by eye
|
|
225
|
+
after a WhatsApp update if anything looks wrong.
|
|
226
|
+
|
|
227
|
+
## Where the schema comes from
|
|
228
|
+
|
|
229
|
+
Nobody opened a real store to write this — not the owner's, not even for its layout. Every table and column the
|
|
230
|
+
reader uses is one that public, open-source readers of `ChatStorage.sqlite` query by name (commits pinned in
|
|
231
|
+
`src/source/schema.ts`):
|
|
232
|
+
|
|
233
|
+
| Source | What it establishes |
|
|
234
|
+
|---|---|
|
|
235
|
+
| [kenn-io/msgvault](https://github.com/kenn-io/msgvault) `internal/whatsapp/apple.go` | the **macOS** app: `ZWACHATSESSION` (`ZCONTACTJID`, `ZPARTNERNAME`, `ZSESSIONTYPE`, `ZLASTMESSAGEDATE`), `ZWAGROUPMEMBER` (`ZMEMBERJID`, `ZCONTACTNAME`, `ZFIRSTNAME`), `@lid` chats, Core Data seconds since 2001-01-01 |
|
|
236
|
+
| [raycast/extensions](https://github.com/raycast/extensions) `extensions/whatsapp/src/services/readLocalDatabase.ts` | the **macOS** path; `ZSESSIONTYPE = 0` for one-to-one chats |
|
|
237
|
+
| [KnugiHK/WhatsApp-Chat-Exporter](https://github.com/KnugiHK/WhatsApp-Chat-Exporter) `ios_handler.py` | `ZWAMESSAGE` (`ZISFROMME`, `ZMESSAGEDATE`, `ZTEXT`, `ZMESSAGETYPE`, `ZSTANZAID`, `ZGROUPMEMBER`), `ZWAMEDIAITEM`, `ZWAPROFILEPUSHNAME`; epoch 978307200 |
|
|
238
|
+
| [abrignoni/iLEAPP](https://github.com/abrignoni/iLEAPP) `scripts/artifacts/whatsApp.py` | `ZFROMJID`, `ZTOJID`, `ZMEDIAITEM`, `ZGROUPEVENTTYPE`; type 5 is a location |
|
|
239
|
+
| [sepinf-inc/IPED](https://github.com/sepinf-inc/IPED) `ExtractorIOS.java` | `ZWAMEDIAITEM.ZFILESIZE`; `ZTITLE` is missing from older stores |
|
|
240
|
+
| [ForensicWace](https://github.com/Alessiop01/ForensicWace-ServerEdition) `globalConstants.py`, [wa-explorer](https://github.com/ludufre/wa-explorer) `docs/IOS_STORAGE.md` | the `ZMESSAGETYPE` values; `ZVCARDSTRING` holding a media item's MIME type |
|
|
241
|
+
|
|
242
|
+
The macOS app is the iOS app built for the Mac, which is why the iOS readers apply; the first two read the Mac file
|
|
243
|
+
itself. Facts were taken from these projects, not code. Where the sources say nothing — most `ZMESSAGETYPE` numbers
|
|
244
|
+
— the reader reports `unknown:<n>` rather than guess.
|
|
245
|
+
|
|
246
|
+
**What counts as media.** WhatsApp keeps a `ZWAMEDIAITEM` row for much more than media: a reply's quoted message lives
|
|
247
|
+
in it (KnugiHK reads replies from its `ZMETADATA`; iLEAPP counts 1,350 such rows), and the first run against a real
|
|
248
|
+
store found one on most text messages and on every call, with no type, no size and no file. So a row alone is not
|
|
249
|
+
media. A message is media when its type is one ForensicWace and wa-explorer name as media — a photo not yet downloaded
|
|
250
|
+
has no file and is still a photo — or when its row names a stored file (`ZMEDIALOCALPATH`, the test KnugiHK and
|
|
251
|
+
iLEAPP use). Anything else — text, a call, a location — shows no media line and is not counted as media. A call is
|
|
252
|
+
shown as a call, without a duration: both KnugiHK and iLEAPP read call durations from `CallHistory.sqlite`, a separate
|
|
253
|
+
file this reader never opens.
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
## Licence
|
|
257
|
+
|
|
258
|
+
[MIT](LICENSE). The bundled dependencies' notices are in `THIRD_PARTY_LICENSES`.
|