| 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; |
|
|