@manybot/manybot 5.6.0 → 5.6.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/dist/drivers/baileys/adapter.js +145 -72
- package/dist/drivers/baileys/api/index.js +48 -35
- 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 +1 -1
|
@@ -25,12 +25,111 @@
|
|
|
25
25
|
import { normalizeMessageContent, downloadMediaMessage, jidNormalizedUser, } from "@whiskeysockets/baileys";
|
|
26
26
|
import { createHash } from "node:crypto";
|
|
27
27
|
import { logger } from "#logger";
|
|
28
|
+
/**
|
|
29
|
+
* Classify a Baileys message-content payload (`WAMessageContent`) into
|
|
30
|
+
* the neutral `(type, body, mimetype)` triple used by `BotMessage`. Pure
|
|
31
|
+
* function — no side effects, no closures — so it's safe to call for
|
|
32
|
+
* both real incoming messages and the embedded `quotedMessage` content
|
|
33
|
+
* carried in `contextInfo` (which can be an `ephemeralMessage`/
|
|
34
|
+
* `viewOnceMessage` wrapper, hence the `normalizeMessageContent` call).
|
|
35
|
+
*
|
|
36
|
+
* Exported so `api/index.ts` can decode the quoted payload when
|
|
37
|
+
* synthesizing the `BotMessage` returned by `getReply()`.
|
|
38
|
+
*/
|
|
39
|
+
export function decodeContent(content) {
|
|
40
|
+
const m = normalizeMessageContent(content) ?? undefined;
|
|
41
|
+
let type = "other";
|
|
42
|
+
let body = "";
|
|
43
|
+
let mimetype;
|
|
44
|
+
if (m?.conversation) {
|
|
45
|
+
type = "text";
|
|
46
|
+
body = m.conversation;
|
|
47
|
+
}
|
|
48
|
+
else if (m?.extendedTextMessage?.text) {
|
|
49
|
+
type = "text";
|
|
50
|
+
body = m.extendedTextMessage.text ?? "";
|
|
51
|
+
}
|
|
52
|
+
else if (m?.imageMessage) {
|
|
53
|
+
type = "image";
|
|
54
|
+
body = m.imageMessage.caption ?? "";
|
|
55
|
+
mimetype = m.imageMessage.mimetype ?? undefined;
|
|
56
|
+
}
|
|
57
|
+
else if (m?.videoMessage) {
|
|
58
|
+
type = "video";
|
|
59
|
+
body = m.videoMessage.caption ?? "";
|
|
60
|
+
mimetype = m.videoMessage.mimetype ?? undefined;
|
|
61
|
+
}
|
|
62
|
+
else if (m?.audioMessage) {
|
|
63
|
+
type = "audio";
|
|
64
|
+
mimetype = m.audioMessage.mimetype ?? undefined;
|
|
65
|
+
}
|
|
66
|
+
else if (m?.documentMessage) {
|
|
67
|
+
type = "document";
|
|
68
|
+
body = m.documentMessage.caption ?? "";
|
|
69
|
+
mimetype = m.documentMessage.mimetype ?? undefined;
|
|
70
|
+
}
|
|
71
|
+
else if (m?.stickerMessage) {
|
|
72
|
+
type = "sticker";
|
|
73
|
+
mimetype = m.stickerMessage.mimetype ?? undefined;
|
|
74
|
+
}
|
|
75
|
+
return { type, body, mimetype };
|
|
76
|
+
}
|
|
28
77
|
export function createBaileysAdapter(initial) {
|
|
29
78
|
// mutable so rebind() can swap it; closure-scoped so the contract below
|
|
30
79
|
// always sees the latest sock.
|
|
31
80
|
let sock = initial.sock;
|
|
32
81
|
const store = initial.store;
|
|
33
82
|
// ── Adapter-local helpers ────────────────────────────────────────────────
|
|
83
|
+
/**
|
|
84
|
+
* Build the `quoted` option for `sock.sendMessage(jid, content, opts)`.
|
|
85
|
+
*
|
|
86
|
+
* Baileys expects the `quoted` field to be a full message-shaped object —
|
|
87
|
+
* `{ key: {...}, message: {...} }` — so it can extract both the attribution
|
|
88
|
+
* (stanzaId/participant/fromMe from `key`) and the preview content (from
|
|
89
|
+
* `message`) when generating contextInfo. Passing only `key` makes
|
|
90
|
+
* `generateWAMessageFromContent` call `normalizeMessageContent(undefined)`
|
|
91
|
+
* → `getContentType(undefined)` → `undefined`, then index it with `[]`
|
|
92
|
+
* and throw `Cannot read properties of undefined (reading 'undefined')`.
|
|
93
|
+
*
|
|
94
|
+
* We resolve the full envelope from the in-memory store, indexed by the
|
|
95
|
+
* (remoteJid, id) pair carried in the neutral `BotQuotedRef`. If the
|
|
96
|
+
* envelope is no longer present (evicted past MAX_MSGS_PER_CHAT, lost on
|
|
97
|
+
* restart, etc.) we return `undefined` rather than emit a half-formed
|
|
98
|
+
* `quoted` — degrading to a plain unquoted reply is safer than crashing
|
|
99
|
+
* the calling plugin.
|
|
100
|
+
*/
|
|
101
|
+
function buildQuotedOpts(quoted) {
|
|
102
|
+
if (!quoted?.id || !quoted?.remoteJid)
|
|
103
|
+
return undefined;
|
|
104
|
+
const raw = store.messages.get(quoted.remoteJid)?.get(quoted.id);
|
|
105
|
+
const inner = raw?.message;
|
|
106
|
+
if (!inner)
|
|
107
|
+
return undefined; // safe fallback: no quoted at all
|
|
108
|
+
// `quoted` (used by sendMessage for reply-citation) is a
|
|
109
|
+
// message-shaped object: `{ key: {...}, message: {...} }`. Baileys
|
|
110
|
+
// reads `quoted.key.*` to populate contextInfo (stanzaId, participant,
|
|
111
|
+
// fromMe) — passing the fields flat used to misattribute the reply to
|
|
112
|
+
// the bot itself. `quoted.message` is also required: Baileys' internal
|
|
113
|
+
// `generateWAMessageFromContent` calls `normalizeMessageContent(
|
|
114
|
+
// quoted.message)` and indexes the result with `getContentType(...)`,
|
|
115
|
+
// which throws if `.message` is missing.
|
|
116
|
+
return { quoted: { key: toFlatKey(quoted), message: inner } };
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Translate a neutral `BotQuotedRef` into the FLAT key shape Baileys
|
|
120
|
+
* expects for `react`/`delete`/`edit`/`readMessages` (proto.IMessageKey
|
|
121
|
+
* — `{ id, remoteJid, fromMe, participant }` directly, NOT nested under
|
|
122
|
+
* a `.key` field). The nested `{key, message}` form is reserved for the
|
|
123
|
+
* `quoted` field on sendMessage — see `buildQuotedOpts` above.
|
|
124
|
+
*/
|
|
125
|
+
function toFlatKey(ref) {
|
|
126
|
+
return {
|
|
127
|
+
id: ref.id ?? null,
|
|
128
|
+
remoteJid: ref.remoteJid ?? "",
|
|
129
|
+
fromMe: !!ref.fromMe,
|
|
130
|
+
participant: ref.participant ?? undefined,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
34
133
|
/** Compute sha1-hex of a normalized buffer or string. */
|
|
35
134
|
function sha1(input) {
|
|
36
135
|
const hash = createHash("sha1");
|
|
@@ -45,40 +144,7 @@ export function createBaileysAdapter(initial) {
|
|
|
45
144
|
/** Translate a Baileys WAMessage into the neutral BotMessage envelope. */
|
|
46
145
|
function toBotMessage(msg) {
|
|
47
146
|
const m = normalizeMessageContent(msg.message) ?? undefined;
|
|
48
|
-
|
|
49
|
-
let body = "";
|
|
50
|
-
let mimetype;
|
|
51
|
-
if (m?.conversation) {
|
|
52
|
-
type = "text";
|
|
53
|
-
body = m.conversation;
|
|
54
|
-
}
|
|
55
|
-
else if (m?.extendedTextMessage?.text) {
|
|
56
|
-
type = "text";
|
|
57
|
-
body = m.extendedTextMessage.text ?? "";
|
|
58
|
-
}
|
|
59
|
-
else if (m?.imageMessage) {
|
|
60
|
-
type = "image";
|
|
61
|
-
body = m.imageMessage.caption ?? "";
|
|
62
|
-
mimetype = m.imageMessage.mimetype ?? undefined;
|
|
63
|
-
}
|
|
64
|
-
else if (m?.videoMessage) {
|
|
65
|
-
type = "video";
|
|
66
|
-
body = m.videoMessage.caption ?? "";
|
|
67
|
-
mimetype = m.videoMessage.mimetype ?? undefined;
|
|
68
|
-
}
|
|
69
|
-
else if (m?.audioMessage) {
|
|
70
|
-
type = "audio";
|
|
71
|
-
mimetype = m.audioMessage.mimetype ?? undefined;
|
|
72
|
-
}
|
|
73
|
-
else if (m?.documentMessage) {
|
|
74
|
-
type = "document";
|
|
75
|
-
body = m.documentMessage.caption ?? "";
|
|
76
|
-
mimetype = m.documentMessage.mimetype ?? undefined;
|
|
77
|
-
}
|
|
78
|
-
else if (m?.stickerMessage) {
|
|
79
|
-
type = "sticker";
|
|
80
|
-
mimetype = m.stickerMessage.mimetype ?? undefined;
|
|
81
|
-
}
|
|
147
|
+
const { type, body, mimetype } = decodeContent(msg.message);
|
|
82
148
|
const key = msg.key;
|
|
83
149
|
const contextInfo = m?.extendedTextMessage?.contextInfo ??
|
|
84
150
|
m?.imageMessage?.contextInfo ??
|
|
@@ -86,6 +152,7 @@ export function createBaileysAdapter(initial) {
|
|
|
86
152
|
m?.audioMessage?.contextInfo ??
|
|
87
153
|
m?.documentMessage?.contextInfo ??
|
|
88
154
|
undefined;
|
|
155
|
+
const ciTyped = contextInfo;
|
|
89
156
|
return {
|
|
90
157
|
id: msg.key.id ?? "",
|
|
91
158
|
chatId: msg.key.remoteJid ?? "",
|
|
@@ -96,20 +163,27 @@ export function createBaileysAdapter(initial) {
|
|
|
96
163
|
body,
|
|
97
164
|
mimetype: mimetype ?? undefined,
|
|
98
165
|
pushName: msg.pushName,
|
|
99
|
-
mentionedJid:
|
|
100
|
-
quotedKey:
|
|
101
|
-
id:
|
|
166
|
+
mentionedJid: ciTyped?.mentionedJid ?? undefined,
|
|
167
|
+
quotedKey: ciTyped?.stanzaId ? {
|
|
168
|
+
id: ciTyped.stanzaId,
|
|
102
169
|
remoteJid: msg.key.remoteJid ?? undefined,
|
|
103
170
|
fromMe: false,
|
|
104
|
-
participant:
|
|
171
|
+
participant: ciTyped.participant ?? undefined,
|
|
105
172
|
} : undefined,
|
|
106
173
|
fromLid: key.participantAlt,
|
|
107
174
|
fromPn: key.participant,
|
|
108
175
|
participantAlt: key.participantAlt,
|
|
109
176
|
remoteJidAlt: key.remoteJidAlt,
|
|
110
|
-
//
|
|
177
|
+
// Driver-specific escape hatches:
|
|
178
|
+
// - pollEncKeyRaw: poll-decryption key for vote decryption
|
|
179
|
+
// - contextInfo: full IContextInfo (incl. embedded quotedMessage)
|
|
180
|
+
// so quoted-message consumers (api/index.ts
|
|
181
|
+
// buildMessageContext / downloadMedia fallback)
|
|
182
|
+
// can decode the quoted payload without going
|
|
183
|
+
// back through the store.
|
|
111
184
|
_raw: {
|
|
112
185
|
pollEncKeyRaw: m?.messageContextInfo?.messageSecret ?? undefined,
|
|
186
|
+
contextInfo: ciTyped ?? undefined,
|
|
113
187
|
},
|
|
114
188
|
};
|
|
115
189
|
}
|
|
@@ -237,17 +311,17 @@ export function createBaileysAdapter(initial) {
|
|
|
237
311
|
return toSentRef(ref, jid);
|
|
238
312
|
},
|
|
239
313
|
async react(jid, target, emoji) {
|
|
240
|
-
const key =
|
|
314
|
+
const key = toFlatKey(target);
|
|
241
315
|
await sock.sendMessage(jid, { react: { text: emoji, key } });
|
|
242
316
|
},
|
|
243
317
|
async deleteMessage(jid, target, forEveryone) {
|
|
244
318
|
if (!forEveryone)
|
|
245
319
|
return;
|
|
246
|
-
const key =
|
|
320
|
+
const key = toFlatKey(target);
|
|
247
321
|
await sock.sendMessage(jid, { delete: key });
|
|
248
322
|
},
|
|
249
323
|
async editMessage(jid, target, text) {
|
|
250
|
-
const key =
|
|
324
|
+
const key = toFlatKey(target);
|
|
251
325
|
await sock.sendMessage(jid, { text, edit: key });
|
|
252
326
|
},
|
|
253
327
|
// ── presence + read ───────────────────────────────────────────────────
|
|
@@ -258,7 +332,7 @@ export function createBaileysAdapter(initial) {
|
|
|
258
332
|
await sock.sendPresenceUpdate(baileyState, jid);
|
|
259
333
|
},
|
|
260
334
|
async readMessages(keys) {
|
|
261
|
-
const baileyKeys = keys.map(
|
|
335
|
+
const baileyKeys = keys.map((k) => toFlatKey(k));
|
|
262
336
|
await sock.readMessages(baileyKeys);
|
|
263
337
|
},
|
|
264
338
|
// ── contacts ──────────────────────────────────────────────────────────
|
|
@@ -385,10 +459,30 @@ export function createBaileysAdapter(initial) {
|
|
|
385
459
|
},
|
|
386
460
|
// ── media (download) ───────────────────────────────────────────────────
|
|
387
461
|
async downloadMedia(msg, opts) {
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
|
|
462
|
+
// Resolve the Baileys message envelope needed by `downloadMediaMessage`.
|
|
463
|
+
// Preferred path: if the caller already carries the embedded message
|
|
464
|
+
// payload (synthetic `BotMessage` for a quoted message, whose
|
|
465
|
+
// `_raw.contextInfo.quotedMessage` is the full WAMessageContent),
|
|
466
|
+
// build the envelope directly from that. This avoids the silent
|
|
467
|
+
// failure mode where the quoted message's original envelope has
|
|
468
|
+
// aged out of the store's per-chat ring buffer.
|
|
469
|
+
//
|
|
470
|
+
// Fallback: the regular case (downloading media for the incoming
|
|
471
|
+
// message itself, or any other BotMessage that has a real envelope
|
|
472
|
+
// in the store) — look it up by (chatId, id).
|
|
473
|
+
const embedded = msg._raw;
|
|
474
|
+
const embeddedContent = embedded?.contextInfo?.quotedMessage;
|
|
475
|
+
const raw = embeddedContent
|
|
476
|
+
? {
|
|
477
|
+
key: {
|
|
478
|
+
id: embedded.contextInfo?.stanzaId ?? msg.id,
|
|
479
|
+
remoteJid: msg.chatId,
|
|
480
|
+
fromMe: false,
|
|
481
|
+
participant: embedded.contextInfo?.participant ?? undefined,
|
|
482
|
+
},
|
|
483
|
+
message: embeddedContent,
|
|
484
|
+
}
|
|
485
|
+
: store.messages.get(msg.chatId)?.get(msg.id);
|
|
392
486
|
if (!raw)
|
|
393
487
|
return null;
|
|
394
488
|
try {
|
|
@@ -508,31 +602,10 @@ export function createBaileysAdapter(initial) {
|
|
|
508
602
|
};
|
|
509
603
|
}
|
|
510
604
|
// ── Helpers used inside the adapter above ────────────────────────────────────
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
function toBaileysKey(ref) {
|
|
517
|
-
// Baileys expects `quoted` to be a message-shaped object with these
|
|
518
|
-
// fields nested under `.key` (quoted.key.id / .remoteJid / .fromMe /
|
|
519
|
-
// .participant) — it reads quoted.key.* to build contextInfo
|
|
520
|
-
// (stanzaId, participant, fromMe). Passing them flat (as this used to)
|
|
521
|
-
// means quoted.key is undefined inside Baileys, so participant/fromMe
|
|
522
|
-
// resolve to undefined and the quoted reply gets misattributed to the
|
|
523
|
-
// bot itself instead of the original sender. The quoted text still
|
|
524
|
-
// rendered before this fix because WhatsApp's client resolves the
|
|
525
|
-
// preview content locally via stanzaId — only the author attribution
|
|
526
|
-
// was broken.
|
|
527
|
-
return {
|
|
528
|
-
key: {
|
|
529
|
-
id: ref.id ?? null,
|
|
530
|
-
remoteJid: ref.remoteJid ?? "",
|
|
531
|
-
fromMe: !!ref.fromMe,
|
|
532
|
-
participant: ref.participant ?? undefined,
|
|
533
|
-
},
|
|
534
|
-
};
|
|
535
|
-
}
|
|
605
|
+
//
|
|
606
|
+
// `buildQuotedOpts` and `toFlatKey` are defined inside the
|
|
607
|
+
// `createBaileysAdapter` closure (so they can see the `store`) — only
|
|
608
|
+
// `toSentRef` and `silentBaileysLogger` remain module-scoped helpers.
|
|
536
609
|
function toSentRef(raw, fallbackChatId) {
|
|
537
610
|
const r = raw;
|
|
538
611
|
return {
|
|
@@ -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";
|
|
@@ -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,
|
|
@@ -1831,25 +1863,6 @@ export function buildApi({ msg, chat, contract, store, pluginRegistry, pluginNam
|
|
|
1831
1863
|
const cooldown = (guardOptions.cooldown ?? true);
|
|
1832
1864
|
const jitter = (guardOptions.jitter ?? true);
|
|
1833
1865
|
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
1866
|
// Group participant JIDs come back in whatever addressing mode the group
|
|
1854
1867
|
// uses (@lid or @s.whatsapp.net/@c.us) — same issue as poll vote decryption.
|
|
1855
1868
|
// "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) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync, chmodSync } from "node:fs";
|
|
1
|
+
import { mkdirSync, writeFileSync, chmodSync, existsSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import * as clack from "@clack/prompts";
|
|
4
|
-
import { persistConfigValue } from "#config";
|
|
4
|
+
import { persistConfigValue, CONFIG_DIR } from "#config";
|
|
5
5
|
import { t } from "#i18n";
|
|
6
6
|
const SUPPORTED = [
|
|
7
7
|
{ os: "linux", arch: "x64", name: "whatsmeow-service-linux-x64" },
|
|
@@ -22,7 +22,7 @@ async function fetchLatestTag() {
|
|
|
22
22
|
return data.tag_name ?? "v5.6.1";
|
|
23
23
|
}
|
|
24
24
|
function binaryDir() {
|
|
25
|
-
return path.resolve(
|
|
25
|
+
return path.resolve(CONFIG_DIR, "whatsmeow-service", "bin");
|
|
26
26
|
}
|
|
27
27
|
export async function promptWhatsmeowInstall() {
|
|
28
28
|
const target = detectTarget();
|
|
@@ -30,6 +30,16 @@ export async function promptWhatsmeowInstall() {
|
|
|
30
30
|
clack.log.warn(str(t("whatsmeow.unsupportedArch", { os: process.platform, arch: process.arch })));
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
|
+
const outPath = path.join(binaryDir(), "whatsmeow-service");
|
|
34
|
+
// Binary already on disk → skip the prompt and the download, but
|
|
35
|
+
// make sure the config flag is set so the supervisor boots on the
|
|
36
|
+
// next run. The earlier "no, declined install" flow only writes the
|
|
37
|
+
// TOML on success, so users who later build the binary by hand also
|
|
38
|
+
// hit this branch on their next first-login.
|
|
39
|
+
if (existsSync(outPath)) {
|
|
40
|
+
await persistConfigValue("driver_whatsmeow_enabled", "true");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
33
43
|
const choice = await clack.confirm({
|
|
34
44
|
message: str(t("whatsmeow.installPrompt")),
|
|
35
45
|
initialValue: false,
|
|
@@ -61,10 +71,16 @@ export async function promptWhatsmeowInstall() {
|
|
|
61
71
|
const buffer = Buffer.from(await res.arrayBuffer());
|
|
62
72
|
const dir = binaryDir();
|
|
63
73
|
mkdirSync(dir, { recursive: true });
|
|
64
|
-
const outPath = path.join(dir, "whatsmeow-service");
|
|
65
74
|
writeFileSync(outPath, buffer);
|
|
66
75
|
chmodSync(outPath, 0o755);
|
|
67
76
|
await persistConfigValue("driver_whatsmeow_enabled", "true");
|
|
68
77
|
spin.stop(str(t("whatsmeow.installed", { path: outPath })));
|
|
69
78
|
clack.note(str(t("whatsmeow.restartNotice")), str(t("whatsmeow.installTitle")));
|
|
79
|
+
// The bot is still in its very first run (no Baileys session yet, no
|
|
80
|
+
// supervisor spawned) — the config has just been updated on disk but
|
|
81
|
+
// the in-memory `CONFIG` object and the supervisor lifecycle were
|
|
82
|
+
// initialized at startup with `whatsmeow.enabled = false`. Restarting
|
|
83
|
+
// is required for the new value to take effect. Exit cleanly so the
|
|
84
|
+
// user just re-runs the bot.
|
|
85
|
+
setTimeout(() => process.exit(0), 100);
|
|
70
86
|
}
|
|
@@ -30,12 +30,13 @@
|
|
|
30
30
|
* See the lifecycle contract this implements.
|
|
31
31
|
*/
|
|
32
32
|
import { spawn } from "node:child_process";
|
|
33
|
-
import { existsSync } from "node:fs";
|
|
33
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
34
34
|
import path from "node:path";
|
|
35
35
|
import { fileURLToPath } from "node:url";
|
|
36
|
-
import { CONFIG } from "#config";
|
|
36
|
+
import { CONFIG, CONFIG_DIR, CLIENT_ID } from "#config";
|
|
37
37
|
import { logger } from "#logger";
|
|
38
38
|
import { fireAlert } from "#kernel/alerts.js";
|
|
39
|
+
import { getDriverManager } from "#kernel/driverManager.js";
|
|
39
40
|
// ── Tunables (mirror drivers/baileys/index.ts:98-106) ──────────────────────
|
|
40
41
|
const RECONNECT_BASE_MS = 1000;
|
|
41
42
|
const RECONNECT_MAX_MS = 60_000;
|
|
@@ -49,7 +50,8 @@ const SHUTDOWN_GRACE_MS = 5_000;
|
|
|
49
50
|
* first match that exists and is a regular file. Order:
|
|
50
51
|
* 1. CONFIG.drivers.whatsmeow.binaryPath (explicit user choice)
|
|
51
52
|
* 2. env WM_BINARY_PATH (escape hatch for exotic installs)
|
|
52
|
-
* 3.
|
|
53
|
+
* 3. stable config dir (~/.manybot/whatsmeow-service/bin/whatsmeow-service)
|
|
54
|
+
* 4. dev layout (<cwd>/whatsmeow-service/bin/whatsmeow-service)
|
|
53
55
|
* 4. npm-global layout (sibling of the node binary, /usr/local style)
|
|
54
56
|
*/
|
|
55
57
|
function resolveBinaryPath() {
|
|
@@ -60,6 +62,8 @@ function resolveBinaryPath() {
|
|
|
60
62
|
const fromEnv = process.env.WM_BINARY_PATH;
|
|
61
63
|
if (fromEnv)
|
|
62
64
|
candidates.push(path.resolve(fromEnv));
|
|
65
|
+
// Stable config dir: ~/.manybot/whatsmeow-service/bin/whatsmeow-service
|
|
66
|
+
candidates.push(path.resolve(CONFIG_DIR, "whatsmeow-service", "bin", "whatsmeow-service"));
|
|
63
67
|
// Dev: `<repo>/whatsmeow-service/bin/whatsmeow-service`
|
|
64
68
|
candidates.push(path.resolve(process.cwd(), "whatsmeow-service", "bin", "whatsmeow-service"));
|
|
65
69
|
// Global npm install: `<prefix>/bin/../share/manybot/bin/whatsmeow-service`
|
|
@@ -148,8 +152,12 @@ export async function startWhatsmeowSupervisor() {
|
|
|
148
152
|
"whatsmeow-service/bin/whatsmeow-service relative to cwd. Bot will run on Baileys only.");
|
|
149
153
|
return null;
|
|
150
154
|
}
|
|
155
|
+
logger.info(`[supervisor] using binary: ${binary}`);
|
|
151
156
|
const grpcAddress = CONFIG.drivers.whatsmeow.grpcAddress || "localhost:50051";
|
|
152
|
-
const sessionDir = path.resolve(
|
|
157
|
+
const sessionDir = path.resolve(CONFIG_DIR, "sessions", CLIENT_ID, "whatsmeow", "session.db");
|
|
158
|
+
// Ensure the parent directory exists before the Go subprocess tries to
|
|
159
|
+
// create/open the SQLite file.
|
|
160
|
+
mkdirSync(path.dirname(sessionDir), { recursive: true });
|
|
153
161
|
const state = {
|
|
154
162
|
proc: null,
|
|
155
163
|
pid: null,
|
|
@@ -196,6 +204,7 @@ export async function startWhatsmeowSupervisor() {
|
|
|
196
204
|
state.halted = true;
|
|
197
205
|
state.ready = false;
|
|
198
206
|
state.readyDeferred?.reject(new Error(reason));
|
|
207
|
+
getDriverManager().markDegraded("whatsmeow", 600_000);
|
|
199
208
|
fireAlert("whatsmeow_subprocess_halted", { reason });
|
|
200
209
|
}
|
|
201
210
|
function scheduleRestart() {
|
|
@@ -220,7 +229,17 @@ export async function startWhatsmeowSupervisor() {
|
|
|
220
229
|
let proc;
|
|
221
230
|
try {
|
|
222
231
|
proc = spawn(state.binary, ["--grpc-addr", state.addr, "--session-dir", state.sessionDir], {
|
|
223
|
-
stdio: "ignore",
|
|
232
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
233
|
+
});
|
|
234
|
+
// Forward Go service stdout/stderr to the bot log so we can see
|
|
235
|
+
// crashes, missing dependencies, port-in-use, etc. Without this
|
|
236
|
+
// `stdio: "ignore"` would discard everything and a crashed
|
|
237
|
+
// subprocess would only surface as an exit code.
|
|
238
|
+
proc.stdout?.on("data", (chunk) => {
|
|
239
|
+
process.stdout.write(`[whatsmeow-stdout] ${chunk}`);
|
|
240
|
+
});
|
|
241
|
+
proc.stderr?.on("data", (chunk) => {
|
|
242
|
+
process.stderr.write(`[whatsmeow-stderr] ${chunk}`);
|
|
224
243
|
});
|
|
225
244
|
}
|
|
226
245
|
catch (e) {
|
|
@@ -81,11 +81,21 @@ export async function sendWithFallback(jid, text, opts = {}) {
|
|
|
81
81
|
// waitForSendSlot is the same throttle the rest of the senders use
|
|
82
82
|
// (fallback must respect rate-limit too).
|
|
83
83
|
await waitForSendSlot(jid, { cooldown: true, jitter: true });
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
let primaryRef = null;
|
|
85
|
+
let primarySendFailed = false;
|
|
86
|
+
try {
|
|
87
|
+
primaryRef = await primary.sendText(jid, text, opts);
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
primarySendFailed = true;
|
|
91
|
+
logger.warn({ driver: primaryKey, jid, error: String(err) }, "send threw on primary");
|
|
92
|
+
}
|
|
93
|
+
if (!primarySendFailed) {
|
|
94
|
+
if (await verifyDelivery(primary, jid, primaryRef, drivers.verifyWindowMs)) {
|
|
95
|
+
return primaryRef;
|
|
96
|
+
}
|
|
97
|
+
logger.warn({ driver: primaryKey, jid, messageId: primaryRef.id }, "send not confirmed by primary");
|
|
87
98
|
}
|
|
88
|
-
logger.warn({ driver: primaryKey, jid, messageId: ref.id }, "send not confirmed by primary");
|
|
89
99
|
dm.markDegraded(primaryKey, drivers.fallbackCooldownMs);
|
|
90
100
|
const secondary = pickSecondary(dm, primaryKey);
|
|
91
101
|
if (!secondary || !secondary.isReady()) {
|
|
@@ -94,7 +104,7 @@ export async function sendWithFallback(jid, text, opts = {}) {
|
|
|
94
104
|
}
|
|
95
105
|
try {
|
|
96
106
|
const fallbackRef = await sendVia(secondary, jid, text, opts, drivers.verifyWindowMs, /*skipGuard=*/ true);
|
|
97
|
-
logger.info({ driver: secondary.name, jid, messageId: fallbackRef.id, reason: "primary verification failed" }, "message sent via fallback");
|
|
107
|
+
logger.info({ driver: secondary.name, jid, messageId: fallbackRef.id, reason: primarySendFailed ? "send threw" : "primary verification failed" }, "message sent via fallback");
|
|
98
108
|
return fallbackRef;
|
|
99
109
|
}
|
|
100
110
|
catch (err) {
|
package/dist/locales/en.json
CHANGED
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"connectGaveUp": "Couldn't connect after several attempts. Check your network and try again."
|
|
72
72
|
},
|
|
73
73
|
"whatsmeow": {
|
|
74
|
-
"installPrompt": "Install whatsmeow driver for
|
|
74
|
+
"installPrompt": "Install whatsmeow driver for fallback support? (EXPERIMENTAL — only text send & history work; other methods throw)",
|
|
75
75
|
"unsupportedArch": "whatsmeow driver not available for {{os}}-{{arch}}. The bot will use Baileys only.",
|
|
76
76
|
"fetchingTag": "Fetching latest whatsmeow release...",
|
|
77
77
|
"fetchFailed": "Could not reach Codeberg. Skipping whatsmeow install.",
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
"downloadFailed": "Download failed: {{reason}}",
|
|
80
80
|
"installTitle": "whatsmeow driver",
|
|
81
81
|
"installed": "whatsmeow driver installed at {{path}}",
|
|
82
|
-
"restartNotice": "Restart the bot for the whatsmeow driver to take effect."
|
|
82
|
+
"restartNotice": "Restart the bot for the whatsmeow driver to take effect.",
|
|
83
|
+
"experimentalNotice": "whatsmeow is EXPERIMENTAL: only sendText and getHistory are implemented. sendImage, sendPoll, groupMetadata, and other methods throw."
|
|
83
84
|
}
|
|
84
85
|
}
|
package/dist/locales/es.json
CHANGED
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"connectGaveUp": "No se pudo conectar tras varios intentos. Revisa tu conexión e intenta de nuevo."
|
|
72
72
|
},
|
|
73
73
|
"whatsmeow": {
|
|
74
|
-
"installPrompt": "¿Instalar el driver whatsmeow para
|
|
74
|
+
"installPrompt": "¿Instalar el driver whatsmeow para soporte de fallback? (EXPERIMENTAL — solo sendText e historial funcionan; otros métodos lanzan error)",
|
|
75
75
|
"unsupportedArch": "Driver whatsmeow no disponible para {{os}}-{{arch}}. El bot usará solo Baileys.",
|
|
76
76
|
"fetchingTag": "Obteniendo última versión de whatsmeow...",
|
|
77
77
|
"fetchFailed": "No se pudo contactar a Codeberg. Instalación de whatsmeow omitida.",
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
"downloadFailed": "Descarga fallida: {{reason}}",
|
|
80
80
|
"installTitle": "Driver whatsmeow",
|
|
81
81
|
"installed": "Driver whatsmeow instalado en {{path}}",
|
|
82
|
-
"restartNotice": "Reinicia el bot para que el driver whatsmeow surta efecto."
|
|
82
|
+
"restartNotice": "Reinicia el bot para que el driver whatsmeow surta efecto.",
|
|
83
|
+
"experimentalNotice": "whatsmeow es EXPERIMENTAL: solo sendText y getHistory están implementados. sendImage, sendPoll, groupMetadata y otros métodos lanzan error."
|
|
83
84
|
}
|
|
84
85
|
}
|
package/dist/locales/pt.json
CHANGED
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"connectGaveUp": "Não foi possível conectar após várias tentativas. Verifique sua conexão e tente de novo."
|
|
72
72
|
},
|
|
73
73
|
"whatsmeow": {
|
|
74
|
-
"installPrompt": "Instalar driver whatsmeow para
|
|
74
|
+
"installPrompt": "Instalar driver whatsmeow para suporte a fallback? (EXPERIMENTAL — apenas sendText e histórico funcionam; outros métodos lançam erro)",
|
|
75
75
|
"unsupportedArch": "Driver whatsmeow não disponível para {{os}}-{{arch}}. O bot usará apenas Baileys.",
|
|
76
76
|
"fetchingTag": "Buscando última versão do whatsmeow...",
|
|
77
77
|
"fetchFailed": "Não foi possível acessar o Codeberg. Instalação do whatsmeow ignorada.",
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
"downloadFailed": "Download falhou: {{reason}}",
|
|
80
80
|
"installTitle": "Driver whatsmeow",
|
|
81
81
|
"installed": "Driver whatsmeow instalado em {{path}}",
|
|
82
|
-
"restartNotice": "Reinicie o bot para que o driver whatsmeow entre em efeito."
|
|
82
|
+
"restartNotice": "Reinicie o bot para que o driver whatsmeow entre em efeito.",
|
|
83
|
+
"experimentalNotice": "whatsmeow é EXPERIMENTAL: apenas sendText e getHistory estão implementados. sendImage, sendPoll, groupMetadata e outros métodos lançam erro."
|
|
83
84
|
}
|
|
84
85
|
}
|
package/dist/main.js
CHANGED
|
@@ -11,6 +11,7 @@ process.env.NODE_PATH = path.resolve(process.cwd(), "node_modules");
|
|
|
11
11
|
Module._initPaths();
|
|
12
12
|
import { baileysContract } from "#drivers/baileys/index.js";
|
|
13
13
|
import { whatsmeowContract, startWhatsmeowSupervisor, wrapWithSupervisor } from "#drivers/whatsmeow/index.js";
|
|
14
|
+
import { promptWhatsmeowInstall } from "#drivers/whatsmeow/installer.js";
|
|
14
15
|
import { cleanupPlugins } from "#kernel/pluginLoader.js";
|
|
15
16
|
import { stopAll as stopScheduler } from "#kernel/scheduler.js";
|
|
16
17
|
import { sendAlert } from "#kernel/alerts.js";
|
|
@@ -35,12 +36,20 @@ driverManager.register(baileysContract, { isPrimary: CONFIG.drivers.primary ===
|
|
|
35
36
|
// running on Baileys alone, no fallback.
|
|
36
37
|
let supervisor = null;
|
|
37
38
|
if (CONFIG.drivers.whatsmeow.enabled) {
|
|
39
|
+
logger.info("[driverManager] whatsmeow enabled — spawning supervisor");
|
|
38
40
|
supervisor = await startWhatsmeowSupervisor();
|
|
39
41
|
if (supervisor) {
|
|
40
42
|
const wrapped = wrapWithSupervisor(whatsmeowContract, supervisor);
|
|
41
43
|
driverManager.register(wrapped, { isPrimary: CONFIG.drivers.primary === "whatsmeow" });
|
|
44
|
+
logger.info(`[driverManager] whatsmeow registered (primary=${CONFIG.drivers.primary === "whatsmeow"})`);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
logger.warn("[driverManager] whatsmeow supervisor failed to start — fallback disabled");
|
|
42
48
|
}
|
|
43
49
|
}
|
|
50
|
+
else {
|
|
51
|
+
logger.info("[driverManager] whatsmeow disabled by config — no fallback");
|
|
52
|
+
}
|
|
44
53
|
const activeDriver = driverManager.active();
|
|
45
54
|
const secondaryName = (activeDriver.name === "baileys" ? "whatsmeow" : "baileys");
|
|
46
55
|
const secondaryDriver = driverManager.get(secondaryName);
|
|
@@ -125,6 +134,16 @@ if (process.argv.includes("--getid")) {
|
|
|
125
134
|
process.exit(1);
|
|
126
135
|
});
|
|
127
136
|
}
|
|
137
|
+
else if (process.argv.includes("--install-whatsmeow")) {
|
|
138
|
+
// Re-run the whatsmeow installer outside the normal setup flow.
|
|
139
|
+
// Useful when the initial install failed or the binary was moved.
|
|
140
|
+
promptWhatsmeowInstall()
|
|
141
|
+
.then(() => process.exit(0))
|
|
142
|
+
.catch((err) => {
|
|
143
|
+
logger.error(`--install-whatsmeow failed: ${err.message}`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
});
|
|
146
|
+
}
|
|
128
147
|
else {
|
|
129
148
|
// Start bot
|
|
130
149
|
logger.info(t("bot.initialized"));
|
|
@@ -143,10 +162,14 @@ else {
|
|
|
143
162
|
// here is non-fatal: the primary keeps running, fallback just stays
|
|
144
163
|
// unavailable (sendFallbackGuard's `isReady()` check covers that).
|
|
145
164
|
if (secondaryDriver) {
|
|
165
|
+
logger.info(`[driverManager] connecting secondary "${secondaryName}" in background…`);
|
|
146
166
|
secondaryDriver.connect()
|
|
147
167
|
.then(() => logger.info(`[driverManager] secondary "${secondaryName}" connected — fallback available`))
|
|
148
168
|
.catch((err) => logger.warn(`[driverManager] secondary "${secondaryName}" connect failed: ${err.message} — fallback unavailable`));
|
|
149
169
|
}
|
|
170
|
+
else {
|
|
171
|
+
logger.info(`[driverManager] no secondary driver registered — running on ${activeDriver.name} only`);
|
|
172
|
+
}
|
|
150
173
|
})
|
|
151
174
|
.catch((err) => {
|
|
152
175
|
shutdown(`Failed to connect driver: ${err.message}`, true);
|