import crypto from 'crypto'; import path from 'path'; import { loadEncryptedJson, saveEncryptedJson } from './cryptoUtils.js'; const DATA_ROOT = '/data/deleted_chats'; const INDEX_FILE = path.join(DATA_ROOT, 'index.json'); const RETENTION_MS = 30 * 24 * 60 * 60 * 1000; const state = { loaded: false, index: { deletedChats: {}, }, }; function nowIso() { return new Date().toISOString(); } function ensureOwner(owner) { if (!owner?.type || !owner?.id) throw new Error('Invalid deleted chat owner'); return owner; } async function ensureLoaded() { if (state.loaded) return; const stored = await loadEncryptedJson(INDEX_FILE); state.index = { deletedChats: stored?.deletedChats || {}, }; state.loaded = true; await thisStore.purgeExpired(); } async function saveIndex() { await saveEncryptedJson(INDEX_FILE, state.index); } function matchesOwner(record, owner) { return record.ownerType === owner.type && record.ownerId === owner.id; } function sanitize(record) { return { id: record.id, originalSessionId: record.originalSessionId, name: record.name, deletedAt: record.deletedAt, purgeAt: record.purgeAt, created: record.created, }; } const thisStore = { async add(owner, sessionSnapshot) { ensureOwner(owner); await ensureLoaded(); const sessionId = sessionSnapshot?.id; if (!sessionId) return null; for (const record of Object.values(state.index.deletedChats)) { if ( record.originalSessionId === sessionId && record.ownerType === owner.type && record.ownerId === owner.id ) { record.sessionSnapshot = sessionSnapshot; record.name = sessionSnapshot.name || 'Deleted Chat'; record.created = sessionSnapshot.created || Date.now(); record.deletedAt = nowIso(); record.purgeAt = new Date(Date.now() + RETENTION_MS).toISOString(); await saveIndex(); return sanitize(record); } } const deleted = { id: crypto.randomUUID(), originalSessionId: sessionId, ownerType: owner.type, ownerId: owner.id, name: sessionSnapshot.name || 'Deleted Chat', created: sessionSnapshot.created || Date.now(), sessionSnapshot, deletedAt: nowIso(), purgeAt: new Date(Date.now() + RETENTION_MS).toISOString(), }; state.index.deletedChats[deleted.id] = deleted; await saveIndex(); return sanitize(deleted); }, async list(owner) { ensureOwner(owner); await ensureLoaded(); return Object.values(state.index.deletedChats) .filter((record) => matchesOwner(record, owner)) .sort((a, b) => new Date(b.deletedAt).getTime() - new Date(a.deletedAt).getTime()) .map(sanitize); }, async restore(owner, ids) { ensureOwner(owner); await ensureLoaded(); const restored = []; for (const id of ids || []) { const record = state.index.deletedChats[id]; if (!record || !matchesOwner(record, owner)) continue; restored.push(record.sessionSnapshot); delete state.index.deletedChats[id]; } if (restored.length) await saveIndex(); return restored; }, async deleteForever(owner, ids) { ensureOwner(owner); await ensureLoaded(); const removed = []; for (const id of ids || []) { const record = state.index.deletedChats[id]; if (!record || !matchesOwner(record, owner)) continue; delete state.index.deletedChats[id]; removed.push(id); } if (removed.length) await saveIndex(); return removed; }, async purgeExpired() { if (!state.loaded) return; const now = Date.now(); let changed = false; for (const [id, record] of Object.entries(state.index.deletedChats)) { if (new Date(record.purgeAt).getTime() <= now) { delete state.index.deletedChats[id]; changed = true; } } if (changed) await saveIndex(); }, }; setInterval(() => { thisStore.purgeExpired().catch((err) => console.error('chatTrashStore cleanup failed:', err)); }, 6 * 60 * 60 * 1000); export const chatTrashStore = thisStore;