Community-Paket: Playtester-Programm, Starboard, Screenshot-Galerie

- Playtester: /playtester-setup postet Bewerbungs-Embed mit 🧪-Button;
  Toggle vergibt Rolle + DB-Eintrag; Liste + Rollen-Auswahl auf der Setup-Seite
- Starboard: ab konfigurierbarem -Schwellwert Repost als Embed in den
  Best-of-Kanal (Dedupe, kein Selbst-Boarding); neue Intents/Partials für Reaktionen
- Galerie: Bilder aus dem Screenshot-Kanal lokal archiviert (live + /galerie-backfill,
  Delete-Sync, Bot reagiert mit ) → öffentliche /galerie-Seite mit Hover-Grid
- Setup-Seite: Sektion // Community (Rolle, Starboard-Kanal + Schwellwert, Screenshot-Kanal)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 12:34:34 +02:00
co-authored by Claude Opus 4.8
parent 6d8f383aed
commit a3c47656c5
13 changed files with 563 additions and 12 deletions
+40 -8
View File
@@ -3,6 +3,7 @@ import { EmbedBuilder } from 'discord.js';
import {
listDevlogs, searchDevlogs, listCommits, listReleases, archiveStats,
getSetting, setSetting, createApiKey, listApiKeys, deleteApiKey,
listGallery, listPlaytesters,
} from '../db.js';
import { config } from '../config.js';
import { removeDevlog } from '../bot/devlog-archive.js';
@@ -53,6 +54,23 @@ export function registerApiRoutes(app, client) {
return { items, total, page, pageSize: PAGE_SIZE };
});
// Screenshot-Galerie — öffentlich
app.get('/api/gallery', async (request) => {
const { limit, offset, page } = paging(request);
const { items, total } = listGallery(limit, offset);
const mapped = items.map(({ images, ...rest }) => ({
...rest,
images: JSON.parse(images || '[]').map((f) => `/gallery-assets/${f}`),
}));
return { items: mapped, total, page, pageSize: PAGE_SIZE };
});
// Playtester-Liste — Admin
app.get('/api/playtesters', async (request, reply) => {
if (requireAdmin(request, reply)) return;
return { playtesters: listPlaytesters() };
});
// Roadmap — öffentlich, aus Gitea-Milestones (5-Minuten-Cache gegen API-Hammering)
let roadmapCache = { at: 0, repo: null, data: null };
app.get('/api/roadmap', async (request, reply) => {
@@ -181,6 +199,10 @@ ${rssItems}
gitea_url: getSetting('gitea_url') ?? config.giteaUrl,
backup_enabled: getSetting('backup_enabled') !== '0',
backup_channel_id: getSetting('backup_channel_id') ?? '',
playtester_role_id: getSetting('playtester_role_id') ?? '',
starboard_channel_id: getSetting('starboard_channel_id') ?? '',
starboard_threshold: Number(getSetting('starboard_threshold')) || 3,
screenshot_channel_id: getSetting('screenshot_channel_id') ?? '',
};
}
@@ -208,11 +230,12 @@ ${rssItems}
const body = request.body ?? {};
// Kanäle: müssen existierende Textkanäle sein (release/backup dürfen leer sein = aus)
for (const key of ['commit_channel_id', 'devlog_channel_id', 'release_channel_id', 'backup_channel_id']) {
// Kanäle: müssen existierende Textkanäle sein (optionale dürfen leer sein = aus)
const OPTIONAL_CHANNELS = ['release_channel_id', 'backup_channel_id', 'starboard_channel_id', 'screenshot_channel_id'];
for (const key of ['commit_channel_id', 'devlog_channel_id', ...OPTIONAL_CHANNELS]) {
if (body[key] === undefined) continue;
const value = String(body[key]);
if (value === '' && ['release_channel_id', 'backup_channel_id'].includes(key)) {
if (value === '' && OPTIONAL_CHANNELS.includes(key)) {
setSetting(key, '');
continue;
}
@@ -222,18 +245,27 @@ ${rssItems}
}
setSetting(key, value);
}
// Ping-Rolle: muss existieren ('' = kein Ping)
if (body.devlog_ping_role_id !== undefined) {
const value = String(body.devlog_ping_role_id);
// Rollen: müssen existieren ('' = Feature aus)
for (const key of ['devlog_ping_role_id', 'playtester_role_id']) {
if (body[key] === undefined) continue;
const value = String(body[key]);
if (value !== '') {
const exists = [...client.guilds.cache.values()].some((g) =>
g.roles?.cache?.has?.(value)
);
if (!exists) {
return reply.code(400).send({ error: 'devlog_ping_role_id: Rolle nicht gefunden' });
return reply.code(400).send({ error: `${key}: Rolle nicht gefunden` });
}
}
setSetting('devlog_ping_role_id', value);
setSetting(key, value);
}
// Starboard-Schwellwert: 150
if (body.starboard_threshold !== undefined) {
const n = Number(body.starboard_threshold);
if (!Number.isInteger(n) || n < 1 || n > 50) {
return reply.code(400).send({ error: 'starboard_threshold: 150' });
}
setSetting('starboard_threshold', String(n));
}
if (body.commit_feed_enabled !== undefined) {
setSetting('commit_feed_enabled', body.commit_feed_enabled ? '1' : '0');
+9
View File
@@ -77,6 +77,15 @@ export async function startWebServer(client) {
maxAge: '30d',
immutable: true,
});
// Screenshot-Galerie-Bilder
const { galleryDir } = await import('../bot/community.js');
await app.register(fastifyStatic, {
root: galleryDir,
prefix: '/gallery-assets/',
decorateReply: false,
maxAge: '30d',
immutable: true,
});
if (existsSync(frontendDist)) {
// React-Frontend ausliefern; unbekannte GET-Pfade → index.html (SPA-Routing)