openzca 0.1.49 → 0.1.50

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.
Files changed (3) hide show
  1. package/README.md +12 -1
  2. package/dist/cli.js +329 -36
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -40,6 +40,12 @@ openzca msg send GROUP_ID "Hello team" --group
40
40
  openzca msg send GROUP_ID "Hi @Alice Nguyen" --group
41
41
  openzca msg send GROUP_ID "Hi @123456789" --group
42
42
 
43
+ # Reply using a stored DB message id
44
+ openzca msg send USER_ID "Reply text" --reply-id MSG_ID
45
+
46
+ # Reply without DB using a listen --raw payload
47
+ openzca msg send USER_ID "Reply text" --reply-message '{"threadId":"...","msgId":"...","cliMsgId":"...","content":"...","msgType":"webchat","senderId":"...","toId":"...","ts":"..."}'
48
+
43
49
  # Listen for incoming messages
44
50
  openzca listen
45
51
 
@@ -91,7 +97,7 @@ You can also open the saved file manually (for example: `open qr.png` on macOS).
91
97
 
92
98
  | Command | Description |
93
99
  |---------|-------------|
94
- | `openzca msg send <threadId> <message>` | Send text with formatting (`**bold**`, `*italic*`, `~~strike~~`, etc.) and group @mention resolution (`--raw` to skip formatting) |
100
+ | `openzca msg send <threadId> <message>` | Send text with formatting (`**bold**`, `*italic*`, `~~strike~~`, etc.), group @mention resolution (`--raw` to skip formatting), and quote replies via `--reply-id` or `--reply-message` |
95
101
  | `openzca msg image <threadId> [file]` | Send image(s) from file or URL |
96
102
  | `openzca msg video <threadId> [file]` | Send video(s) from file or URL; single `.mp4` inputs try native video mode |
97
103
  | `openzca msg voice <threadId> [file]` | Send voice message from local file or URL (`.aac`, `.mp3`, `.m4a`, `.wav`, `.ogg`) |
@@ -115,6 +121,11 @@ Media commands accept local files, `file://` paths, and repeatable `--url` optio
115
121
  `openzca msg video` attempts native video send for a single `.mp4` input by uploading the video and thumbnail to Zalo first. If `ffmpeg` is unavailable, the input is not a single `.mp4`, or native send fails, it falls back to the normal attachment send path. Use `--thumbnail <path-or-url>` to supply the preview image explicitly.
116
122
  Local paths using `~` are expanded automatically (for positional file args, `--url`, and `OPENZCA_LISTEN_MEDIA_DIR`).
117
123
  Group text sends via `openzca msg send --group` resolve unique `@Name` or `@userId` mentions against the current group member list using member ids, display names, and usernames. Mention offsets are computed after formatting markers are parsed, so messages like `**@Alice Nguyen** hello` work. If multiple members share the same label, the command fails instead of guessing.
124
+ Reply flows:
125
+
126
+ - `--reply-id <id>` resolves a stored message from the local DB by `msgId`, `cliMsgId`, or internal message uid. This requires DB persistence to be enabled for the profile.
127
+ - `--reply-message <json>` accepts either the original `message.data` object from `zca-js` or the current `openzca listen --raw` payload. Use this path when DB is disabled or when a caller already has the inbound payload in memory.
128
+ - Use exactly one of `--reply-id` or `--reply-message`.
118
129
  `msg recent` keeps the previous live behavior by default. Use `--source db` to read only from the local SQLite store, or `--source auto` to try DB first and fall back to live history.
119
130
 
120
131
  ### Debug Logging
package/dist/cli.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  Gender,
17
17
  Reactions,
18
18
  ReviewPendingMemberRequestStatus,
19
- ThreadType as ThreadType2
19
+ ThreadType as ThreadType3
20
20
  } from "zca-js";
21
21
 
22
22
  // src/lib/store.ts
@@ -2494,6 +2494,231 @@ async function sendNativeVideo(params) {
2494
2494
  }
2495
2495
  }
2496
2496
 
