Vulcan/packages/nova-posts/lib/callbacks.js

403 lines
12 KiB
JavaScript
Raw Normal View History

2016-08-08 11:18:21 +09:00
import Telescope from 'meteor/nova:lib';
2016-06-23 12:17:39 +09:00
import Posts from './collection.js'
import marked from 'marked';
2016-06-23 15:00:58 +09:00
import Users from 'meteor/nova:users';
//////////////////////////////////////////////////////
// Collection Hooks //
//////////////////////////////////////////////////////
/**
* @summary Generate HTML body and excerpt from Markdown on post insert
*/
Posts.before.insert(function (userId, doc) {
if(!!doc.body) {
const htmlBody = Telescope.utils.sanitize(marked(doc.body));
doc.htmlBody = htmlBody;
doc.excerpt = Telescope.utils.trimHTML(htmlBody,30);
}
});
/**
* @summary Generate HTML body and excerpt from Markdown when post body is updated
*/
Posts.before.update(function (userId, doc, fieldNames, modifier) {
// if body is being modified or $unset, update htmlBody too
if (Meteor.isServer && modifier.$set && modifier.$set.body) {
const htmlBody = Telescope.utils.sanitize(marked(modifier.$set.body));
modifier.$set.htmlBody = htmlBody;
modifier.$set.excerpt = Telescope.utils.trimHTML(htmlBody,30);
}
if (Meteor.isServer && modifier.$unset && (typeof modifier.$unset.body !== "undefined")) {
modifier.$unset.htmlBody = "";
modifier.$unset.excerpt = "";
}
});
/**
* @summary Generate slug when post title is updated
*/
Posts.before.update(function (userId, doc, fieldNames, modifier) {
// if title is being modified, update slug too
if (Meteor.isServer && modifier.$set && modifier.$set.title) {
2015-06-30 19:31:09 +09:00
modifier.$set.slug = Telescope.utils.slugify(modifier.$set.title);
}
});
2015-06-30 19:11:25 +09:00
/**
* @summary Disallow $rename
2015-06-30 19:11:25 +09:00
*/
Posts.before.update(function (userId, doc, fieldNames, modifier) {
if (!!modifier.$rename) {
throw new Meteor.Error("illegal $rename operator detected!");
}
});
//////////////////////////////////////////////////////
// Callbacks //
//////////////////////////////////////////////////////
2016-04-14 09:54:41 +09:00
/*
### posts.new.method
- PostsNewUserCheck
- PostsNewRateLimit
- PostsNewSubmittedPropertiesCheck
### posts.new.sync
- PostsNewDuplicateLinksCheck
- PostsNewRequiredPropertiesCheck
### posts.new.async
- PostsNewIncrementPostCount
- PostsNewUpvoteOwnPost
- PostsNewNotifications
### posts.edit.method
- PostsEditUserCheck
- PostsEditSubmittedPropertiesCheck
### posts.edit.sync
2016-09-28 13:46:59 +02:00
- PostsEditDuplicateLinksCheck
2016-04-14 09:54:41 +09:00
- PostsEditForceStickyToFalse
### posts.edit.async
- PostsEditSetPostedAt
- PostsEditRunPostApprovedCallbacks
### posts.approve.async
- PostsApprovedNotification
2016-06-23 11:40:35 +09:00
### users.remove.async
- UsersRemoveDeletePosts
2016-04-14 09:54:41 +09:00
*/
// ------------------------------------- posts.new.method -------------------------------- //
/**
* @summary Check that the current user can post
*/
2016-04-14 09:54:41 +09:00
function PostsNewUserCheck (post, user) {
// check that user can post
2016-07-19 17:30:59 +09:00
if (!user || !Users.canDo(user, "posts.new"))
2016-07-07 10:38:18 +09:00
throw new Meteor.Error(601, 'you_need_to_login_or_be_invited_to_post_new_stories');
return post;
}
2016-04-14 09:54:41 +09:00
Telescope.callbacks.add("posts.new.method", PostsNewUserCheck);
/**
* @summary Rate limiting
*/
2016-04-14 09:54:41 +09:00
function PostsNewRateLimit (post, user) {
2016-07-21 14:51:58 +09:00
if(!Users.isAdmin(user)){
var timeSinceLastPost = Users.timeSinceLast(user, Posts),
numberOfPostsInPast24Hours = Users.numberOfItemsInPast24Hours(user, Posts),
postInterval = Math.abs(parseInt(Telescope.settings.get('postInterval', 30))),
maxPostsPer24Hours = Math.abs(parseInt(Telescope.settings.get('maxPostsPerDay', 30)));
// check that user waits more than X seconds between posts
if(timeSinceLastPost < postInterval)
2016-07-04 10:42:50 +09:00
throw new Meteor.Error(604, 'please_wait'+(postInterval-timeSinceLastPost)+'seconds_before_posting_again');
// check that the user doesn't post more than Y posts per day
if(numberOfPostsInPast24Hours > maxPostsPer24Hours)
2016-07-04 10:42:50 +09:00
throw new Meteor.Error(605, 'sorry_you_cannot_submit_more_than'+maxPostsPer24Hours+'posts_per_day');
}
return post;
}
2016-04-14 09:54:41 +09:00
Telescope.callbacks.add("posts.new.method", PostsNewRateLimit);
/**
* @summary Properties
*/
2016-04-14 09:54:41 +09:00
function PostsNewSubmittedPropertiesCheck (post, user) {
// admin-only properties
// status
// postedAt
// userId
// sticky (default to false)
const schema = Posts.simpleSchema()._schema;
// go over each schema field and throw an error if it's not editable
_.keys(post).forEach(function (fieldName) {
var field = schema[fieldName];
2016-07-20 10:25:05 +09:00
if (!Users.canSubmitField (user, field)) {
2016-07-07 10:38:18 +09:00
throw new Meteor.Error("disallowed_property", 'disallowed_property_detected' + ": " + fieldName);
}
});
// note: not needed there anymore, this is already set in the next callback 'posts.new.sync' with other related properties (status, createdAt)
// if no post status has been set, set it now
// if (!post.status) {
// post.status = Posts.getDefaultStatus(user);
// }
// if no userId has been set, default to current user id
if (!post.userId) {
post.userId = user._id;
}
return post;
}
2016-04-14 09:54:41 +09:00
Telescope.callbacks.add("posts.new.method", PostsNewSubmittedPropertiesCheck);
// ------------------------------------- posts.new.sync -------------------------------- //
/**
2016-04-14 09:54:41 +09:00
* @summary Check for duplicate links
*/
2016-04-14 09:54:41 +09:00
function PostsNewDuplicateLinksCheck (post, user) {
if(!!post.url) {
2016-09-28 13:46:59 +02:00
Posts.checkForSameUrl(post.url);
2016-04-14 09:54:41 +09:00
}
return post;
}
2016-04-14 09:54:41 +09:00
Telescope.callbacks.add("posts.new.sync", PostsNewDuplicateLinksCheck);
/**
2016-04-14 09:54:41 +09:00
* @summary Check for necessary properties
*/
2016-04-14 09:54:41 +09:00
function PostsNewRequiredPropertiesCheck (post, user) {
2016-04-14 09:54:41 +09:00
// initialize default properties
const defaultProperties = {
createdAt: new Date(),
author: Users.getDisplayNameById(post.userId),
status: Posts.getDefaultStatus(user)
};
post = _.extend(defaultProperties, post);
// generate slug
post.slug = Telescope.utils.slugify(post.title);
// post is not pending and has been scheduled to be posted in the future by a moderator/admin
if (post.status !== Posts.config.STATUS_PENDING && post.postedAt && post.postedAt > post.createdAt) {
post.status = Posts.config.STATUS_SCHEDULED;
}
// if post is approved but doesn't have a postedAt date, give it a default date
// note: pending posts get their postedAt date only once theyre approved
if (Posts.isApproved(post) && !post.postedAt) {
2016-02-24 14:47:44 +09:00
post.postedAt = new Date();
}
2016-04-14 09:54:41 +09:00
2016-02-24 14:47:44 +09:00
return post;
}
2016-04-14 09:54:41 +09:00
Telescope.callbacks.add("posts.new.sync", PostsNewRequiredPropertiesCheck);
/**
* @summary Set the post's isFuture to true if necessary
*/
function PostsNewSetFuture (post, user) {
2016-07-25 10:46:43 +09:00
post.isFuture = post.postedAt && post.postedAt.getTime() > post.createdAt.getTime() + 1000; // round up to the second
return post;
}
Telescope.callbacks.add("posts.new.sync", PostsNewSetFuture);
// ------------------------------------- posts.new.async -------------------------------- //
2016-02-18 12:16:32 +09:00
/**
* @summary Increment the user's post count
*/
2016-04-14 09:54:41 +09:00
function PostsNewIncrementPostCount (post) {
var userId = post.userId;
Meteor.users.update({_id: userId}, {$inc: {"telescope.postCount": 1}});
}
2016-04-14 09:54:41 +09:00
Telescope.callbacks.add("posts.new.async", PostsNewIncrementPostCount);
2016-02-18 12:16:32 +09:00
/**
* @summary Make users upvote their own new posts
*/
2016-04-14 09:54:41 +09:00
function PostsNewUpvoteOwnPost (post) {
if (typeof Telescope.operateOnItem !== "undefined") {
2016-02-18 12:16:32 +09:00
var postAuthor = Meteor.users.findOne(post.userId);
Telescope.operateOnItem(Posts, post._id, postAuthor, "upvote");
}
}
2016-04-14 09:54:41 +09:00
Telescope.callbacks.add("posts.new.async", PostsNewUpvoteOwnPost);
2016-02-18 12:16:32 +09:00
/**
* @summary Add new post notification callback on post submit
*/
2016-04-14 09:54:41 +09:00
function PostsNewNotifications (post) {
if (typeof Telescope.notifications !== "undefined") {
var adminIds = _.pluck(Users.adminUsers({fields: {_id:1}}), '_id');
2016-05-20 09:32:37 +09:00
var notifiedUserIds = _.pluck(Users.find({'telescope.notifications_posts': true}, {fields: {_id:1}}).fetch(), '_id');
var notificationData = {
post: _.pick(post, '_id', 'userId', 'title', 'url')
};
// remove post author ID from arrays
adminIds = _.without(adminIds, post.userId);
notifiedUserIds = _.without(notifiedUserIds, post.userId);
if (post.status === Posts.config.STATUS_PENDING && !!adminIds.length) {
// if post is pending, only notify admins
2016-05-17 09:50:37 +09:00
Telescope.notifications.create(adminIds, 'newPendingPost', notificationData);
} else if (!!notifiedUserIds.length) {
// if post is approved, notify everybody
2016-05-17 09:50:37 +09:00
Telescope.notifications.create(notifiedUserIds, 'newPost', notificationData);
}
}
2016-04-14 09:54:41 +09:00
}
Telescope.callbacks.add("posts.new.async", PostsNewNotifications);
2016-04-14 09:54:41 +09:00
// ------------------------------------- posts.edit.method -------------------------------- //
2016-04-14 09:54:41 +09:00
function PostsEditUserCheck (modifier, post, user) {
// check that user can edit document
2016-07-20 10:25:05 +09:00
if (!user || !Users.canEdit(user, post)) {
2016-07-04 10:42:50 +09:00
throw new Meteor.Error(601, 'sorry_you_cannot_edit_this_post');
}
2016-04-14 09:54:41 +09:00
return modifier;
}
2016-04-14 09:54:41 +09:00
Telescope.callbacks.add("posts.edit.method", PostsEditUserCheck);
2016-02-25 17:44:43 +09:00
2016-04-14 09:54:41 +09:00
function PostsEditSubmittedPropertiesCheck (modifier, post, user) {
const schema = Posts.simpleSchema()._schema;
// go over each field and throw an error if it's not editable
// loop over each operation ($set, $unset, etc.)
_.each(modifier, function (operation) {
// loop over each property being operated on
_.keys(operation).forEach(function (fieldName) {
var field = schema[fieldName];
2016-07-20 10:25:05 +09:00
if (!Users.canEditField(user, field, post)) {
2016-07-07 10:38:18 +09:00
throw new Meteor.Error("disallowed_property", 'disallowed_property_detected' + ": " + fieldName);
2016-04-14 09:54:41 +09:00
}
});
});
return modifier;
}
Telescope.callbacks.add("posts.edit.method", PostsEditSubmittedPropertiesCheck);
// ------------------------------------- posts.edit.sync -------------------------------- //
2016-09-28 13:46:59 +02:00
/**
* @summary Check for duplicate links
*/
const PostsEditDuplicateLinksCheck = (modifier, post) => {
if(post.url !== modifier.$set.url && !!modifier.$set.url) {
Posts.checkForSameUrl(modifier.$set.url);
}
return modifier;
};
Telescope.callbacks.add("posts.edit.sync", PostsEditDuplicateLinksCheck);
/**
* @summary Force sticky to default to false when it's not specified
2016-04-14 09:54:41 +09:00
* (simpleSchema's defaultValue does not work on edit, so do it manually in callback)
*/
2016-04-14 09:54:41 +09:00
function PostsEditForceStickyToFalse (modifier, post) {
if (!modifier.$set.sticky) {
2016-05-12 11:49:15 +09:00
if (modifier.$unset && modifier.$unset.sticky) {
delete modifier.$unset.sticky;
}
modifier.$set.sticky = false;
}
return modifier;
}
2016-04-14 09:54:41 +09:00
Telescope.callbacks.add("posts.edit.sync", PostsEditForceStickyToFalse);
/**
* @summary Set status
*/
function PostsEditSetIsFuture (modifier, post) {
// if a post's postedAt date is in the future, set isFuture to true
2016-07-25 10:46:43 +09:00
modifier.$set.isFuture = modifier.$set.postedAt && modifier.$set.postedAt.getTime() > new Date().getTime() + 1000;
return modifier;
}
Telescope.callbacks.add("posts.edit.sync", PostsEditSetIsFuture);
/**
* @summary Set postedAt date
*/
function PostsEditSetPostedAt (modifier, post) {
// if post is approved but doesn't have a postedAt date, give it a default date
// note: pending posts get their postedAt date only once theyre approved
if (Posts.isApproved(post) && !post.postedAt) {
modifier.$set.postedAt = new Date();
}
return modifier;
}
Telescope.callbacks.add("posts.edit.sync", PostsEditSetPostedAt);
// ------------------------------------- posts.edit.async -------------------------------- //
2016-04-14 09:54:41 +09:00
function PostsEditRunPostApprovedCallbacks (post, oldPost) {
var now = new Date();
if (Posts.isApproved(post) && !Posts.isApproved(oldPost)) {
Telescope.callbacks.runAsync("posts.approve.async", post);
}
}
2016-04-14 09:54:41 +09:00
Telescope.callbacks.add("posts.edit.async", PostsEditRunPostApprovedCallbacks);
// ------------------------------------- posts.approve.async -------------------------------- //
/**
* @summary Add notification callback when a post is approved
*/
function PostsApprovedNotification (post) {
if (typeof Telescope.notifications !== "undefined") {
var notificationData = {
post: _.pick(post, '_id', 'userId', 'title', 'url')
};
2016-05-17 09:50:37 +09:00
Telescope.notifications.create(post.userId, 'postApproved', notificationData);
2016-04-14 09:54:41 +09:00
}
}
Telescope.callbacks.add("posts.approve.async", PostsApprovedNotification);
2016-06-23 11:40:35 +09:00
// ------------------------------------- users.remove.async -------------------------------- //
function UsersRemoveDeletePosts (user, options) {
if (options.deletePosts) {
var deletedPosts = Posts.remove({userId: userId});
} else {
// not sure if anything should be done in that scenario yet
// Posts.update({userId: userId}, {$set: {author: "\[deleted\]"}}, {multi: true});
}
}
Telescope.callbacks.add("users.remove.async", UsersRemoveDeletePosts);