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/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
|
|
175
|
-
- **`node:sqlite`** for a single-file datastore
|
|
176
|
-
|
|
177
|
-
- **Express + EJS** for
|
|
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
|
|
194
|
-
npm
|
|
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
|
-
|
|
26
|
-
|
|
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
|
|
48
|
-
//
|
|
49
|
-
|
|
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.
|
|
64
|
-
//
|
|
65
|
-
//
|
|
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
|
|
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
|
|
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
|
|
183
|
+
if (isPanelSessionValid(socket.request.session)) return next();
|
|
182
184
|
next(new Error("unauthorized"));
|
|
183
185
|
});
|
|
184
186
|
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
208
|
-
const
|
|
209
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
+
"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"
|