104 lines
3.9 KiB
JavaScript
104 lines
3.9 KiB
JavaScript
/*
|
|
* whatsapp-bridge — a real WhatsApp Web session that only ever reads.
|
|
*
|
|
* Long-lived sidecar to digest-engine. It keeps a logged-in web.whatsapp.com
|
|
* session open and appends every incoming message to /data/messages.jsonl, which
|
|
* digest-engine/ingest/whatsapp_ingest.py drains once per digest run.
|
|
*
|
|
* There is no send path in this file, and there must never be one: no reply(),
|
|
* no sendMessage(), no sendSeen(), no chat.markUnread(). Per docs/project-plan.md
|
|
* Phase 12 step 8, digest-engine has no mutation path anywhere by construction.
|
|
*
|
|
* READ digest-engine/README.md BEFORE ENABLING THIS. Automating a personal
|
|
* WhatsApp account carries a real ban risk, mitigated here but not eliminated.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const qrcode = require('qrcode-terminal');
|
|
const { Client, LocalAuth } = require('whatsapp-web.js');
|
|
|
|
const DATA_DIR = process.env.WHATSAPP_DATA_DIR || '/data';
|
|
const MESSAGES_PATH = path.join(DATA_DIR, 'messages.jsonl');
|
|
const AUTH_PATH = path.join(DATA_DIR, '.wwebjs_auth');
|
|
|
|
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
|
|
const client = new Client({
|
|
authStrategy: new LocalAuth({ dataPath: AUTH_PATH }),
|
|
puppeteer: {
|
|
/*
|
|
* headless: false is deliberate — do NOT "fix" this to true.
|
|
*
|
|
* WhatsApp's automation detection fingerprints headless Chrome specifically
|
|
* (navigator.webdriver, the HeadlessChrome UA token, missing GPU/permissions
|
|
* surface). Running a genuinely headful Chromium under Xvfb inside the
|
|
* container removes that signal entirely rather than trying to patch around
|
|
* it. The Dockerfile's `xvfb-run -a node index.js` is what supplies the
|
|
* virtual display that makes headful possible with no monitor attached.
|
|
*/
|
|
headless: false,
|
|
executablePath: process.env.CHROMIUM_PATH || '/usr/bin/chromium',
|
|
args: [
|
|
// Chromium's sandbox needs privileges this container deliberately does not
|
|
// have; the container itself is the isolation boundary here.
|
|
'--no-sandbox',
|
|
'--disable-setuid-sandbox',
|
|
// /dev/shm defaults to 64MB in Docker, which crashes Chromium's renderer.
|
|
'--disable-dev-shm-usage'
|
|
]
|
|
}
|
|
});
|
|
|
|
client.on('qr', (qr) => {
|
|
console.log('--- Scan this QR code with WhatsApp -> Linked devices -> Link a device ---');
|
|
qrcode.generate(qr, { small: true });
|
|
console.log('--- (docker compose logs -f whatsapp-bridge) ---');
|
|
});
|
|
|
|
client.on('authenticated', () => {
|
|
console.log(`Authenticated. Session persisted under ${AUTH_PATH}.`);
|
|
});
|
|
|
|
client.on('auth_failure', (message) => {
|
|
console.error(`Authentication failed: ${message}`);
|
|
console.error(`Delete ${AUTH_PATH} and restart to re-scan the QR code.`);
|
|
});
|
|
|
|
client.on('ready', () => {
|
|
console.log(`Ready. Appending received messages to ${MESSAGES_PATH}.`);
|
|
});
|
|
|
|
client.on('disconnected', (reason) => {
|
|
console.error(`Disconnected: ${reason}. Exiting so the container restart policy reconnects.`);
|
|
process.exit(1);
|
|
});
|
|
|
|
// 'message' fires for incoming messages only ('message_create' would also fire
|
|
// for our own outgoing ones, which are not what the digest summarises).
|
|
client.on('message', async (message) => {
|
|
try {
|
|
const contact = await message.getContact();
|
|
const chat = await message.getChat();
|
|
const record = {
|
|
id: message.id ? message.id._serialized : null,
|
|
from: message.from,
|
|
from_name: contact ? (contact.pushname || contact.name || contact.number) : message.from,
|
|
chat: chat ? chat.name : null,
|
|
is_group: chat ? Boolean(chat.isGroup) : false,
|
|
timestamp: message.timestamp,
|
|
type: message.type,
|
|
body: message.body || ''
|
|
};
|
|
// appendFileSync opens/appends/closes per message, so whatsapp_ingest.py can
|
|
// rename the file out from under us mid-run without losing a partial write.
|
|
fs.appendFileSync(MESSAGES_PATH, JSON.stringify(record) + '\n', 'utf8');
|
|
} catch (err) {
|
|
console.error(`Could not record a message: ${err}`);
|
|
}
|
|
});
|
|
|
|
client.initialize();
|