feat: implement permission checks for ManageGuild and staff roles across commands

This commit is contained in:
2025-10-04 01:10:02 -05:00
parent 3cd9efcbcb
commit 312ccc7b2a
16 changed files with 237 additions and 275 deletions

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "public"."Guild" ADD COLUMN "staff" JSONB;

View File

@@ -22,6 +22,7 @@ model Guild {
id String @id id String @id
name String name String
prefix String @default("!") prefix String @default("!")
staff Json?
// Relaciones // Relaciones
alliances Alliance[] alliances Alliance[]

View File

@@ -12,6 +12,7 @@ import {listVariables} from "../../../core/lib/vars";
import type Amayo from "../../../core/client"; import type Amayo from "../../../core/client";
import {BlockState, DisplayComponentUtils, EditorActionRow} from "../../../core/types/displayComponentEditor"; import {BlockState, DisplayComponentUtils, EditorActionRow} from "../../../core/types/displayComponentEditor";
import type {DisplayComponentContainer} from "../../../core/types/displayComponents"; import type {DisplayComponentContainer} from "../../../core/types/displayComponents";
import { hasManageGuildOrStaff } from "../../../core/lib/permissions";
interface EditorData { interface EditorData {
content?: string; content?: string;
@@ -51,8 +52,9 @@ export const command: CommandMessage = {
category: "Alianzas", category: "Alianzas",
usage: "crear-embed <nombre>", usage: "crear-embed <nombre>",
run: async (message, args, client) => { run: async (message, args, client) => {
if (!message.member?.permissions.has("Administrator")) { const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, client.prisma);
await message.reply("❌ No tienes permisos de Administrador."); if (!allowed) {
await message.reply("❌ No tienes permisos de ManageGuild ni rol de staff.");
return; return;
} }
@@ -719,7 +721,7 @@ async function handleSaveBlock(
}); });
await interaction.reply({ 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 flags: MessageFlags.Ephemeral
}); });

View File

