2026-05-26 20:11:22 +01:00
|
|
|
const fs = require("fs");
|
|
|
|
|
const path = require("path");
|
|
|
|
|
const axios = require("axios");
|
|
|
|
|
const YAML = require("yaml");
|
|
|
|
|
const configStore = require("./ConfigStore.js");
|
|
|
|
|
const { CALLBACK_PATH } = require("./DiscordOAuth.js");
|
|
|
|
|
|
|
|
|
|
const DISCORD_API = "https://discord.com/api/v10";
|
|
|
|
|
const CONFIG_DIR = path.join(__dirname, "../Configs");
|
|
|
|
|
const API_FILE = path.join(CONFIG_DIR, "api.yml");
|
|
|
|
|
const SUPPORTBOT_FILE = path.join(CONFIG_DIR, "supportbot.yml");
|
|
|
|
|
|
|
|
|
|
const PLACEHOLDER_PATTERNS = [
|
|
|
|
|
/DISCORD_CLIENT_ID/i,
|
|
|
|
|
/DISCORD_CLIENT_SECRET/i,
|
|
|
|
|
/YOUR_DISCORD_USER_ID/i,
|
|
|
|
|
/CHANGE_ME/i,
|
|
|
|
|
/BOT_TOKEN/i,
|
|
|
|
|
/PUT_RANDOM_STRING/i,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function isEmpty(value) {
|
|
|
|
|
if (value == null) return true;
|
|
|
|
|
if (typeof value === "boolean") return false;
|
|
|
|
|
if (typeof value === "number") return false;
|
|
|
|
|
return String(value).trim() === "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isPlaceholder(value) {
|
|
|
|
|
if (isEmpty(value)) return true;
|
|
|
|
|
const s = String(value).trim();
|
|
|
|
|
return PLACEHOLDER_PATTERNS.some((p) => p.test(s));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isDiscordSnowflake(value) {
|
|
|
|
|
return /^\d{17,20}$/.test(String(value).trim());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isValidBotToken(value) {
|
|
|
|
|
if (isPlaceholder(value)) return false;
|
|
|
|
|
const s = String(value).trim();
|
|
|
|
|
return s.length >= 50 && s.includes(".");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isValidSecretKey(value) {
|
|
|
|
|
if (isPlaceholder(value)) return false;
|
|
|
|
|
return String(value).trim().length >= 16;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readApiRoot() {
|
|
|
|
|
if (!fs.existsSync(API_FILE)) return null;
|
|
|
|
|
try {
|
|
|
|
|
const raw = YAML.parse(fs.readFileSync(API_FILE, "utf8"));
|
|
|
|
|
return raw?.API ?? raw ?? null;
|
|
|
|
|
} catch {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readSupportbotRoot() {
|
|
|
|
|
if (!fs.existsSync(SUPPORTBOT_FILE)) return {};
|
|
|
|
|
try {
|
|
|
|
|
return YAML.parse(fs.readFileSync(SUPPORTBOT_FILE, "utf8")) || {};
|
|
|
|
|
} catch {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getOwnerIds(oauth) {
|
|
|
|
|
const ids = [];
|
|
|
|
|
for (const id of oauth?.OwnerUserIds || []) {
|
|
|
|
|
const s = String(id).trim();
|
|
|
|
|
if (s) ids.push(s);
|
|
|
|
|
}
|
|
|
|
|
if (ids.length === 0) {
|
|
|
|
|
for (const id of oauth?.AllowedUserIds || []) {
|
|
|
|
|
const s = String(id).trim();
|
|
|
|
|
if (s) ids.push(s);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ids;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildSteps(apiRoot, supportbot) {
|
|
|
|
|
const oauth = apiRoot?.OAuth || {};
|
|
|
|
|
const token = supportbot?.General?.Token;
|
2026-08-10 20:15:00 +01:00
|
|
|
const dbConfig = supportbot?.Database || {};
|
2026-05-26 20:11:22 +01:00
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
{
|
|
|
|
|
id: "botToken",
|
|
|
|
|
title: "Bot token",
|
|
|
|
|
description: "From the Discord Developer Portal → your application → Bot → Reset Token.",
|
|
|
|
|
complete: isValidBotToken(token),
|
|
|
|
|
},
|
2026-08-10 20:15:00 +01:00
|
|
|
{
|
|
|
|
|
id: "database",
|
|
|
|
|
title: "Database storage",
|
|
|
|
|
description: "Choose SQLite or MySQL database engine for storing bot data.",
|
|
|
|
|
complete: Boolean(dbConfig.Driver),
|
|
|
|
|
},
|
2026-05-26 20:11:22 +01:00
|
|
|
{
|
|
|
|
|
id: "secretKey",
|
|
|
|
|
title: "Dashboard session secret",
|
|
|
|
|
description: "Random string used to sign login cookies. Keep it private.",
|
|
|
|
|
complete: isValidSecretKey(apiRoot?.SecretKey),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "oauth",
|
|
|
|
|
title: "Discord OAuth app",
|
|
|
|
|
description: "Same application as your bot — Client ID and Client Secret from OAuth2.",
|
|
|
|
|
complete:
|
|
|
|
|
!isPlaceholder(oauth.ClientId) &&
|
|
|
|
|
!isPlaceholder(oauth.ClientSecret) &&
|
|
|
|
|
isDiscordSnowflake(oauth.ClientId),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "redirectUri",
|
|
|
|
|
title: "OAuth redirect URL",
|
|
|
|
|
description: `Add this exact URL under OAuth2 → Redirects in the Discord portal.`,
|
|
|
|
|
complete: isValidRedirectUriConfigured(oauth.RedirectUri),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "owners",
|
|
|
|
|
title: "Dashboard owners",
|
|
|
|
|
description: "Discord user IDs that can sign in and manage the dashboard (at least one).",
|
|
|
|
|
complete: getOwnerIds(oauth).length > 0 && getOwnerIds(oauth).every(isDiscordSnowflake),
|
|
|
|
|
},
|
2026-07-26 20:33:00 +01:00
|
|
|
{
|
|
|
|
|
id: "emeraldApi",
|
|
|
|
|
title: "Emerald API",
|
|
|
|
|
description: "Optional. Used for the one-click addon installer.",
|
|
|
|
|
complete: true,
|
|
|
|
|
},
|
2026-05-26 20:11:22 +01:00
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isValidRedirectUriConfigured(uri) {
|
|
|
|
|
if (isPlaceholder(uri)) return false;
|
|
|
|
|
try {
|
|
|
|
|
const u = new URL(String(uri).trim());
|
|
|
|
|
if (!["http:", "https:"].includes(u.protocol)) return false;
|
|
|
|
|
return u.pathname.replace(/\/$/, "") === CALLBACK_PATH.replace(/\/$/, "");
|
|
|
|
|
} catch {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isSetupComplete() {
|
|
|
|
|
const apiRoot = readApiRoot();
|
|
|
|
|
if (!apiRoot?.Enabled) return false;
|
|
|
|
|
const oauth = apiRoot.OAuth || {};
|
|
|
|
|
if (!oauth.Enabled) return false;
|
|
|
|
|
const steps = buildSteps(apiRoot, readSupportbotRoot());
|
|
|
|
|
return steps.every((s) => s.complete);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getSetupStatus(req) {
|
|
|
|
|
const apiRoot = readApiRoot() || {};
|
|
|
|
|
const steps = buildSteps(apiRoot, readSupportbotRoot());
|
|
|
|
|
const complete = steps.every((s) => s.complete);
|
|
|
|
|
|
|
|
|
|
let suggestedRedirectUri = null;
|
|
|
|
|
if (req) {
|
|
|
|
|
const proto =
|
|
|
|
|
req.get("x-forwarded-proto")?.split(",")[0]?.trim() ||
|
|
|
|
|
req.protocol ||
|
|
|
|
|
"http";
|
|
|
|
|
const host = req.get("x-forwarded-host")?.split(",")[0]?.trim() || req.get("host");
|
|
|
|
|
if (host) {
|
|
|
|
|
suggestedRedirectUri = `${proto}://${host}${CALLBACK_PATH}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (!suggestedRedirectUri) {
|
|
|
|
|
suggestedRedirectUri = `http://localhost:${apiRoot.Port || 3000}${CALLBACK_PATH}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
complete,
|
|
|
|
|
steps,
|
|
|
|
|
suggestedRedirectUri,
|
|
|
|
|
oauthCallbackPath: CALLBACK_PATH,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function validateBotToken(token) {
|
|
|
|
|
const trimmed = String(token || "").trim();
|
|
|
|
|
if (!isValidBotToken(trimmed)) {
|
|
|
|
|
return { ok: false, error: "Enter a valid bot token from the Discord Developer Portal." };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const [userRes, appRes] = await Promise.all([
|
|
|
|
|
axios.get(`${DISCORD_API}/users/@me`, {
|
|
|
|
|
headers: { Authorization: `Bot ${trimmed}` },
|
|
|
|
|
timeout: 12_000,
|
|
|
|
|
}),
|
|
|
|
|
axios.get(`${DISCORD_API}/oauth2/applications/@me`, {
|
|
|
|
|
headers: { Authorization: `Bot ${trimmed}` },
|
|
|
|
|
timeout: 12_000,
|
|
|
|
|
}),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
bot: {
|
|
|
|
|
id: userRes.data.id,
|
|
|
|
|
username: userRes.data.username,
|
|
|
|
|
discriminator: userRes.data.discriminator,
|
|
|
|
|
},
|
|
|
|
|
applicationId: String(appRes.data.id),
|
|
|
|
|
};
|
|
|
|
|
} catch (err) {
|
|
|
|
|
const status = err.response?.status;
|
|
|
|
|
if (status === 401) {
|
|
|
|
|
return { ok: false, error: "Discord rejected this bot token (401 Unauthorized)." };
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
error: err.response?.data?.message || err.message || "Could not verify bot token.",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function validateOAuthCredentials(clientId, clientSecret, botToken) {
|
|
|
|
|
const id = String(clientId || "").trim();
|
|
|
|
|
const secret = String(clientSecret || "").trim();
|
|
|
|
|
|
|
|
|
|
if (!isDiscordSnowflake(id)) {
|
|
|
|
|
return { ok: false, error: "Client ID must be a numeric Discord application ID." };
|
|
|
|
|
}
|
|
|
|
|
if (isPlaceholder(secret) || secret.length < 20) {
|
|
|
|
|
return { ok: false, error: "Enter the OAuth Client Secret from the Discord portal." };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (botToken) {
|
|
|
|
|
const botCheck = await validateBotToken(botToken);
|
|
|
|
|
if (botCheck.ok && botCheck.applicationId && botCheck.applicationId !== id) {
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
error: `Client ID does not match the bot application (${botCheck.applicationId}). Use the same app's credentials.`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await axios.post(
|
|
|
|
|
`${DISCORD_API}/oauth2/token`,
|
|
|
|
|
new URLSearchParams({ grant_type: "client_credentials", scope: "identify" }),
|
|
|
|
|
{
|
|
|
|
|
auth: { username: id, password: secret },
|
|
|
|
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
|
|
|
timeout: 12_000,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
return { ok: true, clientId: id };
|
|
|
|
|
} catch (err) {
|
|
|
|
|
const status = err.response?.status;
|
|
|
|
|
if (botToken) {
|
|
|
|
|
const botCheck = await validateBotToken(botToken);
|
|
|
|
|
if (botCheck.ok && botCheck.applicationId === id) {
|
|
|
|
|
return {
|
|
|
|
|
ok: true,
|
|
|
|
|
clientId: id,
|
|
|
|
|
warning:
|
|
|
|
|
"Could not verify the client secret with Discord (token endpoint). ID matches your bot — double-check the secret in the portal.",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (status === 401 || status === 400) {
|
|
|
|
|
return { ok: false, error: "Invalid Client ID or Client Secret." };
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
error: err.response?.data?.error_description || err.message || "OAuth validation failed.",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateRedirectUri(uri) {
|
|
|
|
|
const trimmed = String(uri || "").trim();
|
|
|
|
|
if (!isValidRedirectUriConfigured(trimmed)) {
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
error: `Redirect URL must end with ${CALLBACK_PATH} and use http or https.`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return { ok: true, redirectUri: trimmed };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function validateOwnerUserIds(ownerIds, botToken) {
|
|
|
|
|
const ids = (Array.isArray(ownerIds) ? ownerIds : [ownerIds])
|
|
|
|
|
.map((id) => String(id).trim())
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
|
|
|
|
if (ids.length === 0) {
|
|
|
|
|
return { ok: false, error: "Add at least one Discord user ID." };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const invalid = ids.filter((id) => !isDiscordSnowflake(id));
|
|
|
|
|
if (invalid.length) {
|
|
|
|
|
return { ok: false, error: `Invalid user ID: ${invalid[0]}` };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!botToken || !isValidBotToken(botToken)) {
|
|
|
|
|
return { ok: true, ownerUserIds: ids, warning: "User IDs format OK (bot token not verified yet)." };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const users = [];
|
|
|
|
|
try {
|
|
|
|
|
for (const id of ids) {
|
|
|
|
|
const res = await axios.get(`${DISCORD_API}/users/${id}`, {
|
|
|
|
|
headers: { Authorization: `Bot ${String(botToken).trim()}` },
|
|
|
|
|
timeout: 12_000,
|
|
|
|
|
});
|
|
|
|
|
users.push({
|
|
|
|
|
id: res.data.id,
|
|
|
|
|
username: res.data.username,
|
|
|
|
|
globalName: res.data.global_name || null,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return { ok: true, ownerUserIds: ids, users };
|
|
|
|
|
} catch (err) {
|
|
|
|
|
if (err.response?.status === 404) {
|
|
|
|
|
return { ok: false, error: "One or more user IDs were not found. Check Developer Mode → Copy User ID." };
|
|
|
|
|
}
|
|
|
|
|
if (err.response?.status === 403) {
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
error: "Bot could not look up users. Ensure the bot is in a server with those users, or verify IDs manually.",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return { ok: false, error: err.message || "Failed to verify owner user IDs." };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function validateSecretKey(secretKey) {
|
|
|
|
|
if (!isValidSecretKey(secretKey)) {
|
|
|
|
|
return { ok: false, error: "Use at least 16 characters (not CHANGE_ME)." };
|
|
|
|
|
}
|
|
|
|
|
return { ok: true };
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-10 20:15:00 +01:00
|
|
|
async function validateDatabase(dbDriver, dbHost, dbPort, dbUser, dbPassword, dbDatabase) {
|
|
|
|
|
const driver = String(dbDriver || "sqlite").toLowerCase();
|
|
|
|
|
if (driver === "mysql" || driver === "mariadb") {
|
|
|
|
|
const host = String(dbHost || "").trim();
|
|
|
|
|
const port = Number(dbPort) || 3306;
|
|
|
|
|
const user = String(dbUser || "").trim();
|
|
|
|
|
const password = String(dbPassword || "");
|
|
|
|
|
const database = String(dbDatabase || "").trim();
|
|
|
|
|
|
|
|
|
|
if (isEmpty(host)) return { ok: false, error: "MySQL Host is required." };
|
|
|
|
|
if (isEmpty(user)) return { ok: false, error: "MySQL User is required." };
|
|
|
|
|
if (isEmpty(database)) return { ok: false, error: "MySQL Database Name is required." };
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const mysql = require("mysql2/promise");
|
|
|
|
|
const connection = await mysql.createConnection({
|
|
|
|
|
host,
|
|
|
|
|
port,
|
|
|
|
|
user,
|
|
|
|
|
password,
|
|
|
|
|
database,
|
|
|
|
|
connectTimeout: 5000,
|
|
|
|
|
});
|
|
|
|
|
await connection.end();
|
|
|
|
|
return { ok: true, driver: "mysql" };
|
|
|
|
|
} catch (err) {
|
|
|
|
|
return {
|
|
|
|
|
ok: false,
|
|
|
|
|
error: `MySQL Connection Failed: ${err.message || err.code || "Could not connect to database host."}`,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return { ok: true, driver: "sqlite" };
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-26 20:11:22 +01:00
|
|
|
async function validateSetupPayload(payload) {
|
|
|
|
|
const results = {};
|
|
|
|
|
|
|
|
|
|
if (payload.botToken != null) {
|
|
|
|
|
results.botToken = await validateBotToken(payload.botToken);
|
|
|
|
|
}
|
2026-08-10 20:15:00 +01:00
|
|
|
if (payload.dbDriver != null) {
|
|
|
|
|
results.database = await validateDatabase(
|
|
|
|
|
payload.dbDriver,
|
|
|
|
|
payload.dbHost,
|
|
|
|
|
payload.dbPort,
|
|
|
|
|
payload.dbUser,
|
|
|
|
|
payload.dbPassword,
|
|
|
|
|
payload.dbDatabase,
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-05-26 20:11:22 +01:00
|
|
|
if (payload.secretKey != null) {
|
|
|
|
|
results.secretKey = validateSecretKey(payload.secretKey);
|
|
|
|
|
}
|
|
|
|
|
if (payload.clientId != null && payload.clientSecret != null) {
|
|
|
|
|
results.oauth = await validateOAuthCredentials(
|
|
|
|
|
payload.clientId,
|
|
|
|
|
payload.clientSecret,
|
|
|
|
|
payload.botToken,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (payload.redirectUri != null) {
|
|
|
|
|
results.redirectUri = validateRedirectUri(payload.redirectUri);
|
|
|
|
|
}
|
|
|
|
|
if (payload.ownerUserIds != null) {
|
|
|
|
|
results.owners = await validateOwnerUserIds(payload.ownerUserIds, payload.botToken);
|
|
|
|
|
}
|
2026-07-26 20:33:00 +01:00
|
|
|
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 };
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-26 20:11:22 +01:00
|
|
|
|
|
|
|
|
const allOk = Object.values(results).every((r) => r?.ok);
|
|
|
|
|
return { ok: allOk, results };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ensureApiFile() {
|
|
|
|
|
if (fs.existsSync(API_FILE)) return;
|
|
|
|
|
const example = path.join(CONFIG_DIR, "api.yml.example");
|
|
|
|
|
if (fs.existsSync(example)) {
|
|
|
|
|
fs.copyFileSync(example, API_FILE);
|
|
|
|
|
configStore.reloadFile("api");
|
|
|
|
|
} else {
|
|
|
|
|
throw new Error("Configs/api.yml is missing. Copy Configs/api.yml.example to api.yml.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applySetup(payload) {
|
|
|
|
|
ensureApiFile();
|
|
|
|
|
|
|
|
|
|
const ownerIds = (Array.isArray(payload.ownerUserIds)
|
|
|
|
|
? payload.ownerUserIds
|
|
|
|
|
: String(payload.ownerUserIds || "").split(/[\s,]+/)
|
|
|
|
|
)
|
|
|
|
|
.map((id) => String(id).trim())
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
|
|
|
|
const apiDoc = YAML.parseDocument(fs.readFileSync(API_FILE, "utf8"));
|
|
|
|
|
if (!apiDoc.get("API")) apiDoc.set("API", {});
|
|
|
|
|
|
|
|
|
|
apiDoc.setIn(["API", "Enabled"], true);
|
|
|
|
|
apiDoc.setIn(["API", "SecretKey"], String(payload.secretKey).trim());
|
2026-07-26 20:33:00 +01:00
|
|
|
if (payload.emeraldApiKey != null) {
|
|
|
|
|
apiDoc.setIn(["API", "EmeraldAPIKey"], String(payload.emeraldApiKey).trim());
|
|
|
|
|
}
|
2026-05-26 20:11:22 +01:00
|
|
|
if (payload.port != null && payload.port !== "") {
|
|
|
|
|
apiDoc.setIn(["API", "Port"], Number(payload.port) || 3000);
|
|
|
|
|
}
|
|
|
|
|
if (!apiDoc.getIn(["API", "OAuth"])) apiDoc.setIn(["API", "OAuth"], {});
|
|
|
|
|
|
|
|
|
|
apiDoc.setIn(["API", "OAuth", "Enabled"], true);
|
|
|
|
|
apiDoc.setIn(["API", "OAuth", "ClientId"], String(payload.clientId).trim());
|
|
|
|
|
apiDoc.setIn(["API", "OAuth", "ClientSecret"], String(payload.clientSecret).trim());
|
|
|
|
|
apiDoc.setIn(["API", "OAuth", "RedirectUri"], String(payload.redirectUri).trim());
|
|
|
|
|
apiDoc.setIn(["API", "OAuth", "OwnerUserIds"], ownerIds);
|
|
|
|
|
apiDoc.setIn(["API", "OAuth", "AllowedUserIds"], []);
|
|
|
|
|
|
|
|
|
|
fs.writeFileSync(API_FILE, apiDoc.toString());
|
|
|
|
|
|
|
|
|
|
const sbDoc = YAML.parseDocument(fs.readFileSync(SUPPORTBOT_FILE, "utf8"));
|
2026-08-10 20:15:00 +01:00
|
|
|
if (payload.botToken != null && String(payload.botToken).trim() !== "") {
|
|
|
|
|
sbDoc.setIn(["General", "Token"], String(payload.botToken).trim());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (payload.dbDriver != null) {
|
|
|
|
|
const driver = String(payload.dbDriver).toLowerCase() === "mysql" ? "mysql" : "sqlite";
|
|
|
|
|
sbDoc.setIn(["Database", "Driver"], driver);
|
|
|
|
|
if (driver === "mysql") {
|
|
|
|
|
sbDoc.setIn(["Database", "MySQL", "Host"], String(payload.dbHost || "127.0.0.1").trim());
|
|
|
|
|
sbDoc.setIn(["Database", "MySQL", "Port"], Number(payload.dbPort) || 3306);
|
|
|
|
|
sbDoc.setIn(["Database", "MySQL", "User"], String(payload.dbUser || "").trim());
|
|
|
|
|
sbDoc.setIn(["Database", "MySQL", "Password"], String(payload.dbPassword || ""));
|
|
|
|
|
sbDoc.setIn(["Database", "MySQL", "Database"], String(payload.dbDatabase || "").trim());
|
|
|
|
|
} else {
|
|
|
|
|
sbDoc.setIn(["Database", "SQLite", "File"], "./Data/supportbot.db");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-26 20:11:22 +01:00
|
|
|
fs.writeFileSync(SUPPORTBOT_FILE, sbDoc.toString());
|
|
|
|
|
|
|
|
|
|
configStore.reloadFile("api");
|
|
|
|
|
configStore.reloadFile("supportbot");
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
ownerUserIds: ownerIds,
|
|
|
|
|
api: configStore.api?.API || configStore.api,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
|
isSetupComplete,
|
|
|
|
|
getSetupStatus,
|
|
|
|
|
validateBotToken,
|
|
|
|
|
validateOAuthCredentials,
|
|
|
|
|
validateRedirectUri,
|
|
|
|
|
validateOwnerUserIds,
|
|
|
|
|
validateSecretKey,
|
|
|
|
|
validateSetupPayload,
|
|
|
|
|
applySetup,
|
|
|
|
|
};
|