feat: initial setup with WhatsApp CRM apps and sync services

This commit is contained in:
2026-04-05 17:29:28 +00:00
parent d983027450
commit e8bb767884
57 changed files with 26156 additions and 0 deletions

View File

@@ -0,0 +1,185 @@
var Database;
try {
Database = require('better-sqlite3');
} catch (e) {
Database = function() {
throw new Error('better-sqlite3 not available');
};
}
var STAGES = [
'LEADS DE ENTRADA',
'DECIDINDO',
'DISCUSSAO DE CONTRATO',
'DECISAO FINAL',
];
function initialize(dbPath) {
var db = new Database(dbPath);
db.exec("\n CREATE TABLE IF NOT EXISTS contacts (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n phone TEXT UNIQUE NOT NULL,\n name TEXT,\n stage TEXT DEFAULT 'LEADS DE ENTRADA',\n payment_status TEXT DEFAULT 'pending',\n schedule_date INTEGER,\n notes TEXT,\n created_at INTEGER DEFAULT (strftime('%s', 'now')),\n updated_at INTEGER DEFAULT (strftime('%s', 'now'))\n )\n ");
db.exec("\n CREATE TABLE IF NOT EXISTS messages (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n message_id TEXT UNIQUE NOT NULL,\n contact_phone TEXT NOT NULL,\n body TEXT,\n timestamp INTEGER,\n has_media INTEGER DEFAULT 0,\n created_at INTEGER DEFAULT (strftime('%s', 'now')),\n FOREIGN KEY (contact_phone) REFERENCES contacts(phone)\n )\n ");
db.exec("\n CREATE TABLE IF NOT EXISTS tasks (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n contact_phone TEXT NOT NULL,\n title TEXT NOT NULL,\n due_date INTEGER,\n status TEXT DEFAULT 'pending',\n created_at INTEGER DEFAULT (strftime('%s', 'now')),\n FOREIGN KEY (contact_phone) REFERENCES contacts(phone)\n )\n ");
db.exec("CREATE INDEX IF NOT EXISTS idx_messages_contact ON messages(contact_phone)");
db.exec("CREATE INDEX IF NOT EXISTS idx_tasks_contact ON tasks(contact_phone)");
db.exec("CREATE INDEX IF NOT EXISTS idx_contacts_phone ON contacts(phone)");
return db;
}
function insertContact(db, contact) {
var stmt = db.prepare("\n INSERT INTO contacts (phone, name, stage, payment_status, schedule_date, notes)\n VALUES (@phone, @name, @stage, @paymentStatus, @scheduleDate, @notes)\n ");
var result = stmt.run({
phone: contact.phone,
name: contact.name || null,
stage: contact.stage || 'LEADS DE ENTRADA',
paymentStatus: contact.paymentStatus || 'pending',
scheduleDate: contact.scheduleDate || null,
notes: contact.notes || null,
});
return { id: result.lastInsertRowid, phone: contact.phone };
}
function findContactByPhone(db, phone) {
var stmt = db.prepare('SELECT * FROM contacts WHERE phone = ?');
return stmt.get(phone);
}
function updateContactStage(db, phone, stage) {
if (STAGES.indexOf(stage) === -1) {
throw new Error('Invalid stage: ' + stage);
}
var stmt = db.prepare("\n UPDATE contacts\n SET stage = ?, updated_at = strftime('%s', 'now')\n WHERE phone = ?\n ");
return stmt.run(stage, phone);
}
function updateContactPaymentStatus(db, phone, status) {
var stmt = db.prepare("\n UPDATE contacts\n SET payment_status = ?, updated_at = strftime('%s', 'now')\n WHERE phone = ?\n ");
return stmt.run(status, phone);
}
function updateContactScheduleDate(db, phone, date) {
var stmt = db.prepare("\n UPDATE contacts\n SET schedule_date = ?, updated_at = strftime('%s', 'now')\n WHERE phone = ?\n ");
return stmt.run(date, phone);
}
function listContactsByStage(db, stage) {
var stmt = db.prepare('SELECT * FROM contacts WHERE stage = ? ORDER BY updated_at DESC');
return stmt.all(stage);
}
function listContactsByPaymentStatus(db, status) {
var stmt = db.prepare('SELECT * FROM contacts WHERE payment_status = ? ORDER BY updated_at DESC');
return stmt.all(status);
}
function listAllContacts(db) {
var stmt = db.prepare('SELECT * FROM contacts ORDER BY updated_at DESC');
return stmt.all();
}
function insertMessage(db, message) {
var stmt = db.prepare("\n INSERT OR IGNORE INTO messages (message_id, contact_phone, body, timestamp, has_media)\n VALUES (@id, @contactPhone, @body, @timestamp, @hasMedia)\n ");
return stmt.run({
id: message.id,
contactPhone: message.contactPhone,
body: message.body || '',
timestamp: message.timestamp || null,
hasMedia: message.hasMedia ? 1 : 0,
});
}
function findMessagesByContact(db, phone) {
var stmt = db.prepare("\n SELECT * FROM messages\n WHERE contact_phone = ?\n ORDER BY timestamp DESC\n ");
return stmt.all(phone);
}
function messageExists(db, messageId) {
var stmt = db.prepare('SELECT 1 FROM messages WHERE message_id = ?');
return stmt.get(messageId);
}
function insertTask(db, task) {
var stmt = db.prepare("\n INSERT INTO tasks (contact_phone, title, due_date, status)\n VALUES (@contactPhone, @title, @dueDate, @status)\n ");
return stmt.run({
contactPhone: task.contactPhone,
title: task.title,
dueDate: task.dueDate || null,
status: task.status || 'pending',
});
}
function findTasksByContact(db, phone, status) {
var sql = 'SELECT * FROM tasks WHERE contact_phone = ?';
var params = [phone];
if (status) {
sql += ' AND status = ?';
params.push(status);
}
sql += ' ORDER BY due_date ASC';
var stmt = db.prepare(sql);
return stmt.all.apply(stmt, params);
}
function findOverdueTasks(db) {
var now = Math.floor(Date.now() / 1000);
var stmt = db.prepare("\n SELECT * FROM tasks\n WHERE status = 'pending' AND due_date < ?\n ORDER BY due_date ASC\n ");
return stmt.all(now);
}
function updateTaskStatus(db, taskId, status) {
var stmt = db.prepare('UPDATE tasks SET status = ? WHERE id = ?');
return stmt.run(status, taskId);
}
function updateTaskDueDate(db, taskId, dueDate) {
var stmt = db.prepare('UPDATE tasks SET due_date = ? WHERE id = ?');
return stmt.run(dueDate, taskId);
}
function deleteContact(db, phone) {
var stmt = db.prepare('DELETE FROM contacts WHERE phone = ?');
return stmt.run(phone);
}
function close(db) {
db.close();
}
module.exports = {
initialize: initialize,
insertContact: insertContact,
findContactByPhone: findContactByPhone,
updateContactStage: updateContactStage,
updateContactPaymentStatus: updateContactPaymentStatus,
updateContactScheduleDate: updateContactScheduleDate,
listContactsByStage: listContactsByStage,
listContactsByPaymentStatus: listContactsByPaymentStatus,
listAllContacts: listAllContacts,
insertMessage: insertMessage,
findMessagesByContact: findMessagesByContact,
messageExists: messageExists,
insertTask: insertTask,
findTasksByContact: findTasksByContact,
findOverdueTasks: findOverdueTasks,
updateTaskStatus: updateTaskStatus,
updateTaskDueDate: updateTaskDueDate,
deleteContact: deleteContact,
close: close,
STAGES: STAGES,
};

