Branding-Tab: Name/Farben/Bot-Status/Avatar/Banner + Button-Farben für Rollen-Menüs
- Alle Embeds nutzen jetzt brandName/brandColor/brandColor2/brandFooter aus den Settings (Codemod über 20 Module) — Farbe & Name serverweit per Klick änderbar - Bot-Status (Presence): Typ (Spielt/Schaut/Hört/Status) + Text, sofort angewendet; Server-Monitor nutzt die Presence nur noch, wenn kein eigener Status gesetzt ist - Avatar-/Banner-Upload direkt aufs Bot-Profil (Base64, 8-MB-Limit, Discord-Rate-Limit sauber gemeldet) - Env-Verlagerung: GITEA_API_TOKEN (write-only Setting) und DISCORD_GUILD_ID jetzt auch über den Brand-Tab pflegbar — weniger Redeploys - Rollen-Menüs: Button-Farbe pro Eintrag wählbar (Grau/Blau/Grün/Rot) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+79
-7
@@ -12,7 +12,7 @@ import { publishRoleMenu, unpublishRoleMenu, MAX_ENTRIES } from '../bot/role-men
|
||||
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';
|
||||
import { commitChannelId, devlogChannelId, releaseChannelId, devlogPingRoleId, publicUrl, roadmapRepo, brandColor, brandFooter, brandName, giteaApiToken } from '../runtime-settings.js';
|
||||
import { getMilestones } from '../gitea-api.js';
|
||||
import { getSessionUser, isAdmin } from './auth.js';
|
||||
|
||||
@@ -137,7 +137,7 @@ export function registerApiRoutes(app, client) {
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0">
|
||||
<channel>
|
||||
<title>D4RKST3R // DEVLOG</title>
|
||||
<title>${brandFooter('DEVLOG')}</title>
|
||||
<link>${publicUrl()}/devlogs</link>
|
||||
<description>Entwicklungs-Updates, automatisch archiviert.</description>
|
||||
<language>de</language>
|
||||
@@ -223,6 +223,13 @@ ${rssItems}
|
||||
ticket_channel_id: getSetting('ticket_channel_id') ?? '',
|
||||
gameservers: getSetting('gameservers') ?? '',
|
||||
autorole_id: getSetting('autorole_id') ?? '',
|
||||
brand_name: getSetting('brand_name') ?? 'D4RKST3R',
|
||||
brand_color: getSetting('brand_color') ?? '#f5c518',
|
||||
brand_color2: getSetting('brand_color2') ?? '#ff4d00',
|
||||
bot_status_type: getSetting('bot_status_type') ?? 'custom',
|
||||
bot_status_text: getSetting('bot_status_text') ?? '',
|
||||
discord_guild_id: getSetting('discord_guild_id') ?? '',
|
||||
gitea_api_token_set: Boolean(giteaApiToken()),
|
||||
sticky_roles_enabled: getSetting('sticky_roles_enabled') === '1',
|
||||
levels_enabled: getSetting('levels_enabled') === '1',
|
||||
levels_announce: getSetting('levels_announce') !== '0',
|
||||
@@ -242,7 +249,7 @@ ${rssItems}
|
||||
botTag: client.user?.tag ?? null,
|
||||
uptimeSeconds: Math.floor(process.uptime()),
|
||||
guilds: client.guilds.cache.size,
|
||||
giteaTokenConfigured: Boolean(config.giteaApiToken),
|
||||
giteaTokenConfigured: Boolean(giteaApiToken()),
|
||||
lastBackup: getSetting('last_backup'),
|
||||
...stats,
|
||||
},
|
||||
@@ -319,6 +326,42 @@ ${rssItems}
|
||||
if (body.level_rewards !== undefined) {
|
||||
setSetting('level_rewards', String(body.level_rewards).trim());
|
||||
}
|
||||
// Branding
|
||||
if (body.brand_name !== undefined) {
|
||||
setSetting('brand_name', String(body.brand_name).trim().slice(0, 40) || 'D4RKST3R');
|
||||
}
|
||||
for (const key of ['brand_color', 'brand_color2']) {
|
||||
if (body[key] === undefined) continue;
|
||||
const value = String(body[key]).trim();
|
||||
if (!/^#[0-9a-fA-F]{6}$/.test(value)) {
|
||||
return reply.code(400).send({ error: `${key}: Hex-Farbe wie #f5c518 erwartet` });
|
||||
}
|
||||
setSetting(key, value.toLowerCase());
|
||||
}
|
||||
// Bot-Status (Presence) — wird sofort angewendet
|
||||
let statusChanged = false;
|
||||
if (body.bot_status_type !== undefined) {
|
||||
if (!['playing', 'watching', 'listening', 'custom'].includes(body.bot_status_type)) {
|
||||
return reply.code(400).send({ error: 'bot_status_type ungültig' });
|
||||
}
|
||||
setSetting('bot_status_type', body.bot_status_type);
|
||||
statusChanged = true;
|
||||
}
|
||||
if (body.bot_status_text !== undefined) {
|
||||
setSetting('bot_status_text', String(body.bot_status_text).trim().slice(0, 120));
|
||||
statusChanged = true;
|
||||
}
|
||||
if (statusChanged) {
|
||||
const { applyBotStatus } = await import('../bot/presence.js');
|
||||
applyBotStatus(client);
|
||||
}
|
||||
// Aus der Env verlagerbar: Guild-ID (greift beim nächsten Start) + Gitea-Token (write-only)
|
||||
if (body.discord_guild_id !== undefined) {
|
||||
setSetting('discord_guild_id', String(body.discord_guild_id).trim());
|
||||
}
|
||||
if (body.gitea_api_token !== undefined && String(body.gitea_api_token).trim() !== '') {
|
||||
setSetting('gitea_api_token', String(body.gitea_api_token).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());
|
||||
@@ -353,6 +396,7 @@ ${rssItems}
|
||||
emoji: String(e.emoji ?? '').trim(),
|
||||
label: String(e.label ?? '').trim(),
|
||||
role_id: String(e.role_id ?? '').trim(),
|
||||
style: ['primary', 'secondary', 'success', 'danger'].includes(e.style) ? e.style : 'secondary',
|
||||
}))
|
||||
.filter((e) => e.label && e.role_id);
|
||||
if (entries.length > MAX_ENTRIES) {
|
||||
@@ -532,10 +576,10 @@ ${rssItems}
|
||||
content: null,
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xf5c518)
|
||||
.setColor(brandColor())
|
||||
.setTitle(String(embedTitle).slice(0, 200))
|
||||
.setDescription(text.slice(0, 4000))
|
||||
.setFooter({ text: 'D4RKST3R' })
|
||||
.setFooter({ text: brandName() })
|
||||
.setTimestamp(),
|
||||
],
|
||||
}
|
||||
@@ -557,6 +601,34 @@ ${rssItems}
|
||||
return { ok: true, edited: false, message_id: message.id };
|
||||
});
|
||||
|
||||
// Bot-Avatar/-Banner setzen (Admin) — Bild als Base64-Data-URL im Body
|
||||
app.post('/api/branding/:target', { bodyLimit: 12 * 1024 * 1024 }, async (request, reply) => {
|
||||
if (requireAdmin(request, reply)) return;
|
||||
|
||||
const target = request.params.target;
|
||||
if (!['avatar', 'banner'].includes(target)) {
|
||||
return reply.code(400).send({ error: 'target: avatar oder banner' });
|
||||
}
|
||||
const dataUrl = String(request.body?.data ?? '');
|
||||
const match = dataUrl.match(/^data:image\/(png|jpe?g|gif|webp);base64,(.+)$/s);
|
||||
if (!match) return reply.code(400).send({ error: 'data: Base64-Data-URL eines Bildes erwartet' });
|
||||
const buffer = Buffer.from(match[2], 'base64');
|
||||
if (buffer.length > 8 * 1024 * 1024) {
|
||||
return reply.code(400).send({ error: 'Bild zu groß (max 8 MB)' });
|
||||
}
|
||||
|
||||
try {
|
||||
if (target === 'avatar') await client.user.setAvatar(buffer);
|
||||
else await client.user.setBanner(buffer);
|
||||
request.log.info(`Bot-${target} aktualisiert (${(buffer.length / 1024).toFixed(0)} KB)`);
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
request.log.error({ err: error }, `Bot-${target} fehlgeschlagen`);
|
||||
// Discord rate-limitet Avatar-Änderungen hart (~2 pro 10 min)
|
||||
return reply.code(502).send({ error: `Discord lehnt ab: ${error.message?.slice(0, 120)}` });
|
||||
}
|
||||
});
|
||||
|
||||
// --- API-Keys (Admin) — für /api/v1/* ---
|
||||
|
||||
const VALID_SCOPES = ['message', 'dm', 'roles', 'read'];
|
||||
@@ -629,12 +701,12 @@ ${rssItems}
|
||||
await channel.send({
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xf5c518)
|
||||
.setColor(brandColor())
|
||||
.setTitle('🔧 Test')
|
||||
.setDescription(
|
||||
`Test-Nachricht für den **${{ commit: 'Commit', devlog: 'Devlog', release: 'Release' }[target]}-Kanal** — von der Settings-Seite ausgelöst.`
|
||||
)
|
||||
.setFooter({ text: 'D4RKST3R // SETUP' }),
|
||||
.setFooter({ text: brandFooter('SETUP') }),
|
||||
],
|
||||
});
|
||||
return { ok: true };
|
||||
|
||||
@@ -10,10 +10,9 @@ import { join } from 'node:path';
|
||||
import { ActionRowBuilder, AttachmentBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js';
|
||||
import { config } from '../config.js';
|
||||
import { saveDevlog } from '../db.js';
|
||||
import { devlogChannelId, devlogPingRoleId, devlogThreadsEnabled, publicUrl } from '../runtime-settings.js';
|
||||
import { devlogChannelId, devlogPingRoleId, devlogThreadsEnabled, publicUrl, brandColor, brandFooter } from '../runtime-settings.js';
|
||||
import { imagesDir } from '../bot/devlog-archive.js';
|
||||
|
||||
const BRAND_YELLOW = 0xf5c518;
|
||||
const MAX_IMAGES = 4;
|
||||
|
||||
/** Secret timing-safe vergleichen */
|
||||
@@ -98,12 +97,12 @@ export function registerDevlogEndpoint(app, client) {
|
||||
(f, i) => new AttachmentBuilder(f.buffer, { name: `devlog_${i}.${f.name.split('.').pop().toLowerCase()}` })
|
||||
);
|
||||
const mainEmbed = new EmbedBuilder()
|
||||
.setColor(BRAND_YELLOW)
|
||||
.setColor(brandColor())
|
||||
.setTitle(`📔 Devlog — ${dateStr}`)
|
||||
.setURL(groupUrl)
|
||||
.setDescription(prose)
|
||||
.setFooter({
|
||||
text: `D4RKST3R // DEVLOG${commitCount ? ` • ${commitCount} Commits` : ''}`,
|
||||
text: `${brandFooter('DEVLOG')}${commitCount ? ` • ${commitCount} Commits` : ''}`,
|
||||
})
|
||||
.setTimestamp();
|
||||
if (project) {
|
||||
@@ -180,7 +179,7 @@ export function registerDevlogEndpoint(app, client) {
|
||||
message_id: message.id,
|
||||
channel_id: message.channelId,
|
||||
content: `${title}\n\n${description}`,
|
||||
author_name: 'D4RKST3R // DEVLOG',
|
||||
author_name: brandFooter('DEVLOG'),
|
||||
posted_at: message.createdAt.toISOString(),
|
||||
images: JSON.stringify(imageFiles),
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// Zeiten sind lokale Serverzeit (TZ im Compose = Europe/Berlin), gespeichert als UTC.
|
||||
import { EmbedBuilder } from 'discord.js';
|
||||
import { dueScheduledPosts, markScheduledRun } from '../db.js';
|
||||
import { brandColor, brandName } from '../runtime-settings.js';
|
||||
|
||||
const CHECK_INTERVAL_MS = 60 * 1000;
|
||||
|
||||
@@ -47,10 +48,10 @@ export function buildScheduledPayload(post) {
|
||||
return {
|
||||
embeds: [
|
||||
new EmbedBuilder()
|
||||
.setColor(0xf5c518)
|
||||
.setColor(brandColor())
|
||||
.setTitle(post.embed_title.slice(0, 200))
|
||||
.setDescription(post.content.slice(0, 4000))
|
||||
.setFooter({ text: 'D4RKST3R' })
|
||||
.setFooter({ text: brandName() })
|
||||
.setTimestamp(),
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user