186 lines
4.9 KiB
JavaScript
186 lines
4.9 KiB
JavaScript
const fs = require("fs");
|
|
const path = require("path");
|
|
const axios = require("axios");
|
|
const { withTimeout } = require("./asyncTimeout.js");
|
|
const AdmZip = require("adm-zip");
|
|
const yaml = require("js-yaml");
|
|
|
|
const EMERALD_API = "https://emeraldsrv.dev/api/v1";
|
|
|
|
function getApiKey() {
|
|
try {
|
|
if (fs.existsSync("./Configs/api.yml")) {
|
|
const config = yaml.load(fs.readFileSync("./Configs/api.yml", "utf8"));
|
|
return config?.API?.EmeraldAPIKey || "";
|
|
}
|
|
} catch (e) {
|
|
console.error("Failed to read api.yml:", e);
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function ensureAddonDirs() {
|
|
if (!fs.existsSync("./Addons")) {
|
|
fs.mkdirSync("./Addons", { recursive: true });
|
|
}
|
|
if (!fs.existsSync("./Addons/Configs")) {
|
|
fs.mkdirSync("./Addons/Configs", { recursive: true });
|
|
}
|
|
}
|
|
|
|
async function listCatalogAddons() {
|
|
const key = getApiKey();
|
|
if (!key) throw new Error("EmeraldAPIKey not configured in api.yml");
|
|
|
|
const res = await withTimeout(
|
|
axios.get(`${EMERALD_API}/resources`, {
|
|
headers: { Authorization: `Bearer ${key}` },
|
|
timeout: 15_000,
|
|
}),
|
|
18_000,
|
|
"Emerald API request timed out",
|
|
);
|
|
|
|
if (res.data.status !== "success") {
|
|
throw new Error(res.data.message || "Failed to fetch catalog");
|
|
}
|
|
|
|
return res.data.data.map(item => ({
|
|
id: item.id.toString(),
|
|
name: item.title,
|
|
slug: item.slug,
|
|
description: `Emerald Services Resource - ${item.slug}`,
|
|
price: item.price,
|
|
is_external: item.is_external,
|
|
external_store: item.external_store,
|
|
repositoryUrl: item.external_url || `https://emeraldsrv.dev`,
|
|
jsFiles: [],
|
|
configFiles: []
|
|
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
}
|
|
|
|
async function downloadFile(id) {
|
|
const key = getApiKey();
|
|
const res = await withTimeout(
|
|
axios.get(`${EMERALD_API}/download?id=${id}`, {
|
|
headers: { Authorization: `Bearer ${key}` },
|
|
responseType: "arraybuffer",
|
|
timeout: 30_000
|
|
}),
|
|
35_000,
|
|
"Addon file download timed out",
|
|
);
|
|
return Buffer.from(res.data);
|
|
}
|
|
|
|
function listLocalAddonJs() {
|
|
ensureAddonDirs();
|
|
return fs
|
|
.readdirSync("./Addons")
|
|
.filter((f) => f.endsWith(".js") && f !== "addon.js");
|
|
}
|
|
|
|
function listLocalAddonConfigs() {
|
|
ensureAddonDirs();
|
|
const dir = "./Addons/Configs";
|
|
if (!fs.existsSync(dir)) return [];
|
|
return fs
|
|
.readdirSync(dir)
|
|
.filter((f) => f.endsWith(".yml") || f.endsWith(".yaml") || f.endsWith(".json"));
|
|
}
|
|
|
|
function isAddonInstalled(catalogEntry) {
|
|
const localJs = listLocalAddonJs();
|
|
return localJs.some(f => f.includes(catalogEntry.slug) || f.includes(catalogEntry.id.toString()) || f.includes(catalogEntry.name.replace(/ /g, '')));
|
|
}
|
|
|
|
async function listInstalledAddons(catalog) {
|
|
const localJs = listLocalAddonJs();
|
|
const installed = [];
|
|
|
|
for (const file of localJs) {
|
|
const name = file.replace(/\.js$/, "");
|
|
const match = catalog.find(c => c.slug === name || c.id.toString() === name || c.name.replace(/ /g, '') === name);
|
|
|
|
installed.push({
|
|
id: match ? match.id : name,
|
|
name: match ? match.name : name,
|
|
jsFiles: [file],
|
|
configFiles: listLocalAddonConfigs().filter(f => f.startsWith(name)),
|
|
custom: !match
|
|
});
|
|
}
|
|
return installed;
|
|
}
|
|
|
|
async function installAddonFromCatalog(addonId) {
|
|
const catalog = await listCatalogAddons();
|
|
const addon = catalog.find(c => c.id === addonId.toString());
|
|
|
|
if (!addon) {
|
|
throw new Error(`Addon not found in catalog`);
|
|
}
|
|
|
|
ensureAddonDirs();
|
|
const content = await downloadFile(addon.id);
|
|
|
|
const zip = new AdmZip(content);
|
|
zip.extractAllTo("./Addons", true);
|
|
|
|
return {
|
|
addon: addon,
|
|
filesWritten: [],
|
|
};
|
|
}
|
|
|
|
function readAddonConfig(filename) {
|
|
const safe = path.basename(filename);
|
|
if (!/^[a-zA-Z0-9_.-]+\.(yml|yaml|json)$/.test(safe)) {
|
|
throw new Error("Invalid config filename");
|
|
}
|
|
|
|
const filePath = path.join("./Addons/Configs", safe);
|
|
if (!fs.existsSync(filePath)) {
|
|
throw new Error("Addon config not found");
|
|
}
|
|
|
|
const raw = fs.readFileSync(filePath, "utf8");
|
|
if (safe.endsWith(".json")) {
|
|
return { filename: safe, format: "json", data: JSON.parse(raw), raw };
|
|
}
|
|
|
|
const yaml = require("js-yaml");
|
|
return { filename: safe, format: "yaml", data: yaml.load(raw) || {}, raw };
|
|
}
|
|
|
|
function writeAddonConfig(filename, data) {
|
|
const safe = path.basename(filename);
|
|
if (!/^[a-zA-Z0-9_.-]+\.(yml|yaml|json)$/.test(safe)) {
|
|
throw new Error("Invalid config filename");
|
|
}
|
|
|
|
ensureAddonDirs();
|
|
const filePath = path.join("./Addons/Configs", safe);
|
|
let content;
|
|
|
|
if (safe.endsWith(".json")) {
|
|
content = `${JSON.stringify(data, null, 2)}\n`;
|
|
} else {
|
|
const yaml = require("js-yaml");
|
|
content = yaml.dump(data, { lineWidth: 120, noRefs: true });
|
|
}
|
|
|
|
fs.writeFileSync(filePath, content);
|
|
return { filename: safe, path: filePath.replace(/\\/g, "/") };
|
|
}
|
|
|
|
module.exports = {
|
|
listCatalogAddons,
|
|
listInstalledAddons,
|
|
isAddonInstalled,
|
|
installAddonFromCatalog,
|
|
listLocalAddonConfigs,
|
|
readAddonConfig,
|
|
writeAddonConfig,
|
|
};
|