View File

@@ -0,0 +1,89 @@
var fs = require('fs');
var path = require('path');
function ParseError(message) {
this.message = message;
}
ParseError.prototype = new Error();
function parseMessageLine(line) {
if (!line || typeof line !== 'string') {
return null;
}
var trimmed = line.trim();
if (!trimmed) {
return null;
}
try {
var parsed = JSON.parse(trimmed);
return {
id: parsed.id,
from: parsed.from,
body: parsed.body || '',
timestamp: parsed.timestamp,
hasMedia: parsed.hasMedia || false,
chatId: parsed.chatId,
};
} catch (e) {
throw new ParseError('Invalid JSON: ' + trimmed);
}
}
function parseFile(content) {
if (!content || typeof content !== 'string') {
return [];
}
var lines = content.split('\n');
var messages = [];
for (var i = 0; i < lines.length; i++) {
var parsed = parseMessageLine(lines[i]);
if (parsed) {
messages.push(parsed);
}
}
return messages;
}
function filterNewMessages(messageIds, existingMessages) {
var newMessages = [];
for (var i = 0; i < messageIds.length; i++) {
if (!existingMessages.has(messageIds[i])) {
newMessages.push(messageIds[i]);
}
}
return newMessages;
}
function getOrCreateContact(phone, contactsMap) {
var existing = contactsMap.get(phone);
if (existing) {
return existing;
}
var newContact = {
phone: phone,
name: null,
stage: 'LEADS DE ENTRADA',
paymentStatus: 'pending',
createdAt: Date.now(),
};
contactsMap.set(phone, newContact);
return newContact;
}
module.exports = {
parseMessageLine: parseMessageLine,
parseFile: parseFile,
filterNewMessages: filterNewMessages,
getOrCreateContact: getOrCreateContact,
ParseError: ParseError,
};

View File

@@ -0,0 +1,57 @@
var fs = require('fs');
var path = require('path');
var DATA_DIR = process.env.DATA_DIR || path.join(__dirname, '..', 'data');
var MESSAGES_FILE = path.join(DATA_DIR, 'messages.jsonl');
function ensureDataDir() {
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
}
}
function formatMessageForJsonl(message) {
var phoneNumber = message.from ? message.from.split('@')[0] : '';
return JSON.stringify({
id: message.id ? message.id._serialized || message.id.id : message.id,
from: phoneNumber,
body: message.body || '',
timestamp: message.timestamp,
hasMedia: message.hasMedia || false,
chatId: message.chatId ? message.chatId._serialized || message.chatId : message.chatId,
});
}
function syncMessages(client) {
ensureDataDir();
return client.getChats().then(function(chats) {
var writeStream = fs.createWriteStream(MESSAGES_FILE, { flags: 'a' });
var messagePromises = chats.map(function(chat) {
return chat.fetchMessages({ limit: 100 }).then(function(messages) {
var writePromises = messages
.filter(function(message) { return !message.fromMe; })
.map(function(message) {
var jsonlLine = formatMessageForJsonl(message);
writeStream.write(jsonlLine + '\n');
});
return Promise.all(writePromises);
});
});
return Promise.all(messagePromises).then(function() {
return new Promise(function(resolve, reject) {
writeStream.end(resolve);
});
});
});
}
module.exports = {
syncMessages: syncMessages,
formatMessageForJsonl: formatMessageForJsonl,
DATA_DIR: DATA_DIR,
MESSAGES_FILE: MESSAGES_FILE,
};