12/7/2025 | Entrada nº 146 | Dentro de Herramientas

Cómo crear un sistema abierto de seguimiento de fuentes públicas

Crea paso a paso un sistema para seleccionar noticias relevantes de un universo de fuentes con la ayuda de la IA, publicarlas en un canal de Telegram, traducirlas a otra lengua en otro canal y publicar los resultados de forma automática en tu web.

Inteligencia pública a tu medida

Un grupo humano, sea una cooperativa, un club de rol o lo que sea, es capaz de generar conocimiento mientras mantenga una agenda propia de temas de conversación. Eso significa referencias comunes, cosas que todos escuchan o leen y de las que puedan discutir. Sin esa conversación común no puede haber un hacer común compartido y por tanto un aprendizaje común diferenciado.

Todos sabemos sin embargo lo difícil que es mantener una agenda de noticias común sobre un tema específico. La presión mediática es brutal, sus temas son cansinos, sus debates están viciados... Poco hay que aprender y aún menos que lleve a un hacer colectivo útil para nadie.

Pero no será porque falten fuentes de información. Monitorear un tema, seleccionar las noticias que aportan algo y compartirlas para poder charlar sobre ellas nos abre mundos. Nuestro propio canal es un ejemplo.

En realidad sólo hace falta buscar un juego de fuentes lo suficientemente actualizadas y diversas de distintos lugares e idiomas... y seleccionar lo interesante que aparezca en ellas.

La cuestión es que eso lleva tiempo. Sólo la veintena de RSSs que seguimos para hacer la selección de noticias sobre cooperativismo del canal @maximalistas, produce más de 300 titulares al día. Leerlos y decidir rápidamente qué podía ser interesante nos consumía casi una hora cada mañana. Traducir los resultados entre idiomas, casi otro tanto.

Así que nos preguntamos si combinando un poquito de software sencillo (en javascript) e Inteligencia Artificial, podríamos agilizar el proceso de selección de noticias y automatizar traducciones.

Por otro lado, si automatizábamos la inserción de resultados en una web pública, podíamos convertir páginas estáticas en dinámicas y darles mucho más interés y sentido, y de hecho compartir los resultados con quienes no usan Telegram, que no deja de ser un servicio digital centralizado.

Lo que sigue es una serie de herramientas muy sencillas que hemos hecho en el intento de responder a esos objetivos.

Antes de entrar en faena, eso sí, hemos de deciros algo: ahorramos (mucho) tiempo ahora, pero también perdemos algunas noticias que antes hubiéramos seleccionado y que, a veces, son importantes. Así que, al menos una vez a la semana, damos un paseo por las fuentes a la vieja usanza, leyendo todos los titulares y buscando contexto para las entras más interesantes seleccionadas durante la semana.

Seleccionador de noticias

Instalación

  • Crea una cuenta en https://platform.openai.com y obtén una API key.
  • Crea un bot de Telegram con Botfather y dale poderes de admin en el canal en el que quieras publicar. Guarda el token y el ChatID.

Copia todos los archivos (al final de este artículo) en el directorio en el que vayas a correr el programa. Crea en él un archivo llamado .env con la clave de OpenAI y el token y ChatID de Telegram:

OPENAI_API_KEY=sk-...
TELEGRAM_BOT_TOKEN=...
TELEGRAM_CHAT_ID=...

Colócate en la carpeta con la línea de comandos e instala las dependencias

npm install axios rss-parser openai dotenv

Especifica las fuentes RSS de las que nos nutriremos de noticias en el archivo sources.json que crearás en el directorio del siguiente modo:

[
  {
    "name": "RSS de ejemplo",
    "url": "https://ejemplo1.com/feed/"
  }
 {
    "name": "ejemplo 2",
    "url": "https://ejemplo2.com/rss.xml"
  }
]

Crea bot.js utilizando el código que tienes abajo. Edita el prompt (lo que le pedimos a la IA) en la función buildPrompt(item) en el espacio que encontrarás en el código entre la comilla que sigue al return y la que es seguida por ;}. Aquí tienes un ejemplo que usamos para hacer el canal @maximalistas.

