const express = require('express'); const WebSocket = require('ws'); const { createHash } = require('crypto'); const app = express(); const port = 8355; const geoJsonTemplate = { type: "FeatureCollection", features: [], "marker-symbol-images": { "star": "data:image/svg+xml;utf8,", "star-stroked": "data:image/svg+xml;utf8," } }; const hazeTranslator = { category: [ "Urbex", "Nature", "Spontaneous" ], difficulty: [ "Not Sure", "Baby", "Walk in the Park", "Challenge", "Madman", "Deathwish" ], rating: [ "Not Sure", "Check it Out", "Worth the Trip", "Must See" ], level: [ "Lost Soul", "Roamer", "Adventurer", "Pathfinder", "Trailblazer", "Maven" ], tag: [ "Vicious Dogs", "Biohazard", "Radioactive", "Fence", "Rats", "Still Water", "Barbed Wire", "Fire", "Flashlight Recommended", "Climbing Rope Recommended", "Boat", "Electrical Hazard", "Helmet Recommended", "Boots Recommended", "Falling Debris", "Dust Mask Recommended", "Asbestos", "Extreme Temperature", "Squatters", "Risk of Violence or Crime", "Emotional Distress", "Landmines", "Respirator Recommended", "Active Security", "No Trespassing Sign", "Private Property Sign", "Flood Risk", "Locked", "Dead Air", "Unstable Structure", "Black Mold", "Ticks", "Mosquitoes", "Razor Wire", "Fiberglass", "Bears", "Wolves", "Wildlife", "Broken Glass", "Scorpions", "Snakes", "Venomous Spiders", "Karens Present", "Gloves Recommended", "Boarded Up", "Thorns", "Dangerous Hights", "Hazmat Suit Recommended", "Heavy Overgrowth", "Bats" ] }; // Assemble full Haze location data, can take a LONG time app.all('/locations_full.geojson', (req, res) => { // Verify Inputs if (!req.headers['authorization']) { res.status(401).send('Unauthorized'); return; } // Setup standard authentication variables let hazePwHash = null; let hazeUser = null; let hazeSocketCounter = 0; let authHeader = req.headers['authorization']; let paramAuth = Buffer.from(authHeader.replace('Basic ', ''), 'base64').toString('utf-8'); hazeUser = paramAuth.split(":")[0]; hazePwHash = paramAuth.split(":")[1].replace(/\r?\n|\r/g, ''); if (!/\b[A-Fa-f0-9]{64}\b/.test(hazePwHash)) { hazePwHash = createHash('sha256').update(hazePwHash).digest('hex'); } const haze = new WebSocket('wss://do.ecven.com:8120/explr-mb'); let locations = []; let failCounter = 0; let queryComplete = false; console.log(`Starting request at ${new Date().toISOString()} for ${hazeUser}`); // Socket Message Handlers, defined here to use the local variables above const sendComplete = () => { if (!queryComplete) { queryComplete = true; haze.close(); let testTime = (new Date().getTime() - requestStartTime); const countExported = locations.filter(location => location && location.completed).length; console.log(`${(new Date().getTime() - requestStartTime)/1000}s | FAILS: ${failCounter} | TIMEOUT: ${testTime} | TOTAL LOCATIONS: ${locations.length} | EXPORTED: ${countExported} | TIMEDOUT: ${locations.length - countExported - failCounter}`); res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ ...geoJsonTemplate, features: locations.map(location => ({ type: "Feature", geometry: { type: "Point", coordinates: [location['longitude'], location['latitude']] }, properties: { id: location['id'], title: location['name'], "marker-symbol": location['isUnconfirmedLocation'] === 1 ? "star-stroked" : "star", author: location['author'] || '', explored: location['isExplored'] || false, confirmed: location['isConfirmed'] || false, category: location['category'] || '', level: location['level'] || '', rating: location['rating'] || '', difficulty: location['difficulty'] || '', comments: location['comments'] || [], querySuccess: location['completed'] } })) })); } } const queryLocation = (index) => { haze.send(locations[index]['requestQuery']); setTimeout(() => { if (locations[index]['completed'] === 0) { console.log(`Timeout on ${locations[index]['id']}}`); locations[index]['completed'] = -1; // mark as failed failCounter++; let nextIndex = locations.findIndex(location => location && location.completed === 0); if (locations.every(location => location && location.completed !== 0) || nextIndex === -1) { sendComplete(); } else { queryLocation(nextIndex); } } }, 10000); } // These are the socket event handlers, in order of expected use haze.addEventListener('message', (event) => { const data = JSON.parse(event.data); let haze_authToken = '', haze_username = ''; switch (data['event']) { case "connected": haze.send(JSON.stringify({ "event": "authenticateAccount_request", "body": [ { "password": hazePwHash, "email": hazeUser } ], "socketMessageId": hazeSocketCounter++ })); break; case "authenticateAccount_response": if (data['body']['response'] === 1) { haze_authToken = data['body']['authToken']; haze_username = data['body']['username']; // TODO: add platform report haze.send('{"event": "getMyLocationsRequest_request","body": [{"higlightImages": true}],"socketMessageId": ' + hazeSocketCounter++ + '}'); } else { console.dir(req.query); res.status(401).json({ ...data, password: req.auth.password }); haze.close(); res.end(); } break; case "getMyLocationsRequest_response": if (data['body']['response'] === 1) { data['body']['locations'].forEach(location => { hazeSocketCounter++; locations[hazeSocketCounter] = { id: location['id'], title: location['name'], latitude: location['coordinates']['latitude'], longitude: location['coordinates']['longitude'], isExplored: location['explored'] > 0, isConfirmed: location['isUnconfirmedLocation'] !== 1, category: hazeTranslator.category[location['locationCategory']], rawBasicData: JSON.stringify(location), requestQuery: JSON.stringify({ event: "getLocation_request", body: [ location['id'] ], socketMessageId: hazeSocketCounter }), completed: 0 } }); queryLocation(locations.findIndex(location => location && location.completed === 0)); } else { console.dir(data); res.status(500).send('Failed to retrieve locations'); haze.close(); res.end(); } break; case "getLocation_response": if (data['body']['response'] === 1) { let locationTags = {}; data['body']['location']['locationRating']['ratingItemsList'].map(tag => { locationTags[hazeTranslator.tag[tag['id']] ? hazeTranslator.tag[tag['id']] : tag['id']] = tag['count']; }); locations[data['socketMessageId']] = { ...locations[data['socketMessageId']], completed: 1, author: `${data['body']['location']['submittorUserDatas']['username']} (${data['body']['location']['submittorUserDatas']['email']})`, rating: hazeTranslator.rating[data['body']['location']['locationRating']['overallRating']], difficulty: hazeTranslator.difficulty[data['body']['location']['locationRating']['difficultyRating']], level: hazeTranslator.level[data['body']['location']['locationRating']['levelRating']], tags: locationTags, images: [ ... data['body']['location']['gallery'].map(image => ({ id: image['id'], mimetype: `image/${image['mediaType']}`, author: `${image['uploaderData']['username']} (${image['uploaderData']['email']})`, url: image['url'], thumbnail: image['thumbnailUrl'], likes: image['likes'], date: image['uploadDate'], })) ], comments: data['body']['location']['locationCommentsAndCommunityVotes'].map(comment => ({ id: comment['id'], text: comment['comment'], rating: (comment['upvotes']*1)+(comment['downvotes']* -1), author: comment['submittor']['username'], })) }; } else { console.log(`Failure on ${locations[data['socketMessageId']]['id']}: ${data['body']['responseDetails']}`); locations[data['socketMessageId']]['completed'] = -1; // mark as failed failCounter++; } // Completion Check and increment let nextIndex = locations.findIndex(location => location && location.completed === 0); if (locations.every(location => location && location.completed !== 0) || nextIndex === -1) { sendComplete(); } else { queryLocation(nextIndex); } break; case "ping": console.log('Ping received from Haze API'); break; default: console.log(JSON.stringify(data, null, 2)); res.status(500).send('Unexpected response from Haze API'); haze.close(); res.end(); } }); }); // Original API, for only listing locations with minimal data app.all('/locations.geojson', (req, res) => { // Verify Inputs if (!req.headers['authorization']) { res.status(401).send('Unauthorized'); return; } // Setup standard authentication variables let hazePwHash = null; let hazeUser = null; let authHeader = req.headers['authorization']; let paramAuth = Buffer.from(authHeader.replace('Basic ', ''), 'base64').toString('utf-8'); hazeUser = paramAuth.split(":")[0]; hazePwHash = paramAuth.split(":")[1].replace(/\r?\n|\r/g, ''); if (!/\b[A-Fa-f0-9]{64}\b/.test(hazePwHash)) { hazePwHash = createHash('sha256').update(hazePwHash).digest('hex'); } // At this point we need hazeUser as the email and hazePwHas as the sha256 hashed password // Query Haze API Websocket console.log(`Starting request at ${new Date().toISOString()} for ${hazeUser}`); const haze = new WebSocket('wss://do.ecven.com:8120/explr-mb'); haze.addEventListener('message', (event) => { const data = JSON.parse(event.data); let haze_authToken = '', haze_username = ''; switch (data['event']) { case "connected": haze.send(JSON.stringify({ "event": "authenticateAccount_request", "body": [ { "password": hazePwHash, "email": hazeUser } ], "socketMessageId": 0 })); break; case "authenticateAccount_response": if (data['body']['response'] === 1) { haze_authToken = data['body']['authToken']; haze_username = data['body']['username']; // TODO: add platform report haze.send('{"event": "getMyLocationsRequest_request","body": [{"higlightImages": true}],"socketMessageId": 0}'); } else { console.dir(req.query); res.status(401).json({ ...data }); haze.close(); res.end(); } break; case "getMyLocationsRequest_response": if (data['body']['response'] === 1) { const hazeLocations = data['body']['locations'].map(location => { return { type: "Feature", geometry: { type: "Point", coordinates: [location['coordinates']['longitude'], location['coordinates']['latitude']] }, properties: { id: location['id'], title: location['name'], "marker-symbol": location['isUnconfirmedLocation'] === 1 ? "star-stroked" : "star", } } }); res.setHeader('Content-Type', 'application/json'); haze.close(); res.end(JSON.stringify({ ...geoJsonTemplate, features: hazeLocations })); } else { console.dir(data); res.status(500).send('Failed to retrieve locations'); haze.close(); res.end(); } break; default: console.dir(data); res.status(500).send('Unexpected response from Haze API'); haze.close(); res.end(); } }); }); app.listen(port, () => { console.log(`Server is running on http://localhost:${port}`); });