302 lines
9.9 KiB
JavaScript
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(`====================================================`);
|
|
});
|