openzca 0.1.41 → 0.1.43
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 +4 -0
- package/dist/cli.js +236 -97
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,6 +36,9 @@ openzca msg send USER_ID "Hello"
|
|
|
36
36
|
# Send to a group
|
|
37
37
|
openzca msg send GROUP_ID "Hello team" --group
|
|
38
38
|
|
|
39
|
+
# Mention a group member by display name or username
|
|
40
|
+
openzca msg send GROUP_ID "Hi @Alice Nguyen" --group
|
|
41
|
+
|
|
39
42
|
# Listen for incoming messages
|
|
40
43
|
openzca listen
|
|
41
44
|
```
|
|
@@ -80,6 +83,7 @@ You can also open the saved file manually (for example: `open qr.png` on macOS).
|
|
|
80
83
|
|
|
81
84
|
Media commands accept local files, `file://` paths, and repeatable `--url` options. Add `--group` for group threads.
|
|
82
85
|
Local paths using `~` are expanded automatically (for positional file args, `--url`, and `OPENZCA_LISTEN_MEDIA_DIR`).
|
|
86
|
+
Group text sends via `openzca msg send --group` resolve unique `@Name` mentions against the current group member list using 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.
|
|
83
87
|
|
|
84
88
|
### Debug Logging
|
|
85
89
|
|
package/dist/cli.js
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
Gender,
|
|
16
16
|
Reactions,
|
|
17
17
|
ReviewPendingMemberRequestStatus,
|
|
18
|
-
ThreadType
|
|
18
|
+
ThreadType as ThreadType2
|
|
19
19
|
} from "zca-js";
|
|
20
20
|
|
|
21
21
|
// src/lib/store.ts
|
|
@@ -578,6 +578,113 @@ async function assertFilesExist(files) {
|
|
|
578
578
|
}
|
|
579
579
|
}
|
|
580
580
|
|
|
581
|
+
// src/lib/text-send.ts
|
|
582
|
+
import { ThreadType } from "zca-js";
|
|
583
|
+
|
|
584
|
+
// src/lib/group-mentions.ts
|
|
585
|
+
var ALLOWED_START_BOUNDARY_CHARS = /* @__PURE__ */ new Set(["(", "[", "{", "<", '"', ",", ";", ":"]);
|
|
586
|
+
var ALLOWED_END_BOUNDARY_CHARS = /* @__PURE__ */ new Set([",", ";", ":", "!", "?", ")", "]", "}", ">", '"']);
|
|
587
|
+
function resolveOutboundGroupMentions(text, members) {
|
|
588
|
+
if (!text.includes("@") || members.length === 0) {
|
|
589
|
+
return [];
|
|
590
|
+
}
|
|
591
|
+
const candidates = buildCandidates(members);
|
|
592
|
+
if (candidates.length === 0) {
|
|
593
|
+
return [];
|
|
594
|
+
}
|
|
595
|
+
const lowerText = text.toLowerCase();
|
|
596
|
+
const mentions = [];
|
|
597
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
598
|
+
if (text[index] !== "@") continue;
|
|
599
|
+
if (!isMentionStartBoundary(text, index)) continue;
|
|
600
|
+
const match = resolveMentionAtIndex(text, lowerText, index, candidates);
|
|
601
|
+
if (!match) continue;
|
|
602
|
+
mentions.push({
|
|
603
|
+
pos: index,
|
|
604
|
+
len: match.label.length + 1,
|
|
605
|
+
uid: match.userId
|
|
606
|
+
});
|
|
607
|
+
index += match.label.length;
|
|
608
|
+
}
|
|
609
|
+
return mentions;
|
|
610
|
+
}
|
|
611
|
+
function hasPotentialOutboundGroupMention(text) {
|
|
612
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
613
|
+
if (text[index] !== "@") continue;
|
|
614
|
+
if (!isMentionStartBoundary(text, index)) continue;
|
|
615
|
+
const next = text[index + 1];
|
|
616
|
+
if (next && !/\s/u.test(next)) {
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
return false;
|
|
621
|
+
}
|
|
622
|
+
function resolveMentionAtIndex(text, lowerText, atIndex, candidates) {
|
|
623
|
+
const start = atIndex + 1;
|
|
624
|
+
const matches = candidates.filter((candidate) => {
|
|
625
|
+
const end = start + candidate.label.length;
|
|
626
|
+
return lowerText.startsWith(candidate.normalizedLabel, start) && isMentionBoundary(text, start, end);
|
|
627
|
+
});
|
|
628
|
+
if (matches.length === 0) {
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
const longestLength = matches.reduce((max, candidate) => Math.max(max, candidate.label.length), 0);
|
|
632
|
+
const longestMatches = matches.filter((candidate) => candidate.label.length === longestLength);
|
|
633
|
+
const matchedUserIds = [...new Set(longestMatches.map((candidate) => candidate.userId))];
|
|
634
|
+
if (matchedUserIds.length !== 1) {
|
|
635
|
+
const label = text.slice(atIndex, start + longestLength);
|
|
636
|
+
throw new Error(`Ambiguous mention "${label}": matched multiple group members.`);
|
|
637
|
+
}
|
|
638
|
+
return longestMatches[0];
|
|
639
|
+
}
|
|
640
|
+
function buildCandidates(members) {
|
|
641
|
+
const candidates = /* @__PURE__ */ new Map();
|
|
642
|
+
for (const member of members) {
|
|
643
|
+
const userId = member.userId.trim();
|
|
644
|
+
if (!userId) continue;
|
|
645
|
+
for (const rawLabel of [member.displayName, member.zaloName]) {
|
|
646
|
+
const label = rawLabel?.trim();
|
|
647
|
+
if (!label) continue;
|
|
648
|
+
const normalizedLabel = label.toLowerCase();
|
|
649
|
+
const key = `${userId}\0${normalizedLabel}`;
|
|
650
|
+
if (candidates.has(key)) continue;
|
|
651
|
+
candidates.set(key, {
|
|
652
|
+
label,
|
|
653
|
+
normalizedLabel,
|
|
654
|
+
userId
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return [...candidates.values()].sort((left, right) => right.label.length - left.label.length);
|
|
659
|
+
}
|
|
660
|
+
function isMentionBoundary(text, start, end) {
|
|
661
|
+
if (end > text.length) {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
const next = text[end];
|
|
665
|
+
if (!next) {
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
if (/\s/u.test(next)) {
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
if (ALLOWED_END_BOUNDARY_CHARS.has(next)) {
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
674
|
+
if (next === ".") {
|
|
675
|
+
const following = text[end + 1];
|
|
676
|
+
return !following || /\s/u.test(following) || ALLOWED_END_BOUNDARY_CHARS.has(following);
|
|
677
|
+
}
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
function isMentionStartBoundary(text, atIndex) {
|
|
681
|
+
if (atIndex === 0) {
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
684
|
+
const previous = text[atIndex - 1];
|
|
685
|
+
return /\s/u.test(previous) || ALLOWED_START_BOUNDARY_CHARS.has(previous);
|
|
686
|
+
}
|
|
687
|
+
|
|
581
688
|
// src/lib/text-styles.ts
|
|
582
689
|
import { TextStyle } from "zca-js";
|
|
583
690
|
var TAG_STYLE_MAP = {
|
|
@@ -624,7 +731,6 @@ function parseTextStyles(input) {
|
|
|
624
731
|
if (inCodeBlock) {
|
|
625
732
|
const outputLineIndex = processedLines.length;
|
|
626
733
|
codeOutputLineIndices.add(outputLineIndex);
|
|
627
|
-
lineStyles.push({ lineIndex: outputLineIndex, style: TextStyle.Indent, indentSize: 1 });
|
|
628
734
|
processedLines.push(normalizeCodeBlockLeadingWhitespace(line));
|
|
629
735
|
continue;
|
|
630
736
|
}
|
|
@@ -827,6 +933,35 @@ function normalizeCodeBlockLeadingWhitespace(line) {
|
|
|
827
933
|
);
|
|
828
934
|
}
|
|
829
935
|
|
|
936
|
+
// src/lib/text-send.ts
|
|
937
|
+
async function buildTextSendPayload(params) {
|
|
938
|
+
if (params.raw) {
|
|
939
|
+
const mentions2 = await resolveGroupMentionsIfNeeded(params, params.message);
|
|
940
|
+
return mentions2 ? { msg: params.message, mentions: mentions2 } : params.message;
|
|
941
|
+
}
|
|
942
|
+
const { text, styles } = parseTextStyles(params.message);
|
|
943
|
+
const mentions = await resolveGroupMentionsIfNeeded(params, text);
|
|
944
|
+
return {
|
|
945
|
+
msg: text,
|
|
946
|
+
styles: styles.length > 0 ? styles : void 0,
|
|
947
|
+
mentions
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
async function resolveGroupMentionsIfNeeded(params, text) {
|
|
951
|
+
if (params.threadType !== ThreadType.Group) {
|
|
952
|
+
return void 0;
|
|
953
|
+
}
|
|
954
|
+
if (!hasPotentialOutboundGroupMention(text)) {
|
|
955
|
+
return void 0;
|
|
956
|
+
}
|
|
957
|
+
if (!params.listGroupMembers) {
|
|
958
|
+
return void 0;
|
|
959
|
+
}
|
|
960
|
+
const members = await params.listGroupMembers(params.threadId);
|
|
961
|
+
const mentions = resolveOutboundGroupMentions(text, members);
|
|
962
|
+
return mentions.length > 0 ? mentions : void 0;
|
|
963
|
+
}
|
|
964
|
+
|
|
830
965
|
// src/cli.ts
|
|
831
966
|
var require2 = createRequire(import.meta.url);
|
|
832
967
|
var { version: PKG_VERSION } = require2("../package.json");
|
|
@@ -951,7 +1086,7 @@ function output(value, asJson = false) {
|
|
|
951
1086
|
console.log(String(value));
|
|
952
1087
|
}
|
|
953
1088
|
function asThreadType(groupFlag) {
|
|
954
|
-
return groupFlag ?
|
|
1089
|
+
return groupFlag ? ThreadType2.Group : ThreadType2.User;
|
|
955
1090
|
}
|
|
956
1091
|
function parseBooleanFromEnv(name, fallback) {
|
|
957
1092
|
const raw = process.env[name]?.trim();
|
|
@@ -1161,7 +1296,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
|
|
|
1161
1296
|
fail(parsed.requestId, "Invalid upload payload.");
|
|
1162
1297
|
return;
|
|
1163
1298
|
}
|
|
1164
|
-
const threadType = parsed.threadType === "group" ?
|
|
1299
|
+
const threadType = parsed.threadType === "group" ? ThreadType2.Group : ThreadType2.User;
|
|
1165
1300
|
const requestTimeoutMs = parsePositiveIntFromUnknown(parsed.uploadTimeoutMs) ?? uploadTimeoutMs;
|
|
1166
1301
|
writeDebugLine(
|
|
1167
1302
|
"listen.ipc.upload.start",
|
|
@@ -1318,7 +1453,7 @@ async function tryUploadViaListenerIpc(profile, threadId, threadType, attachment
|
|
|
1318
1453
|
{
|
|
1319
1454
|
profile,
|
|
1320
1455
|
threadId,
|
|
1321
|
-
threadType: threadType ===
|
|
1456
|
+
threadType: threadType === ThreadType2.Group ? "group" : "user",
|
|
1322
1457
|
attachmentCount: attachments.length,
|
|
1323
1458
|
socketPath,
|
|
1324
1459
|
requestId,
|
|
@@ -1362,7 +1497,7 @@ async function tryUploadViaListenerIpc(profile, threadId, threadType, attachment
|
|
|
1362
1497
|
requestId,
|
|
1363
1498
|
profile,
|
|
1364
1499
|
threadId,
|
|
1365
|
-
threadType: threadType ===
|
|
1500
|
+
threadType: threadType === ThreadType2.Group ? "group" : "user",
|
|
1366
1501
|
attachments
|
|
1367
1502
|
};
|
|
1368
1503
|
socket.write(`${JSON.stringify(payload)}
|
|
@@ -1424,21 +1559,21 @@ async function tryUploadViaListenerIpc(profile, threadId, threadType, attachment
|
|
|
1424
1559
|
}
|
|
1425
1560
|
async function resolveUploadThreadType(api, profile, threadId, groupFlag, command) {
|
|
1426
1561
|
if (groupFlag) {
|
|
1427
|
-
return { type:
|
|
1562
|
+
return { type: ThreadType2.Group, reason: "explicit_group_flag" };
|
|
1428
1563
|
}
|
|
1429
1564
|
const autoDetectEnabled = parseBooleanFromEnv("OPENZCA_UPLOAD_AUTO_THREAD_TYPE", false);
|
|
1430
1565
|
if (!autoDetectEnabled) {
|
|
1431
|
-
return { type:
|
|
1566
|
+
return { type: ThreadType2.User, reason: "auto_detect_disabled" };
|
|
1432
1567
|
}
|
|
1433
1568
|
try {
|
|
1434
1569
|
const cache = await readCache(profile);
|
|
1435
1570
|
const groupIds = collectIdsFromCacheEntries(cache.groups, ["groupId", "grid", "threadId", "id"]);
|
|
1436
1571
|
if (groupIds.has(threadId)) {
|
|
1437
|
-
return { type:
|
|
1572
|
+
return { type: ThreadType2.Group, reason: "cache_group_match" };
|
|
1438
1573
|
}
|
|
1439
1574
|
const friendIds = collectIdsFromCacheEntries(cache.friends, ["userId", "uid", "id", "threadId"]);
|
|
1440
1575
|
if (friendIds.has(threadId)) {
|
|
1441
|
-
return { type:
|
|
1576
|
+
return { type: ThreadType2.User, reason: "cache_friend_match" };
|
|
1442
1577
|
}
|
|
1443
1578
|
} catch (error) {
|
|
1444
1579
|
writeDebugLine(
|
|
@@ -1453,7 +1588,7 @@ async function resolveUploadThreadType(api, profile, threadId, groupFlag, comman
|
|
|
1453
1588
|
}
|
|
1454
1589
|
const probeEnabled = parseBooleanFromEnv("OPENZCA_UPLOAD_GROUP_PROBE", true);
|
|
1455
1590
|
if (!probeEnabled) {
|
|
1456
|
-
return { type:
|
|
1591
|
+
return { type: ThreadType2.User, reason: "probe_disabled" };
|
|
1457
1592
|
}
|
|
1458
1593
|
const probeTimeoutMs = parsePositiveIntFromEnv("OPENZCA_UPLOAD_GROUP_PROBE_TIMEOUT_MS", 5e3);
|
|
1459
1594
|
try {
|
|
@@ -1463,7 +1598,7 @@ async function resolveUploadThreadType(api, profile, threadId, groupFlag, comman
|
|
|
1463
1598
|
`Timed out waiting ${probeTimeoutMs}ms while probing group thread type.`
|
|
1464
1599
|
);
|
|
1465
1600
|
if (groupInfo?.gridInfoMap?.[threadId]) {
|
|
1466
|
-
return { type:
|
|
1601
|
+
return { type: ThreadType2.Group, reason: "probe_group_match" };
|
|
1467
1602
|
}
|
|
1468
1603
|
} catch (error) {
|
|
1469
1604
|
writeDebugLine(
|
|
@@ -1476,7 +1611,7 @@ async function resolveUploadThreadType(api, profile, threadId, groupFlag, comman
|
|
|
1476
1611
|
command
|
|
1477
1612
|
);
|
|
1478
1613
|
}
|
|
1479
|
-
return { type:
|
|
1614
|
+
return { type: ThreadType2.User, reason: "default_user" };
|
|
1480
1615
|
}
|
|
1481
1616
|
function parseReaction(input) {
|
|
1482
1617
|
const normalized = input.trim();
|
|
@@ -1566,6 +1701,64 @@ async function buildGroupsDetailed(api) {
|
|
|
1566
1701
|
const info = await api.getGroupInfo(ids);
|
|
1567
1702
|
return ids.map((id) => info.gridInfoMap?.[id]).filter((item) => Boolean(item));
|
|
1568
1703
|
}
|
|
1704
|
+
function normalizeGroupMemberId(value) {
|
|
1705
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1706
|
+
return String(Math.trunc(value));
|
|
1707
|
+
}
|
|
1708
|
+
if (typeof value !== "string") return "";
|
|
1709
|
+
const trimmed = value.trim();
|
|
1710
|
+
if (!trimmed) return "";
|
|
1711
|
+
return trimmed.replace(/_\d+$/, "");
|
|
1712
|
+
}
|
|
1713
|
+
async function listGroupMemberRows(api, groupId) {
|
|
1714
|
+
const info = await api.getGroupInfo(groupId);
|
|
1715
|
+
const groupInfo = info.gridInfoMap[groupId];
|
|
1716
|
+
if (!groupInfo) {
|
|
1717
|
+
throw new Error(`Group not found: ${groupId}`);
|
|
1718
|
+
}
|
|
1719
|
+
const idsFromMemberIds = Array.isArray(groupInfo.memberIds) ? groupInfo.memberIds.map((id) => normalizeGroupMemberId(id)).filter(Boolean) : [];
|
|
1720
|
+
const memVerList = groupInfo.memVerList;
|
|
1721
|
+
const idsFromMemVerList = Array.isArray(memVerList) ? memVerList.map((id) => normalizeGroupMemberId(id)).filter(Boolean) : [];
|
|
1722
|
+
const currentMems = Array.isArray(groupInfo.currentMems) ? groupInfo.currentMems : [];
|
|
1723
|
+
const currentMemberMap = /* @__PURE__ */ new Map();
|
|
1724
|
+
for (const member of currentMems) {
|
|
1725
|
+
const userId = normalizeGroupMemberId(member.id);
|
|
1726
|
+
if (!userId) continue;
|
|
1727
|
+
currentMemberMap.set(userId, {
|
|
1728
|
+
displayName: member.dName?.trim() || member.zaloName?.trim() || "",
|
|
1729
|
+
zaloName: member.zaloName?.trim() || ""
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
const ids = Array.from(
|
|
1733
|
+
/* @__PURE__ */ new Set([
|
|
1734
|
+
...idsFromMemberIds,
|
|
1735
|
+
...idsFromMemVerList,
|
|
1736
|
+
...Array.from(currentMemberMap.keys())
|
|
1737
|
+
])
|
|
1738
|
+
);
|
|
1739
|
+
const profiles = ids.length > 0 ? await api.getGroupMembersInfo(ids) : { profiles: {} };
|
|
1740
|
+
const rawProfileMap = profiles.profiles;
|
|
1741
|
+
const profileMap = /* @__PURE__ */ new Map();
|
|
1742
|
+
for (const [key, profile] of Object.entries(rawProfileMap)) {
|
|
1743
|
+
if (!profile) continue;
|
|
1744
|
+
const normalizedKey = normalizeGroupMemberId(key);
|
|
1745
|
+
if (normalizedKey && !profileMap.has(normalizedKey)) {
|
|
1746
|
+
profileMap.set(normalizedKey, profile);
|
|
1747
|
+
}
|
|
1748
|
+
const profileId = normalizeGroupMemberId(profile.id);
|
|
1749
|
+
if (profileId && !profileMap.has(profileId)) {
|
|
1750
|
+
profileMap.set(profileId, profile);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
return ids.map((id) => ({
|
|
1754
|
+
userId: id,
|
|
1755
|
+
displayName: profileMap.get(id)?.displayName ?? currentMemberMap.get(id)?.displayName ?? "",
|
|
1756
|
+
zaloName: profileMap.get(id)?.zaloName ?? currentMemberMap.get(id)?.zaloName ?? ""
|
|
1757
|
+
}));
|
|
1758
|
+
}
|
|
1759
|
+
async function listGroupMentionMembers(api, threadId) {
|
|
1760
|
+
return await listGroupMemberRows(api, threadId);
|
|
1761
|
+
}
|
|
1569
1762
|
async function refreshCacheForProfile(profile, api) {
|
|
1570
1763
|
const [friends, groups] = await Promise.all([
|
|
1571
1764
|
api.getAllFriends(),
|
|
@@ -1883,7 +2076,7 @@ function normalizeGroupHistoryMessages(messages, fallbackThreadId) {
|
|
|
1883
2076
|
const threadIdRaw = String(raw.idTo ?? "").trim();
|
|
1884
2077
|
normalized.push({
|
|
1885
2078
|
threadId: threadIdRaw || fallbackThreadId,
|
|
1886
|
-
type:
|
|
2079
|
+
type: ThreadType2.Group,
|
|
1887
2080
|
data: {
|
|
1888
2081
|
actionId: typeof raw.actionId === "string" && raw.actionId.trim() ? raw.actionId : void 0,
|
|
1889
2082
|
msgId: String(raw.msgId ?? ""),
|
|
@@ -1976,7 +2169,7 @@ async function fetchRecentGroupMessagesViaListener(api, threadId, count) {
|
|
|
1976
2169
|
requestedCursors.add(cursor);
|
|
1977
2170
|
}
|
|
1978
2171
|
pagesRequested += 1;
|
|
1979
|
-
api.listener.requestOldMessages(
|
|
2172
|
+
api.listener.requestOldMessages(ThreadType2.Group, cursor || null);
|
|
1980
2173
|
return true;
|
|
1981
2174
|
};
|
|
1982
2175
|
const cleanup = () => {
|
|
@@ -2008,7 +2201,7 @@ async function fetchRecentGroupMessagesViaListener(api, threadId, count) {
|
|
|
2008
2201
|
}
|
|
2009
2202
|
};
|
|
2010
2203
|
const onOldMessages = (messages, type) => {
|
|
2011
|
-
if (type !==
|
|
2204
|
+
if (type !== ThreadType2.Group) return;
|
|
2012
2205
|
const typedMessages = messages;
|
|
2013
2206
|
for (const message of typedMessages) {
|
|
2014
2207
|
if (message.threadId === threadId) {
|
|
@@ -2084,7 +2277,7 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
|
|
|
2084
2277
|
requestedCursors.add(cursor);
|
|
2085
2278
|
}
|
|
2086
2279
|
pagesRequested += 1;
|
|
2087
|
-
api.listener.requestOldMessages(
|
|
2280
|
+
api.listener.requestOldMessages(ThreadType2.User, cursor || null);
|
|
2088
2281
|
return true;
|
|
2089
2282
|
};
|
|
2090
2283
|
const cleanup = () => {
|
|
@@ -2116,7 +2309,7 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
|
|
|
2116
2309
|
}
|
|
2117
2310
|
};
|
|
2118
2311
|
const onOldMessages = (messages, type) => {
|
|
2119
|
-
if (type !==
|
|
2312
|
+
if (type !== ThreadType2.User) return;
|
|
2120
2313
|
const typedMessages = messages;
|
|
2121
2314
|
for (const message of typedMessages) {
|
|
2122
2315
|
if (message.threadId === threadId) {
|
|
@@ -3136,21 +3329,19 @@ auth.command("cache-clear").description("Clear local cache").action(
|
|
|
3136
3329
|
})
|
|
3137
3330
|
);
|
|
3138
3331
|
var msg = program.command("msg").description("Messaging commands");
|
|
3139
|
-
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)").action(
|
|
3332
|
+
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 mentions.").action(
|
|
3140
3333
|
wrapAction(async (threadId, message, opts, command) => {
|
|
3141
3334
|
const { api } = await requireApi(command);
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
output(response, false);
|
|
3153
|
-
}
|
|
3335
|
+
const threadType = asThreadType(opts.group);
|
|
3336
|
+
const payload = await buildTextSendPayload({
|
|
3337
|
+
message,
|
|
3338
|
+
raw: opts.raw,
|
|
3339
|
+
threadType,
|
|
3340
|
+
threadId,
|
|
3341
|
+
listGroupMembers: threadType === ThreadType2.Group ? (groupId) => listGroupMentionMembers(api, groupId) : void 0
|
|
3342
|
+
});
|
|
3343
|
+
const response = await api.sendMessage(payload, threadId, threadType);
|
|
3344
|
+
output(response, false);
|
|
3154
3345
|
})
|
|
3155
3346
|
);
|
|
3156
3347
|
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(
|
|
@@ -3444,8 +3635,8 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
|
|
|
3444
3635
|
{
|
|
3445
3636
|
threadId,
|
|
3446
3637
|
explicitGroupFlag: Boolean(opts.group),
|
|
3447
|
-
isGroup: threadResolution.type ===
|
|
3448
|
-
threadType: threadResolution.type ===
|
|
3638
|
+
isGroup: threadResolution.type === ThreadType2.Group,
|
|
3639
|
+
threadType: threadResolution.type === ThreadType2.Group ? "group" : "user",
|
|
3449
3640
|
threadTypeReason: threadResolution.reason,
|
|
3450
3641
|
localFiles,
|
|
3451
3642
|
urlInputs
|
|
@@ -3473,7 +3664,7 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
|
|
|
3473
3664
|
"msg.upload.ipc.done",
|
|
3474
3665
|
{
|
|
3475
3666
|
threadId,
|
|
3476
|
-
threadType: threadResolution.type ===
|
|
3667
|
+
threadType: threadResolution.type === ThreadType2.Group ? "group" : "user"
|
|
3477
3668
|
},
|
|
3478
3669
|
command
|
|
3479
3670
|
);
|
|
@@ -3484,7 +3675,7 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
|
|
|
3484
3675
|
"msg.upload.ipc.fallback",
|
|
3485
3676
|
{
|
|
3486
3677
|
threadId,
|
|
3487
|
-
threadType: threadResolution.type ===
|
|
3678
|
+
threadType: threadResolution.type === ThreadType2.Group ? "group" : "user",
|
|
3488
3679
|
reason: ipcResult.reason
|
|
3489
3680
|
},
|
|
3490
3681
|
command
|
|
@@ -3523,7 +3714,7 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
|
|
|
3523
3714
|
const { api } = await requireApi(command);
|
|
3524
3715
|
const parsedCount = Number(opts.count);
|
|
3525
3716
|
const count = Number.isFinite(parsedCount) ? Math.min(Math.max(Math.trunc(parsedCount), 1), 200) : 20;
|
|
3526
|
-
const threadType = opts.group ?
|
|
3717
|
+
const threadType = opts.group ? ThreadType2.Group : ThreadType2.User;
|
|
3527
3718
|
const messages = opts.group ? await fetchRecentGroupMessagesViaApi(api, threadId, count) : await fetchRecentUserMessagesViaListener(
|
|
3528
3719
|
api,
|
|
3529
3720
|
threadId,
|
|
@@ -3533,7 +3724,7 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
|
|
|
3533
3724
|
msgId: message.data.msgId,
|
|
3534
3725
|
cliMsgId: message.data.cliMsgId,
|
|
3535
3726
|
threadId: message.threadId || threadId,
|
|
3536
|
-
threadType: message.type ===
|
|
3727
|
+
threadType: message.type === ThreadType2.Group ? "group" : "user",
|
|
3537
3728
|
senderId: message.data.uidFrom,
|
|
3538
3729
|
senderName: message.data.dName,
|
|
3539
3730
|
ts: message.data.ts,
|
|
@@ -3542,7 +3733,7 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
|
|
|
3542
3733
|
msgId: message.data.msgId,
|
|
3543
3734
|
cliMsgId: message.data.cliMsgId,
|
|
3544
3735
|
threadId: message.threadId || threadId,
|
|
3545
|
-
group: message.type ===
|
|
3736
|
+
group: message.type === ThreadType2.Group
|
|
3546
3737
|
},
|
|
3547
3738
|
content: typeof message.data.content === "string" ? message.data.content : JSON.stringify(message.data.content)
|
|
3548
3739
|
}));
|
|
@@ -3550,7 +3741,7 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
|
|
|
3550
3741
|
output(
|
|
3551
3742
|
{
|
|
3552
3743
|
threadId,
|
|
3553
|
-
threadType: threadType ===
|
|
3744
|
+
threadType: threadType === ThreadType2.Group ? "group" : "user",
|
|
3554
3745
|
count: rows.length,
|
|
3555
3746
|
messages: rows
|
|
3556
3747
|
},
|
|
@@ -3570,7 +3761,7 @@ msg.command("pin <threadId>").option("-g, --group", "Pin group conversation").de
|
|
|
3570
3761
|
output(
|
|
3571
3762
|
{
|
|
3572
3763
|
threadId,
|
|
3573
|
-
threadType: type ===
|
|
3764
|
+
threadType: type === ThreadType2.Group ? "group" : "user",
|
|
3574
3765
|
pinned: true,
|
|
3575
3766
|
response
|
|
3576
3767
|
},
|
|
@@ -3586,7 +3777,7 @@ msg.command("unpin <threadId>").option("-g, --group", "Unpin group conversation"
|
|
|
3586
3777
|
output(
|
|
3587
3778
|
{
|
|
3588
3779
|
threadId,
|
|
3589
|
-
threadType: type ===
|
|
3780
|
+
threadType: type === ThreadType2.Group ? "group" : "user",
|
|
3590
3781
|
pinned: false,
|
|
3591
3782
|
response
|
|
3592
3783
|
},
|
|
@@ -3660,59 +3851,7 @@ group.command("info <groupId>").description("Get group info").action(
|
|
|
3660
3851
|
group.command("members <groupId>").option("-j, --json", "JSON output").description("List group members").action(
|
|
3661
3852
|
wrapAction(async (groupId, opts, command) => {
|
|
3662
3853
|
const { api } = await requireApi(command);
|
|
3663
|
-
const
|
|
3664
|
-
const groupInfo = info.gridInfoMap[groupId];
|
|
3665
|
-
if (!groupInfo) {
|
|
3666
|
-
throw new Error(`Group not found: ${groupId}`);
|
|
3667
|
-
}
|
|
3668
|
-
const normalizeMemberId = (value) => {
|
|
3669
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
3670
|
-
return String(Math.trunc(value));
|
|
3671
|
-
}
|
|
3672
|
-
if (typeof value !== "string") return "";
|
|
3673
|
-
const trimmed = value.trim();
|
|
3674
|
-
if (!trimmed) return "";
|
|
3675
|
-
return trimmed.replace(/_\d+$/, "");
|
|
3676
|
-
};
|
|
3677
|
-
const idsFromMemberIds = Array.isArray(groupInfo.memberIds) ? groupInfo.memberIds.map((id) => normalizeMemberId(id)).filter(Boolean) : [];
|
|
3678
|
-
const memVerList = groupInfo.memVerList;
|
|
3679
|
-
const idsFromMemVerList = Array.isArray(memVerList) ? memVerList.map((id) => normalizeMemberId(id)).filter(Boolean) : [];
|
|
3680
|
-
const currentMems = Array.isArray(groupInfo.currentMems) ? groupInfo.currentMems : [];
|
|
3681
|
-
const currentMemberMap = /* @__PURE__ */ new Map();
|
|
3682
|
-
for (const member of currentMems) {
|
|
3683
|
-
const userId = normalizeMemberId(member.id);
|
|
3684
|
-
if (!userId) continue;
|
|
3685
|
-
currentMemberMap.set(userId, {
|
|
3686
|
-
displayName: member.dName?.trim() || member.zaloName?.trim() || "",
|
|
3687
|
-
zaloName: member.zaloName?.trim() || ""
|
|
3688
|
-
});
|
|
3689
|
-
}
|
|
3690
|
-
const ids = Array.from(
|
|
3691
|
-
/* @__PURE__ */ new Set([
|
|
3692
|
-
...idsFromMemberIds,
|
|
3693
|
-
...idsFromMemVerList,
|
|
3694
|
-
...Array.from(currentMemberMap.keys())
|
|
3695
|
-
])
|
|
3696
|
-
);
|
|
3697
|
-
const profiles = ids.length > 0 ? await api.getGroupMembersInfo(ids) : { profiles: {} };
|
|
3698
|
-
const rawProfileMap = profiles.profiles;
|
|
3699
|
-
const profileMap = /* @__PURE__ */ new Map();
|
|
3700
|
-
for (const [key, profile] of Object.entries(rawProfileMap)) {
|
|
3701
|
-
if (!profile) continue;
|
|
3702
|
-
const normalizedKey = normalizeMemberId(key);
|
|
3703
|
-
if (normalizedKey && !profileMap.has(normalizedKey)) {
|
|
3704
|
-
profileMap.set(normalizedKey, profile);
|
|
3705
|
-
}
|
|
3706
|
-
const profileId = normalizeMemberId(profile.id);
|
|
3707
|
-
if (profileId && !profileMap.has(profileId)) {
|
|
3708
|
-
profileMap.set(profileId, profile);
|
|
3709
|
-
}
|
|
3710
|
-
}
|
|
3711
|
-
const rows = ids.map((id) => ({
|
|
3712
|
-
userId: id,
|
|
3713
|
-
displayName: profileMap.get(id)?.displayName ?? currentMemberMap.get(id)?.displayName ?? "",
|
|
3714
|
-
zaloName: profileMap.get(id)?.zaloName ?? currentMemberMap.get(id)?.zaloName ?? ""
|
|
3715
|
-
}));
|
|
3854
|
+
const rows = await listGroupMemberRows(api, groupId);
|
|
3716
3855
|
if (opts.json) {
|
|
3717
3856
|
output(rows, true);
|
|
3718
3857
|
return;
|
|
@@ -4431,7 +4570,7 @@ ${replyMediaText}` : replyMediaText;
|
|
|
4431
4570
|
processedText = processedText.trim() ? `${processedText}
|
|
4432
4571
|
${replyContextText}` : replyContextText;
|
|
4433
4572
|
}
|
|
4434
|
-
const chatType = message.type ===
|
|
4573
|
+
const chatType = message.type === ThreadType2.Group ? "group" : "user";
|
|
4435
4574
|
const senderId = getStringCandidate(messageData, ["uidFrom"]) || message.data.uidFrom;
|
|
4436
4575
|
const senderDisplayNameRaw = getStringCandidate(messageData, [
|
|
4437
4576
|
"dName",
|
|
@@ -4440,7 +4579,7 @@ ${replyContextText}` : replyContextText;
|
|
|
4440
4579
|
"displayName"
|
|
4441
4580
|
]);
|
|
4442
4581
|
const senderDisplayName = senderDisplayNameRaw || void 0;
|
|
4443
|
-
const senderNameForMetadata = message.type ===
|
|
4582
|
+
const senderNameForMetadata = message.type === ThreadType2.Group ? senderDisplayName : void 0;
|
|
4444
4583
|
const toId = getStringCandidate(messageData, ["idTo"]) || void 0;
|
|
4445
4584
|
const threadName = typeof messageData.threadName === "string" ? messageData.threadName : typeof messageData.tName === "string" ? messageData.tName : void 0;
|
|
4446
4585
|
const mentions = extractInboundMentions({
|
|
@@ -4477,7 +4616,7 @@ ${replyContextText}` : replyContextText;
|
|
|
4477
4616
|
mentions: mentions.length > 0 ? mentions : void 0,
|
|
4478
4617
|
mentionIds: mentionIds.length > 0 ? mentionIds : void 0,
|
|
4479
4618
|
metadata: {
|
|
4480
|
-
isGroup: message.type ===
|
|
4619
|
+
isGroup: message.type === ThreadType2.Group,
|
|
4481
4620
|
chatType,
|
|
4482
4621
|
threadId: message.threadId,
|
|
4483
4622
|
targetId: message.threadId,
|