levix-bot 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +105 -0
- package/SETUP.md +242 -0
- package/app.cjs +337 -0
- package/bin/levix.js +7 -0
- package/deploy/install.sh +182 -0
- package/deploy/levix.service +56 -0
- package/deploy/nginx/levix.leviro.net.conf +79 -0
- package/package.json +78 -0
- package/public/brand/apple-touch-icon.png +0 -0
- package/public/brand/banner.jpg +0 -0
- package/public/brand/banner.webp +0 -0
- package/public/brand/icon-16.png +0 -0
- package/public/brand/icon-192.png +0 -0
- package/public/brand/icon-32.png +0 -0
- package/public/brand/icon-512.png +0 -0
- package/public/brand/icon-512.webp +0 -0
- package/public/brand/mark.png +0 -0
- package/public/brand/mark.svg +16 -0
- package/public/brand/mark.webp +0 -0
- package/public/brand/wordmark.png +0 -0
- package/public/brand/wordmark.webp +0 -0
- package/public/dashboard.css +881 -0
- package/public/dashboard.js +1057 -0
- package/public/logo.svg +16 -0
- package/public/qrcode.min.js +1 -0
- package/public/socket.io.min.js +7 -0
- package/scheduler.cjs +197 -0
- package/src/auth/auth-storage.cjs +74 -0
- package/src/auth/use-database-auth-state.js +130 -0
- package/src/bootstrap/core.js +74 -0
- package/src/bootstrap/events.cjs +43 -0
- package/src/bootstrap/panel.js +97 -0
- package/src/cli.js +153 -0
- package/src/commands/autoschedule.cjs +84 -0
- package/src/commands/blacklist.cjs +116 -0
- package/src/commands/block.cjs +70 -0
- package/src/commands/calc.cjs +387 -0
- package/src/commands/debt.cjs +175 -0
- package/src/commands/deleteschedule.cjs +36 -0
- package/src/commands/gemini.cjs +727 -0
- package/src/commands/group/add.cjs +136 -0
- package/src/commands/group/all.cjs +43 -0
- package/src/commands/group/antiSpam.cjs +96 -0
- package/src/commands/group/antilink.cjs +167 -0
- package/src/commands/group/approveall.cjs +54 -0
- package/src/commands/group/clearwarns.cjs +41 -0
- package/src/commands/group/deletenote.cjs +33 -0
- package/src/commands/group/demote.cjs +62 -0
- package/src/commands/group/kick.cjs +85 -0
- package/src/commands/group/media.cjs +120 -0
- package/src/commands/group/members.cjs +59 -0
- package/src/commands/group/note.cjs +32 -0
- package/src/commands/group/promote.cjs +60 -0
- package/src/commands/group/removeall.cjs +42 -0
- package/src/commands/group/rules.cjs +31 -0
- package/src/commands/group/save.cjs +36 -0
- package/src/commands/group/setname.cjs +41 -0
- package/src/commands/group/setpp.cjs +51 -0
- package/src/commands/group/setrules.cjs +46 -0
- package/src/commands/group/setwarn.cjs +58 -0
- package/src/commands/group/tagadmins.cjs +50 -0
- package/src/commands/group/warn.cjs +105 -0
- package/src/commands/group/warns.cjs +66 -0
- package/src/commands/group/welcome.cjs +109 -0
- package/src/commands/group.cjs +160 -0
- package/src/commands/help.cjs +230 -0
- package/src/commands/listschedules.cjs +47 -0
- package/src/commands/loop.cjs +163 -0
- package/src/commands/memory.cjs +201 -0
- package/src/commands/mod.cjs +155 -0
- package/src/commands/notes.cjs +28 -0
- package/src/commands/perm.cjs +236 -0
- package/src/commands/ping.cjs +17 -0
- package/src/commands/poll.cjs +80 -0
- package/src/commands/prayer.cjs +64 -0
- package/src/commands/qr.cjs +39 -0
- package/src/commands/rand.cjs +130 -0
- package/src/commands/restart.cjs +25 -0
- package/src/commands/schedule.cjs +86 -0
- package/src/commands/score.cjs +130 -0
- package/src/commands/setprefix.cjs +40 -0
- package/src/commands/shortlink.cjs +74 -0
- package/src/commands/shutdown.cjs +45 -0
- package/src/commands/status.cjs +75 -0
- package/src/commands/stt.cjs +150 -0
- package/src/commands/todo.cjs +88 -0
- package/src/commands/tts.cjs +170 -0
- package/src/commands/unblock.cjs +57 -0
- package/src/commands/weather.cjs +79 -0
- package/src/config/ai-persona.md +17 -0
- package/src/config/baileys.config.js +51 -0
- package/src/config/brand.cjs +36 -0
- package/src/config/brand.esm.js +17 -0
- package/src/config/constants.js +16 -0
- package/src/config/defaults.cjs +91 -0
- package/src/config/lock.cjs +110 -0
- package/src/config/paths.cjs +90 -0
- package/src/config/runtime-config.cjs +219 -0
- package/src/config/secrets.cjs +130 -0
- package/src/config/settings.cjs +386 -0
- package/src/core/connection.js +199 -0
- package/src/core/events.js +61 -0
- package/src/core/socket.js +64 -0
- package/src/db/db.cjs +282 -0
- package/src/db/store.cjs +837 -0
- package/src/db/store.esm.js +17 -0
- package/src/domain/caddy.js +120 -0
- package/src/domain/command.js +380 -0
- package/src/domain/detect.js +173 -0
- package/src/domain/nginx.js +138 -0
- package/src/domain/system.js +148 -0
- package/src/handlers/command.handler.js +414 -0
- package/src/handlers/group.handler.js +73 -0
- package/src/handlers/message.handler.js +149 -0
- package/src/index.js +217 -0
- package/src/middleware/antispam.middleware.js +68 -0
- package/src/middleware/blacklist.middleware.js +29 -0
- package/src/middleware/forward-tracking.middleware.js +49 -0
- package/src/middleware/permissions.middleware.js +114 -0
- package/src/routes/dashboard.api.esm.js +779 -0
- package/src/services/aiAgent.cjs +317 -0
- package/src/services/aiTools.cjs +635 -0
- package/src/utils/datetime.cjs +61 -0
- package/src/utils/lid-helper.esm.js +143 -0
- package/src/utils/logger.cjs +72 -0
- package/src/utils/memory.cjs +385 -0
- package/src/utils/normalizeJid.cjs +65 -0
- package/src/utils/normalizeJid.esm.js +37 -0
- package/src/utils/openBrowser.cjs +129 -0
- package/src/utils/permissions.cjs +154 -0
- package/src/utils/permissions.esm.js +375 -0
- package/src/utils/recentMessageCache.esm.js +70 -0
- package/src/utils/requestOrigin.cjs +102 -0
- package/src/utils/sendBotMessage.cjs +43 -0
- package/src/utils/sendBotMessage.esm.js +139 -0
- package/src/utils/statusMessage.cjs +179 -0
- package/src/utils/storage-hub.cjs +49 -0
- package/src/utils/storage-hub.esm.js +24 -0
- package/src/utils/storage.cjs +80 -0
- package/src/utils/storage.esm.js +88 -0
- package/src/utils/textDecode.cjs +197 -0
- package/src/utils/thumbnail.cjs +591 -0
- package/src/utils/typing.esm.js +102 -0
- package/views/dashboard.ejs +329 -0
- package/views/login.ejs +105 -0
- package/views/qr.ejs +108 -0
- package/views/setup.ejs +122 -0
package/src/db/db.cjs
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// The database. One SQLite file, opened synchronously, no server to install.
|
|
2
|
+
//
|
|
3
|
+
// WHY node:sqlite
|
|
4
|
+
// ---------------
|
|
5
|
+
// Node 22.5+ ships SQLite in the standard library, so this costs zero
|
|
6
|
+
// dependencies: nothing to compile, nothing to `npm install`, no daemon to
|
|
7
|
+
// start. That matters here because the bot is meant to be handed to someone
|
|
8
|
+
// who is not a developer — "install Node, run levix" has to be the whole
|
|
9
|
+
// story.
|
|
10
|
+
//
|
|
11
|
+
// It is also synchronous, which is what the rest of the codebase already
|
|
12
|
+
// assumes: ~40 storage call sites (the anti-spam and blacklist middleware, the
|
|
13
|
+
// prefix lookup, every permission check) read inline with no `await`.
|
|
14
|
+
//
|
|
15
|
+
// The file lives in the data directory (src/config/paths.cjs), together with
|
|
16
|
+
// everything else the bot writes. The schema is created on open and versioned
|
|
17
|
+
// with `PRAGMA user_version`, so upgrading never asks the operator to run a
|
|
18
|
+
// migration by hand.
|
|
19
|
+
|
|
20
|
+
const path = require("node:path");
|
|
21
|
+
const { DatabaseSync } = require("node:sqlite");
|
|
22
|
+
|
|
23
|
+
const logger = require("../utils/logger.cjs");
|
|
24
|
+
const { DATA_DIR, dataPath } = require("../config/paths.cjs");
|
|
25
|
+
|
|
26
|
+
const DB_PATH = path.join(DATA_DIR, "levix.db");
|
|
27
|
+
|
|
28
|
+
// ===================================================================
|
|
29
|
+
// --- Open ---
|
|
30
|
+
// ===================================================================
|
|
31
|
+
|
|
32
|
+
const db = new DatabaseSync(DB_PATH);
|
|
33
|
+
|
|
34
|
+
// WAL: a reader never blocks the writer. The bot is a single process, but the
|
|
35
|
+
// scheduler and the HTTP handlers do interleave.
|
|
36
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
37
|
+
// NORMAL is the WAL-appropriate setting: a crash can lose the last commit or
|
|
38
|
+
// two, never the file. FULL would fsync on every warning counter.
|
|
39
|
+
db.exec("PRAGMA synchronous = NORMAL");
|
|
40
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
41
|
+
// Wait instead of throwing SQLITE_BUSY if the checkpointer holds the lock.
|
|
42
|
+
db.exec("PRAGMA busy_timeout = 5000");
|
|
43
|
+
|
|
44
|
+
// ===================================================================
|
|
45
|
+
// --- Schema ---
|
|
46
|
+
// ===================================================================
|
|
47
|
+
//
|
|
48
|
+
// Every migration is a function that takes the database from version N-1 to N.
|
|
49
|
+
// `PRAGMA user_version` records where we are; new versions are appended to the
|
|
50
|
+
// array and run on the next start. Never edit a migration that has shipped.
|
|
51
|
+
|
|
52
|
+
const MIGRATIONS = [
|
|
53
|
+
// v1 — the initial schema.
|
|
54
|
+
(database) => {
|
|
55
|
+
database.exec(`
|
|
56
|
+
-- Bot-wide key/value. Holds the prefix, every dashboard setting
|
|
57
|
+
-- ("setting:*"), the command permission/alias/disabled override maps,
|
|
58
|
+
-- and the generated secrets. Values are JSON so an object round-trips.
|
|
59
|
+
CREATE TABLE IF NOT EXISTS bot_settings (
|
|
60
|
+
key TEXT PRIMARY KEY,
|
|
61
|
+
value TEXT
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
CREATE TABLE IF NOT EXISTS group_settings (
|
|
65
|
+
group_id TEXT PRIMARY KEY,
|
|
66
|
+
settings TEXT NOT NULL DEFAULT '{}'
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
CREATE TABLE IF NOT EXISTS warnings (
|
|
70
|
+
group_id TEXT NOT NULL,
|
|
71
|
+
user_id TEXT NOT NULL,
|
|
72
|
+
warnings TEXT NOT NULL DEFAULT '[]',
|
|
73
|
+
PRIMARY KEY (group_id, user_id)
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
CREATE TABLE IF NOT EXISTS todos (
|
|
77
|
+
user_id TEXT PRIMARY KEY,
|
|
78
|
+
tasks TEXT NOT NULL DEFAULT '[]'
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
CREATE TABLE IF NOT EXISTS notes (
|
|
82
|
+
group_id TEXT NOT NULL,
|
|
83
|
+
keyword TEXT NOT NULL,
|
|
84
|
+
note_text TEXT NOT NULL,
|
|
85
|
+
PRIMARY KEY (group_id, keyword)
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
-- There is only ever one pairing QR.
|
|
89
|
+
CREATE TABLE IF NOT EXISTS qr_codes (
|
|
90
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
91
|
+
qr_string TEXT NOT NULL
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
CREATE TABLE IF NOT EXISTS lid_mapping (
|
|
95
|
+
lid TEXT PRIMARY KEY,
|
|
96
|
+
pn TEXT NOT NULL,
|
|
97
|
+
device_index INTEGER NOT NULL DEFAULT 0,
|
|
98
|
+
updated_at INTEGER NOT NULL
|
|
99
|
+
);
|
|
100
|
+
CREATE INDEX IF NOT EXISTS idx_lid_mapping_pn ON lid_mapping (pn);
|
|
101
|
+
|
|
102
|
+
CREATE TABLE IF NOT EXISTS user_metadata (
|
|
103
|
+
user_jid TEXT PRIMARY KEY,
|
|
104
|
+
user_lid TEXT,
|
|
105
|
+
phone_number TEXT,
|
|
106
|
+
is_owner INTEGER NOT NULL DEFAULT 0,
|
|
107
|
+
is_admin INTEGER NOT NULL DEFAULT 0,
|
|
108
|
+
first_seen INTEGER NOT NULL,
|
|
109
|
+
last_seen INTEGER NOT NULL,
|
|
110
|
+
display_name TEXT
|
|
111
|
+
);
|
|
112
|
+
CREATE INDEX IF NOT EXISTS idx_users_lid ON user_metadata (user_lid);
|
|
113
|
+
CREATE INDEX IF NOT EXISTS idx_users_phone ON user_metadata (phone_number);
|
|
114
|
+
CREATE INDEX IF NOT EXISTS idx_users_owner ON user_metadata (is_owner);
|
|
115
|
+
CREATE INDEX IF NOT EXISTS idx_users_admin ON user_metadata (is_admin);
|
|
116
|
+
|
|
117
|
+
CREATE TABLE IF NOT EXISTS debts (
|
|
118
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
119
|
+
group_id TEXT NOT NULL,
|
|
120
|
+
debtor_id TEXT NOT NULL,
|
|
121
|
+
creditor_id TEXT NOT NULL,
|
|
122
|
+
amount REAL NOT NULL,
|
|
123
|
+
currency TEXT NOT NULL DEFAULT 'USD',
|
|
124
|
+
description TEXT,
|
|
125
|
+
created_at INTEGER NOT NULL,
|
|
126
|
+
settled INTEGER NOT NULL DEFAULT 0,
|
|
127
|
+
settled_at INTEGER
|
|
128
|
+
);
|
|
129
|
+
CREATE INDEX IF NOT EXISTS idx_debts_group ON debts (group_id, settled);
|
|
130
|
+
|
|
131
|
+
-- Forward counters. One row per forwarded message, so this one expires;
|
|
132
|
+
-- the sweep runs at boot and every few hours (see sweepExpired()).
|
|
133
|
+
CREATE TABLE IF NOT EXISTS forward_scores (
|
|
134
|
+
message_id TEXT PRIMARY KEY,
|
|
135
|
+
group_id TEXT NOT NULL,
|
|
136
|
+
original_sender TEXT NOT NULL,
|
|
137
|
+
forward_count INTEGER NOT NULL DEFAULT 0,
|
|
138
|
+
first_forwarded_at INTEGER NOT NULL,
|
|
139
|
+
last_forwarded_at INTEGER NOT NULL,
|
|
140
|
+
expires_at INTEGER NOT NULL
|
|
141
|
+
);
|
|
142
|
+
CREATE INDEX IF NOT EXISTS idx_forward_group ON forward_scores (group_id);
|
|
143
|
+
CREATE INDEX IF NOT EXISTS idx_forward_expires ON forward_scores (expires_at);
|
|
144
|
+
|
|
145
|
+
-- Gemini conversation history, one row per chat. No expiry: wiped with
|
|
146
|
+
-- !del / !delall only.
|
|
147
|
+
CREATE TABLE IF NOT EXISTS ai_history (
|
|
148
|
+
chat_id TEXT PRIMARY KEY,
|
|
149
|
+
history TEXT NOT NULL DEFAULT '[]',
|
|
150
|
+
updated_at INTEGER NOT NULL
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
-- Baileys credentials and session keys. No expiry either: losing these
|
|
154
|
+
-- means scanning the QR again.
|
|
155
|
+
CREATE TABLE IF NOT EXISTS baileys_auth (
|
|
156
|
+
key TEXT PRIMARY KEY,
|
|
157
|
+
value TEXT NOT NULL
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
-- Scheduled messages (!schedule / !autoschedule). Used to be
|
|
161
|
+
-- config/schedule.json, which the dashboard couldn't see and a crash
|
|
162
|
+
-- mid-write could truncate.
|
|
163
|
+
CREATE TABLE IF NOT EXISTS schedules (
|
|
164
|
+
id TEXT PRIMARY KEY,
|
|
165
|
+
type TEXT NOT NULL, -- 'once' | 'recurring'
|
|
166
|
+
target_jid TEXT NOT NULL,
|
|
167
|
+
message TEXT NOT NULL,
|
|
168
|
+
cron_string TEXT, -- recurring only
|
|
169
|
+
date TEXT, -- once only, ISO 8601
|
|
170
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
171
|
+
creator_jid TEXT,
|
|
172
|
+
created_at INTEGER NOT NULL
|
|
173
|
+
);
|
|
174
|
+
`);
|
|
175
|
+
},
|
|
176
|
+
];
|
|
177
|
+
|
|
178
|
+
function migrate(database, migrations = MIGRATIONS) {
|
|
179
|
+
const { user_version: current } = database
|
|
180
|
+
.prepare("PRAGMA user_version")
|
|
181
|
+
.get();
|
|
182
|
+
|
|
183
|
+
if (current >= migrations.length) return;
|
|
184
|
+
|
|
185
|
+
for (let version = current; version < migrations.length; version += 1) {
|
|
186
|
+
// One transaction per migration, covering the version stamp as well.
|
|
187
|
+
// `user_version` lives in the database header and is written inside the
|
|
188
|
+
// transaction like anything else, so a crash or a power cut halfway
|
|
189
|
+
// through rolls the whole step back: the next start sees the old version
|
|
190
|
+
// and runs it again from a known state. Without this a half-applied
|
|
191
|
+
// migration would be recorded as complete.
|
|
192
|
+
database.exec("BEGIN IMMEDIATE");
|
|
193
|
+
try {
|
|
194
|
+
migrations[version](database);
|
|
195
|
+
// PRAGMA takes no bound parameter; the value is a loop counter, not
|
|
196
|
+
// anything a user can reach.
|
|
197
|
+
database.exec(`PRAGMA user_version = ${version + 1}`);
|
|
198
|
+
database.exec("COMMIT");
|
|
199
|
+
} catch (err) {
|
|
200
|
+
try {
|
|
201
|
+
database.exec("ROLLBACK");
|
|
202
|
+
} catch {}
|
|
203
|
+
logger.error({ err }, `[DB] Migration to v${version + 1} failed and was rolled back`);
|
|
204
|
+
throw err;
|
|
205
|
+
}
|
|
206
|
+
logger.info(`[DB] Migrated to schema v${version + 1}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
migrate(db);
|
|
211
|
+
|
|
212
|
+
// ===================================================================
|
|
213
|
+
// --- Helpers shared by the store ---
|
|
214
|
+
// ===================================================================
|
|
215
|
+
|
|
216
|
+
// Prepared statements are cached: the hot path (a permission check per
|
|
217
|
+
// message) shouldn't re-parse the same SQL every time.
|
|
218
|
+
const statementCache = new Map();
|
|
219
|
+
|
|
220
|
+
function q(sql) {
|
|
221
|
+
let statement = statementCache.get(sql);
|
|
222
|
+
if (!statement) {
|
|
223
|
+
statement = db.prepare(sql);
|
|
224
|
+
statementCache.set(sql, statement);
|
|
225
|
+
}
|
|
226
|
+
return statement;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** JSON that survives a corrupt/legacy row instead of taking the bot down. */
|
|
230
|
+
function parseJson(text, fallback) {
|
|
231
|
+
if (text === null || text === undefined) return fallback;
|
|
232
|
+
try {
|
|
233
|
+
return JSON.parse(text);
|
|
234
|
+
} catch {
|
|
235
|
+
return fallback;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Delete whatever has aged out. Cheap; called at boot and on a timer. */
|
|
240
|
+
function sweepExpired() {
|
|
241
|
+
try {
|
|
242
|
+
const { changes } = q("DELETE FROM forward_scores WHERE expires_at < ?").run(
|
|
243
|
+
Date.now()
|
|
244
|
+
);
|
|
245
|
+
if (changes) logger.debug(`[DB] Swept ${changes} expired forward score(s)`);
|
|
246
|
+
} catch (err) {
|
|
247
|
+
logger.error({ err }, "[DB] sweep failed");
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** Flush the WAL into the main file — called on shutdown and after a backup. */
|
|
252
|
+
function checkpoint() {
|
|
253
|
+
try {
|
|
254
|
+
db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
|
|
255
|
+
} catch (err) {
|
|
256
|
+
logger.error({ err }, "[DB] checkpoint failed");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function close() {
|
|
261
|
+
checkpoint();
|
|
262
|
+
try {
|
|
263
|
+
db.close();
|
|
264
|
+
} catch {}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
module.exports = {
|
|
268
|
+
db,
|
|
269
|
+
q,
|
|
270
|
+
// Exported so the migration tests can drive the real machinery against a
|
|
271
|
+
// scratch database — including a deliberately failing step, which is the
|
|
272
|
+
// only way to prove the rollback works.
|
|
273
|
+
migrate,
|
|
274
|
+
MIGRATIONS,
|
|
275
|
+
parseJson,
|
|
276
|
+
sweepExpired,
|
|
277
|
+
checkpoint,
|
|
278
|
+
close,
|
|
279
|
+
DATA_DIR,
|
|
280
|
+
DB_PATH,
|
|
281
|
+
dataPath,
|
|
282
|
+
};
|