levix-bot 2.0.0 → 2.1.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.
@@ -174,11 +174,46 @@
174
174
  }
175
175
 
176
176
  // --- connection status --------------------------------------------------
177
+ //
178
+ // The backend owns the session (src/core/session.js); this file only draws
179
+ // what it reports. Nothing here starts, stops or retries a connection on its
180
+ // own, and closing this tab does not touch the bot.
181
+
182
+ // What each backend state looks like: the pill class, the label, and the
183
+ // sentence under the Connection card's heading.
184
+ const SESSION_LABELS = {
185
+ idle: { tone: "off", label: "Not started", tag: "danger", tagText: "Not started" },
186
+ starting: { tone: "warn", label: "Connecting", tag: "warn", tagText: "Connecting" },
187
+ waiting_for_qr: { tone: "warn", label: "Waiting for scan", tag: "warn", tagText: "Waiting for scan" },
188
+ linking: { tone: "warn", label: "Linking", tag: "warn", tagText: "Linking" },
189
+ connected: { tone: "on", label: "Connected", tag: "ok", tagText: "Linked" },
190
+ reconnecting: { tone: "warn", label: "Reconnecting", tag: "warn", tagText: "Reconnecting" },
191
+ disconnected: { tone: "off", label: "Disconnected", tag: "danger", tagText: "Disconnected" },
192
+ retry_exhausted: { tone: "off", label: "Disconnected", tag: "danger", tagText: "Gave up reconnecting" },
193
+ logged_out: { tone: "off", label: "Not linked", tag: "danger", tagText: "Not linked" },
194
+ error: { tone: "off", label: "Error", tag: "danger", tagText: "Error" },
195
+ };
196
+
197
+ const describeSession = (state) =>
198
+ SESSION_LABELS[state] || { tone: "off", label: "Disconnected", tag: "danger", tagText: "Disconnected" };
177
199
 
