Begin shaping codebase for loopback migration.
This commit is contained in:
599
server/boot/story.js
Executable file
599
server/boot/story.js
Executable file
@@ -0,0 +1,599 @@
|
||||
/* eslint-disable no-catch-shadow, no-unused-vars */
|
||||
var R = require('ramda'),
|
||||
debug = require('debug')('freecc:cntr:story'),
|
||||
Story = require('./../../models/Story'),
|
||||
Comment = require('./../../models/Comment'),
|
||||
User = require('./../../models/User'),
|
||||
moment = require('moment'),
|
||||
resources = require('./../resources/resources'),
|
||||
mongodb = require('mongodb'),
|
||||
MongoClient = mongodb.MongoClient,
|
||||
secrets = require('../../config/secrets'),
|
||||
nodemailer = require('nodemailer'),
|
||||
sanitizeHtml = require('sanitize-html'),
|
||||
express = require('express'),
|
||||
router = express.Router();
|
||||
|
||||
router.get('/stories/hotStories', hotJSON);
|
||||
router.get('/stories/recentStories', recentJSON);
|
||||
router.get('/stories/comments/:id', comments);
|
||||
router.post('/stories/comment/', commentSubmit);
|
||||
router.post('/stories/comment/:id/comment', commentOnCommentSubmit);
|
||||
router.put('/stories/comment/:id/edit', commentEdit);
|
||||
router.get('/stories/submit', submitNew);
|
||||
router.get('/stories/submit/new-story', preSubmit);
|
||||
router.post('/stories/preliminary', newStory);
|
||||
router.post('/stories/', storySubmission);
|
||||
router.get('/news/', hot);
|
||||
router.post('/stories/search', getStories);
|
||||
router.get('/news/:storyName', returnIndividualStory);
|
||||
router.post('/stories/upvote/', upvote);
|
||||
|
||||
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) {
|
||||
var story = Story.find({}).sort({'timePosted': -1}).limit(1000);
|
||||
story.exec(function(err, stories) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
var foundationDate = 1413298800000;
|
||||
|
||||
var sliceVal = stories.length >= 100 ? 100 : stories.length;
|
||||
return res.json(stories.map(function(elem) {
|
||||
return elem;
|
||||
}).sort(function(a, b) {
|
||||
return hotRank(b.timePosted - foundationDate, b.rank, b.headline)
|
||||
- hotRank(a.timePosted - foundationDate, a.rank, a.headline);
|
||||
}).slice(0, sliceVal));
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
function recentJSON(req, res, next) {
|
||||
var story = Story.find({}).sort({'timePosted': -1}).limit(100);
|
||||
story.exec(function(err, stories) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
return res.json(stories);
|
||||
});
|
||||
}
|
||||
|
||||
function hot(req, res) {
|
||||
return res.render('stories/index', {
|
||||
title: 'Hot stories currently trending on Camper News',
|
||||
page: 'hot'
|
||||
});
|
||||
}
|
||||
|
||||
function submitNew(req, res) {
|
||||
return res.render('stories/index', {
|
||||
title: 'Submit a new story to Camper News',
|
||||
page: 'submit'
|
||||
});
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
var data = req.query;
|
||||
var cleanData = sanitizeHtml(data.url, {
|
||||
allowedTags: [],
|
||||
allowedAttributes: []
|
||||
}).replace(/";/g, '"');
|
||||
if (data.url.replace(/&/g, '&') !== cleanData) {
|
||||
|
||||
req.flash('errors', {
|
||||
msg: 'The data for this post is malformed'
|
||||
});
|
||||
return res.render('stories/index', {
|
||||
page: 'stories/submit'
|
||||
});
|
||||
}
|
||||
|
||||
var title = data.title || '';
|
||||
var image = data.image || '';
|
||||
var description = data.description || '';
|
||||
return res.render('stories/index', {
|
||||
title: 'Confirm your Camper News story submission',
|
||||
page: 'storySubmission',
|
||||
storyURL: data.url,
|
||||
storyTitle: title,
|
||||
storyImage: image,
|
||||
storyMetaDescription: description
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function returnIndividualStory(req, res, next) {
|
||||
var dashedName = req.params.storyName;
|
||||
|
||||
var storyName = dashedName.replace(/\-/g, ' ').trim();
|
||||
|
||||
Story.find({'storyLink': storyName}, function(err, story) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
|
||||
|
||||
if (story.length < 1) {
|
||||
req.flash('errors', {
|
||||
msg: "404: We couldn't find a story with that name. " +
|
||||
'Please double check the name.'
|
||||
});
|
||||
|
||||
return res.redirect('/news/');
|
||||
}
|
||||
|
||||
story = story.pop();
|
||||
var dashedNameFull = story.storyLink.toLowerCase()
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/\s/g, '-');
|
||||
if (dashedNameFull !== dashedName) {
|
||||
return res.redirect('../news/' + dashedNameFull);
|
||||
}
|
||||
|
||||
var userVoted = false;
|
||||
try {
|
||||
var votedObj = story.upVotes.filter(function(a) {
|
||||
return a['upVotedByUsername'] === req.user['profile']['username'];
|
||||
});
|
||||
if (votedObj.length > 0) {
|
||||
userVoted = true;
|
||||
}
|
||||
} catch(e) {
|
||||
userVoted = false;
|
||||
}
|
||||
res.render('stories/index', {
|
||||
title: story.headline,
|
||||
link: story.link,
|
||||
originalStoryLink: dashedName,
|
||||
originalStoryAuthorEmail: story.author.email || '',
|
||||
author: story.author,
|
||||
description: story.description,
|
||||
rank: story.upVotes.length,
|
||||
upVotes: story.upVotes,
|
||||
comments: story.comments,
|
||||
id: story._id,
|
||||
timeAgo: moment(story.timePosted).fromNow(),
|
||||
image: story.image,
|
||||
page: 'show',
|
||||
storyMetaDescription: story.metaDescription,
|
||||
hasUserVoted: userVoted
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getStories(req, res, next) {
|
||||
MongoClient.connect(secrets.db, function(err, database) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
database.collection('stories').find({
|
||||
'$text': {
|
||||
'$search': req.body.data.searchValue
|
||||
}
|
||||
}, {
|
||||
headline: 1,
|
||||
timePosted: 1,
|
||||
link: 1,
|
||||
description: 1,
|
||||
rank: 1,
|
||||
upVotes: 1,
|
||||
author: 1,
|
||||
comments: 1,
|
||||
image: 1,
|
||||
storyLink: 1,
|
||||
metaDescription: 1,
|
||||
textScore: {
|
||||
$meta: 'textScore'
|
||||
}
|
||||
}, {
|
||||
sort: {
|
||||
textScore: {
|
||||
$meta: 'textScore'
|
||||
}
|
||||
}
|
||||
}).toArray(function(err, items) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
if (items !== null && items.length !== 0) {
|
||||
return res.json(items);
|
||||
}
|
||||
return res.sendStatus(404);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function upvote(req, res, next) {
|
||||
var data = req.body.data;
|
||||
Story.find({'_id': data.id}, function(err, story) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
story = story.pop();
|
||||
story.rank++;
|
||||
story.upVotes.push(
|
||||
{
|
||||
upVotedBy: req.user._id,
|
||||
upVotedByUsername: req.user.profile.username
|
||||
}
|
||||
);
|
||||
story.markModified('rank');
|
||||
story.save();
|
||||
User.findOne({'_id': story.author.userId}, function(err, user) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
user.progressTimestamps.push(Date.now() || 0);
|
||||
user.save(function (err, user) {
|
||||
req.user.save(function (err, user) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
req.user.progressTimestamps.push(Date.now() || 0);
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
return res.send(story);
|
||||
});
|
||||
}
|
||||
|
||||
function comments(req, res, next) {
|
||||
var data = req.params.id;
|
||||
Comment.find({'_id': data}, function(err, comment) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
comment = comment.pop();
|
||||
return res.send(comment);
|
||||
});
|
||||
}
|
||||
|
||||
function newStory(req, res, next) {
|
||||
if (!req.user) {
|
||||
return next(new Error('Must be logged in'));
|
||||
}
|
||||
var url = req.body.data.url;
|
||||
var cleanURL = sanitizeHtml(url, {
|
||||
allowedTags: [],
|
||||
allowedAttributes: []
|
||||
}).replace(/"/g, '"');
|
||||
if (cleanURL !== url) {
|
||||
req.flash('errors', {
|
||||
msg: "The URL you submitted doesn't appear valid"
|
||||
});
|
||||
return res.json({
|
||||
alreadyPosted: true,
|
||||
storyURL: '/stories/submit'
|
||||
});
|
||||
|
||||
}
|
||||
if (url.search(/^https?:\/\//g) === -1) {
|
||||
url = 'http://' + url;
|
||||
}
|
||||
Story.find({'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: '/news/' + story.pop().storyLink
|
||||
});
|
||||
}
|
||||
resources.getURLTitle(url, processResponse);
|
||||
});
|
||||
|
||||
function processResponse(err, story) {
|
||||
if (err) {
|
||||
res.json({
|
||||
alreadyPosted: false,
|
||||
storyURL: url,
|
||||
storyTitle: '',
|
||||
storyImage: '',
|
||||
storyMetaDescription: ''
|
||||
});
|
||||
} else {
|
||||
res.json({
|
||||
alreadyPosted: false,
|
||||
storyURL: url,
|
||||
storyTitle: story.title,
|
||||
storyImage: story.image,
|
||||
storyMetaDescription: story.description
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function storySubmission(req, res, next) {
|
||||
var data = req.body.data;
|
||||
if (!req.user) {
|
||||
return next(new Error('Not authorized'));
|
||||
}
|
||||
var storyLink = data.headline
|
||||
.replace(/[^a-z0-9\s]/gi, '')
|
||||
.replace(/\s+/g, ' ')
|
||||
.toLowerCase()
|
||||
.trim();
|
||||
var link = data.link;
|
||||
if (link.search(/^https?:\/\//g) === -1) {
|
||||
link = 'http://' + link;
|
||||
}
|
||||
Story.count({ storyLink: new RegExp('^' + storyLink + '(?: [0-9]+)?$', 'i')}, function (err, storyCount) {
|
||||
if (err) {
|
||||
return res.status(500);
|
||||
}
|
||||
|
||||
// if duplicate storyLink add unique number
|
||||
storyLink = (storyCount === 0) ? storyLink : storyLink + ' ' + storyCount;
|
||||
|
||||
var link = data.link;
|
||||
if (link.search(/^https?:\/\//g) === -1) {
|
||||
link = 'http://' + link;
|
||||
}
|
||||
var story = new Story({
|
||||
headline: sanitizeHtml(data.headline, {
|
||||
allowedTags: [],
|
||||
allowedAttributes: []
|
||||
}).replace(/"/g, '"'),
|
||||
timePosted: Date.now(),
|
||||
link: link,
|
||||
description: sanitizeHtml(data.description, {
|
||||
allowedTags: [],
|
||||
allowedAttributes: []
|
||||
}).replace(/"/g, '"'),
|
||||
rank: 1,
|
||||
upVotes: [({
|
||||
upVotedBy: req.user._id,
|
||||
upVotedByUsername: req.user.profile.username
|
||||
})],
|
||||
author: {
|
||||
picture: req.user.profile.picture,
|
||||
userId: req.user._id,
|
||||
username: req.user.profile.username,
|
||||
email: req.user.email
|
||||
},
|
||||
comments: [],
|
||||
image: data.image,
|
||||
storyLink: storyLink,
|
||||
metaDescription: data.storyMetaDescription,
|
||||
originalStoryAuthorEmail: req.user.email
|
||||
});
|
||||
story.save(function (err) {
|
||||
if (err) {
|
||||
return res.status(500);
|
||||
}
|
||||
req.user.progressTimestamps.push(Date.now() || 0);
|
||||
req.user.save(function (err, user) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
res.send(JSON.stringify({
|
||||
storyLink: story.storyLink.replace(/\s+/g, '-').toLowerCase()
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function commentSubmit(req, res, next) {
|
||||
var data = req.body.data;
|
||||
if (!req.user) {
|
||||
return next(new Error('Not authorized'));
|
||||
}
|
||||
var sanitizedBody = sanitizeHtml(data.body,
|
||||
{
|
||||
allowedTags: [],
|
||||
allowedAttributes: []
|
||||
}).replace(/"/g, '"');
|
||||
if (data.body !== sanitizedBody) {
|
||||
req.flash('errors', {
|
||||
msg: 'HTML is not allowed'
|
||||
});
|
||||
return res.send(true);
|
||||
}
|
||||
var comment = new Comment({
|
||||
associatedPost: data.associatedPost,
|
||||
originalStoryLink: data.originalStoryLink,
|
||||
originalStoryAuthorEmail: data.originalStoryAuthorEmail,
|
||||
body: sanitizedBody,
|
||||
rank: 0,
|
||||
upvotes: 0,
|
||||
author: {
|
||||
picture: req.user.profile.picture,
|
||||
userId: req.user._id,
|
||||
username: req.user.profile.username,
|
||||
email: req.user.email
|
||||
},
|
||||
comments: [],
|
||||
topLevel: true,
|
||||
commentOn: Date.now()
|
||||
});
|
||||
|
||||
commentSave(comment, Story, res, next);
|
||||
}
|
||||
|
||||
function commentOnCommentSubmit(req, res, next) {
|
||||
var data = req.body.data;
|
||||
if (!req.user) {
|
||||
return next(new Error('Not authorized'));
|
||||
}
|
||||
|
||||
var sanitizedBody = sanitizeHtml(data.body,
|
||||
{
|
||||
allowedTags: [],
|
||||
allowedAttributes: []
|
||||
}).replace(/"/g, '"');
|
||||
if (data.body !== sanitizedBody) {
|
||||
req.flash('errors', {
|
||||
msg: 'HTML is not allowed'
|
||||
});
|
||||
return res.send(true);
|
||||
}
|
||||
var comment = new Comment({
|
||||
associatedPost: data.associatedPost,
|
||||
body: sanitizedBody,
|
||||
rank: 0,
|
||||
upvotes: 0,
|
||||
originalStoryLink: data.originalStoryLink,
|
||||
originalStoryAuthorEmail: data.originalStoryAuthorEmail,
|
||||
author: {
|
||||
picture: req.user.profile.picture,
|
||||
userId: req.user._id,
|
||||
username: req.user.profile.username,
|
||||
email: req.user.email
|
||||
},
|
||||
comments: [],
|
||||
topLevel: false,
|
||||
commentOn: Date.now()
|
||||
});
|
||||
commentSave(comment, Comment, res, next);
|
||||
}
|
||||
|
||||
function commentEdit(req, res, next) {
|
||||
|
||||
Comment.find({'_id': req.params.id}, function(err, cmt) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
cmt = cmt.pop();
|
||||
|
||||
if (!req.user && cmt.author.userId !== req.user._id) {
|
||||
return next(new Error('Not authorized'));
|
||||
}
|
||||
|
||||
|
||||
var sanitizedBody = sanitizeHtml(req.body.body, {
|
||||
allowedTags: [],
|
||||
allowedAttributes: []
|
||||
}).replace(/"/g, '"');
|
||||
if (req.body.body !== sanitizedBody) {
|
||||
req.flash('errors', {
|
||||
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);
|
||||
}
|
||||
res.send(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function commentSave(comment, Context, res, next) {
|
||||
comment.save(function(err, data) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
try {
|
||||
// Based on the context retrieve the parent
|
||||
// object of the comment (Story/Comment)
|
||||
Context.find({'_id': data.associatedPost}, function (err, associatedContext) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
associatedContext = associatedContext.pop();
|
||||
if (associatedContext) {
|
||||
associatedContext.comments.push(data._id);
|
||||
associatedContext.save(function (err) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
res.send(true);
|
||||
});
|
||||
}
|
||||
// Find the author of the parent object
|
||||
User.findOne({'profile.username': associatedContext.author.username}, function(err, recipient) {
|
||||
if (err) {
|
||||
return next(err);
|
||||
}
|
||||
// If the emails of both authors differ,
|
||||
// only then proceed with email notification
|
||||
if (
|
||||
typeof data.author !== 'undefined' &&
|
||||
data.author.email &&
|
||||
typeof recipient !== 'undefined' &&
|
||||
recipient.email &&
|
||||
(data.author.email !== recipient.email)
|
||||
) {
|
||||
var transporter = nodemailer.createTransport({
|
||||
service: 'Mandrill',
|
||||
auth: {
|
||||
user: secrets.mandrill.user,
|
||||
pass: secrets.mandrill.password
|
||||
}
|
||||
});
|
||||
|
||||
var mailOptions = {
|
||||
to: recipient.email,
|
||||
from: 'Team@freecodecamp.com',
|
||||
subject: data.author.username +
|
||||
' replied to your post on Camper News',
|
||||
text: [
|
||||
'Just a quick heads-up: ',
|
||||
data.author.username + ' replied to you on Camper News.',
|
||||
'You can keep this conversation going.',
|
||||
'Just head back to the discussion here: ',
|
||||
'http://freecodecamp.com/news/' + data.originalStoryLink,
|
||||
'- the Free Code Camp Volunteer Team'
|
||||
].join('\n')
|
||||
};
|
||||
|
||||
transporter.sendMail(mailOptions, function (err) {
|
||||
if (err) {
|
||||
return err;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
return next(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = router;
|
Reference in New Issue
Block a user