@@ -3,9 +3,9 @@
|
||||
"section": 1,
|
||||
"title": "discord-bot",
|
||||
"synopsis": [
|
||||
"discord-bot [--env PATH] [--token TOKEN] [--guild ID] [--check]"
|
||||
"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). Replies pong to the /ping slash command and to the message ping. Token comes from --token, guest DISCORD_TOKEN, a VFS .env file (--env, DISCORD_ENV_FILE, ~/.discord.env), or a host DISCORD_ENV_FILE / DISCORD_TOKEN copied into the session by the booter. Enable MESSAGE CONTENT INTENT in the Discord Developer Portal for channel message ping/pong; /ping works without that privileged intent. Custom bots should use ctx.bare.discordJS.Client the same way (see examples/discord-ping-pong).",
|
||||
"description": "Foreground Discord ping-pong bot using ctx.bare.discordJS (vendored bare-discord-js / official discord.js). 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. Requesting that privileged intent without enabling it closes the gateway (4014) and used to hang after 'logging in'. Token comes from --token, guest DISCORD_TOKEN, a VFS .env file (--env, DISCORD_ENV_FILE, ~/.discord.env), or a host DISCORD_ENV_FILE / DISCORD_TOKEN copied into the session by the booter. Custom bots should use ctx.bare.discordJS.Client the same way (see examples/discord-ping-pong).",
|
||||
"options": [
|
||||
{
|
||||
"flag": "--env PATH",
|
||||
@@ -22,6 +22,18 @@
|
||||
{
|
||||
"flag": "--check",
|
||||
"meaning": "Verify ctx.bare.discordJS and that a token can be resolved; do not login."
|
||||
},
|
||||
{
|
||||
"flag": "--debug",
|
||||
"meaning": "Print discord.js debug lines (also DISCORD_DEBUG=1)."
|
||||
},
|
||||
{
|
||||
"flag": "--message-content",
|
||||
"meaning": "Request the privileged Message Content Intent so channel 'ping' replies pong. Enable the same intent in the Developer Portal first."
|
||||
},
|
||||
{
|
||||
"flag": "--login-timeout MS",
|
||||
"meaning": "Fail login if the gateway is not ready after MS milliseconds (default 45000)."
|
||||
}
|
||||
],
|
||||
"keywords": ["discord", "bot", "ping", "pong", "ctx.bare"],
|
||||
@@ -29,6 +41,9 @@
|
||||
"DISCORD_TOKEN — bot token in the guest session (also copied from the host when set).",
|
||||
"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.",
|
||||
"DISCORD_GUILD_ID — optional guild for slash-command registration.",
|
||||
"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).",
|
||||
"BARE_OS_DISCORD — set 0 / false to skip loading ctx.bare.discordJS.",
|
||||
"BARE_OS_BARE_MODULES — set 0 to omit ctx.bare entirely."
|
||||
],
|
||||
|
||||
@@ -67,15 +67,60 @@ function discordHasFlag(argv, names) {
|
||||
return false
|
||||
}
|
||||
|
||||
function discordEnvFlag(ctx, keys) {
|
||||
if (!ctx.env) return false
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const v = String(ctx.env[keys[i]] || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
if (v === '1' || v === 'true' || v === 'yes') return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function discordWantsMessageContent(ctx, argv) {
|
||||
return (
|
||||
discordHasFlag(argv, ['--message-content', '--privileged-intents']) ||
|
||||
discordEnvFlag(ctx, [
|
||||
'DISCORD_MESSAGE_CONTENT',
|
||||
'BARE_OS_DISCORD_MESSAGE_CONTENT'
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
function discordDebugOn(ctx, argv) {
|
||||
return (
|
||||
discordHasFlag(argv, ['--debug', '-v', '--verbose']) ||
|
||||
discordEnvFlag(ctx, ['DISCORD_DEBUG', 'BARE_OS_DISCORD_DEBUG'])
|
||||
)
|
||||
}
|
||||
|
||||
function discordLoginTimeoutMs(ctx, argv) {
|
||||
const raw =
|
||||
discordArgValue(argv, ['--login-timeout']) ||
|
||||
(ctx.env &&
|
||||
(ctx.env.DISCORD_LOGIN_TIMEOUT_MS ||
|
||||
ctx.env.BARE_OS_DISCORD_LOGIN_TIMEOUT_MS)) ||
|
||||
'45000'
|
||||
const n = Number(raw)
|
||||
return Number.isFinite(n) && n > 0 ? n : 45000
|
||||
}
|
||||
|
||||
function discordErrText(err) {
|
||||
return (err && err.message) || String(err)
|
||||
}
|
||||
|
||||
function discordUsage(argv0) {
|
||||
return (
|
||||
'usage: ' +
|
||||
(argv0 || 'discord-bot') +
|
||||
' [--env PATH] [--token TOKEN] [--guild ID] [--check]\n' +
|
||||
' [--debug] [--message-content] [--login-timeout MS]\n' +
|
||||
'Ping-pong bot via ctx.bare.discordJS.\n' +
|
||||
'Token from --token, DISCORD_TOKEN, or a .env file (--env, DISCORD_ENV_FILE,\n' +
|
||||
'host DISCORD_ENV_FILE, ~/.discord.env).\n' +
|
||||
'Replies pong to /ping and to the message "ping".'
|
||||
'Replies pong to /ping. Message "ping" needs --message-content and the\n' +
|
||||
'Message Content Intent in the Developer Portal (privileged).'
|
||||
)
|
||||
}
|
||||
|
||||
@@ -202,13 +247,18 @@ async function run(ctx, argv) {
|
||||
const Routes = dj.Routes
|
||||
const SlashCommandBuilder = dj.SlashCommandBuilder
|
||||
|
||||
const wantsMessageContent = discordWantsMessageContent(ctx, argv)
|
||||
const debugOn = discordDebugOn(ctx, argv)
|
||||
const loginMs = discordLoginTimeoutMs(ctx, argv)
|
||||
|
||||
const intents = [GatewayIntentBits.Guilds]
|
||||
if (GatewayIntentBits.GuildMessages)
|
||||
intents.push(GatewayIntentBits.GuildMessages)
|
||||
if (GatewayIntentBits.DirectMessages)
|
||||
intents.push(GatewayIntentBits.DirectMessages)
|
||||
if (GatewayIntentBits.MessageContent)
|
||||
if (wantsMessageContent && GatewayIntentBits.MessageContent) {
|
||||
intents.push(GatewayIntentBits.MessageContent)
|
||||
}
|
||||
|
||||
const client = new Client({ intents: intents })
|
||||
const pingCommand = new SlashCommandBuilder()
|
||||
@@ -268,37 +318,122 @@ async function run(ctx, argv) {
|
||||
await message.reply('pong')
|
||||
} catch (err) {
|
||||
ctx.console.error(
|
||||
'discord-bot: message reply failed: ' +
|
||||
((err && err.message) || String(err))
|
||||
'discord-bot: message reply failed: ' + discordErrText(err)
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
client.on(Events.Error, function (error) {
|
||||
ctx.console.error(
|
||||
'discord-bot: client error: ' +
|
||||
((error && error.message) || String(error))
|
||||
)
|
||||
ctx.console.error('discord-bot: client error: ' + discordErrText(error))
|
||||
})
|
||||
|
||||
if (Events.ShardError) {
|
||||
client.on(Events.ShardError, function (error) {
|
||||
ctx.console.error('discord-bot: shard error: ' + discordErrText(error))
|
||||
})
|
||||
}
|
||||
|
||||
if (Events.Warn) {
|
||||
client.on(Events.Warn, function (message) {
|
||||
ctx.console.error('discord-bot: ' + String(message || ''))
|
||||
})
|
||||
}
|
||||
|
||||
if (debugOn && Events.Debug) {
|
||||
client.on(Events.Debug, function (message) {
|
||||
ctx.console.log('discord-bot: ' + String(message || ''))
|
||||
})
|
||||
}
|
||||
|
||||
ctx.console.log(
|
||||
'discord-bot: logging in (token from ' + loaded.source + ')...'
|
||||
)
|
||||
try {
|
||||
await client.login(loaded.token)
|
||||
} catch (err) {
|
||||
ctx.console.error(
|
||||
'discord-bot: login failed: ' + ((err && err.message) || String(err))
|
||||
if (!wantsMessageContent) {
|
||||
ctx.console.log(
|
||||
'discord-bot: /ping only (pass --message-content after enabling Message Content Intent for channel ping/pong)'
|
||||
)
|
||||
}
|
||||
|
||||
let loggingIn = true
|
||||
let loginTimer = null
|
||||
function discordGatewayFatal(event) {
|
||||
const code = event && typeof event === 'object' ? event.code : event
|
||||
if (code === 4014) {
|
||||
const err = new Error(
|
||||
'gateway closed 4014 (disallowed intents). Enable Message Content Intent in the Developer Portal, or omit --message-content (default).'
|
||||
)
|
||||
err.code = 4014
|
||||
return err
|
||||
}
|
||||
if (code === 4013) {
|
||||
const err = new Error('gateway closed 4013 (invalid intents)')
|
||||
err.code = 4013
|
||||
return err
|
||||
}
|
||||
if (code === 4004) {
|
||||
const err = new Error(
|
||||
'gateway closed 4004 (authentication failed). Check the bot token.'
|
||||
)
|
||||
err.code = 4004
|
||||
return err
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.race([
|
||||
client.login(loaded.token),
|
||||
new Promise(function (_, reject) {
|
||||
loginTimer = setTimeout(function () {
|
||||
reject(
|
||||
new Error(
|
||||
'login timed out after ' +
|
||||
loginMs +
|
||||
'ms (REST/gateway). Retry with --debug or DISCORD_DEBUG=1.'
|
||||
)
|
||||
)
|
||||
}, loginMs)
|
||||
if (loginTimer && typeof loginTimer.unref === 'function') {
|
||||
loginTimer.unref()
|
||||
}
|
||||
if (Events.ShardError) {
|
||||
client.on(Events.ShardError, function (error) {
|
||||
if (!loggingIn) return
|
||||
reject(
|
||||
error instanceof Error ? error : new Error(discordErrText(error))
|
||||
)
|
||||
})
|
||||
}
|
||||
if (Events.ShardDisconnect) {
|
||||
client.on(Events.ShardDisconnect, function (event) {
|
||||
if (!loggingIn) return
|
||||
const fatal = discordGatewayFatal(event)
|
||||
if (fatal) reject(fatal)
|
||||
})
|
||||
}
|
||||
})
|
||||
])
|
||||
} catch (err) {
|
||||
ctx.console.error('discord-bot: login failed: ' + discordErrText(err))
|
||||
if (err && err.code === 'TokenInvalid') {
|
||||
ctx.console.error(
|
||||
'Invalid token: Developer Portal → Bot → Token (not the OAuth2 client secret).'
|
||||
)
|
||||
}
|
||||
if (err && (err.code === 4014 || /disallowed intents/i.test(String(err)))) {
|
||||
ctx.console.error(
|
||||
'Privileged intents: Developer Portal → Bot → Privileged Gateway Intents.'
|
||||
)
|
||||
}
|
||||
ctx.exitCode = 1
|
||||
loggingIn = false
|
||||
if (loginTimer) clearTimeout(loginTimer)
|
||||
await Promise.resolve(client.destroy()).catch(function () {})
|
||||
return
|
||||
}
|
||||
loggingIn = false
|
||||
if (loginTimer) clearTimeout(loginTimer)
|
||||
|
||||
await new Promise(function (resolve) {
|
||||
function shutdown() {
|
||||
|
||||
Reference in New Issue
Block a user