diff --git a/package-lock.json b/package-lock.json index 3972688..874d2ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "whatsapp-bot", - "version": "2.2.1", + "version": "2.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "whatsapp-bot", - "version": "2.2.1", + "version": "2.3.0", "dependencies": { "node-addon-api": "^7", "node-gyp": "^12.2.0", diff --git a/src/commands/figurinha.js b/src/commands/figurinha.js index fc75510..16300ed 100644 --- a/src/commands/figurinha.js +++ b/src/commands/figurinha.js @@ -51,7 +51,7 @@ async function convertVideoToGif(inputPath, outputPath, fps = 12) { await execFileAsync(FFMPEG, [ "-i", inputPath, - "-filter_complex", filter, // <-- era -vf, tem que ser -filter_complex pro split funcionar + "-filter_complex", filter, "-loop", "0", "-y", outputPath @@ -63,7 +63,7 @@ async function resizeToSticker(inputPath, outputPath) { await execFileAsync(FFMPEG, [ "-i", inputPath, - "-vf", "scale=512:512:flags=lanczos", // lanczos = melhor qualidade no resize + "-vf", "scale=512:512:flags=lanczos", "-y", outputPath ]); @@ -98,33 +98,40 @@ async function createStickerWithFallback(stickerInputPath, isAnimated) { // ───────────────── Sessão ───────────────── -export function iniciarSessao(chatId, author) { +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(() => { - + const timeout = setTimeout(async () => { stickerSessions.delete(chatId); - client.sendMessage(chatId, botMsg("Sessão de figurinha expirou.")); - + 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 - }); - + 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; + const chatId = chat.id._serialized; // ← volta pra isso const session = stickerSessions.get(chatId); if (!session) return; @@ -158,33 +165,40 @@ export async function coletarMidia(msg) { // ───────────────── 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 de figurinha ativa.")); + return msg.reply(botMsg( + "❌ *Nenhuma sessão ativa.*\n\n" + help + )); } if (session.author !== sender) { - return msg.reply(botMsg("Apenas quem iniciou a sessão pode criar as figurinhas.")); + return msg.reply(botMsg( + "🚫 Só quem digitou `!figurinha` pode usar `!figurinha criar`." + )); } const medias = session.medias; if (!medias.length) { - return msg.reply(botMsg("Nenhuma imagem recebida.")); + 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("Aguarde! Estou criando as suas figurinhas...")); + await msg.reply(botMsg("⏳ Gerando suas figurinhas, aguarde um momento...")); ensureDownloadsDir(); - for (const media of medias) { + for (const media of medias) { try { const ext = media.mimetype.split("/")[1]; const isVideo = media.mimetype.startsWith("video/"); @@ -198,7 +212,6 @@ export async function gerarSticker(msg, chatId) { fs.writeFileSync(inputPath, Buffer.from(media.data, "base64")); - // LOG 1 — arquivo de entrada const inputSize = fs.statSync(inputPath).size; console.log(`[1] mimetype: ${media.mimetype} | isAnimated: ${isAnimated} | inputPath: ${inputPath} | size: ${inputSize} bytes`); @@ -208,7 +221,6 @@ export async function gerarSticker(msg, chatId) { console.log("[2] Convertendo para GIF..."); await convertVideoToGif(inputPath, gifPath, isVideo ? 12 : 24); - // LOG 2 — gif gerado if (fs.existsSync(gifPath)) { console.log(`[2] GIF gerado: ${fs.statSync(gifPath).size} bytes`); } else { @@ -229,12 +241,10 @@ export async function gerarSticker(msg, chatId) { stickerInputPath = resizedPath; } - // LOG 3 — antes de criar o sticker 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); - // LOG 4 — sticker gerado console.log(`[4] Sticker buffer: ${stickerBuffer.length} bytes`); const stickerMedia = new MessageMedia("image/webp", stickerBuffer.toString("base64")); @@ -244,11 +254,17 @@ export async function gerarSticker(msg, chatId) { } catch (err) { console.error("Erro ao gerar sticker:", err); - await msg.reply(botMsg("Erro ao gerar uma das figurinhas.")); + 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 geradas com sucesso!")); + await msg.reply(botMsg( + "✅ *Figurinhas criadas com sucesso!*\n" + + "Salve as que quiser no seu WhatsApp. 😄" + )); stickerSessions.delete(chatId); emptyFolder("downloads"); diff --git a/src/commands/index.js b/src/commands/index.js index 848de27..caa4ee1 100644 --- a/src/commands/index.js +++ b/src/commands/index.js @@ -1,8 +1,9 @@ import { enqueueDownload } from "../download/queue.js"; -import { iniciarSessao, gerarSticker } from "./figurinha.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(); @@ -34,88 +35,93 @@ export async function processarComando(msg, chat, chatId) { switch (cmd) { case "!many": await chat.sendMessage(botMsg( - "Comandos:\n\n" + - "- `!ping`\n" + - "- `!video `\n" + - "- `!audio `\n" + - "- `!figurinha`\n" + - "- `!adivinhação começar|parar`\n" + - "- `!info `" + "*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 "!ping": - await msg.reply(botMsg("pong 🏓")); - log.ok("pong enviado"); - break; - case "!video": - if (!tokens[1]) { log.warn("!video sem link"); return; } - await msg.reply(botMsg("⏳ Baixando vídeo...")); + 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]) { log.warn("!audio sem link"); return; } - await msg.reply(botMsg("⏳ Baixando áudio...")); + 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, chatId); + await gerarSticker(msg, groupId); } else { - if (stickerSessions.has(chatId)) { - return msg.reply(botMsg("Já existe uma sessão ativa.")); + 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(chatId, author); - + iniciarSessao(groupId, author, msg); await msg.reply(botMsg( - `Sessão de figurinha iniciada por @${author.split("@")[0]}. Envie no máximo 10 imagens, quando estiver pronto mande \`!figurinha criar\``, - null, - { mentions: [author] } + `✅ Sessão iniciada por *${name}*!\n\n` + help )); } - break; - + case "!adivinhação": if (!tokens[1]) { - await chat.sendMessage(botMsg("`!adivinhação começar`\n`!adivinhação parar`")); + 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! Tente adivinhar o número de 1 a 100.")); + 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 parado.")); + 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 "!info": - if (!tokens[1]) { - await chat.sendMessage(botMsg("Use:\n`!info `")); - return; - } - processarInfo(tokens[1], chat); - log.ok("info →", tokens[1]); - break; - case "!obrigado": case "!valeu": case "!brigado": - await msg.reply(botMsg("Por nada!")); + await msg.reply(botMsg("😊 Por nada!")); break; case "a": @@ -124,6 +130,9 @@ export async function processarComando(msg, chat, chatId) { } } catch (err) { log.error("Falha em", cmd, "—", err.message); - await chat.sendMessage(botMsg("Erro:\n`" + err.message + "`")); + await chat.sendMessage(botMsg( + "❌ Algo deu errado ao executar esse comando.\n" + + "Tente novamente em instantes." + )); } } \ No newline at end of file diff --git a/src/download/audio.js b/src/download/audio.js index 40c20f8..ac949d4 100644 --- a/src/download/audio.js +++ b/src/download/audio.js @@ -5,20 +5,30 @@ 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]; + const video = await get_video(url, id); + const output = `downloads/${id}.mp3`; - await runCmd(cmd, args); - return output; + 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 => code === 0 ? resolve() : reject(new Error("Processo saiu com código "+code))); + 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/queue.js b/src/download/queue.js index 42c26ae..6e55f15 100644 --- a/src/download/queue.js +++ b/src/download/queue.js @@ -1,42 +1,53 @@ import { get_video } from "./video.js"; import { get_audio } from "./audio.js"; import pkg from "whatsapp-web.js"; -const { MessageMedia } = pkg; import fs from "fs"; import { botMsg } from "../utils/botMsg.js"; import { emptyFolder } from "../utils/file.js"; import client from "../client/whatsappClient.js"; +const { MessageMedia } = pkg; + let downloadQueue = []; let processingQueue = false; export function enqueueDownload(type, url, msg, chatId) { - downloadQueue.push({ type, url, msg, chatId }); - if (!processingQueue) processQueue(); + downloadQueue.push({ type, url, msg, chatId }); + if (!processingQueue) processQueue(); } async function processQueue() { - processingQueue = true; - while (downloadQueue.length) { - const job = downloadQueue.shift(); - try { - let path; - if (job.type === "video") path = await get_video(job.url, job.msg.id._serialized); - else path = await get_audio(job.url, job.msg.id._serialized); + processingQueue = true; - const file = fs.readFileSync(path); - const media = new MessageMedia( - job.type === "video" ? "video/mp4" : "audio/mpeg", - file.toString("base64"), - path.split("/").pop() - ); - await client.sendMessage(job.chatId, media); - fs.unlinkSync(path); - emptyFolder("downloads"); + while (downloadQueue.length) { + const job = downloadQueue.shift(); + const label = job.type === "video" ? "vídeo" : "áudio"; - } catch (err) { - await client.sendMessage(job.chatId, botMsg(`❌ Erro ao baixar ${job.type}\n\`${err.message}\``)); - } + 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." + )); } - processingQueue = false; + } + + processingQueue = false; } \ No newline at end of file diff --git a/src/download/video.js b/src/download/video.js index d598250..9807e27 100644 --- a/src/download/video.js +++ b/src/download/video.js @@ -6,50 +6,54 @@ import os from "os"; const platform = os.platform(); export async function get_video(url, id) { - // garante que a pasta exista - const downloadsDir = path.resolve("downloads"); - fs.mkdirSync(downloadsDir, { recursive: true }); + 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 - ]; + 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); + return await runCmd(cmd, args); } async function runCmd(cmd, args) { - return new Promise((resolve, reject) => { - const proc = spawn(cmd, args); - let stdout = ""; + 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.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("yt-dlp saiu com código " + code)); + 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." + )); + } - // Pega a última linha, que é o caminho final do arquivo - const lines = stdout.trim().split("\n").filter(l => l.trim()); - const filepath = lines[lines.length - 1]; + const lines = stdout.trim().split("\n").filter(l => l.trim()); + const filepath = lines[lines.length - 1]; - if (!fs.existsSync(filepath)) { - return reject(new Error("Arquivo não encontrado: " + filepath)); - } + if (!fs.existsSync(filepath)) { + return reject(new Error( + "O download foi concluído, mas o arquivo não foi encontrado. Tente novamente." + )); + } - resolve(filepath); - }); + resolve(filepath); }); + }); } \ No newline at end of file diff --git a/src/games/adivinhacao.js b/src/games/adivinhacao.js index a645615..82f1b59 100644 --- a/src/games/adivinhacao.js +++ b/src/games/adivinhacao.js @@ -3,27 +3,33 @@ import { botMsg } from "../utils/botMsg.js"; let jogoAtivo = null; export function iniciarJogo() { - jogoAtivo = Math.floor(Math.random()*100)+1; - return jogoAtivo; + jogoAtivo = Math.floor(Math.random() * 100) + 1; + return jogoAtivo; } export function pararJogo() { - jogoAtivo = null; + jogoAtivo = null; } export async function processarJogo(msg, chat) { - if (!jogoAtivo) return; + if (!jogoAtivo) return; - const tentativa = msg.body.trim(); - if (!/^\d+$/.test(tentativa)) return; + const tentativa = msg.body.trim(); + if (!/^\d+$/.test(tentativa)) return; - const num = parseInt(tentativa); - if (num === jogoAtivo) { - await msg.reply(botMsg(`Acertou! Número: ${jogoAtivo}`)); - pararJogo(); - } else if (num > jogoAtivo) { - await chat.sendMessage(botMsg("Menor.")); - } else { - await chat.sendMessage(botMsg("Maior.")); - } + 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/main.js b/src/main.js index fb90d56..eb651a5 100644 --- a/src/main.js +++ b/src/main.js @@ -55,7 +55,7 @@ const logger = { : `${c.bold}${chatName}${c.reset} ${c.dim}(privado)${c.reset}`; const bodyPreview = body?.trim() - ? `${isCommand ? c.yellow : c.green}"${body.length > 80 ? body.slice(0, 80) + "…" : body}"${c.reset}` + ? `${isCommand ? c.yellow : c.green}"${body.length > 200 ? body.slice(0, 200) + "..." : body}"${c.reset}` : `${c.dim}<${typeLabel}>${c.reset}`; // Resolve reply @@ -70,7 +70,7 @@ const logger = { quotedName = quotedContact?.pushname || quotedContact?.formattedName || quotedNumber; } catch {} const quotedPreview = quoted.body?.trim() - ? `"${quoted.body.length > 60 ? quoted.body.slice(0, 60) + "…" : quoted.body}"` + ? `"${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}` + diff --git a/src/utils/get_id.js b/src/utils/get_id.js index 3cfc719..7cda038 100644 --- a/src/utils/get_id.js +++ b/src/utils/get_id.js @@ -1,4 +1,6 @@ // get_id.js +import { CLIENT_ID } from "../config.js"; + const arg = process.argv[2]; // argumento passado no node if (!arg) { @@ -12,8 +14,6 @@ import pkg from 'whatsapp-web.js'; const { Client, LocalAuth } = pkg; import qrcode from 'qrcode-terminal'; -const CLIENT_ID = "bot_permanente"; // sempre o mesmo - const client = new Client({ authStrategy: new LocalAuth({ clientId: CLIENT_ID }), puppeteer: { headless: true } @@ -24,10 +24,10 @@ client.on('qr', qr => { qrcode.generate(qr, { small: true }); }); -client.on('ready', async () => { +client.on('change_state', async state => { console.log("[WPP] Conectado"); - const chats = await client.getChats(); // <- precisa do await + const chats = await client.getChats(); let filtered = []; @@ -40,18 +40,14 @@ client.on('ready', async () => { filtered = chats.filter(c => (c.name || c.id.user).toLowerCase().includes(search)); } - if (filtered.length === 0) { - console.log("Nenhum chat encontrado com esse filtro."); - } else { - console.log(`Encontrados ${filtered.length} chats:`); - filtered.forEach(c => { - console.log("================================"); - console.log("NAME:", c.name || c.id.user); - console.log("ID:", c.id._serialized); - console.log("GROUP:", c.isGroup); - }); - } + filtered.forEach(c => { + console.log("================================"); + console.log("NAME:", c.name || c.id.user); + console.log("ID:", c.id._serialized); + console.log("GROUP:", c.isGroup); + }); + await client.destroy(); process.exit(0); });