[deploy] add register module backend
Some checks failed
Deploy Discord Bot / deploy (push) Failing after 20s

This commit is contained in:
Pascal Prießnitz
2025-12-03 18:08:08 +01:00
parent b672e2c6a2
commit a8b4713ffe
9 changed files with 497 additions and 10 deletions

View File

@@ -10,7 +10,8 @@ export type ModuleKey =
| 'statuspageEnabled'
| 'birthdayEnabled'
| 'reactionRolesEnabled'
| 'eventsEnabled';
| 'eventsEnabled'
| 'registerEnabled';
export interface GuildModuleState {
key: ModuleKey;
@@ -29,7 +30,8 @@ const MODULES: Record<ModuleKey, { name: string; description: string }> = {
statuspageEnabled: { name: 'Statuspage', description: 'Service Checks, Uptime und Status-Embed.' },
birthdayEnabled: { name: 'Birthday', description: 'Geburtstage speichern und Glueckwuensche senden.' },
reactionRolesEnabled: { name: 'Reaction Roles', description: 'Reaktionen vergeben und entfernen Rollen.' },
eventsEnabled: { name: 'Termine', description: 'Events planen, erinnern und Anmeldungen sammeln.' }
eventsEnabled: { name: 'Termine', description: 'Events planen, erinnern und Anmeldungen sammeln.' },
registerEnabled: { name: 'Register', description: 'Registrierungsformulare und Bewerbungen.' }
};
export class BotModuleService {

View File

@@ -0,0 +1,256 @@
import {
ActionRowBuilder,
ButtonBuilder,
ButtonInteraction,
ButtonStyle,
Client,
EmbedBuilder,
ModalBuilder,
TextInputBuilder,
TextInputStyle,
ModalSubmitInteraction,
GuildMember
} from 'discord.js';
import { prisma } from '../database';
import { settingsStore } from '../config/state';
import { env } from '../config/env';
export class RegisterService {
private client: Client | null = null;
public setClient(client: Client) {
this.client = client;
}
public async listForms(guildId: string) {
return prisma.registerForm.findMany({ where: { guildId }, include: { fields: { orderBy: { order: 'asc' } } }, orderBy: { createdAt: 'desc' } });
}
public async saveForm(form: {
id?: string;
guildId: string;
name: string;
description?: string;
reviewChannelId?: string;
notifyRoleIds?: string[];
isActive?: boolean;
fields: { id?: string; label: string; type: string; required?: boolean; order?: number }[];
}) {
const notify = (form.notifyRoleIds || []).filter(Boolean);
if (form.id) {
await prisma.registerForm.update({
where: { id: form.id },
data: {
name: form.name,
description: form.description,
reviewChannelId: form.reviewChannelId,
notifyRoleIds: notify,
isActive: form.isActive ?? true
}
});
await prisma.registerFormField.deleteMany({ where: { formId: form.id } });
await prisma.registerFormField.createMany({
data: (form.fields || []).map((f, idx) => ({
formId: form.id as string,
label: f.label,
type: f.type,
required: f.required ?? false,
order: f.order ?? idx
}))
});
return prisma.registerForm.findUnique({ where: { id: form.id }, include: { fields: { orderBy: { order: 'asc' } } } });
}
const created = await prisma.registerForm.create({
data: {
guildId: form.guildId,
name: form.name,
description: form.description,
reviewChannelId: form.reviewChannelId,
notifyRoleIds: notify,
isActive: form.isActive ?? true,
fields: {
create: (form.fields || []).map((f, idx) => ({
label: f.label,
type: f.type,
required: f.required ?? false,
order: f.order ?? idx
}))
}
},
include: { fields: { orderBy: { order: 'asc' } } }
});
return created;
}
public async deleteForm(guildId: string, id: string) {
const form = await prisma.registerForm.findFirst({ where: { id, guildId } });
if (!form) return false;
await prisma.registerFormField.deleteMany({ where: { formId: id } });
await prisma.registerForm.delete({ where: { id } });
return true;
}
public async sendPanel(guildId: string, formId: string, channelId?: string, message?: string) {
if (!this.client) return null;
const form = await prisma.registerForm.findFirst({ where: { id: formId, guildId }, include: { fields: true } });
if (!form) return null;
const targetChannelId = channelId || form.reviewChannelId || settingsStore.get(guildId)?.registerConfig?.reviewChannelId;
if (!targetChannelId) return null;
const guild = this.client.guilds.cache.get(guildId) ?? (await this.client.guilds.fetch(guildId).catch(() => null));
if (!guild) return null;
const channel = await guild.channels.fetch(targetChannelId).catch(() => null);
if (!channel || !channel.isTextBased()) return null;
const embed = new EmbedBuilder()
.setTitle(form.name)
.setDescription(message || 'Klicke auf Registrieren, um das Formular auszufüllen.')
.setColor(0xf97316);
const btn = new ButtonBuilder().setCustomId(`register:form:${form.id}`).setLabel('Registrieren').setStyle(ButtonStyle.Primary);
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(btn);
const sent = await (channel as any).send({ embeds: [embed], components: [row] });
return sent.id;
}
public async handleButton(interaction: ButtonInteraction) {
if (interaction.customId.startsWith('register:form:')) {
const formId = interaction.customId.split(':')[2];
const form = await prisma.registerForm.findFirst({ where: { id: formId }, include: { fields: { orderBy: { order: 'asc' } } } });
if (!form) return interaction.reply({ content: 'Formular nicht gefunden.', ephemeral: true });
const modal = new ModalBuilder().setTitle(form.name).setCustomId(`register:submit:${form.id}`);
const components: any[] = [];
form.fields.slice(0, 5).forEach((f) => {
const input = new TextInputBuilder()
.setCustomId(f.id)
.setLabel(f.label.slice(0, 45) || 'Feld')
.setStyle(f.type === 'longText' ? TextInputStyle.Paragraph : TextInputStyle.Short)
.setRequired(f.required ?? false);
components.push(new ActionRowBuilder<TextInputBuilder>().addComponents(input));
});
modal.addComponents(components as any);
await interaction.showModal(modal);
return;
}
if (interaction.customId.startsWith('register:review:')) {
const [, , action, appId] = interaction.customId.split(':');
const app = await prisma.registerApplication.findUnique({ where: { id: appId }, include: { form: true } });
if (!app) {
await interaction.reply({ content: 'Antrag nicht gefunden.', ephemeral: true });
return;
}
const statusMap: any = { accept: 'accepted', invite: 'invited', reject: 'rejected' };
const newStatus = statusMap[action] || 'pending';
const updated = await prisma.registerApplication.update({
where: { id: appId },
data: { status: newStatus, reviewedBy: interaction.user.id }
});
await this.updateReviewMessage(interaction, updated);
const user = await this.client?.users.fetch(app.userId).catch(() => null);
if (user) {
const msg =
newStatus === 'accepted'
? 'Deine Registrierung wurde akzeptiert.'
: newStatus === 'invited'
? 'Bitte komm für ein Gespräch vorbei.'
: 'Deine Registrierung wurde abgelehnt.';
user.send(msg).catch(() => undefined);
}
await interaction.reply({ content: 'Status aktualisiert.', ephemeral: true });
return;
}
}
public async handleModal(interaction: ModalSubmitInteraction) {
if (!interaction.customId.startsWith('register:submit:')) return;
const formId = interaction.customId.split(':')[2];
const form = await prisma.registerForm.findFirst({
where: { id: formId },
include: { fields: { orderBy: { order: 'asc' } } }
});
if (!form) {
await interaction.reply({ content: 'Formular nicht gefunden.', ephemeral: true });
return;
}
const answersPayload = form.fields.map((f) => ({
fieldId: f.id,
value: interaction.fields.getTextInputValue(f.id) || ''
}));
const app = await prisma.registerApplication.create({
data: {
guildId: interaction.guildId ?? '',
userId: interaction.user.id,
formId: form.id,
status: 'pending',
answers: {
create: answersPayload
}
},
include: { answers: true }
});
await interaction.reply({ content: 'Registrierung gesendet.', ephemeral: true });
await this.postReviewEmbed(form, app, interaction.user.id, interaction.guildId || '');
}
private async postReviewEmbed(form: any, app: any, userId: string, guildId: string) {
if (!this.client) return;
const cfg = settingsStore.get(guildId);
const channelId = form.reviewChannelId || cfg?.registerConfig?.reviewChannelId;
if (!channelId) return;
const guild = this.client.guilds.cache.get(guildId) ?? (await this.client.guilds.fetch(guildId).catch(() => null));
if (!guild) return;
const channel = await guild.channels.fetch(channelId).catch(() => null);
if (!channel || !channel.isTextBased()) return;
const member = await guild.members.fetch(userId).catch(() => null);
const fields = await prisma.registerFormField.findMany({ where: { formId: form.id }, orderBy: { order: 'asc' } });
const answers = await prisma.registerApplicationAnswer.findMany({ where: { applicationId: app.id } });
const embed = new EmbedBuilder()
.setTitle(`Registrierung: ${form.name}`)
.setDescription(form.description || '')
.setColor(0xf97316)
.addFields(
...fields.map((f) => ({
name: f.label,
value: answers.find((a) => a.fieldId === f.id)?.value || '-',
inline: false
}))
)
.setFooter({ text: `Status: ${app.status}` })
.setTimestamp(new Date(app.createdAt));
if (member) embed.setAuthor({ name: member.user.tag, iconURL: member.user.displayAvatarURL() });
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder().setCustomId(`register:review:accept:${app.id}`).setLabel('Akzeptieren').setStyle(ButtonStyle.Success),
new ButtonBuilder().setCustomId(`register:review:invite:${app.id}`).setLabel('Gespräch einladen').setStyle(ButtonStyle.Primary),
new ButtonBuilder().setCustomId(`register:review:reject:${app.id}`).setLabel('Ablehnen').setStyle(ButtonStyle.Danger)
);
const notifyRoles = form.notifyRoleIds || cfg?.registerConfig?.notifyRoleIds || [];
const content = notifyRoles.length ? notifyRoles.map((id: string) => `<@&${id}>`).join(' ') : null;
await (channel as any).send({ content: content || undefined, embeds: [embed], components: [row] });
}
private async updateReviewMessage(interaction: ButtonInteraction, app: any) {
if (!interaction.message || !interaction.message.editable) return;
const embed = (interaction.message.embeds?.[0] as any) ?? null;
if (embed) {
embed.data = { ...(embed.data || {}), footer: { text: `Status: ${app.status} | Reviewer: ${interaction.user.tag}` } };
}
const components = interaction.message.components;
await interaction.message.edit({ embeds: embed ? [embed] : interaction.message.embeds, components });
}
public async listApplications(guildId: string, status?: string, formId?: string) {
const where: any = { guildId };
if (status) where.status = status;
if (formId) where.formId = formId;
return prisma.registerApplication.findMany({
where,
orderBy: { createdAt: 'desc' },
include: { form: true }
});
}
public async getApplication(id: string) {
return prisma.registerApplication.findUnique({
where: { id },
include: { form: true, answers: true }
});
}
}