failed attempt

This commit is contained in:
2023-01-14 21:11:52 -03:00
parent 205b865592
commit efecaf2a76
17 changed files with 2570 additions and 204 deletions

View File

@@ -1,55 +1,65 @@
// {
// "extends": "eslint:recommended",
// "env": {
// "node": true,
// "es6": true
// },
// "parserOptions": {
// "ecmaVersion": 2021
// },
// "rules": {
// "arrow-spacing": ["warn", { "before": true, "after": true }],
// "brace-style": ["error", "stroustrup", { "allowSingleLine": true }],
// "comma-dangle": ["error", "always-multiline"],
// "comma-spacing": "error",
// "comma-style": "error",
// "curly": ["error", "multi-line", "consistent"],
// "dot-location": ["error", "property"],
// "handle-callback-err": "off",
// "indent": ["error", "tab"],
// "keyword-spacing": "error",
// "max-nested-callbacks": ["error", { "max": 4 }],
// "max-statements-per-line": ["error", { "max": 2 }],
// "no-console": "off",
// "no-empty-function": "error",
// "no-floating-decimal": "error",
// "no-inline-comments": "error",
// "no-lonely-if": "error",
// "no-multi-spaces": "error",
// "no-multiple-empty-lines": [
// "error",
// { "max": 2, "maxEOF": 1, "maxBOF": 0 }
// ],
// "no-shadow": ["error", { "allow": ["err", "resolve", "reject"] }],
// "no-trailing-spaces": ["error"],
// "no-var": "error",
// "object-curly-spacing": ["error", "always"],
// "prefer-const": "error",
// "quotes": ["error", "single"],
// "semi": ["error", "always"],
// "space-before-blocks": "error",
// "space-before-function-paren": [
// "error",
// {
// "anonymous": "never",
// "named": "never",
// "asyncArrow": "always"
// }
// ],
// "space-in-parens": "error",
// "space-infix-ops": "error",
// "space-unary-ops": "error",
// "spaced-comment": "error",
// "yoda": "error"
// }
// }
{
"extends": "eslint:recommended",
"env": {
"node": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 2021
},
"rules": {
"arrow-spacing": ["warn", { "before": true, "after": true }],
"brace-style": ["error", "stroustrup", { "allowSingleLine": true }],
"comma-dangle": ["error", "always-multiline"],
"comma-spacing": "error",
"comma-style": "error",
"curly": ["error", "multi-line", "consistent"],
"dot-location": ["error", "property"],
"handle-callback-err": "off",
"indent": ["error", "tab"],
"keyword-spacing": "error",
"max-nested-callbacks": ["error", { "max": 4 }],
"max-statements-per-line": ["error", { "max": 2 }],
"no-console": "off",
"no-empty-function": "error",
"no-floating-decimal": "error",
"no-inline-comments": "error",
"no-lonely-if": "error",
"no-multi-spaces": "error",
"no-multiple-empty-lines": [
"error",
{ "max": 2, "maxEOF": 1, "maxBOF": 0 }
],
"no-shadow": ["error", { "allow": ["err", "resolve", "reject"] }],
"no-trailing-spaces": ["error"],
"no-var": "error",
"object-curly-spacing": ["error", "always"],
"prefer-const": "error",
"quotes": ["error", "single"],
"semi": ["error", "always"],
"space-before-blocks": "error",
"space-before-function-paren": [
"error",
{
"anonymous": "never",
"named": "never",
"asyncArrow": "always"
}
],
"space-in-parens": "error",
"space-infix-ops": "error",
"space-unary-ops": "error",
"spaced-comment": "error",
"yoda": "error"
}
}
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"rules": {
"semi": ["error", "never"],
"quotes": [2, "single"],
"no-unused-vars": "error",
"@typescript-eslint/no-unused-vars": "error"
}
}

View File

@@ -7,6 +7,7 @@ RUN apt-get update \
&& apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
RUN npm install -g typescript
RUN yarn add puppeteer \
&& groupadd -r pptruser && useradd -r -g pptruser -G audio,video pptruser \
&& mkdir -p /home/pptruser/Downloads \

View File