function buildPrompt(item) {
  const selectedExamples = examples.slice(-16).map(ex => `Título: "${ex.title}"
Resumen generado: ${ex.resumen_original}
Decisión: ${ex.decision}
Resumen final: ${ex.resumen_final}`).join('\n\n');

  return `Estos son ejemplos previos de evaluación de titulares:\n\n${selectedExamples}\n\nAhora analiza este nuevo titular:\n\nTítulo: "${item.title}"\n\nSi la noticia está relacionada con:
- Cooperativas de trabajo, worker cooperatives, SCOP, cooperativa di lavoratori, coopératives ouvrières, mitarbeiter genossenschaft, autogestión, selfmanagement, autogestione, autogestion
- Compras de empresas por sus trabajadores, workers buyout, employees ownership 
- Casos de éxito, relevancia social, crecimiento, impacto positivo de cooperativas de trabajo y sociedades laborales
- Reflexiones valiosas de pensadores o articulistas sobre el modelo cooperativo

Y:

- Afecta sólo a países europeos, EEUU, Canadá, Norte de Africa, Oriente Medio, Australia, Nueva Zelanda, Gran Bretaña, Rusia, Europa del Este, Japón, Marruecos, Argelia, Túnez, Líbano, Israel, Turquía, Unión Europea 

Devuelve un resumen breve en español (máximo 30 palabras) con el siguiente formato:

#País. #CooperativaDeTrabajo. Resumen. ${cleanURL(item.link)}

Si el contenido no es relevante para ese tema, responde sólo con: IGNORAR.`;
}

Correr el programa

Nos colocamos en el directorio y ejecutamos bot.js

node bot.js

Afinando y entrenando a la IA

Cada vez que respondes con s (enviar), e (editar) o d (descartar), el bot guarda un objeto como este en examples.json:

{
  "title": "Título de la noticia",
  "link": "https://...",
  "decision": "enviada" | "editada" | "descartada",
  "resumen_original": "...",
  "resumen_final": "..."
}

Luego, los 16 últimos se usan como ejemplos contextuales para mejorar la precisión del prompt. Si quieres aumentar el número o reducirlo, tienes que cambiar el -16 por el número de entradas que quieras enviar con un menos delante, en bot.js dentro de la función buildPrompt()

