levix-bot 3.3.0-rc.2 → 4.0.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 +13 -9
- package/app.cjs +49 -46
- package/package.json +9 -1
- package/public/dashboard/assets/index-BBBXWzM0.js +391 -0
- package/public/dashboard/assets/index-pYaivHON.css +1 -0
- package/public/dashboard/index.html +19 -0
- package/public/dashboard.css +391 -66
- package/public/dashboard.js +593 -42
- package/scheduler.cjs +9 -14
- package/src/auth/use-database-auth-state.js +2 -4
- package/src/bootstrap/core.js +2 -5
- package/src/bootstrap/panel.js +10 -5
- package/src/cli.js +3 -5
- package/src/commands/autoschedule.cjs +1 -4
- package/src/commands/blacklist.cjs +22 -20
- package/src/commands/block.cjs +2 -4
- package/src/commands/calc.cjs +9 -16
- package/src/commands/digit.cjs +97 -0
- package/src/commands/gemini.cjs +147 -161
- package/src/commands/group/add.cjs +7 -16
- package/src/commands/group/antiSpam.cjs +5 -11
- package/src/commands/group/antilink.cjs +34 -28
- package/src/commands/group/approveall.cjs +3 -11
- package/src/commands/group/clearwarns.cjs +2 -5
- package/src/commands/group/demote.cjs +3 -8
- package/src/commands/group/kick.cjs +12 -15
- package/src/commands/group/media.cjs +17 -24
- package/src/commands/group/members.cjs +1 -2
- package/src/commands/group/promote.cjs +4 -12
- package/src/commands/group/removeall.cjs +2 -5
- package/src/commands/group/setpp.cjs +1 -1
- package/src/commands/group/setrules.cjs +2 -8
- package/src/commands/group/setwarn.cjs +4 -2
- package/src/commands/group/tagadmins.cjs +2 -5
- package/src/commands/group/warn.cjs +26 -24
- package/src/commands/group/warns.cjs +1 -2
- package/src/commands/group/welcome.cjs +13 -17
- package/src/commands/group.cjs +7 -10
- package/src/commands/help.cjs +6 -12
- package/src/commands/lang.cjs +104 -0
- package/src/commands/listschedules.cjs +2 -6
- package/src/commands/loop.cjs +6 -13
- package/src/commands/memory.cjs +19 -32
- package/src/commands/mod.cjs +97 -35
- package/src/commands/perm.cjs +18 -56
- package/src/commands/ping.cjs +1 -5
- package/src/commands/poll.cjs +1 -1
- package/src/commands/rand.cjs +2 -5
- package/src/commands/schedule.cjs +4 -8
- package/src/commands/score.cjs +8 -2
- package/src/commands/setprefix.cjs +2 -2
- package/src/commands/shortlink.cjs +7 -15
- package/src/commands/shutdown.cjs +1 -1
- package/src/commands/sticker.cjs +222 -0
- package/src/commands/stt.cjs +136 -68
- package/src/commands/todo.cjs +9 -15
- package/src/commands/tts.cjs +10 -16
- package/src/commands/unblock.cjs +2 -6
- package/src/commands/weather.cjs +5 -12
- package/src/config/ai-persona.md +5 -3
- package/src/config/available-models.json +1156 -0
- package/src/config/baileys.config.js +2 -1
- package/src/config/brand.esm.js +1 -10
- package/src/config/constants.js +1 -1
- package/src/config/defaults.cjs +66 -66
- package/src/config/lock.cjs +2 -2
- package/src/config/modelCapabilities.cjs +172 -0
- package/src/config/runtime-config.cjs +8 -13
- package/src/config/secrets.cjs +18 -10
- package/src/config/settings.cjs +94 -10
- package/src/config/vault.cjs +107 -0
- package/src/core/connection.js +13 -12
- package/src/core/events.js +41 -23
- package/src/core/proxy.js +7 -7
- package/src/core/session.js +91 -48
- package/src/core/socket.js +12 -15
- package/src/db/db.cjs +14 -6
- package/src/db/store.cjs +221 -87
- package/src/domain/caddy.js +1 -1
- package/src/domain/command.js +40 -19
- package/src/domain/detect.js +1 -1
- package/src/domain/system.js +2 -5
- package/src/handlers/command.handler.js +22 -43
- package/src/handlers/group.handler.js +13 -13
- package/src/handlers/message.handler.js +11 -21
- package/src/index.js +8 -4
- package/src/middleware/antispam.middleware.js +46 -23
- package/src/middleware/blacklist.middleware.js +14 -9
- package/src/middleware/forward-tracking.middleware.js +3 -2
- package/src/middleware/permissions.middleware.js +7 -10
- package/src/panel/login-throttle.cjs +43 -0
- package/src/panel/session-auth.cjs +17 -0
- package/src/routes/dashboard.api.esm.js +394 -72
- package/src/services/aiAgent.cjs +136 -49
- package/src/services/aiProviders.cjs +193 -47
- package/src/services/aiRouter.cjs +137 -0
- package/src/services/aiTools.cjs +305 -128
- package/src/services/llmDiscovery.cjs +481 -0
- package/src/services/modelRegistry.cjs +412 -0
- package/src/utils/datetime.cjs +2 -2
- package/src/utils/lid-helper.esm.js +7 -7
- package/src/utils/logger.cjs +1 -1
- package/src/utils/markdownParser.cjs +159 -0
- package/src/utils/memory.cjs +43 -13
- package/src/utils/messageContent.cjs +40 -0
- package/src/utils/normalizeJid.esm.js +7 -7
- package/src/utils/numerals.cjs +75 -0
- package/src/utils/permissions.esm.js +10 -15
- package/src/utils/providerUrl.cjs +281 -0
- package/src/utils/recurrence.cjs +7 -11
- package/src/utils/sendBotMessage.esm.js +4 -14
- package/src/utils/statusMessage.cjs +5 -13
- package/src/utils/storage.cjs +10 -0
- package/src/utils/storage.esm.js +11 -0
- package/src/utils/textDecode.cjs +4 -9
- package/src/utils/thumbnail.cjs +8 -14
- package/src/utils/typing.esm.js +12 -12
- package/views/dashboard.ejs +134 -106
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// Data-at-rest encryption for high-value secrets.
|
|
2
|
+
//
|
|
3
|
+
// The WhatsApp session (baileys_auth) and operator API keys must not sit in
|
|
4
|
+
// levix.db as plaintext. A 32-byte AES-256-GCM key is generated on first use
|
|
5
|
+
// and stored as `levix.key` in the data directory (mode 0600), never in the
|
|
6
|
+
// database — a leaked .db backup is not enough to recover the session or keys.
|
|
7
|
+
//
|
|
8
|
+
// On Android that directory is already the app-private files dir
|
|
9
|
+
// (`/data/data/net.leviro.levix/files/data`). On a desktop install it is the
|
|
10
|
+
// same folder as the database. Root on the device can still read both files;
|
|
11
|
+
// this stops a copied database, a partial backup, or a casual file dump.
|
|
12
|
+
//
|
|
13
|
+
// Format: enc:v1:<iv_hex>.<tag_hex>.<ciphertext_hex>
|
|
14
|
+
// Unsealed legacy values pass through `open()` so upgrades re-encrypt on write.
|
|
15
|
+
|
|
16
|
+
const crypto = require("node:crypto");
|
|
17
|
+
const fs = require("node:fs");
|
|
18
|
+
|
|
19
|
+
const logger = require("../utils/logger.cjs");
|
|
20
|
+
|
|
21
|
+
const PREFIX = "enc:v1:";
|
|
22
|
+
const KEY_BYTES = 32;
|
|
23
|
+
const IV_BYTES = 12;
|
|
24
|
+
const KEY_FILE = "levix.key";
|
|
25
|
+
|
|
26
|
+
let cachedKey = null;
|
|
27
|
+
|
|
28
|
+
function keyPath() {
|
|
29
|
+
const { dataPath, ensureDataDir } = require("./paths.cjs");
|
|
30
|
+
ensureDataDir();
|
|
31
|
+
return dataPath(KEY_FILE);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function loadMasterKey() {
|
|
35
|
+
if (cachedKey) return cachedKey;
|
|
36
|
+
const file = keyPath();
|
|
37
|
+
if (fs.existsSync(file)) {
|
|
38
|
+
const raw = fs.readFileSync(file);
|
|
39
|
+
if (raw.length === KEY_BYTES) {
|
|
40
|
+
cachedKey = raw;
|
|
41
|
+
return cachedKey;
|
|
42
|
+
}
|
|
43
|
+
const hex = raw.toString("utf8").trim();
|
|
44
|
+
if (/^[0-9a-f]{64}$/i.test(hex)) {
|
|
45
|
+
cachedKey = Buffer.from(hex, "hex");
|
|
46
|
+
return cachedKey;
|
|
47
|
+
}
|
|
48
|
+
throw new Error("levix.key is not a valid 32-byte encryption key");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const key = crypto.randomBytes(KEY_BYTES);
|
|
52
|
+
fs.writeFileSync(file, key, { mode: 0o600 });
|
|
53
|
+
try {
|
|
54
|
+
fs.chmodSync(file, 0o600);
|
|
55
|
+
} catch {
|
|
56
|
+
// Windows has no POSIX modes; the data directory still holds the file.
|
|
57
|
+
}
|
|
58
|
+
logger.info("[vault] Generated a new data-at-rest encryption key");
|
|
59
|
+
cachedKey = key;
|
|
60
|
+
return cachedKey;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isSealed(value) {
|
|
64
|
+
return typeof value === "string" && value.startsWith(PREFIX);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function seal(plaintext) {
|
|
68
|
+
const text = plaintext == null ? "" : String(plaintext);
|
|
69
|
+
const key = loadMasterKey();
|
|
70
|
+
const iv = crypto.randomBytes(IV_BYTES);
|
|
71
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
|
|
72
|
+
const encrypted = Buffer.concat([cipher.update(text, "utf8"), cipher.final()]);
|
|
73
|
+
const tag = cipher.getAuthTag();
|
|
74
|
+
return `${PREFIX}${iv.toString("hex")}.${tag.toString("hex")}.${encrypted.toString("hex")}`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function open(value) {
|
|
78
|
+
if (value == null) return value;
|
|
79
|
+
if (!isSealed(value)) return value;
|
|
80
|
+
const body = value.slice(PREFIX.length);
|
|
81
|
+
const parts = body.split(".");
|
|
82
|
+
if (parts.length !== 3) throw new Error("Malformed sealed secret");
|
|
83
|
+
const [ivHex, tagHex, dataHex] = parts;
|
|
84
|
+
const key = loadMasterKey();
|
|
85
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", key, Buffer.from(ivHex, "hex"));
|
|
86
|
+
decipher.setAuthTag(Buffer.from(tagHex, "hex"));
|
|
87
|
+
return Buffer.concat([
|
|
88
|
+
decipher.update(Buffer.from(dataHex, "hex")),
|
|
89
|
+
decipher.final(),
|
|
90
|
+
]).toString("utf8");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Reset the in-process key cache. Tests that switch data dirs need this. */
|
|
94
|
+
function resetVaultCache() {
|
|
95
|
+
cachedKey = null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = {
|
|
99
|
+
PREFIX,
|
|
100
|
+
KEY_FILE,
|
|
101
|
+
isSealed,
|
|
102
|
+
seal,
|
|
103
|
+
open,
|
|
104
|
+
loadMasterKey,
|
|
105
|
+
keyPath,
|
|
106
|
+
resetVaultCache,
|
|
107
|
+
};
|
package/src/core/connection.js
CHANGED
|
@@ -11,13 +11,14 @@
|
|
|
11
11
|
|
|
12
12
|
import { DisconnectReason } from "@whiskeysockets/baileys";
|
|
13
13
|
import { createRequire } from "module";
|
|
14
|
-
import {
|
|
14
|
+
import { bootstrapAdmins, bootstrapOwners } from "../utils/permissions.esm.js";
|
|
15
15
|
import {
|
|
16
|
-
saveUserMetadata,
|
|
17
|
-
getAllOwners,
|
|
18
16
|
getAllBotAdmins,
|
|
17
|
+
getAllOwners,
|
|
18
|
+
saveUserMetadata,
|
|
19
|
+
upsertGroupDirectory,
|
|
19
20
|
} from "../utils/storage.esm.js";
|
|
20
|
-
import {
|
|
21
|
+
import { groupMetadataCache } from "./socket.js";
|
|
21
22
|
|
|
22
23
|
const require = createRequire(import.meta.url);
|
|
23
24
|
const logger = require("../utils/logger.cjs");
|
|
@@ -123,9 +124,7 @@ export async function handleConnectionOpen(sock, initializeScheduledJobs) {
|
|
|
123
124
|
const botPn = sock?.user?.id
|
|
124
125
|
? `${sock.user.id.split(":")[0].split("@")[0]}@s.whatsapp.net`
|
|
125
126
|
: null;
|
|
126
|
-
const botLid = sock?.user?.lid
|
|
127
|
-
? `${sock.user.lid.split(":")[0].split("@")[0]}@lid`
|
|
128
|
-
: null;
|
|
127
|
+
const botLid = sock?.user?.lid ? `${sock.user.lid.split(":")[0].split("@")[0]}@lid` : null;
|
|
129
128
|
|
|
130
129
|
// Both rosters are seeded from the database so a permission check never has
|
|
131
130
|
// to hit it on the hot path.
|
|
@@ -144,7 +143,7 @@ export async function handleConnectionOpen(sock, initializeScheduledJobs) {
|
|
|
144
143
|
await primePermissions();
|
|
145
144
|
logger.info(
|
|
146
145
|
`[Permissions] Owner roster: ${ownersRoster.length} entries; ` +
|
|
147
|
-
`admin roster: ${adminsRoster.length} entries
|
|
146
|
+
`admin roster: ${adminsRoster.length} entries`,
|
|
148
147
|
);
|
|
149
148
|
|
|
150
149
|
// Called on every open, including a reconnect: scheduleNewJob() stops the
|
|
@@ -163,9 +162,7 @@ async function saveBotOwnerMetadata(sock) {
|
|
|
163
162
|
|
|
164
163
|
const phoneNumber = sock?.user?.id?.split(":")[0]?.split("@")[0];
|
|
165
164
|
const userJid = phoneNumber ? `${phoneNumber}@s.whatsapp.net` : null;
|
|
166
|
-
const userLid = sock?.user?.lid
|
|
167
|
-
? `${sock.user.lid.split(":")[0].split("@")[0]}@lid`
|
|
168
|
-
: null;
|
|
165
|
+
const userLid = sock?.user?.lid ? `${sock.user.lid.split(":")[0].split("@")[0]}@lid` : null;
|
|
169
166
|
const displayName = sock?.user?.name || sock?.user?.verifiedName || null;
|
|
170
167
|
|
|
171
168
|
if (userJid) {
|
|
@@ -177,7 +174,7 @@ async function saveBotOwnerMetadata(sock) {
|
|
|
177
174
|
displayName,
|
|
178
175
|
});
|
|
179
176
|
logger.info(
|
|
180
|
-
`[User Metadata] Saved bot owner: ${phoneNumber} (JID: ${userJid}, LID: ${userLid})
|
|
177
|
+
`[User Metadata] Saved bot owner: ${phoneNumber} (JID: ${userJid}, LID: ${userLid})`,
|
|
181
178
|
);
|
|
182
179
|
}
|
|
183
180
|
} catch (err) {
|
|
@@ -195,6 +192,10 @@ async function cacheAllGroups(sock) {
|
|
|
195
192
|
|
|
196
193
|
for (const jid in groups) {
|
|
197
194
|
groupMetadataCache.set(jid, groups[jid]);
|
|
195
|
+
upsertGroupDirectory(jid, {
|
|
196
|
+
subject: groups[jid]?.subject || null,
|
|
197
|
+
participantCount: groups[jid]?.participants?.length ?? null,
|
|
198
|
+
});
|
|
198
199
|
cachedCount++;
|
|
199
200
|
}
|
|
200
201
|
|
package/src/core/events.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { createRequire } from
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { createRequire } from "module";
|
|
2
|
+
import {
|
|
3
|
+
handleGroupJoinRequests,
|
|
4
|
+
handleGroupParticipantsUpdate,
|
|
5
|
+
} from "../handlers/group.handler.js";
|
|
6
|
+
import { handleIncomingMessage } from "../handlers/message.handler.js";
|
|
7
|
+
import { storeLidPnMappings, upsertGroupDirectory } from "../utils/storage.esm.js";
|
|
8
|
+
import { groupMetadataCache } from "./socket.js";
|
|
6
9
|
|
|
7
10
|
const require = createRequire(import.meta.url);
|
|
8
|
-
const logger = require(
|
|
11
|
+
const logger = require("../utils/logger.cjs");
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* Wire one socket up.
|
|
@@ -22,7 +25,7 @@ const logger = require('../utils/logger.cjs');
|
|
|
22
25
|
*/
|
|
23
26
|
export function setupEventListeners(
|
|
24
27
|
sock,
|
|
25
|
-
{ saveCreds, onConnectionUpdate, onHandshakeRejected } = {}
|
|
28
|
+
{ saveCreds, onConnectionUpdate, onHandshakeRejected } = {},
|
|
26
29
|
) {
|
|
27
30
|
// Every listener below goes through this. A bare `async` listener hands its
|
|
28
31
|
// promise to Baileys' emitter, which drops it — so one rejection anywhere in
|
|
@@ -36,8 +39,8 @@ export function setupEventListeners(
|
|
|
36
39
|
};
|
|
37
40
|
|
|
38
41
|
sock.ev.on(
|
|
39
|
-
|
|
40
|
-
contained(
|
|
42
|
+
"connection.update",
|
|
43
|
+
contained("connection.update", (update) => onConnectionUpdate?.(update)),
|
|
41
44
|
);
|
|
42
45
|
|
|
43
46
|
// A handshake the server answered with an HTTP status instead of an upgrade.
|
|
@@ -50,17 +53,17 @@ export function setupEventListeners(
|
|
|
50
53
|
// therefore produced no error, no close and no connection.update at all: the
|
|
51
54
|
// session simply sat in `starting` until somebody noticed.
|
|
52
55
|
sock.ws?.on?.(
|
|
53
|
-
|
|
54
|
-
contained(
|
|
56
|
+
"unexpected-response",
|
|
57
|
+
contained("unexpected-response", (_request, response) => {
|
|
55
58
|
onHandshakeRejected?.(response);
|
|
56
|
-
})
|
|
59
|
+
}),
|
|
57
60
|
);
|
|
58
61
|
|
|
59
62
|
// Save credentials
|
|
60
|
-
if (saveCreds) sock.ev.on(
|
|
63
|
+
if (saveCreds) sock.ev.on("creds.update", saveCreds);
|
|
61
64
|
|
|
62
65
|
// V7: LID mapping updates
|
|
63
|
-
sock.ev.on(
|
|
66
|
+
sock.ev.on("lid-mapping.update", (updates) => {
|
|
64
67
|
logger.info(`[LID] Received ${Object.keys(updates).length} LID mapping updates`);
|
|
65
68
|
const mappings = [];
|
|
66
69
|
for (const [lid, pn] of Object.entries(updates)) {
|
|
@@ -74,30 +77,45 @@ export function setupEventListeners(
|
|
|
74
77
|
});
|
|
75
78
|
|
|
76
79
|
// Group metadata updates
|
|
77
|
-
sock.ev.on(
|
|
80
|
+
sock.ev.on("groups.upsert", (updates) => {
|
|
78
81
|
for (const group of updates) {
|
|
79
82
|
logger.info(`[Cache] Caching metadata for group: ${group.id}`);
|
|
80
83
|
groupMetadataCache.set(group.id, group);
|
|
84
|
+
upsertGroupDirectory(group.id, {
|
|
85
|
+
subject: group.subject || null,
|
|
86
|
+
participantCount: group.participants?.length ?? null,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
sock.ev.on("groups.update", (updates) => {
|
|
92
|
+
for (const group of updates || []) {
|
|
93
|
+
if (!group?.id) continue;
|
|
94
|
+
const current = groupMetadataCache.get(group.id) || {};
|
|
95
|
+
const next = { ...current, ...group };
|
|
96
|
+
groupMetadataCache.set(group.id, next);
|
|
97
|
+
upsertGroupDirectory(group.id, {
|
|
98
|
+
subject: next.subject || null,
|
|
99
|
+
participantCount: next.participants?.length ?? null,
|
|
100
|
+
});
|
|
81
101
|
}
|
|
82
102
|
});
|
|
83
103
|
|
|
84
104
|
// Incoming messages
|
|
85
105
|
sock.ev.on(
|
|
86
|
-
|
|
87
|
-
contained(
|
|
106
|
+
"messages.upsert",
|
|
107
|
+
contained("messages.upsert", (m) => handleIncomingMessage(sock, m)),
|
|
88
108
|
);
|
|
89
109
|
|
|
90
110
|
// Group participant updates
|
|
91
111
|
sock.ev.on(
|
|
92
|
-
|
|
93
|
-
contained(
|
|
94
|
-
handleGroupParticipantsUpdate(sock, update)
|
|
95
|
-
)
|
|
112
|
+
"group-participants.update",
|
|
113
|
+
contained("group-participants.update", (update) => handleGroupParticipantsUpdate(sock, update)),
|
|
96
114
|
);
|
|
97
115
|
|
|
98
116
|
// Group join requests
|
|
99
117
|
sock.ev.on(
|
|
100
|
-
|
|
101
|
-
contained(
|
|
118
|
+
"group-requests.update",
|
|
119
|
+
contained("group-requests.update", (events) => handleGroupJoinRequests(sock, events)),
|
|
102
120
|
);
|
|
103
121
|
}
|
package/src/core/proxy.js
CHANGED
|
@@ -37,12 +37,12 @@
|
|
|
37
37
|
// string, its result never reaches a log or an HTTP response, and everything
|
|
38
38
|
// human-facing goes through `redactProxy()`.
|
|
39
39
|
|
|
40
|
-
import
|
|
40
|
+
import tls from "node:tls";
|
|
41
41
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
42
|
+
import { createRequire } from "module";
|
|
43
|
+
import { SocksClient } from "socks";
|
|
42
44
|
import { SocksProxyAgent } from "socks-proxy-agent";
|
|
43
45
|
import { Agent as UndiciAgent, ProxyAgent as UndiciProxyAgent } from "undici";
|
|
44
|
-
import { SocksClient } from "socks";
|
|
45
|
-
import tls from "node:tls";
|
|
46
46
|
|
|
47
47
|
const require = createRequire(import.meta.url);
|
|
48
48
|
const settings = require("../config/settings.cjs");
|
|
@@ -74,7 +74,9 @@ export function readProxyConfig() {
|
|
|
74
74
|
*/
|
|
75
75
|
export function normalizeProxyConfig(raw = {}) {
|
|
76
76
|
const enabled = raw.enabled === true || raw.enabled === "true" || raw.enabled === 1;
|
|
77
|
-
const protocol = String(raw.protocol ?? "http")
|
|
77
|
+
const protocol = String(raw.protocol ?? "http")
|
|
78
|
+
.trim()
|
|
79
|
+
.toLowerCase();
|
|
78
80
|
const host = String(raw.host ?? "").trim();
|
|
79
81
|
const port = Number(raw.port ?? 0);
|
|
80
82
|
// Usernames and passwords are taken exactly as given except for surrounding
|
|
@@ -179,9 +181,7 @@ function socksDispatcher(config) {
|
|
|
179
181
|
host: config.host,
|
|
180
182
|
port: config.port,
|
|
181
183
|
type: 5,
|
|
182
|
-
...(config.username
|
|
183
|
-
? { userId: config.username, password: config.password }
|
|
184
|
-
: {}),
|
|
184
|
+
...(config.username ? { userId: config.username, password: config.password } : {}),
|
|
185
185
|
};
|
|
186
186
|
|
|
187
187
|
return new UndiciAgent({
|
package/src/core/session.js
CHANGED
|
@@ -46,14 +46,21 @@
|
|
|
46
46
|
// Regenerating one forever is how the old code burned a socket a minute on
|
|
47
47
|
// installs nobody was pairing.
|
|
48
48
|
|
|
49
|
-
import { createRequire } from "module";
|
|
50
49
|
import { DisconnectReason } from "@whiskeysockets/baileys";
|
|
51
|
-
|
|
52
|
-
import { createWhatsAppSocket } from "./socket.js";
|
|
53
|
-
import { setupEventListeners } from "./events.js";
|
|
54
|
-
import { handleConnectionOpen, classifyDisconnect } from "./connection.js";
|
|
50
|
+
import { createRequire } from "module";
|
|
55
51
|
import { RETRY_SCHEDULE_MS } from "../config/constants.js";
|
|
56
|
-
import {
|
|
52
|
+
import {
|
|
53
|
+
clearAuthState,
|
|
54
|
+
clearWhatsAppDirectory,
|
|
55
|
+
deleteAllChatHistories,
|
|
56
|
+
deleteQrCode,
|
|
57
|
+
isPairedSession,
|
|
58
|
+
pauseAllSchedules,
|
|
59
|
+
saveQrCode,
|
|
60
|
+
} from "../utils/storage.esm.js";
|
|
61
|
+
import { bootstrapAdmins, bootstrapOwners } from "../utils/permissions.esm.js";
|
|
62
|
+
import { classifyDisconnect, handleConnectionOpen } from "./connection.js";
|
|
63
|
+
import { setupEventListeners } from "./events.js";
|
|
57
64
|
import {
|
|
58
65
|
createProxyAgents,
|
|
59
66
|
describeProxyFailure,
|
|
@@ -61,10 +68,33 @@ import {
|
|
|
61
68
|
readProxyConfig,
|
|
62
69
|
redactProxy,
|
|
63
70
|
} from "./proxy.js";
|
|
71
|
+
import { createWhatsAppSocket, groupMetadataCache } from "./socket.js";
|
|
64
72
|
|
|
65
73
|
const require = createRequire(import.meta.url);
|
|
66
74
|
const logger = require("../utils/logger.cjs");
|
|
67
75
|
|
|
76
|
+
function clearAccountScopedState() {
|
|
77
|
+
clearWhatsAppDirectory();
|
|
78
|
+
deleteAllChatHistories();
|
|
79
|
+
pauseAllSchedules();
|
|
80
|
+
bootstrapOwners([]);
|
|
81
|
+
bootstrapAdmins([]);
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
require("../utils/memory.cjs").clearAllMemoryFiles();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
logger.warn({ err: error }, "[Session] failed to clear AI memory after unlink");
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
require("../commands/gemini.cjs").clearAllContextBuffers();
|
|
90
|
+
} catch (error) {
|
|
91
|
+
logger.warn({ err: error }, "[Session] failed to clear AI context buffers after unlink");
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
groupMetadataCache.flushAll();
|
|
95
|
+
} catch {}
|
|
96
|
+
}
|
|
97
|
+
|
|
68
98
|
// Baileys' `end()` awaits `ws.close()`, which waits for the socket's own
|
|
69
99
|
// 'close' event with no timeout of its own. A half-dead TCP connection can
|
|
70
100
|
// therefore hang it forever — and this is on the SIGTERM path, so "forever"
|
|
@@ -89,14 +119,14 @@ export function normalizePairingPhone(input) {
|
|
|
89
119
|
const digits = value.replace(/\D/g, "");
|
|
90
120
|
if (digits.startsWith("0")) {
|
|
91
121
|
const error = new Error(
|
|
92
|
-
"Drop the leading 0 and include the country code (e.g. 2010… not 010…)."
|
|
122
|
+
"Drop the leading 0 and include the country code (e.g. 2010… not 010…).",
|
|
93
123
|
);
|
|
94
124
|
error.code = "PAIRING_PHONE";
|
|
95
125
|
throw error;
|
|
96
126
|
}
|
|
97
127
|
if (digits.length < 8 || digits.length > 15) {
|
|
98
128
|
const error = new Error(
|
|
99
|
-
"Enter the WhatsApp number with country code, digits only (e.g. 2010…)."
|
|
129
|
+
"Enter the WhatsApp number with country code, digits only (e.g. 2010…).",
|
|
100
130
|
);
|
|
101
131
|
error.code = "PAIRING_PHONE";
|
|
102
132
|
throw error;
|
|
@@ -221,12 +251,16 @@ export class WhatsAppSession {
|
|
|
221
251
|
retryDelaysMs = RETRY_SCHEDULE_MS,
|
|
222
252
|
log = logger,
|
|
223
253
|
pairingCodeDelayMs = 1500,
|
|
254
|
+
isLinked = isPairedSession,
|
|
255
|
+
clearDirectory = clearAccountScopedState,
|
|
224
256
|
} = {}) {
|
|
225
257
|
this.createSocket = createSocket;
|
|
226
258
|
this.attachListeners = attachListeners;
|
|
227
259
|
this.onOpen = onOpen;
|
|
228
260
|
this.initializeScheduledJobs = initializeScheduledJobs;
|
|
229
261
|
this.stopScheduledJobs = stopScheduledJobs;
|
|
262
|
+
this.isLinked = isLinked;
|
|
263
|
+
this.clearDirectory = clearDirectory;
|
|
230
264
|
this.loadProxy = loadProxy;
|
|
231
265
|
this.buildProxyAgents = buildProxyAgents;
|
|
232
266
|
this.emitEvent = emit;
|
|
@@ -271,9 +305,7 @@ export class WhatsAppSession {
|
|
|
271
305
|
/** Everything the dashboard needs to render the Connection screen. */
|
|
272
306
|
getState() {
|
|
273
307
|
const terminal =
|
|
274
|
-
this.#state === S.LOGGED_OUT ||
|
|
275
|
-
this.#state === S.RETRY_EXHAUSTED ||
|
|
276
|
-
this.#state === S.ERROR;
|
|
308
|
+
this.#state === S.LOGGED_OUT || this.#state === S.RETRY_EXHAUSTED || this.#state === S.ERROR;
|
|
277
309
|
|
|
278
310
|
return {
|
|
279
311
|
state: this.#state,
|
|
@@ -288,6 +320,8 @@ export class WhatsAppSession {
|
|
|
288
320
|
// leaves dead credentials behind, and unlinking is the only way out of
|
|
289
321
|
// it. The one state with nothing to unlink is the one that just did.
|
|
290
322
|
canUnlink: this.#state !== S.LOGGED_OUT && !this.#shuttingDown,
|
|
323
|
+
// Finished WhatsApp pairing — resume/connect, do not offer QR/pair.
|
|
324
|
+
linked: this.#state !== S.LOGGED_OUT && this.isLinked(),
|
|
291
325
|
// Redacted — never the password. Null when the socket is direct.
|
|
292
326
|
proxy: this.#proxyLabel,
|
|
293
327
|
// True when the saved proxy settings differ from what the live socket was
|
|
@@ -300,8 +334,7 @@ export class WhatsAppSession {
|
|
|
300
334
|
hasQr: !!this.#qr,
|
|
301
335
|
hasPairingCode: !!this.#pairingCode,
|
|
302
336
|
pairingMethod: this.#pairingIntent?.method || "qr",
|
|
303
|
-
pairingPhone:
|
|
304
|
-
this.#pairingIntent?.method === "pairing" ? this.#pairingIntent.phone : null,
|
|
337
|
+
pairingPhone: this.#pairingIntent?.method === "pairing" ? this.#pairingIntent.phone : null,
|
|
305
338
|
reason: this.#reason,
|
|
306
339
|
detail: this.#detail,
|
|
307
340
|
lastDisconnect: this.#lastDisconnect,
|
|
@@ -333,8 +366,7 @@ export class WhatsAppSession {
|
|
|
333
366
|
|
|
334
367
|
if (!isOnline) {
|
|
335
368
|
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;
|
|
369
|
+
this.#wasActiveBeforeOffline = BUSY_STATES.has(this.#state) || this.#state === S.PAUSED;
|
|
338
370
|
this.#cancelRetry();
|
|
339
371
|
|
|
340
372
|
if (this.#socket) {
|
|
@@ -378,7 +410,12 @@ export class WhatsAppSession {
|
|
|
378
410
|
}
|
|
379
411
|
if (BUSY_STATES.has(this.#state) && this.#state !== S.PAUSED) return this.getState();
|
|
380
412
|
|
|
381
|
-
|
|
413
|
+
// A known pairing reuses the stored session. Do not request a pairing code.
|
|
414
|
+
if (this.isLinked()) {
|
|
415
|
+
this.#pairingIntent = { method: "qr", phone: null };
|
|
416
|
+
} else {
|
|
417
|
+
this.#pairingIntent = parseStartOptions({ method, phone });
|
|
418
|
+
}
|
|
382
419
|
|
|
383
420
|
// A manual start clears whatever the last failure left behind.
|
|
384
421
|
this.#cancelRetry();
|
|
@@ -467,6 +504,13 @@ export class WhatsAppSession {
|
|
|
467
504
|
this.#clearAll = null;
|
|
468
505
|
this.#pairing = false;
|
|
469
506
|
this.#clearQr();
|
|
507
|
+
this.#stopJobs();
|
|
508
|
+
try {
|
|
509
|
+
this.clearDirectory?.();
|
|
510
|
+
} catch (error) {
|
|
511
|
+
this.log.warn({ err: error }, "[Session] failed to clear WhatsApp directory after unlink");
|
|
512
|
+
}
|
|
513
|
+
|
|
470
514
|
this.#transition(S.LOGGED_OUT, {
|
|
471
515
|
reason: "unlinked",
|
|
472
516
|
detail: "The WhatsApp account was unlinked. Start a session to pair a new one.",
|
|
@@ -534,10 +578,7 @@ export class WhatsAppSession {
|
|
|
534
578
|
proxyConfig = this.loadProxy();
|
|
535
579
|
proxy = this.buildProxyAgents(proxyConfig);
|
|
536
580
|
} catch (error) {
|
|
537
|
-
this.log.error(
|
|
538
|
-
{ err: error?.message },
|
|
539
|
-
"[Session] the proxy configuration is not usable"
|
|
540
|
-
);
|
|
581
|
+
this.log.error({ err: error?.message }, "[Session] the proxy configuration is not usable");
|
|
541
582
|
this.#proxyFingerprint = null;
|
|
542
583
|
this.#proxyLabel = null;
|
|
543
584
|
this.#transition(S.ERROR, {
|
|
@@ -557,7 +598,7 @@ export class WhatsAppSession {
|
|
|
557
598
|
try {
|
|
558
599
|
created = await this.createSocket({
|
|
559
600
|
proxy,
|
|
560
|
-
pairingCode: this.#pairingIntent.method === "pairing",
|
|
601
|
+
pairingCode: !this.isLinked() && this.#pairingIntent.method === "pairing",
|
|
561
602
|
});
|
|
562
603
|
} catch (error) {
|
|
563
604
|
this.log.error({ err: error }, "[Session] failed to create the WhatsApp socket");
|
|
@@ -594,12 +635,10 @@ export class WhatsAppSession {
|
|
|
594
635
|
this.attachListeners(sock, {
|
|
595
636
|
saveCreds,
|
|
596
637
|
onConnectionUpdate: (update) => this.#onConnectionUpdate(generation, update),
|
|
597
|
-
onHandshakeRejected: (response) =>
|
|
598
|
-
this.#onHandshakeRejected(generation, sock, response),
|
|
638
|
+
onHandshakeRejected: (response) => this.#onHandshakeRejected(generation, sock, response),
|
|
599
639
|
});
|
|
600
640
|
|
|
601
|
-
const waitingForCode =
|
|
602
|
-
this.#pairing && this.#pairingIntent.method === "pairing";
|
|
641
|
+
const waitingForCode = this.#pairing && this.#pairingIntent.method === "pairing";
|
|
603
642
|
this.#transition(this.#pairing ? S.WAITING_FOR_QR : S.STARTING, {
|
|
604
643
|
reason,
|
|
605
644
|
detail: this.#pairing
|
|
@@ -632,8 +671,8 @@ export class WhatsAppSession {
|
|
|
632
671
|
viaProxy && (status === 407 || status === 401)
|
|
633
672
|
? `The proxy at ${this.#proxyLabel} rejected the username or password (HTTP ${status}).`
|
|
634
673
|
: viaProxy
|
|
635
|
-
|
|
636
|
-
|
|
674
|
+
? `The proxy at ${this.#proxyLabel} refused to connect to WhatsApp (HTTP ${status}).`
|
|
675
|
+
: `WhatsApp refused the connection (HTTP ${status}).`;
|
|
637
676
|
|
|
638
677
|
this.log.error(`[Session] handshake rejected with HTTP ${status} — ${detail}`);
|
|
639
678
|
|
|
@@ -732,11 +771,7 @@ export class WhatsAppSession {
|
|
|
732
771
|
async #onQr(qr) {
|
|
733
772
|
// Pair-device readiness. In pairing-code mode the QR must not be shown;
|
|
734
773
|
// requestPairingCode waits for this stanza (or queues until it arrives).
|
|
735
|
-
if (
|
|
736
|
-
this.#pairing &&
|
|
737
|
-
this.#pairingIntent.method === "pairing" &&
|
|
738
|
-
this.#pairingIntent.phone
|
|
739
|
-
) {
|
|
774
|
+
if (this.#pairing && this.#pairingIntent.method === "pairing" && this.#pairingIntent.phone) {
|
|
740
775
|
await this.#requestPairingCode();
|
|
741
776
|
return;
|
|
742
777
|
}
|
|
@@ -779,18 +814,14 @@ export class WhatsAppSession {
|
|
|
779
814
|
this.#pairingCode = String(raw || "").replace(/\s|-/g, "");
|
|
780
815
|
this.#transition(S.WAITING_FOR_QR, {
|
|
781
816
|
reason: "pairing_code",
|
|
782
|
-
detail:
|
|
783
|
-
"Enter the code in WhatsApp → Linked devices → Link with phone number.",
|
|
817
|
+
detail: "Enter the code in WhatsApp → Linked devices → Link with phone number.",
|
|
784
818
|
});
|
|
785
819
|
this.emitEvent("pairing_code", {
|
|
786
820
|
code: this.#pairingCode,
|
|
787
821
|
phone,
|
|
788
822
|
});
|
|
789
823
|
} catch (error) {
|
|
790
|
-
this.log.error(
|
|
791
|
-
{ err: error?.message },
|
|
792
|
-
"[Session] pairing code request failed"
|
|
793
|
-
);
|
|
824
|
+
this.log.error({ err: error?.message }, "[Session] pairing code request failed");
|
|
794
825
|
if (!this.#pairing || this.#shuttingDown) return;
|
|
795
826
|
this.#transition(S.ERROR, {
|
|
796
827
|
reason: "pairing_failed",
|
|
@@ -822,9 +853,7 @@ export class WhatsAppSession {
|
|
|
822
853
|
async #onClose(lastDisconnect) {
|
|
823
854
|
const statusCode = lastDisconnect?.error?.output?.statusCode;
|
|
824
855
|
const reasonText =
|
|
825
|
-
lastDisconnect?.error?.output?.payload?.error ||
|
|
826
|
-
lastDisconnect?.error?.message ||
|
|
827
|
-
"Unknown";
|
|
856
|
+
lastDisconnect?.error?.output?.payload?.error || lastDisconnect?.error?.message || "Unknown";
|
|
828
857
|
|
|
829
858
|
this.#lastDisconnect = { statusCode: statusCode ?? null, reason: reasonText, at: Date.now() };
|
|
830
859
|
const closedSocket = this.#socket;
|
|
@@ -850,7 +879,7 @@ export class WhatsAppSession {
|
|
|
850
879
|
|
|
851
880
|
this.log.warn(
|
|
852
881
|
`[Session] Connection closed (${statusCode ?? "no status"}: ${reasonText}) — ${verdict.label}` +
|
|
853
|
-
(this.#proxyLabel ? ` via ${this.#proxyLabel}` : "")
|
|
882
|
+
(this.#proxyLabel ? ` via ${this.#proxyLabel}` : ""),
|
|
854
883
|
);
|
|
855
884
|
|
|
856
885
|
if (this.#shuttingDown) return;
|
|
@@ -868,7 +897,9 @@ export class WhatsAppSession {
|
|
|
868
897
|
if (verdict.terminal) {
|
|
869
898
|
this.#clearQr();
|
|
870
899
|
this.#pairing = false;
|
|
871
|
-
const codeInfo = this.#lastDisconnect?.statusCode
|
|
900
|
+
const codeInfo = this.#lastDisconnect?.statusCode
|
|
901
|
+
? ` (${this.#lastDisconnect.statusCode}: ${this.#lastDisconnect.reason})`
|
|
902
|
+
: "";
|
|
872
903
|
return this.#transition(S.DISCONNECTED, {
|
|
873
904
|
reason: verdict.reason,
|
|
874
905
|
detail: `${verdict.detail || ""}${codeInfo}`.trim(),
|
|
@@ -880,7 +911,9 @@ export class WhatsAppSession {
|
|
|
880
911
|
if (this.#pairing && !verdict.restartRequired) {
|
|
881
912
|
this.#pairing = false;
|
|
882
913
|
this.#clearQr();
|
|
883
|
-
const codeInfo = this.#lastDisconnect?.statusCode
|
|
914
|
+
const codeInfo = this.#lastDisconnect?.statusCode
|
|
915
|
+
? ` (${this.#lastDisconnect.statusCode}: ${this.#lastDisconnect.reason})`
|
|
916
|
+
: "";
|
|
884
917
|
return this.#transition(S.DISCONNECTED, {
|
|
885
918
|
reason: "pairing_cancelled",
|
|
886
919
|
detail: `The pairing attempt ended before the code was scanned${codeInfo}. Start a session to try again.`,
|
|
@@ -914,6 +947,12 @@ export class WhatsAppSession {
|
|
|
914
947
|
this.#clearQr();
|
|
915
948
|
this.#cancelRetry();
|
|
916
949
|
this.#attempt = 0;
|
|
950
|
+
this.#stopJobs();
|
|
951
|
+
try {
|
|
952
|
+
this.clearDirectory?.();
|
|
953
|
+
} catch (error) {
|
|
954
|
+
this.log.warn({ err: error }, "[Session] failed to clear WhatsApp directory after logout");
|
|
955
|
+
}
|
|
917
956
|
this.#transition(S.LOGGED_OUT, { reason: verdict.reason, detail: verdict.detail });
|
|
918
957
|
}
|
|
919
958
|
|
|
@@ -937,10 +976,12 @@ export class WhatsAppSession {
|
|
|
937
976
|
|
|
938
977
|
if (this.#attempt >= this.retryDelaysMs.length) {
|
|
939
978
|
this.log.error(
|
|
940
|
-
`[Session] ${this.retryDelaysMs.length} reconnect attempts failed. Levix stays up; start the session again from the panel
|
|
979
|
+
`[Session] ${this.retryDelaysMs.length} reconnect attempts failed. Levix stays up; start the session again from the panel.`,
|
|
941
980
|
);
|
|
942
981
|
this.#clearQr();
|
|
943
|
-
const codeInfo = this.#lastDisconnect?.statusCode
|
|
982
|
+
const codeInfo = this.#lastDisconnect?.statusCode
|
|
983
|
+
? ` (${this.#lastDisconnect.statusCode}: ${this.#lastDisconnect.reason})`
|
|
984
|
+
: "";
|
|
944
985
|
return this.#transition(S.RETRY_EXHAUSTED, {
|
|
945
986
|
reason: "retry_exhausted",
|
|
946
987
|
detail: `Gave up after ${this.retryDelaysMs.length} attempts${codeInfo}. Start the session to try again.`,
|
|
@@ -952,10 +993,12 @@ export class WhatsAppSession {
|
|
|
952
993
|
this.#nextRetryAt = Date.now() + delayMs;
|
|
953
994
|
|
|
954
995
|
this.log.warn(
|
|
955
|
-
`[Session] Reconnecting in ${Math.round(delayMs / 1000)}s (attempt ${this.#attempt}/${this.retryDelaysMs.length})
|
|
996
|
+
`[Session] Reconnecting in ${Math.round(delayMs / 1000)}s (attempt ${this.#attempt}/${this.retryDelaysMs.length})`,
|
|
956
997
|
);
|
|
957
998
|
|
|
958
|
-
const codeInfo = this.#lastDisconnect?.statusCode
|
|
999
|
+
const codeInfo = this.#lastDisconnect?.statusCode
|
|
1000
|
+
? ` (${this.#lastDisconnect.statusCode}: ${this.#lastDisconnect.reason})`
|
|
1001
|
+
: "";
|
|
959
1002
|
this.#transition(S.RECONNECTING, {
|
|
960
1003
|
reason: verdict.reason,
|
|
961
1004
|
detail:
|