Add caching to news
Deprecate story creation
This commit is contained in:
@ -1,27 +1,11 @@
|
|||||||
var Rx = require('rx'),
|
import moment from 'moment';
|
||||||
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 {
|
import { unDasherize } from '../utils';
|
||||||
ifNoUser401,
|
import { observeMethod } from '../utils/rx';
|
||||||
ifNoUserRedirectTo
|
|
||||||
} from '../utils/middleware';
|
|
||||||
|
|
||||||
const foundationDate = 1413298800000;
|
const foundationDate = 1413298800000;
|
||||||
const time48Hours = 172800000;
|
const time48Hours = 172800000;
|
||||||
|
|
||||||
const unDasherize = utils.unDasherize;
|
|
||||||
const dasherize = utils.dasherize;
|
|
||||||
const getURLTitle = utils.getURLTitle;
|
|
||||||
const sendNonUserToNews = ifNoUserRedirectTo('/news');
|
|
||||||
|
|
||||||
function hotRank(timeValue, rank) {
|
function hotRank(timeValue, rank) {
|
||||||
/*
|
/*
|
||||||
* Hotness ranking algorithm: http://amix.dk/blog/post/19588
|
* Hotness ranking algorithm: http://amix.dk/blog/post/19588
|
||||||
@ -39,81 +23,54 @@ function sortByRank(a, b) {
|
|||||||
hotRank(a.timePosted - foundationDate, a.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();
|
const router = app.loopback.Router();
|
||||||
var User = app.models.User;
|
const Story = app.models.Story;
|
||||||
var findUserById = observeMethod(User, 'findById');
|
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;
|
const redirectToNews = (req, res) => res.redirect('/news');
|
||||||
var findStory = observeMethod(Story, 'find');
|
const deprecated = (req, res) => res.sendStatus(410);
|
||||||
var findOneStory = observeMethod(Story, 'findOne');
|
router.post('/news/userstories', deprecated);
|
||||||
var findStoryById = observeMethod(Story, 'findById');
|
|
||||||
var countStories = observeMethod(Story, 'count');
|
|
||||||
|
|
||||||
router.post('/news/userstories', userStories);
|
|
||||||
router.get('/news/hot', hotJSON);
|
router.get('/news/hot', hotJSON);
|
||||||
router.get('/news/feed', RSSFeed);
|
router.get('/news/feed', RSSFeed);
|
||||||
router.get('/stories/hotStories', hotJSON);
|
router.get('/stories/hotStories', hotJSON);
|
||||||
router.get(
|
router.get('/stories/submit', redirectToNews);
|
||||||
'/stories/submit',
|
router.get('/stories/submit/new-story', redirectToNews);
|
||||||
sendNonUserToNews,
|
router.post('/stories/preliminary', deprecated);
|
||||||
submitNew
|
router.post('/stories/', deprecated);
|
||||||
);
|
router.post('/stories/search', deprecated);
|
||||||
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('/news/:storyName', returnIndividualStory);
|
router.get('/news/:storyName', returnIndividualStory);
|
||||||
router.post('/stories/upvote/', ifNoUser401, upvote);
|
router.post('/stories/upvote/', deprecated);
|
||||||
router.get('/stories/:storyName', redirectToNews);
|
router.get('/stories/:storyName', replaceStoryWithNews);
|
||||||
|
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|
||||||
function redirectToNews(req, res) {
|
function replaceStoryWithNews(req, res) {
|
||||||
var url = req.originalUrl.replace(/^\/stories/, '/news');
|
var url = req.originalUrl.replace(/^\/stories/, '/news');
|
||||||
return res.redirect(url);
|
return res.redirect(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hotJSON(req, res, next) {
|
function hotJSON(req, res, next) {
|
||||||
var query = {
|
storiesData$.subscribe(
|
||||||
order: 'timePosted DESC',
|
stories => res.json(stories),
|
||||||
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
|
next
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RSSFeed(req, res, next) {
|
function RSSFeed(req, res, next) {
|
||||||
var query = {
|
storiesData$.subscribe(
|
||||||
order: 'timePosted DESC',
|
data => {
|
||||||
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.set('Content-Type', 'text/xml');
|
||||||
res.render('feed', {
|
res.render('feed', {
|
||||||
title: 'FreeCodeCamp Camper News RSS 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) {
|
function returnIndividualStory(req, res, next) {
|
||||||
var dashedName = req.params.storyName;
|
var dashedName = req.params.storyName;
|
||||||
var storyName = unDasherize(dashedName);
|
var storyName = unDasherize(dashedName);
|
||||||
@ -217,251 +129,4 @@ module.exports = function(app) {
|
|||||||
next
|
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