Der Knopf unter dem Playtester-Aufruf hat jeden aufgenommen, der ihn gedrueckt hat — ohne Frage, ohne Pruefung. Fuer ein Programm, dessen Teilnehmer Zugang und Alpha-Keys bekommen, ist das die falsche Tuer. Statt einen zweiten Pruef-Ablauf danebenzustellen, laeuft die Aufnahme jetzt ueber die Bewerbungs-Formulare, die es laengst gibt: Modal ausfuellen, Review-Embed im Staff-Kanal, ✅ oder ❌, Rolle und Antwort-DM bei Zusage. Das ist dieselbe Maschinerie, nur ein anderer Aufhaenger — und damit auch nur eine Stelle, an der spaeter etwas kaputtgehen kann. Ein Formular laesst sich als Playtester-Formular markieren ("Annahme traegt als Playtester ein"). Wer dort angenommen wird, landet zusaetzlich in der Playtester-Liste, damit die Schluessel-Verteilung ihn kennt. Nur eines kann es sein — sonst wuesste /playtester-setup nicht, welches es posten soll. /playtester-setup postet jetzt den Knopf dieses Formulars. Fehlt das Formular, fehlen Fragen oder fehlt der Review-Kanal, sagt der Befehl das, statt einen Aufruf zu posten, der ins Leere fuehrt. Der alte Knopf in bereits geposteten Nachrichten nimmt niemanden mehr auf: er verweist auf den Aufruf. Austreten bleibt Selbstbedienung — dafuer braucht es keine Freigabe. Im Panel steht im Playtester-Bereich, ueber welches Formular die Aufnahme laeuft, ob es schon gepostet ist, und ein Weg dorthin. Ohne markiertes Formular steht dort, dass gerade niemand hereinkommt — das ist sonst der Grund, warum sich tagelang niemand bewirbt. Geprueft: Routen, SQL gegen das Schema, und im Browser beide Zustaende des Hinweises sowie der neue Schalter — beim Wechsel zwischen zwei Formularen folgt er dem richtigen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
195 lines
8.2 KiB
JavaScript
195 lines
8.2 KiB
JavaScript
// 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: [] });
|
|
}
|