import { Database } from 'bun:sqlite'; import { NewMessage } from '../types.js'; export interface ChatInfo { jid: string; name: string; last_message_time: string; channel: string; is_group: number; } function normalizeMessageRow( row: NewMessage & { is_from_me?: boolean | number; is_bot_message?: boolean | number; }, ): NewMessage { return { ...row, is_from_me: !!row.is_from_me, is_bot_message: !!row.is_bot_message, }; } export function normalizeSeqCursor( cursor: string | number | null | undefined, ): number { if (typeof cursor === 'number') { return Number.isFinite(cursor) && cursor > 0 ? cursor : 0; } if (!cursor) return 0; const parsed = Number.parseInt(cursor, 10); return Number.isFinite(parsed) && parsed > 0 ? parsed : 0; } export function storeChatMetadataInDatabase( database: Database, chatJid: string, timestamp: string, name?: string, channel?: string, isGroup?: boolean, ): void { const ch = channel ?? null; const group = isGroup === undefined ? null : isGroup ? 1 : 0; if (name) { database .prepare( ` INSERT INTO chats (jid, name, last_message_time, channel, is_group) VALUES (?, ?, ?, ?, ?) ON CONFLICT(jid) DO UPDATE SET name = excluded.name, last_message_time = MAX(last_message_time, excluded.last_message_time), channel = COALESCE(excluded.channel, channel), is_group = COALESCE(excluded.is_group, is_group) `, ) .run(chatJid, name, timestamp, ch, group); } else { database .prepare( ` INSERT INTO chats (jid, name, last_message_time, channel, is_group) VALUES (?, ?, ?, ?, ?) ON CONFLICT(jid) DO UPDATE SET last_message_time = MAX(last_message_time, excluded.last_message_time), channel = COALESCE(excluded.channel, channel), is_group = COALESCE(excluded.is_group, is_group) `, ) .run(chatJid, chatJid, timestamp, ch, group); } } export function getAllChatsFromDatabase(database: Database): ChatInfo[] { return database .prepare( ` SELECT jid, name, last_message_time, channel, is_group FROM chats ORDER BY last_message_time DESC `, ) .all() as ChatInfo[]; } export function storeMessageInDatabase( database: Database, msg: NewMessage, ): void { const nextSeq = () => { database.prepare('INSERT INTO message_sequence DEFAULT VALUES').run(); return ( database.prepare('SELECT last_insert_rowid() as id').get() as { id: number; } ).id; }; database.transaction(() => { const existing = database .prepare('SELECT seq FROM messages WHERE id = ? AND chat_jid = ?') .get(msg.id, msg.chat_jid) as { seq: number | null } | undefined; const seq = existing?.seq ?? nextSeq(); database .prepare( `INSERT INTO messages ( id, chat_jid, sender, sender_name, content, timestamp, seq, is_from_me, is_bot_message ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id, chat_jid) DO UPDATE SET sender = excluded.sender, sender_name = excluded.sender_name, content = excluded.content, timestamp = excluded.timestamp, is_from_me = excluded.is_from_me, is_bot_message = excluded.is_bot_message`, ) .run( msg.id, msg.chat_jid, msg.sender, msg.sender_name, msg.content, msg.timestamp, seq, msg.is_from_me ? 1 : 0, msg.is_bot_message ? 1 : 0, ); })(); } export function getNewMessagesFromDatabase( database: Database, jids: string[], lastTimestamp: string, botPrefix: string, limit: number = 200, ): { messages: NewMessage[]; newTimestamp: string } { if (jids.length === 0) return { messages: [], newTimestamp: lastTimestamp }; const placeholders = jids.map(() => '?').join(','); const sql = ` SELECT * FROM ( SELECT id, chat_jid, sender, sender_name, content, timestamp, is_from_me, is_bot_message FROM messages WHERE timestamp > ? AND chat_jid IN (${placeholders}) AND content NOT LIKE ? AND content != '' AND content IS NOT NULL ORDER BY timestamp DESC LIMIT ? ) ORDER BY timestamp `; const rows = database .prepare(sql) .all(lastTimestamp, ...jids, `${botPrefix}:%`, limit) as Array< NewMessage & { is_from_me?: boolean | number; is_bot_message?: boolean | number; } >; let newTimestamp = lastTimestamp; for (const row of rows) { if (row.timestamp > newTimestamp) newTimestamp = row.timestamp; } return { messages: rows.map(normalizeMessageRow), newTimestamp }; } export function getMessagesSinceFromDatabase( database: Database, chatJid: string, sinceTimestamp: string, botPrefix: string, limit: number = 200, ): NewMessage[] { const sql = ` SELECT * FROM ( SELECT id, chat_jid, sender, sender_name, content, timestamp, is_from_me, is_bot_message FROM messages WHERE chat_jid = ? AND timestamp > ? AND content NOT LIKE ? AND content != '' AND content IS NOT NULL ORDER BY timestamp DESC LIMIT ? ) ORDER BY timestamp `; const rows = database .prepare(sql) .all(chatJid, sinceTimestamp, `${botPrefix}:%`, limit) as Array< NewMessage & { is_from_me?: boolean | number; is_bot_message?: boolean | number; } >; return rows.map(normalizeMessageRow); } export function getLatestMessageSeqAtOrBeforeFromDatabase( database: Database, timestamp: string, chatJid?: string, ): number { if (!timestamp) return 0; const row = ( chatJid ? database .prepare( `SELECT COALESCE(MAX(seq), 0) AS maxSeq FROM messages WHERE chat_jid = ? AND timestamp <= ?`, ) .get(chatJid, timestamp) : database .prepare( `SELECT COALESCE(MAX(seq), 0) AS maxSeq FROM messages WHERE timestamp <= ?`, ) .get(timestamp) ) as { maxSeq: number | null }; return row.maxSeq ?? 0; } export function getNewMessagesBySeqFromDatabase( database: Database, jids: string[], lastSeqCursor: string | number, botPrefix: string, limit: number = 200, ): { messages: NewMessage[]; newSeqCursor: string } { const sinceSeq = normalizeSeqCursor(lastSeqCursor); if (jids.length === 0) { return { messages: [], newSeqCursor: String(sinceSeq) }; } const placeholders = jids.map(() => '?').join(','); const sql = ` SELECT id, chat_jid, sender, sender_name, content, timestamp, seq, is_from_me, is_bot_message FROM messages WHERE seq > ? AND chat_jid IN (${placeholders}) AND content NOT LIKE ? AND content != '' AND content IS NOT NULL ORDER BY seq LIMIT ? `; const rows = database .prepare(sql) .all(sinceSeq, ...jids, `${botPrefix}:%`, limit) as Array< NewMessage & { seq: number; is_from_me?: boolean | number; is_bot_message?: boolean | number; } >; const lastSeq = rows.length > 0 ? rows[rows.length - 1].seq : sinceSeq; return { messages: rows.map(normalizeMessageRow), newSeqCursor: String(lastSeq), }; } export function getMessagesSinceSeqFromDatabase( database: Database, chatJid: string, sinceSeqCursor: string | number, botPrefix: string, limit: number = 200, ): NewMessage[] { const sinceSeq = normalizeSeqCursor(sinceSeqCursor); const sql = ` SELECT id, chat_jid, sender, sender_name, content, timestamp, seq, is_from_me, is_bot_message FROM messages WHERE chat_jid = ? AND seq > ? AND content NOT LIKE ? AND content != '' AND content IS NOT NULL ORDER BY seq LIMIT ? `; const rows = database .prepare(sql) .all(chatJid, sinceSeq, `${botPrefix}:%`, limit) as Array< NewMessage & { seq: number; is_from_me?: boolean | number; is_bot_message?: boolean | number; } >; return rows.map(normalizeMessageRow); } export function getRecentChatMessagesFromDatabase( database: Database, chatJid: string, limit: number = 20, ): NewMessage[] { const sql = ` SELECT * FROM ( SELECT id, chat_jid, sender, sender_name, content, timestamp, is_from_me, is_bot_message FROM messages WHERE chat_jid = ? AND content != '' AND content IS NOT NULL ORDER BY timestamp DESC LIMIT ? ) ORDER BY timestamp `; const rows = database.prepare(sql).all(chatJid, limit) as Array< NewMessage & { is_from_me?: boolean | number; is_bot_message?: boolean | number; } >; return rows.map(normalizeMessageRow); } export function getLastHumanMessageTimestampFromDatabase( database: Database, chatJid: string, ): string | null { const row = database .prepare( `SELECT timestamp FROM messages WHERE chat_jid = ? AND is_bot_message = 0 AND is_from_me = 0 AND content != '' AND content IS NOT NULL ORDER BY timestamp DESC LIMIT 1`, ) .get(chatJid) as { timestamp: string } | undefined; return row?.timestamp ?? null; } export function getLastHumanMessageSenderFromDatabase( database: Database, chatJid: string, ): string | null { const row = database .prepare( `SELECT sender FROM messages WHERE chat_jid = ? AND is_bot_message = 0 AND is_from_me = 0 AND content != '' AND content IS NOT NULL ORDER BY timestamp DESC, seq DESC LIMIT 1`, ) .get(chatJid) as { sender: string } | undefined; return row?.sender ?? null; } export function getLastHumanMessageContentFromDatabase( database: Database, chatJid: string, ): string | null { const row = database .prepare( `SELECT content FROM messages WHERE chat_jid = ? AND is_bot_message = 0 AND is_from_me = 0 AND content != '' AND content IS NOT NULL ORDER BY timestamp DESC, seq DESC LIMIT 1`, ) .get(chatJid) as { content: string } | undefined; return row?.content ?? null; } export function hasRecentRestartAnnouncementInDatabase( database: Database, chatJid: string, sinceTimestamp: string, ): boolean { const row = database .prepare( `SELECT 1 FROM messages WHERE chat_jid = ? AND timestamp >= ? AND is_bot_message = 1 AND ( content LIKE '재시작 완료.%' OR content LIKE '재시작 감지.%' OR content LIKE '서비스 재시작으로 이전 작업이 중단됐습니다.%' ) LIMIT 1`, ) .get(chatJid, sinceTimestamp) as { 1: number } | undefined; return !!row; }