openzca 0.1.42 → 0.1.44
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 +5 -0
- package/dist/cli.js +236 -96
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,6 +36,10 @@ 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, username, or member id
|
|
40
|
+
openzca msg send GROUP_ID "Hi @Alice Nguyen" --group
|
|
41
|
+
openzca msg send GROUP_ID "Hi @123456789" --group
|
|
42
|
+
|
|
39
43
|
# Listen for incoming messages
|
|
40
44
|
openzca listen
|
|
41
45
|
```
|
|
@@ -80,6 +84,7 @@ You can also open the saved file manually (for example: `open qr.png` on macOS).
|
|
|
80
84
|
|
|
81
85
|
Media commands accept local files, `file://` paths, and repeatable `--url` options. Add `--group` for group threads.
|
|
82
86
|
Local paths using `~` are expanded automatically (for positional file args, `--url`, and `OPENZCA_LISTEN_MEDIA_DIR`).
|
|
87
|
+
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.
|
|
83
88
|
|
|
84
89
|
### Debug Logging
|
|
85
90
|
|
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.userId, 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 = {
|
|
@@ -826,6 +933,35 @@ function normalizeCodeBlockLeadingWhitespace(line) {
|
|
|
826
933
|
);
|
|
827
934
|
}
|
|
828
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
|
+
|
|
829
965
|
// src/cli.ts
|
|
830
966
|
var require2 = createRequire(import.meta.url);
|
|
831
967
|
var { version: PKG_VERSION } = require2("../package.json");
|
|
@@ -950,7 +1086,7 @@ function output(value, asJson = false) {
|
|
|
950
1086
|
console.log(String(value));
|
|
951
1087
|
}
|
|
952
1088
|
function asThreadType(groupFlag) {
|
|
953
|
-
return groupFlag ?
|
|
1089
|
+
return groupFlag ? ThreadType2.Group : ThreadType2.User;
|
|
954
1090
|
}
|
|
955
1091
|
function parseBooleanFromEnv(name, fallback) {
|
|
956
1092
|
const raw = process.env[name]?.trim();
|
|
@@ -1160,7 +1296,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
|
|
|
1160
1296
|
fail(parsed.requestId, "Invalid upload payload.");
|
|
1161
1297
|
return;
|
|
1162
1298
|
}
|
|
1163
|
-
const threadType = parsed.threadType === "group" ?
|
|
1299
|
+
const threadType = parsed.threadType === "group" ? ThreadType2.Group : ThreadType2.User;
|
|
1164
1300
|
const requestTimeoutMs = parsePositiveIntFromUnknown(parsed.uploadTimeoutMs) ?? uploadTimeoutMs;
|
|
1165
1301
|
writeDebugLine(
|
|
1166
1302
|
"listen.ipc.upload.start",
|
|
@@ -1317,7 +1453,7 @@ async function tryUploadViaListenerIpc(profile, threadId, threadType, attachment
|
|
|
1317
1453
|
{
|
|
1318
1454
|
profile,
|
|
1319
1455
|
threadId,
|
|
1320
|
-
threadType: threadType ===
|
|
1456
|
+
threadType: threadType === ThreadType2.Group ? "group" : "user",
|
|
1321
1457
|
attachmentCount: attachments.length,
|
|
1322
1458
|
socketPath,
|
|
1323
1459
|
requestId,
|
|
@@ -1361,7 +1497,7 @@ async function tryUploadViaListenerIpc(profile, threadId, threadType, attachment
|
|
|
1361
1497
|
requestId,
|
|
1362
1498
|
profile,
|
|
1363
1499
|
threadId,
|
|
1364
|
-
threadType: threadType ===
|
|
1500
|
+
threadType: threadType === ThreadType2.Group ? "group" : "user",
|
|
1365
1501
|
attachments
|
|
1366
1502
|
};
|
|
1367
1503
|
socket.write(`${JSON.stringify(payload)}
|
|
@@ -1423,21 +1559,21 @@ async function tryUploadViaListenerIpc(profile, threadId, threadType, attachment
|
|
|
1423
1559
|
}
|
|
1424
1560
|
async function resolveUploadThreadType(api, profile, threadId, groupFlag, command) {
|
|
1425
1561
|
if (groupFlag) {
|
|
1426
|
-
return { type:
|
|
1562
|
+
return { type: ThreadType2.Group, reason: "explicit_group_flag" };
|
|
1427
1563
|
}
|
|
1428
1564
|
const autoDetectEnabled = parseBooleanFromEnv("OPENZCA_UPLOAD_AUTO_THREAD_TYPE", false);
|
|
1429
1565
|
if (!autoDetectEnabled) {
|
|
1430
|
-
return { type:
|
|
1566
|
+
return { type: ThreadType2.User, reason: "auto_detect_disabled" };
|
|
1431
1567
|
}
|
|
1432
1568
|
try {
|
|
1433
1569
|
const cache = await readCache(profile);
|
|
1434
1570
|
const groupIds = collectIdsFromCacheEntries(cache.groups, ["groupId", "grid", "threadId", "id"]);
|
|
1435
1571
|
if (groupIds.has(threadId)) {
|
|
1436
|
-
return { type:
|
|
1572
|
+
return { type: ThreadType2.Group, reason: "cache_group_match" };
|
|
1437
1573
|
}
|
|
1438
1574
|
const friendIds = collectIdsFromCacheEntries(cache.friends, ["userId", "uid", "id", "threadId"]);
|
|
1439
1575
|
if (friendIds.has(threadId)) {
|
|
1440
|
-
return { type:
|
|
1576
|
+
return { type: ThreadType2.User, reason: "cache_friend_match" };
|
|
1441
1577
|
}
|
|
1442
1578
|
} catch (error) {
|
|
1443
1579
|
writeDebugLine(
|
|
@@ -1452,7 +1588,7 @@ async function resolveUploadThreadType(api, profile, threadId, groupFlag, comman
|
|
|
1452
1588
|
}
|
|
1453
1589
|
const probeEnabled = parseBooleanFromEnv("OPENZCA_UPLOAD_GROUP_PROBE", true);
|
|
1454
1590
|
if (!probeEnabled) {
|
|
1455
|
-
return { type:
|
|
1591
|
+
return { type: ThreadType2.User, reason: "probe_disabled" };
|
|
1456
1592
|
}
|
|
1457
1593
|
const probeTimeoutMs = parsePositiveIntFromEnv("OPENZCA_UPLOAD_GROUP_PROBE_TIMEOUT_MS", 5e3);
|
|
1458
1594
|
try {
|
|
@@ -1462,7 +1598,7 @@ async function resolveUploadThreadType(api, profile, threadId, groupFlag, comman
|
|
|
1462
1598
|
`Timed out waiting ${probeTimeoutMs}ms while probing group thread type.`
|
|
1463
1599
|
);
|
|
1464
1600
|
if (groupInfo?.gridInfoMap?.[threadId]) {
|
|
1465
|
-
return { type:
|
|
1601
|
+
return { type: ThreadType2.Group, reason: "probe_group_match" };
|
|
1466
1602
|
}
|
|
1467
1603
|
} catch (error) {
|
|
1468
1604
|
writeDebugLine(
|
|
@@ -1475,7 +1611,7 @@ async function resolveUploadThreadType(api, profile, threadId, groupFlag, comman
|
|
|
1475
1611
|
command
|
|
1476
1612
|
);
|
|
1477
1613
|
}
|
|
1478
|
-
return { type:
|
|
1614
|
+
return { type: ThreadType2.User, reason: "default_user" };
|
|
1479
1615
|
}
|
|
1480
1616
|
function parseReaction(input) {
|
|
1481
1617
|
const normalized = input.trim();
|
|
@@ -1565,6 +1701,64 @@ async function buildGroupsDetailed(api) {
|
|
|
1565
1701
|
const info = await api.getGroupInfo(ids);
|
|
1566
1702
|
return ids.map((id) => info.gridInfoMap?.[id]).filter((item) => Boolean(item));
|
|
1567
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
|
+
}
|
|
1568
1762
|
async function refreshCacheForProfile(profile, api) {
|
|
1569
1763
|
const [friends, groups] = await Promise.all([
|
|
1570
1764
|
api.getAllFriends(),
|
|
@@ -1882,7 +2076,7 @@ function normalizeGroupHistoryMessages(messages, fallbackThreadId) {
|
|
|
1882
2076
|
const threadIdRaw = String(raw.idTo ?? "").trim();
|
|
1883
2077
|
normalized.push({
|
|
1884
2078
|
threadId: threadIdRaw || fallbackThreadId,
|
|
1885
|
-
type:
|
|
2079
|
+
type: ThreadType2.Group,
|
|
1886
2080
|
data: {
|
|
1887
2081
|
actionId: typeof raw.actionId === "string" && raw.actionId.trim() ? raw.actionId : void 0,
|
|
1888
2082
|
msgId: String(raw.msgId ?? ""),
|
|
@@ -1975,7 +2169,7 @@ async function fetchRecentGroupMessagesViaListener(api, threadId, count) {
|
|
|
1975
2169
|
requestedCursors.add(cursor);
|
|
1976
2170
|
}
|
|
1977
2171
|
pagesRequested += 1;
|
|
1978
|
-
api.listener.requestOldMessages(
|
|
2172
|
+
api.listener.requestOldMessages(ThreadType2.Group, cursor || null);
|
|
1979
2173
|
return true;
|
|
1980
2174
|
};
|
|
1981
2175
|
const cleanup = () => {
|
|
@@ -2007,7 +2201,7 @@ async function fetchRecentGroupMessagesViaListener(api, threadId, count) {
|
|
|
2007
2201
|
}
|
|
2008
2202
|
};
|
|
2009
2203
|
const onOldMessages = (messages, type) => {
|
|
2010
|
-
if (type !==
|
|
2204
|
+
if (type !== ThreadType2.Group) return;
|
|
2011
2205
|
const typedMessages = messages;
|
|
2012
2206
|
for (const message of typedMessages) {
|
|
2013
2207
|
if (message.threadId === threadId) {
|
|
@@ -2083,7 +2277,7 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
|
|
|
2083
2277
|
requestedCursors.add(cursor);
|
|
2084
2278
|
}
|
|
2085
2279
|
pagesRequested += 1;
|
|
2086
|
-
api.listener.requestOldMessages(
|
|
2280
|
+
api.listener.requestOldMessages(ThreadType2.User, cursor || null);
|
|
2087
2281
|
return true;
|
|
2088
2282
|
};
|
|
2089
2283
|
const cleanup = () => {
|
|
@@ -2115,7 +2309,7 @@ async function fetchRecentUserMessagesViaListener(api, threadId, count) {
|
|
|
2115
2309
|
}
|
|
2116
2310
|
};
|
|
2117
2311
|
const onOldMessages = (messages, type) => {
|
|
2118
|
-
if (type !==
|
|
2312
|
+
if (type !== ThreadType2.User) return;
|
|
2119
2313
|
const typedMessages = messages;
|
|
2120
2314
|
for (const message of typedMessages) {
|
|
2121
2315
|
if (message.threadId === threadId) {
|
|
@@ -3135,21 +3329,19 @@ auth.command("cache-clear").description("Clear local cache").action(
|
|
|
3135
3329
|
})
|
|
3136
3330
|
);
|
|
3137
3331
|
var msg = program.command("msg").description("Messaging commands");
|
|
3138
|
-
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/@userId mentions.").action(
|
|
3139
3333
|
wrapAction(async (threadId, message, opts, command) => {
|
|
3140
3334
|
const { api } = await requireApi(command);
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
output(response, false);
|
|
3152
|
-
}
|
|
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);
|
|
3153
3345
|
})
|
|
3154
3346
|
);
|
|
3155
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(
|
|
@@ -3443,8 +3635,8 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
|
|
|
3443
3635
|
{
|
|
3444
3636
|
threadId,
|
|
3445
3637
|
explicitGroupFlag: Boolean(opts.group),
|
|
3446
|
-
isGroup: threadResolution.type ===
|
|
3447
|
-
threadType: threadResolution.type ===
|
|
3638
|
+
isGroup: threadResolution.type === ThreadType2.Group,
|
|
3639
|
+
threadType: threadResolution.type === ThreadType2.Group ? "group" : "user",
|
|
3448
3640
|
threadTypeReason: threadResolution.reason,
|
|
3449
3641
|
localFiles,
|
|
3450
3642
|
urlInputs
|
|
@@ -3472,7 +3664,7 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
|
|
|
3472
3664
|
"msg.upload.ipc.done",
|
|
3473
3665
|
{
|
|
3474
3666
|
threadId,
|
|
3475
|
-
threadType: threadResolution.type ===
|
|
3667
|
+
threadType: threadResolution.type === ThreadType2.Group ? "group" : "user"
|
|
3476
3668
|
},
|
|
3477
3669
|
command
|
|
3478
3670
|
);
|
|
@@ -3483,7 +3675,7 @@ msg.command("upload <arg1> [arg2]").option("-u, --url <url>", "File URL (repeata
|
|
|
3483
3675
|
"msg.upload.ipc.fallback",
|
|
3484
3676
|
{
|
|
3485
3677
|
threadId,
|
|
3486
|
-
threadType: threadResolution.type ===
|
|
3678
|
+
threadType: threadResolution.type === ThreadType2.Group ? "group" : "user",
|
|
3487
3679
|
reason: ipcResult.reason
|
|
3488
3680
|
},
|
|
3489
3681
|
command
|
|
@@ -3522,7 +3714,7 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
|
|
|
3522
3714
|
const { api } = await requireApi(command);
|
|
3523
3715
|
const parsedCount = Number(opts.count);
|
|
3524
3716
|
const count = Number.isFinite(parsedCount) ? Math.min(Math.max(Math.trunc(parsedCount), 1), 200) : 20;
|
|
3525
|
-
const threadType = opts.group ?
|
|
3717
|
+
const threadType = opts.group ? ThreadType2.Group : ThreadType2.User;
|
|
3526
3718
|
const messages = opts.group ? await fetchRecentGroupMessagesViaApi(api, threadId, count) : await fetchRecentUserMessagesViaListener(
|
|
3527
3719
|
api,
|
|
3528
3720
|
threadId,
|
|
@@ -3532,7 +3724,7 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
|
|
|
3532
3724
|
msgId: message.data.msgId,
|
|
3533
3725
|
cliMsgId: message.data.cliMsgId,
|
|
3534
3726
|
threadId: message.threadId || threadId,
|
|
3535
|
-
threadType: message.type ===
|
|
3727
|
+
threadType: message.type === ThreadType2.Group ? "group" : "user",
|
|
3536
3728
|
senderId: message.data.uidFrom,
|
|
3537
3729
|
senderName: message.data.dName,
|
|
3538
3730
|
ts: message.data.ts,
|
|
@@ -3541,7 +3733,7 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
|
|
|
3541
3733
|
msgId: message.data.msgId,
|
|
3542
3734
|
cliMsgId: message.data.cliMsgId,
|
|
3543
3735
|
threadId: message.threadId || threadId,
|
|
3544
|
-
group: message.type ===
|
|
3736
|
+
group: message.type === ThreadType2.Group
|
|
3545
3737
|
},
|
|
3546
3738
|
content: typeof message.data.content === "string" ? message.data.content : JSON.stringify(message.data.content)
|
|
3547
3739
|
}));
|
|
@@ -3549,7 +3741,7 @@ msg.command("recent <threadId>").option("-g, --group", "List recent messages for
|
|
|
3549
3741
|
output(
|
|
3550
3742
|
{
|
|
3551
3743
|
threadId,
|
|
3552
|
-
threadType: threadType ===
|
|
3744
|
+
threadType: threadType === ThreadType2.Group ? "group" : "user",
|
|
3553
3745
|
count: rows.length,
|
|
3554
3746
|
messages: rows
|
|
3555
3747
|
},
|
|
@@ -3569,7 +3761,7 @@ msg.command("pin <threadId>").option("-g, --group", "Pin group conversation").de
|
|
|
3569
3761
|
output(
|
|
3570
3762
|
{
|
|
3571
3763
|
threadId,
|
|
3572
|
-
threadType: type ===
|
|
3764
|
+
threadType: type === ThreadType2.Group ? "group" : "user",
|
|
3573
3765
|
pinned: true,
|
|
3574
3766
|
response
|
|
3575
3767
|
},
|
|
@@ -3585,7 +3777,7 @@ msg.command("unpin <threadId>").option("-g, --group", "Unpin group conversation"
|
|
|
3585
3777
|
output(
|
|
3586
3778
|
{
|
|
3587
3779
|
threadId,
|
|
3588
|
-
threadType: type ===
|
|
3780
|
+
threadType: type === ThreadType2.Group ? "group" : "user",
|
|
3589
3781
|
pinned: false,
|
|
3590
3782
|
response
|
|
3591
3783
|
},
|
|
@@ -3659,59 +3851,7 @@ group.command("info <groupId>").description("Get group info").action(
|
|
|
3659
3851
|
group.command("members <groupId>").option("-j, --json", "JSON output").description("List group members").action(
|
|
3660
3852
|
wrapAction(async (groupId, opts, command) => {
|
|
3661
3853
|
const { api } = await requireApi(command);
|
|
3662
|
-
const
|
|
3663
|
-
const groupInfo = info.gridInfoMap[groupId];
|
|
3664
|
-
if (!groupInfo) {
|
|
3665
|
-
throw new Error(`Group not found: ${groupId}`);
|
|
3666
|
-
}
|
|
3667
|
-
const normalizeMemberId = (value) => {
|
|
3668
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
3669
|
-
return String(Math.trunc(value));
|
|
3670
|
-
}
|
|
3671
|
-
if (typeof value !== "string") return "";
|
|
3672
|
-
const trimmed = value.trim();
|
|
3673
|
-
if (!trimmed) return "";
|
|
3674
|
-
return trimmed.replace(/_\d+$/, "");
|
|
3675
|
-
};
|
|
3676
|
-
const idsFromMemberIds = Array.isArray(groupInfo.memberIds) ? groupInfo.memberIds.map((id) => normalizeMemberId(id)).filter(Boolean) : [];
|
|
3677
|
-
const memVerList = groupInfo.memVerList;
|
|
3678
|
-
const idsFromMemVerList = Array.isArray(memVerList) ? memVerList.map((id) => normalizeMemberId(id)).filter(Boolean) : [];
|
|
3679
|
-
const currentMems = Array.isArray(groupInfo.currentMems) ? groupInfo.currentMems : [];
|
|
3680
|
-
const currentMemberMap = /* @__PURE__ */ new Map();
|
|
3681
|
-
for (const member of currentMems) {
|
|
3682
|
-
const userId = normalizeMemberId(member.id);
|
|
3683
|
-
if (!userId) continue;
|
|
3684
|
-
currentMemberMap.set(userId, {
|
|
3685
|
-
displayName: member.dName?.trim() || member.zaloName?.trim() || "",
|
|
3686
|
-
zaloName: member.zaloName?.trim() || ""
|
|
3687
|
-
});
|
|
3688
|
-
}
|
|
3689
|
-
const ids = Array.from(
|
|
3690
|
-
/* @__PURE__ */ new Set([
|
|
3691
|
-
...idsFromMemberIds,
|
|
3692
|
-
...idsFromMemVerList,
|
|
3693
|
-
...Array.from(currentMemberMap.keys())
|
|
3694
|
-
])
|
|
3695
|
-
);
|
|
3696
|
-
const profiles = ids.length > 0 ? await api.getGroupMembersInfo(ids) : { profiles: {} };
|
|
3697
|
-
const rawProfileMap = profiles.profiles;
|
|
3698
|
-
const profileMap = /* @__PURE__ */ new Map();
|
|
3699
|
-
for (const [key, profile] of Object.entries(rawProfileMap)) {
|
|
3700
|
-
if (!profile) continue;
|
|
3701
|
-
const normalizedKey = normalizeMemberId(key);
|
|
3702
|
-
if (normalizedKey && !profileMap.has(normalizedKey)) {
|
|
3703
|
-
profileMap.set(normalizedKey, profile);
|
|
3704
|
-
}
|
|
3705
|
-
const profileId = normalizeMemberId(profile.id);
|
|
3706
|
-
if (profileId && !profileMap.has(profileId)) {
|
|
3707
|
-
profileMap.set(profileId, profile);
|
|
3708
|
-
}
|
|
3709
|
-
}
|
|
3710
|
-
const rows = ids.map((id) => ({
|
|
3711
|
-
userId: id,
|
|
3712
|
-
displayName: profileMap.get(id)?.displayName ?? currentMemberMap.get(id)?.displayName ?? "",
|
|
3713
|
-
zaloName: profileMap.get(id)?.zaloName ?? currentMemberMap.get(id)?.zaloName ?? ""
|
|
3714
|
-
}));
|
|
3854
|
+
const rows = await listGroupMemberRows(api, groupId);
|
|
3715
3855
|
if (opts.json) {
|
|
3716
3856
|
output(rows, true);
|
|
3717
3857
|
return;
|
|
@@ -4430,7 +4570,7 @@ ${replyMediaText}` : replyMediaText;
|
|
|
4430
4570
|
processedText = processedText.trim() ? `${processedText}
|
|
4431
4571
|
${replyContextText}` : replyContextText;
|
|
4432
4572
|
}
|
|
4433
|
-
const chatType = message.type ===
|
|
4573
|
+
const chatType = message.type === ThreadType2.Group ? "group" : "user";
|
|
4434
4574
|
const senderId = getStringCandidate(messageData, ["uidFrom"]) || message.data.uidFrom;
|
|
4435
4575
|
const senderDisplayNameRaw = getStringCandidate(messageData, [
|
|
4436
4576
|
"dName",
|
|
@@ -4439,7 +4579,7 @@ ${replyContextText}` : replyContextText;
|
|
|
4439
4579
|
"displayName"
|
|
4440
4580
|
]);
|
|
4441
4581
|
const senderDisplayName = senderDisplayNameRaw || void 0;
|
|
4442
|
-
const senderNameForMetadata = message.type ===
|
|
4582
|
+
const senderNameForMetadata = message.type === ThreadType2.Group ? senderDisplayName : void 0;
|
|
4443
4583
|
const toId = getStringCandidate(messageData, ["idTo"]) || void 0;
|
|
4444
4584
|
const threadName = typeof messageData.threadName === "string" ? messageData.threadName : typeof messageData.tName === "string" ? messageData.tName : void 0;
|
|
4445
4585
|
const mentions = extractInboundMentions({
|
|
@@ -4476,7 +4616,7 @@ ${replyContextText}` : replyContextText;
|
|
|
4476
4616
|
mentions: mentions.length > 0 ? mentions : void 0,
|
|
4477
4617
|
mentionIds: mentionIds.length > 0 ? mentionIds : void 0,
|
|
4478
4618
|
metadata: {
|
|
4479
|
-
isGroup: message.type ===
|
|
4619
|
+
isGroup: message.type === ThreadType2.Group,
|
|
4480
4620
|
chatType,
|
|
4481
4621
|
threadId: message.threadId,
|
|
4482
4622
|
targetId: message.threadId,
|