feat: initial setup with WhatsApp CRM apps and sync services
This commit is contained in:
185
apps/whatsapp-sync/src/database.js
Normal file
185
apps/whatsapp-sync/src/database.js
Normal 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,
|
||||
};
|
||||
89
apps/whatsapp-sync/src/jsonl-parser.js
Normal file
89
apps/whatsapp-sync/src/jsonl-parser.js
Normal 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,
|
||||
};
|
||||
57
apps/whatsapp-sync/src/sync.js
Normal file
57
apps/whatsapp-sync/src/sync.js
Normal 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,
|
||||
};
|
||||
Reference in New Issue
Block a user