implement new console
This commit is contained in:
parent
4ba694ff18
commit
52573e21bc
5 changed files with 252 additions and 113 deletions
1
bot.js
1
bot.js
|
@ -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'))
|
||||
|
|
93
index.js
93
index.js
|
@ -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()
|
||||
|
|
|
@ -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
179
util/console.js
Normal 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
12
util/file_exists.js
Normal 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
|
Loading…
Reference in a new issue