-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathserver.js
4043 lines (3772 loc) · 161 KB
/
server.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
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
var express = require('express');
var path = require('path');
var bodyParser = require('body-parser');
var session = require('cookie-session');
var bcrypt = require('bcryptjs');
var MongoClient = require('mongodb').MongoClient;
var ObjectId = require('mongodb').ObjectId;
var axios = require('axios');
var enforce = require('express-sslify');
var RSS = require('rss');
var fs = require("fs");
var pool = require('./public/pool.js');
var schlaunquer = require('./public/schlaunquer.js');
var adminB = require('./public/adminB.js');
var BSON = require('bson').BSON;
var randomBytes = require('crypto').randomBytes;
//connect mongoDB
var dbURI = process.env.MONGO_DB_KEY || 'mongodb://mongo:27017/schlaugh';
var db = new MongoClient(dbURI).db("heroku_kr76r150");
// Init App
var app = express();
// Load View Engine
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
// Body Parser Middleware
app.use(bodyParser.urlencoded({ limit: '5mb', extended: true }));
app.use(bodyParser.json({limit: '5mb'}));
// Set Public Folder
app.use(express.static(path.join(__dirname, 'public')));
var sessionKey = ['SECRETSECRETIVEGOTTASECRET'];
if (process.env.COOKIE_SESSION_KEY) {
sessionKey = [process.env.COOKIE_SESSION_KEY];
}
// Configure cookie-session middleware
app.use(session({
name: 'session',
keys: sessionKey,
maxAge: 90 * 24 * 60 * 60 * 1000, // (90 days?)
}))
var devFlag = false; //NEVER EVER LET THIS BE TRUE ON THE LIVE PRODUCTION VERSION, FOR LOCAL TESTING ONLY
// enforce https, "trustProtoHeader" is because heroku proxy
// hack to make it not enforce https on localDev
if (process.env.MONGO_DB_KEY) {
app.use(enforce.HTTPS({ trustProtoHeader: true }));
} else {
devFlag = true; // on production we'll always have the DB key, so if we don't have the DB key, put it in dev mode
}
// sendgrid email config
var sgMail = require('@sendgrid/mail');
sgMail.setApiKey(process.env.SENDGRID_API_KEY);
//*******//HELPER FUNCTIONS//*******//
var pushNewToPostlist = function (user) { // pushes New pending posts to postlist
// needs user.postListPending, postList, and postListUpdatedOn, does not perform it's own save
var today = pool.getCurDate();
if (user.postListUpdatedOn !== today) {
user.postListUpdatedOn = today;
var plp = user.postListPending;
while (plp[0] && plp[0].date <= today) {
user.postList.push({
date: plp[0].date,
num: plp[0].num,
});
plp.splice(0,1);
}
return true;
} else {
return false;
}
}
var userUpdateCache = {};
var checkForUserUpdates = function (req, res, errMsg, userID, callback) { // pushes edits on OLD posts, BIO, and userPic
var today = pool.getCurDate();
if (!userUpdateCache[today]) { userUpdateCache[today] = {}}
if (userUpdateCache[today][userID]) {
return callback();
} else {
userUpdateCache[today][userID] = true;
}
// first cursory DB fetch to see if updates are needed
var props = ['postListUpdatedOn','pendingUpdates'];
readUser(req, res, errMsg, userID, {list:props}, function (user) {
if (!user) {return callback();}
var updatesNeedsUpdate = false;
var postListNeedsUpdate = false;
// is postListUpdatedOn fresh?
if (user.postListUpdatedOn !== today) {
props.push('postListPending', 'postList');
postListNeedsUpdate = true;
}
// check for 'pendingUpdates'
if (user.pendingUpdates && user.pendingUpdates.updates && user.pendingUpdates.lastUpdatedOn && user.pendingUpdates.lastUpdatedOn !== today) {
updatesNeedsUpdate = true;
var updateList = [];
// so we do need to update something, what exactly? (or maybe we just bump the 'lastUpdatedOn')
var ref = user.pendingUpdates.updates;
var alreadyGotPosts = false;
for (var date in ref) {if (ref.hasOwnProperty(date)) {
updateList.push(date);
if (date === "bio" || date === "iconURI") {
props.push(date);
} else {
// if we have even one post, then we need the whole darn posts object
if (!alreadyGotPosts) {
props.push('posts');
alreadyGotPosts = true;
}
}
}}
}
if (postListNeedsUpdate || updatesNeedsUpdate) {
// fetch the props we need for this user
readUser(req, res, errMsg, userID, {list:props}, function (user) {
if (postListNeedsUpdate) {pushNewToPostlist(user);}
if (updatesNeedsUpdate) {
user.pendingUpdates.lastUpdatedOn = today;
var ref = user.pendingUpdates.updates;
//
var loop = function (i, callback) {
if (!updateList[i]) {return callback(user);}
//
else if (updateList[i] === "bio" || updateList[i] === "iconURI") {
user[updateList[i]] = ref[updateList[i]];
delete ref[updateList[i]];
return loop(i+1, callback);
//
} else if (user.posts[updateList[i]]) { // it's an edit to an old post
// check existing tags
var badTagArr = [];
for (var tag in user.posts[updateList[i]][0].tags) {
if (user.posts[updateList[i]][0].tags.hasOwnProperty(tag)) {
if (!ref[updateList[i]][0].tags[tag]) { // if the old tag is NOT a new tag too
badTagArr.push(tag.toLowerCase());
}
}
}
deleteTagRefs(req, res, errMsg, badTagArr, updateList[i], user._id, function (resp) {
user.posts[resp.date] = ref[resp.date]; // the line where the update actually happens!
delete ref[resp.date];
return loop(i+1, callback);
});
} else { // can't find the thing we're supposed to be editing?
delete ref[updateList[i]];
return loop(i+1, callback);
}
}
loop(0, function (user) {
callback();
writeToUser(req, null, errMsg+'write failure in "checkForUserUpdates"<br>', user);
});
} else {
callback();
writeToUser(req, null, errMsg+'write failure in "checkForUserUpdates"<br>', user);
}
});
} else { // no update needed
return callback();
}
});
}
var checkMultiUsersForUpdates = function (req, res, errMsg, idList, callback) {
var loop = function (i, callback) {
if (!idList[i]) {return callback();}
else {
checkForUserUpdates(req, res, errMsg, idList[i], function () {
return loop(i+1, callback);
});
}
}
loop(0, function () {
callback();
});
}
var imageValidate = function (arr, callback, byteLimit) {
if (!arr) {return callback({error:'"""type error""" on the "imageValidate" probably because staff is a dingus'})}
if (arr.length !== 0) { // does the post contain images?
var count = arr.length;
var byteCount = 104857600; // 100mb(-ish...)
if (byteLimit) {
byteCount = byteLimit;
}
for (var i = 0; i < arr.length; i++) {
(function (index) {
fetchHeaders(arr[index], function (error, resp) {
if (count > 0) {
count -=1;
if (error || resp.status !== 200) {
count = 0;
return callback({error:'the url for image '+(index+1)+' seems to be invalid'});
} else if (!resp.headers['content-type'] || (resp.headers['content-type'].substr(0,5) !== "image" && resp.headers.server !== "AmazonS3")) {
count = 0;
return callback({error:'the url for image '+(index+1)+' is not a url for an image'});
} else {
byteCount -= resp.headers['content-length'];
}
if (count === 0) {
if (byteCount < 0) {
return callback({error:"your image(s) exceed the byte limit by "+(-byteCount)+" bytes"});
} else {return callback({error:false});}
}
}
});
})(i);
} // no images to check
} else {return callback({error:false});}
}
var fetchHeaders = function (url, callback) {
axios.head(url)
.then(response => {
// Calling the callback function with the status code and headers
callback(null, response);
})
.catch(error => {
// If there's an error, calling the callback function with the error
callback(error);
});
}
var linkValidate = function (arr, callback) {
if (!arr || arr.length === 0) {return callback(false);}
var timer = setTimeout(function () {
// this is because of the heroku 30sec response time limit
timer = null;
return callback("schlaugh's link checker is having trouble checking multiple links in your post. So much so that it got tired and gave up. They might be fine, but schlaugh's link checker is stalling when trying to follow them, so please try clicking on all your links yourself to make sure they work");
}, 28000);
var manageLinkValidate = function (i, problems) {
// init
if (!i) {
i = 0;
problems = [];
//
}
var url = arr[i];
// give it 4 seconds to try checking the link
var linkTimer = setTimeout(function () {
// after 4 seconds:
problems.push(url);
linkTimer = null;
wrapUpLinkValidate(i, problems);
}, 4000);
fetchHeaders(url, function (error, resp) {
if (linkTimer !== null) { // otherwise, the timer already went off and this link has already been marked as bad, so do nothing
clearTimeout(linkTimer);
if (error || !resp || !resp.status) {
problems.push(url);
}
//
wrapUpLinkValidate(i, problems);
}
});
}
var wrapUpLinkValidate = function (i, problems) {
i++;
if (i === arr.length) { // done
if (timer === null) {
return;
}
clearTimeout(timer);
if (problems.length === 0) {return callback(false);}
if (problems.length === 1) {
return callback(`your url: <code>`+problems[0]+`</code> does not seem to be valid. It might be fine, but schlaugh's link checker is flagging it, so please try clicking on it yourself to make sure it works<br><br>were you perhaps intending to link to schlaugh itself using the shorter form of link? please note that URL must start with a "/" character`);
} else {
var badUrls = "<br>";
for (var j = 0; j < problems.length; j++) {
badUrls += problems[j]+"<br>"
}
return callback(`your urls: <code>`+badUrls+`</code> do not seem to be valid. They might be fine, but schlaugh's link checker is flagging them, so please try clicking on them yourself to make sure they work<br><br>were you perhaps intending to link to schlaugh itself using the shorter form of link? please note that URL must start with a "/" character`);
}
} else {
return manageLinkValidate(i, problems);
}
}
manageLinkValidate();
}
var getUserPic = function (user) {
var userPic;
if (user) {
userPic = user.iconURI;
}
if (typeof userPic !== 'string') {userPic = "";}
return userPic;
}
var checkObjForProp = function (obj, prop, value) { //and add the prop if it doesn't exist
if (obj[prop]) {return false} //note: returns FALSE to indicate no action taken
else {
obj[prop] = value;
return obj;
}
}
var checkLastTwoMessages = function (t, tmrw, inbound) {
//'inbound' = true to indicate we are seeking inbound messages, false = outbound
if (t && t.length !== undefined) {
var len = t.length;
if (t[len-1] && t[len-1].date === tmrw) {
if (t[len-1].inbound === inbound) {
return len-1;
} else if (t[len-2] && t[len-2].date === tmrw && t[len-2].inbound === inbound) {
return len-2;
}
}
}
//returns false for nothing found, else returns index of hit
return false;
}
var checkLastTwoForPending = function (thread, remove, text, tmrw, inbound) {
// ore either of the last two messages in a thread an extant pending message to overwrite?
var overwrite = function (i) {
if (remove) {thread.splice(i, 1);}
else {thread[i].body = text;}
return thread;
}
var x = checkLastTwoMessages(thread, tmrw, inbound);
//returns false for nothing found, else returns modified thread
if (x !== false) {return overwrite(x);}
else {return false;}
}
var bumpThreadList = function (box) { //bumps pending threads to top of list
var today = pool.getCurDate();
if (box.updatedOn !== today) {
box.updatedOn = today;
// for each name stored in 'pending',
for (var x in box.pending) {
if (box.pending.hasOwnProperty(x)) {
// remove extant refference in the List, if there is one
for (var i = 0; i < box.list.length; i++) {
if (String(box.list[i]) === String(x)) {
box.list.splice(i, 1);
break;
}
}
// push to the top(end) of the stack
box.list.push(x);
// set the newly bumped threads to unread
box.threads[x].unread = true;
}
}
// empty the pending collection
box.pending = {};
return box;
} return false; // false indicates no update needed/performed
}
var removeListRefIfRemovingOnlyMessage = function (box, id, remove, tmrw) {
if (remove && box.threads[id].thread.length < 2) {
if (!box.threads[id].thread[0] || box.threads[id].thread[0].date === tmrw) {
for (var i = box.list.length-1; i > -1 ; i--) {
if (String(box.list[i]) === String(id)) {
box.list.splice(i, 1);
return box.list;
}
}
}
} return false;
}
var newSessionKey = function (userID) {
var key = genRandString(5)
if (sessions && sessions[userID] && sessions[userID][key]) {
return newSessionKey();
} else {
return key;
}
}
var getPayload = function (req, res, callback) {
var errMsg = 'unable to acquire payload<br><br>';
checkForUserUpdates(req, res, errMsg, req.session.user._id, function () {
var propList = ['posts','username', 'iconURI', 'settings', 'inbox', 'keyPrivate', 'keyPublic', 'following', 'muted', 'pendingUpdates', 'bio', 'bookmarks', 'collapsed', 'savedTags', 'games', 'drafts', 'allowIndexing'];
readCurrentUser(req, res, errMsg, {list:propList}, function (user) {
// check if user needs keys
if (!user.keyPrivate || !user.keyPublic) {return res.send({needKeys:true});}
if (!user.bookmarks || user.bookmarks.length === undefined) {user.bookmarks = [];}
if (!user.collapsed || user.collapsed.length === undefined) {user.collapsed = [];}
if (!user.savedTags || user.savedTags.length === undefined) {user.savedTags = [];}
var payload = {
keys: {privKey:user.keyPrivate, pubKey:user.keyPublic },
username: user.username,
userID: req.session.user._id,
settings: user.settings,
following: user.following,
bookmarks: user.bookmarks,
collapsed: user.collapsed,
savedTags: user.savedTags,
muted: user.muted,
games: user.games,
}
// session key set
var sessionKey = newSessionKey(req.session.user._id);
payload.sessionKey = sessionKey;
//pending post
var tmrw = pool.getCurDate(-1);
if (user.posts[tmrw]) {
payload.pending = user.posts[tmrw][0];
} else if (user.drafts && user.drafts[0]) {
payload.pending = user.drafts[0];
payload.pending.draft = true;
}
//
if (user.allowIndexing) {
payload.settings.allowIndexing = true;
}
//
if (user.pendingUpdates && user.pendingUpdates.updates) {
payload.pendingUpdates = user.pendingUpdates.updates;
} else {
payload.pendingUpdates = {};
}
// bio/userPic need to be filled in AFTER we check for updates
payload.userPic = getUserPic(user);
payload.bio = user.bio;
if (typeof payload.bio !== 'string') {payload.bio = "";}
//inbox
if (user.inbox) {
var threads = [];
var bump = bumpThreadList(user.inbox);
if (bump) {
writeToUser(req, null, errMsg, user);
}
var list = user.inbox.list;
//reverse thread order so as to send a list ordered newest to oldest
for (var i = list.length-1; i > -1; i--) {
if (user.inbox.threads[list[i]] && user.inbox.threads[list[i]].thread && user.inbox.threads[list[i]].thread.length !== undefined) {
//check the last two messages of each thread, see if they are allowed
var x = checkLastTwoMessages(user.inbox.threads[list[i]].thread, tmrw, true);
if (x !== false) {user.inbox.threads[list[i]].thread.splice(x, 1);}
// add in the authorID for the FE
user.inbox.threads[list[i]]._id = list[i];
// all threads are locked until unlocked on the FE
user.inbox.threads[list[i]].locked = true;
threads.push(user.inbox.threads[list[i]]);
}
}
payload.threads = threads;
return callback(payload);
} else {
payload.threads = [];
return callback(payload);
}
});
});
}
var getSettings = function (req, res, callback) {
if (!req.session.user) {return callback({user:false, settings:null});}
var errMsg = "unable to access user settings<br><br>";
readCurrentUser(req, res, errMsg, {list:['settings']}, function (user) {
var settings = {};
settings.colors = user.settings.colors;
settings.font = user.settings.font;
return callback({user:true, settings:settings});
});
}
var genRandString = function (length) {
var bank = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
var output = "";
for (var i = 0; i < length; i++) {
output += bank[Math.floor(Math.random() * (bank.length))];
}
return output;
}
var genID = function (req, res, errMsg, clctn, length, callback) {
var output = genRandString(length);
dbReadOneByID(req, res, errMsg, clctn, output, getProjection(['_id']), function (record) {
if (record) {return genID(req, res, errMsg, clctn, length, callback);} // collision! try again
else {return callback(output)}
});
}
var createStat = function (date, callback) {
dbCreateOne(null, null, null, 'stats', {_id:date}, function () {
return callback();
});
}
var incrementStat = function (kind) {
var date = pool.getCurDate();
// does statBucket already exist for day?
dbReadOneByID(null, null, 'error incrementing stat<br><br>', 'stats', date, null, function (stat) {
if (!stat) {
createStat(date, function () {
updateStat(date, kind, 1);
// send a weekly email to myself
var today = new Date();
var dayOfYear = Math.ceil((today - new Date(today.getFullYear(),0,1)) / 86400000);
if (dayOfYear % 7 === 0) {
var msg = {
to: "[email protected]",
from: '[email protected]',
subject: 'day '+dayOfYear,
text: `beep boop email is still working\n\n<3`,
};
sgMail.send(msg, (error) => {
if (error) { sendError(null, null, "email server malapropriationologification"); }
// that's all
});
}
});
} else {
var x = 1;
if (stat[kind]) {x = stat[kind] + 1;}
updateStat(date, kind, x);
}
});
}
var updateStat = function (date, kind, val) {
var obj = {};
obj[kind] = val;
dbWriteByID(null, null, "error updating stat: ", 'stats', date, obj, null);
}
var createPost = function (req, res, errMsg, authorID, callback, date) { //creates a postID and ref in the postDB
if (!ObjectId.isValid(authorID)) {return callback({error:"invalid authorID format"});}
authorID = getObjId(authorID);
if (!date) {date = pool.getCurDate(-1)}
genID(req, res, errMsg, 'posts', 7, function (newID) {
var newPostObject = {
_id: newID,
date: date,
authorID: authorID,
num:0,
}
dbCreateOne(req, res, errMsg, 'posts', newPostObject, function (newID) {
return callback({error:false, post_id:newID});
});
});
}
var nullPostFromPosts = function (req, res, errMsg, postID, callback) {
var emptyPost = {date: null, authorID: null,};
dbWriteByID(req, res, errMsg, 'posts', postID, emptyPost, function () {
callback();
});
}
var lowercaseTagRef = function (ref) {
var newRef = {}
for (var tag in ref) {
if (ref.hasOwnProperty(tag)) {
if (!newRef[tag.toLowerCase()]) {
newRef[tag.toLowerCase()] = true;
}
}
}
return newRef;
}
var updateUserPost = function (req, res, errMsg, text, newTags, title, url, userID, user, callback, daysAgo) {
var tmrw = pool.getCurDate(-1);
if (devFlag && daysAgo) { // CHEATING, for local testing only
var tmrw = pool.getCurDate(daysAgo);
}
// lowercase copy of the newTags
var newTagsLowerCased = lowercaseTagRef(newTags);
//
if (user.posts[tmrw]) { //edit existing
// lowercase the existing tags
var oldTagsLowerCased = lowercaseTagRef(user.posts[tmrw][0].tags);
// check existing tags
var badTagArr = [];
for (var tag in oldTagsLowerCased) {
if (oldTagsLowerCased.hasOwnProperty(tag)) {
if (!newTagsLowerCased[tag]) { // if the old tag is NOT a new tag too
badTagArr.push(tag.toLowerCase());
}
}
}
deleteTagRefs(req, res, errMsg, badTagArr, tmrw, userID, function () {
var newTagArr = [];
for (var tag in newTagsLowerCased) {
if (newTagsLowerCased.hasOwnProperty(tag)) {
if (!oldTagsLowerCased[tag]) { // if the new tag is NOT an old tag too
newTagArr.push(tag.toLowerCase());
}
}
}
createTagRefs(req, res, errMsg, newTagArr, tmrw, userID, function () {
user.posts[tmrw][0].body = text;
user.posts[tmrw][0].tags = newTags;
user.posts[tmrw][0].title = title;
user.posts[tmrw][0].url = url;
return callback();
});
});
} else { //create new
createPost(req, res, errMsg, userID, function (resp) {
user.posts[tmrw] = [{
body: text,
tags: newTags,
title: title,
url: url,
post_id: resp.post_id,
}];
user.postListPending.push({date:tmrw, num:0});
//
var tagArr = [];
for (var tag in newTagsLowerCased) { // add a ref in the tag db for each tag
if (newTagsLowerCased.hasOwnProperty(tag)) {tagArr.push(tag.toLowerCase())}
}
createTagRefs(req, res, errMsg, tagArr, tmrw, userID, function () {
return callback();
});
});
}
}
var deletePost = function (req, res, errMsg, user, date, callback) {
if (!user.posts[date]) {return sendError(req, res, errMsg+" post not found");}
var deadTags = [];
for (var tag in user.posts[date][0].tags) {
if (user.posts[date][0].tags.hasOwnProperty(tag)) {
deadTags.push(tag.toLowerCase());
}
}
deleteTagRefs(req, res, errMsg, deadTags, date, user._id, function () {
if (user.posts[date][0].url) {
delete user.customURLs[user.posts[date][0].url];
}
// is this a pending or an OLD post?
if (date === pool.getCurDate(-1)) {
user.postListPending.pop(); //currently assumes that postListPending only ever contains 1 post
//
dbDeleteByID(req, res, errMsg, 'posts', user.posts[date][0].post_id, function () {
delete user.posts[date];
writeToUser(req, res, errMsg, user, function () {
return callback();
});
});
} else { // for OLD posts
for (var i = 0; i < user.postList.length; i++) {
if (user.postList[i].date === date) {
user.postList.splice(i, 1);
break;
}
}
if (user.pendingUpdates && user.pendingUpdates.updates && user.pendingUpdates.updates[date]) {
delete user.pendingUpdates.updates[date];
}
if (postCache && postCache[date] && postCache[date][user._id]) {
delete postCache[date][user._id];
}
nullPostFromPosts(req, res, errMsg, user.posts[date][0].post_id, function () {
delete user.posts[date];
writeToUser(req, res, errMsg, user, function () {
return callback();
});
});
}
});
}
// **** the "tagsByTag" db is where we store references to posts, indexed by TAG, and in arrays sorted by date, oldest to newest
var multiTagIndexAddOrRemove = function (tagArr, date, authorID, creating, callback) {
var count = tagArr.length;
for (var i = 0; i < tagArr.length; i++) {
tagIndexAddOrRemove(tagArr[i], date, authorID, creating, function (resp) {
if (resp.error && count > 0) {
count = -1;
return callback({error:resp.error});
} else {
count--;
if (count === 0) {
return callback({error:false,});
}
}
})
}
}
var tagIndexAddOrRemove = function (tag, date, authorID, creating, callback) {
tag = tag.toLowerCase();
var postItem = {date:date, authorID:authorID, num:0};
var nextInLine = 0;
if (tagIndexOccupiedSign[tag]) { // if there line, how long it?
nextInLine = tagIndexOccupiedSign[tag].length +1;
}
var lineDecisions = function (resp) {
if (tagIndexOccupiedSign[tag]) { // on way out, if line, then tap person after you
if (tagIndexOccupiedSign[tag][nextInLine]) {
tagIndexOccupiedSign[tag][nextInLine]();
} else { // you are last in line, close the line
delete tagIndexOccupiedSign[tag];
}
}
if (resp.error) {return callback(resp);}
else {return callback({error:false});}
}
var execute = function () {
if (creating) {
createTagIndexItem(tag, postItem, lineDecisions)
} else {
removeTagIndexItem(tag, postItem, lineDecisions)
}
}
if (tagIndexOccupiedSign[tag]) { // if there is a line, then get in line
tagIndexOccupiedSign[tag].push(execute);
} else {
tagIndexOccupiedSign[tag] = [];
execute();
}
}
var tagIndexOccupiedSign = {};
var createTagIndexItem = function (tag, postItem, callback) {
// check if tag listing is extant
dbReadOneByID(null, null, null, 'tagsByTag', tag, null, function (tagListing) {
if (tagListing && tagListing.error) {return callback({error:tagListing.error});}
if (!tagListing) { // tag does not exist, make it
var newTag = {_id: tag};
newTag.list = [postItem];
dbCreateOne(null, null, null, 'tagsByTag', newTag, function (newID) {
if (newID.error) {return callback({error:newID.error});}
else {return callback({error:false});}
});
} else { // tag exists, add to it
tagListing.list.push(postItem);
dbWriteByID(null, null, null, 'tagsByTag', tag, tagListing, function (resp) {
if (resp && resp.error) {return callback({error:resp.error});}
else {return callback({error:false});}
});
}
});
}
var removeTagIndexItem = function (tag, postItem, callback) {
// check if tag listing is extant
dbReadOneByID(null, null, null, 'tagsByTag', tag, null, function (tagListing) {
if (tagListing && tagListing.error) {return callback({error:tagListing.error});}
if (!tagListing) {return callback({error:false});} // tag does not exist, so can't be deleted...but it doesn't exist, so we're good?
// tag exists, find the item to be removed
for (var i = 0; i < tagListing.list.length; i++) {
// this is potentially very slow, cycling through every post ever w/ that tag
// since the posts are sorted, it could search faster, or at the very least just go from the end since most deletions will be recent, i think
if (tagListing.list[i].date === postItem.date && String(tagListing.list[i].authorID) === String(postItem.authorID)) {
tagListing.list.splice(i, 1);
break;
}
}
if (tagListing.list.length > 0) { // write the new array with the item taken out
dbWriteByID(null, null, null, 'tagsByTag', tag, tagListing, function (resp) {
if (resp && resp.error) {return callback({error:resp.error});}
else {return callback({error:false});}
});
} else { // hde array is empty, delete the whole thing
dbDeleteByID(null, null, null, 'tagsByTag', tag, function (resp) {
if (resp && resp.error) {return callback({error:resp.error});}
else {return callback({error:false});}
});
}
});
}
// **** the "tagsByDate" db is where we store references to posts, indexed by DATE, then by tag for each day, then unsorted arrays for each date[tag]
var createTagRefs = function (req, res, errMsg, tagArr, date, authorID, callback) {
if (tagArr.length === 0) {return callback();}
//
if (!ObjectId.isValid(authorID)) {return sendError(req, res, errMsg+"invalid authorID format");}
authorID = getObjId(authorID);
multiTagIndexAddOrRemove(tagArr, date, authorID, true, function (resp) {
if (resp.error) {return sendError(req, res, errMsg+resp.error);}
// check if dateBucket is extant
dbReadOneByID(req, res, errMsg, 'tagsByDate', date, null, function (dateBucket) {
if (!dateBucket) { // dateBucket does not exist, make it
var newDateBucket = {_id: date,};
newDateBucket.ref = {};
for (var i = 0; i < tagArr.length; i++) {
newDateBucket.ref[tagArr[i]] = [authorID];
}
dbCreateOne(req, res, errMsg, 'tagsByDate', newDateBucket, function () {
return callback();
});
} else { // dateBucket exists, add to it
var tagObject = [authorID];
for (var i = 0; i < tagArr.length; i++) {
if (!checkObjForProp(dateBucket.ref, tagArr[i], tagObject)) { // is tag extant?
dateBucket.ref[tagArr[i]].push(authorID);
}
}
dbWriteByID(req, res, errMsg, 'tagsByDate', date, dateBucket, function () {
return callback();
});
}
});
});
}
var deleteTagRefs = function (req, res, errMsg, tagArr, date, authorID, callback) {
if (tagArr.length === 0) {return callback({date:date});}
//
if (!ObjectId.isValid(authorID)) {return sendError(req, res, errMsg+" invalid authorID format");}
authorID = getObjId(authorID);
multiTagIndexAddOrRemove(tagArr, date, authorID, false, function (resp) {
if (resp.error) {return sendError(req, res, errMsg+resp.error);}
dbReadOneByID(req, res, errMsg, 'tagsByDate', date, null, function (dateBucket) {
if (!dateBucket) {return callback({date:date});}
// for each tag to be deleted,
for (var i = 0; i < tagArr.length; i++) {
if (dateBucket.ref[tagArr[i]]) {
var array = dateBucket.ref[tagArr[i]];
for (var j = 0; j < array.length; j++) {
if (String(array[j]) === String(authorID)) {
array.splice(j, 1);
break;
}
}
if (array.length === 0) {
delete dateBucket.ref[tagArr[i]];
}
}
}
dbWriteByID(req, res, errMsg, 'tagsByDate', date, dateBucket, function () {
return callback({date:date});
});
});
});
}
//
var parseInboundTags = function (tagString) {
var tags = {};
tagString = tagString.replace(/[^ a-zA-Z0-9-_!?@&*%:=+`"'~,]/g, '');
var arr = tagString.match(/[ a-zA-Z0-9-_!?@&*%:=+`"'~]+/g);
if (arr) {
for (var i = 0; i < arr.length; i++) {
arr[i] = arr[i].trim();
if (arr[i] === '') {
arr.splice(i,1);
}
}
if (arr.length > 41) {return "this is not actually an 'error', this is just me preventing you from using this many tags. Do you <i>really</i> need this many tags?? Tell staff about this if you have legitimate reason for the limit to be higher. I might have drawn the line too low, i dunno, i had to draw it somewhere.<br><br>beep boop"}
for (var i = 0; i < arr.length; i++) {
if (arr[i].length > 280) {return "this is not actually an 'error', it's just one of your tags is very long and I'm preventing you from submitting it. Do you <i>really</i> need that many characters in one tag?? I mean, maybe. Tell staff if you think there is good reason to nudge the limit higher. I just had to draw the line somewhere, lest someone submit the entire text of <i>Worth the Candle</i> as tags in an attempt to break the site.<br><br>enjoy your breakfast"}
tags[arr[i]] = true;
}
}
return tags;
}
var validatePostTitle = function (string) {
if (string.length > 140) {
return {error: "this is not actually an 'error', this is just me preventing you from making a title this long. Do you really need it to be this long? I mean, maybe. Tell staff if you think there is good reason to nudge the limit higher. I just had to draw the line somewhere, lest someone submit the entire text of <i>Gödel, Escher, Bach</i> as a title in an attempt to break the site.<br><br>have a nice lunch"};
}
string = string.replace(/</g, '<');
string = string.replace(/>/g, '>');
return string;
}
var validatePostURL = function (user, date, string) {
if (!string) {string = "";}
if (!user || !user.posts) {return {error:"url validation: missing user data"};}
if (!date) {return {error:"url validation: missing date"};}
if (user.posts && user.posts[date] && user.posts[date][0].url) {
if (user.posts[date][0].url === string) { // no change, we good
return {user:user, url:string};
} else {
delete user.customURLs[user.posts[date][0].url]
}
}
if (string === "") {
return {user:user, url:string};
}
if (string.length > 60) {
return {error: "this is not actually an 'error', this is just me preventing you from making a url >60 characters. Like, you really want it that long? <i>Short</i> urls are typically the thing people want? I mean, maybe. Tell staff if you think there is good reason to nudge the limit higher. I just had to draw the line somewhere, lest someone submit the entire text of <i>The Power Broker</i> as a url in an attempt to break the site.<br><br>remember to floss!"};
}
string = string.replace(/[^a-zA-Z0-9-_]/g, '');
if (!user.customURLs) {user.customURLs = {}}
if (user.customURLs[string]) {
if (user.customURLs[string].date !== date) {
var errorString = "you have already used this url for another post, if you wish to use it for this post, please go edit that post and unassign it first:<br><a class='special' target='_blank' href='/"+user.username+"/"+string+"'>schlaugh.com/"+user.username+"/"+string+"</a>";
return {deny: errorString};
}
// else, do nothing, the url is taken, for THIS post
} else {
user.customURLs[string] = {date: date}
}
return {user:user, url:string};
}
var sendError = function (req, res, errMsg) {
// req and res are optional. if no req, then no user is associated w/ the error. if no res, then no response to the client about it
// log it
var newErrorLog = {
error: errMsg,
creationTime: new Date(),
}
if (req && req.session && req.session.user && req.session.user._id) {
newErrorLog.user = req.session.user._id;
}
console.log('-----------',errMsg);
dbCreateOne(req, res, null, 'errorLogs', newErrorLog, function () {
// do nothing, don't report an error if reporting the error doesn't work, because loop
// the "null" entry for the errMsg is also so that send Error isn't called again
});
//
if (res) { // so we can NOT notify the FE about an error, but still log it above
errMsg = "ERROR! SORRY! Please screenshot this and note all details of the situation and tell staff. SORRY<br><br>" + errMsg;
if (!res.headersSent) {
res.send({error: errMsg});
}
}
}
var postCache = {}
var postsFromAuthorListAndDate = function (req, res, errMsg, authorList, date, postRef, callback) {
let seen = {};
let uniqueAuthors = [];
for (var i = 0; i < authorList.length; i++) {
if (!seen[authorList[i]]) {
seen[authorList[i]] = true;
uniqueAuthors.push(authorList[i]);
}
}
// for getting posts by following for main feed, and all posts with tag on date,
checkMultiUsersForUpdates(req, res, errMsg, uniqueAuthors, function () {
if (!postCache[date]) { postCache[date] = {}}
var cache = postCache[date];
var filteredAuthorList = [];
var posts = [];
for (var i = 0; i < uniqueAuthors.length; i++) {
if (cache[uniqueAuthors[i]] === undefined) {
filteredAuthorList.push(uniqueAuthors[i]);
} else if (cache[uniqueAuthors[i]]) {
if (postRef[cache[uniqueAuthors[i]].post_id]) {
posts.push({post_id: cache[uniqueAuthors[i]].post_id,});
} else {
posts.push(cache[uniqueAuthors[i]]);
}
} // else it's not undefined and is falsey so do nothing, there is no post
}
readMultiUsers(req, res, errMsg, filteredAuthorList, {list:['username', 'iconURI'], dates:[date]}, function (users) {
// previously the line above had {list:['username', 'iconURI', 'pendingUpdates',], dates:[date]},
// i don't think i need "pendingUpdates" there, but if something errs after this push, check here
for (var i = 0; i < users.length; i++) {
if (users[i].posts && users[i].posts[date]) {
//
var post_id = null;
if (users[i].posts[date][0].post_id) {post_id = users[i].posts[date][0].post_id}
// strip out private posts
if (!users[i].posts[date][0].private) {
// if the postRef indicates that FE already has it, we don't need it again
if (postRef[post_id]) {
posts.push({post_id: post_id,});
} else {
var authorPic = getUserPic(users[i]);
var post = {
body: users[i].posts[date][0].body,
tags: users[i].posts[date][0].tags,
post_id: post_id,
author: users[i].username,
authorPic: authorPic,
_id: users[i]._id,
date: date,
title: users[i].posts[date][0].title,
url: users[i].posts[date][0].url,
}