levix-bot 3.3.0-rc.1 → 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
package/README.md CHANGED
@@ -171,14 +171,15 @@ start.
171
171
  ## Architecture
172
172
 
173
173
  - **Node.js 24+** with ES modules and CommonJS where needed.
174
- - **Baileys v7** for the WhatsApp connection and LID support.
175
- - **`node:sqlite`** for a single-file datastore with no database server or
176
- native SQLite dependency.
177
- - **Express + EJS** for a panel with no frontend build step or CDN dependency.
174
+ - **Baileys v7** for the WhatsApp protocol engine, LID support, and phone pairing codes.
175
+ - **`node:sqlite`** for a zero-dependency, single-file datastore.
176
+ - **React + Vite Control Panel** (`frontend/`) with TypeScript, native RTL Arabic / English internationalization, Lucide icons, responsive drawer, modal dialogs, and real-time Socket.IO synchronization.
177
+ - **Express + EJS Gateways** for lightweight setup, login, and pairing QR gateway screens.
178
178
  - **Pluggable AI providers** — Google Gemini, OpenAI / any OpenAI-compatible endpoint, or
179
179
  Anthropic — each with its own endpoint, API key, and model in the control panel, no env file.
180
+ - **Biome** for fast formatting and linting across the codebase.
180
181
  - **GitHub Actions** validation for tests, npm packaging, Docker persistence,
181
- and standalone executables on Linux, macOS, and Windows.
182
+ Android APK packaging, and standalone executables on Linux, macOS, and Windows.
182
183
 
183
184
  [AGENTS.md](AGENTS.md) documents the module layout, message flow, storage API,
184
185
  and architectural constraints. [PACKAGING.md](PACKAGING.md) covers npm, Docker,
@@ -190,8 +191,11 @@ systemd, installers, and single-executable releases.
190
191
  git clone https://github.com/Abdodiab2005/levix
191
192
  cd levix
192
193
  npm ci