function buildPrompt(item) {
const selectedExamples = examples.slice(-16).map(ex => `Título: "${ex.title}"
Resumen generado: ${ex.resumen_original}
Decisión: ${ex.decision}
Resumen final: ${ex.resumen_final}`).join('\n\n');
...

Si crees que está tomando alguna escora, porque todas las noticias de los últimos días apuntan en una única dirección, elimina o vacía los archivos de ejemplos y cachés para empezar de cero el afinado de resultados:

> examples.json && > .sent_articles_cache.json && > .sent_titles_cache.json && > .discarded_cache.json && > published_messages.json

Archivos que debes crear en el directorio

bot.js

// bot.js
import dotenv from 'dotenv';
import axios from 'axios';
import Parser from 'rss-parser';
import OpenAI from 'openai';
import fs from 'fs';
import readline from 'readline';

dotenv.config();

const parser = new Parser();
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

const SOURCES_FILE = './sources.json';
const CACHE_FILE = './.sent_articles_cache.json';
const CACHE_TITLES_FILE = './.sent_titles_cache.json';
const DISCARD_FILE = './.discarded_cache.json';
const EXAMPLES_FILE = './examples.json';
const PUBLISHED_FILE = './published_messages.json';
const MAX_ITEMS_PER_FEED = 16;
const FEED_TIMEOUT = 10000;

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

let sentCache = new Set();
let sentTitles = new Set();
let discardedTitles = new Set();
let examples = [];
let publishedMessages = [];

if (fs.existsSync(CACHE_FILE)) {
  try {
    sentCache = new Set(JSON.parse(fs.readFileSync(CACHE_FILE)));
  } catch {
    console.warn('⚠️ No se pudo cargar el cache de enlaces enviados.');
  }
}

if (fs.existsSync(CACHE_TITLES_FILE)) {
  try {
    sentTitles = new Set(JSON.parse(fs.readFileSync(CACHE_TITLES_FILE)));
  } catch {
    console.warn('⚠️ No se pudo cargar el cache de títulos enviados.');
  }
}

if (fs.existsSync(DISCARD_FILE)) {
  try {
    discardedTitles = new Set(JSON.parse(fs.readFileSync(DISCARD_FILE)));
  } catch {
    console.warn('⚠️ No se pudo cargar el cache de títulos descartados.');
  }
}

if (fs.existsSync(EXAMPLES_FILE)) {
  try {
    examples = JSON.parse(fs.readFileSync(EXAMPLES_FILE));
  } catch {
    console.warn('⚠️ No se pudo cargar el archivo de ejemplos.');
  }
}

if (fs.existsSync(PUBLISHED_FILE)) {
  try {
    const data = JSON.parse(fs.readFileSync(PUBLISHED_FILE));
    const cutoff = Date.now() - 24 * 60 * 60 * 1000;
    publishedMessages = data.filter(m => new Date(m.timestamp).getTime() > cutoff);
  } catch {
    console.warn('⚠️ No se pudo cargar published_messages.json.');
  }
}

const question = (text) => new Promise(resolve => rl.question(text, resolve));
const delay = ms => new Promise(res => setTimeout(res, ms));

function cleanURL(url) {
  try {
    const parsed = new URL(url);
    const bannedParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid'];
    bannedParams.forEach(param => parsed.searchParams.delete(param));
    return parsed.toString();
  } catch {
    return url;
  }
}

function buildPrompt(item) {
  const selectedExamples = examples.slice(-16).map(ex => `Título: "${ex.title}"
Resumen generado: ${ex.resumen_original}
Decisión: ${ex.decision}
Resumen final: ${ex.resumen_final}`).join('\n\n');

  return `Estos son ejemplos previos de evaluación de titulares:\n\n${selectedExamples}\n\nAhora analiza este nuevo titular:\n\nTítulo: "${item.title}"

ESCRIBE AQUÍ TU  PROMPT Y TERMINALO CON UNA DESCRIPCIÓN DE LO QUE TIENE QUE DEVOLVER, POR EJ:

Devuelve un resumen breve en español (máximo 30 palabras) con el siguiente formato:

#País. #CooperativaDeTrabajo. Resumen. ${cleanURL(item.link)}

Y LA INSTRUCCIÓN DE QUE DEVUELVA «IGNORAR» SI EL CONTENIDO NO ES RELEVANTE

Si el contenido no es relevante para ese tema, responde sólo con: IGNORAR.`;
}

async function fetchFeed(url) {
  try {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), FEED_TIMEOUT);
    const response = await axios.get(url, { signal: controller.signal });
    clearTimeout(timeout);
    return await parser.parseString(response.data);
  } catch (error) {
    throw error;
  }
}

async function classifyAndSend(item) {
  if (sentCache.has(item.link) || sentTitles.has(item.title) || discardedTitles.has(item.title)) return;

  const prompt = buildPrompt(item);
  const completion = await openai.chat.completions.create({
    model: 'gpt-4o',
    messages: [{ role: 'user', content: prompt }],
    temperature: 0.4
  });

  let summary = completion.choices[0].message.content.trim();
  if (summary.toUpperCase().startsWith('IGNORAR')) {
    examples.push({ title: item.title, link: item.link, decision: 'descartada', resumen_original: summary, resumen_final: 'IGNORAR' });
    discardedTitles.add(item.title);
    return;
  }

  console.log('\n📝 Resumen generado:\n' + summary);
  const choice = await question('\n¿Enviar (s), editar (e), descartar (d)? ');
  if (choice.toLowerCase() === 's') {
    const parts = summary.split('http');
    const texto = parts[0].trim();
    const link = cleanURL('http' + parts[1].trim());
    const finalMessage = `${texto}\n\n${link}`;
    await axios.post(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`, {
      chat_id: process.env.TELEGRAM_CHAT_ID,
      text: finalMessage,
      parse_mode: 'Markdown'
    });
    console.log(`📤 Enviado: ${finalMessage}`);
    sentCache.add(item.link);
    sentTitles.add(item.title);
    examples.push({ title: item.title, link: item.link, decision: 'enviada', resumen_original: summary, resumen_final: finalMessage });
    publishedMessages.push({ title: item.title, text: finalMessage, timestamp: new Date().toISOString() });
  } else if (choice.toLowerCase() === 'e') {
    const newSummary = await question('📝 Escribe el nuevo resumen: ');
    const newLink = await question('🔗 Escribe el enlace de la noticia: ');
    const finalMessage = `${newSummary.trim()}\n\n${cleanURL(newLink.trim())}`;
    await axios.post(`https://api.telegram.org/bot${process.env.TELEGRAM_BOT_TOKEN}/sendMessage`, {
      chat_id: process.env.TELEGRAM_CHAT_ID,
      text: finalMessage,
      parse_mode: 'Markdown'
    });
    console.log(`📤 Enviado editado: ${finalMessage}`);
    sentCache.add(item.link);
    sentTitles.add(item.title);
    examples.push({ title: item.title, link: item.link, decision: 'editada', resumen_original: summary, resumen_final: finalMessage });
    publishedMessages.push({ title: item.title, text: finalMessage, timestamp: new Date().toISOString() });
  } else {
    console.log('🚫 Entrada descartada.');
    discardedTitles.add(item.title);
    examples.push({ title: item.title, link: item.link, decision: 'descartada', resumen_original: summary, resumen_final: 'IGNORAR' });
  }
}

