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