178
- function setStatus(connected, label) {
200
+ // The last snapshot the backend sent. Every renderer reads this rather than
201
+ // its own idea of what is going on, so the pill and the Connection card can
202
+ // never disagree.
203
+ let sessionSnapshot = null;
204
+
205
+ function setStatus(snapshot) {
206
+ if (!snapshot) return;
207
+ sessionSnapshot = snapshot;
208
+ const look = describeSession(snapshot.state);
179
209
  const pill = $("#status-pill");
180
- pill.className = `pill ${connected ? "on" : "off"}`;
181
- pill.innerHTML = `<span class="dot"></span>${esc(label)}`;
210
+ pill.className = `pill ${look.tone}`;
211
+ pill.innerHTML = `<span class="dot"></span>${esc(look.label)}`;
212
+ // Outside the `currentView` check: leaving the Connection screen while a
213
+ // reconnect was pending used to leave the ticker running for the life of
214
+ // the page, because only renderConnection() could ever stop it.
215
+ syncRetryTicker(snapshot);
216
+ if (currentView === "connection") renderConnection();
182
217
  }
183
218
 
184
219
  // --- overview -----------------------------------------------------------
@@ -186,7 +221,10 @@
186
221
  async function loadStats() {
187
222
  const { stats } = await api("/stats");
188
223
 
189
- setStatus(stats.isConnected, stats.isConnected ? "Connected" : "Disconnected");
224
+ // /stats carries the same session snapshot the socket pushes, so the poll
225
+ // that runs every 15s refreshes the pill instead of flattening whatever the
226
+ // last live event put there.
227
+ setStatus(stats.connection);
190
228
 
191
229
  const cards = [
192
230
  ["Groups", stats.totalGroups],
@@ -253,55 +291,174 @@
253
291
  $("#qr-note").textContent = "Scan it from WhatsApp → Linked devices.";
254
292
  }
255
293
 
294
+ function clearQr() {
295
+ qrRendered = null;
296
+ $("#qr-frame").innerHTML = "";
297
+ $("#qr-note").textContent = "Waiting for a QR code…";
298
+ }
299
+
300
+ function retryHint(snapshot) {
301
+ if (snapshot.state !== "reconnecting" || !snapshot.nextRetryAt) return "";
302
+ const seconds = Math.max(0, Math.round((snapshot.nextRetryAt - Date.now()) / 1000));
303
+ return ` Next attempt in ${seconds}s.`;
304
+ }
305
+
306
+ // A countdown that only ticks while there is one to show. Rendering it once
307
+ // and leaving it would put a number on screen that stops being true a second
308
+ // later.
309
+ let retryTicker = null;
310
+
311
+ function syncRetryTicker(snapshot) {
312
+ const counting = snapshot.state === "reconnecting" && snapshot.nextRetryAt;
313
+ if (counting && !retryTicker) {
314
+ retryTicker = setInterval(() => {
315
+ if (currentView === "connection") renderConnection();
316
+ }, 1000);
317
+ } else if (!counting && retryTicker) {
318
+ clearInterval(retryTicker);
319
+ retryTicker = null;
320
+ }
321
+ }
322
+
323
+ /** Draw the Connection card from the last snapshot. No network call. */
324
+ function renderConnection() {
325
+ const snapshot = sessionSnapshot;
326
+ if (!snapshot) return;
327
+
328
+ const look = describeSession(snapshot.state);
329
+ const who = snapshot.user?.id ? shortJid(snapshot.user.id) : "";
330
+
331
+ $("#conn-state").innerHTML =
332
+ `<span class="tag ${esc(look.tag)}">${esc(look.tagText)}</span>` +
333
+ (who ? ` <span class="hint">${esc(who)}</span>` : "");
334
+
335
+ $("#conn-detail").textContent = (snapshot.detail || "") + retryHint(snapshot);
336
+
337
+ // The proxy line, and the one action that can apply a changed one. The
338
+ // label is already redacted server-side — there is no password to leak
339
+ // here because the browser was never told it.
340
+ const proxyLine = $("#conn-proxy");
341
+ if (snapshot.proxy) {
342
+ proxyLine.textContent = `Connecting through ${snapshot.proxy}.`;
343
+ } else if (snapshot.connected || snapshot.state === "waiting_for_qr") {
344
+ proxyLine.textContent = "Connecting directly, with no proxy.";
345
+ } else {
346
+ proxyLine.textContent = "";
347
+ }
348
+ if (snapshot.proxyChanged) {
349
+ proxyLine.textContent +=
350
+ " The saved proxy settings differ from the connection in use — reconnect to apply them.";
351
+ }
352
+
353
+ // Only offered when it would do something: there has to be a live session
354
+ // to replace, and a reason to replace it.
355
+ const canReconnect = snapshot.canStop && snapshot.proxyChanged;
356
+ $("#reconnect-btn").style.display = canReconnect ? "" : "none";
357
+ $("#reconnect-btn").disabled = !canReconnect;
358
+
359
+ // Buttons follow the backend, so a state that cannot accept an action
360
+ // cannot be asked for one.
361
+ $("#start-btn").disabled = !snapshot.canStart;
362
+ $("#start-btn").textContent =
363
+ snapshot.state === "logged_out" || snapshot.state === "idle"
364
+ ? "Start session"
365
+ : "Retry connection";
366
+ $("#stop-btn").disabled = !snapshot.canStop;
367
+ // Not "only while connected": a refused pairing leaves dead credentials
368
+ // that can only be cleared by unlinking.
369
+ $("#unlink-btn").disabled = !snapshot.canUnlink;
370
+
371
+ // The QR frame is only up while a code can actually be scanned.
372
+ const pairing = snapshot.state === "waiting_for_qr";
373
+ $("#qr-panel").style.display = pairing ? "block" : "none";
374
+ if (!pairing) clearQr();
375
+ }
376
+
256
377
  VIEWS.connection = {
257
378
  title: "Connection",
379
+ // One request for the whole screen, including a QR that was already on
380
+ // offer before this tab existed — a reload has to show the real backend
381
+ // state, not an empty frame.
258
382
  load: guard(async () => {
259
- const { stats } = await api("/stats");
260
- const connected = stats.isConnected;
261
-
262
- $("#conn-state").innerHTML = connected
263
- ? `<span class="tag ok">Linked</span> <span class="hint">${esc(shortJid(stats.botNumber))}</span>`
264
- : `<span class="tag danger">Not linked</span>`;
265
-
266
- $("#qr-panel").style.display = connected ? "none" : "block";
267
- if (connected) {
268
- qrRendered = null;
269
- $("#qr-frame").innerHTML = "";
270
- $("#qr-note").textContent = "";
271
- }
383
+ const { session, qr } = await api("/bot/session");
384
+ setStatus(session);
385
+ renderConnection();
386
+ if (qr && session.state === "waiting_for_qr") renderQr(qr);
272
387
  }),
273
388
  };
274
389
 
390
+ function confirmed(question, fn) {
391
+ return guard(async () => {
392
+ if (!confirm(question)) return;
393
+ await fn();
394
+ });
395
+ }
396
+
275
397
  function initConnectionActions() {
276
- $("#unlink-btn").addEventListener(
398
+ // Starting is idempotent on the server: a second click while a socket is
399
+ // being made is answered with the state it is already in.
400
+ $("#start-btn").addEventListener(
277
401
  "click",
278
402
  guard(async () => {
279
- if (
280
- !confirm(
281
- "Unlink this WhatsApp account? The bot stops answering until you scan a new QR code."
282
- )
283
- ) {
284
- return;
403
+ // Disabled straight away so an impatient second click cannot even be
404
+ // made, and put back from the backend's answer either way — a failed
405
+ // request must not leave a dead button behind.
406
+ $("#start-btn").disabled = true;
407
+ try {
408
+ const { session } = await api("/bot/session/start", { method: "POST" });
409
+ setStatus(session);
410
+ toast("Session starting…", "ok");
411
+ } finally {
412
+ renderConnection();
285
413
  }
286
- await api("/bot/logout", { method: "POST" });
287
- toast("Unlinked. A new QR code will appear here shortly.", "ok");
288
- setStatus(false, "Disconnected");
289
414
  })
290
415
  );
291
416
 
417
+ $("#reconnect-btn").addEventListener(
418
+ "click",
419
+ confirmed(
420
+ "Reconnect WhatsApp now to apply the saved settings? The bot stops answering for a few seconds.",
421
+ async () => {
422
+ const { session } = await api("/bot/session/reconnect", { method: "POST" });
423
+ setStatus(session);
424
+ toast("Reconnecting with the new settings…", "ok");
425
+ }
426
+ )
427
+ );
428
+
429
+ $("#stop-btn").addEventListener(
430
+ "click",
431
+ confirmed(
432
+ "Stop the WhatsApp session? The bot stops answering until you start it again. The pairing is kept.",
433
+ async () => {
434
+ const { session } = await api("/bot/session/stop", { method: "POST" });
435
+ setStatus(session);
436
+ toast("Session stopped.", "ok");
437
+ }
438
+ )
439
+ );
440
+
441
+ $("#unlink-btn").addEventListener(
442
+ "click",
443
+ confirmed(
444
+ "Unlink this WhatsApp account? The bot stops answering until you start a new session and scan a QR code.",
445
+ async () => {
446
+ const { session } = await api("/bot/logout", { method: "POST" });
447
+ setStatus(session);
448
+ toast("Unlinked. Press Start session to pair again.", "ok");
449
+ }
450
+ )
451
+ );
452
+
292
453
  $("#restart-btn").addEventListener(
293
454
  "click",
294
- guard(async () => {
295
- if (
296
- !confirm(
297
- "Restart the bot? It only comes back on its own if it runs under a supervisor (pm2, systemd, Docker)."
298
- )
299
- ) {
300
- return;
455
+ confirmed(
456
+ "Restart the bot? It only comes back on its own if it runs under a supervisor (pm2, systemd, Docker).",
457
+ async () => {
458
+ const data = await api("/bot/restart", { method: "POST" });
459
+ toast(data.message || "Restarting…", "ok");
301
460
  }
302
- const data = await api("/bot/restart", { method: "POST" });
303
- toast(data.message || "Restarting…", "ok");
304
- })
461
+ )
305
462
  );
306
463
  }
307
464
 
@@ -925,7 +1082,8 @@
925
1082
  if (setting.type === "secret") {
926
1083
  control = `
927
1084
  <div class="inline">
928
- <input type="password" id="${id}" placeholder="${setting.configured ? "•••••••• (set)" : "not set"}" autocomplete="new-password" />
1085
+ <input type="password" id="${id}" placeholder="${setting.configured ? "•••••••• (set)" : "not set"}"
1086
+ autocomplete="new-password"${setting.configured ? ' data-configured="1"' : ""} />
929
1087
  <button class="btn btn-sm" data-save-setting="${esc(setting.key)}">Save</button>
930
1088
  ${setting.configured ? `<button class="btn btn-sm btn-danger" data-clear-setting="${esc(setting.key)}">Clear</button>` : ""}
931
1089
  </div>`;
@@ -938,6 +1096,19 @@
938
1096
  </label>
939
1097
  <button class="btn btn-sm" data-save-setting="${esc(setting.key)}">Save</button>
940
1098
  </div>`;
1099
+ } else if (Array.isArray(setting.choices) && setting.choices.length) {
1100
+ control = `
1101
+ <div class="inline">
1102
+ <select id="${id}">
1103
+ ${setting.choices
1104
+ .map(
1105
+ (choice) =>
1106
+ `<option value="${esc(choice)}"${choice === setting.value ? " selected" : ""}>${esc(choice)}</option>`
1107
+ )
1108
+ .join("")}
1109
+ </select>
1110
+ <button class="btn btn-sm" data-save-setting="${esc(setting.key)}">Save</button>
1111
+ </div>`;
941
1112
  } else {
942
1113
  control = `
943
1114
  <div class="inline">
@@ -1005,6 +1176,16 @@
1005
1176
 
1006
1177
  const key = save ? save.dataset.saveSetting : clear.dataset.clearSetting;
1007
1178
  const input = document.getElementById(`set-${key}`);
1179
+
1180
+ // An already-set secret with an empty box means "leave it alone", not
1181
+ // "erase it". The box is always empty — the server never sends the value
1182
+ // back — so saving the field as-is would wipe a working password every
1183
+ // time somebody edited the host next to it. Clear is the explicit way.
1184
+ if (save && input.dataset.configured === "1" && input.value === "") {
1185
+ toast("Left unchanged. Use Clear to remove it.");
1186
+ return;
1187
+ }
1188
+
1008
1189
  const value = clear
1009
1190
  ? ""
1010
1191
  : input.type === "checkbox"
@@ -1015,28 +1196,37 @@
1015
1196
  await api("/settings", { method: "PATCH", body: { key, value } });
1016
1197
  toast(clear ? "Cleared" : "Saved", "ok");
1017
1198
  VIEWS.settings.load();
1199
+ // A proxy change cannot reach a socket that is already open; the
1200
+ // Connection screen is where that is offered, so refresh its state.
1201
+ if (key.startsWith("whatsapp_proxy_")) loadStats().catch(() => {});
1018
1202
  })();
1019
1203
  });
1020
1204
  }
1021
1205
 
1022
1206
  // --- boot ---------------------------------------------------------------
1023
1207
 
1208
+ // The socket is a one-way feed of what the backend is already doing. Opening
1209
+ // it, losing it or reloading the page changes nothing about the WhatsApp
1210
+ // session; a reconnect happens in the backend with every tab shut.
1024
1211
  function initSocket() {
1025
1212
  // eslint-disable-next-line no-undef
1026
1213
  const socket = io();
1027
1214
 
1215
+ socket.on("session", (snapshot) => setStatus(snapshot));
1216
+
1028
1217
  socket.on("qr", (qr) => {
1029
1218
  renderQr(qr);
1030
- setStatus(false, "Waiting for scan");
1031
1219
  if (currentView !== "connection") {
1032
1220
  toast("A new QR code is ready — open Connection to scan it.");
1033
1221
  }
1034
1222
  });
1035
1223
 
1036
- socket.on("status_update", (data) => {
1037
- const connected = data.status === "Connected";
1038
- setStatus(connected, data.status);
1039
- if (connected && currentView === "connection") VIEWS.connection.load();
1224
+ socket.on("qr_cleared", () => clearQr());
1225
+
1226
+ // A socket that dropped may have missed transitions while it was away.
1227
+ socket.on("connect", () => {
1228
+ if (currentView === "connection") VIEWS.connection.load();
1229
+ else loadStats().catch(() => {});
1040
1230
  });
1041
1231
  }
1042
1232
 
package/scheduler.cjs CHANGED
@@ -103,6 +103,20 @@ function scheduleAt(whenMs, run) {
103
103
  return { stop: () => clearTimeout(timer) };
104
104
  }
105
105
 
106
+ /**
107
+ * Stop every running task.
108
+ *
109
+ * Each task closes over the socket it was armed with (see scheduleNewJob), so
110
+ * once that socket is gone the task can only fail. The connection handler calls
111
+ * initializeScheduledJobs() again on the next open, which re-arms every job
112
+ * against the socket that is actually alive.
113
+ */
114
+ function stopAllScheduledJobs() {
115
+ const count = runningTasks.size;
116
+ for (const jobId of [...runningTasks.keys()]) stopTask(jobId);
117
+ if (count) logger.info(`[Scheduler] Stopped ${count} job(s) — no live connection`);
118
+ }
119
+
106
120
  function stopTask(jobId) {
107
121
  const existing = runningTasks.get(jobId);
108
122
  if (!existing) return;
@@ -189,6 +203,7 @@ function updateJobStatus(jobId, status) {
189
203
 
190
204
  module.exports = {
191
205
  initializeScheduledJobs,
206
+ stopAllScheduledJobs,
192
207
  scheduleNewJob,
193
208
  getScheduledJobs,
194
209
  saveScheduledJob,
@@ -1,15 +1,21 @@
1
1
  // Everything Levix needs to be a WhatsApp bot.
2
2
  //
3
- // Database, settings, commands, the WhatsApp socket, the scheduler. No HTTP,
4
- // no Express, no socket.io — those live in bootstrap/panel.js and are entirely
5
- // optional. `levix headless` runs this file and nothing else, which is what
6
- // makes "no panel port is opened" a fact rather than a promise.
3
+ // Database, settings, commands, the WhatsApp session manager, the scheduler.
4
+ // No HTTP, no Express, no socket.io — those live in bootstrap/panel.js and are
5
+ // entirely optional. `levix headless` runs this file and nothing else, which is
6
+ // what makes "no panel port is opened" a fact rather than a promise.
7
+ //
8
+ // Note what this does NOT do: it does not dial WhatsApp. It builds the session
9
+ // manager and hands it back. Panel mode leaves it idle until somebody presses
10
+ // Start on the Connection screen; headless has nobody to press anything, so it
11
+ // asks for `autoStart`. That is the whole of the difference between the two
12
+ // modes — there is no `if (headless)` anywhere below this line.
7
13
 
8
14
  import { createRequire } from "module";
9
- import { createWhatsAppSocket } from "../core/socket.js";
10
- import { setupEventListeners } from "../core/events.js";
15
+ import { WhatsAppSession } from "../core/session.js";
11
16
  import { loadCommands, getLoadedCommands } from "../handlers/command.handler.js";
12
17
  import { initStore } from "../db/store.esm.js";
18
+ import { deleteQrCode } from "../utils/storage.esm.js";
13
19
 
14
20
  const require = createRequire(import.meta.url);
15
21
  const logger = require("../utils/logger.cjs");
@@ -17,18 +23,17 @@ const { sink } = require("./events.cjs");
17
23
  const { acquireSingleInstanceLock } = require("../config/lock.cjs");
18
24
  const { ensureDataDir } = require("../config/paths.cjs");
19
25
  const store = require("../db/store.cjs");
20
- const { initializeScheduledJobs } = require("../../scheduler.cjs");
26
+ const { initializeScheduledJobs, stopAllScheduledJobs } = require("../../scheduler.cjs");
21
27
 
22
28
  /**
23
29
  * Start the bot.
24
30
  *
25
31
  * @param {object} [options]
26
- * @param {(sock: object, controls: object) => void} [options.onSocket]
27
- * Called with every socket, including the ones a reconnect produces — the
28
- * panel uses it to keep its "unlink" button pointed at the live connection.
29
- * @returns {Promise<{ sock: object, commandCount: number, wasPaired: boolean }>}
32
+ * @param {boolean} [options.autoStart] - connect to WhatsApp straight away.
33
+ * Headless does; the panel waits to be asked.
34
+ * @returns {Promise<{ session: object, commandCount: number, wasPaired: boolean }>}
30
35
  */
31
- export async function bootstrapCore({ onSocket } = {}) {
36
+ export async function bootstrapCore({ autoStart = false } = {}) {
32
37
  // Claim the data directory before anything touches the WhatsApp session. A
33
38
  // second copy has to fail here, not after it has written to the auth rows
34
39
  // the first one is using.
@@ -45,30 +50,29 @@ export async function bootstrapCore({ onSocket } = {}) {
45
50
  await initStore();
46
51
  await loadCommands();
47
52
 
48
- const sock = await connect({ onSocket });
49
-
50
- return { sock, commandCount: getLoadedCommands().size, wasPaired };
51
- }
52
-
53
- async function connect({ onSocket, isReconnect = false } = {}) {
54
- const { sock, saveCreds, clearAll } = await createWhatsAppSocket();
53
+ // A pairing code belongs to the socket that issued it. One left in the table
54
+ // by a process that was killed rather than stopped can never be scanned, and
55
+ // GET /qr would happily serve it to somebody who then waits for nothing.
56
+ deleteQrCode();
55
57
 
56
- setupEventListeners(
57
- sock,
58
- saveCreds,
59
- clearAll,
58
+ const session = new WhatsAppSession({
60
59
  // The bot reports into the hub; whoever is listening decides what that
61
60
  // means — a socket.io broadcast, a line in the terminal, or nothing.
62
- sink,
61
+ emit: sink.emit,
63
62
  initializeScheduledJobs,
64
- async () => {
65
- logger.info("[Reconnect] Attempting to reconnect to WhatsApp...");
66
- await connect({ onSocket, isReconnect: true });
67
- }
68
- );
63
+ stopScheduledJobs: stopAllScheduledJobs,
64
+ log: logger,
65
+ });
69
66
 
70
- if (onSocket) onSocket(sock, { clearAll, isReconnect });
67
+ if (autoStart) await session.start({ reason: "autostart" });
71
68
 
72
- logger.info("WhatsApp socket initialized successfully");
73
- return sock;
69
+ return {
70
+ session,
71
+ commandCount: getLoadedCommands().size,
72
+ wasPaired,
73
+ /** The live socket, or null. Re-read it; a reconnect replaces it. */
74
+ get sock() {
75
+ return session.socket;
76
+ },
77
+ };
74
78
  }
@@ -10,8 +10,10 @@
10
10
  // panel -> forwards every event to socket.io
11
11
  // headless -> prints the ones a person waiting at a terminal cares about
12
12
  //
13
- // `sink` is shaped like the socket.io server object because that is what
14
- // `setupEventListeners` is handed. Nothing in src/core had to change.
13
+ // `sink` is shaped like the socket.io server object because that is what the
14
+ // panel forwards from. The WhatsApp session manager (src/core/session.js) emits
15
+ // into it; nothing that listens here can emit back into the bot, which is what
16
+ // keeps a browser attaching or leaving from touching the connection.
15
17
 
16
18
  const listeners = new Set();
17
19
 
@@ -31,8 +31,9 @@ export async function bootstrapPanel({ core } = {}) {
31
31
  installFinalHandlers,
32
32
  } = require("../../app.cjs");
33
33
 
34
- const { default: dashboardApiRoutes, setBotInstance, setBotControls } =
35
- await import("../routes/dashboard.api.esm.js");
34
+ const { default: dashboardApiRoutes, setSession } = await import(
35
+ "../routes/dashboard.api.esm.js"
36
+ );
36
37
 
37
38
  // Same session as the control panel: these routes hand back groups, debts
38
39
  // and warnings with people's numbers in them.
@@ -40,12 +41,13 @@ export async function bootstrapPanel({ core } = {}) {
40
41
  logger.info("Dashboard API routes registered");
41
42
 
42
43
  // Everything the bot reports goes out to the browsers watching the panel.
44
+ // One direction only: a browser attaching or leaving cannot reach back and
45
+ // change the WhatsApp session's lifecycle. See src/core/session.js.
43
46
  attach((event, payload) => io.emit(event, payload));
44
47
 
45
- if (core?.sock) {
46
- setBotInstance(core.sock);
47
- setBotControls({ clearAll: core.clearAll });
48
- }
48
+ // The routes read the live socket off the session manager, so a reconnect
49
+ // needs no re-wiring here.
50
+ if (core?.session) setSession(core.session);
49
51
 
50
52
  // After every route is registered — the 404 and the error handler have to be
51
53
  // last, or they swallow what comes after them.
@@ -73,7 +75,7 @@ export async function bootstrapPanel({ core } = {}) {
73
75
  server.listen(port, host, resolve);
74
76
  });
75
77
 
76
- return { port, host, server, setBotInstance, setBotControls };
78
+ return { port, host, server };
77
79
  }
78
80
 
79
81
  /**
@@ -2,7 +2,7 @@ const {
2
2
  scheduleNewJob,
3
3
  saveScheduledJob,
4
4
  } = require("../../scheduler.cjs");
5
- const { v4: uuidv4 } = require("uuid"); // Use UUID for unique IDs
5
+ const { randomUUID } = require("node:crypto");
6
6
  const { defaultTimezone } = require("../utils/datetime.cjs");
7
7
 
8
8
  module.exports = {
@@ -58,7 +58,7 @@ module.exports = {
58
58
  }
59
59
 
60
60
  const newJob = {
61
- id: uuidv4(),
61
+ id: randomUUID(),
62
62
  type: "recurring",
63
63
  cronString: cronString,
64
64
  message: message,
@@ -81,4 +81,3 @@ module.exports = {
81
81
  });
82
82
  },
83
83
  };
84
- // Note: You need to run `npm install uuid` for this command.