2015-06-25 22:03:46 +00:00
|
|
|
var Rx = require('rx'),
|
|
|
|
assign = require('object.assign'),
|
2015-06-01 23:23:53 +00:00
|
|
|
sanitizeHtml = require('sanitize-html'),
|
|
|
|
moment = require('moment'),
|
|
|
|
mongodb = require('mongodb'),
|
2015-06-26 07:35:13 +00:00
|
|
|
debug = require('debug')('freecc:cntr:story'),
|
2015-06-04 00:14:45 +00:00
|
|
|
utils = require('../utils'),
|
2015-06-25 22:03:46 +00:00
|
|
|
observeMethod = require('../utils/rx').observeMethod,
|
|
|
|
saveUser = require('../utils/rx').saveUser,
|
|
|
|
saveInstance = require('../utils/rx').saveInstance,
|
2015-06-01 23:23:53 +00:00
|
|
|
MongoClient = mongodb.MongoClient,
|
2015-07-15 10:41:09 +00:00
|
|
|
validator = require('validator'),
|
2015-06-03 23:19:23 +00:00
|
|
|
secrets = require('../../config/secrets');
|
|
|
|
|
2015-06-25 22:03:46 +00:00
|
|
|
var foundationDate = 1413298800000;
|
|
|
|
var time48Hours = 172800000;
|
|
|
|
|
|
|
|
var unDasherize = utils.unDasherize;
|
|
|
|
var dasherize = utils.dasherize;
|
|
|
|
var getURLTitle = utils.getURLTitle;
|
2015-08-19 02:48:42 +00:00
|
|
|
var ifNoUser401 = require('../utils/middleware').ifNoUser401;
|
2015-06-25 22:03:46 +00:00
|
|
|
|
|
|
|
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, '"');
|
|
|
|
}
|
|
|
|
|
2015-06-03 23:19:23 +00:00
|
|
|
module.exports = function(app) {
|
2015-06-03 23:31:42 +00:00
|
|
|
var router = app.loopback.Router();
|
2015-06-03 23:19:23 +00:00
|
|
|
var User = app.models.User;
|
2015-06-25 22:03:46 +00:00
|
|
|
var findUserById = observeMethod(User, 'findById');
|
|
|
|
|
2015-06-03 23:19:23 +00:00
|
|
|
var Story = app.models.Story;
|
2015-06-25 22:03:46 +00:00
|
|
|
var findStory = observeMethod(Story, 'find');
|
|
|
|
var findOneStory = observeMethod(Story, 'findOne');
|
|
|
|
var findStoryById = observeMethod(Story, 'findById');
|
|
|
|
var countStories = observeMethod(Story, 'count');
|
|
|
|
|
2015-08-01 02:51:21 +00:00
|
|
|
router.get('/news/hot', hotJSON);
|
2015-08-01 03:10:30 +00:00
|
|
|
router.get('/stories/hotStories', hotJSON);
|
2015-06-03 23:19:23 +00:00
|
|
|
router.get('/stories/submit', submitNew);
|
|
|
|
router.get('/stories/submit/new-story', preSubmit);
|
2015-08-19 02:48:42 +00:00
|
|
|
router.post('/stories/preliminary', ifNoUser401, newStory);
|
|
|
|
router.post('/stories/', ifNoUser401, storySubmission);
|
2015-06-26 06:39:38 +00:00
|
|
|
router.get('/news/', hot);
|
2015-06-03 23:19:23 +00:00
|
|
|
router.post('/stories/search', getStories);
|
2015-06-26 06:39:38 +00:00
|
|
|
router.get('/news/:storyName', returnIndividualStory);
|
2015-08-19 02:48:42 +00:00
|
|
|
router.post('/stories/upvote/', ifNoUser401, upvote);
|
2015-06-26 06:39:38 +00:00
|
|
|
router.get('/stories/:storyName', redirectToNews);
|
2015-06-03 23:19:23 +00:00
|
|
|
|
2015-06-03 23:31:42 +00:00
|
|
|
app.use(router);
|
|
|
|
|
2015-06-26 06:39:38 +00:00
|
|
|
function redirectToNews(req, res) {
|
|
|
|
var url = req.originalUrl.replace(/^\/stories/, '/news');
|
|
|
|
return res.redirect(url);
|
|
|
|
}
|
|
|
|
|
2015-06-09 18:54:45 +00:00
|
|
|
function hotJSON(req, res, next) {
|
2015-06-25 22:03:46 +00:00
|
|
|
var query = {
|
2015-06-23 23:28:16 +00:00
|
|
|
order: 'timePosted DESC',
|
|
|
|
limit: 1000
|
2015-06-25 22:03:46 +00:00
|
|
|
};
|
|
|
|
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
|
|
|
|
);
|
2015-06-03 23:19:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function hot(req, res) {
|
|
|
|
return res.render('stories/index', {
|
|
|
|
title: 'Hot stories currently trending on Camper News',
|
|
|
|
page: 'hot'
|
|
|
|
});
|
|
|
|
}
|
2015-03-07 08:42:22 +00:00
|
|
|
|
2015-06-03 23:19:23 +00:00
|
|
|
function submitNew(req, res) {
|
|
|
|
return res.render('stories/index', {
|
|
|
|
title: 'Submit a new story to Camper News',
|
|
|
|
page: 'submit'
|
|
|
|
});
|
|
|
|
}
|
2015-03-03 10:50:16 +00:00
|
|
|
|
2015-08-18 22:54:58 +00:00
|
|
|
function preSubmit(req, res, next) {
|
2015-06-03 23:19:23 +00:00
|
|
|
var data = req.query;
|
2015-08-18 22:54:58 +00:00
|
|
|
if (typeof data.url !== 'string') {
|
|
|
|
req.flash('errors', { msg: 'No URL supplied with story' });
|
|
|
|
return next(new TypeError('No URL supplied with story'));
|
|
|
|
}
|
2015-06-25 22:03:46 +00:00
|
|
|
var cleanedData = cleanData(data.url);
|
2015-03-03 10:50:16 +00:00
|
|
|
|
2015-06-25 22:03:46 +00:00
|
|
|
if (data.url.replace(/&/g, '&') !== cleanedData) {
|
2015-03-22 11:20:08 +00:00
|
|
|
req.flash('errors', {
|
2015-06-03 23:19:23 +00:00
|
|
|
msg: 'The data for this post is malformed'
|
|
|
|
});
|
|
|
|
return res.render('stories/index', {
|
|
|
|
page: 'stories/submit'
|
2015-03-22 11:20:08 +00:00
|
|
|
});
|
|
|
|
}
|
2015-03-03 10:50:16 +00:00
|
|
|
|
2015-06-03 23:19:23 +00:00
|
|
|
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
|
|
|
|
});
|
|
|
|
}
|
2015-03-03 10:50:16 +00:00
|
|
|
|
2015-06-03 23:19:23 +00:00
|
|
|
function returnIndividualStory(req, res, next) {
|
|
|
|
var dashedName = req.params.storyName;
|
2015-06-25 22:03:46 +00:00
|
|
|
var storyName = unDasherize(dashedName);
|
2015-06-03 23:19:23 +00:00
|
|
|
|
2015-06-25 22:03:46 +00:00
|
|
|
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.'
|
|
|
|
});
|
2015-06-26 07:16:18 +00:00
|
|
|
return res.redirect('/news');
|
|
|
|
}
|
2015-06-03 23:19:23 +00:00
|
|
|
|
2015-06-26 07:16:18 +00:00
|
|
|
var dashedNameFull = story.storyLink.toLowerCase()
|
|
|
|
.replace(/\s+/g, ' ')
|
|
|
|
.replace(/\s/g, '-');
|
2015-03-03 13:03:33 +00:00
|
|
|
|
2015-06-26 07:16:18 +00:00
|
|
|
if (dashedNameFull !== dashedName) {
|
|
|
|
return res.redirect('../stories/' + dashedNameFull);
|
2015-06-25 22:03:46 +00:00
|
|
|
}
|
2015-06-03 23:19:23 +00:00
|
|
|
|
2015-06-26 06:00:49 +00:00
|
|
|
var username = req.user ? req.user.username : '';
|
2015-06-25 22:03:46 +00:00
|
|
|
// true if any of votes are made by user
|
|
|
|
var userVoted = story.upVotes.some(function(upvote) {
|
2015-06-26 06:00:49 +00:00
|
|
|
return upvote.upVotedByUsername === username;
|
2015-06-03 23:19:23 +00:00
|
|
|
});
|
|
|
|
|
2015-06-25 22:03:46 +00:00
|
|
|
res.render('stories/index', {
|
|
|
|
title: story.headline,
|
|
|
|
link: story.link,
|
|
|
|
originalStoryLink: dashedName,
|
|
|
|
originalStoryAuthorEmail: story.author.email || '',
|
|
|
|
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
|
2015-06-03 23:19:23 +00:00
|
|
|
});
|
2015-06-25 22:03:46 +00:00
|
|
|
},
|
|
|
|
next
|
|
|
|
);
|
2015-06-03 23:19:23 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function getStories(req, res, next) {
|
|
|
|
MongoClient.connect(secrets.db, function(err, database) {
|
2015-03-22 11:20:08 +00:00
|
|
|
if (err) {
|
2015-03-25 17:28:04 +00:00
|
|
|
return next(err);
|
2015-03-22 11:20:08 +00:00
|
|
|
}
|
2015-06-03 23:19:23 +00:00
|
|
|
database.collection('stories').find({
|
|
|
|
'$text': {
|
2015-06-09 20:57:23 +00:00
|
|
|
'$search': req.body.data ? req.body.data.searchValue : ''
|
2015-06-03 23:19:23 +00:00
|
|
|
}
|
|
|
|
}, {
|
|
|
|
headline: 1,
|
|
|
|
timePosted: 1,
|
|
|
|
link: 1,
|
|
|
|
description: 1,
|
|
|
|
rank: 1,
|
|
|
|
upVotes: 1,
|
|
|
|
author: 1,
|
|
|
|
image: 1,
|
|
|
|
storyLink: 1,
|
|
|
|
metaDescription: 1,
|
|
|
|
textScore: {
|
|
|
|
$meta: 'textScore'
|
|
|
|
}
|
|
|
|
}, {
|
|
|
|
sort: {
|
|
|
|
textScore: {
|
|
|
|
$meta: 'textScore'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}).toArray(function(err, items) {
|
2015-05-09 23:23:45 +00:00
|
|
|
if (err) {
|
|
|
|
return next(err);
|
|
|
|
}
|
2015-06-03 23:19:23 +00:00
|
|
|
if (items !== null && items.length !== 0) {
|
|
|
|
return res.json(items);
|
|
|
|
}
|
|
|
|
return res.sendStatus(404);
|
2015-05-09 23:23:45 +00:00
|
|
|
});
|
2015-03-03 13:03:33 +00:00
|
|
|
});
|
2015-03-22 11:20:08 +00:00
|
|
|
}
|
|
|
|
|
2015-06-03 23:19:23 +00:00
|
|
|
function upvote(req, res, next) {
|
2015-08-01 02:51:21 +00:00
|
|
|
const { id } = req.body;
|
2015-06-26 07:35:13 +00:00
|
|
|
var story$ = findStoryById(id).shareReplay();
|
2015-06-25 22:03:46 +00:00
|
|
|
|
2015-06-26 07:35:13 +00:00
|
|
|
story$.flatMap(function(story) {
|
2015-06-25 22:03:46 +00:00
|
|
|
// find story author
|
|
|
|
return findUserById(story.author.userId);
|
|
|
|
})
|
|
|
|
.flatMap(function(user) {
|
|
|
|
// if user deletes account then this will not exist
|
|
|
|
if (user) {
|
2015-08-07 20:31:48 +00:00
|
|
|
user.progressTimestamps.push({
|
|
|
|
timestamp: Date.now()
|
|
|
|
});
|
2015-06-03 23:19:23 +00:00
|
|
|
}
|
2015-06-25 22:03:46 +00:00
|
|
|
return saveUser(user);
|
|
|
|
})
|
2015-06-26 07:35:13 +00:00
|
|
|
.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);
|
|
|
|
})
|
2015-06-25 22:03:46 +00:00
|
|
|
.subscribe(
|
|
|
|
function(story) {
|
|
|
|
return res.send(story);
|
|
|
|
},
|
|
|
|
next
|
2015-06-03 23:19:23 +00:00
|
|
|
);
|
2015-03-22 11:20:08 +00:00
|
|
|
}
|
2015-06-03 23:19:23 +00:00
|
|
|
|
|
|
|
function newStory(req, res, next) {
|
|
|
|
if (!req.user) {
|
|
|
|
return next(new Error('Must be logged in'));
|
2015-03-17 08:44:52 +00:00
|
|
|
}
|
2015-06-03 23:19:23 +00:00
|
|
|
var url = req.body.data.url;
|
2015-06-25 22:03:46 +00:00
|
|
|
|
2015-07-15 10:41:09 +00:00
|
|
|
if (!validator.isURL(url)) {
|
2015-03-22 11:20:08 +00:00
|
|
|
req.flash('errors', {
|
2015-06-03 23:19:23 +00:00
|
|
|
msg: "The URL you submitted doesn't appear valid"
|
2015-03-22 11:20:08 +00:00
|
|
|
});
|
|
|
|
return res.json({
|
|
|
|
alreadyPosted: true,
|
2015-06-03 23:19:23 +00:00
|
|
|
storyURL: '/stories/submit'
|
2015-03-22 11:20:08 +00:00
|
|
|
});
|
2015-06-03 23:19:23 +00:00
|
|
|
|
2015-03-09 09:30:37 +00:00
|
|
|
}
|
2015-06-03 23:19:23 +00:00
|
|
|
if (url.search(/^https?:\/\//g) === -1) {
|
|
|
|
url = 'http://' + url;
|
2015-03-07 08:42:22 +00:00
|
|
|
}
|
2015-06-25 22:03:46 +00:00
|
|
|
|
|
|
|
findStory({ where: { link: url } })
|
|
|
|
.map(function(stories) {
|
|
|
|
if (stories.length) {
|
|
|
|
return {
|
2015-06-03 23:19:23 +00:00
|
|
|
alreadyPosted: true,
|
2015-06-25 22:03:46 +00:00
|
|
|
storyURL: '/stories/' + stories.pop().storyLink
|
|
|
|
};
|
2015-06-03 23:19:23 +00:00
|
|
|
}
|
2015-06-25 22:03:46 +00:00
|
|
|
return {
|
2015-06-03 23:19:23 +00:00
|
|
|
alreadyPosted: false,
|
2015-06-25 22:03:46 +00:00
|
|
|
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
|
|
|
|
);
|
2015-03-22 11:20:08 +00:00
|
|
|
}
|
2015-06-01 23:23:53 +00:00
|
|
|
|
2015-06-03 23:19:23 +00:00
|
|
|
function storySubmission(req, res, next) {
|
|
|
|
var data = req.body.data;
|
|
|
|
if (!req.user) {
|
|
|
|
return next(new Error('Not authorized'));
|
2015-04-20 04:28:55 +00:00
|
|
|
}
|
2015-06-03 23:19:23 +00:00
|
|
|
var storyLink = data.headline
|
|
|
|
.replace(/[^a-z0-9\s]/gi, '')
|
|
|
|
.replace(/\s+/g, ' ')
|
|
|
|
.toLowerCase()
|
|
|
|
.trim();
|
2015-03-22 11:20:08 +00:00
|
|
|
|
2015-04-20 04:28:55 +00:00
|
|
|
var link = data.link;
|
2015-06-03 23:19:23 +00:00
|
|
|
|
2015-04-20 04:28:55 +00:00
|
|
|
if (link.search(/^https?:\/\//g) === -1) {
|
|
|
|
link = 'http://' + link;
|
|
|
|
}
|
2015-06-03 23:19:23 +00:00
|
|
|
|
2015-06-25 22:03:46 +00:00
|
|
|
var query = {
|
2015-06-23 23:28:16 +00:00
|
|
|
storyLink: {
|
|
|
|
like: ('^' + storyLink + '(?: [0-9]+)?$'),
|
|
|
|
options: 'i'
|
|
|
|
}
|
2015-06-25 22:03:46 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
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;
|
2015-05-09 22:53:01 +00:00
|
|
|
}
|
2015-06-25 22:03:46 +00:00
|
|
|
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,
|
|
|
|
email: req.user.email
|
|
|
|
},
|
|
|
|
image: data.image,
|
|
|
|
storyLink: storyLink,
|
|
|
|
metaDescription: data.storyMetaDescription,
|
|
|
|
originalStoryAuthorEmail: req.user.email
|
2015-06-03 23:19:23 +00:00
|
|
|
});
|
2015-06-25 22:03:46 +00:00
|
|
|
return saveInstance(newStory);
|
2015-05-09 22:53:01 +00:00
|
|
|
});
|
2015-06-25 22:03:46 +00:00
|
|
|
|
2015-08-07 20:31:48 +00:00
|
|
|
req.user.progressTimestamps.push({
|
|
|
|
timestamp: Date.now()
|
|
|
|
});
|
2015-06-25 22:03:46 +00:00
|
|
|
return saveUser(req.user)
|
|
|
|
.flatMap(savedStory)
|
|
|
|
.subscribe(
|
|
|
|
function(story) {
|
|
|
|
res.json({
|
|
|
|
storyLink: dasherize(story.storyLink)
|
|
|
|
});
|
|
|
|
},
|
|
|
|
next
|
|
|
|
);
|
2015-06-01 22:30:43 +00:00
|
|
|
}
|
2015-06-03 23:19:23 +00:00
|
|
|
};
|