feat: wikipedia and rps

This commit is contained in:
2025-07-06 18:36:51 +02:00
parent 52598aa223
commit 5acccc6ffe
6 changed files with 318 additions and 8 deletions

12
TODO.md
View File

@@ -2,9 +2,8 @@
## Fun Commands
- [ ] /animal - Animal pictures with voting system (cat, dog, capybara, fox, raccoon)
- [ ] /chiste - Joke command fetching from API
- [ ] /rps - Rock Paper Scissors game
- [ ] /tictactoe - Tic-tac-toe game (currently disabled)
- [x] /chiste - Joke command fetching from API
- [x] /rps - Rock Paper Scissors game
- [x] /8ball - Magic 8-ball responses
- [x] /megamind - Megamind meme generator with canvas
- [x] /makesweet - Heart locket image generator
@@ -13,16 +12,14 @@
## Miscellaneous Commands
- [ ] /rolemenu - Role selection menu (owner only)
- [ ] /creditos - Bot credits and acknowledgments
- [ ] /infinitecraft - InfiniteCraft recipe solver
- [x] ~~/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)
- [x] /wikipedia - Wikipedia search (Spanish/English)
- [ ] /faq - FAQ system with Minecraft questions
- [ ] /afk - AFK status management
- [ ] /stats - Bot statistics (currently disabled)
- [ ] /acortar - URL shortener (currently disabled)
- [ ] /askjavi - Question system (disabled)
## Minecraft Commands
- [ ] /ip - Minecraft server IP information
@@ -40,7 +37,6 @@
# Utility Systems to Rewrite
- [ ] Resolver - Role/user resolution utility
- [ ] InfiniteCraft Finder - Recipe pathfinding algorithm
- [ ] Wikipedia utility - Wikipedia search helper
# Plugin Systems

View File

