embed size limits

This commit is contained in:
Raven Scott
2026-08-13 14:20:11 -04:00
parent 58e6e62ee9
commit 0d08dfd0d2
11 changed files with 1329 additions and 406 deletions
@@ -6,6 +6,20 @@
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 =
@@ -126,7 +140,44 @@ function discordCmdClip(text, max) {
const s = String(text == null ? '' : text)
const n = max || BARE_OS_DISCORD_REPLY_MAX
if (s.length <= n) return s
return s.slice(0, n - 20) + '\n…(truncated)'
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) {
@@ -135,9 +186,126 @@ function discordCmdRedact(text) {
.replace(/(DISCORD_TOKEN|BOT_TOKEN|TOKEN|SECRET|PASSWORD|PASSWD|API_KEY)\s*[=:]\s*\S+/gi, '$1=[redacted]')
}
function discordCmdFence(text, lang) {
const body = discordCmdClip(discordCmdRedact(text))
return '```' + (lang || '') + '\n' + body.replace(/```/g, '`ˋ`') + '\n```'
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) {
@@ -340,15 +508,20 @@ function discordPrettySnapshot(title, raw, extras) {
const parsed = discordTryJson(raw)
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
const text = raw == null ? '' : String(raw)
return discordResult(
discordEmbed({
if (!text) {
return discordResult(discordEmbed({ title: title, desc: 'unavailable' }), extras)
}
return {
more: {
title: title,
desc: text
? discordCmdFence(discordCmdClip(text, 1400))
: 'unavailable'
}),
extras
)
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)
@@ -479,34 +652,119 @@ function discordCmdReplyPayload(result) {
}
function discordEmbed(opts) {
const o = opts || {}
const e = {
color: o.color == null ? BARE_OS_DISCORD_COLOR : o.color,
timestamp: new Date().toISOString(),
footer: { text: o.footer || 'Bare OS · expires after 2m idle' }
}
if (o.title) e.title = String(o.title).slice(0, 256)
if (o.desc) e.description = discordCmdClip(discordCmdRedact(o.desc), 1800)
if (o.fields && o.fields.length) {
const fields = []
for (let i = 0; i < o.fields.length && fields.length < 25; i++) {
const f = o.fields[i]
if (!f) continue
const v = String(f.value == null ? '' : f.value).trim()
if (!v || v === '—') continue
fields.push(f)
}
if (fields.length) e.fields = fields
}
if (o.author) e.author = o.author
return e
return discordPackEmbed(opts).embed
}
function discordField(name, value, inline) {
let v = value == null || value === '' ? '—' : String(value)
v = discordCmdClip(discordCmdRedact(v), 1024)
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, 256), value: v, inline: Boolean(inline) }
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) {
@@ -1241,12 +1499,10 @@ async function discordCmdHandleBare(ctx, sub) {
}
if (sub === 'motd') {
const t = await discordCmdReadText(ctx, '/etc/motd')
return discordResult(
discordEmbed({
title: 'motd',
desc: t ? discordCmdFence(t) : '(no /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 }
}
@@ -1646,19 +1902,26 @@ async function discordCmdHandleSvc(ctx, sub, unit) {
: sub === 'start' || sub === 'restart'
? BARE_OS_DISCORD_COLOR_OK
: BARE_OS_DISCORD_COLOR
return discordResult(
discordEmbed({
if (!out) {
return discordResult(
discordEmbed({
title: name,
desc: 'No output from `systemctl ' + sub + '`.',
color: color
}),
{ components: act ? [act] : [] }
)
}
return {
more: {
title: name,
desc: out
? '`' +
sub +
'`\n' +
discordCmdFence(discordCmdClip(discordCmdRedact(out), 1100))
: 'No output from `systemctl ' + sub + '`.',
color: color
}),
{ components: act ? [act] : [] }
)
lead: '`' + sub + '`',
body: discordCmdRedact(out),
fence: true,
color: color,
components: act ? [act] : []
}
}
}
async function discordCmdHandleFs(ctx, sub, rawPath, nlines) {
@@ -1682,22 +1945,21 @@ async function discordCmdHandleFs(ctx, sub, rawPath, nlines) {
})
.join(' ')
: '(empty)'
return discordResult(
discordEmbed({
return {
more: {
title: 'Listing',
desc:
lead:
'`' +
p +
'` · ' +
list.length +
' ' +
(list.length === 1 ? 'entry' : 'entries') +
'\n' +
desc +
(list.length > shown.length ? '\n+' + (list.length - shown.length) + ' more' : ''),
(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 }
}
@@ -1753,23 +2015,30 @@ async function discordCmdHandleFs(ctx, sub, rawPath, nlines) {
return discordPrettySnapshot(p, asJson, extra)
}
if (sub === 'head') {
const n = Math.max(1, Math.min(40, Number(nlines) || 12))
return discordResult(
discordEmbed({
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',
desc: '`' + p + '` · first ' + n + ' lines\n' + discordCmdFence(text.split(/\r?\n/).slice(0, n).join('\n'))
}),
extra
)
lead: '`' + p + '` · first ' + n + ' lines',
body: head,
fence: true,
components: extra.components,
editPath: extra.editPath
}
}
}
return discordResult(
discordEmbed({
return {
more: {
title: 'File',
desc: '`' + p + '`\n' + discordCmdFence(text.slice(0, BARE_OS_DISCORD_FS_MAX)),
footer: text.length + ' bytes · ' + discordSessionUser(ctx)
}),
extra
)
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) {
@@ -1962,13 +2231,13 @@ async function discordCmdHandleMan(ctx, page) {
const clean = String(out)
.replace(/\x1b\[[0-9;]*m/g, '')
.trim()
const first = clean.split(/\r?\n/).slice(0, 28).join('\n')
return discordResult(
discordEmbed({
return {
more: {
title: 'man ' + name,
desc: discordCmdClip(first, 1600)
})
)
body: clean,
fence: false
}
}
}
}
const t = await discordCmdReadText(ctx, '/share/man/man.json')
@@ -1979,14 +2248,14 @@ async function discordCmdHandleMan(ctx, page) {
for (let i = 0; i < pages.length; i++) {
if (pages[i] && pages[i].name === name) {
const p = pages[i]
return discordResult(
discordEmbed({
return {
more: {
title: 'man ' + (p.title || name),
desc:
(p.synopsis && p.synopsis[0] ? '`' + p.synopsis[0] + '`\n\n' : '') +
String(p.description || '').slice(0, 1400)
})
)
lead: p.synopsis && p.synopsis[0] ? '`' + p.synopsis[0] + '`' : '',
body: String(p.description || ''),
fence: false
}
}
}
}
} catch {
@@ -2036,15 +2305,23 @@ async function discordCmdHandleRun(ctx, raw) {
if (parsed && typeof parsed === 'object') {
return discordPrettySnapshot('$ ' + key, parsed)
}
return discordResult(
discordEmbed({
if (!out) {
return discordResult(
discordEmbed({
title: '$ ' + key,
footer: 'ran as ' + discordSessionUser(ctx),
desc: '(no output, exit ' + String(ctx.exitCode || 0) + ')'
})
)
}
return {
more: {
title: '$ ' + key,
footer: 'ran as ' + discordSessionUser(ctx),
desc: out
? discordCmdFence(discordCmdClip(discordCmdRedact(out), 1400))
: '(no output, exit ' + String(ctx.exitCode || 0) + ')'
})
)
body: discordCmdRedact(out),
fence: true,
footer: 'ran as ' + discordSessionUser(ctx)
}
}
}
async function discordCmdHandleJournal(ctx, unit) {
@@ -2058,47 +2335,23 @@ async function discordCmdHandleJournal(ctx, unit) {
'30'
])
})
const lines = String(out || '')
.split(/\r?\n/)
.filter(Boolean)
.slice(-20)
return discordResult(
discordEmbed({
title: 'Journal · ' + unit,
desc: lines.length
? discordCmdFence(
lines
.map(function (ln) {
return discordCmdRedact(ln).slice(0, 180)
})
.join('\n')
)
: 'Empty journal for `' + unit + '`.'
})
)
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 lines = String(t || '')
.split(/\r?\n/)
.filter(Boolean)
.slice(-18)
return discordResult(
discordEmbed({
title: 'Journal',
desc: lines.length
? discordCmdFence(
lines
.map(function (ln) {
return discordCmdRedact(ln).slice(0, 180)
})
.join('\n')
)
: 'No journal in `/var/log/bare-os`.'
})
)
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) {
@@ -3697,6 +3950,7 @@ function discordIdleForgetSessions(userId) {
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]
}
function discordUnwrapMessage(sent) {
@@ -3815,6 +4069,9 @@ async function discordIdleWatch(interaction, payload, sent) {
}
async function discordSendResult(ctx, interaction, result) {
if (result && result.more) {
return discordSendResult(ctx, interaction, discordLongTextResult(interaction, result.more))
}
if (result && result.editPath) {
const prev = discordEditGet(interaction)
discordEditPut(interaction, {
@@ -4128,6 +4385,17 @@ async function discordDispatchComponent(ctx, interaction) {
const opt = function () {
return ''
}
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
@@ -4596,6 +4864,11 @@ var bareOsDiscordCommands = {
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,