2497
+ // src/lib/reply.ts
2498
+ import { ThreadType as ThreadType2 } from "zca-js";
2499
+ function prepareReplyMessage(value, params) {
2500
+ const sourceRecord = asReplyMessageRecord(value);
2501
+ const metadata = asOptionalReplyMessageRecord(sourceRecord.metadata);
2502
+ const rawMessageRecord = asOptionalReplyMessageRecord(sourceRecord.rawMessage);
2503
+ const rawPayloadRecord = asOptionalReplyMessageRecord(sourceRecord.rawPayload);
2504
+ const canonicalRecord = rawMessageRecord ?? sourceRecord;
2505
+ const content = parseReplyMessageContent(
2506
+ canonicalRecord.content ?? sourceRecord.content,
2507
+ isLikelyOpenzcaListenPayload(sourceRecord) && !rawMessageRecord
2508
+ );
2509
+ const msgType = requireStringLike(
2510
+ [canonicalRecord.msgType, sourceRecord.msgType, metadata?.msgType],
2511
+ "reply message msgType"
2512
+ );
2513
+ const uidFrom = requireStringLike(
2514
+ [
2515
+ canonicalRecord.uidFrom,
2516
+ sourceRecord.uidFrom,
2517
+ sourceRecord.senderId,
2518
+ sourceRecord.fromId,
2519
+ metadata?.senderId,
2520
+ metadata?.fromId
2521
+ ],
2522
+ "reply message uidFrom"
2523
+ );
2524
+ const msgId = requireStringLike(
2525
+ [canonicalRecord.msgId, sourceRecord.msgId, rawPayloadRecord?.msgId],
2526
+ "reply message msgId"
2527
+ );
2528
+ const cliMsgId = requireStringLike(
2529
+ [canonicalRecord.cliMsgId, sourceRecord.cliMsgId, rawPayloadRecord?.cliMsgId],
2530
+ "reply message cliMsgId"
2531
+ );
2532
+ const ts = requireTsString(
2533
+ [canonicalRecord.ts, sourceRecord.ts, maybeTimestampSecondsToMsString(sourceRecord.timestamp)],
2534
+ "reply message ts"
2535
+ );
2536
+ const ttl = parseReplyMessageTtl(canonicalRecord.ttl ?? sourceRecord.ttl);
2537
+ const propertyExt = parseReplyMessagePropertyExt(canonicalRecord.propertyExt);
2538
+ return {
2539
+ quote: {
2540
+ content,
2541
+ msgType,
2542
+ propertyExt,
2543
+ uidFrom,
2544
+ msgId,
2545
+ cliMsgId,
2546
+ ts,
2547
+ ttl
2548
+ },
2549
+ inferredThreadId: inferReplyMessageThreadId({
2550
+ sourceRecord,
2551
+ canonicalRecord,
2552
+ metadata,
2553
+ threadType: params?.threadType,
2554
+ selfId: params?.selfId
2555
+ })
2556
+ };
2557
+ }
2558
+ function prepareStoredReplyMessage(value, params) {
2559
+ const record = asReplyMessageRecord(value);
2560
+ const storedThreadType = record.threadType === "group" ? ThreadType2.Group : record.threadType === "user" ? ThreadType2.User : void 0;
2561
+ if (storedThreadType !== void 0 && storedThreadType !== params.threadType) {
2562
+ throw new Error("Reply source thread type does not match --group.");
2563
+ }
2564
+ const storedThreadId = firstString([record.threadId, record.rawThreadId]) ?? void 0;
2565
+ if (storedThreadId && storedThreadId !== params.threadId) {
2566
+ throw new Error("Reply source belongs to a different thread.");
2567
+ }
2568
+ const rawMessage = asOptionalReplyMessageRecord(record.rawMessage);
2569
+ const rawPayload = asOptionalReplyMessageRecord(record.rawPayload);
2570
+ const replyRecord = rawMessage ?? rawPayload;
2571
+ if (!replyRecord) {
2572
+ throw new Error(
2573
+ "Reply source found in DB but has no reusable raw message payload. Re-sync or capture it via listener first."
2574
+ );
2575
+ }
2576
+ return prepareReplyMessage(replyRecord, {
2577
+ threadType: params.threadType,
2578
+ selfId: params.selfId
2579
+ }).quote;
2580
+ }
2581
+ function asReplyMessageRecord(value) {
2582
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2583
+ throw new Error("Reply message must be a JSON object matching the raw message.data shape.");
2584
+ }
2585
+ return value;
2586
+ }
2587
+ function asOptionalReplyMessageRecord(value) {
2588
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2589
+ return void 0;
2590
+ }
2591
+ return value;
2592
+ }
2593
+ function parseReplyMessageContent(value, stripOpenzcaDecorations) {
2594
+ if (typeof value === "string") {
2595
+ return stripOpenzcaDecorations ? stripEnrichedReplyDecorations(value) : value;
2596
+ }
2597
+ if (value && typeof value === "object" && !Array.isArray(value)) {
2598
+ return value;
2599
+ }
2600
+ throw new Error("Reply message content must be a string or object.");
2601
+ }
2602
+ function stripEnrichedReplyDecorations(value) {
2603
+ const lines = value.split("\n");
2604
+ while (lines.length > 0) {
2605
+ const last = lines[lines.length - 1].trim();
2606
+ if (last.startsWith("[reply context: ") || last.startsWith("[reply media attached:") || last.startsWith("[reply media attached ")) {
2607
+ lines.pop();
2608
+ continue;
2609
+ }
2610
+ break;
2611
+ }
2612
+ return lines.join("\n");
2613
+ }
2614
+ function parseReplyMessagePropertyExt(value) {
2615
+ if (value === void 0) {
2616
+ return void 0;
2617
+ }
2618
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2619
+ throw new Error("Reply message propertyExt must be an object when provided.");
2620
+ }
2621
+ return value;
2622
+ }
2623
+ function parseReplyMessageTtl(value) {
2624
+ if (value === void 0 || value === null || value === "") {
2625
+ return 0;
2626
+ }
2627
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN;
2628
+ if (!Number.isFinite(parsed)) {
2629
+ throw new Error("Reply message ttl must be a finite number.");
2630
+ }
2631
+ return Math.trunc(parsed);
2632
+ }
2633
+ function requireStringLike(values, label) {
2634
+ const value = firstString(values);
2635
+ if (!value) {
2636
+ throw new Error(`Missing ${label}.`);
2637
+ }
2638
+ return value;
2639
+ }
2640
+ function requireTsString(values, label) {
2641
+ for (const value of values) {
2642
+ if (typeof value === "string" && value.trim()) {
2643
+ return value.trim();
2644
+ }
2645
+ if (typeof value === "number" && Number.isFinite(value)) {
2646
+ return String(Math.trunc(value));
2647
+ }
2648
+ }
2649
+ throw new Error(`Missing ${label}.`);
2650
+ }
2651
+ function firstString(values) {
2652
+ for (const value of values) {
2653
+ if (typeof value === "string" && value.trim()) {
2654
+ return value.trim();
2655
+ }
2656
+ if (typeof value === "number" && Number.isFinite(value)) {
2657
+ return String(Math.trunc(value));
2658
+ }
2659
+ }
2660
+ return void 0;
2661
+ }
2662
+ function maybeTimestampSecondsToMsString(value) {
2663
+ if (typeof value !== "number" || !Number.isFinite(value)) {
2664
+ return void 0;
2665
+ }
2666
+ return String(Math.trunc(value * 1e3));
2667
+ }
2668
+ function isLikelyOpenzcaListenPayload(record) {
2669
+ return typeof record.threadId === "string" && (typeof record.senderId === "string" || typeof record.chatType === "string" || typeof record.metadata === "object");
2670
+ }
2671
+ function inferReplyMessageThreadId(params) {
2672
+ const directThreadId = firstString([
2673
+ params.sourceRecord.threadId,
2674
+ params.sourceRecord.targetId,
2675
+ params.sourceRecord.conversationId,
2676
+ params.metadata?.threadId,
2677
+ params.metadata?.targetId
2678
+ ]);
2679
+ if (directThreadId) {
2680
+ return directThreadId;
2681
+ }
2682
+ if (params.threadType === void 0) {
2683
+ return void 0;
2684
+ }
2685
+ const idTo = firstString([
2686
+ params.canonicalRecord.idTo,
2687
+ params.sourceRecord.idTo,
2688
+ params.sourceRecord.toId,
2689
+ params.metadata?.toId
2690
+ ]);
2691
+ if (params.threadType === ThreadType2.Group) {
2692
+ return idTo;
2693
+ }
2694
+ const uidFrom = firstString([
2695
+ params.canonicalRecord.uidFrom,
2696
+ params.sourceRecord.uidFrom,
2697
+ params.sourceRecord.senderId,
2698
+ params.sourceRecord.fromId,
2699
+ params.metadata?.senderId,
2700
+ params.metadata?.fromId
2701
+ ]);
2702
+ if (!uidFrom && !idTo) {
2703
+ return void 0;
2704
+ }
2705
+ if (params.selfId) {
2706
+ if (uidFrom && uidFrom !== params.selfId && uidFrom !== "0") {
2707
+ return uidFrom;
2708
+ }
2709
+ if (idTo && idTo !== params.selfId && idTo !== "0") {
2710
+ return idTo;
2711
+ }
2712
+ }
2713
+ if (uidFrom && uidFrom !== "0") {
2714
+ return uidFrom;
2715
+ }
2716
+ if (idTo && idTo !== "0") {
2717
+ return idTo;
2718
+ }
2719
+ return void 0;
2720
+ }
2721
+
2497
2722
  // src/cli.ts
