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
+64
View File
@@ -114,6 +114,70 @@ export function takeBugReport(repo, issueNumber) {
return row ?? null;
}
// Community: Playtester, Starboard, Screenshot-Galerie
db.exec(`
CREATE TABLE IF NOT EXISTS playtesters (
user_id TEXT PRIMARY KEY,
username TEXT,
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS starboard (
message_id TEXT PRIMARY KEY,
board_message_id TEXT,
starred_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS gallery (
message_id TEXT PRIMARY KEY,
author_name TEXT,
images TEXT NOT NULL,
posted_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_gallery_posted ON gallery (posted_at DESC);
`);
const insertPlaytester = db.prepare(
'INSERT OR IGNORE INTO playtesters (user_id, username) VALUES (?, ?)'
);
const deletePlaytester = db.prepare('DELETE FROM playtesters WHERE user_id = ?');
const listPlaytestersStmt = db.prepare(
'SELECT user_id, username, applied_at FROM playtesters ORDER BY applied_at'
);
export const addPlaytester = (userId, username) => insertPlaytester.run(userId, username).changes > 0;
export const removePlaytester = (userId) => deletePlaytester.run(userId).changes > 0;
export const listPlaytesters = () => listPlaytestersStmt.all();
const hasStarboardStmt = db.prepare('SELECT 1 FROM starboard WHERE message_id = ?');
const insertStarboard = db.prepare(
'INSERT OR IGNORE INTO starboard (message_id, board_message_id) VALUES (?, ?)'
);
export const hasStarboard = (id) => Boolean(hasStarboardStmt.get(id));
export const saveStarboard = (id, boardId) => insertStarboard.run(id, boardId);
const insertGallery = db.prepare(`
INSERT OR IGNORE INTO gallery (message_id, author_name, images, posted_at)
VALUES (@message_id, @author_name, @images, @posted_at)
`);
const hasGalleryStmt = db.prepare('SELECT 1 FROM gallery WHERE message_id = ?');
const getGalleryImagesStmt = db.prepare('SELECT images FROM gallery WHERE message_id = ?');
const deleteGalleryStmt = db.prepare('DELETE FROM gallery WHERE message_id = ?');
const listGalleryStmt = db.prepare(`
SELECT message_id, author_name, images, posted_at
FROM gallery ORDER BY posted_at DESC LIMIT ? OFFSET ?
`);
const countGalleryStmt = db.prepare('SELECT count(*) AS n FROM gallery');
export const saveGalleryItem = (item) => insertGallery.run(item).changes > 0;
export const hasGalleryItem = (id) => Boolean(hasGalleryStmt.get(id));
export function deleteGalleryItem(id) {
const row = getGalleryImagesStmt.get(id);
if (!row) return { deleted: false, images: [] };
deleteGalleryStmt.run(id);
return { deleted: true, images: JSON.parse(row.images || '[]') };
}
export const listGallery = (limit, offset) => ({
items: listGalleryStmt.all(limit, offset),
total: countGalleryStmt.get().n,
});
// API-Keys für externe Skripte (/api/v1/*) — nur der SHA-256-Hash wird gespeichert
db.exec(`
CREATE TABLE IF NOT EXISTS api_keys (