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.
Files changed (118) hide show
  1. package/README.md +11 -7
  2. package/app.cjs +42 -45
  3. package/package.json +9 -1
  4. package/public/dashboard/assets/index-C1b7ijfu.js +391 -0
  5. package/public/dashboard/assets/index-pYaivHON.css +1 -0
  6. package/public/dashboard/index.html +19 -0
  7. package/public/dashboard.css +391 -66
  8. package/public/dashboard.js +593 -42
  9. package/scheduler.cjs +9 -14
  10. package/src/auth/use-database-auth-state.js +2 -4
  11. package/src/bootstrap/core.js +2 -5
  12. package/src/bootstrap/panel.js +10 -5
  13. package/src/cli.js +3 -5
  14. package/src/commands/autoschedule.cjs +1 -4
  15. package/src/commands/blacklist.cjs +22 -20
  16. package/src/commands/block.cjs +2 -4
  17. package/src/commands/calc.cjs +9 -16
  18. package/src/commands/digit.cjs +97 -0
  19. package/src/commands/gemini.cjs +147 -161
  20. package/src/commands/group/add.cjs +7 -16
  21. package/src/commands/group/antiSpam.cjs +5 -11
  22. package/src/commands/group/antilink.cjs +34 -28
  23. package/src/commands/group/approveall.cjs +3 -11
  24. package/src/commands/group/clearwarns.cjs +2 -5
  25. package/src/commands/group/demote.cjs +3 -8
  26. package/src/commands/group/kick.cjs +12 -15
  27. package/src/commands/group/media.cjs +17 -24
  28. package/src/commands/group/members.cjs +1 -2
  29. package/src/commands/group/promote.cjs +4 -12
  30. package/src/commands/group/removeall.cjs +2 -5
  31. package/src/commands/group/setpp.cjs +1 -1
  32. package/src/commands/group/setrules.cjs +2 -8
  33. package/src/commands/group/setwarn.cjs +4 -2
  34. package/src/commands/group/tagadmins.cjs +2 -5
  35. package/src/commands/group/warn.cjs +26 -24
  36. package/src/commands/group/warns.cjs +1 -2
  37. package/src/commands/group/welcome.cjs +13 -17
  38. package/src/commands/group.cjs +7 -10
  39. package/src/commands/help.cjs +6 -12
  40. package/src/commands/lang.cjs +104 -0
  41. package/src/commands/listschedules.cjs +2 -6
  42. package/src/commands/loop.cjs +6 -13
  43. package/src/commands/memory.cjs +19 -32
  44. package/src/commands/mod.cjs +97 -35
  45. package/src/commands/perm.cjs +18 -56
  46. package/src/commands/ping.cjs +1 -5
  47. package/src/commands/poll.cjs +1 -1
  48. package/src/commands/rand.cjs +2 -5
  49. package/src/commands/schedule.cjs +4 -8
  50. package/src/commands/score.cjs +8 -2
  51. package/src/commands/setprefix.cjs +2 -2
  52. package/src/commands/shortlink.cjs +7 -15
  53. package/src/commands/shutdown.cjs +1 -1
  54. package/src/commands/sticker.cjs +222 -0
  55. package/src/commands/stt.cjs +136 -68
  56. package/src/commands/todo.cjs +9 -15
  57. package/src/commands/tts.cjs +10 -16
  58. package/src/commands/unblock.cjs +2 -6
  59. package/src/commands/weather.cjs +5 -12
  60. package/src/config/ai-persona.md +5 -3
  61. package/src/config/available-models.json +1156 -0
  62. package/src/config/baileys.config.js +2 -1
  63. package/src/config/brand.esm.js +1 -10
  64. package/src/config/constants.js +1 -1
  65. package/src/config/defaults.cjs +66 -66
  66. package/src/config/lock.cjs +2 -2
  67. package/src/config/modelCapabilities.cjs +172 -0
  68. package/src/config/runtime-config.cjs +8 -13
  69. package/src/config/secrets.cjs +18 -10
  70. package/src/config/settings.cjs +94 -10
  71. package/src/config/vault.cjs +107 -0
  72. package/src/core/connection.js +13 -12
  73. package/src/core/events.js +41 -23
  74. package/src/core/proxy.js +7 -7
  75. package/src/core/session.js +91 -48
  76. package/src/core/socket.js +12 -15
  77. package/src/db/db.cjs +14 -6
  78. package/src/db/store.cjs +221 -87
  79. package/src/domain/caddy.js +1 -1
  80. package/src/domain/command.js +40 -19
  81. package/src/domain/detect.js +1 -1
  82. package/src/domain/system.js +2 -5
  83. package/src/handlers/command.handler.js +22 -43
  84. package/src/handlers/group.handler.js +13 -13
  85. package/src/handlers/message.handler.js +11 -21
  86. package/src/index.js +8 -4
  87. package/src/middleware/antispam.middleware.js +46 -23
  88. package/src/middleware/blacklist.middleware.js +14 -9
  89. package/src/middleware/forward-tracking.middleware.js +3 -2
  90. package/src/middleware/permissions.middleware.js +7 -10
  91. package/src/panel/login-throttle.cjs +43 -0
  92. package/src/panel/session-auth.cjs +17 -0
  93. package/src/routes/dashboard.api.esm.js +394 -72
  94. package/src/services/aiAgent.cjs +136 -49
  95. package/src/services/aiProviders.cjs +193 -47
  96. package/src/services/aiRouter.cjs +137 -0
  97. package/src/services/aiTools.cjs +305 -125
  98. package/src/services/llmDiscovery.cjs +481 -0
  99. package/src/services/modelRegistry.cjs +412 -0
  100. package/src/utils/datetime.cjs +2 -2
  101. package/src/utils/lid-helper.esm.js +7 -7
  102. package/src/utils/logger.cjs +1 -1
  103. package/src/utils/markdownParser.cjs +159 -0
  104. package/src/utils/memory.cjs +43 -13
  105. package/src/utils/messageContent.cjs +40 -0
  106. package/src/utils/normalizeJid.esm.js +7 -7
  107. package/src/utils/numerals.cjs +75 -0
  108. package/src/utils/permissions.esm.js +10 -15
  109. package/src/utils/providerUrl.cjs +281 -0
  110. package/src/utils/recurrence.cjs +7 -11
  111. package/src/utils/sendBotMessage.esm.js +4 -14
  112. package/src/utils/statusMessage.cjs +5 -13
  113. package/src/utils/storage.cjs +10 -0
  114. package/src/utils/storage.esm.js +11 -0
  115. package/src/utils/textDecode.cjs +4 -9
  116. package/src/utils/thumbnail.cjs +8 -14
  117. package/src/utils/typing.esm.js +12 -12
  118. package/views/dashboard.ejs +134 -106
