@manybot/manybot 5.7.0 → 5.9.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 (81) hide show
  1. package/README.md +28 -3
  2. package/dist/client/banner.js +10 -0
  3. package/dist/client/banner.test.js +31 -0
  4. package/dist/client/store.js +91 -6
  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/download/queue.js +13 -4
  9. package/dist/drivers/baileys/adapter.js +133 -15
  10. package/dist/drivers/baileys/api/contacts.integration.test.js +261 -0
  11. package/dist/drivers/baileys/api/groupMeta.test.js +235 -0
  12. package/dist/drivers/baileys/api/index.js +384 -62
  13. package/dist/drivers/baileys/index.js +92 -36
  14. package/dist/drivers/baileys/loginPrompt.js +0 -2
  15. package/dist/drivers/baileys/messageHandler.js +344 -4
  16. package/dist/drivers/baileys/messageHandler.test.js +445 -0
  17. package/dist/drivers/baileysAdapter.test.js +378 -0
  18. package/dist/drivers/jid.js +26 -0
  19. package/dist/drivers/jid.test.js +74 -0
  20. package/dist/drivers/types.js +5 -5
  21. package/dist/i18n/index.js +20 -24
  22. package/dist/kernel/activeDriverSend.js +21 -0
  23. package/dist/kernel/activeDriverSend.test.js +89 -0
  24. package/dist/kernel/alerts.js +3 -9
  25. package/dist/kernel/chatOverrides.js +46 -0
  26. package/dist/kernel/chatOverrides.test.js +59 -0
  27. package/dist/kernel/chatSession.js +65 -0
  28. package/dist/kernel/chatSession.test.js +46 -0
  29. package/dist/kernel/commandAccess.js +66 -0
  30. package/dist/kernel/commandAccess.test.js +74 -0
  31. package/dist/kernel/commandDeprecation.js +170 -0
  32. package/dist/kernel/commandDeprecation.test.js +114 -0
  33. package/dist/kernel/commandMenu.js +357 -0
  34. package/dist/kernel/commandMenu.test.js +363 -0
  35. package/dist/kernel/commandPermissions.js +171 -0
  36. package/dist/kernel/commandPermissions.test.js +227 -0
  37. package/dist/kernel/commandRegistry.js +583 -0
  38. package/dist/kernel/commandRegistry.test.js +158 -0
  39. package/dist/kernel/commandsConfig.js +949 -0
  40. package/dist/kernel/commandsConfig.test.js +482 -0
  41. package/dist/kernel/contactAutoSave.js +6 -6
  42. package/dist/kernel/contactAutoSave.test.js +87 -0
  43. package/dist/kernel/coreCommands.js +62 -0
  44. package/dist/kernel/driverManager.js +10 -6
  45. package/dist/kernel/driverManager.test.js +90 -0
  46. package/dist/kernel/integrationMode.js +88 -0
  47. package/dist/kernel/integrationMode.test.js +95 -0
  48. package/dist/kernel/loadIntegrationPlugin.test.js +67 -0
  49. package/dist/kernel/pluginApi.test.js +600 -0
  50. package/dist/kernel/pluginGuard.js +18 -13
  51. package/dist/kernel/pluginGuard.test.js +39 -0
  52. package/dist/kernel/pluginLoader.js +169 -11
  53. package/dist/kernel/pluginLoader.test.js +190 -0
  54. package/dist/kernel/runCommand.js +284 -0
  55. package/dist/kernel/runCommand.test.js +497 -0
  56. package/dist/kernel/sendFallbackGuard.js +19 -48
  57. package/dist/kernel/sendFallbackGuard.test.js +80 -0
  58. package/dist/kernel/sendGuard.js +38 -42
  59. package/dist/kernel/sendGuard.test.js +102 -0
  60. package/dist/kernel/settingsDb.js +19 -5
  61. package/dist/kernel/statusServer.js +9 -2
  62. package/dist/kernel/statusServer.test.js +70 -0
  63. package/dist/kernel/testConfig.js +192 -0
  64. package/dist/kernel/testConfig.test.js +181 -0
  65. package/dist/kernel/updateCheck.js +33 -10
  66. package/dist/locales/en.json +77 -13
  67. package/dist/locales/es.json +77 -13
  68. package/dist/locales/pt.json +77 -13
  69. package/dist/logger/logger.js +23 -3
  70. package/dist/logger/logger.test.js +45 -0
  71. package/dist/main.js +5 -76
  72. package/dist/plugins/__manybot_integration__/index.js +184 -0
  73. package/dist/plugins/__manybot_integration__/index.test.js +218 -0
  74. package/dist/utils/phoneNumber.js +83 -0
  75. package/dist/utils/phoneNumber.test.js +53 -0
  76. package/package.json +76 -18
  77. package/dist/drivers/whatsmeow/client.js +0 -252
  78. package/dist/drivers/whatsmeow/index.js +0 -79
  79. package/dist/drivers/whatsmeow/installer.js +0 -86
  80. package/dist/drivers/whatsmeow/supervisor.js +0 -328
  81. package/dist/drivers/whatsmeow/whatsmeow.proto +0 -64
@@ -13,9 +13,12 @@ import { decodeContent } from "#drivers/baileys/adapter.js";
13
13
  import { logger } from "#logger";
14
14
  import { t, createPluginT, reloadTranslations, getCurrentLang } from "#i18n";
15
15
  import { CONFIG, CONFIG_DIR } from "#config";
16
+ import { getChatPrefix } from "#kernel/chatOverrides.js";
16
17
  import { enqueue } from "#download";
18
+ import { waitForEditSlot } from "#kernel/sendGuard.js";
17
19
  import { schedule, cancelPlugin } from "#kernel/scheduler.js";
18
20
  import { emptyFolder } from "#utils/file.js";
21
+ import { parsePhone } from "#utils/phoneNumber.js";
19
22
  import { normalizeJid, denormalizeJid, toWireJid } from "#drivers/jid.js";
20
23
  import { mkdirSync } from "fs";
21
24
  import { readFile, writeFile, unlink, mkdtemp, rm } from "fs/promises";
@@ -24,9 +27,12 @@ import path from "path";
24
27
  import os from "os";
25
28
  import { spawn } from "child_process";
