26.2 pre3
This commit is contained in:
1 parent
a7598d26aa
commit
4eef1867e3
26 files changed
+1221
-385
No files matched your search
@@ -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 = {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
@@ -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;
|
||||
@@ -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
@@ -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;
|
||||
Reference in new issue
Block a user