Implement leveling and economy features with associated models and commands
- Added new models for leveling and economy functionalities in the Prisma schema, including LevelingConfig, MemberLevel, LevelReward, EconomyConfig, MemberEconomy, ShopItem, and InventoryItem. - Introduced commands for managing XP, economy transactions, and shop interactions, enhancing user engagement. - Updated job handling to include reminders and poll closing functionalities. - Enhanced localization support for new commands and features in both German and English. - Improved the bot's job processing capabilities with a dedicated reminder worker for scheduled tasks.
This commit is contained in:
438
apps/bot/src/modules/utility/commands.ts
Normal file
438
apps/bot/src/modules/utility/commands.ts
Normal file
@@ -0,0 +1,438 @@
|
||||
import {
|
||||
EmbedBuilder,
|
||||
PermissionFlagsBits,
|
||||
type ChatInputCommandInteraction,
|
||||
type GuildBasedChannel
|
||||
} 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 { showEmbedBuilderModal } from './buttons.js';
|
||||
import {
|
||||
afkCommandData,
|
||||
avatarCommandData,
|
||||
bannerCommandData,
|
||||
channelinfoCommandData,
|
||||
editsnipeCommandData,
|
||||
embedCommandData,
|
||||
emojiCommandData,
|
||||
pollCommandData,
|
||||
remindmeCommandData,
|
||||
remindersCommandData,
|
||||
roleinfoCommandData,
|
||||
serverinfoCommandData,
|
||||
snipeCommandData,
|
||||
stickerCommandData,
|
||||
timestampCommandData,
|
||||
userinfoCommandData
|
||||
} from './command-definitions.js';
|
||||
import { createPoll } from './poll.js';
|
||||
import { createReminder, deleteReminder, listReminders } from './reminders.js';
|
||||
import { setAfk } from './afk.js';
|
||||
import { getDeletedSnipe, getEditedSnipe } from './snipe.js';
|
||||
import {
|
||||
UtilityError,
|
||||
buildChannelInfoEmbed,
|
||||
buildRoleInfoEmbed,
|
||||
buildServerInfoEmbed,
|
||||
buildUserInfoEmbed,
|
||||
extractFirstCustomEmoji,
|
||||
formatDiscordTimestamps,
|
||||
parseTimestampInput
|
||||
} from './service.js';
|
||||
|
||||
async function ensureGuildCommand(
|
||||
interaction: ChatInputCommandInteraction,
|
||||
locale: 'de' | 'en'
|
||||
): Promise<boolean> {
|
||||
if (!interaction.inGuild()) {
|
||||
await interaction.reply({ content: t(locale, 'generic.guildOnly'), ephemeral: true });
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function handleUtilityError(
|
||||
interaction: ChatInputCommandInteraction,
|
||||
locale: 'de' | 'en',
|
||||
error: unknown
|
||||
): Promise<void> {
|
||||
if (error instanceof UtilityError) {
|
||||
await interaction.reply({
|
||||
content: t(locale, `utility.error.${error.code}` as 'utility.error.invalid_when'),
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const userinfoCommand: SlashCommand = {
|
||||
data: userinfoCommandData,
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
if (!(await ensureGuildCommand(interaction, locale))) return;
|
||||
|
||||
const user = interaction.options.getUser('user') ?? interaction.user;
|
||||
const member = await interaction.guild!.members.fetch(user.id).catch(() => null);
|
||||
const embed = buildUserInfoEmbed(user, member, locale);
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
};
|
||||
|
||||
const serverinfoCommand: SlashCommand = {
|
||||
data: serverinfoCommandData,
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
if (!(await ensureGuildCommand(interaction, locale))) return;
|
||||
const embed = buildServerInfoEmbed(interaction.guild!, locale);
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
};
|
||||
|
||||
const roleinfoCommand: SlashCommand = {
|
||||
data: roleinfoCommandData,
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
if (!(await ensureGuildCommand(interaction, locale))) return;
|
||||
const role = interaction.options.getRole('role', true);
|
||||
const guildRole =
|
||||
interaction.guild!.roles.cache.get(role.id) ?? (await interaction.guild!.roles.fetch(role.id));
|
||||
if (!guildRole) {
|
||||
await interaction.reply({ content: t(locale, 'utility.error.role_not_found'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
await interaction.reply({ embeds: [buildRoleInfoEmbed(guildRole, locale)] });
|
||||
}
|
||||
};
|
||||
|
||||
const channelinfoCommand: SlashCommand = {
|
||||
data: channelinfoCommandData,
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
if (!(await ensureGuildCommand(interaction, locale))) return;
|
||||
const channel = interaction.options.getChannel('channel') ?? interaction.channel;
|
||||
if (!channel || !('guild' in channel)) {
|
||||
await interaction.reply({ content: t(locale, 'utility.error.channel_not_found'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
await interaction.reply({ embeds: [buildChannelInfoEmbed(channel as GuildBasedChannel, locale)] });
|
||||
}
|
||||
};
|
||||
|
||||
const avatarCommand: SlashCommand = {
|
||||
data: avatarCommandData,
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
const user = interaction.options.getUser('user') ?? interaction.user;
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(tf(locale, 'utility.avatar.title', { user: user.tag }))
|
||||
.setImage(user.displayAvatarURL({ size: 512 }))
|
||||
.setColor(0x6366f1);
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
};
|
||||
|
||||
const bannerCommand: SlashCommand = {
|
||||
data: bannerCommandData,
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
const user = interaction.options.getUser('user') ?? interaction.user;
|
||||
const fetched = await context.client.users.fetch(user.id, { force: true });
|
||||
const banner = fetched.bannerURL({ size: 512 });
|
||||
if (!banner) {
|
||||
await interaction.reply({ content: t(locale, 'utility.banner.none'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(tf(locale, 'utility.banner.title', { user: fetched.tag }))
|
||||
.setImage(banner)
|
||||
.setColor(0x6366f1);
|
||||
await interaction.reply({ embeds: [embed] });
|
||||
}
|
||||
};
|
||||
|
||||
const pollCommand: SlashCommand = {
|
||||
data: pollCommandData,
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
if (!(await ensureGuildCommand(interaction, locale))) return;
|
||||
|
||||
const question = interaction.options.getString('question', true);
|
||||
const optionsInput = interaction.options.getString('options', true);
|
||||
const multiSelect = interaction.options.getBoolean('multiselect') ?? false;
|
||||
const anonymous = interaction.options.getBoolean('anonymous') ?? false;
|
||||
const duration = interaction.options.getString('duration');
|
||||
|
||||
try {
|
||||
await createPoll(
|
||||
context,
|
||||
{
|
||||
guildId: interaction.guildId!,
|
||||
channelId: interaction.channelId!,
|
||||
creatorId: interaction.user.id,
|
||||
question,
|
||||
optionsInput,
|
||||
multiSelect,
|
||||
anonymous,
|
||||
durationInput: duration
|
||||
},
|
||||
locale
|
||||
);
|
||||
await interaction.reply({ content: t(locale, 'utility.poll.created'), ephemeral: true });
|
||||
} catch (error) {
|
||||
await handleUtilityError(interaction, locale, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const remindmeCommand: SlashCommand = {
|
||||
data: remindmeCommandData,
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
if (!(await ensureGuildCommand(interaction, locale))) return;
|
||||
|
||||
const whenInput = interaction.options.getString('when', true);
|
||||
const content = interaction.options.getString('content', true);
|
||||
const recurringCron = interaction.options.getString('recurring_cron');
|
||||
|
||||
try {
|
||||
const id = await createReminder(context, {
|
||||
guildId: interaction.guildId,
|
||||
userId: interaction.user.id,
|
||||
channelId: interaction.channelId!,
|
||||
content,
|
||||
whenInput,
|
||||
recurringCron
|
||||
});
|
||||
await interaction.reply({
|
||||
content: tf(locale, 'utility.remindme.created', { id: id.slice(0, 8) }),
|
||||
ephemeral: true
|
||||
});
|
||||
} catch (error) {
|
||||
await handleUtilityError(interaction, locale, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const remindersCommand: SlashCommand = {
|
||||
data: remindersCommandData,
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
const sub = interaction.options.getSubcommand();
|
||||
|
||||
if (sub === 'list') {
|
||||
const content = await listReminders(context, interaction.user.id, locale);
|
||||
await interaction.reply({ content, ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const id = interaction.options.getString('id', true);
|
||||
const deleted = await deleteReminder(context, interaction.user.id, id);
|
||||
await interaction.reply({
|
||||
content: t(locale, deleted ? 'utility.reminders.deleted' : 'utility.reminders.notFound'),
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const afkCommand: SlashCommand = {
|
||||
data: afkCommandData,
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
if (!(await ensureGuildCommand(interaction, locale))) return;
|
||||
|
||||
const reason = interaction.options.getString('reason');
|
||||
await setAfk(context, interaction.guildId!, interaction.user.id, reason);
|
||||
await interaction.reply({
|
||||
content: tf(locale, 'utility.afk.set', { reason: reason ?? '—' }),
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const emojiCommand: SlashCommand = {
|
||||
data: emojiCommandData,
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
if (!(await ensureGuildCommand(interaction, locale))) return;
|
||||
if (!(await requirePermission(interaction, PermissionFlagsBits.ManageGuildExpressions, locale))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sub = interaction.options.getSubcommand();
|
||||
|
||||
if (sub === 'add') {
|
||||
const name = interaction.options.getString('name', true);
|
||||
const attachment = interaction.options.getAttachment('attachment');
|
||||
const url = interaction.options.getString('url') ?? attachment?.url;
|
||||
if (!url) {
|
||||
await interaction.reply({ content: t(locale, 'utility.emoji.noSource'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
const emoji = await interaction.guild!.emojis.create({ attachment: url, name });
|
||||
await interaction.reply({
|
||||
content: tf(locale, 'utility.emoji.added', { emoji: emoji.toString() }),
|
||||
ephemeral: true
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === 'remove') {
|
||||
const input = interaction.options.getString('emoji', true);
|
||||
const match = input.match(/:(\d+)>$/) ?? input.match(/^(\d+)$/);
|
||||
const emojiId = match?.[1] ?? input;
|
||||
const emoji = interaction.guild!.emojis.cache.get(emojiId);
|
||||
if (!emoji) {
|
||||
await interaction.reply({ content: t(locale, 'utility.emoji.notFound'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
await emoji.delete();
|
||||
await interaction.reply({ content: t(locale, 'utility.emoji.removed'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const messageId = interaction.options.getString('message_id', true);
|
||||
const channel = interaction.channel;
|
||||
if (!channel?.isTextBased()) {
|
||||
await interaction.reply({ content: t(locale, 'utility.error.channel_not_found'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
const message = await channel.messages.fetch(messageId).catch(() => null);
|
||||
if (!message) {
|
||||
await interaction.reply({ content: t(locale, 'utility.emoji.messageNotFound'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
const stolen = extractFirstCustomEmoji(message.content);
|
||||
if (!stolen) {
|
||||
await interaction.reply({ content: t(locale, 'utility.emoji.noEmojiInMessage'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
const isAnimated = message.content.includes(`<a:${stolen.name}:${stolen.id}>`);
|
||||
const emoji = await interaction.guild!.emojis.create({
|
||||
attachment: `https://cdn.discordapp.com/emojis/${stolen.id}.${isAnimated ? 'gif' : 'png'}`,
|
||||
name: stolen.name
|
||||
});
|
||||
await interaction.reply({
|
||||
content: tf(locale, 'utility.emoji.stolen', { emoji: emoji.toString() }),
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const stickerCommand: SlashCommand = {
|
||||
data: stickerCommandData,
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
if (!(await ensureGuildCommand(interaction, locale))) return;
|
||||
if (!(await requirePermission(interaction, PermissionFlagsBits.ManageGuildExpressions, locale))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const name = interaction.options.getString('name', true);
|
||||
const attachment = interaction.options.getAttachment('attachment');
|
||||
const url = interaction.options.getString('url') ?? attachment?.url;
|
||||
if (!url) {
|
||||
await interaction.reply({ content: t(locale, 'utility.sticker.noSource'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
|
||||
const sticker = await interaction.guild!.stickers.create({
|
||||
file: url,
|
||||
name,
|
||||
tags: name
|
||||
});
|
||||
await interaction.reply({
|
||||
content: tf(locale, 'utility.sticker.added', { name: sticker.name }),
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const timestampCommand: SlashCommand = {
|
||||
data: timestampCommandData,
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
const input = interaction.options.getString('datetime', true);
|
||||
try {
|
||||
const ms = parseTimestampInput(input);
|
||||
const unix = Math.floor(ms / 1000);
|
||||
await interaction.reply({
|
||||
content: formatDiscordTimestamps(unix),
|
||||
ephemeral: true
|
||||
});
|
||||
} catch (error) {
|
||||
await handleUtilityError(interaction, locale, error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const snipeCommand: SlashCommand = {
|
||||
data: snipeCommandData,
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
if (!(await ensureGuildCommand(interaction, locale))) return;
|
||||
|
||||
const result = await getDeletedSnipe(context, interaction.channelId!, locale);
|
||||
if (!result) {
|
||||
await interaction.reply({ content: t(locale, 'utility.snipe.none'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
await interaction.reply({
|
||||
embeds: [result.embed],
|
||||
content: result.privacyNote,
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const editsnipeCommand: SlashCommand = {
|
||||
data: editsnipeCommandData,
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
if (!(await ensureGuildCommand(interaction, locale))) return;
|
||||
|
||||
const result = await getEditedSnipe(context, interaction.channelId!, locale);
|
||||
if (!result) {
|
||||
await interaction.reply({ content: t(locale, 'utility.snipe.none'), ephemeral: true });
|
||||
return;
|
||||
}
|
||||
await interaction.reply({
|
||||
embeds: [result.embed],
|
||||
content: result.privacyNote,
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const embedCommand: SlashCommand = {
|
||||
data: embedCommandData,
|
||||
async execute(interaction, context) {
|
||||
const locale = await getGuildLocale(context.prisma, interaction.guildId);
|
||||
if (!(await ensureGuildCommand(interaction, locale))) return;
|
||||
if (!(await requirePermission(interaction, PermissionFlagsBits.ManageMessages, locale))) {
|
||||
return;
|
||||
}
|
||||
await showEmbedBuilderModal(interaction);
|
||||
}
|
||||
};
|
||||
|
||||
export const utilityCommands: SlashCommand[] = [
|
||||
userinfoCommand,
|
||||
serverinfoCommand,
|
||||
roleinfoCommand,
|
||||
channelinfoCommand,
|
||||
avatarCommand,
|
||||
bannerCommand,
|
||||
pollCommand,
|
||||
remindmeCommand,
|
||||
remindersCommand,
|
||||
afkCommand,
|
||||
emojiCommand,
|
||||
stickerCommand,
|
||||
timestampCommand,
|
||||
snipeCommand,
|
||||
editsnipeCommand,
|
||||
embedCommand
|
||||
];
|
||||
Reference in New Issue
Block a user