Dashboard 1.0 Release

This commit is contained in:
1tzEmerald committed 2026-07-26 20:33:00 +01:00
1 parent 69371ae619
commit 004b638dbf
47 files changed
+2566 -1166

No files matched your search

+62 -147
View File
@@ -2,13 +2,22 @@ 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 ADDONS_REPO = "Emerald-Services/Addons";
const ADDONS_BRANCH = "main";
const GITHUB_API = `https://api.github.com/repos/${ADDONS_REPO}/contents`;
const GITHUB_RAW = `https://raw.githubusercontent.com/${ADDONS_REPO}/${ADDONS_BRANCH}`;
const EMERALD_API = "https://emeraldsrv.dev/api/v1";
const README_PATTERN = /^readme\.md$/i;
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")) {
@@ -19,104 +28,45 @@ function ensureAddonDirs() {
}
}
async function githubList(dirPath = "") {
const url = dirPath
? `${GITHUB_API}/${encodeURIComponent(dirPath)}?ref=${ADDONS_BRANCH}`
: `${GITHUB_API}?ref=${ADDONS_BRANCH}`;
async function listCatalogAddons() {
const key = getApiKey();
if (!key) throw new Error("EmeraldAPIKey not configured in api.yml");
const res = await withTimeout(
axios.get(url, {
headers: { Accept: "application/vnd.github+json" },
axios.get(`${EMERALD_API}/resources`, {
headers: { Authorization: `Bearer ${key}` },
timeout: 15_000,
}),
18_000,
"GitHub API request timed out",
"Emerald API request timed out",
);
return res.data;
}
async function listCatalogAddons() {
const root = await githubList("");
const dirs = root.filter((entry) => entry.type === "dir");
const addons = [];
for (const dir of dirs) {
const summary = await summarizeAddon(dir.name);
if (summary) addons.push(summary);
if (res.data.status !== "success") {
throw new Error(res.data.message || "Failed to fetch catalog");
}
return addons.sort((a, b) => a.name.localeCompare(b.name));
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 summarizeAddon(name) {
const files = await listAddonRepoFiles(name);
const jsFiles = files.filter((f) => f.name.endsWith(".js"));
if (jsFiles.length === 0) return null;
const configFiles = files
.filter((f) => f.relativePath.startsWith("Configs/"))
.map((f) => f.name);
const readme = files.find((f) => README_PATTERN.test(f.name));
return {
id: name,
name,
description: readme
? `Community addon from Emerald-Services/Addons`
: `Addon from ${name}`,
repositoryUrl: `https://github.com/${ADDONS_REPO}/tree/${ADDONS_BRANCH}/${name}`,
jsFiles: jsFiles.map((f) => f.name),
configFiles,
fileCount: files.length,
};
}
async function listAddonRepoFiles(addonName) {
const files = [];
const queue = [addonName];
while (queue.length) {
const dir = queue.shift();
const entries = await githubList(dir);
for (const entry of entries) {
if (entry.type === "dir") {
queue.push(entry.path);
continue;
}
if (entry.type !== "file") continue;
if (README_PATTERN.test(entry.name)) continue;
const relativePath = entry.path.slice(`${addonName}/`.length);
files.push({
name: entry.name,
path: entry.path,
relativePath,
downloadUrl: entry.download_url,
size: entry.size,
});
}
}
return files;
}
function mapRepoFileToLocal(addonName, relativePath) {
const parts = relativePath.split("/").filter(Boolean);
if (parts[0] === "Configs") {
return path.join("Addons", "Configs", ...parts.slice(1));
}
if (parts.length === 1 && parts[0].endsWith(".js")) {
return path.join("Addons", parts[0]);
}
return path.join("Addons", ...parts);
}
async function downloadFile(url) {
async function downloadFile(id) {
const key = getApiKey();
const res = await withTimeout(
axios.get(url, { responseType: "arraybuffer", timeout: 30_000 }),
axios.get(`${EMERALD_API}/download?id=${id}`, {
headers: { Authorization: `Bearer ${key}` },
responseType: "arraybuffer",
timeout: 30_000
}),
35_000,
"Addon file download timed out",
);
@@ -141,78 +91,45 @@ function listLocalAddonConfigs() {
function isAddonInstalled(catalogEntry) {
const localJs = listLocalAddonJs();
return catalogEntry.jsFiles.every((file) => localJs.includes(file));
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 entry of catalog) {
const allPresent = entry.jsFiles.every((file) => localJs.includes(file));
if (allPresent) {
installed.push({
id: entry.id,
name: entry.name,
jsFiles: entry.jsFiles,
configFiles: entry.configFiles.filter((f) => {
const local = listLocalAddonConfigs();
return local.includes(f);
}),
});
}
}
const orphanJs = localJs.filter(
(file) => !installed.some((a) => a.jsFiles.includes(file)),
);
for (const file of orphanJs) {
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: file.replace(/\.js$/, ""),
name: file.replace(/\.js$/, ""),
id: match ? match.id : name,
name: match ? match.name : name,
jsFiles: [file],
configFiles: [],
custom: true,
configFiles: listLocalAddonConfigs().filter(f => f.startsWith(name)),
custom: !match
});
}
return installed;
}
async function installAddonFromCatalog(addonName) {
const safeName = String(addonName).replace(/[^a-zA-Z0-9_-]/g, "");
if (!safeName) {
throw new Error("Invalid addon name");
}
const summary = await summarizeAddon(safeName);
if (!summary) {
throw new Error(`Addon "${safeName}" was not found in the catalog`);
}
const files = await listAddonRepoFiles(safeName);
if (files.length === 0) {
throw new Error(`Addon "${safeName}" has no installable files`);
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 written = [];
for (const file of files) {
const localPath = mapRepoFileToLocal(safeName, file.relativePath);
const dir = path.dirname(localPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const content = await downloadFile(file.downloadUrl);
fs.writeFileSync(localPath, content);
written.push(localPath.replace(/\\/g, "/"));
}
const content = await downloadFile(addon.id);
const zip = new AdmZip(content);
zip.extractAllTo("./Addons", true);
return {
addon: summary,
filesWritten: written,
addon: addon,
filesWritten: [],
};
}
@@ -258,7 +175,6 @@ function writeAddonConfig(filename, data) {
}
module.exports = {
ADDONS_REPO,
listCatalogAddons,
listInstalledAddons,
isAddonInstalled,
@@ -266,5 +182,4 @@ module.exports = {
listLocalAddonConfigs,
readAddonConfig,
writeAddonConfig,
mapRepoFileToLocal,
};
+6
View File
@@ -91,6 +91,12 @@ async function logStartup(client, options = {}) {
section("Links");
info("Docs", "https://emerald-services.gitbook.io/");
info("Discord", "https://dsc.gg/emerald-dev");
if (configStore.api?.API?.Enabled) {
const port = configStore.api?.API?.Port || 3000;
info("Dashboard", `http://localhost:${port}`);
}
info("Invite", inviteUrl);
console.log("");
+1
View File
@@ -4,6 +4,7 @@ const CONFIG_FILES = [
"commands",
"messages",
"supportbot-ai",
"api",
];
const ROLES = ["owner", "admin", "moderator", "editor", "viewer", "custom"];
+19
View File
@@ -119,6 +119,12 @@ function buildSteps(apiRoot, supportbot) {
description: "Discord user IDs that can sign in and manage the dashboard (at least one).",
complete: getOwnerIds(oauth).length > 0 && getOwnerIds(oauth).every(isDiscordSnowflake),
},
{
id: "emeraldApi",
title: "Emerald API",
description: "Optional. Used for the one-click addon installer.",
complete: true,
},
];
}
@@ -350,6 +356,16 @@ async function validateSetupPayload(payload) {
if (payload.ownerUserIds != null) {
results.owners = await validateOwnerUserIds(payload.ownerUserIds, payload.botToken);
}
if (payload.emeraldApiKey != null) {
const key = String(payload.emeraldApiKey).trim();
if (key === "") {
results.emeraldApi = { ok: true };
} else if (key.length < 10) {
results.emeraldApi = { ok: false, error: "Invalid API key." };
} else {
results.emeraldApi = { ok: true };
}
}
const allOk = Object.values(results).every((r) => r?.ok);
return { ok: allOk, results };
@@ -381,6 +397,9 @@ function applySetup(payload) {
apiDoc.setIn(["API", "Enabled"], true);
apiDoc.setIn(["API", "SecretKey"], String(payload.secretKey).trim());
if (payload.emeraldApiKey != null) {
apiDoc.setIn(["API", "EmeraldAPIKey"], String(payload.emeraldApiKey).trim());
}
if (payload.port != null && payload.port !== "") {
apiDoc.setIn(["API", "Port"], Number(payload.port) || 3000);
}
+40 -22
View File
@@ -1,8 +1,10 @@
const fs = require("fs");
const path = require("path");
const UPDATE_REPO = "C-h-a-r/SupportBot-Dashboard";
const UPDATE_BRANCH = "release";
const REPOSITORIES = [
{ id: "bot", name: "SupportBot", repo: "Emerald-Services/SupportBot", branch: "release" },
{ id: "dashboard", name: "SupportBot Dashboard", repo: "Emerald-Services/SupportBot-Dashboard", branch: "release" }
];
function compareVersions(a, b) {
const pa = String(a)
@@ -24,32 +26,48 @@ async function fetchUpdateInfo() {
const axios = require("axios");
const pkgPath = path.join(__dirname, "../package.json");
const currentPkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
const current = currentPkg.version;
const currentVersion = currentPkg.version;
const remoteRes = await axios.get(
`https://raw.githubusercontent.com/${UPDATE_REPO}/${UPDATE_BRANCH}/package.json`,
{
timeout: 15000,
headers: { "User-Agent": "SupportBot-Dashboard" },
},
);
const checks = [];
const latest = remoteRes.data?.version || current;
const updateAvailable = compareVersions(latest, current) > 0;
for (const target of REPOSITORIES) {
let installedVersion = target.id === "bot" ? currentPkg.version : (currentPkg.dashboardVersion || "1.0.0");
let latest = installedVersion;
try {
const pkgUrl = target.id === 'dashboard'
? `https://raw.githubusercontent.com/${target.repo}/${target.branch}/dashboard/package.json`
: `https://raw.githubusercontent.com/${target.repo}/${target.branch}/package.json`;
const remoteRes = await axios.get(pkgUrl, {
timeout: 15000,
headers: { "User-Agent": "SupportBot-Updater" },
});
if (remoteRes.data && remoteRes.data.version) {
latest = remoteRes.data.version;
}
} catch (e) {
console.warn(`[Updater] Failed to check ${target.repo} for updates.`);
}
return {
current,
latest,
updateAvailable,
repository: `https://github.com/${UPDATE_REPO}`,
branch: UPDATE_BRANCH,
zipUrl: `https://github.com/${UPDATE_REPO}/archive/refs/heads/${UPDATE_BRANCH}.zip`,
};
const updateAvailable = compareVersions(latest, installedVersion) > 0;
checks.push({
id: target.id,
name: target.name,
current: installedVersion,
latest,
updateAvailable,
repository: `https://github.com/${target.repo}`,
branch: target.branch,
zipUrl: `https://github.com/${target.repo}/archive/refs/heads/${target.branch}.zip`,
});
}
return checks;
}
module.exports = {
compareVersions,
fetchUpdateInfo,
UPDATE_REPO,
UPDATE_BRANCH,
REPOSITORIES,
};