@manybot/manybot 5.6.1 → 5.8.0

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 (71) hide show
  1. package/README.md +20 -3
  2. package/dist/client/banner.js +10 -0
  3. package/dist/client/banner.test.js +31 -0
  4. package/dist/client/store.js +56 -5
  5. package/dist/client/store.test.js +170 -0
  6. package/dist/config.js +28 -44
  7. package/dist/config.test.js +26 -0
  8. package/dist/drivers/baileys/adapter.js +207 -8
  9. package/dist/drivers/baileys/api/index.js +300 -130
  10. package/dist/drivers/baileys/index.js +62 -30
  11. package/dist/drivers/baileys/loginPrompt.js +0 -2
  12. package/dist/drivers/baileys/messageHandler.js +158 -4
  13. package/dist/drivers/baileys/messageHandler.test.js +203 -0
  14. package/dist/drivers/baileysAdapter.test.js +281 -0
  15. package/dist/drivers/jid.test.js +40 -0
  16. package/dist/drivers/types.js +5 -5
  17. package/dist/i18n/index.js +15 -2
  18. package/dist/kernel/activeDriverSend.js +21 -0
  19. package/dist/kernel/activeDriverSend.test.js +89 -0
  20. package/dist/kernel/alerts.js +3 -9
  21. package/dist/kernel/chatSession.js +65 -0
  22. package/dist/kernel/chatSession.test.js +46 -0
  23. package/dist/kernel/commandAccess.js +66 -0
  24. package/dist/kernel/commandAccess.test.js +74 -0
  25. package/dist/kernel/commandDeprecation.js +168 -0
  26. package/dist/kernel/commandDeprecation.test.js +107 -0
  27. package/dist/kernel/commandMenu.js +268 -0
  28. package/dist/kernel/commandMenu.test.js +234 -0
  29. package/dist/kernel/commandPermissions.js +125 -0
  30. package/dist/kernel/commandPermissions.test.js +159 -0
  31. package/dist/kernel/commandRegistry.js +459 -0
  32. package/dist/kernel/commandRegistry.test.js +156 -0
  33. package/dist/kernel/commandsConfig.js +517 -0
  34. package/dist/kernel/commandsConfig.test.js +236 -0
  35. package/dist/kernel/contactAutoSave.test.js +87 -0
  36. package/dist/kernel/driverManager.js +10 -6
  37. package/dist/kernel/driverManager.test.js +90 -0
  38. package/dist/kernel/integrationMode.js +88 -0
  39. package/dist/kernel/integrationMode.test.js +95 -0
  40. package/dist/kernel/loadIntegrationPlugin.test.js +67 -0
  41. package/dist/kernel/pluginApi.test.js +583 -0
  42. package/dist/kernel/pluginGuard.js +15 -12
  43. package/dist/kernel/pluginGuard.test.js +39 -0
  44. package/dist/kernel/pluginLoader.js +96 -1
  45. package/dist/kernel/pluginLoader.test.js +80 -0
  46. package/dist/kernel/runCommand.js +245 -0
  47. package/dist/kernel/runCommand.test.js +235 -0
  48. package/dist/kernel/sendFallbackGuard.js +19 -48
  49. package/dist/kernel/sendFallbackGuard.test.js +80 -0
  50. package/dist/kernel/sendGuard.js +38 -42
  51. package/dist/kernel/sendGuard.test.js +102 -0
  52. package/dist/kernel/settingsDb.js +4 -3
  53. package/dist/kernel/statusServer.js +9 -2
  54. package/dist/kernel/statusServer.test.js +70 -0
  55. package/dist/kernel/testConfig.js +183 -0
  56. package/dist/kernel/testConfig.test.js +181 -0
  57. package/dist/kernel/updateCheck.js +33 -10
  58. package/dist/locales/en.json +64 -13
  59. package/dist/locales/es.json +64 -13
  60. package/dist/locales/pt.json +64 -13
  61. package/dist/logger/logger.js +23 -3
  62. package/dist/logger/logger.test.js +45 -0
  63. package/dist/main.js +5 -76
  64. package/dist/plugins/__manybot_integration__/index.js +167 -0
  65. package/dist/plugins/__manybot_integration__/index.test.js +184 -0
  66. package/package.json +75 -18
  67. package/dist/drivers/whatsmeow/client.js +0 -252
  68. package/dist/drivers/whatsmeow/index.js +0 -79
  69. package/dist/drivers/whatsmeow/installer.js +0 -86
  70. package/dist/drivers/whatsmeow/supervisor.js +0 -328
  71. package/dist/drivers/whatsmeow/whatsmeow.proto +0 -64
@@ -14,6 +14,7 @@ import { logger } from "#logger";
14
14
  import { t, createPluginT, reloadTranslations, getCurrentLang } from "#i18n";
15
15
  import { CONFIG, CONFIG_DIR } from "#config";
16
16
  import { enqueue } from "#download";
17
+ import { waitForEditSlot } from "#kernel/sendGuard.js";
17
18
  import { schedule, cancelPlugin } from "#kernel/scheduler.js";
18
19
  import { emptyFolder } from "#utils/file.js";
19
20
  import { normalizeJid, denormalizeJid, toWireJid } from "#drivers/jid.js";
