Fixes
This commit is contained in:
@@ -0,0 +1,703 @@
|
||||
/**
|
||||
* Bare OS Discord slash-command catalog (guest-safe: no import/export).
|
||||
* Used by /bin/discord-bot (prepended) and the bare-os-discord initd unit.
|
||||
*/
|
||||
|
||||
var BARE_OS_DISCORD_REPLY_MAX = 1900
|
||||
var BARE_OS_DISCORD_FS_MAX = 12 * 1024
|
||||
var BARE_OS_DISCORD_WHITELIST_DENY =
|
||||
'Access denied. Your Discord user id is not on DISCORD_ID_WHITELIST.'
|
||||
var BARE_OS_DISCORD_RUN_ALLOW = {
|
||||
uname: 1,
|
||||
whoami: 1,
|
||||
hostname: 1,
|
||||
date: 1,
|
||||
uptime: 1,
|
||||
id: 1,
|
||||
pwd: 1,
|
||||
arch: 1,
|
||||
nproc: 1,
|
||||
help: 1,
|
||||
motd: 1,
|
||||
true: 1,
|
||||
false: 1,
|
||||
uname: 1,
|
||||
df: 1,
|
||||
ps: 1,
|
||||
procstat: 1,
|
||||
'uname -a': 1
|
||||
}
|
||||
|
||||
function discordCmdClip(text, max) {
|
||||
const s = String(text == null ? '' : text)
|
||||
const n = max || BARE_OS_DISCORD_REPLY_MAX
|
||||
if (s.length <= n) return s
|
||||
return s.slice(0, n - 20) + '\n…(truncated)'
|
||||
}
|
||||
|
||||
function discordCmdRedact(text) {
|
||||
return String(text == null ? '' : text)
|
||||
.replace(/[A-Za-z0-9_\-]{20,}\.[A-Za-z0-9_\-]{5,}\.[A-Za-z0-9_\-]{20,}/g, '[token]')
|
||||
.replace(/(DISCORD_TOKEN|BOT_TOKEN|TOKEN|SECRET|PASSWORD|PASSWD|API_KEY)\s*[=:]\s*\S+/gi, '$1=[redacted]')
|
||||
}
|
||||
|
||||
function discordCmdFence(text, lang) {
|
||||
const body = discordCmdClip(discordCmdRedact(text))
|
||||
return '```' + (lang || '') + '\n' + body.replace(/```/g, '`ˋ`') + '\n```'
|
||||
}
|
||||
|
||||
async function discordCmdReadText(ctx, logicalPath) {
|
||||
if (!logicalPath || typeof ctx.vfs?.readFile !== 'function') return ''
|
||||
try {
|
||||
const buf = await ctx.vfs.readFile(logicalPath)
|
||||
if (!buf) return ''
|
||||
if (typeof ctx.b4a?.toString === 'function') return ctx.b4a.toString(buf)
|
||||
return String(buf)
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
async function discordCmdReadJson(ctx, logicalPath) {
|
||||
const t = await discordCmdReadText(ctx, logicalPath)
|
||||
if (!t) return null
|
||||
try {
|
||||
return JSON.parse(t)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function discordCmdKv(obj) {
|
||||
const keys = Object.keys(obj || {})
|
||||
const lines = []
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const k = keys[i]
|
||||
if (obj[k] == null || obj[k] === '') continue
|
||||
lines.push(k + ': ' + String(obj[k]))
|
||||
}
|
||||
return lines.join('\n') || '(empty)'
|
||||
}
|
||||
|
||||
function discordCmdPathOk(raw) {
|
||||
const p = String(raw || '').trim() || '.'
|
||||
if (!p || p.indexOf('\0') >= 0) return null
|
||||
if (p.indexOf('..') >= 0) return null
|
||||
if (p === '~/.discord/.env' || p === '~/.discord.env') return null
|
||||
const allow =
|
||||
p === '.' ||
|
||||
p === '~' ||
|
||||
p.charAt(0) === '~' ||
|
||||
p.indexOf('/proc') === 0 ||
|
||||
p.indexOf('/etc') === 0 ||
|
||||
p.indexOf('/var/log') === 0 ||
|
||||
p.indexOf('/run') === 0 ||
|
||||
p.indexOf('/home') === 0 ||
|
||||
p.indexOf('/usr/share') === 0 ||
|
||||
p.indexOf('/share') === 0 ||
|
||||
p.indexOf('/tmp') === 0
|
||||
return allow ? p : null
|
||||
}
|
||||
|
||||
async function discordCmdCapture(ctx, fn) {
|
||||
const lines = []
|
||||
const cons = ctx.console || {}
|
||||
const ol = cons.log
|
||||
const oe = cons.error
|
||||
cons.log = function (s) {
|
||||
lines.push(String(s))
|
||||
}
|
||||
cons.error = function (s) {
|
||||
lines.push(String(s))
|
||||
}
|
||||
try {
|
||||
await fn()
|
||||
} finally {
|
||||
cons.log = ol
|
||||
cons.error = oe
|
||||
}
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
async function discordCmdOsRelease(ctx) {
|
||||
const t = await discordCmdReadText(ctx, '/etc/os-release')
|
||||
const out = {}
|
||||
const lines = String(t).split(/\r?\n/)
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const m = /^([A-Z0-9_]+)=(.*)$/.exec(lines[i].trim())
|
||||
if (!m) continue
|
||||
out[m[1]] = m[2].replace(/^"|"$/g, '')
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function discordCmdEnv(ctx) {
|
||||
return (ctx && ctx.env) || (ctx && ctx.vfs && ctx.vfs.env) || {}
|
||||
}
|
||||
|
||||
/** Comma-separated Discord snowflake ids → lookup map. Empty / unset → {}. */
|
||||
function discordParseIdWhitelist(raw) {
|
||||
const ids = Object.create(null)
|
||||
const s = String(raw == null ? '' : raw).trim()
|
||||
if (!s) return ids
|
||||
const parts = s.split(',')
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
let id = String(parts[i] || '').trim()
|
||||
if (!id) continue
|
||||
if (id.charAt(0) === '<' && id.charAt(id.length - 1) === '>') {
|
||||
id = id.slice(1, -1)
|
||||
if (id.charAt(0) === '@') id = id.slice(1)
|
||||
if (id.charAt(0) === '!') id = id.slice(1)
|
||||
id = id.trim()
|
||||
}
|
||||
if (id) ids[id] = 1
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
function discordWhitelistRaw(ctx) {
|
||||
const e = discordCmdEnv(ctx)
|
||||
return e.DISCORD_ID_WHITELIST || e.BARE_OS_DISCORD_ID_WHITELIST || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Unset / empty whitelist: allow everyone.
|
||||
* Non-empty: only listed Discord user ids may use the bot.
|
||||
*/
|
||||
function discordUserAllowed(ctx, userId) {
|
||||
const ids = discordParseIdWhitelist(discordWhitelistRaw(ctx))
|
||||
let n = 0
|
||||
for (const k in ids) {
|
||||
if (Object.prototype.hasOwnProperty.call(ids, k)) n++
|
||||
}
|
||||
if (n === 0) return true
|
||||
const id = String(userId == null ? '' : userId).trim()
|
||||
return Boolean(id && ids[id])
|
||||
}
|
||||
|
||||
function discordInteractionUserId(interaction) {
|
||||
if (!interaction) return ''
|
||||
if (interaction.user && interaction.user.id) return String(interaction.user.id)
|
||||
const member = interaction.member
|
||||
if (member && member.user && member.user.id) return String(member.user.id)
|
||||
if (member && member.id) return String(member.id)
|
||||
return ''
|
||||
}
|
||||
|
||||
async function discordReplyWhitelistDenied(ctx, interaction) {
|
||||
const payload = {
|
||||
content: BARE_OS_DISCORD_WHITELIST_DENY,
|
||||
ephemeral: true
|
||||
}
|
||||
try {
|
||||
if (interaction.deferred && typeof interaction.editReply === 'function') {
|
||||
await interaction.editReply(payload)
|
||||
} else if (interaction.replied && typeof interaction.followUp === 'function') {
|
||||
await interaction.followUp(payload)
|
||||
} else if (typeof interaction.reply === 'function') {
|
||||
await interaction.reply(payload)
|
||||
}
|
||||
} catch (err) {
|
||||
if (ctx && ctx.console && typeof ctx.console.error === 'function') {
|
||||
ctx.console.error(
|
||||
'discord-bot: deny reply failed: ' + ((err && err.message) || err)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function discordCmdStatus(ctx) {
|
||||
const e = discordCmdEnv(ctx)
|
||||
const os = await discordCmdOsRelease(ctx)
|
||||
return discordCmdKv({
|
||||
os: os.PRETTY_NAME || os.NAME || 'Bare OS',
|
||||
version: os.VERSION_ID || e.BARE_OS_KERNEL_VERSION || e.BARE_OS_RELEASE || '',
|
||||
hostname: e.HOSTNAME || e.NAME || 'bare-os',
|
||||
user: e.USER || e.LOGNAME || e.USERNAME || '',
|
||||
home: e.HOME || '',
|
||||
shell: e.SHELL || '/bin/sh',
|
||||
arch: e.BARE_OS_ARCH || e.MACHINE || '',
|
||||
booter: e.BARE_OS_BOOTER_PACKAGE_VERSION || '',
|
||||
now: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
async function discordCmdUname(ctx) {
|
||||
const e = discordCmdEnv(ctx)
|
||||
const os = await discordCmdOsRelease(ctx)
|
||||
return [
|
||||
os.NAME || 'BareOS',
|
||||
e.HOSTNAME || 'bare-os',
|
||||
os.VERSION_ID || e.BARE_OS_KERNEL_VERSION || '0.1',
|
||||
e.BARE_OS_ARCH || e.MACHINE || 'unknown',
|
||||
os.PRETTY_NAME || e.BARE_OS_BUILD || 'bare-userland'
|
||||
].join(' ')
|
||||
}
|
||||
|
||||
async function discordCmdHandleBare(ctx, sub) {
|
||||
const e = discordCmdEnv(ctx)
|
||||
if (sub === 'ping') return { text: 'pong · Bare OS is online' }
|
||||
if (sub === 'about') {
|
||||
return {
|
||||
text: discordCmdKv({
|
||||
name: 'Bare OS Discord bot',
|
||||
role: 'slash control surface for this guest session',
|
||||
commands: '/bare /sys /svc /fs /net /man /say /run /journal /ping',
|
||||
service: 'bare-os-discord (systemctl; requires ~/.discord/.env)',
|
||||
stop: 'Ctrl+C in the foreground, or systemctl stop bare-os-discord'
|
||||
})
|
||||
}
|
||||
}
|
||||
if (sub === 'help') {
|
||||
return {
|
||||
text:
|
||||
'**Bare OS bot**\n' +
|
||||
'`/bare` ping about help status whoami hostname date uptime motd uname\n' +
|
||||
'`/sys` df mem ps env doctor features rlimits\n' +
|
||||
'`/svc` list status start stop restart logs\n' +
|
||||
'`/fs` ls cat stat head\n' +
|
||||
'`/net` peers swarm summary\n' +
|
||||
'`/man <page>` `/say <text>` `/run <cmd>` `/journal [unit]` `/ping`'
|
||||
}
|
||||
}
|
||||
if (sub === 'status') return { text: discordCmdFence(await discordCmdStatus(ctx)) }
|
||||
if (sub === 'uname') return { text: discordCmdFence(await discordCmdUname(ctx)) }
|
||||
if (sub === 'whoami') return { text: String(e.USER || e.LOGNAME || e.USERNAME || 'guest') }
|
||||
if (sub === 'hostname') return { text: String(e.HOSTNAME || e.NAME || 'bare-os') }
|
||||
if (sub === 'date') return { text: new Date().toISOString() + ' · ' + String(Date()) }
|
||||
if (sub === 'uptime') {
|
||||
const t = await discordCmdReadText(ctx, '/proc/uptime')
|
||||
return { text: t ? t.trim() : 'uptime unavailable' }
|
||||
}
|
||||
if (sub === 'motd') {
|
||||
const t = await discordCmdReadText(ctx, '/etc/motd')
|
||||
return { text: t ? discordCmdFence(t) : '(no /etc/motd)' }
|
||||
}
|
||||
return { text: 'unknown /bare subcommand', ephemeral: true }
|
||||
}
|
||||
|
||||
async function discordCmdHandleSys(ctx, sub) {
|
||||
if (sub === 'df') {
|
||||
const t =
|
||||
(await discordCmdReadText(ctx, '/proc/bare_os/host_os.json')) ||
|
||||
(await discordCmdReadText(ctx, '/proc/bare_os_resources'))
|
||||
return { text: t ? discordCmdFence(discordCmdClip(t, 1600), 'json') : 'disk info unavailable' }
|
||||
}
|
||||
if (sub === 'mem') {
|
||||
const t =
|
||||
(await discordCmdReadText(ctx, '/proc/meminfo')) ||
|
||||
(await discordCmdReadText(ctx, '/proc/bare_os/host_os.json'))
|
||||
return { text: t ? discordCmdFence(discordCmdClip(t, 1600)) : 'meminfo unavailable' }
|
||||
}
|
||||
if (sub === 'ps') {
|
||||
const table = await discordCmdReadJson(ctx, '/proc/bare_os/process_table.json')
|
||||
const rows = table && Array.isArray(table.processes) ? table.processes : []
|
||||
const lines = ['pid\tname\tstate']
|
||||
for (let i = 0; i < Math.min(rows.length, 30); i++) {
|
||||
const r = rows[i] || {}
|
||||
lines.push(
|
||||
String(r.pid || r.id || '') +
|
||||
'\t' +
|
||||
String(r.name || r.comm || r.cmd || '') +
|
||||
'\t' +
|
||||
String(r.state || r.status || '')
|
||||
)
|
||||
}
|
||||
if (rows.length > 30) lines.push('…' + (rows.length - 30) + ' more')
|
||||
return { text: discordCmdFence(lines.join('\n')) }
|
||||
}
|
||||
if (sub === 'env') {
|
||||
const e = discordCmdEnv(ctx)
|
||||
const keys = Object.keys(e).sort()
|
||||
const lines = []
|
||||
const skip = /TOKEN|SECRET|PASSWORD|PASSWD|KEY|PRIVATE|CREDENTIAL/i
|
||||
for (let i = 0; i < keys.length && lines.length < 40; i++) {
|
||||
if (skip.test(keys[i])) {
|
||||
lines.push(keys[i] + '=[redacted]')
|
||||
continue
|
||||
}
|
||||
lines.push(keys[i] + '=' + String(e[keys[i]]).slice(0, 80))
|
||||
}
|
||||
return { text: discordCmdFence(lines.join('\n')) }
|
||||
}
|
||||
if (sub === 'doctor') {
|
||||
const t =
|
||||
(await discordCmdReadText(ctx, '/proc/bare_os/security_posture.json')) ||
|
||||
(await discordCmdReadText(ctx, '/proc/bare_os/debug.json'))
|
||||
return { text: t ? discordCmdFence(discordCmdClip(t, 1600), 'json') : 'doctor snapshot unavailable' }
|
||||
}
|
||||
if (sub === 'features') {
|
||||
const t = await discordCmdReadText(ctx, '/proc/bare_os_features')
|
||||
return { text: t ? discordCmdFence(discordCmdClip(t, 1600), 'json') : 'features unavailable' }
|
||||
}
|
||||
if (sub === 'rlimits') {
|
||||
const t = await discordCmdReadText(ctx, '/proc/bare_os/rlimits.json')
|
||||
return { text: t ? discordCmdFence(discordCmdClip(t, 1600), 'json') : 'rlimits unavailable' }
|
||||
}
|
||||
return { text: 'unknown /sys subcommand', ephemeral: true }
|
||||
}
|
||||
|
||||
async function discordCmdHandleSvc(ctx, sub, unit) {
|
||||
const name = String(unit || '').replace(/\.service$/, '')
|
||||
if (typeof ctx.bareOsRunSystemctlCli !== 'function') {
|
||||
return { text: 'systemctl is not available on this ctx', ephemeral: true }
|
||||
}
|
||||
const argv =
|
||||
sub === 'list'
|
||||
? ['systemctl', 'list']
|
||||
: name
|
||||
? ['systemctl', sub, name]
|
||||
: null
|
||||
if (!argv) return { text: 'unit name required', ephemeral: true }
|
||||
const out = await discordCmdCapture(ctx, function () {
|
||||
return ctx.bareOsRunSystemctlCli(argv)
|
||||
})
|
||||
return { text: out ? discordCmdFence(out) : '(no output)' }
|
||||
}
|
||||
|
||||
async function discordCmdHandleFs(ctx, sub, rawPath, nlines) {
|
||||
const p = discordCmdPathOk(rawPath || '.')
|
||||
if (!p) return { text: 'path not allowed (no `..`; stay under /proc /etc /var/log /run /home ~ /share)', ephemeral: true }
|
||||
if (sub === 'ls') {
|
||||
if (typeof ctx.vfs?.readdir !== 'function') return { text: 'readdir unavailable', ephemeral: true }
|
||||
try {
|
||||
const names = await ctx.vfs.readdir(p)
|
||||
const list = Array.isArray(names) ? names : []
|
||||
return { text: discordCmdFence(list.slice(0, 80).join('\n') || '(empty)') }
|
||||
} catch (err) {
|
||||
return { text: 'ls failed: ' + ((err && err.message) || err), ephemeral: true }
|
||||
}
|
||||
}
|
||||
if (sub === 'stat') {
|
||||
const stfn = ctx.vfs && (ctx.vfs.lstat || ctx.vfs.stat)
|
||||
if (typeof stfn !== 'function') return { text: 'stat unavailable', ephemeral: true }
|
||||
try {
|
||||
const st = await stfn.call(ctx.vfs, p)
|
||||
return { text: discordCmdFence(JSON.stringify(st, null, 2), 'json') }
|
||||
} catch (err) {
|
||||
return { text: 'stat failed: ' + ((err && err.message) || err), ephemeral: true }
|
||||
}
|
||||
}
|
||||
const text = await discordCmdReadText(ctx, p)
|
||||
if (!text) return { text: '(empty or unreadable)' }
|
||||
if (sub === 'head') {
|
||||
const n = Math.max(1, Math.min(40, Number(nlines) || 12))
|
||||
return { text: discordCmdFence(text.split(/\r?\n/).slice(0, n).join('\n')) }
|
||||
}
|
||||
return { text: discordCmdFence(text.slice(0, BARE_OS_DISCORD_FS_MAX)) }
|
||||
}
|
||||
|
||||
async function discordCmdHandleNet(ctx, sub) {
|
||||
if (sub === 'peers' || sub === 'swarm') {
|
||||
const t =
|
||||
(await discordCmdReadText(ctx, '/proc/bare_os/swarm')) ||
|
||||
(await discordCmdReadText(ctx, '/proc/bare_os_swarm'))
|
||||
return { text: t ? discordCmdFence(discordCmdClip(t, 1600), 'json') : 'swarm snapshot unavailable' }
|
||||
}
|
||||
const t =
|
||||
(await discordCmdReadText(ctx, '/proc/bare_os/net_summary.json')) ||
|
||||
(await discordCmdReadText(ctx, '/proc/net/dev'))
|
||||
return { text: t ? discordCmdFence(discordCmdClip(t, 1600)) : 'net summary unavailable' }
|
||||
}
|
||||
|
||||
async function discordCmdHandleMan(ctx, page) {
|
||||
const name = String(page || '').replace(/[^a-zA-Z0-9._+-]/g, '')
|
||||
if (!name) return { text: 'man page name required', ephemeral: true }
|
||||
if (typeof ctx.execLine === 'function') {
|
||||
const out = await discordCmdCapture(ctx, function () {
|
||||
return ctx.execLine('man ' + name)
|
||||
})
|
||||
if (out) return { text: discordCmdFence(out) }
|
||||
}
|
||||
const t = await discordCmdReadText(ctx, '/share/man/man.json')
|
||||
if (!t) return { text: 'man database unavailable' }
|
||||
try {
|
||||
const db = JSON.parse(t)
|
||||
const pages = (db && db.pages) || []
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
if (pages[i] && pages[i].name === name) {
|
||||
const p = pages[i]
|
||||
return {
|
||||
text: discordCmdFence(
|
||||
(p.title || name) +
|
||||
'\n' +
|
||||
(p.synopsis && p.synopsis[0] ? p.synopsis[0] : '') +
|
||||
'\n\n' +
|
||||
String(p.description || '').slice(0, 1400)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
/* fall through */
|
||||
}
|
||||
return { text: 'no man page for ' + name, ephemeral: true }
|
||||
}
|
||||
|
||||
function discordCmdSayBox(text) {
|
||||
const s = String(text || '').slice(0, 200)
|
||||
const lines = s.split(/\r?\n/).slice(0, 6)
|
||||
let w = 8
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (lines[i].length > w) w = lines[i].length
|
||||
}
|
||||
if (w > 48) w = 48
|
||||
const bar = '+' + Array(w + 3).join('-') + '+'
|
||||
const body = lines.map(function (ln) {
|
||||
const t = ln.slice(0, w)
|
||||
return '| ' + t + Array(w - t.length + 1).join(' ') + ' |'
|
||||
})
|
||||
return [bar, body.join('\n'), bar, ' \\', ' cow-ish · bare-os'].join('\n')
|
||||
}
|
||||
|
||||
async function discordCmdHandleRun(ctx, raw) {
|
||||
const cmd = String(raw || '').trim()
|
||||
if (!cmd) return { text: 'command required', ephemeral: true }
|
||||
if (/[;&|`$<>(){}]/.test(cmd)) {
|
||||
return { text: 'metacharacters are not allowed', ephemeral: true }
|
||||
}
|
||||
const key = cmd.replace(/\s+/g, ' ')
|
||||
const bin = key.split(' ')[0]
|
||||
if (!BARE_OS_DISCORD_RUN_ALLOW[key] && !BARE_OS_DISCORD_RUN_ALLOW[bin]) {
|
||||
return {
|
||||
text:
|
||||
'not in allowlist. Try: uname, whoami, hostname, date, uptime, id, pwd, arch, nproc, help, motd, df, ps, procstat, uname -a',
|
||||
ephemeral: true
|
||||
}
|
||||
}
|
||||
if (typeof ctx.execLine !== 'function') {
|
||||
return { text: 'execLine unavailable', ephemeral: true }
|
||||
}
|
||||
const out = await discordCmdCapture(ctx, function () {
|
||||
return ctx.execLine(key)
|
||||
})
|
||||
return { text: out ? discordCmdFence(out) : '(no output, exit ' + String(ctx.exitCode || 0) + ')' }
|
||||
}
|
||||
|
||||
async function discordCmdHandleJournal(ctx, unit) {
|
||||
if (typeof ctx.bareOsRunSystemctlCli === 'function' && unit) {
|
||||
const out = await discordCmdCapture(ctx, function () {
|
||||
return ctx.bareOsRunSystemctlCli([
|
||||
'journalctl',
|
||||
'-u',
|
||||
String(unit),
|
||||
'--lines',
|
||||
'30'
|
||||
])
|
||||
})
|
||||
return { text: out ? discordCmdFence(out) : '(empty journal)' }
|
||||
}
|
||||
const t =
|
||||
(await discordCmdReadText(ctx, '/var/log/bare-os/discord.log')) ||
|
||||
(await discordCmdReadText(ctx, '/var/log/bare-os/kernel-console.log')) ||
|
||||
(await discordCmdReadText(ctx, '/var/log/messages'))
|
||||
return { text: t ? discordCmdFence(t.split(/\r?\n/).slice(-30).join('\n')) : '(no journal)' }
|
||||
}
|
||||
|
||||
function discordCmdOpt(s, name, desc, required) {
|
||||
if (typeof s.addStringOption !== 'function') return s
|
||||
return s.addStringOption(function (o) {
|
||||
o.setName(name).setDescription(desc)
|
||||
if (required && typeof o.setRequired === 'function') o.setRequired(true)
|
||||
return o
|
||||
})
|
||||
}
|
||||
|
||||
function discordCmdAddSubs(builder, items) {
|
||||
if (!builder || typeof builder.addSubcommand !== 'function') return false
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const it = items[i]
|
||||
builder.addSubcommand(function (s) {
|
||||
s.setName(it[0]).setDescription(it[1])
|
||||
if (it[2]) it[2](s)
|
||||
return s
|
||||
})
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function discordBuildSlashCommands(dj) {
|
||||
const B = dj && dj.SlashCommandBuilder
|
||||
if (typeof B !== 'function') return []
|
||||
const bare = new B().setName('bare').setDescription('Bare OS session')
|
||||
const sys = new B().setName('sys').setDescription('Bare OS system snapshots')
|
||||
const svc = new B().setName('svc').setDescription('systemctl units')
|
||||
const fsCmd = new B().setName('fs').setDescription('Read-only VFS')
|
||||
const net = new B().setName('net').setDescription('Swarm / network')
|
||||
const man = new B().setName('man').setDescription('Look up a man page')
|
||||
const say = new B().setName('say').setDescription('Speak as Bare OS')
|
||||
const run = new B().setName('run').setDescription('Run an allowlisted utility')
|
||||
const journal = new B()
|
||||
.setName('journal')
|
||||
.setDescription('Tail a unit or system log')
|
||||
const ping = new B().setName('ping').setDescription('Reply pong')
|
||||
const ok =
|
||||
discordCmdAddSubs(bare, [
|
||||
['ping', 'Latency / liveness'],
|
||||
['about', 'What this bot is'],
|
||||
['help', 'Command map'],
|
||||
['status', 'Session snapshot'],
|
||||
['whoami', 'Guest user'],
|
||||
['hostname', 'Guest hostname'],
|
||||
['date', 'Clock'],
|
||||
['uptime', '/proc/uptime'],
|
||||
['motd', '/etc/motd'],
|
||||
['uname', 'uname -a style']
|
||||
]) &&
|
||||
discordCmdAddSubs(sys, [
|
||||
['df', 'Disk / host resources'],
|
||||
['mem', 'Memory info'],
|
||||
['ps', 'Process table'],
|
||||
['env', 'Redacted environment'],
|
||||
['doctor', 'Security / debug posture'],
|
||||
['features', '/proc/bare_os_features'],
|
||||
['rlimits', 'Resource limits']
|
||||
]) &&
|
||||
discordCmdAddSubs(svc, [
|
||||
['list', 'systemctl list'],
|
||||
['status', 'Unit status', function (s) {
|
||||
discordCmdOpt(s, 'unit', 'Unit name', true)
|
||||
}],
|
||||
['start', 'Start a unit', function (s) {
|
||||
discordCmdOpt(s, 'unit', 'Unit name', true)
|
||||
}],
|
||||
['stop', 'Stop a unit', function (s) {
|
||||
discordCmdOpt(s, 'unit', 'Unit name', true)
|
||||
}],
|
||||
['restart', 'Restart a unit', function (s) {
|
||||
discordCmdOpt(s, 'unit', 'Unit name', true)
|
||||
}],
|
||||
['logs', 'Unit logs', function (s) {
|
||||
discordCmdOpt(s, 'unit', 'Unit name', true)
|
||||
}]
|
||||
]) &&
|
||||
discordCmdAddSubs(fsCmd, [
|
||||
['ls', 'List a directory', function (s) {
|
||||
discordCmdOpt(s, 'path', 'VFS path', false)
|
||||
}],
|
||||
['cat', 'Read a file', function (s) {
|
||||
discordCmdOpt(s, 'path', 'VFS path', true)
|
||||
}],
|
||||
['stat', 'Stat a path', function (s) {
|
||||
discordCmdOpt(s, 'path', 'VFS path', true)
|
||||
}],
|
||||
['head', 'First lines of a file', function (s) {
|
||||
discordCmdOpt(s, 'path', 'VFS path', true)
|
||||
}]
|
||||
]) &&
|
||||
discordCmdAddSubs(net, [
|
||||
['peers', 'Swarm peers'],
|
||||
['swarm', 'Swarm snapshot'],
|
||||
['summary', 'net_summary']
|
||||
])
|
||||
if (!ok) {
|
||||
return [ping.toJSON()]
|
||||
}
|
||||
discordCmdOpt(man, 'page', 'Command name (e.g. uname)', true)
|
||||
discordCmdOpt(say, 'text', 'Text to box', true)
|
||||
discordCmdOpt(run, 'cmd', 'Allowlisted utility', true)
|
||||
discordCmdOpt(journal, 'unit', 'Optional unit name', false)
|
||||
return [
|
||||
bare,
|
||||
sys,
|
||||
svc,
|
||||
fsCmd,
|
||||
net,
|
||||
man,
|
||||
say,
|
||||
run,
|
||||
journal,
|
||||
ping
|
||||
].map(function (c) {
|
||||
return c.toJSON()
|
||||
})
|
||||
}
|
||||
|
||||
async function discordDispatchInteraction(ctx, interaction) {
|
||||
if (!interaction || typeof interaction.isChatInputCommand !== 'function') {
|
||||
return false
|
||||
}
|
||||
if (!interaction.isChatInputCommand()) return false
|
||||
if (!discordUserAllowed(ctx, discordInteractionUserId(interaction))) {
|
||||
if (ctx && ctx.console && typeof ctx.console.log === 'function') {
|
||||
ctx.console.log(
|
||||
'discord-bot: denied user ' +
|
||||
(discordInteractionUserId(interaction) || '?')
|
||||
)
|
||||
}
|
||||
await discordReplyWhitelistDenied(ctx, interaction)
|
||||
return true
|
||||
}
|
||||
const name = String(interaction.commandName || '')
|
||||
let sub = ''
|
||||
let opt = function () {
|
||||
return ''
|
||||
}
|
||||
if (interaction.options) {
|
||||
if (typeof interaction.options.getSubcommand === 'function') {
|
||||
try {
|
||||
sub = String(interaction.options.getSubcommand(false) || '')
|
||||
} catch {
|
||||
sub = ''
|
||||
}
|
||||
}
|
||||
opt = function (key) {
|
||||
if (typeof interaction.options.getString === 'function') {
|
||||
const v = interaction.options.getString(key)
|
||||
return v == null ? '' : String(v)
|
||||
}
|
||||
return ''
|
||||
}
|
||||
}
|
||||
let result
|
||||
try {
|
||||
if (name === 'ping' || (name === 'bare' && (sub === 'ping' || !sub))) {
|
||||
result = { text: 'pong · Bare OS is online' }
|
||||
} else if (name === 'bare') result = await discordCmdHandleBare(ctx, sub)
|
||||
else if (name === 'sys') result = await discordCmdHandleSys(ctx, sub)
|
||||
else if (name === 'svc') result = await discordCmdHandleSvc(ctx, sub, opt('unit'))
|
||||
else if (name === 'fs') {
|
||||
result = await discordCmdHandleFs(ctx, sub, opt('path'), opt('lines'))
|
||||
} else if (name === 'net') result = await discordCmdHandleNet(ctx, sub)
|
||||
else if (name === 'man') result = await discordCmdHandleMan(ctx, opt('page'))
|
||||
else if (name === 'say') result = { text: discordCmdFence(discordCmdSayBox(opt('text'))) }
|
||||
else if (name === 'run') result = await discordCmdHandleRun(ctx, opt('cmd'))
|
||||
else if (name === 'journal') result = await discordCmdHandleJournal(ctx, opt('unit'))
|
||||
else result = { text: 'unknown command: /' + name, ephemeral: true }
|
||||
} catch (err) {
|
||||
result = {
|
||||
text: 'error: ' + ((err && err.message) || String(err)),
|
||||
ephemeral: true
|
||||
}
|
||||
}
|
||||
const payload = {
|
||||
content: discordCmdClip(result && result.text ? result.text : '(no output)'),
|
||||
ephemeral: Boolean(result && result.ephemeral)
|
||||
}
|
||||
try {
|
||||
if (interaction.deferred && typeof interaction.editReply === 'function') {
|
||||
await interaction.editReply(payload)
|
||||
} else if (interaction.replied && typeof interaction.followUp === 'function') {
|
||||
await interaction.followUp(payload)
|
||||
} else {
|
||||
await interaction.reply(payload)
|
||||
}
|
||||
} catch (err) {
|
||||
if (ctx && ctx.console && typeof ctx.console.error === 'function') {
|
||||
ctx.console.error(
|
||||
'discord-bot: reply failed: ' + ((err && err.message) || err)
|
||||
)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var bareOsDiscordCommands = {
|
||||
buildSlashCommands: discordBuildSlashCommands,
|
||||
dispatchInteraction: discordDispatchInteraction,
|
||||
parseIdWhitelist: discordParseIdWhitelist,
|
||||
userAllowed: discordUserAllowed
|
||||
}
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = bareOsDiscordCommands
|
||||
}
|
||||
Reference in New Issue
Block a user