2026-08-10 20:15:00 +01:00
// SupportBot | Unified SQL Database Adapter (MySQL/MariaDB & SQLite)
2026-08-06 17:29:37 +01:00
const fs = require ( "fs" );
2025-09-20 23:09:22 +01:00
const path = require ( "path" );
2026-08-10 20:15:00 +01:00
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." );
}
2025-09-20 23:09:22 +01:00
2026-08-10 20:15:00 +01:00
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 : {},
};
2026-08-06 17:29:37 +01:00
2026-08-10 20:15:00 +01:00
// ----------------------------------------------------
// 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 ,
2026-08-06 17:29:37 +01:00
};
2025-09-20 23:09:22 +01:00
}
2026-08-06 17:29:37 +01:00
}
2026-03-10 20:02:43 +00:00
2026-08-10 20:15:00 +01:00
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 );
2026-08-12 00:12:18 +01:00
console . warn ( "[Database] Falling back to SQLite database." );
mysqlReady = false ;
if ( mysqlPool ) {
try { mysqlPool . end (); } catch ( e ) {}
mysqlPool = null ;
}
initSQLite ();
2026-08-10 20:15:00 +01:00
}
})();
}
// ----------------------------------------------------
// Legacy JSON Migration Utility
// ----------------------------------------------------
function migrateLegacyJSON () {
if ( ! fs . existsSync ( LEGACY_JSON_PATH )) return ;
2026-08-06 17:29:37 +01:00
try {
2026-08-10 20:15:00 +01:00
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 } ` );
2026-08-06 17:29:37 +01:00
} catch ( e ) {
2026-08-10 20:15:00 +01:00
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 );
}
2026-08-06 17:29:37 +01:00
}
}
2026-08-10 20:15:00 +01:00
// ----------------------------------------------------
// SQL Persistence Helpers
// ----------------------------------------------------
function persistTicket ( ticketId ) {
const t = state . tickets [ ticketId ];
if ( ! t ) return ;
2026-08-12 00:12:18 +01:00
if ( isMySQL && mysqlReady && mysqlPool ) {
2026-08-10 20:15:00 +01:00
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 ) {
2026-08-12 00:12:18 +01:00
if ( isMySQL && mysqlReady && mysqlPool ) {
2026-08-10 20:15:00 +01:00
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" ) {
2026-08-12 00:12:18 +01:00
if ( isMySQL && mysqlReady && mysqlPool ) {
2026-08-10 20:15:00 +01:00
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 {
2026-08-12 00:12:18 +01:00
if ( isMySQL && mysqlReady && mysqlPool ) {
2026-08-10 20:15:00 +01:00
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 ;
2026-08-12 00:12:18 +01:00
if ( isMySQL && mysqlReady && mysqlPool ) {
2026-08-10 20:15:00 +01:00
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" ) {
2026-08-12 00:12:18 +01:00
if ( isMySQL && mysqlReady && ! mysqlPool ) {
2026-08-10 20:15:00 +01:00
mysqlWriteQueue . push (() => persistBlacklistedUser ( userId , action ));
return ;
}
const b = state . blacklisted_users [ userId ];
if ( action === "add" && b ) {
2026-08-12 00:12:18 +01:00
if ( isMySQL && mysqlReady && mysqlPool ) {
2026-08-10 20:15:00 +01:00
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 {
2026-08-12 00:12:18 +01:00
if ( isMySQL && mysqlReady && mysqlPool ) {
2026-08-10 20:15:00 +01:00
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 );
}
}
}
2025-09-20 23:09:22 +01:00
2026-03-10 20:02:43 +00:00
function ensureProfile ( userId ) {
2026-08-06 17:29:37 +01:00
if ( ! state . profiles [ userId ]) {
state . profiles [ userId ] = { user_id : userId , bio : "" , timezone : "" , clockedIn : 0 };
2026-08-10 20:15:00 +01:00
persistProfile ( userId );
2026-03-10 20:02:43 +00:00
}
}
2025-09-20 23:09:22 +01:00
2026-03-11 18:58:15 +00:00
function normaliseTicketRow ( row ) {
if ( ! row ) return null ;
let questionAnswers = [];
if ( row . questionAnswers ) {
try {
2026-08-10 20:15:00 +01:00
questionAnswers = typeof row . questionAnswers === "string" ? JSON . parse ( row . questionAnswers ) : row . questionAnswers ;
2026-03-11 18:58:15 +00:00
} catch {
questionAnswers = [];
}
}
return {
... row ,
aiModeEnabled : Boolean ( row . aiModeEnabled ),
2026-08-06 17:29:37 +01:00
clockedIn : row . clockedIn !== undefined ? Boolean ( row . clockedIn ) : row . clockedIn ,
2026-03-11 18:58:15 +00:00
questionAnswers ,
};
}
2026-08-10 20:15:00 +01:00
const exportsModule = {
2025-09-20 23:09:22 +01:00
addTicket ( ticket ) {
const now = Date . now ();
2026-08-06 17:29:37 +01:00
const id = ticket . id ;
state . tickets [ id ] = state . tickets [ id ] || {};
const existing = state . tickets [ id ];
2026-03-10 20:02:43 +00:00
2026-08-06 17:29:37 +01:00
state . tickets [ id ] = {
ticket_id : id ,
2025-09-20 23:09:22 +01:00
user_id : ticket . user ,
subject : ticket . reason || "" ,
description : ticket . description || "" ,
2026-08-06 17:29:37 +01:00
created_at : existing . created_at || now ,
2026-03-10 20:02:43 +00:00
updated_at : now ,
department : ticket . department || "general" ,
priority : ticket . priority || "medium" ,
2026-03-11 18:58:15 +00:00
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 ,
2026-08-06 17:29:37 +01:00
status : existing . status || "open" ,
};
2026-08-10 20:15:00 +01:00
persistTicket ( id );
2026-08-06 16:30:04 +01:00
if ( global . apiServer ) {
global . apiServer . broadcast ( "ticket_created" , {
2026-08-06 17:29:37 +01:00
ticket_id : id ,
2026-08-06 16:30:04 +01:00
user_id : ticket . user ,
subject : ticket . reason || "" ,
description : ticket . description || "" ,
department : ticket . department || "general" ,
priority : ticket . priority || "medium" ,
2026-08-06 17:29:37 +01:00
created_at : now ,
2026-08-06 16:30:04 +01:00
});
}
2026-03-11 18:58:15 +00:00
},
updateTicketQuestionAnswers ( ticketId , questionAnswers = []) {
2026-08-06 17:29:37 +01:00
const t = state . tickets [ ticketId ];
if ( ! t ) return ;
t . questionAnswers = JSON . stringify ( questionAnswers );
t . updated_at = Date . now ();
2026-08-10 20:15:00 +01:00
persistTicket ( ticketId );
2025-09-20 23:09:22 +01:00
},
updateTicketVoice ( ticketId , voiceChannelId ) {
2026-08-06 17:29:37 +01:00
const t = state . tickets [ ticketId ];
if ( ! t ) return ;
t . voiceChannelId = voiceChannelId ;
t . updated_at = Date . now ();
2026-08-10 20:15:00 +01:00
persistTicket ( ticketId );
2025-09-20 23:09:22 +01:00
},
2026-03-10 20:02:43 +00:00
updateTicketStatus ( ticketId , status ) {
2026-08-06 17:29:37 +01:00
const t = state . tickets [ ticketId ];
if ( ! t ) return ;
t . status = status ;
t . updated_at = Date . now ();
2026-08-10 20:15:00 +01:00
persistTicket ( ticketId );
2026-08-06 16:30:04 +01:00
if ( global . apiServer && status === "closed" ) {
global . apiServer . broadcast ( "ticket_closed" , { ticket_id : ticketId });
}
2025-09-20 23:09:22 +01:00
},
2026-03-10 20:02:43 +00:00
updateTicketDepartment ( ticketId , department ) {
2026-08-06 17:29:37 +01:00
const t = state . tickets [ ticketId ];
if ( ! t ) return ;
t . department = department ;
t . updated_at = Date . now ();
2026-08-10 20:15:00 +01:00
persistTicket ( ticketId );
2025-09-20 23:09:22 +01:00
},
2026-03-10 20:02:43 +00:00
updateTicketPriority ( ticketId , priority ) {
2026-08-06 17:29:37 +01:00
const t = state . tickets [ ticketId ];
if ( ! t ) return ;
t . priority = priority ;
t . updated_at = Date . now ();
2026-08-10 20:15:00 +01:00
persistTicket ( ticketId );
2025-09-20 23:09:22 +01:00
},
2026-03-11 18:58:15 +00:00
setTicketAIState ( ticketId , aiState = {}) {
2026-08-06 17:29:37 +01:00
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 ();
2026-08-10 20:15:00 +01:00
persistTicket ( ticketId );
2026-08-06 17:29:37 +01:00
},
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 ();
2026-08-10 20:15:00 +01:00
persistTicket ( ticketId );
2026-03-11 18:58:15 +00:00
},
disableTicketAI ( ticketId ) {
2026-08-06 17:29:37 +01:00
const t = state . tickets [ ticketId ];
if ( ! t ) return ;
t . aiModeEnabled = 0 ;
t . updated_at = Date . now ();
2026-08-10 20:15:00 +01:00
persistTicket ( ticketId );
2026-03-11 18:58:15 +00:00
},
2025-09-20 23:09:22 +01:00
getTicket ( ticketId ) {
2026-08-06 17:29:37 +01:00
return normaliseTicketRow ( state . tickets [ ticketId ]);
2026-03-11 18:58:15 +00:00
},
getTicketByAIChannel ( aiChannelId ) {
2026-08-06 17:29:37 +01:00
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 ;
2025-09-20 23:09:22 +01:00
},
getAllTickets () {
2026-08-06 17:29:37 +01:00
return Object . values ( state . tickets ). map ( normaliseTicketRow );
2025-09-20 23:09:22 +01:00
},
saveTicketPanel ( messageId , channelId ) {
2026-08-10 20:15:00 +01:00
const row = { id : Date . now (), message_id : messageId , channel_id : channelId , created_at : Date . now () };
state . ticket_panel . push ( row );
persistPanel ( row );
2025-09-20 23:09:22 +01:00
},
getTicketPanel () {
2026-08-06 17:29:37 +01:00
const arr = state . ticket_panel ;
if ( ! arr || arr . length === 0 ) return null ;
return arr [ arr . length - 1 ];
2025-09-20 23:09:22 +01:00
},
addUserToTicket ( ticketId , userId ) {
2026-08-10 20:15:00 +01:00
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" );
}
2025-09-20 23:09:22 +01:00
},
removeUserFromTicket ( ticketId , userId ) {
2026-08-10 20:15:00 +01:00
if ( state . ticket_users [ ticketId ]) {
state . ticket_users [ ticketId ] = state . ticket_users [ ticketId ]. filter (( u ) => u !== userId );
persistTicketUser ( ticketId , userId , "remove" );
}
2025-09-20 23:09:22 +01:00
},
getTicketUsers ( ticketId ) {
2026-08-06 17:29:37 +01:00
return ( state . ticket_users [ ticketId ] || []). slice ();
2025-09-20 23:09:22 +01:00
},
getSettings () {
2026-08-06 17:29:37 +01:00
return state . settings || {};
2025-09-20 23:09:22 +01:00
},
2026-03-10 20:02:43 +00:00
getProfile ( userId ) {
ensureProfile ( userId );
2026-08-06 17:29:37 +01:00
const row = state . profiles [ userId ];
if ( ! row ) return { user_id : userId , bio : "" , timezone : "" , clockedIn : false };
return { ... row , clockedIn : Boolean ( row . clockedIn ) };
2026-03-10 20:02:43 +00:00
},
setClockedIn ( userId , clockedIn ) {
ensureProfile ( userId );
2026-08-06 17:29:37 +01:00
state . profiles [ userId ]. clockedIn = clockedIn ? 1 : 0 ;
2026-08-10 20:15:00 +01:00
persistProfile ( userId );
2026-03-10 20:02:43 +00:00
},
2025-09-20 23:09:22 +01:00
saveSettings ( obj ) {
2026-08-06 17:29:37 +01:00
state . settings = obj || {};
2026-08-10 20:15:00 +01:00
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"
};
2026-03-10 20:02:43 +00:00
},
2025-09-20 23:09:22 +01:00
};
2026-08-10 20:15:00 +01:00
// Initialize database on require
initDatabase ();
module . exports = exportsModule ;