37 Commits

Author SHA1 Message Date
synt-xerror
6b7ae6b2c2 new logs, removing sharp as dependency, stickers are resized 2026-03-15 03:31:35 -03:00
synt-xerror
9fc65440d0 [termux] testing support for termux v21 2026-03-14 04:01:59 -03:00
synt-xerror
295f8505e1 [termux] testing support for termux v20 2026-03-14 03:43:03 -03:00
synt-xerror
bc3191fadf [termux] testing support for termux v20 2026-03-14 03:24:20 -03:00
synt-xerror
0582acb6a0 [termux] testing support for termux v19 2026-03-14 03:22:05 -03:00
synt-xerror
2886690d64 [termux] testing support for termux v18 2026-03-14 03:20:00 -03:00
synt-xerror
f3bdf1a41d [termux] testing support for termux v17 2026-03-14 03:09:11 -03:00
synt-xerror
60cca88111 [termux] testing support for termux v16 2026-03-14 02:47:58 -03:00
synt-xerror
f816646b3b [termux] testing support for termux v15 2026-03-14 02:43:29 -03:00
synt-xerror
cc577ec087 [termux] testing support for termux v14 2026-03-14 02:42:38 -03:00
synt-xerror
9ca03ad853 [termux] testing support for termux v13 2026-03-14 02:41:04 -03:00
synt-xerror
3c09738c06 [termux] testing support for termux v12 2026-03-14 02:38:06 -03:00
synt-xerror
490827dc17 [termux] testing support for termux v11 2026-03-14 02:33:13 -03:00
synt-xerror
60768a9ed3 [termux] testing support for termux v10 2026-03-14 02:19:57 -03:00
synt-xerror
97e6b665c7 [termux] testing support for termux v9 2026-03-14 02:12:22 -03:00
synt-xerror
2b757e180e [termux] testing support for termux v8 2026-03-14 02:08:45 -03:00
synt-xerror
f6cb33029c [termux] testing support for termux v7 2026-03-14 02:06:13 -03:00
synt-xerror
806b84eaf5 [termux] testing support for termux v6 2026-03-14 01:58:54 -03:00
synt-xerror
26eef4db57 [termux] testing support for termux v5 2026-03-14 01:52:02 -03:00
synt-xerror
284c673b14 [termux] testing support for termux v4 2026-03-14 01:44:59 -03:00
synt-xerror
5126770ffb [termux] testing support for termux v3 2026-03-14 01:43:20 -03:00
synt-xerror
54bfeb81ae [setup] completing setup 2026-03-14 01:34:31 -03:00
synt-xerror
989d89df83 [termux] testing termux support v2 2026-03-14 01:32:23 -03:00
synt-xerror
5cd27a01c5 [termux] testing support to termux 2026-03-14 01:21:19 -03:00
synt-xerror
8e1b3e482d [fix] pinterest videos can be downloaded without problems 2026-03-13 16:35:11 -03:00
synt-xerror
2b39b616b3 [setup] nodejs -> bash. adding suport to windows. binaries separated. 2026-03-13 16:33:37 -03:00
synt-xerror
928f873cfa [repo] adding /node_modules to .gitignore 2026-03-13 16:31:37 -03:00
synt-xerror
b2fecab660 –
[repo] desacoplamento e maior coesão
2026-03-13 11:30:34 -03:00
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
24 changed files with 4421 additions and 137 deletions

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
env
.wwebjs_auth
.wwebjs_cache
downloads
src/node_modules/
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

44
deploy.sh Executable file
View File

@@ -0,0 +1,44 @@
#!/bin/bash
# development tool
# ./deploy <commit> <branch> <version (if branch=master)>
COMMIT_MSG="$1"
BRANCH="$2"
VERSION="$3"
if [ -z "$COMMIT_MSG" ] || [ -z "$BRANCH" ]; then
echo "Uso: ./deploy <commit> <branch> [version if branch=master]"
exit 1
fi
echo "Rewriting config.js"
cat > "src/config.js" << 'EOF'
export const CLIENT_ID = "bot_permanente";
export const BOT_PREFIX = "🤖 *ManyBot:* ";
export const CHATS = [
// coloque os chats que quer aqui
];
EOF
# mudar para a branch
git checkout $BRANCH || { echo "Error ao change to $BRANCH"; exit 1; }
# adicionar alterações e commit
git add .
git commit -m "$COMMIT_MSG"
# push
git push origin $BRANCH
# se for master, atualizar versão
if [ "$BRANCH" == "master" ] && [ -n "$VERSION" ]; then
echo "Updating version to $VERSION"
git tag $VERSION
npm version $VERSION --no-git-tag-version
git add package.json
git commit -m "Bump version to $VERSION"
git push origin $VERSION
fi
echo "Deploy completed."

