// Bewerbungs-Formulare: Button-Post → Discord-Modal (bis 5 Fragen) → Review-Embed // mit ✅/❌ im Staff-Kanal; Annahme vergibt optional eine Rolle + DM. import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, MessageFlags, ModalBuilder, TextInputBuilder, TextInputStyle, } from 'discord.js'; import { getAppForm, setAppFormMessage, addPlaytester, createApplication, getApplication, openApplicationOf, setApplicationStatus, } from '../db.js'; import { brandColor, brandColor2, brandFooter } from '../runtime-settings.js'; import { renderTemplate } from '../templates.js'; export const MAX_QUESTIONS = 5; // Discord-Modal-Limit /** Bewerbungs-Post (Embed + Button) senden/aktualisieren */ export async function publishAppForm(client, formId) { const form = getAppForm(formId); if (!form) throw new Error('Formular nicht gefunden'); if (!form.post_channel_id) throw new Error('Kein Post-Kanal gewählt'); if (!form.review_channel_id) throw new Error('Kein Review-Kanal gewählt'); if (JSON.parse(form.questions || '[]').length === 0) throw new Error('Formular hat keine Fragen'); const channel = await client.channels.fetch(form.post_channel_id); if (!channel?.isTextBased()) throw new Error('Post-Kanal nicht gefunden'); const payload = { embeds: [ new EmbedBuilder() .setColor(brandColor()) .setTitle(`📋 ${form.title}`) .setDescription(form.description?.trim() || 'Klick auf den Button und füll das Formular aus.') .setFooter({ text: brandFooter('BEWERBUNG') }), ], components: [ new ActionRowBuilder().addComponents( new ButtonBuilder() .setCustomId(`apply:${form.id}`) .setStyle(ButtonStyle.Primary) .setLabel('Jetzt bewerben') .setEmoji('📋') ), ], }; if (form.message_id) { const existing = await channel.messages.fetch(form.message_id).catch(() => null); if (existing) { await existing.edit(payload); return form.message_id; } } const message = await channel.send(payload); setAppFormMessage(form.id, message.id); return message.id; } export async function unpublishAppForm(client, form) { if (!form.post_channel_id || !form.message_id) return; const channel = await client.channels.fetch(form.post_channel_id).catch(() => null); const message = await channel?.messages?.fetch(form.message_id).catch(() => null); await message?.delete().catch(() => {}); } /** Button „Jetzt bewerben" → Modal öffnen */ export async function handleApplyButton(interaction) { const formId = Number(interaction.customId.split(':')[1]); const form = getAppForm(formId); if (!form) { await interaction.reply({ content: '❌ Dieses Formular ist nicht mehr aktiv.', flags: MessageFlags.Ephemeral }); return; } if (openApplicationOf(formId, interaction.user.id)) { await interaction.reply({ content: '❕ Du hast hier schon eine offene Bewerbung — das Team meldet sich!', flags: MessageFlags.Ephemeral, }); return; } const modal = new ModalBuilder() .setCustomId(`applymodal:${form.id}`) .setTitle(form.title.slice(0, 45)); JSON.parse(form.questions).slice(0, MAX_QUESTIONS).forEach((q, i) => { modal.addComponents( new ActionRowBuilder().addComponents( new TextInputBuilder() .setCustomId(`q${i}`) .setLabel(String(q).slice(0, 45)) .setStyle(TextInputStyle.Paragraph) .setMaxLength(600) .setRequired(true) ) ); }); await interaction.showModal(modal); } /** Modal abgeschickt → speichern + Review-Embed in den Staff-Kanal */ export async function handleApplyModal(interaction) { const formId = Number(interaction.customId.split(':')[1]); const form = getAppForm(formId); if (!form) { await interaction.reply({ content: '❌ Formular nicht mehr aktiv.', flags: MessageFlags.Ephemeral }); return; } const questions = JSON.parse(form.questions); const answers = questions.map((q, i) => ({ q, a: interaction.fields.getTextInputValue(`q${i}`), })); const appId = createApplication( form.id, interaction.user.id, interaction.member?.displayName ?? interaction.user.username, answers ); const review = await interaction.client.channels.fetch(form.review_channel_id).catch(() => null); if (review?.isTextBased()) { await review.send({ embeds: [ new EmbedBuilder() .setColor(brandColor()) .setAuthor({ name: `${interaction.user.username} (${interaction.user.id})`, iconURL: interaction.user.displayAvatarURL({ size: 64 }), }) .setTitle(`📋 Bewerbung #${appId} — ${form.title}`) .addFields(answers.map(({ q, a }) => ({ name: String(q).slice(0, 250), value: String(a).slice(0, 1000) || '—', }))) .setFooter({ text: brandFooter('BEWERBUNG') }) .setTimestamp(), ], components: [ new ActionRowBuilder().addComponents( new ButtonBuilder().setCustomId(`appdec:${appId}:approve`).setStyle(ButtonStyle.Success).setLabel('Annehmen').setEmoji('✅'), new ButtonBuilder().setCustomId(`appdec:${appId}:deny`).setStyle(ButtonStyle.Danger).setLabel('Ablehnen').setEmoji('❌') ), ], }); } await interaction.reply({ content: renderTemplate('application.submitted', { user: interaction.member?.displayName ?? interaction.user.username, mention: `<@${interaction.user.id}>`, form: form.title, }), flags: MessageFlags.Ephemeral, }); } /** ✅/❌ im Review-Kanal → Status, Rolle, DM, Embed abschließen */ export async function handleReviewButton(interaction) { const [, appIdRaw, decision] = interaction.customId.split(':'); const application = getApplication(Number(appIdRaw)); if (!application || application.status !== 'open') { await interaction.reply({ content: '❌ Bewerbung nicht (mehr) offen.', flags: MessageFlags.Ephemeral }); return; } const form = getAppForm(application.form_id); const approved = decision === 'approve'; setApplicationStatus(application.id, approved ? 'approved' : 'denied'); // Rolle bei Annahme if (approved && form?.approve_role_id) { const member = await interaction.guild.members.fetch(application.user_id).catch(() => null); await member?.roles?.add(form.approve_role_id).catch((e) => console.error(`[bewerbung] Rolle fehlgeschlagen: ${e.message}`) ); } // Playtester-Formular: Annahme trägt zusätzlich in die Liste ein, damit die // Schlüssel-Verteilung denjenigen kennt if (approved && form?.playtester) { addPlaytester(application.user_id, application.username ?? application.user_id); } // DM an den Bewerber const user = await interaction.client.users.fetch(application.user_id).catch(() => null); await user?.send(renderTemplate(approved ? 'application.approved' : 'application.denied', { user: user.username, mention: `<@${application.user_id}>`, form: form?.title ?? '', })).catch(() => {}); // Review-Embed abschließen const original = EmbedBuilder.from(interaction.message.embeds[0]) .setColor(approved ? brandColor() : brandColor2()) .setTitle(`${approved ? '✅ Angenommen' : '❌ Abgelehnt'} — ${interaction.message.embeds[0].title?.replace('📋 ', '') ?? ''}`) .setFooter({ text: `${brandFooter('BEWERBUNG')} • ${approved ? 'angenommen' : 'abgelehnt'} von ${interaction.member?.displayName ?? interaction.user.username}` }); await interaction.update({ embeds: [original], components: [] }); }