async function run() {
  console.log('📰 Iniciando recopilación con modelo: gpt-4o');

  const sources = JSON.parse(fs.readFileSync(SOURCES_FILE, 'utf-8'));
  const erroredFeeds = [];

  for (const source of sources) {
    console.log(`🔗 Procesando: ${source.name}`);
    try {
      const feed = await fetchFeed(source.url);
      const items = feed.items.slice(0, MAX_ITEMS_PER_FEED);

      for (const item of items) {
        try {
          await classifyAndSend(item);
          await delay(1000);
        } catch (err) {
          console.error(`⚠️ Error clasificando entrada: ${err.message}`);
        }
      }
    } catch (err) {
      console.error(`⚠️ Error procesando ${source.name}: ${err.message}`);
      erroredFeeds.push(source);
    }
  }

  fs.writeFileSync(CACHE_FILE, JSON.stringify([...sentCache], null, 2));
  fs.writeFileSync(CACHE_TITLES_FILE, JSON.stringify([...sentTitles], null, 2));
  fs.writeFileSync(DISCARD_FILE, JSON.stringify([...discardedTitles], null, 2));
  fs.writeFileSync(EXAMPLES_FILE, JSON.stringify(examples, null, 2));
  fs.writeFileSync(PUBLISHED_FILE, JSON.stringify(publishedMessages, null, 2));

  rl.close();

  if (erroredFeeds.length > 0) {
    console.log('\n📛 Feeds con errores:');
    erroredFeeds.forEach(feed => {
      console.log(`- ${feed.name}: ${feed.url}`);
    });
  }
}

run();

package.json

{
  "name": "Nombre de tu proyecto",
  "version": "1.0.0",
  "description": "Bot de noticias sobre ....",
  "main": "bot.js",
  "type": "module",
  "scripts": {
    "start": "node bot.js"
  },
  "dependencies": {
    "axios": "^1.10.0",
    "dotenv": "^16.6.1",
    "node-telegram-bot-api": "^0.61.0",
    "openai": "^4.104.0",
    "rss-parser": "^3.13.0"
  }
}

Licencia

Todo nuestro software está publicado bajo la Licencia Pública de la Unión Europea v. 1.2

Traductor de canales

Obtén y configura tu cuenta de API de Google Translate

  • Accede a «Google Cloud Console» entrando en https://console.cloud.google.com/
  • En la parte superior, pincha en Nuevo proyecto.
  • Habilita la API de traducción en el menú lateral (≡) > APIs y servicios > Biblioteca y busca Cloud Translation API. Pincha y dale luego a Habilitar
  • Crea la clave API yendo al menú lateral > APIs y servicios > Credenciales y pulsando + CREAR CREDENCIALES
  • Selecciona Clave de API y copia el resultado
  • Pincha Restringir clave y en Restricciones de API, selecciona Cloud Translation API y limita el acceso a la IP de tu servidor.

En Telegram

  • Crea un robot con BotFather y dale permisos de admin en el grupo que quieras traducir y en el que quieras publicar los contenidos traducidos.
  • Copia el token del bot
  • Oobtener API_ID y API_HASH en https://my.telegram.org en la opción API Development Tools

De Telegram sólo necesitamos ya el STRING_SESSION. Para ello corremos el siguiente programa

const { TelegramClient } = require("telegram");
const { StringSession } = require("telegram/sessions");
const input = require("input"); // módulo para pedir inputs por consola
const fetch = require("node-fetch");

const apiId = 22254225; // Consigue esto de https://my.telegram.org
const apiHash = "TU_API_HASH";
const stringSession = new StringSession(""); // Primera vez va vacío

