5958 lines
183 KiB
JavaScript
5958 lines
183 KiB
JavaScript
/**
|
||
* Bare OS Discord slash-command catalog (guest-safe: no import/export).
|
||
* Used by /bin/discord-bot (prepended) and the bare-os-discord initd unit.
|
||
* Host/pack must static-import this .cjs (createRequire fails under app.bundle).
|
||
*/
|
||
|
||
var BARE_OS_DISCORD_REPLY_MAX = 1900
|
||
var BARE_OS_DISCORD_FS_MAX = 12 * 1024
|
||
/** Discord API hard caps (message content / embed parts / combined embed). */
|
||
var BARE_OS_DISCORD_LIMIT = {
|
||
content: 2000,
|
||
title: 256,
|
||
desc: 4096,
|
||
fieldName: 256,
|
||
fieldValue: 1024,
|
||
footer: 2048,
|
||
author: 256,
|
||
fields: 25,
|
||
embedTotal: 6000,
|
||
pageChars: 3000
|
||
}
|
||
var BARE_OS_DISCORD_MORE_SESSIONS = Object.create(null)
|
||
/** discord.js MessageFlags.Ephemeral (1 << 6). Avoid the deprecated `ephemeral` option. */
|
||
var BARE_OS_DISCORD_FLAG_EPHEMERAL = 64
|
||
var BARE_OS_DISCORD_WHITELIST_DENY =
|
||
'Access denied. Your Discord user id is not on DISCORD_ID_WHITELIST.'
|
||
var BARE_OS_DISCORD_COLOR = 0x5865f2
|
||
var BARE_OS_DISCORD_COLOR_OK = 0x57f287
|
||
var BARE_OS_DISCORD_COLOR_WARN = 0xfee75c
|
||
var BARE_OS_DISCORD_COLOR_ERR = 0xed4245
|
||
var BARE_OS_DISCORD_SH_TTL_MS = 30 * 60 * 1000
|
||
var BARE_OS_DISCORD_SH_SESSIONS = Object.create(null)
|
||
var BARE_OS_DISCORD_SH_BINS = null
|
||
var BARE_OS_DISCORD_SH_BINS_AT = 0
|
||
var BARE_OS_DISCORD_SH_EXEC_MS = 20000
|
||
var BARE_OS_DISCORD_SH_BUILTINS = [
|
||
'alias',
|
||
'unalias',
|
||
'barerc',
|
||
'cd',
|
||
'export',
|
||
'unset',
|
||
'set',
|
||
'true',
|
||
'false',
|
||
'echo',
|
||
'pwd',
|
||
'type',
|
||
'command',
|
||
'umask',
|
||
'readonly',
|
||
'test',
|
||
'[',
|
||
'jobs',
|
||
'wait',
|
||
':',
|
||
'help'
|
||
]
|
||
var BARE_OS_DISCORD_SH_TTY = {
|
||
edit: 1,
|
||
nano: 1,
|
||
vim: 1,
|
||
vi: 1,
|
||
btop: 1,
|
||
baretop: 1,
|
||
irc: 1,
|
||
chat: 1,
|
||
dhttop: 1,
|
||
swarmmap: 1,
|
||
summon: 1,
|
||
'holepunch-view': 1,
|
||
routeview: 1
|
||
}
|
||
var BARE_OS_DISCORD_SH_FLAGS = {
|
||
ls: ['-a', '-l', '-la', '-lh', '-h', '-1', '-F'],
|
||
grep: ['-i', '-v', '-n', '-r', '-E', '-F'],
|
||
find: ['-name', '-type', '-size', '-path'],
|
||
rm: ['-r', '-f', '-rf', '-v'],
|
||
cp: ['-r', '-v', '-n'],
|
||
mv: ['-v', '-n'],
|
||
cat: ['-n', '-A'],
|
||
head: ['-n'],
|
||
tail: ['-n', '-f'],
|
||
chmod: ['-R'],
|
||
chown: ['-R'],
|
||
df: ['-h', '-T'],
|
||
du: ['-h', '-s', '-a'],
|
||
ps: ['-e', '-f'],
|
||
mkdir: ['-p'],
|
||
tar: ['-xvf', '-cvf', '-tzf']
|
||
}
|
||
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,
|
||
df: 1,
|
||
ps: 1,
|
||
procstat: 1,
|
||
'uname -a': 1
|
||
}
|
||
var BARE_OS_DISCORD_KNOWN_UNITS = [
|
||
'bare-os-discord',
|
||
'bare-os-www',
|
||
'bare-os-chat',
|
||
'bare-holesail',
|
||
'kernel-logger'
|
||
]
|
||
var BARE_OS_DISCORD_EDIT_CHUNK = 4000
|
||
var BARE_OS_DISCORD_EDIT_MAX_CHUNKS = 5
|
||
var BARE_OS_DISCORD_IDLE_MS = 2 * 60 * 1000
|
||
var BARE_OS_DISCORD_EDIT_TTL_MS = BARE_OS_DISCORD_IDLE_MS
|
||
var BARE_OS_DISCORD_EDIT_SESSIONS = Object.create(null)
|
||
var BARE_OS_DISCORD_FM_PAGE = 20
|
||
var BARE_OS_DISCORD_FM_TTL_MS = BARE_OS_DISCORD_IDLE_MS
|
||
var BARE_OS_DISCORD_FM_SESSIONS = Object.create(null)
|
||
var BARE_OS_DISCORD_LIVE = Object.create(null)
|
||
var BARE_OS_DISCORD_IDLE_SEQ = 0
|
||
var BARE_OS_DISCORD_EDIT_SUGGEST = [
|
||
'~/notes.txt',
|
||
'~/.barerc',
|
||
'~/TODO.md',
|
||
'/tmp/scratch.txt'
|
||
]
|
||
var BARE_OS_DISCORD_FS_SUGGEST = [
|
||
'~',
|
||
'/etc/os-release',
|
||
'/etc/motd',
|
||
'/proc/uptime',
|
||
'/proc/meminfo',
|
||
'/proc/bare_os/features',
|
||
'/proc/bare_os/security_posture.json',
|
||
'/proc/bare_os/process_table.json',
|
||
'/proc/bare_os/net_summary.json',
|
||
'/var/log/bare-os/discord.log',
|
||
'/var/log/bare-os/kernel-console.log',
|
||
'/share/man/man.json',
|
||
'/tmp'
|
||
]
|
||
|
||
/**
|
||
* bare-process has no process.emitWarning. discord.js 14 calls it whenever
|
||
* reply options include the deprecated `ephemeral` key (even when false).
|
||
*/
|
||
function discordInstallProcessEmitWarning(proc) {
|
||
const p =
|
||
proc ||
|
||
(typeof globalThis.process !== 'undefined' ? globalThis.process : null)
|
||
if (!p || typeof p.emitWarning === 'function') return p
|
||
p.emitWarning = function emitWarning(warning, type, code) {
|
||
let name = 'Warning'
|
||
let id = ''
|
||
let msg = ''
|
||
if (warning && typeof warning === 'object' && warning.type && !(warning instanceof Error)) {
|
||
name = String(warning.type || 'Warning')
|
||
id = String(warning.code || '')
|
||
msg = String(warning.message || warning)
|
||
} else if (type && typeof type === 'object') {
|
||
name = String(type.type || 'Warning')
|
||
id = String(type.code || '')
|
||
msg = warning instanceof Error ? warning.message : String(warning)
|
||
} else {
|
||
name = typeof type === 'string' ? type : 'Warning'
|
||
id = typeof code === 'string' ? code : ''
|
||
msg = warning instanceof Error ? warning.message : String(warning)
|
||
}
|
||
const line = id ? name + ' [' + id + ']: ' + msg : name + ': ' + msg
|
||
try {
|
||
if (typeof p.emit === 'function') {
|
||
const err = warning instanceof Error ? warning : new Error(msg)
|
||
err.name = name
|
||
if (id) err.code = id
|
||
p.emit('warning', err)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
try {
|
||
if (typeof console !== 'undefined' && typeof console.error === 'function') {
|
||
console.error(line)
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
return p
|
||
}
|
||
|
||
discordInstallProcessEmitWarning()
|
||
|
||
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 discordClipSmart(s, n, '\n…(truncated)').text
|
||
}
|
||
|
||
function discordClipSmart(text, max, suffix) {
|
||
const s = String(text == null ? '' : text)
|
||
const tail = suffix == null ? '\n…' : suffix
|
||
if (s.length <= max) return { text: s, truncated: false, hidden: 0 }
|
||
const room = Math.max(8, max - tail.length)
|
||
let cut = s.lastIndexOf('\n', room)
|
||
if (cut < room * 0.55) {
|
||
const sp = s.lastIndexOf(' ', room)
|
||
if (sp > room * 0.55) cut = sp
|
||
}
|
||
if (cut < Math.floor(room * 0.4)) cut = room
|
||
return { text: s.slice(0, cut) + tail, truncated: true, hidden: s.length - cut }
|
||
}
|
||
|
||
function discordSafeDesc(desc, max) {
|
||
const s = String(desc == null ? '' : desc)
|
||
if (max <= 0) return ''
|
||
if (s.length <= max) return s
|
||
const open = s.indexOf('```')
|
||
if (open >= 0) {
|
||
const nl = s.indexOf('\n', open)
|
||
const close = s.lastIndexOf('```')
|
||
if (nl > open && close > nl) {
|
||
const head = s.slice(0, nl + 1)
|
||
const after = s.slice(close + 3)
|
||
const hint = '\n…'
|
||
const budget = max - head.length - 4 - after.length - hint.length
|
||
if (budget >= 16) {
|
||
const body = s.slice(nl + 1, close).replace(/\n+$/, '')
|
||
const fit = discordClipSmart(body, budget, hint)
|
||
return head + fit.text.replace(/```/g, '`ˋ`') + '\n```' + after
|
||
}
|
||
}
|
||
}
|
||
return discordClipSmart(s, max, '\n…').text
|
||
}
|
||
|
||
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, max) {
|
||
const tag = lang ? String(lang) : ''
|
||
const wrap = 8 + tag.length
|
||
const cap = Math.max(32, (max || BARE_OS_DISCORD_LIMIT.desc) - wrap)
|
||
const fit = discordClipSmart(discordCmdRedact(text).replace(/```/g, '`ˋ`'), cap, '\n…')
|
||
return '```' + tag + '\n' + fit.text + '\n```'
|
||
}
|
||
|
||
function discordTextPages(text, pageChars) {
|
||
const s = String(text == null ? '' : text)
|
||
const size = Math.max(200, pageChars || BARE_OS_DISCORD_LIMIT.pageChars)
|
||
if (!s) return ['']
|
||
if (s.length <= size) return [s]
|
||
const pages = []
|
||
let i = 0
|
||
while (i < s.length) {
|
||
if (s.length - i <= size) {
|
||
pages.push(s.slice(i))
|
||
break
|
||
}
|
||
let cut = s.lastIndexOf('\n', i + size)
|
||
if (cut <= i + Math.floor(size * 0.4)) cut = i + size
|
||
pages.push(s.slice(i, cut))
|
||
i = cut
|
||
while (s.charAt(i) === '\n') i++
|
||
}
|
||
return pages
|
||
}
|
||
|
||
function discordEmbedSize(e) {
|
||
if (!e) return 0
|
||
let n = 0
|
||
if (e.title) n += String(e.title).length
|
||
if (e.description) n += String(e.description).length
|
||
if (e.footer && e.footer.text) n += String(e.footer.text).length
|
||
if (e.author && e.author.name) n += String(e.author.name).length
|
||
const fs = e.fields || []
|
||
for (let i = 0; i < fs.length; i++) {
|
||
n += String(fs[i].name || '').length + String(fs[i].value || '').length
|
||
}
|
||
return n
|
||
}
|
||
|
||
function discordPackEmbed(opts) {
|
||
const o = opts || {}
|
||
const L = BARE_OS_DISCORD_LIMIT
|
||
const title = o.title ? String(o.title).slice(0, L.title) : ''
|
||
const footer = String(o.footer || 'Bare OS · expires after 2m idle').slice(0, L.footer)
|
||
const authorName = o.author && o.author.name ? String(o.author.name).slice(0, L.author) : ''
|
||
const chrome = title.length + footer.length + authorName.length
|
||
const preferDesc = o.prefer === 'desc'
|
||
const rawFields = []
|
||
if (o.fields && o.fields.length) {
|
||
for (let i = 0; i < o.fields.length && rawFields.length < L.fields; i++) {
|
||
const f = o.fields[i]
|
||
if (!f) continue
|
||
let v = String(f.value == null ? '' : f.value).trim()
|
||
if (!v || v === '—') continue
|
||
v = discordCmdRedact(v)
|
||
if (v.length > L.fieldValue) v = discordClipSmart(v, L.fieldValue, '\n…').text
|
||
rawFields.push({
|
||
name: String(f.name || '·').slice(0, L.fieldName),
|
||
value: v,
|
||
inline: Boolean(f.inline)
|
||
})
|
||
}
|
||
}
|
||
let fieldBytes = 0
|
||
for (let i = 0; i < rawFields.length; i++) {
|
||
fieldBytes += rawFields[i].name.length + rawFields[i].value.length
|
||
}
|
||
let descRoom
|
||
if (preferDesc) {
|
||
descRoom = Math.min(L.desc, Math.max(0, L.embedTotal - chrome - Math.min(fieldBytes, 900)))
|
||
} else {
|
||
descRoom = Math.min(L.desc, Math.max(0, L.embedTotal - chrome - fieldBytes))
|
||
}
|
||
let desc = o.desc ? discordCmdRedact(String(o.desc)) : ''
|
||
if (desc) desc = discordSafeDesc(desc, descRoom)
|
||
let fields = rawFields.slice()
|
||
let used = chrome + desc.length + fieldBytes
|
||
if (used > L.embedTotal && fields.length) {
|
||
const keep = []
|
||
let acc = chrome + desc.length
|
||
for (let i = 0; i < fields.length; i++) {
|
||
const add = fields[i].name.length + fields[i].value.length
|
||
if (acc + add > L.embedTotal - 48) break
|
||
keep.push(fields[i])
|
||
acc += add
|
||
}
|
||
const omitted = fields.length - keep.length
|
||
if (omitted > 0) {
|
||
const note = '_' + omitted + ' more not shown_'
|
||
if (keep.length && acc + note.length + 8 <= L.embedTotal) {
|
||
keep.push({ name: '…', value: note, inline: false })
|
||
} else if (desc.length + 2 + note.length <= L.desc) {
|
||
desc = (desc ? desc + '\n' : '') + note
|
||
}
|
||
}
|
||
fields = keep
|
||
used = chrome + desc.length
|
||
for (let i = 0; i < fields.length; i++) used += fields[i].name.length + fields[i].value.length
|
||
}
|
||
if (used > L.embedTotal && desc) {
|
||
desc = discordSafeDesc(desc, Math.max(0, desc.length - (used - L.embedTotal)))
|
||
}
|
||
const embed = {
|
||
color: o.color == null ? BARE_OS_DISCORD_COLOR : o.color,
|
||
timestamp: new Date().toISOString(),
|
||
footer: { text: footer }
|
||
}
|
||
if (title) embed.title = title
|
||
if (desc) embed.description = desc
|
||
if (fields.length) embed.fields = fields
|
||
if (o.author) embed.author = { name: authorName || 'Bare OS' }
|
||
return {
|
||
embed: embed,
|
||
truncated: fields.length < rawFields.length || !!(o.desc && desc.length < String(o.desc).length),
|
||
size: discordEmbedSize(embed)
|
||
}
|
||
}
|
||
|
||
function discordTryJson(raw) {
|
||
if (raw && typeof raw === 'object') return raw
|
||
const s = String(raw || '').trim()
|
||
if (!s || (s.charAt(0) !== '{' && s.charAt(0) !== '[')) return null
|
||
try {
|
||
return JSON.parse(s)
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
function discordPrettyBytes(n) {
|
||
const x = Number(n)
|
||
if (!Number.isFinite(x) || x < 0) return String(n)
|
||
if (x < 1024) return String(Math.round(x)) + ' B'
|
||
const units = ['KiB', 'MiB', 'GiB', 'TiB']
|
||
let v = x / 1024
|
||
let i = 0
|
||
while (v >= 1024 && i < units.length - 1) {
|
||
v /= 1024
|
||
i++
|
||
}
|
||
return (v >= 10 ? v.toFixed(0) : v.toFixed(1)) + ' ' + units[i]
|
||
}
|
||
|
||
function discordIsByteishKey(k) {
|
||
return /byte|bytes|mem|rss|size|freemem|totalmem|avail|queued|f_bsize|f_frsize/i.test(
|
||
String(k || '')
|
||
)
|
||
}
|
||
|
||
function discordPrettyValue(key, val) {
|
||
if (val == null || val === '') return '—'
|
||
if (typeof val === 'boolean') return val ? 'yes' : 'no'
|
||
if (typeof val === 'number') {
|
||
if (discordIsByteishKey(key) && val >= 1024) return discordPrettyBytes(val)
|
||
if (key === 'atMs' || /AtMs$/.test(key)) {
|
||
if (val > 1e12) return new Date(val).toISOString()
|
||
return String(val)
|
||
}
|
||
if (/Ms$/.test(key) && val >= 1000) {
|
||
const s = Math.round(val / 1000)
|
||
if (s < 60) return s + 's'
|
||
return Math.floor(s / 60) + 'm ' + (s % 60) + 's'
|
||
}
|
||
return String(val)
|
||
}
|
||
if (typeof val === 'string') {
|
||
const t = discordCmdRedact(val)
|
||
if (/^[0-9a-f]{24,}$/i.test(t)) return '`' + t.slice(0, 16) + (t.length > 16 ? '…' : '') + '`'
|
||
return t.length > 180 ? t.slice(0, 177) + '…' : t
|
||
}
|
||
if (Array.isArray(val)) {
|
||
if (!val.length) return '(none)'
|
||
if (val.every(function (x) { return x == null || typeof x !== 'object' })) {
|
||
const shown = val.slice(0, 10).map(function (x) {
|
||
return String(x)
|
||
})
|
||
return shown.join(', ') + (val.length > 10 ? ' +' + (val.length - 10) : '')
|
||
}
|
||
return String(val.length) + ' items'
|
||
}
|
||
if (typeof val === 'object') return Object.keys(val).length + ' keys'
|
||
return String(val)
|
||
}
|
||
|
||
function discordSkipPrettyKey(k) {
|
||
return /TOKEN|SECRET|PASSWORD|PASSWD|PRIVATE|CREDENTIAL|API_KEY/i.test(k) ||
|
||
k === 'note' ||
|
||
k === 'schema' ||
|
||
k === 'schemaVersion'
|
||
}
|
||
|
||
function discordValueEmpty(val) {
|
||
if (val == null) return true
|
||
if (val === '') return true
|
||
if (val === '—') return true
|
||
if (Array.isArray(val) && !val.length) return true
|
||
if (typeof val === 'object' && !Array.isArray(val) && !Object.keys(val).length) return true
|
||
return false
|
||
}
|
||
|
||
function discordHumanLabel(key) {
|
||
const known = {
|
||
schemaVersion: 'Schema',
|
||
topicHex: 'Topic',
|
||
peerCount: 'Peers',
|
||
seedHandshakeError: 'Handshake',
|
||
seedRole: 'Role',
|
||
replicationQueueDepth: 'Queue depth',
|
||
replicationQueue: 'Replication',
|
||
stagingSlot: 'Staging',
|
||
snapshotHints: 'Snapshot',
|
||
peerFirewallStats: 'Firewall',
|
||
peerFirewallAcceptedTotal: 'Accepted',
|
||
peerFirewallRejectedTotal: 'Rejected',
|
||
peerFirewallInboundTotal: 'Inbound',
|
||
peerFirewallOutboundTotal: 'Outbound',
|
||
peerFirewallE2e: 'Firewall e2e',
|
||
manifestPathCount: 'Manifests',
|
||
localRamBlockCount: 'RAM blocks',
|
||
hypercoreLengthHint: 'Core length',
|
||
queueDepthEstimate: 'Queue estimate',
|
||
snapshotWorkflowNote: 'Note',
|
||
activeSlot: 'Active slot',
|
||
pendingSlot: 'Pending',
|
||
previousSlot: 'Previous',
|
||
canarySlot: 'Canary',
|
||
drainDeadlineMs: 'Drain',
|
||
atMs: 'Updated'
|
||
}
|
||
if (known[key]) return known[key]
|
||
const s = String(key || '')
|
||
.replace(/[_-]+/g, ' ')
|
||
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
||
.replace(/\s+/g, ' ')
|
||
.trim()
|
||
if (!s) return String(key || '')
|
||
return s.charAt(0).toUpperCase() + s.slice(1)
|
||
}
|
||
|
||
function discordCollectFields(obj, prefix, out, depth) {
|
||
if (!obj || typeof obj !== 'object' || Array.isArray(obj) || depth > 2) return
|
||
const keys = Object.keys(obj)
|
||
for (let i = 0; i < keys.length && out.length < 18; i++) {
|
||
const k = keys[i]
|
||
if (discordSkipPrettyKey(k)) continue
|
||
const val = obj[k]
|
||
if (discordValueEmpty(val)) continue
|
||
const label = prefix ? prefix + ' · ' + discordHumanLabel(k) : discordHumanLabel(k)
|
||
if (val && typeof val === 'object' && !Array.isArray(val) && depth < 2) {
|
||
const lines = []
|
||
const sub = Object.keys(val)
|
||
for (let j = 0; j < sub.length && lines.length < 8; j++) {
|
||
if (discordSkipPrettyKey(sub[j])) continue
|
||
const sv = val[sub[j]]
|
||
if (discordValueEmpty(sv)) continue
|
||
if (sv && typeof sv === 'object') continue
|
||
const pretty = discordPrettyValue(sub[j], sv)
|
||
if (pretty === '—' || pretty === '(none)') continue
|
||
lines.push('**' + discordHumanLabel(sub[j]) + '** ' + pretty)
|
||
}
|
||
if (lines.length) out.push(discordField(discordHumanLabel(k), lines.join('\n'), false))
|
||
} else {
|
||
const pretty = discordPrettyValue(k, val)
|
||
if (pretty === '—' || pretty === '(none)') continue
|
||
out.push(discordField(label, pretty, true))
|
||
}
|
||
}
|
||
}
|
||
|
||
function discordPrettyUptime(raw) {
|
||
const s = String(raw || '').trim()
|
||
const sec = Number(s.split(/\s+/)[0])
|
||
if (!Number.isFinite(sec) || sec < 0) return s || '—'
|
||
const d = Math.floor(sec / 86400)
|
||
const h = Math.floor((sec % 86400) / 3600)
|
||
const m = Math.floor((sec % 3600) / 60)
|
||
const parts = []
|
||
if (d) parts.push(d + 'd')
|
||
if (h || d) parts.push(h + 'h')
|
||
parts.push(m + 'm')
|
||
return parts.join(' ') + ' (' + Math.round(sec) + 's)'
|
||
}
|
||
|
||
function discordParseMeminfo(text) {
|
||
const out = {}
|
||
const lines = String(text || '').split(/\r?\n/)
|
||
for (let i = 0; i < lines.length; i++) {
|
||
const m = /^([A-Za-z0-9_()]+):\s+(\d+)/.exec(lines[i])
|
||
if (!m) continue
|
||
out[m[1]] = Number(m[2]) * 1024
|
||
}
|
||
return out
|
||
}
|
||
|
||
function discordParseSystemctlList(text) {
|
||
const rows = []
|
||
const lines = String(text || '').split(/\r?\n/)
|
||
for (let i = 0; i < lines.length; i++) {
|
||
const line = lines[i].trim()
|
||
if (!line || /^UNIT\b/.test(line) || line.indexOf('LOAD') === 0) continue
|
||
const parts = line.split(/\s+/)
|
||
if (parts.length < 4) continue
|
||
rows.push({
|
||
unit: parts[0].replace(/\.service$/, ''),
|
||
load: parts[1] || '',
|
||
preset: parts[2] || '',
|
||
active: parts[3] || '',
|
||
sub: parts[4] || '',
|
||
desc: parts.slice(5).join(' ')
|
||
})
|
||
}
|
||
return rows
|
||
}
|
||
|
||
function discordPrettySnapshot(title, raw, extras) {
|
||
const parsed = discordTryJson(raw)
|
||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||
const text = raw == null ? '' : String(raw)
|
||
if (!text) {
|
||
return discordResult(discordEmbed({ title: title, desc: 'unavailable' }), extras)
|
||
}
|
||
return {
|
||
more: {
|
||
title: title,
|
||
body: text,
|
||
fence: true,
|
||
components: extras && extras.components,
|
||
editPath: extras && extras.editPath,
|
||
created: extras && extras.created,
|
||
color: extras && extras.color
|
||
}
|
||
}
|
||
}
|
||
const fields = []
|
||
discordCollectFields(parsed, '', fields, 0)
|
||
const note = typeof parsed.note === 'string' ? parsed.note.slice(0, 280) : ''
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: title,
|
||
desc: note,
|
||
fields: fields.slice(0, 18),
|
||
color: extras && extras.color
|
||
}),
|
||
extras
|
||
)
|
||
}
|
||
|
||
function discordHex12(ctx, u8) {
|
||
if (!u8 || typeof u8.length !== 'number') return ''
|
||
if (ctx && ctx.b4a && typeof ctx.b4a.toString === 'function') {
|
||
try {
|
||
return String(ctx.b4a.toString(u8, 'hex') || '').slice(0, 12)
|
||
} catch {
|
||
/* fall through */
|
||
}
|
||
}
|
||
let hex = ''
|
||
const n = Math.min(u8.length, 6)
|
||
for (let i = 0; i < n; i++) {
|
||
const h = (u8[i] & 0xff).toString(16)
|
||
hex += h.length < 2 ? '0' + h : h
|
||
}
|
||
return hex.slice(0, 12)
|
||
}
|
||
|
||
/** Prefer live VFS env (identity mutates this). Keep ctx.env in sync. */
|
||
function discordSyncSessionIdentity(ctx) {
|
||
if (!ctx) return
|
||
const vfsEnv = ctx.vfs && ctx.vfs.env && typeof ctx.vfs.env === 'object' ? ctx.vfs.env : null
|
||
const env = vfsEnv || (ctx.env && typeof ctx.env === 'object' ? ctx.env : null)
|
||
if (!env) return
|
||
if (ctx.env && vfsEnv && ctx.env !== vfsEnv) {
|
||
const keys = [
|
||
'USER',
|
||
'LOGNAME',
|
||
'HOME',
|
||
'PWD',
|
||
'UID',
|
||
'GID',
|
||
'GROUP',
|
||
'SHELL',
|
||
'BARE_OS_IDENTITY',
|
||
'BARE_OS_PUBLIC_KEY'
|
||
]
|
||
for (let i = 0; i < keys.length; i++) {
|
||
const k = keys[i]
|
||
if (vfsEnv[k] != null && String(vfsEnv[k]) !== '') ctx.env[k] = vfsEnv[k]
|
||
}
|
||
}
|
||
const ident = ctx.identity
|
||
if (!ident || ident.state !== 'unlocked') return
|
||
let name = String(env.USER || env.LOGNAME || '').trim()
|
||
if (!name || name === 'guest') {
|
||
name = discordHex12(ctx, ident.publicKey)
|
||
if (!name) return
|
||
env.USER = name
|
||
env.LOGNAME = name
|
||
env.GROUP = name
|
||
if (ctx.env && ctx.env !== env) {
|
||
ctx.env.USER = name
|
||
ctx.env.LOGNAME = name
|
||
}
|
||
}
|
||
if (!env.HOME || env.HOME === '/home/guest') env.HOME = '/home/' + name
|
||
if (!env.PWD || env.PWD === '/home/guest') env.PWD = env.HOME
|
||
env.BARE_OS_IDENTITY = 'unlocked'
|
||
}
|
||
|
||
function discordCmdEnv(ctx) {
|
||
discordSyncSessionIdentity(ctx)
|
||
if (ctx && ctx.vfs && ctx.vfs.env) return ctx.vfs.env
|
||
if (ctx && ctx.env) return ctx.env
|
||
return {}
|
||
}
|
||
|
||
function discordSessionUser(ctx) {
|
||
const e = discordCmdEnv(ctx)
|
||
const u = String(e.USER || e.LOGNAME || e.USERNAME || '').trim()
|
||
return u || 'guest'
|
||
}
|
||
|
||
function discordUniqueComponents(rows) {
|
||
const seen = Object.create(null)
|
||
const out = []
|
||
if (!Array.isArray(rows)) return out
|
||
for (let i = 0; i < rows.length && out.length < 5; i++) {
|
||
const row = rows[i]
|
||
if (!row || !Array.isArray(row.components)) continue
|
||
const comps = []
|
||
for (let j = 0; j < row.components.length; j++) {
|
||
const c = row.components[j]
|
||
if (!c) continue
|
||
const id = c.custom_id != null ? String(c.custom_id) : ''
|
||
if (id) {
|
||
if (seen[id]) continue
|
||
seen[id] = 1
|
||
}
|
||
comps.push(c)
|
||
}
|
||
if (!comps.length) continue
|
||
out.push({
|
||
type: row.type || 1,
|
||
components: comps
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
function discordCmdReplyPayload(result) {
|
||
const payload = {}
|
||
if (result && result.embeds && result.embeds.length) payload.embeds = result.embeds
|
||
if (result && result.components && result.components.length) {
|
||
payload.components = discordUniqueComponents(result.components)
|
||
if (!payload.components.length) delete payload.components
|
||
}
|
||
if (result && result.text) payload.content = discordCmdClip(result.text)
|
||
if (!payload.content && !payload.embeds) payload.content = '(no output)'
|
||
if (result && result.ephemeral) payload.flags = BARE_OS_DISCORD_FLAG_EPHEMERAL
|
||
delete payload.fetchReply
|
||
delete payload.ephemeral
|
||
return payload
|
||
}
|
||
|
||
function discordEmbed(opts) {
|
||
return discordPackEmbed(opts).embed
|
||
}
|
||
|
||
function discordField(name, value, inline) {
|
||
let v = value == null || value === '' ? '—' : String(value)
|
||
v = discordCmdRedact(v)
|
||
if (v.length > BARE_OS_DISCORD_LIMIT.fieldValue) {
|
||
v = discordClipSmart(v, BARE_OS_DISCORD_LIMIT.fieldValue, '\n…').text
|
||
}
|
||
if (!v) v = '—'
|
||
return {
|
||
name: String(name).slice(0, BARE_OS_DISCORD_LIMIT.fieldName),
|
||
value: v,
|
||
inline: Boolean(inline)
|
||
}
|
||
}
|
||
|
||
function discordMoreKey(interaction) {
|
||
return discordInteractionUserId(interaction) || 'anon'
|
||
}
|
||
|
||
function discordMoreGc() {
|
||
const now = Date.now()
|
||
for (const k in BARE_OS_DISCORD_MORE_SESSIONS) {
|
||
const r = BARE_OS_DISCORD_MORE_SESSIONS[k]
|
||
if (!r || now - r.atMs > BARE_OS_DISCORD_IDLE_MS) delete BARE_OS_DISCORD_MORE_SESSIONS[k]
|
||
}
|
||
}
|
||
|
||
function discordMorePut(interaction, rec) {
|
||
discordMoreGc()
|
||
rec.atMs = Date.now()
|
||
BARE_OS_DISCORD_MORE_SESSIONS[discordMoreKey(interaction)] = rec
|
||
}
|
||
|
||
function discordMoreGet(interaction) {
|
||
discordMoreGc()
|
||
return BARE_OS_DISCORD_MORE_SESSIONS[discordMoreKey(interaction)] || null
|
||
}
|
||
|
||
function discordMoreResult(interaction) {
|
||
const rec = discordMoreGet(interaction)
|
||
if (!rec) return { text: 'This view expired after 2 minutes idle.', ephemeral: true }
|
||
const n = rec.pages.length
|
||
if (rec.page < 0) rec.page = n - 1
|
||
if (rec.page >= n) rec.page = 0
|
||
rec.atMs = Date.now()
|
||
const chunk = rec.pages[rec.page] || ''
|
||
const inner = rec.fence === false ? chunk : discordCmdFence(chunk, rec.lang, 3600)
|
||
const navline =
|
||
n > 1
|
||
? '\nPage **' + (rec.page + 1) + '/' + n + '** · ' + rec.total + ' characters'
|
||
: ''
|
||
const btns =
|
||
n > 1
|
||
? discordButtons([
|
||
{ id: 'more:prev', label: '← Prev' },
|
||
{ id: 'more:next', label: 'Next →', style: 1 }
|
||
])
|
||
: null
|
||
const extra = (rec.extra || []).concat(btns ? [btns] : [])
|
||
const out = discordResult(
|
||
discordEmbed({
|
||
title: rec.title,
|
||
desc: (rec.lead || '') + inner + navline,
|
||
color: rec.color,
|
||
footer: rec.footer,
|
||
prefer: 'desc'
|
||
}),
|
||
{ components: extra }
|
||
)
|
||
return out
|
||
}
|
||
|
||
function discordLongTextResult(interaction, opts) {
|
||
const o = opts || {}
|
||
const body = String(o.body == null ? '' : o.body)
|
||
const pages = discordTextPages(body, o.pageChars || BARE_OS_DISCORD_LIMIT.pageChars)
|
||
const extraComps = o.components || []
|
||
if (pages.length > 1) {
|
||
let lead = o.lead ? String(o.lead) : ''
|
||
if (lead && !/\n$/.test(lead)) lead += '\n'
|
||
discordMorePut(interaction, {
|
||
title: o.title,
|
||
pages: pages,
|
||
page: 0,
|
||
fence: o.fence !== false,
|
||
lang: o.lang || '',
|
||
lead: lead,
|
||
color: o.color,
|
||
footer: o.footer,
|
||
extra: extraComps,
|
||
total: body.length
|
||
})
|
||
const view = discordMoreResult(interaction)
|
||
if (o.editPath) view.editPath = o.editPath
|
||
if (o.created) view.created = true
|
||
return view
|
||
}
|
||
const inner = o.fence === false ? body : discordCmdFence(body, o.lang, 3800)
|
||
let lead = o.lead ? String(o.lead) : ''
|
||
if (lead && inner && !/\n$/.test(lead)) lead += '\n'
|
||
const out = discordResult(
|
||
discordEmbed({
|
||
title: o.title,
|
||
desc: lead + inner,
|
||
color: o.color,
|
||
footer: o.footer,
|
||
prefer: 'desc'
|
||
}),
|
||
{ components: extraComps, editPath: o.editPath, created: o.created }
|
||
)
|
||
return out
|
||
}
|
||
|
||
function discordButtons(items) {
|
||
const row = { type: 1, components: [] }
|
||
for (let i = 0; i < items.length && row.components.length < 5; i++) {
|
||
const it = items[i]
|
||
if (!it || !it.id) continue
|
||
row.components.push({
|
||
type: 2,
|
||
style: it.style || 2,
|
||
label: String(it.label || it.id).slice(0, 80),
|
||
custom_id: String(it.id).slice(0, 100)
|
||
})
|
||
}
|
||
return row.components.length ? row : null
|
||
}
|
||
|
||
function discordSelect(id, placeholder, options) {
|
||
const opts = []
|
||
for (let i = 0; i < options.length && opts.length < 25; i++) {
|
||
const o = options[i]
|
||
if (!o) continue
|
||
const value = String(o.value || o.label || '').slice(0, 100)
|
||
if (!value) continue
|
||
const item = {
|
||
label: String(o.label || value).slice(0, 100),
|
||
value: value
|
||
}
|
||
if (o.description) item.description = String(o.description).slice(0, 100)
|
||
opts.push(item)
|
||
}
|
||
if (!opts.length) return null
|
||
return {
|
||
type: 1,
|
||
components: [
|
||
{
|
||
type: 3,
|
||
custom_id: String(id).slice(0, 100),
|
||
placeholder: String(placeholder || 'Choose…').slice(0, 150),
|
||
min_values: 1,
|
||
max_values: 1,
|
||
options: opts
|
||
}
|
||
]
|
||
}
|
||
}
|
||
|
||
function discordNavRows() {
|
||
const rows = []
|
||
const a = discordButtons([
|
||
{ id: 'nav:panel', label: 'Panel', style: 1 },
|
||
{ id: 'bare:status', label: 'Status', style: 1 },
|
||
{ id: 'bare:whoami', label: 'Whoami' },
|
||
{ id: 'bare:help', label: 'Help' },
|
||
{ id: 'sys:doctor', label: 'Doctor' }
|
||
])
|
||
const b = discordButtons([
|
||
{ id: 'svc:list', label: 'Services' },
|
||
{ id: 'sys:ps', label: 'Processes' },
|
||
{ id: 'net:summary', label: 'Network' },
|
||
{ id: 'say:compose', label: 'Say…', style: 3 },
|
||
{ id: 'run:compose', label: 'Shell' }
|
||
])
|
||
const c = discordButtons([
|
||
{ id: 'edit:new', label: 'Edit file…', style: 1 },
|
||
{ id: 'create:new', label: 'Create file…', style: 3 },
|
||
{ id: 'fm:home', label: 'Files', style: 1 },
|
||
{ id: 'set:home', label: 'Settings' }
|
||
])
|
||
if (a) rows.push(a)
|
||
if (b) rows.push(b)
|
||
if (c) rows.push(c)
|
||
return rows
|
||
}
|
||
|
||
function discordResult(embed, extra) {
|
||
const out = { embeds: [embed] }
|
||
const extraRows = extra && extra.components ? extra.components : []
|
||
const nav = extra && extra.nav === false ? [] : discordNavRows()
|
||
const rows = discordUniqueComponents(extraRows.concat(nav))
|
||
if (rows.length) out.components = rows
|
||
if (extra && extra.ephemeral) out.ephemeral = true
|
||
if (extra && extra.text) out.text = extra.text
|
||
if (extra && extra.editPath) out.editPath = extra.editPath
|
||
if (extra && extra.created) out.created = true
|
||
return out
|
||
}
|
||
|
||
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 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
|
||
}
|
||
|
||
function discordCmdPathWriteOk(ctx, raw) {
|
||
const p = discordCmdPathOk(raw)
|
||
if (!p) return null
|
||
if (p === '~/.discord/.env' || p === '~/.discord.env') return null
|
||
if (p.indexOf('~/.discord') === 0) return null
|
||
const user = discordSessionUser(ctx)
|
||
const home = '/home/' + user
|
||
const allow =
|
||
p === '~' ||
|
||
p.indexOf('~/') === 0 ||
|
||
p.indexOf('/tmp/') === 0 ||
|
||
p === '/tmp' ||
|
||
p === home ||
|
||
p.indexOf(home + '/') === 0
|
||
return allow ? p : null
|
||
}
|
||
|
||
async function discordFileExists(ctx, p) {
|
||
if (!ctx || !ctx.vfs) return false
|
||
const stfn = ctx.vfs.lstat || ctx.vfs.stat
|
||
if (typeof stfn === 'function') {
|
||
try {
|
||
const st = await stfn.call(ctx.vfs, p)
|
||
return Boolean(st)
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
if (typeof ctx.vfs.readFile !== 'function') return false
|
||
try {
|
||
await ctx.vfs.readFile(p)
|
||
return true
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
|
||
function discordLooksBinary(text) {
|
||
const s = String(text || '')
|
||
if (s.indexOf('\0') >= 0) return true
|
||
let bad = 0
|
||
const n = Math.min(s.length, 800)
|
||
for (let i = 0; i < n; i++) {
|
||
const c = s.charCodeAt(i)
|
||
if (c < 9 || (c > 13 && c < 32)) bad++
|
||
}
|
||
return bad > 8
|
||
}
|
||
|
||
function discordTextToBuf(ctx, text) {
|
||
const s = String(text == null ? '' : text)
|
||
if (ctx && ctx.b4a && typeof ctx.b4a.from === 'function') return ctx.b4a.from(s)
|
||
if (typeof Buffer !== 'undefined') return Buffer.from(s, 'utf8')
|
||
const out = new Uint8Array(s.length)
|
||
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i) & 0xff
|
||
return out
|
||
}
|
||
|
||
function discordEditGc() {
|
||
const now = Date.now()
|
||
for (const k in BARE_OS_DISCORD_EDIT_SESSIONS) {
|
||
const rec = BARE_OS_DISCORD_EDIT_SESSIONS[k]
|
||
if (!rec || now - rec.atMs > BARE_OS_DISCORD_EDIT_TTL_MS) {
|
||
delete BARE_OS_DISCORD_EDIT_SESSIONS[k]
|
||
}
|
||
}
|
||
}
|
||
|
||
function discordEditKey(interaction) {
|
||
return discordInteractionUserId(interaction) || 'anon'
|
||
}
|
||
|
||
function discordEditPut(interaction, rec) {
|
||
discordEditGc()
|
||
BARE_OS_DISCORD_EDIT_SESSIONS[discordEditKey(interaction)] = {
|
||
path: rec.path,
|
||
origLen: rec.origLen || 0,
|
||
truncated: !!rec.truncated,
|
||
created: !!rec.created,
|
||
atMs: Date.now()
|
||
}
|
||
}
|
||
|
||
function discordEditGet(interaction) {
|
||
discordEditGc()
|
||
const rec = BARE_OS_DISCORD_EDIT_SESSIONS[discordEditKey(interaction)]
|
||
return rec || null
|
||
}
|
||
|
||
function discordEditClear(interaction) {
|
||
delete BARE_OS_DISCORD_EDIT_SESSIONS[discordEditKey(interaction)]
|
||
}
|
||
|
||
function discordEditChunks(text) {
|
||
const s = String(text == null ? '' : text)
|
||
const cap = BARE_OS_DISCORD_EDIT_CHUNK * BARE_OS_DISCORD_EDIT_MAX_CHUNKS
|
||
const truncated = s.length > cap
|
||
const body = truncated ? s.slice(0, cap) : s
|
||
const chunks = []
|
||
if (!body.length) chunks.push('')
|
||
else {
|
||
for (let i = 0; i < body.length; i += BARE_OS_DISCORD_EDIT_CHUNK) {
|
||
chunks.push(body.slice(i, i + BARE_OS_DISCORD_EDIT_CHUNK))
|
||
}
|
||
}
|
||
return {
|
||
chunks: chunks.slice(0, BARE_OS_DISCORD_EDIT_MAX_CHUNKS),
|
||
truncated: truncated,
|
||
origLen: s.length
|
||
}
|
||
}
|
||
|
||
function discordEditModalPayload(path, chunks) {
|
||
const base = String(path || 'file').split('/').pop() || String(path)
|
||
const n = Math.max(1, chunks.length)
|
||
const components = []
|
||
for (let i = 0; i < n; i++) {
|
||
const label = n === 1 ? 'Contents (save closes the form)' : 'Part ' + (i + 1) + ' / ' + n
|
||
const field = {
|
||
type: 4,
|
||
custom_id: 'c' + i,
|
||
label: label.slice(0, 45),
|
||
style: 2,
|
||
required: false,
|
||
max_length: BARE_OS_DISCORD_EDIT_CHUNK
|
||
}
|
||
const v = String(chunks[i] || '').slice(0, BARE_OS_DISCORD_EDIT_CHUNK)
|
||
if (v) field.value = v
|
||
components.push({
|
||
type: 1,
|
||
components: [field]
|
||
})
|
||
}
|
||
return {
|
||
custom_id: 'edit:save',
|
||
title: ('Edit ' + base).slice(0, 45),
|
||
components: components
|
||
}
|
||
}
|
||
|
||
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 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 || ''
|
||
}
|
||
|
||
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 ''
|
||
}
|
||
|
||
function discordFilterChoices(items, q, toChoice) {
|
||
const needle = String(q || '').toLowerCase()
|
||
const out = []
|
||
for (let i = 0; i < items.length && out.length < 25; i++) {
|
||
const raw = items[i]
|
||
const choice = toChoice ? toChoice(raw) : { name: String(raw), value: String(raw) }
|
||
if (!choice || !choice.value) continue
|
||
const hay = (choice.name + ' ' + choice.value).toLowerCase()
|
||
if (needle && hay.indexOf(needle) < 0) continue
|
||
out.push({ name: String(choice.name).slice(0, 100), value: String(choice.value).slice(0, 100) })
|
||
}
|
||
return out
|
||
}
|
||
|
||
async function discordSuggestUnits(ctx, q) {
|
||
const names = BARE_OS_DISCORD_KNOWN_UNITS.slice()
|
||
if (typeof ctx.bareOsRunSystemctlCli === 'function') {
|
||
const out = await discordCmdCapture(ctx, function () {
|
||
return ctx.bareOsRunSystemctlCli(['systemctl', 'list'])
|
||
})
|
||
const lines = String(out || '').split(/\r?\n/)
|
||
for (let i = 0; i < lines.length; i++) {
|
||
const m = /^([A-Za-z0-9_@.:-]+)\b/.exec(lines[i].trim())
|
||
if (!m) continue
|
||
const n = m[1].replace(/\.service$/, '')
|
||
if (names.indexOf(n) < 0) names.push(n)
|
||
}
|
||
}
|
||
return discordFilterChoices(names, q)
|
||
}
|
||
|
||
async function discordSuggestMan(ctx, q) {
|
||
const db = await discordCmdReadJson(ctx, '/share/man/man.json')
|
||
const pages = db && Array.isArray(db.pages) ? db.pages : []
|
||
const names = []
|
||
for (let i = 0; i < pages.length; i++) {
|
||
if (pages[i] && pages[i].name) names.push(String(pages[i].name))
|
||
}
|
||
if (!names.length) {
|
||
names.push('uname', 'whoami', 'login', 'discord-bot', 'systemctl', 'help')
|
||
}
|
||
return discordFilterChoices(names, q)
|
||
}
|
||
|
||
function discordShKey(interaction) {
|
||
return discordInteractionUserId(interaction) || 'anon'
|
||
}
|
||
|
||
function discordShGc() {
|
||
const now = Date.now()
|
||
for (const k in BARE_OS_DISCORD_SH_SESSIONS) {
|
||
const r = BARE_OS_DISCORD_SH_SESSIONS[k]
|
||
if (!r || now - r.atMs > BARE_OS_DISCORD_SH_TTL_MS) delete BARE_OS_DISCORD_SH_SESSIONS[k]
|
||
}
|
||
}
|
||
|
||
function discordShGet(interaction, ctx) {
|
||
discordShGc()
|
||
const key = discordShKey(interaction)
|
||
let rec = BARE_OS_DISCORD_SH_SESSIONS[key]
|
||
const env = discordCmdEnv(ctx)
|
||
const home = env.HOME || '/home/' + discordSessionUser(ctx)
|
||
if (!rec) {
|
||
rec = {
|
||
cwd: env.PWD || home || '~',
|
||
oldpwd: home,
|
||
hist: [],
|
||
lastCmd: '',
|
||
lastExit: 0,
|
||
lastMs: 0,
|
||
atMs: Date.now()
|
||
}
|
||
BARE_OS_DISCORD_SH_SESSIONS[key] = rec
|
||
}
|
||
rec.atMs = Date.now()
|
||
if (!rec.cwd) rec.cwd = env.PWD || home || '~'
|
||
return rec
|
||
}
|
||
|
||
function discordShPrettyCwd(ctx, cwd) {
|
||
const env = discordCmdEnv(ctx)
|
||
const home = env.HOME || '/home/' + discordSessionUser(ctx)
|
||
const c = String(cwd || '~')
|
||
if (c === '~' || c === home) return '~'
|
||
if (home && c.indexOf(home + '/') === 0) return '~' + c.slice(home.length)
|
||
return c
|
||
}
|
||
|
||
function discordShPrompt(ctx, rec) {
|
||
return (
|
||
discordSessionUser(ctx) +
|
||
'@' +
|
||
(discordCmdEnv(ctx).HOSTNAME || 'bare-os') +
|
||
':' +
|
||
discordShPrettyCwd(ctx, rec.cwd)
|
||
)
|
||
}
|
||
|
||
async function discordShPrepare(ctx, rec) {
|
||
const env = discordCmdEnv(ctx)
|
||
const cwd = rec.cwd || env.HOME || '~'
|
||
rec.cwd = cwd
|
||
env.PWD = cwd
|
||
if (ctx.env && ctx.env !== env) ctx.env.PWD = cwd
|
||
if (ctx.vfs && typeof ctx.vfs.chdir === 'function') {
|
||
try {
|
||
await ctx.vfs.chdir(cwd)
|
||
} catch {
|
||
/* keep env PWD */
|
||
}
|
||
}
|
||
}
|
||
|
||
function discordShHarvest(ctx, rec) {
|
||
const env = discordCmdEnv(ctx)
|
||
if (env.PWD) rec.cwd = String(env.PWD)
|
||
if (ctx.vfs && ctx.vfs.env && ctx.vfs.env.PWD) rec.cwd = String(ctx.vfs.env.PWD)
|
||
}
|
||
|
||
function discordShPushHist(rec, cmd, exit, ms, preview) {
|
||
rec.lastCmd = cmd
|
||
rec.lastExit = exit
|
||
rec.lastMs = ms
|
||
rec.hist = rec.hist.filter(function (h) {
|
||
return h && h.cmd !== cmd
|
||
})
|
||
rec.hist.unshift({
|
||
cmd: cmd,
|
||
exit: exit,
|
||
ms: ms,
|
||
preview: String(preview || '').slice(0, 80)
|
||
})
|
||
if (rec.hist.length > 25) rec.hist.length = 25
|
||
}
|
||
|
||
async function discordShCommandNames(ctx) {
|
||
const now = Date.now()
|
||
if (BARE_OS_DISCORD_SH_BINS && now - BARE_OS_DISCORD_SH_BINS_AT < 60000) {
|
||
return BARE_OS_DISCORD_SH_BINS
|
||
}
|
||
const set = Object.create(null)
|
||
for (let i = 0; i < BARE_OS_DISCORD_SH_BUILTINS.length; i++) {
|
||
set[BARE_OS_DISCORD_SH_BUILTINS[i]] = 1
|
||
}
|
||
for (const k in BARE_OS_DISCORD_RUN_ALLOW) {
|
||
if (Object.prototype.hasOwnProperty.call(BARE_OS_DISCORD_RUN_ALLOW, k) && k.indexOf(' ') < 0) {
|
||
set[k] = 1
|
||
}
|
||
}
|
||
if (ctx && ctx.shellAliases && typeof ctx.shellAliases === 'object') {
|
||
const aks = Object.keys(ctx.shellAliases)
|
||
for (let i = 0; i < aks.length; i++) set[aks[i]] = 1
|
||
}
|
||
try {
|
||
if (ctx && ctx.vfs && typeof ctx.vfs.readdir === 'function') {
|
||
const n = await ctx.vfs.readdir('/bin')
|
||
if (Array.isArray(n)) {
|
||
for (let i = 0; i < n.length; i++) set[String(n[i])] = 1
|
||
}
|
||
}
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
const man = await discordCmdReadText(ctx, '/share/man/man.json')
|
||
const j = discordTryJson(man)
|
||
if (j && Array.isArray(j.pages)) {
|
||
for (let i = 0; i < j.pages.length; i++) {
|
||
if (j.pages[i] && j.pages[i].name) set[String(j.pages[i].name)] = 1
|
||
}
|
||
}
|
||
const names = Object.keys(set).sort()
|
||
BARE_OS_DISCORD_SH_BINS = names
|
||
BARE_OS_DISCORD_SH_BINS_AT = now
|
||
return names
|
||
}
|
||
|
||
function discordShSplitLine(q) {
|
||
const s = String(q || '')
|
||
const m = /^(.*?)(\S*)$/.exec(s)
|
||
return { prefix: m ? m[1] : '', token: m ? m[2] : s }
|
||
}
|
||
|
||
function discordShFirstWord(q) {
|
||
const s = String(q || '').trim()
|
||
const i = s.search(/\s/)
|
||
return i < 0 ? s : s.slice(0, i)
|
||
}
|
||
|
||
async function discordShCompletePath(ctx, rec, token, dirsOnly) {
|
||
let dir = rec.cwd || '~'
|
||
let base = token
|
||
const slash = token.lastIndexOf('/')
|
||
if (slash >= 0) {
|
||
const head = token.slice(0, slash + 1)
|
||
base = token.slice(slash + 1)
|
||
if (head.charAt(0) === '/' || head.charAt(0) === '~') dir = head === '/' ? '/' : head.replace(/\/$/, '') || '/'
|
||
else dir = discordJoinPath(rec.cwd, head.replace(/\/$/, '') || '.')
|
||
}
|
||
let names = []
|
||
try {
|
||
if (ctx.vfs && typeof ctx.vfs.readdir === 'function') {
|
||
const n = await ctx.vfs.readdir(dir)
|
||
if (Array.isArray(n)) names = n
|
||
}
|
||
} catch {
|
||
names = []
|
||
}
|
||
const out = []
|
||
const want = String(base || '').toLowerCase()
|
||
for (let i = 0; i < names.length && out.length < 20; i++) {
|
||
const name = String(names[i])
|
||
if (want && name.toLowerCase().indexOf(want) !== 0) continue
|
||
const full = discordJoinPath(dir, name)
|
||
let isDir = false
|
||
try {
|
||
const st = await discordFmStat(ctx, full)
|
||
isDir = discordIsDirStat(st)
|
||
} catch {
|
||
isDir = false
|
||
}
|
||
if (dirsOnly && !isDir) continue
|
||
const shown = (slash >= 0 ? token.slice(0, slash + 1) : '') + name + (isDir ? '/' : '')
|
||
out.push({
|
||
label: shown,
|
||
value: shown,
|
||
description: isDir ? 'directory' : 'file'
|
||
})
|
||
}
|
||
return out
|
||
}
|
||
|
||
function discordShPushChoice(out, seen, name, value, description) {
|
||
const v = String(value || '').slice(0, 100)
|
||
if (!v || seen[v]) return
|
||
if (out.length >= 25) return
|
||
seen[v] = 1
|
||
out.push({
|
||
name: String(name || v).slice(0, 100),
|
||
value: v,
|
||
description: description ? String(description).slice(0, 100) : undefined
|
||
})
|
||
}
|
||
|
||
async function discordSuggestRun(ctx, interaction, q) {
|
||
const rec = discordShGet(interaction, ctx)
|
||
const raw = String(q || '')
|
||
const split = discordShSplitLine(raw)
|
||
const first = discordShFirstWord(raw)
|
||
const out = []
|
||
const seen = Object.create(null)
|
||
if (!raw.trim() || (!split.prefix && first === split.token)) {
|
||
for (let i = 0; i < rec.hist.length && out.length < 6; i++) {
|
||
const h = rec.hist[i]
|
||
if (!h || !h.cmd) continue
|
||
if (split.token && h.cmd.indexOf(split.token) !== 0) continue
|
||
discordShPushChoice(out, seen, '↵ ' + h.cmd, h.cmd, 'history · exit ' + h.exit)
|
||
}
|
||
const cmds = await discordShCommandNames(ctx)
|
||
const needle = split.token.toLowerCase()
|
||
for (let i = 0; i < cmds.length && out.length < 25; i++) {
|
||
if (needle && cmds[i].toLowerCase().indexOf(needle) !== 0) continue
|
||
const kind = BARE_OS_DISCORD_SH_TTY[cmds[i]]
|
||
? 'TUI (needs a real TTY)'
|
||
: ctx.shellAliases && ctx.shellAliases[cmds[i]]
|
||
? 'alias'
|
||
: BARE_OS_DISCORD_SH_BUILTINS.indexOf(cmds[i]) >= 0
|
||
? 'builtin'
|
||
: 'command'
|
||
discordShPushChoice(out, seen, cmds[i], cmds[i], kind)
|
||
}
|
||
return out
|
||
}
|
||
const flags = BARE_OS_DISCORD_SH_FLAGS[first]
|
||
if (split.token.charAt(0) === '-' && flags) {
|
||
for (let i = 0; i < flags.length && out.length < 25; i++) {
|
||
if (split.token && flags[i].indexOf(split.token) !== 0) continue
|
||
const line = (split.prefix + flags[i]).slice(0, 100)
|
||
discordShPushChoice(out, seen, line, line, first + ' flag')
|
||
}
|
||
return out
|
||
}
|
||
const pathish =
|
||
!split.token ||
|
||
split.token.charAt(0) === '.' ||
|
||
split.token.charAt(0) === '/' ||
|
||
split.token.charAt(0) === '~' ||
|
||
split.token.indexOf('/') >= 0 ||
|
||
first === 'cd' ||
|
||
first === 'ls' ||
|
||
first === 'cat' ||
|
||
first === 'rm' ||
|
||
first === 'mv' ||
|
||
first === 'cp' ||
|
||
first === 'mkdir' ||
|
||
first === 'stat' ||
|
||
first === 'head' ||
|
||
first === 'tail' ||
|
||
first === 'edit'
|
||
if (pathish) {
|
||
const paths = await discordShCompletePath(ctx, rec, split.token, first === 'cd')
|
||
for (let i = 0; i < paths.length && out.length < 25; i++) {
|
||
const line = (split.prefix + paths[i].value).slice(0, 100)
|
||
discordShPushChoice(out, seen, line, line, paths[i].description)
|
||
}
|
||
}
|
||
if (!out.length && split.token) {
|
||
const cmds = await discordShCommandNames(ctx)
|
||
const needle = split.token.toLowerCase()
|
||
for (let i = 0; i < cmds.length && out.length < 25; i++) {
|
||
if (cmds[i].toLowerCase().indexOf(needle) !== 0) continue
|
||
const line = (split.prefix + cmds[i]).slice(0, 100)
|
||
discordShPushChoice(out, seen, line, line, 'command')
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
function discordFmKey(interaction) {
|
||
return discordInteractionUserId(interaction) || 'anon'
|
||
}
|
||
|
||
function discordFmGc() {
|
||
const now = Date.now()
|
||
for (const k in BARE_OS_DISCORD_FM_SESSIONS) {
|
||
const rec = BARE_OS_DISCORD_FM_SESSIONS[k]
|
||
if (!rec || now - rec.atMs > BARE_OS_DISCORD_FM_TTL_MS) {
|
||
delete BARE_OS_DISCORD_FM_SESSIONS[k]
|
||
}
|
||
}
|
||
}
|
||
|
||
function discordFmGet(interaction, ctx) {
|
||
discordFmGc()
|
||
let rec = BARE_OS_DISCORD_FM_SESSIONS[discordFmKey(interaction)]
|
||
if (!rec) {
|
||
rec = {
|
||
cwd: '~',
|
||
page: 0,
|
||
sel: '',
|
||
confirm: '',
|
||
atMs: Date.now()
|
||
}
|
||
BARE_OS_DISCORD_FM_SESSIONS[discordFmKey(interaction)] = rec
|
||
}
|
||
rec.atMs = Date.now()
|
||
if (ctx) discordSyncSessionIdentity(ctx)
|
||
return rec
|
||
}
|
||
|
||
function discordJoinPath(dir, name) {
|
||
const n = String(name || '').replace(/^\/+/, '')
|
||
if (!n || n === '.') return dir || '~'
|
||
if (n === '..') return discordParentPath(dir)
|
||
const d = String(dir || '~').replace(/\/+$/, '') || '/'
|
||
if (d === '/') return '/' + n
|
||
if (d === '~') return '~/' + n
|
||
return d + '/' + n
|
||
}
|
||
|
||
function discordParentPath(p) {
|
||
const s = String(p || '~').replace(/\/+$/, '') || '/'
|
||
if (s === '/' || s === '~') return s
|
||
const i = s.lastIndexOf('/')
|
||
if (i <= 0) return s.charAt(0) === '~' ? '~' : '/'
|
||
const parent = s.slice(0, i)
|
||
return parent || (s.charAt(0) === '~' ? '~' : '/')
|
||
}
|
||
|
||
function discordBaseName(p) {
|
||
const s = String(p || '').replace(/\/+$/, '')
|
||
const i = s.lastIndexOf('/')
|
||
return i >= 0 ? s.slice(i + 1) : s
|
||
}
|
||
|
||
function discordIsDirStat(st) {
|
||
if (!st) return false
|
||
if (st.isDirectory === true) return true
|
||
if (st.type === 'directory') return true
|
||
return false
|
||
}
|
||
|
||
async function discordFmList(ctx, cwd) {
|
||
const p = discordCmdPathOk(cwd || '~') || '~'
|
||
if (typeof ctx.vfs?.readdir !== 'function') return { path: p, names: [], err: 'readdir unavailable' }
|
||
try {
|
||
const raw = await ctx.vfs.readdir(p)
|
||
const names = []
|
||
const list = Array.isArray(raw) ? raw : []
|
||
for (let i = 0; i < list.length; i++) {
|
||
const n = String(list[i] || '')
|
||
if (!n || n === '.bareos_empty') continue
|
||
names.push(n)
|
||
}
|
||
names.sort(function (a, b) {
|
||
return a < b ? -1 : a > b ? 1 : 0
|
||
})
|
||
return { path: p, names: names, err: '' }
|
||
} catch (err) {
|
||
return { path: p, names: [], err: (err && err.message) || String(err) }
|
||
}
|
||
}
|
||
|
||
async function discordFmStat(ctx, path) {
|
||
const stfn = ctx.vfs && (ctx.vfs.lstat || ctx.vfs.stat)
|
||
if (typeof stfn !== 'function') return null
|
||
try {
|
||
return await stfn.call(ctx.vfs, path)
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
|
||
async function discordFmView(ctx, interaction) {
|
||
const rec = discordFmGet(interaction, ctx)
|
||
const listing = await discordFmList(ctx, rec.cwd)
|
||
rec.cwd = listing.path
|
||
const names = listing.names
|
||
const pages = Math.max(1, Math.ceil(names.length / BARE_OS_DISCORD_FM_PAGE))
|
||
if (rec.page >= pages) rec.page = pages - 1
|
||
if (rec.page < 0) rec.page = 0
|
||
const start = rec.page * BARE_OS_DISCORD_FM_PAGE
|
||
const slice = names.slice(start, start + BARE_OS_DISCORD_FM_PAGE)
|
||
if (rec.sel && names.indexOf(rec.sel) < 0) rec.sel = ''
|
||
const writable = Boolean(discordCmdPathWriteOk(ctx, rec.cwd))
|
||
const fields = []
|
||
const options = []
|
||
if (rec.cwd !== '/' && rec.cwd !== '~') {
|
||
options.push({ label: '⬆ .. (parent)', value: '..', description: discordParentPath(rec.cwd) })
|
||
}
|
||
for (let i = 0; i < slice.length; i++) {
|
||
const name = slice[i]
|
||
const full = discordJoinPath(rec.cwd, name)
|
||
const st = await discordFmStat(ctx, full)
|
||
const dir = discordIsDirStat(st)
|
||
const size = st && st.size != null && !dir ? discordPrettyBytes(st.size) : dir ? 'dir' : ''
|
||
const mark = rec.sel === name ? '▸ ' : ''
|
||
fields.push(
|
||
discordField(
|
||
mark + (dir ? '📁 ' : '📄 ') + name,
|
||
size || (dir ? 'folder' : 'file'),
|
||
true
|
||
)
|
||
)
|
||
options.push({
|
||
label: (dir ? '📁 ' : '📄 ') + name,
|
||
value: name.slice(0, 100),
|
||
description: size || (dir ? 'open folder' : 'file')
|
||
})
|
||
}
|
||
const sel = discordSelect(
|
||
'fm:pick',
|
||
slice.length ? 'Open an entry…' : 'Empty folder',
|
||
options
|
||
)
|
||
const nav = discordButtons([
|
||
{ id: 'fm:up', label: 'Parent' },
|
||
{ id: 'fm:home', label: 'Home', style: 1 },
|
||
{ id: 'fm:goto', label: 'Go to…' },
|
||
{ id: 'fm:prev', label: '◀' },
|
||
{ id: 'fm:next', label: '▶' }
|
||
])
|
||
const acts = discordButtons(
|
||
writable
|
||
? [
|
||
{ id: 'fm:open', label: 'Open', style: 1 },
|
||
{ id: 'fm:edit', label: 'Edit' },
|
||
{ id: 'fm:new', label: 'New file', style: 3 },
|
||
{ id: 'fm:mkdir', label: 'New folder' },
|
||
{ id: 'fm:more', label: 'Manage…' }
|
||
]
|
||
: [
|
||
{ id: 'fm:open', label: 'Open', style: 1 },
|
||
{ id: 'nav:panel', label: 'Panel' }
|
||
]
|
||
)
|
||
const manage = rec.confirm
|
||
? discordButtons([
|
||
{ id: 'fm:ok', label: 'Confirm delete', style: 4 },
|
||
{ id: 'fm:no', label: 'Cancel', style: 2 }
|
||
])
|
||
: writable
|
||
? discordButtons([
|
||
{ id: 'fm:ren', label: 'Rename' },
|
||
{ id: 'fm:copy', label: 'Copy' },
|
||
{ id: 'fm:del', label: 'Delete', style: 4 },
|
||
{ id: 'fm:ref', label: 'Refresh' },
|
||
{ id: 'nav:panel', label: 'Panel' }
|
||
])
|
||
: discordButtons([{ id: 'fm:ref', label: 'Refresh' }])
|
||
const rows = [sel, nav, acts, manage].filter(Boolean)
|
||
let desc = '`' + rec.cwd + '` · ' + names.length + ' items · page ' + (rec.page + 1) + '/' + pages
|
||
desc += writable ? ' · writable' : ' · read-only'
|
||
if (rec.sel) desc += '\nSelected: **' + rec.sel + '**'
|
||
if (rec.confirm) desc += '\n**Delete `' + rec.confirm + '`?** This cannot be undone.'
|
||
if (listing.err) desc += '\n' + listing.err
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Files',
|
||
desc: desc,
|
||
fields: fields.slice(0, 20),
|
||
color: rec.confirm ? BARE_OS_DISCORD_COLOR_ERR : BARE_OS_DISCORD_COLOR,
|
||
footer: rec.cwd + ' · ' + discordSessionUser(ctx)
|
||
}),
|
||
{ components: rows, nav: false }
|
||
)
|
||
}
|
||
|
||
function discordSuggestEditPaths(ctx, q) {
|
||
const user = discordSessionUser(ctx)
|
||
const names = BARE_OS_DISCORD_EDIT_SUGGEST.concat([
|
||
'/home/' + user + '/notes.txt',
|
||
'/tmp/' + user + '.txt'
|
||
])
|
||
return discordFilterChoices(names, q)
|
||
}
|
||
|
||
function discordSuggestPaths(ctx, q) {
|
||
const e = discordCmdEnv(ctx)
|
||
const home = e.HOME || '/home/' + discordSessionUser(ctx)
|
||
const extra = [home, home + '/.discord', '/home/' + discordSessionUser(ctx)]
|
||
const names = BARE_OS_DISCORD_FS_SUGGEST.concat(extra)
|
||
return discordFilterChoices(names, q)
|
||
}
|
||
|
||
async function discordCmdStatus(ctx) {
|
||
const e = discordCmdEnv(ctx)
|
||
const os = await discordCmdOsRelease(ctx)
|
||
const user = discordSessionUser(ctx)
|
||
const ident = ctx && ctx.identity && ctx.identity.state ? String(ctx.identity.state) : e.BARE_OS_IDENTITY || 'guest'
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Bare OS session',
|
||
color: ident === 'unlocked' ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR_WARN,
|
||
author: { name: user + '@' + (e.HOSTNAME || e.NAME || 'bare-os') },
|
||
fields: [
|
||
discordField('OS', os.PRETTY_NAME || os.NAME || 'Bare OS', true),
|
||
discordField('User', user, true),
|
||
discordField('Identity', ident, true),
|
||
discordField('Home', e.HOME || '/home/' + user, true),
|
||
discordField('Shell', e.SHELL || '/bin/sh', true),
|
||
discordField('UID', e.UID || '—', true),
|
||
discordField('Version', os.VERSION_ID || e.BARE_OS_KERNEL_VERSION || e.BARE_OS_RELEASE || '—', true),
|
||
discordField('Arch', e.BARE_OS_ARCH || e.MACHINE || '—', true),
|
||
discordField('Booter', e.BARE_OS_BOOTER_PACKAGE_VERSION || '—', true)
|
||
]
|
||
})
|
||
)
|
||
}
|
||
|
||
async function discordCmdUname(ctx) {
|
||
const e = discordCmdEnv(ctx)
|
||
const os = await discordCmdOsRelease(ctx)
|
||
const line = [
|
||
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(' ')
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'uname',
|
||
desc: discordCmdFence(line)
|
||
})
|
||
)
|
||
}
|
||
|
||
async function discordCmdHandleBare(ctx, sub) {
|
||
const e = discordCmdEnv(ctx)
|
||
const user = discordSessionUser(ctx)
|
||
if (sub === 'ping') {
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Pong',
|
||
color: BARE_OS_DISCORD_COLOR_OK,
|
||
desc: 'Bare OS is online as **' + user + '**.'
|
||
})
|
||
)
|
||
}
|
||
if (sub === 'about') {
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Bare OS Discord bot',
|
||
desc:
|
||
'Slash control surface for **this logged-in session** (`' +
|
||
user +
|
||
'`). Commands run with that user’s `HOME`, VFS, and `systemctl`.'
|
||
})
|
||
)
|
||
}
|
||
if (sub === 'help') {
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Commands',
|
||
desc: 'Running as **' + user + '**. Use the buttons below, or slash commands.',
|
||
fields: [
|
||
discordField('/bare', 'Session — status, whoami, hostname, date, uptime, motd, uname'),
|
||
discordField('/sys', 'Host — disk, memory, processes, env, doctor, features, limits'),
|
||
discordField('/svc', 'Services — list, start, stop, restart, logs'),
|
||
discordField('/fs', 'Read-only VFS — ls, cat, stat, head'),
|
||
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('/plugins', 'List, reload, enable/disable ~/.discord/plugins')
|
||
]
|
||
})
|
||
)
|
||
}
|
||
if (sub === 'status') return discordCmdStatus(ctx)
|
||
if (sub === 'uname') return discordCmdUname(ctx)
|
||
if (sub === 'whoami') {
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Whoami',
|
||
color: BARE_OS_DISCORD_COLOR_OK,
|
||
fields: [
|
||
discordField('User', user, true),
|
||
discordField('Home', '`' + (e.HOME || '/home/' + user) + '`', true),
|
||
discordField('Identity', e.BARE_OS_IDENTITY || (ctx.identity && ctx.identity.state) || '', true)
|
||
]
|
||
})
|
||
)
|
||
}
|
||
if (sub === 'hostname') {
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Hostname',
|
||
desc: '`' + String(e.HOSTNAME || e.NAME || 'bare-os') + '`'
|
||
})
|
||
)
|
||
}
|
||
if (sub === 'date') {
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Date',
|
||
desc: '`' + new Date().toISOString() + '`'
|
||
})
|
||
)
|
||
}
|
||
if (sub === 'uptime') {
|
||
const t = await discordCmdReadText(ctx, '/proc/uptime')
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Uptime',
|
||
fields: [
|
||
discordField('Session', t ? discordPrettyUptime(t) : 'unavailable', true)
|
||
]
|
||
})
|
||
)
|
||
}
|
||
if (sub === 'motd') {
|
||
const t = await discordCmdReadText(ctx, '/etc/motd')
|
||
if (!t) {
|
||
return discordResult(discordEmbed({ title: 'motd', desc: 'No `/etc/motd`.' }))
|
||
}
|
||
return { more: { title: 'motd', body: t, fence: true } }
|
||
}
|
||
return { text: 'unknown /bare subcommand', ephemeral: true }
|
||
}
|
||
|
||
function discordFormatHostDf(obj) {
|
||
const host = obj && obj.host && typeof obj.host === 'object' ? obj.host : obj
|
||
const sv = obj && obj.statvfs && typeof obj.statvfs === 'object' ? obj.statvfs : null
|
||
const fields = []
|
||
if (host) {
|
||
if (host.platform || host.arch) {
|
||
fields.push(
|
||
discordField(
|
||
'Host',
|
||
[host.platform, host.arch, host.hostname || host.machine]
|
||
.filter(Boolean)
|
||
.join(' · '),
|
||
true
|
||
)
|
||
)
|
||
}
|
||
if (host.totalmem != null) {
|
||
const used =
|
||
host.freemem != null ? Number(host.totalmem) - Number(host.freemem) : null
|
||
fields.push(discordField('RAM', discordPrettyBytes(host.totalmem), true))
|
||
if (host.freemem != null) {
|
||
fields.push(discordField('Free RAM', discordPrettyBytes(host.freemem), true))
|
||
}
|
||
if (used != null && host.totalmem) {
|
||
const pct = Math.round((used / Number(host.totalmem)) * 100)
|
||
fields.push(discordField('RAM used', pct + '%', true))
|
||
}
|
||
}
|
||
if (host.availableParallelism != null) {
|
||
fields.push(discordField('CPUs', String(host.availableParallelism), true))
|
||
}
|
||
if (host.release) fields.push(discordField('Release', host.release, true))
|
||
}
|
||
if (obj && obj.peers != null) fields.push(discordField('Peers', String(obj.peers), true))
|
||
if (sv) {
|
||
fields.push(discordField('Volume', sv.volume_class || sv.f_basetype || '—', true))
|
||
if (sv.pwd_logical) fields.push(discordField('PWD', sv.pwd_logical, true))
|
||
const bsize = Number(sv.f_frsize || sv.f_bsize || 4096)
|
||
if (sv.f_blocks != null) {
|
||
fields.push(discordField('System size', discordPrettyBytes(Number(sv.f_blocks) * bsize), true))
|
||
}
|
||
if (sv.f_bavail != null) {
|
||
fields.push(discordField('System avail', discordPrettyBytes(Number(sv.f_bavail) * bsize), true))
|
||
}
|
||
const pers = sv.personalDrive
|
||
if (pers && pers.f_blocks != null) {
|
||
const pb = Number(pers.f_frsize || pers.f_bsize || bsize)
|
||
fields.push(
|
||
discordField('Personal size', discordPrettyBytes(Number(pers.f_blocks) * pb), true)
|
||
)
|
||
}
|
||
}
|
||
if (obj && obj.pipeline && obj.pipeline.maxBytes != null) {
|
||
fields.push(discordField('Pipe cap', discordPrettyBytes(obj.pipeline.maxBytes), true))
|
||
}
|
||
return fields
|
||
}
|
||
|
||
function discordFormatMem(raw) {
|
||
const json = discordTryJson(raw)
|
||
if (json && json.host) {
|
||
return discordFormatHostDf(json)
|
||
}
|
||
const mi = discordParseMeminfo(raw)
|
||
const fields = []
|
||
const pick = [
|
||
['MemTotal', 'Total'],
|
||
['MemFree', 'Free'],
|
||
['MemAvailable', 'Available'],
|
||
['Buffers', 'Buffers'],
|
||
['Cached', 'Cached'],
|
||
['SwapTotal', 'Swap'],
|
||
['SwapFree', 'Swap free']
|
||
]
|
||
for (let i = 0; i < pick.length; i++) {
|
||
if (mi[pick[i][0]] != null) {
|
||
fields.push(discordField(pick[i][1], discordPrettyBytes(mi[pick[i][0]]), true))
|
||
}
|
||
}
|
||
if (mi.MemTotal && mi.MemAvailable != null) {
|
||
const pct = Math.round((1 - mi.MemAvailable / mi.MemTotal) * 100)
|
||
fields.push(discordField('Used', pct + '%', true))
|
||
}
|
||
return fields
|
||
}
|
||
|
||
function discordFormatRlimits(obj) {
|
||
const lim = obj && obj.rlimits && typeof obj.rlimits === 'object' ? obj.rlimits : obj
|
||
const fields = []
|
||
if (!lim || typeof lim !== 'object') return fields
|
||
const names = {
|
||
NOFILE: 'Open files',
|
||
NPROC: 'Processes',
|
||
AS: 'Address space',
|
||
DATA: 'Data',
|
||
STACK: 'Stack',
|
||
CORE: 'Core dump',
|
||
RSS: 'RSS',
|
||
CPU: 'CPU time',
|
||
FSIZE: 'File size',
|
||
NOVM: 'No VM',
|
||
MEMLOCK: 'Locked mem'
|
||
}
|
||
const keys = Object.keys(lim)
|
||
for (let i = 0; i < keys.length && fields.length < 18; i++) {
|
||
const raw = keys[i]
|
||
const v = lim[raw]
|
||
if (v && typeof v === 'object' && (v.cur != null || v.max != null)) {
|
||
const short = raw.replace(/^RLIMIT_/, '')
|
||
const label = names[short] || discordHumanLabel(short)
|
||
const cur = discordIsByteishKey(raw) ? discordPrettyBytes(v.cur) : String(v.cur)
|
||
const max = discordIsByteishKey(raw) ? discordPrettyBytes(v.max) : String(v.max)
|
||
fields.push(discordField(label, cur + ' / ' + max, true))
|
||
}
|
||
}
|
||
if (obj && obj.bareOsExecMaxDepth != null) {
|
||
fields.push(discordField('Exec depth', String(obj.bareOsExecMaxDepth), true))
|
||
}
|
||
return fields
|
||
}
|
||
|
||
function discordFormatFeatures(obj) {
|
||
const feat =
|
||
obj && obj.features && typeof obj.features === 'object' ? obj.features : obj
|
||
if (!feat || typeof feat !== 'object') return []
|
||
const on = []
|
||
const off = []
|
||
const keys = Object.keys(feat).sort()
|
||
for (let i = 0; i < keys.length; i++) {
|
||
const v = feat[keys[i]]
|
||
if (v === true || v === 1 || v === '1') on.push('`' + keys[i] + '`')
|
||
else if (v === false || v === 0 || v === '0') off.push('`' + keys[i] + '`')
|
||
}
|
||
const fields = []
|
||
if (on.length) fields.push(discordField('On', on.slice(0, 24).join(' ')))
|
||
if (off.length) fields.push(discordField('Off', off.slice(0, 16).join(' ')))
|
||
return fields
|
||
}
|
||
|
||
function discordFormatDoctor(obj) {
|
||
if (!obj || typeof obj !== 'object') {
|
||
return { fields: [], desc: '', color: BARE_OS_DISCORD_COLOR_WARN }
|
||
}
|
||
const pa = obj.peerAdmission && typeof obj.peerAdmission === 'object' ? obj.peerAdmission : {}
|
||
const hn = obj.hostnameMutation && typeof obj.hostnameMutation === 'object' ? obj.hostnameMutation : {}
|
||
const kh = obj.keyHandlePolicy && typeof obj.keyHandlePolicy === 'object' ? obj.keyHandlePolicy : {}
|
||
const fields = []
|
||
if (pa.allowlistConfigured != null) {
|
||
fields.push(discordField('Peer allowlist', pa.allowlistConfigured ? 'configured' : 'open', true))
|
||
}
|
||
if (pa.denylistConfigured != null) {
|
||
fields.push(discordField('Peer denylist', pa.denylistConfigured ? 'configured' : 'none', true))
|
||
}
|
||
if (pa.requireCapsConfigured != null) {
|
||
const n = pa.requireCapsTokenCount
|
||
fields.push(
|
||
discordField(
|
||
'Require caps',
|
||
pa.requireCapsConfigured ? (n ? n + ' tokens' : 'yes') : 'no',
|
||
true
|
||
)
|
||
)
|
||
}
|
||
if (hn.enabled != null) {
|
||
fields.push(discordField('Hostname set', hn.enabled ? 'allowed' : 'locked', true))
|
||
}
|
||
if (kh.defaultTtlMs) {
|
||
fields.push(discordField('Key TTL', discordPrettyValue('defaultTtlMs', kh.defaultTtlMs), true))
|
||
}
|
||
if (Array.isArray(obj.mfaExtensionPoints) && obj.mfaExtensionPoints.length) {
|
||
fields.push(discordField('MFA hooks', obj.mfaExtensionPoints.join(' · '), false))
|
||
}
|
||
const bits = []
|
||
if (pa.allowlistConfigured) bits.push('peer allowlist on')
|
||
else if (pa.allowlistConfigured === false) bits.push('peer allowlist **open**')
|
||
if (hn.enabled === true) bits.push('hostname mutation on')
|
||
else if (hn.enabled === false) bits.push('hostname locked')
|
||
return {
|
||
fields: fields,
|
||
desc: bits.join(' · '),
|
||
color: pa.allowlistConfigured ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR_WARN
|
||
}
|
||
}
|
||
|
||
async function discordCmdHandleSys(ctx, sub) {
|
||
if (sub === 'df') {
|
||
const t =
|
||
(await discordCmdReadText(ctx, '/proc/bare_os_resources')) ||
|
||
(await discordCmdReadText(ctx, '/proc/bare_os/resources')) ||
|
||
(await discordCmdReadText(ctx, '/proc/bare_os/host_os.json'))
|
||
const obj = discordTryJson(t)
|
||
const fields = obj ? discordFormatHostDf(obj) : []
|
||
if (fields.length) {
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Disk / host',
|
||
fields: fields
|
||
})
|
||
)
|
||
}
|
||
return discordPrettySnapshot('Disk / host', t)
|
||
}
|
||
if (sub === 'mem') {
|
||
const t =
|
||
(await discordCmdReadText(ctx, '/proc/meminfo')) ||
|
||
(await discordCmdReadText(ctx, '/proc/bare_os/host_os.json'))
|
||
const fields = discordFormatMem(t)
|
||
if (fields.length) {
|
||
return discordResult(discordEmbed({ title: 'Memory', fields: fields }))
|
||
}
|
||
return discordPrettySnapshot('Memory', t)
|
||
}
|
||
if (sub === 'ps') {
|
||
const table = await discordCmdReadJson(ctx, '/proc/bare_os/process_table.json')
|
||
const rows = table && Array.isArray(table.processes) ? table.processes : []
|
||
const fields = []
|
||
for (let i = 0; i < Math.min(rows.length, 18); i++) {
|
||
const r = rows[i] || {}
|
||
const name = String(r.name || r.comm || r.cmd || r.id || 'proc')
|
||
const st = String(r.state || r.status || '')
|
||
const pid = String(r.pid || r.id || i)
|
||
fields.push(discordField(name, 'pid ' + pid + (st ? ' · ' + st : ''), true))
|
||
}
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Processes · ' + rows.length,
|
||
desc: rows.length ? rows.length + ' logical processes in this session.' : 'No logical processes in the session table.',
|
||
fields: fields
|
||
})
|
||
)
|
||
}
|
||
if (sub === 'env') {
|
||
const e = discordCmdEnv(ctx)
|
||
const fields = [
|
||
discordField('User', e.USER, true),
|
||
discordField('Home', e.HOME, true),
|
||
discordField('Identity', e.BARE_OS_IDENTITY, true),
|
||
discordField('Shell', e.SHELL, true),
|
||
discordField('Pwd', e.PWD, true),
|
||
discordField(
|
||
'UID / GID',
|
||
e.UID || e.GID ? String(e.UID || '—') + ' / ' + String(e.GID || '—') : '',
|
||
true
|
||
)
|
||
]
|
||
const prefer = [
|
||
'HOSTNAME',
|
||
'PATH',
|
||
'TERM',
|
||
'TZ',
|
||
'LANG',
|
||
'EDITOR',
|
||
'PAGER',
|
||
'BARE_OS_THEME',
|
||
'BARE_OS_COLOR_DEPTH'
|
||
]
|
||
const skip = /TOKEN|SECRET|PASSWORD|PASSWD|KEY|PRIVATE|CREDENTIAL|^USER$|^HOME$|^SHELL$|^PWD$|^LOGNAME$|^UID$|^GID$|^GROUP$|^BARE_OS_IDENTITY$/i
|
||
const seen = Object.create(null)
|
||
for (let i = 0; i < prefer.length; i++) {
|
||
const k = prefer[i]
|
||
seen[k] = 1
|
||
if (e[k] == null || e[k] === '') continue
|
||
fields.push(discordField(discordHumanLabel(k.replace(/^BARE_OS_/, '')), String(e[k]).slice(0, 80), true))
|
||
}
|
||
const keys = Object.keys(e).sort()
|
||
for (let i = 0; i < keys.length && fields.length < 15; i++) {
|
||
if (seen[keys[i]] || skip.test(keys[i])) continue
|
||
if (e[keys[i]] == null || e[keys[i]] === '') continue
|
||
fields.push(discordField(keys[i], String(e[keys[i]]).slice(0, 72), true))
|
||
}
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Environment',
|
||
desc: 'Session env for **' + discordSessionUser(ctx) + '** (secrets omitted).',
|
||
fields: fields
|
||
}),
|
||
{ ephemeral: true }
|
||
)
|
||
}
|
||
if (sub === 'doctor') {
|
||
const t =
|
||
(await discordCmdReadText(ctx, '/proc/bare_os/security_posture.json')) ||
|
||
(await discordCmdReadText(ctx, '/proc/bare_os/debug.json'))
|
||
const obj = discordTryJson(t)
|
||
const fmt = obj ? discordFormatDoctor(obj) : null
|
||
if (fmt && fmt.fields.length) {
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Doctor',
|
||
desc: fmt.desc,
|
||
fields: fmt.fields,
|
||
color: fmt.color
|
||
})
|
||
)
|
||
}
|
||
return discordPrettySnapshot('Doctor', t, { color: fmt && fmt.color })
|
||
}
|
||
if (sub === 'features') {
|
||
const t =
|
||
(await discordCmdReadText(ctx, '/proc/bare_os/features')) ||
|
||
(await discordCmdReadText(ctx, '/proc/bare_os_features')) ||
|
||
(await discordCmdReadText(ctx, '/proc/bare_os/features.json'))
|
||
const obj = discordTryJson(t)
|
||
const fields = obj ? discordFormatFeatures(obj) : []
|
||
if (fields.length) {
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Features',
|
||
desc: 'Guest capability flags on this booter.',
|
||
fields: fields
|
||
})
|
||
)
|
||
}
|
||
return discordPrettySnapshot('Features', t)
|
||
}
|
||
if (sub === 'rlimits') {
|
||
const t = await discordCmdReadText(ctx, '/proc/bare_os/rlimits.json')
|
||
const obj = discordTryJson(t)
|
||
const fields = obj ? discordFormatRlimits(obj) : []
|
||
if (fields.length) {
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Resource limits',
|
||
desc: 'Soft / hard · Bare OS runtime caps (not Linux rlimits).',
|
||
fields: fields
|
||
})
|
||
)
|
||
}
|
||
return discordPrettySnapshot('Resource limits', t)
|
||
}
|
||
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 }
|
||
}
|
||
if (sub === 'list') {
|
||
const out = await discordCmdCapture(ctx, function () {
|
||
return ctx.bareOsRunSystemctlCli(['systemctl', 'list'])
|
||
})
|
||
const parsed = discordParseSystemctlList(out)
|
||
const fields = []
|
||
for (let i = 0; i < parsed.length && fields.length < 20; i++) {
|
||
const r = parsed[i]
|
||
const bits = []
|
||
if (r.active) bits.push('**' + r.active + '**')
|
||
if (r.sub && r.sub !== r.active) bits.push(r.sub)
|
||
if (r.preset && r.preset !== 'enabled') bits.push(r.preset)
|
||
fields.push(discordField(r.unit, bits.join(' · ') || 'unknown', true))
|
||
}
|
||
const units = await discordSuggestUnits(ctx, '')
|
||
const sel = discordSelect(
|
||
'svc:pick',
|
||
'Inspect a unit…',
|
||
units.map(function (c) {
|
||
return { label: c.name, value: c.value }
|
||
})
|
||
)
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Services · ' + (parsed.length || 0),
|
||
desc: parsed.length
|
||
? 'Pick a unit to inspect, start, or stop.'
|
||
: out
|
||
? discordCmdClip(out, 800)
|
||
: 'No units registered in this session.',
|
||
fields: fields,
|
||
color: BARE_OS_DISCORD_COLOR
|
||
}),
|
||
{ components: sel ? [sel] : [] }
|
||
)
|
||
}
|
||
if (!name) return { text: 'unit name required — pick from autocomplete', ephemeral: true }
|
||
const argv = ['systemctl', sub === 'logs' ? 'status' : sub, name]
|
||
if (sub === 'logs') {
|
||
return discordCmdHandleJournal(ctx, name)
|
||
}
|
||
const out = await discordCmdCapture(ctx, function () {
|
||
return ctx.bareOsRunSystemctlCli(argv)
|
||
})
|
||
const act = discordButtons([
|
||
{ id: 'svc:status:' + name, label: 'Status', style: 1 },
|
||
{ id: 'svc:start:' + name, label: 'Start', style: 3 },
|
||
{ id: 'svc:stop:' + name, label: 'Stop', style: 4 },
|
||
{ id: 'svc:restart:' + name, label: 'Restart' },
|
||
{ id: 'svc:logs:' + name, label: 'Logs' }
|
||
])
|
||
const color =
|
||
sub === 'stop'
|
||
? BARE_OS_DISCORD_COLOR_WARN
|
||
: sub === 'start' || sub === 'restart'
|
||
? BARE_OS_DISCORD_COLOR_OK
|
||
: BARE_OS_DISCORD_COLOR
|
||
if (!out) {
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: name,
|
||
desc: 'No output from `systemctl ' + sub + '`.',
|
||
color: color
|
||
}),
|
||
{ components: act ? [act] : [] }
|
||
)
|
||
}
|
||
return {
|
||
more: {
|
||
title: name,
|
||
lead: '`' + sub + '`',
|
||
body: discordCmdRedact(out),
|
||
fence: true,
|
||
color: color,
|
||
components: act ? [act] : []
|
||
}
|
||
}
|
||
}
|
||
|
||
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 : []
|
||
const shown = list.slice(0, 60)
|
||
const desc = shown.length
|
||
? shown
|
||
.map(function (n) {
|
||
return '`' + String(n).replace(/`/g, "'") + '`'
|
||
})
|
||
.join(' ')
|
||
: '(empty)'
|
||
return {
|
||
more: {
|
||
title: 'Listing',
|
||
lead:
|
||
'`' +
|
||
p +
|
||
'` · ' +
|
||
list.length +
|
||
' ' +
|
||
(list.length === 1 ? 'entry' : 'entries'),
|
||
body: desc === '(empty)' ? '' : shown.join('\n'),
|
||
fence: desc !== '(empty)',
|
||
footer: discordSessionUser(ctx)
|
||
}
|
||
}
|
||
} 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)
|
||
const fields = []
|
||
const kind = st.isDirectory
|
||
? 'directory'
|
||
: st.isFile
|
||
? 'file'
|
||
: st.isSymbolicLink
|
||
? 'symlink'
|
||
: st.type || 'entry'
|
||
fields.push(discordField('Type', String(kind), true))
|
||
if (st.size != null) fields.push(discordField('Size', discordPrettyBytes(st.size), true))
|
||
if (st.mode != null) fields.push(discordField('Mode', String(st.mode), true))
|
||
if (st.mtime || st.mtimeMs) {
|
||
const ms = st.mtimeMs || Date.parse(st.mtime)
|
||
fields.push(
|
||
discordField('Modified', Number.isFinite(ms) ? new Date(ms).toISOString() : String(st.mtime), true)
|
||
)
|
||
}
|
||
if (st.path && st.path !== p) fields.push(discordField('Resolved', String(st.path), false))
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Stat',
|
||
desc: '`' + p + '`',
|
||
fields: fields.slice(0, 12)
|
||
})
|
||
)
|
||
} catch (err) {
|
||
return { text: 'stat failed: ' + ((err && err.message) || err), ephemeral: true }
|
||
}
|
||
}
|
||
const text = await discordCmdReadText(ctx, p)
|
||
if (!text) {
|
||
return discordResult(discordEmbed({ title: 'File', desc: '`' + p + '` is empty or unreadable.' }))
|
||
}
|
||
const writable = Boolean(discordCmdPathWriteOk(ctx, p))
|
||
const editRow = writable
|
||
? discordButtons([{ id: 'edit:open', label: 'Edit in modal', style: 1 }])
|
||
: null
|
||
const extra = {
|
||
components: editRow ? [editRow] : [],
|
||
editPath: writable ? p : ''
|
||
}
|
||
const asJson = discordTryJson(text)
|
||
if (asJson && typeof asJson === 'object' && !Array.isArray(asJson)) {
|
||
return discordPrettySnapshot(p, asJson, extra)
|
||
}
|
||
if (sub === 'head') {
|
||
const n = Math.max(1, Math.min(80, Number(nlines) || 12))
|
||
const head = text.split(/\r?\n/).slice(0, n).join('\n')
|
||
return {
|
||
more: {
|
||
title: 'Head',
|
||
lead: '`' + p + '` · first ' + n + ' lines',
|
||
body: head,
|
||
fence: true,
|
||
components: extra.components,
|
||
editPath: extra.editPath
|
||
}
|
||
}
|
||
}
|
||
return {
|
||
more: {
|
||
title: 'File',
|
||
lead: '`' + p + '`',
|
||
body: text.slice(0, BARE_OS_DISCORD_FS_MAX),
|
||
fence: true,
|
||
footer: text.length + ' bytes · ' + discordSessionUser(ctx),
|
||
components: extra.components,
|
||
editPath: extra.editPath
|
||
}
|
||
}
|
||
}
|
||
|
||
function discordFormatNetSummary(obj) {
|
||
if (!obj || typeof obj !== 'object') {
|
||
return { fields: [], desc: '', color: BARE_OS_DISCORD_COLOR }
|
||
}
|
||
const rq =
|
||
obj.replicationQueue && typeof obj.replicationQueue === 'object' ? obj.replicationQueue : {}
|
||
const st = obj.stagingSlot && typeof obj.stagingSlot === 'object' ? obj.stagingSlot : {}
|
||
const fields = []
|
||
const peers = obj.peerCount
|
||
const role = obj.seedRole || rq.role || st.role
|
||
const proto = rq.protocol || st.protocol || obj.protocol
|
||
if (peers != null) fields.push(discordField('Peers', String(peers), true))
|
||
if (role) fields.push(discordField('Role', String(role), true))
|
||
if (proto) fields.push(discordField('Protocol', '`' + String(proto) + '`', true))
|
||
if (obj.topicHex) {
|
||
const hex = String(obj.topicHex)
|
||
fields.push(
|
||
discordField('Topic', '`' + hex.slice(0, 16) + (hex.length > 16 ? '…' : '') + '`', true)
|
||
)
|
||
}
|
||
if (obj.replicationQueueDepth != null) {
|
||
fields.push(discordField('Queue depth', String(obj.replicationQueueDepth), true))
|
||
}
|
||
if (rq.queueDepthEstimate != null) {
|
||
fields.push(discordField('Queue estimate', String(rq.queueDepthEstimate), true))
|
||
}
|
||
if (rq.hypercoreLengthHint != null) {
|
||
fields.push(discordField('Core length', String(rq.hypercoreLengthHint), true))
|
||
}
|
||
if (rq.manifestPathCount != null) {
|
||
fields.push(discordField('Manifests', String(rq.manifestPathCount), true))
|
||
}
|
||
if (rq.localRamBlockCount != null) {
|
||
fields.push(discordField('RAM blocks', String(rq.localRamBlockCount), true))
|
||
}
|
||
if (st.activeSlot) fields.push(discordField('Active slot', String(st.activeSlot), true))
|
||
const fwA = obj.peerFirewallAcceptedTotal
|
||
const fwR = obj.peerFirewallRejectedTotal
|
||
const fwI = obj.peerFirewallInboundTotal
|
||
const fwO = obj.peerFirewallOutboundTotal
|
||
if (fwA != null || fwR != null || fwI != null || fwO != null) {
|
||
fields.push(
|
||
discordField(
|
||
'Firewall',
|
||
'accept ' +
|
||
(fwA == null ? '0' : fwA) +
|
||
' · reject ' +
|
||
(fwR == null ? '0' : fwR) +
|
||
'\nin ' +
|
||
(fwI == null ? '0' : fwI) +
|
||
' · out ' +
|
||
(fwO == null ? '0' : fwO),
|
||
true
|
||
)
|
||
)
|
||
}
|
||
if (obj.seedHandshakeError) {
|
||
fields.push(discordField('Handshake', String(obj.seedHandshakeError), false))
|
||
}
|
||
const peerN = Number(peers)
|
||
const head = []
|
||
if (Number.isFinite(peerN)) head.push('**' + peerN + '** peer' + (peerN === 1 ? '' : 's'))
|
||
if (role) head.push(String(role))
|
||
if (proto) head.push('`' + proto + '`')
|
||
let desc = head.join(' · ')
|
||
const note = rq.snapshotWorkflowNote || obj.note
|
||
if (note) desc += (desc ? '\n' : '') + '*' + String(note).slice(0, 220) + '*'
|
||
const at = rq.atMs || obj.atMs
|
||
return {
|
||
fields: fields,
|
||
desc: desc,
|
||
color: Number.isFinite(peerN) && peerN > 0 ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR_WARN,
|
||
footer: at ? 'Updated ' + new Date(Number(at)).toISOString() + ' · expires after 2m idle' : ''
|
||
}
|
||
}
|
||
|
||
function discordFormatSwarm(obj) {
|
||
if (!obj || typeof obj !== 'object') return []
|
||
const fields = []
|
||
if (obj.peerCount != null) fields.push(discordField('Peers', String(obj.peerCount), true))
|
||
if (obj.protocol) fields.push(discordField('Protocol', '`' + String(obj.protocol) + '`', true))
|
||
if (obj.topicCount != null) fields.push(discordField('Topics', String(obj.topicCount), true))
|
||
if (obj.topicHex) {
|
||
const hex = String(obj.topicHex)
|
||
fields.push(
|
||
discordField('Topic', '`' + hex.slice(0, 16) + (hex.length > 16 ? '…' : '') + '`', true)
|
||
)
|
||
}
|
||
const lc = obj.lifecycle
|
||
if (lc && typeof lc === 'object') {
|
||
const bits = []
|
||
const lk = Object.keys(lc)
|
||
for (let i = 0; i < lk.length && bits.length < 6; i++) {
|
||
if (typeof lc[lk[i]] === 'object' || discordValueEmpty(lc[lk[i]])) continue
|
||
bits.push('**' + discordHumanLabel(lk[i]) + '** ' + discordPrettyValue(lk[i], lc[lk[i]]))
|
||
}
|
||
if (bits.length) fields.push(discordField('Lifecycle', bits.join('\n'), false))
|
||
}
|
||
const sc = obj.peerScoringAggregates
|
||
if (sc && typeof sc === 'object') {
|
||
const banned = sc.bannedActiveCount
|
||
const hi = sc.highLatencyEwmaCount
|
||
const low = sc.lowSuccessRateBucketCount
|
||
if (banned != null || hi != null || low != null) {
|
||
fields.push(
|
||
discordField(
|
||
'Scoring',
|
||
'banned ' +
|
||
String(banned || 0) +
|
||
' · high-lat ' +
|
||
String(hi || 0) +
|
||
' · low-ok ' +
|
||
String(low || 0),
|
||
true
|
||
)
|
||
)
|
||
}
|
||
}
|
||
if (Array.isArray(obj.peers) && obj.peers.length) {
|
||
fields.push(discordField('Peer list', String(obj.peers.length), true))
|
||
}
|
||
return fields
|
||
}
|
||
|
||
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.json')) ||
|
||
(await discordCmdReadText(ctx, '/proc/bare_os_swarm'))
|
||
const obj = discordTryJson(t)
|
||
const fields = obj ? discordFormatSwarm(obj) : []
|
||
if (fields.length) {
|
||
const n = obj && obj.peerCount
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Swarm',
|
||
desc:
|
||
n != null
|
||
? '**' + n + '** peer' + (Number(n) === 1 ? '' : 's') + (obj.protocol ? ' · `' + obj.protocol + '`' : '')
|
||
: '',
|
||
fields: fields,
|
||
color: obj && obj.peerCount > 0 ? BARE_OS_DISCORD_COLOR_OK : BARE_OS_DISCORD_COLOR_WARN
|
||
})
|
||
)
|
||
}
|
||
return discordPrettySnapshot('Swarm', t)
|
||
}
|
||
const t =
|
||
(await discordCmdReadText(ctx, '/proc/bare_os/net_summary.json')) ||
|
||
(await discordCmdReadText(ctx, '/proc/net/dev'))
|
||
const obj = discordTryJson(t)
|
||
if (obj) {
|
||
const fmt = discordFormatNetSummary(obj)
|
||
if (fmt.fields.length) {
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Network',
|
||
desc: fmt.desc,
|
||
fields: fmt.fields,
|
||
color: fmt.color,
|
||
footer: fmt.footer || undefined
|
||
})
|
||
)
|
||
}
|
||
return discordPrettySnapshot('Network', obj)
|
||
}
|
||
if (t && t.indexOf('Inter-|') >= 0) {
|
||
const lines = t.split(/\r?\n/).filter(Boolean)
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Network interfaces',
|
||
desc: discordCmdFence(lines.slice(0, 16).join('\n'))
|
||
})
|
||
)
|
||
}
|
||
return discordPrettySnapshot('Network', t)
|
||
}
|
||
|
||
async function discordCmdHandleMan(ctx, page) {
|
||
const name = String(page || '').replace(/[^a-zA-Z0-9._+-]/g, '')
|
||
if (!name) return { text: 'man page name required — start typing for autocomplete', ephemeral: true }
|
||
if (typeof ctx.execLine === 'function') {
|
||
const out = await discordCmdCapture(ctx, function () {
|
||
return ctx.execLine('man ' + name)
|
||
})
|
||
if (out) {
|
||
const clean = String(out)
|
||
.replace(/\x1b\[[0-9;]*m/g, '')
|
||
.trim()
|
||
return {
|
||
more: {
|
||
title: 'man ' + name,
|
||
body: clean,
|
||
fence: false
|
||
}
|
||
}
|
||
}
|
||
}
|
||
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 {
|
||
more: {
|
||
title: 'man ' + (p.title || name),
|
||
lead: p.synopsis && p.synopsis[0] ? '`' + p.synopsis[0] + '`' : '',
|
||
body: String(p.description || ''),
|
||
fence: false
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} 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')
|
||
}
|
||
|
||
function discordShHud(ctx, interaction, note) {
|
||
const rec = discordShGet(interaction, ctx)
|
||
const fields = [
|
||
discordField('Directory', '`' + discordShPrettyCwd(ctx, rec.cwd) + '`', true),
|
||
discordField('Last exit', rec.lastCmd ? String(rec.lastExit) : '—', true),
|
||
discordField('User', discordSessionUser(ctx), true)
|
||
]
|
||
if (rec.lastCmd) {
|
||
fields.push(discordField('Last command', '`' + rec.lastCmd + '`', false))
|
||
}
|
||
const hist = rec.hist.slice(0, 6)
|
||
if (hist.length) {
|
||
fields.push(
|
||
discordField(
|
||
'History',
|
||
hist
|
||
.map(function (h) {
|
||
return '`' + h.cmd + '` · ' + h.exit
|
||
})
|
||
.join('\n')
|
||
)
|
||
)
|
||
}
|
||
const histSel = discordSelect(
|
||
'sh:hist',
|
||
'Replay a command…',
|
||
rec.hist.slice(0, 20).map(function (h, i) {
|
||
return {
|
||
label: h.cmd.slice(0, 90),
|
||
value: String(i),
|
||
description: 'exit ' + h.exit + (h.ms ? ' · ' + h.ms + 'ms' : '')
|
||
}
|
||
})
|
||
)
|
||
const acts = discordButtons([
|
||
{ id: 'sh:again', label: 'Repeat', style: 1 },
|
||
{ id: 'sh:up', label: 'cd ..' },
|
||
{ id: 'sh:home', label: 'cd ~' },
|
||
{ id: 'sh:pwd', label: 'pwd' }
|
||
])
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Shell · ' + discordShPrompt(ctx, rec),
|
||
desc:
|
||
(note ? note + '\n' : '') +
|
||
'Type **`/run cmd:`** and keep going — `cd`, pipes, `&&`, redirects, and `$VAR` all work.\n' +
|
||
'Working directory sticks for this Discord user.',
|
||
fields: fields,
|
||
color: rec.lastCmd && rec.lastExit ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR_OK,
|
||
footer: discordShPrompt(ctx, rec) + ' · /run cmd:'
|
||
}),
|
||
{ components: [histSel, acts].filter(Boolean) }
|
||
)
|
||
}
|
||
|
||
async function discordCmdHandleRun(ctx, interaction, raw) {
|
||
const rec = discordShGet(interaction, ctx)
|
||
const cmd = String(raw || '').trim()
|
||
if (!cmd) return discordShHud(ctx, interaction, '')
|
||
if (cmd.length > 4000) {
|
||
return { text: 'Command line is too long (max 4000).', ephemeral: true }
|
||
}
|
||
const first = discordShFirstWord(cmd)
|
||
if (first === 'exit') {
|
||
rec.lastCmd = cmd
|
||
rec.lastExit = 0
|
||
return discordShHud(ctx, interaction, 'Stayed attached — `exit` does not stop the Discord bot.')
|
||
}
|
||
if (first === 'logout' || first === 'login') {
|
||
return {
|
||
text:
|
||
'`login` / `logout` need the guest TTY. This Discord shell stays as **' +
|
||
discordSessionUser(ctx) +
|
||
'**.',
|
||
ephemeral: true
|
||
}
|
||
}
|
||
if (typeof ctx.execLine !== 'function') {
|
||
return { text: 'execLine unavailable in this session', ephemeral: true }
|
||
}
|
||
await discordShPrepare(ctx, rec)
|
||
const t0 = Date.now()
|
||
let out = ''
|
||
let errNote = ''
|
||
try {
|
||
out = await discordCmdCapture(ctx, function () {
|
||
return ctx.execLine(cmd, { timeoutMs: BARE_OS_DISCORD_SH_EXEC_MS })
|
||
})
|
||
} catch (err) {
|
||
errNote = String((err && err.message) || err)
|
||
out = (out ? out + '\n' : '') + errNote
|
||
}
|
||
const ms = Date.now() - t0
|
||
const exit = ctx.exitCode == null ? (errNote ? 1 : 0) : Number(ctx.exitCode) || 0
|
||
discordShHarvest(ctx, rec)
|
||
discordShPushHist(rec, cmd, exit, ms, out)
|
||
const tty = BARE_OS_DISCORD_SH_TTY[first]
|
||
if (!String(out || '').trim() && (first === 'cd' || first === 'export' || first === 'unset' || first === 'alias')) {
|
||
return discordShHud(ctx, interaction, '`$ ' + cmd + '` · exit **' + exit + '** · ' + ms + 'ms')
|
||
}
|
||
const parsed = discordTryJson(out)
|
||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && String(out).length < 800) {
|
||
return discordPrettySnapshot(discordShPrompt(ctx, rec) + ' $ ' + cmd, parsed, {
|
||
color: exit ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR_OK
|
||
})
|
||
}
|
||
const lead =
|
||
'`' +
|
||
discordShPrompt(ctx, rec) +
|
||
'`\n`$ ' +
|
||
cmd +
|
||
'`' +
|
||
(tty ? '\n_Interactive TUI — needs a real terminal; output may be empty._' : '')
|
||
if (!String(out || '').trim()) {
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Shell',
|
||
desc: lead + '\n(no output)',
|
||
color: exit ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR_OK,
|
||
footer: 'exit ' + exit + ' · ' + ms + 'ms · ' + discordShPrettyCwd(ctx, rec.cwd)
|
||
}),
|
||
{
|
||
components: [
|
||
discordButtons([
|
||
{ id: 'sh:again', label: 'Repeat', style: 1 },
|
||
{ id: 'sh:hud', label: 'Shell' }
|
||
])
|
||
]
|
||
}
|
||
)
|
||
}
|
||
return {
|
||
more: {
|
||
title: 'Shell',
|
||
lead: lead,
|
||
body: discordCmdRedact(out),
|
||
fence: true,
|
||
color: exit ? BARE_OS_DISCORD_COLOR_WARN : BARE_OS_DISCORD_COLOR_OK,
|
||
footer: 'exit ' + exit + ' · ' + ms + 'ms · ' + discordShPrettyCwd(ctx, rec.cwd),
|
||
components: [
|
||
discordButtons([
|
||
{ id: 'sh:again', label: 'Repeat', style: 1 },
|
||
{ id: 'sh:up', label: 'cd ..' },
|
||
{ id: 'sh:home', label: 'cd ~' },
|
||
{ id: 'sh:hud', label: 'Shell' }
|
||
])
|
||
]
|
||
}
|
||
}
|
||
}
|
||
|
||
async function discordShAction(ctx, interaction, id) {
|
||
const rec = discordShGet(interaction, ctx)
|
||
if (id === 'sh:hud' || id === 'run:compose') {
|
||
return discordSendResult(ctx, interaction, discordShHud(ctx, interaction, ''))
|
||
}
|
||
if (id === 'sh:pwd') {
|
||
return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, 'pwd'))
|
||
}
|
||
if (id === 'sh:again') {
|
||
if (!rec.lastCmd) {
|
||
return discordSendResult(ctx, interaction, discordShHud(ctx, interaction, 'No command to repeat.'))
|
||
}
|
||
return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, rec.lastCmd))
|
||
}
|
||
if (id === 'sh:up') {
|
||
return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, 'cd ..'))
|
||
}
|
||
if (id === 'sh:home') {
|
||
return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, 'cd'))
|
||
}
|
||
if (id === 'sh:hist') {
|
||
const i = Number((interaction.values && interaction.values[0]) || 0)
|
||
const hit = rec.hist[i]
|
||
if (!hit) return discordSendResult(ctx, interaction, discordShHud(ctx, interaction, ''))
|
||
return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, hit.cmd))
|
||
}
|
||
return discordSendResult(ctx, interaction, discordShHud(ctx, interaction, ''))
|
||
}
|
||
|
||
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'
|
||
])
|
||
})
|
||
const body = discordCmdRedact(String(out || ''))
|
||
if (!body.trim()) {
|
||
return discordResult(
|
||
discordEmbed({ title: 'Journal · ' + unit, desc: 'Empty journal for `' + unit + '`.' })
|
||
)
|
||
}
|
||
return { more: { title: 'Journal · ' + unit, body: body, fence: true } }
|
||
}
|
||
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'))
|
||
const body = discordCmdRedact(String(t || ''))
|
||
if (!body.trim()) {
|
||
return discordResult(discordEmbed({ title: 'Journal', desc: 'No journal in `/var/log/bare-os`.' }))
|
||
}
|
||
return { more: { title: 'Journal', body: body, fence: true } }
|
||
}
|
||
|
||
function discordPanel(ctx) {
|
||
const user = discordSessionUser(ctx)
|
||
const e = discordCmdEnv(ctx)
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Bare OS control panel',
|
||
color: BARE_OS_DISCORD_COLOR,
|
||
desc:
|
||
'Signed in as **' +
|
||
user +
|
||
'** · `' +
|
||
(e.HOME || '/home/' + user) +
|
||
'`\nPick a button, or use a slash command.',
|
||
fields: [
|
||
discordField('User', user, true),
|
||
discordField('Identity', e.BARE_OS_IDENTITY || (ctx.identity && ctx.identity.state) || '', true),
|
||
discordField('Host', e.HOSTNAME || 'bare-os', true)
|
||
]
|
||
})
|
||
)
|
||
}
|
||
|
||
function discordCmdOpt(s, name, desc, required, autocomplete) {
|
||
if (!s || 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)
|
||
if (autocomplete && typeof o.setAutocomplete === 'function') o.setAutocomplete(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
|
||
}
|
||
|
||
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 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({
|
||
custom_id: spec.id,
|
||
title: String(spec.title || 'Bare OS').slice(0, 45),
|
||
components: [
|
||
{
|
||
type: 1,
|
||
components: [
|
||
{
|
||
type: 4,
|
||
custom_id: spec.field || 'text',
|
||
label: String(spec.label || 'Value').slice(0, 45),
|
||
style: spec.paragraph ? 2 : 1,
|
||
required: true,
|
||
max_length: spec.max || 200,
|
||
placeholder: spec.placeholder || ''
|
||
}
|
||
]
|
||
}
|
||
]
|
||
})
|
||
}
|
||
|
||
async function discordRouteCommand(ctx, name, sub, opt) {
|
||
if (name === 'panel') return discordPanel(ctx)
|
||
if (name === 'ping' || (name === 'bare' && (sub === 'ping' || !sub))) {
|
||
return discordCmdHandleBare(ctx, 'ping')
|
||
}
|
||
if (name === 'bare') return discordCmdHandleBare(ctx, sub)
|
||
if (name === 'sys') return discordCmdHandleSys(ctx, sub)
|
||
if (name === 'svc') return discordCmdHandleSvc(ctx, sub, opt('unit'))
|
||
if (name === 'fs') return discordCmdHandleFs(ctx, sub, opt('path'), opt('lines'))
|
||
if (name === 'net') return discordCmdHandleNet(ctx, sub)
|
||
if (name === 'man') return discordCmdHandleMan(ctx, opt('page'))
|
||
if (name === 'say') {
|
||
const text = opt('text')
|
||
if (!text) return { modal: 'say' }
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'say',
|
||
desc: discordCmdFence(discordCmdSayBox(text))
|
||
})
|
||
)
|
||
}
|
||
if (name === 'run') return { run: opt('cmd') || '' }
|
||
if (name === 'journal') return discordCmdHandleJournal(ctx, opt('unit'))
|
||
if (name === 'edit') {
|
||
const path = opt('path')
|
||
if (!path) return { modal: 'edit:path' }
|
||
return { edit: path }
|
||
}
|
||
if (name === 'create') {
|
||
const path = opt('path')
|
||
if (!path) return { createPick: true }
|
||
return { create: path }
|
||
}
|
||
if (name === 'files' || name === 'browse') {
|
||
return { files: opt('path') || '' }
|
||
}
|
||
if (name === 'settings') return { settings: true }
|
||
return { text: 'unknown command: /' + name, ephemeral: true }
|
||
}
|
||
|
||
async function discordBeginEdit(ctx, interaction, rawPath) {
|
||
const p = discordCmdPathWriteOk(ctx, rawPath)
|
||
if (!p) {
|
||
return discordSendResult(ctx, interaction, {
|
||
text:
|
||
'Cannot edit that path. Writable locations: `~/…`, `/home/' +
|
||
discordSessionUser(ctx) +
|
||
'/…`, `/tmp/…`. Token files are blocked.',
|
||
ephemeral: true
|
||
})
|
||
}
|
||
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') {
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'vfs.writeFile is unavailable in this session',
|
||
ephemeral: true
|
||
})
|
||
}
|
||
let text = await discordCmdReadText(ctx, p)
|
||
const created = !text
|
||
if (text && discordLooksBinary(text)) {
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'Refusing to open a binary file in a Discord modal: `' + p + '`',
|
||
ephemeral: true
|
||
})
|
||
}
|
||
const split = discordEditChunks(text)
|
||
discordEditPut(interaction, {
|
||
path: p,
|
||
origLen: split.origLen,
|
||
truncated: split.truncated,
|
||
created: created
|
||
})
|
||
if (typeof interaction.showModal !== 'function') {
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'This client cannot show Discord modals.',
|
||
ephemeral: true
|
||
})
|
||
}
|
||
try {
|
||
await interaction.showModal(discordEditModalPayload(p, split.chunks))
|
||
} catch (err) {
|
||
if (ctx.console && typeof ctx.console.error === 'function') {
|
||
ctx.console.error(
|
||
'discord-bot: edit modal failed: ' + ((err && err.message) || err)
|
||
)
|
||
}
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'Could not open the editor modal: ' + ((err && err.message) || err),
|
||
ephemeral: true
|
||
})
|
||
}
|
||
}
|
||
|
||
async function discordEditSaveFromModal(ctx, interaction) {
|
||
const rec = discordEditGet(interaction)
|
||
if (!rec || !rec.path) {
|
||
return {
|
||
text: 'Edit session expired (15 minutes). Run `/edit` again.',
|
||
ephemeral: true
|
||
}
|
||
}
|
||
const p = discordCmdPathWriteOk(ctx, rec.path)
|
||
if (!p) {
|
||
return { text: 'Write not allowed for `' + rec.path + '`', ephemeral: true }
|
||
}
|
||
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') {
|
||
return { text: 'vfs.writeFile is unavailable', ephemeral: true }
|
||
}
|
||
const parts = []
|
||
for (let i = 0; i < BARE_OS_DISCORD_EDIT_MAX_CHUNKS; i++) {
|
||
try {
|
||
if (interaction.fields && typeof interaction.fields.getTextInputValue === 'function') {
|
||
parts.push(String(interaction.fields.getTextInputValue('c' + i) || ''))
|
||
}
|
||
} catch {
|
||
/* missing chunk */
|
||
}
|
||
}
|
||
const body = parts.join('')
|
||
if (rec.created && (await discordFileExists(ctx, p))) {
|
||
return {
|
||
text: '`' + p + '` already exists. Use `/edit` to change it.',
|
||
ephemeral: true
|
||
}
|
||
}
|
||
try {
|
||
await ctx.vfs.writeFile(p, discordTextToBuf(ctx, body))
|
||
} catch (err) {
|
||
return {
|
||
text: 'Save failed: ' + ((err && err.message) || err),
|
||
ephemeral: true
|
||
}
|
||
}
|
||
discordEditClear(interaction)
|
||
const lines = body ? body.split(/\r?\n/).length : 0
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: rec.created ? 'Created ' + p : 'Saved ' + p,
|
||
color: BARE_OS_DISCORD_COLOR_OK,
|
||
fields: [
|
||
discordField('Path', p, false),
|
||
discordField('Bytes', String(body.length), true),
|
||
discordField('Lines', String(lines), true),
|
||
discordField('User', discordSessionUser(ctx), true)
|
||
],
|
||
desc: rec.truncated
|
||
? 'Original file was larger than the Discord modal cap (20 000 characters). Only the loaded window was saved.'
|
||
: 'Written through `ctx.vfs.writeFile` as **' +
|
||
discordSessionUser(ctx) +
|
||
'**.'
|
||
}),
|
||
{
|
||
components: [
|
||
discordButtons([
|
||
{ id: 'edit:open', label: 'Edit again', style: 1 },
|
||
{ id: 'edit:new', label: 'Edit another…' },
|
||
{ id: 'create:new', label: 'Create another…', style: 3 }
|
||
])
|
||
].filter(Boolean),
|
||
editPath: p
|
||
}
|
||
)
|
||
}
|
||
|
||
function discordCreatePicker(ctx) {
|
||
const choices = discordSuggestEditPaths(ctx, '')
|
||
const sel = discordSelect(
|
||
'create:pick',
|
||
'Choose a new file path…',
|
||
choices.map(function (c) {
|
||
return { label: c.name, value: c.value, description: 'Create this path' }
|
||
})
|
||
)
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Create a file',
|
||
desc:
|
||
'Pick a suggested path, or **Custom path…** to type one. Then enter the contents in a Discord modal.\nWritable: `~/…`, `/tmp/…`, `/home/' +
|
||
discordSessionUser(ctx) +
|
||
'/…`.'
|
||
}),
|
||
{
|
||
components: [
|
||
sel,
|
||
discordButtons([
|
||
{ id: 'create:custom', label: 'Custom path…', style: 1 }
|
||
])
|
||
].filter(Boolean)
|
||
}
|
||
)
|
||
}
|
||
|
||
var BARE_OS_DISCORD_SET_SESSIONS = Object.create(null)
|
||
var BARE_OS_DISCORD_SET_TTL_MS = BARE_OS_DISCORD_IDLE_MS
|
||
var BARE_OS_DISCORD_THEME_FALLBACK = [
|
||
'catppuccin_mocha',
|
||
'default',
|
||
'dracula',
|
||
'github_dark',
|
||
'gruvbox_dark',
|
||
'nord',
|
||
'solarized_dark',
|
||
'tokyo_night'
|
||
]
|
||
var BARE_OS_DISCORD_SET_PAGE = 8
|
||
var BARE_OS_DISCORD_SET_SECRET_RE = /token|secret|password|passwd|api[_-]?key|authorization|sasl/i
|
||
var BARE_OS_DISCORD_SET_GROUPS = [
|
||
{ id: 'appearance', label: 'Appearance', description: 'Theme, colors, TUI' },
|
||
{ id: 'shell', label: 'Shell', description: 'Live flags + completion' },
|
||
{ id: 'session', label: 'Session', description: 'Hostname, locale, editor' },
|
||
{ id: 'discord', label: 'Discord', description: 'Guild, whitelist (not token)' },
|
||
{ id: 'agent', label: 'Agent', description: '~/.agent/config.json' },
|
||
{ id: 'irc', label: 'IRC', description: '~/.irc/config.json' },
|
||
{ id: 'aliases', label: 'Aliases', description: 'shellAliases + ~/.barerc' }
|
||
]
|
||
var BARE_OS_DISCORD_SETTINGS = [
|
||
{
|
||
id: 'theme',
|
||
group: 'appearance',
|
||
label: 'Theme',
|
||
kind: 'theme',
|
||
env: 'BARE_OS_THEME',
|
||
persist: 'theme',
|
||
live: 'now (prompt / ls)'
|
||
},
|
||
{
|
||
id: 'color_depth',
|
||
group: 'appearance',
|
||
label: 'Color depth',
|
||
kind: 'enum',
|
||
env: 'BARE_OS_COLOR_DEPTH',
|
||
values: ['truecolor', '256', '16', '8'],
|
||
persist: 'export',
|
||
live: 'now (reapply theme)'
|
||
},
|
||
{
|
||
id: 'ls_lock',
|
||
group: 'appearance',
|
||
label: 'Lock LS_COLORS',
|
||
kind: 'bool',
|
||
env: 'BARE_OS_LS_COLORS_LOCKED',
|
||
persist: 'export',
|
||
live: 'now (reapply theme)'
|
||
},
|
||
{
|
||
id: 'tui_noalt',
|
||
group: 'appearance',
|
||
label: 'TUI no altscreen',
|
||
kind: 'bool',
|
||
env: 'BARE_OS_TUI_NO_ALTSCREEN',
|
||
persist: 'export',
|
||
live: 'next TUI'
|
||
},
|
||
{
|
||
id: 'no_color',
|
||
group: 'appearance',
|
||
label: 'NO_COLOR',
|
||
kind: 'bool',
|
||
env: 'NO_COLOR',
|
||
persist: 'export',
|
||
live: 'now (reapply theme)'
|
||
},
|
||
{
|
||
id: 'dircolors',
|
||
group: 'appearance',
|
||
label: 'dircolors file',
|
||
kind: 'string',
|
||
env: 'BARE_OS_DIRCOLORS',
|
||
persist: 'export',
|
||
live: 'now (reapply theme)'
|
||
},
|
||
{
|
||
id: 'compact',
|
||
group: 'shell',
|
||
label: 'Compact completion',
|
||
kind: 'bool',
|
||
env: 'BARE_OS_COMPACT_MENU',
|
||
persist: 'export',
|
||
live: 'next Tab'
|
||
},
|
||
{
|
||
id: 'errexit',
|
||
group: 'shell',
|
||
label: 'errexit (set -e)',
|
||
kind: 'bool',
|
||
env: 'BARE_OS_SHELL_ERREXIT',
|
||
persist: 'export',
|
||
live: 'next command'
|
||
},
|
||
{
|
||
id: 'nounset',
|
||
group: 'shell',
|
||
label: 'nounset (set -u)',
|
||
kind: 'bool',
|
||
env: 'BARE_OS_SHELL_NOUNSET',
|
||
persist: 'export',
|
||
live: 'next command'
|
||
},
|
||
{
|
||
id: 'noglob',
|
||
group: 'shell',
|
||
label: 'noglob (set -f)',
|
||
kind: 'bool',
|
||
env: 'BARE_OS_SHELL_NOGLOB',
|
||
persist: 'export',
|
||
live: 'next command'
|
||
},
|
||
{
|
||
id: 'pipefail',
|
||
group: 'shell',
|
||
label: 'pipefail',
|
||
kind: 'bool',
|
||
env: 'BARE_OS_SHELL_PIPEFAIL',
|
||
persist: 'export',
|
||
live: 'next pipeline'
|
||
},
|
||
{
|
||
id: 'pipestatus',
|
||
group: 'shell',
|
||
label: 'PIPESTATUS',
|
||
kind: 'bool',
|
||
env: 'BARE_OS_SHELL_PIPESTATUS',
|
||
persist: 'export',
|
||
live: 'next pipeline'
|
||
},
|
||
{
|
||
id: 'posix',
|
||
group: 'shell',
|
||
label: 'POSIX mode',
|
||
kind: 'bool',
|
||
env: 'BARE_OS_SHELL_POSIX_MODE',
|
||
persist: 'export',
|
||
live: 'next command'
|
||
},
|
||
{
|
||
id: 'grouping',
|
||
group: 'shell',
|
||
label: 'Grouping ( … )',
|
||
kind: 'bool',
|
||
env: 'BARE_OS_SHELL_GROUPING',
|
||
persist: 'export',
|
||
live: 'next command'
|
||
},
|
||
{
|
||
id: 'dbracket',
|
||
group: 'shell',
|
||
label: '[[ … ]] tests',
|
||
kind: 'bool',
|
||
env: 'BARE_OS_SHELL_DOUBLE_BRACKET',
|
||
persist: 'export',
|
||
live: 'next command'
|
||
},
|
||
{
|
||
id: 'cmdsubst',
|
||
group: 'shell',
|
||
label: 'Command subst',
|
||
kind: 'bool',
|
||
env: 'BARE_OS_SHELL_CMDSUBST',
|
||
persist: 'export',
|
||
live: 'next $(…)'
|
||
},
|
||
{
|
||
id: 'streaming',
|
||
group: 'shell',
|
||
label: 'Pipeline streaming',
|
||
kind: 'bool',
|
||
env: 'BARE_OS_SHELL_STREAMING',
|
||
persist: 'export',
|
||
live: 'next pipeline'
|
||
},
|
||
{
|
||
id: 'brace',
|
||
group: 'shell',
|
||
label: 'Brace expansion',
|
||
kind: 'bool',
|
||
env: 'BARE_OS_SHELL_BRACE_EXPANSION',
|
||
persist: 'export',
|
||
live: 'next command'
|
||
},
|
||
{
|
||
id: 'paramexp',
|
||
group: 'shell',
|
||
label: 'Param expansion',
|
||
kind: 'bool',
|
||
env: 'BARE_OS_SHELL_PARAM_EXPANSION',
|
||
persist: 'export',
|
||
live: 'next ${…}'
|
||
},
|
||
{
|
||
id: 'until',
|
||
group: 'shell',
|
||
label: 'until loops',
|
||
kind: 'bool',
|
||
env: 'BARE_OS_SHELL_UNTIL',
|
||
persist: 'export',
|
||
live: 'next command'
|
||
},
|
||
{
|
||
id: 'loopctl',
|
||
group: 'shell',
|
||
label: 'break / continue',
|
||
kind: 'bool',
|
||
env: 'BARE_OS_SHELL_LOOP_CONTROL',
|
||
persist: 'export',
|
||
live: 'next loop'
|
||
},
|
||
{
|
||
id: 'readb',
|
||
group: 'shell',
|
||
label: 'read builtin',
|
||
kind: 'bool',
|
||
env: 'BARE_OS_SHELL_READ_BUILTIN',
|
||
persist: 'export',
|
||
live: 'next read'
|
||
},
|
||
{
|
||
id: 'hostname',
|
||
group: 'session',
|
||
label: 'Hostname',
|
||
kind: 'string',
|
||
env: 'HOSTNAME',
|
||
persist: 'hostname',
|
||
live: 'now (needs BARE_OS_HOSTNAME_SET=1)'
|
||
},
|
||
{
|
||
id: 'tz',
|
||
group: 'session',
|
||
label: 'Timezone (TZ)',
|
||
kind: 'string',
|
||
env: 'TZ',
|
||
persist: 'export',
|
||
live: 'next date'
|
||
},
|
||
{
|
||
id: 'lang',
|
||
group: 'session',
|
||
label: 'Locale (LANG)',
|
||
kind: 'string',
|
||
env: 'LANG',
|
||
persist: 'export',
|
||
live: 'next command'
|
||
},
|
||
{
|
||
id: 'editor',
|
||
group: 'session',
|
||
label: 'EDITOR',
|
||
kind: 'string',
|
||
env: 'EDITOR',
|
||
persist: 'export',
|
||
live: 'next editor'
|
||
},
|
||
{
|
||
id: 'pager',
|
||
group: 'session',
|
||
label: 'PAGER',
|
||
kind: 'string',
|
||
env: 'PAGER',
|
||
persist: 'export',
|
||
live: 'next pager'
|
||
},
|
||
{
|
||
id: 'guild',
|
||
group: 'discord',
|
||
label: 'Guild id',
|
||
kind: 'string',
|
||
env: 'DISCORD_GUILD_ID',
|
||
persist: 'discord',
|
||
live: 'next slash register'
|
||
},
|
||
{
|
||
id: 'whitelist',
|
||
group: 'discord',
|
||
label: 'User id whitelist',
|
||
kind: 'string',
|
||
env: 'DISCORD_ID_WHITELIST',
|
||
persist: 'discord',
|
||
live: 'next command'
|
||
},
|
||
{
|
||
id: 'dbg',
|
||
group: 'discord',
|
||
label: 'Debug logs',
|
||
kind: 'bool',
|
||
env: 'DISCORD_DEBUG',
|
||
persist: 'discord',
|
||
live: 'next bot start'
|
||
},
|
||
{
|
||
id: 'msgc',
|
||
group: 'discord',
|
||
label: 'Message Content',
|
||
kind: 'bool',
|
||
env: 'DISCORD_MESSAGE_CONTENT',
|
||
persist: 'discord',
|
||
live: 'next bot start'
|
||
},
|
||
{
|
||
id: 'loginms',
|
||
group: 'discord',
|
||
label: 'Login timeout ms',
|
||
kind: 'number',
|
||
env: 'DISCORD_LOGIN_TIMEOUT_MS',
|
||
persist: 'discord',
|
||
live: 'next bot start'
|
||
},
|
||
{
|
||
id: 'ag_backend',
|
||
group: 'agent',
|
||
label: 'Backend',
|
||
kind: 'enum',
|
||
jsonKey: 'backend',
|
||
values: ['qvac', 'rest'],
|
||
persist: 'agent',
|
||
live: 'next agent'
|
||
},
|
||
{
|
||
id: 'ag_profile',
|
||
group: 'agent',
|
||
label: 'QVAC profile',
|
||
kind: 'enum',
|
||
jsonKey: 'qvac_profile',
|
||
values: ['recommended', 'strong', 'tool-tiny'],
|
||
persist: 'agent',
|
||
live: 'next agent'
|
||
},
|
||
{
|
||
id: 'ag_model',
|
||
group: 'agent',
|
||
label: 'Model',
|
||
kind: 'string',
|
||
jsonKey: 'model',
|
||
persist: 'agent',
|
||
live: 'next agent'
|
||
},
|
||
{
|
||
id: 'ag_qvac_model',
|
||
group: 'agent',
|
||
label: 'QVAC model',
|
||
kind: 'string',
|
||
jsonKey: 'qvac_model',
|
||
persist: 'agent',
|
||
live: 'next agent'
|
||
},
|
||
{
|
||
id: 'ag_owner',
|
||
group: 'agent',
|
||
label: 'Owner name',
|
||
kind: 'string',
|
||
jsonKey: 'owner_name',
|
||
persist: 'agent',
|
||
live: 'next agent'
|
||
},
|
||
{
|
||
id: 'ag_label',
|
||
group: 'agent',
|
||
label: 'Agent label',
|
||
kind: 'string',
|
||
jsonKey: 'agent_label',
|
||
persist: 'agent',
|
||
live: 'next agent'
|
||
},
|
||
{
|
||
id: 'ag_url',
|
||
group: 'agent',
|
||
label: 'REST base URL',
|
||
kind: 'string',
|
||
jsonKey: 'rest_base_url',
|
||
persist: 'agent',
|
||
live: 'next agent'
|
||
},
|
||
{
|
||
id: 'ag_temp',
|
||
group: 'agent',
|
||
label: 'Temperature',
|
||
kind: 'number',
|
||
jsonKey: 'temperature',
|
||
persist: 'agent',
|
||
live: 'next agent'
|
||
},
|
||
{
|
||
id: 'ag_maxtok',
|
||
group: 'agent',
|
||
label: 'Max tokens',
|
||
kind: 'number',
|
||
jsonKey: 'max_tokens',
|
||
persist: 'agent',
|
||
live: 'next agent'
|
||
},
|
||
{
|
||
id: 'ag_maxiter',
|
||
group: 'agent',
|
||
label: 'Max iterations',
|
||
kind: 'number',
|
||
jsonKey: 'max_iterations',
|
||
persist: 'agent',
|
||
live: 'next agent'
|
||
},
|
||
{
|
||
id: 'ag_stream',
|
||
group: 'agent',
|
||
label: 'Stream',
|
||
kind: 'bool',
|
||
jsonKey: 'stream',
|
||
persist: 'agent',
|
||
live: 'next agent'
|
||
},
|
||
{
|
||
id: 'ag_del',
|
||
group: 'agent',
|
||
label: 'Allow delete',
|
||
kind: 'bool',
|
||
jsonKey: 'allow_delete',
|
||
persist: 'agent',
|
||
live: 'next agent'
|
||
},
|
||
{
|
||
id: 'ag_reason',
|
||
group: 'agent',
|
||
label: 'Show reasoning',
|
||
kind: 'bool',
|
||
jsonKey: 'show_reasoning',
|
||
persist: 'agent',
|
||
live: 'next agent'
|
||
},
|
||
{
|
||
id: 'ag_reason_mode',
|
||
group: 'agent',
|
||
label: 'Reasoning mode',
|
||
kind: 'enum',
|
||
jsonKey: 'reasoning_mode',
|
||
values: ['off', 'summary', 'trace'],
|
||
persist: 'agent',
|
||
live: 'next agent'
|
||
},
|
||
{
|
||
id: 'ag_bridge',
|
||
group: 'agent',
|
||
label: 'Bridge mutations',
|
||
kind: 'bool',
|
||
jsonKey: 'allow_bridge_mutations',
|
||
persist: 'agent',
|
||
live: 'next agent'
|
||
},
|
||
{
|
||
id: 'ag_notify',
|
||
group: 'agent',
|
||
label: 'Host notifications',
|
||
kind: 'bool',
|
||
jsonKey: 'allow_host_notifications',
|
||
persist: 'agent',
|
||
live: 'next agent'
|
||
},
|
||
{
|
||
id: 'ag_hostact',
|
||
group: 'agent',
|
||
label: 'Host actions',
|
||
kind: 'bool',
|
||
jsonKey: 'allow_host_actions',
|
||
persist: 'agent',
|
||
live: 'next agent'
|
||
},
|
||
{
|
||
id: 'irc_nick',
|
||
group: 'irc',
|
||
label: 'Nick',
|
||
kind: 'string',
|
||
jsonKey: 'nick',
|
||
persist: 'irc',
|
||
live: 'next irc'
|
||
},
|
||
{
|
||
id: 'irc_join',
|
||
group: 'irc',
|
||
label: 'Autojoin',
|
||
kind: 'string',
|
||
jsonKey: 'autojoin',
|
||
persist: 'irc',
|
||
live: 'next irc'
|
||
},
|
||
{
|
||
id: 'irc_share',
|
||
group: 'irc',
|
||
label: 'Share channels',
|
||
kind: 'bool',
|
||
jsonKey: 'shareChannels',
|
||
persist: 'irc',
|
||
live: 'next irc'
|
||
}
|
||
]
|
||
|
||
function discordSetKey(interaction) {
|
||
return discordInteractionUserId(interaction) || 'anon'
|
||
}
|
||
|
||
function discordSetGet(interaction) {
|
||
const now = Date.now()
|
||
for (const k in BARE_OS_DISCORD_SET_SESSIONS) {
|
||
const r = BARE_OS_DISCORD_SET_SESSIONS[k]
|
||
if (!r || now - r.atMs > BARE_OS_DISCORD_SET_TTL_MS) delete BARE_OS_DISCORD_SET_SESSIONS[k]
|
||
}
|
||
let rec = BARE_OS_DISCORD_SET_SESSIONS[discordSetKey(interaction)]
|
||
if (!rec) {
|
||
rec = { group: 'appearance', sel: '', page: 0, pending: '', atMs: now }
|
||
BARE_OS_DISCORD_SET_SESSIONS[discordSetKey(interaction)] = rec
|
||
}
|
||
rec.atMs = now
|
||
if (rec.page == null) rec.page = 0
|
||
if (rec.pending == null) rec.pending = ''
|
||
return rec
|
||
}
|
||
|
||
function discordSettingsGet(interaction) {
|
||
return discordSetGet(interaction)
|
||
}
|
||
|
||
function discordSettingsSpec(id) {
|
||
for (let i = 0; i < BARE_OS_DISCORD_SETTINGS.length; i++) {
|
||
if (BARE_OS_DISCORD_SETTINGS[i].id === id) return BARE_OS_DISCORD_SETTINGS[i]
|
||
}
|
||
return null
|
||
}
|
||
|
||
function discordSettingsFind(id) {
|
||
return discordSettingsSpec(id)
|
||
}
|
||
|
||
function discordSettingsInGroup(group) {
|
||
const out = []
|
||
for (let i = 0; i < BARE_OS_DISCORD_SETTINGS.length; i++) {
|
||
if (BARE_OS_DISCORD_SETTINGS[i].group === group) out.push(BARE_OS_DISCORD_SETTINGS[i])
|
||
}
|
||
return out
|
||
}
|
||
|
||
function discordSettingsPageOf(items, page) {
|
||
const size = BARE_OS_DISCORD_SET_PAGE
|
||
const pages = Math.max(1, Math.ceil(items.length / size) || 1)
|
||
const p = Math.min(Math.max(0, page | 0), pages - 1)
|
||
return { slice: items.slice(p * size, p * size + size), page: p, pages: pages }
|
||
}
|
||
|
||
function discordThemeNames(ctx) {
|
||
if (ctx && typeof ctx.bareOsListThemes === 'function') {
|
||
try {
|
||
const n = ctx.bareOsListThemes()
|
||
if (Array.isArray(n) && n.length) return n.slice().sort()
|
||
} catch {
|
||
/* fall through */
|
||
}
|
||
}
|
||
return BARE_OS_DISCORD_THEME_FALLBACK.slice()
|
||
}
|
||
|
||
function discordEnvOn(raw) {
|
||
if (raw === true) return true
|
||
const v = String(raw == null ? '' : raw).trim().toLowerCase()
|
||
return v === '1' || v === 'true' || v === 'yes' || v === 'on'
|
||
}
|
||
|
||
function discordSettingsIsSecretKey(key) {
|
||
return BARE_OS_DISCORD_SET_SECRET_RE.test(String(key || ''))
|
||
}
|
||
|
||
function discordSettingsAliasName(raw) {
|
||
const t = String(raw || '').trim()
|
||
const eq = t.indexOf('=')
|
||
const name = (eq >= 0 ? t.slice(0, eq) : t).trim()
|
||
if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(name)) return ''
|
||
return name
|
||
}
|
||
|
||
function discordSettingsAliasValue(raw) {
|
||
const t = String(raw || '').trim()
|
||
const eq = t.indexOf('=')
|
||
if (eq < 0) return ''
|
||
let v = t.slice(eq + 1).trim()
|
||
if (
|
||
(v.charAt(0) === "'" && v.charAt(v.length - 1) === "'") ||
|
||
(v.charAt(0) === '"' && v.charAt(v.length - 1) === '"')
|
||
) {
|
||
v = v.slice(1, -1)
|
||
}
|
||
return v
|
||
}
|
||
|
||
function discordSettingsQuoteAlias(value) {
|
||
const v = String(value == null ? '' : value)
|
||
if (/^[A-Za-z0-9_./:+@%-]+$/.test(v)) return v
|
||
return "'" + v.replace(/'/g, "'\\''") + "'"
|
||
}
|
||
|
||
async function discordEnsureDir(ctx, path) {
|
||
if (!ctx || !ctx.vfs || typeof ctx.vfs.mkdir !== 'function') return
|
||
try {
|
||
await ctx.vfs.mkdir(path, { recursive: true })
|
||
} catch {
|
||
/* already exists or vfs without mkdir */
|
||
}
|
||
}
|
||
|
||
async function discordReadJsonFile(ctx, path) {
|
||
const t = await discordCmdReadText(ctx, path)
|
||
const j = discordTryJson(t)
|
||
return j && typeof j === 'object' && !Array.isArray(j) ? j : {}
|
||
}
|
||
|
||
async function discordWriteJsonFile(ctx, path, obj) {
|
||
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') {
|
||
throw new Error('writeFile unavailable')
|
||
}
|
||
const parent = discordParentPath(path)
|
||
if (parent && parent !== path) await discordEnsureDir(ctx, parent)
|
||
const sanitized = {}
|
||
const keys = Object.keys(obj || {})
|
||
for (let i = 0; i < keys.length; i++) {
|
||
const k = keys[i]
|
||
if (discordSettingsIsSecretKey(k)) {
|
||
sanitized[k] = obj[k]
|
||
continue
|
||
}
|
||
sanitized[k] = obj[k]
|
||
}
|
||
const body = JSON.stringify(sanitized, null, 2) + '\n'
|
||
await ctx.vfs.writeFile(path, discordTextToBuf(ctx, body))
|
||
}
|
||
|
||
async function discordPersistBarercLine(ctx, matcher, line) {
|
||
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') {
|
||
throw new Error('writeFile unavailable')
|
||
}
|
||
const text = await discordCmdReadText(ctx, '~/.barerc')
|
||
const lines = text ? text.split(/\r?\n/) : []
|
||
const drop = line == null || line === ''
|
||
let replaced = false
|
||
const out = []
|
||
for (let i = 0; i < lines.length; i++) {
|
||
if (matcher.test(lines[i])) {
|
||
if (!replaced && !drop) {
|
||
out.push(line)
|
||
replaced = true
|
||
}
|
||
} else out.push(lines[i])
|
||
}
|
||
if (!replaced && !drop) {
|
||
if (out.length && String(out[out.length - 1]).trim() !== '') out.push('')
|
||
out.push(line)
|
||
}
|
||
while (out.length && String(out[out.length - 1]).trim() === '') out.pop()
|
||
let next = out.join('\n')
|
||
if (next) next += '\n'
|
||
await ctx.vfs.writeFile('~/.barerc', discordTextToBuf(ctx, next))
|
||
}
|
||
|
||
async function discordPersistDiscordEnvKey(ctx, key, value) {
|
||
if (discordSettingsIsSecretKey(key) || key === 'DISCORD_TOKEN') {
|
||
throw new Error('refusing to write a secret key')
|
||
}
|
||
const path = '~/.discord/.env'
|
||
let text = await discordCmdReadText(ctx, path)
|
||
const line = key + '=' + String(value)
|
||
const re = new RegExp('^\\s*(?:export\\s+)?' + key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '=.*$', 'm')
|
||
if (re.test(text)) text = text.replace(re, line)
|
||
else text = (text && !/\n$/.test(text) ? text + '\n' : text || '') + line + '\n'
|
||
await discordEnsureDir(ctx, '~/.discord')
|
||
await ctx.vfs.writeFile(path, discordTextToBuf(ctx, text))
|
||
}
|
||
|
||
async function discordSettingsCurrent(ctx, spec) {
|
||
const env = discordCmdEnv(ctx)
|
||
if (spec.persist === 'agent') {
|
||
const cfg = await discordReadJsonFile(ctx, '~/.agent/config.json')
|
||
return cfg[spec.jsonKey]
|
||
}
|
||
if (spec.persist === 'irc') {
|
||
const cfg = await discordReadJsonFile(ctx, '~/.irc/config.json')
|
||
const v = cfg[spec.jsonKey]
|
||
if (Array.isArray(v)) return v.join(',')
|
||
return v
|
||
}
|
||
if (spec.env) return env[spec.env]
|
||
return ''
|
||
}
|
||
|
||
function discordSettingsDisplay(spec, raw) {
|
||
if (spec.kind === 'bool') return discordEnvOn(raw) ? 'on' : 'off'
|
||
if (raw == null || raw === '') return '(unset)'
|
||
return String(raw)
|
||
}
|
||
|
||
function discordSettingsCoerce(spec, value) {
|
||
if (spec.kind === 'bool') {
|
||
const on = value === true || value === 'on' || discordEnvOn(value)
|
||
if (spec.persist === 'agent' || spec.persist === 'irc') return on
|
||
return on ? '1' : '0'
|
||
}
|
||
if (spec.kind === 'number') {
|
||
const n = Number(value)
|
||
if (!Number.isFinite(n)) throw new Error('not a number')
|
||
return n
|
||
}
|
||
if (spec.kind === 'theme') {
|
||
const name = String(value || '')
|
||
.toLowerCase()
|
||
.trim()
|
||
.replace(/\s+/g, '_')
|
||
if (!/^[a-z0-9_.-]+$/.test(name)) throw new Error('invalid theme name')
|
||
return name
|
||
}
|
||
if (spec.kind === 'enum') {
|
||
const want = String(value == null ? '' : value)
|
||
const ok = spec.values && spec.values.indexOf(want) >= 0
|
||
if (!ok) throw new Error('invalid value')
|
||
return want
|
||
}
|
||
return String(value == null ? '' : value)
|
||
}
|
||
|
||
async function discordSettingsApply(ctx, spec, value) {
|
||
if (!spec) throw new Error('unknown setting')
|
||
if (spec.env && discordSettingsIsSecretKey(spec.env)) {
|
||
throw new Error('refusing to write a secret')
|
||
}
|
||
if (spec.jsonKey && discordSettingsIsSecretKey(spec.jsonKey)) {
|
||
throw new Error('refusing to write a secret')
|
||
}
|
||
const env = discordCmdEnv(ctx)
|
||
let v = discordSettingsCoerce(spec, value)
|
||
if (spec.kind === 'hostname' || spec.persist === 'hostname') {
|
||
const host = String(v).trim()
|
||
if (!/^[A-Za-z0-9][A-Za-z0-9.-]{0,62}$/.test(host)) throw new Error('invalid hostname')
|
||
v = host
|
||
}
|
||
if (spec.env) {
|
||
env[spec.env] = String(v)
|
||
if (ctx.env && ctx.env !== env) ctx.env[spec.env] = String(v)
|
||
}
|
||
if (spec.persist === 'export') {
|
||
await discordPersistBarercLine(
|
||
ctx,
|
||
new RegExp('^\\s*export\\s+' + spec.env + '='),
|
||
'export ' + spec.env + '=' + v
|
||
)
|
||
} else if (spec.persist === 'theme') {
|
||
await discordPersistBarercLine(ctx, /^\s*theme\s+/, 'theme ' + v)
|
||
if (typeof ctx.bareOsApplyTheme === 'function') await ctx.bareOsApplyTheme()
|
||
} else if (spec.persist === 'discord') {
|
||
await discordPersistDiscordEnvKey(ctx, spec.env, String(v))
|
||
} else if (spec.persist === 'hostname') {
|
||
try {
|
||
if (typeof ctx.bareOsSetSessionHostname === 'function') {
|
||
ctx.bareOsSetSessionHostname(String(v))
|
||
} else {
|
||
env.HOSTNAME = String(v)
|
||
env.COMPUTERNAME = String(v)
|
||
if (ctx.env && ctx.env !== env) {
|
||
ctx.env.HOSTNAME = String(v)
|
||
ctx.env.COMPUTERNAME = String(v)
|
||
}
|
||
}
|
||
} catch {
|
||
env.HOSTNAME = String(v)
|
||
env.COMPUTERNAME = String(v)
|
||
if (ctx.env && ctx.env !== env) {
|
||
ctx.env.HOSTNAME = String(v)
|
||
ctx.env.COMPUTERNAME = String(v)
|
||
}
|
||
}
|
||
await discordPersistBarercLine(ctx, /^\s*export\s+HOSTNAME=/, 'export HOSTNAME=' + v)
|
||
} else if (spec.persist === 'agent') {
|
||
const cfg = await discordReadJsonFile(ctx, '~/.agent/config.json')
|
||
if (spec.jsonKey === 'backend') cfg.backend = String(v) === 'rest' ? 'rest' : 'qvac'
|
||
else cfg[spec.jsonKey] = v
|
||
await discordWriteJsonFile(ctx, '~/.agent/config.json', cfg)
|
||
} else if (spec.persist === 'irc') {
|
||
const cfg = await discordReadJsonFile(ctx, '~/.irc/config.json')
|
||
if (spec.jsonKey === 'autojoin') {
|
||
cfg.autojoin = String(v)
|
||
.split(',')
|
||
.map(function (s) {
|
||
return s.trim()
|
||
})
|
||
.filter(Boolean)
|
||
} else cfg[spec.jsonKey] = v
|
||
await discordWriteJsonFile(ctx, '~/.irc/config.json', cfg)
|
||
}
|
||
if (
|
||
spec.persist === 'theme' ||
|
||
spec.id === 'color_depth' ||
|
||
spec.id === 'ls_lock' ||
|
||
spec.id === 'no_color' ||
|
||
spec.id === 'dircolors'
|
||
) {
|
||
if (spec.persist !== 'theme' && typeof ctx.bareOsApplyTheme === 'function') {
|
||
await ctx.bareOsApplyTheme()
|
||
}
|
||
}
|
||
}
|
||
|
||
async function discordSettingsSetAlias(ctx, raw) {
|
||
const name = discordSettingsAliasName(raw)
|
||
const value = discordSettingsAliasValue(raw)
|
||
if (!name || !value) throw new Error('use name=command')
|
||
if (!ctx.shellAliases || typeof ctx.shellAliases !== 'object') ctx.shellAliases = {}
|
||
ctx.shellAliases[name] = value
|
||
await discordPersistBarercLine(
|
||
ctx,
|
||
new RegExp('^\\s*unalias\\s+' + name + '\\b'),
|
||
''
|
||
)
|
||
await discordPersistBarercLine(
|
||
ctx,
|
||
new RegExp('^\\s*alias\\s+' + name + '='),
|
||
'alias ' + name + '=' + discordSettingsQuoteAlias(value)
|
||
)
|
||
}
|
||
|
||
async function discordSettingsRemoveAlias(ctx, name) {
|
||
const n = discordSettingsAliasName(name)
|
||
if (!n) throw new Error('bad alias name')
|
||
if (ctx.shellAliases && n) delete ctx.shellAliases[n]
|
||
await discordPersistBarercLine(ctx, new RegExp('^\\s*alias\\s+' + n + '='), '')
|
||
await discordPersistBarercLine(ctx, new RegExp('^\\s*unalias\\s+' + n + '\\b'), 'unalias ' + n)
|
||
}
|
||
|
||
function discordSettingsChoices(ctx, spec) {
|
||
if (spec.kind === 'theme') {
|
||
return discordThemeNames(ctx)
|
||
.slice(0, 25)
|
||
.map(function (n) {
|
||
return { label: n, value: n }
|
||
})
|
||
}
|
||
if (spec.kind === 'enum' && spec.values) {
|
||
return spec.values.map(function (n) {
|
||
return { label: String(n), value: String(n) }
|
||
})
|
||
}
|
||
if (spec.kind === 'bool') {
|
||
return [
|
||
{ label: 'on', value: '1' },
|
||
{ label: 'off', value: '0' }
|
||
]
|
||
}
|
||
return []
|
||
}
|
||
|
||
function discordSettingsGroupMeta(id) {
|
||
for (let i = 0; i < BARE_OS_DISCORD_SET_GROUPS.length; i++) {
|
||
if (BARE_OS_DISCORD_SET_GROUPS[i].id === id) return BARE_OS_DISCORD_SET_GROUPS[i]
|
||
}
|
||
return { id: id, label: id, description: '' }
|
||
}
|
||
|
||
function discordSettingsAliasEntries(ctx) {
|
||
const al = (ctx.shellAliases && typeof ctx.shellAliases === 'object' && ctx.shellAliases) || {}
|
||
return Object.keys(al)
|
||
.sort()
|
||
.map(function (k) {
|
||
return { name: k, value: String(al[k]) }
|
||
})
|
||
}
|
||
|
||
async function discordSettingsView(ctx, interaction) {
|
||
const rec = discordSetGet(interaction)
|
||
const group = rec.group || 'appearance'
|
||
const meta = discordSettingsGroupMeta(group)
|
||
const fields = []
|
||
let pages = 1
|
||
let page = 0
|
||
let itemOpts = []
|
||
if (group === 'aliases') {
|
||
const entries = discordSettingsAliasEntries(ctx)
|
||
const pg = discordSettingsPageOf(entries, rec.page)
|
||
rec.page = pg.page
|
||
pages = pg.pages
|
||
page = pg.page
|
||
if (!entries.length) fields.push(discordField('Aliases', 'None defined. Add one from the menu.'))
|
||
for (let i = 0; i < pg.slice.length; i++) {
|
||
const e = pg.slice[i]
|
||
fields.push(discordField((rec.sel === e.name ? '▸ ' : '') + e.name, '`' + (e.value || '') + '`'))
|
||
}
|
||
itemOpts = [{ label: 'Add alias', value: 'alias_add', description: 'name=command' }].concat(
|
||
pg.slice.map(function (e) {
|
||
return { label: 'Remove ' + e.name, value: 'alias_rm:' + e.name, description: e.value.slice(0, 80) }
|
||
})
|
||
)
|
||
} else {
|
||
const items = discordSettingsInGroup(group)
|
||
const pg = discordSettingsPageOf(items, rec.page)
|
||
rec.page = pg.page
|
||
pages = pg.pages
|
||
page = pg.page
|
||
for (let i = 0; i < pg.slice.length; i++) {
|
||
const spec = pg.slice[i]
|
||
const cur = await discordSettingsCurrent(ctx, spec)
|
||
fields.push(
|
||
discordField(
|
||
(rec.sel === spec.id ? '▸ ' : '') + spec.label,
|
||
'**' + discordSettingsDisplay(spec, cur) + '**\n' + spec.live
|
||
)
|
||
)
|
||
}
|
||
itemOpts = pg.slice.map(function (s) {
|
||
return { label: s.label, value: s.id, description: s.live }
|
||
})
|
||
}
|
||
const gsel = discordSelect(
|
||
'set:group',
|
||
'Category',
|
||
BARE_OS_DISCORD_SET_GROUPS.map(function (g) {
|
||
return { label: g.label, value: g.id, description: g.description }
|
||
})
|
||
)
|
||
const isel = discordSelect('set:item', pages > 1 ? 'Setting… p' + (page + 1) + '/' + pages : 'Setting…', itemOpts)
|
||
const acts = discordButtons([
|
||
{ id: 'set:edit', label: 'Edit', style: 1 },
|
||
{ id: 'set:toggle', label: 'Toggle' },
|
||
{ id: 'set:reload', label: 'Reload barerc' },
|
||
{ id: 'nav:panel', label: 'Panel' }
|
||
])
|
||
const pageRow =
|
||
pages > 1
|
||
? discordButtons([
|
||
{ id: 'set:prev', label: '← Prev', style: 2 },
|
||
{ id: 'set:next', label: 'Next →', style: 2 }
|
||
])
|
||
: null
|
||
return discordResult(
|
||
discordEmbed({
|
||
title: 'Settings · ' + meta.label + (pages > 1 ? ' · ' + (page + 1) + '/' + pages : ''),
|
||
desc: meta.description + '. Secrets are never shown. Pick a row, then **Edit** or **Toggle**.',
|
||
fields: fields.slice(0, 20),
|
||
footer: discordSessionUser(ctx) + ' · expires after 2m idle'
|
||
}),
|
||
{ components: [gsel, isel, acts, pageRow].filter(Boolean), nav: false }
|
||
)
|
||
}
|
||
|
||
async function discordSettingsOpenEditor(ctx, interaction, spec) {
|
||
if (!spec) {
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'Pick a setting from the menu first.',
|
||
ephemeral: true
|
||
})
|
||
}
|
||
const rec = discordSetGet(interaction)
|
||
rec.sel = spec.id
|
||
rec.pending = spec.id
|
||
if (spec.kind === 'enum' || spec.kind === 'theme' || spec.kind === 'bool') {
|
||
const opts = discordSettingsChoices(ctx, spec)
|
||
const cur = await discordSettingsCurrent(ctx, spec)
|
||
return discordSendResult(
|
||
ctx,
|
||
interaction,
|
||
discordResult(
|
||
discordEmbed({
|
||
title: spec.label,
|
||
desc:
|
||
'Current: **' +
|
||
discordSettingsDisplay(spec, cur) +
|
||
'**. Applies live where possible and persists. ' +
|
||
spec.live +
|
||
'.'
|
||
}),
|
||
{
|
||
components: [discordSelect('set:val', spec.label, opts), discordButtons([{ id: 'set:back', label: 'Back' }])],
|
||
nav: false
|
||
}
|
||
)
|
||
)
|
||
}
|
||
const cur = await discordSettingsCurrent(ctx, spec)
|
||
await discordShowModal(interaction, {
|
||
id: 'set:str',
|
||
title: spec.label.slice(0, 45),
|
||
label: spec.label.slice(0, 45),
|
||
placeholder: discordSettingsDisplay(spec, cur).slice(0, 80),
|
||
max: 200
|
||
})
|
||
}
|
||
|
||
async function discordSettingsAction(ctx, interaction, id) {
|
||
const rec = discordSetGet(interaction)
|
||
if (id === 'set:home') {
|
||
rec.group = 'appearance'
|
||
rec.sel = ''
|
||
rec.page = 0
|
||
rec.pending = ''
|
||
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
|
||
}
|
||
if (id === 'set:group') {
|
||
rec.group = String((interaction.values && interaction.values[0]) || rec.group)
|
||
rec.sel = ''
|
||
rec.page = 0
|
||
rec.pending = ''
|
||
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
|
||
}
|
||
if (id === 'set:prev') {
|
||
rec.page = Math.max(0, (rec.page | 0) - 1)
|
||
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
|
||
}
|
||
if (id === 'set:next') {
|
||
rec.page = (rec.page | 0) + 1
|
||
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
|
||
}
|
||
if (id === 'set:item') {
|
||
const v = String((interaction.values && interaction.values[0]) || '')
|
||
if (v === 'alias_add') {
|
||
rec.sel = 'alias_add'
|
||
rec.pending = 'alias_add'
|
||
await discordShowModal(interaction, {
|
||
id: 'set:alias',
|
||
title: 'Add alias',
|
||
label: 'name=command',
|
||
placeholder: 'll=ls -la',
|
||
max: 80
|
||
})
|
||
return
|
||
}
|
||
if (v.indexOf('alias_rm:') === 0) {
|
||
try {
|
||
await discordSettingsRemoveAlias(ctx, v.slice(9))
|
||
} catch (err) {
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'Remove alias failed: ' + ((err && err.message) || err),
|
||
ephemeral: true
|
||
})
|
||
}
|
||
rec.sel = ''
|
||
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
|
||
}
|
||
rec.sel = v
|
||
rec.pending = v
|
||
return discordSettingsOpenEditor(ctx, interaction, discordSettingsSpec(v))
|
||
}
|
||
if (id === 'set:val') {
|
||
const spec = discordSettingsSpec(rec.sel)
|
||
const v = String((interaction.values && interaction.values[0]) || '')
|
||
if (spec) {
|
||
try {
|
||
await discordSettingsApply(ctx, spec, v)
|
||
} catch (err) {
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'Apply failed: ' + ((err && err.message) || err),
|
||
ephemeral: true
|
||
})
|
||
}
|
||
}
|
||
rec.pending = ''
|
||
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
|
||
}
|
||
if (id === 'set:toggle') {
|
||
const spec = discordSettingsSpec(rec.sel)
|
||
if (!spec || spec.kind !== 'bool') {
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'Select a boolean setting first, then Toggle.',
|
||
ephemeral: true
|
||
})
|
||
}
|
||
const cur = await discordSettingsCurrent(ctx, spec)
|
||
const next = discordEnvOn(cur) ? '0' : '1'
|
||
try {
|
||
await discordSettingsApply(ctx, spec, next)
|
||
} catch (err) {
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'Toggle failed: ' + ((err && err.message) || err),
|
||
ephemeral: true
|
||
})
|
||
}
|
||
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
|
||
}
|
||
if (id === 'set:edit') {
|
||
if (rec.group === 'aliases') {
|
||
rec.pending = 'alias_add'
|
||
await discordShowModal(interaction, {
|
||
id: 'set:alias',
|
||
title: 'Add alias',
|
||
label: 'name=command',
|
||
placeholder: 'll=ls -la',
|
||
max: 80
|
||
})
|
||
return
|
||
}
|
||
return discordSettingsOpenEditor(ctx, interaction, discordSettingsSpec(rec.sel))
|
||
}
|
||
if (id === 'set:reload') {
|
||
if (typeof ctx.execLine === 'function') {
|
||
try {
|
||
await ctx.execLine('barerc reload')
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
}
|
||
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
|
||
}
|
||
if (id === 'set:back') {
|
||
rec.pending = ''
|
||
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
|
||
}
|
||
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
|
||
}
|
||
|
||
async function discordBeginCreate(ctx, interaction, rawPath) {
|
||
const p = discordCmdPathWriteOk(ctx, rawPath)
|
||
if (!p) {
|
||
return discordSendResult(ctx, interaction, {
|
||
text:
|
||
'Cannot create that path. Use `~/file`, `/tmp/file`, or `/home/' +
|
||
discordSessionUser(ctx) +
|
||
'/file`. Token files are blocked.',
|
||
ephemeral: true
|
||
})
|
||
}
|
||
if (!ctx.vfs || typeof ctx.vfs.writeFile !== 'function') {
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'vfs.writeFile is unavailable in this session',
|
||
ephemeral: true
|
||
})
|
||
}
|
||
if (await discordFileExists(ctx, p)) {
|
||
discordEditPut(interaction, { path: p })
|
||
return discordSendResult(
|
||
ctx,
|
||
interaction,
|
||
discordResult(
|
||
discordEmbed({
|
||
title: 'Already exists',
|
||
color: BARE_OS_DISCORD_COLOR_WARN,
|
||
desc:
|
||
'`' +
|
||
p +
|
||
'` is already on disk. Use **Edit** to change it, or pick another path.'
|
||
}),
|
||
{
|
||
components: [
|
||
discordButtons([
|
||
{ id: 'edit:open', label: 'Edit instead', style: 1 },
|
||
{ id: 'create:new', label: 'Different path…', style: 3 }
|
||
])
|
||
],
|
||
editPath: p
|
||
}
|
||
)
|
||
)
|
||
}
|
||
const split = discordEditChunks('')
|
||
discordEditPut(interaction, {
|
||
path: p,
|
||
origLen: 0,
|
||
truncated: false,
|
||
created: true
|
||
})
|
||
if (typeof interaction.showModal !== 'function') {
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'This client cannot show Discord modals.',
|
||
ephemeral: true
|
||
})
|
||
}
|
||
try {
|
||
const modal = discordEditModalPayload(p, split.chunks)
|
||
modal.custom_id = 'create:save'
|
||
modal.title = ('Create ' + (String(p).split('/').pop() || p)).slice(0, 45)
|
||
await interaction.showModal(modal)
|
||
} catch (err) {
|
||
if (ctx.console && typeof ctx.console.error === 'function') {
|
||
ctx.console.error(
|
||
'discord-bot: create modal failed: ' + ((err && err.message) || err)
|
||
)
|
||
}
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'Could not open the create modal: ' + ((err && err.message) || err),
|
||
ephemeral: true
|
||
})
|
||
}
|
||
}
|
||
|
||
function discordIdleNow() {
|
||
return Date.now()
|
||
}
|
||
|
||
function discordIdleSchedule(fn, ms) {
|
||
const tfn = typeof setTimeout === 'function' ? setTimeout : globalThis.setTimeout
|
||
if (typeof tfn !== 'function') return null
|
||
const t = tfn(fn, ms)
|
||
if (t && typeof t.unref === 'function') t.unref()
|
||
return t
|
||
}
|
||
|
||
function discordIdleClearTimer(t) {
|
||
const cfn = typeof clearTimeout === 'function' ? clearTimeout : globalThis.clearTimeout
|
||
if (t != null && typeof cfn === 'function') cfn(t)
|
||
}
|
||
|
||
function discordIdleExpiredEmbeds(embeds) {
|
||
const out = []
|
||
if (!Array.isArray(embeds)) return out
|
||
for (let i = 0; i < embeds.length; i++) {
|
||
const e = embeds[i] || {}
|
||
const n = {}
|
||
for (const k in e) n[k] = e[k]
|
||
n.footer = { text: 'Expired after 2 minutes idle' }
|
||
n.color = BARE_OS_DISCORD_COLOR_WARN
|
||
out.push(n)
|
||
}
|
||
return out
|
||
}
|
||
|
||
function discordIdleForgetSessions(userId) {
|
||
if (!userId) return
|
||
for (const k in BARE_OS_DISCORD_LIVE) {
|
||
const r = BARE_OS_DISCORD_LIVE[k]
|
||
if (r && r.userId === userId) return
|
||
}
|
||
delete BARE_OS_DISCORD_EDIT_SESSIONS[userId]
|
||
delete BARE_OS_DISCORD_FM_SESSIONS[userId]
|
||
delete BARE_OS_DISCORD_SET_SESSIONS[userId]
|
||
delete BARE_OS_DISCORD_MORE_SESSIONS[userId]
|
||
delete BARE_OS_DISCORD_SH_SESSIONS[userId]
|
||
}
|
||
|
||
function discordUnwrapMessage(sent) {
|
||
if (!sent || typeof sent !== 'object') return null
|
||
if (sent.resource && sent.resource.message) return sent.resource.message
|
||
if (sent.message && (sent.message.id || typeof sent.message.edit === 'function')) {
|
||
return sent.message
|
||
}
|
||
if (sent.id || typeof sent.delete === 'function' || typeof sent.edit === 'function') return sent
|
||
return null
|
||
}
|
||
|
||
async function discordResolvePostedMessage(interaction, sent) {
|
||
const unwrapped = discordUnwrapMessage(sent)
|
||
if (unwrapped) return unwrapped
|
||
if (interaction && interaction.message && (interaction.message.id || typeof interaction.message.delete === 'function')) {
|
||
return interaction.message
|
||
}
|
||
if (interaction && typeof interaction.fetchReply === 'function') {
|
||
try {
|
||
const m = await interaction.fetchReply()
|
||
return discordUnwrapMessage(m) || m
|
||
} catch {
|
||
return null
|
||
}
|
||
}
|
||
return null
|
||
}
|
||
|
||
function discordIdleArm(msg, payload, userId) {
|
||
if (!msg) return null
|
||
const hasUi =
|
||
(payload && payload.embeds && payload.embeds.length) ||
|
||
(payload && payload.components && payload.components.length)
|
||
if (!hasUi) return null
|
||
const id = String(msg.id || '') || 'anon:' + ++BARE_OS_DISCORD_IDLE_SEQ
|
||
const prev = BARE_OS_DISCORD_LIVE[id]
|
||
if (prev) discordIdleClearTimer(prev.timer)
|
||
const rec = {
|
||
id: id,
|
||
message: msg,
|
||
userId: userId || '',
|
||
embeds: payload && payload.embeds ? payload.embeds : null,
|
||
atMs: discordIdleNow(),
|
||
timer: null
|
||
}
|
||
BARE_OS_DISCORD_LIVE[id] = rec
|
||
rec.timer = discordIdleSchedule(function () {
|
||
Promise.resolve(discordIdleExpire(id)).catch(function () {})
|
||
}, BARE_OS_DISCORD_IDLE_MS)
|
||
return rec
|
||
}
|
||
|
||
async function discordIdleExpire(id) {
|
||
const rec = BARE_OS_DISCORD_LIVE[id]
|
||
if (!rec) return false
|
||
delete BARE_OS_DISCORD_LIVE[id]
|
||
discordIdleClearTimer(rec.timer)
|
||
rec.timer = null
|
||
const msg = rec.message
|
||
let cleaned = false
|
||
if (msg && typeof msg.delete === 'function') {
|
||
try {
|
||
await msg.delete()
|
||
cleaned = true
|
||
} catch {
|
||
cleaned = false
|
||
}
|
||
}
|
||
if (!cleaned && msg && typeof msg.edit === 'function') {
|
||
try {
|
||
const body = { components: [] }
|
||
if (rec.embeds && rec.embeds.length) body.embeds = discordIdleExpiredEmbeds(rec.embeds)
|
||
await msg.edit(body)
|
||
cleaned = true
|
||
} catch {
|
||
cleaned = false
|
||
}
|
||
}
|
||
discordIdleForgetSessions(rec.userId)
|
||
return cleaned
|
||
}
|
||
|
||
function discordIdleSweep() {
|
||
const now = discordIdleNow()
|
||
const due = []
|
||
for (const k in BARE_OS_DISCORD_LIVE) {
|
||
const r = BARE_OS_DISCORD_LIVE[k]
|
||
if (!r || now - r.atMs >= BARE_OS_DISCORD_IDLE_MS) due.push(k)
|
||
}
|
||
for (let i = 0; i < due.length; i++) {
|
||
Promise.resolve(discordIdleExpire(due[i])).catch(function () {})
|
||
}
|
||
}
|
||
|
||
function discordIdleTouch(interaction) {
|
||
const mid = interaction && interaction.message && interaction.message.id
|
||
if (!mid) return
|
||
const rec = BARE_OS_DISCORD_LIVE[String(mid)]
|
||
if (!rec) return
|
||
if (discordIdleNow() - rec.atMs >= BARE_OS_DISCORD_IDLE_MS) return
|
||
rec.atMs = discordIdleNow()
|
||
}
|
||
|
||
async function discordIdleWatch(interaction, payload, sent) {
|
||
if (!payload) return
|
||
if (
|
||
payload.flags === BARE_OS_DISCORD_FLAG_EPHEMERAL &&
|
||
!(payload.embeds && payload.embeds.length) &&
|
||
!(payload.components && payload.components.length)
|
||
) {
|
||
return
|
||
}
|
||
const msg = await discordResolvePostedMessage(interaction, sent)
|
||
discordIdleArm(msg, payload, discordInteractionUserId(interaction))
|
||
}
|
||
|
||
async function discordSendResult(ctx, interaction, result) {
|
||
if (result && result.run !== undefined) {
|
||
return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, result.run))
|
||
}
|
||
if (result && result.more) {
|
||
return discordSendResult(ctx, interaction, discordLongTextResult(interaction, result.more))
|
||
}
|
||
if (result && result.editPath) {
|
||
const prev = discordEditGet(interaction)
|
||
discordEditPut(interaction, {
|
||
path: result.editPath,
|
||
created: !!(result.created || (prev && prev.created))
|
||
})
|
||
}
|
||
if (result && result.edit) {
|
||
await discordBeginEdit(ctx, interaction, result.edit)
|
||
return
|
||
}
|
||
if (result && result.create) {
|
||
await discordBeginCreate(ctx, interaction, result.create)
|
||
return
|
||
}
|
||
if (result && result.createPick) {
|
||
return discordSendResult(ctx, interaction, discordCreatePicker(ctx))
|
||
}
|
||
if (result && result.settings) {
|
||
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
|
||
}
|
||
if (result && result.files !== undefined) {
|
||
const rec = discordFmGet(interaction, ctx)
|
||
const want = String(result.files || '').trim()
|
||
if (want) {
|
||
const p = discordCmdPathOk(want)
|
||
if (p) {
|
||
rec.cwd = p
|
||
rec.page = 0
|
||
rec.sel = ''
|
||
rec.confirm = ''
|
||
}
|
||
}
|
||
return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction))
|
||
}
|
||
if (result && result.modal === 'create:path') {
|
||
await discordShowModal(interaction, {
|
||
id: 'create:path',
|
||
title: 'New file path',
|
||
label: 'Path under ~ or /tmp',
|
||
placeholder: '~/notes.txt',
|
||
max: 200
|
||
})
|
||
return
|
||
}
|
||
if (result && result.modal === 'edit:path') {
|
||
await discordShowModal(interaction, {
|
||
id: 'edit:path',
|
||
title: 'Open a file to edit',
|
||
label: 'Path under ~ or /tmp',
|
||
placeholder: '~/notes.txt',
|
||
max: 200
|
||
})
|
||
return
|
||
}
|
||
if (result && result.modal === 'say') {
|
||
await discordShowModal(interaction, {
|
||
id: 'say:modal',
|
||
title: 'Say as Bare OS',
|
||
label: 'Message',
|
||
paragraph: true,
|
||
placeholder: 'Hello from Bare OS'
|
||
})
|
||
return
|
||
}
|
||
if (result && result.modal === 'run') {
|
||
return discordSendResult(ctx, interaction, discordShHud(ctx, interaction, ''))
|
||
}
|
||
const payload = discordCmdReplyPayload(result)
|
||
const isComp =
|
||
(typeof interaction.isButton === 'function' && interaction.isButton()) ||
|
||
(typeof interaction.isStringSelectMenu === 'function' &&
|
||
interaction.isStringSelectMenu())
|
||
let sent = null
|
||
try {
|
||
if (isComp && typeof interaction.update === 'function' && !interaction.replied && !interaction.deferred) {
|
||
sent = await interaction.update(payload)
|
||
} else if (interaction.deferred && typeof interaction.editReply === 'function') {
|
||
sent = await interaction.editReply(payload)
|
||
} else if (interaction.replied && typeof interaction.followUp === 'function') {
|
||
sent = await interaction.followUp(payload)
|
||
} else {
|
||
sent = 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
|
||
}
|
||
try {
|
||
await discordIdleWatch(interaction, payload, sent)
|
||
} catch {
|
||
/* tracking must not break the bot */
|
||
}
|
||
}
|
||
|
||
async function discordReplyWhitelistDenied(ctx, interaction) {
|
||
await discordSendResult(ctx, interaction, {
|
||
text: BARE_OS_DISCORD_WHITELIST_DENY,
|
||
ephemeral: true
|
||
})
|
||
}
|
||
|
||
async function discordDispatchAutocomplete(ctx, interaction) {
|
||
let focused = { name: '', value: '' }
|
||
try {
|
||
if (interaction.options && typeof interaction.options.getFocused === 'function') {
|
||
const f = interaction.options.getFocused(true)
|
||
if (f && typeof f === 'object') focused = f
|
||
else focused = { name: 'cmd', value: String(f || '') }
|
||
}
|
||
} catch {
|
||
focused = { name: '', value: '' }
|
||
}
|
||
const q = focused.value
|
||
const fname = String(focused.name || '')
|
||
let choices = []
|
||
try {
|
||
if (fname === 'unit') choices = await discordSuggestUnits(ctx, q)
|
||
else if (fname === 'page') choices = await discordSuggestMan(ctx, q)
|
||
else if (fname === 'cmd') choices = await discordSuggestRun(ctx, interaction, q)
|
||
else if (fname === 'path') {
|
||
const cmd = String(interaction.commandName || '')
|
||
choices =
|
||
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 = []
|
||
}
|
||
try {
|
||
if (typeof interaction.respond === 'function') await interaction.respond(choices.slice(0, 25))
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
return true
|
||
}
|
||
|
||
async function discordFmDelete(ctx, path) {
|
||
if (typeof ctx.vfs.rm === 'function') {
|
||
await ctx.vfs.rm(path, { recursive: true, force: true })
|
||
return
|
||
}
|
||
const st = await discordFmStat(ctx, path)
|
||
if (discordIsDirStat(st) && typeof ctx.vfs.rmdir === 'function') {
|
||
await ctx.vfs.rmdir(path)
|
||
return
|
||
}
|
||
if (typeof ctx.vfs.unlink === 'function') await ctx.vfs.unlink(path)
|
||
else throw new Error('unlink unavailable')
|
||
}
|
||
|
||
async function discordFmCopyFile(ctx, from, to) {
|
||
if (typeof ctx.vfs.readFile !== 'function' || typeof ctx.vfs.writeFile !== 'function') {
|
||
throw new Error('copy requires readFile/writeFile')
|
||
}
|
||
const buf = await ctx.vfs.readFile(from)
|
||
await ctx.vfs.writeFile(to, buf)
|
||
}
|
||
|
||
async function discordFmAction(ctx, interaction, id) {
|
||
const rec = discordFmGet(interaction, ctx)
|
||
if (id === 'fm:goto') {
|
||
await discordShowModal(interaction, {
|
||
id: 'fm:goto',
|
||
title: 'Go to path',
|
||
label: 'Directory',
|
||
placeholder: rec.cwd || '~',
|
||
max: 200
|
||
})
|
||
return
|
||
}
|
||
if (id === 'fm:mkdir') {
|
||
await discordShowModal(interaction, {
|
||
id: 'fm:mkdir',
|
||
title: 'New folder in ' + discordBaseName(rec.cwd || '~'),
|
||
label: 'Folder name',
|
||
placeholder: 'docs',
|
||
max: 80
|
||
})
|
||
return
|
||
}
|
||
if (id === 'fm:ren') {
|
||
if (!rec.sel) {
|
||
return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction))
|
||
}
|
||
await discordShowModal(interaction, {
|
||
id: 'fm:ren',
|
||
title: 'Rename ' + rec.sel,
|
||
label: 'New name',
|
||
placeholder: rec.sel,
|
||
max: 80
|
||
})
|
||
return
|
||
}
|
||
if (id === 'fm:copy') {
|
||
if (!rec.sel) {
|
||
return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction))
|
||
}
|
||
await discordShowModal(interaction, {
|
||
id: 'fm:copy',
|
||
title: 'Copy ' + rec.sel,
|
||
label: 'Destination path',
|
||
placeholder: discordJoinPath(rec.cwd, rec.sel + '.copy'),
|
||
max: 200
|
||
})
|
||
return
|
||
}
|
||
if (id === 'fm:new') {
|
||
const name = 'new-' + String(Date.now()).slice(-6) + '.txt'
|
||
await discordBeginCreate(ctx, interaction, discordJoinPath(rec.cwd, name))
|
||
return
|
||
}
|
||
if (id === 'fm:edit') {
|
||
if (!rec.sel) {
|
||
return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction))
|
||
}
|
||
await discordBeginEdit(ctx, interaction, discordJoinPath(rec.cwd, rec.sel))
|
||
return
|
||
}
|
||
if (id === 'fm:up') {
|
||
rec.cwd = discordParentPath(rec.cwd)
|
||
rec.sel = ''
|
||
rec.page = 0
|
||
rec.confirm = ''
|
||
} else if (id === 'fm:home') {
|
||
rec.cwd = '~'
|
||
rec.sel = ''
|
||
rec.page = 0
|
||
rec.confirm = ''
|
||
} else if (id === 'fm:prev') {
|
||
rec.page--
|
||
rec.confirm = ''
|
||
} else if (id === 'fm:next') {
|
||
rec.page++
|
||
rec.confirm = ''
|
||
} else if (id === 'fm:ref' || id === 'fm:more' || id === 'fm:no') {
|
||
rec.confirm = ''
|
||
} else if (id === 'fm:pick') {
|
||
const v = String((interaction.values && interaction.values[0]) || '')
|
||
rec.confirm = ''
|
||
if (v === '..') {
|
||
rec.cwd = discordParentPath(rec.cwd)
|
||
rec.sel = ''
|
||
rec.page = 0
|
||
} else if (v) {
|
||
const full = discordJoinPath(rec.cwd, v)
|
||
const st = await discordFmStat(ctx, full)
|
||
if (discordIsDirStat(st)) {
|
||
rec.cwd = full
|
||
rec.sel = ''
|
||
rec.page = 0
|
||
} else {
|
||
rec.sel = v
|
||
return discordSendResult(ctx, interaction, await discordCmdHandleFs(ctx, 'cat', full))
|
||
}
|
||
}
|
||
} else if (id === 'fm:open') {
|
||
if (!rec.sel) {
|
||
/* stay */
|
||
} else {
|
||
const full = discordJoinPath(rec.cwd, rec.sel)
|
||
const st = await discordFmStat(ctx, full)
|
||
if (discordIsDirStat(st)) {
|
||
rec.cwd = full
|
||
rec.sel = ''
|
||
rec.page = 0
|
||
} else {
|
||
return discordSendResult(ctx, interaction, await discordCmdHandleFs(ctx, 'cat', full))
|
||
}
|
||
}
|
||
} else if (id === 'fm:del') {
|
||
rec.confirm = rec.sel || ''
|
||
} else if (id === 'fm:ok') {
|
||
const name = rec.confirm || rec.sel
|
||
rec.confirm = ''
|
||
if (name) {
|
||
const full = discordJoinPath(rec.cwd, name)
|
||
if (!discordCmdPathWriteOk(ctx, full)) {
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'Delete not allowed: `' + full + '`',
|
||
ephemeral: true
|
||
})
|
||
}
|
||
try {
|
||
await discordFmDelete(ctx, full)
|
||
rec.sel = ''
|
||
} catch (err) {
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'Delete failed: ' + ((err && err.message) || err),
|
||
ephemeral: true
|
||
})
|
||
}
|
||
}
|
||
}
|
||
return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction))
|
||
}
|
||
|
||
async function discordDispatchComponent(ctx, interaction) {
|
||
const id = String(interaction.customId || '')
|
||
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
|
||
}
|
||
if (id === 'more:prev' || id === 'more:next') {
|
||
const rec = discordMoreGet(interaction)
|
||
if (!rec) {
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'This view expired after 2 minutes idle.',
|
||
ephemeral: true
|
||
})
|
||
}
|
||
rec.page += id === 'more:next' ? 1 : -1
|
||
return discordSendResult(ctx, interaction, discordMoreResult(interaction))
|
||
}
|
||
if (id.indexOf('set:') === 0) {
|
||
await discordSettingsAction(ctx, interaction, id)
|
||
return true
|
||
}
|
||
if (id.indexOf('fm:') === 0) {
|
||
await discordFmAction(ctx, interaction, id)
|
||
return true
|
||
}
|
||
if (id === 'nav:panel') return discordSendResult(ctx, interaction, discordPanel(ctx))
|
||
if (id === 'say:compose') {
|
||
await discordShowModal(interaction, {
|
||
id: 'say:modal',
|
||
title: 'Say as Bare OS',
|
||
label: 'Message',
|
||
paragraph: true,
|
||
placeholder: 'Hello from Bare OS'
|
||
})
|
||
return true
|
||
}
|
||
if (id === 'edit:new') {
|
||
await discordShowModal(interaction, {
|
||
id: 'edit:path',
|
||
title: 'Open a file to edit',
|
||
label: 'Path under ~ or /tmp',
|
||
placeholder: '~/notes.txt',
|
||
max: 200
|
||
})
|
||
return true
|
||
}
|
||
if (id === 'create:new' || id === 'create:custom') {
|
||
await discordShowModal(interaction, {
|
||
id: 'create:path',
|
||
title: 'New file path',
|
||
label: 'Path under ~ or /tmp',
|
||
placeholder: '~/notes.txt',
|
||
max: 200
|
||
})
|
||
return true
|
||
}
|
||
if (id === 'create:pick') {
|
||
const values = interaction.values || []
|
||
const path = values[0] ? String(values[0]) : ''
|
||
if (!path) {
|
||
return discordSendResult(ctx, interaction, discordCreatePicker(ctx))
|
||
}
|
||
await discordBeginCreate(ctx, interaction, path)
|
||
return true
|
||
}
|
||
if (id === 'create:open') {
|
||
const rec = discordEditGet(interaction)
|
||
const path = rec && rec.path
|
||
if (!path) {
|
||
await discordShowModal(interaction, {
|
||
id: 'create:path',
|
||
title: 'New file path',
|
||
label: 'Path under ~ or /tmp',
|
||
placeholder: '~/notes.txt',
|
||
max: 200
|
||
})
|
||
return true
|
||
}
|
||
await discordBeginCreate(ctx, interaction, path)
|
||
return true
|
||
}
|
||
if (id === 'edit:open') {
|
||
const rec = discordEditGet(interaction)
|
||
const path = rec && rec.path
|
||
if (!path) {
|
||
await discordShowModal(interaction, {
|
||
id: 'edit:path',
|
||
title: 'Open a file to edit',
|
||
label: 'Path under ~ or /tmp',
|
||
placeholder: '~/notes.txt',
|
||
max: 200
|
||
})
|
||
return true
|
||
}
|
||
await discordBeginEdit(ctx, interaction, path)
|
||
return true
|
||
}
|
||
if (id.indexOf('bare:') === 0) {
|
||
return discordSendResult(ctx, interaction, await discordCmdHandleBare(ctx, id.slice(5)))
|
||
}
|
||
if (id.indexOf('sys:') === 0) {
|
||
return discordSendResult(ctx, interaction, await discordCmdHandleSys(ctx, id.slice(4)))
|
||
}
|
||
if (id.indexOf('net:') === 0) {
|
||
return discordSendResult(ctx, interaction, await discordCmdHandleNet(ctx, id.slice(4)))
|
||
}
|
||
if (id === 'svc:list') {
|
||
return discordSendResult(ctx, interaction, await discordCmdHandleSvc(ctx, 'list', ''))
|
||
}
|
||
if (id === 'svc:pick') {
|
||
const values = interaction.values || []
|
||
const unit = values[0] ? String(values[0]) : ''
|
||
return discordSendResult(ctx, interaction, await discordCmdHandleSvc(ctx, 'status', unit))
|
||
}
|
||
const svcAct = /^svc:(status|start|stop|restart|logs):(.+)$/.exec(id)
|
||
if (svcAct) {
|
||
return discordSendResult(
|
||
ctx,
|
||
interaction,
|
||
await discordCmdHandleSvc(ctx, svcAct[1], svcAct[2])
|
||
)
|
||
}
|
||
return discordSendResult(ctx, interaction, await discordRouteCommand(ctx, 'panel', '', opt))
|
||
}
|
||
|
||
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') {
|
||
value = String(interaction.fields.getTextInputValue('text') || '')
|
||
}
|
||
} catch {
|
||
value = ''
|
||
}
|
||
if (id === 'fm:goto') {
|
||
const rec = discordFmGet(interaction, ctx)
|
||
const p = discordCmdPathOk(value)
|
||
if (p) {
|
||
rec.cwd = p
|
||
rec.page = 0
|
||
rec.sel = ''
|
||
rec.confirm = ''
|
||
}
|
||
return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction))
|
||
}
|
||
if (id === 'fm:mkdir') {
|
||
const rec = discordFmGet(interaction, ctx)
|
||
const name = String(value || '').replace(/[\/\0]/g, '')
|
||
const dest = discordJoinPath(rec.cwd, name)
|
||
if (!name || !discordCmdPathWriteOk(ctx, dest)) {
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'Cannot create folder `' + (name || '?') + '` here.',
|
||
ephemeral: true
|
||
})
|
||
}
|
||
try {
|
||
if (typeof ctx.vfs.mkdir !== 'function') throw new Error('mkdir unavailable')
|
||
await ctx.vfs.mkdir(dest, { recursive: true })
|
||
rec.sel = name
|
||
} catch (err) {
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'mkdir failed: ' + ((err && err.message) || err),
|
||
ephemeral: true
|
||
})
|
||
}
|
||
return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction))
|
||
}
|
||
if (id === 'fm:ren') {
|
||
const rec = discordFmGet(interaction, ctx)
|
||
const name = String(value || '').replace(/[\/\0]/g, '')
|
||
const from = rec.sel ? discordJoinPath(rec.cwd, rec.sel) : ''
|
||
const to = discordJoinPath(rec.cwd, name)
|
||
if (!from || !name || !discordCmdPathWriteOk(ctx, from) || !discordCmdPathWriteOk(ctx, to)) {
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'Rename not allowed.',
|
||
ephemeral: true
|
||
})
|
||
}
|
||
try {
|
||
const st = await discordFmStat(ctx, from)
|
||
if (discordIsDirStat(st)) throw new Error('renaming folders is not supported')
|
||
if (await discordFileExists(ctx, to)) throw new Error('destination exists')
|
||
await discordFmCopyFile(ctx, from, to)
|
||
await discordFmDelete(ctx, from)
|
||
rec.sel = name
|
||
} catch (err) {
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'Rename failed: ' + ((err && err.message) || err),
|
||
ephemeral: true
|
||
})
|
||
}
|
||
return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction))
|
||
}
|
||
if (id === 'fm:copy') {
|
||
const rec = discordFmGet(interaction, ctx)
|
||
const to = discordCmdPathWriteOk(ctx, value)
|
||
const from = rec.sel ? discordJoinPath(rec.cwd, rec.sel) : ''
|
||
if (!from || !to || !discordCmdPathWriteOk(ctx, from)) {
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'Copy not allowed.',
|
||
ephemeral: true
|
||
})
|
||
}
|
||
try {
|
||
const st = await discordFmStat(ctx, from)
|
||
if (discordIsDirStat(st)) throw new Error('copying folders is not supported')
|
||
if (await discordFileExists(ctx, to)) throw new Error('destination exists')
|
||
await discordFmCopyFile(ctx, from, to)
|
||
} catch (err) {
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'Copy failed: ' + ((err && err.message) || err),
|
||
ephemeral: true
|
||
})
|
||
}
|
||
return discordSendResult(ctx, interaction, await discordFmView(ctx, interaction))
|
||
}
|
||
if (id === 'set:str') {
|
||
const rec = discordSettingsGet(interaction)
|
||
const spec = discordSettingsFind(rec.pending || rec.sel)
|
||
rec.pending = ''
|
||
if (!spec || (spec.kind !== 'string' && spec.kind !== 'number')) {
|
||
return discordSendResult(ctx, interaction, { text: 'No pending setting.', ephemeral: true })
|
||
}
|
||
try {
|
||
await discordSettingsApply(ctx, spec, value)
|
||
} catch (err) {
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'Could not apply `' + spec.id + '`: ' + ((err && err.message) || err),
|
||
ephemeral: true
|
||
})
|
||
}
|
||
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
|
||
}
|
||
if (id === 'set:alias') {
|
||
const rec = discordSettingsGet(interaction)
|
||
rec.pending = ''
|
||
rec.group = 'aliases'
|
||
try {
|
||
await discordSettingsSetAlias(ctx, value)
|
||
rec.sel = discordSettingsAliasName(value)
|
||
} catch (err) {
|
||
return discordSendResult(ctx, interaction, {
|
||
text: 'Alias failed: ' + ((err && err.message) || err),
|
||
ephemeral: true
|
||
})
|
||
}
|
||
return discordSendResult(ctx, interaction, await discordSettingsView(ctx, interaction))
|
||
}
|
||
if (id === 'edit:save' || id === 'create:save') {
|
||
if (id === 'create:save') {
|
||
const rec = discordEditGet(interaction)
|
||
if (rec) rec.created = true
|
||
}
|
||
return discordSendResult(ctx, interaction, await discordEditSaveFromModal(ctx, interaction))
|
||
}
|
||
if (id === 'create:path') {
|
||
const p = discordCmdPathWriteOk(ctx, value)
|
||
if (!p) {
|
||
return discordSendResult(ctx, interaction, {
|
||
text:
|
||
'Not writable: `' +
|
||
(value || '(empty)') +
|
||
'`. Use `~/file`, `/tmp/file`, or `/home/' +
|
||
discordSessionUser(ctx) +
|
||
'/file`.',
|
||
ephemeral: true
|
||
})
|
||
}
|
||
if (await discordFileExists(ctx, p)) {
|
||
discordEditPut(interaction, { path: p })
|
||
return discordSendResult(
|
||
ctx,
|
||
interaction,
|
||
discordResult(
|
||
discordEmbed({
|
||
title: 'Already exists',
|
||
color: BARE_OS_DISCORD_COLOR_WARN,
|
||
desc: '`' + p + '` is already on disk. Edit it, or choose another name.'
|
||
}),
|
||
{
|
||
components: [
|
||
discordButtons([
|
||
{ id: 'edit:open', label: 'Edit instead', style: 1 },
|
||
{ id: 'create:custom', label: 'Different path…', style: 3 }
|
||
])
|
||
],
|
||
editPath: p
|
||
}
|
||
)
|
||
)
|
||
}
|
||
discordEditPut(interaction, { path: p, created: true })
|
||
return discordSendResult(
|
||
ctx,
|
||
interaction,
|
||
discordResult(
|
||
discordEmbed({
|
||
title: 'Ready to create',
|
||
desc:
|
||
'`' +
|
||
p +
|
||
'`\nDiscord cannot open a second modal from this form. Click **Compose contents**.',
|
||
color: BARE_OS_DISCORD_COLOR
|
||
}),
|
||
{
|
||
components: [
|
||
discordButtons([{ id: 'create:open', label: 'Compose contents', style: 3 }])
|
||
],
|
||
editPath: p,
|
||
created: true
|
||
}
|
||
)
|
||
)
|
||
}
|
||
if (id === 'edit:path') {
|
||
const p = discordCmdPathWriteOk(ctx, value)
|
||
if (!p) {
|
||
return discordSendResult(ctx, interaction, {
|
||
text:
|
||
'Not writable: `' +
|
||
(value || '(empty)') +
|
||
'`. Use `~/file`, `/tmp/file`, or `/home/' +
|
||
discordSessionUser(ctx) +
|
||
'/file`.',
|
||
ephemeral: true
|
||
})
|
||
}
|
||
discordEditPut(interaction, { path: p })
|
||
const exists = Boolean(await discordCmdReadText(ctx, p))
|
||
return discordSendResult(
|
||
ctx,
|
||
interaction,
|
||
discordResult(
|
||
discordEmbed({
|
||
title: exists ? 'Ready to edit' : 'New file',
|
||
desc:
|
||
'`' +
|
||
p +
|
||
'`\nDiscord cannot open a second modal from this form. Click **Open editor**.',
|
||
color: BARE_OS_DISCORD_COLOR
|
||
}),
|
||
{
|
||
components: [
|
||
discordButtons([{ id: 'edit:open', label: 'Open editor', style: 1 }])
|
||
],
|
||
editPath: p
|
||
}
|
||
)
|
||
)
|
||
}
|
||
if (id === 'say:modal') {
|
||
return discordSendResult(
|
||
ctx,
|
||
interaction,
|
||
discordResult(
|
||
discordEmbed({
|
||
title: 'say',
|
||
desc: discordCmdFence(discordCmdSayBox(value))
|
||
})
|
||
)
|
||
)
|
||
}
|
||
if (id === 'run:modal') {
|
||
return discordSendResult(ctx, interaction, await discordCmdHandleRun(ctx, interaction, value))
|
||
}
|
||
return discordSendResult(ctx, interaction, discordPanel(ctx))
|
||
}
|
||
|
||
async function discordDispatchInteraction(ctx, interaction) {
|
||
if (!interaction) return false
|
||
discordSyncSessionIdentity(ctx)
|
||
discordIdleTouch(interaction)
|
||
discordIdleSweep()
|
||
if (typeof interaction.isAutocomplete === 'function' && interaction.isAutocomplete()) {
|
||
if (!discordUserAllowed(ctx, discordInteractionUserId(interaction))) {
|
||
try {
|
||
if (typeof interaction.respond === 'function') await interaction.respond([])
|
||
} catch {
|
||
/* ignore */
|
||
}
|
||
return true
|
||
}
|
||
await discordPluginsEnsure(ctx)
|
||
return discordDispatchAutocomplete(ctx, interaction)
|
||
}
|
||
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
|
||
}
|
||
if (typeof interaction.isButton === 'function' && interaction.isButton()) {
|
||
await discordDispatchComponent(ctx, interaction)
|
||
return true
|
||
}
|
||
if (typeof interaction.isStringSelectMenu === 'function' && interaction.isStringSelectMenu()) {
|
||
await discordDispatchComponent(ctx, interaction)
|
||
return true
|
||
}
|
||
if (typeof interaction.isModalSubmit === 'function' && interaction.isModalSubmit()) {
|
||
await discordDispatchModal(ctx, interaction)
|
||
return true
|
||
}
|
||
if (typeof interaction.isChatInputCommand !== 'function' || !interaction.isChatInputCommand()) {
|
||
return false
|
||
}
|
||
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 {
|
||
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)),
|
||
ephemeral: true
|
||
}
|
||
}
|
||
await discordSendResult(ctx, interaction, result)
|
||
return true
|
||
}
|
||
|
||
var bareOsDiscordCommands = {
|
||
buildSlashCommands: discordBuildSlashCommands,
|
||
dispatchInteraction: discordDispatchInteraction,
|
||
parseIdWhitelist: discordParseIdWhitelist,
|
||
userAllowed: discordUserAllowed,
|
||
installProcessEmitWarning: discordInstallProcessEmitWarning,
|
||
replyPayload: discordCmdReplyPayload,
|
||
syncSessionIdentity: discordSyncSessionIdentity,
|
||
sessionUser: discordSessionUser,
|
||
FLAG_EPHEMERAL: BARE_OS_DISCORD_FLAG_EPHEMERAL,
|
||
prettySnapshot: discordPrettySnapshot,
|
||
prettyBytes: discordPrettyBytes,
|
||
parseMeminfo: discordParseMeminfo,
|
||
parseSystemctlList: discordParseSystemctlList,
|
||
formatRlimits: discordFormatRlimits,
|
||
formatFeatures: discordFormatFeatures,
|
||
formatDoctor: discordFormatDoctor,
|
||
formatSwarm: discordFormatSwarm,
|
||
formatNetSummary: discordFormatNetSummary,
|
||
formatHostDf: discordFormatHostDf,
|
||
pathWriteOk: discordCmdPathWriteOk,
|
||
editChunks: discordEditChunks,
|
||
editPut: discordEditPut,
|
||
editGet: discordEditGet,
|
||
editModalPayload: discordEditModalPayload,
|
||
joinPath: discordJoinPath,
|
||
parentPath: discordParentPath,
|
||
settingsSpecs: BARE_OS_DISCORD_SETTINGS,
|
||
settingsApply: discordSettingsApply,
|
||
settingsCurrent: discordSettingsCurrent,
|
||
uniqueComponents: discordUniqueComponents,
|
||
navRows: discordNavRows,
|
||
result: discordResult,
|
||
packEmbed: discordPackEmbed,
|
||
textPages: discordTextPages,
|
||
clipSmart: discordClipSmart,
|
||
embedSize: discordEmbedSize,
|
||
LIMIT: BARE_OS_DISCORD_LIMIT,
|
||
idleMs: BARE_OS_DISCORD_IDLE_MS,
|
||
idleExpire: discordIdleExpire,
|
||
idleSweep: discordIdleSweep,
|
||
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
|
||
}
|