9 Commits
2.4.3 ... 2.0.0

Author SHA1 Message Date
synt-xerror
a48b747b8e Bump version to 2.0.0 2026-03-13 11:22:15 -03:00
synt-xerror
d71da321d8 [repo] desacoplamento e maior coesão 2026-03-13 11:22:14 -03:00
synt-xerror
3d4c6da746 corrigindo download de videos e audios 2026-03-13 09:35:48 -03:00
synt-xerror
30e40cb3e1 teste pra 2.0 2026-03-13 06:59:12 -03:00
synt-xerror
2340deab4e a 2026-03-12 11:32:07 -03:00
synt-xerror
f9eb14e5e3 removing node_modules 2026-03-12 07:52:29 -03:00
synt-xerror
3780936e01 yt-dlp support 2026-03-12 01:22:29 -03:00
synt-xerror
2040382842 Merge branch 'main' 2026-03-11 20:10:28 -03:00
SyntaxError!
46d9c424b7 Initial commit 2026-03-11 20:00:13 -03:00
12 changed files with 3189 additions and 137 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
env.js
.wwebjs_auth
.wwebjs_cache
downloads
node_modules/
cookies.txt
bin/

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 SyntaxError!
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

15
README.md Normal file
View File

@@ -0,0 +1,15 @@
![ManyBot Logo](logo.png)
Criei esse bot para servir um grupo de amigos. Meu foco não é fazer ele funcionar para todo mundo.
Ele é 100% local e gratuito, sem necessidade de APIs burocraticas. Usufrui da biblioteca `whatsapp-web.js`, que permite bastante coisa mesmo sem a API oficial.
Você consegue totalmente clonar esse repoistório e rodar seu próprio ManyBot. A licenca MIT permite que você modifique o que quiser e faça seu próprio bot.
Algumas funcionalidades desse bot inclui:
- Funciona em multiplos chats em apenas uma única sessão
- Comandos de jogos e download com yt-dlp
- Gerador de figurinhas
- Ferramenta para pegar IDs dos chats
- Entre outros

4
deploy.sh Normal file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
TAG=$(git describe --tags --abbrev=0)
npm version $TAG --no-git-tag-version

58
get_id.js Normal file
View File

@@ -0,0 +1,58 @@
// get_id.js
const arg = process.argv[2]; // argumento passado no node
if (!arg) {
console.log("Use: node get_id.js grupos|contatos|<nome>");
process.exit(0);
}
console.log("[PESQUISANDO] Aguarde...");
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 }
});
client.on('qr', qr => {
console.log("[WPP] QR Code gerado. Escaneie apenas uma vez:");
qrcode.generate(qr, { small: true });
});
client.on('ready', async () => {
console.log("[WPP] Conectado");
const chats = await client.getChats(); // <- precisa do await
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));
}
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);
});
}
process.exit(0);
});
client.initialize();

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

380
main.js Normal file
View File

