openzca 0.1.51 → 0.1.53
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 +10 -0
- package/dist/cli.js +405 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,6 +48,9 @@ openzca msg send USER_ID "Reply text" --reply-id MSG_ID
|
|
|
48
48
|
# Reply without DB using a listen --raw payload
|
|
49
49
|
openzca msg send USER_ID "Reply text" --reply-message '{"threadId":"...","msgId":"...","cliMsgId":"...","content":"...","msgType":"webchat","senderId":"...","toId":"...","ts":"..."}'
|
|
50
50
|
|
|
51
|
+
# Inspect how a formatted message expands before sending/chunking
|
|
52
|
+
openzca msg analyze-text GROUP_ID "- item one\n- item two" --group --json
|
|
53
|
+
|
|
51
54
|
# Listen for incoming messages
|
|
52
55
|
openzca listen
|
|
53
56
|
|
|
@@ -100,6 +103,7 @@ You can also open the saved file manually (for example: `open qr.png` on macOS).
|
|
|
100
103
|
| Command | Description |
|
|
101
104
|
|---------|-------------|
|
|
102
105
|
| `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` |
|
|
106
|
+
| `openzca msg analyze-text <threadId> <message>` | Build and inspect the exact text payload `msg send` would hand to `zca-js`, including rendered text length, style count, mention count, `textProperties` size, and request size estimate |
|
|
103
107
|
| `openzca msg image <threadId> [file]` | Send image(s) from file or URL |
|
|
104
108
|
| `openzca msg video <threadId> [file]` | Send video(s) from file or URL; single `.mp4` inputs try native video mode |
|
|
105
109
|
| `openzca msg voice <threadId> [file]` | Send voice message from local file or URL (`.aac`, `.mp3`, `.m4a`, `.wav`, `.ogg`) |
|
|
@@ -123,6 +127,8 @@ Media commands accept local files, `file://` paths, and repeatable `--url` optio
|
|
|
123
127
|
`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.
|
|
124
128
|
Local paths using `~` are expanded automatically (for positional file args, `--url`, and `OPENZCA_LISTEN_MEDIA_DIR`).
|
|
125
129
|
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.
|
|
130
|
+
When formatted text would produce an oversized outbound payload, `openzca msg send` automatically splits it into multiple sequential text messages using the final outbound text and rebased style/mention offsets. The split happens after formatting is parsed, using both rendered text length and estimated request payload size rather than the raw input string.
|
|
131
|
+
Use `openzca msg analyze-text ... --json` when you need to predict whether a formatted reply will expand into a large `textProperties` payload before attempting delivery.
|
|
126
132
|
Reply flows:
|
|
127
133
|
|
|
128
134
|
- `--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.
|
|
@@ -386,6 +392,10 @@ Listener resilience override:
|
|
|
386
392
|
- `OPENZCA_RECENT_GROUP_MAX_PAGES`: max websocket history pages to scan for `msg recent -g` when direct group-history path fails.
|
|
387
393
|
- Default: `20`.
|
|
388
394
|
- Increase if a group thread is old and not found quickly.
|
|
395
|
+
- `OPENZCA_TEXT_MESSAGE_MAX_LENGTH`: max rendered characters allowed per outbound text chunk before `msg send` splits it.
|
|
396
|
+
- Default: `2000`.
|
|
397
|
+
- `OPENZCA_TEXT_REQUEST_PARAMS_MAX_ESTIMATE`: max estimated serialized request size allowed per outbound text chunk before `msg send` splits it.
|
|
398
|
+
- Default: `4000`.
|
|
389
399
|
- `OPENZCA_LISTEN_ENFORCE_SINGLE_OWNER`: enforce one `listen` owner process per profile.
|
|
390
400
|
- Default: enabled.
|
|
391
401
|
- Set to `0` to allow multiple listeners on the same profile (not recommended).
|
package/dist/cli.js
CHANGED
|
@@ -1979,9 +1979,6 @@ function parseTimeBoundaryInput(value, _nowMs = Date.now()) {
|
|
|
1979
1979
|
return void 0;
|
|
1980
1980
|
}
|
|
1981
1981
|
|
|
1982
|
-
// src/lib/text-send.ts
|
|
1983
|
-
import { ThreadType } from "zca-js";
|
|
1984
|
-
|
|
1985
1982
|
// src/lib/group-mentions.ts
|
|
1986
1983
|
var ALLOWED_START_BOUNDARY_CHARS = /* @__PURE__ */ new Set(["(", "[", "{", "<", '"', ",", ";", ":"]);
|
|
1987
1984
|
var ALLOWED_END_BOUNDARY_CHARS = /* @__PURE__ */ new Set([",", ";", ":", "!", "?", ")", "]", "}", ">", '"']);
|
|
@@ -2086,6 +2083,9 @@ function isMentionStartBoundary(text, atIndex) {
|
|
|
2086
2083
|
return /\s/u.test(previous) || ALLOWED_START_BOUNDARY_CHARS.has(previous);
|
|
2087
2084
|
}
|
|
2088
2085
|
|
|
2086
|
+
// src/lib/text-send.ts
|
|
2087
|
+
import { ThreadType } from "zca-js";
|
|
2088
|
+
|
|
2089
2089
|
// src/lib/text-styles.ts
|
|
2090
2090
|
import { TextStyle } from "zca-js";
|
|
2091
2091
|
var TAG_STYLE_MAP = {
|
|
@@ -2333,6 +2333,8 @@ function normalizeCodeBlockLeadingWhitespace(line) {
|
|
|
2333
2333
|
}
|
|
2334
2334
|
|
|
2335
2335
|
// src/lib/text-send.ts
|
|
2336
|
+
var ZALO_TEXT_MESSAGE_MAX_LENGTH = 2e3;
|
|
2337
|
+
var ZALO_TEXT_REQUEST_PARAMS_MAX_ESTIMATE = 4e3;
|
|
2336
2338
|
async function buildTextSendPayload(params) {
|
|
2337
2339
|
if (params.raw) {
|
|
2338
2340
|
const mentions2 = await resolveGroupMentionsIfNeeded(params, params.message);
|
|
@@ -2346,6 +2348,127 @@ async function buildTextSendPayload(params) {
|
|
|
2346
2348
|
mentions
|
|
2347
2349
|
};
|
|
2348
2350
|
}
|
|
2351
|
+
async function analyzeTextSendPayload(params) {
|
|
2352
|
+
const payload = await buildTextSendPayload(params);
|
|
2353
|
+
const payloadObject = normalizeTextSendPayload(payload);
|
|
2354
|
+
const textProperties = buildTextProperties(payloadObject.styles);
|
|
2355
|
+
const mentionInfo = buildMentionInfo(
|
|
2356
|
+
params.threadType,
|
|
2357
|
+
payloadObject.msg,
|
|
2358
|
+
payloadObject.mentions
|
|
2359
|
+
);
|
|
2360
|
+
const requestParams = omitUndefined({
|
|
2361
|
+
message: payloadObject.msg,
|
|
2362
|
+
clientId: 17e11,
|
|
2363
|
+
mentionInfo,
|
|
2364
|
+
imei: params.threadType === ThreadType.Group ? void 0 : "000000000000000",
|
|
2365
|
+
ttl: 0,
|
|
2366
|
+
visibility: params.threadType === ThreadType.Group ? 0 : void 0,
|
|
2367
|
+
toid: params.threadType === ThreadType.Group ? void 0 : params.threadId,
|
|
2368
|
+
grid: params.threadType === ThreadType.Group ? params.threadId : void 0,
|
|
2369
|
+
textProperties
|
|
2370
|
+
});
|
|
2371
|
+
return {
|
|
2372
|
+
payload,
|
|
2373
|
+
...buildTextSendPayloadAnalysis({
|
|
2374
|
+
payloadObject,
|
|
2375
|
+
rawInputLength: params.message.length,
|
|
2376
|
+
textProperties,
|
|
2377
|
+
mentionInfo,
|
|
2378
|
+
requestParamsLengthEstimate: JSON.stringify(requestParams).length,
|
|
2379
|
+
threadType: params.threadType
|
|
2380
|
+
})
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
function planTextSendPayloadsForDelivery(params) {
|
|
2384
|
+
const maxMessageLength = resolvePositiveLimit(
|
|
2385
|
+
params.maxMessageLength,
|
|
2386
|
+
ZALO_TEXT_MESSAGE_MAX_LENGTH
|
|
2387
|
+
);
|
|
2388
|
+
const maxRequestParamsLengthEstimate = resolvePositiveLimit(
|
|
2389
|
+
params.maxRequestParamsLengthEstimate,
|
|
2390
|
+
ZALO_TEXT_REQUEST_PARAMS_MAX_ESTIMATE
|
|
2391
|
+
);
|
|
2392
|
+
const chunks = [];
|
|
2393
|
+
const analyses = [];
|
|
2394
|
+
const pending = [params.payload];
|
|
2395
|
+
while (pending.length > 0) {
|
|
2396
|
+
const currentPayload = pending.shift();
|
|
2397
|
+
const analysis = analyzePreparedTextSendPayload({
|
|
2398
|
+
payload: currentPayload,
|
|
2399
|
+
threadType: params.threadType,
|
|
2400
|
+
threadId: params.threadId
|
|
2401
|
+
});
|
|
2402
|
+
if (isTextSendPayloadWithinDeliveryLimits(analysis, {
|
|
2403
|
+
maxMessageLength,
|
|
2404
|
+
maxRequestParamsLengthEstimate
|
|
2405
|
+
})) {
|
|
2406
|
+
chunks.push(currentPayload);
|
|
2407
|
+
analyses.push(analysis);
|
|
2408
|
+
continue;
|
|
2409
|
+
}
|
|
2410
|
+
const targetLength = computeNextChunkLength(analysis, {
|
|
2411
|
+
maxMessageLength,
|
|
2412
|
+
maxRequestParamsLengthEstimate
|
|
2413
|
+
});
|
|
2414
|
+
const splitChunks = splitTextSendPayload(currentPayload, targetLength);
|
|
2415
|
+
if (splitChunks.length <= 1) {
|
|
2416
|
+
throw new Error(
|
|
2417
|
+
`Unable to split formatted text payload into deliverable chunks within ${targetLength} characters.`
|
|
2418
|
+
);
|
|
2419
|
+
}
|
|
2420
|
+
pending.unshift(...splitChunks);
|
|
2421
|
+
}
|
|
2422
|
+
return { chunks, analyses };
|
|
2423
|
+
}
|
|
2424
|
+
function splitTextSendPayload(payload, maxLength = ZALO_TEXT_MESSAGE_MAX_LENGTH) {
|
|
2425
|
+
if (!Number.isInteger(maxLength) || maxLength <= 0) {
|
|
2426
|
+
throw new Error("Text chunk size must be a positive integer");
|
|
2427
|
+
}
|
|
2428
|
+
const payloadObject = normalizeTextSendPayload(payload);
|
|
2429
|
+
if (payloadObject.msg.length <= maxLength) {
|
|
2430
|
+
return [payload];
|
|
2431
|
+
}
|
|
2432
|
+
const chunks = [];
|
|
2433
|
+
let start = 0;
|
|
2434
|
+
while (start < payloadObject.msg.length) {
|
|
2435
|
+
const end = findChunkEnd(payloadObject, start, maxLength);
|
|
2436
|
+
chunks.push(sliceTextSendPayload(payloadObject, start, end));
|
|
2437
|
+
start = end;
|
|
2438
|
+
}
|
|
2439
|
+
return chunks;
|
|
2440
|
+
}
|
|
2441
|
+
function analyzePreparedTextSendPayload(params) {
|
|
2442
|
+
const payloadObject = normalizeTextSendPayload(params.payload);
|
|
2443
|
+
const textProperties = buildTextProperties(payloadObject.styles);
|
|
2444
|
+
const mentionInfo = buildMentionInfo(
|
|
2445
|
+
params.threadType,
|
|
2446
|
+
payloadObject.msg,
|
|
2447
|
+
payloadObject.mentions
|
|
2448
|
+
);
|
|
2449
|
+
const requestParams = omitUndefined({
|
|
2450
|
+
message: payloadObject.msg,
|
|
2451
|
+
clientId: 17e11,
|
|
2452
|
+
mentionInfo,
|
|
2453
|
+
imei: params.threadType === ThreadType.Group ? void 0 : "000000000000000",
|
|
2454
|
+
ttl: 0,
|
|
2455
|
+
visibility: params.threadType === ThreadType.Group ? 0 : void 0,
|
|
2456
|
+
toid: params.threadType === ThreadType.Group ? void 0 : params.threadId,
|
|
2457
|
+
grid: params.threadType === ThreadType.Group ? params.threadId : void 0,
|
|
2458
|
+
textProperties
|
|
2459
|
+
});
|
|
2460
|
+
return {
|
|
2461
|
+
payload: params.payload,
|
|
2462
|
+
...buildTextSendPayloadAnalysis({
|
|
2463
|
+
payloadObject,
|
|
2464
|
+
rawInputLength: payloadObject.msg.length,
|
|
2465
|
+
textProperties,
|
|
2466
|
+
mentionInfo,
|
|
2467
|
+
requestParamsLengthEstimate: JSON.stringify(requestParams).length,
|
|
2468
|
+
threadType: params.threadType
|
|
2469
|
+
})
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2349
2472
|
async function resolveGroupMentionsIfNeeded(params, text) {
|
|
2350
2473
|
if (params.threadType !== ThreadType.Group) {
|
|
2351
2474
|
return void 0;
|
|
@@ -2360,6 +2483,208 @@ async function resolveGroupMentionsIfNeeded(params, text) {
|
|
|
2360
2483
|
const mentions = resolveOutboundGroupMentions(text, members);
|
|
2361
2484
|
return mentions.length > 0 ? mentions : void 0;
|
|
2362
2485
|
}
|
|
2486
|
+
function normalizeTextSendPayload(payload) {
|
|
2487
|
+
if (typeof payload === "string") {
|
|
2488
|
+
return { msg: payload };
|
|
2489
|
+
}
|
|
2490
|
+
return payload;
|
|
2491
|
+
}
|
|
2492
|
+
function buildTextSendPayloadAnalysis(params) {
|
|
2493
|
+
return {
|
|
2494
|
+
payloadObject: params.payloadObject,
|
|
2495
|
+
rawInputLength: params.rawInputLength,
|
|
2496
|
+
renderedTextLength: params.payloadObject.msg.length,
|
|
2497
|
+
styleCount: params.payloadObject.styles?.length ?? 0,
|
|
2498
|
+
mentionCount: params.payloadObject.mentions?.length ?? 0,
|
|
2499
|
+
textPropertiesLength: params.textProperties?.length ?? 0,
|
|
2500
|
+
mentionInfoLength: params.mentionInfo?.length ?? 0,
|
|
2501
|
+
requestParamsLengthEstimate: params.requestParamsLengthEstimate,
|
|
2502
|
+
sendPath: params.threadType === ThreadType.Group ? params.mentionInfo ? "mention" : "sendmsg" : "sms"
|
|
2503
|
+
};
|
|
2504
|
+
}
|
|
2505
|
+
function isTextSendPayloadWithinDeliveryLimits(analysis, limits) {
|
|
2506
|
+
return analysis.renderedTextLength <= limits.maxMessageLength && analysis.requestParamsLengthEstimate <= limits.maxRequestParamsLengthEstimate;
|
|
2507
|
+
}
|
|
2508
|
+
function computeNextChunkLength(analysis, limits) {
|
|
2509
|
+
const currentLength = analysis.renderedTextLength;
|
|
2510
|
+
const targetLengths = [limits.maxMessageLength, currentLength - 1].filter((value) => value > 0);
|
|
2511
|
+
if (analysis.requestParamsLengthEstimate > limits.maxRequestParamsLengthEstimate) {
|
|
2512
|
+
targetLengths.push(
|
|
2513
|
+
Math.floor(
|
|
2514
|
+
currentLength * limits.maxRequestParamsLengthEstimate / analysis.requestParamsLengthEstimate
|
|
2515
|
+
)
|
|
2516
|
+
);
|
|
2517
|
+
}
|
|
2518
|
+
const targetLength = Math.max(
|
|
2519
|
+
1,
|
|
2520
|
+
Math.min(...targetLengths.filter((value) => Number.isFinite(value) && value > 0))
|
|
2521
|
+
);
|
|
2522
|
+
return Math.min(targetLength, currentLength - 1);
|
|
2523
|
+
}
|
|
2524
|
+
function resolvePositiveLimit(value, fallback) {
|
|
2525
|
+
if (!value || !Number.isInteger(value) || value <= 0) {
|
|
2526
|
+
return fallback;
|
|
2527
|
+
}
|
|
2528
|
+
return value;
|
|
2529
|
+
}
|
|
2530
|
+
function sliceTextSendPayload(payloadObject, start, end) {
|
|
2531
|
+
const msg2 = payloadObject.msg.slice(start, end);
|
|
2532
|
+
const styles = sliceStyles(payloadObject.styles, start, end);
|
|
2533
|
+
const mentions = sliceMentions(payloadObject.mentions, start, end);
|
|
2534
|
+
if (!styles && !mentions) {
|
|
2535
|
+
return msg2;
|
|
2536
|
+
}
|
|
2537
|
+
return omitUndefined({
|
|
2538
|
+
msg: msg2,
|
|
2539
|
+
styles,
|
|
2540
|
+
mentions
|
|
2541
|
+
});
|
|
2542
|
+
}
|
|
2543
|
+
function sliceStyles(styles, start, end) {
|
|
2544
|
+
if (!styles || styles.length === 0) {
|
|
2545
|
+
return void 0;
|
|
2546
|
+
}
|
|
2547
|
+
const sliced = [];
|
|
2548
|
+
for (const style of styles) {
|
|
2549
|
+
const styleStart = style.start;
|
|
2550
|
+
const styleEnd = style.start + style.len;
|
|
2551
|
+
const overlapStart = Math.max(styleStart, start);
|
|
2552
|
+
const overlapEnd = Math.min(styleEnd, end);
|
|
2553
|
+
if (overlapStart >= overlapEnd) {
|
|
2554
|
+
continue;
|
|
2555
|
+
}
|
|
2556
|
+
if (style.st === "ind_$") {
|
|
2557
|
+
sliced.push({
|
|
2558
|
+
start: overlapStart - start,
|
|
2559
|
+
len: overlapEnd - overlapStart,
|
|
2560
|
+
st: style.st,
|
|
2561
|
+
indentSize: style.indentSize
|
|
2562
|
+
});
|
|
2563
|
+
continue;
|
|
2564
|
+
}
|
|
2565
|
+
sliced.push({
|
|
2566
|
+
start: overlapStart - start,
|
|
2567
|
+
len: overlapEnd - overlapStart,
|
|
2568
|
+
st: style.st
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2571
|
+
return sliced.length > 0 ? sliced : void 0;
|
|
2572
|
+
}
|
|
2573
|
+
function sliceMentions(mentions, start, end) {
|
|
2574
|
+
if (!mentions || mentions.length === 0) {
|
|
2575
|
+
return void 0;
|
|
2576
|
+
}
|
|
2577
|
+
const sliced = mentions.filter((mention) => mention.pos >= start && mention.pos + mention.len <= end).map((mention) => ({
|
|
2578
|
+
pos: mention.pos - start,
|
|
2579
|
+
uid: mention.uid,
|
|
2580
|
+
len: mention.len
|
|
2581
|
+
}));
|
|
2582
|
+
return sliced.length > 0 ? sliced : void 0;
|
|
2583
|
+
}
|
|
2584
|
+
function findChunkEnd(payloadObject, start, maxLength) {
|
|
2585
|
+
const remaining = payloadObject.msg.length - start;
|
|
2586
|
+
if (remaining <= maxLength) {
|
|
2587
|
+
return payloadObject.msg.length;
|
|
2588
|
+
}
|
|
2589
|
+
const maxEnd = start + maxLength;
|
|
2590
|
+
const newlineBreak = findPreferredBreak(payloadObject, start, maxEnd, "\n");
|
|
2591
|
+
if (newlineBreak > start) {
|
|
2592
|
+
return newlineBreak;
|
|
2593
|
+
}
|
|
2594
|
+
const whitespaceBreak = findWhitespaceBreak(payloadObject, start, maxEnd);
|
|
2595
|
+
if (whitespaceBreak > start) {
|
|
2596
|
+
return whitespaceBreak;
|
|
2597
|
+
}
|
|
2598
|
+
for (let cursor = maxEnd; cursor > start; cursor -= 1) {
|
|
2599
|
+
if (isSafeSplitPosition(payloadObject.mentions, cursor)) {
|
|
2600
|
+
return cursor;
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
throw new Error(
|
|
2604
|
+
`Unable to split text payload safely within ${maxLength} characters.`
|
|
2605
|
+
);
|
|
2606
|
+
}
|
|
2607
|
+
function findPreferredBreak(payloadObject, start, maxEnd, marker) {
|
|
2608
|
+
for (let cursor = maxEnd; cursor > start; cursor -= 1) {
|
|
2609
|
+
if (!isSafeSplitPosition(payloadObject.mentions, cursor)) {
|
|
2610
|
+
continue;
|
|
2611
|
+
}
|
|
2612
|
+
if (payloadObject.msg[cursor - 1] === marker) {
|
|
2613
|
+
return cursor;
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
return start;
|
|
2617
|
+
}
|
|
2618
|
+
function findWhitespaceBreak(payloadObject, start, maxEnd) {
|
|
2619
|
+
for (let cursor = maxEnd; cursor > start; cursor -= 1) {
|
|
2620
|
+
if (!isSafeSplitPosition(payloadObject.mentions, cursor)) {
|
|
2621
|
+
continue;
|
|
2622
|
+
}
|
|
2623
|
+
const previousChar = payloadObject.msg[cursor - 1];
|
|
2624
|
+
if (previousChar === " " || previousChar === " ") {
|
|
2625
|
+
return cursor;
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
return start;
|
|
2629
|
+
}
|
|
2630
|
+
function isSafeSplitPosition(mentions, position) {
|
|
2631
|
+
if (!mentions || mentions.length === 0) {
|
|
2632
|
+
return true;
|
|
2633
|
+
}
|
|
2634
|
+
return mentions.every((mention) => {
|
|
2635
|
+
const mentionEnd = mention.pos + mention.len;
|
|
2636
|
+
return position <= mention.pos || position >= mentionEnd;
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
function buildTextProperties(styles) {
|
|
2640
|
+
if (!styles || styles.length === 0) {
|
|
2641
|
+
return void 0;
|
|
2642
|
+
}
|
|
2643
|
+
return JSON.stringify({
|
|
2644
|
+
styles: styles.map((style) => {
|
|
2645
|
+
if (style.st === "ind_$") {
|
|
2646
|
+
return omitUndefined({
|
|
2647
|
+
start: style.start,
|
|
2648
|
+
len: style.len,
|
|
2649
|
+
st: `ind_${style.indentSize ?? 1}0`
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2652
|
+
return {
|
|
2653
|
+
start: style.start,
|
|
2654
|
+
len: style.len,
|
|
2655
|
+
st: style.st
|
|
2656
|
+
};
|
|
2657
|
+
}),
|
|
2658
|
+
ver: 0
|
|
2659
|
+
});
|
|
2660
|
+
}
|
|
2661
|
+
function buildMentionInfo(threadType, msg2, mentions) {
|
|
2662
|
+
if (threadType !== ThreadType.Group || !mentions || mentions.length === 0) {
|
|
2663
|
+
return void 0;
|
|
2664
|
+
}
|
|
2665
|
+
let totalMentionLen = 0;
|
|
2666
|
+
const mentionsFinal = mentions.filter((mention) => mention.pos >= 0 && Boolean(mention.uid) && mention.len > 0).map((mention) => {
|
|
2667
|
+
totalMentionLen += mention.len;
|
|
2668
|
+
return {
|
|
2669
|
+
pos: mention.pos,
|
|
2670
|
+
uid: mention.uid,
|
|
2671
|
+
len: mention.len,
|
|
2672
|
+
type: mention.uid === "-1" ? 1 : 0
|
|
2673
|
+
};
|
|
2674
|
+
});
|
|
2675
|
+
if (totalMentionLen > msg2.length) {
|
|
2676
|
+
throw new Error("Invalid mentions: total mention characters exceed message length");
|
|
2677
|
+
}
|
|
2678
|
+
if (mentionsFinal.length === 0) {
|
|
2679
|
+
return void 0;
|
|
2680
|
+
}
|
|
2681
|
+
return JSON.stringify(mentionsFinal);
|
|
2682
|
+
}
|
|
2683
|
+
function omitUndefined(value) {
|
|
2684
|
+
return Object.fromEntries(
|
|
2685
|
+
Object.entries(value).filter(([, entry]) => entry !== void 0)
|
|
2686
|
+
);
|
|
2687
|
+
}
|
|
2363
2688
|
|
|
2364
2689
|
// src/lib/video-send.ts
|
|
2365
2690
|
import { execFile } from "child_process";
|
|
@@ -6435,24 +6760,90 @@ msg.command("send <threadId> <message>").option("-g, --group", "Send to group").
|
|
|
6435
6760
|
...typeof textPayload === "string" ? { msg: textPayload } : textPayload,
|
|
6436
6761
|
...quote ? { quote } : {}
|
|
6437
6762
|
} : textPayload;
|
|
6438
|
-
const
|
|
6763
|
+
const deliveryPlan = planTextSendPayloadsForDelivery({
|
|
6764
|
+
payload: textPayload,
|
|
6765
|
+
threadType,
|
|
6766
|
+
threadId,
|
|
6767
|
+
maxMessageLength: parsePositiveIntFromEnv(
|
|
6768
|
+
"OPENZCA_TEXT_MESSAGE_MAX_LENGTH",
|
|
6769
|
+
ZALO_TEXT_MESSAGE_MAX_LENGTH
|
|
6770
|
+
),
|
|
6771
|
+
maxRequestParamsLengthEstimate: parsePositiveIntFromEnv(
|
|
6772
|
+
"OPENZCA_TEXT_REQUEST_PARAMS_MAX_ESTIMATE",
|
|
6773
|
+
ZALO_TEXT_REQUEST_PARAMS_MAX_ESTIMATE
|
|
6774
|
+
)
|
|
6775
|
+
});
|
|
6776
|
+
const payloadChunks = deliveryPlan.chunks;
|
|
6777
|
+
const responses = [];
|
|
6778
|
+
const sentPayloads = [];
|
|
6779
|
+
for (let index = 0; index < payloadChunks.length; index += 1) {
|
|
6780
|
+
const chunk = payloadChunks[index];
|
|
6781
|
+
const chunkPayload = quote && index === 0 ? {
|
|
6782
|
+
...typeof chunk === "string" ? { msg: chunk } : chunk,
|
|
6783
|
+
quote
|
|
6784
|
+
} : chunk;
|
|
6785
|
+
sentPayloads.push(chunkPayload);
|
|
6786
|
+
responses.push(await api.sendMessage(chunkPayload, threadId, threadType));
|
|
6787
|
+
}
|
|
6788
|
+
const response = responses.length === 1 ? responses[0] : {
|
|
6789
|
+
chunked: true,
|
|
6790
|
+
chunkCount: responses.length,
|
|
6791
|
+
msgId: responses.at(-1)?.message?.msgId?.toString(),
|
|
6792
|
+
response: responses
|
|
6793
|
+
};
|
|
6439
6794
|
output(response, false);
|
|
6440
6795
|
if (await shouldWriteToDb(profile)) {
|
|
6441
6796
|
scheduleDbWrite(profile, command, "msg.send.db.persist_error", async () => {
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6797
|
+
if (payloadChunks.length === 1) {
|
|
6798
|
+
await persistOutgoingMessageBestEffort({
|
|
6799
|
+
profile,
|
|
6800
|
+
api,
|
|
6801
|
+
threadId,
|
|
6802
|
+
group: opts.group,
|
|
6803
|
+
text: message,
|
|
6804
|
+
msgType: "text",
|
|
6805
|
+
response,
|
|
6806
|
+
rawPayload: payload
|
|
6807
|
+
});
|
|
6808
|
+
return;
|
|
6809
|
+
}
|
|
6810
|
+
for (let index = 0; index < payloadChunks.length; index += 1) {
|
|
6811
|
+
const chunk = sentPayloads[index];
|
|
6812
|
+
const chunkText = typeof chunk === "string" ? chunk : chunk.msg;
|
|
6813
|
+
await persistOutgoingMessageBestEffort({
|
|
6814
|
+
profile,
|
|
6815
|
+
api,
|
|
6816
|
+
threadId,
|
|
6817
|
+
group: opts.group,
|
|
6818
|
+
text: chunkText,
|
|
6819
|
+
msgType: "text",
|
|
6820
|
+
response: responses[index],
|
|
6821
|
+
rawPayload: chunk
|
|
6822
|
+
});
|
|
6823
|
+
}
|
|
6452
6824
|
});
|
|
6453
6825
|
}
|
|
6454
6826
|
})
|
|
6455
6827
|
);
|
|
6828
|
+
msg.command("analyze-text <threadId> <message>").option("-g, --group", "Analyze as group text").option("--raw", "Analyze raw text without parsing formatting markers").option("-j, --json", "JSON output").description("Build and analyze the exact text payload that msg send would hand to zca-js. Useful for pre-send chunking/debugging.").action(
|
|
6829
|
+
wrapAction(async (threadId, message, opts, command) => {
|
|
6830
|
+
const threadType = asThreadType(opts.group);
|
|
6831
|
+
const mentionProbeText = opts.raw ? message : parseTextStyles(message).text;
|
|
6832
|
+
let listGroupMembers;
|
|
6833
|
+
if (threadType === ThreadType3.Group && hasPotentialOutboundGroupMention(mentionProbeText)) {
|
|
6834
|
+
const { api } = await requireApi(command);
|
|
6835
|
+
listGroupMembers = (groupId) => listGroupMentionMembers(api, groupId);
|
|
6836
|
+
}
|
|
6837
|
+
const analysis = await analyzeTextSendPayload({
|
|
6838
|
+
message,
|
|
6839
|
+
raw: opts.raw,
|
|
6840
|
+
threadType,
|
|
6841
|
+
threadId,
|
|
6842
|
+
listGroupMembers
|
|
6843
|
+
});
|
|
6844
|
+
output(analysis, shouldOutputJson(opts));
|
|
6845
|
+
})
|
|
6846
|
+
);
|
|
6456
6847
|
msg.command("image <threadId> [file]").option("-u, --url <url>", "Image URL (repeatable)", collectValues, []).option("-m, --message <message>", "Caption").option("-g, --group", "Send to group").description("Send image(s) from file or URL").action(
|
|
6457
6848
|
wrapAction(
|
|
6458
6849
|
async (threadId, file, opts, command) => {
|