@manybot/manybot 5.3.0 → 5.3.2
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/dist/client/store.js +27 -18
- package/dist/drivers/whatsapp/api/index.js +102 -31
- package/dist/drivers/whatsapp/index.js +6 -2
- package/package.json +1 -1
package/dist/client/store.js
CHANGED
|
@@ -73,12 +73,36 @@ export function createStore() {
|
|
|
73
73
|
chatMsgs.delete(oldest);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
+
// pushName only ever arrives on a message stanza (live or synced history)
|
|
77
|
+
// — contact/history-contact sync alone won't give us a name for someone
|
|
78
|
+
// who never messaged and isn't a saved contact. Shared by both
|
|
79
|
+
// "messages.upsert" (live) and "messaging-history.set" (backfill on
|
|
80
|
+
// connect), since history-sync messages carry the same field.
|
|
81
|
+
function capturePushNames(msgs) {
|
|
82
|
+
for (const msg of msgs) {
|
|
83
|
+
const key = msg.key;
|
|
84
|
+
learnLid(key.participantAlt, key.participant);
|
|
85
|
+
learnLid(key.remoteJidAlt, key.remoteJid);
|
|
86
|
+
const pushName = msg.pushName;
|
|
87
|
+
const senderId = key.participant ?? msg.key.remoteJid;
|
|
88
|
+
if (pushName && senderId && !msg.key.fromMe && !senderId.endsWith("@g.us")) {
|
|
89
|
+
const existing = contacts[senderId];
|
|
90
|
+
if (existing?.notify !== pushName) {
|
|
91
|
+
contacts[senderId] = { ...existing, id: senderId, notify: pushName };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
76
96
|
function bind(ev) {
|
|
77
|
-
ev.on("messaging-history.set", ({ chats, contacts }) => {
|
|
97
|
+
ev.on("messaging-history.set", ({ chats, contacts, messages }) => {
|
|
78
98
|
for (const chat of chats)
|
|
79
99
|
upsertChat(chat);
|
|
80
100
|
for (const contact of contacts)
|
|
81
101
|
upsertContact(contact);
|
|
102
|
+
// Backfilled messages carry pushName too — was previously dropped,
|
|
103
|
+
// leaving contacts unresolved until they next messaged live.
|
|
104
|
+
if (messages?.length)
|
|
105
|
+
capturePushNames(messages);
|
|
82
106
|
});
|
|
83
107
|
ev.on("chats.upsert", (newChats) => {
|
|
84
108
|
for (const chat of newChats)
|
|
@@ -109,24 +133,9 @@ export function createStore() {
|
|
|
109
133
|
}
|
|
110
134
|
});
|
|
111
135
|
ev.on("messages.upsert", ({ messages: msgs }) => {
|
|
112
|
-
for (const msg of msgs)
|
|
136
|
+
for (const msg of msgs)
|
|
113
137
|
storeMessage(msg);
|
|
114
|
-
|
|
115
|
-
learnLid(key.participantAlt, key.participant);
|
|
116
|
-
learnLid(key.remoteJidAlt, key.remoteJid);
|
|
117
|
-
// pushName only ever arrives on a live message — contact/history
|
|
118
|
-
// sync alone won't give us a name for someone who never messaged
|
|
119
|
-
// and isn't a saved contact. Worth capturing here, since it's the
|
|
120
|
-
// one extra source we have (feeds the on-disk cache too).
|
|
121
|
-
const pushName = msg.pushName;
|
|
122
|
-
const senderId = key.participant ?? msg.key.remoteJid;
|
|
123
|
-
if (pushName && senderId && !senderId.endsWith("@g.us")) {
|
|
124
|
-
const existing = contacts[senderId];
|
|
125
|
-
if (existing?.notify !== pushName) {
|
|
126
|
-
contacts[senderId] = { ...existing, id: senderId, notify: pushName };
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
138
|
+
capturePushNames(msgs);
|
|
130
139
|
});
|
|
131
140
|
ev.on("messages.update", (updates) => {
|
|
132
141
|
for (const { key, update } of updates) {
|
|
@@ -15,6 +15,16 @@ import { enqueue } from "#download";
|
|
|
15
15
|
import { schedule, cancelPlugin } from "#kernel/scheduler.js";
|
|
16
16
|
import { emptyFolder } from "#utils/file.js";
|
|
17
17
|
import { normalizeJid, toPresenceCapable } from "../sdk/baileysSock.js";
|
|
18
|
+
// store.contacts is keyed by whatever raw JID Baileys handed the store
|
|
19
|
+
// (e.g. "...@s.whatsapp.net" for a normal DM) — never run through
|
|
20
|
+
// normalizeJid. Every lookup here works in normalized ("...@c.us") form,
|
|
21
|
+
// so a plain `store.contacts[normalizedId]` misses for any non-@lid
|
|
22
|
+
// contact. @lid contacts don't hit this: normalizeJid doesn't touch
|
|
23
|
+
// "@lid", so the raw store key already matches. Used as a fallback key
|
|
24
|
+
// alongside the normalized one wherever we read store.contacts directly.
|
|
25
|
+
function denormalizeJid(jid) {
|
|
26
|
+
return jid.replace(/@c\.us$/, "@s.whatsapp.net");
|
|
27
|
+
}
|
|
18
28
|
import { mkdirSync } from "fs";
|
|
19
29
|
import { readFile, writeFile } from "fs/promises";
|
|
20
30
|
import { readFileSync } from "fs";
|
|
@@ -322,6 +332,26 @@ const log = {
|
|
|
322
332
|
success: (...a) => logger.success(...a),
|
|
323
333
|
};
|
|
324
334
|
// ── Contact normalization ─────────────────────────────────────────────────────
|
|
335
|
+
/**
|
|
336
|
+
* Resolve the text to show after "@" in a mention.
|
|
337
|
+
*
|
|
338
|
+
* WhatsApp's tag/notify behavior (who gets pinged, the bold highlight) is
|
|
339
|
+
* driven entirely by contextInfo.mentionedJid — the visible text after "@"
|
|
340
|
+
* is cosmetic and each client fills it in independently. Web commonly
|
|
341
|
+
* resolves it from the group's synced metadata, but mobile clients only do
|
|
342
|
+
* this when the number is saved in the phone's own contacts — otherwise
|
|
343
|
+
* they show the raw digits (e.g. "@5516999999999"), which is the "só
|
|
344
|
+
* aparece @numero no celular" behavior. Using the name Baileys already
|
|
345
|
+
* knows (contact name / business verifiedName / WhatsApp "notify" push
|
|
346
|
+
* name) instead of the number sidesteps that client-side lookup and
|
|
347
|
+
* renders consistently everywhere.
|
|
348
|
+
* @param {string} jid
|
|
349
|
+
* @param {WAStoreContact} [info]
|
|
350
|
+
*/
|
|
351
|
+
function mentionDisplayName(jid, info) {
|
|
352
|
+
const number = jid.split("@")[0];
|
|
353
|
+
return info?.name ?? info?.verifiedName ?? info?.notify ?? number;
|
|
354
|
+
}
|
|
325
355
|
/**
|
|
326
356
|
* Build a normalized contact object from a JID and optional store metadata.
|
|
327
357
|
* isBusiness is resolved via sock.getBusinessProfile(jid) — it resolves to
|
|
@@ -380,7 +410,7 @@ async function normalizeContact(jid, info, botJid, sock) {
|
|
|
380
410
|
isWAAccount,
|
|
381
411
|
isUser: !jid.endsWith("@g.us"),
|
|
382
412
|
isGroup: jid.endsWith("@g.us"),
|
|
383
|
-
mention: { text: `@${
|
|
413
|
+
mention: { text: `@${mentionDisplayName(jid, info)}`, mentions: [jid] },
|
|
384
414
|
};
|
|
385
415
|
}
|
|
386
416
|
// ── Contact API ───────────────────────────────────────────────────────────────
|
|
@@ -467,8 +497,10 @@ function buildContactsApi(sock, store, botJid) {
|
|
|
467
497
|
// `undefined` silently clobber a real value already present in raw
|
|
468
498
|
// (or vice versa) — which is how a contact could resolve correctly
|
|
469
499
|
// by id yet still come back with a null pushname/name.
|
|
470
|
-
const raw = store.contacts[contactId]
|
|
471
|
-
|
|
500
|
+
const raw = store.contacts[contactId]
|
|
501
|
+
?? store.contacts[denormalizeJid(contactId)];
|
|
502
|
+
const resolvedInfo = store.contacts[resolved]
|
|
503
|
+
?? store.contacts[denormalizeJid(resolved)];
|
|
472
504
|
const info = (raw || resolvedInfo) ? {
|
|
473
505
|
id: resolved,
|
|
474
506
|
name: resolvedInfo?.name ?? raw?.name,
|
|
@@ -629,8 +661,10 @@ export function buildMessageContext(msg, sock, store, guardOptions = {}) {
|
|
|
629
661
|
*/
|
|
630
662
|
async getContact() {
|
|
631
663
|
const info = store.contacts[sender]
|
|
632
|
-
?? store.contacts[
|
|
633
|
-
|
|
664
|
+
?? store.contacts[denormalizeJid(sender)]
|
|
665
|
+
?? store.contacts[store.resolveJid(msg.key.participant ?? "")]
|
|
666
|
+
?? store.contacts[denormalizeJid(store.resolveJid(msg.key.participant ?? ""))];
|
|
667
|
+
const botJid = sock.user?.id ? jidNormalizedUser(sock.user.id) : null;
|
|
634
668
|
return normalizeContact(sender, info, botJid, sock);
|
|
635
669
|
},
|
|
636
670
|
};
|
|
@@ -730,7 +764,7 @@ function makeSender(sock, store, jid, quoted = null, { cooldown = true, jitter =
|
|
|
730
764
|
text(content, opts = {}) {
|
|
731
765
|
return new MessageHandle((async () => {
|
|
732
766
|
const quotedMsg = await resolveQuoted();
|
|
733
|
-
const sendOpts = quotedMsg ? { quoted: quotedMsg } :
|
|
767
|
+
const sendOpts = quotedMsg ? { quoted: quotedMsg } : {};
|
|
734
768
|
if (opts.linkPreview === false) {
|
|
735
769
|
sendOpts.linkPreview = false;
|
|
736
770
|
}
|
|
@@ -745,10 +779,13 @@ function makeSender(sock, store, jid, quoted = null, { cooldown = true, jitter =
|
|
|
745
779
|
image(filePath, caption = "", opts = {}) {
|
|
746
780
|
return new MessageHandle((async () => {
|
|
747
781
|
const quotedMsg = await resolveQuoted();
|
|
748
|
-
const sendOpts = quotedMsg ? { quoted: quotedMsg } :
|
|
782
|
+
const sendOpts = quotedMsg ? { quoted: quotedMsg } : {};
|
|
749
783
|
if (opts.viewOnce) {
|
|
750
784
|
sendOpts.viewOnce = true;
|
|
751
785
|
}
|
|
786
|
+
if (opts.mentions?.length) {
|
|
787
|
+
sendOpts.mentions = opts.mentions;
|
|
788
|
+
}
|
|
752
789
|
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
753
790
|
await simulateState(toPresenceCapable(sock), jid, mediaDuration(), "typing");
|
|
754
791
|
const buffer = await readFile(filePath);
|
|
@@ -758,10 +795,13 @@ function makeSender(sock, store, jid, quoted = null, { cooldown = true, jitter =
|
|
|
758
795
|
video(filePath, caption = "", opts = {}) {
|
|
759
796
|
return new MessageHandle((async () => {
|
|
760
797
|
const quotedMsg = await resolveQuoted();
|
|
761
|
-
const sendOpts = quotedMsg ? { quoted: quotedMsg } :
|
|
798
|
+
const sendOpts = quotedMsg ? { quoted: quotedMsg } : {};
|
|
762
799
|
if (opts.viewOnce) {
|
|
763
800
|
sendOpts.viewOnce = true;
|
|
764
801
|
}
|
|
802
|
+
if (opts.mentions?.length) {
|
|
803
|
+
sendOpts.mentions = opts.mentions;
|
|
804
|
+
}
|
|
765
805
|
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
766
806
|
await simulateState(toPresenceCapable(sock), jid, mediaDuration(), "typing");
|
|
767
807
|
const buffer = await readFile(filePath);
|
|
@@ -771,7 +811,7 @@ function makeSender(sock, store, jid, quoted = null, { cooldown = true, jitter =
|
|
|
771
811
|
audio(filePath, { asVoice = true, viewOnce = false } = {}) {
|
|
772
812
|
return new MessageHandle((async () => {
|
|
773
813
|
const quotedMsg = await resolveQuoted();
|
|
774
|
-
const sendOpts = quotedMsg ? { quoted: quotedMsg } :
|
|
814
|
+
const sendOpts = quotedMsg ? { quoted: quotedMsg } : {};
|
|
775
815
|
if (viewOnce) {
|
|
776
816
|
sendOpts.viewOnce = true;
|
|
777
817
|
}
|
|
@@ -837,8 +877,8 @@ function buildSendApi(sock, store, rawJid, guardOptions = {}) {
|
|
|
837
877
|
return {
|
|
838
878
|
send: {
|
|
839
879
|
text: (text, opts) => current.text(text, opts),
|
|
840
|
-
image: (filePath, caption) => current.image(filePath, caption),
|
|
841
|
-
video: (filePath, caption) => current.video(filePath, caption),
|
|
880
|
+
image: (filePath, caption, opts) => current.image(filePath, caption, opts),
|
|
881
|
+
video: (filePath, caption, opts) => current.video(filePath, caption, opts),
|
|
842
882
|
audio: (filePath, opts) => current.audio(filePath, opts),
|
|
843
883
|
sticker: (source) => current.sticker(source),
|
|
844
884
|
file: (filePath, filename) => current.file(filePath, filename),
|
|
@@ -914,7 +954,31 @@ function buildEventsApi(sock, pluginName) {
|
|
|
914
954
|
* @param {string|null} chatJid — raw JID of the current group (null in setup context)
|
|
915
955
|
*/
|
|
916
956
|
function buildAdminApi(sock, chatJid) {
|
|
917
|
-
|
|
957
|
+
/**
|
|
958
|
+
* Normalize a participant identifier to the wire JID format WhatsApp's
|
|
959
|
+
* servers require (`...@s.whatsapp.net`). `groupParticipantsUpdate()`
|
|
960
|
+
* doesn't fail per-participant for a malformed entry — the WHOLE IQ
|
|
961
|
+
* query gets rejected by the server as a "bad-request" Boom error
|
|
962
|
+
* before any per-participant status even exists. This silently broke
|
|
963
|
+
* for the two most common inputs a plugin author reaches for:
|
|
964
|
+
* - `contact.id` — this framework's own normalized `@c.us` form
|
|
965
|
+
* (see normalizeJid()/denormalizeJid()), which isn't wire-valid.
|
|
966
|
+
* - a bare phone number (with or without "+", spaces or dashes),
|
|
967
|
+
* which has no `@...` server segment at all.
|
|
968
|
+
* Already-correct JIDs (`@s.whatsapp.net`, `@lid`, `@g.us`) pass through
|
|
969
|
+
* unchanged.
|
|
970
|
+
* @param {string} id
|
|
971
|
+
*/
|
|
972
|
+
function toParticipantJid(id) {
|
|
973
|
+
const trimmed = id.trim();
|
|
974
|
+
if (/@(s\.whatsapp\.net|lid|g\.us)$/.test(trimmed))
|
|
975
|
+
return trimmed;
|
|
976
|
+
if (trimmed.endsWith("@c.us"))
|
|
977
|
+
return denormalizeJid(trimmed);
|
|
978
|
+
const digits = trimmed.replace(/\D/g, "");
|
|
979
|
+
return `${digits}@s.whatsapp.net`;
|
|
980
|
+
}
|
|
981
|
+
const norm = (v) => (Array.isArray(v) ? v : [v]).map(toParticipantJid);
|
|
918
982
|
function requireChat() {
|
|
919
983
|
if (!chatJid)
|
|
920
984
|
throw new Error("This admin operation requires a runtime group context.");
|
|
@@ -947,6 +1011,23 @@ function buildAdminApi(sock, chatJid) {
|
|
|
947
1011
|
throw new Error(`groupParticipantsUpdate("${action}") rejeitado para: ${detail}`);
|
|
948
1012
|
}
|
|
949
1013
|
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Thin wrapper around `sock.groupParticipantsUpdate()` that turns an
|
|
1016
|
+
* opaque Boom rejection (e.g. the whole IQ query bounced with
|
|
1017
|
+
* "bad-request") into an error that names the group/action/participants
|
|
1018
|
+
* involved, then still runs the per-participant status check above.
|
|
1019
|
+
*/
|
|
1020
|
+
async function runParticipantsUpdate(jid, users, action) {
|
|
1021
|
+
let results;
|
|
1022
|
+
try {
|
|
1023
|
+
results = await sock.groupParticipantsUpdate(jid, users, action);
|
|
1024
|
+
}
|
|
1025
|
+
catch (err) {
|
|
1026
|
+
throw new Error(`groupParticipantsUpdate("${action}") falhou para o grupo "${jid}" com participantes [${users.join(", ")}]: ${err.message}`);
|
|
1027
|
+
}
|
|
1028
|
+
assertParticipantsUpdateOk(action, results);
|
|
1029
|
+
return results;
|
|
1030
|
+
}
|
|
950
1031
|
function createTargetableAction(action, memberIds) {
|
|
951
1032
|
const users = norm(memberIds);
|
|
952
1033
|
const executeCurrent = async () => { requireChat(); return action(chatJid, users); };
|
|
@@ -964,34 +1045,24 @@ function buildAdminApi(sock, chatJid) {
|
|
|
964
1045
|
};
|
|
965
1046
|
}
|
|
966
1047
|
return {
|
|
967
|
-
/** @param {string|string[]} memberIds */
|
|
1048
|
+
/** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
|
|
968
1049
|
add(memberIds) {
|
|
969
|
-
return createTargetableAction(
|
|
970
|
-
const results = await sock.groupParticipantsUpdate(jid, users, "add");
|
|
971
|
-
assertParticipantsUpdateOk("add", results);
|
|
972
|
-
return results;
|
|
973
|
-
}, memberIds);
|
|
1050
|
+
return createTargetableAction((jid, users) => runParticipantsUpdate(jid, users, "add"), memberIds);
|
|
974
1051
|
},
|
|
975
|
-
/** @param {string|string[]} memberIds */
|
|
1052
|
+
/** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
|
|
976
1053
|
async kick(memberIds) {
|
|
977
1054
|
requireChat();
|
|
978
|
-
|
|
979
|
-
assertParticipantsUpdateOk("remove", results);
|
|
980
|
-
return results;
|
|
1055
|
+
return runParticipantsUpdate(chatJid, norm(memberIds), "remove");
|
|
981
1056
|
},
|
|
982
|
-
/** @param {string|string[]} memberIds */
|
|
1057
|
+
/** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
|
|
983
1058
|
async promote(memberIds) {
|
|
984
1059
|
requireChat();
|
|
985
|
-
|
|
986
|
-
assertParticipantsUpdateOk("promote", results);
|
|
987
|
-
return results;
|
|
1060
|
+
return runParticipantsUpdate(chatJid, norm(memberIds), "promote");
|
|
988
1061
|
},
|
|
989
|
-
/** @param {string|string[]} memberIds */
|
|
1062
|
+
/** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
|
|
990
1063
|
async demote(memberIds) {
|
|
991
1064
|
requireChat();
|
|
992
|
-
|
|
993
|
-
assertParticipantsUpdateOk("demote", results);
|
|
994
|
-
return results;
|
|
1065
|
+
return runParticipantsUpdate(chatJid, norm(memberIds), "demote");
|
|
995
1066
|
},
|
|
996
1067
|
/** @param {string} name */
|
|
997
1068
|
async setSubject(name) {
|
|
@@ -1268,7 +1339,7 @@ function buildPollApi(sock, store, rawJid, guardOptions, pluginName) {
|
|
|
1268
1339
|
}
|
|
1269
1340
|
// ── Base API (shared between setup and runtime) ───────────────────────────────
|
|
1270
1341
|
function buildBaseApi(sock, store, pluginRegistry, pluginName) {
|
|
1271
|
-
const botJid = sock.user?.id ?
|
|
1342
|
+
const botJid = sock.user?.id ? jidNormalizedUser(sock.user.id) : null;
|
|
1272
1343
|
if (!botJid)
|
|
1273
1344
|
logger.warn("[pluginApi] botId is null — socket may not be ready yet.");
|
|
1274
1345
|
return {
|
|
@@ -102,6 +102,7 @@ async function startBot() {
|
|
|
102
102
|
currentSock = sock;
|
|
103
103
|
currentStore = store;
|
|
104
104
|
connecting = false;
|
|
105
|
+
let pluginsReady = false;
|
|
105
106
|
// ── Normal bot mode ─────────────────────────────────────────────────────────
|
|
106
107
|
sock.ev.on("connection.update", async (update) => {
|
|
107
108
|
const { connection, lastDisconnect } = update;
|
|
@@ -111,8 +112,11 @@ async function startBot() {
|
|
|
111
112
|
logger.success(t("system.connected"));
|
|
112
113
|
logger.info(t("system.clientId", { id: CLIENT_ID }));
|
|
113
114
|
printBanner();
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
if (!pluginsReady) {
|
|
116
|
+
pluginsReady = true;
|
|
117
|
+
await loadPlugins(PLUGINS);
|
|
118
|
+
await setupPlugins(sock, store);
|
|
119
|
+
}
|
|
116
120
|
startCacheAutosave(store);
|
|
117
121
|
// buffer anti-replay / sync ghost messages
|
|
118
122
|
setTimeout(() => { state = "READY"; }, 2000);
|