- 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.
185 lines
6.0 KiB
TypeScript
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];
|