@@ -24,11 +25,14 @@ import path from "path";
24
25
  import os from "os";
25
26
  import { spawn } from "child_process";
26
27
  import { randomUUID } from "crypto";
27
- import { waitForSendSlot, simulateState, typingDuration, mediaDuration, waitForEditSlot } from "#sendguard";
28
+ import { waitForSendSlot, simulateState, typingDuration, mediaDuration } from "#sendguard";
28
29
  import { sendWithFallback } from "#kernel/sendFallbackGuard.js";
29
30
  import { buildSettingsApi } from "#settingsdb";
31
+ import * as commandAccess from "#kernel/commandAccess.js";
32
+ import * as chatSession from "#kernel/chatSession.js";
33
+ import { resolveDispatch, runCommand as dispatchCommand } from "#kernel/runCommand.js";
30
34
  import WebP from "node-webpmux";
31
- import { getAggregateVotesInPollMessage, decryptPollVote, jidNormalizedUser, } from "@whiskeysockets/baileys";
35
+ import { jidNormalizedUser, } from "@whiskeysockets/baileys";
32
36
  // ── Raw-Baileys escape hatch ─────────────────────────────────────────────────
33
37
  //
34
38
  // This whole file is the Baileys driver's plugin-context builder, so the
@@ -175,13 +179,13 @@ function hasMention(contextInfo) {
175
179
  return !!(ci?.mentionedJid && Array.isArray(ci.mentionedJid) && ci.mentionedJid.length > 0);
176
180
  }
177
181
  /** True if the bot's own JID (PN or LID) is in contextInfo.mentionedJid. */
