@chainlesschain/personal-data-hub 0.1.0 → 0.2.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.
Files changed (116) hide show
  1. package/__tests__/adapters/ai-chat-history.test.js +395 -0
  2. package/__tests__/adapters/ai-chat-http-client.test.js +242 -0
  3. package/__tests__/adapters/ai-chat-vendors.test.js +733 -0
  4. package/__tests__/adapters/alipay-bill-adapter.test.js +538 -0
  5. package/__tests__/adapters/email-adapter.test.js +138 -1
  6. package/__tests__/adapters/email-classifier.test.js +347 -0
  7. package/__tests__/adapters/email-pdf-extractor.test.js +529 -0
  8. package/__tests__/adapters/email-retry-progress.test.js +294 -0
  9. package/__tests__/adapters/email-templates.test.js +699 -0
  10. package/__tests__/adapters/system-data-adapter.test.js +440 -0
  11. package/__tests__/adapters/system-data-disclosure.test.js +153 -0
  12. package/__tests__/analysis-skills.test.js +409 -0
  13. package/__tests__/entity-resolver-ingest-hook.test.js +177 -0
  14. package/__tests__/entity-resolver-stages.test.js +411 -0
  15. package/__tests__/entity-resolver-vault.test.js +246 -0
  16. package/__tests__/entity-resolver.test.js +526 -0
  17. package/__tests__/fixtures/entity-resolver-200-mock.json +96 -0
  18. package/__tests__/longtail-adapters.test.js +217 -0
  19. package/__tests__/mobile-extractor.test.js +288 -0
  20. package/__tests__/shopping-adapters.test.js +296 -0
  21. package/__tests__/sidecar-contacts-cross-validate.test.js +163 -0
  22. package/__tests__/sidecar-supervisor.test.js +120 -0
  23. package/__tests__/social-adapters.test.js +206 -0
  24. package/__tests__/travel-adapters.test.js +325 -0
  25. package/__tests__/vault.test.js +3 -3
  26. package/__tests__/wechat-adapter.test.js +476 -0
  27. package/__tests__/whatsapp-adapter.test.js +135 -0
  28. package/lib/adapter-spec.js +12 -0
  29. package/lib/adapters/_python-sidecar-base.js +207 -0
  30. package/lib/adapters/ai-chat-history/ai-chat-adapter.js +335 -0
  31. package/lib/adapters/ai-chat-history/cookie-auth.js +109 -0
  32. package/lib/adapters/ai-chat-history/http-client.js +211 -0
  33. package/lib/adapters/ai-chat-history/index.js +28 -0
  34. package/lib/adapters/ai-chat-history/schema-map.js +221 -0
  35. package/lib/adapters/ai-chat-history/vendor-spec.js +85 -0
  36. package/lib/adapters/ai-chat-history/vendors/coze.js +179 -0
  37. package/lib/adapters/ai-chat-history/vendors/deepseek.js +199 -0
  38. package/lib/adapters/ai-chat-history/vendors/dreamina.js +174 -0
  39. package/lib/adapters/ai-chat-history/vendors/hunyuan.js +176 -0
  40. package/lib/adapters/ai-chat-history/vendors/kimi.js +182 -0
  41. package/lib/adapters/ai-chat-history/vendors/qianfan.js +160 -0
  42. package/lib/adapters/ai-chat-history/vendors/tongyi.js +193 -0
  43. package/lib/adapters/ai-chat-history/vendors/zhipu.js +202 -0
  44. package/lib/adapters/alipay-bill/alipay-bill-adapter.js +307 -0
  45. package/lib/adapters/alipay-bill/counterparty.js +129 -0
  46. package/lib/adapters/alipay-bill/csv-parser.js +217 -0
  47. package/lib/adapters/alipay-bill/index.js +41 -0
  48. package/lib/adapters/alipay-bill/zip-decryptor.js +111 -0
  49. package/lib/adapters/email-imap/classifier.js +495 -0
  50. package/lib/adapters/email-imap/email-adapter.js +419 -8
  51. package/lib/adapters/email-imap/index.js +42 -0
  52. package/lib/adapters/email-imap/pdf-extractor.js +192 -0
  53. package/lib/adapters/email-imap/templates/bill.js +232 -0
  54. package/lib/adapters/email-imap/templates/government.js +120 -0
  55. package/lib/adapters/email-imap/templates/index.js +78 -0
  56. package/lib/adapters/email-imap/templates/order.js +186 -0
  57. package/lib/adapters/email-imap/templates/other.js +114 -0
  58. package/lib/adapters/email-imap/templates/register.js +113 -0
  59. package/lib/adapters/email-imap/templates/travel.js +157 -0
  60. package/lib/adapters/email-imap/templates/utils.js +275 -0
  61. package/lib/adapters/email-imap/transactions.js +234 -0
  62. package/lib/adapters/messaging-qq/index.js +158 -0
  63. package/lib/adapters/messaging-telegram/index.js +142 -0
  64. package/lib/adapters/messaging-whatsapp/index.js +189 -0
  65. package/lib/adapters/shopping-base/index.js +208 -0
  66. package/lib/adapters/shopping-jd/index.js +150 -0
  67. package/lib/adapters/shopping-meituan/index.js +154 -0
  68. package/lib/adapters/shopping-taobao/index.js +176 -0
  69. package/lib/adapters/social-bilibili/index.js +171 -0
  70. package/lib/adapters/social-douyin/index.js +116 -0
  71. package/lib/adapters/social-weibo/index.js +164 -0
  72. package/lib/adapters/social-xiaohongshu/index.js +96 -0
  73. package/lib/adapters/system-data/disclosure.js +166 -0
  74. package/lib/adapters/system-data/index.js +34 -0
  75. package/lib/adapters/system-data/system-data-adapter.js +344 -0
  76. package/lib/adapters/travel-12306/index.js +151 -0
  77. package/lib/adapters/travel-amap/index.js +164 -0
  78. package/lib/adapters/travel-baidu-map/index.js +162 -0
  79. package/lib/adapters/travel-base/index.js +240 -0
  80. package/lib/adapters/travel-ctrip/index.js +151 -0
  81. package/lib/adapters/wechat/content-parser.js +326 -0
  82. package/lib/adapters/wechat/db-reader.js +209 -0
  83. package/lib/adapters/wechat/index.js +28 -0
  84. package/lib/adapters/wechat/key-extractor.js +158 -0
  85. package/lib/adapters/wechat/normalize.js +220 -0
  86. package/lib/adapters/wechat/wechat-adapter.js +205 -0
  87. package/lib/analysis-skills/base.js +113 -0
  88. package/lib/analysis-skills/footprint.js +167 -0
  89. package/lib/analysis-skills/index.js +58 -0
  90. package/lib/analysis-skills/interests.js +161 -0
  91. package/lib/analysis-skills/relations.js +226 -0
  92. package/lib/analysis-skills/spending.js +216 -0
  93. package/lib/analysis-skills/timeline.js +167 -0
  94. package/lib/entity-resolver/embedding-stage.js +198 -0
  95. package/lib/entity-resolver/entity-resolver.js +384 -0
  96. package/lib/entity-resolver/index.js +42 -0
  97. package/lib/entity-resolver/llm-stage.js +191 -0
  98. package/lib/entity-resolver/rule-stage.js +208 -0
  99. package/lib/entity-resolver/worker.js +149 -0
  100. package/lib/index.js +115 -0
  101. package/lib/migrations.js +73 -0
  102. package/lib/mobile-extractor/android.js +193 -0
  103. package/lib/mobile-extractor/index.js +9 -0
  104. package/lib/mobile-extractor/ios.js +223 -0
  105. package/lib/registry.js +42 -0
  106. package/lib/sidecar/index.js +15 -0
  107. package/lib/sidecar/supervisor.js +359 -0
  108. package/lib/vault.js +266 -0
  109. package/package.json +29 -3
  110. package/scripts/_make-fixture-all.js +126 -0
  111. package/scripts/_make-fixture-contacts.js +84 -0
  112. package/scripts/evaluate-entity-resolver.js +213 -0
  113. package/scripts/smoke-phase-5-5.js +196 -0
  114. package/scripts/smoke-phase-5-7.js +181 -0
  115. package/scripts/smoke-system-data-contacts.js +309 -0
  116. package/scripts/smoke-system-data.js +312 -0
