@manybot/manybot 5.3.3 → 5.4.1

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ![ManyBot Logo](logo.png)
4
4
 
5
- ![Node.js 24+](https://img.shields.io/badge/Node.js-20+-339933?logo=node.js&logoColor=white)
5
+ ![Node.js 24+](https://img.shields.io/badge/Node.js-24+-339933?logo=node.js&logoColor=white)
6
6
  ![npm 9+](https://img.shields.io/badge/npm-9+-CB3837?logo=npm&logoColor=white)
7
7
  ![GPL v3](https://img.shields.io/badge/License-GPL--v3-blue.svg)
8
8
  ![Linux](https://img.shields.io/badge/Linux%20%7C%20Windows-lightgrey)
@@ -46,9 +46,9 @@ Browse available plugins at **[manybot.org/plugins](https://manybot.org/plugins/
46
46
  All kinds of contributions are welcome:
47
47
 
48
48
  - **Bug reports and feature requests**: open an issue on GitHub or Codeberg
49
- - **Code**: pull requests are welcome on [GitHub](https://github.com/many-bot/manybot) or [Codeberg](https://codeberg.org/many-bot/manybot); patches by email (`devel+manybot.org`) are also accepted - subscribe to the mailing list [here](https://list.stxerr.dev).
49
+ - **Code**: pull requests are welcome on [GitHub](https://github.com/many-bot/manybot) or [Codeberg](https://codeberg.org/many-bot/manybot); patches by email (`manybot@pm.me`) are also accepted.
50
50
  - **Plugins**: submit your plugin to [manyplug-repo](https://github.com/many-bot/manyplug-repo), which has instructions on how to do it
51
- - **Anything else**: suggestions, translations, documentation fixes - reach out by email or open an issue
51
+ - **Anything else**: suggestions, translations, documentation fixes, art - reach out by email or open an issue
52
52
 
53
53
  ## License
54
54
 
@@ -412,6 +412,22 @@ async function normalizeContact(jid, info, botJid, sock) {
412
412
  mention: { text: `@${mentionDisplayName(jid)}`, mentions: [toWireJid(jid)] },
413
413
  };
414
414
  }
415
+ // ── Chats API ─────────────────────────────────────────────────────────────────
416
+ function buildChatsApi(store) {
417
+ return {
418
+ /**
419
+ * Chats currently known from the in-memory cache (populated from
420
+ * Baileys' chats.upsert / messaging-history.set events) — no network call.
421
+ * @returns {Array<{ id: string, name: string, isGroup: boolean }>}
422
+ */
423
+ all() {
424
+ return store.chats.all().map((c) => {
425
+ const id = normalizeJid(c.id);
426
+ return { id, name: c.name, isGroup: id.endsWith("@g.us") };
427
+ });
428
+ },
429
+ };
430
+ }
415
431
  // ── Contact API ───────────────────────────────────────────────────────────────
416
432
  function buildContactsApi(sock, store, botJid) {
417
433
  return {
@@ -419,6 +435,7 @@ function buildContactsApi(sock, store, botJid) {
419
435
  * Get a normalized contact object by JID.
420
436
  * @param {string} contactId
421
437
  * @param {{groupId?: string}} [opts] — when `contactId` is a raw `@lid`, this
438
+
422
439
  * always cross-checks it against Baileys' own protocol-level
423
440
  * `sock.signalRepository.lidMapping` first (populated from real Signal
424
441
  * session/identity resolution — not a heuristic, and doesn't need a
@@ -587,6 +604,15 @@ function buildContactsApi(sock, store, botJid) {
587
604
  },
588
605
  };
589
606
  }
607
+ function makeHistoryArray(entries, store) {
608
+ const arr = entries;
609
+ arr.last = (n) => makeHistoryArray(typeof n === "number" ? entries.slice(-n) : entries.slice(), store);
610
+ arr.from = (senderId) => {
611
+ const target = normalizeJid(store.resolveJid(normalizeJid(senderId)));
612
+ return makeHistoryArray(entries.filter((e) => e.sender === target), store);
613
+ };
614
+ return arr;
615
+ }
590
616
  export function buildMessageContext(msg, sock, store, guardOptions = {}) {
591
617
  const body = getMsgBody(msg);
592
618
  const prefix = CONFIG.CMD_PREFIX;
@@ -612,6 +638,8 @@ export function buildMessageContext(msg, sock, store, guardOptions = {}) {
612
638
  }
613
639
  : null;
614
640
  return {
641
+ id: msg.key.id ?? "",
642
+ timestamp: Number(msg.messageTimestamp) || 0,
615
643
  body,
616
644
  type: getMsgType(msg),
617
645
  fromMe: !!(msg.key.fromMe),
@@ -817,6 +845,28 @@ function makeSender(sock, store, jid, quoted = null, { cooldown = true, jitter =
817
845
  return sock.sendMessage(jid, messageContent, sendOpts);
818
846
  })(), sock, store, { cooldown, jitter });
819
847
  },
848
+ /**
849
+ * Send a GIF. WhatsApp has no native GIF format — this sends an mp4
850
+ * with the `gifPlayback` flag, which the client auto-loops, muted.
851
+ * `filePath` must already be mp4-encoded (no .gif input, no conversion).
852
+ */
853
+ gif(filePath, caption = "", opts = {}) {
854
+ return new MessageHandle((async () => {
855
+ const quotedMsg = await resolveQuoted();
856
+ const sendOpts = quotedMsg ? { quoted: quotedMsg } : {};
857
+ await waitForSendSlot(normJid, { cooldown, jitter });
858
+ await simulateState(toPresenceCapable(sock), jid, mediaDuration(), "typing");
859
+ const buffer = await readFile(filePath);
860
+ const messageContent = { video: buffer, caption, gifPlayback: true };
861
+ if (opts.viewOnce) {
862
+ messageContent.viewOnce = true;
863
+ }
864
+ if (opts.mentions?.length) {
865
+ messageContent.mentions = await resolveMentionJids(sock, store, jid, opts.mentions);
866
+ }
867
+ return sock.sendMessage(jid, messageContent, sendOpts);
868
+ })(), sock, store, { cooldown, jitter });
869
+ },
820
870
  audio(filePath, { asVoice = true, viewOnce = false } = {}) {
821
871
  return new MessageHandle((async () => {
822
872
  const quotedMsg = await resolveQuoted();
@@ -890,6 +940,7 @@ function buildSendApi(sock, store, rawJid, guardOptions = {}) {
890
940
  text: (text, opts) => current.text(text, opts),
891
941
  image: (filePath, caption, opts) => current.image(filePath, caption, opts),
892
942
  video: (filePath, caption, opts) => current.video(filePath, caption, opts),
943
+ gif: (filePath, caption, opts) => current.gif(filePath, caption, opts),
893
944
  audio: (filePath, opts) => current.audio(filePath, opts),
894
945
  sticker: (source) => current.sticker(source),
895
946
  file: (filePath, filename) => current.file(filePath, filename),
@@ -1406,6 +1457,7 @@ function buildBaseApi(sock, store, pluginRegistry, pluginName) {
1406
1457
  download: buildDownloadApi(),
1407
1458
  scheduler: buildSchedulerApi(pluginName),
1408
1459
  plugins: buildPluginsApi(pluginRegistry),
1460
+ chats: buildChatsApi(store),
1409
1461
  contacts: buildContactsApi(sock, store, botJid),
1410
1462
  storage: buildStorageApi(pluginName),
1411
1463
  botId: botJid,
@@ -1502,6 +1554,19 @@ export function buildApi({ msg, chat, sock, store, pluginRegistry, pluginName, g
1502
1554
  id: normJid,
1503
1555
  name: chat.name,
1504
1556
  isGroup: chat.isGroup,
1557
+ /**
1558
+ * Cached message history for this chat (oldest → newest), capped at
1559
+ * the store's per-chat limit. Supports index access (`history[10]`)
1560
+ * and two chainable filters: `.last(n)` and `.from(senderId)`.
1561
+ * @returns {WAHistoryArray}
1562
+ */
1563
+ get history() {
1564
+ const chatMsgs = store.messages.get(rawJid);
1565
+ const entries = chatMsgs
1566
+ ? [...chatMsgs.values()].map((m) => buildMessageContext(m, sock, store, { cooldown: false, jitter: false }))
1567
+ : [];
1568
+ return makeHistoryArray(entries, store);
1569
+ },
1505
1570
  /**
1506
1571
  * List of group participants.
1507
1572
  * Returns [] for non-group chats.
@@ -31,6 +31,39 @@ let connecting = false;
31
31
  let reconnectAttempts = 0;
32
32
  let cacheHydrated = false;
33
33
  let cacheSaveTimer = null;
34
+ // ── Per-chat message queue ──────────────────────────────────────────────────
35
+ // Messages from the same chat are processed one at a time (in order), but
36
+ // different chats run concurrently — a slow plugin in one chat (e.g. sticker
37
+ // generation) no longer blocks replies in every other chat.
38
+ const chatQueues = new Map();
39
+ function enqueueForChat(jid, task) {
40
+ const prev = chatQueues.get(jid) ?? Promise.resolve();
41
+ const settled = prev.catch(() => { }).then(task).catch((e) => {
42
+ const err = e instanceof Error ? e : new Error(String(e));
43
+ logger.error(`${err.message}\n${err.stack}`);
44
+ });
45
+ chatQueues.set(jid, settled);
46
+ settled.finally(() => {
47
+ if (chatQueues.get(jid) === settled)
48
+ chatQueues.delete(jid);
49
+ });
50
+ }
51
+ // Messages older than this (WhatsApp's own delivery delay — e.g. backlog
52
+ // dumped after the bot reconnects) are skipped. Checked at arrival time,
53
+ // so time spent waiting in chatQueues never counts against a message.
54
+ const MAX_MESSAGE_AGE_SECONDS = 60;
55
+ function isMessageStale(msg) {
56
+ const msgTimestamp = Number(msg.messageTimestamp);
57
+ if (!msgTimestamp)
58
+ return false;
59
+ const nowInSeconds = Math.floor(Date.now() / 1000);
60
+ const age = nowInSeconds - msgTimestamp;
61
+ if (age > MAX_MESSAGE_AGE_SECONDS) {
62
+ logger.debug(`[whatsapp] Skipping stale message (age: ${age}s, id: ${msg.key.id})`);
63
+ return true;
64
+ }
65
+ return false;
66
+ }
34
67
  const RECONNECT_BASE_MS = 1000;
35
68
  const RECONNECT_MAX_MS = 60000;
36
69
  const CACHE_SAVE_INTERVAL_MS = 5 * 60 * 1000; // 5min
@@ -156,13 +189,10 @@ async function startBot() {
156
189
  const body = getBodyQuick(m);
157
190
  if (!body && !msgHasMediaQuick(m))
158
191
  continue;
159
- try {
160
- await handleMessage(m, sock, store);
161
- }
162
- catch (e) {
163
- const err = e instanceof Error ? e : new Error(String(e));
164
- logger.error(`${err.message}\n${err.stack}`);
165
- }
192
+ if (isMessageStale(m))
193
+ continue;
194
+ const jid = normalizeJid(m.key.remoteJid ?? "");
195
+ enqueueForChat(jid, () => handleMessage(m, sock, store));
166
196
  }
167
197
  });
168
198
  }
@@ -45,14 +45,6 @@ function alreadyProcessed(id) {
45
45
  * @param {WAStore} store
46
46
  */
47
47
  export async function handleMessage(msg, sock, store) {
48
- const msgTimestamp = Number(msg.messageTimestamp);
49
- if (msgTimestamp) {
50
- const nowInSeconds = Math.floor(Date.now() / 1000);
51
- const messageAge = nowInSeconds - msgTimestamp;
52
- if (messageAge > 60) {
53
- return;
54
- }
55
- }
56
48
  const rawJid = msg.key.remoteJid ?? "";
57
49
  const jid = normalizeJid(rawJid);
58
50
  if (CHATS.length > 0 && !CHATS.includes(jid)) {
package/dist/main.js CHANGED
File without changes
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "name": "SyntaxError!",
6
6
  "email": "me@stxerr.dev"
7
7
  },
8
- "version": "5.3.3",
8
+ "version": "5.4.1",
9
9
  "license": "GPL-3.0-only",
10
10
  "private": false,
11
11
  "engines": {