(async () => {
    const client = new TelegramClient(stringSession, apiId, apiHash, { connectionRetries: 5 });
    await client.start({
        phoneNumber: async () => await input.text("Tu número de teléfono: "),
        password: async () => await input.text("Tu contraseña 2FA (si aplica): "),
        phoneCode: async () => await input.text("Código que recibiste por Telegram: "),
        onError: (err) => console.log(err),
    });
    console.log("Sesión guardada:");
    console.log(client.session.save()); // Guarda esto para uso futuro

    await client.sendMessage("me", { message: "Autenticación exitosa." });
})();

Instalación

Sitúate con la línea de comandos en el directorio en el que quieras hacer residente el programa. Vamos a empezar instalando dependencias.

npm install telegram node-fetch dotenv

Crea el archivo .env

API_ID=xxxxxxxx
API_HASH=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
STRING_SESSION=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
GOOGLE_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
SOURCE_CHANNEL=canal_origen_sin_arroba
DEST_CHANNEL=canal_destino_sin_arroba

Crea el archivo traductor.js

require("dotenv").config();

const { TelegramClient } = require("telegram");
const { StringSession } = require("telegram/sessions");
const fetch = require("node-fetch");
const fs = require("fs");
const path = require("path");
const crypto = require("crypto");

// === CONFIGURACIÓN DESDE .env ===
const apiId = Number(process.env.API_ID);
const apiHash = process.env.API_HASH;
const stringSession = new StringSession(process.env.STRING_SESSION);

const SOURCE_CHANNEL = process.env.SOURCE_CHANNEL;
const DEST_CHANNEL = process.env.DEST_CHANNEL;
const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY;

// === VALIDACIÓN DE VARIABLES ===
if (!apiId || !apiHash || !SOURCE_CHANNEL || !DEST_CHANNEL || !GOOGLE_API_KEY || !process.env.STRING_SESSION) {
    console.error("❌ Faltan variables en el archivo .env");
    process.exit(1);
}

// === RUTAS DE ARCHIVOS ===
const LAST_FILE = path.join(__dirname, "lastTranslated.json");
const TEXT_HASH_FILE = path.join(__dirname, "translated_texts.json");

// === CARGAR HASHES TRADUCIDOS ===
let translatedHashCache = new Set();
if (fs.existsSync(TEXT_HASH_FILE)) {
    try {
        translatedHashCache = new Set(JSON.parse(fs.readFileSync(TEXT_HASH_FILE)));
    } catch {
        console.warn("⚠️ No se pudo cargar el cache de hashes traducidos.");
    }
}

// === FUNCIONES ===
async function translate(text) {
    const res = await fetch(`https://translation.googleapis.com/language/translate/v2?key=${GOOGLE_API_KEY}`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
            q: text,
            source: "es",
            target: "en",
            format: "text"
        })
    });

    const data = await res.json();
    if (!data.data?.translations) throw new Error("Error en traducción");
    return data.data.translations[0].translatedText;
}

function hashText(text) {
    return crypto.createHash("sha256").update(text.trim()).digest("hex");
}

function readLastId() {
    if (!fs.existsSync(LAST_FILE)) return 0;
    try {
        const raw = fs.readFileSync(LAST_FILE);
        return JSON.parse(raw).last_id || 0;
    } catch {
        return 0;
    }
}

function saveLastId(id) {
    fs.writeFileSync(LAST_FILE, JSON.stringify({ last_id: id }, null, 2));
}

function saveHashCache() {
    fs.writeFileSync(TEXT_HASH_FILE, JSON.stringify([...translatedHashCache], null, 2));
}

