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;