Files
Nexumi/apps/bot/src/modules/giveaways/commands.ts
TheOnlyMace 57cdf847f5 Implement overdue giveaway recovery and enhance giveaway command handling
- Added functionality to recover overdue giveaways during bot startup, ensuring that giveaways that have passed their end time are processed correctly.
- Introduced `cancelGiveawayJob` to safely remove scheduled jobs, preventing issues with locked jobs.
- Updated giveaway command handling to trim IDs and validate existence within the guild context, improving error handling and user feedback.
- Enhanced the role picker component to normalize search queries and handle errors more gracefully, improving user experience.
- Implemented caching strategies for guild roles to avoid stale or empty responses, ensuring more reliable data retrieval.
2026-07-24 21:49:20 +02:00

185 lines
6.0 KiB
TypeScript

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 { parseDuration } from '../moderation/duration.js';
import { giveawayCommandData } from './command-definitions.js';
import {
cancelGiveawayJob,
deleteGiveaway,
endGiveaway,
GiveawayError,
listActiveGiveaways,
pauseGiveaway,
rerollGiveaway,
startGiveaway
} from './service.js';
async function ensureManageGuild(
interaction: Parameters<SlashCommand['execute']>[0],
locale: 'de' | 'en'
): Promise<boolean> {
if (!interaction.inGuild()) {
await interaction.reply({ content: t(locale, 'generic.guildOnly'), ephemeral: true });
return false;
}
return requirePermission(interaction, PermissionFlagsBits.ManageGuild, locale);
}
const giveawayCommand: SlashCommand = {
data: giveawayCommandData,
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 sub = interaction.options.getSubcommand();
if (sub === 'start') {
if (!(await ensureManageGuild(interaction, locale))) {
return;
}
const durationInput = interaction.options.getString('duration', true);
let durationMs: number;
try {
durationMs = parseDuration(durationInput);
} catch {
await interaction.reply({
content: t(locale, 'giveaway.error.invalid_duration'),
ephemeral: true
});
return;
}
if (durationMs < 60_000) {
await interaction.reply({
content: t(locale, 'giveaway.error.duration_too_short'),
ephemeral: true
});
return;
}
const channel = interaction.channel;
if (!channel?.isTextBased() || channel.isDMBased()) {
await interaction.reply({
content: t(locale, 'giveaway.error.invalid_channel'),
ephemeral: true
});
return;
}
const giveaway = await startGiveaway(
context,
{
guildId: interaction.guildId!,
channelId: channel.id,
hostId: interaction.user.id,
prize: interaction.options.getString('prize', true),
winnerCount: interaction.options.getInteger('winners', true),
durationMs,
requiredRoleId: interaction.options.getRole('role')?.id,
requiredLevel: interaction.options.getInteger('level'),
requiredMemberDays: interaction.options.getInteger('member_days')
},
locale
);
await interaction.reply({
content: tf(locale, 'giveaway.started', { id: giveaway.id }),
ephemeral: true
});
return;
}
if (!(await ensureManageGuild(interaction, locale))) {
return;
}
try {
if (sub === 'end') {
const id = interaction.options.getString('id', true).trim();
const giveaway = await context.prisma.giveaway.findUnique({ where: { id } });
if (!giveaway || giveaway.guildId !== interaction.guildId) {
throw new GiveawayError('not_found');
}
await cancelGiveawayJob(id);
await endGiveaway(context, id);
await interaction.reply({ content: t(locale, 'giveaway.ended'), ephemeral: true });
return;
}
if (sub === 'reroll') {
const id = interaction.options.getString('id', true).trim();
const existing = await context.prisma.giveaway.findUnique({ where: { id } });
if (!existing || existing.guildId !== interaction.guildId) {
throw new GiveawayError('not_found');
}
const updated = await rerollGiveaway(context, id, locale);
await interaction.reply({
content: tf(locale, 'giveaway.rerolled', {
winners: updated.winners.map((w) => `<@${w}>`).join(', ') || '—'
}),
ephemeral: true
});
return;
}
if (sub === 'list') {
const active = await listActiveGiveaways(context, interaction.guildId!);
if (active.length === 0) {
await interaction.reply({ content: t(locale, 'giveaway.list.empty'), ephemeral: true });
return;
}
const lines = active.map(
(g) =>
`\`${g.id}\` — **${g.prize}** (${g.entrants.length} ${t(locale, 'giveaway.list.entrants')}, <t:${Math.floor(g.endsAt.getTime() / 1000)}:R>)`
);
await interaction.reply({
content: `${t(locale, 'giveaway.list.header')}\n${lines.join('\n')}`,
ephemeral: true
});
return;
}
if (sub === 'delete') {
const id = interaction.options.getString('id', true).trim();
const existing = await context.prisma.giveaway.findUnique({ where: { id } });
if (!existing || existing.guildId !== interaction.guildId) {
throw new GiveawayError('not_found');
}
await deleteGiveaway(context, id);
await interaction.reply({ content: t(locale, 'giveaway.deleted'), ephemeral: true });
return;
}
if (sub === 'pause') {
const id = interaction.options.getString('id', true).trim();
const existing = await context.prisma.giveaway.findUnique({ where: { id } });
if (!existing || existing.guildId !== interaction.guildId) {
throw new GiveawayError('not_found');
}
const updated = await pauseGiveaway(context, id, locale);
await interaction.reply({
content: t(locale, updated.paused ? 'giveaway.paused' : 'giveaway.resumed'),
ephemeral: true
});
}
} catch (error) {
if (error instanceof GiveawayError) {
await interaction.reply({
content: t(locale, `giveaway.error.${error.code}`),
ephemeral: true
});
return;
}
throw error;
}
}
};
export const giveawayCommands = [giveawayCommand];