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:
2026-07-30 00:40:08 +02:00
co-authored by Claude Fable 5
parent db9c7d07c9
commit f7b261c648
27 changed files with 395 additions and 93 deletions
+9 -6
View File
@@ -1,11 +1,12 @@
// Discord-Client: Commands laden, registrieren und Interactions verarbeiten
import { Client, Collection, Events, GatewayIntentBits, MessageFlags, Partials, REST, Routes } from 'discord.js';
import { config } from '../config.js';
import { devlogChannelId, devlogPingRoleId, playtesterRoleId, ticketChannelId } from '../runtime-settings.js';
import { devlogChannelId, devlogPingRoleId, playtesterRoleId, ticketChannelId, brandColor, brandFooter, discordGuildId } from '../runtime-settings.js';
import { archiveDevlogMessage, removeDevlog } from './devlog-archive.js';
import { registerCommunityListeners } from './community.js';
import { registerModTools } from './mod-tools.js';
import { registerLevels } from './levels.js';
import { registerPresence } from './presence.js';
import { handleRoleMenuButton } from './role-menus.js';
import {
addPlaytester, removePlaytester, isWish, bumpWish,
@@ -186,12 +187,12 @@ export async function startBot() {
content: `<@${interaction.user.id}>`,
embeds: [
new EmbedBuilder()
.setColor(0xf5c518)
.setColor(brandColor())
.setDescription(
'Willkommen in deinem Ticket! Beschreib dein Anliegen — das Team meldet sich.\n' +
'Wenn alles geklärt ist, kann das Ticket hier geschlossen werden.'
)
.setFooter({ text: 'D4RKST3R // SUPPORT' }),
.setFooter({ text: brandFooter('SUPPORT') }),
],
components: [
new ActionRowBuilder().addComponents(
@@ -296,6 +297,7 @@ export async function startBot() {
registerCommunityListeners(client);
registerModTools(client);
registerLevels(client);
registerPresence(client);
await client.login(config.discordToken);
return client;
@@ -306,12 +308,13 @@ async function registerCommands() {
const rest = new REST().setToken(config.discordToken);
const body = commandModules.map((c) => c.data.toJSON());
const route = config.discordGuildId
? Routes.applicationGuildCommands(config.discordClientId, config.discordGuildId)
const guildId = discordGuildId();
const route = guildId
? Routes.applicationGuildCommands(config.discordClientId, guildId)
: Routes.applicationCommands(config.discordClientId);
await rest.put(route, { body });
console.log(
`[bot] ${body.length} Slash-Command(s) registriert (${config.discordGuildId ? 'Guild' : 'global'})`
`[bot] ${body.length} Slash-Command(s) registriert (${guildId ? 'Guild' : 'global'})`
);
}
+3 -3
View File
@@ -1,7 +1,7 @@
// /bug — Bug-Report aus Discord: erstellt ein Gitea-Issue (inkl. Screenshot-Upload)
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
import { config } from '../../config.js';
import { bugReportRepo } from '../../runtime-settings.js';
import { bugReportRepo, giteaApiToken } from '../../runtime-settings.js';
import { createIssue, uploadIssueAsset } from '../../gitea-api.js';
import { saveBugReport } from '../../db.js';
@@ -24,7 +24,7 @@ export const data = new SlashCommandBuilder()
export async function execute(interaction) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
if (!config.giteaApiToken) {
if (!giteaApiToken()) {
await interaction.editReply('❌ Bug-Reports sind gerade nicht konfiguriert (kein Gitea-Token).');
return;
}
+3 -2
View File
@@ -4,6 +4,7 @@ import {
ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder,
} from 'discord.js';
import { saveGiveaway } from '../../db.js';
import { brandColor, brandFooter } from '../../runtime-settings.js';
/** "30m" / "2h" / "1d" → Millisekunden (null bei Unsinn) */
export function parseDuration(input) {
@@ -36,13 +37,13 @@ export async function execute(interaction) {
const endsAt = new Date(Date.now() + duration);
const embed = new EmbedBuilder()
.setColor(0xf5c518)
.setColor(brandColor())
.setTitle(`🎉 Giveaway: ${prize}`)
.setDescription(
`Klick auf den Button zum Teilnehmen!\n\n` +
`🏆 **${winners}** Gewinner · ⏰ Ziehung <t:${Math.floor(endsAt.getTime() / 1000)}:R>`
)
.setFooter({ text: 'D4RKST3R // GIVEAWAY' })
.setFooter({ text: brandFooter('GIVEAWAY') })
.setTimestamp(endsAt);
const row = new ActionRowBuilder().addComponents(
+3 -3
View File
@@ -3,7 +3,7 @@ import {
SlashCommandBuilder, PermissionFlagsBits, MessageFlags,
ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder,
} from 'discord.js';
import { playtesterRoleId } from '../../runtime-settings.js';
import { playtesterRoleId, brandColor, brandFooter } from '../../runtime-settings.js';
export const data = new SlashCommandBuilder()
.setName('playtester-setup')
@@ -20,7 +20,7 @@ export async function execute(interaction) {
}
const embed = new EmbedBuilder()
.setColor(0xf5c518)
.setColor(brandColor())
.setTitle('🧪 Playtester werden')
.setDescription(
'Du willst **EcoGame** vor allen anderen spielen und mit deinem Feedback ' +
@@ -29,7 +29,7 @@ export async function execute(interaction) {
'sobald die Alpha startet, bekommst du deinen Zugang direkt per DM. ' +
'Abmelden geht jederzeit mit einem zweiten Klick.'
)
.setFooter({ text: 'D4RKST3R // PLAYTEST' });
.setFooter({ text: brandFooter('PLAYTEST') });
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
+2 -2
View File
@@ -1,6 +1,6 @@
// /purge — Nachrichten im aktuellen Kanal massenlöschen (max 100, jünger als 14 Tage)
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, EmbedBuilder } from 'discord.js';
import { modlogChannelId } from '../../runtime-settings.js';
import { modlogChannelId, brandColor2 } from '../../runtime-settings.js';
export const data = new SlashCommandBuilder()
.setName('purge')
@@ -29,7 +29,7 @@ export async function execute(interaction) {
await channel?.send({
embeds: [
new EmbedBuilder()
.setColor(0xff4d00)
.setColor(brandColor2())
.setTitle('🧹 Purge')
.setDescription(
`**${deleted.size}** Nachrichten in <#${interaction.channelId}> gelöscht von **${interaction.user.username}**`
+3 -3
View File
@@ -2,7 +2,7 @@
import { SlashCommandBuilder, EmbedBuilder } from 'discord.js';
import { getLevelRow, rankOf } from '../../db.js';
import { xpForLevel } from '../levels.js';
import { levelsEnabled, publicUrl } from '../../runtime-settings.js';
import { levelsEnabled, publicUrl, brandColor, brandFooter } from '../../runtime-settings.js';
export const data = new SlashCommandBuilder()
.setName('rank')
@@ -32,12 +32,12 @@ export async function execute(interaction) {
await interaction.reply({
embeds: [
new EmbedBuilder()
.setColor(0xf5c518)
.setColor(brandColor())
.setAuthor({ name: user.username, iconURL: user.displayAvatarURL({ size: 64 }) })
.setTitle(`Level ${row.level} · Platz #${rankOf(user.id)}`)
.setDescription(`\`${bar}\` ${pct}%\n**${progress} / ${needed} XP** bis Level ${row.level + 1} · gesamt ${row.xp} XP`)
.setURL(`${publicUrl()}/level`)
.setFooter({ text: 'D4RKST3R // LEVEL' }),
.setFooter({ text: brandFooter('LEVEL') }),
],
});
}
+3 -3
View File
@@ -3,7 +3,7 @@ import {
SlashCommandBuilder, PermissionFlagsBits, MessageFlags,
ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder,
} from 'discord.js';
import { ticketChannelId } from '../../runtime-settings.js';
import { ticketChannelId, brandColor, brandFooter } from '../../runtime-settings.js';
export const data = new SlashCommandBuilder()
.setName('ticket-setup')
@@ -20,14 +20,14 @@ export async function execute(interaction) {
}
const embed = new EmbedBuilder()
.setColor(0xf5c518)
.setColor(brandColor())
.setTitle('🎫 Support-Ticket')
.setDescription(
'Du hast eine Frage, ein Problem oder ein privates Anliegen?\n\n' +
'Klick auf den Button — es öffnet sich ein **privater Thread**, ' +
'den nur du und das Team sehen können.'
)
.setFooter({ text: 'D4RKST3R // SUPPORT' });
.setFooter({ text: brandFooter('SUPPORT') });
const row = new ActionRowBuilder().addComponents(
new ButtonBuilder()
+2 -2
View File
@@ -1,7 +1,7 @@
// /timeout — Member stummschalten (Discord-Timeout) mit Mod-Log
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, EmbedBuilder } from 'discord.js';
import { parseDuration } from './giveaway.js';
import { modlogChannelId } from '../../runtime-settings.js';
import { modlogChannelId, brandColor2 } from '../../runtime-settings.js';
const MAX_TIMEOUT_MS = 28 * 86_400_000; // Discord-Limit
@@ -42,7 +42,7 @@ export async function execute(interaction) {
await channel?.send({
embeds: [
new EmbedBuilder()
.setColor(0xff4d00)
.setColor(brandColor2())
.setTitle('🔇 Timeout')
.addFields(
{ name: 'Member', value: `${user.username} (<@${user.id}>)`, inline: true },
+2 -2
View File
@@ -1,7 +1,7 @@
// /warn — Member verwarnen: DB-Eintrag, DM an den User, Mod-Log (Moderatoren)
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, EmbedBuilder } from 'discord.js';
import { addWarn, warnsOf } from '../../db.js';
import { modlogChannelId } from '../../runtime-settings.js';
import { modlogChannelId, brandColor2 } from '../../runtime-settings.js';
export const data = new SlashCommandBuilder()
.setName('warn')
@@ -34,7 +34,7 @@ export async function execute(interaction) {
await channel?.send({
embeds: [
new EmbedBuilder()
.setColor(0xff4d00)
.setColor(brandColor2())
.setTitle(`⚠️ Verwarnung #${count}`)
.addFields(
{ name: 'Member', value: `${user.username} (<@${user.id}>)`, inline: true },
+2 -1
View File
@@ -1,6 +1,7 @@
// /warns — Verwarnungs-Historie eines Members einsehen / Eintrag löschen (Moderatoren)
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, EmbedBuilder } from 'discord.js';
import { warnsOf, deleteWarn } from '../../db.js';
import { brandColor } from '../../runtime-settings.js';
export const data = new SlashCommandBuilder()
.setName('warns')
@@ -34,7 +35,7 @@ export async function execute(interaction) {
await interaction.reply({
embeds: [
new EmbedBuilder()
.setColor(0xf5c518)
.setColor(brandColor())
.setTitle(`⚠️ Verwarnungen: ${user.username} (${warns.length})`)
.setDescription(lines.join('\n'))
.setFooter({ text: 'Löschen: /warns member:<user> loeschen:<ID>' }),
+3 -3
View File
@@ -1,6 +1,6 @@
// /wunsch — Feature-Wunsch einreichen: Voting-Post mit 👍, Rangliste auf der Webseite
import { SlashCommandBuilder, MessageFlags, EmbedBuilder } from 'discord.js';
import { votingChannelId, publicUrl } from '../../runtime-settings.js';
import { votingChannelId, publicUrl, brandColor, brandFooter } from '../../runtime-settings.js';
import { saveWish } from '../../db.js';
export const data = new SlashCommandBuilder()
@@ -25,7 +25,7 @@ export async function execute(interaction) {
const channel = await interaction.client.channels.fetch(channelId);
const embed = new EmbedBuilder()
.setColor(0xf5c518)
.setColor(brandColor())
.setAuthor({
name: interaction.member?.displayName ?? interaction.user.username,
iconURL: interaction.user.displayAvatarURL({ size: 64 }),
@@ -33,7 +33,7 @@ export async function execute(interaction) {
.setTitle('💡 Feature-Wunsch')
.setDescription(idea)
.setURL(`${publicUrl()}/roadmap`)
.setFooter({ text: 'D4RKST3R // VOTING • 👍 = will ich!' });
.setFooter({ text: `${brandFooter('VOTING')} • 👍 = will ich!` });
const message = await channel.send({ embeds: [embed] });
await message.react('👍');
+2 -3
View File
@@ -7,10 +7,9 @@ import {
hasStarboard, saveStarboard,
saveGalleryItem, hasGalleryItem, deleteGalleryItem,
} from '../db.js';
import { starboardChannelId, starboardThreshold, screenshotChannelId } from '../runtime-settings.js';
import { starboardChannelId, starboardThreshold, screenshotChannelId, brandColor } from '../runtime-settings.js';
import { config } from '../config.js';
const BRAND_YELLOW = 0xf5c518;
const MAX_GALLERY_IMAGES = 4;
// Galerie-Bilder liegen neben der SQLite: data/gallery/
@@ -83,7 +82,7 @@ async function handleStarReaction(reaction) {
a.contentType?.startsWith('image/')
);
const embed = new EmbedBuilder()
.setColor(BRAND_YELLOW)
.setColor(brandColor())
.setAuthor({
name: message.member?.displayName ?? message.author?.username ?? 'unbekannt',
iconURL: message.author?.displayAvatarURL?.() ?? undefined,
+2 -1
View File
@@ -1,6 +1,7 @@
// Giveaway-Ziehung: Minuten-Scheduler beendet fällige Verlosungen automatisch
import { EmbedBuilder } from 'discord.js';
import { dueGiveaways, markGiveawayEnded, giveawayEntries } from '../db.js';
import { brandFooter } from '../runtime-settings.js';
const CHECK_INTERVAL_MS = 60 * 1000;
@@ -39,7 +40,7 @@ export async function giveawayTick(client) {
? `🏆 Gewonnen: ${mentions}\n👥 ${entries.length} Teilnahmen`
: 'Keine Teilnahmen — niemand hat gewonnen. 😢'
)
.setFooter({ text: 'D4RKST3R // GIVEAWAY' }),
.setFooter({ text: brandFooter('GIVEAWAY') }),
],
components: [],
}).catch(() => {});
+9 -14
View File
@@ -1,13 +1,8 @@
// Moderation & Kontakt: Modmail (DM ↔ Staff-Thread), Willkommens-Embed, Mod-Log
import { ChannelType, EmbedBuilder, Events } from 'discord.js';
import { saveModmail, modmailByUser, modmailByThread, saveStickyRoles, stickyRolesOf } from '../db.js';
import {
modmailChannelId, welcomeChannelId, modlogChannelId, publicUrl,
autoroleId, stickyRolesEnabled,
} from '../runtime-settings.js';
import { modmailChannelId, welcomeChannelId, modlogChannelId, publicUrl, autoroleId, stickyRolesEnabled, brandColor, brandColor2, brandFooter } from '../runtime-settings.js';
const BRAND_YELLOW = 0xf5c518;
const BRAND_ORANGE = 0xff4d00;
/* ── Modmail ───────────────────────────────────────── */
@@ -36,12 +31,12 @@ async function ensureModmailThread(client, user) {
await thread.send({
embeds: [
new EmbedBuilder()
.setColor(BRAND_YELLOW)
.setColor(brandColor())
.setDescription(
`Neue Modmail-Konversation mit **${user.username}** (<@${user.id}>).\n` +
'Antworten in diesem Thread gehen als DM an den User.'
)
.setFooter({ text: 'D4RKST3R // MODMAIL' }),
.setFooter({ text: brandFooter('MODMAIL') }),
],
});
return thread;
@@ -89,7 +84,7 @@ async function handleMemberAdd(member) {
if (!channel?.isTextBased()) return;
const embed = new EmbedBuilder()
.setColor(BRAND_YELLOW)
.setColor(brandColor())
.setTitle(`👋 Willkommen, ${member.displayName}!`)
.setThumbnail(member.user.displayAvatarURL({ size: 128 }))
.setDescription(
@@ -98,7 +93,7 @@ async function handleMemberAdd(member) {
'🧪 Playtester werden? Schau nach dem Bewerbungs-Post!\n' +
'🐛 Bug gefunden? Einfach `/bug` benutzen.'
)
.setFooter({ text: 'D4RKST3R // COMMUNITY' })
.setFooter({ text: brandFooter('COMMUNITY') })
.setTimestamp();
await channel.send({ content: `<@${member.id}>`, embeds: [embed] });
@@ -183,7 +178,7 @@ export function registerModTools(client) {
if (roleIds.length > 0) saveStickyRoles(member.id, roleIds);
await logToModlog(client, new EmbedBuilder()
.setColor(BRAND_ORANGE)
.setColor(brandColor2())
.setTitle('📤 Member gegangen')
.setDescription(`**${member.user?.username ?? member.id}**${roleIds.length ? ` — Rollen gesichert (${roleIds.length})` : ''}`)
.setTimestamp());
@@ -211,7 +206,7 @@ export function registerModTools(client) {
if (changes.length === 0) return;
await logToModlog(client, new EmbedBuilder()
.setColor(BRAND_YELLOW)
.setColor(brandColor())
.setTitle('👤 Member geändert')
.setDescription(`**${newMember.user?.username ?? newMember.id}**`)
.addFields(changes)
@@ -227,7 +222,7 @@ export function registerModTools(client) {
if (!message.guildId || message.author?.bot) return;
if (message.channelId === modlogChannelId()) return;
await logToModlog(client, new EmbedBuilder()
.setColor(BRAND_ORANGE)
.setColor(brandColor2())
.setTitle('🗑️ Nachricht gelöscht')
.addFields(
{ name: 'User', value: message.author ? `${message.author.username}` : 'unbekannt', inline: true },
@@ -245,7 +240,7 @@ export function registerModTools(client) {
if (!newMessage.guildId || newMessage.author?.bot) return;
if (oldMessage.content === newMessage.content) return; // z. B. nur Embed-Update
await logToModlog(client, new EmbedBuilder()
.setColor(BRAND_YELLOW)
.setColor(brandColor())
.setTitle('✏️ Nachricht bearbeitet')
.addFields(
{ name: 'User', value: newMessage.author?.username ?? 'unbekannt', inline: true },
+29
View File
@@ -0,0 +1,29 @@
// Bot-Status (Presence) aus den Branding-Settings — hat Vorrang vor dem
// Server-Monitor (der die Presence nur nutzt, wenn hier nichts gesetzt ist).
import { ActivityType, Events } from 'discord.js';
import { botStatusText, botStatusType } from '../runtime-settings.js';
const TYPES = {
playing: ActivityType.Playing,
watching: ActivityType.Watching,
listening: ActivityType.Listening,
custom: ActivityType.Custom,
};
/** Presence aus den Settings anwenden (leerer Text = Presence löschen) */
export function applyBotStatus(client) {
const text = botStatusText();
try {
if (!text) {
client.user?.setPresence?.({ activities: [] });
return;
}
client.user?.setActivity?.(text, { type: TYPES[botStatusType()] ?? ActivityType.Custom });
} catch (error) {
console.error('[presence] Status setzen fehlgeschlagen:', error.message);
}
}
export function registerPresence(client) {
client.once(Events.ClientReady, () => applyBotStatus(client));
}
+3 -4
View File
@@ -1,9 +1,8 @@
// Release-Ankündigungen: großes Embed in den konfigurierten Kanal (Setup-Seite)
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js';
import { releaseChannelId, publicUrl } from '../runtime-settings.js';
import { releaseChannelId, publicUrl, brandColor2, brandFooter } from '../runtime-settings.js';
import { config } from '../config.js';
const BRAND_ORANGE = 0xff4d00;
/**
* Release ankündigen (falls ein Kanal konfiguriert ist).
@@ -26,12 +25,12 @@ export async function postReleaseEmbed(client, release) {
const title = release.name?.trim() || release.tag;
const embed = new EmbedBuilder()
.setColor(BRAND_ORANGE)
.setColor(brandColor2())
.setTitle(`🚀 ${title}${release.prerelease ? ' (Pre-Release)' : ''}`)
.setDescription(body || '*Keine Release-Notes.*')
.addFields({ name: 'Version', value: `\`${release.tag}\``, inline: true },
{ name: 'Projekt', value: release.repo, inline: true })
.setFooter({ text: 'D4RKST3R // RELEASE' })
.setFooter({ text: brandFooter('RELEASE') })
.setTimestamp();
const buttons = new ActionRowBuilder().addComponents(
+12 -4
View File
@@ -3,16 +3,24 @@
// pro Menü, z. B. für Farb- oder Plattform-Rollen). Verwaltet über die /rollen-Seite.
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder, MessageFlags } from 'discord.js';
import { getRoleMenu, setRoleMenuMessage } from '../db.js';
import { brandColor, brandFooter } from '../runtime-settings.js';
const BRAND_YELLOW = 0xf5c518;
export const MAX_ENTRIES = 25; // Discord: 5 Button-Reihen à 5 Buttons
// Discord-Button-Farben (pro Menü-Eintrag wählbar)
export const BUTTON_STYLES = {
secondary: ButtonStyle.Secondary, // grau
primary: ButtonStyle.Primary, // blau
success: ButtonStyle.Success, // grün
danger: ButtonStyle.Danger, // rot
};
/** Embed + Button-Reihen aus einem Menü-Datensatz bauen */
function buildMenuMessage(menu) {
const entries = JSON.parse(menu.entries || '[]');
const embed = new EmbedBuilder()
.setColor(BRAND_YELLOW)
.setColor(brandColor())
.setTitle(menu.title)
.setDescription(
(menu.description?.trim() ? `${menu.description.trim()}\n\n` : '') +
@@ -20,7 +28,7 @@ function buildMenuMessage(menu) {
? '*Klick = Rolle nehmen — es ist immer nur eine Rolle aus diesem Menü aktiv.*'
: '*Klick = Rolle nehmen, nochmal klicken = wieder abgeben.*')
)
.setFooter({ text: 'D4RKST3R // ROLLEN' });
.setFooter({ text: brandFooter('ROLLEN') });
const rows = [];
for (let i = 0; i < entries.length; i += 5) {
@@ -29,7 +37,7 @@ function buildMenuMessage(menu) {
entries.slice(i, i + 5).map((e) => {
const button = new ButtonBuilder()
.setCustomId(`rolemenu:${menu.id}:${e.role_id}`)
.setStyle(ButtonStyle.Secondary)
.setStyle(BUTTON_STYLES[e.style] ?? ButtonStyle.Secondary)
.setLabel(e.label.slice(0, 80));
if (e.emoji) button.setEmoji(e.emoji);
return button;
+12 -10
View File
@@ -3,10 +3,10 @@
// Setting "gameservers": kommagetrennt "Name=http://ip:30120, Name2=…"
import { ActivityType, EmbedBuilder, Events } from 'discord.js';
import { getSetting, setSetting } from '../db.js';
import { brandColor, brandFooter, botStatusText } from '../runtime-settings.js';
const CHECK_INTERVAL_MS = 2 * 60 * 1000;
const TIMEOUT_MS = 8000;
const BRAND_YELLOW = 0xf5c518;
function serverList() {
return (getSetting('gameservers') ?? '')
@@ -48,10 +48,10 @@ function buildEmbed(results) {
: `🔴 **${r.name}** — offline`
);
return new EmbedBuilder()
.setColor(BRAND_YELLOW)
.setColor(brandColor())
.setTitle('🎮 Server-Status')
.setDescription(lines.join('\n'))
.setFooter({ text: 'D4RKST3R // STATUS • aktualisiert alle 2 min' })
.setFooter({ text: `${brandFooter('STATUS')} • aktualisiert alle 2 min` })
.setTimestamp();
}
@@ -63,15 +63,17 @@ export async function monitorTick(client) {
const results = await Promise.all(servers.map(queryServer));
// Bot-Presence: Gesamtspielerzahl (nur wenn mindestens ein Server online)
// Bot-Presence: Gesamtspielerzahl nur wenn kein eigener Status (Brand-Tab) gesetzt ist
const online = results.filter((r) => r.online);
const totalPlayers = online.reduce((sum, r) => sum + r.players, 0);
try {
client.user?.setActivity?.(
online.length > 0 ? `${totalPlayers} Spieler online` : 'Server offline',
{ type: ActivityType.Watching }
);
} catch { /* Presence ist nice-to-have */ }
if (!botStatusText()) {
try {
client.user?.setActivity?.(
online.length > 0 ? `${totalPlayers} Spieler online` : 'Server offline',
{ type: ActivityType.Watching }
);
} catch { /* Presence ist nice-to-have */ }
}
// Persistentes Embed im Status-Kanal pflegen (einmal posten, danach editieren)
if (!channelId) return;
+3 -4
View File
@@ -2,10 +2,9 @@
// eine Zusammenfassung der Woche in den Devlog-Kanal.
import { EmbedBuilder } from 'discord.js';
import { weeklyStats, getSetting, setSetting } from '../db.js';
import { devlogChannelId, publicUrl } from '../runtime-settings.js';
import { devlogChannelId, publicUrl, brandColor, brandFooter } from '../runtime-settings.js';
import { config } from '../config.js';
const BRAND_YELLOW = 0xf5c518;
const CHECK_INTERVAL_MS = 5 * 60 * 1000;
/** Balken-Zeile: `Mo 21.07 ████████ 12` */
@@ -46,7 +45,7 @@ export async function postWeeklyRecap(client) {
.join(' · ');
const embed = new EmbedBuilder()
.setColor(BRAND_YELLOW)
.setColor(brandColor())
.setTitle('📊 Wochen-Rückblick')
.setURL(`${publicUrl()}/devlogs`)
.setDescription(
@@ -57,7 +56,7 @@ export async function postWeeklyRecap(client) {
{ name: 'Devlogs', value: String(stats.devlogs), inline: true },
...(repoLine ? [{ name: 'Aktive Projekte', value: repoLine, inline: false }] : [])
)
.setFooter({ text: 'D4RKST3R // WEEKLY' })
.setFooter({ text: brandFooter('WEEKLY') })
.setTimestamp();
await channel.send({ embeds: [embed] });