BIN
logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

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)

3236
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

13
package.json Executable file
View File

@@ -0,0 +1,13 @@
{
"name": "whatsapp-bot",
"version": "2.0.0",
"type": "module",
"dependencies": {
"node-addon-api": "^7",
"node-gyp": "^12.2.0",
"node-webpmux": "^3.2.1",
"qrcode-terminal": "^0.12.0",
"wa-sticker-formatter": "^4.4.4",
"whatsapp-web.js": "^1.24.0"
}
}

204
setup Executable file
View File

@@ -0,0 +1,204 @@
#!/bin/bash
set -e
# ------------------------
# Cores
# ------------------------
RESET="\033[0m"
BOLD="\033[1m"
RED="\033[31m"
GREEN="\033[32m"
YELLOW="\033[33m"
BLUE="\033[34m"
CYAN="\033[36m"
MAGENTA="\033[35m"
GRAY="\033[90m"
timestamp() {
date +"%H:%M:%S"
}
log() {
local level="$1"
local color="$2"
shift 2
echo -e "${GRAY}[$(timestamp)]${RESET} ${color}${level}${RESET} $*"
}
log_info() { log "[INFO]" "$BLUE" "$@"; }
log_ok() { log "[OK]" "$GREEN" "$@"; }
log_warn() { log "[WARN]" "$YELLOW" "$@"; }
log_error() { log "[ERROR]" "$RED" "$@"; }
log_cmd() { log "[CMD]" "$CYAN" "${BOLD}$*${RESET}"; }
log_debug() { log "[DBG]" "$GRAY" "$@"; }
# ------------------------
# Banner
# ------------------------
print_banner() {
echo -e "${MAGENTA}${BOLD}"
cat << "EOF"
_____ _____ _
| |___ ___ _ _| __ |___| |_
| | | | .'| | | | __ -| . | _|
|_|_|_|__,|_|_|_ |_____|___|_|
|___|
website: www.mlplovers.com.br/manybot
repo: github.com/synt-xerror/manybot
A Amizade é Mágica!
EOF
echo -e "${RESET}"
}
print_banner
log_info "Inicializando setup..."
# ------------------------
# Executar comandos
# ------------------------
run_cmd() {
log_cmd "$*"
"$@"
log_ok "Comando finalizado: $1"
}
# ------------------------
# Download
# ------------------------
download_file() {
local url="$1"
local dest="$2"
log_debug "download_file(url=$url, dest=$dest)"
if [[ -f "$dest" ]]; then
log_warn "Arquivo já existe: $dest"
return
fi
log_info "Baixando $url"
log_debug "Destino: $dest"
if command -v curl >/dev/null 2>&1; then
log_debug "Downloader: curl"
curl -L "$url" -o "$dest"
elif command -v wget >/dev/null 2>&1; then
log_debug "Downloader: wget"
wget "$url" -O "$dest"
else
log_error "curl ou wget não encontrados"
exit 1
fi
chmod +x "$dest" 2>/dev/null || true
log_ok "Arquivo pronto: $dest"
}
# ------------------------
# Detectar plataforma
# ------------------------
log_info "Detectando plataforma"
UNAME="$(uname -s)"
ARCH="$(uname -m)"
PLATFORM=""
case "$UNAME" in
Linux*) PLATFORM="linux";;
Darwin*) PLATFORM="mac";;
MINGW*|MSYS*|CYGWIN*) PLATFORM="win";;
*) PLATFORM="unknown";;
esac
log_info "Sistema: $UNAME"
log_info "Arquitetura: $ARCH"
log_info "Plataforma: $PLATFORM"
# ------------------------
# Informações do ambiente
# ------------------------
log_info "Verificando ambiente"
log_debug "Node: $(node -v 2>/dev/null || echo 'não encontrado')"
log_debug "npm: $(npm -v 2>/dev/null || echo 'não encontrado')"
log_debug "PREFIX: ${PREFIX:-<vazio>}"
# ------------------------
# Termux
# ------------------------
install_deps() {
local packages=("$@")
for pkg in "${packages[@]}"; do
if ! dpkg -s "$pkg" >/dev/null 2>&1; then
log_warn "$pkg não encontrado, instalando"
run_cmd pkg install -y "$pkg"
else
log_ok "$pkg já instalado"
fi
done
}
if [[ "$PREFIX" == *"com.termux"* ]]; then
log_info "Ambiente Termux detectado"
deps=(
chromium
)
install_deps "${deps[@]}"
fi
# ------------------------
# Setup npm
# ------------------------
log_info "Instalando dependências npm"
export PUPPETEER_SKIP_DOWNLOAD=1
run_cmd npm install
# ------------------------
# Diretórios
# ------------------------
log_info "Preparando diretórios"
mkdir -p bin
log_debug "Diretório bin garantido"
# ------------------------
# Arquivos por plataforma
# ------------------------
log_info "Selecionando dependências binárias"
files=()
if [[ "$PLATFORM" == "win" ]]; then
log_debug "Usando binários Windows"
files=(
"https://github.com/synt-xerror/manybot/releases/download/dependencies/yt-dlp.exe bin/yt-dlp.exe"
"https://github.com/synt-xerror/manybot/releases/download/dependencies/ffmpeg.exe bin/ffmpeg.exe"
)
else
log_debug "Usando binários Unix"
files=(
"https://github.com/synt-xerror/manybot/releases/download/dependencies/yt-dlp bin/yt-dlp"
"https://github.com/synt-xerror/manybot/releases/download/dependencies/ffmpeg bin/ffmpeg"
)
fi
log_debug "Total de arquivos para baixar: ${#files[@]}"
# ------------------------
# Download
# ------------------------
for file in "${files[@]}"; do
url="${file%% *}"
dest="${file##* }"
log_info "Processando dependência"
download_file "$url" "$dest"
done
log_ok "Setup concluído com sucesso"

