feat(settings): invalidate guild cache after settings updates and improve settings panel UX

This commit is contained in:
2025-10-07 10:52:47 -05:00
parent a9087261ca
commit 6a9135c03a

View File

@@ -3,94 +3,118 @@ import { CommandMessage } from "../../../core/types/commands";
import { ComponentType, TextInputStyle } from "discord-api-types/v10";
import { hasManageGuildOrStaff } from "../../../core/lib/permissions";
import { aiService } from "../../../core/services/AIService";
import { invalidateGuildCache } from "../../../core/database/guildCache";
function toStringArray(input: unknown): string[] {
if (!Array.isArray(input)) return [];
return (input as unknown[]).filter((v): v is string => typeof v === 'string');
return (input as unknown[]).filter((v): v is string => typeof v === "string");
}
export const command: CommandMessage = {
name: 'configuracion',
name: "configuracion",
type: "message",
aliases: ['config', 'ajustes', 'settings'],
aliases: ["config", "ajustes", "settings"],
cooldown: 5,
description: 'Abre el panel de configuración del servidor (prefix, staff y más).',
category: 'Configuración',
usage: 'configuracion',
description:
"Abre el panel de configuración del servidor (prefix, staff y más).",
category: "Configuración",
usage: "configuracion",
run: async (message, args, client) => {
const allowed = await hasManageGuildOrStaff(message.member, message.guild!.id, client.prisma);
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.");
await message.reply(
"❌ No tienes permisos de ManageGuild ni rol de staff."
);
return;
}
const server = await client.prisma.guild.findFirst({
where: { id: message.guild!.id }
where: { id: message.guild!.id },
});
const currentPrefix = server?.prefix || "!";
const staffRoles: string[] = toStringArray(server?.staff);
const staffDisplay = staffRoles.length
? staffRoles.map((id) => `<@&${id}>`).join(', ')
: 'Sin staff configurado';
? staffRoles.map((id) => `<@&${id}>`).join(", ")
: "Sin staff configurado";
const aiRolePrompt = server?.aiRolePrompt ?? null;
const aiPreview = aiRolePrompt ? (aiRolePrompt.length > 80 ? aiRolePrompt.slice(0, 77) + '…' : aiRolePrompt) : 'No configurado';
const aiPreview = aiRolePrompt
? aiRolePrompt.length > 80
? aiRolePrompt.slice(0, 77) + "…"
: aiRolePrompt
: "No 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: 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:**<:invisible:1418684224441028608>\`${currentPrefix}\`` } ],
components: [
{
type: 10,
content: `**Prefix:**<:invisible:1418684224441028608>\`${currentPrefix}\``,
},
],
accessory: {
type: 2,
style: 2,
emoji: { name: "⚙️" },
custom_id: "open_prefix_modal",
label: "Cambiar"
}
label: "Cambiar",
},
},
{ type: 14, divider: false },
{
type: 9,
components: [ { type: 10, content: `**Staff (roles):** ${staffDisplay}` } ],
components: [
{ type: 10, content: `**Staff (roles):** ${staffDisplay}` },
],
accessory: {
type: 2,
style: 2, // Secondary
emoji: { name: "🛡️" },
custom_id: "open_staff_modal",
label: "Configurar"
}
label: "Configurar",
},
},
{ type: 14, divider: false },
{
type: 9,
components: [ { type: 10, content: `**AI Role Prompt:** ${aiPreview}` } ],
components: [
{ type: 10, content: `**AI Role Prompt:** ${aiPreview}` },
],
accessory: {
type: 2,
style: 2,
emoji: { name: "🧠" },
custom_id: "open_ai_role_modal",
label: "Configurar"
}
label: "Configurar",
},
{ type: 14, divider: false }
]
},
{ type: 14, divider: false },
],
};
const panelMessage = await message.reply({
flags: 32768, // Components v2
components: [settingsPanel]
components: [settingsPanel],
});
const collector = panelMessage.createMessageComponentCollector({
time: 300000, // 5 minutos
filter: (i: any) => i.user.id === message.author.id
filter: (i: any) => i.user.id === message.author.id,
});
collector.on("collect", async (interaction: any) => {
@@ -111,8 +135,8 @@ export const command: CommandMessage = {
required: true,
maxLength: 10,
minLength: 1,
value: currentPrefix
}
value: currentPrefix,
},
},
{
type: ComponentType.Label,
@@ -123,56 +147,115 @@ export const command: CommandMessage = {
style: TextInputStyle.Paragraph,
placeholder: "Ej: evitar conflictos con otros bots...",
required: false,
maxLength: 200
}
}
]
maxLength: 200,
},
},
],
} as const;
try {
await interaction.showModal(prefixModal);
} catch (err) {
try { await interaction.reply({ content: '❌ No se pudo abrir el modal de prefix.', flags: 64 }); } catch {}
try {
await interaction.reply({
content: "❌ No se pudo abrir el modal de prefix.",
flags: 64,
});
} catch {}
return;
}
try {
const modalInteraction = await interaction.awaitModalSubmit({
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,
});
const newPrefix = modalInteraction.components.getTextInputValue("new_prefix_input");
const description = modalInteraction.components.getTextInputValue("prefix_description") || "Sin descripción";
const newPrefix =
modalInteraction.components.getTextInputValue("new_prefix_input");
const description =
modalInteraction.components.getTextInputValue(
"prefix_description"
) || "Sin descripción";
if (!newPrefix || newPrefix.length > 10) {
await modalInteraction.reply({ content: "❌ **Error:** El prefix debe tener entre 1 y 10 caracteres.", flags: 64 });
await modalInteraction.reply({
content:
"❌ **Error:** El prefix debe tener entre 1 y 10 caracteres.",
flags: 64,
});
return;
}
try {
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 },
});
// Invalidar el caché del guild para reflejar el cambio
await invalidateGuildCache(message.guild!.id);
const successPanel = {
type: 17,
accent_color: 3066993,
components: [
{ type: 10, content: "### ✅ **Prefix Actualizado Exitosamente**" },
{
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: 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: 10,
content:
"🚀 **¡Listo!** Ahora puedes usar los comandos con el nuevo prefix.\n\n💡 **Ejemplo:** `" +
newPrefix +
"help`, `" +
newPrefix +
"embedlist`",
},
],
};
const backToSettingsRow = { type: 1, components: [ { type: 2, style: 2, label: "↩️ Volver a Configuración", custom_id: "back_to_settings" } ] };
await modalInteraction.update({ components: [successPanel, backToSettingsRow] });
const backToSettingsRow = {
type: 1,
components: [
{
type: 2,
style: 2,
label: "↩️ Volver a Configuración",
custom_id: "back_to_settings",
},
],
};
await modalInteraction.update({
components: [successPanel, backToSettingsRow],
});
} catch (error) {
const errorPanel = {
type: 17,
@@ -180,16 +263,40 @@ export const command: CommandMessage = {
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:** 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, 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 (error: any) {
logger.info("Modal timeout o error:", error?.message || String(error));
logger.info(
"Modal timeout o error:",
error?.message || String(error)
);
}
}
@@ -199,25 +306,50 @@ export const command: CommandMessage = {
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..." } }
]
{
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 modalInteraction = await interaction.awaitModalSubmit({
time: 300000,
});
const selected =
modalInteraction.components.getSelectedRoles("staff_roles");
//@ts-ignore
const roleIds: string[] = selected ? Array.from(selected.keys()).slice(0, 3) : [];
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 }
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';
// Invalidar el caché del guild para reflejar el cambio
await invalidateGuildCache(message.guild!.id);
const updatedDisplay = roleIds.length
? roleIds.map((id) => `<@&${id}>`).join(", ")
: "Sin staff configurado";
const successPanel = {
type: 17,
@@ -225,20 +357,37 @@ export const command: CommandMessage = {
components: [
{ type: 10, content: "### ✅ **Staff Actualizado**" },
{ type: 14, spacing: 2, divider: true },
{ type: 10, content: `**Nuevos roles de staff:** ${updatedDisplay}` }
]
{
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] });
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
}
}
if (interaction.customId === "open_ai_role_modal") {
const currentServer = await client.prisma.guild.findFirst({ where: { id: message.guild!.id } });
const currentAiPrompt = currentServer?.aiRolePrompt ?? '';
const currentServer = await client.prisma.guild.findFirst({
where: { id: message.guild!.id },
});
const currentAiPrompt = currentServer?.aiRolePrompt ?? "";
const aiModal = {
title: "🧠 Configurar AI Role Prompt",
customId: "ai_role_prompt_modal",
@@ -251,41 +400,63 @@ export const command: CommandMessage = {
customId: "ai_role_prompt_input",
style: TextInputStyle.Paragraph,
required: false,
placeholder: "Ej: Eres un asistente amistoso del servidor, responde en español, evita spoilers...",
placeholder:
"Ej: Eres un asistente amistoso del servidor, responde en español, evita spoilers...",
maxLength: 1500,
value: currentAiPrompt.slice(0, 1500)
}
}
]
value: currentAiPrompt.slice(0, 1500),
},
},
],
} as const;
try {
await interaction.showModal(aiModal);
} catch (err) {
try { await interaction.reply({ content: '❌ No se pudo abrir el modal de AI.', flags: 64 }); } catch {}
try {
await interaction.reply({
content: "❌ No se pudo abrir el modal de AI.",
flags: 64,
});
} catch {}
return;
}
try {
const modalInteraction = await interaction.awaitModalSubmit({
time: 300000,
filter: (m: any) => m.customId === 'ai_role_prompt_modal' && m.user.id === message.author.id
filter: (m: any) =>
m.customId === "ai_role_prompt_modal" &&
m.user.id === message.author.id,
});
const newPromptRaw = modalInteraction.components.getTextInputValue('ai_role_prompt_input') ?? '';
const newPromptRaw =
modalInteraction.components.getTextInputValue(
"ai_role_prompt_input"
) ?? "";
const newPrompt = newPromptRaw.trim();
const toSave: string | null = newPrompt.length > 0 ? newPrompt : null;
await client.prisma.guild.upsert({
where: { id: message.guild!.id },
create: { id: message.guild!.id, name: message.guild!.name, aiRolePrompt: toSave },
update: { aiRolePrompt: toSave, name: message.guild!.name }
create: {
id: message.guild!.id,
name: message.guild!.name,
aiRolePrompt: toSave,
},
update: { aiRolePrompt: toSave, name: message.guild!.name },
});
// Invalida el cache del servicio para reflejar cambios al instante
aiService.invalidateGuildConfig(message.guild!.id);
const preview = toSave ? (toSave.length > 200 ? toSave.slice(0, 197) + '…' : toSave) : 'Prompt eliminado (sin configuración)';
// Invalidar el caché del guild también
await invalidateGuildCache(message.guild!.id);
const preview = toSave
? toSave.length > 200
? toSave.slice(0, 197) + "…"
: toSave
: "Prompt eliminado (sin configuración)";
const successPanel = {
type: 17,
@@ -293,12 +464,24 @@ export const command: CommandMessage = {
components: [
{ type: 10, content: "### ✅ **AI Role Prompt Actualizado**" },
{ type: 14, spacing: 2, divider: true },
{ type: 10, content: `**Nuevo valor:**\n${preview}` }
]
{ type: 10, content: `**Nuevo valor:**\n${preview}` },
],
};
const backRow = {
type: 1,
components: [
{
type: 2,
style: 2,
label: "↩️ Volver a Configuración",
custom_id: "back_to_settings",
},
],
};
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] });
await modalInteraction.update({
components: [successPanel, backRow],
});
} catch (e) {
// timeout o cancelado
}
@@ -306,27 +489,75 @@ export const command: CommandMessage = {
// Manejar botones adicionales
if (interaction.customId === "back_to_settings") {
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 staffDisplay2 = staffRoles2.length
? staffRoles2.map((id) => `<@&${id}>`).join(", ")
: "Sin staff configurado";
const aiRolePrompt2 = updatedServer?.aiRolePrompt ?? null;
const aiPreview2 = aiRolePrompt2 ? (aiRolePrompt2.length > 80 ? aiRolePrompt2.slice(0, 77) + '…' : aiRolePrompt2) : 'No configurado';
const aiPreview2 = aiRolePrompt2
? aiRolePrompt2.length > 80
? aiRolePrompt2.slice(0, 77) + "…"
: aiRolePrompt2
: "No configurado";
const updatedSettingsPanel = {
type: 17,
accent_color: 6178018,
components: [
{ type: 10, content: "### <:invisible:1418684224441028608> 梅panel admin📢\n" },
{
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: 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: 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 },
{ type: 9, components: [ { type: 10, content: `**AI Role Prompt:** ${aiPreview2}` } ], accessory: { type: 2, style: 2, emoji: { name: "🧠" }, custom_id: "open_ai_role_modal", label: "Configurar" } },
{ type: 14, divider: false }
]
{
type: 9,
components: [
{ type: 10, content: `**AI Role Prompt:** ${aiPreview2}` },
],
accessory: {
type: 2,
style: 2,
emoji: { name: "🧠" },
custom_id: "open_ai_role_modal",
label: "Configurar",
},
},
{ type: 14, divider: false },
],
};
await interaction.update({ components: [updatedSettingsPanel] });
@@ -334,26 +565,74 @@ export const command: CommandMessage = {
if (interaction.customId === "cancel_prefix_change") {
// Volver al panel original
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 staffRoles3: string[] = toStringArray(updatedServer?.staff);
const staffDisplay3 = staffRoles3.length ? staffRoles3.map((id) => `<@&${id}>`).join(', ') : 'Sin staff configurado';
const staffDisplay3 = staffRoles3.length
? staffRoles3.map((id) => `<@&${id}>`).join(", ")
: "Sin staff configurado";
const aiRolePrompt3 = updatedServer?.aiRolePrompt ?? null;
const aiPreview3 = aiRolePrompt3 ? (aiRolePrompt3.length > 80 ? aiRolePrompt3.slice(0, 77) + '…' : aiRolePrompt3) : 'No configurado';
const aiPreview3 = aiRolePrompt3
? aiRolePrompt3.length > 80
? aiRolePrompt3.slice(0, 77) + "…"
: aiRolePrompt3
: "No configurado";
const originalPanel = {
type: 17,
accent_color: 6178018,
components: [
{ type: 10, content: "### <:invisible:1418684224441028608> 梅panel admin📢\n" },
{
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: 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: 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 },
{ type: 9, components: [ { type: 10, content: `**AI Role Prompt:** ${aiPreview3}` } ], accessory: { type: 2, style: 2, emoji: { name: "🧠" }, custom_id: "open_ai_role_modal", label: "Configurar" } },
{ type: 14, divider: false }
]
{
type: 9,
components: [
{ type: 10, content: `**AI Role Prompt:** ${aiPreview3}` },
],
accessory: {
type: 2,
style: 2,
emoji: { name: "🧠" },
custom_id: "open_ai_role_modal",
label: "Configurar",
},
},
{ type: 14, divider: false },
],
};
await interaction.update({ components: [originalPanel] });
@@ -368,8 +647,12 @@ export const command: CommandMessage = {
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:
"El panel de configuración ha expirado por inactividad.\n\nUsa `!settings` para abrir un nuevo panel.",
},
],
};
try {
@@ -379,5 +662,5 @@ export const command: CommandMessage = {
}
}
});
}
},
};