Add global /about command and configuration
- Introduced `BotAboutConfig` model for managing the global `/about` message in the owner panel. - Implemented the `/about` command with localization support and integrated it into the help catalog. - Enhanced the dashboard with a new owner section for configuring the `/about` message type and content. - Updated components to handle the new command and its interactions, including support for different message formats (TEXT, EMBED, COMPONENTS_V2). - Added localization entries for the `/about` command in English and German, improving accessibility for users. - Documented the implementation in the phase tracking documentation for future reference.
This commit is contained in:
@@ -20,6 +20,7 @@ describe('moduleForCommand', () => {
|
||||
it('leaves core commands unmapped', () => {
|
||||
expect(moduleForCommand('help')).toBeNull();
|
||||
expect(moduleForCommand('info')).toBeNull();
|
||||
expect(moduleForCommand('about')).toBeNull();
|
||||
expect(moduleForCommand('gdpr')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -67,6 +67,13 @@ async function loadPayload(
|
||||
}
|
||||
return parseMessageComponentsV2(binding.payload);
|
||||
}
|
||||
case 'a': {
|
||||
if (ref !== 'singleton') {
|
||||
return null;
|
||||
}
|
||||
const config = await context.prisma.botAboutConfig.findUnique({ where: { id: 'singleton' } });
|
||||
return parseMessageComponentsV2(config?.components);
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
98
apps/bot/src/modules/core/about.ts
Normal file
98
apps/bot/src/modules/core/about.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
ABOUT_REDIS_KEY,
|
||||
BotAboutConfigSchema,
|
||||
expandBracketChannelMentions,
|
||||
parseMessageComponentsV2,
|
||||
t,
|
||||
type BotAboutConfig,
|
||||
type Locale,
|
||||
type WelcomeEmbed
|
||||
} from '@nexumi/shared';
|
||||
import type { InteractionReplyOptions, MessageCreateOptions } from 'discord.js';
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
import { buildComponentsV2Payload } from '../../lib/components-v2-payload.js';
|
||||
import { buildEmbedFromPayload } from '../../lib/embed-payload.js';
|
||||
import { redis } from '../../redis.js';
|
||||
import type { BotContext } from '../../types.js';
|
||||
|
||||
const NEXUMI_COLOR = 0x6366f1;
|
||||
|
||||
function parseEmbed(raw: unknown): WelcomeEmbed | null {
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return null;
|
||||
}
|
||||
return raw as WelcomeEmbed;
|
||||
}
|
||||
|
||||
export async function readAboutConfig(context: BotContext): Promise<BotAboutConfig> {
|
||||
const cached = await redis.get(ABOUT_REDIS_KEY);
|
||||
if (cached) {
|
||||
const parsed = BotAboutConfigSchema.safeParse(JSON.parse(cached));
|
||||
if (parsed.success) {
|
||||
return parsed.data;
|
||||
}
|
||||
}
|
||||
|
||||
const row = await context.prisma.botAboutConfig.upsert({
|
||||
where: { id: 'singleton' },
|
||||
create: { id: 'singleton' },
|
||||
update: {}
|
||||
});
|
||||
const config = BotAboutConfigSchema.parse({
|
||||
enabled: row.enabled,
|
||||
messageType: row.messageType,
|
||||
content: row.content,
|
||||
embed: parseEmbed(row.embed),
|
||||
components: parseMessageComponentsV2(row.components)
|
||||
});
|
||||
await redis.set(ABOUT_REDIS_KEY, JSON.stringify(config));
|
||||
return config;
|
||||
}
|
||||
|
||||
function defaultAboutReply(locale: Locale): InteractionReplyOptions {
|
||||
return {
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setTitle(t(locale, 'core.about.defaultTitle'))
|
||||
.setDescription(t(locale, 'core.about.defaultBody'))
|
||||
.setColor(NEXUMI_COLOR)
|
||||
.setFooter({ text: 'Nexumi' })
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
export async function buildAboutReply(
|
||||
context: BotContext,
|
||||
locale: Locale
|
||||
): Promise<InteractionReplyOptions | MessageCreateOptions> {
|
||||
const config = await readAboutConfig(context);
|
||||
if (!config.enabled) {
|
||||
return { content: t(locale, 'core.about.disabled'), ephemeral: true };
|
||||
}
|
||||
|
||||
const renderText = (value: string) => expandBracketChannelMentions(value);
|
||||
|
||||
if (config.messageType === 'COMPONENTS_V2') {
|
||||
const payload = buildComponentsV2Payload(config.components, {
|
||||
source: 'a',
|
||||
ref: 'singleton',
|
||||
renderText
|
||||
});
|
||||
if (payload) {
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.messageType === 'EMBED') {
|
||||
const embed = buildEmbedFromPayload(config.embed, { renderText });
|
||||
if (embed) {
|
||||
return { embeds: [embed] };
|
||||
}
|
||||
}
|
||||
|
||||
if (config.messageType === 'TEXT' && config.content?.trim()) {
|
||||
return { content: renderText(config.content) };
|
||||
}
|
||||
|
||||
return defaultAboutReply(locale);
|
||||
}
|
||||
@@ -13,6 +13,11 @@ export const infoCommandData = applyCommandDescription(
|
||||
'core.info.description'
|
||||
);
|
||||
|
||||
export const aboutCommandData = applyCommandDescription(
|
||||
new SlashCommandBuilder().setName('about'),
|
||||
'core.about.description'
|
||||
);
|
||||
|
||||
export const inviteCommandData = applyCommandDescription(
|
||||
new SlashCommandBuilder().setName('invite'),
|
||||
'core.invite.description'
|
||||
|
||||
@@ -6,10 +6,12 @@ import { buildBotInviteUrl, env } from '../../env.js';
|
||||
import {
|
||||
helpCommandData,
|
||||
infoCommandData,
|
||||
aboutCommandData,
|
||||
inviteCommandData,
|
||||
supportCommandData
|
||||
} from './command-definitions.js';
|
||||
import { HELP_MODULES } from './help-catalog.js';
|
||||
import { buildAboutReply } from './about.js';
|
||||
|
||||
const NEXUMI_COLOR = 0x6366f1;
|
||||
const PACKAGE_VERSION = '0.1.0';
|
||||
@@ -118,6 +120,15 @@ const infoCommand: SlashCommand = {
|
||||
}
|
||||
};
|
||||
|
||||
const aboutCommand: SlashCommand = {
|
||||
data: aboutCommandData,
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
const reply = await buildAboutReply(context, locale);
|
||||
await interaction.reply(reply as import('discord.js').InteractionReplyOptions);
|
||||
}
|
||||
};
|
||||
|
||||
const inviteCommand: SlashCommand = {
|
||||
data: inviteCommandData,
|
||||
async execute(interaction, context) {
|
||||
@@ -153,6 +164,7 @@ const supportCommand: SlashCommand = {
|
||||
export const coreCommands: SlashCommand[] = [
|
||||
helpCommand,
|
||||
infoCommand,
|
||||
aboutCommand,
|
||||
inviteCommand,
|
||||
supportCommand
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** 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', 'privacy', 'gdpr'] },
|
||||
{ id: 'core', commands: ['help', 'info', 'about', 'invite', 'support', 'privacy', 'gdpr'] },
|
||||
{
|
||||
id: 'moderation',
|
||||
commands: [
|
||||
|
||||
Reference in New Issue
Block a user