const express = require('express'); const WebSocket = require('ws'); const auth = require('express-basic-auth'); 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" ] }; app.use(auth({ authorizer: (username, password) => { return true; } })); // Assemble full Haze location data, can take a LONG time app.all('/locations_full.geojson', (req, res) => { // Verify Inputs if ((!req.auth.user || !req.auth.password) && (!req.query.auth)) { res.status(401).send('Unauthorized'); return; } // Setup local variables for request let hazePwHash = null; let hazeUser = null; let hazeSocketCounter = 0; let requestStartTime = new Date().getTime(); if (req.query.auth) { let paramAuth = Buffer.from(req.query.auth, 'base64').toString('utf-8'); hazeUser = paramAuth.split(":")[0]; hazePwHash = paramAuth.split(":")[1]; } else { hazeUser = req.auth.user; if (/\b[A-Fa-f0-9]{64}\b/.test(req.auth.password)) { hazePwHash = req.auth.password; } else { hazePwHash = createHash('sha256').update(req.auth.password).digest('hex'); } } const haze = new WebSocket('wss://do.ecven.com:8120/explr-mb'); let locations = []; let failCounter = 0; let queryComplete = false; // 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']; 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.auth.user || !req.auth.password) && (!req.query.auth)) { res.status(401).send('Unauthorized'); return; } // Query Haze API Websocket let hazePwHash = null; let hazeUser = null; if (req.query.auth) { let paramAuth = Buffer.from(req.query.auth, 'base64').toString('utf-8'); hawUser = paramAuth.split(":")[0]; hazePwHash = paramAuth.split(":")[1]; } else { hazeUser = req.auth.user; if (/\b[A-Fa-f0-9]{64}\b/.test(req.auth.password)) { hazePwHash = req.auth.password; } else { hazePwHash = createHash('sha256').update(req.auth.password).digest('hex'); } } const haze = new WebSocket('wss://do.ecven.com:8120/explr'); 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']; haze.send('{"event": "getMyLocationsRequest_request","body": [{"higlightImages": true}],"socketMessageId": 0}'); } 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) { 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({ ...geonJsonTemplate, 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}`); });