Files
Nexumi/apps/webui/src/lib/module-configs/verification.ts
TheOnlyMace ef1803f0ab Implement verification panel synchronization with BullMQ integration
- Introduced a new `verificationPanelSync` job to handle posting and updating the verification panel in Discord channels via BullMQ.
- Enhanced the `syncVerificationPanel` function to manage panel updates and error handling, ensuring proper feedback for synchronization issues.
- Updated the WebUI to support verification panel management, including improved error messages for timeout and posting failures.
- Added localization entries for new error messages related to verification panel synchronization in both English and German.
- Refactored existing verification logic to accommodate the new synchronization process, improving overall functionality and user experience.
2026-07-25 15:55:13 +02:00

137 lines
4.6 KiB
TypeScript

import type { CaptchaProvider, VerificationConfigDashboard, VerificationConfigDashboardPatch } from '@nexumi/shared';
import type { Prisma } from '@prisma/client';
import { prisma } from '../prisma';
import { getAvailableCaptchaProviders } from '../captcha';
import {
addJobAndAwait,
getVerificationQueue,
getVerificationQueueEvents
} from '../queues';
export class VerificationPanelSyncError extends Error {
constructor(
message: string,
public readonly code: 'timeout' | 'not_posted' = 'not_posted'
) {
super(message);
this.name = 'VerificationPanelSyncError';
}
}
async function ensureVerificationConfig(guildId: string) {
await prisma.guild.upsert({ where: { id: guildId }, update: {}, create: { id: guildId } });
return prisma.verificationConfig.upsert({
where: { guildId },
update: {},
create: { guildId }
});
}
function toDashboard(
config: Awaited<ReturnType<typeof ensureVerificationConfig>>,
available: CaptchaProvider[]
): VerificationConfigDashboard {
const rawProvider = (config.captchaProvider as CaptchaProvider) ?? 'MATH';
const captchaProvider = available.includes(rawProvider) ? rawProvider : 'MATH';
return {
enabled: config.enabled,
channelId: config.channelId ?? '',
verifiedRoleId: config.verifiedRoleId ?? '',
unverifiedRoleId: config.unverifiedRoleId ?? '',
mode: config.mode as VerificationConfigDashboard['mode'],
captchaProvider,
minAccountAgeDays: config.minAccountAgeDays,
failAction: config.failAction as VerificationConfigDashboard['failAction'],
altDetectionEnabled: config.altDetectionEnabled,
altBlockOnMatch: config.altBlockOnMatch,
altIpWindowDays: config.altIpWindowDays,
altInviteWindowHours: config.altInviteWindowHours,
altMaxAccountsPerInvite: config.altMaxAccountsPerInvite
};
}
export async function getVerificationDashboard(guildId: string): Promise<{
config: VerificationConfigDashboard;
availableCaptchaProviders: CaptchaProvider[];
}> {
const config = await ensureVerificationConfig(guildId);
const availableCaptchaProviders = getAvailableCaptchaProviders();
return {
config: toDashboard(config, availableCaptchaProviders),
availableCaptchaProviders
};
}
/**
* Dashboard has no discord.js Client, so panel posting is delegated to the bot
* via the `verification` BullMQ queue (`verificationPanelSync`) — same pattern
* as self-role panels / giveaways.
*/
async function syncVerificationPanel(guildId: string): Promise<void> {
const { confirmed, result } = await addJobAndAwait<{
panelChannelId: string;
panelMessageId: string;
}>(
getVerificationQueue(),
getVerificationQueueEvents(),
'verificationPanelSync',
{ guildId },
{
jobId: `verification-panel-${guildId}-${Date.now()}`,
removeOnComplete: true,
removeOnFail: 25
},
30_000
);
if (!confirmed || !result?.panelMessageId) {
throw new VerificationPanelSyncError(
confirmed
? 'Verification panel job finished without posting a Discord message'
: 'The bot did not confirm the verification panel in time. It may still be posting — check the channel shortly.',
confirmed ? 'not_posted' : 'timeout'
);
}
const config = await prisma.verificationConfig.findUnique({ where: { guildId } });
if (!config?.panelMessageId) {
throw new VerificationPanelSyncError(
'Verification panel job finished without storing a Discord message id',
'not_posted'
);
}
}
export async function updateVerificationDashboard(
guildId: string,
patch: VerificationConfigDashboardPatch
): Promise<VerificationConfigDashboard> {
await ensureVerificationConfig(guildId);
const available = new Set(getAvailableCaptchaProviders());
if (patch.captchaProvider !== undefined && !available.has(patch.captchaProvider)) {
throw new Error(`Captcha provider ${patch.captchaProvider} is not configured in .env`);
}
const { channelId, verifiedRoleId, unverifiedRoleId, ...rest } = patch;
const data: Prisma.VerificationConfigUpdateInput = { ...rest };
if (channelId !== undefined) {
data.channelId = channelId === '' ? null : channelId;
}
if (verifiedRoleId !== undefined) {
data.verifiedRoleId = verifiedRoleId === '' ? null : verifiedRoleId;
}
if (unverifiedRoleId !== undefined) {
data.unverifiedRoleId = unverifiedRoleId === '' ? null : unverifiedRoleId;
}
const updated = await prisma.verificationConfig.update({ where: { guildId }, data });
const dashboard = toDashboard(updated, getAvailableCaptchaProviders());
if (dashboard.enabled && dashboard.channelId && dashboard.verifiedRoleId) {
await syncVerificationPanel(guildId);
}
return dashboard;
}