Enhance verification module with improved command handling and localization

- Updated the verification command to utilize a new `ephemeral` utility for consistent ephemeral message replies.
- Refactored the `buildVerificationPanel` function to include localization support for panel titles and descriptions.
- Added permission checks to ensure the bot can post in the designated verification channel.
- Introduced a new verification queue in the queue management system for better handling of verification tasks.
- Updated localization files to include new keys for verification-related messages in both English and German.
This commit is contained in:
smueller
2026-07-23 10:54:21 +02:00
parent 4b6736444c
commit e24f99ad38
8 changed files with 229 additions and 22 deletions

View File

@@ -1,21 +1,41 @@
import { PermissionFlagsBits } from 'discord.js';
import {
PermissionFlagsBits,
type GuildBasedChannel,
type GuildTextBasedChannel
} from 'discord.js';
import { t } from '@nexumi/shared';
import type { SlashCommand } from '../../types.js';
import { getGuildLocale } from '../../i18n.js';
import { requirePermission } from '../../permissions.js';
import { ephemeral } from '../../interaction-reply.js';
import { verifyCommandData } from './command-definitions.js';
import {
getVerificationConfig,
updateVerificationConfig
} from './service.js';
import { getVerificationConfig, updateVerificationConfig } from './service.js';
import { buildVerificationPanel } from './handlers.js';
function botCanPostPanel(
channel: GuildBasedChannel,
meId: string
): channel is GuildTextBasedChannel {
if (!channel.isTextBased() || channel.isDMBased()) {
return false;
}
const permissions = channel.permissionsFor(meId);
if (!permissions) {
return false;
}
return (
permissions.has(PermissionFlagsBits.ViewChannel) &&
permissions.has(PermissionFlagsBits.SendMessages) &&
permissions.has(PermissionFlagsBits.EmbedLinks)
);
}
const verifyCommand: SlashCommand = {
data: verifyCommandData,
async execute(interaction, context) {
const locale = await getGuildLocale(context.prisma, interaction.guildId);
if (!interaction.inGuild()) {
await interaction.reply({ content: t(locale, 'generic.guildOnly'), ephemeral: true });
await interaction.reply({ content: t(locale, 'generic.guildOnly'), ...ephemeral() });
return;
}
if (!(await requirePermission(interaction, PermissionFlagsBits.ManageGuild, locale))) {
@@ -46,13 +66,16 @@ const verifyCommand: SlashCommand = {
failAction
});
await interaction.reply({ content: t(locale, 'verification.setupDone'), ephemeral: true });
await interaction.reply({ content: t(locale, 'verification.setupDone'), ...ephemeral() });
return;
}
const config = await getVerificationConfig(context.prisma, guildId);
if (!config.enabled || !config.verifiedRoleId) {
await interaction.reply({ content: t(locale, 'verification.notConfigured'), ephemeral: true });
await interaction.reply({
content: t(locale, 'verification.notConfigured'),
...ephemeral()
});
return;
}
@@ -63,12 +86,16 @@ const verifyCommand: SlashCommand = {
? await interaction.guild!.channels.fetch(config.channelId)
: null;
if (!panelChannel?.isTextBased() || panelChannel.isDMBased()) {
await interaction.reply({ content: t(locale, 'verification.invalidChannel'), ephemeral: true });
const me = interaction.guild!.members.me;
if (!panelChannel || !me || !botCanPostPanel(panelChannel, me.id)) {
await interaction.reply({
content: t(locale, 'verification.panelMissingPermission'),
...ephemeral()
});
return;
}
const panel = buildVerificationPanel(config);
const panel = buildVerificationPanel(config, locale);
const message = await panelChannel.send(panel);
await context.prisma.verificationConfig.update({
where: { guildId },
@@ -77,7 +104,7 @@ const verifyCommand: SlashCommand = {
panelMessageId: message.id
}
});
await interaction.reply({ content: t(locale, 'verification.panelPosted'), ephemeral: true });
await interaction.reply({ content: t(locale, 'verification.panelPosted'), ...ephemeral() });
}
};

View File

@@ -20,6 +20,7 @@ import {
} from './service.js';
import { env } from '../../env.js';
import { logger } from '../../logger.js';
import { ephemeral } from '../../interaction-reply.js';
async function applyFailAction(
member: GuildMember,
@@ -85,10 +86,13 @@ export async function completeVerification(
return { ok: true };
}
export function buildVerificationPanel(config: Awaited<ReturnType<typeof getVerificationConfig>>) {
export function buildVerificationPanel(
config: Awaited<ReturnType<typeof getVerificationConfig>>,
locale: 'de' | 'en'
) {
const embed = new EmbedBuilder()
.setTitle('Verification')
.setDescription('Click the button below to verify and gain access to the server.')
.setTitle(t(locale, 'verification.panel.title'))
.setDescription(t(locale, 'verification.panel.description'))
.setColor(0x6366f1);
const customId =
@@ -99,7 +103,11 @@ export function buildVerificationPanel(config: Awaited<ReturnType<typeof getVeri
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setCustomId(customId)
.setLabel(config.mode === 'CAPTCHA' ? 'Verify (Captcha)' : 'Verify')
.setLabel(
config.mode === 'CAPTCHA'
? t(locale, 'verification.panel.buttonCaptcha')
: t(locale, 'verification.panel.button')
)
.setStyle(ButtonStyle.Success)
);
@@ -119,7 +127,7 @@ export async function handleVerificationButton(
const config = await getVerificationConfig(context.prisma, parsed.guildId);
if (!config.enabled) {
await interaction.reply({ content: t(locale, 'verification.disabled'), ephemeral: true });
await interaction.reply({ content: t(locale, 'verification.disabled'), ...ephemeral() });
return;
}
@@ -129,15 +137,16 @@ export async function handleVerificationButton(
parsed.guildId,
interaction.user.id
);
const url = `${env.PUBLIC_BASE_URL}/verify/captcha?token=${challenge.token}`;
const base = (env.WEBUI_URL ?? env.PUBLIC_BASE_URL).replace(/\/$/, '');
const url = `${base}/verify/captcha?token=${challenge.token}`;
await interaction.reply({
content: tf(locale, 'verification.captchaLink', { url }),
ephemeral: true
...ephemeral()
});
return;
}
await interaction.deferReply({ ephemeral: true });
await interaction.deferReply(ephemeral());
const result = await completeVerification(context, parsed.guildId, interaction.user.id);
if (result.ok) {
await interaction.editReply({ content: t(locale, 'verification.success') });