@@ -4,8 +4,12 @@ require('dotenv').config();
const enviroment = process.env.NODE_ENV;
const commandFilesDir = enviroment === "production" ? './dist/commands' : './src/commands'
const commandFilesExtension = enviroment === "production" ? '.js' : '.ts'
const commands = [];
const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));
// const commandFiles = fs.readdirSync(commandFilesDir).filter(file => file.endsWith(commandFilesExtension));
const commandFiles = fs.readdirSync('./dist/commands').filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
const command = require(`./commands/${file}`);

66
dist/app.js vendored Normal file
View File

@@ -0,0 +1,66 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
const fs = require('fs');
const path = require('path');
const { Collection, Client, GatewayIntentBits, Events } = require('discord.js');
const { getRandomElementFromArray } = require('./constants');
require('dotenv').config();
const enviroment = process.env.NODE_ENV;
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.on('ready', () => {
console.log('working');
});
client.login(enviroment === 'production' ? process.env.PROD_BOT_TOKEN : process.env.DEV_BOT_TOKEN);
client.commands = new Collection();
const commandsPath = path.join(__dirname, 'commands');
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = require(filePath);
if ('data' in command && 'execute' in command) {
client.commands.set(command.data.name, command);
}
else {
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
}
}
client.on(Events.InteractionCreate, (interaction) => __awaiter(void 0, void 0, void 0, function* () {
if (enviroment !== 'production' && interaction.user.id !== process.env.MY_DISCORD_USER_ID) {
interaction.reply(`The dev instance of this bot is only for ${getRandomElementFromArray([`<@${process.env.MY_DISCORD_USER_ID}>`, 'the king'])}`);
return;
}
const command = interaction.client.commands.get(interaction.commandName);
if (interaction.isChatInputCommand()) {
try {
yield command.execute(interaction);
}
catch (error) {
console.error(error);
yield 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 {
yield command.autocomplete(interaction);
}
catch (error) {
console.error(error);
}
}
if (!command) {
console.error(`No command matching ${interaction.commandName} was found.`);
return;
}
}));

213
dist/commands/price.js vendored Normal file
View File

@@ -0,0 +1,213 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const { SlashCommandSubcommandBuilder, hyperlink, bold, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
const puppeteer = require('puppeteer');
const jsdom = require('jsdom');
const { responses } = require('../../constants');
function truncateText(text, max) {
return text.substr(0, max - 1).trim() + (text.length > max ? '...' : '');
}
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 ELEMENTS_LIMIT = 3;
const DISCORD_MESSAGE_LENGTH_LIMIT = 2000;
exports.default = {
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)),
autocomplete(interaction) {
return __awaiter(this, void 0, void 0, function* () {
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));
yield interaction.respond(filtered.map(choice => ({ name: choice, value: choice })));
});
},
execute(interaction) {
return __awaiter(this, void 0, void 0, function* () {
const userLanguage = interaction.locale || 'en-US';
yield interaction.deferReply();
const product = interaction.options.getString('product');
const platform = interaction.options.getString('platform');
if (platform && !pages.some(page => (page.name === platform))) {
yield interaction.editReply(responses(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 = yield puppeteer.launch({
args: ['--no-sandbox'],
});
const page = yield browser.newPage();
const searchUrl = countryPage.searchUrl.replace('%S', product);
const response = yield page.goto(searchUrl, { waitUntil: 'domcontentloaded' });
const body = yield 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);
}
});
yield 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(responses(userLanguage).platformInBrowser.replace('%P', page.name))
.setURL(page.searchUrl)
.setStyle(ButtonStyle.Link))));
const replyTexts = [
pagesScraped.length &&
`${responses(userLanguage).extractedFrom} ${pagesScraped.map(({ name }) => name).join(' ')}`,
`${productPrices.join('\n')}`,
pagesWithErrorScrapping.length &&
`${responses(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 = responses(userLanguage).discordMessageLengthLimit;
}
yield interaction.editReply({ content, components: [...buttons] });
});
},
};

5
nodemon.json Normal file
View File

@@ -0,0 +1,5 @@
{
"watch": ["src"],
"ext": "ts, json",
"exec": "ts-node ./src/app.ts"
}

View File

@@ -1,7 +1,7 @@
{
"scripts": {
"start": "node deploy-commands.js && node app.js",
"dev": "node deploy-commands.js && nodemon -L app.js"
"start": "node deploy-commands.js && npx tsc && node dist/app.js",
"dev": "npx tsc && node deploy-commands.js && nodemon -L src/app.ts"
},
"dependencies": {
"discord.js": "^14.6.0",
@@ -16,7 +16,11 @@
"author": "Francisco Pessano <franpessano1@gmail.com>",
"license": "MIT",
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.48.1",
"@typescript-eslint/parser": "^5.48.1",
"eslint": "^8.27.0",
"nodemon": "^2.0.20"
"nodemon": "^2.0.20",
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
}
}

View File

@@ -1,67 +1,67 @@
const fs = require('fs');
const path = require('path');
const { Collection, Client, GatewayIntentBits, Events } = require('discord.js');
const { getRandomElementFromArray } = require('./constants');
require('dotenv').config();
const fs = require('fs')
const path = require('path')
const { Collection, Client, GatewayIntentBits, Events } = require('discord.js')
const { getRandomElementFromArray } = require('./constants')
require('dotenv').config()
const enviroment = process.env.NODE_ENV;
const enviroment = process.env.NODE_ENV
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
const client = new Client({ intents: [GatewayIntentBits.Guilds] })
client.on('ready', () => {
console.log('working');
});
console.log('working')
})
client.login(enviroment === 'production' ? process.env.PROD_BOT_TOKEN : process.env.DEV_BOT_TOKEN);
client.login(enviroment === 'production' ? process.env.PROD_BOT_TOKEN : process.env.DEV_BOT_TOKEN)
client.commands = new Collection();
client.commands = new Collection()
const commandsPath = path.join(__dirname, 'commands');
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
const commandsPath = path.join(__dirname, 'commands')
const commandFiles = fs.readdirSync(commandsPath).filter((file: any) => file.endsWith('.js'))
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = require(filePath);
const filePath = path.join(commandsPath, file)
const command = require(filePath)
if ('data' in command && 'execute' in command) {
client.commands.set(command.data.name, command);
client.commands.set(command.data.name, command)
}
else {
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`);
console.log(`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`)
}
}
client.on(Events.InteractionCreate, async interaction => {
if (enviroment !== 'production' && interaction.user.id !== process.env.MY_DISCORD_USER_ID) {
interaction.reply(`The dev instance of this bot is only for ${getRandomElementFromArray([`<@${process.env.MY_DISCORD_USER_ID}>`, 'the king'])}`);
return;
interaction.reply(`The dev instance of this bot is only for ${getRandomElementFromArray([`<@${process.env.MY_DISCORD_USER_ID}>`, 'the king'])}`)
return
}
const command = interaction.client.commands.get(interaction.commandName);
const command = interaction.client.commands.get(interaction.commandName)
if (interaction.isChatInputCommand()) {
try {
await command.execute(interaction);
await command.execute(interaction)
}
catch (error) {
console.error(error);
await interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
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;
console.error(`No command matching ${interaction.commandName} was found.`)
return
}
try {
await command.autocomplete(interaction);
await command.autocomplete(interaction)
}
catch (error) {
console.error(error);
console.error(error)
}
}
if (!command) {
console.error(`No command matching ${interaction.commandName} was found.`);
return;
console.error(`No command matching ${interaction.commandName} was found.`)
return
}
});
})

