fable
This commit is contained in:
@@ -163,6 +163,31 @@ class Database {
|
||||
UNIQUE(site_id, player_name)
|
||||
)
|
||||
`);
|
||||
|
||||
// Application settings table (runtime-tunable, editable via web UI and LLM)
|
||||
await this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'string' CHECK(type IN ('string', 'number', 'boolean', 'json', 'secret')),
|
||||
category TEXT NOT NULL DEFAULT 'general',
|
||||
label TEXT,
|
||||
description TEXT,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
`);
|
||||
|
||||
// Per-bot settings table (autoConnect, autoReConnect, onDemand, plugins, etc.)
|
||||
await this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS bot_settings (
|
||||
bot_name TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'string' CHECK(type IN ('string', 'number', 'boolean', 'json', 'secret')),
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY(bot_name, key)
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
async insertDefaultPermissions() {
|
||||
@@ -181,6 +206,134 @@ class Database {
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Settings
|
||||
// ========================================
|
||||
|
||||
async seedDefaultSettings(defaults) {
|
||||
for (const entry of defaults) {
|
||||
try {
|
||||
await this.db.run(
|
||||
'INSERT OR IGNORE INTO settings (key, value, type, category, label, description) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
[entry.key, entry.value, entry.type, entry.category, entry.label, entry.description || null]
|
||||
);
|
||||
} catch (error) {
|
||||
console.error('Error seeding setting:', entry.key, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getAllSettings() {
|
||||
return await this.db.all('SELECT * FROM settings ORDER BY category, key');
|
||||
}
|
||||
|
||||
async getSettingsByCategory(category) {
|
||||
return await this.db.all('SELECT * FROM settings WHERE category = ? ORDER BY key', [category]);
|
||||
}
|
||||
|
||||
async getSetting(key) {
|
||||
return await this.db.get('SELECT * FROM settings WHERE key = ?', [key]);
|
||||
}
|
||||
|
||||
async setSetting(key, value) {
|
||||
const row = await this.db.get('SELECT type FROM settings WHERE key = ?', [key]);
|
||||
if (!row) throw new Error(`Unknown setting: ${key}`);
|
||||
const coerced = this._coerceValue(value, row.type);
|
||||
await this.db.run(
|
||||
'UPDATE settings SET value = ?, updated_at = CURRENT_TIMESTAMP WHERE key = ?',
|
||||
[coerced, key]
|
||||
);
|
||||
return coerced;
|
||||
}
|
||||
|
||||
_coerceValue(value, type) {
|
||||
switch (type) {
|
||||
case 'number': {
|
||||
const n = Number(value);
|
||||
if (isNaN(n)) throw new Error(`Expected number, got: ${value}`);
|
||||
return String(n);
|
||||
}
|
||||
case 'boolean': return value === true || value === 'true' || value === '1' ? 'true' : 'false';
|
||||
case 'json': return JSON.stringify(typeof value === 'string' ? JSON.parse(value) : value);
|
||||
default: return String(value);
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Bot Settings (per-bot configuration)
|
||||
// ========================================
|
||||
|
||||
async getAllBotSettings(botName) {
|
||||
return await this.db.all('SELECT * FROM bot_settings WHERE bot_name = ? ORDER BY key', [botName]);
|
||||
}
|
||||
|
||||
async getBotSetting(botName, key) {
|
||||
return await this.db.get('SELECT * FROM bot_settings WHERE bot_name = ? AND key = ?', [botName, key]);
|
||||
}
|
||||
|
||||
async setBotSetting(botName, key, value, type) {
|
||||
const coerced = this._coerceValue(value, type || 'string');
|
||||
await this.db.run(`
|
||||
INSERT INTO bot_settings (bot_name, key, value, type)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(bot_name, key) DO UPDATE SET
|
||||
value = excluded.value,
|
||||
type = excluded.type,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`, [botName, key, coerced, type || 'string']);
|
||||
return coerced;
|
||||
}
|
||||
|
||||
async setBotSettings(botName, settings) {
|
||||
await this.db.run('SAVEPOINT setBotSettings');
|
||||
try {
|
||||
for (const [key, entry] of Object.entries(settings)) {
|
||||
const type = entry.type || 'string';
|
||||
const coerced = this._coerceValue(entry.value, type);
|
||||
await this.db.run(`
|
||||
INSERT INTO bot_settings (bot_name, key, value, type)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(bot_name, key) DO UPDATE SET
|
||||
value = excluded.value,
|
||||
type = excluded.type,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
`, [botName, key, coerced, type]);
|
||||
}
|
||||
await this.db.run('RELEASE setBotSettings');
|
||||
} catch (error) {
|
||||
await this.db.run('ROLLBACK TO setBotSettings');
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async seedDefaultBotSettings() {
|
||||
const conf = require('../../conf');
|
||||
const bots = conf.mc?.bots || {};
|
||||
for (const [botName, botConfig] of Object.entries(bots)) {
|
||||
const defaults = {
|
||||
username: { value: String(botConfig.username || ''), type: 'string' },
|
||||
password: { value: String(botConfig.password || ''), type: 'secret' },
|
||||
auth: { value: String(botConfig.auth || 'microsoft'), type: 'string' },
|
||||
autoConnect: { value: String(botConfig.autoConnect ?? true), type: 'boolean' },
|
||||
autoReConnect: { value: String(botConfig.autoReConnect ?? true), type: 'boolean' },
|
||||
onDemand: { value: String(botConfig.onDemand || false), type: 'boolean' },
|
||||
idleTimeout: { value: String(botConfig.idleTimeout || 30000), type: 'number' },
|
||||
commands: { value: JSON.stringify(botConfig.commands || []), type: 'json' },
|
||||
plugins: { value: JSON.stringify(botConfig.plugins || {}), type: 'json' },
|
||||
hasAi: { value: String(botConfig.hasAi || false), type: 'boolean' },
|
||||
};
|
||||
for (const [key, entry] of Object.entries(defaults)) {
|
||||
try {
|
||||
await this.db.run(
|
||||
'INSERT OR IGNORE INTO bot_settings (bot_name, key, value, type) VALUES (?, ?, ?, ?)',
|
||||
[botName, key, entry.value, entry.type]
|
||||
);
|
||||
} catch (e) { /* ignore duplicates */ }
|
||||
}
|
||||
}
|
||||
console.log('Bot settings seeded from config defaults');
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Permissions
|
||||
// ========================================
|
||||
@@ -238,6 +391,11 @@ class Database {
|
||||
return await this.db.get('SELECT * FROM chests WHERE id = ?', [id]);
|
||||
}
|
||||
|
||||
async markChestLost(x, y, z) {
|
||||
await this.db.run('DELETE FROM chests WHERE pos_x = ? AND pos_y = ? AND pos_z = ?', [x, y, z]);
|
||||
console.log(`Scanner: Removed lost chest at ${x},${y},${z}`);
|
||||
}
|
||||
|
||||
async getChestByPosition(x, y, z) {
|
||||
return await this.db.get(
|
||||
'SELECT * FROM chests WHERE pos_x = ? AND pos_y = ? AND pos_z = ?',
|
||||
@@ -258,6 +416,29 @@ class Database {
|
||||
`, values);
|
||||
}
|
||||
|
||||
async batchUpsertChests(chests) {
|
||||
if (!chests || chests.length === 0) return;
|
||||
await this.db.run('BEGIN TRANSACTION');
|
||||
try {
|
||||
for (const c of chests) {
|
||||
await this.db.run(`
|
||||
INSERT INTO chests (pos_x, pos_y, pos_z, chest_type, row, column, category)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(pos_x, pos_y, pos_z) DO UPDATE SET
|
||||
chest_type = excluded.chest_type,
|
||||
row = excluded.row,
|
||||
column = excluded.column,
|
||||
category = excluded.category,
|
||||
last_scan = CURRENT_TIMESTAMP
|
||||
`, [c.x, c.y, c.z, c.type, c.row, c.column, c.category]);
|
||||
}
|
||||
await this.db.run('COMMIT');
|
||||
} catch (err) {
|
||||
await this.db.run('ROLLBACK');
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Shulkers
|
||||
// ========================================
|
||||
@@ -325,6 +506,10 @@ class Database {
|
||||
return await this.db.run('DELETE FROM shulkers WHERE id = ?', [id]);
|
||||
}
|
||||
|
||||
async deleteShulkersByChest(chestId) {
|
||||
return await this.db.run('DELETE FROM shulkers WHERE chest_id = ?', [chestId]);
|
||||
}
|
||||
|
||||
// Find a shulker that already stores this item type and has space (<27 slots used, not in-transit)
|
||||
async findShulkerWithSpace(itemName, excludeId = null) {
|
||||
return await this.db.get(`
|
||||
|
||||
Reference in New Issue
Block a user