@@ -23,6 +23,7 @@ const preamble = {
|
||||
awk: ['awk-engine.js'],
|
||||
jq: ['jq-engine.js'],
|
||||
man: ['man-render.js'],
|
||||
'discord-bot': ['bare-os-discord-commands-guest.js'],
|
||||
ls: ['bare-os-lscolors.js', 'ls-colors.js'],
|
||||
dircolors: ['bare-os-lscolors.js'],
|
||||
edit: [
|
||||
@@ -199,7 +200,12 @@ export async function build() {
|
||||
const chunkPath =
|
||||
f === 'bare-os-lscolors.js'
|
||||
? join(repoRoot, 'packages/bare-os-lscolors/bare-os-lscolors.js')
|
||||
: join(__dirname, 'lib', f)
|
||||
: f === 'bare-os-discord-commands-guest.js'
|
||||
? join(
|
||||
repoRoot,
|
||||
'packages/bare-os-booter/lib/bare-os-discord-commands-guest.js'
|
||||
)
|
||||
: join(__dirname, 'lib', f)
|
||||
let chunk = await readFile(chunkPath, 'utf8')
|
||||
if (f === 'bare-os-lscolors.js')
|
||||
chunk = stripLscolorsBundleExport(chunk)
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"synopsis": [
|
||||
"discord-bot [--env PATH] [--token TOKEN] [--guild ID] [--check] [--debug] [--message-content] [--login-timeout MS]"
|
||||
],
|
||||
"description": "Foreground Discord ping-pong bot using ctx.bare.discordJS (vendored bare-discord-js / official discord.js). Ctrl+C (or host SIGINT) stops the process. Replies pong to the /ping slash command. Channel message ping/pong is opt-in: pass --message-content (or DISCORD_MESSAGE_CONTENT=1) and enable MESSAGE CONTENT INTENT in the Discord Developer Portal. Token comes from --token, guest DISCORD_TOKEN, or a VFS .env file (--env, DISCORD_ENV_FILE, ~/.discord/.env, ~/.discord.env). The initd unit bare-os-discord is registered and listed by systemctl only when ~/.discord/.env exists with DISCORD_TOKEN=. Disable the unit with BARE_OS_DISCORD_INITD=0. Custom bots should use ctx.bare.discordJS.Client the same way (see examples/discord-ping-pong).",
|
||||
"description": "Bare OS Discord bot via ctx.bare.discordJS. Foreground Ctrl+C stops the process. Slash commands: /bare (status, uname, whoami, …), /sys (df, mem, ps, env, doctor), /svc (systemctl), /fs (ls/cat/stat), /net (swarm), /man, /say, /run (allowlisted utilities), /journal, /ping. Channel message ping still replies pong when Message Content Intent is enabled. Token from --token, DISCORD_TOKEN, or ~/.discord/.env. Optional DISCORD_ID_WHITELIST=id,id in that .env restricts slash commands and channel ping replies to those Discord user ids (anyone else is denied). The initd unit bare-os-discord is listed by systemctl only when ~/.discord/.env exists. Disable with BARE_OS_DISCORD_INITD=0.",
|
||||
"options": [
|
||||
{
|
||||
"flag": "--env PATH",
|
||||
@@ -42,6 +42,7 @@
|
||||
"DISCORD_ENV_FILE / BARE_OS_DISCORD_ENV_FILE — path to a .env file. On the host this may be a host filesystem path (booter reads it). In the guest it is a VFS path. Preferred guest path: ~/.discord/.env.",
|
||||
"BARE_OS_DISCORD_INITD — set 0 / false to never register the bare-os-discord systemctl unit, even when ~/.discord/.env exists.",
|
||||
"DISCORD_GUILD_ID — optional guild for slash-command registration.",
|
||||
"DISCORD_ID_WHITELIST — comma-separated Discord user ids allowed to use the bot. Unset or empty allows everyone. A user not on a non-empty list is denied (ephemeral for slash commands).",
|
||||
"DISCORD_MESSAGE_CONTENT / BARE_OS_DISCORD_MESSAGE_CONTENT — set 1 to request Message Content Intent (same as --message-content).",
|
||||
"DISCORD_DEBUG / BARE_OS_DISCORD_DEBUG — set 1 to print discord.js debug lines.",
|
||||
"DISCORD_LOGIN_TIMEOUT_MS — login deadline in milliseconds (default 45000).",
|
||||
@@ -52,6 +53,10 @@
|
||||
{
|
||||
"name": "bare-os-ctx-bare",
|
||||
"section": 7
|
||||
},
|
||||
{
|
||||
"name": "devguide-21-discord-bots",
|
||||
"section": 7
|
||||
}
|
||||
],
|
||||
"bareOsNotes": "Requires a Bare/Pear host so vendored bare-discord-js can remap Node builtins. Guest scripts have no import/require; use ctx.bare.discordJS.",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* Stock Discord ping-pong bot via ctx.bare.discordJS (vendored bare-discord-js).
|
||||
* Token: --token, DISCORD_TOKEN, --env PATH, DISCORD_ENV_FILE, ~/.discord.env.
|
||||
* Optional DISCORD_ID_WHITELIST=id,id in .env restricts who may use the bot.
|
||||
*/
|
||||
|
||||
function discordNormalizeToken(raw) {
|
||||
@@ -164,8 +165,9 @@ function discordUsage(argv0) {
|
||||
'host DISCORD_ENV_FILE, ~/.discord/.env, ~/.discord.env).\n' +
|
||||
'Ctrl+C stops the foreground bot. Initd unit bare-os-discord appears in\n' +
|
||||
'systemctl only when ~/.discord/.env exists with DISCORD_TOKEN=.\n' +
|
||||
'Replies pong to /ping. Message "ping" needs --message-content and the\n' +
|
||||
'Message Content Intent in the Developer Portal (privileged).'
|
||||
'Slash commands: /bare /sys /svc /fs /net /man /say /run /journal /ping.\n' +
|
||||
'Channel "ping" still replies pong when Message Content Intent is enabled.\n' +
|
||||
'DISCORD_ID_WHITELIST=id,id in .env restricts the bot to those Discord user ids.'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -214,15 +216,54 @@ async function discordLoadToken(ctx, argv) {
|
||||
const parsed = discordParseDotEnvText(text)
|
||||
const token = discordNormalizeToken(parsed.DISCORD_TOKEN || '')
|
||||
if (token) {
|
||||
if (parsed.DISCORD_GUILD_ID && ctx.env && !ctx.env.DISCORD_GUILD_ID) {
|
||||
ctx.env.DISCORD_GUILD_ID = String(parsed.DISCORD_GUILD_ID).trim()
|
||||
}
|
||||
discordApplyDotEnvExtras(ctx, parsed)
|
||||
return { token: token, source: p }
|
||||
}
|
||||
}
|
||||
return { token: '', source: '' }
|
||||
}
|
||||
|
||||
function discordApplyDotEnvExtras(ctx, parsed) {
|
||||
if (!parsed || !ctx) return
|
||||
if (!ctx.env) ctx.env = {}
|
||||
const extras = ['DISCORD_GUILD_ID', 'DISCORD_ID_WHITELIST']
|
||||
for (let i = 0; i < extras.length; i++) {
|
||||
const k = extras[i]
|
||||
const v = parsed[k]
|
||||
if (v == null || String(v).trim() === '') continue
|
||||
if (!ctx.env[k] || String(ctx.env[k]).trim() === '') {
|
||||
ctx.env[k] = String(v).trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function discordHydrateEnvFromFiles(ctx, argv) {
|
||||
const envPaths = []
|
||||
const flagPath = discordArgValue(argv, ['--env', '--env-file'])
|
||||
if (flagPath) envPaths.push(flagPath)
|
||||
if (ctx.env && ctx.env.DISCORD_ENV_FILE)
|
||||
envPaths.push(ctx.env.DISCORD_ENV_FILE)
|
||||
if (ctx.env && ctx.env.BARE_OS_DISCORD_ENV_FILE) {
|
||||
envPaths.push(ctx.env.BARE_OS_DISCORD_ENV_FILE)
|
||||
}
|
||||
envPaths.push(
|
||||
'~/.discord/.env',
|
||||
'~/.discord.env',
|
||||
'~/discord.env',
|
||||
'./.env',
|
||||
'~/.env'
|
||||
)
|
||||
const seen = {}
|
||||
for (let i = 0; i < envPaths.length; i++) {
|
||||
const p = String(envPaths[i] || '').trim()
|
||||
if (!p || seen[p]) continue
|
||||
seen[p] = true
|
||||
const text = await discordReadVfsText(ctx, p)
|
||||
if (!text) continue
|
||||
discordApplyDotEnvExtras(ctx, discordParseDotEnvText(text))
|
||||
}
|
||||
}
|
||||
|
||||
async function run(ctx, argv) {
|
||||
if (discordHasFlag(argv, ['-h', '--help'])) {
|
||||
ctx.console.log(discordUsage(argv[0]))
|
||||
@@ -256,6 +297,7 @@ async function run(ctx, argv) {
|
||||
}
|
||||
|
||||
const loaded = await discordLoadToken(ctx, argv)
|
||||
await discordHydrateEnvFromFiles(ctx, argv)
|
||||
const checkOnly =
|
||||
discordHasFlag(argv, ['--check', '--dry-run']) ||
|
||||
(argv[1] && String(argv[1]) === 'check')
|
||||
@@ -319,10 +361,15 @@ async function run(ctx, argv) {
|
||||
}
|
||||
}
|
||||
})
|
||||
const pingCommand = new SlashCommandBuilder()
|
||||
.setName('ping')
|
||||
.setDescription('Replies with pong.')
|
||||
.toJSON()
|
||||
const slashBody =
|
||||
typeof discordBuildSlashCommands === 'function'
|
||||
? discordBuildSlashCommands(dj)
|
||||
: [
|
||||
new SlashCommandBuilder()
|
||||
.setName('ping')
|
||||
.setDescription('Replies with pong.')
|
||||
.toJSON()
|
||||
]
|
||||
|
||||
client.once(Events.ClientReady, async function (readyClient) {
|
||||
ctx.console.log('Logged in as ' + readyClient.user.tag)
|
||||
@@ -332,15 +379,19 @@ async function run(ctx, argv) {
|
||||
const appId = readyClient.user.id
|
||||
if (guildId) {
|
||||
await rest.put(Routes.applicationGuildCommands(appId, guildId), {
|
||||
body: [pingCommand]
|
||||
})
|
||||
ctx.console.log('Registered /ping for guild ' + guildId)
|
||||
} else {
|
||||
await rest.put(Routes.applicationCommands(appId), {
|
||||
body: [pingCommand]
|
||||
body: slashBody
|
||||
})
|
||||
ctx.console.log(
|
||||
'Registered global /ping (may take up to ~1 hour). Set DISCORD_GUILD_ID or --guild for instant updates.'
|
||||
'Registered ' + slashBody.length + ' slash commands for guild ' + guildId
|
||||
)
|
||||
} else {
|
||||
await rest.put(Routes.applicationCommands(appId), {
|
||||
body: slashBody
|
||||
})
|
||||
ctx.console.log(
|
||||
'Registered ' +
|
||||
slashBody.length +
|
||||
' global slash commands (may take up to ~1 hour). Set DISCORD_GUILD_ID or --guild for instant updates.'
|
||||
)
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -352,9 +403,32 @@ async function run(ctx, argv) {
|
||||
})
|
||||
|
||||
client.on(Events.InteractionCreate, async function (interaction) {
|
||||
if (typeof discordDispatchInteraction === 'function') {
|
||||
await discordDispatchInteraction(ctx, interaction)
|
||||
return
|
||||
}
|
||||
if (!interaction.isChatInputCommand || !interaction.isChatInputCommand())
|
||||
return
|
||||
if (interaction.commandName !== 'ping') return
|
||||
const uid =
|
||||
interaction.user && interaction.user.id ? interaction.user.id : ''
|
||||
if (typeof discordUserAllowed === 'function' && !discordUserAllowed(ctx, uid)) {
|
||||
try {
|
||||
await interaction.reply({
|
||||
content:
|
||||
typeof BARE_OS_DISCORD_WHITELIST_DENY === 'string'
|
||||
? BARE_OS_DISCORD_WHITELIST_DENY
|
||||
: 'Access denied. Your Discord user id is not on DISCORD_ID_WHITELIST.',
|
||||
ephemeral: true
|
||||
})
|
||||
} catch (err) {
|
||||
ctx.console.error(
|
||||
'discord-bot: deny reply failed: ' +
|
||||
((err && err.message) || String(err))
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
try {
|
||||
await interaction.reply({ content: 'pong' })
|
||||
} catch (err) {
|
||||
@@ -372,6 +446,21 @@ async function run(ctx, argv) {
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
if (text !== 'ping') return
|
||||
const uid = message.author && message.author.id ? message.author.id : ''
|
||||
if (typeof discordUserAllowed === 'function' && !discordUserAllowed(ctx, uid)) {
|
||||
try {
|
||||
await message.reply(
|
||||
typeof BARE_OS_DISCORD_WHITELIST_DENY === 'string'
|
||||
? BARE_OS_DISCORD_WHITELIST_DENY
|
||||
: 'Access denied. Your Discord user id is not on DISCORD_ID_WHITELIST.'
|
||||
)
|
||||
} catch (err) {
|
||||
ctx.console.error(
|
||||
'discord-bot: deny reply failed: ' + discordErrText(err)
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
try {
|
||||
await message.reply('pong')
|
||||
} catch (err) {
|
||||
@@ -475,6 +564,22 @@ async function run(ctx, argv) {
|
||||
ctx.console.log(
|
||||
'discord-bot: logging in (token from ' + loaded.source + ')...'
|
||||
)
|
||||
if (typeof discordParseIdWhitelist === 'function') {
|
||||
const wl = discordParseIdWhitelist(
|
||||
(ctx.env &&
|
||||
(ctx.env.DISCORD_ID_WHITELIST || ctx.env.BARE_OS_DISCORD_ID_WHITELIST)) ||
|
||||
''
|
||||
)
|
||||
let wlN = 0
|
||||
for (const k in wl) {
|
||||
if (Object.prototype.hasOwnProperty.call(wl, k)) wlN++
|
||||
}
|
||||
if (wlN) {
|
||||
ctx.console.log(
|
||||
'discord-bot: DISCORD_ID_WHITELIST active (' + wlN + ' user id(s))'
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!wantsMessageContent) {
|
||||
ctx.console.log(
|
||||
'discord-bot: /ping only (pass --message-content after enabling Message Content Intent for channel ping/pong)'
|
||||
|
||||
Reference in New Issue
Block a user