@chainlesschain/personal-data-hub 0.3.1 → 0.3.6
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/__tests__/adapters/email-adapter-snapshot.test.js +237 -0
- package/__tests__/adapters/email-adapter.test.js +1 -1
- package/__tests__/adapters/email-pdf-extractor.test.js +1 -1
- package/__tests__/adapters/email-retry-progress.test.js +1 -1
- package/__tests__/adapters/email-templates.test.js +1 -1
- package/__tests__/adapters/social-bilibili-adb-api-client.test.js +721 -0
- package/__tests__/adapters/social-bilibili-adb-chromium-cookies-reader.test.js +346 -0
- package/__tests__/adapters/social-bilibili-adb-collector.test.js +284 -0
- package/__tests__/adapters/social-bilibili-adb-cookies-extension.test.js +343 -0
- package/__tests__/adapters/social-bilibili-adb-snapshot-builder.test.js +296 -0
- package/__tests__/adapters/social-douyin-adb-collector.test.js +254 -0
- package/__tests__/adapters/social-douyin-adb-im-db-parser.test.js +304 -0
- package/__tests__/adapters/social-douyin-adb-snapshot-builder.test.js +216 -0
- package/__tests__/adapters/social-weibo-adb-api-client.test.js +362 -0
- package/__tests__/adapters/social-weibo-adb-collector.test.js +201 -0
- package/__tests__/adapters/social-weibo-adb-snapshot-builder.test.js +189 -0
- package/__tests__/adapters/social-xiaohongshu-adb-collector.test.js +207 -0
- package/__tests__/adapters/social-xiaohongshu-adb-sign.test.js +130 -0
- package/__tests__/adapters/system-data-android.test.js +32 -1
- package/__tests__/longtail-adapters.test.js +15 -2
- package/__tests__/shopping-adapters.test.js +96 -0
- package/__tests__/sign-providers.test.js +62 -0
- package/__tests__/travel-adapters.test.js +66 -0
- package/__tests__/whatsapp-adapter.test.js +5 -2
- package/lib/adapters/browser-history-chrome/chrome-db-reader.js +11 -1
- package/lib/adapters/email-imap/email-adapter.js +224 -17
- package/lib/adapters/messaging-telegram/index.js +15 -12
- package/lib/adapters/messaging-whatsapp/index.js +15 -12
- package/lib/adapters/shopping-taobao/index.js +161 -21
- package/lib/adapters/social-bilibili-adb/api-client.js +555 -0
- package/lib/adapters/social-bilibili-adb/chromium-cookies-reader.js +296 -0
- package/lib/adapters/social-bilibili-adb/collector.js +190 -0
- package/lib/adapters/social-bilibili-adb/cookies-extension.js +250 -0
- package/lib/adapters/social-bilibili-adb/index.js +51 -0
- package/lib/adapters/social-bilibili-adb/snapshot-builder.js +197 -0
- package/lib/adapters/social-douyin/index.js +4 -0
- package/lib/adapters/social-douyin-adb/collector.js +165 -0
- package/lib/adapters/social-douyin-adb/db-extension.js +281 -0
- package/lib/adapters/social-douyin-adb/im-db-parser.js +287 -0
- package/lib/adapters/social-douyin-adb/index.js +57 -0
- package/lib/adapters/social-douyin-adb/snapshot-builder.js +174 -0
- package/lib/adapters/social-weibo-adb/api-client.js +281 -0
- package/lib/adapters/social-weibo-adb/collector.js +169 -0
- package/lib/adapters/social-weibo-adb/cookies-extension.js +251 -0
- package/lib/adapters/social-weibo-adb/index.js +55 -0
- package/lib/adapters/social-weibo-adb/snapshot-builder.js +145 -0
- package/lib/adapters/social-xiaohongshu-adb/api-client.js +278 -0
- package/lib/adapters/social-xiaohongshu-adb/collector.js +158 -0
- package/lib/adapters/social-xiaohongshu-adb/cookies-extension.js +211 -0
- package/lib/adapters/social-xiaohongshu-adb/index.js +50 -0
- package/lib/adapters/social-xiaohongshu-adb/sign.js +90 -0
- package/lib/adapters/social-xiaohongshu-adb/snapshot-builder.js +126 -0
- package/lib/adapters/system-data-android/adapter.js +77 -3
- package/lib/adapters/travel-amap/index.js +16 -10
- package/lib/adapters/travel-ctrip/index.js +25 -9
- package/lib/adapters/vscode/vscode-reader.js +7 -1
- package/lib/sign-providers/index.js +20 -0
- package/lib/sign-providers/interface.js +82 -0
- package/lib/sign-providers/null-sign-provider.js +30 -0
- package/package.json +6 -1
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Phase 3a (Weibo C 路径 — 2026-05-25): weibo.cookies ADB extension factory.
|
|
5
|
+
*
|
|
6
|
+
* Mirror of `social-bilibili-adb/cookies-extension.js` (P1a). Pipeline:
|
|
7
|
+
*
|
|
8
|
+
* 1. ADB-pull /data/data/com.sina.weibo/app_webview/Default/Cookies
|
|
9
|
+
* via `su -c "base64 ..."` streaming (avoids MIUI FUSE SELinux trap)
|
|
10
|
+
* 2. Parse the chromium-shape sqlite via the shared
|
|
11
|
+
* chromium-cookies-reader (Phase 1a generic module)
|
|
12
|
+
* 3. Filter to host_key match `m.weibo.cn` (Weibo's actual API host;
|
|
13
|
+
* `weibo.com` chromium cookies exist on desktop but not on the
|
|
14
|
+
* mobile App where chromium-cookies lives)
|
|
15
|
+
* 4. Validate at minimum SUB cookie present (the session cookie —
|
|
16
|
+
* without it /api/config returns "not logged in")
|
|
17
|
+
* 5. Assemble Cookie header from all m.weibo.cn cookies (Weibo's API
|
|
18
|
+
* doesn't enforce a strict required-cookie list like Bilibili's
|
|
19
|
+
* 5-cookie requirement; pass everything through and let the server
|
|
20
|
+
* pick what it needs)
|
|
21
|
+
*
|
|
22
|
+
* Returns:
|
|
23
|
+
* {
|
|
24
|
+
* cookie: string, // full Cookie header
|
|
25
|
+
* extractedAt: number,
|
|
26
|
+
* diagnostic: {
|
|
27
|
+
* cookieCount: number,
|
|
28
|
+
* hadEncrypted: boolean,
|
|
29
|
+
* hasSub: boolean,
|
|
30
|
+
* cookieNames: string[],
|
|
31
|
+
* }
|
|
32
|
+
* }
|
|
33
|
+
*
|
|
34
|
+
* Failure modes (all throw, UI maps the typed reason to a banner):
|
|
35
|
+
* - WEIBO_NOT_INSTALLED — package not on device
|
|
36
|
+
* - WEIBO_NO_ROOT — su not available
|
|
37
|
+
* - WEIBO_COOKIES_EMPTY — base64 stream returned 0 bytes
|
|
38
|
+
* - WEIBO_COOKIES_TRUNCATED — decoded file too small
|
|
39
|
+
* - WEIBO_NOT_SQLITE — magic header check failed
|
|
40
|
+
* - WEIBO_COOKIES_INCOMPLETE — SUB cookie missing (user logged out
|
|
41
|
+
* on the Weibo App or app uses a non-standard storage path)
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
const fs = require("node:fs");
|
|
45
|
+
const path = require("node:path");
|
|
46
|
+
const os = require("node:os");
|
|
47
|
+
const crypto = require("node:crypto");
|
|
48
|
+
|
|
49
|
+
const {
|
|
50
|
+
readChromiumCookies,
|
|
51
|
+
} = require("../social-bilibili-adb/chromium-cookies-reader");
|
|
52
|
+
|
|
53
|
+
const WEIBO_COOKIES_REMOTE_PATH =
|
|
54
|
+
"/data/data/com.sina.weibo/app_webview/Default/Cookies";
|
|
55
|
+
|
|
56
|
+
const WEIBO_COOKIE_HOST_DOMAIN = "m.weibo.cn";
|
|
57
|
+
|
|
58
|
+
/** Minimum required cookie name — without SUB, /api/config returns login=false. */
|
|
59
|
+
const WEIBO_REQUIRED_COOKIE = "SUB";
|
|
60
|
+
|
|
61
|
+
async function pullCookiesViaSu(adb, serial, opts) {
|
|
62
|
+
const adbOpts = { serial, timeoutMs: opts?.timeoutMs || 60_000 };
|
|
63
|
+
const lsOut = await adb(
|
|
64
|
+
[
|
|
65
|
+
"shell",
|
|
66
|
+
"su",
|
|
67
|
+
"-c",
|
|
68
|
+
`ls ${WEIBO_COOKIES_REMOTE_PATH} 2>/dev/null || echo NOT_FOUND`,
|
|
69
|
+
],
|
|
70
|
+
adbOpts,
|
|
71
|
+
);
|
|
72
|
+
const lsLine = lsOut.replace(/\r+$/gm, "").trim();
|
|
73
|
+
if (lsLine === "NOT_FOUND" || lsLine === "") {
|
|
74
|
+
throw new Error(
|
|
75
|
+
"WEIBO_NOT_INSTALLED: " +
|
|
76
|
+
WEIBO_COOKIES_REMOTE_PATH +
|
|
77
|
+
" not found. Install Weibo App + log in once on the phone, then retry. (Some Weibo App versions store cookies in a non-default WebView profile dir; if Weibo is installed but the path is missing, file a bug to track the actual path.)",
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
// Probe root.
|
|
81
|
+
const idOut = await adb(["shell", "su", "-c", "id -u"], adbOpts);
|
|
82
|
+
const idLine = idOut.replace(/\r+$/gm, "").trim();
|
|
83
|
+
if (idLine !== "0" && !idLine.includes("uid=0")) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
"WEIBO_NO_ROOT: this phone isn't rooted (su returned `" +
|
|
86
|
+
idLine.substring(0, 60) +
|
|
87
|
+
"`). Weibo release APK isn't debuggable, so root is required to read its Cookies DB.",
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
// Stream base64 (avoids MIUI FUSE label remap trap).
|
|
91
|
+
const b64 = await adb(
|
|
92
|
+
[
|
|
93
|
+
"shell",
|
|
94
|
+
"su",
|
|
95
|
+
"-c",
|
|
96
|
+
`base64 ${WEIBO_COOKIES_REMOTE_PATH} | tr -d '\\n\\r'`,
|
|
97
|
+
],
|
|
98
|
+
{ ...adbOpts, timeoutMs: opts?.timeoutMs || 60_000 },
|
|
99
|
+
);
|
|
100
|
+
const b64Clean = b64.replace(/[\r\n\t ]+/g, "");
|
|
101
|
+
if (b64Clean.length === 0) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
"WEIBO_COOKIES_EMPTY: base64 stream returned 0 bytes (su exec may have silently failed on MIUI / OEM ROM, retry or check `adb logcat`)",
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
let buf;
|
|
107
|
+
try {
|
|
108
|
+
buf = Buffer.from(b64Clean, "base64");
|
|
109
|
+
} catch (e) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
"WEIBO_BASE64_PARSE: stream wasn't valid base64 (" +
|
|
112
|
+
(e.message || String(e)) +
|
|
113
|
+
")",
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
if (buf.length < 1024) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
"WEIBO_COOKIES_TRUNCATED: decoded file is only " +
|
|
119
|
+
buf.length +
|
|
120
|
+
" bytes — expected ≥4KB sqlite. Possible MIUI silent su fail; check `adb logcat`.",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
const magic = buf.subarray(0, 16).toString("latin1");
|
|
124
|
+
if (!magic.startsWith("SQLite format 3")) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
"WEIBO_NOT_SQLITE: decoded file lacks `SQLite format 3` magic header. Got bytes: " +
|
|
127
|
+
buf.subarray(0, 16).toString("hex"),
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
const tmpDir = os.tmpdir();
|
|
131
|
+
const tmpFile = path.join(
|
|
132
|
+
tmpDir,
|
|
133
|
+
`cc-weibo-cookies-${crypto.randomUUID()}.db`,
|
|
134
|
+
);
|
|
135
|
+
fs.writeFileSync(tmpFile, buf);
|
|
136
|
+
return tmpFile;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Build a Cookie header from the chromium-cookies array. Weibo doesn't
|
|
141
|
+
* have a strict required-cookie list like Bilibili's 5 — but SUB must
|
|
142
|
+
* be present (it's the session cookie). Everything else is best-effort
|
|
143
|
+
* passthrough.
|
|
144
|
+
*/
|
|
145
|
+
function assembleWeiboCookieHeader(cookies) {
|
|
146
|
+
if (!Array.isArray(cookies)) {
|
|
147
|
+
throw new TypeError("assembleWeiboCookieHeader: cookies must be an array");
|
|
148
|
+
}
|
|
149
|
+
const byName = new Map();
|
|
150
|
+
for (const c of cookies) {
|
|
151
|
+
// Most-recently-set wins on duplicate names; prefer more-specific host
|
|
152
|
+
if (
|
|
153
|
+
!byName.has(c.name) ||
|
|
154
|
+
c.hostKey.length > (byName.get(c.name).hostKey || "").length
|
|
155
|
+
) {
|
|
156
|
+
byName.set(c.name, c);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const hasSub = byName.has(WEIBO_REQUIRED_COOKIE);
|
|
160
|
+
if (!hasSub) {
|
|
161
|
+
return {
|
|
162
|
+
header: null,
|
|
163
|
+
present: new Set(byName.keys()),
|
|
164
|
+
missing: [WEIBO_REQUIRED_COOKIE],
|
|
165
|
+
hasSub: false,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
// Pass everything through — Weibo's m.weibo.cn API picks what it needs
|
|
169
|
+
// (SUB / SUBP / _T_WM / MLOGIN / WEIBOCN_FROM / etc.)
|
|
170
|
+
const header = Array.from(byName.values())
|
|
171
|
+
.map((c) => `${c.name}=${c.value}`)
|
|
172
|
+
.join("; ");
|
|
173
|
+
return {
|
|
174
|
+
header,
|
|
175
|
+
present: new Set(byName.keys()),
|
|
176
|
+
missing: [],
|
|
177
|
+
hasSub: true,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Factory: returns the extension handler. Same contract as Bilibili
|
|
183
|
+
* Phase 1a — stateless, no closure-captured device serial.
|
|
184
|
+
*/
|
|
185
|
+
function createWeiboCookiesExtension(factoryOpts = {}) {
|
|
186
|
+
const timeoutMs = factoryOpts.timeoutMs || 60_000;
|
|
187
|
+
const onCleanupFailed = factoryOpts.onCleanupFailed || (() => {});
|
|
188
|
+
|
|
189
|
+
return async function weiboCookiesHandler(_params, ctx) {
|
|
190
|
+
if (
|
|
191
|
+
!ctx ||
|
|
192
|
+
typeof ctx.adb !== "function" ||
|
|
193
|
+
typeof ctx.pickDevice !== "function"
|
|
194
|
+
) {
|
|
195
|
+
throw new TypeError(
|
|
196
|
+
"weibo.cookies extension: ctx must provide {adb, pickDevice} (got " +
|
|
197
|
+
typeof ctx +
|
|
198
|
+
")",
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
const serial = await ctx.pickDevice();
|
|
202
|
+
let tmpFile = null;
|
|
203
|
+
try {
|
|
204
|
+
tmpFile = await pullCookiesViaSu(ctx.adb, serial, { timeoutMs });
|
|
205
|
+
const cookies = readChromiumCookies(tmpFile, WEIBO_COOKIE_HOST_DOMAIN);
|
|
206
|
+
const cookieCount = cookies.length;
|
|
207
|
+
const hadEncrypted = (cookies._skippedEncryptedCount || 0) > 0;
|
|
208
|
+
const { header, missing, present, hasSub } =
|
|
209
|
+
assembleWeiboCookieHeader(cookies);
|
|
210
|
+
if (header === null) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
"WEIBO_COOKIES_INCOMPLETE: missing required cookie " +
|
|
213
|
+
JSON.stringify(missing) +
|
|
214
|
+
". User probably logged out, or Weibo App uses a non-default WebView storage path (hadEncrypted=" +
|
|
215
|
+
hadEncrypted +
|
|
216
|
+
"). Tell user to relog on phone.",
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
cookie: header,
|
|
221
|
+
extractedAt: Date.now(),
|
|
222
|
+
diagnostic: {
|
|
223
|
+
cookieCount,
|
|
224
|
+
hadEncrypted,
|
|
225
|
+
hasSub,
|
|
226
|
+
cookieNames: Array.from(present),
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
} finally {
|
|
230
|
+
if (tmpFile) {
|
|
231
|
+
try {
|
|
232
|
+
fs.unlinkSync(tmpFile);
|
|
233
|
+
} catch (_e) {
|
|
234
|
+
onCleanupFailed(tmpFile);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = {
|
|
242
|
+
createWeiboCookiesExtension,
|
|
243
|
+
WEIBO_COOKIES_REMOTE_PATH,
|
|
244
|
+
WEIBO_COOKIE_HOST_DOMAIN,
|
|
245
|
+
WEIBO_REQUIRED_COOKIE,
|
|
246
|
+
assembleWeiboCookieHeader,
|
|
247
|
+
// Exposed for tests
|
|
248
|
+
_internals: {
|
|
249
|
+
pullCookiesViaSu,
|
|
250
|
+
},
|
|
251
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* social-weibo-adb — Phase 3 (Weibo C 路径) entry.
|
|
5
|
+
*
|
|
6
|
+
* Phase 3a (this commit) — desktop ADB cookies + m.weibo.cn HTTP path:
|
|
7
|
+
* - weibo.cookies extension (pulls Chromium cookies from Weibo App)
|
|
8
|
+
* - WeiboApiClient (Node port, 4 endpoints, no signing)
|
|
9
|
+
* - buildSnapshot (post / favourite / follow → schemaVersion=1)
|
|
10
|
+
* - collect / collectAndSync
|
|
11
|
+
*
|
|
12
|
+
* Pipeline:
|
|
13
|
+
* bridge.invoke("weibo.cookies")
|
|
14
|
+
* → WeiboApiClient.fetchUid (cookie has no inline UID)
|
|
15
|
+
* → fetchPosts + fetchFavourites + fetchFollows
|
|
16
|
+
* → buildSnapshot + writeSnapshotJson
|
|
17
|
+
* → registry.syncAdapter("social-weibo", { inputPath })
|
|
18
|
+
*
|
|
19
|
+
* Reuses the existing `social-weibo` adapter's snapshot mode — same
|
|
20
|
+
* vault schema / dedup / event types. No 2nd adapter.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
createWeiboCookiesExtension,
|
|
25
|
+
WEIBO_COOKIES_REMOTE_PATH,
|
|
26
|
+
WEIBO_COOKIE_HOST_DOMAIN,
|
|
27
|
+
WEIBO_REQUIRED_COOKIE,
|
|
28
|
+
assembleWeiboCookieHeader,
|
|
29
|
+
} = require("./cookies-extension");
|
|
30
|
+
const { WeiboApiClient } = require("./api-client");
|
|
31
|
+
const {
|
|
32
|
+
buildSnapshot,
|
|
33
|
+
writeSnapshotJson,
|
|
34
|
+
cleanupSnapshotJson,
|
|
35
|
+
SNAPSHOT_SCHEMA_VERSION,
|
|
36
|
+
} = require("./snapshot-builder");
|
|
37
|
+
const { collect, collectAndSync } = require("./collector");
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
// Extension factory (wiring registers this on the bridge)
|
|
41
|
+
createWeiboCookiesExtension,
|
|
42
|
+
WEIBO_COOKIES_REMOTE_PATH,
|
|
43
|
+
WEIBO_COOKIE_HOST_DOMAIN,
|
|
44
|
+
WEIBO_REQUIRED_COOKIE,
|
|
45
|
+
assembleWeiboCookieHeader,
|
|
46
|
+
// API client + builder
|
|
47
|
+
WeiboApiClient,
|
|
48
|
+
buildSnapshot,
|
|
49
|
+
writeSnapshotJson,
|
|
50
|
+
cleanupSnapshotJson,
|
|
51
|
+
SNAPSHOT_SCHEMA_VERSION,
|
|
52
|
+
// Collector orchestrator
|
|
53
|
+
collect,
|
|
54
|
+
collectAndSync,
|
|
55
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Phase 3a (Weibo C 路径 — 2026-05-25): API responses → snapshot JSON.
|
|
5
|
+
*
|
|
6
|
+
* Matches the existing `social-weibo` adapter's snapshot mode schema:
|
|
7
|
+
*
|
|
8
|
+
* {
|
|
9
|
+
* "schemaVersion": 1,
|
|
10
|
+
* "snapshottedAt": <ms>,
|
|
11
|
+
* "account": { "uid": "<numeric uid as string>", "displayName": "" },
|
|
12
|
+
* "events": [
|
|
13
|
+
* { "kind": "post", "id": "post-<mid>", "capturedAt": <ms>,
|
|
14
|
+
* "text", "mid", "source", "repostsCount", "commentsCount",
|
|
15
|
+
* "likesCount", "picCount" },
|
|
16
|
+
* { "kind": "favourite", "id": "fav-<mid>", "capturedAt": <ms>,
|
|
17
|
+
* "text", "mid", "authorScreenName" },
|
|
18
|
+
* { "kind": "follow", "id": "follow-<uid>", "capturedAt": <ms>,
|
|
19
|
+
* "uid", "screenName", "description", "avatarUrl" }
|
|
20
|
+
* ]
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* Note: `follow` items don't have an authoritative timestamp from
|
|
24
|
+
* m.weibo.cn's /api/friendships/friends — we use snapshottedAt as
|
|
25
|
+
* fallback so the timestamp is at least monotonic per sync.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const fs = require("node:fs");
|
|
29
|
+
const path = require("node:path");
|
|
30
|
+
const os = require("node:os");
|
|
31
|
+
const crypto = require("node:crypto");
|
|
32
|
+
|
|
33
|
+
const SNAPSHOT_SCHEMA_VERSION = 1;
|
|
34
|
+
|
|
35
|
+
function buildSnapshot(input) {
|
|
36
|
+
if (!input || typeof input !== "object") {
|
|
37
|
+
throw new TypeError("buildSnapshot: input must be an object");
|
|
38
|
+
}
|
|
39
|
+
const uid = input.uid;
|
|
40
|
+
if (!Number.isFinite(uid) || uid <= 0) {
|
|
41
|
+
throw new TypeError(
|
|
42
|
+
"buildSnapshot: input.uid must be a positive integer (was " + uid + ")",
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
const snapshottedAt =
|
|
46
|
+
Number.isFinite(input.snapshottedAt) && input.snapshottedAt > 0
|
|
47
|
+
? input.snapshottedAt
|
|
48
|
+
: Date.now();
|
|
49
|
+
const account = {
|
|
50
|
+
uid: String(uid),
|
|
51
|
+
displayName:
|
|
52
|
+
typeof input.displayName === "string" ? input.displayName : "",
|
|
53
|
+
};
|
|
54
|
+
const events = [];
|
|
55
|
+
|
|
56
|
+
// posts
|
|
57
|
+
const posts = Array.isArray(input.posts) ? input.posts : [];
|
|
58
|
+
posts.forEach((p, idx) => {
|
|
59
|
+
if (!p || typeof p !== "object") return;
|
|
60
|
+
events.push({
|
|
61
|
+
kind: "post",
|
|
62
|
+
id: p.mid ? `post-${p.mid}` : `post-${idx}`,
|
|
63
|
+
capturedAt: typeof p.createdAt === "number" && p.createdAt > 0 ? p.createdAt : snapshottedAt,
|
|
64
|
+
text: p.text || null,
|
|
65
|
+
mid: p.mid || null,
|
|
66
|
+
source: p.source || null,
|
|
67
|
+
repostsCount: typeof p.repostsCount === "number" ? p.repostsCount : 0,
|
|
68
|
+
commentsCount:
|
|
69
|
+
typeof p.commentsCount === "number" ? p.commentsCount : 0,
|
|
70
|
+
likesCount: typeof p.likesCount === "number" ? p.likesCount : 0,
|
|
71
|
+
picCount: typeof p.picCount === "number" ? p.picCount : 0,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// favourites
|
|
76
|
+
const favs = Array.isArray(input.favourites) ? input.favourites : [];
|
|
77
|
+
favs.forEach((f, idx) => {
|
|
78
|
+
if (!f || typeof f !== "object") return;
|
|
79
|
+
events.push({
|
|
80
|
+
kind: "favourite",
|
|
81
|
+
id: f.mid ? `fav-${f.mid}` : `fav-${idx}`,
|
|
82
|
+
capturedAt: typeof f.favAt === "number" && f.favAt > 0 ? f.favAt : snapshottedAt,
|
|
83
|
+
text: f.text || null,
|
|
84
|
+
mid: f.mid || null,
|
|
85
|
+
authorScreenName: f.authorScreenName || null,
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// follows
|
|
90
|
+
const fols = Array.isArray(input.follows) ? input.follows : [];
|
|
91
|
+
fols.forEach((fol, idx) => {
|
|
92
|
+
if (!fol || typeof fol !== "object") return;
|
|
93
|
+
const followUid = typeof fol.uid === "number" ? fol.uid : null;
|
|
94
|
+
events.push({
|
|
95
|
+
kind: "follow",
|
|
96
|
+
id: followUid != null ? `follow-${followUid}` : `follow-${idx}`,
|
|
97
|
+
// /api/friendships/friends doesn't return follow time → fall back
|
|
98
|
+
capturedAt:
|
|
99
|
+
typeof fol.followedAt === "number" && fol.followedAt > 0
|
|
100
|
+
? fol.followedAt
|
|
101
|
+
: snapshottedAt,
|
|
102
|
+
uid: followUid != null ? followUid : null,
|
|
103
|
+
screenName: fol.screenName || null,
|
|
104
|
+
description: fol.description || null,
|
|
105
|
+
avatarUrl: fol.avatarUrl || null,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
schemaVersion: SNAPSHOT_SCHEMA_VERSION,
|
|
111
|
+
snapshottedAt,
|
|
112
|
+
account,
|
|
113
|
+
events,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function writeSnapshotJson(snapshot, opts = {}) {
|
|
118
|
+
const dir = opts.dir || os.tmpdir();
|
|
119
|
+
const fileName =
|
|
120
|
+
opts.fileName || `cc-weibo-snapshot-${crypto.randomUUID()}.json`;
|
|
121
|
+
if (fileName.includes("/") || fileName.includes("\\")) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
"writeSnapshotJson: opts.fileName must be a basename, not a path",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const full = path.join(dir, fileName);
|
|
127
|
+
fs.writeFileSync(full, JSON.stringify(snapshot), "utf-8");
|
|
128
|
+
return full;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function cleanupSnapshotJson(filePath) {
|
|
132
|
+
if (!filePath) return;
|
|
133
|
+
try {
|
|
134
|
+
fs.unlinkSync(filePath);
|
|
135
|
+
} catch (_e) {
|
|
136
|
+
// ignore
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = {
|
|
141
|
+
buildSnapshot,
|
|
142
|
+
writeSnapshotJson,
|
|
143
|
+
cleanupSnapshotJson,
|
|
144
|
+
SNAPSHOT_SCHEMA_VERSION,
|
|
145
|
+
};
|