26
29
  import { randomUUID } from "crypto";
27
- import { waitForSendSlot, simulateState, typingDuration, mediaDuration, waitForEditSlot } from "#sendguard";
30
+ import { waitForSendSlot, simulateState, typingDuration, mediaDuration } from "#sendguard";
28
31
  import { sendWithFallback } from "#kernel/sendFallbackGuard.js";
29
32
  import { buildSettingsApi } from "#settingsdb";
33
+ import * as commandAccess from "#kernel/commandAccess.js";
34
+ import * as chatSession from "#kernel/chatSession.js";
35
+ import { resolveDispatch, runCommand as dispatchCommand } from "#kernel/runCommand.js";
30
36
  import WebP from "node-webpmux";
31
37
  import { jidNormalizedUser, } from "@whiskeysockets/baileys";
32
38
  // ── Raw-Baileys escape hatch ─────────────────────────────────────────────────
@@ -142,14 +148,35 @@ function msgIsGif(msg, store) {
142
148
  return !!raw.message?.videoMessage?.gifPlayback;
143
149
  }
144
150
  /** Sender JID — group participant or DM remote JID, normalized. */
145
- function getMsgSender(msg, store) {
146
- // Prefer the @s.whatsapp.net form when we know it (Baileys-advisor split
147
- // exposes both forms on incoming messages). The adapter fills
148
- // `fromPn` when known; fall back to `chatId` for DMs.
149
- const participant = msg._raw?.key?.participant
150
- ?? msg.fromPn;
151
- const raw = participant ?? msg.chatId;
152
- return normalizeJid(store.resolveJid(normalizeJid(raw)));
151
+ function getMsgSender(msg) {
152
+ // LID-canonical, matching normalizeContact()'s `id` and every other
153
+ // participant lookup in this file (`participantAlt ?? fromLid ?? fromPn
154
+ // ?? null`). `participantAlt` carries the alt-LID on group messages;
155
+ // `remoteJidAlt` carries it on DMs (Baileys puts the DM alt-LID on the
156
+ // key's remoteJidAlt, not participantAlt — there's no participant field
157
+ // at all in a 1:1 chat). When the account is already LID-addressed
158
+ // (addressingMode "lid"), `chatId` itself IS the @lid form and
159
+ // `remoteJidAlt` may hold the PN alt instead — check chatId first so
160
+ // that case isn't missed. No PN fallback: null is the correct, expected
161
+ // value when no LID has been learned for this contact yet, not a
162
+ // reason to silently hand back a legacy @c.us identity.
163
+ const dmLid = msg.chatId?.endsWith("@lid") ? msg.chatId : msg.remoteJidAlt;
164
+ const lid = msg.participantAlt ?? msg.fromLid ?? dmLid ?? null;
165
+ return lid ? normalizeJid(lid) : null;
166
+ }
167
+ /**
168
+ * PN companion of getMsgSender(), for the few internal consumers (command
169
+ * permissions today) that still need to match against phone-number-based
170
+ * config the way it's always been written. Never exposed on ctx.msg —
171
+ * `sender` there is LID-canonical only, per Baileys' own "migrate to LID,
172
+ * don't try to restore PN" guidance.
173
+ */
174
+ function getMsgSenderPn(msg) {
175
+ if (msg.fromPn)
176
+ return normalizeJid(msg.fromPn);
177
+ if (msg.chatId && !msg.chatId.endsWith("@lid"))
178
+ return normalizeJid(msg.chatId);
179
+ return null;
153
180
  }
154
181
  /** Quoted-message metadata as the rest of the api uses it. */
155
182
  function getQuotedContext(msg) {
@@ -175,13 +202,13 @@ function hasMention(contextInfo) {
175
202
  return !!(ci?.mentionedJid && Array.isArray(ci.mentionedJid) && ci.mentionedJid.length > 0);
176
203
  }
177
204
  /** True if the bot's own JID (PN or LID) is in contextInfo.mentionedJid. */
178
- function hasBotMention(contextInfo, sock, store) {
205
+ function hasBotMention(contextInfo, contract, store) {
179
206
  const ci = contextInfo;
180
207
  const mentioned = ci?.mentionedJid;
181
208
  if (!mentioned || mentioned.length === 0)
182
209
  return false;
183
- const botLid = sock.user?.lid;
184
- const botCandidates = [sock.user?.id, botLid]
210
+ const me = contract.me();
211
+ const botCandidates = [me.id, me.lid]
185
212
  .filter((v) => !!v)
186
213
  .map(v => normalizeJid(store.resolveJid(normalizeJid(v))));
187
214
  if (botCandidates.length === 0)
@@ -218,6 +245,25 @@ async function getGroupMetadataCached(contract, jid) {
218
245
  groupMetaCache.set(jid, { meta, at: Date.now() });
219
246
  return meta;
220
247
  }
248
+ /**
249
+ * Same fetch as {@link getGroupMetadataCached}, but always hits the network
250
+ * and never reads the TTL cache — used for admin permission checks
251
+ * (`isAdmin`/`isSenderAdmin`), which gate the `admin:`/`botAdmin:` command
252
+ * permission and so cannot tolerate the up-to-5-minute staleness window.
253
+ * Invalidation on `group-participants.update` closes most of that window,
254
+ * but Baileys' own event-buffer bugs during reconnects (see the welcome-
255
+ * message gating fix) can drop the event entirely — leaving a demoted
256
+ * admin able to keep running admin-only commands until the TTL expires,
257
+ * or indefinitely if no later promote/demote re-triggers invalidation.
258
+ * The result is still written into the shared cache so unrelated cached
259
+ * reads (`getParticipants()`, chat name) benefit from the fresh fetch too.
260
+ */
261
+ async function getGroupMetadataFresh(contract, jid) {
262
+ const sock = rawSocketOf(contract);
263
+ const meta = await sock.groupMetadata(jid);
264
+ groupMetaCache.set(jid, { meta, at: Date.now() });
265
+ return meta;
266
+ }
221
267
  let groupMetaInvalidationBound = false;
222
268
  function bindGroupMetaInvalidation(contract) {
223
269
  if (groupMetaInvalidationBound)
@@ -232,6 +278,14 @@ function bindGroupMetaInvalidation(contract) {
232
278
  groupMetaCache.delete(u.id);
233
279
  });
234
280
  }
281
+ /** Test-only: clears the group-metadata cache and re-arms invalidation
282
+ * binding so each test starts isolated (module-level singletons otherwise
283
+ * persist for the life of the process). Not for production use. */
284
+ export function __resetGroupMetaCacheForTests() {
285
+ groupMetaCache.clear();
286
+ groupNameCache.clear();
287
+ groupMetaInvalidationBound = false;
288
+ }
235
289
  /**
236
290
  * Build a WAChat adapter from a BotMessage + store.
237
291
  * Exposed for use in messageHandler.ts.
@@ -351,7 +405,9 @@ function buildConfigApi() {
351
405
  * @param {any} [defaultValue]
352
406
  */
353
407
  get(key, defaultValue = null) {
354
- return CONFIG[key] ?? defaultValue;
408
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- `get<T>` is a
409
+ // type-level convenience for callers; CONFIG itself is untyped.
410
+ return (CONFIG[key] ?? defaultValue);
355
411
  },
356
412
  };
357
413
  }
@@ -459,13 +515,72 @@ function mentionDisplayName(jid) {
459
515
  * Build a normalized contact object from a JID and optional store metadata.
460
516
  * isBusiness is resolved via contract.getBusinessProfile(jid) — it resolves
461
517
  * to a profile only for WhatsApp Business accounts, undefined otherwise.
518
+ *
519
+ * `id` invariant: the returned ID is the LID form whenever the bot can
520
+ * guarantee it (input was a `@lid`, or we resolved a `@s.whatsapp.net`
521
+ * JID through the LID↔PN cache). When the input is a phone-based JID
522
+ * and we don't have a LID mapping yet, `id` is `null` — handing back a
523
+ * PN in that case would be a lie about the user's preferred identity
524
+ * (LID is now the internal addressing mode WhatsApp uses; see MANYBOT-7
525
+ * and the Baileys 7 LID rollout).
526
+ *
527
+ * Phone fields (`number`/`numberRaw`/`numberPretty`/`country`/
528
+ * `countryCallingCode`) are populated by {@link parsePhone} once a PN
529
+ * form is known. If we have neither a PN nor a resolvable LID, every
530
+ * phone field stays `null`.
531
+ *
532
+ * Groups keep their `@g.us` JID as `id` — they're not subject to the
533
+ * LID/PN distinction and never have a phone number to parse.
534
+ *
462
535
  * @param {string} jid
463
536
  * @param {RawStoreContact} [info]
464
537
  * @param {string|null} [botJid]
465
538
  * @param {WaContract} [contract]
539
+ * @param {string|null} [botLid]
466
540
  */
467
- async function normalizeContact(jid, info, botJid, contract) {
468
- const number = jid.split("@")[0];
541
+ async function normalizeContact(jid, info, botJid, contract, store, botLid) {
542
+ // Compute the canonical ID form (LID for users, group JID for groups,
543
+ // null when we can't pin a LID) and the resolved PN form (if any) in
544
+ // one pass — both are derived from the same LID↔PN cache + protocol-level
545
+ // resolver lookups, and a successful PN resolution feeds learnLid() so
546
+ // the next call is fully synchronous.
547
+ const isGroup = jid.endsWith("@g.us");
548
+ let resolvedId = isGroup ? jid : null;
549
+ let resolvedPn = null;
550
+ if (!isGroup) {
551
+ if (jid.endsWith("@lid")) {
552
+ // Input is already LID — that's the canonical ID; look up the PN.
553
+ resolvedId = jid;
554
+ const cached = store.resolveJid(jid);
555
+ if (cached && cached !== jid) {
556
+ resolvedPn = cached;
557
+ }
558
+ else if (contract?.resolveLid) {
559
+ try {
560
+ const fresh = await contract.resolveLid(jid);
561
+ if (fresh) {
562
+ store.learnLid(jid, fresh);
563
+ resolvedPn = fresh;
564
+ }
565
+ }
566
+ catch { /* fall through to null — caller decides */ }
567
+ }
568
+ }
569
+ else if (jid.endsWith("@s.whatsapp.net") || jid.endsWith("@c.us")) {
570
+ // Input is PN. Prefer the LID form for `id`; cache lookup is sync.
571
+ const lid = store.resolvePn(jid);
572
+ if (lid) {
573
+ resolvedId = lid;
574
+ resolvedPn = normalizeJid(jid);
575
+ }
576
+ else {
577
+ // No LID mapping yet — id stays null per the invariant.
578
+ resolvedPn = normalizeJid(jid);
579
+ }
580
+ }
581
+ // Anything else (status broadcast, newsletter, meta AI…) — id = null.
582
+ }
583
+ const phone = parsePhone(resolvedPn);
469
584
  let isBusiness = false;
470
585
  // We already have a contact record for this jid (learned from a real
471
586
  // contacts.upsert or an actual message from them) — that alone proves
@@ -474,7 +589,7 @@ async function normalizeContact(jid, info, botJid, contract) {
474
589
  // raw @lid we've never resolved to a phone number (returns a false
475
590
  // "doesn't exist" instead of throwing).
476
591
  let isWAAccount = Boolean(info);
477
- if (!isWAAccount && contract && !jid.endsWith("@g.us")) {
592
+ if (!isWAAccount && contract && !isGroup) {
478
593
  try {
479
594
  const results = await contract.onWhatsApp(jid);
480
595
  isWAAccount = Boolean(results?.[0]?.exists);
@@ -488,9 +603,9 @@ async function normalizeContact(jid, info, botJid, contract) {
488
603
  // which we can't verify this way) — nothing backs this contact.
489
604
  // Matches the old whatsapp-web.js contract: getContactById() threw /
490
605
  // resolved to null for an unknown ID instead of returning a hollow object.
491
- if (!jid.endsWith("@g.us") && !isWAAccount)
606
+ if (!isGroup && !isWAAccount)
492
607
  return null;
493
- if (contract && !jid.endsWith("@g.us")) {
608
+ if (contract && !isGroup) {
494
609
  try {
495
610
  isBusiness = Boolean(await contract.getBusinessProfile(jid));
496
611
  }
@@ -499,8 +614,12 @@ async function normalizeContact(jid, info, botJid, contract) {
499
614
  }
500
615
  }
501
616
  return {
502
- id: jid,
503
- number,
617
+ id: resolvedId,
618
+ number: phone.number,
619
+ numberRaw: phone.numberRaw,
620
+ numberPretty: phone.numberPretty,
621
+ country: phone.country,
622
+ countryCallingCode: phone.countryCallingCode,
504
623
  pushname: info?.notify ?? null,
505
624
  name: info?.name ?? info?.verifiedName ?? null,
506
625
  // Baileys' Contact type has no "short name" equivalent (that's a
@@ -509,14 +628,49 @@ async function normalizeContact(jid, info, botJid, contract) {
509
628
  isBusiness,
510
629
  isEnterprise: false,
511
630
  isBlocked: false,
512
- isMe: botJid ? jid === normalizeJid(botJid) : false,
631
+ isMe: (botJid && jid === normalizeJid(botJid)) || (botLid && jid === normalizeJid(botLid)) || false,
513
632
  isWAAccount,
514
- isUser: !jid.endsWith("@g.us"),
515
- isGroup: jid.endsWith("@g.us"),
516
- mention: { text: `@${mentionDisplayName(jid)}`, mentions: [toWireJid(jid)] },
633
+ isUser: !isGroup,
634
+ isGroup,
635
+ // Mention text: for `@lid` contacts WhatsApp always renders the raw
636
+ // LID digits (privacy — it never reveals the phone number here, even
637
+ // when we've resolved one internally), matching mobile's "no saved
638
+ // contact" fallback. Only PN-native contacts get the pretty/raw phone
639
+ // form.
640
+ mention: {
641
+ text: resolvedId?.endsWith("@lid")
642
+ ? `@${mentionDisplayName(resolvedId)}`
643
+ : phone.numberPretty
644
+ ? `@${phone.numberPretty}`
645
+ : phone.numberRaw
646
+ ? `@${phone.numberRaw}`
647
+ : `@${mentionDisplayName(jid)}`,
648
+ mentions: [toWireJid(jid)],
649
+ },
517
650
  };
518
651
  }
519
652
  // ── Chats API ─────────────────────────────────────────────────────────────────
653
+ function buildCommandsApi() {
654
+ return {
655
+ exists: commandAccess.exists,
656
+ desc: commandAccess.desc,
657
+ manual: commandAccess.manual,
658
+ list: commandAccess.list,
659
+ isMenuAlias: commandAccess.isMenuAlias,
660
+ };
661
+ }
662
+ // ── Session API (Phase 7, MANYBOT-6.md) ─────────────────────────────────────
663
+ // Scoped to the current chat (normJid) + the calling plugin. Only built in
664
+ // buildApi() (runtime), not buildBaseApi() — there is no "current chat" at
665
+ // setup time to lock.
666
+ function buildSessionApi(chatId, pluginName) {
667
+ return {
668
+ acquire: () => chatSession.acquireSession(chatId, pluginName),
669
+ release: () => { chatSession.releaseSession(chatId, pluginName); },
670
+ isLocked: () => chatSession.isSessionLocked(chatId),
671
+ isMine: () => chatSession.getSessionHolder(chatId) === pluginName,
672
+ };
673
+ }
520
674
  function buildChatsApi(store) {
521
675
  return {
522
676
  /**
@@ -533,7 +687,15 @@ function buildChatsApi(store) {
533
687
  };
534
688
  }
535
689
  // ── Contact API ───────────────────────────────────────────────────────────────
536
- function buildContactsApi(contract, store, botJid) {
690
+ export function buildContactsApi(contract, store, botJid, botLid) {
691
+ // Teach the store the bot's own LID↔PN mapping proactively — it's
692
+ // known synchronously from contract.me() and doesn't need a resolveLid()
693
+ // round trip or a prior sync to have taught the store this pairing
694
+ // (e.g. contacts.get() on the bot's own id before any message from the
695
+ // bot's own account arrived to populate it via message handling).
696
+ // Without this, self-lookups keep every phone-number field null.
697
+ if (botLid && botJid)
698
+ store.learnLid(botLid, botJid);
537
699
  return {
538
700
  /**
539
701
  * Get a normalized contact object by JID.
@@ -602,7 +764,8 @@ function buildContactsApi(contract, store, botJid) {
602
764
  if (botJid &&
603
765
  normalizedContactId.endsWith("@lid") &&
604
766
  resolved === normalizeJid(botJid) &&
605
- normalizedContactId !== normalizeJid(botJid)) {
767
+ normalizedContactId !== normalizeJid(botJid) &&
768
+ !(botLid && normalizedContactId === normalizeJid(botLid))) {
606
769
  logger.warn(`[contacts.get] "${contactId}" resolved to the bot's own JID — discarding stale lidMap entry, treating as unresolved.`);
607
770
  store.forgetLid(normalizedContactId);
608
771
  resolved = normalizedContactId;
@@ -625,7 +788,7 @@ function buildContactsApi(contract, store, botJid) {
625
788
  notify: resolvedInfo?.notify ?? raw?.notify,
626
789
  verifiedName: resolvedInfo?.verifiedName ?? raw?.verifiedName,
627
790
  } : undefined;
628
- return normalizeContact(resolved, info, botJid, contract);
791
+ return normalizeContact(resolved, info, botJid, contract, store, botLid);
629
792
  },
630
793
  /**
631
794
  * Get the profile picture URL of a contact.
@@ -713,21 +876,33 @@ function makeHistoryArray(entries, store) {
713
876
  const arr = entries;
714
877
  arr.last = (n) => makeHistoryArray(typeof n === "number" ? entries.slice(-n) : entries.slice(), store);
715
878
  arr.from = (senderId) => {
716
- const target = normalizeJid(store.resolveJid(normalizeJid(senderId)));
879
+ // `e.sender` is LID-canonical (see getMsgSender()) — resolve a
880
+ // phone-number input to its LID before comparing. If it's already
881
+ // `@lid`, pass through unchanged. If no LID mapping is known for a
882
+ // PN input, there's nothing to match against (filters to empty),
883
+ // same as e.sender being null for that contact.
884
+ const normalized = normalizeJid(senderId.trim());
885
+ const target = normalized.endsWith("@lid") ? normalized : store.resolvePn(normalized);
886
+ if (!target)
887
+ return makeHistoryArray([], store);
717
888
  return makeHistoryArray(entries.filter((e) => e.sender === target), store);
718
889
  };
719
890
  return arr;
720
891
  }
721
892
  export function buildMessageContext(msg, contract, store, guardOptions = {}) {
722
- const sock = rawSocketOf(contract);
723
893
  const body = getMsgBody(msg);
724
- const prefix = CONFIG.CMD_PREFIX;
894
+ const prefix = getChatPrefix(msg.chatId);
725
895
  const rawArgs = body.trim().split(/\s+/);
726
896
  const first = rawArgs[0]?.toLowerCase() ?? "";
727
897
  const hasPrefix = first.startsWith(prefix);
728
898
  const command = hasPrefix ? first.slice(prefix.length) : "";
729
899
  const rawJid = msg.chatId;
730
- const sender = getMsgSender(msg, store);
900
+ const sender = getMsgSender(msg);
901
+ const senderPn = getMsgSenderPn(msg);
902
+ // Best-known identity for anything that still needs a non-null jid
903
+ // internally (contact lookup, the sender-name fallback below) — prefers
904
+ // LID, falls back to PN, and finally the chat itself so it's never empty.
905
+ const contactId = sender ?? senderPn ?? msg.chatId;
731
906
  const cooldown = guardOptions.cooldown ?? true;
732
907
  const jitter = guardOptions.jitter ?? true;
733
908
  const contextInfo = getContextInfo(msg);
@@ -742,6 +917,21 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
742
917
  // We carry the same _raw.contextInfo on the synthetic so a recursive
743
918
  // getReply().getReply() keeps working (the inner call re-reads
744
919
  // getContextInfo off _raw.contextInfo).
920
+ //
921
+ // WhatsApp's contextInfo never carries the quoted author's pushname, so
922
+ // we best-effort it from the local contact store (same lookup pattern
923
+ // as getContact() below) instead of leaving it undefined -> falling
924
+ // back to the raw phone number in senderName.
925
+ const lookupPushName = (jid) => {
926
+ if (!jid)
927
+ return undefined;
928
+ const contacts = store.contacts;
929
+ const info = contacts[jid]
930
+ ?? contacts[denormalizeJid(jid)]
931
+ ?? contacts[store.resolveJid(jid)]
932
+ ?? contacts[denormalizeJid(store.resolveJid(jid))];
933
+ return info?.notify ?? undefined;
934
+ };
745
935
  const quotedRaw = contextInfo?.quotedMessage
746
936
  ? (() => {
747
937
  const decoded = decodeContent(contextInfo.quotedMessage);
@@ -754,6 +944,8 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
754
944
  timestamp: 0,
755
945
  body: decoded.body,
756
946
  mimetype: decoded.mimetype,
947
+ fromPn: contextInfo.participant ?? undefined,
948
+ pushName: lookupPushName(contextInfo.participant),
757
949
  _raw: {
758
950
  contextInfo: {
759
951
  stanzaId: contextInfo.stanzaId,
@@ -777,6 +969,8 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
777
969
  type: "other",
778
970
  contentHash: "",
779
971
  timestamp: 0,
972
+ fromPn: msg.quotedKey.participant ?? undefined,
973
+ pushName: lookupPushName(msg.quotedKey.participant),
780
974
  }
781
975
  : null;
782
976
  return {
@@ -786,7 +980,8 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
786
980
  type: getMsgType(msg),
787
981
  fromMe: msg.fromMe,
788
982
  sender,
789
- senderName: msg.pushName ?? sender.replace(/(:\d+)?@.*$/, ""),
983
+ senderPn,
984
+ senderName: msg.pushName ?? contactId.replace(/(:\d+)?@.*$/, ""),
790
985
  command,
791
986
  args: rawArgs.slice(1),
792
987
  is(cmd) {
@@ -794,6 +989,7 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
794
989
  },
795
990
  hasMedia: msgHasMedia(msg),
796
991
  isGif: msgIsGif(msg, store),
992
+ mentionedJid: msg.mentionedJid ?? [],
797
993
  async downloadMedia(opts = {}) {
798
994
  try {
799
995
  // contract.downloadMedia handles reupload internally via the
@@ -821,7 +1017,7 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
821
1017
  return buildMessageContext(quotedRaw, contract, store, { cooldown: false, jitter: false });
822
1018
  },
823
1019
  hasMention: hasMention(contextInfo),
824
- hasBotMention: hasBotMention(contextInfo, sock, store),
1020
+ hasBotMention: hasBotMention(contextInfo, contract, store),
825
1021
  reply: makeSender(contract, store, rawJid, msg, { cooldown, jitter }),
826
1022
  async react(emoji) {
827
1023
  await contract.react(rawJid, {
@@ -845,7 +1041,10 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
845
1041
  if (!msg.fromMe) {
846
1042
  throw new Error("[pluginApi] edit() can only be used on the bot's own messages");
847
1043
  }
848
- if (!msg.id || !(await waitForEditSlot(msg.id)))
1044
+ if (!msg.id)
1045
+ return;
1046
+ const allowed = await waitForEditSlot(msg.id);
1047
+ if (!allowed)
849
1048
  return;
850
1049
  await contract.editMessage(rawJid, {
851
1050
  id: msg.id,
@@ -863,12 +1062,13 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
863
1062
  * @returns {Promise<object|null>} null if the sender can't be confirmed as a real WhatsApp account.
864
1063
  */
865
1064
  async getContact() {
866
- const info = store.contacts[sender]
867
- ?? store.contacts[denormalizeJid(sender)]
1065
+ const info = store.contacts[contactId]
1066
+ ?? store.contacts[denormalizeJid(contactId)]
868
1067
  ?? store.contacts[store.resolveJid(msg.fromPn ?? "")]
869
1068
  ?? store.contacts[denormalizeJid(store.resolveJid(msg.fromPn ?? ""))];
870
- const botJid = sock.user?.id ? jidNormalizedUser(sock.user.id) : null;
871
- return normalizeContact(sender, info, botJid, contract);
1069
+ const botJid = contract.me().id ? jidNormalizedUser(contract.me().id) : null;
1070
+ const botLid = contract.me().lid ? normalizeJid(contract.me().lid) : null;
1071
+ return normalizeContact(contactId, info, botJid, contract, store, botLid);
872
1072
  },
873
1073
  };
874
1074
  }
@@ -961,7 +1161,10 @@ class MessageHandle {
961
1161
  if (!msg.fromMe) {
962
1162
  throw new Error("[pluginApi] edit() can only be used on the bot's own messages");
963
1163
  }
964
- if (!msg.id || !(await waitForEditSlot(msg.id)))
1164
+ if (!msg.id)
1165
+ return;
1166
+ const allowed = await waitForEditSlot(msg.id);
1167
+ if (!allowed)
965
1168
  return;
966
1169
  await this._contract.editMessage(msg.chatId, {
967
1170
  id: msg.id,
@@ -1472,8 +1675,45 @@ async function resolveMentionJids(contract, store, jid, mentions) {
1472
1675
  }
1473
1676
  return Array.from(new Set(result.filter(Boolean)));
1474
1677
  }
1475
- function buildAdminApi(contract, chatJid) {
1476
- const norm = (v) => (Array.isArray(v) ? v : [v]).map(toWireJid);
1678
+ function buildAdminApi(contract, store, chatJid) {
1679
+ const me = contract.me();
1680
+ const botJid = me.id ? normalizeJid(jidNormalizedUser(me.id)) : null;
1681
+ const botLid = me.lid ? normalizeJid(me.lid) : null;
1682
+ /**
1683
+ * Resolve admin-supplied identifiers (bare phone number, @c.us,
1684
+ * @s.whatsapp.net, or @lid) to the exact jid WhatsApp has on file for
1685
+ * that participant *in this group*. A naive toWireJid() guess is only
1686
+ * correct for pn-addressed groups — lid-addressed groups (increasingly
1687
+ * common, e.g. when a member hides their phone number) only recognize
1688
+ * that member by @lid, which a phone number typed by the admin can't
1689
+ * produce on its own. Baileys' raw groupMetadata() participants carry
1690
+ * `id` (whatever WA addresses them as here), `jid` (its best
1691
+ * phone-number guess) and `lid` — check all three.
1692
+ *
1693
+ * Throws per-identifier if it doesn't match a current member, instead
1694
+ * of silently sending a guessed jid that WhatsApp rejects deep inside
1695
+ * the per-participant status array (see assertParticipantsUpdateOk).
1696
+ */
1697
+ async function resolveTargets(groupJid, identifiers) {
1698
+ const meta = await getGroupMetadataCached(contract, groupJid);
1699
+ const out = [];
1700
+ for (const id of identifiers) {
1701
+ const wire = toWireJid(id);
1702
+ const resolved = normalizeJid(store.resolveJid(id));
1703
+ const match = meta.participants.find((p) => {
1704
+ const pAny = p;
1705
+ return (toWireJid(pAny.id) === wire ||
1706
+ normalizeJid(store.resolveJid(pAny.id)) === resolved ||
1707
+ (pAny.jid ? toWireJid(pAny.jid) === wire : false) ||
1708
+ (pAny.lid ? toWireJid(pAny.lid) === wire : false));
1709
+ });
1710
+ if (!match) {
1711
+ throw new Error(t("driver.groupParticipantNotFound", { id, group: groupJid }));
1712
+ }
1713
+ out.push(match.id);
1714
+ }
1715
+ return Array.from(new Set(out));
1716
+ }
1477
1717
  function requireChat() {
1478
1718
  if (!chatJid)
1479
1719
  throw new Error("This admin operation requires a runtime group context.");
@@ -1503,7 +1743,7 @@ function buildAdminApi(contract, chatJid) {
1503
1743
  const failed = results.filter((r) => r?.status && r.status !== "200");
1504
1744
  if (failed.length > 0) {
1505
1745
  const detail = failed.map((r) => `${r.jid ?? "?"}=${r.status}`).join(", ");
1506
- throw new Error(`groupParticipantsUpdate("${action}") rejeitado para: ${detail}`);
1746
+ throw new Error(t("driver.groupParticipantsUpdateRejected", { action, detail }));
1507
1747
  }
1508
1748
  }
1509
1749
  /**
@@ -1518,16 +1758,35 @@ function buildAdminApi(contract, chatJid) {
1518
1758
  results = await contract.groupParticipantsUpdate(jid, users, action);
1519
1759
  }
1520
1760
  catch (err) {
1521
- throw new Error(`groupParticipantsUpdate("${action}") falhou para o grupo "${jid}" com participantes [${users.join(", ")}]: ${err.message}`);
1761
+ throw new Error(t("driver.groupParticipantsUpdateFailed", {
1762
+ action,
1763
+ group: jid,
1764
+ users: users.join(", "),
1765
+ message: err.message,
1766
+ }));
1522
1767
  }
1523
1768
  assertParticipantsUpdateOk(action, results);
1524
1769
  return results;
1525
1770
  }
1526
- function createTargetableAction(action, memberIds) {
1527
- const users = norm(memberIds);
1528
- const executeCurrent = async () => { requireChat(); return action(chatJid, users); };
1771
+ function createTargetableAction(action, memberIds, mode = "existingMember") {
1772
+ const raw = Array.isArray(memberIds) ? memberIds : [memberIds];
1773
+ const resolve = async (groupJid) => mode === "existingMember"
1774
+ ? resolveTargets(groupJid, raw)
1775
+ // `add` targets people who aren't members yet — there's no
1776
+ // participant-list entry to match against, so fall back to a
1777
+ // plain jid guess (the historical behavior).
1778
+ : Array.from(new Set(raw.map(toWireJid)));
1779
+ const executeCurrent = async () => {
1780
+ requireChat();
1781
+ const users = await resolve(chatJid);
1782
+ return action(chatJid, users);
1783
+ };
1529
1784
  return {
1530
- async to(targetJid) { await getGroup(targetJid); return action(targetJid, users); },
1785
+ async to(targetJid) {
1786
+ await getGroup(targetJid);
1787
+ const users = await resolve(targetJid);
1788
+ return action(targetJid, users);
1789
+ },
1531
1790
  then(onfulfilled, onrejected) {
1532
1791
  return executeCurrent().then(onfulfilled, onrejected);
1533
1792
  },
@@ -1542,22 +1801,31 @@ function buildAdminApi(contract, chatJid) {
1542
1801
  return {
1543
1802
  /** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
1544
1803
  add(memberIds) {
1545
- return createTargetableAction((jid, users) => runParticipantsUpdate(jid, users, "add"), memberIds);
1804
+ return createTargetableAction((jid, users) => runParticipantsUpdate(jid, users, "add"), memberIds, "newMember");
1546
1805
  },
1547
1806
  /** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
1548
1807
  async kick(memberIds) {
1549
1808
  requireChat();
1550
- return runParticipantsUpdate(chatJid, norm(memberIds), "remove");
1809
+ const users = await resolveTargets(chatJid, Array.isArray(memberIds) ? memberIds : [memberIds]);
1810
+ if (users.some((u) => {
1811
+ const n = normalizeJid(u);
1812
+ return (botJid && n === botJid) || (botLid && n === botLid);
1813
+ })) {
1814
+ throw new Error(t("driver.cannotKickSelf"));
1815
+ }
1816
+ return runParticipantsUpdate(chatJid, users, "remove");
1551
1817
  },
1552
1818
  /** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
1553
1819
  async promote(memberIds) {
1554
1820
  requireChat();
1555
- return runParticipantsUpdate(chatJid, norm(memberIds), "promote");
1821
+ const users = await resolveTargets(chatJid, Array.isArray(memberIds) ? memberIds : [memberIds]);
1822
+ return runParticipantsUpdate(chatJid, users, "promote");
1556
1823
  },
1557
1824
  /** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
1558
1825
  async demote(memberIds) {
1559
1826
  requireChat();
1560
- return runParticipantsUpdate(chatJid, norm(memberIds), "demote");
1827
+ const users = await resolveTargets(chatJid, Array.isArray(memberIds) ? memberIds : [memberIds]);
1828
+ return runParticipantsUpdate(chatJid, users, "demote");
1561
1829
  },
1562
1830
  /** @param {string} name */
1563
1831
  async setSubject(name) {
@@ -1765,7 +2033,7 @@ function buildPollApi(contract, store, rawJid, guardOptions, pluginName) {
1765
2033
  handle._updateFromAggregated(aggregated);
1766
2034
  }
1767
2035
  catch (err) {
1768
- logger.error(`[poll] erro ao decriptar voto: ${err}`);
2036
+ logger.error(`[poll] ${t("driver.pollDecryptFailed", { error: err })}`);
1769
2037
  }
1770
2038
  }
1771
2039
  });
@@ -1819,7 +2087,9 @@ function buildPollApi(contract, store, rawJid, guardOptions, pluginName) {
1819
2087
  }
1820
2088
  // ── Base API (shared between setup and runtime) ───────────────────────────────
1821
2089
  function buildBaseApi(contract, store, pluginRegistry, pluginName) {
1822
- const botJid = contract.me().id ? jidNormalizedUser(contract.me().id) : null;
2090
+ const me = contract.me();
2091
+ const botJid = me.id ? jidNormalizedUser(me.id) : null;
2092
+ const botLid = me.lid ? normalizeJid(me.lid) : null;
1823
2093
  if (!botJid)
1824
2094
  logger.warn("[pluginApi] botId is null — socket may not be ready yet.");
1825
2095
  return {
@@ -1832,9 +2102,10 @@ function buildBaseApi(contract, store, pluginRegistry, pluginName) {
1832
2102
  scheduler: buildSchedulerApi(pluginName),
1833
2103
  plugins: buildPluginsApi(pluginRegistry),
1834
2104
  chats: buildChatsApi(store),
1835
- contacts: buildContactsApi(contract, store, botJid),
2105
+ contacts: buildContactsApi(contract, store, botJid, botLid),
1836
2106
  storage: buildStorageApi(pluginName),
1837
- botId: botJid,
2107
+ botId: botLid ?? botJid,
2108
+ commands: buildCommandsApi(),
1838
2109
  };
1839
2110
  }
1840
2111
  // ── Setup API ─────────────────────────────────────────────────────────────────
@@ -1852,12 +2123,52 @@ export function buildSetupApi(contract, store, pluginRegistry, pluginName) {
1852
2123
  return {
1853
2124
  ...buildBaseApi(contract, store, pluginRegistry, pluginName),
1854
2125
  ...buildSetupSendApi(contract, store),
1855
- admin: buildAdminApi(contract, null),
2126
+ admin: buildAdminApi(contract, store, null),
1856
2127
  events: buildEventsApi(contract, pluginName),
1857
2128
  me: buildMeApi(contract),
1858
2129
  settings: { global: buildSettingsApi(pluginName, "_global").global },
1859
2130
  };
1860
2131
  }
2132
+ // ── ctx.runCommand facet ─────────────────────────────────────────────────────
2133
+ /**
2134
+ * Builds the `ctx.runCommand(invocation, rawArgs?)` facet for a runtime
2135
+ * ctx bound to `msg`/`chat`. Deliberately built AFTER `buildApi` is
2136
+ * declared (function declarations hoist) so it can call `buildApi`
2137
+ * again, recursively, to build a context scoped to the TARGET
2138
+ * command's owning plugin rather than reusing the caller's — same
2139
+ * "own storage/plugins facet, not the caller's" principle as
2140
+ * `ctx.plugins.require()`.
2141
+ *
2142
+ * @param {BotMessage} msg
2143
+ * @param {WAChat} chat
2144
+ * @param {WaContract} contract
2145
+ * @param {BotStore} store
2146
+ * @param {Map} pluginRegistry
2147
+ * @param {string} callerPluginName
2148
+ */
2149
+ function buildRunCommandFacet(msg, chat, contract, store, pluginRegistry, callerPluginName) {
2150
+ return async function runCommandFacet(invocation, rawArgs = "") {
2151
+ const resolution = resolveDispatch(invocation, rawArgs);
2152
+ if (resolution.target.kind === "none") {
2153
+ return { status: "no_dispatch", sentReply: null, suggestedReply: null };
2154
+ }
2155
+ const entry = resolution.target.kind === "sub" ? resolution.target.parent : resolution.target.entry;
2156
+ const targetPluginName = entry.source === "plugin" ? entry.pluginName : null;
2157
+ const targetCtx = targetPluginName && targetPluginName !== callerPluginName
2158
+ ? buildApi({
2159
+ msg, chat, contract, store, pluginRegistry,
2160
+ pluginName: targetPluginName,
2161
+ guardOptions: pluginRegistry.get(targetPluginName)?.guardOptions ?? {},
2162
+ })
2163
+ : buildApi({ msg, chat, contract, store, pluginRegistry, pluginName: callerPluginName });
2164
+ return dispatchCommand({
2165
+ pluginName: targetPluginName,
2166
+ ctx: targetCtx,
2167
+ resolution,
2168
+ reply: targetCtx.msg.reply,
2169
+ });
2170
+ };
2171
+ }
1861
2172
  // ── Runtime API ───────────────────────────────────────────────────────────────
1862
2173
  /**
1863
2174
  * Runtime API — full context with message and chat.
@@ -1873,7 +2184,7 @@ export function buildSetupApi(contract, store, pluginRegistry, pluginName) {
1873
2184
  * @param {object} [params.guardOptions]
1874
2185
  */
1875
2186
  export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginName, guardOptions = {}, }) {
1876
- const prefix = CONFIG.CMD_PREFIX;
2187
+ const prefix = getChatPrefix(msg.chatId);
1877
2188
  const body = getMsgBody(msg);
1878
2189
  const rawArgs = body.trim().split(/\s+/);
1879
2190
  const first = rawArgs[0]?.toLowerCase() ?? "";
@@ -1881,7 +2192,7 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
1881
2192
  const command = hasPrefix ? first.slice(prefix.length) : "";
1882
2193
  const rawJid = msg.chatId;
1883
2194
  const normJid = normalizeJid(rawJid);
1884
- const sender = getMsgSender(msg, store);
2195
+ const sender = getMsgSender(msg);
1885
2196
  const cooldown = (guardOptions.cooldown ?? true);
1886
2197
  const jitter = (guardOptions.jitter ?? true);
1887
2198
  bindGroupMetaInvalidation(contract);
@@ -1907,6 +2218,8 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
1907
2218
  return {
1908
2219
  ...buildBaseApi(contract, store, pluginRegistry, pluginName),
1909
2220
  ...buildSendApi(contract, store, rawJid, guardOptions),
2221
+ runCommand: buildRunCommandFacet(msg, chat, contract, store, pluginRegistry, pluginName),
2222
+ session: buildSessionApi(normJid, pluginName),
1910
2223
  // ── msg ──────────────────────────────────────────────────────────────────
1911
2224
  msg: buildMessageContext(msg, contract, store, { cooldown, jitter }),
1912
2225
  // ── chat ─────────────────────────────────────────────────────────────────
@@ -1949,6 +2262,9 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
1949
2262
  },
1950
2263
  /**
1951
2264
  * Check if a contact is an admin of this group.
2265
+ * Always fetches fresh (bypasses the group-metadata TTL cache) —
2266
+ * this gates the `admin:` command permission, so it cannot risk
2267
+ * serving a stale "still admin" result after a demote.
1952
2268
  * @param {string} contactId
1953
2269
  * @returns {Promise<boolean>}
1954
2270
  */
@@ -1956,7 +2272,7 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
1956
2272
  if (!chat.isGroup)
1957
2273
  return false;
1958
2274
  try {
1959
- const meta = await getGroupMetadataCached(contract, rawJid);
2275
+ const meta = await getGroupMetadataFresh(contract, rawJid);
1960
2276
  return meta.participants.some(p => matchesParticipant([contactId], p.id) && (p.admin === "admin" || p.admin === "superadmin"));
1961
2277
  }
1962
2278
  catch {
@@ -1965,13 +2281,16 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
1965
2281
  },
1966
2282
  /**
1967
2283
  * Check if the message sender is an admin of this group.
2284
+ * Always fetches fresh (bypasses the group-metadata TTL cache) — see
2285
+ * {@link isAdmin} for why: this gates the `botAdmin:`/`admin:`
2286
+ * permission checks in commandPermissions.ts.
1968
2287
  * @returns {Promise<boolean>}
1969
2288
  */
1970
2289
  async isSenderAdmin() {
1971
2290
  if (!chat.isGroup)
1972
2291
  return false;
1973
2292
  try {
1974
- const meta = await getGroupMetadataCached(contract, rawJid);
2293
+ const meta = await getGroupMetadataFresh(contract, rawJid);
1975
2294
  // The adapter's pre-extracted participant fields give us both the
1976
2295
  // LID and PN forms of the sender; match either against the group's
1977
2296
  // own participant list.
@@ -1984,6 +2303,9 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
1984
2303
  },
1985
2304
  /**
1986
2305
  * Check if the bot is an admin of this group.
2306
+ * Always fetches fresh (bypasses the group-metadata TTL cache) — see
2307
+ * {@link isAdmin} for why: this gates the `botAdmin:` permission
2308
+ * check in commandPermissions.ts.
1987
2309
  * @returns {Promise<boolean>}
1988
2310
  */
1989
2311
  async isBotAdmin() {
@@ -1995,7 +2317,7 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
1995
2317
  if (!botCandidates.some(Boolean))
1996
2318
  return false;
1997
2319
  try {
1998
- const meta = await getGroupMetadataCached(contract, rawJid);
2320
+ const meta = await getGroupMetadataFresh(contract, rawJid);
1999
2321
  return meta.participants.some(p => matchesParticipant(botCandidates, p.id) && (p.admin === "admin" || p.admin === "superadmin"));
2000
2322
  }
2001
2323
  catch {
@@ -2008,7 +2330,7 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
2008
2330
  },
2009
2331
  },
2010
2332
  // ── admin ─────────────────────────────────────────────────────────────────
2011
- admin: buildAdminApi(contract, rawJid),
2333
+ admin: buildAdminApi(contract, store, rawJid),
2012
2334
  // ── me ────────────────────────────────────────────────────────────────────
2013
2335
  me: buildMeApi(contract),
2014
2336
  // ── poll ──────────────────────────────────────────────────────────────────