const utils = require("../modules/utils") const crypto = require("node:crypto") const database = require("../modules/database") const { DefaultError } = require("../errors/errors") async function getSkins(uuid) { try { const sql = ` SELECT t.uuid as textureUuid, t.hash, t.url, ps.variant, ps.isSelected FROM playersSkins ps JOIN textures t ON ps.assetHash = t.hash WHERE ps.playerUuid = ? ` const rows = await database.query(sql, [uuid]) return rows } catch (error) { return utils.handleDBError(error) } } async function getCapes(uuid) { try { const sql = ` SELECT t.uuid as textureUuid, t.hash, t.url, t.alias, pc.isSelected FROM playersCapes pc JOIN textures t ON pc.assetHash = t.hash WHERE pc.playerUuid = ? ` const rows = await database.query(sql, [uuid]) return rows } catch (error) { return utils.handleDBError(error) } } async function addPropertyToPlayer(key, value, uuid) { try { const sql = ` INSERT INTO playersProperties (name, value, uuid) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE value = VALUES(value) ` const result = await database.query(sql, [key, value, uuid]) if (result.affectedRows > 0) { return { code: 200, key, value, uuid } } else { throw new DefaultError(500, "Please contact an administrator.", "InternalServerError") } } catch (error) { return utils.handleDBError(error) } } async function deletePropertyToPlayer(key, uuid) { try { const sql = `DELETE FROM playersProperties WHERE name = ? AND uuid = ?` const result = await database.query(sql, [key, uuid]) if (result.affectedRows > 0) { return { code: 200, key, uuid } } else { throw new DefaultError(500, "Property not found for this user/key combination.") } } catch (error) { return utils.handleDBError(error) } } async function updatePropertyToPlayer(key, value, uuid) { try { const sql = `UPDATE playersProperties SET value = ? WHERE name = ? AND uuid = ?` const result = await database.query(sql, [value, key, uuid]) if (result.affectedRows > 0) { return { code: 200, key, value, uuid } } else { throw new DefaultError(404, "Property not found for this user/key combination") } } catch (error) { return utils.handleDBError(error) } } async function getPlayerProperties(uuid) { try { const sql = `SELECT * FROM playersProperties WHERE uuid = ?` const rows = await database.query(sql, [uuid]) if (rows.length === 0) { throw new DefaultError(404, "Properties not found for this user") } return { code: 200, properties: rows.map(property => ({ name: property.name, value: property.value })) } } catch (error) { return utils.handleDBError(error) } } async function getPlayerProperty(key, uuid) { try { const sql = `SELECT * FROM playersProperties WHERE name = ? AND uuid = ?` const rows = await database.query(sql, [key, uuid]) const property = rows[0] if (!property) { throw new DefaultError(404, "Property not found for this user/key combination") } return { code: 200, property } } catch (error) { return utils.handleDBError(error) } } async function getPlayerPropertyByValue(key, value) { try { const sql = `SELECT * FROM playersProperties WHERE name = ? AND value = ?` const rows = await database.query(sql, [key, value]) const property = rows[0] if (!property) { throw new DefaultError(404, "No property found with this value for the specified key") } return { code: 200, property: { name: property.name, value: property.value, uuid: property.uuid } } } catch (error) { return utils.handleDBError(error) } } async function getPlayerSettingsSchema() { const RAW_SCHEMA_CACHE = { privileges: {}, preferences: {} } try { const privilegesRows = await database.query("DESCRIBE playersPrivileges") const preferencesRows = await database.query("DESCRIBE playersPreferences") RAW_SCHEMA_CACHE.privileges = privilegesRows.map(c => c.Field).filter(n => n !== "uuid") RAW_SCHEMA_CACHE.preferences = preferencesRows.map(c => c.Field).filter(n => n !== "uuid") return RAW_SCHEMA_CACHE } catch (error) { return utils.handleDBError(error) } } async function updatePlayerPreferences(uuid, updates) { try { const keys = Object.keys(updates) if (keys.length === 0) { throw new DefaultError(400, "No fields provided for update.") } const setClause = keys.map(key => `\`${key}\` = ?`).join(', ') const sql = `UPDATE playersPreferences SET ${setClause} WHERE uuid = ?` const values = keys.map(key => updates[key]) values.push(uuid) const result = await database.query(sql, values) if (result.affectedRows > 0) { return { code: 200, message: "Preferences updated successfully." } } else { throw new DefaultError(404, "Player preferences not found or no changes made.") } } catch (error) { return utils.handleDBError(error) } } async function getPlayerPreferences(uuid) { try { const sql = `SELECT profanityFilter FROM playersPreferences WHERE uuid = ?` const rows = await database.query(sql, [uuid]) const data = rows[0] if (data) { return { code: 200, message: "Preferences retrieved successfully.", data: data } } else { throw new DefaultError(404, "Preferences not found for this UUID.") } } catch (error) { return utils.handleDBError(error) } } async function getPlayerPrivileges(uuid) { try { const sql = ` SELECT onlineChat, multiplayerServer, multiplayerRealms, telemetry FROM playersPrivileges WHERE uuid = ? ` const rows = await database.query(sql, [uuid]) const data = rows[0] if (data) { return { code: 200, message: "Privileges retrieved successfully.", data: data } } else { throw new DefaultError(404, "Privileges not found for this UUID.") } } catch (error) { return utils.handleDBError(error) } } async function updatePlayerPrivileges(uuid, updates) { try { const keys = Object.keys(updates) if (keys.length === 0) { throw new DefaultError(404, "No fields provided for update.") } const setClause = keys.map(key => `\`${key}\` = ?`).join(', ') const sql = `UPDATE playersPrivileges SET ${setClause} WHERE uuid = ?` const values = keys.map(key => updates[key]) values.push(uuid) const result = await database.query(sql, values) if (result.affectedRows > 0) { return { code: 200, message: "Privileges updated successfully." } } else { throw new DefaultError(404, "Player privileges not found or no changes made.") } } catch (error) { return utils.handleDBError(error) } } async function banUser(uuid, { reasonKey, reasonMessage, expires = null }) { try { if (!uuid || !reasonKey) { throw new DefaultError(400, "Missing uuid or reasonKey.") } let reasonId const reasonRows = await database.query("SELECT id FROM banReasons WHERE reasonKey = ?", [reasonKey]) if (reasonRows.length > 0) { reasonId = reasonRows[0].id } else { const insertReason = await database.query("INSERT INTO banReasons (reasonKey) VALUES (?)", [reasonKey]) reasonId = insertReason.insertId } const banId = crypto.randomUUID() const insertSql = ` INSERT INTO bans (banId, uuid, reason, reasonMessage, expires) VALUES (?, ?, ?, ?, ?) ` const result = await database.query(insertSql, [banId, uuid, reasonId, reasonMessage || "Banned by operator", expires]) if (result.affectedRows > 0) { return { code: 200, message: "User successfully banned.", banId: banId } } else { throw new DefaultError(500, "Failed to ban user.") } } catch (error) { if (error instanceof DefaultError) throw error if (error.code === "ER_NO_REFERENCED_ROW_2" || error.toString().includes("foreign key constraint")) { throw new DefaultError(404, "User not found (cannot ban a ghost).") } return utils.handleDBError(error) } } async function unbanUser(uuid) { try { if (!uuid) { throw new DefaultError(400, "Missing uuid.") } const sql = "DELETE FROM bans WHERE uuid = ?" const result = await database.query(sql, [uuid]) if (result.affectedRows > 0) { return { code: 200, message: "User successfully unbanned.", count: result.affectedRows } } else { throw new DefaultError(404, "User was not banned.") } } catch (error) { return utils.handleDBError(error) } } async function getPlayerBans(uuid) { try { const sql = ` SELECT b.banId, b.expires, b.reasonMessage, r.reasonKey as reason FROM bans b JOIN banReasons r ON b.reason = r.id WHERE b.uuid = ? ORDER BY b.expires ASC ` const rows = await database.query(sql, [uuid]) if (rows.length > 0) { return { code: 200, bans: rows } } else { return { code: 204 } } } catch (error) { return utils.handleDBError(error) } } async function changeUsername(uuid, newName) { try { const sql = "UPDATE players SET username = ? WHERE uuid = ?" const result = await database.query(sql, [newName, uuid]) if (result.affectedRows > 0) { return { code: 200, message: "Username changed successfully" } } else { throw new DefaultError(404, "User not found") } } catch (error) { if (error instanceof DefaultError) throw error if (error.code === "ER_DUP_ENTRY" || error.errno === 1062) { throw new DefaultError(409, "Username already taken", "ForbiddenOperationException") } return utils.handleDBError(error) } } async function createTexture(uuid, hash, type, url, alias) { try { const sql = ` INSERT INTO textures (uuid, hash, type, url, alias) VALUES (?, ?, ?, ?, ?) ` await database.query(sql, [uuid, hash, type, url, alias]) return true } catch (error) { if (error.code === 'ER_DUP_ENTRY') { return false } return utils.handleDBError(error) } } async function getTextureByUuid(textureUuid) { try { const sql = "SELECT hash FROM textures WHERE uuid = ?" const rows = await database.query(sql, [textureUuid]) return rows[0] || null } catch (error) { return utils.handleDBError(error) } } async function getTextureByHash(hash) { try { const sql = "SELECT uuid FROM textures WHERE hash = ?" const rows = await database.query(sql, [hash]) return rows[0] } catch (error) { return utils.handleDBError(error) } } async function resetSkin(uuid, hash, variant) { try { const insertSql = ` INSERT IGNORE INTO playersSkins (playerUuid, assetHash, variant, isSelected) VALUES (?, ?, ?, 0) ` await database.query(insertSql, [uuid, hash, variant]) const updateSql = ` UPDATE playersSkins SET isSelected = (assetHash = ?) WHERE playerUuid = ? ` await database.query(updateSql, [hash, uuid]) return { code: 200 } } catch (error) { return utils.handleDBError(error) } } async function hideCape(uuid) { try { const sql = "UPDATE playersCapes SET isSelected = 0 WHERE playerUuid = ?" await database.query(sql, [uuid]) return { code: 200 } } catch (error) { return utils.handleDBError(error) } } async function showCape(uuid, hash) { try { await database.query( "UPDATE playersCapes SET isSelected = 0 WHERE playerUuid = ?", [uuid] ) const result = await database.query( "UPDATE playersCapes SET isSelected = 1 WHERE playerUuid = ? AND assetHash = ?", [uuid, hash] ) const affectedRows = Array.isArray(result) ? result[0].affectedRows : result.affectedRows return { code: 200, changed: affectedRows > 0 } } catch (error) { return utils.handleDBError(error) } } async function checkCapeOwnership(uuid, hash) { try { const sql = "SELECT 1 FROM playersCapes WHERE playerUuid = ? AND assetHash = ?" const rows = await database.query(sql, [uuid, hash]) return rows.length > 0 } catch (error) { return utils.handleDBError(error) } } async function getPlayerMeta(uuid) { try { const sql = `SELECT createdAt, nameChangeAllowed FROM players WHERE uuid = ?` const rows = await database.query(sql, [uuid]) return rows[0] } catch (error) { return utils.handleDBError(error) } } async function getLastNameChange(uuid) { try { const sql = ` SELECT changedAt FROM uuidToNameHistory WHERE uuid = ? AND changedAt IS NOT NULL ORDER BY changedAt DESC LIMIT 1 ` const rows = await database.query(sql, [uuid]) return rows[0] } catch (error) { return utils.handleDBError(error) } } async function getPlayerCertificate(uuid) { try { const sql = "SELECT * FROM playerCertificates WHERE uuid = ?" const rows = await database.query(sql, [uuid]) return rows[0] } catch (error) { return utils.handleDBError(error) } } async function savePlayerCertificate(uuid, privateKey, publicKey, signatureV2, expiresAt, refreshedAfter) { try { const sql = ` REPLACE INTO playerCertificates (uuid, privateKey, publicKey, publicKeySignatureV2, expiresAt, refreshedAfter) VALUES (?, ?, ?, ?, ?, ?) ` const result = await database.query(sql, [uuid, privateKey, publicKey, signatureV2, expiresAt, refreshedAfter]) return result.affectedRows > 0 } catch (error) { return utils.handleDBError(error) } } async function deleteExpiredCertificates(isoDate) { try { const sql = "DELETE FROM playerCertificates WHERE expiresAt < ?" const result = await database.query(sql, [isoDate]) return result.affectedRows } catch (error) { return utils.handleDBError(error) } } async function addProfileAction(uuid, actionCode) { try { const cleanUuid = uuid.replace(/-/g, "") const sql = "INSERT IGNORE INTO playerProfileActions (uuid, action) VALUES (?, ?)" const result = await database.query(sql, [cleanUuid, actionCode]) return result.affectedRows > 0 } catch (error) { return utils.handleDBError(error) } } async function removeProfileAction(uuid, actionCode) { try { const cleanUuid = uuid.replace(/-/g, "") const sql = "DELETE FROM playerProfileActions WHERE uuid = ? AND action = ?" const result = await database.query(sql, [cleanUuid, actionCode]) return result.affectedRows } catch (error) { return utils.handleDBError(error) } } async function getPlayerActions(uuid) { try { const cleanUuid = uuid.replace(/-/g, "") const sql = "SELECT action FROM playerProfileActions WHERE uuid = ?" const rows = await database.query(sql, [cleanUuid]) return rows.map(r => r.action) } catch (error) { return utils.handleDBError(error) } } async function clearAllPlayerActions(uuid) { try { const cleanUuid = uuid.replace(/-/g, "") const sql = "DELETE FROM playerProfileActions WHERE uuid = ?" const result = await database.query(sql, [cleanUuid]) return result.affectedRows } catch (error) { return utils.handleDBError(error) } } async function blockPlayer(blockerUuid, blockedUuid) { try { const sql = `INSERT IGNORE INTO playersBlockslist (blockerUuid, blockedUuid) VALUES (?, ?)` const result = await database.query(sql, [blockerUuid, blockedUuid]) return result.affectedRows > 0 } catch (error) { return utils.handleDBError(error) } } async function unblockPlayer(blockerUuid, blockedUuid) { try { const sql = `DELETE FROM playersBlockslist WHERE blockerUuid = ? AND blockedUuid = ?` const result = await database.query(sql, [blockerUuid, blockedUuid]) return result.affectedRows > 0 } catch (error) { return utils.handleDBError(error) } } async function getBlockedUuids(blockerUuid) { try { const sql = `SELECT blockedUuid FROM playersBlockslist WHERE blockerUuid = ?` const rows = await database.query(sql, [blockerUuid]) return rows.map(r => r.blockedUuid) } catch (error) { return utils.handleDBError(error) } } async function isBlocked(blockerUuid, targetUuid) { try { const sql = `SELECT 1 FROM playersBlockslist WHERE blockerUuid = ? AND blockedUuid = ? LIMIT 1` const rows = await database.query(sql, [blockerUuid, targetUuid]) return rows.length > 0 } catch (error) { return utils.handleDBError(error) } } async function getUsersByNames(usernames) { try { if (!usernames || usernames.length === 0) return [] const uniqueNames = [...new Set(usernames)] const placeholders = uniqueNames.map(() => "?").join(", ") const sql = `SELECT uuid, username FROM players WHERE username IN (${placeholders})` const rows = await database.query(sql, uniqueNames) return rows } catch (error) { return utils.handleDBError(error) } } async function getUuidAndUsername(username) { try { const sql = "SELECT uuid, username FROM players WHERE username = ?" const rows = await database.query(sql, [username]) return rows[0] || null } catch (error) { return utils.handleDBError(error) } } async function getProfileByUsername(username) { try { const sql = "SELECT uuid, username FROM players WHERE username = ?" const rows = await database.query(sql, [username]) return rows[0] || null } catch (error) { return utils.handleDBError(error) } } async function getProfileByHistory(username, isoDate) { try { const sql = ` SELECT uuid, username FROM uuidToNameHistory WHERE username = ? AND (changedAt <= ? OR changedAt IS NULL) ORDER BY changedAt DESC LIMIT 1 ` const rows = await database.query(sql, [username, isoDate]) return rows[0] || null } catch (error) { return utils.handleDBError(error) } } async function getNameHistory(uuid) { try { const sql = ` SELECT username, changedAt FROM uuidToNameHistory WHERE uuid = ? ORDER BY changedAt ASC ` const rows = await database.query(sql, [uuid]) return rows } catch (error) { return utils.handleDBError(error) } } async function setSkin(uuid, hash, variant) { try { const resetSql = `UPDATE playersSkins SET isSelected = 0 WHERE playerUuid = ?` await database.query(resetSql, [uuid]) const upsertSql = ` INSERT INTO playersSkins (playerUuid, assetHash, variant, isSelected) VALUES (?, ?, ?, 1) ON DUPLICATE KEY UPDATE isSelected = 1, variant = VALUES(variant) ` await database.query(upsertSql, [uuid, hash, variant.toLowerCase()]) return true } catch (error) { return utils.handleDBError(error) } } async function updatePassword(uuid, hashedPassword) { try { const sql = "UPDATE players SET password = ? WHERE uuid = ?" const result = await database.query(sql, [hashedPassword, uuid]) if (result.affectedRows > 0) { return { code: 200, message: "Password updated successfully" } } else { throw new DefaultError(404, "User not found") } } catch (error) { return utils.handleDBError(error) } } async function addCapeToPlayer(uuid, hash) { try { const sql = ` INSERT INTO playersCapes (playerUuid, assetHash, isSelected) VALUES (?, ?, 0) ` const result = await database.query(sql, [uuid, hash]) if (result.affectedRows > 0) { return { code: 200, message: "Cape granted to the player." } } throw new DefaultError(500, "Error when assigning the cape.") } catch (error) { if (error.code === "ER_DUP_ENTRY") { throw new DefaultError(409, "The player already possesses this cloak.") } return utils.handleDBError(error) } } async function removeCapeFromPlayer(uuid, hash) { try { const sql = "DELETE FROM playersCapes WHERE playerUuid = ? AND assetHash = ?" const result = await database.query(sql, [uuid, hash]) if (result.affectedRows > 0) { return { code: 200, message: "Cape removed from player." } } else { throw new DefaultError(404, "The player does not own this cloak.") } } catch (error) { return utils.handleDBError(error) } } async function deleteTexture(hash) { try { const sql = "DELETE FROM textures WHERE hash = ?" const result = await database.query(sql, [hash]) return result.affectedRows > 0 } catch (error) { return utils.handleDBError(error) } } module.exports = { setSkin, banUser, getSkins, getCapes, showCape, hideCape, resetSkin, isBlocked, unbanUser, blockPlayer, deleteTexture, createTexture, getPlayerBans, getPlayerMeta, unblockPlayer, changeUsername, getNameHistory, updatePassword, getBlockedUuids, getUsersByNames, addCapeToPlayer, getPlayerActions, getTextureByUuid, getTextureByHash, addProfileAction, getLastNameChange, getPlayerProperty, getUuidAndUsername, checkCapeOwnership, getProfileByHistory, getPlayerPrivileges, getPlayerProperties, removeProfileAction, addPropertyToPlayer, removeCapeFromPlayer, getProfileByUsername, getPlayerPreferences, getPlayerCertificate, clearAllPlayerActions, savePlayerCertificate, updatePlayerPrivileges, deletePropertyToPlayer, updatePropertyToPlayer, getPlayerSettingsSchema, updatePlayerPreferences, getPlayerPropertyByValue, deleteExpiredCertificates, }