@@ -10,6 +10,7 @@ import {
import { CommandMessage } from "../../../core/types/commands"; import { CommandMessage } from "../../../core/types/commands";
import type Amayo from "../../../core/client"; import type Amayo from "../../../core/client";
import type { JsonValue } from "@prisma/client/runtime/library"; import type { JsonValue } from "@prisma/client/runtime/library";
import { hasManageGuildOrStaff } from "../../../core/lib/permissions";
interface BlockItem { interface BlockItem {
name: string; name: string;
@@ -22,16 +23,17 @@ interface ActionRowBuilder {
} }
export const command: CommandMessage = { export const command: CommandMessage = {
name: "eliminar-embed", name: "eliminar-bloque",
type: "message", type: "message",
aliases: ["embed-eliminar", "borrar-embed", "embeddelete"], aliases: ["bloque-eliminar", "bloque-embed", "blockdelete"],
cooldown: 10, cooldown: 10,
description: "Elimina bloques DisplayComponents del servidor", description: "Elimina bloques DisplayComponents del servidor",
category: "Alianzas", category: "Creacion",
usage: "eliminar-embed [nombre_bloque]", usage: "eliminar-bloque [nombre_bloque]",
run: async (message: Message, args: string[], client: Amayo): Promise<void> => { run: async (message: Message, args: string[], client: Amayo): Promise<void> => {
if (!message.member?.permissions.has("Administrator")) { const allowed = await hasManageGuildOrStaff(message.member, message.guildId!, client.prisma);
await message.reply("❌ No tienes permisos de Administrador."); if (!allowed) {
await message.reply("❌ No tienes permisos de ManageGuild ni rol de staff.");
return; return;
} }

View File

@@ -16,6 +16,7 @@ import type {
} from "../../../core/types/displayComponents"; } from "../../../core/types/displayComponents";
import type Amayo from "../../../core/client"; import type Amayo from "../../../core/client";
import type { JsonValue } from "@prisma/client/runtime/library"; import type { JsonValue } from "@prisma/client/runtime/library";
import { hasManageGuildOrStaff } from "../../../core/lib/permissions";
interface BlockListItem { interface BlockListItem {
name: string; name: string;
@@ -29,17 +30,17 @@ interface ActionRowBuilder {
} }
export const command: CommandMessage = { export const command: CommandMessage = {
name: "lista-embeds", name: "lista-bloques",
type: "message", type: "message",
aliases: ["embeds", "ver-embeds", "embedlist"], aliases: ["bloques", "ver-bloques", "blocks"],
cooldown: 10, cooldown: 10,
description: "Muestra todos los bloques DisplayComponents configurados en el servidor", description: "Muestra todos los bloques DisplayComponents configurados en el servidor",
category: "Alianzas", category: "Alianzas",
usage: "lista-embeds", usage: "lista-bloques",
run: async (message: Message, args: string[], client: Amayo): Promise<void> => { run: async (message: Message, args: string[], client: Amayo): Promise<void> => {
// Permission check const allowed = await hasManageGuildOrStaff(message.member, message.guildId!, client.prisma);
if (!message.member?.permissions.has("Administrator")) { if (!allowed) {
await message.reply("❌ No tienes permisos de Administrador."); await message.reply("❌ No tienes permisos de ManageGuild ni rol de staff.");
return; return;
} }

View File

@@ -2,6 +2,7 @@ import { CommandMessage } from "../../../core/types/commands";
import { MessageFlags } from "discord.js"; import { MessageFlags } from "discord.js";
import { ComponentType, ButtonStyle, TextInputStyle } from "discord-api-types/v10"; import { ComponentType, ButtonStyle, TextInputStyle } from "discord-api-types/v10";
import { replaceVars, isValidUrlOrVariable, listVariables } from "../../../core/lib/vars"; import { replaceVars, isValidUrlOrVariable, listVariables } from "../../../core/lib/vars";
import { hasManageGuildOrStaff } from "../../../core/lib/permissions";
// Botones de edición (máx 5 por fila) // Botones de edición (máx 5 por fila)
const btns = (disabled = false) => ([ const btns = (disabled = false) => ([
@@ -134,8 +135,9 @@ export const command: CommandMessage = {
category: "Alianzas", category: "Alianzas",
usage: "editar-embed <nombre>", usage: "editar-embed <nombre>",
run: async (message, args, client) => { run: async (message, args, client) => {
if (!message.member?.permissions.has("Administrator")) { const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, client.prisma);
await message.reply("❌ No tienes permisos de Administrador."); if (!allowed) {
await message.reply("❌ No tienes permisos de ManageGuild ni rol de staff.");
return; return;
} }
@@ -149,7 +151,7 @@ export const command: CommandMessage = {
where: { guildId: message.guild!.id, name: blockName } where: { guildId: message.guild!.id, name: blockName }
}); });
if (!existingBlock) { if (!existingBlock) {
await message.reply("❌ Block no encontrado. Usa `!blockcreatev2 <nombre>` para crear uno nuevo."); await message.reply("❌ Block no encontrado. Usa `!editar-bloque <nombre>` para crear uno nuevo.");
return; return;
} }

View File

@@ -1,9 +1,10 @@
// Comando para mostrar el leaderboard de alianzas con botón de refresco // Comando para mostrar el leaderboard de alianzas con botón de refresco
// @ts-ignore // @ts-ignore
import { CommandMessage } from "../../../core/types/commands"; import { CommandMessage } from "../../../core/types/commands";
import { PermissionFlagsBits } from "discord.js";
import { hasManageGuildOrStaff } from '../../../core/lib/permissions';
import { prisma } from "../../../core/database/prisma"; import { prisma } from "../../../core/database/prisma";
import type { Message } from "discord.js"; import type { Message } from "discord.js";
import { PermissionFlagsBits } from "discord.js";
const MAX_ENTRIES = 10; const MAX_ENTRIES = 10;
@@ -236,7 +237,7 @@ export const command: CommandMessage = {
aliases: ['ld'], aliases: ['ld'],
cooldown: 5, cooldown: 5,
description: 'Muestra el leaderboard de alianzas (semanal, mensual y total) con botón de refresco.', description: 'Muestra el leaderboard de alianzas (semanal, mensual y total) con botón de refresco.',
category: 'Utilidad', category: 'Alianzas',
usage: 'leaderboard', usage: 'leaderboard',
run: async (message) => { run: async (message) => {
if (!message.guild) { if (!message.guild) {
@@ -244,9 +245,9 @@ export const command: CommandMessage = {
return; 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 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); const panel = await buildLeaderboardPanel(message, isAdmin);
await message.reply({ await message.reply({

View File

@@ -263,13 +263,13 @@ async function buildChannelListPanel(message: Message) {
} }
export const command: CommandMessage = { export const command: CommandMessage = {
name: "listar-canales-alianza-v2", name: "lista-canales",
type: "message", type: "message",
aliases: ["listchannels-alliance-v2", "listalchannel-v2", "channelsally-v2", "alliancechannels-v2"], aliases: ["lca", "channelist", "alliacechannels"],
cooldown: 5, cooldown: 5,
description: "Lista todos los canales configurados para alianzas (versión V2 con components)", description: "Lista todos los canales configurados para alianzas (versión V2 con components)",
category: "Alianzas", category: "Alianzas",
usage: "listar-canales-alianza-v2", usage: "lista-canales",
run: async (message) => { run: async (message) => {
if (!message.guild) { if (!message.guild) {
await message.reply({ content: '❌ Este comando solo puede usarse en servidores.' }); await message.reply({ content: '❌ Este comando solo puede usarse en servidores.' });

View File

@@ -2,16 +2,18 @@ import logger from "../../../core/lib/logger";
import { CommandMessage } from "../../../core/types/commands"; import { CommandMessage } from "../../../core/types/commands";
// @ts-ignore // @ts-ignore
import { EmbedBuilder, ButtonStyle, MessageFlags, ChannelType } from "discord.js"; import { EmbedBuilder, ButtonStyle, MessageFlags, ChannelType } from "discord.js";
import { hasManageGuildOrStaff } from "../../../core/lib/permissions";
export const command: CommandMessage = { export const command: CommandMessage = {
name: "eliminar-canal-alianza", name: "eliminar-canal",
type: "message", type: "message",
aliases: ["removechannel-alliance", "removealchannel", "delalchannel"], aliases: ["removechannel-alliance", "removealchannel", "delalchannel"],
cooldown: 10, cooldown: 10,
// @ts-ignore // @ts-ignore
run: async (message, args, client) => { run: async (message, args, client) => {
if (!message.member?.permissions.has("Administrator")) { const allowed = await hasManageGuildOrStaff(message.member, message.guildId!, client.prisma);
return message.reply("❌ No tienes permisos de Administrador."); if (!allowed) {
return message.reply("❌ No tienes permisos de ManageGuild ni rol de staff.");
} }
// Obtener canales configurados existentes // Obtener canales configurados existentes

View File

@@ -3,6 +3,7 @@ import { MessageFlags } from "discord.js";
import { DisplayComponentUtils } from "../../../core/types/displayComponentEditor"; import { DisplayComponentUtils } from "../../../core/types/displayComponentEditor";
import { sendComponentsV2Message } from "../../../core/api/discordAPI"; import { sendComponentsV2Message } from "../../../core/api/discordAPI";
import logger from "../../../core/lib/logger"; import logger from "../../../core/lib/logger";
import { hasManageGuildOrStaff } from "../../../core/lib/permissions";
export const command: CommandMessage = { export const command: CommandMessage = {
name: "send-embed", name: "send-embed",
@@ -13,9 +14,9 @@ export const command: CommandMessage = {
category: "Alianzas", category: "Alianzas",
usage: "send-embed <nombre>", usage: "send-embed <nombre>",
run: async (message, args, client) => { run: async (message, args, client) => {
// Requiere administrador para evitar abuso mientras se prueba const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, client.prisma);
if (!message.member?.permissions.has("Administrator")) { if (!allowed) {
await message.reply("❌ No tienes permisos de Administrador."); await message.reply("❌ No tienes permisos de ManageGuild ni rol de staff.");
return; return;
} }
@@ -35,7 +36,6 @@ export const command: CommandMessage = {
return; return;
} }
// Renderizamos usando la misma utilidad del editor (fuente: node_modules/discord.js APIs via repo util DisplayComponentUtils)
const container = await DisplayComponentUtils.renderPreview( const container = await DisplayComponentUtils.renderPreview(
// @ts-ignore - guardamos BlockState como config // @ts-ignore - guardamos BlockState como config
existingBlock.config, existingBlock.config,
@@ -43,7 +43,6 @@ export const command: CommandMessage = {
message.guild! message.guild!
); );
// Enviamos como Components v2 (fuente: repositorio local sendComponentsV2Message)
await sendComponentsV2Message(message.channel.id, { await sendComponentsV2Message(message.channel.id, {
components: [container as any], components: [container as any],
replyToMessageId: message.id replyToMessageId: message.id
@@ -59,4 +58,3 @@ export const command: CommandMessage = {
} }
} }
}; };

View File

@@ -1,16 +1,21 @@
import { CommandMessage } from "../../../core/types/commands"; import { CommandMessage } from "../../../core/types/commands";
// @ts-ignore // @ts-ignore
import { ComponentType, ButtonStyle, MessageFlags, ChannelType } from "discord.js"; import { ComponentType, ButtonStyle, MessageFlags, ChannelType } from "discord.js";
import { hasManageGuildOrStaff } from "../../../core/lib/permissions";
export const command: CommandMessage = { export const command: CommandMessage = {
name: "canal-alianza", name: "canal-alianza",
type: "message", type: "message",
aliases: ["alchannel", "channelally"], aliases: ["alchannel", "channelally"],
description: "Configura canales para el sistema de alianzas con bloques DisplayComponents.",
usage: "canal-alianza",
category: "Alianzas",
cooldown: 10, cooldown: 10,
// @ts-ignore // @ts-ignore
run: async (message, args, client) => { run: async (message, args, client) => {
if (!message.member?.permissions.has("Administrator")) { const allowed = await hasManageGuildOrStaff(message.member, message.guildId!, client.prisma);
return message.reply("❌ No tienes permisos de Administrador."); if (!allowed) {
return message.reply("❌ No tienes permisos de ManageGuild ni rol de staff.");
} }
// Obtener canales configurados existentes // Obtener canales configurados existentes

View File

@@ -1,17 +1,25 @@
import logger from "../../../core/lib/logger"; import logger from "../../../core/lib/logger";
import { CommandMessage } from "../../../core/types/commands"; 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 = { export const command: CommandMessage = {
name: 'configuracion', name: 'configuracion',
type: "message", type: "message",
aliases: ['config', 'ajustes', 'settings'], aliases: ['config', 'ajustes', 'settings'],
cooldown: 5, 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', category: 'Configuración',
usage: 'configuracion', usage: 'configuracion',
run: async (message, args, client) => { run: async (message, args, client) => {
if (!message.member?.permissions.has("Administrator")) { const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, client.prisma);
await message.reply("❌ No tienes permisos de Administrador."); if (!allowed) {
await message.reply("❌ No tienes permisos de ManageGuild ni rol de staff.");
return; return;
} }
@@ -20,52 +28,48 @@ export const command: CommandMessage = {
}); });
const currentPrefix = server?.prefix || "!"; 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 // Panel de configuración usando DisplayComponents
const settingsPanel = { const settingsPanel = {
type: 17, type: 17,
accent_color: 6178018, // Color del ejemplo accent_color: 6178018, // Color del ejemplo
components: [ components: [
{ type: 10, content: "### <:invisible:1418684224441028608> 梅panel admin📢\n" },
{ type: 14, spacing: 1, divider: false },
{ type: 10, content: "Configuracion del Servidor:" },
{ {
type: 10, type: 9,
content: "### <:invisible:1418684224441028608> 梅panel admin📢\n" components: [ { type: 10, content: `**Prefix:**<:invisible:1418684224441028608>\`${currentPrefix}\`` } ],
},
{
type: 14,
spacing: 1,
divider: false
},
{
type: 10,
content: "Configuracion del Servidor:"
},
{
type: 9, // Section
components: [
{
type: 10,
content: `**Prefix:**<:invisible:1418684224441028608>\`${currentPrefix}\``
}
],
accessory: { accessory: {
type: 2, // Button type: 2,
style: 2, // Secondary style: 2,
emoji: { emoji: { name: "⚙️" },
name: "⚙️"
},
custom_id: "open_prefix_modal", custom_id: "open_prefix_modal",
label: "Cambiar" label: "Cambiar"
} }
}, },
{ type: 14, divider: false },
{ {
type: 14, type: 9,
divider: false 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({ const panelMessage = await message.reply({
flags: 32768, // SuppressEmbeds flags: 32768, // Components v2
components: [settingsPanel] components: [settingsPanel]
}); });
@@ -81,44 +85,15 @@ export const command: CommandMessage = {
title: "⚙️ Configurar Prefix del Servidor", title: "⚙️ Configurar Prefix del Servidor",
custom_id: "prefix_settings_modal", custom_id: "prefix_settings_modal",
components: [ components: [
{ { 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, // ActionRow { 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 } ] }
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
}
]
}
] ]
}; };
await interaction.showModal(prefixModal); await interaction.showModal(prefixModal);
// Crear un collector específico para este modal
const modalCollector = interaction.awaitModalSubmit({ const modalCollector = interaction.awaitModalSubmit({
time: 300000, // 5 minutos time: 300000,
filter: (modalInt: any) => modalInt.customId === "prefix_settings_modal" && modalInt.user.id === message.author.id 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 newPrefix = modalInteraction.fields.getTextInputValue("new_prefix_input");
const description = modalInteraction.fields.getTextInputValue("prefix_description") || "Sin descripción"; const description = modalInteraction.fields.getTextInputValue("prefix_description") || "Sin descripción";
// Validar prefix
if (!newPrefix || newPrefix.length > 10) { if (!newPrefix || newPrefix.length > 10) {
await modalInteraction.reply({ await modalInteraction.reply({ content: "❌ **Error:** El prefix debe tener entre 1 y 10 caracteres.", flags: 64 });
content: "❌ **Error:** El prefix debe tener entre 1 y 10 caracteres.",
flags: 64 // Ephemeral
});
return; return;
} }
try { try {
// Actualizar prefix en la base de datos
await client.prisma.guild.upsert({ await client.prisma.guild.upsert({
where: { id: message.guild!.id }, where: { id: message.guild!.id },
create: { create: { id: message.guild!.id, name: message.guild!.name, prefix: newPrefix },
id: message.guild!.id, update: { prefix: newPrefix, name: message.guild!.name }
name: message.guild!.name,
prefix: newPrefix
},
update: {
prefix: newPrefix,
name: message.guild!.name
}
}); });
// Panel de confirmación
const successPanel = { const successPanel = {
type: 17, type: 17,
accent_color: 3066993, // Verde accent_color: 3066993,
components: [ components: [
{ { type: 10, content: "### ✅ **Prefix Actualizado Exitosamente**" },
type: 10, { type: 14, spacing: 2, divider: true },
content: "### ✅ **Prefix Actualizado Exitosamente**" { 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`" }
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`"
}
] ]
}; };
// Botón para volver al panel principal const backToSettingsRow = { type: 1, components: [ { type: 2, style: 2, label: "↩️ Volver a Configuración", custom_id: "back_to_settings" } ] };
const backToSettingsRow = {
type: 1,
components: [
{
type: 2,
style: 2, // Secondary
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) { } catch (error) {
const errorPanel = { const errorPanel = {
type: 17, type: 17,
accent_color: 15548997, // Rojo accent_color: 15548997,
components: [ components: [
{ { type: 10, content: "### ❌ **Error al Actualizar Prefix**" },
type: 10, { type: 14, spacing: 2, divider: true },
content: "### ❌ **Error al Actualizar Prefix**" { 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: 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 = { 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" } ] };
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"
}
]
};
await modalInteraction.update({ await modalInteraction.update({ components: [errorPanel, retryRow] });
components: [errorPanel, retryRow]
});
} }
}).catch(async (error: any) => { }).catch(async (error: any) => {
// Modal timeout o cancelado
logger.info("Modal timeout o error:", error.message); 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 // Manejar botones adicionales
if (interaction.customId === "back_to_settings") { 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 newCurrentPrefix = updatedServer?.prefix || "!";
const staffRoles2: string[] = toStringArray(updatedServer?.staff);
const staffDisplay2 = staffRoles2.length ? staffRoles2.map((id) => `<@&${id}>`).join(', ') : 'Sin staff configurado';
const updatedSettingsPanel = { const updatedSettingsPanel = {
type: 17, type: 17,
accent_color: 6178018, accent_color: 6178018,
components: [ components: [
{ { type: 10, content: "### <:invisible:1418684224441028608> 梅panel admin📢\n" },
type: 10, { type: 14, spacing: 1, divider: false },
content: "### <:invisible:1418684224441028608> 梅panel admin📢\n" { 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: 14, { type: 9, components: [ { type: 10, content: `**Staff (roles):** ${staffDisplay2}` } ], accessory: { type: 2, style: 2, emoji: { name: "🛡️" }, custom_id: "open_staff_modal", label: "Configurar" } },
spacing: 1, { type: 14, divider: false }
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
}
] ]
}; };
await interaction.update({ await interaction.update({ components: [updatedSettingsPanel] });
components: [updatedSettingsPanel]
});
} }
if (interaction.customId === "cancel_prefix_change") { if (interaction.customId === "cancel_prefix_change") {
// Volver al panel original sin cambios // Volver al panel original
await interaction.update({ const updatedServer = await client.prisma.guild.findFirst({ where: { id: message.guild!.id } });
components: [settingsPanel] 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") { if (reason === "time") {
const timeoutPanel = { const timeoutPanel = {
type: 17, type: 17,
accent_color: 6178018, accent_color: 6178018,
components: [ components: [
{ { type: 10, content: "### ⏰ **Panel Expirado**" },
type: 10, { type: 14, spacing: 1, divider: true },
content: "### ⏰ **Panel Expirado**" { type: 10, content: "El panel de configuración ha expirado por inactividad.\n\nUsa `!settings` para abrir un nuevo panel." }
},
{
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 { try {
await panelMessage.edit({ await panelMessage.edit({ components: [timeoutPanel] });
components: [timeoutPanel]
});
} catch (error) { } catch (error) {
// Mensaje eliminado o error de edición // Mensaje eliminado o error de edición
} }

View File

@@ -1,11 +1,11 @@
import logger from "../../core/lib/logger"; import logger from "../../core/lib/logger";
import { import {
ButtonInteraction, ButtonInteraction,
MessageFlags, MessageFlags
PermissionFlagsBits
} from 'discord.js'; } from 'discord.js';
import { prisma } from '../../core/database/prisma'; import { prisma } from '../../core/database/prisma';
import { ComponentType, TextInputStyle } from 'discord-api-types/v10'; import { ComponentType, TextInputStyle } from 'discord-api-types/v10';
import { hasManageGuildOrStaff } from "../../core/lib/permissions";
export default { export default {
customId: 'ld_manage_points', 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); 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({ return interaction.reply({
content: '❌ Solo los administradores pueden gestionar puntos.', content: '❌ Solo admins o staff pueden gestionar puntos.',
flags: MessageFlags.Ephemeral flags: MessageFlags.Ephemeral
}); });
} }

View File

@@ -1,6 +1,8 @@
import logger from "../../core/lib/logger"; 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 { buildLeaderboardPanel } from '../../commands/messages/alliaces/leaderboard';
import { hasManageGuildOrStaff } from "../../core/lib/permissions";
import { prisma } from "../../core/database/prisma";
export default { export default {
customId: 'ld_refresh', customId: 'ld_refresh',
@@ -11,9 +13,9 @@ export default {
try { try {
await interaction.deferUpdate(); 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 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 // Reusar el builder esperando un objeto con guild y author
const fakeMessage: any = { guild: interaction.guild, author: interaction.user }; const fakeMessage: any = { guild: interaction.guild, author: interaction.user };

View File

@@ -2,13 +2,13 @@ import logger from "../../core/lib/logger";
import { import {
ModalSubmitInteraction, ModalSubmitInteraction,
MessageFlags, MessageFlags,
PermissionFlagsBits,
EmbedBuilder, EmbedBuilder,
User, User,
Collection, Collection,
Snowflake Snowflake
} from 'discord.js'; } from 'discord.js';
import { prisma } from '../../core/database/prisma'; import { prisma } from '../../core/database/prisma';
import { hasManageGuildOrStaff } from "../../core/lib/permissions";
interface UserSelectComponent { interface UserSelectComponent {
custom_id: string; 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); 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({ return interaction.reply({
content: '❌ Solo los administradores pueden gestionar puntos.', content: '❌ Solo admins o staff pueden gestionar puntos.',
flags: MessageFlags.Ephemeral flags: MessageFlags.Ephemeral
}); });
} }

View File

@@ -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<boolean> {
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;
}
}