MotorSignal-App/server/waze_relay/server.js

302 lines
9.9 KiB
JavaScript

const http = require('http');
const https = require('https');
const express = require('express');
const cors = require('cors');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(cors());
app.use(express.json());
// In-memory cache for Waze requests (30s TTL per geo tile)
const cache = new Map();
const CACHE_TTL_MS = 30 * 1000;
// User agents and headers mimicking genuine browser requests
const BROWSER_USER_AGENTS = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
];
function getRandomUserAgent() {
return BROWSER_USER_AGENTS[Math.floor(Math.random() * BROWSER_USER_AGENTS.length)];
}
/**
* Determine Waze server environment based on longitude
* na: North America (Western Hemisphere approx)
* il: Israel
* row: Rest of World
*/
function getWazeEnv(lat, lng) {
if (lat >= 29.0 && lat <= 33.5 && lng >= 34.0 && lng <= 36.0) {
return 'il';
}
if (lng >= -170.0 && lng <= -50.0) {
return 'na';
}
return 'row';
}
/**
* Fetch raw GeoRSS alerts from Waze with browser impersonation
*/
function fetchWazeAlerts(top, bottom, left, right, env = 'na') {
return new Promise((resolve, reject) => {
const query = new URLSearchParams({
top: top.toString(),
bottom: bottom.toString(),
left: left.toString(),
right: right.toString(),
env: env,
types: 'alerts,traffic,jams'
});
const url = `https://www.waze.com/live-map/api/georss?${query.toString()}`;
const parsed = new URL(url);
const req = https.request({
hostname: parsed.hostname,
path: parsed.pathname + parsed.search,
method: 'GET',
headers: {
'User-Agent': getRandomUserAgent(),
'Accept': 'application/json, text/plain, */*',
'Accept-Language': 'en-US,en;q=0.9',
'Referer': 'https://www.waze.com/live-map/',
'Sec-Ch-Ua': '"Chromium";v="124", "Google Chrome";v="124"',
'Sec-Ch-Ua-Mobile': '?0',
'Sec-Ch-Ua-Platform': '"Windows"',
'Sec-Fetch-Dest': 'empty',
'Sec-Fetch-Mode': 'cors',
'Sec-Fetch-Site': 'same-origin',
'Connection': 'keep-alive'
},
timeout: 8000
}, (res) => {
let body = '';
res.on('data', chunk => body += chunk);
res.on('end', () => {
if (res.statusCode >= 200 && res.statusCode < 300) {
try {
const data = JSON.parse(body);
resolve(data);
} catch (e) {
reject(new Error(`Failed to parse Waze JSON: ${e.message}`));
}
} else {
reject(new Error(`Waze responded with HTTP ${res.statusCode}: ${body.slice(0, 150)}`));
}
});
});
req.on('timeout', () => {
req.destroy();
reject(new Error('Waze request timed out'));
});
req.on('error', (err) => {
reject(err);
});
req.end();
});
}
/**
* Normalize and format Waze raw alerts into MotorSignal Community Schema
*/
function normalizeAlerts(wazeData) {
const rawAlerts = wazeData.alerts || [];
const normalized = [];
for (const item of rawAlerts) {
const lat = item.location?.y ?? item.latitude ?? item.lat;
const lng = item.location?.x ?? item.longitude ?? item.lng;
if (lat == null || lng == null) continue;
const type = (item.type || 'HAZARD').toUpperCase();
const subtype = (item.subtype || item.subType || '').toUpperCase();
const street = item.street || item.road || '';
const city = item.city || '';
let category = 'other';
let description = item.reportDescription || '';
if (type.includes('POLICE') || subtype.includes('POLICE')) {
category = 'police';
if (subtype.includes('HIDDEN')) {
description = description || 'Hidden police speed trap reported';
} else if (subtype.includes('VISIBLE')) {
description = description || 'Police speed trap reported';
} else {
description = description || 'Police presence reported';
}
} else if (type.includes('SPEED_CAMERA') || subtype.includes('SPEED_CAMERA') || type.includes('CAMERA')) {
category = 'speed_camera';
description = description || 'Speed enforcement camera';
} else if (type.includes('RED_LIGHT_CAMERA') || subtype.includes('RED_LIGHT_CAMERA')) {
category = 'speed_camera';
description = description || 'Red light camera';
} else if (type.includes('ACCIDENT') || subtype.includes('ACCIDENT')) {
category = 'accident';
description = description || (subtype.includes('MAJOR') ? 'Major collision' : 'Accident reported');
} else if (type.includes('HAZARD') || subtype.includes('HAZARD') || subtype.includes('WEATHER') || subtype.includes('DEBRIS')) {
category = 'hazard';
description = description || (subtype.includes('POT_HOLE') ? 'Pothole on roadway' : 'Road hazard / debris');
} else if (type.includes('ROAD_CLOSED') || subtype.includes('ROAD_CLOSED')) {
category = 'road_closure';
description = description || 'Road closed';
} else if (type.includes('CONSTRUCTION') || subtype.includes('CONSTRUCTION')) {
category = 'construction';
description = description || 'Road work / Construction';
} else if (type.includes('JAM') || type.includes('TRAFFIC')) {
category = 'traffic';
description = description || 'Heavy traffic congestion';
}
if (street) {
description += ` on ${street}`;
}
normalized.push({
id: item.uuid || item.id || `wz_${lat.toFixed(5)}_${lng.toFixed(5)}`,
providerId: 'community_traffic',
source: 'Waze Community Relay',
type: type,
subtype: subtype,
category: category,
latitude: parseFloat(lat),
longitude: parseFloat(lng),
heading: item.magvar || item.heading || null,
roadName: street,
city: city,
description: description,
confidence: (item.confidence != null ? item.confidence / 10.0 : (item.reportRating != null ? item.reportRating / 5.0 : 0.85)),
severity: item.reportRating >= 4 ? 'high' : (item.reportRating >= 2 ? 'medium' : 'low'),
reliability: item.reliability || 5,
reportRating: item.reportRating || 3,
nThumbsUp: item.nThumbsUp || 0,
pubMillis: item.pubMillis || Date.now(),
reportedAt: new Date(item.pubMillis || Date.now()).toISOString()
});
}
return normalized;
}
// -------------------------------------------------------------
// REST API ROUTES
// -------------------------------------------------------------
/**
* Health check & discovery endpoint (Supports MotorSignal TEST CONNECTION button)
*/
app.get(['/', '/health', '/test'], (req, res) => {
res.json({
status: 'ok',
service: 'MotorSignal Waze Community Relay',
version: '1.0.0',
timestamp: new Date().toISOString(),
uptimeSeconds: Math.floor(process.uptime()),
features: ['police', 'speed_cameras', 'hazards', 'accidents', 'traffic']
});
});
/**
* Main alerts endpoint
*/
app.get(['/alerts', '/api/alerts'], async (req, res) => {
try {
let top, bottom, left, right, lat, lng;
if (req.query.top && req.query.bottom && req.query.left && req.query.right) {
top = parseFloat(req.query.top);
bottom = parseFloat(req.query.bottom);
left = parseFloat(req.query.left);
right = parseFloat(req.query.right);
lat = (top + bottom) / 2;
lng = (left + right) / 2;
} else if (req.query.lat && req.query.lng) {
lat = parseFloat(req.query.lat);
lng = parseFloat(req.query.lng);
const radiusMeters = parseFloat(req.query.radius || 5000);
const latDelta = radiusMeters / 111000.0;
const lngDelta = radiusMeters / (111000.0 * Math.cos(lat * Math.PI / 180.0));
top = lat + latDelta;
bottom = lat - latDelta;
left = lng - lngDelta;
right = lng + lngDelta;
} else {
lat = 37.7749;
lng = -122.4194;
top = 37.82;
bottom = 37.72;
left = -122.48;
right = -122.36;
}
const env = req.query.env || getWazeEnv(lat, lng);
const cacheKey = `${lat.toFixed(2)}_${lng.toFixed(2)}_${env}`;
const cached = cache.get(cacheKey);
if (cached && (Date.now() - cached.time < CACHE_TTL_MS)) {
return res.json({
provider: 'waze_relay',
cached: true,
count: cached.alerts.length,
alerts: cached.alerts
});
}
try {
const wazeRaw = await fetchWazeAlerts(top, bottom, left, right, env);
const normalizedAlerts = normalizeAlerts(wazeRaw);
cache.set(cacheKey, { time: Date.now(), alerts: normalizedAlerts });
return res.json({
provider: 'waze_relay',
cached: false,
count: normalizedAlerts.length,
alerts: normalizedAlerts
});
} catch (fetchErr) {
console.warn(`[WazeRelay] Warning: ${fetchErr.message}`);
if (cached) {
return res.json({
provider: 'waze_relay',
cached: true,
stale: true,
count: cached.alerts.length,
alerts: cached.alerts
});
}
return res.json({
provider: 'waze_relay',
status: 'partial',
message: fetchErr.message,
count: 0,
alerts: []
});
}
} catch (err) {
console.error('[WazeRelay] Server Error:', err);
res.status(500).json({ error: err.message, alerts: [] });
}
});
app.listen(PORT, '0.0.0.0', () => {
console.log(`====================================================`);
console.log(` 🏍️ MotorSignal Waze Alert Relay Server running`);
console.log(` 📡 Listening on: http://0.0.0.0:${PORT}`);
console.log(` 🏥 Healthcheck: http://localhost:${PORT}/health`);
console.log(` 🚨 Alerts API: http://localhost:${PORT}/alerts?lat=37.77&lng=-122.42`);
console.log(`====================================================`);
});