@@ -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
- "CloudPanel": 'Sites → Add Site → Reverse Proxy, then "SSL/TLS → Let\'s Encrypt".',
33
- "Plesk": "Domains → Hosting & DNS → Apache & nginx Settings → additional nginx directives.",
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
- "CyberPanel": "Websites → Manage → vHost Conf, then Issue SSL.",
36
- "aaPanel": "Website → Settings → Reverse proxy, then Let's Encrypt under SSL.",
37
- "HestiaCP": "Web → Edit domain → Proxy Template, then enable Let's Encrypt.",
38
- "ISPConfig": "Sites → Options → nginx Directives.",
39
- "Coolify": "Add a service pointing at the host port, and let Coolify terminate TLS.",
40
- "Dokploy": "Create an application/domain entry pointing at the host port.",
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.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/.*$/, "");
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(` Port ${port}: ${state.taken ? `in use by ${state.process || "an unknown process"}` : "free"}`);
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(applied.error.split("\n").map((l) => ` ${l}`).join("\n"));
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(cert.error.split("\n").slice(0, 8).map((l) => ` ${l}`).join("\n"));
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(applied.error.split("\n").map((l) => ` ${l}`).join("\n"));
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
  }
@@ -170,4 +170,4 @@ export async function checkDns(system, domain) {
170
170
  return { status: matches ? "ok" : "elsewhere", resolved, mine };
171
171
  }
172
172
 
173
- export { PANELS, KNOWN_WEB_SERVERS };
173
+ export { KNOWN_WEB_SERVERS, PANELS };
@@ -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
- checkCommandPermission,
7
- checkBotAdmin,
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 function (jid, content, options) {
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
- command.__category === "group" ? `group:${command.name}` : command.name;
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 { createRequire } from 'module';
2
- import { delay } from '@whiskeysockets/baileys';
3
- import { getGroupSettings } from '../utils/storage.esm.js';
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('../utils/logger.cjs');
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 }, 'Received group participants update');
11
+ logger.info({ update }, "Received group participants update");
12
12
 
