levix-bot 3.3.0-rc.2 → 4.0.0-beta
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 +11 -7
- package/app.cjs +42 -45
- package/package.json +9 -1
- package/public/dashboard/assets/index-C1b7ijfu.js +391 -0
- package/public/dashboard/assets/index-pYaivHON.css +1 -0
- package/public/dashboard/index.html +19 -0
- package/public/dashboard.css +391 -66
- package/public/dashboard.js +593 -42
- package/scheduler.cjs +9 -14
- package/src/auth/use-database-auth-state.js +2 -4
- package/src/bootstrap/core.js +2 -5
- package/src/bootstrap/panel.js +10 -5
- package/src/cli.js +3 -5
- package/src/commands/autoschedule.cjs +1 -4
- package/src/commands/blacklist.cjs +22 -20
- package/src/commands/block.cjs +2 -4
- package/src/commands/calc.cjs +9 -16
- package/src/commands/digit.cjs +97 -0
- package/src/commands/gemini.cjs +147 -161
- package/src/commands/group/add.cjs +7 -16
- package/src/commands/group/antiSpam.cjs +5 -11
- package/src/commands/group/antilink.cjs +34 -28
- package/src/commands/group/approveall.cjs +3 -11
- package/src/commands/group/clearwarns.cjs +2 -5
- package/src/commands/group/demote.cjs +3 -8
- package/src/commands/group/kick.cjs +12 -15
- package/src/commands/group/media.cjs +17 -24
- package/src/commands/group/members.cjs +1 -2
- package/src/commands/group/promote.cjs +4 -12
- package/src/commands/group/removeall.cjs +2 -5
- package/src/commands/group/setpp.cjs +1 -1
- package/src/commands/group/setrules.cjs +2 -8
- package/src/commands/group/setwarn.cjs +4 -2
- package/src/commands/group/tagadmins.cjs +2 -5
- package/src/commands/group/warn.cjs +26 -24
- package/src/commands/group/warns.cjs +1 -2
- package/src/commands/group/welcome.cjs +13 -17
- package/src/commands/group.cjs +7 -10
- package/src/commands/help.cjs +6 -12
- package/src/commands/lang.cjs +104 -0
- package/src/commands/listschedules.cjs +2 -6
- package/src/commands/loop.cjs +6 -13
- package/src/commands/memory.cjs +19 -32
- package/src/commands/mod.cjs +97 -35
- package/src/commands/perm.cjs +18 -56
- package/src/commands/ping.cjs +1 -5
- package/src/commands/poll.cjs +1 -1
- package/src/commands/rand.cjs +2 -5
- package/src/commands/schedule.cjs +4 -8
- package/src/commands/score.cjs +8 -2
- package/src/commands/setprefix.cjs +2 -2
- package/src/commands/shortlink.cjs +7 -15
- package/src/commands/shutdown.cjs +1 -1
- package/src/commands/sticker.cjs +222 -0
- package/src/commands/stt.cjs +136 -68
- package/src/commands/todo.cjs +9 -15
- package/src/commands/tts.cjs +10 -16
- package/src/commands/unblock.cjs +2 -6
- package/src/commands/weather.cjs +5 -12
- package/src/config/ai-persona.md +5 -3
- package/src/config/available-models.json +1156 -0
- package/src/config/baileys.config.js +2 -1
- package/src/config/brand.esm.js +1 -10
- package/src/config/constants.js +1 -1
- package/src/config/defaults.cjs +66 -66
- package/src/config/lock.cjs +2 -2
- package/src/config/modelCapabilities.cjs +172 -0
- package/src/config/runtime-config.cjs +8 -13
- package/src/config/secrets.cjs +18 -10
- package/src/config/settings.cjs +94 -10
- package/src/config/vault.cjs +107 -0
- package/src/core/connection.js +13 -12
- package/src/core/events.js +41 -23
- package/src/core/proxy.js +7 -7
- package/src/core/session.js +91 -48
- package/src/core/socket.js +12 -15
- package/src/db/db.cjs +14 -6
- package/src/db/store.cjs +221 -87
- package/src/domain/caddy.js +1 -1
- package/src/domain/command.js +40 -19
- package/src/domain/detect.js +1 -1
- package/src/domain/system.js +2 -5
- package/src/handlers/command.handler.js +22 -43
- package/src/handlers/group.handler.js +13 -13
- package/src/handlers/message.handler.js +11 -21
- package/src/index.js +8 -4
- package/src/middleware/antispam.middleware.js +46 -23
- package/src/middleware/blacklist.middleware.js +14 -9
- package/src/middleware/forward-tracking.middleware.js +3 -2
- package/src/middleware/permissions.middleware.js +7 -10
- package/src/panel/login-throttle.cjs +43 -0
- package/src/panel/session-auth.cjs +17 -0
- package/src/routes/dashboard.api.esm.js +394 -72
- package/src/services/aiAgent.cjs +136 -49
- package/src/services/aiProviders.cjs +193 -47
- package/src/services/aiRouter.cjs +137 -0
- package/src/services/aiTools.cjs +305 -125
- package/src/services/llmDiscovery.cjs +481 -0
- package/src/services/modelRegistry.cjs +412 -0
- package/src/utils/datetime.cjs +2 -2
- package/src/utils/lid-helper.esm.js +7 -7
- package/src/utils/logger.cjs +1 -1
- package/src/utils/markdownParser.cjs +159 -0
- package/src/utils/memory.cjs +43 -13
- package/src/utils/messageContent.cjs +40 -0
- package/src/utils/normalizeJid.esm.js +7 -7
- package/src/utils/numerals.cjs +75 -0
- package/src/utils/permissions.esm.js +10 -15
- package/src/utils/providerUrl.cjs +281 -0
- package/src/utils/recurrence.cjs +7 -11
- package/src/utils/sendBotMessage.esm.js +4 -14
- package/src/utils/statusMessage.cjs +5 -13
- package/src/utils/storage.cjs +10 -0
- package/src/utils/storage.esm.js +11 -0
- package/src/utils/textDecode.cjs +4 -9
- package/src/utils/thumbnail.cjs +8 -14
- package/src/utils/typing.esm.js +12 -12
- package/views/dashboard.ejs +134 -106
package/src/domain/command.js
CHANGED
|
@@ -7,13 +7,12 @@
|
|
|
7
7
|
//
|
|
8
8
|
// The bot itself never needs root. This command may, and says so when it does.
|
|
9
9
|
|
|
10
|
-
import readline from "node:readline/promises";
|
|
11
10
|
import { createRequire } from "node:module";
|
|
12
|
-
|
|
13
|
-
import { createSystem } from "./system.js";
|
|
14
|
-
import { inspect, classify, checkDns } from "./detect.js";
|
|
15
|
-
import * as nginx from "./nginx.js";
|
|
11
|
+
import readline from "node:readline/promises";
|
|
16
12
|
import * as caddy from "./caddy.js";
|
|
13
|
+
import { checkDns, classify, inspect } from "./detect.js";
|
|
14
|
+
import * as nginx from "./nginx.js";
|
|
15
|
+
import { createSystem } from "./system.js";
|
|
17
16
|
|
|
18
17
|
const require = createRequire(import.meta.url);
|
|
19
18
|
const settings = require("../config/settings.cjs");
|
|
@@ -29,15 +28,15 @@ const DOMAIN_PATTERN = /^(?!-)[a-z0-9-]{1,63}(?<!-)(\.(?!-)[a-z0-9-]{1,63}(?<!-)
|
|
|
29
28
|
|
|
30
29
|
/** Short, reliable guidance per panel. No panel APIs — just where to click. */
|
|
31
30
|
const PANEL_HINTS = {
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
CloudPanel: 'Sites → Add Site → Reverse Proxy, then "SSL/TLS → Let\'s Encrypt".',
|
|
32
|
+
Plesk: "Domains → Hosting & DNS → Apache & nginx Settings → additional nginx directives.",
|
|
34
33
|
"cPanel / WHM": "WHM → Apache → Reverse Proxy (or ProxyPass in an include file).",
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
CyberPanel: "Websites → Manage → vHost Conf, then Issue SSL.",
|
|
35
|
+
aaPanel: "Website → Settings → Reverse proxy, then Let's Encrypt under SSL.",
|
|
36
|
+
HestiaCP: "Web → Edit domain → Proxy Template, then enable Let's Encrypt.",
|
|
37
|
+
ISPConfig: "Sites → Options → nginx Directives.",
|
|
38
|
+
Coolify: "Add a service pointing at the host port, and let Coolify terminate TLS.",
|
|
39
|
+
Dokploy: "Create an application/domain entry pointing at the host port.",
|
|
41
40
|
"Virtualmin / Webmin": "Services → Configure Website → Edit Directives.",
|
|
42
41
|
};
|
|
43
42
|
|
|
@@ -108,7 +107,11 @@ export async function runDomainCommand({ domain = null, assumeYes = false, syste
|
|
|
108
107
|
cross("No domain given. Try: levix domain bot.example.com");
|
|
109
108
|
return 1;
|
|
110
109
|
}
|
|
111
|
-
name = name
|
|
110
|
+
name = name
|
|
111
|
+
.trim()
|
|
112
|
+
.toLowerCase()
|
|
113
|
+
.replace(/^https?:\/\//, "")
|
|
114
|
+
.replace(/\/.*$/, "");
|
|
112
115
|
|
|
113
116
|
if (!DOMAIN_PATTERN.test(name)) {
|
|
114
117
|
cross(`"${name}" doesn't look like a domain name.`);
|
|
@@ -189,7 +192,9 @@ function printFindings(report) {
|
|
|
189
192
|
line(` Found: ${found.length ? found.join(", ") : "no web server"}`);
|
|
190
193
|
for (const port of [80, 443]) {
|
|
191
194
|
const state = report.ports[port];
|
|
192
|
-
line(
|
|
195
|
+
line(
|
|
196
|
+
` Port ${port}: ${state.taken ? `in use by ${state.process || "an unknown process"}` : "free"}`,
|
|
197
|
+
);
|
|
193
198
|
}
|
|
194
199
|
line();
|
|
195
200
|
}
|
|
@@ -223,7 +228,7 @@ async function confirmDns(sys, name, { assumeYes }) {
|
|
|
223
228
|
cross(
|
|
224
229
|
dnsCheck.status === "unresolved"
|
|
225
230
|
? `${name} does not resolve yet.`
|
|
226
|
-
: `${name} resolves to ${dnsCheck.resolved.v4.join(", ") || dnsCheck.resolved.v6.join(", ")}, not to this server
|
|
231
|
+
: `${name} resolves to ${dnsCheck.resolved.v4.join(", ") || dnsCheck.resolved.v6.join(", ")}, not to this server.`,
|
|
227
232
|
);
|
|
228
233
|
line();
|
|
229
234
|
line(" Add this DNS record:");
|
|
@@ -264,7 +269,12 @@ async function configureNginx(sys, { name, port, report, assumeYes }) {
|
|
|
264
269
|
if (!applied.ok) {
|
|
265
270
|
cross("nginx rejected the configuration — nothing was reloaded.");
|
|
266
271
|
line();
|
|
267
|
-
line(
|
|
272
|
+
line(
|
|
273
|
+
applied.error
|
|
274
|
+
.split("\n")
|
|
275
|
+
.map((l) => ` ${l}`)
|
|
276
|
+
.join("\n"),
|
|
277
|
+
);
|
|
268
278
|
line();
|
|
269
279
|
line(" The previous state has been restored.");
|
|
270
280
|
return 1;
|
|
@@ -284,7 +294,13 @@ async function configureNginx(sys, { name, port, report, assumeYes }) {
|
|
|
284
294
|
} else {
|
|
285
295
|
cross("certbot failed — the site is up over HTTP.");
|
|
286
296
|
line();
|
|
287
|
-
line(
|
|
297
|
+
line(
|
|
298
|
+
cert.error
|
|
299
|
+
.split("\n")
|
|
300
|
+
.slice(0, 8)
|
|
301
|
+
.map((l) => ` ${l}`)
|
|
302
|
+
.join("\n"),
|
|
303
|
+
);
|
|
288
304
|
line();
|
|
289
305
|
}
|
|
290
306
|
}
|
|
@@ -348,7 +364,12 @@ async function configureCaddy(sys, { name, port, report, assumeYes, install }) {
|
|
|
348
364
|
if (!applied.ok) {
|
|
349
365
|
cross("Caddy rejected the configuration — nothing was reloaded.");
|
|
350
366
|
line();
|
|
351
|
-
line(
|
|
367
|
+
line(
|
|
368
|
+
applied.error
|
|
369
|
+
.split("\n")
|
|
370
|
+
.map((l) => ` ${l}`)
|
|
371
|
+
.join("\n"),
|
|
372
|
+
);
|
|
352
373
|
line();
|
|
353
374
|
return 1;
|
|
354
375
|
}
|
package/src/domain/detect.js
CHANGED
package/src/domain/system.js
CHANGED
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
// The default implementation is the real one. Tests pass their own.
|
|
12
12
|
|
|
13
13
|
import { spawnSync } from "node:child_process";
|
|
14
|
-
import fs from "node:fs";
|
|
15
14
|
import dns from "node:dns/promises";
|
|
15
|
+
import fs from "node:fs";
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* @typedef {object} System
|
|
@@ -135,10 +135,7 @@ export function parseListeners(output) {
|
|
|
135
135
|
if (!Number.isFinite(port)) continue;
|
|
136
136
|
|
|
137
137
|
// users:(("nginx",pid=123,fd=6)) | 123/nginx: master
|
|
138
|
-
const owner =
|
|
139
|
-
/users:\(\("([^"]+)"/.exec(line)?.[1] ||
|
|
140
|
-
/\d+\/([\w.-]+)/.exec(line)?.[1] ||
|
|
141
|
-
null;
|
|
138
|
+
const owner = /users:\(\("([^"]+)"/.exec(line)?.[1] || /\d+\/([\w.-]+)/.exec(line)?.[1] || null;
|
|
142
139
|
|
|
143
140
|
found.push({ port, process: owner });
|
|
144
141
|
}
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
import { readdirSync } from "fs";
|
|
2
|
-
import { join, dirname } from "path";
|
|
3
|
-
import { fileURLToPath } from "url";
|
|
4
2
|
import { createRequire } from "module";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} from "../middleware/permissions.middleware.js";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
import { checkBotAdmin, checkCommandPermission } from "../middleware/permissions.middleware.js";
|
|
9
6
|
import { sendBotMessage } from "../utils/sendBotMessage.esm.js";
|
|
10
7
|
|
|
11
8
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -65,7 +62,7 @@ function wrapSockWithReply(sock, originalMsg, editInPlace = false) {
|
|
|
65
62
|
return new Proxy(sock, {
|
|
66
63
|
get(target, prop, receiver) {
|
|
67
64
|
if (prop === "sendMessage") {
|
|
68
|
-
return async
|
|
65
|
+
return async (jid, content, options) => {
|
|
69
66
|
// Try to edit the trigger message in place for the first eligible
|
|
70
67
|
// text reply. We can only edit messages we sent ourselves.
|
|
71
68
|
const eligibleForEdit =
|
|
@@ -109,16 +106,13 @@ function wrapSockWithReply(sock, originalMsg, editInPlace = false) {
|
|
|
109
106
|
* override when there is one, otherwise what the command file declared.
|
|
110
107
|
*/
|
|
111
108
|
function indexAliases(command) {
|
|
112
|
-
const aliases = runtimeConfig.getAliases(
|
|
113
|
-
command.name,
|
|
114
|
-
command.__declaredAliases || []
|
|
115
|
-
);
|
|
109
|
+
const aliases = runtimeConfig.getAliases(command.name, command.__declaredAliases || []);
|
|
116
110
|
|
|
117
111
|
for (const alias of aliases) {
|
|
118
112
|
const existing = commands.get(alias);
|
|
119
113
|
if (existing && existing.name !== command.name) {
|
|
120
114
|
logger.warn(
|
|
121
|
-
`[Commands] Alias conflict: ${alias} already maps to ${existing.name}, ignoring for ${command.name}
|
|
115
|
+
`[Commands] Alias conflict: ${alias} already maps to ${existing.name}, ignoring for ${command.name}`,
|
|
122
116
|
);
|
|
123
117
|
continue;
|
|
124
118
|
}
|
|
@@ -141,9 +135,7 @@ export function rebuildCommandIndex() {
|
|
|
141
135
|
for (const command of commandList.values()) {
|
|
142
136
|
indexAliases(command);
|
|
143
137
|
}
|
|
144
|
-
logger.info(
|
|
145
|
-
`[Commands] Index rebuilt: ${commandList.size} commands, ${commands.size} entries`
|
|
146
|
-
);
|
|
138
|
+
logger.info(`[Commands] Index rebuilt: ${commandList.size} commands, ${commands.size} entries`);
|
|
147
139
|
}
|
|
148
140
|
|
|
149
141
|
/**
|
|
@@ -201,9 +193,7 @@ export async function loadCommands() {
|
|
|
201
193
|
if (entry.isDirectory()) {
|
|
202
194
|
// Load commands from subdirectories (e.g., group/)
|
|
203
195
|
const subDirPath = join(commandsPath, entry.name);
|
|
204
|
-
const subFiles = readdirSync(subDirPath).filter((file) =>
|
|
205
|
-
file.endsWith(".cjs")
|
|
206
|
-
);
|
|
196
|
+
const subFiles = readdirSync(subDirPath).filter((file) => file.endsWith(".cjs"));
|
|
207
197
|
|
|
208
198
|
for (const file of subFiles) {
|
|
209
199
|
registerCommandFile(join(subDirPath, file), entry.name);
|
|
@@ -215,7 +205,7 @@ export async function loadCommands() {
|
|
|
215
205
|
}
|
|
216
206
|
|
|
217
207
|
logger.info(
|
|
218
|
-
`[Commands] Loaded ${commandList.size} commands (${commands.size} entries with aliases)
|
|
208
|
+
`[Commands] Loaded ${commandList.size} commands (${commands.size} entries with aliases)`,
|
|
219
209
|
);
|
|
220
210
|
}
|
|
221
211
|
|
|
@@ -252,13 +242,11 @@ function registerCommand(command, label, category) {
|
|
|
252
242
|
command.__category = category || "general";
|
|
253
243
|
// What the FILE declares, kept aside so a dashboard override can be
|
|
254
244
|
// cleared back to it later (command.aliases itself is what we index).
|
|
255
|
-
command.__declaredAliases = Array.isArray(command.aliases)
|
|
256
|
-
? [...command.aliases]
|
|
257
|
-
: [];
|
|
245
|
+
command.__declaredAliases = Array.isArray(command.aliases) ? [...command.aliases] : [];
|
|
258
246
|
commands.set(command.name, command);
|
|
259
247
|
commandList.set(command.name, command);
|
|
260
248
|
logger.info(
|
|
261
|
-
`[Commands] Loaded command: ${command.name}${category ? ` (from ${category}/)` : ""}
|
|
249
|
+
`[Commands] Loaded command: ${command.name}${category ? ` (from ${category}/)` : ""}`,
|
|
262
250
|
);
|
|
263
251
|
|
|
264
252
|
indexAliases(command);
|
|
@@ -295,7 +283,7 @@ export async function handleCommand(sock, msg, body) {
|
|
|
295
283
|
sock,
|
|
296
284
|
msg.key.remoteJid,
|
|
297
285
|
{ text: "⛔ الأمر ده متوقف حاليًا." },
|
|
298
|
-
{ replyTo: msg }
|
|
286
|
+
{ replyTo: msg },
|
|
299
287
|
);
|
|
300
288
|
return true;
|
|
301
289
|
}
|
|
@@ -314,7 +302,7 @@ export async function handleCommand(sock, msg, body) {
|
|
|
314
302
|
sock,
|
|
315
303
|
msg.key.remoteJid,
|
|
316
304
|
{ text: "⚠️ هذا الأمر يعمل في المجموعات فقط." },
|
|
317
|
-
{ replyTo: msg }
|
|
305
|
+
{ replyTo: msg },
|
|
318
306
|
);
|
|
319
307
|
return true;
|
|
320
308
|
}
|
|
@@ -323,7 +311,7 @@ export async function handleCommand(sock, msg, body) {
|
|
|
323
311
|
sock,
|
|
324
312
|
msg.key.remoteJid,
|
|
325
313
|
{ text: "⚠️ هذا الأمر يعمل في المحادثات الخاصة فقط." },
|
|
326
|
-
{ replyTo: msg }
|
|
314
|
+
{ replyTo: msg },
|
|
327
315
|
);
|
|
328
316
|
return true;
|
|
329
317
|
}
|
|
@@ -331,22 +319,16 @@ export async function handleCommand(sock, msg, body) {
|
|
|
331
319
|
// Group sub-commands are keyed "group:<name>" everywhere — defaults.cjs,
|
|
332
320
|
// the dashboard, and `!group kick`. Use the same key here so running
|
|
333
321
|
// `!kick` directly is gated exactly like running `!group kick`.
|
|
334
|
-
const permissionKey =
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const permissionCheck = checkCommandPermission(
|
|
338
|
-
permissionKey,
|
|
339
|
-
msg,
|
|
340
|
-
groupMetadata,
|
|
341
|
-
sock
|
|
342
|
-
);
|
|
322
|
+
const permissionKey = command.__category === "group" ? `group:${command.name}` : command.name;
|
|
323
|
+
|
|
324
|
+
const permissionCheck = checkCommandPermission(permissionKey, msg, groupMetadata, sock);
|
|
343
325
|
|
|
344
326
|
if (!permissionCheck.hasPermission) {
|
|
345
327
|
await sendBotMessage(
|
|
346
328
|
sock,
|
|
347
329
|
msg.key.remoteJid,
|
|
348
330
|
{ text: permissionCheck.reason },
|
|
349
|
-
{ replyTo: msg }
|
|
331
|
+
{ replyTo: msg },
|
|
350
332
|
);
|
|
351
333
|
return true;
|
|
352
334
|
}
|
|
@@ -356,7 +338,7 @@ export async function handleCommand(sock, msg, body) {
|
|
|
356
338
|
sock,
|
|
357
339
|
msg.key.remoteJid,
|
|
358
340
|
{ text: "⚠️ هذا الأمر يتطلب أن تكون مشرفًا." },
|
|
359
|
-
{ replyTo: msg }
|
|
341
|
+
{ replyTo: msg },
|
|
360
342
|
);
|
|
361
343
|
return true;
|
|
362
344
|
}
|
|
@@ -366,7 +348,7 @@ export async function handleCommand(sock, msg, body) {
|
|
|
366
348
|
sock,
|
|
367
349
|
msg.key.remoteJid,
|
|
368
350
|
{ text: "⚠️ يجب أن أكون مشرفًا لتنفيذ هذا الأمر." },
|
|
369
|
-
{ replyTo: msg }
|
|
351
|
+
{ replyTo: msg },
|
|
370
352
|
);
|
|
371
353
|
return true;
|
|
372
354
|
}
|
|
@@ -392,10 +374,7 @@ export async function handleCommand(sock, msg, body) {
|
|
|
392
374
|
await command.execute(proxiedSock, msg, args, body, groupMetadata, ctx);
|
|
393
375
|
return true;
|
|
394
376
|
} catch (error) {
|
|
395
|
-
logger.error(
|
|
396
|
-
{ err: error, command: invokedName },
|
|
397
|
-
"Error executing command"
|
|
398
|
-
);
|
|
377
|
+
logger.error({ err: error, command: invokedName }, "Error executing command");
|
|
399
378
|
// Surface the error name to the user instead of silently re-throwing.
|
|
400
379
|
try {
|
|
401
380
|
await sendBotMessage(
|
|
@@ -406,7 +385,7 @@ export async function handleCommand(sock, msg, body) {
|
|
|
406
385
|
error.message || "غير معروف"
|
|
407
386
|
}`,
|
|
408
387
|
},
|
|
409
|
-
{ replyTo: msg }
|
|
388
|
+
{ replyTo: msg },
|
|
410
389
|
);
|
|
411
390
|
} catch {}
|
|
412
391
|
throw error;
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { getGroupSettings } from
|
|
1
|
+
import { delay } from "@whiskeysockets/baileys";
|
|
2
|
+
import { createRequire } from "module";
|
|
3
|
+
import { getGroupSettings } from "../utils/storage.esm.js";
|
|
4
4
|
|
|
5
5
|
const require = createRequire(import.meta.url);
|
|
6
|
-
const logger = require(
|
|
6
|
+
const logger = require("../utils/logger.cjs");
|
|
7
7
|
|
|
8
8
|
// Handle group participant updates (join/leave)
|
|
9
9
|
export async function handleGroupParticipantsUpdate(sock, update) {
|
|
10
10
|
const { id, participants, action } = update;
|
|
11
|
-
logger.info({ update },
|
|
11
|
+
logger.info({ update }, "Received group participants update");
|
|
12
12
|
|
|
13
|
-
if (action !==
|
|
13
|
+
if (action !== "add") {
|
|
14
14
|
logger.info(`[Group] ${action} action for ${id}`);
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
@@ -29,7 +29,7 @@ export async function handleGroupParticipantsUpdate(sock, update) {
|
|
|
29
29
|
const randomIndex = Math.floor(Math.random() * welcomeConfig.messages.length);
|
|
30
30
|
const welcomeMessage = welcomeConfig.messages[randomIndex];
|
|
31
31
|
|
|
32
|
-
const finalMessage = welcomeMessage.replace(/\${user}/g, `@${participant.split(
|
|
32
|
+
const finalMessage = welcomeMessage.replace(/\${user}/g, `@${participant.split("@")[0]}`);
|
|
33
33
|
|
|
34
34
|
await delay(1000);
|
|
35
35
|
await sock.sendMessage(id, {
|
|
@@ -38,16 +38,16 @@ export async function handleGroupParticipantsUpdate(sock, update) {
|
|
|
38
38
|
});
|
|
39
39
|
}
|
|
40
40
|
} catch (error) {
|
|
41
|
-
logger.error({ err: error, update },
|
|
41
|
+
logger.error({ err: error, update }, "Error in group-participants.update event");
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// Handle group join requests
|
|
46
46
|
export async function handleGroupJoinRequests(sock, events) {
|
|
47
|
-
logger.info({ events },
|
|
47
|
+
logger.info({ events }, "Received group join request update");
|
|
48
48
|
|
|
49
49
|
for (const request of events) {
|
|
50
|
-
if (request.type !==
|
|
50
|
+
if (request.type !== "request") continue;
|
|
51
51
|
|
|
52
52
|
const groupId = request.jid;
|
|
53
53
|
const participantId = request.from;
|
|
@@ -58,16 +58,16 @@ export async function handleGroupJoinRequests(sock, events) {
|
|
|
58
58
|
if (settings?.join_requests?.auto_approve_enabled) {
|
|
59
59
|
logger.info(`[Auto-Approve] Approving ${participantId} for group ${groupId}`);
|
|
60
60
|
|
|
61
|
-
await sock.groupRequestUpdate(groupId, participantId,
|
|
61
|
+
await sock.groupRequestUpdate(groupId, participantId, "approve");
|
|
62
62
|
|
|
63
63
|
await delay(500);
|
|
64
64
|
await sock.sendMessage(groupId, {
|
|
65
|
-
text: `✅ تم قبول طلب انضمام @${participantId.split(
|
|
65
|
+
text: `✅ تم قبول طلب انضمام @${participantId.split("@")[0]} تلقائيًا.`,
|
|
66
66
|
mentions: [participantId],
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
} catch (error) {
|
|
70
|
-
logger.error({ err: error, request },
|
|
70
|
+
logger.error({ err: error, request }, "Error in auto-approve event");
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { createRequire } from "module";
|
|
2
|
-
import { handleCommand } from "./command.handler.js";
|
|
3
2
|
import { handleAntiSpam } from "../middleware/antispam.middleware.js";
|
|
4
3
|
import { checkBlacklist } from "../middleware/blacklist.middleware.js";
|
|
5
4
|
import { trackForwardedMessage } from "../middleware/forward-tracking.middleware.js";
|
|
6
|
-
import { updateUserLastSeen, saveUserMetadata } from "../utils/storage.esm.js";
|
|
7
|
-
import { rememberMessage } from "../utils/recentMessageCache.esm.js";
|
|
8
5
|
import normalizeJid from "../utils/normalizeJid.esm.js";
|
|
6
|
+
import { rememberMessage } from "../utils/recentMessageCache.esm.js";
|
|
7
|
+
import { saveUserMetadata, updateUserLastSeen } from "../utils/storage.esm.js";
|
|
8
|
+
import { handleCommand } from "./command.handler.js";
|
|
9
9
|
|
|
10
10
|
const require = createRequire(import.meta.url);
|
|
11
11
|
const logger = require("../utils/logger.cjs");
|
|
12
|
+
const { visibleText } = require("../utils/messageContent.cjs");
|
|
12
13
|
|
|
13
14
|
// Envelopes that carry no user-visible content: message revokes and edits
|
|
14
15
|
// (protocolMessage), reactions, poll votes, the undecrypted edit envelope, and
|
|
@@ -74,22 +75,12 @@ export async function handleIncomingMessage(sock, m) {
|
|
|
74
75
|
// Parse message context. Crucially we ALSO consider media captions —
|
|
75
76
|
// otherwise sending an image with caption "!gemini حلل ده" is invisible
|
|
76
77
|
// to the command handler and the user sees nothing happen.
|
|
77
|
-
const body =
|
|
78
|
-
msg.message?.conversation ||
|
|
79
|
-
msg.message?.extendedTextMessage?.text ||
|
|
80
|
-
msg.message?.imageMessage?.caption ||
|
|
81
|
-
msg.message?.videoMessage?.caption ||
|
|
82
|
-
msg.message?.documentMessage?.caption ||
|
|
83
|
-
"";
|
|
78
|
+
const body = visibleText(msg.message);
|
|
84
79
|
const isGroup = msg.key.remoteJid.endsWith("@g.us");
|
|
85
80
|
|
|
86
81
|
// Update user metadata (last seen)
|
|
87
82
|
const senderId = normalizeJid(msg.key.participant || msg.key.remoteJid);
|
|
88
|
-
if (
|
|
89
|
-
senderId &&
|
|
90
|
-
!senderId.includes("@broadcast") &&
|
|
91
|
-
!senderId.includes("@newsletter")
|
|
92
|
-
) {
|
|
83
|
+
if (senderId && !senderId.includes("@broadcast") && !senderId.includes("@newsletter")) {
|
|
93
84
|
try {
|
|
94
85
|
saveUserMetadata({
|
|
95
86
|
jid: senderId,
|
|
@@ -130,16 +121,15 @@ async function handleGroupModeration(sock, msg) {
|
|
|
130
121
|
try {
|
|
131
122
|
const { handleAntiLink } = require("../commands/group/antilink.cjs");
|
|
132
123
|
const { handleMediaControl } = require("../commands/group/media.cjs");
|
|
124
|
+
const { handleForbiddenWords } = require("../commands/mod.cjs");
|
|
125
|
+
|
|
126
|
+
const wordActionTaken = await handleForbiddenWords(sock, msg);
|
|
127
|
+
if (wordActionTaken) return;
|
|
133
128
|
|
|
134
129
|
const linkActionTaken = await handleAntiLink(sock, msg, {}, normalizeJid);
|
|
135
130
|
if (linkActionTaken) return;
|
|
136
131
|
|
|
137
|
-
const mediaActionTaken = await handleMediaControl(
|
|
138
|
-
sock,
|
|
139
|
-
msg,
|
|
140
|
-
{},
|
|
141
|
-
normalizeJid
|
|
142
|
-
);
|
|
132
|
+
const mediaActionTaken = await handleMediaControl(sock, msg, {}, normalizeJid);
|
|
143
133
|
if (mediaActionTaken) return;
|
|
144
134
|
|
|
145
135
|
await handleAntiSpam(sock, msg);
|
package/src/index.js
CHANGED
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
import os from "node:os";
|
|
17
17
|
import { createRequire } from "module";
|
|
18
18
|
import { bootstrapCore } from "./bootstrap/core.js";
|
|
19
|
-
import { flushStore } from "./db/store.esm.js";
|
|
20
19
|
import brand from "./config/brand.esm.js";
|
|
20
|
+
import { flushStore } from "./db/store.esm.js";
|
|
21
21
|
|
|
22
22
|
const require = createRequire(import.meta.url);
|
|
23
23
|
const qrcodeTerminal = require("qrcode-terminal");
|
|
@@ -141,7 +141,9 @@ async function startHeadless() {
|
|
|
141
141
|
line(" ✓ WhatsApp connected");
|
|
142
142
|
break;
|
|
143
143
|
case "reconnecting":
|
|
144
|
-
line(
|
|
144
|
+
line(
|
|
145
|
+
` … WhatsApp disconnected, reconnecting (attempt ${payload.attempt}/${payload.maxAttempts})`,
|
|
146
|
+
);
|
|
145
147
|
break;
|
|
146
148
|
case "logged_out":
|
|
147
149
|
case "retry_exhausted":
|
|
@@ -245,7 +247,7 @@ async function startWithPanel({ open }) {
|
|
|
245
247
|
line(
|
|
246
248
|
opened
|
|
247
249
|
? ` Opening ${brand.name} in your browser...`
|
|
248
|
-
: ` Open that link yourself (${reason})
|
|
250
|
+
: ` Open that link yourself (${reason}).`,
|
|
249
251
|
);
|
|
250
252
|
}
|
|
251
253
|
|
|
@@ -337,7 +339,9 @@ function installShutdownHandlers() {
|
|
|
337
339
|
else await finish();
|
|
338
340
|
};
|
|
339
341
|
|
|
340
|
-
["SIGTERM", "SIGINT"].forEach((signal) =>
|
|
342
|
+
["SIGTERM", "SIGINT"].forEach((signal) => {
|
|
343
|
+
process.on(signal, () => shutdown(signal));
|
|
344
|
+
});
|
|
341
345
|
}
|
|
342
346
|
|
|
343
347
|
export default start;
|
|
@@ -1,22 +1,38 @@
|
|
|
1
|
-
import { createRequire } from
|
|
2
|
-
import { getGroupSettings } from '../utils/storage.esm.js';
|
|
3
|
-
import normalizeJid from '../utils/normalizeJid.esm.js';
|
|
1
|
+
import { createRequire } from "module";
|
|
4
2
|
import {
|
|
5
|
-
|
|
3
|
+
getSenderCandidates,
|
|
6
4
|
isAdminInGroup,
|
|
7
|
-
|
|
5
|
+
isBotAdminUser,
|
|
6
|
+
isOwnerJid,
|
|
7
|
+
sameUser,
|
|
8
|
+
} from "../utils/permissions.esm.js";
|
|
9
|
+
import { getGroupSettings, getPnForLid } from "../utils/storage.esm.js";
|
|
8
10
|
|
|
9
11
|
const require = createRequire(import.meta.url);
|
|
10
|
-
const logger = require(
|
|
12
|
+
const logger = require("../utils/logger.cjs");
|
|
11
13
|
|
|
12
14
|
const userMessageTimestamps = new Map();
|
|
13
15
|
|
|
16
|
+
function stableSenderId(candidates) {
|
|
17
|
+
const phoneJid = candidates.find((jid) => jid.endsWith("@s.whatsapp.net"));
|
|
18
|
+
if (phoneJid) return phoneJid;
|
|
19
|
+
for (const candidate of candidates) {
|
|
20
|
+
try {
|
|
21
|
+
const mapped = getPnForLid(candidate);
|
|
22
|
+
if (mapped) return mapped;
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
return candidates[0] || null;
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
// Handle anti-spam detection and action
|
|
15
29
|
export async function handleAntiSpam(sock, msg) {
|
|
16
30
|
const groupId = msg.key.remoteJid;
|
|
17
|
-
if (!groupId.endsWith(
|
|
31
|
+
if (!groupId.endsWith("@g.us")) return;
|
|
18
32
|
|
|
19
|
-
const
|
|
33
|
+
const senderCandidates = getSenderCandidates(msg, sock);
|
|
34
|
+
const senderId = senderCandidates[0];
|
|
35
|
+
if (!senderId) return;
|
|
20
36
|
const settings = getGroupSettings(groupId);
|
|
21
37
|
const spamConfig = settings?.antispam;
|
|
22
38
|
|
|
@@ -25,44 +41,51 @@ export async function handleAntiSpam(sock, msg) {
|
|
|
25
41
|
// Owners and admins are immune. The legacy owner mirror it used to import was
|
|
26
42
|
// only ever populated with the bot's own JID, so real operators were
|
|
27
43
|
// counted as spammers — the centralized check fixes that.
|
|
28
|
-
const isOwner = isOwnerJid(senderId);
|
|
29
44
|
const groupMetadata = await sock.groupMetadata(groupId);
|
|
30
|
-
const
|
|
45
|
+
const protectedSender = senderCandidates.some(
|
|
46
|
+
(candidate) =>
|
|
47
|
+
isOwnerJid(candidate) ||
|
|
48
|
+
isBotAdminUser(candidate) ||
|
|
49
|
+
isAdminInGroup(groupMetadata, candidate) ||
|
|
50
|
+
[sock.user?.id, sock.user?.lid].filter(Boolean).some((botId) => sameUser(candidate, botId)),
|
|
51
|
+
);
|
|
31
52
|
|
|
32
|
-
if (
|
|
53
|
+
if (protectedSender) return;
|
|
33
54
|
|
|
34
55
|
// Track message timestamps
|
|
35
56
|
const now = Date.now();
|
|
36
|
-
|
|
37
|
-
|
|
57
|
+
const timeWindow = (spamConfig.time_window || 10) * 1000;
|
|
58
|
+
for (const [key, entry] of userMessageTimestamps) {
|
|
59
|
+
if (now - entry.lastSeen > timeWindow) userMessageTimestamps.delete(key);
|
|
38
60
|
}
|
|
39
|
-
|
|
40
|
-
const
|
|
61
|
+
const identity = stableSenderId(senderCandidates);
|
|
62
|
+
const bucketKey = `${groupId}::${identity}`;
|
|
63
|
+
const bucket = userMessageTimestamps.get(bucketKey) || { timestamps: [], lastSeen: now };
|
|
64
|
+
const userTimestamps = bucket.timestamps;
|
|
41
65
|
userTimestamps.push(now);
|
|
42
66
|
|
|
43
67
|
// Filter timestamps within time window
|
|
44
|
-
const timeWindow = (spamConfig.time_window || 10) * 1000;
|
|
45
68
|
const recentTimestamps = userTimestamps.filter((ts) => now - ts < timeWindow);
|
|
46
|
-
userMessageTimestamps.set(
|
|
69
|
+
userMessageTimestamps.set(bucketKey, { timestamps: recentTimestamps, lastSeen: now });
|
|
47
70
|
|
|
48
71
|
// Check if user exceeded message count
|
|
49
72
|
if (recentTimestamps.length > (spamConfig.message_count || 5)) {
|
|
50
|
-
logger.warn({ user: senderId, group: groupId },
|
|
73
|
+
logger.warn({ user: senderId, group: groupId }, "Spam detected, taking action.");
|
|
51
74
|
|
|
52
75
|
// Take action based on config
|
|
53
|
-
if (spamConfig.action ===
|
|
76
|
+
if (spamConfig.action === "KICK") {
|
|
54
77
|
await sock.sendMessage(groupId, {
|
|
55
|
-
text: `🚫 تم حذف @${senderId.split(
|
|
78
|
+
text: `🚫 تم حذف @${senderId.split("@")[0]} بسبب الإزعاج (Spam).`,
|
|
56
79
|
mentions: [senderId],
|
|
57
80
|
});
|
|
58
|
-
await sock.groupParticipantsUpdate(groupId, [senderId],
|
|
81
|
+
await sock.groupParticipantsUpdate(groupId, [senderId], "remove");
|
|
59
82
|
} else {
|
|
60
83
|
await sock.sendMessage(groupId, {
|
|
61
|
-
text: `⚠️ تحذير لـ @${senderId.split(
|
|
84
|
+
text: `⚠️ تحذير لـ @${senderId.split("@")[0]}! الرجاء عدم إرسال رسائل مزعجة.`,
|
|
62
85
|
mentions: [senderId],
|
|
63
86
|
});
|
|
64
87
|
}
|
|
65
88
|
|
|
66
|
-
userMessageTimestamps.delete(
|
|
89
|
+
userMessageTimestamps.delete(bucketKey);
|
|
67
90
|
}
|
|
68
91
|
}
|
|
@@ -1,22 +1,27 @@
|
|
|
1
|
-
import { createRequire } from
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import { getSenderCandidates, isAdminInGroup, sameUser } from "../utils/permissions.esm.js";
|
|
3
|
+
import { getGroupSettings } from "../utils/storage.esm.js";
|
|
4
4
|
|
|
5
5
|
const require = createRequire(import.meta.url);
|
|
6
|
-
const logger = require(
|
|
6
|
+
const logger = require("../utils/logger.cjs");
|
|
7
7
|
|
|
8
8
|
// Check if user is blacklisted in the group
|
|
9
9
|
export async function checkBlacklist(sock, msg) {
|
|
10
10
|
const groupId = msg.key.remoteJid;
|
|
11
|
-
if (!groupId.endsWith(
|
|
11
|
+
if (!groupId.endsWith("@g.us")) return false;
|
|
12
12
|
|
|
13
|
-
const
|
|
13
|
+
const senderCandidates = getSenderCandidates(msg, sock);
|
|
14
|
+
const senderId = senderCandidates[0];
|
|
15
|
+
if (!senderId) return false;
|
|
14
16
|
const settings = getGroupSettings(groupId);
|
|
17
|
+
const listed = (settings?.blacklist || []).some((entry) =>
|
|
18
|
+
senderCandidates.some((candidate) => sameUser(entry, candidate)),
|
|
19
|
+
);
|
|
15
20
|
|
|
16
|
-
if (
|
|
21
|
+
if (listed) {
|
|
17
22
|
const groupMetadata = await sock.groupMetadata(groupId);
|
|
18
|
-
const isSenderAdmin =
|
|
19
|
-
(
|
|
23
|
+
const isSenderAdmin = senderCandidates.some((candidate) =>
|
|
24
|
+
isAdminInGroup(groupMetadata, candidate),
|
|
20
25
|
);
|
|
21
26
|
|
|
22
27
|
if (!isSenderAdmin) {
|