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
@@ -1,17 +1,14 @@
1
- import { makeWASocket } from '@whiskeysockets/baileys';
2
- import NodeCache from 'node-cache';
3
- import {
4
- getBaileysConfig,
5
- setRecentMessageGetter,
6
- } from '../config/baileys.config.js';
7
- import { CACHE_CONFIG } from '../config/constants.js';
8
- import { useDatabaseAuthState } from '../auth/use-database-auth-state.js';
9
- import { getMessageFromRecent } from '../utils/recentMessageCache.esm.js';
10
- import { createRequire } from 'module';
1
+ import { makeWASocket } from "@whiskeysockets/baileys";
2
+ import { createRequire } from "module";
3
+ import NodeCache from "node-cache";
4
+ import { useDatabaseAuthState } from "../auth/use-database-auth-state.js";
5
+ import { getBaileysConfig, setRecentMessageGetter } from "../config/baileys.config.js";
6
+ import { CACHE_CONFIG } from "../config/constants.js";
7
+ import { getMessageFromRecent } from "../utils/recentMessageCache.esm.js";
11
8
 
12
9
  const require = createRequire(import.meta.url);
13
- const logger = require('../utils/logger.cjs');
14
- const { withMediaThumbnail } = require('../utils/thumbnail.cjs');
10
+ const logger = require("../utils/logger.cjs");
11
+ const { withMediaThumbnail } = require("../utils/thumbnail.cjs");
15
12
 
16
13
  // Group metadata cache
17
14
  export const groupMetadataCache = new NodeCache(CACHE_CONFIG);
@@ -36,7 +33,7 @@ function withThumbnailSupport(sock) {
36
33
  try {
37
34
  payload = await withMediaThumbnail(content);
38
35
  } catch (err) {
39
- logger.debug({ err: err?.message }, '[Socket] thumbnail step skipped');
36
+ logger.debug({ err: err?.message }, "[Socket] thumbnail step skipped");
40
37
  payload = content;
41
38
  }
42
39
  return original(jid, payload, options);
@@ -58,7 +55,7 @@ export async function createWhatsAppSocket({ proxy = null, pairingCode = false }
58
55
  logger.info(
59
56
  proxy
60
57
  ? `[Socket] Initializing WhatsApp socket through ${proxy.label}`
61
- : '[Socket] Initializing WhatsApp socket with database auth'
58
+ : "[Socket] Initializing WhatsApp socket with database auth",
62
59
  );
63
60
 
64
61
  const { state, saveCreds, clearAll } = await useDatabaseAuthState();
@@ -90,7 +87,7 @@ export async function createWhatsAppSocket({ proxy = null, pairingCode = false }
90
87
  makeWASocket({
91
88
  auth: state,
92
89
  ...config,
93
- })
90
+ }),
94
91
  );
95
92
 
96
93
  return { sock, saveCreds, clearAll, isPaired };
