From 06916014696cfc2804390816653babd3a99bd8b7 Mon Sep 17 00:00:00 2001 From: gavrielc Date: Sat, 31 Jan 2026 19:20:41 +0200 Subject: [PATCH] Extract database operations into separate db.ts module - src/db.ts: initDatabase, closeDatabase, storeMessage, getNewMessages - Removes SQL from index.ts - Database initialization happens once at startup Co-Authored-By: Claude Opus 4.5 --- src/db.ts | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 81 ++++++---------------------------------------------- 2 files changed, 86 insertions(+), 73 deletions(-) create mode 100644 src/db.ts diff --git a/src/db.ts b/src/db.ts new file mode 100644 index 0000000..5a5ba7d --- /dev/null +++ b/src/db.ts @@ -0,0 +1,78 @@ +import Database from 'better-sqlite3'; +import fs from 'fs'; +import path from 'path'; +import { proto } from '@whiskeysockets/baileys'; +import { NewMessage } from './types.js'; +import { STORE_DIR } from './config.js'; + +let db: Database.Database; + +export function initDatabase(): void { + const dbPath = path.join(STORE_DIR, 'messages.db'); + fs.mkdirSync(path.dirname(dbPath), { recursive: true }); + + db = new Database(dbPath); + db.exec(` + CREATE TABLE IF NOT EXISTS chats ( + jid TEXT PRIMARY KEY, + name TEXT, + last_message_time TEXT + ); + CREATE TABLE IF NOT EXISTS messages ( + id TEXT, + chat_jid TEXT, + sender TEXT, + content TEXT, + timestamp TEXT, + is_from_me INTEGER, + PRIMARY KEY (id, chat_jid), + FOREIGN KEY (chat_jid) REFERENCES chats(jid) + ); + CREATE INDEX IF NOT EXISTS idx_timestamp ON messages(timestamp); + `); +} + +export function closeDatabase(): void { + db.close(); +} + +export function storeMessage(msg: proto.IWebMessageInfo, chatJid: string, isFromMe: boolean): void { + if (!msg.key) return; + + const content = + msg.message?.conversation || + msg.message?.extendedTextMessage?.text || + msg.message?.imageMessage?.caption || + msg.message?.videoMessage?.caption || + ''; + + const timestamp = new Date(Number(msg.messageTimestamp) * 1000).toISOString(); + const sender = msg.key.participant || msg.key.remoteJid || ''; + const msgId = msg.key.id || ''; + + db.prepare(`INSERT OR REPLACE INTO chats (jid, name, last_message_time) VALUES (?, ?, ?)`) + .run(chatJid, chatJid, timestamp); + db.prepare(`INSERT OR REPLACE INTO messages (id, chat_jid, sender, content, timestamp, is_from_me) VALUES (?, ?, ?, ?, ?, ?)`) + .run(msgId, chatJid, sender, content, timestamp, isFromMe ? 1 : 0); +} + +export function getNewMessages(jids: string[], lastTimestamp: string): { messages: NewMessage[]; newTimestamp: string } { + if (jids.length === 0) return { messages: [], newTimestamp: lastTimestamp }; + + const placeholders = jids.map(() => '?').join(','); + const sql = ` + SELECT id, chat_jid, sender, content, timestamp + FROM messages + WHERE timestamp > ? AND chat_jid IN (${placeholders}) + ORDER BY timestamp + `; + + const rows = db.prepare(sql).all(lastTimestamp, ...jids) as NewMessage[]; + + let newTimestamp = lastTimestamp; + for (const row of rows) { + if (row.timestamp > newTimestamp) newTimestamp = row.timestamp; + } + + return { messages: rows, newTimestamp }; +} diff --git a/src/index.ts b/src/index.ts index 5abeb84..de14852 100644 --- a/src/index.ts +++ b/src/index.ts @@ -2,12 +2,10 @@ import makeWASocket, { useMultiFileAuthState, DisconnectReason, makeCacheableSignalKeyStore, - WASocket, - proto + WASocket } from '@whiskeysockets/baileys'; import { query } from '@anthropic-ai/claude-agent-sdk'; import pino from 'pino'; -import Database from 'better-sqlite3'; import { exec } from 'child_process'; import fs from 'fs'; import path from 'path'; @@ -22,42 +20,18 @@ import { CLEAR_COMMAND } from './config.js'; import { RegisteredGroup, Session, NewMessage } from './types.js'; +import { initDatabase, closeDatabase, storeMessage, getNewMessages } from './db.js'; const logger = pino({ level: process.env.LOG_LEVEL || 'info', transport: { target: 'pino-pretty', options: { colorize: true } } }); -let db: Database.Database; let sock: WASocket; let lastTimestamp = ''; let sessions: Session = {}; let registeredGroups: Record = {}; -function initDatabase(dbPath: string): Database.Database { - fs.mkdirSync(path.dirname(dbPath), { recursive: true }); - const database = new Database(dbPath); - database.exec(` - CREATE TABLE IF NOT EXISTS chats ( - jid TEXT PRIMARY KEY, - name TEXT, - last_message_time TEXT - ); - CREATE TABLE IF NOT EXISTS messages ( - id TEXT, - chat_jid TEXT, - sender TEXT, - content TEXT, - timestamp TEXT, - is_from_me INTEGER, - PRIMARY KEY (id, chat_jid), - FOREIGN KEY (chat_jid) REFERENCES chats(jid) - ); - CREATE INDEX IF NOT EXISTS idx_timestamp ON messages(timestamp); - `); - return database; -} - function loadJson(filePath: string, defaultValue: T): T { try { if (fs.existsSync(filePath)) { @@ -88,48 +62,6 @@ function saveState(): void { saveJson(path.join(DATA_DIR, 'sessions.json'), sessions); } -function storeMessage(msg: proto.IWebMessageInfo, chatJid: string, isFromMe: boolean): void { - if (!msg.key) return; - - const content = - msg.message?.conversation || - msg.message?.extendedTextMessage?.text || - msg.message?.imageMessage?.caption || - msg.message?.videoMessage?.caption || - ''; - - const timestamp = new Date(Number(msg.messageTimestamp) * 1000).toISOString(); - const sender = msg.key.participant || msg.key.remoteJid || ''; - const msgId = msg.key.id || ''; - - try { - db.prepare(`INSERT OR REPLACE INTO chats (jid, name, last_message_time) VALUES (?, ?, ?)`).run(chatJid, chatJid, timestamp); - db.prepare(`INSERT OR REPLACE INTO messages (id, chat_jid, sender, content, timestamp, is_from_me) VALUES (?, ?, ?, ?, ?, ?)`).run(msgId, chatJid, sender, content, timestamp, isFromMe ? 1 : 0); - logger.debug({ chatJid, msgId }, 'Message stored'); - } catch (err) { - logger.error({ err, msgId }, 'Failed to store message'); - } -} - -function getNewMessages(): NewMessage[] { - const jids = Object.keys(registeredGroups); - if (jids.length === 0) return []; - - const placeholders = jids.map(() => '?').join(','); - const sql = ` - SELECT id, chat_jid, sender, content, timestamp - FROM messages - WHERE timestamp > ? AND chat_jid IN (${placeholders}) - ORDER BY timestamp - `; - - const rows = db.prepare(sql).all(lastTimestamp, ...jids) as NewMessage[]; - for (const row of rows) { - if (row.timestamp > lastTimestamp) lastTimestamp = row.timestamp; - } - return rows; -} - async function processMessage(msg: NewMessage): Promise { const group = registeredGroups[msg.chat_jid]; if (!group) return; @@ -281,7 +213,10 @@ async function startMessageLoop(): Promise { while (true) { try { - const messages = getNewMessages(); + const jids = Object.keys(registeredGroups); + const { messages, newTimestamp } = getNewMessages(jids, lastTimestamp); + lastTimestamp = newTimestamp; + if (messages.length > 0) logger.info({ count: messages.length }, 'New messages'); for (const msg of messages) await processMessage(msg); saveState(); @@ -293,14 +228,14 @@ async function startMessageLoop(): Promise { } async function main(): Promise { - db = initDatabase(path.join(STORE_DIR, 'messages.db')); + initDatabase(); logger.info('Database initialized'); loadState(); await connectWhatsApp(); const shutdown = () => { logger.info('Shutting down...'); - db.close(); + closeDatabase(); process.exit(0); }; process.on('SIGINT', shutdown);