@manybot/manybot 5.3.1 → 5.3.3
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 +170 -44
- 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 && !msg.key.fromMe && !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,25 @@ 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) {
|
|
352
|
+
return jid.split("@")[0];
|
|
353
|
+
}
|
|
325
354
|
/**
|
|
326
355
|
* Build a normalized contact object from a JID and optional store metadata.
|
|
327
356
|
* isBusiness is resolved via sock.getBusinessProfile(jid) — it resolves to
|
|
@@ -380,7 +409,7 @@ async function normalizeContact(jid, info, botJid, sock) {
|
|
|
380
409
|
isWAAccount,
|
|
381
410
|
isUser: !jid.endsWith("@g.us"),
|
|
382
411
|
isGroup: jid.endsWith("@g.us"),
|
|
383
|
-
mention: { text: `@${
|
|
412
|
+
mention: { text: `@${mentionDisplayName(jid)}`, mentions: [toWireJid(jid)] },
|
|
384
413
|
};
|
|
385
414
|
}
|
|
386
415
|
// ── Contact API ───────────────────────────────────────────────────────────────
|
|
@@ -467,8 +496,10 @@ function buildContactsApi(sock, store, botJid) {
|
|
|
467
496
|
// `undefined` silently clobber a real value already present in raw
|
|
468
497
|
// (or vice versa) — which is how a contact could resolve correctly
|
|
469
498
|
// by id yet still come back with a null pushname/name.
|
|
470
|
-
const raw = store.contacts[contactId]
|
|
471
|
-
|
|
499
|
+
const raw = store.contacts[contactId]
|
|
500
|
+
?? store.contacts[denormalizeJid(contactId)];
|
|
501
|
+
const resolvedInfo = store.contacts[resolved]
|
|
502
|
+
?? store.contacts[denormalizeJid(resolved)];
|
|
472
503
|
const info = (raw || resolvedInfo) ? {
|
|
473
504
|
id: resolved,
|
|
474
505
|
name: resolvedInfo?.name ?? raw?.name,
|
|
@@ -629,7 +660,9 @@ export function buildMessageContext(msg, sock, store, guardOptions = {}) {
|
|
|
629
660
|
*/
|
|
630
661
|
async getContact() {
|
|
631
662
|
const info = store.contacts[sender]
|
|
632
|
-
?? store.contacts[
|
|
663
|
+
?? store.contacts[denormalizeJid(sender)]
|
|
664
|
+
?? store.contacts[store.resolveJid(msg.key.participant ?? "")]
|
|
665
|
+
?? store.contacts[denormalizeJid(store.resolveJid(msg.key.participant ?? ""))];
|
|
633
666
|
const botJid = sock.user?.id ? jidNormalizedUser(sock.user.id) : null;
|
|
634
667
|
return normalizeContact(sender, info, botJid, sock);
|
|
635
668
|
},
|
|
@@ -730,55 +763,73 @@ function makeSender(sock, store, jid, quoted = null, { cooldown = true, jitter =
|
|
|
730
763
|
text(content, opts = {}) {
|
|
731
764
|
return new MessageHandle((async () => {
|
|
732
765
|
const quotedMsg = await resolveQuoted();
|
|
733
|
-
const sendOpts = quotedMsg ? { quoted: quotedMsg } :
|
|
766
|
+
const sendOpts = quotedMsg ? { quoted: quotedMsg } : {};
|
|
767
|
+
// mentions/linkPreview are part of the message CONTENT, not the send
|
|
768
|
+
// options — Baileys reads contextInfo.mentionedJid off the content
|
|
769
|
+
// object. Putting them on sendOpts (3rd arg) is silently ignored: the
|
|
770
|
+
// message sends fine, "@name" shows as plain text, and nobody actually
|
|
771
|
+
// gets tagged/notified.
|
|
772
|
+
const messageContent = { text: content };
|
|
734
773
|
if (opts.linkPreview === false) {
|
|
735
|
-
|
|
774
|
+
messageContent.linkPreview = null;
|
|
736
775
|
}
|
|
737
776
|
if (opts.mentions?.length) {
|
|
738
|
-
|
|
777
|
+
messageContent.mentions = await resolveMentionJids(sock, store, jid, opts.mentions);
|
|
739
778
|
}
|
|
740
779
|
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
741
780
|
await simulateState(toPresenceCapable(sock), jid, typingDuration(content), "typing");
|
|
742
|
-
return sock.sendMessage(jid,
|
|
781
|
+
return sock.sendMessage(jid, messageContent, sendOpts);
|
|
743
782
|
})(), sock, store, { cooldown, jitter });
|
|
744
783
|
},
|
|
745
784
|
image(filePath, caption = "", opts = {}) {
|
|
746
785
|
return new MessageHandle((async () => {
|
|
747
786
|
const quotedMsg = await resolveQuoted();
|
|
748
|
-
const sendOpts = quotedMsg ? { quoted: quotedMsg } :
|
|
749
|
-
if (opts.viewOnce) {
|
|
750
|
-
sendOpts.viewOnce = true;
|
|
751
|
-
}
|
|
787
|
+
const sendOpts = quotedMsg ? { quoted: quotedMsg } : {};
|
|
752
788
|
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
753
789
|
await simulateState(toPresenceCapable(sock), jid, mediaDuration(), "typing");
|
|
754
790
|
const buffer = await readFile(filePath);
|
|
755
|
-
|
|
791
|
+
// viewOnce/mentions are part of the message CONTENT — see note above.
|
|
792
|
+
const messageContent = { image: buffer, caption };
|
|
793
|
+
if (opts.viewOnce) {
|
|
794
|
+
messageContent.viewOnce = true;
|
|
795
|
+
}
|
|
796
|
+
if (opts.mentions?.length) {
|
|
797
|
+
messageContent.mentions = await resolveMentionJids(sock, store, jid, opts.mentions);
|
|
798
|
+
}
|
|
799
|
+
return sock.sendMessage(jid, messageContent, sendOpts);
|
|
756
800
|
})(), sock, store, { cooldown, jitter });
|
|
757
801
|
},
|
|
758
802
|
video(filePath, caption = "", opts = {}) {
|
|
759
803
|
return new MessageHandle((async () => {
|
|
760
804
|
const quotedMsg = await resolveQuoted();
|
|
761
|
-
const sendOpts = quotedMsg ? { quoted: quotedMsg } :
|
|
762
|
-
if (opts.viewOnce) {
|
|
763
|
-
sendOpts.viewOnce = true;
|
|
764
|
-
}
|
|
805
|
+
const sendOpts = quotedMsg ? { quoted: quotedMsg } : {};
|
|
765
806
|
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
766
807
|
await simulateState(toPresenceCapable(sock), jid, mediaDuration(), "typing");
|
|
767
808
|
const buffer = await readFile(filePath);
|
|
768
|
-
|
|
809
|
+
// viewOnce/mentions are part of the message CONTENT — see note above.
|
|
810
|
+
const messageContent = { video: buffer, caption };
|
|
811
|
+
if (opts.viewOnce) {
|
|
812
|
+
messageContent.viewOnce = true;
|
|
813
|
+
}
|
|
814
|
+
if (opts.mentions?.length) {
|
|
815
|
+
messageContent.mentions = await resolveMentionJids(sock, store, jid, opts.mentions);
|
|
816
|
+
}
|
|
817
|
+
return sock.sendMessage(jid, messageContent, sendOpts);
|
|
769
818
|
})(), sock, store, { cooldown, jitter });
|
|
770
819
|
},
|
|
771
820
|
audio(filePath, { asVoice = true, viewOnce = false } = {}) {
|
|
772
821
|
return new MessageHandle((async () => {
|
|
773
822
|
const quotedMsg = await resolveQuoted();
|
|
774
|
-
const sendOpts = quotedMsg ? { quoted: quotedMsg } :
|
|
775
|
-
if (viewOnce) {
|
|
776
|
-
sendOpts.viewOnce = true;
|
|
777
|
-
}
|
|
823
|
+
const sendOpts = quotedMsg ? { quoted: quotedMsg } : {};
|
|
778
824
|
await waitForSendSlot(normJid, { cooldown, jitter });
|
|
779
825
|
await simulateState(toPresenceCapable(sock), jid, mediaDuration(), "recording");
|
|
780
826
|
const buffer = await readFile(filePath);
|
|
781
|
-
|
|
827
|
+
// viewOnce is part of the message CONTENT — see note above.
|
|
828
|
+
const messageContent = { audio: buffer, mimetype: "audio/mp4", ptt: asVoice };
|
|
829
|
+
if (viewOnce) {
|
|
830
|
+
messageContent.viewOnce = true;
|
|
831
|
+
}
|
|
832
|
+
return sock.sendMessage(jid, messageContent, sendOpts);
|
|
782
833
|
})(), sock, store, { cooldown, jitter });
|
|
783
834
|
},
|
|
784
835
|
sticker(source) {
|
|
@@ -837,8 +888,8 @@ function buildSendApi(sock, store, rawJid, guardOptions = {}) {
|
|
|
837
888
|
return {
|
|
838
889
|
send: {
|
|
839
890
|
text: (text, opts) => current.text(text, opts),
|
|
840
|
-
image: (filePath, caption) => current.image(filePath, caption),
|
|
841
|
-
video: (filePath, caption) => current.video(filePath, caption),
|
|
891
|
+
image: (filePath, caption, opts) => current.image(filePath, caption, opts),
|
|
892
|
+
video: (filePath, caption, opts) => current.video(filePath, caption, opts),
|
|
842
893
|
audio: (filePath, opts) => current.audio(filePath, opts),
|
|
843
894
|
sticker: (source) => current.sticker(source),
|
|
844
895
|
file: (filePath, filename) => current.file(filePath, filename),
|
|
@@ -913,8 +964,76 @@ function buildEventsApi(sock, pluginName) {
|
|
|
913
964
|
* @param {WASocket} sock
|
|
914
965
|
* @param {string|null} chatJid — raw JID of the current group (null in setup context)
|
|
915
966
|
*/
|
|
967
|
+
/**
|
|
968
|
+
* Normalize any identifier to the wire JID format WhatsApp's servers and
|
|
969
|
+
* protocol actually expect (`...@s.whatsapp.net`). Used anywhere a JID is
|
|
970
|
+
* handed straight to Baileys — `groupParticipantsUpdate()` and message
|
|
971
|
+
* `mentions` — both of which silently misbehave otherwise: a malformed
|
|
972
|
+
* group-update JID gets the whole IQ query rejected ("bad-request"), and
|
|
973
|
+
* a malformed mentions entry just doesn't tag/notify anyone (the message
|
|
974
|
+
* still sends fine, so it looks like nothing is wrong until you check).
|
|
975
|
+
* Handles this framework's own normalized `@c.us` form (`contact.id`,
|
|
976
|
+
* `getMsgSender()`'s output — see normalizeJid()/denormalizeJid()), a bare
|
|
977
|
+
* phone number (with or without "+", spaces or dashes), and already-valid
|
|
978
|
+
* wire JIDs (`@s.whatsapp.net`, `@lid`, `@g.us`), which pass through
|
|
979
|
+
* unchanged.
|
|
980
|
+
* @param {string} id
|
|
981
|
+
*/
|
|
982
|
+
function toWireJid(id) {
|
|
983
|
+
const trimmed = id.trim();
|
|
984
|
+
if (/@(s\.whatsapp\.net|lid|g\.us)$/.test(trimmed))
|
|
985
|
+
return trimmed;
|
|
986
|
+
if (trimmed.endsWith("@c.us"))
|
|
987
|
+
return denormalizeJid(trimmed);
|
|
988
|
+
const digits = trimmed.replace(/\D/g, "");
|
|
989
|
+
return `${digits}@s.whatsapp.net`;
|
|
990
|
+
}
|
|
991
|
+
/**
|
|
992
|
+
* Resolve mentions to the exact JID form the destination group uses for
|
|
993
|
+
* that participant — not whatever store.resolveJid() happens to have
|
|
994
|
+
* mapped it to. A group can address a member via @lid even when we've
|
|
995
|
+
* separately learned their phone number; WhatsApp only tags/notifies a
|
|
996
|
+
* mention when the JID matches the group's own addressing for that
|
|
997
|
+
* member. DMs have no participant list to check against, so just wire-
|
|
998
|
+
* normalize as before (mentions aren't a real thing in DMs anyway).
|
|
999
|
+
*/
|
|
1000
|
+
async function resolveMentionJids(sock, store, jid, mentions) {
|
|
1001
|
+
const result = [];
|
|
1002
|
+
for (const m of mentions) {
|
|
1003
|
+
const wire = toWireJid(m);
|
|
1004
|
+
if (wire)
|
|
1005
|
+
result.push(wire);
|
|
1006
|
+
const resolved = store.resolveJid(m);
|
|
1007
|
+
if (resolved && resolved !== m) {
|
|
1008
|
+
const resolvedWire = toWireJid(resolved);
|
|
1009
|
+
if (resolvedWire)
|
|
1010
|
+
result.push(resolvedWire);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
if (!jid.endsWith("@g.us")) {
|
|
1014
|
+
return Array.from(new Set(result.filter(Boolean)));
|
|
1015
|
+
}
|
|
1016
|
+
let meta;
|
|
1017
|
+
try {
|
|
1018
|
+
meta = await getGroupMetadataCached(sock, jid);
|
|
1019
|
+
}
|
|
1020
|
+
catch {
|
|
1021
|
+
return Array.from(new Set(result.filter(Boolean)));
|
|
1022
|
+
}
|
|
1023
|
+
for (const m of mentions) {
|
|
1024
|
+
const wire = toWireJid(m);
|
|
1025
|
+
const resolved = normalizeJid(store.resolveJid(m));
|
|
1026
|
+
const match = meta.participants.find(p => toWireJid(p.id) === wire || normalizeJid(store.resolveJid(p.id)) === resolved);
|
|
1027
|
+
if (match) {
|
|
1028
|
+
const matchWire = toWireJid(match.id);
|
|
1029
|
+
if (matchWire)
|
|
1030
|
+
result.push(matchWire);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return Array.from(new Set(result.filter(Boolean)));
|
|
1034
|
+
}
|
|
916
1035
|
function buildAdminApi(sock, chatJid) {
|
|
917
|
-
const norm = (v) => Array.isArray(v) ? v : [v];
|
|
1036
|
+
const norm = (v) => (Array.isArray(v) ? v : [v]).map(toWireJid);
|
|
918
1037
|
function requireChat() {
|
|
919
1038
|
if (!chatJid)
|
|
920
1039
|
throw new Error("This admin operation requires a runtime group context.");
|
|
@@ -947,6 +1066,23 @@ function buildAdminApi(sock, chatJid) {
|
|
|
947
1066
|
throw new Error(`groupParticipantsUpdate("${action}") rejeitado para: ${detail}`);
|
|
948
1067
|
}
|
|
949
1068
|
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Thin wrapper around `sock.groupParticipantsUpdate()` that turns an
|
|
1071
|
+
* opaque Boom rejection (e.g. the whole IQ query bounced with
|
|
1072
|
+
* "bad-request") into an error that names the group/action/participants
|
|
1073
|
+
* involved, then still runs the per-participant status check above.
|
|
1074
|
+
*/
|
|
1075
|
+
async function runParticipantsUpdate(jid, users, action) {
|
|
1076
|
+
let results;
|
|
1077
|
+
try {
|
|
1078
|
+
results = await sock.groupParticipantsUpdate(jid, users, action);
|
|
1079
|
+
}
|
|
1080
|
+
catch (err) {
|
|
1081
|
+
throw new Error(`groupParticipantsUpdate("${action}") falhou para o grupo "${jid}" com participantes [${users.join(", ")}]: ${err.message}`);
|
|
1082
|
+
}
|
|
1083
|
+
assertParticipantsUpdateOk(action, results);
|
|
1084
|
+
return results;
|
|
1085
|
+
}
|
|
950
1086
|
function createTargetableAction(action, memberIds) {
|
|
951
1087
|
const users = norm(memberIds);
|
|
952
1088
|
const executeCurrent = async () => { requireChat(); return action(chatJid, users); };
|
|
@@ -964,34 +1100,24 @@ function buildAdminApi(sock, chatJid) {
|
|
|
964
1100
|
};
|
|
965
1101
|
}
|
|
966
1102
|
return {
|
|
967
|
-
/** @param {string|string[]} memberIds */
|
|
1103
|
+
/** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
|
|
968
1104
|
add(memberIds) {
|
|
969
|
-
return createTargetableAction(
|
|
970
|
-
const results = await sock.groupParticipantsUpdate(jid, users, "add");
|
|
971
|
-
assertParticipantsUpdateOk("add", results);
|
|
972
|
-
return results;
|
|
973
|
-
}, memberIds);
|
|
1105
|
+
return createTargetableAction((jid, users) => runParticipantsUpdate(jid, users, "add"), memberIds);
|
|
974
1106
|
},
|
|
975
|
-
/** @param {string|string[]} memberIds */
|
|
1107
|
+
/** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
|
|
976
1108
|
async kick(memberIds) {
|
|
977
1109
|
requireChat();
|
|
978
|
-
|
|
979
|
-
assertParticipantsUpdateOk("remove", results);
|
|
980
|
-
return results;
|
|
1110
|
+
return runParticipantsUpdate(chatJid, norm(memberIds), "remove");
|
|
981
1111
|
},
|
|
982
|
-
/** @param {string|string[]} memberIds */
|
|
1112
|
+
/** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
|
|
983
1113
|
async promote(memberIds) {
|
|
984
1114
|
requireChat();
|
|
985
|
-
|
|
986
|
-
assertParticipantsUpdateOk("promote", results);
|
|
987
|
-
return results;
|
|
1115
|
+
return runParticipantsUpdate(chatJid, norm(memberIds), "promote");
|
|
988
1116
|
},
|
|
989
|
-
/** @param {string|string[]} memberIds */
|
|
1117
|
+
/** @param {string|string[]} memberIds — JID (@s.whatsapp.net/@lid), this framework's @c.us form, or a bare phone number */
|
|
990
1118
|
async demote(memberIds) {
|
|
991
1119
|
requireChat();
|
|
992
|
-
|
|
993
|
-
assertParticipantsUpdateOk("demote", results);
|
|
994
|
-
return results;
|
|
1120
|
+
return runParticipantsUpdate(chatJid, norm(memberIds), "demote");
|
|
995
1121
|
},
|
|
996
1122
|
/** @param {string} name */
|
|
997
1123
|
async setSubject(name) {
|
|
@@ -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);
|