@manybot/manybot 5.7.0 → 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 +58 -7
  9. package/dist/drivers/baileys/api/index.js +172 -24
  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 +74 -17
  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
@@ -72,6 +72,22 @@ export function decodeContent(content) {
72
72
  type = "sticker";
73
73
  mimetype = m.stickerMessage.mimetype ?? undefined;
74
74
  }
75
+ else if (m?.templateMessage) {
76
+ type = "text";
77
+ const tpl = m.templateMessage.hydratedTemplate ?? m.templateMessage.hydratedFourRowTemplate;
78
+ const buttonUrls = tpl?.hydratedButtons?.map((b) => b.urlButton?.url).filter(Boolean).join(" ") ?? "";
79
+ body = [tpl?.hydratedContentText ?? "", buttonUrls].filter(Boolean).join(" ");
80
+ }
81
+ else if (m?.interactiveMessage) {
82
+ type = "text";
83
+ const buttonParams = m.interactiveMessage.nativeFlowMessage?.buttons
84
+ ?.map((b) => b.buttonParamsJson).filter(Boolean).join(" ") ?? "";
85
+ body = [m.interactiveMessage.body?.text ?? "", buttonParams].filter(Boolean).join(" ");
86
+ }
87
+ else if (m?.buttonsMessage) {
88
+ type = "text";
89
+ body = [m.buttonsMessage.contentText ?? "", m.buttonsMessage.footerText ?? ""].filter(Boolean).join(" ");
90
+ }
75
91
  return { type, body, mimetype };
76
92
  }
