@chainlesschain/personal-data-hub 0.4.7 → 0.4.23
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/biz-tianyancha.test.js +159 -0
- package/__tests__/adapters/doc-baidu-netdisk.test.js +102 -0
- package/__tests__/adapters/doc-camscanner.test.js +147 -0
- package/__tests__/adapters/doc-platforms.test.js +177 -0
- package/__tests__/adapters/gov-ixiamen.test.js +150 -0
- package/__tests__/adapters/gov-tax.test.js +135 -0
- package/__tests__/adapters/health-meiyou.test.js +125 -0
- package/__tests__/adapters/music-kugou.test.js +187 -0
- package/__tests__/adapters/recruit-boss.test.js +180 -0
- package/__tests__/adapters/shopping-dianping.test.js +239 -0
- package/__tests__/adapters/social-csdn.test.js +175 -0
- package/__tests__/adapters/social-dongchedi.test.js +165 -0
- package/__tests__/adapters/social-zhihu.test.js +246 -0
- package/__tests__/adapters/travel-ctrip.test.js +175 -1
- package/__tests__/adapters/travel-didi.test.js +204 -0
- package/__tests__/adapters/travel-tongcheng.test.js +289 -0
- package/__tests__/adapters/video-platforms.test.js +152 -0
- package/__tests__/adapters/video-xigua.test.js +106 -0
- package/__tests__/adapters/wework-pc.test.js +124 -0
- package/lib/adapter-guide.js +25 -3
- package/lib/adapters/_document-base.js +370 -0
- package/lib/adapters/_video-base.js +331 -0
- package/lib/adapters/biz-tianyancha/index.js +348 -0
- package/lib/adapters/doc-baidu-netdisk/index.js +91 -0
- package/lib/adapters/doc-camscanner/index.js +102 -0
- package/lib/adapters/doc-tencent-docs/index.js +94 -0
- package/lib/adapters/doc-wps/index.js +77 -0
- package/lib/adapters/gov-ixiamen/index.js +380 -0
- package/lib/adapters/gov-tax/index.js +451 -0
- package/lib/adapters/health-meiyou/index.js +393 -0
- package/lib/adapters/music-kugou/index.js +418 -0
- package/lib/adapters/recruit-boss/index.js +442 -0
- package/lib/adapters/shopping-dianping/index.js +473 -0
- package/lib/adapters/social-csdn/index.js +444 -0
- package/lib/adapters/social-dongchedi/index.js +360 -0
- package/lib/adapters/social-zhihu/index.js +488 -0
- package/lib/adapters/travel-ctrip/index.js +255 -40
- package/lib/adapters/travel-didi/index.js +327 -0
- package/lib/adapters/travel-tongcheng/index.js +393 -0
- package/lib/adapters/video-iqiyi/index.js +75 -0
- package/lib/adapters/video-tencent/index.js +78 -0
- package/lib/adapters/video-xigua/index.js +68 -0
- package/lib/adapters/wework-pc/index.js +31 -0
- package/lib/index.js +40 -0
- package/package.json +1 -1
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* §A13 — 懂车帝 (Dongchedi, com.ss.android.auto) adapter, dual-mode (snapshot +
|
|
3
|
+
* cookie-api). Phase 13+ §12.1 line-784 ROI ⭐⭐ "汽车关注".
|
|
4
|
+
*
|
|
5
|
+
* 懂车帝 (ByteDance auto vertical) personal data = the user's car-interest
|
|
6
|
+
* footprint: car series / creators they follow, and articles / videos / series
|
|
7
|
+
* they favourited. Both are interest signals → favourite maps to a LIKE event,
|
|
8
|
+
* follow to a contact Person (the followed series/creator). Mirrors the
|
|
9
|
+
* social-zhihu two-mode shape.
|
|
10
|
+
*
|
|
11
|
+
* 1. snapshot mode (opts.inputPath): JSON schemaVersion 1, stateless.
|
|
12
|
+
* 2. cookie-api mode (opts.account.cookies): fetch the user's favourites +
|
|
13
|
+
* follows from dongchedi.com via the injected `fetchFn`, paginate; a sign
|
|
14
|
+
* seam (opts.signProvider) covers ByteDance's signed-request token (X-Bogus
|
|
15
|
+
* / msToken family); best-effort unsigned when absent. Endpoints overridable
|
|
16
|
+
* via opts.favouritesUrl / opts.followsUrl (best-effort, not field-verified
|
|
17
|
+
* — FAMILY-23 playbook). account OPTIONAL — the cookie carries identity.
|
|
18
|
+
*
|
|
19
|
+
* Snapshot schema (schemaVersion 1):
|
|
20
|
+
* {
|
|
21
|
+
* "schemaVersion": 1, "snapshottedAt": <ms>,
|
|
22
|
+
* "account": { "userId": "...", "name": "..." },
|
|
23
|
+
* "events": [
|
|
24
|
+
* { "kind": "favourite", "id": "fav-<id>", "itemId": "...", "title": "...",
|
|
25
|
+
* "contentType": "article|video|series", "url": "...", "capturedAt": <ms> },
|
|
26
|
+
* { "kind": "follow", "id": "follow-<id>", "followId": "...", "name": "...",
|
|
27
|
+
* "followType": "series|creator", "url": "...", "capturedAt": <ms> }
|
|
28
|
+
* ]
|
|
29
|
+
* }
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
"use strict";
|
|
33
|
+
|
|
34
|
+
const fs = require("node:fs");
|
|
35
|
+
const { newId } = require("../../ids");
|
|
36
|
+
const {
|
|
37
|
+
ENTITY_TYPES,
|
|
38
|
+
PERSON_SUBTYPES,
|
|
39
|
+
EVENT_SUBTYPES,
|
|
40
|
+
CAPTURED_BY,
|
|
41
|
+
} = require("../../constants");
|
|
42
|
+
const { CookieAuth } = require("../shopping-base");
|
|
43
|
+
|
|
44
|
+
const NAME = "social-dongchedi";
|
|
45
|
+
const VERSION = "0.1.0";
|
|
46
|
+
const SNAPSHOT_SCHEMA_VERSION = 1;
|
|
47
|
+
|
|
48
|
+
const KIND_FAVOURITE = "favourite";
|
|
49
|
+
const KIND_FOLLOW = "follow";
|
|
50
|
+
const VALID_SNAPSHOT_KINDS = Object.freeze([KIND_FAVOURITE, KIND_FOLLOW]);
|
|
51
|
+
|
|
52
|
+
// Best-effort dongchedi.com endpoints. Overridable via opts.*Url.
|
|
53
|
+
const FAVOURITES_URL = "https://www.dongchedi.com/motor/profile/get_favorite_list";
|
|
54
|
+
const FOLLOWS_URL = "https://www.dongchedi.com/motor/profile/get_follow_list";
|
|
55
|
+
const PAGE_SIZE = 20;
|
|
56
|
+
|
|
57
|
+
function parseTime(v) {
|
|
58
|
+
if (Number.isFinite(v)) return v > 1e12 ? v : v >= 1e9 ? v * 1000 : v;
|
|
59
|
+
if (typeof v === "string") {
|
|
60
|
+
if (/^\d+$/.test(v)) {
|
|
61
|
+
const n = parseInt(v, 10);
|
|
62
|
+
return n > 1e12 ? n : n >= 1e9 ? n * 1000 : n;
|
|
63
|
+
}
|
|
64
|
+
const t = Date.parse(v);
|
|
65
|
+
return Number.isFinite(t) ? t : null;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function stableOriginalId(kind, id) {
|
|
71
|
+
const safe =
|
|
72
|
+
(typeof id === "string" && id.length > 0 && id) ||
|
|
73
|
+
(typeof id === "number" && Number.isFinite(id) && String(id)) ||
|
|
74
|
+
`unknown-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
75
|
+
return `dongchedi:${kind}:${safe}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
class DongchediAdapter {
|
|
79
|
+
constructor(opts = {}) {
|
|
80
|
+
this.account = opts.account || null;
|
|
81
|
+
this._cookieAuth =
|
|
82
|
+
opts.account && opts.account.cookies
|
|
83
|
+
? new CookieAuth({ platform: "dongchedi", cookies: opts.account.cookies })
|
|
84
|
+
: null;
|
|
85
|
+
this._fetchFn = typeof opts.fetchFn === "function" ? opts.fetchFn : defaultFetch;
|
|
86
|
+
this._signProvider =
|
|
87
|
+
typeof opts.signProvider === "function" ? opts.signProvider : null;
|
|
88
|
+
this._urls = {
|
|
89
|
+
favourites: opts.favouritesUrl || FAVOURITES_URL,
|
|
90
|
+
follows: opts.followsUrl || FOLLOWS_URL,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
this.name = NAME;
|
|
94
|
+
this.version = VERSION;
|
|
95
|
+
this.capabilities = [
|
|
96
|
+
"sync:snapshot",
|
|
97
|
+
"sync:cookie-api",
|
|
98
|
+
"parse:dongchedi-favourite",
|
|
99
|
+
"parse:dongchedi-follow",
|
|
100
|
+
];
|
|
101
|
+
this.extractMode = "web-api";
|
|
102
|
+
this.rateLimits = { perMinute: 8, perDay: 200 };
|
|
103
|
+
this.dataDisclosure = {
|
|
104
|
+
fields: [
|
|
105
|
+
"dongchedi:favourite (title / contentType / url)",
|
|
106
|
+
"dongchedi:follow (name / followType)",
|
|
107
|
+
],
|
|
108
|
+
sensitivity: "low",
|
|
109
|
+
legalGate: false,
|
|
110
|
+
defaultInclude: { favourite: true, follow: true },
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
this._deps = { fs };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async authenticate(ctx = {}) {
|
|
117
|
+
if (ctx && typeof ctx.inputPath === "string" && ctx.inputPath.length > 0) {
|
|
118
|
+
try {
|
|
119
|
+
this._deps.fs.accessSync(ctx.inputPath, this._deps.fs.constants.R_OK);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return {
|
|
122
|
+
ok: false,
|
|
123
|
+
reason: "INPUT_PATH_UNREADABLE",
|
|
124
|
+
message: `snapshot not readable at ${ctx.inputPath}: ${err.message}`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return { ok: true, mode: "snapshot-file" };
|
|
128
|
+
}
|
|
129
|
+
if (this._cookieAuth) {
|
|
130
|
+
const ok = await this._cookieAuth.validate();
|
|
131
|
+
if (!ok) return { ok: false, reason: "INVALID_COOKIE", error: "cookies missing" };
|
|
132
|
+
return { ok: true, account: (this.account && this.account.userId) || null, mode: "cookie" };
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
ok: false,
|
|
136
|
+
reason: "NO_INPUT",
|
|
137
|
+
message:
|
|
138
|
+
"social-dongchedi.authenticate: needs opts.inputPath (snapshot mode) OR opts.account.cookies (cookie-api mode)",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async healthCheck() {
|
|
143
|
+
if (this._cookieAuth) {
|
|
144
|
+
const r = await this.authenticate();
|
|
145
|
+
return r.ok ? { ok: true, lastChecked: Date.now() } : { ok: false, reason: r.reason, error: r.error };
|
|
146
|
+
}
|
|
147
|
+
return { ok: true, lastChecked: Date.now() };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async *sync(opts = {}) {
|
|
151
|
+
if (typeof opts.inputPath === "string" && opts.inputPath.length > 0) {
|
|
152
|
+
yield* this._syncViaSnapshot(opts);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (this._cookieAuth) {
|
|
156
|
+
yield* this._syncViaCookie(opts);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
throw new Error(
|
|
160
|
+
"social-dongchedi.sync: needs opts.inputPath (snapshot mode) OR opts.account.cookies (cookie-api mode)",
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async *_syncViaSnapshot(opts) {
|
|
165
|
+
const raw = this._deps.fs.readFileSync(opts.inputPath, "utf-8");
|
|
166
|
+
const snapshot = JSON.parse(raw);
|
|
167
|
+
if (!snapshot || typeof snapshot !== "object" || snapshot.schemaVersion !== SNAPSHOT_SCHEMA_VERSION) {
|
|
168
|
+
throw new Error(
|
|
169
|
+
`social-dongchedi.sync: snapshot schemaVersion mismatch (got ${snapshot && snapshot.schemaVersion}, expected ${SNAPSHOT_SCHEMA_VERSION})`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
const fallback =
|
|
173
|
+
Number.isFinite(snapshot.snapshottedAt) && snapshot.snapshottedAt > 0
|
|
174
|
+
? Math.floor(snapshot.snapshottedAt)
|
|
175
|
+
: Date.now();
|
|
176
|
+
const account = snapshot.account && typeof snapshot.account === "object" ? snapshot.account : null;
|
|
177
|
+
const include = opts.include || {};
|
|
178
|
+
const limit = Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : Infinity;
|
|
179
|
+
const events = Array.isArray(snapshot.events) ? snapshot.events : [];
|
|
180
|
+
let emitted = 0;
|
|
181
|
+
for (const ev of events) {
|
|
182
|
+
if (emitted >= limit) return;
|
|
183
|
+
if (!ev || typeof ev !== "object" || !VALID_SNAPSHOT_KINDS.includes(ev.kind)) continue;
|
|
184
|
+
if (include[ev.kind] === false) continue;
|
|
185
|
+
const id = (typeof ev.id === "string" && ev.id) || ev.itemId || ev.followId || null;
|
|
186
|
+
yield {
|
|
187
|
+
adapter: NAME,
|
|
188
|
+
kind: ev.kind,
|
|
189
|
+
originalId: stableOriginalId(ev.kind, id),
|
|
190
|
+
capturedAt: parseTime(ev.capturedAt) || fallback,
|
|
191
|
+
payload: { ...ev, account },
|
|
192
|
+
};
|
|
193
|
+
emitted += 1;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async *_syncViaCookie(opts = {}) {
|
|
198
|
+
if (!(await this._cookieAuth.validate())) return;
|
|
199
|
+
const cookies = this._cookieAuth.toHeader();
|
|
200
|
+
const include = opts.include || {};
|
|
201
|
+
const limit = Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : Infinity;
|
|
202
|
+
const maxPages = Number.isInteger(opts.maxPages) && opts.maxPages > 0 ? opts.maxPages : 10;
|
|
203
|
+
|
|
204
|
+
const plan = [
|
|
205
|
+
{ kind: KIND_FAVOURITE, url: this._urls.favourites },
|
|
206
|
+
{ kind: KIND_FOLLOW, url: this._urls.follows },
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
let emitted = 0;
|
|
210
|
+
for (const step of plan) {
|
|
211
|
+
if (include[step.kind] === false) continue;
|
|
212
|
+
let offset = 0;
|
|
213
|
+
let page = 0;
|
|
214
|
+
while (page < maxPages) {
|
|
215
|
+
const query = { offset, count: PAGE_SIZE };
|
|
216
|
+
let sign = null;
|
|
217
|
+
if (this._signProvider) {
|
|
218
|
+
sign = await this._signProvider({ url: step.url, query, cookies });
|
|
219
|
+
}
|
|
220
|
+
const resp = await this._fetchFn({ url: step.url, cookies, query, sign });
|
|
221
|
+
const items = extractData(resp);
|
|
222
|
+
if (!items.length) break;
|
|
223
|
+
for (const it of items) {
|
|
224
|
+
if (!it || typeof it !== "object") continue;
|
|
225
|
+
if (emitted >= limit) return;
|
|
226
|
+
const id = step.kind === KIND_FOLLOW
|
|
227
|
+
? it.follow_id || it.user_id || it.series_id || it.id
|
|
228
|
+
: it.group_id || it.item_id || it.id;
|
|
229
|
+
yield {
|
|
230
|
+
adapter: NAME,
|
|
231
|
+
kind: step.kind,
|
|
232
|
+
originalId: stableOriginalId(step.kind, id),
|
|
233
|
+
capturedAt: parseTime(it.create_time || it.favorite_time || it.follow_time) || Date.now(),
|
|
234
|
+
payload: { item: it, kind: step.kind, cookie: true },
|
|
235
|
+
};
|
|
236
|
+
emitted += 1;
|
|
237
|
+
}
|
|
238
|
+
if (isEnd(resp) || items.length < PAGE_SIZE) break;
|
|
239
|
+
offset += items.length;
|
|
240
|
+
page += 1;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
normalize(raw) {
|
|
246
|
+
if (!raw || !raw.payload) throw new Error("DongchediAdapter.normalize: payload missing");
|
|
247
|
+
const ingestedAt = Date.now();
|
|
248
|
+
const kind = raw.kind || raw.payload.kind;
|
|
249
|
+
if (kind === KIND_FAVOURITE) return normalizeFavourite(raw, ingestedAt);
|
|
250
|
+
if (kind === KIND_FOLLOW) return normalizeFollow(raw, ingestedAt);
|
|
251
|
+
throw new Error(`DongchediAdapter.normalize: unknown kind ${kind}`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ─── cookie response helpers ─────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
function extractData(resp) {
|
|
258
|
+
if (!resp || typeof resp !== "object") return [];
|
|
259
|
+
if (Array.isArray(resp.data)) return resp.data;
|
|
260
|
+
if (Array.isArray(resp.list)) return resp.list;
|
|
261
|
+
const d = resp.data;
|
|
262
|
+
if (d && typeof d === "object") {
|
|
263
|
+
if (Array.isArray(d.list)) return d.list;
|
|
264
|
+
if (Array.isArray(d.favorite_list)) return d.favorite_list;
|
|
265
|
+
if (Array.isArray(d.follow_list)) return d.follow_list;
|
|
266
|
+
if (Array.isArray(d.records)) return d.records;
|
|
267
|
+
}
|
|
268
|
+
return [];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function isEnd(resp) {
|
|
272
|
+
const d = resp && resp.data && typeof resp.data === "object" ? resp.data : resp;
|
|
273
|
+
if (d && typeof d === "object" && (d.has_more === false || d.has_more === 0)) return true;
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ─── per-kind normalizers (snapshot fields OR cookie payload.item) ────────────
|
|
278
|
+
|
|
279
|
+
function buildSource(raw, occurredAt) {
|
|
280
|
+
return {
|
|
281
|
+
adapter: NAME,
|
|
282
|
+
adapterVersion: VERSION,
|
|
283
|
+
originalId: raw.originalId,
|
|
284
|
+
capturedAt: raw.capturedAt || occurredAt,
|
|
285
|
+
capturedBy: CAPTURED_BY.API,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function normalizeFavourite(raw, ingestedAt) {
|
|
290
|
+
const p = raw.payload;
|
|
291
|
+
const it = p.cookie ? p.item : p;
|
|
292
|
+
const title = it.title || it.group_title || it.name || "";
|
|
293
|
+
const occurredAt = parseTime(it.capturedAt || it.create_time || it.favorite_time || raw.capturedAt) || ingestedAt;
|
|
294
|
+
const source = buildSource(raw, occurredAt);
|
|
295
|
+
return {
|
|
296
|
+
events: [
|
|
297
|
+
{
|
|
298
|
+
id: newId(),
|
|
299
|
+
type: ENTITY_TYPES.EVENT,
|
|
300
|
+
subtype: EVENT_SUBTYPES.LIKE,
|
|
301
|
+
occurredAt,
|
|
302
|
+
actor: "person-self",
|
|
303
|
+
content: { title: `收藏: ${title}`.trim(), text: title },
|
|
304
|
+
ingestedAt,
|
|
305
|
+
source,
|
|
306
|
+
extra: {
|
|
307
|
+
platform: "dongchedi",
|
|
308
|
+
itemId: (it.itemId || it.group_id || it.item_id || it.id) != null
|
|
309
|
+
? String(it.itemId || it.group_id || it.item_id || it.id) : null,
|
|
310
|
+
contentType: it.contentType || it.content_type || it.type || null,
|
|
311
|
+
url: it.url || it.share_url || null,
|
|
312
|
+
},
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
persons: [],
|
|
316
|
+
places: [],
|
|
317
|
+
items: [],
|
|
318
|
+
topics: [],
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function normalizeFollow(raw, ingestedAt) {
|
|
323
|
+
const p = raw.payload;
|
|
324
|
+
const it = p.cookie ? p.item : p;
|
|
325
|
+
const followId = it.followId || it.follow_id || it.user_id || it.series_id || it.id || `unknown-${newId()}`;
|
|
326
|
+
const name = it.name || it.series_name || it.user_name || it.screen_name || "(unnamed)";
|
|
327
|
+
const followType = it.followType || it.follow_type || (it.series_id ? "series" : "creator");
|
|
328
|
+
const occurredAt = parseTime(it.capturedAt || it.follow_time || raw.capturedAt) || ingestedAt;
|
|
329
|
+
const source = buildSource(raw, occurredAt);
|
|
330
|
+
const person = {
|
|
331
|
+
id: `person-dongchedi-${followId}`,
|
|
332
|
+
type: ENTITY_TYPES.PERSON,
|
|
333
|
+
subtype: PERSON_SUBTYPES.CONTACT,
|
|
334
|
+
names: [name],
|
|
335
|
+
ingestedAt,
|
|
336
|
+
source,
|
|
337
|
+
identifiers: { "dongchedi-id": [String(followId)] },
|
|
338
|
+
extra: {
|
|
339
|
+
platform: "dongchedi",
|
|
340
|
+
followType,
|
|
341
|
+
url: it.url || it.share_url || null,
|
|
342
|
+
followedAt: occurredAt,
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
return { events: [], persons: [person], places: [], items: [], topics: [] };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function defaultFetch(_opts) {
|
|
349
|
+
throw new Error("social-dongchedi: no fetchFn configured for cookie-api mode");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
module.exports = {
|
|
353
|
+
DongchediAdapter,
|
|
354
|
+
extractData,
|
|
355
|
+
isEnd,
|
|
356
|
+
NAME,
|
|
357
|
+
VERSION,
|
|
358
|
+
SNAPSHOT_SCHEMA_VERSION,
|
|
359
|
+
VALID_SNAPSHOT_KINDS,
|
|
360
|
+
};
|