rework main.js for a more OOP approach

This commit is contained in:
1e99 2025-07-22 18:25:18 +02:00
parent 9c8b5437b2
commit c67cd50ff1
3 changed files with 139 additions and 109 deletions

View file

@ -1,4 +1,5 @@
{
"database": "./data.sqlite",
"minecraft": {
"host": "localhost",
"port": 25565,
@ -7,6 +8,5 @@
"auth": "microsoft",
"username": "Notch"
},
"database": "./data.sqlite",
"prefix": "!"
}

View file

@ -21,113 +21,150 @@ import TPSCommand from "./commands/tps.js"
import WorstPingCommand from "./commands/worst-ping.js"
import JoinsCommand from "./commands/joins.js"
class Bot {
console.log(`____ _ ____ _ ____ _ `)
console.log(`|___ \\| | |___ \\| | | _ \\ | | `)
console.log(` __) | |__ __) | |_| |_) | ___ | |_ `)
console.log(` |__ <| '_ \\|__ <| __| _ < / _ \\| __|`)
console.log(` ___) | |_) |__) | |_| |_) | (_) | |_ `)
console.log(`|____/|_.__/____/ \\__|____/ \\___/ \\__|`)
console.log(` `)
static #STOP_DISCONNECT_REASON = "__stop__9sE7pxOZub__"
let configPath = process.env["CONFIG_FILE"]
#dbPath
#clientFactory
#prefix
#dateFormat
#db
#client
// Modules
#chat
#death
#players
#tps
#commands
constructor(dbPath, clientFactory, prefix) {
this.#dbPath = dbPath
this.#clientFactory = clientFactory
this.#prefix = prefix
this.#dateFormat = new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
timeStyle: "medium",
timeZone: "UTC",
})
}
start() {
console.log(`____ _ ____ _ ____ _ `)
console.log(`|___ \\| | |___ \\| | | _ \\ | | `)
console.log(` __) | |__ __) | |_| |_) | ___ | |_ `)
console.log(` |__ <| '_ \\|__ <| __| _ < / _ \\| __|`)
console.log(` ___) | |_) |__) | |_| |_) | (_) | |_ `)
console.log(`|____/|_.__/____/ \\__|____/ \\___/ \\__|`)
console.log(` `)
try {
this.#db = new sqlite.DatabaseSync(this.#dbPath)
console.debug("Opened database")
migrateDB(this.#db)
console.debug("Migrated database")
} catch (e) {
throw new Error(`Failed to open/migrate database: ${e}`)
}
this.#connect()
}
#connect() {
this.#client = this.#clientFactory()
this.#chat = new Chat(this.#client)
this.#chat.addPattern("chat", new RegExp(`^<([a-zA-Z0-9_]+)> (.+)$`))
this.#chat.addPattern("whisper", new RegExp(`^\\[([a-zA-Z0-9_]+) -> me\\] (.+)$`))
this.#chat.addListener("chat", this.#onChat.bind(this))
this.#death = new Death(this.#client)
this.#death.addListener("death", this.#onDeath.bind(this))
this.#players = new Players(this.#client)
this.#tps = new TPS(this.#client)
this.#client.addListener("connect", this.#onClientConnect.bind(this))
this.#client.addListener("end", this.#onClientEnd.bind(this))
this.#commands = new Commands(this.#chat, this.#prefix)
this.#commands.register(new BestPingCommand(this.#chat, this.#players))
this.#commands.register(new HelpCommand(this.#chat, this.#commands))
this.#commands.register(new InfoCommand(this.#chat))
this.#commands.register(new JoinDateCommand(this.#chat, this.#db, this.#dateFormat))
this.#commands.register(new JoinsCommand(this.#chat, this.#db))
this.#commands.register(new PingCommand(this.#chat, this.#players))
this.#commands.register(new PlaytimeCommand(this.#chat, this.#db))
this.#commands.register(new SeenCommand(this.#chat, this.#db, this.#dateFormat))
this.#commands.register(new TPSCommand(this.#chat, this.#tps))
this.#commands.register(new WorstPingCommand(this.#chat, this.#players))
new PlayerTracker(this.#client, this.#players, this.#db)
}
#onChat(message) {
console.log(`Chat: ${message}`)
}
#onDeath() {
console.log("Client died")
this.#death.respawn()
}
#onClientConnect() {
console.log("Client connected")
}
#onClientEnd(reason) {
this.#client = null
if (reason == Bot.#STOP_DISCONNECT_REASON) {
console.log("Client closed")
return
}
console.log(`Client disconnected: ${reason}`)
setTimeout(() => this.#connect(), 10_000)
}
stop() {
// Not much we can do if this fails
try {
this.#db.close()
console.log("Database closed")
} catch (ignored) { }
if (this.#client !== null) {
this.#client.end(Bot.#STOP_DISCONNECT_REASON)
}
}
}
let configPath = process.env["CONFIG_PATH"]
if (configPath === undefined) {
console.log(`"CONFIG_FILE" not defined, defaulting to "./config.json"`)
console.log("No \"CONFIG_PATH\" defined, defaulting to \"./config.json\"")
configPath = "./config.json"
}
let config
try {
const raw = await fs.readFile(configPath, "utf-8")
const raw = await fs.readFile(configPath, "utf8")
config = JSON.parse(raw)
} catch (e) {
console.error(`Failed to read config: ${e}`)
process.exit(1)
throw new Error(`Failed to reas config: ${e}`)
}
const db = new sqlite.DatabaseSync(config["database"])
console.log("Database opened")
const bot = new Bot(
config["database"],
() => mcProtocol.createClient(config["minecraft"]),
config["prefix"],
)
migrateDB(db)
const reasonProcessExiting = "__processExiting__"
const reconnectDelay = 10_000
const dateFormat = new Intl.DateTimeFormat("en-US", {
dateStyle: "medium",
timeStyle: "medium",
timeZone: "UTC",
bot.start()
process.on("SIGINT", () => {
console.log("Detected SIGINT, exiting")
bot.stop()
})
let client = null
newClient()
function newClient() {
console.log("Client connecting")
// Client
client = mcProtocol.createClient(config["minecraft"])
client.addListener("connect", () => {
console.log("Client connected")
})
client.addListener("end", reason => {
if (reason === reasonProcessExiting) {
console.log("Client disconnected")
return
}
console.log(`Client disconnected, reconnecting in ${reconnectDelay}ms: ${reason}`)
setTimeout(() => newClient(), reconnectDelay)
})
// Modules
const chat = new Chat(client)
const death = new Death(client)
const players = new Players(client)
const tps = new TPS(client)
chat.addPattern("chat", new RegExp(`^<([a-zA-Z0-9_]+)> (.+)$`))
chat.addPattern("whisper", new RegExp(`^\\[([a-zA-Z0-9_]+) -> me\\] (.+)$`))
chat.addListener("chat", message => {
console.log(`Chat: ${message}`)
})
chat.addListener("chat:chat", (username, message) => { })
chat.addListener("chat:whisper", (username, message) => { })
death.addListener("death", () => {
console.log("Client died, respawning")
death.respawn()
})
new PlayerTracker(client, players, db)
// Commands
const commands = new Commands(chat, config["prefix"])
commands.register(new BestPingCommand(chat, players))
commands.register(new HelpCommand(chat, commands))
commands.register(new InfoCommand(chat))
commands.register(new JoinDateCommand(chat, db, dateFormat))
commands.register(new JoinsCommand(chat, db))
commands.register(new PingCommand(chat, players))
commands.register(new PlaytimeCommand(chat, db))
commands.register(new SeenCommand(chat, db, dateFormat))
commands.register(new TPSCommand(chat, tps))
commands.register(new WorstPingCommand(chat, players))
}
function end() {
console.log("Detected end, quitting")
client.end(reasonProcessExiting)
db.close()
console.log("Database closed")
}
process.on("SIGINT", end)
process.on("SIGUSR1", end)
process.on("SIGUSR2", end)
process.on("uncaughtException", end)

View file

@ -10,26 +10,19 @@ const migrations = [
]
export default function migrateDB(db) {
const getStmt = db.prepare("PRAGMA user_version;")
let version = getStmt.get()["user_version"]
const version = db.prepare(`PRAGMA user_version;`).get()["user_version"]
for (let i = 0; i < migrations.length; i++) {
const nextVersion = i + 1
if (version >= nextVersion) {
const newVersion = i + 1
if (version >= newVersion) {
continue
}
console.log(`Migrating database to version ${nextVersion}`)
try {
db.exec(migrations[i])
version = nextVersion
db.prepare(`PRAGMA user_version = ${version};`).run()
db.prepare(`PRAGMA user_version = ${newVersion};`).run()
} catch (e) {
console.error(`Database migration to version ${nextVersion} failed: ${e}`)
break
throw new Error(`Failed to apply migration ${i}: ${e}`)
}
}
console.log(`Database is migrated. Current version is ${version}`)
}