@wednesdayai/whatsapp 0.4.10
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/dist/index.js +3678 -0
- package/dist/login-qr-B5goEXCP.js +1064 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +43 -0
|
@@ -0,0 +1,1064 @@
|
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID, VERSION, acquireFileLock, createAccountListHelpers, danger, defaultRuntime, encodePngRgba, ensureDir, fillPixel, formatCliCommand, getChildLogger, info, jidToE164, logInfo, normalizeAccountId, resolveAccountEntry, resolveOAuthDir, resolveUserPath, success, toPinoLikeLogger } from "openclaw/plugin-sdk";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import fs$1 from "node:fs/promises";
|
|
6
|
+
import { resolveProcessScopedMap } from "openclaw/plugin-sdk/process-scoped-map";
|
|
7
|
+
import QRCodeModule from "qrcode-terminal/vendor/QRCode/index.js";
|
|
8
|
+
import QRErrorCorrectLevelModule from "qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js";
|
|
9
|
+
import qrcode from "qrcode-terminal";
|
|
10
|
+
|
|
11
|
+
//#region \0rolldown/runtime.js
|
|
12
|
+
var __defProp = Object.defineProperty;
|
|
13
|
+
var __exportAll = (all, no_symbols) => {
|
|
14
|
+
let target = {};
|
|
15
|
+
for (var name in all) {
|
|
16
|
+
__defProp(target, name, {
|
|
17
|
+
get: all[name],
|
|
18
|
+
enumerable: true
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
if (!no_symbols) {
|
|
22
|
+
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
23
|
+
}
|
|
24
|
+
return target;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
//#region src/accounts.ts
|
|
29
|
+
const { listConfiguredAccountIds, listAccountIds, resolveDefaultAccountId } = createAccountListHelpers("whatsapp");
|
|
30
|
+
const listWhatsAppAccountIds = listAccountIds;
|
|
31
|
+
const resolveDefaultWhatsAppAccountId = resolveDefaultAccountId;
|
|
32
|
+
function resolveAccountConfig(cfg, accountId) {
|
|
33
|
+
return resolveAccountEntry(cfg.channels?.whatsapp?.accounts, accountId);
|
|
34
|
+
}
|
|
35
|
+
function resolveDefaultAuthDir(accountId) {
|
|
36
|
+
return path.join(resolveOAuthDir(), "whatsapp", normalizeAccountId(accountId));
|
|
37
|
+
}
|
|
38
|
+
function resolveLegacyAuthDir() {
|
|
39
|
+
return resolveOAuthDir();
|
|
40
|
+
}
|
|
41
|
+
function legacyAuthExists(authDir) {
|
|
42
|
+
try {
|
|
43
|
+
return fs.existsSync(path.join(authDir, "creds.json"));
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function resolveWhatsAppAuthDir(params) {
|
|
49
|
+
const accountId = params.accountId.trim() || DEFAULT_ACCOUNT_ID;
|
|
50
|
+
const configured = resolveAccountConfig(params.cfg, accountId)?.authDir?.trim();
|
|
51
|
+
if (configured) return {
|
|
52
|
+
authDir: resolveUserPath(configured),
|
|
53
|
+
isLegacy: false
|
|
54
|
+
};
|
|
55
|
+
const defaultDir = resolveDefaultAuthDir(accountId);
|
|
56
|
+
if (accountId === DEFAULT_ACCOUNT_ID) {
|
|
57
|
+
const legacyDir = resolveLegacyAuthDir();
|
|
58
|
+
if (legacyAuthExists(legacyDir) && !legacyAuthExists(defaultDir)) return {
|
|
59
|
+
authDir: legacyDir,
|
|
60
|
+
isLegacy: true
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
authDir: defaultDir,
|
|
65
|
+
isLegacy: false
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function resolveWhatsAppAccount$1(params) {
|
|
69
|
+
const rootCfg = params.cfg.channels?.whatsapp;
|
|
70
|
+
const accountId = params.accountId?.trim() || resolveDefaultWhatsAppAccountId(params.cfg);
|
|
71
|
+
const accountCfg = resolveAccountConfig(params.cfg, accountId);
|
|
72
|
+
const enabled = rootCfg?.enabled !== false && accountCfg?.enabled !== false;
|
|
73
|
+
const { authDir, isLegacy } = resolveWhatsAppAuthDir({
|
|
74
|
+
cfg: params.cfg,
|
|
75
|
+
accountId
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
accountId,
|
|
79
|
+
name: accountCfg?.name?.trim() || void 0,
|
|
80
|
+
enabled,
|
|
81
|
+
sendReadReceipts: accountCfg?.sendReadReceipts ?? rootCfg?.sendReadReceipts ?? true,
|
|
82
|
+
messagePrefix: accountCfg?.messagePrefix ?? rootCfg?.messagePrefix ?? params.cfg.messages?.messagePrefix,
|
|
83
|
+
authDir,
|
|
84
|
+
isLegacyAuthDir: isLegacy,
|
|
85
|
+
selfChatMode: accountCfg?.selfChatMode ?? rootCfg?.selfChatMode,
|
|
86
|
+
dmPolicy: accountCfg?.dmPolicy ?? rootCfg?.dmPolicy,
|
|
87
|
+
allowFrom: accountCfg?.allowFrom ?? rootCfg?.allowFrom,
|
|
88
|
+
groupAllowFrom: accountCfg?.groupAllowFrom ?? rootCfg?.groupAllowFrom,
|
|
89
|
+
groupPolicy: accountCfg?.groupPolicy ?? rootCfg?.groupPolicy,
|
|
90
|
+
textChunkLimit: accountCfg?.textChunkLimit ?? rootCfg?.textChunkLimit,
|
|
91
|
+
chunkMode: accountCfg?.chunkMode ?? rootCfg?.chunkMode,
|
|
92
|
+
chunkNewlinePacking: accountCfg?.chunkNewlinePacking ?? rootCfg?.chunkNewlinePacking,
|
|
93
|
+
mediaMaxMb: accountCfg?.mediaMaxMb ?? rootCfg?.mediaMaxMb,
|
|
94
|
+
blockStreaming: accountCfg?.blockStreaming ?? rootCfg?.blockStreaming,
|
|
95
|
+
ackReaction: accountCfg?.ackReaction ?? rootCfg?.ackReaction,
|
|
96
|
+
groups: accountCfg?.groups ?? rootCfg?.groups,
|
|
97
|
+
debounceMs: accountCfg?.debounceMs ?? rootCfg?.debounceMs
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/active-listener.ts
|
|
103
|
+
const listeners = /* @__PURE__ */ new Map();
|
|
104
|
+
const connectionLocks = /* @__PURE__ */ new Map();
|
|
105
|
+
function withAccountLock(accountId, fn) {
|
|
106
|
+
const next = (connectionLocks.get(accountId) ?? Promise.resolve()).then(fn);
|
|
107
|
+
const settled = next.catch(() => void 0);
|
|
108
|
+
connectionLocks.set(accountId, settled);
|
|
109
|
+
settled.finally(() => {
|
|
110
|
+
if (connectionLocks.get(accountId) === settled) connectionLocks.delete(accountId);
|
|
111
|
+
});
|
|
112
|
+
return next;
|
|
113
|
+
}
|
|
114
|
+
function resolveWebAccountId(accountId) {
|
|
115
|
+
return (accountId ?? "").trim() || DEFAULT_ACCOUNT_ID;
|
|
116
|
+
}
|
|
117
|
+
function setActiveWebListener(accountIdOrListener, maybeListener) {
|
|
118
|
+
const { accountId, listener } = typeof accountIdOrListener === "string" ? {
|
|
119
|
+
accountId: accountIdOrListener,
|
|
120
|
+
listener: maybeListener ?? null
|
|
121
|
+
} : {
|
|
122
|
+
accountId: DEFAULT_ACCOUNT_ID,
|
|
123
|
+
listener: accountIdOrListener ?? null
|
|
124
|
+
};
|
|
125
|
+
const id = resolveWebAccountId(accountId);
|
|
126
|
+
if (!listener) listeners.delete(id);
|
|
127
|
+
else listeners.set(id, listener);
|
|
128
|
+
}
|
|
129
|
+
function getActiveWebListener(accountId) {
|
|
130
|
+
const id = resolveWebAccountId(accountId);
|
|
131
|
+
return listeners.get(id) ?? null;
|
|
132
|
+
}
|
|
133
|
+
async function stopActiveWebListener(accountId, reason = {
|
|
134
|
+
status: void 0,
|
|
135
|
+
isLoggedOut: false,
|
|
136
|
+
error: "closed"
|
|
137
|
+
}) {
|
|
138
|
+
const id = resolveWebAccountId(accountId);
|
|
139
|
+
if (!listeners.has(id)) return false;
|
|
140
|
+
let stopped = false;
|
|
141
|
+
await withAccountLock(id, async () => {
|
|
142
|
+
const current = listeners.get(id);
|
|
143
|
+
if (!current) return;
|
|
144
|
+
listeners.delete(id);
|
|
145
|
+
try {
|
|
146
|
+
current.signalClose?.(reason);
|
|
147
|
+
} catch {}
|
|
148
|
+
try {
|
|
149
|
+
await current.close?.();
|
|
150
|
+
stopped = true;
|
|
151
|
+
} catch (err) {
|
|
152
|
+
if (!listeners.has(id)) listeners.set(id, current);
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
return stopped;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
//#endregion
|
|
160
|
+
//#region src/runtime.ts
|
|
161
|
+
let runtime = null;
|
|
162
|
+
function setWhatsAppRuntime(next) {
|
|
163
|
+
runtime = next;
|
|
164
|
+
}
|
|
165
|
+
function getWhatsAppRuntime() {
|
|
166
|
+
if (!runtime) throw new Error("WhatsApp runtime not initialized");
|
|
167
|
+
return runtime;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/auth-store.ts
|
|
172
|
+
function resolveDefaultWebAuthDir() {
|
|
173
|
+
return path.join(resolveOAuthDir(), "whatsapp", DEFAULT_ACCOUNT_ID);
|
|
174
|
+
}
|
|
175
|
+
const WA_WEB_AUTH_DIR = resolveDefaultWebAuthDir();
|
|
176
|
+
function resolveWebCredsPath(authDir) {
|
|
177
|
+
return path.join(authDir, "creds.json");
|
|
178
|
+
}
|
|
179
|
+
function resolveWebCredsBackupPath(authDir) {
|
|
180
|
+
return path.join(authDir, "creds.json.bak");
|
|
181
|
+
}
|
|
182
|
+
const RECENT_CREDS_WRITE_GRACE_MS = 5e3;
|
|
183
|
+
const AUTH_CLEAR_SAVE_SUPPRESSION_MS = 5e3;
|
|
184
|
+
const WA_SOCKET_LOCK_OPTIONS$1 = {
|
|
185
|
+
retries: {
|
|
186
|
+
retries: 0,
|
|
187
|
+
factor: 1,
|
|
188
|
+
minTimeout: 100,
|
|
189
|
+
maxTimeout: 100
|
|
190
|
+
},
|
|
191
|
+
stale: 10080 * 60 * 1e3,
|
|
192
|
+
reentrant: false
|
|
193
|
+
};
|
|
194
|
+
const authDirsBeingCleared = resolveProcessScopedMap(Symbol.for("openclaw.webAuthDirsBeingCleared"));
|
|
195
|
+
const authDirClearSuppressedUntil = resolveProcessScopedMap(Symbol.for("openclaw.webAuthDirClearSuppressedUntil"));
|
|
196
|
+
const credsSaveQueues = resolveProcessScopedMap(Symbol.for("openclaw.webCredsSaveQueues"));
|
|
197
|
+
function canonicalWebAuthDir(authDir) {
|
|
198
|
+
const resolvedAuthDir = resolveUserPath(authDir);
|
|
199
|
+
try {
|
|
200
|
+
return fs.realpathSync.native(resolvedAuthDir);
|
|
201
|
+
} catch (err) {
|
|
202
|
+
if (err.code === "ENOENT") return resolvedAuthDir;
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function isWebAuthBeingCleared(authDir) {
|
|
207
|
+
const authDirKey = canonicalWebAuthDir(authDir);
|
|
208
|
+
const suppressedUntil = authDirClearSuppressedUntil.get(authDirKey) ?? 0;
|
|
209
|
+
if (suppressedUntil > Date.now()) return true;
|
|
210
|
+
if (suppressedUntil) authDirClearSuppressedUntil.delete(authDirKey);
|
|
211
|
+
return authDirsBeingCleared.has(authDirKey);
|
|
212
|
+
}
|
|
213
|
+
function statFileSync(filePath) {
|
|
214
|
+
try {
|
|
215
|
+
return fs.statSync(filePath);
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function isRecentFileWrite(stats) {
|
|
221
|
+
return Boolean(stats?.isFile() && Date.now() - stats.mtimeMs < RECENT_CREDS_WRITE_GRACE_MS);
|
|
222
|
+
}
|
|
223
|
+
function readCredsJsonRaw(filePath) {
|
|
224
|
+
try {
|
|
225
|
+
if (!fs.existsSync(filePath)) return null;
|
|
226
|
+
const stats = fs.statSync(filePath);
|
|
227
|
+
if (!stats.isFile() || stats.size <= 1) return null;
|
|
228
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
229
|
+
} catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function enqueueWebCredsSave(authDir, saveCreds, logger) {
|
|
234
|
+
const resolvedAuthDir = resolveUserPath(authDir);
|
|
235
|
+
const authDirKey = canonicalWebAuthDir(resolvedAuthDir);
|
|
236
|
+
if (isWebAuthBeingCleared(authDirKey)) return;
|
|
237
|
+
const previous = credsSaveQueues.get(authDirKey) ?? Promise.resolve();
|
|
238
|
+
let next;
|
|
239
|
+
next = previous.then(() => safeSaveCreds(resolvedAuthDir, saveCreds, logger)).catch((err) => {
|
|
240
|
+
logger.warn({ error: String(err) }, "WhatsApp creds save queue error");
|
|
241
|
+
}).finally(() => {
|
|
242
|
+
if (credsSaveQueues.get(authDirKey) === next) credsSaveQueues.delete(authDirKey);
|
|
243
|
+
});
|
|
244
|
+
credsSaveQueues.set(authDirKey, next);
|
|
245
|
+
}
|
|
246
|
+
function suppressPostClearCredsSaves(authDir) {
|
|
247
|
+
const authDirKey = canonicalWebAuthDir(authDir);
|
|
248
|
+
const suppressedUntil = Date.now() + AUTH_CLEAR_SAVE_SUPPRESSION_MS;
|
|
249
|
+
authDirClearSuppressedUntil.set(authDirKey, suppressedUntil);
|
|
250
|
+
setTimeout(() => {
|
|
251
|
+
if (authDirClearSuppressedUntil.get(authDirKey) === suppressedUntil) authDirClearSuppressedUntil.delete(authDirKey);
|
|
252
|
+
}, AUTH_CLEAR_SAVE_SUPPRESSION_MS).unref?.();
|
|
253
|
+
}
|
|
254
|
+
function clearWebAuthClearSuppression(authDir) {
|
|
255
|
+
authDirClearSuppressedUntil.delete(canonicalWebAuthDir(authDir));
|
|
256
|
+
}
|
|
257
|
+
async function waitForWebCredsSaveQueue(authDir) {
|
|
258
|
+
const authDirKey = canonicalWebAuthDir(authDir);
|
|
259
|
+
while (true) {
|
|
260
|
+
const queue = credsSaveQueues.get(authDirKey);
|
|
261
|
+
if (!queue) return;
|
|
262
|
+
await queue;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async function safeSaveCreds(authDir, saveCreds, logger) {
|
|
266
|
+
if (isWebAuthBeingCleared(authDir)) return;
|
|
267
|
+
try {
|
|
268
|
+
const credsPath = resolveWebCredsPath(authDir);
|
|
269
|
+
const backupPath = resolveWebCredsBackupPath(authDir);
|
|
270
|
+
const raw = readCredsJsonRaw(credsPath);
|
|
271
|
+
if (raw) try {
|
|
272
|
+
JSON.parse(raw);
|
|
273
|
+
fs.copyFileSync(credsPath, backupPath);
|
|
274
|
+
try {
|
|
275
|
+
fs.chmodSync(backupPath, 384);
|
|
276
|
+
} catch {}
|
|
277
|
+
} catch {}
|
|
278
|
+
} catch {}
|
|
279
|
+
if (isWebAuthBeingCleared(authDir)) return;
|
|
280
|
+
try {
|
|
281
|
+
await Promise.resolve(saveCreds());
|
|
282
|
+
try {
|
|
283
|
+
fs.chmodSync(resolveWebCredsPath(authDir), 384);
|
|
284
|
+
} catch {}
|
|
285
|
+
} catch (err) {
|
|
286
|
+
logger.warn({ error: String(err) }, "failed saving WhatsApp creds");
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function isParseableJson(raw) {
|
|
290
|
+
try {
|
|
291
|
+
JSON.parse(raw);
|
|
292
|
+
return true;
|
|
293
|
+
} catch {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
function maybeRestoreCredsFromBackup(authDir) {
|
|
298
|
+
const logger = getChildLogger({ module: "web-session" });
|
|
299
|
+
try {
|
|
300
|
+
if (isWebAuthBeingCleared(authDir)) return;
|
|
301
|
+
const credsPath = resolveWebCredsPath(authDir);
|
|
302
|
+
const backupPath = resolveWebCredsBackupPath(authDir);
|
|
303
|
+
const raw = readCredsJsonRaw(credsPath);
|
|
304
|
+
if (raw && isParseableJson(raw)) return;
|
|
305
|
+
if (isRecentFileWrite(statFileSync(credsPath))) return;
|
|
306
|
+
const backupRaw = readCredsJsonRaw(backupPath);
|
|
307
|
+
const backupStats = statFileSync(backupPath);
|
|
308
|
+
if (!backupStats?.isFile() || !backupRaw || !isParseableJson(backupRaw)) return;
|
|
309
|
+
fs.copyFileSync(backupPath, credsPath);
|
|
310
|
+
try {
|
|
311
|
+
fs.chmodSync(credsPath, 384);
|
|
312
|
+
} catch {}
|
|
313
|
+
try {
|
|
314
|
+
fs.utimesSync(credsPath, backupStats.atime, backupStats.mtime);
|
|
315
|
+
} catch {}
|
|
316
|
+
logger.warn({ credsPath }, "restored corrupted WhatsApp creds.json from backup");
|
|
317
|
+
} catch {}
|
|
318
|
+
}
|
|
319
|
+
async function webAuthExists(authDir = resolveDefaultWebAuthDir()) {
|
|
320
|
+
const resolvedAuthDir = resolveUserPath(authDir);
|
|
321
|
+
maybeRestoreCredsFromBackup(resolvedAuthDir);
|
|
322
|
+
const credsPath = resolveWebCredsPath(resolvedAuthDir);
|
|
323
|
+
try {
|
|
324
|
+
await fs$1.access(resolvedAuthDir);
|
|
325
|
+
} catch {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
const stats = await fs$1.stat(credsPath);
|
|
330
|
+
if (!stats.isFile() || stats.size <= 1) return false;
|
|
331
|
+
const raw = await fs$1.readFile(credsPath, "utf-8");
|
|
332
|
+
JSON.parse(raw);
|
|
333
|
+
return true;
|
|
334
|
+
} catch {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function isLegacyBaileysAuthFile(name) {
|
|
339
|
+
if (name === "creds.json" || name === "creds.json.bak") return true;
|
|
340
|
+
if (!name.endsWith(".json")) return false;
|
|
341
|
+
return /^(app-state-sync|session|sender-key|pre-key)-/.test(name);
|
|
342
|
+
}
|
|
343
|
+
async function clearBaileysAuthState(authDir, isLegacyAuthDir) {
|
|
344
|
+
const entries = await fs$1.readdir(authDir, { withFileTypes: true });
|
|
345
|
+
const shouldDelete = (name) => {
|
|
346
|
+
if (name === "oauth.json") return false;
|
|
347
|
+
if (isLegacyAuthDir) return isLegacyBaileysAuthFile(name);
|
|
348
|
+
return name === "creds.json.bak" || name.endsWith(".json");
|
|
349
|
+
};
|
|
350
|
+
const deleteNames = entries.filter((entry) => (entry.isFile() || entry.isSymbolicLink()) && shouldDelete(entry.name)).map((entry) => entry.name).toSorted((a, b) => {
|
|
351
|
+
if (a === "creds.json.bak") return -1;
|
|
352
|
+
if (b === "creds.json.bak") return 1;
|
|
353
|
+
if (a === "creds.json") return -1;
|
|
354
|
+
if (b === "creds.json") return 1;
|
|
355
|
+
return a.localeCompare(b);
|
|
356
|
+
});
|
|
357
|
+
for (const name of deleteNames) await fs$1.rm(path.join(authDir, name), { force: true });
|
|
358
|
+
return deleteNames.length;
|
|
359
|
+
}
|
|
360
|
+
function resolveWaSocketLockPath$1(authDir) {
|
|
361
|
+
return path.join(authDir, ".socket-owner");
|
|
362
|
+
}
|
|
363
|
+
async function acquireWaLogoutLock(authDir) {
|
|
364
|
+
try {
|
|
365
|
+
return await acquireFileLock(resolveWaSocketLockPath$1(authDir), WA_SOCKET_LOCK_OPTIONS$1);
|
|
366
|
+
} catch (err) {
|
|
367
|
+
throw new Error(`WhatsApp auth directory is already in use by another openclaw process or socket in this process (${authDir}). Stop the gateway before logging out.`, { cause: err });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async function releaseWaLogoutLock(lock) {
|
|
371
|
+
try {
|
|
372
|
+
await lock.release();
|
|
373
|
+
} catch {
|
|
374
|
+
await lock.release();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
async function logoutWeb(params) {
|
|
378
|
+
const runtime = params.runtime ?? defaultRuntime;
|
|
379
|
+
const resolvedAuthDir = resolveUserPath(params.authDir ?? resolveDefaultWebAuthDir());
|
|
380
|
+
let stats;
|
|
381
|
+
try {
|
|
382
|
+
stats = await fs$1.lstat(resolvedAuthDir);
|
|
383
|
+
} catch {
|
|
384
|
+
runtime.log(info("No WhatsApp Web session found; nothing to delete."));
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
if (stats.isSymbolicLink()) throw new Error(`Refusing to clear WhatsApp auth directory because it is a symlink: ${resolvedAuthDir}`);
|
|
388
|
+
if (!stats.isDirectory()) {
|
|
389
|
+
runtime.log(info("No WhatsApp Web session found; nothing to delete."));
|
|
390
|
+
return false;
|
|
391
|
+
}
|
|
392
|
+
const socketLock = await acquireWaLogoutLock(resolvedAuthDir);
|
|
393
|
+
let authDirKey = null;
|
|
394
|
+
try {
|
|
395
|
+
authDirKey = canonicalWebAuthDir(resolvedAuthDir);
|
|
396
|
+
authDirsBeingCleared.set(authDirKey, true);
|
|
397
|
+
await waitForWebCredsSaveQueue(authDirKey);
|
|
398
|
+
const exists = await webAuthExists(resolvedAuthDir);
|
|
399
|
+
const deletedCount = await clearBaileysAuthState(resolvedAuthDir, Boolean(params.isLegacyAuthDir));
|
|
400
|
+
if (!exists && deletedCount === 0) {
|
|
401
|
+
runtime.log(info("No WhatsApp Web session found; nothing to delete."));
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
runtime.log(success("Cleared WhatsApp Web credentials."));
|
|
405
|
+
return true;
|
|
406
|
+
} finally {
|
|
407
|
+
if (authDirKey) {
|
|
408
|
+
authDirsBeingCleared.delete(authDirKey);
|
|
409
|
+
suppressPostClearCredsSaves(authDirKey);
|
|
410
|
+
}
|
|
411
|
+
await releaseWaLogoutLock(socketLock);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
function readWebSelfId(authDir = resolveDefaultWebAuthDir()) {
|
|
415
|
+
try {
|
|
416
|
+
const credsPath = resolveWebCredsPath(resolveUserPath(authDir));
|
|
417
|
+
if (!fs.existsSync(credsPath)) return {
|
|
418
|
+
e164: null,
|
|
419
|
+
jid: null
|
|
420
|
+
};
|
|
421
|
+
const raw = fs.readFileSync(credsPath, "utf-8");
|
|
422
|
+
const jid = JSON.parse(raw)?.me?.id ?? null;
|
|
423
|
+
return {
|
|
424
|
+
e164: jid ? jidToE164(jid, { authDir }) : null,
|
|
425
|
+
jid
|
|
426
|
+
};
|
|
427
|
+
} catch {
|
|
428
|
+
return {
|
|
429
|
+
e164: null,
|
|
430
|
+
jid: null
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Return the age (in milliseconds) of the cached WhatsApp web auth state, or null when missing.
|
|
436
|
+
* Helpful for heartbeats/observability to spot stale credentials.
|
|
437
|
+
*/
|
|
438
|
+
function getWebAuthAgeMs(authDir = resolveDefaultWebAuthDir()) {
|
|
439
|
+
try {
|
|
440
|
+
const stats = fs.statSync(resolveWebCredsPath(resolveUserPath(authDir)));
|
|
441
|
+
return Date.now() - stats.mtimeMs;
|
|
442
|
+
} catch {
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
function logWebSelfId(authDir = resolveDefaultWebAuthDir(), runtime = defaultRuntime, includeChannelPrefix = false) {
|
|
447
|
+
const { e164, jid } = readWebSelfId(authDir);
|
|
448
|
+
const details = e164 || jid ? `${e164 ?? "unknown"}${jid ? ` (jid ${jid})` : ""}` : "unknown";
|
|
449
|
+
const prefix = includeChannelPrefix ? "Web Channel: " : "";
|
|
450
|
+
runtime.log(info(`${prefix}${details}`));
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
//#endregion
|
|
454
|
+
//#region src/baileys.ts
|
|
455
|
+
var WhatsAppDependencyError = class extends Error {
|
|
456
|
+
constructor(cause) {
|
|
457
|
+
super("WhatsApp dependency '@whiskeysockets/baileys' is not installed. Install the WhatsApp extension's dependencies (run `npm install --omit=dev` in the extension directory, or install the channel via `openclaw plugins install`).", { cause });
|
|
458
|
+
this.name = "WhatsAppDependencyError";
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
function isModuleNotFound(err) {
|
|
462
|
+
if (typeof err !== "object" || err === null) return false;
|
|
463
|
+
const code = err.code;
|
|
464
|
+
if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") return true;
|
|
465
|
+
const message = err.message;
|
|
466
|
+
return typeof message === "string" && message.includes("Cannot find module");
|
|
467
|
+
}
|
|
468
|
+
let cached = null;
|
|
469
|
+
/**
|
|
470
|
+
* Resolve baileys via an injectable importer. `getBaileys` calls this with the
|
|
471
|
+
* real dynamic import; tests inject a controlled importer to exercise the
|
|
472
|
+
* dependency-missing path without uninstalling the package. Throws
|
|
473
|
+
* WhatsAppDependencyError when the package is absent; re-throws any other
|
|
474
|
+
* import failure unchanged.
|
|
475
|
+
*/
|
|
476
|
+
async function resolveBaileys(importer) {
|
|
477
|
+
try {
|
|
478
|
+
return await importer();
|
|
479
|
+
} catch (err) {
|
|
480
|
+
if (isModuleNotFound(err)) throw new WhatsAppDependencyError(err);
|
|
481
|
+
throw err;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Lazily resolve @whiskeysockets/baileys. Caches the in-flight promise so
|
|
486
|
+
* concurrent first-callers share one import. On failure the cache is cleared so
|
|
487
|
+
* a later call can retry.
|
|
488
|
+
*/
|
|
489
|
+
async function getBaileys() {
|
|
490
|
+
if (cached) return cached;
|
|
491
|
+
cached = resolveBaileys(() => import("@whiskeysockets/baileys")).catch((err) => {
|
|
492
|
+
cached = null;
|
|
493
|
+
throw err;
|
|
494
|
+
});
|
|
495
|
+
return cached;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
//#endregion
|
|
499
|
+
//#region src/qr-image.ts
|
|
500
|
+
const QRCode = QRCodeModule;
|
|
501
|
+
const QRErrorCorrectLevel = QRErrorCorrectLevelModule;
|
|
502
|
+
function createQrMatrix(input) {
|
|
503
|
+
const qr = new QRCode(-1, QRErrorCorrectLevel.L);
|
|
504
|
+
qr.addData(input);
|
|
505
|
+
qr.make();
|
|
506
|
+
return qr;
|
|
507
|
+
}
|
|
508
|
+
async function renderQrPngBase64(input, opts = {}) {
|
|
509
|
+
const { scale = 6, marginModules = 4 } = opts;
|
|
510
|
+
const qr = createQrMatrix(input);
|
|
511
|
+
const modules = qr.getModuleCount();
|
|
512
|
+
const size = (modules + marginModules * 2) * scale;
|
|
513
|
+
const buf = Buffer.alloc(size * size * 4, 255);
|
|
514
|
+
for (let row = 0; row < modules; row += 1) for (let col = 0; col < modules; col += 1) {
|
|
515
|
+
if (!qr.isDark(row, col)) continue;
|
|
516
|
+
const startX = (col + marginModules) * scale;
|
|
517
|
+
const startY = (row + marginModules) * scale;
|
|
518
|
+
for (let y = 0; y < scale; y += 1) {
|
|
519
|
+
const pixelY = startY + y;
|
|
520
|
+
for (let x = 0; x < scale; x += 1) fillPixel(buf, startX + x, pixelY, size, 0, 0, 0, 255);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return encodePngRgba(buf, size, size).toString("base64");
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
//#endregion
|
|
527
|
+
//#region src/session.ts
|
|
528
|
+
const BAILEYS_VERSION_CACHE_TTL_MS = 1440 * 60 * 1e3;
|
|
529
|
+
const BAILEYS_VERSION_FETCH_TIMEOUT_MS = 3e3;
|
|
530
|
+
const BAILEYS_VERSION_FETCH_BACKOFF_MS = 300 * 1e3;
|
|
531
|
+
const WA_SOCKET_LOCK_OPTIONS = {
|
|
532
|
+
retries: {
|
|
533
|
+
retries: 0,
|
|
534
|
+
factor: 1,
|
|
535
|
+
minTimeout: 100,
|
|
536
|
+
maxTimeout: 100
|
|
537
|
+
},
|
|
538
|
+
stale: 10080 * 60 * 1e3,
|
|
539
|
+
reentrant: false
|
|
540
|
+
};
|
|
541
|
+
const WA_SOCKET_LOCK_RELEASE_KEY = "__openclawWaSocketLockRelease";
|
|
542
|
+
let cachedBaileysVersion = null;
|
|
543
|
+
let inflightBaileysVersion = null;
|
|
544
|
+
let baileysVersionFetchBackoffUntil = 0;
|
|
545
|
+
function fetchBaileysVersionOnce() {
|
|
546
|
+
if (inflightBaileysVersion) return inflightBaileysVersion;
|
|
547
|
+
const controller = new AbortController();
|
|
548
|
+
const timeout = setTimeout(() => controller.abort(), BAILEYS_VERSION_FETCH_TIMEOUT_MS);
|
|
549
|
+
const next = getBaileys().then(({ fetchLatestBaileysVersion }) => fetchLatestBaileysVersion({ signal: controller.signal })).finally(() => {
|
|
550
|
+
clearTimeout(timeout);
|
|
551
|
+
if (inflightBaileysVersion === next) inflightBaileysVersion = null;
|
|
552
|
+
});
|
|
553
|
+
inflightBaileysVersion = next;
|
|
554
|
+
return next;
|
|
555
|
+
}
|
|
556
|
+
function backOffBaileysVersionFetch() {
|
|
557
|
+
baileysVersionFetchBackoffUntil = Date.now() + BAILEYS_VERSION_FETCH_BACKOFF_MS;
|
|
558
|
+
inflightBaileysVersion = null;
|
|
559
|
+
}
|
|
560
|
+
function resolveWaSocketLockPath(authDir) {
|
|
561
|
+
return path.join(authDir, ".socket-owner");
|
|
562
|
+
}
|
|
563
|
+
function shouldAcquireWaSocketLock() {
|
|
564
|
+
return process.env.VITEST !== "true" || process.env.OPENCLAW_TEST_WA_SOCKET_LOCK === "1";
|
|
565
|
+
}
|
|
566
|
+
async function acquireWaSocketLock(authDir) {
|
|
567
|
+
try {
|
|
568
|
+
return await acquireFileLock(resolveWaSocketLockPath(authDir), WA_SOCKET_LOCK_OPTIONS);
|
|
569
|
+
} catch (err) {
|
|
570
|
+
throw new Error(`WhatsApp auth directory is already in use by another openclaw process or socket in this process (${authDir}). Stop the gateway or use the gateway login flow before relinking.`, { cause: err });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
function attachWaSocketLockRelease(sock, lock) {
|
|
574
|
+
let releasePromise = null;
|
|
575
|
+
const release = () => {
|
|
576
|
+
if (!releasePromise) releasePromise = lock.release().catch((err) => {
|
|
577
|
+
releasePromise = null;
|
|
578
|
+
throw err;
|
|
579
|
+
});
|
|
580
|
+
return releasePromise;
|
|
581
|
+
};
|
|
582
|
+
sock[WA_SOCKET_LOCK_RELEASE_KEY] = release;
|
|
583
|
+
}
|
|
584
|
+
async function closeWaSocket(sock) {
|
|
585
|
+
const release = sock[WA_SOCKET_LOCK_RELEASE_KEY];
|
|
586
|
+
try {
|
|
587
|
+
sock.ws?.close();
|
|
588
|
+
} catch {}
|
|
589
|
+
await release?.();
|
|
590
|
+
if (release) delete sock[WA_SOCKET_LOCK_RELEASE_KEY];
|
|
591
|
+
}
|
|
592
|
+
function withBaileysVersionTimeout(promise, timeoutMs) {
|
|
593
|
+
return new Promise((resolve, reject) => {
|
|
594
|
+
const timeout = setTimeout(() => resolve(null), timeoutMs);
|
|
595
|
+
promise.then((result) => {
|
|
596
|
+
clearTimeout(timeout);
|
|
597
|
+
resolve(result);
|
|
598
|
+
}, (err) => {
|
|
599
|
+
clearTimeout(timeout);
|
|
600
|
+
reject(err);
|
|
601
|
+
});
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
async function resolveBaileysVersion(logger) {
|
|
605
|
+
const { DEFAULT_CONNECTION_CONFIG } = await getBaileys();
|
|
606
|
+
const now = Date.now();
|
|
607
|
+
if (cachedBaileysVersion && now - cachedBaileysVersion.fetchedAt < BAILEYS_VERSION_CACHE_TTL_MS) return cachedBaileysVersion.version;
|
|
608
|
+
if (now < baileysVersionFetchBackoffUntil) return cachedBaileysVersion?.version ?? DEFAULT_CONNECTION_CONFIG.version;
|
|
609
|
+
try {
|
|
610
|
+
const result = await withBaileysVersionTimeout(fetchBaileysVersionOnce(), BAILEYS_VERSION_FETCH_TIMEOUT_MS);
|
|
611
|
+
if (!result) {
|
|
612
|
+
logger.warn("timed out fetching latest WhatsApp Web version");
|
|
613
|
+
backOffBaileysVersionFetch();
|
|
614
|
+
return cachedBaileysVersion?.version ?? DEFAULT_CONNECTION_CONFIG.version;
|
|
615
|
+
}
|
|
616
|
+
if (result.error) {
|
|
617
|
+
logger.warn({ error: safeStringify(result.error) }, "failed fetching latest WhatsApp Web version");
|
|
618
|
+
backOffBaileysVersionFetch();
|
|
619
|
+
} else {
|
|
620
|
+
cachedBaileysVersion = {
|
|
621
|
+
version: result.version,
|
|
622
|
+
fetchedAt: now
|
|
623
|
+
};
|
|
624
|
+
baileysVersionFetchBackoffUntil = 0;
|
|
625
|
+
return result.version;
|
|
626
|
+
}
|
|
627
|
+
} catch (err) {
|
|
628
|
+
logger.warn({ error: String(err) }, "failed fetching latest WhatsApp Web version");
|
|
629
|
+
backOffBaileysVersionFetch();
|
|
630
|
+
}
|
|
631
|
+
return cachedBaileysVersion?.version ?? DEFAULT_CONNECTION_CONFIG.version;
|
|
632
|
+
}
|
|
633
|
+
async function createWaSocket(printQr, verbose, opts = {}) {
|
|
634
|
+
const noop = (..._args) => {};
|
|
635
|
+
const noopLogger = {
|
|
636
|
+
level: "silent",
|
|
637
|
+
child: () => noopLogger,
|
|
638
|
+
trace: noop,
|
|
639
|
+
debug: noop,
|
|
640
|
+
info: noop,
|
|
641
|
+
warn: noop,
|
|
642
|
+
error: noop,
|
|
643
|
+
fatal: noop
|
|
644
|
+
};
|
|
645
|
+
const logger = verbose ? toPinoLikeLogger(getChildLogger({ module: "baileys" }, { level: "info" }), "info") : noopLogger;
|
|
646
|
+
const authDir = resolveUserPath(opts.authDir ?? resolveDefaultWebAuthDir());
|
|
647
|
+
await ensureDir(authDir);
|
|
648
|
+
const sessionLogger = getChildLogger({ module: "web-session" });
|
|
649
|
+
const socketLock = shouldAcquireWaSocketLock() ? await acquireWaSocketLock(authDir) : null;
|
|
650
|
+
let sock;
|
|
651
|
+
try {
|
|
652
|
+
const { useMultiFileAuthState, makeWASocket, makeCacheableSignalKeyStore } = await getBaileys();
|
|
653
|
+
clearWebAuthClearSuppression(authDir);
|
|
654
|
+
maybeRestoreCredsFromBackup(authDir);
|
|
655
|
+
const { state, saveCreds } = await useMultiFileAuthState(authDir);
|
|
656
|
+
const version = await resolveBaileysVersion(sessionLogger);
|
|
657
|
+
sock = makeWASocket({
|
|
658
|
+
auth: {
|
|
659
|
+
creds: state.creds,
|
|
660
|
+
keys: makeCacheableSignalKeyStore(state.keys, logger)
|
|
661
|
+
},
|
|
662
|
+
version,
|
|
663
|
+
logger,
|
|
664
|
+
printQRInTerminal: false,
|
|
665
|
+
browser: [
|
|
666
|
+
"openclaw",
|
|
667
|
+
"cli",
|
|
668
|
+
VERSION
|
|
669
|
+
],
|
|
670
|
+
syncFullHistory: false,
|
|
671
|
+
markOnlineOnConnect: false
|
|
672
|
+
});
|
|
673
|
+
sock.ev.on("creds.update", () => enqueueWebCredsSave(authDir, saveCreds, sessionLogger));
|
|
674
|
+
} catch (err) {
|
|
675
|
+
await socketLock?.release();
|
|
676
|
+
throw err;
|
|
677
|
+
}
|
|
678
|
+
if (socketLock) attachWaSocketLockRelease(sock, socketLock);
|
|
679
|
+
const attachedWebSockets = /* @__PURE__ */ new WeakSet();
|
|
680
|
+
const attachWebSocketErrorHandler = () => {
|
|
681
|
+
const ws = sock.ws;
|
|
682
|
+
if (!ws || typeof ws !== "object" || attachedWebSockets.has(ws)) return;
|
|
683
|
+
const on = ws.on;
|
|
684
|
+
if (typeof on !== "function") return;
|
|
685
|
+
on.call(ws, "error", (err) => {
|
|
686
|
+
sessionLogger.error({ error: String(err) }, "WebSocket error");
|
|
687
|
+
});
|
|
688
|
+
attachedWebSockets.add(ws);
|
|
689
|
+
};
|
|
690
|
+
const { DisconnectReason } = await getBaileys();
|
|
691
|
+
sock.ev.on("connection.update", (update) => {
|
|
692
|
+
try {
|
|
693
|
+
const { connection, lastDisconnect, qr } = update;
|
|
694
|
+
attachWebSocketErrorHandler();
|
|
695
|
+
if (qr) {
|
|
696
|
+
opts.onQr?.(qr);
|
|
697
|
+
if (printQr) {
|
|
698
|
+
sessionLogger.info("Scan this QR in WhatsApp (Linked Devices):");
|
|
699
|
+
qrcode.generate(qr, { small: true });
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (connection === "close") {
|
|
703
|
+
if (getStatusCode(lastDisconnect?.error) === DisconnectReason.loggedOut) sessionLogger.error(danger(`WhatsApp session logged out. Run: ${formatCliCommand("openclaw channels login")}`));
|
|
704
|
+
}
|
|
705
|
+
if (connection === "open" && verbose) sessionLogger.info(success("WhatsApp Web connected."));
|
|
706
|
+
} catch (err) {
|
|
707
|
+
sessionLogger.error({ error: String(err) }, "connection.update handler error");
|
|
708
|
+
}
|
|
709
|
+
});
|
|
710
|
+
attachWebSocketErrorHandler();
|
|
711
|
+
return sock;
|
|
712
|
+
}
|
|
713
|
+
async function waitForWaConnection(sock) {
|
|
714
|
+
return new Promise((resolve, reject) => {
|
|
715
|
+
const evWithOff = sock.ev;
|
|
716
|
+
const handler = (...args) => {
|
|
717
|
+
const update = args[0] ?? {};
|
|
718
|
+
if (update.connection === "open") {
|
|
719
|
+
removeHandler();
|
|
720
|
+
resolve();
|
|
721
|
+
}
|
|
722
|
+
if (update.connection === "close") {
|
|
723
|
+
removeHandler();
|
|
724
|
+
reject(update.lastDisconnect ?? /* @__PURE__ */ new Error("Connection closed"));
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
function removeHandler() {
|
|
728
|
+
if (typeof evWithOff.off === "function") {
|
|
729
|
+
evWithOff.off("connection.update", handler);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
evWithOff.removeListener?.("connection.update", handler);
|
|
733
|
+
}
|
|
734
|
+
sock.ev.on("connection.update", handler);
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
function getStatusCode(err) {
|
|
738
|
+
const nested = err;
|
|
739
|
+
return nested.output?.statusCode ?? nested.status ?? nested.error?.output?.statusCode ?? nested.error?.status ?? nested.lastDisconnect?.error?.output?.statusCode ?? nested.lastDisconnect?.error?.status;
|
|
740
|
+
}
|
|
741
|
+
function safeStringify(value, limit = 800) {
|
|
742
|
+
try {
|
|
743
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
744
|
+
const raw = JSON.stringify(value, (_key, v) => {
|
|
745
|
+
if (typeof v === "bigint") return v.toString();
|
|
746
|
+
if (typeof v === "function") {
|
|
747
|
+
const maybeName = v.name;
|
|
748
|
+
return `[Function ${typeof maybeName === "string" && maybeName.length > 0 ? maybeName : "anonymous"}]`;
|
|
749
|
+
}
|
|
750
|
+
if (typeof v === "object" && v) {
|
|
751
|
+
if (seen.has(v)) return "[Circular]";
|
|
752
|
+
seen.add(v);
|
|
753
|
+
}
|
|
754
|
+
return v;
|
|
755
|
+
}, 2);
|
|
756
|
+
if (!raw) return String(value);
|
|
757
|
+
return raw.length > limit ? `${raw.slice(0, limit)}...` : raw;
|
|
758
|
+
} catch {
|
|
759
|
+
return String(value);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
function extractBoomDetails(err) {
|
|
763
|
+
if (!err || typeof err !== "object") return null;
|
|
764
|
+
const output = err?.output;
|
|
765
|
+
if (!output || typeof output !== "object") return null;
|
|
766
|
+
const payload = output.payload;
|
|
767
|
+
const statusCode = typeof output.statusCode === "number" ? output.statusCode : typeof payload?.statusCode === "number" ? payload.statusCode : void 0;
|
|
768
|
+
const error = typeof payload?.error === "string" ? payload.error : void 0;
|
|
769
|
+
const message = typeof payload?.message === "string" ? payload.message : void 0;
|
|
770
|
+
if (!statusCode && !error && !message) return null;
|
|
771
|
+
return {
|
|
772
|
+
statusCode,
|
|
773
|
+
error,
|
|
774
|
+
message
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
function formatError(err) {
|
|
778
|
+
if (err instanceof Error) return err.message;
|
|
779
|
+
if (typeof err === "string") return err;
|
|
780
|
+
if (!err || typeof err !== "object") return String(err);
|
|
781
|
+
const boom = extractBoomDetails(err) ?? extractBoomDetails(err?.error) ?? extractBoomDetails(err?.lastDisconnect?.error);
|
|
782
|
+
const status = boom?.statusCode ?? getStatusCode(err);
|
|
783
|
+
const code = err?.code;
|
|
784
|
+
const codeText = typeof code === "string" || typeof code === "number" ? String(code) : void 0;
|
|
785
|
+
const message = [
|
|
786
|
+
boom?.message,
|
|
787
|
+
typeof err?.message === "string" ? err.message : void 0,
|
|
788
|
+
typeof err?.error?.message === "string" ? err.error?.message : void 0
|
|
789
|
+
].filter((v) => Boolean(v && v.trim().length > 0))[0];
|
|
790
|
+
const pieces = [];
|
|
791
|
+
if (typeof status === "number") pieces.push(`status=${status}`);
|
|
792
|
+
if (boom?.error) pieces.push(boom.error);
|
|
793
|
+
if (message) pieces.push(message);
|
|
794
|
+
if (codeText) pieces.push(`code=${codeText}`);
|
|
795
|
+
if (pieces.length > 0) return pieces.join(" ");
|
|
796
|
+
return safeStringify(err);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
//#endregion
|
|
800
|
+
//#region src/login-qr.ts
|
|
801
|
+
var login_qr_exports = /* @__PURE__ */ __exportAll({
|
|
802
|
+
canonicalWhatsAppAuthDir: () => canonicalWhatsAppAuthDir,
|
|
803
|
+
closeActiveWebLoginsForAuthDir: () => closeActiveWebLoginsForAuthDir,
|
|
804
|
+
startWebLoginWithQr: () => startWebLoginWithQr,
|
|
805
|
+
tryCanonicalWhatsAppAuthDir: () => tryCanonicalWhatsAppAuthDir,
|
|
806
|
+
waitForWebLogin: () => waitForWebLogin
|
|
807
|
+
});
|
|
808
|
+
const ACTIVE_LOGIN_TTL_MS = 3 * 6e4;
|
|
809
|
+
const activeLogins = /* @__PURE__ */ new Map();
|
|
810
|
+
function loadRuntimeConfig(cfg) {
|
|
811
|
+
return cfg ?? getWhatsAppRuntime().config.loadConfig();
|
|
812
|
+
}
|
|
813
|
+
async function closeSocket(sock) {
|
|
814
|
+
try {
|
|
815
|
+
await closeWaSocket(sock);
|
|
816
|
+
} catch {
|
|
817
|
+
await closeWaSocket(sock);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
async function resetActiveLogin(accountId, reason) {
|
|
821
|
+
const login = activeLogins.get(accountId);
|
|
822
|
+
if (login) try {
|
|
823
|
+
await closeSocket(login.sock);
|
|
824
|
+
} finally {
|
|
825
|
+
activeLogins.delete(accountId);
|
|
826
|
+
}
|
|
827
|
+
if (reason) logInfo(reason);
|
|
828
|
+
}
|
|
829
|
+
function canonicalWhatsAppAuthDir(authDir) {
|
|
830
|
+
const resolvedAuthDir = resolveUserPath(authDir);
|
|
831
|
+
try {
|
|
832
|
+
return fs.realpathSync.native(resolvedAuthDir);
|
|
833
|
+
} catch (err) {
|
|
834
|
+
if (err.code === "ENOENT") return resolvedAuthDir;
|
|
835
|
+
throw err;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
function tryCanonicalWhatsAppAuthDir(authDir) {
|
|
839
|
+
try {
|
|
840
|
+
return canonicalWhatsAppAuthDir(authDir);
|
|
841
|
+
} catch {
|
|
842
|
+
return null;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
async function closeActiveWebLoginsForAuthDir(authDir) {
|
|
846
|
+
const resolvedAuthDir = tryCanonicalWhatsAppAuthDir(authDir);
|
|
847
|
+
if (!resolvedAuthDir) return 0;
|
|
848
|
+
const accountIds = [...activeLogins.values()].filter((login) => tryCanonicalWhatsAppAuthDir(login.authDir) === resolvedAuthDir).map((login) => login.accountId);
|
|
849
|
+
await Promise.all(accountIds.map((accountId) => resetActiveLogin(accountId)));
|
|
850
|
+
return accountIds.length;
|
|
851
|
+
}
|
|
852
|
+
function isLoginFresh(login) {
|
|
853
|
+
return Date.now() - login.startedAt < ACTIVE_LOGIN_TTL_MS;
|
|
854
|
+
}
|
|
855
|
+
function findActiveGatewayAccountForAuthDir(params) {
|
|
856
|
+
const resolvedAuthDir = tryCanonicalWhatsAppAuthDir(params.authDir);
|
|
857
|
+
if (resolvedAuthDir === null) return null;
|
|
858
|
+
const candidateAccountIds = new Set([params.accountId, ...listWhatsAppAccountIds(params.cfg)]);
|
|
859
|
+
for (const accountId of candidateAccountIds) {
|
|
860
|
+
if (tryCanonicalWhatsAppAuthDir(resolveWhatsAppAccount$1({
|
|
861
|
+
cfg: params.cfg,
|
|
862
|
+
accountId
|
|
863
|
+
}).authDir) !== resolvedAuthDir) continue;
|
|
864
|
+
if (getActiveWebListener(accountId)) return accountId;
|
|
865
|
+
}
|
|
866
|
+
return null;
|
|
867
|
+
}
|
|
868
|
+
function attachLoginWaiter(accountId, login) {
|
|
869
|
+
login.waitPromise = waitForWaConnection(login.sock).then(() => {
|
|
870
|
+
const current = activeLogins.get(accountId);
|
|
871
|
+
if (current?.id === login.id) current.connected = true;
|
|
872
|
+
}).catch((err) => {
|
|
873
|
+
const current = activeLogins.get(accountId);
|
|
874
|
+
if (current?.id !== login.id) return;
|
|
875
|
+
current.error = formatError(err);
|
|
876
|
+
current.errorStatus = getStatusCode(err);
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
async function restartLoginSocket(login, runtime) {
|
|
880
|
+
if (login.restartAttempted) return false;
|
|
881
|
+
login.restartAttempted = true;
|
|
882
|
+
runtime.log(info("WhatsApp asked for a restart after pairing (code 515); retrying connection once..."));
|
|
883
|
+
await closeSocket(login.sock);
|
|
884
|
+
try {
|
|
885
|
+
login.sock = await createWaSocket(false, login.verbose, { authDir: login.authDir });
|
|
886
|
+
login.connected = false;
|
|
887
|
+
login.error = void 0;
|
|
888
|
+
login.errorStatus = void 0;
|
|
889
|
+
attachLoginWaiter(login.accountId, login);
|
|
890
|
+
return true;
|
|
891
|
+
} catch (err) {
|
|
892
|
+
login.error = formatError(err);
|
|
893
|
+
login.errorStatus = getStatusCode(err);
|
|
894
|
+
return false;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
async function startWebLoginWithQr(opts = {}) {
|
|
898
|
+
const runtime = opts.runtime ?? defaultRuntime;
|
|
899
|
+
const cfg = loadRuntimeConfig(opts.cfg);
|
|
900
|
+
const account = resolveWhatsAppAccount$1({
|
|
901
|
+
cfg,
|
|
902
|
+
accountId: opts.accountId
|
|
903
|
+
});
|
|
904
|
+
const hasWeb = await webAuthExists(account.authDir);
|
|
905
|
+
const selfId = readWebSelfId(account.authDir);
|
|
906
|
+
if (hasWeb && !opts.force) return { message: `WhatsApp is already linked (${selfId.e164 ?? selfId.jid ?? "unknown"}). Say "relink" if you want a fresh QR.` };
|
|
907
|
+
if (tryCanonicalWhatsAppAuthDir(account.authDir) === null) return { message: `Cannot start QR login: WhatsApp auth directory ${account.authDir} is not accessible (broken symlink or permission error). Fix the directory before relinking.` };
|
|
908
|
+
const activeGatewayAccount = findActiveGatewayAccountForAuthDir({
|
|
909
|
+
cfg,
|
|
910
|
+
accountId: account.accountId,
|
|
911
|
+
authDir: account.authDir
|
|
912
|
+
});
|
|
913
|
+
if (activeGatewayAccount) return { message: activeGatewayAccount === account.accountId ? "WhatsApp gateway is running for this account. Stop the channel or use the gateway login flow before relinking." : `WhatsApp gateway is running for account ${activeGatewayAccount} using this auth directory. Stop that channel or use the gateway login flow before relinking.` };
|
|
914
|
+
const existing = activeLogins.get(account.accountId);
|
|
915
|
+
if (existing && isLoginFresh(existing) && existing.qrDataUrl) return {
|
|
916
|
+
qrDataUrl: existing.qrDataUrl,
|
|
917
|
+
message: "QR already active. Scan it in WhatsApp -> Linked Devices."
|
|
918
|
+
};
|
|
919
|
+
await resetActiveLogin(account.accountId);
|
|
920
|
+
let resolveQr = null;
|
|
921
|
+
let rejectQr = null;
|
|
922
|
+
const qrPromise = new Promise((resolve, reject) => {
|
|
923
|
+
resolveQr = resolve;
|
|
924
|
+
rejectQr = reject;
|
|
925
|
+
});
|
|
926
|
+
const qrTimer = setTimeout(() => {
|
|
927
|
+
rejectQr?.(/* @__PURE__ */ new Error("Timed out waiting for WhatsApp QR"));
|
|
928
|
+
}, Math.max(opts.timeoutMs ?? 3e4, 5e3));
|
|
929
|
+
let sock;
|
|
930
|
+
let pendingQr = null;
|
|
931
|
+
try {
|
|
932
|
+
sock = await createWaSocket(false, Boolean(opts.verbose), {
|
|
933
|
+
authDir: account.authDir,
|
|
934
|
+
onQr: (qr) => {
|
|
935
|
+
if (pendingQr) return;
|
|
936
|
+
pendingQr = qr;
|
|
937
|
+
const current = activeLogins.get(account.accountId);
|
|
938
|
+
if (current && !current.qr) current.qr = qr;
|
|
939
|
+
clearTimeout(qrTimer);
|
|
940
|
+
runtime.log(info("WhatsApp QR received."));
|
|
941
|
+
resolveQr?.(qr);
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
} catch (err) {
|
|
945
|
+
clearTimeout(qrTimer);
|
|
946
|
+
await resetActiveLogin(account.accountId);
|
|
947
|
+
return { message: `Failed to start WhatsApp login: ${String(err)}` };
|
|
948
|
+
}
|
|
949
|
+
const login = {
|
|
950
|
+
accountId: account.accountId,
|
|
951
|
+
authDir: account.authDir,
|
|
952
|
+
isLegacyAuthDir: account.isLegacyAuthDir,
|
|
953
|
+
id: randomUUID(),
|
|
954
|
+
sock,
|
|
955
|
+
startedAt: Date.now(),
|
|
956
|
+
connected: false,
|
|
957
|
+
waitPromise: Promise.resolve(),
|
|
958
|
+
restartAttempted: false,
|
|
959
|
+
verbose: Boolean(opts.verbose)
|
|
960
|
+
};
|
|
961
|
+
activeLogins.set(account.accountId, login);
|
|
962
|
+
if (pendingQr && !login.qr) login.qr = pendingQr;
|
|
963
|
+
attachLoginWaiter(account.accountId, login);
|
|
964
|
+
let qr;
|
|
965
|
+
try {
|
|
966
|
+
qr = await qrPromise;
|
|
967
|
+
} catch (err) {
|
|
968
|
+
clearTimeout(qrTimer);
|
|
969
|
+
await resetActiveLogin(account.accountId);
|
|
970
|
+
return { message: `Failed to get QR: ${String(err)}` };
|
|
971
|
+
}
|
|
972
|
+
login.qrDataUrl = `data:image/png;base64,${await renderQrPngBase64(qr)}`;
|
|
973
|
+
return {
|
|
974
|
+
qrDataUrl: login.qrDataUrl,
|
|
975
|
+
message: "Scan this QR in WhatsApp -> Linked Devices."
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
async function waitForWebLogin(opts = {}) {
|
|
979
|
+
const { DisconnectReason } = await getBaileys();
|
|
980
|
+
const runtime = opts.runtime ?? defaultRuntime;
|
|
981
|
+
const account = resolveWhatsAppAccount$1({
|
|
982
|
+
cfg: loadRuntimeConfig(opts.cfg),
|
|
983
|
+
accountId: opts.accountId
|
|
984
|
+
});
|
|
985
|
+
const activeLogin = activeLogins.get(account.accountId);
|
|
986
|
+
if (!activeLogin) return {
|
|
987
|
+
connected: false,
|
|
988
|
+
message: "No active WhatsApp login in progress."
|
|
989
|
+
};
|
|
990
|
+
const login = activeLogin;
|
|
991
|
+
if (!isLoginFresh(login)) {
|
|
992
|
+
await resetActiveLogin(account.accountId);
|
|
993
|
+
return {
|
|
994
|
+
connected: false,
|
|
995
|
+
message: "The login QR expired. Ask me to generate a new one."
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
const timeoutMs = Math.max(opts.timeoutMs ?? 12e4, 1e3);
|
|
999
|
+
const deadline = Date.now() + timeoutMs;
|
|
1000
|
+
while (true) {
|
|
1001
|
+
const remaining = deadline - Date.now();
|
|
1002
|
+
if (remaining <= 0) return {
|
|
1003
|
+
connected: false,
|
|
1004
|
+
message: "Still waiting for the QR scan. Let me know when you've scanned it."
|
|
1005
|
+
};
|
|
1006
|
+
let timeoutHandle;
|
|
1007
|
+
const timeout = new Promise((resolve) => {
|
|
1008
|
+
timeoutHandle = setTimeout(() => resolve("timeout"), remaining);
|
|
1009
|
+
});
|
|
1010
|
+
let result;
|
|
1011
|
+
try {
|
|
1012
|
+
result = await Promise.race([login.waitPromise.then(() => "done"), timeout]);
|
|
1013
|
+
} finally {
|
|
1014
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
1015
|
+
}
|
|
1016
|
+
if (result === "timeout") return {
|
|
1017
|
+
connected: false,
|
|
1018
|
+
message: "Still waiting for the QR scan. Let me know when you've scanned it."
|
|
1019
|
+
};
|
|
1020
|
+
if (login.error) {
|
|
1021
|
+
if (login.errorStatus === DisconnectReason.loggedOut) {
|
|
1022
|
+
await closeActiveWebLoginsForAuthDir(login.authDir);
|
|
1023
|
+
await logoutWeb({
|
|
1024
|
+
authDir: login.authDir,
|
|
1025
|
+
isLegacyAuthDir: login.isLegacyAuthDir,
|
|
1026
|
+
runtime
|
|
1027
|
+
});
|
|
1028
|
+
const message = "WhatsApp reported the session is logged out. Cleared cached web session; please scan a new QR.";
|
|
1029
|
+
logInfo(message);
|
|
1030
|
+
runtime.log(danger(message));
|
|
1031
|
+
return {
|
|
1032
|
+
connected: false,
|
|
1033
|
+
message
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
if (login.errorStatus === 515) {
|
|
1037
|
+
if (await restartLoginSocket(login, runtime) && isLoginFresh(login)) continue;
|
|
1038
|
+
}
|
|
1039
|
+
const message = `WhatsApp login failed: ${login.error}`;
|
|
1040
|
+
await resetActiveLogin(account.accountId, message);
|
|
1041
|
+
runtime.log(danger(message));
|
|
1042
|
+
return {
|
|
1043
|
+
connected: false,
|
|
1044
|
+
message
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
if (login.connected) {
|
|
1048
|
+
const message = "Linked! WhatsApp is ready.";
|
|
1049
|
+
runtime.log(success(message));
|
|
1050
|
+
await resetActiveLogin(account.accountId);
|
|
1051
|
+
return {
|
|
1052
|
+
connected: true,
|
|
1053
|
+
message
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
return {
|
|
1057
|
+
connected: false,
|
|
1058
|
+
message: "Login ended without a connection."
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
//#endregion
|
|
1064
|
+
export { listWhatsAppAccountIds as C, resolveWhatsAppAuthDir as E, stopActiveWebListener as S, resolveWhatsAppAccount$1 as T, webAuthExists as _, waitForWebLogin as a, getActiveWebListener as b, formatError as c, getBaileys as d, getWebAuthAgeMs as f, waitForWebCredsSaveQueue as g, readWebSelfId as h, tryCanonicalWhatsAppAuthDir as i, getStatusCode as l, logoutWeb as m, login_qr_exports as n, closeWaSocket as o, logWebSelfId as p, startWebLoginWithQr as r, createWaSocket as s, closeActiveWebLoginsForAuthDir as t, waitForWaConnection as u, getWhatsAppRuntime as v, resolveDefaultWhatsAppAccountId as w, setActiveWebListener as x, setWhatsAppRuntime as y };
|