mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-15 15:52:21 +00:00
Serve the News layer without a news server: GDELT, Liveuamap and World News API
With GEV_NEWS_ORIGIN unset, Core answers the News layer's routes itself. Conflicts need no key: the last 24 h of GDELT 2.0 event exports, physical violence only (CAMEO 18-20), one report per story (copies of one agency story merged), placed where GDELT put it, a town the article's own words name winning over a bare country, and titled by the article's page title from the GKG export of the same quarter hour (the newest 3 h), else by its address words. GDELT's actor coding is never shown. Exports are read up to the newest GDELT lists; one that cannot be read is given up on without holding the rest back, and an outage of two hours answers 502 rather than an empty feed. With LIVEUAMAP_API_KEY, Liveuamap's mapped items join them. Local reads World News API by the source country at the view centre (WORLDNEWSAPI_KEY, at most WORLDNEWSAPI_DAILY_READS a day), with the Conflicts reports placed inside the area. Disputed areas (11 from Natural Earth, bundled) and territories without a country code are named as themselves and read from no country's outlets; a country's outline leaves them out. Categories, relevance and severity come from shared rules; keyed answers are cached and served while they refresh, and no key or address is logged.
This commit is contained in:
1 parent
edb5837ae5
commit
34c75ff5de
13 files changed
+4399
-32
No files matched your search
+51
-14
@@ -1,11 +1,14 @@
|
||||
import { newsQueryAllowed } from '../../src/layers/news/source.js';
|
||||
import { readResponseTextCapped } from './common/http.js';
|
||||
import { createKeyedNews } from './news/keyed.js';
|
||||
|
||||
// News (/api/news/*): Core serves no news of its own. With GEV_NEWS_ORIGIN
|
||||
// set (a news server such as https://news.example), this forwards exactly
|
||||
// the GET requests the news layer sends there and relays the JSON; without
|
||||
// it, every route answers "not configured" and the layer shows itself
|
||||
// unavailable without retrying.
|
||||
// News (/api/news/*). With GEV_NEWS_ORIGIN set (a news server such as
|
||||
// https://news.example), this forwards exactly the GET requests the news
|
||||
// layer sends there and relays the JSON. Without it (or with a value that
|
||||
// is no plain origin, warned of once), Core answers the same routes itself
|
||||
// (news/keyed.js): Conflicts from GDELT's event exports with no key, and
|
||||
// Liveuamap's items beside them with LIVEUAMAP_API_KEY; Local from World
|
||||
// News API, "key required" while WORLDNEWSAPI_KEY is unset.
|
||||
|
||||
const TIMEOUT_MS = 15_000;
|
||||
const MAX_BYTES = 4 * 1024 * 1024;
|
||||
@@ -34,14 +37,42 @@ export function newsOrigin(raw = process.env.GEV_NEWS_ORIGIN) {
|
||||
}
|
||||
}
|
||||
|
||||
/** Text for the log: no address (a key may ride in one), one short line. */
|
||||
const logText = (error) =>
|
||||
`${error?.name || 'Error'}: ${String(error?.message ?? '')}`
|
||||
.replace(/[a-z][a-z0-9+.-]*:\/\/\S*/gi, '<url>')
|
||||
.replace(/\s+/g, ' ')
|
||||
.slice(0, 200);
|
||||
|
||||
/** GEV_NEWS_ORIGIN as newsOrigin reads it; a value it refuses is warned of once. */
|
||||
function configuredOrigin(env, log) {
|
||||
let warned = null;
|
||||
return () => {
|
||||
const raw = String(env().GEV_NEWS_ORIGIN ?? '').trim();
|
||||
const base = newsOrigin(raw);
|
||||
if (!base && raw && warned !== raw) {
|
||||
warned = raw;
|
||||
// Not the value: it may carry credentials.
|
||||
log(
|
||||
'[news] GEV_NEWS_ORIGIN is not a plain http(s) origin; answering with the built-in news backend',
|
||||
);
|
||||
}
|
||||
return base;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* A thin, validated GET proxy for the news layer's three read routes.
|
||||
* @param {{origin?: () => string|null, fetchImpl?: typeof fetch, timeoutMs?: number}} [options]
|
||||
* A thin, validated GET proxy for the news layer's three read routes, or
|
||||
* their keyed answers without a news server.
|
||||
* @param {{env?: () => object, log?: (message: string) => void, origin?: () => string|null, fetchImpl?: typeof fetch, timeoutMs?: number, keyed?: {answer: (path: string, params: URLSearchParams) => Promise<{status: number, body: object}>}}} [options]
|
||||
*/
|
||||
export function newsProxy({
|
||||
origin = () => newsOrigin(),
|
||||
env = () => process.env,
|
||||
log = (message) => console.warn(message),
|
||||
origin = configuredOrigin(env, log),
|
||||
fetchImpl = (...args) => globalThis.fetch(...args),
|
||||
timeoutMs = TIMEOUT_MS,
|
||||
keyed = createKeyedNews({ env, fetchImpl, log }),
|
||||
} = {}) {
|
||||
const json = (res, status, value) => {
|
||||
if (res.destroyed) return;
|
||||
@@ -64,12 +95,18 @@ export function newsProxy({
|
||||
if (!newsQueryAllowed(target))
|
||||
return json(res, 400, { error: 'invalid_news_request' });
|
||||
const base = origin();
|
||||
if (!base)
|
||||
return json(res, 503, {
|
||||
error: 'news_not_configured',
|
||||
unavailable: true,
|
||||
retryable: false,
|
||||
});
|
||||
if (!base) {
|
||||
try {
|
||||
const { status, body } = await keyed.answer(
|
||||
local.pathname,
|
||||
target.searchParams,
|
||||
);
|
||||
return json(res, status, body);
|
||||
} catch (error) {
|
||||
log(`[news] built-in news answer failed: ${logText(error)}`);
|
||||
return json(res, 502, { error: 'news_unavailable' });
|
||||
}
|
||||
}
|
||||
try {
|
||||
const upstream = await fetchImpl(
|
||||
new URL(`${target.pathname}${target.search}`, base),
|
||||
|
||||
Reference in new issue
Block a user