refactor rxify stories
fixes many bugs
This commit is contained in:
@ -264,9 +264,8 @@ $(document).ready(function() {
|
|||||||
$('#story-submit').bind('click', storySubmitButtonHandler);
|
$('#story-submit').bind('click', storySubmitButtonHandler);
|
||||||
})
|
})
|
||||||
.done(function(data, textStatus, xhr) {
|
.done(function(data, textStatus, xhr) {
|
||||||
window.location = '/stories/' + JSON.parse(data).storyLink;
|
window.location = '/stories/' + data.storyLink;
|
||||||
});
|
});
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$('#story-submit').on('click', storySubmitButtonHandler);
|
$('#story-submit').on('click', storySubmitButtonHandler);
|
||||||
|
@ -1,17 +1,84 @@
|
|||||||
var nodemailer = require('nodemailer'),
|
var Rx = require('rx'),
|
||||||
|
nodemailer = require('nodemailer'),
|
||||||
|
assign = require('object.assign'),
|
||||||
sanitizeHtml = require('sanitize-html'),
|
sanitizeHtml = require('sanitize-html'),
|
||||||
moment = require('moment'),
|
moment = require('moment'),
|
||||||
mongodb = require('mongodb'),
|
mongodb = require('mongodb'),
|
||||||
// debug = require('debug')('freecc:cntr:story'),
|
// debug = require('debug')('freecc:cntr:story'),
|
||||||
utils = require('../utils'),
|
utils = require('../utils'),
|
||||||
|
observeMethod = require('../utils/rx').observeMethod,
|
||||||
|
saveUser = require('../utils/rx').saveUser,
|
||||||
|
saveInstance = require('../utils/rx').saveInstance,
|
||||||
MongoClient = mongodb.MongoClient,
|
MongoClient = mongodb.MongoClient,
|
||||||
secrets = require('../../config/secrets');
|
secrets = require('../../config/secrets');
|
||||||
|
|
||||||
|
var foundationDate = 1413298800000;
|
||||||
|
var time48Hours = 172800000;
|
||||||
|
|
||||||
|
var unDasherize = utils.unDasherize;
|
||||||
|
var dasherize = utils.dasherize;
|
||||||
|
var getURLTitle = utils.getURLTitle;
|
||||||
|
|
||||||
|
var transporter = nodemailer.createTransport({
|
||||||
|
service: 'Mandrill',
|
||||||
|
auth: {
|
||||||
|
user: secrets.mandrill.user,
|
||||||
|
pass: secrets.mandrill.password
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function sendMailWhillyNilly(mailOptions) {
|
||||||
|
transporter.sendMail(mailOptions, function(err) {
|
||||||
|
if (err) {
|
||||||
|
console.log('err sending mail whilly nilly', err);
|
||||||
|
console.log('logging err but not carring');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function hotRank(timeValue, rank) {
|
||||||
|
/*
|
||||||
|
* Hotness ranking algorithm: http://amix.dk/blog/post/19588
|
||||||
|
* tMS = postedOnDate - foundationTime;
|
||||||
|
* Ranking...
|
||||||
|
* f(ts, 1, rank) = log(10)z + (ts)/45000;
|
||||||
|
*/
|
||||||
|
var z = Math.log(rank) / Math.log(10);
|
||||||
|
var hotness = z + (timeValue / time48Hours);
|
||||||
|
return hotness;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortByRank(a, b) {
|
||||||
|
return hotRank(b.timePosted - foundationDate, b.rank) -
|
||||||
|
hotRank(a.timePosted - foundationDate, a.rank);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanData(data, opts) {
|
||||||
|
var options = assign(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
allowedTags: [],
|
||||||
|
allowedAttributes: []
|
||||||
|
},
|
||||||
|
opts || {}
|
||||||
|
);
|
||||||
|
return sanitizeHtml(data, options).replace(/";/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = function(app) {
|
module.exports = function(app) {
|
||||||
var router = app.loopback.Router();
|
var router = app.loopback.Router();
|
||||||
var User = app.models.User;
|
var User = app.models.User;
|
||||||
|
var findUserById = observeMethod(User, 'findById');
|
||||||
|
var findOneUser = observeMethod(User, 'findOne');
|
||||||
|
|
||||||
var Story = app.models.Story;
|
var Story = app.models.Story;
|
||||||
|
var findStory = observeMethod(Story, 'find');
|
||||||
|
var findOneStory = observeMethod(Story, 'findOne');
|
||||||
|
var findStoryById = observeMethod(Story, 'findById');
|
||||||
|
var countStories = observeMethod(Story, 'count');
|
||||||
|
|
||||||
var Comment = app.models.Comment;
|
var Comment = app.models.Comment;
|
||||||
|
var findCommentById = observeMethod(Comment, 'findById');
|
||||||
|
|
||||||
router.get('/stories/hotStories', hotJSON);
|
router.get('/stories/hotStories', hotJSON);
|
||||||
router.get('/stories/comments/:id', comments);
|
router.get('/stories/comments/:id', comments);
|
||||||
@ -29,39 +96,19 @@ module.exports = function(app) {
|
|||||||
|
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
function hotRank(timeValue, rank) {
|
|
||||||
/*
|
|
||||||
* Hotness ranking algorithm: http://amix.dk/blog/post/19588
|
|
||||||
* tMS = postedOnDate - foundationTime;
|
|
||||||
* Ranking...
|
|
||||||
* f(ts, 1, rank) = log(10)z + (ts)/45000;
|
|
||||||
*/
|
|
||||||
var time48Hours = 172800000;
|
|
||||||
var hotness;
|
|
||||||
var z = Math.log(rank) / Math.log(10);
|
|
||||||
hotness = z + (timeValue / time48Hours);
|
|
||||||
return hotness;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hotJSON(req, res, next) {
|
function hotJSON(req, res, next) {
|
||||||
Story.find({
|
var query = {
|
||||||
order: 'timePosted DESC',
|
order: 'timePosted DESC',
|
||||||
limit: 1000
|
limit: 1000
|
||||||
}, function(err, stories) {
|
};
|
||||||
if (err) {
|
findStory(query).subscribe(
|
||||||
return next(err);
|
function(stories) {
|
||||||
}
|
|
||||||
var foundationDate = 1413298800000;
|
|
||||||
|
|
||||||
var sliceVal = stories.length >= 100 ? 100 : stories.length;
|
var sliceVal = stories.length >= 100 ? 100 : stories.length;
|
||||||
return res.json(stories.map(function(elem) {
|
var data = stories.sort(sortByRank).slice(0, sliceVal);
|
||||||
return elem;
|
res.json(data);
|
||||||
}).sort(function(a, b) {
|
},
|
||||||
return hotRank(b.timePosted - foundationDate, b.rank, b.headline)
|
next
|
||||||
- hotRank(a.timePosted - foundationDate, a.rank, a.headline);
|
);
|
||||||
}).slice(0, sliceVal));
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hot(req, res) {
|
function hot(req, res) {
|
||||||
@ -78,32 +125,11 @@ module.exports = function(app) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* no used anywhere
|
|
||||||
function search(req, res) {
|
|
||||||
return res.render('stories/index', {
|
|
||||||
title: 'Search the archives of Camper News',
|
|
||||||
page: 'search'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function recent(req, res) {
|
|
||||||
return res.render('stories/index', {
|
|
||||||
title: 'Recently submitted stories on Camper News',
|
|
||||||
page: 'recent'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function preSubmit(req, res) {
|
function preSubmit(req, res) {
|
||||||
|
|
||||||
var data = req.query;
|
var data = req.query;
|
||||||
var cleanData = sanitizeHtml(data.url, {
|
var cleanedData = cleanData(data.url);
|
||||||
allowedTags: [],
|
|
||||||
allowedAttributes: []
|
|
||||||
}).replace(/";/g, '"');
|
|
||||||
if (data.url.replace(/&/g, '&') !== cleanData) {
|
|
||||||
|
|
||||||
|
if (data.url.replace(/&/g, '&') !== cleanedData) {
|
||||||
req.flash('errors', {
|
req.flash('errors', {
|
||||||
msg: 'The data for this post is malformed'
|
msg: 'The data for this post is malformed'
|
||||||
});
|
});
|
||||||
@ -125,46 +151,33 @@ module.exports = function(app) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function returnIndividualStory(req, res, next) {
|
function returnIndividualStory(req, res, next) {
|
||||||
var dashedName = req.params.storyName;
|
var dashedName = req.params.storyName;
|
||||||
|
var storyName = unDasherize(dashedName);
|
||||||
|
|
||||||
var storyName = dashedName.replace(/\-/g, ' ').trim();
|
findOneStory({ where: { storyLink: storyName } }).subscribe(
|
||||||
|
function(story) {
|
||||||
Story.find({ where: { storyLink: storyName } }, function(err, story) {
|
if (!story) {
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (story.length < 1) {
|
|
||||||
req.flash('errors', {
|
req.flash('errors', {
|
||||||
msg: "404: We couldn't find a story with that name. " +
|
msg: "404: We couldn't find a story with that name. " +
|
||||||
'Please double check the name.'
|
'Please double check the name.'
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.redirect('/stories/');
|
|
||||||
}
|
|
||||||
|
|
||||||
story = story.pop();
|
|
||||||
var dashedNameFull = story.storyLink.toLowerCase()
|
var dashedNameFull = story.storyLink.toLowerCase()
|
||||||
.replace(/\s+/g, ' ')
|
.replace(/\s+/g, ' ')
|
||||||
.replace(/\s/g, '-');
|
.replace(/\s/g, '-');
|
||||||
|
|
||||||
if (dashedNameFull !== dashedName) {
|
if (dashedNameFull !== dashedName) {
|
||||||
return res.redirect('../stories/' + dashedNameFull);
|
return res.redirect('../stories/' + dashedNameFull);
|
||||||
}
|
}
|
||||||
|
return res.redirect('/stories/');
|
||||||
|
}
|
||||||
|
|
||||||
var userVoted = false;
|
// true if any of votes are made by user
|
||||||
try {
|
var userVoted = story.upVotes.some(function(upvote) {
|
||||||
var votedObj = story.upVotes.filter(function(a) {
|
return upvote.upVotedByUsername === req.user.username;
|
||||||
return a['upVotedByUsername'] === req.user['profile']['username'];
|
|
||||||
});
|
});
|
||||||
if (votedObj.length > 0) {
|
|
||||||
userVoted = true;
|
|
||||||
}
|
|
||||||
} catch(e) {
|
|
||||||
userVoted = false;
|
|
||||||
}
|
|
||||||
res.render('stories/index', {
|
res.render('stories/index', {
|
||||||
title: story.headline,
|
title: story.headline,
|
||||||
link: story.link,
|
link: story.link,
|
||||||
@ -182,7 +195,9 @@ module.exports = function(app) {
|
|||||||
storyMetaDescription: story.metaDescription,
|
storyMetaDescription: story.metaDescription,
|
||||||
hasUserVoted: userVoted
|
hasUserVoted: userVoted
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
next
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStories(req, res, next) {
|
function getStories(req, res, next) {
|
||||||
@ -228,54 +243,50 @@ module.exports = function(app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function upvote(req, res, next) {
|
function upvote(req, res, next) {
|
||||||
var data = req.body.data;
|
var id = req.body.data.id;
|
||||||
Story.find({ where: { id: data.id } }, function(err, story) {
|
var savedStory = findStoryById(id)
|
||||||
if (err) {
|
.flatMap(function(story) {
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
story = story.pop();
|
|
||||||
story.rank += 1;
|
story.rank += 1;
|
||||||
story.upVotes.push({
|
story.upVotes.push({
|
||||||
upVotedBy: req.user.id,
|
upVotedBy: req.user.id,
|
||||||
upVotedByUsername: req.user.username
|
upVotedByUsername: req.user.username
|
||||||
});
|
});
|
||||||
story.save();
|
return saveInstance(story);
|
||||||
// NOTE(Berks): This logic is full of wholes and race conditions
|
})
|
||||||
// this could be the source of many 'can't set headers after
|
.shareReplay();
|
||||||
// they are sent'
|
|
||||||
// errors. This needs cleaning
|
|
||||||
User.findOne(
|
|
||||||
{ where: { id: story.author.userId } },
|
|
||||||
function(err, user) {
|
|
||||||
if (err) { return next(err); }
|
|
||||||
|
|
||||||
user.progressTimestamps.push(Date.now() || 0);
|
savedStory.flatMap(function(story) {
|
||||||
user.save(function (err) {
|
// find story author
|
||||||
req.user.save(function (err) {
|
return findUserById(story.author.userId);
|
||||||
if (err) { return next(err); }
|
})
|
||||||
});
|
.flatMap(function(user) {
|
||||||
req.user.progressTimestamps.push(Date.now() || 0);
|
// if user deletes account then this will not exist
|
||||||
if (err) {
|
if (user) {
|
||||||
return next(err);
|
user.progressTimestamps.push(Date.now());
|
||||||
}
|
}
|
||||||
});
|
return saveUser(user);
|
||||||
}
|
})
|
||||||
);
|
.flatMap(function() {
|
||||||
|
req.user.progressTimestamps.push(Date.now());
|
||||||
|
return saveUser(req.user);
|
||||||
|
})
|
||||||
|
.flatMap(savedStory)
|
||||||
|
.subscribe(
|
||||||
|
function(story) {
|
||||||
return res.send(story);
|
return res.send(story);
|
||||||
});
|
},
|
||||||
|
next
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function comments(req, res, next) {
|
function comments(req, res, next) {
|
||||||
var data = req.params.id;
|
var id = req.params.id;
|
||||||
Comment.find(
|
findCommentById(id).subscribe(
|
||||||
{ where: { id: data } },
|
function(comment) {
|
||||||
function(err, comment) {
|
res.send(comment);
|
||||||
if (err) {
|
},
|
||||||
return next(err);
|
next
|
||||||
}
|
);
|
||||||
comment = comment.pop();
|
|
||||||
return res.send(comment);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function newStory(req, res, next) {
|
function newStory(req, res, next) {
|
||||||
@ -283,10 +294,8 @@ module.exports = function(app) {
|
|||||||
return next(new Error('Must be logged in'));
|
return next(new Error('Must be logged in'));
|
||||||
}
|
}
|
||||||
var url = req.body.data.url;
|
var url = req.body.data.url;
|
||||||
var cleanURL = sanitizeHtml(url, {
|
var cleanURL = cleanData(url);
|
||||||
allowedTags: [],
|
|
||||||
allowedAttributes: []
|
|
||||||
}).replace(/"/g, '"');
|
|
||||||
if (cleanURL !== url) {
|
if (cleanURL !== url) {
|
||||||
req.flash('errors', {
|
req.flash('errors', {
|
||||||
msg: "The URL you submitted doesn't appear valid"
|
msg: "The URL you submitted doesn't appear valid"
|
||||||
@ -300,44 +309,46 @@ module.exports = function(app) {
|
|||||||
if (url.search(/^https?:\/\//g) === -1) {
|
if (url.search(/^https?:\/\//g) === -1) {
|
||||||
url = 'http://' + url;
|
url = 'http://' + url;
|
||||||
}
|
}
|
||||||
Story.find(
|
|
||||||
{ where: { link: url } },
|
|
||||||
function(err, story) {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
if (story.length) {
|
|
||||||
req.flash('errors', {
|
|
||||||
msg: "Someone's already posted that link. Here's the discussion."
|
|
||||||
});
|
|
||||||
return res.json({
|
|
||||||
alreadyPosted: true,
|
|
||||||
storyURL: '/stories/' + story.pop().storyLink
|
|
||||||
});
|
|
||||||
}
|
|
||||||
utils.getURLTitle(url, processResponse);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
function processResponse(err, story) {
|
findStory({ where: { link: url } })
|
||||||
if (err) {
|
.map(function(stories) {
|
||||||
res.json({
|
if (stories.length) {
|
||||||
|
return {
|
||||||
|
alreadyPosted: true,
|
||||||
|
storyURL: '/stories/' + stories.pop().storyLink
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
alreadyPosted: false,
|
alreadyPosted: false,
|
||||||
storyURL: url,
|
storyURL: url
|
||||||
storyTitle: '',
|
};
|
||||||
storyImage: '',
|
})
|
||||||
storyMetaDescription: ''
|
.flatMap(function(data) {
|
||||||
});
|
if (data.alreadyPosted) {
|
||||||
} else {
|
return Rx.Observable.just(data);
|
||||||
res.json({
|
}
|
||||||
|
return Rx.Observable.fromNodeCallback(getURLTitle)(data.storyURL)
|
||||||
|
.map(function(story) {
|
||||||
|
return {
|
||||||
alreadyPosted: false,
|
alreadyPosted: false,
|
||||||
storyURL: url,
|
storyURL: data.storyURL,
|
||||||
storyTitle: story.title,
|
storyTitle: story.title,
|
||||||
storyImage: story.image,
|
storyImage: story.image,
|
||||||
storyMetaDescription: story.description
|
storyMetaDescription: story.description
|
||||||
|
};
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.subscribe(
|
||||||
|
function(story) {
|
||||||
|
if (story.alreadyPosted) {
|
||||||
|
req.flash('errors', {
|
||||||
|
msg: "Someone's already posted that link. Here's the discussion."
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
res.json(story);
|
||||||
|
},
|
||||||
|
next
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function storySubmission(req, res, next) {
|
function storySubmission(req, res, next) {
|
||||||
@ -357,34 +368,29 @@ module.exports = function(app) {
|
|||||||
link = 'http://' + link;
|
link = 'http://' + link;
|
||||||
}
|
}
|
||||||
|
|
||||||
Story.count({
|
var query = {
|
||||||
storyLink: {
|
storyLink: {
|
||||||
like: ('^' + storyLink + '(?: [0-9]+)?$'),
|
like: ('^' + storyLink + '(?: [0-9]+)?$'),
|
||||||
options: 'i'
|
options: 'i'
|
||||||
}
|
}
|
||||||
}, function (err, storyCount) {
|
};
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
var savedStory = countStories(query)
|
||||||
|
.flatMap(function(storyCount) {
|
||||||
// if duplicate storyLink add unique number
|
// if duplicate storyLink add unique number
|
||||||
storyLink = (storyCount === 0) ? storyLink : storyLink + ' ' + storyCount;
|
storyLink = (storyCount === 0) ?
|
||||||
|
storyLink :
|
||||||
|
storyLink + ' ' + storyCount;
|
||||||
|
|
||||||
var link = data.link;
|
var link = data.link;
|
||||||
if (link.search(/^https?:\/\//g) === -1) {
|
if (link.search(/^https?:\/\//g) === -1) {
|
||||||
link = 'http://' + link;
|
link = 'http://' + link;
|
||||||
}
|
}
|
||||||
var story = new Story({
|
var newStory = new Story({
|
||||||
headline: sanitizeHtml(data.headline, {
|
headline: cleanData(data.headline),
|
||||||
allowedTags: [],
|
|
||||||
allowedAttributes: []
|
|
||||||
}).replace(/"/g, '"'),
|
|
||||||
timePosted: Date.now(),
|
timePosted: Date.now(),
|
||||||
link: link,
|
link: link,
|
||||||
description: sanitizeHtml(data.description, {
|
description: cleanData(data.description),
|
||||||
allowedTags: [],
|
|
||||||
allowedAttributes: []
|
|
||||||
}).replace(/"/g, '"'),
|
|
||||||
rank: 1,
|
rank: 1,
|
||||||
upVotes: [({
|
upVotes: [({
|
||||||
upVotedBy: req.user.id,
|
upVotedBy: req.user.id,
|
||||||
@ -402,21 +408,20 @@ module.exports = function(app) {
|
|||||||
metaDescription: data.storyMetaDescription,
|
metaDescription: data.storyMetaDescription,
|
||||||
originalStoryAuthorEmail: req.user.email
|
originalStoryAuthorEmail: req.user.email
|
||||||
});
|
});
|
||||||
story.save(function (err) {
|
return saveInstance(newStory);
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
req.user.progressTimestamps.push(Date.now() || 0);
|
|
||||||
req.user.save(function (err) {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
res.send(JSON.stringify({
|
|
||||||
storyLink: story.storyLink.replace(/\s+/g, '-').toLowerCase()
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
req.user.progressTimestamps.push(Date.now());
|
||||||
|
return saveUser(req.user)
|
||||||
|
.flatMap(savedStory)
|
||||||
|
.subscribe(
|
||||||
|
function(story) {
|
||||||
|
res.json({
|
||||||
|
storyLink: dasherize(story.storyLink)
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
next
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function commentSubmit(req, res, next) {
|
function commentSubmit(req, res, next) {
|
||||||
@ -424,11 +429,8 @@ module.exports = function(app) {
|
|||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
return next(new Error('Not authorized'));
|
return next(new Error('Not authorized'));
|
||||||
}
|
}
|
||||||
var sanitizedBody = sanitizeHtml(data.body,
|
var sanitizedBody = cleanData(data.body);
|
||||||
{
|
|
||||||
allowedTags: [],
|
|
||||||
allowedAttributes: []
|
|
||||||
}).replace(/"/g, '"');
|
|
||||||
if (data.body !== sanitizedBody) {
|
if (data.body !== sanitizedBody) {
|
||||||
req.flash('errors', {
|
req.flash('errors', {
|
||||||
msg: 'HTML is not allowed'
|
msg: 'HTML is not allowed'
|
||||||
@ -453,7 +455,13 @@ module.exports = function(app) {
|
|||||||
commentOn: Date.now()
|
commentOn: Date.now()
|
||||||
});
|
});
|
||||||
|
|
||||||
commentSave(comment, Story, res, next);
|
commentSave(comment, findStoryById).subscribe(
|
||||||
|
function() {},
|
||||||
|
next,
|
||||||
|
function() {
|
||||||
|
res.send(true);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function commentOnCommentSubmit(req, res, next) {
|
function commentOnCommentSubmit(req, res, next) {
|
||||||
@ -462,13 +470,7 @@ module.exports = function(app) {
|
|||||||
return next(new Error('Not authorized'));
|
return next(new Error('Not authorized'));
|
||||||
}
|
}
|
||||||
|
|
||||||
var sanitizedBody = sanitizeHtml(
|
var sanitizedBody = cleanData(data.body);
|
||||||
data.body,
|
|
||||||
{
|
|
||||||
allowedTags: [],
|
|
||||||
allowedAttributes: []
|
|
||||||
}
|
|
||||||
).replace(/"/g, '"');
|
|
||||||
|
|
||||||
if (data.body !== sanitizedBody) {
|
if (data.body !== sanitizedBody) {
|
||||||
req.flash('errors', {
|
req.flash('errors', {
|
||||||
@ -494,119 +496,101 @@ module.exports = function(app) {
|
|||||||
topLevel: false,
|
topLevel: false,
|
||||||
commentOn: Date.now()
|
commentOn: Date.now()
|
||||||
});
|
});
|
||||||
commentSave(comment, Comment, res, next);
|
commentSave(comment, findCommentById).subscribe(
|
||||||
|
function() {},
|
||||||
|
next,
|
||||||
|
function() {
|
||||||
|
res.send(true);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function commentEdit(req, res, next) {
|
function commentEdit(req, res, next) {
|
||||||
|
findCommentById(req.params.id)
|
||||||
Comment.find({ where: { id: req.params.id } }, function(err, cmt) {
|
.doOnNext(function(comment) {
|
||||||
if (err) {
|
if (!req.user && comment.author.userId !== req.user.id) {
|
||||||
return next(err);
|
throw new Error('Not authorized');
|
||||||
}
|
}
|
||||||
cmt = cmt.pop();
|
})
|
||||||
|
.flatMap(function(comment) {
|
||||||
if (!req.user && cmt.author.userId !== req.user.id) {
|
var sanitizedBody = cleanData(req.body.body);
|
||||||
return next(new Error('Not authorized'));
|
|
||||||
}
|
|
||||||
|
|
||||||
var sanitizedBody = sanitizeHtml(req.body.body, {
|
|
||||||
allowedTags: [],
|
|
||||||
allowedAttributes: []
|
|
||||||
}).replace(/"/g, '"');
|
|
||||||
if (req.body.body !== sanitizedBody) {
|
if (req.body.body !== sanitizedBody) {
|
||||||
req.flash('errors', {
|
req.flash('errors', {
|
||||||
msg: 'HTML is not allowed'
|
msg: 'HTML is not allowed'
|
||||||
});
|
});
|
||||||
return res.send(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
cmt.body = sanitizedBody;
|
|
||||||
cmt.commentOn = Date.now();
|
|
||||||
cmt.save(function(err) {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
}
|
||||||
|
comment.body = sanitizedBody;
|
||||||
|
comment.commentOn = Date.now();
|
||||||
|
return saveInstance(comment);
|
||||||
|
})
|
||||||
|
.subscribe(
|
||||||
|
function() {
|
||||||
res.send(true);
|
res.send(true);
|
||||||
});
|
},
|
||||||
|
next
|
||||||
});
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function commentSave(comment, Context, res, next) {
|
function commentSave(comment, findContextById) {
|
||||||
comment.save(function(err, data) {
|
return saveInstance(comment)
|
||||||
if (err) {
|
.flatMap(function(comment) {
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
// Based on the context retrieve the parent
|
// Based on the context retrieve the parent
|
||||||
// object of the comment (Story/Comment)
|
// object of the comment (Story/Comment)
|
||||||
Context.find({
|
return findContextById(comment.associatedPost);
|
||||||
where: { id: data.associatedPost }
|
})
|
||||||
}, function (err, associatedContext) {
|
.flatMap(function(associatedContext) {
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
associatedContext = associatedContext.pop();
|
|
||||||
if (associatedContext) {
|
if (associatedContext) {
|
||||||
associatedContext.comments.push(data.id);
|
associatedContext.comments.push(comment.id);
|
||||||
associatedContext.save(function (err) {
|
|
||||||
if (err) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
res.send(true);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
// NOTE(berks): saveInstance is safe
|
||||||
|
// it will automatically call onNext with null and onCompleted if
|
||||||
|
// argument is falsey or has no method save
|
||||||
|
return saveInstance(associatedContext);
|
||||||
|
})
|
||||||
|
.flatMap(function(associatedContext) {
|
||||||
// Find the author of the parent object
|
// Find the author of the parent object
|
||||||
User.findOne({
|
// if no username
|
||||||
username: associatedContext.author.username
|
var username = associatedContext && associatedContext.author ?
|
||||||
}, function(err, recipient) {
|
associatedContext.author.username :
|
||||||
if (err) {
|
null;
|
||||||
return next(err);
|
|
||||||
}
|
var query = { where: { username: username } };
|
||||||
|
return findOneUser(query);
|
||||||
|
})
|
||||||
|
// if no user is found we don't want to hit the doOnNext
|
||||||
|
// filter here will call onCompleted without running through the following
|
||||||
|
// steps
|
||||||
|
.filter(function(user) {
|
||||||
|
return !!user;
|
||||||
|
})
|
||||||
|
// if this is called user is guarenteed to exits
|
||||||
|
// this is a side effect, hence we use do/tap observable methods
|
||||||
|
.doOnNext(function(user) {
|
||||||
// If the emails of both authors differ,
|
// If the emails of both authors differ,
|
||||||
// only then proceed with email notification
|
// only then proceed with email notification
|
||||||
if (
|
if (
|
||||||
typeof data.author !== 'undefined' &&
|
comment.author &&
|
||||||
data.author.email &&
|
comment.author.email &&
|
||||||
typeof recipient !== 'undefined' &&
|
user.email &&
|
||||||
recipient.email &&
|
(comment.author.email !== user.email)
|
||||||
(data.author.email !== recipient.email)
|
|
||||||
) {
|
) {
|
||||||
var transporter = nodemailer.createTransport({
|
sendMailWhillyNilly({
|
||||||
service: 'Mandrill',
|
to: user.email,
|
||||||
auth: {
|
|
||||||
user: secrets.mandrill.user,
|
|
||||||
pass: secrets.mandrill.password
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var mailOptions = {
|
|
||||||
to: recipient.email,
|
|
||||||
from: 'Team@freecodecamp.com',
|
from: 'Team@freecodecamp.com',
|
||||||
subject: data.author.username +
|
subject: comment.author.username +
|
||||||
' replied to your post on Camper News',
|
' replied to your post on Camper News',
|
||||||
text: [
|
text: [
|
||||||
'Just a quick heads-up: ',
|
'Just a quick heads-up: ',
|
||||||
data.author.username + ' replied to you on Camper News.',
|
comment.author.username,
|
||||||
|
' replied to you on Camper News.',
|
||||||
'You can keep this conversation going.',
|
'You can keep this conversation going.',
|
||||||
'Just head back to the discussion here: ',
|
'Just head back to the discussion here: ',
|
||||||
'http://freecodecamp.com/stories/' + data.originalStoryLink,
|
'http://freecodecamp.com/stories/',
|
||||||
|
comment.originalStoryLink,
|
||||||
'- the Free Code Camp Volunteer Team'
|
'- the Free Code Camp Volunteer Team'
|
||||||
].join('\n')
|
].join('\n')
|
||||||
};
|
|
||||||
|
|
||||||
transporter.sendMail(mailOptions, function (err) {
|
|
||||||
if (err) {
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
return next(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -20,11 +20,6 @@ var allFieldGuideIds, allFieldGuideNames, allNonprofitNames,
|
|||||||
challengeMapWithNames, allChallengeIds,
|
challengeMapWithNames, allChallengeIds,
|
||||||
challengeMapWithDashedNames;
|
challengeMapWithDashedNames;
|
||||||
|
|
||||||
/**
|
|
||||||
* GET /
|
|
||||||
* Resources.
|
|
||||||
*/
|
|
||||||
|
|
||||||
Array.zip = function(left, right, combinerFunction) {
|
Array.zip = function(left, right, combinerFunction) {
|
||||||
var counter,
|
var counter,
|
||||||
results = [];
|
results = [];
|
||||||
@ -68,7 +63,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
unDasherize: function unDasherize(name) {
|
unDasherize: function unDasherize(name) {
|
||||||
return ('' + name).replace(/\-/g, ' ');
|
return ('' + name).replace(/\-/g, ' ').trim();
|
||||||
},
|
},
|
||||||
|
|
||||||
getChallengeMapForDisplay: function () {
|
getChallengeMapForDisplay: function () {
|
||||||
@ -200,10 +195,16 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
getURLTitle: function(url, callback) {
|
getURLTitle: function(url, callback) {
|
||||||
(function () {
|
var result = {
|
||||||
var result = {title: '', image: '', url: '', description: ''};
|
title: '',
|
||||||
request(url, function (error, response, body) {
|
image: '',
|
||||||
if (!error && response.statusCode === 200) {
|
url: '',
|
||||||
|
description: ''
|
||||||
|
};
|
||||||
|
request(url, function(err, response, body) {
|
||||||
|
if (err || response.statusCode !== 200) {
|
||||||
|
return callback(new Error('failed'));
|
||||||
|
}
|
||||||
var $ = cheerio.load(body);
|
var $ = cheerio.load(body);
|
||||||
var metaDescription = $("meta[name='description']");
|
var metaDescription = $("meta[name='description']");
|
||||||
var metaImage = $("meta[property='og:image']");
|
var metaImage = $("meta[property='og:image']");
|
||||||
@ -223,11 +224,7 @@ module.exports = {
|
|||||||
result.image = urlImage;
|
result.image = urlImage;
|
||||||
result.description = description;
|
result.description = description;
|
||||||
callback(null, result);
|
callback(null, result);
|
||||||
} else {
|
|
||||||
callback(new Error('failed'));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
})();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getMDNLinks: function(links) {
|
getMDNLinks: function(links) {
|
||||||
|
@ -1,24 +1,27 @@
|
|||||||
var Rx = require('rx');
|
var Rx = require('rx');
|
||||||
var debug = require('debug')('freecc:rxUtils');
|
var debug = require('debug')('freecc:rxUtils');
|
||||||
|
|
||||||
exports.saveUser = function saveUser(user) {
|
exports.saveInstance = function saveInstance(instance) {
|
||||||
return new Rx.Observable.create(function(observer) {
|
return new Rx.Observable.create(function(observer) {
|
||||||
if (!user || typeof user.save !== 'function') {
|
if (!instance || typeof instance.save !== 'function') {
|
||||||
debug('no user or save method');
|
debug('no instance or save method');
|
||||||
observer.onNext();
|
observer.onNext();
|
||||||
return observer.onCompleted();
|
return observer.onCompleted();
|
||||||
}
|
}
|
||||||
user.save(function(err, savedUser) {
|
instance.save(function(err, savedInstance) {
|
||||||
if (err) {
|
if (err) {
|
||||||
return observer.onError(err);
|
return observer.onError(err);
|
||||||
}
|
}
|
||||||
debug('user saved');
|
debug('instance saved');
|
||||||
observer.onNext(savedUser);
|
observer.onNext(savedInstance);
|
||||||
observer.onCompleted();
|
observer.onCompleted();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// alias saveInstance
|
||||||
|
exports.saveUser = exports.saveInstance;
|
||||||
|
|
||||||
exports.observableQueryFromModel =
|
exports.observableQueryFromModel =
|
||||||
function observableQueryFromModel(Model, method, query) {
|
function observableQueryFromModel(Model, method, query) {
|
||||||
return Rx.Observable.fromNodeCallback(Model[method], Model)(query);
|
return Rx.Observable.fromNodeCallback(Model[method], Model)(query);
|
||||||
|
Reference in New Issue
Block a user