@chronicle.app/imessage 0.2.0 → 0.4.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/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @chronicle.app/imessage
|
|
2
2
|
|
|
3
|
-
Read `chat.db` with `ImessageExtractor`, then emit MessageAction/Message records with `ImessageTransformer`. `input` defaults to the macOS Messages database; use a consistent backup for exported data. The connection is read-only via built-in `node:sqlite`. Date windows are exclusive, records newest-first, and `limit: 0` means unlimited.
|
|
3
|
+
Read `chat.db` with `ImessageExtractor`, then emit MessageAction/Message records with `ImessageTransformer`. `input` defaults to the macOS Messages database; use a consistent backup for exported data. Reading it needs Full Disk Access for the app that runs Chronicle (Terminal, iTerm, your editor): System Settings → Privacy & Security → Full Disk Access. Without it the run stops with that hint, not SQLite's `unable to open database file`. The connection is read-only via built-in `node:sqlite`. Date windows are exclusive, records newest-first, and `limit: 0` means unlimited.
|
|
4
4
|
|
|
5
|
-
Automatic iCloud account and AddressBook enrichment comes from `@chronicle.app/icloud`. `includeContactNames: false` skips contacts. An explicit `account` object (or null) lets callers describe exported data without host account lookup. Transformer config can inject `lookupContact` for exported contacts. Missing permissions/account/contact data fall back to source-local identity and unlabelled participants.
|
|
5
|
+
Automatic iCloud account and AddressBook enrichment comes from `@chronicle.app/icloud`. `includeContactNames: false` skips contacts. An explicit `account` object (or null) lets callers describe exported data without host account lookup. Transformer config can inject `lookupContact` for exported contacts. Missing permissions/account/contact data fall back to source-local identity and unlabelled participants. When no identifier for the account owner is available, the owner is left out of the agent and recipients.
|
|
6
6
|
|
|
7
7
|
Attachments are local file references with MIME metadata, not copied bytes. Missing files are skipped. Attributed-body decoding retains the existing heuristic; it is not a complete typedstream decoder. Raw records preserve the source blob, and large integer timestamps are decimal strings so serialization is safe.
|
|
8
8
|
|
|
@@ -26,6 +26,7 @@ export default class ImessageTransformer extends ChronicleTransformer {
|
|
|
26
26
|
* more than once keeps its first spelling.
|
|
27
27
|
*/
|
|
28
28
|
private emailIdentities;
|
|
29
|
+
/** The account owner, or null when no identifier for them is available. */
|
|
29
30
|
private buildMyIdentity;
|
|
30
31
|
private buildMyPhoneIdentity;
|
|
31
32
|
private determineAgentAndRecipients;
|
|
@@ -23,7 +23,7 @@ export default class ImessageTransformer extends ChronicleTransformer {
|
|
|
23
23
|
sourceId: record.context.guid, // Use GUID instead of ROWID
|
|
24
24
|
'@key': ['source', 'sourceId', '@type'],
|
|
25
25
|
object: this.buildMessage(record, recipients),
|
|
26
|
-
agent,
|
|
26
|
+
...(agent && { agent }),
|
|
27
27
|
};
|
|
28
28
|
return [obj];
|
|
29
29
|
}
|
|
@@ -102,15 +102,18 @@ export default class ImessageTransformer extends ChronicleTransformer {
|
|
|
102
102
|
'@key': ['@type', 'source', 'handle'],
|
|
103
103
|
}));
|
|
104
104
|
}
|
|
105
|
+
/** The account owner, or null when no identifier for them is available. */
|
|
105
106
|
async buildMyIdentity(record) {
|
|
106
107
|
const { service } = record.context;
|
|
107
108
|
const agentSource = this.getAgentSource(service);
|
|
108
109
|
const identity = agentSource === 'icloud'
|
|
109
|
-
?
|
|
110
|
+
? await buildICloudPersonSchema(record.context.myIcloudAccount)
|
|
110
111
|
: this.buildMyPhoneIdentity(record);
|
|
112
|
+
if (!identity)
|
|
113
|
+
return null;
|
|
111
114
|
// Tag the account owner as self. The iCloud branch (buildICloudPersonSchema)
|
|
112
115
|
// already self-tags, so add `@me` only when it isn't already present.
|
|
113
|
-
const sameAs = identity
|
|
116
|
+
const sameAs = identity.sameAs ?? [];
|
|
114
117
|
return { ...identity, sameAs: sameAs.includes('@me') ? sameAs : [...sameAs, '@me'] };
|
|
115
118
|
}
|
|
116
119
|
buildMyPhoneIdentity(record) {
|
|
@@ -141,22 +144,18 @@ export default class ImessageTransformer extends ChronicleTransformer {
|
|
|
141
144
|
'@key': ['@type', 'source', 'handle'],
|
|
142
145
|
};
|
|
143
146
|
}
|
|
144
|
-
//
|
|
145
|
-
return
|
|
146
|
-
'@type': 'Person',
|
|
147
|
-
source: 'phone',
|
|
148
|
-
'@key': ['@type', 'source'],
|
|
149
|
-
};
|
|
147
|
+
// No identifier for me: a bare phone Person would merge every such owner.
|
|
148
|
+
return null;
|
|
150
149
|
}
|
|
151
150
|
determineAgentAndRecipients(record, participants, myIdentity) {
|
|
152
151
|
const isFromMe = record.context.isFromMe === 1;
|
|
153
152
|
if (isFromMe) {
|
|
154
153
|
// I sent the message - I'm the agent, everyone else are recipients
|
|
155
|
-
const recipients = myIdentity
|
|
154
|
+
const recipients = myIdentity?.handle
|
|
156
155
|
? participants.filter(p => p.handle !== myIdentity.handle)
|
|
157
156
|
: participants; // If I have no handle (SMS fallback), all participants are recipients
|
|
158
157
|
return {
|
|
159
|
-
agent: myIdentity,
|
|
158
|
+
agent: myIdentity ?? undefined,
|
|
160
159
|
recipients,
|
|
161
160
|
};
|
|
162
161
|
}
|
|
@@ -169,12 +168,15 @@ export default class ImessageTransformer extends ChronicleTransformer {
|
|
|
169
168
|
const agentSource = this.getAgentSource(record.context.service);
|
|
170
169
|
return {
|
|
171
170
|
agent: this.buildParticipantAgent(record, agentSource, handleIdText),
|
|
172
|
-
recipients: [myIdentity],
|
|
171
|
+
recipients: myIdentity ? [myIdentity] : [],
|
|
173
172
|
};
|
|
174
173
|
}
|
|
175
174
|
return {
|
|
176
175
|
agent,
|
|
177
|
-
recipients: [
|
|
176
|
+
recipients: [
|
|
177
|
+
...participants.filter(p => p.handle !== agent.handle),
|
|
178
|
+
...(myIdentity ? [myIdentity] : []),
|
|
179
|
+
],
|
|
178
180
|
};
|
|
179
181
|
}
|
|
180
182
|
getServiceSource(_service) {
|
|
@@ -202,7 +204,7 @@ export default class ImessageTransformer extends ChronicleTransformer {
|
|
|
202
204
|
const filePath = this.resolveAttachmentPath(attachment.filename);
|
|
203
205
|
// Check if file exists
|
|
204
206
|
if (!fs.existsSync(filePath)) {
|
|
205
|
-
|
|
207
|
+
this.logger.warn('Attachment file not found', { file: filePath });
|
|
206
208
|
continue;
|
|
207
209
|
}
|
|
208
210
|
// MIME drives the media object @type (ImageObject vs DocumentObject…).
|
|
@@ -226,7 +228,10 @@ export default class ImessageTransformer extends ChronicleTransformer {
|
|
|
226
228
|
mediaObjects.push(mediaObject);
|
|
227
229
|
}
|
|
228
230
|
catch (error) {
|
|
229
|
-
|
|
231
|
+
this.logger.warn('Failed to process attachment', {
|
|
232
|
+
file: attachment.filename,
|
|
233
|
+
error: String(error),
|
|
234
|
+
});
|
|
230
235
|
}
|
|
231
236
|
}
|
|
232
237
|
return mediaObjects;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chronicle.app/imessage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"types": "./dist/index.d.ts",
|
|
6
6
|
"files": [
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
"type": "module",
|
|
20
20
|
"description": "Chronicle imessage extraction plugin",
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@chronicle.app/etl": "0.
|
|
23
|
-
"@chronicle.app/schema": "0.
|
|
24
|
-
"@chronicle.app/tsconfig": "0.
|
|
22
|
+
"@chronicle.app/etl": "0.4.0",
|
|
23
|
+
"@chronicle.app/schema": "0.4.0",
|
|
24
|
+
"@chronicle.app/tsconfig": "0.4.0",
|
|
25
25
|
"@types/node": "^22.10.1",
|
|
26
26
|
"typescript": "^5.7.2"
|
|
27
27
|
},
|
|
@@ -36,8 +36,8 @@
|
|
|
36
36
|
"registry": "https://registry.npmjs.org/"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@chronicle.app/etl-sqlite": "0.
|
|
40
|
-
"@chronicle.app/icloud": "0.
|
|
39
|
+
"@chronicle.app/etl-sqlite": "0.4.0",
|
|
40
|
+
"@chronicle.app/icloud": "0.4.0",
|
|
41
41
|
"zod": "^3.24.1"
|
|
42
42
|
},
|
|
43
43
|
"peerDependencies": {
|
|
@@ -54,7 +54,25 @@
|
|
|
54
54
|
}
|
|
55
55
|
},
|
|
56
56
|
"chronicle": {
|
|
57
|
-
"plugin": true
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
"plugin": true,
|
|
58
|
+
"sources": {
|
|
59
|
+
"imessage": {
|
|
60
|
+
"strategies": {
|
|
61
|
+
"app-db": {
|
|
62
|
+
"delivery": "local",
|
|
63
|
+
"recordTypes": [
|
|
64
|
+
"messages"
|
|
65
|
+
],
|
|
66
|
+
"default": true
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"platforms": [
|
|
70
|
+
"darwin"
|
|
71
|
+
],
|
|
72
|
+
"requires": [
|
|
73
|
+
"full-disk-access"
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
60
78
|
}
|