325 lines
11 KiB
TypeScript
325 lines
11 KiB
TypeScript
import { createHash } from "crypto";
|
|
import fs from "fs";
|
|
import { Plugins as PluginsType } from "#type/db"; // Renamed to avoid conflict
|
|
import { compareSemanticVersions } from "#Modules/semantic";
|
|
import store from "../types/store"; // Assuming this type is correctly defined
|
|
import dotenv from "dotenv";
|
|
import { finished } from "stream/promises";
|
|
import { Readable } from "stream";
|
|
import { ReadableStream } from "stream/web";
|
|
import { createConfig, migrateToLatest } from "#scripts/migrate";
|
|
import db from "#database"; // Kysely instance
|
|
import { Selectable } from "kysely";
|
|
|
|
// Load environment variables
|
|
if (process.env.APP_ENV !== "test") {
|
|
dotenv.config({ path: ".env.local" });
|
|
} else {
|
|
dotenv.config({ path: ".env.test.local" });
|
|
}
|
|
// Downloads and generates all the plugin code
|
|
async function compilePlugins() {
|
|
console.log("Compiling plugins");
|
|
let mainFileTextStart = `// Note that this file is autogenerated by startup.ts DO NOT EDIT\nimport dataGetter from "#type/data";\n`;
|
|
let mainFileText =
|
|
"export const plugins: { [key: string]: dataGetter } = {\n";
|
|
|
|
const request =
|
|
process.env.APP_ENV !== "test"
|
|
? await fetch(
|
|
"https://git.lschaefer.xyz/lukasdotcom/fantasy-manager/raw/branch/main/store/default_store.json",
|
|
).catch((err) => {
|
|
console.error("Could not get the default store:", err);
|
|
return "error" as const; // Ensure type is 'error' literal
|
|
})
|
|
: ("error" as const);
|
|
|
|
const defaultStore: string[] =
|
|
request instanceof Response
|
|
? await request.json().catch((err) => {
|
|
console.error("Could not parse the default store JSON:", err);
|
|
return [];
|
|
})
|
|
: // Uses a fallback store if the request fails(this is also the testing store)
|
|
[
|
|
"https://git.lschaefer.xyz/lukasdotcom/fantasy-manager/raw/branch/main/store/Bundesliga_Testing/Bundesliga_Testing.json",
|
|
];
|
|
|
|
// Installs all plugins that should be installed by default
|
|
if (defaultStore.length > 0) {
|
|
await db
|
|
.insertInto("plugins")
|
|
.onConflict((eb) => eb.doNothing())
|
|
.values(
|
|
defaultStore.map((pluginUrl) => ({
|
|
name: "",
|
|
settings: "{}",
|
|
enabled: 0,
|
|
installed: 0,
|
|
url: pluginUrl,
|
|
})),
|
|
)
|
|
.execute();
|
|
}
|
|
|
|
// Makes sure that bundesliga is enabled when testing
|
|
if (process.env.APP_ENV === "test") {
|
|
await db.updateTable("plugins").set({ enabled: 1 }).execute(); // This will enable ALL plugins
|
|
}
|
|
|
|
const currentVersion = (await import("../package.json")).default.version;
|
|
|
|
// Makes sure that the store is correct
|
|
await db
|
|
.insertInto("data")
|
|
.values({
|
|
value1: "defaultStore",
|
|
value2: JSON.stringify(defaultStore),
|
|
})
|
|
.onConflict((oc) =>
|
|
oc.column("value1").doUpdateSet({ value2: JSON.stringify(defaultStore) }),
|
|
)
|
|
.execute();
|
|
|
|
const pluginRecords: Selectable<PluginsType>[] = await db
|
|
.selectFrom("plugins")
|
|
.selectAll()
|
|
.execute();
|
|
|
|
await Promise.all(
|
|
pluginRecords.map(async (pluginEntry) => {
|
|
const dataResponse = await fetch(pluginEntry.url).catch(
|
|
() => "error" as const,
|
|
);
|
|
if (dataResponse === "error" || !(dataResponse instanceof Response)) {
|
|
console.log(`Failed to fetch plugin data from ${pluginEntry.url}`);
|
|
return;
|
|
}
|
|
|
|
const jsonStore: store | "error" = await dataResponse
|
|
.json()
|
|
.catch(() => "error" as const);
|
|
if (jsonStore === "error") {
|
|
console.log(`Failed to parse JSON for plugin at ${pluginEntry.url}`);
|
|
return;
|
|
}
|
|
|
|
// Update plugin name in DB
|
|
await db
|
|
.updateTable("plugins")
|
|
.set({ name: jsonStore.id })
|
|
.where("url", "=", pluginEntry.url)
|
|
.execute();
|
|
pluginEntry.name = jsonStore.id; // Update local copy
|
|
|
|
// Makes sure the plugin is compatible with the current version
|
|
if (
|
|
compareSemanticVersions(
|
|
jsonStore.min_version || "0.0.1",
|
|
currentVersion,
|
|
) === -1
|
|
) {
|
|
console.error(
|
|
`Plugin ${pluginEntry.name} (min_version: ${
|
|
jsonStore.min_version || "0.0.1"
|
|
}) is not compatible with the current program version ${currentVersion}. Disabling.`,
|
|
);
|
|
await db
|
|
.updateTable("plugins")
|
|
.set({ version: "", enabled: 0, installed: 0 })
|
|
.where("url", "=", pluginEntry.url)
|
|
.execute();
|
|
return;
|
|
}
|
|
|
|
const hash = createHash("sha256").update(pluginEntry.url).digest("hex");
|
|
const pluginDir = `scripts/data/${hash}`;
|
|
|
|
if (
|
|
pluginEntry.version !== jsonStore.version ||
|
|
!fs.existsSync(pluginDir) || // Check directory existence
|
|
!pluginEntry.installed
|
|
) {
|
|
if (pluginEntry.version === jsonStore.version) {
|
|
console.log(
|
|
`Updating plugin ${pluginEntry.name} (version ${jsonStore.version}) files.`,
|
|
);
|
|
} else {
|
|
console.log(
|
|
`Installing plugin ${pluginEntry.name} (version ${jsonStore.version}). Old version: ${pluginEntry.version}`,
|
|
);
|
|
}
|
|
|
|
if (!fs.existsSync("scripts/data")) {
|
|
fs.mkdirSync("scripts/data");
|
|
}
|
|
if (fs.existsSync(pluginDir)) {
|
|
fs.rmSync(pluginDir, { recursive: true, force: true });
|
|
}
|
|
fs.mkdirSync(pluginDir);
|
|
|
|
try {
|
|
await Promise.all(
|
|
jsonStore.files.map(async (fileUrl) => {
|
|
const fileName = fileUrl.split("/").pop();
|
|
if (!fileName) {
|
|
console.error(
|
|
`Could not determine filename from ${fileUrl} for plugin ${pluginEntry.name}`,
|
|
);
|
|
throw new Error(`Invalid file URL in plugin store: ${fileUrl}`);
|
|
}
|
|
// __dirname points to the directory of the current module (startup.ts)
|
|
// If scripts/data is relative to project root, adjust path.
|
|
// Assuming scripts/data is at project root:
|
|
const filePath = `scripts/data/${hash}/${fileName}`;
|
|
const stream = fs.createWriteStream(filePath);
|
|
const { body, status } = await fetch(fileUrl);
|
|
if (!body || status !== 200) {
|
|
throw new Error(
|
|
`Failed to download ${fileUrl} (status: ${status})`,
|
|
);
|
|
}
|
|
await finished(
|
|
Readable.fromWeb(body as ReadableStream<Uint8Array>).pipe(
|
|
stream,
|
|
),
|
|
);
|
|
}),
|
|
);
|
|
|
|
console.log(`Finished downloading plugin ${pluginEntry.name}`);
|
|
mainFileTextStart += `import plugin${hash} from "./data/${hash}";\n`; // Assuming default export from plugin's index
|
|
mainFileText += ` "${pluginEntry.url}":\n plugin${hash},\n`;
|
|
await db
|
|
.updateTable("plugins")
|
|
.set({
|
|
version: jsonStore.version,
|
|
installed: 1,
|
|
enabled: pluginEntry.enabled ?? 0,
|
|
}) // Preserve enabled state
|
|
.where("url", "=", pluginEntry.url)
|
|
.execute();
|
|
} catch (downloadError) {
|
|
console.error(
|
|
`Failed to download files for plugin ${pluginEntry.name}. Error: ${downloadError}. Restart server to try again.`,
|
|
);
|
|
await db
|
|
.updateTable("plugins")
|
|
.set({ version: "", enabled: 0, installed: 0 })
|
|
.where("url", "=", pluginEntry.url)
|
|
.execute();
|
|
}
|
|
} else {
|
|
// Already installed and up-to-date
|
|
mainFileTextStart += `import plugin${hash} from "./data/${hash}";\n`;
|
|
mainFileText += ` "${pluginEntry.url}":\n plugin${hash},\n`;
|
|
}
|
|
}),
|
|
);
|
|
|
|
mainFileText += "};\nexport default plugins;\n";
|
|
// Ensure scripts directory exists
|
|
if (!fs.existsSync("scripts")) {
|
|
fs.mkdirSync("scripts");
|
|
}
|
|
fs.writeFileSync("scripts/data.ts", mainFileTextStart + mainFileText);
|
|
console.log("Done compiling plugins");
|
|
}
|
|
|
|
async function startUp() {
|
|
const currentProgVersion = (await import("../package.json")).default.version;
|
|
|
|
// Ensures that invalid database migration does not happen
|
|
const dbVersionEntry = await db
|
|
.selectFrom("data")
|
|
.select("value2")
|
|
.where("value1", "=", "version")
|
|
.executeTakeFirst()
|
|
.catch(() => undefined); // Handle case where 'data' table or 'version' row might not exist yet
|
|
|
|
if (dbVersionEntry) {
|
|
const storedDbVersion = dbVersionEntry.value2;
|
|
if (compareSemanticVersions(storedDbVersion, "1.20.3") === 1) {
|
|
console.error(
|
|
`Cannot upgrade from database version ${storedDbVersion} (produced before program version 1.20.3). Please upgrade to 1.20.3 first.`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
if (compareSemanticVersions(storedDbVersion, currentProgVersion) === -1) {
|
|
console.error(
|
|
`Database version ${storedDbVersion} is newer than the current program version ${currentProgVersion}. Cannot downgrade. Please use a compatible program version or database.`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
await migrateToLatest(); // Assumes this handles DB schema creation/migration
|
|
|
|
// Checks if the server hash has been created and if not makes one
|
|
const serverId =
|
|
Math.random().toString(36).substring(2) +
|
|
Math.random().toString(36).substring(2);
|
|
await db
|
|
.insertInto("data")
|
|
.onConflict((eb) => eb.doNothing())
|
|
.values({ value1: "serverID", value2: serverId })
|
|
.execute();
|
|
|
|
// Unlocks the database by removing 'locked' entries for plugins
|
|
const allPlugins: Selectable<PluginsType>[] = await db
|
|
.selectFrom("plugins")
|
|
.selectAll()
|
|
.execute();
|
|
for (const plugin of allPlugins) {
|
|
if (plugin.name) {
|
|
// Ensure plugin.name is not null or empty
|
|
await db
|
|
.deleteFrom("data")
|
|
.where("value1", "=", `locked${plugin.name}`)
|
|
.execute();
|
|
}
|
|
}
|
|
|
|
await createConfig(); // Assumes this sets up necessary config in 'data' table
|
|
|
|
// Makes sure that the admin user is the correct user
|
|
await db.updateTable("users").set({ admin: 0 }).execute(); // Reset all admins
|
|
|
|
if (process.env.ADMIN !== undefined) {
|
|
const adminUserId = parseInt(process.env.ADMIN);
|
|
if (!isNaN(adminUserId)) {
|
|
console.log(`Setting user ${adminUserId} as the admin user.`);
|
|
const result = await db
|
|
.updateTable("users")
|
|
.set({ admin: 1 })
|
|
.where("id", "=", adminUserId)
|
|
.executeTakeFirst(); // Kysely returns UpdateResult, check affected rows
|
|
if (
|
|
result &&
|
|
result.numUpdatedRows === BigInt(0) &&
|
|
result.numChangedRows === BigInt(0)
|
|
) {
|
|
console.warn(`Admin user ID ${adminUserId} not found. No admin set.`);
|
|
}
|
|
} else {
|
|
console.warn(
|
|
`Invalid ADMIN user ID in environment: ${process.env.ADMIN}. No admin set.`,
|
|
);
|
|
}
|
|
} else {
|
|
console.log("ADMIN environment variable not set. Admin user is disabled.");
|
|
}
|
|
|
|
// Updated version of database in table
|
|
await db
|
|
.insertInto("data")
|
|
.values({ value1: "version", value2: currentProgVersion })
|
|
.onConflict((oc) =>
|
|
oc.column("value1").doUpdateSet({ value2: currentProgVersion }),
|
|
)
|
|
.execute();
|
|
|
|
await compilePlugins(); // Compile plugins after DB setup and migration
|
|
}
|
|
|
|
startUp();
|