File size: 4,116 Bytes
0f84d64 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 | 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;
|