@manybot/manybot 5.6.0 → 5.7.0
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/drivers/baileys/adapter.js +294 -73
- package/dist/drivers/baileys/api/index.js +176 -141
- package/dist/drivers/baileys/index.js +36 -2
- package/dist/drivers/baileys/sdk/baileysSock.js +1 -1
- package/dist/drivers/whatsmeow/client.js +54 -5
- package/dist/drivers/whatsmeow/installer.js +20 -4
- package/dist/drivers/whatsmeow/supervisor.js +24 -5
- package/dist/kernel/sendFallbackGuard.js +15 -5
- package/dist/locales/en.json +3 -2
- package/dist/locales/es.json +3 -2
- package/dist/locales/pt.json +3 -2
- package/dist/main.js +23 -0
- package/package.json +2 -2
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* The ctx surface area is preserved so existing plugins stay compatible.
|
|
10
10
|
*/
|
|
11
11
|
import { toBotMessage } from "#drivers/baileys/index.js";
|
|
12
|
+
import { decodeContent } from "#drivers/baileys/adapter.js";
|
|
12
13
|
import { logger } from "#logger";
|
|
13
14
|
import { t, createPluginT, reloadTranslations, getCurrentLang } from "#i18n";
|
|
14
15
|
import { CONFIG, CONFIG_DIR } from "#config";
|
|
@@ -27,7 +28,7 @@ import { waitForSendSlot, simulateState, typingDuration, mediaDuration, waitForE
|
|
|
27
28
|
import { sendWithFallback } from "#kernel/sendFallbackGuard.js";
|
|
28
29
|
import { buildSettingsApi } from "#settingsdb";
|
|
29
30
|
import WebP from "node-webpmux";
|
|
30
|
-
import {
|
|
31
|
+
import { jidNormalizedUser, } from "@whiskeysockets/baileys";
|
|
31
32
|
// ── Raw-Baileys escape hatch ─────────────────────────────────────────────────
|
|
32
33
|
//
|
|
33
34
|
// This whole file is the Baileys driver's plugin-context builder, so the
|
|
@@ -222,11 +223,11 @@ function bindGroupMetaInvalidation(contract) {
|
|
|
222
223
|
if (groupMetaInvalidationBound)
|
|
223
224
|
return;
|
|
224
225
|
groupMetaInvalidationBound = true;
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
for (const u of updates)
|
|
226
|
+
contract.on("group-participants.update", (u) => {
|
|
227
|
+
groupMetaCache.delete(u.id);
|
|
228
|
+
});
|
|
229
|
+
contract.on("groups.update", (p) => {
|
|
230
|
+
for (const u of p.updates)
|
|
230
231
|
if (u.id)
|
|
231
232
|
groupMetaCache.delete(u.id);
|
|
232
233
|
});
|
|
@@ -731,22 +732,53 @@ export function buildMessageContext(msg, contract, store, guardOptions = {}) {
|
|
|
731
732
|
const jitter = guardOptions.jitter ?? true;
|
|
732
733
|
const contextInfo = getContextInfo(msg);
|
|
733
734
|
// Build a synthetic quoted BotMessage when the original envelope carries
|
|
734
|
-
// a quotedMessage (the adapter pre-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
735
|
+
// a quotedMessage (the adapter pre-decodes the full IContextInfo into
|
|
736
|
+
// msg._raw.contextInfo). The synthetic uses the same decodeContent
|
|
737
|
+
// helper as toBotMessage so type/body/mimetype reflect what's actually
|
|
738
|
+
// in the quoted payload — without this, msgHasMedia()/downloadMedia()
|
|
739
|
+
// on the result of getReply() always reported type=other / no media,
|
|
740
|
+
// even when the quoted message was an image/video/document/etc.
|
|
741
|
+
//
|
|
742
|
+
// We carry the same _raw.contextInfo on the synthetic so a recursive
|
|
743
|
+
// getReply().getReply() keeps working (the inner call re-reads
|
|
744
|
+
// getContextInfo off _raw.contextInfo).
|
|
745
|
+
const quotedRaw = contextInfo?.quotedMessage
|
|
746
|
+
? (() => {
|
|
747
|
+
const decoded = decodeContent(contextInfo.quotedMessage);
|
|
748
|
+
return {
|
|
749
|
+
id: contextInfo.stanzaId ?? "",
|
|
750
|
+
chatId: msg.chatId,
|
|
751
|
+
fromMe: false,
|
|
752
|
+
type: decoded.type,
|
|
753
|
+
contentHash: "",
|
|
754
|
+
timestamp: 0,
|
|
755
|
+
body: decoded.body,
|
|
756
|
+
mimetype: decoded.mimetype,
|
|
757
|
+
_raw: {
|
|
758
|
+
contextInfo: {
|
|
759
|
+
stanzaId: contextInfo.stanzaId,
|
|
760
|
+
participant: contextInfo.participant,
|
|
761
|
+
mentionedJid: contextInfo.mentionedJid,
|
|
762
|
+
quotedMessage: contextInfo.quotedMessage,
|
|
763
|
+
},
|
|
764
|
+
},
|
|
765
|
+
};
|
|
766
|
+
})()
|
|
767
|
+
: msg.quotedKey
|
|
768
|
+
// No embedded quotedMessage (older envelopes, evicted from store, or
|
|
769
|
+
// the quoted message pre-dates contextInfo-quoting). Fall back to a
|
|
770
|
+
// key-only synthetic so hasReply()/getReply() still work, but
|
|
771
|
+
// hasMedia/downloadMedia on the result will degrade gracefully
|
|
772
|
+
// (type=other, mimetype=undefined).
|
|
773
|
+
? {
|
|
774
|
+
id: msg.quotedKey.id ?? "",
|
|
775
|
+
chatId: msg.chatId,
|
|
776
|
+
fromMe: false,
|
|
777
|
+
type: "other",
|
|
778
|
+
contentHash: "",
|
|
779
|
+
timestamp: 0,
|
|
780
|
+
}
|
|
781
|
+
: null;
|
|
750
782
|
return {
|
|
751
783
|
id: msg.id,
|
|
752
784
|
timestamp: msg.timestamp || 0,
|
|
@@ -1289,20 +1321,58 @@ function buildSetupSendApi(contract, store) {
|
|
|
1289
1321
|
},
|
|
1290
1322
|
};
|
|
1291
1323
|
}
|
|
1292
|
-
|
|
1324
|
+
const WA_EVENT_NAMES = new Set([
|
|
1325
|
+
"messages.upsert",
|
|
1326
|
+
"messages.update",
|
|
1327
|
+
"messages.delete",
|
|
1328
|
+
"messaging-history.set",
|
|
1329
|
+
"chats.upsert",
|
|
1330
|
+
"chats.update",
|
|
1331
|
+
"chats.delete",
|
|
1332
|
+
"contacts.upsert",
|
|
1333
|
+
"contacts.update",
|
|
1334
|
+
"group-participants.update",
|
|
1335
|
+
"groups.upsert",
|
|
1336
|
+
"groups.update",
|
|
1337
|
+
"group.join-request",
|
|
1338
|
+
"blocklist.set",
|
|
1339
|
+
"blocklist.update",
|
|
1340
|
+
"connection.update",
|
|
1341
|
+
]);
|
|
1342
|
+
function assertSupportedEvent(event) {
|
|
1343
|
+
if (!WA_EVENT_NAMES.has(event)) {
|
|
1344
|
+
throw new Error(`[events] unsupported event "${event}". Supported: ${[...WA_EVENT_NAMES].join(", ")}. ` +
|
|
1345
|
+
`If you need this event, file an issue — adding events to WaEventName is a contract change.`);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1293
1348
|
const listenerRegistry = new Map();
|
|
1294
|
-
export function cleanupPluginEvents(pluginName,
|
|
1295
|
-
const sock = rawSocketOf(contract);
|
|
1349
|
+
export function cleanupPluginEvents(pluginName, _contract) {
|
|
1296
1350
|
const list = listenerRegistry.get(pluginName);
|
|
1297
1351
|
if (list) {
|
|
1298
|
-
for (const
|
|
1352
|
+
for (const ref of list) {
|
|
1299
1353
|
try {
|
|
1300
|
-
|
|
1354
|
+
ref.detach();
|
|
1301
1355
|
}
|
|
1302
1356
|
catch { }
|
|
1303
1357
|
}
|
|
1304
1358
|
listenerRegistry.delete(pluginName);
|
|
1305
1359
|
}
|
|
1360
|
+
// The poll-vote subscription is created in `buildPollApi` by calling
|
|
1361
|
+
// `contract.on(...)` directly, so it lives outside `listenerRegistry`.
|
|
1362
|
+
// Detach it here so the listener doesn't outlive the plugin (and a
|
|
1363
|
+
// reloaded plugin can re-subscribe — `buildPollApi` gates on the
|
|
1364
|
+
// `boundPollPlugins` map).
|
|
1365
|
+
const pollDetach = boundPollPlugins.get(pluginName);
|
|
1366
|
+
if (pollDetach) {
|
|
1367
|
+
try {
|
|
1368
|
+
pollDetach();
|
|
1369
|
+
}
|
|
1370
|
+
catch { }
|
|
1371
|
+
boundPollPlugins.delete(pluginName);
|
|
1372
|
+
}
|
|
1373
|
+
// Drop the per-plugin poll registry so reloading a plugin doesn't see
|
|
1374
|
+
// stale PollHandles from a prior instance.
|
|
1375
|
+
pollRegistry.delete(pluginName);
|
|
1306
1376
|
cancelPlugin(pluginName);
|
|
1307
1377
|
}
|
|
1308
1378
|
/**
|
|
@@ -1310,21 +1380,26 @@ export function cleanupPluginEvents(pluginName, contract) {
|
|
|
1310
1380
|
* @param {string} pluginName
|
|
1311
1381
|
*/
|
|
1312
1382
|
function buildEventsApi(contract, pluginName) {
|
|
1313
|
-
const sock = rawSocketOf(contract);
|
|
1314
1383
|
return {
|
|
1315
1384
|
on(event, handler) {
|
|
1316
|
-
|
|
1385
|
+
assertSupportedEvent(event);
|
|
1386
|
+
const wrapped = (payload) => handler(payload);
|
|
1387
|
+
const detach = contract.on(event, wrapped);
|
|
1317
1388
|
if (!listenerRegistry.has(pluginName))
|
|
1318
1389
|
listenerRegistry.set(pluginName, new Set());
|
|
1319
|
-
const ref = { event, handler };
|
|
1390
|
+
const ref = { event, handler: wrapped, detach };
|
|
1320
1391
|
listenerRegistry.get(pluginName).add(ref);
|
|
1321
1392
|
return () => {
|
|
1322
|
-
|
|
1393
|
+
try {
|
|
1394
|
+
detach();
|
|
1395
|
+
}
|
|
1396
|
+
catch { }
|
|
1323
1397
|
listenerRegistry.get(pluginName)?.delete(ref);
|
|
1324
1398
|
};
|
|
1325
1399
|
},
|
|
1326
1400
|
once(event) {
|
|
1327
|
-
|
|
1401
|
+
assertSupportedEvent(event);
|
|
1402
|
+
return new Promise((resolve) => {
|
|
1328
1403
|
const off = this.on(event, (data) => { off(); resolve(data); });
|
|
1329
1404
|
});
|
|
1330
1405
|
},
|
|
@@ -1332,8 +1407,12 @@ function buildEventsApi(contract, pluginName) {
|
|
|
1332
1407
|
const list = listenerRegistry.get(pluginName);
|
|
1333
1408
|
if (!list)
|
|
1334
1409
|
return;
|
|
1335
|
-
for (const
|
|
1336
|
-
|
|
1410
|
+
for (const ref of list) {
|
|
1411
|
+
try {
|
|
1412
|
+
ref.detach();
|
|
1413
|
+
}
|
|
1414
|
+
catch { }
|
|
1415
|
+
}
|
|
1337
1416
|
listenerRegistry.delete(pluginName);
|
|
1338
1417
|
},
|
|
1339
1418
|
};
|
|
@@ -1531,11 +1610,15 @@ function buildMeApi(contract) {
|
|
|
1531
1610
|
}
|
|
1532
1611
|
// ── Poll API ──────────────────────────────────────────────────────────────────
|
|
1533
1612
|
const pollRegistry = new Map();
|
|
1534
|
-
//
|
|
1535
|
-
//
|
|
1536
|
-
//
|
|
1537
|
-
//
|
|
1538
|
-
|
|
1613
|
+
// Per-process map of plugin names whose poll-vote subscription is bound,
|
|
1614
|
+
// to its unsubscribe handle. The contract handles rebinding to a fresh
|
|
1615
|
+
// socket on reconnect (its `on()` returns an unsubscribe that drops
|
|
1616
|
+
// cleanly on the dead socket's fan-out once it's torn down), so a simple
|
|
1617
|
+
// per-process map is enough — no per-sock WeakMap needed anymore.
|
|
1618
|
+
// `cleanupPluginEvents` MUST call the stored detach handle on unload so
|
|
1619
|
+
// the listener doesn't outlive the plugin and so a reloaded plugin can
|
|
1620
|
+
// re-subscribe.
|
|
1621
|
+
const boundPollPlugins = new Map();
|
|
1539
1622
|
/**
|
|
1540
1623
|
* Tracks votes for an active poll.
|
|
1541
1624
|
* Obtained via ctx.poll.create().
|
|
@@ -1606,59 +1689,29 @@ function buildPollApi(contract, store, rawJid, guardOptions, pluginName) {
|
|
|
1606
1689
|
pollRegistry.set(pluginName, new Map());
|
|
1607
1690
|
const registry = pollRegistry.get(pluginName);
|
|
1608
1691
|
// Keyed by creationId -> (voterKey -> latest vote entry). WhatsApp resends
|
|
1609
|
-
// the *entire current selection* on every tap (not a diff), and
|
|
1610
|
-
//
|
|
1611
|
-
// with no dedup — so we must keep only the latest entry per
|
|
1612
|
-
// or retracted/changed votes keep counting alongside
|
|
1692
|
+
// the *entire current selection* on every tap (not a diff), and the
|
|
1693
|
+
// aggregatePollVotes() contract method replays whatever pollUpdates you
|
|
1694
|
+
// give it with no dedup — so we must keep only the latest entry per
|
|
1695
|
+
// voter ourselves, or retracted/changed votes keep counting alongside
|
|
1696
|
+
// the new one.
|
|
1613
1697
|
const pollVotesByCreationId = new Map();
|
|
1614
|
-
// Poll decryption
|
|
1615
|
-
// messages.upsert
|
|
1616
|
-
// contract
|
|
1617
|
-
//
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
boundPlugins.add(pluginName);
|
|
1626
|
-
const meId = sock.user?.id ? jidNormalizedUser(sock.user.id) : "me";
|
|
1627
|
-
// WhatsApp doesn't consistently use the same JID shape (LID vs PN) for
|
|
1628
|
-
// pollCreatorJid/voterJid when deriving the poll-vote decryption key —
|
|
1629
|
-
// it depends on addressingMode, 1:1 vs group, and which side sent last.
|
|
1630
|
-
// Trying to compute "the" correct JID up front (as the old resolveAuthor
|
|
1631
|
-
// did, always preferring participantPn) causes AES-GCM auth failures
|
|
1632
|
-
// whenever WhatsApp actually used the LID for that message. Instead,
|
|
1633
|
-
// gather every plausible JID for each side and brute-force combinations
|
|
1634
|
-
// until one decrypts successfully — see
|
|
1635
|
-
// https://github.com/WhiskeySockets/Baileys/issues/2342 and #1678.
|
|
1636
|
-
function jidCandidates(key) {
|
|
1637
|
-
const cands = [];
|
|
1638
|
-
if (key.fromMe) {
|
|
1639
|
-
const selfLid = sock.user?.lid;
|
|
1640
|
-
if (selfLid)
|
|
1641
|
-
cands.push(jidNormalizedUser(selfLid));
|
|
1642
|
-
if (sock.user?.id)
|
|
1643
|
-
cands.push(jidNormalizedUser(sock.user.id));
|
|
1644
|
-
}
|
|
1645
|
-
else {
|
|
1646
|
-
const rawParticipant = key.participant ?? key.remoteJid;
|
|
1647
|
-
if (rawParticipant)
|
|
1648
|
-
cands.push(jidNormalizedUser(rawParticipant));
|
|
1649
|
-
if (key.participantPn)
|
|
1650
|
-
cands.push(jidNormalizedUser(key.participantPn));
|
|
1651
|
-
if (rawParticipant?.endsWith("@lid")) {
|
|
1652
|
-
const resolved = store.resolveJid(rawParticipant);
|
|
1653
|
-
if (resolved && resolved !== rawParticipant)
|
|
1654
|
-
cands.push(jidNormalizedUser(resolved));
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
return Array.from(new Set(cands.filter(Boolean)));
|
|
1658
|
-
}
|
|
1659
|
-
sock.ev.on("messages.upsert", async ({ messages: msgs }) => {
|
|
1698
|
+
// Poll decryption is Baileys-specific. Both the subscription
|
|
1699
|
+
// (`messages.upsert`) and the decryption/aggregation go through the
|
|
1700
|
+
// contract — `buildPollApi` doesn't touch the raw socket anymore. The
|
|
1701
|
+
// contract's optional methods are only implemented by the Baileys
|
|
1702
|
+
// adapter; drivers that don't have poll decryption leave them off the
|
|
1703
|
+
// contract and we silently no-op here (a bot on a non-Baileys driver
|
|
1704
|
+
// simply can't track poll votes).
|
|
1705
|
+
if (!boundPollPlugins.has(pluginName) &&
|
|
1706
|
+
typeof contract.decryptPollVote === "function" &&
|
|
1707
|
+
typeof contract.aggregatePollVotes === "function") {
|
|
1708
|
+
const detach = contract.on("messages.upsert", async ({ messages: msgs }) => {
|
|
1660
1709
|
for (const msg of msgs) {
|
|
1661
|
-
|
|
1710
|
+
// Filter poll-update messages by reading the raw envelope from
|
|
1711
|
+
// the store (the neutral `BotMessage` doesn't carry the
|
|
1712
|
+
// `pollUpdateMessage` field — it's a Baileys-proto detail).
|
|
1713
|
+
const raw = store.messages.get(msg.chatId)?.get(msg.id);
|
|
1714
|
+
const pum = raw?.message?.pollUpdateMessage;
|
|
1662
1715
|
if (!pum)
|
|
1663
1716
|
continue;
|
|
1664
1717
|
const creationKey = pum.pollCreationMessageKey;
|
|
@@ -1671,44 +1724,44 @@ function buildPollApi(contract, store, rawJid, guardOptions, pluginName) {
|
|
|
1671
1724
|
if (!storeMsg || !pollEncKeyRaw || !pum.vote)
|
|
1672
1725
|
continue;
|
|
1673
1726
|
try {
|
|
1674
|
-
const
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
catch {
|
|
1692
|
-
// try next JID combination
|
|
1693
|
-
}
|
|
1694
|
-
}
|
|
1695
|
-
if (decryptedVote)
|
|
1696
|
-
break;
|
|
1697
|
-
}
|
|
1698
|
-
if (!decryptedVote) {
|
|
1699
|
-
throw new Error(`all JID combinations failed (creator=${JSON.stringify(creatorCandidates)}, voter=${JSON.stringify(voterCandidates)})`);
|
|
1727
|
+
const decrypted = await contract.decryptPollVote({
|
|
1728
|
+
voteKey: {
|
|
1729
|
+
id: msg.id,
|
|
1730
|
+
remoteJid: msg.chatId,
|
|
1731
|
+
fromMe: msg.fromMe,
|
|
1732
|
+
participant: msg.participantAlt ?? msg.fromLid ?? msg.fromPn ?? null,
|
|
1733
|
+
},
|
|
1734
|
+
pollKey: {
|
|
1735
|
+
id: creationId,
|
|
1736
|
+
remoteJid: creationKey?.remoteJid ?? null,
|
|
1737
|
+
fromMe: null,
|
|
1738
|
+
participant: creationKey?.participant ?? null,
|
|
1739
|
+
},
|
|
1740
|
+
pollEncKey: pollEncKeyRaw,
|
|
1741
|
+
});
|
|
1742
|
+
if (!decrypted) {
|
|
1743
|
+
throw new Error("decryptPollVote returned null — JID candidate exhaustion or stale enc key");
|
|
1700
1744
|
}
|
|
1701
|
-
const voterKey = msg.
|
|
1702
|
-
?
|
|
1703
|
-
: jidNormalizedUser(msg.
|
|
1745
|
+
const voterKey = msg.fromMe
|
|
1746
|
+
? (contract.me().id ?? "me")
|
|
1747
|
+
: jidNormalizedUser(msg.participantAlt ?? msg.fromLid ?? msg.chatId);
|
|
1704
1748
|
const votesByVoter = pollVotesByCreationId.get(creationId) ?? new Map();
|
|
1705
1749
|
votesByVoter.set(voterKey, {
|
|
1706
|
-
pollUpdateMessageKey: msg.
|
|
1707
|
-
vote:
|
|
1750
|
+
pollUpdateMessageKey: { id: msg.id, remoteJid: msg.chatId, fromMe: msg.fromMe },
|
|
1751
|
+
vote: decrypted,
|
|
1708
1752
|
senderTimestampMs: pum.senderTimestampMs,
|
|
1709
1753
|
});
|
|
1710
1754
|
pollVotesByCreationId.set(creationId, votesByVoter);
|
|
1711
|
-
const
|
|
1755
|
+
const pollAggregateOpts = contract.aggregatePollVotes;
|
|
1756
|
+
const aggregated = pollAggregateOpts({
|
|
1757
|
+
pollKey: {
|
|
1758
|
+
id: creationId,
|
|
1759
|
+
remoteJid: creationKey?.remoteJid ?? null,
|
|
1760
|
+
fromMe: null,
|
|
1761
|
+
},
|
|
1762
|
+
votes: Array.from(votesByVoter.values()).map((v) => v.vote),
|
|
1763
|
+
selfJid: contract.me().id ?? undefined,
|
|
1764
|
+
});
|
|
1712
1765
|
handle._updateFromAggregated(aggregated);
|
|
1713
1766
|
}
|
|
1714
1767
|
catch (err) {
|
|
@@ -1716,6 +1769,7 @@ function buildPollApi(contract, store, rawJid, guardOptions, pluginName) {
|
|
|
1716
1769
|
}
|
|
1717
1770
|
}
|
|
1718
1771
|
});
|
|
1772
|
+
boundPollPlugins.set(pluginName, detach);
|
|
1719
1773
|
}
|
|
1720
1774
|
const cooldown = (guardOptions.cooldown ?? true);
|
|
1721
1775
|
const jitter = (guardOptions.jitter ?? true);
|
|
@@ -1831,25 +1885,6 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
|
|
|
1831
1885
|
const cooldown = (guardOptions.cooldown ?? true);
|
|
1832
1886
|
const jitter = (guardOptions.jitter ?? true);
|
|
1833
1887
|
bindGroupMetaInvalidation(contract);
|
|
1834
|
-
// Sender for quoted messages — synthesize a neutral BotQuotedRef off
|
|
1835
|
-
// the adapter's pre-extracted contextInfo fields. No raw Baileys
|
|
1836
|
-
// envelope access needed here.
|
|
1837
|
-
const contextInfo = getContextInfo(msg);
|
|
1838
|
-
const quotedRaw = contextInfo?.quotedMessage
|
|
1839
|
-
? {
|
|
1840
|
-
id: contextInfo.stanzaId ?? "",
|
|
1841
|
-
chatId: msg.chatId,
|
|
1842
|
-
fromMe: false,
|
|
1843
|
-
type: "other",
|
|
1844
|
-
contentHash: "",
|
|
1845
|
-
timestamp: 0,
|
|
1846
|
-
_raw: {
|
|
1847
|
-
quotedMessage: contextInfo.quotedMessage,
|
|
1848
|
-
stanzaId: contextInfo.stanzaId,
|
|
1849
|
-
participant: contextInfo.participant,
|
|
1850
|
-
},
|
|
1851
|
-
}
|
|
1852
|
-
: null;
|
|
1853
1888
|
// Group participant JIDs come back in whatever addressing mode the group
|
|
1854
1889
|
// uses (@lid or @s.whatsapp.net/@c.us) — same issue as poll vote decryption.
|
|
1855
1890
|
// "sender" and the bot's own JID are usually PN-normalized, so a straight
|
|
@@ -27,6 +27,7 @@ import { normalizeJid } from "#drivers/jid.js";
|
|
|
27
27
|
import { loadPlugins, setupPlugins } from "#kernel/pluginLoader.js";
|
|
28
28
|
import { runContactRefreshSweep } from "#kernel/contactAutoSave.js";
|
|
29
29
|
import { registerAlertSockProvider, sendAlert } from "#kernel/alerts.js";
|
|
30
|
+
import { getDriverManager } from "#kernel/driverManager.js";
|
|
30
31
|
import { startUpdateCheckSchedule, stopUpdateCheckSchedule } from "#kernel/updateCheck.js";
|
|
31
32
|
import { setStatus } from "#kernel/statusServer.js";
|
|
32
33
|
import { logger } from "#logger";
|
|
@@ -93,6 +94,12 @@ const RECONNECT_MAX_MS = 60000;
|
|
|
93
94
|
// silently retrying past this point can make a restriction last longer.
|
|
94
95
|
const MAX_RECONNECT_ATTEMPTS = 6;
|
|
95
96
|
const CACHE_SAVE_INTERVAL_MS = 5 * 60 * 1000; // 5min
|
|
97
|
+
// Track consecutive restartRequired (515) — same counter space as
|
|
98
|
+
// reconnectAttempts but with a lower threshold for degradation since
|
|
99
|
+
// repeated 515 signals a protocol drift the current session can't
|
|
100
|
+
// recover from on its own.
|
|
101
|
+
const MAX_RESTART_REQUIRED = 3;
|
|
102
|
+
let restartRequiredCount = 0;
|
|
96
103
|
/**
|
|
97
104
|
* Loads the on-disk cache and merges it into `store` (union, never
|
|
98
105
|
* overwrite — see client/cache.ts). Runs once per process: the shared
|
|
@@ -193,6 +200,7 @@ async function startBot() {
|
|
|
193
200
|
if (connection === "open") {
|
|
194
201
|
state = "READY_INIT";
|
|
195
202
|
reconnectAttempts = 0;
|
|
203
|
+
restartRequiredCount = 0;
|
|
196
204
|
setStatus(true);
|
|
197
205
|
logger.success(t("system.connected"));
|
|
198
206
|
logger.info(t("system.clientId", { id: CLIENT_ID }));
|
|
@@ -211,11 +219,19 @@ async function startBot() {
|
|
|
211
219
|
if (connection === "close") {
|
|
212
220
|
const code = lastDisconnect?.error?.output?.statusCode;
|
|
213
221
|
const loggedOut = code === DisconnectReason.loggedOut;
|
|
222
|
+
const badSession = code === DisconnectReason.badSession;
|
|
223
|
+
const restartReq = code === DisconnectReason.restartRequired;
|
|
214
224
|
state = "BOOT";
|
|
215
225
|
setStatus(false, String(code));
|
|
216
226
|
logger.warn(t("system.disconnected", { reason: String(code) }));
|
|
217
|
-
if (loggedOut) {
|
|
218
|
-
|
|
227
|
+
if (loggedOut || badSession) {
|
|
228
|
+
if (badSession) {
|
|
229
|
+
logger.warn("Session data corrupted (badSession=500). Clearing session dir.");
|
|
230
|
+
getDriverManager().markDegraded("baileys", 300_000);
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
logger.warn(t("system.sessionExpired"));
|
|
234
|
+
}
|
|
219
235
|
try {
|
|
220
236
|
await fs.rm(AUTH_DIR, { recursive: true, force: true });
|
|
221
237
|
}
|
|
@@ -224,9 +240,27 @@ async function startBot() {
|
|
|
224
240
|
}
|
|
225
241
|
scheduleReconnect(1000);
|
|
226
242
|
}
|
|
243
|
+
else if (restartReq) {
|
|
244
|
+
restartRequiredCount++;
|
|
245
|
+
if (restartRequiredCount >= MAX_RESTART_REQUIRED) {
|
|
246
|
+
halted = true;
|
|
247
|
+
logger.error(`restartRequired (515) recurring — protocol drift suspected. Halting.`);
|
|
248
|
+
getDriverManager().markDegraded("baileys", 600_000);
|
|
249
|
+
sendAlert({
|
|
250
|
+
level: "critical",
|
|
251
|
+
title: "manybot — restartRequired recurring",
|
|
252
|
+
message: `Protocol drift suspected after ${restartRequiredCount}x restartRequired. Bot halted on Baileys. Run connect() manually.`,
|
|
253
|
+
}).catch(() => { });
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
const delay = Math.min(500, RECONNECT_BASE_MS);
|
|
257
|
+
logger.info(t("system.reconnecting", { secs: Math.round(delay / 1000) }));
|
|
258
|
+
scheduleReconnect(delay);
|
|
259
|
+
}
|
|
227
260
|
else if (!shuttingDown) {
|
|
228
261
|
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
|
|
229
262
|
halted = true;
|
|
263
|
+
getDriverManager().markDegraded("baileys", 600_000);
|
|
230
264
|
logger.error(t("system.reconnectHalted", { attempts: reconnectAttempts }));
|
|
231
265
|
sendAlert({
|
|
232
266
|
level: "critical",
|
|
@@ -19,7 +19,7 @@ import pino from "pino";
|
|
|
19
19
|
// setMaxListeners (see the comment in createSocket() below) — raised
|
|
20
20
|
// globally instead, before makeWASocket() ever constructs one.
|
|
21
21
|
EventEmitter.defaultMaxListeners = 50;
|
|
22
|
-
export const AUTH_DIR = path.join(CONFIG_DIR, "sessions", CLIENT_ID);
|
|
22
|
+
export const AUTH_DIR = path.join(CONFIG_DIR, "sessions", CLIENT_ID, "baileys");
|
|
23
23
|
// ── Shared store (survives socket reconnects) ─────────────────────────────────
|
|
24
24
|
export const store = createStore();
|
|
25
25
|
/**
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { logger } from "#logger";
|
|
2
2
|
import { CONFIG } from "#config";
|
|
3
|
+
import { t } from "#i18n";
|
|
3
4
|
import * as grpc from "@grpc/grpc-js";
|
|
4
5
|
import * as protoLoader from "@grpc/proto-loader";
|
|
5
6
|
import path from "path";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
8
|
+
import qrcode from "qrcode-terminal";
|
|
7
9
|
/**
|
|
8
10
|
* Whatsmeow gRPC client implementing the WaContract interface.
|
|
9
11
|
*
|
|
@@ -50,7 +52,7 @@ class WhatsmeowClient {
|
|
|
50
52
|
const address = CONFIG.drivers.whatsmeow.grpcAddress ?? "localhost:50051";
|
|
51
53
|
const Service = this.loadProto();
|
|
52
54
|
this.client = new Service(address, grpc.credentials.createInsecure());
|
|
53
|
-
//
|
|
55
|
+
// 1. Health check — confirm the gRPC server is up
|
|
54
56
|
await new Promise((resolve, reject) => {
|
|
55
57
|
this.client.HealthCheck({}, (err, resp) => {
|
|
56
58
|
if (err)
|
|
@@ -62,12 +64,43 @@ class WhatsmeowClient {
|
|
|
62
64
|
reject(new Error("Whatsmeow service not ready"));
|
|
63
65
|
});
|
|
64
66
|
});
|
|
65
|
-
logger.info("[whatsmeow]
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
logger.info("[whatsmeow] gRPC service ready");
|
|
68
|
+
// 2. Call Connect RPC — initiates WhatsApp auth (QR or reuse existing session)
|
|
69
|
+
const connectResp = await new Promise((resolve, reject) => {
|
|
70
|
+
this.client.Connect({}, (err, resp) => {
|
|
71
|
+
if (err)
|
|
72
|
+
return reject(err);
|
|
73
|
+
resolve(resp);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
const needsAuth = !connectResp.ok;
|
|
77
|
+
if (needsAuth && connectResp.qrCode) {
|
|
78
|
+
logger.info(t("system.qrScan"));
|
|
79
|
+
qrcode.generate(connectResp.qrCode, { small: true });
|
|
80
|
+
}
|
|
81
|
+
// 3. Set up auth deferred BEFORE SubscribeEvents to avoid race
|
|
82
|
+
let authDeferred = null;
|
|
83
|
+
let authDone = false;
|
|
84
|
+
const authPromise = needsAuth
|
|
85
|
+
? new Promise((resolve, reject) => {
|
|
86
|
+
authDeferred = { resolve, reject };
|
|
87
|
+
setTimeout(() => {
|
|
88
|
+
if (!authDone) {
|
|
89
|
+
authDone = true;
|
|
90
|
+
reject(new Error("Whatsmeow auth timeout (2 min)"));
|
|
91
|
+
}
|
|
92
|
+
}, 120_000);
|
|
93
|
+
})
|
|
94
|
+
: Promise.resolve();
|
|
95
|
+
// 4. Open the server-streaming event subscription
|
|
69
96
|
const stream = this.client.SubscribeEvents({});
|
|
70
97
|
stream.on("data", (raw) => {
|
|
98
|
+
// Resolve auth promise when connection opens (QR scanned / session reused)
|
|
99
|
+
if (!authDone && authDeferred && raw.connState?.state === "open") {
|
|
100
|
+
authDone = true;
|
|
101
|
+
authDeferred.resolve();
|
|
102
|
+
authDeferred = null;
|
|
103
|
+
}
|
|
71
104
|
try {
|
|
72
105
|
if (raw.connState) {
|
|
73
106
|
const state = raw.connState.state ?? "connecting";
|
|
@@ -87,13 +120,29 @@ class WhatsmeowClient {
|
|
|
87
120
|
}
|
|
88
121
|
});
|
|
89
122
|
stream.on("error", (err) => {
|
|
123
|
+
if (!authDone) {
|
|
124
|
+
authDone = true;
|
|
125
|
+
authDeferred?.reject(err);
|
|
126
|
+
authDeferred = null;
|
|
127
|
+
}
|
|
90
128
|
logger.warn(`[whatsmeow] event stream error: ${err.message}`);
|
|
91
129
|
this.ready = false;
|
|
92
130
|
});
|
|
93
131
|
stream.on("end", () => {
|
|
132
|
+
if (!authDone) {
|
|
133
|
+
authDone = true;
|
|
134
|
+
authDeferred?.reject(new Error("Event stream ended before auth completed"));
|
|
135
|
+
authDeferred = null;
|
|
136
|
+
}
|
|
94
137
|
logger.warn(`[whatsmeow] event stream ended`);
|
|
95
138
|
this.ready = false;
|
|
96
139
|
});
|
|
140
|
+
// 5. If not authenticated, wait for connState === "open" from the event stream
|
|
141
|
+
await authPromise;
|
|
142
|
+
if (needsAuth) {
|
|
143
|
+
logger.info("[whatsmeow] authenticated");
|
|
144
|
+
}
|
|
145
|
+
this.ready = true;
|
|
97
146
|
}
|
|
98
147
|
async disconnect() {
|
|
99
148
|
if (this.client) {
|