Enhance bot and WebUI functionality with new features and environment updates

- Added support for new environment variables in `.env.example` for public site and legal operator information.
- Integrated core commands into the bot's command structure for improved functionality.
- Implemented a new `publishBotStatus` function to update bot status in Redis, enhancing monitoring capabilities.
- Updated the landing page to include features, invite links, and support server access, improving user experience.
- Enhanced localization with new keys for core commands and landing page elements in both English and German.
- Improved error handling and logging for bot presence updates and status publishing.
This commit is contained in:
smueller
2026-07-22 16:47:09 +02:00
parent 0b4ac0da29
commit 4852f16f79
32 changed files with 1579 additions and 83 deletions

View File

@@ -0,0 +1,24 @@
import { SlashCommandBuilder } from 'discord.js';
import { applyCommandDescription, applyOptionDescription } from '@nexumi/shared';
export const helpCommandData = applyCommandDescription(
new SlashCommandBuilder().setName('help'),
'core.help.description'
).addStringOption((option) =>
applyOptionDescription(option.setName('module'), 'core.help.options.module')
);
export const infoCommandData = applyCommandDescription(
new SlashCommandBuilder().setName('info'),
'core.info.description'
);
export const inviteCommandData = applyCommandDescription(
new SlashCommandBuilder().setName('invite'),
'core.invite.description'
);
export const supportCommandData = applyCommandDescription(
new SlashCommandBuilder().setName('support'),
'core.support.description'
);

View File

@@ -0,0 +1,158 @@
import { EmbedBuilder } from 'discord.js';
import { t, tf } from '@nexumi/shared';
import type { SlashCommand } from '../../types.js';
import { getGuildLocale } from '../../i18n.js';
import { buildBotInviteUrl, env } from '../../env.js';
import {
helpCommandData,
infoCommandData,
inviteCommandData,
supportCommandData
} from './command-definitions.js';
import { HELP_MODULES } from './help-catalog.js';
const NEXUMI_COLOR = 0x6366f1;
const PACKAGE_VERSION = '0.1.0';
function formatUptime(seconds: number): string {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${h}h ${m}m ${s}s`;
}
function webuiBase(): string {
return env.WEBUI_URL ?? env.PUBLIC_BASE_URL;
}
const helpCommand: SlashCommand = {
data: helpCommandData,
async execute(interaction, context) {
const locale = await getGuildLocale(context.prisma, interaction.guildId);
const filter = interaction.options.getString('module')?.trim().toLowerCase() ?? null;
const modules = filter
? HELP_MODULES.filter((module) => module.id === filter)
: HELP_MODULES;
if (modules.length === 0) {
await interaction.reply({
content: tf(locale, 'core.help.unknownModule', {
modules: HELP_MODULES.map((module) => module.id).join(', ')
}),
ephemeral: true
});
return;
}
const lines = modules.map((module) =>
tf(locale, 'core.help.moduleLine', {
module: t(locale, `core.module.${module.id}`),
commands: module.commands.map((name) => `\`/${name}\``).join(', ')
})
);
// Discord embed description limit is 4096; split across fields if needed.
const embed = new EmbedBuilder()
.setTitle(t(locale, 'core.help.title'))
.setDescription(t(locale, 'core.help.subtitle'))
.setColor(NEXUMI_COLOR)
.setFooter({
text: tf(locale, 'core.help.footer', { webui: webuiBase() })
});
const chunkSize = 8;
for (let i = 0; i < lines.length; i += chunkSize) {
const chunk = lines.slice(i, i + chunkSize);
embed.addFields({
name: '\u200b',
value: chunk.join('\n')
});
}
await interaction.reply({ embeds: [embed], ephemeral: true });
}
};
const infoCommand: SlashCommand = {
data: infoCommandData,
async execute(interaction, context) {
const locale = await getGuildLocale(context.prisma, interaction.guildId);
const shardId = context.client.shard?.ids[0] ?? 0;
const inviteUrl = buildBotInviteUrl();
const base = webuiBase();
const linkLines = [
`[${t(locale, 'core.info.link.dashboard')}](${base})`,
`[${t(locale, 'core.info.link.invite')}](${inviteUrl})`,
`[${t(locale, 'core.info.link.status')}](${base.replace(/\/$/, '')}/status)`
];
if (env.SUPPORT_SERVER_URL) {
linkLines.push(`[${t(locale, 'core.info.link.support')}](${env.SUPPORT_SERVER_URL})`);
}
const embed = new EmbedBuilder()
.setTitle(t(locale, 'core.info.title'))
.setColor(NEXUMI_COLOR)
.addFields(
{ name: t(locale, 'core.info.version'), value: PACKAGE_VERSION, inline: true },
{
name: t(locale, 'core.info.uptime'),
value: formatUptime(process.uptime()),
inline: true
},
{ name: t(locale, 'core.info.shard'), value: String(shardId), inline: true },
{
name: t(locale, 'core.info.guilds'),
value: String(context.client.guilds.cache.size),
inline: true
},
{
name: t(locale, 'core.info.latency'),
value: `${context.client.ws.ping} ms`,
inline: true
},
{ name: t(locale, 'core.info.links'), value: linkLines.join('\n') }
)
.setFooter({ text: 'Nexumi' });
await interaction.reply({ embeds: [embed], ephemeral: true });
}
};
const inviteCommand: SlashCommand = {
data: inviteCommandData,
async execute(interaction, context) {
const locale = await getGuildLocale(context.prisma, interaction.guildId);
const inviteUrl = buildBotInviteUrl();
const embed = new EmbedBuilder()
.setTitle(t(locale, 'core.invite.title'))
.setDescription(`${t(locale, 'core.invite.body')}\n${inviteUrl}`)
.setColor(NEXUMI_COLOR);
await interaction.reply({ embeds: [embed], ephemeral: true });
}
};
const supportCommand: SlashCommand = {
data: supportCommandData,
async execute(interaction, context) {
const locale = await getGuildLocale(context.prisma, interaction.guildId);
if (!env.SUPPORT_SERVER_URL) {
await interaction.reply({
content: t(locale, 'core.support.missing'),
ephemeral: true
});
return;
}
const embed = new EmbedBuilder()
.setTitle(t(locale, 'core.support.title'))
.setDescription(`${t(locale, 'core.support.body')}\n${env.SUPPORT_SERVER_URL}`)
.setColor(NEXUMI_COLOR);
await interaction.reply({ embeds: [embed], ephemeral: true });
}
};
export const coreCommands: SlashCommand[] = [
helpCommand,
infoCommand,
inviteCommand,
supportCommand
];

