From e60c5819e23fbaf9f93af148baf7fdc87dd14d1e Mon Sep 17 00:00:00 2001 From: synt-xerror <169557594+synt-xerror@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:32:57 -0300 Subject: [PATCH] [repo] reorganization --- package-lock.json | 4 +- src/client/banner.js | 48 +++++ src/client/environment.js | 25 +++ src/client/qrHandler.js | 25 +++ src/client/whatsappClient.js | 99 ++------- src/commands/figurinha.js | 272 ------------------------ src/commands/handlers/a.js | 5 + src/commands/handlers/adivinhacao.js | 44 ++++ src/commands/handlers/audio.js | 15 ++ src/commands/handlers/figurinha.js | 26 +++ src/commands/handlers/info.js | 24 +++ src/commands/handlers/many.js | 11 + src/commands/handlers/obrigado.js | 5 + src/commands/handlers/video.js | 15 ++ src/commands/index.js | 145 ++----------- src/commands/info.js | 20 -- src/commands/logic/figurinha.js | 192 +++++++++++++++++ src/commands/logic/games/adivinhacao.js | 61 ++++++ src/commands/logic/stickerSessions.js | 7 + src/commands/parser.js | 24 +++ src/commands/registry.js | 25 +++ src/download/audio.js | 34 --- src/download/downloader.js | 112 ++++++++++ src/download/mediaType.js | 15 ++ src/download/queue.js | 103 +++++---- src/download/video.js | 59 ----- src/games/adivinhacao.js | 35 --- src/handlers/messageHandler.js | 29 +++ src/logger/formatter.js | 36 ++++ src/logger/logger.js | 53 +++++ src/logger/messageContext.js | 83 ++++++++ src/main.js | 125 +---------- src/utils/get_id.js | 68 +++--- todo.txt | 8 +- 34 files changed, 1023 insertions(+), 829 deletions(-) create mode 100644 src/client/banner.js create mode 100644 src/client/environment.js create mode 100644 src/client/qrHandler.js delete mode 100644 src/commands/figurinha.js create mode 100644 src/commands/handlers/a.js create mode 100644 src/commands/handlers/adivinhacao.js create mode 100644 src/commands/handlers/audio.js create mode 100644 src/commands/handlers/figurinha.js create mode 100644 src/commands/handlers/info.js create mode 100644 src/commands/handlers/many.js create mode 100644 src/commands/handlers/obrigado.js create mode 100644 src/commands/handlers/video.js delete mode 100644 src/commands/info.js create mode 100644 src/commands/logic/figurinha.js create mode 100644 src/commands/logic/games/adivinhacao.js create mode 100644 src/commands/logic/stickerSessions.js create mode 100644 src/commands/parser.js create mode 100644 src/commands/registry.js delete mode 100644 src/download/audio.js create mode 100644 src/download/downloader.js create mode 100644 src/download/mediaType.js delete mode 100644 src/download/video.js delete mode 100644 src/games/adivinhacao.js create mode 100644 src/handlers/messageHandler.js create mode 100644 src/logger/formatter.js create mode 100644 src/logger/logger.js create mode 100644 src/logger/messageContext.js diff --git a/package-lock.json b/package-lock.json index 874d2ca..ec19f36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "whatsapp-bot", - "version": "2.3.0", + "version": "2.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "whatsapp-bot", - "version": "2.3.0", + "version": "2.3.1", "dependencies": { "node-addon-api": "^7", "node-gyp": "^12.2.0", diff --git a/src/client/banner.js b/src/client/banner.js new file mode 100644 index 0000000..5815605 --- /dev/null +++ b/src/client/banner.js @@ -0,0 +1,48 @@ +import { c } from "../logger/formatter.js"; + +export function printBanner() { + + const banner = [ +` _____ _____ _ `, +` | |___ ___ _ _| __ |___| |_ `, +` | | | | . | | | | __ -| . | _|`, +` |_|_|_|__,|_|_|_ |_____|___|_| `, +` |___| ` + ]; + + const pony = [ +` ⠴⢮⠭⠍⠉⠉⠒⠤⣀`, +` ⢀⢊ ⢱⠊⠑⡀`, +` ⠋⡎ ⣀⡠⠤⠠⠖⠋⢉⠉ ⡄⢸`, +` ⣘⡠⠊⣩⡅ ⣴⡟⣯⠙⣊ ⢁⠜`, +` ⣿⡇⢸⣿⣷⡿⢀⠇⢀⢎`, +` ⠰⡉ ⠈⠛⠛⠋⠁⢀⠜ ⢂`, +` ⠈⠒⠒⡲⠂⣠⣔⠁ ⡇ ⢀⡴⣾⣛⡛⠻⣦`, +` ⢠⠃ ⢠⠞ ⡸⠉⠲⣿⠿⢿⣿⣿⣷⡌⢷`, +` ⢀⠔⠂⢼ ⡎⡔⡄⠰⠃ ⢣ ⢻⣿⣿⣿⠘⣷`, +` ⡐⠁ ⠸⡀ ⠏ ⠈⠃ ⢸ ⣿⣿⣿⡇⣿⡇`, +` ⡇ ⡎⠉⠉⢳ ⡤⠤⡤⠲⡀ ⢇ ⣿⣿⣿⣇⣿⣷`, +` ⡇ ⡠⠃ ⡸ ⡇ ⡇ ⢱⡀ ⢣ ⣿⣿⣿⣿⣿⡄`, +` ⠑⠊ ⢰ ⠇ ⢸ ⡇⡇ ⡇ ⢳⣿⣿⣿⣿⡇`, +` ⢠⠃ ⡸ ⡎ ⡜⡇ ⡇ ⠻⡏⠻⣿⣿⣄`, +` ⣔⣁⣀⣀⡠⠁ ⠈⠉⠉⠁⣎⣀⣀⣀⡸` + ]; + + + + console.log(`${c.blue}${c.bold}`); + + const max = Math.max(banner.length, pony.length); + + for (let i = 0; i < max; i++) { + const left = banner[i] || " ".repeat(banner[0].length); + const right = pony[i] || ""; + console.log(left + " " + right); + } + + console.log(); + console.log(` website : ${c.reset}${c.cyan}www.mlplovers.com.br/manybot${c.reset}`); + console.log(` repo : ${c.reset}${c.cyan}github.com/synt-xerror/manybot${c.reset}`); + console.log(); +} + diff --git a/src/client/environment.js b/src/client/environment.js new file mode 100644 index 0000000..2def205 --- /dev/null +++ b/src/client/environment.js @@ -0,0 +1,25 @@ +import os from "os"; + +/** + * Detecta se o processo está rodando dentro do Termux. + */ +export const isTermux = + (os.platform() === "linux" || os.platform() === "android") && + process.env.PREFIX?.startsWith("/data/data/com.termux"); + +/** + * Retorna a config de Puppeteer adequada ao ambiente. + * @returns {import("puppeteer").LaunchOptions} + */ +export function resolvePuppeteerConfig() { + if (!isTermux) return {}; + + return { + executablePath: "/data/data/com.termux/files/usr/bin/chromium-browser", + args: [ + "--headless=new", "--no-sandbox", "--disable-setuid-sandbox", + "--disable-dev-shm-usage", "--disable-gpu", "--single-process", + "--no-zygote", "--disable-software-rasterizer", + ], + }; +} \ No newline at end of file diff --git a/src/client/qrHandler.js b/src/client/qrHandler.js new file mode 100644 index 0000000..8abb3a1 --- /dev/null +++ b/src/client/qrHandler.js @@ -0,0 +1,25 @@ +import qrcode from "qrcode-terminal"; +import path from "path"; +import { logger } from "../logger/logger.js"; +import { isTermux } from "./environment.js"; + +const QR_PATH = path.resolve("qr.png"); + +/** + * Exibe ou salva o QR Code conforme o ambiente. + * @param {string} qr — string bruta do evento "qr" + */ +export async function handleQR(qr) { + if (isTermux) { + try { + await QRCode.toFile(QR_PATH, qr, { width: 400 }); + logger.info(`QR Code salvo em: ${QR_PATH}`); + logger.info(`Abra com: termux-open qr.png`); + } catch (err) { + logger.error("Falha ao salvar QR Code:", err.message); + } + } else { + logger.info("Escaneie o QR Code abaixo:"); + qrcode.generate(qr, { small: true }); + } +} \ No newline at end of file diff --git a/src/client/whatsappClient.js b/src/client/whatsappClient.js index d72d472..e5496c3 100644 --- a/src/client/whatsappClient.js +++ b/src/client/whatsappClient.js @@ -1,100 +1,39 @@ -import pkg from "whatsapp-web.js"; -import qrcode from "qrcode-terminal"; -import { exec } from "child_process"; -import { CLIENT_ID } from "../config.js"; -import os from "os"; +import pkg from "whatsapp-web.js"; +import { CLIENT_ID } from "../config.js"; +import { logger } from "../logger/logger.js"; +import { isTermux, resolvePuppeteerConfig } from "./environment.js"; +import { handleQR } from "./qrHandler.js"; +import { printBanner } from "./banner.js"; export const { Client, LocalAuth, MessageMedia } = pkg; -// ── Logger ────────────────────────────────────────────────── -const c = { - reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", - green: "\x1b[32m", yellow: "\x1b[33m", cyan: "\x1b[36m", - red: "\x1b[31m", gray: "\x1b[90m", white: "\x1b[37m", - blue: "\x1b[34m", magenta: "\x1b[35m", -}; - -const now = () => - new Date().toLocaleString("pt-BR", { dateStyle: "short", timeStyle: "short" }); - -export const logger = { - info: (...a) => console.log(`${c.gray}${now()}${c.reset} ℹ️ `, ...a), - success: (...a) => console.log(`${c.gray}${now()}${c.reset} ${c.green}✅${c.reset}`, ...a), - warn: (...a) => console.log(`${c.gray}${now()}${c.reset} ${c.yellow}⚠️ ${c.reset}`, ...a), - error: (...a) => console.log(`${c.gray}${now()}${c.reset} ${c.red}❌${c.reset}`, ...a), - bot: (...a) => console.log(`${c.gray}${now()}${c.reset} ${c.magenta}🤖${c.reset}`, ...a), -}; - -// ── Banner ─────────────────────────────────────────────────── -function printBanner() { - console.log(`${c.blue}${c.bold}`); - console.log(` _____ _____ _ `); - console.log(`| |___ ___ _ _| __ |___| |_ `); - console.log(`| | | | .'| | | | __ -| . | _|`); - console.log(`|_|_|_|__,|_|_|_ |_____|___|_| `); - console.log(` |___| `); - console.log(); - console.log(` website : ${c.reset}${c.cyan}www.mlplovers.com.br/manybot${c.reset}`); - console.log(` repo : ${c.reset}${c.cyan}github.com/synt-xerror/manybot${c.reset}`); - console.log(); - console.log(` ${c.bold}✨ A Amizade é Mágica!${c.reset}`); - console.log(); -} - // ── Ambiente ───────────────────────────────────────────────── -const isTermux = - (os.platform() === "linux" || os.platform() === "android") && - process.env.PREFIX?.startsWith("/data/data/com.termux"); - logger.info(isTermux - ? `Ambiente: ${c.yellow}${c.bold}Termux${c.reset} — usando Chromium do sistema` - : `Ambiente: ${c.blue}${c.bold}${os.platform()}${c.reset} — usando Puppeteer padrão` + ? "Ambiente: Termux — usando Chromium do sistema" + : `Ambiente: ${process.platform} — usando Puppeteer padrão` ); -const puppeteerConfig = isTermux - ? { - executablePath: "/data/data/com.termux/files/usr/bin/chromium-browser", - args: [ - "--headless=new", "--no-sandbox", "--disable-setuid-sandbox", - "--disable-dev-shm-usage", "--disable-gpu", "--single-process", - "--no-zygote", "--disable-software-rasterizer", - ], - } - : {}; - -// ── Cliente ────────────────────────────────────────────────── +// ── Instância ───────────────────────────────────────────────── export const client = new Client({ authStrategy: new LocalAuth({ clientId: CLIENT_ID }), - puppeteer: { headless: true, ...puppeteerConfig }, + puppeteer: { headless: true, ...resolvePuppeteerConfig() }, }); -client.on("qr", async qr => { - if (isTermux) { - try { - await QRCode.toFile(QR_PATH, qr, { width: 400 }); - logger.bot(`QR Code salvo em: ${c.cyan}${c.bold}${QR_PATH}${c.reset}`); - logger.bot(`Abra com: ${c.yellow}termux-open qr.png${c.reset}`); - } catch (err) { - logger.error("Falha ao salvar QR Code:", err.message); - } - } else { - logger.bot(`Escaneie o ${c.yellow}${c.bold}QR Code${c.reset} abaixo:`); - qrcode.generate(qr, { small: true }); - } -}); +// ── Eventos ─────────────────────────────────────────────────── +client.on("qr", handleQR); client.on("ready", () => { - exec("clear"); + console.log("READY DISPAROU"); // temporário printBanner(); - logger.success(`${c.green}${c.bold}WhatsApp conectado e pronto!${c.reset}`); - logger.info(`Client ID: ${c.cyan}${CLIENT_ID}${c.reset}`); + logger.success("WhatsApp conectado e pronto!"); + logger.info(`Client ID: ${CLIENT_ID}`); }); -client.on("disconnected", reason => { - logger.warn(`Desconectado — motivo: ${c.yellow}${reason}${c.reset}`); - logger.info(`Reconectando em ${c.cyan}5s${c.reset}...`); +client.on("disconnected", (reason) => { + logger.warn(`Desconectado — motivo: ${reason}`); + logger.info("Reconectando em 5s..."); setTimeout(() => { - logger.bot("Reinicializando cliente..."); + logger.info("Reinicializando cliente..."); client.initialize(); }, 5000); }); diff --git a/src/commands/figurinha.js b/src/commands/figurinha.js deleted file mode 100644 index 16300ed..0000000 --- a/src/commands/figurinha.js +++ /dev/null @@ -1,272 +0,0 @@ -import fs from "fs"; -import path from "path"; -import os from "os"; -import { execFile } from "child_process"; -import { promisify } from "util"; - -import pkg from "whatsapp-web.js"; -import { createSticker } from "wa-sticker-formatter"; - -import { client } from "../client/whatsappClient.js"; -import { botMsg } from "../utils/botMsg.js"; -import { emptyFolder } from "../utils/file.js"; -import { stickerSessions } from "./index.js"; - -const { MessageMedia } = pkg; -const execFileAsync = promisify(execFile); - -const DOWNLOADS_DIR = path.resolve("downloads"); -const FFMPEG = os.platform() === "win32" - ? ".\\bin\\ffmpeg.exe" - : "./bin/ffmpeg"; - -const MAX_STICKER_SIZE = 900 * 1024; -const SESSION_TIMEOUT = 2 * 60 * 1000; -const MAX_MEDIA = 10; - -// ───────────────── Helpers ───────────────── - -function ensureDownloadsDir() { - if (!fs.existsSync(DOWNLOADS_DIR)) { - fs.mkdirSync(DOWNLOADS_DIR, { recursive: true }); - } -} - -function cleanupFiles(...files) { - for (const f of files) { - if (f && fs.existsSync(f)) fs.unlinkSync(f); - } -} - -// Converte vídeo/gif → GIF 512x512 com paleta preservada -async function convertVideoToGif(inputPath, outputPath, fps = 12) { - - const clampedFps = Math.min(fps, 12); - - const filter = [ - `fps=${clampedFps},scale=512:512:flags=lanczos,split[s0][s1]`, - `[s0]palettegen=max_colors=256:reserve_transparent=1[p]`, - `[s1][p]paletteuse=dither=bayer` - ].join(";"); - - await execFileAsync(FFMPEG, [ - "-i", inputPath, - "-filter_complex", filter, - "-loop", "0", - "-y", - outputPath - ]); -} - -// Força imagem estática para 512x512 -async function resizeToSticker(inputPath, outputPath) { - - await execFileAsync(FFMPEG, [ - "-i", inputPath, - "-vf", "scale=512:512:flags=lanczos", - "-y", - outputPath - ]); - -} - -async function createStickerWithFallback(stickerInputPath, isAnimated) { - - const qualities = [80, 60, 40, 20]; - - for (const quality of qualities) { - - const buffer = await createSticker( - fs.readFileSync(stickerInputPath), - { - pack: "Criada por ManyBot\n", - author: "\ngithub.com/synt-xerror/manybot", - type: isAnimated ? "FULL" : "STATIC", - categories: ["🤖"], - quality, - } - ); - - if (buffer.length <= MAX_STICKER_SIZE) { - return buffer; - } - - } - - throw new Error("Não foi possível reduzir o sticker para menos de 900 KB."); -} - -// ───────────────── Sessão ───────────────── - -export const help = - "📌 *Como criar figurinhas:*\n\n" + - "1️⃣ Digite `!figurinha` para iniciar\n" + - "2️⃣ Envie as imagens, GIFs ou vídeos que quer transformar\n" + - "3️⃣ Digite `!figurinha criar` para gerar as figurinhas\n\n" + - "⏳ A sessão expira em 2 minutos se nenhuma mídia for enviada."; - -export function iniciarSessao(chatId, author, msg) { - if (stickerSessions.has(chatId)) return false; - - const timeout = setTimeout(async () => { - stickerSessions.delete(chatId); - try { - await msg.reply(botMsg( - "⏰ *Sessão expirada!*\n\n" + - "Você demorou mais de 2 minutos para enviar as mídias.\n" + - "Digite `!figurinha` para começar de novo." - )); - } catch (err) { - console.error("Erro ao notificar expiração:", err.message); - } - }, SESSION_TIMEOUT); - - stickerSessions.set(chatId, { author, medias: [], timeout }); - return true; -} - -// ───────────────── Coleta de mídia ───────────────── - -export async function coletarMidia(msg) { -; - // figurinha.js — coletarMidia - const chat = await msg.getChat(); - const chatId = chat.id._serialized; // ← volta pra isso - - const session = stickerSessions.get(chatId); - if (!session) return; - - const sender = msg.author || msg.from; - if (sender !== session.author) return; - if (!msg.hasMedia) return; - - const media = await msg.downloadMedia(); - if (!media) return; - - const isGif = - media.mimetype === "image/gif" || - (media.mimetype === "video/mp4" && msg._data?.isGif); - - if ( - !media.mimetype || - (!media.mimetype.startsWith("image/") && - !media.mimetype.startsWith("video/") && - !isGif) - ) { - return; - } - - if (session.medias.length < MAX_MEDIA) { - session.medias.push(media); - } - -} - -// ───────────────── Criar stickers ───────────────── - -export async function gerarSticker(msg, chatId) { - console.log("[gerarSticker] chatId:", chatId); - - const sender = msg.author || msg.from; - const session = stickerSessions.get(chatId); - - if (!session) { - return msg.reply(botMsg( - "❌ *Nenhuma sessão ativa.*\n\n" + help - )); - } - - if (session.author !== sender) { - return msg.reply(botMsg( - "🚫 Só quem digitou `!figurinha` pode usar `!figurinha criar`." - )); - } - - const medias = session.medias; - - if (!medias.length) { - return msg.reply(botMsg( - "📭 *Você ainda não enviou nenhuma mídia!*\n\n" + help - )); - } - - clearTimeout(session.timeout); - - console.log("midias:", medias.length); - - await msg.reply(botMsg("⏳ Gerando suas figurinhas, aguarde um momento...")); - - ensureDownloadsDir(); - - for (const media of medias) { - try { - const ext = media.mimetype.split("/")[1]; - const isVideo = media.mimetype.startsWith("video/"); - const isGif = media.mimetype === "image/gif"; - const isAnimated = isVideo || isGif; - - const id = Date.now() + "-" + Math.random().toString(36).slice(2); - const inputPath = path.join(DOWNLOADS_DIR, `${id}.${ext}`); - const gifPath = path.join(DOWNLOADS_DIR, `${id}.gif`); - const resizedPath = path.join(DOWNLOADS_DIR, `${id}-scaled.${ext}`); - - fs.writeFileSync(inputPath, Buffer.from(media.data, "base64")); - - const inputSize = fs.statSync(inputPath).size; - console.log(`[1] mimetype: ${media.mimetype} | isAnimated: ${isAnimated} | inputPath: ${inputPath} | size: ${inputSize} bytes`); - - let stickerInputPath = inputPath; - - if (isAnimated) { - console.log("[2] Convertendo para GIF..."); - await convertVideoToGif(inputPath, gifPath, isVideo ? 12 : 24); - - if (fs.existsSync(gifPath)) { - console.log(`[2] GIF gerado: ${fs.statSync(gifPath).size} bytes`); - } else { - console.error("[2] ERRO: gifPath não foi criado pelo ffmpeg!"); - } - - stickerInputPath = gifPath; - } else { - console.log("[2] Redimensionando imagem estática..."); - await resizeToSticker(inputPath, resizedPath); - - if (fs.existsSync(resizedPath)) { - console.log(`[2] Resized gerado: ${fs.statSync(resizedPath).size} bytes`); - } else { - console.error("[2] ERRO: resizedPath não foi criado!"); - } - - stickerInputPath = resizedPath; - } - - console.log(`[3] stickerInputPath: ${stickerInputPath} | exists: ${fs.existsSync(stickerInputPath)} | size: ${fs.existsSync(stickerInputPath) ? fs.statSync(stickerInputPath).size : "N/A"} bytes`); - - const stickerBuffer = await createStickerWithFallback(stickerInputPath, isAnimated); - - console.log(`[4] Sticker buffer: ${stickerBuffer.length} bytes`); - - const stickerMedia = new MessageMedia("image/webp", stickerBuffer.toString("base64")); - await client.sendMessage(chatId, stickerMedia, { sendMediaAsSticker: true }); - - cleanupFiles(inputPath, gifPath, resizedPath); - - } catch (err) { - console.error("Erro ao gerar sticker:", err); - await msg.reply(botMsg( - "⚠️ Não consegui criar uma das figurinhas.\n" + - "Tente reenviar essa mídia ou use outro formato (JPG, PNG, GIF, MP4)." - )); - } - } - - await msg.reply(botMsg( - "✅ *Figurinhas criadas com sucesso!*\n" + - "Salve as que quiser no seu WhatsApp. 😄" - )); - - stickerSessions.delete(chatId); - emptyFolder("downloads"); - -} \ No newline at end of file diff --git a/src/commands/handlers/a.js b/src/commands/handlers/a.js new file mode 100644 index 0000000..f352d66 --- /dev/null +++ b/src/commands/handlers/a.js @@ -0,0 +1,5 @@ +import { botMsg } from "../../utils/botMsg.js"; + +export async function cmdA(msg, _chat, _chatId, args) { + if (!args[0]) await msg.reply(botMsg("B!")); +} \ No newline at end of file diff --git a/src/commands/handlers/adivinhacao.js b/src/commands/handlers/adivinhacao.js new file mode 100644 index 0000000..cb23069 --- /dev/null +++ b/src/commands/handlers/adivinhacao.js @@ -0,0 +1,44 @@ +import { iniciarJogo, pararJogo } from "../logic/games/adivinhacao.js"; +import { botMsg } from "../../utils/botMsg.js"; +import { logger } from "../../logger/logger.js"; + +const SUBCOMANDOS = new Map([ + ["começar", async (chat) => { + iniciarJogo(chat.id._serialized); + await chat.sendMessage(botMsg( + "🎮 *Jogo iniciado!*\n\n" + + "Estou pensando em um número de 1 a 100.\n" + + "Tente adivinhar! 🤔" + )); + logger.done("!adivinhação", "jogo iniciado"); + }], + ["parar", async (chat) => { + pararJogo(chat.id._serialized); + await chat.sendMessage(botMsg("🛑 Jogo encerrado.")); + logger.done("!adivinhação", "jogo parado"); + }], +]); + +export async function cmdAdivinhacao(msg, chat, _chatId, args) { + if (!args[0]) { + await chat.sendMessage(botMsg( + "🎮 *Jogo de adivinhação:*\n\n" + + "`!adivinhação começar` — inicia o jogo\n" + + "`!adivinhação parar` — encerra o jogo" + )); + return; + } + + const subcomando = SUBCOMANDOS.get(args[0]); + + if (!subcomando) { + await chat.sendMessage(botMsg( + `❌ Subcomando *${args[0]}* não existe.\n\n` + + "Use `!adivinhação começar` ou `!adivinhação parar`." + )); + logger.warn(`!adivinhação — subcomando desconhecido: ${args[0]}`); + return; + } + + await subcomando(chat); +} \ No newline at end of file diff --git a/src/commands/handlers/audio.js b/src/commands/handlers/audio.js new file mode 100644 index 0000000..0840db0 --- /dev/null +++ b/src/commands/handlers/audio.js @@ -0,0 +1,15 @@ +import { enqueueDownload } from "../../download/queue.js"; +import { botMsg } from "../../utils/botMsg.js"; +import { logger } from "../../logger/logger.js"; + +export async function cmdAudio(msg, chat, chatId, args) { + if (!args[0]) { + await msg.reply(botMsg("❌ Você precisa informar um link.\n\nExemplo: `!audio https://youtube.com/...`")); + logger.warn("!audio sem link"); + return; + } + + await msg.reply(botMsg("⏳ Baixando o áudio, aguarde...")); + enqueueDownload("audio", args[0], msg, chatId); + logger.done("!audio", `enfileirado → ${args[0]}`); +} \ No newline at end of file diff --git a/src/commands/handlers/figurinha.js b/src/commands/handlers/figurinha.js new file mode 100644 index 0000000..eb06442 --- /dev/null +++ b/src/commands/handlers/figurinha.js @@ -0,0 +1,26 @@ +import { iniciarSessao, gerarSticker, help } from "../logic/figurinha.js"; +import { stickerSessions } from "../logic/stickerSessions.js"; +import { botMsg } from "../../utils/botMsg.js"; + +export async function cmdFigurinha(msg, chat, _chatId, args) { + const author = msg.author || msg.from; + const name = msg._data?.notifyName || author.replace(/(:\d+)?@.*$/, ""); + const groupId = chat.id._serialized; + + if (args[0] === "criar") { + await gerarSticker(msg, groupId); + return; + } + + if (stickerSessions.has(groupId)) { + await msg.reply(botMsg( + "⚠️ Já existe uma sessão aberta.\n\n" + + "Envie as mídias e depois use `!figurinha criar`.\n" + + "Ou aguarde 2 minutos para a sessão expirar." + )); + return; + } + + iniciarSessao(groupId, author, msg); + await msg.reply(botMsg(`✅ Sessão iniciada por *${name}*!\n\n${help}`)); +} \ No newline at end of file diff --git a/src/commands/handlers/info.js b/src/commands/handlers/info.js new file mode 100644 index 0000000..e4bda03 --- /dev/null +++ b/src/commands/handlers/info.js @@ -0,0 +1,24 @@ +import { botMsg } from "../../utils/botMsg.js"; + +const HELP = new Map([ + ["ping", "> `!ping`\nResponde pong."], + ["video", "> `!video `\nBaixa vídeo da internet."], + ["audio", "> `!audio `\nBaixa áudio da internet."], + ["figurinha", "> `!figurinha`\nTransforma imagem/GIF em sticker."], +]); + +/** + * Envia a descrição de um comando específico. + * @param {string} cmd — nome do comando (sem prefixo) + * @param {object} chat + */ +export async function processarInfo(cmd, chat) { + const texto = HELP.get(cmd); + + if (!texto) { + await chat.sendMessage(botMsg(`❌ Comando '${cmd}' não encontrado.`)); + return; + } + + await chat.sendMessage(botMsg(texto)); +} \ No newline at end of file diff --git a/src/commands/handlers/many.js b/src/commands/handlers/many.js new file mode 100644 index 0000000..c813314 --- /dev/null +++ b/src/commands/handlers/many.js @@ -0,0 +1,11 @@ +import { botMsg } from "../../utils/botMsg.js"; + +export async function cmdMany(msg, chat) { + await chat.sendMessage(botMsg( + "*Comandos disponíveis:*\n\n" + + "🎬 `!video ` — baixa um vídeo\n" + + "🎵 `!audio ` — baixa um áudio\n" + + "🖼️ `!figurinha` — cria figurinhas\n" + + "🎮 `!adivinhação começar|parar` — jogo de adivinhar número\n" + )); +} \ No newline at end of file diff --git a/src/commands/handlers/obrigado.js b/src/commands/handlers/obrigado.js new file mode 100644 index 0000000..fe0d8c7 --- /dev/null +++ b/src/commands/handlers/obrigado.js @@ -0,0 +1,5 @@ +import { botMsg } from "../../utils/botMsg.js"; + +export async function cmdObrigado(msg) { + await msg.reply(botMsg("😊 Por nada!")); +} \ No newline at end of file diff --git a/src/commands/handlers/video.js b/src/commands/handlers/video.js new file mode 100644 index 0000000..9a0af97 --- /dev/null +++ b/src/commands/handlers/video.js @@ -0,0 +1,15 @@ +import { enqueueDownload } from "../../download/queue.js"; +import { botMsg } from "../../utils/botMsg.js"; +import { logger } from "../../logger/logger.js"; + +export async function cmdVideo(msg, chat, chatId, args) { + if (!args[0]) { + await msg.reply(botMsg("❌ Você precisa informar um link.\n\nExemplo: `!video https://youtube.com/...`")); + logger.warn("!video sem link"); + return; + } + + await msg.reply(botMsg("⏳ Baixando o vídeo, aguarde...")); + enqueueDownload("video", args[0], msg, chatId); + logger.done("!video", `enfileirado → ${args[0]}`); +} \ No newline at end of file diff --git a/src/commands/index.js b/src/commands/index.js index caa4ee1..e022f10 100644 --- a/src/commands/index.js +++ b/src/commands/index.js @@ -1,135 +1,30 @@ -import { enqueueDownload } from "../download/queue.js"; -import { iniciarSessao, gerarSticker, help } from "./figurinha.js"; -import { botMsg } from "../utils/botMsg.js"; -import { iniciarJogo, pararJogo } from "../games/adivinhacao.js"; -import { processarInfo } from "./info.js"; -import client from "../client/whatsappClient.js"; - -export const stickerSessions = new Map(); - -const c = { - reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", - green: "\x1b[32m", yellow: "\x1b[33m", cyan: "\x1b[36m", - red: "\x1b[31m", gray: "\x1b[90m", -}; - -const now = () => - new Date().toLocaleString("pt-BR", { dateStyle: "short", timeStyle: "medium" }); - -const log = { - cmd: (cmd, ...a) => console.log(`${c.gray}${now()}${c.reset} ${c.cyan}⚙️ ${c.bold}${cmd}${c.reset}`, ...a), - ok: (...a) => console.log(`${c.gray}${now()}${c.reset} ${c.green}✅${c.reset}`, ...a), - warn: (...a) => console.log(`${c.gray}${now()}${c.reset} ${c.yellow}⚠️ ${c.reset}`, ...a), - error: (...a) => console.log(`${c.gray}${now()}${c.reset} ${c.red}❌${c.reset}`, ...a), -}; +import { parseCommand } from "./parser.js"; +import { commandRegistry } from "./registry.js"; +import { logger } from "../logger/logger.js"; +import { botMsg } from "../utils/botMsg.js"; +/** + * Roteia a mensagem para o handler correto. + * Não conhece nenhum comando — apenas delega. + */ export async function processarComando(msg, chat, chatId) { - const tokens = msg.body.trim().split(/\s+/); - const cmd = tokens[0]?.toLowerCase(); + const { cmd, args, valid } = parseCommand(msg.body); - if (!cmd?.startsWith("!") && cmd !== "a") return; + if (!valid) return; - log.cmd(cmd); + const handler = commandRegistry.get(cmd); + + if (!handler) { + logger.warn(`Comando desconhecido: ${cmd}`); + return; + } + + logger.cmd(cmd); try { - switch (cmd) { - case "!many": - await chat.sendMessage(botMsg( - "*Comandos disponíveis:*\n\n" + - "🎬 `!video ` — baixa um vídeo\n" + - "🎵 `!audio ` — baixa um áudio\n" + - "🖼️ `!figurinha` — cria figurinhas\n" + - "🎮 `!adivinhação começar|parar` — jogo de adivinhar número\n" - )); - break; - - case "!video": - if (!tokens[1]) { - await msg.reply(botMsg("❌ Você precisa informar um link.\n\nExemplo: `!video https://youtube.com/...`")); - log.warn("!video sem link"); - return; - } - await msg.reply(botMsg("⏳ Baixando o vídeo, aguarde...")); - enqueueDownload("video", tokens[1], msg, chatId); - log.ok("vídeo enfileirado →", tokens[1]); - break; - - case "!audio": - if (!tokens[1]) { - await msg.reply(botMsg("❌ Você precisa informar um link.\n\nExemplo: `!audio https://youtube.com/...`")); - log.warn("!audio sem link"); - return; - } - await msg.reply(botMsg("⏳ Baixando o áudio, aguarde...")); - enqueueDownload("audio", tokens[1], msg, chatId); - log.ok("áudio enfileirado →", tokens[1]); - break; - - case "!figurinha": - const author = msg.author || msg.from; - const name = msg._data?.notifyName || author.replace(/(:\d+)?@.*$/, ""); - const groupId = chat.id._serialized; // < fonte única de verdade - - if (tokens[1] === "criar") { - await gerarSticker(msg, groupId); - } else { - if (stickerSessions.has(groupId)) { - return msg.reply(botMsg( - "⚠️ Já existe uma sessão aberta.\n\n" + - "Envie as mídias e depois use `!figurinha criar`.\n" + - "Ou aguarde 2 minutos para a sessão expirar." - )); - } - iniciarSessao(groupId, author, msg); - await msg.reply(botMsg( - `✅ Sessão iniciada por *${name}*!\n\n` + help - )); - } - break; - - - case "!adivinhação": - if (!tokens[1]) { - await chat.sendMessage(botMsg( - "🎮 *Jogo de adivinhação:*\n\n" + - "`!adivinhação começar` — inicia o jogo\n" + - "`!adivinhação parar` — encerra o jogo" - )); - return; - } - if (tokens[1] === "começar") { - iniciarJogo(); - await chat.sendMessage(botMsg( - "🎮 *Jogo iniciado!*\n\n" + - "Estou pensando em um número de 1 a 100.\n" + - "Tente adivinhar! 🤔" - )); - log.ok("jogo iniciado"); - } else if (tokens[1] === "parar") { - pararJogo(); - await chat.sendMessage(botMsg("🛑 Jogo encerrado.")); - log.ok("jogo parado"); - } else { - await chat.sendMessage(botMsg( - `❌ Subcomando *${tokens[1]}* não existe.\n\n` + - "Use `!adivinhação começar` ou `!adivinhação parar`." - )); - log.warn("!adivinhação — subcomando desconhecido:", tokens[1]); - } - break; - - case "!obrigado": - case "!valeu": - case "!brigado": - await msg.reply(botMsg("😊 Por nada!")); - break; - - case "a": - if (!tokens[1]) await msg.reply(botMsg("B")); - break; - } + await handler(msg, chat, chatId, args); } catch (err) { - log.error("Falha em", cmd, "—", err.message); + logger.error(`Falha em ${cmd} — ${err.message}`); await chat.sendMessage(botMsg( "❌ Algo deu errado ao executar esse comando.\n" + "Tente novamente em instantes." diff --git a/src/commands/info.js b/src/commands/info.js deleted file mode 100644 index 73f2dc9..0000000 --- a/src/commands/info.js +++ /dev/null @@ -1,20 +0,0 @@ -import { botMsg } from "../utils/botMsg.js"; - -export async function processarInfo(cmd, chat) { - switch(cmd) { - case "ping": - await chat.sendMessage(botMsg("> `!ping`\nResponde pong.")); - break; - case "video": - await chat.sendMessage(botMsg("> `!video `\nBaixa vídeo da internet.")); - break; - case "audio": - await chat.sendMessage(botMsg("> `!audio `\nBaixa áudio da internet.")); - break; - case "figurinha": - await chat.sendMessage(botMsg("`!figurinha`\nTransforma imagem/GIF em sticker.")); - break; - default: - await chat.sendMessage(botMsg(`❌ Comando '${tokens[1]}' não encontrado.`)); - } -} \ No newline at end of file diff --git a/src/commands/logic/figurinha.js b/src/commands/logic/figurinha.js new file mode 100644 index 0000000..2fb476c --- /dev/null +++ b/src/commands/logic/figurinha.js @@ -0,0 +1,192 @@ +import fs from "fs"; +import path from "path"; +import os from "os"; +import { execFile } from "child_process"; +import { promisify } from "util"; + +import pkg from "whatsapp-web.js"; +import { createSticker } from "wa-sticker-formatter"; + +import { client } from "../../client/whatsappClient.js"; +import { botMsg } from "../../utils/botMsg.js"; +import { emptyFolder } from "../../utils/file.js"; +import { stickerSessions } from "./stickerSessions.js"; // ← sem circular +import { logger } from "../../logger/logger.js"; + +const { MessageMedia } = pkg; +const execFileAsync = promisify(execFile); + +// ── Constantes ──────────────────────────────────────────────── +const DOWNLOADS_DIR = path.resolve("downloads"); +const FFMPEG = os.platform() === "win32" ? ".\\bin\\ffmpeg.exe" : "./bin/ffmpeg"; +const MAX_STICKER_SIZE = 900 * 1024; +const SESSION_TIMEOUT = 2 * 60 * 1000; +const MAX_MEDIA = 10; + +// ── Helpers ─────────────────────────────────────────────────── +function ensureDownloadsDir() { + fs.mkdirSync(DOWNLOADS_DIR, { recursive: true }); +} + +function cleanupFiles(...files) { + for (const f of files) { + try { if (f && fs.existsSync(f)) fs.unlinkSync(f); } catch { /* ignora */ } + } +} + +async function convertVideoToGif(inputPath, outputPath, fps = 12) { + const clampedFps = Math.min(fps, 12); + const filter = [ + `fps=${clampedFps},scale=512:512:flags=lanczos,split[s0][s1]`, + `[s0]palettegen=max_colors=256:reserve_transparent=1[p]`, + `[s1][p]paletteuse=dither=bayer`, + ].join(";"); + + await execFileAsync(FFMPEG, ["-i", inputPath, "-filter_complex", filter, "-loop", "0", "-y", outputPath]); +} + +async function resizeToSticker(inputPath, outputPath) { + await execFileAsync(FFMPEG, ["-i", inputPath, "-vf", "scale=512:512:flags=lanczos", "-y", outputPath]); +} + +async function createStickerWithFallback(stickerInputPath, isAnimated) { + for (const quality of [80, 60, 40, 20]) { + const buffer = await createSticker(fs.readFileSync(stickerInputPath), { + pack: "Criada por ManyBot\n", + author: "\ngithub.com/synt-xerror/manybot", + type: isAnimated ? "FULL" : "STATIC", + categories: ["🤖"], + quality, + }); + + if (buffer.length <= MAX_STICKER_SIZE) return buffer; + } + + throw new Error("Não foi possível reduzir o sticker para menos de 900 KB."); +} + +// ── Textos ──────────────────────────────────────────────────── +export const help = + "📌 *Como criar figurinhas:*\n\n" + + "1️⃣ Digite `!figurinha` para iniciar\n" + + "2️⃣ Envie as imagens, GIFs ou vídeos que quer transformar\n" + + "3️⃣ Digite `!figurinha criar` para gerar as figurinhas\n\n" + + "⏳ A sessão expira em 2 minutos se nenhuma mídia for enviada."; + +// ── Sessão ──────────────────────────────────────────────────── +export function iniciarSessao(chatId, author, msg) { + if (stickerSessions.has(chatId)) return false; + + const timeout = setTimeout(async () => { + stickerSessions.delete(chatId); + try { + await msg.reply(botMsg( + "⏰ *Sessão expirada!*\n\n" + + "Você demorou mais de 2 minutos para enviar as mídias.\n" + + "Digite `!figurinha` para começar de novo." + )); + } catch (err) { + logger.warn(`Erro ao notificar expiração da sessão: ${err.message}`); + } + }, SESSION_TIMEOUT); + + stickerSessions.set(chatId, { author, medias: [], timeout }); + return true; +} + +// ── Coleta de mídia ─────────────────────────────────────────── +export async function coletarMidia(msg) { + const chat = await msg.getChat(); + const chatId = chat.id._serialized; + const session = stickerSessions.get(chatId); + + if (!session) return; + + const sender = msg.author || msg.from; + if (sender !== session.author) return; + if (!msg.hasMedia) return; + + const media = await msg.downloadMedia(); + if (!media) return; + + const isGif = + media.mimetype === "image/gif" || + (media.mimetype === "video/mp4" && msg._data?.isGif); + + const isSupported = + media.mimetype?.startsWith("image/") || + media.mimetype?.startsWith("video/") || + isGif; + + if (!isSupported) return; + + if (session.medias.length < MAX_MEDIA) { + session.medias.push(media); + } +} + +// ── Geração de stickers ─────────────────────────────────────── +export async function gerarSticker(msg, chatId) { + const sender = msg.author || msg.from; + const session = stickerSessions.get(chatId); + + if (!session) { + return msg.reply(botMsg("❌ *Nenhuma sessão ativa.*\n\n" + help)); + } + + if (session.author !== sender) { + return msg.reply(botMsg("🚫 Só quem digitou `!figurinha` pode usar `!figurinha criar`.")); + } + + if (!session.medias.length) { + return msg.reply(botMsg("📭 *Você ainda não enviou nenhuma mídia!*\n\n" + help)); + } + + clearTimeout(session.timeout); + await msg.reply(botMsg("⏳ Gerando suas figurinhas, aguarde um momento...")); + ensureDownloadsDir(); + + for (const media of session.medias) { + try { + const ext = media.mimetype.split("/")[1]; + const isVideo = media.mimetype.startsWith("video/"); + const isGif = media.mimetype === "image/gif"; + const isAnimated = isVideo || isGif; + + const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`; + const inputPath = path.join(DOWNLOADS_DIR, `${id}.${ext}`); + const gifPath = path.join(DOWNLOADS_DIR, `${id}.gif`); + const resizedPath = path.join(DOWNLOADS_DIR, `${id}-scaled.${ext}`); + + fs.writeFileSync(inputPath, Buffer.from(media.data, "base64")); + + let stickerInputPath; + + if (isAnimated) { + await convertVideoToGif(inputPath, gifPath, isVideo ? 12 : 24); + stickerInputPath = gifPath; + } else { + await resizeToSticker(inputPath, resizedPath); + stickerInputPath = resizedPath; + } + + const stickerBuffer = await createStickerWithFallback(stickerInputPath, isAnimated); + const stickerMedia = new MessageMedia("image/webp", stickerBuffer.toString("base64")); + + await client.sendMessage(chatId, stickerMedia, { sendMediaAsSticker: true }); + cleanupFiles(inputPath, gifPath, resizedPath); + + } catch (err) { + logger.error(`Erro ao gerar sticker: ${err.message}`); + await msg.reply(botMsg( + "⚠️ Não consegui criar uma das figurinhas.\n" + + "Tente reenviar essa mídia ou use outro formato (JPG, PNG, GIF, MP4)." + )); + } + } + + await msg.reply(botMsg("✅ *Figurinhas criadas com sucesso!*\nSalve as que quiser no seu WhatsApp. 😄")); + + stickerSessions.delete(chatId); + emptyFolder("downloads"); +} \ No newline at end of file diff --git a/src/commands/logic/games/adivinhacao.js b/src/commands/logic/games/adivinhacao.js new file mode 100644 index 0000000..4a5675c --- /dev/null +++ b/src/commands/logic/games/adivinhacao.js @@ -0,0 +1,61 @@ +import { botMsg } from "../../../utils/botMsg.js"; + +/** + * Estado dos jogos ativos, keyed por chatId. + * Permite múltiplos grupos jogando simultaneamente sem conflito. + * @type {Map} + */ +const jogosAtivos = new Map(); + +const RANGE = { min: 1, max: 100 }; + +const sorteio = () => + Math.floor(Math.random() * (RANGE.max - RANGE.min + 1)) + RANGE.min; + +/** + * @param {string} chatId + */ +export function iniciarJogo(chatId) { + const numero = sorteio(); + jogosAtivos.set(chatId, numero); + return numero; +} + +/** + * @param {string} chatId + */ +export function pararJogo(chatId) { + jogosAtivos.delete(chatId); +} + +/** + * Processa uma tentativa de adivinhação. + * @param {import("whatsapp-web.js").Message} msg + * @param {import("whatsapp-web.js").Chat} chat + */ +export async function processarJogo(msg, chat) { + const chatId = chat.id._serialized; + const numero = jogosAtivos.get(chatId); + + if (numero === undefined) return; + + const tentativa = msg.body.trim(); + if (!/^\d+$/.test(tentativa)) return; + + const num = parseInt(tentativa, 10); + + if (num < RANGE.min || num > RANGE.max) { + await msg.reply(botMsg(`⚠️ Digite um número entre ${RANGE.min} e ${RANGE.max}.`)); + return; + } + + if (num === numero) { + await msg.reply(botMsg( + `🎉 *Acertou!* O número era ${numero}!\n\n` + + "Use \`!adivinhação começar\` para jogar de novo." + )); + pararJogo(chatId); + } else { + await chat.sendMessage(botMsg(num > numero ? "📉 Tente um número *menor*!" : "📈 Tente um número *maior*!")); + } +} \ No newline at end of file diff --git a/src/commands/logic/stickerSessions.js b/src/commands/logic/stickerSessions.js new file mode 100644 index 0000000..db31222 --- /dev/null +++ b/src/commands/logic/stickerSessions.js @@ -0,0 +1,7 @@ +/** + * Armazena sessões ativas de criação de figurinha. + * Módulo neutro — não importa nada do projeto, pode ser importado por qualquer um. + * + * @type {Map} + */ +export const stickerSessions = new Map(); \ No newline at end of file diff --git a/src/commands/parser.js b/src/commands/parser.js new file mode 100644 index 0000000..219cc47 --- /dev/null +++ b/src/commands/parser.js @@ -0,0 +1,24 @@ +import { CMD_PREFIX } from "../config.js"; + +/** + * @typedef {Object} ParsedCommand + * @property {string} cmd — ex: "!video", "a" + * @property {string[]} args — tokens restantes + * @property {boolean} valid — false se não for um comando reconhecível + */ + +/** + * Extrai comando e argumentos de uma mensagem. + * Retorna `valid: false` para mensagens que não são comandos. + * + * @param {string} body + * @returns {ParsedCommand} + */ +export function parseCommand(body) { + const tokens = body?.trim().split(/\s+/) ?? []; + const cmd = tokens[0]?.toLowerCase() ?? ""; + const args = tokens.slice(1); + const valid = cmd.startsWith(CMD_PREFIX) || cmd === "a"; + + return { cmd, args, valid }; +} \ No newline at end of file diff --git a/src/commands/registry.js b/src/commands/registry.js new file mode 100644 index 0000000..dcae333 --- /dev/null +++ b/src/commands/registry.js @@ -0,0 +1,25 @@ +import { cmdMany } from "./handlers/many.js"; +import { cmdVideo } from "./handlers/video.js"; +import { cmdAudio } from "./handlers/audio.js"; +import { cmdFigurinha } from "./handlers/figurinha.js"; +import { cmdAdivinhacao } from "./handlers/adivinhacao.js"; +import { cmdObrigado } from "./handlers/obrigado.js"; +import { cmdA } from "./handlers/a.js"; + +/** + * Mapa de comando → handler. + * Cada handler tem a assinatura: (msg, chat, chatId, args) => Promise + * + * @type {Map} + */ +export const commandRegistry = new Map([ + ["!many", cmdMany], + ["!video", cmdVideo], + ["!audio", cmdAudio], + ["!figurinha", cmdFigurinha], + ["!adivinhação", cmdAdivinhacao], + ["!obrigado", cmdObrigado], + ["!valeu", cmdObrigado], + ["!brigado", cmdObrigado], + ["a", cmdA], +]); \ No newline at end of file diff --git a/src/download/audio.js b/src/download/audio.js deleted file mode 100644 index ac949d4..0000000 --- a/src/download/audio.js +++ /dev/null @@ -1,34 +0,0 @@ -import { get_video } from "./video.js"; -import { spawn } from "child_process"; -import os from "os"; - -const so = os.platform(); - -export async function get_audio(url, id) { - const video = await get_video(url, id); - const output = `downloads/${id}.mp3`; - - const cmd = so === "win32" ? ".\\bin\\ffmpeg.exe" : "./bin/ffmpeg"; - const args = ["-i", video, "-vn", "-acodec", "libmp3lame", "-q:a", "2", output]; - - await runCmd(cmd, args); - return output; -} - -async function runCmd(cmd, args) { - return new Promise((resolve, reject) => { - const proc = spawn(cmd, args); - - proc.stdout.on("data", data => console.log("[cmd]", data.toString())); - proc.stderr.on("data", data => console.error("[cmd ERR]", data.toString())); - - proc.on("close", code => { - if (code !== 0) { - return reject(new Error( - "Não foi possível converter o áudio. Tente novamente com outro link." - )); - } - resolve(); - }); - }); -} \ No newline at end of file diff --git a/src/download/downloader.js b/src/download/downloader.js new file mode 100644 index 0000000..9123197 --- /dev/null +++ b/src/download/downloader.js @@ -0,0 +1,112 @@ +import { spawn } from "child_process"; +import { execFile } from "child_process"; +import { promisify } from "util"; +import fs from "fs"; +import path from "path"; +import os from "os"; +import { logger } from "../logger/logger.js"; + +const execFileAsync = promisify(execFile); + +const DOWNLOADS_DIR = path.resolve("downloads"); +const YT_DLP = os.platform() === "win32" ? ".\\bin\\yt-dlp.exe" : "./bin/yt-dlp"; +const FFMPEG = os.platform() === "win32" ? ".\\bin\\ffmpeg.exe" : "./bin/ffmpeg"; + +const ARGS_BASE = [ + "--extractor-args", "youtube:player_client=android", + "--print", "after_move:filepath", + "--cookies", "cookies.txt", + "--add-header", "User-Agent:Mozilla/5.0", + "--add-header", "Referer:https://www.youtube.com/", + "--retries", "4", + "--fragment-retries", "5", + "--socket-timeout", "15", + "--sleep-interval", "1", + "--max-sleep-interval", "4", + "--no-playlist", +]; + +// Ambos baixam como vídeo — áudio é convertido depois via ffmpeg +const ARGS_BY_TYPE = { + video: ["-f", "bv+ba/best"], + audio: ["-f", "bv+ba/best"], // baixa vídeo, converte depois +}; + +/** + * Baixa um vídeo ou áudio via yt-dlp. + * Para áudio, baixa o vídeo e converte para mp3 com ffmpeg. + * @param {"video"|"audio"} type + * @param {string} url + * @param {string} id + * @returns {Promise} caminho do arquivo final + */ +export async function download(type, url, id) { + fs.mkdirSync(DOWNLOADS_DIR, { recursive: true }); + + const output = path.join(DOWNLOADS_DIR, `${id}.%(ext)s`); + const args = [...ARGS_BASE, ...ARGS_BY_TYPE[type], "--output", output, url]; + const videoPath = await runProcess(YT_DLP, args, type); + + if (type === "audio") { + return convertToMp3(videoPath, id); + } + + return videoPath; +} + +/** + * Converte um arquivo de vídeo para mp3 via ffmpeg. + * Remove o vídeo original após a conversão. + * @param {string} videoPath + * @param {string} id + * @returns {Promise} caminho do mp3 + */ +async function convertToMp3(videoPath, id) { + const mp3Path = path.join(DOWNLOADS_DIR, `${id}.mp3`); + + await execFileAsync(FFMPEG, [ + "-i", videoPath, + "-vn", // sem vídeo + "-ar", "44100", // sample rate + "-ac", "2", // stereo + "-b:a", "192k", // bitrate + "-y", // sobrescreve se existir + mp3Path, + ]); + + fs.unlinkSync(videoPath); // remove o vídeo intermediário + return mp3Path; +} + +// ── Compat ──────────────────────────────────────────────────── +export const get_video = (url, id) => download("video", url, id); +export const get_audio = (url, id) => download("audio", url, id); + +// ── Interno ─────────────────────────────────────────────────── +function runProcess(cmd, args, type) { + return new Promise((resolve, reject) => { + const proc = spawn(cmd, args); + let stdout = ""; + + proc.stdout.on("data", (data) => { stdout += data.toString(); }); + proc.stderr.on("data", (data) => { logger.warn(`yt-dlp [${type}]: ${data.toString().trim()}`); }); + + proc.on("close", (code) => { + if (code !== 0) { + return reject(new Error( + `Não foi possível baixar o ${type}. Verifique se o link é válido e tente novamente.` + )); + } + + const filepath = stdout.trim().split("\n").filter(Boolean).at(-1); + + if (!filepath || !fs.existsSync(filepath)) { + return reject(new Error( + "O download foi concluído, mas o arquivo não foi encontrado. Tente novamente." + )); + } + + resolve(filepath); + }); + }); +} \ No newline at end of file diff --git a/src/download/mediaType.js b/src/download/mediaType.js new file mode 100644 index 0000000..51340cb --- /dev/null +++ b/src/download/mediaType.js @@ -0,0 +1,15 @@ +/** + * Retorna o MIME type e extensão para cada tipo de download suportado. + * @param {"video"|"audio"} type + * @returns {{ mime: string, label: string }} + */ +export function resolveMediaType(type) { + const types = { + video: { mime: "video/mp4", label: "vídeo" }, + audio: { mime: "audio/mpeg", label: "áudio" }, + }; + + const resolved = types[type]; + if (!resolved) throw new Error(`Tipo de mídia desconhecido: ${type}`); + return resolved; +} \ No newline at end of file diff --git a/src/download/queue.js b/src/download/queue.js index 6e55f15..ca45b4f 100644 --- a/src/download/queue.js +++ b/src/download/queue.js @@ -1,53 +1,74 @@ -import { get_video } from "./video.js"; -import { get_audio } from "./audio.js"; -import pkg from "whatsapp-web.js"; -import fs from "fs"; -import { botMsg } from "../utils/botMsg.js"; -import { emptyFolder } from "../utils/file.js"; -import client from "../client/whatsappClient.js"; +import fs from "fs"; +import path from "path"; +import pkg from "whatsapp-web.js"; +import { download } from "./downloader.js"; +import { resolveMediaType } from "./mediaType.js"; +import { botMsg } from "../utils/botMsg.js"; +import { emptyFolder } from "../utils/file.js"; +import { logger } from "../logger/logger.js"; +import client from "../client/whatsappClient.js"; const { MessageMedia } = pkg; -let downloadQueue = []; -let processingQueue = false; +/** + * @typedef {{ type: "video"|"audio", url: string, msg: object, chatId: string }} DownloadJob + */ +/** @type {DownloadJob[]} */ +let queue = []; +let processing = false; + +/** + * Adiciona um job à fila e inicia o processamento se estiver idle. + * @param {"video"|"audio"} type + * @param {string} url + * @param {object} msg + * @param {string} chatId + */ export function enqueueDownload(type, url, msg, chatId) { - downloadQueue.push({ type, url, msg, chatId }); - if (!processingQueue) processQueue(); + queue.push({ type, url, msg, chatId }); + if (!processing) processQueue(); } async function processQueue() { - processingQueue = true; + processing = true; - while (downloadQueue.length) { - const job = downloadQueue.shift(); - const label = job.type === "video" ? "vídeo" : "áudio"; - - try { - const filePath = job.type === "video" - ? await get_video(job.url, job.msg.id._serialized) - : await get_audio(job.url, job.msg.id._serialized); - - const file = fs.readFileSync(filePath); - const media = new MessageMedia( - job.type === "video" ? "video/mp4" : "audio/mpeg", - file.toString("base64"), - filePath.split("/").pop() - ); - - await client.sendMessage(job.chatId, media); - fs.unlinkSync(filePath); - emptyFolder("downloads"); - - } catch (err) { - console.error(`[queue] Erro ao baixar ${label}:`, err.message); - await job.msg.reply(botMsg( - `❌ Não consegui baixar o ${label}.\n\n` + - "Verifique se o link é válido e tente novamente.\n" + - "Se o problema persistir, o conteúdo pode estar indisponível ou protegido." - )); - } + while (queue.length) { + const job = queue.shift(); + await processJob(job); } - processingQueue = false; + processing = false; +} + +/** + * Executa um único job: baixa, envia e limpa. + * @param {DownloadJob} job + */ +async function processJob(job) { + const { mime, label } = resolveMediaType(job.type); + + try { + const filePath = await download(job.type, job.url, job.msg.id._serialized); + + const media = new MessageMedia( + mime, + fs.readFileSync(filePath).toString("base64"), + path.basename(filePath) + ); + + await client.sendMessage(job.chatId, media); + + fs.unlinkSync(filePath); + emptyFolder("downloads"); + + logger.done(`download:${job.type}`, job.url); + } catch (err) { + logger.error(`Falha ao baixar ${label} — ${err.message}`); + await job.msg.reply(botMsg( + `❌ Não consegui baixar o ${label}.\n\n` + + "Verifique se o link é válido e tente novamente.\n" + + "Se o problema persistir, o conteúdo pode estar indisponível ou protegido." + )); + } } \ No newline at end of file diff --git a/src/download/video.js b/src/download/video.js deleted file mode 100644 index 9807e27..0000000 --- a/src/download/video.js +++ /dev/null @@ -1,59 +0,0 @@ -import { spawn } from "child_process"; -import fs from "fs"; -import path from "path"; -import os from "os"; - -const platform = os.platform(); - -export async function get_video(url, id) { - const downloadsDir = path.resolve("downloads"); - fs.mkdirSync(downloadsDir, { recursive: true }); - - const cmd = platform === "win32" ? ".\\bin\\yt-dlp.exe" : "./bin/yt-dlp"; - const args = [ - "--extractor-args", "youtube:player_client=android", - "-f", "bv+ba/best", - "--print", "after_move:filepath", - "--output", path.join(downloadsDir, `${id}.%(ext)s`), - "--cookies", "cookies.txt", - "--add-header", "User-Agent:Mozilla/5.0", - "--add-header", "Referer:https://www.youtube.com/", - "--retries", "4", - "--fragment-retries", "5", - "--socket-timeout", "15", - "--sleep-interval", "1", "--max-sleep-interval", "4", - "--no-playlist", - url, - ]; - - return await runCmd(cmd, args); -} - -async function runCmd(cmd, args) { - return new Promise((resolve, reject) => { - const proc = spawn(cmd, args); - let stdout = ""; - - proc.stdout.on("data", data => stdout += data.toString()); - proc.stderr.on("data", data => console.error("[yt-dlp ERR]", data.toString())); - - proc.on("close", code => { - if (code !== 0) { - return reject(new Error( - "Não foi possível baixar o vídeo. Verifique se o link é válido e tente novamente." - )); - } - - const lines = stdout.trim().split("\n").filter(l => l.trim()); - const filepath = lines[lines.length - 1]; - - if (!fs.existsSync(filepath)) { - return reject(new Error( - "O download foi concluído, mas o arquivo não foi encontrado. Tente novamente." - )); - } - - resolve(filepath); - }); - }); -} \ No newline at end of file diff --git a/src/games/adivinhacao.js b/src/games/adivinhacao.js deleted file mode 100644 index 82f1b59..0000000 --- a/src/games/adivinhacao.js +++ /dev/null @@ -1,35 +0,0 @@ -import { botMsg } from "../utils/botMsg.js"; - -let jogoAtivo = null; - -export function iniciarJogo() { - jogoAtivo = Math.floor(Math.random() * 100) + 1; - return jogoAtivo; -} - -export function pararJogo() { - jogoAtivo = null; -} - -export async function processarJogo(msg, chat) { - if (!jogoAtivo) return; - - const tentativa = msg.body.trim(); - if (!/^\d+$/.test(tentativa)) return; - - const num = parseInt(tentativa); - - if (num === jogoAtivo) { - await msg.reply(botMsg( - `🎉 *Acertou!* O número era ${jogoAtivo}!\n\n` + - "Use `!adivinhação começar` para jogar de novo." - )); - pararJogo(); - } else if (num < 1 || num > 100) { - await msg.reply(botMsg("⚠️ Digite um número entre 1 e 100.")); - } else if (num > jogoAtivo) { - await chat.sendMessage(botMsg("📉 Tente um número *menor*!")); - } else { - await chat.sendMessage(botMsg("📈 Tente um número *maior*!")); - } -} \ No newline at end of file diff --git a/src/handlers/messageHandler.js b/src/handlers/messageHandler.js new file mode 100644 index 0000000..95c780f --- /dev/null +++ b/src/handlers/messageHandler.js @@ -0,0 +1,29 @@ +import { CHATS, BOT_PREFIX } from "../config.js"; +import { getChatId } from "../utils/getChatId.js"; +import { processarComando } from "../commands/index.js"; +import { coletarMidia } from "../commands/logic/figurinha.js"; +import { processarJogo } from "../commands/logic/games/adivinhacao.js"; +import { buildMessageContext } from "../logger/messageContext.js"; +import { logger } from "../logger/logger.js"; + +/** + * Pipeline de processamento de uma mensagem recebida. + * Ordem: filtro de chat → log → mídia → comando → jogo. + * + * @param {import("whatsapp-web.js").Message} msg + */ +export async function handleMessage(msg) { + const chat = await msg.getChat(); + const chatId = getChatId(chat); + + if (!CHATS.includes(chatId)) return; + + const ctx = await buildMessageContext(msg, chat, BOT_PREFIX); + logger.msg(ctx); + + await coletarMidia(msg); + await processarComando(msg, chat, chatId); + await processarJogo(msg, chat); + + logger.done("message_create", `de +${ctx.senderNumber}`); +} \ No newline at end of file diff --git a/src/logger/formatter.js b/src/logger/formatter.js new file mode 100644 index 0000000..8561205 --- /dev/null +++ b/src/logger/formatter.js @@ -0,0 +1,36 @@ +// ── Paleta ANSI ────────────────────────────────────────────── +export const c = { + reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", + green: "\x1b[32m", yellow: "\x1b[33m", cyan: "\x1b[36m", + red: "\x1b[31m", gray: "\x1b[90m", white: "\x1b[37m", + blue: "\x1b[34m", magenta: "\x1b[35m", +}; + +export const SEP = `${c.gray}${"─".repeat(52)}${c.reset}`; + +export const now = () => + new Date().toLocaleString("pt-BR", { dateStyle: "short", timeStyle: "medium" }); + +export const formatType = (type) => ({ + sticker: `${c.magenta}sticker${c.reset}`, + image: `${c.cyan}imagem${c.reset}`, + video: `${c.cyan}vídeo${c.reset}`, + audio: `${c.cyan}áudio${c.reset}`, + ptt: `${c.cyan}áudio${c.reset}`, + document: `${c.cyan}arquivo${c.reset}`, + chat: `${c.white}texto${c.reset}`, +}[type] ?? `${c.gray}${type}${c.reset}`); + +export const formatContext = (chatName, isGroup) => + isGroup + ? `${c.bold}${chatName}${c.reset} ${c.dim}(grupo)${c.reset}` + : `${c.bold}${chatName}${c.reset} ${c.dim}(privado)${c.reset}`; + +export const formatBody = (body, isCommand) => + body?.trim() + ? `${isCommand ? c.yellow : c.green}"${body.length > 200 ? body.slice(0, 200) + "..." : body}"${c.reset}` + : `${c.dim}${c.reset}`; + +export const formatReply = (quotedName, quotedNumber, quotedPreview) => + `\n${c.gray} ↩ Para: ${c.reset}${c.white}${quotedName}${c.reset} ${c.dim}+${quotedNumber}${c.reset}` + + `\n${c.gray} ↩ Msg: ${c.reset}${c.dim}${quotedPreview}${c.reset}`; \ No newline at end of file diff --git a/src/logger/logger.js b/src/logger/logger.js new file mode 100644 index 0000000..8a48a51 --- /dev/null +++ b/src/logger/logger.js @@ -0,0 +1,53 @@ +import { + c, SEP, now, + formatType, formatContext, formatBody, formatReply, +} from "./formatter.js"; + +/** + * Logger central do ManyBot. + * Cada método lida apenas com saída — sem lógica de negócio ou I/O externo. + */ +export const logger = { + info: (...a) => console.log(`${SEP}\n${c.gray}[${now()}]${c.reset} ${c.cyan}INFO ${c.reset}`, ...a), + success: (...a) => console.log(`${c.gray}[${now()}]${c.reset} ${c.green}OK ${c.reset}`, ...a), + warn: (...a) => console.log(`${c.gray}[${now()}]${c.reset} ${c.yellow}WARN ${c.reset}`, ...a), + error: (...a) => console.log(`${c.gray}[${now()}]${c.reset} ${c.red}ERROR ${c.reset}`, ...a), + + /** + * Loga uma mensagem recebida a partir de um contexto já resolvido. + * @param {import("./messageContext.js").MessageContext} ctx + */ + msg(ctx) { + const { chatName, isGroup, senderName, senderNumber, type, body, isCommand, quoted } = ctx; + + const typeLabel = formatType(type); + const context = formatContext(chatName, isGroup); + const bodyText = formatBody(body, isCommand); + const replyLine = quoted + ? formatReply(quoted.name, quoted.number, quoted.preview) + : ""; + + console.log( + `${SEP}\n` + + `${c.gray}[${now()}]${c.reset} ${c.cyan}MSG ${c.reset}${context}\n` + + `${c.gray} De: ${c.reset}${c.white}${senderName}${c.reset} ${c.dim}+${senderNumber}${c.reset}\n` + + `${c.gray} Tipo: ${c.reset}${typeLabel}${isCommand ? ` ${c.yellow}(bot)${c.reset}` : ""}\n` + + `${c.gray} Text: ${c.reset}${bodyText}` + + replyLine + ); + }, + + cmd: (cmd, extra = "") => + console.log( + `${c.gray}[${now()}]${c.reset} ${c.yellow}CMD ${c.reset}` + + `${c.bold}${cmd}${c.reset}` + + (extra ? ` ${c.dim}${extra}${c.reset}` : "") + ), + + done: (cmd, detail = "") => + console.log( + `${c.gray}[${now()}]${c.reset} ${c.green}DONE ${c.reset}` + + `${c.dim}${cmd}${c.reset}` + + (detail ? ` — ${detail}` : "") + ), +}; \ No newline at end of file diff --git a/src/logger/messageContext.js b/src/logger/messageContext.js new file mode 100644 index 0000000..433e15e --- /dev/null +++ b/src/logger/messageContext.js @@ -0,0 +1,83 @@ +import client from "../client/whatsappClient.js"; + +/** + * Extrai o número limpo de uma mensagem. + * @param {import("whatsapp-web.js").Message} msg + * @returns {Promise} + */ +export async function getNumber(msg) { + if (msg.fromMe) return String(msg.from).split("@")[0]; + const contact = await msg.getContact(); + return contact.number; +} + +/** + * Monta o contexto completo de uma mensagem para logging. + * Resolve contato, quoted message e metadados do chat. + * + * @param {import("whatsapp-web.js").Message} msg + * @param {import("whatsapp-web.js").Chat} chat + * @param {string} botPrefix + * @returns {Promise} + * + * @typedef {Object} MessageContext + * @property {string} chatName + * @property {string} chatId + * @property {boolean} isGroup + * @property {string} senderName + * @property {string} senderNumber + * @property {string} type + * @property {string} body + * @property {boolean} isCommand + * @property {{ name: string, number: string, preview: string } | null} quoted + */ +export async function buildMessageContext(msg, chat, botPrefix) { + const chatId = chat.id._serialized; + const isGroup = /@g\.us$/.test(chatId); + const number = await getNumber(msg); + const name = msg._data?.notifyName || String(msg.from).replace(/(:\d+)?@.*$/, ""); + + const quoted = await resolveQuotedMessage(msg); + + return { + chatName: chat.name || chat.id.user, + chatId, + isGroup, + senderName: name, + senderNumber: number, + type: msg?.type || "text", + body: msg.body, + isCommand: !!msg.body?.trimStart().startsWith(botPrefix), + quoted, + }; +} + +/** + * Resolve os dados da mensagem citada, se existir. + * Retorna null em caso de erro ou ausência. + * + * @param {import("whatsapp-web.js").Message} msg + * @returns {Promise<{ name: string, number: string, preview: string } | null>} + */ +async function resolveQuotedMessage(msg) { + if (!msg?.hasQuotedMsg) return null; + + try { + const quoted = await msg.getQuotedMessage(); + const quotedNumber = String(quoted.from).split("@")[0]; + + let quotedName = quotedNumber; + try { + const contact = await client.getContactById(quoted.from); + quotedName = contact?.pushname || contact?.formattedName || quotedNumber; + } catch { /* contato não encontrado — usa o número */ } + + const quotedPreview = quoted.body?.trim() + ? `"${quoted.body.length > 80 ? quoted.body.slice(0, 80) + "…" : quoted.body}"` + : `<${quoted.type}>`; + + return { name: quotedName, number: quotedNumber, preview: quotedPreview }; + } catch { + return null; + } +} \ No newline at end of file diff --git a/src/main.js b/src/main.js index eb651a5..6d99c7a 100644 --- a/src/main.js +++ b/src/main.js @@ -1,127 +1,12 @@ -import client from "./client/whatsappClient.js"; -import { CHATS, BOT_PREFIX } from "./config.js"; // <- importar PREFIX -import { processarComando } from "./commands/index.js"; -import { coletarMidia } from "./commands/figurinha.js"; -import { processarJogo } from "./games/adivinhacao.js"; -import { getChatId } from "./utils/getChatId.js"; +import client from "./client/whatsappClient.js"; +import { handleMessage } from "./handlers/messageHandler.js"; +import { logger } from "./logger/logger.js"; -// ── Cores ──────────────────────────────────────────────────── -const c = { - reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m", - green: "\x1b[32m", yellow: "\x1b[33m", cyan: "\x1b[36m", - red: "\x1b[31m", gray: "\x1b[90m", white: "\x1b[37m", - blue: "\x1b[34m", magenta: "\x1b[35m", -}; - -const now = () => - new Date().toLocaleString("pt-BR", { dateStyle: "short", timeStyle: "medium" }); - -const SEP = `${c.gray}${"─".repeat(52)}${c.reset}`; - -// ── Logger ─────────────────────────────────────────────────── -const logger = { - info: (...a) => console.log(`${SEP}\n${c.gray}[${now()}]${c.reset} ${c.cyan}INFO ${c.reset}`, ...a), - success: (...a) => console.log(`${c.gray}[${now()}]${c.reset} ${c.green}OK ${c.reset}`, ...a), - warn: (...a) => console.log(`${c.gray}[${now()}]${c.reset} ${c.yellow}WARN ${c.reset}`, ...a), - error: (...a) => console.log(`${c.gray}[${now()}]${c.reset} ${c.red}ERROR ${c.reset}`, ...a), - - msg: async (chatName, chatId, from, body, msg = {}) => { - const number = String(from).split("@")[0]; - const isGroup = /@g\.us$/.test(chatId); - let name = msg?.notifyName || number; - - try { - if (typeof client?.getContactById === "function") { - const contact = await client.getContactById(from); - name = contact?.pushname || contact?.formattedName || name; - } - } catch {} - - const type = msg?.type || "text"; - const typeLabel = - type === "sticker" ? `${c.magenta}sticker${c.reset}` : - type === "image" ? `${c.cyan}imagem${c.reset}` : - type === "video" ? `${c.cyan}vídeo${c.reset}` : - type === "audio" ? `${c.cyan}áudio${c.reset}` : - type === "ptt" ? `${c.cyan}áudio${c.reset}` : - type === "document" ? `${c.cyan}arquivo${c.reset}` : - type === "chat" ? `${c.white}texto${c.reset}` : - `${c.gray}${type}${c.reset}`; - - const isCommand = body?.trimStart().startsWith(BOT_PREFIX); - - const context = isGroup - ? `${c.bold}${chatName}${c.reset} ${c.dim}(grupo)${c.reset}` - : `${c.bold}${chatName}${c.reset} ${c.dim}(privado)${c.reset}`; - - const bodyPreview = body?.trim() - ? `${isCommand ? c.yellow : c.green}"${body.length > 200 ? body.slice(0, 200) + "..." : body}"${c.reset}` - : `${c.dim}<${typeLabel}>${c.reset}`; - - // Resolve reply - let replyLine = ""; - if (msg?.hasQuotedMsg) { - try { - const quoted = await msg.getQuotedMessage(); - const quotedNumber = String(quoted.from).split("@")[0]; - let quotedName = quotedNumber; - try { - const quotedContact = await client.getContactById(quoted.from); - quotedName = quotedContact?.pushname || quotedContact?.formattedName || quotedNumber; - } catch {} - const quotedPreview = quoted.body?.trim() - ? `"${quoted.body.length > 80 ? quoted.body.slice(0, 80) + "…" : quoted.body}"` - : `<${quoted.type}>`; - replyLine = - `\n${c.gray} ↩ Para: ${c.reset}${c.white}${quotedName}${c.reset} ${c.dim}+${quotedNumber}${c.reset}` + - `\n${c.gray} ↩ Msg: ${c.reset}${c.dim}${quotedPreview}${c.reset}`; - } catch {} - } - - console.log( - `${SEP}\n` + - `${c.gray}[${now()}]${c.reset} ${c.cyan}MSG ${c.reset}${context}\n` + - `${c.gray} De: ${c.reset}${c.white}${name}${c.reset} ${c.dim}+${number}${c.reset}\n` + - `${c.gray} Tipo: ${c.reset}${typeLabel}${isCommand ? ` ${c.yellow}(bot)${c.reset}` : ""}\n` + - `${c.gray} Text: ${c.reset}${bodyPreview}` + - replyLine - ); - }, - - cmd: (cmd, extra = "") => - console.log( - `${c.gray}[${now()}]${c.reset} ${c.yellow}CMD ${c.reset}` + - `${c.bold}${cmd}${c.reset}` + - (extra ? ` ${c.dim}${extra}${c.reset}` : "") - ), - - done: (cmd, detail = "") => - console.log( - `${c.gray}[${now()}]${c.reset} ${c.green}DONE ${c.reset}` + - `${c.dim}${cmd}${c.reset}` + - (detail ? ` — ${detail}` : "") - ), -}; - -export { logger }; - -// ── Boot ───────────────────────────────────────────────────── logger.info("Iniciando ManyBot..."); -client.on("message_create", async msg => { +client.on("message_create", async (msg) => { try { - const chat = await msg.getChat(); - const chatId = getChatId(chat); - - if (!CHATS.includes(chatId)) return; - - await logger.msg(chat.name || chat.id.user, chatId, msg.from, msg.body, msg); - - await coletarMidia(msg); - await processarComando(msg, chat, chatId); - await processarJogo(msg, chat); - - logger.done("message_create", `de +${String(msg.from).split("@")[0]}`); + await handleMessage(msg); } catch (err) { logger.error( `Falha ao processar — ${err.message}`, diff --git a/src/utils/get_id.js b/src/utils/get_id.js index 7cda038..3da624f 100644 --- a/src/utils/get_id.js +++ b/src/utils/get_id.js @@ -1,54 +1,54 @@ -// get_id.js +/** + * Utilitário CLI para descobrir IDs de chats/grupos. + * Uso: node src/utils/get_id.js grupos|contatos| + */ +import pkg from "whatsapp-web.js"; +import qrcode from "qrcode-terminal"; import { CLIENT_ID } from "../config.js"; +const { Client, LocalAuth } = pkg; + +const arg = process.argv[2]; -const arg = process.argv[2]; // argumento passado no node if (!arg) { - console.log("Use: node get_id.js grupos|contatos|"); - process.exit(0); + console.log("Uso: node get_id.js grupos|contatos|"); + process.exit(0); } -console.log("[PESQUISANDO] Aguarde..."); - -import pkg from 'whatsapp-web.js'; -const { Client, LocalAuth } = pkg; -import qrcode from 'qrcode-terminal'; - const client = new Client({ - authStrategy: new LocalAuth({ clientId: CLIENT_ID }), - puppeteer: { headless: true } + authStrategy: new LocalAuth({ clientId: CLIENT_ID }), + puppeteer: { headless: true }, }); -client.on('qr', qr => { - console.log("[WPP] QR Code gerado. Escaneie apenas uma vez:"); - qrcode.generate(qr, { small: true }); +client.on("qr", (qr) => { + console.log("[QR] Escaneie para autenticar:"); + qrcode.generate(qr, { small: true }); }); -client.on('change_state', async state => { - console.log("[WPP] Conectado"); +client.on("ready", async () => { + console.log("[OK] Conectado. Buscando chats...\n"); - const chats = await client.getChats(); + const chats = await client.getChats(); + const search = arg.toLowerCase(); - let filtered = []; - - if (arg.toLowerCase() === "grupos") { - filtered = chats.filter(c => c.isGroup); - } else if (arg.toLowerCase() === "contatos") { - filtered = chats.filter(c => !c.isGroup); - } else { - const search = arg.toLowerCase(); - filtered = chats.filter(c => (c.name || c.id.user).toLowerCase().includes(search)); - } + const filtered = + search === "grupos" ? chats.filter(c => c.isGroup) : + search === "contatos" ? chats.filter(c => !c.isGroup) : + chats.filter(c => (c.name || c.id.user).toLowerCase().includes(search)); + if (!filtered.length) { + console.log("Nenhum resultado encontrado."); + } else { filtered.forEach(c => { - console.log("================================"); - console.log("NAME:", c.name || c.id.user); - console.log("ID:", c.id._serialized); - console.log("GROUP:", c.isGroup); + console.log("─".repeat(40)); + console.log("Nome: ", c.name || c.id.user); + console.log("ID: ", c.id._serialized); + console.log("Grupo: ", c.isGroup); }); + } - await client.destroy(); - process.exit(0); + await client.destroy(); + process.exit(0); }); client.initialize(); \ No newline at end of file diff --git a/todo.txt b/todo.txt index 7b74601..ad73ac4 100644 --- a/todo.txt +++ b/todo.txt @@ -1,9 +1,3 @@ -Deixar o log mais claro, permanecendo limpo e simples - Possibilidade de tirar fundo de figurinhas -Salvar mensagens num banco de dados - -Mudar a licença para GPLv3 - -Possibilidade de baixar playlists do youtube \ No newline at end of file +Salvar mensagens num banco de dados \ No newline at end of file