Community-Endgame: Alpha-Keys, Bewerbungen, Events, Stats, Triggers, /remind, Social, Temp-Voice + MEE6-Bot-Karte
- Alpha-Keys: Pool im Community-Tab, Ein-Klick-Verteilung per DM an alle Playtester (Key bleibt frei, wenn die DM geblockt wird) - Bewerbungs-Formulare: Builder im neuen Bewerbungen-Tab (bis 5 Fragen), Button → Discord-Modal → Review-Embed mit ✅/❌, Rolle + DM bei Entscheidung - Events: GuildScheduledEventCreate → Announce-Embed; öffentliche /events-Seite aus den Discord-Events (5-min-Cache) - Server-Stats: activity_daily (Nachrichten/Joins/Leaves) → Balken-Chart auf /level - Triggers (Auto-Antworten, 30s-Cooldown), /remind (DM-Scheduler), Twitch-Live (Helix, App-Creds write-only) + YouTube-RSS-Announcements, Temp-Voice (Join to Create, Cleanup bei Leerstand + Start) - Brand-Tab: MEE6-Style Bot-Identity-Karte (Avatar-Vorschau mit Status-Dot, Bot-Name via setUsername, Presence online/idle/dnd, Aktivität) - Neue Intents: GuildVoiceStates, GuildScheduledEvents; alles smoke-getestet Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
// 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,
|
||||
createApplication, getApplication, openApplicationOf, setApplicationStatus,
|
||||
} from '../db.js';
|
||||
import { brandColor, brandColor2, brandFooter } from '../runtime-settings.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: '✅ Bewerbung eingereicht — danke! Du bekommst eine DM, sobald sie geprüft wurde.',
|
||||
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}`)
|
||||
);
|
||||
}
|
||||
// DM an den Bewerber
|
||||
const user = await interaction.client.users.fetch(application.user_id).catch(() => null);
|
||||
await user?.send(
|
||||
approved
|
||||
? `✅ Deine Bewerbung **„${form?.title ?? ''}"** wurde angenommen — willkommen! 🎉`
|
||||
: `❌ Deine Bewerbung **„${form?.title ?? ''}"** wurde leider abgelehnt.`
|
||||
).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: [] });
|
||||
}
|
||||
Reference in New Issue
Block a user