Carl-Bot-Paket: Moderation, Auto-/Sticky-Roles, Level-System, Tags, geplante Posts

- 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 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 00:30:23 +02:00
co-authored by Claude Fable 5
parent a27a594a07
commit db9c7d07c9
19 changed files with 1066 additions and 5 deletions
+107
View File
@@ -252,6 +252,113 @@ export function commitHeatmap() {
.all();
}
// Moderation: Verwarnungen + Sticky-Roles
db.exec(`
CREATE TABLE IF NOT EXISTS warns (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
moderator TEXT,
reason TEXT NOT NULL,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_warns_user ON warns (user_id);
CREATE TABLE IF NOT EXISTS sticky_roles (
user_id TEXT PRIMARY KEY,
role_ids TEXT NOT NULL DEFAULT '[]',
saved_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`);
const insertWarn = db.prepare('INSERT INTO warns (user_id, moderator, reason) VALUES (?, ?, ?)');
const warnsOfStmt = db.prepare('SELECT * FROM warns WHERE user_id = ? ORDER BY id');
const deleteWarnStmt = db.prepare('DELETE FROM warns WHERE id = ?');
export const addWarn = (userId, moderator, reason) => insertWarn.run(userId, moderator, reason).lastInsertRowid;
export const warnsOf = (userId) => warnsOfStmt.all(userId);
export const deleteWarn = (id) => deleteWarnStmt.run(id).changes > 0;
const upsertSticky = db.prepare(`
INSERT INTO sticky_roles (user_id, role_ids, saved_at) VALUES (?, ?, datetime('now'))
ON CONFLICT(user_id) DO UPDATE SET role_ids = excluded.role_ids, saved_at = excluded.saved_at
`);
const stickyOfStmt = db.prepare('SELECT role_ids FROM sticky_roles WHERE user_id = ?');
export const saveStickyRoles = (userId, roleIds) => upsertSticky.run(userId, JSON.stringify(roleIds));
export const stickyRolesOf = (userId) => JSON.parse(stickyOfStmt.get(userId)?.role_ids ?? '[]');
// Level-System: XP pro Nachricht (Cooldown), Bestenliste auf der Webseite
db.exec(`
CREATE TABLE IF NOT EXISTS levels (
user_id TEXT PRIMARY KEY,
username TEXT,
xp INTEGER NOT NULL DEFAULT 0,
level INTEGER NOT NULL DEFAULT 0,
last_xp_ms INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_levels_xp ON levels (xp DESC);
`);
const getLevelStmt = db.prepare('SELECT * FROM levels WHERE user_id = ?');
const upsertLevelStmt = db.prepare(`
INSERT INTO levels (user_id, username, xp, level, last_xp_ms) VALUES (?, ?, ?, ?, ?)
ON CONFLICT(user_id) DO UPDATE SET username = excluded.username, xp = excluded.xp,
level = excluded.level, last_xp_ms = excluded.last_xp_ms
`);
const topLevelsStmt = db.prepare('SELECT user_id, username, xp, level FROM levels ORDER BY xp DESC LIMIT ?');
const rankOfStmt = db.prepare('SELECT count(*) + 1 AS rank FROM levels WHERE xp > (SELECT xp FROM levels WHERE user_id = ?)');
export const getLevelRow = (userId) => getLevelStmt.get(userId) ?? null;
export const setLevelRow = (r) => upsertLevelStmt.run(r.user_id, r.username, r.xp, r.level, r.last_xp_ms);
export const topLevels = (limit = 50) => topLevelsStmt.all(limit);
export const rankOf = (userId) => (getLevelStmt.get(userId) ? rankOfStmt.get(userId).rank : null);
// Tags (eigene Text-Befehle) + geplante Posts
db.exec(`
CREATE TABLE IF NOT EXISTS tags (
name TEXT PRIMARY KEY,
content TEXT NOT NULL,
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
);
CREATE TABLE IF NOT EXISTS scheduled_posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel_id TEXT NOT NULL,
content TEXT NOT NULL,
embed_title TEXT,
type TEXT NOT NULL,
date TEXT,
time TEXT NOT NULL,
weekday INTEGER,
next_run_at TEXT NOT NULL,
last_run_at TEXT,
enabled INTEGER NOT NULL DEFAULT 1
);
`);
const upsertTag = db.prepare(`
INSERT INTO tags (name, content, updated_at) VALUES (?, ?, datetime('now'))
ON CONFLICT(name) DO UPDATE SET content = excluded.content, updated_at = excluded.updated_at
`);
const getTagStmt = db.prepare('SELECT * FROM tags WHERE name = ?');
const listTagsStmt = db.prepare('SELECT name, content, updated_at FROM tags ORDER BY name');
const deleteTagStmt = db.prepare('DELETE FROM tags WHERE name = ?');
export const saveTag = (name, content) => upsertTag.run(name, content);
export const getTag = (name) => getTagStmt.get(name) ?? null;
export const listTags = () => listTagsStmt.all();
export const deleteTag = (name) => deleteTagStmt.run(name).changes > 0;
const insertScheduled = db.prepare(`
INSERT INTO scheduled_posts (channel_id, content, embed_title, type, date, time, weekday, next_run_at)
VALUES (@channel_id, @content, @embed_title, @type, @date, @time, @weekday, @next_run_at)
`);
const listScheduledStmt = db.prepare('SELECT * FROM scheduled_posts ORDER BY next_run_at');
const dueScheduledStmt = db.prepare(
`SELECT * FROM scheduled_posts WHERE enabled = 1 AND next_run_at <= datetime('now')`
);
const updateScheduledRunStmt = db.prepare(
`UPDATE scheduled_posts SET next_run_at = ?, last_run_at = datetime('now'), enabled = ? WHERE id = ?`
);
const deleteScheduledStmt = db.prepare('DELETE FROM scheduled_posts WHERE id = ?');
export const createScheduledPost = (p) => insertScheduled.run(p).lastInsertRowid;
export const listScheduledPosts = () => listScheduledStmt.all();
export const dueScheduledPosts = () => dueScheduledStmt.all();
export const markScheduledRun = (id, nextRunAt, stillEnabled) =>
updateScheduledRunStmt.run(nextRunAt, stillEnabled ? 1 : 0, id);
export const deleteScheduledPost = (id) => deleteScheduledStmt.run(id).changes > 0;
// Rollen-Menüs (Carl-Bot-Ersatz): selbst zuweisbare Rollen per Button-Post
db.exec(`
CREATE TABLE IF NOT EXISTS role_menus (