refactor: complete config unification and split db helpers
This commit is contained in:
385
src/db/messages.ts
Normal file
385
src/db/messages.ts
Normal file
@@ -0,0 +1,385 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user