View File

@@ -0,0 +1,93 @@
/** Module id → top-level slash command names for `/help`. */
export const HELP_MODULES: ReadonlyArray<{ id: string; commands: readonly string[] }> = [
{ id: 'core', commands: ['help', 'info', 'invite', 'support'] },
{
id: 'moderation',
commands: [
'ban',
'unban',
'kick',
'timeout',
'untimeout',
'warn',
'purge',
'slowmode',
'lock',
'unlock',
'nick',
'case',
'modnote'
]
},
{ id: 'automod', commands: ['automod'] },
{ id: 'welcome', commands: ['welcome'] },
{ id: 'verification', commands: ['verify'] },
{ id: 'leveling', commands: ['rank', 'leaderboard', 'xp'] },
{
id: 'economy',
commands: [
'balance',
'daily',
'weekly',
'work',
'pay',
'gamble',
'slots',
'blackjack',
'coinflip',
'shop',
'inventory',
'eco'
]
},
{
id: 'utility',
commands: [
'userinfo',
'serverinfo',
'roleinfo',
'channelinfo',
'avatar',
'banner',
'poll',
'remindme',
'reminders',
'afk',
'emoji',
'sticker',
'timestamp',
'snipe',
'editsnipe',
'embed'
]
},
{
id: 'fun',
commands: [
'8ball',
'dice',
'flip',
'rps',
'choose',
'trivia',
'tictactoe',
'connect4',
'hangman',
'meme',
'cat',
'dog'
]
},
{ id: 'giveaways', commands: ['giveaway'] },
{ id: 'tickets', commands: ['ticket'] },
{ id: 'selfroles', commands: ['selfroles'] },
{ id: 'tags', commands: ['tag'] },
{ id: 'starboard', commands: ['starboard'] },
{ id: 'suggestions', commands: ['suggest', 'suggestion'] },
{ id: 'birthdays', commands: ['birthday'] },
{ id: 'tempvoice', commands: ['voice'] },
{ id: 'stats', commands: ['stats', 'invites'] },
{ id: 'feeds', commands: ['feeds'] },
{ id: 'scheduler', commands: ['schedule', 'announce'] },
{ id: 'guildbackup', commands: ['backup'] }
];

View File

@@ -0,0 +1,2 @@
export { coreCommands } from './commands.js';
export { HELP_MODULES } from './help-catalog.js';