13
- if (action !== 'add') {
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('@')[0]}`);
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 }, 'Error in group-participants.update event');
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 }, 'Received group join request update');
47
+ logger.info({ events }, "Received group join request update");
48
48
 
49
49
  for (const request of events) {
50
- if (request.type !== 'request') continue;
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, 'approve');
61
+ await sock.groupRequestUpdate(groupId, participantId, "approve");
62
62
 
63
63
  await delay(500);
64
64
  await sock.sendMessage(groupId, {
65
- text: `✅ تم قبول طلب انضمام @${participantId.split('@')[0]} تلقائيًا.`,
65
+ text: `✅ تم قبول طلب انضمام @${participantId.split("@")[0]} تلقائيًا.`,
66
66
  mentions: [participantId],
67
67
  });
68
68
  }
69
69
  } catch (error) {
70
- logger.error({ err: error, request }, 'Error in auto-approve event');
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(` … WhatsApp disconnected, reconnecting (attempt ${payload.attempt}/${payload.maxAttempts})`);
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) => process.on(signal, () => shutdown(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 'module';
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
- isOwnerJid,
3
+ getSenderCandidates,
6
4
  isAdminInGroup,
7
- } from '../utils/permissions.esm.js';
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('../utils/logger.cjs');
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('@g.us')) return;
31
+ if (!groupId.endsWith("@g.us")) return;
18
32
 
19
- const senderId = normalizeJid(msg.key.participant);
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 isSenderAdmin = isAdminInGroup(groupMetadata, senderId);
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 (isOwner || isSenderAdmin) return;
53
+ if (protectedSender) return;
33
54
 
34
55
  // Track message timestamps
35
56
  const now = Date.now();
36
- if (!userMessageTimestamps.has(senderId)) {
37
- userMessageTimestamps.set(senderId, []);
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 userTimestamps = userMessageTimestamps.get(senderId);
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(senderId, recentTimestamps);
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 }, 'Spam detected, taking action.');
73
+ logger.warn({ user: senderId, group: groupId }, "Spam detected, taking action.");
51
74
 
52
75
  // Take action based on config
53
- if (spamConfig.action === 'KICK') {
76
+ if (spamConfig.action === "KICK") {
54
77
  await sock.sendMessage(groupId, {
55
- text: `🚫 تم حذف @${senderId.split('@')[0]} بسبب الإزعاج (Spam).`,
78
+ text: `🚫 تم حذف @${senderId.split("@")[0]} بسبب الإزعاج (Spam).`,
56
79
  mentions: [senderId],
57
80
  });
58
- await sock.groupParticipantsUpdate(groupId, [senderId], 'remove');
81
+ await sock.groupParticipantsUpdate(groupId, [senderId], "remove");
59
82
  } else {
60
83
  await sock.sendMessage(groupId, {
61
- text: `⚠️ تحذير لـ @${senderId.split('@')[0]}! الرجاء عدم إرسال رسائل مزعجة.`,
84
+ text: `⚠️ تحذير لـ @${senderId.split("@")[0]}! الرجاء عدم إرسال رسائل مزعجة.`,
62
85
  mentions: [senderId],
63
86
  });
64
87
  }
65
88
 
66
- userMessageTimestamps.delete(senderId);
89
+ userMessageTimestamps.delete(bucketKey);
67
90
  }
68
91
  }
@@ -1,22 +1,27 @@
1
- import { createRequire } from 'module';
2
- import { getGroupSettings } from '../utils/storage.esm.js';
3
- import normalizeJid from '../utils/normalizeJid.esm.js';
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('../utils/logger.cjs');
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('@g.us')) return false;
11
+ if (!groupId.endsWith("@g.us")) return false;
12
12
 
13
- const senderId = normalizeJid(msg.key.participant || msg.key.remoteJid);
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 (settings?.blacklist?.includes(senderId)) {
21
+ if (listed) {
17
22
  const groupMetadata = await sock.groupMetadata(groupId);
18
- const isSenderAdmin = groupMetadata.participants.some(
19
- (p) => p.admin && p.id === senderId
23
+ const isSenderAdmin = senderCandidates.some((candidate) =>
24
+ isAdminInGroup(groupMetadata, candidate),
20
25
  );
21
26
 
22
27
  if (!isSenderAdmin) {