@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.
@@ -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 && !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: `@${number}`, mentions: [jid] },
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
- const resolvedInfo = store.contacts[resolved];
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[store.resolveJid(msg.key.participant ?? "")];
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 } : undefined;
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
- sendOpts.linkPreview = false;
774
+ messageContent.linkPreview = null;
736
775
  }
737
776
  if (opts.mentions?.length) {
738
- sendOpts.mentions = opts.mentions;
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, { text: content }, sendOpts);
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 } : undefined;
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
- return sock.sendMessage(jid, { image: buffer, caption }, sendOpts);
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 } : undefined;
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
- return sock.sendMessage(jid, { video: buffer, caption }, sendOpts);
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 } : undefined;
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
- return sock.sendMessage(jid, { audio: buffer, mimetype: "audio/mp4", ptt: asVoice }, sendOpts);
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(async (jid, users) => {
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
- const results = await sock.groupParticipantsUpdate(chatJid, norm(memberIds), "remove");
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
- const results = await sock.groupParticipantsUpdate(chatJid, norm(memberIds), "promote");
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
- const results = await sock.groupParticipantsUpdate(chatJid, norm(memberIds), "demote");
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
- 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.1",
8
+ "version": "5.3.3",
9
9
  "license": "GPL-3.0-only",
10
10
  "private": false,
11
11
  "engines": {