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();
}

View File

@@ -0,0 +1,50 @@
const path = require('path');
const fs = require('fs');
describe('whatsapp-leads.js module', () => {
const scriptPath = path.join(__dirname, '../src/whatsapp-leads.js');
test('script file exists', () => {
expect(fs.existsSync(scriptPath)).toBe(true);
});
test('exports normalizePhone function', () => {
const script = require(scriptPath);
expect(typeof script.normalizePhone).toBe('function');
});
test('exports isGroupChat function', () => {
const script = require(scriptPath);
expect(typeof script.isGroupChat).toBe('function');
});
});
describe('WhatsApp message filtering', () => {
const scriptPath = path.join(__dirname, '../src/whatsapp-leads.js');
test('filters out group messages (only 1:1 chats)', () => {
const groupMessage = {
key: { remoteJid: '1234567890@g group' },
message: { conversation: 'Group message' },
};
const personalMessage = {
key: { remoteJid: '1234567890@c.us' },
message: { conversation: 'Personal message' },
};
expect(isGroupChat(groupMessage)).toBe(true);
expect(isGroupChat(personalMessage)).toBe(false);
});
test('normalizes phone numbers', () => {
const script = require(scriptPath);
expect(script.normalizePhone('+5521987654321')).toBe('+5521987654321');
expect(script.normalizePhone('21987654321')).toBe('+21987654321');
expect(script.normalizePhone('')).toBe('');
});
});
function isGroupChat(message) {
return message.key.remoteJid?.includes('@g');
}