-
Notifications
You must be signed in to change notification settings - Fork 1
/
app.js
747 lines (650 loc) · 20.7 KB
/
app.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
/*jshint esversion: 8 */
var siteState;
var siteStateCheck = Date.now()-10000;
//load the additional script collections for the server
var twitterApiExt = require('./twitApiExt.js');
var utilities = require('./utilityFunctions.js')
//load all required packages
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var bodyParser = require('body-parser');
var indexRouter = require('./routes/index');
var exampleRouter = require('./routes/exampleIndex');
var mongoose = require('mongoose');
var request = require('request');
var nodeHTMLParser = require('node-html-parser');
var yaml = require('js-yaml');
var fs = require('fs')
const https = require('https');
const turf = require('@turf/turf');
var app = express();
var configurations = {};
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'pug');
// parse application/x-www-form-urlencoded
// devnote: changed from false to true, blame FELIX if it broke anything.
app.use(bodyParser.urlencoded({ extended: true }));
// parse application/json
app.use(bodyParser.json());
// mongoose setup
mongoose.connect('mongodb://localhost:27017/geomergency', {useNewUrlParser: true, useUnifiedTopology: true}, function(err){
if (err) {
console.log("mongoDB connect failed");
console.log(err);
}
console.log("mongoDB connect succeeded");
console.log(mongoose.connection.host);
console.log(mongoose.connection.port);
});
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
// sets paths to routers
app.use('/', indexRouter);
app.use('/geomergency', indexRouter);
app.use('/geomergency/:coords', indexRouter);
app.use('/example', exampleRouter);
app.use('/example/:coords', exampleRouter);
//loop that checks the site status in the status-api each seconds
setInterval(
async function(){
var statuses = await queryStatuses({
messageType:"siteState",
created_at: { $gt: siteStateCheck}
})
//if the state has changed, update it
if(statuses.length > 0){
if(statuses[statuses.length -1].message != siteState){
siteState = statuses[statuses.length -1].message;
console.log("set new siteState: "+siteState);
//remove the siteStates after checking
rmStatuses({
messageType: "siteState"
// visible: visible
});
updateTweetStream(configurations.tweetParams,
function(tweet){
if(tweet.coordinates != null){
// call getEmbeddedTweet() -> postTweetToMongo()
getEmbeddedTweet(tweet);
}
},
siteState);
}
//set the timestamp of last check to current time - one sec
siteStateCheck = Date.now()-1000;
}
},
2000
);
//initialise tweet stream
// updateTweetStream(configurations.tweetParams,
// function(tweet){
// if(tweet.coordinates != null){
// // call getEmbeddedTweet() -> postTweetToMongo()
// getEmbeddedTweet(tweet);
// }
// },
// "geomergency");
/**
* @function geomergencyRouter
* sets the server internal siteState and returns the router.
* @returns indexRouter as defined in app.js
*/
function geomergencyRouter(){
siteState = "geomergency";
console.log("attempting stream init!!!")
updateTweetStream(
configurations.tweetParams,
function(tweet){
console.log(tweet.id_str)
if(tweet.coordinates != null){
// call getEmbeddedTweet() -> postTweetToMongo()
getEmbeddedTweet(tweet);
}},
"geomergency"
);
return indexRouter;
}
/**
* @function exampleScenarioRouter
* sets the server internal siteState and returns the router.
* @returns exampleRouter as defined in app.js
*/
function exampleScenarioRouter(){
siteState = "example";
updateTweetStream(
configurations.tweetParams,
function(tweet){
return true
},
"example"
);
//return the router
return exampleRouter;
}
/**
* @function updateTweetStream
* @desc initialises the tweet stream with given parameters and current Site-state
* @param params the params of the stream
* @param callback callback function, what to do with the returned tweets. only works when page in standard mode
* @param siteState String that indicates whether or not the site is currently in demo-scenario mode or standard mode
*/
async function updateTweetStream(params, callback, siteState){
//kill the running stream when called
twitterApiExt.killStreamExt();
//initialise a new one
twitterApiExt.tweetStreamExt(params, callback, siteState);
}
app.use("/leaflet", express.static(__dirname + "/node_modules/leaflet/dist"));
app.use("/leafletdraw", express.static(__dirname + '/node_modules/leaflet-draw/dist'));
app.use('/jquery', express.static(__dirname + '/node_modules/jquery/dist'));
app.use('/bootstrap', express.static(__dirname + '/node_modules/bootstrap/dist'));
app.use('/popper.js', express.static(__dirname + '/node_modules/popper.js/dist'))
app.use('/turf', express.static(__dirname + '/node_modules/@turf/turf'))
// use scripts, styles in webserver
app.use("/stylesheetpug", express.static(__dirname + '/public/stylesheets/style.css'));
app.use("/leafletscript", express.static(__dirname + '/public/javascripts/leaflet.js'));
app.use("/siteScripts", express.static(__dirname+'/public/javascripts/siteScripts.js'));
app.use('/gemeinden', express.static(__dirname + '/public/jsons/landkreiseSimp.json'));
// mongoDB models:
var Kreis = require("./models/kreis");
var UnwetterKreis = require("./models/unwetterkreis");
const Status = require("./models/status.js");
const Tweet = require('./models/tweet.js');
var weatherRouter = require("./routes/badweather");
app.use('/weather', weatherRouter);
var rRouter = require("./routes/r");
app.use('/r', rRouter);
var radarRouter = require("./routes/radar");
app.use('/radar', radarRouter);
var summaryRouter = require("./routes/summary");
app.use('/summary', summaryRouter);
/**
* @function queryTweets
* @param queries, Object of mongoose queries
* @return mongoose docs
*/
async function queryTweets(queries){
let output = await Tweet.find(
queries,
{__v:0, _id:0},
function(err,docs){
if(err){
console.log("~~~~~! error in mongoDB query !~~~~~");
console.log(error);
throw error;
} else {
return docs;
}
}
);
return output
}
//set the configutrations
configurations = utilities.loadConfigs(__dirname+'/config.yml');
////////////////////////////////////////////////////////////////////////////////
//Tweet api
////////////////////////////////////////////////////////////////////////////////
//~~~~~~~API-endpoints~~~~~~~
//public DB search API
app.get('/tweets', async (req, res) => {
res.send(await tweetSearch(req, res));
});
app.delete('/tweets', async (req, res) => {
await tweetDelete(req,res)
});
/**
* @function tweetDelete middleware function
* @desc function that is being called when a delete tweet call is made to the tweets endpoint
* param: id_str the id of the tweet(s) to delete
*/
async function tweetDelete(req,res){
if(!req.query.id_str){
await Tweet.deleteMany(
{},
function(err){
if(err){
output = false;
} else {
output = true;
}
}
);
return true
}
//get the id params
let ids = req.query.id_str;
ids = ids.split(",");
//delete each id
for(let id of ids){
console.log({
'id_str': id
})
await Tweet.remove({
'id_str': id
}, function(error){
if(error){
res.status(500)
res.send(`error in deleting tweet ${id}: ${error}`)
}
});
}
res.status(200)
res.send(`tweets deleted from cache`)
}
/**
* @function tweetSearch middleware function
* @desc callback function that looks at the arguments passed in the tweet API request and returns the according response.
* example http://localhost:3000/tweets?fields=id,text
* params: bbox: The bounding Box of the geographical area to fetch tweets from
* include: The strings that are to be included in the returned tweets
* exclude: The strings that aren't to be included in the returned tweets
* fields: The fields of the tweets that are to be returned
* latest: whether or not to only show the latest tweet
* @param req
* @param res
*/
async function tweetSearch(req,res){
let outJSON = {tweets : []};
let newOutJSON = {tweets : []};
const geoJSONtemplate = {"type": "FeatureCollection","features": [{"type": "Feature","properties": {},"geometry": {"type": "","coordinates": [[]]}}]};
//access the provided parameters
var older_than = req.query.older_than;
var bbox = req.query.bbox;
var include = req.query.include;
var exclude = req.query.exclude;
var fields = req.query.fields;
var latest = req.query.latest;
//array-ize include and exclude
if(include != undefined){
include = include.split(",")
}
if(exclude!=undefined){
exclude = exclude.split(",")
}
//check validity of parameters
if (bbox == undefined){
res.status(400),
res.send("bbox is not defined")
}
//QUERY BoundingBox
//create boundingBox geojson from given parameters
try {
bbox = bbox.split(",");
}catch(err){
//check
res.status(400)
res.send("error in bbox parameter<hr>"+err)
}
//numberify the strings
for(let i = 0; i < bbox.length; i++){
bbox[i] = parseFloat(bbox[i]);
//return error when bbox coord was not given a number
if(isNaN(bbox[i])){
res.status(400);
res.send("bbox parameter "+i+" is not a number <hr>");
}
}
//check validity of bbox
if(bbox.length != 4){
res.status(400)
res.send("invalid parameter for bbox")
}
//check validity of bbox coordinates
if(!(
(bbox[0]>bbox[2])&& //north to be more north than south
(bbox[1]<bbox[3])&& //west to be less east than east
bbox[0]<=85 && bbox[0]>=-85&& //north and south in range of 85 to -85 degrees
bbox[2]<=85 && bbox[2]>=-85&&
bbox[1]<=180 && bbox[1]>=-180&& //east and west in range of 180 to -180 degrees
bbox[3]<=180 && bbox[3]>=-180
)){
res.status(400)
res.send("bbox coordinates are not geographically valid")
};
//use the filters on the data
try{
//QUERY older_than
//if no or incorrect time data is given, set to unix timestamp 0
if (older_than == undefined || isNaN(older_than)){
older_than = 0;
}
//call to function that will look for tweets on TweetDB within bounding box.
//outJSON.tweets = await getTweetsInRect(bbox)
outJSON.tweets = await queryTweets({
'geojson.geometry.coordinates': {
$geoWithin: {
$box : [
[bbox[1],bbox[2]], //West-Sount
[bbox[3],bbox[0]] //East-North
]
}
},
created_at: {$gt: older_than}
});
//QUERY include
if(include != undefined){
//loop through each substring that has to be included
for(let i = 0; i < include.length; i++){
//check for substrings existence in each tweet
for(let tweet of outJSON.tweets){
if(
tweet.text.includes(include[i])
// ||
// tweet.text.toLowerCase().includes(include[i].toLowerCase())
){
//lastly, make sure the tweet hasn't already been matched by previous substrings to prevent duplicates
/**
* @function containsPreviousSubstring
* @desc helping function that checks whether a previous substring is contained within the examined tweet
* only works within tweetSearch.
* @see tweetSearch
* @returns boolean
*/
let containsPreviousSubstring = function(){
for(let j=0;j<i;j++){
if(
tweet.text.includes(include[j])
// ||
// tweet.text.toLowerCase().includes(include[j].toLowerCase())
){
return true;}
else {
return false;
}
}
};
//still making sure the tweet hasn't been matched with previous substrings...
if(i==0){newOutJSON.tweets.push(tweet);
}else if(!containsPreviousSubstring()){
newOutJSON.tweets.push(tweet);
}
}
}
}
//make newOutJSON the new outJSON, reset the former
outJSON = newOutJSON;
newOutJSON = {"tweets":[]};
}
//QUERY exclude
if(exclude != undefined && outJSON.tweets != undefined){
//loop through each substring and make sure they're in none of the tweets
for(let substring of exclude){
// exclude = exclude.match(/(["'])(?:(?=(\\?))\2.)*?\1/g);
for(let i= outJSON.tweets.length-1; i >= 0; i--){
//console.log(outJSON.tweets[i].text)
if(
outJSON.tweets[i].text.includes(substring)
// ||
// tweet.text.toLowerCase().includes(include[j].toLowerCase())
){
outJSON.tweets.splice(i,1);
}
}
}
}
//QUERY latest
//if latest is requested, return only latest tweet meeting given parameters
if(latest != undefined){
if(latest.toUpperCase() === "TRUE"){
//in the beginning was Jan 01 1970
let latestTime = new Date("Thu Jan 01 00:00:00 +0000 1970");
for(let tweet of outJSON.tweets){
//if there is a younger one than the previous, make that the new latest
if(new Date(tweet.created_at) > latestTime){
latestTime = tweet.created_at;
newOutJSON.tweets = [];
newOutJSON.tweets.push(tweet);
}
}
//make newOutJSON the new outJSON, reset the former
outJSON = newOutJSON;
newOutJSON = {"tweets":[]};
}
}
//QUERY fields
//if field params are passed, return requested fields only
if(fields != undefined){
fields = fields.split(",");
//check if requested fields exist
for (let field of fields){
if(!(
field == "geojson" ||
field == "_id" ||
field == "id_str" ||
field == "text" ||
field == "created_at"
)){
res.status(400)
res.send("requested field "+field+" does not exist")
}
}
let fieldtweets = {"tweets" : []};
//traverse every tweet in the given list
for (let entry of outJSON.tweets){
//for every tweet, pick only the fields that are specified
let tweet = {};
for (let field of fields){
tweet[field] = entry[field];
}
fieldtweets.tweets.push(tweet);
}
outJSON = fieldtweets;
}
}catch(error){
res.status(500)
res.send(`filter of tweet data failed <br> ${error}`)
}
return outJSON;
}
module.exports = app;
/**
* @function postTweetToMongo
* @param tweet the tweet object
* @param includes array containing strings that have to be contained in tweets
* @param excludes array containing strings that mustn't be in tweets
*/
function postTweetToMongo(tweet){
console.log(`posting ${tweet.text}`)
//initialise embeddedTweet as false
var embeddedTweet = false;
//get the plain text of the tweet
//it needs to be parsed from the html because twitter API doesn't always return full text
var plaintext = nodeHTMLParser.parse(tweet.embeddedTweet);
plaintext = plaintext.firstChild.text;
Tweet.create({
id_str : tweet.id_str,
text : plaintext,
created_at : Date.parse(tweet.created_at),
embeddedTweet : tweet.embeddedTweet,
geojson: {
type: "Feature",
properties: {
},
geometry: {
type : "Point",
coordinates : [tweet.coordinates.coordinates[0], tweet.coordinates.coordinates[1]]
}
}
},
function(err, tweet){
if(err){
console.log("error in saving tweet to DB");
console.log(err);
return false;
}
});
//indicate status
utilities.indicateStatus(`fetched tweet: ${tweet.id_str}`);
}
/**
* @function getEmbeddedTweet
* @desc sends a request to the twitter Oembed API to get an embedded tweet https://developer.twitter.com/en/docs/tweets/post-and-engage/api-reference/get-statuses-oembed
* then calls postTweetToMongo in order to add the tweet to the database
* @param tweet the tweet-object
*/
async function getEmbeddedTweet(tweet){
var output;
var requestURL = "http://publish.twitter.com/oembed?url=https://twitter.com/t/status/";
//let requestURL = "https://localhost:3000/embedTweet?id="
requestURL = requestURL.concat(tweet.id_str);
var requestSettings = {
uri: requestURL,
method: 'GET',
encoding: null,
};
await request(requestSettings, function(error, response, body){
if(error){console.log(error);}
else{
tweet.embeddedTweet = JSON.parse(body).html;
postTweetToMongo(tweet);
}
});
}
////////////////////////////////////////////////////////////////////////////////
//status-indication api
////////////////////////////////////////////////////////////////////////////////
//endpoints
app.get('/statuses', async (req,res)=> {
res.send(await getProcesses(req, res));
});
app.post('/statuses', async (req,res)=> {
postProcesses(req, res);
});
//functions
/**
* @function getProcesses middleware function for getting Processes
* @desc middleware function that looks for running processes
* @param req
* @param res
*/
async function getProcesses(req,res){
let outJSON = {messages: []};
//access provided parameters
var older_than = req.query.older_than;
var remove = req.query.remove;
var messageType = req.query.messageType;
// var visible = req.query.visible;
//if param empty, assign default values
if(remove == undefined){
remove = true;
}
if(older_than == undefined || older_than == ""){
older_than = 0;
}
if(!(messageType == undefined || messageType == "")){
messageType = messageType;
} else {messageType = "processIndication"}
//check validity of parameters and convert them from strings
try{
if(!(older_than == undefined || older_than == "")){
older_than = parseInt(older_than);
}
if(typeof remove != "boolean"){
if(remove.toLowerCase() == "true" || remove.toLowerCase() == "false"){
remove = (remove.toLowerCase() == "true");
}
}
}catch(err){
res.status(400);
res.send(`invalid parameters older_than: ${older_than}, remove: ${remove}`);
}
//get the Status messages
outJSON.messages = await queryStatuses({
created_at: {$gt: older_than},
messageType: messageType
}, res);
//remove the status messages from DB if specified
if(remove){
rmStatuses({
created_at: {$gt: older_than},
messageType: messageType
// visible: visible
});
}
//return the status messages
return outJSON.messages;
}
/**
* @function queryStatuses
* @desc function that queries the mongo status collection
* @param queries, Object of mongoose queries
* @param res, express response for error handling
* @return mongoose docs
*/
async function queryStatuses(queries, res){
let output;
//look for statuses with the given parameters
output = await Status.find(
queries,
//exclude visible, messageType, __V and _id
{
__v:0,
_id:0,
messageType:0
}
);
return output;
}
/**
* @function rmStatuses
* @desc function that removes messages from the mongo status collection
* @param queries, Object of mongoose queries
* @param res, express response for error handling
* @return true or false
*/
async function rmStatuses(queries){
let output = false;
await Status.deleteMany(
queries,
function(err){
if(err){
output = false;
} else {
output = true;
}
}
);
return output;
}
/**
* @function postProcesses middleware function for posting Processes
* @desc middleware function that takes the attributes in a function body (x-www-form-urlencoded)
* @param req
* @param res
*/
async function postProcesses(req,res){
var created_at = req.body.created_at;
var message = req.body.message;
var messageType;
if(req.body.messageType != undefined){
messageType = req.body.messageType;
} else {messageType = "processIndication"}
//check if all attributes are there
if(message == undefined || message == "" || created_at == undefined || created_at == ""){
res.status(400);
res.send(`invalid attributes: created_at: ${created_at}, message: ${message}`);
} else {
//add the status to the designated Mongo collection
Status.create({
created_at : created_at,
message : message,
// visible : visible,
messageType : messageType
},
function(err, tweet){
if(err){
// res.status(400).send("error in posting status: ", err);
}
});
//if it went well, tell them
res.status(200).send(`Status successfully posted`);
}
}