Skip to content

Commit fcd9e7e

Browse files
authored
Merge pull request #13 from CapituloJaverianoACM/develop
feat/interactiveAnnouncement
2 parents fb557c0 + 8c0a8c3 commit fcd9e7e

File tree

8 files changed

+851
-54
lines changed

8 files changed

+851
-54
lines changed

src/commands/announce-handlers.ts

Lines changed: 551 additions & 0 deletions
Large diffs are not rendered by default.

src/commands/announce-old.ts.bak

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/**
2+
* @file announce.ts
3+
* @description Comando para publicar anuncios en el canal de anuncios configurado.
4+
* Solo accesible para administradores. Permite personalizar título, color y mencionar roles.
5+
*/
6+
7+
import { SlashCommandBuilder, PermissionsBitField, ColorResolvable } from 'discord.js';
8+
import { getGuildConfig } from '../config/store';
9+
import { buildEmbed, parseHexColor } from '../utils/embed';
10+
11+
/** Definición del comando /announce con sus opciones */
12+
const data = new SlashCommandBuilder()
13+
.setName('announce')
14+
.setDescription('Publica un anuncio en el canal de anuncios configurado')
15+
.addStringOption((opt) =>
16+
opt.setName('message').setDescription('Contenido del anuncio').setRequired(true),
17+
)
18+
.addStringOption((opt) =>
19+
opt.setName('title').setDescription('Título del anuncio').setRequired(false),
20+
)
21+
.addBooleanOption((opt) =>
22+
opt.setName('ping').setDescription('Mencionar rol de eventos/anuncios si está configurado'),
23+
)
24+
.addStringOption((opt) =>
25+
opt.setName('color').setDescription('Color del embed (hex, ej. #5865F2)').setRequired(false),
26+
);
27+
28+
/**
29+
* Ejecuta el comando announce para publicar un anuncio
30+
* @param {any} interaction - La interacción de Discord
31+
* @returns {Promise<void>}
32+
*/
33+
async function execute(interaction: any) {
34+
if (!interaction.memberPermissions?.has(PermissionsBitField.Flags.Administrator)) {
35+
return interaction.reply({ content: 'Solo admin/junta.', flags: 1 << 6 });
36+
}
37+
const cfg = getGuildConfig(interaction.guildId);
38+
if (!cfg?.channels.announcements) {
39+
return interaction.reply({
40+
content: 'Canal de anuncios no configurado. Usa /setup.',
41+
flags: 1 << 6,
42+
});
43+
}
44+
const channel = interaction.guild.channels.cache.get(cfg.channels.announcements);
45+
if (!channel || !channel.isTextBased?.()) {
46+
return interaction.reply({ content: 'Canal de anuncios inválido.', flags: 1 << 6 });
47+
}
48+
const title = interaction.options.getString('title') ?? 'Anuncio';
49+
const message = interaction.options.getString('message', true);
50+
const color = parseHexColor(interaction.options.getString('color') || undefined) as
51+
| ColorResolvable
52+
| undefined;
53+
const doPing = interaction.options.getBoolean('ping') ?? false;
54+
const pingRole = doPing ? cfg.roles.notificacionesGenerales : undefined;
55+
56+
const embed = buildEmbed({ title, description: message, color });
57+
try {
58+
if (pingRole) {
59+
await channel.send({ content: `<@&${pingRole}>` });
60+
}
61+
await channel.send({ embeds: [embed] });
62+
return interaction.reply({ content: 'Anuncio publicado.', flags: 1 << 6 });
63+
} catch (err) {
64+
console.error('announce error', err);
65+
return interaction.reply({ content: 'Error publicando el anuncio.', flags: 1 << 6 });
66+
}
67+
}
68+
69+
export default { data, execute };

src/commands/announce.ts

Lines changed: 168 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,192 @@
11
/**
2-
* @file announce.ts
3-
* @description Comando para publicar anuncios en el canal de anuncios configurado.
4-
* Solo accesible para administradores. Permite personalizar título, color y mencionar roles.
2+
* @file announce-interactive.ts
3+
* @description Sistema interactivo para crear anuncios usando embeds y dropdowns.
4+
* Permite configurar título, mensaje, color y roles a mencionar de forma visual.
5+
* Solo accesible para administradores.
56
*/
67

7-
import { SlashCommandBuilder, PermissionsBitField, ColorResolvable } from 'discord.js';
8+
import {
9+
SlashCommandBuilder,
10+
PermissionsBitField,
11+
ActionRowBuilder,
12+
ModalBuilder,
13+
TextInputBuilder,
14+
TextInputStyle,
15+
} from 'discord.js';
816
import { getGuildConfig } from '../config/store';
9-
import { buildEmbed, parseHexColor } from '../utils/embed';
17+
import { buildEmbed } from '../utils/embed';
18+
import { logger, generateRequestId } from '../utils/logger';
1019