2498
2723
  var require2 = createRequire(import.meta.url);
2499
2724
  var { version: PKG_VERSION } = require2("../package.json");
@@ -2656,7 +2881,7 @@ function normalizeCommandAliases(argv) {
2656
2881
  return normalized;
2657
2882
  }
2658
2883
  function asThreadType(groupFlag) {
2659
- return groupFlag ? ThreadType2.Group : ThreadType2.User;
2884
+ return groupFlag ? ThreadType3.Group : ThreadType3.User;
2660
2885
  }
2661
2886
  function parseBooleanFromEnv(name, fallback) {
2662
2887
  const raw = process.env[name]?.trim();
@@ -2866,7 +3091,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
2866
3091
  fail(parsed.requestId, "Invalid upload payload.");
2867
3092
  return;
2868
3093
  }
2869
- const threadType = parsed.threadType === "group" ? ThreadType2.Group : ThreadType2.User;
3094
+ const threadType = parsed.threadType === "group" ? ThreadType3.Group : ThreadType3.User;
2870
3095
  const requestTimeoutMs = parsePositiveIntFromUnknown(parsed.uploadTimeoutMs) ?? uploadTimeoutMs;
2871
3096
  writeDebugLine(
2872
3097
  "listen.ipc.upload.start",
@@ -3023,7 +3248,7 @@ async function tryUploadViaListenerIpc(profile, threadId, threadType, attachment
3023
3248
  {
3024
3249
  profile,
3025
3250
  threadId,
3026
- threadType: threadType === ThreadType2.Group ? "group" : "user",
3251
+ threadType: threadType === ThreadType3.Group ? "group" : "user",
3027
3252
  attachmentCount: attachments.length,
3028
3253
  socketPath,
3029
3254
  requestId,
@@ -3067,7 +3292,7 @@ async function tryUploadViaListenerIpc(profile, threadId, threadType, attachment
3067
3292
  requestId,
3068
3293
  profile,
3069
3294
  threadId,
3070
- threadType: threadType === ThreadType2.Group ? "group" : "user",
3295
+ threadType: threadType === ThreadType3.Group ? "group" : "user",
3071
3296
  attachments
3072
3297
  };
3073
3298
  socket.write(`${JSON.stringify(payload)}
@@ -3129,21 +3354,21 @@ async function tryUploadViaListenerIpc(profile, threadId, threadType, attachment
3129
3354
  }
3130
3355
  async function resolveUploadThreadType(api, profile, threadId, groupFlag, command) {
3131
3356
  if (groupFlag) {
3132
- return { type: ThreadType2.Group, reason: "explicit_group_flag" };
3357
+ return { type: ThreadType3.Group, reason: "explicit_group_flag" };
3133
3358
  }
3134
3359
  const autoDetectEnabled = parseBooleanFromEnv("OPENZCA_UPLOAD_AUTO_THREAD_TYPE", false);
3135
3360
  if (!autoDetectEnabled) {
3136
- return { type: ThreadType2.User, reason: "auto_detect_disabled" };
3361
+ return { type: ThreadType3.User, reason: "auto_detect_disabled" };
3137
3362
  }
3138
3363
  try {
3139
3364
  const cache = await readCache(profile);
3140
3365
  const groupIds = collectIdsFromCacheEntries(cache.groups, ["groupId", "grid", "threadId", "id"]);
3141
3366
  if (groupIds.has(threadId)) {
3142
- return { type: ThreadType2.Group, reason: "cache_group_match" };
3367
+ return { type: ThreadType3.Group, reason: "cache_group_match" };
3143
3368
  }
3144
3369
  const friendIds = collectIdsFromCacheEntries(cache.friends, ["userId", "uid", "id", "threadId"]);
3145
3370
  if (friendIds.has(threadId)) {
3146
- return { type: ThreadType2.User, reason: "cache_friend_match" };
3371
+ return { type: ThreadType3.User, reason: "cache_friend_match" };
3147
3372
  }
3148
3373
  } catch (error) {
3149
3374
  writeDebugLine(
@@ -3158,7 +3383,7 @@ async function resolveUploadThreadType(api, profile, threadId, groupFlag, comman
3158
3383
  }
3159
3384
  const probeEnabled = parseBooleanFromEnv("OPENZCA_UPLOAD_GROUP_PROBE", true);
3160
3385
  if (!probeEnabled) {
3161
- return { type: ThreadType2.User, reason: "probe_disabled" };
3386
+ return { type: ThreadType3.User, reason: "probe_disabled" };
3162
3387
  }
3163
3388
  const probeTimeoutMs = parsePositiveIntFromEnv("OPENZCA_UPLOAD_GROUP_PROBE_TIMEOUT_MS", 5e3);
3164
3389
  try {
@@ -3168,7 +3393,7 @@ async function resolveUploadThreadType(api, profile, threadId, groupFlag, comman
3168
3393
  `Timed out waiting ${probeTimeoutMs}ms while probing group thread type.`
3169
3394
  );
3170
3395
  if (groupInfo?.gridInfoMap?.[threadId]) {
3171
- return { type: ThreadType2.Group, reason: "probe_group_match" };
3396
+ return { type: ThreadType3.Group, reason: "probe_group_match" };
3172
3397
  }
3173
3398
  } catch (error) {
3174
3399
  writeDebugLine(
@@ -3181,7 +3406,7 @@ async function resolveUploadThreadType(api, profile, threadId, groupFlag, comman
3181
3406
  command
3182
3407
  );
3183
3408
  }
3184
- return { type: ThreadType2.User, reason: "default_user" };
3409
+ return { type: ThreadType3.User, reason: "default_user" };
3185
3410
  }
3186
3411
  function parseReaction(input) {
3187
3412
  const normalized = input.trim();
@@ -3279,6 +3504,62 @@ async function shouldWriteToDb(profile, override) {
3279
3504
  }
3280
3505
  return isDbEnabled(profile);
3281
3506
  }
3507
+ async function resolveSendReplyQuote(params) {
3508
+ const replyId = params.replyId?.trim();
3509
+ const replyMessage = params.replyMessage?.trim();
3510
+ if (replyId && replyMessage) {
3511
+ throw new Error("Use either --reply-id or --reply-message, not both.");
3512
+ }
3513
+ if (!replyId && !replyMessage) {
3514
+ return void 0;
3515
+ }
3516
+ if (replyId) {
3517
+ if (!await shouldWriteToDb(params.profile)) {
3518
+ throw new Error("`--reply-id` requires the local DB. Enable DB/listen sync first.");
3519
+ }
3520
+ const row = await getMessageById({
3521
+ profile: params.profile,
3522
+ id: replyId
3523
+ });
3524
+ if (!row) {
3525
+ throw new Error(`Reply source not found in DB: ${replyId}`);
3526
+ }
3527
+ if (row.threadType === "group" !== (params.threadType === ThreadType3.Group)) {
3528
+ throw new Error("Reply source thread type does not match --group.");
3529
+ }
3530
+ if (row.threadId !== params.threadId) {
3531
+ throw new Error("Reply source belongs to a different thread.");
3532
+ }
3533
+ if (!row.rawMessage || typeof row.rawMessage !== "object") {
3534
+ if (!row.rawPayload || typeof row.rawPayload !== "object") {
3535
+ throw new Error(
3536
+ "Reply source found in DB but has no reusable raw message payload. Re-sync or capture it via listener first."
3537
+ );
3538
+ }
3539
+ }
3540
+ return prepareStoredReplyMessage(row, {
3541
+ threadId: params.threadId,
3542
+ threadType: params.threadType,
3543
+ selfId: params.api.getOwnId()
3544
+ });
3545
+ }
3546
+ let parsedReplyMessage;
3547
+ try {
3548
+ parsedReplyMessage = JSON.parse(replyMessage);
3549
+ } catch (error) {
3550
+ throw new Error(
3551
+ `Invalid JSON for --reply-message: ${error instanceof Error ? error.message : String(error)}`
3552
+ );
3553
+ }
3554
+ const preparedReply = prepareReplyMessage(parsedReplyMessage, {
3555
+ threadType: params.threadType,
3556
+ selfId: params.api.getOwnId()
3557
+ });
3558
+ if (preparedReply.inferredThreadId && preparedReply.inferredThreadId !== params.threadId) {
3559
+ throw new Error("Reply message belongs to a different thread.");
3560
+ }
3561
+ return preparedReply.quote;
3562
+ }
3282
3563
  function scheduleDbWrite(profile, command, event, task) {
3283
3564
  enqueueDbWrite(profile, async () => {
3284
3565
  try {
@@ -4259,7 +4540,7 @@ function normalizeGroupHistoryMessages(messages, fallbackThreadId) {
4259
4540
  const threadIdRaw = String(raw.idTo ?? "").trim();
4260
4541
  normalized.push({
4261
4542
  threadId: threadIdRaw || fallbackThreadId,
4262
- type: ThreadType2.Group,
4543
+ type: ThreadType3.Group,
4263
4544
  data: {
4264
4545
  actionId: typeof raw.actionId === "string" && raw.actionId.trim() ? raw.actionId : void 0,
4265
4546
  msgId: String(raw.msgId ?? ""),
@@ -4364,7 +4645,7 @@ async function crawlGroupHistoryViaListener(api, options) {
4364
4645
  requestedCursors.add(cursor);
4365
4646
  }
4366
4647
  pagesRequested += 1;
4367
- api.listener.requestOldMessages(ThreadType2.Group, cursor || null);
4648
+ api.listener.requestOldMessages(ThreadType3.Group, cursor || null);
4368
4649
  return true;
4369
4650
  };
4370
4651
  const armIdleTimer = () => {
@@ -4419,7 +4700,7 @@ async function crawlGroupHistoryViaListener(api, options) {
4419
4700
  }
4420
4701
  };
4421
4702
  const onOldMessages = (messages, type) => {
4422
- if (type !== ThreadType2.Group) return;
4703
+ if (type !== ThreadType3.Group) return;
4423
4704
  armIdleTimer();
4424
4705
  const typedMessages = messages;
4425
4706
  processing = processing.then(async () => {
@@ -4508,7 +4789,7 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
4508
4789
  requestedCursors.add(cursor);
4509
4790
  }
4510
4791
  pagesRequested += 1;
4511
- api.listener.requestOldMessages(ThreadType2.User, cursor || null);
4792
+ api.listener.requestOldMessages(ThreadType3.User, cursor || null);
4512
4793
  return true;
4513
4794
  };
4514
4795
  const cleanup = () => {
@@ -4540,7 +4821,7 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
4540
4821
  }
4541
4822
  };
4542
4823
  const onOldMessages = (messages, type) => {
4543
- if (type !== ThreadType2.User) return;
4824
+ if (type !== ThreadType3.User) return;
4544
4825
  const typedMessages = messages;
4545
4826
  for (const message of typedMessages) {
4546
4827
  if (message.threadId === threadId) {
@@ -4616,7 +4897,7 @@ async function fetchRecentUserMessagesAcrossThreads(api, maxMessages) {
4616
4897
  requestedCursors.add(cursor);
4617
4898
  }
4618
4899
  pagesRequested += 1;
4619
- api.listener.requestOldMessages(ThreadType2.User, cursor || null);
4900
+ api.listener.requestOldMessages(ThreadType3.User, cursor || null);
4620
4901
  return true;
4621
4902
  };
4622
4903
  const cleanup = () => {
@@ -4648,7 +4929,7 @@ async function fetchRecentUserMessagesAcrossThreads(api, maxMessages) {
4648
4929
  }
4649
4930
  };
4650
4931
  const onOldMessages = (messages, type) => {
4651
- if (type !== ThreadType2.User) return;
4932
+ if (type !== ThreadType3.User) return;
4652
4933
  const typedMessages = messages;
4653
4934
  for (const message of typedMessages) {
4654
4935
  const key = toKey(message);
@@ -4731,7 +5012,7 @@ function toDbRecordFromRecentMessage(params) {
4731
5012
  const quote = params.message.data?.quote;
4732
5013
  return normalizeInboundListenRecord({
4733
5014
  profile: params.profile,
4734
- threadType: params.message.type === ThreadType2.Group ? "group" : "user",
5015
+ threadType: params.message.type === ThreadType3.Group ? "group" : "user",
4735
5016
  rawThreadId: params.message.threadId,
4736
5017
  senderId: params.message.data?.uidFrom,
4737
5018
  senderName: params.message.data?.dName,
@@ -6083,17 +6364,29 @@ dbSync.command("chat <chatId>").option("-n, --count <count>", "Recent messages t
6083
6364
  })
6084
6365
  );
6085
6366
  var msg = program.command("msg").description("Messaging commands");
6086
- msg.command("send <threadId> <message>").option("-g, --group", "Send to group").option("--raw", "Send raw text without parsing formatting markers").description("Send text message with formatting (**bold** *italic* __bold__ ~~strike~~ {underline}text{/underline} {red}color{/red} {big}size{/big} lists indents). Group sends also resolve unique @Name/@userId mentions.").action(
6367
+ msg.command("send <threadId> <message>").option("-g, --group", "Send to group").option("--raw", "Send raw text without parsing formatting markers").option("--reply-id <id>", "Reply using a stored DB message id/msgId/cliMsgId").option("--reply-message <json>", "Reply using a raw message.data JSON object").description("Send text message with formatting (**bold** *italic* __bold__ ~~strike~~ {underline}text{/underline} {red}color{/red} {big}size{/big} lists indents). Group sends also resolve unique @Name/@userId mentions.").action(
6087
6368
  wrapAction(async (threadId, message, opts, command) => {
6088
6369
  const { api, profile } = await requireApi(command);
6089
6370
  const threadType = asThreadType(opts.group);
6090
- const payload = await buildTextSendPayload({
6371
+ const textPayload = await buildTextSendPayload({
6091
6372
  message,
6092
6373
  raw: opts.raw,
6093
6374
  threadType,
6094
6375
  threadId,
6095
- listGroupMembers: threadType === ThreadType2.Group ? (groupId) => listGroupMentionMembers(api, groupId) : void 0
6376
+ listGroupMembers: threadType === ThreadType3.Group ? (groupId) => listGroupMentionMembers(api, groupId) : void 0
6377
+ });
6378
+ const quote = await resolveSendReplyQuote({
6379
+ profile,
6380
+ api,
6381
+ threadId,
6382
+ threadType,
6383
+ replyId: opts.replyId,
6384
+ replyMessage: opts.replyMessage
6096
6385
  });
6386
+ const payload = quote || typeof textPayload !== "string" ? {
6387
+ ...typeof textPayload === "string" ? { msg: textPayload } : textPayload,
6388
+ ...quote ? { quote } : {}
6389
+ } : textPayload;
6097
6390
  const response = await api.sendMessage(payload, threadId, threadType);
6098
6391
  output(response, false);
6099
6392
  if (await shouldWriteToDb(profile)) {
@@ -6582,8 +6875,8 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
6582
6875
  {
6583
6876
  threadId,
6584
6877
  explicitGroupFlag: Boolean(opts.group),
6585
- isGroup: threadResolution.type === ThreadType2.Group,
6586
- threadType: threadResolution.type === ThreadType2.Group ? "group" : "user",
6878
+ isGroup: threadResolution.type === ThreadType3.Group,
6879
+ threadType: threadResolution.type === ThreadType3.Group ? "group" : "user",
6587
6880
  threadTypeReason: threadResolution.reason,
6588
6881
  localFiles,
6589
6882
  urlInputs
@@ -6611,7 +6904,7 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
6611
6904
  "msg.upload.ipc.done",
6612
6905
  {
6613
6906
  threadId,
6614
- threadType: threadResolution.type === ThreadType2.Group ? "group" : "user"
6907
+ threadType: threadResolution.type === ThreadType3.Group ? "group" : "user"
6615
6908
  },
6616
6909
  command
6617
6910
  );
@@ -6622,7 +6915,7 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
6622
6915
  "msg.upload.ipc.fallback",
6623
6916
  {
6624
6917
  threadId,
6625
- threadType: threadResolution.type === ThreadType2.Group ? "group" : "user",
6918
+ threadType: threadResolution.type === ThreadType3.Group ? "group" : "user",
6626
6919
  reason: ipcResult.reason
6627
6920
  },
6628
6921
  command
@@ -6661,7 +6954,7 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
6661
6954
  const { api, profile } = await requireApi(command);
6662
6955
  const parsedCount = Number(opts.count);
6663
6956
  const count = Number.isFinite(parsedCount) ? Math.min(Math.max(Math.trunc(parsedCount), 1), 200) : 20;
6664
- const threadType = opts.group ? ThreadType2.Group : ThreadType2.User;
6957
+ const threadType = opts.group ? ThreadType3.Group : ThreadType3.User;
6665
6958
  const source = (opts.source ?? "live").trim().toLowerCase();
6666
6959
  if (!["live", "db", "auto"].includes(source)) {
6667
6960
  throw new Error("--source must be one of: live, db, auto");
@@ -6682,7 +6975,7 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
6682
6975
  msgId: message.data.msgId,
6683
6976
  cliMsgId: message.data.cliMsgId,
6684
6977
  threadId: message.threadId || threadId,
6685
- threadType: message.type === ThreadType2.Group ? "group" : "user",
6978
+ threadType: message.type === ThreadType3.Group ? "group" : "user",
6686
6979
  senderId: message.data.uidFrom,
6687
6980
  senderName: message.data.dName ?? "",
6688
6981
  ts: message.data.ts,
@@ -6691,7 +6984,7 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
6691
6984
  msgId: message.data.msgId,
6692
6985
  cliMsgId: message.data.cliMsgId,
6693
6986
  threadId: message.threadId || threadId,
6694
- group: message.type === ThreadType2.Group
6987
+ group: message.type === ThreadType3.Group
6695
6988
  },
6696
6989
  content: typeof message.data.content === "string" ? message.data.content : JSON.stringify(message.data.content)
6697
6990
  }));
@@ -6700,7 +6993,7 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
6700
6993
  output(
6701
6994
  {
6702
6995
  threadId,
6703
- threadType: threadType === ThreadType2.Group ? "group" : "user",
6996
+ threadType: threadType === ThreadType3.Group ? "group" : "user",
6704
6997
  count: rows.length,
6705
6998
  messages: rows
6706
6999
  },
@@ -6720,7 +7013,7 @@ msg.command("pin <threadId>").option("-g, --group", "Pin group conversation").de
6720
7013
  output(
6721
7014
  {
6722
7015
  threadId,
6723
- threadType: type === ThreadType2.Group ? "group" : "user",
7016
+ threadType: type === ThreadType3.Group ? "group" : "user",
6724
7017
  pinned: true,
6725
7018
  response
6726
7019
  },
@@ -6736,7 +7029,7 @@ msg.command("unpin <threadId>").option("-g, --group", "Unpin group conversation"
6736
7029
  output(
6737
7030
  {
6738
7031
  threadId,
6739
- threadType: type === ThreadType2.Group ? "group" : "user",
7032
+ threadType: type === ThreadType3.Group ? "group" : "user",
6740
7033
  pinned: false,
6741
7034
  response
6742
7035
  },
@@ -7576,7 +7869,7 @@ ${replyMediaText}` : replyMediaText;
7576
7869
  processedText = processedText.trim() ? `${processedText}
7577
7870
  ${replyContextText}` : replyContextText;
7578
7871
  }
7579
- const chatType = message.type === ThreadType2.Group ? "group" : "user";
7872
+ const chatType = message.type === ThreadType3.Group ? "group" : "user";
7580
7873
  const senderId = getStringCandidate(messageData, ["uidFrom"]) || message.data.uidFrom;
7581
7874
  const senderDisplayNameRaw = getStringCandidate(messageData, [
7582
7875
  "dName",
@@ -7585,7 +7878,7 @@ ${replyContextText}` : replyContextText;
7585
7878
  "displayName"
7586
7879
  ]);
7587
7880
  const senderDisplayName = senderDisplayNameRaw || void 0;
7588
- const senderNameForMetadata = message.type === ThreadType2.Group ? senderDisplayName : void 0;
7881
+ const senderNameForMetadata = message.type === ThreadType3.Group ? senderDisplayName : void 0;
7589
7882
  const toId = getStringCandidate(messageData, ["idTo"]) || void 0;
7590
7883
  const threadName = typeof messageData.threadName === "string" ? messageData.threadName : typeof messageData.tName === "string" ? messageData.tName : void 0;
7591
7884
  const mentions = extractInboundMentions({
@@ -7623,7 +7916,7 @@ ${replyContextText}` : replyContextText;
7623
7916
  mentions: mentions.length > 0 ? mentions : void 0,
7624
7917
  mentionIds: mentionIds.length > 0 ? mentionIds : void 0,
7625
7918
  metadata: {
7626
- isGroup: message.type === ThreadType2.Group,
7919
+ isGroup: message.type === ThreadType3.Group,
7627
7920
  chatType,
7628
7921
  threadId: message.threadId,
7629
7922
  targetId: message.threadId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzca",
3
- "version": "0.1.49",
3
+ "version": "0.1.50",
4
4
  "description": "Open-source zca-compatible CLI to integrate Zalo with OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {