Files
arestools/src/core/db.js
T

138 lines
4.4 KiB
JavaScript
Raw Normal View History

import { mkdirSync } from 'node:fs';
import { DatabaseSync } from 'node:sqlite';
import { config, dataPath } from '../config.js';
import { encryptString, decryptString } from './crypto.js';
import { logger } from './logger.js';
let db = null;
/**
* Локальный индекс на node:sqlite. Тексты храним зашифрованными (AES-GCM),
* бинарные медиа лежат отдельными файлами в data/media, здесь только путь.
*/
export function openDb() {
if (db) return db;
mkdirSync(dataPath(), { recursive: true });
db = new DatabaseSync(`${dataPath()}/userbot.db`);
db.exec(`
CREATE TABLE IF NOT EXISTS messages (
msg_id INTEGER PRIMARY KEY,
chat_id TEXT,
sender_id TEXT,
sender TEXT,
text_enc TEXT,
media_kind TEXT,
media_path TEXT,
created_at INTEGER
);
CREATE TABLE IF NOT EXISTS captures (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT, -- 'deleted' | 'onetime'
chat_id TEXT,
sender_id TEXT,
sender TEXT,
text_enc TEXT,
media_kind TEXT,
media_path TEXT,
captured_at INTEGER
);
CREATE INDEX IF NOT EXISTS idx_captures_time ON captures(captured_at DESC);
`);
logger.debug('sqlite: база готова');
return db;
}
const now = () => Math.floor(Date.now() / 1000);
/** Кладём каждое входящее в кольцевой кэш — источник для восстановления удалённых. */
export function cacheMessage({ msgId, chatId, senderId, sender, text, mediaKind, mediaPath }) {
openDb()
.prepare(
`INSERT OR REPLACE INTO messages
(msg_id, chat_id, sender_id, sender, text_enc, media_kind, media_path, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
msgId,
String(chatId ?? ''),
String(senderId ?? ''),
sender ?? '',
text ? encryptString(text) : null,
mediaKind ?? null,
mediaPath ?? null,
now(),
);
}
export function getCachedMessage(msgId) {
const row = openDb().prepare('SELECT * FROM messages WHERE msg_id = ?').get(msgId);
return row ? decodeRow(row) : null;
}
/** Записываем факт перехвата (удаление или one-time) — это и есть лента в боте. */
export function saveCapture(kind, data) {
const info = openDb()
.prepare(
`INSERT INTO captures
(kind, chat_id, sender_id, sender, text_enc, media_kind, media_path, captured_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
)
.run(
kind,
String(data.chatId ?? ''),
String(data.senderId ?? ''),
data.sender ?? '',
data.text ? encryptString(data.text) : null,
data.mediaKind ?? null,
data.mediaPath ?? null,
now(),
);
return Number(info.lastInsertRowid);
}
export function listCaptures({ kind, limit = 10, offset = 0 } = {}) {
const where = kind ? 'WHERE kind = ?' : '';
const args = kind ? [kind, limit, offset] : [limit, offset];
return openDb()
.prepare(`SELECT * FROM captures ${where} ORDER BY captured_at DESC LIMIT ? OFFSET ?`)
.all(...args)
.map(decodeRow);
}
export function getCapture(id) {
const row = openDb().prepare('SELECT * FROM captures WHERE id = ?').get(id);
return row ? decodeRow(row) : null;
}
export function countCaptures(kind) {
const where = kind ? 'WHERE kind = ?' : '';
const args = kind ? [kind] : [];
return openDb().prepare(`SELECT COUNT(*) AS n FROM captures ${where}`).get(...args).n;
}
/** Чистка по retentionDays — вызывается по таймеру из index.js. */
export function purgeOld() {
if (!config.retentionDays) return { messages: 0, captures: 0 };
const cutoff = now() - config.retentionDays * 86400;
const m = openDb().prepare('DELETE FROM messages WHERE created_at < ?').run(cutoff);
const c = openDb().prepare('DELETE FROM captures WHERE captured_at < ?').run(cutoff);
return { messages: Number(m.changes), captures: Number(c.changes) };
}
function decodeRow(row) {
return {
...row,
text: row.text_enc ? safeDecrypt(row.text_enc) : null,
};
}
function safeDecrypt(value) {
try {
return decryptString(value);
} catch (err) {
logger.warn('sqlite: не удалось расшифровать текст записи', err.message);
return '[не удалось расшифровать]';
}
}