From 266189776788ce1d8788058f3847032fe8bd31bb Mon Sep 17 00:00:00 2001 From: Francisco Pessano Date: Sat, 12 Nov 2022 16:48:31 -0300 Subject: [PATCH] first version of prices command done --- .gitignore | 2 +- app.js | 34 +++++--- commands/price.js | 193 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 6 +- 4 files changed, 222 insertions(+), 13 deletions(-) create mode 100644 commands/price.js diff --git a/.gitignore b/.gitignore index aba5c70..c580a33 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ node_modules yarn.lock -.env \ No newline at end of file +.env diff --git a/app.js b/app.js index 7130234..dd53596 100644 --- a/app.js +++ b/app.js @@ -7,7 +7,6 @@ const client = new Client({ intents: [GatewayIntentBits.Guilds] }); client.on('ready', () => { console.log('working'); - console.log(client.user); }); client.login(process.env.BOT_TOKEN); @@ -29,21 +28,36 @@ for (const file of commandFiles) { } client.on(Events.InteractionCreate, async interaction => { - if (!interaction.isChatInputCommand()) return; - console.log(interaction); + // if (!interaction.isChatInputCommand()) return; const command = interaction.client.commands.get(interaction.commandName); + if (interaction.isChatInputCommand()) { + try { + await command.execute(interaction); + } + catch (error) { + console.error(error); + await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true }); + } + } + else if (interaction.isAutocomplete()) { + if (!command) { + console.error(`No command matching ${interaction.commandName} was found.`); + return; + } + + try { + await command.autocomplete(interaction); + } + catch (error) { + console.error(error); + } + } if (!command) { console.error(`No command matching ${interaction.commandName} was found.`); return; } - try { - await command.execute(interaction); - } - catch (error) { - console.error(error); - await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true }); - } + }); diff --git a/commands/price.js b/commands/price.js new file mode 100644 index 0000000..b305eea --- /dev/null +++ b/commands/price.js @@ -0,0 +1,193 @@ +const { SlashCommandSubcommandBuilder, hyperlink, bold } = require('discord.js'); +const puppeteer = require('puppeteer'); +const jsdom = require('jsdom'); + +const countryData = { + 'ar': { + name: 'Argentina', + currency: 'ARS', + pages: [ + { + name: 'Mercado Libre (Argentina)', + searchUrl: 'https://listado.mercadolibre.com.ar/%S#D%5BA:%S', + productUrl: 'https://articulo.mercadolibre.com.ar%S', + selectors: { + container: 'div.andes-card.ui-search-result', + link: 'a.ui-search-link', + price: 'span.price-tag-fraction', + title: 'h2.ui-search-item__title.shops__item-title', + }, + }, + ], + }, + 'us': { + name: 'United States', + currency: 'USD', + pages: [ + { + name: 'Amazon (United States)', + searchUrl: 'https://www.amazon.com/s?k=%S', + productUrl: 'https://www.amazon.com%S', + selectors: { + container: 'div.s-card-container > div.a-section > div.sg-row', + link: 'h2.a-size-mini a.a-link-normal', + price: 'span.a-price span.a-offscreen', + title: 'h2.a-size-mini span.a-size-medium', + }, + }, + ], + }, + 'cl': { + name: 'Chile', + currency: 'CLP', + pages: [ + { + name: 'Falabella', + searchUrl: 'https://www.falabella.com/falabella-cl/search?Ntt=%S', + productUrl: 'https://www.falabella.com%S', + selectors: { + container: 'div.pod-4_GRID', + link: 'a', + price: 'div.prices span.copy10', + title: 'div.pod-details a.pod-link span > b.pod-subTitle', + }, + }, + ], + }, +}; + +const pages = Object.values(countryData).map((country) => country.pages).flat(1); + +const responses = { + 'extractedFrom': { + 'en-US': 'Prices extracted from:', + 'es-ES': 'Precios extraídos de:', + }, + 'missingPlatform': { + 'en-US': 'ERROR: Platform don\'t found!!', + 'es-ES': 'ERROR: Plataforma no encontrada!!', + }, +}; + +const ELEMENTS_LIMIT = 3; + +module.exports = { + data: new SlashCommandSubcommandBuilder() + .setName('prices') + .setNameLocalizations({ + 'es-ES': 'precios', + }) + .setDescription('Get the prices of a product') + .setDescriptionLocalizations({ + 'es-ES': 'Consigue los precios de algún producto', + }) + .addStringOption(option => option + .setName('product') + .setNameLocalizations({ + 'es-ES': 'producto', + }) + .setDescription('Product that you want to search') + .setDescriptionLocalizations({ + 'es-ES': 'Producto que quieres buscar', + }) + .setRequired(true) + .setMaxLength(200)) + .addStringOption(option => option + .setName('country') + .setNameLocalizations({ + 'es-ES': 'país', + }) + .setDescription('Country where search the prices') + .setDescriptionLocalizations({ + 'es-ES': 'País en donde encontrar los precios', + }) + .addChoices( + ...Object.entries(countryData).map(([key, value]) => ({ + name: value.name, + value: key, + })), + )) + .addStringOption(option => option + .setName('platform') + .setNameLocalizations({ + 'es-ES': 'plataforma', + }) + .setDescription('Specify a platform to search') + .setDescriptionLocalizations({ + 'es-ES': 'Especificar una plataforma para la busqueda', + }) + .setMaxLength(200) + .setAutocomplete(true), + ), + async autocomplete(interaction) { + const focusedValue = interaction.options.getFocused(); + const country = interaction.options.getString('country'); + const choices = (country ? countryData[country].pages : pages).map(page => (page.name)); + const filtered = choices.filter(choice => choice.startsWith(focusedValue)); + await interaction.respond( + filtered.map(choice => ({ name: choice, value: choice })), + ); + }, + async execute(interaction) { + await interaction.deferReply(); + const product = interaction.options.getString('product'); + const platform = interaction.options.getString('platform'); + if (platform && !pages.some(page => (page.name === platform))) { + await interaction.editReply(responses.missingPlatform[interaction.locale]); + return; + } + const country = interaction.options.getString('country') || + (platform ? + Object.entries(countryData) + .map(([key, value]) => value.pages.some(page => page.name === platform) ? key : false) + .filter(a => a)[0] + : 'us' + ); + const countryPages = countryData[country].pages.filter(countryPage => platform ? countryPage.name === platform : countryPage); + const productPrices = []; + const pagesScraped = []; + try { + for (const countryPage of countryPages) { + const browser = await puppeteer.launch() ; + const page = await browser.newPage(); + const response = await page.goto( + countryPage.searchUrl.replace('%S', product), { waitUntil: 'domcontentloaded' }, + ); + const body = await response.text(); + + const { window: { document } } = new jsdom.JSDOM(body); + + document.querySelectorAll(countryPage.selectors.container) + .forEach((element) => { + if (productPrices.length >= ELEMENTS_LIMIT) { + return; + } + try { + const productRelativePath = element + .querySelector(countryPage.selectors.link) + .getAttribute('href') + .replace(/.*\/\/[^/]*/, ''); + const link = hyperlink( + element.querySelector(countryPage.selectors.title).textContent, + countryPage.productUrl.replace('%S', productRelativePath), + ); + const priceNumber = element.querySelector(countryPage.selectors.price).textContent.replace('$', '').replace(' ', ''); + const price = `${countryData[country].currency} ${bold(priceNumber)}`; + productPrices.push(`${link} - ${price}`); + } + catch (err) { + console.log(`FUCK ${countryPage.name} MAQUETATION`); + console.log(err); + } + }); + + await browser.close(); + pagesScraped.push(countryPage.name); + } + } + catch (err) { + console.error(err); + } + await interaction.editReply(`${responses.extractedFrom[interaction.locale || 'en-US']} ${pagesScraped.join(' ')}\n\n${productPrices.join('\n')}`); + }, +}; \ No newline at end of file diff --git a/package.json b/package.json index 9ccd6ea..f1e66b3 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,13 @@ { "scripts": { "start": "node app.js", - "dev": "nodemon app.js" + "dev": "node deploy-commands.js && nodemon app.js" }, "dependencies": { "discord.js": "^14.6.0", - "dotenv": "^16.0.3" + "dotenv": "^16.0.3", + "jsdom": "^20.0.2", + "puppeteer": "^19.2.2" }, "name": "shopping-discord-bot", "version": "1.0.0",