From 5acccc6ffe6fea1524bf15f8fbd6a09519196089 Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Sun, 6 Jul 2025 18:36:51 +0200 Subject: [PATCH] feat: wikipedia and rps --- TODO.md | 12 +-- bun.lock | 3 + package.json | 1 + src/commands/misc/wikipedia.ts | 69 +++++++++++++++ src/commands/silly/rps.ts | 148 +++++++++++++++++++++++++++++++++ src/utils/wikipedia.ts | 93 +++++++++++++++++++++ 6 files changed, 318 insertions(+), 8 deletions(-) create mode 100644 src/commands/misc/wikipedia.ts create mode 100644 src/commands/silly/rps.ts create mode 100644 src/utils/wikipedia.ts diff --git a/TODO.md b/TODO.md index 682daba..b13520d 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/bun.lock b/bun.lock index 8751428..63411fe 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], diff --git a/package.json b/package.json index e32c559..2143710 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/commands/misc/wikipedia.ts b/src/commands/misc/wikipedia.ts new file mode 100644 index 0000000..3ac036e --- /dev/null +++ b/src/commands/misc/wikipedia.ts @@ -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 + switch (ctx.options.getSubcommand()) { + case 'español': { + getWikipedia('es', ctx, options); + } break; + case 'ingles': { + getWikipedia('en', ctx, options); + } break; + } + }, +}); \ No newline at end of file diff --git a/src/commands/silly/rps.ts b/src/commands/silly/rps.ts new file mode 100644 index 0000000..63d8a30 --- /dev/null +++ b/src/commands/silly/rps.ts @@ -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(); + 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: `` }); + }); + }, +}); diff --git a/src/utils/wikipedia.ts b/src/utils/wikipedia.ts new file mode 100644 index 0000000..3b77e2f --- /dev/null +++ b/src/utils/wikipedia.ts @@ -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} - A promise that resolves with an array of search results. +**/ +export async function searchWikipedia( + lang: string, + autocomplete: AutocompleteInteraction +) { + 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 +) { + 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().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; +}