@manybot/manybot 5.8.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.
- package/README.md +15 -7
- package/dist/client/store.js +35 -1
- package/dist/download/queue.js +13 -4
- package/dist/drivers/baileys/adapter.js +75 -8
- package/dist/drivers/baileys/api/contacts.integration.test.js +261 -0
- package/dist/drivers/baileys/api/groupMeta.test.js +235 -0
- package/dist/drivers/baileys/api/index.js +212 -38
- package/dist/drivers/baileys/index.js +30 -6
- package/dist/drivers/baileys/messageHandler.js +207 -21
- package/dist/drivers/baileys/messageHandler.test.js +256 -14
- package/dist/drivers/baileysAdapter.test.js +97 -0
- package/dist/drivers/jid.js +26 -0
- package/dist/drivers/jid.test.js +35 -1
- package/dist/i18n/index.js +5 -22
- package/dist/kernel/chatOverrides.js +46 -0
- package/dist/kernel/chatOverrides.test.js +59 -0
- package/dist/kernel/commandAccess.test.js +2 -2
- package/dist/kernel/commandDeprecation.js +4 -2
- package/dist/kernel/commandDeprecation.test.js +8 -1
- package/dist/kernel/commandMenu.js +91 -2
- package/dist/kernel/commandMenu.test.js +131 -2
- package/dist/kernel/commandPermissions.js +69 -23
- package/dist/kernel/commandPermissions.test.js +77 -9
- package/dist/kernel/commandRegistry.js +167 -43
- package/dist/kernel/commandRegistry.test.js +4 -2
- package/dist/kernel/commandsConfig.js +470 -38
- package/dist/kernel/commandsConfig.test.js +249 -3
- package/dist/kernel/contactAutoSave.js +6 -6
- package/dist/kernel/coreCommands.js +62 -0
- package/dist/kernel/pluginApi.test.js +20 -3
- package/dist/kernel/pluginGuard.js +5 -3
- package/dist/kernel/pluginLoader.js +73 -10
- package/dist/kernel/pluginLoader.test.js +111 -1
- package/dist/kernel/runCommand.js +57 -18
- package/dist/kernel/runCommand.test.js +269 -7
- package/dist/kernel/settingsDb.js +15 -2
- package/dist/kernel/testConfig.js +9 -0
- package/dist/locales/en.json +14 -1
- package/dist/locales/es.json +17 -4
- package/dist/locales/pt.json +18 -5
- package/dist/plugins/__manybot_integration__/index.js +33 -16
- package/dist/plugins/__manybot_integration__/index.test.js +42 -8
- package/dist/utils/phoneNumber.js +83 -0
- package/dist/utils/phoneNumber.test.js +53 -0
- package/package.json +4 -3
|
@@ -13,10 +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";
|
|
17
18
|
import { waitForEditSlot } from "#kernel/sendGuard.js";
|
|
18
19
|
import { schedule, cancelPlugin } from "#kernel/scheduler.js";
|
|
19
20
|
import { emptyFolder } from "#utils/file.js";
|
|
21
|
+
import { parsePhone } from "#utils/phoneNumber.js";
|
|
20
22
|
import { normalizeJid, denormalizeJid, toWireJid } from "#drivers/jid.js";
|
|
21
23
|
import { mkdirSync } from "fs";
|
|
22
24
|
import { readFile, writeFile, unlink, mkdtemp, rm } from "fs/promises";
|
|
@@ -146,14 +148,35 @@ function msgIsGif(msg, store) {
|
|
|
146
148
|
return !!raw.message?.videoMessage?.gifPlayback;
|
|
147
149
|
}
|
|
148
150
|
/** Sender JID — group participant or DM remote JID, normalized. */
|
|
149
|
-
function getMsgSender(msg
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
// `
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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;
|
|
157
180
|
}
|
|
158
181
|
/** Quoted-message metadata as the rest of the api uses it. */
|
|
159
182
|
function getQuotedContext(msg) {
|
|
@@ -222,6 +245,25 @@ async function getGroupMetadataCached(contract, jid) {
|
|
|
222
245
|
groupMetaCache.set(jid, { meta, at: Date.now() });
|
|
223
246
|
return meta;
|
|
224
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
|
+
}
|
|
225
267
|
let groupMetaInvalidationBound = false;
|
|
226
268
|
function bindGroupMetaInvalidation(contract) {
|
|
227
269
|
if (groupMetaInvalidationBound)
|
|
@@ -236,6 +278,14 @@ function bindGroupMetaInvalidation(contract) {
|
|
|
236
278
|
groupMetaCache.delete(u.id);
|
|
237
279
|
});
|
|
238
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
|
+
}
|
|
239
289
|
/**
|
|
240
290
|
* Build a WAChat adapter from a BotMessage + store.
|
|
241
291
|
* Exposed for use in messageHandler.ts.
|
|
@@ -355,7 +405,9 @@ function buildConfigApi() {
|
|
|
355
405
|
* @param {any} [defaultValue]
|
|
356
406
|
*/
|
|
357
407
|
get(key, defaultValue = null) {
|
|
358
|
-
|
|
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);
|
|
359
411
|
},
|
|
360
412
|
};
|
|
361
413
|
}
|
|
@@ -463,13 +515,72 @@ function mentionDisplayName(jid) {
|
|
|
463
515
|
* Build a normalized contact object from a JID and optional store metadata.
|
|
464
516
|
* isBusiness is resolved via contract.getBusinessProfile(jid) — it resolves
|
|
465
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
|
+
*
|
|
466
535
|
* @param {string} jid
|
|
467
536
|
* @param {RawStoreContact} [info]
|
|
468
537
|
* @param {string|null} [botJid]
|
|
469
538
|
* @param {WaContract} [contract]
|
|
539
|
+
* @param {string|null} [botLid]
|
|
470
540
|
*/
|
|
471
|
-
async function normalizeContact(jid, info, botJid, contract) {
|
|
472
|
-
|
|
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);
|
|
473
584
|
let isBusiness = false;
|
|
474
585
|
// We already have a contact record for this jid (learned from a real
|
|
475
586
|
// contacts.upsert or an actual message from them) — that alone proves
|
|
@@ -478,7 +589,7 @@ async function normalizeContact(jid, info, botJid, contract) {
|
|
|
478
589
|
// raw @lid we've never resolved to a phone number (returns a false
|
|
479
590
|
// "doesn't exist" instead of throwing).
|
|
480
591
|
let isWAAccount = Boolean(info);
|
|
481
|
-
if (!isWAAccount && contract && !
|
|
592
|
+
if (!isWAAccount && contract && !isGroup) {
|
|
482
593
|
try {
|
|
483
594
|
const results = await contract.onWhatsApp(jid);
|
|
484
595
|
isWAAccount = Boolean(results?.[0]?.exists);
|
|
@@ -492,9 +603,9 @@ async function normalizeContact(jid, info, botJid, contract) {
|
|
|
492
603
|
// which we can't verify this way) — nothing backs this contact.
|
|
493
604
|
// Matches the old whatsapp-web.js contract: getContactById() threw /
|
|
494
605
|
// resolved to null for an unknown ID instead of returning a hollow object.
|
|
495
|
-
if (!
|
|
606
|
+
if (!isGroup && !isWAAccount)
|
|
496
607
|
return null;
|
|
497
|
-
if (contract && !
|
|
608
|
+
if (contract && !isGroup) {
|
|
498
609
|
try {
|
|
499
610
|
isBusiness = Boolean(await contract.getBusinessProfile(jid));
|
|
500
611
|
}
|
|
@@ -503,8 +614,12 @@ async function normalizeContact(jid, info, botJid, contract) {
|
|
|
503
614
|
}
|
|
504
615
|
}
|
|
505
616
|
return {
|
|
506
|
-
id:
|
|
507
|
-
number,
|
|
617
|
+
id: resolvedId,
|
|
618
|
+
number: phone.number,
|
|
619
|
+
numberRaw: phone.numberRaw,
|
|
620
|
+
numberPretty: phone.numberPretty,
|
|
621
|
+
country: phone.country,
|
|
622
|
+
countryCallingCode: phone.countryCallingCode,
|
|
508
623
|
pushname: info?.notify ?? null,
|
|
509
624
|
name: info?.name ?? info?.verifiedName ?? null,
|
|
510
625
|
// Baileys' Contact type has no "short name" equivalent (that's a
|
|
@@ -513,11 +628,25 @@ async function normalizeContact(jid, info, botJid, contract) {
|
|
|
513
628
|
isBusiness,
|
|
514
629
|
isEnterprise: false,
|
|
515
630
|
isBlocked: false,
|
|
516
|
-
isMe: botJid
|
|
631
|
+
isMe: (botJid && jid === normalizeJid(botJid)) || (botLid && jid === normalizeJid(botLid)) || false,
|
|
517
632
|
isWAAccount,
|
|
518
|
-
isUser: !
|
|
519
|
-
isGroup
|
|
520
|
-
|
|
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
|
+
},
|
|
521
650
|
};
|
|
522
651
|
}
|
|
523
652
|
// ── Chats API ─────────────────────────────────────────────────────────────────
|
|
@@ -558,7 +687,15 @@ function buildChatsApi(store) {
|
|
|
558
687
|
};
|
|
559
688
|
}
|
|
560
689
|
// ── Contact API ───────────────────────────────────────────────────────────────
|
|
561
|
-
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);
|
|
562
699
|
return {
|
|
563
700
|
/**
|
|
564
701
|
* Get a normalized contact object by JID.
|
|
@@ -627,7 +764,8 @@ function buildContactsApi(contract, store, botJid) {
|
|
|
627
764
|
if (botJid &&
|
|
628
765
|
normalizedContactId.endsWith("@lid") &&
|
|
629
766
|
resolved === normalizeJid(botJid) &&
|
|
630
|
-
normalizedContactId !== normalizeJid(botJid)
|
|
767
|
+
normalizedContactId !== normalizeJid(botJid) &&
|
|
768
|
+
!(botLid && normalizedContactId === normalizeJid(botLid))) {
|
|
631
769
|
logger.warn(`[contacts.get] "${contactId}" resolved to the bot's own JID — discarding stale lidMap entry, treating as unresolved.`);
|
|
632
770
|
store.forgetLid(normalizedContactId);
|
|
633
771
|
resolved = normalizedContactId;
|
|
@@ -650,7 +788,7 @@ function buildContactsApi(contract, store, botJid) {
|
|
|
650
788
|
notify: resolvedInfo?.notify ?? raw?.notify,
|
|
651
789
|
verifiedName: resolvedInfo?.verifiedName ?? raw?.verifiedName,
|
|
652
790
|
} : undefined;
|
|
653
|
-
return normalizeContact(resolved, info, botJid, contract);
|
|
791
|
+
return normalizeContact(resolved, info, botJid, contract, store, botLid);
|
|
654
792
|
},
|
|
655
793
|
/**
|
|
656
794
|
* Get the profile picture URL of a contact.
|
|
@@ -738,20 +876,33 @@ function makeHistoryArray(entries, store) {
|
|
|
738
876
|
const arr = entries;
|
|
739
877
|
arr.last = (n) => makeHistoryArray(typeof n === "number" ? entries.slice(-n) : entries.slice(), store);
|
|
740
878
|
arr.from = (senderId) => {
|
|
741
|
-
|
|
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);
|
|
742
888
|
return makeHistoryArray(entries.filter((e) => e.sender === target), store);
|
|
743
889
|
};
|
|
744
890
|
return arr;
|
|
745
891
|
}
|
|
746
892
|
export function buildMessageContext(msg, contract, store, guardOptions = {}) {
|
|
747
893
|
const body = getMsgBody(msg);
|
|
748
|
-
const prefix =
|
|
894
|
+
const prefix = getChatPrefix(msg.chatId);
|
|
749
895
|
const rawArgs = body.trim().split(/\s+/);
|
|
750
896
|
const first = rawArgs[0]?.toLowerCase() ?? "";
|
|
751
897
|
const hasPrefix = first.startsWith(prefix);
|
|
752
898
|
const command = hasPrefix ? first.slice(prefix.length) : "";
|
|
753
899
|
const rawJid = msg.chatId;
|
|
754
|
-
const sender = getMsgSender(msg
|
|
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;
|
|
755
906
|
const cooldown = guardOptions.cooldown ?? true;
|
|
756
907
|
const jitter = guardOptions.jitter ?? true;
|
|
757
908
|
const contextInfo = getContextInfo(msg);
|
|
@@ -829,7 +980,8 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
|
|
|
829
980
|
type: getMsgType(msg),
|
|
830
981
|
fromMe: msg.fromMe,
|
|
831
982
|
sender,
|
|
832
|
-
|
|
983
|
+
senderPn,
|
|
984
|
+
senderName: msg.pushName ?? contactId.replace(/(:\d+)?@.*$/, ""),
|
|
833
985
|
command,
|
|
834
986
|
args: rawArgs.slice(1),
|
|
835
987
|
is(cmd) {
|
|
@@ -837,6 +989,7 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
|
|
|
837
989
|
},
|
|
838
990
|
hasMedia: msgHasMedia(msg),
|
|
839
991
|
isGif: msgIsGif(msg, store),
|
|
992
|
+
mentionedJid: msg.mentionedJid ?? [],
|
|
840
993
|
async downloadMedia(opts = {}) {
|
|
841
994
|
try {
|
|
842
995
|
// contract.downloadMedia handles reupload internally via the
|
|
@@ -909,12 +1062,13 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
|
|
|
909
1062
|
* @returns {Promise<object|null>} null if the sender can't be confirmed as a real WhatsApp account.
|
|
910
1063
|
*/
|
|
911
1064
|
async getContact() {
|
|
912
|
-
const info = store.contacts[
|
|
913
|
-
?? store.contacts[denormalizeJid(
|
|
1065
|
+
const info = store.contacts[contactId]
|
|
1066
|
+
?? store.contacts[denormalizeJid(contactId)]
|
|
914
1067
|
?? store.contacts[store.resolveJid(msg.fromPn ?? "")]
|
|
915
1068
|
?? store.contacts[denormalizeJid(store.resolveJid(msg.fromPn ?? ""))];
|
|
916
1069
|
const botJid = contract.me().id ? jidNormalizedUser(contract.me().id) : null;
|
|
917
|
-
|
|
1070
|
+
const botLid = contract.me().lid ? normalizeJid(contract.me().lid) : null;
|
|
1071
|
+
return normalizeContact(contactId, info, botJid, contract, store, botLid);
|
|
918
1072
|
},
|
|
919
1073
|
};
|
|
920
1074
|
}
|
|
@@ -1522,6 +1676,9 @@ async function resolveMentionJids(contract, store, jid, mentions) {
|
|
|
1522
1676
|
return Array.from(new Set(result.filter(Boolean)));
|
|
1523
1677
|
}
|
|
1524
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;
|
|
1525
1682
|
/**
|
|
1526
1683
|
* Resolve admin-supplied identifiers (bare phone number, @c.us,
|
|
1527
1684
|
* @s.whatsapp.net, or @lid) to the exact jid WhatsApp has on file for
|
|
@@ -1650,6 +1807,12 @@ function buildAdminApi(contract, store, chatJid) {
|
|
|
1650
1807
|
async kick(memberIds) {
|
|
1651
1808
|
requireChat();
|
|
1652
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
|
+
}
|
|
1653
1816
|
return runParticipantsUpdate(chatJid, users, "remove");
|
|
1654
1817
|
},
|
|
1655
1818
|
/** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
|
|
@@ -1924,7 +2087,9 @@ function buildPollApi(contract, store, rawJid, guardOptions, pluginName) {
|
|
|
1924
2087
|
}
|
|
1925
2088
|
// ── Base API (shared between setup and runtime) ───────────────────────────────
|
|
1926
2089
|
function buildBaseApi(contract, store, pluginRegistry, pluginName) {
|
|
1927
|
-
const
|
|
2090
|
+
const me = contract.me();
|
|
2091
|
+
const botJid = me.id ? jidNormalizedUser(me.id) : null;
|
|
2092
|
+
const botLid = me.lid ? normalizeJid(me.lid) : null;
|
|
1928
2093
|
if (!botJid)
|
|
1929
2094
|
logger.warn("[pluginApi] botId is null — socket may not be ready yet.");
|
|
1930
2095
|
return {
|
|
@@ -1937,9 +2102,9 @@ function buildBaseApi(contract, store, pluginRegistry, pluginName) {
|
|
|
1937
2102
|
scheduler: buildSchedulerApi(pluginName),
|
|
1938
2103
|
plugins: buildPluginsApi(pluginRegistry),
|
|
1939
2104
|
chats: buildChatsApi(store),
|
|
1940
|
-
contacts: buildContactsApi(contract, store, botJid),
|
|
2105
|
+
contacts: buildContactsApi(contract, store, botJid, botLid),
|
|
1941
2106
|
storage: buildStorageApi(pluginName),
|
|
1942
|
-
botId: botJid,
|
|
2107
|
+
botId: botLid ?? botJid,
|
|
1943
2108
|
commands: buildCommandsApi(),
|
|
1944
2109
|
};
|
|
1945
2110
|
}
|
|
@@ -2019,7 +2184,7 @@ function buildRunCommandFacet(msg, chat, contract, store, pluginRegistry, caller
|
|
|
2019
2184
|
* @param {object} [params.guardOptions]
|
|
2020
2185
|
*/
|
|
2021
2186
|
export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginName, guardOptions = {}, }) {
|
|
2022
|
-
const prefix =
|
|
2187
|
+
const prefix = getChatPrefix(msg.chatId);
|
|
2023
2188
|
const body = getMsgBody(msg);
|
|
2024
2189
|
const rawArgs = body.trim().split(/\s+/);
|
|
2025
2190
|
const first = rawArgs[0]?.toLowerCase() ?? "";
|
|
@@ -2027,7 +2192,7 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
|
|
|
2027
2192
|
const command = hasPrefix ? first.slice(prefix.length) : "";
|
|
2028
2193
|
const rawJid = msg.chatId;
|
|
2029
2194
|
const normJid = normalizeJid(rawJid);
|
|
2030
|
-
const sender = getMsgSender(msg
|
|
2195
|
+
const sender = getMsgSender(msg);
|
|
2031
2196
|
const cooldown = (guardOptions.cooldown ?? true);
|
|
2032
2197
|
const jitter = (guardOptions.jitter ?? true);
|
|
2033
2198
|
bindGroupMetaInvalidation(contract);
|
|
@@ -2097,6 +2262,9 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
|
|
|
2097
2262
|
},
|
|
2098
2263
|
/**
|
|
2099
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.
|
|
2100
2268
|
* @param {string} contactId
|
|
2101
2269
|
* @returns {Promise<boolean>}
|
|
2102
2270
|
*/
|
|
@@ -2104,7 +2272,7 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
|
|
|
2104
2272
|
if (!chat.isGroup)
|
|
2105
2273
|
return false;
|
|
2106
2274
|
try {
|
|
2107
|
-
const meta = await
|
|
2275
|
+
const meta = await getGroupMetadataFresh(contract, rawJid);
|
|
2108
2276
|
return meta.participants.some(p => matchesParticipant([contactId], p.id) && (p.admin === "admin" || p.admin === "superadmin"));
|
|
2109
2277
|
}
|
|
2110
2278
|
catch {
|
|
@@ -2113,13 +2281,16 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
|
|
|
2113
2281
|
},
|
|
2114
2282
|
/**
|
|
2115
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.
|
|
2116
2287
|
* @returns {Promise<boolean>}
|
|
2117
2288
|
*/
|
|
2118
2289
|
async isSenderAdmin() {
|
|
2119
2290
|
if (!chat.isGroup)
|
|
2120
2291
|
return false;
|
|
2121
2292
|
try {
|
|
2122
|
-
const meta = await
|
|
2293
|
+
const meta = await getGroupMetadataFresh(contract, rawJid);
|
|
2123
2294
|
// The adapter's pre-extracted participant fields give us both the
|
|
2124
2295
|
// LID and PN forms of the sender; match either against the group's
|
|
2125
2296
|
// own participant list.
|
|
@@ -2132,6 +2303,9 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
|
|
|
2132
2303
|
},
|
|
2133
2304
|
/**
|
|
2134
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.
|
|
2135
2309
|
* @returns {Promise<boolean>}
|
|
2136
2310
|
*/
|
|
2137
2311
|
async isBotAdmin() {
|
|
@@ -2143,7 +2317,7 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
|
|
|
2143
2317
|
if (!botCandidates.some(Boolean))
|
|
2144
2318
|
return false;
|
|
2145
2319
|
try {
|
|
2146
|
-
const meta = await
|
|
2320
|
+
const meta = await getGroupMetadataFresh(contract, rawJid);
|
|
2147
2321
|
return meta.participants.some(p => matchesParticipant(botCandidates, p.id) && (p.admin === "admin" || p.admin === "superadmin"));
|
|
2148
2322
|
}
|
|
2149
2323
|
catch {
|
|
@@ -22,8 +22,9 @@
|
|
|
22
22
|
import { createSocket, AUTH_DIR, store as sharedStore } from "./sdk/baileysSock.js";
|
|
23
23
|
import { createBaileysAdapter } from "./adapter.js";
|
|
24
24
|
import { handleMessage } from "./messageHandler.js";
|
|
25
|
-
import { normalizeJid } from "#drivers/jid.js";
|
|
26
|
-
import { loadPlugins, setupPlugins } from "#kernel/pluginLoader.js";
|
|
25
|
+
import { normalizeJid, splitLidPn } from "#drivers/jid.js";
|
|
26
|
+
import { loadPlugins, setupPlugins, loadIntegrationPlugin } from "#kernel/pluginLoader.js";
|
|
27
|
+
import { isIntegrationOptIn } from "#kernel/integrationMode.js";
|
|
27
28
|
import { runContactRefreshSweep } from "#kernel/contactAutoSave.js";
|
|
28
29
|
import { registerAlertSockProvider, sendAlert } from "#kernel/alerts.js";
|
|
29
30
|
import { startUpdateCheckSchedule, stopUpdateCheckSchedule } from "#kernel/updateCheck.js";
|
|
@@ -211,6 +212,24 @@ async function startBot() {
|
|
|
211
212
|
pluginsReady = true;
|
|
212
213
|
await loadPlugins(PLUGINS);
|
|
213
214
|
await setupPlugins(contract, store);
|
|
215
|
+
// Opt-in: when the operator is running the integration test
|
|
216
|
+
// suite (`MANYBOT_RUN_WHATSAPP_TESTS=1`), also load the
|
|
217
|
+
// reserved integration plugin so its public API
|
|
218
|
+
// (`waitForMarker`, `testChat`, ...) is available for the
|
|
219
|
+
// test harness. Done after setupPlugins() so the integration
|
|
220
|
+
// plugin's own setup() can subscribe to events already wired
|
|
221
|
+
// up by the regular plugins. Production runs (no opt-in flag)
|
|
222
|
+
// are completely unaffected — `loadIntegrationPlugin()` itself
|
|
223
|
+
// throws on missing opt-in, so we gate it here too for an
|
|
224
|
+
// early, clean skip.
|
|
225
|
+
if (isIntegrationOptIn()) {
|
|
226
|
+
try {
|
|
227
|
+
await loadIntegrationPlugin();
|
|
228
|
+
}
|
|
229
|
+
catch (e) {
|
|
230
|
+
logger.warn(`[baileys] integration plugin failed to load: ${e.message}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
214
233
|
}
|
|
215
234
|
startCacheAutosave(store);
|
|
216
235
|
startContactRefreshSweep(contract);
|
|
@@ -595,6 +614,11 @@ export function toBotMessage(msg) {
|
|
|
595
614
|
m?.interactiveMessage?.contextInfo ??
|
|
596
615
|
m?.buttonsMessage?.contextInfo ??
|
|
597
616
|
undefined;
|
|
617
|
+
// See splitLidPn() in #drivers/jid.js — `key.participant` is only the PN
|
|
618
|
+
// form under legacy `addressingMode: "pn"`; under the modern default
|
|
619
|
+
// "lid" mode it's already the LID and `key.participantAlt` carries the
|
|
620
|
+
// PN instead. Resolve by JID suffix, not by field position.
|
|
621
|
+
const participantIds = splitLidPn(key.participant, key.participantAlt);
|
|
598
622
|
return {
|
|
599
623
|
id: msg.key?.id ?? "",
|
|
600
624
|
chatId: normalizeJid(msg.key?.remoteJid ?? ""),
|
|
@@ -612,10 +636,10 @@ export function toBotMessage(msg) {
|
|
|
612
636
|
fromMe: false,
|
|
613
637
|
participant: contextInfo.participant ?? undefined,
|
|
614
638
|
} : undefined,
|
|
615
|
-
fromLid:
|
|
616
|
-
fromPn:
|
|
617
|
-
participantAlt:
|
|
618
|
-
remoteJidAlt: key.remoteJidAlt,
|
|
639
|
+
fromLid: participantIds.lid,
|
|
640
|
+
fromPn: participantIds.pn,
|
|
641
|
+
participantAlt: participantIds.lid,
|
|
642
|
+
remoteJidAlt: splitLidPn(key.remoteJid, key.remoteJidAlt).lid,
|
|
619
643
|
_raw: {
|
|
620
644
|
pollEncKeyRaw: m?.messageContextInfo?.messageSecret ?? undefined,
|
|
621
645
|
},
|