openzca 0.1.52 → 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/README.md +5 -0
- package/dist/cli.js +755 -179
- 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
|
}
|
|
@@ -2333,6 +2465,8 @@ function normalizeCodeBlockLeadingWhitespace(line) {
|
|
|
2333
2465
|
}
|
|
2334
2466
|
|
|
2335
2467
|
// src/lib/text-send.ts
|
|
2468
|
+
var ZALO_TEXT_MESSAGE_MAX_LENGTH = 2e3;
|
|
2469
|
+
var ZALO_TEXT_REQUEST_PARAMS_MAX_ESTIMATE = 4e3;
|
|
2336
2470
|
async function buildTextSendPayload(params) {
|
|
2337
2471
|
if (params.raw) {
|
|
2338
2472
|
const mentions2 = await resolveGroupMentionsIfNeeded(params, params.message);
|
|
@@ -2368,15 +2502,103 @@ async function analyzeTextSendPayload(params) {
|
|
|
2368
2502
|
});
|
|
2369
2503
|
return {
|
|
2370
2504
|
payload,
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2505
|
+
...buildTextSendPayloadAnalysis({
|
|
2506
|
+
payloadObject,
|
|
2507
|
+
rawInputLength: params.message.length,
|
|
2508
|
+
textProperties,
|
|
2509
|
+
mentionInfo,
|
|
2510
|
+
requestParamsLengthEstimate: JSON.stringify(requestParams).length,
|
|
2511
|
+
threadType: params.threadType
|
|
2512
|
+
})
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
function planTextSendPayloadsForDelivery(params) {
|
|
2516
|
+
const maxMessageLength = resolvePositiveLimit(
|
|
2517
|
+
params.maxMessageLength,
|
|
2518
|
+
ZALO_TEXT_MESSAGE_MAX_LENGTH
|
|
2519
|
+
);
|
|
2520
|
+
const maxRequestParamsLengthEstimate = resolvePositiveLimit(
|
|
2521
|
+
params.maxRequestParamsLengthEstimate,
|
|
2522
|
+
ZALO_TEXT_REQUEST_PARAMS_MAX_ESTIMATE
|
|
2523
|
+
);
|
|
2524
|
+
const chunks = [];
|
|
2525
|
+
const analyses = [];
|
|
2526
|
+
const pending = [params.payload];
|
|
2527
|
+
while (pending.length > 0) {
|
|
2528
|
+
const currentPayload = pending.shift();
|
|
2529
|
+
const analysis = analyzePreparedTextSendPayload({
|
|
2530
|
+
payload: currentPayload,
|
|
2531
|
+
threadType: params.threadType,
|
|
2532
|
+
threadId: params.threadId
|
|
2533
|
+
});
|
|
2534
|
+
if (isTextSendPayloadWithinDeliveryLimits(analysis, {
|
|
2535
|
+
maxMessageLength,
|
|
2536
|
+
maxRequestParamsLengthEstimate
|
|
2537
|
+
})) {
|
|
2538
|
+
chunks.push(currentPayload);
|
|
2539
|
+
analyses.push(analysis);
|
|
2540
|
+
continue;
|
|
2541
|
+
}
|
|
2542
|
+
const targetLength = computeNextChunkLength(analysis, {
|
|
2543
|
+
maxMessageLength,
|
|
2544
|
+
maxRequestParamsLengthEstimate
|
|
2545
|
+
});
|
|
2546
|
+
const splitChunks = splitTextSendPayload(currentPayload, targetLength);
|
|
2547
|
+
if (splitChunks.length <= 1) {
|
|
2548
|
+
throw new Error(
|
|
2549
|
+
`Unable to split formatted text payload into deliverable chunks within ${targetLength} characters.`
|
|
2550
|
+
);
|
|
2551
|
+
}
|
|
2552
|
+
pending.unshift(...splitChunks);
|
|
2553
|
+
}
|
|
2554
|
+
return { chunks, analyses };
|
|
2555
|
+
}
|
|
2556
|
+
function splitTextSendPayload(payload, maxLength = ZALO_TEXT_MESSAGE_MAX_LENGTH) {
|
|
2557
|
+
if (!Number.isInteger(maxLength) || maxLength <= 0) {
|
|
2558
|
+
throw new Error("Text chunk size must be a positive integer");
|
|
2559
|
+
}
|
|
2560
|
+
const payloadObject = normalizeTextSendPayload(payload);
|
|
2561
|
+
if (payloadObject.msg.length <= maxLength) {
|
|
2562
|
+
return [payload];
|
|
2563
|
+
}
|
|
2564
|
+
const chunks = [];
|
|
2565
|
+
let start = 0;
|
|
2566
|
+
while (start < payloadObject.msg.length) {
|
|
2567
|
+
const end = findChunkEnd(payloadObject, start, maxLength);
|
|
2568
|
+
chunks.push(sliceTextSendPayload(payloadObject, start, end));
|
|
2569
|
+
start = end;
|
|
2570
|
+
}
|
|
2571
|
+
return chunks;
|
|
2572
|
+
}
|
|
2573
|
+
function analyzePreparedTextSendPayload(params) {
|
|
2574
|
+
const payloadObject = normalizeTextSendPayload(params.payload);
|
|
2575
|
+
const textProperties = buildTextProperties(payloadObject.styles);
|
|
2576
|
+
const mentionInfo = buildMentionInfo(
|
|
2577
|
+
params.threadType,
|
|
2578
|
+
payloadObject.msg,
|
|
2579
|
+
payloadObject.mentions
|
|
2580
|
+
);
|
|
2581
|
+
const requestParams = omitUndefined({
|
|
2582
|
+
message: payloadObject.msg,
|
|
2583
|
+
clientId: 17e11,
|
|
2584
|
+
mentionInfo,
|
|
2585
|
+
imei: params.threadType === ThreadType.Group ? void 0 : "000000000000000",
|
|
2586
|
+
ttl: 0,
|
|
2587
|
+
visibility: params.threadType === ThreadType.Group ? 0 : void 0,
|
|
2588
|
+
toid: params.threadType === ThreadType.Group ? void 0 : params.threadId,
|
|
2589
|
+
grid: params.threadType === ThreadType.Group ? params.threadId : void 0,
|
|
2590
|
+
textProperties
|
|
2591
|
+
});
|
|
2592
|
+
return {
|
|
2593
|
+
payload: params.payload,
|
|
2594
|
+
...buildTextSendPayloadAnalysis({
|
|
2595
|
+
payloadObject,
|
|
2596
|
+
rawInputLength: payloadObject.msg.length,
|
|
2597
|
+
textProperties,
|
|
2598
|
+
mentionInfo,
|
|
2599
|
+
requestParamsLengthEstimate: JSON.stringify(requestParams).length,
|
|
2600
|
+
threadType: params.threadType
|
|
2601
|
+
})
|
|
2380
2602
|
};
|
|
2381
2603
|
}
|
|
2382
2604
|
async function resolveGroupMentionsIfNeeded(params, text) {
|
|
@@ -2399,6 +2621,153 @@ function normalizeTextSendPayload(payload) {
|
|
|
2399
2621
|
}
|
|
2400
2622
|
return payload;
|
|
2401
2623
|
}
|
|
2624
|
+
function buildTextSendPayloadAnalysis(params) {
|
|
2625
|
+
return {
|
|
2626
|
+
payloadObject: params.payloadObject,
|
|
2627
|
+
rawInputLength: params.rawInputLength,
|
|
2628
|
+
renderedTextLength: params.payloadObject.msg.length,
|
|
2629
|
+
styleCount: params.payloadObject.styles?.length ?? 0,
|
|
2630
|
+
mentionCount: params.payloadObject.mentions?.length ?? 0,
|
|
2631
|
+
textPropertiesLength: params.textProperties?.length ?? 0,
|
|
2632
|
+
mentionInfoLength: params.mentionInfo?.length ?? 0,
|
|
2633
|
+
requestParamsLengthEstimate: params.requestParamsLengthEstimate,
|
|
2634
|
+
sendPath: params.threadType === ThreadType.Group ? params.mentionInfo ? "mention" : "sendmsg" : "sms"
|
|
2635
|
+
};
|
|
2636
|
+
}
|
|
2637
|
+
function isTextSendPayloadWithinDeliveryLimits(analysis, limits) {
|
|
2638
|
+
return analysis.renderedTextLength <= limits.maxMessageLength && analysis.requestParamsLengthEstimate <= limits.maxRequestParamsLengthEstimate;
|
|
2639
|
+
}
|
|
2640
|
+
function computeNextChunkLength(analysis, limits) {
|
|
2641
|
+
const currentLength = analysis.renderedTextLength;
|
|
2642
|
+
const targetLengths = [limits.maxMessageLength, currentLength - 1].filter((value) => value > 0);
|
|
2643
|
+
if (analysis.requestParamsLengthEstimate > limits.maxRequestParamsLengthEstimate) {
|
|
2644
|
+
targetLengths.push(
|
|
2645
|
+
Math.floor(
|
|
2646
|
+
currentLength * limits.maxRequestParamsLengthEstimate / analysis.requestParamsLengthEstimate
|
|
2647
|
+
)
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2650
|
+
const targetLength = Math.max(
|
|
2651
|
+
1,
|
|
2652
|
+
Math.min(...targetLengths.filter((value) => Number.isFinite(value) && value > 0))
|
|
2653
|
+
);
|
|
2654
|
+
return Math.min(targetLength, currentLength - 1);
|
|
2655
|
+
}
|
|
2656
|
+
function resolvePositiveLimit(value, fallback) {
|
|
2657
|
+
if (!value || !Number.isInteger(value) || value <= 0) {
|
|
2658
|
+
return fallback;
|
|
2659
|
+
}
|
|
2660
|
+
return value;
|
|
2661
|
+
}
|
|
2662
|
+
function sliceTextSendPayload(payloadObject, start, end) {
|
|
2663
|
+
const msg2 = payloadObject.msg.slice(start, end);
|
|
2664
|
+
const styles = sliceStyles(payloadObject.styles, start, end);
|
|
2665
|
+
const mentions = sliceMentions(payloadObject.mentions, start, end);
|
|
2666
|
+
if (!styles && !mentions) {
|
|
2667
|
+
return msg2;
|
|
2668
|
+
}
|
|
2669
|
+
return omitUndefined({
|
|
2670
|
+
msg: msg2,
|
|
2671
|
+
styles,
|
|
2672
|
+
mentions
|
|
2673
|
+
});
|
|
2674
|
+
}
|
|
2675
|
+
function sliceStyles(styles, start, end) {
|
|
2676
|
+
if (!styles || styles.length === 0) {
|
|
2677
|
+
return void 0;
|
|
2678
|
+
}
|
|
2679
|
+
const sliced = [];
|
|
2680
|
+
for (const style of styles) {
|
|
2681
|
+
const styleStart = style.start;
|
|
2682
|
+
const styleEnd = style.start + style.len;
|
|
2683
|
+
const overlapStart = Math.max(styleStart, start);
|
|
2684
|
+
const overlapEnd = Math.min(styleEnd, end);
|
|
2685
|
+
if (overlapStart >= overlapEnd) {
|
|
2686
|
+
continue;
|
|
2687
|
+
}
|
|
2688
|
+
if (style.st === "ind_$") {
|
|
2689
|
+
sliced.push({
|
|
2690
|
+
start: overlapStart - start,
|
|
2691
|
+
len: overlapEnd - overlapStart,
|
|
2692
|
+
st: style.st,
|
|
2693
|
+
indentSize: style.indentSize
|
|
2694
|
+
});
|
|
2695
|
+
continue;
|
|
2696
|
+
}
|
|
2697
|
+
sliced.push({
|
|
2698
|
+
start: overlapStart - start,
|
|
2699
|
+
len: overlapEnd - overlapStart,
|
|
2700
|
+
st: style.st
|
|
2701
|
+
});
|
|
2702
|
+
}
|
|
2703
|
+
return sliced.length > 0 ? sliced : void 0;
|
|
2704
|
+
}
|
|
2705
|
+
function sliceMentions(mentions, start, end) {
|
|
2706
|
+
if (!mentions || mentions.length === 0) {
|
|
2707
|
+
return void 0;
|
|
2708
|
+
}
|
|
2709
|
+
const sliced = mentions.filter((mention) => mention.pos >= start && mention.pos + mention.len <= end).map((mention) => ({
|
|
2710
|
+
pos: mention.pos - start,
|
|
2711
|
+
uid: mention.uid,
|
|
2712
|
+
len: mention.len
|
|
2713
|
+
}));
|
|
2714
|
+
return sliced.length > 0 ? sliced : void 0;
|
|
2715
|
+
}
|
|
2716
|
+
function findChunkEnd(payloadObject, start, maxLength) {
|
|
2717
|
+
const remaining = payloadObject.msg.length - start;
|
|
2718
|
+
if (remaining <= maxLength) {
|
|
2719
|
+
return payloadObject.msg.length;
|
|
2720
|
+
}
|
|
2721
|
+
const maxEnd = start + maxLength;
|
|
2722
|
+
const newlineBreak = findPreferredBreak(payloadObject, start, maxEnd, "\n");
|
|
2723
|
+
if (newlineBreak > start) {
|
|
2724
|
+
return newlineBreak;
|
|
2725
|
+
}
|
|
2726
|
+
const whitespaceBreak = findWhitespaceBreak(payloadObject, start, maxEnd);
|
|
2727
|
+
if (whitespaceBreak > start) {
|
|
2728
|
+
return whitespaceBreak;
|
|
2729
|
+
}
|
|
2730
|
+
for (let cursor = maxEnd; cursor > start; cursor -= 1) {
|
|
2731
|
+
if (isSafeSplitPosition(payloadObject.mentions, cursor)) {
|
|
2732
|
+
return cursor;
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
throw new Error(
|
|
2736
|
+
`Unable to split text payload safely within ${maxLength} characters.`
|
|
2737
|
+
);
|
|
2738
|
+
}
|
|
2739
|
+
function findPreferredBreak(payloadObject, start, maxEnd, marker) {
|
|
2740
|
+
for (let cursor = maxEnd; cursor > start; cursor -= 1) {
|
|
2741
|
+
if (!isSafeSplitPosition(payloadObject.mentions, cursor)) {
|
|
2742
|
+
continue;
|
|
2743
|
+
}
|
|
2744
|
+
if (payloadObject.msg[cursor - 1] === marker) {
|
|
2745
|
+
return cursor;
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
return start;
|
|
2749
|
+
}
|
|
2750
|
+
function findWhitespaceBreak(payloadObject, start, maxEnd) {
|
|
2751
|
+
for (let cursor = maxEnd; cursor > start; cursor -= 1) {
|
|
2752
|
+
if (!isSafeSplitPosition(payloadObject.mentions, cursor)) {
|
|
2753
|
+
continue;
|
|
2754
|
+
}
|
|
2755
|
+
const previousChar = payloadObject.msg[cursor - 1];
|
|
2756
|
+
if (previousChar === " " || previousChar === " ") {
|
|
2757
|
+
return cursor;
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
return start;
|
|
2761
|
+
}
|
|
2762
|
+
function isSafeSplitPosition(mentions, position) {
|
|
2763
|
+
if (!mentions || mentions.length === 0) {
|
|
2764
|
+
return true;
|
|
2765
|
+
}
|
|
2766
|
+
return mentions.every((mention) => {
|
|
2767
|
+
const mentionEnd = mention.pos + mention.len;
|
|
2768
|
+
return position <= mention.pos || position >= mentionEnd;
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2402
2771
|
function buildTextProperties(styles) {
|
|
2403
2772
|
if (!styles || styles.length === 0) {
|
|
2404
2773
|
return void 0;
|
|
@@ -3781,6 +4150,20 @@ async function persistOutgoingMessageBestEffort(params) {
|
|
|
3781
4150
|
async function persistGroupMembersSnapshot(profile, groupId, api) {
|
|
3782
4151
|
const rows = await listGroupMemberRows(api, groupId);
|
|
3783
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
|
+
}
|
|
3784
4167
|
await replaceThreadMembers(
|
|
3785
4168
|
profile,
|
|
3786
4169
|
groupId,
|
|
@@ -3790,7 +4173,9 @@ async function persistGroupMembersSnapshot(profile, groupId, api) {
|
|
|
3790
4173
|
userId: row.userId,
|
|
3791
4174
|
displayName: row.displayName,
|
|
3792
4175
|
zaloName: row.zaloName,
|
|
3793
|
-
|
|
4176
|
+
avatar: row.avatar,
|
|
4177
|
+
accountStatus: row.accountStatus,
|
|
4178
|
+
rawJson: row.rawJson ?? JSON.stringify(row),
|
|
3794
4179
|
snapshotAtMs
|
|
3795
4180
|
}))
|
|
3796
4181
|
);
|
|
@@ -3806,13 +4191,14 @@ async function persistFriendDirectory(profile, api) {
|
|
|
3806
4191
|
const zaloName = typeof record.zaloName === "string" && record.zaloName.trim() ? record.zaloName.trim() : void 0;
|
|
3807
4192
|
const avatar = typeof record.avatar === "string" && record.avatar.trim() ? record.avatar.trim() : void 0;
|
|
3808
4193
|
const title = displayName || zaloName || userId;
|
|
3809
|
-
await
|
|
4194
|
+
await persistContact({
|
|
3810
4195
|
profile,
|
|
3811
4196
|
userId,
|
|
3812
4197
|
displayName,
|
|
3813
4198
|
zaloName,
|
|
3814
4199
|
avatar,
|
|
3815
4200
|
accountStatus: typeof record.accountStatus === "number" && Number.isFinite(record.accountStatus) ? Math.trunc(record.accountStatus) : void 0,
|
|
4201
|
+
relationship: "friend",
|
|
3816
4202
|
rawJson: JSON.stringify(friend2)
|
|
3817
4203
|
});
|
|
3818
4204
|
await persistThread({
|
|
@@ -3826,6 +4212,10 @@ async function persistFriendDirectory(profile, api) {
|
|
|
3826
4212
|
});
|
|
3827
4213
|
nameById.set(userId, title);
|
|
3828
4214
|
}
|
|
4215
|
+
await reconcileFriendRelationships({
|
|
4216
|
+
profile,
|
|
4217
|
+
currentFriendIds: Array.from(nameById.keys())
|
|
4218
|
+
});
|
|
3829
4219
|
return nameById;
|
|
3830
4220
|
}
|
|
3831
4221
|
function parseSinceDuration(label, value) {
|
|
@@ -3965,6 +4355,99 @@ async function prepareDbGroupTarget(params) {
|
|
|
3965
4355
|
});
|
|
3966
4356
|
await persistGroupMembersSnapshot(params.profile, params.groupId, params.api);
|
|
3967
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
|
+
}
|
|
3968
4451
|
async function syncDbGroupHistoryFull(params) {
|
|
3969
4452
|
if (params.targetGroupIds.size === 0) {
|
|
3970
4453
|
return;
|
|
@@ -4343,17 +4826,26 @@ async function listGroupMemberRows(api, groupId) {
|
|
|
4343
4826
|
if (!profile) continue;
|
|
4344
4827
|
const normalizedKey = normalizeGroupMemberId(key);
|
|
4345
4828
|
if (normalizedKey && !profileMap.has(normalizedKey)) {
|
|
4346
|
-
profileMap.set(normalizedKey,
|
|
4829
|
+
profileMap.set(normalizedKey, {
|
|
4830
|
+
...profile,
|
|
4831
|
+
rawJson: JSON.stringify(profile)
|
|
4832
|
+
});
|
|
4347
4833
|
}
|
|
4348
4834
|
const profileId = normalizeGroupMemberId(profile.id);
|
|
4349
4835
|
if (profileId && !profileMap.has(profileId)) {
|
|
4350
|
-
profileMap.set(profileId,
|
|
4836
|
+
profileMap.set(profileId, {
|
|
4837
|
+
...profile,
|
|
4838
|
+
rawJson: JSON.stringify(profile)
|
|
4839
|
+
});
|
|
4351
4840
|
}
|
|
4352
4841
|
}
|
|
4353
4842
|
return ids.map((id) => ({
|
|
4354
4843
|
userId: id,
|
|
4355
4844
|
displayName: profileMap.get(id)?.displayName ?? currentMemberMap.get(id)?.displayName ?? "",
|
|
4356
|
-
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
|
|
4357
4849
|
}));
|
|
4358
4850
|
}
|
|
4359
4851
|
async function listGroupMentionMembers(api, threadId) {
|
|
@@ -6318,52 +6810,69 @@ dbGroup.command("messages <groupId>").option("--since <duration>", "Rolling wind
|
|
|
6318
6810
|
);
|
|
6319
6811
|
})
|
|
6320
6812
|
);
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
)
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
);
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
);
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
|
|
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
|
+
});
|
|
6367
6876
|
var dbChat = dbCmd.command("chat").description("Query stored conversation data");
|
|
6368
6877
|
dbChat.command("list").option("-j, --json", "JSON output").description("List chats stored in the local DB").action(
|
|
6369
6878
|
wrapAction(async (opts, command) => {
|
|
@@ -6523,20 +7032,67 @@ msg.command("send <threadId> <message>").option("-g, --group", "Send to group").
|
|
|
6523
7032
|
...typeof textPayload === "string" ? { msg: textPayload } : textPayload,
|
|
6524
7033
|
...quote ? { quote } : {}
|
|
6525
7034
|
} : textPayload;
|
|
6526
|
-
const
|
|
7035
|
+
const deliveryPlan = planTextSendPayloadsForDelivery({
|
|
7036
|
+
payload: textPayload,
|
|
7037
|
+
threadType,
|
|
7038
|
+
threadId,
|
|
7039
|
+
maxMessageLength: parsePositiveIntFromEnv(
|
|
7040
|
+
"OPENZCA_TEXT_MESSAGE_MAX_LENGTH",
|
|
7041
|
+
ZALO_TEXT_MESSAGE_MAX_LENGTH
|
|
7042
|
+
),
|
|
7043
|
+
maxRequestParamsLengthEstimate: parsePositiveIntFromEnv(
|
|
7044
|
+
"OPENZCA_TEXT_REQUEST_PARAMS_MAX_ESTIMATE",
|
|
7045
|
+
ZALO_TEXT_REQUEST_PARAMS_MAX_ESTIMATE
|
|
7046
|
+
)
|
|
7047
|
+
});
|
|
7048
|
+
const payloadChunks = deliveryPlan.chunks;
|
|
7049
|
+
const responses = [];
|
|
7050
|
+
const sentPayloads = [];
|
|
7051
|
+
for (let index = 0; index < payloadChunks.length; index += 1) {
|
|
7052
|
+
const chunk = payloadChunks[index];
|
|
7053
|
+
const chunkPayload = quote && index === 0 ? {
|
|
7054
|
+
...typeof chunk === "string" ? { msg: chunk } : chunk,
|
|
7055
|
+
quote
|
|
7056
|
+
} : chunk;
|
|
7057
|
+
sentPayloads.push(chunkPayload);
|
|
7058
|
+
responses.push(await api.sendMessage(chunkPayload, threadId, threadType));
|
|
7059
|
+
}
|
|
7060
|
+
const response = responses.length === 1 ? responses[0] : {
|
|
7061
|
+
chunked: true,
|
|
7062
|
+
chunkCount: responses.length,
|
|
7063
|
+
msgId: responses.at(-1)?.message?.msgId?.toString(),
|
|
7064
|
+
response: responses
|
|
7065
|
+
};
|
|
6527
7066
|
output(response, false);
|
|
6528
7067
|
if (await shouldWriteToDb(profile)) {
|
|
6529
7068
|
scheduleDbWrite(profile, command, "msg.send.db.persist_error", async () => {
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
7069
|
+
if (payloadChunks.length === 1) {
|
|
7070
|
+
await persistOutgoingMessageBestEffort({
|
|
7071
|
+
profile,
|
|
7072
|
+
api,
|
|
7073
|
+
threadId,
|
|
7074
|
+
group: opts.group,
|
|
7075
|
+
text: message,
|
|
7076
|
+
msgType: "text",
|
|
7077
|
+
response,
|
|
7078
|
+
rawPayload: payload
|
|
7079
|
+
});
|
|
7080
|
+
return;
|
|
7081
|
+
}
|
|
7082
|
+
for (let index = 0; index < payloadChunks.length; index += 1) {
|
|
7083
|
+
const chunk = sentPayloads[index];
|
|
7084
|
+
const chunkText = typeof chunk === "string" ? chunk : chunk.msg;
|
|
7085
|
+
await persistOutgoingMessageBestEffort({
|
|
7086
|
+
profile,
|
|
7087
|
+
api,
|
|
7088
|
+
threadId,
|
|
7089
|
+
group: opts.group,
|
|
7090
|
+
text: chunkText,
|
|
7091
|
+
msgType: "text",
|
|
7092
|
+
response: responses[index],
|
|
7093
|
+
rawPayload: chunk
|
|
7094
|
+
});
|
|
7095
|
+
}
|
|
6540
7096
|
});
|
|
6541
7097
|
}
|
|
6542
7098
|
})
|
|
@@ -8125,34 +8681,54 @@ ${replyContextText}` : replyContextText;
|
|
|
8125
8681
|
rawJson: JSON.stringify(mention)
|
|
8126
8682
|
}));
|
|
8127
8683
|
scheduleDbWrite(profile, command, "listen.db.persist_error", async () => {
|
|
8128
|
-
|
|
8129
|
-
|
|
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({
|
|
8130
8719
|
profile,
|
|
8131
|
-
|
|
8132
|
-
|
|
8133
|
-
|
|
8720
|
+
api,
|
|
8721
|
+
peerId: normalizedRecord.scopeThreadId,
|
|
8722
|
+
senderDisplayName,
|
|
8134
8723
|
senderName: senderDisplayName,
|
|
8135
|
-
toId,
|
|
8136
|
-
selfId,
|
|
8137
|
-
title: threadName,
|
|
8138
|
-
msgId: message.data.msgId,
|
|
8139
|
-
cliMsgId: message.data.cliMsgId,
|
|
8140
|
-
actionId: getStringCandidate(messageData, ["actionId"]),
|
|
8141
8724
|
timestampMs,
|
|
8142
|
-
|
|
8143
|
-
|
|
8144
|
-
|
|
8145
|
-
|
|
8146
|
-
|
|
8147
|
-
|
|
8148
|
-
|
|
8149
|
-
media: mediaForDb,
|
|
8150
|
-
mentions: mentionsForDb,
|
|
8151
|
-
rawMessage: message.data,
|
|
8152
|
-
rawPayload: payload,
|
|
8153
|
-
source: "listen"
|
|
8154
|
-
})
|
|
8155
|
-
);
|
|
8725
|
+
rawJson: JSON.stringify({
|
|
8726
|
+
userId: normalizedRecord.scopeThreadId,
|
|
8727
|
+
displayName: senderDisplayName
|
|
8728
|
+
})
|
|
8729
|
+
});
|
|
8730
|
+
}
|
|
8731
|
+
await persistMessage(normalizedRecord);
|
|
8156
8732
|
});
|
|
8157
8733
|
}
|
|
8158
8734
|
if (opts.raw) {
|