diff --git a/config.example.json b/config.example.json index 22dbb5b..e5f0730 100644 --- a/config.example.json +++ b/config.example.json @@ -29,9 +29,7 @@ "keyWords": ["radio", "zamrock", "psychedelic", "blues", "rock"] } }, - "radio": [ - - ], + "radio": [], "cache": { "guilds": { "enabled": true, @@ -45,6 +43,7 @@ "helpCatalog": true, "helpPagination": true, "mapMembers": true, + "db": "mongo/mysql", "mysql": { "host": "your-mysql-host.com", "port": 3306, @@ -52,6 +51,10 @@ "password": "password of your user", "database": "name of the target database" }, + "mongodb": { + "uri": "mongodb://localhost:27017", + "database": "your_bot_database" + }, "rbl": "Your token for revoltbots.org; optional", "webPort": 80, "dashboardUrl": "The url the dashboard is accessible with.", @@ -72,4 +75,4 @@ "owners": ["not necessary unless you want to use testing commands", "another user id"], "playerAFKTimeout": 60000, "Comment": "The above specifies the time in ms after which a player goes inactive; Optional" -} +} \ No newline at end of file diff --git a/index.js b/index.js index b520897..6c68e65 100644 --- a/index.js +++ b/index.js @@ -4,7 +4,8 @@ const { Revoice } = require("revoice.js"); const { Client } = require("revolt.js"); const path = require("path"); const fs = require("fs"); -const { SettingsManager, RemoteSettingsManager } = require("./settings/Settings.js"); +const dns = require('node:dns'); +const { createSettingsManager } = require("./settings/Settings.js"); if (!process.execArgv.includes("--inspect")) require('console-stamp')(console, 'HH:MM:ss.l'); const YTDlpWrap = require("yt-dlp-wrap-extended").default; const { Innertube, Platform } = require("youtubei.js"); @@ -43,8 +44,9 @@ class Remix { this.settingsMgr.loadDefaultsSync("./storage/defaults.json");*/ // updated settings manager based on a mysql database: // TODO: add self-hosting instr - this.settingsMgr = new RemoteSettingsManager(this.config.mysql, "./storage/defaults.json"); - + // Pass the URI and the Database name from your new mongodb config block + dns.setDefaultResultOrder('ipv4first'); + this.settingsMgr = createSettingsManager(this.config, "./storage/defaults.json"); this.uploader = new Uploader(this.client); this.geniusClient = new Genius.Client(this.config.geniusToken); diff --git a/package.json b/package.json index 2375202..0ea24a6 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "genius-lyrics": "^4.4.3", "i18next": "^23.1.0", "i18next-fs-backend": "^2.1.5", + "mongodb": "^7.1.0", "mysql": "^2.18.1", "prism-media": "^1.3.5", "revoice.js": "^0.2.168", @@ -43,3 +44,4 @@ "defaultsSync": "node index.js sreload" } } + diff --git a/settings/Settings.js b/settings/Settings.js index aa1aa5d..a405d01 100644 --- a/settings/Settings.js +++ b/settings/Settings.js @@ -1,8 +1,7 @@ const fs = require("fs"); -const mysql = require("mysql") const EventEmitter = require("events"); -class ServerSettings { // TODO: switch to better db system +class ServerSettings { id; manager; data = {}; @@ -10,9 +9,7 @@ class ServerSettings { // TODO: switch to better db system constructor(id, mgr) { this.id = id; this.manager = mgr; - this.loadDefaults(); - return this; } set(key, value) { @@ -28,7 +25,6 @@ class ServerSettings { // TODO: switch to better db system getAll() { return this.data; } - loadDefaults() { for (let key in this.manager.defaults) { this.data[key] = this.manager.defaults[key]; @@ -42,32 +38,19 @@ class ServerSettings { // TODO: switch to better db system } checkDefaults(d) { for (let key in d) { - if (!this.data[key]) this.data[key] = d[key]; + if (this.data[key] === undefined) this.data[key] = d[key]; } } get serializationData() { - return { - ...this.data, - id: this.id - } - } - serialize() { - return this.serializationData; - } - serializeObject() { - return this.serializationData; + return { ...this.data, id: this.id }; } + serialize() { return this.serializationData; } + serializeObject() { return this.serializationData; } } - /** - * Creates a server settings manager. Stores everything in a JSON file. - * To save the current configs you have to call .saveAsync() - * Deprecated for obvious reasons. Use RemoteSettingsManager instead. - * - * @class SettingsManager - * - * @deprecated + * Local file-based settings manager. + * @deprecated Use MySQLSettingsManager or MongoSettingsManager instead. */ class SettingsManager { guilds = new Map(); @@ -75,23 +58,11 @@ class SettingsManager { defaults = {}; descriptions = {}; - constructor(storagePath=null) { + constructor(storagePath = null) { if (storagePath) this.storagePath = storagePath; - this.load(); - return this; } - loadDefaults(filePath) { - return new Promise(res => { - fs.readFile(filePath, (d) => { - let parsed = JSON.parse(d); - this.descriptions = parsed.descriptions; - this.defaults = parsed.values; - res(this.defaults); - }); - }); - } loadDefaultsSync(filePath) { const d = fs.readFileSync(filePath, "utf8"); let parsed = JSON.parse(d); @@ -100,10 +71,7 @@ class SettingsManager { } load() { if (!fs.existsSync(this.storagePath)) { - const data = { - servers: [] - }; - fs.writeFileSync(this.storagePath, JSON.stringify(data)); + fs.writeFileSync(this.storagePath, JSON.stringify({ servers: [] })); } let json = JSON.parse(fs.readFileSync(this.storagePath, "utf8")); json.servers.forEach((s) => { @@ -114,164 +82,219 @@ class SettingsManager { }); } syncDefaults() { - let missing = (arr, target) => { - return arr.filter(i => target[i] === undefined); - } this.guilds.forEach((val, key) => { if (!this.hasServer(key)) return; - let missingValues = missing(Object.keys(this.defaults), this.guilds.get(key).getAll()); - if (missingValues.length == 0) return; - - missingValues.forEach(m => { - val.set(m, this.defaults[m]); - }); + const missing = Object.keys(this.defaults).filter(i => this.guilds.get(key).getAll()[i] === undefined); + missing.forEach(m => val.set(m, this.defaults[m])); }); } save() { let s = []; - this.guilds.forEach((val, _k) => { - s.push(val.serialize()); - }); - const d = { - servers: s - } - fs.writeFileSync(this.storagePath, JSON.stringify(d)); + this.guilds.forEach((val) => s.push(val.serialize())); + fs.writeFileSync(this.storagePath, JSON.stringify({ servers: s })); } saveAsync() { return new Promise((res) => { let s = []; - this.guilds.forEach((val, _k) => { - s.push(val.serializeObject()); - }); - const d = { - servers: s - } - fs.writeFile(this.storagePath, JSON.stringify(d), () => { - res(); - }); + this.guilds.forEach((val) => s.push(val.serializeObject())); + fs.writeFile(this.storagePath, JSON.stringify({ servers: s }), () => res()); }); } update(server, key) { - if (!this.guilds.has(server.id)) { - this.guilds.set(server.id, server); - } - const s = this.guilds.get(server.id); - s.data[key] = server.data[key]; - } - isOption(key) { - return key in this.defaults; - } - - hasServer(id) { - return this.guilds.has(id); + if (!this.guilds.has(server.id)) this.guilds.set(server.id, server); + this.guilds.get(server.id).data[key] = server.data[key]; } + isOption(key) { return key in this.defaults; } + hasServer(id) { return this.guilds.has(id); } getServer(id) { return (!this.guilds.has(id)) ? new ServerSettings(id, this) : this.guilds.get(id); } } -class RemoteSettingsManager extends EventEmitter { // mysql based manager - // TODO: setup instructions +/** + * MySQL-based remote settings manager. + * Config: pass a mysql connection config object. + */ +class MySQLSettingsManager extends EventEmitter { guilds = new Map(); - descriptions = {}; defaults = {}; - - serverConfig = null; - + descriptions = {}; db = null; constructor(config, defaultsPath) { super(); - - this.db = mysql.createPool({ - connectionLimit : 15, - ...config - }); - + const mysql = require("mysql"); + this.db = mysql.createPool({ connectionLimit: 15, ...config }); if (defaultsPath) this.loadDefaultsSync(defaultsPath); - this.load(); - return this; } - query(query) { return new Promise(res => { - this.db.query(query, (error, results, fields) => { res({ error, results, fields })}); + this.db.query(query, (error, results, fields) => res({ error, results, fields })); }); } - async load() { const res = await this.query("SELECT * FROM settings"); if (res.error) { - console.error("settings init error; ", res.error); - console.error("retrying in 2 seconds"); - return setTimeout(() => { - this.load(); - }, 2000); + console.error("Settings init error:", res.error, "— retrying in 2s"); + return setTimeout(() => this.load(), 2000); } - - const results = res.results; - results.forEach((r) => { + res.results.forEach((r) => { let server = new ServerSettings(r.id, this); server.deserialize(JSON.parse(r.data)); server.checkDefaults(this.defaults); this.guilds.set(server.id, server); }); - this.emit("ready"); } async remoteUpdate(server, key) { - const r = await this.query("UPDATE settings SET data = JSON_SET(data, '$." + key + "', '" + server.data[key] + "') WHERE id='" + server.id + "'") - if (r.error) console.error("settings update error; ", r.error); + const r = await this.query(`UPDATE settings SET data = JSON_SET(data, '$.${key}', '${server.data[key]}') WHERE id='${server.id}'`); + if (r.error) console.error("Settings update error:", r.error); } async remoteSave(server) { - const r = await this.query("UPDATE settings SET data = '" + JSON.stringify(server.data) + "' WHERE id='" + server.id + "'"); - if (r.error) console.error("settings server save error; ", r.error); + const r = await this.query(`UPDATE settings SET data = '${JSON.stringify(server.data)}' WHERE id='${server.id}'`); + if (r.error) console.error("Settings save error:", r.error); } - - loadDefaultsSync(filePath) { - const d = fs.readFileSync(filePath, "utf8"); - let parsed = JSON.parse(d); - this.descriptions = parsed.descriptions; - this.defaults = parsed.values; + async create(id, server) { + const r = await this.query(`INSERT INTO settings (id, data) VALUES ('${id}', '${JSON.stringify(server.data)}')`); + if (r.error) console.error("Settings create error:", r.error); } - saveAsync() { return new Promise(async (res) => { - const p = []; // promises - this.guilds.forEach((val, _k) => { - p.push(this.remoteSave(val)); - }); - + const p = []; + this.guilds.forEach((val) => p.push(this.remoteSave(val))); await Promise.allSettled(p); res(); }); } - async create(id, server) { - const r = await this.query("INSERT INTO settings (id, data) VALUES ('" + id + "', '" + JSON.stringify(server.data) + "')"); - if (r.error) console.error("settings create server error; ", r.error); + loadDefaultsSync(filePath) { + const d = fs.readFileSync(filePath, "utf8"); + let parsed = JSON.parse(d); + this.descriptions = parsed.descriptions; + this.defaults = parsed.values; } - update(server, key) { if (!this.guilds.has(server.id)) { this.guilds.set(server.id, server); this.create(server.id, server); } - const s = this.guilds.get(server.id); - s.data[key] = server.data[key]; + this.guilds.get(server.id).data[key] = server.data[key]; this.remoteUpdate(server, key); } - isOption(key) { - return key in this.defaults; + isOption(key) { return key in this.defaults; } + hasServer(id) { return this.guilds.has(id); } + getServer(id) { + return (!this.guilds.has(id)) ? new ServerSettings(id, this) : this.guilds.get(id); } +} + +/** + * MongoDB-based remote settings manager. + * Config: pass mongoURI and dbName. + */ +class MongoSettingsManager extends EventEmitter { + guilds = new Map(); + defaults = {}; + descriptions = {}; + db = null; + collection = null; - hasServer(id) { - return this.guilds.has(id); + constructor(mongoURI, dbName, defaultsPath) { + super(); + const { MongoClient } = require("mongodb"); + if (defaultsPath) this.loadDefaultsSync(defaultsPath); + const client = new MongoClient(mongoURI); + client.connect() + .then(() => { + console.log("MongoDB connected"); + this.db = client.db(dbName); + this.collection = this.db.collection("ServerSettings"); + this.load(); + }) + .catch(err => { + console.error("Mongo connection error:", err, "— retrying in 3s"); + setTimeout(() => new MongoSettingsManager(mongoURI, dbName, defaultsPath), 3000); + }); + } + async load() { + try { + const results = await this.collection.find().toArray(); + results.forEach(doc => { + const server = new ServerSettings(doc.id, this); + server.deserialize(doc.data); + server.checkDefaults(this.defaults); + this.guilds.set(server.id, server); + }); + this.emit("ready"); + } catch (err) { + console.error("Settings load error:", err, "— retrying in 3s"); + setTimeout(() => this.load(), 3000); + } } + async update(server, key) { + if (!this.collection) return console.error("DB not ready, cannot update."); + if (!this.guilds.has(server.id)) this.guilds.set(server.id, server); + await this.collection.updateOne( + { id: server.id }, + { $set: { [`data.${key}`]: server.data[key] } }, + { upsert: true } + ); + } + async saveServer(server) { + if (!this.collection) return console.error("DB not ready, cannot save."); + await this.collection.updateOne( + { id: server.id }, + { $set: { data: server.data } }, + { upsert: true } + ); + } + async saveAsync() { + const promises = []; + this.guilds.forEach(server => promises.push(this.saveServer(server))); + await Promise.allSettled(promises); + } + loadDefaultsSync(filePath) { + const raw = fs.readFileSync(filePath, "utf8"); + const parsed = JSON.parse(raw); + this.descriptions = parsed.descriptions; + this.defaults = parsed.values; + } + isOption(key) { return key in this.defaults; } + hasServer(id) { return this.guilds.has(id); } getServer(id) { - return (!this.guilds.has(id)) ? new ServerSettings(id, this) : this.guilds.get(id); + if (!this.guilds.has(id)) { + const server = new ServerSettings(id, this); + this.guilds.set(id, server); + return server; + } + return this.guilds.get(id); } } -module.exports = { SettingsManager, RemoteSettingsManager, ServerSettings }; +/** + * Factory: returns the right manager based on config. + * + * In config.json, set: + * "db": "mongo" → uses MongoSettingsManager + * "db": "mysql" → uses MySQLSettingsManager + * (omitted) → defaults to mongo + */ +function createSettingsManager(config, defaultsPath) { + const type = (config.db || "mongo").toLowerCase(); + if (type === "mysql") { + console.log("[Settings] Using MySQL backend"); + return new MySQLSettingsManager(config.mysql, defaultsPath); + } + console.log("[Settings] Using MongoDB backend"); + return new MongoSettingsManager(config.mongodb.uri, config.mongodb.database, defaultsPath); +} + +module.exports = { + SettingsManager, + MySQLSettingsManager, + MongoSettingsManager, + RemoteSettingsManager: MongoSettingsManager, // backwards compat alias + createSettingsManager, + ServerSettings +}; \ No newline at end of file diff --git a/settings/migrate.js b/settings/migrate.js index 36cb270..b9f56e0 100644 --- a/settings/migrate.js +++ b/settings/migrate.js @@ -1,21 +1,54 @@ -const { SettingsManager, RemoteSettingsManager } = require("./Settings.js"); +const { SettingsManager, MySQLSettingsManager, MongoSettingsManager } = require("./Settings.js"); const config = require("../config.json"); +const TARGET = (config.db || "mongo").toLowerCase(); + const sm = new SettingsManager(); -const rsm = new RemoteSettingsManager(config.mysql); +sm.loadDefaultsSync("./storage/defaults.json"); +const servers = Array.from(sm.guilds.entries()); -const servers = sm.guilds.entries(); +if (servers.length === 0) { + console.log("No servers found in local storage. Nothing to migrate."); + process.exit(0); +} -rsm.on("ready", async () => { - for (const [id, s] of servers) { - console.log(id); - if (rsm.hasServer(id)) { - await rsm.remoteSave(s); - continue; - } +console.log(`Migrating ${servers.length} server(s) to ${TARGET}...`); - await rsm.create(id, s); - } +if (TARGET === "mongo") { + const rsm = new MongoSettingsManager( + config.mongodb.uri, + config.mongodb.database, + "./storage/defaults.json" + ); + rsm.on("ready", async () => { + for (const [id, s] of servers) { + try { + await rsm.saveServer(s); + console.log(`✓ ${id}`); + } catch (err) { + console.error(`✗ ${id}:`, err); + } + } + console.log("Done!"); + process.exit(0); + }); - process.exit(1); -}); +} else if (TARGET === "mysql") { + const rsm = new MySQLSettingsManager(config.mysql, "./storage/defaults.json"); + rsm.on("ready", async () => { + for (const [id, s] of servers) { + try { + if (rsm.hasServer(id)) { + await rsm.remoteSave(s); + } else { + await rsm.create(id, s); + } + console.log(`✓ ${id}`); + } catch (err) { + console.error(`✗ ${id}:`, err); + } + } + console.log("Done!"); + process.exit(0); + }); +} \ No newline at end of file