Community-Endgame: Alpha-Keys, Bewerbungen, Events, Stats, Triggers, /remind, Social, Temp-Voice + MEE6-Bot-Karte

- Alpha-Keys: Pool im Community-Tab, Ein-Klick-Verteilung per DM an alle Playtester
  (Key bleibt frei, wenn die DM geblockt wird)
- Bewerbungs-Formulare: Builder im neuen Bewerbungen-Tab (bis 5 Fragen),
  Button → Discord-Modal → Review-Embed mit /, Rolle + DM bei Entscheidung
- Events: GuildScheduledEventCreate → Announce-Embed; öffentliche /events-Seite
  aus den Discord-Events (5-min-Cache)
- Server-Stats: activity_daily (Nachrichten/Joins/Leaves) → Balken-Chart auf /level
- Triggers (Auto-Antworten, 30s-Cooldown), /remind (DM-Scheduler),
  Twitch-Live (Helix, App-Creds write-only) + YouTube-RSS-Announcements,
  Temp-Voice (Join to Create, Cleanup bei Leerstand + Start)
- Brand-Tab: MEE6-Style Bot-Identity-Karte (Avatar-Vorschau mit Status-Dot,
  Bot-Name via setUsername, Presence online/idle/dnd, Aktivität)
- Neue Intents: GuildVoiceStates, GuildScheduledEvents; alles smoke-getestet

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 01:02:47 +02:00
co-authored by Claude Fable 5
parent f7b261c648
commit e72e6c8991
16 changed files with 1368 additions and 47 deletions
+152
View File
@@ -252,6 +252,158 @@ export function commitHeatmap() {
.all();
}
// Alpha-Keys: Pool + Zuweisung an Playtester
db.exec(`
CREATE TABLE IF NOT EXISTS alpha_keys (
key TEXT PRIMARY KEY,
assigned_to TEXT,
assigned_at TEXT
);
`);
const insertAlphaKey = db.prepare('INSERT OR IGNORE INTO alpha_keys (key) VALUES (?)');
const freeKeyCountStmt = db.prepare('SELECT count(*) AS n FROM alpha_keys WHERE assigned_to IS NULL');
const assignedKeysStmt = db.prepare(
`SELECT key, assigned_to, assigned_at FROM alpha_keys WHERE assigned_to IS NOT NULL ORDER BY assigned_at`
);
const hasKeyForUserStmt = db.prepare('SELECT key FROM alpha_keys WHERE assigned_to = ?');
const reserveKeyStmt = db.prepare(`
UPDATE alpha_keys SET assigned_to = ?, assigned_at = datetime('now')
WHERE key = (SELECT key FROM alpha_keys WHERE assigned_to IS NULL LIMIT 1)
RETURNING key
`);
const unreserveKeyStmt = db.prepare('UPDATE alpha_keys SET assigned_to = NULL, assigned_at = NULL WHERE key = ?');
export const addAlphaKeys = db.transaction((keys) => {
let added = 0;
for (const k of keys) added += insertAlphaKey.run(k).changes;
return added;
});
export const freeAlphaKeyCount = () => freeKeyCountStmt.get().n;
export const assignedAlphaKeys = () => assignedKeysStmt.all();
export const alphaKeyOf = (userId) => hasKeyForUserStmt.get(userId)?.key ?? null;
export const reserveAlphaKey = (userId) => reserveKeyStmt.get(userId)?.key ?? null;
export const unreserveAlphaKey = (key) => unreserveKeyStmt.run(key);
// Bewerbungs-Formulare (Modals) + eingereichte Bewerbungen
db.exec(`
CREATE TABLE IF NOT EXISTS app_forms (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
review_channel_id TEXT,
approve_role_id TEXT,
post_channel_id TEXT,
message_id TEXT,
questions TEXT NOT NULL DEFAULT '[]'
);
CREATE TABLE IF NOT EXISTS applications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
form_id INTEGER NOT NULL,
user_id TEXT NOT NULL,
username TEXT,
answers TEXT NOT NULL DEFAULT '[]',
status TEXT NOT NULL DEFAULT 'open',
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
`);
const insertAppForm = db.prepare(`
INSERT INTO app_forms (title, description, review_channel_id, approve_role_id, post_channel_id, questions)
VALUES (@title, @description, @review_channel_id, @approve_role_id, @post_channel_id, @questions)
`);
const updateAppFormStmt = db.prepare(`
UPDATE app_forms SET title = @title, description = @description, review_channel_id = @review_channel_id,
approve_role_id = @approve_role_id, post_channel_id = @post_channel_id, questions = @questions
WHERE id = @id
`);
const setAppFormMessageStmt = db.prepare('UPDATE app_forms SET message_id = ? WHERE id = ?');
const getAppFormStmt = db.prepare('SELECT * FROM app_forms WHERE id = ?');
const listAppFormsStmt = db.prepare('SELECT * FROM app_forms ORDER BY id');
const deleteAppFormStmt = db.prepare('DELETE FROM app_forms WHERE id = ?');
export const createAppForm = (f) => insertAppForm.run(f).lastInsertRowid;
export const updateAppForm = (f) => updateAppFormStmt.run(f).changes > 0;
export const setAppFormMessage = (id, messageId) => setAppFormMessageStmt.run(messageId, id);
export const getAppForm = (id) => getAppFormStmt.get(id) ?? null;
export const listAppForms = () => listAppFormsStmt.all();
export const deleteAppForm = (id) => deleteAppFormStmt.run(id).changes > 0;
const insertApplication = db.prepare(`
INSERT INTO applications (form_id, user_id, username, answers) VALUES (?, ?, ?, ?)
`);
const getApplicationStmt = db.prepare('SELECT * FROM applications WHERE id = ?');
const openApplicationStmt = db.prepare(
`SELECT * FROM applications WHERE form_id = ? AND user_id = ? AND status = 'open'`
);
const setApplicationStatusStmt = db.prepare('UPDATE applications SET status = ? WHERE id = ?');
export const createApplication = (formId, userId, username, answers) =>
insertApplication.run(formId, userId, username, JSON.stringify(answers)).lastInsertRowid;
export const getApplication = (id) => getApplicationStmt.get(id) ?? null;
export const openApplicationOf = (formId, userId) => openApplicationStmt.get(formId, userId) ?? null;
export const setApplicationStatus = (id, status) => setApplicationStatusStmt.run(status, id);
// Triggers (Auto-Antworten), Reminders, Server-Aktivität, Temp-Voice
db.exec(`
CREATE TABLE IF NOT EXISTS triggers (
keyword TEXT PRIMARY KEY,
reply TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS reminders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
text TEXT NOT NULL,
remind_at TEXT NOT NULL,
sent INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS activity_daily (
day TEXT PRIMARY KEY,
messages INTEGER NOT NULL DEFAULT 0,
joins INTEGER NOT NULL DEFAULT 0,
leaves INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS temp_voice (
channel_id TEXT PRIMARY KEY
);
`);
const upsertTrigger = db.prepare(`
INSERT INTO triggers (keyword, reply) VALUES (?, ?)
ON CONFLICT(keyword) DO UPDATE SET reply = excluded.reply
`);
const listTriggersStmt = db.prepare('SELECT keyword, reply FROM triggers ORDER BY keyword');
const deleteTriggerStmt = db.prepare('DELETE FROM triggers WHERE keyword = ?');
export const saveTrigger = (keyword, reply) => upsertTrigger.run(keyword, reply);
export const listTriggers = () => listTriggersStmt.all();
export const deleteTrigger = (keyword) => deleteTriggerStmt.run(keyword).changes > 0;
const insertReminder = db.prepare('INSERT INTO reminders (user_id, text, remind_at) VALUES (?, ?, ?)');
const dueRemindersStmt = db.prepare(
`SELECT * FROM reminders WHERE sent = 0 AND remind_at <= datetime('now')`
);
const markReminderSentStmt = db.prepare('UPDATE reminders SET sent = 1 WHERE id = ?');
export const createReminder = (userId, text, remindAt) => insertReminder.run(userId, text, remindAt).lastInsertRowid;
export const dueReminders = () => dueRemindersStmt.all();
export const markReminderSent = (id) => markReminderSentStmt.run(id);
const bumpActivityStmt = (col) => db.prepare(`
INSERT INTO activity_daily (day, ${col}) VALUES (date('now'), 1)
ON CONFLICT(day) DO UPDATE SET ${col} = ${col} + 1
`);
const bumpMessages = bumpActivityStmt('messages');
const bumpJoins = bumpActivityStmt('joins');
const bumpLeaves = bumpActivityStmt('leaves');
export const countActivity = (kind) =>
(kind === 'join' ? bumpJoins : kind === 'leave' ? bumpLeaves : bumpMessages).run();
const activityRangeStmt = db.prepare(
`SELECT day, messages, joins, leaves FROM activity_daily WHERE day >= date('now', ?) ORDER BY day`
);
export const activityRange = (days = 30) => activityRangeStmt.all(`-${days} days`);
const insertTempVoice = db.prepare('INSERT OR IGNORE INTO temp_voice (channel_id) VALUES (?)');
const deleteTempVoiceStmt = db.prepare('DELETE FROM temp_voice WHERE channel_id = ?');
const listTempVoiceStmt = db.prepare('SELECT channel_id FROM temp_voice');
export const trackTempVoice = (id) => insertTempVoice.run(id);
export const untrackTempVoice = (id) => deleteTempVoiceStmt.run(id);
export const listTempVoice = () => listTempVoiceStmt.all().map((r) => r.channel_id);
const isTempVoiceStmt = db.prepare('SELECT 1 FROM temp_voice WHERE channel_id = ?');
export const isTempVoice = (id) => Boolean(isTempVoiceStmt.get(id));
// Moderation: Verwarnungen + Sticky-Roles
db.exec(`
CREATE TABLE IF NOT EXISTS warns (