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:
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user