11-
/** Definición del comando /announce con sus opciones */
20+
// Estado temporal de anuncios en progreso
21+
export const announceSessions = new Map<string, any>();
22+
23+
/** Definición del comando /announce simplificado */
1224
const data = new SlashCommandBuilder()
1325
.setName('announce')
14-
.setDescription('Publica un anuncio en el canal de anuncios configurado')
15-
.addStringOption((opt) =>
16-
opt.setName('message').setDescription('Contenido del anuncio').setRequired(true),
17-
)
18-
.addStringOption((opt) =>
19-
opt.setName('title').setDescription('Título del anuncio').setRequired(false),
20-
)
21-
.addBooleanOption((opt) =>
22-
opt.setName('ping').setDescription('Mencionar rol de eventos/anuncios si está configurado'),
23-
)
24-
.addStringOption((opt) =>
25-
opt.setName('color').setDescription('Color del embed (hex, ej. #5865F2)').setRequired(false),
26-
);
26+
.setDescription('📢 Crear un anuncio interactivo (Sistema visual paso a paso)');
27+
28+
/**
29+
* Crea el embed de preview del anuncio
30+
*/
31+
export function createAnnouncementPreview(session: any) {
32+
const { title, message, color, roles, image } = session.announcement;
33+
34+
// Debug logging
35+
console.log('[PREVIEW DEBUG]', {
36+
hasTitle: !!title,
37+
hasMessage: !!message,
38+
color: color || 'default',
39+
rolesCount: roles?.length || 0,
40+
hasImage: !!image,
41+
imageUrl: image,
42+
});
43+
44+
const rolesText =
45+
roles && roles.length > 0 ? roles.map((r: string) => `<@&${r}>`).join(' ') : 'Ninguno';
46+
47+
const fields = [
48+
{
49+
name: '📝 Título',
50+
value: title || 'Sin título',
51+
inline: false,
52+
},
53+
{
54+
name: '💬 Mensaje',
55+
value: message || 'Sin mensaje',
56+
inline: false,
57+
},
58+
{
59+
name: '🎨 Color',
60+
value: color || 'Default (#5865F2)',
61+
inline: true,
62+
},
63+
{
64+
name: '🔔 Menciones',
65+
value: rolesText,
66+
inline: true,
67+
},
68+
];
69+
70+
// Agregar indicador de imagen si existe
71+
if (image) {
72+
fields.push({
73+
name: '🖼️ Imagen',
74+
value: '✅ Configurada',
75+
inline: false,
76+
});
77+
}
78+
79+
const embedOptions = {
80+
title: '📢 Preview del Anuncio',
81+
description: 'Así es como se verá tu anuncio. Revisa antes de publicar.',
82+
color: color || '#5865F2',
83+
fields,
84+
image: image,
85+
footer: 'Haz clic en los botones para editar o publicar',
86+
};
87+
88+
console.log('[EMBED OPTIONS]', {
89+
hasImage: !!embedOptions.image,
90+
imageValue: embedOptions.image,
91+
});
92+
93+
return buildEmbed(embedOptions);
94+
}
2795

