openzca 0.1.53 → 0.1.54
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/dist/cli.js +451 -159
- package/dist/db-migrations.js +52 -0
- package/dist/db-worker.js +14 -146
- package/dist/migrations/001-initial-schema.sql +138 -0
- package/dist/migrations/002-add-contacts.sql +18 -0
- package/dist/migrations/003-backfill-contacts.sql +158 -0
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -438,6 +438,35 @@ var INSERT_THREAD_MEMBER_SQL = `
|
|
|
438
438
|
account_status, member_type, raw_json, snapshot_at_ms, created_at, updated_at
|
|
439
439
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
440
440
|
`;
|
|
441
|
+
var UPSERT_CONTACT_SQL = `
|
|
442
|
+
INSERT INTO contacts (
|
|
443
|
+
profile, user_id, display_name, zalo_name, avatar, account_status,
|
|
444
|
+
relationship, first_seen_at_ms, last_seen_at_ms, raw_json, created_at, updated_at
|
|
445
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
446
|
+
ON CONFLICT(profile, user_id) DO UPDATE SET
|
|
447
|
+
display_name = COALESCE(excluded.display_name, contacts.display_name),
|
|
448
|
+
zalo_name = COALESCE(excluded.zalo_name, contacts.zalo_name),
|
|
449
|
+
avatar = COALESCE(excluded.avatar, contacts.avatar),
|
|
450
|
+
account_status = COALESCE(excluded.account_status, contacts.account_status),
|
|
451
|
+
relationship = CASE
|
|
452
|
+
WHEN contacts.relationship = 'friend' OR excluded.relationship = 'friend' THEN 'friend'
|
|
453
|
+
WHEN contacts.relationship = 'seen_dm' OR excluded.relationship = 'seen_dm' THEN 'seen_dm'
|
|
454
|
+
WHEN contacts.relationship = 'seen_group' OR excluded.relationship = 'seen_group' THEN 'seen_group'
|
|
455
|
+
ELSE COALESCE(excluded.relationship, contacts.relationship, 'unknown')
|
|
456
|
+
END,
|
|
457
|
+
first_seen_at_ms = CASE
|
|
458
|
+
WHEN contacts.first_seen_at_ms IS NULL THEN excluded.first_seen_at_ms
|
|
459
|
+
WHEN excluded.first_seen_at_ms IS NULL THEN contacts.first_seen_at_ms
|
|
460
|
+
ELSE MIN(contacts.first_seen_at_ms, excluded.first_seen_at_ms)
|
|
461
|
+
END,
|
|
462
|
+
last_seen_at_ms = CASE
|
|
463
|
+
WHEN contacts.last_seen_at_ms IS NULL THEN excluded.last_seen_at_ms
|
|
464
|
+
WHEN excluded.last_seen_at_ms IS NULL THEN contacts.last_seen_at_ms
|
|
465
|
+
ELSE MAX(contacts.last_seen_at_ms, excluded.last_seen_at_ms)
|
|
466
|
+
END,
|
|
467
|
+
raw_json = COALESCE(excluded.raw_json, contacts.raw_json),
|
|
468
|
+
updated_at = excluded.updated_at
|
|
469
|
+
`;
|
|
441
470
|
var UPSERT_MESSAGE_SQL = `
|
|
442
471
|
INSERT INTO messages (
|
|
443
472
|
profile, message_uid, scope_thread_id, raw_thread_id, thread_type,
|
|
@@ -500,6 +529,12 @@ function normalizeOptionalText(value) {
|
|
|
500
529
|
const trimmed = value.trim();
|
|
501
530
|
return trimmed || void 0;
|
|
502
531
|
}
|
|
532
|
+
function normalizeRelationship(value) {
|
|
533
|
+
if (value !== "friend" && value !== "seen_dm" && value !== "seen_group" && value !== "unknown") {
|
|
534
|
+
return void 0;
|
|
535
|
+
}
|
|
536
|
+
return value;
|
|
537
|
+
}
|
|
503
538
|
function normalizeSearchText(value) {
|
|
504
539
|
return value.normalize("NFD").replace(new RegExp("\\p{Diacritic}", "gu"), "").toLowerCase().trim();
|
|
505
540
|
}
|
|
@@ -755,35 +790,23 @@ async function replaceThreadMembers(profile, scopeThreadId, members) {
|
|
|
755
790
|
];
|
|
756
791
|
await db.batch(commands, true);
|
|
757
792
|
}
|
|
758
|
-
async function
|
|
793
|
+
async function persistContact(record) {
|
|
759
794
|
const db = await getDb(record.profile);
|
|
760
795
|
const now = nowIso2();
|
|
761
|
-
await db.run(
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
[
|
|
776
|
-
record.profile,
|
|
777
|
-
record.userId,
|
|
778
|
-
record.displayName ?? null,
|
|
779
|
-
record.zaloName ?? null,
|
|
780
|
-
record.avatar ?? null,
|
|
781
|
-
record.accountStatus ?? null,
|
|
782
|
-
record.rawJson ?? null,
|
|
783
|
-
now,
|
|
784
|
-
now
|
|
785
|
-
]
|
|
786
|
-
);
|
|
796
|
+
await db.run(UPSERT_CONTACT_SQL, [
|
|
797
|
+
record.profile,
|
|
798
|
+
record.userId,
|
|
799
|
+
record.displayName ?? null,
|
|
800
|
+
record.zaloName ?? null,
|
|
801
|
+
record.avatar ?? null,
|
|
802
|
+
record.accountStatus ?? null,
|
|
803
|
+
normalizeRelationship(record.relationship) ?? "unknown",
|
|
804
|
+
record.firstSeenAtMs ?? null,
|
|
805
|
+
record.lastSeenAtMs ?? null,
|
|
806
|
+
record.rawJson ?? null,
|
|
807
|
+
now,
|
|
808
|
+
now
|
|
809
|
+
]);
|
|
787
810
|
}
|
|
788
811
|
async function persistSelfProfile(params) {
|
|
789
812
|
const db = await getDb(params.profile);
|
|
@@ -990,8 +1013,8 @@ async function listMessages(params) {
|
|
|
990
1013
|
NULLIF(m.sender_name, ''),
|
|
991
1014
|
NULLIF(tm.display_name, ''),
|
|
992
1015
|
NULLIF(tm.zalo_name, ''),
|
|
993
|
-
NULLIF(
|
|
994
|
-
NULLIF(
|
|
1016
|
+
NULLIF(c.display_name, ''),
|
|
1017
|
+
NULLIF(c.zalo_name, '')
|
|
995
1018
|
) AS sender_name,
|
|
996
1019
|
m.to_id,
|
|
997
1020
|
m.timestamp_ms,
|
|
@@ -1008,9 +1031,9 @@ async function listMessages(params) {
|
|
|
1008
1031
|
ON tm.profile = m.profile
|
|
1009
1032
|
AND tm.scope_thread_id = m.scope_thread_id
|
|
1010
1033
|
AND tm.user_id = m.sender_id
|
|
1011
|
-
LEFT JOIN
|
|
1012
|
-
ON
|
|
1013
|
-
AND
|
|
1034
|
+
LEFT JOIN contacts c
|
|
1035
|
+
ON c.profile = m.profile
|
|
1036
|
+
AND c.user_id = m.sender_id
|
|
1014
1037
|
WHERE m.profile = ?
|
|
1015
1038
|
AND m.scope_thread_id = ?
|
|
1016
1039
|
AND m.thread_type = ?
|
|
@@ -1222,34 +1245,74 @@ async function getThreadInfo(params) {
|
|
|
1222
1245
|
raw: row.raw_json ? JSON.parse(row.raw_json) : void 0
|
|
1223
1246
|
};
|
|
1224
1247
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1248
|
+
var CONTACT_CHAT_CTE = `
|
|
1249
|
+
WITH ranked_contact_threads AS (
|
|
1250
|
+
SELECT
|
|
1251
|
+
c.profile AS contact_profile,
|
|
1252
|
+
c.user_id,
|
|
1253
|
+
t.scope_thread_id,
|
|
1254
|
+
t.title,
|
|
1255
|
+
COUNT(m.message_uid) AS message_count,
|
|
1256
|
+
MAX(m.timestamp_ms) AS last_message_at_ms,
|
|
1257
|
+
ROW_NUMBER() OVER (
|
|
1258
|
+
PARTITION BY c.profile, c.user_id
|
|
1259
|
+
ORDER BY
|
|
1260
|
+
COALESCE(MAX(m.timestamp_ms), 0) DESC,
|
|
1261
|
+
CASE
|
|
1262
|
+
WHEN t.scope_thread_id = c.user_id THEN 0
|
|
1263
|
+
WHEN t.peer_id = c.user_id THEN 1
|
|
1264
|
+
WHEN t.raw_thread_id = c.user_id THEN 2
|
|
1265
|
+
ELSE 3
|
|
1266
|
+
END,
|
|
1267
|
+
t.updated_at DESC,
|
|
1268
|
+
t.scope_thread_id
|
|
1269
|
+
) AS thread_rank
|
|
1270
|
+
FROM contacts c
|
|
1271
|
+
LEFT JOIN threads t
|
|
1272
|
+
ON t.profile = c.profile
|
|
1273
|
+
AND t.thread_type = 'user'
|
|
1274
|
+
AND (t.peer_id = c.user_id OR t.scope_thread_id = c.user_id OR t.raw_thread_id = c.user_id)
|
|
1275
|
+
LEFT JOIN messages m
|
|
1276
|
+
ON m.profile = t.profile
|
|
1277
|
+
AND m.scope_thread_id = t.scope_thread_id
|
|
1278
|
+
GROUP BY
|
|
1279
|
+
c.profile,
|
|
1280
|
+
c.user_id,
|
|
1281
|
+
t.scope_thread_id,
|
|
1282
|
+
t.title,
|
|
1283
|
+
t.updated_at,
|
|
1284
|
+
t.peer_id,
|
|
1285
|
+
t.raw_thread_id
|
|
1286
|
+
)
|
|
1287
|
+
`;
|
|
1288
|
+
async function listContacts(params) {
|
|
1289
|
+
const db = await getDb(params.profile);
|
|
1227
1290
|
const rows = await db.all(
|
|
1228
1291
|
`
|
|
1292
|
+
${CONTACT_CHAT_CTE}
|
|
1229
1293
|
SELECT
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
AND
|
|
1247
|
-
WHERE
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
ORDER BY COALESCE(f.display_name, f.zalo_name, f.user_id), f.user_id
|
|
1294
|
+
c.user_id,
|
|
1295
|
+
c.display_name,
|
|
1296
|
+
c.zalo_name,
|
|
1297
|
+
c.avatar,
|
|
1298
|
+
c.account_status,
|
|
1299
|
+
c.relationship,
|
|
1300
|
+
c.first_seen_at_ms,
|
|
1301
|
+
c.last_seen_at_ms,
|
|
1302
|
+
r.scope_thread_id AS chat_id,
|
|
1303
|
+
r.title,
|
|
1304
|
+
COALESCE(r.message_count, 0) AS message_count,
|
|
1305
|
+
r.last_message_at_ms
|
|
1306
|
+
FROM contacts c
|
|
1307
|
+
LEFT JOIN ranked_contact_threads r
|
|
1308
|
+
ON r.contact_profile = c.profile
|
|
1309
|
+
AND r.user_id = c.user_id
|
|
1310
|
+
AND r.thread_rank = 1
|
|
1311
|
+
WHERE c.profile = ?
|
|
1312
|
+
AND (? IS NULL OR c.relationship = ?)
|
|
1313
|
+
ORDER BY COALESCE(c.display_name, c.zalo_name, c.user_id), c.user_id
|
|
1251
1314
|
`,
|
|
1252
|
-
[profile]
|
|
1315
|
+
[params.profile, params.relationship ?? null, params.relationship ?? null]
|
|
1253
1316
|
);
|
|
1254
1317
|
return rows.map((row) => ({
|
|
1255
1318
|
userId: row.user_id,
|
|
@@ -1257,43 +1320,53 @@ async function listFriends(profile) {
|
|
|
1257
1320
|
zaloName: row.zalo_name ?? void 0,
|
|
1258
1321
|
avatar: row.avatar ?? void 0,
|
|
1259
1322
|
accountStatus: row.account_status ?? void 0,
|
|
1323
|
+
relationship: normalizeRelationship(row.relationship) ?? "unknown",
|
|
1324
|
+
firstSeenAtMs: row.first_seen_at_ms ?? void 0,
|
|
1325
|
+
lastSeenAtMs: row.last_seen_at_ms ?? void 0,
|
|
1260
1326
|
title: row.title ?? void 0,
|
|
1261
1327
|
chatId: row.chat_id ?? row.user_id,
|
|
1262
1328
|
messageCount: row.message_count,
|
|
1263
1329
|
lastMessageAtMs: row.last_message_at_ms ?? void 0
|
|
1264
1330
|
}));
|
|
1265
1331
|
}
|
|
1266
|
-
async function
|
|
1332
|
+
async function listFriends(profile) {
|
|
1333
|
+
return await listContacts({ profile, relationship: "friend" });
|
|
1334
|
+
}
|
|
1335
|
+
async function getContactInfo(params) {
|
|
1267
1336
|
const db = await getDb(params.profile);
|
|
1268
1337
|
const row = await db.get(
|
|
1269
1338
|
`
|
|
1339
|
+
${CONTACT_CHAT_CTE}
|
|
1270
1340
|
SELECT
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
AND
|
|
1289
|
-
WHERE
|
|
1290
|
-
AND
|
|
1291
|
-
|
|
1292
|
-
f.user_id, f.display_name, f.zalo_name, f.avatar, f.account_status,
|
|
1293
|
-
f.raw_json, t.scope_thread_id, t.title
|
|
1341
|
+
c.user_id,
|
|
1342
|
+
c.display_name,
|
|
1343
|
+
c.zalo_name,
|
|
1344
|
+
c.avatar,
|
|
1345
|
+
c.account_status,
|
|
1346
|
+
c.relationship,
|
|
1347
|
+
c.first_seen_at_ms,
|
|
1348
|
+
c.last_seen_at_ms,
|
|
1349
|
+
c.raw_json,
|
|
1350
|
+
r.scope_thread_id AS chat_id,
|
|
1351
|
+
r.title,
|
|
1352
|
+
COALESCE(r.message_count, 0) AS message_count,
|
|
1353
|
+
r.last_message_at_ms
|
|
1354
|
+
FROM contacts c
|
|
1355
|
+
LEFT JOIN ranked_contact_threads r
|
|
1356
|
+
ON r.contact_profile = c.profile
|
|
1357
|
+
AND r.user_id = c.user_id
|
|
1358
|
+
AND r.thread_rank = 1
|
|
1359
|
+
WHERE c.profile = ?
|
|
1360
|
+
AND c.user_id = ?
|
|
1361
|
+
AND (? IS NULL OR c.relationship = ?)
|
|
1294
1362
|
LIMIT 1
|
|
1295
1363
|
`,
|
|
1296
|
-
[
|
|
1364
|
+
[
|
|
1365
|
+
params.profile,
|
|
1366
|
+
params.userId,
|
|
1367
|
+
params.relationship ?? null,
|
|
1368
|
+
params.relationship ?? null
|
|
1369
|
+
]
|
|
1297
1370
|
);
|
|
1298
1371
|
if (!row) {
|
|
1299
1372
|
return null;
|
|
@@ -1304,6 +1377,9 @@ async function getFriendInfo(params) {
|
|
|
1304
1377
|
zaloName: row.zalo_name ?? void 0,
|
|
1305
1378
|
avatar: row.avatar ?? void 0,
|
|
1306
1379
|
accountStatus: row.account_status ?? void 0,
|
|
1380
|
+
relationship: normalizeRelationship(row.relationship) ?? "unknown",
|
|
1381
|
+
firstSeenAtMs: row.first_seen_at_ms ?? void 0,
|
|
1382
|
+
lastSeenAtMs: row.last_seen_at_ms ?? void 0,
|
|
1307
1383
|
title: row.title ?? void 0,
|
|
1308
1384
|
chatId: row.chat_id ?? row.user_id,
|
|
1309
1385
|
messageCount: row.message_count,
|
|
@@ -1311,12 +1387,22 @@ async function getFriendInfo(params) {
|
|
|
1311
1387
|
raw: row.raw_json ? JSON.parse(row.raw_json) : void 0
|
|
1312
1388
|
};
|
|
1313
1389
|
}
|
|
1314
|
-
async function
|
|
1390
|
+
async function getFriendInfo(params) {
|
|
1391
|
+
return await getContactInfo({
|
|
1392
|
+
profile: params.profile,
|
|
1393
|
+
userId: params.userId,
|
|
1394
|
+
relationship: "friend"
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
async function findContacts(params) {
|
|
1315
1398
|
const query = normalizeSearchText(params.query);
|
|
1316
1399
|
if (!query) {
|
|
1317
1400
|
return [];
|
|
1318
1401
|
}
|
|
1319
|
-
const rows = await
|
|
1402
|
+
const rows = await listContacts({
|
|
1403
|
+
profile: params.profile,
|
|
1404
|
+
relationship: params.relationship
|
|
1405
|
+
});
|
|
1320
1406
|
return rows.filter((row) => {
|
|
1321
1407
|
const haystacks = [
|
|
1322
1408
|
row.userId,
|
|
@@ -1327,6 +1413,52 @@ async function findFriends(params) {
|
|
|
1327
1413
|
return haystacks.some((value) => matchesSearchPattern(value, query));
|
|
1328
1414
|
});
|
|
1329
1415
|
}
|
|
1416
|
+
async function findFriends(params) {
|
|
1417
|
+
return await findContacts({
|
|
1418
|
+
profile: params.profile,
|
|
1419
|
+
query: params.query,
|
|
1420
|
+
relationship: "friend"
|
|
1421
|
+
});
|
|
1422
|
+
}
|
|
1423
|
+
async function reconcileFriendRelationships(params) {
|
|
1424
|
+
const db = await getDb(params.profile);
|
|
1425
|
+
const now = nowIso2();
|
|
1426
|
+
const friendIds = Array.from(
|
|
1427
|
+
new Set(params.currentFriendIds.map((value) => normalizeId(value)).filter(Boolean))
|
|
1428
|
+
);
|
|
1429
|
+
const stalePredicate = friendIds.length > 0 ? `AND contacts.user_id NOT IN (${friendIds.map(() => "?").join(", ")})` : "";
|
|
1430
|
+
const sqlParams = [now, params.profile, ...friendIds];
|
|
1431
|
+
await db.run(
|
|
1432
|
+
`
|
|
1433
|
+
UPDATE contacts
|
|
1434
|
+
SET relationship = CASE
|
|
1435
|
+
WHEN EXISTS (
|
|
1436
|
+
SELECT 1
|
|
1437
|
+
FROM threads t
|
|
1438
|
+
WHERE t.profile = contacts.profile
|
|
1439
|
+
AND t.thread_type = 'user'
|
|
1440
|
+
AND (
|
|
1441
|
+
t.peer_id = contacts.user_id
|
|
1442
|
+
OR t.scope_thread_id = contacts.user_id
|
|
1443
|
+
OR t.raw_thread_id = contacts.user_id
|
|
1444
|
+
)
|
|
1445
|
+
) THEN 'seen_dm'
|
|
1446
|
+
WHEN EXISTS (
|
|
1447
|
+
SELECT 1
|
|
1448
|
+
FROM thread_members tm
|
|
1449
|
+
WHERE tm.profile = contacts.profile
|
|
1450
|
+
AND tm.user_id = contacts.user_id
|
|
1451
|
+
) THEN 'seen_group'
|
|
1452
|
+
ELSE 'unknown'
|
|
1453
|
+
END,
|
|
1454
|
+
updated_at = ?
|
|
1455
|
+
WHERE contacts.profile = ?
|
|
1456
|
+
AND contacts.relationship = 'friend'
|
|
1457
|
+
${stalePredicate}
|
|
1458
|
+
`,
|
|
1459
|
+
sqlParams
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1330
1462
|
async function listChats(profile) {
|
|
1331
1463
|
return listThreads({ profile });
|
|
1332
1464
|
}
|
|
@@ -4018,6 +4150,20 @@ async function persistOutgoingMessageBestEffort(params) {
|
|
|
4018
4150
|
async function persistGroupMembersSnapshot(profile, groupId, api) {
|
|
4019
4151
|
const rows = await listGroupMemberRows(api, groupId);
|
|
4020
4152
|
const snapshotAtMs = Date.now();
|
|
4153
|
+
for (const row of rows) {
|
|
4154
|
+
await persistContact({
|
|
4155
|
+
profile,
|
|
4156
|
+
userId: row.userId,
|
|
4157
|
+
displayName: row.displayName,
|
|
4158
|
+
zaloName: row.zaloName,
|
|
4159
|
+
avatar: row.avatar,
|
|
4160
|
+
accountStatus: row.accountStatus,
|
|
4161
|
+
relationship: "seen_group",
|
|
4162
|
+
firstSeenAtMs: snapshotAtMs,
|
|
4163
|
+
lastSeenAtMs: snapshotAtMs,
|
|
4164
|
+
rawJson: row.rawJson
|
|
4165
|
+
});
|
|
4166
|
+
}
|
|
4021
4167
|
await replaceThreadMembers(
|
|
4022
4168
|
profile,
|
|
4023
4169
|
groupId,
|
|
@@ -4027,7 +4173,9 @@ async function persistGroupMembersSnapshot(profile, groupId, api) {
|
|
|
4027
4173
|
userId: row.userId,
|
|
4028
4174
|
displayName: row.displayName,
|
|
4029
4175
|
zaloName: row.zaloName,
|
|
4030
|
-
|
|
4176
|
+
avatar: row.avatar,
|
|
4177
|
+
accountStatus: row.accountStatus,
|
|
4178
|
+
rawJson: row.rawJson ?? JSON.stringify(row),
|
|
4031
4179
|
snapshotAtMs
|
|
4032
4180
|
}))
|
|
4033
4181
|
);
|
|
@@ -4043,13 +4191,14 @@ async function persistFriendDirectory(profile, api) {
|
|
|
4043
4191
|
const zaloName = typeof record.zaloName === "string" && record.zaloName.trim() ? record.zaloName.trim() : void 0;
|
|
4044
4192
|
const avatar = typeof record.avatar === "string" && record.avatar.trim() ? record.avatar.trim() : void 0;
|
|
4045
4193
|
const title = displayName || zaloName || userId;
|
|
4046
|
-
await
|
|
4194
|
+
await persistContact({
|
|
4047
4195
|
profile,
|
|
4048
4196
|
userId,
|
|
4049
4197
|
displayName,
|
|
4050
4198
|
zaloName,
|
|
4051
4199
|
avatar,
|
|
4052
4200
|
accountStatus: typeof record.accountStatus === "number" && Number.isFinite(record.accountStatus) ? Math.trunc(record.accountStatus) : void 0,
|
|
4201
|
+
relationship: "friend",
|
|
4053
4202
|
rawJson: JSON.stringify(friend2)
|
|
4054
4203
|
});
|
|
4055
4204
|
await persistThread({
|
|
@@ -4063,6 +4212,10 @@ async function persistFriendDirectory(profile, api) {
|
|
|
4063
4212
|
});
|
|
4064
4213
|
nameById.set(userId, title);
|
|
4065
4214
|
}
|
|
4215
|
+
await reconcileFriendRelationships({
|
|
4216
|
+
profile,
|
|
4217
|
+
currentFriendIds: Array.from(nameById.keys())
|
|
4218
|
+
});
|
|
4066
4219
|
return nameById;
|
|
4067
4220
|
}
|
|
4068
4221
|
function parseSinceDuration(label, value) {
|
|
@@ -4202,6 +4355,99 @@ async function prepareDbGroupTarget(params) {
|
|
|
4202
4355
|
});
|
|
4203
4356
|
await persistGroupMembersSnapshot(params.profile, params.groupId, params.api);
|
|
4204
4357
|
}
|
|
4358
|
+
function resolveContactDisplayName(params) {
|
|
4359
|
+
return params.displayName?.trim() || params.zaloName?.trim() || params.fallbackTitle?.trim() || params.userId.trim() || void 0;
|
|
4360
|
+
}
|
|
4361
|
+
async function persistLiveDmContact(params) {
|
|
4362
|
+
if (!params.peerId) {
|
|
4363
|
+
return;
|
|
4364
|
+
}
|
|
4365
|
+
let displayName = params.senderDisplayName?.trim() || void 0;
|
|
4366
|
+
let zaloName = params.senderName?.trim() || void 0;
|
|
4367
|
+
let avatar;
|
|
4368
|
+
let accountStatus;
|
|
4369
|
+
let rawJson = params.rawJson;
|
|
4370
|
+
const existing = await getContactInfo({
|
|
4371
|
+
profile: params.profile,
|
|
4372
|
+
userId: params.peerId
|
|
4373
|
+
});
|
|
4374
|
+
if (!displayName || !existing?.avatar) {
|
|
4375
|
+
try {
|
|
4376
|
+
const response = await params.api.getUserInfo(params.peerId);
|
|
4377
|
+
const profiles = response.changed_profiles ?? {};
|
|
4378
|
+
const profile = profiles[params.peerId] ?? profiles[`${params.peerId}_0`] ?? Object.values(profiles).find((value) => normalizeCachedId(value?.userId ?? value?.uid) === params.peerId) ?? void 0;
|
|
4379
|
+
if (profile) {
|
|
4380
|
+
displayName = displayName || (typeof profile.displayName === "string" && profile.displayName.trim() ? profile.displayName.trim() : void 0) || (typeof profile.display_name === "string" && profile.display_name.trim() ? profile.display_name.trim() : void 0);
|
|
4381
|
+
zaloName = zaloName || (typeof profile.zaloName === "string" && profile.zaloName.trim() ? profile.zaloName.trim() : void 0) || (typeof profile.zalo_name === "string" && profile.zalo_name.trim() ? profile.zalo_name.trim() : void 0);
|
|
4382
|
+
avatar = typeof profile.avatar === "string" && profile.avatar.trim() ? profile.avatar.trim() : void 0;
|
|
4383
|
+
accountStatus = typeof profile.accountStatus === "number" && Number.isFinite(profile.accountStatus) ? Math.trunc(profile.accountStatus) : void 0;
|
|
4384
|
+
rawJson = JSON.stringify(profile);
|
|
4385
|
+
}
|
|
4386
|
+
} catch {
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
const title = resolveContactDisplayName({
|
|
4390
|
+
userId: params.peerId,
|
|
4391
|
+
displayName,
|
|
4392
|
+
zaloName,
|
|
4393
|
+
fallbackTitle: typeof existing?.title === "string" ? existing.title : void 0
|
|
4394
|
+
});
|
|
4395
|
+
await persistContact({
|
|
4396
|
+
profile: params.profile,
|
|
4397
|
+
userId: params.peerId,
|
|
4398
|
+
displayName,
|
|
4399
|
+
zaloName,
|
|
4400
|
+
avatar,
|
|
4401
|
+
accountStatus,
|
|
4402
|
+
relationship: "seen_dm",
|
|
4403
|
+
firstSeenAtMs: params.timestampMs,
|
|
4404
|
+
lastSeenAtMs: params.timestampMs,
|
|
4405
|
+
rawJson
|
|
4406
|
+
});
|
|
4407
|
+
await persistThread({
|
|
4408
|
+
profile: params.profile,
|
|
4409
|
+
scopeThreadId: params.peerId,
|
|
4410
|
+
rawThreadId: params.peerId,
|
|
4411
|
+
threadType: "user",
|
|
4412
|
+
peerId: params.peerId,
|
|
4413
|
+
title,
|
|
4414
|
+
rawJson
|
|
4415
|
+
});
|
|
4416
|
+
}
|
|
4417
|
+
async function hydrateUnknownLiveGroup(params) {
|
|
4418
|
+
const existing = await getThreadInfo({
|
|
4419
|
+
profile: params.profile,
|
|
4420
|
+
threadId: params.groupId,
|
|
4421
|
+
threadType: "group"
|
|
4422
|
+
});
|
|
4423
|
+
if (existing && (existing.title || typeof existing.memberCount === "number" && existing.memberCount > 0)) {
|
|
4424
|
+
return;
|
|
4425
|
+
}
|
|
4426
|
+
try {
|
|
4427
|
+
const info = await params.api.getGroupInfo(params.groupId);
|
|
4428
|
+
const group2 = info.gridInfoMap[params.groupId];
|
|
4429
|
+
const title = typeof group2?.name === "string" && group2.name.trim() ? group2.name.trim() : typeof group2?.groupName === "string" && group2.groupName.trim() ? group2.groupName.trim() : params.fallbackTitle?.trim() || void 0;
|
|
4430
|
+
await persistThread({
|
|
4431
|
+
profile: params.profile,
|
|
4432
|
+
scopeThreadId: params.groupId,
|
|
4433
|
+
rawThreadId: params.groupId,
|
|
4434
|
+
threadType: "group",
|
|
4435
|
+
title,
|
|
4436
|
+
rawJson: group2 ? JSON.stringify(group2) : void 0
|
|
4437
|
+
});
|
|
4438
|
+
await persistGroupMembersSnapshot(params.profile, params.groupId, params.api);
|
|
4439
|
+
} catch {
|
|
4440
|
+
if (params.fallbackTitle?.trim()) {
|
|
4441
|
+
await persistThread({
|
|
4442
|
+
profile: params.profile,
|
|
4443
|
+
scopeThreadId: params.groupId,
|
|
4444
|
+
rawThreadId: params.groupId,
|
|
4445
|
+
threadType: "group",
|
|
4446
|
+
title: params.fallbackTitle.trim()
|
|
4447
|
+
});
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
}
|
|
4205
4451
|
async function syncDbGroupHistoryFull(params) {
|
|
4206
4452
|
if (params.targetGroupIds.size === 0) {
|
|
4207
4453
|
return;
|
|
@@ -4580,17 +4826,26 @@ async function listGroupMemberRows(api, groupId) {
|
|
|
4580
4826
|
if (!profile) continue;
|
|
4581
4827
|
const normalizedKey = normalizeGroupMemberId(key);
|
|
4582
4828
|
if (normalizedKey && !profileMap.has(normalizedKey)) {
|
|
4583
|
-
profileMap.set(normalizedKey,
|
|
4829
|
+
profileMap.set(normalizedKey, {
|
|
4830
|
+
...profile,
|
|
4831
|
+
rawJson: JSON.stringify(profile)
|
|
4832
|
+
});
|
|
4584
4833
|
}
|
|
4585
4834
|
const profileId = normalizeGroupMemberId(profile.id);
|
|
4586
4835
|
if (profileId && !profileMap.has(profileId)) {
|
|
4587
|
-
profileMap.set(profileId,
|
|
4836
|
+
profileMap.set(profileId, {
|
|
4837
|
+
...profile,
|
|
4838
|
+
rawJson: JSON.stringify(profile)
|
|
4839
|
+
});
|
|
4588
4840
|
}
|
|
4589
4841
|
}
|
|
4590
4842
|
return ids.map((id) => ({
|
|
4591
4843
|
userId: id,
|
|
4592
4844
|
displayName: profileMap.get(id)?.displayName ?? currentMemberMap.get(id)?.displayName ?? "",
|
|
4593
|
-
zaloName: profileMap.get(id)?.zaloName ?? currentMemberMap.get(id)?.zaloName ?? ""
|
|
4845
|
+
zaloName: profileMap.get(id)?.zaloName ?? currentMemberMap.get(id)?.zaloName ?? "",
|
|
4846
|
+
avatar: profileMap.get(id)?.avatar,
|
|
4847
|
+
accountStatus: profileMap.get(id)?.accountStatus,
|
|
4848
|
+
rawJson: profileMap.get(id)?.rawJson
|
|
4594
4849
|
}));
|
|
4595
4850
|
}
|
|
4596
4851
|
async function listGroupMentionMembers(api, threadId) {
|
|
@@ -6555,52 +6810,69 @@ dbGroup.command("messages <groupId>").option("--since <duration>", "Rolling wind
|
|
|
6555
6810
|
);
|
|
6556
6811
|
})
|
|
6557
6812
|
);
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
)
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
);
|
|
6571
|
-
|
|
6572
|
-
|
|
6573
|
-
|
|
6574
|
-
|
|
6575
|
-
|
|
6576
|
-
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
);
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
|
|
6591
|
-
|
|
6592
|
-
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6598
|
-
|
|
6599
|
-
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6813
|
+
function registerDbContactQueryCommand(params) {
|
|
6814
|
+
params.command.command("list").option("-j, --json", "JSON output").description(`List ${params.label} stored in the local DB`).action(
|
|
6815
|
+
wrapAction(async (opts, command) => {
|
|
6816
|
+
const profile = await currentProfile(command);
|
|
6817
|
+
const rows = params.relationship === "friend" ? await listFriends(profile) : await listContacts({ profile });
|
|
6818
|
+
output(rows, Boolean(opts.json));
|
|
6819
|
+
})
|
|
6820
|
+
);
|
|
6821
|
+
params.command.command("find <query>").option("-j, --json", "JSON output").description(`Find stored ${params.label} by ID or name`).action(
|
|
6822
|
+
wrapAction(async (query, opts, command) => {
|
|
6823
|
+
const profile = await currentProfile(command);
|
|
6824
|
+
const rows = params.relationship === "friend" ? await findFriends({ profile, query }) : await findContacts({ profile, query });
|
|
6825
|
+
output(rows, Boolean(opts.json));
|
|
6826
|
+
})
|
|
6827
|
+
);
|
|
6828
|
+
params.command.command("info <userId>").option("-j, --json", "JSON output").description(`Show stored info for a ${params.label.slice(0, -1)}`).action(
|
|
6829
|
+
wrapAction(async (userId, opts, command) => {
|
|
6830
|
+
const profile = await currentProfile(command);
|
|
6831
|
+
const row = params.relationship === "friend" ? await getFriendInfo({ profile, userId }) : await getContactInfo({ profile, userId });
|
|
6832
|
+
if (!row) {
|
|
6833
|
+
throw new Error(`${params.label.slice(0, -1).replace(/^./, (value) => value.toUpperCase())} not found in DB: ${userId}`);
|
|
6834
|
+
}
|
|
6835
|
+
output(row, Boolean(opts.json));
|
|
6836
|
+
})
|
|
6837
|
+
);
|
|
6838
|
+
params.command.command("messages <userId>").option("--since <duration>", "Rolling window ending now: duration like 30s, 7m, 24h, 7d, or 2w").option("--from <time>", "Lower time bound: ISO timestamp, date, or unix seconds/ms").option("--until <time>", "Upper time bound: ISO timestamp, date, or unix seconds/ms").option("--to <time>", "Alias for --until").option("--limit <count>", "Maximum number of rows").option("--all", "Return all matching rows").option("--oldest-first", "Sort oldest-first instead of newest-first").option("-j, --json", "JSON output").description(`List stored direct-message rows for a ${params.label.slice(0, -1)}`).action(
|
|
6839
|
+
wrapAction(async (userId, opts, command) => {
|
|
6840
|
+
const profile = await currentProfile(command);
|
|
6841
|
+
const { sinceMs, untilMs, limit, newestFirst } = resolveMessageQueryOptions(opts);
|
|
6842
|
+
const contact = params.relationship === "friend" ? await getFriendInfo({ profile, userId }) : await getContactInfo({ profile, userId });
|
|
6843
|
+
const threadId = contact && typeof contact.chatId === "string" && contact.chatId.trim() ? contact.chatId : userId;
|
|
6844
|
+
const rows = await listMessages({
|
|
6845
|
+
profile,
|
|
6846
|
+
threadId,
|
|
6847
|
+
threadType: "user",
|
|
6848
|
+
sinceMs,
|
|
6849
|
+
untilMs,
|
|
6850
|
+
limit,
|
|
6851
|
+
newestFirst
|
|
6852
|
+
});
|
|
6853
|
+
output(
|
|
6854
|
+
{
|
|
6855
|
+
userId,
|
|
6856
|
+
chatId: threadId,
|
|
6857
|
+
count: rows.length,
|
|
6858
|
+
messages: rows
|
|
6859
|
+
},
|
|
6860
|
+
Boolean(opts.json)
|
|
6861
|
+
);
|
|
6862
|
+
})
|
|
6863
|
+
);
|
|
6864
|
+
}
|
|
6865
|
+
var dbContact = dbCmd.command("contact").description("Query stored contact data");
|
|
6866
|
+
registerDbContactQueryCommand({
|
|
6867
|
+
command: dbContact,
|
|
6868
|
+
label: "contacts"
|
|
6869
|
+
});
|
|
6870
|
+
var dbFriend = dbCmd.command("friend").description("Query stored confirmed friend contacts");
|
|
6871
|
+
registerDbContactQueryCommand({
|
|
6872
|
+
command: dbFriend,
|
|
6873
|
+
label: "friends",
|
|
6874
|
+
relationship: "friend"
|
|
6875
|
+
});
|
|
6604
6876
|
var dbChat = dbCmd.command("chat").description("Query stored conversation data");
|
|
6605
6877
|
dbChat.command("list").option("-j, --json", "JSON output").description("List chats stored in the local DB").action(
|
|
6606
6878
|
wrapAction(async (opts, command) => {
|
|
@@ -8409,34 +8681,54 @@ ${replyContextText}` : replyContextText;
|
|
|
8409
8681
|
rawJson: JSON.stringify(mention)
|
|
8410
8682
|
}));
|
|
8411
8683
|
scheduleDbWrite(profile, command, "listen.db.persist_error", async () => {
|
|
8412
|
-
|
|
8413
|
-
|
|
8684
|
+
const normalizedRecord = normalizeInboundListenRecord({
|
|
8685
|
+
profile,
|
|
8686
|
+
threadType: chatType,
|
|
8687
|
+
rawThreadId: message.threadId,
|
|
8688
|
+
senderId,
|
|
8689
|
+
senderName: senderDisplayName,
|
|
8690
|
+
toId,
|
|
8691
|
+
selfId,
|
|
8692
|
+
title: chatType === "group" ? threadName : senderDisplayName,
|
|
8693
|
+
msgId: message.data.msgId,
|
|
8694
|
+
cliMsgId: message.data.cliMsgId,
|
|
8695
|
+
actionId: getStringCandidate(messageData, ["actionId"]),
|
|
8696
|
+
timestampMs,
|
|
8697
|
+
msgType: msgType || void 0,
|
|
8698
|
+
contentText: processedText || rawText || void 0,
|
|
8699
|
+
contentJson: rawContent && typeof rawContent === "object" ? JSON.stringify(rawContent) : void 0,
|
|
8700
|
+
quoteMsgId: quote?.globalMsgId ? String(quote.globalMsgId) : void 0,
|
|
8701
|
+
quoteCliMsgId: quote?.cliMsgId ? String(quote.cliMsgId) : void 0,
|
|
8702
|
+
quoteOwnerId: quote?.ownerId ? String(quote.ownerId) : void 0,
|
|
8703
|
+
quoteText: quote?.msg,
|
|
8704
|
+
media: mediaForDb,
|
|
8705
|
+
mentions: mentionsForDb,
|
|
8706
|
+
rawMessage: message.data,
|
|
8707
|
+
rawPayload: payload,
|
|
8708
|
+
source: "listen"
|
|
8709
|
+
});
|
|
8710
|
+
if (chatType === "group") {
|
|
8711
|
+
await hydrateUnknownLiveGroup({
|
|
8712
|
+
profile,
|
|
8713
|
+
api,
|
|
8714
|
+
groupId: normalizedRecord.scopeThreadId,
|
|
8715
|
+
fallbackTitle: threadName
|
|
8716
|
+
});
|
|
8717
|
+
} else {
|
|
8718
|
+
await persistLiveDmContact({
|
|
8414
8719
|
profile,
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8720
|
+
api,
|
|
8721
|
+
peerId: normalizedRecord.scopeThreadId,
|
|
8722
|
+
senderDisplayName,
|
|
8418
8723
|
senderName: senderDisplayName,
|
|
8419
|
-
toId,
|
|
8420
|
-
selfId,
|
|
8421
|
-
title: threadName,
|
|
8422
|
-
msgId: message.data.msgId,
|
|
8423
|
-
cliMsgId: message.data.cliMsgId,
|
|
8424
|
-
actionId: getStringCandidate(messageData, ["actionId"]),
|
|
8425
8724
|
timestampMs,
|
|
8426
|
-
|
|
8427
|
-
|
|
8428
|
-
|
|
8429
|
-
|
|
8430
|
-
|
|
8431
|
-
|
|
8432
|
-
|
|
8433
|
-
media: mediaForDb,
|
|
8434
|
-
mentions: mentionsForDb,
|
|
8435
|
-
rawMessage: message.data,
|
|
8436
|
-
rawPayload: payload,
|
|
8437
|
-
source: "listen"
|
|
8438
|
-
})
|
|
8439
|
-
);
|
|
8725
|
+
rawJson: JSON.stringify({
|
|
8726
|
+
userId: normalizedRecord.scopeThreadId,
|
|
8727
|
+
displayName: senderDisplayName
|
|
8728
|
+
})
|
|
8729
|
+
});
|
|
8730
|
+
}
|
|
8731
|
+
await persistMessage(normalizedRecord);
|
|
8440
8732
|
});
|
|
8441
8733
|
}
|
|
8442
8734
|
if (opts.raw) {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// src/lib/db-migrations.ts
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
var MIGRATIONS = [
|
|
4
|
+
{ version: "001", file: "001-initial-schema.sql" },
|
|
5
|
+
{ version: "002", file: "002-add-contacts.sql" },
|
|
6
|
+
{ version: "003", file: "003-backfill-contacts.sql" }
|
|
7
|
+
];
|
|
8
|
+
var CREATE_MIGRATIONS_TABLE_SQL = `
|
|
9
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
10
|
+
version TEXT NOT NULL PRIMARY KEY,
|
|
11
|
+
applied_at TEXT NOT NULL
|
|
12
|
+
)
|
|
13
|
+
`;
|
|
14
|
+
function nowIso() {
|
|
15
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
16
|
+
}
|
|
17
|
+
async function readMigrationSql(file) {
|
|
18
|
+
return await fs.readFile(new URL(`./migrations/${file}`, import.meta.url), "utf8");
|
|
19
|
+
}
|
|
20
|
+
function listAppliedVersions(db) {
|
|
21
|
+
const rows = db.prepare("SELECT version FROM schema_migrations").all();
|
|
22
|
+
return new Set(
|
|
23
|
+
rows.map((row) => typeof row.version === "string" ? row.version : "").filter(Boolean)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
async function runMigrations(db) {
|
|
27
|
+
db.exec(CREATE_MIGRATIONS_TABLE_SQL);
|
|
28
|
+
const applied = listAppliedVersions(db);
|
|
29
|
+
for (const migration of MIGRATIONS) {
|
|
30
|
+
if (applied.has(migration.version)) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const sql = await readMigrationSql(migration.file);
|
|
34
|
+
db.exec("BEGIN");
|
|
35
|
+
try {
|
|
36
|
+
db.exec(sql);
|
|
37
|
+
db.prepare(
|
|
38
|
+
"INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)"
|
|
39
|
+
).run(migration.version, nowIso());
|
|
40
|
+
db.exec("COMMIT");
|
|
41
|
+
} catch (error) {
|
|
42
|
+
try {
|
|
43
|
+
db.exec("ROLLBACK");
|
|
44
|
+
} catch {
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
runMigrations
|
|
52
|
+
};
|
package/dist/db-worker.js
CHANGED
|
@@ -1,150 +1,5 @@
|
|
|
1
1
|
// src/lib/db-worker.ts
|
|
2
2
|
import { parentPort, workerData } from "worker_threads";
|
|
3
|
-
var INIT_SQL = `
|
|
4
|
-
PRAGMA journal_mode = WAL;
|
|
5
|
-
PRAGMA synchronous = FULL;
|
|
6
|
-
PRAGMA busy_timeout = 5000;
|
|
7
|
-
PRAGMA foreign_keys = ON;
|
|
8
|
-
|
|
9
|
-
CREATE TABLE IF NOT EXISTS threads (
|
|
10
|
-
profile TEXT NOT NULL,
|
|
11
|
-
scope_thread_id TEXT NOT NULL,
|
|
12
|
-
raw_thread_id TEXT NOT NULL,
|
|
13
|
-
thread_type TEXT NOT NULL,
|
|
14
|
-
peer_id TEXT,
|
|
15
|
-
title TEXT,
|
|
16
|
-
is_pinned INTEGER NOT NULL DEFAULT 0,
|
|
17
|
-
is_hidden INTEGER NOT NULL DEFAULT 0,
|
|
18
|
-
is_archived INTEGER NOT NULL DEFAULT 0,
|
|
19
|
-
raw_json TEXT,
|
|
20
|
-
created_at TEXT NOT NULL,
|
|
21
|
-
updated_at TEXT NOT NULL,
|
|
22
|
-
PRIMARY KEY (profile, scope_thread_id)
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
CREATE TABLE IF NOT EXISTS thread_members (
|
|
26
|
-
profile TEXT NOT NULL,
|
|
27
|
-
scope_thread_id TEXT NOT NULL,
|
|
28
|
-
user_id TEXT NOT NULL,
|
|
29
|
-
display_name TEXT,
|
|
30
|
-
zalo_name TEXT,
|
|
31
|
-
avatar TEXT,
|
|
32
|
-
account_status INTEGER,
|
|
33
|
-
member_type INTEGER,
|
|
34
|
-
raw_json TEXT,
|
|
35
|
-
snapshot_at_ms INTEGER NOT NULL,
|
|
36
|
-
created_at TEXT NOT NULL,
|
|
37
|
-
updated_at TEXT NOT NULL,
|
|
38
|
-
PRIMARY KEY (profile, scope_thread_id, user_id)
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
CREATE TABLE IF NOT EXISTS friends (
|
|
42
|
-
profile TEXT NOT NULL,
|
|
43
|
-
user_id TEXT NOT NULL,
|
|
44
|
-
display_name TEXT,
|
|
45
|
-
zalo_name TEXT,
|
|
46
|
-
avatar TEXT,
|
|
47
|
-
account_status INTEGER,
|
|
48
|
-
raw_json TEXT,
|
|
49
|
-
created_at TEXT NOT NULL,
|
|
50
|
-
updated_at TEXT NOT NULL,
|
|
51
|
-
PRIMARY KEY (profile, user_id)
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
CREATE TABLE IF NOT EXISTS self_profiles (
|
|
55
|
-
profile TEXT NOT NULL,
|
|
56
|
-
user_id TEXT NOT NULL,
|
|
57
|
-
display_name TEXT,
|
|
58
|
-
info_json TEXT,
|
|
59
|
-
created_at TEXT NOT NULL,
|
|
60
|
-
updated_at TEXT NOT NULL,
|
|
61
|
-
PRIMARY KEY (profile)
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
65
|
-
profile TEXT NOT NULL,
|
|
66
|
-
message_uid TEXT NOT NULL,
|
|
67
|
-
scope_thread_id TEXT NOT NULL,
|
|
68
|
-
raw_thread_id TEXT NOT NULL,
|
|
69
|
-
thread_type TEXT NOT NULL,
|
|
70
|
-
msg_id TEXT,
|
|
71
|
-
cli_msg_id TEXT,
|
|
72
|
-
action_id TEXT,
|
|
73
|
-
sender_id TEXT,
|
|
74
|
-
sender_name TEXT,
|
|
75
|
-
to_id TEXT,
|
|
76
|
-
timestamp_ms INTEGER NOT NULL,
|
|
77
|
-
msg_type TEXT,
|
|
78
|
-
content_text TEXT,
|
|
79
|
-
content_json TEXT,
|
|
80
|
-
quote_msg_id TEXT,
|
|
81
|
-
quote_cli_msg_id TEXT,
|
|
82
|
-
quote_owner_id TEXT,
|
|
83
|
-
quote_text TEXT,
|
|
84
|
-
source TEXT NOT NULL,
|
|
85
|
-
raw_message_json TEXT,
|
|
86
|
-
raw_payload_json TEXT,
|
|
87
|
-
created_at TEXT NOT NULL,
|
|
88
|
-
updated_at TEXT NOT NULL,
|
|
89
|
-
PRIMARY KEY (profile, message_uid)
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
CREATE TABLE IF NOT EXISTS message_media (
|
|
93
|
-
profile TEXT NOT NULL,
|
|
94
|
-
message_uid TEXT NOT NULL,
|
|
95
|
-
item_index INTEGER NOT NULL,
|
|
96
|
-
media_kind TEXT,
|
|
97
|
-
media_url TEXT,
|
|
98
|
-
media_path TEXT,
|
|
99
|
-
media_type TEXT,
|
|
100
|
-
raw_json TEXT,
|
|
101
|
-
created_at TEXT NOT NULL,
|
|
102
|
-
updated_at TEXT NOT NULL,
|
|
103
|
-
PRIMARY KEY (profile, message_uid, item_index)
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
CREATE TABLE IF NOT EXISTS message_mentions (
|
|
107
|
-
profile TEXT NOT NULL,
|
|
108
|
-
message_uid TEXT NOT NULL,
|
|
109
|
-
item_index INTEGER NOT NULL,
|
|
110
|
-
target_user_id TEXT NOT NULL,
|
|
111
|
-
pos INTEGER,
|
|
112
|
-
len INTEGER,
|
|
113
|
-
mention_type INTEGER,
|
|
114
|
-
raw_json TEXT,
|
|
115
|
-
created_at TEXT NOT NULL,
|
|
116
|
-
updated_at TEXT NOT NULL,
|
|
117
|
-
PRIMARY KEY (profile, message_uid, item_index)
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
CREATE TABLE IF NOT EXISTS sync_state (
|
|
121
|
-
profile TEXT NOT NULL,
|
|
122
|
-
scope TEXT NOT NULL,
|
|
123
|
-
scope_thread_id TEXT NOT NULL,
|
|
124
|
-
thread_type TEXT NOT NULL,
|
|
125
|
-
status TEXT NOT NULL,
|
|
126
|
-
completeness TEXT,
|
|
127
|
-
cursor TEXT,
|
|
128
|
-
last_sync_at TEXT,
|
|
129
|
-
error TEXT,
|
|
130
|
-
created_at TEXT NOT NULL,
|
|
131
|
-
updated_at TEXT NOT NULL,
|
|
132
|
-
PRIMARY KEY (profile, scope)
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
CREATE INDEX IF NOT EXISTS idx_messages_thread_time
|
|
136
|
-
ON messages (profile, scope_thread_id, timestamp_ms DESC);
|
|
137
|
-
CREATE INDEX IF NOT EXISTS idx_messages_msg_id
|
|
138
|
-
ON messages (profile, msg_id);
|
|
139
|
-
CREATE INDEX IF NOT EXISTS idx_messages_cli_msg_id
|
|
140
|
-
ON messages (profile, cli_msg_id);
|
|
141
|
-
CREATE INDEX IF NOT EXISTS idx_threads_type
|
|
142
|
-
ON threads (profile, thread_type, updated_at DESC);
|
|
143
|
-
CREATE INDEX IF NOT EXISTS idx_members_thread
|
|
144
|
-
ON thread_members (profile, scope_thread_id);
|
|
145
|
-
CREATE INDEX IF NOT EXISTS idx_friends_name
|
|
146
|
-
ON friends (profile, display_name, zalo_name, user_id);
|
|
147
|
-
`;
|
|
148
3
|
if (!parentPort) {
|
|
149
4
|
throw new Error("DB worker requires parentPort");
|
|
150
5
|
}
|
|
@@ -180,6 +35,12 @@ async function loadSqliteModule() {
|
|
|
180
35
|
process.emitWarning = originalEmitWarning;
|
|
181
36
|
}
|
|
182
37
|
}
|
|
38
|
+
async function loadMigrationModule() {
|
|
39
|
+
const currentUrl = new URL(import.meta.url);
|
|
40
|
+
const specifier = currentUrl.pathname.endsWith("/src/lib/db-worker.ts") ? new URL("./db-migrations.ts", currentUrl).href : new URL("./db-migrations.js", currentUrl).href;
|
|
41
|
+
const importDynamic = new Function("specifier", "return import(specifier);");
|
|
42
|
+
return await importDynamic(specifier);
|
|
43
|
+
}
|
|
183
44
|
function setDefensiveMode(db) {
|
|
184
45
|
const maybeDb = db;
|
|
185
46
|
if (typeof maybeDb.enableDefensive === "function") {
|
|
@@ -197,9 +58,16 @@ function allStatement(db, statement) {
|
|
|
197
58
|
}
|
|
198
59
|
async function main() {
|
|
199
60
|
const { DatabaseSync } = await loadSqliteModule();
|
|
61
|
+
const { runMigrations } = await loadMigrationModule();
|
|
200
62
|
const { filename } = workerData;
|
|
201
63
|
const db = new DatabaseSync(filename);
|
|
202
|
-
db.exec(
|
|
64
|
+
db.exec(`
|
|
65
|
+
PRAGMA journal_mode = WAL;
|
|
66
|
+
PRAGMA synchronous = FULL;
|
|
67
|
+
PRAGMA busy_timeout = 5000;
|
|
68
|
+
PRAGMA foreign_keys = ON;
|
|
69
|
+
`);
|
|
70
|
+
await runMigrations(db);
|
|
203
71
|
setDefensiveMode(db);
|
|
204
72
|
port.postMessage({ type: "ready" });
|
|
205
73
|
port.on("message", (message) => {
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS threads (
|
|
2
|
+
profile TEXT NOT NULL,
|
|
3
|
+
scope_thread_id TEXT NOT NULL,
|
|
4
|
+
raw_thread_id TEXT NOT NULL,
|
|
5
|
+
thread_type TEXT NOT NULL,
|
|
6
|
+
peer_id TEXT,
|
|
7
|
+
title TEXT,
|
|
8
|
+
is_pinned INTEGER NOT NULL DEFAULT 0,
|
|
9
|
+
is_hidden INTEGER NOT NULL DEFAULT 0,
|
|
10
|
+
is_archived INTEGER NOT NULL DEFAULT 0,
|
|
11
|
+
raw_json TEXT,
|
|
12
|
+
created_at TEXT NOT NULL,
|
|
13
|
+
updated_at TEXT NOT NULL,
|
|
14
|
+
PRIMARY KEY (profile, scope_thread_id)
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE TABLE IF NOT EXISTS thread_members (
|
|
18
|
+
profile TEXT NOT NULL,
|
|
19
|
+
scope_thread_id TEXT NOT NULL,
|
|
20
|
+
user_id TEXT NOT NULL,
|
|
21
|
+
display_name TEXT,
|
|
22
|
+
zalo_name TEXT,
|
|
23
|
+
avatar TEXT,
|
|
24
|
+
account_status INTEGER,
|
|
25
|
+
member_type INTEGER,
|
|
26
|
+
raw_json TEXT,
|
|
27
|
+
snapshot_at_ms INTEGER NOT NULL,
|
|
28
|
+
created_at TEXT NOT NULL,
|
|
29
|
+
updated_at TEXT NOT NULL,
|
|
30
|
+
PRIMARY KEY (profile, scope_thread_id, user_id)
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE TABLE IF NOT EXISTS friends (
|
|
34
|
+
profile TEXT NOT NULL,
|
|
35
|
+
user_id TEXT NOT NULL,
|
|
36
|
+
display_name TEXT,
|
|
37
|
+
zalo_name TEXT,
|
|
38
|
+
avatar TEXT,
|
|
39
|
+
account_status INTEGER,
|
|
40
|
+
raw_json TEXT,
|
|
41
|
+
created_at TEXT NOT NULL,
|
|
42
|
+
updated_at TEXT NOT NULL,
|
|
43
|
+
PRIMARY KEY (profile, user_id)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
CREATE TABLE IF NOT EXISTS self_profiles (
|
|
47
|
+
profile TEXT NOT NULL,
|
|
48
|
+
user_id TEXT NOT NULL,
|
|
49
|
+
display_name TEXT,
|
|
50
|
+
info_json TEXT,
|
|
51
|
+
created_at TEXT NOT NULL,
|
|
52
|
+
updated_at TEXT NOT NULL,
|
|
53
|
+
PRIMARY KEY (profile)
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
57
|
+
profile TEXT NOT NULL,
|
|
58
|
+
message_uid TEXT NOT NULL,
|
|
59
|
+
scope_thread_id TEXT NOT NULL,
|
|
60
|
+
raw_thread_id TEXT NOT NULL,
|
|
61
|
+
thread_type TEXT NOT NULL,
|
|
62
|
+
msg_id TEXT,
|
|
63
|
+
cli_msg_id TEXT,
|
|
64
|
+
action_id TEXT,
|
|
65
|
+
sender_id TEXT,
|
|
66
|
+
sender_name TEXT,
|
|
67
|
+
to_id TEXT,
|
|
68
|
+
timestamp_ms INTEGER NOT NULL,
|
|
69
|
+
msg_type TEXT,
|
|
70
|
+
content_text TEXT,
|
|
71
|
+
content_json TEXT,
|
|
72
|
+
quote_msg_id TEXT,
|
|
73
|
+
quote_cli_msg_id TEXT,
|
|
74
|
+
quote_owner_id TEXT,
|
|
75
|
+
quote_text TEXT,
|
|
76
|
+
source TEXT NOT NULL,
|
|
77
|
+
raw_message_json TEXT,
|
|
78
|
+
raw_payload_json TEXT,
|
|
79
|
+
created_at TEXT NOT NULL,
|
|
80
|
+
updated_at TEXT NOT NULL,
|
|
81
|
+
PRIMARY KEY (profile, message_uid)
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
CREATE TABLE IF NOT EXISTS message_media (
|
|
85
|
+
profile TEXT NOT NULL,
|
|
86
|
+
message_uid TEXT NOT NULL,
|
|
87
|
+
item_index INTEGER NOT NULL,
|
|
88
|
+
media_kind TEXT,
|
|
89
|
+
media_url TEXT,
|
|
90
|
+
media_path TEXT,
|
|
91
|
+
media_type TEXT,
|
|
92
|
+
raw_json TEXT,
|
|
93
|
+
created_at TEXT NOT NULL,
|
|
94
|
+
updated_at TEXT NOT NULL,
|
|
95
|
+
PRIMARY KEY (profile, message_uid, item_index)
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
CREATE TABLE IF NOT EXISTS message_mentions (
|
|
99
|
+
profile TEXT NOT NULL,
|
|
100
|
+
message_uid TEXT NOT NULL,
|
|
101
|
+
item_index INTEGER NOT NULL,
|
|
102
|
+
target_user_id TEXT NOT NULL,
|
|
103
|
+
pos INTEGER,
|
|
104
|
+
len INTEGER,
|
|
105
|
+
mention_type INTEGER,
|
|
106
|
+
raw_json TEXT,
|
|
107
|
+
created_at TEXT NOT NULL,
|
|
108
|
+
updated_at TEXT NOT NULL,
|
|
109
|
+
PRIMARY KEY (profile, message_uid, item_index)
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
CREATE TABLE IF NOT EXISTS sync_state (
|
|
113
|
+
profile TEXT NOT NULL,
|
|
114
|
+
scope TEXT NOT NULL,
|
|
115
|
+
scope_thread_id TEXT NOT NULL,
|
|
116
|
+
thread_type TEXT NOT NULL,
|
|
117
|
+
status TEXT NOT NULL,
|
|
118
|
+
completeness TEXT,
|
|
119
|
+
cursor TEXT,
|
|
120
|
+
last_sync_at TEXT,
|
|
121
|
+
error TEXT,
|
|
122
|
+
created_at TEXT NOT NULL,
|
|
123
|
+
updated_at TEXT NOT NULL,
|
|
124
|
+
PRIMARY KEY (profile, scope)
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
CREATE INDEX IF NOT EXISTS idx_messages_thread_time
|
|
128
|
+
ON messages (profile, scope_thread_id, timestamp_ms DESC);
|
|
129
|
+
CREATE INDEX IF NOT EXISTS idx_messages_msg_id
|
|
130
|
+
ON messages (profile, msg_id);
|
|
131
|
+
CREATE INDEX IF NOT EXISTS idx_messages_cli_msg_id
|
|
132
|
+
ON messages (profile, cli_msg_id);
|
|
133
|
+
CREATE INDEX IF NOT EXISTS idx_threads_type
|
|
134
|
+
ON threads (profile, thread_type, updated_at DESC);
|
|
135
|
+
CREATE INDEX IF NOT EXISTS idx_members_thread
|
|
136
|
+
ON thread_members (profile, scope_thread_id);
|
|
137
|
+
CREATE INDEX IF NOT EXISTS idx_friends_name
|
|
138
|
+
ON friends (profile, display_name, zalo_name, user_id);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS contacts (
|
|
2
|
+
profile TEXT NOT NULL,
|
|
3
|
+
user_id TEXT NOT NULL,
|
|
4
|
+
display_name TEXT,
|
|
5
|
+
zalo_name TEXT,
|
|
6
|
+
avatar TEXT,
|
|
7
|
+
account_status INTEGER,
|
|
8
|
+
relationship TEXT NOT NULL DEFAULT 'unknown',
|
|
9
|
+
first_seen_at_ms INTEGER,
|
|
10
|
+
last_seen_at_ms INTEGER,
|
|
11
|
+
raw_json TEXT,
|
|
12
|
+
created_at TEXT NOT NULL,
|
|
13
|
+
updated_at TEXT NOT NULL,
|
|
14
|
+
PRIMARY KEY (profile, user_id)
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE INDEX IF NOT EXISTS idx_contacts_name
|
|
18
|
+
ON contacts (profile, display_name, zalo_name, user_id);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
INSERT INTO contacts (
|
|
2
|
+
profile,
|
|
3
|
+
user_id,
|
|
4
|
+
display_name,
|
|
5
|
+
zalo_name,
|
|
6
|
+
avatar,
|
|
7
|
+
account_status,
|
|
8
|
+
relationship,
|
|
9
|
+
first_seen_at_ms,
|
|
10
|
+
last_seen_at_ms,
|
|
11
|
+
raw_json,
|
|
12
|
+
created_at,
|
|
13
|
+
updated_at
|
|
14
|
+
)
|
|
15
|
+
SELECT
|
|
16
|
+
profile,
|
|
17
|
+
user_id,
|
|
18
|
+
display_name,
|
|
19
|
+
zalo_name,
|
|
20
|
+
avatar,
|
|
21
|
+
account_status,
|
|
22
|
+
'friend',
|
|
23
|
+
NULL,
|
|
24
|
+
NULL,
|
|
25
|
+
raw_json,
|
|
26
|
+
created_at,
|
|
27
|
+
updated_at
|
|
28
|
+
FROM friends
|
|
29
|
+
WHERE 1 = 1
|
|
30
|
+
ON CONFLICT(profile, user_id) DO UPDATE SET
|
|
31
|
+
display_name = COALESCE(excluded.display_name, contacts.display_name),
|
|
32
|
+
zalo_name = COALESCE(excluded.zalo_name, contacts.zalo_name),
|
|
33
|
+
avatar = COALESCE(excluded.avatar, contacts.avatar),
|
|
34
|
+
account_status = COALESCE(excluded.account_status, contacts.account_status),
|
|
35
|
+
relationship = CASE
|
|
36
|
+
WHEN contacts.relationship = 'friend' OR excluded.relationship = 'friend' THEN 'friend'
|
|
37
|
+
WHEN contacts.relationship = 'seen_dm' OR excluded.relationship = 'seen_dm' THEN 'seen_dm'
|
|
38
|
+
WHEN contacts.relationship = 'seen_group' OR excluded.relationship = 'seen_group' THEN 'seen_group'
|
|
39
|
+
ELSE COALESCE(excluded.relationship, contacts.relationship, 'unknown')
|
|
40
|
+
END,
|
|
41
|
+
raw_json = COALESCE(excluded.raw_json, contacts.raw_json),
|
|
42
|
+
updated_at = excluded.updated_at;
|
|
43
|
+
|
|
44
|
+
INSERT INTO contacts (
|
|
45
|
+
profile,
|
|
46
|
+
user_id,
|
|
47
|
+
display_name,
|
|
48
|
+
zalo_name,
|
|
49
|
+
avatar,
|
|
50
|
+
account_status,
|
|
51
|
+
relationship,
|
|
52
|
+
first_seen_at_ms,
|
|
53
|
+
last_seen_at_ms,
|
|
54
|
+
raw_json,
|
|
55
|
+
created_at,
|
|
56
|
+
updated_at
|
|
57
|
+
)
|
|
58
|
+
SELECT
|
|
59
|
+
profile,
|
|
60
|
+
user_id,
|
|
61
|
+
NULLIF(MAX(display_name), ''),
|
|
62
|
+
NULLIF(MAX(zalo_name), ''),
|
|
63
|
+
NULLIF(MAX(avatar), ''),
|
|
64
|
+
MAX(account_status),
|
|
65
|
+
'seen_group',
|
|
66
|
+
MIN(snapshot_at_ms),
|
|
67
|
+
MAX(snapshot_at_ms),
|
|
68
|
+
NULLIF(MAX(raw_json), ''),
|
|
69
|
+
MIN(created_at),
|
|
70
|
+
MAX(updated_at)
|
|
71
|
+
FROM thread_members
|
|
72
|
+
WHERE 1 = 1
|
|
73
|
+
GROUP BY profile, user_id
|
|
74
|
+
ON CONFLICT(profile, user_id) DO UPDATE SET
|
|
75
|
+
display_name = COALESCE(excluded.display_name, contacts.display_name),
|
|
76
|
+
zalo_name = COALESCE(excluded.zalo_name, contacts.zalo_name),
|
|
77
|
+
avatar = COALESCE(excluded.avatar, contacts.avatar),
|
|
78
|
+
account_status = COALESCE(excluded.account_status, contacts.account_status),
|
|
79
|
+
relationship = CASE
|
|
80
|
+
WHEN contacts.relationship = 'friend' OR excluded.relationship = 'friend' THEN 'friend'
|
|
81
|
+
WHEN contacts.relationship = 'seen_dm' OR excluded.relationship = 'seen_dm' THEN 'seen_dm'
|
|
82
|
+
WHEN contacts.relationship = 'seen_group' OR excluded.relationship = 'seen_group' THEN 'seen_group'
|
|
83
|
+
ELSE COALESCE(excluded.relationship, contacts.relationship, 'unknown')
|
|
84
|
+
END,
|
|
85
|
+
first_seen_at_ms = CASE
|
|
86
|
+
WHEN contacts.first_seen_at_ms IS NULL THEN excluded.first_seen_at_ms
|
|
87
|
+
WHEN excluded.first_seen_at_ms IS NULL THEN contacts.first_seen_at_ms
|
|
88
|
+
ELSE MIN(contacts.first_seen_at_ms, excluded.first_seen_at_ms)
|
|
89
|
+
END,
|
|
90
|
+
last_seen_at_ms = CASE
|
|
91
|
+
WHEN contacts.last_seen_at_ms IS NULL THEN excluded.last_seen_at_ms
|
|
92
|
+
WHEN excluded.last_seen_at_ms IS NULL THEN contacts.last_seen_at_ms
|
|
93
|
+
ELSE MAX(contacts.last_seen_at_ms, excluded.last_seen_at_ms)
|
|
94
|
+
END,
|
|
95
|
+
raw_json = COALESCE(excluded.raw_json, contacts.raw_json),
|
|
96
|
+
updated_at = excluded.updated_at;
|
|
97
|
+
|
|
98
|
+
INSERT INTO contacts (
|
|
99
|
+
profile,
|
|
100
|
+
user_id,
|
|
101
|
+
display_name,
|
|
102
|
+
zalo_name,
|
|
103
|
+
avatar,
|
|
104
|
+
account_status,
|
|
105
|
+
relationship,
|
|
106
|
+
first_seen_at_ms,
|
|
107
|
+
last_seen_at_ms,
|
|
108
|
+
raw_json,
|
|
109
|
+
created_at,
|
|
110
|
+
updated_at
|
|
111
|
+
)
|
|
112
|
+
SELECT
|
|
113
|
+
t.profile,
|
|
114
|
+
COALESCE(NULLIF(t.peer_id, ''), t.scope_thread_id) AS user_id,
|
|
115
|
+
COALESCE(
|
|
116
|
+
NULLIF(MAX(CASE
|
|
117
|
+
WHEN m.sender_id = COALESCE(NULLIF(t.peer_id, ''), t.scope_thread_id) THEN m.sender_name
|
|
118
|
+
ELSE NULL
|
|
119
|
+
END), ''),
|
|
120
|
+
NULLIF(MAX(t.title), '')
|
|
121
|
+
) AS display_name,
|
|
122
|
+
NULL,
|
|
123
|
+
NULL,
|
|
124
|
+
NULL,
|
|
125
|
+
'seen_dm',
|
|
126
|
+
MIN(m.timestamp_ms),
|
|
127
|
+
MAX(m.timestamp_ms),
|
|
128
|
+
t.raw_json,
|
|
129
|
+
MIN(t.created_at),
|
|
130
|
+
MAX(COALESCE(m.updated_at, t.updated_at))
|
|
131
|
+
FROM threads t
|
|
132
|
+
LEFT JOIN messages m
|
|
133
|
+
ON m.profile = t.profile
|
|
134
|
+
AND m.scope_thread_id = t.scope_thread_id
|
|
135
|
+
AND m.thread_type = 'user'
|
|
136
|
+
WHERE t.thread_type = 'user'
|
|
137
|
+
AND COALESCE(NULLIF(t.peer_id, ''), t.scope_thread_id) <> ''
|
|
138
|
+
GROUP BY t.profile, COALESCE(NULLIF(t.peer_id, ''), t.scope_thread_id), t.raw_json
|
|
139
|
+
ON CONFLICT(profile, user_id) DO UPDATE SET
|
|
140
|
+
display_name = COALESCE(excluded.display_name, contacts.display_name),
|
|
141
|
+
relationship = CASE
|
|
142
|
+
WHEN contacts.relationship = 'friend' OR excluded.relationship = 'friend' THEN 'friend'
|
|
143
|
+
WHEN contacts.relationship = 'seen_dm' OR excluded.relationship = 'seen_dm' THEN 'seen_dm'
|
|
144
|
+
WHEN contacts.relationship = 'seen_group' OR excluded.relationship = 'seen_group' THEN 'seen_group'
|
|
145
|
+
ELSE COALESCE(excluded.relationship, contacts.relationship, 'unknown')
|
|
146
|
+
END,
|
|
147
|
+
first_seen_at_ms = CASE
|
|
148
|
+
WHEN contacts.first_seen_at_ms IS NULL THEN excluded.first_seen_at_ms
|
|
149
|
+
WHEN excluded.first_seen_at_ms IS NULL THEN contacts.first_seen_at_ms
|
|
150
|
+
ELSE MIN(contacts.first_seen_at_ms, excluded.first_seen_at_ms)
|
|
151
|
+
END,
|
|
152
|
+
last_seen_at_ms = CASE
|
|
153
|
+
WHEN contacts.last_seen_at_ms IS NULL THEN excluded.last_seen_at_ms
|
|
154
|
+
WHEN excluded.last_seen_at_ms IS NULL THEN contacts.last_seen_at_ms
|
|
155
|
+
ELSE MAX(contacts.last_seen_at_ms, excluded.last_seen_at_ms)
|
|
156
|
+
END,
|
|
157
|
+
raw_json = COALESCE(excluded.raw_json, contacts.raw_json),
|
|
158
|
+
updated_at = excluded.updated_at;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openzca",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.54",
|
|
4
4
|
"description": "Open-source zca-compatible CLI to integrate Zalo with OpenClaw",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "npm run build:cli && npm run build:worker",
|
|
18
18
|
"build:cli": "tsup src/cli.ts --format esm --target node22 --out-dir dist --clean",
|
|
19
|
-
"build:worker": "tsup src/lib/db-worker.ts --format esm --target node22 --out-dir dist",
|
|
19
|
+
"build:worker": "tsup src/lib/db-worker.ts src/lib/db-migrations.ts --format esm --target node22 --out-dir dist && node scripts/copy-db-migrations.mjs",
|
|
20
20
|
"dev": "tsx src/cli.ts",
|
|
21
21
|
"test": "tsx --test tests/*.test.ts",
|
|
22
22
|
"typecheck": "tsc -p tsconfig.json",
|