fix: WhatsApp auth improvements and LID translation for DMs

- Add pairing code auth with 515 reconnect handling (Baileys stream
  error after pairing is now retried instead of failing)
- Use Browsers.macOS('Chrome') identifier for WhatsApp compatibility
- Fix LID-to-phone translation for DMs using signalRepository.getPNForLID
- Strip device suffix (:0) from resolved phone JIDs
- Update setup skill with three auth options (browser QR, pairing code,
  terminal QR), DM channel type, and LID troubleshooting

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Gavriel Cohen
2026-02-12 22:49:04 +02:00
parent 6863c0bf6b
commit acdc6454db
4 changed files with 265 additions and 30 deletions

View File

@@ -3,6 +3,7 @@ import fs from 'fs';
import path from 'path';
import makeWASocket, {
Browsers,
DisconnectReason,
WASocket,
makeCacheableSignalKeyStore,
@@ -62,7 +63,7 @@ export class WhatsAppChannel implements Channel {
},
printQRInTerminal: false,
logger,
browser: ['NanoClaw', 'Chrome', '1.0.0'],
browser: Browsers.macOS('Chrome'),
});
this.sock.ev.on('connection.update', (update) => {
@@ -141,14 +142,14 @@ export class WhatsAppChannel implements Channel {
this.sock.ev.on('creds.update', saveCreds);
this.sock.ev.on('messages.upsert', ({ messages }) => {
this.sock.ev.on('messages.upsert', async ({ messages }) => {
for (const msg of messages) {
if (!msg.message) continue;
const rawJid = msg.key.remoteJid;
if (!rawJid || rawJid === 'status@broadcast') continue;
// Translate LID JID to phone JID if applicable
const chatJid = this.translateJid(rawJid);
const chatJid = await this.translateJid(rawJid);
const timestamp = new Date(
Number(msg.messageTimestamp) * 1000,
@@ -256,14 +257,30 @@ export class WhatsAppChannel implements Channel {
}
}
private translateJid(jid: string): string {
private async translateJid(jid: string): Promise<string> {
if (!jid.endsWith('@lid')) return jid;
const lidUser = jid.split('@')[0].split(':')[0];
const phoneJid = this.lidToPhoneMap[lidUser];
if (phoneJid) {
logger.debug({ lidJid: jid, phoneJid }, 'Translated LID to phone JID');
return phoneJid;
// Check local cache first
const cached = this.lidToPhoneMap[lidUser];
if (cached) {
logger.debug({ lidJid: jid, phoneJid: cached }, 'Translated LID to phone JID (cached)');
return cached;
}
// Query Baileys' signal repository for the mapping
try {
const pn = await this.sock.signalRepository?.lidMapping?.getPNForLID(jid);
if (pn) {
const phoneJid = `${pn.split('@')[0].split(':')[0]}@s.whatsapp.net`;
this.lidToPhoneMap[lidUser] = phoneJid;
logger.info({ lidJid: jid, phoneJid }, 'Translated LID to phone JID (signalRepository)');
return phoneJid;
}
} catch (err) {
logger.debug({ err, jid }, 'Failed to resolve LID via signalRepository');
}
return jid;
}

View File

@@ -10,25 +10,42 @@ import fs from 'fs';
import path from 'path';
import pino from 'pino';
import qrcode from 'qrcode-terminal';
import readline from 'readline';
import makeWASocket, {
Browsers,
DisconnectReason,
makeCacheableSignalKeyStore,
useMultiFileAuthState,
} from '@whiskeysockets/baileys';
const AUTH_DIR = './store/auth';
const QR_FILE = './store/qr-data.txt';
const STATUS_FILE = './store/auth-status.txt';
const logger = pino({
level: 'warn', // Quiet logging - only show errors
});
async function authenticate(): Promise<void> {
fs.mkdirSync(AUTH_DIR, { recursive: true });
// Check for --pairing-code flag and phone number
const usePairingCode = process.argv.includes('--pairing-code');
const phoneArg = process.argv.find((_, i, arr) => arr[i - 1] === '--phone');
function askQuestion(prompt: string): Promise<string> {
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
return new Promise((resolve) => {
rl.question(prompt, (answer) => {
rl.close();
resolve(answer.trim());
});
});
}
async function connectSocket(phoneNumber?: string): Promise<void> {
const { state, saveCreds } = await useMultiFileAuthState(AUTH_DIR);
if (state.creds.registered) {
fs.writeFileSync(STATUS_FILE, 'already_authenticated');
console.log('✓ Already authenticated with WhatsApp');
console.log(
' To re-authenticate, delete the store/auth folder and run again.',
@@ -36,8 +53,6 @@ async function authenticate(): Promise<void> {
process.exit(0);
}
console.log('Starting WhatsApp authentication...\n');
const sock = makeWASocket({
auth: {
creds: state.creds,
@@ -45,13 +60,34 @@ async function authenticate(): Promise<void> {
},
printQRInTerminal: false,
logger,
browser: ['NanoClaw', 'Chrome', '1.0.0'],
browser: Browsers.macOS('Chrome'),
});
if (usePairingCode && phoneNumber && !state.creds.me) {
// Request pairing code after a short delay for connection to initialize
// Only on first connect (not reconnect after 515)
setTimeout(async () => {
try {
const code = await sock.requestPairingCode(phoneNumber!);
console.log(`\n🔗 Your pairing code: ${code}\n`);
console.log(' 1. Open WhatsApp on your phone');
console.log(' 2. Tap Settings → Linked Devices → Link a Device');
console.log(' 3. Tap "Link with phone number instead"');
console.log(` 4. Enter this code: ${code}\n`);
fs.writeFileSync(STATUS_FILE, `pairing_code:${code}`);
} catch (err: any) {
console.error('Failed to request pairing code:', err.message);
process.exit(1);
}
}, 3000);
}
sock.ev.on('connection.update', (update) => {
const { connection, lastDisconnect, qr } = update;
if (qr) {
// Write raw QR data to file so the setup skill can render it
fs.writeFileSync(QR_FILE, qr);
console.log('Scan this QR code with WhatsApp:\n');
console.log(' 1. Open WhatsApp on your phone');
console.log(' 2. Tap Settings → Linked Devices → Link a Device');
@@ -63,15 +99,29 @@ async function authenticate(): Promise<void> {
const reason = (lastDisconnect?.error as any)?.output?.statusCode;
if (reason === DisconnectReason.loggedOut) {
fs.writeFileSync(STATUS_FILE, 'failed:logged_out');
console.log('\n✗ Logged out. Delete store/auth and try again.');
process.exit(1);
} else if (reason === DisconnectReason.timedOut) {
fs.writeFileSync(STATUS_FILE, 'failed:qr_timeout');
console.log('\n✗ QR code timed out. Please try again.');
process.exit(1);
} else if (reason === 515) {
// 515 = stream error, often happens after pairing succeeds but before
// registration completes. Reconnect to finish the handshake.
console.log('\n⟳ Stream error (515) after pairing — reconnecting...');
connectSocket(phoneNumber);
} else {
fs.writeFileSync(STATUS_FILE, `failed:${reason || 'unknown'}`);
console.log('\n✗ Connection failed. Please try again.');
process.exit(1);
}
}
if (connection === 'open') {
fs.writeFileSync(STATUS_FILE, 'authenticated');
// Clean up QR file now that we're connected
try { fs.unlinkSync(QR_FILE); } catch {}
console.log('\n✓ Successfully authenticated with WhatsApp!');
console.log(' Credentials saved to store/auth/');
console.log(' You can now start the NanoClaw service.\n');
@@ -84,6 +134,23 @@ async function authenticate(): Promise<void> {
sock.ev.on('creds.update', saveCreds);
}
async function authenticate(): Promise<void> {
fs.mkdirSync(AUTH_DIR, { recursive: true });
// Clean up any stale QR/status files from previous runs
try { fs.unlinkSync(QR_FILE); } catch {}
try { fs.unlinkSync(STATUS_FILE); } catch {}
let phoneNumber = phoneArg;
if (usePairingCode && !phoneNumber) {
phoneNumber = await askQuestion('Enter your phone number (with country code, no + or spaces, e.g. 14155551234): ');
}
console.log('Starting WhatsApp authentication...\n');
await connectSocket(phoneNumber);
}
authenticate().catch((err) => {
console.error('Authentication failed:', err.message);
process.exit(1);