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:
@@ -5,7 +5,7 @@ description: Run initial NanoClaw setup. Use when user wants to install dependen
|
|||||||
|
|
||||||
# NanoClaw Setup
|
# NanoClaw Setup
|
||||||
|
|
||||||
Run all commands automatically. Only pause when user action is required (scanning QR codes).
|
Run all commands automatically. Only pause when user action is required (WhatsApp authentication, configuration choices).
|
||||||
|
|
||||||
**UX Note:** When asking the user questions, prefer using the `AskUserQuestion` tool instead of just outputting text. This integrates with Claude's built-in question/answer system for a better experience.
|
**UX Note:** When asking the user questions, prefer using the `AskUserQuestion` tool instead of just outputting text. This integrates with Claude's built-in question/answer system for a better experience.
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ Tell the user:
|
|||||||
> 1. Paste it here and I'll add it to `.env` for you, or
|
> 1. Paste it here and I'll add it to `.env` for you, or
|
||||||
> 2. Add it to `.env` yourself as `CLAUDE_CODE_OAUTH_TOKEN=<your-token>`
|
> 2. Add it to `.env` yourself as `CLAUDE_CODE_OAUTH_TOKEN=<your-token>`
|
||||||
|
|
||||||
If they give you the token, add it to `.env`:
|
If they give you the token, add it to `.env`. **Never echo the full token in commands or output** — use the Write tool to write the `.env` file directly, or tell the user to add it themselves:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
echo "CLAUDE_CODE_OAUTH_TOKEN=<token>" > .env
|
echo "CLAUDE_CODE_OAUTH_TOKEN=<token>" > .env
|
||||||
@@ -114,7 +114,7 @@ Tell the user to add their key from https://console.anthropic.com/
|
|||||||
**Verify:**
|
**Verify:**
|
||||||
```bash
|
```bash
|
||||||
KEY=$(grep "^ANTHROPIC_API_KEY=" .env | cut -d= -f2)
|
KEY=$(grep "^ANTHROPIC_API_KEY=" .env | cut -d= -f2)
|
||||||
[ -n "$KEY" ] && echo "API key configured: ${KEY:0:10}...${KEY: -4}" || echo "Missing"
|
[ -n "$KEY" ] && echo "API key configured: ${KEY:0:7}..." || echo "Missing"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4. Build Container Image
|
## 4. Build Container Image
|
||||||
@@ -141,23 +141,131 @@ fi
|
|||||||
|
|
||||||
**USER ACTION REQUIRED**
|
**USER ACTION REQUIRED**
|
||||||
|
|
||||||
**IMPORTANT:** Run this command in the **foreground**. The QR code is multi-line ASCII art that must be displayed in full. Do NOT run in background or truncate the output.
|
The auth script supports two methods: QR code scanning and pairing code (phone number). Ask the user which they prefer.
|
||||||
|
|
||||||
Tell the user:
|
The auth script writes status to `store/auth-status.txt`:
|
||||||
> A QR code will appear below. On your phone:
|
- `already_authenticated` — credentials already exist
|
||||||
> 1. Open WhatsApp
|
- `pairing_code:<CODE>` — pairing code generated, waiting for user to enter it
|
||||||
> 2. Tap **Settings → Linked Devices → Link a Device**
|
- `authenticated` — successfully authenticated
|
||||||
> 3. Scan the QR code
|
- `failed:<reason>` — authentication failed
|
||||||
|
|
||||||
Run with a long Bash tool timeout (120000ms) so the user has time to scan. Do NOT use the `timeout` shell command (it's not available on macOS).
|
The script automatically handles error 515 (stream error after pairing) by reconnecting — this is normal and expected during pairing code auth.
|
||||||
|
|
||||||
|
### Ask the user which method to use
|
||||||
|
|
||||||
|
> How would you like to authenticate WhatsApp?
|
||||||
|
>
|
||||||
|
> 1. **QR code in browser** (Recommended) — Opens a page with the QR code to scan
|
||||||
|
> 2. **Pairing code** — Enter a numeric code on your phone, no camera needed
|
||||||
|
> 3. **QR code in terminal** — Run the auth command yourself in another terminal
|
||||||
|
|
||||||
|
### Option A: QR Code in Browser (Recommended)
|
||||||
|
|
||||||
|
Clean any stale auth state and start auth in background:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
rm -rf store/auth store/qr-data.txt store/auth-status.txt
|
||||||
npm run auth
|
npm run auth
|
||||||
```
|
```
|
||||||
|
|
||||||
Wait for the script to output "Successfully authenticated" then continue.
|
Run this with `run_in_background: true`.
|
||||||
|
|
||||||
If it says "Already authenticated", skip to the next step.
|
Poll for QR data (up to 15 seconds):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for i in $(seq 1 15); do if [ -f store/qr-data.txt ]; then echo "qr_ready"; exit 0; fi; STATUS=$(cat store/auth-status.txt 2>/dev/null || echo "waiting"); if [ "$STATUS" = "already_authenticated" ]; then echo "$STATUS"; exit 0; fi; sleep 1; done; echo "timeout"
|
||||||
|
```
|
||||||
|
|
||||||
|
If `already_authenticated`, skip to the next step.
|
||||||
|
|
||||||
|
If QR data is ready, generate the QR as SVG and inject it into the HTML template:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
node -e "
|
||||||
|
const QR = require('qrcode');
|
||||||
|
const fs = require('fs');
|
||||||
|
const qrData = fs.readFileSync('store/qr-data.txt', 'utf8');
|
||||||
|
QR.toString(qrData, { type: 'svg' }, (err, svg) => {
|
||||||
|
if (err) process.exit(1);
|
||||||
|
const template = fs.readFileSync('.claude/skills/setup/qr-auth.html', 'utf8');
|
||||||
|
fs.writeFileSync('store/qr-auth.html', template.replace('{{QR_SVG}}', svg));
|
||||||
|
console.log('done');
|
||||||
|
});
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
open store/qr-auth.html
|
||||||
|
```
|
||||||
|
|
||||||
|
Tell the user:
|
||||||
|
> A browser window should have opened with the QR code. It expires in about 60 seconds.
|
||||||
|
>
|
||||||
|
> Scan it with WhatsApp: **Settings → Linked Devices → Link a Device**
|
||||||
|
|
||||||
|
Then poll for completion (up to 120 seconds):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for i in $(seq 1 60); do STATUS=$(cat store/auth-status.txt 2>/dev/null || echo "waiting"); if [ "$STATUS" = "authenticated" ] || [ "$STATUS" = "already_authenticated" ]; then echo "$STATUS"; exit 0; elif echo "$STATUS" | grep -q "^failed:"; then echo "$STATUS"; exit 0; fi; sleep 2; done; echo "timeout"
|
||||||
|
```
|
||||||
|
|
||||||
|
- If `authenticated`, success — clean up with `rm -f store/qr-auth.html` and continue.
|
||||||
|
- If `failed:qr_timeout`, offer to retry (re-run the auth and regenerate the HTML page).
|
||||||
|
- If `failed:logged_out`, delete `store/auth/` and retry.
|
||||||
|
|
||||||
|
### Option B: Pairing Code
|
||||||
|
|
||||||
|
Ask the user for their phone number (with country code, no + or spaces, e.g. `14155551234`).
|
||||||
|
|
||||||
|
Clean any stale auth state and start:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -rf store/auth store/qr-data.txt store/auth-status.txt
|
||||||
|
npx tsx src/whatsapp-auth.ts --pairing-code --phone PHONE_NUMBER
|
||||||
|
```
|
||||||
|
|
||||||
|
Run this with `run_in_background: true`.
|
||||||
|
|
||||||
|
Poll for the pairing code (up to 15 seconds):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for i in $(seq 1 15); do STATUS=$(cat store/auth-status.txt 2>/dev/null || echo "waiting"); if echo "$STATUS" | grep -q "^pairing_code:"; then echo "$STATUS"; exit 0; elif [ "$STATUS" = "authenticated" ] || [ "$STATUS" = "already_authenticated" ]; then echo "$STATUS"; exit 0; elif echo "$STATUS" | grep -q "^failed:"; then echo "$STATUS"; exit 0; fi; sleep 1; done; echo "timeout"
|
||||||
|
```
|
||||||
|
|
||||||
|
Extract the code from the status (e.g. `pairing_code:ABC12DEF` → `ABC12DEF`) and tell the user:
|
||||||
|
|
||||||
|
> Your pairing code: **CODE_HERE**
|
||||||
|
>
|
||||||
|
> 1. Open WhatsApp on your phone
|
||||||
|
> 2. Tap **Settings → Linked Devices → Link a Device**
|
||||||
|
> 3. Tap **"Link with phone number instead"**
|
||||||
|
> 4. Enter the code: **CODE_HERE**
|
||||||
|
|
||||||
|
Then poll for completion (up to 120 seconds):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
for i in $(seq 1 60); do STATUS=$(cat store/auth-status.txt 2>/dev/null || echo "waiting"); if [ "$STATUS" = "authenticated" ] || [ "$STATUS" = "already_authenticated" ]; then echo "$STATUS"; exit 0; elif echo "$STATUS" | grep -q "^failed:"; then echo "$STATUS"; exit 0; fi; sleep 2; done; echo "timeout"
|
||||||
|
```
|
||||||
|
|
||||||
|
- If `authenticated` or `already_authenticated`, success — continue to next step.
|
||||||
|
- If `failed:logged_out`, delete `store/auth/` and retry.
|
||||||
|
- If `failed:515` or timeout, the 515 reconnect should handle this automatically. If it persists, the user may need to temporarily stop other WhatsApp-connected apps on the same device.
|
||||||
|
|
||||||
|
### Option C: QR Code in Terminal
|
||||||
|
|
||||||
|
Tell the user to run the auth command in another terminal window:
|
||||||
|
|
||||||
|
> Open another terminal and run:
|
||||||
|
> ```
|
||||||
|
> cd PROJECT_PATH && npm run auth
|
||||||
|
> ```
|
||||||
|
> Scan the QR code that appears, then let me know when it says "Successfully authenticated".
|
||||||
|
|
||||||
|
Replace `PROJECT_PATH` with the actual project path (use `pwd`).
|
||||||
|
|
||||||
|
Wait for the user to confirm authentication succeeded, then continue to the next step.
|
||||||
|
|
||||||
## 6. Configure Assistant Name and Main Channel
|
## 6. Configure Assistant Name and Main Channel
|
||||||
|
|
||||||
@@ -191,10 +299,11 @@ Store their choice for use in the steps below.
|
|||||||
>
|
>
|
||||||
> Options:
|
> Options:
|
||||||
> 1. Personal chat (Message Yourself) - Recommended
|
> 1. Personal chat (Message Yourself) - Recommended
|
||||||
> 2. Solo WhatsApp group (just me)
|
> 2. DM with a specific phone number (e.g. your other phone)
|
||||||
> 3. Group with other people (I understand the security implications)
|
> 3. Solo WhatsApp group (just me)
|
||||||
|
> 4. Group with other people (I understand the security implications)
|
||||||
|
|
||||||
If they choose option 3, ask a follow-up:
|
If they choose option 4, ask a follow-up:
|
||||||
|
|
||||||
> You've chosen a group with other people. This means everyone in that group will have admin privileges over NanoClaw.
|
> You've chosen a group with other people. This means everyone in that group will have admin privileges over NanoClaw.
|
||||||
>
|
>
|
||||||
@@ -222,9 +331,13 @@ npm run dev
|
|||||||
|
|
||||||
**For personal chat** (they chose option 1):
|
**For personal chat** (they chose option 1):
|
||||||
|
|
||||||
Personal chats are NOT synced to the database on startup — only groups are. Instead, ask the user for their phone number (with country code, no + or spaces, e.g. `14155551234`), then construct the JID as `{number}@s.whatsapp.net`.
|
Personal chats are NOT synced to the database on startup — only groups are. The JID for "Message Yourself" is the bot's own number. Use the number from the WhatsApp auth step and construct the JID as `{number}@s.whatsapp.net`.
|
||||||
|
|
||||||
**For group** (they chose option 2 or 3):
|
**For DM with a specific number** (they chose option 2):
|
||||||
|
|
||||||
|
Ask the user for the phone number (with country code, no + or spaces, e.g. `14155551234`), then construct the JID as `{number}@s.whatsapp.net`.
|
||||||
|
|
||||||
|
**For group** (they chose option 3 or 4):
|
||||||
|
|
||||||
Groups are synced on startup via `groupFetchAllParticipating`. Query the database for recent groups:
|
Groups are synced on startup via `groupFetchAllParticipating`. Query the database for recent groups:
|
||||||
```bash
|
```bash
|
||||||
@@ -472,6 +585,12 @@ The user should receive a response in WhatsApp.
|
|||||||
- Check that the chat JID is in the database: `sqlite3 store/messages.db "SELECT * FROM registered_groups"`
|
- Check that the chat JID is in the database: `sqlite3 store/messages.db "SELECT * FROM registered_groups"`
|
||||||
- Check `logs/nanoclaw.log` for errors
|
- Check `logs/nanoclaw.log` for errors
|
||||||
|
|
||||||
|
**Messages sent but not received by NanoClaw (DMs)**:
|
||||||
|
- WhatsApp may use LID (Linked Identity) JIDs for DMs instead of phone numbers
|
||||||
|
- Check logs for `Translated LID to phone JID` — if missing, the LID isn't being resolved
|
||||||
|
- The `translateJid` method in `src/channels/whatsapp.ts` uses `sock.signalRepository.lidMapping.getPNForLID()` to resolve LIDs
|
||||||
|
- Verify the registered JID doesn't have a device suffix (should be `number@s.whatsapp.net`, not `number:0@s.whatsapp.net`)
|
||||||
|
|
||||||
**WhatsApp disconnected**:
|
**WhatsApp disconnected**:
|
||||||
- The service will show a macOS notification
|
- The service will show a macOS notification
|
||||||
- Run `npm run auth` to re-authenticate
|
- Run `npm run auth` to re-authenticate
|
||||||
|
|||||||
32
.claude/skills/setup/qr-auth.html
Normal file
32
.claude/skills/setup/qr-auth.html
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html><head><title>NanoClaw - WhatsApp Auth</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: -apple-system, sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; margin: 0; background: #f5f5f5; }
|
||||||
|
.card { background: white; border-radius: 16px; padding: 40px; box-shadow: 0 4px 24px rgba(0,0,0,0.1); text-align: center; max-width: 400px; }
|
||||||
|
h2 { margin: 0 0 8px; }
|
||||||
|
.timer { font-size: 18px; color: #666; margin: 12px 0; }
|
||||||
|
.timer.urgent { color: #e74c3c; font-weight: bold; }
|
||||||
|
.instructions { color: #666; font-size: 14px; margin-top: 16px; }
|
||||||
|
svg { width: 280px; height: 280px; }
|
||||||
|
</style></head><body>
|
||||||
|
<div class="card">
|
||||||
|
<h2>Scan with WhatsApp</h2>
|
||||||
|
<div class="timer" id="timer">Expires in <span id="countdown">60</span>s</div>
|
||||||
|
<div id="qr">{{QR_SVG}}</div>
|
||||||
|
<div class="instructions">Settings → Linked Devices → Link a Device</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
let seconds = 60;
|
||||||
|
const countdown = document.getElementById('countdown');
|
||||||
|
const timer = document.getElementById('timer');
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
seconds--;
|
||||||
|
countdown.textContent = seconds;
|
||||||
|
if (seconds <= 10) timer.classList.add('urgent');
|
||||||
|
if (seconds <= 0) {
|
||||||
|
clearInterval(interval);
|
||||||
|
timer.textContent = 'QR code expired — re-run auth to get a new one';
|
||||||
|
timer.classList.add('urgent');
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
</script></body></html>
|
||||||
@@ -3,6 +3,7 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import makeWASocket, {
|
import makeWASocket, {
|
||||||
|
Browsers,
|
||||||
DisconnectReason,
|
DisconnectReason,
|
||||||
WASocket,
|
WASocket,
|
||||||
makeCacheableSignalKeyStore,
|
makeCacheableSignalKeyStore,
|
||||||
@@ -62,7 +63,7 @@ export class WhatsAppChannel implements Channel {
|
|||||||
},
|
},
|
||||||
printQRInTerminal: false,
|
printQRInTerminal: false,
|
||||||
logger,
|
logger,
|
||||||
browser: ['NanoClaw', 'Chrome', '1.0.0'],
|
browser: Browsers.macOS('Chrome'),
|
||||||
});
|
});
|
||||||
|
|
||||||
this.sock.ev.on('connection.update', (update) => {
|
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('creds.update', saveCreds);
|
||||||
|
|
||||||
this.sock.ev.on('messages.upsert', ({ messages }) => {
|
this.sock.ev.on('messages.upsert', async ({ messages }) => {
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
if (!msg.message) continue;
|
if (!msg.message) continue;
|
||||||
const rawJid = msg.key.remoteJid;
|
const rawJid = msg.key.remoteJid;
|
||||||
if (!rawJid || rawJid === 'status@broadcast') continue;
|
if (!rawJid || rawJid === 'status@broadcast') continue;
|
||||||
|
|
||||||
// Translate LID JID to phone JID if applicable
|
// Translate LID JID to phone JID if applicable
|
||||||
const chatJid = this.translateJid(rawJid);
|
const chatJid = await this.translateJid(rawJid);
|
||||||
|
|
||||||
const timestamp = new Date(
|
const timestamp = new Date(
|
||||||
Number(msg.messageTimestamp) * 1000,
|
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;
|
if (!jid.endsWith('@lid')) return jid;
|
||||||
const lidUser = jid.split('@')[0].split(':')[0];
|
const lidUser = jid.split('@')[0].split(':')[0];
|
||||||
const phoneJid = this.lidToPhoneMap[lidUser];
|
|
||||||
if (phoneJid) {
|
// Check local cache first
|
||||||
logger.debug({ lidJid: jid, phoneJid }, 'Translated LID to phone JID');
|
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;
|
return phoneJid;
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.debug({ err, jid }, 'Failed to resolve LID via signalRepository');
|
||||||
|
}
|
||||||
|
|
||||||
return jid;
|
return jid;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,25 +10,42 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import pino from 'pino';
|
import pino from 'pino';
|
||||||
import qrcode from 'qrcode-terminal';
|
import qrcode from 'qrcode-terminal';
|
||||||
|
import readline from 'readline';
|
||||||
|
|
||||||
import makeWASocket, {
|
import makeWASocket, {
|
||||||
|
Browsers,
|
||||||
DisconnectReason,
|
DisconnectReason,
|
||||||
makeCacheableSignalKeyStore,
|
makeCacheableSignalKeyStore,
|
||||||
useMultiFileAuthState,
|
useMultiFileAuthState,
|
||||||
} from '@whiskeysockets/baileys';
|
} from '@whiskeysockets/baileys';
|
||||||
|
|
||||||
const AUTH_DIR = './store/auth';
|
const AUTH_DIR = './store/auth';
|
||||||
|
const QR_FILE = './store/qr-data.txt';
|
||||||
|
const STATUS_FILE = './store/auth-status.txt';
|
||||||
|
|
||||||
const logger = pino({
|
const logger = pino({
|
||||||
level: 'warn', // Quiet logging - only show errors
|
level: 'warn', // Quiet logging - only show errors
|
||||||
});
|
});
|
||||||
|
|
||||||
async function authenticate(): Promise<void> {
|
// Check for --pairing-code flag and phone number
|
||||||
fs.mkdirSync(AUTH_DIR, { recursive: true });
|
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);
|
const { state, saveCreds } = await useMultiFileAuthState(AUTH_DIR);
|
||||||
|
|
||||||
if (state.creds.registered) {
|
if (state.creds.registered) {
|
||||||
|
fs.writeFileSync(STATUS_FILE, 'already_authenticated');
|
||||||
console.log('✓ Already authenticated with WhatsApp');
|
console.log('✓ Already authenticated with WhatsApp');
|
||||||
console.log(
|
console.log(
|
||||||
' To re-authenticate, delete the store/auth folder and run again.',
|
' To re-authenticate, delete the store/auth folder and run again.',
|
||||||
@@ -36,8 +53,6 @@ async function authenticate(): Promise<void> {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Starting WhatsApp authentication...\n');
|
|
||||||
|
|
||||||
const sock = makeWASocket({
|
const sock = makeWASocket({
|
||||||
auth: {
|
auth: {
|
||||||
creds: state.creds,
|
creds: state.creds,
|
||||||
@@ -45,13 +60,34 @@ async function authenticate(): Promise<void> {
|
|||||||
},
|
},
|
||||||
printQRInTerminal: false,
|
printQRInTerminal: false,
|
||||||
logger,
|
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) => {
|
sock.ev.on('connection.update', (update) => {
|
||||||
const { connection, lastDisconnect, qr } = update;
|
const { connection, lastDisconnect, qr } = update;
|
||||||
|
|
||||||
if (qr) {
|
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('Scan this QR code with WhatsApp:\n');
|
||||||
console.log(' 1. Open WhatsApp on your phone');
|
console.log(' 1. Open WhatsApp on your phone');
|
||||||
console.log(' 2. Tap Settings → Linked Devices → Link a Device');
|
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;
|
const reason = (lastDisconnect?.error as any)?.output?.statusCode;
|
||||||
|
|
||||||
if (reason === DisconnectReason.loggedOut) {
|
if (reason === DisconnectReason.loggedOut) {
|
||||||
|
fs.writeFileSync(STATUS_FILE, 'failed:logged_out');
|
||||||
console.log('\n✗ Logged out. Delete store/auth and try again.');
|
console.log('\n✗ Logged out. Delete store/auth and try again.');
|
||||||
process.exit(1);
|
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 {
|
} else {
|
||||||
|
fs.writeFileSync(STATUS_FILE, `failed:${reason || 'unknown'}`);
|
||||||
console.log('\n✗ Connection failed. Please try again.');
|
console.log('\n✗ Connection failed. Please try again.');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (connection === 'open') {
|
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('\n✓ Successfully authenticated with WhatsApp!');
|
||||||
console.log(' Credentials saved to store/auth/');
|
console.log(' Credentials saved to store/auth/');
|
||||||
console.log(' You can now start the NanoClaw service.\n');
|
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);
|
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) => {
|
authenticate().catch((err) => {
|
||||||
console.error('Authentication failed:', err.message);
|
console.error('Authentication failed:', err.message);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
Reference in New Issue
Block a user