@manybot/manybot 5.7.0 → 5.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -3
- package/dist/client/banner.js +10 -0
- package/dist/client/banner.test.js +31 -0
- package/dist/client/store.js +56 -5
- package/dist/client/store.test.js +170 -0
- package/dist/config.js +28 -44
- package/dist/config.test.js +26 -0
- package/dist/drivers/baileys/adapter.js +58 -7
- package/dist/drivers/baileys/api/index.js +172 -24
- package/dist/drivers/baileys/index.js +62 -30
- package/dist/drivers/baileys/loginPrompt.js +0 -2
- package/dist/drivers/baileys/messageHandler.js +158 -4
- package/dist/drivers/baileys/messageHandler.test.js +203 -0
- package/dist/drivers/baileysAdapter.test.js +281 -0
- package/dist/drivers/jid.test.js +40 -0
- package/dist/drivers/types.js +5 -5
- package/dist/i18n/index.js +15 -2
- 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/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 +168 -0
- package/dist/kernel/commandDeprecation.test.js +107 -0
- package/dist/kernel/commandMenu.js +268 -0
- package/dist/kernel/commandMenu.test.js +234 -0
- package/dist/kernel/commandPermissions.js +125 -0
- package/dist/kernel/commandPermissions.test.js +159 -0
- package/dist/kernel/commandRegistry.js +459 -0
- package/dist/kernel/commandRegistry.test.js +156 -0
- package/dist/kernel/commandsConfig.js +517 -0
- package/dist/kernel/commandsConfig.test.js +236 -0
- package/dist/kernel/contactAutoSave.test.js +87 -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 +583 -0
- package/dist/kernel/pluginGuard.js +15 -12
- package/dist/kernel/pluginGuard.test.js +39 -0
- package/dist/kernel/pluginLoader.js +96 -1
- package/dist/kernel/pluginLoader.test.js +80 -0
- package/dist/kernel/runCommand.js +245 -0
- package/dist/kernel/runCommand.test.js +235 -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 +4 -3
- package/dist/kernel/statusServer.js +9 -2
- package/dist/kernel/statusServer.test.js +70 -0
- package/dist/kernel/testConfig.js +183 -0
- package/dist/kernel/testConfig.test.js +181 -0
- package/dist/kernel/updateCheck.js +33 -10
- package/dist/locales/en.json +64 -13
- package/dist/locales/es.json +64 -13
- package/dist/locales/pt.json +64 -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 +167 -0
- package/dist/plugins/__manybot_integration__/index.test.js +184 -0
- package/package.json +74 -17
- 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
|
@@ -72,6 +72,22 @@ export function decodeContent(content) {
|
|
|
72
72
|
type = "sticker";
|
|
73
73
|
mimetype = m.stickerMessage.mimetype ?? undefined;
|
|
74
74
|
}
|
|
75
|
+
else if (m?.templateMessage) {
|
|
76
|
+
type = "text";
|
|
77
|
+
const tpl = m.templateMessage.hydratedTemplate ?? m.templateMessage.hydratedFourRowTemplate;
|
|
78
|
+
const buttonUrls = tpl?.hydratedButtons?.map((b) => b.urlButton?.url).filter(Boolean).join(" ") ?? "";
|
|
79
|
+
body = [tpl?.hydratedContentText ?? "", buttonUrls].filter(Boolean).join(" ");
|
|
80
|
+
}
|
|
81
|
+
else if (m?.interactiveMessage) {
|
|
82
|
+
type = "text";
|
|
83
|
+
const buttonParams = m.interactiveMessage.nativeFlowMessage?.buttons
|
|
84
|
+
?.map((b) => b.buttonParamsJson).filter(Boolean).join(" ") ?? "";
|
|
85
|
+
body = [m.interactiveMessage.body?.text ?? "", buttonParams].filter(Boolean).join(" ");
|
|
86
|
+
}
|
|
87
|
+
else if (m?.buttonsMessage) {
|
|
88
|
+
type = "text";
|
|
89
|
+
body = [m.buttonsMessage.contentText ?? "", m.buttonsMessage.footerText ?? ""].filter(Boolean).join(" ");
|
|
90
|
+
}
|
|
75
91
|
return { type, body, mimetype };
|
|
76
92
|
}
|
|
77
93
|
export function createBaileysAdapter(initial) {
|
|
@@ -115,6 +131,29 @@ export function createBaileysAdapter(initial) {
|
|
|
115
131
|
// which throws if `.message` is missing.
|
|
116
132
|
return { quoted: { key: toFlatKey(quoted), message: inner } };
|
|
117
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Merge `quoted` reply-citation with the chat's current
|
|
136
|
+
* `ephemeralExpiration` so every outgoing send in a chat with a
|
|
137
|
+
* disappearing-message timer inherits it automatically. Returns
|
|
138
|
+
* `undefined` when there's nothing to set, so callers can pass it
|
|
139
|
+
* through as the third argument to `sock.sendMessage` without
|
|
140
|
+
* conditionally building the options object.
|
|
141
|
+
*
|
|
142
|
+
* The timer is read off the in-memory store, which is populated by
|
|
143
|
+
* `chats.upsert` / `chats.update` / `messages.upsert` (via the
|
|
144
|
+
* `ephemeralMessage` envelope) and from `groupMetadata().ephemeralDuration`
|
|
145
|
+
* in this same adapter — i.e. everything WhatsApp tells us about the
|
|
146
|
+
* chat's timer ends up there, and the send path just reads it back.
|
|
147
|
+
*/
|
|
148
|
+
function buildSendOpts(jid, quoted) {
|
|
149
|
+
const quotedOpts = buildQuotedOpts(quoted);
|
|
150
|
+
const timer = store.chats.get(jid)?.ephemeralExpiration;
|
|
151
|
+
if (!timer)
|
|
152
|
+
return quotedOpts;
|
|
153
|
+
if (!quotedOpts)
|
|
154
|
+
return { ephemeralExpiration: timer };
|
|
155
|
+
return { ...quotedOpts, ephemeralExpiration: timer };
|
|
156
|
+
}
|
|
118
157
|
/**
|
|
119
158
|
* Translate a neutral `BotQuotedRef` into the FLAT key shape Baileys
|
|
120
159
|
* expects for `react`/`delete`/`edit`/`readMessages` (proto.IMessageKey
|
|
@@ -151,6 +190,9 @@ export function createBaileysAdapter(initial) {
|
|
|
151
190
|
m?.videoMessage?.contextInfo ??
|
|
152
191
|
m?.audioMessage?.contextInfo ??
|
|
153
192
|
m?.documentMessage?.contextInfo ??
|
|
193
|
+
m?.templateMessage?.contextInfo ??
|
|
194
|
+
m?.interactiveMessage?.contextInfo ??
|
|
195
|
+
m?.buttonsMessage?.contextInfo ??
|
|
154
196
|
undefined;
|
|
155
197
|
const ciTyped = contextInfo;
|
|
156
198
|
return {
|
|
@@ -295,7 +337,7 @@ export function createBaileysAdapter(initial) {
|
|
|
295
337
|
async sendText(jid, text, opts) {
|
|
296
338
|
const content = { text };
|
|
297
339
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
298
|
-
const sendOpts =
|
|
340
|
+
const sendOpts = buildSendOpts(jid, opts?.quoted);
|
|
299
341
|
if (opts?.mentions?.length)
|
|
300
342
|
content.mentions = opts.mentions;
|
|
301
343
|
const ref = await sock.sendMessage(jid, content, sendOpts);
|
|
@@ -309,7 +351,7 @@ export function createBaileysAdapter(initial) {
|
|
|
309
351
|
content.viewOnce = true;
|
|
310
352
|
if (opts?.mentions?.length)
|
|
311
353
|
content.mentions = opts.mentions;
|
|
312
|
-
const ref = await sock.sendMessage(jid, content,
|
|
354
|
+
const ref = await sock.sendMessage(jid, content, buildSendOpts(jid, opts?.quoted));
|
|
313
355
|
return toSentRef(ref, jid);
|
|
314
356
|
},
|
|
315
357
|
async sendVideo(jid, buffer, opts) {
|
|
@@ -322,7 +364,7 @@ export function createBaileysAdapter(initial) {
|
|
|
322
364
|
content.gifPlayback = true;
|
|
323
365
|
if (opts?.mentions?.length)
|
|
324
366
|
content.mentions = opts.mentions;
|
|
325
|
-
const ref = await sock.sendMessage(jid, content,
|
|
367
|
+
const ref = await sock.sendMessage(jid, content, buildSendOpts(jid, opts?.quoted));
|
|
326
368
|
return toSentRef(ref, jid);
|
|
327
369
|
},
|
|
328
370
|
async sendAudio(jid, buffer, opts) {
|
|
@@ -332,20 +374,20 @@ export function createBaileysAdapter(initial) {
|
|
|
332
374
|
content.ptt = true;
|
|
333
375
|
if (opts?.viewOnce)
|
|
334
376
|
content.viewOnce = true;
|
|
335
|
-
const ref = await sock.sendMessage(jid, content,
|
|
377
|
+
const ref = await sock.sendMessage(jid, content, buildSendOpts(jid, opts?.quoted));
|
|
336
378
|
return toSentRef(ref, jid);
|
|
337
379
|
},
|
|
338
380
|
async sendSticker(jid, buffer, opts) {
|
|
339
|
-
const ref = await sock.sendMessage(jid, { sticker: buffer },
|
|
381
|
+
const ref = await sock.sendMessage(jid, { sticker: buffer }, buildSendOpts(jid, opts?.quoted));
|
|
340
382
|
return toSentRef(ref, jid);
|
|
341
383
|
},
|
|
342
384
|
async sendDocument(jid, buffer, filename, mimetype, opts) {
|
|
343
|
-
const ref = await sock.sendMessage(jid, { document: buffer, mimetype, fileName: filename },
|
|
385
|
+
const ref = await sock.sendMessage(jid, { document: buffer, mimetype, fileName: filename }, buildSendOpts(jid, opts?.quoted));
|
|
344
386
|
return toSentRef(ref, jid);
|
|
345
387
|
},
|
|
346
388
|
async sendPoll(jid, opts) {
|
|
347
389
|
const poll = { name: opts.name, values: opts.values, selectableCount: opts.selectableCount ?? 1 };
|
|
348
|
-
const ref = await sock.sendMessage(jid, { poll },
|
|
390
|
+
const ref = await sock.sendMessage(jid, { poll }, buildSendOpts(jid, opts.quoted));
|
|
349
391
|
return toSentRef(ref, jid);
|
|
350
392
|
},
|
|
351
393
|
async react(jid, target, emoji) {
|
|
@@ -427,6 +469,15 @@ export function createBaileysAdapter(initial) {
|
|
|
427
469
|
// ── groups ─────────────────────────────────────────────────────────────
|
|
428
470
|
async groupMetadata(jid) {
|
|
429
471
|
const meta = await sock.groupMetadata(jid);
|
|
472
|
+
// WhatsApp reports the group's current disappearing-message
|
|
473
|
+
// timer as `ephemeralDuration` on the metadata payload — promote
|
|
474
|
+
// it to the in-memory store so the next send to this group
|
|
475
|
+
// picks it up automatically (and so the timer survives a
|
|
476
|
+
// snapshot round-trip without us re-fetching metadata).
|
|
477
|
+
const rawDuration = meta.ephemeralDuration;
|
|
478
|
+
if (rawDuration !== undefined) {
|
|
479
|
+
store.setChatEphemeralExpiration(jid, Number(rawDuration) || 0);
|
|
480
|
+
}
|
|
430
481
|
return {
|
|
431
482
|
subject: meta.subject,
|
|
432
483
|
participants: meta.participants.map(p => ({
|
|
@@ -14,6 +14,7 @@ import { logger } from "#logger";
|
|
|
14
14
|
import { t, createPluginT, reloadTranslations, getCurrentLang } from "#i18n";
|
|
15
15
|
import { CONFIG, CONFIG_DIR } from "#config";
|
|
16
16
|
import { enqueue } from "#download";
|
|
17
|
+
import { waitForEditSlot } from "#kernel/sendGuard.js";
|
|
17
18
|
import { schedule, cancelPlugin } from "#kernel/scheduler.js";
|
|
18
19
|
import { emptyFolder } from "#utils/file.js";
|
|
19
20
|
import { normalizeJid, denormalizeJid, toWireJid } from "#drivers/jid.js";
|
|
@@ -24,9 +25,12 @@ import path from "path";
|
|
|
24
25
|
import os from "os";
|
|
25
26
|
import { spawn } from "child_process";
|
|
26
27
|
import { randomUUID } from "crypto";
|
|
27
|
-
import { waitForSendSlot, simulateState, typingDuration, mediaDuration
|
|
28
|
+
import { waitForSendSlot, simulateState, typingDuration, mediaDuration } from "#sendguard";
|
|
28
29
|
import { sendWithFallback } from "#kernel/sendFallbackGuard.js";
|
|
29
30
|
import { buildSettingsApi } from "#settingsdb";
|
|
31
|
+
import * as commandAccess from "#kernel/commandAccess.js";
|
|
32
|
+
import * as chatSession from "#kernel/chatSession.js";
|
|
33
|
+
import { resolveDispatch, runCommand as dispatchCommand } from "#kernel/runCommand.js";
|
|
30
34
|
import WebP from "node-webpmux";
|
|
31
35
|
import { jidNormalizedUser, } from "@whiskeysockets/baileys";
|
|
32
36
|
// ── Raw-Baileys escape hatch ─────────────────────────────────────────────────
|
|
@@ -175,13 +179,13 @@ function hasMention(contextInfo) {
|
|
|
175
179
|
return !!(ci?.mentionedJid && Array.isArray(ci.mentionedJid) && ci.mentionedJid.length > 0);
|
|
176
180
|
}
|
|
177
181
|
/** True if the bot's own JID (PN or LID) is in contextInfo.mentionedJid. */
|
|
178
|
-
function hasBotMention(contextInfo,
|
|
182
|
+
function hasBotMention(contextInfo, contract, store) {
|
|
179
183
|
const ci = contextInfo;
|
|
180
184
|
const mentioned = ci?.mentionedJid;
|
|
181
185
|
if (!mentioned || mentioned.length === 0)
|
|
182
186
|
return false;
|
|
183
|
-
const
|
|
184
|
-
const botCandidates = [
|
|
187
|
+
const me = contract.me();
|
|
188
|
+
const botCandidates = [me.id, me.lid]
|
|
185
189
|
.filter((v) => !!v)
|
|
186
190
|
.map(v => normalizeJid(store.resolveJid(normalizeJid(v))));
|
|
187
191
|
if (botCandidates.length === 0)
|
|
@@ -517,6 +521,27 @@ async function normalizeContact(jid, info, botJid, contract) {
|
|
|
517
521
|
};
|
|
518
522
|
}
|
|
519
523
|
// ── Chats API ─────────────────────────────────────────────────────────────────
|
|
524
|
+
function buildCommandsApi() {
|
|
525
|
+
return {
|
|
526
|
+
exists: commandAccess.exists,
|
|
527
|
+
desc: commandAccess.desc,
|
|
528
|
+
manual: commandAccess.manual,
|
|
529
|
+
list: commandAccess.list,
|
|
530
|
+
isMenuAlias: commandAccess.isMenuAlias,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
// ── Session API (Phase 7, MANYBOT-6.md) ─────────────────────────────────────
|
|
534
|
+
// Scoped to the current chat (normJid) + the calling plugin. Only built in
|
|
535
|
+
// buildApi() (runtime), not buildBaseApi() — there is no "current chat" at
|
|
536
|
+
// setup time to lock.
|
|
537
|
+
function buildSessionApi(chatId, pluginName) {
|
|
538
|
+
return {
|
|
539
|
+
acquire: () => chatSession.acquireSession(chatId, pluginName),
|
|
540
|
+
release: () => { chatSession.releaseSession(chatId, pluginName); },
|
|
541
|
+
isLocked: () => chatSession.isSessionLocked(chatId),
|
|
542
|
+
isMine: () => chatSession.getSessionHolder(chatId) === pluginName,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
520
545
|
function buildChatsApi(store) {
|
|
521
546
|
return {
|
|
522
547
|
/**
|
|
@@ -719,7 +744,6 @@ function makeHistoryArray(entries, store) {
|
|
|
719
744
|
return arr;
|
|
720
745
|
}
|
|
721
746
|
export function buildMessageContext(msg, contract, store, guardOptions = {}) {
|
|
722
|
-
const sock = rawSocketOf(contract);
|
|
723
747
|
const body = getMsgBody(msg);
|
|
724
748
|
const prefix = CONFIG.CMD_PREFIX;
|
|
725
749
|
const rawArgs = body.trim().split(/\s+/);
|
|
@@ -742,6 +766,21 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
|
|
|
742
766
|
// We carry the same _raw.contextInfo on the synthetic so a recursive
|
|
743
767
|
// getReply().getReply() keeps working (the inner call re-reads
|
|
744
768
|
// getContextInfo off _raw.contextInfo).
|
|
769
|
+
//
|
|
770
|
+
// WhatsApp's contextInfo never carries the quoted author's pushname, so
|
|
771
|
+
// we best-effort it from the local contact store (same lookup pattern
|
|
772
|
+
// as getContact() below) instead of leaving it undefined -> falling
|
|
773
|
+
// back to the raw phone number in senderName.
|
|
774
|
+
const lookupPushName = (jid) => {
|
|
775
|
+
if (!jid)
|
|
776
|
+
return undefined;
|
|
777
|
+
const contacts = store.contacts;
|
|
778
|
+
const info = contacts[jid]
|
|
779
|
+
?? contacts[denormalizeJid(jid)]
|
|
780
|
+
?? contacts[store.resolveJid(jid)]
|
|
781
|
+
?? contacts[denormalizeJid(store.resolveJid(jid))];
|
|
782
|
+
return info?.notify ?? undefined;
|
|
783
|
+
};
|
|
745
784
|
const quotedRaw = contextInfo?.quotedMessage
|
|
746
785
|
? (() => {
|
|
747
786
|
const decoded = decodeContent(contextInfo.quotedMessage);
|
|
@@ -754,6 +793,8 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
|
|
|
754
793
|
timestamp: 0,
|
|
755
794
|
body: decoded.body,
|
|
756
795
|
mimetype: decoded.mimetype,
|
|
796
|
+
fromPn: contextInfo.participant ?? undefined,
|
|
797
|
+
pushName: lookupPushName(contextInfo.participant),
|
|
757
798
|
_raw: {
|
|
758
799
|
contextInfo: {
|
|
759
800
|
stanzaId: contextInfo.stanzaId,
|
|
@@ -777,6 +818,8 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
|
|
|
777
818
|
type: "other",
|
|
778
819
|
contentHash: "",
|
|
779
820
|
timestamp: 0,
|
|
821
|
+
fromPn: msg.quotedKey.participant ?? undefined,
|
|
822
|
+
pushName: lookupPushName(msg.quotedKey.participant),
|
|
780
823
|
}
|
|
781
824
|
: null;
|
|
782
825
|
return {
|
|
@@ -821,7 +864,7 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
|
|
|
821
864
|
return buildMessageContext(quotedRaw, contract, store, { cooldown: false, jitter: false });
|
|
822
865
|
},
|
|
823
866
|
hasMention: hasMention(contextInfo),
|
|
824
|
-
hasBotMention: hasBotMention(contextInfo,
|
|
867
|
+
hasBotMention: hasBotMention(contextInfo, contract, store),
|
|
825
868
|
reply: makeSender(contract, store, rawJid, msg, { cooldown, jitter }),
|
|
826
869
|
async react(emoji) {
|
|
827
870
|
await contract.react(rawJid, {
|
|
@@ -845,7 +888,10 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
|
|
|
845
888
|
if (!msg.fromMe) {
|
|
846
889
|
throw new Error("[pluginApi] edit() can only be used on the bot's own messages");
|
|
847
890
|
}
|
|
848
|
-
if (!msg.id
|
|
891
|
+
if (!msg.id)
|
|
892
|
+
return;
|
|
893
|
+
const allowed = await waitForEditSlot(msg.id);
|
|
894
|
+
if (!allowed)
|
|
849
895
|
return;
|
|
850
896
|
await contract.editMessage(rawJid, {
|
|
851
897
|
id: msg.id,
|
|
@@ -867,7 +913,7 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
|
|
|
867
913
|
?? store.contacts[denormalizeJid(sender)]
|
|
868
914
|
?? store.contacts[store.resolveJid(msg.fromPn ?? "")]
|
|
869
915
|
?? store.contacts[denormalizeJid(store.resolveJid(msg.fromPn ?? ""))];
|
|
870
|
-
const botJid =
|
|
916
|
+
const botJid = contract.me().id ? jidNormalizedUser(contract.me().id) : null;
|
|
871
917
|
return normalizeContact(sender, info, botJid, contract);
|
|
872
918
|
},
|
|
873
919
|
};
|
|
@@ -961,7 +1007,10 @@ class MessageHandle {
|
|
|
961
1007
|
if (!msg.fromMe) {
|
|
962
1008
|
throw new Error("[pluginApi] edit() can only be used on the bot's own messages");
|
|
963
1009
|
}
|
|
964
|
-
if (!msg.id
|
|
1010
|
+
if (!msg.id)
|
|
1011
|
+
return;
|
|
1012
|
+
const allowed = await waitForEditSlot(msg.id);
|
|
1013
|
+
if (!allowed)
|
|
965
1014
|
return;
|
|
966
1015
|
await this._contract.editMessage(msg.chatId, {
|
|
967
1016
|
id: msg.id,
|
|
@@ -1472,8 +1521,42 @@ async function resolveMentionJids(contract, store, jid, mentions) {
|
|
|
1472
1521
|
}
|
|
1473
1522
|
return Array.from(new Set(result.filter(Boolean)));
|
|
1474
1523
|
}
|
|
1475
|
-
function buildAdminApi(contract, chatJid) {
|
|
1476
|
-
|
|
1524
|
+
function buildAdminApi(contract, store, chatJid) {
|
|
1525
|
+
/**
|
|
1526
|
+
* Resolve admin-supplied identifiers (bare phone number, @c.us,
|
|
1527
|
+
* @s.whatsapp.net, or @lid) to the exact jid WhatsApp has on file for
|
|
1528
|
+
* that participant *in this group*. A naive toWireJid() guess is only
|
|
1529
|
+
* correct for pn-addressed groups — lid-addressed groups (increasingly
|
|
1530
|
+
* common, e.g. when a member hides their phone number) only recognize
|
|
1531
|
+
* that member by @lid, which a phone number typed by the admin can't
|
|
1532
|
+
* produce on its own. Baileys' raw groupMetadata() participants carry
|
|
1533
|
+
* `id` (whatever WA addresses them as here), `jid` (its best
|
|
1534
|
+
* phone-number guess) and `lid` — check all three.
|
|
1535
|
+
*
|
|
1536
|
+
* Throws per-identifier if it doesn't match a current member, instead
|
|
1537
|
+
* of silently sending a guessed jid that WhatsApp rejects deep inside
|
|
1538
|
+
* the per-participant status array (see assertParticipantsUpdateOk).
|
|
1539
|
+
*/
|
|
1540
|
+
async function resolveTargets(groupJid, identifiers) {
|
|
1541
|
+
const meta = await getGroupMetadataCached(contract, groupJid);
|
|
1542
|
+
const out = [];
|
|
1543
|
+
for (const id of identifiers) {
|
|
1544
|
+
const wire = toWireJid(id);
|
|
1545
|
+
const resolved = normalizeJid(store.resolveJid(id));
|
|
1546
|
+
const match = meta.participants.find((p) => {
|
|
1547
|
+
const pAny = p;
|
|
1548
|
+
return (toWireJid(pAny.id) === wire ||
|
|
1549
|
+
normalizeJid(store.resolveJid(pAny.id)) === resolved ||
|
|
1550
|
+
(pAny.jid ? toWireJid(pAny.jid) === wire : false) ||
|
|
1551
|
+
(pAny.lid ? toWireJid(pAny.lid) === wire : false));
|
|
1552
|
+
});
|
|
1553
|
+
if (!match) {
|
|
1554
|
+
throw new Error(t("driver.groupParticipantNotFound", { id, group: groupJid }));
|
|
1555
|
+
}
|
|
1556
|
+
out.push(match.id);
|
|
1557
|
+
}
|
|
1558
|
+
return Array.from(new Set(out));
|
|
1559
|
+
}
|
|
1477
1560
|
function requireChat() {
|
|
1478
1561
|
if (!chatJid)
|
|
1479
1562
|
throw new Error("This admin operation requires a runtime group context.");
|
|
@@ -1503,7 +1586,7 @@ function buildAdminApi(contract, chatJid) {
|
|
|
1503
1586
|
const failed = results.filter((r) => r?.status && r.status !== "200");
|
|
1504
1587
|
if (failed.length > 0) {
|
|
1505
1588
|
const detail = failed.map((r) => `${r.jid ?? "?"}=${r.status}`).join(", ");
|
|
1506
|
-
throw new Error(
|
|
1589
|
+
throw new Error(t("driver.groupParticipantsUpdateRejected", { action, detail }));
|
|
1507
1590
|
}
|
|
1508
1591
|
}
|
|
1509
1592
|
/**
|
|
@@ -1518,16 +1601,35 @@ function buildAdminApi(contract, chatJid) {
|
|
|
1518
1601
|
results = await contract.groupParticipantsUpdate(jid, users, action);
|
|
1519
1602
|
}
|
|
1520
1603
|
catch (err) {
|
|
1521
|
-
throw new Error(
|
|
1604
|
+
throw new Error(t("driver.groupParticipantsUpdateFailed", {
|
|
1605
|
+
action,
|
|
1606
|
+
group: jid,
|
|
1607
|
+
users: users.join(", "),
|
|
1608
|
+
message: err.message,
|
|
1609
|
+
}));
|
|
1522
1610
|
}
|
|
1523
1611
|
assertParticipantsUpdateOk(action, results);
|
|
1524
1612
|
return results;
|
|
1525
1613
|
}
|
|
1526
|
-
function createTargetableAction(action, memberIds) {
|
|
1527
|
-
const
|
|
1528
|
-
const
|
|
1614
|
+
function createTargetableAction(action, memberIds, mode = "existingMember") {
|
|
1615
|
+
const raw = Array.isArray(memberIds) ? memberIds : [memberIds];
|
|
1616
|
+
const resolve = async (groupJid) => mode === "existingMember"
|
|
1617
|
+
? resolveTargets(groupJid, raw)
|
|
1618
|
+
// `add` targets people who aren't members yet — there's no
|
|
1619
|
+
// participant-list entry to match against, so fall back to a
|
|
1620
|
+
// plain jid guess (the historical behavior).
|
|
1621
|
+
: Array.from(new Set(raw.map(toWireJid)));
|
|
1622
|
+
const executeCurrent = async () => {
|
|
1623
|
+
requireChat();
|
|
1624
|
+
const users = await resolve(chatJid);
|
|
1625
|
+
return action(chatJid, users);
|
|
1626
|
+
};
|
|
1529
1627
|
return {
|
|
1530
|
-
async to(targetJid) {
|
|
1628
|
+
async to(targetJid) {
|
|
1629
|
+
await getGroup(targetJid);
|
|
1630
|
+
const users = await resolve(targetJid);
|
|
1631
|
+
return action(targetJid, users);
|
|
1632
|
+
},
|
|
1531
1633
|
then(onfulfilled, onrejected) {
|
|
1532
1634
|
return executeCurrent().then(onfulfilled, onrejected);
|
|
1533
1635
|
},
|
|
@@ -1542,22 +1644,25 @@ function buildAdminApi(contract, chatJid) {
|
|
|
1542
1644
|
return {
|
|
1543
1645
|
/** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
|
|
1544
1646
|
add(memberIds) {
|
|
1545
|
-
return createTargetableAction((jid, users) => runParticipantsUpdate(jid, users, "add"), memberIds);
|
|
1647
|
+
return createTargetableAction((jid, users) => runParticipantsUpdate(jid, users, "add"), memberIds, "newMember");
|
|
1546
1648
|
},
|
|
1547
1649
|
/** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
|
|
1548
1650
|
async kick(memberIds) {
|
|
1549
1651
|
requireChat();
|
|
1550
|
-
|
|
1652
|
+
const users = await resolveTargets(chatJid, Array.isArray(memberIds) ? memberIds : [memberIds]);
|
|
1653
|
+
return runParticipantsUpdate(chatJid, users, "remove");
|
|
1551
1654
|
},
|
|
1552
1655
|
/** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
|
|
1553
1656
|
async promote(memberIds) {
|
|
1554
1657
|
requireChat();
|
|
1555
|
-
|
|
1658
|
+
const users = await resolveTargets(chatJid, Array.isArray(memberIds) ? memberIds : [memberIds]);
|
|
1659
|
+
return runParticipantsUpdate(chatJid, users, "promote");
|
|
1556
1660
|
},
|
|
1557
1661
|
/** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
|
|
1558
1662
|
async demote(memberIds) {
|
|
1559
1663
|
requireChat();
|
|
1560
|
-
|
|
1664
|
+
const users = await resolveTargets(chatJid, Array.isArray(memberIds) ? memberIds : [memberIds]);
|
|
1665
|
+
return runParticipantsUpdate(chatJid, users, "demote");
|
|
1561
1666
|
},
|
|
1562
1667
|
/** @param {string} name */
|
|
1563
1668
|
async setSubject(name) {
|
|
@@ -1765,7 +1870,7 @@ function buildPollApi(contract, store, rawJid, guardOptions, pluginName) {
|
|
|
1765
1870
|
handle._updateFromAggregated(aggregated);
|
|
1766
1871
|
}
|
|
1767
1872
|
catch (err) {
|
|
1768
|
-
logger.error(`[poll]
|
|
1873
|
+
logger.error(`[poll] ${t("driver.pollDecryptFailed", { error: err })}`);
|
|
1769
1874
|
}
|
|
1770
1875
|
}
|
|
1771
1876
|
});
|
|
@@ -1835,6 +1940,7 @@ function buildBaseApi(contract, store, pluginRegistry, pluginName) {
|
|
|
1835
1940
|
contacts: buildContactsApi(contract, store, botJid),
|
|
1836
1941
|
storage: buildStorageApi(pluginName),
|
|
1837
1942
|
botId: botJid,
|
|
1943
|
+
commands: buildCommandsApi(),
|
|
1838
1944
|
};
|
|
1839
1945
|
}
|
|
1840
1946
|
// ── Setup API ─────────────────────────────────────────────────────────────────
|
|
@@ -1852,12 +1958,52 @@ export function buildSetupApi(contract, store, pluginRegistry, pluginName) {
|
|
|
1852
1958
|
return {
|
|
1853
1959
|
...buildBaseApi(contract, store, pluginRegistry, pluginName),
|
|
1854
1960
|
...buildSetupSendApi(contract, store),
|
|
1855
|
-
admin: buildAdminApi(contract, null),
|
|
1961
|
+
admin: buildAdminApi(contract, store, null),
|
|
1856
1962
|
events: buildEventsApi(contract, pluginName),
|
|
1857
1963
|
me: buildMeApi(contract),
|
|
1858
1964
|
settings: { global: buildSettingsApi(pluginName, "_global").global },
|
|
1859
1965
|
};
|
|
1860
1966
|
}
|
|
1967
|
+
// ── ctx.runCommand facet ─────────────────────────────────────────────────────
|
|
1968
|
+
/**
|
|
1969
|
+
* Builds the `ctx.runCommand(invocation, rawArgs?)` facet for a runtime
|
|
1970
|
+
* ctx bound to `msg`/`chat`. Deliberately built AFTER `buildApi` is
|
|
1971
|
+
* declared (function declarations hoist) so it can call `buildApi`
|
|
1972
|
+
* again, recursively, to build a context scoped to the TARGET
|
|
1973
|
+
* command's owning plugin rather than reusing the caller's — same
|
|
1974
|
+
* "own storage/plugins facet, not the caller's" principle as
|
|
1975
|
+
* `ctx.plugins.require()`.
|
|
1976
|
+
*
|
|
1977
|
+
* @param {BotMessage} msg
|
|
1978
|
+
* @param {WAChat} chat
|
|
1979
|
+
* @param {WaContract} contract
|
|
1980
|
+
* @param {BotStore} store
|
|
1981
|
+
* @param {Map} pluginRegistry
|
|
1982
|
+
* @param {string} callerPluginName
|
|
1983
|
+
*/
|
|
1984
|
+
function buildRunCommandFacet(msg, chat, contract, store, pluginRegistry, callerPluginName) {
|
|
1985
|
+
return async function runCommandFacet(invocation, rawArgs = "") {
|
|
1986
|
+
const resolution = resolveDispatch(invocation, rawArgs);
|
|
1987
|
+
if (resolution.target.kind === "none") {
|
|
1988
|
+
return { status: "no_dispatch", sentReply: null, suggestedReply: null };
|
|
1989
|
+
}
|
|
1990
|
+
const entry = resolution.target.kind === "sub" ? resolution.target.parent : resolution.target.entry;
|
|
1991
|
+
const targetPluginName = entry.source === "plugin" ? entry.pluginName : null;
|
|
1992
|
+
const targetCtx = targetPluginName && targetPluginName !== callerPluginName
|
|
1993
|
+
? buildApi({
|
|
1994
|
+
msg, chat, contract, store, pluginRegistry,
|
|
1995
|
+
pluginName: targetPluginName,
|
|
1996
|
+
guardOptions: pluginRegistry.get(targetPluginName)?.guardOptions ?? {},
|
|
1997
|
+
})
|
|
1998
|
+
: buildApi({ msg, chat, contract, store, pluginRegistry, pluginName: callerPluginName });
|
|
1999
|
+
return dispatchCommand({
|
|
2000
|
+
pluginName: targetPluginName,
|
|
2001
|
+
ctx: targetCtx,
|
|
2002
|
+
resolution,
|
|
2003
|
+
reply: targetCtx.msg.reply,
|
|
2004
|
+
});
|
|
2005
|
+
};
|
|
2006
|
+
}
|
|
1861
2007
|
// ── Runtime API ───────────────────────────────────────────────────────────────
|
|
1862
2008
|
/**
|
|
1863
2009
|
* Runtime API — full context with message and chat.
|
|
@@ -1907,6 +2053,8 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
|
|
|
1907
2053
|
return {
|
|
1908
2054
|
...buildBaseApi(contract, store, pluginRegistry, pluginName),
|
|
1909
2055
|
...buildSendApi(contract, store, rawJid, guardOptions),
|
|
2056
|
+
runCommand: buildRunCommandFacet(msg, chat, contract, store, pluginRegistry, pluginName),
|
|
2057
|
+
session: buildSessionApi(normJid, pluginName),
|
|
1910
2058
|
// ── msg ──────────────────────────────────────────────────────────────────
|
|
1911
2059
|
msg: buildMessageContext(msg, contract, store, { cooldown, jitter }),
|
|
1912
2060
|
// ── chat ─────────────────────────────────────────────────────────────────
|
|
@@ -2008,7 +2156,7 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
|
|
|
2008
2156
|
},
|
|
2009
2157
|
},
|
|
2010
2158
|
// ── admin ─────────────────────────────────────────────────────────────────
|
|
2011
|
-
admin: buildAdminApi(contract, rawJid),
|
|
2159
|
+
admin: buildAdminApi(contract, store, rawJid),
|
|
2012
2160
|
// ── me ────────────────────────────────────────────────────────────────────
|
|
2013
2161
|
me: buildMeApi(contract),
|
|
2014
2162
|
// ── poll ──────────────────────────────────────────────────────────────────
|