From 185666169277341c4bac7f2df94d153b993dd36b Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Sat, 28 Jun 2025 19:58:29 +0200 Subject: [PATCH] feat: suggestions --- TODO.md | 15 ++-- src/commands/handlers/sugerencias.ts | 85 ++++++++++++++++++++ src/commands/handlers/suggestions-no-who.ts | 23 ++++++ src/commands/handlers/suggestions-no.ts | 73 +++++++++++++++++ src/commands/handlers/suggestions-yes-who.ts | 23 ++++++ src/commands/handlers/suggestions-yes.ts | 74 +++++++++++++++++ src/commands/misc/sugerencias.ts | 27 +++++++ 7 files changed, 312 insertions(+), 8 deletions(-) create mode 100644 src/commands/handlers/sugerencias.ts create mode 100644 src/commands/handlers/suggestions-no-who.ts create mode 100644 src/commands/handlers/suggestions-no.ts create mode 100644 src/commands/handlers/suggestions-yes-who.ts create mode 100644 src/commands/handlers/suggestions-yes.ts create mode 100644 src/commands/misc/sugerencias.ts diff --git a/TODO.md b/TODO.md index dfbe31f..5210f50 100644 --- a/TODO.md +++ b/TODO.md @@ -16,6 +16,7 @@ - [ ] /infinitecraft - InfiniteCraft recipe solver - [ ] /letra - Song lyrics search via Genius API - [x] /google - Google search results +- [x] /sugerencias - Suggestion system with upvote/downvote - [ ] /wikipedia - Wikipedia search (Spanish/English) - [ ] /faq - FAQ system with Minecraft questions - [ ] /afk - AFK status management @@ -27,10 +28,10 @@ - [ ] /ip - Minecraft server IP information # Button Handlers -- [ ] suggestions-yes - Upvote button handler -- [ ] suggestions-no - Downvote button handler -- [ ] suggestions-yes-who - Show upvoters -- [ ] suggestions-no-who - Show downvoters +- [x] suggestions-yes - Upvote button handler +- [x] suggestions-no - Downvote button handler +- [x] suggestions-yes-who - Show upvoters +- [x] suggestions-no-who - Show downvoters # Context Menu Commands - [ ] bonzify - Text-to-speech with Bonzi Buddy voice @@ -55,10 +56,8 @@ - [ ] Minecraft server status checker - [ ] Activity status rotation -# Database Schemas to Rewrite -- [ ] Suggestions voting system -- [ ] AFK status tracking -- [ ] Question/answer system (askjavi) +# Database +- [x] Migration to sqlite # Command Features to Preserve - [ ] Autocomplete functionality for various commands diff --git a/src/commands/handlers/sugerencias.ts b/src/commands/handlers/sugerencias.ts new file mode 100644 index 0000000..10f95bd --- /dev/null +++ b/src/commands/handlers/sugerencias.ts @@ -0,0 +1,85 @@ +import { commandModule, CommandType } from '@sern/handler'; +import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from 'discord.js'; +import { ThreadAutoArchiveDuration } from 'discord.js'; + +export default commandModule({ + type: CommandType.Modal, + async execute(modal) { + const value = modal.fields.getTextInputValue('sugerenciasInput'); + + if (onlySpaces(value) === true) { + return await modal.reply({ + content: 'Buen intento enviando un mensaje vacío >:D', + ephemeral: true, + }); + } + + const embed = new EmbedBuilder() + .setColor('Random') + .setTitle('Sugerencia') + .setAuthor({ + name: `${modal.user.username}`, + iconURL: `${modal.user.displayAvatarURL()}`, + }) + .setDescription(value); + const row = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('suggestions-yes') + .setEmoji('✅') + .setLabel('0') + .setStyle(ButtonStyle.Success), + new ButtonBuilder() + .setCustomId('suggestions-no') + .setEmoji('❎') + .setLabel('0') + .setStyle(ButtonStyle.Danger) + ); + const row2 = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId('suggestions-yes-who') + .setEmoji('✅') + .setLabel('Quién') + .setStyle(ButtonStyle.Secondary), + new ButtonBuilder() + .setCustomId('suggestions-no-who') + .setEmoji('❎') + .setLabel('Quién') + .setStyle(ButtonStyle.Secondary) + ); + + const guild = await modal.client.guilds.fetch(process.env.GUILDID!); + const channel = await guild.channels.fetch(process.env.SUGGESTIONS_CHANNEL!); + if (!channel || !channel.isTextBased()) { + return await modal.reply({ + content: 'ERROR: Canal de sugerencias no encontrado.', + ephemeral: true, + }); + } + + const msg = await channel.send({ embeds: [embed], components: [row, row2] }); + msg.startThread({ + name: `Sugerencia de ${modal.user.username}`, + autoArchiveDuration: ThreadAutoArchiveDuration.ThreeDays, + reason: 'AUTOMATIZADO: Hilo para discutir sobre la sugerencia.', + }); + + const rowSuccess = new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setURL(msg.url) + .setLabel('Ver sugerencia') + .setStyle(ButtonStyle.Link) + ); + const modalReply = await modal.reply({ + content: '¡Enviado!', + ephemeral: true, + components: [rowSuccess], + }); + setTimeout(() => { + modalReply.delete().catch(() => {}); + }, 3000); + }, +}); + +function onlySpaces(str: string) { + return str.trim().length === 0; +} diff --git a/src/commands/handlers/suggestions-no-who.ts b/src/commands/handlers/suggestions-no-who.ts new file mode 100644 index 0000000..51bf645 --- /dev/null +++ b/src/commands/handlers/suggestions-no-who.ts @@ -0,0 +1,23 @@ +import { commandModule, CommandType } from '@sern/handler'; +import db from '../../utils/db'; + +export default commandModule({ + type: CommandType.Button, + async execute(interaction) { + await interaction.deferReply({ ephemeral: true }); + const votes = await db.suggestion.findMany({ + where: { msgId: interaction.message.id, upDown: -1 }, + }); + const fetchedIds = await Promise.all( + votes.map(async (v) => { + return interaction.client.users.fetch(v.userId); + }) + ); + await interaction.editReply({ + content: `Gente que ha hecho downvote:\n${ + fetchedIds.length > 0 ? fetchedIds.join(', ') : 'Nadie, de momento' + }`, + allowedMentions: { repliedUser: false }, + }); + }, +}); diff --git a/src/commands/handlers/suggestions-no.ts b/src/commands/handlers/suggestions-no.ts new file mode 100644 index 0000000..3e66329 --- /dev/null +++ b/src/commands/handlers/suggestions-no.ts @@ -0,0 +1,73 @@ +import { commandModule, CommandType } from '@sern/handler'; +import { + ActionRow, + ActionRowBuilder, + APIButtonComponentWithCustomId, + ButtonBuilder, + ButtonComponent, + ButtonComponentData, + ButtonStyle, + MessageActionRowComponent, +} from 'discord.js'; +import db from '../../utils/db'; + +export default commandModule({ + type: CommandType.Button, + async execute(interaction) { + const row1 = interaction.message!.components[0] as ActionRow; + const row2 = interaction.message!.components[1] as ActionRow; + const rows = { + yes: row1.components[0], + no: row1.components[1], + yesWho: row2.components[0], + noWho: row2.components[1], + } as { + yes: ButtonComponent; + no: ButtonComponent; + yesWho: ButtonComponent; + noWho: ButtonComponent; + }; + const upvoteData = rows.yes.data as APIButtonComponentWithCustomId; + const downvoteData = rows.no.data as APIButtonComponentWithCustomId; + const userSuggestion = await db.suggestion.findFirst({ + where: { msgId: interaction.message.id, userId: interaction.user.id }, + }); + + if (!userSuggestion) { + const row1 = new ActionRowBuilder().setComponents( + new ButtonBuilder(rows.yes.data), + new ButtonBuilder(rows.no.data).setLabel((parseInt(downvoteData.label!) + 1).toString()) + ); + + await db.suggestion.create({ + data: { + msgId: interaction.message.id, + userId: interaction.user.id, + upDown: -1, + }, + }); + await interaction.message.edit({ components: [row1, row2] }); + await interaction.deferUpdate(); + return; + } + + const userSuggestionUpDown = userSuggestion.upDown === 1; + if (userSuggestionUpDown) { + await db.suggestion.updateMany({ + where: { msgId: interaction.message.id, userId: interaction.user.id, upDown: 1 }, + data: { upDown: -1 }, + }); + + const row1 = new ActionRowBuilder().setComponents( + new ButtonBuilder(rows.yes.data).setLabel((parseInt(upvoteData.label!) - 1).toString()), + new ButtonBuilder(rows.no.data).setLabel((parseInt(downvoteData.label!) + 1).toString()) + ); + await interaction.message.edit({ components: [row1, row2] }); + await interaction.deferUpdate(); + return; + } else { + await interaction.deferUpdate() + return; + } + }, +}); diff --git a/src/commands/handlers/suggestions-yes-who.ts b/src/commands/handlers/suggestions-yes-who.ts new file mode 100644 index 0000000..5e4b7f8 --- /dev/null +++ b/src/commands/handlers/suggestions-yes-who.ts @@ -0,0 +1,23 @@ +import { commandModule, CommandType } from '@sern/handler'; +import db from '../../utils/db'; + +export default commandModule({ + type: CommandType.Button, + async execute(interaction) { + await interaction.deferReply({ ephemeral: true }); + const votes = await db.suggestion.findMany({ + where: { msgId: interaction.message.id, upDown: 1 }, + }); + const fetchedIds = await Promise.all( + votes.map(async (v) => { + return interaction.client.users.fetch(v.userId); + }) + ); + await interaction.editReply({ + content: `Gente que ha hecho upvote:\n${ + fetchedIds.length > 0 ? fetchedIds.join(', ') : 'Nadie, de momento' + }`, + allowedMentions: { repliedUser: false }, + }); + }, +}); diff --git a/src/commands/handlers/suggestions-yes.ts b/src/commands/handlers/suggestions-yes.ts new file mode 100644 index 0000000..50a1db7 --- /dev/null +++ b/src/commands/handlers/suggestions-yes.ts @@ -0,0 +1,74 @@ +import { commandModule, CommandType } from '@sern/handler'; +import { + ActionRow, + ActionRowBuilder, + APIButtonComponentWithCustomId, + ButtonBuilder, + ButtonComponent, + ButtonComponentData, + ButtonStyle, + MessageActionRowComponent, +} from 'discord.js'; +import db from '../../utils/db'; + +export default commandModule({ + type: CommandType.Button, + async execute(interaction) { + const convertToNumber = parseInt((interaction.component as ButtonComponent).label!); + const row1 = interaction.message!.components[0] as ActionRow; + const row2 = interaction.message!.components[1] as ActionRow; + const rows = { + yes: row1.components[0], + no: row1.components[1], + yesWho: row2.components[0], + noWho: row2.components[1], + } as { + yes: ButtonComponent; + no: ButtonComponent; + yesWho: ButtonComponent; + noWho: ButtonComponent; + }; + const upvoteData = rows.yes.data as APIButtonComponentWithCustomId; + const downvoteData = rows.no.data as APIButtonComponentWithCustomId; + const userSuggestion = await db.suggestion.findFirst({ + where: { msgId: interaction.message.id, userId: interaction.user.id }, + }); + + if (!userSuggestion) { + const row1 = new ActionRowBuilder().setComponents( + new ButtonBuilder(rows.yes.data).setLabel((parseInt(upvoteData.label!) + 1).toString()), + new ButtonBuilder(rows.no.data) + ); + + await db.suggestion.create({ + data: { + msgId: interaction.message.id, + userId: interaction.user.id, + upDown: 1, + }, + }); + await interaction.message.edit({ components: [row1, row2] }); + await interaction.deferUpdate(); + return; + } + + const userSuggestionUpDown = userSuggestion.upDown === 1; + if (userSuggestionUpDown) { + await interaction.deferUpdate() + return; + } else { + await db.suggestion.updateMany({ + where: { msgId: interaction.message.id, userId: interaction.user.id, upDown: -1 }, + data: { upDown: 1 }, + }); + + const row1 = new ActionRowBuilder().setComponents( + new ButtonBuilder(rows.yes.data).setLabel((parseInt(upvoteData.label!) + 1).toString()), + new ButtonBuilder(rows.no.data).setLabel((parseInt(downvoteData.label!) - 1).toString()) + ); + await interaction.message.edit({ components: [row1, row2] }); + await interaction.deferUpdate(); + return; + } + }, +}); diff --git a/src/commands/misc/sugerencias.ts b/src/commands/misc/sugerencias.ts new file mode 100644 index 0000000..9ebba01 --- /dev/null +++ b/src/commands/misc/sugerencias.ts @@ -0,0 +1,27 @@ +import { commandModule, CommandType } from '@sern/handler'; +import { + ActionRowBuilder, + ModalBuilder, + TextInputBuilder, + TextInputStyle, + ModalActionRowComponentBuilder, +} from 'discord.js'; + +export default commandModule({ + name: 'sugerencias', + type: CommandType.Slash, + plugins: [], + description: 'Envia una sugerencia.', + execute: async (ctx) => { + const modal = new ModalBuilder().setCustomId('sugerencias').setTitle('Sugerencias'); + + const input = new TextInputBuilder() + .setCustomId('sugerenciasInput') + .setLabel('Tienes sugerencias?') + .setStyle(TextInputStyle.Paragraph); + const suggestionsActionRow = + new ActionRowBuilder().addComponents(input); + modal.addComponents(suggestionsActionRow); + await ctx.interaction.showModal(modal); + }, +});