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

@@ -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 <nombre>",
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
});

View File

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

View File

@@ -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<void> => {
// 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;
}

View File

@@ -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 <nombre>",
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 <nombre>` para crear uno nuevo.");
await message.reply("❌ Block no encontrado. Usa `!editar-bloque <nombre>` para crear uno nuevo.");
return;
}

View File

@@ -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({

View File

@@ -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.' });

View File

@@ -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

View File

@@ -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 <nombre>",
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 = {
}
}
};

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
});
}

View File

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

View File

@@ -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
});
}

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;
}
}