diff --git a/README.md b/README.md
index 08f9519..c1d2668 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,18 @@

-Criei esse bot para servir um grupo de amigos. Meu foco não é fazer ele funcionar para todo mundo.
+ManyBot é um bot para WhatsApp que roda 100% localmente, sem depender da API oficial do WhatsApp. Ele utiliza a biblioteca `whatsapp-web.js`, que automatiza o WhatsApp Web sem depender de gráficos (headless).
-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.
+Algumas funcionalidades desse bot incluem:
+- Suporte a múltiplos chats em uma única sessão
+- Sistema de plugins — adicione, remova ou crie funcionalidades sem mexer no núcleo do bot
-Você consegue totalmente clonar esse repoistório e rodar seu próprio ManyBot. A licenca GPLv3 permite que você modifique o que quiser e faça seu próprio bot, mas se for publicar, deve ser open source assim como o ManyBot original.
+# Exemplos
-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
+
+
+
+
+
---
@@ -25,32 +26,283 @@ obs: Sistemas Android e iOS ainda não são 100% compatíveis. O suporte para Te
# Instalação (Linux)
1. Clone o repositório e entre:
-```
+```bash
git clone https://github.com/synt-xerror/manybot
cd manybot
```
-2. Execute o script de instalação:
-```
-bash setup
+2. Crie e abra o arquivo de configuração (use o editor de sua preferência):
+```bash
+touch manybot.conf
+nano manybot.conf
```
-3. Rode o bot pela primeira vez:
+3. Nele você pode configurar algumas coisas do ManyBot. Esse é o arquivo base para que possa modificar:
+```bash
+# Comentários com '#'
+
+CLIENT_ID=bot_permanente
+CMD_PREFIX=!
+CHATS=[
+ 123456789@c.us,
+ 123456789@g.us
+]
+PLUGINS=[
+ video,
+ audio,
+ figurinha,
+ adivinhacao
+]
```
-node src/main.js
+- **CLIENT_ID:** ID do cliente, serve para identificar sua sessão.
+ - Valor padrão: `bot_permanente`
+- **CMD_PREFIX:** Prefixo do comando, o caractere que você usa para executar um comando (!many, !figurinha).
+ - Valor padrão: `!`
+- **CHATS:** ID dos chats no qual você quer que o bot assista. Use o utilitário: `src/utils/get_id.js` para descobrir os IDs. Deixe vazio caso queira que funcione com qualquer chat.
+ - Valor padrão: (nenhum)
+- **PLUGINS:** Lista de plugins ativos. Cada nome corresponde a uma pasta dentro de `src/plugins/`. Remova ou comente uma linha para desativar o plugin sem apagá-lo.
+ - Valor padrão: (nenhum)
+
+obs: o utilitário `src/utils/get_id.js` usa um CLIENT_ID separado para que não entre em conflito com a sessão principal do ManyBot. Você terá que escanear o QR Code novamente para executá-lo.
+
+4. Execute o script de instalação:
+```bash
+bash ./setup
+```
+
+5. Rode o bot pela primeira vez (você deve rodar da raiz, não dentro de `src`):
+```bash
+node ./src/main.js
```
Ele vai pedir para que escaneie o QR Code com seu celular.
-Vá em: WhatsApp > Trẽs pontos no canto inferior > Dispositivos conectados > Conectar um dispositivo
+
+No WhatsApp:
+Menu (três pontos) > Dispositivos conectados > Conectar um dispositivo
# Instalação (Windows)
-O uso desse bot foi pensado para rodar em um terminal Linux com Bash. No entanto, você pode usar o Git Bash, que simula um terminal Linux com Bash real:
+O uso desse bot foi pensado para rodar em um terminal Linux. No entanto, você pode usar o Git Bash, que simula um terminal Linux com Bash real:
1. Para baixar o Git Bash: https://git-scm.com/install/windows
Selecione a versão que deseja (portátil ou instalador)
-2. Para baixar o Node.js: https://nodejs.org/pt-br/download/current
-Role a tela e selecione "binário independente (.zip)"
+2. Para baixar o Node.js: https://nodejs.org/pt-br/download
+Role a tela e selecione "Instalador Windows (.msi)"
Ou se preferir, use um gerenciador de pacotes como mostra no conteúdo inicial
-Após baixar e instalar ambos, abra o Git Bash e execute exatamente os mesmos passos do Linux
+Depois de instalar ambos, abra o Git Bash e execute exatamente os mesmos comandos mostrados na seção Linux.
+
+# Uso
+
+Feito a instalação, você pode executar o bot apenas rodando:
+```bash
+node ./src/main.js
+```
+
+## Atualizações
+
+É recomendável sempre ter a versão mais recente do ManyBot. Para isso, temos um utilitário logo na raíz. Para executar:
+```bash
+bash ./update
+```
+
+## Criando um serviço (opcional)
+
+Se estiver rodando numa VPS ou apenas quer mais controle, é recomendável criar um serviço systemd. Siga os passos abaixo para saber como criar, habilitar e gerenciar um.
+
+1. Configurando o diretório
+
+Primeiro passo é garantir que o diretório do ManyBot esteja no local adequado, é recomendável guardar em `/root/manybot` (os passos a seguir supõem que esteja essa localização)
+
+2. Criando o serviço
+
+Abra o arquivo:
+```bash
+/etc/systemd/system/manybot.service
+```
+
+E cole o seguinte conteúdo:
+```conf
+[Unit]
+Description=ManyBot
+After=network.target
+
+[Service]
+ExecStart=/usr/bin/env node /root/manybot/src/main.js
+WorkingDirectory=/root/manybot
+Restart=always
+Environment=NODE_ENV=production
+
+[Install]
+WantedBy=multi-user.target
+```
+
+3. Iniciando e habilitando o serviço:
+
+Primeiro reinicie o daemon do systemd:
+```bash
+systemctl daemon-reload
+```
+
+Inicie o serviço:
+```bash
+systemctl start manybot
+```
+
+Habilite para que ele seja iniciado junto com o seu sistema (opcional):
+```bash
+systemctl enable manybot
+```
+
+4. Gerenciando o serviço:
+
+Ver logs:
+```bash
+journalctl -u manybot
+```
+
+Em tempo real:
+```bash
+journalctl -u manybot -f
+```
+
+Parar o serviço:
+```bash
+systemctl stop manybot
+```
+
+Reiniciar o serviço:
+```bash
+systemctl restart manybot
+```
+
+Saiba mais sobre como gerenciar serviços em: https://www.digitalocean.com/community/tutorials/how-to-use-systemctl-to-manage-systemd-services-and-units-pt
+Sobre o journalctl: https://www.digitalocean.com/community/tutorials/how-to-use-journalctl-to-view-and-manipulate-systemd-logs-pt
+
+# Plugins
+
+O ManyBot é construído em torno de um sistema de plugins. O núcleo do bot (kernel) apenas conecta ao WhatsApp e distribui as mensagens — quem decide o que fazer com elas são os plugins.
+
+Isso significa que você pode adicionar, remover ou criar funcionalidades sem tocar no código principal do bot.
+
+## Plugins incluídos
+
+O ManyBot vem com alguns plugins prontos para uso, como:
+
+- **video** — baixa um vídeo da internet e envia no chat (`!video `)
+- **audio** — baixa o áudio de um vídeo e envia como mensagem de voz (`!audio `)
+- **figurinha** — converte imagens, GIFs e vídeos em figurinhas (`!figurinha`)
+- **adivinhacao** — jogo de adivinhação de um número entre 1 e 100 (`!adivinhação começar`)
+- **forca** — clássico jogo da forca (`!forca começar`)
+- **many** — exibe a lista de comandos disponíveis (`!many`)
+- **obrigado** — responde agradecimentos (`!obrigado`, `!valeu`, `!brigado`)
+
+Para ativar ou desativar qualquer um deles, basta editar a lista `PLUGINS` no `manybot.conf`.
+
+## Criando um plugin
+
+Cada plugin é uma pasta dentro de `plugins/` com um arquivo `index.js`. O bot carrega automaticamente todos os plugins listados no `manybot.conf`.
+
+A estrutura mínima de um plugin:
+
+```
+plugins/
+└── meu-plugin/
+ └── index.js
+```
+
+O `index.js` deve exportar uma função `default` que o kernel chama a cada mensagem recebida. A função recebe `{ msg, api }` e decide por conta própria se age ou ignora:
+
+```js
+// plugins/meu-plugin/index.js
+
+import { CMD_PREFIX } from "../../config.js"
+
+export default async function ({ msg, api }) {
+ if (!msg.is(CMD_PREFIX + "oi")) return;
+
+ await msg.reply("Olá! 👋");
+}
+```
+
+### O objeto `msg`
+
+Contém as informações da mensagem recebida:
+
+| Propriedade | Descrição |
+|---|---|
+| `msg.body` | Texto da mensagem |
+| `msg.args` | Tokens da mensagem — `["!video", "https://..."]` |
+| `msg.type` | Tipo — `"chat"`, `"image"`, `"video"`, `"audio"`, `"sticker"` |
+| `msg.sender` | ID de quem enviou |
+| `msg.senderName` | Nome de quem enviou |
+| `msg.fromMe` | `true` se foi o próprio bot que enviou |
+| `msg.hasMedia` | `true` se a mensagem tem mídia |
+| `msg.hasReply` | `true` se é uma resposta a outra mensagem |
+| `msg.isGif` | `true` se a mídia é um GIF |
+| `msg.is(cmd)` | Retorna `true` se a mensagem começa com `cmd` |
+| `msg.reply(text)` | Responde à mensagem com quote |
+| `msg.downloadMedia()` | Baixa a mídia — retorna `{ mimetype, data }` |
+| `msg.getReply()` | Retorna a mensagem citada, ou `null` |
+
+### O objeto `api`
+
+Contém tudo que o plugin pode fazer — enviar mensagens, acessar outros plugins, registrar logs:
+
+| Método | Descrição |
+|---|---|
+| `api.send(text)` | Envia texto no chat |
+| `api.sendVideo(filePath)` | Envia um vídeo a partir de um arquivo local |
+| `api.sendAudio(filePath)` | Envia um áudio a partir de um arquivo local |
+| `api.sendImage(filePath, caption?)` | Envia uma imagem a partir de um arquivo local |
+| `api.sendSticker(bufferOuPath)` | Envia uma figurinha — aceita `Buffer` ou caminho |
+| `api.getPlugin(name)` | Retorna a API pública de outro plugin |
+| `api.chat.id` | ID do chat atual |
+| `api.chat.name` | Nome do chat atual |
+| `api.chat.isGroup` | `true` se é um grupo |
+| `api.log.info(...)` | Loga uma mensagem informativa |
+| `api.log.warn(...)` | Loga um aviso |
+| `api.log.error(...)` | Loga um erro |
+
+### Lendo o manybot.conf no plugin
+
+Se o seu plugin precisar de configurações próprias, você pode adicioná-las diretamente no `manybot.conf` e importá-las no código:
+
+```js
+import { MEU_PREFIXO } from "../../src/config.js";
+
+const prefixo = MEU_PREFIXO ?? "padrão";
+```
+
+### Expondo uma API para outros plugins
+
+Um plugin pode expor funções para que outros plugins as utilizem. Para isso, basta exportar um objeto `api`:
+
+```js
+// plugins/utilidades/index.js
+
+export const api = {
+ formatarData: (date) => date.toLocaleDateString("pt-BR"),
+};
+
+export default async function ({ msg }) {
+ // lógica normal do plugin
+}
+```
+
+Outro plugin pode chamar:
+
+```js
+const utils = api.getPlugin("utilidades");
+utils.formatarData(new Date());
+```
+
+### Erros no plugin
+
+Se um plugin lançar um erro, o kernel o desativa automaticamente e loga o problema — o restante dos plugins continua funcionando normalmente. Isso garante que um plugin com bug não derruba o bot inteiro.
+
+# Considerações
+
+ManyBot é distribuído sob a licença GPLv3. Você pode usar, modificar e redistribuir o software conforme os termos da licença.
+
+Saiba mais sobre as permissões lendo o arquivo [LICENSE](LICENSE) ou em: https://www.gnu.org/licenses/quick-guide-gplv3.pt-br.html
\ No newline at end of file
diff --git a/examples/figurinha.gif b/examples/figurinha.gif
new file mode 100644
index 0000000..3dde5e4
Binary files /dev/null and b/examples/figurinha.gif differ
diff --git a/package-lock.json b/package-lock.json
index 74b0cf1..36b0a69 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
- "name": "whatsapp-bot",
- "version": "2.3.4",
+ "name": "manybot",
+ "version": "2.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
- "name": "whatsapp-bot",
- "version": "2.3.4",
+ "name": "manybot",
+ "version": "2.4.0",
"dependencies": {
"node-addon-api": "^7",
"node-gyp": "^12.2.0",
diff --git a/setup b/setup
index e046515..d2031ee 100755
--- a/setup
+++ b/setup
@@ -162,6 +162,13 @@ log_info "Instalando dependências npm"
export PUPPETEER_SKIP_DOWNLOAD=1
run_cmd npm install
+# ------------------------
+# Chrome Puppeeter
+# ------------------------
+log_info "Instalando Chrome"
+
+npx puppeteer browsers install chrome
+
# ------------------------
# Diretórios
# ------------------------
@@ -189,29 +196,9 @@ else
)
fi
+chmod +x bin/*
log_debug "Total de arquivos para baixar: ${#files[@]}"
-# ------------------------
-# Config
-# ------------------------
-log_info "Criando arquivo de configuração"
-
-cat > "src/config.js" << 'EOF'
-export const CLIENT_ID = "bot_permanente";
-export const BOT_PREFIX = "🤖 *ManyBot:* ";
-export const CMD_PREFIX = "!";
-export const CHATS = [
- // coloque os chats que quer aqui
-];
-
-EOF
-
-if [[ -f src/config.js ]]; then
- log_ok "Arquivo de configuração criado"
-else
- log_error "Erro durante criação do arquivo de configuração"
-fi
-
# ------------------------
# Download
# ------------------------
diff --git a/src/client/whatsappClient.js b/src/client/whatsappClient.js
index e5496c3..fadbeca 100644
--- a/src/client/whatsappClient.js
+++ b/src/client/whatsappClient.js
@@ -16,14 +16,21 @@ logger.info(isTermux
// ── Instância ─────────────────────────────────────────────────
export const client = new Client({
authStrategy: new LocalAuth({ clientId: CLIENT_ID }),
- puppeteer: { headless: true, ...resolvePuppeteerConfig() },
+ puppeteer: {
+ headless: true,
+ args: [
+ '--no-sandbox',
+ '--disable-setuid-sandbox',
+ ...(resolvePuppeteerConfig().args || [])
+ ],
+ ...resolvePuppeteerConfig()
+ },
});
// ── Eventos ───────────────────────────────────────────────────
client.on("qr", handleQR);
client.on("ready", () => {
- console.log("READY DISPAROU"); // temporário
printBanner();
logger.success("WhatsApp conectado e pronto!");
logger.info(`Client ID: ${CLIENT_ID}`);
diff --git a/src/commands/handlers/a.js b/src/commands/handlers/a.js
deleted file mode 100644
index f352d66..0000000
--- a/src/commands/handlers/a.js
+++ /dev/null
@@ -1,5 +0,0 @@
-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
deleted file mode 100644
index cb23069..0000000
--- a/src/commands/handlers/adivinhacao.js
+++ /dev/null
@@ -1,44 +0,0 @@
-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
deleted file mode 100644
index 0840db0..0000000
--- a/src/commands/handlers/audio.js
+++ /dev/null
@@ -1,15 +0,0 @@
-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
deleted file mode 100644
index eb06442..0000000
--- a/src/commands/handlers/figurinha.js
+++ /dev/null
@@ -1,26 +0,0 @@
-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
deleted file mode 100644
index e4bda03..0000000
--- a/src/commands/handlers/info.js
+++ /dev/null
@@ -1,24 +0,0 @@
-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
deleted file mode 100644
index c813314..0000000
--- a/src/commands/handlers/many.js
+++ /dev/null
@@ -1,11 +0,0 @@
-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
deleted file mode 100644
index fe0d8c7..0000000
--- a/src/commands/handlers/obrigado.js
+++ /dev/null
@@ -1,5 +0,0 @@
-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
deleted file mode 100644
index 9a0af97..0000000
--- a/src/commands/handlers/video.js
+++ /dev/null
@@ -1,15 +0,0 @@
-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
deleted file mode 100644
index e022f10..0000000
--- a/src/commands/index.js
+++ /dev/null
@@ -1,33 +0,0 @@
-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 { cmd, args, valid } = parseCommand(msg.body);
-
- if (!valid) return;
-
- const handler = commandRegistry.get(cmd);
-
- if (!handler) {
- logger.warn(`Comando desconhecido: ${cmd}`);
- return;
- }
-
- logger.cmd(cmd);
-
- try {
- await handler(msg, chat, chatId, args);
- } catch (err) {
- logger.error(`Falha em ${cmd} — ${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/commands/logic/figurinha.js b/src/commands/logic/figurinha.js
deleted file mode 100644
index 94ddaaf..0000000
--- a/src/commands/logic/figurinha.js
+++ /dev/null
@@ -1,192 +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 "./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 = 30;
-
-// ── 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
deleted file mode 100644
index 4a5675c..0000000
--- a/src/commands/logic/games/adivinhacao.js
+++ /dev/null
@@ -1,61 +0,0 @@
-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
deleted file mode 100644
index db31222..0000000
--- a/src/commands/logic/stickerSessions.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * 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
deleted file mode 100644
index 219cc47..0000000
--- a/src/commands/parser.js
+++ /dev/null
@@ -1,24 +0,0 @@
-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
deleted file mode 100644
index dcae333..0000000
--- a/src/commands/registry.js
+++ /dev/null
@@ -1,25 +0,0 @@
-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/config.js b/src/config.js
index f0b2407..313b794 100644
--- a/src/config.js
+++ b/src/config.js
@@ -1,7 +1,13 @@
+/**
+ * config.js
+ *
+ * Lê e parseia o manybot.conf.
+ * Suporta listas multilinhas e comentários inline.
+ */
+
import fs from "fs";
function parseConf(raw) {
- // Remove comentários inline e de linha inteira, preservando estrutura
const lines = raw.split("\n");
const cleaned = [];
@@ -9,13 +15,11 @@ function parseConf(raw) {
let buffer = "";
for (let line of lines) {
- // Remove comentário inline (# ...) — mas só fora de strings
line = line.replace(/#.*$/, "").trim();
if (!line) continue;
if (!insideList) {
if (line.includes("=[") && !line.includes("]")) {
- // Início de lista multilinha
insideList = true;
buffer = line;
} else {
@@ -24,7 +28,6 @@ function parseConf(raw) {
} else {
buffer += line;
if (line.includes("]")) {
- // Fim da lista
insideList = false;
cleaned.push(buffer);
buffer = "";
@@ -32,7 +35,6 @@ function parseConf(raw) {
}
}
- // Parseia cada linha chave=valor
const result = {};
for (const line of cleaned) {
const eqIdx = line.indexOf("=");
@@ -55,10 +57,15 @@ function parseConf(raw) {
return result;
}
-const raw = fs.readFileSync("manybot.conf", "utf8");
+const raw = fs.readFileSync("manybot.conf", "utf8");
const config = parseConf(raw);
-export const CLIENT_ID = config.CLIENT_ID ?? "bot_permanente";
-export const BOT_PREFIX = config.BOT_PREFIX ?? "🤖 *ManyBot:* ";
-export const CMD_PREFIX = config.CMD_PREFIX ?? "!";
-export const CHATS = config.CHATS ?? [];
\ No newline at end of file
+export const CLIENT_ID = config.CLIENT_ID ?? "bot_permanente";
+export const CMD_PREFIX = config.CMD_PREFIX ?? "!";
+export const CHATS = config.CHATS ?? [];
+
+/** Lista de plugins ativos — ex: PLUGINS=[video, audio, hello] */
+export const PLUGINS = config.PLUGINS ?? [];
+
+/** Exporta o config completo para plugins que precisam de valores customizados */
+export const CONFIG = config;
\ No newline at end of file
diff --git a/src/download/downloader.js b/src/download/downloader.js
deleted file mode 100644
index 9123197..0000000
--- a/src/download/downloader.js
+++ /dev/null
@@ -1,112 +0,0 @@
-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
deleted file mode 100644
index 51340cb..0000000
--- a/src/download/mediaType.js
+++ /dev/null
@@ -1,15 +0,0 @@
-/**
- * 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 ca45b4f..23ac1fa 100644
--- a/src/download/queue.js
+++ b/src/download/queue.js
@@ -1,74 +1,54 @@
-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;
-
/**
- * @typedef {{ type: "video"|"audio", url: string, msg: object, chatId: string }} DownloadJob
+ * src/download/queue.js
+ *
+ * Fila de execução sequencial para jobs pesados (downloads, conversões).
+ * Garante que apenas um job roda por vez — sem sobrecarregar yt-dlp ou ffmpeg.
+ *
+ * O plugin passa uma `workFn` que faz tudo: baixar, converter, enviar.
+ * A fila só garante a sequência e trata erros.
+ *
+ * Uso:
+ * import { enqueue } from "../../src/download/queue.js";
+ * enqueue(async () => { ... toda a lógica do plugin ... }, onError);
*/
-/** @type {DownloadJob[]} */
+import { logger } from "../logger/logger.js";
+
+/**
+ * @typedef {{
+ * workFn: () => Promise,
+ * errorFn: (err: Error) => Promise,
+ * }} Job
+ */
+
+/** @type {Job[]} */
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
+ *
+ * @param {Function} workFn — async () => void — toda a lógica do plugin
+ * @param {Function} errorFn — async (err) => void — chamado se workFn lançar
*/
-export function enqueueDownload(type, url, msg, chatId) {
- queue.push({ type, url, msg, chatId });
+export function enqueue(workFn, errorFn) {
+ queue.push({ workFn, errorFn });
if (!processing) processQueue();
}
async function processQueue() {
processing = true;
-
while (queue.length) {
- const job = queue.shift();
- await processJob(job);
+ await processJob(queue.shift());
}
-
processing = false;
}
-/**
- * Executa um único job: baixa, envia e limpa.
- * @param {DownloadJob} job
- */
-async function processJob(job) {
- const { mime, label } = resolveMediaType(job.type);
-
+async function processJob({ workFn, errorFn }) {
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);
+ await workFn();
} 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."
- ));
+ logger.error(`Falha no job — ${err.message}`);
+ try { await errorFn(err); } catch { }
}
}
\ No newline at end of file
diff --git a/src/handlers/messageHandler.js b/src/handlers/messageHandler.js
deleted file mode 100644
index 95c780f..0000000
--- a/src/handlers/messageHandler.js
+++ /dev/null
@@ -1,29 +0,0 @@
-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/kernel/messageHandler.js b/src/kernel/messageHandler.js
new file mode 100644
index 0000000..73cc094
--- /dev/null
+++ b/src/kernel/messageHandler.js
@@ -0,0 +1,48 @@
+/**
+ * messageHandler.js
+ *
+ * Pipeline central de uma mensagem recebida.
+ *
+ * Ordem:
+ * 1. Filtra chats não permitidos (CHATS do .conf)
+ * 2. Loga a mensagem
+ * 3. Passa o contexto para todos os plugins ativos
+ *
+ * O kernel não conhece nenhum comando — só distribui.
+ * Cada plugin decide por conta própria se age ou ignora.
+ */
+
+import { CHATS } from "../config.js";
+import { getChatId } from "../utils/getChatId.js";
+import { buildApi } from "./pluginApi.js";
+import { pluginRegistry } from "./pluginLoader.js";
+import { runPlugin } from "./pluginGuard.js";
+import { buildMessageContext } from "../logger/messageContext.js";
+import { logger } from "../logger/logger.js";
+
+/**
+ * @param {import("whatsapp-web.js").Message} msg
+ */
+export async function handleMessage(msg) {
+ const chat = await msg.getChat();
+ const chatId = getChatId(chat);
+
+ // Filtra chats não autorizados
+ if (!CHATS.includes(chatId)) return;
+
+ // Loga a mensagem recebida
+ const ctx = await buildMessageContext(msg, chat);
+ logger.msg(ctx);
+
+ // Monta a api que será passada para os plugins
+ const api = buildApi({ msg, chat, pluginRegistry });
+
+ // Distribui para todos os plugins ativos
+ const context = { msg: api.msg, chat: api.chat, api };
+
+ for (const plugin of pluginRegistry.values()) {
+ await runPlugin(plugin, context);
+ }
+
+ logger.done("message_create", `de +${ctx.senderNumber}`);
+}
\ No newline at end of file
diff --git a/src/kernel/pluginApi.js b/src/kernel/pluginApi.js
new file mode 100644
index 0000000..ee06ef4
--- /dev/null
+++ b/src/kernel/pluginApi.js
@@ -0,0 +1,187 @@
+/**
+ * pluginApi.js
+ *
+ * Monta o objeto `api` que cada plugin recebe.
+ * Plugins só podem fazer o que está aqui — nunca tocam no client diretamente.
+ *
+ * O `chat` já vem filtrado pelo kernel (só chats permitidos no .conf),
+ * então plugins não precisam e não podem escolher destino.
+ */
+
+import { logger } from "../logger/logger.js";
+import pkg from "whatsapp-web.js";
+
+const { MessageMedia } = pkg;
+
+/**
+ * @param {object} params
+ * @param {import("whatsapp-web.js").Message} params.msg
+ * @param {import("whatsapp-web.js").Chat} params.chat
+ * @param {Map} params.pluginRegistry
+ * @returns {object} api
+ */
+export function buildApi({ msg, chat, pluginRegistry }) {
+
+ // ── Helpers internos ──────────────────────────────────────
+ const currentChat = chat;
+
+ return {
+
+ // ── Leitura de mensagem ──────────────────────────────────
+
+ msg: {
+ /** Corpo da mensagem */
+ body: msg.body ?? "",
+
+ /** Tipo: "chat", "image", "video", "audio", "ptt", "sticker", "document" */
+ type: msg.type,
+
+ /** true se a mensagem veio do próprio bot */
+ fromMe: msg.fromMe,
+
+ /** ID de quem enviou (ex: "5511999999999@c.us") */
+ sender: msg.author || msg.from,
+
+ /** Nome de exibição de quem enviou */
+ senderName: msg._data?.notifyName || String(msg.from).replace(/(:\d+)?@.*$/, ""),
+
+ /** Tokens: ["!video", "https://..."] */
+ args: msg.body?.trim().split(/\s+/) ?? [],
+
+ /**
+ * Verifica se a mensagem é um comando específico.
+ * @param {string} cmd — ex: "!hello"
+ */
+ is(cmd) {
+ return msg.body?.trim().toLowerCase().startsWith(cmd.toLowerCase());
+ },
+
+ /** true se a mensagem tem mídia anexada */
+ hasMedia: msg.hasMedia,
+
+ /** true se a mídia é um GIF (vídeo curto em loop) */
+ isGif: msg._data?.isGif ?? false,
+
+ /**
+ * Baixa a mídia da mensagem.
+ * Retorna um objeto neutro { mimetype, data } — sem expor MessageMedia.
+ * @returns {Promise<{ mimetype: string, data: string } | null>}
+ */
+ async downloadMedia() {
+ const media = await msg.downloadMedia();
+ if (!media) return null;
+ return { mimetype: media.mimetype, data: media.data };
+ },
+
+ /** true se a mensagem é uma resposta a outra */
+ hasReply: msg.hasQuotedMsg,
+
+ /**
+ * Retorna a mensagem citada, se existir.
+ * @returns {Promise}
+ */
+ async getReply() {
+ if (!msg.hasQuotedMsg) return null;
+ return msg.getQuotedMessage();
+ },
+
+ /**
+ * Responde diretamente à mensagem (com quote).
+ * @param {string} text
+ */
+ async reply(text) {
+ return msg.reply(text);
+ },
+ },
+
+ // ── Envio para o chat atual ──────────────────────────────
+
+ /**
+ * Envia texto simples.
+ * @param {string} text
+ */
+ async send(text) {
+ return currentChat.sendMessage(text);
+ },
+
+ /**
+ * Envia uma mídia (imagem, vídeo, áudio, documento).
+ * @param {import("whatsapp-web.js").MessageMedia} media
+ * @param {string} [caption]
+ */
+ async sendMedia(media, caption = "") {
+ return currentChat.sendMessage(media, { caption });
+ },
+
+ /**
+ * Envia um arquivo de vídeo a partir de um caminho local.
+ * @param {string} filePath
+ * @param {string} [caption]
+ */
+ async sendVideo(filePath, caption = "") {
+ const media = MessageMedia.fromFilePath(filePath);
+ return currentChat.sendMessage(media, { caption });
+ },
+
+ /**
+ * Envia um arquivo de áudio a partir de um caminho local.
+ * @param {string} filePath
+ */
+ async sendAudio(filePath) {
+ const media = MessageMedia.fromFilePath(filePath);
+ return currentChat.sendMessage(media, { sendAudioAsVoice: true });
+ },
+
+ /**
+ * Envia uma imagem a partir de um caminho local.
+ * @param {string} filePath
+ * @param {string} [caption]
+ */
+ async sendImage(filePath, caption = "") {
+ const media = MessageMedia.fromFilePath(filePath);
+ return currentChat.sendMessage(media, { caption });
+ },
+
+ /**
+ * Envia uma figurinha (sticker).
+ * Aceita filePath (string) ou buffer (Buffer) — o plugin nunca precisa
+ * saber que MessageMedia existe.
+ * @param {string | Buffer} source
+ */
+ async sendSticker(source) {
+ const media = typeof source === "string"
+ ? MessageMedia.fromFilePath(source)
+ : new MessageMedia("image/webp", source.toString("base64"));
+ return currentChat.sendMessage(media, { sendMediaAsSticker: true });
+ },
+
+ // ── Acesso a outros plugins ──────────────────────────────
+
+ /**
+ * Retorna a API pública de outro plugin (o que ele exportou em `exports`).
+ * Retorna null se o plugin não existir ou estiver desativado.
+ * @param {string} name — nome do plugin (pasta em /plugins)
+ * @returns {any|null}
+ */
+ getPlugin(name) {
+ return pluginRegistry.get(name)?.exports ?? null;
+ },
+
+ // ── Logger ───────────────────────────────────────────────
+
+ log: {
+ info: (...a) => logger.info(...a),
+ warn: (...a) => logger.warn(...a),
+ error: (...a) => logger.error(...a),
+ success: (...a) => logger.success(...a),
+ },
+
+ // ── Info do chat atual ───────────────────────────────────
+
+ chat: {
+ id: currentChat.id._serialized,
+ name: currentChat.name || currentChat.id.user,
+ isGroup: /@g\.us$/.test(currentChat.id._serialized),
+ },
+ };
+}
\ No newline at end of file
diff --git a/src/kernel/pluginGuard.js b/src/kernel/pluginGuard.js
new file mode 100644
index 0000000..a8910c9
--- /dev/null
+++ b/src/kernel/pluginGuard.js
@@ -0,0 +1,36 @@
+/**
+ * pluginGuard.js
+ *
+ * Executa um plugin com segurança.
+ * Se o plugin lançar um erro:
+ * - Loga o erro com contexto
+ * - Marca o plugin como "error" no registry
+ * - Nunca derruba o bot
+ *
+ * Plugins desativados ou com erro são ignorados silenciosamente.
+ */
+
+import { logger } from "../logger/logger.js";
+import { pluginRegistry } from "./pluginLoader.js";
+
+/**
+ * @param {object} plugin — entrada do pluginRegistry
+ * @param {object} context — { msg, chat, api }
+ */
+export async function runPlugin(plugin, context) {
+ if (plugin.status !== "active") return;
+
+ try {
+ await plugin.run(context);
+ } catch (err) {
+ // Desativa o plugin para não continuar quebrando
+ plugin.status = "error";
+ plugin.error = err;
+ pluginRegistry.set(plugin.name, plugin);
+
+ logger.error(
+ `Plugin "${plugin.name}" desativado após erro: ${err.message}`,
+ `\n Stack: ${err.stack?.split("\n")[1]?.trim() ?? ""}`
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/kernel/pluginLoader.js b/src/kernel/pluginLoader.js
new file mode 100644
index 0000000..84e5a30
--- /dev/null
+++ b/src/kernel/pluginLoader.js
@@ -0,0 +1,88 @@
+/**
+ * pluginLoader.js
+ *
+ * Responsável por:
+ * 1. Ler quais plugins estão ativos no manybot.conf (PLUGINS=[...])
+ * 2. Carregar cada plugin da pasta /plugins
+ * 3. Registrar no pluginRegistry com status e exports públicos
+ * 4. Expor o pluginRegistry para o kernel e para a pluginApi
+ */
+
+import fs from "fs";
+import path from "path";
+import { logger } from "../logger/logger.js";
+
+const PLUGINS_DIR = path.resolve("src/plugins");
+
+/**
+ * Cada entrada no registry:
+ * {
+ * name: string,
+ * status: "active" | "disabled" | "error",
+ * run: async function({ msg, chat, api }) — a função default do plugin
+ * exports: any — o que o plugin expôs via `export const api = { ... }`
+ * error: Error | null
+ * }
+ *
+ * @type {Map}
+ */
+export const pluginRegistry = new Map();
+
+/**
+ * Carrega todos os plugins ativos listados em `activePlugins`.
+ * Chamado uma vez na inicialização do bot.
+ *
+ * @param {string[]} activePlugins — nomes dos plugins ativos (do .conf)
+ */
+export async function loadPlugins(activePlugins) {
+ if (!fs.existsSync(PLUGINS_DIR)) {
+ logger.warn("Pasta /plugins não encontrada. Nenhum plugin carregado.");
+ return;
+ }
+
+ for (const name of activePlugins) {
+ await loadPlugin(name);
+ }
+
+ const total = pluginRegistry.size;
+ const ativos = [...pluginRegistry.values()].filter(p => p.status === "active").length;
+ const erros = total - ativos;
+
+ logger.success(`Plugins carregados: ${ativos} ativos${erros ? `, ${erros} com erro` : ""}`);
+}
+
+/**
+ * Carrega um único plugin pelo nome.
+ * @param {string} name
+ */
+async function loadPlugin(name) {
+ const pluginPath = path.join(PLUGINS_DIR, name, "index.js");
+
+ if (!fs.existsSync(pluginPath)) {
+ logger.warn(`Plugin "${name}" não encontrado em ${pluginPath}`);
+ pluginRegistry.set(name, { name, status: "disabled", run: null, exports: null, error: null });
+ return;
+ }
+
+ try {
+ const mod = await import(pluginPath);
+
+ // O plugin deve exportar uma função default — essa é a função chamada a cada mensagem
+ if (typeof mod.default !== "function") {
+ throw new Error(`Plugin "${name}" não exporta uma função default`);
+ }
+
+ pluginRegistry.set(name, {
+ name,
+ status: "active",
+ run: mod.default,
+ exports: mod.api ?? null, // exports públicos opcionais (api de outros plugins)
+ error: null,
+ });
+
+ logger.info(`Plugin carregado: ${name}`);
+ } catch (err) {
+ logger.error(`Falha ao carregar plugin "${name}": ${err.message}`);
+ pluginRegistry.set(name, { name, status: "error", run: null, exports: null, error: err });
+ }
+}
\ No newline at end of file
diff --git a/src/kernel/scheduler.js b/src/kernel/scheduler.js
new file mode 100644
index 0000000..7b067db
--- /dev/null
+++ b/src/kernel/scheduler.js
@@ -0,0 +1,47 @@
+/**
+ * scheduler.js
+ *
+ * Permite que plugins registrem tarefas agendadas via cron.
+ * Usa node-cron por baixo, mas plugins nunca importam node-cron diretamente —
+ * eles chamam apenas api.schedule(cron, fn).
+ *
+ * Uso no plugin:
+ * import { schedule } from "many";
+ * schedule("0 9 * * 1", async () => { await api.send("Bom dia!"); });
+ */
+
+import cron from "node-cron";
+import { logger } from "../logger/logger.js";
+
+/** Lista de tasks ativas (para eventual teardown) */
+const tasks = [];
+
+/**
+ * Registra uma tarefa cron.
+ * @param {string} expression — expressão cron ex: "0 9 * * 1"
+ * @param {Function} fn — função async a executar
+ * @param {string} pluginName — nome do plugin (para log)
+ */
+export function schedule(expression, fn, pluginName = "unknown") {
+ if (!cron.validate(expression)) {
+ logger.warn(`Plugin "${pluginName}" registrou expressão cron inválida: "${expression}"`);
+ return;
+ }
+
+ const task = cron.schedule(expression, async () => {
+ try {
+ await fn();
+ } catch (err) {
+ logger.error(`Erro no agendamento do plugin "${pluginName}": ${err.message}`);
+ }
+ });
+
+ tasks.push({ pluginName, expression, task });
+ logger.info(`Agendamento registrado — plugin "${pluginName}" → "${expression}"`);
+}
+
+/** Para todos os agendamentos (útil no shutdown) */
+export function stopAll() {
+ tasks.forEach(({ task }) => task.stop());
+ tasks.length = 0;
+}
\ No newline at end of file
diff --git a/src/logger/formatter.js b/src/logger/formatter.js
index 8561205..275441a 100644
--- a/src/logger/formatter.js
+++ b/src/logger/formatter.js
@@ -6,8 +6,6 @@ export const c = {
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" });
@@ -26,9 +24,9 @@ export const formatContext = (chatName, 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) =>
+export const formatBody = (body) =>
body?.trim()
- ? `${isCommand ? c.yellow : c.green}"${body.length > 200 ? body.slice(0, 200) + "..." : body}"${c.reset}`
+ ? `${c.green}"${body.length > 200 ? body.slice(0, 200) + "..." : body}"${c.reset}`
: `${c.dim}${c.reset}`;
export const formatReply = (quotedName, quotedNumber, quotedPreview) =>
diff --git a/src/logger/logger.js b/src/logger/logger.js
index 8a48a51..6827f39 100644
--- a/src/logger/logger.js
+++ b/src/logger/logger.js
@@ -1,5 +1,5 @@
import {
- c, SEP, now,
+ c, now,
formatType, formatContext, formatBody, formatReply,
} from "./formatter.js";
@@ -8,7 +8,7 @@ import {
* 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),
+ info: (...a) => console.log(`${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),
@@ -18,23 +18,10 @@ export const logger = {
* @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
- );
+ const { chatName, isGroup, senderName, senderNumber, type, body, quoted } = ctx;
+ const context = isGroup ? `${chatName} (grupo)` : chatName;
+ const reply = quoted ? ` → Responde ${quoted.name} +${quoted.number}: "${quoted.preview}"` : "";
+ console.log(`\n${c.gray}[${now()}]${c.reset} ${c.cyan}MSG${c.reset} ${context} ${c.gray}— De:${c.reset} ${c.white}${senderName}${c.reset} ${c.dim}+${senderNumber}${c.reset} ${c.gray}— Tipo:${c.reset} ${type} — ${c.green}"${body}"${c.reset}${c.gray}${reply}${c.reset}`);
},
cmd: (cmd, extra = "") =>
diff --git a/src/logger/messageContext.js b/src/logger/messageContext.js
index 433e15e..23d1853 100644
--- a/src/logger/messageContext.js
+++ b/src/logger/messageContext.js
@@ -28,7 +28,6 @@ export async function getNumber(msg) {
* @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) {
@@ -47,7 +46,6 @@ export async function buildMessageContext(msg, chat, botPrefix) {
senderNumber: number,
type: msg?.type || "text",
body: msg.body,
- isCommand: !!msg.body?.trimStart().startsWith(botPrefix),
quoted,
};
}
diff --git a/src/main.js b/src/main.js
index 6d99c7a..2c18b76 100644
--- a/src/main.js
+++ b/src/main.js
@@ -1,8 +1,30 @@
-import client from "./client/whatsappClient.js";
-import { handleMessage } from "./handlers/messageHandler.js";
-import { logger } from "./logger/logger.js";
+/**
+ * main.js
+ *
+ * Ponto de entrada do ManyBot.
+ * Inicializa o cliente WhatsApp e carrega os plugins.
+ */
-logger.info("Iniciando ManyBot...");
+import client from "./client/whatsappClient.js";
+import { handleMessage } from "./kernel/messageHandler.js";
+import { loadPlugins } from "./kernel/pluginLoader.js";
+import { logger } from "./logger/logger.js";
+import { PLUGINS } from "./config.js";
+
+logger.info("Iniciando ManyBot...\n");
+
+// Rede de segurança global — nenhum erro deve derrubar o bot
+process.on("uncaughtException", (err) => {
+ logger.error(`uncaughtException — ${err.message}`, `\n Stack: ${err.stack?.split("\n")[1]?.trim() ?? ""}`);
+});
+
+process.on("unhandledRejection", (reason) => {
+ const msg = reason instanceof Error ? reason.message : String(reason);
+ logger.error(`unhandledRejection — ${msg}`);
+});
+
+// Carrega plugins antes de conectar
+await loadPlugins(PLUGINS);
client.on("message_create", async (msg) => {
try {
@@ -16,4 +38,5 @@ client.on("message_create", async (msg) => {
});
client.initialize();
+console.log("\n");
logger.info("Cliente inicializado. Aguardando conexão com WhatsApp...");
\ No newline at end of file
diff --git a/src/plugins/a/index.js b/src/plugins/a/index.js
new file mode 100644
index 0000000..7850963
--- /dev/null
+++ b/src/plugins/a/index.js
@@ -0,0 +1,9 @@
+import { forcaAtiva } from "../forca/index.js";
+
+export default async function ({ msg }) {
+ if (msg.body.trim().toLowerCase() !== "a") return;
+ if (msg.args.length > 1) return;
+ if (forcaAtiva) return;
+
+ await msg.reply("B!");
+}
\ No newline at end of file
diff --git a/src/plugins/adivinhação/index.js b/src/plugins/adivinhação/index.js
new file mode 100644
index 0000000..e50f639
--- /dev/null
+++ b/src/plugins/adivinhação/index.js
@@ -0,0 +1,79 @@
+/**
+ * plugins/adivinhacao/index.js
+ *
+ * Estado dos jogos fica aqui dentro — isolado no plugin.
+ * Múltiplos grupos jogam simultaneamente sem conflito.
+ */
+
+const RANGE = { min: 1, max: 100 };
+const jogosAtivos = new Map();
+import { CMD_PREFIX } from "../../config.js"
+
+const sorteio = () =>
+ Math.floor(Math.random() * (RANGE.max - RANGE.min + 1)) + RANGE.min;
+
+export default async function ({ msg, api }) {
+ const chatId = api.chat.id;
+
+ // ── Comando adivinhação ──────────────────────────────────
+ if (msg.is(CMD_PREFIX + "adivinhação")) {
+ const sub = msg.args[1];
+
+ if (!sub) {
+ await api.send(
+ "🎮 *Jogo de adivinhação:*\n\n" +
+ `\`${CMD_PREFIX}adivinhação começar\` — inicia o jogo\n` +
+ `\`${CMD_PREFIX}adivinhação parar\` — encerra o jogo`
+ );
+ return;
+ }
+
+ if (sub === "começar") {
+ jogosAtivos.set(chatId, sorteio());
+ await api.send(
+ "🎮 *Jogo iniciado!*\n\n" +
+ "Estou pensando em um número de 1 a 100.\n" +
+ "Tente adivinhar! 🤔"
+ );
+ api.log.info(CMD_PREFIX + "adivinhação — jogo iniciado");
+ return;
+ }
+
+ if (sub === "parar") {
+ jogosAtivos.delete(chatId);
+ await api.send("🛑 Jogo encerrado.");
+ api.log.info(CMD_PREFIX + "adivinhação — jogo parado");
+ return;
+ }
+
+ await api.send(
+ `❌ Subcomando *${sub}* não existe.\n\n` +
+ `Use ${CMD_PREFIX} + \`adivinhação começar\` ou ${CMD_PREFIX} + \`adivinhação parar\`.`
+ );
+ return;
+ }
+
+ // ── Tentativas durante o jogo ─────────────────────────────
+ 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(`⚠️ Digite um número entre ${RANGE.min} e ${RANGE.max}.`);
+ return;
+ }
+
+ if (num === numero) {
+ await msg.reply(
+ `🎉 *Acertou!* O número era ${numero}!\n\n` +
+ `Use ${CMD_PREFIX} + \`adivinhação começar\` para jogar de novo.`
+ );
+ jogosAtivos.delete(chatId);
+ } else {
+ await api.send(num > numero ? "📉 Tente um número *menor*!" : "📈 Tente um número *maior*!");
+ }
+}
\ No newline at end of file
diff --git a/src/plugins/audio/index.js b/src/plugins/audio/index.js
new file mode 100644
index 0000000..9a8dcdb
--- /dev/null
+++ b/src/plugins/audio/index.js
@@ -0,0 +1,123 @@
+/**
+ * plugins/audio/index.js
+ *
+ * Baixa vídeo via yt-dlp, converte para mp3 via ffmpeg e envia no chat.
+ * Todo o processo (download + conversão + envio + limpeza) fica aqui.
+ */
+
+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 { enqueue } from "../../download/queue.js";
+import { emptyFolder } from "../../utils/file.js";
+import { CMD_PREFIX } from "../../config.js";
+
+const logStream = fs.createWriteStream("logs/audio-error.log", { flags: "a" });
+
+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",
+ "-f", "bv+ba/best",
+];
+
+function downloadRaw(url, id) {
+ return new Promise((resolve, reject) => {
+ fs.mkdirSync(DOWNLOADS_DIR, { recursive: true });
+
+ const output = path.join(DOWNLOADS_DIR, `${id}.%(ext)s`);
+ const proc = spawn(YT_DLP, [...ARGS_BASE, "--output", output, url]);
+ let stdout = "";
+
+ proc.on("error", err => reject(new Error(
+ err.code === "EACCES"
+ ? "Sem permissão para executar o yt-dlp. Rode: chmod +x ./bin/yt-dlp"
+ : err.code === "ENOENT"
+ ? "yt-dlp não encontrado em ./bin/yt-dlp"
+ : `Erro ao iniciar o yt-dlp: ${err.message}`
+ )));
+
+ proc.stdout.on("data", d => { stdout += d.toString(); });
+ proc.stderr.on("data", d => logStream.write(d));
+
+ proc.on("close", code => {
+ if (code !== 0) return reject(new Error(
+ "Não foi possível baixar o áudio. 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(
+ "Download concluído mas arquivo não encontrado. Tente novamente."
+ ));
+
+ resolve(filePath);
+ });
+ });
+}
+
+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;
+}
+
+export default async function ({ msg, api }) {
+ if (!msg.is(CMD_PREFIX + "audio")) return;
+
+ const url = msg.args[1];
+
+ if (!url) {
+ await msg.reply(`❌ Você precisa informar um link.\n\nExemplo: \`${CMD_PREFIX}audio https://youtube.com/...\``);
+ return;
+ }
+
+ await msg.reply("⏳ Baixando o áudio, aguarde...");
+
+ const id = `audio-${Date.now()}`;
+
+ enqueue(
+ async () => {
+ const videoPath = await downloadRaw(url, id);
+ const mp3Path = await convertToMp3(videoPath, id);
+ await api.sendAudio(mp3Path);
+ fs.unlinkSync(mp3Path);
+ emptyFolder(DOWNLOADS_DIR);
+ api.log.info(`${CMD_PREFIX}audio concluído → ${url}`);
+ },
+ async () => {
+ await msg.reply(
+ "❌ Não consegui baixar o áudio.\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/plugins/figurinha/index.js b/src/plugins/figurinha/index.js
new file mode 100644
index 0000000..7553b6d
--- /dev/null
+++ b/src/plugins/figurinha/index.js
@@ -0,0 +1,247 @@
+/**
+ * plugins/figurinha/index.js
+ *
+ * Modos de uso:
+ * comando + mídia anexa → cria 1 sticker direto
+ * comando + respondendo mídia → cria 1 sticker direto
+ * comando + mídia anexa + respondendo mídia → cria 2 stickers direto
+ * comando (sem mídia nenhuma) → abre sessão
+ * comando criar (com sessão aberta) → processa as mídias da sessão
+ */
+
+import fs from "fs";
+import path from "path";
+import os from "os";
+import { execFile } from "child_process";
+import { promisify } from "util";
+
+import { createSticker } from "wa-sticker-formatter";
+import { emptyFolder } from "../../utils/file.js";
+import { CMD_PREFIX } from "../../config.js";
+
+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 = 30;
+
+const HELP =
+ "📌 *Como criar figurinhas:*\n\n" +
+ `1️⃣ Envie \`${CMD_PREFIX}figurinha\` junto com uma mídia, ou respondendo uma mídia\n` +
+ " — o sticker é criado na hora\n\n" +
+ "2️⃣ Ou use o modo sessão para várias mídias de uma vez:\n" +
+ ` — \`${CMD_PREFIX}figurinha\` sem mídia para iniciar\n` +
+ " — envie as imagens, GIFs ou vídeos\n" +
+ ` — \`${CMD_PREFIX}figurinha criar\` para gerar todas\n\n` +
+ "⏳ A sessão expira em 2 minutos se nenhuma mídia for enviada.";
+
+// ── Estado interno ────────────────────────────────────────────
+// { chatId → { author, medias[], timeout } }
+const sessions = new Map();
+
+// ── Conversão ─────────────────────────────────────────────────
+function ensureDir() {
+ fs.mkdirSync(DOWNLOADS_DIR, { recursive: true });
+}
+
+function cleanup(...files) {
+ for (const f of files) {
+ try { if (f && fs.existsSync(f)) fs.unlinkSync(f); } catch { }
+ }
+}
+
+async function convertToGif(input, output, fps = 12) {
+ const filter = [
+ `fps=${Math.min(fps, 12)},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", input, "-filter_complex", filter, "-loop", "0", "-y", output]);
+}
+
+async function resizeImage(input, output) {
+ await execFileAsync(FFMPEG, ["-i", input, "-vf", "scale=512:512:flags=lanczos", "-y", output]);
+}
+
+async function buildSticker(inputPath, isAnimated) {
+ for (const quality of [80, 60, 40, 20]) {
+ const buf = await createSticker(fs.readFileSync(inputPath), {
+ pack: "Criada por ManyBot\n",
+ author: "\ngithub.com/synt-xerror/manybot",
+ type: isAnimated ? "FULL" : "STATIC",
+ categories: ["🤖"],
+ quality,
+ });
+ if (buf.length <= MAX_STICKER_SIZE) return buf;
+ }
+ throw new Error("Não foi possível reduzir o sticker para menos de 900 KB.");
+}
+
+/**
+ * Converte um objeto { mimetype, data } em sticker e envia.
+ * Retorna true se ok, false se falhou.
+ */
+async function processarUmaMedia(media, isGif, api, msg) {
+ ensureDir();
+
+ const ext = media.mimetype.split("/")[1];
+ const isVideo = media.mimetype.startsWith("video/");
+ 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}`);
+
+ try {
+ fs.writeFileSync(inputPath, Buffer.from(media.data, "base64"));
+
+ let stickerInput;
+ if (isAnimated) {
+ await convertToGif(inputPath, gifPath, isVideo ? 12 : 24);
+ stickerInput = gifPath;
+ } else {
+ await resizeImage(inputPath, resizedPath);
+ stickerInput = resizedPath;
+ }
+
+ const buf = await buildSticker(stickerInput, isAnimated);
+ await api.sendSticker(buf);
+ return true;
+ } catch (err) {
+ api.log.error(`Erro ao gerar sticker: ${err.message}`);
+ await msg.reply(
+ "⚠️ Não consegui criar uma das figurinhas.\n" +
+ "Tente reenviar essa mídia ou use outro formato (JPG, PNG, GIF, MP4)."
+ );
+ return false;
+ } finally {
+ cleanup(inputPath, gifPath, resizedPath);
+ }
+}
+
+/**
+ * Verifica se uma mídia é suportada para sticker.
+ */
+function isSupported(media, isGif) {
+ return (
+ media.mimetype?.startsWith("image/") ||
+ media.mimetype?.startsWith("video/") ||
+ isGif
+ );
+}
+
+// ── Plugin ────────────────────────────────────────────────────
+export default async function ({ msg, api }) {
+ const chatId = api.chat.id;
+
+ if (!msg.is(CMD_PREFIX + "figurinha")) {
+ // ── Coleta de mídia durante sessão ──────────────────────
+ const session = sessions.get(chatId);
+ if (!session) return;
+ if (!msg.hasMedia) return;
+ if (msg.sender !== session.author) return;
+
+ const media = await msg.downloadMedia();
+ if (!media) return;
+
+ const gif = media.mimetype === "image/gif" ||
+ (media.mimetype === "video/mp4" && msg.isGif);
+
+ if (isSupported(media, gif) && session.medias.length < MAX_MEDIA) {
+ session.medias.push({ media, isGif: gif });
+ }
+ return;
+ }
+
+ // ── figurinha criar ──────────────────────────────────────
+ const sub = msg.args[1];
+
+ if (sub === "criar") {
+ const session = sessions.get(chatId);
+
+ if (!session) {
+ await msg.reply(`❌ *Nenhuma sessão ativa.*\n\n${HELP}`);
+ return;
+ }
+ if (!session.medias.length) {
+ await msg.reply(`📭 *Você ainda não enviou nenhuma mídia!*\n\n${HELP}`);
+ return;
+ }
+
+ clearTimeout(session.timeout);
+ await msg.reply("⏳ Gerando suas figurinhas, aguarde um momento...");
+
+ for (const { media, isGif } of session.medias) {
+ await processarUmaMedia(media, isGif, api, msg);
+ }
+
+ await msg.reply("✅ *Figurinhas criadas com sucesso!*\nSalve as que quiser no seu WhatsApp. 😄");
+ sessions.delete(chatId);
+ emptyFolder(DOWNLOADS_DIR);
+ return;
+ }
+
+ // ── figurinha com mídia direta ───────────────────────────
+ const mediasParaCriar = [];
+
+ // Mídia anexa à própria mensagem
+ if (msg.hasMedia) {
+ const media = await msg.downloadMedia();
+ if (media) {
+ const gif = media.mimetype === "image/gif" ||
+ (media.mimetype === "video/mp4" && msg.isGif);
+ if (isSupported(media, gif)) mediasParaCriar.push({ media, isGif: gif });
+ }
+ }
+
+ // Mídia da mensagem citada
+ if (msg.hasReply) {
+ const quoted = await msg.getReply();
+ if (quoted?.hasMedia) {
+ const media = await quoted.downloadMedia();
+ if (media) {
+ const gif = media.mimetype === "image/gif" ||
+ (media.mimetype === "video/mp4" && quoted.isGif);
+ if (isSupported(media, gif)) mediasParaCriar.push({ media, isGif: gif });
+ }
+ }
+ }
+
+ // Tem mídia para criar direto
+ if (mediasParaCriar.length > 0) {
+ await msg.reply("⏳ Gerando figurinha, aguarde...");
+ for (const { media, isGif } of mediasParaCriar) {
+ await processarUmaMedia(media, isGif, api, msg);
+ }
+ emptyFolder(DOWNLOADS_DIR);
+ return;
+ }
+
+ // ── figurinha sem mídia → abre sessão ───────────────────
+ if (sessions.has(chatId)) {
+ await msg.reply(
+ "⚠️ Já existe uma sessão aberta.\n\n" +
+ `Envie as mídias e depois use \`${CMD_PREFIX}figurinha criar\`.\n` +
+ "Ou aguarde 2 minutos para a sessão expirar."
+ );
+ return;
+ }
+
+ const timeout = setTimeout(async () => {
+ sessions.delete(chatId);
+ try {
+ await msg.reply(
+ "⏰ *Sessão expirada!*\n\n" +
+ "Você demorou mais de 2 minutos para enviar as mídias.\n" +
+ `Digite \`${CMD_PREFIX}figurinha\` para começar de novo.`
+ );
+ } catch { }
+ }, SESSION_TIMEOUT);
+
+ sessions.set(chatId, { author: msg.sender, medias: [], timeout });
+ await msg.reply(`✅ Sessão iniciada por *${msg.senderName}*!\n\n${HELP}`);
+}
\ No newline at end of file
diff --git a/src/plugins/forca/index.js b/src/plugins/forca/index.js
new file mode 100644
index 0000000..aad8518
--- /dev/null
+++ b/src/plugins/forca/index.js
@@ -0,0 +1,159 @@
+/**
+ * plugins/forca/index.js
+ *
+ * Estado dos jogos de forca fica aqui dentro — isolado no plugin.
+ * Múltiplos grupos jogam simultaneamente sem conflito.
+ */
+
+import { CMD_PREFIX } from "../../config.js";
+
+// Estados dos jogos
+const jogosAtivos = new Map(); // chatId -> { palavra, tema, vidas, progresso }
+const participantesAtivos = new Map(); // chatId -> Set de usuários que reagiram
+export let forcaAtiva = false;
+
+
+// Palavras de exemplo
+const PALAVRAS = [
+ { palavra: "python", tema: "Linguagem de programação" },
+ { palavra: "javascript", tema: "Linguagem de programação" },
+ { palavra: "java", tema: "Linguagem de programação" },
+ { palavra: "cachorro", tema: "Animal" },
+ { palavra: "gato", tema: "Animal" },
+ { palavra: "elefante", tema: "Animal" },
+ { palavra: "girafa", tema: "Animal" },
+ { palavra: "guitarra", tema: "Instrumento musical" },
+ { palavra: "piano", tema: "Instrumento musical" },
+ { palavra: "bateria", tema: "Instrumento musical" },
+ { palavra: "violino", tema: "Instrumento musical" },
+ { palavra: "futebol", tema: "Esporte" },
+ { palavra: "basquete", tema: "Esporte" },
+ { palavra: "natação", tema: "Esporte" },
+ { palavra: "tênis", tema: "Esporte" },
+ { palavra: "brasil", tema: "País" },
+ { palavra: "japão", tema: "País" },
+ { palavra: "canadá", tema: "País" },
+ { palavra: "frança", tema: "País" },
+ { palavra: "marte", tema: "Planeta" },
+ { palavra: "vênus", tema: "Planeta" },
+ { palavra: "júpiter", tema: "Planeta" },
+ { palavra: "saturno", tema: "Planeta" },
+ { palavra: "minecraft", tema: "Jogo" },
+ { palavra: "fortnite", tema: "Jogo" },
+ { palavra: "roblox", tema: "Jogo" },
+ { palavra: "amongus", tema: "Jogo" },
+ { palavra: "rosa", tema: "Flor" },
+ { palavra: "girassol", tema: "Flor" },
+ { palavra: "tulipa", tema: "Flor" },
+ { palavra: "orquídea", tema: "Flor" },
+ { palavra: "tesoura", tema: "Objeto" },
+ { palavra: "caderno", tema: "Objeto" },
+ { palavra: "computador", tema: "Objeto" },
+ { palavra: "telefone", tema: "Objeto" },
+ { palavra: "lua", tema: "Corpo celeste" },
+ { palavra: "sol", tema: "Corpo celeste" },
+ { palavra: "estrela", tema: "Corpo celeste" },
+ { palavra: "cometa", tema: "Corpo celeste" },
+ { palavra: "oceano", tema: "Natureza" },
+ { palavra: "montanha", tema: "Natureza" },
+];
+
+// Função para gerar a palavra com underscores
+const gerarProgresso = palavra =>
+ palavra.replace(/[a-zA-Z]/g, "_");
+
+export default async function ({ msg, api }) {
+ const chatId = api.chat.id;
+ const sub = msg.args[1];
+
+ // ── Comando principal do jogo
+ if (msg.is(CMD_PREFIX + "forca")) {
+ if (!sub) {
+ await api.send(
+ `🎮 *Jogo da Forca*\n\n` +
+ `\`${CMD_PREFIX}forca começar\` — inicia o jogo\n` +
+ `\`${CMD_PREFIX}forca parar\` — encerra o jogo`
+ );
+ return;
+ }
+
+ if (sub === "começar") {
+ forcaAtiva = true;
+ // Pega uma palavra aleatória
+ const sorteio = PALAVRAS[Math.floor(Math.random() * PALAVRAS.length)];
+
+ // Inicializa o jogo
+ jogosAtivos.set(chatId, {
+ palavra: sorteio.palavra.toLowerCase(),
+ tema: sorteio.tema,
+ vidas: 6,
+ progresso: gerarProgresso(sorteio.palavra)
+ });
+
+ participantesAtivos.set(chatId, new Set()); // reset participantes
+
+ await api.send(
+ `🎮 *Jogo da Forca iniciado!*\n\n` +
+ `Tema: *${sorteio.tema}*\n` +
+ `Palavra: \`${gerarProgresso(sorteio.palavra)}\`\n` +
+ `Vidas: 6\n\n` +
+ `Digite uma letra para adivinhar!`
+ );
+ return;
+ }
+
+ if (sub === "parar") {
+ jogosAtivos.delete(chatId);
+ participantesAtivos.delete(chatId);
+ await api.send("🛑 Jogo da Forca encerrado.");
+ return;
+ }
+
+ await api.send(
+ `❌ Subcomando *${sub}* não existe.\n` +
+ `Use ${CMD_PREFIX} + \`forca começar\` ou ${CMD_PREFIX} + \`forca parar\`.`
+ );
+ return;
+ }
+
+ // ── Tentativas durante o jogo
+ const jogo = jogosAtivos.get(chatId);
+ if (!jogo) return; // Nenhum jogo ativo
+
+ const tentativa = msg.body.trim().toLowerCase();
+ if (!/^[a-z]$/.test(tentativa)) return; // apenas letras simples
+
+ // Se a letra está na palavra
+ let acerto = false;
+ let novoProgresso = jogo.progresso.split("");
+ for (let i = 0; i < jogo.palavra.length; i++) {
+ if (jogo.palavra[i] === tentativa) {
+ novoProgresso[i] = tentativa;
+ acerto = true;
+ }
+ }
+ jogo.progresso = novoProgresso.join("");
+
+ if (!acerto) jogo.vidas--;
+
+ // Feedback para o grupo
+ if (jogo.progresso === jogo.palavra) {
+ await msg.reply(`🎉 Parabéns! Palavra completa: \`${jogo.palavra}\``);
+ jogosAtivos.delete(chatId);
+ participantesAtivos.delete(chatId);
+ return;
+ }
+
+ if (jogo.vidas <= 0) {
+ await msg.reply(`💀 Fim de jogo! Palavra era: \`${jogo.palavra}\``);
+ jogosAtivos.delete(chatId);
+ participantesAtivos.delete(chatId);
+ return;
+ }
+
+ await msg.reply(
+ `Palavra: \`${jogo.progresso}\`\n` +
+ `Vidas: ${jogo.vidas}\n` +
+ (acerto ? "✅ Acertou a letra!" : "❌ Errou a letra!")
+ );
+}
\ No newline at end of file
diff --git a/src/plugins/many/index.js b/src/plugins/many/index.js
new file mode 100644
index 0000000..d70f9e7
--- /dev/null
+++ b/src/plugins/many/index.js
@@ -0,0 +1,14 @@
+import { CMD_PREFIX } from "../../config.js"
+
+export default async function ({ msg, api }) {
+ if (!msg.is(CMD_PREFIX + "many")) return;
+
+ await api.send(
+ `🤖 *ManyBot — Comandos disponíveis:*\n\n` +
+ `🎬 \`${CMD_PREFIX}video \` — baixa um vídeo\n` +
+ `🎵 \`${CMD_PREFIX}audio \` — baixa um áudio\n` +
+ `🖼️ \`${CMD_PREFIX}figurinha\` — cria figurinhas\n` +
+ `🎮 \`${CMD_PREFIX}adivinhação começar|parar\` — jogo de adivinhar número\n` +
+ `🎮 \`${CMD_PREFIX}forca começar|parar\` — jogo da forca\n`
+ );
+}
\ No newline at end of file
diff --git a/src/plugins/obrigado/index.js b/src/plugins/obrigado/index.js
new file mode 100644
index 0000000..eb4dfaf
--- /dev/null
+++ b/src/plugins/obrigado/index.js
@@ -0,0 +1,8 @@
+import { CMD_PREFIX } from "../../config.js";
+const gatilhos = ["obrigado", "valeu", "brigado"];
+
+export default async function ({ msg }) {
+ if (!gatilhos.some(g => msg.is(CMD_PREFIX + g))) return;
+
+ await msg.reply("😊 Por nada!");
+}
\ No newline at end of file
diff --git a/src/plugins/video/index.js b/src/plugins/video/index.js
new file mode 100644
index 0000000..1a302eb
--- /dev/null
+++ b/src/plugins/video/index.js
@@ -0,0 +1,98 @@
+/**
+ * plugins/video/index.js
+ *
+ * Baixa vídeo via yt-dlp e envia no chat.
+ * Todo o processo (download + envio + limpeza) fica aqui.
+ */
+
+import { spawn } from "child_process";
+import fs from "fs";
+import path from "path";
+import os from "os";
+import { enqueue } from "../../download/queue.js";
+import { emptyFolder } from "../../utils/file.js";
+import { CMD_PREFIX } from "../../config.js";
+
+const logStream = fs.createWriteStream("logs/video-error.log", { flags: "a" });
+
+const DOWNLOADS_DIR = path.resolve("downloads");
+const YT_DLP = os.platform() === "win32" ? ".\\bin\\yt-dlp.exe" : "./bin/yt-dlp";
+
+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",
+ "-f", "bv+ba/best",
+];
+
+function downloadVideo(url, id) {
+ return new Promise((resolve, reject) => {
+ fs.mkdirSync(DOWNLOADS_DIR, { recursive: true });
+
+ const output = path.join(DOWNLOADS_DIR, `${id}.%(ext)s`);
+ const proc = spawn(YT_DLP, [...ARGS_BASE, "--output", output, url]);
+ let stdout = "";
+
+ proc.on("error", err => reject(new Error(
+ err.code === "EACCES"
+ ? "Sem permissão para executar o yt-dlp. Rode: chmod +x ./bin/yt-dlp"
+ : err.code === "ENOENT"
+ ? "yt-dlp não encontrado em ./bin/yt-dlp"
+ : `Erro ao iniciar o yt-dlp: ${err.message}`
+ )));
+
+ proc.stdout.on("data", d => { stdout += d.toString(); });
+ proc.stderr.on("data", d => logStream.write(d));
+
+ 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 filePath = stdout.trim().split("\n").filter(Boolean).at(-1);
+ if (!filePath || !fs.existsSync(filePath)) return reject(new Error(
+ "Download concluído mas arquivo não encontrado. Tente novamente."
+ ));
+
+ resolve(filePath);
+ });
+ });
+}
+
+export default async function ({ msg, api }) {
+ if (!msg.is(CMD_PREFIX + "video")) return;
+
+ const url = msg.args[1];
+
+ if (!url) {
+ await msg.reply(`❌ Você precisa informar um link.\n\nExemplo: \`${CMD_PREFIX}video https://youtube.com/...\``);
+ return;
+ }
+
+ await msg.reply("⏳ Baixando o vídeo, aguarde...");
+
+ enqueue(
+ async () => {
+ const filePath = await downloadVideo(url, `video-${Date.now()}`);
+ await api.sendVideo(filePath);
+ fs.unlinkSync(filePath);
+ emptyFolder(DOWNLOADS_DIR);
+ api.log.info(`${CMD_PREFIX}video concluído → ${url}`);
+ },
+ async () => {
+ await msg.reply(
+ "❌ Não consegui baixar o vídeo.\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/utils/get_id.js b/src/utils/get_id.js
index 3da624f..7e5c8e7 100644
--- a/src/utils/get_id.js
+++ b/src/utils/get_id.js
@@ -4,8 +4,9 @@
*/
import pkg from "whatsapp-web.js";
import qrcode from "qrcode-terminal";
-import { CLIENT_ID } from "../config.js";
+import { resolvePuppeteerConfig } from "../client/environment.js";
+const CLIENT_ID="getId"
const { Client, LocalAuth } = pkg;
const arg = process.argv[2];
@@ -17,7 +18,15 @@ if (!arg) {
const client = new Client({
authStrategy: new LocalAuth({ clientId: CLIENT_ID }),
- puppeteer: { headless: true },
+ puppeteer: {
+ headless: true,
+ args: [
+ '--no-sandbox',
+ '--disable-setuid-sandbox',
+ ...(resolvePuppeteerConfig().args || [])
+ ],
+ ...resolvePuppeteerConfig()
+ },
});
client.on("qr", (qr) => {
diff --git a/todo.txt b/todo.txt
deleted file mode 100644
index ad73ac4..0000000
--- a/todo.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-Possibilidade de tirar fundo de figurinhas
-
-Salvar mensagens num banco de dados
\ No newline at end of file
diff --git a/update b/update
index 0d962bc..60bbcc8 100644
--- a/update
+++ b/update
@@ -114,6 +114,64 @@ fi
log "INFO" "Instalando dependências..."
npm ci --omit=dev 2>&1 | tee -a "$log_file"
+# ------------------------
+# Download
+# ------------------------
+download_file() {
+ local url="$1"
+ local dest="$2"
+
+ log "download_file(url=$url, dest=$dest)"
+
+ log "Baixando $url"
+ log "Destino: $dest"
+
+ if command -v curl >/dev/null 2>&1; then
+ log "Downloader: curl"
+ curl -L "$url" -o "$dest"
+ elif command -v wget >/dev/null 2>&1; then
+ log "Downloader: wget"
+ wget "$url" -O "$dest"
+ else
+ log "curl ou wget não encontrados"
+ exit 1
+ fi
+
+ chmod +x "$dest" 2>/dev/null || true
+ log "Arquivo pronto: $dest"
+}
+
+# ------------------------
+# Arquivos por plataforma
+# ------------------------
+log "Selecionando dependências binárias"
+
+files=()
+if [[ "$PLATFORM" == "win" ]]; then
+ log "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 "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"
+ )
+log "Total de arquivos para baixar: ${#files[@]}"
+
+# ------------------------
+# Download
+# ------------------------
+for file in "${files[@]}"; do
+ url="${file%% *}"
+ dest="${file##* }"
+
+ log "Processando dependência"
+ download_file "$url" "$dest"
+done
+
# ------------------------------------------------------------------------------
# Restauração dos arquivos de configuração
# ------------------------------------------------------------------------------
@@ -125,7 +183,7 @@ if [ ${#backed_up[@]} -gt 0 ]; then
# Remove o que npm possa ter criado (ex: node_modules)
rm -rf "$dst"
mv "$src" "$dst"
- log "INFO" "Restaurado: $item"
+ l"INFO" "Restaurado: $item"
else
log "WARN" "Item esperado no backup não encontrado: $item"
fi