import { PermissionFlagsBits } from 'discord.js'; import { t, tf } from '@nexumi/shared'; import type { SlashCommand } from '../../types.js'; import { getGuildLocale } from '../../i18n.js'; import { requirePermission } from '../../permissions.js'; import { birthdayCommandData } from './command-definitions.js'; import { BirthdaySetSchema, daysUntilBirthday, getBirthdayConfig, isValidTimezone, listBirthdays, removeBirthday, setBirthday, updateBirthdayConfig } from './service.js'; const birthdayCommand: SlashCommand = { data: birthdayCommandData, async execute(interaction, context) { const locale = await getGuildLocale(context.prisma, interaction.guildId); if (!interaction.inGuild()) { await interaction.reply({ content: t(locale, 'generic.guildOnly'), ephemeral: true }); return; } const guildId = interaction.guildId!; const sub = interaction.options.getSubcommand(); if (sub === 'setup') { if (!(await requirePermission(interaction, PermissionFlagsBits.ManageGuild, locale))) { return; } const channel = interaction.options.getChannel('channel', true); const role = interaction.options.getRole('role'); const timezone = interaction.options.getString('timezone') ?? 'Europe/Berlin'; const message = interaction.options.getString('message'); if (!isValidTimezone(timezone)) { await interaction.reply({ content: t(locale, 'birthdays.error.invalidTimezone'), ephemeral: true }); return; } await updateBirthdayConfig(context.prisma, guildId, { enabled: true, channelId: channel.id, roleId: role?.id ?? null, message: message ?? null, timezone }); await interaction.reply({ content: t(locale, 'birthdays.setup.done'), ephemeral: true }); return; } const config = await getBirthdayConfig(context.prisma, guildId); if (sub === 'set') { const month = interaction.options.getInteger('month', true); const day = interaction.options.getInteger('day', true); const year = interaction.options.getInteger('year') ?? undefined; const parsed = BirthdaySetSchema.safeParse({ month, day, year }); if (!parsed.success) { await interaction.reply({ content: t(locale, 'birthdays.error.invalidDate'), ephemeral: true }); return; } try { await setBirthday(context.prisma, guildId, interaction.user.id, parsed.data); } catch (error) { if (error instanceof Error && error.message === 'invalid_date') { await interaction.reply({ content: t(locale, 'birthdays.error.invalidDate'), ephemeral: true }); return; } throw error; } await interaction.reply({ content: tf(locale, 'birthdays.set.success', { month, day, year: year ?? '—' }), ephemeral: true }); return; } if (sub === 'remove') { const removed = await removeBirthday(context.prisma, guildId, interaction.user.id); if (!removed) { await interaction.reply({ content: t(locale, 'birthdays.remove.none'), ephemeral: true }); return; } await interaction.reply({ content: t(locale, 'birthdays.remove.success'), ephemeral: true }); return; } if (sub === 'list' || sub === 'next') { const entries = await listBirthdays(context.prisma, guildId, config.timezone); if (entries.length === 0) { await interaction.reply({ content: t(locale, 'birthdays.list.empty'), ephemeral: true }); return; } const slice = sub === 'next' ? entries.slice(0, 10) : entries.slice(0, 25); const lines = slice.map((entry, index) => { const days = daysUntilBirthday(entry.month, entry.day, config.timezone); const dateLabel = `${String(entry.day).padStart(2, '0')}.${String(entry.month).padStart(2, '0')}`; const yearLabel = entry.year ? ` (${entry.year})` : ''; return tf(locale, 'birthdays.list.line', { rank: index + 1, user: `<@${entry.userId}>`, date: `${dateLabel}${yearLabel}`, days }); }); const header = t(locale, sub === 'next' ? 'birthdays.next.header' : 'birthdays.list.header'); await interaction.reply({ content: `${header}\n${lines.join('\n')}`, ephemeral: true }); } } }; export const birthdayCommands: SlashCommand[] = [birthdayCommand];