@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.
@@ -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
- const key = msg.key;
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: `@${number}`, mentions: [jid] },
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
- const resolvedInfo = store.contacts[resolved];
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[store.resolveJid(msg.key.participant ?? "")];
633
- const botJid = sock.user?.id ? normalizeJid(sock.user.id) : null;
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 } : undefined;
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 } : undefined;
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 } : undefined;
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 } : undefined;
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
- const norm = (v) => Array.isArray(v) ? v : [v];
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(async (jid, users) => {
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
- const results = await sock.groupParticipantsUpdate(chatJid, norm(memberIds), "remove");
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
- const results = await sock.groupParticipantsUpdate(chatJid, norm(memberIds), "promote");
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
- const results = await sock.groupParticipantsUpdate(chatJid, norm(memberIds), "demote");
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 ? normalizeJid(sock.user.id) : null;
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
- await loadPlugins(PLUGINS);
115
- await setupPlugins(sock, store);
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);
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "SyntaxError!",
6
6
  "email": "me@stxerr.dev"
7
7
  },
8
- "version": "5.3.0",
8
+ "version": "5.3.2",
9
9
  "license": "GPL-3.0-only",
10
10
  "private": false,
11
11
  "engines": {