implement new console

This commit is contained in:
Chipmunk 2024-04-04 21:44:45 -04:00
parent 4ba694ff18
commit 52573e21bc
5 changed files with 252 additions and 113 deletions

1
bot.js
View file

@ -52,6 +52,7 @@ function createBot (options = {}) {
bot.autoReconnect = options.autoReconnect
bot.randomizeUsername = options.randomizeUsername
bot['online-mode'] = options['online-mode']
// set the client and add listeners
bot.on('set_client', (client) => {
client.on('connect', () => bot.emit('connect'))

View file

@ -1,57 +1,54 @@
const readline = require('readline')
const createBot = require('./bot.js')
const fs = require('fs')
const fs = require('fs/promises')
const path = require('path')
const moment = require('moment')
const readline = require('readline')
const json5 = require('json5')
const matrix = require('matrix-js-sdk')
const configPath = process.argv[2] ?? 'config.json5'
if (!fs.existsSync(configPath)) {
fs.copyFileSync(path.join(__dirname, 'default.json5'), configPath)
console.info('No config file was found, so a default one was created.')
}
const config = json5.parse(fs.readFileSync(configPath, 'utf-8'))
const createBot = require('./bot.js')
const fileExists = require('./util/file_exists')
const Console = require('./util/console')
const logdir = config.paths.logs ?? 'logs'
if (!fs.existsSync(logdir)) fs.mkdirSync(logdir)
async function main () {
// config loading
const configPath = process.argv[2] ?? 'config.json5'
if (!await fileExists(configPath)) {
await fs.copyFile(path.join(__dirname, 'default.json5'), configPath)
console.info('No config file was found, so a default one was created')
}
const config = json5.parse(await fs.readFile(configPath, 'utf-8'))
let logfile = path.join(logdir, moment().format('YYYY-MM-DD'))
if (fs.existsSync(logfile)) {
const pathWithSeparator = logfile + '-'
let i = 0
while (fs.existsSync(logfile)) {
logfile = pathWithSeparator + (i++)
// logging
const logdir = config.paths.logs ?? 'logs'
if (!await fileExists(logdir)) await fs.mkdir(logdir)
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prefix: '> '
})
const console = new Console({ readline: rl })
await console.createLogFile(logdir)
const matrixClients = {}
for (const key in config.matrixClients) {
const client = matrix.createClient(config.matrixClients[key])
matrixClients[key] = client
client.startClient()
}
const bots = []
for (const options of config.bots) {
const mergedOptions = { console, ...(config.all ?? {}), ...options }
if (mergedOptions.matrix && typeof mergedOptions.matrix.client !== 'object') mergedOptions.matrix.client = matrixClients[mergedOptions.matrix.client]
const bot = createBot(mergedOptions)
bots.push(bot)
bot.bots = bots
bot.on('error', error => bot.console.error(error))
}
}
logfile += '.log'
fs.writeFileSync(logfile, '')
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
prefix: '> '
})
const matrixClients = {}
for (const key in config.matrixClients) {
const client = matrix.createClient(config.matrixClients[key])
matrixClients[key] = client
client.startClient()
}
const bots = []
for (const options of config.bots) {
const mergedOptions = { ...(config.all ?? {}), ...options }
if (mergedOptions.matrix && typeof mergedOptions.matrix.client !== 'object') mergedOptions.matrix.client = matrixClients[mergedOptions.matrix.client]
const bot = createBot(mergedOptions)
bots.push(bot)
bot.bots = bots
bot.on('error', console.error)
bot.console.filepath = logfile
bot.console.setRl(rl)
}
main()

View file

@ -1,77 +1,27 @@
const fs = require('fs')
const util = require('util')
const colorCodeStringify = require('../util/chat/stringify/color_code')
const ansiStringify = require('../util/chat/stringify/ansi')
const CommandSource = require('../util/command/command_source')
const Console = require('../util/console')
function inject (bot) {
bot.console = {
filepath: null,
host: 'all',
log,
warn,
error,
_log,
setRl,
_rl: null
}
function log (data) {
_log('\u00a72INFO', process.stdout, data)
}
function warn (data) {
_log('\u00a7eWARN', process.stderr, data)
}
function error (data) {
_log('\u00a7cERROR', process.stderr, data)
}
function _log (prefix, stdout, data) {
// format it
const _prefix = `[${formatDate()} ${prefix}\u00a7r] [${bot.host}] `
const stringifyOptions = { lang: bot.registry?.language ?? {} }
function inject (bot, options) {
bot.console = options.console ? options.console.fork(bot.host) : new Console({ namespace: bot.host })
const formattedData = _prefix + colorCodeStringify(data, stringifyOptions) + '\n'
const ansi = ansiStringify(_prefix, stringifyOptions) + ansiStringify(data, stringifyOptions) + '\x1b[0m\n'
bot.on('registry_loaded', () => (bot.console.language = bot.registry.language))
// log to file
const filepath = bot.console.filepath
if (filepath != null) {
fs.appendFile(filepath, formattedData, err => {
if (err) console.error(err)
})
bot.console.on('line', handleLine)
function handleLine (line) {
// if (bot.host !== bot.console.host && bot.console.host !== 'all') return
if (line.startsWith('.')) {
const source = new CommandSource({ bot, permissionLevel: Infinity, sendFeedback, displayName: '_ChipMC_' })
bot.commands.execute(line.substring(1), source)
} else {
bot.fancyMsg('test', '_ChipMC_', line)
}
// log to stdout
stdout.write(ansi + '')
}
function formatDate (date = new Date()) {
const hours = date.getHours()
const minutes = date.getMinutes()
const seconds = date.getSeconds()
return [hours, minutes, seconds].map(n => n.toString().padStart(2, '0')).join(':')
}
function setRl (rl) {
rl?.prompt(true)
rl?.on('line', handleLine)
// bot.console._rl?.removeListener('line', handleLine)
bot.console._rl = rl
function handleLine (line) {
if (bot.host !== bot.console.host && bot.console.host !== 'all') return
if (line.startsWith('.')) {
const source = new CommandSource({ bot, permissionLevel: Infinity, sendFeedback, displayName: 'console' })
bot.commands.execute(line.substring(1), source)
} else {
bot.fancyMsg('test', '_ChipMC_', line)
rl?.prompt(true)
}
}
function sendFeedback (text, sendFeedback) {
bot.console.log(text)
}
function sendFeedback (text, broadcase) {
bot.console.log(text)
}
}

179
util/console.js Normal file
View file

@ -0,0 +1,179 @@
const fs = require('fs')
const path = require('path')
const zlib = require('zlib')
const stream = require('stream/promises')
const EventEmitter = require('events')
const fileExists = require('./file_exists')
const colorCodeStringify = require('../util/chat/stringify/color_code')
const ansiStringify = require('../util/chat/stringify/ansi')
class Console extends EventEmitter {
#writeStream = null
#forks = []
logdir = null
filename = null
parent = null
constructor ({ lang = {}, namespace = '', stdout = process.stdout, stderr = process.stderr, readline = null } = {}) {
super()
this.language = lang
this.namespace = namespace
this.stdout = stdout
this.stderr = stderr
this.readline = readline
this.readline?.on('line', line => {
this.emit('line', line)
this.readline.prompt(true)
})
this.on('local_error', error => this.emit('error', error))
this.on('error', error => this.#forks.forEach(fork => fork.emit('error', error)))
this.on('line', line => this.#forks.forEach(fork => fork.emit('line', line)))
this.on('end', () => this.#forks.forEach(fork => fork.emit('end')))
}
info (text) {
this.#log(text, '\xa72INFO', this.stdout)
}
warn (text) {
this.#log(text, '\xa7eWARN', this.stderr)
}
error (text) {
this.#log(text, '\xa7cERROR', this.stderr)
}
log (text) {
this.info(text)
}
#log (text, prefix, stdout) {
const raw = [`[${formatTimestamp()} ${prefix}\xa7r] `, text]
if (this.namespace) raw[0] += `[${this.namespace}] `
const options = { lang: this.language }
const colorStr = colorCodeStringify(raw, options)
const ansiStr = ansiStringify(raw, options)
if (stdout) stdout.write(ansiStr + '\x1b[0m\n')
this.#writeToLog(colorStr + '\n')
}
#writeToLog (text) {
if (this.parent) return this.parent.#writeToLog(text)
this.#writeStream?.write(text)
}
fork (namespace) {
if (this.namespace) namespace = this.namespace + '/' + namespace
const fork = new Console({ lang: this.language, namespace })
fork.parent = this
this.#forks.push(fork)
fork.on('end', () => (this.#forks = this.#forks.filter(_fork => _fork !== fork)))
return fork
}
end () {
this.#writeStream?.end()
this.stdout = null
this.stderr = null
this.parent = null
this.emit('end')
}
async createLogFile (logdir) {
this.logdir = logdir
const filename = path.join(logdir, 'latest.log')
if (await fileExists(filename)) {
const inStream = fs.createReadStream(filename)
let outStreamRaw
let outStreamGzip
inStream.on('data', chunk => {
if (!outStreamRaw) {
// Attempt to read the first line (assumed to have a date)
let idx = -1
for (let i = 0; i < chunk.length; i++) {
if (chunk[i] === 0x0a || chunk[i] === 0x0d) {
idx = i + 1
break
}
}
if (chunk[idx] === 0x0d && chunk[idx + 1] === 0x0a) idx++
let date
if (idx !== -1) {
date = chunk.subarray(0, idx).toString().trim()
// sanity checking
const parts = date.split('-').map(Number)
if (parts.length !== 3) idx = -1
else if (parts.some(part => Number.isNaN(part))) idx = -1
}
if (idx !== -1) {
// The sanity checks passed, so we can skip the date now
chunk = chunk.subarray(idx)
} else {
// stat the file for the creation date
const stat = fs.statSync(filename)
date = formatDate(stat.birthtime)
}
// handle duplicates
const prefix = this.logdir + '/'
const suffix = '.log.gz'
let outFilename = prefix + date + suffix
let count = 1
while (fs.existsSync(outFilename)) {
outFilename = prefix + date + '-' + count++ + suffix
}
// so much for just some output streams...
outStreamRaw = fs.createWriteStream(outFilename)
outStreamGzip = zlib.createGzip()
outStreamGzip.on('data', chunk => outStreamRaw.write(chunk))
outStreamGzip.on('end', () => outStreamRaw.end())
}
outStreamGzip.write(chunk)
})
await stream.finished(inStream)
outStreamGzip?.end()
}
this.filename = filename
this.#writeStream = fs.createWriteStream(filename)
this.#writeStream.setDefaultEncoding('utf-8')
this.#writeStream.write(formatDate() + '\n')
}
}
function formatDate (date = new Date()) {
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDay()
return `${year.toString().padStart(4, '0')}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`
}
function formatTimestamp (date = new Date()) {
const hours = date.getHours()
const minutes = date.getMinutes()
const seconds = date.getSeconds()
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`
}
module.exports = Console

12
util/file_exists.js Normal file
View file

@ -0,0 +1,12 @@
const fs = require('fs/promises')
async function fileExists (filename) {
try {
await fs.stat(filename)
return true
} catch {
return false
}
}
module.exports = fileExists