@manybot/manybot 5.5.3 → 5.6.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/README.md +15 -1
- package/dist/client/cache.js +1 -1
- package/dist/client/store.js +11 -0
- package/dist/config.js +230 -12
- package/dist/drivers/baileys/adapter.js +556 -0
- package/dist/drivers/{whatsapp → baileys}/api/index.js +533 -386
- package/dist/drivers/baileys/index.js +560 -0
- package/dist/drivers/{whatsapp → baileys}/loginPrompt.js +2 -0
- package/dist/drivers/{whatsapp → baileys}/messageHandler.js +46 -16
- package/dist/drivers/{whatsapp → baileys}/sdk/baileysSock.js +11 -31
- package/dist/drivers/jid.js +31 -0
- package/dist/drivers/types.js +14 -0
- package/dist/drivers/whatsmeow/client.js +203 -0
- package/dist/drivers/whatsmeow/index.js +79 -0
- package/dist/drivers/whatsmeow/installer.js +70 -0
- package/dist/drivers/whatsmeow/supervisor.js +309 -0
- package/dist/drivers/whatsmeow/whatsmeow.proto +64 -0
- package/dist/i18n/index.js +8 -12
- package/dist/kernel/alerts.js +190 -0
- package/dist/kernel/contactAutoSave.js +200 -0
- package/dist/kernel/driverManager.js +117 -0
- package/dist/kernel/pluginApi.js +25 -7
- package/dist/kernel/pluginLoader.js +12 -12
- package/dist/kernel/sendFallbackGuard.js +173 -0
- package/dist/kernel/sendGuard.js +143 -33
- package/dist/kernel/statusServer.js +39 -0
- package/dist/kernel/updateCheck.js +88 -0
- package/dist/kernel/waContract.js +16 -0
- package/dist/locales/en.json +15 -1
- package/dist/locales/es.json +15 -1
- package/dist/locales/pt.json +15 -1
- package/dist/main.js +77 -5
- package/dist/types.js +18 -11
- package/package.json +7 -9
- package/dist/core/adapter.js +0 -12
- package/dist/core/capabilities.js +0 -16
- package/dist/core/types.js +0 -6
- package/dist/drivers/index.js +0 -14
- package/dist/drivers/whatsapp/adapter.js +0 -7
- package/dist/drivers/whatsapp/index.js +0 -382
package/dist/kernel/sendGuard.js
CHANGED
|
@@ -3,41 +3,82 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Anti-detection throttle layer for all outbound sends.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* 1. Global token bucket
|
|
8
|
-
* 2. Per-chat cooldown
|
|
9
|
-
* 3. Human jitter
|
|
6
|
+
* Protections applied before every message:
|
|
7
|
+
* 1. Global token bucket — hard cap on messages/second across all chats
|
|
8
|
+
* 2. Per-chat cooldown — minimum gap between sends to the same chat
|
|
9
|
+
* 3. Human jitter — random delay to break robotic timing patterns
|
|
10
|
+
* 4. Chat-concurrency gate — caps how many different chats the bot can be
|
|
11
|
+
* actively answering at the same time
|
|
12
|
+
* 5. Edit throttle — jittered minimum gap + cap on edits per
|
|
13
|
+
* message, so things like loading animations
|
|
14
|
+
* don't edit on a fixed, bot-like cadence
|
|
15
|
+
*
|
|
16
|
+
* All of the above scale with SECURITY_LEVEL ("low" | "medium" | "high").
|
|
17
|
+
* Higher levels are slower and more conservative — lower risk of WhatsApp's
|
|
18
|
+
* automation detection, at the cost of response speed.
|
|
10
19
|
*
|
|
11
20
|
* Text sends simulate the typing/recording presence indicator before
|
|
12
21
|
* the message arrives, so the chat shows "typing..." realistically.
|
|
13
22
|
*/
|
|
23
|
+
import { CONFIG } from "#config";
|
|
14
24
|
import { logger } from "#logger";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
const PROFILES = {
|
|
26
|
+
low: {
|
|
27
|
+
globalMsgPerSec: 8,
|
|
28
|
+
chatCooldownMs: 100,
|
|
29
|
+
jitterMs: { min: 30, max: 120 },
|
|
30
|
+
concurrency: Infinity,
|
|
31
|
+
editIntervalMs: { min: 800, max: 2000 },
|
|
32
|
+
maxEditsPerMessage: 20,
|
|
33
|
+
typingMaxMs: 2000,
|
|
34
|
+
},
|
|
35
|
+
medium: {
|
|
36
|
+
globalMsgPerSec: 5,
|
|
37
|
+
chatCooldownMs: 150,
|
|
38
|
+
jitterMs: { min: 50, max: 200 },
|
|
39
|
+
concurrency: 2,
|
|
40
|
+
editIntervalMs: { min: 1200, max: 3000 },
|
|
41
|
+
maxEditsPerMessage: 12,
|
|
42
|
+
typingMaxMs: 4000,
|
|
43
|
+
},
|
|
44
|
+
high: {
|
|
45
|
+
globalMsgPerSec: 2,
|
|
46
|
+
chatCooldownMs: 400,
|
|
47
|
+
jitterMs: { min: 150, max: 500 },
|
|
48
|
+
concurrency: 1,
|
|
49
|
+
editIntervalMs: { min: 2000, max: 5000 },
|
|
50
|
+
maxEditsPerMessage: 6,
|
|
51
|
+
typingMaxMs: 8000,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
function currentProfile() {
|
|
55
|
+
const level = CONFIG.SECURITY_LEVEL;
|
|
56
|
+
return PROFILES[level] ?? PROFILES.medium;
|
|
57
|
+
}
|
|
19
58
|
const TYPING_CPS = 90;
|
|
20
|
-
const TYPING_MAX_MS = 2000;
|
|
21
59
|
const MEDIA_INDICATOR_MS = { min: 400, max: 1000 };
|
|
22
60
|
// ── Global token bucket ───────────────────────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
|
|
61
|
+
// Refill rate follows the active profile, re-read on every call so a
|
|
62
|
+
// SECURITY_LEVEL change via reloadConfig() takes effect immediately.
|
|
63
|
+
let tokens = PROFILES.medium.globalMsgPerSec;
|
|
25
64
|
let lastRefill = Date.now();
|
|
26
65
|
function consumeGlobalToken() {
|
|
66
|
+
const rate = currentProfile().globalMsgPerSec;
|
|
67
|
+
const msPerToken = 1000 / rate;
|
|
27
68
|
const now = Date.now();
|
|
28
69
|
const elapsed = now - lastRefill;
|
|
29
|
-
tokens = Math.min(
|
|
70
|
+
tokens = Math.min(rate, tokens + elapsed / msPerToken);
|
|
30
71
|
lastRefill = now;
|
|
31
72
|
if (tokens >= 1) {
|
|
32
73
|
tokens -= 1;
|
|
33
74
|
return 0;
|
|
34
75
|
}
|
|
35
|
-
return Math.ceil((1 - tokens) *
|
|
76
|
+
return Math.ceil((1 - tokens) * msPerToken);
|
|
36
77
|
}
|
|
37
78
|
// ── Per-chat cooldown ─────────────────────────────────────────────────────────
|
|
38
79
|
const lastSentAt = new Map();
|
|
39
80
|
function chatCooldownMs(jid) {
|
|
40
|
-
const wait = (lastSentAt.get(jid) ?? 0) +
|
|
81
|
+
const wait = (lastSentAt.get(jid) ?? 0) + currentProfile().chatCooldownMs - Date.now();
|
|
41
82
|
return wait > 0 ? wait : 0;
|
|
42
83
|
}
|
|
43
84
|
function recordSend(jid) {
|
|
@@ -45,26 +86,100 @@ function recordSend(jid) {
|
|
|
45
86
|
}
|
|
46
87
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
47
88
|
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
48
|
-
function
|
|
49
|
-
return
|
|
89
|
+
function randomBetween(range) {
|
|
90
|
+
return range.min + Math.random() * (range.max - range.min);
|
|
50
91
|
}
|
|
51
92
|
/**
|
|
52
93
|
* How long the typing indicator should appear before sending text.
|
|
94
|
+
* Capped by the active profile's `typingMaxMs` — higher SECURITY_LEVELs
|
|
95
|
+
* tolerate a longer "typing..." for long messages instead of flatlining.
|
|
53
96
|
* @param {string} text
|
|
54
97
|
* @returns {number} ms
|
|
55
98
|
*/
|
|
56
99
|
export function typingDuration(text) {
|
|
57
100
|
if (typeof text !== "string" || text.length === 0)
|
|
58
101
|
return 0;
|
|
59
|
-
return Math.min((text.length / TYPING_CPS) * 1000,
|
|
102
|
+
return Math.min((text.length / TYPING_CPS) * 1000, currentProfile().typingMaxMs);
|
|
60
103
|
}
|
|
61
104
|
/**
|
|
62
|
-
* A human-feeling duration for media "processing" indicator.
|
|
105
|
+
* A human-feeling duration for media "processing" indicator. If a caption
|
|
106
|
+
* is given, adds its own typing time on top (same per-profile cap as
|
|
107
|
+
* typingDuration) so a media message with a long caption doesn't look
|
|
108
|
+
* instant.
|
|
109
|
+
* @param {string} [caption]
|
|
63
110
|
* @returns {number} ms
|
|
64
111
|
*/
|
|
65
|
-
export function mediaDuration() {
|
|
66
|
-
|
|
67
|
-
|
|
112
|
+
export function mediaDuration(caption) {
|
|
113
|
+
const base = randomBetween(MEDIA_INDICATOR_MS);
|
|
114
|
+
return caption ? base + typingDuration(caption) : base;
|
|
115
|
+
}
|
|
116
|
+
// ── Chat-concurrency gate ─────────────────────────────────────────────────────
|
|
117
|
+
// Caps how many DIFFERENT chats can be actively answered at once, globally
|
|
118
|
+
// (not per-chat — messageHandler.ts already serializes a single chat's own
|
|
119
|
+
// messages). high=1 effectively locks the bot to one chat at a time,
|
|
120
|
+
// medium=2, low=unlimited. FIFO queue for anything past the cap.
|
|
121
|
+
let activeSlots = 0;
|
|
122
|
+
const slotWaiters = [];
|
|
123
|
+
function releaseChatSlot() {
|
|
124
|
+
activeSlots--;
|
|
125
|
+
const next = slotWaiters.shift();
|
|
126
|
+
if (next)
|
|
127
|
+
next();
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Acquire a global chat-concurrency slot before processing a message for
|
|
131
|
+
* `jid`. Resolves once a slot is free. Always call the returned release
|
|
132
|
+
* function (e.g. in a `finally`) or the pool leaks.
|
|
133
|
+
* @param {string} jid — kept for logging/debugging, not used for scoping
|
|
134
|
+
* @returns {Promise<() => void>} release function
|
|
135
|
+
*/
|
|
136
|
+
export async function acquireChatSlot(jid) {
|
|
137
|
+
const max = currentProfile().concurrency;
|
|
138
|
+
if (activeSlots < max) {
|
|
139
|
+
activeSlots++;
|
|
140
|
+
return releaseChatSlot;
|
|
141
|
+
}
|
|
142
|
+
logger.debug(`[sendGuard] chat-concurrency gate full — queuing ${jid}`);
|
|
143
|
+
return new Promise(resolve => {
|
|
144
|
+
slotWaiters.push(() => {
|
|
145
|
+
activeSlots++;
|
|
146
|
+
resolve(releaseChatSlot);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const editState = new Map();
|
|
151
|
+
const EDIT_STATE_STALE_MS = 10 * 60 * 1000;
|
|
152
|
+
function cleanupEditState(now) {
|
|
153
|
+
for (const [id, s] of editState) {
|
|
154
|
+
if (now - s.lastEditAt > EDIT_STATE_STALE_MS)
|
|
155
|
+
editState.delete(id);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Waits for a safe edit slot for `messageId`, applying a jittered minimum
|
|
160
|
+
* gap since its last edit. Returns false once the message has hit its
|
|
161
|
+
* per-level edit cap — callers should skip the edit silently in that case.
|
|
162
|
+
* @param {string} messageId
|
|
163
|
+
* @returns {Promise<boolean>} true if the edit may proceed
|
|
164
|
+
*/
|
|
165
|
+
export async function waitForEditSlot(messageId) {
|
|
166
|
+
const now = Date.now();
|
|
167
|
+
cleanupEditState(now);
|
|
168
|
+
const profile = currentProfile();
|
|
169
|
+
const s = editState.get(messageId) ?? { lastEditAt: 0, count: 0 };
|
|
170
|
+
if (s.count >= profile.maxEditsPerMessage) {
|
|
171
|
+
editState.set(messageId, s);
|
|
172
|
+
logger.debug(`[sendGuard] edit cap reached for ${messageId}`);
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
const minGap = randomBetween(profile.editIntervalMs);
|
|
176
|
+
const wait = s.lastEditAt + minGap - Date.now();
|
|
177
|
+
if (wait > 0)
|
|
178
|
+
await sleep(wait);
|
|
179
|
+
s.lastEditAt = Date.now();
|
|
180
|
+
s.count += 1;
|
|
181
|
+
editState.set(messageId, s);
|
|
182
|
+
return true;
|
|
68
183
|
}
|
|
69
184
|
// ── Public API ────────────────────────────────────────────────────────────────
|
|
70
185
|
/**
|
|
@@ -90,30 +205,25 @@ export async function waitForSendSlot(jid, { cooldown = true, jitter = true } =
|
|
|
90
205
|
}
|
|
91
206
|
}
|
|
92
207
|
if (jitter)
|
|
93
|
-
await sleep(
|
|
208
|
+
await sleep(randomBetween(currentProfile().jitterMs));
|
|
94
209
|
recordSend(jid);
|
|
95
210
|
}
|
|
96
211
|
/**
|
|
97
212
|
* Show a presence indicator for `ms` milliseconds, then clear it.
|
|
98
|
-
*
|
|
99
|
-
* errors are swallowed.
|
|
213
|
+
* Best-effort — errors are swallowed.
|
|
100
214
|
*
|
|
101
|
-
* @param {
|
|
215
|
+
* @param {WaContract|null} contract
|
|
102
216
|
* @param {string|null} chatId
|
|
103
217
|
* @param {number} ms
|
|
104
218
|
* @param {"typing"|"recording"} [state="typing"]
|
|
105
219
|
*/
|
|
106
|
-
export async function simulateState(
|
|
107
|
-
if (!
|
|
108
|
-
return;
|
|
109
|
-
if (!adapter.capabilities.has("presence") || !adapter.setPresence)
|
|
220
|
+
export async function simulateState(contract, chatId, ms, state = "typing") {
|
|
221
|
+
if (!contract || !chatId || ms <= 0)
|
|
110
222
|
return;
|
|
111
223
|
try {
|
|
112
|
-
|
|
113
|
-
// collapsed here until a driver needs to distinguish it.
|
|
114
|
-
await adapter.setPresence(chatId, "composing");
|
|
224
|
+
await contract.sendPresenceUpdate(state === "recording" ? "recording" : "composing", chatId);
|
|
115
225
|
await sleep(ms);
|
|
116
|
-
await
|
|
226
|
+
await contract.sendPresenceUpdate("paused", chatId);
|
|
117
227
|
}
|
|
118
228
|
catch (e) {
|
|
119
229
|
logger.debug(`[sendGuard] presence simulation failed (non-fatal): ${e.message}`);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kernel/statusServer.ts
|
|
3
|
+
*
|
|
4
|
+
* Minimal HTTP endpoint exposing the bot's connection state as JSON,
|
|
5
|
+
* for an external status page (or any other consumer) to poll.
|
|
6
|
+
*/
|
|
7
|
+
import http from "http";
|
|
8
|
+
import { logger } from "#logger";
|
|
9
|
+
let status = {
|
|
10
|
+
online: false,
|
|
11
|
+
since: new Date().toISOString(),
|
|
12
|
+
};
|
|
13
|
+
export function setStatus(online, lastError) {
|
|
14
|
+
if (status.online === online)
|
|
15
|
+
return;
|
|
16
|
+
status = {
|
|
17
|
+
online,
|
|
18
|
+
since: new Date().toISOString(),
|
|
19
|
+
...(lastError ? { lastError } : {}),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function getStatus() {
|
|
23
|
+
return status;
|
|
24
|
+
}
|
|
25
|
+
export function startStatusServer(port) {
|
|
26
|
+
const server = http.createServer((req, res) => {
|
|
27
|
+
res.writeHead(200, {
|
|
28
|
+
"Content-Type": "application/json",
|
|
29
|
+
"Access-Control-Allow-Origin": "*",
|
|
30
|
+
});
|
|
31
|
+
res.end(JSON.stringify(getStatus()));
|
|
32
|
+
});
|
|
33
|
+
server.on("error", (err) => {
|
|
34
|
+
logger.error(`[status] Failed to start status server: ${err.message}`);
|
|
35
|
+
});
|
|
36
|
+
server.listen(port, () => {
|
|
37
|
+
logger.info(`[status] JSON endpoint em http://localhost:${port}`);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* updateCheck.ts
|
|
3
|
+
*
|
|
4
|
+
* Compares the locally installed manybot version against the latest
|
|
5
|
+
* published on npm, and fires an "info" alert (via alerts.ts) when a
|
|
6
|
+
* newer version is available. Runs once on startup and then on a
|
|
7
|
+
* schedule — both configurable (UPDATE_CHECK_ENABLED,
|
|
8
|
+
* UPDATE_CHECK_INTERVAL_HOURS).
|
|
9
|
+
*
|
|
10
|
+
* Never throws — a failed check (offline, npm down) is logged at debug
|
|
11
|
+
* level and silently skipped; it'll just try again next cycle.
|
|
12
|
+
*/
|
|
13
|
+
import { readFileSync } from "fs";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
import path from "path";
|
|
16
|
+
import { UPDATE_CHECK_ENABLED, UPDATE_CHECK_INTERVAL_HOURS } from "#config";
|
|
17
|
+
import { sendAlert } from "#kernel/alerts.js";
|
|
18
|
+
import { logger } from "#logger";
|
|
19
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
20
|
+
const __dirname = path.dirname(__filename);
|
|
21
|
+
const pkg = JSON.parse(readFileSync(path.join(__dirname, "../../package.json"), "utf8"));
|
|
22
|
+
const REGISTRY_URL = `https://registry.npmjs.org/${encodeURIComponent(pkg.name)}/latest`;
|
|
23
|
+
/** Naive semver compare — good enough for x.y.z, no pre-release handling. */
|
|
24
|
+
function isNewer(latest, current) {
|
|
25
|
+
const a = latest.split(".").map(Number);
|
|
26
|
+
const b = current.split(".").map(Number);
|
|
27
|
+
for (let i = 0; i < Math.max(a.length, b.length); i++) {
|
|
28
|
+
const x = a[i] ?? 0;
|
|
29
|
+
const y = b[i] ?? 0;
|
|
30
|
+
if (x > y)
|
|
31
|
+
return true;
|
|
32
|
+
if (x < y)
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
let alreadyNotifiedFor = null;
|
|
38
|
+
/**
|
|
39
|
+
* Runs a single check. Safe to call anytime (startup, interval, manual
|
|
40
|
+
* trigger) — never throws.
|
|
41
|
+
*/
|
|
42
|
+
export async function checkForUpdate() {
|
|
43
|
+
if (!UPDATE_CHECK_ENABLED)
|
|
44
|
+
return;
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch(REGISTRY_URL);
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
logger.debug(`[updateCheck] npm registry responded ${res.status}`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const data = await res.json();
|
|
52
|
+
const latest = data.version;
|
|
53
|
+
if (!latest || !isNewer(latest, pkg.version))
|
|
54
|
+
return;
|
|
55
|
+
// Don't re-alert every cycle for the same version once already notified.
|
|
56
|
+
if (alreadyNotifiedFor === latest)
|
|
57
|
+
return;
|
|
58
|
+
alreadyNotifiedFor = latest;
|
|
59
|
+
await sendAlert({
|
|
60
|
+
level: "info",
|
|
61
|
+
title: "Nova versão do manybot disponível",
|
|
62
|
+
message: `Instalada: ${pkg.version} → disponível: ${latest}. Rode "npm install -g ${pkg.name}@${latest}" (ou equivalente) para atualizar.`,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
logger.debug(`[updateCheck] check failed (non-fatal): ${e.message}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
let intervalTimer = null;
|
|
70
|
+
/**
|
|
71
|
+
* Runs one check immediately, then schedules recurring checks every
|
|
72
|
+
* UPDATE_CHECK_INTERVAL_HOURS. Safe to call multiple times — a second
|
|
73
|
+
* call is a no-op while a schedule is already running.
|
|
74
|
+
*/
|
|
75
|
+
export function startUpdateCheckSchedule() {
|
|
76
|
+
if (!UPDATE_CHECK_ENABLED || intervalTimer)
|
|
77
|
+
return;
|
|
78
|
+
checkForUpdate().catch(() => { });
|
|
79
|
+
intervalTimer = setInterval(() => {
|
|
80
|
+
checkForUpdate().catch(() => { });
|
|
81
|
+
}, UPDATE_CHECK_INTERVAL_HOURS * 60 * 60 * 1000);
|
|
82
|
+
}
|
|
83
|
+
export function stopUpdateCheckSchedule() {
|
|
84
|
+
if (!intervalTimer)
|
|
85
|
+
return;
|
|
86
|
+
clearInterval(intervalTimer);
|
|
87
|
+
intervalTimer = null;
|
|
88
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* kernel/waContract.ts
|
|
3
|
+
*
|
|
4
|
+
* Driver-neutral contract that the core (kernel + pluginApi + sendGuard +
|
|
5
|
+
* contactAutoSave + pluginLoader + messageHandler) consumes. Every WhatsApp
|
|
6
|
+
* driver — Baileys today, whatsmeow in a later phase — implements this
|
|
7
|
+
* interface. The core never imports a driver package directly.
|
|
8
|
+
*
|
|
9
|
+
* Event payloads are all driver-neutral: adapters translate incoming
|
|
10
|
+
* driver-specific event shapes (e.g. Baileys WAMessage) into the records
|
|
11
|
+
* declared here before letting the rest of the kernel see them.
|
|
12
|
+
*
|
|
13
|
+
* This file MUST NOT import from `@whiskeysockets/baileys` or any
|
|
14
|
+
* other driver package.
|
|
15
|
+
*/
|
|
16
|
+
export {};
|
package/dist/locales/en.json
CHANGED
|
@@ -31,9 +31,12 @@
|
|
|
31
31
|
"schedulerRegistered": "Schedule registered — plugin \"{{name}}\" → \"{{expression}}\"",
|
|
32
32
|
"downloadJobFailed": "Download job failed — {{message}}",
|
|
33
33
|
"reconnecting": "Reconnecting in {{secs}}s...",
|
|
34
|
+
"reconnectHalted": "Gave up reconnecting after {{attempts}} attempts — call connect() manually to retry. Repeated reconnect loops can make a WhatsApp restriction last longer.",
|
|
34
35
|
"sessionExpired": "Session expired or logged out — removing local session and restarting...",
|
|
35
36
|
"cacheLoaded": "{{count}} chat(s) loaded from cache.",
|
|
36
|
-
"cacheLoadedStale": "{{count}} chat(s) loaded from cache (stale)."
|
|
37
|
+
"cacheLoadedStale": "{{count}} chat(s) loaded from cache (stale).",
|
|
38
|
+
"sendFailedNoFallback": "manybot: no fallback driver available (jid={{jid}}, primary={{driver}})",
|
|
39
|
+
"sendFailedBothDrivers": "manybot: send failed on both drivers (jid={{jid}}, tried={{primary}} then {{secondary}})"
|
|
37
40
|
},
|
|
38
41
|
"errors": {
|
|
39
42
|
"stack": "Stack",
|
|
@@ -66,5 +69,16 @@
|
|
|
66
69
|
"retrying": "Retrying ({{attempt}}/{{max}})...",
|
|
67
70
|
"sessionWiped": "Session discarded, pairing again ({{round}}/{{max}})...",
|
|
68
71
|
"connectGaveUp": "Couldn't connect after several attempts. Check your network and try again."
|
|
72
|
+
},
|
|
73
|
+
"whatsmeow": {
|
|
74
|
+
"installPrompt": "Install whatsmeow driver for better fallback support?",
|
|
75
|
+
"unsupportedArch": "whatsmeow driver not available for {{os}}-{{arch}}. The bot will use Baileys only.",
|
|
76
|
+
"fetchingTag": "Fetching latest whatsmeow release...",
|
|
77
|
+
"fetchFailed": "Could not reach Codeberg. Skipping whatsmeow install.",
|
|
78
|
+
"downloading": "Downloading {{url}}...",
|
|
79
|
+
"downloadFailed": "Download failed: {{reason}}",
|
|
80
|
+
"installTitle": "whatsmeow driver",
|
|
81
|
+
"installed": "whatsmeow driver installed at {{path}}",
|
|
82
|
+
"restartNotice": "Restart the bot for the whatsmeow driver to take effect."
|
|
69
83
|
}
|
|
70
84
|
}
|
package/dist/locales/es.json
CHANGED
|
@@ -31,9 +31,12 @@
|
|
|
31
31
|
"schedulerRegistered": "Programación registrada — plugin \"{{name}}\" → \"{{expression}}\"",
|
|
32
32
|
"downloadJobFailed": "Error en el trabajo de descarga — {{message}}",
|
|
33
33
|
"reconnecting": "Reconectando en {{secs}}s...",
|
|
34
|
+
"reconnectHalted": "Se dejó de reconectar tras {{attempts}} intentos — llama a connect() manualmente para reintentar. Los bucles de reconexión repetidos pueden prolongar una restricción de WhatsApp.",
|
|
34
35
|
"sessionExpired": "Sesión expirada o cerrada — eliminando sesión local y reiniciando...",
|
|
35
36
|
"cacheLoaded": "{{count}} chat(s) cargado(s) desde la caché.",
|
|
36
|
-
"cacheLoadedStale": "{{count}} chat(s) cargado(s) desde la caché (desactualizada)."
|
|
37
|
+
"cacheLoadedStale": "{{count}} chat(s) cargado(s) desde la caché (desactualizada).",
|
|
38
|
+
"sendFailedNoFallback": "manybot: no hay driver de respaldo disponible (jid={{jid}}, primario={{driver}})",
|
|
39
|
+
"sendFailedBothDrivers": "manybot: envío falló en ambos drivers (jid={{jid}}, se probó {{primary}} y luego {{secondary}})"
|
|
37
40
|
},
|
|
38
41
|
"errors": {
|
|
39
42
|
"stack": "Stack",
|
|
@@ -66,5 +69,16 @@
|
|
|
66
69
|
"retrying": "Reintentando ({{attempt}}/{{max}})...",
|
|
67
70
|
"sessionWiped": "Sesión descartada, emparejando de nuevo ({{round}}/{{max}})...",
|
|
68
71
|
"connectGaveUp": "No se pudo conectar tras varios intentos. Revisa tu conexión e intenta de nuevo."
|
|
72
|
+
},
|
|
73
|
+
"whatsmeow": {
|
|
74
|
+
"installPrompt": "¿Instalar el driver whatsmeow para mejor soporte de fallback?",
|
|
75
|
+
"unsupportedArch": "Driver whatsmeow no disponible para {{os}}-{{arch}}. El bot usará solo Baileys.",
|
|
76
|
+
"fetchingTag": "Obteniendo última versión de whatsmeow...",
|
|
77
|
+
"fetchFailed": "No se pudo contactar a Codeberg. Instalación de whatsmeow omitida.",
|
|
78
|
+
"downloading": "Descargando {{url}}...",
|
|
79
|
+
"downloadFailed": "Descarga fallida: {{reason}}",
|
|
80
|
+
"installTitle": "Driver whatsmeow",
|
|
81
|
+
"installed": "Driver whatsmeow instalado en {{path}}",
|
|
82
|
+
"restartNotice": "Reinicia el bot para que el driver whatsmeow surta efecto."
|
|
69
83
|
}
|
|
70
84
|
}
|
package/dist/locales/pt.json
CHANGED
|
@@ -31,9 +31,12 @@
|
|
|
31
31
|
"schedulerRegistered": "Agendamento registrado — plugin \"{{name}}\" → \"{{expression}}\"",
|
|
32
32
|
"downloadJobFailed": "Falha no job de download — {{message}}",
|
|
33
33
|
"reconnecting": "Reconectando em {{secs}}s...",
|
|
34
|
+
"reconnectHalted": "Desisti de reconectar após {{attempts}} tentativas — chame connect() manualmente para tentar de novo. Loops de reconexão repetidos podem prolongar uma restrição do WhatsApp.",
|
|
34
35
|
"sessionExpired": "Sessão expirada ou desconectada — removendo sessão local e reiniciando...",
|
|
35
36
|
"cacheLoaded": "{{count}} conversa(s) carregada(s) do cache.",
|
|
36
|
-
"cacheLoadedStale": "{{count}} conversa(s) carregada(s) do cache (desatualizado)."
|
|
37
|
+
"cacheLoadedStale": "{{count}} conversa(s) carregada(s) do cache (desatualizado).",
|
|
38
|
+
"sendFailedNoFallback": "manybot: nenhum driver de fallback disponível (jid={{jid}}, primário={{driver}})",
|
|
39
|
+
"sendFailedBothDrivers": "manybot: envio falhou nos dois drivers (jid={{jid}}, tentou {{primary}} depois {{secondary}})"
|
|
37
40
|
},
|
|
38
41
|
"errors": {
|
|
39
42
|
"stack": "Stack",
|
|
@@ -66,5 +69,16 @@
|
|
|
66
69
|
"retrying": "Tentando novamente ({{attempt}}/{{max}})...",
|
|
67
70
|
"sessionWiped": "Sessão descartada, tentando parear novamente ({{round}}/{{max}})...",
|
|
68
71
|
"connectGaveUp": "Não foi possível conectar após várias tentativas. Verifique sua conexão e tente de novo."
|
|
72
|
+
},
|
|
73
|
+
"whatsmeow": {
|
|
74
|
+
"installPrompt": "Instalar driver whatsmeow para melhor suporte a fallback?",
|
|
75
|
+
"unsupportedArch": "Driver whatsmeow não disponível para {{os}}-{{arch}}. O bot usará apenas Baileys.",
|
|
76
|
+
"fetchingTag": "Buscando última versão do whatsmeow...",
|
|
77
|
+
"fetchFailed": "Não foi possível acessar o Codeberg. Instalação do whatsmeow ignorada.",
|
|
78
|
+
"downloading": "Baixando {{url}}...",
|
|
79
|
+
"downloadFailed": "Download falhou: {{reason}}",
|
|
80
|
+
"installTitle": "Driver whatsmeow",
|
|
81
|
+
"installed": "Driver whatsmeow instalado em {{path}}",
|
|
82
|
+
"restartNotice": "Reinicie o bot para que o driver whatsmeow entre em efeito."
|
|
69
83
|
}
|
|
70
84
|
}
|
package/dist/main.js
CHANGED
|
@@ -9,19 +9,58 @@ import Module from "module";
|
|
|
9
9
|
import path from "path";
|
|
10
10
|
process.env.NODE_PATH = path.resolve(process.cwd(), "node_modules");
|
|
11
11
|
Module._initPaths();
|
|
12
|
-
import {
|
|
12
|
+
import { baileysContract } from "#drivers/baileys/index.js";
|
|
13
|
+
import { whatsmeowContract, startWhatsmeowSupervisor, wrapWithSupervisor } from "#drivers/whatsmeow/index.js";
|
|
13
14
|
import { cleanupPlugins } from "#kernel/pluginLoader.js";
|
|
14
15
|
import { stopAll as stopScheduler } from "#kernel/scheduler.js";
|
|
16
|
+
import { sendAlert } from "#kernel/alerts.js";
|
|
17
|
+
import { startStatusServer } from "#kernel/statusServer.js";
|
|
18
|
+
import { getDriverManager } from "#kernel/driverManager.js";
|
|
19
|
+
import { CONFIG, STATUS_ENABLED, STATUS_PORT } from "#config";
|
|
15
20
|
import { logger } from "#logger";
|
|
16
21
|
import { t } from "#i18n";
|
|
17
22
|
let shuttingDown = false;
|
|
18
|
-
|
|
23
|
+
// DriverManager registration: only register drivers that are enabled in
|
|
24
|
+
// the config. whatsmeow.enabled = false means no gRPC
|
|
25
|
+
// subprocess, no Go binary lookup, no extra work at boot — the manager
|
|
26
|
+
// simply doesn't know about it and sendFallbackGuard sees a missing
|
|
27
|
+
// secondary and fires send_failed_no_fallback if needed.
|
|
28
|
+
const driverManager = getDriverManager();
|
|
29
|
+
driverManager.register(baileysContract, { isPrimary: CONFIG.drivers.primary === "baileys" });
|
|
30
|
+
// Whatsmeow supervisor: spawns the Go subprocess when enabled=true,
|
|
31
|
+
// owns the restart/backoff/circuit-breaker logic, and gates the
|
|
32
|
+
// driver's connect()/isReady() until HealthCheck{ready:true}. When
|
|
33
|
+
// enabled=false or the binary can't be located, `supervisor` is null
|
|
34
|
+
// and the whatsmeow driver is simply not registered — ManyBot keeps
|
|
35
|
+
// running on Baileys alone, no fallback.
|
|
36
|
+
let supervisor = null;
|
|
37
|
+
if (CONFIG.drivers.whatsmeow.enabled) {
|
|
38
|
+
supervisor = await startWhatsmeowSupervisor();
|
|
39
|
+
if (supervisor) {
|
|
40
|
+
const wrapped = wrapWithSupervisor(whatsmeowContract, supervisor);
|
|
41
|
+
driverManager.register(wrapped, { isPrimary: CONFIG.drivers.primary === "whatsmeow" });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const activeDriver = driverManager.active();
|
|
45
|
+
const secondaryName = (activeDriver.name === "baileys" ? "whatsmeow" : "baileys");
|
|
46
|
+
const secondaryDriver = driverManager.get(secondaryName);
|
|
19
47
|
async function shutdown(reason, isError = false) {
|
|
20
48
|
if (shuttingDown)
|
|
21
49
|
return;
|
|
22
50
|
shuttingDown = true;
|
|
23
51
|
if (isError) {
|
|
24
52
|
logger.error(`${t("bot.error.uncaught")}: ${reason}`);
|
|
53
|
+
try {
|
|
54
|
+
await sendAlert({
|
|
55
|
+
level: "critical",
|
|
56
|
+
title: "manybot crashed",
|
|
57
|
+
message: reason,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// sendAlert already swallows sink failures internally; this is only
|
|
62
|
+
// a final safety net so a crash alert never blocks shutdown itself.
|
|
63
|
+
}
|
|
25
64
|
}
|
|
26
65
|
else {
|
|
27
66
|
logger.warn(t("bot.signal.sigterm", { signal: reason }));
|
|
@@ -34,11 +73,21 @@ async function shutdown(reason, isError = false) {
|
|
|
34
73
|
}
|
|
35
74
|
stopScheduler();
|
|
36
75
|
try {
|
|
37
|
-
await
|
|
76
|
+
await driverManager.shutdown();
|
|
38
77
|
}
|
|
39
78
|
catch (err) {
|
|
40
79
|
logger.error(`Error disconnecting driver: ${err.message}`);
|
|
41
80
|
}
|
|
81
|
+
// Belt-and-suspenders: driverManager.shutdown() should already have
|
|
82
|
+
// disconnected the wrapped contract, which in turn calls
|
|
83
|
+
// supervisor.shutdown(). This catches the case where the supervisor
|
|
84
|
+
// was started but the driver wasn't registered (binary missing).
|
|
85
|
+
if (supervisor) {
|
|
86
|
+
try {
|
|
87
|
+
await supervisor.shutdown();
|
|
88
|
+
}
|
|
89
|
+
catch { }
|
|
90
|
+
}
|
|
42
91
|
process.exit(isError ? 1 : 0);
|
|
43
92
|
}
|
|
44
93
|
// Global error listeners
|
|
@@ -58,11 +107,18 @@ process.on("SIGINT", () => shutdown("SIGINT"));
|
|
|
58
107
|
// the JID to the console, to paste into CHATS in manybot.toml.
|
|
59
108
|
// Does not enter the normal bot flow (plugins are not loaded).
|
|
60
109
|
if (process.argv.includes("--getid")) {
|
|
61
|
-
|
|
110
|
+
// getId? is a Baileys-only diagnostic method. Look it
|
|
111
|
+
// up on the registered Baileys driver regardless of which one is
|
|
112
|
+
// active — --getid always uses Baileys, even in a whatsmeow-primary
|
|
113
|
+
// configuration, because it needs the diagnostic session, not the
|
|
114
|
+
// bot's normal one.
|
|
115
|
+
const baileys = driverManager.get("baileys");
|
|
116
|
+
const getIdFn = baileys?.getId;
|
|
117
|
+
if (!getIdFn) {
|
|
62
118
|
logger.error(`Current driver does not support --getid.`);
|
|
63
119
|
process.exit(1);
|
|
64
120
|
}
|
|
65
|
-
|
|
121
|
+
getIdFn()
|
|
66
122
|
.then(() => process.exit(0))
|
|
67
123
|
.catch((err) => {
|
|
68
124
|
logger.error(`--getid mode failed: ${err.message}`);
|
|
@@ -72,9 +128,25 @@ if (process.argv.includes("--getid")) {
|
|
|
72
128
|
else {
|
|
73
129
|
// Start bot
|
|
74
130
|
logger.info(t("bot.initialized"));
|
|
131
|
+
if (STATUS_ENABLED) {
|
|
132
|
+
startStatusServer(STATUS_PORT);
|
|
133
|
+
}
|
|
75
134
|
activeDriver.connect()
|
|
76
135
|
.then(() => {
|
|
77
136
|
logger.success(t("bot.ready"));
|
|
137
|
+
// The secondary driver is connected and paired in the
|
|
138
|
+
// background so sendFallbackGuard can reach for it without first
|
|
139
|
+
// having to wait through a connect() round-trip when the primary
|
|
140
|
+
// fails. The secondary does NOT register `messages.upsert` handlers
|
|
141
|
+
// (no kernel code subscribes on it — only the primary path does),
|
|
142
|
+
// so connecting it does not duplicate inbound processing. A failure
|
|
143
|
+
// here is non-fatal: the primary keeps running, fallback just stays
|
|
144
|
+
// unavailable (sendFallbackGuard's `isReady()` check covers that).
|
|
145
|
+
if (secondaryDriver) {
|
|
146
|
+
secondaryDriver.connect()
|
|
147
|
+
.then(() => logger.info(`[driverManager] secondary "${secondaryName}" connected — fallback available`))
|
|
148
|
+
.catch((err) => logger.warn(`[driverManager] secondary "${secondaryName}" connect failed: ${err.message} — fallback unavailable`));
|
|
149
|
+
}
|
|
78
150
|
})
|
|
79
151
|
.catch((err) => {
|
|
80
152
|
shutdown(`Failed to connect driver: ${err.message}`, true);
|
package/dist/types.js
CHANGED
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* src/types.ts
|
|
3
3
|
*
|
|
4
|
-
* Shared WhatsApp-facing types
|
|
5
|
-
* tsconfig.json paths / package.json imports).
|
|
6
|
-
* call sites don't need to know whether a type comes straight from
|
|
7
|
-
* Baileys or from ManyBot's own store/adapter layer.
|
|
4
|
+
* Shared WhatsApp-facing types. Imported everywhere as "#types" (see
|
|
5
|
+
* tsconfig.json paths / package.json imports).
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
7
|
+
* IMPORTANT: This file is a thin facade over the Baileys SDK types.
|
|
8
|
+
* The driver-neutral `WaContract` and `BotMessage` types live in
|
|
9
|
+
* `#drivers/types.ts` and `#kernel/waContract.ts`. The core (kernel,
|
|
10
|
+
* pluginApi, pluginLoader, sendGuard, contactAutoSave, messageHandler)
|
|
11
|
+
* is being migrated to consume `WaContract` instead of these aliases.
|
|
12
|
+
* The aliases remain so the few remaining call sites (and the Baileys
|
|
13
|
+
* driver itself) keep compiling while the migration is in progress.
|
|
14
|
+
*
|
|
15
|
+
* After the migration is complete, nothing in the core should import
|
|
16
|
+
* from this file; only the Baileys driver will.
|
|
15
17
|
*/
|
|
16
|
-
|
|
18
|
+
// WAProto has to come in as a namespace (api/index.ts reads WAProto.IContextInfo,
|
|
19
|
+
// WAProto.HistorySync, etc.) — Baileys exports it as `proto`, but we re-export
|
|
20
|
+
// it here under the project's existing WAProto name so call sites stay stable.
|
|
21
|
+
import { proto } from "@whiskeysockets/baileys";
|
|
22
|
+
/** The Baileys-generated proto namespace (re-exported under the WAProto name). */
|
|
23
|
+
export { proto as WAProto };
|