902 lines
29 KiB
JavaScript
902 lines
29 KiB
JavaScript
// SupportBot | Unified SQL Database Adapter (MySQL/MariaDB & SQLite)
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const yaml = require("js-yaml");
|
|
|
|
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");
|
|
|
|
// 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 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 {
|
|
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("[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);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------
|
|
// 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 };
|
|
persistProfile(userId);
|
|
}
|
|
}
|
|
|
|
function normaliseTicketRow(row) {
|
|
if (!row) return null;
|
|
let questionAnswers = [];
|
|
if (row.questionAnswers) {
|
|
try {
|
|
questionAnswers = typeof row.questionAnswers === "string" ? JSON.parse(row.questionAnswers) : row.questionAnswers;
|
|
} catch {
|
|
questionAnswers = [];
|
|
}
|
|
}
|
|
|
|
return {
|
|
...row,
|
|
aiModeEnabled: Boolean(row.aiModeEnabled),
|
|
clockedIn: row.clockedIn !== undefined ? Boolean(row.clockedIn) : row.clockedIn,
|
|
questionAnswers,
|
|
};
|
|
}
|
|
|
|
const exportsModule = {
|
|
addTicket(ticket) {
|
|
const now = Date.now();
|
|
const id = ticket.id;
|
|
state.tickets[id] = state.tickets[id] || {};
|
|
const existing = state.tickets[id];
|
|
|
|
state.tickets[id] = {
|
|
ticket_id: id,
|
|
user_id: ticket.user,
|
|
subject: ticket.reason || "",
|
|
description: ticket.description || "",
|
|
created_at: existing.created_at || now,
|
|
updated_at: now,
|
|
department: ticket.department || "general",
|
|
priority: ticket.priority || "medium",
|
|
aiModeEnabled: ticket.aiModeEnabled ? 1 : 0,
|
|
aiChannelId: ticket.aiChannelId || null,
|
|
aiType: ticket.aiType || null,
|
|
aiEnabledBy: ticket.aiEnabledBy || null,
|
|
aiEnabledAt: ticket.aiEnabledAt || null,
|
|
questionAnswers: JSON.stringify(ticket.questionAnswers || []),
|
|
ticketName: ticket.name || null,
|
|
ticketNumber: ticket.number || null,
|
|
status: existing.status || "open",
|
|
};
|
|
|
|
persistTicket(id);
|
|
|
|
if (global.apiServer) {
|
|
global.apiServer.broadcast("ticket_created", {
|
|
ticket_id: id,
|
|
user_id: ticket.user,
|
|
subject: ticket.reason || "",
|
|
description: ticket.description || "",
|
|
department: ticket.department || "general",
|
|
priority: ticket.priority || "medium",
|
|
created_at: now,
|
|
});
|
|
}
|
|
},
|
|
|
|
updateTicketQuestionAnswers(ticketId, questionAnswers = []) {
|
|
const t = state.tickets[ticketId];
|
|
if (!t) return;
|
|
t.questionAnswers = JSON.stringify(questionAnswers);
|
|
t.updated_at = Date.now();
|
|
persistTicket(ticketId);
|
|
},
|
|
|
|
updateTicketVoice(ticketId, voiceChannelId) {
|
|
const t = state.tickets[ticketId];
|
|
if (!t) return;
|
|
t.voiceChannelId = voiceChannelId;
|
|
t.updated_at = Date.now();
|
|
persistTicket(ticketId);
|
|
},
|
|
|
|
updateTicketStatus(ticketId, status) {
|
|
const t = state.tickets[ticketId];
|
|
if (!t) return;
|
|
t.status = status;
|
|
t.updated_at = Date.now();
|
|
persistTicket(ticketId);
|
|
|
|
if (global.apiServer && status === "closed") {
|
|
global.apiServer.broadcast("ticket_closed", { ticket_id: ticketId });
|
|
}
|
|
},
|
|
|
|
updateTicketDepartment(ticketId, department) {
|
|
const t = state.tickets[ticketId];
|
|
if (!t) return;
|
|
t.department = department;
|
|
t.updated_at = Date.now();
|
|
persistTicket(ticketId);
|
|
},
|
|
|
|
updateTicketPriority(ticketId, priority) {
|
|
const t = state.tickets[ticketId];
|
|
if (!t) return;
|
|
t.priority = priority;
|
|
t.updated_at = Date.now();
|
|
persistTicket(ticketId);
|
|
},
|
|
|
|
setTicketAIState(ticketId, aiState = {}) {
|
|
const t = state.tickets[ticketId];
|
|
if (!t) return;
|
|
t.aiModeEnabled = aiState.enabled ? 1 : 0;
|
|
t.aiChannelId = aiState.aiChannelId || null;
|
|
t.aiType = aiState.aiType || null;
|
|
t.aiEnabledBy = aiState.enabledBy || null;
|
|
t.aiEnabledAt = aiState.enabledAt ? new Date(aiState.enabledAt).getTime() : aiState.enabled ? Date.now() : null;
|
|
t.updated_at = Date.now();
|
|
persistTicket(ticketId);
|
|
},
|
|
|
|
enableTicketAI(ticketId, aiChannelId = null, aiType = null, enabledBy = null) {
|
|
const t = state.tickets[ticketId];
|
|
if (!t) return;
|
|
t.aiModeEnabled = 1;
|
|
t.aiChannelId = aiChannelId;
|
|
t.aiType = aiType;
|
|
t.aiEnabledBy = enabledBy;
|
|
t.aiEnabledAt = Date.now();
|
|
t.updated_at = Date.now();
|
|
persistTicket(ticketId);
|
|
},
|
|
|
|
disableTicketAI(ticketId) {
|
|
const t = state.tickets[ticketId];
|
|
if (!t) return;
|
|
t.aiModeEnabled = 0;
|
|
t.updated_at = Date.now();
|
|
persistTicket(ticketId);
|
|
},
|
|
|
|
getTicket(ticketId) {
|
|
return normaliseTicketRow(state.tickets[ticketId]);
|
|
},
|
|
|
|
getTicketByAIChannel(aiChannelId) {
|
|
const keys = Object.keys(state.tickets);
|
|
for (const k of keys) {
|
|
const t = state.tickets[k];
|
|
if (t && t.aiChannelId === aiChannelId) return normaliseTicketRow(t);
|
|
}
|
|
return null;
|
|
},
|
|
|
|
getAllTickets() {
|
|
return Object.values(state.tickets).map(normaliseTicketRow);
|
|
},
|
|
|
|
saveTicketPanel(messageId, channelId) {
|
|
const row = { id: Date.now(), message_id: messageId, channel_id: channelId, created_at: Date.now() };
|
|
state.ticket_panel.push(row);
|
|
persistPanel(row);
|
|
},
|
|
|
|
getTicketPanel() {
|
|
const arr = state.ticket_panel;
|
|
if (!arr || arr.length === 0) return null;
|
|
return arr[arr.length - 1];
|
|
},
|
|
|
|
addUserToTicket(ticketId, userId) {
|
|
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) {
|
|
if (state.ticket_users[ticketId]) {
|
|
state.ticket_users[ticketId] = state.ticket_users[ticketId].filter((u) => u !== userId);
|
|
persistTicketUser(ticketId, userId, "remove");
|
|
}
|
|
},
|
|
|
|
getTicketUsers(ticketId) {
|
|
return (state.ticket_users[ticketId] || []).slice();
|
|
},
|
|
|
|
getSettings() {
|
|
return state.settings || {};
|
|
},
|
|
|
|
getProfile(userId) {
|
|
ensureProfile(userId);
|
|
const row = state.profiles[userId];
|
|
if (!row) return { user_id: userId, bio: "", timezone: "", clockedIn: false };
|
|
return { ...row, clockedIn: Boolean(row.clockedIn) };
|
|
},
|
|
|
|
setClockedIn(userId, clockedIn) {
|
|
ensureProfile(userId);
|
|
state.profiles[userId].clockedIn = clockedIn ? 1 : 0;
|
|
persistProfile(userId);
|
|
},
|
|
|
|
saveSettings(obj) {
|
|
state.settings = obj || {};
|
|
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;
|