@@ -0,0 +1,326 @@
1
+ /**
2
+ * Phase 12 v0.5 — WeChat message.content parser.
3
+ *
4
+ * Frida-INDEPENDENT — operates on decrypted message rows AFTER db-reader
5
+ * has done its job. Pure string/XML parsing.
6
+ *
7
+ * Handles the 6 common message types per `Adapter_WeChat_SQLCipher.md` §4.4:
8
+ * type=1 text
9
+ * type=3 image (XML w/ cdnUrl/md5/imgPath)
10
+ * type=34 voice .amr (XML w/ voiceLength/fileName)
11
+ * type=43 video (XML w/ cdnUrl)
12
+ * type=47 GIF/emoji (XML w/ md5/filename)
13
+ * type=49 composite — nested <appmsg type="N">, sub-types:
14
+ * 2 image, 3 music, 4 video, 5 link, 6 file, 8 GIF,
15
+ * 17 location, 19 forwarded, 21 redpacket, 33/36 mini-program,
16
+ * 51 channel video
17
+ * type=10000 system message
18
+ *
19
+ * Output is always `{ kind, text, structured }`:
20
+ * - kind: short string ("text" / "image" / "voice" / "link" / etc.)
21
+ * - text: human-readable summary (for vault content.text)
22
+ * - structured: parsed fields (for vault content.extra)
23
+ *
24
+ * Group-message prefix `<wxid_xxx>:\n` is stripped + returned in
25
+ * `structured.senderWxid` so the message text stays clean.
26
+ */
27
+
28
+ "use strict";
29
+
30
+ const TYPE_NAMES = {
31
+ 1: "text",
32
+ 3: "image",
33
+ 34: "voice",
34
+ 42: "card",
35
+ 43: "video",
36
+ 47: "emoji",
37
+ 48: "location",
38
+ 49: "appmsg",
39
+ 50: "voipcall",
40
+ 10000: "system",
41
+ };
42
+
43
+ const APPMSG_SUBTYPES = {
44
+ 1: "text-link",
45
+ 2: "image-share",
46
+ 3: "music",
47
+ 4: "video",
48
+ 5: "link",
49
+ 6: "file",
50
+ 8: "gif",
51
+ 17: "location-share",
52
+ 19: "forwarded",
53
+ 21: "redpacket",
54
+ 33: "miniprogram",
55
+ 36: "miniprogram",
56
+ 51: "channel-video",
57
+ };
58
+
59
+ /**
60
+ * Top-level: parse a WeChat message row's content + type.
61
+ *
62
+ * @param {object} row { content, type, isSend, talker, ... }
63
+ * @returns {{ kind, text, structured }}
64
+ */
65
+ function parseContent(row) {
66
+ if (!row || typeof row !== "object") {
67
+ return { kind: "unknown", text: "", structured: {} };
68
+ }
69
+ const type = Number(row.type);
70
+ const isGroup = isGroupTalker(row.talker);
71
+ const rawContent = typeof row.content === "string" ? row.content : "";
72
+
73
+ // Strip group sender prefix
74
+ let groupSenderWxid = null;
75
+ let body = rawContent;
76
+ if (isGroup) {
77
+ const m = /^([a-zA-Z0-9_-]+):\n/.exec(rawContent);
78
+ if (m) {
79
+ groupSenderWxid = m[1];
80
+ body = rawContent.slice(m[0].length);
81
+ }
82
+ }
83
+
84
+ let result;
85
+ switch (type) {
86
+ case 1:
87
+ result = parseText(body);
88
+ break;
89
+ case 3:
90
+ result = parseImage(body);
91
+ break;
92
+ case 34:
93
+ result = parseVoice(body);
94
+ break;
95
+ case 42:
96
+ result = parseCard(body);
97
+ break;
98
+ case 43:
99
+ result = parseVideo(body);
100
+ break;
101
+ case 47:
102
+ result = parseEmoji(body);
103
+ break;
104
+ case 48:
105
+ result = parseLocation(body);
106
+ break;
107
+ case 49:
108
+ result = parseAppMsg(body);
109
+ break;
110
+ case 50:
111
+ result = parseVoipCall(body);
112
+ break;
113
+ case 10000:
114
+ result = parseSystem(body);
115
+ break;
116
+ default:
117
+ result = {
118
+ kind: TYPE_NAMES[type] || `type-${type}`,
119
+ text: body.slice(0, 200),
120
+ structured: { type, body: body.slice(0, 1000) },
121
+ };
122
+ }
123
+
124
+ if (groupSenderWxid) {
125
+ result.structured = { ...result.structured, senderWxid: groupSenderWxid };
126
+ }
127
+ return result;
128
+ }
129
+
130
+ // ─── per-type parsers ────────────────────────────────────────────────────
131
+
132
+ function parseText(body) {
133
+ return { kind: "text", text: body, structured: {} };
134
+ }
135
+
136
+ function parseImage(body) {
137
+ const meta = parseXmlAttrs(body, "img");
138
+ return {
139
+ kind: "image",
140
+ text: "[图片]",
141
+ structured: {
142
+ cdnUrl: meta.cdnbigimgurl || meta.cdnmidimgurl || null,
143
+ md5: meta.md5 || null,
144
+ length: meta.length ? parseInt(meta.length, 10) : null,
145
+ },
146
+ };
147
+ }
148
+
149
+ function parseVoice(body) {
150
+ const meta = parseXmlAttrs(body, "voicemsg");
151
+ return {
152
+ kind: "voice",
153
+ text: "[语音]",
154
+ structured: {
155
+ fileName: meta.clientmsgid || null,
156
+ voiceLength: meta.voicelength ? parseInt(meta.voicelength, 10) : null,
157
+ fileType: meta.fromusername || null,
158
+ },
159
+ };
160
+ }
161
+
162
+ function parseCard(body) {
163
+ const meta = parseXmlAttrs(body, "msg");
164
+ return {
165
+ kind: "card",
166
+ text: `[名片] ${meta.nickname || meta.username || ""}`,
167
+ structured: {
168
+ nickname: meta.nickname || null,
169
+ username: meta.username || null,
170
+ province: meta.province || null,
171
+ city: meta.city || null,
172
+ },
173
+ };
174
+ }
175
+
176
+ function parseVideo(body) {
177
+ const meta = parseXmlAttrs(body, "videomsg");
178
+ return {
179
+ kind: "video",
180
+ text: "[视频]",
181
+ structured: {
182
+ cdnUrl: meta.cdnvideourl || null,
183
+ length: meta.length ? parseInt(meta.length, 10) : null,
184
+ playLength: meta.playlength ? parseInt(meta.playlength, 10) : null,
185
+ },
186
+ };
187
+ }
188
+
189
+ function parseEmoji(body) {
190
+ const meta = parseXmlAttrs(body, "emoji");
191
+ return {
192
+ kind: "emoji",
193
+ text: "[表情]",
194
+ structured: { md5: meta.md5 || null, type: meta.type || null },
195
+ };
196
+ }
197
+
198
+ function parseLocation(body) {
199
+ const meta = parseXmlAttrs(body, "location");
200
+ return {
201
+ kind: "location",
202
+ text: `[位置] ${meta.label || meta.poiname || ""}`,
203
+ structured: {
204
+ x: meta.x ? parseFloat(meta.x) : null,
205
+ y: meta.y ? parseFloat(meta.y) : null,
206
+ label: meta.label || null,
207
+ poiName: meta.poiname || null,
208
+ },
209
+ };
210
+ }
211
+
212
+ function parseAppMsg(body) {
213
+ // Type 49: <msg><appmsg type="N"><...subtype-specific...></appmsg></msg>
214
+ const appType = extractAppMsgType(body);
215
+ const subtype = APPMSG_SUBTYPES[appType] || `appmsg-${appType}`;
216
+ const title = extractTag(body, "title");
217
+ const desc = extractTag(body, "des");
218
+ const url = extractTag(body, "url");
219
+
220
+ const structured = {
221
+ appType,
222
+ subtype,
223
+ title: title || null,
224
+ desc: desc || null,
225
+ url: url || null,
226
+ };
227
+
228
+ // Redpacket-specific
229
+ if (appType === 21) {
230
+ structured.redPacketTitle = title;
231
+ }
232
+ // File-specific
233
+ if (appType === 6) {
234
+ structured.fileName = title;
235
+ structured.fileSize = extractTag(body, "totallen");
236
+ }
237
+ // Mini program
238
+ if (appType === 33 || appType === 36) {
239
+ structured.miniProgramName = extractTag(body, "sourcedisplayname")
240
+ || extractTag(body, "weappiconurl") || title;
241
+ }
242
+
243
+ const text = title ? `[${subtype}] ${title}` : `[${subtype}]`;
244
+ return { kind: subtype, text, structured };
245
+ }
246
+
247
+ function parseVoipCall(body) {
248
+ return {
249
+ kind: "voipcall",
250
+ text: "[通话]",
251
+ structured: { raw: body.slice(0, 500) },
252
+ };
253
+ }
254
+
255
+ function parseSystem(body) {
256
+ return {
257
+ kind: "system",
258
+ text: body.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 300),
259
+ structured: {},
260
+ };
261
+ }
262
+
263
+ // ─── helpers ────────────────────────────────────────────────────────────
264
+
265
+ /**
266
+ * Parse XML attributes of a named tag into a flat key-value map.
267
+ * E.g. <img attr1="v1" attr2="v2" /> → { attr1: "v1", attr2: "v2" }.
268
+ * Returns {} when the tag isn't found.
269
+ */
270
+ function parseXmlAttrs(xml, tagName) {
271
+ if (typeof xml !== "string" || xml.length === 0) return {};
272
+ const re = new RegExp(`<${tagName}\\b([^>]*)`, "i");
273
+ const m = re.exec(xml);
274
+ if (!m) return {};
275
+ const attrsText = m[1];
276
+ const out = {};
277
+ const attrRe = /(\w+)\s*=\s*"([^"]*)"/g;
278
+ let am;
279
+ while ((am = attrRe.exec(attrsText)) !== null) {
280
+ out[am[1].toLowerCase()] = am[2];
281
+ }
282
+ return out;
283
+ }
284
+
285
+ /**
286
+ * Pull the text content of a tag: <title>X</title> → "X".
287
+ */
288
+ function extractTag(xml, tagName) {
289
+ if (typeof xml !== "string") return null;
290
+ const re = new RegExp(`<${tagName}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${tagName}>`, "i");
291
+ const m = re.exec(xml);
292
+ if (!m) return null;
293
+ return decodeXmlEntities(m[1].trim());
294
+ }
295
+
296
+ function extractAppMsgType(xml) {
297
+ if (typeof xml !== "string") return -1;
298
+ const re = /<appmsg\s+[^>]*type\s*=\s*"(\d+)"|<type>(\d+)<\/type>/i;
299
+ const m = re.exec(xml);
300
+ if (!m) return -1;
301
+ return parseInt(m[1] || m[2], 10);
302
+ }
303
+
304
+ function decodeXmlEntities(s) {
305
+ return String(s)
306
+ .replace(/&amp;/g, "&")
307
+ .replace(/&lt;/g, "<")
308
+ .replace(/&gt;/g, ">")
309
+ .replace(/&quot;/g, '"')
310
+ .replace(/&#(\d+);/g, (_, n) => String.fromCharCode(parseInt(n, 10)));
311
+ }
312
+
313
+ function isGroupTalker(talker) {
314
+ // Group chat talker IDs end with @chatroom
315
+ return typeof talker === "string" && talker.endsWith("@chatroom");
316
+ }
317
+
318
+ module.exports = {
319
+ parseContent,
320
+ parseXmlAttrs,
321
+ extractTag,
322
+ extractAppMsgType,
323
+ isGroupTalker,
324
+ TYPE_NAMES,
325
+ APPMSG_SUBTYPES,
326
+ };
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Phase 12 v0.5 — WeChat SQLCipher DB reader (frida-INDEPENDENT).
3
+ *
4
+ * Opens the decrypted-via-MD5-key EnMicroMsg.db using
5
+ * better-sqlite3-multiple-ciphers (the same package LocalVault uses).
6
+ *
7
+ * Note about the key flow:
8
+ * - For WeChat < 8.0: key comes from KeyExtractor (MD5(IMEI+UIN)[:7]).
9
+ * - For WeChat 8.0+ : key comes from Frida hook (Phase 12.6). DI seam
10
+ * `keyProvider.getKey()` lets both paths plug in cleanly.
11
+ *
12
+ * SQLCipher PRAGMA settings per design doc + sjqz reference:
13
+ * PRAGMA cipher_default_kdf_iter = 4000;
14
+ * PRAGMA cipher_default_use_hmac = OFF;
15
+ * PRAGMA cipher_compatibility = 1; // WCDB legacy SQLCipher v1 layout
16
+ *
17
+ * Different WeChat builds use different SQLCipher params — we try a few
18
+ * standard ones in order via tryOpen().
19
+ */
20
+
21
+ "use strict";
22
+
23
+ const fs = require("node:fs");
24
+ const path = require("node:path");
25
+
26
+ const KNOWN_PRAGMA_PROFILES = [
27
+ // WCDB legacy (most common, WeChat < 8.0)
28
+ {
29
+ name: "wcdb-legacy",
30
+ pragmas: [
31
+ "PRAGMA cipher_compatibility = 1",
32
+ "PRAGMA cipher_default_kdf_iter = 4000",
33
+ "PRAGMA cipher_default_use_hmac = OFF",
34
+ "PRAGMA cipher_default_page_size = 1024",
35
+ ],
36
+ },
37
+ // SQLCipher v3 default — fallback for some PC WeChat builds
38
+ {
39
+ name: "sqlcipher-v3",
40
+ pragmas: [
41
+ "PRAGMA cipher_compatibility = 3",
42
+ "PRAGMA cipher_default_kdf_iter = 64000",
43
+ ],
44
+ },
45
+ // SQLCipher v4 default
46
+ {
47
+ name: "sqlcipher-v4",
48
+ pragmas: [
49
+ "PRAGMA cipher_compatibility = 4",
50
+ ],
51
+ },
52
+ ];
53
+
54
+ class WeChatDBReader {
55
+ constructor(opts = {}) {
56
+ if (!opts || typeof opts !== "object") {
57
+ throw new Error("WeChatDBReader: opts required");
58
+ }
59
+ if (!opts.dbPath || typeof opts.dbPath !== "string") {
60
+ throw new Error("WeChatDBReader: opts.dbPath required");
61
+ }
62
+ if (!opts.keyProvider || typeof opts.keyProvider.getKey !== "function") {
63
+ throw new Error("WeChatDBReader: opts.keyProvider with getKey() required");
64
+ }
65
+ this._dbPath = opts.dbPath;
66
+ this._keyProvider = opts.keyProvider;
67
+ this._driver = opts.driver || null; // DI seam for tests
68
+ this._db = null;
69
+ this._profile = null;
70
+ }
71
+
72
+ async open() {
73
+ if (!fs.existsSync(this._dbPath)) {
74
+ throw new Error(`WeChatDBReader: DB not found: ${this._dbPath}`);
75
+ }
76
+ const key = await this._keyProvider.getKey();
77
+ if (!key) {
78
+ throw new Error("WeChatDBReader: keyProvider returned empty key");
79
+ }
80
+ const Database = this._driver || loadDriver();
81
+ let lastError = null;
82
+ for (const profile of KNOWN_PRAGMA_PROFILES) {
83
+ try {
84
+ const db = new Database(this._dbPath, { readonly: true });
85
+ db.pragma(`key = '${key}'`);
86
+ for (const p of profile.pragmas) db.exec(p);
87
+ // Probe — does sqlite_master open?
88
+ const row = db.prepare("SELECT count(*) AS n FROM sqlite_master").get();
89
+ if (row && Number.isFinite(row.n)) {
90
+ this._db = db;
91
+ this._profile = profile.name;
92
+ return { profile: profile.name, tables: row.n };
93
+ }
94
+ db.close();
95
+ } catch (err) {
96
+ lastError = err;
97
+ }
98
+ }
99
+ throw new Error(
100
+ `WeChatDBReader: failed to open with any pragma profile. Last error: ${lastError && lastError.message}`,
101
+ );
102
+ }
103
+
104
+ /**
105
+ * Quick check: does this look like an EnMicroMsg.db (the main WeChat
106
+ * message DB)? Verifies the `message` + `rcontact` tables exist.
107
+ */
108
+ isEnMicroMsg() {
109
+ if (!this._db) return false;
110
+ try {
111
+ const tables = this._db
112
+ .prepare("SELECT name FROM sqlite_master WHERE type='table'")
113
+ .all()
114
+ .map((r) => r.name);
115
+ return tables.includes("message") && tables.includes("rcontact");
116
+ } catch (_e) {
117
+ return false;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * List tables present.
123
+ */
124
+ listTables() {
125
+ if (!this._db) return [];
126
+ return this._db
127
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
128
+ .all()
129
+ .map((r) => r.name);
130
+ }
131
+
132
+ /**
133
+ * Fetch up to `limit` messages since `sinceMsgSvrId` (per design doc
134
+ * §6 OQ-6 watermark = per-talker last msgSvrId). For initial v0 we
135
+ * accept a global watermark and let the adapter post-filter per
136
+ * talker.
137
+ */
138
+ fetchMessages({ sinceMsgSvrId = 0, limit = 1000, talker = null } = {}) {
139
+ if (!this._db) throw new Error("WeChatDBReader: call open() first");
140
+ let sql = "SELECT msgId, msgSvrId, talker, content, type, createTime, isSend, status FROM message";
141
+ const params = [];
142
+ const where = [];
143
+ if (sinceMsgSvrId) {
144
+ where.push("msgSvrId > ?");
145
+ params.push(sinceMsgSvrId);
146
+ }
147
+ if (talker) {
148
+ where.push("talker = ?");
149
+ params.push(talker);
150
+ }
151
+ if (where.length > 0) sql += " WHERE " + where.join(" AND ");
152
+ sql += " ORDER BY msgSvrId ASC LIMIT ?";
153
+ params.push(limit);
154
+ return this._db.prepare(sql).all(...params);
155
+ }
156
+
157
+ /**
158
+ * Fetch contacts. WeChat rcontact has many columns; we pull the ones
159
+ * relevant for normalization.
160
+ */
161
+ fetchContacts({ limit = 5000 } = {}) {
162
+ if (!this._db) throw new Error("WeChatDBReader: call open() first");
163
+ return this._db
164
+ .prepare(
165
+ "SELECT username, alias, nickname, conRemark, type FROM rcontact LIMIT ?",
166
+ )
167
+ .all(limit);
168
+ }
169
+
170
+ /**
171
+ * Fetch chatroom info.
172
+ */
173
+ fetchChatrooms({ limit = 1000 } = {}) {
174
+ if (!this._db) throw new Error("WeChatDBReader: call open() first");
175
+ return this._db
176
+ .prepare(
177
+ "SELECT chatroomname, memberlist, displayname, roomowner FROM chatroom LIMIT ?",
178
+ )
179
+ .all(limit);
180
+ }
181
+
182
+ close() {
183
+ if (this._db) {
184
+ try { this._db.close(); } catch (_e) {}
185
+ this._db = null;
186
+ }
187
+ }
188
+
189
+ /** Active pragma profile name (set after successful open). */
190
+ profile() { return this._profile; }
191
+ }
192
+
193
+ let _driverCache = null;
194
+ function loadDriver() {
195
+ if (_driverCache) return _driverCache;
196
+ try {
197
+ _driverCache = require("better-sqlite3-multiple-ciphers");
198
+ } catch (err) {
199
+ throw new Error(
200
+ `WeChatDBReader: better-sqlite3-multiple-ciphers required: ${err && err.message ? err.message : err}`,
201
+ );
202
+ }
203
+ return _driverCache;
204
+ }
205
+
206
+ module.exports = {
207
+ WeChatDBReader,
208
+ KNOWN_PRAGMA_PROFILES,
209
+ };
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+
3
+ const { WechatAdapter, NAME, VERSION } = require("./wechat-adapter");
4
+ const { parseContent, parseXmlAttrs, extractTag, isGroupTalker, TYPE_NAMES, APPMSG_SUBTYPES } = require("./content-parser");
5
+ const { extractWeChatKey, deriveLegacyKey, extractUinFromPrefs, extractImeiFromCompatibleInfo } = require("./key-extractor");
6
+ const { WeChatDBReader, KNOWN_PRAGMA_PROFILES } = require("./db-reader");
7
+ const { normalizeMessage, normalizeContact, wxidToPersonId } = require("./normalize");
8
+
9
+ module.exports = {
10
+ WechatAdapter,
11
+ WECHAT_NAME: NAME,
12
+ WECHAT_VERSION: VERSION,
13
+ parseWeChatContent: parseContent,
14
+ parseWeChatXmlAttrs: parseXmlAttrs,
15
+ extractWeChatTag: extractTag,
16
+ isWeChatGroupTalker: isGroupTalker,
17
+ WECHAT_TYPE_NAMES: TYPE_NAMES,
18
+ WECHAT_APPMSG_SUBTYPES: APPMSG_SUBTYPES,
19
+ extractWeChatKey,
20
+ deriveWeChatLegacyKey: deriveLegacyKey,
21
+ extractWeChatUinFromPrefs: extractUinFromPrefs,
22
+ extractWeChatImeiFromCompatibleInfo: extractImeiFromCompatibleInfo,
23
+ WeChatDBReader,
24
+ WECHAT_PRAGMA_PROFILES: KNOWN_PRAGMA_PROFILES,
25
+ normalizeWeChatMessage: normalizeMessage,
26
+ normalizeWeChatContact: normalizeContact,
27
+ wxidToWeChatPersonId: wxidToPersonId,
28
+ };