feat: suggestions

This commit is contained in:
2025-06-28 19:58:29 +02:00
parent e430d43f1d
commit 1856661692
7 changed files with 312 additions and 8 deletions

15
TODO.md
View File

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

View File

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

View File

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

View File

@@ -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<MessageActionRowComponent>;
const row2 = interaction.message!.components[1] as ActionRow<MessageActionRowComponent>;
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<ButtonBuilder>().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<ButtonBuilder>().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;
}
},
});

View File

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

View File

@@ -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<MessageActionRowComponent>;
const row2 = interaction.message!.components[1] as ActionRow<MessageActionRowComponent>;
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<ButtonBuilder>().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<ButtonBuilder>().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;
}
},
});

View File

@@ -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<ModalActionRowComponentBuilder>().addComponents(input);
modal.addComponents(suggestionsActionRow);
await ctx.interaction.showModal(modal);
},
});