@@ -40,6 +40,16 @@ var BARE_OS_DISCORD_KNOWN_UNITS = [
|
||||
'bare-holesail',
|
||||
'kernel-logger'
|
||||
]
|
||||
var BARE_OS_DISCORD_EDIT_CHUNK = 4000
|
||||
var BARE_OS_DISCORD_EDIT_MAX_CHUNKS = 5
|
||||
var BARE_OS_DISCORD_EDIT_TTL_MS = 15 * 60 * 1000
|
||||
var BARE_OS_DISCORD_EDIT_SESSIONS = Object.create(null)
|
||||
var BARE_OS_DISCORD_EDIT_SUGGEST = [
|
||||
'~/notes.txt',
|
||||
'~/.barerc',
|
||||
'~/TODO.md',
|
||||
'/tmp/scratch.txt'
|
||||
]
|
||||
var BARE_OS_DISCORD_FS_SUGGEST = [
|
||||
'~',
|
||||
'/etc/os-release',
|
||||
@@ -466,8 +476,13 @@ function discordNavRows() {
|
||||
{ id: 'say:compose', label: 'Say…', style: 3 },
|
||||
{ id: 'run:compose', label: 'Run…' }
|
||||
])
|
||||
const c = discordButtons([
|
||||
{ id: 'edit:new', label: 'Edit file…', style: 1 },
|
||||
{ id: 'create:new', label: 'Create file…', style: 3 }
|
||||
])
|
||||
if (a) rows.push(a)
|
||||
if (b) rows.push(b)
|
||||
if (c) rows.push(c)
|
||||
return rows
|
||||
}
|
||||
|
||||
@@ -479,6 +494,8 @@ function discordResult(embed, extra) {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -524,6 +541,146 @@ function discordCmdPathOk(raw) {
|
||||
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 || {}
|
||||
@@ -653,6 +810,15 @@ function discordSuggestRun(q) {
|
||||
return discordFilterChoices(names, q)
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -737,7 +903,10 @@ async function discordCmdHandleBare(ctx, sub) {
|
||||
discordField('/svc', 'list · status · start · stop · restart · logs *(unit autocomplete)*'),
|
||||
discordField('/fs', 'ls · cat · stat · head *(path autocomplete)*'),
|
||||
discordField('/net', 'peers · swarm · summary'),
|
||||
discordField('/man /run /journal /say /panel /ping', 'Lookups, allowlisted run, logs, compose modal')
|
||||
discordField(
|
||||
'/man /run /journal /say /edit /create /panel /ping',
|
||||
'Lookups, run, logs, **edit** / **create** file modals'
|
||||
)
|
||||
]
|
||||
})
|
||||
)
|
||||
@@ -1178,9 +1347,17 @@ async function discordCmdHandleFs(ctx, sub, rawPath, nlines) {
|
||||
if (!text) {
|
||||
return discordResult(discordEmbed({ title: p, desc: '(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((sub === 'head' ? 'head ' : '') + p, asJson)
|
||||
return discordPrettySnapshot((sub === 'head' ? 'head ' : '') + p, asJson, extra)
|
||||
}
|
||||
if (sub === 'head') {
|
||||
const n = Math.max(1, Math.min(40, Number(nlines) || 12))
|
||||
@@ -1188,7 +1365,8 @@ async function discordCmdHandleFs(ctx, sub, rawPath, nlines) {
|
||||
discordEmbed({
|
||||
title: 'head ' + p,
|
||||
desc: discordCmdFence(text.split(/\r?\n/).slice(0, n).join('\n'))
|
||||
})
|
||||
}),
|
||||
extra
|
||||
)
|
||||
}
|
||||
return discordResult(
|
||||
@@ -1196,7 +1374,8 @@ async function discordCmdHandleFs(ctx, sub, rawPath, nlines) {
|
||||
title: p,
|
||||
desc: discordCmdFence(text.slice(0, BARE_OS_DISCORD_FS_MAX)),
|
||||
footer: text.length + ' bytes · ' + discordSessionUser(ctx)
|
||||
})
|
||||
}),
|
||||
extra
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1479,6 +1658,12 @@ function discordBuildSlashCommands(dj) {
|
||||
const journal = new B()
|
||||
.setName('journal')
|
||||
.setDescription('Tail a unit or system log')
|
||||
const edit = new B()
|
||||
.setName('edit')
|
||||
.setDescription('Edit a text file in a Discord modal (home or /tmp)')
|
||||
const create = new B()
|
||||
.setName('create')
|
||||
.setDescription('Create a new text file (pick a path, then enter contents)')
|
||||
const panel = new B()
|
||||
.setName('panel')
|
||||
.setDescription('Interactive Bare OS control panel')
|
||||
@@ -1550,7 +1735,9 @@ function discordBuildSlashCommands(dj) {
|
||||
discordCmdOpt(say, 'text', 'Text to box (omit to open a form)', false, false)
|
||||
discordCmdOpt(run, 'cmd', 'Allowlisted utility', false, true)
|
||||
discordCmdOpt(journal, 'unit', 'Unit name', false, true)
|
||||
return [bare, sys, svc, fsCmd, net, man, say, run, journal, panel, ping].map(
|
||||
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)
|
||||
return [bare, sys, svc, fsCmd, net, man, say, run, journal, edit, create, panel, ping].map(
|
||||
function (c) {
|
||||
return c.toJSON()
|
||||
}
|
||||
@@ -1608,10 +1795,284 @@ async function discordRouteCommand(ctx, name, sub, opt) {
|
||||
return discordCmdHandleRun(ctx, 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 }
|
||||
}
|
||||
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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function discordSendResult(ctx, interaction, result) {
|
||||
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.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',
|
||||
@@ -1680,7 +2141,13 @@ async function discordDispatchAutocomplete(ctx, interaction) {
|
||||
if (fname === 'unit') choices = await discordSuggestUnits(ctx, q)
|
||||
else if (fname === 'page') choices = await discordSuggestMan(ctx, q)
|
||||
else if (fname === 'cmd') choices = discordSuggestRun(q)
|
||||
else if (fname === 'path') choices = discordSuggestPaths(ctx, q)
|
||||
else if (fname === 'path') {
|
||||
const cmd = String(interaction.commandName || '')
|
||||
choices =
|
||||
cmd === 'edit' || cmd === 'create'
|
||||
? discordSuggestEditPaths(ctx, q)
|
||||
: discordSuggestPaths(ctx, q)
|
||||
}
|
||||
} catch {
|
||||
choices = []
|
||||
}
|
||||
@@ -1717,6 +2184,67 @@ async function discordDispatchComponent(ctx, interaction) {
|
||||
})
|
||||
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)))
|
||||
}
|
||||
@@ -1755,6 +2283,108 @@ async function discordDispatchModal(ctx, interaction) {
|
||||
} catch {
|
||||
value = ''
|
||||
}
|
||||
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,
|
||||
@@ -1862,7 +2492,12 @@ var bareOsDiscordCommands = {
|
||||
formatRlimits: discordFormatRlimits,
|
||||
formatFeatures: discordFormatFeatures,
|
||||
formatSwarm: discordFormatSwarm,
|
||||
formatHostDf: discordFormatHostDf
|
||||
formatHostDf: discordFormatHostDf,
|
||||
pathWriteOk: discordCmdPathWriteOk,
|
||||
editChunks: discordEditChunks,
|
||||
editPut: discordEditPut,
|
||||
editGet: discordEditGet,
|
||||
editModalPayload: discordEditModalPayload
|
||||
}
|
||||
if (typeof module !== 'undefined' && module.exports) {
|
||||
module.exports = bareOsDiscordCommands
|
||||
|
||||
Reference in New Issue
Block a user