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}`);
});