package/src/db/db.cjs CHANGED
@@ -181,12 +181,22 @@ const MIGRATIONS = [
181
181
  ALTER TABLE schedules ADD COLUMN last_error TEXT;
182
182
  `);
183
183
  },
184
+ // v3 — persisted group names/sizes so the panel still shows them after a
185
+ // restart, and so unlink can wipe the WhatsApp directory in one place.
186
+ (database) => {
187
+ database.exec(`
188
+ CREATE TABLE IF NOT EXISTS group_directory (
189
+ group_id TEXT PRIMARY KEY,
190
+ subject TEXT,
191
+ participant_count INTEGER,
192
+ updated_at INTEGER NOT NULL
193
+ );
194
+ `);
195
+ },
184
196
  ];
185
197
 
186
198
  function migrate(database, migrations = MIGRATIONS) {
187
- const { user_version: current } = database
188
- .prepare("PRAGMA user_version")
189
- .get();
199
+ const { user_version: current } = database.prepare("PRAGMA user_version").get();
190
200
 
191
201
  if (current >= migrations.length) return;
192
202
 
@@ -247,9 +257,7 @@ function parseJson(text, fallback) {
247
257
  /** Delete whatever has aged out. Cheap; called at boot and on a timer. */
248
258
  function sweepExpired() {
249
259
  try {
250
- const { changes } = q("DELETE FROM forward_scores WHERE expires_at < ?").run(
251
- Date.now()
252
- );
260
+ const { changes } = q("DELETE FROM forward_scores WHERE expires_at < ?").run(Date.now());
253
261
  if (changes) logger.debug(`[DB] Swept ${changes} expired forward score(s)`);
254
262
  } catch (err) {
255
263
  logger.error({ err }, "[DB] sweep failed");
package/src/db/store.cjs CHANGED
@@ -30,15 +30,43 @@ const DEFAULT_FORWARD_TTL_DAYS = 30;
30
30
 
31
31
  let sweepTimer = null;
32
32
 
33
+ function migrateSecretsAtRest() {
34
+ const vault = require("../config/vault.cjs");
35
+ const rows = q("SELECT key, value FROM baileys_auth").all();
36
+ for (const row of rows) {
37
+ if (row.value && !vault.isSealed(row.value)) {
38
+ q("UPDATE baileys_auth SET value = ? WHERE key = ?").run(vault.seal(row.value), row.key);
39
+ }
40
+ }
41
+
42
+ let secretKeys = [];
43
+ try {
44
+ secretKeys = require("../config/settings.cjs").SECRET_SETTING_KEYS || [];
45
+ } catch {
46
+ secretKeys = [];
47
+ }
48
+ for (const key of secretKeys) {
49
+ const stored = getBotSetting(`setting:${key}`, null);
50
+ if (typeof stored === "string" && stored && !vault.isSealed(stored)) {
51
+ saveBotSetting(`setting:${key}`, vault.seal(stored));
52
+ }
53
+ }
54
+ }
55
+
33
56
  async function initStore() {
34
57
  sweepExpired();
58
+ try {
59
+ migrateSecretsAtRest();
60
+ } catch (err) {
61
+ logger.error({ err }, "[Store] Failed to encrypt existing secrets at rest");
62
+ }
35
63
  if (!sweepTimer) {
36
64
  sweepTimer = setInterval(sweepExpired, 6 * 60 * 60 * 1000);
37
65
  sweepTimer.unref();
38
66
  }
39
67
  logger.info(
40
68
  `[Store] SQLite ready at ${DB_PATH} — ${countGroups()} group(s), ` +
41
- `${countUsers()} user(s), ${countNotes()} note(s), ${countLidMappings()} LID mapping(s)`
69
+ `${countUsers()} user(s), ${countNotes()} note(s), ${countLidMappings()} LID mapping(s)`,
42
70
  );
43
71
  }
44
72
 
@@ -97,7 +125,7 @@ function getBotSetting(key, defaultValue = null) {
97
125
  function saveBotSetting(key, value) {
98
126
  q(
99
127
  `INSERT INTO bot_settings (key, value) VALUES (?, ?)
100
- ON CONFLICT(key) DO UPDATE SET value = excluded.value`
128
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
101
129
  ).run(key, JSON.stringify(value ?? null));
102
130
  }
103
131
 
@@ -110,16 +138,14 @@ function deleteBotSetting(key) {
110
138
  // ===================================================================
111
139
 
112
140
  function getGroupSettings(groupId) {
113
- const row = q("SELECT settings FROM group_settings WHERE group_id = ?").get(
114
- groupId
115
- );
141
+ const row = q("SELECT settings FROM group_settings WHERE group_id = ?").get(groupId);
116
142
  return row ? parseJson(row.settings, {}) : {};
117
143
  }
118
144
 
119
145
  function saveGroupSettings(groupId, settings) {
120
146
  q(
121
147
  `INSERT INTO group_settings (group_id, settings) VALUES (?, ?)
122
- ON CONFLICT(group_id) DO UPDATE SET settings = excluded.settings`
148
+ ON CONFLICT(group_id) DO UPDATE SET settings = excluded.settings`,
123
149
  ).run(groupId, JSON.stringify(settings || {}));