77
93
  export function createBaileysAdapter(initial) {
@@ -115,6 +131,29 @@ export function createBaileysAdapter(initial) {
115
131
  // which throws if `.message` is missing.
116
132
  return { quoted: { key: toFlatKey(quoted), message: inner } };
117
133
  }
134
+ /**
135
+ * Merge `quoted` reply-citation with the chat's current
136
+ * `ephemeralExpiration` so every outgoing send in a chat with a
137
+ * disappearing-message timer inherits it automatically. Returns
138
+ * `undefined` when there's nothing to set, so callers can pass it
139
+ * through as the third argument to `sock.sendMessage` without
140
+ * conditionally building the options object.
141
+ *
142
+ * The timer is read off the in-memory store, which is populated by
143
+ * `chats.upsert` / `chats.update` / `messages.upsert` (via the
144
+ * `ephemeralMessage` envelope) and from `groupMetadata().ephemeralDuration`
145
+ * in this same adapter — i.e. everything WhatsApp tells us about the
146
+ * chat's timer ends up there, and the send path just reads it back.
147
+ */
148
+ function buildSendOpts(jid, quoted) {
149
+ const quotedOpts = buildQuotedOpts(quoted);
150
+ const timer = store.chats.get(jid)?.ephemeralExpiration;
151
+ if (!timer)
152
+ return quotedOpts;
153
+ if (!quotedOpts)
154
+ return { ephemeralExpiration: timer };
155
+ return { ...quotedOpts, ephemeralExpiration: timer };
156
+ }
118
157
  /**
119
158
  * Translate a neutral `BotQuotedRef` into the FLAT key shape Baileys
120
159
  * expects for `react`/`delete`/`edit`/`readMessages` (proto.IMessageKey
@@ -151,6 +190,9 @@ export function createBaileysAdapter(initial) {
151
190
  m?.videoMessage?.contextInfo ??
152
191
  m?.audioMessage?.contextInfo ??
153
192
  m?.documentMessage?.contextInfo ??
193
+ m?.templateMessage?.contextInfo ??
194
+ m?.interactiveMessage?.contextInfo ??
195
+ m?.buttonsMessage?.contextInfo ??
154
196
  undefined;
155
197
  const ciTyped = contextInfo;
156
198
  return {
@@ -295,7 +337,7 @@ export function createBaileysAdapter(initial) {
295
337
  async sendText(jid, text, opts) {
296
338
  const content = { text };
297
339
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
298
- const sendOpts = buildQuotedOpts(opts?.quoted);
340
+ const sendOpts = buildSendOpts(jid, opts?.quoted);
299
341
  if (opts?.mentions?.length)
300
342
  content.mentions = opts.mentions;
301
343
  const ref = await sock.sendMessage(jid, content, sendOpts);
@@ -309,7 +351,7 @@ export function createBaileysAdapter(initial) {
309
351
  content.viewOnce = true;
310
352
  if (opts?.mentions?.length)
311
353
  content.mentions = opts.mentions;
312
- const ref = await sock.sendMessage(jid, content, buildQuotedOpts(opts?.quoted));
354
+ const ref = await sock.sendMessage(jid, content, buildSendOpts(jid, opts?.quoted));
313
355
  return toSentRef(ref, jid);
314
356
  },
315
357
  async sendVideo(jid, buffer, opts) {
@@ -322,7 +364,7 @@ export function createBaileysAdapter(initial) {
322
364
  content.gifPlayback = true;
323
365
  if (opts?.mentions?.length)
324
366
  content.mentions = opts.mentions;
325
- const ref = await sock.sendMessage(jid, content, buildQuotedOpts(opts?.quoted));
367
+ const ref = await sock.sendMessage(jid, content, buildSendOpts(jid, opts?.quoted));
326
368
  return toSentRef(ref, jid);
327
369
  },
328
370
  async sendAudio(jid, buffer, opts) {
@@ -332,20 +374,20 @@ export function createBaileysAdapter(initial) {
332
374
  content.ptt = true;
333
375
  if (opts?.viewOnce)
334
376
  content.viewOnce = true;
335
- const ref = await sock.sendMessage(jid, content, buildQuotedOpts(opts?.quoted));
377
+ const ref = await sock.sendMessage(jid, content, buildSendOpts(jid, opts?.quoted));
336
378
  return toSentRef(ref, jid);
337
379
  },
338
380
  async sendSticker(jid, buffer, opts) {
339
- const ref = await sock.sendMessage(jid, { sticker: buffer }, buildQuotedOpts(opts?.quoted));
381
+ const ref = await sock.sendMessage(jid, { sticker: buffer }, buildSendOpts(jid, opts?.quoted));
340
382
  return toSentRef(ref, jid);
341
383
  },
342
384
  async sendDocument(jid, buffer, filename, mimetype, opts) {
343
- const ref = await sock.sendMessage(jid, { document: buffer, mimetype, fileName: filename }, buildQuotedOpts(opts?.quoted));
385
+ const ref = await sock.sendMessage(jid, { document: buffer, mimetype, fileName: filename }, buildSendOpts(jid, opts?.quoted));
344
386
  return toSentRef(ref, jid);
345
387
  },
346
388
  async sendPoll(jid, opts) {
347
389
  const poll = { name: opts.name, values: opts.values, selectableCount: opts.selectableCount ?? 1 };
348
- const ref = await sock.sendMessage(jid, { poll }, buildQuotedOpts(opts.quoted));
390
+ const ref = await sock.sendMessage(jid, { poll }, buildSendOpts(jid, opts.quoted));
349
391
  return toSentRef(ref, jid);
350
392
  },
351
393
  async react(jid, target, emoji) {
@@ -427,6 +469,15 @@ export function createBaileysAdapter(initial) {
427
469
  // ── groups ─────────────────────────────────────────────────────────────
428
470
  async groupMetadata(jid) {
429
471
  const meta = await sock.groupMetadata(jid);
472
+ // WhatsApp reports the group's current disappearing-message
473
+ // timer as `ephemeralDuration` on the metadata payload — promote
474
+ // it to the in-memory store so the next send to this group
475
+ // picks it up automatically (and so the timer survives a
476
+ // snapshot round-trip without us re-fetching metadata).
477
+ const rawDuration = meta.ephemeralDuration;
478
+ if (rawDuration !== undefined) {
479
+ store.setChatEphemeralExpiration(jid, Number(rawDuration) || 0);
480
+ }
430
481
  return {
431
482
  subject: meta.subject,
432
483
  participants: meta.participants.map(p => ({
@@ -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,9 +25,12 @@ 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
35
  import { jidNormalizedUser, } from "@whiskeysockets/baileys";
32
36
  // ── Raw-Baileys escape hatch ─────────────────────────────────────────────────
@@ -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)
@@ -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,
@@ -1472,8 +1521,42 @@ async function resolveMentionJids(contract, store, jid, mentions) {
1472
1521
  }
1473
1522
  return Array.from(new Set(result.filter(Boolean)));
1474
1523
  }
1475
- function buildAdminApi(contract, chatJid) {
1476
- 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
+ }
1477
1560
  function requireChat() {
1478
1561
  if (!chatJid)
1479
1562
  throw new Error("This admin operation requires a runtime group context.");
@@ -1503,7 +1586,7 @@ function buildAdminApi(contract, chatJid) {
1503
1586
  const failed = results.filter((r) => r?.status && r.status !== "200");
1504
1587
  if (failed.length > 0) {
1505
1588
  const detail = failed.map((r) => `${r.jid ?? "?"}=${r.status}`).join(", ");
1506
- throw new Error(`groupParticipantsUpdate("${action}") rejeitado para: ${detail}`);
1589
+ throw new Error(t("driver.groupParticipantsUpdateRejected", { action, detail }));
1507
1590
  }
1508
1591
  }
1509
1592
  /**
@@ -1518,16 +1601,35 @@ function buildAdminApi(contract, chatJid) {
1518
1601
  results = await contract.groupParticipantsUpdate(jid, users, action);
1519
1602
  }
1520
1603
  catch (err) {
1521
- 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
+ }));
1522
1610
  }
1523
1611
  assertParticipantsUpdateOk(action, results);
1524
1612
  return results;
1525
1613
  }
1526
- function createTargetableAction(action, memberIds) {
1527
- const users = norm(memberIds);
1528
- 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
+ };
1529
1627
  return {
1530
- 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
+ },
1531
1633
  then(onfulfilled, onrejected) {
1532
1634
  return executeCurrent().then(onfulfilled, onrejected);
1533
1635
  },
@@ -1542,22 +1644,25 @@ function buildAdminApi(contract, chatJid) {
1542
1644
  return {
1543
1645
  /** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
1544
1646
  add(memberIds) {
1545
- return createTargetableAction((jid, users) => runParticipantsUpdate(jid, users, "add"), memberIds);
1647
+ return createTargetableAction((jid, users) => runParticipantsUpdate(jid, users, "add"), memberIds, "newMember");
1546
1648
  },
1547
1649
  /** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
1548
1650
  async kick(memberIds) {
1549
1651
  requireChat();
1550
- return runParticipantsUpdate(chatJid, norm(memberIds), "remove");
1652
+ const users = await resolveTargets(chatJid, Array.isArray(memberIds) ? memberIds : [memberIds]);
1653
+ return runParticipantsUpdate(chatJid, users, "remove");
1551
1654
  },
1552
1655
  /** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
1553
1656
  async promote(memberIds) {
1554
1657
  requireChat();
1555
- return runParticipantsUpdate(chatJid, norm(memberIds), "promote");
1658
+ const users = await resolveTargets(chatJid, Array.isArray(memberIds) ? memberIds : [memberIds]);
1659
+ return runParticipantsUpdate(chatJid, users, "promote");
1556
1660
  },
1557
1661
  /** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
1558
1662
  async demote(memberIds) {
1559
1663
  requireChat();
1560
- return runParticipantsUpdate(chatJid, norm(memberIds), "demote");
1664
+ const users = await resolveTargets(chatJid, Array.isArray(memberIds) ? memberIds : [memberIds]);
1665
+ return runParticipantsUpdate(chatJid, users, "demote");
1561
1666
  },
1562
1667
  /** @param {string} name */
1563
1668
  async setSubject(name) {
@@ -1765,7 +1870,7 @@ function buildPollApi(contract, store, rawJid, guardOptions, pluginName) {
1765
1870
  handle._updateFromAggregated(aggregated);
1766
1871
  }
1767
1872
  catch (err) {
1768
- logger.error(`[poll] erro ao decriptar voto: ${err}`);
1873
+ logger.error(`[poll] ${t("driver.pollDecryptFailed", { error: err })}`);
1769
1874
  }
1770
1875
  }
1771
1876
  });
