Add caching to news
Deprecate story creation
This commit is contained in:
@ -1,27 +1,11 @@
|
||||
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 moment from 'moment';
|
||||
|
||||
import {
|
||||
ifNoUser401,
|
||||
ifNoUserRedirectTo
|
||||
} from '../utils/middleware';
|
||||
import { unDasherize } from '../utils';
|
||||
import { observeMethod } from '../utils/rx';
|
||||
|
||||
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
|
||||
@ -39,81 +23,54 @@ function sortByRank(a, b) {
|
||||
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');
|
||||
const router = app.loopback.Router();
|
||||
const Story = app.models.Story;
|
||||
const findStory = observeMethod(Story, 'find');
|
||||
const findOneStory = observeMethod(Story, 'findOne');
|
||||
const query = {
|
||||
order: 'timePosted DESC',
|
||||
limit: 1000
|
||||
};
|
||||
const storiesData$ = findStory(query)
|
||||
.map(stories => {
|
||||
const sliceVal = stories.length >= 100 ? 100 : stories.length;
|
||||
return stories.sort(sortByRank).slice(0, sliceVal);
|
||||
})
|
||||
.shareReplay();
|
||||
|
||||
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);
|
||||
const redirectToNews = (req, res) => res.redirect('/news');
|
||||
const deprecated = (req, res) => res.sendStatus(410);
|
||||
router.post('/news/userstories', deprecated);
|
||||
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.post('/stories/search', getStories);
|
||||
router.get('/stories/submit', redirectToNews);
|
||||
router.get('/stories/submit/new-story', redirectToNews);
|
||||
router.post('/stories/preliminary', deprecated);
|
||||
router.post('/stories/', deprecated);
|
||||
router.post('/stories/search', deprecated);
|
||||
router.get('/news/:storyName', returnIndividualStory);
|
||||
router.post('/stories/upvote/', ifNoUser401, upvote);
|
||||
router.get('/stories/:storyName', redirectToNews);
|
||||
router.post('/stories/upvote/', deprecated);
|
||||
router.get('/stories/:storyName', replaceStoryWithNews);
|
||||
|
||||
app.use(router);
|
||||
|
||||
function redirectToNews(req, res) {
|
||||
function replaceStoryWithNews(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);
|
||||
},
|
||||
storiesData$.subscribe(
|
||||
stories => res.json(stories),
|
||||
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);
|
||||
storiesData$.subscribe(
|
||||
data => {
|
||||
res.set('Content-Type', 'text/xml');
|
||||
res.render('feed', {
|
||||
title: 'FreeCodeCamp Camper News RSS Feed',
|
||||
@ -126,51 +83,6 @@ module.exports = function(app) {
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
@ -217,251 +129,4 @@ module.exports = function(app) {
|
||||
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
|
||||
);
|
||||
}
|
||||
};
|
||||
|
Reference in New Issue
Block a user