You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
417 lines
11 KiB
417 lines
11 KiB
// post.js
|
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
|
// License: Apache-2.0
|
|
|
|
'use strict';
|
|
|
|
const striptags = require('striptags');
|
|
const slug = require('slug');
|
|
|
|
const { SiteService, SiteError } = require('../../lib/site-lib');
|
|
|
|
const mongoose = require('mongoose');
|
|
const ObjectId = mongoose.Types.ObjectId;
|
|
|
|
const Post = mongoose.model('Post');
|
|
const Comment = mongoose.model('Comment'); // jshint ignore:line
|
|
|
|
const moment = require('moment');
|
|
|
|
class PostService extends SiteService {
|
|
|
|
constructor (dtp) {
|
|
super(dtp, module.exports);
|
|
this.populatePost = [
|
|
{
|
|
path: 'author',
|
|
select: 'core coreUserId username username_lc displayName bio picture',
|
|
populate: [
|
|
{
|
|
path: 'core',
|
|
select: 'created updated flags meta',
|
|
strictPopulate: false,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
path: 'image',
|
|
},
|
|
];
|
|
}
|
|
|
|
async createPlaceholder (author) {
|
|
const NOW = new Date();
|
|
|
|
if (!author.flags.isAdmin){
|
|
if (!author.permissions.canAuthorPosts) {
|
|
throw new SiteError(403, 'You are not permitted to author posts');
|
|
}
|
|
}
|
|
|
|
let post = new Post();
|
|
post.created = NOW;
|
|
post.authorType = author.type;
|
|
post.author = author._id;
|
|
post.title = "New Draft Post";
|
|
post.slug = `draft-post-${post._id}`;
|
|
await post.save();
|
|
|
|
post = post.toObject();
|
|
post.author = author; // self-populate instead of calling db
|
|
|
|
return post;
|
|
}
|
|
|
|
async create (author, postDefinition) {
|
|
const NOW = new Date();
|
|
|
|
if (!author.flags.isAdmin){
|
|
if (!author.permissions.canAuthorPosts) {
|
|
throw new SiteError(403, 'You are not permitted to author posts');
|
|
}
|
|
if ((postDefinition.status === 'published') && !author.permissions.canPublishPosts) {
|
|
throw new SiteError(403, 'You are not permitted to publish posts');
|
|
}
|
|
}
|
|
|
|
if (postDefinition.tags) {
|
|
postDefinition.tags = postDefinition.tags.split(',').map((tag) => striptags(tag.trim()));
|
|
} else {
|
|
postDefinition.tags = [ ];
|
|
}
|
|
|
|
const post = new Post();
|
|
post.created = NOW;
|
|
post.authorType = author.type;
|
|
post.author = author._id;
|
|
post.title = striptags(postDefinition.title.trim());
|
|
post.slug = this.createPostSlug(post._id, post.title);
|
|
post.summary = striptags(postDefinition.summary.trim());
|
|
post.content = postDefinition.content.trim();
|
|
post.tags = postDefinition.tags;
|
|
post.status = postDefinition.status || 'draft';
|
|
post.flags = {
|
|
enableComments: postDefinition.enableComments === 'on',
|
|
isFeatured: postDefinition.isFeatured === 'on',
|
|
};
|
|
|
|
await post.save();
|
|
|
|
return post.toObject();
|
|
}
|
|
|
|
async update (user, post, postDefinition) {
|
|
const { coreNode: coreNodeService } = this.dtp.services;
|
|
|
|
if (!user.flags.isAdmin){
|
|
if (!user.permissions.canAuthorPosts) {
|
|
throw new SiteError(403, 'You are not permitted to author posts');
|
|
}
|
|
}
|
|
const NOW = new Date();
|
|
const updateOp = {
|
|
$setOnInsert: {
|
|
created: NOW,
|
|
},
|
|
$set: {
|
|
updated: NOW,
|
|
},
|
|
};
|
|
|
|
if (postDefinition.title) {
|
|
updateOp.$set.title = striptags(postDefinition.title.trim());
|
|
updateOp.$set.slug = this.createPostSlug(post._id, updateOp.$set.title);
|
|
}
|
|
if (postDefinition.slug) {
|
|
let postSlug = striptags(slug(postDefinition.slug.trim())).split('-');
|
|
while (ObjectId.isValid(postSlug[postSlug.length - 1])) {
|
|
postSlug.pop();
|
|
}
|
|
postSlug = postSlug.splice(0, 4);
|
|
postSlug.push(post._id.toString());
|
|
updateOp.$set.slug = `${postSlug.join('-')}`;
|
|
}
|
|
if (postDefinition.summary) {
|
|
updateOp.$set.summary = striptags(postDefinition.summary.trim());
|
|
}
|
|
if (postDefinition.content) {
|
|
updateOp.$set.content = postDefinition.content.trim();
|
|
}
|
|
|
|
await this.updateTags(post._id, postDefinition.tags);
|
|
|
|
if (!postDefinition.status) {
|
|
throw new SiteError(406, 'Must include post status');
|
|
}
|
|
|
|
// const postWillBeUnpublished = post.status === 'published' && postDefinition.status !== 'published';
|
|
const postWillBePublished = post.status !== 'published' && postDefinition.status === 'published';
|
|
|
|
if (postWillBePublished) {
|
|
|
|
if (!user.flags.isAdmin && !user.permissions.canPublishPosts) {
|
|
throw new SiteError(403, 'You are not permitted to publish posts');
|
|
}
|
|
}
|
|
updateOp.$set.status = striptags(postDefinition.status.trim());
|
|
|
|
updateOp.$set['flags.enableComments'] = postDefinition.enableComments === 'on';
|
|
updateOp.$set['flags.isFeatured'] = postDefinition.isFeatured === 'on';
|
|
|
|
const OLD_STATUS = post.status;
|
|
const postAuthor = post.author;
|
|
|
|
post = await Post.findOneAndUpdate(
|
|
{ _id: post._id },
|
|
updateOp,
|
|
{ upsert: true, new: true },
|
|
);
|
|
|
|
const CORE_SCHEME = coreNodeService.getCoreRequestScheme();
|
|
const { site } = this.dtp.config;
|
|
|
|
if ((OLD_STATUS === 'draft') && (updateOp.$set.status === 'published')) {
|
|
const event = {
|
|
action: 'post-create',
|
|
emitter: postAuthor,
|
|
label: updateOp.$set.title,
|
|
content: updateOp.$set.summary,
|
|
href: `${CORE_SCHEME}://${site.domain}/post/${updateOp.$set.slug}`,
|
|
};
|
|
if (post.image) {
|
|
event.thumbnail = `${CORE_SCHEME}://${site.domain}/image/${post.image}`;
|
|
}
|
|
await coreNodeService.sendKaleidoscopeEvent(event);
|
|
}
|
|
}
|
|
|
|
// pass the post._id and its tags to function
|
|
async updateTags (id, tags) {
|
|
if (tags) {
|
|
tags = tags.split(',').map((tag) => striptags(tag.trim().toLowerCase()));
|
|
} else {
|
|
tags = [ ];
|
|
}
|
|
|
|
const NOW = new Date();
|
|
const updateOp = {
|
|
$setOnInsert: {
|
|
created: NOW,
|
|
},
|
|
$set: {
|
|
updated: NOW,
|
|
},
|
|
};
|
|
updateOp.$set.tags = tags;
|
|
await Post.findOneAndUpdate(
|
|
{ _id: id },
|
|
updateOp,
|
|
);
|
|
}
|
|
|
|
async getByTags (tag, pagination, status = ['published']) {
|
|
if (!Array.isArray(status)) {
|
|
status = [status];
|
|
}
|
|
// const search = { status: { $in: status }, tags: tag };
|
|
const posts = await Post.find( { status: { $in: status }, tags: tag })
|
|
.sort({ created: -1 })
|
|
.skip(pagination.skip)
|
|
.limit(pagination.cpp)
|
|
.populate(this.populatePost);
|
|
const totalPosts = await Post.countDocuments({ status: { $in: status }, tags: tag });
|
|
return {posts, totalPosts};
|
|
}
|
|
|
|
async updateImage (user, post, file) {
|
|
const { image: imageService } = this.dtp.services;
|
|
|
|
if (!user.permissions.canAuthorPosts) {
|
|
throw new SiteError(403, 'You are not permitted to change posts');
|
|
}
|
|
|
|
const images = [
|
|
{
|
|
width: 960,
|
|
height: 540,
|
|
format: 'jpeg',
|
|
formatParameters: {
|
|
quality: 80,
|
|
},
|
|
},
|
|
];
|
|
await imageService.processImageFile(user, file, images);
|
|
await Post.updateOne(
|
|
{ _id: post._id },
|
|
{
|
|
$set: {
|
|
image: images[0].image._id,
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
async getPosts (pagination, status = ['published'], count = false) {
|
|
if (!Array.isArray(status)) {
|
|
status = [status];
|
|
}
|
|
|
|
var search = {
|
|
status: { $in: status },
|
|
'flags.isFeatured': false
|
|
};
|
|
if ( count ) {
|
|
search = {
|
|
status: { $in: status },
|
|
};
|
|
}
|
|
const posts = await Post
|
|
.find(search)
|
|
.sort({ created: -1 })
|
|
.skip(pagination.skip)
|
|
.limit(pagination.cpp)
|
|
.populate(this.populatePost)
|
|
.lean();
|
|
posts.forEach((post) => {
|
|
post.author.type = post.authorType;
|
|
});
|
|
if (count) {
|
|
const totalPostCount = await Post
|
|
.countDocuments(search);
|
|
return { posts, totalPostCount };
|
|
}
|
|
return posts;
|
|
}
|
|
|
|
async getFeaturedPosts (maxCount = 3) {
|
|
const posts = await Post
|
|
.find({ status: 'published', 'flags.isFeatured': true })
|
|
.sort({ created: -1 })
|
|
.limit(maxCount)
|
|
.lean();
|
|
return posts;
|
|
}
|
|
|
|
async getAllPosts (pagination) {
|
|
const posts = await Post
|
|
.find()
|
|
.sort({ created: -1 })
|
|
.skip(pagination.skip)
|
|
.limit(pagination.cpp)
|
|
.populate(this.populatePost)
|
|
.lean();
|
|
const totalPostCount = await Post.countDocuments();
|
|
return {posts, totalPostCount};
|
|
}
|
|
|
|
async getForAuthor (author, status, pagination) {
|
|
if (!Array.isArray(status)) {
|
|
status = [status];
|
|
}
|
|
|
|
pagination = Object.assign({ skip: 0, cpp: 5 }, pagination);
|
|
|
|
const search = {
|
|
authorType: author.type,
|
|
author: author._id,
|
|
status: { $in: status },
|
|
};
|
|
|
|
const posts = await Post
|
|
.find(search)
|
|
.sort({ created: -1 })
|
|
.skip(pagination.skip)
|
|
.limit(pagination.cpp)
|
|
.populate(this.populatePost)
|
|
.lean();
|
|
|
|
posts.forEach((post) => {
|
|
post.author.type = post.authorType;
|
|
});
|
|
|
|
const totalPostCount = await Post.countDocuments(search);
|
|
|
|
return { posts, totalPostCount };
|
|
}
|
|
|
|
async getCommentsForAuthor (author, pagination) {
|
|
const { comment: commentService } = this.dtp.services;
|
|
|
|
const NOW = new Date();
|
|
const START_DATE = moment(NOW).subtract(3, 'days').toDate();
|
|
|
|
const published = await this.getForAuthor(author, 'published', 5);
|
|
if (!published || (published.length === 0)) {
|
|
return [ ];
|
|
}
|
|
|
|
const postIds = published.posts.map((post) => post._id);
|
|
const search = { // index: 'comment_replies'
|
|
created: { $gt: START_DATE },
|
|
status: 'published',
|
|
resource: { $in: postIds },
|
|
};
|
|
|
|
let q = Comment
|
|
.find(search)
|
|
.sort({ created: -1 });
|
|
|
|
if (pagination) {
|
|
q = q.skip(pagination.skip).limit(pagination.cpp);
|
|
} else {
|
|
q = q.limit(20);
|
|
}
|
|
|
|
const comments = await q
|
|
.populate(commentService.populateCommentWithResource)
|
|
.lean();
|
|
const totalCommentCount = await Comment.countDocuments(search);
|
|
return { comments, totalCommentCount };
|
|
}
|
|
|
|
async getById (postId) {
|
|
const post = await Post
|
|
.findById(postId)
|
|
.select('+content')
|
|
.populate(this.populatePost)
|
|
.lean();
|
|
|
|
post.author.type = post.authorType;
|
|
|
|
return post;
|
|
}
|
|
|
|
async getBySlug (postSlug) {
|
|
const slugParts = postSlug.split('-');
|
|
const postId = slugParts[slugParts.length - 1];
|
|
return this.getById(postId);
|
|
}
|
|
|
|
async deletePost (post) {
|
|
const {
|
|
comment: commentService,
|
|
contentReport: contentReportService,
|
|
} = this.dtp.services;
|
|
|
|
await commentService.deleteForResource(post);
|
|
await contentReportService.removeForResource(post);
|
|
|
|
this.log.info('deleting post', { postId: post._id });
|
|
await Post.deleteOne({ _id: post._id });
|
|
}
|
|
|
|
createPostSlug (postId, postTitle) {
|
|
if ((typeof postTitle !== 'string') || (postTitle.length < 1)) {
|
|
throw new Error('Invalid input for making a post slug');
|
|
}
|
|
const postSlug = slug(postTitle.trim().toLowerCase()).split('-').slice(0, 4).join('-');
|
|
return `${postSlug}-${postId}`;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
slug: 'post',
|
|
name: 'post',
|
|
className: 'PostService',
|
|
create: (dtp) => { return new PostService(dtp); },
|
|
}; |