From db9c7d07c9df5b0628671f5939c4cbd601e66f47 Mon Sep 17 00:00:00 2001 From: D4rkst3r Date: Thu, 30 Jul 2026 00:30:23 +0200 Subject: [PATCH] Carl-Bot-Paket: Moderation, Auto-/Sticky-Roles, Level-System, Tags, geplante Posts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Moderation: /warn (DM + Mod-Log), /warns (Historie + löschen), /timeout (max 28d), /purge (bulk, 14-Tage-Limit sauber gemeldet); Mod-Log erweitert um Join/Leave und Nick-/Rollen-Änderungen - Auto-Rolle bei Join + Sticky-Roles (Rollen bei Leave gesichert, bei Rejoin wiederhergestellt; managed/@everyone ausgenommen) — Rollen-Tab - Level-System (opt-in): 15-25 XP/Nachricht mit 60s-Cooldown, MEE6-Formel, Level-Up-Announce, Rollen-Belohnungen (Level=RolleID), /rank mit Fortschritt, öffentliche Bestenliste /level; Community-Tab - Tags: /tag mit Autocomplete, Verwaltung im Composer-Tab (/api/tags) - Geplante Posts: einmalig/täglich/wöchentlich, Minuten-Scheduler, DST-sicher über Neuberechnung aus Zeitfeldern; Composer-Tab (/api/scheduled) - Autocomplete-Dispatch im InteractionCreate; alles smoke-getestet Co-Authored-By: Claude Fable 5 --- README.md | 6 ++ frontend/src/App.jsx | 3 + frontend/src/pages/Level.jsx | 64 ++++++++++++ frontend/src/pages/Settings.jsx | 179 ++++++++++++++++++++++++++++++++ frontend/src/style.css | 39 +++++++ src/bot/client.js | 22 +++- src/bot/commands/purge.js | 45 ++++++++ src/bot/commands/rank.js | 43 ++++++++ src/bot/commands/tag.js | 33 ++++++ src/bot/commands/timeout.js | 62 +++++++++++ src/bot/commands/warn.js | 53 ++++++++++ src/bot/commands/warns.js | 44 ++++++++ src/bot/levels.js | 76 ++++++++++++++ src/bot/mod-tools.js | 82 ++++++++++++++- src/db.js | 107 +++++++++++++++++++ src/index.js | 2 + src/runtime-settings.js | 32 ++++++ src/web/api.js | 93 ++++++++++++++++- src/web/scheduled-posts.js | 86 +++++++++++++++ 19 files changed, 1066 insertions(+), 5 deletions(-) create mode 100644 frontend/src/pages/Level.jsx create mode 100644 src/bot/commands/purge.js create mode 100644 src/bot/commands/rank.js create mode 100644 src/bot/commands/tag.js create mode 100644 src/bot/commands/timeout.js create mode 100644 src/bot/commands/warn.js create mode 100644 src/bot/commands/warns.js create mode 100644 src/bot/levels.js create mode 100644 src/web/scheduled-posts.js diff --git a/README.md b/README.md index b12bde8..e49edbe 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,11 @@ SQLite (better-sqlite3, FTS5), Docker Multi-Stage — deploybar als Portainer-St | 📬 **Modmail** | DM an den Bot → Thread im privaten Staff-Kanal; Antworten im Thread gehen als DM zurück | | 🎫 **Tickets** | `/ticket-setup` postet den Button; Klick öffnet einen privaten Thread (ein offenes Ticket pro User, Schließen-Button) | | 🎭 **Rollen-Menüs** | Carl-Bot-Ersatz: Menüs im Webinterface bauen (Emoji, Label, Rolle, optional exklusiv), Bot postet Button-Embeds; Klick = Rolle nehmen/abgeben, Posts jederzeit editierbar | +| 🛡️ **Moderation** | `/warn` (DM + Historie), `/warns`, `/timeout`, `/purge` — alles im Mod-Log; dazu Join/Leave- und Nick-/Rollen-Änderungs-Logging | +| 🪪 **Auto- & Sticky-Roles** | Start-Rolle für neue Member; Rollen werden bei Leave gesichert und bei Rejoin wiederhergestellt | +| 📈 **Level-System** | XP pro Nachricht (60s-Cooldown, MEE6-Formel), `/rank`, Level-Up-Announce, Rollen-Belohnungen, öffentliche Bestenliste auf `/level` | +| 🏷️ **Tags** | `/tag ` (mit Autocomplete) postet gespeicherte Text-Bausteine — verwaltet im Composer-Tab | +| ⏰ **Geplante Posts** | Einmalig, täglich oder wöchentlich — im Composer-Tab geplant, vom Bot gepostet (bei Carl Premium) | | 👋 **Willkommens-Embed** | Begrüßung neuer Member im Brand-Look (braucht Server-Members-Intent) | | 📋 **Mod-Log** | Gelöschte/bearbeitete Nachrichten in einen privaten Log-Kanal | | 🎮 **Server-Monitor** | FiveM-kompatible Game-Server (/dynamic.json): Live-Status-Embed + Spielerzahl in der Bot-Presence | @@ -42,6 +47,7 @@ SQLite (better-sqlite3, FTS5), Docker Multi-Stage — deploybar als Portainer-St | `/devlogs` | öffentlich | Devlog-Archiv: Timeline, Bildergalerien, Projekt-Chips, **Volltextsuche** | | `/roadmap` | öffentlich | Meilensteine aus Gitea mit Fortschrittsbalken | | `/galerie` | öffentlich | Community-Screenshots aus dem Discord | +| `/level` | öffentlich | XP-Bestenliste mit Fortschrittsbalken | | `/changelog` | öffentlich | Alle Releases mit Notes, Tag- und Pre-Release-Chips | | `/feed.xml` | öffentlich | RSS-Feed der Devlogs | | `/commits` | nur Admin | Archivierte Commits aller Repos (SHA → Gitea-Link) | diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 29b40bc..748ca20 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -5,6 +5,7 @@ import Devlogs from './pages/Devlogs.jsx'; import Changelog from './pages/Changelog.jsx'; import Roadmap from './pages/Roadmap.jsx'; import Galerie from './pages/Galerie.jsx'; +import Level from './pages/Level.jsx'; import Commits from './pages/Commits.jsx'; import Settings from './pages/Settings.jsx'; @@ -74,6 +75,7 @@ export default function App() { Devlog Roadmap Galerie + Level Changelog {me.admin && Commits} {me.admin && Setup} @@ -100,6 +102,7 @@ export default function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/frontend/src/pages/Level.jsx b/frontend/src/pages/Level.jsx new file mode 100644 index 0000000..9086d29 --- /dev/null +++ b/frontend/src/pages/Level.jsx @@ -0,0 +1,64 @@ +import { useEffect, useState } from 'react'; +import { apiGet } from '../api.js'; + +/** Muss der Formel in src/bot/levels.js entsprechen */ +function xpForLevel(level) { + let total = 0; + for (let n = 0; n < level; n++) total += 5 * n * n + 50 * n + 100; + return total; +} + +const MEDALS = ['🥇', '🥈', '🥉']; + +export default function Level() { + const [rows, setRows] = useState(null); + const [error, setError] = useState(false); + + useEffect(() => { + apiGet('/api/levels').then((d) => setRows(d.levels)).catch(() => setError(true)); + window.scrollTo(0, 0); + }, []); + + return ( + <> +
+
LEVEL
+
+ +
+ {error &&

Bestenliste konnte nicht geladen werden.

} + {!error && !rows &&

Lade …

} + {rows?.length === 0 &&

Noch keine XP vergeben — schreibt was im Discord! ✍️

} + + {rows?.length > 0 && ( +
+ {rows.map((r, i) => { + const base = xpForLevel(r.level); + const next = xpForLevel(r.level + 1); + const pct = Math.floor(((r.xp - base) / (next - base)) * 100); + return ( +
+ {MEDALS[i] ?? `#${i + 1}`} + {r.username ?? r.user_id} +
+
+
+ Lv. {r.level} + {r.xp.toLocaleString('de-DE')} XP +
+ ); + })} +
+ )} +
+ + ); +} diff --git a/frontend/src/pages/Settings.jsx b/frontend/src/pages/Settings.jsx index b6a725c..a65ebb1 100644 --- a/frontend/src/pages/Settings.jsx +++ b/frontend/src/pages/Settings.jsx @@ -49,6 +49,14 @@ export default function Settings({ me }) { const [menuDraft, setMenuDraft] = useState(emptyMenu); // Composer const [composer, setComposer] = useState({ channel: '', title: '', text: '', messageId: '' }); + // Tags + geplante Posts + const [tags, setTags] = useState([]); + const [tagDraft, setTagDraft] = useState({ name: '', content: '' }); + const [scheduled, setScheduled] = useState([]); + const [schedDraft, setSchedDraft] = useState({ + channel_id: '', embed_title: '', content: '', type: 'once', + date: '', time: '18:00', weekday: '5', + }); // API-Keys const [keys, setKeys] = useState([]); const [newKeyName, setNewKeyName] = useState(''); @@ -66,8 +74,51 @@ export default function Settings({ me }) { apiGet('/api/apikeys').then((d) => setKeys(d.keys)).catch(() => {}); apiGet('/api/playtesters').then((d) => setPlaytesters(d.playtesters)).catch(() => {}); apiGet('/api/rolemenus').then((d) => setMenus(d.menus)).catch(() => {}); + apiGet('/api/tags').then((d) => setTags(d.tags)).catch(() => {}); + apiGet('/api/scheduled').then((d) => setScheduled(d.posts)).catch(() => {}); }, [me.admin]); + async function saveTagDraft() { + if (!tagDraft.name.trim() || !tagDraft.content.trim()) { + flash('✗ Name und Inhalt nötig'); + return; + } + try { + await apiPut(`/api/tags/${encodeURIComponent(tagDraft.name.trim().toLowerCase())}`, { content: tagDraft.content }); + setTags((await apiGet('/api/tags')).tags); + setTagDraft({ name: '', content: '' }); + flash('✓ Tag gespeichert'); + } catch { + flash('✗ Tag fehlgeschlagen (Name: a-z, 0-9, -, _)'); + } + } + + async function removeTag(name) { + if (!window.confirm(`Tag „${name}" löschen?`)) return; + await apiDelete(`/api/tags/${encodeURIComponent(name)}`).catch(() => {}); + setTags((await apiGet('/api/tags')).tags); + } + + async function createSchedule() { + if (!schedDraft.channel_id || !schedDraft.content.trim()) { + flash('✗ Kanal und Text nötig'); + return; + } + try { + await apiPost('/api/scheduled', schedDraft); + setScheduled((await apiGet('/api/scheduled')).posts); + setSchedDraft({ ...schedDraft, content: '', embed_title: '' }); + flash('✓ Post geplant'); + } catch { + flash('✗ Planung fehlgeschlagen — Datum/Zeit prüfen'); + } + } + + async function removeSchedule(id) { + await apiDelete(`/api/scheduled/${id}`).catch(() => {}); + setScheduled((await apiGet('/api/scheduled')).posts); + } + function flash(msg) { setFeedback(msg); setTimeout(() => setFeedback(null), 3000); @@ -375,11 +426,54 @@ export default function Settings({ me }) { + +
+

// Level-System

+
+ +
+
+ +
+
+ + setForm({ ...form, level_rewards: e.target.value })} /> +
+
); const tabRollen = ( <> +
+

// Rollen bei Join

+
+ + +
+
+ +
+
+ +
+
+

// Rollen-Menüs

@@ -497,6 +591,7 @@ export default function Settings({ me }) { ); const tabComposer = ( + <>

// Post verfassen

@@ -524,6 +619,90 @@ export default function Settings({ me }) {
+ +
+

// Geplante Posts

+ {scheduled.length > 0 && ( +
+ {scheduled.map((p) => ( +
+ {channelName(p.channel_id)} + + {{ once: 'einmalig', daily: 'täglich', weekly: 'wöchentlich' }[p.type]} {p.time} + {p.enabled ? '' : ' · erledigt'} + + {p.content.slice(0, 40)}… + +
+ ))} +
+ )} +
+ +
+ + +
+
+
+
+ {schedDraft.type === 'once' && ( + setSchedDraft({ ...schedDraft, date: e.target.value })} /> + )} + {schedDraft.type === 'weekly' && ( + + )} + setSchedDraft({ ...schedDraft, time: e.target.value })} /> +
+
+
+ setSchedDraft({ ...schedDraft, embed_title: e.target.value })} /> +
+
+