107 changed files with 3317 additions and 376 deletions
@ -1,5 +1,6 @@ |
|||
.env |
|||
|
|||
data/minio |
|||
node_modules |
|||
dist |
|||
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 |
|||
block content |
|||
|
|||
table.uk-table.uk-table-small.uk-table-divider |
|||
thead |
|||
th Host |
|||
th Status |
|||
th Memory |
|||
th Platform |
|||
th Arch |
|||
th Created |
|||
th Updated |
|||
tbody |
|||
each host in hosts |
|||
tr |
|||
td |
|||
a(href=`/admin/host/${host._id}`)= host.hostname |
|||
td= host.status |
|||
td= numeral((host.totalmem - host.freemem) / host.totalmem).format('0.00%') |
|||
td= host.platform |
|||
td= host.arch |
|||
td= moment(host.created).fromNow() |
|||
td= host.updated ? moment(host.updated).fromNow() : 'N/A' |
|||
mixin renderHostList (hosts) |
|||
if Array.isArray(hosts) && (hosts.length > 0) |
|||
table.uk-table.uk-table-small.uk-table-divider |
|||
thead |
|||
th Host |
|||
th Status |
|||
th Memory |
|||
th Platform |
|||
th Arch |
|||
th Created |
|||
th Updated |
|||
tbody |
|||
each host in hosts |
|||
tr(data-host-id= host._id) |
|||
td |
|||
a(href=`/admin/host/${host._id}`)= host.hostname |
|||
td= host.status |
|||
td= numeral((host.totalmem - host.freemem) / host.totalmem).format('0.00%') |
|||
td= host.platform |
|||
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 |
|||
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 |
|||
+renderCell('Members', formatCount(stats.memberCount)) |
|||
.uk-width-auto |
|||
+renderCell('Channels', formatCount(stats.channelCount)) |
|||
+renderCell('Posts', formatCount(stats.postCount)) |
|||
.uk-width-auto |
|||
+renderCell('Streams', formatCount(stats.streamCount)) |
|||
.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) |
|||
.uk-card.uk-card-secondary.uk-card-small.uk-border-rounded |
|||
.uk-card-body |
|||
div(uk-grid).uk-grid-small |
|||
- var resourceId = comment.resource._id || comment.resource; |
|||
article(data-comment-id= comment._id).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 |
|||
img(src="/img/default-member.png").site-profile-picture.sb-small.uk-comment-avatar |
|||
|
|||
.uk-width-expand |
|||
div(uk-grid).uk-grid-small.uk-flex-middle.uk-text-small |
|||
if comment.author.displayName |
|||
.uk-width-auto |
|||
span= comment.author.displayName |
|||
.uk-width-auto= comment.author.username |
|||
.uk-width-auto= moment(comment.created).fromNow() |
|||
div!= marked.parse(comment.content) |
|||
div(uk-grid).uk-grid-small.uk-text-small |
|||
.uk-width-auto |
|||
button( |
|||
type="button", |
|||
data-comment-id= comment._id, |
|||
onclick="return dtp.app.upvoteComment(event);", |
|||
title="Upvote this comment", |
|||
).uk-button.uk-button-link |
|||
+renderButtonIcon('fa-chevron-up', formatCount(comment.stats.upvoteCount)) |
|||
.uk-width-auto |
|||
button( |
|||
type="button", |
|||
data-comment-id= comment._id, |
|||
onclick="return dtp.app.downvoteComment(event);", |
|||
title="Downvote this comment", |
|||
).uk-button.uk-button-link |
|||
+renderButtonIcon('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 |
|||
+renderButtonIcon('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 |
|||
+renderButtonIcon('fa-reply', 'reply') |
|||
h4.uk-comment-title.uk-margin-remove= comment.author.displayName || comment.author.username |
|||
.uk-comment-meta= moment(comment.created).fromNow() |
|||
|
|||
if user && (comment.status === 'published') |
|||
.uk-width-auto |
|||
button(type="button").uk-button.uk-button-link |
|||
span |
|||
i.fas.fa-ellipsis-h |
|||
div(data-comment-id= comment._id, uk-dropdown={ mode: 'click', pos: 'bottom-right' }) |
|||
ul.uk-nav.uk-dropdown-nav |
|||
if user && user._id.equals(comment.author._id) |
|||
li.uk-nav-header.no-select Author menu |
|||
li |
|||
a( |
|||
href="", |
|||
data-comment-id= comment._id, |
|||
onclick="return dtp.app.deleteComment(event);", |
|||
) Delete |
|||
else if user |
|||
li.uk-nav-header.no-select Moderation menu |
|||
li |
|||
a( |
|||
href="", |
|||
data-resource-type= comment.resourceType, |
|||
data-resource-id= resourceId, |
|||
data-comment-id= comment._id, |
|||
onclick="return dtp.app.showReportCommentForm(event);", |
|||
) Report |
|||
li |
|||
a( |
|||
href="", |
|||
data-resource-type= comment.resourceType, |
|||
data-resource-id= resourceId, |
|||
data-comment-id= comment._id, |
|||
onclick="return dtp.app.blockCommentAuthor(event);", |
|||
) Block author |
|||
|
|||
.uk-comment-body |
|||
case comment.status |
|||
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 |
|||
.uk-container.uk-text-small.uk-text-center |
|||
ul.uk-subnav.uk-flex-center |
|||
each socialIcon in socialIcons |
|||
if site.gabUrl |
|||
li |
|||
a(href=socialIcon.url).dtp-social-link |
|||
a(href= site.gabUrl).dtp-social-link |
|||
span |
|||
i(class=`fab ${socialIcon.icon}`) |
|||
span.uk-margin-small-left= socialIcon.label |
|||
img(src="/img/gab-g.svg", style="width: auto; height: 1em;") |
|||
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 |
|||
hr |
|||
div Copyright © 2021 #[+renderSiteLink()] |
|||
|
|||
div Copyright © 2021 #[+renderSiteLink()] |
|||
div All Rights Reserved |
@ -1,8 +1,8 @@ |
|||
block facebook-card |
|||
meta(property='og:site_name', content= site.name) |
|||
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:title', content= `${site.name} | ${site.description}`) |
|||
meta(property='og:description', content= site.description) |
|||
meta(property='og:image:alt', content= `${site.name} | ${site.description}`) |
|||
meta(property='og:title', content= pageTitle || site.name) |
|||
meta(property='og:description', content= pageDescription || site.description) |
|||
meta(property='og:image:alt', content= `${site.name} | ${site.description}`) |
|||
|
@ -1,5 +1,5 @@ |
|||
block twitter-card |
|||
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:title', content= `${site.name} | ${site.description}`) |
|||
meta(name='twitter:description', content= site.description) |
|||
meta(name='twitter:image' content= `https://${site.domain}/img/social-cards/${site.domainKey}.png?v=${pkg.version}`) |
|||
meta(name='twitter:title', content= pageTitle || site.name) |
|||
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 |
|||
block content |
|||
|
|||
include ../comment/components/comment |
|||
|
|||
section.uk-section.uk-section-default |
|||
.uk-container |
|||
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