View File

@@ -1,72 +1,16 @@
const { SlashCommandSubcommandBuilder, hyperlink, bold, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js');
const puppeteer = require('puppeteer');
const jsdom = require('jsdom');
const { responses } = require('../constants');
import { AutocompleteInteraction, CommandOptionChoiceResolvableType } from 'discord.js'
import { countryData } from '../utils/constants'
function truncateText(text, max) {
return text.substr(0, max - 1).trim() + (text.length > max ? '...' : '');
}
const { SlashCommandSubcommandBuilder, hyperlink, bold, ActionRowBuilder, ButtonBuilder, ButtonStyle } = require('discord.js')
const puppeteer = require('puppeteer')
const jsdom = require('jsdom')
const { responses } = require('../../constants')
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 pages = Object.values(countryData).map((country) => country.pages).flat(1);
const ELEMENTS_LIMIT = 3
const ELEMENTS_LIMIT = 3;
const DISCORD_MESSAGE_LENGTH_LIMIT = 2000;
module.exports = {
export default {
data: new SlashCommandSubcommandBuilder()
.setName('prices')
.setNameLocalizations({
@@ -76,7 +20,7 @@ module.exports = {
.setDescriptionLocalizations({
'es-ES': 'Consigue los precios de algún producto',
})
.addStringOption(option => option
.addStringOption((option: any) => option
.setName('product')
.setNameLocalizations({
'es-ES': 'producto',
@@ -87,7 +31,7 @@ module.exports = {
})
.setRequired(true)
.setMaxLength(200))
.addStringOption(option => option
.addStringOption((option: any) => option
.setName('country')
.setNameLocalizations({
'es-ES': 'país',
@@ -102,7 +46,7 @@ module.exports = {
value: key,
})),
))
.addStringOption(option => option
.addStringOption((option: any) => option
.setName('platform')
.setNameLocalizations({
'es-ES': 'plataforma',
@@ -114,23 +58,23 @@ module.exports = {
.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));
async autocomplete(interaction: AutocompleteInteraction) {
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: string) => choice.startsWith(focusedValue))
await interaction.respond(
filtered.map(choice => ({ name: choice, value: choice })),
);
filtered.map((choice: CommandOptionChoiceResolvableType) => ({ 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 userLanguage = interaction.locale || 'en-US'
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(userLanguage).missingPlatform);
return;
await interaction.editReply(responses(userLanguage).missingPlatform)
return
}
const country = interaction.options.getString('country') ||
(platform ?
@@ -138,58 +82,58 @@ module.exports = {
.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 = [];
)
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 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 { window: { document } } = new jsdom.JSDOM(body)
const products = document.querySelectorAll(countryPage.selectors.container);
const products = document.querySelectorAll(countryPage.selectors.container)
if (!products.length) {
throw Error();
throw Error()
}
products
.forEach((element) => {
if (productPrices.length >= ELEMENTS_LIMIT) {
return;
return
}
try {
const productRelativePath = element
.querySelector(countryPage.selectors.link)
.getAttribute('href')
.replace(/.*\/\/[^/]*/, '');
const productName = element.querySelector(countryPage.selectors.title).textContent;
.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}`);
)
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);
console.log(`FUCK ${countryPage.name} MAQUETATION`)
console.error(err)
}
});
await browser.close();
pagesScraped.push({ name: countryPage.name, searchUrl: encodeURI(searchUrl) });
})
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);
pagesWithErrorScrapping.push(countryPage.name)
console.log(`FUCK ${countryPage.name}`)
console.error(err)
}
}
const buttons = pagesScraped.map(page =>
@@ -200,19 +144,19 @@ module.exports = {
.setURL(page.searchUrl)
.setStyle(ButtonStyle.Link),
)),
);
)
const replyTexts = [
pagesScraped.length &&
`${responses(userLanguage).extractedFrom} ${pagesScraped.map(({ name }) => name).join(' ')}`,
`${productPrices.join('\n')}`,
pagesWithErrorScrapping.length &&
`${responses(userLanguage).errorScrapping} ${pagesWithErrorScrapping.map((name) => name).join(' ')}`,
].filter(a => a);
const response = replyTexts.join('\n\n');
let content;
].filter(a => a)
const response = replyTexts.join('\n\n')
let content
if (response.length >= DISCORD_MESSAGE_LENGTH_LIMIT) {
content = responses(userLanguage).discordMessageLengthLimit;
content = responses(userLanguage).discordMessageLengthLimit
}
await interaction.editReply({ content, components: [...buttons] });
await interaction.editReply({ content, components: [...buttons] })
},
};
}

View File

@@ -0,0 +1,3 @@
export default function truncateText(text: string, max: number) {
return text.substr(0, max - 1).trim() + (text.length > max ? '...' : '')
}

55
src/utils/constants.ts Normal file
View File

@@ -0,0 +1,55 @@
export 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',
},
},
],
},
}
export const DISCORD_MESSAGE_LENGTH_LIMIT = 2000

103
tsconfig.json Normal file
View File

@@ -0,0 +1,103 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "commonjs", /* Specify what module code is generated. */
"rootDir": "./src", /* Specify the root folder within your source files. */
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
"strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */
}
}

1958
yarn-error.log Normal file

File diff suppressed because it is too large Load Diff