124
150
  }
125
151
 
@@ -136,29 +162,83 @@ function countGroups() {
136
162
  return q("SELECT COUNT(*) AS n FROM group_settings").get().n;
137
163
  }
138
164
 
165
+ function upsertGroupDirectory(groupId, { subject = null, participantCount = null } = {}) {
166
+ if (!groupId) return;
167
+ const existing = q("SELECT subject, participant_count FROM group_directory WHERE group_id = ?").get(
168
+ groupId,
169
+ );
170
+ q(
171
+ `INSERT INTO group_directory (group_id, subject, participant_count, updated_at)
172
+ VALUES (?, ?, ?, ?)
173
+ ON CONFLICT(group_id) DO UPDATE SET
174
+ subject = COALESCE(excluded.subject, group_directory.subject),
175
+ participant_count = COALESCE(excluded.participant_count, group_directory.participant_count),
176
+ updated_at = excluded.updated_at`,
177
+ ).run(
178
+ groupId,
179
+ subject || existing?.subject || null,
180
+ participantCount == null ? (existing?.participant_count ?? null) : participantCount,
181
+ Date.now(),
182
+ );
183
+ }
184
+
185
+ function getGroupDirectory(groupId) {
186
+ return q("SELECT group_id, subject, participant_count, updated_at FROM group_directory WHERE group_id = ?").get(
187
+ groupId,
188
+ );
189
+ }
190
+
191
+ function getAllGroupDirectory() {
192
+ return q(
193
+ "SELECT group_id, subject, participant_count, updated_at FROM group_directory ORDER BY subject COLLATE NOCASE",
194
+ ).all();
195
+ }
196
+
197
+ function clearGroupDirectory() {
198
+ q("DELETE FROM group_directory").run();
199
+ }
200
+
201
+ function clearGroupSettings() {
202
+ q("DELETE FROM group_settings").run();
203
+ }
204
+
205
+ function clearUserMetadata() {
206
+ q("DELETE FROM user_metadata").run();
207
+ }
208
+
209
+ function clearLidMappings() {
210
+ q("DELETE FROM lid_mapping").run();
211
+ }
212
+
213
+ /** Drop WhatsApp-derived directory data after unlink / logged-out. */
214
+ function clearWhatsAppDirectory() {
215
+ clearGroupDirectory();
216
+ clearGroupSettings();
217
+ clearUserMetadata();
218
+ clearLidMappings();
219
+ }
220
+
139
221
  // ===================================================================
140
222
  // --- Warnings ---
141
223
  // ===================================================================
142
224
 
143
225
  function getUserWarnings(groupId, userId) {
144
- const row = q(
145
- "SELECT warnings FROM warnings WHERE group_id = ? AND user_id = ?"
146
- ).get(groupId, userId);
226
+ const row = q("SELECT warnings FROM warnings WHERE group_id = ? AND user_id = ?").get(
227
+ groupId,
228
+ userId,
229
+ );
147
230
  return row ? parseJson(row.warnings, []) : [];
148
231
  }
149
232
 
150
233
  function saveUserWarnings(groupId, userId, warningsArray) {
151
234
  q(
152
235
  `INSERT INTO warnings (group_id, user_id, warnings) VALUES (?, ?, ?)
153
- ON CONFLICT(group_id, user_id) DO UPDATE SET warnings = excluded.warnings`
236
+ ON CONFLICT(group_id, user_id) DO UPDATE SET warnings = excluded.warnings`,
154
237
  ).run(groupId, userId, JSON.stringify(warningsArray || []));
155
238
  }
156
239
 
157
240
  function clearUserWarnings(groupId, userId) {
158
- q("DELETE FROM warnings WHERE group_id = ? AND user_id = ?").run(
159
- groupId,
160
- userId
161
- );
241
+ q("DELETE FROM warnings WHERE group_id = ? AND user_id = ?").run(groupId, userId);
162
242
  }
163
243
 
164
244
  function getAllWarnings() {
@@ -187,7 +267,7 @@ function getUserTodos(userId) {
187
267
  function saveUserTodos(userId, tasksArray) {
188
268
  q(
189
269
  `INSERT INTO todos (user_id, tasks) VALUES (?, ?)
190
- ON CONFLICT(user_id) DO UPDATE SET tasks = excluded.tasks`
270
+ ON CONFLICT(user_id) DO UPDATE SET tasks = excluded.tasks`,
191
271
  ).run(userId, JSON.stringify(tasksArray || []));
192
272
  }
