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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/dist/cli.js +236 -97
  3. 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 ? ThreadType.Group : ThreadType.User;
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" ? ThreadType.Group : ThreadType.User;
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 === ThreadType.Group ? "group" : "user",
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 === ThreadType.Group ? "group" : "user",
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: ThreadType.Group, reason: "explicit_group_flag" };
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: ThreadType.User, reason: "auto_detect_disabled" };
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: ThreadType.Group, reason: "cache_group_match" };
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: ThreadType.User, reason: "cache_friend_match" };
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: ThreadType.User, reason: "probe_disabled" };
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: ThreadType.Group, reason: "probe_group_match" };
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: ThreadType.User, reason: "default_user" };
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: ThreadType.Group,
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(ThreadType.Group, cursor || null);
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 !== ThreadType.Group) return;
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(ThreadType.User, cursor || null);
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 !== ThreadType.User) return;
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
- if (opts.raw) {
3143
- const response = await api.sendMessage(message, threadId, asThreadType(opts.group));
3144
- output(response, false);
3145
- } else {
3146
- const { text, styles } = parseTextStyles(message);
3147
- const response = await api.sendMessage(
3148
- { msg: text, styles: styles.length > 0 ? styles : void 0 },
3149
- threadId,
3150
- asThreadType(opts.group)
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 === ThreadType.Group,
3448
- threadType: threadResolution.type === ThreadType.Group ? "group" : "user",
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 === ThreadType.Group ? "group" : "user"
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 === ThreadType.Group ? "group" : "user",
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 ? ThreadType.Group : ThreadType.User;
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 === ThreadType.Group ? "group" : "user",
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 === ThreadType.Group
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 === ThreadType.Group ? "group" : "user",
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 === ThreadType.Group ? "group" : "user",
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 === ThreadType.Group ? "group" : "user",
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 info = await api.getGroupInfo(groupId);
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 === ThreadType.Group ? "group" : "user";
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 === ThreadType.Group ? senderDisplayName : void 0;
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 === ThreadType.Group,
4619
+ isGroup: message.type === ThreadType2.Group,
4481
4620
  chatType,
4482
4621
  threadId: message.threadId,
4483
4622
  targetId: message.threadId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openzca",
3
- "version": "0.1.41",
3
+ "version": "0.1.43",
4
4
  "description": "Open-source zca-compatible CLI to integrate Zalo with OpenClaw",
5
5
  "type": "module",
6
6
  "bin": {