fix(monitoring): lista negra estricta e ignorado permanente de grupos ajenos, eliminacion de productos no calzado y proteccion en webhook

This commit is contained in:
AI Bot 2026-09-12 09:19:24 -05:00
parent 02bac37af0
commit 31130bfbb5
9 changed files with 494 additions and 121 deletions

View File

@ -82,7 +82,20 @@ export default function AdminWhatsAppGroupsPage() {
}
};
const isGroupIgnored = (jid: string, name?: string): boolean => {
if (!jid) return false;
if (jid === '120363400148850031@g.us' || jid === '120363420942364759@g.us') return true;
if (name && /black diamonds|team emporio|emporio/i.test(name)) return true;
return false;
};
const handleToggleSelectJid = (jid: string) => {
const grp = liveGroups.find((g) => g.jid === jid);
if (grp && isGroupIgnored(grp.jid, grp.name)) {
setToast(`⛔ El grupo "${grp.name}" está excluido permanentemente y no puede seleccionarse.`);
setTimeout(() => setToast(null), 4000);
return;
}
setSelectedJids((prev) => {
const next = new Set(prev);
if (next.has(jid)) next.delete(jid);
@ -94,7 +107,11 @@ export default function AdminWhatsAppGroupsPage() {
const handleSelectAllFiltered = (filtered: MonitoredWhatsAppGroup[]) => {
setSelectedJids((prev) => {
const next = new Set(prev);
filtered.forEach((g) => next.add(g.jid));
filtered.forEach((g) => {
if (!isGroupIgnored(g.jid, g.name)) {
next.add(g.jid);
}
});
return next;
});
};
@ -108,9 +125,10 @@ export default function AdminWhatsAppGroupsPage() {
try {
const updatedGroups: MonitoredWhatsAppGroup[] = liveGroups.map((lg) => ({
...lg,
isMonitored: selectedJids.has(lg.jid),
autoPublish: true,
isMonitored: !isGroupIgnored(lg.jid, lg.name) && selectedJids.has(lg.jid),
autoPublish: !isGroupIgnored(lg.jid, lg.name),
profitMarginOverride: 35,
notes: isGroupIgnored(lg.jid, lg.name) ? 'IGNORADO EXPLICITAMENTE' : lg.notes,
}));
const res = await fetch('/api/whatsapp-groups', {
@ -134,6 +152,12 @@ export default function AdminWhatsAppGroupsPage() {
};
const handleToggleGroup = async (group: MonitoredWhatsAppGroup) => {
if (isGroupIgnored(group.jid, group.name)) {
setToast(`⛔ El grupo "${group.name}" está en la lista de exclusión (ignorado permanente) y no puede ser monitoreado.`);
setTimeout(() => setToast(null), 4500);
return;
}
const nextState = !group.isMonitored;
const updatedList = groups.map((g) =>
g.jid === group.jid || g.id === group.id ? { ...g, isMonitored: nextState } : g
@ -329,12 +353,15 @@ export default function AdminWhatsAppGroupsPage() {
) : (
filteredGroups.map((group) => {
const isMonitored = group.isMonitored;
const isIgnored = isGroupIgnored(group.jid, group.name);
return (
<div
key={group.id || group.jid}
className={`p-5 rounded-3xl border transition-all space-y-4 ${
isMonitored
isIgnored
? 'bg-neutral-950/80 border-rose-900/40 opacity-70'
: isMonitored
? 'bg-gradient-to-br from-neutral-900 to-neutral-900/90 border-purple-500/40 shadow-lg shadow-purple-950/20 ring-1 ring-purple-500/20'
: 'bg-neutral-900/60 border-neutral-800 hover:border-neutral-700 opacity-80'
}`}
@ -342,15 +369,20 @@ export default function AdminWhatsAppGroupsPage() {
{/* Header */}
<div className="flex items-start justify-between gap-2">
<div className="flex-1">
<div className="flex items-center gap-2">
<div className="flex items-center gap-2 flex-wrap">
<span
className={`w-2.5 h-2.5 rounded-full shrink-0 ${
isMonitored ? 'bg-purple-400 animate-pulse' : 'bg-neutral-600'
isIgnored ? 'bg-rose-500' : isMonitored ? 'bg-purple-400 animate-pulse' : 'bg-neutral-600'
}`}
/>
<h3 className="font-black text-white text-sm leading-snug line-clamp-1">
{group.name}
</h3>
{isIgnored && (
<span className="bg-rose-500/20 text-rose-300 border border-rose-500/30 text-[9px] font-black px-2 py-0.5 rounded-full uppercase tracking-wider">
⛔ Ignorado
</span>
)}
</div>
<p className="text-[11px] font-mono text-neutral-400 pl-4 mt-0.5 truncate">
{group.jid}
@ -360,13 +392,21 @@ export default function AdminWhatsAppGroupsPage() {
{/* Toggle Switch */}
<button
onClick={() => handleToggleGroup(group)}
className={`shrink-0 text-xs font-bold px-3 py-1.5 rounded-xl border flex items-center gap-1.5 transition-all cursor-pointer ${
isMonitored
? 'bg-purple-600 text-white border-purple-400 shadow-md font-black'
: 'bg-neutral-800 text-neutral-400 border-neutral-700 hover:text-white'
disabled={isIgnored}
className={`shrink-0 text-xs font-bold px-3 py-1.5 rounded-xl border flex items-center gap-1.5 transition-all ${
isIgnored
? 'bg-rose-950/40 text-rose-400 border-rose-900/50 cursor-not-allowed'
: isMonitored
? 'bg-purple-600 text-white border-purple-400 shadow-md font-black cursor-pointer'
: 'bg-neutral-800 text-neutral-400 border-neutral-700 hover:text-white cursor-pointer'
}`}
>
{isMonitored ? (
{isIgnored ? (
<>
<XCircle className="w-3.5 h-3.5" />
<span>Bloqueado</span>
</>
) : isMonitored ? (
<>
<CheckCircle2 className="w-3.5 h-3.5 fill-white text-purple-600" />
<span>Monitoreando</span>
@ -500,22 +540,27 @@ export default function AdminWhatsAppGroupsPage() {
</div>
) : (
filteredPickerGroups.map((group) => {
const isSelected = selectedJids.has(group.jid);
const isIgnored = isGroupIgnored(group.jid, group.name);
const isSelected = !isIgnored && selectedJids.has(group.jid);
return (
<div
key={group.jid}
onClick={() => handleToggleSelectJid(group.jid)}
className={`p-3.5 rounded-2xl border transition-all cursor-pointer flex items-center justify-between gap-3 ${
isSelected
? 'bg-purple-950/30 border-purple-500/50 shadow-md ring-1 ring-purple-500/20'
: 'bg-neutral-950 border-neutral-800 hover:border-neutral-700'
onClick={() => !isIgnored && handleToggleSelectJid(group.jid)}
className={`p-3.5 rounded-2xl border transition-all flex items-center justify-between gap-3 ${
isIgnored
? 'bg-neutral-950/60 border-rose-950/60 opacity-60 cursor-not-allowed'
: isSelected
? 'bg-purple-950/30 border-purple-500/50 shadow-md ring-1 ring-purple-500/20 cursor-pointer'
: 'bg-neutral-950 border-neutral-800 hover:border-neutral-700 cursor-pointer'
}`}
>
<div className="flex items-center space-x-3 flex-1 min-w-0">
<div
className={`w-5 h-5 rounded-lg border flex items-center justify-center shrink-0 transition-all ${
isSelected
isIgnored
? 'bg-neutral-900 border-neutral-800 text-neutral-600'
: isSelected
? 'bg-purple-600 border-purple-400 text-white'
: 'border-neutral-700 bg-neutral-900'
}`}
@ -524,8 +569,13 @@ export default function AdminWhatsAppGroupsPage() {
</div>
<div className="min-w-0">
<h4 className="font-bold text-white text-xs truncate">
{group.name}
<h4 className="font-bold text-white text-xs truncate flex items-center gap-1.5">
<span>{group.name}</span>
{isIgnored && (
<span className="bg-rose-500/20 text-rose-300 border border-rose-500/30 text-[9px] font-black px-1.5 py-0.2 rounded-full uppercase">
⛔ Ignorado
</span>
)}
</h4>
<p className="text-[10px] font-mono text-neutral-400 truncate">
{group.jid}
@ -534,7 +584,11 @@ export default function AdminWhatsAppGroupsPage() {
</div>
<div className="shrink-0 flex items-center gap-2">
{isSelected ? (
{isIgnored ? (
<span className="bg-rose-950/40 text-rose-400 border border-rose-900/50 text-[10px] font-bold px-2 py-0.5 rounded-full">
Bloqueado
</span>
) : isSelected ? (
<span className="bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 text-[10px] font-black px-2 py-0.5 rounded-full uppercase">
Monitorear
</span>

View File

@ -1,5 +1,5 @@
import { NextResponse } from 'next/server';
import { dataService } from '@/lib/data-service';
import { dataService, isGroupIgnoredOrBlacklisted } from '@/lib/data-service';
import { parseSupplierMessage, detectCategory } from '@/lib/gemini-parser';
import { sanitizePublicationTitle } from '@/lib/sanitizer';
import { generateAttractiveProductCopy } from '@/lib/attractive-copy';
@ -14,6 +14,7 @@ import {
} from '@/lib/evolution-api';
import {
getMonitoredGroupsFromAppwrite,
saveMonitoredGroupToAppwrite,
saveProductToAppwrite,
saveSupplierPostToAppwrite,
updateSupplierPostStatusInAppwrite,
@ -238,7 +239,7 @@ export async function POST(request: Request) {
}
// =========================================================================
// CASO B: GRUPOS MAYORISTAS DE PROVEEDORES (Publicación Automática Incondicional)
// CASO B: GRUPOS MAYORISTAS DE PROVEEDORES (Monitoreo Estricto por Lista Blanca)
// =========================================================================
// Resolver el nombre del grupo de forma precisa usando Evolution API si no viene en el payload
let groupSubject = data.groupInfo?.subject || data.chatName || '';
@ -255,6 +256,17 @@ export async function POST(request: Request) {
}
}
// 0. LISTA NEGRA PERMANENTE DE GRUPOS IGNORADOS (Blacklist incondicional)
if (isGroupIgnoredOrBlacklisted(remoteJid, groupSubject)) {
console.log(`[DripZone Webhook] Grupo IGNORADO por lista negra: "${groupSubject}" (${remoteJid})`);
return NextResponse.json({
success: true,
ignored: 'Grupo en lista negra comercial permanente (no monitorear ni publicar)',
chat: groupSubject,
remoteJid,
});
}
// 1. FILTRAR EXCLUSIVAMENTE GRUPOS DE PRENSA Y GOBIERNO (Reenviar a portal de noticias Almeira)
const isNonProductNews = /gob cauca|gobernacion|alcaldia|prensa|medios|noticias|comunicaciones|sedes asociadas|notificaciones almeira|inelpac|meliextra|almeira producciones|publicaciones almeira|quilichao en medios/i.test(
groupSubject
@ -292,25 +304,68 @@ export async function POST(request: Request) {
});
}
// Auto-registrar grupo como monitoreado y activo si no existía previamente
try {
const currentGroups = dataService.getMonitoredGroups();
const existingGrp = currentGroups.find((g) => g.jid === remoteJid);
if (!existingGrp) {
dataService.saveMonitoredGroups([
...currentGroups,
{
id: `grp-${remoteJid.replace(/\D/g, '').slice(-8)}`,
jid: remoteJid,
name: groupSubject,
isMonitored: true,
autoPublish: true,
notes: 'Auto-registrado desde Webhook activo de WhatsApp',
supplierName: groupSubject,
},
]);
}
} catch (_) {}
// 1.2 FILTRAR CONTENIDO DE EVENTOS, CONFERENCIAS, REUNIONES VIRTUALES O SPAM
const isSpamOrAnnouncement = /conferencia|webinar|zoom\.us|pantalla de zoom|podcast|mañanas millonarias|liderazgo|liderea|orador|taller virtual|evento online|conversaciones m[aá]s dinero|estrategia que funciona|elevax10n|smoothie|strawberry flavored|smartwatch|reloj inteligente|forex|cripto|trading|hoy 🚨|empezamos en \d+ minutos|llamada 911/i.test(
`${messageContent} ${groupSubject}`
);
if (isSpamOrAnnouncement) {
console.log(`[DripZone Webhook] Mensaje descartado por ser anuncio/evento/no calzado: "${messageContent.slice(0, 60)}"`);
return NextResponse.json({
success: true,
ignored: 'Mensaje es un anuncio, evento o contenido no calzado',
chat: groupSubject,
remoteJid,
});
}
// 1.3 VERIFICAR QUE EL GRUPO ESTÉ EXPRESAMENTE MONITOREADO Y ACTIVO
let currentGroups = dataService.getMonitoredGroups();
let existingGrp = currentGroups.find(
(g) => g.jid === remoteJid || g.name.toLowerCase().replace(/\s+/g, ' ').trim() === groupSubject.toLowerCase().replace(/\s+/g, ' ').trim()
);
if (!existingGrp) {
try {
const appwriteGroups = await getMonitoredGroupsFromAppwrite();
existingGrp = appwriteGroups.find((g) => g.jid === remoteJid);
if (existingGrp) {
dataService.saveMonitoredGroups([...currentGroups, existingGrp]);
}
} catch (_) {}
}
// Si el grupo nunca se ha configurado, registrarlo como INACTIVO (pendiente de aprobación en panel)
if (!existingGrp) {
const newGroupDoc = {
id: `grp-${remoteJid.replace(/\D/g, '').slice(-8)}`,
jid: remoteJid,
name: groupSubject,
isMonitored: false,
autoPublish: false,
notes: 'Detectado en webhook (inactivo por defecto)',
supplierName: groupSubject,
};
dataService.saveMonitoredGroups([...currentGroups, newGroupDoc]);
saveMonitoredGroupToAppwrite(newGroupDoc).catch(() => {});
console.log(`[DripZone Webhook] Nuevo grupo detectado pero REGISTRADO COMO INACTIVO: "${groupSubject}" (${remoteJid})`);
return NextResponse.json({
success: true,
ignored: 'Grupo nuevo no monitoreado (registrado inactivo en panel admin)',
chat: groupSubject,
remoteJid,
});
}
// Si el grupo está desactivado o tiene auto-publicación apagada
if (!existingGrp.isMonitored || existingGrp.autoPublish === false) {
console.log(`[DripZone Webhook] Mensaje descartado: grupo no monitoreado o autoPublish apagado: "${groupSubject}" (${remoteJid})`);
return NextResponse.json({
success: true,
ignored: 'Grupo no monitoreado o auto-publicación apagada',
chat: groupSubject,
remoteJid,
});
}
// 2. VERIFICACIÓN DE DUPLICADOS
if (isDuplicateMessage(whatsappMessageId, messageContent)) {
@ -397,6 +452,17 @@ export async function POST(request: Request) {
};
}
// Validación de coherencia y descarte de spam
const invalidTitleCheck = /hoy 🚨|conferencia|evento|zoom|podcast|mañanas millonarias|liderea|sin producto|elevax10n|smoothie|no especificado/i;
if (invalidTitleCheck.test(extracted.name || '') || invalidTitleCheck.test(messageContent)) {
console.log(`[DripZone Webhook] Publicación cancelada por contenido no comercial/calzado: "${extracted.name}"`);
return NextResponse.json({
success: true,
ignored: 'Contenido descartado por no ser calzado o producto legítimo',
name: extracted.name,
});
}
// Categorización segura
if (!extracted.category || extracted.category === 'Calzado / Sneakers' || extracted.category === 'Sneakers / Colección') {
extracted.category = detectCategory(`${extracted.name || ''} ${messageContent}`).category;

View File

@ -1,5 +1,5 @@
import { NextResponse } from 'next/server';
import { dataService } from '@/lib/data-service';
import { dataService, isGroupIgnoredOrBlacklisted } from '@/lib/data-service';
import { MonitoredWhatsAppGroup } from '@/types';
import {
getMonitoredGroupsFromAppwrite,
@ -22,7 +22,7 @@ export async function GET(request: Request) {
const appwriteGroups = await getMonitoredGroupsFromAppwrite();
if (appwriteGroups.length > 0) {
dataService.saveMonitoredGroups(appwriteGroups);
localGroups = appwriteGroups;
localGroups = dataService.getMonitoredGroups();
}
} catch (err) {
console.warn('Error leyendo grupos desde Appwrite:', err);
@ -36,24 +36,46 @@ export async function GET(request: Request) {
});
}
// Consultar grupos en vivo desde Evolution API usando findChats (base de datos local rápida sin rate-limits)
// Consultar grupos en vivo desde Evolution API usando findChats y fetchAllGroups
try {
const response = await fetch(
`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`,
{
method: 'POST',
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
let liveGroups: any[] = [];
try {
const response = await fetch(
`${EVOLUTION_API_URL}/group/fetchAllGroups/${EVOLUTION_INSTANCE_NAME}?getParticipants=false`,
{
headers: { apikey: EVOLUTION_API_KEY },
signal: AbortSignal.timeout(8000),
}
);
if (response.ok) {
const all = await response.json();
if (Array.isArray(all) && all.length > 0) {
liveGroups = all.map((g: any) => ({
remoteJid: g.id || g.jid,
name: g.subject || g.name || 'Grupo WhatsApp',
size: g.size,
}));
}
}
);
} catch (_) {}
const allChats = await response.json();
const liveGroups = (Array.isArray(allChats) ? allChats : []).filter(
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
);
if (liveGroups.length === 0) {
const response = await fetch(
`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`,
{
method: 'POST',
headers: {
apikey: EVOLUTION_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
}
);
const allChats = await response.json();
liveGroups = (Array.isArray(allChats) ? allChats : []).filter(
(c: any) => (c.remoteJid && c.remoteJid.includes('@g.us')) || (c.id && c.id.includes('@g.us'))
);
}
if (liveGroups.length > 0) {
const merged: MonitoredWhatsAppGroup[] = await Promise.all(
@ -61,49 +83,34 @@ export async function GET(request: Request) {
const jid = lg.remoteJid || lg.id || lg.jid;
const name = lg.name || lg.pushName || lg.subject || 'Grupo WhatsApp';
let isStillMember = true;
let participants = undefined;
let participants = lg.size || undefined;
try {
const infoRes = await fetch(
`${EVOLUTION_API_URL}/group/findGroupInfos/${EVOLUTION_INSTANCE_NAME}?groupJid=${jid}`,
{
headers: { apikey: EVOLUTION_API_KEY },
signal: AbortSignal.timeout(3500),
}
);
if (infoRes.status === 404 || infoRes.status === 401 || infoRes.status === 403) {
isStillMember = false;
} else if (infoRes.ok) {
const infoData = await infoRes.json();
if (Array.isArray(infoData.participants)) {
participants = infoData.participants.length;
}
}
} catch {
// Si falla por timeout, mantener estado previo
}
const isBlacklisted = isGroupIgnoredOrBlacklisted(jid, name);
const existing = localGroups.find(
(g) => g.jid === jid || g.name.toLowerCase().replace(/\s+/g, ' ').trim() === name.toLowerCase().replace(/\s+/g, ' ').trim()
);
const existing = localGroups.find((g) => g.jid === jid || g.name.toLowerCase() === name.toLowerCase());
// Pre-marcar como monitoreados grupos reconocidos de proveedores si no había configuración previa
const isKnownSupplierGroup = /exclusive|emporio|jovic|diamonds|mayorista|calzado|sneakers|zapatillas/i.test(name);
const isMonitored = existing
// Pre-marcar SOLO proveedores exclusivos verificados de calzado si no había configuración previa
const isKnownSupplierGroup = !isBlacklisted && /exclusive cc|jovic store/i.test(name);
const isMonitored = isBlacklisted ? false : (existing
? (existing.isMonitored && isStillMember)
: (isKnownSupplierGroup && isStillMember);
: (isKnownSupplierGroup && isStillMember));
return {
id: existing?.id || `grp-${jid.replace(/\D/g, '').slice(-6)}`,
id: existing?.id || `grp-${jid.replace(/\D/g, '').slice(-8)}`,
jid,
name,
isMonitored: isStillMember ? isMonitored : false,
isMonitored: isBlacklisted ? false : (isStillMember ? isMonitored : false),
isStillMember,
hasLeft: !isStillMember,
supplierId: existing?.supplierId,
supplierName: existing?.supplierName || name,
profitMarginOverride: existing?.profitMarginOverride || 35,
autoPublish: existing?.autoPublish ?? true,
autoPublish: isBlacklisted ? false : (existing?.autoPublish ?? true),
participantsCount: participants ?? lg.unreadCount,
notes: isStillMember ? (existing?.notes || '') : 'Saliste de este grupo en WhatsApp',
notes: isBlacklisted
? 'IGNORADO EXPLICITAMENTE'
: isStillMember ? (existing?.notes || '') : 'Saliste de este grupo en WhatsApp',
};
})
);
@ -145,7 +152,8 @@ export async function POST(request: Request) {
if (body.action === 'toggle' || body.action === 'deactivate-group') {
const { idOrJid, isMonitored } = body;
const targetState = isMonitored !== undefined ? isMonitored : false;
const isBlacklisted = isGroupIgnoredOrBlacklisted(idOrJid);
const targetState = isBlacklisted ? false : (isMonitored !== undefined ? isMonitored : false);
const updated = dataService.toggleGroupMonitoring(idOrJid, targetState);
toggleMonitoredGroupInAppwrite(idOrJid, targetState).catch((e) =>
console.warn('Error sincronizando toggle con Appwrite:', e)
@ -154,8 +162,14 @@ export async function POST(request: Request) {
}
if (Array.isArray(body.groups)) {
const saved = dataService.saveMonitoredGroups(body.groups);
Promise.all(body.groups.map((g: any) => saveMonitoredGroupToAppwrite(g))).catch((e) =>
const sanitized = body.groups.map((g: any) => {
if (isGroupIgnoredOrBlacklisted(g.jid, g.name)) {
return { ...g, isMonitored: false, autoPublish: false, notes: 'IGNORADO EXPLICITAMENTE' };
}
return g;
});
const saved = dataService.saveMonitoredGroups(sanitized);
Promise.all(sanitized.map((g: any) => saveMonitoredGroupToAppwrite(g))).catch((e) =>
console.warn('Error sincronizando grupos con Appwrite:', e)
);
return NextResponse.json({ success: true, groups: saved });

View File

@ -227,6 +227,19 @@ export const INITIAL_USERS: User[] = [
},
];
export const BLACKLISTED_WHATSAPP_GROUPS: string[] = [
'120363400148850031@g.us', // SOMOS BLACK DIAMONDS 💎💎💎
'120363420942364759@g.us', // 💎TEAM EMPORIO💎🔥👑❤️‍🔥🤯
];
export function isGroupIgnoredOrBlacklisted(jid: string, name?: string): boolean {
if (!jid) return false;
const cleanJid = jid.trim();
if (BLACKLISTED_WHATSAPP_GROUPS.includes(cleanJid)) return true;
if (name && /black diamonds|team emporio|emporio/i.test(name)) return true;
return false;
}
export const INITIAL_MONITORED_GROUPS: MonitoredWhatsAppGroup[] = [];
function getFsModule() {
@ -1092,21 +1105,48 @@ class DataService {
return user || null;
}
private monitoredGroups: MonitoredWhatsAppGroup[] = [...INITIAL_MONITORED_GROUPS];
private monitoredGroups: MonitoredWhatsAppGroup[] = loadPersistedData<MonitoredWhatsAppGroup[]>(
'persisted_monitored_groups.json',
[...INITIAL_MONITORED_GROUPS]
);
getMonitoredGroups(): MonitoredWhatsAppGroup[] {
return this.monitoredGroups;
if (!this.monitoredGroups || this.monitoredGroups.length === 0) {
this.monitoredGroups = loadPersistedData<MonitoredWhatsAppGroup[]>(
'persisted_monitored_groups.json',
[...INITIAL_MONITORED_GROUPS]
);
}
return this.monitoredGroups.map((g) => {
if (isGroupIgnoredOrBlacklisted(g.jid, g.name)) {
return { ...g, isMonitored: false, autoPublish: false, notes: 'IGNORADO EXPLICITAMENTE' };
}
return g;
});
}
saveMonitoredGroups(groups: MonitoredWhatsAppGroup[]): MonitoredWhatsAppGroup[] {
this.monitoredGroups = groups;
this.monitoredGroups = groups.map((g) => {
if (isGroupIgnoredOrBlacklisted(g.jid, g.name)) {
return { ...g, isMonitored: false, autoPublish: false, notes: 'IGNORADO EXPLICITAMENTE' };
}
return g;
});
savePersistedData('persisted_monitored_groups.json', this.monitoredGroups);
return this.monitoredGroups;
}
toggleGroupMonitoring(idOrJid: string, isMonitored: boolean): MonitoredWhatsAppGroup | null {
const grp = this.monitoredGroups.find((g) => g.id === idOrJid || g.jid === idOrJid);
if (grp) {
grp.isMonitored = isMonitored;
if (isGroupIgnoredOrBlacklisted(grp.jid, grp.name)) {
grp.isMonitored = false;
grp.autoPublish = false;
grp.notes = 'IGNORADO EXPLICITAMENTE';
} else {
grp.isMonitored = isMonitored;
}
savePersistedData('persisted_monitored_groups.json', this.monitoredGroups);
return grp;
}
return null;

View File

@ -11,7 +11,7 @@ import {
MessageTemplate,
CustomProductRequest,
} from '@/types';
import { INITIAL_CITIES, INITIAL_PAYMENT_METHODS, INITIAL_SUPPLIERS } from './data-service';
import { INITIAL_CITIES, INITIAL_PAYMENT_METHODS, INITIAL_SUPPLIERS, isGroupIgnoredOrBlacklisted } from './data-service';
import { DEFAULT_MESSAGE_TEMPLATES } from './templates-service';
import { sanitizePublicationTitle, sanitizePublicationText } from './sanitizer';
@ -717,15 +717,18 @@ export async function getMonitoredGroupsFromAppwrite(): Promise<MonitoredWhatsAp
Query.limit(100),
]);
if (res.documents && res.documents.length > 0) {
return res.documents.map((d: any) => ({
id: d.$id,
jid: d.jid || '',
name: d.name || '',
isMonitored: Boolean(d.isMonitored),
autoPublish: d.autoPublish !== false,
notes: d.notes || '',
supplierName: d.name || '',
}));
return res.documents.map((d: any) => {
const isBlacklisted = isGroupIgnoredOrBlacklisted(d.jid, d.name);
return {
id: d.$id,
jid: d.jid || '',
name: d.name || '',
isMonitored: isBlacklisted ? false : Boolean(d.isMonitored),
autoPublish: isBlacklisted ? false : d.autoPublish !== false,
notes: isBlacklisted ? 'IGNORADO EXPLICITAMENTE' : d.notes || '',
supplierName: d.name || '',
};
});
}
return [];
} catch (e: any) {
@ -737,13 +740,14 @@ export async function getMonitoredGroupsFromAppwrite(): Promise<MonitoredWhatsAp
export async function saveMonitoredGroupToAppwrite(
group: Partial<MonitoredWhatsAppGroup>
): Promise<MonitoredWhatsAppGroup> {
const isBlacklisted = isGroupIgnoredOrBlacklisted(group.jid || '', group.name);
const docId = group.id || `grp-${(group.jid || '').replace(/\D/g, '').slice(-8)}` || `grp-${Date.now()}`;
const payload = {
jid: group.jid || '',
name: group.name || '',
isMonitored: Boolean(group.isMonitored),
autoPublish: group.autoPublish !== false,
notes: group.notes || '',
isMonitored: isBlacklisted ? false : Boolean(group.isMonitored),
autoPublish: isBlacklisted ? false : group.autoPublish !== false,
notes: isBlacklisted ? 'IGNORADO EXPLICITAMENTE' : group.notes || '',
};
try {
@ -780,12 +784,24 @@ export async function toggleMonitoredGroupInAppwrite(
isMonitored: boolean
): Promise<boolean> {
try {
const groups = await getMonitoredGroupsFromAppwrite();
const found = groups.find((g) => g.id === idOrJid || g.jid === idOrJid);
if (found) {
await serverDatabases.updateDocument(DB_ID, 'monitored_groups', found.id, {
isMonitored,
});
const isBlacklisted = isGroupIgnoredOrBlacklisted(idOrJid);
const finalState = isBlacklisted ? false : isMonitored;
const res = await serverDatabases.listDocuments(DB_ID, 'monitored_groups', [Query.limit(100)]);
const matchingDocs = res.documents.filter(
(d: any) => d.$id === idOrJid || d.jid === idOrJid || d.name === idOrJid
);
if (matchingDocs.length > 0) {
await Promise.all(
matchingDocs.map((doc: any) =>
serverDatabases.updateDocument(DB_ID, 'monitored_groups', doc.$id, {
isMonitored: finalState,
autoPublish: finalState,
notes: isBlacklisted ? 'IGNORADO EXPLICITAMENTE' : doc.notes,
})
)
);
return true;
}
return false;

View File

@ -0,0 +1,114 @@
const sdk = require('node-appwrite');
const client = new sdk.Client()
.setEndpoint('https://appwrite.almeira.com.co/v1')
.setProject('69b84df5003bd8957c5c')
.setKey('standard_cce3cdb866ea6433888d40cedd24dc45987372308fdc3dd0697de08648c0e5c7e04b02c3bcee271403fd656b00a5820f04c08b51631284a6e8a3582d8aed2aa33234f3c29cc777e9b162eb54810b789251b0d006a9a3b081ebc2da53b9baa94614b124b3508ad849fd9a4187cc6094f506d4b57f409b69d9562a74df594f3ec3');
const dbs = new sdk.Databases(client);
const DB_ID = 'sneakers_multistore_db';
async function main() {
console.log('=== [1] ELIMINANDO PRODUCTOS BASURA DE LOS GRUPOS IGNORADOS ===');
let offset = 0;
const limit = 100;
const toDelete = [];
while (true) {
const res = await dbs.listDocuments(DB_ID, 'products', [
sdk.Query.limit(limit),
sdk.Query.offset(offset)
]);
if (!res.documents || res.documents.length === 0) break;
for (const doc of res.documents) {
const sup = (doc.supplierName || '').toLowerCase();
const name = (doc.name || '').toLowerCase();
const raw = JSON.stringify(doc).toLowerCase();
if (
sup.includes('emporio') ||
sup.includes('black diamonds') ||
raw.includes('120363400148850031') ||
raw.includes('120363420942364759') ||
name.includes('hoy 🚨') ||
name.includes('liderea con')
) {
// Asegurar que no borramos productos legítimos de Jovic o CC Mío
if (sup.includes('jovic') || sup.includes('cc mío') || sup.includes('exclusive')) {
continue;
}
toDelete.push({ id: doc['$id'], name: doc.name, supplier: doc.supplierName });
}
}
offset += limit;
if (offset >= res.total) break;
}
console.log(`Encontrados ${toDelete.length} productos para eliminar:`);
for (const p of toDelete) {
try {
await dbs.deleteDocument(DB_ID, 'products', p.id);
console.log(` ✓ Eliminado producto: ${p.id} - ${p.name} (${p.supplier})`);
} catch (e) {
console.error(` ✗ Error eliminando ${p.id}:`, e.message);
}
}
console.log('\n=== [2] SANITIZANDO COLECCIÓN MONITORED_GROUPS EN APPWRITE ===');
const groupsRes = await dbs.listDocuments(DB_ID, 'monitored_groups', [sdk.Query.limit(100)]);
console.log(`Total grupos en Appwrite: ${groupsRes.total}`);
const blacklistedJids = [
'120363400148850031@g.us', // SOMOS BLACK DIAMONDS
'120363420942364759@g.us', // TEAM EMPORIO
];
// Agrupar por JID para eliminar duplicados
const groupsByJid = new Map();
for (const doc of groupsRes.documents) {
const jid = doc.jid;
if (!groupsByJid.has(jid)) {
groupsByJid.set(jid, []);
}
groupsByJid.get(jid).push(doc);
}
for (const [jid, docs] of groupsByJid.entries()) {
const isBlacklisted = blacklistedJids.includes(jid) ||
docs.some(d => /black diamonds|team emporio|emporio/i.test(d.name || ''));
// Mantener 1 documento canónico y eliminar duplicados
const canonicalDoc = docs[0];
const duplicates = docs.slice(1);
for (const dup of duplicates) {
try {
await dbs.deleteDocument(DB_ID, 'monitored_groups', dup['$id']);
console.log(` ✓ Eliminado documento duplicado: ${dup['$id']} (${jid})`);
} catch (e) {
console.error(` ✗ Error eliminando duplicado ${dup['$id']}:`, e.message);
}
}
if (isBlacklisted) {
try {
await dbs.updateDocument(DB_ID, 'monitored_groups', canonicalDoc['$id'], {
isMonitored: 0,
autoPublish: 0,
notes: 'IGNORADO EXPLICITAMENTE - NO PUBLICAR NI MONITOREAR',
});
console.log(` 🛑 Grupo marcado como IGNORADO permanentemente: ${canonicalDoc.name} (${jid})`);
} catch (e) {
console.error(` ✗ Error actualizando grupo ${canonicalDoc['$id']}:`, e.message);
}
}
}
console.log('\n=== [3] VERIFICACIÓN FINAL DE GRUPOS EN APPWRITE ===');
const finalRes = await dbs.listDocuments(DB_ID, 'monitored_groups', [sdk.Query.limit(100)]);
finalRes.documents.forEach((d) => {
console.log(`ID: ${d['$id']} | JID: ${d.jid} | isMonitored: ${d.isMonitored} | Name: ${d.name}`);
});
}
main().catch(console.error);

View File

@ -0,0 +1,45 @@
const sdk = require('node-appwrite');
const client = new sdk.Client()
.setEndpoint('https://appwrite.almeira.com.co/v1')
.setProject('69b84df5003bd8957c5c')
.setKey('standard_cce3cdb866ea6433888d40cedd24dc45987372308fdc3dd0697de08648c0e5c7e04b02c3bcee271403fd656b00a5820f04c08b51631284a6e8a3582d8aed2aa33234f3c29cc777e9b162eb54810b789251b0d006a9a3b081ebc2da53b9baa94614b124b3508ad849fd9a4187cc6094f506d4b57f409b69d9562a74df594f3ec3');
const dbs = new sdk.Databases(client);
async function inspectAll() {
let offset = 0;
const limit = 100;
const toDelete = [];
while (true) {
const res = await dbs.listDocuments('sneakers_multistore_db', 'products', [
sdk.Query.limit(limit),
sdk.Query.offset(offset)
]);
if (!res.documents || res.documents.length === 0) break;
for (const doc of res.documents) {
const sup = (doc.supplierName || '').toLowerCase();
const name = (doc.name || '').toLowerCase();
const raw = JSON.stringify(doc).toLowerCase();
if (
sup.includes('emporio') ||
sup.includes('black diamonds') ||
raw.includes('120363400148850031') ||
raw.includes('120363420942364759') ||
name.includes('hoy 🚨') ||
name.includes('liderea con')
) {
toDelete.push({ id: doc.$id, name: doc.name, supplier: doc.supplierName });
}
}
offset += limit;
if (offset >= res.total) break;
}
console.log(`Encontrados ${toDelete.length} productos publicados de estos grupos:`);
toDelete.forEach(p => console.log(` - ID: ${p.id} | Nombre: ${p.name} | Proveedor: ${p.supplier}`));
}
inspectAll().catch(console.error);

View File

@ -0,0 +1,24 @@
const sdk = require('node-appwrite');
const client = new sdk.Client()
.setEndpoint('https://appwrite.almeira.com.co/v1')
.setProject('69b84df5003bd8957c5c')
.setKey('standard_cce3cdb866ea6433888d40cedd24dc45987372308fdc3dd0697de08648c0e5c7e04b02c3bcee271403fd656b00a5820f04c08b51631284a6e8a3582d8aed2aa33234f3c29cc777e9b162eb54810b789251b0d006a9a3b081ebc2da53b9baa94614b124b3508ad849fd9a4187cc6094f506d4b57f409b69d9562a74df594f3ec3');
const dbs = new sdk.Databases(client);
async function inspect() {
const ids = ['snk-1789056531032-5ut8', 'snk-1789057093753-lz2a', 'snk-1789058687729-e6zk'];
for (const id of ids) {
try {
const doc = await dbs.getDocument('sneakers_multistore_db', 'products', id);
console.log('--- PRODUCT:', doc['$id']);
console.log('Name:', doc.name);
console.log('Description:', (doc.description || '').slice(0, 150));
console.log('SupplierPostId:', doc.supplierPostId);
console.log('SupplierName:', doc.supplierName);
console.log('IsActive:', doc.isActive);
} catch(e) {
console.log('Error', id, e.message);
}
}
}
inspect().catch(console.error);

View File

@ -20,9 +20,9 @@ const GROUPS_TO_SEED = [
{
jid: '120363420942364759@g.us',
name: '💎TEAM EMPORIO💎🔥👑❤️‍🔥🤯',
isMonitored: true,
autoPublish: true,
notes: 'Proveedor Mayorista Team Emporio Sneakers',
isMonitored: false,
autoPublish: false,
notes: 'IGNORADO EXPLICITAMENTE - NO PUBLICAR NI MONITOREAR',
},
{
jid: '120363027254133375@g.us',
@ -34,9 +34,9 @@ const GROUPS_TO_SEED = [
{
jid: '120363400148850031@g.us',
name: 'SOMOS BLACK DIAMONDS 💎💎💎',
isMonitored: true,
autoPublish: true,
notes: 'Proveedor Mayorista Black Diamonds Sneakers',
isMonitored: false,
autoPublish: false,
notes: 'IGNORADO EXPLICITAMENTE - NO PUBLICAR NI MONITOREAR',
},
{
jid: '120363317242539420@g.us',