levix-bot 3.0.0-beta → 3.2.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/app.cjs +6 -0
- package/package.json +1 -1
- package/public/dashboard.js +13 -0
- package/src/bootstrap/panel.js +30 -6
- package/src/commands/tts.cjs +38 -38
- package/src/core/session.js +92 -11
- package/src/index.js +12 -1
- package/src/routes/dashboard.api.esm.js +15 -0
- package/src/utils/requestOrigin.cjs +4 -1
- package/src/utils/thumbnail.cjs +15 -2
- package/views/dashboard.ejs +3 -0
package/app.cjs
CHANGED
|
@@ -292,6 +292,12 @@ app.get("/", noStore, (req, res) => {
|
|
|
292
292
|
return res.render("login", { error: null });
|
|
293
293
|
});
|
|
294
294
|
|
|
295
|
+
app.get("/login", noStore, (req, res) => {
|
|
296
|
+
if (!secrets.hasDashboardPassword()) return res.redirect("/setup");
|
|
297
|
+
if (req.session.loggedIn) return res.redirect("/");
|
|
298
|
+
return res.render("login", { error: null });
|
|
299
|
+
});
|
|
300
|
+
|
|
295
301
|
app.post("/login", (req, res, next) => {
|
|
296
302
|
if (!secrets.hasDashboardPassword()) return res.redirect("/setup");
|
|
297
303
|
|
package/package.json
CHANGED
package/public/dashboard.js
CHANGED
|
@@ -391,6 +391,19 @@
|
|
|
391
391
|
|
|
392
392
|
$("#conn-detail").textContent = (snapshot.detail || "") + retryHint(snapshot);
|
|
393
393
|
|
|
394
|
+
const alertBox = $("#conn-disconnect-alert");
|
|
395
|
+
const alertText = $("#conn-disconnect-text");
|
|
396
|
+
if (alertBox && alertText) {
|
|
397
|
+
if (snapshot.lastDisconnect?.statusCode && snapshot.state !== "connected" && snapshot.state !== "idle") {
|
|
398
|
+
const code = snapshot.lastDisconnect.statusCode;
|
|
399
|
+
const reason = snapshot.lastDisconnect.reason || "Unknown";
|
|
400
|
+
alertText.innerHTML = `⚠️ <strong>Connection failure (${esc(code)}: ${esc(reason)})</strong> — check network connectivity or proxy settings.`;
|
|
401
|
+
alertBox.style.display = "block";
|
|
402
|
+
} else {
|
|
403
|
+
alertBox.style.display = "none";
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
394
407
|
// The proxy line, and the one action that can apply a changed one. The
|
|
395
408
|
// label is already redacted server-side — there is no password to leak
|
|
396
409
|
// here because the browser was never told it.
|
package/src/bootstrap/panel.js
CHANGED
|
@@ -7,10 +7,21 @@
|
|
|
7
7
|
import { createRequire } from "module";
|
|
8
8
|
|
|
9
9
|
const require = createRequire(import.meta.url);
|
|
10
|
+
const fs = require("node:fs");
|
|
10
11
|
const logger = require("../utils/logger.cjs");
|
|
11
12
|
const settings = require("../config/settings.cjs");
|
|
13
|
+
const paths = require("../config/paths.cjs");
|
|
12
14
|
const { attach } = require("./events.cjs");
|
|
13
15
|
|
|
16
|
+
/** Unix socket path for the Android host. Bionic Node cannot bind TCP. */
|
|
17
|
+
export function androidPanelSocketPath() {
|
|
18
|
+
return paths.dataPath("panel.sock");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isAndroidHost() {
|
|
22
|
+
return process.env.LEVIX_ANDROID === "1";
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
/**
|
|
15
26
|
* Start the panel and bind its port.
|
|
16
27
|
*
|
|
@@ -55,6 +66,7 @@ export async function bootstrapPanel({ core } = {}) {
|
|
|
55
66
|
|
|
56
67
|
const port = settings.get("port");
|
|
57
68
|
const host = resolveBindAddress();
|
|
69
|
+
const androidSock = isAndroidHost() ? androidPanelSocketPath() : null;
|
|
58
70
|
|
|
59
71
|
await new Promise((resolve, reject) => {
|
|
60
72
|
// "EADDRINUSE" plus a stack trace is not an answer anyone can act on, and
|
|
@@ -72,10 +84,19 @@ export async function bootstrapPanel({ core } = {}) {
|
|
|
72
84
|
}
|
|
73
85
|
reject(error);
|
|
74
86
|
});
|
|
75
|
-
|
|
87
|
+
if (androidSock) {
|
|
88
|
+
try {
|
|
89
|
+
fs.unlinkSync(androidSock);
|
|
90
|
+
} catch {
|
|
91
|
+
// first run has no leftover socket
|
|
92
|
+
}
|
|
93
|
+
server.listen(androidSock, resolve);
|
|
94
|
+
} else {
|
|
95
|
+
server.listen(port, host, resolve);
|
|
96
|
+
}
|
|
76
97
|
});
|
|
77
98
|
|
|
78
|
-
return { port, host, server };
|
|
99
|
+
return { port, host, server, socketPath: androidSock };
|
|
79
100
|
}
|
|
80
101
|
|
|
81
102
|
/**
|
|
@@ -89,15 +110,18 @@ export async function bootstrapPanel({ core } = {}) {
|
|
|
89
110
|
export function resolveBindAddress() {
|
|
90
111
|
const configured = settings.get("bind_address");
|
|
91
112
|
if (configured) return configured;
|
|
92
|
-
//
|
|
93
|
-
//
|
|
94
|
-
|
|
113
|
+
// Desktop / Docker: every interface. Android never reaches this — it
|
|
114
|
+
// binds a unix socket instead (see androidPanelSocketPath). The WebView
|
|
115
|
+
// still opens http://127.0.0.1:port and the host serves that over the socket.
|
|
95
116
|
return "0.0.0.0";
|
|
96
117
|
}
|
|
97
118
|
|
|
98
119
|
/** The address a person should type, given how this install is reachable. */
|
|
99
120
|
export function panelUrl({ port, firstRun = false } = {}) {
|
|
100
121
|
const domain = settings.get("public_domain");
|
|
101
|
-
const
|
|
122
|
+
const host =
|
|
123
|
+
domain ||
|
|
124
|
+
(isAndroidHost() ? "127.0.0.1" : "localhost");
|
|
125
|
+
const base = domain ? `https://${host}` : `http://${host}:${port}`;
|
|
102
126
|
return firstRun ? `${base}/setup` : base;
|
|
103
127
|
}
|
package/src/commands/tts.cjs
CHANGED
|
@@ -22,15 +22,7 @@ const { sendBotMessage } = require("../utils/sendBotMessage.cjs");
|
|
|
22
22
|
|
|
23
23
|
const execFileAsync = promisify(execFile);
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
ffmpegStatic = require("ffmpeg-static");
|
|
28
|
-
} catch (err) {
|
|
29
|
-
logger.warn(
|
|
30
|
-
{ err: err?.message },
|
|
31
|
-
"[TTS] ffmpeg unavailable — will fall back to raw MP3"
|
|
32
|
-
);
|
|
33
|
-
}
|
|
25
|
+
const { ffmpegPath } = require("../utils/thumbnail.cjs");
|
|
34
26
|
|
|
35
27
|
function tmpPath(ext) {
|
|
36
28
|
return path.join(
|
|
@@ -56,37 +48,45 @@ async function synthesizeMp3(text, mp3Path) {
|
|
|
56
48
|
}
|
|
57
49
|
|
|
58
50
|
async function transcodeToOpus(mp3Path) {
|
|
59
|
-
|
|
51
|
+
const bin = ffmpegPath();
|
|
52
|
+
if (!bin) return null;
|
|
60
53
|
|
|
61
54
|
const oggPath = tmpPath("ogg");
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
55
|
+
try {
|
|
56
|
+
await execFileAsync(
|
|
57
|
+
bin,
|
|
58
|
+
[
|
|
59
|
+
"-hide_banner",
|
|
60
|
+
"-loglevel",
|
|
61
|
+
"error",
|
|
62
|
+
"-y",
|
|
63
|
+
"-i",
|
|
64
|
+
mp3Path,
|
|
65
|
+
"-vn",
|
|
66
|
+
"-c:a",
|
|
67
|
+
"libopus",
|
|
68
|
+
"-ac",
|
|
69
|
+
"1",
|
|
70
|
+
"-ar",
|
|
71
|
+
"48000",
|
|
72
|
+
"-b:a",
|
|
73
|
+
"48k",
|
|
74
|
+
"-application",
|
|
75
|
+
"voip",
|
|
76
|
+
"-f",
|
|
77
|
+
"ogg",
|
|
78
|
+
oggPath,
|
|
79
|
+
],
|
|
80
|
+
{ windowsHide: true, timeout: 30_000 },
|
|
81
|
+
);
|
|
82
|
+
return oggPath;
|
|
83
|
+
} catch (err) {
|
|
84
|
+
logger.warn(
|
|
85
|
+
{ err: err?.message, bin },
|
|
86
|
+
"[TTS] ffmpeg transcode failed — will fall back to raw MP3"
|
|
87
|
+
);
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
module.exports = {
|
package/src/core/session.js
CHANGED
|
@@ -124,6 +124,7 @@ export const SESSION_STATES = Object.freeze({
|
|
|
124
124
|
LINKING: "linking",
|
|
125
125
|
CONNECTED: "connected",
|
|
126
126
|
RECONNECTING: "reconnecting",
|
|
127
|
+
PAUSED: "paused",
|
|
127
128
|
DISCONNECTED: "disconnected",
|
|
128
129
|
RETRY_EXHAUSTED: "retry_exhausted",
|
|
129
130
|
LOGGED_OUT: "logged_out",
|
|
@@ -139,6 +140,7 @@ const BUSY_STATES = new Set([
|
|
|
139
140
|
S.LINKING,
|
|
140
141
|
S.CONNECTED,
|
|
141
142
|
S.RECONNECTING,
|
|
143
|
+
S.PAUSED,
|
|
142
144
|
]);
|
|
143
145
|
|
|
144
146
|
/** What the dashboard's status pill has always been told. Kept stable. */
|
|
@@ -148,6 +150,7 @@ function legacyStatus(state, hasQr) {
|
|
|
148
150
|
if (state === S.STARTING) return "Connecting";
|
|
149
151
|
if (state === S.LINKING) return "Linking";
|
|
150
152
|
if (state === S.RECONNECTING) return "Reconnecting";
|
|
153
|
+
if (state === S.PAUSED) return "Paused (Offline)";
|
|
151
154
|
return "Disconnected";
|
|
152
155
|
}
|
|
153
156
|
|
|
@@ -195,6 +198,8 @@ export class WhatsAppSession {
|
|
|
195
198
|
#reason = null;
|
|
196
199
|
#detail = null;
|
|
197
200
|
#lastDisconnect = null;
|
|
201
|
+
#isOnline = true;
|
|
202
|
+
#wasActiveBeforeOffline = false;
|
|
198
203
|
|
|
199
204
|
constructor({
|
|
200
205
|
createSocket = createWhatsAppSocket,
|
|
@@ -290,6 +295,7 @@ export class WhatsAppSession {
|
|
|
290
295
|
// answer crosses the wire, never the fingerprint it was computed from.
|
|
291
296
|
proxyChanged: this.#proxyIsStale(),
|
|
292
297
|
connected: this.#state === S.CONNECTED,
|
|
298
|
+
isOnline: this.#isOnline,
|
|
293
299
|
terminal,
|
|
294
300
|
hasQr: !!this.#qr,
|
|
295
301
|
hasPairingCode: !!this.#pairingCode,
|
|
@@ -309,6 +315,52 @@ export class WhatsAppSession {
|
|
|
309
315
|
// commands
|
|
310
316
|
// -------------------------------------------------------------------------
|
|
311
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Set device internet connectivity state.
|
|
320
|
+
*
|
|
321
|
+
* When offline (false):
|
|
322
|
+
* - Pauses active connection attempts and cancels pending retry timers.
|
|
323
|
+
* - Transitions to S.PAUSED without exhausting retries.
|
|
324
|
+
*
|
|
325
|
+
* When online (true):
|
|
326
|
+
* - Restores connection if it was active before going offline or currently paused.
|
|
327
|
+
* - Resets the retry attempt counter so a fresh retry ladder is available.
|
|
328
|
+
*/
|
|
329
|
+
async setInternetOnline(online, { reason = "network" } = {}) {
|
|
330
|
+
const isOnline = !!online;
|
|
331
|
+
if (this.#isOnline === isOnline) return this.getState();
|
|
332
|
+
this.#isOnline = isOnline;
|
|
333
|
+
|
|
334
|
+
if (!isOnline) {
|
|
335
|
+
this.log.warn("[Session] Device is offline — pausing WhatsApp connection and retries");
|
|
336
|
+
this.#wasActiveBeforeOffline =
|
|
337
|
+
BUSY_STATES.has(this.#state) || this.#state === S.PAUSED;
|
|
338
|
+
this.#cancelRetry();
|
|
339
|
+
|
|
340
|
+
if (this.#socket) {
|
|
341
|
+
await this.#destroySocket();
|
|
342
|
+
}
|
|
343
|
+
this.#transition(S.PAUSED, {
|
|
344
|
+
reason: "offline",
|
|
345
|
+
detail: "Device is offline. Connection and retries are paused until internet is restored.",
|
|
346
|
+
});
|
|
347
|
+
return this.getState();
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
this.log.info("[Session] Device is back online — restoring WhatsApp connection");
|
|
351
|
+
if (this.#wasActiveBeforeOffline || this.#state === S.PAUSED) {
|
|
352
|
+
this.#wasActiveBeforeOffline = false;
|
|
353
|
+
this.#attempt = 0;
|
|
354
|
+
this.#cancelRetry();
|
|
355
|
+
await this.start({ reason: "network_restored" });
|
|
356
|
+
}
|
|
357
|
+
return this.getState();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
get isOnline() {
|
|
361
|
+
return this.#isOnline;
|
|
362
|
+
}
|
|
363
|
+
|
|
312
364
|
/**
|
|
313
365
|
* Bring the session up. Idempotent: if a socket exists or is being made, this
|
|
314
366
|
* returns the state it is already in and creates nothing.
|
|
@@ -316,11 +368,15 @@ export class WhatsAppSession {
|
|
|
316
368
|
async start({ reason = "manual", method, phone } = {}) {
|
|
317
369
|
if (this.#shuttingDown) return this.getState();
|
|
318
370
|
|
|
371
|
+
if (this.#state === S.PAUSED && !this.#isOnline) {
|
|
372
|
+
return this.getState();
|
|
373
|
+
}
|
|
374
|
+
|
|
319
375
|
if (this.#starting) {
|
|
320
376
|
await this.#starting.catch(() => {});
|
|
321
377
|
return this.getState();
|
|
322
378
|
}
|
|
323
|
-
if (BUSY_STATES.has(this.#state)) return this.getState();
|
|
379
|
+
if (BUSY_STATES.has(this.#state) && this.#state !== S.PAUSED) return this.getState();
|
|
324
380
|
|
|
325
381
|
this.#pairingIntent = parseStartOptions({ method, phone });
|
|
326
382
|
|
|
@@ -451,6 +507,15 @@ export class WhatsAppSession {
|
|
|
451
507
|
// -------------------------------------------------------------------------
|
|
452
508
|
|
|
453
509
|
async #open({ reason }) {
|
|
510
|
+
if (!this.#isOnline) {
|
|
511
|
+
this.#wasActiveBeforeOffline = true;
|
|
512
|
+
this.#transition(S.PAUSED, {
|
|
513
|
+
reason: "offline",
|
|
514
|
+
detail: "Device is offline. Connection and retries are paused until internet is restored.",
|
|
515
|
+
});
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
|
|
454
519
|
// Whatever code was on offer belonged to the previous attempt.
|
|
455
520
|
this.#clearQr();
|
|
456
521
|
this.#transition(S.STARTING, { reason });
|
|
@@ -790,13 +855,23 @@ export class WhatsAppSession {
|
|
|
790
855
|
|
|
791
856
|
if (this.#shuttingDown) return;
|
|
792
857
|
|
|
858
|
+
if (!this.#isOnline) {
|
|
859
|
+
this.#wasActiveBeforeOffline = true;
|
|
860
|
+
this.#cancelRetry();
|
|
861
|
+
return this.#transition(S.PAUSED, {
|
|
862
|
+
reason: "offline",
|
|
863
|
+
detail: `Connection closed (${statusCode ?? "no status"}: ${reasonText}). Paused until internet is restored.`,
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
|
|
793
867
|
if (verdict.loggedOut) return this.#handleLoggedOut(verdict, closedSocket);
|
|
794
868
|
if (verdict.terminal) {
|
|
795
869
|
this.#clearQr();
|
|
796
870
|
this.#pairing = false;
|
|
871
|
+
const codeInfo = this.#lastDisconnect?.statusCode ? ` (${this.#lastDisconnect.statusCode}: ${this.#lastDisconnect.reason})` : "";
|
|
797
872
|
return this.#transition(S.DISCONNECTED, {
|
|
798
873
|
reason: verdict.reason,
|
|
799
|
-
detail: verdict.detail,
|
|
874
|
+
detail: `${verdict.detail || ""}${codeInfo}`.trim(),
|
|
800
875
|
});
|
|
801
876
|
}
|
|
802
877
|
|
|
@@ -805,9 +880,10 @@ export class WhatsAppSession {
|
|
|
805
880
|
if (this.#pairing && !verdict.restartRequired) {
|
|
806
881
|
this.#pairing = false;
|
|
807
882
|
this.#clearQr();
|
|
883
|
+
const codeInfo = this.#lastDisconnect?.statusCode ? ` (${this.#lastDisconnect.statusCode}: ${this.#lastDisconnect.reason})` : "";
|
|
808
884
|
return this.#transition(S.DISCONNECTED, {
|
|
809
885
|
reason: "pairing_cancelled",
|
|
810
|
-
detail:
|
|
886
|
+
detail: `The pairing attempt ended before the code was scanned${codeInfo}. Start a session to try again.`,
|
|
811
887
|
});
|
|
812
888
|
}
|
|
813
889
|
|
|
@@ -846,6 +922,15 @@ export class WhatsAppSession {
|
|
|
846
922
|
// -------------------------------------------------------------------------
|
|
847
923
|
|
|
848
924
|
#scheduleRetry(verdict) {
|
|
925
|
+
if (!this.#isOnline) {
|
|
926
|
+
this.#wasActiveBeforeOffline = true;
|
|
927
|
+
this.#cancelRetry();
|
|
928
|
+
return this.#transition(S.PAUSED, {
|
|
929
|
+
reason: "offline",
|
|
930
|
+
detail: "Device is offline. Connection and retries are paused until internet is restored.",
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
|
|
849
934
|
// Never two timers. A close that arrives while one is pending replaces it
|
|
850
935
|
// rather than adding to it.
|
|
851
936
|
this.#cancelRetry();
|
|
@@ -855,9 +940,10 @@ export class WhatsAppSession {
|
|
|
855
940
|
`[Session] ${this.retryDelaysMs.length} reconnect attempts failed. Levix stays up; start the session again from the panel.`
|
|
856
941
|
);
|
|
857
942
|
this.#clearQr();
|
|
943
|
+
const codeInfo = this.#lastDisconnect?.statusCode ? ` (${this.#lastDisconnect.statusCode}: ${this.#lastDisconnect.reason})` : "";
|
|
858
944
|
return this.#transition(S.RETRY_EXHAUSTED, {
|
|
859
945
|
reason: "retry_exhausted",
|
|
860
|
-
detail: `Gave up after ${this.retryDelaysMs.length} attempts. Start the session to try again.`,
|
|
946
|
+
detail: `Gave up after ${this.retryDelaysMs.length} attempts${codeInfo}. Start the session to try again.`,
|
|
861
947
|
});
|
|
862
948
|
}
|
|
863
949
|
|
|
@@ -869,17 +955,12 @@ export class WhatsAppSession {
|
|
|
869
955
|
`[Session] Reconnecting in ${Math.round(delayMs / 1000)}s (attempt ${this.#attempt}/${this.retryDelaysMs.length})`
|
|
870
956
|
);
|
|
871
957
|
|
|
958
|
+
const codeInfo = this.#lastDisconnect?.statusCode ? ` (${this.#lastDisconnect.statusCode}: ${this.#lastDisconnect.reason})` : "";
|
|
872
959
|
this.#transition(S.RECONNECTING, {
|
|
873
960
|
reason: verdict.reason,
|
|
874
|
-
// The close's own explanation first: a 515 straight after a scan is
|
|
875
|
-
// WhatsApp asking for a fresh connection, not a failed attempt, and
|
|
876
|
-
// "attempt 1 of 5" on its own reads like something went wrong.
|
|
877
|
-
// No countdown in here: this string is baked once, at schedule time, and
|
|
878
|
-
// would go on claiming "in 5s" for the whole wait. The panel renders the
|
|
879
|
-
// remaining seconds from nextRetryAt, which actually ticks.
|
|
880
961
|
detail:
|
|
881
962
|
`${verdict.detail ? `${verdict.detail} ` : ""}` +
|
|
882
|
-
`Reconnecting (attempt ${this.#attempt} of ${this.retryDelaysMs.length}).`,
|
|
963
|
+
`Reconnecting (attempt ${this.#attempt} of ${this.retryDelaysMs.length})${codeInfo}.`,
|
|
883
964
|
});
|
|
884
965
|
|
|
885
966
|
this.#retryTimer = setTimeout(() => {
|
package/src/index.js
CHANGED
|
@@ -41,6 +41,10 @@ function short(path) {
|
|
|
41
41
|
// soon as the core is up.
|
|
42
42
|
let liveSession = null;
|
|
43
43
|
|
|
44
|
+
export function getLiveSession() {
|
|
45
|
+
return liveSession;
|
|
46
|
+
}
|
|
47
|
+
|
|
44
48
|
/**
|
|
45
49
|
* Start Levix.
|
|
46
50
|
*
|
|
@@ -194,9 +198,16 @@ async function startWithPanel({ open }) {
|
|
|
194
198
|
try {
|
|
195
199
|
panel = await bootstrapPanel({ core });
|
|
196
200
|
} catch (error) {
|
|
197
|
-
if (error.code === "EADDRINUSE") {
|
|
201
|
+
if (error.code === "EADDRINUSE" || error.code === "ECONNREFUSED") {
|
|
198
202
|
console.error(`\n ${error.message}\n`);
|
|
199
203
|
logger.error(error.message);
|
|
204
|
+
if (process.env.LEVIX_ANDROID === "1") {
|
|
205
|
+
// WhatsApp still works without the panel. process.exit here also
|
|
206
|
+
// trips pino's sonic-boom ("not ready yet") as a second crash.
|
|
207
|
+
console.log("Panel failed " + (error.code || error.message));
|
|
208
|
+
console.log("Levix ready");
|
|
209
|
+
return { ...core, panel: null, url: null };
|
|
210
|
+
}
|
|
200
211
|
process.exit(1);
|
|
201
212
|
}
|
|
202
213
|
throw error;
|
|
@@ -912,6 +912,21 @@ router.post(
|
|
|
912
912
|
})
|
|
913
913
|
);
|
|
914
914
|
|
|
915
|
+
router.post(
|
|
916
|
+
"/bot/session/network",
|
|
917
|
+
asyncRoute(async (req, res) => {
|
|
918
|
+
if (!session) {
|
|
919
|
+
return res.status(503).json({ success: false, error: "Session manager is not ready" });
|
|
920
|
+
}
|
|
921
|
+
const { online } = req.body || {};
|
|
922
|
+
if (typeof online !== "boolean") {
|
|
923
|
+
return badRequest(res, "online (boolean) is required");
|
|
924
|
+
}
|
|
925
|
+
const state = await session.setInternetOnline(online);
|
|
926
|
+
res.json({ success: true, session: state });
|
|
927
|
+
})
|
|
928
|
+
);
|
|
929
|
+
|
|
915
930
|
// Unlink the WhatsApp account: WhatsApp drops the companion device and the
|
|
916
931
|
// stored credentials go with it. Levix stays up, and the operator can start a
|
|
917
932
|
// fresh pairing whenever they want one.
|
|
@@ -78,7 +78,10 @@ function hasForwardingHeader(headers = {}) {
|
|
|
78
78
|
* the right key for rate limiting a password or setup-code guess.
|
|
79
79
|
*/
|
|
80
80
|
function clientAddress(req) {
|
|
81
|
-
|
|
81
|
+
const addr = req?.socket?.remoteAddress || req?.connection?.remoteAddress;
|
|
82
|
+
if (addr) return addr;
|
|
83
|
+
if (process.env.LEVIX_ANDROID === "1") return "127.0.0.1";
|
|
84
|
+
return "";
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
/**
|
package/src/utils/thumbnail.cjs
CHANGED
|
@@ -48,9 +48,22 @@ const ffmpegStatic = (() => {
|
|
|
48
48
|
})();
|
|
49
49
|
|
|
50
50
|
// Read at call time so the dashboard can point at another binary without a
|
|
51
|
-
// restart; ffmpeg-static is what ships with the bot
|
|
51
|
+
// restart; ffmpeg-static is what ships with the bot on desktop/docker.
|
|
52
|
+
// On Android, check LEVIX_FFMPEG_PATH or libffmpeg.so in nativeLibraryDir (LD_LIBRARY_PATH).
|
|
52
53
|
function ffmpegBin() {
|
|
53
|
-
|
|
54
|
+
const configured = settings.get("ffmpeg_path");
|
|
55
|
+
if (configured) return configured;
|
|
56
|
+
if (process.env.LEVIX_FFMPEG_PATH && fs.existsSync(process.env.LEVIX_FFMPEG_PATH)) {
|
|
57
|
+
return process.env.LEVIX_FFMPEG_PATH;
|
|
58
|
+
}
|
|
59
|
+
if (process.env.FFMPEG_PATH && fs.existsSync(process.env.FFMPEG_PATH)) {
|
|
60
|
+
return process.env.FFMPEG_PATH;
|
|
61
|
+
}
|
|
62
|
+
if (process.env.LEVIX_ANDROID === "1" && process.env.LD_LIBRARY_PATH) {
|
|
63
|
+
const androidBin = path.join(process.env.LD_LIBRARY_PATH, "libffmpeg.so");
|
|
64
|
+
if (fs.existsSync(androidBin)) return androidBin;
|
|
65
|
+
}
|
|
66
|
+
return ffmpegStatic || "ffmpeg";
|
|
54
67
|
}
|
|
55
68
|
const FFMPEG_TIMEOUT_MS = 20000;
|
|
56
69
|
|
package/views/dashboard.ejs
CHANGED
|
@@ -316,6 +316,9 @@
|
|
|
316
316
|
</div>
|
|
317
317
|
|
|
318
318
|
<div class="hint session-detail" id="conn-detail"></div>
|
|
319
|
+
<div id="conn-disconnect-alert" class="session-error-callout" style="display: none; margin-top: 10px; padding: 10px 14px; border-radius: 8px; background: rgba(239, 68, 68, 0.12); color: #ef4444; font-size: 13px; font-weight: 500; border: 1px solid rgba(239, 68, 68, 0.25);">
|
|
320
|
+
<span id="conn-disconnect-text"></span>
|
|
321
|
+
</div>
|
|
319
322
|
|
|
320
323
|
<div class="hint" id="conn-proxy"></div>
|
|
321
324
|
|