// === EJECUCIÓN PRINCIPAL ===
(async () => {
    const client = new TelegramClient(stringSession, apiId, apiHash, { connectionRetries: 5 });

    await client.start({
        phoneNumber: async () => "",
        password: async () => "",
        phoneCode: async () => "",
        onError: err => console.log("❌ Error de sesión:", err)
    });

    console.log("✅ Cliente Telegram conectado.");

    const lastId = readLastId();
    const messages = await client.getMessages(SOURCE_CHANNEL, { limit: 20 });

    const newMessages = messages
        .filter(m => m.id > lastId && m.message && m.message.trim().length > 0)
        .sort((a, b) => a.id - b.id);

    if (newMessages.length === 0) {
        console.log("🟡 No hay mensajes nuevos para traducir.");
        process.exit();
    }

    for (const msg of newMessages) {
        try {
            const hash = hashText(msg.message);
            if (translatedHashCache.has(hash)) {
                console.log(`⏭️ Saltado (hash duplicado): mensaje ID ${msg.id}`);
                saveLastId(msg.id);
                continue;
            }

            const translated = await translate(msg.message);
            await client.sendMessage(DEST_CHANNEL, { message: translated });

            console.log(`✅ Traducido y reenviado mensaje ID ${msg.id}`);
            translatedHashCache.add(hash);
            saveLastId(msg.id);
        } catch (e) {
            console.error(`❌ Error con mensaje ${msg.id}:`, e.message);
        }
    }

    saveHashCache();
    process.exit();
})();

En el mismo directorio crea el siguiente package.json

{
  "name": "traductor",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "dotenv": "^17.1.0",
    "input": "^1.0.1",
    "libretranslate": "^1.0.1",
    "telegram": "^2.26.22",
    "translate-node": "^1.0.5"
  }
}

Cambiar idioma de origen o de destino

Para cambiar el idioma de origen o de destino busca en traductor.js la función principal

async function translate(text) {
    const res = await fetch(`https://translation.googleapis.com/language/translate/v2?key=${GOOGLE_API_KEY}`, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
            q: text,
            source: "es",
            target: "en",

Y cambia el valor de source por el idioma del canal de origen (fr para francés, en para ingles, pt para portugés, etc.) y/o el valor de target por el código del idioma en el que quieras publicar la traducción.

Para correr el programa

node traductor.js

Licencia

Todo nuestro software está publicado bajo la Licencia Pública de la Unión Europea v. 1.2

Parseador para publicar canales de Telegram y RSS en una web

Preparación

Crea un bot con botfather dale poderes de admin en el canal que quieras publicar en tu web y anota su token.

Instalación

¡En el directorio principal de tu web crea el archivo scripts.js

// Data you know

const TOKEN = 'TOKEN_de_tu_ROBOT'; // Token of the robot handling your Telegram Channel

// Function to fetch RSS updates from multiple feeds
async function fetchRSSUpdates() {
    try {
        const urls = [
            'json.php?url=https://maximalismo.blog/fuenterss'
        ];
        const requests = urls.map(url => fetch(url));
        const responses = await Promise.all(requests);
        const data = await Promise.all(responses.map(response => response.json()));

        let posts = [];
        data.forEach(feed => {
            posts = posts.concat(feed.items);
        });

        // Sort posts by date
        posts.sort((a, b) => new Date(b.pubDate) - new Date(a.pubDate));

        const rssUpdatesContainer = document.getElementById('rss-updates');
        rssUpdatesContainer.innerHTML = '';

        posts.slice(0, 4).forEach(post => {
            const listItem = document.createElement('li');
            listItem.classList.add('list-group-item');

            // Check if post has a thumbnail, otherwise create a circle
            let mediaElement;
            if (post.thumbnail) {
                // Create the img tag if the thumbnail exists
                mediaElement = `<img src="${post.thumbnail}" alt="${post.title}" style="object-fit: cover; float:right; width:120px; height:120px; border-radius: 50%; margin-left: 8px;" />`;
            } else {
                // Create a red circle div with the same size as the image
                mediaElement = `<div style="float:right; width:120px; height:120px; background-color: rgba(0,0,0,0.1); border-radius: 50%; margin-left: 8px;"></div>`;
            }

            // Set innerHTML with the mediaElement and post content
            listItem.innerHTML = `${mediaElement}<strong style="font-size: 1.75rem;"><a href="${post.link}" target="_blank">${post.title}</a></strong><br>${post.description}`;
            rssUpdatesContainer.appendChild(listItem);
        });
    } catch (error) {
        console.error('Error fetching RSS updates:', error);
    }
}

// Function to fetch updates from your Telegram Channel
async function fetchTelegramUpdates() {
    try {
        const url = `https://api.telegram.org/bot${TOKEN}/getUpdates`;
        const response = await fetch(url);
        const data = await response.json();

        if (!data.ok) throw new Error('Failed to fetch Telegram updates');

        const updates = data.result;
        const telegramUpdatesContainer = document.getElementById('telegram-updates');
        telegramUpdatesContainer.innerHTML = '';

        const messagesMap = new Map();

        updates.forEach(update => {
            if (update.channel_post) {
                messagesMap.set(update.channel_post.message_id, update.channel_post);
            } else if (update.edited_channel_post) {
                messagesMap.set(update.edited_channel_post.message_id, update.edited_channel_post);
            }
        });

        const sortedMessages = Array.from(messagesMap.values()).sort((a, b) => b.message_id - a.message_id);

        for (const message of sortedMessages.slice(0, 100)) {
            let text = message.text || message.caption || '';
            if (!text && !message.photo && !message.video) continue;

            // Extract URLs from the message
            const urls = (message.entities || message.caption_entities || [])
                .filter(entity => entity.type === 'url')
                .map(entity => text.substring(entity.offset, entity.offset + entity.length));

            // Remove URLs from the main text
            urls.forEach(url => {
                text = text.replace(url, '').trim();
            });

            // Create list item for the message
            const listItem = document.createElement('li');
            listItem.classList.add('list-group-item');
            listItem.innerHTML = text.replace(/#(\w+)/g, '$1');

            // Format the "Lee más" message depending on the number of URLs
            if (urls.length > 0) {
                const linkedDomains = urls.map((url, index) => {
                    const domain = new URL(url).hostname.replace(/^www\./, '');
                    return `<a href="${url}" target="_blank">${domain}</a>`;
                });

                // Handle 1, 2, or more links
                let readMoreMessage = '';
                if (linkedDomains.length === 1) {
                    readMoreMessage = `Lee más en ${linkedDomains[0]}.`;
                } else if (linkedDomains.length === 2) {
                    readMoreMessage = `Lee más en ${linkedDomains[0]} y ${linkedDomains[1]}.`;
                } else {
                    const lastDomain = linkedDomains.pop();
                    readMoreMessage = `Lee más en ${linkedDomains.join(', ')}, y ${lastDomain}.`;
                }

                listItem.innerHTML += ` ${readMoreMessage}`;
            }

            let skipPost = false;

            if (message.photo) {
                skipPost = await handlePhoto(message.photo, listItem, text);  // Pass 'text' for the alt attribute
            }

            if (message.video && !skipPost) {
                skipPost = await handleVideo(message.video, listItem);  // Handle video if available
            }

            if (!skipPost) {
                telegramUpdatesContainer.appendChild(listItem);
            }
        }
    } catch (error) {
        console.error('Error fetching Telegram updates:', error);
    }
}

// Function to handle photo rendering and add alt attribute to images
async function handlePhoto(photos, listItem, altText) {
    try {
        // Telegram returns an array of photos, the last one is typically the largest
        const largestPhoto = photos[photos.length - 1];
        const fileId = largestPhoto.file_id;

        // Fetch the file path from Telegram API using the file_id
        const filePathResponse = await fetch(`https://api.telegram.org/bot${TOKEN}/getFile?file_id=${fileId}`);
        const fileData = await filePathResponse.json();

        if (!fileData.ok) {
            throw new Error('Failed to get the file path');
        }

        // Construct the image URL using the file_path from the response
        const filePath = fileData.result.file_path;
        const photoUrl = `https://api.telegram.org/file/bot${TOKEN}/${filePath}`;

        // Create an img element
        const img = document.createElement('img');
        img.src = photoUrl;
        img.alt = altText || 'Telegram photo';  // Use post text as the alt attribute

        // Apply the desired style to the image
        img.style.maxWidth = '100%';
        img.style.marginTop = '8px';

        // Append the img to the listItem
        listItem.appendChild(img);

        return false;  // Don't skip the post
    } catch (error) {
        console.error('Error handling photo:', error);
        return true;  // Skip the post on error
    }
}

// Function to handle video rendering (optional)
async function handleVideo(video, listItem) {
    try {
        const fileId = video.file_id;

        // Fetch the file path from Telegram API using the file_id
        const filePathResponse = await fetch(`https://api.telegram.org/bot${TOKEN}/getFile?file_id=${fileId}`);
        const fileData = await filePathResponse.json();

        if (!fileData.ok) {
            throw new Error('Failed to get the file path');
        }

        // Construct the video URL using the file_path from the response
        const filePath = fileData.result.file_path;
        const videoUrl = `https://api.telegram.org/file/bot${TOKEN}/${filePath}`;

        // Create a video element
        const videoElement = document.createElement('video');
        videoElement.src = videoUrl;
        videoElement.controls = true;

        // Apply the desired style to the video
        videoElement.style.maxWidth = '100%';
        videoElement.style.marginTop = '8px';

        // Append the video to the listItem
        listItem.appendChild(videoElement);

        return false;  // Don't skip the post
    } catch (error) {
        console.error('Error handling video:', error);
        return true;  // Skip the post on error
    }
}

document.addEventListener('DOMContentLoaded', () => {
    fetchTelegramUpdates();    
    fetchRSSUpdates();
});

Personaliza las RSS de los blogs cuyos últimos titulares quieras enlazar en portada. Para ello, edita el código anterior y busca la siguiente función:

        const urls = [
            'json.php?url=https://maximalism.blog/fuenterss',
            'json.php?url=https://planet.communia.org/rss.xml',
            'json.php?url=https://slobodnadomena.hr/en/feed/',
            'json.php?url=https://slobodnadomena.hr/hr/feed/',
            'json.php?url=https://memoria.repoblacion.ong/fuenterss'
        ];

Cambia las direcciones que siguen a json.php?url= por las direcciones de las feeds RS-S que te interesen. Puedes agregar o quitar todas las que quieras, pero no olvides poner las comillas simples ni las comas donde tocan.

Crea en el directorio el archivo json.php

 "error", "message" => "Failed to load RSS feed"]);
    }

    $feed = [
        "url" => $url,
        "title" => (string) $rss->channel->title,
        "link" => (string) $rss->channel->link,
        "author" => "",
        "description" => (string) $rss->channel->description,
        "image" => isset($rss->channel->image->url) ? (string) $rss->channel->image->url : ""
    ];

    $items = [];
    foreach ($rss->channel->item as $item) {
        // Extract the first image from the description
        $description = (string) $item->description;
        $image = (string) $item->description->img->attributes()['src'];

        // Prepare the entry with all required fields
        $entry = [
            "title" => (string) $item->title,
            "pubDate" => date("Y-m-d H:i:s", strtotime((string) $item->pubDate)),
            "link" => (string) $item->link,
            "guid" => (string) $item->guid,
            "author" => "",
            "thumbnail" => $image,
            "description" => strip_tags($description),
            "content" => $description,
            "image" => $image,
            "enclosure" => (object)[],
            "categories" => []
        ];

        $items[] = $entry;
    }

    $response = [
        "status" => "ok",
        "feed" => $feed,
        "items" => $items
    ];

    return json_encode($response, JSON_PRETTY_PRINT);
}

// Get the RSS feed URL from the query parameter
if (isset($_GET['url'])) {
    $rssUrl = $_GET['url'];
    echo fetchRssFeed($rssUrl);
} else {
    echo json_encode(["status" => "error", "message" => "No RSS feed URL provided"]);
}
?>

Inserción de los contenidos en la web

En tu página html tienes que llamar al script (recomendamos que al final de la página) con:

<script>scripts.js</script>

Los contenidos extraídos de Telegram se insertarán como elementos de una lista no numerada que podrás personalizar en CSS. Los llamas en el código html de tu web con id="telegram-updates". Por ejemplo:

<ul class="list-group" id="rss-updates">
<!-- Aquí se insertarán las entradas de tu canal telegram de las últimas 24 horas -->
</ul>

Y lo mismo con las RSS con id="rss-updates"

<ul class="list-group" id="rss-updates">
<!-- Aquí se insertarán los posts actualizados de tus feeds favoritas -->
</ul>

Peculiaridades y advertencias

  • El script extrae del feed de Telegram imágenes, audios y vídeos.
  • No borres posts de tu canal de Telegram, seguirán apareciendo en el feed de actividad y por tanto en tu blog. Si quieres que desaparezcan, sencillamente edítalos en el canal y dales otro contenido.
  • El lector de RSS extrae la imagen principal de las RSS generadas por nuestro sistema de blogs. Lo hace con un sistema que no extrae las imágenes asociadas a los posts en Wordpress o Drupal. En esos casos deja un hueco que podrás estilar también. Si quisieras extraerlas, tendrías que modificar el código. Es una de las mejoras pendientes.

Licencia

Todo nuestro software está publicado bajo la Licencia Pública de la Unión Europea v. 1.2

Fin del artículo
Envíanos tus comentarios usando nuestro buzón en Telegram