var nodemailer = require('nodemailer'), sanitizeHtml = require('sanitize-html'), express = require('express'), moment = require('moment'), // debug = require('debug')('freecc:cntr:story'), Story = require('./../../models/Story'), Comment = require('./../../models/Comment'), User = require('./../../models/User'), resources = require('./../resources/resources'), mongodb = require('mongodb'), MongoClient = mongodb.MongoClient, secrets = require('../../config/secrets'), 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' }); } /* * 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) { 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(); // NOTE(Berks): This logic is full of wholes and race conditions // this could be the source of many 'can't set headers after they are sent' // errors. This needs cleaning User.findOne({'_id': story.author.userId}, function(err, user) { if (err) { return next(err); } user.progressTimestamps.push(Date.now() || 0); user.save(function (err) { req.user.save(function (err) { 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 next(err); } // 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 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() })); }); }); }); } 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;