diff --git a/app/views/deleteNodeForm.html b/app/views/deleteNodeForm.html
index 9cf734a..f422af1 100644
--- a/app/views/deleteNodeForm.html
+++ b/app/views/deleteNodeForm.html
@@ -30,8 +30,8 @@
Die Daten Deines Freifunk-Knotens sind gelöscht worden. Es kann jetzt noch bis zu 20 Minuten dauern,
- bis die Änderungen überall wirksam werden und sich im Knotengraph
- und in der Knotenkarte auswirken.
+ bis die Änderungen überall wirksam werden und sich in der
+ Knotenkarte auswirken.
diff --git a/app/views/newNodeForm.html b/app/views/newNodeForm.html
index cfb4013..c82af5c 100644
--- a/app/views/newNodeForm.html
+++ b/app/views/newNodeForm.html
@@ -17,8 +17,7 @@
Geschafft!
Dein Freifunk-Knoten ist erfolgreich angemeldet worden. Es kann jetzt noch bis zu 20 Minuten dauern, bis
- Dein Knoten funktioniert und im Knotengraph
- und in der Knotenkarte auftaucht.
+ Dein Knoten funktioniert und in der Knotenkarte auftaucht.
diff --git a/app/views/updateNodeForm.html b/app/views/updateNodeForm.html
index f4e1e6b..1ca5fdd 100644
--- a/app/views/updateNodeForm.html
+++ b/app/views/updateNodeForm.html
@@ -30,8 +30,8 @@
Geschafft!
Die Daten Deines Freifunk-Knotens sind erfolgreich geändert worden. Es kann jetzt noch bis zu 20 Minuten dauern,
- bis die Änderungen überall wirksam werden und sich im Knotengraph
- und in der Knotenkarte auswirken.
+ bis die Änderungen überall wirksam werden und sich in der
+ Knotenkarte auswirken.
diff --git a/config.json.example b/config.json.example
index 86cc6a3..9a87c81 100644
--- a/config.json.example
+++ b/config.json.example
@@ -18,6 +18,10 @@
"pass": "pass"
}
}
+ },
+
+ "map": {
+ "nodesJsonUrl": "https://map.hamburg.freifunk.net/nodes.json"
}
},
"client": {
@@ -27,8 +31,7 @@
"contactEmail": "kontakt@hamburg.freifunk.net"
},
"map": {
- "graphUrl": "http://graph.hamburg.freifunk.net/graph.html",
- "mapUrl": "http://graph.hamburg.freifunk.net/geomap.html"
+ "mapUrl": "http://map.hamburg.freifunk.net"
},
"coordsSelector": {
"lat": 53.565278,
diff --git a/package.json b/package.json
index 546532c..f9f14ae 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
"node-cron": "~1.1.1",
"nodemailer": "~2.4.1",
"nodemailer-html-to-text": "~2.1.0",
+ "request": "~2.72.0",
"serve-static": "~1.10.2",
"sqlite3": "~3.1.4",
"time-grunt": "~1.3.0"
diff --git a/server/config.js b/server/config.js
index b75e99b..b40b95f 100644
--- a/server/config.js
+++ b/server/config.js
@@ -24,6 +24,10 @@ var defaultConfig = {
pass: 'pass'
}
}
+ },
+
+ map: {
+ nodesJsonUrl: 'http://map.musterstadt.freifunk.net/nodes.json'
}
},
client: {
@@ -33,8 +37,7 @@ var defaultConfig = {
contactEmail: 'kontakt@musterstadt.freifunk.net'
},
map: {
- graphUrl: 'http://graph.musterstadt.freifunk.net/graph.html',
- mapUrl: 'http://graph.musterstadt.freifunk.net/geomap.html'
+ mapUrl: 'http://map.musterstadt.freifunk.net'
},
monitoring: {
enabled: true
diff --git a/server/db/patches/002_add-node-status-table.sql b/server/db/patches/002_add-node-status-table.sql
new file mode 100644
index 0000000..970e562
--- /dev/null
+++ b/server/db/patches/002_add-node-status-table.sql
@@ -0,0 +1,15 @@
+CREATE TABLE node_state (
+ id INTEGER PRIMARY KEY,
+
+ mac VARCHAR(17) NOT NULL UNIQUE,
+
+ state VARCHAR(10) NOT NULL,
+ last_seen DATETIME NOT NULL,
+
+ import_timestamp DATETIME NOT NULL,
+
+ last_status_mail_send DATETIME,
+
+ created_at DATETIME DEFAULT (strftime('%s','now')) NOT NULL,
+ modified_at DATETIME DEFAULT (strftime('%s','now')) NOT NULL
+);
diff --git a/server/jobs/mailQueueJob.js b/server/jobs/mailQueueJob.js
index 39878cb..2846ba0 100644
--- a/server/jobs/mailQueueJob.js
+++ b/server/jobs/mailQueueJob.js
@@ -1,6 +1,6 @@
'use strict';
-angular.module('ffffng').factory('MailQueueJob', function (Database, MailService) {
+angular.module('ffffng').factory('MailQueueJob', function (MailService) {
return {
run: function () {
MailService.sendPendingMails(function (err) {
diff --git a/server/jobs/nodeInformationRetrievalJob.js b/server/jobs/nodeInformationRetrievalJob.js
new file mode 100644
index 0000000..1cbe9e9
--- /dev/null
+++ b/server/jobs/nodeInformationRetrievalJob.js
@@ -0,0 +1,13 @@
+'use strict';
+
+angular.module('ffffng').factory('NodeInformationRetrievalJob', function (MonitoringService) {
+ return {
+ run: function () {
+ MonitoringService.retrieveNodeInformation(function (err) {
+ if (err) {
+ console.error(err);
+ }
+ });
+ }
+ };
+});
diff --git a/server/jobs/scheduler.js b/server/jobs/scheduler.js
index 5a72515..24da90c 100644
--- a/server/jobs/scheduler.js
+++ b/server/jobs/scheduler.js
@@ -24,6 +24,9 @@ angular.module('ffffng').factory('Scheduler', function ($injector) {
return {
init: function () {
schedule('*/5 * * * * *', 'MailQueueJob');
+ // schedule('0 */1 * * * *', 'NodeInformationRetrievalJob');
+ schedule('*/10 * * * * *', 'NodeInformationRetrievalJob');
+ // schedule('0 */1 * * * *', 'NodeInformationCleanupJob');
}
};
});
diff --git a/server/libs.js b/server/libs.js
index 9c3b047..2889df8 100644
--- a/server/libs.js
+++ b/server/libs.js
@@ -20,4 +20,5 @@
lib('fs');
lib('glob');
lib('moment');
+ lib('request');
})();
diff --git a/server/services/monitoringService.js b/server/services/monitoringService.js
index 3cd8010..21c1dc1 100644
--- a/server/services/monitoringService.js
+++ b/server/services/monitoringService.js
@@ -1,8 +1,169 @@
'use strict';
angular.module('ffffng')
-.service('MonitoringService', function (NodeService, ErrorTypes) {
- return {
+.service('MonitoringService', function (
+ _,
+ async,
+ config,
+ Database,
+ ErrorTypes,
+ moment,
+ NodeService,
+ request,
+ Strings,
+ Validator,
+ Constraints
+) {
+ var previousImportTimestamp = null;
+
+ function insertNodeInformation(nodeData, node, callback) {
+ return Database.run(
+ 'INSERT INTO node_state ' +
+ '(mac, state, last_seen, import_timestamp, last_status_mail_send) ' +
+ 'VALUES (?, ?, ?, ?, ?)',
+ [
+ node.mac,
+ nodeData.state,
+ nodeData.lastSeen.unix(),
+ nodeData.importTimestamp.unix(),
+ null // new node so we haven't send a mail yet
+ ],
+ callback
+ );
+ }
+
+ function updateNodeInformation(nodeData, node, row, callback) {
+ debugger;
+ if (!moment(row.import_timestamp).isBefore(nodeData.importTimestamp)) {
+ return callback();
+ }
+
+ return Database.run(
+ 'UPDATE node_state ' +
+ 'WHERE id = ? AND mac = ?' +
+ 'SET state = ?, last_seen = ?, import_timestamp = ?, modified_at = ?',
+ [
+ row.id, node.mac,
+ nodeData.state, nodeData.lastSeen.unix(), nodeData.importTimestamp.unix(), moment.unix()
+ ],
+ callback
+ );
+ }
+
+ function deleteNodeInformation(nodeData, node, callback) {
+ return Database.run(
+ 'DELETE FROM node_state WHERE mac = ? AND import_timestamp < ?',
+ [node.mac, nodeData.importTimestamp.unix()],
+ callback
+ );
+ }
+
+ function storeNodeInformation(nodeData, node, callback) {
+ if (node.monitoring && node.monitoringConfirmed) {
+ return Database.get('SELECT * FROM node_state WHERE mac = ?', [node.mac], function (err, row) {
+ if (err) {
+ return callback(err);
+ }
+
+ if (_.isUndefined(row)) {
+ return insertNodeInformation(nodeData, node, callback);
+ } else {
+ return updateNodeInformation(nodeData, node, row, callback);
+ }
+ });
+ } else {
+ return deleteNodeInformation(node, callback);
+ }
+ }
+
+ var isValidMac = Validator.forConstraint(Constraints.node.mac);
+
+ function parseNodesJson(body, callback) {
+ function parseTimestamp(timestamp) {
+ if (!_.isString(json.timestamp)) {
+ return moment.invalid();
+ }
+ return moment(timestamp);
+ }
+
+ var data = {};
+ try {
+ var json = JSON.parse(body);
+
+ if (json.version !== 1) {
+ return callback(new Error('Unexpected nodes.json version: ' + json.version));
+ }
+
+ data.importTimestamp = parseTimestamp(json.timestamp);
+ if (!data.importTimestamp.isValid()) {
+ return callback(new Error('Invalid timestamp: ' + json.timestamp));
+ }
+
+ if (!_.isPlainObject(json.nodes)) {
+ return callback(new Error('Invalid nodes object type: ' + (typeof json.nodes)));
+ }
+
+ data.nodes = _.values(_.map(json.nodes, function (nodeData, nodeId) {
+ if (!_.isPlainObject(nodeData)) {
+ throw new Error(
+ 'Node ' + nodeId + ': Unexpected node type: ' + (typeof nodeData)
+ );
+ }
+
+ if (!_.isPlainObject(nodeData.nodeinfo)) {
+ throw new Error(
+ 'Node ' + nodeId + ': Unexpected nodeinfo type: ' + (typeof nodeData.nodeinfo)
+ );
+ }
+ if (!_.isPlainObject(nodeData.nodeinfo.network)) {
+ throw new Error(
+ 'Node ' + nodeId + ': Unexpected nodeinfo.network type: ' +
+ (typeof nodeData.nodeinfo.network)
+ );
+ }
+
+ if (!isValidMac(nodeData.nodeinfo.network.mac)) {
+ throw new Error(
+ 'Node ' + nodeId + ': Invalid MAC: ' + nodeData.nodeinfo.network.mac
+ );
+ }
+ var mac = Strings.normalizeMac(nodeData.nodeinfo.network.mac);
+
+ if (!_.isPlainObject(nodeData.flags)) {
+ throw new Error(
+ 'Node ' + nodeId + ': Unexpected flags type: ' + (typeof nodeData.flags)
+ );
+ }
+ if (!_.isBoolean(nodeData.flags.online)) {
+ throw new Error(
+ 'Node ' + nodeId + ': Unexpected flags.online type: ' + (typeof nodeData.flags.online)
+ );
+ }
+ var isOnline = nodeData.flags.online;
+
+ var lastSeen = parseTimestamp(nodeData.lastseen);
+ if (!lastSeen.isValid()) {
+ throw new Error(
+ 'Node ' + nodeId + ': Invalid lastseen timestamp: ' + nodeData.lastseen
+ );
+ }
+
+ return {
+ mac: mac,
+ importTimestamp: data.importTimestamp,
+ state: isOnline ? 'ONLINE' : 'OFFLINE',
+ lastSeen: lastSeen
+ };
+ }));
+ }
+ catch (error) {
+ return callback(error);
+ }
+
+ callback(null, data);
+ }
+
+ return {
confirm: function (token, callback) {
NodeService.getNodeDataByMonitoringToken(token, function (err, node, nodeSecrets) {
if (err) {
@@ -48,6 +209,44 @@ angular.module('ffffng')
callback(null, node);
});
});
+ },
+
+ retrieveNodeInformation: function (callback) {
+ console.info();
+ request(config.server.map.nodesJsonUrl, function (err, response, body) {
+ if (err) {
+ return callback(err);
+ }
+
+ parseNodesJson(body, function (err, data) {
+ if (err) {
+ return callback(err);
+ }
+
+ if (previousImportTimestamp !== null && !data.importTimestamp.isAfter(previousImportTimestamp)) {
+ return callback();
+ }
+ previousImportTimestamp = data.importTimestamp;
+
+ async.each(
+ data.nodes,
+ function (nodeData, nodeCallback) {
+ NodeService.findNodeDataByMac(nodeData.mac, function (err, node) {
+ if (err) {
+ return nodeCallback(err);
+ }
+
+ if (!node) {
+ return nodeCallback(null);
+ }
+
+ storeNodeInformation(nodeData, node, nodeCallback);
+ });
+ },
+ callback
+ );
+ });
+ });
}
};
});
diff --git a/server/services/nodeService.js b/server/services/nodeService.js
index 09493e5..afa7af3 100644
--- a/server/services/nodeService.js
+++ b/server/services/nodeService.js
@@ -224,17 +224,31 @@ angular.module('ffffng')
callback(null, node, nodeSecrets);
}
- function getNodeDataByFilePattern(filter, callback) {
+ function findNodeDataByFilePattern(filter, callback) {
var files = findNodeFiles(filter);
if (files.length !== 1) {
- return callback({data: 'Node not found.', type: ErrorTypes.notFound});
+ return callback(null);
}
var file = files[0];
return parseNodeFile(file, callback);
}
+ function getNodeDataByFilePattern(filter, callback) {
+ findNodeDataByFilePattern(filter, function (err, node, nodeSecrets) {
+ if (err) {
+ return callback(err);
+ }
+
+ if (!node) {
+ return callback({data: 'Node not found.', type: ErrorTypes.notFound});
+ }
+
+ callback(null, node, nodeSecrets);
+ });
+ }
+
function sendMonitoringConfirmationMail(node, nodeSecrets, callback) {
var confirmUrl = UrlBuilder.monitoringConfirmUrl(nodeSecrets);
var disableUrl = UrlBuilder.monitoringDisableUrl(nodeSecrets);
@@ -351,6 +365,10 @@ angular.module('ffffng')
deleteNodeFile(token, callback);
},
+ findNodeDataByMac: function (mac, callback) {
+ return findNodeDataByFilePattern({ mac: mac }, callback);
+ },
+
getNodeDataByToken: function (token, callback) {
return getNodeDataByFilePattern({ token: token }, callback);
},
diff --git a/server/validation/validator.js b/server/validation/validator.js
index 287d7bd..ad814b6 100644
--- a/server/validation/validator.js
+++ b/server/validation/validator.js
@@ -6,6 +6,10 @@ angular.module('ffffng').factory('Validator', function (_) {
return acceptUndefined || constraint.optional;
}
+ if (!_.isString(value)) {
+ return false;
+ }
+
var trimmed = value.trim();
return (trimmed === '' && constraint.optional) || trimmed.match(constraint.regex);
};