2896
/**
29-
* Ejecuta el comando announce para publicar un anuncio
30-
* @param {any} interaction - La interacción de Discord
31-
* @returns {Promise<void>}
97+
* Ejecuta el comando announce interactivo
3298
*/
3399
async function execute(interaction: any) {
100+
const requestId = generateRequestId();
101+
const guildId = interaction.guildId;
102+
const userId = interaction.user.id;
103+
104+
logger.info('Interactive announce started', { requestId, userId, guildId });
105+
106+
// Verificar permisos
34107
if (!interaction.memberPermissions?.has(PermissionsBitField.Flags.Administrator)) {
35-
return interaction.reply({ content: 'Solo admin/junta.', flags: 1 << 6 });
108+
return interaction.reply({
109+
content: '❌ Solo administradores pueden crear anuncios.',
110+
flags: 1 << 6,
111+
});
36112
}
37-
const cfg = getGuildConfig(interaction.guildId);
113+
114+
// Verificar que existe canal de anuncios configurado
115+
const cfg = getGuildConfig(guildId);
38116
if (!cfg?.channels.announcements) {
39117
return interaction.reply({
40-
content: 'Canal de anuncios no configurado. Usa /setup.',
118+
content: 'Canal de anuncios no configurado. Usa `/setup` primero.',
41119
flags: 1 << 6,
42120
});
43121
}
122+
123+
// Verificar que el canal existe
44124
const channel = interaction.guild.channels.cache.get(cfg.channels.announcements);
45125
if (!channel || !channel.isTextBased?.()) {
46-
return interaction.reply({ content: 'Canal de anuncios inválido.', flags: 1 << 6 });
47-
}
48-
const title = interaction.options.getString('title') ?? 'Anuncio';
49-
const message = interaction.options.getString('message', true);
50-
const color = parseHexColor(interaction.options.getString('color') || undefined) as
51-
| ColorResolvable
52-
| undefined;
53-
const doPing = interaction.options.getBoolean('ping') ?? false;
54-
const pingRole = doPing ? cfg.roles.notificacionesGenerales : undefined;
55-
56-
const embed = buildEmbed({ title, description: message, color });
57-
try {
58-
if (pingRole) {
59-
await channel.send({ content: `<@&${pingRole}>` });
60-
}
61-
await channel.send({ embeds: [embed] });
62-
return interaction.reply({ content: 'Anuncio publicado.', flags: 1 << 6 });
63-
} catch (err) {
64-
console.error('announce error', err);
65-
return interaction.reply({ content: 'Error publicando el anuncio.', flags: 1 << 6 });
126+
return interaction.reply({
127+
content: '❌ El canal de anuncios configurado no es válido.',
128+
flags: 1 << 6,
129+
});
66130
}
131+
132+
// Crear sesión
133+
const session = {
134+
userId,
135+
guildId,
136+
requestId,
137+
channelId: cfg.channels.announcements,
138+
previewMessage: undefined, // Se guardará el mensaje del preview para actualizarlo
139+
announcement: {
140+
title: undefined,
141+
message: undefined,
142+
color: undefined,
143+
roles: [],
144+
image: undefined,
145+
},
146+
startedAt: Date.now(),
147+
};
148+
149+
// Guardar sesión
150+
const sessionKey = `${guildId}-${userId}`;
151+
announceSessions.set(sessionKey, session);
152+
153+
// Cleanup después de 15 minutos
154+
setTimeout(
155+
() => {
156+
if (announceSessions.has(sessionKey)) {
157+
announceSessions.delete(sessionKey);
158+
logger.info('Announce session expired', { requestId, guildId, userId });
159+
}
160+
},
161+
15 * 60 * 1000,
162+
);
163+
164+
// Mostrar modal para título y mensaje
165+
const modal = new ModalBuilder()
166+
.setCustomId(`announce:modal:${userId}`)
167+
.setTitle('📢 Crear Anuncio')
168+
.addComponents(
169+
new ActionRowBuilder<TextInputBuilder>().addComponents(
170+
new TextInputBuilder()
171+
.setCustomId('title')
172+
.setLabel('Título del Anuncio')
173+
.setStyle(TextInputStyle.Short)
174+
.setPlaceholder('Ej: Importante - Leer')
175+
.setRequired(false)
176+
.setMaxLength(256),
177+
),
178+
new ActionRowBuilder<TextInputBuilder>().addComponents(
179+
new TextInputBuilder()
180+
.setCustomId('message')
181+
.setLabel('Mensaje del Anuncio')
182+
.setStyle(TextInputStyle.Paragraph)
183+
.setPlaceholder('Escribe el contenido del anuncio aquí...')
184+
.setRequired(true)
185+
.setMaxLength(4000),
186+
),
187+
);
188+
189+
await interaction.showModal(modal);
67190
}
68191

69192
export default { data, execute };

src/commands/presence.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import { SlashCommandBuilder, ActivityType, PermissionsBitField } from 'discord.js';
99
import { buildEmbed } from '../utils/embed';
10+
import { getGuildConfig } from '../config/store';
1011

1112
/** Definición del comando /presence con subcomandos set y clear */
1213
const data = new SlashCommandBuilder()
@@ -54,9 +55,18 @@ const data = new SlashCommandBuilder()
5455
* @returns {Promise<void>}
5556
*/
5657
async function execute(interaction: any) {
57-
if (!interaction.memberPermissions?.has(PermissionsBitField.Flags.Administrator)) {
58-
return interaction.reply({ content: 'Solo admin/junta.', flags: 1 << 6 });
58+
// Verificar si es admin o tiene rol de junta
59+
const cfg = getGuildConfig(interaction.guildId);
60+
const isAdmin = interaction.memberPermissions?.has(PermissionsBitField.Flags.Administrator);
61+
const isJunta = cfg?.roles.junta ? interaction.member?.roles.cache.has(cfg.roles.junta) : false;
62+
63+
if (!isAdmin && !isJunta) {
64+
return interaction.reply({
65+
content: 'Solo admin/junta pueden usar este comando.',
66+
flags: 1 << 6,
67+
});
5968
}
69+
6070
const sub = interaction.options.getSubcommand();
6171

6272
const respond =

0 commit comments

Comments
 (0)