Feature-Voting, Giveaways und Contribution-Heatmap

- /wunsch: Voting-Embed mit 👍, Reaction-Tracking (add/remove) in SQLite,
  Top-20-Rangliste öffentlich auf der Roadmap-Seite
- /giveaway (Admin): Preis/Dauer/Gewinnerzahl, 🎉-Teilnahme-Button (toggle),
  Minuten-Scheduler zieht Gewinner, schließt das Embed ab und pingt sie
- Contribution-Heatmap: 52-Wochen-Grid in Brand-Gelb aus dem Commit-Archiv
  (/api/heatmap) auf der Roadmap-Seite
- Setting voting_channel_id auf der Setup-Seite

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 13:20:44 +02:00
co-authored by Claude Opus 4.8
parent 075ea36bd2
commit bc000aaa9f
12 changed files with 391 additions and 4 deletions
+35 -2
View File
@@ -5,15 +5,17 @@ import { devlogChannelId, devlogPingRoleId, playtesterRoleId } from '../runtime-
import { archiveDevlogMessage, removeDevlog } from './devlog-archive.js';
import { registerCommunityListeners } from './community.js';
import { registerModTools } from './mod-tools.js';
import { addPlaytester, removePlaytester } from '../db.js';
import { addPlaytester, removePlaytester, isWish, bumpWish, getGiveaway, toggleGiveawayEntry, giveawayEntries } from '../db.js';
import * as ping from './commands/ping.js';
import * as devlogBackfill from './commands/devlog-backfill.js';
import * as bug from './commands/bug.js';
import * as playtesterSetup from './commands/playtester-setup.js';
import * as galerieBackfill from './commands/galerie-backfill.js';
import * as wunsch from './commands/wunsch.js';
import * as giveaway from './commands/giveaway.js';
// Alle Commands hier eintragen — jedes Modul exportiert { data, execute }
const commandModules = [ping, devlogBackfill, bug, playtesterSetup, galerieBackfill];
const commandModules = [ping, devlogBackfill, bug, playtesterSetup, galerieBackfill, wunsch, giveaway];
export async function startBot() {
const client = new Client({
@@ -57,6 +59,19 @@ export async function startBot() {
}
});
// 👍-Reaktionen auf Feature-Wünsche zählen (für die Rangliste auf der Webseite)
const trackWishReaction = (delta) => async (reaction) => {
try {
if (reaction.emoji.name !== '👍') return;
if (reaction.partial) await reaction.fetch().catch(() => {});
if (isWish(reaction.message.id)) bumpWish(reaction.message.id, delta);
} catch (error) {
console.error('[voting] Reaction-Tracking fehlgeschlagen:', error);
}
};
client.on(Events.MessageReactionAdd, trackWishReaction(1));
client.on(Events.MessageReactionRemove, trackWishReaction(-1));
// In Discord gelöscht = im Archiv gelöscht (inkl. lokaler Bilder)
client.on(Events.MessageDelete, async (message) => {
if (message.channelId !== devlogChannelId()) return;
@@ -102,6 +117,24 @@ export async function startBot() {
return;
}
// 🎉-Button: Giveaway-Teilnahme togglen
if (interaction.isButton() && interaction.customId === 'giveaway_enter') {
const g = getGiveaway(interaction.message.id);
if (!g || g.ended) {
await interaction.reply({ content: '❌ Dieses Giveaway ist schon vorbei.', flags: MessageFlags.Ephemeral }).catch(() => {});
return;
}
const entered = toggleGiveawayEntry(g.message_id, interaction.user.id);
const count = giveawayEntries(g.message_id).length;
await interaction.reply({
content: entered
? `🎉 Du bist dabei! (${count} Teilnahmen)`
: `🚪 Teilnahme zurückgezogen. (${count} Teilnahmen)`,
flags: MessageFlags.Ephemeral,
}).catch(() => {});
return;
}
// 🧪-Button: Playtester-Rolle + Liste togglen
if (interaction.isButton() && interaction.customId === 'playtester_toggle') {
const roleId = playtesterRoleId();