feat(feed): control por identificador de whatsapp para excluir mensajes publicados y descartados y avanzar lista
This commit is contained in:
parent
2e5ea56007
commit
b7e920dcaf
@ -44,7 +44,7 @@ export default function SupplierFeedPage() {
|
|||||||
const [monitoredGroups, setMonitoredGroups] = useState<MonitoredWhatsAppGroup[]>([]);
|
const [monitoredGroups, setMonitoredGroups] = useState<MonitoredWhatsAppGroup[]>([]);
|
||||||
const [existingProducts, setExistingProducts] = useState<Product[]>([]);
|
const [existingProducts, setExistingProducts] = useState<Product[]>([]);
|
||||||
const [selectedGroupFilter, setSelectedGroupFilter] = useState<string>('all');
|
const [selectedGroupFilter, setSelectedGroupFilter] = useState<string>('all');
|
||||||
const [tabStatus, setTabStatus] = useState<'all' | 'pending' | 'published' | 'discarded'>('all');
|
const [tabStatus, setTabStatus] = useState<'pending' | 'published' | 'discarded' | 'all'>('pending');
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isSyncingLive, setIsSyncingLive] = useState(false);
|
const [isSyncingLive, setIsSyncingLive] = useState(false);
|
||||||
const [actionMessage, setActionMessage] = useState<string | null>(null);
|
const [actionMessage, setActionMessage] = useState<string | null>(null);
|
||||||
@ -77,7 +77,7 @@ export default function SupplierFeedPage() {
|
|||||||
// Target brand selector for publishing
|
// Target brand selector for publishing
|
||||||
const [targetBrand, setTargetBrand] = useState<BrandId | 'all'>('almeira');
|
const [targetBrand, setTargetBrand] = useState<BrandId | 'all'>('almeira');
|
||||||
|
|
||||||
// Cargar grupos monitoreados, productos existentes y posts en vivo
|
// Cargar grupos monitoreados, productos existentes y posts en vivo (por defecto PENDIENTES)
|
||||||
const loadInitialData = async () => {
|
const loadInitialData = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
@ -95,8 +95,8 @@ export default function SupplierFeedPage() {
|
|||||||
setExistingProducts(prodData.products);
|
setExistingProducts(prodData.products);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Posts en vivo desde los grupos de WhatsApp (todos los mensajes: leídos y sin leer)
|
// 3. Posts en vivo desde los grupos de WhatsApp (iniciando con PENDIENTES para que la lista avance)
|
||||||
const feedRes = await fetch('/api/whatsapp-feed?fetchLive=true&limit=60');
|
const feedRes = await fetch('/api/whatsapp-feed?fetchLive=true&limit=60&status=pending');
|
||||||
const feedData = await feedRes.json();
|
const feedData = await feedRes.json();
|
||||||
if (feedData.success && Array.isArray(feedData.posts)) {
|
if (feedData.success && Array.isArray(feedData.posts)) {
|
||||||
setPosts(feedData.posts);
|
setPosts(feedData.posts);
|
||||||
@ -120,13 +120,13 @@ export default function SupplierFeedPage() {
|
|||||||
try {
|
try {
|
||||||
const url =
|
const url =
|
||||||
groupJid !== 'all'
|
groupJid !== 'all'
|
||||||
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(groupJid)}`
|
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(groupJid)}&status=${tabStatus}`
|
||||||
: '/api/whatsapp-feed?fetchLive=true&limit=60';
|
: `/api/whatsapp-feed?fetchLive=true&limit=60&status=${tabStatus}`;
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success && Array.isArray(data.posts)) {
|
if (data.success && Array.isArray(data.posts)) {
|
||||||
setPosts(data.posts);
|
setPosts(data.posts);
|
||||||
setActionMessage(`✅ Se cargaron ${data.totalFound || data.posts.length} mensajes (leídos y sin leer) del grupo.`);
|
setActionMessage(`✅ Se cargaron ${data.totalFound || data.posts.length} mensajes.`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
@ -143,15 +143,15 @@ export default function SupplierFeedPage() {
|
|||||||
try {
|
try {
|
||||||
const url =
|
const url =
|
||||||
selectedGroupFilter !== 'all'
|
selectedGroupFilter !== 'all'
|
||||||
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}`
|
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}&status=${tabStatus}`
|
||||||
: '/api/whatsapp-feed?fetchLive=true&limit=60';
|
: `/api/whatsapp-feed?fetchLive=true&limit=60&status=${tabStatus}`;
|
||||||
|
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (data.success && Array.isArray(data.posts)) {
|
if (data.success && Array.isArray(data.posts)) {
|
||||||
setPosts(data.posts);
|
setPosts(data.posts);
|
||||||
setActionMessage(
|
setActionMessage(
|
||||||
`✅ ¡Sincronización completa! Se escanearon ${data.totalFound || data.posts.length} mensajes de WhatsApp (leídos y sin leer) y se sincronizaron con Appwrite.`
|
`✅ ¡Sincronización completa! Se escanearon los mensajes de WhatsApp y se omitieron los ya publicados y descartados.`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setActionMessage('⚠️ No se encontraron mensajes nuevos en los grupos seleccionados.');
|
setActionMessage('⚠️ No se encontraron mensajes nuevos en los grupos seleccionados.');
|
||||||
@ -284,7 +284,7 @@ export default function SupplierFeedPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Descartar mensaje
|
// Descartar mensaje individual
|
||||||
const handleDiscardPost = async (post: SupplierPost) => {
|
const handleDiscardPost = async (post: SupplierPost) => {
|
||||||
try {
|
try {
|
||||||
await fetch('/api/whatsapp-feed', {
|
await fetch('/api/whatsapp-feed', {
|
||||||
@ -300,8 +300,45 @@ export default function SupplierFeedPage() {
|
|||||||
next.delete(post.id);
|
next.delete(post.id);
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
|
setActionMessage(`🗑️ Mensaje #${post.whatsappMessageId || post.id.replace('post-', '')} descartado y archivado. No se volverá a mostrar en pendientes.`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
setActionMessage('❌ Error al descartar mensaje.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Descarte masivo de mensajes seleccionados
|
||||||
|
const handleBulkDiscard = async () => {
|
||||||
|
const selectedPosts = posts.filter(
|
||||||
|
(p) => selectedPostIds.has(p.id) && p.status !== 'discarded'
|
||||||
|
);
|
||||||
|
if (selectedPosts.length === 0) return;
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/whatsapp-feed', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
action: 'bulk-discard',
|
||||||
|
posts: selectedPosts,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success) {
|
||||||
|
setPosts((prev) =>
|
||||||
|
prev.map((p) => (selectedPostIds.has(p.id) ? { ...p, status: 'discarded' } : p))
|
||||||
|
);
|
||||||
|
setSelectedPostIds(new Set());
|
||||||
|
setActionMessage(
|
||||||
|
`🗑️ Se han descartado ${selectedPosts.length} mensajes. No volverán a aparecer en la lista de pendientes.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setActionMessage('❌ Error en descarte masivo.');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -315,6 +352,24 @@ export default function SupplierFeedPage() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Cambiar pestaña y actualizar vista
|
||||||
|
const handleTabChange = async (status: 'pending' | 'published' | 'discarded' | 'all') => {
|
||||||
|
setTabStatus(status);
|
||||||
|
setSelectedPostIds(new Set());
|
||||||
|
setCurrentPage(1);
|
||||||
|
try {
|
||||||
|
const url =
|
||||||
|
selectedGroupFilter !== 'all'
|
||||||
|
? `/api/whatsapp-feed?fetchLive=true&groupJid=${encodeURIComponent(selectedGroupFilter)}&status=${status}`
|
||||||
|
: `/api/whatsapp-feed?fetchLive=true&limit=60&status=${status}`;
|
||||||
|
const res = await fetch(url);
|
||||||
|
const data = await res.json();
|
||||||
|
if (data.success && Array.isArray(data.posts)) {
|
||||||
|
setPosts(data.posts);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
};
|
||||||
|
|
||||||
// Filtrado de posts por estado y grupo
|
// Filtrado de posts por estado y grupo
|
||||||
const filteredPosts = useMemo(() => {
|
const filteredPosts = useMemo(() => {
|
||||||
return posts.filter((p) => {
|
return posts.filter((p) => {
|
||||||
@ -445,7 +500,7 @@ export default function SupplierFeedPage() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setTabStatus('pending')}
|
onClick={() => handleTabChange('pending')}
|
||||||
className={`px-3 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
|
className={`px-3 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
|
||||||
tabStatus === 'pending'
|
tabStatus === 'pending'
|
||||||
? 'bg-purple-600 text-white shadow'
|
? 'bg-purple-600 text-white shadow'
|
||||||
@ -459,7 +514,7 @@ export default function SupplierFeedPage() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setTabStatus('published')}
|
onClick={() => handleTabChange('published')}
|
||||||
className={`px-3 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
|
className={`px-3 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
|
||||||
tabStatus === 'published'
|
tabStatus === 'published'
|
||||||
? 'bg-emerald-600 text-white shadow'
|
? 'bg-emerald-600 text-white shadow'
|
||||||
@ -473,7 +528,7 @@ export default function SupplierFeedPage() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => setTabStatus('discarded')}
|
onClick={() => handleTabChange('discarded')}
|
||||||
className={`px-3 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
|
className={`px-3 py-2 rounded-xl transition-all cursor-pointer flex items-center gap-1.5 ${
|
||||||
tabStatus === 'discarded'
|
tabStatus === 'discarded'
|
||||||
? 'bg-rose-600 text-white shadow'
|
? 'bg-rose-600 text-white shadow'
|
||||||
@ -518,10 +573,18 @@ export default function SupplierFeedPage() {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedPostIds(new Set())}
|
onClick={() => setSelectedPostIds(new Set())}
|
||||||
className="text-xs text-neutral-400 hover:text-white px-3 py-1.5"
|
className="text-xs text-neutral-400 hover:text-white px-3 py-1.5 cursor-pointer"
|
||||||
>
|
>
|
||||||
Deseleccionar
|
Deseleccionar
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleBulkDiscard}
|
||||||
|
disabled={isLoading}
|
||||||
|
className="bg-neutral-800 hover:bg-rose-950/80 border border-neutral-700 hover:border-rose-800 text-neutral-300 hover:text-rose-200 font-bold text-xs px-3.5 py-2 rounded-xl flex items-center gap-1.5 cursor-pointer transition-all"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 text-rose-400" />
|
||||||
|
<span>Descartar Seleccionados</span>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleBulkPublish}
|
onClick={handleBulkPublish}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
@ -643,11 +706,19 @@ export default function SupplierFeedPage() {
|
|||||||
<span className="text-[10px] font-bold text-purple-400 uppercase tracking-wider">
|
<span className="text-[10px] font-bold text-purple-400 uppercase tracking-wider">
|
||||||
{ext?.brand || 'Sneakers'}
|
{ext?.brand || 'Sneakers'}
|
||||||
</span>
|
</span>
|
||||||
{ext?.styleCode && (
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="text-[9px] font-mono bg-neutral-800 text-neutral-300 px-1.5 py-0.5 rounded border border-neutral-700">
|
<span
|
||||||
SKU: {ext.styleCode}
|
className="text-[9px] font-mono bg-neutral-950 text-neutral-400 px-2 py-0.5 rounded-md border border-neutral-800/80"
|
||||||
|
title={`ID WhatsApp: ${post.whatsappMessageId || post.id.replace('post-', '')}`}
|
||||||
|
>
|
||||||
|
#{(post.whatsappMessageId || post.id.replace('post-', '')).slice(-8)}
|
||||||
</span>
|
</span>
|
||||||
)}
|
{ext?.styleCode && (
|
||||||
|
<span className="text-[9px] font-mono bg-neutral-800 text-neutral-300 px-1.5 py-0.5 rounded border border-neutral-700">
|
||||||
|
SKU: {ext.styleCode}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-bold text-white text-sm mt-0.5 line-clamp-1">
|
<h3 className="font-bold text-white text-sm mt-0.5 line-clamp-1">
|
||||||
{ext?.name || 'Zapatilla Urbana'}
|
{ext?.name || 'Zapatilla Urbana'}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import {
|
|||||||
saveSupplierPostToAppwrite,
|
saveSupplierPostToAppwrite,
|
||||||
updateSupplierPostStatusInAppwrite,
|
updateSupplierPostStatusInAppwrite,
|
||||||
saveProductToAppwrite,
|
saveProductToAppwrite,
|
||||||
|
getProductsFromAppwrite,
|
||||||
} from '@/lib/server-appwrite';
|
} from '@/lib/server-appwrite';
|
||||||
import { SupplierPost, MonitoredWhatsAppGroup } from '@/types';
|
import { SupplierPost, MonitoredWhatsAppGroup } from '@/types';
|
||||||
|
|
||||||
@ -23,9 +24,41 @@ export async function GET(request: Request) {
|
|||||||
const fetchLive = searchParams.get('fetchLive') !== 'false';
|
const fetchLive = searchParams.get('fetchLive') !== 'false';
|
||||||
const limit = parseInt(searchParams.get('limit') || '60', 10);
|
const limit = parseInt(searchParams.get('limit') || '60', 10);
|
||||||
const filterJid = searchParams.get('groupJid');
|
const filterJid = searchParams.get('groupJid');
|
||||||
|
const statusFilter = (searchParams.get('status') || 'pending').toLowerCase();
|
||||||
|
|
||||||
// 1. Obtener posts guardados en Appwrite
|
// 1. Obtener posts y productos existentes en Appwrite para control estricto de IDs de mensajes de WhatsApp
|
||||||
const savedPosts = await getSupplierPostsFromAppwrite();
|
const [savedPosts, existingProducts] = await Promise.all([
|
||||||
|
getSupplierPostsFromAppwrite(),
|
||||||
|
getProductsFromAppwrite(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Índices de WhatsApp message IDs ya procesados
|
||||||
|
const publishedMsgIds = new Set<string>();
|
||||||
|
const discardedMsgIds = new Set<string>();
|
||||||
|
const appwritePostMap = new Map<string, SupplierPost>();
|
||||||
|
|
||||||
|
savedPosts.forEach((p) => {
|
||||||
|
appwritePostMap.set(p.id, p);
|
||||||
|
const msgId = p.whatsappMessageId || p.id.replace('post-', '');
|
||||||
|
if (msgId) {
|
||||||
|
if (p.status === 'published' || p.status === 'published_store' || p.status === 'published_facebook') {
|
||||||
|
publishedMsgIds.add(msgId);
|
||||||
|
publishedMsgIds.add(`post-${msgId}`);
|
||||||
|
publishedMsgIds.add(p.id);
|
||||||
|
} else if (p.status === 'discarded' || p.status === 'ignored') {
|
||||||
|
discardedMsgIds.add(msgId);
|
||||||
|
discardedMsgIds.add(`post-${msgId}`);
|
||||||
|
discardedMsgIds.add(p.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
existingProducts.forEach((prod) => {
|
||||||
|
if (prod.whatsappMessageId) {
|
||||||
|
publishedMsgIds.add(prod.whatsappMessageId);
|
||||||
|
publishedMsgIds.add(`post-${prod.whatsappMessageId}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 2. Obtener grupos locales y descubrir grupos en vivo desde Evolution API
|
// 2. Obtener grupos locales y descubrir grupos en vivo desde Evolution API
|
||||||
const localGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
|
const localGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
|
||||||
@ -57,7 +90,7 @@ export async function GET(request: Request) {
|
|||||||
console.warn('Error resolviendo chats de WhatsApp:', e);
|
console.warn('Error resolviendo chats de WhatsApp:', e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Determinar JIDs objetivo a consultar (traer TODOS los mensajes de los grupos)
|
// 3. Determinar JIDs objetivo a consultar
|
||||||
let targetJids: string[] = [];
|
let targetJids: string[] = [];
|
||||||
if (filterJid && filterJid !== 'all') {
|
if (filterJid && filterJid !== 'all') {
|
||||||
targetJids = [filterJid];
|
targetJids = [filterJid];
|
||||||
@ -73,20 +106,34 @@ export async function GET(request: Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!fetchLive || targetJids.length === 0) {
|
if (!fetchLive || targetJids.length === 0) {
|
||||||
|
const filteredSaved = savedPosts.filter((p) => {
|
||||||
|
if (statusFilter === 'pending') return p.status === 'pending_review';
|
||||||
|
if (statusFilter === 'published') return p.status === 'published' || p.status === 'published_store';
|
||||||
|
if (statusFilter === 'discarded') return p.status === 'discarded';
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
isLive: false,
|
isLive: false,
|
||||||
totalFound: savedPosts.length,
|
totalFound: filteredSaved.length,
|
||||||
posts: savedPosts.length > 0 ? savedPosts : dataService.getSupplierPosts(),
|
counts: {
|
||||||
|
pending: savedPosts.filter((p) => p.status === 'pending_review').length,
|
||||||
|
published: publishedMsgIds.size,
|
||||||
|
discarded: discardedMsgIds.size,
|
||||||
|
all: savedPosts.length,
|
||||||
|
},
|
||||||
|
posts: filteredSaved,
|
||||||
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
|
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Consultar mensajes de cada grupo objetivo en paralelo (TODOS los mensajes sin distinción de leídos o sin leer)
|
// 4. Consultar mensajes de cada grupo en paralelo desde Evolution API
|
||||||
const groupFetchPromises = targetJids.map(async (jid) => {
|
const groupFetchPromises = targetJids.map(async (jid) => {
|
||||||
try {
|
try {
|
||||||
|
const fetchLimit = Math.max(limit * 2, 80);
|
||||||
const msgRes = await fetch(
|
const msgRes = await fetch(
|
||||||
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${limit}`,
|
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${fetchLimit}`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -111,15 +158,25 @@ export async function GET(request: Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Ordenar cronológicamente (más recientes primero)
|
// Ordenar cronológicamente (más recientes primero)
|
||||||
allRecords.sort((a, b) => (b.messageTimestamp || 0) - (a.messageTimestamp || 0));
|
allRecords.sort((a, b) => (b.messageTimestamp || 0) - (a.messageTimestamp || 0));
|
||||||
|
|
||||||
// 6. Procesar cada mensaje y descargar imágenes / IA
|
// 5. Procesar mensajes con control estricto de identificador de WhatsApp
|
||||||
const processedPosts: SupplierPost[] = [];
|
const processedPosts: SupplierPost[] = [];
|
||||||
const sliceRecords = allRecords.slice(0, 60);
|
const seenMessageIds = new Set<string>();
|
||||||
|
|
||||||
for (const record of sliceRecords) {
|
for (const record of allRecords) {
|
||||||
const remoteJid = record.key?.remoteJid || '';
|
const remoteJid = record.key?.remoteJid || '';
|
||||||
|
const rawMsgId = (record.key?.id || '').trim();
|
||||||
|
const cleanMsgId = rawMsgId.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 36);
|
||||||
|
const postId = `post-${cleanMsgId || Date.now()}`;
|
||||||
|
|
||||||
|
if (seenMessageIds.has(postId) || (rawMsgId && seenMessageIds.has(rawMsgId))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenMessageIds.add(postId);
|
||||||
|
if (rawMsgId) seenMessageIds.add(rawMsgId);
|
||||||
|
|
||||||
const text =
|
const text =
|
||||||
record.message?.conversation ||
|
record.message?.conversation ||
|
||||||
record.message?.extendedTextMessage?.text ||
|
record.message?.extendedTextMessage?.text ||
|
||||||
@ -130,15 +187,46 @@ export async function GET(request: Request) {
|
|||||||
const groupName = groupsMap.get(remoteJid) || record.pushName || 'Grupo Mayorista';
|
const groupName = groupsMap.get(remoteJid) || record.pushName || 'Grupo Mayorista';
|
||||||
const hasImage = Boolean(record.message?.imageMessage);
|
const hasImage = Boolean(record.message?.imageMessage);
|
||||||
|
|
||||||
// Si no tiene texto ni imagen, omitir
|
|
||||||
if (!text && !hasImage) continue;
|
if (!text && !hasImage) continue;
|
||||||
|
|
||||||
const postId = `post-${record.key?.id || Date.now()}`;
|
const existingInAppwrite =
|
||||||
|
appwritePostMap.get(postId) ||
|
||||||
|
savedPosts.find(
|
||||||
|
(p) =>
|
||||||
|
p.id === postId ||
|
||||||
|
p.whatsappMessageId === rawMsgId ||
|
||||||
|
p.whatsappMessageId === cleanMsgId ||
|
||||||
|
(p.rawMessage === text && (p as any).channelJid === remoteJid)
|
||||||
|
);
|
||||||
|
|
||||||
// Verificar si ya existe en Appwrite para preservar su estado de publicación o descarte
|
const isPublished =
|
||||||
const existingInAppwrite = savedPosts.find(
|
existingInAppwrite?.status === 'published' ||
|
||||||
(p) => p.id === postId || (p.rawMessage === text && (p as any).channelJid === remoteJid)
|
existingInAppwrite?.status === 'published_store' ||
|
||||||
);
|
existingInAppwrite?.status === 'published_facebook' ||
|
||||||
|
publishedMsgIds.has(rawMsgId) ||
|
||||||
|
publishedMsgIds.has(cleanMsgId) ||
|
||||||
|
publishedMsgIds.has(postId);
|
||||||
|
|
||||||
|
const isDiscarded =
|
||||||
|
existingInAppwrite?.status === 'discarded' ||
|
||||||
|
existingInAppwrite?.status === 'ignored' ||
|
||||||
|
discardedMsgIds.has(rawMsgId) ||
|
||||||
|
discardedMsgIds.has(cleanMsgId) ||
|
||||||
|
discardedMsgIds.has(postId);
|
||||||
|
|
||||||
|
const effectiveStatus: SupplierPost['status'] = isPublished
|
||||||
|
? 'published'
|
||||||
|
: isDiscarded
|
||||||
|
? 'discarded'
|
||||||
|
: 'pending_review';
|
||||||
|
|
||||||
|
if (statusFilter === 'pending') {
|
||||||
|
if (isPublished || isDiscarded) continue;
|
||||||
|
} else if (statusFilter === 'published') {
|
||||||
|
if (!isPublished) continue;
|
||||||
|
} else if (statusFilter === 'discarded') {
|
||||||
|
if (!isDiscarded) continue;
|
||||||
|
}
|
||||||
|
|
||||||
let imageUrls: string[] = [];
|
let imageUrls: string[] = [];
|
||||||
|
|
||||||
@ -169,10 +257,8 @@ export async function GET(request: Request) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Si ya existía con datos extraídos en Appwrite, usarlo directamente asegurando sanitización
|
|
||||||
let extracted = existingInAppwrite?.extractedProduct;
|
let extracted = existingInAppwrite?.extractedProduct;
|
||||||
if (!extracted) {
|
if (!extracted) {
|
||||||
// NUNCA pasar groupName como texto de calzado. Si text está vacío, pasar string vacío para que Gemini use la imagen y visión sin contaminar el nombre del producto
|
|
||||||
const cleanInputText = text || '';
|
const cleanInputText = text || '';
|
||||||
extracted = await parseSupplierMessage(cleanInputText, imageUrls[0], [groupName]);
|
extracted = await parseSupplierMessage(cleanInputText, imageUrls[0], [groupName]);
|
||||||
} else if (extracted && extracted.name) {
|
} else if (extracted && extracted.name) {
|
||||||
@ -190,12 +276,13 @@ export async function GET(request: Request) {
|
|||||||
|
|
||||||
const post: SupplierPost = {
|
const post: SupplierPost = {
|
||||||
id: postId,
|
id: postId,
|
||||||
|
whatsappMessageId: rawMsgId || cleanMsgId,
|
||||||
channelName: groupName,
|
channelName: groupName,
|
||||||
channelJid: remoteJid,
|
channelJid: remoteJid,
|
||||||
rawMessage: text || `[Foto de Calzado enviada en ${groupName}]`,
|
rawMessage: text || `[Foto de Calzado enviada en ${groupName}]`,
|
||||||
rawImages: imageUrls,
|
rawImages: imageUrls,
|
||||||
extractedProduct: extracted,
|
extractedProduct: extracted,
|
||||||
status: existingInAppwrite?.status || 'pending_review',
|
status: effectiveStatus,
|
||||||
receivedAt: timestamp,
|
receivedAt: timestamp,
|
||||||
supplierName: groupName,
|
supplierName: groupName,
|
||||||
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877',
|
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877',
|
||||||
@ -203,26 +290,23 @@ export async function GET(request: Request) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
processedPosts.push(post);
|
processedPosts.push(post);
|
||||||
|
|
||||||
// Persistir de forma no bloqueante en Appwrite
|
|
||||||
saveSupplierPostToAppwrite(post).catch(() => {});
|
saveSupplierPostToAppwrite(post).catch(() => {});
|
||||||
|
|
||||||
|
if (processedPosts.length >= limit) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergedPosts = [...processedPosts];
|
|
||||||
// Agregar los posts de Appwrite que no hayan estado en esta ráfaga en vivo
|
|
||||||
savedPosts.forEach((sp) => {
|
|
||||||
if (!mergedPosts.some((p) => p.id === sp.id)) {
|
|
||||||
mergedPosts.push(sp);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
dataService.saveSupplierPosts(mergedPosts);
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
isLive: true,
|
isLive: true,
|
||||||
totalFound: mergedPosts.length,
|
statusFilter,
|
||||||
posts: mergedPosts,
|
totalFound: processedPosts.length,
|
||||||
|
counts: {
|
||||||
|
pending: processedPosts.filter((p) => p.status === 'pending_review').length,
|
||||||
|
published: publishedMsgIds.size,
|
||||||
|
discarded: discardedMsgIds.size,
|
||||||
|
all: processedPosts.length + publishedMsgIds.size + discardedMsgIds.size,
|
||||||
|
},
|
||||||
|
posts: processedPosts,
|
||||||
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
|
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -246,7 +330,6 @@ export async function POST(request: Request) {
|
|||||||
marketIntelligence,
|
marketIntelligence,
|
||||||
} = body;
|
} = body;
|
||||||
|
|
||||||
// 1. PUBLICAR UN PRODUCTO A LA TIENDA (APP MÓVIL Y WEB)
|
|
||||||
if (action === 'publish' && post) {
|
if (action === 'publish' && post) {
|
||||||
const retailPrice =
|
const retailPrice =
|
||||||
customRetailPrice ||
|
customRetailPrice ||
|
||||||
@ -267,6 +350,8 @@ export async function POST(request: Request) {
|
|||||||
forbiddenTerms: forbiddenContext,
|
forbiddenTerms: forbiddenContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const whatsappMsgId = post.whatsappMessageId || post.id.replace('post-', '');
|
||||||
|
|
||||||
const newProduct = {
|
const newProduct = {
|
||||||
id: `snk-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
id: `snk-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
||||||
name: prodName,
|
name: prodName,
|
||||||
@ -292,6 +377,7 @@ export async function POST(request: Request) {
|
|||||||
originBadge: proof.badge,
|
originBadge: proof.badge,
|
||||||
supplierName: post.supplierName || post.channelName || 'Proveedor Mayorista',
|
supplierName: post.supplierName || post.channelName || 'Proveedor Mayorista',
|
||||||
supplierPhone: post.supplierPhone || '573167455877',
|
supplierPhone: post.supplierPhone || '573167455877',
|
||||||
|
whatsappMessageId: whatsappMsgId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const added = dataService.addProduct(newProduct as any);
|
const added = dataService.addProduct(newProduct as any);
|
||||||
@ -301,12 +387,11 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: `¡${prodName} publicado y sincronizado en Appwrite, App Móvil y Web!`,
|
message: `¡${prodName} publicado!`,
|
||||||
product: added,
|
product: added,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. PUBLICACIÓN MASIVA (BULK)
|
|
||||||
if (action === 'bulk-publish' && Array.isArray(posts)) {
|
if (action === 'bulk-publish' && Array.isArray(posts)) {
|
||||||
const createdProducts = [];
|
const createdProducts = [];
|
||||||
for (const p of posts) {
|
for (const p of posts) {
|
||||||
@ -328,6 +413,8 @@ export async function POST(request: Request) {
|
|||||||
forbiddenTerms: forbiddenContext,
|
forbiddenTerms: forbiddenContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const whatsappMsgId = p.whatsappMessageId || p.id.replace('post-', '');
|
||||||
|
|
||||||
const newProd = {
|
const newProd = {
|
||||||
id: `snk-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
id: `snk-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
||||||
name: prodName,
|
name: prodName,
|
||||||
@ -352,6 +439,7 @@ export async function POST(request: Request) {
|
|||||||
originBadge: proof.badge,
|
originBadge: proof.badge,
|
||||||
supplierName: p.supplierName || p.channelName || 'Proveedor Mayorista',
|
supplierName: p.supplierName || p.channelName || 'Proveedor Mayorista',
|
||||||
supplierPhone: p.supplierPhone || '573167455877',
|
supplierPhone: p.supplierPhone || '573167455877',
|
||||||
|
whatsappMessageId: whatsappMsgId,
|
||||||
};
|
};
|
||||||
|
|
||||||
const added = dataService.addProduct(newProd as any);
|
const added = dataService.addProduct(newProd as any);
|
||||||
@ -363,19 +451,31 @@ export async function POST(request: Request) {
|
|||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
message: `¡Se publicaron ${createdProducts.length} productos con persistencia en Appwrite!`,
|
message: `¡Se publicaron ${createdProducts.length} productos!`,
|
||||||
products: createdProducts,
|
products: createdProducts,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. DESCARTAR POST
|
|
||||||
if (action === 'discard' && post?.id) {
|
if (action === 'discard' && post?.id) {
|
||||||
await updateSupplierPostStatusInAppwrite(post.id, 'discarded');
|
await updateSupplierPostStatusInAppwrite(post.id, 'discarded');
|
||||||
dataService.updateSupplierPostStatus(post.id, 'discarded');
|
dataService.updateSupplierPostStatus(post.id, 'discarded');
|
||||||
return NextResponse.json({ success: true, message: 'Mensaje descartado' });
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: `Mensaje [${post.whatsappMessageId || post.id}] descartado y archivado.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'bulk-discard' && Array.isArray(posts)) {
|
||||||
|
for (const p of posts) {
|
||||||
|
await updateSupplierPostStatusInAppwrite(p.id, 'discarded');
|
||||||
|
dataService.updateSupplierPostStatus(p.id, 'discarded');
|
||||||
|
}
|
||||||
|
return NextResponse.json({
|
||||||
|
success: true,
|
||||||
|
message: `¡${posts.length} mensajes descartados y archivados con éxito!`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. BÚSQUEDA VISUAL E INTELIGENCIA DE MERCADO SOBRE POST
|
|
||||||
if (action === 'market-research' && post) {
|
if (action === 'market-research' && post) {
|
||||||
const img = post.rawImages && post.rawImages[0];
|
const img = post.rawImages && post.rawImages[0];
|
||||||
const res = await performMarketIntelligenceSearch(img, post.rawMessage, post.extractedProduct?.supplierPrice);
|
const res = await performMarketIntelligenceSearch(img, post.rawMessage, post.extractedProduct?.supplierPrice);
|
||||||
|
|||||||
@ -543,6 +543,7 @@ export async function getProductsFromAppwrite(): Promise<Product[]> {
|
|||||||
isActive: d.isActive !== false,
|
isActive: d.isActive !== false,
|
||||||
supplierName: d.supplierName || '',
|
supplierName: d.supplierName || '',
|
||||||
supplierPhone: d.supplierPhone || '',
|
supplierPhone: d.supplierPhone || '',
|
||||||
|
whatsappMessageId: d.whatsappMessageId || '',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -574,6 +575,7 @@ export async function saveProductToAppwrite(product: Product): Promise<Product>
|
|||||||
isActive: product.isActive !== false,
|
isActive: product.isActive !== false,
|
||||||
supplierName: product.supplierName || '',
|
supplierName: product.supplierName || '',
|
||||||
supplierPhone: product.supplierPhone || '',
|
supplierPhone: product.supplierPhone || '',
|
||||||
|
whatsappMessageId: product.whatsappMessageId || '',
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -620,6 +622,7 @@ export async function getSupplierPostsFromAppwrite(): Promise<SupplierPost[]> {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: d.$id,
|
id: d.$id,
|
||||||
|
whatsappMessageId: d.whatsappMessageId || d.$id.replace('post-', ''),
|
||||||
channelName: d.channelName || '',
|
channelName: d.channelName || '',
|
||||||
channelJid: d.channelJid || '',
|
channelJid: d.channelJid || '',
|
||||||
supplierName: d.supplierName || '',
|
supplierName: d.supplierName || '',
|
||||||
@ -639,7 +642,9 @@ export async function getSupplierPostsFromAppwrite(): Promise<SupplierPost[]> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function saveSupplierPostToAppwrite(post: SupplierPost): Promise<SupplierPost> {
|
export async function saveSupplierPostToAppwrite(post: SupplierPost): Promise<SupplierPost> {
|
||||||
|
const whatsappMessageId = post.whatsappMessageId || post.id.replace('post-', '');
|
||||||
const payload = {
|
const payload = {
|
||||||
|
whatsappMessageId,
|
||||||
channelName: post.channelName || 'Grupo WhatsApp',
|
channelName: post.channelName || 'Grupo WhatsApp',
|
||||||
channelJid: post.channelJid || '',
|
channelJid: post.channelJid || '',
|
||||||
supplierName: post.supplierName || '',
|
supplierName: post.supplierName || '',
|
||||||
@ -653,22 +658,39 @@ export async function saveSupplierPostToAppwrite(post: SupplierPost): Promise<Su
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await serverDatabases.createDocument(DB_ID, 'supplier_posts', post.id, payload);
|
await serverDatabases.createDocument(DB_ID, 'supplier_posts', post.id, payload);
|
||||||
return post;
|
return { ...post, whatsappMessageId };
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.code === 409) {
|
if (e.code === 409) {
|
||||||
try {
|
try {
|
||||||
await serverDatabases.updateDocument(DB_ID, 'supplier_posts', post.id, payload);
|
await serverDatabases.updateDocument(DB_ID, 'supplier_posts', post.id, payload);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
return post;
|
return { ...post, whatsappMessageId };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateSupplierPostStatusInAppwrite(id: string, status: SupplierPost['status']): Promise<boolean> {
|
export async function updateSupplierPostStatusInAppwrite(
|
||||||
|
id: string,
|
||||||
|
status: SupplierPost['status']
|
||||||
|
): Promise<boolean> {
|
||||||
|
const cleanId = id.trim();
|
||||||
|
const whatsappMessageId = cleanId.replace('post-', '');
|
||||||
try {
|
try {
|
||||||
await serverDatabases.updateDocument(DB_ID, 'supplier_posts', id, { status });
|
await serverDatabases.updateDocument(DB_ID, 'supplier_posts', cleanId, {
|
||||||
|
status,
|
||||||
|
whatsappMessageId,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
try {
|
||||||
|
await serverDatabases.createDocument(DB_ID, 'supplier_posts', cleanId, {
|
||||||
|
status,
|
||||||
|
whatsappMessageId,
|
||||||
|
receivedAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,6 +78,7 @@ export interface Product {
|
|||||||
supplierName?: string;
|
supplierName?: string;
|
||||||
supplierPhone?: string;
|
supplierPhone?: string;
|
||||||
supplierSku?: string;
|
supplierSku?: string;
|
||||||
|
whatsappMessageId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -145,6 +146,7 @@ export interface Order {
|
|||||||
|
|
||||||
export interface SupplierPost {
|
export interface SupplierPost {
|
||||||
id: string;
|
id: string;
|
||||||
|
whatsappMessageId?: string;
|
||||||
channelName: string;
|
channelName: string;
|
||||||
channelJid?: string;
|
channelJid?: string;
|
||||||
supplierName?: string;
|
supplierName?: string;
|
||||||
@ -154,6 +156,7 @@ export interface SupplierPost {
|
|||||||
extractedProduct?: ExtractedProductData;
|
extractedProduct?: ExtractedProductData;
|
||||||
status: 'pending_review' | 'published_facebook' | 'published_store' | 'published' | 'discarded' | 'ignored';
|
status: 'pending_review' | 'published_facebook' | 'published_store' | 'published' | 'discarded' | 'ignored';
|
||||||
receivedAt: string;
|
receivedAt: string;
|
||||||
|
publishedAt?: string;
|
||||||
isRead?: boolean;
|
isRead?: boolean;
|
||||||
marketResearchDone?: boolean;
|
marketResearchDone?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,7 +16,7 @@ async function sendApkToCarlos() {
|
|||||||
sizeMB = (stat.size / (1024 * 1024)).toFixed(2);
|
sizeMB = (stat.size / (1024 * 1024)).toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const caption = `🚀 *DripZone Sneakers - APK Oficial Actualizado (Build Ultra-Rápido)*\n\n📅 *Generado:* ${new Date().toLocaleString('es-CO', { timeZone: 'America/Bogota' })}\n📦 *Tamaño:* ${sizeMB} MB\n\n⚡ *Novedades de Rendimiento & Optimización:*\n🖼️ *Optimización Total de Imágenes & WebP:* Motor inteligente de compresión automática para Appwrite Storage, Unsplash y CDNs. Reduce el peso de las imágenes hasta un 85% con carga diferida (\`loading="lazy"\`), decodificación asíncrona y skeletons animados de precarga (\`shimmer\`) para eliminar parpadeos y retrasos.\n📑 *Paginación Dinámica en la App & Web:* Catálogo principal con selector de zapatillas por página (\`12 / 24 / 48\`), navegación interactiva de páginas, contador de modelos y desplazamiento automático suave al cambiar de página.\n📊 *Paginación en Paneles Admin:* Tablas de productos y feed de WhatsApp optimizadas con paginación integrada para fluidez instantánea sin congelar la pantalla.\n🚀 *Despliegue en VPS:* Servidor de producción actualizado y activo en \`https://dripzone.almeira.com.co\`.\n\n📲 *Descarga Directa del APK:* https://dripzone.almeira.com.co/DripZone.apk\n\nDescarga y abre el archivo APK para actualizar la app en tu Android.`;
|
const caption = `🚀 *DripZone Sneakers - APK Oficial Actualizado (Privacidad & Sanitización Total)*\n\n📅 *Generado:* ${new Date().toLocaleString('es-CO', { timeZone: 'America/Bogota' })}\n📦 *Tamaño:* ${sizeMB} MB\n\n🔒 *Novedades de Privacidad Comercial & Sanitización:*\n🛡️ *Cero Nombres de Grupos o Proveedores:* Se sanitizan estrictamente títulos y textos de todas las publicaciones (IA multimodal, WhatsApp Feed, panel admin y Appwrite). Ni en el título ni en la descripción aparece jamás el nombre del grupo de WhatsApp ni el proveedor o bodega.\n💰 *Exclusión Total del Costo del Producto:* Se prohíbe y elimina automáticamente cualquier mención al costo o precio mayorista de compra en títulos, copys y textos públicos. Solo se extrae como dato numérico para cálculo de margen interno en el panel administrativo.\n🚀 *Gitea Sincronizado:* Cambios enviados y respaldados en el servidor de repositorios Gitea (\`gitea.almeira.com.co\`).\n\n📲 *Descarga Directa del APK:* https://dripzone.almeira.com.co/DripZone.apk\n\nDescarga y abre el archivo APK para actualizar la app en tu Android.`;
|
||||||
|
|
||||||
console.log(`📤 [1/2] Enviando notificación oficial a WhatsApp (+${TARGET_PHONE})...`);
|
console.log(`📤 [1/2] Enviando notificación oficial a WhatsApp (+${TARGET_PHONE})...`);
|
||||||
try {
|
try {
|
||||||
|
|||||||
74
scripts/send_whatsapp_delivery.py
Normal file
74
scripts/send_whatsapp_delivery.py
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import urllib.request
|
||||||
|
import json
|
||||||
|
import ssl
|
||||||
|
import sys
|
||||||
|
import io
|
||||||
|
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||||
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
|
||||||
|
|
||||||
|
EVOLUTION_API_URL = "https://evolution.almeira.com.co"
|
||||||
|
EVOLUTION_API_KEY = "EE49029EEB85-4568-8F81-C9FDFC6E2943"
|
||||||
|
INSTANCE_NAME = "AutomataAlmeira"
|
||||||
|
TARGET_PHONE = "573167455877"
|
||||||
|
|
||||||
|
APK_URL = "https://dripzone.almeira.com.co/api/download-apk"
|
||||||
|
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
|
||||||
|
def send_request(endpoint, payload):
|
||||||
|
url = f"{EVOLUTION_API_URL}{endpoint}"
|
||||||
|
data = json.dumps(payload).encode("utf-8")
|
||||||
|
req = urllib.request.Request(url, data=data, headers={
|
||||||
|
"apikey": EVOLUTION_API_KEY,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, context=ctx, timeout=60) as response:
|
||||||
|
return response.status, response.read().decode("utf-8")
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return e.code, e.read().decode("utf-8")
|
||||||
|
except Exception as e:
|
||||||
|
return 0, str(e)
|
||||||
|
|
||||||
|
def send_update():
|
||||||
|
message_text = (
|
||||||
|
"🚀 *DripZone Sneakers - APK Oficial Actualizado (Privacidad Total)*\n\n"
|
||||||
|
"🔒 *Novedades de Privacidad Comercial & Sanitización:*\n"
|
||||||
|
"🛡️ *Cero Nombres de Grupos o Proveedores:* Se sanitizan estrictamente títulos y textos en toda la plataforma "
|
||||||
|
"(IA multimodal, feed de WhatsApp, panel administrativo y Appwrite). Ni en el título ni en la descripción figura jamás "
|
||||||
|
"el nombre del grupo de WhatsApp ni el proveedor/bodega.\n\n"
|
||||||
|
"💰 *Exclusión Total del Costo del Producto:* Se prohíbe y elimina automáticamente cualquier mención al costo o precio mayorista "
|
||||||
|
"de compra en títulos, copys y textos públicos. El costo solo se almacena internamente para control de márgenes.\n\n"
|
||||||
|
"⚡ *Gitea Sincronizado:* Código respaldado en el servidor Gitea (`gitea.almeira.com.co`).\n"
|
||||||
|
"🌐 *VPS Desplegado:* Nueva versión activa en `https://dripzone.almeira.com.co`.\n\n"
|
||||||
|
f"📲 *Enlace directo al APK:* {APK_URL}\n\n"
|
||||||
|
"Descarga el archivo adjunto para instalar o actualizar la aplicación en tu dispositivo Android."
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"📤 [1/2] Enviando mensaje de texto a WhatsApp (+{TARGET_PHONE})...")
|
||||||
|
payload_text = {
|
||||||
|
"number": TARGET_PHONE,
|
||||||
|
"text": message_text,
|
||||||
|
"delay": 1000
|
||||||
|
}
|
||||||
|
status, res = send_request(f"/message/sendText/{INSTANCE_NAME}", payload_text)
|
||||||
|
print(" -> Respuesta de texto:", status, res[:200] if res else "")
|
||||||
|
|
||||||
|
print(f"📤 [2/2] Enviando documento APK a WhatsApp (+{TARGET_PHONE})...")
|
||||||
|
payload_media = {
|
||||||
|
"number": TARGET_PHONE,
|
||||||
|
"mediatype": "document",
|
||||||
|
"mimetype": "application/vnd.android.package-archive",
|
||||||
|
"fileName": "DripZone.apk",
|
||||||
|
"caption": "📲 *DripZone.apk - Versión Oficial Actualizada*",
|
||||||
|
"media": APK_URL
|
||||||
|
}
|
||||||
|
status, res = send_request(f"/message/sendMedia/{INSTANCE_NAME}", payload_media)
|
||||||
|
print(" -> Respuesta de APK:", status, res[:200] if res else "")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
send_update()
|
||||||
@ -15,12 +15,33 @@ export async function GET(request: Request) {
|
|||||||
const fetchLive = searchParams.get('fetchLive') !== 'false';
|
const fetchLive = searchParams.get('fetchLive') !== 'false';
|
||||||
const limit = parseInt(searchParams.get('limit') || '50', 10);
|
const limit = parseInt(searchParams.get('limit') || '50', 10);
|
||||||
const filterJid = searchParams.get('groupJid');
|
const filterJid = searchParams.get('groupJid');
|
||||||
|
const statusFilter = (searchParams.get('status') || 'pending').toLowerCase();
|
||||||
|
|
||||||
// 1. Obtener grupos locales y descubrir grupos en vivo desde Evolution API
|
// 1. Obtener grupos locales y descubrir grupos en vivo desde Evolution API
|
||||||
const localGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
|
const localGroups: MonitoredWhatsAppGroup[] = dataService.getMonitoredGroups();
|
||||||
const groupsMap = new Map<string, string>();
|
const groupsMap = new Map<string, string>();
|
||||||
localGroups.forEach((g) => groupsMap.set(g.jid, g.name));
|
localGroups.forEach((g) => groupsMap.set(g.jid, g.name));
|
||||||
|
|
||||||
|
// Obtener posts almacenados localmente para control de IDs
|
||||||
|
const localPosts = dataService.getSupplierPosts();
|
||||||
|
const publishedMsgIds = new Set<string>();
|
||||||
|
const discardedMsgIds = new Set<string>();
|
||||||
|
const localPostMap = new Map<string, SupplierPost>();
|
||||||
|
|
||||||
|
localPosts.forEach((p) => {
|
||||||
|
localPostMap.set(p.id, p);
|
||||||
|
const msgId = p.whatsappMessageId || p.id.replace('post-', '');
|
||||||
|
if (msgId) {
|
||||||
|
if (p.status === 'published' || p.status === 'published_store' || p.status === 'published_facebook') {
|
||||||
|
publishedMsgIds.add(msgId);
|
||||||
|
publishedMsgIds.add(p.id);
|
||||||
|
} else if (p.status === 'discarded' || p.status === 'ignored') {
|
||||||
|
discardedMsgIds.add(msgId);
|
||||||
|
discardedMsgIds.add(p.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let liveGroups: any[] = [];
|
let liveGroups: any[] = [];
|
||||||
try {
|
try {
|
||||||
const chatsRes = await fetch(`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`, {
|
const chatsRes = await fetch(`${EVOLUTION_API_URL}/chat/findChats/${EVOLUTION_INSTANCE_NAME}`, {
|
||||||
@ -61,12 +82,19 @@ export async function GET(request: Request) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (targetJids.length === 0) {
|
if (targetJids.length === 0 || !fetchLive) {
|
||||||
|
const filtered = localPosts.filter((p) => {
|
||||||
|
if (statusFilter === 'pending') return p.status === 'pending_review';
|
||||||
|
if (statusFilter === 'published') return p.status === 'published';
|
||||||
|
if (statusFilter === 'discarded') return p.status === 'discarded';
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
isLive: true,
|
isLive: false,
|
||||||
totalFound: 0,
|
totalFound: filtered.length,
|
||||||
posts: dataService.getSupplierPosts(),
|
posts: filtered,
|
||||||
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
|
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -74,8 +102,9 @@ export async function GET(request: Request) {
|
|||||||
// 3. Consultar mensajes de cada grupo objetivo en paralelo
|
// 3. Consultar mensajes de cada grupo objetivo en paralelo
|
||||||
const groupFetchPromises = targetJids.map(async (jid) => {
|
const groupFetchPromises = targetJids.map(async (jid) => {
|
||||||
try {
|
try {
|
||||||
|
const fetchLimit = Math.max(limit * 2, 80);
|
||||||
const msgRes = await fetch(
|
const msgRes = await fetch(
|
||||||
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${limit}`,
|
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=${fetchLimit}`,
|
||||||
{
|
{
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -105,12 +134,20 @@ export async function GET(request: Request) {
|
|||||||
|
|
||||||
// 5. Procesar cada mensaje y descargar imágenes/IA
|
// 5. Procesar cada mensaje y descargar imágenes/IA
|
||||||
const processedPosts: SupplierPost[] = [];
|
const processedPosts: SupplierPost[] = [];
|
||||||
|
const seenMessageIds = new Set<string>();
|
||||||
|
|
||||||
// Limitar a los 60 más relevantes para rendimiento óptimo
|
for (const record of allRecords) {
|
||||||
const sliceRecords = allRecords.slice(0, 60);
|
|
||||||
|
|
||||||
for (const record of sliceRecords) {
|
|
||||||
const remoteJid = record.key?.remoteJid || '';
|
const remoteJid = record.key?.remoteJid || '';
|
||||||
|
const rawMsgId = (record.key?.id || '').trim();
|
||||||
|
const cleanMsgId = rawMsgId.replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 36);
|
||||||
|
const postId = `post-${cleanMsgId || Date.now()}`;
|
||||||
|
|
||||||
|
if (seenMessageIds.has(postId) || (rawMsgId && seenMessageIds.has(rawMsgId))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenMessageIds.add(postId);
|
||||||
|
if (rawMsgId) seenMessageIds.add(rawMsgId);
|
||||||
|
|
||||||
const text =
|
const text =
|
||||||
record.message?.conversation ||
|
record.message?.conversation ||
|
||||||
record.message?.extendedTextMessage?.text ||
|
record.message?.extendedTextMessage?.text ||
|
||||||
@ -121,9 +158,38 @@ export async function GET(request: Request) {
|
|||||||
const groupName = groupsMap.get(remoteJid) || record.pushName || 'Grupo Mayorista';
|
const groupName = groupsMap.get(remoteJid) || record.pushName || 'Grupo Mayorista';
|
||||||
const hasImage = Boolean(record.message?.imageMessage);
|
const hasImage = Boolean(record.message?.imageMessage);
|
||||||
|
|
||||||
// Si no tiene texto ni imagen, omitir
|
|
||||||
if (!text && !hasImage) continue;
|
if (!text && !hasImage) continue;
|
||||||
|
|
||||||
|
const existingInLocal =
|
||||||
|
localPostMap.get(postId) ||
|
||||||
|
localPosts.find((p) => p.id === postId || p.whatsappMessageId === rawMsgId || p.whatsappMessageId === cleanMsgId);
|
||||||
|
|
||||||
|
const isPublished =
|
||||||
|
existingInLocal?.status === 'published' ||
|
||||||
|
publishedMsgIds.has(rawMsgId) ||
|
||||||
|
publishedMsgIds.has(cleanMsgId) ||
|
||||||
|
publishedMsgIds.has(postId);
|
||||||
|
|
||||||
|
const isDiscarded =
|
||||||
|
existingInLocal?.status === 'discarded' ||
|
||||||
|
discardedMsgIds.has(rawMsgId) ||
|
||||||
|
discardedMsgIds.has(cleanMsgId) ||
|
||||||
|
discardedMsgIds.has(postId);
|
||||||
|
|
||||||
|
const effectiveStatus: SupplierPost['status'] = isPublished
|
||||||
|
? 'published'
|
||||||
|
: isDiscarded
|
||||||
|
? 'discarded'
|
||||||
|
: 'pending_review';
|
||||||
|
|
||||||
|
if (statusFilter === 'pending' && (isPublished || isDiscarded)) {
|
||||||
|
continue;
|
||||||
|
} else if (statusFilter === 'published' && !isPublished) {
|
||||||
|
continue;
|
||||||
|
} else if (statusFilter === 'discarded' && !isDiscarded) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let imageUrls: string[] = [];
|
let imageUrls: string[] = [];
|
||||||
|
|
||||||
// Descargar imagen en Base64 desde Evolution API si está presente
|
// Descargar imagen en Base64 desde Evolution API si está presente
|
||||||
@ -167,31 +233,39 @@ export async function GET(request: Request) {
|
|||||||
: 'Reciente';
|
: 'Reciente';
|
||||||
|
|
||||||
const post: SupplierPost = {
|
const post: SupplierPost = {
|
||||||
id: `post-${record.key?.id || Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
|
id: postId,
|
||||||
|
whatsappMessageId: rawMsgId || cleanMsgId,
|
||||||
channelName: groupName,
|
channelName: groupName,
|
||||||
channelJid: remoteJid,
|
channelJid: remoteJid,
|
||||||
rawMessage: text || `[Foto de Calzado enviada en ${groupName}]`,
|
rawMessage: text || `[Foto de Calzado enviada en ${groupName}]`,
|
||||||
rawImages: imageUrls,
|
rawImages: imageUrls,
|
||||||
extractedProduct: extracted,
|
extractedProduct: extracted,
|
||||||
status: 'pending_review',
|
status: effectiveStatus,
|
||||||
receivedAt: timestamp,
|
receivedAt: timestamp,
|
||||||
supplierName: groupName,
|
supplierName: groupName,
|
||||||
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877',
|
supplierPhone: record.key?.participant?.replace(/\D/g, '') || '573167455877',
|
||||||
};
|
};
|
||||||
|
|
||||||
processedPosts.push(post);
|
processedPosts.push(post);
|
||||||
}
|
dataService.addSupplierPost(post);
|
||||||
|
|
||||||
// Guardar en el servicio local los nuevos posts
|
if (processedPosts.length >= limit) {
|
||||||
if (processedPosts.length > 0) {
|
break;
|
||||||
dataService.saveSupplierPosts(processedPosts);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
success: true,
|
success: true,
|
||||||
isLive: true,
|
isLive: true,
|
||||||
|
statusFilter,
|
||||||
totalFound: processedPosts.length,
|
totalFound: processedPosts.length,
|
||||||
posts: processedPosts.length > 0 ? processedPosts : dataService.getSupplierPosts(),
|
counts: {
|
||||||
|
pending: processedPosts.filter((p) => p.status === 'pending_review').length,
|
||||||
|
published: publishedMsgIds.size,
|
||||||
|
discarded: discardedMsgIds.size,
|
||||||
|
all: processedPosts.length + publishedMsgIds.size + discardedMsgIds.size,
|
||||||
|
},
|
||||||
|
posts: processedPosts,
|
||||||
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
|
groups: Array.from(groupsMap.entries()).map(([jid, name]) => ({ jid, name })),
|
||||||
});
|
});
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@ -298,7 +372,15 @@ export async function POST(request: Request) {
|
|||||||
// 3. DESCARTAR POST
|
// 3. DESCARTAR POST
|
||||||
if (action === 'discard' && post?.id) {
|
if (action === 'discard' && post?.id) {
|
||||||
dataService.updateSupplierPostStatus(post.id, 'discarded');
|
dataService.updateSupplierPostStatus(post.id, 'discarded');
|
||||||
return NextResponse.json({ success: true, message: 'Mensaje descartado' });
|
return NextResponse.json({ success: true, message: 'Mensaje descartado y archivado' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. DESCARTE MASIVO (BULK DISCARD)
|
||||||
|
if (action === 'bulk-discard' && Array.isArray(posts)) {
|
||||||
|
for (const p of posts) {
|
||||||
|
dataService.updateSupplierPostStatus(p.id, 'discarded');
|
||||||
|
}
|
||||||
|
return NextResponse.json({ success: true, message: `¡${posts.length} mensajes descartados y archivados!` });
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({ success: false, error: 'Acción no válida' }, { status: 400 });
|
return NextResponse.json({ success: false, error: 'Acción no válida' }, { status: 400 });
|
||||||
|
|||||||
@ -72,6 +72,7 @@ export interface Product {
|
|||||||
supplierName?: string;
|
supplierName?: string;
|
||||||
supplierPhone?: string;
|
supplierPhone?: string;
|
||||||
supplierSku?: string;
|
supplierSku?: string;
|
||||||
|
whatsappMessageId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface City {
|
export interface City {
|
||||||
@ -138,6 +139,7 @@ export interface Order {
|
|||||||
|
|
||||||
export interface SupplierPost {
|
export interface SupplierPost {
|
||||||
id: string;
|
id: string;
|
||||||
|
whatsappMessageId?: string;
|
||||||
channelName: string;
|
channelName: string;
|
||||||
channelJid?: string;
|
channelJid?: string;
|
||||||
supplierName?: string;
|
supplierName?: string;
|
||||||
@ -147,6 +149,7 @@ export interface SupplierPost {
|
|||||||
extractedProduct?: ExtractedProductData;
|
extractedProduct?: ExtractedProductData;
|
||||||
status: 'pending_review' | 'published_facebook' | 'published_store' | 'published' | 'discarded' | 'ignored';
|
status: 'pending_review' | 'published_facebook' | 'published_store' | 'published' | 'discarded' | 'ignored';
|
||||||
receivedAt: string;
|
receivedAt: string;
|
||||||
|
publishedAt?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExtractedProductData {
|
export interface ExtractedProductData {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user