|
|
import { Elysia } from "elysia"; |
|
|
import { authPlugin } from "../../authPlugin"; |
|
|
import { loginEnabled } from "$lib/server/auth"; |
|
|
import { collections } from "$lib/server/database"; |
|
|
import { authCondition } from "$lib/server/auth"; |
|
|
import { config } from "$lib/server/config"; |
|
|
import yazl from "yazl"; |
|
|
import { downloadFile } from "$lib/server/files/downloadFile"; |
|
|
import mimeTypes from "mime-types"; |
|
|
import { logger } from "$lib/server/logger"; |
|
|
|
|
|
export interface FeatureFlags { |
|
|
enableAssistants: boolean; |
|
|
loginEnabled: boolean; |
|
|
isAdmin: boolean; |
|
|
transcriptionEnabled: boolean; |
|
|
} |
|
|
|
|
|
export const misc = new Elysia() |
|
|
.use(authPlugin) |
|
|
.get("/public-config", async () => config.getPublicConfig()) |
|
|
.get("/feature-flags", async ({ locals }) => { |
|
|
return { |
|
|
enableAssistants: config.ENABLE_ASSISTANTS === "true", |
|
|
loginEnabled, |
|
|
isAdmin: locals.isAdmin, |
|
|
transcriptionEnabled: !!config.get("TRANSCRIPTION_MODEL"), |
|
|
} satisfies FeatureFlags; |
|
|
}) |
|
|
.get("/export", async ({ locals }) => { |
|
|
if (!locals.user) { |
|
|
throw new Error("Not logged in"); |
|
|
} |
|
|
|
|
|
if (!locals.isAdmin) { |
|
|
throw new Error("Not admin"); |
|
|
} |
|
|
|
|
|
if (config.ENABLE_DATA_EXPORT !== "true") { |
|
|
throw new Error("Data export is not enabled"); |
|
|
} |
|
|
|
|
|
const nExports = await collections.messageEvents.countDocuments({ |
|
|
userId: locals.user._id, |
|
|
type: "export", |
|
|
expiresAt: { $gt: new Date() }, |
|
|
}); |
|
|
|
|
|
if (nExports >= 1) { |
|
|
throw new Error( |
|
|
"You have already exported your data recently. Please wait 1 hour before exporting again." |
|
|
); |
|
|
} |
|
|
|
|
|
const stats: { |
|
|
nConversations: number; |
|
|
nMessages: number; |
|
|
nFiles: number; |
|
|
nAssistants: number; |
|
|
nAvatars: number; |
|
|
} = { |
|
|
nConversations: 0, |
|
|
nMessages: 0, |
|
|
nFiles: 0, |
|
|
nAssistants: 0, |
|
|
nAvatars: 0, |
|
|
}; |
|
|
|
|
|
const zipfile = new yazl.ZipFile(); |
|
|
|
|
|
const promises = [ |
|
|
collections.conversations |
|
|
.find({ ...authCondition(locals) }) |
|
|
.toArray() |
|
|
.then(async (conversations) => { |
|
|
const formattedConversations = await Promise.all( |
|
|
conversations.map(async (conversation) => { |
|
|
stats.nConversations++; |
|
|
const hashes: string[] = []; |
|
|
conversation.messages.forEach(async (message) => { |
|
|
stats.nMessages++; |
|
|
if (message.files) { |
|
|
message.files.forEach((file) => { |
|
|
hashes.push(file.value); |
|
|
}); |
|
|
} |
|
|
}); |
|
|
const files = await Promise.all( |
|
|
hashes.map(async (hash) => { |
|
|
try { |
|
|
const fileData = await downloadFile(hash, conversation._id); |
|
|
return fileData; |
|
|
} catch { |
|
|
return null; |
|
|
} |
|
|
}) |
|
|
); |
|
|
|
|
|
const filenames: string[] = []; |
|
|
files.forEach((file) => { |
|
|
if (!file) return; |
|
|
|
|
|
const extension = mimeTypes.extension(file.mime) || null; |
|
|
const convId = conversation._id.toString(); |
|
|
const fileId = file.name.split("-")[1].slice(0, 8); |
|
|
const fileName = `file-${convId}-${fileId}` + (extension ? `.${extension}` : ""); |
|
|
filenames.push(fileName); |
|
|
zipfile.addBuffer(Buffer.from(file.value, "base64"), fileName); |
|
|
stats.nFiles++; |
|
|
}); |
|
|
|
|
|
return { |
|
|
...conversation, |
|
|
messages: conversation.messages.map((message) => { |
|
|
return { |
|
|
...message, |
|
|
files: filenames, |
|
|
updates: undefined, |
|
|
}; |
|
|
}), |
|
|
}; |
|
|
}) |
|
|
); |
|
|
|
|
|
zipfile.addBuffer( |
|
|
Buffer.from(JSON.stringify(formattedConversations, null, 2)), |
|
|
"conversations.json" |
|
|
); |
|
|
}), |
|
|
collections.assistants |
|
|
.find({ createdById: locals.user._id }) |
|
|
.toArray() |
|
|
.then(async (assistants) => { |
|
|
const formattedAssistants = await Promise.all( |
|
|
assistants.map(async (assistant) => { |
|
|
if (assistant.avatar) { |
|
|
const fileId = collections.bucket.find({ filename: assistant._id.toString() }); |
|
|
|
|
|
const content = await fileId.next().then(async (file) => { |
|
|
if (!file?._id) return; |
|
|
|
|
|
const fileStream = collections.bucket.openDownloadStream(file?._id); |
|
|
|
|
|
const fileBuffer = await new Promise<Buffer>((resolve, reject) => { |
|
|
const chunks: Uint8Array[] = []; |
|
|
fileStream.on("data", (chunk) => chunks.push(chunk)); |
|
|
fileStream.on("error", reject); |
|
|
fileStream.on("end", () => resolve(Buffer.concat(chunks))); |
|
|
}); |
|
|
|
|
|
return fileBuffer; |
|
|
}); |
|
|
|
|
|
if (!content) return; |
|
|
|
|
|
zipfile.addBuffer(content, `avatar-${assistant._id.toString()}.jpg`); |
|
|
stats.nAvatars++; |
|
|
} |
|
|
|
|
|
stats.nAssistants++; |
|
|
|
|
|
return { |
|
|
_id: assistant._id.toString(), |
|
|
name: assistant.name, |
|
|
createdById: assistant.createdById.toString(), |
|
|
createdByName: assistant.createdByName, |
|
|
avatar: `avatar-${assistant._id.toString()}.jpg`, |
|
|
modelId: assistant.modelId, |
|
|
preprompt: assistant.preprompt, |
|
|
description: assistant.description, |
|
|
dynamicPrompt: assistant.dynamicPrompt, |
|
|
exampleInputs: assistant.exampleInputs, |
|
|
generateSettings: assistant.generateSettings, |
|
|
createdAt: assistant.createdAt.toISOString(), |
|
|
updatedAt: assistant.updatedAt.toISOString(), |
|
|
}; |
|
|
}) |
|
|
); |
|
|
|
|
|
zipfile.addBuffer( |
|
|
Buffer.from(JSON.stringify(formattedAssistants, null, 2)), |
|
|
"assistants.json" |
|
|
); |
|
|
}), |
|
|
]; |
|
|
|
|
|
Promise.all(promises).then(async () => { |
|
|
logger.info( |
|
|
{ |
|
|
userId: locals.user?._id, |
|
|
...stats, |
|
|
}, |
|
|
"Exported user data" |
|
|
); |
|
|
zipfile.end(); |
|
|
if (locals.user?._id) { |
|
|
await collections.messageEvents.insertOne({ |
|
|
userId: locals.user?._id, |
|
|
type: "export", |
|
|
createdAt: new Date(), |
|
|
expiresAt: new Date(Date.now() + 1000 * 60 * 60), |
|
|
}); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
return new Response(zipfile.outputStream, { |
|
|
headers: { |
|
|
"Content-Type": "application/zip", |
|
|
"Content-Disposition": 'attachment; filename="export.zip"', |
|
|
}, |
|
|
}); |
|
|
}); |
|
|
|