coyotte508
A new start
fc69895
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;
}
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, // login feature is on when OID is configured
isAdmin: locals.isAdmin,
} 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), // 1 hour
});
}
});
// @ts-expect-error - zipfile.outputStream is not typed correctly
return new Response(zipfile.outputStream, {
headers: {
"Content-Type": "application/zip",
"Content-Disposition": 'attachment; filename="export.zip"',
},
});
});