@@ -12,6 +12,7 @@
"dotenv": "^16.3.1",
"mongodb": "^6.17.0",
"node-html-parser": "^7.0.1",
"rockpaperscissors-checker": "^1.2.0",
"sharp": "^0.34.2",
},
"devDependencies": {
@@ -384,6 +385,8 @@
"restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
"rockpaperscissors-checker": ["rockpaperscissors-checker@1.2.0", "", {}, "sha512-JfndRzDvMo1st+iK9dKZIEToFDouc+53+whmVFs+zyBOp0zv/sMNY9hUZI5J7ulygmW1fI6QmvRyXmfpN9Sh8Q=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],

View File

@@ -25,6 +25,7 @@
"dotenv": "^16.3.1",
"mongodb": "^6.17.0",
"node-html-parser": "^7.0.1",
"rockpaperscissors-checker": "^1.2.0",
"sharp": "^0.34.2"
},
"devDependencies": {

View File

@@ -0,0 +1,69 @@
import { commandModule, CommandType } from '@sern/handler'
import { ApplicationCommandOptionType, AutocompleteInteraction, CacheType, CommandInteractionOptionResolver } from 'discord.js';
import { getWikipedia, searchWikipedia } from '../../utils/wikipedia';
export default commandModule({
type: CommandType.Slash,
plugins: [],
description: 'Busca cosas por wikipedia',
//alias : [],
options: [
{
name: 'español',
description: 'Busca cosas por Wikipedia en español',
type: ApplicationCommandOptionType.Subcommand,
options: [
{
name: 'busqueda',
description: 'Escribe qué buscar.',
type: ApplicationCommandOptionType.String,
required: true,
autocomplete: true,
command: {
onEvent: [],
execute: async (ctx) => {
const search = await searchWikipedia('es', ctx as AutocompleteInteraction)
await ctx.respond(
search.map(res => ({ name: res.title.toString(), value: res.pageid.toString() }))
)
}
}
}
]
},
{
name: 'ingles',
description: 'Busca cosas por Wikipedia en inglés',
type: ApplicationCommandOptionType.Subcommand,
options: [
{
name: 'search',
description: 'Escribe qué buscar.',
type: ApplicationCommandOptionType.String,
required: true,
autocomplete: true,
command: {
onEvent: [],
execute: async (ctx) => {
const search = await searchWikipedia('en', ctx as AutocompleteInteraction)
await ctx.respond(
search.map(res => ({ name: res.title.toString(), value: res.pageid.toString() }))
)
}
}
}
]
}
],
execute: async (ctx) => {
const options = ctx.options as unknown as CommandInteractionOptionResolver<CacheType>
switch (ctx.options.getSubcommand()) {
case 'español': {
getWikipedia('es', ctx, options);
} break;
case 'ingles': {
getWikipedia('en', ctx, options);
} break;
}
},
});

148
src/commands/silly/rps.ts Normal file
View File

@@ -0,0 +1,148 @@
import { commandModule, CommandType } from '@sern/handler';
import {
ActionRowBuilder,
ApplicationCommandOptionType,
ButtonBuilder,
ButtonStyle,
ComponentType,
EmbedBuilder,
GuildMember,
} from 'discord.js';
import rockpaperscissors from 'rockpaperscissors-checker';
export default commandModule({
type: CommandType.Slash,
plugins: [],
description: 'Juega piedra papel tijeras',
options: [
{
name: 'usuario',
description: 'El usuario con el que enfrentarse',
type: ApplicationCommandOptionType.User,
required: true,
},
],
execute: async (ctx) => {
let player1: number, player2: number, winner, bothResponded: boolean;
const option = ctx.options.getMember('usuario') as GuildMember;
if (ctx.user.id === option.id) {
return await ctx.reply({ content: `no puedes jugar contigo mismo 💀`, ephemeral: true });
}
if (option.user.bot) {
return await ctx.reply({ content: `no puedes seleccionar a un bot.`, ephemeral: true });
}
const waitingEmbed = new EmbedBuilder()
.setColor('Red')
.setAuthor({ name: ctx.user.username, iconURL: ctx.user.displayAvatarURL() })
.setTitle(`Piedra, papel o tijera? <:PauseChamp:1030169623070519388>`)
.setDescription(
`Esperando a que ambos jugadores eligan...\nJugador 1: ${ctx.user}\nJugador 2: ${option}`
)
.setFooter({ text: `Hay un máximo de 30 segundos para elegir.` });
const winEmbed = new EmbedBuilder()
.setColor('Green')
.setAuthor({ name: ctx.user.username, iconURL: ctx.user.displayAvatarURL() })
.setFooter({ text: `Gracias por jugar!` });
const tieEmbed = new EmbedBuilder()
.setColor('Yellow')
.setAuthor({ name: ctx.user.username, iconURL: ctx.user.displayAvatarURL() })
.setTitle(`Ha habido un empate <:Sadge:1015764348385382451>`)
.setDescription(`Qué sadge, ha habido un empate...`)
.setFooter({ text: `Volvemos a intentarlo?` });
const timeUpEmbed = new EmbedBuilder()
.setColor('Red')
.setAuthor({ name: ctx.user.username, iconURL: ctx.user.displayAvatarURL() })
.setTitle(`Se acabó!`)
.setDescription(
`Uno de los dos jugadores no han respondido en los 30 segundos, así que se acabó la partida!`
)
.setFooter({ text: `Volvemos a intentarlo?` });
const buttons = ['Piedra', 'Papel', 'Tijera'].map((choice) => {
return new ButtonBuilder()
.setLabel(choice)
.setCustomId(`rps-${choice.toLowerCase()}`)
.setStyle(ButtonStyle.Secondary);
});
const row = new ActionRowBuilder<ButtonBuilder>();
const message = await ctx.interaction.reply({
content: `${option}, te han retado a Piedra Papel o Tijera!`,
embeds: [waitingEmbed],
fetchReply: true,
components: [row.setComponents(buttons)],
});
const collector = message.createMessageComponentCollector({
time: 30_000,
componentType: ComponentType.Button,
filter: (i) => [ctx.user.id, option.id].includes(i.user.id),
});
collector.on('collect', async (i) => {
await i.deferReply({ ephemeral: true });
if (i.customId === 'rps-piedra') {
if (i.user.id === ctx.user.id) {
player1 = 1;
await i.editReply({
content: `Se ha respondido **piedra** correctamente, buena suerte!\n[Volver al mensaje](${message.url})`,
});
} else if (i.user.id === option.id) {
player2 = 1;
await i.editReply({
content: `Se ha respondido **piedra** correctamente, buena suerte!\n[Volver al mensaje](${message.url})`,
});
}
} else if (i.customId === 'rps-papel') {
if (i.user.id === ctx.user.id) {
player1 = 2;
await i.editReply({
content: `Se ha respondido **papel** correctamente, buena suerte!\n[Volver al mensaje](${message.url})`,
});
} else if (i.user.id === option.id) {
player2 = 2;
await i.editReply({
content: `Se ha respondido **papel** correctamente, buena suerte!\n[Volver al mensaje](${message.url})`,
});
}
} else if (i.customId === 'rps-tijera') {
if (i.user.id === ctx.user.id) {
player1 = 3;
await i.editReply({
content: `Se ha respondido **tijera** correctamente, buena suerte!\n[Volver al mensaje](${message.url})`,
});
} else if (i.user.id === option.id) {
player2 = 3;
await i.editReply({
content: `Se ha respondido **tijera** correctamente, buena suerte!\n[Volver al mensaje](${message.url})`,
});
}
}
if (player1 && player2) {
const checker = rockpaperscissors(player1, player2);
bothResponded = true;
if (checker === 'player1') {
winner = ctx.user.username;
const setDescription = winEmbed
.setDescription(`Tenemos resultados!\n**${winner}** ha ganado.`)
.setTitle(`Ha ganado ${winner}! <:Pog:1030169609178976346>`);
await message.edit({ embeds: [setDescription], components: [], content: `` });
message.react('<:Pog:1030169609178976346>');
} else if (checker === 'player2') {
winner = option.user.username;
const setDescription = winEmbed
.setDescription(`Tenemos resultados!\n**${winner}** ha ganado.`)
.setTitle(`Ha ganado ${winner}! <:Pog:1030169609178976346>`);
await message.edit({ embeds: [setDescription], components: [], content: `` });
message.react('<:Pog:1030169609178976346>');
} else if (checker === 'tie') {
await message.edit({ embeds: [tieEmbed], components: [], content: `` });
}
}
});
collector.on('ignore', async (i) => {
await i.reply({ content: 'No estás jugando!', ephemeral: true });
});
collector.on('end', async () => {
if (bothResponded) return;
await message.edit({ embeds: [timeUpEmbed], components: [], content: `` });
});
},
});

93
src/utils/wikipedia.ts Normal file
View File

@@ -0,0 +1,93 @@
import {
ActionRowBuilder,
AutocompleteInteraction,
ButtonBuilder,
ButtonStyle,
CacheType,
CommandInteractionOptionResolver,
EmbedBuilder,
} from 'discord.js';
import { Context } from '@sern/handler';
/**
Search Wikipedia for a given input string in the specified language using the Wikipedia API.
@async
@function searchWikipedia
@param {string} lang - The language code for the Wikipedia language edition to search in.
@param {AutocompleteInteraction} autocomplete - The autocomplete interaction object containing the input to search for.
@returns {Promise<SearchWikipediaObject[]>} - A promise that resolves with an array of search results.
**/
export async function searchWikipedia(
lang: string,
autocomplete: AutocompleteInteraction<CacheType>
) {
const input = autocomplete.options.getFocused();
if (!input) {
return [
{
ns: 0,
title: 'Empieza a escribir para buscar!',
pageid: 0,
size: 0,
wordcount: 0,
snippet: '',
timestamp: '',
},
] as SearchWikipediaObject[];
}
const request = await fetch(
`https://${lang}.wikipedia.org/w/api.php?action=query&list=search&format=json&srsearch=${input}`
).then(r => r.json())
return request.query.search as SearchWikipediaObject[];
}
export async function getWikipedia(
lang: string,
ctx: Context,
options: CommandInteractionOptionResolver<CacheType>
) {
const pageid = options.getString(lang === 'es' ? 'busqueda' : 'search', true);
if (Number.isNaN(Number(pageid)))
return ctx.reply({ content: 'Elige en el autocompletado el artículo.', ephemeral: true });
const request = await fetch(
`https://${lang}.wikipedia.org/w/api.php?action=query&prop=extracts&exintro&explaintext&pageids=${pageid}&format=json`
).then(r => r.json());
const firstParagraph = request.query.pages[pageid].extract.split('\n\n')[0] as string;
const title = request.query.pages[pageid].title as string;
const canonicalArticleURL = await fetch(
`https://${lang}.wikipedia.org/w/api.php?action=query&prop=info&pageids=${pageid}&inprop=url&format=json`
)
.then(async (res) => {
return (await res.json()).query.pages[pageid].canonicalurl as string;
});
const embed = new EmbedBuilder()
.setTitle(title)
.setColor('Random')
.setAuthor({ name: ctx.user.username, iconURL: ctx.user.displayAvatarURL() })
.setDescription(`${firstParagraph.slice(0, 500)}...`)
.setFooter({
text: `Resultado de Wikipedia en ${lang === 'es' ? 'español' : 'inglés'}`,
iconURL: 'https://i.imgur.com/pcpfc8i.png',
});
const button = new ActionRowBuilder<ButtonBuilder>().addComponents(
new ButtonBuilder()
.setLabel('URL al artículo')
.setURL(canonicalArticleURL)
.setStyle(ButtonStyle.Link)
);
await ctx.reply({ embeds: [embed], components: [button] });
}
export interface SearchWikipediaObject {
ns: number;
title: string;
pageid: number;
size: number;
wordcount: number;
snippet: string;
timestamp: string;
}