Playtester werden jetzt beworben, nicht angeklickt
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled

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>
This commit is contained in:
2026-08-01 10:04:16 +02:00
co-authored by Claude Opus 5
parent ddd7610d6c
commit 1c5c7d2470
8 changed files with 139 additions and 31 deletions
+6 -1
View File
@@ -5,7 +5,7 @@ import {
ModalBuilder, TextInputBuilder, TextInputStyle,
} from 'discord.js';
import {
getAppForm, setAppFormMessage,
getAppForm, setAppFormMessage, addPlaytester,
createApplication, getApplication, openApplicationOf, setApplicationStatus,
} from '../db.js';
import { brandColor, brandColor2, brandFooter } from '../runtime-settings.js';
@@ -172,6 +172,11 @@ export async function handleReviewButton(interaction) {
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', {
+16 -10
View File
@@ -22,7 +22,7 @@ import {
import { handleRoleMenuButton } from './role-menus.js';
import { handleGiveawayAdminButton } from './giveaways.js';
import {
addPlaytester, removePlaytester, isWish, bumpWish,
removePlaytester, playtesterForm, isWish, bumpWish,
getGiveaway, toggleGiveawayEntry, giveawayEntries,
} from '../db.js';
import * as ping from './commands/ping.js';
@@ -309,22 +309,28 @@ export async function startBot() {
return;
}
// 🧪-Button: Playtester-Rolle + Liste togglen
// 🧪-Button aus älteren Posts. Aufgenommen wird hier niemand mehr —
// das läuft über das Playtester-Formular mit Prüfung durch das Team.
// Austreten bleibt Selbstbedienung: dafür braucht es keine Freigabe.
if (interaction.isButton() && interaction.customId === 'playtester_toggle') {
const roleId = playtesterRoleId();
try {
if (!roleId || !interaction.inGuild()) throw new Error('Keine Playtester-Rolle konfiguriert');
const member = interaction.member;
const hasRole = member.roles.cache.has(roleId);
if (hasRole) {
await member.roles.remove(roleId);
removePlaytester(interaction.user.id);
} else {
await member.roles.add(roleId);
addPlaytester(interaction.user.id, interaction.user.username);
if (!member.roles.cache.has(roleId)) {
const form = playtesterForm();
await interaction.reply({
content: form
? '❕ Dieser Knopf ist veraltet. Bewirb dich über den Playtester-Aufruf — dort füllst du ein kurzes Formular aus, das Team meldet sich.'
: '❕ Bewerbungen laufen gerade nicht über diesen Knopf. Melde dich beim Team.',
flags: MessageFlags.Ephemeral,
});
return;
}
await member.roles.remove(roleId);
removePlaytester(interaction.user.id);
await interaction.reply({
content: renderTemplate(hasRole ? 'playtester.left' : 'playtester.joined', {
content: renderTemplate('playtester.left', {
user: member.displayName ?? interaction.user.username,
mention: `<@${interaction.user.id}>`,
}),
+42 -5
View File
@@ -1,20 +1,52 @@
// /playtester-setup — postet das Bewerbungs-Embed mit Button in den aktuellen Kanal (Admin)
// /playtester-setup — postet den Playtester-Aufruf in den aktuellen Kanal (Admin)
//
// Ist ein Bewerbungs-Formular als Playtester-Formular markiert, führt der Knopf
// dorthin: ausfüllen, das Team entscheidet. Ohne Formular gäbe es nur den alten
// Selbstbedienungs-Knopf — und der macht jeden zum Playtester, der draufdrückt.
// Deshalb sagt der Befehl dann, was zu tun ist, statt ihn zu posten.
import {
SlashCommandBuilder, PermissionFlagsBits, MessageFlags,
ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder,
} from 'discord.js';
import { playtesterRoleId, brandColor, brandFooter } from '../../runtime-settings.js';
import { renderTemplate } from '../../templates.js';
import { playtesterForm } from '../../db.js';
export const data = new SlashCommandBuilder()
.setName('playtester-setup')
.setDescription('Postet das Playtester-Bewerbungs-Embed in diesen Kanal')
.setDescription('Postet den Playtester-Aufruf in diesen Kanal')
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator);
export async function execute(interaction) {
if (!playtesterRoleId()) {
await interaction.reply({
content: '❌ Erst auf der Config-Seite eine Playtester-Rolle wählen.',
content: '❌ Erst in der Config eine Playtester-Rolle wählen.',
flags: MessageFlags.Ephemeral,
});
return;
}
const form = playtesterForm();
if (!form) {
await interaction.reply({
content: '❌ Kein Playtester-Formular gesetzt.\n'
+ 'Config → **Bewerbungen**: ein Formular anlegen und dort '
+ '„Annahme trägt als Playtester ein" ankreuzen. Dann kann sich '
+ 'niemand mehr per Knopfdruck selbst aufnehmen.',
flags: MessageFlags.Ephemeral,
});
return;
}
if (JSON.parse(form.questions || '[]').length === 0) {
await interaction.reply({
content: `❌ Das Formular „${form.title}" hat noch keine Fragen.`,
flags: MessageFlags.Ephemeral,
});
return;
}
if (!form.review_channel_id) {
await interaction.reply({
content: `❌ Für „${form.title}" fehlt der Review-Kanal — sonst sieht niemand die Bewerbungen.`,
flags: MessageFlags.Ephemeral,
});
return;
@@ -26,14 +58,19 @@ export async function execute(interaction) {
.setDescription(renderTemplate('playtester.panel_text'))
.setFooter({ text: brandFooter('PLAYTEST') });
// Derselbe Knopf wie beim Bewerbungs-Formular — das Formular, die Prüfung
// und die Antwort-DMs sind dieselbe Maschinerie, nur ein anderer Aufhänger.
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
.setCustomId('playtester_toggle')
.setCustomId(`apply:${form.id}`)
.setStyle(ButtonStyle.Success)
.setLabel('Als Playtester bewerben')
.setEmoji('🧪')
);
await interaction.channel.send({ embeds: [embed], components: [row] });
await interaction.reply({ content: '✅ Bewerbungs-Post erstellt.', flags: MessageFlags.Ephemeral });
await interaction.reply({
content: `✅ Aufruf gepostet — Bewerbungen laufen über „${form.title}".`,
flags: MessageFlags.Ephemeral,
});
}