Files
d4rkbot/src/bot/app-forms.js
T
D4rkst3randClaude Opus 5 1c5c7d2470
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Playtester werden jetzt beworben, nicht angeklickt
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>
2026-08-01 10:04:16 +02:00

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: [] });
}