- Introduced new models in the Prisma schema for statistics, social feeds, scheduled messages, and guild backups, enhancing the bot's functionality. - Implemented command modules for managing stats channels, feeds, scheduling messages, and creating/restoring backups, improving user engagement and server management. - Updated job handling to include dedicated workers for stats updates, feed polling, scheduling, and backup restoration, enhancing automation. - Enhanced command localization for new features in both German and English. - Documented the new features and their setup processes in PHASE-TRACKING.md for clarity on implementation steps.
198 lines
6.8 KiB
TypeScript
198 lines
6.8 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 { voiceCommandData } from './command-definitions.js';
|
|
import {
|
|
claimTempChannel,
|
|
fetchCategory,
|
|
getTempVoiceChannelForMember,
|
|
getTempVoiceConfig,
|
|
hideTempChannel,
|
|
kickFromTempChannel,
|
|
lockTempChannel,
|
|
renameTempChannel,
|
|
sendControlPanelToChannel,
|
|
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 sendControlPanelToChannel(owned.channel, locale);
|
|
await interaction.reply({ content: t(locale, 'tempvoice.panel.sent'), 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];
|