The DTP Sites web app development engine. https://digitaltelepresence.com/
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.
 
 
 
 

416 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',
create: (dtp) => { return new PostService(dtp); },
};