mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-08-01 01:34:37 +02:00
Gate messages now propagate via the Infonet hashchain as encrypted blobs — every node syncs them through normal chain sync while only Gate members with MLS keys can decrypt. Added mesh reputation system, peer push workers, voluntary Wormhole opt-in for node participation, fork recovery, killwormhole scripts, obfuscated terminology, and hardened the self-updater to protect encryption keys and chain state during updates. New features: Shodan search, train tracking, Sentinel Hub imagery, 8 new intelligence layers, CCTV expansion to 11,000+ cameras across 6 countries, Mesh Terminal CLI, prediction markets, desktop-shell scaffold, and comprehensive mesh test suite (215 frontend + backend tests passing). Community contributors: @wa1id, @AlborzNazari, @adust09, @Xpirix, @imqdcr, @csysp, @suranyami, @chr0n1x, @johan-martensson, @singularfailure, @smithbh, @OrfeoTerkuci, @deuza, @tm-const, @Elhard1, @ttulttul
140 lines
5.4 KiB
Python
140 lines
5.4 KiB
Python
"""Ukraine air raid alerts via alerts.in.ua API.
|
|
|
|
Polls active alerts every 2 minutes, matches to oblast boundary polygons,
|
|
and produces GeoJSON-style records for map rendering.
|
|
|
|
Requires ALERTS_IN_UA_TOKEN env var (free registration at alerts.in.ua).
|
|
Gracefully skips if token is not set.
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from services.network_utils import fetch_with_curl
|
|
from services.fetchers._store import latest_data, _data_lock, _mark_fresh
|
|
from services.fetchers.retry import with_retry
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# ─── Alert type → color mapping ──────────────────────────────────────────────
|
|
ALERT_COLORS = {
|
|
"air_raid": "#ef4444", # red
|
|
"artillery_shelling": "#f97316", # orange
|
|
"urban_fights": "#eab308", # yellow
|
|
"chemical": "#a855f7", # purple
|
|
"nuclear": "#dc2626", # dark red
|
|
}
|
|
|
|
# ─── Load oblast boundary polygons (once) ────────────────────────────────────
|
|
_oblast_geojson = None
|
|
|
|
|
|
def _load_oblasts():
|
|
global _oblast_geojson
|
|
if _oblast_geojson is not None:
|
|
return _oblast_geojson
|
|
|
|
data_path = Path(__file__).resolve().parent.parent.parent / "data" / "ukraine_oblasts.geojson"
|
|
if not data_path.exists():
|
|
logger.error(f"Ukraine oblasts GeoJSON not found at {data_path}")
|
|
_oblast_geojson = {}
|
|
return _oblast_geojson
|
|
|
|
with open(data_path, "r", encoding="utf-8") as f:
|
|
_oblast_geojson = json.load(f)
|
|
|
|
logger.info(f"Loaded {len(_oblast_geojson.get('features', []))} Ukraine oblast boundaries")
|
|
return _oblast_geojson
|
|
|
|
|
|
def _find_oblast_geometry(location_title: str):
|
|
"""Find the polygon geometry for an oblast by matching Ukrainian name."""
|
|
oblasts = _load_oblasts()
|
|
features = oblasts.get("features", [])
|
|
for feat in features:
|
|
props = feat.get("properties", {})
|
|
name = props.get("name", "")
|
|
# Exact match on Ukrainian name (e.g. "Луганська область")
|
|
if name == location_title:
|
|
return feat.get("geometry"), props.get("name_en", "")
|
|
# Fuzzy: try partial match (alert may say "Київська область" but GeoJSON says "Київ")
|
|
for feat in features:
|
|
props = feat.get("properties", {})
|
|
name = props.get("name", "")
|
|
if location_title in name or name in location_title:
|
|
return feat.get("geometry"), props.get("name_en", "")
|
|
return None, ""
|
|
|
|
|
|
# ─── Fetcher ─────────────────────────────────────────────────────────────────
|
|
|
|
@with_retry(max_retries=1, base_delay=2)
|
|
def fetch_ukraine_air_raid_alerts():
|
|
"""Fetch active Ukraine air raid alerts from alerts.in.ua."""
|
|
from services.fetchers._store import is_any_active
|
|
|
|
if not is_any_active("ukraine_alerts"):
|
|
return
|
|
|
|
token = os.environ.get("ALERTS_IN_UA_TOKEN", "")
|
|
if not token:
|
|
logger.debug("ALERTS_IN_UA_TOKEN not set, skipping Ukraine air raid alerts")
|
|
return
|
|
|
|
alerts_out = []
|
|
try:
|
|
url = f"https://api.alerts.in.ua/v1/alerts/active.json?token={token}"
|
|
headers = {
|
|
"Authorization": f"Bearer {token}",
|
|
"Accept": "application/json",
|
|
}
|
|
response = fetch_with_curl(url, timeout=10, headers=headers)
|
|
|
|
if response.status_code == 200:
|
|
data = response.json()
|
|
raw_alerts = data.get("alerts", [])
|
|
|
|
for alert in raw_alerts:
|
|
loc_type = alert.get("location_type", "")
|
|
# Only render oblast-level alerts (not raion/city/hromada)
|
|
if loc_type != "oblast":
|
|
continue
|
|
|
|
location_title = alert.get("location_title", "")
|
|
alert_type = alert.get("alert_type", "air_raid")
|
|
geometry, name_en = _find_oblast_geometry(location_title)
|
|
|
|
if not geometry:
|
|
logger.debug(f"No geometry for oblast: {location_title}")
|
|
continue
|
|
|
|
alerts_out.append({
|
|
"id": alert.get("id", 0),
|
|
"alert_type": alert_type,
|
|
"location_title": location_title,
|
|
"location_uid": alert.get("location_uid", ""),
|
|
"name_en": name_en,
|
|
"started_at": alert.get("started_at", ""),
|
|
"color": ALERT_COLORS.get(alert_type, "#ef4444"),
|
|
"geometry": geometry,
|
|
})
|
|
|
|
logger.info(f"Ukraine alerts: {len(alerts_out)} active oblast-level alerts "
|
|
f"(from {len(raw_alerts)} total)")
|
|
elif response.status_code == 401:
|
|
logger.warning("alerts.in.ua returned 401 — check ALERTS_IN_UA_TOKEN")
|
|
elif response.status_code == 429:
|
|
logger.warning("alerts.in.ua rate-limited (429)")
|
|
else:
|
|
logger.warning(f"alerts.in.ua returned HTTP {response.status_code}")
|
|
|
|
except (ConnectionError, TimeoutError, OSError, ValueError, KeyError, TypeError) as e:
|
|
logger.error(f"Error fetching Ukraine alerts: {e}")
|
|
|
|
with _data_lock:
|
|
latest_data["ukraine_alerts"] = alerts_out
|
|
if alerts_out:
|
|
_mark_fresh("ukraine_alerts")
|