From 312ccc7b2a5c0478f5a5075b9e594f3774bb670c Mon Sep 17 00:00:00 2001 From: shni Date: Sat, 4 Oct 2025 01:10:02 -0500 Subject: [PATCH] feat: implement permission checks for ManageGuild and staff roles across commands --- .../migration.sql | 2 + prisma/schema.prisma | 1 + .../alliaces/createDisplayComponent.ts | 8 +- .../alliaces/deleteDisplayComponent.ts | 14 +- .../messages/alliaces/displayComponentList.ts | 13 +- .../messages/alliaces/editDisplayComponent.ts | 8 +- src/commands/messages/alliaces/leaderboard.ts | 9 +- .../messages/alliaces/listChannelsV2.ts | 6 +- .../messages/alliaces/removeChannel.ts | 8 +- src/commands/messages/alliaces/sendEmbed.ts | 10 +- .../messages/alliaces/setupChannel.ts | 9 +- .../messages/settings-server/settings.ts | 361 +++++++----------- src/components/buttons/ldManagePoints.ts | 11 +- src/components/buttons/ldRefresh.ts | 8 +- src/components/modals/ldPointsModal.ts | 9 +- src/core/lib/permissions.ts | 35 ++ 16 files changed, 237 insertions(+), 275 deletions(-) create mode 100644 prisma/migrations/20251004054453_staff_role_json/migration.sql create mode 100644 src/core/lib/permissions.ts diff --git a/prisma/migrations/20251004054453_staff_role_json/migration.sql b/prisma/migrations/20251004054453_staff_role_json/migration.sql new file mode 100644 index 0000000..a6083d7 --- /dev/null +++ b/prisma/migrations/20251004054453_staff_role_json/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "public"."Guild" ADD COLUMN "staff" JSONB; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 9d46489..0ef2222 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -22,6 +22,7 @@ model Guild { id String @id name String prefix String @default("!") + staff Json? // Relaciones alliances Alliance[] diff --git a/src/commands/messages/alliaces/createDisplayComponent.ts b/src/commands/messages/alliaces/createDisplayComponent.ts index 6da476c..8227345 100644 --- a/src/commands/messages/alliaces/createDisplayComponent.ts +++ b/src/commands/messages/alliaces/createDisplayComponent.ts @@ -12,6 +12,7 @@ import {listVariables} from "../../../core/lib/vars"; import type Amayo from "../../../core/client"; import {BlockState, DisplayComponentUtils, EditorActionRow} from "../../../core/types/displayComponentEditor"; import type {DisplayComponentContainer} from "../../../core/types/displayComponents"; +import { hasManageGuildOrStaff } from "../../../core/lib/permissions"; interface EditorData { content?: string; @@ -51,8 +52,9 @@ export const command: CommandMessage = { category: "Alianzas", usage: "crear-embed ", run: async (message, args, client) => { - if (!message.member?.permissions.has("Administrator")) { - await message.reply("❌ No tienes permisos de Administrador."); + const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, client.prisma); + if (!allowed) { + await message.reply("❌ No tienes permisos de ManageGuild ni rol de staff."); return; } @@ -719,7 +721,7 @@ async function handleSaveBlock( }); await interaction.reply({ - content: `✅ **Bloque guardado exitosamente!**\n\n📄 **Nombre:** \`${blockName}\`\n🎨 **Componentes:** ${blockState.components.length}\n\n🎯 **Uso:** \`!send ${blockName}\``, + content: `✅ **Bloque guardado exitosamente!**\n\n📄 **Nombre:** \`${blockName}\`\n🎨 **Componentes:** ${blockState.components.length}\n\n🎯 **Uso:** \`!send-embed ${blockName}\``, flags: MessageFlags.Ephemeral }); diff --git a/src/commands/messages/alliaces/deleteDisplayComponent.ts b/src/commands/messages/alliaces/deleteDisplayComponent.ts index b11d1ce..a9fe50a 100644 --- a/src/commands/messages/alliaces/deleteDisplayComponent.ts +++ b/src/commands/messages/alliaces/deleteDisplayComponent.ts @@ -10,6 +10,7 @@ import { import { CommandMessage } from "../../../core/types/commands"; import type Amayo from "../../../core/client"; import type { JsonValue } from "@prisma/client/runtime/library"; +import { hasManageGuildOrStaff } from "../../../core/lib/permissions"; interface BlockItem { name: string; @@ -22,16 +23,17 @@ interface ActionRowBuilder { } export const command: CommandMessage = { - name: "eliminar-embed", + name: "eliminar-bloque", type: "message", - aliases: ["embed-eliminar", "borrar-embed", "embeddelete"], + aliases: ["bloque-eliminar", "bloque-embed", "blockdelete"], cooldown: 10, description: "Elimina bloques DisplayComponents del servidor", - category: "Alianzas", - usage: "eliminar-embed [nombre_bloque]", + category: "Creacion", + usage: "eliminar-bloque [nombre_bloque]", run: async (message: Message, args: string[], client: Amayo): Promise => { - if (!message.member?.permissions.has("Administrator")) { - await message.reply("❌ No tienes permisos de Administrador."); + const allowed = await hasManageGuildOrStaff(message.member, message.guildId!, client.prisma); + if (!allowed) { + await message.reply("❌ No tienes permisos de ManageGuild ni rol de staff."); return; } diff --git a/src/commands/messages/alliaces/displayComponentList.ts b/src/commands/messages/alliaces/displayComponentList.ts index 7d9d88a..4f1c672 100644 --- a/src/commands/messages/alliaces/displayComponentList.ts +++ b/src/commands/messages/alliaces/displayComponentList.ts @@ -16,6 +16,7 @@ import type { } from "../../../core/types/displayComponents"; import type Amayo from "../../../core/client"; import type { JsonValue } from "@prisma/client/runtime/library"; +import { hasManageGuildOrStaff } from "../../../core/lib/permissions"; interface BlockListItem { name: string; @@ -29,17 +30,17 @@ interface ActionRowBuilder { } export const command: CommandMessage = { - name: "lista-embeds", + name: "lista-bloques", type: "message", - aliases: ["embeds", "ver-embeds", "embedlist"], + aliases: ["bloques", "ver-bloques", "blocks"], cooldown: 10, description: "Muestra todos los bloques DisplayComponents configurados en el servidor", category: "Alianzas", - usage: "lista-embeds", + usage: "lista-bloques", run: async (message: Message, args: string[], client: Amayo): Promise => { - // Permission check - if (!message.member?.permissions.has("Administrator")) { - await message.reply("❌ No tienes permisos de Administrador."); + const allowed = await hasManageGuildOrStaff(message.member, message.guildId!, client.prisma); + if (!allowed) { + await message.reply("❌ No tienes permisos de ManageGuild ni rol de staff."); return; } diff --git a/src/commands/messages/alliaces/editDisplayComponent.ts b/src/commands/messages/alliaces/editDisplayComponent.ts index 3198962..566d294 100644 --- a/src/commands/messages/alliaces/editDisplayComponent.ts +++ b/src/commands/messages/alliaces/editDisplayComponent.ts @@ -2,6 +2,7 @@ import { CommandMessage } from "../../../core/types/commands"; import { MessageFlags } from "discord.js"; import { ComponentType, ButtonStyle, TextInputStyle } from "discord-api-types/v10"; import { replaceVars, isValidUrlOrVariable, listVariables } from "../../../core/lib/vars"; +import { hasManageGuildOrStaff } from "../../../core/lib/permissions"; // Botones de edición (máx 5 por fila) const btns = (disabled = false) => ([ @@ -134,8 +135,9 @@ export const command: CommandMessage = { category: "Alianzas", usage: "editar-embed ", run: async (message, args, client) => { - if (!message.member?.permissions.has("Administrator")) { - await message.reply("❌ No tienes permisos de Administrador."); + const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, client.prisma); + if (!allowed) { + await message.reply("❌ No tienes permisos de ManageGuild ni rol de staff."); return; } @@ -149,7 +151,7 @@ export const command: CommandMessage = { where: { guildId: message.guild!.id, name: blockName } }); if (!existingBlock) { - await message.reply("❌ Block no encontrado. Usa `!blockcreatev2 ` para crear uno nuevo."); + await message.reply("❌ Block no encontrado. Usa `!editar-bloque ` para crear uno nuevo."); return; } diff --git a/src/commands/messages/alliaces/leaderboard.ts b/src/commands/messages/alliaces/leaderboard.ts index 2d1aeaa..65068c6 100644 --- a/src/commands/messages/alliaces/leaderboard.ts +++ b/src/commands/messages/alliaces/leaderboard.ts @@ -1,9 +1,10 @@ // Comando para mostrar el leaderboard de alianzas con botón de refresco // @ts-ignore import { CommandMessage } from "../../../core/types/commands"; +import { PermissionFlagsBits } from "discord.js"; +import { hasManageGuildOrStaff } from '../../../core/lib/permissions'; import { prisma } from "../../../core/database/prisma"; import type { Message } from "discord.js"; -import { PermissionFlagsBits } from "discord.js"; const MAX_ENTRIES = 10; @@ -236,7 +237,7 @@ export const command: CommandMessage = { aliases: ['ld'], cooldown: 5, description: 'Muestra el leaderboard de alianzas (semanal, mensual y total) con botón de refresco.', - category: 'Utilidad', + category: 'Alianzas', usage: 'leaderboard', run: async (message) => { if (!message.guild) { @@ -244,9 +245,9 @@ export const command: CommandMessage = { return; } - // Verificar si el usuario es administrador + // Verificar si el usuario es admin o staff const member = await message.guild.members.fetch(message.author.id); - const isAdmin = member.permissions.has(PermissionFlagsBits.ManageGuild); + const isAdmin = await hasManageGuildOrStaff(member, message.guild.id, prisma); const panel = await buildLeaderboardPanel(message, isAdmin); await message.reply({ diff --git a/src/commands/messages/alliaces/listChannelsV2.ts b/src/commands/messages/alliaces/listChannelsV2.ts index 0de3ac6..db10fa0 100644 --- a/src/commands/messages/alliaces/listChannelsV2.ts +++ b/src/commands/messages/alliaces/listChannelsV2.ts @@ -263,13 +263,13 @@ async function buildChannelListPanel(message: Message) { } export const command: CommandMessage = { - name: "listar-canales-alianza-v2", + name: "lista-canales", type: "message", - aliases: ["listchannels-alliance-v2", "listalchannel-v2", "channelsally-v2", "alliancechannels-v2"], + aliases: ["lca", "channelist", "alliacechannels"], cooldown: 5, description: "Lista todos los canales configurados para alianzas (versión V2 con components)", category: "Alianzas", - usage: "listar-canales-alianza-v2", + usage: "lista-canales", run: async (message) => { if (!message.guild) { await message.reply({ content: '❌ Este comando solo puede usarse en servidores.' }); diff --git a/src/commands/messages/alliaces/removeChannel.ts b/src/commands/messages/alliaces/removeChannel.ts index df60f28..8e00a51 100644 --- a/src/commands/messages/alliaces/removeChannel.ts +++ b/src/commands/messages/alliaces/removeChannel.ts @@ -2,16 +2,18 @@ import logger from "../../../core/lib/logger"; import { CommandMessage } from "../../../core/types/commands"; // @ts-ignore import { EmbedBuilder, ButtonStyle, MessageFlags, ChannelType } from "discord.js"; +import { hasManageGuildOrStaff } from "../../../core/lib/permissions"; export const command: CommandMessage = { - name: "eliminar-canal-alianza", + name: "eliminar-canal", type: "message", aliases: ["removechannel-alliance", "removealchannel", "delalchannel"], cooldown: 10, // @ts-ignore run: async (message, args, client) => { - if (!message.member?.permissions.has("Administrator")) { - return message.reply("❌ No tienes permisos de Administrador."); + const allowed = await hasManageGuildOrStaff(message.member, message.guildId!, client.prisma); + if (!allowed) { + return message.reply("❌ No tienes permisos de ManageGuild ni rol de staff."); } // Obtener canales configurados existentes diff --git a/src/commands/messages/alliaces/sendEmbed.ts b/src/commands/messages/alliaces/sendEmbed.ts index 8127abc..a2d2ec1 100644 --- a/src/commands/messages/alliaces/sendEmbed.ts +++ b/src/commands/messages/alliaces/sendEmbed.ts @@ -3,6 +3,7 @@ import { MessageFlags } from "discord.js"; import { DisplayComponentUtils } from "../../../core/types/displayComponentEditor"; import { sendComponentsV2Message } from "../../../core/api/discordAPI"; import logger from "../../../core/lib/logger"; +import { hasManageGuildOrStaff } from "../../../core/lib/permissions"; export const command: CommandMessage = { name: "send-embed", @@ -13,9 +14,9 @@ export const command: CommandMessage = { category: "Alianzas", usage: "send-embed ", run: async (message, args, client) => { - // Requiere administrador para evitar abuso mientras se prueba - if (!message.member?.permissions.has("Administrator")) { - await message.reply("❌ No tienes permisos de Administrador."); + const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, client.prisma); + if (!allowed) { + await message.reply("❌ No tienes permisos de ManageGuild ni rol de staff."); return; } @@ -35,7 +36,6 @@ export const command: CommandMessage = { return; } - // Renderizamos usando la misma utilidad del editor (fuente: node_modules/discord.js APIs via repo util DisplayComponentUtils) const container = await DisplayComponentUtils.renderPreview( // @ts-ignore - guardamos BlockState como config existingBlock.config, @@ -43,7 +43,6 @@ export const command: CommandMessage = { message.guild! ); - // Enviamos como Components v2 (fuente: repositorio local sendComponentsV2Message) await sendComponentsV2Message(message.channel.id, { components: [container as any], replyToMessageId: message.id @@ -59,4 +58,3 @@ export const command: CommandMessage = { } } }; - diff --git a/src/commands/messages/alliaces/setupChannel.ts b/src/commands/messages/alliaces/setupChannel.ts index 003ad7a..08e3e08 100644 --- a/src/commands/messages/alliaces/setupChannel.ts +++ b/src/commands/messages/alliaces/setupChannel.ts @@ -1,16 +1,21 @@ import { CommandMessage } from "../../../core/types/commands"; // @ts-ignore import { ComponentType, ButtonStyle, MessageFlags, ChannelType } from "discord.js"; +import { hasManageGuildOrStaff } from "../../../core/lib/permissions"; export const command: CommandMessage = { name: "canal-alianza", type: "message", aliases: ["alchannel", "channelally"], + description: "Configura canales para el sistema de alianzas con bloques DisplayComponents.", + usage: "canal-alianza", + category: "Alianzas", cooldown: 10, // @ts-ignore run: async (message, args, client) => { - if (!message.member?.permissions.has("Administrator")) { - return message.reply("❌ No tienes permisos de Administrador."); + const allowed = await hasManageGuildOrStaff(message.member, message.guildId!, client.prisma); + if (!allowed) { + return message.reply("❌ No tienes permisos de ManageGuild ni rol de staff."); } // Obtener canales configurados existentes diff --git a/src/commands/messages/settings-server/settings.ts b/src/commands/messages/settings-server/settings.ts index 72165e2..4844d1c 100644 --- a/src/commands/messages/settings-server/settings.ts +++ b/src/commands/messages/settings-server/settings.ts @@ -1,17 +1,25 @@ import logger from "../../../core/lib/logger"; import { CommandMessage } from "../../../core/types/commands"; +import { ComponentType } from "discord-api-types/v10"; +import { hasManageGuildOrStaff } from "../../../core/lib/permissions"; + +function toStringArray(input: unknown): string[] { + if (!Array.isArray(input)) return []; + return (input as unknown[]).filter((v): v is string => typeof v === 'string'); +} export const command: CommandMessage = { name: 'configuracion', type: "message", aliases: ['config', 'ajustes', 'settings'], cooldown: 5, - description: 'Abre el panel de configuración del servidor (prefix y más).', + description: 'Abre el panel de configuración del servidor (prefix, staff y más).', category: 'Configuración', usage: 'configuracion', run: async (message, args, client) => { - if (!message.member?.permissions.has("Administrator")) { - await message.reply("❌ No tienes permisos de Administrador."); + const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, client.prisma); + if (!allowed) { + await message.reply("❌ No tienes permisos de ManageGuild ni rol de staff."); return; } @@ -20,52 +28,48 @@ export const command: CommandMessage = { }); const currentPrefix = server?.prefix || "!"; + const staffRoles: string[] = toStringArray(server?.staff); + const staffDisplay = staffRoles.length + ? staffRoles.map((id) => `<@&${id}>`).join(', ') + : 'Sin staff configurado'; // Panel de configuración usando DisplayComponents const settingsPanel = { type: 17, accent_color: 6178018, // Color del ejemplo components: [ + { type: 10, content: "### <:invisible:1418684224441028608> 梅,panel admin,📢\n" }, + { type: 14, spacing: 1, divider: false }, + { type: 10, content: "Configuracion del Servidor:" }, { - type: 10, - content: "### <:invisible:1418684224441028608> 梅,panel admin,📢\n" - }, - { - type: 14, - spacing: 1, - divider: false - }, - { - type: 10, - content: "Configuracion del Servidor:" - }, - { - type: 9, // Section - components: [ - { - type: 10, - content: `**Prefix:**<:invisible:1418684224441028608>\`${currentPrefix}\`` - } - ], + type: 9, + components: [ { type: 10, content: `**Prefix:**<:invisible:1418684224441028608>\`${currentPrefix}\`` } ], accessory: { - type: 2, // Button - style: 2, // Secondary - emoji: { - name: "⚙️" - }, + type: 2, + style: 2, + emoji: { name: "⚙️" }, custom_id: "open_prefix_modal", label: "Cambiar" } }, + { type: 14, divider: false }, { - type: 14, - divider: false - } + type: 9, + components: [ { type: 10, content: `**Staff (roles):** ${staffDisplay}` } ], + accessory: { + type: 2, + style: 2, // Secondary + emoji: { name: "🛡️" }, + custom_id: "open_staff_modal", + label: "Configurar" + } + }, + { type: 14, divider: false } ] }; const panelMessage = await message.reply({ - flags: 32768, // SuppressEmbeds + flags: 32768, // Components v2 components: [settingsPanel] }); @@ -81,44 +85,15 @@ export const command: CommandMessage = { title: "⚙️ Configurar Prefix del Servidor", custom_id: "prefix_settings_modal", components: [ - { - type: 1, // ActionRow - components: [ - { - type: 4, // TextInput - custom_id: "new_prefix_input", - label: "Nuevo Prefix", - style: 1, // Short - placeholder: `Prefix actual: ${currentPrefix}`, - required: true, - max_length: 10, - min_length: 1, - value: currentPrefix - } - ] - }, - { - type: 1, - components: [ - { - type: 4, - custom_id: "prefix_description", - label: "¿Por qué cambiar el prefix? (Opcional)", - style: 2, // Paragraph - placeholder: "Ej: Evitar conflictos con otros bots...", - required: false, - max_length: 200 - } - ] - } + { type: 1, components: [ { type: 4, custom_id: "new_prefix_input", label: "Nuevo Prefix", style: 1, placeholder: `Prefix actual: ${currentPrefix}`, required: true, max_length: 10, min_length: 1, value: currentPrefix } ] }, + { type: 1, components: [ { type: 4, custom_id: "prefix_description", label: "¿Por qué cambiar el prefix? (Opcional)", style: 2, placeholder: "Ej: Evitar conflictos con otros bots...", required: false, max_length: 200 } ] } ] }; await interaction.showModal(prefixModal); - // Crear un collector específico para este modal const modalCollector = interaction.awaitModalSubmit({ - time: 300000, // 5 minutos + time: 300000, filter: (modalInt: any) => modalInt.customId === "prefix_settings_modal" && modalInt.user.id === message.author.id }); @@ -126,226 +101,158 @@ export const command: CommandMessage = { const newPrefix = modalInteraction.fields.getTextInputValue("new_prefix_input"); const description = modalInteraction.fields.getTextInputValue("prefix_description") || "Sin descripción"; - // Validar prefix if (!newPrefix || newPrefix.length > 10) { - await modalInteraction.reply({ - content: "❌ **Error:** El prefix debe tener entre 1 y 10 caracteres.", - flags: 64 // Ephemeral - }); + await modalInteraction.reply({ content: "❌ **Error:** El prefix debe tener entre 1 y 10 caracteres.", flags: 64 }); return; } try { - // Actualizar prefix en la base de datos await client.prisma.guild.upsert({ where: { id: message.guild!.id }, - create: { - id: message.guild!.id, - name: message.guild!.name, - prefix: newPrefix - }, - update: { - prefix: newPrefix, - name: message.guild!.name - } + create: { id: message.guild!.id, name: message.guild!.name, prefix: newPrefix }, + update: { prefix: newPrefix, name: message.guild!.name } }); - // Panel de confirmación const successPanel = { type: 17, - accent_color: 3066993, // Verde + accent_color: 3066993, components: [ - { - type: 10, - content: "### ✅ **Prefix Actualizado Exitosamente**" - }, - { - type: 14, - spacing: 2, - divider: true - }, - { - type: 9, - components: [ - { - type: 10, - content: `**Prefix anterior:** \`${currentPrefix}\`\n**Prefix nuevo:** \`${newPrefix}\`\n\n**Motivo:** ${description}` - } - ], - accessory: { - type: 2, - style: 3, // Success - label: "✓ Listo", - custom_id: "prefix_confirmed", - emoji: { name: "✅" } - } - }, - { - type: 14, - spacing: 1, - divider: false - }, - { - type: 10, - content: "🚀 **¡Listo!** Ahora puedes usar los comandos con el nuevo prefix.\n\n💡 **Ejemplo:** `" + newPrefix + "help`, `" + newPrefix + "embedlist`" - } + { type: 10, content: "### ✅ **Prefix Actualizado Exitosamente**" }, + { type: 14, spacing: 2, divider: true }, + { type: 9, components: [ { type: 10, content: `**Prefix anterior:** \`${currentPrefix}\`\n**Prefix nuevo:** \`${newPrefix}\`\n\n**Motivo:** ${description}` } ], accessory: { type: 2, style: 3, label: "✓ Listo", custom_id: "prefix_confirmed", emoji: { name: "✅" } } }, + { type: 14, spacing: 1, divider: false }, + { type: 10, content: "🚀 **¡Listo!** Ahora puedes usar los comandos con el nuevo prefix.\n\n💡 **Ejemplo:** `" + newPrefix + "help`, `" + newPrefix + "embedlist`" } ] }; - // Botón para volver al panel principal - const backToSettingsRow = { - type: 1, - components: [ - { - type: 2, - style: 2, // Secondary - label: "↩️ Volver a Configuración", - custom_id: "back_to_settings" - } - ] - }; + const backToSettingsRow = { type: 1, components: [ { type: 2, style: 2, label: "↩️ Volver a Configuración", custom_id: "back_to_settings" } ] }; - // Actualizar el panel original - await modalInteraction.update({ - components: [successPanel, backToSettingsRow] - }); + await modalInteraction.update({ components: [successPanel, backToSettingsRow] }); } catch (error) { const errorPanel = { type: 17, - accent_color: 15548997, // Rojo + accent_color: 15548997, components: [ - { - type: 10, - content: "### ❌ **Error al Actualizar Prefix**" - }, - { - type: 14, - spacing: 2, - divider: true - }, - { - type: 10, - content: `**Error:** No se pudo actualizar el prefix a \`${newPrefix}\`\n\n**Posibles causas:**\n• Error de conexión con la base de datos\n• Prefix contiene caracteres no válidos\n• Permisos insuficientes\n\n🔄 **Solución:** Intenta nuevamente con un prefix diferente.` - } + { type: 10, content: "### ❌ **Error al Actualizar Prefix**" }, + { type: 14, spacing: 2, divider: true }, + { type: 10, content: `**Error:** No se pudo actualizar el prefix a \`${newPrefix}\`\n\n**Posibles causas:**\n• Error de conexión con la base de datos\n• Prefix contiene caracteres no válidos\n• Permisos insuficientes\n\n🔄 **Solución:** Intenta nuevamente con un prefix diferente.` } ] }; - const retryRow = { - type: 1, - components: [ - { - type: 2, - style: 2, - label: "🔄 Reintentar", - custom_id: "open_prefix_modal" - }, - { - type: 2, - style: 4, // Danger - label: "❌ Cancelar", - custom_id: "cancel_prefix_change" - } - ] - }; + const retryRow = { type: 1, components: [ { type: 2, style: 2, label: "🔄 Reintentar", custom_id: "open_prefix_modal" }, { type: 2, style: 4, label: "❌ Cancelar", custom_id: "cancel_prefix_change" } ] }; - await modalInteraction.update({ - components: [errorPanel, retryRow] - }); + await modalInteraction.update({ components: [errorPanel, retryRow] }); } }).catch(async (error: any) => { - // Modal timeout o cancelado logger.info("Modal timeout o error:", error.message); }); } + if (interaction.customId === "open_staff_modal") { + // Modal para seleccionar hasta 3 roles de staff + const staffModal = { + title: "🛡️ Configurar Roles de Staff", + customId: "staff_roles_modal", + components: [ + { type: ComponentType.Label, label: "Selecciona hasta 3 roles de staff", component: { type: ComponentType.RoleSelect, customId: "staff_roles", required: false, minValues: 0, maxValues: 3, placeholder: "Roles de staff..." } } + ] + } as const; + + await interaction.showModal(staffModal); + + try { + const modalInteraction = await interaction.awaitModalSubmit({ time: 300000 }); + const selected = modalInteraction.components.getSelectedRoles('staff_roles'); + const roleIds: string[] = selected ? Array.from(selected.keys()).slice(0, 3) : []; + + await client.prisma.guild.upsert({ + where: { id: message.guild!.id }, + create: { id: message.guild!.id, name: message.guild!.name, staff: roleIds }, + update: { staff: roleIds, name: message.guild!.name } + }); + + const updatedDisplay = roleIds.length ? roleIds.map((id) => `<@&${id}>`).join(', ') : 'Sin staff configurado'; + + const successPanel = { + type: 17, + accent_color: 3066993, + components: [ + { type: 10, content: "### ✅ **Staff Actualizado**" }, + { type: 14, spacing: 2, divider: true }, + { type: 10, content: `**Nuevos roles de staff:** ${updatedDisplay}` } + ] + }; + + const backRow = { type: 1, components: [ { type: 2, style: 2, label: '↩️ Volver a Configuración', custom_id: 'back_to_settings' } ] }; + await modalInteraction.update({ components: [successPanel, backRow] }); + } catch (error) { + // timeout o error + } + } + // Manejar botones adicionales if (interaction.customId === "back_to_settings") { - // Volver al panel principal - const updatedServer = await client.prisma.guild.findFirst({ - where: { id: message.guild!.id } - }); + const updatedServer = await client.prisma.guild.findFirst({ where: { id: message.guild!.id } }); const newCurrentPrefix = updatedServer?.prefix || "!"; + const staffRoles2: string[] = toStringArray(updatedServer?.staff); + const staffDisplay2 = staffRoles2.length ? staffRoles2.map((id) => `<@&${id}>`).join(', ') : 'Sin staff configurado'; const updatedSettingsPanel = { type: 17, accent_color: 6178018, components: [ - { - type: 10, - content: "### <:invisible:1418684224441028608> 梅,panel admin,📢\n" - }, - { - type: 14, - spacing: 1, - divider: false - }, - { - type: 10, - content: "Configuracion del Servidor:" - }, - { - type: 9, - components: [ - { - type: 10, - content: `**Prefix:** \`${newCurrentPrefix}\`` - } - ], - accessory: { - type: 2, - style: 2, - emoji: { name: "⚙️" }, - custom_id: "open_prefix_modal", - label: "Cambiar" - } - }, - { - type: 14, - divider: false - } + { type: 10, content: "### <:invisible:1418684224441028608> 梅,panel admin,📢\n" }, + { type: 14, spacing: 1, divider: false }, + { type: 10, content: "Configuracion del Servidor:" }, + { type: 9, components: [ { type: 10, content: `**Prefix:** \`${newCurrentPrefix}\`` } ], accessory: { type: 2, style: 2, emoji: { name: "⚙️" }, custom_id: "open_prefix_modal", label: "Cambiar" } }, + { type: 14, divider: false }, + { type: 9, components: [ { type: 10, content: `**Staff (roles):** ${staffDisplay2}` } ], accessory: { type: 2, style: 2, emoji: { name: "🛡️" }, custom_id: "open_staff_modal", label: "Configurar" } }, + { type: 14, divider: false } ] }; - await interaction.update({ - components: [updatedSettingsPanel] - }); + await interaction.update({ components: [updatedSettingsPanel] }); } if (interaction.customId === "cancel_prefix_change") { - // Volver al panel original sin cambios - await interaction.update({ - components: [settingsPanel] - }); + // Volver al panel original + const updatedServer = await client.prisma.guild.findFirst({ where: { id: message.guild!.id } }); + const staffRoles3: string[] = toStringArray(updatedServer?.staff); + const staffDisplay3 = staffRoles3.length ? staffRoles3.map((id) => `<@&${id}>`).join(', ') : 'Sin staff configurado'; + + const originalPanel = { + type: 17, + accent_color: 6178018, + components: [ + { type: 10, content: "### <:invisible:1418684224441028608> 梅,panel admin,📢\n" }, + { type: 14, spacing: 1, divider: false }, + { type: 10, content: "Configuracion del Servidor:" }, + { type: 9, components: [ { type: 10, content: `**Prefix:** \`${currentPrefix}\`` } ], accessory: { type: 2, style: 2, emoji: { name: "⚙️" }, custom_id: "open_prefix_modal", label: "Cambiar" } }, + { type: 14, divider: false }, + { type: 9, components: [ { type: 10, content: `**Staff (roles):** ${staffDisplay3}` } ], accessory: { type: 2, style: 2, emoji: { name: "🛡️" }, custom_id: "open_staff_modal", label: "Configurar" } }, + { type: 14, divider: false } + ] + }; + + await interaction.update({ components: [originalPanel] }); } }); - collector.on("end", async (collected: any, reason: string) => { + collector.on("end", async (_: any, reason: string) => { if (reason === "time") { const timeoutPanel = { type: 17, accent_color: 6178018, components: [ - { - type: 10, - content: "### ⏰ **Panel Expirado**" - }, - { - type: 14, - spacing: 1, - divider: true - }, - { - type: 10, - content: "El panel de configuración ha expirado por inactividad.\n\nUsa `!settings` para abrir un nuevo panel." - } + { type: 10, content: "### ⏰ **Panel Expirado**" }, + { type: 14, spacing: 1, divider: true }, + { type: 10, content: "El panel de configuración ha expirado por inactividad.\n\nUsa `!settings` para abrir un nuevo panel." } ] }; try { - await panelMessage.edit({ - components: [timeoutPanel] - }); + await panelMessage.edit({ components: [timeoutPanel] }); } catch (error) { // Mensaje eliminado o error de edición } diff --git a/src/components/buttons/ldManagePoints.ts b/src/components/buttons/ldManagePoints.ts index 009a331..0fff5d4 100644 --- a/src/components/buttons/ldManagePoints.ts +++ b/src/components/buttons/ldManagePoints.ts @@ -1,11 +1,11 @@ import logger from "../../core/lib/logger"; import { ButtonInteraction, - MessageFlags, - PermissionFlagsBits + MessageFlags } from 'discord.js'; import { prisma } from '../../core/database/prisma'; import { ComponentType, TextInputStyle } from 'discord-api-types/v10'; +import { hasManageGuildOrStaff } from "../../core/lib/permissions"; export default { customId: 'ld_manage_points', @@ -17,11 +17,12 @@ export default { }); } - // Verificar permisos de administrador + // Verificar permisos (ManageGuild o rol de staff) const member = await interaction.guild.members.fetch(interaction.user.id); - if (!member.permissions.has(PermissionFlagsBits.ManageGuild)) { + const allowed = await hasManageGuildOrStaff(member, interaction.guild.id, prisma); + if (!allowed) { return interaction.reply({ - content: '❌ Solo los administradores pueden gestionar puntos.', + content: '❌ Solo admins o staff pueden gestionar puntos.', flags: MessageFlags.Ephemeral }); } diff --git a/src/components/buttons/ldRefresh.ts b/src/components/buttons/ldRefresh.ts index a1f4411..447a195 100644 --- a/src/components/buttons/ldRefresh.ts +++ b/src/components/buttons/ldRefresh.ts @@ -1,6 +1,8 @@ import logger from "../../core/lib/logger"; -import { ButtonInteraction, MessageFlags, PermissionFlagsBits } from 'discord.js'; +import { ButtonInteraction, MessageFlags } from 'discord.js'; import { buildLeaderboardPanel } from '../../commands/messages/alliaces/leaderboard'; +import { hasManageGuildOrStaff } from "../../core/lib/permissions"; +import { prisma } from "../../core/database/prisma"; export default { customId: 'ld_refresh', @@ -11,9 +13,9 @@ export default { try { await interaction.deferUpdate(); - // Verificar si el usuario es administrador + // Verificar si el usuario es admin o staff const member = await interaction.guild.members.fetch(interaction.user.id); - const isAdmin = member.permissions.has(PermissionFlagsBits.ManageGuild); + const isAdmin = await hasManageGuildOrStaff(member, interaction.guild.id, prisma); // Reusar el builder esperando un objeto con guild y author const fakeMessage: any = { guild: interaction.guild, author: interaction.user }; diff --git a/src/components/modals/ldPointsModal.ts b/src/components/modals/ldPointsModal.ts index 2ebc13c..ab82775 100644 --- a/src/components/modals/ldPointsModal.ts +++ b/src/components/modals/ldPointsModal.ts @@ -2,13 +2,13 @@ import logger from "../../core/lib/logger"; import { ModalSubmitInteraction, MessageFlags, - PermissionFlagsBits, EmbedBuilder, User, Collection, Snowflake } from 'discord.js'; import { prisma } from '../../core/database/prisma'; +import { hasManageGuildOrStaff } from "../../core/lib/permissions"; interface UserSelectComponent { custom_id: string; @@ -36,11 +36,12 @@ export default { }); } - // Verificar permisos + // Verificar permisos (ManageGuild o rol staff) const member = await interaction.guild.members.fetch(interaction.user.id); - if (!member.permissions.has(PermissionFlagsBits.ManageGuild)) { + const allowed = await hasManageGuildOrStaff(member, interaction.guild.id, prisma); + if (!allowed) { return interaction.reply({ - content: '❌ Solo los administradores pueden gestionar puntos.', + content: '❌ Solo admins o staff pueden gestionar puntos.', flags: MessageFlags.Ephemeral }); } diff --git a/src/core/lib/permissions.ts b/src/core/lib/permissions.ts new file mode 100644 index 0000000..c2ecfd2 --- /dev/null +++ b/src/core/lib/permissions.ts @@ -0,0 +1,35 @@ +import type { GuildMember } from 'discord.js'; +import type { PrismaClient } from '@prisma/client'; + +function toStringArray(input: unknown): string[] { + if (!Array.isArray(input)) return []; + return (input as unknown[]).filter((v): v is string => typeof v === 'string'); +} + +/** + * Returns true if the member has ManageGuild permission or has a role included + * in the Guild.staff JSON (expected: string[] of role IDs). + */ +export async function hasManageGuildOrStaff( + member: GuildMember | null | undefined, + guildId: string, + prisma: PrismaClient +): Promise { + if (!member) return false; + + try { + // Native permission first + if (member.permissions.has('ManageGuild')) return true; + + // Load guild staff config and coerce safely to string[] + const guild = await prisma.guild.findFirst({ where: { id: guildId } }); + const staff = toStringArray(guild?.staff ?? []); + if (!staff.length) return false; + + // Check role intersection + const memberRoles = member.roles?.cache ? Array.from(member.roles.cache.keys()) : []; + return staff.some((roleId) => memberRoles.includes(roleId)); + } catch { + return false; + } +}