Add leads handlers, WhatsApp integration, and test files

This commit is contained in:
2026-05-02 17:40:38 +00:00
parent 8833e38b80
commit 57e6e5a6fd
24 changed files with 2305 additions and 115 deletions

View File

@@ -0,0 +1,176 @@
const { Client, LocalAuth } = require('whatsapp-web.js');
const fs = require('fs');
const path = require('path');
const http = require('http');
function normalizePhone(phone) {
if (!phone) return '';
phone = phone.replace(/[^\d+]/g, '');
if (!phone.startsWith('+')) {
if (phone.length === 10) {
return '+1' + phone;
} else if (phone.length === 11) {
return '+' + phone;
}
}
return phone;
}
function isGroupChat(msg) {
return msg.key?.remoteJid?.includes('@g');
}
module.exports = { normalizePhone, isGroupChat };
var clientID, goEndpoint, internalSecret;
function logEvent(event, data) {
console.log(JSON.stringify({
timestamp: Date.now(),
event,
client_id: clientID,
...data,
}));
}
function postCustomer(data) {
const postData = JSON.stringify(data);
const url = new URL('/customers', goEndpoint);
const options = {
hostname: url.hostname,
port: url.port || 8080,
path: url.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(postData),
'X-Internal-Secret': internalSecret,
},
};
const req = http.request(options, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
logEvent('customer_synced', { phone: data.phone, success: res.statusCode === 200 });
});
});
req.on('error', (err) => {
logEvent('error', { message: err.message });
});
req.write(postData);
req.end();
}
async function syncChat(client, chat) {
const messages = await chat.fetchMessages({ limit: 100 });
for (const msg of messages) {
if (!msg.fromMe && msg.type === 'chat') {
const phone = normalizePhone(msg.from);
if (phone) {
postCustomer({
client_id: parseInt(clientID),
name: phone,
phone: phone,
});
}
}
}
}
async function main() {
const args = process.argv.slice(2);
clientID = args.find(a => a.startsWith('--client-id='))?.split('=')[1] || args[0];
goEndpoint = args.find(a => a.startsWith('--go-endpoint='))?.split('=')[1] || 'http://localhost:8080';
internalSecret = args.find(a => a.startsWith('--secret='))?.split('=')[1] || 'internal-secret';
const authDir = process.env.WA_AUTH_DIR || path.join('.wwebjs_auth', clientID);
if (!fs.existsSync(authDir)) {
fs.mkdirSync(authDir, { recursive: true });
}
logEvent('starting', { auth_dir: authDir });
const client = new Client({
authStrategy: new LocalAuth({
dataPath: authDir,
}),
puppeteer: {
executablePath: process.env.PUPPETEER_EXECUTABLE_PATH || '/usr/bin/chromium',
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox'],
},
});
client.on('qr', (qr) => {
logEvent('qr', { qr });
});
client.on('ready', async () => {
logEvent('ready', { timestamp: Date.now() });
const chats = await client.getChats();
for (const chat of chats) {
const isGroup = chat.id.server === 'g';
if (!isGroup) {
await syncChat(client, chat);
}
}
});
client.on('message', async (msg) => {
if (msg.fromMe) return;
const isGroup = msg.key.remoteJid?.includes('@g');
if (isGroup) {
logEvent('group_skipped', { from: msg.from });
return;
}
const phone = normalizePhone(msg.from);
if (phone) {
postCustomer({
client_id: parseInt(clientID),
name: phone,
phone: phone,
});
}
});
client.on('disconnected', (reason) => {
logEvent('disconnected', { reason: String(reason) });
process.exit(1);
});
client.on('auth_failure', (err) => {
logEvent('auth_failure', { error: String(err), stack: err?.stack });
process.exit(1);
});
client.on('authenticated', () => {
logEvent('authenticated', { timestamp: Date.now() });
});
client.on('authed', () => {
logEvent('authed', { timestamp: Date.now() });
});
client.on('error', (err) => {
logEvent('client_error', { error: String(err), stack: err?.stack });
});
try {
await client.initialize();
} catch (err) {
logEvent('error', { message: err.message });
process.exit(1);
}
}
if (require.main === module) {
main();
}