@manybot/manybot 5.0.0 → 5.1.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/package.json +3 -1
- package/src/client/whatsappClient.js +11 -4
- package/src/config.js +187 -89
- package/src/kernel/messageHandler.js +65 -12
- package/src/kernel/pluginApi.js +180 -74
- package/src/kernel/pluginGuard.js +53 -12
- package/src/kernel/pluginLoader.js +62 -19
- package/src/kernel/sendGuard.js +166 -0
- package/src/logger/logger.js +1 -0
- package/src/main.js +42 -11
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sendGuard.js
|
|
3
|
+
*
|
|
4
|
+
* Anti-detection throttle layer for all outbound sends.
|
|
5
|
+
*
|
|
6
|
+
* Three 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
|
+
*
|
|
11
|
+
* Text sends also simulate the typing indicator so the chat shows
|
|
12
|
+
* "typing…" for a realistic duration before the message arrives.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { logger } from "#logger";
|
|
16
|
+
|
|
17
|
+
// ── Tunables ──────────────────────────────────────────────────────────────────
|
|
18
|
+
// Adjust conservatively — WhatsApp is more sensitive to burst than average rate.
|
|
19
|
+
|
|
20
|
+
/** Hard cap: max messages per second, globally (across all chats). */
|
|
21
|
+
const GLOBAL_MSG_PER_SEC = 3;
|
|
22
|
+
|
|
23
|
+
/** Minimum ms between two sends to the same chat. */
|
|
24
|
+
const CHAT_COOLDOWN_MS = 900;
|
|
25
|
+
|
|
26
|
+
/** Random jitter window added before every send (ms). */
|
|
27
|
+
const JITTER_MS = { min: 400, max: 1400 };
|
|
28
|
+
|
|
29
|
+
/** Typing speed used to calculate indicator duration (chars/sec). */
|
|
30
|
+
const TYPING_CPS = 55;
|
|
31
|
+
|
|
32
|
+
/** Upper cap on typing simulation, regardless of message length. */
|
|
33
|
+
const TYPING_MAX_MS = 4500;
|
|
34
|
+
|
|
35
|
+
/** Fixed indicator duration for media sends (ms), before jitter. */
|
|
36
|
+
const MEDIA_INDICATOR_MS = { min: 800, max: 2000 };
|
|
37
|
+
|
|
38
|
+
// ── Global token bucket ───────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
const MS_PER_TOKEN = 1000 / GLOBAL_MSG_PER_SEC;
|
|
41
|
+
|
|
42
|
+
let tokens = GLOBAL_MSG_PER_SEC;
|
|
43
|
+
let lastRefill = Date.now();
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Consume one send token.
|
|
47
|
+
* Returns the number of ms to wait if no token is available (0 = proceed now).
|
|
48
|
+
*/
|
|
49
|
+
function consumeGlobalToken() {
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
const elapsed = now - lastRefill;
|
|
52
|
+
|
|
53
|
+
tokens = Math.min(GLOBAL_MSG_PER_SEC, tokens + elapsed / MS_PER_TOKEN);
|
|
54
|
+
lastRefill = now;
|
|
55
|
+
|
|
56
|
+
if (tokens >= 1) {
|
|
57
|
+
tokens -= 1;
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return Math.ceil((1 - tokens) * MS_PER_TOKEN);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Per-chat cooldown ─────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
/** chatId → timestamp of the last outbound send */
|
|
67
|
+
const lastSentAt = new Map();
|
|
68
|
+
|
|
69
|
+
function chatCooldownMs(chatId) {
|
|
70
|
+
const last = lastSentAt.get(chatId) ?? 0;
|
|
71
|
+
const wait = last + CHAT_COOLDOWN_MS - Date.now();
|
|
72
|
+
return wait > 0 ? wait : 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function recordSend(chatId) {
|
|
76
|
+
lastSentAt.set(chatId, Date.now());
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
82
|
+
|
|
83
|
+
function randomJitter() {
|
|
84
|
+
return JITTER_MS.min + Math.random() * (JITTER_MS.max - JITTER_MS.min);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* How long the typing indicator should appear before sending text.
|
|
90
|
+
* Based on simulated typing speed, capped at TYPING_MAX_MS.
|
|
91
|
+
* @param {string} text
|
|
92
|
+
* @returns {number} ms
|
|
93
|
+
*/
|
|
94
|
+
export function typingDuration(text) {
|
|
95
|
+
if (typeof text !== "string" || text.length === 0) return 0;
|
|
96
|
+
return Math.min((text.length / TYPING_CPS) * 1000, TYPING_MAX_MS);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A human-feeling duration for media "processing" indicator.
|
|
101
|
+
* Randomized so repeated sends don't have identical pauses.
|
|
102
|
+
* @returns {number} ms
|
|
103
|
+
*/
|
|
104
|
+
export function mediaDuration() {
|
|
105
|
+
return MEDIA_INDICATOR_MS.min
|
|
106
|
+
+ Math.random() * (MEDIA_INDICATOR_MS.max - MEDIA_INDICATOR_MS.min);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Wait for a safe send slot: global rate → per-chat cooldown → jitter.
|
|
113
|
+
* Must be called before every outbound message.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} chatId
|
|
116
|
+
* @param {object} [opts]
|
|
117
|
+
* @param {boolean} [opts.cooldown=true] — set to `false` to skip per-chat cooldown.
|
|
118
|
+
* Use for plugins that reply instantly and
|
|
119
|
+
* don't benefit from anti-detection pacing
|
|
120
|
+
* (e.g. sticker). Global rate limit is kept.
|
|
121
|
+
* @param {boolean} [opts.jitter=true] — set to `false` to skip random jitter delay.
|
|
122
|
+
*/
|
|
123
|
+
export async function waitForSendSlot(chatId, { cooldown = true, jitter = true } = {}) {
|
|
124
|
+
const tokenWait = consumeGlobalToken();
|
|
125
|
+
if (tokenWait > 0) {
|
|
126
|
+
logger.debug(`[sendGuard] global rate hit — queuing ${tokenWait}ms`);
|
|
127
|
+
await sleep(tokenWait);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (cooldown) {
|
|
131
|
+
const coolWait = chatCooldownMs(chatId);
|
|
132
|
+
if (coolWait > 0) {
|
|
133
|
+
logger.debug(`[sendGuard] chat cooldown (${chatId}) — waiting ${coolWait}ms`);
|
|
134
|
+
await sleep(coolWait);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (jitter) {
|
|
139
|
+
await sleep(randomJitter());
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
recordSend(chatId);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Show a WhatsApp presence indicator for `ms` milliseconds, then clear it.
|
|
147
|
+
* Best-effort — errors are swallowed so they never break a send.
|
|
148
|
+
*
|
|
149
|
+
* @param {import("whatsapp-web.js").Chat} chat
|
|
150
|
+
* @param {number} ms
|
|
151
|
+
* @param {"typing"|"recording"} [state="typing"]
|
|
152
|
+
*/
|
|
153
|
+
export async function simulateState(chat, ms, state = "typing") {
|
|
154
|
+
if (!chat || ms <= 0) return;
|
|
155
|
+
try {
|
|
156
|
+
if (state === "recording") {
|
|
157
|
+
await chat.sendStateRecording();
|
|
158
|
+
} else {
|
|
159
|
+
await chat.sendStateTyping();
|
|
160
|
+
}
|
|
161
|
+
await sleep(ms);
|
|
162
|
+
await chat.clearState();
|
|
163
|
+
} catch (err) {
|
|
164
|
+
logger.debug(`[sendGuard] state simulation failed (non-fatal): ${err.message}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
package/src/logger/logger.js
CHANGED
|
@@ -14,6 +14,7 @@ export const logger = {
|
|
|
14
14
|
success: (...a) => console.log(`${c.green }OK ${c.reset}`, ...a),
|
|
15
15
|
warn: (...a) => console.log(`${c.yellow}WARN ${c.reset}`, ...a),
|
|
16
16
|
error: (...a) => console.log(`${c.red }ERROR ${c.reset}`, ...a),
|
|
17
|
+
debug: (...a) => console.log(`${c.blue }DEBUG ${c.reset}`, ...a),
|
|
17
18
|
|
|
18
19
|
cmd: (cmd, extra = "") =>
|
|
19
20
|
console.log(
|
package/src/main.js
CHANGED
|
@@ -16,12 +16,10 @@ Module._initPaths();
|
|
|
16
16
|
import client, { handleQR, handlePairingCode } from "#client/whatsappClient";
|
|
17
17
|
import { handleMessage } from "#kernel/messageHandler";
|
|
18
18
|
import { loadPlugins, setupPlugins } from "#kernel/pluginLoader";
|
|
19
|
-
import { buildSetupApi } from "#manyapi";
|
|
20
19
|
import { logger } from "#logger";
|
|
21
|
-
import { PLUGINS }
|
|
20
|
+
import { PLUGINS, CLIENT_ID } from "#config";
|
|
22
21
|
import { t } from "#i18n";
|
|
23
22
|
import { printBanner } from "#client/banner";
|
|
24
|
-
import { CLIENT_ID } from "#config";
|
|
25
23
|
|
|
26
24
|
logger.info(t("bot.starting"));
|
|
27
25
|
|
|
@@ -37,10 +35,27 @@ process.on("unhandledRejection", (reason) => {
|
|
|
37
35
|
});
|
|
38
36
|
|
|
39
37
|
// Clean shutdown
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
let shuttingDown = false;
|
|
39
|
+
async function shutdown(signal) {
|
|
40
|
+
if (shuttingDown)
|
|
41
|
+
return;
|
|
42
|
+
|
|
43
|
+
shuttingDown = true;
|
|
44
|
+
logger.warn(
|
|
45
|
+
t("bot.signal.sigterm", {
|
|
46
|
+
signal
|
|
47
|
+
})
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await client.destroy();
|
|
52
|
+
} catch {}
|
|
53
|
+
|
|
42
54
|
process.exit(0);
|
|
43
|
-
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
58
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
44
59
|
|
|
45
60
|
let state = "BOOT";
|
|
46
61
|
// BOOT → AUTH → SYNC → READY
|
|
@@ -49,6 +64,10 @@ function setState(next) {
|
|
|
49
64
|
state = next;
|
|
50
65
|
}
|
|
51
66
|
|
|
67
|
+
client.on("authenticated", () => {
|
|
68
|
+
setState("AUTH");
|
|
69
|
+
});
|
|
70
|
+
|
|
52
71
|
client.on("loading_screen", (p, msg) => {
|
|
53
72
|
setState("SYNC");
|
|
54
73
|
logger.info(`loading ${p}% ${msg}`);
|
|
@@ -63,7 +82,7 @@ client.on("ready", async () => {
|
|
|
63
82
|
printBanner();
|
|
64
83
|
|
|
65
84
|
await loadPlugins(PLUGINS);
|
|
66
|
-
await setupPlugins(
|
|
85
|
+
await setupPlugins(client);
|
|
67
86
|
|
|
68
87
|
// buffer anti-replay / sync ghost messages
|
|
69
88
|
setTimeout(() => {
|
|
@@ -79,17 +98,29 @@ client.on("message_create", async (msg) => {
|
|
|
79
98
|
try {
|
|
80
99
|
await handleMessage(msg);
|
|
81
100
|
} catch (err) {
|
|
82
|
-
logger.error(
|
|
101
|
+
logger.error(
|
|
102
|
+
`${err.message}\n${err.stack}`
|
|
103
|
+
);
|
|
83
104
|
}
|
|
84
105
|
});
|
|
85
106
|
|
|
86
107
|
client.on("disconnected", (reason) => {
|
|
87
|
-
|
|
88
|
-
logger.
|
|
108
|
+
|
|
109
|
+
logger.warn(
|
|
110
|
+
t("system.disconnected", { reason })
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
String(reason)
|
|
115
|
+
.includes("LOGOUT")
|
|
116
|
+
) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
89
120
|
setTimeout(() => {
|
|
90
|
-
logger.info(t("system.reinitializing"));
|
|
91
121
|
client.initialize();
|
|
92
122
|
}, 5000);
|
|
123
|
+
|
|
93
124
|
});
|
|
94
125
|
|
|
95
126
|
// -- Events ----------------------------------------------------
|