Files
SupportBot/Structures/Database.js
T
2026-08-10 20:15:00 +01:00

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;