// post.js // Copyright (C) 2021 Digital Telepresence, LLC // License: Apache-2.0 'use strict'; const striptags = require('striptags'); const slug = require('slug'); const { SiteService } = 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: '_id username username_lc displayName bio picture', }, { path: 'image', }, ]; } async createPlaceholder (author) { const NOW = new Date(); 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; // I'll populate it myself return post; } async create (author, postDefinition) { const NOW = new Date(); 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.status = postDefinition.status || 'draft'; post.flags = { enableComments: postDefinition.enableComments === 'on', isFeatured: postDefinition.isFeatured === 'on', }; await post.save(); return post.toObject(); } async update (post, postDefinition) { 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(); } if (postDefinition.status) { updateOp.$set.status = striptags(postDefinition.status.trim()); } updateOp.$set['flags.enableComments'] = postDefinition.enableComments === 'on'; updateOp.$set['flags.isFeatured'] = postDefinition.isFeatured === 'on'; await Post.updateOne( { _id: post._id }, updateOp, { upsert: true }, ); } async updateImage (user, post, file) { const { image: imageService } = this.dtp.services; 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']) { if (!Array.isArray(status)) { status = [status]; } const posts = await Post .find({ status: { $in: status }, 'flags.isFeatured': false }) .sort({ created: -1 }) .skip(pagination.skip) .limit(pagination.cpp) .populate(this.populatePost) .lean(); 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) .lean(); return posts; } async getForAuthor (author, status, maxCount) { let q = Post .find({ authorType: author.type, author: author._id, status, }) .sort({ created: -1 }) .populate(this.populatePost); if (maxCount) { q = q.limit(maxCount); } const posts = await q.lean(); return posts; } async getCommentsForAuthor (author, pagination) { const { comment: commentService } = this.dtp.services; const NOW = new Date(); const START_DATE = moment(NOW).subtract(3, 'days').toDate(); const posts = await this.getForAuthor(author, 'published', 5); if (!posts || (posts.length === 0)) { return [ ]; } const postIds = posts.map((post) => post._id); let q = Comment .find({ // index: 'comment_replies' created: { $gt: START_DATE }, status: 'published', resource: { $in: postIds }, }) .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(); return comments; } async getById (postId) { const post = await Post .findById(postId) .select('+content') .populate(this.populatePost) .lean(); 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); }, };