mirror of
https://github.com/FranP-code/shopping-discord-bot.git
synced 2025-10-13 00:22:44 +00:00
175 lines
6.1 KiB
JavaScript
175 lines
6.1 KiB
JavaScript
const { SlashCommandSubcommandBuilder, hyperlink, bold, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
|
|
const puppeteer = require('puppeteer');
|
|
const jsdom = require('jsdom');
|
|
const { countryData, DISCORD_MESSAGE_LENGTH_LIMIT } = require('../utils/constants');
|
|
const truncateText = require('../scripts/truncateText');
|
|
const generateLocalizedResponses = require('../scripts/generateLocalizedResponses');
|
|
|
|
const pages = Object.values(countryData).map((country) => country.pages).flat(1);
|
|
|
|
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),
|
|
)
|
|
.addIntegerOption(option => option
|
|
.setName('limit')
|
|
.setNameLocalizations({
|
|
'es-ES': 'límite',
|
|
})
|
|
.setDescription('Define the limit of results of search')
|
|
.setDescriptionLocalizations({
|
|
'es-ES': 'Definir el límite de resultados de la busqueda',
|
|
})
|
|
.setMaxValue(5)
|
|
,
|
|
),
|
|
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) {
|
|
const userLanguage = interaction.locale || 'en-US';
|
|
await interaction.deferReply();
|
|
const product = interaction.options.getString('product');
|
|
const platform = interaction.options.getString('platform');
|
|
const ELEMENTS_LIMIT = interaction.options.getInteger('limit') || 3;
|
|
if (platform && !pages.some(page => (page.name === platform))) {
|
|
await interaction.editReply(generateLocalizedResponses(userLanguage).missingPlatform);
|
|
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 = [];
|
|
const pagesWithErrorScrapping = [];
|
|
for (const countryPage of countryPages) {
|
|
try {
|
|
const browser = await puppeteer.launch({
|
|
args: ['--no-sandbox'],
|
|
}) ;
|
|
const page = await browser.newPage();
|
|
const searchUrl = countryPage.searchUrl.replace('%S', product);
|
|
const response = await page.goto(searchUrl, { waitUntil: 'domcontentloaded' });
|
|
const body = await response.text();
|
|
|
|
const { window: { document } } = new jsdom.JSDOM(body);
|
|
|
|
const products = document.querySelectorAll(countryPage.selectors.container);
|
|
if (!products.length) {
|
|
throw Error();
|
|
}
|
|
products
|
|
.forEach((element) => {
|
|
if (productPrices.length >= ELEMENTS_LIMIT) {
|
|
return;
|
|
}
|
|
try {
|
|
const productRelativePath = element
|
|
.querySelector(countryPage.selectors.link)
|
|
.getAttribute('href')
|
|
.replace(/.*\/\/[^/]*/, '');
|
|
const productName = element.querySelector(countryPage.selectors.title).textContent;
|
|
const link = hyperlink(
|
|
truncateText(productName, 100),
|
|
countryPage.productUrl.replace('%S', encodeURI(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.error(err);
|
|
}
|
|
});
|
|
await browser.close();
|
|
pagesScraped.push({ name: countryPage.name, searchUrl: encodeURI(searchUrl) });
|
|
}
|
|
catch (err) {
|
|
pagesWithErrorScrapping.push(countryPage.name);
|
|
console.log(`FUCK ${countryPage.name}`);
|
|
console.error(err);
|
|
}
|
|
}
|
|
const buttons = pagesScraped.map(page =>
|
|
(new ActionRowBuilder()
|
|
.addComponents(
|
|
new ButtonBuilder()
|
|
.setLabel(generateLocalizedResponses(userLanguage).platformInBrowser.replace('%P', page.name))
|
|
.setURL(page.searchUrl)
|
|
.setStyle(ButtonStyle.Link),
|
|
)),
|
|
);
|
|
const replyTexts = [
|
|
pagesScraped.length &&
|
|
`${generateLocalizedResponses(userLanguage).extractedFrom} ${pagesScraped.map(({ name }) => name).join(' ')}`,
|
|
`${productPrices.join('\n')}`,
|
|
pagesWithErrorScrapping.length &&
|
|
`${generateLocalizedResponses(userLanguage).errorScrapping} ${pagesWithErrorScrapping.map((name) => name).join(' ')}`,
|
|
].filter(a => a);
|
|
const response = replyTexts.join('\n\n');
|
|
let content;
|
|
if (response.length >= DISCORD_MESSAGE_LENGTH_LIMIT) {
|
|
content = generateLocalizedResponses(userLanguage).discordMessageLengthLimit;
|
|
}
|
|
else {
|
|
content = response;
|
|
}
|
|
await interaction.editReply({ content, components: [...buttons] });
|
|
},
|
|
}; |