feat: add resource checker prompts and display component types for enhanced component management
This commit is contained in:
@@ -1,217 +1,411 @@
|
||||
import {
|
||||
Message,
|
||||
ButtonInteraction,
|
||||
StringSelectMenuInteraction,
|
||||
MessageComponentInteraction,
|
||||
ComponentType,
|
||||
ButtonStyle,
|
||||
APIEmbed
|
||||
} from "discord.js";
|
||||
import { CommandMessage } from "../../../core/types/commands";
|
||||
import type Amayo from "../../../core/client";
|
||||
import type { JsonValue } from "@prisma/client/runtime/library";
|
||||
|
||||
interface BlockItem {
|
||||
name: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface ActionRowBuilder {
|
||||
type: ComponentType.ActionRow;
|
||||
components: any[];
|
||||
}
|
||||
|
||||
export const command: CommandMessage = {
|
||||
name: "eliminar-embed",
|
||||
type: "message",
|
||||
aliases: ["embed-eliminar", "borrar-embed", "embeddelete"],
|
||||
cooldown: 10,
|
||||
run: async (message: any, args: string[], client: any) => {
|
||||
description: "Elimina bloques DisplayComponents del servidor",
|
||||
category: "Alianzas",
|
||||
usage: "eliminar-embed [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.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Obtener todos los bloques del servidor
|
||||
const blocks = await client.prisma.blockV2Config.findMany({
|
||||
where: { guildId: message.guildId! },
|
||||
select: { name: true, id: true }
|
||||
});
|
||||
|
||||
if (blocks.length === 0) {
|
||||
const noBlocksEmbed = {
|
||||
color: 0xf04747,
|
||||
title: "🗂️ Panel de Eliminación de Bloques",
|
||||
description: "📭 **No hay bloques disponibles**\n\nNo se encontraron bloques para eliminar en este servidor.\n\nPuedes crear nuevos bloques usando `!blockcreate`.",
|
||||
footer: {
|
||||
text: "Sistema de gestión de bloques • Amayo Bot"
|
||||
}
|
||||
};
|
||||
|
||||
await message.reply({
|
||||
embeds: [noBlocksEmbed]
|
||||
});
|
||||
// If specific block name provided, handle direct deletion
|
||||
if (args.length > 0) {
|
||||
const blockName = args.join(" ").trim();
|
||||
await handleDirectDeletion(message, client, blockName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Crear opciones para el select menu
|
||||
const selectOptions = blocks.slice(0, 25).map((block: any, index: number) => ({
|
||||
label: block.name,
|
||||
value: block.name,
|
||||
description: `ID: ${block.id}`,
|
||||
emoji: index < 10 ? { name: `${index + 1}️⃣` } : { name: "📄" }
|
||||
}));
|
||||
|
||||
// Crear embed principal de eliminación
|
||||
const deleteEmbed = {
|
||||
color: 0xff6b35,
|
||||
title: "🗑️ Panel de Eliminación de Bloques",
|
||||
description: `📊 **${blocks.length} bloque(s) encontrado(s)**\n\n⚠️ **ADVERTENCIA:** La eliminación es permanente e irreversible.\n\nSelecciona el bloque que deseas eliminar del menú de abajo:`,
|
||||
footer: {
|
||||
text: "Selecciona un bloque para eliminar • Timeout: 5 minutos"
|
||||
}
|
||||
};
|
||||
|
||||
const actionRow = {
|
||||
type: 1,
|
||||
components: [
|
||||
{
|
||||
type: 3, // StringSelect
|
||||
custom_id: "delete_block_select",
|
||||
placeholder: "🗑️ Selecciona un bloque para eliminar...",
|
||||
min_values: 1,
|
||||
max_values: 1,
|
||||
options: selectOptions
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const cancelRow = {
|
||||
type: 1,
|
||||
components: [
|
||||
{
|
||||
type: 2, // Button
|
||||
style: 4, // Danger
|
||||
label: "❌ Cancelar",
|
||||
custom_id: "cancel_delete"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const panelMessage = await message.reply({
|
||||
embeds: [deleteEmbed],
|
||||
components: [actionRow, cancelRow]
|
||||
});
|
||||
|
||||
const collector = panelMessage.createMessageComponentCollector({
|
||||
time: 300000, // 5 minutos
|
||||
filter: (i: any) => i.user.id === message.author.id
|
||||
});
|
||||
|
||||
collector.on("collect", async (interaction: any) => {
|
||||
if (interaction.customId === "cancel_delete") {
|
||||
const canceledEmbed = {
|
||||
color: 0x36393f,
|
||||
title: "❌ Operación Cancelada",
|
||||
description: "La eliminación de bloques ha sido cancelada.\nNingún bloque fue eliminado.",
|
||||
footer: { text: "Operación cancelada por el usuario" }
|
||||
};
|
||||
|
||||
await interaction.update({
|
||||
embeds: [canceledEmbed],
|
||||
components: []
|
||||
});
|
||||
|
||||
collector.stop("cancelled");
|
||||
return;
|
||||
}
|
||||
|
||||
if (interaction.customId === "delete_block_select" && interaction.isStringSelectMenu()) {
|
||||
const selectedBlock = interaction.values[0];
|
||||
|
||||
const confirmationEmbed = {
|
||||
color: 0xf04747,
|
||||
title: "⚠️ CONFIRMAR ELIMINACIÓN",
|
||||
description: `🗑️ **Bloque a eliminar:** \`${selectedBlock}\`\n\n❗ **ESTA ACCIÓN ES IRREVERSIBLE**\n\nUna vez eliminado, no podrás recuperar:\n• Toda la configuración del bloque\n• Los componentes y contenido\n• Las imágenes y colores personalizados\n\n¿Estás seguro de que quieres continuar?`,
|
||||
footer: { text: "⚠️ Acción irreversible - Piénsalo bien" }
|
||||
};
|
||||
|
||||
const confirmationRow = {
|
||||
type: 1,
|
||||
components: [
|
||||
{
|
||||
type: 2,
|
||||
style: 4, // Danger
|
||||
label: "🗑️ SÍ, ELIMINAR",
|
||||
custom_id: `confirm_delete_${selectedBlock}`
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
style: 2, // Secondary
|
||||
label: "↩️ Volver Atrás",
|
||||
custom_id: "back_to_selection"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
await interaction.update({
|
||||
embeds: [confirmationEmbed],
|
||||
components: [confirmationRow]
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (interaction.customId.startsWith("confirm_delete_")) {
|
||||
const blockName = interaction.customId.replace("confirm_delete_", "");
|
||||
|
||||
try {
|
||||
await client.prisma.blockV2Config.delete({
|
||||
where: {
|
||||
guildId_name: {
|
||||
guildId: message.guildId!,
|
||||
name: blockName,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const successEmbed = {
|
||||
color: 0x57f287,
|
||||
title: "✅ Eliminación Exitosa",
|
||||
description: `🗑️ **Bloque eliminado:** \`${blockName}\`\n\n✨ El bloque ha sido eliminado permanentemente de la base de datos.\n\n📋 Para ver los bloques restantes, usa: \`!embedlist\`\n📝 Para crear un nuevo bloque, usa: \`!blockcreate\``,
|
||||
footer: { text: "Bloque eliminado exitosamente" }
|
||||
};
|
||||
|
||||
await interaction.update({
|
||||
embeds: [successEmbed],
|
||||
components: []
|
||||
});
|
||||
|
||||
collector.stop("success");
|
||||
|
||||
} catch (error) {
|
||||
const errorEmbed = {
|
||||
color: 0xf04747,
|
||||
title: "❌ Error en la Eliminación",
|
||||
description: `🔍 **Bloque no encontrado:** \`${blockName}\`\n\n💭 Posibles causas:\n• El bloque ya fue eliminado\n• Error de conexión con la base de datos\n• El nombre del bloque cambió\n\n🔄 Intenta refrescar la lista con \`!embedlist\``,
|
||||
footer: { text: "Error de eliminación" }
|
||||
};
|
||||
|
||||
await interaction.update({
|
||||
embeds: [errorEmbed],
|
||||
components: []
|
||||
});
|
||||
|
||||
collector.stop("error");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (interaction.customId === "back_to_selection") {
|
||||
await interaction.update({
|
||||
embeds: [deleteEmbed],
|
||||
components: [actionRow, cancelRow]
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
collector.on("end", async (collected: any, reason: string) => {
|
||||
if (reason === "time") {
|
||||
const timeoutEmbed = {
|
||||
color: 0x36393f,
|
||||
title: "⏰ Tiempo Agotado",
|
||||
description: "El panel de eliminación ha expirado por inactividad.\nUsa el comando nuevamente si necesitas eliminar bloques.",
|
||||
footer: { text: "Panel expirado por inactividad" }
|
||||
};
|
||||
|
||||
try {
|
||||
await panelMessage.edit({
|
||||
embeds: [timeoutEmbed],
|
||||
components: []
|
||||
});
|
||||
} catch (error) {
|
||||
// Mensaje ya eliminado o error de edición
|
||||
}
|
||||
}
|
||||
});
|
||||
// Otherwise, show interactive panel
|
||||
await showDeletionPanel(message, client);
|
||||
},
|
||||
};
|
||||
|
||||
async function handleDirectDeletion(
|
||||
message: Message,
|
||||
client: Amayo,
|
||||
blockName: string
|
||||
): Promise<void> {
|
||||
const block = await client.prisma.blockV2Config.findFirst({
|
||||
where: {
|
||||
guildId: message.guildId!,
|
||||
name: blockName
|
||||
}
|
||||
});
|
||||
|
||||
if (!block) {
|
||||
await message.reply(`❌ No se encontró un bloque llamado \`${blockName}\`.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show confirmation for direct deletion
|
||||
const confirmEmbed: APIEmbed = {
|
||||
color: 0xff6b35,
|
||||
title: "⚠️ Confirmar Eliminación",
|
||||
description: `¿Estás seguro de que quieres eliminar el bloque \`${blockName}\`?\n\n**Esta acción es irreversible.**`,
|
||||
footer: { text: "Confirma la eliminación usando los botones" }
|
||||
};
|
||||
|
||||
const confirmRow: ActionRowBuilder = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.Button,
|
||||
style: ButtonStyle.Danger,
|
||||
label: "🗑️ Confirmar Eliminación",
|
||||
custom_id: `confirm_delete_${block.id}`
|
||||
},
|
||||
{
|
||||
type: ComponentType.Button,
|
||||
style: ButtonStyle.Secondary,
|
||||
label: "❌ Cancelar",
|
||||
custom_id: "cancel_delete"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const confirmMessage = await message.reply({
|
||||
embeds: [confirmEmbed],
|
||||
components: [confirmRow]
|
||||
});
|
||||
|
||||
await handleConfirmationInteraction(confirmMessage, message, client, block);
|
||||
}
|
||||
|
||||
async function showDeletionPanel(message: Message, client: Amayo): Promise<void> {
|
||||
const blocks = await fetchBlocks(client, message.guildId!);
|
||||
|
||||
if (blocks.length === 0) {
|
||||
await handleNoBlocks(message);
|
||||
return;
|
||||
}
|
||||
|
||||
const deleteEmbed = createDeletionEmbed(blocks);
|
||||
const actionRow = createBlockSelectRow(blocks);
|
||||
const cancelRow = createCancelRow();
|
||||
|
||||
const panelMessage = await message.reply({
|
||||
embeds: [deleteEmbed],
|
||||
components: [actionRow, cancelRow]
|
||||
});
|
||||
|
||||
await handlePanelInteractions(panelMessage, message, client, blocks);
|
||||
}
|
||||
|
||||
async function fetchBlocks(client: Amayo, guildId: string): Promise<BlockItem[]> {
|
||||
return await client.prisma.blockV2Config.findMany({
|
||||
where: { guildId },
|
||||
select: { name: true, id: true },
|
||||
orderBy: { name: 'asc' }
|
||||
});
|
||||
}
|
||||
|
||||
async function handleNoBlocks(message: Message): Promise<void> {
|
||||
const noBlocksEmbed: APIEmbed = {
|
||||
color: 0xf04747,
|
||||
title: "🗂️ Panel de Eliminación de Bloques",
|
||||
description: "📭 **No hay bloques disponibles**\n\nNo se encontraron bloques para eliminar en este servidor.\n\nPuedes crear nuevos bloques usando `!crear-embed`.",
|
||||
footer: { text: "Sistema de gestión de bloques • Amayo Bot" }
|
||||
};
|
||||
|
||||
await message.reply({
|
||||
embeds: [noBlocksEmbed]
|
||||
});
|
||||
}
|
||||
|
||||
function createDeletionEmbed(blocks: BlockItem[]): APIEmbed {
|
||||
return {
|
||||
color: 0xff6b35,
|
||||
title: "🗑️ Panel de Eliminación de Bloques",
|
||||
description: `📊 **${blocks.length} bloque(s) encontrado(s)**\n\n⚠️ **ADVERTENCIA:** La eliminación es permanente e irreversible.\n\nSelecciona el bloque que deseas eliminar del menú de abajo:`,
|
||||
footer: { text: "Selecciona un bloque para eliminar • Timeout: 5 minutos" }
|
||||
};
|
||||
}
|
||||
|
||||
function createBlockSelectRow(blocks: BlockItem[]): ActionRowBuilder {
|
||||
const selectOptions = blocks.slice(0, 25).map((block, index) => ({
|
||||
label: block.name,
|
||||
value: block.id, // Use ID instead of name for better uniqueness
|
||||
description: `ID: ${block.id.slice(-8)}`,
|
||||
emoji: index < 10 ? { name: `${index + 1}️⃣` } : { name: "📄" }
|
||||
}));
|
||||
|
||||
return {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.StringSelect,
|
||||
custom_id: "delete_block_select",
|
||||
placeholder: "🗑️ Selecciona un bloque para eliminar...",
|
||||
min_values: 1,
|
||||
max_values: 1,
|
||||
options: selectOptions
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
function createCancelRow(): ActionRowBuilder {
|
||||
return {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.Button,
|
||||
style: ButtonStyle.Danger,
|
||||
label: "❌ Cancelar",
|
||||
custom_id: "cancel_delete"
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
async function handlePanelInteractions(
|
||||
panelMessage: Message,
|
||||
originalMessage: Message,
|
||||
client: Amayo,
|
||||
blocks: BlockItem[]
|
||||
): Promise<void> {
|
||||
const collector = panelMessage.createMessageComponentCollector({
|
||||
time: 300000, // 5 minutes
|
||||
filter: (interaction: MessageComponentInteraction) => interaction.user.id === originalMessage.author.id
|
||||
});
|
||||
|
||||
collector.on("collect", async (interaction: MessageComponentInteraction) => {
|
||||
try {
|
||||
if (interaction.isButton() && interaction.customId === "cancel_delete") {
|
||||
await handleCancellation(interaction);
|
||||
collector.stop();
|
||||
} else if (interaction.isStringSelectMenu() && interaction.customId === "delete_block_select") {
|
||||
const selectedBlockId = interaction.values[0];
|
||||
const selectedBlock = blocks.find(b => b.id === selectedBlockId);
|
||||
|
||||
if (selectedBlock) {
|
||||
await handleBlockSelection(interaction, client, selectedBlock);
|
||||
collector.stop();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error handling deletion interaction:", error);
|
||||
if (!interaction.replied && !interaction.deferred) {
|
||||
await interaction.reply({
|
||||
content: "❌ Ocurrió un error al procesar la interacción.",
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
collector.on("end", async (collected, reason) => {
|
||||
if (reason === "time") {
|
||||
await handlePanelTimeout(panelMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function handleCancellation(interaction: ButtonInteraction): Promise<void> {
|
||||
const canceledEmbed: APIEmbed = {
|
||||
color: 0x36393f,
|
||||
title: "❌ Operación Cancelada",
|
||||
description: "La eliminación de bloques ha sido cancelada.\nNingún bloque fue eliminado.",
|
||||
footer: { text: "Operación cancelada por el usuario" }
|
||||
};
|
||||
|
||||
await interaction.update({
|
||||
embeds: [canceledEmbed],
|
||||
components: []
|
||||
});
|
||||
}
|
||||
|
||||
async function handleBlockSelection(
|
||||
interaction: StringSelectMenuInteraction,
|
||||
client: Amayo,
|
||||
selectedBlock: BlockItem
|
||||
): Promise<void> {
|
||||
const confirmEmbed: APIEmbed = {
|
||||
color: 0xff4444,
|
||||
title: "⚠️ Confirmar Eliminación",
|
||||
description: `¿Estás seguro de que quieres **eliminar permanentemente** el bloque?\n\n📄 **Nombre:** \`${selectedBlock.name}\`\n🔑 **ID:** \`${selectedBlock.id}\`\n\n❗ **Esta acción NO se puede deshacer.**`,
|
||||
footer: { text: "Confirma tu decisión usando los botones" }
|
||||
};
|
||||
|
||||
const confirmRow: ActionRowBuilder = {
|
||||
type: ComponentType.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: ComponentType.Button,
|
||||
style: ButtonStyle.Danger,
|
||||
label: "🗑️ SÍ, ELIMINAR",
|
||||
custom_id: `confirm_delete_${selectedBlock.id}`
|
||||
},
|
||||
{
|
||||
type: ComponentType.Button,
|
||||
style: ButtonStyle.Secondary,
|
||||
label: "❌ Cancelar",
|
||||
custom_id: "cancel_delete_final"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
await interaction.update({
|
||||
embeds: [confirmEmbed],
|
||||
components: [confirmRow]
|
||||
});
|
||||
|
||||
// Handle final confirmation
|
||||
const finalCollector = interaction.message.createMessageComponentCollector({
|
||||
time: 60000, // 1 minute for final confirmation
|
||||
filter: (i: MessageComponentInteraction) => i.user.id === interaction.user.id
|
||||
});
|
||||
|
||||
finalCollector.on("collect", async (finalInteraction: ButtonInteraction) => {
|
||||
try {
|
||||
if (finalInteraction.customId === "cancel_delete_final") {
|
||||
await handleCancellation(finalInteraction);
|
||||
} else if (finalInteraction.customId === `confirm_delete_${selectedBlock.id}`) {
|
||||
await executeBlockDeletion(finalInteraction, client, selectedBlock);
|
||||
}
|
||||
finalCollector.stop();
|
||||
} catch (error) {
|
||||
console.error("Error in final confirmation:", error);
|
||||
}
|
||||
});
|
||||
|
||||
finalCollector.on("end", async (collected, reason) => {
|
||||
if (reason === "time") {
|
||||
await handleConfirmationTimeout(interaction.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function handleConfirmationInteraction(
|
||||
confirmMessage: Message,
|
||||
originalMessage: Message,
|
||||
client: Amayo,
|
||||
block: any
|
||||
): Promise<void> {
|
||||
const collector = confirmMessage.createMessageComponentCollector({
|
||||
time: 60000, // 1 minute
|
||||
filter: (interaction: MessageComponentInteraction) => interaction.user.id === originalMessage.author.id
|
||||
});
|
||||
|
||||
collector.on("collect", async (interaction: ButtonInteraction) => {
|
||||
try {
|
||||
if (interaction.customId === "cancel_delete") {
|
||||
await handleCancellation(interaction);
|
||||
} else if (interaction.customId === `confirm_delete_${block.id}`) {
|
||||
await executeBlockDeletion(interaction, client, { name: block.name, id: block.id });
|
||||
}
|
||||
collector.stop();
|
||||
} catch (error) {
|
||||
console.error("Error in confirmation interaction:", error);
|
||||
}
|
||||
});
|
||||
|
||||
collector.on("end", async (collected, reason) => {
|
||||
if (reason === "time") {
|
||||
await handleConfirmationTimeout(confirmMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function executeBlockDeletion(
|
||||
interaction: ButtonInteraction,
|
||||
client: Amayo,
|
||||
block: BlockItem
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Delete the block from database
|
||||
await client.prisma.blockV2Config.delete({
|
||||
where: { id: block.id }
|
||||
});
|
||||
|
||||
const successEmbed: APIEmbed = {
|
||||
color: 0x57f287,
|
||||
title: "✅ Bloque Eliminado",
|
||||
description: `El bloque \`${block.name}\` ha sido eliminado exitosamente.\n\n🗑️ **Operación completada**\n📄 **Bloque:** \`${block.name}\`\n🔑 **ID:** \`${block.id}\``,
|
||||
footer: { text: "Bloque eliminado permanentemente" }
|
||||
};
|
||||
|
||||
await interaction.update({
|
||||
embeds: [successEmbed],
|
||||
components: []
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error deleting block:", error);
|
||||
|
||||
const errorEmbed: APIEmbed = {
|
||||
color: 0xf04747,
|
||||
title: "❌ Error al Eliminar",
|
||||
description: `No se pudo eliminar el bloque \`${block.name}\`.\n\nPor favor, inténtalo de nuevo más tarde.`,
|
||||
footer: { text: "Error en la eliminación" }
|
||||
};
|
||||
|
||||
await interaction.update({
|
||||
embeds: [errorEmbed],
|
||||
components: []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePanelTimeout(panelMessage: Message): Promise<void> {
|
||||
const timeoutEmbed: APIEmbed = {
|
||||
color: 0x36393f,
|
||||
title: "⏰ Panel Expirado",
|
||||
description: "El panel de eliminación ha expirado por inactividad.\n\nUsa `!eliminar-embed` para abrir un nuevo panel.",
|
||||
footer: { text: "Panel expirado por inactividad" }
|
||||
};
|
||||
|
||||
try {
|
||||
await panelMessage.edit({
|
||||
embeds: [timeoutEmbed],
|
||||
components: []
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Could not edit message on timeout, likely deleted");
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConfirmationTimeout(confirmMessage: Message): Promise<void> {
|
||||
const timeoutEmbed: APIEmbed = {
|
||||
color: 0x36393f,
|
||||
title: "⏰ Confirmación Expirada",
|
||||
description: "La confirmación ha expirado por inactividad.\nLa eliminación ha sido cancelada.",
|
||||
footer: { text: "Confirmación expirada" }
|
||||
};
|
||||
|
||||
try {
|
||||
await confirmMessage.edit({
|
||||
embeds: [timeoutEmbed],
|
||||
components: []
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("Could not edit confirmation message on timeout");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user