@manybot/manybot 5.8.0 → 5.9.1
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 +245 -51
- 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,16 +1676,30 @@ 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
|
-
* Resolve admin-supplied identifiers (bare phone number,
|
|
1527
|
-
* @s.whatsapp.net, or @lid) to the exact jid WhatsApp has on
|
|
1528
|
-
* that participant *in this group*. A naive toWireJid() guess
|
|
1529
|
-
* correct for pn-addressed groups — lid-addressed groups
|
|
1530
|
-
* common, e.g. when a member hides their phone number)
|
|
1531
|
-
* that member by @lid, which a phone number typed by
|
|
1532
|
-
* produce on its own. Baileys' raw groupMetadata()
|
|
1533
|
-
* `id` (whatever WA addresses them as here), `jid`
|
|
1534
|
-
* phone-number guess) and `lid` — check all three.
|
|
1683
|
+
* Resolve admin-supplied identifiers (bare phone number, bare LID,
|
|
1684
|
+
* @c.us, @s.whatsapp.net, or @lid) to the exact jid WhatsApp has on
|
|
1685
|
+
* file for that participant *in this group*. A naive toWireJid() guess
|
|
1686
|
+
* is only correct for pn-addressed groups — lid-addressed groups
|
|
1687
|
+
* (increasingly common, e.g. when a member hides their phone number)
|
|
1688
|
+
* only recognize that member by @lid, which a phone number typed by
|
|
1689
|
+
* the admin can't produce on its own. Baileys' raw groupMetadata()
|
|
1690
|
+
* participants carry `id` (whatever WA addresses them as here), `jid`
|
|
1691
|
+
* (its best phone-number guess) and `lid` — check all three.
|
|
1692
|
+
*
|
|
1693
|
+
* When the identifier has no explicit domain (the admin typed a bare
|
|
1694
|
+
* number), we don't know if it's a PN or a LID — toWireJid() would
|
|
1695
|
+
* silently guess PN, which never matches a participant that's only
|
|
1696
|
+
* addressable by @lid even when the digits are identical (e.g. typed
|
|
1697
|
+
* "118077710708981", the actual member is "118077710708981@lid"). In
|
|
1698
|
+
* that case, compare by digits alone against id/jid/lid instead of
|
|
1699
|
+
* forcing a domain. When the identifier *does* carry an explicit
|
|
1700
|
+
* domain (@c.us, @s.whatsapp.net, @lid), the domain is meaningful
|
|
1701
|
+
* (disambiguates a PN from a same-digit LID) and the original
|
|
1702
|
+
* wire/resolved comparison is used as before.
|
|
1535
1703
|
*
|
|
1536
1704
|
* Throws per-identifier if it doesn't match a current member, instead
|
|
1537
1705
|
* of silently sending a guessed jid that WhatsApp rejects deep inside
|
|
@@ -1541,14 +1709,23 @@ function buildAdminApi(contract, store, chatJid) {
|
|
|
1541
1709
|
const meta = await getGroupMetadataCached(contract, groupJid);
|
|
1542
1710
|
const out = [];
|
|
1543
1711
|
for (const id of identifiers) {
|
|
1712
|
+
const hasExplicitDomain = /@(s\.whatsapp\.net|lid|g\.us|c\.us)$/.test(id.trim());
|
|
1544
1713
|
const wire = toWireJid(id);
|
|
1545
1714
|
const resolved = normalizeJid(store.resolveJid(id));
|
|
1715
|
+
const idDigits = id.replace(/\D/g, "");
|
|
1546
1716
|
const match = meta.participants.find((p) => {
|
|
1547
1717
|
const pAny = p;
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1718
|
+
if (hasExplicitDomain) {
|
|
1719
|
+
return (toWireJid(pAny.id) === wire ||
|
|
1720
|
+
normalizeJid(store.resolveJid(pAny.id)) === resolved ||
|
|
1721
|
+
(pAny.jid ? toWireJid(pAny.jid) === wire : false) ||
|
|
1722
|
+
(pAny.lid ? toWireJid(pAny.lid) === wire : false));
|
|
1723
|
+
}
|
|
1724
|
+
// Bare number, no domain hint: match by digits against id/jid/lid,
|
|
1725
|
+
// instead of assuming a PN domain that may not be the one WhatsApp
|
|
1726
|
+
// actually uses for this participant (see PN-vs-LID note above).
|
|
1727
|
+
const cands = [pAny.id, pAny.jid, pAny.lid].filter((v) => !!v);
|
|
1728
|
+
return idDigits.length > 0 && cands.some((c) => c.replace(/\D/g, "") === idDigits);
|
|
1552
1729
|
});
|
|
1553
1730
|
if (!match) {
|
|
1554
1731
|
throw new Error(t("driver.groupParticipantNotFound", { id, group: groupJid }));
|
|
@@ -1650,6 +1827,12 @@ function buildAdminApi(contract, store, chatJid) {
|
|
|
1650
1827
|
async kick(memberIds) {
|
|
1651
1828
|
requireChat();
|
|
1652
1829
|
const users = await resolveTargets(chatJid, Array.isArray(memberIds) ? memberIds : [memberIds]);
|
|
1830
|
+
if (users.some((u) => {
|
|
1831
|
+
const n = normalizeJid(u);
|
|
1832
|
+
return (botJid && n === botJid) || (botLid && n === botLid);
|
|
1833
|
+
})) {
|
|
1834
|
+
throw new Error(t("driver.cannotKickSelf"));
|
|
1835
|
+
}
|
|
1653
1836
|
return runParticipantsUpdate(chatJid, users, "remove");
|
|
1654
1837
|
},
|
|
1655
1838
|
/** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
|
|
@@ -1924,7 +2107,9 @@ function buildPollApi(contract, store, rawJid, guardOptions, pluginName) {
|
|
|
1924
2107
|
}
|
|
1925
2108
|
// ── Base API (shared between setup and runtime) ───────────────────────────────
|
|
1926
2109
|
function buildBaseApi(contract, store, pluginRegistry, pluginName) {
|
|
1927
|
-
const
|
|
2110
|
+
const me = contract.me();
|
|
2111
|
+
const botJid = me.id ? jidNormalizedUser(me.id) : null;
|
|
2112
|
+
const botLid = me.lid ? normalizeJid(me.lid) : null;
|
|
1928
2113
|
if (!botJid)
|
|
1929
2114
|
logger.warn("[pluginApi] botId is null — socket may not be ready yet.");
|
|
1930
2115
|
return {
|
|
@@ -1937,9 +2122,9 @@ function buildBaseApi(contract, store, pluginRegistry, pluginName) {
|
|
|
1937
2122
|
scheduler: buildSchedulerApi(pluginName),
|
|
1938
2123
|
plugins: buildPluginsApi(pluginRegistry),
|
|
1939
2124
|
chats: buildChatsApi(store),
|
|
1940
|
-
contacts: buildContactsApi(contract, store, botJid),
|
|
2125
|
+
contacts: buildContactsApi(contract, store, botJid, botLid),
|
|
1941
2126
|
storage: buildStorageApi(pluginName),
|
|
1942
|
-
botId: botJid,
|
|
2127
|
+
botId: botLid ?? botJid,
|
|
1943
2128
|
commands: buildCommandsApi(),
|
|
1944
2129
|
};
|
|
1945
2130
|
}
|
|
@@ -2019,7 +2204,7 @@ function buildRunCommandFacet(msg, chat, contract, store, pluginRegistry, caller
|
|
|
2019
2204
|
* @param {object} [params.guardOptions]
|
|
2020
2205
|
*/
|
|
2021
2206
|
export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginName, guardOptions = {}, }) {
|
|
2022
|
-
const prefix =
|
|
2207
|
+
const prefix = getChatPrefix(msg.chatId);
|
|
2023
2208
|
const body = getMsgBody(msg);
|
|
2024
2209
|
const rawArgs = body.trim().split(/\s+/);
|
|
2025
2210
|
const first = rawArgs[0]?.toLowerCase() ?? "";
|
|
@@ -2027,7 +2212,7 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
|
|
|
2027
2212
|
const command = hasPrefix ? first.slice(prefix.length) : "";
|
|
2028
2213
|
const rawJid = msg.chatId;
|
|
2029
2214
|
const normJid = normalizeJid(rawJid);
|
|
2030
|
-
const sender = getMsgSender(msg
|
|
2215
|
+
const sender = getMsgSender(msg);
|
|
2031
2216
|
const cooldown = (guardOptions.cooldown ?? true);
|
|
2032
2217
|
const jitter = (guardOptions.jitter ?? true);
|
|
2033
2218
|
bindGroupMetaInvalidation(contract);
|
|
@@ -2097,6 +2282,9 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
|
|
|
2097
2282
|
},
|
|
2098
2283
|
/**
|
|
2099
2284
|
* Check if a contact is an admin of this group.
|
|
2285
|
+
* Always fetches fresh (bypasses the group-metadata TTL cache) —
|
|
2286
|
+
* this gates the `admin:` command permission, so it cannot risk
|
|
2287
|
+
* serving a stale "still admin" result after a demote.
|
|
2100
2288
|
* @param {string} contactId
|
|
2101
2289
|
* @returns {Promise<boolean>}
|
|
2102
2290
|
*/
|
|
@@ -2104,7 +2292,7 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
|
|
|
2104
2292
|
if (!chat.isGroup)
|
|
2105
2293
|
return false;
|
|
2106
2294
|
try {
|
|
2107
|
-
const meta = await
|
|
2295
|
+
const meta = await getGroupMetadataFresh(contract, rawJid);
|
|
2108
2296
|
return meta.participants.some(p => matchesParticipant([contactId], p.id) && (p.admin === "admin" || p.admin === "superadmin"));
|
|
2109
2297
|
}
|
|
2110
2298
|
catch {
|
|
@@ -2113,13 +2301,16 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
|
|
|
2113
2301
|
},
|
|
2114
2302
|
/**
|
|
2115
2303
|
* Check if the message sender is an admin of this group.
|
|
2304
|
+
* Always fetches fresh (bypasses the group-metadata TTL cache) — see
|
|
2305
|
+
* {@link isAdmin} for why: this gates the `botAdmin:`/`admin:`
|
|
2306
|
+
* permission checks in commandPermissions.ts.
|
|
2116
2307
|
* @returns {Promise<boolean>}
|
|
2117
2308
|
*/
|
|
2118
2309
|
async isSenderAdmin() {
|
|
2119
2310
|
if (!chat.isGroup)
|
|
2120
2311
|
return false;
|
|
2121
2312
|
try {
|
|
2122
|
-
const meta = await
|
|
2313
|
+
const meta = await getGroupMetadataFresh(contract, rawJid);
|
|
2123
2314
|
// The adapter's pre-extracted participant fields give us both the
|
|
2124
2315
|
// LID and PN forms of the sender; match either against the group's
|
|
2125
2316
|
// own participant list.
|
|
@@ -2132,6 +2323,9 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
|
|
|
2132
2323
|
},
|
|
2133
2324
|
/**
|
|
2134
2325
|
* Check if the bot is an admin of this group.
|
|
2326
|
+
* Always fetches fresh (bypasses the group-metadata TTL cache) — see
|
|
2327
|
+
* {@link isAdmin} for why: this gates the `botAdmin:` permission
|
|
2328
|
+
* check in commandPermissions.ts.
|
|
2135
2329
|
* @returns {Promise<boolean>}
|
|
2136
2330
|
*/
|
|
2137
2331
|
async isBotAdmin() {
|
|
@@ -2143,7 +2337,7 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
|
|
|
2143
2337
|
if (!botCandidates.some(Boolean))
|
|
2144
2338
|
return false;
|
|
2145
2339
|
try {
|
|
2146
|
-
const meta = await
|
|
2340
|
+
const meta = await getGroupMetadataFresh(contract, rawJid);
|
|
2147
2341
|
return meta.participants.some(p => matchesParticipant(botCandidates, p.id) && (p.admin === "admin" || p.admin === "superadmin"));
|
|
2148
2342
|
}
|
|
2149
2343
|
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
|
},
|