Files
clube67.com/backend/plugins/whatsapp-v1/src/handlers/superAdminMenu.ts
T
2026-05-18 18:32:55 +02:00

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;
}
}
});
}