Fix dashboard PostCard fallback URL generation and hide raw IDs
This commit is contained in:
parent
54220a6c7e
commit
f99709cf35
Binary file not shown.
135
publish-scraper.js
Normal file
135
publish-scraper.js
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
const fs = require('fs');
|
||||||
|
const { Client, Databases, Storage, ID, Query } = require('node-appwrite');
|
||||||
|
require('dotenv').config({ path: '.env.local' });
|
||||||
|
|
||||||
|
let InputFile;
|
||||||
|
try { InputFile = require('node-appwrite/file').InputFile; } catch (e) { InputFile = null; }
|
||||||
|
|
||||||
|
const client = new Client()
|
||||||
|
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT)
|
||||||
|
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID)
|
||||||
|
.setKey(process.env.APPWRITE_API_KEY);
|
||||||
|
|
||||||
|
const databases = new Databases(client);
|
||||||
|
const storage = new Storage(client);
|
||||||
|
const DB_ID = process.env.APPWRITE_DATABASE_ID;
|
||||||
|
const BUCKET_ID = process.env.NEXT_PUBLIC_APPWRITE_MEDIA_BUCKET_ID;
|
||||||
|
|
||||||
|
const urls = [
|
||||||
|
'https://www.multivisiondecolombia.com/node/962',
|
||||||
|
'https://www.multivisiondecolombia.com/node/963',
|
||||||
|
'https://www.multivisiondecolombia.com/node/895',
|
||||||
|
'https://www.multivisiondecolombia.com/node/894',
|
||||||
|
'https://www.multivisiondecolombia.com/node/893',
|
||||||
|
'https://www.multivisiondecolombia.com/node/964'
|
||||||
|
];
|
||||||
|
|
||||||
|
async function extractData(url) {
|
||||||
|
const res = await axios.get(url);
|
||||||
|
const html = res.data;
|
||||||
|
|
||||||
|
let title = '';
|
||||||
|
const titleMatch = html.match(/<h1[^>]*>(.*?)<\/h1>/i) || html.match(/<title>(.*?)<\/title>/i);
|
||||||
|
if (titleMatch) title = titleMatch[1].replace(/\|.*/, '').trim();
|
||||||
|
|
||||||
|
let image = null;
|
||||||
|
const imgMatch = html.match(/<img[^>]+src="([^"]+)"[^>]*>/ig);
|
||||||
|
if (imgMatch) {
|
||||||
|
// Find the main image, usually in a field like field--name-field-image
|
||||||
|
for (const imgTag of imgMatch) {
|
||||||
|
const srcMatch = imgTag.match(/src="([^"]+)"/);
|
||||||
|
if (srcMatch && (srcMatch[1].includes('public/imagenes') || srcMatch[1].includes('styles/'))) {
|
||||||
|
image = srcMatch[1].startsWith('http') ? srcMatch[1] : 'https://www.multivisiondecolombia.com' + srcMatch[1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let date = new Date().toISOString();
|
||||||
|
// Look for <time datetime="..."> or a date string
|
||||||
|
const timeMatch = html.match(/<time[^>]+datetime="([^"]+)"/);
|
||||||
|
if (timeMatch) date = new Date(timeMatch[1]).toISOString();
|
||||||
|
|
||||||
|
let content = '';
|
||||||
|
// Look for the main content body, often in field--name-body or a specific div
|
||||||
|
const bodyMatch = html.match(/<div[^>]+property="schema:text"[^>]*>([\s\S]*?)<\/div>/i) || html.match(/<div[^>]+class="[^"]*field--name-body[^"]*"[^>]*>([\s\S]*?)<\/div>\s*<\/div>/i);
|
||||||
|
if (bodyMatch) {
|
||||||
|
content = bodyMatch[1].trim();
|
||||||
|
} else {
|
||||||
|
// Fallback: extract p tags between article
|
||||||
|
const articleMatch = html.match(/<article[^>]*>([\s\S]*?)<\/article>/i);
|
||||||
|
if (articleMatch) {
|
||||||
|
const pTags = articleMatch[1].match(/<p>([\s\S]*?)<\/p>/ig);
|
||||||
|
if (pTags) content = pTags.join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip out tags except p, b, i, strong, em, ul, li, ol
|
||||||
|
content = content.replace(/<(?!p|b|i|strong|em|ul|li|ol|\/p|\/b|\/i|\/strong|\/em|\/ul|\/li|\/ol)[^>]+>/ig, '');
|
||||||
|
|
||||||
|
return { title, image, date, content };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
try {
|
||||||
|
const list = await databases.listCollections(DB_ID);
|
||||||
|
const map = {};
|
||||||
|
list.collections.forEach(c => { map[c.name.toLowerCase()] = c.$id; });
|
||||||
|
const postsCollectionId = map['posts'] || 'posts';
|
||||||
|
const mediaCollectionId = map['media'] || 'media';
|
||||||
|
const pubTypesCollectionId = map['publication types'] || map['publication_types'] || 'publication_types';
|
||||||
|
|
||||||
|
const typeRes = await databases.listDocuments(DB_ID, pubTypesCollectionId, [Query.equal('codigo', 'NOTICIA')]);
|
||||||
|
const typeId = typeRes.documents.length > 0 ? typeRes.documents[0].$id : null;
|
||||||
|
|
||||||
|
for (let i = 0; i < urls.length; i++) {
|
||||||
|
const url = urls[i];
|
||||||
|
console.log('Processing:', url);
|
||||||
|
const data = await extractData(url);
|
||||||
|
console.log('Extracted Title:', data.title);
|
||||||
|
console.log('Extracted Date:', data.date);
|
||||||
|
console.log('Extracted Image:', data.image);
|
||||||
|
|
||||||
|
let mediaId = null;
|
||||||
|
if (data.image) {
|
||||||
|
try {
|
||||||
|
const imgRes = await axios.get(data.image, { responseType: 'arraybuffer' });
|
||||||
|
const buffer = Buffer.from(imgRes.data, 'binary');
|
||||||
|
const filePath = `/tmp/scraped_img_${i}.jpg`;
|
||||||
|
fs.writeFileSync(filePath, buffer);
|
||||||
|
|
||||||
|
let fileParam;
|
||||||
|
if (InputFile && InputFile.fromPath) fileParam = InputFile.fromPath(filePath, `image${i}.jpg`);
|
||||||
|
else fileParam = fs.createReadStream(filePath);
|
||||||
|
|
||||||
|
const file = await storage.createFile(BUCKET_ID, ID.unique(), fileParam);
|
||||||
|
const mediaDoc = await databases.createDocument(DB_ID, mediaCollectionId, ID.unique(), {
|
||||||
|
file_id: file.$id, bucket_id: BUCKET_ID,
|
||||||
|
titulo: data.title, tipo: 'imagen'
|
||||||
|
});
|
||||||
|
mediaId = mediaDoc.$id;
|
||||||
|
} catch(err) {
|
||||||
|
console.error('Error uploading image:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const doc = {
|
||||||
|
titulo: data.title.substring(0, 100),
|
||||||
|
slug: `multivision-${Date.now()}-${i}`,
|
||||||
|
tipo_publicacion_id: typeId,
|
||||||
|
estado: 'Publicado',
|
||||||
|
fecha_publicacion: data.date,
|
||||||
|
contenido: data.content,
|
||||||
|
imagen_destacada_id: mediaId
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await databases.createDocument(DB_ID, postsCollectionId, ID.unique(), doc);
|
||||||
|
console.log('Published! ID:', res.$id);
|
||||||
|
console.log('---------------------------');
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run();
|
||||||
41
publish-single.js
Normal file
41
publish-single.js
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
const { Client, Databases, ID, Query } = require('node-appwrite');
|
||||||
|
require('dotenv').config({ path: '.env.local' });
|
||||||
|
|
||||||
|
const client = new Client()
|
||||||
|
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT)
|
||||||
|
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID)
|
||||||
|
.setKey(process.env.APPWRITE_API_KEY);
|
||||||
|
|
||||||
|
const databases = new Databases(client);
|
||||||
|
const DB_ID = process.env.APPWRITE_DATABASE_ID;
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
try {
|
||||||
|
const list = await databases.listCollections(DB_ID);
|
||||||
|
const map = {};
|
||||||
|
list.collections.forEach(c => { map[c.name.toLowerCase()] = c.$id; });
|
||||||
|
const postsCollectionId = map['posts'] || 'posts';
|
||||||
|
const pubTypesCollectionId = map['publication types'] || map['publication_types'] || 'publication_types';
|
||||||
|
|
||||||
|
const typeRes = await databases.listDocuments(DB_ID, pubTypesCollectionId, [Query.equal('codigo', 'NOTICIA')]);
|
||||||
|
const typeId = typeRes.documents.length > 0 ? typeRes.documents[0].$id : null;
|
||||||
|
|
||||||
|
const bodyHtml = `<p>En esta Semana Mayor, el departamento se prepara para una de las temporadas turísticas y culturales más importantes del año. Para potenciar la economía local, la Gobernación del Cauca lanzó oficialmente la Ruta "Cauca Lo Es Todo", una estrategia que reúne 5 ferias comerciales diseñadas para enamorar a propios y visitantes.</p><p>Estas vitrinas comerciales, distribuidas estratégicamente, serán el escenario perfecto para disfrutar de la tradición, la gastronomía y las artesanías de la región. Es una apuesta directa por el turismo sostenible, donde el talento de los emprendedores de las diferentes subregiones se convierte en el protagonista.</p><p>La administración departamental invita a la comunidad a recorrer estas ferias, apoyar los negocios locales y descubrir por qué en esta Semana Santa, el #CaucaLoEsTodo.</p><p><em>Etiquetas: #CaucaLoEsTodo #SemanaSanta #Emprendimiento #Cultura #Tradición #GobernaciónDelCauca</em></p>`;
|
||||||
|
|
||||||
|
const doc = {
|
||||||
|
titulo: "🌅 Semana Santa con sello local: La ruta 'Cauca Lo Es Todo' activa 5 ferias comerciales",
|
||||||
|
slug: 'ruta-cauca-lo-es-todo-ferias',
|
||||||
|
tipo_publicacion_id: typeId,
|
||||||
|
estado: 'Publicado',
|
||||||
|
fecha_publicacion: new Date('2026-03-30T10:00:00.000Z').toISOString(),
|
||||||
|
contenido: bodyHtml,
|
||||||
|
resumen_redes: 'La Gobernación invita a recorrer el departamento durante la Semana Mayor a través de 5 vitrinas comerciales que resaltan el talento de nuestros emprendedores.',
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await databases.createDocument(DB_ID, postsCollectionId, ID.unique(), doc);
|
||||||
|
console.log('Publicación exitosa! ID:', res.$id);
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run();
|
||||||
@ -39,8 +39,8 @@ function resolveImageUrl(
|
|||||||
return `${endpoint}/storage/buckets/${bucket_id}/files/${file_id}/preview?project=${projectId}&width=600&height=400&quality=80`;
|
return `${endpoint}/storage/buckets/${bucket_id}/files/${file_id}/preview?project=${projectId}&width=600&height=400&quality=80`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: just an ID, use default 'media' bucket
|
// If it's a string, we can't generate a valid URL because it's a document ID, not a file ID.
|
||||||
return `${endpoint}/storage/buckets/media/files/${post.imagen_destacada_id}/preview?project=${projectId}&width=600&height=400&quality=80`;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Maps PostStatus to a visual badge style */
|
/** Maps PostStatus to a visual badge style */
|
||||||
@ -63,6 +63,7 @@ export const PostCard: React.FC<PostCardProps> = ({
|
|||||||
appwriteProjectId,
|
appwriteProjectId,
|
||||||
globalSettings,
|
globalSettings,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [imageError, setImageError] = React.useState(false);
|
||||||
const imageUrl = resolveImageUrl(post, appwriteEndpoint, appwriteProjectId);
|
const imageUrl = resolveImageUrl(post, appwriteEndpoint, appwriteProjectId);
|
||||||
|
|
||||||
// Safe text rendering — no raw HTML
|
// Safe text rendering — no raw HTML
|
||||||
@ -134,11 +135,12 @@ export const PostCard: React.FC<PostCardProps> = ({
|
|||||||
)}
|
)}
|
||||||
{/* Imagen Destacada */}
|
{/* Imagen Destacada */}
|
||||||
<div className="relative aspect-[16/9] bg-slate-800 overflow-hidden">
|
<div className="relative aspect-[16/9] bg-slate-800 overflow-hidden">
|
||||||
{imageUrl ? (
|
{imageUrl && !imageError ? (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
alt={safeTitle}
|
alt={safeTitle}
|
||||||
|
onError={() => setImageError(true)}
|
||||||
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
23
test-appwrite.js
Normal file
23
test-appwrite.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
const { Client, Databases } = require('node-appwrite');
|
||||||
|
require('dotenv').config({ path: '.env.local' });
|
||||||
|
|
||||||
|
const client = new Client()
|
||||||
|
.setEndpoint(process.env.NEXT_PUBLIC_APPWRITE_ENDPOINT)
|
||||||
|
.setProject(process.env.NEXT_PUBLIC_APPWRITE_PROJECT_ID)
|
||||||
|
.setKey(process.env.APPWRITE_API_KEY);
|
||||||
|
|
||||||
|
const databases = new Databases(client);
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
try {
|
||||||
|
const result = await databases.listDocuments(
|
||||||
|
process.env.APPWRITE_DATABASE_ID,
|
||||||
|
'whatsapp_messages'
|
||||||
|
);
|
||||||
|
console.log(`Found ${result.documents.length} messages.`);
|
||||||
|
console.log(JSON.stringify(result.documents, null, 2));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
run();
|
||||||
30
test-evolution.js
Normal file
30
test-evolution.js
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
const axios = require('axios');
|
||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
|
const EVOLUTION_API_URL = 'https://evolution.almeira.com.co';
|
||||||
|
const EVOLUTION_API_KEY = 'mgXxu2eAwyhWNnptFEouUE6qLkvwVm6K';
|
||||||
|
const EVOLUTION_INSTANCE_NAME = 'AutomataAlmeira';
|
||||||
|
const CHAT_ID = '573134848203-1408546044@g.us';
|
||||||
|
|
||||||
|
async function testGetBase64() {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${EVOLUTION_API_URL}/chat/findMessages/${EVOLUTION_INSTANCE_NAME}?page=1&limit=5`,
|
||||||
|
{ where: { "key": { "remoteJid": CHAT_ID } } },
|
||||||
|
{ headers: { 'apikey': EVOLUTION_API_KEY, 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
const msg = response.data.messages.records[0]; // Assuming it's the imageMessage we saw
|
||||||
|
|
||||||
|
const base64Res = await axios.post(
|
||||||
|
`${EVOLUTION_API_URL}/chat/getBase64FromMediaMessage/${EVOLUTION_INSTANCE_NAME}`,
|
||||||
|
{ message: msg },
|
||||||
|
{ headers: { 'apikey': EVOLUTION_API_KEY, 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("Success getting base64. Base64 length:", base64Res.data.base64 ? base64Res.data.base64.length : 'none');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e.response ? e.response.data : e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testGetBase64();
|
||||||
Loading…
Reference in New Issue
Block a user