levix-bot 2.3.0 → 3.0.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/public/dashboard.css +42 -0
- package/public/dashboard.js +92 -9
- package/src/bootstrap/panel.js +5 -1
- package/src/bootstrap/session-startup-policy.js +6 -3
- package/src/config/baileys.config.js +7 -2
- package/src/core/session.js +137 -7
- package/src/core/socket.js +7 -8
- package/src/index.js +8 -0
- package/src/routes/dashboard.api.esm.js +17 -2
- package/src/utils/logger.cjs +7 -1
- package/views/dashboard.ejs +37 -2
package/package.json
CHANGED
package/public/dashboard.css
CHANGED
|
@@ -1825,6 +1825,48 @@ td.wrap {
|
|
|
1825
1825
|
.steps li::marker {
|
|
1826
1826
|
color: var(--blue-bright);
|
|
1827
1827
|
font-family: var(--mono);
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
.pair-chooser {
|
|
1831
|
+
margin: 16px 0 8px;
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
.pair-methods {
|
|
1835
|
+
display: flex;
|
|
1836
|
+
flex-wrap: wrap;
|
|
1837
|
+
gap: 12px 20px;
|
|
1838
|
+
margin: 8px 0 12px;
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
.pair-option {
|
|
1842
|
+
display: flex;
|
|
1843
|
+
align-items: center;
|
|
1844
|
+
gap: 8px;
|
|
1845
|
+
cursor: pointer;
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
.pair-phone-wrap {
|
|
1849
|
+
display: grid;
|
|
1850
|
+
gap: 6px;
|
|
1851
|
+
max-width: 360px;
|
|
1852
|
+
margin-bottom: 12px;
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
.pair-phone-wrap[hidden] {
|
|
1856
|
+
display: none;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
.pairing-code {
|
|
1860
|
+
font-family: var(--mono);
|
|
1861
|
+
font-size: 2rem;
|
|
1862
|
+
letter-spacing: 0.18em;
|
|
1863
|
+
font-weight: 700;
|
|
1864
|
+
padding: 18px 24px;
|
|
1865
|
+
border-radius: 14px;
|
|
1866
|
+
background: var(--panel-raised);
|
|
1867
|
+
color: var(--text);
|
|
1868
|
+
border: 1px solid var(--line);
|
|
1869
|
+
}
|
|
1828
1870
|
font-weight: 700;
|
|
1829
1871
|
}
|
|
1830
1872
|
|
package/public/dashboard.js
CHANGED
|
@@ -190,7 +190,7 @@
|
|
|
190
190
|
const SESSION_LABELS = {
|
|
191
191
|
idle: { tone: "off", label: "Not started", tag: "danger", tagText: "Not started" },
|
|
192
192
|
starting: { tone: "warn", label: "Connecting", tag: "warn", tagText: "Connecting" },
|
|
193
|
-
waiting_for_qr: { tone: "warn", label: "Waiting
|
|
193
|
+
waiting_for_qr: { tone: "warn", label: "Waiting to link", tag: "warn", tagText: "Waiting to link" },
|
|
194
194
|
linking: { tone: "warn", label: "Linking", tag: "warn", tagText: "Linking" },
|
|
195
195
|
connected: { tone: "on", label: "Connected", tag: "ok", tagText: "Linked" },
|
|
196
196
|
reconnecting: { tone: "warn", label: "Reconnecting", tag: "warn", tagText: "Reconnecting" },
|
|
@@ -284,6 +284,28 @@
|
|
|
284
284
|
// --- connection ---------------------------------------------------------
|
|
285
285
|
|
|
286
286
|
let qrRendered = null;
|
|
287
|
+
let pairingRendered = null;
|
|
288
|
+
|
|
289
|
+
function formatPairingCode(code) {
|
|
290
|
+
const raw = String(code || "").replace(/\s|-/g, "");
|
|
291
|
+
if (raw.length === 8) return `${raw.slice(0, 4)}-${raw.slice(4)}`;
|
|
292
|
+
return raw;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function preferredPairMethod() {
|
|
296
|
+
return /Android|iPhone|Mobile/i.test(navigator.userAgent) ? "pairing" : "qr";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function selectedPairMethod() {
|
|
300
|
+
const picked = document.querySelector('input[name="pair-method"]:checked');
|
|
301
|
+
return picked ? picked.value : preferredPairMethod();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function syncPairChooser() {
|
|
305
|
+
const pairing = selectedPairMethod() === "pairing";
|
|
306
|
+
const wrap = $("#pair-phone-wrap");
|
|
307
|
+
if (wrap) wrap.hidden = !pairing;
|
|
308
|
+
}
|
|
287
309
|
|
|
288
310
|
function renderQr(text) {
|
|
289
311
|
if (!text || qrRendered === text) return;
|
|
@@ -307,6 +329,31 @@
|
|
|
307
329
|
$("#qr-note").textContent = "Waiting for a QR code…";
|
|
308
330
|
}
|
|
309
331
|
|
|
332
|
+
function renderPairingCode(code, phone) {
|
|
333
|
+
if (!code || pairingRendered === code) return;
|
|
334
|
+
pairingRendered = code;
|
|
335
|
+
const el = $("#pairing-code");
|
|
336
|
+
if (el) el.textContent = formatPairingCode(code);
|
|
337
|
+
const phoneEl = $("#pairing-phone");
|
|
338
|
+
if (phoneEl) {
|
|
339
|
+
phoneEl.textContent = phone ? `For WhatsApp +${phone}` : "";
|
|
340
|
+
}
|
|
341
|
+
const note = $("#pairing-note");
|
|
342
|
+
if (note) {
|
|
343
|
+
note.textContent = "Link with phone number instead — not the QR camera.";
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function clearPairingCode() {
|
|
348
|
+
pairingRendered = null;
|
|
349
|
+
const el = $("#pairing-code");
|
|
350
|
+
if (el) el.textContent = "••••-••••";
|
|
351
|
+
const phoneEl = $("#pairing-phone");
|
|
352
|
+
if (phoneEl) phoneEl.textContent = "";
|
|
353
|
+
const note = $("#pairing-note");
|
|
354
|
+
if (note) note.textContent = "Waiting for a pairing code…";
|
|
355
|
+
}
|
|
356
|
+
|
|
310
357
|
function retryHint(snapshot) {
|
|
311
358
|
if (snapshot.state !== "reconnecting" || !snapshot.nextRetryAt) return "";
|
|
312
359
|
const seconds = Math.max(0, Math.round((snapshot.nextRetryAt - Date.now()) / 1000));
|
|
@@ -378,10 +425,21 @@
|
|
|
378
425
|
// that can only be cleared by unlinking.
|
|
379
426
|
$("#unlink-btn").disabled = !snapshot.canUnlink;
|
|
380
427
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
428
|
+
const chooser = $("#pair-chooser");
|
|
429
|
+
if (chooser) chooser.style.display = snapshot.canStart ? "" : "none";
|
|
430
|
+
|
|
431
|
+
// Offer QR or pairing code only while a link attempt is actually waiting.
|
|
432
|
+
const waiting = snapshot.state === "waiting_for_qr";
|
|
433
|
+
$("#qr-panel").style.display = waiting ? "block" : "none";
|
|
434
|
+
const showCode = waiting && (snapshot.hasPairingCode || snapshot.pairingMethod === "pairing");
|
|
435
|
+
const qrBox = $("#qr-box");
|
|
436
|
+
const pairingBox = $("#pairing-box");
|
|
437
|
+
if (qrBox) qrBox.hidden = showCode;
|
|
438
|
+
if (pairingBox) pairingBox.hidden = !showCode;
|
|
439
|
+
if (!waiting) {
|
|
440
|
+
clearQr();
|
|
441
|
+
clearPairingCode();
|
|
442
|
+
}
|
|
385
443
|
}
|
|
386
444
|
|
|
387
445
|
VIEWS.connection = {
|
|
@@ -390,10 +448,13 @@
|
|
|
390
448
|
// offer before this tab existed — a reload has to show the real backend
|
|
391
449
|
// state, not an empty frame.
|
|
392
450
|
load: guard(async () => {
|
|
393
|
-
const { session, qr } = await api("/bot/session");
|
|
451
|
+
const { session, qr, pairingCode } = await api("/bot/session");
|
|
394
452
|
setStatus(session);
|
|
395
453
|
renderConnection();
|
|
396
|
-
if (
|
|
454
|
+
if (session.state === "waiting_for_qr") {
|
|
455
|
+
if (pairingCode) renderPairingCode(pairingCode, session.pairingPhone);
|
|
456
|
+
else if (qr) renderQr(qr);
|
|
457
|
+
}
|
|
397
458
|
}),
|
|
398
459
|
};
|
|
399
460
|
|
|
@@ -415,7 +476,10 @@
|
|
|
415
476
|
// request must not leave a dead button behind.
|
|
416
477
|
$("#start-btn").disabled = true;
|
|
417
478
|
try {
|
|
418
|
-
const
|
|
479
|
+
const method = selectedPairMethod();
|
|
480
|
+
const body = { method };
|
|
481
|
+
if (method === "pairing") body.phone = ($("#pair-phone")?.value || "").trim();
|
|
482
|
+
const { session } = await api("/bot/session/start", { method: "POST", body });
|
|
419
483
|
setStatus(session);
|
|
420
484
|
toast("Session starting…", "ok");
|
|
421
485
|
} finally {
|
|
@@ -451,7 +515,7 @@
|
|
|
451
515
|
$("#unlink-btn").addEventListener(
|
|
452
516
|
"click",
|
|
453
517
|
confirmed(
|
|
454
|
-
"Unlink this WhatsApp account? The bot stops answering until you start a new session and
|
|
518
|
+
"Unlink this WhatsApp account? The bot stops answering until you start a new session and link again.",
|
|
455
519
|
async () => {
|
|
456
520
|
const { session } = await api("/bot/logout", { method: "POST" });
|
|
457
521
|
setStatus(session);
|
|
@@ -1287,6 +1351,16 @@
|
|
|
1287
1351
|
|
|
1288
1352
|
socket.on("qr_cleared", () => clearQr());
|
|
1289
1353
|
|
|
1354
|
+
socket.on("pairing_code", (payload) => {
|
|
1355
|
+
const code = payload && payload.code;
|
|
1356
|
+
if (code) renderPairingCode(code, payload.phone);
|
|
1357
|
+
if (currentView !== "connection") {
|
|
1358
|
+
toast("A pairing code is ready — open Connection to enter it.");
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
socket.on("pairing_code_cleared", () => clearPairingCode());
|
|
1363
|
+
|
|
1290
1364
|
// A socket that dropped may have missed transitions while it was away.
|
|
1291
1365
|
socket.on("connect", () => {
|
|
1292
1366
|
if (currentView === "connection") VIEWS.connection.load();
|
|
@@ -1297,6 +1371,15 @@
|
|
|
1297
1371
|
initTheme();
|
|
1298
1372
|
initRouting();
|
|
1299
1373
|
initConnectionActions();
|
|
1374
|
+
{
|
|
1375
|
+
const def = preferredPairMethod();
|
|
1376
|
+
const radio = document.querySelector(`input[name="pair-method"][value="${def}"]`);
|
|
1377
|
+
if (radio) radio.checked = true;
|
|
1378
|
+
document.querySelectorAll('input[name="pair-method"]').forEach((el) => {
|
|
1379
|
+
el.addEventListener("change", syncPairChooser);
|
|
1380
|
+
});
|
|
1381
|
+
syncPairChooser();
|
|
1382
|
+
}
|
|
1300
1383
|
initCommandActions();
|
|
1301
1384
|
initAiActions();
|
|
1302
1385
|
initGroupActions();
|
package/src/bootstrap/panel.js
CHANGED
|
@@ -88,7 +88,11 @@ export async function bootstrapPanel({ core } = {}) {
|
|
|
88
88
|
*/
|
|
89
89
|
export function resolveBindAddress() {
|
|
90
90
|
const configured = settings.get("bind_address");
|
|
91
|
-
|
|
91
|
+
if (configured) return configured;
|
|
92
|
+
// The phone is the host: keep the panel on loopback until a later phase
|
|
93
|
+
// opens it in a WebView. LAN bind stays an explicit setting.
|
|
94
|
+
if (process.env.LEVIX_ANDROID === "1") return "127.0.0.1";
|
|
95
|
+
return "0.0.0.0";
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
/** The address a person should type, given how this install is reachable. */
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// Decide whether the WhatsApp session should be started during process boot.
|
|
2
2
|
//
|
|
3
3
|
// Baileys writes a `creds` row as soon as auth state is initialized, even before
|
|
4
|
-
// a QR is successfully paired.
|
|
5
|
-
//
|
|
4
|
+
// a QR is successfully paired. `requestPairingCode` also writes `creds.me.id`
|
|
5
|
+
// before the phone accepts the code. A finished link has `me.id` and is not
|
|
6
|
+
// still `registered: false`.
|
|
6
7
|
export function hasPairedCredentials(rawCreds) {
|
|
7
8
|
if (!rawCreds) return false;
|
|
8
9
|
try {
|
|
9
10
|
const creds = typeof rawCreds === "string" ? JSON.parse(rawCreds) : rawCreds;
|
|
10
|
-
|
|
11
|
+
if (!creds?.me?.id) return false;
|
|
12
|
+
if (creds.registered === false) return false;
|
|
13
|
+
return true;
|
|
11
14
|
} catch {
|
|
12
15
|
return false;
|
|
13
16
|
}
|
|
@@ -22,8 +22,13 @@ export function setRecentMessageGetter(fn) {
|
|
|
22
22
|
getRecentMessage = fn;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
// QR pairing is happy with a Windows desktop companion. Pairing *codes*
|
|
26
|
+
// are not: WhatsApp rejects them unless the companion looks like Chrome.
|
|
27
|
+
export const BAILEYS_BROWSER_QR = Browsers.windows("Desktop");
|
|
28
|
+
export const BAILEYS_BROWSER_PAIRING = Browsers.macOS("Chrome");
|
|
29
|
+
|
|
30
|
+
export const getBaileysConfig = (cachedGroupMetadata, { pairingCode = false } = {}) => ({
|
|
31
|
+
browser: pairingCode ? BAILEYS_BROWSER_PAIRING : BAILEYS_BROWSER_QR,
|
|
27
32
|
markOnlineOnConnect: false,
|
|
28
33
|
logger: pino({ level: "silent" }),
|
|
29
34
|
cachedGroupMetadata,
|
package/src/core/session.js
CHANGED
|
@@ -78,6 +78,45 @@ function withTimeout(promise, ms) {
|
|
|
78
78
|
]);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
const ARABIC_INDIC = /[\u0660-\u0669]/g;
|
|
82
|
+
const EASTERN_ARABIC = /[\u06F0-\u06F9]/g;
|
|
83
|
+
|
|
84
|
+
/** Digits-only international number for Baileys `requestPairingCode`. */
|
|
85
|
+
export function normalizePairingPhone(input) {
|
|
86
|
+
let value = String(input ?? "");
|
|
87
|
+
value = value.replace(ARABIC_INDIC, (ch) => String(ch.charCodeAt(0) - 0x0660));
|
|
88
|
+
value = value.replace(EASTERN_ARABIC, (ch) => String(ch.charCodeAt(0) - 0x06f0));
|
|
89
|
+
const digits = value.replace(/\D/g, "");
|
|
90
|
+
if (digits.startsWith("0")) {
|
|
91
|
+
const error = new Error(
|
|
92
|
+
"Drop the leading 0 and include the country code (e.g. 2010… not 010…)."
|
|
93
|
+
);
|
|
94
|
+
error.code = "PAIRING_PHONE";
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
if (digits.length < 8 || digits.length > 15) {
|
|
98
|
+
const error = new Error(
|
|
99
|
+
"Enter the WhatsApp number with country code, digits only (e.g. 2010…)."
|
|
100
|
+
);
|
|
101
|
+
error.code = "PAIRING_PHONE";
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
return digits;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** How this start should pair, if the install is not linked yet. */
|
|
108
|
+
export function parseStartOptions({ method, phone } = {}) {
|
|
109
|
+
if (method == null || method === "" || method === "qr") {
|
|
110
|
+
return { method: "qr", phone: null };
|
|
111
|
+
}
|
|
112
|
+
if (method === "pairing") {
|
|
113
|
+
return { method: "pairing", phone: normalizePairingPhone(phone) };
|
|
114
|
+
}
|
|
115
|
+
const error = new Error("Choose QR code or pairing code.");
|
|
116
|
+
error.code = "PAIRING_PHONE";
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
|
|
81
120
|
export const SESSION_STATES = Object.freeze({
|
|
82
121
|
IDLE: "idle",
|
|
83
122
|
STARTING: "starting",
|
|
@@ -150,6 +189,9 @@ export class WhatsAppSession {
|
|
|
150
189
|
#proxyConfig = null;
|
|
151
190
|
|
|
152
191
|
#qr = null;
|
|
192
|
+
#pairingCode = null;
|
|
193
|
+
#pairingIntent = { method: "qr", phone: null };
|
|
194
|
+
#pairingCodeRequest = null;
|
|
153
195
|
#reason = null;
|
|
154
196
|
#detail = null;
|
|
155
197
|
#lastDisconnect = null;
|
|
@@ -173,6 +215,7 @@ export class WhatsAppSession {
|
|
|
173
215
|
emit = () => {},
|
|
174
216
|
retryDelaysMs = RETRY_SCHEDULE_MS,
|
|
175
217
|
log = logger,
|
|
218
|
+
pairingCodeDelayMs = 1500,
|
|
176
219
|
} = {}) {
|
|
177
220
|
this.createSocket = createSocket;
|
|
178
221
|
this.attachListeners = attachListeners;
|
|
@@ -185,6 +228,7 @@ export class WhatsAppSession {
|
|
|
185
228
|
this.clearCredentials = clearCredentials;
|
|
186
229
|
this.retryDelaysMs = retryDelaysMs;
|
|
187
230
|
this.log = log;
|
|
231
|
+
this.pairingCodeDelayMs = pairingCodeDelayMs;
|
|
188
232
|
}
|
|
189
233
|
|
|
190
234
|
// -------------------------------------------------------------------------
|
|
@@ -212,6 +256,13 @@ export class WhatsAppSession {
|
|
|
212
256
|
return this.#qr;
|
|
213
257
|
}
|
|
214
258
|
|
|
259
|
+
/**
|
|
260
|
+
* The 8-character pairing code, or null. Same rule as `qr`: not in getState().
|
|
261
|
+
*/
|
|
262
|
+
get pairingCode() {
|
|
263
|
+
return this.#pairingCode;
|
|
264
|
+
}
|
|
265
|
+
|
|
215
266
|
/** Everything the dashboard needs to render the Connection screen. */
|
|
216
267
|
getState() {
|
|
217
268
|
const terminal =
|
|
@@ -241,6 +292,10 @@ export class WhatsAppSession {
|
|
|
241
292
|
connected: this.#state === S.CONNECTED,
|
|
242
293
|
terminal,
|
|
243
294
|
hasQr: !!this.#qr,
|
|
295
|
+
hasPairingCode: !!this.#pairingCode,
|
|
296
|
+
pairingMethod: this.#pairingIntent?.method || "qr",
|
|
297
|
+
pairingPhone:
|
|
298
|
+
this.#pairingIntent?.method === "pairing" ? this.#pairingIntent.phone : null,
|
|
244
299
|
reason: this.#reason,
|
|
245
300
|
detail: this.#detail,
|
|
246
301
|
lastDisconnect: this.#lastDisconnect,
|
|
@@ -258,7 +313,7 @@ export class WhatsAppSession {
|
|
|
258
313
|
* Bring the session up. Idempotent: if a socket exists or is being made, this
|
|
259
314
|
* returns the state it is already in and creates nothing.
|
|
260
315
|
*/
|
|
261
|
-
async start({ reason = "manual" } = {}) {
|
|
316
|
+
async start({ reason = "manual", method, phone } = {}) {
|
|
262
317
|
if (this.#shuttingDown) return this.getState();
|
|
263
318
|
|
|
264
319
|
if (this.#starting) {
|
|
@@ -267,6 +322,8 @@ export class WhatsAppSession {
|
|
|
267
322
|
}
|
|
268
323
|
if (BUSY_STATES.has(this.#state)) return this.getState();
|
|
269
324
|
|
|
325
|
+
this.#pairingIntent = parseStartOptions({ method, phone });
|
|
326
|
+
|
|
270
327
|
// A manual start clears whatever the last failure left behind.
|
|
271
328
|
this.#cancelRetry();
|
|
272
329
|
this.#attempt = 0;
|
|
@@ -433,7 +490,10 @@ export class WhatsAppSession {
|
|
|
433
490
|
|
|
434
491
|
let created;
|
|
435
492
|
try {
|
|
436
|
-
created = await this.createSocket({
|
|
493
|
+
created = await this.createSocket({
|
|
494
|
+
proxy,
|
|
495
|
+
pairingCode: this.#pairingIntent.method === "pairing",
|
|
496
|
+
});
|
|
437
497
|
} catch (error) {
|
|
438
498
|
this.log.error({ err: error }, "[Session] failed to create the WhatsApp socket");
|
|
439
499
|
this.#transition(S.ERROR, {
|
|
@@ -473,9 +533,15 @@ export class WhatsAppSession {
|
|
|
473
533
|
this.#onHandshakeRejected(generation, sock, response),
|
|
474
534
|
});
|
|
475
535
|
|
|
536
|
+
const waitingForCode =
|
|
537
|
+
this.#pairing && this.#pairingIntent.method === "pairing";
|
|
476
538
|
this.#transition(this.#pairing ? S.WAITING_FOR_QR : S.STARTING, {
|
|
477
539
|
reason,
|
|
478
|
-
detail: this.#pairing
|
|
540
|
+
detail: this.#pairing
|
|
541
|
+
? waitingForCode
|
|
542
|
+
? "Waiting for a pairing code…"
|
|
543
|
+
: "Waiting for a QR code"
|
|
544
|
+
: "Connecting to WhatsApp",
|
|
479
545
|
});
|
|
480
546
|
|
|
481
547
|
this.log.info("[Session] WhatsApp socket created");
|
|
@@ -580,7 +646,7 @@ export class WhatsAppSession {
|
|
|
580
646
|
|
|
581
647
|
const { connection, lastDisconnect, qr, isNewLogin } = update || {};
|
|
582
648
|
|
|
583
|
-
if (qr) this.#onQr(qr);
|
|
649
|
+
if (qr) await this.#onQr(qr);
|
|
584
650
|
|
|
585
651
|
// The phone scanned the code. Pairing succeeded even though the connection
|
|
586
652
|
// is about to be torn down and restarted — this is WhatsApp's own login
|
|
@@ -598,7 +664,18 @@ export class WhatsAppSession {
|
|
|
598
664
|
else if (connection === "close") await this.#onClose(lastDisconnect);
|
|
599
665
|
}
|
|
600
666
|
|
|
601
|
-
#onQr(qr) {
|
|
667
|
+
async #onQr(qr) {
|
|
668
|
+
// Pair-device readiness. In pairing-code mode the QR must not be shown;
|
|
669
|
+
// requestPairingCode waits for this stanza (or queues until it arrives).
|
|
670
|
+
if (
|
|
671
|
+
this.#pairing &&
|
|
672
|
+
this.#pairingIntent.method === "pairing" &&
|
|
673
|
+
this.#pairingIntent.phone
|
|
674
|
+
) {
|
|
675
|
+
await this.#requestPairingCode();
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
|
|
602
679
|
this.#qr = qr;
|
|
603
680
|
try {
|
|
604
681
|
saveQrCode(qr);
|
|
@@ -612,6 +689,56 @@ export class WhatsAppSession {
|
|
|
612
689
|
this.emitEvent("qr", qr);
|
|
613
690
|
}
|
|
614
691
|
|
|
692
|
+
async #requestPairingCode() {
|
|
693
|
+
if (this.#pairingCode || this.#pairingCodeRequest) return;
|
|
694
|
+
const sock = this.#socket;
|
|
695
|
+
const phone = this.#pairingIntent.phone;
|
|
696
|
+
if (!phone) return;
|
|
697
|
+
if (typeof sock?.requestPairingCode !== "function") {
|
|
698
|
+
this.log.error("[Session] this socket cannot request a pairing code");
|
|
699
|
+
this.#transition(S.ERROR, {
|
|
700
|
+
reason: "pairing_unsupported",
|
|
701
|
+
detail: "Pairing code is not available on this connection. Use QR instead.",
|
|
702
|
+
});
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
this.#pairingCodeRequest = (async () => {
|
|
707
|
+
try {
|
|
708
|
+
if (this.pairingCodeDelayMs > 0) {
|
|
709
|
+
await new Promise((resolve) => setTimeout(resolve, this.pairingCodeDelayMs));
|
|
710
|
+
}
|
|
711
|
+
if (!this.#pairing || this.#shuttingDown) return;
|
|
712
|
+
const raw = await sock.requestPairingCode(phone);
|
|
713
|
+
if (!this.#pairing || this.#shuttingDown) return;
|
|
714
|
+
this.#pairingCode = String(raw || "").replace(/\s|-/g, "");
|
|
715
|
+
this.#transition(S.WAITING_FOR_QR, {
|
|
716
|
+
reason: "pairing_code",
|
|
717
|
+
detail:
|
|
718
|
+
"Enter the code in WhatsApp → Linked devices → Link with phone number.",
|
|
719
|
+
});
|
|
720
|
+
this.emitEvent("pairing_code", {
|
|
721
|
+
code: this.#pairingCode,
|
|
722
|
+
phone,
|
|
723
|
+
});
|
|
724
|
+
} catch (error) {
|
|
725
|
+
this.log.error(
|
|
726
|
+
{ err: error?.message },
|
|
727
|
+
"[Session] pairing code request failed"
|
|
728
|
+
);
|
|
729
|
+
if (!this.#pairing || this.#shuttingDown) return;
|
|
730
|
+
this.#transition(S.ERROR, {
|
|
731
|
+
reason: "pairing_failed",
|
|
732
|
+
detail: "Could not get a pairing code. Check the number and try again.",
|
|
733
|
+
});
|
|
734
|
+
} finally {
|
|
735
|
+
this.#pairingCodeRequest = null;
|
|
736
|
+
}
|
|
737
|
+
})();
|
|
738
|
+
|
|
739
|
+
await this.#pairingCodeRequest;
|
|
740
|
+
}
|
|
741
|
+
|
|
615
742
|
async #onOpen() {
|
|
616
743
|
this.#attempt = 0;
|
|
617
744
|
this.#nextRetryAt = null;
|
|
@@ -797,14 +924,17 @@ export class WhatsAppSession {
|
|
|
797
924
|
* GET /qr would happily serve it to somebody.
|
|
798
925
|
*/
|
|
799
926
|
#clearQr() {
|
|
800
|
-
const
|
|
927
|
+
const hadQr = this.#qr !== null;
|
|
928
|
+
const hadCode = this.#pairingCode !== null;
|
|
801
929
|
this.#qr = null;
|
|
930
|
+
this.#pairingCode = null;
|
|
802
931
|
try {
|
|
803
932
|
deleteQrCode();
|
|
804
933
|
} catch (error) {
|
|
805
934
|
this.log.debug({ err: error?.message }, "[Session] failed to delete the stored QR");
|
|
806
935
|
}
|
|
807
|
-
if (
|
|
936
|
+
if (hadQr) this.emitEvent("qr_cleared", {});
|
|
937
|
+
if (hadCode) this.emitEvent("pairing_code_cleared", {});
|
|
808
938
|
}
|
|
809
939
|
|
|
810
940
|
#transition(state, { reason = null, detail = null } = {}) {
|
package/src/core/socket.js
CHANGED
|
@@ -54,7 +54,7 @@ function withThumbnailSupport(sock) {
|
|
|
54
54
|
* rather than read here, so this factory stays a pure function of its
|
|
55
55
|
* arguments and a test can hand it a fake.
|
|
56
56
|
*/
|
|
57
|
-
export async function createWhatsAppSocket({ proxy = null } = {}) {
|
|
57
|
+
export async function createWhatsAppSocket({ proxy = null, pairingCode = false } = {}) {
|
|
58
58
|
logger.info(
|
|
59
59
|
proxy
|
|
60
60
|
? `[Socket] Initializing WhatsApp socket through ${proxy.label}`
|
|
@@ -64,16 +64,15 @@ export async function createWhatsAppSocket({ proxy = null } = {}) {
|
|
|
64
64
|
const { state, saveCreds, clearAll } = await useDatabaseAuthState();
|
|
65
65
|
|
|
66
66
|
// Whether this install is already paired. NOT `store.hasCredentials()`: the
|
|
67
|
-
// auth state writes a `creds` row on its very first call, paired or not
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
const isPaired = !!state?.creds?.me?.id;
|
|
67
|
+
// auth state writes a `creds` row on its very first call, paired or not.
|
|
68
|
+
// `requestPairingCode` also writes `creds.me.id` *before* the phone accepts
|
|
69
|
+
// the code, so `me.id` alone is not proof. `registered === false` means the
|
|
70
|
+
// handshake never finished.
|
|
71
|
+
const isPaired = !!state?.creds?.me?.id && state?.creds?.registered !== false;
|
|
73
72
|
|
|
74
73
|
const cachedGroupMetadata = (jid) => groupMetadataCache.get(jid);
|
|
75
74
|
|
|
76
|
-
const config = getBaileysConfig(cachedGroupMetadata);
|
|
75
|
+
const config = getBaileysConfig(cachedGroupMetadata, { pairingCode });
|
|
77
76
|
|
|
78
77
|
// Three separate hooks, because Baileys has three separate network paths:
|
|
79
78
|
// agent -> the `ws` WebSocket (https.request, classic agent)
|
package/src/index.js
CHANGED
|
@@ -185,6 +185,10 @@ async function startWithPanel({ open }) {
|
|
|
185
185
|
// the Connection screen offers a Start button.
|
|
186
186
|
const core = await bootstrapCore({ autoStart: false });
|
|
187
187
|
liveSession = core.session;
|
|
188
|
+
if (process.env.LEVIX_ANDROID === "1") {
|
|
189
|
+
console.log("Database ready");
|
|
190
|
+
console.log("Commands loaded " + core.commandCount);
|
|
191
|
+
}
|
|
188
192
|
|
|
189
193
|
let panel;
|
|
190
194
|
try {
|
|
@@ -202,6 +206,10 @@ async function startWithPanel({ open }) {
|
|
|
202
206
|
const url = panelUrl({ port: panel.port, firstRun });
|
|
203
207
|
|
|
204
208
|
logger.info(`✅ Control panel: ${url}`);
|
|
209
|
+
if (process.env.LEVIX_ANDROID === "1") {
|
|
210
|
+
console.log("Panel listening " + url);
|
|
211
|
+
console.log("Levix ready");
|
|
212
|
+
}
|
|
205
213
|
|
|
206
214
|
line();
|
|
207
215
|
line(` ${brand.name} is running`);
|
|
@@ -850,7 +850,12 @@ router.post("/security/password", (req, res) => {
|
|
|
850
850
|
// pairing was already waiting showed an empty frame until Baileys happened to
|
|
851
851
|
// issue the next one.
|
|
852
852
|
router.get("/bot/session", (req, res) => {
|
|
853
|
-
res.json({
|
|
853
|
+
res.json({
|
|
854
|
+
success: true,
|
|
855
|
+
session: sessionState(),
|
|
856
|
+
qr: session?.qr ?? null,
|
|
857
|
+
pairingCode: session?.pairingCode ?? null,
|
|
858
|
+
});
|
|
854
859
|
});
|
|
855
860
|
|
|
856
861
|
// Idempotent by construction: the manager returns the state it is already in
|
|
@@ -862,7 +867,17 @@ router.post(
|
|
|
862
867
|
if (!session) {
|
|
863
868
|
return res.status(503).json({ success: false, error: "Session manager is not ready" });
|
|
864
869
|
}
|
|
865
|
-
|
|
870
|
+
let state;
|
|
871
|
+
try {
|
|
872
|
+
state = await session.start({
|
|
873
|
+
reason: "dashboard",
|
|
874
|
+
method: req.body?.method,
|
|
875
|
+
phone: req.body?.phone,
|
|
876
|
+
});
|
|
877
|
+
} catch (error) {
|
|
878
|
+
if (error.code === "PAIRING_PHONE") return badRequest(res, error.message);
|
|
879
|
+
throw error;
|
|
880
|
+
}
|
|
866
881
|
logger.info(`[Dashboard] Start session requested — now ${state.state}`);
|
|
867
882
|
res.json({ success: true, session: state });
|
|
868
883
|
})
|
package/src/utils/logger.cjs
CHANGED
|
@@ -19,6 +19,12 @@ function isPackaged() {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// Worker-thread transports load targets by module path. That fails inside a
|
|
23
|
+
// SEA, and Android's Bionic Node is happier with in-process streams too.
|
|
24
|
+
function useInProcessLogs() {
|
|
25
|
+
return isPackaged() || process.env.LEVIX_ANDROID === "1";
|
|
26
|
+
}
|
|
27
|
+
|
|
22
28
|
const base = {
|
|
23
29
|
level: "trace",
|
|
24
30
|
serializers: {
|
|
@@ -38,7 +44,7 @@ const PRETTY = {
|
|
|
38
44
|
const combined = path.join(logDir, "combined.log");
|
|
39
45
|
const errors = path.join(logDir, "error.log");
|
|
40
46
|
|
|
41
|
-
const logger =
|
|
47
|
+
const logger = useInProcessLogs()
|
|
42
48
|
? pino(
|
|
43
49
|
base,
|
|
44
50
|
pino.multistream([
|
package/views/dashboard.ejs
CHANGED
|
@@ -319,6 +319,31 @@
|
|
|
319
319
|
|
|
320
320
|
<div class="hint" id="conn-proxy"></div>
|
|
321
321
|
|
|
322
|
+
<div id="pair-chooser" class="pair-chooser">
|
|
323
|
+
<p class="hint">How should this phone link?</p>
|
|
324
|
+
<div class="pair-methods">
|
|
325
|
+
<label class="pair-option">
|
|
326
|
+
<input type="radio" name="pair-method" value="qr" />
|
|
327
|
+
QR code
|
|
328
|
+
</label>
|
|
329
|
+
<label class="pair-option">
|
|
330
|
+
<input type="radio" name="pair-method" value="pairing" />
|
|
331
|
+
Pairing code
|
|
332
|
+
</label>
|
|
333
|
+
</div>
|
|
334
|
+
<div id="pair-phone-wrap" class="pair-phone-wrap" hidden>
|
|
335
|
+
<label for="pair-phone">WhatsApp number with country code</label>
|
|
336
|
+
<input
|
|
337
|
+
id="pair-phone"
|
|
338
|
+
type="tel"
|
|
339
|
+
inputmode="numeric"
|
|
340
|
+
autocomplete="tel"
|
|
341
|
+
placeholder="2010…"
|
|
342
|
+
/>
|
|
343
|
+
<p class="hint">This WhatsApp’s own number. Country code, no leading 0 (Egypt: 2010… not 010…).</p>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
|
|
322
347
|
<div class="inline action-cluster">
|
|
323
348
|
<button class="btn btn-primary" id="start-btn" type="button">
|
|
324
349
|
Start session
|
|
@@ -347,15 +372,25 @@
|
|
|
347
372
|
</div>
|
|
348
373
|
|
|
349
374
|
<div id="qr-panel" style="display: none">
|
|
350
|
-
<div class="qr-box">
|
|
375
|
+
<div class="qr-box" id="qr-box">
|
|
351
376
|
<div class="qr-frame" id="qr-frame"></div>
|
|
352
377
|
<div class="hint" id="qr-note">Waiting for a QR code…</div>
|
|
353
|
-
<ol class="steps">
|
|
378
|
+
<ol class="steps" id="qr-steps">
|
|
354
379
|
<li>Open WhatsApp on the phone the bot should use.</li>
|
|
355
380
|
<li>Settings → Linked devices → Link a device.</li>
|
|
356
381
|
<li>Point the camera at the code above.</li>
|
|
357
382
|
</ol>
|
|
358
383
|
</div>
|
|
384
|
+
<div class="qr-box" id="pairing-box" hidden>
|
|
385
|
+
<div class="pairing-code" id="pairing-code">••••-••••</div>
|
|
386
|
+
<div class="hint" id="pairing-phone"></div>
|
|
387
|
+
<div class="hint" id="pairing-note">Waiting for a pairing code…</div>
|
|
388
|
+
<ol class="steps">
|
|
389
|
+
<li>Open WhatsApp on that number → Settings → Linked devices.</li>
|
|
390
|
+
<li>Link a device → Link with phone number instead.</li>
|
|
391
|
+
<li>Type the code above. Get a new code if it fails.</li>
|
|
392
|
+
</ol>
|
|
393
|
+
</div>
|
|
359
394
|
</div>
|
|
360
395
|
</div>
|
|
361
396
|
</section>
|