193
273
 
@@ -208,14 +288,15 @@ function countTodos() {
208
288
  function saveNote(groupId, keyword, text) {
209
289
  q(
210
290
  `INSERT INTO notes (group_id, keyword, note_text) VALUES (?, ?, ?)
211
- ON CONFLICT(group_id, keyword) DO UPDATE SET note_text = excluded.note_text`
291
+ ON CONFLICT(group_id, keyword) DO UPDATE SET note_text = excluded.note_text`,
212
292
  ).run(groupId, keyword, text);
213
293
  }
214
294
 
215
295
  function getNote(groupId, keyword) {
216
- const row = q(
217
- "SELECT note_text FROM notes WHERE group_id = ? AND keyword = ?"
218
- ).get(groupId, keyword);
296
+ const row = q("SELECT note_text FROM notes WHERE group_id = ? AND keyword = ?").get(
297
+ groupId,
298
+ keyword,
299
+ );
219
300
  return row ? row.note_text : null;
220
301
  }
221
302
 
@@ -226,16 +307,17 @@ function getAllNotes(groupId) {
226
307
  }
227
308
 
228
309
  function deleteNote(groupId, keyword) {
229
- const { changes } = q(
230
- "DELETE FROM notes WHERE group_id = ? AND keyword = ?"
231
- ).run(groupId, keyword);
310
+ const { changes } = q("DELETE FROM notes WHERE group_id = ? AND keyword = ?").run(
311
+ groupId,
312
+ keyword,
313
+ );
232
314
  return changes > 0;
233
315
  }
234
316
 
235
317
  function getAllNotesFlat(limit = 100) {
236
318
  return q(
237
319
  `SELECT group_id, keyword, note_text FROM notes
238
- ORDER BY group_id, keyword LIMIT ?`
320
+ ORDER BY group_id, keyword LIMIT ?`,
239
321
  ).all(limit);
240
322
  }
241
323
 
@@ -250,7 +332,7 @@ function countNotes() {
250
332
  function saveQrCode(qr) {
251
333
  q(
252
334
  `INSERT INTO qr_codes (id, qr_string) VALUES (1, ?)
253
- ON CONFLICT(id) DO UPDATE SET qr_string = excluded.qr_string`
335
+ ON CONFLICT(id) DO UPDATE SET qr_string = excluded.qr_string`,
254
336
  ).run(qr);
255
337
  }
256
338
 
@@ -273,7 +355,7 @@ function storeLidPnMapping(lid, pn, deviceIndex = 0) {
273
355
  ON CONFLICT(lid) DO UPDATE SET
274
356
  pn = excluded.pn,
275
357
  device_index = excluded.device_index,
276
- updated_at = excluded.updated_at`
358
+ updated_at = excluded.updated_at`,
277
359
  ).run(lid, pn, deviceIndex, Date.now());
278
360
  logger.debug(`[LID] Stored mapping: ${lid} <-> ${pn}`);
279
361
  }
@@ -357,17 +439,9 @@ function saveUserMetadata(userData) {
357
439
  user_lid: userData.lid || existing?.user_lid || null,
358
440
  phone_number: userData.phone || existing?.phone_number || null,
359
441
  is_owner:
360
- userData.isOwner === undefined
361
- ? existing?.is_owner === 1
362
- ? 1
363
- : 0
364
- : bool(userData.isOwner),
442
+ userData.isOwner === undefined ? (existing?.is_owner === 1 ? 1 : 0) : bool(userData.isOwner),
365
443
  is_admin:
366
- userData.isAdmin === undefined
367
- ? existing?.is_admin === 1
368
- ? 1
369
- : 0
370
- : bool(userData.isAdmin),
444
+ userData.isAdmin === undefined ? (existing?.is_admin === 1 ? 1 : 0) : bool(userData.isAdmin),
371
445
  first_seen: existing?.first_seen ?? now,
372
446
  last_seen: now,
373
447
  display_name: userData.displayName || existing?.display_name || null,
@@ -383,7 +457,7 @@ function saveUserMetadata(userData) {
383
457
  is_owner = excluded.is_owner,
384
458
  is_admin = excluded.is_admin,
385
459
  last_seen = excluded.last_seen,
386
- display_name = excluded.display_name`
460
+ display_name = excluded.display_name`,
387
461
  ).run(
388
462
  row.user_jid,
389
463
  row.user_lid,
@@ -392,7 +466,7 @@ function saveUserMetadata(userData) {
392
466
  row.is_admin,
393
467
  row.first_seen,
394
468
  row.last_seen,
395
- row.display_name
469
+ row.display_name,
396
470
  );
397
471
  }
398
472
 
@@ -411,9 +485,7 @@ function getUserMetadata(identifier) {
411
485
  }
412
486
 
413
487
  if (!row) {
414
- row = q("SELECT * FROM user_metadata WHERE phone_number = ?").get(
415
- identifier
416
- );
488
+ row = q("SELECT * FROM user_metadata WHERE phone_number = ?").get(identifier);
417
489
  }
418
490
 
419
491
  if (!row) {
@@ -424,7 +496,7 @@ function getUserMetadata(identifier) {
424
496
  q(
425
497
  `SELECT * FROM user_metadata
426
498
  WHERE user_jid LIKE ? OR user_lid LIKE ?
427
- LIMIT 1`
499
+ LIMIT 1`,
428
500
  ).get(`${phone}@%`, `${phone}@%`);
429
501
  }
430
502
  }
@@ -445,21 +517,15 @@ function isUserBotAdmin(identifier) {
445
517
  }
446
518
 
447
519
  function getAllOwners() {
448
- return q("SELECT * FROM user_metadata WHERE is_owner = 1")
449
- .all()
450
- .map(rowToUser);
520
+ return q("SELECT * FROM user_metadata WHERE is_owner = 1").all().map(rowToUser);
451
521
  }
452
522
 
453
523
  function getAllBotAdmins() {
454
- return q("SELECT * FROM user_metadata WHERE is_admin = 1")
455
- .all()
456
- .map(rowToUser);
524
+ return q("SELECT * FROM user_metadata WHERE is_admin = 1").all().map(rowToUser);
457
525
  }
458
526
 
459
527
  function getAllUsers() {
460
- return q("SELECT * FROM user_metadata ORDER BY last_seen DESC")
461
- .all()
462
- .map(rowToUser);
528
+ return q("SELECT * FROM user_metadata ORDER BY last_seen DESC").all().map(rowToUser);
463
529
  }
464
530
 
465
531
  function countUsers() {
@@ -493,8 +559,7 @@ function setUserRole(identifier, role, enabled = true) {
493
559
 
494
560
  saveUserMetadata({
495
561
  jid,
496
- lid:
497
- existing?.lid || (String(identifier).endsWith("@lid") ? identifier : null),
562
+ lid: existing?.lid || (String(identifier).endsWith("@lid") ? identifier : null),
498
563
  phone: existing?.phone || digitsOf(identifier),
499
564
  displayName: existing?.displayName || null,
500
565
  [wanted]: !!enabled,
@@ -504,10 +569,7 @@ function setUserRole(identifier, role, enabled = true) {
504
569
  }
505
570
 
506
571
  function updateUserLastSeen(jid) {
507
- q("UPDATE user_metadata SET last_seen = ? WHERE user_jid = ?").run(
508
- Date.now(),
509
- jid
510
- );
572
+ q("UPDATE user_metadata SET last_seen = ? WHERE user_jid = ?").run(Date.now(), jid);
511
573
  }
512
574
 
513
575
  // ===================================================================
@@ -524,20 +586,17 @@ function incrementForwardScore(messageId, groupId, senderId) {
524
586
  ON CONFLICT(message_id) DO UPDATE SET
525
587
  forward_count = forward_count + 1,
526
588
  last_forwarded_at = excluded.last_forwarded_at,
527
- expires_at = excluded.expires_at`
589
+ expires_at = excluded.expires_at`,
528
590
  ).run(messageId, groupId, senderId, now, now, forwardExpiry());
529
591
 
530
592
  return (
531
- q("SELECT forward_count FROM forward_scores WHERE message_id = ?").get(
532
- messageId
533
- )?.forward_count ?? 1
593
+ q("SELECT forward_count FROM forward_scores WHERE message_id = ?").get(messageId)
594
+ ?.forward_count ?? 1
534
595
  );
535
596
  }
536
597
 
537
598
  function getForwardScore(messageId) {
538
- const row = q("SELECT * FROM forward_scores WHERE message_id = ?").get(
539
- messageId
540
- );
599
+ const row = q("SELECT * FROM forward_scores WHERE message_id = ?").get(messageId);
541
600
  return row
542
601
  ? {
543
602
  count: row.forward_count,
@@ -554,7 +613,7 @@ function getTopForwardedMessages(groupId, limit = 10) {
554
613
  FROM forward_scores
555
614
  WHERE group_id = ?
556
615
  ORDER BY forward_count DESC
557
- LIMIT ?`
616
+ LIMIT ?`,
558
617
  ).all(groupId, limit);
559
618
  }
560
619
 
@@ -567,19 +626,12 @@ function debtRow(row) {
567
626
  return { ...row, settled: row.settled === 1 };
568
627
  }
569
628
 
570
- function addDebt({
571
- groupId,
572
- debtorId,
573
- creditorId,
574
- amount,
575
- currency = "USD",
576
- description = null,
577
- }) {
629
+ function addDebt({ groupId, debtorId, creditorId, amount, currency = "USD", description = null }) {
578
630
  const createdAt = Date.now();
579
631
  const { lastInsertRowid } = q(
580
632
  `INSERT INTO debts
581
633
  (group_id, debtor_id, creditor_id, amount, currency, description, created_at, settled, settled_at)
582
- VALUES (?, ?, ?, ?, ?, ?, ?, 0, NULL)`
634
+ VALUES (?, ?, ?, ?, ?, ?, ?, 0, NULL)`,
583
635
  ).run(groupId, debtorId, creditorId, amount, currency, description, createdAt);
584
636
 
585
637
  return getDebt(Number(lastInsertRowid));
@@ -597,22 +649,18 @@ function deleteDebt(id) {
597
649
  function listDebts(groupId, { settled = false } = {}) {
598
650
  return q(
599
651
  `SELECT * FROM debts WHERE group_id = ? AND settled = ?
600
- ORDER BY created_at DESC`
652
+ ORDER BY created_at DESC`,
601
653
  )
602
654
  .all(groupId, bool(settled))
603
655
  .map(debtRow);
604
656
  }
605
657
 
606
658
  function getRecentDebts(limit = 50) {
607
- return q("SELECT * FROM debts ORDER BY created_at DESC LIMIT ?")
608
- .all(limit)
609
- .map(debtRow);
659
+ return q("SELECT * FROM debts ORDER BY created_at DESC LIMIT ?").all(limit).map(debtRow);
610
660
  }
611
661
 
612
662
  function countDebts(settled = false) {
613
- return q("SELECT COUNT(*) AS n FROM debts WHERE settled = ?").get(
614
- bool(settled)
615
- ).n;
663
+ return q("SELECT COUNT(*) AS n FROM debts WHERE settled = ?").get(bool(settled)).n;
616
664
  }
617
665
 
618
666
  // ===================================================================
@@ -667,7 +715,7 @@ function saveSchedule(job) {
667
715
  creator_jid = excluded.creator_jid,
668
716
  last_run_at = excluded.last_run_at,
669
717
  last_delivery_status = excluded.last_delivery_status,
670
- last_error = excluded.last_error`
718
+ last_error = excluded.last_error`,
671
719
  ).run(
672
720
  String(job.id),
673
721
  job.type,
@@ -680,7 +728,7 @@ function saveSchedule(job) {
680
728
  job.createdAt ?? Date.now(),
681
729
  job.lastRunAt ?? null,
682
730
  job.lastDeliveryStatus ?? null,
683
- job.lastError ?? null
731
+ job.lastError ?? null,
684
732
  );
685
733
  return getSchedule(job.id);
686
734
  }
@@ -693,7 +741,7 @@ function setScheduleDelivery(id, status, runAt, error = null) {
693
741
  q(
694
742
  `UPDATE schedules
695
743
  SET last_delivery_status = ?, last_run_at = ?, last_error = ?
696
- WHERE id = ?`
744
+ WHERE id = ?`,
697
745
  ).run(status, runAt, error, String(id));
698
746
  return getSchedule(id);
699
747
  }
@@ -703,6 +751,12 @@ function deleteSchedule(id) {
703
751
  return changes > 0;
704
752
  }
705
753
 
754
+ /** Keep old jobs visible after unlink, but never arm them for a new account. */
755
+ function pauseAllSchedules() {
756
+ return q("UPDATE schedules SET status = 'paused' WHERE status IN ('active', 'pending')").run()
757
+ .changes;
758
+ }
759
+
706
760
  function countSchedules() {
707
761
  return q("SELECT COUNT(*) AS n FROM schedules").get().n;
708
762
  }
@@ -725,7 +779,7 @@ function saveChatHistory(chatId, historyArray) {
725
779
  `INSERT INTO ai_history (chat_id, history, updated_at) VALUES (?, ?, ?)
726
780
  ON CONFLICT(chat_id) DO UPDATE SET
727
781
  history = excluded.history,
728
- updated_at = excluded.updated_at`
782
+ updated_at = excluded.updated_at`,
729
783
  ).run(chatId, JSON.stringify(historyArray || []), Date.now());
730
784
  }
731
785
 
@@ -747,14 +801,19 @@ function deleteAllChatHistories() {
747
801
 
748
802
  function authRead(key) {
749
803
  const row = q("SELECT value FROM baileys_auth WHERE key = ?").get(key);
750
- return row ? row.value : null;
804
+ if (!row) return null;
805
+ const vault = require("../config/vault.cjs");
806
+ return vault.open(row.value);
751
807
  }
752
808
 
753
809
  function authWrite(key, serialized) {
810
+ const vault = require("../config/vault.cjs");
811
+ const payload = serialized == null ? "" : String(serialized);
812
+ const stored = vault.isSealed(payload) ? payload : vault.seal(payload);
754
813
  q(
755
814
  `INSERT INTO baileys_auth (key, value) VALUES (?, ?)
756
- ON CONFLICT(key) DO UPDATE SET value = excluded.value`
757
- ).run(key, serialized);
815
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
816
+ ).run(key, stored);
758
817
  }
759
818
 
760
819
  function authRemove(key) {
@@ -769,6 +828,68 @@ function hasCredentials() {
769
828
  return !!q("SELECT 1 AS x FROM baileys_auth WHERE key = 'creds'").get();
770
829
  }
771
830
 
831
+ /**
832
+ * A finished pairing — not merely that a creds row exists. Baileys writes
833
+ * creds on the first socket, and requestPairingCode fills me.id before the
834
+ * phone accepts, so only `registered !== false` plus a me.id is proof.
835
+ */
836
+ function isPairedSession() {
837
+ const text = authRead("creds");
838
+ if (!text) return false;
839
+ try {
840
+ const creds = JSON.parse(text);
841
+ const meId = creds?.me?.id;
842
+ if (!meId) return false;
843
+ return creds.registered !== false;
844
+ } catch {
845
+ return false;
846
+ }
847
+ }
848
+
849
+ function resolveUserPhone(jid) {
850
+ if (!jid || String(jid).endsWith("@g.us")) return null;
851
+ const meta = getUserMetadata(jid);
852
+ if (meta?.phone) return String(meta.phone).replace(/\D/g, "");
853
+ const raw = String(jid);
854
+ const local = raw.split("@")[0].split(":")[0];
855
+ if (raw.includes("@lid") || !/^\d{8,15}$/.test(local)) {
856
+ for (const candidate of [raw, local, `${local}@lid`]) {
857
+ const pn = getPnForLid(candidate);
858
+ if (!pn) continue;
859
+ const digits = String(pn).split("@")[0].replace(/\D/g, "");
860
+ if (digits) return digits;
861
+ }
862
+ }
863
+ if (/^\d{8,15}$/.test(local)) return local;
864
+ return null;
865
+ }
866
+
867
+ function describePeer(jid) {
868
+ const id = String(jid || "");
869
+ if (!id) return { jid: "", kind: "unknown", label: "", phone: null, memberCount: null };
870
+ if (id.endsWith("@g.us")) {
871
+ const dir = getGroupDirectory(id);
872
+ const label = dir?.subject || null;
873
+ return {
874
+ jid: id,
875
+ kind: "group",
876
+ label: label || "Group",
877
+ phone: null,
878
+ memberCount: dir?.participant_count ?? null,
879
+ };
880
+ }
881
+ const phone = resolveUserPhone(id);
882
+ const meta = getUserMetadata(id);
883
+ const label = meta?.displayName || (phone ? `+${phone}` : id.split("@")[0]);
884
+ return {
885
+ jid: id,
886
+ kind: "contact",
887
+ label,
888
+ phone: phone ? `+${phone}` : null,
889
+ memberCount: null,
890
+ };
891
+ }
892
+
772
893
  module.exports = {
773
894
  // Lifecycle
774
895
  initStore,
@@ -784,6 +905,14 @@ module.exports = {
784
905
  saveGroupSettings,
785
906
  getAllGroupSettings,
786
907
  countGroups,
908
+ upsertGroupDirectory,
909
+ getGroupDirectory,
910
+ getAllGroupDirectory,
911
+ clearGroupDirectory,
912
+ clearGroupSettings,
913
+ clearUserMetadata,
914
+ clearLidMappings,
915
+ clearWhatsAppDirectory,
787
916
  // Warnings
788
917
  getUserWarnings,
789
918
  saveUserWarnings,
@@ -842,6 +971,7 @@ module.exports = {
842
971
  setScheduleStatus,
843
972
  setScheduleDelivery,
844
973
  deleteSchedule,
974
+ pauseAllSchedules,
845
975
  countSchedules,
846
976
  // AI history
847
977
  getChatHistory,
@@ -854,4 +984,8 @@ module.exports = {
854
984
  authRemove,
855
985
  authClearAll,
856
986
  hasCredentials,
987
+ isPairedSession,
988
+ resolveUserPhone,
989
+ describePeer,
990
+ migrateSecretsAtRest,
857
991
  };
@@ -44,7 +44,7 @@ export function applySite(system, { domain, port, caddyfile = "/etc/caddy/Caddyf
44
44
  if (!current.includes(IMPORT_LINE)) {
45
45
  system.writeFile(
46
46
  caddyfile,
47
- `${current}${current.endsWith("\n") || current === "" ? "" : "\n"}\n# Added by Levix — loads its site files.\n${IMPORT_LINE}\n`
47
+ `${current}${current.endsWith("\n") || current === "" ? "" : "\n"}\n# Added by Levix — loads its site files.\n${IMPORT_LINE}\n`,
48
48
  );
49
49
  }
50
50