plugins
This commit is contained in:
@@ -1775,7 +1775,8 @@ async function discordCmdHandleBare(ctx, sub) {
|
||||
discordField('/net', 'Swarm — peers, summary'),
|
||||
discordField('/files /edit /create', 'Browse and edit files under `~/` and `/tmp`'),
|
||||
discordField('/settings', 'Live session knobs (theme, shell, Discord, agent, aliases)'),
|
||||
discordField('/run /journal /say /man', 'Full shell (cwd + autocomplete), logs, speak, man pages')
|
||||
discordField('/run /journal /say /man', 'Full shell (cwd + autocomplete), logs, speak, man pages'),
|
||||
discordField('/plugins', 'List, reload, enable/disable ~/.discord/plugins')
|
||||
]
|
||||
})
|
||||
)
|
||||
@@ -2860,113 +2861,682 @@ function discordCmdAddSubs(builder, items) {
|
||||
return true
|
||||
}
|
||||
|
||||
function discordBuildSlashCommands(dj) {
|
||||
var BARE_OS_DISCORD_PLUGIN_DIR = '~/.discord/plugins'
|
||||
var BARE_OS_DISCORD_PLUGIN_MAX = 40
|
||||
var BARE_OS_DISCORD_PLUGIN_SRC_MAX = 64 * 1024
|
||||
var BARE_OS_DISCORD_PLUGIN_RESERVED = {
|
||||
bare: 1,
|
||||
sys: 1,
|
||||
svc: 1,
|
||||
fs: 1,
|
||||
net: 1,
|
||||
man: 1,
|
||||
say: 1,
|
||||
run: 1,
|
||||
journal: 1,
|
||||
edit: 1,
|
||||
create: 1,
|
||||
files: 1,
|
||||
browse: 1,
|
||||
settings: 1,
|
||||
panel: 1,
|
||||
ping: 1,
|
||||
plugins: 1
|
||||
}
|
||||
var BARE_OS_DISCORD_PLUGINS = []
|
||||
var BARE_OS_DISCORD_PLUGINS_READY = false
|
||||
var BARE_OS_DISCORD_PLUGIN_REREGISTER = null
|
||||
|
||||
function discordPluginNameOk(name) {
|
||||
return /^[a-z0-9][a-z0-9-]{0,31}$/.test(String(name || ''))
|
||||
}
|
||||
|
||||
function discordPluginsSetRegistrar(fn) {
|
||||
BARE_OS_DISCORD_PLUGIN_REREGISTER = typeof fn === 'function' ? fn : null
|
||||
}
|
||||
|
||||
function discordPluginFind(name) {
|
||||
const n = String(name || '')
|
||||
for (let i = 0; i < BARE_OS_DISCORD_PLUGINS.length; i++) {
|
||||
if (BARE_OS_DISCORD_PLUGINS[i].name === n && BARE_OS_DISCORD_PLUGINS[i].ok) {
|
||||
return BARE_OS_DISCORD_PLUGINS[i]
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
function discordPluginExpand(tpl, opt, env) {
|
||||
return String(tpl == null ? '' : tpl).replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}|\$([A-Za-z_][A-Za-z0-9_]*)/g, function (_, a, def, b) {
|
||||
const key = a || b
|
||||
if (opt) {
|
||||
const v = opt(key)
|
||||
if (v != null && v !== '') return String(v)
|
||||
}
|
||||
if (env && env[key] != null && env[key] !== '') return String(env[key])
|
||||
return def != null ? String(def) : ''
|
||||
})
|
||||
}
|
||||
|
||||
function discordPluginSdk(ctx, plug) {
|
||||
const prefix = 'plug:' + plug.name + ':'
|
||||
const bot = {
|
||||
name: plug.name,
|
||||
version: plug.manifest && plug.manifest.version,
|
||||
command: function (spec) {
|
||||
if (spec && typeof spec === 'object') plug.handlers.push(spec)
|
||||
return bot
|
||||
},
|
||||
embed: function (opts) {
|
||||
return discordEmbed(opts)
|
||||
},
|
||||
field: discordField,
|
||||
result: function (embed, extra) {
|
||||
return discordResult(embed, extra)
|
||||
},
|
||||
fence: discordCmdFence,
|
||||
redact: discordCmdRedact,
|
||||
more: function (opts) {
|
||||
return { more: opts }
|
||||
},
|
||||
id: function (rest) {
|
||||
return prefix + String(rest || '').replace(/^plug:[^:]+:/, '')
|
||||
},
|
||||
user: function () {
|
||||
return discordSessionUser(ctx)
|
||||
},
|
||||
home: function () {
|
||||
return discordCmdEnv(ctx).HOME || '/home/' + discordSessionUser(ctx)
|
||||
},
|
||||
env: function () {
|
||||
const e = discordCmdEnv(ctx)
|
||||
const out = {}
|
||||
const skip = /TOKEN|SECRET|PASSWORD|PASSWD|API_KEY|PRIVATE|CREDENTIAL/i
|
||||
const keys = Object.keys(e)
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
if (skip.test(keys[i])) continue
|
||||
out[keys[i]] = e[keys[i]]
|
||||
}
|
||||
return out
|
||||
},
|
||||
read: function (path) {
|
||||
return discordCmdReadText(ctx, path)
|
||||
},
|
||||
write: function (path, text) {
|
||||
const p = discordCmdPathWriteOk(ctx, path)
|
||||
if (!p) return Promise.reject(new Error('not writable: ' + path))
|
||||
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') {
|
||||
return Promise.reject(new Error('writeFile unavailable'))
|
||||
}
|
||||
return ctx.vfs.writeFile(p, discordTextToBuf(ctx, String(text == null ? '' : text)))
|
||||
},
|
||||
ls: async function (path) {
|
||||
if (!ctx.vfs || typeof ctx.vfs.readdir !== 'function') return []
|
||||
const names = await ctx.vfs.readdir(path || '~')
|
||||
return Array.isArray(names) ? names : []
|
||||
},
|
||||
sh: function (line) {
|
||||
if (typeof ctx.execLine !== 'function') {
|
||||
return Promise.reject(new Error('execLine unavailable'))
|
||||
}
|
||||
return discordCmdCapture(ctx, function () {
|
||||
return ctx.execLine(String(line || ''), { timeoutMs: BARE_OS_DISCORD_SH_EXEC_MS })
|
||||
})
|
||||
}
|
||||
}
|
||||
return bot
|
||||
}
|
||||
|
||||
function discordPluginNormalize(raw, file) {
|
||||
const m = raw && typeof raw === 'object' ? raw : {}
|
||||
const name = String(m.name || '').toLowerCase()
|
||||
return {
|
||||
name: name,
|
||||
description: String(m.description || name || 'plugin'),
|
||||
version: String(m.version || '1.0.0'),
|
||||
enabled: m.enabled !== false,
|
||||
options: Array.isArray(m.options) ? m.options : [],
|
||||
subcommands: Array.isArray(m.subcommands) ? m.subcommands : [],
|
||||
run: typeof m.run === 'string' ? m.run : m.shell || '',
|
||||
title: m.title || '',
|
||||
readFile: m.file || '',
|
||||
embed: m.embed && typeof m.embed === 'object' ? m.embed : null,
|
||||
ephemeral: !!m.ephemeral,
|
||||
source: file || '',
|
||||
handlers: [],
|
||||
onComponent: typeof m.onComponent === 'function' ? m.onComponent : null,
|
||||
onModal: typeof m.onModal === 'function' ? m.onModal : null,
|
||||
onAutocomplete: typeof m.onAutocomplete === 'function' ? m.onAutocomplete : null,
|
||||
runFn: typeof m.run === 'function' ? m.run : null,
|
||||
manifest: m,
|
||||
ok: false,
|
||||
error: '',
|
||||
kind: 'json'
|
||||
}
|
||||
}
|
||||
|
||||
async function discordPluginEvalJs(ctx, plug, source) {
|
||||
const bot = discordPluginSdk(ctx, plug)
|
||||
let AsyncFn
|
||||
try {
|
||||
AsyncFn = Object.getPrototypeOf(async function () {}).constructor
|
||||
} catch {
|
||||
AsyncFn = Function
|
||||
}
|
||||
const body =
|
||||
String(source) +
|
||||
'\n;' +
|
||||
'if (typeof register === "function") await register(bot);\n' +
|
||||
'else if (typeof plugin === "object" && plugin) bot.command(plugin);\n'
|
||||
const fn = new AsyncFn('bot', 'sdk', 'register', body)
|
||||
function register(spec) {
|
||||
bot.command(spec)
|
||||
}
|
||||
await fn(bot, bot, register)
|
||||
if (plug.handlers.length) {
|
||||
const first = plug.handlers[0]
|
||||
if (!plug.name && first.name) plug.name = String(first.name).toLowerCase()
|
||||
if (first.description && plug.description === plug.name) plug.description = first.description
|
||||
if (first.options) plug.options = first.options
|
||||
if (first.subcommands) plug.subcommands = first.subcommands
|
||||
if (typeof first.run === 'function') plug.runFn = first.run
|
||||
if (typeof first.onComponent === 'function') plug.onComponent = first.onComponent
|
||||
if (typeof first.onModal === 'function') plug.onModal = first.onModal
|
||||
if (typeof first.onAutocomplete === 'function') plug.onAutocomplete = first.onAutocomplete
|
||||
}
|
||||
plug.kind = 'js'
|
||||
}
|
||||
|
||||
async function discordPluginsDisabledSet(ctx) {
|
||||
const text = await discordCmdReadText(ctx, BARE_OS_DISCORD_PLUGIN_DIR + '/disabled.txt')
|
||||
const set = Object.create(null)
|
||||
const lines = String(text || '').split(/\r?\n/)
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const n = lines[i].trim()
|
||||
if (!n || n.charAt(0) === '#') continue
|
||||
set[n] = 1
|
||||
}
|
||||
return set
|
||||
}
|
||||
|
||||
async function discordPluginsWriteDisabled(ctx, set) {
|
||||
const names = Object.keys(set).sort()
|
||||
const body = names.length ? names.join('\n') + '\n' : ''
|
||||
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') throw new Error('writeFile unavailable')
|
||||
await discordEnsureDir(ctx, BARE_OS_DISCORD_PLUGIN_DIR)
|
||||
await ctx.vfs.writeFile(BARE_OS_DISCORD_PLUGIN_DIR + '/disabled.txt', discordTextToBuf(ctx, body))
|
||||
}
|
||||
|
||||
async function discordPluginsLoad(ctx) {
|
||||
BARE_OS_DISCORD_PLUGINS = []
|
||||
BARE_OS_DISCORD_PLUGINS_READY = true
|
||||
if (!ctx || !ctx.vfs) return BARE_OS_DISCORD_PLUGINS
|
||||
await discordEnsureDir(ctx, BARE_OS_DISCORD_PLUGIN_DIR)
|
||||
let names = []
|
||||
try {
|
||||
if (typeof ctx.vfs.readdir === 'function') {
|
||||
names = await ctx.vfs.readdir(BARE_OS_DISCORD_PLUGIN_DIR)
|
||||
}
|
||||
} catch {
|
||||
names = []
|
||||
}
|
||||
if (!Array.isArray(names)) names = []
|
||||
const disabled = await discordPluginsDisabledSet(ctx)
|
||||
const seen = Object.create(null)
|
||||
for (let i = 0; i < names.length && BARE_OS_DISCORD_PLUGINS.length < BARE_OS_DISCORD_PLUGIN_MAX; i++) {
|
||||
const entry = String(names[i] || '')
|
||||
if (!entry || entry.charAt(0) === '.' || entry === 'disabled.txt' || entry === 'README.md') continue
|
||||
if (/\.disabled$/.test(entry)) continue
|
||||
let id = entry.replace(/\.(json|js)$/i, '')
|
||||
let kind = ''
|
||||
let srcPath = ''
|
||||
let jsonPath = ''
|
||||
if (/\.json$/i.test(entry)) {
|
||||
kind = 'json'
|
||||
jsonPath = BARE_OS_DISCORD_PLUGIN_DIR + '/' + entry
|
||||
} else if (/\.js$/i.test(entry)) {
|
||||
kind = 'js'
|
||||
srcPath = BARE_OS_DISCORD_PLUGIN_DIR + '/' + entry
|
||||
} else {
|
||||
jsonPath = BARE_OS_DISCORD_PLUGIN_DIR + '/' + entry + '/plugin.json'
|
||||
srcPath = BARE_OS_DISCORD_PLUGIN_DIR + '/' + entry + '/index.js'
|
||||
const hasJson = await discordFileExists(ctx, jsonPath)
|
||||
const hasJs = await discordFileExists(ctx, srcPath)
|
||||
if (hasJson) kind = 'dir-json'
|
||||
else if (hasJs) kind = 'dir-js'
|
||||
else continue
|
||||
if (!hasJson) jsonPath = ''
|
||||
if (!hasJs) srcPath = ''
|
||||
}
|
||||
if (seen[id]) continue
|
||||
seen[id] = 1
|
||||
const plug = discordPluginNormalize({ name: id }, entry)
|
||||
plug.source = BARE_OS_DISCORD_PLUGIN_DIR + '/' + entry
|
||||
if (disabled[id]) {
|
||||
plug.enabled = false
|
||||
plug.error = 'disabled'
|
||||
BARE_OS_DISCORD_PLUGINS.push(plug)
|
||||
continue
|
||||
}
|
||||
try {
|
||||
if (jsonPath) {
|
||||
const raw = await discordCmdReadText(ctx, jsonPath)
|
||||
if (raw.length > BARE_OS_DISCORD_PLUGIN_SRC_MAX) throw new Error('manifest too large')
|
||||
const j = discordTryJson(raw)
|
||||
if (!j) throw new Error('invalid JSON')
|
||||
const n = discordPluginNormalize(j, entry)
|
||||
plug.name = n.name || id
|
||||
plug.description = n.description
|
||||
plug.version = n.version
|
||||
plug.enabled = n.enabled
|
||||
plug.options = n.options
|
||||
plug.subcommands = n.subcommands
|
||||
plug.run = n.run
|
||||
plug.title = n.title
|
||||
plug.readFile = n.readFile
|
||||
plug.embed = n.embed
|
||||
plug.ephemeral = n.ephemeral
|
||||
plug.manifest = j
|
||||
}
|
||||
if (srcPath) {
|
||||
const src = await discordCmdReadText(ctx, srcPath)
|
||||
if (src.length > BARE_OS_DISCORD_PLUGIN_SRC_MAX) throw new Error('script too large')
|
||||
await discordPluginEvalJs(ctx, plug, src)
|
||||
}
|
||||
if (!discordPluginNameOk(plug.name)) throw new Error('bad command name (use a-z0-9-, 1–32)')
|
||||
if (BARE_OS_DISCORD_PLUGIN_RESERVED[plug.name]) throw new Error('name is reserved: /' + plug.name)
|
||||
if (plug.enabled === false) {
|
||||
plug.error = 'disabled in manifest'
|
||||
BARE_OS_DISCORD_PLUGINS.push(plug)
|
||||
continue
|
||||
}
|
||||
plug.ok = true
|
||||
BARE_OS_DISCORD_PLUGINS.push(plug)
|
||||
} catch (err) {
|
||||
plug.ok = false
|
||||
plug.error = String((err && err.message) || err)
|
||||
BARE_OS_DISCORD_PLUGINS.push(plug)
|
||||
}
|
||||
}
|
||||
return BARE_OS_DISCORD_PLUGINS
|
||||
}
|
||||
|
||||
async function discordPluginsEnsure(ctx) {
|
||||
if (!BARE_OS_DISCORD_PLUGINS_READY) await discordPluginsLoad(ctx)
|
||||
return BARE_OS_DISCORD_PLUGINS
|
||||
}
|
||||
|
||||
function discordPluginAddOptions(builder, opts) {
|
||||
if (!opts || !opts.length) return
|
||||
for (let i = 0; i < opts.length && i < 10; i++) {
|
||||
const o = opts[i]
|
||||
if (!o || !o.name) continue
|
||||
discordCmdOpt(
|
||||
builder,
|
||||
String(o.name).slice(0, 32),
|
||||
String(o.description || o.name).slice(0, 100),
|
||||
!!o.required,
|
||||
!!o.autocomplete
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function discordPluginsSlash(dj) {
|
||||
const B = dj && dj.SlashCommandBuilder
|
||||
if (typeof B !== 'function') return []
|
||||
const bare = new B().setName('bare').setDescription('Bare OS session (logged-in user)')
|
||||
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 (or open a compose form)')
|
||||
const run = new B().setName('run').setDescription('Non-interactive Bare OS shell (cwd, pipes, cd, history)')
|
||||
const journal = new B()
|
||||
.setName('journal')
|
||||
.setDescription('Tail a unit or system log')
|
||||
const edit = new B()
|
||||
.setName('edit')
|
||||
.setDescription('Edit a text file in a Discord modal (home or /tmp)')
|
||||
const create = new B()
|
||||
.setName('create')
|
||||
.setDescription('Create a new text file (pick a path, then enter contents)')
|
||||
const files = new B()
|
||||
.setName('files')
|
||||
.setDescription('Browse and manage files (list, open, mkdir, rename, delete)')
|
||||
const settings = new B()
|
||||
.setName('settings')
|
||||
.setDescription('Live session settings (theme, shell, discord, agent, aliases)')
|
||||
const panel = new B()
|
||||
.setName('panel')
|
||||
.setDescription('Interactive Bare OS control panel')
|
||||
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 (logged-in user)'],
|
||||
['whoami', 'Session 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, true)
|
||||
}],
|
||||
['start', 'Start a unit', function (s) {
|
||||
discordCmdOpt(s, 'unit', 'Unit name', true, true)
|
||||
}],
|
||||
['stop', 'Stop a unit', function (s) {
|
||||
discordCmdOpt(s, 'unit', 'Unit name', true, true)
|
||||
}],
|
||||
['restart', 'Restart a unit', function (s) {
|
||||
discordCmdOpt(s, 'unit', 'Unit name', true, true)
|
||||
}],
|
||||
['logs', 'Unit logs', function (s) {
|
||||
discordCmdOpt(s, 'unit', 'Unit name', true, true)
|
||||
}]
|
||||
]) &&
|
||||
discordCmdAddSubs(fsCmd, [
|
||||
['ls', 'List a directory', function (s) {
|
||||
discordCmdOpt(s, 'path', 'VFS path', false, true)
|
||||
}],
|
||||
['cat', 'Read a file', function (s) {
|
||||
discordCmdOpt(s, 'path', 'VFS path', true, true)
|
||||
}],
|
||||
['stat', 'Stat a path', function (s) {
|
||||
discordCmdOpt(s, 'path', 'VFS path', true, true)
|
||||
}],
|
||||
['head', 'First lines of a file', function (s) {
|
||||
discordCmdOpt(s, 'path', 'VFS path', true, true)
|
||||
discordCmdOpt(s, 'lines', 'Line count', false, false)
|
||||
}]
|
||||
]) &&
|
||||
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, true)
|
||||
discordCmdOpt(say, 'text', 'Text to box (omit to open a form)', false, false)
|
||||
discordCmdOpt(run, 'cmd', 'Shell line (cd, pipes, &&, redirects, $VAR)', false, true)
|
||||
discordCmdOpt(journal, 'unit', 'Unit name', false, true)
|
||||
discordCmdOpt(edit, 'path', 'File under ~ or /tmp (omit to pick)', false, true)
|
||||
discordCmdOpt(create, 'path', 'New file under ~ or /tmp (omit to pick)', false, true)
|
||||
discordCmdOpt(files, 'path', 'Directory to open', false, true)
|
||||
return [bare, sys, svc, fsCmd, net, man, say, run, journal, edit, create, files, settings, panel, ping].map(
|
||||
function (c) {
|
||||
return c.toJSON()
|
||||
const out = []
|
||||
for (let i = 0; i < BARE_OS_DISCORD_PLUGINS.length; i++) {
|
||||
const p = BARE_OS_DISCORD_PLUGINS[i]
|
||||
if (!p.ok) continue
|
||||
try {
|
||||
const c = new B().setName(p.name).setDescription(String(p.description || p.name).slice(0, 100))
|
||||
if (p.subcommands && p.subcommands.length) {
|
||||
const items = []
|
||||
for (let s = 0; s < p.subcommands.length && s < 10; s++) {
|
||||
const sc = p.subcommands[s]
|
||||
if (!sc || !sc.name) continue
|
||||
items.push([
|
||||
String(sc.name).slice(0, 32),
|
||||
String(sc.description || sc.name).slice(0, 100),
|
||||
sc.options
|
||||
? function (b) {
|
||||
discordPluginAddOptions(b, sc.options)
|
||||
}
|
||||
: null
|
||||
])
|
||||
}
|
||||
discordCmdAddSubs(c, items)
|
||||
} else {
|
||||
discordPluginAddOptions(c, p.options)
|
||||
}
|
||||
out.push(c.toJSON())
|
||||
} catch {
|
||||
/* skip bad slash shape */
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function discordPluginEvent(ctx, interaction, plug, sub, opt) {
|
||||
const bot = discordPluginSdk(ctx, plug)
|
||||
return {
|
||||
name: plug.name,
|
||||
sub: sub || '',
|
||||
opt: opt || function () {
|
||||
return ''
|
||||
},
|
||||
interaction: interaction,
|
||||
userId: discordInteractionUserId(interaction),
|
||||
user: discordSessionUser(ctx),
|
||||
ctx: ctx,
|
||||
bot: bot,
|
||||
embed: bot.embed,
|
||||
field: bot.field,
|
||||
fence: bot.fence,
|
||||
sh: bot.sh,
|
||||
read: bot.read,
|
||||
write: bot.write,
|
||||
ls: bot.ls
|
||||
}
|
||||
}
|
||||
|
||||
async function discordPluginInvoke(ctx, interaction, plug, sub, opt) {
|
||||
const event = discordPluginEvent(ctx, interaction, plug, sub, opt)
|
||||
let spec = plug
|
||||
if (sub && plug.subcommands) {
|
||||
for (let i = 0; i < plug.subcommands.length; i++) {
|
||||
if (plug.subcommands[i] && plug.subcommands[i].name === sub) {
|
||||
spec = plug.subcommands[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
const runFn = spec.runFn || plug.runFn || (typeof spec.run === 'function' ? spec.run : null)
|
||||
if (typeof runFn === 'function') {
|
||||
const out = await runFn(event)
|
||||
if (out == null) return { text: '(no output)', ephemeral: true }
|
||||
if (typeof out === 'string') return { text: discordCmdRedact(out), ephemeral: !!(spec.ephemeral || plug.ephemeral) }
|
||||
if (out.embeds || out.text || out.more || out.modal || out.components) {
|
||||
if ((spec.ephemeral || plug.ephemeral) && out.ephemeral == null) out.ephemeral = true
|
||||
return out
|
||||
}
|
||||
if (out.title || out.desc || out.fields) {
|
||||
return discordResult(discordEmbed(out), { ephemeral: !!(spec.ephemeral || plug.ephemeral) })
|
||||
}
|
||||
return { text: String(out), ephemeral: !!(spec.ephemeral || plug.ephemeral) }
|
||||
}
|
||||
if (spec.readFile || spec.file || (plug.embed && plug.embed.file)) {
|
||||
const path = spec.readFile || spec.file || plug.embed.file
|
||||
const body = await discordCmdReadText(ctx, path)
|
||||
return {
|
||||
more: {
|
||||
title: spec.title || plug.title || plug.name,
|
||||
body: body || '(empty)',
|
||||
fence: true
|
||||
}
|
||||
}
|
||||
}
|
||||
const shell = typeof spec.run === 'string' && spec.run ? spec.run : plug.run
|
||||
if (shell) {
|
||||
const line = discordPluginExpand(shell, opt, discordCmdEnv(ctx))
|
||||
const out = await discordCmdCapture(ctx, function () {
|
||||
if (typeof ctx.execLine !== 'function') throw new Error('execLine unavailable')
|
||||
return ctx.execLine(line, { timeoutMs: BARE_OS_DISCORD_SH_EXEC_MS })
|
||||
})
|
||||
return {
|
||||
more: {
|
||||
title: spec.title || plug.title || '/' + plug.name,
|
||||
lead: '`' + line + '`',
|
||||
body: out || '(no output)',
|
||||
fence: true,
|
||||
footer: 'plugin · ' + plug.name + ' · ' + discordSessionUser(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (spec.embed || plug.embed) {
|
||||
const em = spec.embed || plug.embed
|
||||
return discordResult(
|
||||
discordEmbed({
|
||||
title: em.title || plug.title || plug.name,
|
||||
desc: em.desc || em.description || '',
|
||||
fields: em.fields
|
||||
}),
|
||||
{ ephemeral: !!(spec.ephemeral || plug.ephemeral) }
|
||||
)
|
||||
}
|
||||
return { text: 'Plugin /' + plug.name + ' has nothing to run.', ephemeral: true }
|
||||
}
|
||||
|
||||
async function discordPluginComponent(ctx, interaction, id) {
|
||||
const parts = String(id || '').split(':')
|
||||
const name = parts[1] || ''
|
||||
const rest = parts.slice(2).join(':')
|
||||
const plug = discordPluginFind(name)
|
||||
if (!plug || typeof plug.onComponent !== 'function') {
|
||||
return discordSendResult(ctx, interaction, {
|
||||
text: 'Plugin button expired or unknown.',
|
||||
ephemeral: true
|
||||
})
|
||||
}
|
||||
const event = discordPluginEvent(ctx, interaction, plug, '', function () {
|
||||
return ''
|
||||
})
|
||||
event.id = rest
|
||||
event.values = interaction.values || []
|
||||
const out = await plug.onComponent(event)
|
||||
if (out == null) return discordSendResult(ctx, interaction, { text: 'ok', ephemeral: true })
|
||||
return discordSendResult(ctx, interaction, typeof out === 'string' ? { text: out } : out)
|
||||
}
|
||||
|
||||
async function discordPluginsAdmin(ctx, interaction, sub, opt) {
|
||||
await discordPluginsEnsure(ctx)
|
||||
const action = sub || 'list'
|
||||
if (action === 'reload') {
|
||||
await discordPluginsLoad(ctx)
|
||||
let note = 'Reloaded **' + BARE_OS_DISCORD_PLUGINS.length + '** plugin file(s).'
|
||||
if (typeof BARE_OS_DISCORD_PLUGIN_REREGISTER === 'function') {
|
||||
try {
|
||||
await BARE_OS_DISCORD_PLUGIN_REREGISTER()
|
||||
note += ' Slash commands re-registered.'
|
||||
} catch (err) {
|
||||
note += ' Reload dispatch ok; slash re-register failed: ' + ((err && err.message) || err)
|
||||
}
|
||||
} else {
|
||||
note += ' Restart `discord-bot` (or the initd unit) to publish new slash names to Discord.'
|
||||
}
|
||||
return discordPluginsAdmin(ctx, interaction, 'list', opt).then(function (res) {
|
||||
if (res && res.embeds && res.embeds[0]) {
|
||||
res.embeds[0].description = note + (res.embeds[0].description ? '\n' + res.embeds[0].description : '')
|
||||
}
|
||||
return res
|
||||
})
|
||||
}
|
||||
if (action === 'disable' || action === 'enable') {
|
||||
const want = String(opt('name') || '').toLowerCase()
|
||||
if (!want) return { text: 'plugin name required', ephemeral: true }
|
||||
const set = await discordPluginsDisabledSet(ctx)
|
||||
if (action === 'disable') set[want] = 1
|
||||
else delete set[want]
|
||||
await discordPluginsWriteDisabled(ctx, set)
|
||||
await discordPluginsLoad(ctx)
|
||||
return {
|
||||
text: (action === 'disable' ? 'Disabled' : 'Enabled') + ' `' + want + '`. Restart or `/plugins reload` to refresh slash names.',
|
||||
ephemeral: true
|
||||
}
|
||||
}
|
||||
if (action === 'info') {
|
||||
const want = String(opt('name') || '').toLowerCase()
|
||||
let plug = null
|
||||
for (let i = 0; i < BARE_OS_DISCORD_PLUGINS.length; i++) {
|
||||
if (BARE_OS_DISCORD_PLUGINS[i].name === want) plug = BARE_OS_DISCORD_PLUGINS[i]
|
||||
}
|
||||
if (!plug) return { text: 'No plugin named `' + want + '`.', ephemeral: true }
|
||||
return discordResult(
|
||||
discordEmbed({
|
||||
title: '/' + plug.name,
|
||||
desc: plug.description,
|
||||
fields: [
|
||||
discordField('Status', plug.ok ? 'loaded' : plug.error || 'error', true),
|
||||
discordField('Kind', plug.kind, true),
|
||||
discordField('Version', plug.version, true),
|
||||
discordField('File', plug.source || plug.file || '—', false)
|
||||
],
|
||||
color: plug.ok ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR_WARN
|
||||
})
|
||||
)
|
||||
}
|
||||
const fields = []
|
||||
for (let i = 0; i < BARE_OS_DISCORD_PLUGINS.length && fields.length < 20; i++) {
|
||||
const p = BARE_OS_DISCORD_PLUGINS[i]
|
||||
fields.push(
|
||||
discordField(
|
||||
'/' + p.name,
|
||||
(p.ok ? '**on**' : '**off**') + ' · ' + (p.error || p.description || p.kind)
|
||||
)
|
||||
)
|
||||
}
|
||||
return discordResult(
|
||||
discordEmbed({
|
||||
title: 'Plugins',
|
||||
desc:
|
||||
'Drop JSON or JS in `~/.discord/plugins`. See **developer-guide ch.21** for the SDK.\n' +
|
||||
BARE_OS_DISCORD_PLUGINS.length +
|
||||
' discovered.',
|
||||
fields: fields.length ? fields : [discordField('plugins', 'None yet. Add `~/.discord/plugins/hello.json`.')]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function discordBuildSlashCommands(dj, ctx) {
|
||||
const B = dj && dj.SlashCommandBuilder
|
||||
if (typeof B !== 'function') return []
|
||||
const finish = function () {
|
||||
const B2 = dj.SlashCommandBuilder
|
||||
const bare = new B2().setName('bare').setDescription('Bare OS session (logged-in user)')
|
||||
const sys = new B2().setName('sys').setDescription('Bare OS system snapshots')
|
||||
const svc = new B2().setName('svc').setDescription('systemctl units')
|
||||
const fsCmd = new B2().setName('fs').setDescription('Read-only VFS')
|
||||
const net = new B2().setName('net').setDescription('Swarm / network')
|
||||
const man = new B2().setName('man').setDescription('Look up a man page')
|
||||
const say = new B2().setName('say').setDescription('Speak as Bare OS (or open a compose form)')
|
||||
const run = new B2().setName('run').setDescription('Non-interactive Bare OS shell (cwd, pipes, cd, history)')
|
||||
const journal = new B2()
|
||||
.setName('journal')
|
||||
.setDescription('Tail a unit or system log')
|
||||
const edit = new B2()
|
||||
.setName('edit')
|
||||
.setDescription('Edit a text file in a Discord modal (home or /tmp)')
|
||||
const create = new B2()
|
||||
.setName('create')
|
||||
.setDescription('Create a new text file (pick a path, then enter contents)')
|
||||
const files = new B2()
|
||||
.setName('files')
|
||||
.setDescription('Browse and manage files (list, open, mkdir, rename, delete)')
|
||||
const settings = new B2()
|
||||
.setName('settings')
|
||||
.setDescription('Live session settings (theme, shell, discord, agent, aliases)')
|
||||
const plugins = new B2()
|
||||
.setName('plugins')
|
||||
.setDescription('List, reload, and manage ~/.discord/plugins')
|
||||
const panel = new B2()
|
||||
.setName('panel')
|
||||
.setDescription('Interactive Bare OS control panel')
|
||||
const ping = new B2().setName('ping').setDescription('Reply pong')
|
||||
const ok =
|
||||
discordCmdAddSubs(bare, [
|
||||
['ping', 'Latency / liveness'],
|
||||
['about', 'What this bot is'],
|
||||
['help', 'Command map'],
|
||||
['status', 'Session snapshot (logged-in user)'],
|
||||
['whoami', 'Session 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, true)
|
||||
}],
|
||||
['start', 'Start a unit', function (s) {
|
||||
discordCmdOpt(s, 'unit', 'Unit name', true, true)
|
||||
}],
|
||||
['stop', 'Stop a unit', function (s) {
|
||||
discordCmdOpt(s, 'unit', 'Unit name', true, true)
|
||||
}],
|
||||
['restart', 'Restart a unit', function (s) {
|
||||
discordCmdOpt(s, 'unit', 'Unit name', true, true)
|
||||
}],
|
||||
['logs', 'Unit logs', function (s) {
|
||||
discordCmdOpt(s, 'unit', 'Unit name', true, true)
|
||||
}]
|
||||
]) &&
|
||||
discordCmdAddSubs(fsCmd, [
|
||||
['ls', 'List a directory', function (s) {
|
||||
discordCmdOpt(s, 'path', 'VFS path', false, true)
|
||||
}],
|
||||
['cat', 'Read a file', function (s) {
|
||||
discordCmdOpt(s, 'path', 'VFS path', true, true)
|
||||
}],
|
||||
['stat', 'Stat a path', function (s) {
|
||||
discordCmdOpt(s, 'path', 'VFS path', true, true)
|
||||
}],
|
||||
['head', 'First lines of a file', function (s) {
|
||||
discordCmdOpt(s, 'path', 'VFS path', true, true)
|
||||
discordCmdOpt(s, 'lines', 'Line count', false, false)
|
||||
}]
|
||||
]) &&
|
||||
discordCmdAddSubs(net, [
|
||||
['peers', 'Swarm peers'],
|
||||
['swarm', 'Swarm snapshot'],
|
||||
['summary', 'net_summary']
|
||||
]) &&
|
||||
discordCmdAddSubs(plugins, [
|
||||
['list', 'Loaded plugins'],
|
||||
['reload', 'Rescan ~/.discord/plugins'],
|
||||
['info', 'Plugin details', function (s) {
|
||||
discordCmdOpt(s, 'name', 'Plugin name', true, false)
|
||||
}],
|
||||
['disable', 'Disable a plugin', function (s) {
|
||||
discordCmdOpt(s, 'name', 'Plugin name', true, false)
|
||||
}],
|
||||
['enable', 'Enable a plugin', function (s) {
|
||||
discordCmdOpt(s, 'name', 'Plugin name', true, false)
|
||||
}]
|
||||
])
|
||||
if (!ok) {
|
||||
return [ping.toJSON()]
|
||||
}
|
||||
discordCmdOpt(man, 'page', 'Command name (e.g. uname)', true, true)
|
||||
discordCmdOpt(say, 'text', 'Text to box (omit to open a form)', false, false)
|
||||
discordCmdOpt(run, 'cmd', 'Shell line (cd, pipes, &&, redirects, $VAR)', false, true)
|
||||
discordCmdOpt(journal, 'unit', 'Unit name', false, true)
|
||||
discordCmdOpt(edit, 'path', 'File under ~ or /tmp (omit to pick)', false, true)
|
||||
discordCmdOpt(create, 'path', 'New file under ~ or /tmp (omit to pick)', false, true)
|
||||
discordCmdOpt(files, 'path', 'Directory to open', false, true)
|
||||
const stock = [bare, sys, svc, fsCmd, net, man, say, run, journal, edit, create, files, settings, plugins, panel, ping].map(
|
||||
function (c) {
|
||||
return c.toJSON()
|
||||
}
|
||||
)
|
||||
return stock.concat(discordPluginsSlash(dj))
|
||||
}
|
||||
if (ctx && typeof ctx.then !== 'function') {
|
||||
return Promise.resolve(discordPluginsEnsure(ctx)).then(finish)
|
||||
}
|
||||
return finish()
|
||||
}
|
||||
|
||||
function discordShowModal(interaction, spec) {
|
||||
if (!interaction || typeof interaction.showModal !== 'function') return false
|
||||
return interaction.showModal({
|
||||
@@ -4663,6 +5233,22 @@ async function discordDispatchAutocomplete(ctx, interaction) {
|
||||
cmd === 'edit' || cmd === 'create'
|
||||
? discordSuggestEditPaths(ctx, q)
|
||||
: discordSuggestPaths(ctx, q)
|
||||
} else if (fname === 'name' && String(interaction.commandName || '') === 'plugins') {
|
||||
await discordPluginsEnsure(ctx)
|
||||
const names = []
|
||||
for (let i = 0; i < BARE_OS_DISCORD_PLUGINS.length; i++) names.push(BARE_OS_DISCORD_PLUGINS[i].name)
|
||||
choices = discordFilterChoices(names, q)
|
||||
} else {
|
||||
const plug = discordPluginFind(interaction.commandName)
|
||||
if (plug && typeof plug.onAutocomplete === 'function') {
|
||||
const ev = discordPluginEvent(ctx, interaction, plug, '', function () {
|
||||
return q
|
||||
})
|
||||
ev.focused = fname
|
||||
ev.query = q
|
||||
const got = await plug.onAutocomplete(ev)
|
||||
if (Array.isArray(got)) choices = discordFilterChoices(got, q)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
choices = []
|
||||
@@ -4840,6 +5426,10 @@ async function discordDispatchComponent(ctx, interaction) {
|
||||
const opt = function () {
|
||||
return ''
|
||||
}
|
||||
if (id.indexOf('plug:') === 0) {
|
||||
await discordPluginComponent(ctx, interaction, id)
|
||||
return true
|
||||
}
|
||||
if (id.indexOf('sh:') === 0 || id === 'run:compose') {
|
||||
await discordShAction(ctx, interaction, id)
|
||||
return true
|
||||
@@ -4965,6 +5555,26 @@ async function discordDispatchComponent(ctx, interaction) {
|
||||
|
||||
async function discordDispatchModal(ctx, interaction) {
|
||||
const id = String(interaction.customId || '')
|
||||
if (id.indexOf('plug:') === 0) {
|
||||
const parts = id.split(':')
|
||||
const plug = discordPluginFind(parts[1] || '')
|
||||
if (plug && typeof plug.onModal === 'function') {
|
||||
const ev = discordPluginEvent(ctx, interaction, plug, '', function () {
|
||||
return ''
|
||||
})
|
||||
ev.id = parts.slice(2).join(':')
|
||||
ev.value = ''
|
||||
try {
|
||||
if (interaction.fields && typeof interaction.fields.getTextInputValue === 'function') {
|
||||
ev.value = String(interaction.fields.getTextInputValue('text') || '')
|
||||
}
|
||||
} catch {
|
||||
ev.value = ''
|
||||
}
|
||||
const out = await plug.onModal(ev)
|
||||
return discordSendResult(ctx, interaction, typeof out === 'string' ? { text: out } : out || { text: 'ok' })
|
||||
}
|
||||
}
|
||||
let value = ''
|
||||
try {
|
||||
if (interaction.fields && typeof interaction.fields.getTextInputValue === 'function') {
|
||||
@@ -5221,6 +5831,7 @@ async function discordDispatchInteraction(ctx, interaction) {
|
||||
}
|
||||
return true
|
||||
}
|
||||
await discordPluginsEnsure(ctx)
|
||||
return discordDispatchAutocomplete(ctx, interaction)
|
||||
}
|
||||
if (!discordUserAllowed(ctx, discordInteractionUserId(interaction))) {
|
||||
@@ -5270,7 +5881,14 @@ async function discordDispatchInteraction(ctx, interaction) {
|
||||
}
|
||||
let result
|
||||
try {
|
||||
result = await discordRouteCommand(ctx, name, sub, opt)
|
||||
await discordPluginsEnsure(ctx)
|
||||
if (name === 'plugins') {
|
||||
result = await discordPluginsAdmin(ctx, interaction, sub, opt)
|
||||
} else {
|
||||
const plug = discordPluginFind(name)
|
||||
if (plug) result = await discordPluginInvoke(ctx, interaction, plug, sub, opt)
|
||||
else result = await discordRouteCommand(ctx, name, sub, opt)
|
||||
}
|
||||
} catch (err) {
|
||||
result = {
|
||||
text: 'error: ' + ((err && err.message) || String(err)),
|
||||
@@ -5325,7 +5943,12 @@ var bareOsDiscordCommands = {
|
||||
idleArm: discordIdleArm,
|
||||
idleLive: function () {
|
||||
return BARE_OS_DISCORD_LIVE
|
||||
}
|
||||
},
|
||||
loadPlugins: discordPluginsLoad,
|
||||
listPlugins: function () {
|
||||
return BARE_OS_DISCORD_PLUGINS
|
||||
},
|
||||
setPluginRegistrar: discordPluginsSetRegistrar
|
||||
}
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = bareOsDiscordCommands
|
||||
|
||||
Reference in New Issue
Block a user