/bug und /wunsch als Eingabefenster
Beide Befehle nahmen ihre Eingaben als Slash-Optionen entgegen. Das heißt: eine Fehlerbeschreibung einzeilig in die Befehlszeile tippen, ohne Umbrüche, ohne Absätze. Entsprechend dürftig fielen die Meldungen aus. /bug öffnet jetzt ein Fenster mit vier Feldern: Titel, was ist passiert, was war erwartet, wie kann man es nachstellen. Die letzten beiden sind freiwillig und erzeugen im Issue nur dann eine Überschrift, wenn sie ausgefüllt sind. /wunsch fragt die Idee und ein optionales „warum wäre das gut?" ab — die Rückfrage macht aus einem Einzeiler einen Vorschlag, über den man abstimmen kann. Der Screenshot bleibt eine Option am Befehl, weil Discord in Fenstern keine Dateien erlaubt. Er hängt damit an der Befehls-Interaktion, das Formular kommt aber als eigene zurück — der Anhang wird deshalb kurz zwischengeparkt und nach dem Absenden verwendet. Abgelaufene Einträge räumt der nächste Aufruf mit weg, damit da nichts liegen bleibt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -155,6 +155,21 @@ export async function startBot() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Eingabefenster von /bug und /wunsch
|
||||
if (interaction.isModalSubmit() && (interaction.customId === 'bugmodal' || interaction.customId === 'wunschmodal')) {
|
||||
const handler = interaction.customId === 'bugmodal' ? bug.handleModal : wunsch.handleModal;
|
||||
try {
|
||||
await handler(interaction);
|
||||
} catch (error) {
|
||||
console.error(`[${interaction.customId}] Fehler:`, error);
|
||||
const meldung = { content: '❌ Da ist etwas schiefgelaufen.', flags: MessageFlags.Ephemeral };
|
||||
await (interaction.deferred || interaction.replied
|
||||
? interaction.editReply(meldung.content)
|
||||
: interaction.reply(meldung)).catch(() => {});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Bewerbungen: Button → Modal → Review-Entscheidung
|
||||
try {
|
||||
if (interaction.isButton() && interaction.customId.startsWith('apply:')) {
|
||||
|
||||
+87
-28
@@ -1,5 +1,14 @@
|
||||
// /bug — Bug-Report aus Discord: erstellt ein Gitea-Issue (inkl. Screenshot-Upload)
|
||||
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
|
||||
// /bug — Bug-Report aus Discord: öffnet ein Eingabefenster und legt daraus ein
|
||||
// Gitea-Issue an (inkl. Screenshot-Upload).
|
||||
//
|
||||
// Warum ein Modal statt Slash-Optionen: In der Befehlszeile tippt man eine
|
||||
// Beschreibung einzeilig und ohne Umbrüche. Das Fenster bietet mehrzeilige
|
||||
// Felder, getrennt nach „was ist passiert" und „was war erwartet" — und die
|
||||
// Meldungen werden dadurch spürbar brauchbarer.
|
||||
import {
|
||||
SlashCommandBuilder, MessageFlags,
|
||||
ModalBuilder, ActionRowBuilder, TextInputBuilder, TextInputStyle,
|
||||
} from 'discord.js';
|
||||
|
||||
import { bugReportRepo, giteaApiToken } from '../../runtime-settings.js';
|
||||
import { createIssue, uploadIssueAsset } from '../../gitea-api.js';
|
||||
@@ -9,48 +18,98 @@ import { renderTemplate } from '../../templates.js';
|
||||
export const data = new SlashCommandBuilder()
|
||||
.setName('bug')
|
||||
.setDescription('Bug melden — landet direkt beim Entwickler')
|
||||
.addStringOption((o) =>
|
||||
o.setName('titel').setDescription('Kurze Zusammenfassung').setRequired(true).setMaxLength(120)
|
||||
)
|
||||
.addStringOption((o) =>
|
||||
o.setName('beschreibung')
|
||||
.setDescription('Was ist passiert? Was hast du erwartet?')
|
||||
.setRequired(true)
|
||||
.setMaxLength(1500)
|
||||
)
|
||||
.addAttachmentOption((o) =>
|
||||
o.setName('screenshot').setDescription('Optional: Screenshot des Problems')
|
||||
);
|
||||
|
||||
export async function execute(interaction) {
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
// Der Screenshot hängt an der Befehls-Interaktion, das Formular kommt aber als
|
||||
// eigene Interaktion zurück — deshalb kurz zwischenparken.
|
||||
const pendingScreenshot = new Map(); // userId → { url, name, at }
|
||||
const SCREENSHOT_TTL_MS = 20 * 60 * 1000;
|
||||
|
||||
function stashScreenshot(userId, attachment) {
|
||||
// Abgelaufene Einträge nebenbei aufräumen, damit die Map nicht wächst
|
||||
for (const [id, entry] of pendingScreenshot) {
|
||||
if (Date.now() - entry.at > SCREENSHOT_TTL_MS) pendingScreenshot.delete(id);
|
||||
}
|
||||
if (attachment?.contentType?.startsWith('image/')) {
|
||||
pendingScreenshot.set(userId, {
|
||||
url: attachment.url,
|
||||
name: attachment.name ?? 'screenshot.png',
|
||||
at: Date.now(),
|
||||
});
|
||||
} else {
|
||||
pendingScreenshot.delete(userId);
|
||||
}
|
||||
}
|
||||
|
||||
export async function execute(interaction) {
|
||||
if (!giteaApiToken()) {
|
||||
await interaction.editReply('❌ Bug-Reports sind gerade nicht konfiguriert (kein Gitea-Token).');
|
||||
await interaction.reply({
|
||||
content: '❌ Bug-Reports sind gerade nicht konfiguriert (kein Gitea-Token).',
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const repo = bugReportRepo();
|
||||
const titel = interaction.options.getString('titel');
|
||||
const beschreibung = interaction.options.getString('beschreibung');
|
||||
const screenshot = interaction.options.getAttachment('screenshot');
|
||||
stashScreenshot(interaction.user.id, interaction.options.getAttachment('screenshot'));
|
||||
|
||||
const body = `${beschreibung}\n\n---\n🎮 Gemeldet via Discord von **${interaction.user.tag}**`;
|
||||
const modal = new ModalBuilder().setCustomId('bugmodal').setTitle('Bug melden');
|
||||
modal.addComponents(
|
||||
new ActionRowBuilder().addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('titel').setLabel('Kurz gesagt: was ist kaputt?')
|
||||
.setPlaceholder('z. B. Inventar öffnet sich nicht nach dem Autoausstieg')
|
||||
.setStyle(TextInputStyle.Short).setMaxLength(120).setRequired(true)
|
||||
),
|
||||
new ActionRowBuilder().addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('was').setLabel('Was ist passiert?')
|
||||
.setStyle(TextInputStyle.Paragraph).setMaxLength(1200).setRequired(true)
|
||||
),
|
||||
new ActionRowBuilder().addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('erwartet').setLabel('Was hättest du erwartet?')
|
||||
.setStyle(TextInputStyle.Paragraph).setMaxLength(600).setRequired(false)
|
||||
),
|
||||
new ActionRowBuilder().addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('schritte').setLabel('Wie kann man es nachstellen?')
|
||||
.setPlaceholder('1. …\n2. …\n3. …')
|
||||
.setStyle(TextInputStyle.Paragraph).setMaxLength(800).setRequired(false)
|
||||
)
|
||||
);
|
||||
await interaction.showModal(modal);
|
||||
}
|
||||
|
||||
/** Formular abgeschickt → Issue anlegen */
|
||||
export async function handleModal(interaction) {
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
const repo = bugReportRepo();
|
||||
const titel = interaction.fields.getTextInputValue('titel');
|
||||
const was = interaction.fields.getTextInputValue('was');
|
||||
const erwartet = interaction.fields.getTextInputValue('erwartet')?.trim();
|
||||
const schritte = interaction.fields.getTextInputValue('schritte')?.trim();
|
||||
|
||||
const body = [
|
||||
was,
|
||||
erwartet ? `\n**Erwartet:**\n${erwartet}` : '',
|
||||
schritte ? `\n**Nachstellen:**\n${schritte}` : '',
|
||||
`\n---\n🎮 Gemeldet via Discord von **${interaction.user.tag}**`,
|
||||
].filter(Boolean).join('\n');
|
||||
|
||||
try {
|
||||
const issue = await createIssue(repo, `[Bug] ${titel}`, body);
|
||||
|
||||
// Screenshot als Issue-Attachment hochladen (Discord-CDN-Links laufen ab)
|
||||
if (screenshot?.contentType?.startsWith('image/')) {
|
||||
// Screenshot als Issue-Anhang hochladen (Discord-CDN-Links laufen ab)
|
||||
const shot = pendingScreenshot.get(interaction.user.id);
|
||||
pendingScreenshot.delete(interaction.user.id);
|
||||
if (shot) {
|
||||
try {
|
||||
const res = await fetch(screenshot.url);
|
||||
const res = await fetch(shot.url);
|
||||
if (res.ok) {
|
||||
await uploadIssueAsset(
|
||||
repo,
|
||||
issue.number,
|
||||
screenshot.name ?? 'screenshot.png',
|
||||
Buffer.from(await res.arrayBuffer())
|
||||
);
|
||||
await uploadIssueAsset(repo, issue.number, shot.name, Buffer.from(await res.arrayBuffer()));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[bug] Screenshot-Upload fehlgeschlagen:', error.message);
|
||||
|
||||
@@ -1,17 +1,50 @@
|
||||
// /wunsch — Feature-Wunsch einreichen: Voting-Post mit 👍, Rangliste auf der Webseite
|
||||
import { SlashCommandBuilder, MessageFlags, EmbedBuilder } from 'discord.js';
|
||||
// /wunsch — Feature-Wunsch einreichen: Eingabefenster → Voting-Post mit 👍,
|
||||
// Rangliste auf der Webseite.
|
||||
//
|
||||
// Das Fenster statt einer Slash-Option, weil eine Idee selten in eine Zeile
|
||||
// passt — und weil die Rückfrage „warum wäre das gut?" die Vorschläge
|
||||
// deutlich besser macht als ein Einzeiler.
|
||||
import {
|
||||
SlashCommandBuilder, MessageFlags, EmbedBuilder,
|
||||
ModalBuilder, ActionRowBuilder, TextInputBuilder, TextInputStyle,
|
||||
} from 'discord.js';
|
||||
import { votingChannelId, publicUrl, brandColor, brandFooter } from '../../runtime-settings.js';
|
||||
import { saveWish } from '../../db.js';
|
||||
import { renderTemplate } from '../../templates.js';
|
||||
|
||||
export const data = new SlashCommandBuilder()
|
||||
.setName('wunsch')
|
||||
.setDescription('Feature-Wunsch für EcoGame einreichen — die Community stimmt ab')
|
||||
.addStringOption((o) =>
|
||||
o.setName('idee').setDescription('Dein Vorschlag').setRequired(true).setMaxLength(500)
|
||||
);
|
||||
.setDescription('Feature-Wunsch einreichen — die Community stimmt ab');
|
||||
|
||||
export async function execute(interaction) {
|
||||
if (!votingChannelId()) {
|
||||
await interaction.reply({
|
||||
content: '❌ Feature-Voting ist gerade nicht aktiviert.',
|
||||
flags: MessageFlags.Ephemeral,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const modal = new ModalBuilder().setCustomId('wunschmodal').setTitle('Feature-Wunsch');
|
||||
modal.addComponents(
|
||||
new ActionRowBuilder().addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('idee').setLabel('Deine Idee')
|
||||
.setPlaceholder('Was soll dazukommen oder anders laufen?')
|
||||
.setStyle(TextInputStyle.Paragraph).setMaxLength(500).setRequired(true)
|
||||
),
|
||||
new ActionRowBuilder().addComponents(
|
||||
new TextInputBuilder()
|
||||
.setCustomId('warum').setLabel('Warum wäre das gut?')
|
||||
.setPlaceholder('Was wird dadurch besser oder einfacher?')
|
||||
.setStyle(TextInputStyle.Paragraph).setMaxLength(400).setRequired(false)
|
||||
)
|
||||
);
|
||||
await interaction.showModal(modal);
|
||||
}
|
||||
|
||||
/** Formular abgeschickt → Voting-Post */
|
||||
export async function handleModal(interaction) {
|
||||
const channelId = votingChannelId();
|
||||
if (!channelId) {
|
||||
await interaction.reply({
|
||||
@@ -22,7 +55,8 @@ export async function execute(interaction) {
|
||||
}
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
const idea = interaction.options.getString('idee');
|
||||
const idee = interaction.fields.getTextInputValue('idee');
|
||||
const warum = interaction.fields.getTextInputValue('warum')?.trim();
|
||||
const channel = await interaction.client.channels.fetch(channelId);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
@@ -32,13 +66,13 @@ export async function execute(interaction) {
|
||||
iconURL: interaction.user.displayAvatarURL({ size: 64 }),
|
||||
})
|
||||
.setTitle('💡 Feature-Wunsch')
|
||||
.setDescription(idea)
|
||||
.setDescription(warum ? `${idee}\n\n**Warum:** ${warum}` : idee)
|
||||
.setURL(`${publicUrl()}/roadmap`)
|
||||
.setFooter({ text: `${brandFooter('VOTING')} • 👍 = will ich!` });
|
||||
|
||||
const message = await channel.send({ embeds: [embed] });
|
||||
await message.react('👍');
|
||||
saveWish(message.id, interaction.user.username, idea);
|
||||
saveWish(message.id, interaction.user.username, idee);
|
||||
|
||||
await interaction.editReply(renderTemplate('wish.submitted', {
|
||||
user: interaction.member?.displayName ?? interaction.user.username,
|
||||
|
||||
Reference in New Issue
Block a user