From 2dd4d9c7f5d9934a3aa3a6763600a166d0098e51 Mon Sep 17 00:00:00 2001 From: Manuel Kasper Date: Fri, 14 Aug 2020 10:25:45 +0200 Subject: [PATCH] Initial import --- README.md | 3 +- activations.js | 78 ++ alerts.js | 82 ++ config.js | 81 ++ db.js | 28 + geoexport.js | 289 +++++ keyzipper.js | 66 ++ package-lock.json | 2177 ++++++++++++++++++++++++++++++++++++ package.json | 39 + photos.js | 108 ++ photos_router.js | 216 ++++ rbn.js | 158 +++ server.js | 266 +++++ sotaspots.js | 135 +++ spots.js | 74 ++ summits.js | 68 ++ tools/importActivators.js | 61 + tools/importPhoto.js | 33 + tools/isocodes.txt | 97 ++ tools/regenThumbs.js | 32 + tools/updateSotaSummits.js | 194 ++++ tools/updateSotatrails.js | 41 + tracks.js | 73 ++ tracks_router.js | 42 + utils.js | 17 + ws-manager.js | 82 ++ 26 files changed, 4539 insertions(+), 1 deletion(-) create mode 100644 activations.js create mode 100644 alerts.js create mode 100644 config.js create mode 100644 db.js create mode 100644 geoexport.js create mode 100644 keyzipper.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 photos.js create mode 100644 photos_router.js create mode 100644 rbn.js create mode 100644 server.js create mode 100644 sotaspots.js create mode 100644 spots.js create mode 100644 summits.js create mode 100644 tools/importActivators.js create mode 100644 tools/importPhoto.js create mode 100644 tools/isocodes.txt create mode 100644 tools/regenThumbs.js create mode 100644 tools/updateSotaSummits.js create mode 100644 tools/updateSotatrails.js create mode 100644 tracks.js create mode 100644 tracks_router.js create mode 100644 utils.js create mode 100644 ws-manager.js diff --git a/README.md b/README.md index e8168f4..5353e90 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ # sotlas-api -SOTLAS backend + +Backend server for SOTLAS (https://sotl.as), serving summit and activator data to the frontend, handling photo uploads and distributing SOTAwatch spots asynchronously via WebSocket. diff --git a/activations.js b/activations.js new file mode 100644 index 0000000..43218eb --- /dev/null +++ b/activations.js @@ -0,0 +1,78 @@ +const axios = require('axios'); +const moment = require('moment'); +const express = require('express'); +const config = require('./config'); +const db = require('./db'); +const summits = require('./summits'); + +let router = express.Router(); +module.exports = router; + +router.get('/:callsign', (req, res) => { + res.cacheControl = { + noCache: true + }; + + db.getDb().collection('activators').findOne({callsign: req.params.callsign}, (err, activator) => { + if (err) { + res.status(500).end(); + return; + } + if (!activator) { + res.status(404).end(); + return; + } + + axios.get('https://api-db.sota.org.uk/admin/activator_log_by_id', { params: { id: activator.userId, year: 'all' } }) + .then(response => { + let activations = response.data.map(activation => { + return { + id: activation[0], + date: moment.utc(activation.ActivationDate).toDate(), + callsignUsed: activation.OwnCallsign, + qsos: activation.QSOs, + modeQsos: extractModeQsos(activation), + bandQsos: extractBandQsos(activation), + points: activation.Points, + bonus: activation.BonusPoints, + summit: { + code: activation.Summit.substring(0, activation.Summit.indexOf(' ')) + } + } + }); + summits.lookupSummits(activations) + .then(activationsWithSummits => { + res.json(activationsWithSummits); + }) + }) + .catch(error => { + console.error(error); + if (error.response && error.response.status === 401) { + res.status(401); + } else { + res.status(500); + } + res.end(); + return; + }) + }); +}); + +function extractModeQsos(activation) { + return { + 'cw': activation.QSOcw, + 'ssb': activation.QSOssb, + 'fm': activation.QSOfm + } +} + +function extractBandQsos(activation) { + let bands = ['160','80','60','40','30','20','17','15','12','10','6','4','2','70c','23c']; + let bandQsos = {}; + bands.forEach(band => { + if (activation['QSO' + band] > 0) { + bandQsos[band + 'm'] = activation['QSO' + band]; + } + }); + return bandQsos; +} diff --git a/alerts.js b/alerts.js new file mode 100644 index 0000000..6732038 --- /dev/null +++ b/alerts.js @@ -0,0 +1,82 @@ +const axios = require('axios'); +const express = require('express'); +const config = require('./config'); +const summits = require('./summits'); + +let router = express.Router(); +module.exports = router; + +let alertCache = []; +let lastLoadDate; +let pendingLoad; + +router.get('/', (req, res) => { + res.cacheControl = { + noCache: true + }; + + loadAlerts(req.query.noCache) + .then(alerts => { + res.json(alerts); + }) + .catch(err => { + console.error(err); + res.status(500).end(); + }) +}); + +function loadAlerts(noCache) { + if (noCache) { + console.log('load alerts (no cache)'); + return loadAlertsDirect(); + } + + if (lastLoadDate && (new Date() - lastLoadDate) < config.alerts.minUpdateInterval) { + return Promise.resolve(alertCache); + } + + if (!pendingLoad) { + console.log('load alerts (cache)'); + pendingLoad = loadAlertsDirect() + .then(response => { + pendingLoad = null; + return response; + }) + .catch(err => { + pendingLoad = null; + return Promise.reject(err); + }) + } + + return pendingLoad; +} + +function loadAlertsDirect() { + return axios.get(config.alerts.url) + .then(response => { + if (response.status !== 200) { + console.error(`Got status ${response.status} when loading alerts`); + return Promise.reject('Cannot load alerts from SOTAwatch'); + } + + let newAlerts = response.data.map(alert => { + return { + id: alert.id, + timeStamp: new Date(alert.timeStamp), + dateActivated: new Date(alert.dateActivated), + summit: {code: alert.associationCode + '/' + alert.summitCode}, + activatorCallsign: alert.activatingCallsign.toUpperCase().replace(/[^A-Z0-9\/-]/g, ''), + posterCallsign: alert.posterCallsign, + frequency: alert.frequency, + comments: alert.comments !== '(null)' ? alert.comments : '' + }; + }); + + return summits.lookupSummits(newAlerts) + .then(alerts => { + alertCache = alerts; + lastLoadDate = new Date(); + return alerts; + }); + }) +} diff --git a/config.js b/config.js new file mode 100644 index 0000000..c2eb3d4 --- /dev/null +++ b/config.js @@ -0,0 +1,81 @@ +var config = {}; +module.exports = config; + +config.http = { + host: '127.0.0.1', + port: 8081 +}; + +config.mongodb = { + url: 'mongodb://sotlas:XXXXXXXX@localhost:27017/sotlas', + dbName: 'sotlas', + batchSize: 1000 +}; + +config.sotaspots = { + initialLoadSpots: -24, + periodicLoadSpots: 100, + maxSpotAge: 86400000, + updateInterval: 30000, + url: 'https://api2.sota.org.uk/api/spots' +}; + +config.alerts = { + minUpdateInterval: 60000, + url: 'https://api2.sota.org.uk/api/alerts/12' +}; + +config.rbn = { + server: { + host: 'telnet.reversebeacon.net', + port: 7000 + }, + login: "HB9DQM-3", + timeout: 180000, + maxSpotHistory: 1000 +}; + +config.geoip = { + path: 'GeoLite2-City.mmdb' +}; + +config.summitListUrl = 'https://www.sotadata.org.uk/summitslist.csv'; + +config.sotatrailsUrl = 'https://sotatrails.ch/api.php'; + +config.photos = { + paths: { + original: '/data/images/photos/original', + thumb: '/data/images/photos/thumb', + large: '/data/images/photos/large' + }, + sizes: { + large: { + width: 1600, + height: 1600 + }, + thumb: { + width: 512, + height: 256 + } + }, + uploadPath: '/data/upload/photos' +}; + +config.tracks = { + paths: { + original: '/data/tracks/original', + simple: '/data/tracks/simple' + }, + tolerance: 0.00001, + uploadPath: '/data/upload/tracks' +}; + +config.sso = { + jwksUri: 'https://sso.sota.org.uk/auth/realms/SOTA/protocol/openid-connect/certs' +}; + +config.mail = { + host: "neon1.net", + port: 587 +}; diff --git a/db.js b/db.js new file mode 100644 index 0000000..544086a --- /dev/null +++ b/db.js @@ -0,0 +1,28 @@ +const MongoClient = require('mongodb').MongoClient; +const config = require('./config'); +const assert = require('assert'); + +let db = null +let client +const connectPromise = new Promise((resolve, reject) => { + client = new MongoClient(config.mongodb.url, {useUnifiedTopology: true}) + client.connect(function (err) { + assert.equal(null, err) + db = client.db(config.mongodb.dbName) + resolve() + }); +}); + +exports.getDb = function() { + return db +} + +exports.waitDb = function(callback) { + connectPromise.then(callback) +} + +exports.closeDb = function() { + if (client) { + client.close() + } +} diff --git a/geoexport.js b/geoexport.js new file mode 100644 index 0000000..862628f --- /dev/null +++ b/geoexport.js @@ -0,0 +1,289 @@ +const axios = require('axios'); +const moment = require('moment'); +const express = require('express'); +const config = require('./config'); +const db = require('./db'); + +let router = express.Router(); +module.exports = router; + +router.get('/associations/:association.gpx', (req, res) => { + res.cacheControl = { + noCache: true + }; + + res.set('Content-Type', 'application/gpx+xml'); + res.set('Content-Disposition', 'attachment; filename="' + req.params.association + '.gpx"'); + gpxForQuery('^' + req.params.association + '/', `SOTA Association ${req.params.association}`, req.query, (err, gpx) => { + if (err) { + console.error(err); + res.status(500).end(); + return; + } + + res.send(gpx).end(); + }); +}); + +router.get('/associations/:association.kml', (req, res) => { + res.cacheControl = { + noCache: true + }; + + res.set('Content-Type', 'application/vnd.google-earth.kml+xml'); + res.set('Content-Disposition', 'attachment; filename="' + req.params.association + '.kml"'); + kmlForAssociation(req.params.association, req.query, (err, kml) => { + if (err) { + console.error(err); + res.status(500).end(); + return; + } + + if (!kml) { + res.status(404).end(); + return; + } + + res.send(kml).end(); + }); +}); + +router.get('/regions/:association/:region.gpx', (req, res) => { + res.cacheControl = { + noCache: true + }; + + res.set('Content-Type', 'application/gpx+xml'); + res.set('Content-Disposition', 'attachment; filename="' + req.params.association + '_' + req.params.region + '.gpx"'); + gpxForQuery('^' + req.params.association + '/' + req.params.region + '-', `SOTA Region ${req.params.association + '/' + req.params.region}`, req.query, (err, gpx) => { + if (err) { + console.error(err); + res.status(500).end(); + return; + } + + res.send(gpx).end(); + }); +}); + +router.get('/regions/:association/:region.kml', (req, res) => { + res.cacheControl = { + noCache: true + }; + + res.set('Content-Type', 'application/vnd.google-earth.kml+xml'); + res.set('Content-Disposition', 'attachment; filename="' + req.params.association + '_' + req.params.region + '.kml"'); + kmlForRegion(req.params.association, req.params.region, req.query, (err, kml) => { + if (err) { + console.error(err); + res.status(500).end(); + return; + } + + if (!kml) { + res.status(404).end(); + return; + } + + res.send(kml).end(); + }); +}); + + +function gpxForQuery(query, name, options, callback) { + let filter = {code: {$regex: query}}; + if (!options.inactive) { + filter.validFrom = {$lte: new Date()}; + filter.validTo = {$gte: new Date()}; + } + db.getDb().collection('summits').find(filter).sort({code: 1}).toArray((err, summits) => { + if (err) { + callback(err); + return; + } + + let minlat, minlon, maxlat, maxlon; + summits.forEach(summit => { + if (!minlat || summit.coordinates.latitude < minlat) { + minlat = summit.coordinates.latitude; + } + if (!minlon || summit.coordinates.longitude < minlon) { + minlon = summit.coordinates.longitude; + } + if (!maxlat || summit.coordinates.latitude > maxlat) { + maxlat = summit.coordinates.latitude; + } + if (!maxlon || summit.coordinates.longitude > maxlon) { + maxlon = summit.coordinates.longitude; + } + }); + + let now = moment.utc(); + let gpx = ` + + + ${name} + + SOTLAS + + + SOTLAS + + + + +`; + + summits.forEach(summit => { + gpx += ` + ${summit.altitude} + + + SOTA${('0' + summit.points).substr(-2)} + Summit + +`; + }); + + gpx += ""; + callback(null, gpx); + }); +} + +function kmlForAssociation(associationCode, options, callback) { + db.getDb().collection('associations').findOne({code: associationCode}, (err, association) => { + if (err) { + callback(err); + return; + } + + if (!association) { + callback(null, null); + return; + } + + let filter = {code: {$regex: "^" + association.code + "/"}}; + if (!options.inactive) { + filter.validFrom = {$lte: new Date()}; + filter.validTo = {$gte: new Date()}; + } + db.getDb().collection('summits').find(filter).sort({code: 1}).toArray((err, summits) => { + let now = moment.utc(); + let kmlName = 'SOTA Association ' + association.code + ' - ' + association.name; + let kml = ` + + + + + + + ${now.toISOString()} + +`; + + association.regions.forEach(region => { + kml += ` + + +`; + + summits.filter(summit => {return summit.code.startsWith(association.code + '/' + region.code)}).forEach(summit => { + kml += kmlForSummit(summit, options); + }); + + kml += ` +`; + }); + + kml += ` + +`; + + callback(null, kml); + }); + }); +} + +function kmlForRegion(associationCode, regionCode, options, callback) { + db.getDb().collection('associations').findOne({code: associationCode}, (err, association) => { + if (err) { + callback(err); + return; + } + + if (!association) { + callback(null, null); + return; + } + + let filter = {code: {$regex: "^" + association.code + "/" + regionCode + '-'}}; + if (!options.inactive) { + filter.validFrom = {$lte: new Date()}; + filter.validTo = {$gte: new Date()}; + } + db.getDb().collection('summits').find(filter).sort({code: 1}).toArray((err, summits) => { + let now = moment.utc(); + let kmlName = 'SOTA Region ' + associationCode + '/' + regionCode; + let kml = ` + + + + + ${now.toISOString()} + +`; + + association.regions.forEach(region => { + if (regionCode && region.code !== regionCode) { + return; + } + kml += ` SOTA Region + +`; + + summits.filter(summit => {return summit.code.startsWith(association.code + '/' + region.code)}).forEach(summit => { + kml += kmlForSummit(summit, options); + }); + }); + + kml += ` + +`; + + callback(null, kml); + }); + }); +} + +function summitName(summit, options) { + let name = summit.code; + let nameopts = []; + if (options.nameopts) { + nameopts = options.nameopts.split(',') + } + if (nameopts.includes('name')) { + name += ' - ' + summit.name; + } + if (nameopts.includes('altitude')) { + name += ', ' + summit.altitude + 'm'; + } + if (nameopts.includes('points')) { + name += ', ' + summit.points + 'pt'; + } + return name; +} + +function kmlForSummit(summit, options) { + return ` + + + + ${summit.coordinates.longitude},${summit.coordinates.latitude},${summit.altitude} + + +`; +} diff --git a/keyzipper.js b/keyzipper.js new file mode 100644 index 0000000..3cedd47 --- /dev/null +++ b/keyzipper.js @@ -0,0 +1,66 @@ +const KEY_DECOMPRESSION_MAP = { + a: 'altitude', + ac: 'activatorCallsign', + ao: 'activationCount', + c: 'comments', + d: 'code', + e: 'speed', + f: 'frequency', + hc: 'homeCallsign', + i: 'isActivator', + ic: 'isoCode', + l: 'callsign', + m: 'mode', + n: 'name', + o: 'continent', + p: 'points', + s: 'summit', + t: 'spotter', + ts: 'timeStamp' +} + +let KEY_COMPRESSION_MAP = null + +function compressKeys (obj) { + // Lazy init + if (KEY_COMPRESSION_MAP === null) { + KEY_COMPRESSION_MAP = {} + Object.keys(KEY_DECOMPRESSION_MAP).forEach(key => { + KEY_COMPRESSION_MAP[KEY_DECOMPRESSION_MAP[key]] = key + }) + } + + return mapKeys(obj, KEY_COMPRESSION_MAP) +} + +function decompressKeys (obj) { + return mapKeys(obj, KEY_DECOMPRESSION_MAP) +} + +function mapKeys (obj, map) { + if (obj === null) { + return null + } else if (Array.isArray(obj)) { + return obj.map(el => { + return mapKeys(el, map) + }) + } else if (typeof obj === 'object' && !(obj instanceof Date)) { + let ret = {} + Object.keys(obj).forEach(key => { + let val = mapKeys(obj[key], map) + + if (map[key]) { + ret[map[key]] = val + } else { + ret[key] = val + } + }) + return ret + } else { + return obj + } +} + +module.exports = { + compressKeys, decompressKeys +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c5219ac --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2177 @@ +{ + "name": "sotlas-api", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@turf/clean-coords": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/clean-coords/-/clean-coords-5.1.5.tgz", + "integrity": "sha1-EoAKmKeMmkUqcuxChJPEOs8q2h8=", + "requires": { + "@turf/helpers": "^5.1.5", + "@turf/invariant": "^5.1.5" + } + }, + "@turf/clone": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/clone/-/clone-5.1.5.tgz", + "integrity": "sha1-JT6NNUdxgZduM636tQoPAqfw42c=", + "requires": { + "@turf/helpers": "^5.1.5" + } + }, + "@turf/helpers": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-5.1.5.tgz", + "integrity": "sha1-FTQFInq5M9AEpbuWQantmZ/L4M8=" + }, + "@turf/invariant": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-5.2.0.tgz", + "integrity": "sha1-8BUP9ykLOFd7c9CIt5MsHuCqkKc=", + "requires": { + "@turf/helpers": "^5.1.5" + } + }, + "@turf/meta": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-5.2.0.tgz", + "integrity": "sha1-OxrUhe4MOwsXdRMqMsOE1T5LpT0=", + "requires": { + "@turf/helpers": "^5.1.5" + } + }, + "@turf/simplify": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@turf/simplify/-/simplify-5.1.5.tgz", + "integrity": "sha1-Csjyei60IYGD7dmZjDJ1q+QIuSY=", + "requires": { + "@turf/clean-coords": "^5.1.5", + "@turf/clone": "^5.1.5", + "@turf/helpers": "^5.1.5", + "@turf/meta": "^5.1.5" + } + }, + "@types/body-parser": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.1.tgz", + "integrity": "sha512-RoX2EZjMiFMjZh9lmYrwgoP9RTpAjSHiJxdp4oidAQVO02T7HER3xj9UKue5534ULWeqVEkujhWcyvUce+d68w==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.32", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", + "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==", + "requires": { + "@types/node": "*" + } + }, + "@types/express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.1.tgz", + "integrity": "sha512-VfH/XCP0QbQk5B5puLqTLEeFgR8lfCJHZJKkInZ9mkYd+u8byX0kztXEQxEk4wZXJs8HI+7km2ALXjn4YKcX9w==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "*", + "@types/serve-static": "*" + } + }, + "@types/express-jwt": { + "version": "0.0.42", + "resolved": "https://registry.npmjs.org/@types/express-jwt/-/express-jwt-0.0.42.tgz", + "integrity": "sha512-WszgUddvM1t5dPpJ3LhWNH8kfNN8GPIBrAGxgIYXVCEGx6Bx4A036aAuf/r5WH9DIEdlmp7gHOYvSM6U87B0ag==", + "requires": { + "@types/express": "*", + "@types/express-unless": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.16.9", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.9.tgz", + "integrity": "sha512-GqpaVWR0DM8FnRUJYKlWgyARoBUAVfRIeVDZQKOttLFp5SmhhF9YFIYeTPwMd/AXfxlP7xVO2dj1fGu0Q+krKQ==", + "requires": { + "@types/node": "*", + "@types/range-parser": "*" + } + }, + "@types/express-unless": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@types/express-unless/-/express-unless-0.5.1.tgz", + "integrity": "sha512-5fuvg7C69lemNgl0+v+CUxDYWVPSfXHhJPst4yTLcqi4zKJpORCxnDrnnilk3k0DTq/WrAUdvXFs01+vUqUZHw==", + "requires": { + "@types/express": "*" + } + }, + "@types/mime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", + "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==" + }, + "@types/node": { + "version": "12.7.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.12.tgz", + "integrity": "sha512-KPYGmfD0/b1eXurQ59fXD1GBzhSQfz6/lKBxkaHX9dKTzjXbK68Zt7yGUxUsCS1jeTy/8aL+d9JEr+S54mpkWQ==" + }, + "@types/range-parser": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz", + "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==" + }, + "@types/serve-static": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.3.tgz", + "integrity": "sha512-oprSwp094zOglVrXdlo/4bAHtKTAxX6VT8FOZlBKrmyLbNvE1zxZyJ6yikMVtHIvwP45+ZQGJn+FdXGKTozq0g==", + "requires": { + "@types/express-serve-static-core": "*", + "@types/mime": "*" + } + }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, + "ajv": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "async-limiter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", + "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "axios": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz", + "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==", + "requires": { + "follow-redirects": "1.5.10", + "is-buffer": "^2.0.2" + } + }, + "backoff": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz", + "integrity": "sha1-9hbtqdPktmuMp/ynn2lXIsX44m8=", + "requires": { + "precond": "0.2" + } + }, + "base64-js": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.2.tgz", + "integrity": "sha1-Ak8Pcq+iW3X5wO5zzU9V7Bvtl4Q=" + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "bl": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.2.tgz", + "integrity": "sha512-j4OH8f6Qg2bGuWfRiltT2HYGx0e1QcBTrK9KAHNMwMZdQnDZFk0ZSYIpADjYCB3U12nicC5tVJwSIhwOWjb4RQ==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + } + } + }, + "body-parser": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", + "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", + "requires": { + "bytes": "3.1.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "on-finished": "~2.3.0", + "qs": "6.7.0", + "raw-body": "2.4.0", + "type-is": "~1.6.17" + }, + "dependencies": { + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + } + } + }, + "bops": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/bops/-/bops-0.0.6.tgz", + "integrity": "sha1-CC0dVfoB5g29wuvC26N/ZZVUzzo=", + "requires": { + "base64-js": "0.0.2", + "to-utf8": "0.0.1" + } + }, + "bson": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.4.tgz", + "integrity": "sha512-S/yKGU1syOMzO86+dGpg2qGoDL0zvzcb262G+gqEy6TgP6rt6z6qxSFX/8X6vLC91P7G7C3nLs0+bvDzmvBA3Q==" + }, + "buffer": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.5.0.tgz", + "integrity": "sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + }, + "dependencies": { + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + } + } + }, + "buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "requires": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "bytes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", + "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" + }, + "carrier": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/carrier/-/carrier-0.3.0.tgz", + "integrity": "sha1-vSldHTp1JMrGPdd5uSnuIqNiutQ=" + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/color/-/color-3.1.2.tgz", + "integrity": "sha512-vXTJhHebByxZn3lDvDJYw4lR5+uB3vuoHsuYA5AKuxRVn5wzzIfQKGLBmgdVRHKTJYeK5rvJcHnrd0Li49CFpg==", + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "color-string": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", + "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", + "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" + }, + "cookie-parser": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.4.tgz", + "integrity": "sha512-lo13tqF3JEtFO7FyA49CqbhaFkskRJ0u/UAiINgrIXeRCY41c88/zxtrECl8AKH3B0hj9q10+h3Kt8I7KlW4tw==", + "requires": { + "cookie": "0.3.1", + "cookie-signature": "1.0.6" + }, + "dependencies": { + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + } + } + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "csv-parse": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-4.6.3.tgz", + "integrity": "sha512-pAxEb5kabSaKEwqSXv7vpq6eucXQgY67MLpeLwnYCd21YjTD5OCIIIXGKyUKN/uNQNnzW/elNfxJfozQ1EjB/g==", + "requires": { + "pad": "^3.2.0" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "requires": { + "mimic-response": "^2.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "requires": { + "clone": "^1.0.2" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "denque": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", + "integrity": "sha512-OfzPuSZKGcgr96rf1oODnfjqBFmr1DVoc/TrItj3Ohe0Ah1C5WX5Baquw/9U9KovnQ88EqmJbD66rKYUQYN1tQ==" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" + }, + "diacritics": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz", + "integrity": "sha1-PvqHMj67hj5mls67AILUj/PW96E=" + }, + "dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", + "requires": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "dom-serializer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", + "integrity": "sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA==", + "requires": { + "domelementtype": "^1.3.0", + "entities": "^1.1.1" + } + }, + "domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==" + }, + "domhandler": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", + "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, + "exif-parser": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/exif-parser/-/exif-parser-0.1.12.tgz", + "integrity": "sha1-WKnS1ywCwfbwKg70qRZicrd2CSI=" + }, + "exif-reader": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/exif-reader/-/exif-reader-1.0.3.tgz", + "integrity": "sha512-tWMBj1+9jUSibgR/kv/GQ/fkR0biaN9GEZ5iPdf7jFeH//d2bSzgPoaWf1OfMv4MXFD4upwvpCCyeMvSyLWSfA==" + }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" + }, + "express": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", + "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "requires": { + "accepts": "~1.3.7", + "array-flatten": "1.1.1", + "body-parser": "1.19.0", + "content-disposition": "0.5.3", + "content-type": "~1.0.4", + "cookie": "0.4.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.5", + "qs": "6.7.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.1.2", + "send": "0.17.1", + "serve-static": "1.14.1", + "setprototypeof": "1.1.1", + "statuses": "~1.5.0", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "qs": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", + "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" + } + } + }, + "express-bearer-token": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/express-bearer-token/-/express-bearer-token-2.4.0.tgz", + "integrity": "sha512-2+kRZT2xo+pmmvSY7Ma5FzxTJpO3kGaPCEXPbAm3GaoZ/z6FE4K6L7cvs1AUZwY2xkk15PcQw7t4dWjsl5rdJw==", + "requires": { + "cookie": "^0.3.1", + "cookie-parser": "^1.4.4" + }, + "dependencies": { + "cookie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz", + "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s=" + } + } + }, + "express-cache-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/express-cache-controller/-/express-cache-controller-1.1.0.tgz", + "integrity": "sha512-aRGah9AdVUqyChcFjbL03SVW0c6siE871wzMRuXxCONzYiBDmUtGFEnzZP1ABM6nCo3ovnKPntFRFzAxGqmWtw==", + "requires": { + "lodash.isnumber": "^3.0.3", + "on-headers": "^1.0.1" + } + }, + "express-jwt": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/express-jwt/-/express-jwt-5.3.1.tgz", + "integrity": "sha512-1C9RNq0wMp/JvsH/qZMlg3SIPvKu14YkZ4YYv7gJQ1Vq+Dv8LH9tLKenS5vMNth45gTlEUGx+ycp9IHIlaHP/g==", + "requires": { + "async": "^1.5.0", + "express-unless": "^0.3.0", + "jsonwebtoken": "^8.1.0", + "lodash.set": "^4.0.0" + }, + "dependencies": { + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + } + } + }, + "express-unless": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/express-unless/-/express-unless-0.3.1.tgz", + "integrity": "sha1-JVfBRudb65A+LSR/m1ugFFJpbiA=" + }, + "express-ws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/express-ws/-/express-ws-4.0.0.tgz", + "integrity": "sha512-KEyUw8AwRET2iFjFsI1EJQrJ/fHeGiJtgpYgEWG3yDv4l/To/m3a2GaYfeGyB3lsWdvbesjF5XCMx+SVBgAAYw==", + "requires": { + "ws": "^5.2.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "follow-redirects": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz", + "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==", + "requires": { + "debug": "=3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, + "forwarded": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", + "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + } + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "requires": { + "ajv": "^6.5.5", + "har-schema": "^2.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "hasha": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.1.0.tgz", + "integrity": "sha512-OFPDWmzPN1l7atOV1TgBVmNtBxaIysToK6Ve9DK+vT6pYuklw/nPNT+HJbZi0KDcI6vWB+9tgvZ5YD7fA3CXcA==", + "requires": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + } + }, + "htmlparser2": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", + "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "requires": { + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" + } + }, + "http-errors": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", + "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, + "ipaddr.js": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", + "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==" + }, + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, + "is-buffer": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", + "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==" + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonwebtoken": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz", + "integrity": "sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==", + "requires": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^5.6.0" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", + "requires": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jwks-rsa": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-1.6.0.tgz", + "integrity": "sha512-gLhpd7Ka7Jy8ofm9OVj0PFPtSdx3+W2dncF3UCA1wDTAbvfiB1GhHbbyQlz8bqLF5+rge7pgD/DALRfgZi8Fgg==", + "requires": { + "@types/express-jwt": "0.0.42", + "debug": "^2.6.9", + "jsonwebtoken": "^8.5.1", + "limiter": "^1.1.4", + "lru-memoizer": "^1.12.0", + "ms": "^2.1.1", + "request": "^2.88.0" + }, + "dependencies": { + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, + "jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "requires": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } + }, + "jxon": { + "version": "2.0.0-beta.5", + "resolved": "https://registry.npmjs.org/jxon/-/jxon-2.0.0-beta.5.tgz", + "integrity": "sha1-O2qUEE+YAe5oL9BWZF/1Rz2bND4=", + "requires": { + "xmldom": "^0.1.21" + } + }, + "limiter": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.4.tgz", + "integrity": "sha512-XCpr5bElgDI65vVgstP8TWjv6/QKWm9GU5UG0Pr5sLQ3QLo8NVKsioe+Jed5/3vFOe3IQuqE7DKwTvKQkjTHvg==" + }, + "lock": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/lock/-/lock-0.1.4.tgz", + "integrity": "sha1-/sfervF+fDoKVeHaBCgD4l2RdF0=" + }, + "lodash": { + "version": "4.17.15", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", + "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=" + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=" + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=" + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=" + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=" + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=" + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=" + }, + "lodash.set": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", + "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=" + }, + "lru-cache": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.0.2.tgz", + "integrity": "sha1-HRdnnAac2l0ECZGgnbwsDbN35V4=", + "requires": { + "pseudomap": "^1.0.1", + "yallist": "^2.0.0" + }, + "dependencies": { + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + } + } + }, + "lru-memoizer": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-1.13.0.tgz", + "integrity": "sha512-q0wMolfI7yimhZ36kBAfMLOIuDBpRkieN9do0YPjSzCaiy6r73s8wOEq7Ue/B95VSRbXzfnOr1O1QdJc5UIqaw==", + "requires": { + "lock": "~0.1.2", + "lodash": "^4.17.4", + "lru-cache": "~4.0.0", + "very-fast-args": "^1.1.0" + } + }, + "maxmind": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/maxmind/-/maxmind-3.1.2.tgz", + "integrity": "sha512-YOWYxgw0ydQYK7UlvjkwWJx9Xo4xBmUgo1wzQpgQVmGK0yfdGpkckOd27tDE7DoZQvtRWVN3DNqMqH+LIbBAXw==", + "requires": { + "tiny-lru": "6.0.1" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, + "memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==", + "optional": true + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.40.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", + "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" + }, + "mime-types": { + "version": "2.1.24", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", + "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", + "requires": { + "mime-db": "1.40.0" + } + }, + "mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "minipass": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.1.tgz", + "integrity": "sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.0.tgz", + "integrity": "sha512-EzTZN/fjSvifSX0SlqUERCN39o6T40AMarPbv0MrarSFtIITCBh7bi+dU8nxGFHuqs9jdIAeoYoKuQAAASsPPA==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } + } + }, + "mkdirp-classic": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.2.tgz", + "integrity": "sha512-ejdnDQcR75gwknmMw/tx02AuRs8jCtqFoFqDZMjiNxsu85sRIJVXDKHuLYvUUPRBUtV2FpSZa9bL1BUa3BdR2g==" + }, + "moment": { + "version": "2.24.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", + "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" + }, + "mongodb": { + "version": "3.5.6", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.5.6.tgz", + "integrity": "sha512-sh3q3GLDLT4QmoDLamxtAECwC3RGjq+oNuK1ENV8+tnipIavss6sMYt77hpygqlMOCt0Sla5cl7H4SKCVBCGEg==", + "requires": { + "bl": "^2.2.0", + "bson": "^1.1.4", + "denque": "^1.4.1", + "require_optional": "^1.0.1", + "safe-buffer": "^5.1.2", + "saslprep": "^1.0.0" + }, + "dependencies": { + "bl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-2.2.0.tgz", + "integrity": "sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA==", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "multer": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz", + "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.1", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + } + }, + "nan": { + "version": "2.14.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", + "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" + }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, + "node-abi": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.15.0.tgz", + "integrity": "sha512-FeLpTS0F39U7hHZU1srAK4Vx+5AHNVOTP+hxBNQknR/54laTHSFIJkDWDqiquY1LeLUgTfPN7sLPhMubx0PLAg==", + "requires": { + "semver": "^5.4.1" + } + }, + "nodemailer": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.4.6.tgz", + "integrity": "sha512-/kJ+FYVEm2HuUlw87hjSqTss+GU35D4giOpdSfGp7DO+5h6RlJj7R94YaYHOkoxu1CSaM0d3WRBtCzwXrY6MKA==" + }, + "noop-logger": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", + "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "optimist": { + "version": "0.3.7", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.3.7.tgz", + "integrity": "sha1-yQlBrVnkJzMokjB00s8ufLxuwNk=", + "requires": { + "wordwrap": "~0.0.2" + } + }, + "pad": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/pad/-/pad-3.2.0.tgz", + "integrity": "sha512-2u0TrjcGbOjBTJpyewEl4hBO3OeX5wWue7eIFPzQTg6wFSvoaHcBTTUY5m+n0hd04gmTCPuY0kCpVIVuw5etwg==", + "requires": { + "wcwidth": "^1.0.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "prebuild-install": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.3.tgz", + "integrity": "sha512-GV+nsUXuPW2p8Zy7SarF/2W/oiK8bFQgJcncoJ0d7kRpekEA0ftChjfEaF9/Y+QJEc/wFR7RAEa8lYByuUIe2g==", + "requires": { + "detect-libc": "^1.0.3", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "napi-build-utils": "^1.0.1", + "node-abi": "^2.7.0", + "noop-logger": "^0.1.1", + "npmlog": "^4.0.1", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^3.0.3", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0", + "which-pm-runs": "^1.0.0" + } + }, + "precond": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz", + "integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw=" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "proxy-addr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", + "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", + "requires": { + "forwarded": "~0.1.2", + "ipaddr.js": "1.9.0" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "psl": { + "version": "1.1.32", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.32.tgz", + "integrity": "sha512-MHACAkHpihU/REGGPLj4sEfc/XKW2bheigvHO1dUqjaKigMp1C8+WLQYRGgeKFMsw5PMfegZcaN8IDXK/cD0+g==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", + "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", + "requires": { + "bytes": "3.1.0", + "http-errors": "1.7.2", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, + "readable-stream": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.4.0.tgz", + "integrity": "sha512-jItXPLmrSR8jmTRmRWJXCnGJsfy85mB3Wd/uINMXA65yrnFo0cPClFIUWzo2najVNSl+mx7/4W8ttlLWJe99pQ==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "reconnect-core": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/reconnect-core/-/reconnect-core-1.2.0.tgz", + "integrity": "sha1-KvJfb+LxHyKBn8v0icUWjUlvDw8=", + "requires": { + "backoff": "~2.5.0" + } + }, + "reconnect-net": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/reconnect-net/-/reconnect-net-1.1.1.tgz", + "integrity": "sha1-sXfQ9UjyLuRszeGaOMZdKyVeo3s=", + "requires": { + "reconnect-core": "~1.2.0" + } + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", + "requires": { + "resolve-from": "^2.0.0", + "semver": "^5.1.0" + } + }, + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "saslprep": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz", + "integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==", + "optional": true, + "requires": { + "sparse-bitfield": "^3.0.3" + } + }, + "semver": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", + "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "sharp": { + "version": "0.23.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.23.4.tgz", + "integrity": "sha512-fJMagt6cT0UDy9XCsgyLi0eiwWWhQRxbwGmqQT6sY8Av4s0SVsT/deg8fobBQCTDU5iXRgz0rAeXoE2LBZ8g+Q==", + "requires": { + "color": "^3.1.2", + "detect-libc": "^1.0.3", + "nan": "^2.14.0", + "npmlog": "^4.1.2", + "prebuild-install": "^5.3.3", + "semver": "^6.3.0", + "simple-get": "^3.1.0", + "tar": "^5.0.5", + "tunnel-agent": "^0.6.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, + "simple-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", + "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=" + }, + "simple-get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", + "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", + "requires": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "^0.3.1" + } + }, + "sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", + "optional": true, + "requires": { + "memory-pager": "^1.0.2" + } + }, + "sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", + "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "tar": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/tar/-/tar-5.0.5.tgz", + "integrity": "sha512-MNIgJddrV2TkuwChwcSNds/5E9VijOiw7kAc1y5hTNJoLDSuIyid2QtLYiCYNnICebpuvjhPQZsXwUL0O3l7OQ==", + "requires": { + "chownr": "^1.1.3", + "fs-minipass": "^2.0.0", + "minipass": "^3.0.0", + "minizlib": "^2.1.0", + "mkdirp": "^0.5.0", + "yallist": "^4.0.0" + } + }, + "tar-fs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz", + "integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.0.0" + } + }, + "tar-stream": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.1.2.tgz", + "integrity": "sha512-UaF6FoJ32WqALZGOIAApXx+OdxhekNMChu6axLJR85zMMjXKWFGjbIRe+J6P4UnRGg9rAwWvbTT0oI7hD/Un7Q==", + "requires": { + "bl": "^4.0.1", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "tiny-lru": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-6.0.1.tgz", + "integrity": "sha512-k/vdHz+bFALjmik0URLWBYNuO0hCABTL5dullbZBXvFDdlL8RrKaeLR6YuHfX+6ZXOLkHw+HpNLCUA7DtLMQmg==" + }, + "to-utf8": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/to-utf8/-/to-utf8-0.0.1.tgz", + "integrity": "sha1-0Xrqcv8vujm55DYBvns/9y4ImFI=" + }, + "togeojson": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/togeojson/-/togeojson-0.16.0.tgz", + "integrity": "sha1-q1cp5PjJkg5tpfCP2CSQodFqym0=", + "requires": { + "concat-stream": "~1.5.1", + "minimist": "1.2.0", + "xmldom": "~0.1.19" + }, + "dependencies": { + "concat-stream": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", + "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=", + "requires": { + "inherits": "~2.0.1", + "readable-stream": "~2.0.0", + "typedarray": "~0.0.5" + } + }, + "process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" + }, + "readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, + "togpx": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/togpx/-/togpx-0.5.4.tgz", + "integrity": "sha1-sz27BUHfBL1rpPULhtqVNCS7d3M=", + "requires": { + "concat-stream": "~1.0.1", + "jxon": "~2.0.0-beta.5", + "optimist": "~0.3.5", + "xmldom": "~0.1.17" + }, + "dependencies": { + "concat-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.0.1.tgz", + "integrity": "sha1-AYsYvBx9BzotyCqkhEI0GixN158=", + "requires": { + "bops": "0.0.6" + } + } + } + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" + } + } + }, + "treemap-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/treemap-js/-/treemap-js-1.2.1.tgz", + "integrity": "sha1-TbMxAct1vEXzPVF0fffoDAGvfuM=" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "very-fast-args": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/very-fast-args/-/very-fast-args-1.1.0.tgz", + "integrity": "sha1-4W0dH6+KbllqJGQh/ZCneWPQs5Y=" + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "requires": { + "defaults": "^1.0.3" + } + }, + "which-pm-runs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", + "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=" + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", + "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", + "requires": { + "async-limiter": "~1.0.0" + } + }, + "xmldom": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz", + "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk=" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..abd746c --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "sotlas-api", + "version": "1.0.0", + "description": "", + "main": "server.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC", + "dependencies": { + "@turf/simplify": "^5.1.5", + "axios": "^0.19.0", + "carrier": "^0.3.0", + "csv-parse": "^4.6.3", + "diacritics": "^1.3.0", + "exif-parser": "^0.1.12", + "exif-reader": "^1.0.3", + "express": "^4.17.1", + "express-bearer-token": "^2.4.0", + "express-cache-controller": "^1.1.0", + "express-jwt": "^5.3.1", + "express-ws": "^4.0.0", + "hasha": "^5.1.0", + "htmlparser2": "^3.10.1", + "jwks-rsa": "^1.6.0", + "maxmind": "^3.1.2", + "moment": "^2.24.0", + "mongodb": "^3.5.6", + "multer": "^1.4.2", + "nodemailer": "^6.4.6", + "reconnect-net": "^1.1.1", + "request": "^2.88.0", + "sharp": "^0.23.4", + "togeojson": "^0.16.0", + "togpx": "^0.5.4", + "treemap-js": "^1.2.1" + } +} diff --git a/photos.js b/photos.js new file mode 100644 index 0000000..271edc0 --- /dev/null +++ b/photos.js @@ -0,0 +1,108 @@ +const sharp = require('sharp') +const crypto = require('crypto') +const fs = require('fs') +const fsPromises = require('fs').promises +const exif = require('exif-reader') +const path = require('path') +const hasha = require('hasha') +const config = require('./config') +const db = require('./db') + +module.exports = { + importPhoto: async function(filename, author) { + // Hash input file to determine filename + let hash = await hasha.fromFile(filename, {algorithm: 'sha256'}) + let hashFilename = hash.substr(0, 32) + '.jpg' + let originalPath = config.photos.paths.original + '/' + hashFilename.substr(0, 2) + '/' + hashFilename + await fsPromises.mkdir(path.dirname(originalPath), {recursive: true}) + + let metadata = await getMetadata(filename) + if (metadata.format !== 'jpeg' && metadata.format != 'png' && metadata.format != 'heif') { + throw new Error('Bad input format, must be JPEG, PNG or HEIF') + } + + await fsPromises.copyFile(filename, originalPath) + + let photo = { + filename: hashFilename, + width: Math.round(metadata.width), + height: Math.round(metadata.height), + author, + uploadDate: new Date() + } + + if (metadata.orientation && metadata.orientation >= 5) { + // Swap width/height + let tmp = photo.width + photo.width = photo.height + photo.height = tmp + } + + if (metadata.exif) { + let exifParsed = exif(metadata.exif) + if (exifParsed) { + if (exifParsed.gps && exifParsed.gps.GPSLatitude && exifParsed.gps.GPSLongitude && + (!exifParsed.gps.GPSStatus || exifParsed.gps.GPSStatus === 'A') && + !isNaN(exifParsed.gps.GPSLatitude[0]) && !isNaN(exifParsed.gps.GPSLongitude[0])) { + photo.coordinates = {} + photo.coordinates.latitude = exifParsed.gps.GPSLatitude[0] + exifParsed.gps.GPSLatitude[1]/60 + exifParsed.gps.GPSLatitude[2]/3600 + if (exifParsed.gps.GPSLatitudeRef === 'S') { + photo.coordinates.latitude = -photo.coordinates.latitude + } + photo.coordinates.longitude = exifParsed.gps.GPSLongitude[0] + exifParsed.gps.GPSLongitude[1]/60 + exifParsed.gps.GPSLongitude[2]/3600 + if (exifParsed.gps.GPSLongitudeRef === 'W') { + photo.coordinates.longitude = -photo.coordinates.longitude + } + + if (exifParsed.gps.GPSImgDirection && exifParsed.gps.GPSImgDirection >= 0 && exifParsed.gps.GPSImgDirection < 360) { + photo.direction = Math.round(exifParsed.gps.GPSImgDirection) + } + + if (exifParsed.gps.GPSHPositioningError) { + photo.positioningError = Math.round(exifParsed.gps.GPSHPositioningError) + } + } + + if (exifParsed.image && exifParsed.image.Make && exifParsed.image.Model) { + photo.camera = exifParsed.image.Make + ' ' + exifParsed.image.Model + } + + if (exifParsed.exif) { + if (exifParsed.exif.DateTimeDigitized) { + photo.date = exifParsed.exif.DateTimeDigitized + } else if (exifParsed.exif.DateTimeOriginal) { + photo.date = exifParsed.exif.DateTimeOriginal + } + } + } + } + + let mkdirTasks = [] + let resizeTasks = [] + Object.keys(config.photos.sizes).forEach(sizeDescr => { + let outPath = config.photos.paths[sizeDescr] + '/' + hashFilename.substr(0, 2) + '/' + hashFilename + mkdirTasks.push(fsPromises.mkdir(path.dirname(outPath), {recursive: true})) + resizeTasks.push(makeResized(originalPath, outPath, config.photos.sizes[sizeDescr].width, config.photos.sizes[sizeDescr].height)) + }) + + await Promise.all(mkdirTasks) + await Promise.all(resizeTasks) + + db.getDb().collection('uploads').insertOne({ + uploadDate: new Date(), + type: 'photo', + filename: hashFilename, + author + }) + + return photo + } +} + +function getMetadata(src) { + return sharp(src).metadata() +} + +function makeResized(src, dst, maxWidth, maxHeight) { + return sharp(src).rotate().resize({ height: maxHeight, width: maxWidth, fit: 'inside' }).toFile(dst) +} diff --git a/photos_router.js b/photos_router.js new file mode 100644 index 0000000..347909b --- /dev/null +++ b/photos_router.js @@ -0,0 +1,216 @@ +const express = require('express') +const multer = require('multer') +const config = require('./config') +const photos = require('./photos') +const jwt = require('express-jwt') +const jwksRsa = require('jwks-rsa') +const nodemailer = require('nodemailer') +const db = require('./db') + +let upload = multer({dest: config.photos.uploadPath}) + +let router = express.Router() +module.exports = router + +let jwtCallback = jwt({ + secret: jwksRsa.expressJwtSecret({ + cache: true, + rateLimit: true, + jwksRequestsPerMinute: 5, + jwksUri: config.sso.jwksUri + }) +}) + +router.post('/summits/:association/:code/upload', jwtCallback, upload.array('photo'), async (req, res) => { + try { + res.cacheControl = { + noCache: true + } + + if (!req.user.callsign) { + res.status(401).send('Missing callsign in SSO token').end() + return + } + + let summitCode = req.params.association + '/' + req.params.code + let summit = await db.getDb().collection('summits').findOne({code: summitCode}) + if (!summit) { + res.status(404).end() + return + } + + if (req.files) { + let dbPhotos = [] + for (let file of req.files) { + let photo = await photos.importPhoto(file.path, req.user.callsign) + dbPhotos.push(photo) + } + + // Check for duplicates + if (summit.photos) { + dbPhotos = dbPhotos.filter(photo => !summit.photos.some(summitPhoto => summitPhoto.filename === photo.filename )) + } + + if (dbPhotos.length > 0) { + await db.getDb().collection('summits').updateOne({code: summitCode}, { $push: { photos: { $each: dbPhotos } } }) + + let transporter = nodemailer.createTransport(config.mail) + transporter.sendMail({ + from: 'api@sotl.as', + to: 'mk@neon1.net', + subject: 'New photos added to summit ' + summitCode + ' by ' + req.user.callsign, + text: `${dbPhotos.length} new photos have been added. https://sotl.as/summits/${summitCode}\n`, + attachments: dbPhotos.map(photo => { + return { + filename: photo.filename, + path: config.photos.paths.thumb + '/' + photo.filename.substr(0, 2) + '/' + photo.filename + } + }) + }) + } + + res.json(dbPhotos) + } else { + res.status(400).end() + } + } catch (err) { + console.error(err) + res.status(500).end() + } +}) + +router.delete('/summits/:association/:code/:filename', jwtCallback, async (req, res) => { + res.cacheControl = { + noCache: true + } + + if (!req.user.callsign) { + res.status(401).send('Missing callsign in SSO token').end() + return + } + + let summitCode = req.params.association + '/' + req.params.code + let summit = await db.getDb().collection('summits').findOne({code: summitCode}) + let photo = summit.photos.find(photo => photo.filename === req.params.filename) + if (!photo) { + res.status(404).end() + return + } + + // Check that uploader is currently logged in user + if (photo.author !== req.user.callsign) { + res.status(401).send('Cannot delete another user\'s photos').end() + return + } + + await db.getDb().collection('summits').updateOne({code: summitCode}, { $pull: { photos: { filename: req.params.filename } } }) + + res.status(204).end() +}) + +router.post('/summits/:association/:code/reorder', jwtCallback, async (req, res) => { + res.cacheControl = { + noCache: true + } + + if (!req.user.callsign) { + res.status(401).send('Missing callsign in SSO token').end() + return + } + + let summitCode = req.params.association + '/' + req.params.code + + // Assign new sortOrder index to photos of this user, in the order given by req.body.filenames + let updates = req.body.filenames.map((filename, index) => { + return db.getDb().collection('summits').updateOne( + { code: summitCode, 'photos.author': req.user.callsign, 'photos.filename': filename }, + { $set: { 'photos.$.sortOrder': index + 1 } } + ) + }) + + await Promise.all(updates) + + res.status(204).end() +}) + +router.post('/summits/:association/:code/:filename', jwtCallback, async (req, res) => { + res.cacheControl = { + noCache: true + } + + if (!req.user.callsign) { + res.status(401).send('Missing callsign in SSO token').end() + return + } + + let summitCode = req.params.association + '/' + req.params.code + let summit = await db.getDb().collection('summits').findOne({code: summitCode}) + let photo = summit.photos.find(photo => photo.filename === req.params.filename) + if (!photo) { + res.status(404).end() + return + } + + // Check that editor is the currently logged in user + if (photo.author !== req.user.callsign) { + res.status(401).send('Cannot delete another user\'s photos').end() + return + } + + let update = { + $set: {}, + $unset: {} + } + + if (req.body.title) { + update.$set['photos.$.title'] = req.body.title + } else { + update.$unset['photos.$.title'] = '' + } + + if (req.body.date) { + update.$set['photos.$.date'] = new Date(req.body.date) + } else { + update.$unset['photos.$.date'] = '' + } + + if (req.body.coordinates) { + update.$set['photos.$.coordinates'] = req.body.coordinates + update.$set['photos.$.positioningError'] = req.body.positioningError + } else { + update.$unset['photos.$.coordinates'] = '' + update.$unset['photos.$.positioningError'] = '' + } + + if (req.body.direction !== null && req.body.direction !== undefined && req.body.direction !== '') { + update.$set['photos.$.direction'] = req.body.direction + } else { + update.$unset['photos.$.direction'] = '' + } + + if (req.body.isCover) { + update.$set['photos.$.isCover'] = true + + // Only one photo can be the cover photo, so unmark all others first + await db.getDb().collection('summits').updateOne( + { code: summitCode }, + { $unset: { 'photos.$[].isCover': '' } } + ) + } else { + update.$unset['photos.$.isCover'] = '' + } + + if (Object.keys(update.$set).length === 0) { + delete update.$set + } + if (Object.keys(update.$unset).length === 0) { + delete update.$unset + } + + await db.getDb().collection('summits').updateOne( + { code: summitCode, 'photos.filename': req.params.filename }, + update + ) + + res.status(204).end() +}) diff --git a/rbn.js b/rbn.js new file mode 100644 index 0000000..458eec7 --- /dev/null +++ b/rbn.js @@ -0,0 +1,158 @@ +const reconnect = require('reconnect-net'); +const wsManager = require('./ws-manager'); +const carrier = require('carrier'); +const config = require('./config'); +const db = require('./db'); +const utils = require('./utils'); + +const rbnSpotRegex = /^DX de (\S+):\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+dB\s+(\S+)\s+\S+\s+(CQ|DX)\s+(\d+)Z$/; + +class RbnReceiver { + start() { + this.restartConnection(); + + wsManager.on('message', (ws, message) => { + if (message.rbnFilter !== undefined) { + console.log("Set RBN filter to " + JSON.stringify(message.rbnFilter)); + ws.rbnFilter = message.rbnFilter; + + this.sendSpotHistory(ws) + } + }); + } + + restartConnection() { + if (this.re) + this.re.disconnect(); + + this.resetTimer(); + this.re = reconnect((stream) => { + console.log("Connected to RBN"); + stream.write(config.rbn.login + "\r\n"); + if (config.rbn.server.commands) { + config.rbn.server.commands.forEach(command => { + stream.write(command + "\r\n"); + }); + } + + carrier.carry(stream, (line) => { + this.handleLine(line); + }); + }); + + this.re.on('error', (err) => { + console.error(`RBN connection error: ${err}`); + }); + + this.re.connect(config.rbn.server); + } + + resetTimer() { + if (this.timeout) { + clearTimeout(this.timeout); + } + + this.timeout = setTimeout(() => { + console.error("RBN: timeout, reconnecting"); + this.restartConnection(); + }, config.rbn.timeout); + } + + handleLine(line) { + this.resetTimer(); + let matches = rbnSpotRegex.exec(line); + if (matches) { + let spot = { + timeStamp: new Date(), + callsign: matches[3], + homeCallsign: this.homeCallsign(matches[3]), + spotter: matches[1].replace("-#", ""), + frequency: parseFloat((matches[2]/1000).toFixed(4)), + mode: matches[4], + snr: parseInt(matches[5]), + speed: parseInt(matches[6]) + }; + + // Check if this is a known SOTA activator + db.getDb().collection('activators').countDocuments({callsign: { $in: utils.makeCallsignVariations(spot.homeCallsign) }}, (error, result) => { + if (result > 0) { + spot.isActivator = true; + } + db.getDb().collection('rbnspots').insertOne(spot, (error, result) => { + // _id has now been added, but not in our preferred format + spot._id = spot._id.toHexString() + wsManager.broadcast({'rbnSpot': spot}, (ws) => { + if (!ws.rbnFilter) { + return false; + } + + if (ws.rbnFilter.homeCallsign && ws.rbnFilter.homeCallsign.includes(spot.homeCallsign)) { + return true; + } + + if (ws.rbnFilter.isActivator && spot.isActivator) { + return true; + } + + return false; + }); + }); + }); + } + } + + sendSpotHistory(ws) { + // Send the spot history for the currently defined RBN filter + if (!ws.rbnFilter.homeCallsign && !ws.rbnFilter.isActivator) { + return; + } + + let query = {}; + + if (ws.rbnFilter.homeCallsign) { + query.homeCallsign = ws.rbnFilter.homeCallsign; + } + if (ws.rbnFilter.isActivator) { + query.isActivator = true; + } + + let maxAge = parseInt(ws.rbnFilter.maxAge) || 3600000; + query.timeStamp = {$gte: new Date(new Date().getTime() - maxAge)}; + + db.getDb().collection('rbnspots').find(query).sort({timeStamp: -1}).limit(config.rbn.maxSpotHistory).toArray((err, rbnSpots) => { + if (err) { + console.error(err); + return; + } + + rbnSpots.forEach(spot => { + spot._id = spot._id.toHexString(); + }); + + let response = {rbnSpotHistory: rbnSpots}; + if (ws.rbnFilter.viewId) { + response.viewId = ws.rbnFilter.viewId; + } + wsManager.unicast(response, ws); + }); + } + + homeCallsign(callsign) { + let parts = callsign.split('/'); + let longestPart = ''; + parts.forEach(part => { + if (part.length > longestPart.length) { + longestPart = part; + } + }) + + // For UK callsigns, normalize them all to 2E/G/M for the sake of comparison + let matches = longestPart.match(/^(2[DEIJMUW]|G[DIJMUW]?|M[DIJMUW]?)(\d[A-Z]{2,3})$/) + if (matches) { + longestPart = matches[1].replace(/^2./, '2E').replace(/^G[DIJMUW]/, 'G').replace(/^M[DIJMUW]/, 'M') + matches[2] + } + return longestPart; + } +} + +module.exports = RbnReceiver; diff --git a/server.js b/server.js new file mode 100644 index 0000000..5ac3b30 --- /dev/null +++ b/server.js @@ -0,0 +1,266 @@ +const express = require('express'); +const config = require('./config'); +const assert = require('assert'); +const app = express(); +const expressWs = require('express-ws')(app); +const cacheControl = require('express-cache-controller'); +const bearerToken = require('express-bearer-token'); +const wsManager = require('./ws-manager'); +const SotaSpotReceiver = require('./sotaspots'); +const RbnReceiver = require('./rbn'); +const db = require('./db'); +const alerts = require('./alerts'); +const geoexport = require('./geoexport'); +const activations = require('./activations'); +const utils = require('./utils'); +const photos_router = require('./photos_router') +const tracks_router = require('./tracks_router') +const maxmind = require('maxmind'); + +let dbChecker = (req, res, next) => { + if (db.getDb() == null) { + console.error('DB error'); + res.status(500).end(); + return; + } + next(); +}; +app.enable('trust proxy'); +app.use(express.json()); +app.use(dbChecker); +app.use(cacheControl({ + maxAge: 3600 +})); +app.use(bearerToken()); +app.use('/ws', wsManager.router); +app.use('/alerts', alerts); +app.use('/geoexport', geoexport); +app.use('/activations', activations); +app.use('/photos', photos_router); +app.use('/tracks', tracks_router); + +let sotaSpotReceiver = new SotaSpotReceiver(); +let rbnReceiver = new RbnReceiver(); +rbnReceiver.start(); + +app.get('/summits/search', (req, res) => { + let limit = 100; + if (req.query.limit) { + let limitOverride = parseInt(req.query.limit); + if (limitOverride > 0 && limitOverride < limit) { + limit = limitOverride; + } + } + db.getDb().collection('summits').find({$or: [{code: {'$regex': req.query.q, '$options': 'i'}}, {name: {'$regex': req.query.q, '$options': 'i'}}, {nameNd: {'$regex': req.query.q, '$options': 'i'}}]}, {projection: {'_id': false, 'photos': false, 'routes': false, 'links': false, 'resources': false}}).limit(limit).toArray((err, summits) => { + if (err) { + console.error(err); + res.status(500).end(); + return; + } + + res.json(summits); + }); +}); + +app.get('/summits/near', (req, res) => { + let limit = 100; + if (req.query.limit) { + let limitOverride = parseInt(req.query.limit); + if (limitOverride > 0 && limitOverride < limit) { + limit = limitOverride; + } + } + let query = { + coordinates: {$near: {$geometry: {type: "Point", coordinates: [parseFloat(req.query.lon), parseFloat(req.query.lat)]}}} + }; + if (req.query.maxDistance) { + query.coordinates.$near.$maxDistance = parseFloat(req.query.maxDistance); + } + if (!req.query.inactive) { + query.validFrom = {$lte: new Date()}; + query.validTo = {$gte: new Date()}; + } + db.getDb().collection('summits').find(query, {projection: {'_id': false, 'photos': false, 'routes': false, 'links': false, 'resources': false}}).limit(limit).toArray((err, summits) => { + if (err) { + console.error(err); + res.status(500).end(); + return; + } + + res.json(summits); + }); +}); + +app.get('/summits/:association/:code', (req, res) => { + db.getDb().collection('summits').findOne({code: req.params.association + '/' + req.params.code}, {projection: {'_id': false}}, (err, summit) => { + if (err) { + console.error(err); + res.status(500).end(); + return; + } + + if (!summit) { + res.status(404).end(); + return; + } + + let associationCode = summit.code.substring(0, summit.code.indexOf('/')); + db.getDb().collection('associations').findOne({code: associationCode}, (err, association) => { + if (association) { + summit.isoCode = association.isoCode; + summit.continent = association.continent; + } + res.json(summit); + }); + }); +}); + +// Dummy POST endpoint to help browser invalidate cache after uploading photos +app.post('/summits/:association/:code', (req, res) => { + res.cacheControl = { + noCache: true + }; + + res.status(204).end(); +}); + +app.get('/associations/all', (req, res) => { + db.getDb().collection('associations').find({}, {projection: {'_id': false}}).toArray((err, associations) => { + if (err) { + console.error(err); + res.status(500).end(); + return; + } + + res.json(associations); + }); +}); + +app.get('/associations/:association', (req, res) => { + db.getDb().collection('associations').findOne({code: req.params.association}, {projection: {'_id': false}}, (err, association) => { + if (err) { + console.error(err); + res.status(500).end(); + return; + } + + if (!association) { + res.status(404).end(); + return; + } + + res.json(association); + }); +}); + +app.get('/regions/:association/:region', (req, res) => { + let region = req.params.association + '/' + req.params.region; + if (!region.match(/^[A-Z0-9]+\/[A-Z0-9]+$/)) { + res.status(400).end(); + return; + } + db.getDb().collection('summits').find({code: {'$regex': '^' + region}}, {projection: {'_id': false, 'routes': false, 'links': false, 'resources': false}}).toArray((err, summits) => { + if (err) { + console.error(err); + res.status(500).end(); + return; + } + + summits.forEach(summit => { + if (summit.photos && summit.photos.length > 0) { + summit.hasPhotos = true; + } + delete summit.photos; + }); + + res.json(summits); + }); +}); + +app.get('/activators/search', (req, res) => { + let skip = 0; + if (req.query.skip) { + skip = parseInt(req.query.skip); + } + let limit = 100; + if (req.query.limit) { + if (parseInt(req.query.limit) <= limit) { + limit = parseInt(req.query.limit); + } + } + let sortField = 'score'; + let sortDirection = -1; + if (req.query.sort === 'callsign' || req.query.sort === 'points' || req.query.sort === 'bonusPoints' || req.query.sort === 'score' || req.query.sort === 'summits' || req.query.sort === 'avgPoints') { + sortField = req.query.sort; + } + if (req.query.sortDirection == 'desc') { + sortDirection = -1; + } else { + sortDirection = 1; + } + let sort = {}; + sort[sortField] = sortDirection; + let query = {}; + if (req.query.q !== undefined && req.query.q !== '') { + query = {callsign: {'$regex': req.query.q, '$options': 'i'}}; + } + let cursor = db.getDb().collection('activators').find(query, {projection: {'_id': false}}).sort(sort); + cursor.count((err, count) => { + if (err) { + console.error(err); + res.status(500).end(); + return; + } + + cursor.skip(skip).limit(limit).toArray((err, activators) => { + res.json({activators, total: count}); + }); + }); +}); + +app.get('/activators/:callsign', (req, res) => { + db.getDb().collection('activators').findOne({callsign: req.params.callsign}, {projection: {'_id': false}}, (err, activator) => { + if (err) { + console.error(err); + res.status(500).end(); + return; + } + if (!activator) { + // Try alternative variations + db.getDb().collection('activators').findOne({callsign: { $in: utils.makeCallsignVariations(req.params.callsign) }}, {projection: {'_id': false}}, (err, activator) => { + if (activator) { + res.json(activator); + } else { + res.status(404).end(); + } + }); + return; + } + + res.json(activator); + }); +}); + +let geoLookup; +maxmind.open(config.geoip.path).then((lookup) => { + geoLookup = lookup; +}); +app.get('/map_server', (req, res) => { + let mapServer = 'us'; + let geo = geoLookup.get(req.ip); + if (geo.continent.code === 'AF' || geo.continent.code === 'EU') { + mapServer = 'eu'; + } + res.json({mapServer}); +}); + +app.get('/my_coordinates', (req, res) => { + let geo = geoLookup.get(req.ip); + if (!geo) { + res.json({}); + } else { + res.json({latitude: geo.location.latitude, longitude: geo.location.longitude}); + } +}); + +app.listen(config.http.port, config.http.host); diff --git a/sotaspots.js b/sotaspots.js new file mode 100644 index 0000000..31f1a6f --- /dev/null +++ b/sotaspots.js @@ -0,0 +1,135 @@ +const axios = require('axios'); +const wsManager = require('./ws-manager'); +const config = require('./config'); +const db = require('./db'); +const TreeMap = require("treemap-js"); + +class SotaSpotReceiver { + constructor() { + this.latestSpots = new TreeMap(); + this.lastUpdate = null; + + wsManager.on('connect', (ws) => { + let spots = [] + this.latestSpots.each(spot => { + spots.push(spot) + }); + wsManager.unicast({spots}, ws); + }) + + this.loadSpots(); + setInterval(() => { + this.loadSpots(); + }, config.sotaspots.updateInterval); + } + + loadSpots() { + let numSpotsToLoad = config.sotaspots.periodicLoadSpots; + if (this.latestSpots.getLength() == 0) { + numSpotsToLoad = config.sotaspots.initialLoadSpots; + } + console.log(`load ${numSpotsToLoad} spots`); + axios.get(config.sotaspots.url + '/' + numSpotsToLoad + '/all') + .then(response => { + let minSpotId = undefined; + let currentSpotIds = new Set(); + response.data.forEach(spot => { + spot.summit = {code: spot.associationCode.toUpperCase().trim() + '/' + spot.summitCode.toUpperCase().trim()}; + spot.timeStamp = new Date(spot.timeStamp); + spot.activatorCallsign = spot.activatorCallsign.toUpperCase().replace(/[^A-Z0-9\/-]/g, '') + delete spot.associationCode; + delete spot.summitCode; + delete spot.summitDetails; + delete spot.highlightColor; + delete spot.activatorName; + if (spot.comments === '(null)') { + spot.comments = ''; + } + this.updateSpot(spot); + + currentSpotIds.add(spot.id); + if (minSpotId === undefined || spot.id < minSpotId) { + minSpotId = spot.id; + } + }); + this.removeDeletedSpots(minSpotId, currentSpotIds); + this.removeExpiredSpots(); + }); + } + + updateSpot(spot) { + // Check if we already have this spot in the list, and if it has changed + if (this.spotsAreEqual(this.latestSpots.get(spot.id), spot)) { + return; + } + + // Spot is new or modified + console.log("New/modified spot id " + spot.id); + this.lookupSummit(spot.summit.code) + .then(summit => { + if (summit) { + spot.summit = summit; + } + + this.lookupAssociation(spot.summit.code.substring(0, spot.summit.code.indexOf('/'))) + .then(association => { + if (association) { + spot.summit.isoCode = association.isoCode; + spot.summit.continent = association.continent; + } + + this.latestSpots.set(spot.id, spot); + wsManager.broadcast({spot}); + }) + }) + } + + deleteSpotById(spotId) { + console.log("Deleted spot id " + spotId); + if (this.latestSpots.get(spotId) !== undefined) { + this.latestSpots.remove(spotId); + wsManager.broadcast({deleteSpot: {id: spotId}}); + } + } + + removeDeletedSpots(minSpotId, currentSpotIds) { + // Consider all spots with ID >= minSpotId and not in currentSpotIds as deleted + this.latestSpots.each((spot, curId) => { + if (curId >= minSpotId && !currentSpotIds.has(curId)) { + this.deleteSpotById(curId); + } + }); + } + + removeExpiredSpots() { + let now = new Date(); + while (this.latestSpots.getLength() > 0) { + let minKey = this.latestSpots.getMinKey(); + if ((now - this.latestSpots.get(minKey).timeStamp) > config.sotaspots.maxSpotAge) { + console.log('Remove spot ID ' + minKey); + this.latestSpots.remove(minKey); + } else { + break; + } + } + } + + lookupSummit(summitCode, callback) { + return db.getDb().collection('summits').findOne({code: summitCode}, {projection: {'_id': false, code: true, name: true, altitude: true, points: true, activationCount: true}}); + } + + lookupAssociation(associationCode, callback) { + return db.getDb().collection('associations').findOne({code: associationCode}); + } + + spotsAreEqual(spot1, spot2) { + if (spot1 === undefined || spot2 === undefined) { + return false; + } + return (spot1.id === spot2.id && spot1.comments === spot2.comments && spot1.callsign === spot2.callsign && + spot1.summit.code === spot2.summit.code && spot1.activatorCallsign === spot2.activatorCallsign && + spot1.frequency === spot2.frequency && spot1.mode === spot2.mode); + } +} + +module.exports = SotaSpotReceiver; diff --git a/spots.js b/spots.js new file mode 100644 index 0000000..a4bcf84 --- /dev/null +++ b/spots.js @@ -0,0 +1,74 @@ +const axios = require('axios'); +const wsManager = require('./ws-manager'); +const config = require('./config'); +const db = require('./db'); +const TreeMap = require("treemap-js"); + +const latestSpots = new TreeMap(); +const maxSpots = 100; +const updateInterval = 30000; +let lastUpdate = null; + +wsManager.on('connect', (ws) => { + let spots = [] + latestSpots.each(spot => { + spots.push(spot) + }); + wsManager.unicast({spots}, ws); +}) + +loadSpots(); +setInterval(loadSpots, updateInterval); + +function loadSpots() { + console.log('load spots'); + axios.get('https://sota-api2.azurewebsites.net/api/spots/' + maxSpots + '/all') + .then(response => { + response.data.forEach(spot => { + spot.summit = {code: spot.associationCode + '/' + spot.summitCode}; + delete spot.associationCode; + delete spot.summitCode; + delete spot.summitDetails; + delete spot.highlightColor; + if (spot.comments === '(null)') { + spot.comments = ''; + } + updateSpot(spot); + }); + }); +} + +function updateSpot(spot) { + // Check if we already have this spot in the list, and if it has changed + if (spotsAreEqual(latestSpots.get(spot.id), spot)) { + return; + } + + // Spot is new or modified + console.log("New/modified spot id " + spot.id); + lookupSummit(spot.summit.code) + .then(summit => { + if (summit) { + spot.summit = summit; + } + + latestSpots.set(spot.id, spot); + while (latestSpots.getLength() > maxSpots) { + latestSpots.remove(latestSpots.getMinKey()); + } + wsManager.broadcast({spot}); + }) +} + +function lookupSummit(summitCode, callback) { + return db.getDb().collection('summits').findOne({code: summitCode}, {projection: {'_id': false, code: true, name: true, altitude: true, points: true, activationCount: true}}); +} + +function spotsAreEqual(spot1, spot2) { + if (spot1 === undefined || spot2 === undefined) { + return false; + } + return (spot1.id === spot2.id && spot1.comments === spot2.comments && spot1.callsign === spot2.callsign && + spot1.summit.code === spot2.summit.code && spot1.activatorCallsign === spot2.activatorCallsign && + spot1.frequency === spot2.frequency && spot1.mode === spot2.mode); +} diff --git a/summits.js b/summits.js new file mode 100644 index 0000000..4bbf2bc --- /dev/null +++ b/summits.js @@ -0,0 +1,68 @@ +const moment = require('moment'); +const db = require('./db'); + +module.exports = { + lookupSummits: function(objects) { + // Get all summit refs so we can look them all up in one go + let summitCodes = new Set(); + let associationCodes = new Set(); + objects.forEach(obj => { + summitCodes.add(obj.summit.code); + associationCodes.add(obj.summit.code.substring(0, obj.summit.code.indexOf('/'))); + }); + + return new Promise((resolve, reject) => { + db.getDb().collection('summits').find({code: {$in: [...summitCodes]}}, {projection: {'_id': false, code: true, name: true, altitude: true, points: true, coordinates: true, activationCount: true, validFrom: true, validTo: true, 'photos.author': true}}) + .toArray((err, summits) => { + if (err) { + reject(err); + return; + } + + let summitMap = {}; + let now = moment(); + summits.forEach(summit => { + if (now.isBefore(summit.validFrom) || now.isAfter(summit.validTo)) { + summit.invalid = true; + } + delete summit.validFrom; + delete summit.validTo; + if (summit.photos) { + let photoAuthors = new Set(); + summit.photos.forEach(photo => { + photoAuthors.add(photo.author); + }); + summit.photoAuthors = [...photoAuthors]; + delete summit.photos; + } + summitMap[summit.code] = summit; + }); + + db.getDb().collection('associations').find({code: {$in: [...associationCodes]}}).toArray((err, associations) => { + if (err) { + reject(err); + return; + } + + let associationMap = {}; + associations.forEach(association => { + associationMap[association.code] = association; + }); + + objects.forEach(object => { + let association = object.summit.code.substring(0, object.summit.code.indexOf('/')); + if (summitMap[object.summit.code]) { + object.summit = summitMap[object.summit.code]; + if (object.summit) { + object.summit.isoCode = associationMap[association].isoCode; + object.summit.continent = associationMap[association].continent; + } + } + }); + + resolve(objects); + }); + }); + }); + } +} diff --git a/tools/importActivators.js b/tools/importActivators.js new file mode 100644 index 0000000..5578e84 --- /dev/null +++ b/tools/importActivators.js @@ -0,0 +1,61 @@ +const axios = require('axios'); +const MongoClient = require('mongodb').MongoClient; +const config = require('../config'); +const assert = require('assert'); +const htmlparser = require('htmlparser2'); + +const client = new MongoClient(config.mongodb.url, {useUnifiedTopology: true}); +client.connect(err => { + assert.equal(null, err); + + importActivators(client.db(config.mongodb.dbName)); +}); + +async function importActivators(db) { + let response = await axios.get('https://api-db.sota.org.uk/admin/activator_roll?associationID=-1') + + // Weed out duplicate callsigns, keeping only the record with the higher number of points + let activators = new Map(); + response.data.forEach(record => { + let callsign = record.Callsign.toUpperCase().trim().replace('/P', ''); + let existingActivator = activators.get(callsign); + if (existingActivator === undefined || existingActivator.Points < record.Points) { + activators.set(callsign, record); + } + }); + + let lastUpdate = new Date(); + let bulkWrites = []; + for (let record of activators.values()) { + let activator = { + callsign: record.Callsign.toUpperCase().trim().replace('/P', ''), + username: record.Username, + userId: record.UserID, + summits: record.Summits, + points: record.Points, + bonusPoints: record.BonusPoints, + score: record.totalPoints, + avgPoints: parseFloat(record.Average), + lastUpdate + }; + + bulkWrites.push({updateOne: { + filter: {callsign: activator.callsign}, + update: { $set: activator}, + upsert: true + }}); + + if (bulkWrites.length >= config.mongodb.batchSize) { + await db.collection('activators').bulkWrite(bulkWrites); + bulkWrites = []; + } + } + + if (bulkWrites.length > 0) { + await db.collection('activators').bulkWrite(bulkWrites); + } + + await db.collection('activators').deleteMany({lastUpdate: {$lt: lastUpdate}}); + + await client.close(); +} diff --git a/tools/importPhoto.js b/tools/importPhoto.js new file mode 100644 index 0000000..c2ed2ca --- /dev/null +++ b/tools/importPhoto.js @@ -0,0 +1,33 @@ +const MongoClient = require('mongodb').MongoClient +const config = require('../config') +const assert = require('assert') +const photos = require('../photos') +const db = require('../db') + +let author = process.argv[2] +if (!author) { + console.error("usage: author file file ...") + process.exit(0) +} + +db.waitDb(() => { + let imports = [] + process.argv.slice(3).forEach(filename => { + imports.push(photos.importPhoto(filename, author)) + }) + + // Run imports in series + return imports.reduce((promiseChain, currentImport) => { + return promiseChain.then(chainResults => + currentImport.then(currentResult => + [ ...chainResults, currentResult ] + ) + ) + }, Promise.resolve([])).then(photos => { + console.log(JSON.stringify(photos)) + + db.closeDb() + }).catch(err => { + console.error(err) + }) +}) diff --git a/tools/isocodes.txt b/tools/isocodes.txt new file mode 100644 index 0000000..693a9c9 --- /dev/null +++ b/tools/isocodes.txt @@ -0,0 +1,97 @@ +3Y,no,AF +4O,me,EU +4X,il,AS +5B,cy,AS +9A,hr,EU +9H,mt,EU +9M,my,AS +9V,sg,AS +A6,ae,AS +BV,tw,AS +CT,pt,EU +CU,pt,EU +CX,uy,SA +D,de,EU +E5,ck,OC +E7,ba,EU +EA,es,EU +EI,ie,EU +ES,ee,EU +FG,gp,NA +FH,yt,AF +FJ,fr,NA +FK,nc,OC +FM,mq,NA +FO,pf,OC +FP,fr,NA +FR,fr,AF +FT,fr,AF +F,fr,EU +GD,im,EU +GI,gb-nir,EU +GJ,je,EU +GM,gb-sct,EU +GU,gg,EU +GW,gb-wls,EU +G,gb-eng,EU +HA,hu,EU +HB0,li,EU +HB,ch,EU +HI,do,NA +HL,kr,AS +HR,hn,NA +I,it,EU +JA,jp,AS +JX,sj,EU +KP4,pr,NA +KH2,gu,OC +KH0,mp,OC +K,us,NA +LA,no,EU +LU,ar,SA +LX,lu,EU +LY,lt,EU +LZ,bg,EU +OD,lb,AS +OE,at,EU +OH,fi,EU +OK,cz,EU +OM,sk,EU +ON,be,EU +OY,fo,EU +OZ,dk,EU +PA,nl,EU +PP,br,SA +PQ,br,SA +PR,br,SA +PS,br,SA +PT,br,SA +PY,br,SA +R,ru,EU +S5,si,EU +S7,sc,AF +SM,se,EU +SP,pl,EU +SV,gr,EU +TF,is,EU +TI,cr,NA +TK,fr,EU +UT,ua,EU +V5,na,AF +VE,ca,NA +VK,au,OC +VO,ca,NA +VP8,fk,SA +VY,ca,EU +W,us,NA +XE,mx,NA +XF,mx,NA +YB,id,AS +YL,lv,EU +YO,ro,EU +YU,rs,EU +Z3,mk,EU +ZB2,gi,EU +ZD,gb,AF +ZL,nz,OC +ZS,za,AF diff --git a/tools/regenThumbs.js b/tools/regenThumbs.js new file mode 100644 index 0000000..4be3fcf --- /dev/null +++ b/tools/regenThumbs.js @@ -0,0 +1,32 @@ +const config = require('../config') +const db = require('../db') +const sharp = require('sharp') + +function regenerateThumbnails() { + // Fetch all summits with photos + db.getDb().collection('summits').find({'photos': {$exists: true, $ne: []}}) + .each((err, summit) => { + if (summit) { + summit.photos.forEach(photo => { + regenerateThumbnailForPhoto(photo) + }) + } else { + db.closeDb() + } + }) +} + +function regenerateThumbnailForPhoto(photo) { + console.dir(photo) + + let sizeDescr = 'thumb' + let originalPath = config.photos.paths.original + '/' + photo.filename.substr(0, 2) + '/' + photo.filename + let outPath = config.photos.paths[sizeDescr] + '/' + photo.filename.substr(0, 2) + '/' + photo.filename + makeResized(originalPath, outPath, config.photos.sizes[sizeDescr].width, config.photos.sizes[sizeDescr].height) +} + +function makeResized(src, dst, maxWidth, maxHeight) { + return sharp(src).rotate().resize({ height: maxHeight, width: maxWidth, fit: 'inside' }).toFile(dst) +} + +db.waitDb(regenerateThumbnails) diff --git a/tools/updateSotaSummits.js b/tools/updateSotaSummits.js new file mode 100644 index 0000000..7cbe6b2 --- /dev/null +++ b/tools/updateSotaSummits.js @@ -0,0 +1,194 @@ +const request = require('request'); +const MongoClient = require('mongodb').MongoClient; +const config = require('../config'); +const assert = require('assert'); +const parse = require('csv-parse'); +const fs = require('fs'); +const removeDiacritics = require('diacritics').remove; + +const client = new MongoClient(config.mongodb.url, {useUnifiedTopology: true}); +client.connect(function (err) { + assert.equal(null, err); + + processSummitList(client.db(config.mongodb.dbName)); +}); + +function processSummitList(db) { + let associations = new Map(); + let now = new Date(); + + let prefixToIsoCode = []; + parse(fs.readFileSync(__dirname + '/isocodes.txt'), function(err, isocodes) { + assert.equal(err, null); + prefixToIsoCode = isocodes; + }); + + request(config.summitListUrl, (error, response, body) => { + assert.equal(error, null); + + body = body.substring(body.indexOf("\n")+1, body.length); + + parse(body, {columns: true, relax_column_count: true}, function(err, summits) { + assert.equal(err, null); + + if (summits.length < 100000) { + console.error("Bad number of summits, expecting more than 100000"); + client.close(); + return; + } + + let bulkWrites = []; + for (let summit of summits) { + summit.SummitCode = summit.SummitCode.trim(); //anomaly GW/NW-003 + summit.ValidFrom = dateToMongo(summit.ValidFrom); + summit.ValidTo = dateToMongo(summit.ValidTo, true); + if (summit.ActivationDate) { + summit.ActivationDate = dateToMongo(summit.ActivationDate); + } else { + summit.ActivationDate = null; + summit.ActivationCall = null; + } + + bulkWrites.push({updateOne: { + filter: {code: summit.SummitCode}, + update: { $set: { + code: summit.SummitCode, + name: summit.SummitName, + nameNd: removeDiacritics(summit.SummitName), + altitude: parseInt(summit.AltM), + points: parseInt(summit.Points), + bonusPoints: parseInt(summit.BonusPoints), + coordinates: { + longitude: Number(parseFloat(summit.Longitude).toFixed(5)), + latitude: Number(parseFloat(summit.Latitude).toFixed(5)) + }, + validFrom: summit.ValidFrom, + validTo: summit.ValidTo, + activationCount: parseInt(summit.ActivationCount), + activationCall: summit.ActivationCall, + activationDate: summit.ActivationDate + }}, + upsert: true + }}); + + if (bulkWrites.length >= config.mongodb.batchSize) { + db.collection('summits').bulkWrite(bulkWrites); + bulkWrites = []; + } + + let SummitAssociation = getAssociation(summit.SummitCode); + let SummitRegion = getRegion(summit.SummitCode); + + let isValid = (summit.ValidFrom <= now && summit.ValidTo >= now); + let association = associations.get(SummitAssociation); + if (!association) { + let info = isoCodeForPrefix(SummitAssociation, prefixToIsoCode) + association = {code: SummitAssociation, name: summit.AssociationName, isoCode: info.isoCode, continent: info.continent, regions: new Map(), summitCount: 0}; + associations.set(SummitAssociation, association); + } + let region = association.regions.get(SummitRegion); + if (!region) { + region = {code: SummitRegion, name: summit.RegionName, summitCount: 0}; + association.regions.set(SummitRegion, region); + } + if (isValid) { + association.summitCount++; + region.summitCount++; + } + + let lat = parseFloat(summit.Latitude); + let lon = parseFloat(summit.Longitude); + + if (!region.bounds) { + region.bounds = [[lon, lat], [lon, lat]]; + } else { + region.bounds[0][0] = Math.min(region.bounds[0][0], lon); + region.bounds[0][1] = Math.min(region.bounds[0][1], lat); + region.bounds[1][0] = Math.max(region.bounds[1][0], lon); + region.bounds[1][1] = Math.max(region.bounds[1][1], lat); + } + + if (!association.bounds) { + association.bounds = [[lon, lat], [lon, lat]]; + } else { + association.bounds[0][0] = Math.min(association.bounds[0][0], lon); + association.bounds[0][1] = Math.min(association.bounds[0][1], lat); + association.bounds[1][0] = Math.max(association.bounds[1][0], lon); + association.bounds[1][1] = Math.max(association.bounds[1][1], lat); + } + } + + if (bulkWrites.length > 0) { + db.collection('summits').bulkWrite(bulkWrites); + } + + for (let association of associations.values()) { + association.regions = [...association.regions.values()]; + } + + let associationCollection = db.collection('associations'); + associationCollection.deleteMany({}, () => { + associationCollection.insertMany([...associations.values()], (err, r) => { + if (err) + console.error(err); + client.close(); + }); + }); + }); + }); +} + +function dateToMongo(date, endOfDay = false) { + let dateRegex = /^(\d\d)\/(\d\d)\/(\d\d\d\d)$/; + let dateRegex2 = /^(\d\d\d\d)-(\d\d)-(\d\d)/; + let matches = dateRegex.exec(date); + let matches2 = dateRegex2.exec(date); + if (matches) { + if (endOfDay) { + return new Date(Date.UTC(matches[3], matches[2]-1, matches[1], 23, 59, 59, 999)); + } else { + return new Date(Date.UTC(matches[3], matches[2]-1, matches[1])); + } + } else if (matches2) { + if (endOfDay) { + return new Date(Date.UTC(matches2[1], matches2[2]-1, matches2[3], 23, 59, 59, 999)); + } else { + return new Date(Date.UTC(matches2[1], matches2[2]-1, matches2[3])); + } + } else { + throw Error("Bad date " + date); + } +} + +let summitRegex = /^(.+)\/(.+)-(\d+)$/; +function getAssociation(summitRef) { + let matches = summitRegex.exec(summitRef); + if (matches) { + return matches[1]; + } else { + throw Error("Bad summit ref '" + summitRef + "'"); + } +} + +function getRegion(summitRef) { + let matches = summitRegex.exec(summitRef); + if (matches) { + return matches[2]; + } else { + throw Error("Bad summit ref '" + summitRef + "'"); + } +} + +function isoCodeForPrefix(prefix, prefixToIsoCode) { + let isoCodeEnt = prefixToIsoCode.find(el => { + if (prefix.startsWith(el[0])) { + return true; + } + }); + if (isoCodeEnt) { + return {isoCode: isoCodeEnt[1], continent: isoCodeEnt[2]}; + } else { + console.error(`ISO code not found for prefix ${prefix}`); + return null; + } +} diff --git a/tools/updateSotatrails.js b/tools/updateSotatrails.js new file mode 100644 index 0000000..132a60a --- /dev/null +++ b/tools/updateSotatrails.js @@ -0,0 +1,41 @@ +const axios = require('axios'); +const MongoClient = require('mongodb').MongoClient; +const config = require('../config'); +const assert = require('assert'); + +const client = new MongoClient(config.mongodb.url, {useUnifiedTopology: true}); +client.connect(function (err) { + assert.equal(null, err); + + updateSotatrails(client.db(config.mongodb.dbName)); +}); + +function updateSotatrails(db) { + axios.get(config.sotatrailsUrl) + .then(response => { + let bulkWrites = []; + response.data.forEach(report => { + let summitCode = report.association + '/' + report.region + '-' + report.code; + bulkWrites.push({updateOne: { + filter: {code: summitCode}, + update: { $set: { + 'resources.sotatrails': { + url: report.url, + details: report.details === 'true' + } + }}, + upsert: false + }}); + }); + + db.collection('summits').bulkWrite(bulkWrites, (err, r) => { + if (err) + console.error(err); + client.close(); + }); + }) + .catch(error => { + console.error(error); + client.close(); + }) +} diff --git a/tracks.js b/tracks.js new file mode 100644 index 0000000..6b8508e --- /dev/null +++ b/tracks.js @@ -0,0 +1,73 @@ +const togeojson = require('togeojson') +const fsPromises = require('fs').promises +const DOMParser = require('xmldom').DOMParser +const simplify = require('@turf/simplify') +const togpx = require('togpx') +const hasha = require('hasha') +const path = require('path') +const config = require('./config') +const db = require('./db') + +module.exports = { + importTrack: async function(filename, author) { + // Hash input file to determine filename + let hash = await hasha.fromFile(filename, {algorithm: 'sha256'}) + let hashFilename = hash.substr(0, 32) + let originalPath = config.tracks.paths.original + '/' + hashFilename.substr(0, 2) + '/' + hashFilename + await fsPromises.mkdir(path.dirname(originalPath), {recursive: true}) + + // Parse first to check if it's valid GPX/KML + let gpxData = await fsPromises.readFile(filename, 'utf-8') + let dom = new DOMParser().parseFromString(gpxData, 'text/xml') + if (!dom) { + throw new Error('Bad XML document') + } + let geojson + if (dom.documentElement.tagName === 'kml') { + geojson = togeojson.kml(dom) + originalPath += '.kml' + } else { + geojson = togeojson.gpx(dom) + originalPath += '.gpx' + } + + if (geojson.type !== 'FeatureCollection') { + throw new Error('Expected feature collection') + } + if (geojson.features.length === 0) { + throw new Error('No features found') + } + + await fsPromises.copyFile(filename, originalPath) + + // Remove times, if present + geojson.features.forEach(feature => { + if (feature.type !== 'Feature') { + throw new Error('Expected feature') + } + + if (feature.properties.coordTimes) { + delete feature.properties.coordTimes + } + }) + + let simplified = simplify(geojson, {tolerance: config.tracks.tolerance, highQuality: true}) + let simpleGpx = togpx(simplified) + + let outPath = config.tracks.paths.simple + '/' + hashFilename.substr(0, 2) + '/' + hashFilename + '.gpx' + await fsPromises.mkdir(path.dirname(outPath), {recursive: true}) + await fsPromises.writeFile(outPath, simpleGpx) + + db.getDb().collection('uploads').insertOne({ + uploadDate: new Date(), + type: 'track', + filename: hashFilename + '.gpx', + author + }) + + return { + filename: hashFilename + '.gpx', + author + } + } +} diff --git a/tracks_router.js b/tracks_router.js new file mode 100644 index 0000000..b856370 --- /dev/null +++ b/tracks_router.js @@ -0,0 +1,42 @@ +const express = require('express') +const multer = require('multer') +const config = require('./config') +const tracks = require('./tracks') +const jwt = require('express-jwt') +const jwksRsa = require('jwks-rsa') + +let upload = multer({dest: config.tracks.uploadPath}) + +let router = express.Router() +module.exports = router + +let jwtCallback = jwt({ + secret: jwksRsa.expressJwtSecret({ + cache: true, + rateLimit: true, + jwksRequestsPerMinute: 5, + jwksUri: config.sso.jwksUri + }) +}) + +router.post('/upload', jwtCallback, upload.single('track'), (req, res) => { + res.cacheControl = { + noCache: true + } + + if (!req.user.callsign) { + res.status(401).send('Missing callsign in SSO token').end() + return + } + + if (req.file) { + tracks.importTrack(req.file.path, req.user.callsign) + .then(track => { + res.json(track) + }) + .catch(err => { + console.error(err) + res.status(500).end() + }) + } +}) diff --git a/utils.js b/utils.js new file mode 100644 index 0000000..e66dc8e --- /dev/null +++ b/utils.js @@ -0,0 +1,17 @@ +module.exports = { + + makeCallsignVariations(callsign) { + let matches = callsign.match(/^(2[DEIJMUW]|G[DIJMUW]?|M[DIJMUW]?)(\d[A-Z]{2,3})$/) + if (matches) { + if (matches[1].substring(0, 1) === '2') { + return ['2D' + matches[2], '2E' + matches[2], '2I' + matches[2], '2J' + matches[2], '2M' + matches[2], '2U' + matches[2], '2W' + matches[2]]; + } else if (matches[1].substring(0, 1) === 'G') { + return ['GD' + matches[2], 'G' + matches[2], 'GI' + matches[2], 'GJ' + matches[2], 'GM' + matches[2], 'GU' + matches[2], 'GW' + matches[2]]; + } else if (matches[1].substring(0, 1) === 'M') { + return ['MD' + matches[2], 'M' + matches[2], 'MI' + matches[2], 'MJ' + matches[2], 'MM' + matches[2], 'MU' + matches[2], 'MW' + matches[2]]; + } + } else { + return [callsign]; + } + } +}; diff --git a/ws-manager.js b/ws-manager.js new file mode 100644 index 0000000..36dded7 --- /dev/null +++ b/ws-manager.js @@ -0,0 +1,82 @@ +const express = require('express'); +const EventEmitter = require('events'); +const keyzipper = require('./keyzipper') + +const PING_INTERVAL = 30000; + +class WebSocketManager extends EventEmitter { + constructor() { + super(); + this.webSocketClients = new Set(); + this.router = express.Router(); + + this.router.ws('/', (ws, req) => { + console.log('WebSocket client connected'); + ws.isAlive = true; + this.webSocketClients.add(ws); + console.log("Number of clients: " + this.webSocketClients.size); + + this.emit('connect', ws); + + ws.on('message', (data) => { + try { + let message = JSON.parse(data); + this.emit('message', ws, message); + } catch (e) {} + }); + ws.on('pong', () => { + ws.isAlive = true; + }); + ws.on('close', () => { + console.log("WebSocket closed"); + clearInterval(ws.pingInterval); + this.webSocketClients.delete(ws); + console.log("Number of clients: " + this.webSocketClients.size); + }); + ws.on('error', (error) => { + console.log("WebSocket error: " + error); + clearInterval(ws.pingInterval); + this.webSocketClients.delete(ws); + console.log("Number of clients: " + this.webSocketClients.size); + }); + + ws.pingInterval = setInterval(() => { + if (!ws.isAlive) { + console.log("WebSocket ping timeout"); + ws.terminate(); + return; + } + ws.isAlive = false; + ws.ping(); + }, PING_INTERVAL); + }); + } + + broadcast(message, filter) { + let str = JSON.stringify(keyzipper.compressKeys(message)); + for (const ws of this.webSocketClients) { + if (filter && !filter(ws)) { + continue; + } + + try { + ws.send(str); + } catch (e) { + console.error(e); + } + } + } + + unicast(message, ws) { + ws.send(JSON.stringify(keyzipper.compressKeys(message))); + } + + numberOfClients() { + return this.webSocketClients.size; + } +} + +let wsManager = new WebSocketManager(); + +// This is a singleton for ease of use +module.exports = wsManager;