View File

@@ -0,0 +1,102 @@
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";
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`
);
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 ──────────────────────────────────────────────────
export const client = new Client({
authStrategy: new LocalAuth({ clientId: CLIENT_ID }),
puppeteer: { headless: true, ...puppeteerConfig },
});
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 });
}
});
client.on("ready", () => {
exec("clear");
printBanner();
logger.success(`${c.green}${c.bold}WhatsApp conectado e pronto!${c.reset}`);
logger.info(`Client ID: ${c.cyan}${CLIENT_ID}${c.reset}`);
});
client.on("disconnected", reason => {
logger.warn(`Desconectado — motivo: ${c.yellow}${reason}${c.reset}`);
logger.info(`Reconectando em ${c.cyan}5s${c.reset}...`);
setTimeout(() => {
logger.bot("Reinicializando cliente...");
client.initialize();
}, 5000);
});
export default client;

256
src/commands/figurinha.js Normal file
View File

@@ -0,0 +1,256 @@
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, // <-- era -vf, tem que ser -filter_complex pro split funcionar
"-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", // lanczos = melhor qualidade no resize
"-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 function iniciarSessao(chatId, author) {
if (stickerSessions.has(chatId)) return false;
const timeout = setTimeout(() => {
stickerSessions.delete(chatId);
client.sendMessage(chatId, botMsg("Sessão de figurinha expirou."));
}, 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);
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) {
const sender = msg.author || msg.from;
const session = stickerSessions.get(chatId);
if (!session) {
return msg.reply(botMsg("Nenhuma sessão de figurinha ativa."));
}
if (session.author !== sender) {
return msg.reply(botMsg("Apenas quem iniciou a sessão pode criar as figurinhas."));
}
const medias = session.medias;
if (!medias.length) {
return msg.reply(botMsg("Nenhuma imagem recebida."));
}
clearTimeout(session.timeout);
console.log("midias:", medias.length);
await msg.reply(botMsg("Aguarde! Estou criando as suas figurinhas..."));
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"));
// LOG 1 — arquivo de entrada
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);
// LOG 2 — gif gerado
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;
}
// 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"));
await client.sendMessage(chatId, stickerMedia, { sendMediaAsSticker: true });
cleanupFiles(inputPath, gifPath, resizedPath);
} catch (err) {
console.error("Erro ao gerar sticker:", err);
await msg.reply(botMsg("Erro ao gerar uma das figurinhas."));
}
}
await msg.reply(botMsg("Figurinhas geradas com sucesso!"));
stickerSessions.delete(chatId);
emptyFolder("downloads");
}

129
src/commands/index.js Normal file
View File

@@ -0,0 +1,129 @@
import { enqueueDownload } from "../download/queue.js";
import { iniciarSessao, gerarSticker } from "./figurinha.js";
import { botMsg } from "../utils/botMsg.js";
import { iniciarJogo, pararJogo } from "../games/adivinhacao.js";
import { processarInfo } from "./info.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),
};
export async function processarComando(msg, chat, chatId) {
const tokens = msg.body.trim().split(/\s+/);
const cmd = tokens[0]?.toLowerCase();
if (!cmd?.startsWith("!") && cmd !== "a") return;
log.cmd(cmd);
try {
switch (cmd) {
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 🏓"));
log.ok("pong enviado");
break;
case "!video":
if (!tokens[1]) { log.warn("!video sem link"); return; }
await msg.reply(botMsg("⏳ Baixando vídeo..."));
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..."));
enqueueDownload("audio", tokens[1], msg, chatId);
log.ok("áudio enfileirado →", tokens[1]);
break;
case "!figurinha":
const author = msg.author || msg.from;
if (tokens[1] === "criar") {
await gerarSticker(msg, chatId);
} else {
if (stickerSessions.has(chatId)) {
return msg.reply("Já existe uma sessão ativa.");
}
iniciarSessao(chatId, author);
await msg.reply(
`Sessão de figurinha iniciada por @${author.split("@")[0]}. Envie no máximo 10 imagens, quando estiver pronto mande \`!figurinha criar\``,
null,
{ mentions: [author] }
);
}
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") {
iniciarJogo();
await chat.sendMessage(botMsg("Jogo iniciado! Tente adivinhar o número de 1 a 100."));
log.ok("jogo iniciado");
} else if (tokens[1] === "parar") {
pararJogo();
await chat.sendMessage(botMsg("Jogo parado."));
log.ok("jogo parado");
} else {
log.warn("!adivinhação — subcomando desconhecido:", tokens[1]);
}
break;
case "!info":
if (!tokens[1]) {
await chat.sendMessage(botMsg("Use:\n`!info <comando>`"));
return;
}
processarInfo(tokens[1], chat);
log.ok("info →", 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;
}
} catch (err) {
log.error("Falha em", cmd, "—", err.message);
await chat.sendMessage(botMsg("Erro:\n`" + err.message + "`"));
}
}

