@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.
- package/README.md +28 -3
- package/dist/client/banner.js +10 -0
- package/dist/client/banner.test.js +31 -0
- package/dist/client/store.js +91 -6
- package/dist/client/store.test.js +170 -0
- package/dist/config.js +28 -44
- package/dist/config.test.js +26 -0
- package/dist/download/queue.js +13 -4
- package/dist/drivers/baileys/adapter.js +133 -15
- 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 +384 -62
- package/dist/drivers/baileys/index.js +92 -36
- package/dist/drivers/baileys/loginPrompt.js +0 -2
- package/dist/drivers/baileys/messageHandler.js +344 -4
- package/dist/drivers/baileys/messageHandler.test.js +445 -0
- package/dist/drivers/baileysAdapter.test.js +378 -0
- package/dist/drivers/jid.js +26 -0
- package/dist/drivers/jid.test.js +74 -0
- package/dist/drivers/types.js +5 -5
- package/dist/i18n/index.js +20 -24
- package/dist/kernel/activeDriverSend.js +21 -0
- package/dist/kernel/activeDriverSend.test.js +89 -0
- package/dist/kernel/alerts.js +3 -9
- package/dist/kernel/chatOverrides.js +46 -0
- package/dist/kernel/chatOverrides.test.js +59 -0
- package/dist/kernel/chatSession.js +65 -0
- package/dist/kernel/chatSession.test.js +46 -0
- package/dist/kernel/commandAccess.js +66 -0
- package/dist/kernel/commandAccess.test.js +74 -0
- package/dist/kernel/commandDeprecation.js +170 -0
- package/dist/kernel/commandDeprecation.test.js +114 -0
- package/dist/kernel/commandMenu.js +357 -0
- package/dist/kernel/commandMenu.test.js +363 -0
- package/dist/kernel/commandPermissions.js +171 -0
- package/dist/kernel/commandPermissions.test.js +227 -0
- package/dist/kernel/commandRegistry.js +583 -0
- package/dist/kernel/commandRegistry.test.js +158 -0
- package/dist/kernel/commandsConfig.js +949 -0
- package/dist/kernel/commandsConfig.test.js +482 -0
- package/dist/kernel/contactAutoSave.js +6 -6
- package/dist/kernel/contactAutoSave.test.js +87 -0
- package/dist/kernel/coreCommands.js +62 -0
- package/dist/kernel/driverManager.js +10 -6
- package/dist/kernel/driverManager.test.js +90 -0
- package/dist/kernel/integrationMode.js +88 -0
- package/dist/kernel/integrationMode.test.js +95 -0
- package/dist/kernel/loadIntegrationPlugin.test.js +67 -0
- package/dist/kernel/pluginApi.test.js +600 -0
- package/dist/kernel/pluginGuard.js +18 -13
- package/dist/kernel/pluginGuard.test.js +39 -0
- package/dist/kernel/pluginLoader.js +169 -11
- package/dist/kernel/pluginLoader.test.js +190 -0
- package/dist/kernel/runCommand.js +284 -0
- package/dist/kernel/runCommand.test.js +497 -0
- package/dist/kernel/sendFallbackGuard.js +19 -48
- package/dist/kernel/sendFallbackGuard.test.js +80 -0
- package/dist/kernel/sendGuard.js +38 -42
- package/dist/kernel/sendGuard.test.js +102 -0
- package/dist/kernel/settingsDb.js +19 -5
- package/dist/kernel/statusServer.js +9 -2
- package/dist/kernel/statusServer.test.js +70 -0
- package/dist/kernel/testConfig.js +192 -0
- package/dist/kernel/testConfig.test.js +181 -0
- package/dist/kernel/updateCheck.js +33 -10
- package/dist/locales/en.json +77 -13
- package/dist/locales/es.json +77 -13
- package/dist/locales/pt.json +77 -13
- package/dist/logger/logger.js +23 -3
- package/dist/logger/logger.test.js +45 -0
- package/dist/main.js +5 -76
- package/dist/plugins/__manybot_integration__/index.js +184 -0
- package/dist/plugins/__manybot_integration__/index.test.js +218 -0
- package/dist/utils/phoneNumber.js +83 -0
- package/dist/utils/phoneNumber.test.js +53 -0
- package/package.json +76 -18
- package/dist/drivers/whatsmeow/client.js +0 -252
- package/dist/drivers/whatsmeow/index.js +0 -79
- package/dist/drivers/whatsmeow/installer.js +0 -86
- package/dist/drivers/whatsmeow/supervisor.js +0 -328
- 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
|
|
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
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
// `
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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,
|
|
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
|
|
184
|
-
const botCandidates = [
|
|
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
|
-
|
|
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
|
-
|
|
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 && !
|
|
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 (!
|
|
606
|
+
if (!isGroup && !isWAAccount)
|
|
492
607
|
return null;
|
|
493
|
-
if (contract && !
|
|
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:
|
|
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
|
|
631
|
+
isMe: (botJid && jid === normalizeJid(botJid)) || (botLid && jid === normalizeJid(botLid)) || false,
|
|
513
632
|
isWAAccount,
|
|
514
|
-
isUser: !
|
|
515
|
-
isGroup
|
|
516
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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[
|
|
867
|
-
?? store.contacts[denormalizeJid(
|
|
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 =
|
|
871
|
-
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
1528
|
-
const
|
|
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) {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ──────────────────────────────────────────────────────────────────
|