178
- function hasBotMention(contextInfo, sock, store) {
182
+ function hasBotMention(contextInfo, contract, store) {
179
183
  const ci = contextInfo;
180
184
  const mentioned = ci?.mentionedJid;
181
185
  if (!mentioned || mentioned.length === 0)
182
186
  return false;
183
- const botLid = sock.user?.lid;
184
- const botCandidates = [sock.user?.id, botLid]
187
+ const me = contract.me();
188
+ const botCandidates = [me.id, me.lid]
185
189
  .filter((v) => !!v)
186
190
  .map(v => normalizeJid(store.resolveJid(normalizeJid(v))));
187
191
  if (botCandidates.length === 0)
@@ -223,11 +227,11 @@ function bindGroupMetaInvalidation(contract) {
223
227
  if (groupMetaInvalidationBound)
224
228
  return;
225
229
  groupMetaInvalidationBound = true;
226
- const sock = rawSocketOf(contract);
227
- const ev = sock.ev;
228
- ev.on("group-participants.update", (u) => groupMetaCache.delete(u.id));
229
- ev.on("groups.update", (updates) => {
230
- for (const u of updates)
230
+ contract.on("group-participants.update", (u) => {
231
+ groupMetaCache.delete(u.id);
232
+ });
233
+ contract.on("groups.update", (p) => {
234
+ for (const u of p.updates)
231
235
  if (u.id)
232
236
  groupMetaCache.delete(u.id);
233
237
  });
@@ -517,6 +521,27 @@ async function normalizeContact(jid, info, botJid, contract) {
517
521
  };
518
522
  }
519
523
  // ── Chats API ─────────────────────────────────────────────────────────────────
524
+ function buildCommandsApi() {
525
+ return {
526
+ exists: commandAccess.exists,
527
+ desc: commandAccess.desc,
528
+ manual: commandAccess.manual,
529
+ list: commandAccess.list,
530
+ isMenuAlias: commandAccess.isMenuAlias,
531
+ };
532
+ }
533
+ // ── Session API (Phase 7, MANYBOT-6.md) ─────────────────────────────────────
534
+ // Scoped to the current chat (normJid) + the calling plugin. Only built in
535
+ // buildApi() (runtime), not buildBaseApi() — there is no "current chat" at
536
+ // setup time to lock.
537
+ function buildSessionApi(chatId, pluginName) {
538
+ return {
539
+ acquire: () => chatSession.acquireSession(chatId, pluginName),
540
+ release: () => { chatSession.releaseSession(chatId, pluginName); },
541
+ isLocked: () => chatSession.isSessionLocked(chatId),
542
+ isMine: () => chatSession.getSessionHolder(chatId) === pluginName,
543
+ };
544
+ }
520
545
  function buildChatsApi(store) {
521
546
  return {
522
547
  /**
@@ -719,7 +744,6 @@ function makeHistoryArray(entries, store) {
719
744
  return arr;
720
745
  }
721
746
  export function buildMessageContext(msg, contract, store, guardOptions = {}) {
722
- const sock = rawSocketOf(contract);
723
747
  const body = getMsgBody(msg);
724
748
  const prefix = CONFIG.CMD_PREFIX;
725
749
  const rawArgs = body.trim().split(/\s+/);
@@ -742,6 +766,21 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
742
766
  // We carry the same _raw.contextInfo on the synthetic so a recursive
743
767
  // getReply().getReply() keeps working (the inner call re-reads
744
768
  // getContextInfo off _raw.contextInfo).
769
+ //
770
+ // WhatsApp's contextInfo never carries the quoted author's pushname, so
771
+ // we best-effort it from the local contact store (same lookup pattern
772
+ // as getContact() below) instead of leaving it undefined -> falling
773
+ // back to the raw phone number in senderName.
774
+ const lookupPushName = (jid) => {
775
+ if (!jid)
776
+ return undefined;
777
+ const contacts = store.contacts;
778
+ const info = contacts[jid]
779
+ ?? contacts[denormalizeJid(jid)]
780
+ ?? contacts[store.resolveJid(jid)]
781
+ ?? contacts[denormalizeJid(store.resolveJid(jid))];
782
+ return info?.notify ?? undefined;
783
+ };
745
784
  const quotedRaw = contextInfo?.quotedMessage
746
785
  ? (() => {
747
786
  const decoded = decodeContent(contextInfo.quotedMessage);
@@ -754,6 +793,8 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
754
793
  timestamp: 0,
755
794
  body: decoded.body,
756
795
  mimetype: decoded.mimetype,
796
+ fromPn: contextInfo.participant ?? undefined,
797
+ pushName: lookupPushName(contextInfo.participant),
757
798
  _raw: {
758
799
  contextInfo: {
759
800
  stanzaId: contextInfo.stanzaId,
@@ -777,6 +818,8 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
777
818
  type: "other",
778
819
  contentHash: "",
779
820
  timestamp: 0,
821
+ fromPn: msg.quotedKey.participant ?? undefined,
822
+ pushName: lookupPushName(msg.quotedKey.participant),
780
823
  }
781
824
  : null;
782
825
  return {
@@ -821,7 +864,7 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
821
864
  return buildMessageContext(quotedRaw, contract, store, { cooldown: false, jitter: false });
822
865
  },
823
866
  hasMention: hasMention(contextInfo),
824
- hasBotMention: hasBotMention(contextInfo, sock, store),
867
+ hasBotMention: hasBotMention(contextInfo, contract, store),
825
868
  reply: makeSender(contract, store, rawJid, msg, { cooldown, jitter }),
826
869
  async react(emoji) {
827
870
  await contract.react(rawJid, {
@@ -845,7 +888,10 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
845
888
  if (!msg.fromMe) {
846
889
  throw new Error("[pluginApi] edit() can only be used on the bot's own messages");
847
890
  }
848
- if (!msg.id || !(await waitForEditSlot(msg.id)))
891
+ if (!msg.id)
892
+ return;
893
+ const allowed = await waitForEditSlot(msg.id);
894
+ if (!allowed)
849
895
  return;
850
896
  await contract.editMessage(rawJid, {
851
897
  id: msg.id,
@@ -867,7 +913,7 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
867
913
  ?? store.contacts[denormalizeJid(sender)]
868
914
  ?? store.contacts[store.resolveJid(msg.fromPn ?? "")]
869
915
  ?? store.contacts[denormalizeJid(store.resolveJid(msg.fromPn ?? ""))];
870
- const botJid = sock.user?.id ? jidNormalizedUser(sock.user.id) : null;
916
+ const botJid = contract.me().id ? jidNormalizedUser(contract.me().id) : null;
871
917
  return normalizeContact(sender, info, botJid, contract);
872
918
  },
873
919
  };
@@ -961,7 +1007,10 @@ class MessageHandle {
961
1007
  if (!msg.fromMe) {
962
1008
  throw new Error("[pluginApi] edit() can only be used on the bot's own messages");
963
1009
  }
964
- if (!msg.id || !(await waitForEditSlot(msg.id)))
1010
+ if (!msg.id)
1011
+ return;
1012
+ const allowed = await waitForEditSlot(msg.id);
1013
+ if (!allowed)
965
1014
  return;
966
1015
  await this._contract.editMessage(msg.chatId, {
967
1016
  id: msg.id,
@@ -1321,20 +1370,58 @@ function buildSetupSendApi(contract, store) {
1321
1370
  },
1322
1371
  };
1323
1372
  }
1324
- // ── Events API ────────────────────────────────────────────────────────────────
1373
+ const WA_EVENT_NAMES = new Set([
1374
+ "messages.upsert",
1375
+ "messages.update",
1376
+ "messages.delete",
1377
+ "messaging-history.set",
1378
+ "chats.upsert",
1379
+ "chats.update",
1380
+ "chats.delete",
1381
+ "contacts.upsert",
1382
+ "contacts.update",
1383
+ "group-participants.update",
1384
+ "groups.upsert",
1385
+ "groups.update",
1386
+ "group.join-request",
1387
+ "blocklist.set",
1388
+ "blocklist.update",
1389
+ "connection.update",
1390
+ ]);
1391
+ function assertSupportedEvent(event) {
1392
+ if (!WA_EVENT_NAMES.has(event)) {
1393
+ throw new Error(`[events] unsupported event "${event}". Supported: ${[...WA_EVENT_NAMES].join(", ")}. ` +
1394
+ `If you need this event, file an issue — adding events to WaEventName is a contract change.`);
1395
+ }
1396
+ }
1325
1397
  const listenerRegistry = new Map();
1326
- export function cleanupPluginEvents(pluginName, contract) {
1327
- const sock = rawSocketOf(contract);
1398
+ export function cleanupPluginEvents(pluginName, _contract) {
1328
1399
  const list = listenerRegistry.get(pluginName);
1329
1400
  if (list) {
1330
- for (const { event, handler } of list) {
1401
+ for (const ref of list) {
1331
1402
  try {
1332
- sock.ev.off(event, handler);
1403
+ ref.detach();
1333
1404
  }
1334
1405
  catch { }
1335
1406
  }
1336
1407
  listenerRegistry.delete(pluginName);
1337
1408
  }
1409
+ // The poll-vote subscription is created in `buildPollApi` by calling
1410
+ // `contract.on(...)` directly, so it lives outside `listenerRegistry`.
1411
+ // Detach it here so the listener doesn't outlive the plugin (and a
1412
+ // reloaded plugin can re-subscribe — `buildPollApi` gates on the
1413
+ // `boundPollPlugins` map).
1414
+ const pollDetach = boundPollPlugins.get(pluginName);
1415
+ if (pollDetach) {
1416
+ try {
1417
+ pollDetach();
1418
+ }
1419
+ catch { }
1420
+ boundPollPlugins.delete(pluginName);
1421
+ }
1422
+ // Drop the per-plugin poll registry so reloading a plugin doesn't see
1423
+ // stale PollHandles from a prior instance.
1424
+ pollRegistry.delete(pluginName);
1338
1425
  cancelPlugin(pluginName);
1339
1426
  }
1340
1427
  /**
@@ -1342,21 +1429,26 @@ export function cleanupPluginEvents(pluginName, contract) {
1342
1429
  * @param {string} pluginName
1343
1430
  */
1344
1431
  function buildEventsApi(contract, pluginName) {
1345
- const sock = rawSocketOf(contract);
1346
1432
  return {
1347
1433
  on(event, handler) {
1348
- sock.ev.on(event, handler);
1434
+ assertSupportedEvent(event);
1435
+ const wrapped = (payload) => handler(payload);
1436
+ const detach = contract.on(event, wrapped);
1349
1437
  if (!listenerRegistry.has(pluginName))
1350
1438
  listenerRegistry.set(pluginName, new Set());
1351
- const ref = { event, handler };
1439
+ const ref = { event, handler: wrapped, detach };
1352
1440
  listenerRegistry.get(pluginName).add(ref);
1353
1441
  return () => {
1354
- sock.ev.off(event, handler);
1442
+ try {
1443
+ detach();
1444
+ }
1445
+ catch { }
1355
1446
  listenerRegistry.get(pluginName)?.delete(ref);
1356
1447
  };
1357
1448
  },
1358
1449
  once(event) {
1359
- return new Promise(resolve => {
1450
+ assertSupportedEvent(event);
1451
+ return new Promise((resolve) => {
1360
1452
  const off = this.on(event, (data) => { off(); resolve(data); });
1361
1453
  });
1362
1454
  },
@@ -1364,8 +1456,12 @@ function buildEventsApi(contract, pluginName) {
1364
1456
  const list = listenerRegistry.get(pluginName);
1365
1457
  if (!list)
1366
1458
  return;
1367
- for (const { event, handler } of list)
1368
- sock.ev.off(event, handler);
1459
+ for (const ref of list) {
1460
+ try {
1461
+ ref.detach();
1462
+ }
1463
+ catch { }
1464
+ }
1369
1465
  listenerRegistry.delete(pluginName);
1370
1466
  },
1371
1467
  };
@@ -1425,8 +1521,42 @@ async function resolveMentionJids(contract, store, jid, mentions) {
1425
1521
  }
1426
1522
  return Array.from(new Set(result.filter(Boolean)));
1427
1523
  }
1428
- function buildAdminApi(contract, chatJid) {
1429
- const norm = (v) => (Array.isArray(v) ? v : [v]).map(toWireJid);
1524
+ function buildAdminApi(contract, store, chatJid) {
1525
+ /**
1526
+ * Resolve admin-supplied identifiers (bare phone number, @c.us,
1527
+ * @s.whatsapp.net, or @lid) to the exact jid WhatsApp has on file for
1528
+ * that participant *in this group*. A naive toWireJid() guess is only
1529
+ * correct for pn-addressed groups — lid-addressed groups (increasingly
1530
+ * common, e.g. when a member hides their phone number) only recognize
1531
+ * that member by @lid, which a phone number typed by the admin can't
1532
+ * produce on its own. Baileys' raw groupMetadata() participants carry
1533
+ * `id` (whatever WA addresses them as here), `jid` (its best
1534
+ * phone-number guess) and `lid` — check all three.
1535
+ *
1536
+ * Throws per-identifier if it doesn't match a current member, instead
1537
+ * of silently sending a guessed jid that WhatsApp rejects deep inside
1538
+ * the per-participant status array (see assertParticipantsUpdateOk).
1539
+ */
1540
+ async function resolveTargets(groupJid, identifiers) {
1541
+ const meta = await getGroupMetadataCached(contract, groupJid);
1542
+ const out = [];
1543
+ for (const id of identifiers) {
1544
+ const wire = toWireJid(id);
1545
+ const resolved = normalizeJid(store.resolveJid(id));
1546
+ const match = meta.participants.find((p) => {
1547
+ const pAny = p;
1548
+ return (toWireJid(pAny.id) === wire ||
1549
+ normalizeJid(store.resolveJid(pAny.id)) === resolved ||
1550
+ (pAny.jid ? toWireJid(pAny.jid) === wire : false) ||
1551
+ (pAny.lid ? toWireJid(pAny.lid) === wire : false));
1552
+ });
1553
+ if (!match) {
1554
+ throw new Error(t("driver.groupParticipantNotFound", { id, group: groupJid }));
1555
+ }
1556
+ out.push(match.id);
1557
+ }
1558
+ return Array.from(new Set(out));
1559
+ }
1430
1560
  function requireChat() {
1431
1561
  if (!chatJid)
1432
1562
  throw new Error("This admin operation requires a runtime group context.");
@@ -1456,7 +1586,7 @@ function buildAdminApi(contract, chatJid) {
1456
1586
  const failed = results.filter((r) => r?.status && r.status !== "200");
1457
1587
  if (failed.length > 0) {
1458
1588
  const detail = failed.map((r) => `${r.jid ?? "?"}=${r.status}`).join(", ");
1459
- throw new Error(`groupParticipantsUpdate("${action}") rejeitado para: ${detail}`);
1589
+ throw new Error(t("driver.groupParticipantsUpdateRejected", { action, detail }));
1460
1590
  }
1461
1591
  }
1462
1592
  /**
@@ -1471,16 +1601,35 @@ function buildAdminApi(contract, chatJid) {
1471
1601
  results = await contract.groupParticipantsUpdate(jid, users, action);
1472
1602
  }
1473
1603
  catch (err) {
1474
- throw new Error(`groupParticipantsUpdate("${action}") falhou para o grupo "${jid}" com participantes [${users.join(", ")}]: ${err.message}`);
1604
+ throw new Error(t("driver.groupParticipantsUpdateFailed", {
1605
+ action,
1606
+ group: jid,
1607
+ users: users.join(", "),
1608
+ message: err.message,
1609
+ }));
1475
1610
  }
1476
1611
  assertParticipantsUpdateOk(action, results);
1477
1612
  return results;
1478
1613
  }
1479
- function createTargetableAction(action, memberIds) {
1480
- const users = norm(memberIds);
1481
- const executeCurrent = async () => { requireChat(); return action(chatJid, users); };
1614
+ function createTargetableAction(action, memberIds, mode = "existingMember") {
1615
+ const raw = Array.isArray(memberIds) ? memberIds : [memberIds];
1616
+ const resolve = async (groupJid) => mode === "existingMember"
1617
+ ? resolveTargets(groupJid, raw)
1618
+ // `add` targets people who aren't members yet — there's no
1619
+ // participant-list entry to match against, so fall back to a
1620
+ // plain jid guess (the historical behavior).
1621
+ : Array.from(new Set(raw.map(toWireJid)));
1622
+ const executeCurrent = async () => {
1623
+ requireChat();
1624
+ const users = await resolve(chatJid);
1625
+ return action(chatJid, users);
1626
+ };
1482
1627
  return {
1483
- async to(targetJid) { await getGroup(targetJid); return action(targetJid, users); },
1628
+ async to(targetJid) {
1629
+ await getGroup(targetJid);
1630
+ const users = await resolve(targetJid);
1631
+ return action(targetJid, users);
1632
+ },
1484
1633
  then(onfulfilled, onrejected) {
1485
1634
  return executeCurrent().then(onfulfilled, onrejected);
1486
1635
  },
@@ -1495,22 +1644,25 @@ function buildAdminApi(contract, chatJid) {
1495
1644
  return {
1496
1645
  /** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
1497
1646
  add(memberIds) {
1498
- return createTargetableAction((jid, users) => runParticipantsUpdate(jid, users, "add"), memberIds);
1647
+ return createTargetableAction((jid, users) => runParticipantsUpdate(jid, users, "add"), memberIds, "newMember");
1499
1648
  },
1500
1649
  /** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
1501
1650
  async kick(memberIds) {
1502
1651
  requireChat();
1503
- return runParticipantsUpdate(chatJid, norm(memberIds), "remove");
1652
+ const users = await resolveTargets(chatJid, Array.isArray(memberIds) ? memberIds : [memberIds]);
1653
+ return runParticipantsUpdate(chatJid, users, "remove");
1504
1654
  },
1505
1655
  /** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
1506
1656
  async promote(memberIds) {
1507
1657
  requireChat();
1508
- return runParticipantsUpdate(chatJid, norm(memberIds), "promote");
1658
+ const users = await resolveTargets(chatJid, Array.isArray(memberIds) ? memberIds : [memberIds]);
1659
+ return runParticipantsUpdate(chatJid, users, "promote");
1509
1660
  },
1510
1661
  /** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
1511
1662
  async demote(memberIds) {
1512
1663
  requireChat();
1513
- return runParticipantsUpdate(chatJid, norm(memberIds), "demote");
1664
+ const users = await resolveTargets(chatJid, Array.isArray(memberIds) ? memberIds : [memberIds]);
1665
+ return runParticipantsUpdate(chatJid, users, "demote");
1514
1666
  },
1515
1667
  /** @param {string} name */
1516
1668
  async setSubject(name) {
@@ -1563,11 +1715,15 @@ function buildMeApi(contract) {
1563
1715
  }
1564
1716
  // ── Poll API ──────────────────────────────────────────────────────────────────
1565
1717
  const pollRegistry = new Map();
1566
- // Rebinding must happen per socket instance a plugin name alone doesn't
1567
- // tell us whether the listener is bound to the CURRENT (post-reconnect)
1568
- // sock.ev or a dead one from before. WeakMap keyed by sock lets old
1569
- // entries fall off automatically once that socket is garbage collected.
1570
- const pollListenersBySocket = new WeakMap();
1718
+ // Per-process map of plugin names whose poll-vote subscription is bound,
1719
+ // to its unsubscribe handle. The contract handles rebinding to a fresh
1720
+ // socket on reconnect (its `on()` returns an unsubscribe that drops
1721
+ // cleanly on the dead socket's fan-out once it's torn down), so a simple
1722
+ // per-process map is enough — no per-sock WeakMap needed anymore.
1723
+ // `cleanupPluginEvents` MUST call the stored detach handle on unload so
1724
+ // the listener doesn't outlive the plugin and so a reloaded plugin can
1725
+ // re-subscribe.
1726
+ const boundPollPlugins = new Map();
1571
1727
  /**
1572
1728
  * Tracks votes for an active poll.
1573
1729
  * Obtained via ctx.poll.create().
@@ -1638,59 +1794,29 @@ function buildPollApi(contract, store, rawJid, guardOptions, pluginName) {
1638
1794
  pollRegistry.set(pluginName, new Map());
1639
1795
  const registry = pollRegistry.get(pluginName);
1640
1796
  // Keyed by creationId -> (voterKey -> latest vote entry). WhatsApp resends
1641
- // the *entire current selection* on every tap (not a diff), and Baileys'
1642
- // getAggregateVotesInPollMessage() replays whatever pollUpdates you give it
1643
- // with no dedup — so we must keep only the latest entry per voter ourselves,
1644
- // or retracted/changed votes keep counting alongside the new one.
1797
+ // the *entire current selection* on every tap (not a diff), and the
1798
+ // aggregatePollVotes() contract method replays whatever pollUpdates you
1799
+ // give it with no dedup — so we must keep only the latest entry per
1800
+ // voter ourselves, or retracted/changed votes keep counting alongside
1801
+ // the new one.
1645
1802
  const pollVotesByCreationId = new Map();
1646
- // Poll decryption needs the raw Baileys socket (sock.ev for
1647
- // messages.upsert, sock.user for self-JID shape), so we lift it off the
1648
- // contract here. The WeakMap key is the socket itself so old listeners
1649
- // fall off automatically when the socket is replaced (reconnect).
1650
- const sock = rawSocketOf(contract);
1651
- let boundPlugins = pollListenersBySocket.get(sock);
1652
- if (!boundPlugins) {
1653
- boundPlugins = new Set();
1654
- pollListenersBySocket.set(sock, boundPlugins);
1655
- }
1656
- if (!boundPlugins.has(pluginName)) {
1657
- boundPlugins.add(pluginName);
1658
- const meId = sock.user?.id ? jidNormalizedUser(sock.user.id) : "me";
1659
- // WhatsApp doesn't consistently use the same JID shape (LID vs PN) for
1660
- // pollCreatorJid/voterJid when deriving the poll-vote decryption key —
1661
- // it depends on addressingMode, 1:1 vs group, and which side sent last.
1662
- // Trying to compute "the" correct JID up front (as the old resolveAuthor
1663
- // did, always preferring participantPn) causes AES-GCM auth failures
1664
- // whenever WhatsApp actually used the LID for that message. Instead,
1665
- // gather every plausible JID for each side and brute-force combinations
1666
- // until one decrypts successfully — see
1667
- // https://github.com/WhiskeySockets/Baileys/issues/2342 and #1678.
1668
- function jidCandidates(key) {
1669
- const cands = [];
1670
- if (key.fromMe) {
1671
- const selfLid = sock.user?.lid;
1672
- if (selfLid)
1673
- cands.push(jidNormalizedUser(selfLid));
1674
- if (sock.user?.id)
1675
- cands.push(jidNormalizedUser(sock.user.id));
1676
- }
1677
- else {
1678
- const rawParticipant = key.participant ?? key.remoteJid;
1679
- if (rawParticipant)
1680
- cands.push(jidNormalizedUser(rawParticipant));
1681
- if (key.participantPn)
1682
- cands.push(jidNormalizedUser(key.participantPn));
1683
- if (rawParticipant?.endsWith("@lid")) {
1684
- const resolved = store.resolveJid(rawParticipant);
1685
- if (resolved && resolved !== rawParticipant)
1686
- cands.push(jidNormalizedUser(resolved));
1687
- }
1688
- }
1689
- return Array.from(new Set(cands.filter(Boolean)));
1690
- }
1691
- sock.ev.on("messages.upsert", async ({ messages: msgs }) => {
1803
+ // Poll decryption is Baileys-specific. Both the subscription
1804
+ // (`messages.upsert`) and the decryption/aggregation go through the
1805
+ // contract `buildPollApi` doesn't touch the raw socket anymore. The
1806
+ // contract's optional methods are only implemented by the Baileys
1807
+ // adapter; drivers that don't have poll decryption leave them off the
1808
+ // contract and we silently no-op here (a bot on a non-Baileys driver
1809
+ // simply can't track poll votes).
1810
+ if (!boundPollPlugins.has(pluginName) &&
1811
+ typeof contract.decryptPollVote === "function" &&
1812
+ typeof contract.aggregatePollVotes === "function") {
1813
+ const detach = contract.on("messages.upsert", async ({ messages: msgs }) => {
1692
1814
  for (const msg of msgs) {
1693
- const pum = msg.message?.pollUpdateMessage;
1815
+ // Filter poll-update messages by reading the raw envelope from
1816
+ // the store (the neutral `BotMessage` doesn't carry the
1817
+ // `pollUpdateMessage` field — it's a Baileys-proto detail).
1818
+ const raw = store.messages.get(msg.chatId)?.get(msg.id);
1819
+ const pum = raw?.message?.pollUpdateMessage;
1694
1820
  if (!pum)
1695
1821
  continue;
1696
1822
  const creationKey = pum.pollCreationMessageKey;
@@ -1703,51 +1829,52 @@ function buildPollApi(contract, store, rawJid, guardOptions, pluginName) {
1703
1829
  if (!storeMsg || !pollEncKeyRaw || !pum.vote)
1704
1830
  continue;
1705
1831
  try {
1706
- const pollEncKey = Buffer.isBuffer(pollEncKeyRaw)
1707
- ? pollEncKeyRaw
1708
- : Buffer.from(pollEncKeyRaw, "base64");
1709
- const creatorCandidates = jidCandidates((creationKey ?? {}));
1710
- const voterCandidates = jidCandidates(msg.key);
1711
- let decryptedVote;
1712
- for (const pollCreatorJid of creatorCandidates) {
1713
- for (const voterJid of voterCandidates) {
1714
- try {
1715
- decryptedVote = decryptPollVote(pum.vote, {
1716
- pollEncKey,
1717
- pollCreatorJid,
1718
- pollMsgId: creationId,
1719
- voterJid,
1720
- });
1721
- break;
1722
- }
1723
- catch {
1724
- // try next JID combination
1725
- }
1726
- }
1727
- if (decryptedVote)
1728
- break;
1729
- }
1730
- if (!decryptedVote) {
1731
- throw new Error(`all JID combinations failed (creator=${JSON.stringify(creatorCandidates)}, voter=${JSON.stringify(voterCandidates)})`);
1832
+ const decrypted = await contract.decryptPollVote({
1833
+ voteKey: {
1834
+ id: msg.id,
1835
+ remoteJid: msg.chatId,
1836
+ fromMe: msg.fromMe,
1837
+ participant: msg.participantAlt ?? msg.fromLid ?? msg.fromPn ?? null,
1838
+ },
1839
+ pollKey: {
1840
+ id: creationId,
1841
+ remoteJid: creationKey?.remoteJid ?? null,
1842
+ fromMe: null,
1843
+ participant: creationKey?.participant ?? null,
1844
+ },
1845
+ pollEncKey: pollEncKeyRaw,
1846
+ });
1847
+ if (!decrypted) {
1848
+ throw new Error("decryptPollVote returned null — JID candidate exhaustion or stale enc key");
1732
1849
  }
1733
- const voterKey = msg.key.fromMe
1734
- ? meId
1735
- : jidNormalizedUser(msg.key.participant ?? msg.key.remoteJid ?? "");
1850
+ const voterKey = msg.fromMe
1851
+ ? (contract.me().id ?? "me")
1852
+ : jidNormalizedUser(msg.participantAlt ?? msg.fromLid ?? msg.chatId);
1736
1853
  const votesByVoter = pollVotesByCreationId.get(creationId) ?? new Map();
1737
1854
  votesByVoter.set(voterKey, {
1738
- pollUpdateMessageKey: msg.key,
1739
- vote: decryptedVote,
1855
+ pollUpdateMessageKey: { id: msg.id, remoteJid: msg.chatId, fromMe: msg.fromMe },
1856
+ vote: decrypted,
1740
1857
  senderTimestampMs: pum.senderTimestampMs,
1741
1858
  });
1742
1859
  pollVotesByCreationId.set(creationId, votesByVoter);
1743
- const aggregated = getAggregateVotesInPollMessage({ message: storeMsg.message, pollUpdates: Array.from(votesByVoter.values()) }, meId);
1860
+ const pollAggregateOpts = contract.aggregatePollVotes;
1861
+ const aggregated = pollAggregateOpts({
1862
+ pollKey: {
1863
+ id: creationId,
1864
+ remoteJid: creationKey?.remoteJid ?? null,
1865
+ fromMe: null,
1866
+ },
1867
+ votes: Array.from(votesByVoter.values()).map((v) => v.vote),
1868
+ selfJid: contract.me().id ?? undefined,
1869
+ });
1744
1870
  handle._updateFromAggregated(aggregated);
1745
1871
  }
1746
1872
  catch (err) {
1747
- logger.error(`[poll] erro ao decriptar voto: ${err}`);
1873
+ logger.error(`[poll] ${t("driver.pollDecryptFailed", { error: err })}`);
1748
1874
  }
1749
1875
  }
1750
1876
  });
1877
+ boundPollPlugins.set(pluginName, detach);
1751
1878
  }
1752
1879
  const cooldown = (guardOptions.cooldown ?? true);
1753
1880
  const jitter = (guardOptions.jitter ?? true);
@@ -1813,6 +1940,7 @@ function buildBaseApi(contract, store, pluginRegistry, pluginName) {
1813
1940
  contacts: buildContactsApi(contract, store, botJid),
1814
1941
  storage: buildStorageApi(pluginName),
1815
1942
  botId: botJid,
1943
+ commands: buildCommandsApi(),
1816
1944
  };
1817
1945
  }
1818
1946
  // ── Setup API ─────────────────────────────────────────────────────────────────
@@ -1830,12 +1958,52 @@ export function buildSetupApi(contract, store, pluginRegistry, pluginName) {
1830
1958
  return {
1831
1959
  ...buildBaseApi(contract, store, pluginRegistry, pluginName),
1832
1960
  ...buildSetupSendApi(contract, store),
1833
- admin: buildAdminApi(contract, null),
1961
+ admin: buildAdminApi(contract, store, null),
1834
1962
  events: buildEventsApi(contract, pluginName),
1835
1963
  me: buildMeApi(contract),
1836
1964
  settings: { global: buildSettingsApi(pluginName, "_global").global },
1837
1965
  };
1838
1966
  }
1967
+ // ── ctx.runCommand facet ─────────────────────────────────────────────────────
1968
+ /**
1969
+ * Builds the `ctx.runCommand(invocation, rawArgs?)` facet for a runtime
1970
+ * ctx bound to `msg`/`chat`. Deliberately built AFTER `buildApi` is
1971
+ * declared (function declarations hoist) so it can call `buildApi`
1972
+ * again, recursively, to build a context scoped to the TARGET
1973
+ * command's owning plugin rather than reusing the caller's — same
1974
+ * "own storage/plugins facet, not the caller's" principle as
1975
+ * `ctx.plugins.require()`.
1976
+ *
1977
+ * @param {BotMessage} msg
1978
+ * @param {WAChat} chat
1979
+ * @param {WaContract} contract
1980
+ * @param {BotStore} store
1981
+ * @param {Map} pluginRegistry
1982
+ * @param {string} callerPluginName
1983
+ */
1984
+ function buildRunCommandFacet(msg, chat, contract, store, pluginRegistry, callerPluginName) {
1985
+ return async function runCommandFacet(invocation, rawArgs = "") {
1986
+ const resolution = resolveDispatch(invocation, rawArgs);
1987
+ if (resolution.target.kind === "none") {
1988
+ return { status: "no_dispatch", sentReply: null, suggestedReply: null };
1989
+ }
1990
+ const entry = resolution.target.kind === "sub" ? resolution.target.parent : resolution.target.entry;
1991
+ const targetPluginName = entry.source === "plugin" ? entry.pluginName : null;
1992
+ const targetCtx = targetPluginName && targetPluginName !== callerPluginName
1993
+ ? buildApi({
1994
+ msg, chat, contract, store, pluginRegistry,
1995
+ pluginName: targetPluginName,
1996
+ guardOptions: pluginRegistry.get(targetPluginName)?.guardOptions ?? {},
1997
+ })
1998
+ : buildApi({ msg, chat, contract, store, pluginRegistry, pluginName: callerPluginName });
1999
+ return dispatchCommand({
2000
+ pluginName: targetPluginName,
2001
+ ctx: targetCtx,
2002
+ resolution,
2003
+ reply: targetCtx.msg.reply,
2004
+ });
2005
+ };
2006
+ }
1839
2007
  // ── Runtime API ───────────────────────────────────────────────────────────────
1840
2008
  /**
1841
2009
  * Runtime API — full context with message and chat.
@@ -1885,6 +2053,8 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
1885
2053
  return {
1886
2054
  ...buildBaseApi(contract, store, pluginRegistry, pluginName),
1887
2055
  ...buildSendApi(contract, store, rawJid, guardOptions),
2056
+ runCommand: buildRunCommandFacet(msg, chat, contract, store, pluginRegistry, pluginName),
2057
+ session: buildSessionApi(normJid, pluginName),
1888
2058
  // ── msg ──────────────────────────────────────────────────────────────────
1889
2059
  msg: buildMessageContext(msg, contract, store, { cooldown, jitter }),
1890
2060
  // ── chat ─────────────────────────────────────────────────────────────────
@@ -1986,7 +2156,7 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
1986
2156
  },
1987
2157
  },
1988
2158
  // ── admin ─────────────────────────────────────────────────────────────────
1989
- admin: buildAdminApi(contract, rawJid),
2159
+ admin: buildAdminApi(contract, store, rawJid),
1990
2160
  // ── me ────────────────────────────────────────────────────────────────────
1991
2161
  me: buildMeApi(contract),
1992
2162
  // ── poll ──────────────────────────────────────────────────────────────────