Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45323b2d3d | ||
|
|
58f5e13eb3 | ||
|
|
a6fda095d8 | ||
|
|
372f644995 | ||
|
|
c75b6249c1 | ||
|
|
f9911f6cf3 | ||
|
|
92e2ea2337 | ||
|
|
438e674eff | ||
|
|
5b74cf2dc5 | ||
|
|
4f5d937265 | ||
|
|
544dc770cd | ||
|
|
5fbe257625 | ||
|
|
e60c5819e2 | ||
|
|
04765868db | ||
|
|
cb50d4b8f8 | ||
|
|
f2e6c12af4 | ||
|
|
308db818da | ||
|
|
541f11af6d |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@ node_modules/
|
|||||||
cookies.txt
|
cookies.txt
|
||||||
bin/
|
bin/
|
||||||
mychats.txt
|
mychats.txt
|
||||||
|
manybot.conf
|
||||||
311
README.md
311
README.md
@@ -1,15 +1,308 @@
|
|||||||

|

|
||||||
|
|
||||||
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:
|
<center>
|
||||||
- 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
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
</center>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Requisitos
|
||||||
|
- Node.js
|
||||||
|
- NPM
|
||||||
|
- Sistema Linux ou Windows
|
||||||
|
|
||||||
|
obs: Sistemas Android e iOS ainda não são 100% compatíveis. O suporte para Termux está em fases de testes e sem garantia de funcionamento correto.
|
||||||
|
|
||||||
|
# Instalação (Linux)
|
||||||
|
|
||||||
|
1. Clone o repositório e entre:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/synt-xerror/manybot
|
||||||
|
cd manybot
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Crie e abra o arquivo de configuração (use o editor de sua preferência):
|
||||||
|
```bash
|
||||||
|
touch manybot.conf
|
||||||
|
nano manybot.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
]
|
||||||
|
```
|
||||||
|
- **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.
|
||||||
|
|
||||||
|
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. 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
|
||||||
|
Role a tela e selecione "Instalador Windows (.msi)"
|
||||||
|
Ou se preferir, use um gerenciador de pacotes como mostra no conteúdo inicial
|
||||||
|
|
||||||
|
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 <link>`)
|
||||||
|
- **audio** — baixa o áudio de um vídeo e envia como mensagem de voz (`!audio <link>`)
|
||||||
|
- **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
|
||||||
25
deploy.sh
25
deploy.sh
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# development tool
|
# ferramenta de desenvolvimento apenas, pode apagar se quiser
|
||||||
# ./deploy <commit> <branch> <version (if branch=master)>
|
# ./deploy <commit> <branch> <version (if branch=master)>
|
||||||
|
|
||||||
COMMIT_MSG="$1"
|
COMMIT_MSG="$1"
|
||||||
@@ -12,25 +12,9 @@ if [ -z "$COMMIT_MSG" ] || [ -z "$BRANCH" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
# mudar para a branch
|
||||||
git checkout $BRANCH || { echo "Error ao change to $BRANCH"; exit 1; }
|
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
|
# se for master, atualizar versão
|
||||||
if [ "$BRANCH" == "master" ] && [ -n "$VERSION" ]; then
|
if [ "$BRANCH" == "master" ] && [ -n "$VERSION" ]; then
|
||||||
echo "Updating version to $VERSION"
|
echo "Updating version to $VERSION"
|
||||||
@@ -41,4 +25,11 @@ if [ "$BRANCH" == "master" ] && [ -n "$VERSION" ]; then
|
|||||||
git push origin $VERSION
|
git push origin $VERSION
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# adicionar alterações e commit
|
||||||
|
git add .
|
||||||
|
git commit -m "$COMMIT_MSG"
|
||||||
|
|
||||||
|
# push
|
||||||
|
git push origin $BRANCH
|
||||||
|
|
||||||
echo "Deploy completed."
|
echo "Deploy completed."
|
||||||
BIN
examples/figurinha.gif
Normal file
BIN
examples/figurinha.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 620 KiB |
8
package-lock.json
generated
8
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "whatsapp-bot",
|
"name": "manybot",
|
||||||
"version": "2.2.0",
|
"version": "2.4.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "whatsapp-bot",
|
"name": "manybot",
|
||||||
"version": "2.2.0",
|
"version": "2.4.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"node-addon-api": "^7",
|
"node-addon-api": "^7",
|
||||||
"node-gyp": "^12.2.0",
|
"node-gyp": "^12.2.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "whatsapp-bot",
|
"name": "manybot",
|
||||||
"version": "2.2.0",
|
"version": "2.4.2",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"node-addon-api": "^7",
|
"node-addon-api": "^7",
|
||||||
|
|||||||
15
setup
15
setup
@@ -1,6 +1,9 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
# Salvando diretório para evitar problemas
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
# ------------------------
|
# ------------------------
|
||||||
# Cores
|
# Cores
|
||||||
# ------------------------
|
# ------------------------
|
||||||
@@ -48,8 +51,6 @@ cat << "EOF"
|
|||||||
website: www.mlplovers.com.br/manybot
|
website: www.mlplovers.com.br/manybot
|
||||||
repo: github.com/synt-xerror/manybot
|
repo: github.com/synt-xerror/manybot
|
||||||
|
|
||||||
A Amizade é Mágica!
|
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
echo -e "${RESET}"
|
echo -e "${RESET}"
|
||||||
}
|
}
|
||||||
@@ -161,6 +162,13 @@ log_info "Instalando dependências npm"
|
|||||||
export PUPPETEER_SKIP_DOWNLOAD=1
|
export PUPPETEER_SKIP_DOWNLOAD=1
|
||||||
run_cmd npm install
|
run_cmd npm install
|
||||||
|
|
||||||
|
# ------------------------
|
||||||
|
# Chrome Puppeeter
|
||||||
|
# ------------------------
|
||||||
|
log_info "Instalando Chrome"
|
||||||
|
|
||||||
|
npx puppeteer browsers install chrome
|
||||||
|
|
||||||
# ------------------------
|
# ------------------------
|
||||||
# Diretórios
|
# Diretórios
|
||||||
# ------------------------
|
# ------------------------
|
||||||
@@ -188,6 +196,7 @@ else
|
|||||||
)
|
)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
chmod +x bin/*
|
||||||
log_debug "Total de arquivos para baixar: ${#files[@]}"
|
log_debug "Total de arquivos para baixar: ${#files[@]}"
|
||||||
|
|
||||||
# ------------------------
|
# ------------------------
|
||||||
@@ -201,4 +210,4 @@ for file in "${files[@]}"; do
|
|||||||
download_file "$url" "$dest"
|
download_file "$url" "$dest"
|
||||||
done
|
done
|
||||||
|
|
||||||
log_ok "Setup concluído com sucesso"
|
log_ok "Setup concluído com sucesso.\nRode sempre na raíz: 'node src/main.js' (ou equivalente) para rodar o bot."
|
||||||
48
src/client/banner.js
Normal file
48
src/client/banner.js
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { c } from "../logger/formatter.js";
|
||||||
|
|
||||||
|
export function printBanner() {
|
||||||
|
|
||||||
|
const banner = [
|
||||||
|
` _____ _____ _ `,
|
||||||
|
` | |___ ___ _ _| __ |___| |_ `,
|
||||||
|
` | | | | . | | | | __ -| . | _|`,
|
||||||
|
` |_|_|_|__,|_|_|_ |_____|___|_| `,
|
||||||
|
` |___| `
|
||||||
|
];
|
||||||
|
|
||||||
|
const pony = [
|
||||||
|
` ⠴⢮⠭⠍⠉⠉⠒⠤⣀`,
|
||||||
|
` ⢀⢊ ⢱⠊⠑⡀`,
|
||||||
|
` ⠋⡎ ⣀⡠⠤⠠⠖⠋⢉⠉ ⡄⢸`,
|
||||||
|
` ⣘⡠⠊⣩⡅ ⣴⡟⣯⠙⣊ ⢁⠜`,
|
||||||
|
` ⣿⡇⢸⣿⣷⡿⢀⠇⢀⢎`,
|
||||||
|
` ⠰⡉ ⠈⠛⠛⠋⠁⢀⠜ ⢂`,
|
||||||
|
` ⠈⠒⠒⡲⠂⣠⣔⠁ ⡇ ⢀⡴⣾⣛⡛⠻⣦`,
|
||||||
|
` ⢠⠃ ⢠⠞ ⡸⠉⠲⣿⠿⢿⣿⣿⣷⡌⢷`,
|
||||||
|
` ⢀⠔⠂⢼ ⡎⡔⡄⠰⠃ ⢣ ⢻⣿⣿⣿⠘⣷`,
|
||||||
|
` ⡐⠁ ⠸⡀ ⠏ ⠈⠃ ⢸ ⣿⣿⣿⡇⣿⡇`,
|
||||||
|
` ⡇ ⡎⠉⠉⢳ ⡤⠤⡤⠲⡀ ⢇ ⣿⣿⣿⣇⣿⣷`,
|
||||||
|
` ⡇ ⡠⠃ ⡸ ⡇ ⡇ ⢱⡀ ⢣ ⣿⣿⣿⣿⣿⡄`,
|
||||||
|
` ⠑⠊ ⢰ ⠇ ⢸ ⡇⡇ ⡇ ⢳⣿⣿⣿⣿⡇`,
|
||||||
|
` ⢠⠃ ⡸ ⡎ ⡜⡇ ⡇ ⠻⡏⠻⣿⣿⣄`,
|
||||||
|
` ⣔⣁⣀⣀⡠⠁ ⠈⠉⠉⠁⣎⣀⣀⣀⡸`
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
console.log(`${c.blue}${c.bold}`);
|
||||||
|
|
||||||
|
const max = Math.max(banner.length, pony.length);
|
||||||
|
|
||||||
|
for (let i = 0; i < max; i++) {
|
||||||
|
const left = banner[i] || " ".repeat(banner[0].length);
|
||||||
|
const right = pony[i] || "";
|
||||||
|
console.log(left + " " + right);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log();
|
||||||
|
console.log(` website : ${c.reset}${c.cyan}www.mlplovers.com.br/manybot${c.reset}`);
|
||||||
|
console.log(` repo : ${c.reset}${c.cyan}github.com/synt-xerror/manybot${c.reset}`);
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
|
||||||
25
src/client/environment.js
Normal file
25
src/client/environment.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import os from "os";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detecta se o processo está rodando dentro do Termux.
|
||||||
|
*/
|
||||||
|
export const isTermux =
|
||||||
|
(os.platform() === "linux" || os.platform() === "android") &&
|
||||||
|
process.env.PREFIX?.startsWith("/data/data/com.termux");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna a config de Puppeteer adequada ao ambiente.
|
||||||
|
* @returns {import("puppeteer").LaunchOptions}
|
||||||
|
*/
|
||||||
|
export function resolvePuppeteerConfig() {
|
||||||
|
if (!isTermux) return {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
executablePath: "/data/data/com.termux/files/usr/bin/chromium-browser",
|
||||||
|
args: [
|
||||||
|
"--headless=new", "--no-sandbox", "--disable-setuid-sandbox",
|
||||||
|
"--disable-dev-shm-usage", "--disable-gpu", "--single-process",
|
||||||
|
"--no-zygote", "--disable-software-rasterizer",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
25
src/client/qrHandler.js
Normal file
25
src/client/qrHandler.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import qrcode from "qrcode-terminal";
|
||||||
|
import path from "path";
|
||||||
|
import { logger } from "../logger/logger.js";
|
||||||
|
import { isTermux } from "./environment.js";
|
||||||
|
|
||||||
|
const QR_PATH = path.resolve("qr.png");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exibe ou salva o QR Code conforme o ambiente.
|
||||||
|
* @param {string} qr — string bruta do evento "qr"
|
||||||
|
*/
|
||||||
|
export async function handleQR(qr) {
|
||||||
|
if (isTermux) {
|
||||||
|
try {
|
||||||
|
await QRCode.toFile(QR_PATH, qr, { width: 400 });
|
||||||
|
logger.info(`QR Code salvo em: ${QR_PATH}`);
|
||||||
|
logger.info(`Abra com: termux-open qr.png`);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error("Falha ao salvar QR Code:", err.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.info("Escaneie o QR Code abaixo:");
|
||||||
|
qrcode.generate(qr, { small: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,100 +1,46 @@
|
|||||||
import pkg from "whatsapp-web.js";
|
import pkg from "whatsapp-web.js";
|
||||||
import qrcode from "qrcode-terminal";
|
import { CLIENT_ID } from "../config.js";
|
||||||
import { exec } from "child_process";
|
import { logger } from "../logger/logger.js";
|
||||||
import { CLIENT_ID } from "../config.js";
|
import { isTermux, resolvePuppeteerConfig } from "./environment.js";
|
||||||
import os from "os";
|
import { handleQR } from "./qrHandler.js";
|
||||||
|
import { printBanner } from "./banner.js";
|
||||||
|
|
||||||
export const { Client, LocalAuth, MessageMedia } = pkg;
|
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 ─────────────────────────────────────────────────
|
// ── Ambiente ─────────────────────────────────────────────────
|
||||||
const isTermux =
|
|
||||||
(os.platform() === "linux" || os.platform() === "android") &&
|
|
||||||
process.env.PREFIX?.startsWith("/data/data/com.termux");
|
|
||||||
|
|
||||||
logger.info(isTermux
|
logger.info(isTermux
|
||||||
? `Ambiente: ${c.yellow}${c.bold}Termux${c.reset} — usando Chromium do sistema`
|
? "Ambiente: Termux — usando Chromium do sistema"
|
||||||
: `Ambiente: ${c.blue}${c.bold}${os.platform()}${c.reset} — usando Puppeteer padrão`
|
: `Ambiente: ${process.platform} — usando Puppeteer padrão`
|
||||||
);
|
);
|
||||||
|
|
||||||
const puppeteerConfig = isTermux
|
// ── Instância ─────────────────────────────────────────────────
|
||||||
? {
|
|
||||||
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({
|
export const client = new Client({
|
||||||
authStrategy: new LocalAuth({ clientId: CLIENT_ID }),
|
authStrategy: new LocalAuth({ clientId: CLIENT_ID }),
|
||||||
puppeteer: { headless: true, ...puppeteerConfig },
|
puppeteer: {
|
||||||
|
headless: true,
|
||||||
|
args: [
|
||||||
|
'--no-sandbox',
|
||||||
|
'--disable-setuid-sandbox',
|
||||||
|
...(resolvePuppeteerConfig().args || [])
|
||||||
|
],
|
||||||
|
...resolvePuppeteerConfig()
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on("qr", async qr => {
|
// ── Eventos ───────────────────────────────────────────────────
|
||||||
if (isTermux) {
|
client.on("qr", handleQR);
|
||||||
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", () => {
|
client.on("ready", () => {
|
||||||
exec("clear");
|
|
||||||
printBanner();
|
printBanner();
|
||||||
logger.success(`${c.green}${c.bold}WhatsApp conectado e pronto!${c.reset}`);
|
logger.success("WhatsApp conectado e pronto!");
|
||||||
logger.info(`Client ID: ${c.cyan}${CLIENT_ID}${c.reset}`);
|
logger.info(`Client ID: ${CLIENT_ID}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on("disconnected", reason => {
|
client.on("disconnected", (reason) => {
|
||||||
logger.warn(`Desconectado — motivo: ${c.yellow}${reason}${c.reset}`);
|
logger.warn(`Desconectado — motivo: ${reason}`);
|
||||||
logger.info(`Reconectando em ${c.cyan}5s${c.reset}...`);
|
logger.info("Reconectando em 5s...");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
logger.bot("Reinicializando cliente...");
|
logger.info("Reinicializando cliente...");
|
||||||
client.initialize();
|
client.initialize();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,256 +0,0 @@
|
|||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import os from "os";
|
|
||||||
import { execFile } from "child_process";
|
|
||||||
import { promisify } from "util";
|
|
||||||
|
|
||||||
import pkg from "whatsapp-web.js";
|
|
||||||
import { createSticker } from "wa-sticker-formatter";
|
|
||||||
|
|
||||||
import { client } from "../client/whatsappClient.js";
|
|
||||||
import { botMsg } from "../utils/botMsg.js";
|
|
||||||
import { emptyFolder } from "../utils/file.js";
|
|
||||||
import { stickerSessions } from "./index.js";
|
|
||||||
|
|
||||||
const { MessageMedia } = pkg;
|
|
||||||
const execFileAsync = promisify(execFile);
|
|
||||||
|
|
||||||
const DOWNLOADS_DIR = path.resolve("downloads");
|
|
||||||
const FFMPEG = os.platform() === "win32"
|
|
||||||
? ".\\bin\\ffmpeg.exe"
|
|
||||||
: "./bin/ffmpeg";
|
|
||||||
|
|
||||||
const MAX_STICKER_SIZE = 900 * 1024;
|
|
||||||
const SESSION_TIMEOUT = 2 * 60 * 1000;
|
|
||||||
const MAX_MEDIA = 10;
|
|
||||||
|
|
||||||
// ───────────────── Helpers ─────────────────
|
|
||||||
|
|
||||||
function ensureDownloadsDir() {
|
|
||||||
if (!fs.existsSync(DOWNLOADS_DIR)) {
|
|
||||||
fs.mkdirSync(DOWNLOADS_DIR, { recursive: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanupFiles(...files) {
|
|
||||||
for (const f of files) {
|
|
||||||
if (f && fs.existsSync(f)) fs.unlinkSync(f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Converte vídeo/gif → GIF 512x512 com paleta preservada
|
|
||||||
async function convertVideoToGif(inputPath, outputPath, fps = 12) {
|
|
||||||
|
|
||||||
const clampedFps = Math.min(fps, 12);
|
|
||||||
|
|
||||||
const filter = [
|
|
||||||
`fps=${clampedFps},scale=512:512:flags=lanczos,split[s0][s1]`,
|
|
||||||
`[s0]palettegen=max_colors=256:reserve_transparent=1[p]`,
|
|
||||||
`[s1][p]paletteuse=dither=bayer`
|
|
||||||
].join(";");
|
|
||||||
|
|
||||||
await execFileAsync(FFMPEG, [
|
|
||||||
"-i", inputPath,
|
|
||||||
"-filter_complex", filter, // <-- 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");
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
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(botMsg("Já existe uma sessão ativa."));
|
|
||||||
}
|
|
||||||
|
|
||||||
iniciarSessao(chatId, author);
|
|
||||||
|
|
||||||
await msg.reply(botMsg(
|
|
||||||
`Sessão de figurinha iniciada por @${author.split("@")[0]}. Envie no máximo 10 imagens, quando estiver pronto mande \`!figurinha criar\``,
|
|
||||||
null,
|
|
||||||
{ mentions: [author] }
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
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 + "`"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
import { botMsg } from "../utils/botMsg.js";
|
|
||||||
|
|
||||||
export async function processarInfo(cmd, chat) {
|
|
||||||
switch(cmd) {
|
|
||||||
case "ping":
|
|
||||||
await chat.sendMessage(botMsg("> `!ping`\nResponde pong."));
|
|
||||||
break;
|
|
||||||
case "video":
|
|
||||||
await chat.sendMessage(botMsg("> `!video <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.`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,71 @@
|
|||||||
export const CLIENT_ID = "bot_permanente";
|
/**
|
||||||
export const BOT_PREFIX = "🤖 *ManyBot:* ";
|
* config.js
|
||||||
export const CHATS = [
|
*
|
||||||
// coloque os chats que quer aqui
|
* Lê e parseia o manybot.conf.
|
||||||
];
|
* Suporta listas multilinhas e comentários inline.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
function parseConf(raw) {
|
||||||
|
const lines = raw.split("\n");
|
||||||
|
|
||||||
|
const cleaned = [];
|
||||||
|
let insideList = false;
|
||||||
|
let buffer = "";
|
||||||
|
|
||||||
|
for (let line of lines) {
|
||||||
|
line = line.replace(/#.*$/, "").trim();
|
||||||
|
if (!line) continue;
|
||||||
|
|
||||||
|
if (!insideList) {
|
||||||
|
if (line.includes("=[") && !line.includes("]")) {
|
||||||
|
insideList = true;
|
||||||
|
buffer = line;
|
||||||
|
} else {
|
||||||
|
cleaned.push(line);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buffer += line;
|
||||||
|
if (line.includes("]")) {
|
||||||
|
insideList = false;
|
||||||
|
cleaned.push(buffer);
|
||||||
|
buffer = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {};
|
||||||
|
for (const line of cleaned) {
|
||||||
|
const eqIdx = line.indexOf("=");
|
||||||
|
if (eqIdx === -1) continue;
|
||||||
|
|
||||||
|
const key = line.slice(0, eqIdx).trim();
|
||||||
|
const raw = line.slice(eqIdx + 1).trim();
|
||||||
|
|
||||||
|
if (raw.startsWith("[") && raw.endsWith("]")) {
|
||||||
|
result[key] = raw
|
||||||
|
.slice(1, -1)
|
||||||
|
.split(",")
|
||||||
|
.map(x => x.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
} else {
|
||||||
|
result[key] = raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const raw = fs.readFileSync("manybot.conf", "utf8");
|
||||||
|
const config = parseConf(raw);
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { get_video } from "./video.js";
|
|
||||||
import { spawn } from "child_process";
|
|
||||||
import os from "os";
|
|
||||||
|
|
||||||
const so = os.platform();
|
|
||||||
|
|
||||||
export async function get_audio(url, id) {
|
|
||||||
const video = await get_video(url, id);
|
|
||||||
const output = `downloads/${id}.mp3`;
|
|
||||||
const cmd = so === "win32" ? ".\\bin\\ffmpeg.exe" : "./bin/ffmpeg";
|
|
||||||
const args = ['-i', video, '-vn', '-acodec', 'libmp3lame', '-q:a', '2', output];
|
|
||||||
|
|
||||||
await runCmd(cmd, args);
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function runCmd(cmd, args) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const proc = spawn(cmd, args);
|
|
||||||
proc.stdout.on("data", data => console.log("[cmd]", data.toString()));
|
|
||||||
proc.stderr.on("data", data => console.error("[cmd ERR]", data.toString()));
|
|
||||||
proc.on("close", code => code === 0 ? resolve() : reject(new Error("Processo saiu com código "+code)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,42 +1,54 @@
|
|||||||
import { get_video } from "./video.js";
|
/**
|
||||||
import { get_audio } from "./audio.js";
|
* src/download/queue.js
|
||||||
import pkg from "whatsapp-web.js";
|
*
|
||||||
const { MessageMedia } = pkg;
|
* Fila de execução sequencial para jobs pesados (downloads, conversões).
|
||||||
import fs from "fs";
|
* Garante que apenas um job roda por vez — sem sobrecarregar yt-dlp ou ffmpeg.
|
||||||
import { botMsg } from "../utils/botMsg.js";
|
*
|
||||||
import { emptyFolder } from "../utils/file.js";
|
* O plugin passa uma `workFn` que faz tudo: baixar, converter, enviar.
|
||||||
import client from "../client/whatsappClient.js";
|
* 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);
|
||||||
|
*/
|
||||||
|
|
||||||
let downloadQueue = [];
|
import { logger } from "../logger/logger.js";
|
||||||
let processingQueue = false;
|
|
||||||
|
|
||||||
export function enqueueDownload(type, url, msg, chatId) {
|
/**
|
||||||
downloadQueue.push({ type, url, msg, chatId });
|
* @typedef {{
|
||||||
if (!processingQueue) processQueue();
|
* workFn: () => Promise<void>,
|
||||||
|
* errorFn: (err: Error) => Promise<void>,
|
||||||
|
* }} Job
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** @type {Job[]} */
|
||||||
|
let queue = [];
|
||||||
|
let processing = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adiciona um job à fila e inicia o processamento se estiver idle.
|
||||||
|
*
|
||||||
|
* @param {Function} workFn — async () => void — toda a lógica do plugin
|
||||||
|
* @param {Function} errorFn — async (err) => void — chamado se workFn lançar
|
||||||
|
*/
|
||||||
|
export function enqueue(workFn, errorFn) {
|
||||||
|
queue.push({ workFn, errorFn });
|
||||||
|
if (!processing) processQueue();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processQueue() {
|
async function processQueue() {
|
||||||
processingQueue = true;
|
processing = true;
|
||||||
while (downloadQueue.length) {
|
while (queue.length) {
|
||||||
const job = downloadQueue.shift();
|
await processJob(queue.shift());
|
||||||
try {
|
}
|
||||||
let path;
|
processing = false;
|
||||||
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);
|
|
||||||
|
async function processJob({ workFn, errorFn }) {
|
||||||
const file = fs.readFileSync(path);
|
try {
|
||||||
const media = new MessageMedia(
|
await workFn();
|
||||||
job.type === "video" ? "video/mp4" : "audio/mpeg",
|
} catch (err) {
|
||||||
file.toString("base64"),
|
logger.error(`Falha no job — ${err.message}`);
|
||||||
path.split("/").pop()
|
try { await errorFn(err); } catch { }
|
||||||
);
|
}
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { spawn } from "child_process";
|
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import os from "os";
|
|
||||||
|
|
||||||
const platform = os.platform();
|
|
||||||
|
|
||||||
export async function get_video(url, id) {
|
|
||||||
// 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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { botMsg } from "../utils/botMsg.js";
|
|
||||||
|
|
||||||
let jogoAtivo = null;
|
|
||||||
|
|
||||||
export function iniciarJogo() {
|
|
||||||
jogoAtivo = Math.floor(Math.random()*100)+1;
|
|
||||||
return jogoAtivo;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function pararJogo() {
|
|
||||||
jogoAtivo = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function processarJogo(msg, chat) {
|
|
||||||
if (!jogoAtivo) return;
|
|
||||||
|
|
||||||
const tentativa = msg.body.trim();
|
|
||||||
if (!/^\d+$/.test(tentativa)) return;
|
|
||||||
|
|
||||||
const num = parseInt(tentativa);
|
|
||||||
if (num === jogoAtivo) {
|
|
||||||
await msg.reply(botMsg(`Acertou! Número: ${jogoAtivo}`));
|
|
||||||
pararJogo();
|
|
||||||
} else if (num > jogoAtivo) {
|
|
||||||
await chat.sendMessage(botMsg("Menor."));
|
|
||||||
} else {
|
|
||||||
await chat.sendMessage(botMsg("Maior."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
43
src/kernel/messageHandler.js
Normal file
43
src/kernel/messageHandler.js
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* messageHandler.js
|
||||||
|
*
|
||||||
|
* Pipeline central de uma mensagem recebida.
|
||||||
|
*
|
||||||
|
* Ordem:
|
||||||
|
* 1. Filtra chats não permitidos (CHATS do .conf)
|
||||||
|
* — se CHATS estiver vazio, aceita todos os chats
|
||||||
|
* 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, BOT_PREFIX } 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";
|
||||||
|
import client from "../client/whatsappClient.js";
|
||||||
|
|
||||||
|
export async function handleMessage(msg) {
|
||||||
|
const chat = await msg.getChat();
|
||||||
|
const chatId = getChatId(chat);
|
||||||
|
|
||||||
|
// CHATS vazio = aceita todos os chats
|
||||||
|
if (CHATS.length > 0 && !CHATS.includes(chatId)) return;
|
||||||
|
|
||||||
|
const ctx = await buildMessageContext(msg, chat, BOT_PREFIX);
|
||||||
|
logger.msg(ctx);
|
||||||
|
|
||||||
|
const api = buildApi({ msg, chat, client, pluginRegistry });
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
241
src/kernel/pluginApi.js
Normal file
241
src/kernel/pluginApi.js
Normal file
@@ -0,0 +1,241 @@
|
|||||||
|
/**
|
||||||
|
* 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<string, any>} params.pluginRegistry
|
||||||
|
* @returns {object} api
|
||||||
|
*/
|
||||||
|
export function buildApi({ msg, chat, client, pluginRegistry }) {
|
||||||
|
|
||||||
|
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<import("whatsapp-web.js").Message|null>}
|
||||||
|
*/
|
||||||
|
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 });
|
||||||
|
},
|
||||||
|
|
||||||
|
// ── Envio para chat específico ───────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envia texto para um chat específico por ID.
|
||||||
|
* @param {string} chatId
|
||||||
|
* @param {string} text
|
||||||
|
*/
|
||||||
|
async sendTo(chatId, text) {
|
||||||
|
return client.sendMessage(chatId, text);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envia vídeo para um chat específico por ID.
|
||||||
|
* @param {string} chatId
|
||||||
|
* @param {string} filePath
|
||||||
|
* @param {string} [caption]
|
||||||
|
*/
|
||||||
|
async sendVideoTo(chatId, filePath, caption = "") {
|
||||||
|
const media = MessageMedia.fromFilePath(filePath);
|
||||||
|
return client.sendMessage(chatId, media, { caption });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envia áudio para um chat específico por ID.
|
||||||
|
* @param {string} chatId
|
||||||
|
* @param {string} filePath
|
||||||
|
*/
|
||||||
|
async sendAudioTo(chatId, filePath) {
|
||||||
|
const media = MessageMedia.fromFilePath(filePath);
|
||||||
|
return client.sendMessage(chatId, media, { sendAudioAsVoice: true });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envia imagem para um chat específico por ID.
|
||||||
|
* @param {string} chatId
|
||||||
|
* @param {string} filePath
|
||||||
|
* @param {string} [caption]
|
||||||
|
*/
|
||||||
|
async sendImageTo(chatId, filePath, caption = "") {
|
||||||
|
const media = MessageMedia.fromFilePath(filePath);
|
||||||
|
return client.sendMessage(chatId, media, { caption });
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envia figurinha para um chat específico por ID.
|
||||||
|
* @param {string} chatId
|
||||||
|
* @param {string | Buffer} source
|
||||||
|
*/
|
||||||
|
async sendStickerTo(chatId, source) {
|
||||||
|
const media = typeof source === "string"
|
||||||
|
? MessageMedia.fromFilePath(source)
|
||||||
|
: new MessageMedia("image/webp", source.toString("base64"));
|
||||||
|
return client.sendMessage(chatId, 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),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
36
src/kernel/pluginGuard.js
Normal file
36
src/kernel/pluginGuard.js
Normal file
@@ -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() ?? ""}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
88
src/kernel/pluginLoader.js
Normal file
88
src/kernel/pluginLoader.js
Normal file
@@ -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<string, object>}
|
||||||
|
*/
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/kernel/scheduler.js
Normal file
47
src/kernel/scheduler.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
34
src/logger/formatter.js
Normal file
34
src/logger/formatter.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// ── Paleta ANSI ──────────────────────────────────────────────
|
||||||
|
export const c = {
|
||||||
|
reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
|
||||||
|
green: "\x1b[32m", yellow: "\x1b[33m", cyan: "\x1b[36m",
|
||||||
|
red: "\x1b[31m", gray: "\x1b[90m", white: "\x1b[37m",
|
||||||
|
blue: "\x1b[34m", magenta: "\x1b[35m",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const now = () =>
|
||||||
|
new Date().toLocaleString("pt-BR", { dateStyle: "short", timeStyle: "medium" });
|
||||||
|
|
||||||
|
export const formatType = (type) => ({
|
||||||
|
sticker: `${c.magenta}sticker${c.reset}`,
|
||||||
|
image: `${c.cyan}imagem${c.reset}`,
|
||||||
|
video: `${c.cyan}vídeo${c.reset}`,
|
||||||
|
audio: `${c.cyan}áudio${c.reset}`,
|
||||||
|
ptt: `${c.cyan}áudio${c.reset}`,
|
||||||
|
document: `${c.cyan}arquivo${c.reset}`,
|
||||||
|
chat: `${c.white}texto${c.reset}`,
|
||||||
|
}[type] ?? `${c.gray}${type}${c.reset}`);
|
||||||
|
|
||||||
|
export const formatContext = (chatName, isGroup) =>
|
||||||
|
isGroup
|
||||||
|
? `${c.bold}${chatName}${c.reset} ${c.dim}(grupo)${c.reset}`
|
||||||
|
: `${c.bold}${chatName}${c.reset} ${c.dim}(privado)${c.reset}`;
|
||||||
|
|
||||||
|
export const formatBody = (body) =>
|
||||||
|
body?.trim()
|
||||||
|
? `${c.green}"${body.length > 200 ? body.slice(0, 200) + "..." : body}"${c.reset}`
|
||||||
|
: `${c.dim}<mídia>${c.reset}`;
|
||||||
|
|
||||||
|
export const formatReply = (quotedName, quotedNumber, quotedPreview) =>
|
||||||
|
`\n${c.gray} ↩ Para: ${c.reset}${c.white}${quotedName}${c.reset} ${c.dim}+${quotedNumber}${c.reset}` +
|
||||||
|
`\n${c.gray} ↩ Msg: ${c.reset}${c.dim}${quotedPreview}${c.reset}`;
|
||||||
40
src/logger/logger.js
Normal file
40
src/logger/logger.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import {
|
||||||
|
c, now,
|
||||||
|
formatType, formatContext, formatBody, formatReply,
|
||||||
|
} from "./formatter.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logger central do ManyBot.
|
||||||
|
* Cada método lida apenas com saída — sem lógica de negócio ou I/O externo.
|
||||||
|
*/
|
||||||
|
export const logger = {
|
||||||
|
info: (...a) => console.log(`${c.gray}[${now()}]${c.reset} ${c.cyan}INFO ${c.reset}`, ...a),
|
||||||
|
success: (...a) => console.log(`${c.gray}[${now()}]${c.reset} ${c.green}OK ${c.reset}`, ...a),
|
||||||
|
warn: (...a) => console.log(`${c.gray}[${now()}]${c.reset} ${c.yellow}WARN ${c.reset}`, ...a),
|
||||||
|
error: (...a) => console.log(`${c.gray}[${now()}]${c.reset} ${c.red}ERROR ${c.reset}`, ...a),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loga uma mensagem recebida a partir de um contexto já resolvido.
|
||||||
|
* @param {import("./messageContext.js").MessageContext} ctx
|
||||||
|
*/
|
||||||
|
msg(ctx) {
|
||||||
|
const { chatName, isGroup, senderName, senderNumber, type, body, 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 = "") =>
|
||||||
|
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}` : "")
|
||||||
|
),
|
||||||
|
};
|
||||||
81
src/logger/messageContext.js
Normal file
81
src/logger/messageContext.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import client from "../client/whatsappClient.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrai o número limpo de uma mensagem.
|
||||||
|
* @param {import("whatsapp-web.js").Message} msg
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
export async function getNumber(msg) {
|
||||||
|
if (msg.fromMe) return String(msg.from).split("@")[0];
|
||||||
|
const contact = await msg.getContact();
|
||||||
|
return contact.number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monta o contexto completo de uma mensagem para logging.
|
||||||
|
* Resolve contato, quoted message e metadados do chat.
|
||||||
|
*
|
||||||
|
* @param {import("whatsapp-web.js").Message} msg
|
||||||
|
* @param {import("whatsapp-web.js").Chat} chat
|
||||||
|
* @param {string} botPrefix
|
||||||
|
* @returns {Promise<MessageContext>}
|
||||||
|
*
|
||||||
|
* @typedef {Object} MessageContext
|
||||||
|
* @property {string} chatName
|
||||||
|
* @property {string} chatId
|
||||||
|
* @property {boolean} isGroup
|
||||||
|
* @property {string} senderName
|
||||||
|
* @property {string} senderNumber
|
||||||
|
* @property {string} type
|
||||||
|
* @property {string} body
|
||||||
|
* @property {{ name: string, number: string, preview: string } | null} quoted
|
||||||
|
*/
|
||||||
|
export async function buildMessageContext(msg, chat, botPrefix) {
|
||||||
|
const chatId = chat.id._serialized;
|
||||||
|
const isGroup = /@g\.us$/.test(chatId);
|
||||||
|
const number = await getNumber(msg);
|
||||||
|
const name = msg._data?.notifyName || String(msg.from).replace(/(:\d+)?@.*$/, "");
|
||||||
|
|
||||||
|
const quoted = await resolveQuotedMessage(msg);
|
||||||
|
|
||||||
|
return {
|
||||||
|
chatName: chat.name || chat.id.user,
|
||||||
|
chatId,
|
||||||
|
isGroup,
|
||||||
|
senderName: name,
|
||||||
|
senderNumber: number,
|
||||||
|
type: msg?.type || "text",
|
||||||
|
body: msg.body,
|
||||||
|
quoted,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve os dados da mensagem citada, se existir.
|
||||||
|
* Retorna null em caso de erro ou ausência.
|
||||||
|
*
|
||||||
|
* @param {import("whatsapp-web.js").Message} msg
|
||||||
|
* @returns {Promise<{ name: string, number: string, preview: string } | null>}
|
||||||
|
*/
|
||||||
|
async function resolveQuotedMessage(msg) {
|
||||||
|
if (!msg?.hasQuotedMsg) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const quoted = await msg.getQuotedMessage();
|
||||||
|
const quotedNumber = String(quoted.from).split("@")[0];
|
||||||
|
|
||||||
|
let quotedName = quotedNumber;
|
||||||
|
try {
|
||||||
|
const contact = await client.getContactById(quoted.from);
|
||||||
|
quotedName = contact?.pushname || contact?.formattedName || quotedNumber;
|
||||||
|
} catch { /* contato não encontrado — usa o número */ }
|
||||||
|
|
||||||
|
const quotedPreview = quoted.body?.trim()
|
||||||
|
? `"${quoted.body.length > 80 ? quoted.body.slice(0, 80) + "…" : quoted.body}"`
|
||||||
|
: `<${quoted.type}>`;
|
||||||
|
|
||||||
|
return { name: quotedName, number: quotedNumber, preview: quotedPreview };
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
142
src/main.js
142
src/main.js
@@ -1,127 +1,34 @@
|
|||||||
import client from "./client/whatsappClient.js";
|
/**
|
||||||
import { CHATS, BOT_PREFIX } from "./config.js"; // <- importar PREFIX
|
* main.js
|
||||||
import { processarComando } from "./commands/index.js";
|
*
|
||||||
import { coletarMidia } from "./commands/figurinha.js";
|
* Ponto de entrada do ManyBot.
|
||||||
import { processarJogo } from "./games/adivinhacao.js";
|
* Inicializa o cliente WhatsApp e carrega os plugins.
|
||||||
import { getChatId } from "./utils/getChatId.js";
|
*/
|
||||||
|
|
||||||
// ── Cores ────────────────────────────────────────────────────
|
import client from "./client/whatsappClient.js";
|
||||||
const c = {
|
import { handleMessage } from "./kernel/messageHandler.js";
|
||||||
reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
|
import { loadPlugins } from "./kernel/pluginLoader.js";
|
||||||
green: "\x1b[32m", yellow: "\x1b[33m", cyan: "\x1b[36m",
|
import { logger } from "./logger/logger.js";
|
||||||
red: "\x1b[31m", gray: "\x1b[90m", white: "\x1b[37m",
|
import { PLUGINS } from "./config.js";
|
||||||
blue: "\x1b[34m", magenta: "\x1b[35m",
|
|
||||||
};
|
|
||||||
|
|
||||||
const now = () =>
|
logger.info("Iniciando ManyBot...\n");
|
||||||
new Date().toLocaleString("pt-BR", { dateStyle: "short", timeStyle: "medium" });
|
|
||||||
|
|
||||||
const SEP = `${c.gray}${"─".repeat(52)}${c.reset}`;
|
// 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() ?? ""}`);
|
||||||
|
});
|
||||||
|
|
||||||
// ── Logger ───────────────────────────────────────────────────
|
process.on("unhandledRejection", (reason) => {
|
||||||
const logger = {
|
const msg = reason instanceof Error ? reason.message : String(reason);
|
||||||
info: (...a) => console.log(`${SEP}\n${c.gray}[${now()}]${c.reset} ${c.cyan}INFO ${c.reset}`, ...a),
|
logger.error(`unhandledRejection — ${msg}`);
|
||||||
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 = {}) => {
|
// Carrega plugins antes de conectar
|
||||||
const number = String(from).split("@")[0];
|
await loadPlugins(PLUGINS);
|
||||||
const isGroup = /@g\.us$/.test(chatId);
|
|
||||||
let name = msg?.notifyName || number;
|
|
||||||
|
|
||||||
try {
|
client.on("message_create", async (msg) => {
|
||||||
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 {
|
try {
|
||||||
const chat = await msg.getChat();
|
await handleMessage(msg);
|
||||||
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) {
|
} catch (err) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Falha ao processar — ${err.message}`,
|
`Falha ao processar — ${err.message}`,
|
||||||
@@ -131,4 +38,5 @@ client.on("message_create", async msg => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
client.initialize();
|
client.initialize();
|
||||||
|
console.log("\n");
|
||||||
logger.info("Cliente inicializado. Aguardando conexão com WhatsApp...");
|
logger.info("Cliente inicializado. Aguardando conexão com WhatsApp...");
|
||||||
9
src/plugins/a/index.js
Normal file
9
src/plugins/a/index.js
Normal file
@@ -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!");
|
||||||
|
}
|
||||||
79
src/plugins/adivinhação/index.js
Normal file
79
src/plugins/adivinhação/index.js
Normal file
@@ -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*!");
|
||||||
|
}
|
||||||
|
}
|
||||||
123
src/plugins/audio/index.js
Normal file
123
src/plugins/audio/index.js
Normal file
@@ -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."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
247
src/plugins/figurinha/index.js
Normal file
247
src/plugins/figurinha/index.js
Normal file
@@ -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}`);
|
||||||
|
}
|
||||||
159
src/plugins/forca/index.js
Normal file
159
src/plugins/forca/index.js
Normal file
@@ -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!")
|
||||||
|
);
|
||||||
|
}
|
||||||
14
src/plugins/many/index.js
Normal file
14
src/plugins/many/index.js
Normal file
@@ -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 <link>\` — baixa um vídeo\n` +
|
||||||
|
`🎵 \`${CMD_PREFIX}audio <link>\` — 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`
|
||||||
|
);
|
||||||
|
}
|
||||||
8
src/plugins/obrigado/index.js
Normal file
8
src/plugins/obrigado/index.js
Normal file
@@ -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!");
|
||||||
|
}
|
||||||
98
src/plugins/video/index.js
Normal file
98
src/plugins/video/index.js
Normal file
@@ -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."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,58 +1,63 @@
|
|||||||
// get_id.js
|
/**
|
||||||
|
* Utilitário CLI para descobrir IDs de chats/grupos.
|
||||||
|
* Uso: node src/utils/get_id.js grupos|contatos|<nome>
|
||||||
|
*/
|
||||||
|
import pkg from "whatsapp-web.js";
|
||||||
|
import qrcode from "qrcode-terminal";
|
||||||
|
import { resolvePuppeteerConfig } from "../client/environment.js";
|
||||||
|
|
||||||
|
const CLIENT_ID="getId"
|
||||||
|
const { Client, LocalAuth } = pkg;
|
||||||
|
|
||||||
|
const arg = process.argv[2];
|
||||||
|
|
||||||
const arg = process.argv[2]; // argumento passado no node
|
|
||||||
if (!arg) {
|
if (!arg) {
|
||||||
console.log("Use: node get_id.js grupos|contatos|<nome>");
|
console.log("Uso: node get_id.js grupos|contatos|<nome>");
|
||||||
process.exit(0);
|
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({
|
const client = new Client({
|
||||||
authStrategy: new LocalAuth({ clientId: CLIENT_ID }),
|
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 => {
|
client.on("qr", (qr) => {
|
||||||
console.log("[WPP] QR Code gerado. Escaneie apenas uma vez:");
|
console.log("[QR] Escaneie para autenticar:");
|
||||||
qrcode.generate(qr, { small: true });
|
qrcode.generate(qr, { small: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
client.on('ready', async () => {
|
client.on("ready", async () => {
|
||||||
console.log("[WPP] Conectado");
|
console.log("[OK] Conectado. Buscando chats...\n");
|
||||||
|
|
||||||
const chats = await client.getChats(); // <- precisa do await
|
const chats = await client.getChats();
|
||||||
|
const search = arg.toLowerCase();
|
||||||
|
|
||||||
let filtered = [];
|
const filtered =
|
||||||
|
search === "grupos" ? chats.filter(c => c.isGroup) :
|
||||||
|
search === "contatos" ? chats.filter(c => !c.isGroup) :
|
||||||
|
chats.filter(c => (c.name || c.id.user).toLowerCase().includes(search));
|
||||||
|
|
||||||
if (arg.toLowerCase() === "grupos") {
|
if (!filtered.length) {
|
||||||
filtered = chats.filter(c => c.isGroup);
|
console.log("Nenhum resultado encontrado.");
|
||||||
} else if (arg.toLowerCase() === "contatos") {
|
} else {
|
||||||
filtered = chats.filter(c => !c.isGroup);
|
filtered.forEach(c => {
|
||||||
} else {
|
console.log("─".repeat(40));
|
||||||
const search = arg.toLowerCase();
|
console.log("Nome: ", c.name || c.id.user);
|
||||||
filtered = chats.filter(c => (c.name || c.id.user).toLowerCase().includes(search));
|
console.log("ID: ", c.id._serialized);
|
||||||
}
|
console.log("Grupo: ", c.isGroup);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (filtered.length === 0) {
|
await client.destroy();
|
||||||
console.log("Nenhum chat encontrado com esse filtro.");
|
process.exit(0);
|
||||||
} 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();
|
client.initialize();
|
||||||
9
todo.txt
9
todo.txt
@@ -1,9 +0,0 @@
|
|||||||
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
|
|
||||||
199
update
Normal file
199
update
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# ==============================================================================
|
||||||
|
# update.sh — Script de atualização segura do ManyBot
|
||||||
|
# ==============================================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Configuração
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
tmp_dir="$dir/tmp"
|
||||||
|
log_file="$dir/update.log"
|
||||||
|
config_items=(".wwebjs_auth" ".wwebjs_cache" "node_modules")
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Logging
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
log() {
|
||||||
|
local level="$1"; shift
|
||||||
|
local msg="$*"
|
||||||
|
local timestamp
|
||||||
|
timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
|
echo "[$timestamp] [$level] $msg" | tee -a "$log_file"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Trap: executa em caso de qualquer erro
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
cleanup_on_error() {
|
||||||
|
local exit_code=$?
|
||||||
|
log "ERROR" "Falha durante a atualização (código: $exit_code)."
|
||||||
|
if [ -d "$tmp_dir" ] && [ "$(ls -A "$tmp_dir" 2>/dev/null)" ]; then
|
||||||
|
log "WARN" "Arquivos de configuração preservados em: $tmp_dir"
|
||||||
|
log "WARN" "Restaure manualmente com: mv $tmp_dir/* $dir/"
|
||||||
|
fi
|
||||||
|
exit $exit_code
|
||||||
|
}
|
||||||
|
trap cleanup_on_error ERR
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Validações iniciais
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
log "INFO" "Iniciando atualização do ManyBot..."
|
||||||
|
|
||||||
|
# Verifica dependências obrigatórias
|
||||||
|
for cmd in git npm; do
|
||||||
|
if ! command -v "$cmd" &>/dev/null; then
|
||||||
|
log "ERROR" "Dependência ausente: '$cmd' não encontrado no PATH."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Verifica se está em um repositório git
|
||||||
|
if ! git -C "$dir" rev-parse --is-inside-work-tree &>/dev/null; then
|
||||||
|
log "ERROR" "'$dir' não é um repositório git."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Exibe o remote para transparência
|
||||||
|
remote_url=$(git -C "$dir" remote get-url origin 2>/dev/null || echo "(não definido)")
|
||||||
|
log "INFO" "Remote: $remote_url"
|
||||||
|
|
||||||
|
# Verifica se há alterações locais não commitadas
|
||||||
|
if ! git -C "$dir" diff --quiet || ! git -C "$dir" diff --cached --quiet; then
|
||||||
|
log "WARN" "Existem alterações locais não commitadas. Elas serão descartadas pelo reset."
|
||||||
|
read -r -p "Deseja continuar mesmo assim? [s/N] " confirm
|
||||||
|
confirm="${confirm:-N}"
|
||||||
|
if [[ ! "$confirm" =~ ^[sS]$ ]]; then
|
||||||
|
log "INFO" "Atualização cancelada pelo usuário."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Backup dos arquivos de configuração
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
rm -rf "$tmp_dir"
|
||||||
|
mkdir -p "$tmp_dir"
|
||||||
|
|
||||||
|
backed_up=()
|
||||||
|
for item in "${config_items[@]}"; do
|
||||||
|
src="$dir/$item"
|
||||||
|
if [ -e "$src" ]; then
|
||||||
|
mv "$src" "$tmp_dir/"
|
||||||
|
backed_up+=("$item")
|
||||||
|
log "INFO" "Backup: $item → tmp/"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Atualização do repositório
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
log "INFO" "Buscando atualizações..."
|
||||||
|
git -C "$dir" fetch origin
|
||||||
|
|
||||||
|
branch=$(git -C "$dir" rev-parse --abbrev-ref HEAD)
|
||||||
|
log "INFO" "Branch atual: $branch"
|
||||||
|
|
||||||
|
# Registra o hash antes e depois para auditoria
|
||||||
|
hash_before=$(git -C "$dir" rev-parse HEAD)
|
||||||
|
git -C "$dir" reset --hard "origin/$branch"
|
||||||
|
hash_after=$(git -C "$dir" rev-parse HEAD)
|
||||||
|
|
||||||
|
if [ "$hash_before" = "$hash_after" ]; then
|
||||||
|
log "INFO" "Repositório já estava atualizado (sem mudanças)."
|
||||||
|
else
|
||||||
|
log "INFO" "Atualizado: $hash_before → $hash_after"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Instalação de dependências
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
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
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
if [ ${#backed_up[@]} -gt 0 ]; then
|
||||||
|
for item in "${backed_up[@]}"; do
|
||||||
|
src="$tmp_dir/$item"
|
||||||
|
dst="$dir/$item"
|
||||||
|
if [ -e "$src" ]; then
|
||||||
|
# Remove o que npm possa ter criado (ex: node_modules)
|
||||||
|
rm -rf "$dst"
|
||||||
|
mv "$src" "$dst"
|
||||||
|
l"INFO" "Restaurado: $item"
|
||||||
|
else
|
||||||
|
log "WARN" "Item esperado no backup não encontrado: $item"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Limpa tmp apenas após restauração bem-sucedida
|
||||||
|
rm -rf "$tmp_dir"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
# Concluído
|
||||||
|
# ------------------------------------------------------------------------------
|
||||||
|
log "INFO" "ManyBot atualizado com sucesso."
|
||||||
Reference in New Issue
Block a user