20
src/commands/info.js Normal file
View File

@@ -0,0 +1,20 @@
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 <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.`));
}
}

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
];

24
src/download/audio.js Normal file
View File

@@ -0,0 +1,24 @@
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 => code === 0 ? resolve() : reject(new Error("Processo saiu com código "+code)));
});
}

42
src/download/queue.js Normal file
View File

@@ -0,0 +1,42 @@
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";
let downloadQueue = [];
let processingQueue = false;
export 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);
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");
} catch (err) {
await client.sendMessage(job.chatId, botMsg(`❌ Erro ao baixar ${job.type}\n\`${err.message}\``));
}
}
processingQueue = false;
}

55
src/download/video.js Normal file
View File

@@ -0,0 +1,55 @@
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) {
// garante que a pasta exista
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("yt-dlp saiu com código " + code));
// 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];
if (!fs.existsSync(filepath)) {
return reject(new Error("Arquivo não encontrado: " + filepath));
}
resolve(filepath);
});
});
}

29
src/games/adivinhacao.js Normal file
View File

@@ -0,0 +1,29 @@
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! Número: ${jogoAtivo}`));
pararJogo();
} else if (num > jogoAtivo) {
await chat.sendMessage(botMsg("Menor."));
} else {
await chat.sendMessage(botMsg("Maior."));
}
}

134
src/main.js Normal file
View File

@@ -0,0 +1,134 @@
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";
// ── 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 > 80 ? body.slice(0, 80) + "…" : 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 > 60 ? quoted.body.slice(0, 60) + "…" : 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 => {
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]}`);
} catch (err) {
logger.error(
`Falha ao processar — ${err.message}`,
`\n Stack: ${err.stack?.split("\n")[1]?.trim() ?? ""}`
);
}
});
client.initialize();
logger.info("Cliente inicializado. Aguardando conexão com WhatsApp...");

5
src/utils/botMsg.js Normal file
View File

@@ -0,0 +1,5 @@
import { BOT_PREFIX } from "../config.js";
export function botMsg(texto) {
return `${BOT_PREFIX}\n${texto}`;
}

9
src/utils/file.js Normal file
View File

@@ -0,0 +1,9 @@
import fs from "fs";
import path from "path";
export function emptyFolder(folder) {
fs.readdirSync(folder).forEach(file => {
const filePath = path.join(folder, file);
if (fs.lstatSync(filePath).isFile()) fs.unlinkSync(filePath);
});
}

3
src/utils/getChatId.js Normal file
View File

@@ -0,0 +1,3 @@
export function getChatId(chat) {
return chat.id._serialized;
}

58
src/utils/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();

9
todo.txt Normal file
View File

@@ -0,0 +1,9 @@
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