Willkommens-Karten, Geburtstage, Devlog-Permalinks mit OG-Vorschau, Auto-Publish

- Willkommens-Karten: gerendertes PNG (SVG → sharp) mit Avatar im
  Neon-Ring, Willkommens-Schriftzug und Member-Nummer; Fallback aufs
  bisherige Embed wenn das Rendering scheitert; Dockerfile installiert
  fonts-dejavu-core für die Text-Darstellung
- Geburtstags-System: /geburtstag setzen|entfernen (birthdays-Tabelle),
  tägliche Runde ab 09:00 Europe/Berlin (Doppel-Post-Schutz über
  last_birthday_run), Gratulations-Embed + Tages-Rolle (wird am
  nächsten Morgen wieder abgeräumt); Kanal + Rolle im Community-Tab
- Devlog-Permalinks: /devlogs/:id als eigene Seite (GET /api/devlogs/:id),
  Link-Symbol an jeder Karte, Link-kopieren-Button; der Server injiziert
  Open-Graph-Tags ins SPA-HTML — Devlog-Links zeigen Titel, Anriss und
  Bild, alle anderen Seiten bekommen Default-Tags (auch die Startseite)
- Auto-Publish: maybeCrosspost() veröffentlicht Devlog-, Release-,
  Composer- und geplante Posts automatisch in Ankündigungs-Kanälen
- brandEmbed: leere Avatar-URL crasht nicht mehr die Footer-Validierung

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 22:43:15 +02:00
co-authored by Claude Fable 5
parent ef0fb066f7
commit 676fc45bb1
23 changed files with 1080 additions and 21 deletions
+43
View File
@@ -0,0 +1,43 @@
// /geburtstag — eintragen/entfernen; der Bot gratuliert morgens im Geburtstags-Kanal
import { SlashCommandBuilder, MessageFlags } from 'discord.js';
import { setBirthday, deleteBirthday } from '../../db.js';
import { birthdayChannelId } from '../../runtime-settings.js';
const DAYS_IN_MONTH = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
export const data = new SlashCommandBuilder()
.setName('geburtstag')
.setDescription('Geburtstag eintragen — der Bot gratuliert dir am großen Tag 🎂')
.addSubcommand((s) =>
s.setName('setzen').setDescription('Deinen Geburtstag eintragen (ohne Jahr)')
.addIntegerOption((o) => o.setName('tag').setDescription('Tag (131)').setRequired(true).setMinValue(1).setMaxValue(31))
.addIntegerOption((o) => o.setName('monat').setDescription('Monat (112)').setRequired(true).setMinValue(1).setMaxValue(12))
)
.addSubcommand((s) =>
s.setName('entfernen').setDescription('Deinen Geburtstag wieder austragen')
);
export async function execute(interaction) {
if (interaction.options.getSubcommand() === 'entfernen') {
const deleted = deleteBirthday(interaction.user.id);
await interaction.reply({
content: deleted ? '🗑️ Geburtstag ausgetragen.' : '❕ Es war kein Geburtstag eingetragen.',
flags: MessageFlags.Ephemeral,
});
return;
}
const day = interaction.options.getInteger('tag');
const month = interaction.options.getInteger('monat');
if (day > DAYS_IN_MONTH[month - 1]) {
await interaction.reply({ content: `❌ Der ${day}.${month}. existiert nicht.`, flags: MessageFlags.Ephemeral });
return;
}
setBirthday(interaction.user.id, interaction.user.username, day, month);
await interaction.reply({
content:
`🎂 Gemerkt: **${day}.${month}.** — ich gratuliere dir dann morgens` +
(birthdayChannelId() ? '!' : '. (Hinweis an die Admins: noch kein Geburtstags-Kanal gesetzt.)'),
flags: MessageFlags.Ephemeral,
});
}