commit 9ddee2bb184b8b524f41b7cc21b8c4e22f448d44 Author: axel Date: Mon Apr 6 20:49:50 2026 -0300 initial-commit diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..b34b449 --- /dev/null +++ b/.env.example @@ -0,0 +1,50 @@ +# ============================================ +# Configuração do Bot Discord de Ofertas +# ============================================ + +# Token do bot Discord (obrigatório) +# Obtenha em: https://discord.com/developers/applications +DISCORD_TOKEN=SEU_TOKEN_AQUI + +# ID do canal onde as ofertas serão enviadas (obrigatório) +# Clique com botão direito no canal > Copiar ID do canal +DISCORD_CHANNEL_ID=SEU_CHANNEL_ID_AQUI + +# ============================================ +# URLs de Monitoramento (opcionais - já há padrões) +# ============================================ + +# Amazon Brasil - Ofertas do Dia +AMAZON_URL=https://www.amazon.com.br/deals + +# AliExpress - Ofertas Relâmpago +ALIEXPRESS_URL=https://pt.aliexpress.com/campaign/wow/gcp-plus/ae/right/shareon + +# Shopee - Flash Sale +SHOPEE_URL=https://shopee.com.br/flash_sale + +# Mercado Livre - Ofertas do Dia +MERCADOLIVRE_URL=https://www.mercadolivre.com.br/ofertas + +# ============================================ +# Configurações do Scraping +# ============================================ + +# Intervalo entre execuções do scraping (em minutos) +SCRAPE_INTERVAL_MINUTES=30 + +# Número máximo de ofertas por site por execução +MAX_OFFERS_PER_SITE=5 + +# Tag de afiliado Amazon (opcional) +AMAZON_AFFILIATE_TAG= + +# ============================================ +# Configurações do Puppeteer +# ============================================ + +# Executar navegador em modo headless (true/false) +PUPPETEER_HEADLESS=true + +# Timeout para carregamento de página (em ms) +PAGE_TIMEOUT=30000 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..40f5df4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.env +data/ +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..dca5c5d --- /dev/null +++ b/README.md @@ -0,0 +1,138 @@ +# 🛒 Bot de Ofertas - E-commerce para Discord + +Bot automatizado que monitora os principais e-commerces brasileiros e envia ofertas diretamente para seu servidor Discord. + +## 🏪 Lojas Monitoradas + +| Loja | Página Monitorada | +|------|-------------------| +| 🛒 Amazon Brasil | Ofertas do Dia | +| 🌐 AliExpress | Super Ofertas / Flash Deals | +| 🛍️ Shopee | Flash Sale / Ofertas Relâmpago | +| 🤝 Mercado Livre | Ofertas do Dia | + +## ✨ Funcionalidades + +- **Monitoramento automático** a cada 30 minutos (configurável) +- **Embeds ricos** com imagem, preço, desconto e link direto +- **Deduplicação inteligente** — nunca envia a mesma oferta duas vezes no dia +- **Reset diário** automático do cache de ofertas +- **Detecção de cupons** quando disponíveis na página +- **Frete grátis** identificado automaticamente (Mercado Livre) +- **Suporte a afiliados** Amazon (tag configurável) +- **Retry automático** em caso de falha no scraping +- **Logs detalhados** de cada ciclo de verificação + +## 📋 Pré-requisitos + +- **Node.js** v18 ou superior +- **Token de Bot Discord** ([criar aqui](https://discord.com/developers/applications)) +- **Chromium** (instalado automaticamente pelo Puppeteer) + +## 🚀 Instalação + +### 1. Clone ou baixe o projeto + +```bash +cd "Nova pasta" +``` + +### 2. Instale as dependências + +```bash +npm install +``` + +### 3. Configure o arquivo `.env` + +```bash +# Copie o template +cp .env.example .env + +# Edite com suas configurações +notepad .env +``` + +Preencha **obrigatoriamente**: +- `DISCORD_TOKEN` — Token do seu bot Discord +- `DISCORD_CHANNEL_ID` — ID do canal onde as ofertas serão enviadas + +### 4. Inicie o bot + +```bash +npm start +``` + +## ⚙️ Configurações + +Todas as configurações ficam no arquivo `.env`: + +| Variável | Descrição | Padrão | +|----------|-----------|--------| +| `DISCORD_TOKEN` | Token do bot Discord | *obrigatório* | +| `DISCORD_CHANNEL_ID` | ID do canal de ofertas | *obrigatório* | +| `SCRAPE_INTERVAL_MINUTES` | Intervalo entre verificações | `30` | +| `MAX_OFFERS_PER_SITE` | Máx. ofertas por site por ciclo | `5` | +| `AMAZON_AFFILIATE_TAG` | Tag de afiliado Amazon | *vazio* | +| `PUPPETEER_HEADLESS` | Navegador invisível | `true` | +| `PAGE_TIMEOUT` | Timeout de carregamento (ms) | `30000` | + +## 📁 Estrutura do Projeto + +``` +├── .env.example # Template de configurações +├── .gitignore # Arquivos ignorados pelo Git +├── package.json # Dependências e scripts +├── README.md # Esta documentação +├── data/ # Cache de deduplicação (auto-criado) +│ └── sent_offers.json # Registro de ofertas enviadas +└── src/ + ├── index.js # Ponto de entrada + ├── config.js # Configurações centralizadas + ├── bot.js # Lógica principal do bot + ├── scrapers/ # Módulos de scraping por loja + │ ├── index.js # Agregador de scrapers + │ ├── amazon.js # Scraper Amazon Brasil + │ ├── aliexpress.js # Scraper AliExpress + │ ├── shopee.js # Scraper Shopee + │ └── mercadolivre.js # Scraper Mercado Livre + └── utils/ # Utilitários + ├── browser.js # Gerenciador Puppeteer + ├── dedup.js # Sistema de deduplicação + └── embed.js # Construtor de embeds Discord +``` + +## 🤖 Criando o Bot no Discord + +1. Acesse [Discord Developer Portal](https://discord.com/developers/applications) +2. Clique em **"New Application"** e dê um nome +3. Vá em **"Bot"** no menu lateral +4. Clique em **"Add Bot"** +5. Copie o **Token** e cole no `.env` +6. Em **"Privileged Gateway Intents"**, ative: + - ✅ Message Content Intent +7. Vá em **"OAuth2" > "URL Generator"** + - Selecione scope: `bot` + - Selecione permissões: `Send Messages`, `Embed Links`, `Attach Files` +8. Copie a URL gerada e abra no navegador para adicionar o bot ao seu servidor + +## ⚠️ Observações Importantes + +- **Sites podem mudar seus layouts** — os seletores CSS podem precisar de atualização periódica. +- **Rate limiting** — o bot respeita limites de taxa do Discord com delays entre envios. +- **Uso de recursos** — Puppeteer consome memória; recomenda-se ao menos 2GB RAM. +- **Termos de uso** — Verifique os termos de cada site antes de usar em produção. + +## 🔧 Troubleshooting + +| Problema | Solução | +|----------|---------| +| Bot não conecta | Verifique se o `DISCORD_TOKEN` está correto | +| Canal não encontrado | Verifique o `DISCORD_CHANNEL_ID` e se o bot tem acesso | +| Nenhuma oferta | Sites podem ter mudado layout; verifique logs | +| Erro de timeout | Aumente `PAGE_TIMEOUT` no `.env` | +| Alto uso de memória | Reduza `MAX_OFFERS_PER_SITE` ou aumente intervalo | + +## 📄 Licença + +MIT diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..189d1b8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1800 @@ +{ + "name": "discord-ofertas-bot", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "discord-ofertas-bot", + "version": "1.0.0", + "dependencies": { + "cheerio": "^1.0.0", + "discord.js": "^14.16.3", + "dotenv": "^16.4.7", + "puppeteer": "^23.11.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz", + "integrity": "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.6.2", + "@discordjs/util": "^1.2.0", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.38.40", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz", + "integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.1.tgz", + "integrity": "sha512-wwQdgjeaoYFiaG+atbqx6aJDpqW7JHAo0HrQkBTbYzM3/PJ3GweQIpgElNcGZ26DCUOXMyawYd0YF7vtr+fZXg==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.2.0", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.5", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.38.40", + "magic-bytes.js": "^1.13.0", + "tslib": "^2.6.3", + "undici": "6.24.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@sapphire/snowflake": { + "version": "3.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz", + "integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@discordjs/rest/node_modules/undici": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", + "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/@discordjs/util": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz", + "integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.38.33" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz", + "integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.5.1", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.38.1", + "tslib": "^2.6.2", + "ws": "^8.17.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.1.tgz", + "integrity": "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==", + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.0", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", + "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.7", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz", + "integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.6.0.tgz", + "integrity": "sha512-2YkS7NuiJceSEbyEOdSNLE9tsGd+f4+f7C+Nik/MCk27SYdwIMPT/yRKvg++FZhQXgk0KWJKJyXX9RhVV0RGqA==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.8.7", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.7.tgz", + "integrity": "sha512-G4Gr1UsGeEy2qtDTZwL7JFLo2wapUarz7iTMcYcMFdS89AIQuBoyjgXZz0Utv7uHs3xA9LckhVbeBi8lEQrC+w==", + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.12.0.tgz", + "integrity": "sha512-w28i8lkBgREV3rPXGbgK+BO66q+ZpKqRWrZLiCdmmUlLPrQ45CzkvRhN+7lnv00Gpi2zy5naRxnUFAxCECDm9g==", + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.0.tgz", + "integrity": "sha512-NSTU5WN+fy/L0DDenfE8SXQna4voXuW0FHM7wH8i3/q9khUSchfPbPezO4zSFMnDGIf9YE+mt/RWhZgNRKRIXA==", + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/basic-ftp": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chromium-bidi": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.11.0.tgz", + "integrity": "sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA==", + "license": "Apache-2.0", + "dependencies": { + "mitt": "3.0.1", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1367902", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz", + "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==", + "license": "BSD-3-Clause" + }, + "node_modules/discord-api-types": { + "version": "0.38.44", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.44.tgz", + "integrity": "sha512-q91MgBzP/gRaCLIbQTaOrOhbD8uVIaPKxpgX2sfFB2nZ9nSiTYM9P3NFQ7cbO6NCxctI6ODttc67MI+YhIfILg==", + "license": "MIT", + "workspaces": [ + "scripts/actions/documentation" + ] + }, + "node_modules/discord.js": { + "version": "14.26.2", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.26.2.tgz", + "integrity": "sha512-feShi+gULJ6R2MAA4/KkCFnkJcuVrROJrKk4czplzq8gE1oqhqgOy9K0Scu44B8oGeWKe04egquzf+ia6VtXAw==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/builders": "^1.14.1", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.2", + "@discordjs/rest": "^2.6.1", + "@discordjs/util": "^1.2.0", + "@discordjs/ws": "^1.2.3", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.38.40", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "magic-bytes.js": "^1.13.0", + "tslib": "^2.6.3", + "undici": "6.24.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/discord.js/node_modules/undici": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", + "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "license": "MIT" + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-bytes.js": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.0.tgz", + "integrity": "sha512-afO2mnxW7GDTXMm5/AoN1WuOcdoKhtgXjIvHmobqTD1grNplhGdv3PFOyjCVmrnOZBIT/gD/koDKpYG+0mvHcg==", + "license": "MIT" + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/netmask": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.0.tgz", + "integrity": "sha512-z9sZrk6wyf8/NDKKqe+Tyl58XtgkYrV4kgt1O8xrzYvpl1LvPacPo0imMLHfpStk3kgCIq1ksJ2bmJn9hue2lQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/puppeteer": { + "version": "23.11.1", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-23.11.1.tgz", + "integrity": "sha512-53uIX3KR5en8l7Vd8n5DUv90Ae9QDQsyIthaUFVzwV6yU750RjqRznEtNMBT20VthqAdemnJN+hxVdmMHKt7Zw==", + "deprecated": "< 24.15.0 is no longer supported", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.6.1", + "chromium-bidi": "0.11.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1367902", + "puppeteer-core": "23.11.1", + "typed-query-selector": "^2.12.0" + }, + "bin": { + "puppeteer": "lib/cjs/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "23.11.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.11.1.tgz", + "integrity": "sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==", + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.6.1", + "chromium-bidi": "0.11.0", + "debug": "^4.4.0", + "devtools-protocol": "0.0.1367902", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "license": "MIT" + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typed-query-selector": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.1.tgz", + "integrity": "sha512-uzR+FzI8qrUEIu96oaeBJmd9E7CFEiQ3goA5qCVgc4s5llSubcfGHq9yUstZx/k4s9dXHVKsE35YWoFyvEqEHA==", + "license": "MIT" + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/undici": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.7.tgz", + "integrity": "sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..4f807f3 --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "discord-ofertas-bot", + "version": "1.0.0", + "description": "Bot Discord para monitoramento de ofertas em e-commerces brasileiros", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "dev": "node --watch src/index.js" + }, + "dependencies": { + "cheerio": "^1.0.0", + "discord.js": "^14.16.3", + "dotenv": "^16.4.7", + "puppeteer": "^23.11.1" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/src/bot.js b/src/bot.js new file mode 100644 index 0000000..3111275 --- /dev/null +++ b/src/bot.js @@ -0,0 +1,187 @@ +/** + * bot.js - Lógica principal do Bot de Ofertas + * + * Gerencia o ciclo de scraping: + * 1. Executa todos os scrapers + * 2. Filtra ofertas duplicadas + * 3. Envia ofertas novas para o canal do Discord + * 4. Agenda próxima execução + */ + +const { Client, GatewayIntentBits, Events } = require('discord.js'); +const { config } = require('./config'); +const { scrapers } = require('./scrapers'); +const { buildOfferEmbed, buildSummaryEmbed, buildStatusEmbed } = require('./utils/embed'); +const { filterNewOffers, markMultipleAsSent } = require('./utils/dedup'); +const { closeBrowser } = require('./utils/browser'); + +// ── Instância do cliente Discord ──────────────────────── +const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + ], +}); + +let scrapeInterval = null; // Referência ao setInterval para limpeza + +/** + * Inicializa o bot e conecta ao Discord + */ +async function startBot() { + // Evento: Bot pronto + client.once(Events.ClientReady, async (readyClient) => { + console.log(''); + console.log('═══════════════════════════════════════════════'); + console.log(` 🤖 Bot conectado como: ${readyClient.user.tag}`); + console.log(` 📡 Canal de ofertas: ${config.discord.channelId}`); + console.log(` ⏱️ Intervalo: ${config.scraping.intervalMinutes} minutos`); + console.log(` 📦 Máx. ofertas/site: ${config.scraping.maxOffersPerSite}`); + console.log('═══════════════════════════════════════════════'); + console.log(''); + + // Verificar se o canal existe e é acessível + const channel = await client.channels.fetch(config.discord.channelId).catch(() => null); + if (!channel) { + console.error('❌ Canal do Discord não encontrado! Verifique o DISCORD_CHANNEL_ID.'); + process.exit(1); + } + + console.log(`✅ Canal encontrado: #${channel.name}`); + + // Enviar mensagem de status inicial + try { + const statusEmbed = buildStatusEmbed( + `🟢 Bot iniciado com sucesso!\n\n` + + `⏱️ Monitorando ofertas a cada **${config.scraping.intervalMinutes} minutos**.\n` + + `🏪 Lojas monitoradas: Amazon, AliExpress, Shopee, Mercado Livre\n\n` + + `Primeira verificação iniciando agora...` + ); + await channel.send({ embeds: [statusEmbed] }); + } catch (error) { + console.error('⚠️ Não foi possível enviar mensagem de status:', error.message); + } + + // Executar primeiro scraping imediatamente + await runScrapingCycle(channel); + + // Agendar execuções periódicas + const intervalMs = config.scraping.intervalMinutes * 60 * 1000; + scrapeInterval = setInterval(() => runScrapingCycle(channel), intervalMs); + + console.log(`\n⏰ Próxima verificação em ${config.scraping.intervalMinutes} minutos.\n`); + }); + + // Evento: Erro do cliente + client.on(Events.Error, (error) => { + console.error('❌ Erro no cliente Discord:', error); + }); + + // Evento: Aviso do cliente + client.on(Events.Warn, (warning) => { + console.warn('⚠️ Aviso do Discord:', warning); + }); + + // Conectar ao Discord + console.log('🔌 Conectando ao Discord...'); + await client.login(config.discord.token); +} + +/** + * Executa um ciclo completo de scraping em todas as lojas + * + * @param {TextChannel} channel - Canal do Discord para enviar ofertas + */ +async function runScrapingCycle(channel) { + const startTime = Date.now(); + + console.log(''); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log(`🔄 Iniciando ciclo de scraping - ${new Date().toLocaleString('pt-BR')}`); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + + let totalNewOffers = 0; + const storesWithOffers = []; + + // Executar cada scraper sequencialmente para evitar sobrecarga + for (const scraper of scrapers) { + try { + console.log(`\n📌 Processando: ${scraper.name}`); + + // Executar o scraping + const rawOffers = await scraper.scrape(); + + if (rawOffers.length === 0) { + console.log(` ⚠️ Nenhuma oferta encontrada em ${scraper.name}.`); + continue; + } + + // Filtrar ofertas já enviadas (deduplicação) + const newOffers = filterNewOffers(rawOffers); + + if (newOffers.length === 0) { + console.log(` ℹ️ Todas as ofertas de ${scraper.name} já foram enviadas hoje.`); + continue; + } + + // Enviar cada oferta nova para o Discord + for (const offer of newOffers) { + try { + const embed = buildOfferEmbed(offer); + await channel.send({ embeds: [embed] }); + + // Pequeno delay entre mensagens para evitar rate limiting + await new Promise((resolve) => setTimeout(resolve, 1500)); + } catch (sendError) { + console.error(` ❌ Erro ao enviar oferta "${offer.name}":`, sendError.message); + } + } + + // Marcar ofertas como enviadas + markMultipleAsSent(newOffers); + totalNewOffers += newOffers.length; + storesWithOffers.push(scraper.name); + + console.log(` ✅ ${newOffers.length} ofertas de ${scraper.name} enviadas.`); + + // Delay entre lojas para não sobrecarregar + await new Promise((resolve) => setTimeout(resolve, 3000)); + + } catch (scraperError) { + console.error(` ❌ Erro ao processar ${scraper.name}:`, scraperError.message); + } + } + + // Fechar navegador após ciclo completo para liberar memória + await closeBrowser(); + + const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); + + console.log(''); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log(`✅ Ciclo concluído em ${elapsed}s`); + console.log(` 📊 Total de novas ofertas: ${totalNewOffers}`); + console.log(` 🏪 Lojas com ofertas: ${storesWithOffers.join(', ') || 'Nenhuma'}`); + console.log(` ⏰ Próxima verificação em ${config.scraping.intervalMinutes} minutos`); + console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'); + console.log(''); +} + +/** + * Encerra o bot de forma limpa + */ +async function stopBot() { + console.log('\n🛑 Encerrando bot...'); + + if (scrapeInterval) { + clearInterval(scrapeInterval); + } + + await closeBrowser(); + client.destroy(); + + console.log('👋 Bot encerrado com sucesso.'); + process.exit(0); +} + +module.exports = { startBot, stopBot }; diff --git a/src/config.js b/src/config.js new file mode 100644 index 0000000..12da7e0 --- /dev/null +++ b/src/config.js @@ -0,0 +1,74 @@ +/** + * config.js - Configurações centralizadas do bot + * + * Carrega variáveis de ambiente e define valores padrão + * para todas as configurações necessárias. + */ + +require('dotenv').config(); + +const config = { + // ── Discord ────────────────────────────────────────── + discord: { + token: process.env.DISCORD_TOKEN, + channelId: process.env.DISCORD_CHANNEL_ID, + }, + + // ── URLs de monitoramento ──────────────────────────── + urls: { + amazon: process.env.AMAZON_URL || 'https://www.amazon.com.br/deals', + aliexpress: process.env.ALIEXPRESS_URL || 'https://pt.aliexpress.com/campaign/wow/gcp-plus/ae/right/shareon', + shopee: process.env.SHOPEE_URL || 'https://shopee.com.br/flash_sale', + mercadolivre: process.env.MERCADOLIVRE_URL || 'https://www.mercadolivre.com.br/ofertas', + }, + + // ── Scraping ───────────────────────────────────────── + scraping: { + intervalMinutes: parseInt(process.env.SCRAPE_INTERVAL_MINUTES, 10) || 30, + maxOffersPerSite: parseInt(process.env.MAX_OFFERS_PER_SITE, 10) || 5, + pageTimeout: parseInt(process.env.PAGE_TIMEOUT, 10) || 30000, + headless: process.env.PUPPETEER_HEADLESS !== 'false', // padrão: true + }, + + // ── Afiliados ──────────────────────────────────────── + affiliate: { + amazonTag: process.env.AMAZON_AFFILIATE_TAG || '', + }, + + // ── Cores dos embeds por loja ──────────────────────── + colors: { + amazon: 0xFF9900, // Laranja Amazon + aliexpress: 0xE43A2B, // Vermelho AliExpress + shopee: 0xEE4D2D, // Laranja Shopee + mercadolivre: 0xFFE600, // Amarelo Mercado Livre + }, + + // ── Emojis por loja ───────────────────────────────── + emojis: { + amazon: '🛒', + aliexpress: '🌐', + shopee: '🛍️', + mercadolivre: '🤝', + }, +}; + +// Validação das configurações obrigatórias +function validateConfig() { + const errors = []; + + if (!config.discord.token) { + errors.push('DISCORD_TOKEN não configurado no .env'); + } + if (!config.discord.channelId) { + errors.push('DISCORD_CHANNEL_ID não configurado no .env'); + } + + if (errors.length > 0) { + console.error('❌ Erros de configuração:'); + errors.forEach((err) => console.error(` → ${err}`)); + console.error('\n📄 Copie o arquivo .env.example para .env e configure as variáveis.'); + process.exit(1); + } +} + +module.exports = { config, validateConfig }; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..768fdaa --- /dev/null +++ b/src/index.js @@ -0,0 +1,53 @@ +/** + * index.js - Ponto de entrada do Bot de Ofertas Discord + * + * ┌─────────────────────────────────────────────────────┐ + * │ 🤖 Bot de Ofertas - E-commerce para Discord │ + * │ │ + * │ Monitora: Amazon BR, AliExpress, Shopee, ML │ + * │ Envia: Ofertas formatadas em embeds ricos │ + * │ Intervalo: Configurável (padrão 30min) │ + * └─────────────────────────────────────────────────────┘ + * + * Para iniciar: + * 1. Copie .env.example para .env + * 2. Configure DISCORD_TOKEN e DISCORD_CHANNEL_ID + * 3. Execute: npm start + */ + +const { validateConfig } = require('./config'); +const { startBot, stopBot } = require('./bot'); + +// ── Banner de inicialização ───────────────────────────── +console.log(''); +console.log('╔═══════════════════════════════════════════════╗'); +console.log('║ ║'); +console.log('║ 🛒 Bot de Ofertas - E-commerce Discord ║'); +console.log('║ ║'); +console.log('║ Amazon BR │ AliExpress │ Shopee │ ML ║'); +console.log('║ ║'); +console.log('╚═══════════════════════════════════════════════╝'); +console.log(''); + +// ── Validar configurações ─────────────────────────────── +validateConfig(); + +// ── Tratamento de sinais para encerramento gracioso ───── +process.on('SIGINT', stopBot); +process.on('SIGTERM', stopBot); + +// Tratamento de erros não capturados +process.on('unhandledRejection', (error) => { + console.error('❌ Erro não tratado (Promise):', error); +}); + +process.on('uncaughtException', (error) => { + console.error('❌ Erro não tratado (Exception):', error); + process.exit(1); +}); + +// ── Iniciar o bot ──────────────────────────────────────── +startBot().catch((error) => { + console.error('❌ Falha ao iniciar o bot:', error); + process.exit(1); +}); diff --git a/src/scrapers/aliexpress.js b/src/scrapers/aliexpress.js new file mode 100644 index 0000000..9db9e62 --- /dev/null +++ b/src/scrapers/aliexpress.js @@ -0,0 +1,161 @@ +/** + * aliexpress.js - Scraper para AliExpress + * + * Monitora a página de ofertas/promoções do AliExpress + * e extrai: nome, preço, preço original, desconto, imagem e link. + */ + +const { config } = require('../config'); +const { scrapeWithRetry } = require('../utils/browser'); + +/** + * Executa o scraping da página de ofertas do AliExpress + * @returns {Array} Lista de ofertas extraídas + */ +async function scrapeAliexpress() { + console.log('🌐 [AliExpress] Iniciando scraping...'); + + const offers = await scrapeWithRetry(async (page) => { + // Navegar para a página de ofertas + await page.goto(config.urls.aliexpress, { + waitUntil: 'domcontentloaded', + timeout: config.scraping.pageTimeout, + }); + + // Aguardar carregamento dos cards de produto + await page.waitForSelector('[class*="product-card"], [class*="card-out-wrapper"], .product-snippet_ProductSnippet, [class*="ProductCard"]', { + timeout: 15000, + }).catch(() => { + console.log(' ⚠️ [AliExpress] Seletores primários não encontrados, tentando alternativas...'); + }); + + // Aguardar um pouco para conteúdo dinâmico carregar + await new Promise((resolve) => setTimeout(resolve, 3000)); + + // Scroll para ativar lazy-loading + await autoScroll(page); + + // Extrair dados dos produtos + const products = await page.evaluate((maxOffers) => { + const items = []; + + // Seletores possíveis para cards do AliExpress + const cardSelectors = [ + '[class*="product-card"]', + '[class*="card-out-wrapper"]', + '.product-snippet_ProductSnippet', + '[class*="ProductCard"]', + '[class*="item-card"]', + '.search-item-card-wrapper-gallery', + ]; + + let cards = []; + for (const selector of cardSelectors) { + cards = document.querySelectorAll(selector); + if (cards.length > 0) break; + } + + cards.forEach((card) => { + if (items.length >= maxOffers) return; + + try { + // Nome do produto + const nameEl = card.querySelector( + '[class*="title"], [class*="Title"], h1, h3, [class*="name"]' + ); + const name = nameEl ? nameEl.textContent.trim() : ''; + + // Preço atual + const priceEl = card.querySelector( + '[class*="price-current"], [class*="price-sale"], [class*="Price"], [class*="price"]' + ); + let price = priceEl ? priceEl.textContent.trim() : ''; + // Limpar preço (remover textos extras) + price = price.replace(/[^\dR$.,\s]/g, '').trim(); + + // Preço original + const originalPriceEl = card.querySelector( + '[class*="price-original"], [class*="price-del"], [class*="OriginPrice"], del, s' + ); + const originalPrice = originalPriceEl ? originalPriceEl.textContent.trim() : ''; + + // Desconto + const discountEl = card.querySelector( + '[class*="discount"], [class*="Discount"], [class*="off"]' + ); + const discount = discountEl ? discountEl.textContent.trim() : ''; + + // Imagem + const imgEl = card.querySelector('img[src*="ae"], img[src*="alicdn"], img'); + let image = imgEl ? (imgEl.getAttribute('src') || imgEl.getAttribute('data-src') || '') : ''; + // Garantir protocolo HTTPS + if (image && image.startsWith('//')) { + image = 'https:' + image; + } + + // Link do produto + const linkEl = card.querySelector('a[href*="aliexpress"], a[href*="/item/"], a[href]'); + let link = linkEl ? linkEl.getAttribute('href') : ''; + if (link && link.startsWith('//')) { + link = 'https:' + link; + } + if (link && !link.startsWith('http')) { + link = 'https://pt.aliexpress.com' + link; + } + + // Cupom (AliExpress às vezes mostra cupons nos cards) + const couponEl = card.querySelector( + '[class*="coupon"], [class*="Coupon"], [class*="voucher"]' + ); + const coupon = couponEl ? couponEl.textContent.trim() : null; + + if (name && price && link) { + items.push({ + name, + price, + originalPrice: originalPrice || null, + discount: discount || null, + image: image || null, + link, + coupon, + store: 'aliexpress', + }); + } + } catch (e) { + // Ignorar cards com erro + } + }); + + return items; + }, config.scraping.maxOffersPerSite); + + console.log(` ✅ [AliExpress] ${products.length} ofertas extraídas.`); + return products; + + }, 'AliExpress'); + + return offers; +} + +/** + * Scroll automático para ativar lazy-loading + */ +async function autoScroll(page) { + await page.evaluate(async () => { + await new Promise((resolve) => { + let totalHeight = 0; + const distance = 500; + const timer = setInterval(() => { + window.scrollBy(0, distance); + totalHeight += distance; + if (totalHeight >= 4000) { + clearInterval(timer); + resolve(); + } + }, 250); + }); + }); + await new Promise((resolve) => setTimeout(resolve, 2000)); +} + +module.exports = { scrapeAliexpress }; diff --git a/src/scrapers/amazon.js b/src/scrapers/amazon.js new file mode 100644 index 0000000..a2ddf35 --- /dev/null +++ b/src/scrapers/amazon.js @@ -0,0 +1,158 @@ +/** + * amazon.js - Scraper para Amazon Brasil + * + * Monitora a página de Ofertas do Dia da Amazon Brasil + * e extrai: nome, preço, preço original, desconto, imagem e link. + */ + +const { config } = require('../config'); +const { scrapeWithRetry } = require('../utils/browser'); + +/** + * Executa o scraping da página de ofertas da Amazon Brasil + * @returns {Array} Lista de ofertas extraídas + */ +async function scrapeAmazon() { + console.log('🛒 [Amazon] Iniciando scraping...'); + + const offers = await scrapeWithRetry(async (page) => { + // Navegar para a página de ofertas + await page.goto(config.urls.amazon, { + waitUntil: 'domcontentloaded', + timeout: config.scraping.pageTimeout, + }); + + // Aguardar carregamento dos cards de produto + await page.waitForSelector('[data-testid="deal-card"], .DealCard-module__dealCard_3BGIW, .a-section.dealCard', { + timeout: 10000, + }).catch(() => { + console.log(' ⚠️ [Amazon] Seletores de deal-card não encontrados, tentando alternativas...'); + }); + + // Scroll para carregar mais produtos (lazy loading) + await autoScroll(page); + + // Extrair dados dos produtos + const products = await page.evaluate((maxOffers, affiliateTag) => { + const items = []; + + // Tentar múltiplos seletores (Amazon muda frequentemente o layout) + const cardSelectors = [ + '[data-testid="deal-card"]', + '.DealCard-module__dealCard_3BGIW', + '.dealCard', + '.octopus-dlp-asin-section', + '[class*="DealCard"]', + ]; + + let cards = []; + for (const selector of cardSelectors) { + cards = document.querySelectorAll(selector); + if (cards.length > 0) break; + } + + // Se não encontrou cards específicos, tenta grid genérico + if (cards.length === 0) { + cards = document.querySelectorAll('.a-section .a-link-normal[href*="/dp/"]'); + } + + cards.forEach((card) => { + if (items.length >= maxOffers) return; + + try { + // Nome do produto + const nameEl = card.querySelector( + '[class*="Title"], [class*="title"], .a-text-normal, .a-size-base' + ); + const name = nameEl ? nameEl.textContent.trim() : ''; + + // Preço atual + const priceEl = card.querySelector( + '[class*="Price"], .a-price .a-offscreen, .a-price-whole, [class*="price"]' + ); + let price = priceEl ? priceEl.textContent.trim() : ''; + + // Preço original + const originalPriceEl = card.querySelector( + '.a-text-price .a-offscreen, [class*="ListPrice"], [class*="originalPrice"]' + ); + const originalPrice = originalPriceEl ? originalPriceEl.textContent.trim() : ''; + + // Desconto + const discountEl = card.querySelector( + '[class*="Discount"], [class*="discount"], .savingsPercentage' + ); + const discount = discountEl ? discountEl.textContent.trim() : ''; + + // Imagem + const imgEl = card.querySelector('img[src*="images-amazon"], img[src*="m.media-amazon"]'); + let image = imgEl ? (imgEl.getAttribute('src') || '') : ''; + // Tentar pegar imagem de maior qualidade + if (image && image.includes('._')) { + image = image.replace(/\._.*_\./, '._SL500_.'); + } + + // Link do produto + const linkEl = card.querySelector('a[href*="/dp/"], a[href*="/deal/"], a[href]'); + let link = linkEl ? linkEl.getAttribute('href') : ''; + if (link && !link.startsWith('http')) { + link = 'https://www.amazon.com.br' + link; + } + + // Adicionar tag de afiliado se configurada + if (link && affiliateTag) { + const separator = link.includes('?') ? '&' : '?'; + link += `${separator}tag=${affiliateTag}`; + } + + if (name && price && link) { + items.push({ + name, + price, + originalPrice: originalPrice || null, + discount: discount || null, + image: image || null, + link, + coupon: null, // Amazon geralmente não tem cupons na página de ofertas + store: 'amazon', + }); + } + } catch (e) { + // Ignorar cards com erro de extração + } + }); + + return items; + }, config.scraping.maxOffersPerSite, config.affiliate.amazonTag); + + console.log(` ✅ [Amazon] ${products.length} ofertas extraídas.`); + return products; + + }, 'Amazon'); + + return offers; +} + +/** + * Scroll automático para ativar lazy-loading + */ +async function autoScroll(page) { + await page.evaluate(async () => { + await new Promise((resolve) => { + let totalHeight = 0; + const distance = 400; + const timer = setInterval(() => { + window.scrollBy(0, distance); + totalHeight += distance; + if (totalHeight >= 3000) { // Limitar a ~3000px de scroll + clearInterval(timer); + resolve(); + } + }, 200); + }); + }); + // Aguardar conteúdo carregar após scroll + await new Promise((resolve) => setTimeout(resolve, 2000)); +} + +module.exports = { scrapeAmazon }; diff --git a/src/scrapers/index.js b/src/scrapers/index.js new file mode 100644 index 0000000..d833d23 --- /dev/null +++ b/src/scrapers/index.js @@ -0,0 +1,38 @@ +/** + * index.js - Agregador de scrapers + * + * Exporta todos os scrapers disponíveis em um único módulo + * para facilitar importação e manutenção. + */ + +const { scrapeAmazon } = require('./amazon'); +const { scrapeAliexpress } = require('./aliexpress'); +const { scrapeShopee } = require('./shopee'); +const { scrapeMercadoLivre } = require('./mercadolivre'); + +// Mapa de scrapers disponíveis +// Cada entrada tem: nome legível, função de scraping e identificador da loja +const scrapers = [ + { + name: 'Amazon Brasil', + store: 'amazon', + scrape: scrapeAmazon, + }, + { + name: 'AliExpress', + store: 'aliexpress', + scrape: scrapeAliexpress, + }, + { + name: 'Shopee', + store: 'shopee', + scrape: scrapeShopee, + }, + { + name: 'Mercado Livre', + store: 'mercadolivre', + scrape: scrapeMercadoLivre, + }, +]; + +module.exports = { scrapers }; diff --git a/src/scrapers/mercadolivre.js b/src/scrapers/mercadolivre.js new file mode 100644 index 0000000..6d43cb1 --- /dev/null +++ b/src/scrapers/mercadolivre.js @@ -0,0 +1,205 @@ +/** + * mercadolivre.js - Scraper para Mercado Livre + * + * Monitora a página de Ofertas do Dia do Mercado Livre + * e extrai: nome, preço, preço original, desconto, imagem e link. + */ + +const { config } = require('../config'); +const { scrapeWithRetry } = require('../utils/browser'); + +/** + * Executa o scraping da página de ofertas do Mercado Livre + * @returns {Array} Lista de ofertas extraídas + */ +async function scrapeMercadoLivre() { + console.log('🤝 [Mercado Livre] Iniciando scraping...'); + + const offers = await scrapeWithRetry(async (page) => { + // Navegar para a página de ofertas + await page.goto(config.urls.mercadolivre, { + waitUntil: 'domcontentloaded', + timeout: config.scraping.pageTimeout, + }); + + // Aguardar carregamento dos cards de produto + await page.waitForSelector('.promotion-item, .poly-card, .andes-card, [class*="poly-card"], .deal-card', { + timeout: 10000, + }).catch(() => { + console.log(' ⚠️ [Mercado Livre] Seletores primários não encontrados, tentando alternativas...'); + }); + + // Scroll para carregar mais itens + await autoScroll(page); + + // Extrair dados dos produtos + const products = await page.evaluate((maxOffers) => { + const items = []; + + // Seletores para cards do Mercado Livre + const cardSelectors = [ + '.poly-card', + '.promotion-item', + '.andes-card', + '[class*="poly-card"]', + '.deal-card', + '.ui-search-layout__item', + ]; + + let cards = []; + for (const selector of cardSelectors) { + cards = document.querySelectorAll(selector); + if (cards.length > 0) break; + } + + cards.forEach((card) => { + if (items.length >= maxOffers) return; + + try { + // Nome do produto + const nameEl = card.querySelector( + '.poly-component__title, .promotion-item__title, .ui-search-item__title, [class*="title"], h2, h3' + ); + const name = nameEl ? nameEl.textContent.trim() : ''; + + // Preço atual + const priceEl = card.querySelector( + '.poly-price__current .andes-money-amount, .andes-money-amount--cents-superscript, [class*="price"], .price-tag-fraction' + ); + let price = ''; + if (priceEl) { + // Mercado Livre separa reais e centavos em elementos diferentes + const fractionEl = priceEl.querySelector('.andes-money-amount__fraction, .price-tag-fraction'); + const centsEl = priceEl.querySelector('.andes-money-amount__cents, .price-tag-cents'); + const currencyEl = priceEl.querySelector('.andes-money-amount__currency-symbol'); + + if (fractionEl) { + const currency = currencyEl ? currencyEl.textContent.trim() : 'R$'; + const fraction = fractionEl.textContent.trim(); + const cents = centsEl ? centsEl.textContent.trim() : '00'; + price = `${currency} ${fraction},${cents}`; + } else { + price = priceEl.textContent.trim(); + } + } + + // Preço original + const originalPriceEl = card.querySelector( + '.andes-money-amount--previous, [class*="original-price"], s .andes-money-amount, .price-tag-deleted' + ); + let originalPrice = ''; + if (originalPriceEl) { + const origFraction = originalPriceEl.querySelector('.andes-money-amount__fraction, .price-tag-fraction'); + if (origFraction) { + originalPrice = `R$ ${origFraction.textContent.trim()}`; + } else { + originalPrice = originalPriceEl.textContent.trim(); + } + } + + // Desconto + const discountEl = card.querySelector( + '.poly-component__discount, [class*="discount"], .ui-search-price__second-line__label' + ); + const discount = discountEl ? discountEl.textContent.trim() : ''; + + // Imagem + const imgEl = card.querySelector( + 'img[src*="http2.mlstatic"], img[data-src*="http2.mlstatic"], img[src*="meli"], img' + ); + let image = ''; + if (imgEl) { + image = imgEl.getAttribute('src') || imgEl.getAttribute('data-src') || ''; + // Garantir URL de imagem válida + if (image && image.startsWith('data:')) { + image = imgEl.getAttribute('data-src') || ''; + } + } + + // Link do produto + const linkEl = card.querySelector( + 'a[href*="mercadolivre.com.br"], a[href*="produto.mercadolivre"], a[href*="/MLB-"], a[href]' + ); + let link = linkEl ? linkEl.getAttribute('href') : ''; + if (link && !link.startsWith('http')) { + link = 'https://www.mercadolivre.com.br' + link; + } + + // Cupom (ML ocasionalmente mostra cupons) + const couponEl = card.querySelector( + '[class*="coupon"], [class*="cupom"], [class*="highlight"]' + ); + let coupon = null; + if (couponEl) { + const couponText = couponEl.textContent.trim(); + if (couponText.toLowerCase().includes('cupom') || couponText.toLowerCase().includes('off')) { + coupon = couponText; + } + } + + // Frete grátis (informação extra útil) + const freeShippingEl = card.querySelector( + '[class*="free-shipping"], .poly-component__shipping, [class*="shipping"]' + ); + const hasFreeShipping = freeShippingEl && + freeShippingEl.textContent.toLowerCase().includes('grátis'); + + if (name && price && link) { + const offer = { + name, + price, + originalPrice: originalPrice || null, + discount: discount || null, + image: image || null, + link, + coupon, + store: 'mercadolivre', + }; + + // Adicionar info de frete grátis ao cupom/observação + if (hasFreeShipping && !coupon) { + offer.coupon = '🚚 Frete Grátis'; + } else if (hasFreeShipping && coupon) { + offer.coupon += ' | 🚚 Frete Grátis'; + } + + items.push(offer); + } + } catch (e) { + // Ignorar cards com erro + } + }); + + return items; + }, config.scraping.maxOffersPerSite); + + console.log(` ✅ [Mercado Livre] ${products.length} ofertas extraídas.`); + return products; + + }, 'Mercado Livre'); + + return offers; +} + +/** + * Scroll automático + */ +async function autoScroll(page) { + await page.evaluate(async () => { + await new Promise((resolve) => { + let totalHeight = 0; + const distance = 400; + const timer = setInterval(() => { + window.scrollBy(0, distance); + totalHeight += distance; + if (totalHeight >= 3000) { + clearInterval(timer); + resolve(); + } + }, 200); + }); + }); + await new Promise((resolve) => setTimeout(resolve, 2000)); +} + +module.exports = { scrapeMercadoLivre }; diff --git a/src/scrapers/shopee.js b/src/scrapers/shopee.js new file mode 100644 index 0000000..6567fa8 --- /dev/null +++ b/src/scrapers/shopee.js @@ -0,0 +1,199 @@ +/** + * shopee.js - Scraper para Shopee Brasil + * + * Monitora a página de Flash Sale / Ofertas Relâmpago da Shopee + * e extrai: nome, preço, preço original, desconto, imagem e link. + * + * Nota: A Shopee é bastante dinâmica e carrega conteúdo via API. + * Este scraper usa Puppeteer para renderizar a página completa. + */ + +const { config } = require('../config'); +const { scrapeWithRetry } = require('../utils/browser'); + +/** + * Executa o scraping da página de ofertas da Shopee + * @returns {Array} Lista de ofertas extraídas + */ +async function scrapeShopee() { + console.log('🛍️ [Shopee] Iniciando scraping...'); + + const offers = await scrapeWithRetry(async (page) => { + // Navegar para a página de flash sale + await page.goto(config.urls.shopee, { + waitUntil: 'networkidle2', + timeout: config.scraping.pageTimeout, + }); + + // A Shopee pode mostrar popups - tentar fechar + await closePopups(page); + + // Aguardar carregamento dos cards + await page.waitForSelector('[class*="flash-sale"], [class*="shopee-search-item"], .shopee-card-atelier-overlay, [data-sqe="item"]', { + timeout: 15000, + }).catch(() => { + console.log(' ⚠️ [Shopee] Seletores primários não encontrados, tentando alternativas...'); + }); + + // Aguardar conteúdo dinâmico + await new Promise((resolve) => setTimeout(resolve, 3000)); + + // Scroll para carregar mais itens + await autoScroll(page); + + // Extrair dados dos produtos + const products = await page.evaluate((maxOffers) => { + const items = []; + + // Seletores para cards da Shopee + const cardSelectors = [ + '[data-sqe="item"]', + '[class*="flash-sale-item"]', + '.shopee-search-item-result__item', + '[class*="product-card"]', + '[class*="item-card"]', + '.shop-search-result-view__item', + ]; + + let cards = []; + for (const selector of cardSelectors) { + cards = document.querySelectorAll(selector); + if (cards.length > 0) break; + } + + cards.forEach((card) => { + if (items.length >= maxOffers) return; + + try { + // Nome do produto + const nameEl = card.querySelector( + '[class*="name"], [class*="title"], [data-sqe="name"]' + ); + const name = nameEl ? nameEl.textContent.trim() : ''; + + // Preço atual + const priceEl = card.querySelector( + '[class*="price"], [class*="Price"]' + ); + let price = ''; + if (priceEl) { + // Shopee formata preços de formas variadas + price = priceEl.textContent.trim(); + // Remover textos como "R$" duplicados ou ranges de preço + if (price.includes(' - ')) { + price = price.split(' - ')[0]; // Pegar menor preço + } + } + + // Preço original + const originalPriceEl = card.querySelector( + '[class*="original"], del, s, [class*="before"]' + ); + const originalPrice = originalPriceEl ? originalPriceEl.textContent.trim() : ''; + + // Desconto + const discountEl = card.querySelector( + '[class*="discount"], [class*="percent"]' + ); + const discount = discountEl ? discountEl.textContent.trim() : ''; + + // Imagem + const imgEl = card.querySelector('img'); + let image = imgEl ? (imgEl.getAttribute('src') || imgEl.getAttribute('data-src') || '') : ''; + if (image && image.startsWith('//')) { + image = 'https:' + image; + } + // Tentar pegar imagem de melhor qualidade + if (image) { + image = image.replace(/_tn\b/, ''); + } + + // Link do produto + const linkEl = card.querySelector('a[href]'); + let link = linkEl ? linkEl.getAttribute('href') : ''; + if (link && !link.startsWith('http')) { + link = 'https://shopee.com.br' + link; + } + + // Cupom (Shopee mostra vouchers em alguns produtos) + const couponEl = card.querySelector( + '[class*="voucher"], [class*="coupon"]' + ); + const coupon = couponEl ? couponEl.textContent.trim() : null; + + if (name && price && link) { + items.push({ + name, + price, + originalPrice: originalPrice || null, + discount: discount || null, + image: image || null, + link, + coupon, + store: 'shopee', + }); + } + } catch (e) { + // Ignorar cards com erro + } + }); + + return items; + }, config.scraping.maxOffersPerSite); + + console.log(` ✅ [Shopee] ${products.length} ofertas extraídas.`); + return products; + + }, 'Shopee'); + + return offers; +} + +/** + * Tenta fechar popups que a Shopee frequentemente exibe + */ +async function closePopups(page) { + try { + // Fechar popup de login/cadastro + const closeButtonSelectors = [ + '[class*="close-btn"]', + '[class*="closeBtn"]', + '.shopee-popup__close-btn', + '[aria-label="Close"]', + 'button[class*="close"]', + ]; + + for (const selector of closeButtonSelectors) { + const btn = await page.$(selector); + if (btn) { + await btn.click(); + await new Promise((resolve) => setTimeout(resolve, 500)); + } + } + } catch { + // Ignorar erros ao fechar popups + } +} + +/** + * Scroll automático + */ +async function autoScroll(page) { + await page.evaluate(async () => { + await new Promise((resolve) => { + let totalHeight = 0; + const distance = 400; + const timer = setInterval(() => { + window.scrollBy(0, distance); + totalHeight += distance; + if (totalHeight >= 3000) { + clearInterval(timer); + resolve(); + } + }, 200); + }); + }); + await new Promise((resolve) => setTimeout(resolve, 2000)); +} + +module.exports = { scrapeShopee }; diff --git a/src/utils/browser.js b/src/utils/browser.js new file mode 100644 index 0000000..06e0633 --- /dev/null +++ b/src/utils/browser.js @@ -0,0 +1,135 @@ +/** + * browser.js - Gerenciador do navegador Puppeteer + * + * Centraliza a criação e gerenciamento da instância do Puppeteer + * com configurações otimizadas para scraping. + */ + +const puppeteer = require('puppeteer'); +const { config } = require('../config'); + +let browserInstance = null; + +/** + * Obtém ou cria uma instância do navegador Puppeteer + * Reutiliza a mesma instância para economizar recursos + */ +async function getBrowser() { + if (browserInstance && browserInstance.connected) { + return browserInstance; + } + + console.log('🌐 Iniciando navegador Puppeteer...'); + + browserInstance = await puppeteer.launch({ + headless: config.scraping.headless ? 'new' : false, + args: [ + '--no-sandbox', + '--disable-setuid-sandbox', + '--disable-dev-shm-usage', + '--disable-accelerated-2d-canvas', + '--disable-gpu', + '--window-size=1920,1080', + '--lang=pt-BR', + ], + defaultViewport: { + width: 1920, + height: 1080, + }, + }); + + console.log('✅ Navegador iniciado com sucesso.'); + return browserInstance; +} + +/** + * Cria uma nova página com configurações otimizadas + * - User-Agent realista para evitar bloqueios + * - Bloqueio de recursos desnecessários (fontes, mídia) + * - Timeout configurável + */ +async function createPage() { + const browser = await getBrowser(); + const page = await browser.newPage(); + + // User-Agent realista (Chrome no Windows) + await page.setUserAgent( + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' + ); + + // Configurar idioma para português + await page.setExtraHTTPHeaders({ + 'Accept-Language': 'pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7', + }); + + // Timeout padrão para navegação + page.setDefaultNavigationTimeout(config.scraping.pageTimeout); + page.setDefaultTimeout(config.scraping.pageTimeout); + + // Bloquear recursos desnecessários para acelerar o scraping + await page.setRequestInterception(true); + page.on('request', (req) => { + const blockedTypes = ['font', 'media']; + if (blockedTypes.includes(req.resourceType())) { + req.abort(); + } else { + req.continue(); + } + }); + + return page; +} + +/** + * Fecha o navegador e libera recursos + */ +async function closeBrowser() { + if (browserInstance) { + await browserInstance.close(); + browserInstance = null; + console.log('🔒 Navegador fechado.'); + } +} + +/** + * Executa scraping com retry automático + * + * @param {Function} scraperFn - Função de scraping que recebe (page) + * @param {string} storeName - Nome da loja para logs + * @param {number} maxRetries - Número máximo de tentativas + * @returns {Array} Lista de ofertas encontradas + */ +async function scrapeWithRetry(scraperFn, storeName, maxRetries = 2) { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + let page = null; + try { + page = await createPage(); + const results = await scraperFn(page); + return results; + } catch (error) { + console.error(` ⚠️ [${storeName}] Tentativa ${attempt}/${maxRetries} falhou: ${error.message}`); + if (attempt === maxRetries) { + console.error(` ❌ [${storeName}] Todas as tentativas falharam.`); + return []; + } + // Aguardar antes de tentar novamente + await new Promise((resolve) => setTimeout(resolve, 3000)); + } finally { + if (page) { + try { + await page.close(); + } catch { + // Página já pode ter sido fechada + } + } + } + } + return []; +} + +module.exports = { + getBrowser, + createPage, + closeBrowser, + scrapeWithRetry, +}; diff --git a/src/utils/dedup.js b/src/utils/dedup.js new file mode 100644 index 0000000..2c91c09 --- /dev/null +++ b/src/utils/dedup.js @@ -0,0 +1,133 @@ +/** + * dedup.js - Sistema de deduplicação de ofertas + * + * Evita que a mesma oferta seja enviada mais de uma vez no mesmo dia. + * Armazena um registro em disco (JSON) com hash das ofertas enviadas. + */ + +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); + +// Diretório para armazenar dados de deduplicação +const DATA_DIR = path.join(__dirname, '..', '..', 'data'); +const DEDUP_FILE = path.join(DATA_DIR, 'sent_offers.json'); + +/** + * Gera um hash único para identificar uma oferta + * Combina nome do produto + loja para criar uma chave única + */ +function generateOfferHash(offer) { + const key = `${offer.store}:${offer.name}:${offer.price}`; + return crypto.createHash('md5').update(key).digest('hex'); +} + +/** + * Carrega o registro de ofertas já enviadas + * Limpa automaticamente ofertas de dias anteriores + */ +function loadSentOffers() { + try { + // Garante que o diretório existe + if (!fs.existsSync(DATA_DIR)) { + fs.mkdirSync(DATA_DIR, { recursive: true }); + } + + if (!fs.existsSync(DEDUP_FILE)) { + return { date: getTodayDate(), offers: [] }; + } + + const data = JSON.parse(fs.readFileSync(DEDUP_FILE, 'utf-8')); + + // Se o registro é de um dia diferente, limpa tudo (reset diário) + if (data.date !== getTodayDate()) { + console.log('📅 Novo dia detectado — limpando registro de ofertas anteriores.'); + return { date: getTodayDate(), offers: [] }; + } + + return data; + } catch (error) { + console.error('⚠️ Erro ao carregar registro de dedup:', error.message); + return { date: getTodayDate(), offers: [] }; + } +} + +/** + * Salva o registro de ofertas enviadas no disco + */ +function saveSentOffers(sentOffers) { + try { + if (!fs.existsSync(DATA_DIR)) { + fs.mkdirSync(DATA_DIR, { recursive: true }); + } + fs.writeFileSync(DEDUP_FILE, JSON.stringify(sentOffers, null, 2), 'utf-8'); + } catch (error) { + console.error('⚠️ Erro ao salvar registro de dedup:', error.message); + } +} + +/** + * Verifica se uma oferta já foi enviada hoje + */ +function isOfferAlreadySent(offer) { + const sentOffers = loadSentOffers(); + const hash = generateOfferHash(offer); + return sentOffers.offers.includes(hash); +} + +/** + * Marca uma oferta como enviada + */ +function markOfferAsSent(offer) { + const sentOffers = loadSentOffers(); + const hash = generateOfferHash(offer); + + if (!sentOffers.offers.includes(hash)) { + sentOffers.offers.push(hash); + saveSentOffers(sentOffers); + } +} + +/** + * Filtra uma lista de ofertas, removendo as que já foram enviadas + */ +function filterNewOffers(offers) { + const sentOffers = loadSentOffers(); + const newOffers = offers.filter((offer) => { + const hash = generateOfferHash(offer); + return !sentOffers.offers.includes(hash); + }); + + console.log(` 🔍 ${offers.length} ofertas encontradas → ${newOffers.length} novas`); + return newOffers; +} + +/** + * Marca múltiplas ofertas como enviadas de uma vez (mais eficiente) + */ +function markMultipleAsSent(offers) { + const sentOffers = loadSentOffers(); + + offers.forEach((offer) => { + const hash = generateOfferHash(offer); + if (!sentOffers.offers.includes(hash)) { + sentOffers.offers.push(hash); + } + }); + + saveSentOffers(sentOffers); +} + +/** + * Retorna a data de hoje no formato YYYY-MM-DD + */ +function getTodayDate() { + return new Date().toISOString().split('T')[0]; +} + +module.exports = { + isOfferAlreadySent, + markOfferAsSent, + filterNewOffers, + markMultipleAsSent, +}; diff --git a/src/utils/embed.js b/src/utils/embed.js new file mode 100644 index 0000000..4f357f4 --- /dev/null +++ b/src/utils/embed.js @@ -0,0 +1,160 @@ +/** + * embed.js - Construtor de embeds do Discord para ofertas + * + * Formata as ofertas extraídas em embeds ricos e visualmente + * atraentes para envio nos canais do Discord. + */ + +const { EmbedBuilder } = require('discord.js'); +const { config } = require('../config'); + +/** + * Cria um embed do Discord para uma oferta individual + * + * @param {Object} offer - Dados da oferta + * @param {string} offer.name - Nome do produto + * @param {string} offer.price - Preço atual (já formatado) + * @param {string} [offer.originalPrice] - Preço original (se houver desconto) + * @param {string} [offer.discount] - Percentual de desconto + * @param {string} offer.link - URL do produto + * @param {string} [offer.image] - URL da imagem do produto + * @param {string} [offer.coupon] - Código do cupom (se disponível) + * @param {string} offer.store - Identificador da loja (amazon, aliexpress, etc.) + * @returns {EmbedBuilder} Embed formatado para envio + */ +function buildOfferEmbed(offer) { + const color = config.colors[offer.store] || 0x5865F2; // Cor padrão Discord + const emoji = config.emojis[offer.store] || '🏷️'; + const storeName = getStoreName(offer.store); + + const embed = new EmbedBuilder() + .setColor(color) + .setTitle(`${emoji} ${truncate(offer.name, 200)}`) + .setURL(offer.link) + .setTimestamp() + .setFooter({ + text: `${storeName} • Encontrado pelo Bot de Ofertas`, + }); + + // ── Imagem do produto ── + if (offer.image && isValidUrl(offer.image)) { + embed.setImage(offer.image); + } + + // ── Campos de preço ── + const priceFields = []; + + // Preço atual em destaque + priceFields.push({ + name: '💰 Preço Atual', + value: `**${offer.price}**`, + inline: true, + }); + + // Preço original (riscado) se houver desconto + if (offer.originalPrice) { + priceFields.push({ + name: '💸 Preço Original', + value: `~~${offer.originalPrice}~~`, + inline: true, + }); + } + + // Percentual de desconto + if (offer.discount) { + priceFields.push({ + name: '🔥 Desconto', + value: `**${offer.discount}**`, + inline: true, + }); + } + + embed.addFields(priceFields); + + // ── Cupom de desconto ── + if (offer.coupon) { + embed.addFields({ + name: '🎟️ Cupom de Desconto', + value: `\`${offer.coupon}\``, + inline: false, + }); + } + + // ── Link direto ── + embed.addFields({ + name: '🔗 Comprar Agora', + value: `[Clique aqui para ver a oferta](${offer.link})`, + inline: false, + }); + + return embed; +} + +/** + * Cria um embed de resumo/cabeçalho para uma rodada de scraping + */ +function buildSummaryEmbed(totalOffers, stores) { + const now = new Date().toLocaleString('pt-BR', { timeZone: 'America/Sao_Paulo' }); + + return new EmbedBuilder() + .setColor(0x5865F2) + .setTitle('🔔 Novas Ofertas Encontradas!') + .setDescription( + `Foram encontradas **${totalOffers}** novas ofertas em **${stores.length}** loja(s).\n` + + `📅 Verificação: ${now}` + ) + .setFooter({ text: 'Bot de Ofertas • Monitoramento Automático' }); +} + +/** + * Cria um embed de status/erro + */ +function buildStatusEmbed(message, isError = false) { + return new EmbedBuilder() + .setColor(isError ? 0xFF0000 : 0x00FF00) + .setTitle(isError ? '❌ Erro no Monitoramento' : '✅ Status do Bot') + .setDescription(message) + .setTimestamp(); +} + +// ── Funções auxiliares ────────────────────────────────── + +/** + * Retorna o nome legível da loja + */ +function getStoreName(storeKey) { + const names = { + amazon: 'Amazon Brasil', + aliexpress: 'AliExpress', + shopee: 'Shopee', + mercadolivre: 'Mercado Livre', + }; + return names[storeKey] || storeKey; +} + +/** + * Trunca texto longo para caber nos limites do Discord + */ +function truncate(text, maxLength) { + if (!text) return ''; + if (text.length <= maxLength) return text; + return text.substring(0, maxLength - 3) + '...'; +} + +/** + * Valida se uma string é uma URL válida + */ +function isValidUrl(string) { + try { + new URL(string); + return true; + } catch { + return false; + } +} + +module.exports = { + buildOfferEmbed, + buildSummaryEmbed, + buildStatusEmbed, +};