parent
d534b7950e
commit
929a8875ef
@ -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 have changed in this diff Show More
Loading…
Reference in new issue