Implement ticket panel synchronization and configuration updates
- Added `panelChannelId` and `panelMessageId` fields to the `TicketConfig` model for managing ticket panel settings. - Introduced `syncTicketPanel` function to handle posting and updating the ticket panel in Discord channels via BullMQ. - Enhanced error handling for ticket panel synchronization, including specific error messages for missing categories and posting failures. - Updated WebUI components to support panel channel selection and improved error messaging for synchronization issues. - Added localization entries for new error messages related to ticket panel synchronization in both English and German. - Refactored ticket category management to trigger panel synchronization upon category creation, update, or deletion.
This commit is contained in:
@@ -7,6 +7,17 @@ import type {
|
||||
} from '@nexumi/shared';
|
||||
import type { TicketCategory } from '@prisma/client';
|
||||
import { prisma } from '../prisma';
|
||||
import { addJobAndAwait, getTicketsQueue, getTicketsQueueEvents } from '../queues';
|
||||
|
||||
export class TicketPanelSyncError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly code: 'timeout' | 'not_posted' | 'no_categories' = 'not_posted'
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'TicketPanelSyncError';
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureTicketConfig(guildId: string) {
|
||||
await prisma.guild.upsert({ where: { id: guildId }, update: {}, create: { id: guildId } });
|
||||
@@ -19,6 +30,7 @@ function toConfigDashboard(config: Awaited<ReturnType<typeof ensureTicketConfig>
|
||||
mode: config.mode as TicketConfigDashboard['mode'],
|
||||
categoryId: config.categoryId ?? '',
|
||||
logChannelId: config.logChannelId ?? '',
|
||||
panelChannelId: config.panelChannelId ?? '',
|
||||
transcriptDm: config.transcriptDm,
|
||||
inactivityHours: config.inactivityHours,
|
||||
ratingEnabled: config.ratingEnabled
|
||||
@@ -30,22 +42,86 @@ export async function getTicketConfigDashboard(guildId: string): Promise<TicketC
|
||||
return toConfigDashboard(config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dashboard has no discord.js Client, so panel posting is delegated to the bot
|
||||
* via the `tickets` BullMQ queue (`ticketPanelSync`).
|
||||
*/
|
||||
async function syncTicketPanel(guildId: string): Promise<void> {
|
||||
const categoryCount = await prisma.ticketCategory.count({ where: { guildId } });
|
||||
if (categoryCount === 0) {
|
||||
throw new TicketPanelSyncError(
|
||||
'Create at least one ticket category before posting the panel',
|
||||
'no_categories'
|
||||
);
|
||||
}
|
||||
|
||||
const { confirmed, result } = await addJobAndAwait<{
|
||||
panelChannelId: string;
|
||||
panelMessageId: string;
|
||||
}>(
|
||||
getTicketsQueue(),
|
||||
getTicketsQueueEvents(),
|
||||
'ticketPanelSync',
|
||||
{ guildId },
|
||||
{
|
||||
jobId: `ticket-panel-${guildId}-${Date.now()}`,
|
||||
removeOnComplete: true,
|
||||
removeOnFail: 25
|
||||
},
|
||||
30_000
|
||||
);
|
||||
|
||||
if (!confirmed || !result?.panelMessageId) {
|
||||
throw new TicketPanelSyncError(
|
||||
confirmed
|
||||
? 'Ticket panel job finished without posting a Discord message'
|
||||
: 'The bot did not confirm the ticket panel in time. It may still be posting — check the channel shortly.',
|
||||
confirmed ? 'not_posted' : 'timeout'
|
||||
);
|
||||
}
|
||||
|
||||
const config = await prisma.ticketConfig.findUnique({ where: { guildId } });
|
||||
if (!config?.panelMessageId) {
|
||||
throw new TicketPanelSyncError(
|
||||
'Ticket panel job finished without storing a Discord message id',
|
||||
'not_posted'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function maybeSyncTicketPanel(guildId: string): Promise<void> {
|
||||
const config = await prisma.ticketConfig.findUnique({ where: { guildId } });
|
||||
if (!config?.enabled || !config.panelChannelId) {
|
||||
return;
|
||||
}
|
||||
await syncTicketPanel(guildId);
|
||||
}
|
||||
|
||||
export async function updateTicketConfigDashboard(
|
||||
guildId: string,
|
||||
patch: TicketConfigDashboardPatch
|
||||
): Promise<TicketConfigDashboard> {
|
||||
await ensureTicketConfig(guildId);
|
||||
|
||||
const { categoryId, logChannelId, ...rest } = patch;
|
||||
const { categoryId, logChannelId, panelChannelId, ...rest } = patch;
|
||||
const updated = await prisma.ticketConfig.update({
|
||||
where: { guildId },
|
||||
data: {
|
||||
...rest,
|
||||
...(categoryId !== undefined ? { categoryId: categoryId === '' ? null : categoryId } : {}),
|
||||
...(logChannelId !== undefined ? { logChannelId: logChannelId === '' ? null : logChannelId } : {})
|
||||
...(logChannelId !== undefined ? { logChannelId: logChannelId === '' ? null : logChannelId } : {}),
|
||||
...(panelChannelId !== undefined
|
||||
? { panelChannelId: panelChannelId === '' ? null : panelChannelId }
|
||||
: {})
|
||||
}
|
||||
});
|
||||
return toConfigDashboard(updated);
|
||||
const dashboard = toConfigDashboard(updated);
|
||||
|
||||
if (dashboard.enabled && dashboard.panelChannelId) {
|
||||
await syncTicketPanel(guildId);
|
||||
}
|
||||
|
||||
return dashboard;
|
||||
}
|
||||
|
||||
function toCategoryDashboard(category: TicketCategory): TicketCategoryDashboard {
|
||||
@@ -87,6 +163,7 @@ export async function createTicketCategory(
|
||||
supportRoleIds: input.supportRoleIds
|
||||
}
|
||||
});
|
||||
await maybeSyncTicketPanel(guildId);
|
||||
return toCategoryDashboard(category);
|
||||
} catch (error) {
|
||||
if (error && typeof error === 'object' && 'code' in error && error.code === 'P2002') {
|
||||
@@ -116,6 +193,7 @@ export async function updateTicketCategory(
|
||||
...(patch.supportRoleIds !== undefined ? { supportRoleIds: patch.supportRoleIds } : {})
|
||||
}
|
||||
});
|
||||
await maybeSyncTicketPanel(guildId);
|
||||
return toCategoryDashboard(updated);
|
||||
} catch (error) {
|
||||
if (error && typeof error === 'object' && 'code' in error && error.code === 'P2002') {
|
||||
@@ -127,5 +205,12 @@ export async function updateTicketCategory(
|
||||
|
||||
export async function deleteTicketCategory(guildId: string, categoryId: string): Promise<boolean> {
|
||||
const result = await prisma.ticketCategory.deleteMany({ where: { id: categoryId, guildId } });
|
||||
return result.count > 0;
|
||||
if (result.count === 0) {
|
||||
return false;
|
||||
}
|
||||
const remaining = await prisma.ticketCategory.count({ where: { guildId } });
|
||||
if (remaining > 0) {
|
||||
await maybeSyncTicketPanel(guildId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user