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