26.2 pre3

This commit is contained in:
1tzEmerald committed 2026-08-10 20:15:00 +01:00
1 parent a7598d26aa
commit 4eef1867e3
26 files changed
+1221 -385

No files matched your search

+20
View File
@@ -85,6 +85,14 @@ async function logStartup(client, options = {}) {
info("Version", botVersion);
info("Node.js", process.version);
info("Discord.js", `v${Discord.version}`);
const db = require("./Database.js");
const dbInfo = db.getDatabaseInfo ? db.getDatabaseInfo() : null;
if (dbInfo) {
const dbDisplay = dbInfo.type === "mysql"
? `${dbInfo.driver} (${dbInfo.host}:${dbInfo.port}/${dbInfo.database})`
: `${dbInfo.driver} (${dbInfo.file})`;
info("Database", dbDisplay);
}
info("Logged in as", client.user.tag);
info("Guilds", `${client.guilds.cache.size}`);
@@ -102,6 +110,16 @@ async function logStartup(client, options = {}) {
console.log("");
}
function checkSetup() {
const { isSetupComplete } = require("./DashboardSetup.js");
if (!isSetupComplete()) {
const port = configStore.api?.API?.Port || 25575;
section("Setup Required");
status("Onboarding setup incomplete", false, `http://localhost:${port}/setup`);
console.log("");
}
}
async function checkConfigs(client) {
const supportbot = configStore.supportbot;
const health = syncGuildHealth(client);
@@ -169,6 +187,8 @@ async function runBotReady(client, options = {}) {
if (logCommands) {
logSlashCommandsRegistered(client);
}
checkSetup();
}
module.exports = {
+4 -1
View File
@@ -22,7 +22,7 @@ const DEFAULT_GROUPS = [
color: "#3B82F6", // Blue
discordRoleId: null,
isSystem: true,
permissions: fullPermissions(),
permissions: fullPermissions(false, false),
},
{
id: "support",
@@ -151,6 +151,9 @@ function createGroup(payload = {}) {
}
function updateGroup(id, payload = {}) {
if (id === "owner") {
throw new Error('System group "Owner" cannot be modified.');
}
const groups = loadStore();
const index = groups.findIndex((g) => g.id === id);
if (index === -1) {
+12 -3
View File
@@ -17,29 +17,32 @@ function configMap(view, edit) {
return configs;
}
function fullPermissions() {
function fullPermissions(rawYaml = true, userManage = true) {
return {
overview: true,
tickets: true,
logs: true,
transcripts: true,
rawYaml: Boolean(rawYaml),
settings: { view: true, update: true },
configs: configMap(true, true),
users: { view: true, manage: true },
users: { view: true, manage: Boolean(userManage) },
};
}
function permissionsFromRole(role, customPermissions) {
switch (role) {
case "owner":
return fullPermissions(true, true);
case "admin":
return fullPermissions();
return fullPermissions(false, false);
case "moderator":
return {
overview: true,
tickets: true,
logs: true,
transcripts: true,
rawYaml: false,
settings: { view: true, update: false },
configs: configMap(true, false),
users: { view: false, manage: false },
@@ -50,6 +53,7 @@ function permissionsFromRole(role, customPermissions) {
tickets: true,
logs: true,
transcripts: false,
rawYaml: false,
settings: { view: true, update: false },
configs: configMap(true, true),
users: { view: false, manage: false },
@@ -60,6 +64,7 @@ function permissionsFromRole(role, customPermissions) {
tickets: true,
logs: true,
transcripts: true,
rawYaml: false,
settings: { view: true, update: false },
configs: configMap(true, false),
users: { view: false, manage: false },
@@ -80,6 +85,7 @@ function normalizePermissions(input) {
tickets: input.tickets ?? input.overview ?? base.tickets,
logs: input.logs ?? base.logs,
transcripts: input.transcripts ?? base.transcripts,
rawYaml: Boolean(input.rawYaml ?? input.configs?.rawYaml ?? base.rawYaml),
settings: {
view: input.settings?.view ?? base.settings.view,
update: input.settings?.update ?? base.settings.update,
@@ -105,6 +111,9 @@ function hasPermission(permissions, check) {
if (check === "tickets") return Boolean(permissions.tickets ?? permissions.overview);
if (check === "logs") return Boolean(permissions.logs);
if (check === "transcripts") return Boolean(permissions.transcripts);
if (check === "configs.raw_yaml" || check === "configs.rawYaml" || check === "rawYaml") {
return Boolean(permissions.rawYaml ?? permissions.configs?.rawYaml ?? false);
}
if (check === "settings.view") return Boolean(permissions.settings?.view);
if (check === "settings.update") return Boolean(permissions.settings?.update);
if (check === "users.view") return Boolean(permissions.users?.view);
+71 -1
View File
@@ -84,6 +84,7 @@ function getOwnerIds(oauth) {
function buildSteps(apiRoot, supportbot) {
const oauth = apiRoot?.OAuth || {};
const token = supportbot?.General?.Token;
const dbConfig = supportbot?.Database || {};
return [
{
@@ -92,6 +93,12 @@ function buildSteps(apiRoot, supportbot) {
description: "From the Discord Developer Portal → your application → Bot → Reset Token.",
complete: isValidBotToken(token),
},
{
id: "database",
title: "Database storage",
description: "Choose SQLite or MySQL database engine for storing bot data.",
complete: Boolean(dbConfig.Driver),
},
{
id: "secretKey",
title: "Dashboard session secret",
@@ -334,12 +341,58 @@ function validateSecretKey(secretKey) {
return { ok: true };
}
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" };
}
async function validateSetupPayload(payload) {
const results = {};
if (payload.botToken != null) {
results.botToken = await validateBotToken(payload.botToken);
}
if (payload.dbDriver != null) {
results.database = await validateDatabase(
payload.dbDriver,
payload.dbHost,
payload.dbPort,
payload.dbUser,
payload.dbPassword,
payload.dbDatabase,
);
}
if (payload.secretKey != null) {
results.secretKey = validateSecretKey(payload.secretKey);
}
@@ -415,7 +468,24 @@ function applySetup(payload) {
fs.writeFileSync(API_FILE, apiDoc.toString());
const sbDoc = YAML.parseDocument(fs.readFileSync(SUPPORTBOT_FILE, "utf8"));
sbDoc.setIn(["General", "Token"], String(payload.botToken).trim());
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");
}
}
fs.writeFileSync(SUPPORTBOT_FILE, sbDoc.toString());
configStore.reloadFile("api");
+22 -4
View File
@@ -160,12 +160,24 @@ function formatPublicUser(entry, access, discordProfile) {
};
}
function listUsers(oauth) {
function listUsers(oauth, discordClient) {
bootstrapFromOAuth(oauth);
const store = readStore();
const yamlOwners = getOwnerIds(oauth);
return Object.values(store.users).map((entry) => {
let updated = false;
const usersList = Object.values(store.users).map((entry) => {
if (discordClient?.users?.cache) {
const cached = discordClient.users.cache.get(String(entry.id));
if (cached && (!entry.username || !entry.avatar)) {
entry.username = cached.username || entry.username;
entry.globalName = cached.globalName || cached.username || entry.globalName;
entry.avatar = cached.avatar || entry.avatar;
updated = true;
}
}
const access = resolvePermissions(entry, oauth, entry.id) || {
permissions: permissionsFromRole("viewer"),
role: entry.role,
@@ -174,8 +186,8 @@ function listUsers(oauth) {
return {
id: entry.id,
username: entry.username,
globalName: entry.globalName,
username: entry.username || (yamlOwners.has(String(entry.id)) ? "Owner" : "User"),
globalName: entry.globalName || entry.username || null,
avatar: entry.avatar
? discordAvatarUrl({ id: entry.id, avatar: entry.avatar })
: discordAvatarUrl({ id: entry.id, avatar: null }),
@@ -192,6 +204,12 @@ function listUsers(oauth) {
updatedAt: entry.updatedAt,
};
});
if (updated) {
writeStore(store);
}
return usersList;
}
function addUser(payload, actorId, oauth) {
+699 -45
View File
@@ -1,40 +1,641 @@
// SupportBot | JSON fallback adapter for environments without native addons (Pterodactyl)
// SupportBot | Unified SQL Database Adapter (MySQL/MariaDB & SQLite)
const fs = require("fs");
const path = require("path");
const yaml = require("js-yaml");
const DB_PATH = path.join(__dirname, "../Data/supportbot.json");
const CONFIG_PATH = path.join(__dirname, "../Configs/supportbot.yml");
const LEGACY_JSON_PATH = path.join(__dirname, "../Data/supportbot.json");
const SQLITE_DB_PATH = path.join(__dirname, "../Data/supportbot.db");
function load() {
try {
const raw = fs.readFileSync(DB_PATH, "utf8");
return JSON.parse(raw);
} catch (e) {
return {
suggestions: {},
profiles: {},
tickets: {},
ticket_panel: [],
ticket_users: {},
settings: {},
// Load config
let dbConfig = { Driver: "sqlite", MySQL: {}, SQLite: { File: "./Data/supportbot.db" } };
try {
if (fs.existsSync(CONFIG_PATH)) {
const rawYaml = fs.readFileSync(CONFIG_PATH, "utf8");
const parsed = yaml.load(rawYaml);
if (parsed && parsed.Database) {
dbConfig = parsed.Database;
}
}
} catch (e) {
console.warn("[Database] Warning: Failed to parse Database section in supportbot.yml, defaulting to SQLite.");
}
const driver = String(dbConfig.Driver || "sqlite").toLowerCase();
const isMySQL = driver === "mysql" || driver === "mariadb";
let sqliteDb = null;
let mysqlPool = null;
let mysqlReady = false;
const mysqlWriteQueue = [];
// In-memory state cache for synchronous, high-performance reads
const state = {
tickets: {},
ticket_panel: [],
ticket_users: {},
profiles: {},
settings: {},
blacklisted_users: {},
};
// ----------------------------------------------------
// Database Initialization
// ----------------------------------------------------
function initDatabase() {
if (isMySQL) {
initMySQL();
} else {
initSQLite();
}
migrateLegacyJSON();
}
function initSQLite() {
const Database = require("better-sqlite3");
const dbDir = path.dirname(SQLITE_DB_PATH);
if (!fs.existsSync(dbDir)) {
fs.mkdirSync(dbDir, { recursive: true });
}
sqliteDb = new Database(SQLITE_DB_PATH);
sqliteDb.pragma("journal_mode = WAL");
sqliteDb.exec(`
CREATE TABLE IF NOT EXISTS tickets (
ticket_id TEXT PRIMARY KEY,
user_id TEXT,
subject TEXT,
description TEXT,
department TEXT,
priority TEXT,
status TEXT,
ai_mode_enabled INTEGER DEFAULT 0,
ai_channel_id TEXT,
ai_type TEXT,
ai_enabled_by TEXT,
ai_enabled_at INTEGER,
question_answers TEXT,
ticket_name TEXT,
ticket_number TEXT,
voice_channel_id TEXT,
created_at INTEGER,
updated_at INTEGER
);
CREATE TABLE IF NOT EXISTS ticket_panels (
id INTEGER PRIMARY KEY,
message_id TEXT,
channel_id TEXT,
created_at INTEGER
);
CREATE TABLE IF NOT EXISTS ticket_users (
ticket_id TEXT,
user_id TEXT,
PRIMARY KEY (ticket_id, user_id)
);
CREATE TABLE IF NOT EXISTS profiles (
user_id TEXT PRIMARY KEY,
bio TEXT DEFAULT '',
timezone TEXT DEFAULT '',
clocked_in INTEGER DEFAULT 0
);
CREATE TABLE IF NOT EXISTS settings (
setting_key TEXT PRIMARY KEY,
setting_value TEXT
);
CREATE TABLE IF NOT EXISTS blacklisted_users (
user_id TEXT PRIMARY KEY,
reason TEXT,
added_by TEXT,
created_at INTEGER
);
`);
loadSQLiteState();
const clr = { reset: "\x1b[0m", gray: "\x1b[90m", green: "\x1b[32m", white: "\x1b[37m" };
console.log(`${clr.green}✓${clr.reset} ${clr.white}Database Connected${clr.reset} ${clr.gray}to SQLite (${SQLITE_DB_PATH})${clr.reset}`);
}
function loadSQLiteState() {
const tickets = sqliteDb.prepare("SELECT * FROM tickets").all();
for (const r of tickets) {
state.tickets[r.ticket_id] = {
ticket_id: r.ticket_id,
user_id: r.user_id,
subject: r.subject || "",
description: r.description || "",
department: r.department || "general",
priority: r.priority || "medium",
status: r.status || "open",
aiModeEnabled: r.ai_mode_enabled,
aiChannelId: r.ai_channel_id,
aiType: r.ai_type,
aiEnabledBy: r.ai_enabled_by,
aiEnabledAt: r.ai_enabled_at,
questionAnswers: r.question_answers || "[]",
ticketName: r.ticket_name,
ticketNumber: r.ticket_number,
voiceChannelId: r.voice_channel_id,
created_at: r.created_at,
updated_at: r.updated_at,
};
}
const panels = sqliteDb.prepare("SELECT * FROM ticket_panels ORDER BY id ASC").all();
state.ticket_panel = panels.map((p) => ({
id: p.id,
message_id: p.message_id,
channel_id: p.channel_id,
created_at: p.created_at,
}));
const ticketUsers = sqliteDb.prepare("SELECT * FROM ticket_users").all();
for (const tu of ticketUsers) {
state.ticket_users[tu.ticket_id] = state.ticket_users[tu.ticket_id] || [];
if (!state.ticket_users[tu.ticket_id].includes(tu.user_id)) {
state.ticket_users[tu.ticket_id].push(tu.user_id);
}
}
const profiles = sqliteDb.prepare("SELECT * FROM profiles").all();
for (const p of profiles) {
state.profiles[p.user_id] = {
user_id: p.user_id,
bio: p.bio || "",
timezone: p.timezone || "",
clockedIn: p.clocked_in,
};
}
const settings = sqliteDb.prepare("SELECT * FROM settings").all();
for (const s of settings) {
try {
state.settings[s.setting_key] = JSON.parse(s.setting_value);
} catch {
state.settings[s.setting_key] = s.setting_value;
}
}
const blacklisted = sqliteDb.prepare("SELECT * FROM blacklisted_users").all();
for (const b of blacklisted) {
state.blacklisted_users[b.user_id] = {
user_id: b.user_id,
reason: b.reason || "",
added_by: b.added_by || "",
created_at: b.created_at,
};
}
}
function save(state) {
function initMySQL() {
const mysql = require("mysql2");
const mysqlOpts = dbConfig.MySQL || {};
mysqlPool = mysql.createPool({
host: mysqlOpts.Host || "127.0.0.1",
port: mysqlOpts.Port || 3306,
user: mysqlOpts.User || "supportbot",
password: mysqlOpts.Password || "",
database: mysqlOpts.Database || "supportbot",
connectionLimit: mysqlOpts.ConnectionLimit || 10,
waitForConnections: true,
});
const poolPromise = mysqlPool.promise();
(async () => {
try {
await poolPromise.query(`
CREATE TABLE IF NOT EXISTS tickets (
ticket_id VARCHAR(255) PRIMARY KEY,
user_id VARCHAR(255),
subject TEXT,
description TEXT,
department VARCHAR(255),
priority VARCHAR(255),
status VARCHAR(255),
ai_mode_enabled INT DEFAULT 0,
ai_channel_id VARCHAR(255),
ai_type VARCHAR(255),
ai_enabled_by VARCHAR(255),
ai_enabled_at BIGINT,
question_answers TEXT,
ticket_name VARCHAR(255),
ticket_number VARCHAR(255),
voice_channel_id VARCHAR(255),
created_at BIGINT,
updated_at BIGINT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await poolPromise.query(`
CREATE TABLE IF NOT EXISTS ticket_panels (
id BIGINT PRIMARY KEY,
message_id VARCHAR(255),
channel_id VARCHAR(255),
created_at BIGINT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await poolPromise.query(`
CREATE TABLE IF NOT EXISTS ticket_users (
ticket_id VARCHAR(255),
user_id VARCHAR(255),
PRIMARY KEY (ticket_id, user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await poolPromise.query(`
CREATE TABLE IF NOT EXISTS profiles (
user_id VARCHAR(255) PRIMARY KEY,
bio TEXT,
timezone VARCHAR(255),
clocked_in INT DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await poolPromise.query(`
CREATE TABLE IF NOT EXISTS settings (
setting_key VARCHAR(255) PRIMARY KEY,
setting_value LONGTEXT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
await poolPromise.query(`
CREATE TABLE IF NOT EXISTS blacklisted_users (
user_id VARCHAR(255) PRIMARY KEY,
reason TEXT,
added_by VARCHAR(255),
created_at BIGINT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
`);
// Load initial state from MySQL
const [tickets] = await poolPromise.query("SELECT * FROM tickets");
for (const r of tickets) {
state.tickets[r.ticket_id] = {
ticket_id: r.ticket_id,
user_id: r.user_id,
subject: r.subject || "",
description: r.description || "",
department: r.department || "general",
priority: r.priority || "medium",
status: r.status || "open",
aiModeEnabled: r.ai_mode_enabled,
aiChannelId: r.ai_channel_id,
aiType: r.ai_type,
aiEnabledBy: r.ai_enabled_by,
aiEnabledAt: r.ai_enabled_at,
questionAnswers: r.question_answers || "[]",
ticketName: r.ticket_name,
ticketNumber: r.ticket_number,
voiceChannelId: r.voice_channel_id,
created_at: Number(r.created_at),
updated_at: Number(r.updated_at),
};
}
const [panels] = await poolPromise.query("SELECT * FROM ticket_panels ORDER BY id ASC");
state.ticket_panel = panels.map((p) => ({
id: Number(p.id),
message_id: p.message_id,
channel_id: p.channel_id,
created_at: Number(p.created_at),
}));
const [ticketUsers] = await poolPromise.query("SELECT * FROM ticket_users");
for (const tu of ticketUsers) {
state.ticket_users[tu.ticket_id] = state.ticket_users[tu.ticket_id] || [];
if (!state.ticket_users[tu.ticket_id].includes(tu.user_id)) {
state.ticket_users[tu.ticket_id].push(tu.user_id);
}
}
const [profiles] = await poolPromise.query("SELECT * FROM profiles");
for (const p of profiles) {
state.profiles[p.user_id] = {
user_id: p.user_id,
bio: p.bio || "",
timezone: p.timezone || "",
clockedIn: p.clocked_in,
};
}
const [settings] = await poolPromise.query("SELECT * FROM settings");
for (const s of settings) {
try {
state.settings[s.setting_key] = JSON.parse(s.setting_value);
} catch {
state.settings[s.setting_key] = s.setting_value;
}
}
const [blacklisted] = await poolPromise.query("SELECT * FROM blacklisted_users");
for (const b of blacklisted) {
state.blacklisted_users[b.user_id] = {
user_id: b.user_id,
reason: b.reason || "",
added_by: b.added_by || "",
created_at: Number(b.created_at),
};
}
mysqlReady = true;
const clr = { reset: "\x1b[0m", gray: "\x1b[90m", green: "\x1b[32m", white: "\x1b[37m" };
console.log(`${clr.green}✓${clr.reset} ${clr.white}Database Connected${clr.reset} ${clr.gray}to MySQL (${mysqlOpts.Host}:${mysqlOpts.Port}/${mysqlOpts.Database})${clr.reset}`);
while (mysqlWriteQueue.length > 0) {
const fn = mysqlWriteQueue.shift();
try { fn(); } catch (e) {}
}
} catch (err) {
console.error("[Database] Error initializing MySQL tables or state:", err.message);
}
})();
}
// ----------------------------------------------------
// Legacy JSON Migration Utility
// ----------------------------------------------------
function migrateLegacyJSON() {
if (!fs.existsSync(LEGACY_JSON_PATH)) return;
try {
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
fs.writeFileSync(DB_PATH, JSON.stringify(state, null, 2), "utf8");
const raw = fs.readFileSync(LEGACY_JSON_PATH, "utf8");
const jsonState = JSON.parse(raw);
console.log("[Database] Legacy supportbot.json detected. Migrating records to SQL...");
if (jsonState.tickets) {
for (const tId of Object.keys(jsonState.tickets)) {
const t = jsonState.tickets[tId];
if (t && !state.tickets[tId]) {
const ticketObj = {
id: t.ticket_id || tId,
user: t.user_id,
reason: t.subject,
description: t.description,
department: t.department,
priority: t.priority,
aiModeEnabled: t.aiModeEnabled,
aiChannelId: t.aiChannelId,
aiType: t.aiType,
aiEnabledBy: t.aiEnabledBy,
aiEnabledAt: t.aiEnabledAt,
questionAnswers: typeof t.questionAnswers === "string" ? JSON.parse(t.questionAnswers) : t.questionAnswers || [],
name: t.ticketName,
number: t.ticketNumber,
};
exportsModule.addTicket(ticketObj);
}
}
}
if (jsonState.ticket_panel && Array.isArray(jsonState.ticket_panel)) {
for (const p of jsonState.ticket_panel) {
exportsModule.saveTicketPanel(p.message_id, p.channel_id);
}
}
if (jsonState.profiles) {
for (const uId of Object.keys(jsonState.profiles)) {
const prof = jsonState.profiles[uId];
if (prof) {
state.profiles[uId] = prof;
persistProfile(uId);
}
}
}
if (jsonState.settings) {
exportsModule.saveSettings(jsonState.settings);
}
// Archive legacy json file
const backupPath = LEGACY_JSON_PATH + ".bak";
fs.renameSync(LEGACY_JSON_PATH, backupPath);
console.log(`[Database] Legacy supportbot.json successfully migrated and archived to ${backupPath}`);
} catch (e) {
console.error("Failed to persist database:", e);
console.error("[Database] Failed to migrate legacy supportbot.json:", e);
}
// Migrate legacy BlacklistedUsers.json if present
const legacyBlacklistPath = path.join(__dirname, "../Data/BlacklistedUsers.json");
if (fs.existsSync(legacyBlacklistPath)) {
try {
const raw = fs.readFileSync(legacyBlacklistPath, "utf8");
const data = JSON.parse(raw);
if (data && Array.isArray(data.blacklistedUsers)) {
for (const uId of data.blacklistedUsers) {
exportsModule.addBlacklistedUser(uId, "Migrated from BlacklistedUsers.json", "System");
}
}
fs.renameSync(legacyBlacklistPath, legacyBlacklistPath + ".bak");
console.log(`[Database] Legacy BlacklistedUsers.json successfully migrated and archived.`);
} catch (e) {
console.error("[Database] Failed to migrate BlacklistedUsers.json:", e);
}
}
}
const state = load();
// ----------------------------------------------------
// SQL Persistence Helpers
// ----------------------------------------------------
function persistTicket(ticketId) {
const t = state.tickets[ticketId];
if (!t) return;
if (isMySQL && mysqlPool) {
const sql = `
INSERT INTO tickets (
ticket_id, user_id, subject, description, department, priority, status,
ai_mode_enabled, ai_channel_id, ai_type, ai_enabled_by, ai_enabled_at,
question_answers, ticket_name, ticket_number, voice_channel_id, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
user_id=VALUES(user_id), subject=VALUES(subject), description=VALUES(description),
department=VALUES(department), priority=VALUES(priority), status=VALUES(status),
ai_mode_enabled=VALUES(ai_mode_enabled), ai_channel_id=VALUES(ai_channel_id),
ai_type=VALUES(ai_type), ai_enabled_by=VALUES(ai_enabled_by), ai_enabled_at=VALUES(ai_enabled_at),
question_answers=VALUES(question_answers), ticket_name=VALUES(ticket_name),
ticket_number=VALUES(ticket_number), voice_channel_id=VALUES(voice_channel_id), updated_at=VALUES(updated_at);
`;
const params = [
t.ticket_id,
t.user_id || "",
t.subject || "",
t.description || "",
t.department || "general",
t.priority || "medium",
t.status || "open",
t.aiModeEnabled ? 1 : 0,
t.aiChannelId || null,
t.aiType || null,
t.aiEnabledBy || null,
t.aiEnabledAt || null,
typeof t.questionAnswers === "string" ? t.questionAnswers : JSON.stringify(t.questionAnswers || []),
t.ticketName || null,
t.ticketNumber || null,
t.voiceChannelId || null,
t.created_at || Date.now(),
t.updated_at || Date.now(),
];
mysqlPool.query(sql, params, (err) => {
if (err) console.error("[Database] MySQL persistTicket error:", err.message);
});
} else if (sqliteDb) {
const stmt = sqliteDb.prepare(`
INSERT INTO tickets (
ticket_id, user_id, subject, description, department, priority, status,
ai_mode_enabled, ai_channel_id, ai_type, ai_enabled_by, ai_enabled_at,
question_answers, ticket_name, ticket_number, voice_channel_id, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(ticket_id) DO UPDATE SET
user_id=excluded.user_id, subject=excluded.subject, description=excluded.description,
department=excluded.department, priority=excluded.priority, status=excluded.status,
ai_mode_enabled=excluded.ai_mode_enabled, ai_channel_id=excluded.ai_channel_id,
ai_type=excluded.ai_type, ai_enabled_by=excluded.ai_enabled_by, ai_enabled_at=excluded.ai_enabled_at,
question_answers=excluded.question_answers, ticket_name=excluded.ticket_name,
ticket_number=excluded.ticket_number, voice_channel_id=excluded.voice_channel_id, updated_at=excluded.updated_at;
`);
stmt.run(
t.ticket_id,
t.user_id || "",
t.subject || "",
t.description || "",
t.department || "general",
t.priority || "medium",
t.status || "open",
t.aiModeEnabled ? 1 : 0,
t.aiChannelId || null,
t.aiType || null,
t.aiEnabledBy || null,
t.aiEnabledAt || null,
typeof t.questionAnswers === "string" ? t.questionAnswers : JSON.stringify(t.questionAnswers || []),
t.ticketName || null,
t.ticketNumber || null,
t.voiceChannelId || null,
t.created_at || Date.now(),
t.updated_at || Date.now()
);
}
}
function persistPanel(panelRow) {
if (isMySQL && mysqlPool) {
mysqlPool.query(
"INSERT INTO ticket_panels (id, message_id, channel_id, created_at) VALUES (?, ?, ?, ?)",
[panelRow.id, panelRow.message_id, panelRow.channel_id, panelRow.created_at],
(err) => {
if (err) console.error("[Database] MySQL persistPanel error:", err.message);
}
);
} else if (sqliteDb) {
const stmt = sqliteDb.prepare("INSERT INTO ticket_panels (id, message_id, channel_id, created_at) VALUES (?, ?, ?, ?)");
stmt.run(panelRow.id, panelRow.message_id, panelRow.channel_id, panelRow.created_at);
}
}
function persistTicketUser(ticketId, userId, action = "add") {
if (action === "add") {
if (isMySQL && mysqlPool) {
mysqlPool.query("INSERT IGNORE INTO ticket_users (ticket_id, user_id) VALUES (?, ?)", [ticketId, userId], (err) => {
if (err) console.error("[Database] MySQL persistTicketUser error:", err.message);
});
} else if (sqliteDb) {
sqliteDb.prepare("INSERT OR IGNORE INTO ticket_users (ticket_id, user_id) VALUES (?, ?)").run(ticketId, userId);
}
} else {
if (isMySQL && mysqlPool) {
mysqlPool.query("DELETE FROM ticket_users WHERE ticket_id = ? AND user_id = ?", [ticketId, userId], (err) => {
if (err) console.error("[Database] MySQL deleteTicketUser error:", err.message);
});
} else if (sqliteDb) {
sqliteDb.prepare("DELETE FROM ticket_users WHERE ticket_id = ? AND user_id = ?").run(ticketId, userId);
}
}
}
function persistProfile(userId) {
const p = state.profiles[userId];
if (!p) return;
if (isMySQL && mysqlPool) {
const sql = `
INSERT INTO profiles (user_id, bio, timezone, clocked_in) VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE bio=VALUES(bio), timezone=VALUES(timezone), clocked_in=VALUES(clocked_in);
`;
mysqlPool.query(sql, [p.user_id, p.bio || "", p.timezone || "", p.clockedIn ? 1 : 0], (err) => {
if (err) console.error("[Database] MySQL persistProfile error:", err.message);
});
} else if (sqliteDb) {
const stmt = sqliteDb.prepare(`
INSERT INTO profiles (user_id, bio, timezone, clocked_in) VALUES (?, ?, ?, ?)
ON CONFLICT(user_id) DO UPDATE SET bio=excluded.bio, timezone=excluded.timezone, clocked_in=excluded.clocked_in;
`);
stmt.run(p.user_id, p.bio || "", p.timezone || "", p.clockedIn ? 1 : 0);
}
}
function persistSettings() {
const settingsObj = state.settings || {};
for (const k of Object.keys(settingsObj)) {
const val = JSON.stringify(settingsObj[k]);
if (isMySQL && mysqlPool) {
mysqlPool.query(
"INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value=VALUES(setting_value)",
[k, val],
(err) => {
if (err) console.error("[Database] MySQL persistSettings error:", err.message);
}
);
} else if (sqliteDb) {
sqliteDb.prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON CONFLICT(setting_key) DO UPDATE SET setting_value=excluded.setting_value").run(k, val);
}
}
}
function persistBlacklistedUser(userId, action = "add") {
if (isMySQL && !mysqlReady) {
mysqlWriteQueue.push(() => persistBlacklistedUser(userId, action));
return;
}
const b = state.blacklisted_users[userId];
if (action === "add" && b) {
if (isMySQL && mysqlPool) {
const sql = `
INSERT INTO blacklisted_users (user_id, reason, added_by, created_at) VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE reason=VALUES(reason), added_by=VALUES(added_by), created_at=VALUES(created_at);
`;
mysqlPool.query(sql, [b.user_id, b.reason || "", b.added_by || "", b.created_at || Date.now()], (err) => {
if (err) console.error("[Database] MySQL persistBlacklistedUser error:", err.message);
});
} else if (sqliteDb) {
const stmt = sqliteDb.prepare(`
INSERT INTO blacklisted_users (user_id, reason, added_by, created_at) VALUES (?, ?, ?, ?)
ON CONFLICT(user_id) DO UPDATE SET reason=excluded.reason, added_by=excluded.added_by, created_at=excluded.created_at;
`);
stmt.run(b.user_id, b.reason || "", b.added_by || "", b.created_at || Date.now());
}
} else {
if (isMySQL && mysqlPool) {
mysqlPool.query("DELETE FROM blacklisted_users WHERE user_id = ?", [userId], (err) => {
if (err) console.error("[Database] MySQL removeBlacklistedUser error:", err.message);
});
} else if (sqliteDb) {
sqliteDb.prepare("DELETE FROM blacklisted_users WHERE user_id = ?").run(userId);
}
}
}
function ensureProfile(userId) {
if (!state.profiles[userId]) {
state.profiles[userId] = { user_id: userId, bio: "", timezone: "", clockedIn: 0 };
save(state);
persistProfile(userId);
}
}
@@ -43,7 +644,7 @@ function normaliseTicketRow(row) {
let questionAnswers = [];
if (row.questionAnswers) {
try {
questionAnswers = JSON.parse(row.questionAnswers);
questionAnswers = typeof row.questionAnswers === "string" ? JSON.parse(row.questionAnswers) : row.questionAnswers;
} catch {
questionAnswers = [];
}
@@ -57,7 +658,7 @@ function normaliseTicketRow(row) {
};
}
module.exports = {
const exportsModule = {
addTicket(ticket) {
const now = Date.now();
const id = ticket.id;
@@ -84,7 +685,7 @@ module.exports = {
status: existing.status || "open",
};
save(state);
persistTicket(id);
if (global.apiServer) {
global.apiServer.broadcast("ticket_created", {
@@ -104,7 +705,7 @@ module.exports = {
if (!t) return;
t.questionAnswers = JSON.stringify(questionAnswers);
t.updated_at = Date.now();
save(state);
persistTicket(ticketId);
},
updateTicketVoice(ticketId, voiceChannelId) {
@@ -112,7 +713,7 @@ module.exports = {
if (!t) return;
t.voiceChannelId = voiceChannelId;
t.updated_at = Date.now();
save(state);
persistTicket(ticketId);
},
updateTicketStatus(ticketId, status) {
@@ -120,7 +721,7 @@ module.exports = {
if (!t) return;
t.status = status;
t.updated_at = Date.now();
save(state);
persistTicket(ticketId);
if (global.apiServer && status === "closed") {
global.apiServer.broadcast("ticket_closed", { ticket_id: ticketId });
@@ -132,7 +733,7 @@ module.exports = {
if (!t) return;
t.department = department;
t.updated_at = Date.now();
save(state);
persistTicket(ticketId);
},
updateTicketPriority(ticketId, priority) {
@@ -140,7 +741,7 @@ module.exports = {
if (!t) return;
t.priority = priority;
t.updated_at = Date.now();
save(state);
persistTicket(ticketId);
},
setTicketAIState(ticketId, aiState = {}) {
@@ -152,7 +753,7 @@ module.exports = {
t.aiEnabledBy = aiState.enabledBy || null;
t.aiEnabledAt = aiState.enabledAt ? new Date(aiState.enabledAt).getTime() : aiState.enabled ? Date.now() : null;
t.updated_at = Date.now();
save(state);
persistTicket(ticketId);
},
enableTicketAI(ticketId, aiChannelId = null, aiType = null, enabledBy = null) {
@@ -164,7 +765,7 @@ module.exports = {
t.aiEnabledBy = enabledBy;
t.aiEnabledAt = Date.now();
t.updated_at = Date.now();
save(state);
persistTicket(ticketId);
},
disableTicketAI(ticketId) {
@@ -172,7 +773,7 @@ module.exports = {
if (!t) return;
t.aiModeEnabled = 0;
t.updated_at = Date.now();
save(state);
persistTicket(ticketId);
},
getTicket(ticketId) {
@@ -193,8 +794,9 @@ module.exports = {
},
saveTicketPanel(messageId, channelId) {
state.ticket_panel.push({ id: Date.now(), message_id: messageId, channel_id: channelId, created_at: Date.now() });
save(state);
const row = { id: Date.now(), message_id: messageId, channel_id: channelId, created_at: Date.now() };
state.ticket_panel.push(row);
persistPanel(row);
},
getTicketPanel() {
@@ -204,19 +806,18 @@ module.exports = {
},
addUserToTicket(ticketId, userId) {
state.ticket_users[ticketId] = state.ticket_users[ticketId] || new Set();
// Sets cannot be serialized directly; store as array
const arr = new Set(state.ticket_users[ticketId]);
arr.add(userId);
state.ticket_users[ticketId] = Array.from(arr);
save(state);
state.ticket_users[ticketId] = state.ticket_users[ticketId] || [];
if (!state.ticket_users[ticketId].includes(userId)) {
state.ticket_users[ticketId].push(userId);
persistTicketUser(ticketId, userId, "add");
}
},
removeUserFromTicket(ticketId, userId) {
const arr = new Set(state.ticket_users[ticketId] || []);
arr.delete(userId);
state.ticket_users[ticketId] = Array.from(arr);
save(state);
if (state.ticket_users[ticketId]) {
state.ticket_users[ticketId] = state.ticket_users[ticketId].filter((u) => u !== userId);
persistTicketUser(ticketId, userId, "remove");
}
},
getTicketUsers(ticketId) {
@@ -237,11 +838,64 @@ module.exports = {
setClockedIn(userId, clockedIn) {
ensureProfile(userId);
state.profiles[userId].clockedIn = clockedIn ? 1 : 0;
save(state);
persistProfile(userId);
},
saveSettings(obj) {
state.settings = obj || {};
save(state);
persistSettings();
},
addBlacklistedUser(userId, reason = "", addedBy = "") {
state.blacklisted_users[userId] = {
user_id: userId,
reason,
added_by: addedBy,
created_at: Date.now(),
};
persistBlacklistedUser(userId, "add");
},
removeBlacklistedUser(userId) {
if (state.blacklisted_users[userId]) {
delete state.blacklisted_users[userId];
persistBlacklistedUser(userId, "remove");
}
},
isUserBlacklisted(userId) {
return Boolean(state.blacklisted_users[userId]);
},
getBlacklistedUsers() {
return Object.keys(state.blacklisted_users);
},
getBlacklistedUserDetails() {
return Object.values(state.blacklisted_users);
},
getDatabaseInfo() {
if (isMySQL) {
const mysqlOpts = dbConfig.MySQL || {};
return {
driver: "MySQL",
host: mysqlOpts.Host || "127.0.0.1",
port: mysqlOpts.Port || 3306,
database: mysqlOpts.Database || "supportbot",
type: "mysql"
};
}
const sqliteOpts = dbConfig.SQLite || {};
return {
driver: "SQLite",
file: sqliteOpts.File || "./Data/supportbot.db",
type: "sqlite"
};
},
};
// Initialize database on require
initDatabase();
module.exports = exportsModule;
+1
View File
@@ -180,6 +180,7 @@ function buildResourcesFromGuild(guild) {
return {
guildId: guild.id,
guildName: guild.name,
guildIcon: guild.iconURL({ extension: "png", size: 64 }) || null,
roles,
channels,
categories,
+28 -12
View File
@@ -1,30 +1,46 @@
// SupportBot | Emerald Services
// Ticket ID Structure
const db = require("./Database")
const db = require("./Database");
class TicketNumberID {
static async pad() {
let num = await this.get()
let n = num + ""
await this.set(num + 1)
return n.length >= 4 ? n : new Array(4 - n.length + 1).join("0") + n
let num = await this.get();
let n = num + "";
await this.set(num + 1);
return n.length >= 4 ? n : new Array(4 - n.length + 1).join("0") + n;
}
static async get() {
const row = db.getTicketPanel()
if (!row) return await this.set(0)
return row.id
const settings = db.getSettings ? db.getSettings() : {};
if (typeof settings.ticket_counter === "number") {
return settings.ticket_counter;
}
const row = db.getTicketPanel ? db.getTicketPanel() : null;
if (row && typeof row.message_id === "string" && row.message_id.startsWith("panel-")) {
const parsed = parseInt(row.message_id.replace("panel-", ""), 10);
if (!isNaN(parsed)) return parsed;
}
if (row && typeof row.id === "number" && row.id < 1000000) {
return row.id;
}
return 0;
}
static async set(value) {
db.saveTicketPanel(`panel-${value}`, "none")
return value
const settings = db.getSettings ? db.getSettings() : {};
settings.ticket_counter = value;
if (db.saveSettings) db.saveSettings(settings);
if (db.saveTicketPanel) db.saveTicketPanel(`panel-${value}`, "none");
return value;
}
static async reset() {
return await this.set(0)
return await this.set(0);
}
}
module.exports = TicketNumberID
module.exports = TicketNumberID;