@@ -1835,6 +1940,7 @@ function buildBaseApi(contract, store, pluginRegistry, pluginName) {
1835
1940
  contacts: buildContactsApi(contract, store, botJid),
1836
1941
  storage: buildStorageApi(pluginName),
1837
1942
  botId: botJid,
1943
+ commands: buildCommandsApi(),
1838
1944
  };
1839
1945
  }
1840
1946
  // ── Setup API ─────────────────────────────────────────────────────────────────
@@ -1852,12 +1958,52 @@ export function buildSetupApi(contract, store, pluginRegistry, pluginName) {
1852
1958
  return {
1853
1959
  ...buildBaseApi(contract, store, pluginRegistry, pluginName),
1854
1960
  ...buildSetupSendApi(contract, store),
1855
- admin: buildAdminApi(contract, null),
1961
+ admin: buildAdminApi(contract, store, null),
1856
1962
  events: buildEventsApi(contract, pluginName),
1857
1963
  me: buildMeApi(contract),
1858
1964
  settings: { global: buildSettingsApi(pluginName, "_global").global },
1859
1965
  };
1860
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
+ }
1861
2007
  // ── Runtime API ───────────────────────────────────────────────────────────────
1862
2008
  /**
1863
2009
  * Runtime API — full context with message and chat.
@@ -1907,6 +2053,8 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
1907
2053
  return {
1908
2054
  ...buildBaseApi(contract, store, pluginRegistry, pluginName),
1909
2055
  ...buildSendApi(contract, store, rawJid, guardOptions),
2056
+ runCommand: buildRunCommandFacet(msg, chat, contract, store, pluginRegistry, pluginName),
2057
+ session: buildSessionApi(normJid, pluginName),
1910
2058
  // ── msg ──────────────────────────────────────────────────────────────────
1911
2059
  msg: buildMessageContext(msg, contract, store, { cooldown, jitter }),
1912
2060
  // ── chat ─────────────────────────────────────────────────────────────────
@@ -2008,7 +2156,7 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
2008
2156
  },
2009
2157
  },
2010
2158
  // ── admin ─────────────────────────────────────────────────────────────────
2011
- admin: buildAdminApi(contract, rawJid),
2159
+ admin: buildAdminApi(contract, store, rawJid),
2012
2160
  // ── me ────────────────────────────────────────────────────────────────────
2013
2161
  me: buildMeApi(contract),
2014
2162
  // ── poll ──────────────────────────────────────────────────────────────────