- 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.
137 lines
4.6 KiB
TypeScript
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;
|
|
}
|