@manybot/manybot 4.4.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
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"name": "SyntaxError!",
|
|
6
6
|
"email": "me@stxerr.dev"
|
|
7
7
|
},
|
|
8
|
-
"version": "
|
|
8
|
+
"version": "5.1.0",
|
|
9
9
|
"license": "GPL-3.0-only",
|
|
10
10
|
"private": false,
|
|
11
11
|
"engines": {
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"node-webpmux": "^3.2.1",
|
|
39
39
|
"qrcode-terminal": "^0.12.0",
|
|
40
|
+
"smol-toml": "^1.7.0",
|
|
40
41
|
"tar": "^7.5.16",
|
|
41
42
|
"whatsapp-web.js": "^1.24.0"
|
|
42
43
|
},
|
|
@@ -44,6 +45,7 @@
|
|
|
44
45
|
"#client/*": "./src/client/*.js",
|
|
45
46
|
"#kernel/*": "./src/kernel/*.js",
|
|
46
47
|
"#manyapi": "./src/kernel/pluginApi.js",
|
|
48
|
+
"#sendguard": "./src/kernel/sendGuard.js",
|
|
47
49
|
"#logger": "./src/logger/logger.js",
|
|
48
50
|
"#utils/*": "./src/utils/*.js",
|
|
49
51
|
"#i18n": "./src/i18n/index.js",
|
|
@@ -20,20 +20,27 @@ import { extract } from "tar";
|
|
|
20
20
|
import { pipeline } from "node:stream/promises";
|
|
21
21
|
import { Readable } from "node:stream";
|
|
22
22
|
import { Transform } from "node:stream";
|
|
23
|
+
const os = process.platform;
|
|
23
24
|
|
|
24
25
|
const __filename = fileURLToPath(import.meta.url);
|
|
25
26
|
const __dirname = path.dirname(__filename);
|
|
26
27
|
|
|
27
28
|
const DATA_PATH = path.join(CONFIG_DIR, "sessions");
|
|
28
29
|
|
|
29
|
-
const
|
|
30
|
+
const chromeDir = path.join(CONFIG_DIR, "chrome");
|
|
31
|
+
|
|
32
|
+
const bins = {
|
|
33
|
+
linux: "chrome",
|
|
34
|
+
win32: "chrome.exe"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const chrome = path.join(chromeDir, bins[os]);
|
|
30
38
|
|
|
31
39
|
if (!fs.existsSync(chrome)) {
|
|
32
40
|
try {
|
|
33
41
|
logger.warn(t("errors.chromeNotFound"));
|
|
34
42
|
const tmpDir = "/tmp/manybot-chrome";
|
|
35
43
|
const tmpPath = path.join(tmpDir, "chrome.tar.gz");
|
|
36
|
-
const os = process.platform;
|
|
37
44
|
const urls = {
|
|
38
45
|
linux: "https://api.manybot.stxerr.dev/download-chrome-linux",
|
|
39
46
|
win32: "https://api.manybot.stxerr.dev/download-chrome-win"
|
|
@@ -41,7 +48,7 @@ if (!fs.existsSync(chrome)) {
|
|
|
41
48
|
|
|
42
49
|
const url = urls[os];
|
|
43
50
|
if (!url) throw new Error(t("errors.OSNotSupported", { os }));
|
|
44
|
-
|
|
51
|
+
|
|
45
52
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
46
53
|
|
|
47
54
|
const res = await fetch(url);
|
|
@@ -77,7 +84,7 @@ if (!fs.existsSync(chrome)) {
|
|
|
77
84
|
);
|
|
78
85
|
} finally {
|
|
79
86
|
clearInterval(spinner);
|
|
80
|
-
process.stderr.write("\r\x1b[2K");
|
|
87
|
+
process.stderr.write("\r\x1b[2K");
|
|
81
88
|
}
|
|
82
89
|
|
|
83
90
|
if (expected && received !== expected) {
|
package/src/config.js
CHANGED
|
@@ -1,178 +1,277 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* config.js
|
|
3
3
|
*
|
|
4
|
-
* Loads:
|
|
4
|
+
* Loads config from up to four files, merged in strict precedence order:
|
|
5
|
+
*
|
|
6
|
+
* defaults < legacy .conf < TOML
|
|
7
|
+
*
|
|
8
|
+
* Legacy files (frozen — no new keys):
|
|
5
9
|
* ~/.manybot/manybot.conf
|
|
6
10
|
* ~/.manybot/manyplug.conf
|
|
7
11
|
*
|
|
8
|
-
*
|
|
12
|
+
* TOML files (all new features go here):
|
|
13
|
+
* ~/.manybot/manybot.toml
|
|
14
|
+
* ~/.manybot/manyplug.toml
|
|
15
|
+
*
|
|
16
|
+
* The final CONFIG object always has the same shape regardless of which
|
|
17
|
+
* files are present. Plugins must never see a structural difference.
|
|
9
18
|
*/
|
|
10
19
|
|
|
11
20
|
import fs from "fs/promises";
|
|
12
21
|
import os from "os";
|
|
13
22
|
import path from "path";
|
|
14
|
-
|
|
23
|
+
import { parse as parseToml } from "smol-toml";
|
|
15
24
|
import { logger } from "#logger";
|
|
16
25
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Paths
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
export const CONFIG_DIR = path.join(os.homedir(), ".manybot");
|
|
31
|
+
|
|
32
|
+
/** @deprecated Use TOML_CONFIG_FILE. Frozen — no new keys. */
|
|
33
|
+
export const CONFIG_FILE = path.join(CONFIG_DIR, "manybot.conf");
|
|
34
|
+
/** @deprecated Use TOML_PLUGIN_FILE. Frozen — no new keys. */
|
|
35
|
+
export const PLUGIN_FILE = path.join(CONFIG_DIR, "manyplug.conf");
|
|
36
|
+
|
|
37
|
+
export const TOML_CONFIG_FILE = path.join(CONFIG_DIR, "manybot.toml");
|
|
38
|
+
export const TOML_PLUGIN_FILE = path.join(CONFIG_DIR, "manyplug.toml");
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// Legacy .conf parser (frozen — do not extend)
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
20
43
|
|
|
21
|
-
/**
|
|
22
|
-
* Converts strings to native JS values.
|
|
23
|
-
*/
|
|
24
44
|
function parseValue(value) {
|
|
25
45
|
value = value.trim();
|
|
26
|
-
|
|
27
46
|
if (
|
|
28
47
|
(value.startsWith('"') && value.endsWith('"')) ||
|
|
29
48
|
(value.startsWith("'") && value.endsWith("'"))
|
|
30
49
|
) {
|
|
31
50
|
value = value.slice(1, -1);
|
|
32
51
|
}
|
|
33
|
-
|
|
34
|
-
if (value === "
|
|
35
|
-
return true;
|
|
36
|
-
|
|
37
|
-
if (value === "false")
|
|
38
|
-
return false;
|
|
39
|
-
|
|
52
|
+
if (value === "true") return true;
|
|
53
|
+
if (value === "false") return false;
|
|
40
54
|
return value;
|
|
41
55
|
}
|
|
42
56
|
|
|
43
|
-
/**
|
|
44
|
-
* Reads comments safely.
|
|
45
|
-
* Ignores # inside quoted strings.
|
|
46
|
-
*/
|
|
47
57
|
function stripInlineComment(line) {
|
|
48
58
|
let result = "";
|
|
49
|
-
let quote
|
|
50
|
-
|
|
59
|
+
let quote = null;
|
|
51
60
|
for (let i = 0; i < line.length; i++) {
|
|
52
61
|
const ch = line[i];
|
|
53
|
-
|
|
54
62
|
if ((ch === '"' || ch === "'") && line[i - 1] !== "\\") {
|
|
55
|
-
if (quote === ch)
|
|
56
|
-
|
|
57
|
-
else if (!quote)
|
|
58
|
-
quote = ch;
|
|
63
|
+
if (quote === ch) quote = null;
|
|
64
|
+
else if (!quote) quote = ch;
|
|
59
65
|
}
|
|
60
|
-
|
|
61
|
-
if (ch === "#" && !quote)
|
|
62
|
-
break;
|
|
63
|
-
|
|
66
|
+
if (ch === "#" && !quote) break;
|
|
64
67
|
result += ch;
|
|
65
68
|
}
|
|
66
|
-
|
|
67
69
|
return result.trim();
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
/**
|
|
71
|
-
* Parses manybot.conf syntax.
|
|
72
|
-
*/
|
|
73
72
|
function parseConf(raw) {
|
|
74
|
-
const lines
|
|
75
|
-
|
|
73
|
+
const lines = raw.split(/\r?\n/);
|
|
76
74
|
const mergedLines = [];
|
|
77
|
-
|
|
78
|
-
let
|
|
79
|
-
let buffer = "";
|
|
75
|
+
let insideList = false;
|
|
76
|
+
let buffer = "";
|
|
80
77
|
|
|
81
78
|
for (let line of lines) {
|
|
82
79
|
line = stripInlineComment(line);
|
|
83
|
-
|
|
84
|
-
if (!line)
|
|
85
|
-
continue;
|
|
80
|
+
if (!line) continue;
|
|
86
81
|
|
|
87
82
|
if (!insideList) {
|
|
88
83
|
if (/=\s*\[$/.test(line)) {
|
|
89
84
|
insideList = true;
|
|
90
|
-
buffer
|
|
85
|
+
buffer = line;
|
|
91
86
|
} else {
|
|
92
87
|
mergedLines.push(line);
|
|
93
88
|
}
|
|
94
89
|
} else {
|
|
95
90
|
buffer += " " + line;
|
|
96
|
-
|
|
97
91
|
if (line.includes("]")) {
|
|
98
92
|
mergedLines.push(buffer);
|
|
99
|
-
buffer
|
|
93
|
+
buffer = "";
|
|
100
94
|
insideList = false;
|
|
101
95
|
}
|
|
102
96
|
}
|
|
103
97
|
}
|
|
104
98
|
|
|
105
99
|
const config = {};
|
|
106
|
-
|
|
107
100
|
for (const line of mergedLines) {
|
|
108
101
|
const idx = line.indexOf("=");
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const key = line.slice(0, idx).trim();
|
|
114
|
-
let value = line.slice(idx + 1).trim();
|
|
102
|
+
if (idx === -1) continue;
|
|
103
|
+
const key = line.slice(0, idx).trim();
|
|
104
|
+
let value = line.slice(idx + 1).trim();
|
|
115
105
|
|
|
116
106
|
if (value.startsWith("[") && value.endsWith("]")) {
|
|
117
107
|
config[key] = value
|
|
118
108
|
.slice(1, -1)
|
|
119
109
|
.split(",")
|
|
120
|
-
.map(v
|
|
110
|
+
.map(v => parseValue(v))
|
|
121
111
|
.filter(v => v !== "");
|
|
122
112
|
continue;
|
|
123
113
|
}
|
|
124
|
-
|
|
125
114
|
config[key] = parseValue(value);
|
|
126
115
|
}
|
|
127
|
-
|
|
128
116
|
return config;
|
|
129
117
|
}
|
|
130
118
|
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// TOML migration
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
function escapeTomlString(s) {
|
|
124
|
+
return s
|
|
125
|
+
.replaceAll("\\", "\\\\")
|
|
126
|
+
.replaceAll('"', '\\"');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function toToml(obj) {
|
|
130
|
+
return Object.entries(obj)
|
|
131
|
+
.map(([k, v]) => {
|
|
132
|
+
if (Array.isArray(v))
|
|
133
|
+
return `${k} = [${v.map(JSON.stringify).join(", ")}]`;
|
|
134
|
+
|
|
135
|
+
if (v == null || v === "")
|
|
136
|
+
return `${k} = ""`;
|
|
137
|
+
|
|
138
|
+
return typeof v === "string"
|
|
139
|
+
? `${k} = "${escapeTomlString(v)}"`
|
|
140
|
+
: `${k} = ${v}`;
|
|
141
|
+
})
|
|
142
|
+
.join("\n") + "\n";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function migrateLegacyIfNeeded() {
|
|
146
|
+
if (await fileExists(TOML_CONFIG_FILE))
|
|
147
|
+
return;
|
|
148
|
+
|
|
149
|
+
const migrate = async (src, dest, omit = []) => {
|
|
150
|
+
const raw = await readFileSafe(src);
|
|
151
|
+
if (!raw) return false;
|
|
152
|
+
|
|
153
|
+
const cfg = parseConf(raw);
|
|
154
|
+
|
|
155
|
+
for (const k of omit)
|
|
156
|
+
delete cfg[k];
|
|
157
|
+
|
|
158
|
+
await fs.writeFile(dest, toToml(cfg), "utf8");
|
|
159
|
+
await fs.rename(src, `${src}.bak`);
|
|
160
|
+
|
|
161
|
+
return true;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const migrated =
|
|
165
|
+
await migrate(CONFIG_FILE, TOML_CONFIG_FILE, ["PLUGINS"]) ||
|
|
166
|
+
await migrate(PLUGIN_FILE, TOML_PLUGIN_FILE);
|
|
167
|
+
|
|
168
|
+
if (migrated)
|
|
169
|
+
logger.success("Config migrated to TOML");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// File helpers
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
async function fileExists(file) {
|
|
177
|
+
try { await fs.stat(file); return true; }
|
|
178
|
+
catch { return false; }
|
|
179
|
+
}
|
|
180
|
+
|
|
131
181
|
async function readFileSafe(file) {
|
|
132
182
|
try {
|
|
133
183
|
return await fs.readFile(file, "utf-8");
|
|
134
184
|
} catch (err) {
|
|
135
|
-
if (err.code !== "ENOENT") {
|
|
136
|
-
|
|
137
|
-
}
|
|
138
|
-
return "";
|
|
185
|
+
if (err.code !== "ENOENT") logger.warn(`Error reading ${file}: ${err.message}`);
|
|
186
|
+
return null;
|
|
139
187
|
}
|
|
140
188
|
}
|
|
141
189
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// Bootstrap: ensure at least one config file exists
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
const DEFAULT_TOML =
|
|
195
|
+
`# ManyBot configuration file
|
|
145
196
|
# See https://manybot.stxerr.dev/docs/config to learn more
|
|
146
197
|
|
|
147
|
-
CLIENT_ID="manybot"
|
|
148
|
-
CMD_PREFIX="!"
|
|
149
|
-
CHATS=[]
|
|
150
|
-
LANGUAGE=en
|
|
151
|
-
PHONE_NUMBER=
|
|
198
|
+
CLIENT_ID = "manybot"
|
|
199
|
+
CMD_PREFIX = "!"
|
|
200
|
+
CHATS = []
|
|
201
|
+
LANGUAGE = "en"
|
|
202
|
+
PHONE_NUMBER = ""
|
|
152
203
|
`;
|
|
153
204
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
205
|
+
await fs.mkdir(CONFIG_DIR, { recursive: true });
|
|
206
|
+
|
|
207
|
+
await migrateLegacyIfNeeded();
|
|
208
|
+
|
|
209
|
+
if (!await fileExists(TOML_CONFIG_FILE)) {
|
|
210
|
+
logger.warn(`Creating ${TOML_CONFIG_FILE}`);
|
|
211
|
+
await fs.writeFile(TOML_CONFIG_FILE, DEFAULT_TOML);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Layer 1 — tOML
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
158
217
|
|
|
159
|
-
|
|
160
|
-
|
|
218
|
+
const legacyLayer = {};
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Layer 2 — TOML (all new features land here)
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
let tomlLayer = {};
|
|
225
|
+
|
|
226
|
+
async function loadToml(file, label) {
|
|
227
|
+
const raw = await readFileSafe(file);
|
|
228
|
+
if (raw === null) return {};
|
|
229
|
+
try {
|
|
230
|
+
return parseToml(raw);
|
|
231
|
+
} catch (err) {
|
|
232
|
+
logger.warn(`Error parsing ${label}: ${err.message}`);
|
|
233
|
+
return {};
|
|
234
|
+
}
|
|
161
235
|
}
|
|
162
236
|
|
|
163
|
-
|
|
164
|
-
|
|
237
|
+
tomlLayer = {
|
|
238
|
+
...await loadToml(TOML_CONFIG_FILE, "manybot.toml"),
|
|
239
|
+
...await loadToml(TOML_PLUGIN_FILE, "manyplug.toml"),
|
|
240
|
+
};
|
|
165
241
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
// Merge + normalize
|
|
244
|
+
//
|
|
245
|
+
// Normalization runs once on the final merged object so both legacy and TOML
|
|
246
|
+
// layers are treated identically. Add new normalization rules here only.
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
const DEFAULTS = {
|
|
250
|
+
CMD_PREFIX: "!",
|
|
251
|
+
CLIENT_ID: "manybot",
|
|
252
|
+
CHATS: [],
|
|
253
|
+
PLUGINS: [],
|
|
254
|
+
LANGUAGE: "en",
|
|
172
255
|
PHONE_NUMBER: null,
|
|
173
|
-
...parseConf(baseConfig + "\n" + pluginConfig),
|
|
174
256
|
};
|
|
175
257
|
|
|
258
|
+
function normalize(cfg) {
|
|
259
|
+
// Empty string and absent PHONE_NUMBER are both treated as null so plugins
|
|
260
|
+
// can always do a simple truthiness check regardless of config source.
|
|
261
|
+
if (cfg.PHONE_NUMBER === "") cfg.PHONE_NUMBER = null;
|
|
262
|
+
return cfg;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export const CONFIG = normalize({
|
|
266
|
+
...DEFAULTS,
|
|
267
|
+
...legacyLayer, // legacy .conf overrides defaults
|
|
268
|
+
...tomlLayer, // TOML overrides legacy .conf
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// Named exports — identical shape regardless of config source
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
176
275
|
export const CLIENT_ID = CONFIG.CLIENT_ID;
|
|
177
276
|
export const CMD_PREFIX = CONFIG.CMD_PREFIX;
|
|
178
277
|
export const CHATS = CONFIG.CHATS;
|
|
@@ -180,11 +279,10 @@ export const PLUGINS = CONFIG.PLUGINS;
|
|
|
180
279
|
export const LANGUAGE = CONFIG.LANGUAGE;
|
|
181
280
|
export const PHONE_NUMBER = CONFIG.PHONE_NUMBER;
|
|
182
281
|
|
|
183
|
-
/**
|
|
184
|
-
* Useful paths for plugins/modules.
|
|
185
|
-
*/
|
|
186
282
|
export const PATHS = {
|
|
187
|
-
HOME:
|
|
283
|
+
HOME: CONFIG_DIR,
|
|
188
284
|
CONFIG_FILE,
|
|
189
|
-
PLUGIN_FILE
|
|
285
|
+
PLUGIN_FILE,
|
|
286
|
+
TOML_CONFIG_FILE,
|
|
287
|
+
TOML_PLUGIN_FILE,
|
|
190
288
|
};
|
|
@@ -6,30 +6,83 @@
|
|
|
6
6
|
* Order:
|
|
7
7
|
* 1. Filter allowed chats (CHATS from .conf)
|
|
8
8
|
* — if CHATS is empty, accepts all chats
|
|
9
|
-
* 2.
|
|
10
|
-
*
|
|
9
|
+
* 2. Per-chat incoming debounce (prevents command spam from
|
|
10
|
+
* saturating the outbound send queue)
|
|
11
|
+
* 3. Log the message
|
|
12
|
+
* 4. Pass context to all active plugins
|
|
11
13
|
*
|
|
12
14
|
* Kernel knows no commands — only distributes.
|
|
13
15
|
* Each plugin decides on its own whether to act or ignore.
|
|
16
|
+
*
|
|
17
|
+
* Per-plugin overrides (via plugin.guardOptions):
|
|
18
|
+
* typing {boolean} — set to `false` to skip the typing indicator
|
|
19
|
+
* and clearState for this plugin. Useful for
|
|
20
|
+
* plugins that reply instantly (e.g. sticker)
|
|
21
|
+
* where the typing state only adds latency.
|
|
22
|
+
*/
|
|
23
|
+
import { CHATS } from "#config";
|
|
24
|
+
import { getChatId } from "#utils/getChatId";
|
|
25
|
+
import { buildApi } from "#manyapi";
|
|
26
|
+
import { pluginRegistry } from "#kernel/pluginLoader";
|
|
27
|
+
import { runPlugin } from "#kernel/pluginGuard";
|
|
28
|
+
import client from "#client/whatsappClient";
|
|
29
|
+
import { logger } from "#logger";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Minimum ms between processing two messages from the same chat.
|
|
33
|
+
* Does NOT drop messages — debounces rapid bursts so plugins
|
|
34
|
+
* aren't invoked faster than the send guard can pace their replies.
|
|
35
|
+
* Set to 0 to disable.
|
|
14
36
|
*/
|
|
37
|
+
const INCOMING_DEBOUNCE_MS = 300;
|
|
15
38
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import { buildApi } from "#manyapi";
|
|
19
|
-
import { pluginRegistry } from "#kernel/pluginLoader";
|
|
20
|
-
import { runPlugin } from "#kernel/pluginGuard";
|
|
21
|
-
import client from "#client/whatsappClient";
|
|
39
|
+
/** chatId → timestamp of last processed message */
|
|
40
|
+
const lastProcessedAt = new Map();
|
|
22
41
|
|
|
23
42
|
export async function handleMessage(msg) {
|
|
24
43
|
const chat = await msg.getChat();
|
|
25
44
|
const chatId = getChatId(chat);
|
|
26
45
|
|
|
27
|
-
if (CHATS.length > 0 && !CHATS.includes(chatId))
|
|
46
|
+
if (CHATS.length > 0 && !CHATS.includes(chatId))
|
|
47
|
+
return;
|
|
28
48
|
|
|
29
|
-
|
|
49
|
+
if (INCOMING_DEBOUNCE_MS > 0) {
|
|
50
|
+
const now = Date.now();
|
|
51
|
+
const last = lastProcessedAt.get(chatId) ?? 0;
|
|
52
|
+
const gap = now - last;
|
|
53
|
+
if (gap < INCOMING_DEBOUNCE_MS) {
|
|
54
|
+
const wait = INCOMING_DEBOUNCE_MS - gap;
|
|
55
|
+
logger.debug(`[messageHandler] ${chatId} delayed ${wait}ms`);
|
|
56
|
+
await new Promise(r => setTimeout(r, wait));
|
|
57
|
+
}
|
|
58
|
+
lastProcessedAt.set(chatId, Date.now());
|
|
59
|
+
}
|
|
30
60
|
|
|
31
61
|
for (const plugin of pluginRegistry.values()) {
|
|
32
|
-
const ctx = {
|
|
33
|
-
|
|
62
|
+
const ctx = buildApi({
|
|
63
|
+
msg,
|
|
64
|
+
chat,
|
|
65
|
+
client,
|
|
66
|
+
pluginRegistry,
|
|
67
|
+
pluginName: plugin.name,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const useTyping = plugin.guardOptions?.typing !== false;
|
|
71
|
+
let typing;
|
|
72
|
+
|
|
73
|
+
if (useTyping) {
|
|
74
|
+
typing = setInterval(() => chat.sendStateTyping(), 4000);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
await runPlugin(plugin, ctx);
|
|
79
|
+
} finally {
|
|
80
|
+
if (useTyping) {
|
|
81
|
+
clearInterval(typing);
|
|
82
|
+
try {
|
|
83
|
+
await chat.clearState();
|
|
84
|
+
} catch {}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
34
87
|
}
|
|
35
88
|
}
|