Permalink
Cannot retrieve contributors at this time
Fetching contributors…

var Rx = require('rx'), | |
assign = require('object.assign'), | |
sanitizeHtml = require('sanitize-html'), | |
moment = require('moment'), | |
debug = require('debug')('fcc:cntr:story'), | |
utils = require('../utils'), | |
observeMethod = require('../utils/rx').observeMethod, | |
saveUser = require('../utils/rx').saveUser, | |
saveInstance = require('../utils/rx').saveInstance, | |
validator = require('validator'); | |
import { | |
ifNoUser401, | |
ifNoUserRedirectTo | |
} from '../utils/middleware'; | |
const foundationDate = 1413298800000; | |
const time48Hours = 172800000; | |
const unDasherize = utils.unDasherize; | |
const dasherize = utils.dasherize; | |
const getURLTitle = utils.getURLTitle; | |
const sendNonUserToNews = ifNoUserRedirectTo('/news'); | |
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) { | |
var router = app.loopback.Router(); | |
var User = app.models.User; | |
var findUserById = observeMethod(User, 'findById'); | |
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'); | |
router.post('/news/userstories', userStories); | |
router.get('/news/hot', hotJSON); | |
router.get('/news/feed', RSSFeed); | |
router.get('/stories/hotStories', hotJSON); | |
router.get( | |
'/stories/submit', | |
sendNonUserToNews, | |
submitNew | |
); | |
router.get( | |
'/stories/submit/new-story', | |
sendNonUserToNews, | |
preSubmit | |
); | |
router.post('/stories/preliminary', ifNoUser401, newStory); | |
router.post('/stories/', ifNoUser401, storySubmission); | |
router.get('/news/', hot); | |
router.post('/stories/search', getStories); | |
router.get('/news/:storyName', returnIndividualStory); | |
router.post('/stories/upvote/', ifNoUser401, upvote); | |
router.get('/stories/:storyName', redirectToNews); | |
app.use(router); | |
function redirectToNews(req, res) { | |
var url = req.originalUrl.replace(/^\/stories/, '/news'); | |
return res.redirect(url); | |
} | |
function hotJSON(req, res, next) { | |
var query = { | |
order: 'timePosted DESC', | |
limit: 1000 | |
}; | |
findStory(query).subscribe( | |
function(stories) { | |
var sliceVal = stories.length >= 100 ? 100 : stories.length; | |
var data = stories.sort(sortByRank).slice(0, sliceVal); | |
res.json(data); | |
}, | |
next | |
); | |
} | |
function RSSFeed(req, res, next) { | |
var query = { | |
order: 'timePosted DESC', | |
limit: 1000 | |
}; | |
findStory(query).subscribe( | |
function(stories) { | |
var sliceVal = stories.length >= 100 ? 100 : stories.length; | |
var data = stories.sort(sortByRank).slice(0, sliceVal); | |
res.set('Content-Type', 'text/xml'); | |
res.render('feed', { | |
title: 'FreeCodeCamp Camper News RSS Feed', | |
description: 'RSS Feed for FreeCodeCamp Top 100 Hot Camper News', | |
url: 'http://www.freecodecamp.com/news', | |
FeedPosts: data | |
}); | |
}, | |
next | |
); | |
} | |
function hot(req, res) { | |
return res.render('stories/index', { | |
title: 'Top Stories on Camper News', | |
page: 'hot' | |
}); | |
} | |
function submitNew(req, res) { | |
if (!req.user.isGithubCool) { | |
req.flash('errors', { | |
msg: 'You must link GitHub with your account before you can post' + | |
' on Camper News.' | |
}); | |
return res.redirect('/news'); | |
} | |
return res.render('stories/index', { | |
title: 'Submit a new story to Camper News', | |
page: 'submit' | |
}); | |
} | |
function preSubmit(req, res) { | |
var data = req.query; | |
if (typeof data.url !== 'string') { | |
req.flash('errors', { msg: 'No URL supplied with story' }); | |
return res.redirect('/news'); | |
} | |
var cleanedData = cleanData(data.url); | |
if (data.url.replace(/&/g, '&') !== cleanedData) { | |
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 = unDasherize(dashedName); | |
findOneStory({ where: { storyLink: storyName } }).subscribe( | |
function(story) { | |
if (!story) { | |
req.flash('errors', { | |
msg: "404: We couldn't find a story with that name. " + | |
'Please double check the name.' | |
}); | |
return res.redirect('/news'); | |
} | |
var dashedNameFull = story.storyLink.toLowerCase() | |
.replace(/\s+/g, ' ') | |
.replace(/\s/g, '-'); | |
if (dashedNameFull !== dashedName) { | |
return res.redirect('../stories/' + dashedNameFull); | |
} | |
var username = req.user ? req.user.username : ''; | |
// true if any of votes are made by user | |
var userVoted = story.upVotes.some(function(upvote) { | |
return upvote.upVotedByUsername === username; | |
}); | |
return res.render('stories/index', { | |
title: story.headline, | |
link: story.link, | |
originalStoryLink: dashedName, | |
author: story.author, | |
rank: story.upVotes.length, | |
upVotes: story.upVotes, | |
id: story.id, | |
timeAgo: moment(story.timePosted).fromNow(), | |
image: story.image, | |
page: 'show', | |
storyMetaDescription: story.metaDescription, | |
hasUserVoted: userVoted | |
}); | |
}, | |
next | |
); | |
} | |
function userStories({ body: { search = '' } = {} }, res, next) { | |
if (!search || typeof search !== 'string') { | |
return res.sendStatus(404); | |
} | |
return app.dataSources.db.connector | |
.collection('story') | |
.find({ | |
'author.username': search.toLowerCase().replace('$', '') | |
}) | |
.toArray(function(err, items) { | |
if (err) { | |
return next(err); | |
} | |
if (items && items.length !== 0) { | |
return res.json(items.sort(sortByRank)); | |
} | |
return res.sendStatus(404); | |
}); | |
} | |
function getStories({ body: { search = '' } = {} }, res, next) { | |
if (!search || typeof search !== 'string') { | |
return res.sendStatus(404); | |
} | |
const query = { | |
'$text': { | |
// protect against NoSQL injection | |
'$search': search.replace('$', '') | |
} | |
}; | |
const fields = { | |
headline: 1, | |
timePosted: 1, | |
link: 1, | |
description: 1, | |
rank: 1, | |
upVotes: 1, | |
author: 1, | |
image: 1, | |
storyLink: 1, | |
metaDescription: 1, | |
textScore: { | |
$meta: 'textScore' | |
} | |
}; | |
const options = { | |
sort: { | |
textScore: { | |
$meta: 'textScore' | |
} | |
} | |
}; | |
return app.dataSources.db.connector | |
.collection('story') | |
.find(query, fields, options) | |
.toArray(function(err, items) { | |
if (err) { | |
return next(err); | |
} | |
if (items && items.length !== 0) { | |
return res.json(items); | |
} | |
return res.sendStatus(404); | |
}); | |
} | |
function upvote(req, res, next) { | |
const { id } = req.body; | |
var story$ = findStoryById(id).shareReplay(); | |
story$.flatMap(function(story) { | |
// find story author | |
return findUserById(story.author.userId); | |
}) | |
.flatMap(function(user) { | |
// if user deletes account then this will not exist | |
if (user) { | |
user.progressTimestamps.push({ | |
timestamp: Date.now() | |
}); | |
} | |
return saveUser(user); | |
}) | |
.flatMap(function() { | |
return story$; | |
}) | |
.flatMap(function(story) { | |
debug('upvoting'); | |
story.rank += 1; | |
story.upVotes.push({ | |
upVotedBy: req.user.id, | |
upVotedByUsername: req.user.username | |
}); | |
return saveInstance(story); | |
}) | |
.subscribe( | |
function(story) { | |
return res.send(story); | |
}, | |
next | |
); | |
} | |
function newStory(req, res, next) { | |
if (!req.user.isGithubCool) { | |
req.flash('errors', { | |
msg: 'You must authenticate with Github to post to Camper News' | |
}); | |
return res.redirect('/news'); | |
} | |
var url = req.body.data.url; | |
if (!validator.isURL(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; | |
} | |
return findStory({ where: { link: url } }) | |
.map(function(stories) { | |
if (stories.length) { | |
return { | |
alreadyPosted: true, | |
storyURL: '/stories/' + stories.pop().storyLink | |
}; | |
} | |
return { | |
alreadyPosted: false, | |
storyURL: url | |
}; | |
}) | |
.flatMap(function(data) { | |
if (data.alreadyPosted) { | |
return Rx.Observable.just(data); | |
} | |
return Rx.Observable.fromNodeCallback(getURLTitle)(data.storyURL) | |
.map(function(story) { | |
return { | |
alreadyPosted: false, | |
storyURL: data.storyURL, | |
storyTitle: story.title, | |
storyImage: story.image, | |
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) { | |
if (req.user.isBanned) { | |
return res.json({ | |
isBanned: true | |
}); | |
} | |
var data = req.body.data; | |
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; | |
} | |
var query = { | |
storyLink: { | |
like: ('^' + storyLink + '(?: [0-9]+)?$'), | |
options: 'i' | |
} | |
}; | |
var savedStory = countStories(query) | |
.flatMap(function(storyCount) { | |
// 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 newStory = new Story({ | |
headline: cleanData(data.headline), | |
timePosted: Date.now(), | |
link: link, | |
description: cleanData(data.description), | |
rank: 1, | |
upVotes: [({ | |
upVotedBy: req.user.id, | |
upVotedByUsername: req.user.username | |
})], | |
author: { | |
picture: req.user.picture, | |
userId: req.user.id, | |
username: req.user.username | |
}, | |
image: data.image, | |
storyLink: storyLink, | |
metaDescription: data.storyMetaDescription | |
}); | |
return saveInstance(newStory); | |
}); | |
req.user.progressTimestamps.push({ | |
timestamp: Date.now() | |
}); | |
return saveUser(req.user) | |
.flatMap(savedStory) | |
.subscribe( | |
function(story) { | |
res.json({ | |
storyLink: dasherize(story.storyLink) | |
}); | |
}, | |
next | |
); | |
} | |
}; |