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:
+92
-1
@@ -5,8 +5,11 @@ import {
|
||||
getSetting, setSetting, createApiKey, listApiKeys, deleteApiKey,
|
||||
listGallery, listPlaytesters, topWishes, commitHeatmap,
|
||||
createRoleMenu, updateRoleMenu, getRoleMenu, listRoleMenus, deleteRoleMenu,
|
||||
topLevels, saveTag, listTags, deleteTag,
|
||||
createScheduledPost, listScheduledPosts, deleteScheduledPost,
|
||||
} from '../db.js';
|
||||
import { publishRoleMenu, unpublishRoleMenu, MAX_ENTRIES } from '../bot/role-menus.js';
|
||||
import { computeNextRun } from './scheduled-posts.js';
|
||||
import { config } from '../config.js';
|
||||
import { removeDevlog } from '../bot/devlog-archive.js';
|
||||
import { commitChannelId, devlogChannelId, releaseChannelId, devlogPingRoleId, publicUrl, roadmapRepo } from '../runtime-settings.js';
|
||||
@@ -77,6 +80,9 @@ export function registerApiRoutes(app, client) {
|
||||
app.get('/api/wishes', async () => ({ wishes: topWishes(20) }));
|
||||
app.get('/api/heatmap', async () => ({ days: commitHeatmap() }));
|
||||
|
||||
// Level-Bestenliste — öffentlich
|
||||
app.get('/api/levels', async () => ({ levels: topLevels(50) }));
|
||||
|
||||
// Roadmap — öffentlich, aus Gitea-Milestones (5-Minuten-Cache gegen API-Hammering)
|
||||
let roadmapCache = { at: 0, repo: null, data: null };
|
||||
app.get('/api/roadmap', async (request, reply) => {
|
||||
@@ -216,6 +222,11 @@ ${rssItems}
|
||||
voting_channel_id: getSetting('voting_channel_id') ?? '',
|
||||
ticket_channel_id: getSetting('ticket_channel_id') ?? '',
|
||||
gameservers: getSetting('gameservers') ?? '',
|
||||
autorole_id: getSetting('autorole_id') ?? '',
|
||||
sticky_roles_enabled: getSetting('sticky_roles_enabled') === '1',
|
||||
levels_enabled: getSetting('levels_enabled') === '1',
|
||||
levels_announce: getSetting('levels_announce') !== '0',
|
||||
level_rewards: getSetting('level_rewards') ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -263,7 +274,7 @@ ${rssItems}
|
||||
setSetting(key, value);
|
||||
}
|
||||
// Rollen: müssen existieren ('' = Feature aus)
|
||||
for (const key of ['devlog_ping_role_id', 'playtester_role_id']) {
|
||||
for (const key of ['devlog_ping_role_id', 'playtester_role_id', 'autorole_id']) {
|
||||
if (body[key] === undefined) continue;
|
||||
const value = String(body[key]);
|
||||
if (value !== '') {
|
||||
@@ -296,6 +307,18 @@ ${rssItems}
|
||||
if (body.backup_enabled !== undefined) {
|
||||
setSetting('backup_enabled', body.backup_enabled ? '1' : '0');
|
||||
}
|
||||
if (body.sticky_roles_enabled !== undefined) {
|
||||
setSetting('sticky_roles_enabled', body.sticky_roles_enabled ? '1' : '0');
|
||||
}
|
||||
if (body.levels_enabled !== undefined) {
|
||||
setSetting('levels_enabled', body.levels_enabled ? '1' : '0');
|
||||
}
|
||||
if (body.levels_announce !== undefined) {
|
||||
setSetting('levels_announce', body.levels_announce ? '1' : '0');
|
||||
}
|
||||
if (body.level_rewards !== undefined) {
|
||||
setSetting('level_rewards', String(body.level_rewards).trim());
|
||||
}
|
||||
for (const key of ['commit_branch_filter', 'ignored_repos', 'bug_report_repo', 'watchdog_urls', 'roadmap_repo', 'gameservers']) {
|
||||
if (body[key] !== undefined) {
|
||||
setSetting(key, String(body[key]).trim());
|
||||
@@ -421,6 +444,74 @@ ${rssItems}
|
||||
return { deleted: true };
|
||||
});
|
||||
|
||||
// --- Tags (Admin-Verwaltung; Abruf via /tag in Discord) ---
|
||||
|
||||
app.get('/api/tags', async (request, reply) => {
|
||||
if (requireAdmin(request, reply)) return;
|
||||
return { tags: listTags() };
|
||||
});
|
||||
|
||||
app.put('/api/tags/:name', async (request, reply) => {
|
||||
if (requireAdmin(request, reply)) return;
|
||||
const name = String(request.params.name).trim().toLowerCase();
|
||||
const content = String(request.body?.content ?? '').trim();
|
||||
if (!/^[a-z0-9äöüß_-]{1,40}$/.test(name)) {
|
||||
return reply.code(400).send({ error: 'Name: nur Kleinbuchstaben, Zahlen, - und _ (max 40)' });
|
||||
}
|
||||
if (!content) return reply.code(400).send({ error: 'content nötig' });
|
||||
saveTag(name, content.slice(0, 2000));
|
||||
return { ok: true, name };
|
||||
});
|
||||
|
||||
app.delete('/api/tags/:name', async (request, reply) => {
|
||||
if (requireAdmin(request, reply)) return;
|
||||
return { deleted: deleteTag(String(request.params.name).trim().toLowerCase()) };
|
||||
});
|
||||
|
||||
// --- Geplante Posts (Admin) ---
|
||||
|
||||
app.get('/api/scheduled', async (request, reply) => {
|
||||
if (requireAdmin(request, reply)) return;
|
||||
return { posts: listScheduledPosts() };
|
||||
});
|
||||
|
||||
app.post('/api/scheduled', async (request, reply) => {
|
||||
if (requireAdmin(request, reply)) return;
|
||||
const body = request.body ?? {};
|
||||
const channelId = String(body.channel_id ?? '');
|
||||
const content = String(body.content ?? '').trim();
|
||||
if (!client.channels.cache.get(channelId)?.isTextBased?.()) {
|
||||
return reply.code(400).send({ error: 'Kanal nicht gefunden' });
|
||||
}
|
||||
if (!content) return reply.code(400).send({ error: 'content nötig' });
|
||||
|
||||
let nextRunAt;
|
||||
try {
|
||||
nextRunAt = computeNextRun({
|
||||
type: body.type, date: body.date, time: body.time, weekday: body.weekday,
|
||||
});
|
||||
} catch (error) {
|
||||
return reply.code(400).send({ error: error.message });
|
||||
}
|
||||
const id = createScheduledPost({
|
||||
channel_id: channelId,
|
||||
content: content.slice(0, 4000),
|
||||
embed_title: String(body.embed_title ?? '').trim() || null,
|
||||
type: body.type,
|
||||
date: body.date ?? null,
|
||||
time: body.time,
|
||||
weekday: body.weekday ?? null,
|
||||
next_run_at: nextRunAt,
|
||||
});
|
||||
request.log.info(`Geplanter Post ${id} (${body.type}) → ${nextRunAt} UTC`);
|
||||
return { ok: true, id, next_run_at: nextRunAt };
|
||||
});
|
||||
|
||||
app.delete('/api/scheduled/:id', async (request, reply) => {
|
||||
if (requireAdmin(request, reply)) return;
|
||||
return { deleted: deleteScheduledPost(Number(request.params.id)) };
|
||||
});
|
||||
|
||||
// Composer (Admin): Nachricht/Embed als Bot senden oder eigene Posts bearbeiten
|
||||
app.post('/api/compose', async (request, reply) => {
|
||||
if (requireAdmin(request, reply)) return;
|
||||
|
||||
Reference in New Issue
Block a user