import { AttachmentBuilder, EmbedBuilder, 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 { addXp, formatLeaderboardLine, getConfig, getLeaderboard, getOrCreateMemberLevel, getRankPosition, memberProgress, removeXp, resetXp } from './service.js'; import { renderRankCard } from './rank-card.js'; import { leaderboardCommandData, rankCommandData, xpCommandData } from './command-definitions.js'; const rankCommand: SlashCommand = { data: rankCommandData, async execute(interaction, context) { const locale = await getGuildLocale(context.prisma, interaction.guildId); if (!interaction.inGuild() || !interaction.guild) { await interaction.reply({ content: t(locale, 'generic.guildOnly'), ephemeral: true }); return; } const targetUser = interaction.options.getUser('user') ?? interaction.user; const member = await interaction.guild.members.fetch(targetUser.id).catch(() => null); if (!member) { await interaction.reply({ content: t(locale, 'leveling.user.notFound'), ephemeral: true }); return; } const config = await getConfig(context.prisma, interaction.guild.id); const memberLevel = await getOrCreateMemberLevel(context.prisma, interaction.guild.id, member.id); const rank = await getRankPosition(context.prisma, interaction.guild.id, member.id); const progress = memberProgress(memberLevel); const buffer = await renderRankCard({ displayName: member.displayName, avatarUrl: member.user.displayAvatarURL({ extension: 'png', size: 256 }), level: progress.level, rank, intoLevel: progress.intoLevel, needed: progress.needed, totalXp: memberLevel.xp, accentColor: config.cardAccentColor, backgroundColor: config.cardBackgroundColor }); const attachment = new AttachmentBuilder(buffer, { name: 'rank.png' }); await interaction.reply({ files: [attachment] }); } }; const leaderboardCommand: SlashCommand = { data: leaderboardCommandData, async execute(interaction, context) { const locale = await getGuildLocale(context.prisma, interaction.guildId); if (!interaction.inGuild() || !interaction.guild) { await interaction.reply({ content: t(locale, 'generic.guildOnly'), ephemeral: true }); return; } const entries = await getLeaderboard(context.prisma, interaction.guild.id, 10); if (entries.length === 0) { await interaction.reply({ content: t(locale, 'leveling.leaderboard.empty'), ephemeral: true }); return; } const lines = await Promise.all( entries.map(async (entry, index) => { const user = await context.client.users.fetch(entry.userId).catch(() => null); const label = user ? `<@${entry.userId}>` : entry.userId; return formatLeaderboardLine(locale, index + 1, label, entry.level, entry.xp); }) ); const embed = new EmbedBuilder() .setTitle(t(locale, 'leveling.leaderboard.title')) .setDescription(lines.join('\n')) .setFooter({ text: tf(locale, 'leveling.leaderboard.footer', { count: entries.length }) }) .setColor(0x6366f1); await interaction.reply({ embeds: [embed] }); } }; const xpCommand: SlashCommand = { data: xpCommandData, async execute(interaction, context) { const locale = await getGuildLocale(context.prisma, interaction.guildId); if (!interaction.inGuild() || !interaction.guild) { await interaction.reply({ content: t(locale, 'generic.guildOnly'), ephemeral: true }); return; } if (!(await requirePermission(interaction, PermissionFlagsBits.ManageGuild, locale))) { return; } const sub = interaction.options.getSubcommand(); const user = interaction.options.getUser('user', true); const member = await interaction.guild.members.fetch(user.id).catch(() => null); if (!member) { await interaction.reply({ content: t(locale, 'leveling.user.notFound'), ephemeral: true }); return; } const channelId = interaction.channelId ?? undefined; if (sub === 'give') { const amount = interaction.options.getInteger('amount', true); const result = await addXp(context, interaction.guild, member, amount, { channelId, force: true }); const level = result?.newLevel ?? (await getOrCreateMemberLevel(context.prisma, interaction.guild.id, member.id)).level; await interaction.reply({ content: tf(locale, 'leveling.xp.give.success', { amount, user: `<@${member.id}>`, level }), ephemeral: true }); return; } if (sub === 'remove') { const amount = interaction.options.getInteger('amount', true); const result = await removeXp(context, interaction.guild, member, amount, channelId); const level = result?.newLevel ?? (await getOrCreateMemberLevel(context.prisma, interaction.guild.id, member.id)).level; await interaction.reply({ content: tf(locale, 'leveling.xp.remove.success', { amount, user: `<@${member.id}>`, level }), ephemeral: true }); return; } await resetXp(context, interaction.guild, member, channelId); await interaction.reply({ content: tf(locale, 'leveling.xp.reset.success', { user: `<@${member.id}>` }), ephemeral: true }); } }; export const levelingCommands: SlashCommand[] = [rankCommand, leaderboardCommand, xpCommand];