@@ -9,6 +9,7 @@ const GDELT_GKG_URL = 'https://api.gdeltproject.org/api/v1/gkg_geojson';
99const ACLED_API_URL = 'https://acleddata.com/api/acled/read' ;
1010const CANONICAL_KEY = 'unrest:events:v1' ;
1111const CACHE_TTL = 16200 ; // 4.5h — 6x the 45 min cron interval (was 1.3x)
12+ const MAX_SOURCE_URLS = 5 ;
1213
1314// ---------- ACLED Event Type Mapping (from _shared.ts) ----------
1415
@@ -44,6 +45,50 @@ function classifyGdeltEventType(name) {
4445 return 'UNREST_EVENT_TYPE_PROTEST' ;
4546}
4647
48+ function normalizeSourceUrl ( value ) {
49+ if ( typeof value !== 'string' ) return '' ;
50+ const trimmed = value . trim ( ) ;
51+ if ( ! trimmed ) return '' ;
52+ try {
53+ const parsed = new URL ( trimmed ) ;
54+ if ( parsed . protocol !== 'http:' && parsed . protocol !== 'https:' ) return '' ;
55+ if ( parsed . username || parsed . password ) return '' ;
56+ parsed . hash = '' ;
57+ return parsed . toString ( ) ;
58+ } catch {
59+ return '' ;
60+ }
61+ }
62+
63+ function uniqueSourceUrls ( values ) {
64+ return [ ...new Set ( values . map ( normalizeSourceUrl ) . filter ( Boolean ) ) ] ;
65+ }
66+
67+ function extractAcledSourceUrls ( event ) {
68+ return mergeSourceUrls ( [
69+ event . source_url ,
70+ event . sourceUrl ,
71+ event . url ,
72+ event . link ,
73+ ] ) ;
74+ }
75+
76+ function extractGdeltSourceUrls ( properties = { } ) {
77+ return mergeSourceUrls ( [
78+ properties . url ,
79+ properties . source_url ,
80+ properties . sourceUrl ,
81+ properties . document_url ,
82+ properties . documentUrl ,
83+ properties . article_url ,
84+ properties . articleUrl ,
85+ ] ) ;
86+ }
87+
88+ function mergeSourceUrls ( ...groups ) {
89+ return uniqueSourceUrls ( groups . flatMap ( ( group ) => Array . isArray ( group ) ? group : [ ] ) ) . slice ( 0 , MAX_SOURCE_URLS ) ;
90+ }
91+
4792// ---------- Deduplication (from _shared.ts) ----------
4893
4994function deduplicateEvents ( events ) {
@@ -61,11 +106,14 @@ function deduplicateEvents(events) {
61106 unique . set ( key , event ) ;
62107 } else if ( event . sourceType === 'UNREST_SOURCE_TYPE_ACLED' && existing . sourceType !== 'UNREST_SOURCE_TYPE_ACLED' ) {
63108 event . sources = [ ...new Set ( [ ...event . sources , ...existing . sources ] ) ] ;
109+ event . sourceUrls = mergeSourceUrls ( event . sourceUrls , existing . sourceUrls ) ;
64110 unique . set ( key , event ) ;
65111 } else if ( existing . sourceType === 'UNREST_SOURCE_TYPE_ACLED' ) {
66112 existing . sources = [ ...new Set ( [ ...existing . sources , ...event . sources ] ) ] ;
113+ existing . sourceUrls = mergeSourceUrls ( existing . sourceUrls , event . sourceUrls ) ;
67114 } else {
68115 existing . sources = [ ...new Set ( [ ...existing . sources , ...event . sources ] ) ] ;
116+ existing . sourceUrls = mergeSourceUrls ( existing . sourceUrls , event . sourceUrls ) ;
69117 if ( existing . sources . length >= 2 ) existing . confidence = 'CONFIDENCE_LEVEL_HIGH' ;
70118 }
71119 }
@@ -153,6 +201,7 @@ async function fetchAcledProtests() {
153201 tags : e . tags ?. split ( ';' ) . map ( ( t ) => t . trim ( ) ) . filter ( Boolean ) ?? [ ] ,
154202 actors : [ e . actor1 , e . actor2 ] . filter ( Boolean ) ,
155203 confidence : 'CONFIDENCE_LEVEL_HIGH' ,
204+ sourceUrls : extractAcledSourceUrls ( e ) ,
156205 } ;
157206 } ) ;
158207}
@@ -246,8 +295,16 @@ export async function fetchGdeltEvents(opts = {}) {
246295 if ( feature . properties ?. urltone < existing . worstTone ) {
247296 existing . worstTone = feature . properties . urltone ;
248297 }
298+ existing . sourceUrls = mergeSourceUrls ( existing . sourceUrls , extractGdeltSourceUrls ( feature . properties ) ) ;
249299 } else {
250- locationMap . set ( key , { name, lat, lon, count : 1 , worstTone : feature . properties ?. urltone ?? 0 } ) ;
300+ locationMap . set ( key , {
301+ name,
302+ lat,
303+ lon,
304+ count : 1 ,
305+ worstTone : feature . properties ?. urltone ?? 0 ,
306+ sourceUrls : mergeSourceUrls ( extractGdeltSourceUrls ( feature . properties ) ) ,
307+ } ) ;
251308 }
252309 }
253310
@@ -273,6 +330,7 @@ export async function fetchGdeltEvents(opts = {}) {
273330 tags : [ ] ,
274331 actors : [ ] ,
275332 confidence : loc . count > 20 ? 'CONFIDENCE_LEVEL_HIGH' : 'CONFIDENCE_LEVEL_MEDIUM' ,
333+ sourceUrls : loc . sourceUrls ,
276334 } ) ;
277335 }
278336
0 commit comments