import { type ChatInputCommandInteraction, type GuildMember, type Interaction } from 'discord.js'; import type { Locale } from '@nexumi/shared'; import { t } from '@nexumi/shared'; export type HierarchyAction = 'ban' | 'kick' | 'timeout' | 'nick'; export type HierarchyOk = { ok: true; member: GuildMember | null }; export type HierarchyDenied = { ok: false }; /** * Validates that the moderator and bot can act on the target member. * Replies ephemerally on denial. For bans, `member` may be null if the user * already left the guild (ban-by-id is still allowed). */ export async function assertModerableMember( interaction: ChatInputCommandInteraction, targetUserId: string, locale: Locale, action: HierarchyAction ): Promise { const guild = interaction.guild; if (!guild) { await interaction.reply({ content: t(locale, 'generic.guildOnly'), ephemeral: true }); return { ok: false }; } if (targetUserId === interaction.user.id) { await interaction.reply({ content: t(locale, 'moderation.hierarchy.self'), ephemeral: true }); return { ok: false }; } if (targetUserId === guild.ownerId) { await interaction.reply({ content: t(locale, 'moderation.hierarchy.owner'), ephemeral: true }); return { ok: false }; } let member: GuildMember | null = null; try { member = await guild.members.fetch(targetUserId); } catch { if (action === 'ban') { return { ok: true, member: null }; } await interaction.reply({ content: t(locale, 'moderation.hierarchy.notMember'), ephemeral: true }); return { ok: false }; } const moderator = interaction.member && 'roles' in interaction.member ? (interaction.member as GuildMember) : await guild.members.fetch(interaction.user.id); if (guild.ownerId !== moderator.id) { if (member.roles.highest.position >= moderator.roles.highest.position) { await interaction.reply({ content: t(locale, 'moderation.hierarchy.moderator'), ephemeral: true }); return { ok: false }; } } const botOk = action === 'ban' ? member.bannable : action === 'kick' ? member.kickable : action === 'timeout' ? member.moderatable : member.manageable; if (!botOk) { await interaction.reply({ content: t(locale, 'moderation.hierarchy.bot'), ephemeral: true }); return { ok: false }; } return { ok: true, member }; } async function replyHierarchyForButton(interaction: Interaction, locale: Locale, key: string): Promise { if (!interaction.isRepliable()) { return; } const content = t(locale, key); if (interaction.isButton()) { await interaction.update({ content, components: [] }); return; } if (interaction.replied || interaction.deferred) { await interaction.followUp({ content, ephemeral: true }); } else { await interaction.reply({ content, ephemeral: true }); } } export async function assertBannableForConfirm( interaction: Interaction, locale: Locale, targetUserId: string ): Promise { const guild = interaction.guild; if (!guild) { await replyHierarchyForButton(interaction, locale, 'generic.guildOnly'); return false; } if (targetUserId === interaction.user.id) { await replyHierarchyForButton(interaction, locale, 'moderation.hierarchy.self'); return false; } if (targetUserId === guild.ownerId) { await replyHierarchyForButton(interaction, locale, 'moderation.hierarchy.owner'); return false; } try { const member = await guild.members.fetch(targetUserId); if (!member.bannable) { await replyHierarchyForButton(interaction, locale, 'moderation.hierarchy.bot'); return false; } const moderator = await guild.members.fetch(interaction.user.id); if ( guild.ownerId !== moderator.id && member.roles.highest.position >= moderator.roles.highest.position ) { await replyHierarchyForButton(interaction, locale, 'moderation.hierarchy.moderator'); return false; } } catch { // Target left the guild — ban by id is still fine. } return true; }