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
+2 -2
View File
@@ -76,10 +76,10 @@ auf nichts mehr: keine Posts, keine Hintergrund-Prüfungen.
| 🎉 **Giveaways** | `/giveaway` (Admin): Teilnahme-Button, automatische Ziehung; am Gewinner-Post 🔁 Neu auslosen + 👥 Teilnehmerliste | | 🎉 **Giveaways** | `/giveaway` (Admin): Teilnahme-Button, automatische Ziehung; am Gewinner-Post 🔁 Neu auslosen + 👥 Teilnehmerliste |
| 🎂 **Geburtstage** | `/geburtstag` zum Eintragen; morgens Gratulation im Kanal + Tages-Rolle | | 🎂 **Geburtstage** | `/geburtstag` zum Eintragen; morgens Gratulation im Kanal + Tages-Rolle |
| 📅 **Events** | Neue Discord-Events werden angekündigt + öffentliche Events-Seite | | 📅 **Events** | Neue Discord-Events werden angekündigt + öffentliche Events-Seite |
| 🧪 **Playtester-Programm** | `/playtester-setup` postet den Bewerbungs-Button; eigener Panel-Bereich mit Liste, Rolle und dem Schlüssel-Vorrat — Playtester lassen sich dort auch wieder entfernen (die Rolle geht mit) | | 🧪 **Playtester-Programm** | Aufnahme läuft über ein **Bewerbungs-Formular**: ausfüllen, das Team nimmt an oder lehnt ab. Wer angenommen wird, bekommt Rolle und Listeneintrag. Eigener Panel-Bereich mit Liste, Rolle und Schlüssel-Vorrat — Playtester lassen sich dort auch wieder entfernen (die Rolle geht mit) |
| 🎟️ **Alpha-Keys** | Schlüssel-Vorrat im Panel: nachlegen, gezielt an eine Person schicken, an alle Wartenden verteilen, zurückziehen oder löschen. Zustellung per Direktnachricht — kommt sie nicht an, bleibt der Schlüssel frei | | 🎟️ **Alpha-Keys** | Schlüssel-Vorrat im Panel: nachlegen, gezielt an eine Person schicken, an alle Wartenden verteilen, zurückziehen oder löschen. Zustellung per Direktnachricht — kommt sie nicht an, bleibt der Schlüssel frei |
| 🎭 **Rollen-Menüs** | Menüs im Webinterface bauen (Emoji, Label, Rolle, optional exklusiv), Bot postet Button-Embeds; Posts jederzeit editierbar | | 🎭 **Rollen-Menüs** | Menüs im Webinterface bauen (Emoji, Label, Rolle, optional exklusiv), Bot postet Button-Embeds; Posts jederzeit editierbar |
| 📋 **Bewerbungen** | Formulare (bis 5 Fragen) als Discord-Modal, Review mit ✅/❌ im Staff-Kanal, Rolle + DM bei Annahme — z. B. FiveM-Whitelist | | 📋 **Bewerbungen** | Formulare (bis 5 Fragen) als Discord-Modal, Review mit ✅/❌ im Staff-Kanal, Rolle + DM bei Annahme — z. B. FiveM-Whitelist. Ein Formular lässt sich als **Playtester-Formular** markieren; Angenommene landen dann auch im Playtester-Programm |
| 🪪 **Verknüpfte Rollen** | Discords *Linked Roles*: Level, Playtester-Status und Dabei-seit werden als geprüfte Kennzahlen an Discord gemeldet — Rollen vergibt dann Discord selbst | | 🪪 **Verknüpfte Rollen** | Discords *Linked Roles*: Level, Playtester-Status und Dabei-seit werden als geprüfte Kennzahlen an Discord gemeldet — Rollen vergibt dann Discord selbst |
### Moderation & Support ### Moderation & Support
+43 -5
View File
@@ -322,7 +322,7 @@ export default function Settings({ me }) {
// Bewerbungs-Formulare // Bewerbungs-Formulare
const emptyForm = { const emptyForm = {
id: null, title: '', description: '', review_channel_id: '', id: null, title: '', description: '', review_channel_id: '',
approve_role_id: '', post_channel_id: '', questions: [''], approve_role_id: '', post_channel_id: '', questions: [''], playtester: false,
}; };
const [appForms, setAppForms] = useState([]); const [appForms, setAppForms] = useState([]);
const [formDraft, setFormDraft] = useState(emptyForm); const [formDraft, setFormDraft] = useState(emptyForm);
@@ -2195,10 +2195,29 @@ export default function Settings({ me }) {
<div className="settings-section"> <div className="settings-section">
<h2 className="settings-title">// Playtester</h2> <h2 className="settings-title">// Playtester</h2>
<p className="section-intro"> <p className="section-intro">
Wer sich über den Knopf aus <strong>/playtester-setup</strong> meldet, landet Beworben wird sich über ein Formular, angenommen vom Team wer angenommen
hier mit Rolle. Schlüssel gehen per Direktnachricht raus; kommt sie nicht wird, landet hier mit Rolle. Schlüssel gehen per Direktnachricht raus; kommt
an, bleibt der Schlüssel im Vorrat. sie nicht an, bleibt der Schlüssel im Vorrat.
</p> </p>
{(() => {
// Ohne markiertes Formular gibt es keinen Weg herein — das
// ist der haeufigste Grund, warum sich niemand bewirbt
const ptForm = appForms.find((f) => f.playtester);
return ptForm ? (
<p className="section-intro">
Aufnahme läuft über <strong>{ptForm.title}</strong>
{ptForm.message_id ? '' : ' — das Formular ist noch nicht gepostet'}.
{' '}<button className="btn btn-mini" onClick={() => setTab('bewerbungen')}>Formular öffnen</button>
</p>
) : (
<p className="notice">
Noch kein Playtester-Formular. Ohne eines kommt niemand herein
unter <strong>Bewerbungen</strong> ein Formular anlegen und dort
Annahme trägt als Playtester ein" ankreuzen.
{' '}<button className="btn btn-mini" onClick={() => setTab('bewerbungen')}>Zu den Bewerbungen</button>
</p>
);
})()}
<div className="field" data-setting="playtester_role_id"> <div className="field" data-setting="playtester_role_id">
<label>Playtester-Rolle <span className="field-hint">Bewerbungs-Post via /playtester-setup; leer = aus</span></label> <label>Playtester-Rolle <span className="field-hint">Bewerbungs-Post via /playtester-setup; leer = aus</span></label>
<PickSelect <PickSelect
@@ -2367,12 +2386,16 @@ export default function Settings({ me }) {
{appForms.map((f) => ( {appForms.map((f) => (
<div className="key-row" key={f.id}> <div className="key-row" key={f.id}>
<span className="key-name">{f.title}</span> <span className="key-name">{f.title}</span>
<span className="key-scopes">{f.questions.length} Fragen{f.message_id ? ' · gepostet' : ' · Entwurf'}</span> <span className="key-scopes">
{f.questions.length} Fragen{f.message_id ? ' · gepostet' : ' · Entwurf'}
{f.playtester ? ' · Playtester' : ''}
</span>
<button className="btn btn-mini" onClick={() => setFormDraft({ <button className="btn btn-mini" onClick={() => setFormDraft({
id: f.id, title: f.title, description: f.description, id: f.id, title: f.title, description: f.description,
review_channel_id: f.review_channel_id ?? '', approve_role_id: f.approve_role_id ?? '', review_channel_id: f.review_channel_id ?? '', approve_role_id: f.approve_role_id ?? '',
post_channel_id: f.post_channel_id ?? '', post_channel_id: f.post_channel_id ?? '',
questions: f.questions.length ? f.questions : [''], questions: f.questions.length ? f.questions : [''],
playtester: Boolean(f.playtester),
})}>Bearbeiten</button> })}>Bearbeiten</button>
<button className="card-delete" title="Formular + Post löschen" onClick={() => removeForm(f.id)}>✕</button> <button className="card-delete" title="Formular + Post löschen" onClick={() => removeForm(f.id)}>✕</button>
</div> </div>
@@ -2411,6 +2434,21 @@ export default function Settings({ me }) {
/> />
</div> </div>
</div> </div>
<div className="field">
<label className="toggle">
<input
type="checkbox"
checked={Boolean(formDraft.playtester)}
onChange={(e) => setFormDraft({ ...formDraft, playtester: e.target.checked })}
/>
<span>Annahme trägt als Playtester ein</span>
<span className="field-hint">
Angenommene landen zusätzlich in der Playtester-Liste und bekommen von dort
ihren Schlüssel. <strong>/playtester-setup</strong> postet dann diesen Knopf —
so kann sich niemand mehr selbst aufnehmen. Es kann nur ein Formular sein.
</span>
</label>
</div>
<div className="field"> <div className="field">
<label>Fragen <span className="field-hint">max 5 (Discord-Modal-Limit)</span></label> <label>Fragen <span className="field-hint">max 5 (Discord-Modal-Limit)</span></label>
{formDraft.questions.map((q, i) => ( {formDraft.questions.map((q, i) => (
+6 -1
View File
@@ -5,7 +5,7 @@ import {
ModalBuilder, TextInputBuilder, TextInputStyle, ModalBuilder, TextInputBuilder, TextInputStyle,
} from 'discord.js'; } from 'discord.js';
import { import {
getAppForm, setAppFormMessage, getAppForm, setAppFormMessage, addPlaytester,
createApplication, getApplication, openApplicationOf, setApplicationStatus, createApplication, getApplication, openApplicationOf, setApplicationStatus,
} from '../db.js'; } from '../db.js';
import { brandColor, brandColor2, brandFooter } from '../runtime-settings.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}`) 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 // DM an den Bewerber
const user = await interaction.client.users.fetch(application.user_id).catch(() => null); const user = await interaction.client.users.fetch(application.user_id).catch(() => null);
await user?.send(renderTemplate(approved ? 'application.approved' : 'application.denied', { await user?.send(renderTemplate(approved ? 'application.approved' : 'application.denied', {
+15 -9
View File
@@ -22,7 +22,7 @@ import {
import { handleRoleMenuButton } from './role-menus.js'; import { handleRoleMenuButton } from './role-menus.js';
import { handleGiveawayAdminButton } from './giveaways.js'; import { handleGiveawayAdminButton } from './giveaways.js';
import { import {
addPlaytester, removePlaytester, isWish, bumpWish, removePlaytester, playtesterForm, isWish, bumpWish,
getGiveaway, toggleGiveawayEntry, giveawayEntries, getGiveaway, toggleGiveawayEntry, giveawayEntries,
} from '../db.js'; } from '../db.js';
import * as ping from './commands/ping.js'; import * as ping from './commands/ping.js';
@@ -309,22 +309,28 @@ export async function startBot() {
return; 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') { if (interaction.isButton() && interaction.customId === 'playtester_toggle') {
const roleId = playtesterRoleId(); const roleId = playtesterRoleId();
try { try {
if (!roleId || !interaction.inGuild()) throw new Error('Keine Playtester-Rolle konfiguriert'); if (!roleId || !interaction.inGuild()) throw new Error('Keine Playtester-Rolle konfiguriert');
const member = interaction.member; const member = interaction.member;
const hasRole = member.roles.cache.has(roleId); if (!member.roles.cache.has(roleId)) {
if (hasRole) { 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); await member.roles.remove(roleId);
removePlaytester(interaction.user.id); removePlaytester(interaction.user.id);
} else {
await member.roles.add(roleId);
addPlaytester(interaction.user.id, interaction.user.username);
}
await interaction.reply({ await interaction.reply({
content: renderTemplate(hasRole ? 'playtester.left' : 'playtester.joined', { content: renderTemplate('playtester.left', {
user: member.displayName ?? interaction.user.username, user: member.displayName ?? interaction.user.username,
mention: `<@${interaction.user.id}>`, 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 { import {
SlashCommandBuilder, PermissionFlagsBits, MessageFlags, SlashCommandBuilder, PermissionFlagsBits, MessageFlags,
ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder,
} from 'discord.js'; } from 'discord.js';
import { playtesterRoleId, brandColor, brandFooter } from '../../runtime-settings.js'; import { playtesterRoleId, brandColor, brandFooter } from '../../runtime-settings.js';
import { renderTemplate } from '../../templates.js'; import { renderTemplate } from '../../templates.js';
import { playtesterForm } from '../../db.js';
export const data = new SlashCommandBuilder() export const data = new SlashCommandBuilder()
.setName('playtester-setup') .setName('playtester-setup')
.setDescription('Postet das Playtester-Bewerbungs-Embed in diesen Kanal') .setDescription('Postet den Playtester-Aufruf in diesen Kanal')
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator); .setDefaultMemberPermissions(PermissionFlagsBits.Administrator);
export async function execute(interaction) { export async function execute(interaction) {
if (!playtesterRoleId()) { if (!playtesterRoleId()) {
await interaction.reply({ 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, flags: MessageFlags.Ephemeral,
}); });
return; return;
@@ -26,14 +58,19 @@ export async function execute(interaction) {
.setDescription(renderTemplate('playtester.panel_text')) .setDescription(renderTemplate('playtester.panel_text'))
.setFooter({ text: brandFooter('PLAYTEST') }); .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( const row = new ActionRowBuilder().addComponents(
new ButtonBuilder() new ButtonBuilder()
.setCustomId('playtester_toggle') .setCustomId(`apply:${form.id}`)
.setStyle(ButtonStyle.Success) .setStyle(ButtonStyle.Success)
.setLabel('Als Playtester bewerben') .setLabel('Als Playtester bewerben')
.setEmoji('🧪') .setEmoji('🧪')
); );
await interaction.channel.send({ embeds: [embed], components: [row] }); 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,
});
} }
+17 -3
View File
@@ -340,24 +340,38 @@ db.exec(`
created_at TEXT NOT NULL DEFAULT (datetime('now')) created_at TEXT NOT NULL DEFAULT (datetime('now'))
); );
`); `);
// Migration: Annahme kann zusaetzlich ins Playtester-Programm eintragen
{
const cols = db.prepare('PRAGMA table_info(app_forms)').all().map((c) => c.name);
if (!cols.includes('playtester')) {
db.exec('ALTER TABLE app_forms ADD COLUMN playtester INTEGER NOT NULL DEFAULT 0');
}
}
const insertAppForm = db.prepare(` const insertAppForm = db.prepare(`
INSERT INTO app_forms (title, description, review_channel_id, approve_role_id, post_channel_id, questions) INSERT INTO app_forms (title, description, review_channel_id, approve_role_id, post_channel_id, questions, playtester)
VALUES (@title, @description, @review_channel_id, @approve_role_id, @post_channel_id, @questions) VALUES (@title, @description, @review_channel_id, @approve_role_id, @post_channel_id, @questions, @playtester)
`); `);
const updateAppFormStmt = db.prepare(` const updateAppFormStmt = db.prepare(`
UPDATE app_forms SET title = @title, description = @description, review_channel_id = @review_channel_id, UPDATE app_forms SET title = @title, description = @description, review_channel_id = @review_channel_id,
approve_role_id = @approve_role_id, post_channel_id = @post_channel_id, questions = @questions approve_role_id = @approve_role_id, post_channel_id = @post_channel_id, questions = @questions,
playtester = @playtester
WHERE id = @id WHERE id = @id
`); `);
const setAppFormMessageStmt = db.prepare('UPDATE app_forms SET message_id = ? WHERE id = ?'); const setAppFormMessageStmt = db.prepare('UPDATE app_forms SET message_id = ? WHERE id = ?');
const getAppFormStmt = db.prepare('SELECT * FROM app_forms WHERE id = ?'); const getAppFormStmt = db.prepare('SELECT * FROM app_forms WHERE id = ?');
const listAppFormsStmt = db.prepare('SELECT * FROM app_forms ORDER BY id'); const listAppFormsStmt = db.prepare('SELECT * FROM app_forms ORDER BY id');
const deleteAppFormStmt = db.prepare('DELETE FROM app_forms WHERE id = ?'); const deleteAppFormStmt = db.prepare('DELETE FROM app_forms WHERE id = ?');
// Nur eines kann das Playtester-Formular sein — sonst weiss /playtester-setup
// nicht, welches es posten soll
const clearPlaytesterFormStmt = db.prepare('UPDATE app_forms SET playtester = 0 WHERE id != ?');
const playtesterFormStmt = db.prepare('SELECT * FROM app_forms WHERE playtester = 1 LIMIT 1');
export const createAppForm = (f) => insertAppForm.run(f).lastInsertRowid; export const createAppForm = (f) => insertAppForm.run(f).lastInsertRowid;
export const updateAppForm = (f) => updateAppFormStmt.run(f).changes > 0; export const updateAppForm = (f) => updateAppFormStmt.run(f).changes > 0;
export const setAppFormMessage = (id, messageId) => setAppFormMessageStmt.run(messageId, id); export const setAppFormMessage = (id, messageId) => setAppFormMessageStmt.run(messageId, id);
export const getAppForm = (id) => getAppFormStmt.get(id) ?? null; export const getAppForm = (id) => getAppFormStmt.get(id) ?? null;
export const listAppForms = () => listAppFormsStmt.all(); export const listAppForms = () => listAppFormsStmt.all();
export const playtesterForm = () => playtesterFormStmt.get() ?? null;
export const clearOtherPlaytesterForms = (id) => clearPlaytesterFormStmt.run(id);
export const deleteAppForm = (id) => deleteAppFormStmt.run(id).changes > 0; export const deleteAppForm = (id) => deleteAppFormStmt.run(id).changes > 0;
const insertApplication = db.prepare(` const insertApplication = db.prepare(`
+2 -2
View File
@@ -213,8 +213,8 @@ export const TEMPLATES = [
vars: [], vars: [],
default: default:
'Du willst **EcoGame** vor allen anderen spielen und mit deinem Feedback die Entwicklung mitgestalten?\n\n' + 'Du willst **EcoGame** vor allen anderen spielen und mit deinem Feedback die Entwicklung mitgestalten?\n\n' +
'Ein Klick auf den Button setzt dich auf die Playtester-Liste — sobald die Alpha startet, ' + 'Klick auf den Button und füll das kurze Formular aus. Wir schauen es uns an und ' +
'bekommst du deinen Zugang direkt per DM. Abmelden geht jederzeit mit einem zweiten Klick.', 'melden uns per DM — bei einer Zusage bekommst du Rolle und Zugang.',
}, },
]; ];
+11 -3
View File
@@ -9,7 +9,7 @@ import {
topLevels, saveTag, listTags, deleteTag, topLevels, saveTag, listTags, deleteTag,
createScheduledPost, listScheduledPosts, deleteScheduledPost, createScheduledPost, listScheduledPosts, deleteScheduledPost,
addAlphaKeys, freeAlphaKeyCount, assignedAlphaKeys, alphaKeyOf, reserveAlphaKey, unreserveAlphaKey, addAlphaKeys, freeAlphaKeyCount, assignedAlphaKeys, alphaKeyOf, reserveAlphaKey, unreserveAlphaKey,
createAppForm, updateAppForm, getAppForm, listAppForms, deleteAppForm, createAppForm, updateAppForm, getAppForm, listAppForms, deleteAppForm, clearOtherPlaytesterForms,
saveTrigger, listTriggers, deleteTrigger, activityRange, saveTrigger, listTriggers, deleteTrigger, activityRange,
createGameserver, updateGameserver, listGameservers, getGameserver, deleteGameserver, createGameserver, updateGameserver, listGameservers, getGameserver, deleteGameserver,
saveWebAdmin, webAdminScopes, listWebAdmins, deleteWebAdmin, saveWebAdmin, webAdminScopes, listWebAdmins, deleteWebAdmin,
@@ -1446,9 +1446,15 @@ ${rssItems}
approve_role_id: roleId || null, approve_role_id: roleId || null,
post_channel_id: String(body.post_channel_id ?? '') || null, post_channel_id: String(body.post_channel_id ?? '') || null,
questions: JSON.stringify(questions), questions: JSON.stringify(questions),
playtester: body.playtester ? 1 : 0,
}; };
} }
/** Nur ein Formular kann das Playtester-Formular sein */
function playtesterExklusiv(form, id) {
if (form.playtester) clearOtherPlaytesterForms(id);
}
app.get('/api/appforms', async (request, reply) => { app.get('/api/appforms', async (request, reply) => {
if (requireScope(request, reply, 'bewerbungen')) return; if (requireScope(request, reply, 'bewerbungen')) return;
return { forms: listAppForms().map(formToJson) }; return { forms: listAppForms().map(formToJson) };
@@ -1459,7 +1465,8 @@ ${rssItems}
const form = parseFormBody(request, reply); const form = parseFormBody(request, reply);
if (!form) return; if (!form) return;
const id = createAppForm(form); const id = createAppForm(form);
return { ok: true, form: formToJson(getAppForm(id)) }; playtesterExklusiv(form, id);
return { ok: true, form: formToJson(getAppForm(id)), forms: listAppForms().map(formToJson) };
}); });
app.put('/api/appforms/:id', async (request, reply) => { app.put('/api/appforms/:id', async (request, reply) => {
@@ -1469,12 +1476,13 @@ ${rssItems}
const form = parseFormBody(request, reply); const form = parseFormBody(request, reply);
if (!form) return; if (!form) return;
updateAppForm({ ...form, id }); updateAppForm({ ...form, id });
playtesterExklusiv(form, id);
if (getAppForm(id).message_id) { if (getAppForm(id).message_id) {
await publishAppForm(client, id).catch((e) => await publishAppForm(client, id).catch((e) =>
request.log.warn(`Formular ${id}: Discord-Update fehlgeschlagen: ${e.message}`) request.log.warn(`Formular ${id}: Discord-Update fehlgeschlagen: ${e.message}`)
); );
} }
return { ok: true, form: formToJson(getAppForm(id)) }; return { ok: true, form: formToJson(getAppForm(id)), forms: listAppForms().map(formToJson) };
}); });
app.post('/api/appforms/:id/publish', async (request, reply) => { app.post('/api/appforms/:id/publish', async (request, reply) => {