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.
- package/README.md +12 -1
- package/dist/cli.js +329 -36
- 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.)
|
|
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
|
|
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 ?
|
|
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" ?
|
|
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 ===
|
|
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 ===
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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 !==
|
|
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(
|
|
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 !==
|
|
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(
|
|
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 !==
|
|
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 ===
|
|
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
|
|
6371
|
+
const textPayload = await buildTextSendPayload({
|
|
6091
6372
|
message,
|
|
6092
6373
|
raw: opts.raw,
|
|
6093
6374
|
threadType,
|
|
6094
6375
|
threadId,
|
|
6095
|
-
listGroupMembers: threadType ===
|
|
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 ===
|
|
6586
|
-
threadType: threadResolution.type ===
|
|
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 ===
|
|
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 ===
|
|
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 ?
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
7919
|
+
isGroup: message.type === ThreadType3.Group,
|
|
7627
7920
|
chatType,
|
|
7628
7921
|
threadId: message.threadId,
|
|
7629
7922
|
targetId: message.threadId,
|