@@ -0,0 +1,380 @@
console.log("[CARREGANDO] Aguarde...");
import pkg from 'whatsapp-web.js';
const { Client, LocalAuth, MessageMedia } = pkg;
import qrcode from 'qrcode-terminal';
import fs from 'fs';
import { exec, spawn } from 'child_process';
import sharp from 'sharp';
import os from 'os';
import path from "path";
console.log(os.platform());
import { CHATS } from "./env.js";
const CLIENT_ID = "bot_permanente";
const BOT_PREFIX = "🤖 *ManyBot:* ";
let jogoAtivo = null;
if (!fs.existsSync("downloads")) fs.mkdirSync("downloads");
const client = new Client({
authStrategy: new LocalAuth({ clientId: CLIENT_ID }),
puppeteer: { headless: true }
});
function botMsg(texto) {
return `${BOT_PREFIX}\n${texto}`;
}
function getChatId(chat) {
return chat.id._serialized;
}
// ---------------- Eventos ----------------
client.on("qr", qr => {
console.log("[BOT] Escaneie o QR Code");
qrcode.generate(qr, { small: true });
});
client.on("ready", () => {
exec("clear");
console.log("[BOT] WhatsApp conectado.");
});
client.on("disconnected", reason => {
console.warn("[BOT] Reconectando:", reason);
setTimeout(() => client.initialize(), 5000);
});
client.on("message_create", async msg => {
try {
const chat = await msg.getChat();
const chatId = getChatId(chat);
if (!CHATS.includes(chatId)) return;
console.log("==================================");
console.log(`CHAT NAME : ${chat.name || chat.id.user}`);
console.log(`CHAT ID : ${chatId}`);
console.log(`FROM : ${msg.from}`);
console.log(`BODY : ${msg.body}`);
console.log("==================================\n");
await processarComando(msg, chat, chatId);
await processarJogo(msg, chat);
} catch (err) {
console.error("[ERRO]", err);
}
});
// ---------------- Download ----------------
let downloadQueue = [];
let processingQueue = false;
function run(cmd, args) {
return new Promise((resolve, reject) => {
const proc = spawn(cmd, args);
let stdout = "";
proc.stdout.on("data", data => {
const text = data.toString();
stdout += text;
console.log("[cmd]", text);
});
proc.stderr.on("data", data => {
console.error("[cmd ERR]", data.toString());
});
proc.on("close", code => {
if (code !== 0) reject(new Error("Processo saiu com código " + code));
else resolve(stdout);
});
});
}
const so = os.platform();
async function get_video(url, id) {
let cmd = "";
const cmd_lin = "./bin/yt-dlp";
const cmd_win = ".\\bin\\yt-dlp.exe";
switch (so) {
case "win32":
cmd = cmd_win;
break;
case "linux":
cmd = cmd_lin;
break;
}
const args = [
'--extractor-args', 'youtube:player_client=android', // mantém seu client preferido
'-f', 'bv+ba/best', // tentar vídeo+áudio; fallback para best
'--print', 'after_move:filepath',
'--output', `downloads/${id}.%(ext)s`,
// autenticacao (use cookies exportado com yt-dlp ou --cookies-from-browser)
'--cookies', 'cookies.txt', // arquivo de cookies (gerar antes)
// cabeçalhos para reduzir 403
'--add-header', 'User-Agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
'--add-header', 'Referer:https://www.youtube.com/',
// tolerância de rede
'--retries', '4',
'--fragment-retries', '5',
'--socket-timeout', '15',
'--sleep-interval', '1', '--max-sleep-interval', '4',
// permitir formatos que podem ser marcados como "unplayable" (útil em alguns casos)
'--allow-unplayable-formats',
// testes / debug: use -v quando precisar diagnosticar
// url
url
];
const stdout = await run(cmd, args);
const filepath = stdout.trim();
if (!filepath) throw new Error("yt-dlp não retornou caminho");
return filepath;
}
async function get_audio(url, id) {
const video = await get_video(url, id);
const output = `downloads/${id}.mp3`;
let cmd = "";
const cmd_lin = "./bin/ffmpeg";
const cmd_win = ".\\bin\\ffmpeg.exe";
switch (so) {
case "win32":
cmd = cmd_win;
break;
case "linux":
cmd = cmd_lin;
break;
}
const args = [
'-i', video,
'-vn', '-acodec',
'libmp3lame', '-q:a', '2',
output
];
await run(cmd, args);
return output;
}
function empty_folder(folder) {
fs.readdirSync(folder).forEach(file => {
const filePath = path.join(folder, file);
if (fs.lstatSync(filePath).isFile()) {
fs.unlinkSync(filePath);
}
});
}
async function enviarVideo(chatId, path) {
const file = fs.readFileSync(path);
const media = new MessageMedia(
"video/mp4",
file.toString("base64"),
path.split("/").pop()
);
await client.sendMessage(chatId, media);
fs.unlinkSync(path);
empty_folder("./downloads");
}
async function enviarAudio(chatId, path) {
const file = fs.readFileSync(path);
const media = new MessageMedia(
"audio/mpeg",
file.toString("base64"),
path.split("/").pop()
);
await client.sendMessage(chatId, media);
fs.unlinkSync(path);
empty_folder("./downloads");
}
function enqueueDownload(type, url, msg, chatId) {
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);
if (job.type === "video")
await enviarVideo(job.chatId, path);
else
await enviarAudio(job.chatId, path);
} catch (err) {
await client.sendMessage(
job.chatId,
botMsg(`❌ Erro ao baixar ${job.type}\n\`${err.message}\``)
);
}
}
processingQueue = false;
}
// ---------------- Figurinha ----------------
async function gerarSticker(msg) {
if (!msg.hasMedia) {
await msg.reply(botMsg("Envie uma imagem junto com o comando: `!figurinha`."));
return;
}
const media = await msg.downloadMedia();
const ext = media.mimetype.split("/")[1];
const input = `downloads/${msg.id._serialized}.${ext}`;
const output = `downloads/${msg.id._serialized}.webp`;
fs.writeFileSync(input, Buffer.from(media.data, "base64"));
await sharp(input)
.resize(512, 512, { fit: "contain", background: { r:0,g:0,b:0,alpha:0 } })
.webp()
.toFile(output);
const data = fs.readFileSync(output);
const sticker = new MessageMedia("image/webp", data.toString("base64"), "sticker.webp");
const chat = await msg.getChat();
await client.sendMessage(chat.id._serialized, sticker, { sendMediaAsSticker: true });
fs.unlinkSync(input);
fs.unlinkSync(output);
}
// ---------------- Comandos ----------------
async function processarComando(msg, chat, chatId) {
const tokens = msg.body.trim().split(/\s+/);
try {
switch(tokens[0]) {
case "!many":
await chat.sendMessage(botMsg(
"Comandos:\n\n"+
"`!ping`\n"+
"`!video <link>`\n"+
"`!audio <link>`\n"+
"`!figurinha`\n"+
"`!adivinhação começar|parar`\n"+
"`!info <comando>`"
));
break;
case "!ping":
await msg.reply(botMsg("pong 🏓"));
break;
case "!video":
if (!tokens[1]) return;
await msg.reply(botMsg("⏳ Baixando vídeo..."));
enqueueDownload("video", tokens[1], msg, chatId);
break;
case "!audio":
if (!tokens[1]) return;
await msg.reply(botMsg("⏳ Baixando áudio..."));
enqueueDownload("audio", tokens[1], msg, chatId);
break;
case "!figurinha":
await gerarSticker(msg);
break;
case "!adivinhação":
if (!tokens[1]) {
await chat.sendMessage(botMsg("`!adivinhação começar`\n`!adivinhação parar`"));
return;
}
if (tokens[1] === "começar") {
jogoAtivo = Math.floor(Math.random()*100)+1;
await chat.sendMessage(botMsg("Adivinhe o número de 1 a 100!"));
}
if (tokens[1] === "parar") {
jogoAtivo = null;
await chat.sendMessage(botMsg("Jogo encerrado."));
}
break;
case "A":
case "a":
if (!tokens[1]) await msg.reply(botMsg("B!"));
break;
case "!info":
if (!tokens[1]) {
await chat.sendMessage(botMsg("Use:\n`!info <comando>`"));
return;
}
switch(tokens[1]) {
case "ping":
await chat.sendMessage(botMsg("> `!ping`\nResponde pong."));
break;
case "video":
await chat.sendMessage(botMsg("> `!video <link>`\nBaixa vídeo da internet."));
break;
case "audio":
await chat.sendMessage(botMsg("> `!audio <link>`\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.`));
}
break;
}
} catch(err) {
console.error(err);
await chat.sendMessage(botMsg("Erro:\n`"+err.message+"`"));
}
}
// ---------------- Jogo ----------------
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! Número: ${jogoAtivo}`));
jogoAtivo = null;
} else if (num > jogoAtivo) {
await chat.sendMessage(botMsg("Menor."));
} else {
await chat.sendMessage(botMsg("Maior."));
}
}
client.initialize();

137
main.py
View File

@@ -1,137 +0,0 @@
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.keys import Keys
from webdriver_manager.chrome import ChromeDriverManager
import time
import pyperclip
import random
GRUPO = "𝑩𝒂𝒕𝒆 𝒑𝒂𝒑𝒐"
BOT_PREFIX = "🤖 *ManyBot:* "
PROFILE_DIR = "/home/syntax/whatsapp-profile"
CHECK_INTERVAL = 0.5
def iniciar_driver():
print("[DRIVER] Iniciando Chrome...")
opts = Options()
opts.add_argument(f"--user-data-dir={PROFILE_DIR}")
opts.add_argument("--profile-directory=Default")
opts.add_argument("--no-sandbox")
opts.add_argument("--disable-dev-shm-usage")
opts.add_argument("--disable-extensions")
opts.add_argument("--disable-gpu")
driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=opts)
driver.get("https://web.whatsapp.com")
wait = WebDriverWait(driver, 120)
print("[DRIVER] Aguardando QR Code ou login...")
wait.until(EC.presence_of_element_located((By.ID, "pane-side")))
print("[DRIVER] WhatsApp Web carregado.")
return driver, wait
def abrir_grupo(driver, wait):
print(f"[GRUPO] Procurando '{GRUPO}'...")
grupo_box = wait.until(EC.presence_of_element_located((By.XPATH, f'//span[@title="{GRUPO}"]')))
grupo_box.click()
print(f"[GRUPO] Aberto.")
def pegar_ultima_mensagem(driver):
msgs = driver.find_elements(By.CSS_SELECTOR, "[data-testid='selectable-text'] span")
if not msgs:
return None
return msgs[-1].text
def enviar_mensagem(driver, wait, texto):
print(f"[ENVIO] Enviando: '{texto}'")
caixa = wait.until(EC.element_to_be_clickable((
By.CSS_SELECTOR, "footer div[contenteditable='true'][role='textbox']"
)))
caixa.click()
pyperclip.copy(texto)
caixa.send_keys(Keys.CONTROL, 'v')
time.sleep(0.3)
caixa.send_keys(Keys.ENTER)
print("[ENVIO] Mensagem enviada.")
def bot_msg(texto):
return f"{BOT_PREFIX}\n{texto}"
# -----------------------------
driver, wait = iniciar_driver()
abrir_grupo(driver, wait)
ultima_mensagem = None
def jogo():
n = random.randint(1, 100)
print(f"[JOGO] Jogo iniciado! Número escolhido: {n}")
enviar_mensagem(driver, wait, bot_msg("Hora do jogo! Tentem adivinhar qual número de 1 a 100 eu estou pensando!"))
while True:
try:
tentativa = pegar_ultima_mensagem(driver)
if not tentativa or tentativa == ultima_mensagem:
time.sleep(CHECK_INTERVAL)
continue
print(f"[JOGO] Nova tentativa: '{tentativa}'")
time.sleep(CHECK_INTERVAL)
if tentativa.isdigit():
num = int(tentativa)
if num == n:
enviar_mensagem(driver, wait, bot_msg(f"Parabéns! Você acertou!! O número era: {n}"))
break
elif num > n:
enviar_mensagem(driver, wait, bot_msg(f"Quase! Um pouco menor. Sua resposta: {num}"))
elif num < n:
enviar_mensagem(driver, wait, bot_msg(f"Quase! Um pouco maior. Sua resposta: {num}"))
except Exception as e:
print(f"[ERRO] {type(e).__name__}: {e}")
time.sleep(1)
def processar_comando(texto):
tokens = texto.split()
if tokens[0] == "!many":
if len(tokens) == 1: # se só tiver "!many"
return bot_msg(
"E aí?! Aqui está a lista de todos os meus comandos:\n"
"- `!many ping` -> testa se estou funcionando\n"
"- `!many jogo` -> jogo de adivinhação\n"
"E ai, vai querer qual? 😄"
)
elif tokens[1] == "ping":
return bot_msg("pong 🏓")
elif tokens[1] == "jogo":
jogo()
return None
while True:
try:
texto = pegar_ultima_mensagem(driver)
if not texto or texto == ultima_mensagem:
time.sleep(CHECK_INTERVAL)
continue
print(f"[MSG] Nova mensagem: '{texto}'")
ultima_mensagem = texto
time.sleep(CHECK_INTERVAL)
resposta = processar_comando(texto)
if resposta:
enviar_mensagem(driver, wait, resposta)
except Exception as e:
print(f"[ERRO] {type(e).__name__}: {e}")
time.sleep(1)

2682
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

10
package.json Executable file
View File

@@ -0,0 +1,10 @@
{
"name": "whatsapp-bot",
"version": "2.0.0",
"type": "module",
"dependencies": {
"qrcode-terminal": "^0.12.0",
"sharp": "^0.34.5",
"whatsapp-web.js": "^1.24.0"
}
}

5
src/config.js Normal file
View File

@@ -0,0 +1,5 @@
export const CLIENT_ID = "bot_permanente";
export const BOT_PREFIX = "🤖 *ManyBot:* ";
export const CHATS = [
// coloque os chats que quer aqui
];

7
teste.js Normal file
View File

@@ -0,0 +1,7 @@
const str = "<22>»⃟⃫⚡<EFB88F><E283A5><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>";
// percorre cada caractere
for (const char of str) {
const code = char.codePointAt(0); // pega o ponto de código Unicode
process.stdout.write(`${char} (U+${code.toString(16).toUpperCase()})\n`);
}