Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 73 additions & 23 deletions src/cli/commands/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,11 @@ function authenticateWhatsApp(): Promise<void> {
rejectPromise = reject;
});

const closeSock = (s: unknown) => {
const closeSock = (s: unknown, removeListeners = false) => {
try {
(s as { ws?: { close: () => void } })?.ws?.close();
const socket = s as { ev?: { removeAllListeners: () => void }; ws?: { close: () => void } };
if (removeListeners) socket?.ev?.removeAllListeners();
socket?.ws?.close();
} catch {
// Ignore
}
Expand All @@ -78,6 +80,7 @@ function authenticateWhatsApp(): Promise<void> {

try {
fs.rmSync(WA_AUTH_DIR, { recursive: true, force: true });
await new Promise((r) => setTimeout(r, 1000));
fs.mkdirSync(WA_AUTH_DIR, { recursive: true });
} catch {
console.log(chalk.yellow("Warning: Could not clear old session"));
Expand All @@ -90,12 +93,33 @@ function authenticateWhatsApp(): Promise<void> {
console.log(chalk.gray("Initializing WhatsApp connection..."));
console.log();

const { state, saveCreds } = await useMultiFileAuthState(WA_AUTH_DIR);
const { state, saveCreds: _saveCreds } = await useMultiFileAuthState(WA_AUTH_DIR);
const saveCreds = async () => {
const credsPath = path.join(WA_AUTH_DIR, "creds.json");
const tmpPath = credsPath + "." + Date.now() + ".tmp";
for (let attempt = 0; attempt < 10; attempt++) {
try {
fs.writeFileSync(tmpPath, JSON.stringify(state.creds, null, 2));
try {
fs.unlinkSync(credsPath);
} catch {}
fs.renameSync(tmpPath, credsPath);
return;
} catch {
try {
fs.unlinkSync(tmpPath);
} catch {}
if (attempt < 9) await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
}
}
// Last resort: try the original saveCreds
await _saveCreds();
};
const { version } = await fetchLatestBaileysVersion();

connectionTimeout = setTimeout(() => {
if (!pairingComplete && sock) {
closeSock(sock);
closeSock(sock, true);
if (!connectionAttempted) {
rejectPromise(
new Error(
Expand Down Expand Up @@ -142,7 +166,7 @@ function authenticateWhatsApp(): Promise<void> {

connectionTimeout = setTimeout(() => {
if (!pairingComplete) {
closeSock(sock);
closeSock(sock, true);
rejectPromise(new Error("QR code scan timeout - Please try again"));
}
}, 120000);
Expand All @@ -154,7 +178,7 @@ function authenticateWhatsApp(): Promise<void> {
console.log(chalk.green("\n[OK] WhatsApp authenticated successfully!"));

setTimeout(() => {
closeSock(sock);
closeSock(sock, true);
resolvePromise();
}, 500);
}
Expand All @@ -166,7 +190,7 @@ function authenticateWhatsApp(): Promise<void> {
const errorMessage = lastDisconnect?.error?.message || "Unknown error";

if (!hasShownQR) {
closeSock(sock);
closeSock(sock, true);

if (statusCode === 405) {
rejectPromise(
Expand All @@ -189,53 +213,79 @@ function authenticateWhatsApp(): Promise<void> {
chalk.cyan("\n[INFO] WhatsApp pairing complete, restarting connection...\n"),
);

closeSock(sock);

await new Promise((r) => setTimeout(r, 2000));
// 515 = stream replaced, normal after QR pairing.
// Close websocket but DON'T remove listeners yet (let creds.update finish saving)
closeSock(sock, false);
await new Promise((r) => setTimeout(r, 3000));

const { state: newState, saveCreds: newSaveCreds } =
await useMultiFileAuthState(WA_AUTH_DIR);
// Now remove old listeners and reconnect using the SAME in-memory state
// (don't re-read from disk — cached keys may not be flushed yet)
closeSock(sock, true);
const retryVersion = await fetchLatestBaileysVersion();
const retrySock = makeWASocket({
auth: {
creds: newState.creds,
keys: makeCacheableSignalKeyStore(newState.keys, silentLogger),
creds: state.creds,
keys: makeCacheableSignalKeyStore(state.keys, silentLogger),
},
version: retryVersion.version,
printQRInTerminal: false,
browser: ["txtcode", "CLI", "0.1.0"],
browser: ["TxtCode", "CLI", "1.0.0"],
syncFullHistory: false,
markOnlineOnConnect: false,
logger: silentLogger,
});

retrySock.ev.on("creds.update", newSaveCreds);
retrySock.ev.on("creds.update", saveCreds);

const retryTimeout = setTimeout(() => {
if (!pairingComplete) {
closeSock(retrySock, true);
rejectPromise(
new Error("WhatsApp linking timed out after restart. Please try again."),
);
}
}, 30000);

retrySock.ev.on("connection.update", (retryUpdate) => {
if (retryUpdate.connection === "open") {
clearTimeout(retryTimeout);
pairingComplete = true;
console.log(chalk.green("[OK] WhatsApp linked successfully!\n"));

setTimeout(() => {
closeSock(retrySock);
closeSock(retrySock, true);
resolvePromise();
}, 500);
}, 1000);
}

if (retryUpdate.connection === "close") {
closeSock(retrySock);
rejectPromise(new Error("WhatsApp authentication failed after restart"));
if (retryUpdate.connection === "close" && !pairingComplete) {
const retryStatusCode = (
retryUpdate as {
lastDisconnect?: { error?: { output?: { statusCode?: number } } };
}
)?.lastDisconnect?.error?.output?.statusCode;
// If we get another 515, keep retrying
if (retryStatusCode === 515) {
return;
}
clearTimeout(retryTimeout);
closeSock(retrySock, true);
rejectPromise(
new Error(
`WhatsApp authentication failed after restart (code: ${retryStatusCode})`,
),
);
}
});
} else if (hasShownQR && statusCode !== 515) {
closeSock(sock);
closeSock(sock, true);
rejectPromise(new Error(`WhatsApp authentication failed (code: ${statusCode})`));
}
}
});
} catch (error) {
if (sock) {
closeSock(sock);
closeSock(sock, true);
}
rejectPromise(error instanceof Error ? error : new Error(String(error)));
}
Expand Down
70 changes: 50 additions & 20 deletions src/cli/commands/reset.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { execSync } from "child_process";
import fs from "fs";
import os from "os";
import path from "path";
Expand Down Expand Up @@ -53,7 +54,7 @@ export async function resetCommand() {
export async function logoutCommand() {
try {
if (fs.existsSync(WA_AUTH_DIR)) {
fs.rmSync(WA_AUTH_DIR, { recursive: true, force: true });
forceRemove(WA_AUTH_DIR);
console.log();
console.log(chalk.green(" ✅ WhatsApp session deleted!"));
console.log(chalk.cyan(" Run start to scan QR code again."));
Expand All @@ -65,11 +66,36 @@ export async function logoutCommand() {
}
} catch {
console.log();
console.log(chalk.red(" ❌ Failed to delete session."));
console.log(
chalk.red(
" ❌ Failed to delete session. Close any running txtcode processes and try again.",
),
);
console.log();
}
}

function forceRemove(target: string): void {
// Try Node's rmSync first
try {
fs.rmSync(target, { recursive: true, force: true, maxRetries: 5, retryDelay: 1000 });
return;
} catch {
// Fall through to OS-level delete
}

// Fallback: use OS command which handles locked files better
try {
if (process.platform === "win32") {
execSync(`rmdir /s /q "${target}"`, { stdio: "ignore" });
} else {
execSync(`rm -rf "${target}"`, { stdio: "ignore" });
}
} catch {
throw new Error(`Could not delete ${target}`);
}
}

export async function hardResetCommand() {
console.log();
console.log(chalk.yellow(" ⚠️ HARD RESET – This will delete ALL TxtCode data:"));
Expand All @@ -85,31 +111,35 @@ export async function hardResetCommand() {
});

if (confirmed) {
let deletedItems = 0;

try {
if (fs.existsSync(CONFIG_DIR)) {
fs.rmSync(CONFIG_DIR, { recursive: true, force: true });
console.log();
console.log(chalk.green(" ✓ Deleted configuration directory"));
deletedItems++;
}
} catch {
if (!fs.existsSync(CONFIG_DIR)) {
console.log();
console.log(chalk.red(" ✗ Failed to delete configuration directory"));
console.log(chalk.yellow(" ⚠️ No data found to delete."));
console.log();
return;
}

if (deletedItems > 0) {
console.log();

try {
forceRemove(CONFIG_DIR);
console.log(chalk.green(" ✓ Deleted all TxtCode data (~/.txtcode)"));
console.log();
console.log(chalk.green(` ✅ Hard reset complete! Deleted ${deletedItems} item(s).`));
console.log(chalk.green(" ✅ Hard reset complete!"));
console.log();
console.log(chalk.cyan(" Run authentication to set up again."));
console.log();
} else {
console.log();
console.log(chalk.yellow(" ⚠️ No data found to delete."));
console.log();
} catch {
// Check what's left
if (!fs.existsSync(CONFIG_DIR)) {
console.log(chalk.green(" ✅ Hard reset complete!"));
console.log();
console.log(chalk.cyan(" Run authentication to set up again."));
} else {
console.log(chalk.red(" ✗ Failed to delete configuration directory."));
console.log(chalk.yellow(" Close any running txtcode processes and try again."));
console.log(chalk.gray(` Or manually delete: ${CONFIG_DIR}`));
}
}
console.log();
} else {
console.log();
console.log(chalk.yellow(" ❌ Hard reset cancelled."));
Expand Down
35 changes: 33 additions & 2 deletions src/platforms/whatsapp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Boom } from "@hapi/boom";
import makeWASocket, {
type ConnectionState,
DisconnectReason,
fetchLatestBaileysVersion,
makeCacheableSignalKeyStore,
useMultiFileAuthState,
type WASocket,
WAMessage,
Expand Down Expand Up @@ -63,11 +65,39 @@ export class WhatsAppBot {
process.exit(1);
}

const { state, saveCreds } = await useMultiFileAuthState(WA_AUTH_DIR);
const { state, saveCreds: _saveCreds } = await useMultiFileAuthState(WA_AUTH_DIR);
const saveCreds = async () => {
const credsPath = path.join(WA_AUTH_DIR, "creds.json");
const tmpPath = credsPath + "." + Date.now() + ".tmp";
for (let attempt = 0; attempt < 10; attempt++) {
try {
fs.writeFileSync(tmpPath, JSON.stringify(state.creds, null, 2));
try {
fs.unlinkSync(credsPath);
} catch {}
fs.renameSync(tmpPath, credsPath);
return;
} catch {
try {
fs.unlinkSync(tmpPath);
} catch {}
if (attempt < 9) await new Promise((r) => setTimeout(r, 500 * (attempt + 1)));
}
}
await _saveCreds();
};
const { version } = await fetchLatestBaileysVersion().catch(() => ({
version: undefined as unknown as [number, number, number],
}));

this.sock = makeWASocket({
auth: state,
auth: {
creds: state.creds,
keys: makeCacheableSignalKeyStore(state.keys, silentLogger),
},
version,
printQRInTerminal: false,
browser: ["TxtCode", "CLI", "1.0.0"],
logger: silentLogger,
});

Expand All @@ -77,6 +107,7 @@ export class WhatsAppBot {
if (connection === "open") {
logger.info("WhatsApp connected!");
logger.info("Waiting for messages...");
this.sock.sendPresenceUpdate("available").catch(() => {});
}

if (connection === "close") {
Expand Down
Loading