107 changed files with 3317 additions and 376 deletions
@ -1,5 +1,6 @@ |
|||||
.env |
.env |
||||
|
|
||||
|
data/minio |
||||
node_modules |
node_modules |
||||
dist |
dist |
||||
data/minio |
data/minio |
||||
|
@ -0,0 +1,95 @@ |
|||||
|
// admin/content-report.js
|
||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const DTP_COMPONENT_NAME = 'admin:content-report'; |
||||
|
|
||||
|
const express = require('express'); |
||||
|
const multer = require('multer'); |
||||
|
|
||||
|
const { /*SiteError,*/ SiteController } = require('../../../lib/site-lib'); |
||||
|
|
||||
|
class ContentReportController extends SiteController { |
||||
|
|
||||
|
constructor (dtp) { |
||||
|
super(dtp, DTP_COMPONENT_NAME); |
||||
|
} |
||||
|
|
||||
|
async start ( ) { |
||||
|
const upload = multer({ dest: `/tmp/${this.dtp.config.site.domainKey}/upload` }); |
||||
|
|
||||
|
const router = express.Router(); |
||||
|
router.use(async (req, res, next) => { |
||||
|
res.locals.currentView = 'admin'; |
||||
|
res.locals.adminView = 'host'; |
||||
|
return next(); |
||||
|
}); |
||||
|
|
||||
|
router.param('reportId', this.populateReportId.bind(this)); |
||||
|
|
||||
|
router.post('/:reportId/action', upload.none(), this.postReportAction.bind(this)); |
||||
|
|
||||
|
router.get('/:reportId', this.getReportView.bind(this)); |
||||
|
|
||||
|
router.get('/', this.getIndex.bind(this)); |
||||
|
|
||||
|
return router; |
||||
|
} |
||||
|
|
||||
|
async populateReportId (req, res, next, reportId) { |
||||
|
const { contentReport: contentReportService } = this.dtp.services; |
||||
|
try { |
||||
|
res.locals.report = await contentReportService.getById(reportId); |
||||
|
return next(); |
||||
|
} catch (error) { |
||||
|
this.log.error('failed to populate content report', { reportId, error }); |
||||
|
return next(error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async postReportAction (req, res, next) { |
||||
|
const { contentReport: contentReportService } = this.dtp.services; |
||||
|
try { |
||||
|
this.log.info('postReportAction', { body: req.body }); |
||||
|
switch (req.body.verb) { |
||||
|
case 'remove': |
||||
|
await contentReportService.removeResource(res.locals.report); |
||||
|
break; |
||||
|
|
||||
|
case 'dismiss': |
||||
|
await contentReportService.setStatus(res.locals.report, 'ignored'); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
const displayList = this.createDisplayList('add-recipient'); |
||||
|
displayList.navigateTo('/admin/content-report'); |
||||
|
res.status(200).json({ success: true, displayList }); |
||||
|
} catch (error) { |
||||
|
this.log.error('failed to perform content report action', { verb: req.body.verb, error }); |
||||
|
return next(error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async getReportView (req, res) { |
||||
|
res.render('admin/content-report/view'); |
||||
|
} |
||||
|
|
||||
|
async getIndex (req, res, next) { |
||||
|
const { contentReport: contentReportService } = this.dtp.services; |
||||
|
try { |
||||
|
res.locals.pagination = this.getPaginationParameters(req, 20); |
||||
|
res.locals.reports = await contentReportService.getReports(['new'], res.locals.pagination); |
||||
|
res.render('admin/content-report/index'); |
||||
|
} catch (error) { |
||||
|
this.log.error('failed to display content report index', { error }); |
||||
|
return next(error); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = async (dtp) => { |
||||
|
let controller = new ContentReportController(dtp); |
||||
|
return controller; |
||||
|
}; |
@ -0,0 +1,57 @@ |
|||||
|
// admin/log.js
|
||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const DTP_COMPONENT_NAME = 'admin:log'; |
||||
|
const express = require('express'); |
||||
|
|
||||
|
const { SiteController } = require('../../../lib/site-lib'); |
||||
|
|
||||
|
class LogController extends SiteController { |
||||
|
|
||||
|
constructor (dtp) { |
||||
|
super(dtp, DTP_COMPONENT_NAME); |
||||
|
} |
||||
|
|
||||
|
async start ( ) { |
||||
|
const router = express.Router(); |
||||
|
router.use(async (req, res, next) => { |
||||
|
res.locals.currentView = 'admin'; |
||||
|
res.locals.adminView = 'log'; |
||||
|
return next(); |
||||
|
}); |
||||
|
|
||||
|
router.get('/', this.getIndex.bind(this)); |
||||
|
|
||||
|
return router; |
||||
|
} |
||||
|
|
||||
|
async getIndex (req, res, next) { |
||||
|
const { log: logService } = this.dtp.services; |
||||
|
try { |
||||
|
res.locals.query = req.query; |
||||
|
|
||||
|
res.locals.components = await logService.getComponentNames(); |
||||
|
res.locals.pagination = this.getPaginationParameters(req, 25); |
||||
|
|
||||
|
const search = { }; |
||||
|
if (req.query.component) { |
||||
|
search.componentName = req.query.component; |
||||
|
} |
||||
|
res.locals.logs = await logService.getRecords(search, res.locals.pagination); |
||||
|
|
||||
|
res.locals.totalLogCount = await logService.getTotalCount(); |
||||
|
|
||||
|
res.render('admin/log/index'); |
||||
|
} catch (error) { |
||||
|
return next(error); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = async (dtp) => { |
||||
|
let controller = new LogController(dtp); |
||||
|
return controller; |
||||
|
}; |
@ -0,0 +1,118 @@ |
|||||
|
// comment.js
|
||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const DTP_COMPONENT_NAME = 'comment'; |
||||
|
|
||||
|
const express = require('express'); |
||||
|
const numeral = require('numeral'); |
||||
|
|
||||
|
const { SiteController, SiteError } = require('../../lib/site-lib'); |
||||
|
|
||||
|
class CommentController extends SiteController { |
||||
|
|
||||
|
constructor (dtp) { |
||||
|
super(dtp, DTP_COMPONENT_NAME); |
||||
|
} |
||||
|
|
||||
|
async start ( ) { |
||||
|
const { dtp } = this; |
||||
|
const { limiter: limiterService, session: sessionService } = dtp.services; |
||||
|
|
||||
|
const authRequired = sessionService.authCheckMiddleware({ requiredLogin: true }); |
||||
|
|
||||
|
const router = express.Router(); |
||||
|
dtp.app.use('/comment', router); |
||||
|
|
||||
|
router.use(async (req, res, next) => { |
||||
|
res.locals.currentView = DTP_COMPONENT_NAME; |
||||
|
return next(); |
||||
|
}); |
||||
|
|
||||
|
router.param('commentId', this.populateCommentId.bind(this)); |
||||
|
|
||||
|
router.post('/:commentId/vote', authRequired, this.postVote.bind(this)); |
||||
|
|
||||
|
router.delete('/:commentId', |
||||
|
authRequired, |
||||
|
limiterService.create(limiterService.config.comment.deleteComment), |
||||
|
this.deleteComment.bind(this), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
async populateCommentId (req, res, next, commentId) { |
||||
|
const { comment: commentService } = this.dtp.services; |
||||
|
try { |
||||
|
res.locals.comment = await commentService.getById(commentId); |
||||
|
if (!res.locals.comment) { |
||||
|
return next(new SiteError(404, 'Comment not found')); |
||||
|
} |
||||
|
return next(); |
||||
|
} catch (error) { |
||||
|
this.log.error('failed to populate commentId', { commentId, error }); |
||||
|
return next(error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async postVote (req, res) { |
||||
|
const { contentVote: contentVoteService } = this.dtp.services; |
||||
|
try { |
||||
|
const displayList = this.createDisplayList('comment-vote'); |
||||
|
const { message, stats } = await contentVoteService.recordVote(req.user, 'Comment', res.locals.comment, req.body.vote); |
||||
|
displayList.setTextContent( |
||||
|
`button[data-comment-id="${res.locals.comment._id}"][data-vote="up"] span.dtp-item-value`, |
||||
|
numeral(stats.upvoteCount).format(stats.upvoteCount > 1000 ? '0,0.0a' : '0,0'), |
||||
|
); |
||||
|
displayList.setTextContent( |
||||
|
`button[data-comment-id="${res.locals.comment._id}"][data-vote="down"] span.dtp-item-value`, |
||||
|
numeral(stats.downvoteCount).format(stats.upvoteCount > 1000 ? '0,0.0a' : '0,0'), |
||||
|
); |
||||
|
displayList.showNotification(message, 'success', 'bottom-center', 3000); |
||||
|
res.status(200).json({ success: true, displayList }); |
||||
|
} catch (error) { |
||||
|
this.log.error('failed to process comment vote', { error }); |
||||
|
return res.status(error.statusCode || 500).json({ |
||||
|
success: false, |
||||
|
message: error.message, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async deleteComment (req, res) { |
||||
|
const { comment: commentService } = this.dtp.services; |
||||
|
try { |
||||
|
const displayList = this.createDisplayList('add-recipient'); |
||||
|
|
||||
|
await commentService.remove(res.locals.comment, 'removed'); |
||||
|
|
||||
|
let selector = `article[data-comment-id="${res.locals.comment._id}"] .comment-content`; |
||||
|
displayList.setTextContent(selector, 'Comment removed'); |
||||
|
|
||||
|
displayList.showNotification( |
||||
|
'Comment removed successfully', |
||||
|
'success', |
||||
|
'bottom-center', |
||||
|
5000, |
||||
|
); |
||||
|
|
||||
|
res.status(200).json({ success: true, displayList }); |
||||
|
} catch (error) { |
||||
|
this.log.error('failed to remove comment', { error }); |
||||
|
return res.status(error.statusCode || 500).json({ |
||||
|
success: false, |
||||
|
message: error.message |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = { |
||||
|
slug: 'comment', |
||||
|
name: 'comment', |
||||
|
create: async (dtp) => { |
||||
|
let controller = new CommentController(dtp); |
||||
|
return controller; |
||||
|
}, |
||||
|
}; |
@ -0,0 +1,112 @@ |
|||||
|
// content-report.js
|
||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const DTP_COMPONENT_NAME = 'content-report'; |
||||
|
|
||||
|
const express = require('express'); |
||||
|
const multer = require('multer'); |
||||
|
|
||||
|
const { SiteController } = require('../../lib/site-lib'); |
||||
|
|
||||
|
class ContentReportController extends SiteController { |
||||
|
|
||||
|
constructor (dtp) { |
||||
|
super(dtp, DTP_COMPONENT_NAME); |
||||
|
} |
||||
|
|
||||
|
async start ( ) { |
||||
|
const { dtp } = this; |
||||
|
const { limiter: limiterService, session: sessionService } = dtp.services; |
||||
|
|
||||
|
const authRequired = sessionService.authCheckMiddleware({ requiredLogin: true }); |
||||
|
const upload = multer({ dest: `/tmp/${this.dtp.config.site.domainKey}/uploads` }); |
||||
|
|
||||
|
const router = express.Router(); |
||||
|
dtp.app.use('/content-report', router); |
||||
|
|
||||
|
router.use(this.dtp.services.gabTV.channelMiddleware('mrjoeprich')); |
||||
|
router.use(async (req, res, next) => { |
||||
|
res.locals.currentView = 'content-report'; |
||||
|
return next(); |
||||
|
}); |
||||
|
|
||||
|
router.post('/comment/form', |
||||
|
limiterService.create(limiterService.config.contentReport.postCommentReportForm), |
||||
|
authRequired, |
||||
|
upload.none(), |
||||
|
this.postCommentReportForm.bind(this), |
||||
|
); |
||||
|
router.post('/comment', |
||||
|
limiterService.create(limiterService.config.contentReport.postCommentReport), |
||||
|
authRequired, |
||||
|
upload.none(), |
||||
|
this.postCommentReport.bind(this), |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
async postCommentReportForm (req, res, next) { |
||||
|
const { comment: commentService } = this.dtp.services; |
||||
|
try { |
||||
|
res.locals.comment = await commentService.getById(req.body.commentId); |
||||
|
res.locals.params = req.body; |
||||
|
res.render('comment/components/report-form'); |
||||
|
} catch (error) { |
||||
|
return next(error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async postCommentReport (req, res) { |
||||
|
const { |
||||
|
contentReport: contentReportService, |
||||
|
comment: commentService, |
||||
|
user: userService, |
||||
|
} = this.dtp.services; |
||||
|
|
||||
|
const displayList = this.createDisplayList('add-recipient'); |
||||
|
|
||||
|
try { |
||||
|
res.locals.report = await contentReportService.create(req.user, { |
||||
|
resourceType: 'Comment', |
||||
|
resourceId: req.body.commentId, |
||||
|
category: req.body.category, |
||||
|
reason: req.body.reason, |
||||
|
}); |
||||
|
displayList.showNotification('Comment reported successfully', 'success', 'bottom-center', 5000); |
||||
|
|
||||
|
if (req.body.blockAuthor === 'on') { |
||||
|
const comment = await commentService.getById(req.body.commentId); |
||||
|
await userService.blockUser(req.user._id, comment.author._id || comment.author); |
||||
|
displayList.showNotification('Comment author blocked successfully', 'success', 'bottom-center', 5000); |
||||
|
} |
||||
|
|
||||
|
res.status(200).json({ success: true, displayList }); |
||||
|
} catch (error) { |
||||
|
this.log.error('failed to post comment report', { error }); |
||||
|
if (error.code === 11000) { |
||||
|
displayList.showNotification( |
||||
|
'You already reported this comment', |
||||
|
'primary', |
||||
|
'bottom-center', |
||||
|
5000, |
||||
|
); |
||||
|
return res.status(200).json({ success: true, displayList }); |
||||
|
} |
||||
|
return res.status(error.statusCode || 500).json({ |
||||
|
success: false, |
||||
|
message: error.message, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = { |
||||
|
slug: 'content-report', |
||||
|
name: 'contentReport', |
||||
|
create: async (dtp) => { |
||||
|
let controller = new ContentReportController(dtp); |
||||
|
return controller; |
||||
|
}, |
||||
|
}; |
@ -0,0 +1,31 @@ |
|||||
|
// content-report.js
|
||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const mongoose = require('mongoose'); |
||||
|
const Schema = mongoose.Schema; |
||||
|
|
||||
|
const REPORT_STATUS_LIST = ['new','resolved','ignored']; |
||||
|
const REPORT_CATEGORY_LIST = ['spam','violence','threat','porn','doxxing','other']; |
||||
|
|
||||
|
const ContentReportSchema = new Schema({ |
||||
|
created: { type: Date, default: Date.now, required: true, index: 1, expires: '30d' }, |
||||
|
user: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' }, |
||||
|
resourceType: { type: String, enum: [ ], required: true }, |
||||
|
resource: { type: Schema.ObjectId, required: true, index: 1, refPath: 'resourceType' }, |
||||
|
status: { type: String, enum: REPORT_STATUS_LIST, required: true, index: 1 }, |
||||
|
category: { type: String, enum: REPORT_CATEGORY_LIST, required: true }, |
||||
|
reason: { type: String }, |
||||
|
}); |
||||
|
|
||||
|
ContentReportSchema.index({ |
||||
|
user: 1, |
||||
|
resource: 1, |
||||
|
}, { |
||||
|
unique: true, |
||||
|
name: 'unique_user_content_report', |
||||
|
}); |
||||
|
|
||||
|
module.exports = mongoose.model('ContentReport', ContentReportSchema); |
@ -0,0 +1,26 @@ |
|||||
|
// content-vote.js
|
||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const mongoose = require('mongoose'); |
||||
|
const Schema = mongoose.Schema; |
||||
|
|
||||
|
const ContentVoteSchema = new Schema({ |
||||
|
created: { type: Date, default: Date.now, required: true }, |
||||
|
resourceType: { type: String, required: true }, |
||||
|
resource: { type: Schema.ObjectId, required: true, refPath: 'resourceType' }, |
||||
|
user: { type: Schema.ObjectId, required: true, ref: 'User' }, |
||||
|
vote: { type: String, enum: ['up','down'], required: true }, |
||||
|
}); |
||||
|
|
||||
|
ContentVoteSchema.index({ |
||||
|
user: 1, |
||||
|
resource: 1, |
||||
|
}, { |
||||
|
unique: true, |
||||
|
name: 'unique_user_content_vote', |
||||
|
}); |
||||
|
|
||||
|
module.exports = mongoose.model('ContentVote', ContentVoteSchema); |
@ -0,0 +1,22 @@ |
|||||
|
// geo-types.js
|
||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const mongoose = require('mongoose'); |
||||
|
const Schema = mongoose.Schema; |
||||
|
|
||||
|
module.exports.GeoPoint = new Schema({ |
||||
|
type: { type: String, enum: ['Point'], default: 'Point', required: true }, |
||||
|
coordinates: { type: [Number], required: true }, |
||||
|
}); |
||||
|
|
||||
|
module.exports.GeoIp = new Schema({ |
||||
|
country: { type: String }, |
||||
|
region: { type: String }, |
||||
|
eu: { type: String }, |
||||
|
timezone: { type: String }, |
||||
|
city: { type: String }, |
||||
|
location: { type: module.exports.GeoPoint }, |
||||
|
}); |
@ -0,0 +1,29 @@ |
|||||
|
// resource-visit.js
|
||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const mongoose = require('mongoose'); |
||||
|
const Schema = mongoose.Schema; |
||||
|
|
||||
|
const { GeoIp } = require('./lib/geo-types'); |
||||
|
|
||||
|
const ResourceVisitSchema = new Schema({ |
||||
|
created: { type: Date, required: true, default: Date.now, index: -1, expires: '7d' }, |
||||
|
resourceType: { type: String, enum: ['Page','Post','User'], required: true }, |
||||
|
resource: { type: Schema.ObjectId, required: true, index: 1, ref: 'Link' }, |
||||
|
user: { type: Schema.ObjectId, ref: 'User' }, |
||||
|
geoip: { type: GeoIp }, |
||||
|
}); |
||||
|
|
||||
|
ResourceVisitSchema.index({ |
||||
|
user: 1, |
||||
|
}, { |
||||
|
partialFilterExpression: { |
||||
|
user: { $exists: true }, |
||||
|
}, |
||||
|
name: 'resource_visits_for_user', |
||||
|
}); |
||||
|
|
||||
|
module.exports = mongoose.model('ResourceVisit', ResourceVisitSchema); |
@ -0,0 +1,15 @@ |
|||||
|
// user-block.js
|
||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const mongoose = require('mongoose'); |
||||
|
const Schema = mongoose.Schema; |
||||
|
|
||||
|
const UserBlockSchema = new Schema({ |
||||
|
user: { type: Schema.ObjectId, required: true, index: true, ref: 'User' }, |
||||
|
blockedUsers: { type: [Schema.ObjectId], ref: 'User' }, |
||||
|
}); |
||||
|
|
||||
|
module.exports = mongoose.model('UserBlock', UserBlockSchema); |
@ -0,0 +1,20 @@ |
|||||
|
// user-notification.js
|
||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const mongoose = require('mongoose'); |
||||
|
const Schema = mongoose.Schema; |
||||
|
|
||||
|
const UserNotificationSchema = new Schema({ |
||||
|
created: { type: Date, default: Date.now, required: true, index: -1, expires: '7d' }, |
||||
|
user: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' }, |
||||
|
source: { type: String, required: true }, |
||||
|
message: { type: String, required: true }, |
||||
|
status: { type: String, enum: ['new', 'seen'], default: 'new', required: true }, |
||||
|
attachmentType: { type: String }, |
||||
|
attachment: { type: Schema.ObjectId, refPath: 'attachmentType' }, |
||||
|
}); |
||||
|
|
||||
|
module.exports = mongoose.model('UserNotification', UserNotificationSchema); |
@ -0,0 +1,53 @@ |
|||||
|
// chat.js
|
||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const mongoose = require('mongoose'); |
||||
|
const ChatMessage = mongoose.model('ChatMessage'); |
||||
|
|
||||
|
const ioEmitter = require('socket.io-emitter'); |
||||
|
|
||||
|
|
||||
|
const { SiteService } = require('../../lib/site-lib'); |
||||
|
|
||||
|
class ChatService extends SiteService { |
||||
|
|
||||
|
constructor (dtp) { |
||||
|
super(dtp, module.exports); |
||||
|
this.populateContentReport = [ |
||||
|
{ |
||||
|
path: 'user', |
||||
|
select: '_id username username_lc displayName picture', |
||||
|
}, |
||||
|
{ |
||||
|
path: 'resource', |
||||
|
populate: [ |
||||
|
{ |
||||
|
path: 'author', |
||||
|
select: '_id username username_lc displayName picture', |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
async start ( ) { |
||||
|
this.emitter = ioEmitter(this.dtp.redis); |
||||
|
} |
||||
|
|
||||
|
async removeMessage (message) { |
||||
|
await ChatMessage.deleteOne({ _id: message._id }); |
||||
|
this.emitter(`site:${this.dtp.config.site.domainKey}:chat`, { |
||||
|
command: 'removeMessage', |
||||
|
params: { messageId: message._id }, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = { |
||||
|
slug: 'chat', |
||||
|
name: 'chat', |
||||
|
create: (dtp) => { return new ChatService(dtp); }, |
||||
|
}; |
@ -0,0 +1,127 @@ |
|||||
|
// content-report.js
|
||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const path = require('path'); |
||||
|
|
||||
|
const mongoose = require('mongoose'); |
||||
|
|
||||
|
const ContentReport = mongoose.model('ContentReport'); |
||||
|
|
||||
|
const pug = require('pug'); |
||||
|
const striptags = require('striptags'); |
||||
|
|
||||
|
const { SiteService, SiteError } = require('../../lib/site-lib'); |
||||
|
|
||||
|
class ContentReportService extends SiteService { |
||||
|
|
||||
|
constructor (dtp) { |
||||
|
super(dtp, module.exports); |
||||
|
this.populateContentReport = [ |
||||
|
{ |
||||
|
path: 'user', |
||||
|
select: '_id username username_lc displayName picture', |
||||
|
}, |
||||
|
{ |
||||
|
path: 'resource', |
||||
|
populate: [ |
||||
|
{ |
||||
|
path: 'author', |
||||
|
select: '_id username username_lc displayName picture', |
||||
|
}, |
||||
|
], |
||||
|
}, |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
async start ( ) { |
||||
|
this.templates = { }; |
||||
|
this.templates.comment = pug.compileFile(path.join(this.dtp.config.root, 'app', 'views', 'comment', 'components', 'comment-standalone.pug')); |
||||
|
} |
||||
|
|
||||
|
async create (user, reportDefinition) { |
||||
|
const NOW = new Date(); |
||||
|
const report = new ContentReport(); |
||||
|
|
||||
|
report.created = NOW; |
||||
|
report.user = user._id; |
||||
|
report.resourceType = reportDefinition.resourceType; |
||||
|
report.resource = reportDefinition.resourceId; |
||||
|
report.status = 'new'; |
||||
|
report.category = reportDefinition.category; |
||||
|
report.reason = striptags(reportDefinition.reason.trim()); |
||||
|
|
||||
|
await report.save(); |
||||
|
|
||||
|
return report.toObject(); |
||||
|
} |
||||
|
|
||||
|
async getReports (status, pagination) { |
||||
|
if (!Array.isArray(status)) { |
||||
|
status = [status]; |
||||
|
} |
||||
|
const reports = await ContentReport |
||||
|
.find({ status: { $in: status } }) |
||||
|
.sort({ created: 1 }) |
||||
|
.skip(pagination.skip) |
||||
|
.limit(pagination.cpp) |
||||
|
.populate(this.populateContentReport) |
||||
|
.lean(); |
||||
|
return reports; |
||||
|
} |
||||
|
|
||||
|
async getById (reportId) { |
||||
|
const report = await ContentReport |
||||
|
.findById(reportId) |
||||
|
.populate(this.populateContentReport) |
||||
|
.lean(); |
||||
|
return report; |
||||
|
} |
||||
|
|
||||
|
async setStatus (report, status) { |
||||
|
await ContentReport.updateOne({ _id: report._id }, { $set: { status } }); |
||||
|
} |
||||
|
|
||||
|
async removeResource (report) { |
||||
|
switch (report.resourceType) { |
||||
|
case 'Comment': |
||||
|
return this.removeComment(report); |
||||
|
case 'Chat': |
||||
|
return this.removeChatMessage(report); |
||||
|
default: |
||||
|
break; |
||||
|
} |
||||
|
this.log.error('invalid resource type in content report', { |
||||
|
reportId: report._id, |
||||
|
resourceType: report.resourceType, |
||||
|
}); |
||||
|
throw new SiteError(406, 'Invalid resource type in content report'); |
||||
|
} |
||||
|
|
||||
|
async removeComment (report) { |
||||
|
const { comment: commentService } = this.dtp.services; |
||||
|
await commentService.remove(report.resource._id || report.resource, 'mod-removed'); |
||||
|
await this.setStatus(report, 'resolved'); |
||||
|
} |
||||
|
|
||||
|
async removeChatMessage (report) { |
||||
|
const { chat: chatService } = this.dtp.services; |
||||
|
await chatService.removeMessage(report.resource); |
||||
|
} |
||||
|
|
||||
|
async removeForResource (resource) { |
||||
|
await ContentReport.deleteMany({ resource: resource._id }); |
||||
|
} |
||||
|
|
||||
|
async removeReport (report) { |
||||
|
await ContentReport.deleteOne({ _id: report._id }); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = { |
||||
|
slug: 'content-report', |
||||
|
name: 'contentReport', |
||||
|
create: (dtp) => { return new ContentReportService(dtp); }, |
||||
|
}; |
@ -0,0 +1,98 @@ |
|||||
|
// content-vote.js
|
||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const mongoose = require('mongoose'); |
||||
|
const ContentVote = mongoose.model('ContentVote'); |
||||
|
|
||||
|
const ioEmitter = require('socket.io-emitter'); |
||||
|
|
||||
|
const { SiteService } = require('../../lib/site-lib'); |
||||
|
|
||||
|
class ContentVoteService extends SiteService { |
||||
|
|
||||
|
constructor (dtp) { |
||||
|
super(dtp, module.exports); |
||||
|
} |
||||
|
|
||||
|
async start ( ) { |
||||
|
this.emitter = ioEmitter(this.dtp.redis); |
||||
|
} |
||||
|
|
||||
|
async recordVote (user, resourceType, resource, vote) { |
||||
|
const NOW = new Date(); |
||||
|
const ResourceModel = mongoose.model(resourceType); |
||||
|
const updateOp = { $inc: { } }; |
||||
|
let message; |
||||
|
|
||||
|
const contentVote = await ContentVote.findOne({ |
||||
|
user: user._id, |
||||
|
resource: resource._id, |
||||
|
}); |
||||
|
|
||||
|
this.log.debug('processing content vote', { resource: resource._id, user: user._id, vote, contentVote }); |
||||
|
|
||||
|
if (!contentVote) { |
||||
|
/* |
||||
|
* It's a new vote from a new user |
||||
|
*/ |
||||
|
await ContentVote.create({ |
||||
|
created: NOW, |
||||
|
resourceType, |
||||
|
resource: resource._id, |
||||
|
user: user._id, |
||||
|
vote |
||||
|
}); |
||||
|
if (vote === 'up') { |
||||
|
updateOp.$inc['stats.upvoteCount'] = 1; |
||||
|
message = 'Comment upvote recorded'; |
||||
|
} else { |
||||
|
updateOp.$inc['stats.downvoteCount'] = 1; |
||||
|
message = 'Comment downvote recorded'; |
||||
|
} |
||||
|
} else { |
||||
|
/* |
||||
|
* If vote not changed, do no further work. |
||||
|
*/ |
||||
|
if (contentVote.vote === vote) { |
||||
|
const updatedResource = await ResourceModel.findById(resource._id).select('stats'); |
||||
|
return { message: "Comment vote unchanged", stats: updatedResource.stats }; |
||||
|
} |
||||
|
|
||||
|
/* |
||||
|
* Update the user's existing vote |
||||
|
*/ |
||||
|
await ContentVote.updateOne( |
||||
|
{ _id: contentVote._id }, |
||||
|
{ $set: { vote } } |
||||
|
); |
||||
|
|
||||
|
/* |
||||
|
* Adjust resource's stats based on the changed vote |
||||
|
*/ |
||||
|
if (vote === 'up') { |
||||
|
updateOp.$inc['stats.upvoteCount'] = 1; |
||||
|
updateOp.$inc['stats.downvoteCount'] = -1; |
||||
|
message = 'Comment vote changed to upvote'; |
||||
|
} else { |
||||
|
updateOp.$inc['stats.upvoteCount'] = -1; |
||||
|
updateOp.$inc['stats.downvoteCount'] = 1; |
||||
|
message = 'Comment vote changed to downvote'; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
this.log.info('updating resource stats', { resourceType, resource, updateOp }); |
||||
|
await ResourceModel.updateOne({ _id: resource._id }, updateOp); |
||||
|
|
||||
|
const updatedResource = await ResourceModel.findById(resource._id).select('stats'); |
||||
|
return { message, stats: updatedResource.stats }; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = { |
||||
|
slug: 'content-vote', |
||||
|
name: 'contentVote', |
||||
|
create: (dtp) => { return new ContentVoteService(dtp); }, |
||||
|
}; |
@ -0,0 +1,44 @@ |
|||||
|
// log.js
|
||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const mongoose = require('mongoose'); |
||||
|
|
||||
|
const Log = mongoose.model('Log'); |
||||
|
|
||||
|
const { SiteService } = require('../../lib/site-lib'); |
||||
|
|
||||
|
class SystemLogService extends SiteService { |
||||
|
|
||||
|
constructor (dtp) { |
||||
|
super(dtp, module.exports); |
||||
|
} |
||||
|
|
||||
|
async getRecords (search, pagination) { |
||||
|
const logs = await Log |
||||
|
.find(search) |
||||
|
.sort({ created: -1 }) |
||||
|
.skip(pagination.skip) |
||||
|
.limit(pagination.cpp) |
||||
|
.lean(); |
||||
|
return logs; |
||||
|
} |
||||
|
|
||||
|
async getComponentNames ( ) { |
||||
|
return await Log.distinct('componentName'); |
||||
|
} |
||||
|
|
||||
|
async getTotalCount ( ) { |
||||
|
const count = await Log.estimatedDocumentCount(); |
||||
|
this.log.debug('log message total count', { count }); |
||||
|
return count; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = { |
||||
|
slug: 'log', |
||||
|
name: 'log', |
||||
|
create: (dtp) => { return new SystemLogService(dtp); }, |
||||
|
}; |
@ -0,0 +1,100 @@ |
|||||
|
// user-notification.js
|
||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const path = require('path'); |
||||
|
|
||||
|
const mongoose = require('mongoose'); |
||||
|
const UserNotification = mongoose.model('UserNotification'); |
||||
|
|
||||
|
const pug = require('pug'); |
||||
|
|
||||
|
const { SiteService } = require('../../lib/site-lib'); |
||||
|
|
||||
|
class UserNotificationService extends SiteService { |
||||
|
|
||||
|
constructor (dtp) { |
||||
|
super(dtp, module.exports); |
||||
|
this.populateComment = [ |
||||
|
{ |
||||
|
path: 'author', |
||||
|
select: '_id username username_lc displayName picture', |
||||
|
}, |
||||
|
{ |
||||
|
path: 'replyTo', |
||||
|
}, |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
async start ( ) { |
||||
|
this.templates = { }; |
||||
|
this.templates.notification = pug.compileFile(path.join(this.dtp.config.root, 'app', 'views', 'notification', 'components', 'notification-standalone.pug')); |
||||
|
} |
||||
|
|
||||
|
async create (user, notificationDefinition) { |
||||
|
const NOW = new Date(); |
||||
|
const notification = new UserNotification(); |
||||
|
|
||||
|
notification.created = NOW; |
||||
|
notification.user = user._id; |
||||
|
notification.source = notificationDefinition.source; |
||||
|
notification.message = notificationDefinition.message; |
||||
|
notification.status = 'new'; |
||||
|
notification.attachmentType = notificationDefinition.attachmentType; |
||||
|
notification.attachment = notificationDefinition.attachment; |
||||
|
|
||||
|
await notification.save(); |
||||
|
|
||||
|
return notification.toObject(); |
||||
|
} |
||||
|
|
||||
|
async getNewCountForUser (user) { |
||||
|
const result = await UserNotification.aggregate([ |
||||
|
{ |
||||
|
$match: { |
||||
|
user: user._id, |
||||
|
status: 'new', |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
$group: { |
||||
|
_id: { user: 1 }, |
||||
|
count: { $sum: 1 }, |
||||
|
}, |
||||
|
}, |
||||
|
{ |
||||
|
$project: { |
||||
|
_id: -1, |
||||
|
count: '$count', |
||||
|
}, |
||||
|
}, |
||||
|
]); |
||||
|
this.log('getNewCountForUser', { result }); |
||||
|
return result[0].count; |
||||
|
} |
||||
|
|
||||
|
async getForUser (user, pagination) { |
||||
|
const notifications = await UserNotification |
||||
|
.find({ user: user._id }) |
||||
|
.sort({ created: -1 }) |
||||
|
.skip(pagination.skip) |
||||
|
.limit(pagination.cpp) |
||||
|
.lean(); |
||||
|
const newNotifications = notifications.map((notif) => notif.status === 'new'); |
||||
|
if (newNotifications.length > 0) { |
||||
|
await UserNotification.updateMany( |
||||
|
{ _id: { $in: newNotifications.map((notif) => notif._id) } }, |
||||
|
{ $set: { stats: 'seen' } }, |
||||
|
); |
||||
|
} |
||||
|
return notifications; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = { |
||||
|
slug: 'user-notification', |
||||
|
name: 'userNotification', |
||||
|
create: (dtp) => { return new UserNotificationService(dtp); }, |
||||
|
}; |
@ -0,0 +1,31 @@ |
|||||
|
extends ../layouts/main |
||||
|
block content |
||||
|
|
||||
|
.uk-margin |
||||
|
+renderSectionTitle('Content Reports') |
||||
|
|
||||
|
if Array.isArray(reports) && (reports.length > 0) |
||||
|
.uk-overflow-auto |
||||
|
table.uk-table.uk-table-small.uk-table-hover |
||||
|
thead |
||||
|
tr |
||||
|
th Created |
||||
|
th Category |
||||
|
th Reporter |
||||
|
th Reason |
||||
|
th Content Type |
||||
|
th Content Author |
||||
|
tbody |
||||
|
each report in reports |
||||
|
tr |
||||
|
td.uk-table-link |
||||
|
a(href=`/admin/content-report/${report._id}`)= moment(report.created).fromNow() |
||||
|
td= report.category |
||||
|
td.uk-table-link |
||||
|
a(href=`/admin/user/${report.user._id}`)= report.user.username |
||||
|
td.uk-table-expand.uk-table-truncate= report.reason |
||||
|
td= report.resourceType.toLowerCase() |
||||
|
td |
||||
|
a(href=`/admin/user/${report.resource.author._id}`)= report.resource.author.username |
||||
|
else |
||||
|
div There are no reports. |
@ -0,0 +1,48 @@ |
|||||
|
extends ../layouts/main |
||||
|
block content |
||||
|
|
||||
|
include ../../comment/components/comment-review |
||||
|
|
||||
|
.uk-margin.uk-width-xlarge |
||||
|
fieldset |
||||
|
legend Reported #{report.resourceType} |
||||
|
+renderCommentReview(report.resource) |
||||
|
|
||||
|
.uk-margin |
||||
|
label(for="reason").uk-form-label.sr-only Reporter's note |
||||
|
.uk-card.uk-card-secondary.uk-card-small |
||||
|
#reason.uk-card-body= report.reason |
||||
|
div(uk-grid).uk-text-small.uk-text-muted |
||||
|
.uk-width-auto reporter: #[a(href=`/admin/user/${report.user._id}`)= report.user.username] |
||||
|
.uk-width-auto reported: #{moment(report.created).fromNow()} |
||||
|
.uk-width-auto author: #[a(href=`/admin/user/${report.resource.author._id}`)= report.resource.author.username] |
||||
|
.uk-width-auto category: #{report.category} |
||||
|
.uk-width-auto status: #{report.status} |
||||
|
|
||||
|
div(uk-grid) |
||||
|
.uk-width-expand |
||||
|
div(uk-grid) |
||||
|
.uk-width-auto |
||||
|
form(method="POST", action= `/admin/content-report/${report._id}/action`, onsubmit="return dtp.app.submitForm(event, 'remove content');") |
||||
|
input(type="hidden", name="verb", value="dismiss") |
||||
|
button( |
||||
|
type="submit", |
||||
|
title="Dismiss this content report with no action", |
||||
|
).uk-button.dtp-button-primary |
||||
|
+renderLabeledIcon('fa-times', 'Dismiss') |
||||
|
.uk-width-auto |
||||
|
form(method="POST", action= `/admin/content-report/${report._id}/action`, onsubmit="return dtp.app.submitForm(event, 'remove content');") |
||||
|
input(type="hidden", name="verb", value="purge") |
||||
|
button( |
||||
|
type="submit", |
||||
|
title=`Dismiss this content report and purge all reports from ${report.user.username}`, |
||||
|
).uk-button.dtp-button-primary |
||||
|
+renderLabeledIcon('fa-bicycle', 'Purge') |
||||
|
.uk-width-auto |
||||
|
form(method="POST", action= `/admin/content-report/${report._id}/action`, onsubmit="return dtp.app.submitForm(event, 'remove content');") |
||||
|
input(type="hidden", name="verb", value="remove") |
||||
|
button( |
||||
|
type="submit", |
||||
|
title=`Remove the reported ${report.resourceType}`, |
||||
|
).uk-button.dtp-button-danger |
||||
|
+renderLabeledIcon('fa-trash', `Remove ${report.resourceType}`) |
@ -1,23 +1,35 @@ |
|||||
extends ../layouts/main |
extends ../layouts/main |
||||
block content |
block content |
||||
|
|
||||
table.uk-table.uk-table-small.uk-table-divider |
mixin renderHostList (hosts) |
||||
thead |
if Array.isArray(hosts) && (hosts.length > 0) |
||||
th Host |
table.uk-table.uk-table-small.uk-table-divider |
||||
th Status |
thead |
||||
th Memory |
th Host |
||||
th Platform |
th Status |
||||
th Arch |
th Memory |
||||
th Created |
th Platform |
||||
th Updated |
th Arch |
||||
tbody |
th Created |
||||
each host in hosts |
th Updated |
||||
tr |
tbody |
||||
td |
each host in hosts |
||||
a(href=`/admin/host/${host._id}`)= host.hostname |
tr(data-host-id= host._id) |
||||
td= host.status |
td |
||||
td= numeral((host.totalmem - host.freemem) / host.totalmem).format('0.00%') |
a(href=`/admin/host/${host._id}`)= host.hostname |
||||
td= host.platform |
td= host.status |
||||
td= host.arch |
td= numeral((host.totalmem - host.freemem) / host.totalmem).format('0.00%') |
||||
td= moment(host.created).fromNow() |
td= host.platform |
||||
td= host.updated ? moment(host.updated).fromNow() : 'N/A' |
td= host.arch |
||||
|
td= moment(host.created).fromNow() |
||||
|
td= host.updated ? moment(host.updated).fromNow() : 'N/A' |
||||
|
else |
||||
|
div The host list is empty |
||||
|
|
||||
|
if Array.isArray(activeHosts) && (activeHosts.length > 0) |
||||
|
h2 Active hosts |
||||
|
+renderHostList(activeHosts) |
||||
|
|
||||
|
if Array.isArray(crashedHosts) && (crashedHosts.length > 0) |
||||
|
h2 Crashed hosts |
||||
|
+renderHostList(crashedHosts) |
||||
|
@ -1,12 +1,22 @@ |
|||||
extends layouts/main |
extends layouts/main |
||||
block content |
block content |
||||
|
|
||||
div(uk-grid).uk-grid-small.uk-flex-between.uk-flex-middle |
.uk-margin |
||||
|
canvas(id="hourly-signups") |
||||
|
|
||||
|
div(uk-grid).uk-grid-small.uk-flex-middle |
||||
.uk-width-auto |
.uk-width-auto |
||||
+renderCell('Members', formatCount(stats.memberCount)) |
+renderCell('Members', formatCount(stats.memberCount)) |
||||
.uk-width-auto |
.uk-width-auto |
||||
+renderCell('Channels', formatCount(stats.channelCount)) |
+renderCell('Posts', formatCount(stats.postCount)) |
||||
.uk-width-auto |
|
||||
+renderCell('Streams', formatCount(stats.streamCount)) |
|
||||
.uk-width-auto |
.uk-width-auto |
||||
+renderCell('Viewers', formatCount(stats.viewerCount)) |
+renderCell('Comments', formatCount(stats.commentCount)) |
||||
|
|
||||
|
block viewjs |
||||
|
script(src="/chart.js/chart.min.js") |
||||
|
script(src="/chartjs-adapter-moment/chartjs-adapter-moment.min.js") |
||||
|
script. |
||||
|
window.addEventListener('dtp-load', ( ) => { |
||||
|
const graphData = !{JSON.stringify(stats.userSignupHourly)}; |
||||
|
dtp.app.renderStatsGraph('#hourly-signups', 'Hourly Signups', graphData); |
||||
|
}); |
@ -0,0 +1,46 @@ |
|||||
|
extends ../layouts/main |
||||
|
block content |
||||
|
|
||||
|
include ../../components/pagination-bar |
||||
|
|
||||
|
.uk-margin |
||||
|
form(method="GET", action="/admin/log").uk-form |
||||
|
div(uk-grid).uk-grid-small.uk-flex-middle |
||||
|
.uk-width-expand |
||||
|
h1 Log #[span.uk-text-small.uk-text-muted #{numeral(totalLogCount).format('0,0')} records] |
||||
|
|
||||
|
.uk-width-auto |
||||
|
- |
||||
|
var urlParams = ''; |
||||
|
if (query.component) { |
||||
|
urlParams += `&component=${query.component}`; |
||||
|
} |
||||
|
+renderPaginationBar(`/admin/log`, totalLogCount, urlParams) |
||||
|
|
||||
|
.uk-width-auto |
||||
|
select(id="component", name="component").uk-select |
||||
|
each componentName in components |
||||
|
option(value= componentName, selected= (query.component === componentName))= componentName |
||||
|
.uk-width-auto |
||||
|
button(type="submit").uk-button.dtp-button-primary Filter |
||||
|
|
||||
|
if Array.isArray(logs) && (logs.length > 0) |
||||
|
table.uk-table.uk-table-small.uk-table-divider |
||||
|
thead |
||||
|
tr |
||||
|
th Timestamp |
||||
|
th Level |
||||
|
th Component |
||||
|
th Message |
||||
|
tbody |
||||
|
each log in logs |
||||
|
tr |
||||
|
td= moment(log.created).format('YYYY-MM-DD hh:mm:ss.SSS') |
||||
|
td= log.level |
||||
|
td= log.componentName |
||||
|
td |
||||
|
div= log.message |
||||
|
if log.metadata |
||||
|
.uk-text-small(style="font-family: Courier New;")!= hljs.highlightAuto(JSON.stringify(log.metadata, null, 1)).value |
||||
|
else |
||||
|
div There are no logs. |
@ -0,0 +1,11 @@ |
|||||
|
include comment |
||||
|
|
||||
|
mixin renderCommentList (comments) |
||||
|
if Array.isArray(comments) && (comments.length > 0) |
||||
|
ul#post-comment-list.uk-list.uk-list-divider.uk-list-large |
||||
|
each comment in comments |
||||
|
li(data-comment-id= comment._id) |
||||
|
+renderComment(comment) |
||||
|
else |
||||
|
ul#post-comment-list.uk-list.uk-list-divider.uk-list-large |
||||
|
div There are no comments at this time. Please check back later. |
@ -0,0 +1,13 @@ |
|||||
|
mixin renderCommentReview (comment) |
||||
|
- var resourceId = comment.resource._id || comment.resource; |
||||
|
article.uk-comment.dtp-site-comment |
||||
|
header.uk-comment-header |
||||
|
div(uk-grid).uk-grid-medium.uk-flex-middle |
||||
|
.uk-width-auto |
||||
|
img(src="/img/default-member.png").site-profile-picture.sb-small.uk-comment-avatar |
||||
|
.uk-width-expand |
||||
|
h4.uk-comment-title.uk-margin-remove= comment.author.displayName || comment.author.username |
||||
|
.uk-comment-meta= moment(comment.created).fromNow() |
||||
|
|
||||
|
.uk-comment-body(class={ 'uk-text-muted': ['removed','mod-removed'].includes(comment.status) }) |
||||
|
.comment-content!= marked.parse(comment.content) |
@ -1,47 +1,114 @@ |
|||||
mixin renderComment (comment) |
mixin renderComment (comment) |
||||
.uk-card.uk-card-secondary.uk-card-small.uk-border-rounded |
- var resourceId = comment.resource._id || comment.resource; |
||||
.uk-card-body |
article(data-comment-id= comment._id).uk-comment.dtp-site-comment |
||||
div(uk-grid).uk-grid-small |
header.uk-comment-header |
||||
|
div(uk-grid).uk-grid-medium.uk-flex-middle |
||||
.uk-width-auto |
.uk-width-auto |
||||
img(src="/img/default-member.png").site-profile-picture.sb-small |
img(src="/img/default-member.png").site-profile-picture.sb-small.uk-comment-avatar |
||||
|
|
||||
.uk-width-expand |
.uk-width-expand |
||||
div(uk-grid).uk-grid-small.uk-flex-middle.uk-text-small |
h4.uk-comment-title.uk-margin-remove= comment.author.displayName || comment.author.username |
||||
if comment.author.displayName |
.uk-comment-meta= moment(comment.created).fromNow() |
||||
.uk-width-auto |
|
||||
span= comment.author.displayName |
if user && (comment.status === 'published') |
||||
.uk-width-auto= comment.author.username |
.uk-width-auto |
||||
.uk-width-auto= moment(comment.created).fromNow() |
button(type="button").uk-button.uk-button-link |
||||
div!= marked.parse(comment.content) |
span |
||||
div(uk-grid).uk-grid-small.uk-text-small |
i.fas.fa-ellipsis-h |
||||
.uk-width-auto |
div(data-comment-id= comment._id, uk-dropdown={ mode: 'click', pos: 'bottom-right' }) |
||||
button( |
ul.uk-nav.uk-dropdown-nav |
||||
type="button", |
if user && user._id.equals(comment.author._id) |
||||
data-comment-id= comment._id, |
li.uk-nav-header.no-select Author menu |
||||
onclick="return dtp.app.upvoteComment(event);", |
li |
||||
title="Upvote this comment", |
a( |
||||
).uk-button.uk-button-link |
href="", |
||||
+renderButtonIcon('fa-chevron-up', formatCount(comment.stats.upvoteCount)) |
data-comment-id= comment._id, |
||||
.uk-width-auto |
onclick="return dtp.app.deleteComment(event);", |
||||
button( |
) Delete |
||||
type="button", |
else if user |
||||
data-comment-id= comment._id, |
li.uk-nav-header.no-select Moderation menu |
||||
onclick="return dtp.app.downvoteComment(event);", |
li |
||||
title="Downvote this comment", |
a( |
||||
).uk-button.uk-button-link |
href="", |
||||
+renderButtonIcon('fa-chevron-down', formatCount(comment.stats.downvoteCount)) |
data-resource-type= comment.resourceType, |
||||
.uk-width-auto |
data-resource-id= resourceId, |
||||
button( |
data-comment-id= comment._id, |
||||
type="button", |
onclick="return dtp.app.showReportCommentForm(event);", |
||||
data-comment-id= comment._id, |
) Report |
||||
onclick="return dtp.app.openReplies(event);", |
li |
||||
title="Load replies to this comment", |
a( |
||||
).uk-button.uk-button-link |
href="", |
||||
+renderButtonIcon('fa-comment', formatCount(comment.stats.replyCount)) |
data-resource-type= comment.resourceType, |
||||
.uk-width-auto |
data-resource-id= resourceId, |
||||
button( |
data-comment-id= comment._id, |
||||
type="button", |
onclick="return dtp.app.blockCommentAuthor(event);", |
||||
data-comment-id= comment._id, |
) Block author |
||||
onclick="return dtp.app.openReplyComposer(event);", |
|
||||
title="Write a reply to this comment", |
.uk-comment-body |
||||
).uk-button.uk-button-link |
case comment.status |
||||
+renderButtonIcon('fa-reply', 'reply') |
when 'published' |
||||
|
if comment.flags && comment.flags.isNSFW |
||||
|
div.uk-alert.uk-alert-info.uk-border-rounded |
||||
|
div(uk-grid).uk-grid-small.uk-text-small.uk-flex-middle |
||||
|
.uk-width-expand NSFW comment hidden by default. Use the eye to show/hide. |
||||
|
.uk-width-auto |
||||
|
button( |
||||
|
type="button", |
||||
|
uk-toggle={ target: `.comment-content[data-comment-id="${comment._id}"]` }, |
||||
|
title="Show/hide the comment text", |
||||
|
).uk-button.uk-button-link |
||||
|
span |
||||
|
i.fas.fa-eye |
||||
|
.comment-content(data-comment-id= comment._id, hidden= comment.flags ? comment.flags.isNSFW : false)!= marked.parse(comment.content) |
||||
|
when 'removed' |
||||
|
.comment-content.uk-text-muted [comment removed] |
||||
|
when 'mod-warn' |
||||
|
alert |
||||
|
span A warning has been added to this comment. |
||||
|
button(type="button", uk-toggle={ target: `.comment-content[data-comment-id="${comment._id}"]` }) |
||||
|
.comment-content(data-comment-id= comment._id, hidden)!= marked.parse(comment.content) |
||||
|
when 'mod-removed' |
||||
|
.comment-content.uk-text-muted [comment removed] |
||||
|
|
||||
|
//- Comment meta bar |
||||
|
div(uk-grid).uk-grid-small |
||||
|
.uk-width-auto |
||||
|
button( |
||||
|
type="button", |
||||
|
data-comment-id= comment._id, |
||||
|
data-vote="up", |
||||
|
onclick="return dtp.app.submitCommentVote(event);", |
||||
|
title="Upvote this comment", |
||||
|
).uk-button.uk-button-link |
||||
|
+renderLabeledIcon('fa-chevron-up', formatCount(comment.stats.upvoteCount)) |
||||
|
.uk-width-auto |
||||
|
button( |
||||
|
type="button", |
||||
|
data-comment-id= comment._id, |
||||
|
data-vote="down", |
||||
|
onclick="return dtp.app.submitCommentVote(event);", |
||||
|
title="Downvote this comment", |
||||
|
).uk-button.uk-button-link |
||||
|
+renderLabeledIcon('fa-chevron-down', formatCount(comment.stats.downvoteCount)) |
||||
|
.uk-width-auto |
||||
|
button( |
||||
|
type="button", |
||||
|
data-comment-id= comment._id, |
||||
|
onclick="return dtp.app.openReplies(event);", |
||||
|
title="Load replies to this comment", |
||||
|
).uk-button.uk-button-link |
||||
|
+renderLabeledIcon('fa-comment', formatCount(comment.stats.replyCount)) |
||||
|
.uk-width-auto |
||||
|
button( |
||||
|
type="button", |
||||
|
data-comment-id= comment._id, |
||||
|
onclick="return dtp.app.openReplyComposer(event);", |
||||
|
title="Write a reply to this comment", |
||||
|
).uk-button.uk-button-link |
||||
|
+renderLabeledIcon('fa-reply', 'reply') |
||||
|
|
||||
|
//- Comment replies and reply composer |
||||
|
div(data-comment-id= comment._id) |
||||
|
if user && user.flags.canComment |
||||
|
.uk-margin |
||||
|
+renderCommentComposer(`/post`, { replyTo: comment._id }) |
@ -0,0 +1,36 @@ |
|||||
|
//- https://owenbenjamin.com/social-channels/ and https://gab.com/owenbenjamin |
||||
|
mixin renderCommentComposer (actionUrl) |
||||
|
form(method="POST", action= actionUrl, onsubmit="return dtp.app.submitForm(event, 'create-comment');").uk-form |
||||
|
.uk-card.uk-card-secondary.uk-card-small |
||||
|
.uk-card-body |
||||
|
textarea( |
||||
|
id="content", |
||||
|
name="content", |
||||
|
rows="4", |
||||
|
maxlength="3000", |
||||
|
placeholder="Enter comment", |
||||
|
oninput="return dtp.app.onCommentInput(event);", |
||||
|
).uk-textarea.uk-resize-vertical |
||||
|
.uk-text-small |
||||
|
div(uk-grid).uk-flex-between |
||||
|
.uk-width-auto You are commenting as: #{user.username} |
||||
|
.uk-width-auto #[span#comment-character-count 0] of 3,000 |
||||
|
.uk-card-footer |
||||
|
div(uk-grid).uk-flex-between |
||||
|
.uk-width-expand |
||||
|
ul.uk-subnav |
||||
|
li |
||||
|
button( |
||||
|
type="button", |
||||
|
data-target-element="content", |
||||
|
title="Add an emoji", |
||||
|
onclick="return dtp.app.showEmojiPicker(event);", |
||||
|
).uk-button.dtp-button-default |
||||
|
span |
||||
|
i.far.fa-smile |
||||
|
li(title="Not Safe For Work will hide your comment text by default") |
||||
|
label |
||||
|
input(id="is-nsfw", name="isNSFW", type="checkbox").uk-checkbox |
||||
|
| NSFW |
||||
|
.uk-width-auto |
||||
|
button(type="submit").uk-button.dtp-button-primary Post |
@ -0,0 +1,35 @@ |
|||||
|
include ../../components/library |
||||
|
include comment-review |
||||
|
.uk-modal-body |
||||
|
h4.uk-modal-title Report Comment |
||||
|
form(method="POST", action=`/content-report/comment`, onsubmit="return dtp.app.submitDialogForm(event, 'report comment');").uk-form |
||||
|
|
||||
|
input(type="hidden", name="resourceType", value= params.resourceType) |
||||
|
input(type="hidden", name="resourceId", value= params.resourceId) |
||||
|
input(type="hidden", name="commentId", value= params.commentId) |
||||
|
|
||||
|
.uk-margin |
||||
|
+renderCommentReview(comment) |
||||
|
|
||||
|
.uk-margin |
||||
|
select(id="category", name="category").uk-select |
||||
|
option(value="none") --- Select category --- |
||||
|
option(value="spam") Spam |
||||
|
option(value="violence") Violence/Threats |
||||
|
option(value="porn") Porn |
||||
|
option(value="doxxing") Doxxing |
||||
|
option(value="other") Other |
||||
|
|
||||
|
.uk-margin |
||||
|
textarea(id="reason", name="reason", rows="4", placeholder="Enter additional notes here").uk-textarea.uk-resize-vertical |
||||
|
|
||||
|
.uk-margin |
||||
|
label |
||||
|
input(id="block-author", name="blockAuthor", type="checkbox", checked).uk-checkbox |
||||
|
| Also block #{comment.author.username} |
||||
|
|
||||
|
div(uk-grid).uk-grid-small.uk-flex-between |
||||
|
.uk-width-auto |
||||
|
button(type="button").uk-button.dtp-button-default.uk-modal-close Cancel |
||||
|
.uk-width-auto |
||||
|
button(type="submit").uk-button.dtp-button-primary Submit Report |
@ -0,0 +1,4 @@ |
|||||
|
mixin renderLabeledIcon (iconClass, iconLabel) |
||||
|
span |
||||
|
i(class=`fas ${iconClass}`) |
||||
|
span.uk-margin-small-left.dtp-item-value= iconLabel |
@ -1,12 +1,71 @@ |
|||||
|
mixin renderSocialIcon (iconClass, iconLabel, url) |
||||
|
a(href= url).dtp-social-link |
||||
|
span |
||||
|
i(class=`fab ${iconClass}`) |
||||
|
span.uk-margin-small-left= iconLabel |
||||
|
|
||||
section.uk-section.uk-section-muted.uk-section-small.dtp-site-footer |
section.uk-section.uk-section-muted.uk-section-small.dtp-site-footer |
||||
.uk-container.uk-text-small.uk-text-center |
.uk-container.uk-text-small.uk-text-center |
||||
ul.uk-subnav.uk-flex-center |
ul.uk-subnav.uk-flex-center |
||||
each socialIcon in socialIcons |
if site.gabUrl |
||||
li |
li |
||||
a(href=socialIcon.url).dtp-social-link |
a(href= site.gabUrl).dtp-social-link |
||||
span |
span |
||||
i(class=`fab ${socialIcon.icon}`) |
img(src="/img/gab-g.svg", style="width: auto; height: 1em;") |
||||
span.uk-margin-small-left= socialIcon.label |
span.uk-margin-small-left Gab Social |
||||
|
if site.gabtvUrl |
||||
|
li |
||||
|
a(href= site.gabtvUrl).dtp-social-link |
||||
|
span |
||||
|
img(src="/img/gab-g.svg", style="width: auto; height: 1em;") |
||||
|
span.uk-margin-small-left Gab TV |
||||
|
|
||||
|
if site.telegramUrl |
||||
|
li |
||||
|
+renderSocialIcon('fa-telegram', 'Telegram', site.telegramUrl) |
||||
|
if site.twitterUrl |
||||
|
li |
||||
|
+renderSocialIcon('fa-twitter', 'Twitter', site.twitterUrl) |
||||
|
if site.facebookUrl |
||||
|
li |
||||
|
+renderSocialIcon('fa-facebook', 'Facebook', site.facebookUrl) |
||||
|
if site.instagramUrl |
||||
|
li |
||||
|
+renderSocialIcon('fa-instagram', 'Instagram', site.instagramUrl) |
||||
|
|
||||
|
if site.bitchuteUrl |
||||
|
li |
||||
|
a(href= site.bitchuteUrl).dtp-social-link |
||||
|
span |
||||
|
img(src="/img/social-icons/bitchute.svg", style="width: auto; height: 1em;") |
||||
|
span.uk-margin-small-left BitChute |
||||
|
if site.odyseeUrl |
||||
|
li |
||||
|
a(href= site.odyseeUrl).dtp-social-link |
||||
|
span |
||||
|
img(src="/img/social-icons/odysee.svg", style="width: auto; height: 1em;") |
||||
|
span.uk-margin-small-left Odysee |
||||
|
if site.rumbleUrl |
||||
|
li |
||||
|
a(href= site.odyseeUrl).dtp-social-link |
||||
|
span |
||||
|
img(src="/img/social-icons/rumble.svg", style="width: auto; height: 1em;") |
||||
|
span.uk-margin-small-left Rumble |
||||
|
if site.twitchUrl |
||||
|
li |
||||
|
+renderSocialIcon('fa-twitch', 'Twitch', site.twitchUrl) |
||||
|
if site.youtubeUrl |
||||
|
li |
||||
|
+renderSocialIcon('fa-youtube', 'YouTube', site.youtubeUrl) |
||||
|
if site.dliveUrl |
||||
|
li |
||||
|
a(href= site.dliveUrl).dtp-social-link |
||||
|
span |
||||
|
img(src="/img/social-icons/dlive.svg", style="width: auto; height: 1em;") |
||||
|
span.uk-margin-small-left DLive |
||||
|
|
||||
.uk-width-medium.uk-margin-auto |
.uk-width-medium.uk-margin-auto |
||||
hr |
hr |
||||
|
|
||||
div Copyright © 2021 #[+renderSiteLink()] |
div Copyright © 2021 #[+renderSiteLink()] |
||||
|
div All Rights Reserved |
@ -1,8 +1,8 @@ |
|||||
block facebook-card |
block facebook-card |
||||
meta(property='og:site_name', content= site.name) |
meta(property='og:site_name', content= site.name) |
||||
meta(property='og:type', content='website') |
meta(property='og:type', content='website') |
||||
meta(property='og:image', content= `https://${site.domain}/img/social-cards/${site.domain}.png?v=${pkg.version}`) |
meta(property='og:image', content= `https://${site.domain}/img/social-cards/${site.domainKey}.png?v=${pkg.version}`) |
||||
meta(property='og:url', content= `https://${site.domain}${dtp.request.url}`) |
meta(property='og:url', content= `https://${site.domain}${dtp.request.url}`) |
||||
meta(property='og:title', content= `${site.name} | ${site.description}`) |
meta(property='og:title', content= pageTitle || site.name) |
||||
meta(property='og:description', content= site.description) |
meta(property='og:description', content= pageDescription || site.description) |
||||
meta(property='og:image:alt', content= `${site.name} | ${site.description}`) |
meta(property='og:image:alt', content= `${site.name} | ${site.description}`) |
@ -1,5 +1,5 @@ |
|||||
block twitter-card |
block twitter-card |
||||
meta(name='twitter:card', content='summary_large_image') |
meta(name='twitter:card', content='summary_large_image') |
||||
meta(name='twitter:image' content= `https://${site.domain}/img/social-cards/${site.domain}.png?v=${pkg.version}`) |
meta(name='twitter:image' content= `https://${site.domain}/img/social-cards/${site.domainKey}.png?v=${pkg.version}`) |
||||
meta(name='twitter:title', content= `${site.name} | ${site.description}`) |
meta(name='twitter:title', content= pageTitle || site.name) |
||||
meta(name='twitter:description', content= site.description) |
meta(name='twitter:description', content= pageDescription || site.description) |
||||
|
@ -0,0 +1,2 @@ |
|||||
|
include notification |
||||
|
+renderUserNotification(notification) |
@ -0,0 +1,5 @@ |
|||||
|
mixin renderUserNotification (userNotification) |
||||
|
div |
||||
|
div= moment(userNotification.created).fromNow() |
||||
|
div= userNotification.source |
||||
|
div= userNotification.message |
@ -1,9 +1,26 @@ |
|||||
extends ../layouts/main |
extends ../layouts/main |
||||
block content |
block content |
||||
|
|
||||
|
include ../comment/components/comment |
||||
|
|
||||
section.uk-section.uk-section-default |
section.uk-section.uk-section-default |
||||
.uk-container |
.uk-container |
||||
h1= user.displayName || user.username || user.email |
h1= user.displayName || user.username || user.email |
||||
p Viewers do not have public profiles on #[+renderSiteLink()]. You must be logged in to view your profile. Only you can view your profile. |
p People don't have public profiles on #[+renderSiteLink()]. You must be logged in to view your profile. And, only you can view your profile. |
||||
|
|
||||
|
p Your profile is where you edit your account settings, configure your commenting defaults, and otherwise manage how you use #[+renderSiteLink()]. |
||||
|
|
||||
|
section.uk-section.uk-section-default |
||||
|
.uk-container |
||||
|
.uk-margin |
||||
|
+renderSectionTitle('Comment History') |
||||
|
|
||||
p Your profile is where you manage your channel subscriptions, edit account settings, configure your chat defaults, and otherwise manage how you use #[+renderSiteLink()]. |
if Array.isArray(commentHistory) && (commentHistory.length > 0) |
||||
|
ul.uk-list.uk-list-divider |
||||
|
each comment in commentHistory |
||||
|
li |
||||
|
.uk-margin-small |
||||
|
.uk-text-small commenting on #[a(href=`/post/${comment.resource.slug}?comment=${comment._id}#featured-comment`)= comment.resource.title] |
||||
|
+renderComment(comment) |
||||
|
else |
||||
|
div You haven't written any comments on posts. |
@ -0,0 +1,78 @@ |
|||||
|
// host-services.js
|
||||
|
// Copyright (C) 2021 Digital Telepresence, LLC
|
||||
|
// License: Apache-2.0
|
||||
|
|
||||
|
'use strict'; |
||||
|
|
||||
|
const path = require('path'); |
||||
|
|
||||
|
require('dotenv').config({ path: path.resolve(__dirname, '..', '..', '.env') }); |
||||
|
|
||||
|
const mongoose = require('mongoose'); |
||||
|
|
||||
|
const { |
||||
|
SitePlatform, |
||||
|
SiteLog, |
||||
|
} = require(path.join(__dirname, '..', '..', 'lib', 'site-lib')); |
||||
|
|
||||
|
const { CronJob } = require('cron'); |
||||
|
|
||||
|
const CRON_TIMEZONE = 'America/New_York'; |
||||
|
|
||||
|
module.pkg = require(path.resolve(__dirname, '..', '..', 'package.json')); |
||||
|
module.config = { |
||||
|
componentName: 'reeeper', |
||||
|
root: path.resolve(__dirname, '..', '..'), |
||||
|
}; |
||||
|
|
||||
|
module.log = new SiteLog(module, module.config.componentName); |
||||
|
|
||||
|
module.expireCrashedHosts = async ( ) => { |
||||
|
const NetHost = mongoose.model('NetHost'); |
||||
|
try { |
||||
|
await NetHost |
||||
|
.find({ status: 'crashed' }) |
||||
|
.select('_id hostname') |
||||
|
.lean() |
||||
|
.cursor() |
||||
|
.eachAsync(async (host) => { |
||||
|
module.log.info('deactivating crashed host', { hostname: host.hostname }); |
||||
|
await NetHost.updateOne({ _id: host._id }, { $set: { status: 'inactive' } }); |
||||
|
}); |
||||
|
} catch (error) { |
||||
|
module.log.error('failed to expire crashed hosts', { error }); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
(async ( ) => { |
||||
|
try { |
||||
|
process.once('SIGINT', async ( ) => { |
||||
|
module.log.info('SIGINT received'); |
||||
|
module.log.info('requesting shutdown...'); |
||||
|
|
||||
|
const exitCode = await SitePlatform.shutdown(); |
||||
|
process.nextTick(( ) => { |
||||
|
process.exit(exitCode); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
/* |
||||
|
* Site Platform startup |
||||
|
*/ |
||||
|
await SitePlatform.startPlatform(module); |
||||
|
|
||||
|
await module.expireCrashedHosts(); // first-run the expirations
|
||||
|
|
||||
|
module.expireJob = new CronJob( |
||||
|
'*/5 * * * * *', |
||||
|
module.expireCrashedHosts, |
||||
|
null, true, CRON_TIMEZONE, |
||||
|
); |
||||
|
|
||||
|
module.log.info(`${module.pkg.name} v${module.pkg.version} ${module.config.componentName} started`); |
||||
|
} catch (error) { |
||||
|
module.log.error('failed to start Host Cache worker', { error }); |
||||
|
process.exit(-1); |
||||
|
} |
||||
|
|
||||
|
})(); |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 6.3 KiB |
After Width: | Height: | Size: 38 KiB |
After Width: | Height: | Size: 21 KiB |
@ -0,0 +1,61 @@ |
|||||
|
.dtp-site-comment { |
||||
|
|
||||
|
.uk-dropdown { |
||||
|
background-color: #e8e8e8; |
||||
|
color: #1a1a1a; |
||||
|
} |
||||
|
|
||||
|
.uk-dropdown-nav .uk-nav-header, |
||||
|
.uk-dropdown-nav .uk-nav-header { |
||||
|
color: #1a1a1a; |
||||
|
} |
||||
|
|
||||
|
.uk-dropdown-nav li a { |
||||
|
color: #1a1a1a; |
||||
|
|
||||
|
&:hover { |
||||
|
color: #808080; |
||||
|
text-decoration: underline; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.comment-content { |
||||
|
padding-right: 4px; |
||||
|
max-height: 320px; |
||||
|
overflow: auto; |
||||
|
|
||||
|
p:last-child { |
||||
|
margin-bottom: none; |
||||
|
} |
||||
|
|
||||
|
blockquote { |
||||
|
padding-top: 20px; |
||||
|
padding-bottom: 20px; |
||||
|
padding-left: 20px; |
||||
|
|
||||
|
border-left: solid 2px @global-color; |
||||
|
border-radius: 4px; |
||||
|
|
||||
|
font-size: inherit; |
||||
|
color: inherit; |
||||
|
} |
||||
|
|
||||
|
em { |
||||
|
color: inherit; |
||||
|
} |
||||
|
|
||||
|
&::-webkit-scrollbar { |
||||
|
width: 10px; |
||||
|
} |
||||
|
&::-webkit-scrollbar-track { |
||||
|
background: none; |
||||
|
} |
||||
|
&::-webkit-scrollbar-thumb { |
||||
|
background: #4a4a4a; |
||||
|
border-radius: 4px; |
||||
|
} |
||||
|
&::-webkit-scrollbar-thumb:hover { |
||||
|
background: #e8e8e8; |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,8 @@ |
|||||
|
#!/bin/sh |
||||
|
|
||||
|
git pull origin master |
||||
|
yarn --production=false |
||||
|
gulp build |
||||
|
|
||||
|
./stop-production |
||||
|
./start-production |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue