Files
gods-eye-view/server/providers/regional/news.js
T

113 lines
3.2 KiB
JavaScript

import { fetchRegionalText, fetchRegionalJson } from './http.js';
import { normalizeRegionalArticles } from '../../../src/data/regionalModel.js';
function decodeRssText(value) {
return String(value || '')
.replace(/<!\[CDATA\[([\s\S]*?)\]\]>/g, '$1')
.replace(/&amp;/g, '&')
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
.trim();
}
function rssTag(block, tag) {
return decodeRssText(
new RegExp(`<${tag}(?:\\s[^>]*)?>([\\s\\S]*?)<\\/${tag}>`, 'i').exec(
block,
)?.[1] || '',
);
}
function normalizeRssArticles(xml, limit = 5) {
const seen = new Set();
const articles = [];
for (const match of String(xml || '').matchAll(
/<item>([\s\S]*?)<\/item>/gi,
)) {
const item = match[1];
const title = rssTag(item, 'title').slice(0, 180);
const url = rssTag(item, 'link');
let parsedUrl;
try {
parsedUrl = new URL(url);
} catch {
continue;
}
if (!title || !['http:', 'https:'].includes(parsedUrl.protocol)) continue;
const source = rssTag(item, 'source');
const signature = `${title.toLowerCase()}|${source.toLowerCase() || parsedUrl.hostname}`;
if (seen.has(signature)) continue;
seen.add(signature);
const rawDate = rssTag(item, 'pubDate');
articles.push({
title,
url: parsedUrl.href,
domain: source || parsedUrl.hostname.replace(/^www\./, ''),
publishedAt: Number.isNaN(Date.parse(rawDate))
? null
: new Date(rawDate).toISOString(),
sourceCountry: null,
});
if (articles.length >= limit) break;
}
return articles;
}
async function fetchRegionalNews(place) {
const query = place?.locality || place?.region || place?.country;
if (!query)
return { status: 'unavailable', query: null, articles: [], source: null };
const rssParams = new URLSearchParams({
q: String(query).replace(/["\\]/g, ' ').trim(),
hl: 'en-US',
gl: 'US',
ceid: 'US:en',
});
try {
const xml = await fetchRegionalText(
`https://news.google.com/rss/search?${rssParams}`,
{
headers: { 'User-Agent': 'GodsEyeView/0.1' },
timeoutMs: 12_000,
},
);
const articles = normalizeRssArticles(xml, 5);
if (articles.length)
return { status: 'ready', query, articles, source: 'Google News RSS' };
} catch {
/* fall through to the existing free index */
}
const params = new URLSearchParams({
query: `"${String(query).replace(/["\\]/g, ' ').trim()}"`,
mode: 'artlist',
format: 'json',
maxrecords: '5',
sort: 'datedesc',
timespan: '48h',
});
try {
const payload = await fetchRegionalJson(
`https://api.gdeltproject.org/api/v2/doc/doc?${params}`,
{
headers: { 'User-Agent': 'GodsEyeView/0.1' },
timeoutMs: 12_000,
},
);
const articles = normalizeRegionalArticles(payload, 5);
return {
status: articles.length ? 'ready' : 'empty',
query,
articles,
source: 'GDELT fallback',
};
} catch {
return { status: 'unavailable', query, articles: [], source: null };
}
}
export { fetchRegionalNews };