Add giveaway, ticket, self-role, starboard, and suggestion features to the bot

- Introduced new models in the Prisma schema for giveaways, tickets, self-role panels, starboard configurations, and suggestions, enhancing the bot's functionality.
- Implemented job handling for self-role expiration and ticket management, improving user experience.
- Updated command localization to support new features in both German and English.
- Enhanced the job processing system to include dedicated workers for managing tickets and self-role expirations.
- Documented the new features and their setup processes in PHASE-TRACKING.md for clarity on implementation steps.
This commit is contained in:
smueller
2026-07-22 13:08:37 +02:00
parent f2f856628d
commit a583db7a15
50 changed files with 7820 additions and 4 deletions

View File

@@ -0,0 +1,201 @@
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 { voiceCommandData } from './command-definitions.js';
import {
buildControlPanelEmbed,
buildControlPanelRows,
claimTempChannel,
fetchCategory,
getTempVoiceChannelForMember,
getTempVoiceConfig,
hideTempChannel,
kickFromTempChannel,
lockTempChannel,
renameTempChannel,
setTempChannelLimit,
transferTempChannelOwnership,
updateTempVoiceConfig
} from './service.js';
async function requireOwnedTempChannel(
interaction: Parameters<SlashCommand['execute']>[0],
context: Parameters<SlashCommand['execute']>[1],
locale: 'de' | 'en'
) {
if (!interaction.inGuild()) {
await interaction.reply({ content: t(locale, 'generic.guildOnly'), ephemeral: true });
return null;
}
const guildMember =
interaction.guild!.members.cache.get(interaction.user.id) ??
(await interaction.guild!.members.fetch(interaction.user.id));
const owned = await getTempVoiceChannelForMember(context.prisma, interaction.guildId!, guildMember);
if (!owned || owned.record.ownerId !== interaction.user.id) {
await interaction.reply({ content: t(locale, 'tempvoice.error.notOwner'), ephemeral: true });
return null;
}
return owned;
}
const voiceCommand: SlashCommand = {
data: voiceCommandData,
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 hub = interaction.options.getChannel('hub_channel', true);
const category = interaction.options.getChannel('category', true);
const defaultName = interaction.options.getString('default_name');
const limit = interaction.options.getInteger('limit');
const categoryChannel = await fetchCategory(interaction.guild!, category.id);
if (!categoryChannel) {
await interaction.reply({ content: t(locale, 'tempvoice.error.invalidCategory'), ephemeral: true });
return;
}
await updateTempVoiceConfig(context.prisma, guildId, {
enabled: true,
hubChannelId: hub.id,
categoryId: category.id,
defaultName: defaultName ?? undefined,
defaultLimit: limit ?? undefined
});
await interaction.reply({ content: t(locale, 'tempvoice.setup.done'), ephemeral: true });
return;
}
const config = await getTempVoiceConfig(context.prisma, guildId);
if (!config.enabled) {
await interaction.reply({ content: t(locale, 'tempvoice.error.disabled'), ephemeral: true });
return;
}
if (sub === 'panel') {
const owned = await requireOwnedTempChannel(interaction, context, locale);
if (!owned) {
return;
}
await interaction.reply({
embeds: [buildControlPanelEmbed(owned.channel.name, locale)],
components: buildControlPanelRows(owned.channel.id, locale),
ephemeral: true
});
return;
}
if (sub === 'claim') {
const member = await interaction.guild!.members.fetch(interaction.user.id);
const inChannel = await getTempVoiceChannelForMember(context.prisma, guildId, member);
if (!inChannel) {
await interaction.reply({ content: t(locale, 'tempvoice.error.notInChannel'), ephemeral: true });
return;
}
const result = await claimTempChannel(context, inChannel.record, inChannel.channel, member, locale);
if (result === 'owner_present') {
await interaction.reply({ content: t(locale, 'tempvoice.claim.ownerPresent'), ephemeral: true });
return;
}
if (result === 'not_in_channel') {
await interaction.reply({ content: t(locale, 'tempvoice.error.notInChannel'), ephemeral: true });
return;
}
await interaction.reply({ content: t(locale, 'tempvoice.claim.success'), ephemeral: true });
return;
}
const owned = await requireOwnedTempChannel(interaction, context, locale);
if (!owned) {
return;
}
if (sub === 'rename') {
const name = interaction.options.getString('name', true);
await renameTempChannel(owned.channel, name);
await interaction.reply({
content: tf(locale, 'tempvoice.rename.success', { name: name.slice(0, 100) }),
ephemeral: true
});
return;
}
if (sub === 'limit') {
const limit = interaction.options.getInteger('limit', true);
await setTempChannelLimit(owned.channel, limit);
await interaction.reply({
content: tf(locale, 'tempvoice.limit.success', { limit }),
ephemeral: true
});
return;
}
if (sub === 'lock') {
await lockTempChannel(owned.channel);
await interaction.reply({ content: t(locale, 'tempvoice.lock.success'), ephemeral: true });
return;
}
if (sub === 'hide') {
await hideTempChannel(owned.channel);
await interaction.reply({ content: t(locale, 'tempvoice.hide.success'), ephemeral: true });
return;
}
if (sub === 'kick') {
const user = interaction.options.getUser('user', true);
if (user.id === interaction.user.id) {
await interaction.reply({ content: t(locale, 'tempvoice.kick.self'), ephemeral: true });
return;
}
const ok = await kickFromTempChannel(owned.channel, user.id);
if (!ok) {
await interaction.reply({ content: t(locale, 'tempvoice.kick.notInChannel'), ephemeral: true });
return;
}
await interaction.reply({
content: tf(locale, 'tempvoice.kick.success', { user: `<@${user.id}>` }),
ephemeral: true
});
return;
}
if (sub === 'transfer') {
const user = interaction.options.getUser('user', true);
if (user.id === interaction.user.id) {
await interaction.reply({ content: t(locale, 'tempvoice.transfer.self'), ephemeral: true });
return;
}
const target = await interaction.guild!.members.fetch(user.id).catch(() => null);
if (!target || !owned.channel.members.has(target.id)) {
await interaction.reply({ content: t(locale, 'tempvoice.transfer.notInChannel'), ephemeral: true });
return;
}
await transferTempChannelOwnership(context, owned.record, owned.channel, target, locale);
await interaction.reply({
content: tf(locale, 'tempvoice.transfer.success', { user: `<@${user.id}>` }),
ephemeral: true
});
}
}
};
export const tempVoiceCommands: SlashCommand[] = [voiceCommand];