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/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 persistFriend(record) {
793
+ async function persistContact(record) {
759
794
  const db = await getDb(record.profile);
760
795
  const now = nowIso2();
761
- await db.run(
762
- `
763
- INSERT INTO friends (
764
- profile, user_id, display_name, zalo_name, avatar,
765
- account_status, raw_json, created_at, updated_at
766
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
767
- ON CONFLICT(profile, user_id) DO UPDATE SET
768
- display_name = COALESCE(excluded.display_name, friends.display_name),
769
- zalo_name = COALESCE(excluded.zalo_name, friends.zalo_name),
770
- avatar = COALESCE(excluded.avatar, friends.avatar),
771
- account_status = COALESCE(excluded.account_status, friends.account_status),
772
- raw_json = COALESCE(excluded.raw_json, friends.raw_json),
773
- updated_at = excluded.updated_at
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(f.display_name, ''),
994
- NULLIF(f.zalo_name, '')
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 friends f
1012
- ON f.profile = m.profile
1013
- AND f.user_id = m.sender_id
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
- async function listFriends(profile) {
1226
- const db = await getDb(profile);
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
- f.user_id,
1231
- f.display_name,
1232
- f.zalo_name,
1233
- f.avatar,
1234
- f.account_status,
1235
- t.scope_thread_id AS chat_id,
1236
- t.title,
1237
- COUNT(m.message_uid) AS message_count,
1238
- MAX(m.timestamp_ms) AS last_message_at_ms
1239
- FROM friends f
1240
- LEFT JOIN threads t
1241
- ON t.profile = f.profile
1242
- AND t.thread_type = 'user'
1243
- AND (t.peer_id = f.user_id OR t.scope_thread_id = f.user_id OR t.raw_thread_id = f.user_id)
1244
- LEFT JOIN messages m
1245
- ON m.profile = t.profile
1246
- AND m.scope_thread_id = t.scope_thread_id
1247
- WHERE f.profile = ?
1248
- GROUP BY
1249
- f.user_id, f.display_name, f.zalo_name, f.avatar, f.account_status, t.scope_thread_id, t.title
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 getFriendInfo(params) {
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
- f.user_id,
1272
- f.display_name,
1273
- f.zalo_name,
1274
- f.avatar,
1275
- f.account_status,
1276
- f.raw_json,
1277
- t.scope_thread_id AS chat_id,
1278
- t.title,
1279
- COUNT(m.message_uid) AS message_count,
1280
- MAX(m.timestamp_ms) AS last_message_at_ms
1281
- FROM friends f
1282
- LEFT JOIN threads t
1283
- ON t.profile = f.profile
1284
- AND t.thread_type = 'user'
1285
- AND (t.peer_id = f.user_id OR t.scope_thread_id = f.user_id OR t.raw_thread_id = f.user_id)
1286
- LEFT JOIN messages m
1287
- ON m.profile = t.profile
1288
- AND m.scope_thread_id = t.scope_thread_id
1289
- WHERE f.profile = ?
1290
- AND f.user_id = ?
1291
- GROUP BY
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
- [params.profile, params.userId]
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 findFriends(params) {
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 listFriends(params.profile);
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
- payloadObject,
2372
- rawInputLength: params.message.length,
2373
- renderedTextLength: payloadObject.msg.length,
2374
- styleCount: payloadObject.styles?.length ?? 0,
2375
- mentionCount: payloadObject.mentions?.length ?? 0,
2376
- textPropertiesLength: textProperties?.length ?? 0,
2377
- mentionInfoLength: mentionInfo?.length ?? 0,
2378
- requestParamsLengthEstimate: JSON.stringify(requestParams).length,
2379
- sendPath: params.threadType === ThreadType.Group ? mentionInfo ? "mention" : "sendmsg" : "sms"
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
- rawJson: JSON.stringify(row),
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 persistFriend({
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, profile);
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, profile);
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
- var dbFriend = dbCmd.command("friend").description("Query stored friend directory data");
6322
- dbFriend.command("list").option("-j, --json", "JSON output").description("List friends stored in the local DB").action(
6323
- wrapAction(async (opts, command) => {
6324
- const profile = await currentProfile(command);
6325
- output(await listFriends(profile), Boolean(opts.json));
6326
- })
6327
- );
6328
- dbFriend.command("find <query>").option("-j, --json", "JSON output").description("Find stored friends by ID or name").action(
6329
- wrapAction(async (query, opts, command) => {
6330
- const profile = await currentProfile(command);
6331
- output(await findFriends({ profile, query }), Boolean(opts.json));
6332
- })
6333
- );
6334
- dbFriend.command("info <userId>").option("-j, --json", "JSON output").description("Show stored info for a friend").action(
6335
- wrapAction(async (userId, opts, command) => {
6336
- const profile = await currentProfile(command);
6337
- const row = await getFriendInfo({ profile, userId });
6338
- if (!row) {
6339
- throw new Error(`Friend not found in DB: ${userId}`);
6340
- }
6341
- output(row, Boolean(opts.json));
6342
- })
6343
- );
6344
- dbFriend.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 friend").action(
6345
- wrapAction(async (userId, opts, command) => {
6346
- const profile = await currentProfile(command);
6347
- const { sinceMs, untilMs, limit, newestFirst } = resolveMessageQueryOptions(opts);
6348
- const rows = await listMessages({
6349
- profile,
6350
- threadId: userId,
6351
- threadType: "user",
6352
- sinceMs,
6353
- untilMs,
6354
- limit,
6355
- newestFirst
6356
- });
6357
- output(
6358
- {
6359
- userId,
6360
- count: rows.length,
6361
- messages: rows
6362
- },
6363
- Boolean(opts.json)
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 response = await api.sendMessage(payload, threadId, threadType);
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
- await persistOutgoingMessageBestEffort({
6531
- profile,
6532
- api,
6533
- threadId,
6534
- group: opts.group,
6535
- text: message,
6536
- msgType: "text",
6537
- response,
6538
- rawPayload: payload
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
- await persistMessage(
8129
- normalizeInboundListenRecord({
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
- threadType: chatType,
8132
- rawThreadId: message.threadId,
8133
- senderId,
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
- msgType: msgType || void 0,
8143
- contentText: processedText || rawText || void 0,
8144
- contentJson: rawContent && typeof rawContent === "object" ? JSON.stringify(rawContent) : void 0,
8145
- quoteMsgId: quote?.globalMsgId ? String(quote.globalMsgId) : void 0,
8146
- quoteCliMsgId: quote?.cliMsgId ? String(quote.cliMsgId) : void 0,
8147
- quoteOwnerId: quote?.ownerId ? String(quote.ownerId) : void 0,
8148
- quoteText: quote?.msg,
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) {