import Posts from './collection.js' import marked from 'marked'; 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) { modifier.$set.slug = Telescope.utils.slugify(modifier.$set.title); } }); /** * @summary Disallow $rename */ Posts.before.update(function (userId, doc, fieldNames, modifier) { if (!!modifier.$rename) { throw new Meteor.Error("illegal $rename operator detected!"); } }); ////////////////////////////////////////////////////// // Callbacks // ////////////////////////////////////////////////////// /* ### 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 - PostsEditForceStickyToFalse ### posts.edit.async - PostsEditSetPostedAt - PostsEditRunPostApprovedCallbacks ### posts.approve.async - PostsApprovedNotification ### users.remove.async - UsersRemoveDeletePosts */ // ------------------------------------- posts.new.method -------------------------------- // /** * @summary Check that the current user can post */ function PostsNewUserCheck (post, user) { // check that user can post if (!user || !Users.canDo(user, "posts.new")) throw new Meteor.Error(601, 'you_need_to_login_or_be_invited_to_post_new_stories'); return post; } Telescope.callbacks.add("posts.new.method", PostsNewUserCheck); /** * @summary Rate limiting */ function PostsNewRateLimit (post, user) { if(!Users.is.admin(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) 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) throw new Meteor.Error(605, 'sorry_you_cannot_submit_more_than'+maxPostsPer24Hours+'posts_per_day'); } return post; } Telescope.callbacks.add("posts.new.method", PostsNewRateLimit); /** * @summary Properties */ 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]; if (!Users.canSubmitField (user, field)) { 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; } Telescope.callbacks.add("posts.new.method", PostsNewSubmittedPropertiesCheck); // ------------------------------------- posts.new.sync -------------------------------- // /** * @summary Check for duplicate links */ function PostsNewDuplicateLinksCheck (post, user) { if(!!post.url) { Posts.checkForSameUrl(post.url, user); } return post; } Telescope.callbacks.add("posts.new.sync", PostsNewDuplicateLinksCheck); /** * @summary Check for necessary properties */ function PostsNewRequiredPropertiesCheck (post, user) { // 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) { post.postedAt = new Date(); } return post; } Telescope.callbacks.add("posts.new.sync", PostsNewRequiredPropertiesCheck); /** * @summary Set the post's isFuture to true if necessary */ function PostsNewSetFuture (post, user) { post.isFuture = post.postedAt > post.createdAt; return post; } Telescope.callbacks.add("posts.new.sync", PostsNewSetFuture); // ------------------------------------- posts.new.async -------------------------------- // /** * @summary Increment the user's post count */ function PostsNewIncrementPostCount (post) { var userId = post.userId; Meteor.users.update({_id: userId}, {$inc: {"telescope.postCount": 1}}); } Telescope.callbacks.add("posts.new.async", PostsNewIncrementPostCount); /** * @summary Make users upvote their own new posts */ function PostsNewUpvoteOwnPost (post) { if (typeof Telescope.operateOnItem !== "undefined") { var postAuthor = Meteor.users.findOne(post.userId); Telescope.operateOnItem(Posts, post._id, postAuthor, "upvote"); } } Telescope.callbacks.add("posts.new.async", PostsNewUpvoteOwnPost); /** * @summary Add new post notification callback on post submit */ function PostsNewNotifications (post) { if (typeof Telescope.notifications !== "undefined") { var adminIds = _.pluck(Users.adminUsers({fields: {_id:1}}), '_id'); 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 Telescope.notifications.create(adminIds, 'newPendingPost', notificationData); } else if (!!notifiedUserIds.length) { // if post is approved, notify everybody Telescope.notifications.create(notifiedUserIds, 'newPost', notificationData); } } } Telescope.callbacks.add("posts.new.async", PostsNewNotifications); // ------------------------------------- posts.edit.method -------------------------------- // function PostsEditUserCheck (modifier, post, user) { // check that user can edit document if (!user || !Users.canEdit(user, post)) { throw new Meteor.Error(601, 'sorry_you_cannot_edit_this_post'); } return modifier; } Telescope.callbacks.add("posts.edit.method", PostsEditUserCheck); 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]; if (!Users.canEditField(user, field, post)) { throw new Meteor.Error("disallowed_property", 'disallowed_property_detected' + ": " + fieldName); } }); }); return modifier; } Telescope.callbacks.add("posts.edit.method", PostsEditSubmittedPropertiesCheck); // ------------------------------------- posts.edit.sync -------------------------------- // /** * @summary Force sticky to default to false when it's not specified * (simpleSchema's defaultValue does not work on edit, so do it manually in callback) */ function PostsEditForceStickyToFalse (modifier, post) { if (!modifier.$set.sticky) { if (modifier.$unset && modifier.$unset.sticky) { delete modifier.$unset.sticky; } modifier.$set.sticky = false; } return modifier; } 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 modifier.$set.isFuture = modifier.$set.postedAt > new Date(); 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 -------------------------------- // function PostsEditRunPostApprovedCallbacks (post, oldPost) { var now = new Date(); if (Posts.isApproved(post) && !Posts.isApproved(oldPost)) { Telescope.callbacks.runAsync("posts.approve.async", post); } } 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') }; Telescope.notifications.create(post.userId, 'postApproved', notificationData); } } Telescope.callbacks.add("posts.approve.async", PostsApprovedNotification); // ------------------------------------- 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);