268 lines
8.7 KiB
TypeScript
268 lines
8.7 KiB
TypeScript
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import type { WASocketLike } from '../types/whatsapp.js';
|
|
|
|
type MenuKey = 'main' | 'users' | 'admins' | 'system' | 'session' | 'logs' | 'database';
|
|
|
|
type MenuOption = {
|
|
label: string;
|
|
next?: MenuKey;
|
|
action?: () => Promise<string>;
|
|
exit?: boolean;
|
|
back?: boolean;
|
|
};
|
|
|
|
type MenuDefinition = {
|
|
title: string;
|
|
options: MenuOption[];
|
|
};
|
|
|
|
const SESSION_TTL_MS = 5 * 60 * 1000;
|
|
const sessions = new Map<string, { menu: MenuKey; updatedAt: number }>();
|
|
|
|
function sanitizePhone(input: string): string {
|
|
return input.replace(/\D/g, '');
|
|
}
|
|
|
|
function getSenderJid(msg: any): string | null {
|
|
return msg?.key?.remoteJid ?? null;
|
|
}
|
|
|
|
function getSenderNumber(jid: string): string {
|
|
const raw = jid.includes('@') ? jid.split('@')[0] : jid;
|
|
return sanitizePhone(raw);
|
|
}
|
|
|
|
function isSuperAdmin(jid: string): boolean {
|
|
const admin = sanitizePhone(process.env.SUPER_ADMIN || '');
|
|
if (!admin) return false;
|
|
const sender = getSenderNumber(jid);
|
|
return sender === admin;
|
|
}
|
|
|
|
function extractText(msg: any): string | null {
|
|
const message = msg?.message;
|
|
if (!message) return null;
|
|
if (message.conversation) return message.conversation;
|
|
if (message.extendedTextMessage?.text) return message.extendedTextMessage.text;
|
|
if (message.imageMessage?.caption) return message.imageMessage.caption;
|
|
if (message.videoMessage?.caption) return message.videoMessage.caption;
|
|
if (message.buttonsResponseMessage?.selectedButtonId) return message.buttonsResponseMessage.selectedButtonId;
|
|
if (message.listResponseMessage?.singleSelectReply?.selectedRowId) return message.listResponseMessage.singleSelectReply.selectedRowId;
|
|
return null;
|
|
}
|
|
|
|
function buildMenuText(menu: MenuDefinition): string {
|
|
let text = `*${menu.title}*\n\n`;
|
|
menu.options.forEach((opt, idx) => {
|
|
text += `*${idx + 1}.* ${opt.label}\n`;
|
|
});
|
|
text += '\nResponda com o número desejado.';
|
|
return text.trim();
|
|
}
|
|
|
|
function isExpired(ts: number): boolean {
|
|
return Date.now() - ts > SESSION_TTL_MS;
|
|
}
|
|
|
|
function setSession(jid: string, menu: MenuKey) {
|
|
sessions.set(jid, { menu, updatedAt: Date.now() });
|
|
}
|
|
|
|
function clearSession(jid: string) {
|
|
sessions.delete(jid);
|
|
}
|
|
|
|
function getSession(jid: string): { menu: MenuKey; updatedAt: number } | null {
|
|
const session = sessions.get(jid);
|
|
if (!session) return null;
|
|
if (isExpired(session.updatedAt)) {
|
|
sessions.delete(jid);
|
|
return null;
|
|
}
|
|
return session;
|
|
}
|
|
|
|
function tailFile(filePath: string, maxLines: number): string {
|
|
try {
|
|
if (!fs.existsSync(filePath)) return 'Arquivo de log nao encontrado.';
|
|
const data = fs.readFileSync(filePath, 'utf8');
|
|
const lines = data.split(/\r?\n/).filter(Boolean);
|
|
return lines.slice(-maxLines).join('\n') || 'Sem logs recentes.';
|
|
} catch {
|
|
return 'Erro ao ler logs.';
|
|
}
|
|
}
|
|
|
|
function formatBytes(bytes: number): string {
|
|
if (bytes === 0) return '0 B';
|
|
const k = 1024;
|
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
|
}
|
|
|
|
function getPm2LogPath(kind: 'out' | 'error'): string {
|
|
const pm2Name = process.env.WHATSAPP_PM2_NAME || 'whatsapp-v1';
|
|
return path.join('/root/.pm2/logs', `${pm2Name}-${kind}.log`);
|
|
}
|
|
|
|
export function attachSuperAdminMenu(sock: WASocketLike, actions: {
|
|
getSessionStatus: () => string;
|
|
resetSession: () => Promise<string>;
|
|
logoutSession: () => Promise<string>;
|
|
}): void {
|
|
if ((sock as any).__superAdminMenuAttached) return;
|
|
(sock as any).__superAdminMenuAttached = true;
|
|
|
|
const menus: Record<MenuKey, MenuDefinition> = {
|
|
main: {
|
|
title: '🧠 PAINEL SUPER ADMIN',
|
|
options: [
|
|
{ label: 'Usuarios', next: 'users' },
|
|
{ label: 'Admins', next: 'admins' },
|
|
{ label: 'Sistema', next: 'system' },
|
|
{ label: 'Sessao WhatsApp', next: 'session' },
|
|
{ label: 'Logs', next: 'logs' },
|
|
{ label: 'Banco de Dados', next: 'database' },
|
|
{ label: 'Sair', exit: true },
|
|
],
|
|
},
|
|
users: {
|
|
title: '1️⃣ Usuarios',
|
|
options: [
|
|
{ label: 'Listar usuarios', action: async () => 'Funcionalidade em desenvolvimento.' },
|
|
{ label: 'Buscar usuario', action: async () => 'Funcionalidade em desenvolvimento.' },
|
|
{ label: 'Remover usuario', action: async () => 'Funcionalidade em desenvolvimento.' },
|
|
{ label: 'Voltar', back: true },
|
|
],
|
|
},
|
|
admins: {
|
|
title: '2️⃣ Admins',
|
|
options: [
|
|
{ label: 'Listar admins', action: async () => 'Funcionalidade em desenvolvimento.' },
|
|
{ label: 'Adicionar admin', action: async () => 'Funcionalidade em desenvolvimento.' },
|
|
{ label: 'Remover admin', action: async () => 'Funcionalidade em desenvolvimento.' },
|
|
{ label: 'Voltar', back: true },
|
|
],
|
|
},
|
|
system: {
|
|
title: '3️⃣ Sistema',
|
|
options: [
|
|
{ label: 'Status do servidor', action: async () => {
|
|
const uptime = Math.floor(os.uptime() / 60);
|
|
return `Servidor online. Uptime: ${uptime} min. Node: ${process.version}.`;
|
|
}},
|
|
{ label: 'Uso de memoria', action: async () => {
|
|
const mem = process.memoryUsage();
|
|
return `Memoria RSS: ${formatBytes(mem.rss)} | Heap: ${formatBytes(mem.heapUsed)} / ${formatBytes(mem.heapTotal)}`;
|
|
}},
|
|
{ label: 'Reiniciar bot', action: async () => {
|
|
setTimeout(() => process.exit(0), 500);
|
|
return 'Reiniciando bot...';
|
|
}},
|
|
{ label: 'Voltar', back: true },
|
|
],
|
|
},
|
|
session: {
|
|
title: '4️⃣ Sessao WhatsApp',
|
|
options: [
|
|
{ label: 'Status conexao', action: async () => `Status: ${actions.getSessionStatus()}` },
|
|
{ label: 'Resetar sessao', action: actions.resetSession },
|
|
{ label: 'Deslogar WhatsApp', action: actions.logoutSession },
|
|
{ label: 'Voltar', back: true },
|
|
],
|
|
},
|
|
logs: {
|
|
title: '5️⃣ Logs',
|
|
options: [
|
|
{ label: 'Ultimos 50 logs', action: async () => tailFile(getPm2LogPath('out'), 50) },
|
|
{ label: 'Erros recentes', action: async () => tailFile(getPm2LogPath('error'), 50) },
|
|
{ label: 'Exportar log', action: async () => tailFile(getPm2LogPath('out'), 50) },
|
|
{ label: 'Voltar', back: true },
|
|
],
|
|
},
|
|
database: {
|
|
title: '6️⃣ Banco de Dados',
|
|
options: [
|
|
{ label: 'Exportar users.json', action: async () => 'Funcionalidade em desenvolvimento.' },
|
|
{ label: 'Backup completo', action: async () => 'Funcionalidade em desenvolvimento.' },
|
|
{ label: 'Limpar registros inativos', action: async () => 'Funcionalidade em desenvolvimento.' },
|
|
{ label: 'Voltar', back: true },
|
|
],
|
|
},
|
|
};
|
|
|
|
sock.ev.on('messages.upsert', async (payload: any) => {
|
|
const list = payload?.messages || [];
|
|
for (const msg of list) {
|
|
if (!msg?.message) continue;
|
|
if (msg?.key?.fromMe) continue;
|
|
|
|
const jid = getSenderJid(msg);
|
|
if (!jid || jid.endsWith('@g.us')) continue;
|
|
|
|
const text = extractText(msg);
|
|
if (!text) continue;
|
|
const body = text.trim();
|
|
if (!body) continue;
|
|
|
|
const lower = body.toLowerCase();
|
|
if (lower === '.menu' || lower === '.panel') {
|
|
if (!isSuperAdmin(jid)) {
|
|
await sock.sendMessage(jid, { text: 'Acesso restrito.' });
|
|
continue;
|
|
}
|
|
setSession(jid, 'main');
|
|
await sock.sendMessage(jid, { text: buildMenuText(menus.main) });
|
|
continue;
|
|
}
|
|
|
|
if (!isSuperAdmin(jid)) continue;
|
|
|
|
const session = getSession(jid);
|
|
if (!session) continue;
|
|
|
|
if (isExpired(session.updatedAt)) {
|
|
clearSession(jid);
|
|
await sock.sendMessage(jid, { text: 'Sessao expirada. Envie .menu novamente.' });
|
|
continue;
|
|
}
|
|
|
|
const menu = menus[session.menu];
|
|
const choice = parseInt(body, 10);
|
|
if (Number.isNaN(choice) || choice < 1 || choice > menu.options.length) {
|
|
await sock.sendMessage(jid, { text: 'Opcao invalida. Responda com um numero da lista.' });
|
|
continue;
|
|
}
|
|
|
|
const option = menu.options[choice - 1];
|
|
if (option.exit) {
|
|
clearSession(jid);
|
|
await sock.sendMessage(jid, { text: 'Sessao encerrada.' });
|
|
continue;
|
|
}
|
|
|
|
if (option.back) {
|
|
setSession(jid, 'main');
|
|
await sock.sendMessage(jid, { text: buildMenuText(menus.main) });
|
|
continue;
|
|
}
|
|
|
|
if (option.next) {
|
|
setSession(jid, option.next);
|
|
await sock.sendMessage(jid, { text: buildMenuText(menus[option.next]) });
|
|
continue;
|
|
}
|
|
|
|
if (option.action) {
|
|
setSession(jid, session.menu);
|
|
const reply = await option.action();
|
|
await sock.sendMessage(jid, { text: reply });
|
|
continue;
|
|
}
|
|
}
|
|
});
|
|
}
|