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.
- package/README.md +23 -5
- package/SETUP.md +24 -4
- package/app.cjs +22 -1
- package/deploy/install.sh +97 -11
- package/deploy/levix.service +3 -0
- package/package.json +18 -9
- package/public/dashboard.css +9 -0
- package/public/dashboard.js +233 -43
- package/scheduler.cjs +15 -0
- package/src/bootstrap/core.js +36 -32
- package/src/bootstrap/events.cjs +4 -2
- package/src/bootstrap/panel.js +9 -7
- package/src/commands/autoschedule.cjs +2 -3
- package/src/commands/gemini.cjs +45 -28
- package/src/commands/restart.cjs +6 -2
- package/src/commands/stt.cjs +43 -26
- package/src/commands/tts.cjs +55 -35
- package/src/config/ai-identity.cjs +61 -0
- package/src/config/ai-persona.md +92 -17
- package/src/config/brand.cjs +8 -18
- package/src/config/constants.js +11 -2
- package/src/config/secrets.cjs +19 -0
- package/src/config/settings.cjs +96 -2
- package/src/core/connection.js +140 -134
- package/src/core/events.js +64 -22
- package/src/core/proxy.js +266 -0
- package/src/core/session.js +828 -0
- package/src/core/socket.js +39 -5
- package/src/index.js +136 -29
- package/src/routes/dashboard.api.esm.js +127 -30
- package/src/services/aiAgent.cjs +278 -48
- package/src/services/aiTools.cjs +14 -12
- package/src/utils/storage.esm.js +5 -0
- package/views/dashboard.ejs +20 -11
- package/views/qr.ejs +23 -3
package/src/config/settings.cjs
CHANGED
|
@@ -59,9 +59,10 @@ const SETTINGS = [
|
|
|
59
59
|
{
|
|
60
60
|
key: "gemini_model",
|
|
61
61
|
type: "string",
|
|
62
|
-
default: "gemini-
|
|
62
|
+
default: "gemini-3.7-flash",
|
|
63
63
|
group: "ai",
|
|
64
64
|
label: "Gemini model",
|
|
65
|
+
hint: "The model that answers. Flash is the default because a WhatsApp reply is judged on how fast it arrives; it still takes Google Search and Levix's own function tools in the same request, which is what the agent depends on. A Gemini 3 Pro model works here too if you would rather trade latency for depth.",
|
|
65
66
|
},
|
|
66
67
|
{
|
|
67
68
|
key: "ai_agent",
|
|
@@ -108,6 +109,30 @@ const SETTINGS = [
|
|
|
108
109
|
label: "Memory injected (chars)",
|
|
109
110
|
hint: "How much of the long-term memory files goes into every prompt.",
|
|
110
111
|
},
|
|
112
|
+
{
|
|
113
|
+
key: "gemini_stt_model",
|
|
114
|
+
type: "string",
|
|
115
|
+
default: "gemini-3.7-flash",
|
|
116
|
+
group: "ai",
|
|
117
|
+
label: "Gemini transcription model",
|
|
118
|
+
hint: "Used by !stt only. Transcription is high-volume and latency-bound, so it stays on Flash whatever the chat model is set to.",
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
key: "gemini_image_model",
|
|
122
|
+
type: "string",
|
|
123
|
+
default: "gemini-3.1-flash-image",
|
|
124
|
+
group: "ai",
|
|
125
|
+
label: "Gemini image model",
|
|
126
|
+
hint: "Used by !generate only. Image output needs a model that can return images, which is not the same one that answers chat — gemini-3.1-flash-image is the dedicated stable one.",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
key: "ai_google_search",
|
|
130
|
+
type: "bool",
|
|
131
|
+
default: true,
|
|
132
|
+
group: "ai",
|
|
133
|
+
label: "Gemini Google Search",
|
|
134
|
+
hint: "Lets Gemini use Google's own search grounding when a question needs current information. Gemini only — the Groq fallback is unaffected.",
|
|
135
|
+
},
|
|
111
136
|
{
|
|
112
137
|
key: "groq_api_key",
|
|
113
138
|
type: "secret",
|
|
@@ -185,6 +210,65 @@ const SETTINGS = [
|
|
|
185
210
|
hint: "Only if the bundled ffmpeg-static binary can't run on this machine.",
|
|
186
211
|
},
|
|
187
212
|
|
|
213
|
+
// --- WhatsApp proxy ----------------------------------------------------
|
|
214
|
+
//
|
|
215
|
+
// Applies to the WhatsApp connection ONLY: the session manager hands these
|
|
216
|
+
// to the one makeWASocket() call (src/core/proxy.js). The control panel, the
|
|
217
|
+
// AI providers and every other outbound request are untouched.
|
|
218
|
+
//
|
|
219
|
+
// None of these apply to a socket that is already open — the Connection
|
|
220
|
+
// screen offers "Reconnect to apply" instead of dropping a healthy session
|
|
221
|
+
// the moment somebody opens Settings.
|
|
222
|
+
{
|
|
223
|
+
key: "whatsapp_proxy_enabled",
|
|
224
|
+
type: "bool",
|
|
225
|
+
default: false,
|
|
226
|
+
group: "whatsapp",
|
|
227
|
+
label: "Route WhatsApp through a proxy",
|
|
228
|
+
hint: "Off means WhatsApp connects directly, exactly as before.",
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
key: "whatsapp_proxy_protocol",
|
|
232
|
+
type: "string",
|
|
233
|
+
default: "http",
|
|
234
|
+
choices: ["http", "https", "socks5"],
|
|
235
|
+
group: "whatsapp",
|
|
236
|
+
label: "Proxy protocol",
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
key: "whatsapp_proxy_host",
|
|
240
|
+
type: "string",
|
|
241
|
+
default: "",
|
|
242
|
+
group: "whatsapp",
|
|
243
|
+
label: "Proxy host",
|
|
244
|
+
hint: "Hostname or IP of the proxy server. Required when the proxy is on.",
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
key: "whatsapp_proxy_port",
|
|
248
|
+
type: "int",
|
|
249
|
+
default: 0,
|
|
250
|
+
min: 0,
|
|
251
|
+
max: 65535,
|
|
252
|
+
group: "whatsapp",
|
|
253
|
+
label: "Proxy port",
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
key: "whatsapp_proxy_username",
|
|
257
|
+
type: "string",
|
|
258
|
+
default: "",
|
|
259
|
+
group: "whatsapp",
|
|
260
|
+
label: "Proxy username",
|
|
261
|
+
hint: "Leave empty for a proxy that needs no authentication.",
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
key: "whatsapp_proxy_password",
|
|
265
|
+
type: "secret",
|
|
266
|
+
default: "",
|
|
267
|
+
group: "whatsapp",
|
|
268
|
+
label: "Proxy password",
|
|
269
|
+
hint: "Stored like an API key: never sent back to this page, never logged.",
|
|
270
|
+
},
|
|
271
|
+
|
|
188
272
|
// --- Server ------------------------------------------------------------
|
|
189
273
|
{
|
|
190
274
|
key: "port",
|
|
@@ -251,6 +335,7 @@ const GROUP_LABELS = {
|
|
|
251
335
|
integrations: "Integrations",
|
|
252
336
|
media: "Media",
|
|
253
337
|
server: "Server",
|
|
338
|
+
whatsapp: "WhatsApp proxy",
|
|
254
339
|
};
|
|
255
340
|
|
|
256
341
|
// This module is required by files that load before the database is opened in
|
|
@@ -319,7 +404,15 @@ function validate(definition, value) {
|
|
|
319
404
|
|
|
320
405
|
if (definition.type === "bool") return coerce(definition, value);
|
|
321
406
|
|
|
322
|
-
const text = String(value ?? "");
|
|
407
|
+
const text = String(value ?? "").trim();
|
|
408
|
+
|
|
409
|
+
// A fixed set of values (the proxy protocol) is still a string setting; the
|
|
410
|
+
// list is what makes the dashboard render a dropdown and what stops a
|
|
411
|
+
// hand-crafted PATCH from storing something the code cannot act on.
|
|
412
|
+
if (definition.choices && text && !definition.choices.includes(text)) {
|
|
413
|
+
throw new Error(`${definition.label} must be one of: ${definition.choices.join(", ")}`);
|
|
414
|
+
}
|
|
415
|
+
|
|
323
416
|
if (text.length > 500) throw new Error(`${definition.label} is too long`);
|
|
324
417
|
if (definition.key === "bot_timezone" && text) {
|
|
325
418
|
try {
|
|
@@ -372,6 +465,7 @@ function describe() {
|
|
|
372
465
|
hint: definition.hint || null,
|
|
373
466
|
min: definition.min ?? null,
|
|
374
467
|
max: definition.max ?? null,
|
|
468
|
+
choices: definition.choices ?? null,
|
|
375
469
|
restart: definition.restart === true,
|
|
376
470
|
source: sourceOf(definition.key),
|
|
377
471
|
};
|
package/src/core/connection.js
CHANGED
|
@@ -1,152 +1,158 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// What a WhatsApp connection means, minus the decision of when to make one.
|
|
2
|
+
//
|
|
3
|
+
// The retry schedule, the socket's lifetime and the state machine live in
|
|
4
|
+
// src/core/session.js. This file answers two narrower questions:
|
|
5
|
+
//
|
|
6
|
+
// classifyDisconnect(statusCode) — is this close terminal, and why?
|
|
7
|
+
// handleConnectionOpen(sock, …) — what has to happen once one is open?
|
|
8
|
+
//
|
|
9
|
+
// Keeping them apart is what lets the state machine be tested without Baileys
|
|
10
|
+
// and without a database.
|
|
11
|
+
|
|
12
|
+
import { DisconnectReason } from "@whiskeysockets/baileys";
|
|
3
13
|
import { createRequire } from "module";
|
|
4
|
-
import { MAX_RETRIES, RETRY_DELAY_MS } from "../config/constants.js";
|
|
5
14
|
import { groupMetadataCache } from "./socket.js";
|
|
6
15
|
import {
|
|
7
|
-
saveQrCode,
|
|
8
|
-
deleteQrCode,
|
|
9
16
|
saveUserMetadata,
|
|
10
17
|
getAllOwners,
|
|
11
18
|
getAllBotAdmins,
|
|
12
19
|
} from "../utils/storage.esm.js";
|
|
13
|
-
import {
|
|
14
|
-
bootstrapOwners,
|
|
15
|
-
bootstrapAdmins,
|
|
16
|
-
} from "../utils/permissions.esm.js";
|
|
20
|
+
import { bootstrapOwners, bootstrapAdmins } from "../utils/permissions.esm.js";
|
|
17
21
|
|
|
18
22
|
const require = createRequire(import.meta.url);
|
|
19
23
|
const logger = require("../utils/logger.cjs");
|
|
20
24
|
const { primePermissions } = require("../utils/permissions.cjs");
|
|
21
25
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
26
|
+
/**
|
|
27
|
+
* WhatsApp answers a `<failure reason="405">` when it refuses the connection
|
|
28
|
+
* outright — the number is not in Baileys' DisconnectReason enum, so it is
|
|
29
|
+
* named here rather than left as a bare literal in a comparison. Retrying it is
|
|
30
|
+
* how an install ends up hammering a server that has already said no, so Levix
|
|
31
|
+
* treats it as terminal and waits for a person.
|
|
32
|
+
*/
|
|
33
|
+
export const CONNECTION_FAILURE_405 = 405;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read a close and say what should happen next.
|
|
37
|
+
*
|
|
38
|
+
* Terminal means: do not reconnect on our own. Everything else gets the staged
|
|
39
|
+
* retry schedule in session.js.
|
|
40
|
+
*
|
|
41
|
+
* @param {number|undefined} statusCode - `lastDisconnect.error.output.statusCode`
|
|
42
|
+
*/
|
|
43
|
+
export function classifyDisconnect(statusCode) {
|
|
44
|
+
// Baileys reports an intentional logout and a server-side unlink with the
|
|
45
|
+
// same code (DisconnectReason.loggedOut === 401), and it is the only case
|
|
46
|
+
// where the stored credentials are genuinely dead.
|
|
47
|
+
if (statusCode === DisconnectReason.loggedOut) {
|
|
48
|
+
return {
|
|
49
|
+
terminal: true,
|
|
50
|
+
loggedOut: true,
|
|
51
|
+
restartRequired: false,
|
|
52
|
+
reason: "logged_out",
|
|
53
|
+
label: "logged out",
|
|
54
|
+
detail: "WhatsApp unlinked this device. Start a session to pair again.",
|
|
55
|
+
};
|
|
40
56
|
}
|
|
41
57
|
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// Log full disconnect info for debugging
|
|
57
|
-
logger.debug(
|
|
58
|
-
{ lastDisconnect, statusCode, reason },
|
|
59
|
-
"[Connection] Disconnect details"
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
if (shouldReconnect && retryCount < MAX_RETRIES) {
|
|
63
|
-
retryCount++;
|
|
64
|
-
const delayMs = RETRY_DELAY_MS * retryCount; // Exponential backoff: 5s, 10s, 15s, 20s, 25s
|
|
65
|
-
|
|
66
|
-
// Special message for QR timeout
|
|
67
|
-
const messagePrefix =
|
|
68
|
-
statusCode === 515 ? "🔄 QR code expired" : "🔌 Connection closed";
|
|
69
|
-
|
|
70
|
-
logger.warn(
|
|
71
|
-
`${messagePrefix}. Reason: ${statusCode || reason}. Retrying in ${
|
|
72
|
-
delayMs / 1000
|
|
73
|
-
}s... (Attempt ${retryCount}/${MAX_RETRIES})`
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
await delay(delayMs);
|
|
77
|
-
return { shouldReconnect: true };
|
|
78
|
-
} else if (!shouldReconnect) {
|
|
79
|
-
if (statusCode === DisconnectReason.loggedOut) {
|
|
80
|
-
logger.error(
|
|
81
|
-
"❌ Connection closed permanently. Logged out. Clearing auth data from database."
|
|
82
|
-
);
|
|
83
|
-
|
|
84
|
-
// Clear authentication data from database
|
|
85
|
-
if (clearAll) {
|
|
86
|
-
await clearAll();
|
|
87
|
-
logger.info("[Auth] Authentication data cleared from database");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Reset retry count and trigger reconnection for new QR
|
|
91
|
-
retryCount = 0;
|
|
92
|
-
return { shouldReconnect: true };
|
|
93
|
-
} else {
|
|
94
|
-
logger.error(
|
|
95
|
-
`❌ Connection closed with non-recoverable error: ${statusCode}. Not retrying.`
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
} else {
|
|
99
|
-
logger.error(`❌ Max retries (${MAX_RETRIES}) reached. Exiting.`);
|
|
100
|
-
|
|
101
|
-
process.exit(1);
|
|
102
|
-
}
|
|
103
|
-
} else if (connection === "open") {
|
|
104
|
-
logger.info("✅ Connection opened successfully!");
|
|
105
|
-
deleteQrCode();
|
|
106
|
-
|
|
107
|
-
retryCount = 0;
|
|
108
|
-
io.emit("status_update", { status: "Connected" });
|
|
109
|
-
|
|
110
|
-
// The paired account is always an owner — that's the whole bootstrap.
|
|
111
|
-
// Everyone else is granted from the dashboard (Roles) or with `!perm`, and
|
|
112
|
-
// is already a row in user_metadata by the time we get here.
|
|
113
|
-
await saveBotOwnerMetadata(sock);
|
|
58
|
+
if (statusCode === CONNECTION_FAILURE_405) {
|
|
59
|
+
return {
|
|
60
|
+
terminal: true,
|
|
61
|
+
loggedOut: false,
|
|
62
|
+
restartRequired: false,
|
|
63
|
+
reason: "connection_failure",
|
|
64
|
+
label: "refused by WhatsApp (405)",
|
|
65
|
+
// Deliberately not a credential wipe: a 405 says WhatsApp would not take
|
|
66
|
+
// this connection, not that the pairing is gone.
|
|
67
|
+
detail:
|
|
68
|
+
"WhatsApp refused the connection (405). The pairing was left alone — try starting the session again later.",
|
|
69
|
+
};
|
|
70
|
+
}
|
|
114
71
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
:
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
:
|
|
72
|
+
if (statusCode === DisconnectReason.forbidden) {
|
|
73
|
+
return {
|
|
74
|
+
terminal: true,
|
|
75
|
+
loggedOut: false,
|
|
76
|
+
restartRequired: false,
|
|
77
|
+
reason: "forbidden",
|
|
78
|
+
label: "forbidden",
|
|
79
|
+
detail: "WhatsApp rejected this account (403). Reconnecting will not help.",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
121
82
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
bootstrapAdmins(adminsRoster);
|
|
135
|
-
|
|
136
|
-
await primePermissions();
|
|
137
|
-
logger.info(
|
|
138
|
-
`[Permissions] Owner roster: ${ownersRoster.length} entries; ` +
|
|
139
|
-
`admin roster: ${adminsRoster.length} entries`
|
|
140
|
-
);
|
|
141
|
-
|
|
142
|
-
// Initialize scheduled jobs
|
|
143
|
-
initializeScheduledJobs(sock);
|
|
144
|
-
|
|
145
|
-
// Cache all groups
|
|
146
|
-
await cacheAllGroups(sock);
|
|
83
|
+
// Part of the pairing handshake: WhatsApp asks for the socket to be made
|
|
84
|
+
// again once a phone has scanned the code. Recoverable, and never the end of
|
|
85
|
+
// a pairing attempt.
|
|
86
|
+
if (statusCode === DisconnectReason.restartRequired) {
|
|
87
|
+
return {
|
|
88
|
+
terminal: false,
|
|
89
|
+
loggedOut: false,
|
|
90
|
+
restartRequired: true,
|
|
91
|
+
reason: "restart_required",
|
|
92
|
+
label: "restart required",
|
|
93
|
+
detail: "WhatsApp asked for a fresh connection.",
|
|
94
|
+
};
|
|
147
95
|
}
|
|
148
96
|
|
|
149
|
-
return {
|
|
97
|
+
return {
|
|
98
|
+
terminal: false,
|
|
99
|
+
loggedOut: false,
|
|
100
|
+
restartRequired: false,
|
|
101
|
+
reason: "connection_closed",
|
|
102
|
+
label: "recoverable",
|
|
103
|
+
detail: null,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Everything that has to happen the moment a connection is open: record the
|
|
109
|
+
* paired account as an owner, seed the permission rosters, (re)start the
|
|
110
|
+
* scheduled jobs against the new socket, and cache the group metadata.
|
|
111
|
+
*
|
|
112
|
+
* @param {object} sock
|
|
113
|
+
* @param {(sock: object) => void} [initializeScheduledJobs]
|
|
114
|
+
*/
|
|
115
|
+
export async function handleConnectionOpen(sock, initializeScheduledJobs) {
|
|
116
|
+
logger.info("✅ Connection opened successfully!");
|
|
117
|
+
|
|
118
|
+
// The paired account is always an owner — that's the whole bootstrap.
|
|
119
|
+
// Everyone else is granted from the dashboard (Roles) or with `!perm`, and
|
|
120
|
+
// is already a row in user_metadata by the time we get here.
|
|
121
|
+
await saveBotOwnerMetadata(sock);
|
|
122
|
+
|
|
123
|
+
const botPn = sock?.user?.id
|
|
124
|
+
? `${sock.user.id.split(":")[0].split("@")[0]}@s.whatsapp.net`
|
|
125
|
+
: null;
|
|
126
|
+
const botLid = sock?.user?.lid
|
|
127
|
+
? `${sock.user.lid.split(":")[0].split("@")[0]}@lid`
|
|
128
|
+
: null;
|
|
129
|
+
|
|
130
|
+
// Both rosters are seeded from the database so a permission check never has
|
|
131
|
+
// to hit it on the hot path.
|
|
132
|
+
const ownersRoster = [
|
|
133
|
+
botPn,
|
|
134
|
+
botLid,
|
|
135
|
+
...getAllOwners().flatMap((user) => [user.jid, user.lid]),
|
|
136
|
+
].filter(Boolean);
|
|
137
|
+
const adminsRoster = getAllBotAdmins()
|
|
138
|
+
.flatMap((user) => [user.jid, user.lid])
|
|
139
|
+
.filter(Boolean);
|
|
140
|
+
|
|
141
|
+
bootstrapOwners(ownersRoster);
|
|
142
|
+
bootstrapAdmins(adminsRoster);
|
|
143
|
+
|
|
144
|
+
await primePermissions();
|
|
145
|
+
logger.info(
|
|
146
|
+
`[Permissions] Owner roster: ${ownersRoster.length} entries; ` +
|
|
147
|
+
`admin roster: ${adminsRoster.length} entries`
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// Called on every open, including a reconnect: scheduleNewJob() stops the
|
|
151
|
+
// previous timer for the same id first, so a weekly message is not sent twice
|
|
152
|
+
// after the first reconnect.
|
|
153
|
+
if (initializeScheduledJobs) initializeScheduledJobs(sock);
|
|
154
|
+
|
|
155
|
+
await cacheAllGroups(sock);
|
|
150
156
|
}
|
|
151
157
|
|
|
152
158
|
// Record the paired WhatsApp account as the bot's first owner. Whoever scanned
|
|
@@ -155,12 +161,12 @@ async function saveBotOwnerMetadata(sock) {
|
|
|
155
161
|
try {
|
|
156
162
|
logger.info("[User Metadata] Saving bot owner metadata...");
|
|
157
163
|
|
|
158
|
-
const phoneNumber = sock
|
|
164
|
+
const phoneNumber = sock?.user?.id?.split(":")[0]?.split("@")[0];
|
|
159
165
|
const userJid = phoneNumber ? `${phoneNumber}@s.whatsapp.net` : null;
|
|
160
|
-
const userLid = sock
|
|
166
|
+
const userLid = sock?.user?.lid
|
|
161
167
|
? `${sock.user.lid.split(":")[0].split("@")[0]}@lid`
|
|
162
168
|
: null;
|
|
163
|
-
const displayName = sock
|
|
169
|
+
const displayName = sock?.user?.name || sock?.user?.verifiedName || null;
|
|
164
170
|
|
|
165
171
|
if (userJid) {
|
|
166
172
|
saveUserMetadata({
|
package/src/core/events.js
CHANGED
|
@@ -1,26 +1,63 @@
|
|
|
1
1
|
import { createRequire } from 'module';
|
|
2
|
-
import { handleConnectionUpdate } from './connection.js';
|
|
3
2
|
import { handleIncomingMessage } from '../handlers/message.handler.js';
|
|
4
3
|
import { handleGroupParticipantsUpdate, handleGroupJoinRequests } from '../handlers/group.handler.js';
|
|
5
4
|
import { groupMetadataCache } from './socket.js';
|
|
6
|
-
import {
|
|
5
|
+
import { storeLidPnMappings } from '../utils/storage.esm.js';
|
|
7
6
|
|
|
8
7
|
const require = createRequire(import.meta.url);
|
|
9
8
|
const logger = require('../utils/logger.cjs');
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Wire one socket up.
|
|
12
|
+
*
|
|
13
|
+
* `connection.update` is handed straight to the session manager
|
|
14
|
+
* (src/core/session.js) — it owns the state machine and the retry schedule, so
|
|
15
|
+
* nothing here decides whether to reconnect.
|
|
16
|
+
*
|
|
17
|
+
* @param {object} sock
|
|
18
|
+
* @param {object} handlers
|
|
19
|
+
* @param {() => Promise<void>} handlers.saveCreds
|
|
20
|
+
* @param {(update: object) => void} handlers.onConnectionUpdate
|
|
21
|
+
* @param {(response: object) => void} [handlers.onHandshakeRejected]
|
|
22
|
+
*/
|
|
23
|
+
export function setupEventListeners(
|
|
24
|
+
sock,
|
|
25
|
+
{ saveCreds, onConnectionUpdate, onHandshakeRejected } = {}
|
|
26
|
+
) {
|
|
27
|
+
// Every listener below goes through this. A bare `async` listener hands its
|
|
28
|
+
// promise to Baileys' emitter, which drops it — so one rejection anywhere in
|
|
29
|
+
// the message pipeline becomes an unhandledRejection, and src/index.js turns
|
|
30
|
+
// that into a process exit. A group lookup failing mid-disconnect is exactly
|
|
31
|
+
// the case the reconnect ladder exists to survive, so it must not be fatal.
|
|
32
|
+
const contained = (label, handler) => (payload) => {
|
|
33
|
+
Promise.resolve()
|
|
34
|
+
.then(() => handler(payload))
|
|
35
|
+
.catch((err) => logger.error({ err }, `[Events] ${label} handler failed`));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
sock.ev.on(
|
|
39
|
+
'connection.update',
|
|
40
|
+
contained('connection.update', (update) => onConnectionUpdate?.(update))
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// A handshake the server answered with an HTTP status instead of an upgrade.
|
|
44
|
+
//
|
|
45
|
+
// This has to be listened for or the connection hangs silently forever: ws
|
|
46
|
+
// only calls abortHandshake() when NOTHING is listening for
|
|
47
|
+
// 'unexpected-response' (ws/lib/websocket.js:928), and Baileys' own
|
|
48
|
+
// WebSocketClient registers a re-emitting listener for it — which satisfies
|
|
49
|
+
// that guard and then does nothing with the event. A proxy answering 407
|
|
50
|
+
// therefore produced no error, no close and no connection.update at all: the
|
|
51
|
+
// session simply sat in `starting` until somebody noticed.
|
|
52
|
+
sock.ws?.on?.(
|
|
53
|
+
'unexpected-response',
|
|
54
|
+
contained('unexpected-response', (_request, response) => {
|
|
55
|
+
onHandshakeRejected?.(response);
|
|
56
|
+
})
|
|
57
|
+
);
|
|
21
58
|
|
|
22
59
|
// Save credentials
|
|
23
|
-
sock.ev.on('creds.update', saveCreds);
|
|
60
|
+
if (saveCreds) sock.ev.on('creds.update', saveCreds);
|
|
24
61
|
|
|
25
62
|
// V7: LID mapping updates
|
|
26
63
|
sock.ev.on('lid-mapping.update', (updates) => {
|
|
@@ -45,17 +82,22 @@ export function setupEventListeners(sock, saveCreds, clearAll, io, initializeSch
|
|
|
45
82
|
});
|
|
46
83
|
|
|
47
84
|
// Incoming messages
|
|
48
|
-
sock.ev.on(
|
|
49
|
-
|
|
50
|
-
|
|
85
|
+
sock.ev.on(
|
|
86
|
+
'messages.upsert',
|
|
87
|
+
contained('messages.upsert', (m) => handleIncomingMessage(sock, m))
|
|
88
|
+
);
|
|
51
89
|
|
|
52
90
|
// Group participant updates
|
|
53
|
-
sock.ev.on(
|
|
54
|
-
|
|
55
|
-
|
|
91
|
+
sock.ev.on(
|
|
92
|
+
'group-participants.update',
|
|
93
|
+
contained('group-participants.update', (update) =>
|
|
94
|
+
handleGroupParticipantsUpdate(sock, update)
|
|
95
|
+
)
|
|
96
|
+
);
|
|
56
97
|
|
|
57
98
|
// Group join requests
|
|
58
|
-
sock.ev.on(
|
|
59
|
-
|
|
60
|
-
|
|
99
|
+
sock.ev.on(
|
|
100
|
+
'group-requests.update',
|
|
101
|
+
contained('group-requests.update', (events) => handleGroupJoinRequests(sock, events))
|
|
102
|
+
);
|
|
61
103
|
}
|