mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-09-15 13:45:28 +02:00
feat: Telegram OSINT map layer, Osiris intel ports, and maritime settings
Add Telegram OSINT with hourly incremental t.me scraping, metro geocoding separate from news centroids, threat-intercept popup UI with inline media, and HTML markers above alert boxes so pins stay clickable. Expose GFW_API_TOKEN in onboarding and Settings Maritime; harden GFW/CCTV/geo fetchers. Port Osiris- derived recon, SCM, entity graph, malware/cyber feeds, sanctions, and submarine cable layers with tests and documentation. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { Marker } from 'react-map-gl/maplibre';
|
||||
import type { Earthquake, SelectedEntity, Ship, TrackedFlight, UAV } from '@/types/dashboard';
|
||||
import type { SpreadAlertItem } from '@/utils/alertSpread';
|
||||
import { TELEGRAM_MARKER_OFFSET } from '@/components/map/geoJSONBuilders';
|
||||
|
||||
// Shared monospace label style base
|
||||
const LABEL_BASE: React.CSSProperties = {
|
||||
@@ -473,3 +474,60 @@ export function ThreatMarkers({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// -- Telegram OSINT pins (HTML, above threat alert boxes) --
|
||||
interface TelegramOsintMarkersProps {
|
||||
features: GeoJSON.Feature[];
|
||||
onEntityClick?: (entity: SelectedEntity | null) => void;
|
||||
}
|
||||
|
||||
export function TelegramOsintMarkers({ features, onEntityClick }: TelegramOsintMarkersProps) {
|
||||
if (!features.length) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
{features.map((feature) => {
|
||||
if (feature.geometry?.type !== 'Point') return null;
|
||||
const [lng, lat] = feature.geometry.coordinates as [number, number];
|
||||
const props = feature.properties || {};
|
||||
const id = String(props.id || '');
|
||||
if (!id) return null;
|
||||
const postCount = Number(props.post_count || 1);
|
||||
const size = postCount > 1 ? Math.min(30, 16 + Math.log2(postCount) * 5) : 16;
|
||||
|
||||
return (
|
||||
<Marker
|
||||
key={`telegram-osint-${id}`}
|
||||
longitude={lng}
|
||||
latitude={lat}
|
||||
anchor="center"
|
||||
offset={TELEGRAM_MARKER_OFFSET}
|
||||
style={{ zIndex: 95 }}
|
||||
onClick={(e) => {
|
||||
e.originalEvent.stopPropagation();
|
||||
onEntityClick?.({
|
||||
id,
|
||||
type: 'telegram_osint',
|
||||
name: String(props.name || 'Telegram OSINT'),
|
||||
});
|
||||
}}
|
||||
>
|
||||
<div
|
||||
title={`Telegram OSINT${postCount > 1 ? ` (${postCount} posts)` : ''}`}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: '50%',
|
||||
background: '#ef4444',
|
||||
border: '2.5px solid #fca5a5',
|
||||
boxShadow: '0 0 14px rgba(239, 68, 68, 0.75)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
/>
|
||||
</Marker>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,8 @@ export type DynamicMapLayersDataPayload = DynamicMapLayersPayload;
|
||||
|
||||
export type DynamicMapLayersBuildPayload = {
|
||||
bounds: BoundsTuple;
|
||||
/** When true, /api/live-data/fast already bbox-filtered this payload — skip client cull. */
|
||||
serverBboxScoped?: boolean;
|
||||
dtSeconds: number;
|
||||
trackedIcaos: string[];
|
||||
activeLayers: {
|
||||
@@ -173,6 +175,16 @@ function inView(lat: number, lng: number, bounds: BoundsTuple): boolean {
|
||||
return lng >= bounds[0] && lng <= bounds[2] && lat >= bounds[1] && lat <= bounds[3];
|
||||
}
|
||||
|
||||
function passesViewFilter(
|
||||
lat: number,
|
||||
lng: number,
|
||||
bounds: BoundsTuple,
|
||||
serverBboxScoped: boolean,
|
||||
): boolean {
|
||||
if (serverBboxScoped) return true;
|
||||
return inView(lat, lng, bounds);
|
||||
}
|
||||
|
||||
function cleanLabel(value: unknown): string {
|
||||
if (typeof value !== 'string' && typeof value !== 'number') return '';
|
||||
return String(value).trim();
|
||||
@@ -239,6 +251,7 @@ function buildFlightLayerGeoJSONWorker(
|
||||
bounds: BoundsTuple,
|
||||
dtSeconds: number,
|
||||
trackedIcaos: Set<string>,
|
||||
serverBboxScoped: boolean,
|
||||
): FC {
|
||||
if (!flights?.length) return null;
|
||||
const { colorMap, groundedMap, typeLabel, idPrefix, milSpecialMap, useTrackHeading } = config;
|
||||
@@ -248,7 +261,7 @@ function buildFlightLayerGeoJSONWorker(
|
||||
const f = flights[i];
|
||||
if (f.lat == null || f.lng == null) continue;
|
||||
const [iLng, iLat] = interpFlightPosition(f, dtSeconds);
|
||||
if (!inView(iLat, iLng, bounds)) continue;
|
||||
if (!passesViewFilter(iLat, iLng, bounds, serverBboxScoped)) continue;
|
||||
if (f.icao24 && trackedIcaos.has(f.icao24.toLowerCase())) continue;
|
||||
|
||||
const acType = classifyAircraft(f.model, f.aircraft_category);
|
||||
@@ -288,6 +301,7 @@ function buildTrackedFlightsGeoJSONWorker(
|
||||
flights: Flight[] | undefined,
|
||||
bounds: BoundsTuple,
|
||||
dtSeconds: number,
|
||||
serverBboxScoped: boolean,
|
||||
): FC {
|
||||
if (!flights?.length) return null;
|
||||
const features: GeoJSON.Feature[] = [];
|
||||
@@ -296,7 +310,7 @@ function buildTrackedFlightsGeoJSONWorker(
|
||||
const f = flights[i];
|
||||
if (f.lat == null || f.lng == null) continue;
|
||||
const [lng, lat] = interpFlightPosition(f, dtSeconds);
|
||||
if (!inView(lat, lng, bounds)) continue;
|
||||
if (!passesViewFilter(lat, lng, bounds, serverBboxScoped)) continue;
|
||||
|
||||
const alertColor = ('alert_color' in f ? f.alert_color : '') || 'white';
|
||||
const acType = classifyAircraft(f.model, f.aircraft_category);
|
||||
@@ -334,6 +348,7 @@ function buildShipsGeoJSONWorker(
|
||||
activeLayers: DynamicMapLayersBuildPayload['activeLayers'],
|
||||
bounds: BoundsTuple,
|
||||
dtSeconds: number,
|
||||
serverBboxScoped: boolean,
|
||||
): FC {
|
||||
if (
|
||||
!ships?.length ||
|
||||
@@ -353,7 +368,7 @@ function buildShipsGeoJSONWorker(
|
||||
const s = ships[i];
|
||||
if (s.lat == null || s.lng == null) continue;
|
||||
const [iLng, iLat] = interpShipPosition(s, dtSeconds);
|
||||
if (!inView(iLat, iLng, bounds)) continue;
|
||||
if (!passesViewFilter(iLat, iLng, bounds, serverBboxScoped)) continue;
|
||||
if (s.type === 'carrier') continue;
|
||||
|
||||
const isTrackedYacht = Boolean(s.yacht_alert);
|
||||
@@ -394,6 +409,7 @@ function buildSigintGeoJSONWorker(
|
||||
signals: SigintSignal[] | undefined,
|
||||
source: 'meshtastic' | 'aprs',
|
||||
bounds: BoundsTuple,
|
||||
serverBboxScoped: boolean,
|
||||
): FC {
|
||||
if (!signals?.length) return null;
|
||||
const wanted =
|
||||
@@ -405,7 +421,7 @@ function buildSigintGeoJSONWorker(
|
||||
for (let i = 0; i < signals.length; i += 1) {
|
||||
const sig = signals[i];
|
||||
if (!wanted(sig) || sig.lat == null || sig.lng == null) continue;
|
||||
if (!inView(sig.lat, sig.lng, bounds)) continue;
|
||||
if (!passesViewFilter(sig.lat, sig.lng, bounds, serverBboxScoped)) continue;
|
||||
features.push({
|
||||
type: 'Feature',
|
||||
properties: {
|
||||
@@ -537,6 +553,7 @@ function applyFilters(activeFilters: Record<string, string[]> | undefined) {
|
||||
function buildDynamicLayers(payload: DynamicMapLayersBuildPayload): DynamicMapLayersResult {
|
||||
const trackedIcaos = new Set(payload.trackedIcaos);
|
||||
const filtered = applyFilters(payload.activeFilters);
|
||||
const serverBboxScoped = Boolean(payload.serverBboxScoped);
|
||||
return {
|
||||
commercialFlightsGeoJSON: payload.activeLayers.flights
|
||||
? buildFlightLayerGeoJSONWorker(
|
||||
@@ -545,6 +562,7 @@ function buildDynamicLayers(payload: DynamicMapLayersBuildPayload): DynamicMapLa
|
||||
payload.bounds,
|
||||
payload.dtSeconds,
|
||||
trackedIcaos,
|
||||
serverBboxScoped,
|
||||
)
|
||||
: null,
|
||||
privateFlightsGeoJSON: payload.activeLayers.private
|
||||
@@ -554,6 +572,7 @@ function buildDynamicLayers(payload: DynamicMapLayersBuildPayload): DynamicMapLa
|
||||
payload.bounds,
|
||||
payload.dtSeconds,
|
||||
trackedIcaos,
|
||||
serverBboxScoped,
|
||||
)
|
||||
: null,
|
||||
privateJetsGeoJSON: payload.activeLayers.jets
|
||||
@@ -563,6 +582,7 @@ function buildDynamicLayers(payload: DynamicMapLayersBuildPayload): DynamicMapLa
|
||||
payload.bounds,
|
||||
payload.dtSeconds,
|
||||
trackedIcaos,
|
||||
serverBboxScoped,
|
||||
)
|
||||
: null,
|
||||
militaryFlightsGeoJSON: payload.activeLayers.military
|
||||
@@ -572,22 +592,29 @@ function buildDynamicLayers(payload: DynamicMapLayersBuildPayload): DynamicMapLa
|
||||
payload.bounds,
|
||||
payload.dtSeconds,
|
||||
trackedIcaos,
|
||||
serverBboxScoped,
|
||||
)
|
||||
: null,
|
||||
trackedFlightsGeoJSON: payload.activeLayers.tracked
|
||||
? buildTrackedFlightsGeoJSONWorker(filtered.tracked, payload.bounds, payload.dtSeconds)
|
||||
? buildTrackedFlightsGeoJSONWorker(
|
||||
filtered.tracked,
|
||||
payload.bounds,
|
||||
payload.dtSeconds,
|
||||
serverBboxScoped,
|
||||
)
|
||||
: null,
|
||||
shipsGeoJSON: buildShipsGeoJSONWorker(
|
||||
filtered.ships,
|
||||
payload.activeLayers,
|
||||
payload.bounds,
|
||||
payload.dtSeconds,
|
||||
serverBboxScoped,
|
||||
),
|
||||
meshtasticGeoJSON: payload.activeLayers.sigint_meshtastic
|
||||
? buildSigintGeoJSONWorker(dynamicData.sigint, 'meshtastic', payload.bounds)
|
||||
? buildSigintGeoJSONWorker(dynamicData.sigint, 'meshtastic', payload.bounds, serverBboxScoped)
|
||||
: null,
|
||||
aprsGeoJSON: payload.activeLayers.sigint_aprs
|
||||
? buildSigintGeoJSONWorker(dynamicData.sigint, 'aprs', payload.bounds)
|
||||
? buildSigintGeoJSONWorker(dynamicData.sigint, 'aprs', payload.bounds, serverBboxScoped)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -165,8 +165,12 @@ export function buildEarthquakesGeoJSON(earthquakes?: Earthquake[]): FC {
|
||||
properties: {
|
||||
id: i,
|
||||
type: 'earthquake',
|
||||
name: `[M${eq.mag}]\n${eq.place || 'Unknown Location'}`,
|
||||
name: `[M${eq.mag}] ${eq.place || 'Unknown Location'}`,
|
||||
title: eq.title,
|
||||
lat: eq.lat,
|
||||
lng: eq.lng,
|
||||
mag: eq.mag,
|
||||
place: eq.place,
|
||||
},
|
||||
geometry: { type: 'Point' as const, coordinates: [eq.lng, eq.lat] },
|
||||
};
|
||||
@@ -1566,6 +1570,183 @@ export function buildCrowdThreatGeoJSON(threats?: CrowdThreatItem[], inView?: In
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Telegram OSINT ───────────────────────────────────────────────────────
|
||||
|
||||
/** Group geoparsed posts by city-level coordinates (~1 km grid). */
|
||||
export function telegramClusterKey(lat: number, lng: number): string {
|
||||
return `${lat.toFixed(2)}_${lng.toFixed(2)}`;
|
||||
}
|
||||
|
||||
/** Small fixed shift (~5 mi NE) only when a threat alert shares the same city grid. */
|
||||
export const TELEGRAM_ALERT_AVOID_METERS = 8_000;
|
||||
export const TELEGRAM_ALERT_AVOID_BEARING = 45;
|
||||
|
||||
/** HTML marker nudge — threat alerts are DOM overlays that cover map canvas dots. */
|
||||
export const TELEGRAM_MARKER_OFFSET: [number, number] = [28, -24];
|
||||
|
||||
export function telegramClusterNearNewsAlert(
|
||||
lat: number,
|
||||
lng: number,
|
||||
news?: Array<{ coords?: [number, number] | null }> | null,
|
||||
): boolean {
|
||||
if (!news?.length) return false;
|
||||
const key = telegramClusterKey(lat, lng);
|
||||
return news.some((item) => {
|
||||
const coords = item.coords;
|
||||
if (!coords || coords.length < 2) return false;
|
||||
return telegramClusterKey(coords[0], coords[1]) === key;
|
||||
});
|
||||
}
|
||||
|
||||
export function telegramMapPinCoords(
|
||||
lat: number,
|
||||
lng: number,
|
||||
avoidAlert: boolean,
|
||||
): [number, number] {
|
||||
if (!avoidAlert) return [lat, lng];
|
||||
return projectPoint(lat, lng, TELEGRAM_ALERT_AVOID_BEARING, TELEGRAM_ALERT_AVOID_METERS);
|
||||
}
|
||||
|
||||
export function applyTelegramAlertAvoidance(
|
||||
geo: FC,
|
||||
news?: Array<{ coords?: [number, number] | null }> | null,
|
||||
): FC {
|
||||
if (!geo?.features?.length) return geo;
|
||||
return {
|
||||
...geo,
|
||||
features: geo.features.map((feature) => {
|
||||
const geometry = feature.geometry;
|
||||
if (!geometry || geometry.type !== 'Point') return feature;
|
||||
const point = geometry.coordinates;
|
||||
if (!point || point.length < 2) return feature;
|
||||
const lng = point[0];
|
||||
const lat = point[1];
|
||||
const avoid = telegramClusterNearNewsAlert(lat, lng, news);
|
||||
if (!avoid) return feature;
|
||||
const [pinLat, pinLng] = telegramMapPinCoords(lat, lng, true);
|
||||
return {
|
||||
...feature,
|
||||
geometry: {
|
||||
type: 'Point' as const,
|
||||
coordinates: [pinLng, pinLat],
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTelegramOsintGeoJSON(
|
||||
payload?: {
|
||||
posts?: Array<{
|
||||
id: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
link?: string;
|
||||
source?: string;
|
||||
channel?: string;
|
||||
risk_score?: number;
|
||||
coords?: [number, number] | null;
|
||||
}>;
|
||||
},
|
||||
inView?: InViewFilter,
|
||||
): FC {
|
||||
const posts = payload?.posts;
|
||||
if (!posts?.length) return null;
|
||||
|
||||
const clusters = new Map<
|
||||
string,
|
||||
{
|
||||
lat: number;
|
||||
lng: number;
|
||||
posts: NonNullable<typeof posts>;
|
||||
maxRisk: number;
|
||||
}
|
||||
>();
|
||||
|
||||
for (const post of posts) {
|
||||
const coords = post.coords;
|
||||
if (!coords || coords.length < 2) continue;
|
||||
const lat = coords[0];
|
||||
const lng = coords[1];
|
||||
if (inView && !inView(lat, lng)) continue;
|
||||
const key = telegramClusterKey(lat, lng);
|
||||
const bucket = clusters.get(key);
|
||||
if (bucket) {
|
||||
bucket.posts.push(post);
|
||||
bucket.maxRisk = Math.max(bucket.maxRisk, post.risk_score ?? 1);
|
||||
} else {
|
||||
clusters.set(key, {
|
||||
lat,
|
||||
lng,
|
||||
posts: [post],
|
||||
maxRisk: post.risk_score ?? 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!clusters.size) return null;
|
||||
|
||||
return {
|
||||
type: 'FeatureCollection' as const,
|
||||
features: Array.from(clusters.entries()).map(([key, cluster]) => {
|
||||
const lead = cluster.posts[0];
|
||||
const count = cluster.posts.length;
|
||||
return {
|
||||
type: 'Feature' as const,
|
||||
properties: {
|
||||
id: key,
|
||||
type: 'telegram_osint',
|
||||
name:
|
||||
count > 1
|
||||
? `Telegram OSINT (${count} posts)`
|
||||
: lead.title || 'Telegram OSINT',
|
||||
description: lead.description || '',
|
||||
link: lead.link || '',
|
||||
source: lead.source || '',
|
||||
channel: lead.channel || '',
|
||||
risk_score: cluster.maxRisk,
|
||||
post_count: count,
|
||||
},
|
||||
geometry: {
|
||||
type: 'Point' as const,
|
||||
coordinates: [cluster.lng, cluster.lat],
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Malware C2 / URLhaus ─────────────────────────────────────────────────
|
||||
|
||||
export function buildMalwareGeoJSON(
|
||||
payload?: { threats?: Array<{ id: string; lat: number; lng: number; ip: string; malware: string; threat_type?: string; country?: string }> },
|
||||
inView?: InViewFilter,
|
||||
): FC {
|
||||
const threats = payload?.threats;
|
||||
if (!threats?.length) return null;
|
||||
return {
|
||||
type: 'FeatureCollection' as const,
|
||||
features: threats
|
||||
.map((t) => {
|
||||
if (t.lat == null || t.lng == null) return null;
|
||||
if (inView && !inView(t.lat, t.lng)) return null;
|
||||
return {
|
||||
type: 'Feature' as const,
|
||||
properties: {
|
||||
id: t.id,
|
||||
type: 'malware',
|
||||
name: t.malware,
|
||||
ip: t.ip,
|
||||
threat_type: t.threat_type || 'malware',
|
||||
country: t.country || '',
|
||||
},
|
||||
geometry: { type: 'Point' as const, coordinates: [t.lng, t.lat] },
|
||||
};
|
||||
})
|
||||
.filter(Boolean) as GeoJSON.Feature[],
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Wastewater colors by alert level ────────────────────────────────────
|
||||
const WW_COLORS = {
|
||||
alert: '#ff3333', // red — elevated pathogen detected
|
||||
|
||||
@@ -48,6 +48,8 @@ const EMPTY_RESULT: StaticMapLayersResult = {
|
||||
uapSightingsGeoJSON: null,
|
||||
wastewaterGeoJSON: null,
|
||||
crowdthreatGeoJSON: null,
|
||||
malwareGeoJSON: null,
|
||||
telegramOsintGeoJSON: null,
|
||||
};
|
||||
|
||||
let worker: Worker | null = null;
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
buildUapSightingsGeoJSON,
|
||||
buildWastewaterGeoJSON,
|
||||
buildCrowdThreatGeoJSON,
|
||||
buildMalwareGeoJSON,
|
||||
buildTelegramOsintGeoJSON,
|
||||
} from '@/components/map/geoJSONBuilders';
|
||||
import type {
|
||||
AirQualityStation,
|
||||
@@ -44,6 +46,7 @@ import type {
|
||||
VIIRSChangeNode,
|
||||
Volcano,
|
||||
CrowdThreatItem,
|
||||
MalwareThreat,
|
||||
} from '@/types/dashboard';
|
||||
|
||||
type BoundsTuple = [number, number, number, number];
|
||||
@@ -71,6 +74,17 @@ export type StaticMapLayersDataPayload = {
|
||||
uapSightings?: UAPSighting[];
|
||||
wastewater?: WastewaterPlant[];
|
||||
crowdthreat?: CrowdThreatItem[];
|
||||
malwareThreats?: MalwareThreat[];
|
||||
telegramOsintPosts?: Array<{
|
||||
id: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
link?: string;
|
||||
source?: string;
|
||||
channel?: string;
|
||||
risk_score?: number;
|
||||
coords?: [number, number] | null;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type StaticMapLayersBuildPayload = {
|
||||
@@ -95,6 +109,8 @@ export type StaticMapLayersBuildPayload = {
|
||||
uap_sightings: boolean;
|
||||
wastewater: boolean;
|
||||
crowdthreat: boolean;
|
||||
malware_c2: boolean;
|
||||
telegram_osint: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -119,6 +135,8 @@ export type StaticMapLayersResult = {
|
||||
uapSightingsGeoJSON: FC;
|
||||
wastewaterGeoJSON: FC;
|
||||
crowdthreatGeoJSON: FC;
|
||||
malwareGeoJSON: FC;
|
||||
telegramOsintGeoJSON: FC;
|
||||
};
|
||||
|
||||
type SyncRequest = {
|
||||
@@ -191,6 +209,12 @@ function buildStaticLayers(payload: StaticMapLayersBuildPayload): StaticMapLayer
|
||||
uapSightingsGeoJSON: payload.activeLayers.uap_sightings ? buildUapSightingsGeoJSON(staticData.uapSightings) : null,
|
||||
wastewaterGeoJSON: payload.activeLayers.wastewater ? buildWastewaterGeoJSON(staticData.wastewater) : null,
|
||||
crowdthreatGeoJSON: payload.activeLayers.crowdthreat ? buildCrowdThreatGeoJSON(staticData.crowdthreat, inView) : null,
|
||||
malwareGeoJSON: payload.activeLayers.malware_c2
|
||||
? buildMalwareGeoJSON({ threats: staticData.malwareThreats }, inView)
|
||||
: null,
|
||||
telegramOsintGeoJSON: payload.activeLayers.telegram_osint
|
||||
? buildTelegramOsintGeoJSON({ posts: staticData.telegramOsintPosts })
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user