193
- npm test
194
- npm start
194
+ cd frontend && npm ci && cd ..
195
+ npm run build # compiles the React frontend to public/dashboard
196
+ npm run lint # runs Biome linter across the project
197
+ npm test # runs the full test suite
198
+ npm start # starts Levix
195
199
  ```
196
200
 
197
201
  Please read [CONTRIBUTING.md](CONTRIBUTING.md) before proposing a substantial
package/app.cjs CHANGED
@@ -9,6 +9,8 @@
9
9
  // (port, proxy hops, extra origin) are settings in the database that the panel
10
10
  // itself can change.
11
11
 
12
+ const fs = require("node:fs");
13
+ const path = require("node:path");
12
14
  const http = require("node:http");
13
15
  const express = require("express");
14
16
  const { Server } = require("socket.io");
@@ -21,10 +23,9 @@ const settings = require("./src/config/settings.cjs");
21
23
  const secrets = require("./src/config/secrets.cjs");
22
24
  const { PanelSessionStore } = require("./src/panel/session-store.cjs");
23
25
  const { getQrCode } = require("./src/utils/storage.cjs");
24
- const {
25
- clientAddress,
26
- isDirectLocalRequest,
27
- } = require("./src/utils/requestOrigin.cjs");
26
+ const { clientAddress, isDirectLocalRequest } = require("./src/utils/requestOrigin.cjs");
27
+ const { blockedFor, recordFailure, clearAttempts } = require("./src/panel/login-throttle.cjs");
28
+ const { isPanelSessionValid, stampPanelSession } = require("./src/panel/session-auth.cjs");
28
29
 
29
30
  const SESSION_COOKIE_NAME = "wa.sid";
30
31
  const SESSION_MAX_AGE_MS = 12 * 60 * 60 * 1000;
@@ -44,9 +45,10 @@ function isAllowedOrigin(origin, host) {
44
45
  // Browsers may legitimately serialize the Origin header as the literal string
45
46
  // "null" for a same-origin navigation in an opaque-origin context. Prefer
46
47
  // Fetch Metadata when the browser sends it. Some Chromium form navigations omit
47
- // Sec-Fetch-Site entirely, so only the two credential-gated auth forms get a
48
- // narrow fallback for Origin:null; every other mutation remains strict.
49
- const OPAQUE_ORIGIN_AUTH_PATHS = new Set(["/setup", "/login"]);
48
+ // Sec-Fetch-Site entirely, so password-gated login gets a narrow fallback for
49
+ // Origin:null. First-run setup is deliberately excluded: a loopback request
50
+ // otherwise skips the setup code and could claim an unconfigured panel.
51
+ const OPAQUE_ORIGIN_AUTH_PATHS = new Set(["/login"]);
50
52
  function isAllowedMutationRequest(req) {
51
53
  const origin = req.get("origin");
52
54
  const host = req.get("host");
@@ -60,9 +62,9 @@ function isAllowedMutationRequest(req) {
60
62
  if (fetchSite === "cross-site") return false;
61
63
 
62
64
  // Real Chromium can submit a top-level auth form with Origin:null and no
63
- // Sec-Fetch-Site header. Setup is still protected by the one-time setup code
64
- // for remote clients; login by the password; both share TCP-peer throttling.
65
- // Do not generalize this exception to dashboard APIs or logout.
65
+ // Sec-Fetch-Site header. Login is still protected by the password and
66
+ // TCP-peer throttling. Do not generalize this exception to setup, dashboard
67
+ // APIs, or logout.
66
68
  if (origin === "null" && !fetchSite && OPAQUE_ORIGIN_AUTH_PATHS.has(req.path)) {
67
69
  return true;
68
70
  }
@@ -126,7 +128,7 @@ app.use((req, res, next) => {
126
128
  });
127
129
 
128
130
  function requireLoginApi(req, res, next) {
129
- if (req.session?.loggedIn) return next();
131
+ if (isPanelSessionValid(req.session)) return next();
130
132
  return res.status(401).json({ error: "Unauthorized" });
131
133
  }
132
134
 
@@ -138,7 +140,7 @@ function noStore(req, res, next) {
138
140
  }
139
141
 
140
142
  function requireLoginPage(req, res, next) {
141
- if (req.session?.loggedIn) return next();
143
+ if (isPanelSessionValid(req.session)) return next();
142
144
  return res.redirect("/");
143
145
  }
144
146
 
@@ -178,40 +180,20 @@ app.use(sessionMiddleware);
178
180
  // Every socket receives the pairing QR, so it goes through the same session.
179
181
  io.engine.use(sessionMiddleware);
180
182
  io.use((socket, next) => {
181
- if (socket.request.session?.loggedIn) return next();
183
+ if (isPanelSessionValid(socket.request.session)) return next();
182
184
  next(new Error("unauthorized"));
183
185
  });
184
186
 
185
- // --- Attempt throttling ---------------------------------------------------
186
- // Shared by /login and /setup: both are a guess at a secret.
187
- //
188
- // Keyed on the TCP peer, never on `req.ip`: with `trust proxy` set, a client
189
- // picks its own `req.ip` by writing a header, so it could rotate the value and
190
- // never hit the limit.
191
-
192
- const ATTEMPT_WINDOW_MS = 15 * 60 * 1000;
193
- const MAX_ATTEMPTS = 5;
194
- const attempts = new Map();
195
-
196
- function blockedFor(ip) {
197
- const now = Date.now();
198
- for (const [key, entry] of attempts) {
199
- if (now - entry.firstAt > ATTEMPT_WINDOW_MS) attempts.delete(key);
187
+ function disconnectInvalidPanelSockets() {
188
+ for (const socket of io.of("/").sockets.values()) {
189
+ if (!isPanelSessionValid(socket.request.session)) socket.disconnect(true);
200
190
  }
201
- const entry = attempts.get(ip);
202
- if (!entry || entry.count < MAX_ATTEMPTS) return 0;
203
- const remaining = ATTEMPT_WINDOW_MS - (now - entry.firstAt);
204
- return remaining > 0 ? Math.ceil(remaining / 1000) : 0;
205
191
  }
206
192
 
207
- function recordFailure(ip) {
208
- const now = Date.now();
209
- const entry = attempts.get(ip);
210
- if (!entry || now - entry.firstAt > ATTEMPT_WINDOW_MS) {
211
- attempts.set(ip, { count: 1, firstAt: now });
212
- return;
193
+ function disconnectSessionSockets(sid) {
194
+ for (const socket of io.of("/").sockets.values()) {
195
+ if (socket.request.sessionID === sid) socket.disconnect(true);
213
196
  }
214
- entry.count += 1;
215
197
  }
216
198
 
217
199
  // --- First run ------------------------------------------------------------
@@ -271,12 +253,12 @@ app.post("/setup", (req, res, next) => {
271
253
  return renderSetup(req, res, error.message, 400);
272
254
  }
273
255
 
274
- attempts.delete(peer);
256
+ clearAttempts(peer);
275
257
  logger.info({ ip: peer }, "[dashboard] Password set — first run complete");
276
258
 
277
259
  req.session.regenerate((err) => {
278
260
  if (err) return next(err);
279
- req.session.loggedIn = true;
261
+ stampPanelSession(req.session);
280
262
  req.session.save((saveErr) => {
281
263
  if (saveErr) return next(saveErr);
282
264
  res.redirect(303, "/");
@@ -288,13 +270,24 @@ app.post("/setup", (req, res, next) => {
288
270
 
289
271
  app.get("/", noStore, (req, res) => {
290
272
  if (!secrets.hasDashboardPassword()) return res.redirect("/setup");
291
- if (req.session.loggedIn) return res.render("dashboard");
273
+ if (isPanelSessionValid(req.session)) {
274
+ const dashboardHtmlPath = path.join(assetPath("public"), "dashboard", "index.html");
275
+ if (fs.existsSync(dashboardHtmlPath)) {
276
+ let html = fs.readFileSync(dashboardHtmlPath, "utf8");
277
+ const credits = `<div class="brand-credits" style="display:none"><a href="${brand.developerSite}">${brand.developer}</a><a href="${brand.studioSite}">${brand.studio}</a></div>`;
278
+ html = html
279
+ .replace("</head>", `<script>window.__BRAND__ = ${JSON.stringify(brand)};</script></head>`)
280
+ .replace("</body>", `${credits}</body>`);
281
+ return res.type("html").send(html);
282
+ }
283
+ return res.render("dashboard");
284
+ }
292
285
  return res.render("login", { error: null });
293
286
  });
294
287
 
295
288
  app.get("/login", noStore, (req, res) => {
296
289
  if (!secrets.hasDashboardPassword()) return res.redirect("/setup");
297
- if (req.session.loggedIn) return res.redirect("/");
290
+ if (isPanelSessionValid(req.session)) return res.redirect("/");
298
291
  return res.render("login", { error: null });
299
292
  });
300
293
 
@@ -316,12 +309,12 @@ app.post("/login", (req, res, next) => {
316
309
  return res.status(401).render("login", { error: "Incorrect Password" });
317
310
  }
318
311
 
319
- attempts.delete(peer);
312
+ clearAttempts(peer);
320
313
 
321
314
  // A fresh id after login, or the id an attacker planted beforehand still works.
322
315
  req.session.regenerate((err) => {
323
316
  if (err) return next(err);
324
- req.session.loggedIn = true;
317
+ stampPanelSession(req.session);
325
318
  req.session.save((saveErr) => {
326
319
  if (saveErr) return next(saveErr);
327
320
  res.redirect(303, "/");
@@ -332,8 +325,10 @@ app.post("/login", (req, res, next) => {
332
325
  // POST, not GET: a link that changes state gets fired by an <img> or by the
333
326
  // browser's prefetch.
334
327
  app.post("/logout", (req, res, next) => {
328
+ const sid = req.sessionID;
335
329
  req.session.destroy((err) => {
336
330
  if (err) return next(err);
331
+ disconnectSessionSockets(sid);
337
332
  res.clearCookie(SESSION_COOKIE_NAME, {
338
333
  httpOnly: true,
339
334
  sameSite: "lax",
@@ -375,4 +370,6 @@ module.exports = {
375
370
  requireLoginPage,
376
371
  noStore,
377
372
  installFinalHandlers,
373
+ disconnectInvalidPanelSockets,
374
+ disconnectSessionSockets,
378
375
  };
package/package.json CHANGED
@@ -1,13 +1,20 @@
1
1
  {
2
2
  "name": "levix-bot",
3
- "version": "3.3.0-rc.1",
3
+ "version": "4.0.0-beta",
4
4
  "description": "A self-hosted personal WhatsApp bot with an AI agent, group moderation and a web control panel.",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
7
7
  "start": "node bin/levix.js",
8
8
  "dev": "nodemon bin/levix.js",
9
9
  "test": "node tests/run.mjs",
10
+ "lint": "biome lint",
11
+ "format": "biome format --write .",
12
+ "check": "biome check .",
13
+ "check:fix": "biome check --write .",
10
14
  "brand:assets": "node scripts/build-brand-assets.mjs",
15
+ "build": "npm run build:frontend",
16
+ "build:frontend": "node scripts/build-frontend.mjs",
17
+ "prepack": "npm run build:frontend",
11
18
  "build:sea": "node scripts/build-sea.mjs",
12
19
  "validate:tarball": "node scripts/validate-tarball.mjs",
13
20
  "validate:sea": "node scripts/validate-sea.mjs",
@@ -68,6 +75,7 @@
68
75
  "node": ">=24"
69
76
  },
70
77
  "devDependencies": {
78
+ "@biomejs/biome": "^2.5.14",
71
79
  "esbuild": "^0.28.2",
72
80
  "postject": "^1.0.0-alpha.6",
73
81
  "sharp": "^0.35.3"