Browse Source

integrate updates from LibertyLinks engine enhancements; new features for blog

develop
Rob Colbert 2 years ago
parent
commit
929a8875ef
  1. 1
      .gitignore
  2. 15
      .vscode/launch.json
  3. 35
      app/controllers/admin.js
  4. 95
      app/controllers/admin/content-report.js
  5. 49
      app/controllers/admin/host.js
  6. 57
      app/controllers/admin/log.js
  7. 4
      app/controllers/admin/newsletter.js
  8. 4
      app/controllers/admin/page.js
  9. 4
      app/controllers/admin/post.js
  10. 4
      app/controllers/admin/user.js
  11. 14
      app/controllers/auth.js
  12. 118
      app/controllers/comment.js
  13. 112
      app/controllers/content-report.js
  14. 11
      app/controllers/home.js
  15. 14
      app/controllers/image.js
  16. 10
      app/controllers/manifest.js
  17. 16
      app/controllers/newsletter.js
  18. 13
      app/controllers/page.js
  19. 56
      app/controllers/post.js
  20. 141
      app/controllers/user.js
  21. 12
      app/controllers/welcome.js
  22. 11
      app/models/comment.js
  23. 31
      app/models/content-report.js
  24. 26
      app/models/content-vote.js
  25. 4
      app/models/email-blacklist.js
  26. 22
      app/models/lib/geo-types.js
  27. 12
      app/models/lib/resource-stats.js
  28. 4
      app/models/log.js
  29. 1
      app/models/page.js
  30. 5
      app/models/post.js
  31. 4
      app/models/resource-view.js
  32. 29
      app/models/resource-visit.js
  33. 15
      app/models/user-block.js
  34. 20
      app/models/user-notification.js
  35. 8
      app/models/user.js
  36. 53
      app/services/chat.js
  37. 60
      app/services/comment.js
  38. 127
      app/services/content-report.js
  39. 98
      app/services/content-vote.js
  40. 7
      app/services/display-engine.js
  41. 44
      app/services/log.js
  42. 12
      app/services/minio.js
  43. 18
      app/services/otp-auth.js
  44. 18
      app/services/page.js
  45. 74
      app/services/resource.js
  46. 100
      app/services/user-notification.js
  47. 185
      app/services/user.js
  48. 21
      app/views/admin/components/menu.pug
  49. 31
      app/views/admin/content-report/index.pug
  50. 48
      app/views/admin/content-report/view.pug
  51. 52
      app/views/admin/host/index.pug
  52. 22
      app/views/admin/index.pug
  53. 5
      app/views/admin/layouts/main.pug
  54. 46
      app/views/admin/log/index.pug
  55. 4
      app/views/admin/page/editor.pug
  56. 68
      app/views/admin/settings/editor.pug
  57. 6
      app/views/admin/user/form.pug
  58. 15
      app/views/admin/user/index.pug
  59. 11
      app/views/comment/components/comment-list.pug
  60. 13
      app/views/comment/components/comment-review.pug
  61. 155
      app/views/comment/components/comment.pug
  62. 36
      app/views/comment/components/composer.pug
  63. 35
      app/views/comment/components/report-form.pug
  64. 13
      app/views/components/file-upload-image.pug
  65. 4
      app/views/components/labeled-icon.pug
  66. 1
      app/views/components/library.pug
  67. 36
      app/views/components/navbar.pug
  68. 23
      app/views/components/off-canvas.pug
  69. 69
      app/views/components/page-footer.pug
  70. 2
      app/views/components/page-sidebar.pug
  71. 8
      app/views/components/social-card/facebook.pug
  72. 6
      app/views/components/social-card/twitter.pug
  73. 5
      app/views/error.pug
  74. 14
      app/views/index.pug
  75. 6
      app/views/layouts/main-sidebar.pug
  76. 5
      app/views/layouts/main.pug
  77. 2
      app/views/notification/components/notification-standalone.pug
  78. 5
      app/views/notification/components/notification.pug
  79. 5
      app/views/page/view.pug
  80. 54
      app/views/post/view.pug
  81. 21
      app/views/user/profile.pug
  82. 22
      app/views/user/settings.pug
  83. 7
      app/views/welcome/index.pug
  84. 4
      app/views/welcome/signup.pug
  85. 78
      app/workers/reeeper.js
  86. 62
      client/img/social-icons/bitchute.svg
  87. 69
      client/img/social-icons/dlive.svg
  88. 102
      client/img/social-icons/odysee.svg
  89. 95
      client/img/social-icons/rumble.svg
  90. 295
      client/js/site-app.js
  91. 25
      client/less/site/button.less
  92. 61
      client/less/site/comment.less
  93. 7
      client/less/site/dashboard.less
  94. 6
      client/less/site/image.less
  95. 1
      client/less/style.less
  96. 36
      config/limiter.js
  97. 8
      deploy
  98. 6
      lib/client/js/dtp-display-engine.js
  99. 8
      lib/site-controller.js
  100. 39
      lib/site-log.js

1
.gitignore

@ -1,5 +1,6 @@
.env
data/minio
node_modules
dist
data/minio

15
.vscode/launch.json

@ -7,7 +7,7 @@
{
"type": "pwa-node",
"request": "launch",
"name": "dtp-sites",
"name": "web:dtp-sites",
"skipFiles": [
"<node_internals>/**"
],
@ -16,6 +16,17 @@
"env": {
"HTTP_BIND_PORT": "3333"
}
},
{
"type": "pwa-node",
"request": "launch",
"name": "cli:dtp-sites",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder:dtp-sites}/dtp-sites-cli.js",
"console": "integratedTerminal",
"args": ["--action=reset-indexes", "all"]
}
]
}
}

35
app/controllers/admin.js

@ -44,19 +44,32 @@ class AdminController extends SiteController {
}),
);
router.use('/content-report',await this.loadChild(path.join(__dirname, 'admin', 'content-report')));
router.use('/host',await this.loadChild(path.join(__dirname, 'admin', 'host')));
router.use('/job-queue',await this.loadChild(path.join(__dirname, 'admin', 'job-queue')));
router.use('/newsletter',await this.loadChild(path.join(__dirname, 'admin', 'newsletter')));
router.use('/page',await this.loadChild(path.join(__dirname, 'admin', 'page')));
router.use('/post',await this.loadChild(path.join(__dirname, 'admin', 'post')));
router.use('/settings',await this.loadChild(path.join(__dirname, 'admin', 'settings')));
router.use('/job-queue', await this.loadChild(path.join(__dirname, 'admin', 'job-queue')));
router.use('/log', await this.loadChild(path.join(__dirname, 'admin', 'log')));
router.use('/newsletter', await this.loadChild(path.join(__dirname, 'admin', 'newsletter')));
router.use('/page', await this.loadChild(path.join(__dirname, 'admin', 'page')));
router.use('/post', await this.loadChild(path.join(__dirname, 'admin', 'post')));
router.use('/settings', await this.loadChild(path.join(__dirname, 'admin', 'settings')));
router.use('/user', await this.loadChild(path.join(__dirname, 'admin', 'user')));
router.get('/diagnostics', this.getDiagnostics.bind(this));
router.get('/', this.getHomeView.bind(this));
return router;
}
async getDiagnostics (req, res) {
res.status(200).json({
success: true,
url: req.url,
ip: req.ip,
headers: req.headers,
});
}
async getHomeView (req, res) {
res.locals.stats = {
memberCount: await User.estimatedDocumentCount(),
@ -65,7 +78,11 @@ class AdminController extends SiteController {
}
}
module.exports = async (dtp) => {
let controller = new AdminController(dtp);
return controller;
};
module.exports = {
slug: 'admin',
name: 'admin',
create: async (dtp) => {
let controller = new AdminController(dtp);
return controller;
},
};

95
app/controllers/admin/content-report.js

@ -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;
};

49
app/controllers/admin/host.js

@ -30,6 +30,8 @@ class HostController extends SiteController {
router.param('hostId', this.populateHostId.bind(this));
router.post('/:hostId/deactivate', this.postDeactiveHost.bind(this));
router.get('/:hostId', this.getHostView.bind(this));
router.get('/', this.getHomeView.bind(this));
@ -46,6 +48,38 @@ class HostController extends SiteController {
}
}
async postDeactiveHost (req, res) {
try {
const displayList = this.createDisplayList('deactivate-host');
await NetHost.updateOne(
{ _id: res.locals.host._id },
{
$set: { status: 'inactive' },
},
);
displayList.removeElement(`tr[data-host-id="${res.locals.host._id}"]`);
displayList.showNotification(
`Host "${res.locals.host.hostname}" deactivated`,
'success',
'bottom-center',
3000,
);
res.status(200).json({ success: true, displayList });
} catch (error) {
this.log.error('failed to deactivate host', {
hostId: res.local.host._id,
error,
});
res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
async getHostView (req, res, next) {
try {
res.locals.stats = await NetHostStats
@ -62,7 +96,20 @@ class HostController extends SiteController {
async getHomeView (req, res, next) {
try {
res.locals.hosts = await NetHost.find({ status: { $ne: 'inactive' } });
const HOST_SELECT = '_id created updated hostname status platform arch totalmem freemem';
res.locals.activeHosts = await NetHost
.find({ status: 'active' })
.select(HOST_SELECT)
.sort({ updated: 1 })
.lean();
res.locals.crashedHosts = await NetHost
.find({ status: 'crashed' })
.select(HOST_SELECT)
.sort({ updated: 1 })
.lean();
res.render('admin/host/index');
} catch (error) {
return next(error);

57
app/controllers/admin/log.js

@ -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;
};

4
app/controllers/admin/newsletter.js

@ -90,9 +90,9 @@ class NewsletterController extends SiteController {
}
async deleteNewsletter (req, res) {
const { newsletter: newsletterService, displayEngine: displayEngineService } = this.dtp.services;
const { newsletter: newsletterService } = this.dtp.services;
try {
const displayList = displayEngineService.createDisplayList('delete-newsletter');
const displayList = this.createDisplayList('delete-newsletter');
await newsletterService.deleteNewsletter(res.locals.newsletter);

4
app/controllers/admin/page.js

@ -102,9 +102,9 @@ class PageController extends SiteController {
}
async deletePage (req, res) {
const { page: pageService, displayEngine: displayEngineService } = this.dtp.services;
const { page: pageService } = this.dtp.services;
try {
const displayList = displayEngineService.createDisplayList('delete-page');
const displayList = this.createDisplayList('delete-page');
await pageService.deletePage(res.locals.page);

4
app/controllers/admin/post.js

@ -91,9 +91,9 @@ class PostController extends SiteController {
}
async deletePost (req, res) {
const { post: postService, displayEngine: displayEngineService } = this.dtp.services;
const { post: postService } = this.dtp.services;
try {
const displayList = displayEngineService.createDisplayList('delete-post');
const displayList = this.createDisplayList('delete-post');
await postService.deletePost(res.locals.post);

4
app/controllers/admin/user.js

@ -27,6 +27,7 @@ class UserController extends SiteController {
router.param('userId', this.populateUserId.bind(this));
router.post('/:userId', this.postUpdateUser.bind(this));
router.get('/:userId', this.getUserView.bind(this));
router.get('/', this.getHomeView.bind(this));
@ -61,7 +62,8 @@ class UserController extends SiteController {
const { user: userService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 10);
res.locals.userAccounts = await userService.getUserAccounts(res.locals.pagination);
res.locals.userAccounts = await userService.getUserAccounts(res.locals.pagination, req.query.u);
res.locals.totalUserCount = await userService.getTotalCount();
res.render('admin/user/index');
} catch (error) {
return next(error);

14
app/controllers/auth.js

@ -24,7 +24,7 @@ class AuthController extends SiteController {
async start ( ) {
const { limiter: limiterService } = this.dtp.services;
const upload = multer({ });
const upload = multer({ dest: `/tmp/${this.dtp.config.site.domainKey}/uploads/${DTP_COMPONENT_NAME}` });
const router = express.Router();
this.dtp.app.use('/auth', router);
@ -189,7 +189,11 @@ class AuthController extends SiteController {
}
}
module.exports = async (dtp) => {
let controller = new AuthController(dtp);
return controller;
};
module.exports = {
slug: 'auth',
name: 'auth',
create: async (dtp) => {
let controller = new AuthController(dtp);
return controller;
},
};

118
app/controllers/comment.js

@ -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;
},
};

112
app/controllers/content-report.js

@ -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;
},
};

11
app/controllers/home.js

@ -47,7 +47,12 @@ class HomeController extends SiteController {
}
}
module.exports = async (dtp) => {
let controller = new HomeController(dtp);
return controller;
module.exports = {
slug: 'home',
name: 'home',
isHome: true,
create: async (dtp) => {
let controller = new HomeController(dtp);
return controller;
},
};

14
app/controllers/image.js

@ -29,7 +29,7 @@ class ImageController extends SiteController {
dtp.app.use('/image', router);
const imageUpload = multer({
dest: '/tmp/dtp-sites/upload/image',
dest: `/tmp/${this.dtp.config.site.domainKey}/uploads/${DTP_COMPONENT_NAME}`,
limits: {
fileSize: 1024 * 1000 * 12,
},
@ -135,7 +135,11 @@ class ImageController extends SiteController {
}
}
module.exports = async (dtp) => {
let controller = new ImageController(dtp);
return controller;
};
module.exports = {
slug: 'image',
name: 'image',
create: async (dtp) => {
let controller = new ImageController(dtp);
return controller;
},
};

10
app/controllers/manifest.js

@ -65,7 +65,11 @@ class ManifestController extends SiteController {
}
}
module.exports = async (dtp) => {
let controller = new ManifestController(dtp);
return controller;
module.exports = {
slug: 'manifest',
name: 'manifest',
create: async (dtp) => {
let controller = new ManifestController(dtp);
return controller;
},
};

16
app/controllers/newsletter.js

@ -21,7 +21,7 @@ class NewsletterController extends SiteController {
const { dtp } = this;
const { limiter: limiterService } = dtp.services;
const upload = multer({ dest: '/tmp' });
const upload = multer({ dest: `/tmp/${this.dtp.config.site.domainKey}/uploads/${DTP_COMPONENT_NAME}` });
const router = express.Router();
dtp.app.use('/newsletter', router);
@ -58,9 +58,9 @@ class NewsletterController extends SiteController {
}
async postAddRecipient (req, res) {
const { newsletter: newsletterService, displayEngine: displayEngineService } = this.dtp.services;
const { newsletter: newsletterService } = this.dtp.services;
try {
const displayList = displayEngineService.createDisplayList('add-recipient');
const displayList = this.createDisplayList('add-recipient');
await newsletterService.addRecipient(req.body.email);
displayList.showNotification(
'You have been added to the newsletter. Please check your email and verify your email address.',
@ -94,7 +94,11 @@ class NewsletterController extends SiteController {
}
}
module.exports = async (dtp) => {
let controller = new NewsletterController(dtp);
return controller;
module.exports = {
slug: 'newsletter',
name: 'newsletter',
create: async (dtp) => {
let controller = new NewsletterController(dtp);
return controller;
},
};

13
app/controllers/page.js

@ -25,7 +25,7 @@ class PageController extends SiteController {
router.use(this.dtp.services.gabTV.channelMiddleware('mrjoeprich'));
router.use(async (req, res, next) => {
res.locals.currentView = 'home';
res.locals.currentView = 'page';
return next();
});
@ -55,6 +55,7 @@ class PageController extends SiteController {
const { resource: resourceService } = this.dtp.services;
try {
await resourceService.recordView(req, 'Page', res.locals.page._id);
res.locals.pageSlug = res.locals.page.slug;
res.render('page/view');
} catch (error) {
this.log.error('failed to service page view', { pageId: res.locals.page._id, error });
@ -63,7 +64,11 @@ class PageController extends SiteController {
}
}
module.exports = async (dtp) => {
let controller = new PageController(dtp);
return controller;
module.exports = {
slug: 'page',
name: 'page',
create: async (dtp) => {
let controller = new PageController(dtp);
return controller;
},
};

56
app/controllers/post.js

@ -19,8 +19,13 @@ class PostController extends SiteController {
async start ( ) {
const { dtp } = this;
const { limiter: limiterService } = dtp.services;
const {
comment: commentService,
limiter: limiterService,
session: sessionService,
} = dtp.services;
const authRequired = sessionService.authCheckMiddleware({ requiredLogin: true });
const upload = multer({ dest: `/tmp/dtp-sites/${this.dtp.config.site.domainKey}`});
const router = express.Router();
@ -33,8 +38,10 @@ class PostController extends SiteController {
});
router.param('postSlug', this.populatePostSlug.bind(this));
router.param('commentId', commentService.populateCommentId.bind(commentService));
router.post('/:postSlug/comment', upload.none(), this.postComment.bind(this));
router.post('/:postSlug/comment/:commentId/block-author', authRequired, upload.none(), this.postBlockCommentAuthor.bind(this));
router.post('/:postSlug/comment', authRequired, upload.none(), this.postComment.bind(this));
router.get('/:postSlug',
limiterService.create(limiterService.config.post.getView),
@ -61,13 +68,31 @@ class PostController extends SiteController {
}
}
async postBlockCommentAuthor (req, res) {
const { user: userService } = this.dtp.services;
try {
const displayList = this.createDisplayList('add-recipient');
await userService.blockUser(req.user._id, req.body.userId);
displayList.showNotification(
'Comment author blocked',
'success',
'bottom-center',
4000,
);
res.status(200).json({ success: true, displayList });
} catch (error) {
this.log.error('failed to report comment', { error });
return res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
async postComment (req, res) {
const {
comment: commentService,
displayEngine: displayEngineService,
} = this.dtp.services;
const { comment: commentService } = this.dtp.services;
try {
const displayList = displayEngineService.createDisplayList('add-recipient');
const displayList = this.createDisplayList('add-recipient');
res.locals.comment = await commentService.create(req.user, 'Post', res.locals.post, req.body);
@ -98,9 +123,14 @@ class PostController extends SiteController {
await resourceService.recordView(req, 'Post', res.locals.post._id);
res.locals.pagination = this.getPaginationParameters(req, 20);
if (req.query.comment) {
res.locals.featuredComment = await commentService.getById(req.query.comment);
}
res.locals.comments = await commentService.getForResource(
res.locals.post,
['published', 'mod-warn'],
['published', 'mod-warn', 'mod-removed', 'removed'],
res.locals.pagination,
);
@ -123,7 +153,11 @@ class PostController extends SiteController {
}
}
module.exports = async (dtp) => {
let controller = new PostController(dtp);
return controller;
module.exports = {
slug: 'post',
name: 'post',
create: async (dtp) => {
let controller = new PostController(dtp);
return controller;
},
};

141
app/controllers/user.js

@ -20,12 +20,17 @@ class UserController extends SiteController {
async start ( ) {
const { dtp } = this;
const { limiter: limiterService, otpAuth: otpAuthService } = dtp.services;
const {
limiter: limiterService,
otpAuth: otpAuthService,
session: sessionService,
} = dtp.services;
const upload = multer({ dest: "/tmp" });
const upload = multer({ dest: `/tmp/${this.dtp.config.site.domainKey}/uploads/${DTP_COMPONENT_NAME}` });
const router = express.Router();
dtp.app.use('/user', router);
const authRequired = sessionService.authCheckMiddleware({ requireLogin: true });
const otpMiddleware = otpAuthService.middleware('Account', {
adminRequired: false,
otpRequired: false,
@ -53,6 +58,12 @@ class UserController extends SiteController {
router.param('userId', this.populateUser.bind(this));
router.post('/:userId/profile-photo',
limiterService.create(limiterService.config.user.postProfilePhoto),
upload.single('imageFile'),
this.postProfilePhoto.bind(this),
);
router.post('/:userId/settings',
limiterService.create(limiterService.config.user.postUpdateSettings),
upload.none(),
@ -66,16 +77,25 @@ class UserController extends SiteController {
router.get('/:userId/settings',
limiterService.create(limiterService.config.user.getSettings),
authRequired,
otpMiddleware,
checkProfileOwner,
this.getUserSettingsView.bind(this),
);
router.get('/:userId',
limiterService.create(limiterService.config.user.getUserProfile),
authRequired,
otpMiddleware,
checkProfileOwner,
this.getUserView.bind(this),
);
router.delete('/:userId/profile-photo',
limiterService.create(limiterService.config.user.deleteProfilePhoto),
authRequired,
checkProfileOwner,
this.deleteProfilePhoto.bind(this),
);
}
async populateUser (req, res, next, userId) {
@ -100,7 +120,23 @@ class UserController extends SiteController {
async postCreateUser (req, res, next) {
const { user: userService } = this.dtp.services;
try {
// verify that the request has submitted a captcha
if ((typeof req.body.captcha !== 'string') || req.body.captcha.length === 0) {
throw new SiteError(403, 'Invalid signup attempt');
}
// verify that the session has a signup captcha
if (!req.session.captcha || !req.session.captcha.signup) {
throw new SiteError(403, 'Invalid signup attempt');
}
// verify that the captcha from the form matches the captcha in the signup session flow
if (req.body.captcha !== req.session.captcha.signup) {
throw new SiteError(403, 'The captcha value is not correct');
}
// create the user account
res.locals.user = await userService.create(req.body);
// log the user in
req.login(res.locals.user, (error) => {
if (error) {
return next(error);
@ -113,10 +149,52 @@ class UserController extends SiteController {
}
}
async postProfilePhoto (req, res) {
const { user: userService } = this.dtp.services;
try {
const displayList = this.createDisplayList('profile-photo');
await userService.updatePhoto(req.user, req.file);
displayList.showNotification(
'Profile photo updated successfully.',
'success',
'bottom-center',
2000,
);
res.status(200).json({ success: true, displayList });
} catch (error) {
this.log.error('failed to update profile photo', { error });
return res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
async postHeaderImage (req, res) {
const { user: userService } = this.dtp.services;
try {
const displayList = this.createDisplayList('header-image');
await userService.updateHeaderImage(req.user, req.file);
displayList.showNotification(
'Header image updated successfully.',
'success',
'bottom-center',
2000,
);
res.status(200).json({ success: true, displayList });
} catch (error) {
this.log.error('failed to update header image', { error });
return res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
async postUpdateSettings (req, res) {
const { user: userService, displayEngine: displayEngineService } = this.dtp.services;
const { user: userService } = this.dtp.services;
try {
const displayList = displayEngineService.createDisplayList('app-settings');
const displayList = this.createDisplayList('app-settings');
await userService.updateSettings(req.user, req.body);
@ -147,16 +225,65 @@ class UserController extends SiteController {
}
async getUserView (req, res, next) {
const { comment: commentService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.commentHistory = await commentService.getForAuthor(req.user, res.locals.pagination);
res.render('user/profile');
} catch (error) {
this.log.error('failed to produce user profile view', { error });
return next(error);
}
}
async deleteProfilePhoto (req, res) {
const { user: userService } = this.dtp.services;
try {
const displayList = this.createDisplayList('app-settings');
await userService.removePhoto(req.user);
displayList.showNotification(
'Profile photo removed successfully.',
'success',
'bottom-center',
2000,
);
res.status(200).json({ success: true, displayList });
} catch (error) {
this.log.error('failed to remove profile photo', { error });
return res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
async deleteHeaderImage (req, res) {
const { user: userService } = this.dtp.services;
try {
const displayList = this.createDisplayList('remove-header-image');
await userService.removeHeaderImage(req.user);
displayList.showNotification(
'Header image removed successfully.',
'success',
'bottom-center',
2000,
);
res.status(200).json({ success: true, displayList });
} catch (error) {
this.log.error('failed to remove header image', { error });
return res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
}
module.exports = async (dtp) => {
let controller = new UserController(dtp);
return controller;
module.exports = {
slug: 'user',
name: 'user',
create: async (dtp) => {
let controller = new UserController(dtp);
return controller;
},
};

12
app/controllers/welcome.js

@ -68,7 +68,11 @@ class WelcomeController extends SiteController {
}
}
module.exports = async (dtp) => {
let controller = new WelcomeController(dtp);
return controller;
};
module.exports = {
slug: 'welcome',
name: 'welcome',
create: async (dtp) => {
let controller = new WelcomeController(dtp);
return controller;
},
};

11
app/models/comment.js

@ -14,19 +14,26 @@ const CommentHistorySchema = new Schema({
content: { type: String, maxlength: 3000 },
});
const { CommentStats, CommentStatsDefaults } = require(path.join(__dirname, 'lib', 'resource-stats.js'));
const {
RESOURCE_TYPE_LIST,
CommentStats,
CommentStatsDefaults,
} = require(path.join(__dirname, 'lib', 'resource-stats.js'));
const COMMENT_STATUS_LIST = ['published', 'removed', 'mod-warn', 'mod-removed'];
const CommentSchema = new Schema({
created: { type: Date, default: Date.now, required: true, index: 1 },
resourceType: { type: String, enum: ['Post', 'Page', 'Newsletter'], required: true },
resourceType: { type: String, enum: RESOURCE_TYPE_LIST, required: true },
resource: { type: Schema.ObjectId, required: true, index: 1, refPath: 'resourceType' },
author: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' },
replyTo: { type: Schema.ObjectId, index: 1, ref: 'Comment' },
status: { type: String, enum: COMMENT_STATUS_LIST, default: 'published', required: true },
content: { type: String, required: true, maxlength: 3000 },
contentHistory: { type: [CommentHistorySchema], select: false },
flags: {
isNSFW: { type: Boolean, default: false, required: true },
},
stats: { type: CommentStats, default: CommentStatsDefaults, required: true },
});

31
app/models/content-report.js

@ -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);

26
app/models/content-vote.js

@ -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);

4
app/models/email-blacklist.js

@ -24,11 +24,11 @@ const EmailBlacklistSchema = new Schema({
EmailBlacklistSchema.index({
email: 1,
'flags.isVerified': true,
'flags.isVerified': 1,
}, {
partialFilterExpression: {
'flags.isVerified': true,
},
});
module.exports = mongoose.model('EmailBlacklist', EmailBlacklistSchema);
module.exports = mongoose.model('EmailBlacklist', EmailBlacklistSchema);

22
app/models/lib/geo-types.js

@ -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 },
});

12
app/models/lib/resource-stats.js

@ -9,17 +9,13 @@ const mongoose = require('mongoose');
const Schema = mongoose.Schema;
module.exports.ResourceStats = new Schema({
totalViewCount: { type: Number, default: 0, required: true },
upvoteCount: { type: Number, default: 0, required: true },
downvoteCount: { type: Number, default: 0, required: true },
commentCount: { type: Number, default: 0, required: true },
uniqueVisitCount: { type: Number, default: 0, required: true, index: -1 },
totalVisitCount: { type: Number, default: 0, required: true },
});
module.exports.ResourceStatsDefaults = {
totalViewCount: 0,
upvoteCount: 0,
downvoteCount: 0,
commentCount: 0,
uniqueVisitCount: 0,
totalVisitCount: 0,
};
module.exports.CommentStats = new Schema({

4
app/models/log.js

@ -20,10 +20,10 @@ const LOG_LEVEL_LIST = [
const LogSchema = new Schema({
created: { type: Date, default: Date.now, required: true, index: -1, expires: '7d' },
componentName: { type: String, required: true },
componentName: { type: String, required: true, index: 1 },
level: { type: String, enum: LOG_LEVEL_LIST, required: true, index: true },
message: { type: String },
metadata: { type: Schema.Types.Mixed },
});
module.exports = mongoose.model('Log', LogSchema);
module.exports = mongoose.model('Log', LogSchema);

1
app/models/page.js

@ -19,6 +19,7 @@ const PageSchema = new Schema({
content: { type: String, required: true, select: false },
status: { type: String, enum: PAGE_STATUS_LIST, default: 'draft', index: true },
menu: {
icon: { type: String, required: true },
label: { type: String, required: true },
order: { type: Number, default: 0, required: true },
parent: { type: Schema.ObjectId, index: 1, ref: 'Page' },

5
app/models/post.js

@ -9,7 +9,10 @@ const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const { ResourceStats, ResourceStatsDefaults } = require(path.join(__dirname, 'lib', 'resource-stats.js'));
const {
ResourceStats,
ResourceStatsDefaults,
} = require(path.join(__dirname, 'lib', 'resource-stats.js'));
const POST_STATUS_LIST = ['draft','published','archived'];

4
app/models/resource-view.js

@ -15,7 +15,7 @@ const ResourceViewSchema = new Schema({
resourceType: { type: String, enum: RESOURCE_TYPE_LIST, required: true },
resource: { type: Schema.ObjectId, required: true, index: 1, refPath: 'resourceType' },
uniqueKey: { type: String, required: true, index: 1 },
viewCount: { type: Number, default: 0, required: true },
visitCount: { type: Number, default: 0, required: true },
});
ResourceViewSchema.index({
@ -27,4 +27,4 @@ ResourceViewSchema.index({
name: 'res_view_daily_unique',
});
module.exports = mongoose.model('ResourceView', ResourceViewSchema);
module.exports = mongoose.model('ResourceView', ResourceViewSchema);

29
app/models/resource-visit.js

@ -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);

15
app/models/user-block.js

@ -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);

20
app/models/user-notification.js

@ -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);

8
app/models/user.js

@ -8,6 +8,8 @@ const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const { ResourceStats, ResourceStatsDefaults } = require('./lib/resource-stats');
const UserFlagsSchema = new Schema({
isAdmin: { type: Boolean, default: false, required: true },
isModerator: { type: Boolean, default: false, required: true },
@ -16,6 +18,8 @@ const UserFlagsSchema = new Schema({
const UserPermissionsSchema = new Schema({
canLogin: { type: Boolean, default: true, required: true },
canChat: { type: Boolean, default: true, required: true },
canComment: { type: Boolean, default: true, required: true },
canReport: { type: Boolean, default: true, required: true },
});
const UserSchema = new Schema({
@ -26,12 +30,14 @@ const UserSchema = new Schema({
passwordSalt: { type: String, required: true },
password: { type: String, required: true },
displayName: { type: String },
bio: { type: String, maxlength: 300 },
picture: {
large: { type: Schema.ObjectId, ref: 'Image' },
small: { type: Schema.ObjectId, ref: 'Image' },
},
flags: { type: UserFlagsSchema, select: false },
permissions: { type: UserPermissionsSchema, select: false },
stats: { type: ResourceStats, default: ResourceStatsDefaults, required: true },
});
module.exports = mongoose.model('User', UserSchema);
module.exports = mongoose.model('User', UserSchema);

53
app/services/chat.js

@ -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); },
};

60
app/services/comment.js

@ -22,12 +22,30 @@ class CommentService extends SiteService {
this.populateComment = [
{
path: 'author',
select: '',
select: '_id username username_lc displayName picture',
},
{
path: 'replyTo',
},
];
this.populateCommentWithResource = [
{
path: 'author',
select: '_id username username_lc displayName picture',
},
{
path: 'replyTo',
},
{
path: 'resource',
populate: [
{
path: 'author',
select: '_id username username_lc displayName picture',
},
],
},
];
}
async start ( ) {
@ -35,6 +53,19 @@ class CommentService extends SiteService {
this.templates.comment = pug.compileFile(path.join(this.dtp.config.root, 'app', 'views', 'comment', 'components', 'comment-standalone.pug'));
}
async populateCommentId (req, res, next, commentId) {
try {
res.locals.comment = await this.getById(commentId);
if (!res.locals.comment) {
throw new SiteError(404, 'Comment not found');
}
return next();
} catch (error) {
this.log.error('failed to populate comment', { commentId, error });
return next(error);
}
}
async create (author, resourceType, resource, commentDefinition) {
const NOW = new Date();
let comment = new Comment();
@ -50,6 +81,10 @@ class CommentService extends SiteService {
comment.content = striptags(commentDefinition.content.trim());
}
comment.flags = {
isNSFW: commentDefinition.isNSFW === 'on',
};
await comment.save();
const model = mongoose.model(resourceType);
@ -116,6 +151,7 @@ class CommentService extends SiteService {
* @param {String} status
*/
async remove (comment, status = 'removed') {
const { contentReport: contentReportService } = this.dtp.services;
await Comment.updateOne(
{ _id: comment._id },
{
@ -131,6 +167,15 @@ class CommentService extends SiteService {
},
},
);
await contentReportService.removeForResource(comment);
}
async getById (commentId) {
const comment = await Comment
.findById(commentId)
.populate(this.populateComment)
.lean();
return comment;
}
async getForResource (resource, statuses, pagination) {
@ -144,6 +189,17 @@ class CommentService extends SiteService {
return comments;
}
async getForAuthor (author, pagination) {
const comments = await Comment
.find({ author: author._id, status: { $in: ['published', 'mod-warn'] } })
.sort({ created: -1 })
.skip(pagination.skip)
.limit(pagination.cpp)
.populate(this.populateCommentWithResource)
.lean();
return comments;
}
async getContentHistory (comment, pagination) {
/*
* Extract a page from the contentHistory using $slice on the array
@ -170,4 +226,4 @@ module.exports = {
slug: 'comment',
name: 'comment',
create: (dtp) => { return new CommentService(dtp); },
};
};

127
app/services/content-report.js

@ -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); },
};

98
app/services/content-vote.js

@ -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); },
};

7
app/services/display-engine.js

@ -102,6 +102,13 @@ class DisplayList {
params: { remove, add },
});
}
navigateTo (href) {
this.commands.push({
action: 'navigateTo',
params: { href },
});
}
}
class DisplayEngineService extends SiteService {

44
app/services/log.js

@ -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); },
};

12
app/services/minio.js

@ -32,6 +32,16 @@ class MinioService extends SiteService {
this.log.info(`stopping ${module.exports.name} service`);
}
async makeBucket (name, region) {
try {
const result = await this.minio.makeBucket(name, region);
return result;
} catch (error) {
this.log.error('failed to create bucket on MinIO', { name, region, error });
throw error;
}
}
async uploadFile (fileInfo) {
try {
const result = await this.minio.fPutObject(
@ -75,4 +85,4 @@ module.exports = {
slug: 'minio',
name: 'minio',
create: (dtp) => { return new MinioService(dtp); },
};
};

18
app/services/otp-auth.js

@ -77,12 +77,14 @@ class OtpAuthService extends SiteService {
}
if (!res.locals.otpAccount) {
let issuer;
if (process.env.NODE_ENV === 'production') {
issuer = `${this.dtp.config.site.name}: ${serviceName}`;
} else {
issuer = `${this.dtp.config.site.name}:${process.env.NODE_ENV}: ${serviceName}`;
}
res.locals.otpTempSecret = authenticator.generateSecret();
res.locals.otpKeyURI = authenticator.keyuri(
req.user.username.trim(),
`${this.dtp.config.site.name}: ${serviceName}`,
res.locals.otpTempSecret,
);
res.locals.otpKeyURI = authenticator.keyuri(req.user.username.trim(), issuer, res.locals.otpTempSecret);
req.session.otp[serviceName] = req.session.otp[serviceName] || { };
req.session.otp[serviceName].secret = res.locals.otpTempSecret;
req.session.otp[serviceName].redirectURL = res.locals.otpRedirectURL;
@ -210,10 +212,14 @@ class OtpAuthService extends SiteService {
return true;
}
async removeForUser (user) {
return await OtpAccount.deleteMany({ user: user });
}
}
module.exports = {
slug: 'otp-auth',
name: 'otpAuth',
create: (dtp) => { return new OtpAuthService(dtp); },
};
};

18
app/services/page.js

@ -22,11 +22,18 @@ class PageService extends SiteService {
async menuMiddleware (req, res, next) {
try {
const pages = await Page.find({ parent: { $exists: false } }).lean();
const pages = await Page
.find({ parent: { $exists: false } })
.select('slug menu')
.lean();
res.locals.mainMenu = pages
.filter((page) => !page.parent)
.map((page) => {
return {
url: `/page/${page.slug}`,
slug: page.slug,
icon: page.menu.icon,
label: page.menu.label,
order: page.menu.order,
};
@ -47,10 +54,13 @@ class PageService extends SiteService {
page.slug = this.createPageSlug(page._id, page.title);
page.content = pageDefinition.content.trim();
page.status = pageDefinition.status || 'draft';
page.menu = {
label: pageDefinition.menuLabel || page.title.slice(0, 10),
icon: striptags((pageDefinition.menuIcon || 'fa-slash').trim().toLowerCase()),
label: striptags((pageDefinition.menuLabel || page.title.slice(0, 10))),
order: parseInt(pageDefinition.menuOrder || '0', 10),
};
if (pageDefinition.parentPageId && (pageDefinition.parentPageId !== 'none')) {
page.menu.parent = pageDefinition.parentPageId;
}
@ -90,9 +100,11 @@ class PageService extends SiteService {
}
updateOp.$set.menu = {
label: pageDefinition.menuLabel || updateOp.$set.title.slice(0, 10),
icon: striptags((pageDefinition.menuIcon || 'fa-slash').trim().toLowerCase()),
label: striptags((pageDefinition.menuLabel || page.title.slice(0, 10))),
order: parseInt(pageDefinition.menuOrder || '0', 10),
};
if (pageDefinition.parentPageId && (pageDefinition.parentPageId !== 'none')) {
updateOp.$set.menu.parent = pageDefinition.parentPageId;
}

74
app/services/resource.js

@ -6,9 +6,12 @@
const { SiteService } = require('../../lib/site-lib');
const geoip = require('geoip-lite');
const mongoose = require('mongoose');
const ResourceView = mongoose.model('ResourceView');
const ResourceVisit = mongoose.model('ResourceVisit');
class ResourceService extends SiteService {
@ -32,7 +35,11 @@ class ResourceService extends SiteService {
* a view is being tracked.
*/
async recordView (req, resourceType, resourceId) {
const CURRENT_DAY = new Date();
const Model = mongoose.model(resourceType);
const modelUpdate = { $inc: { } };
const NOW = new Date();
const CURRENT_DAY = new Date(NOW);
CURRENT_DAY.setHours(0, 0, 0, 0);
let uniqueKey = req.ip.toString().trim().toLowerCase();
@ -48,20 +55,65 @@ class ResourceService extends SiteService {
uniqueKey,
},
{
$inc: { viewCount: 1 },
$inc: { 'stats.visitCount': 1 },
},
{ upsert: true },
);
this.log.debug('resource view', { response });
if (response.upsertedCount > 0) {
const Model = mongoose.model(resourceType);
await Model.updateOne(
{ _id: resourceId },
{
$inc: { 'stats.totalViewCount': 1 },
},
);
modelUpdate.$inc['stats.uniqueViewCount'] = 1;
}
/*
* Record the ResourceVisit
*/
const visit = new ResourceVisit();
visit.created = NOW;
visit.resourceType = resourceType;
visit.resource = resourceId;
if (req.user) {
visit.user = req.user._id;
}
/*
* We geo-analyze (but do not store) the IP address.
*/
const ipAddress = req.ip;
const geo = geoip.lookup(ipAddress);
if (geo) {
visit.geoip = {
country: geo.country,
region: geo.region,
eu: geo.eu,
timezone: geo.timezone,
city: geo.city,
};
if (Array.isArray(geo.ll) && (geo.ll.length === 2)) {
visit.geoip.location = {
type: 'Point',
coordinates: geo.ll,
};
}
}
await visit.save();
modelUpdate.$inc['stats.totalVisitCount'] = 1;
await Model.updateOne({ _id: resourceId }, modelUpdate);
}
async remove (resourceType, resource) {
this.log.debug('removing resource view records', { resourceType, resourceId: resource._id });
await ResourceView.deleteMany({ resource: resource._id });
this.log.debug('removing resource visit records', { resourceType, resourceId: resource._id });
await ResourceVisit.deleteMany({ resource: resource._id });
this.log.debug('removing resource', { resourceType, resourceId: resource._id });
const Model = mongoose.model(resourceType);
await Model.deleteOne({ _id: resource._id });
}
}
@ -69,4 +121,4 @@ module.exports = {
slug: 'resource',
name: 'resource',
create: (dtp) => { return new ResourceService(dtp); },
};
};

100
app/services/user-notification.js

@ -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); },
};

185
app/services/user.js

@ -5,7 +5,9 @@
'use strict';
const mongoose = require('mongoose');
const User = mongoose.model('User');
const UserBlock = mongoose.model('UserBlock');
const passport = require('passport');
const PassportLocal = require('passport-local');
@ -20,6 +22,15 @@ class UserService {
constructor (dtp) {
this.dtp = dtp;
this.log = new SiteLog(dtp, `svc:${module.exports.slug}`);
this.populateUser = [
{
path: 'picture.large',
},
{
path: 'picture.small',
},
];
}
async start ( ) {
@ -47,6 +58,7 @@ class UserService {
// strip characters we don't want to allow in username
userDefinition.username = userDefinition.username.trim().replace(/[^A-Za-z0-9\-_]/gi, '');
const username_lc = userDefinition.username.toLowerCase();
await this.checkUsername(username_lc);
// test the email address for validity, blacklisting, etc.
await mailService.checkEmailAddress(userDefinition.email);
@ -72,6 +84,7 @@ class UserService {
user.email = userDefinition.email;
user.username = userDefinition.username;
user.username_lc = username_lc;
user.displayName = striptags(userDefinition.displayName || userDefinition.username);
user.passwordSalt = passwordSalt;
user.password = maskedPassword;
@ -84,6 +97,8 @@ class UserService {
user.permissions = {
canLogin: true,
canChat: true,
canComment: true,
canReport: true,
};
this.log.info('creating new user account', { email: userDefinition.email });
@ -102,6 +117,7 @@ class UserService {
const username_lc = userDefinition.username.toLowerCase();
userDefinition.displayName = striptags(userDefinition.displayName.trim());
userDefinition.bio = striptags(userDefinition.bio.trim());
this.log.info('updating user', { userDefinition });
await User.updateOne(
@ -111,10 +127,13 @@ class UserService {
username: userDefinition.username,
username_lc,
displayName: userDefinition.displayName,
bio: userDefinition.bio,
'flags.isAdmin': userDefinition.isAdmin === 'on',
'flags.isModerator': userDefinition.isModerator === 'on',
'permissions.canLogin': userDefinition.canLogin === 'on',
'permissions.canChat': userDefinition.canChat === 'on',
'permissions.canComment': userDefinition.canComment === 'on',
'permissions.canReport': userDefinition.canReport === 'on',
},
},
);
@ -126,6 +145,7 @@ class UserService {
const username_lc = userDefinition.username.toLowerCase();
userDefinition.displayName = striptags(userDefinition.displayName.trim());
userDefinition.bio = striptags(userDefinition.bio.trim());
this.log.info('updating user settings', { userDefinition });
await User.updateOne(
@ -135,6 +155,7 @@ class UserService {
username: userDefinition.username,
username_lc,
displayName: userDefinition.displayName,
bio: userDefinition.bio,
},
},
);
@ -253,6 +274,7 @@ class UserService {
const user = await User
.findById(userId)
.select('+email +flags +permissions')
.populate(this.populateUser)
.lean();
if (!user) {
throw new SiteError(404, 'Member account not found');
@ -260,9 +282,13 @@ class UserService {
return user;
}
async getUserAccounts (pagination) {
async getUserAccounts (pagination, username) {
let search = { };
if (username) {
search.username_lc = { $regex: `^${username.toLowerCase().trim()}` };
}
const users = await User
.find()
.find(search)
.sort({ username_lc: 1 })
.select('+email +flags +permissions')
.skip(pagination.skip)
@ -280,10 +306,36 @@ class UserService {
} catch (error) {
user = User.findOne({ username: userId });
}
user = await user.select('+email +flags +settings').lean();
user = await user
.select('+email +flags +settings')
.populate(this.populateUser)
.lean();
return user;
}
async getPublicProfile (username) {
if (!username || (typeof username !== 'string') || (username.length === 0)) {
throw new SiteError(406, 'Invalid username');
}
username = username.trim().toLowerCase();
const user = await User
.findOne({ username_lc: username })
.select('_id created username username_lc displayName bio picture header')
.populate(this.populateUser)
.lean();
return user;
}
async getRecent (maxCount = 3) {
const users = User
.find()
.select('_id created username username_lc displayName bio picture')
.sort({ created: -1 })
.limit(maxCount)
.lean();
return users;
}
async setUserSettings (user, settings) {
const {
crypto: cryptoService,
@ -302,10 +354,7 @@ class UserService {
if (settings.username && (settings.username !== user.username)) {
update.username = this.filterUsername(settings.username);
const isReserved = await this.isUsernameReserved(update.username);
if (!isReserved) {
throw new SiteError(403, 'The username you entered is taken');
}
await this.checkUsername(update.username);
}
if (settings.email && (settings.email !== user.email)) {
@ -360,20 +409,128 @@ class UserService {
return striptags(username.trim().toLowerCase()).replace(/\W/g, '');
}
async isUsernameReserved (username) {
const reservedNames = ['digitaltelepresence', 'dtp', 'rob', 'amy', 'zack'];
if (reservedNames.includes(username)) {
this.log.alert('prohibiting use of reserved username', { username });
return true;
async checkUsername (username) {
if (!username || (typeof username !== 'string') || (username.length === 0)) {
throw new SiteError(406, 'Invalid username');
}
const reservedNames = [
'about',
'admin',
'amy',
'auth',
'digitaltelepresence',
'dist',
'dtp',
'fontawesome',
'fonts',
'img',
'image',
'less',
'manifest.json',
'moment',
'newsletter',
'numeral',
'rob',
'socket.io',
'uikit',
'user',
'welcome',
'zack'
];
if (reservedNames.includes(username.trim().toLowerCase())) {
throw new SiteError(403, 'That username is reserved for system use');
}
const user = await User.findOne({ username: username}).select('username').lean();
if (user) {
this.log.alert('username is already registered', { username });
return true;
throw new SiteError(403, 'Username is already registered');
}
}
async recordProfileView (user, req) {
const { resource: resourceService } = this.dtp.services;
await resourceService.recordView(req, 'User', user._id);
}
return false;
async getTotalCount ( ) {
return await User.estimatedDocumentCount();
}
async updatePhoto (user, file) {
const { image: imageService } = this.dtp.services;
const images = [
{
width: 512,
height: 512,
format: 'jpeg',
formatParameters: {
quality: 80,
},
},
{
width: 64,
height: 64,
format: 'jpeg',
formatParameters: {
conpressionLevel: 9,
},
},
];
await imageService.processImageFile(user, file, images);
await User.updateOne(
{ _id: user._id },
{
$set: {
'picture.large': images[0].image._id,
'picture.small': images[1].image._id,
},
},
);
}
async removePhoto (user) {
const { image: imageService } = this.dtp.services;
this.log.info('remove profile photo', { user: user._id });
user = await this.getUserAccount(user._id);
if (user.picture.large) {
await imageService.deleteImage(user.picture.large);
}
if (user.picture.small) {
await imageService.deleteImage(user.picture.small);
}
await User.updateOne({ _id: user._id }, { $unset: { 'picture': '' } });
}
async blockUser (userId, blockedUserId) {
userId = mongoose.Types.ObjectId(userId);
blockedUserId = mongoose.Types.ObjectId(blockedUserId);
if (userId.equals(blockedUserId)) {
throw new SiteError(406, "You can't block yourself");
}
await UserBlock.updateOne(
{ user: userId },
{
$addToSet: { blockedUsers: blockedUserId },
},
{ upsert: true },
);
}
async unblockUser (userId, blockedUserId) {
userId = mongoose.Types.ObjectId(userId);
blockedUserId = mongoose.Types.ObjectId(blockedUserId);
if (userId.equals(blockedUserId)) {
throw new SiteError(406, "You can't un-block yourself");
}
await UserBlock.updateOne(
{ user: userId },
{
$removeFromSet: { blockedUsers: blockedUserId },
},
{ upsert: true },
);
}
}

21
app/views/admin/components/menu.pug

@ -6,6 +6,7 @@ ul.uk-nav.uk-nav-default
span.nav-item-icon
i.fas.fa-home
span.uk-margin-small-left Home
li(class={ 'uk-active': (adminView === 'settings') })
a(href="/admin/settings")
span.nav-item-icon
i.fas.fa-cog
@ -29,6 +30,19 @@ ul.uk-nav.uk-nav-default
i.fas.fa-newspaper
span.uk-margin-small-left Newsletter
li.uk-nav-divider
li(class={ 'uk-active': (adminView === 'user') })
a(href="/admin/user")
span.nav-item-icon
i.fas.fa-user
span.uk-margin-small-left Users
li(class={ 'uk-active': (adminView === 'content-report') })
a(href="/admin/content-report")
span.nav-item-icon
i.fas.fa-ban
span.uk-margin-small-left Content Reports
li.uk-nav-divider
li(class={ 'uk-active': (adminView === 'host') })
@ -41,6 +55,11 @@ ul.uk-nav.uk-nav-default
span.nav-item-icon
i.fas.fa-microchip
span.uk-margin-small-left Jobs
li(class={ 'uk-active': (adminView === 'log') })
a(href="/admin/log")
span.nav-item-icon
i.fas.fa-clipboard-list
span.uk-margin-small-left Logs
li.uk-nav-divider
@ -48,4 +67,4 @@ ul.uk-nav.uk-nav-default
a(href="/admin/user")
span.nav-item-icon
i.fas.fa-user
span.uk-margin-small-left Users
span.uk-margin-small-left Users

31
app/views/admin/content-report/index.pug

@ -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.

48
app/views/admin/content-report/view.pug

@ -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}`)

52
app/views/admin/host/index.pug

@ -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)

22
app/views/admin/index.pug

@ -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);
});

5
app/views/admin/layouts/main.pug

@ -1,4 +1,7 @@
extends ../../layouts/main
block vendorcss
link(rel="stylesheet", href="/highlight.js/styles/default.css")
block content-container
block page-header
@ -16,4 +19,4 @@ block content-container
include ../components/menu
div(class="uk-width-1-1 uk-flex-first uk-width-expand@m").uk-width-expand
block content
block content

46
app/views/admin/log/index.pug

@ -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.

4
app/views/admin/page/editor.pug

@ -35,6 +35,10 @@ block content
fieldset
legend Menu
.uk-margin
label(for="menu-icon").uk-form-label Menu item icon
input(id="menu-icon", name="menuIcon", type="text", maxlength="80", placeholder="Enter icon class", value= page ? page.menu.icon : undefined).uk-input
.uk-text-small Visit #[a(href="https://fontawesome.com/v5.15/icons?d=gallery&p=2&q=blog&m=free", target="_blank") FontAwesome] for a list of usable icons.
.uk-margin
label(for="menu-label").uk-form-label Menu item label
input(id="menu-label", name="menuLabel", type="text", maxlength="80", placeholder="Enter label", value= page ? page.menu.label : undefined).uk-input

68
app/views/admin/settings/editor.pug

@ -2,14 +2,64 @@ extends ../layouts/main
block content
form(method="POST", action="/admin/settings").uk-form
.uk-margin
label(for="name").uk-form-label Site name
input(id="name", name="name", type="text", maxlength="200", placeholder="Enter site name", value= site.name).uk-input
.uk-margin
label(for="description").uk-form-label Site description
input(id="description", name="description", type="text", maxlength="500", placeholder="Enter site description", value= site.description).uk-input
.uk-margin
label(for="company").uk-form-label Company name
input(id="company", name="company", type="text", maxlength="200", placeholder="Enter company name", value= site.company).uk-input
fieldset
legend Site Information
.uk-margin
label(for="name").uk-form-label Site name
input(id="name", name="name", type="text", maxlength="200", placeholder="Enter site name", value= site.name).uk-input
.uk-margin
label(for="description").uk-form-label Site description
input(id="description", name="description", type="text", maxlength="500", placeholder="Enter site description", value= site.description).uk-input
.uk-margin
label(for="company").uk-form-label Company name
input(id="company", name="company", type="text", maxlength="200", placeholder="Enter company name", value= site.company).uk-input
fieldset
legend Gab links
div(uk-grid).uk-grid-small
div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl")
label(for="gab-url").uk-form-label Gab Social Profile
input(id="gab-url", name="gabUrl", type="url", placeholder="Enter Gab profile URL", value= site.gabUrl).uk-input
div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl")
label(for="gabtv-url").uk-form-label Gab TV Channel
input(id="gabtv-url", name="gabtvUrl", type="url", placeholder="Enter Gab TV URL", value= site.gabtvUrl).uk-input
fieldset
legend Social links
div(uk-grid).uk-grid-small
div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl")
label(for="telegram-url").uk-form-label Telegram URL
input(id="telegram-url", name="telegramUrl", type="url", placeholder="Enter Telegram URL", value= site.telegramUrl).uk-input
div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl")
label(for="twitter-url").uk-form-label Twitter URL
input(id="twitter-url", name="twitterUrl", type="url", placeholder="Enter Twitter URL", value= site.twitterUrl).uk-input
div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl")
label(for="facebook-url").uk-form-label Facebook URL
input(id="facebook-url", name="facebookUrl", type="url", placeholder="Enter Facebook URL", value= site.facebookUrl).uk-input
div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl")
label(for="instagram-url").uk-form-label Instagram URL
input(id="instagram-url", name="instagramUrl", type="url", placeholder="Enter Instagram URL", value= site.instagramUrl).uk-input
fieldset
legend Social links
div(uk-grid).uk-grid-small
div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl")
label(for="bitchute-url").uk-form-label BitChute URL
input(id="bitchute-url", name="bitchuteUrl", type="url", placeholder="Enter BitChute URL", value= site.bitchuteUrl).uk-input
div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl")
label(for="odysee-url").uk-form-label Odysee URL
input(id="odysee-url", name="odyseeUrl", type="url", placeholder="Enter Odysee channel URL", value= site.odyseeUrl).uk-input
div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl")
label(for="rumble-url").uk-form-label Rumble URL
input(id="rumble-url", name="rumbleUrl", type="url", placeholder="Enter Rumble channel URL", value= site.rumbleUrl).uk-input
div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl")
label(for="twitch-url").uk-form-label Twitch URL
input(id="twitch-url", name="twitchUrl", type="url", placeholder="Enter Twitch channel URL", value= site.twitchUrl).uk-input
div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl")
label(for="youtube-url").uk-form-label YouTube URL
input(id="youtube-url", name="youtubeUrl", type="url", placeholder="Enter YouTube channel URL", value= site.youtubeUrl).uk-input
div(class="uk-width-1-1 uk-width-1-2@m uk-width-1-3@xl")
label(for="dlive-url").uk-form-label DLive URL
input(id="dlive-url", name="dliveUrl", type="url", placeholder="Enter DLive channel URL", value= site.dliveUrl).uk-input
button(type="submit").uk-button.dtp-button-primary Save Settings

6
app/views/admin/user/form.pug

@ -33,5 +33,11 @@ block content
label
input(id="can-chat", name="canChat", type="checkbox", checked= userAccount.permissions.canChat)
| Can Chat
label
input(id="can-comment", name="canComment", type="checkbox", checked= userAccount.permissions.canComment)
| Can Comment
label
input(id="can-report", name="canReport", type="checkbox", checked= userAccount.permissions.canReport)
| Can Report
button(type="submit").uk-button.uk-button-primary Update User

15
app/views/admin/user/index.pug

@ -1,6 +1,16 @@
extends ../layouts/main
block content
include ../../components/pagination-bar
.uk-margin
form(method="GET", action="/admin/user").uk-form
div(uk-grid).uk-grid-collapse
.uk-width-expand
input(id="username", name="u", placeholder="Enter username", value= query && query.u ? query.u : undefined).uk-input
.uk-width-auto
button(type="submit").uk-button.uk-button-secondary.uk-light Search
.uk-overflow-auto
table.uk-table.uk-table-divider.uk-table-hover.uk-table-small.uk-table-justify
thead
@ -19,4 +29,7 @@ block content
else
.uk-text-muted N/A
td= moment(userAccount.created).format('YYYY-MM-DD hh:mm a')
td= userAccount._id
td= userAccount._id
.uk-margin
+renderPaginationBar("/admin/user", totalUserCount)

11
app/views/comment/components/comment-list.pug

@ -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.

13
app/views/comment/components/comment-review.pug

@ -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)

155
app/views/comment/components/comment.pug

@ -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 })

36
app/views/comment/components/composer.pug

@ -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

35
app/views/comment/components/report-form.pug

@ -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

13
app/views/components/file-upload-image.pug

@ -1,6 +1,6 @@
mixin renderFileUploadImage (actionUrl, containerId, imageId, imageClass, defaultImage, currentImage, cropperOptions)
div(id= containerId).dtp-file-upload
form(method="POST", action= actionUrl, enctype="multipart/form-data", onsubmit= "return dtp.app.submitForm(event);").uk-form
form(method="POST", action= actionUrl, enctype="multipart/form-data", onsubmit= "return dtp.app.submitImageForm(event);").uk-form
.uk-margin
.uk-card.uk-card-default.uk-card-small
.uk-card-body
@ -8,9 +8,9 @@ mixin renderFileUploadImage (actionUrl, containerId, imageId, imageClass, defaul
div(class="uk-width-1-1 uk-width-auto@m")
.upload-image-container.size-512
if !!currentImage
img(id= imageId, data-cropper-options= cropperOptions, src= `/image/${currentImage._id}`, class= imageClass).sb-large
img(id= imageId, src= `/image/${currentImage._id}`, class= imageClass).sb-large
else
img(id= imageId, data-cropper-options= cropperOptions, src= defaultImage, class= imageClass).sb-large
img(id= imageId, src= defaultImage, class= imageClass).sb-large
div(class="uk-width-1-1 uk-width-auto@m")
.uk-text-small.uk-margin(hidden= !!currentImage)
@ -21,7 +21,6 @@ mixin renderFileUploadImage (actionUrl, containerId, imageId, imageClass, defaul
div(uk-form-custom).uk-margin-small-left
input(
type="file",
name="imageFile",
formenctype="multipart/form-data",
accept=".jpg,.png,image/jpeg,image/png",
data-file-select-container= containerId,
@ -29,8 +28,7 @@ mixin renderFileUploadImage (actionUrl, containerId, imageId, imageClass, defaul
data-file-size-element= "file-size",
data-file-max-size= 15 * 1024000,
data-image-id= imageId,
data-image-w= 512,
data-image-h= 512,
data-cropper-options= cropperOptions,
onchange="return dtp.app.selectImageFile(event);",
)
button(type="button", tabindex="-1").uk-button.dtp-button-default Select
@ -51,10 +49,11 @@ mixin renderFileUploadImage (actionUrl, containerId, imageId, imageClass, defaul
#remove-btn(hidden= !currentImage).uk-width-auto
button(
type= "button",
data-image-type= imageId,
onclick= "return dtp.app.removeImageFile(event);",
).uk-button.uk-button-danger Remove
#file-save-btn(hidden).uk-width-auto
button(
type="submit",
).uk-button.uk-button-primary Save
).uk-button.uk-button-primary Save

4
app/views/components/labeled-icon.pug

@ -0,0 +1,4 @@
mixin renderLabeledIcon (iconClass, iconLabel)
span
i(class=`fas ${iconClass}`)
span.uk-margin-small-left.dtp-item-value= iconLabel

1
app/views/components/library.pug

@ -1,6 +1,7 @@
//- common routines for all views everywhere
include button-icon
include labeled-icon
include section-title
-

36
app/views/components/navbar.pug

@ -3,24 +3,38 @@ nav(uk-navbar).uk-navbar-container.uk-position-fixed.uk-position-top
.uk-navbar-item
button(type="button", uk-toggle="target: #dtp-offcanvas").uk-button.uk-button-link.uk-padding-small
i.fas.fa-bars
div(class="uk-visible@m").uk-navbar-left
//- Site icon
a(href="/", class="uk-visible@m").uk-navbar-item
a(href="/").uk-navbar-item
img(src=`/img/icon/icon-48x48.png`)
//- Site name
a(href="/", class="uk-visible@xl").uk-navbar-item.uk-logo
span= site.name
ul.uk-navbar-nav
li(class={ 'uk-active': currentView === 'home' })
a(href="/", title= "Home")
+renderButtonIcon('fa-home', 'Home')
each menuItem in mainMenu
li(class={ 'uk-active': (pageSlug === menuItem.slug) })
a(href= menuItem.url, title= menuItem.label)
+renderButtonIcon(menuItem.icon || 'fa-file', menuItem.label)
each menuItem in mainMenu
a(href= menuItem.url).uk-navbar-item= menuItem.label
//- Center menu (visible only on tablet and mobile)
div(class="uk-hidden@m").uk-navbar-center
a(href="/").uk-navbar-item
img(src=`/img/icon/icon-48x48.png`)
//- Site name
ul.uk-navbar-nav
li
a(href="/").uk-navbar-item
img(src=`/img/icon/icon-48x48.png`)
each menuItem in mainMenu
li
a(href= menuItem.url, title= menuItem.label)= menuItem.label
.uk-navbar-right
if user
ul.uk-navbar-nav
.uk-navbar-item
if user
div.no-select
@ -36,7 +50,7 @@ nav(uk-navbar).uk-navbar-container.uk-position-fixed.uk-position-top
).site-profile-picture.sb-navbar
div(uk-dropdown={ mode: 'click' }).uk-navbar-dropdown
ul.uk-nav.uk-navbar-dropdown-nav(style="z-index: 1024;")
ul.uk-nav.uk-navbar-dropdown-nav
li.uk-nav-heading.uk-text-center= user.displayName || user.username
li.uk-nav-divider
if (user.channel)

23
app/views/components/off-canvas.pug

@ -1,18 +1,29 @@
mixin renderMenuItem (iconClass, label)
div(uk-grid).uk-grid-collapse
.uk-width-auto
.app-menu-icon
i(class=`fas ${iconClass}`)
.uk-width-expand= label
#dtp-offcanvas(uk-offcanvas="mode: slide; overlay: true; bg-close: true;")
.uk-offcanvas-bar
.uk-margin
a(href="/", style="color: white;").uk-display-block
a(href="/").uk-display-block
.uk-text-large= site.name
.uk-text-small.uk-text-muted= site.description
ul.uk-nav.uk-nav-default.dtp-app-menu
li.uk-nav-header Site Menu
li(class={ "uk-active": (currentView === 'home') })
a(href='/').uk-display-block
div(uk-grid).uk-grid-collapse
.uk-width-auto
.app-menu-icon
i(class=`fas fa-home`)
.uk-width-expand Home
+renderMenuItem('fa-home', 'Home')
each menuItem in mainMenu
li(class={ 'uk-active': (pageSlug === menuItem.slug) })
a(href= menuItem.url, title= menuItem.label)
+renderMenuItem(menuItem.icon || 'fa-file', menuItem.label)
if user
li.uk-nav-header Member Menu

69
app/views/components/page-footer.pug

@ -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 &copy; 2021 #[+renderSiteLink()]
div Copyright &copy; 2021 #[+renderSiteLink()]
div All Rights Reserved

2
app/views/components/page-sidebar.pug

@ -25,7 +25,7 @@ mixin renderPageSidebar ( )
+renderSidebarEpisode(episode)
//- Newsletter Signup
div(uk-sticky={ offset: 60, bottom: '#dtp-content-grid' })
div(uk-sticky={ offset: 60, bottom: '#dtp-content-grid' }, style="z-index: initial;")
+renderSectionTitle('Mailing List')
.uk-margin
form(method="post", action="/newsletter", onsubmit="return dtp.app.submitForm(event, 'Subscribe to newsletter');").uk-form

8
app/views/components/social-card/facebook.pug

@ -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}`)

6
app/views/components/social-card/twitter.pug

@ -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)

5
app/views/error.pug

@ -3,9 +3,12 @@ block content
section.uk-section.uk-section-default.uk-section-xsmall
.uk-container
h1 Well, That's Not Right!
.uk-text-large= message
//- if error.stack
//- pre= error.stack
if error && error.status
div.uk-text-small.uk-text-muted status:#{error.status}
@ -14,4 +17,4 @@ block content
a(href="/").uk-button.uk-button-default.uk-border-rounded
span.uk-margin-small-right
i.fas.fa-home
span Home
span Back to #{site.name}

14
app/views/index.pug

@ -18,15 +18,15 @@ block content
'uk-flex-last': ((postIndex % postIndexModulus) === 0),
})
article.uk-article
h4(style="line-height: 1;").uk-article-title.uk-margin-remove= post.title
.uk-text-truncate= post.summary
h4(style="line-height: 1.1;").uk-article-title.uk-margin-small= post.title
.uk-article-meta
div(uk-grid).uk-grid-small
.uk-width-auto published: #{moment(post.created).format("MMM DD YYYY HH:MM a")}
if post.updated
.uk-width-auto updated: #{moment(post.updated).format("MMM DD YYYY HH:MM a")}
if post.updated
span updated: #{moment(post.updated).format("MMM DD YYYY HH:MM a")}
else
span published: #{moment(post.created).format("MMM DD YYYY HH:MM a")}
+renderSectionTitle('Featured')
.uk-margin
div(style="position: relative; overflow: hidden; width: 100%; padding-top: 56.25%")
iframe(
@ -47,7 +47,7 @@ block content
ul.uk-list.uk-list-divider.uk-list-small
each post in posts
li
+renderBlogPostListItem(post, postIndex, 2)
+renderBlogPostListItem(post, postIndex, 4)
- postIndex += 1;
else
div There are no posts at this time. Please check back later!

6
app/views/layouts/main-sidebar.pug

@ -1,5 +1,4 @@
extends main
block content-container
section.uk-section.uk-section-default
.uk-container
@ -7,7 +6,4 @@ block content-container
div(class="uk-width-1-1 uk-width-2-3@m")
block content
div(class="uk-width-1-1 uk-width-1-3@m")
+renderPageSidebar()
block page-footer
include ../components/page-footer
+renderPageSidebar()

5
app/views/layouts/main.pug

@ -59,8 +59,9 @@ html(lang='en')
block content-container
block content
block page-footer
include ../components/page-footer
block page-footer
include ../components/page-footer
block dtp-navbar
include ../components/navbar

2
app/views/notification/components/notification-standalone.pug

@ -0,0 +1,2 @@
include notification
+renderUserNotification(notification)

5
app/views/notification/components/notification.pug

@ -0,0 +1,5 @@
mixin renderUserNotification (userNotification)
div
div= moment(userNotification.created).fromNow()
div= userNotification.source
div= userNotification.message

5
app/views/page/view.pug

@ -7,7 +7,10 @@ block content
.uk-margin
div(uk-grid)
.uk-width-expand
h1.article-title= page.title
h1.article-title
span
i(class=`fas ${page.menu.icon}`)
span.uk-margin-left= page.title
if user && user.flags.isAdmin
.uk-width-auto
a(href=`/admin/page/${page._id}`).uk-button.dtp-button-text EDIT

54
app/views/post/view.pug

@ -1,7 +1,8 @@
extends ../layouts/main-sidebar
block content
include ../comment/components/comment
include ../comment/components/comment-list
include ../comment/components/composer
article(dtp-post-id= post._id)
.uk-margin
@ -37,43 +38,16 @@ block content
+renderSectionTitle('Add a comment')
.uk-margin
form(method="POST", action=`/post/${post._id}/comment`, 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
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
.uk-width-auto
button(type="submit").uk-button.dtp-button-primary Post comment
.uk-margin
+renderSectionTitle('Comments')
+renderCommentComposer(`/post/${post._id}/comment`)
.uk-margin
if Array.isArray(comments) && (comments.length > 0)
ul#post-comment-list.uk-list
each comment in comments
+renderComment(comment)
else
ul#post-comment-list.uk-list
div There are no comments at this time. Please check back later.
if featuredComment
#featured-comment.uk-margin-large
.uk-margin
+renderSectionTitle('Linked Comment')
+renderComment(featuredComment)
.uk-margin
+renderSectionTitle('Comments')
.uk-margin
+renderCommentList(comments)

21
app/views/user/profile.pug

@ -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.

22
app/views/user/settings.pug

@ -14,11 +14,21 @@ block content
div(uk-grid)
div(class="uk-width-1-1 uk-width-1-3@m")
-
var currentImage = null;
var currentProfile = null;
if (user.picture && user.picture.large) {
currentImage = user.picture.large;
currentProfile = user.picture.large;
}
+renderFileUploadImage(`/user/${user._id}/profile-photo`, 'test-image-upload', 'profile-picture-file', 'site-profile-picture', `/img/default-member.png`, currentImage)
.uk-margin
+renderFileUploadImage(
`/user/${user._id}/header-image`,
'header-image-upload',
'header-image-file',
'header-image-picture',
`/img/default-header.png`,
user.header,
{ aspectRatio: 1400 / 400 },
)
div(class="uk-width-1-1 uk-width-expand@m")
form(method="POST", action=`/user/${user._id}/settings`, onsubmit="return dtp.app.submitForm(event, 'user account update');").uk-form
.uk-margin
@ -28,4 +38,8 @@ block content
label(for="display-name").uk-form-label Display Name
input(id="display-name", name="displayName", type="text", placeholder="Enter display name", value= user.displayName).uk-input
.uk-margin
button(type="submit").uk-button.dtp-button-primary Update account settings
label(for="bio").uk-form-label Profile bio
textarea(id="bio", name="bio", rows="3", placeholder="Tell people about yourself").uk-textarea.uk-resize-vertival= user.bio
.uk-margin
button(type="submit").uk-button.dtp-button-primary Update account settings

7
app/views/welcome/index.pug

@ -3,7 +3,10 @@ block content
section.uk-section.uk-section-secondary
.uk-container.uk-text-center
h1 Welcome to #{site.name}
.uk-width-auto.uk-margin-auto
img(src="/img/icon/icon-256x256.png")
h1= site.name
.uk-text-lead= site.description
.uk-margin-medium-top
@ -11,4 +14,4 @@ block content
.uk-width-auto
a(href="/welcome/signup").uk-button.dtp-button-primary Create Account
.uk-width-auto
a(href="/welcome/login").uk-button.dtp-button-secondary Sign In
a(href="/welcome/login").uk-button.dtp-button-secondary Sign In

4
app/views/welcome/signup.pug

@ -2,7 +2,7 @@ extends ../layouts/main
block content
form(method="POST", action="/user").uk-form
section.uk-section.uk-section-muted.uk-section
section.uk-section.uk-section-default.uk-section-xsmall
.uk-container.uk-container-small
p You are creating a new member account on #[+renderSiteLink()]. If you have an account, please #[a(href="/welcome/login") log in here]. An account is required to comment on posts and use other site features.
@ -50,4 +50,4 @@ block content
img(src='/welcome/signup/captcha', style="padding: 8px 0;").uk-display-block.uk-margin-auto
input(id="captcha", name="captcha", type="text", placeholder="Enter captcha text").uk-input.uk-text-center
.uk-margin-small
button(type="submit").uk-button.dtp-button-primary Create Account
button(type="submit").uk-button.dtp-button-primary Create Account

78
app/workers/reeeper.js

@ -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);
}
})();

62
client/img/social-icons/bitchute.svg

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="105.54111mm"
height="105.46744mm"
viewBox="0 0 105.54111 105.46744"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="bitchute.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="-336.31874"
inkscape:cy="33.403361"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1055"
inkscape:window-x="1920"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-57.612308,-52.203871)">
<path
style="fill:#eb3127;stroke-width:0.26458332"
d="m 102.82842,157.59655 c -6.262607,-1.17802 -10.38368,-2.43562 -14.855903,-4.53348 -10.946194,-5.13473 -20.115996,-14.25413 -25.273426,-25.13453 -2.410127,-5.08454 -3.734679,-9.45836 -4.605483,-15.20785 -0.486919,-3.21488 -0.548354,-4.49245 -0.428819,-8.91754 0.158399,-5.863835 0.66795,-9.194117 2.159211,-14.111999 4.775325,-15.748068 16.92513,-28.59889 32.451158,-34.323519 14.705462,-5.422076 32.227822,-3.78722 45.345972,4.230828 3.14473,1.922114 5.67969,3.829719 5.68422,4.27748 0.002,0.235086 -0.85888,1.13841 -1.91391,2.007386 -3.15067,2.595063 -15.70894,12.685747 -16.05114,12.897237 -0.17435,0.107759 -1.32775,-0.238286 -2.56311,-0.768988 -4.38758,-1.884885 -5.60911,-2.093171 -12.27585,-2.093171 -5.2562,0 -6.19087,0.06582 -7.98392,0.562204 -4.907588,1.358617 -8.435044,3.371869 -12.272593,7.004426 -4.961787,4.696746 -7.914155,9.980616 -8.836378,15.814519 -0.399196,2.525277 -0.325145,9.811517 0.125039,12.303127 0.210341,1.16416 0.384781,2.14034 0.387644,2.16928 0.0029,0.0289 -3.923857,3.22533 -8.726044,7.10309 -4.802188,3.87776 -8.73125,7.16609 -8.73125,7.3074 0,0.14131 0.208359,0.19854 0.46302,0.12718 0.254662,-0.0714 3.022865,-0.80302 6.151563,-1.62589 5.603221,-1.47371 6.88683,-1.813 11.94603,-3.15764 1.404326,-0.37325 2.748087,-0.67863 2.986135,-0.67863 0.238049,0 1.517716,1.13483 2.843708,2.52184 3.929795,4.11064 7.505141,6.4654 11.989756,7.8966 3.0373,0.96931 5.01688,1.21923 9.65729,1.21923 4.6404,0 6.61999,-0.24992 9.65729,-1.21923 4.40549,-1.40595 8.00462,-3.75183 11.7839,-7.68065 4.51371,-4.69232 6.97921,-9.68149 7.79203,-15.76783 0.1042,-0.78025 0.33704,-1.34546 0.61466,-1.492 0.40397,-0.21325 22.25611,-6.11546 22.64166,-6.11546 0.0894,0 0.16254,2.1223 0.16254,4.71623 0,3.56365 -0.1296,5.43596 -0.53031,7.66132 -3.78842,21.03913 -19.95572,37.69205 -41.18449,42.42157 -2.29833,0.51205 -3.50074,0.58763 -10.27582,0.64589 -4.22011,0.0363 -7.97058,0.01 -8.33438,-0.0584 z"
id="path826"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.6 KiB

69
client/img/social-icons/dlive.svg

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="86.753304mm"
height="72.148483mm"
viewBox="0 0 86.753304 72.148483"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="dlive.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1.979899"
inkscape:cx="153.9598"
inkscape:cy="104.79721"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1055"
inkscape:window-x="1920"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-54.186327,-79.631304)">
<rect
style="opacity:1;fill:#ffd300;fill-opacity:1;stroke:none;stroke-width:0.96351701;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="rect828"
width="66.039062"
height="37.789749"
x="71.722221"
y="91.651352" />
<path
style="fill:#030200;stroke-width:0.13170107"
d="m 84.396161,151.58276 c -5.915239,-0.92016 -11.131194,-4.7455 -13.815368,-10.13208 -0.678391,-1.36139 -1.185128,-2.82357 -1.584627,-4.57242 -0.273144,-1.19572 -0.280268,-1.52241 -0.326046,-14.95226 -0.02574,-7.55141 -0.08166,-13.72984 -0.124256,-13.72984 -0.0426,0 -0.707045,0.60747 -1.47654,1.34994 -1.520683,1.46726 -2.989757,2.6812 -4.788358,3.95675 -1.668619,1.18338 -3.824187,2.479 -3.934051,2.3646 -0.05201,-0.0542 -0.371951,-0.86893 -0.710971,-1.81059 -0.84543,-2.34827 -1.501097,-3.88574 -2.597701,-6.09134 -0.513249,-1.0323 -0.894861,-1.91523 -0.848026,-1.96207 0.04684,-0.0468 1.345664,-0.3149 2.886282,-0.5957 4.500495,-0.8203 9.948924,-2.27524 10.075283,-2.69049 0.05802,-0.19068 -1.430897,-0.84955 -3.068352,-1.35778 -1.324458,-0.41109 -4.005188,-0.99364 -7.65858,-1.664296 -1.102235,-0.202339 -2.077875,-0.413507 -2.168091,-0.469262 -0.120177,-0.07428 0.105459,-0.640401 0.844033,-2.117696 0.916975,-1.834136 2.360437,-5.312868 2.92629,-7.052337 0.123431,-0.379435 0.282431,-0.725735 0.353335,-0.769556 0.172779,-0.106783 4.313172,2.664946 6.028652,4.035797 0.753841,0.6024 1.961897,1.67671 2.684569,2.387356 0.722673,0.710647 1.358468,1.292085 1.412879,1.292085 0.05441,0 0.169005,-0.48894 0.254657,-1.086534 0.787212,-5.492372 3.73197,-10.249677 8.179099,-13.213472 1.925437,-1.28321 3.848939,-2.114646 6.314291,-2.729358 1.111576,-0.277161 1.259056,-0.279429 20.940466,-0.321915 21.57173,-0.04657 20.86364,-0.06735 23.50864,0.689797 1.5499,0.443664 4.05301,1.675643 5.49946,2.706715 2.41902,1.724357 4.52,4.212071 5.83473,6.90876 0.88439,1.814007 1.31726,3.143068 1.64498,5.050653 0.24658,1.435282 0.25679,2.258364 0.25679,20.696903 0,18.41497 -0.0105,19.26346 -0.25594,20.69691 -0.6573,3.83875 -2.31786,7.0247 -5.14323,9.86783 -2.90176,2.91999 -6.07933,4.57297 -10.12172,5.26534 -1.43606,0.24597 -2.2476,0.25551 -20.67706,0.24326 -15.567125,-0.0103 -19.401566,-0.0469 -20.345519,-0.1937 z m 3.994745,-23.82301 c 1.898536,-0.28833 3.44523,-0.78355 5.338036,-1.70913 1.85445,-0.90683 3.252046,-1.87201 4.88465,-3.37333 2.194638,-2.01818 4.072898,-2.89143 6.219078,-2.89143 2.39487,0 4.11967,0.82555 6.7177,3.21533 4.16195,3.82835 10.79895,5.74203 15.49427,4.46754 4.07132,-1.10511 7.30244,-4.50039 8.39218,-8.81853 0.28731,-1.13849 0.29511,-1.35542 0.29783,-8.28097 l 0.003,-7.11186 -0.35283,-1.27715 c -0.19406,-0.70243 -0.65224,-1.86002 -1.01817,-2.572421 -1.64863,-3.209535 -4.18276,-5.197229 -7.97978,-6.259083 l -1.05361,-0.294649 -19.75516,-0.03889 c -13.378172,-0.02634 -20.100055,0.0062 -20.823617,0.100736 -5.319423,0.695165 -9.135712,3.933985 -10.633474,9.024447 -0.255999,0.87007 -0.273846,1.31643 -0.317375,7.93807 -0.05083,7.73235 -0.0163,8.19992 0.763199,10.33581 1.209242,3.3134 4.158485,6.09583 7.634333,7.20252 1.700592,0.54145 4.032394,0.67066 6.18995,0.34299 z m 3.985209,-12.46592 c -1.543371,-0.25714 -2.950367,-1.34205 -3.653801,-2.81738 -0.481816,-1.01054 -0.608708,-2.60864 -0.283023,-3.56447 0.940713,-2.76084 3.927881,-4.22451 6.574147,-3.22124 1.057339,0.40087 2.203386,1.48517 2.755844,2.60738 0.387472,0.78706 0.427367,0.9804 0.427367,2.07111 0,1.09071 -0.03989,1.28404 -0.427367,2.07111 -1.02362,2.07927 -3.182919,3.22174 -5.393167,2.85349 z m 22.207335,-0.20526 c -0.83896,-0.2801 -1.79374,-1.03434 -2.39029,-1.88823 -1.69451,-2.42547 -0.88644,-5.82846 1.71302,-7.21396 2.54253,-1.35515 5.75559,-0.24865 6.96291,2.39787 0.48733,1.06825 0.51023,2.66176 0.055,3.82879 -0.42779,1.09678 -1.74317,2.41406 -2.80664,2.8107 -0.92358,0.34446 -2.60413,0.37529 -3.53403,0.0648 z M 96.095418,103.83443 c -0.579485,-0.0621 -2.80194,-0.29808 -4.93879,-0.52449 -2.283093,-0.2419 -4.03361,-0.48788 -4.24513,-0.59651 -0.749895,-0.38515 -1.026827,-1.78057 -0.489046,-2.46425 0.50619,-0.643514 1.000089,-0.673055 4.858188,-0.290566 6.715751,0.665796 8.081508,0.844876 8.436557,1.106246 0.503133,0.37039 0.676013,0.79931 0.615143,1.52618 -0.0441,0.52627 -0.13368,0.72143 -0.468599,1.02069 -0.391057,0.34941 -0.478113,0.36904 -1.564408,0.35277 -0.632669,-0.009 -1.62443,-0.068 -2.203915,-0.13007 z m 13.756722,-0.0215 c -0.71794,-0.56575 -0.89543,-1.8307 -0.35215,-2.50961 0.35539,-0.44409 0.67078,-0.51511 3.60058,-0.81078 1.15033,-0.11609 3.49995,-0.35989 5.22138,-0.541774 1.82696,-0.193037 3.37075,-0.294579 3.70851,-0.243929 1.09733,0.164554 1.68195,1.310203 1.16858,2.290033 -0.43492,0.83011 -0.6097,0.88323 -4.12179,1.25285 -7.4278,0.78171 -8.83854,0.86784 -9.22511,0.56321 z"
id="path826"
inkscape:connector-curvature="0" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

102
client/img/social-icons/odysee.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 38 KiB

95
client/img/social-icons/rumble.svg

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="134.23224mm"
height="140.34915mm"
viewBox="0 0 134.23224 140.34915"
version="1.1"
id="svg8"
inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"
sodipodi:docname="rumble.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.35"
inkscape:cx="-252.90368"
inkscape:cy="216.77065"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1920"
inkscape:window-height="1055"
inkscape:window-x="3840"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-35.542074,-65.838087)">
<g
id="g826">
<path
style="fill:#b0c996;stroke-width:0.1355703"
d="m 80.401373,206.11871 c -5.688214,-0.33546 -10.079344,-1.98588 -13.722006,-5.15746 -1.040354,-0.90582 -2.618758,-2.77296 -3.686388,-4.36076 l -0.80505,-1.19727 -1.415368,-0.0915 c -2.76585,-0.17882 -4.792466,-0.66675 -7.385561,-1.77816 -3.12576,-1.3397 -4.667925,-2.47931 -6.790478,-5.01792 -7.534298,-9.01114 -11.052511,-25.57955 -11.054447,-52.05899 -6.78e-4,-8.56341 0.287709,-15.45506 0.94156,-22.50467 1.781342,-19.20588 6.015958,-30.386075 13.617775,-35.953541 1.169228,-0.856326 3.526897,-2.176916 4.810661,-2.694571 2.351159,-0.948063 5.850188,-1.5332 8.183608,-1.368528 l 1.145802,0.08086 0.531714,-0.571013 c 0.292441,-0.314056 0.747197,-0.829103 1.010567,-1.144548 0.579225,-0.693752 2.385579,-2.230965 3.500846,-2.97923 1.217156,-0.816626 3.179786,-1.762614 4.725402,-2.277645 8.886329,-2.961112 20.857308,-0.506186 37.97924,7.788523 11.6792,5.657977 27.94382,15.494227 37.41741,22.628683 5.29306,3.98615 10.15599,8.45402 13.22733,12.15276 2.6043,3.13627 4.97629,7.20673 5.95161,10.21327 0.80586,2.48418 1.00588,3.77326 1.0132,6.52966 0.008,2.98889 -0.26052,4.59082 -1.2381,7.38858 -0.33869,0.96933 -0.6185,1.94544 -0.62178,2.16913 -0.003,0.22369 0.24952,0.97591 0.56179,1.67159 1.32671,2.95568 1.63103,4.81491 1.40601,8.58987 -0.17317,2.90504 -0.55899,4.78556 -1.45791,7.10584 -3.96574,10.23636 -16.41311,21.25293 -38.77099,34.31433 -18.36651,10.72966 -31.515163,16.37789 -42.026791,18.05332 -1.592283,0.25379 -5.422305,0.5876 -6.100664,0.53172 -0.149127,-0.0123 -0.576173,-0.0403 -0.948992,-0.0623 z m 3.795969,-27.84475 c 7.75745,-0.52448 16.421918,-3.18639 25.826138,-7.93433 7.52785,-3.8006 13.93723,-8.16856 20.28292,-13.82271 0.79245,-0.70609 1.49964,-1.28379 1.57153,-1.28379 0.0719,0 -0.0693,0.25251 -0.31371,0.56114 -0.93243,1.1773 0.18352,0.22021 2.9194,-2.5038 6.81964,-6.79004 10.74698,-12.62159 11.6019,-17.2272 0.20021,-1.07853 0.11334,-3.02252 -0.20053,-4.48716 -0.72522,-3.38426 -2.77931,-7.388 -5.72067,-11.15045 -8.6358,-11.04652 -25.67293,-22.262932 -42.071022,-27.697494 -5.365474,-1.778196 -10.062793,-2.679751 -14.592426,-2.800722 -3.136129,-0.08375 -4.609679,0.04011 -6.804398,0.571951 -2.846774,0.689857 -4.738736,1.776157 -6.94475,3.987437 -2.373075,2.378741 -4.404119,5.728478 -6.130461,10.110788 -4.667899,11.84943 -6.210258,30.2844 -3.879122,46.36504 1.237363,8.53556 3.248726,15.13915 6.060867,19.89867 0.358014,0.60594 0.692651,1.02767 0.786776,0.99155 0.09008,-0.0346 0.611258,0.43411 1.158166,1.0415 2.205983,2.44995 3.632021,3.40378 6.531051,4.36843 2.947379,0.98074 5.906355,1.2824 9.918341,1.01115 z m 2.960149,-19.79062 c -4.293861,-0.45237 -7.239696,-3.86805 -9.058073,-10.50278 -0.97439,-3.55526 -1.459798,-7.59804 -1.465039,-12.20177 -0.0038,-3.35884 0.178455,-5.73253 0.660992,-8.6076 0.986626,-5.87857 2.79173,-10.13031 5.295117,-12.4721 1.764614,-1.65071 3.75576,-2.25956 6.961881,-2.12881 3.693263,0.15063 7.288454,1.12334 12.201331,3.30119 7.75032,3.43567 14.92353,9.07524 17.94744,14.11026 1.24126,2.0668 1.73183,4.51739 1.24177,6.20315 -2.01391,6.92761 -16.12816,18.14611 -26.916721,21.39436 -2.569807,0.77372 -5.031116,1.09769 -6.868698,0.9041 z"
id="path840"
inkscape:connector-curvature="0" />
<path
style="fill:#8db168;stroke-width:0.1355703"
d="m 78.9101,205.85018 c -3.756579,-0.40808 -7.421852,-1.67618 -10.21751,-3.53502 -1.912753,-1.2718 -4.179416,-3.68856 -5.901119,-6.29189 -0.463368,-0.70065 -0.554187,-0.76902 -1.125377,-0.84731 -0.341733,-0.0468 -0.872096,-0.0855 -1.178584,-0.086 -2.570762,-0.004 -7.098185,-1.42767 -9.451898,-2.97286 -8.241526,-5.41048 -13.106072,-18.07492 -14.688277,-38.23971 -0.414799,-5.28651 -0.496736,-7.3866 -0.573901,-14.70938 -0.121113,-11.49318 0.324782,-21.04464 1.389189,-29.75768 1.797166,-14.711237 5.302355,-24.155083 10.9578,-29.523014 1.331482,-1.263791 2.291621,-1.965963 4.217195,-3.084129 3.309212,-1.921636 6.0076,-2.633098 10.006934,-2.638448 l 2.007584,-0.0027 0.5618,-0.643959 c 1.01802,-1.166897 2.728888,-2.801247 3.693203,-3.528025 1.270848,-0.957805 4.426935,-2.506955 6.100281,-2.994295 2.552838,-0.743481 3.756721,-0.891447 7.253012,-0.891449 2.353502,-3e-6 3.610958,0.063 4.812745,0.241114 11.353639,1.682749 24.901843,7.56897 44.591333,19.373379 16.54957,9.921942 26.81826,18.083966 32.64627,25.948756 1.32034,1.78178 2.22318,3.28603 3.21522,5.35698 2.67199,5.57787 2.97318,10.93974 0.94206,16.77042 -0.30216,0.86739 -0.58251,1.71691 -0.62298,1.88784 -0.0468,0.19766 0.15158,0.83909 0.54512,1.76241 1.37787,3.23287 1.68779,5.05112 1.45803,8.55387 -0.18057,2.75256 -0.48731,4.42808 -1.173,6.40705 -3.62508,10.46262 -15.92811,21.52735 -38.89741,34.9824 -18.38402,10.76908 -31.912282,16.55368 -42.433502,18.14436 -1.837508,0.2778 -6.737891,0.46893 -8.134218,0.31725 z m 1.990072,-25.2993 c 0.247676,-0.24233 0.72223,-0.74276 1.054566,-1.11206 l 0.604246,-0.67145 1.803713,-0.16788 c 13.77755,-1.28225 30.654103,-8.99363 43.827213,-20.0259 0.93204,-0.78058 1.71799,-1.39601 1.74656,-1.36762 0.0286,0.0284 -0.13921,0.2693 -0.37282,0.53537 -1.7123,1.9502 1.95765,-1.21329 5.33943,-4.60259 5.73985,-5.75259 9.3938,-10.86812 10.86606,-15.2125 0.43781,-1.29188 0.50494,-1.66636 0.55503,-3.09646 0.093,-2.65533 -0.51735,-5.09114 -2.07222,-8.26978 -1.80851,-3.69716 -4.40043,-7.12472 -8.46438,-11.19331 -3.59107,-3.59518 -6.84165,-6.31156 -11.18173,-9.34414 -6.99433,-4.8872 -14.01492,-8.643383 -22.37765,-11.972568 -8.890316,-3.539218 -17.244653,-5.011409 -23.515371,-4.143847 -6.178406,0.854791 -10.320084,4.255476 -13.764909,11.302215 -2.328198,4.76256 -3.860523,9.85869 -5.013951,16.67515 -0.866613,5.12145 -1.258984,9.55578 -1.37953,15.59058 -0.152686,7.64376 0.395084,14.38824 1.791673,22.06016 2.019368,11.09307 6.402749,19.70517 11.79644,23.17667 2.132179,1.37231 4.91155,2.24885 7.198509,2.27021 1.074186,0.01 1.122861,-0.003 1.559121,-0.43025 z m 5.262939,-22.41202 c -2.962441,-0.64153 -5.198485,-2.94008 -6.841326,-7.03257 -0.985246,-2.45435 -1.734897,-5.60206 -2.197442,-9.22684 -0.343803,-2.69425 -0.343967,-9.64039 -2.71e-4,-12.3369 0.518089,-4.06497 1.520786,-7.97436 2.699797,-10.52618 1.536531,-3.32562 3.717689,-5.42715 6.237583,-6.00988 4.631795,-1.07111 12.170553,0.87068 19.691588,5.07206 8.04105,4.49186 13.79368,10.19175 14.97841,14.84105 0.46573,1.82771 0.34735,2.75195 -0.60927,4.75717 -2.61224,5.4756 -11.44545,13.00796 -20.439338,17.42926 -5.412962,2.66095 -10.238371,3.74342 -13.519731,3.03283 z"
id="path838"
inkscape:connector-curvature="0" />
<path
style="fill:#73a240;stroke-width:0.1355703"
d="m 78.9101,205.70549 c -3.543842,-0.32534 -7.288501,-1.61893 -10.094995,-3.48729 -1.583657,-1.05428 -3.883377,-3.38116 -5.156664,-5.21756 -1.422289,-2.05129 -1.166699,-1.88558 -3.148675,-2.04151 -4.089197,-0.32174 -7.487704,-1.51072 -10.231336,-3.57948 -8.825131,-6.65434 -13.239557,-20.74581 -14.275268,-45.56866 -0.172928,-4.14456 -0.172802,-14.42243 2.71e-4,-18.91206 0.750139,-19.4635 2.947651,-31.688986 7.187296,-39.98531 2.602794,-5.093261 5.654307,-8.211085 10.388872,-10.614628 2.778363,-1.41046 4.594406,-1.853794 7.977539,-1.947484 1.342146,-0.03717 2.562279,-0.09314 2.711406,-0.124374 0.149128,-0.03124 0.698188,-0.553175 1.220133,-1.159863 1.029151,-1.196241 2.856502,-2.800735 4.099494,-3.599533 2.095186,-1.346453 5.118639,-2.494527 7.830693,-2.973492 1.67056,-0.29503 5.458677,-0.4023 7.456367,-0.211145 10.257646,0.981536 23.065647,6.063638 40.535517,16.084133 23.50117,13.479972 36.53476,24.238456 41.47116,34.232076 2.86245,5.79495 3.22487,11.15797 1.15918,17.15359 -0.30682,0.89055 -0.59036,1.75569 -0.63009,1.92255 -0.0446,0.18755 0.13751,0.78277 0.47713,1.55906 0.7613,1.7402 1.21859,3.20044 1.47582,4.71273 0.19087,1.12208 0.19888,1.62831 0.0622,3.93153 -0.16288,2.74542 -0.41562,4.21547 -1.06059,6.16845 -3.64226,11.02897 -17.24478,22.94103 -42.68423,37.37964 -6.34284,3.59998 -14.00278,7.53669 -18.77596,9.64959 -8.570533,3.79384 -15.836927,5.96081 -22.078434,6.58418 -1.313666,0.1312 -4.696929,0.15685 -5.916837,0.0449 z m 6.57516,-24.57471 c 2.849188,-0.47456 5.669581,-1.21083 7.901263,-2.06265 1.499603,-0.57239 2.089015,-1.00754 2.84034,-2.097 0.397893,-0.57694 0.652281,-0.70972 3.086568,-1.61089 10.203879,-3.77749 19.474929,-9.03864 27.791909,-15.77139 1.8607,-1.50628 1.8695,-1.51214 1.51987,-1.01332 -0.16485,0.2352 -0.29973,0.47099 -0.29973,0.52398 0,0.68841 8.26531,-7.23037 11.03997,-10.57711 4.80162,-5.79162 7.12645,-10.36344 7.12645,-14.01432 0,-2.91462 -1.10399,-6.38937 -3.13286,-9.86047 -5.54429,-9.48549 -18.42065,-20.03256 -33.26777,-27.249747 -4.52855,-2.201327 -9.86426,-4.388234 -13.082535,-5.362031 C 86.800682,88.947041 78.56378,88.722574 73.345967,91.390991 66.096894,95.098204 60.870082,106.83641 59.050173,123.49603 c -0.4631,4.23926 -0.594775,6.89642 -0.595238,12.01162 -6.78e-4,7.60244 0.455179,12.67627 1.827081,20.33554 0.544083,3.0376 1.055736,5.05057 2.015849,7.93086 1.951386,5.85407 4.189988,9.88976 7.114209,12.82531 0.824658,0.82786 1.872253,1.75477 2.32799,2.0598 1.930896,1.2924 4.142845,2.15611 6.492184,2.53504 1.387872,0.22384 5.757728,0.18564 7.253012,-0.0634 z m 0.711811,-23.12071 c -2.873432,-0.62421 -5.162488,-2.99482 -6.778882,-7.02042 -3.03323,-7.55419 -3.317091,-20.35533 -0.63892,-28.81306 1.018487,-3.21641 2.149795,-5.32612 3.723444,-6.94361 1.12742,-1.15883 2.153884,-1.76291 3.557328,-2.09349 4.059524,-0.95624 10.047866,0.33993 16.845999,3.64629 8.09627,3.93772 14.45238,9.23819 16.90205,14.09491 0.60709,1.20361 1.06181,2.80612 1.05941,3.73354 -0.011,4.26655 -7.04185,11.88064 -16.30936,17.66237 -2.15373,1.34365 -6.118682,3.36786 -8.140124,4.15573 -3.94719,1.53845 -7.720499,2.12091 -10.220945,1.57774 z"
id="path836"
inkscape:connector-curvature="0" />
<path
style="fill:#71a13e;stroke-width:0.1355703"
d="m 59.867262,194.8078 c -2.551041,-0.24558 -5.613551,-1.14887 -7.531376,-2.22138 -0.470833,-0.26331 -1.333972,-0.83348 -1.918086,-1.26704 -1.144271,-0.84935 -3.496476,-3.1487 -3.862021,-3.77524 l -0.22416,-0.38421 8.680754,-9.35435 c 4.774414,-5.14489 8.717985,-9.34653 8.763491,-9.33698 0.0455,0.01 0.468467,0.77214 0.939914,1.69463 4.205671,8.22932 9.606685,11.65587 17.85472,11.32753 5.100196,-0.20303 11.867799,-2.10208 17.758842,-4.98329 1.45557,-0.7119 1.83994,-0.97093 2.62168,-1.76674 0.88562,-0.90155 1.06002,-1.00445 5.24829,-3.09665 2.38272,-1.19026 5.33883,-2.75295 6.56914,-3.47266 4.11065,-2.40466 8.48335,-5.41602 11.98479,-8.25359 0.80256,-0.65039 1.47782,-1.16218 1.50057,-1.13731 0.0228,0.0249 -0.14461,0.37238 -0.37192,0.77225 -0.66475,1.1694 -6.4418,6.40513 -11.75774,10.65602 -16.41458,13.12592 -33.497627,21.91028 -46.907327,24.12047 -3.660935,0.60339 -6.528243,0.75015 -9.349561,0.47854 z m 93.541808,-42.82669 c -6.82088,-2.67503 -12.41905,-4.91605 -12.44039,-4.98005 -0.0213,-0.064 0.39477,-0.72398 0.92466,-1.46663 2.72705,-3.82203 4.2262,-7.01767 4.62928,-9.86785 0.28441,-2.01119 -0.25606,-4.89847 -1.45071,-7.74993 -4.0081,-9.5667 -16.00401,-20.51291 -31.62926,-28.86154 -2.7643,-1.476976 -4.85137,-2.46907 -8.62028,-4.097667 -1.63032,-0.704482 -3.12498,-1.370861 -3.32148,-1.480843 l -0.35726,-0.199967 0.40671,0.08874 c 0.98656,0.215271 6.77347,2.063062 9.55771,3.051827 24.87962,8.83551 45.0294,21.90767 53.98591,35.02334 1.57345,2.30414 2.31825,3.6713 2.23558,4.10373 -0.0477,0.24944 0.14007,0.86429 0.59199,1.93873 1.35481,3.22096 1.6564,5.24486 1.36217,9.14141 -0.17396,2.3038 -0.36971,3.3522 -0.98528,5.27717 -0.43824,1.37048 -2.10585,4.85208 -2.35219,4.91087 -0.0746,0.0178 -5.71629,-2.1563 -12.53716,-4.83134 z m -94.070092,1.39218 C 54.71464,134.68465 53.208184,114.59337 55.254493,98.8994 c 1.2744,-9.773888 3.986708,-17.942584 7.90869,-23.81872 0.331173,-0.496182 0.509761,-0.641252 0.792748,-0.643959 0.287397,-0.0028 0.778767,-0.417821 2.304695,-1.94682 2.110081,-2.114327 3.234455,-2.941391 5.464248,-4.019373 1.30663,-0.631683 2.458927,-1.054962 2.556849,-0.939218 0.02588,0.03059 1.819752,4.936145 3.986388,10.901238 3.36733,9.270797 3.90664,10.857533 3.714152,10.927638 -0.123853,0.04511 -0.89626,0.116987 -1.71646,0.159737 -2.465589,0.128504 -4.935518,0.755361 -6.914086,1.754764 -3.664573,1.851029 -6.839341,5.76828 -9.424249,11.628303 -1.363992,3.09219 -3.066984,8.91281 -3.847504,13.15032 -1.063808,5.77549 -1.537566,10.73314 -1.664745,17.42078 -0.126531,6.65343 0.197183,11.52987 1.254331,18.89531 0.33579,2.33955 0.585825,4.27843 0.555632,4.30862 -0.03019,0.0302 -0.428983,-1.45694 -0.886204,-3.30473 z"
id="path834"
inkscape:connector-curvature="0" />
<path
style="fill:#699838;stroke-width:0.1355703"
d="m 58.710125,194.4744 c -1.545044,-0.20423 -3.876526,-0.77281 -4.249979,-1.03647 -0.230144,-0.16248 0.379533,-0.85647 7.273533,-8.27963 l 7.524732,-8.10229 0.352024,0.22756 c 0.193614,0.12514 0.657058,0.47958 1.029876,0.78762 1.822361,1.50575 4.41424,2.68477 7.049656,3.20681 1.585728,0.31411 6.525515,0.24621 8.562752,-0.11772 4.594514,-0.82071 9.485294,-2.42245 13.981281,-4.57886 2.64453,-1.2684 3.95996,-2.09528 3.95996,-2.48922 0,-0.0726 0.12201,-0.17083 0.27114,-0.21816 0.14913,-0.0473 0.27114,-0.17053 0.27114,-0.27377 0,-0.10959 1.42462,-0.9026 3.42315,-1.90549 7.39423,-3.71051 12.58222,-6.97945 18.2681,-11.51069 1.54526,-1.23146 1.89799,-1.48993 1.89799,-1.39084 0,0.23035 -1.33762,1.73743 -2.54118,2.86311 -6.74246,6.30619 -13.65697,11.78881 -21.25141,16.85057 -14.067985,9.3764 -27.250019,14.89699 -38.298613,16.03929 -1.776447,0.18367 -5.888156,0.14442 -7.524152,-0.0718 z m 1.05205,-39.85134 c -0.0899,-0.37282 -0.405955,-1.68446 -0.702351,-2.91476 -1.212649,-5.03355 -2.508218,-12.12207 -3.183994,-17.42079 -2.575387,-20.19343 -1.493939,-37.945283 3.059956,-50.228794 1.546367,-4.17112 4.56173,-9.61691 5.328457,-9.623276 0.146784,-0.0012 0.589082,-0.320417 0.982885,-0.709329 0.393805,-0.388912 0.716008,-0.632938 0.716008,-0.542281 0,0.09066 -0.05985,0.164832 -0.132988,0.164832 -0.441502,0 -0.105917,1.07115 2.973129,9.489921 l 3.24768,8.879855 -0.705322,0.604768 c -1.328893,1.13944 -3.029089,2.971465 -3.928899,4.233538 -5.660913,7.939986 -9.036649,22.353976 -9.042872,38.611976 -0.0022,5.68014 0.331973,10.38242 1.160642,16.33316 0.465829,3.34516 0.545699,4.44011 0.227669,3.12118 z m 108.803565,-5.56639 c -0.042,-0.34873 -0.49393,-0.53895 -11.49936,-4.83978 -6.30063,-2.46223 -11.4814,-4.49902 -11.51282,-4.52621 -0.0314,-0.0272 0.10657,-0.52194 0.30667,-1.09945 1.02885,-2.96952 0.9952,-5.53658 -0.11709,-8.93208 -3.21032,-9.80024 -15.09412,-21.19363 -31.44919,-30.151383 -2.71653,-1.487855 -5.55975,-2.861953 -8.99937,-4.349299 -1.37059,-0.592667 -2.65173,-1.167026 -2.84697,-1.276351 l -0.35499,-0.198773 0.40671,0.08768 c 0.67409,0.145319 6.10429,1.882835 8.13422,2.602731 22.81969,8.092785 41.64195,19.613675 51.64246,31.609735 2.258,2.70858 4.95814,6.85278 4.95814,7.6098 0,0.20718 0.23576,0.91323 0.52389,1.56901 1.10308,2.51048 1.64578,4.68799 1.64468,6.599 0,1.17378 -0.24684,4.44942 -0.38254,5.08988 -0.12131,0.57265 -0.39536,0.69657 -0.45444,0.20549 z"
id="path832"
inkscape:connector-curvature="0" />
<path
style="fill:#59862a;stroke-width:0.1355703"
d="m 74.433922,192.3832 c 0.04479,-0.0725 2.245842,-2.46262 4.891236,-5.31146 3.375906,-3.63553 4.758481,-5.2125 4.63764,-5.28972 -0.113423,-0.0725 -0.05652,-0.10821 0.166759,-0.10473 0.534327,0.008 3.237742,-0.50711 5.33563,-1.01728 3.477672,-0.84572 7.526056,-2.31181 11.653333,-4.22015 3.80309,-1.75846 9.08927,-4.77628 10.13649,-5.78682 0.69523,-0.67087 0.96748,-0.85362 3.78458,-2.54048 4.14028,-2.47918 7.87835,-5.05301 11.32012,-7.79443 1.57326,-1.25313 1.72478,-1.33472 1.1648,-0.62726 -0.83627,1.05651 -5.99022,5.70253 -9.77352,8.81032 -8.94397,7.34703 -18.183418,13.44582 -27.204358,17.95712 -5.196037,2.59849 -8.86619,4.07452 -13.557795,5.45252 -2.046163,0.601 -2.710296,0.72378 -2.554915,0.47237 z M 59.330999,152.22073 c -3.232594,-13.6727 -4.800633,-27.43461 -4.576619,-40.16675 0.04329,-2.4606 0.144735,-5.29741 0.225429,-6.30402 0.262997,-3.28076 0.826491,-7.950927 1.079606,-8.947636 0.06627,-0.260972 0.194206,-0.929853 0.284292,-1.486401 0.09009,-0.556547 0.233322,-1.212726 0.318297,-1.458173 0.147786,-0.426868 0.259811,-0.15596 2.576792,6.23136 1.33226,3.6727 2.506018,6.8133 2.608354,6.97913 0.165069,0.26748 0.110193,0.59636 -0.486338,2.91476 -2.127467,8.26834 -3.03262,15.83014 -3.039087,25.38908 -0.004,5.90622 0.261059,9.70282 1.126297,16.13286 0.140467,1.04389 0.238526,1.91506 0.217906,1.93592 -0.02062,0.0209 -0.171338,-0.5282 -0.334929,-1.22013 z M 162.53043,133.21461 c -1.95324,-0.76861 -3.80749,-1.43183 -4.12054,-1.47382 -0.31304,-0.042 -0.60613,-0.13123 -0.6513,-0.19833 -0.0452,-0.0671 -0.37193,-0.20167 -0.7261,-0.29904 -0.35418,-0.0974 -0.64396,-0.23012 -0.64396,-0.295 0,-0.0649 -0.11456,-0.11795 -0.25459,-0.11795 -0.33493,0 -1.22494,-0.3756 -1.46949,-0.62014 -0.1063,-0.1063 -0.28829,-0.19328 -0.4044,-0.19328 -0.42217,0 -2.34534,-0.72707 -2.34534,-0.88667 0,-0.16668 -0.58484,-0.33346 -1.1693,-0.33346 -0.18238,0 -0.32198,-0.0806 -0.32198,-0.18589 0,-0.1207 -0.23772,-0.21743 -0.67785,-0.27581 -0.37282,-0.0495 -0.67785,-0.14883 -0.67785,-0.22082 0,-0.072 -0.15251,-0.13091 -0.33892,-0.13091 -0.18641,0 -0.33893,-0.0556 -0.33893,-0.12365 0,-0.068 -0.24403,-0.16488 -0.54228,-0.21527 -0.29826,-0.0504 -0.54228,-0.14047 -0.54228,-0.20019 0,-0.0597 -0.20908,-0.14779 -0.46463,-0.19573 -0.25555,-0.0479 -0.48819,-0.15783 -0.51697,-0.2442 -0.0288,-0.0864 -0.44456,-0.31374 -0.92394,-0.50526 -0.85039,-0.33976 -0.88889,-0.37715 -1.58229,-1.53631 -4.83213,-8.07794 -14.01304,-16.39812 -25.86314,-23.43841 -3.96964,-2.358419 -7.7503,-4.279824 -12.6425,-6.425173 -0.83916,-0.367991 -1.4724,-0.686851 -1.40722,-0.70858 0.0652,-0.02173 1.36184,0.365956 2.88144,0.861521 14.0929,4.595904 27.16028,10.755532 37.94341,17.885542 5.28867,3.49697 9.28649,6.66591 12.99466,10.30044 3.50864,3.43895 5.83969,6.27029 7.59538,9.22548 0.86968,1.46388 1.09685,1.96882 0.87977,1.95556 -0.0646,-0.004 -1.7156,-0.63603 -3.66886,-1.40465 z"
id="path830"
inkscape:connector-curvature="0" />
<path
style="fill:#507c22;stroke-width:0.1355703"
d="m 99.394096,180.95759 c 0.481492,-0.56869 3.228764,-3.50768 4.687914,-5.01506 1.06438,-1.09956 1.24704,-1.23125 2.89114,-2.08451 0.52195,-0.27088 1.10151,-0.59972 1.28792,-0.73076 0.18641,-0.13104 0.55245,-0.36071 0.81342,-0.51039 2.43713,-1.3978 7.44811,-4.6789 9.151,-5.99192 0.59651,-0.45994 2.3657,-1.79853 3.93154,-2.97464 1.56583,-1.17611 3.30452,-2.48809 3.86375,-2.9155 l 1.01678,-0.7771 -1.49128,1.45273 c -3.78558,3.68775 -10.00063,8.82724 -15.59058,12.89254 -3.3341,2.42472 -10.321003,6.95964 -10.722652,6.95964 -0.05347,0 0.01901,-0.13726 0.161048,-0.30503 z m -40.18807,-30.06271 c 0,-0.18641 0.03077,-0.26267 0.06838,-0.16947 0.03761,0.0932 0.03761,0.24572 0,0.33893 -0.03761,0.0932 -0.06838,0.0169 -0.06838,-0.16946 z m -0.146272,-0.45369 c -0.0059,-0.0631 -0.281734,-1.40526 -0.612995,-2.98255 -1.325894,-6.3132 -2.339568,-13.20587 -2.844186,-19.33958 -0.251783,-3.06045 -0.506504,-7.0734 -0.452417,-7.12748 0.02181,-0.0218 0.291817,0.67309 0.600007,1.54425 0.308189,0.87115 0.805064,2.25498 1.104167,3.07518 0.299102,0.8202 0.730127,2.00983 0.957834,2.64362 l 0.41401,1.15235 0.05852,6.16845 c 0.05794,6.10688 0.196248,8.62958 0.704373,12.84751 0.119165,0.98919 0.186227,1.87378 0.149025,1.96577 -0.0372,0.092 -0.07246,0.1156 -0.07834,0.0525 z m 93.275926,-31.00376 c -0.53569,-0.24305 -2.91755,-1.17971 -4.89479,-1.92485 -0.4101,-0.15456 -0.98967,-0.3866 -1.28792,-0.51565 -0.49752,-0.21528 -3.14641,-1.24555 -4.94832,-1.92462 -0.4101,-0.15455 -0.98966,-0.38659 -1.28791,-0.51565 -0.64327,-0.27834 -3.72902,-1.47388 -4.87151,-1.8874 -0.44242,-0.16014 -1.13055,-0.44034 -1.52919,-0.62268 -0.39863,-0.18234 -0.77831,-0.33152 -0.84374,-0.33152 -0.0654,0 -0.68893,-0.46736 -1.38557,-1.03857 -6.95776,-5.70502 -16.06883,-11.286881 -24.4175,-14.959287 -0.72823,-0.320333 -1.29615,-0.610329 -1.26204,-0.644436 0.0778,-0.07782 5.08011,1.610471 7.98621,2.695382 11.16232,4.167131 21.87449,9.486651 30.4069,15.099641 3.45478,2.27271 8.16334,5.79904 8.85335,6.63045 0.22417,0.27011 0.20694,0.26809 -0.51797,-0.0608 z"
id="path828"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 21 KiB

295
client/js/site-app.js

@ -15,6 +15,14 @@ import Cropper from 'cropperjs';
import { EmojiButton } from '@joeattardi/emoji-button';
const GRID_COLOR = 'rgb(64, 64, 64)';
const GRID_TICK_COLOR = 'rgb(192,192,192)';
const AXIS_TICK_COLOR = 'rgb(192, 192, 192)';
const CHART_LINE_USER = 'rgb(0, 192, 0)';
const CHART_FILL_USER = 'rgb(0, 128, 0)';
export default class DtpSiteApp extends DtpApp {
constructor (user) {
@ -39,6 +47,21 @@ export default class DtpSiteApp extends DtpApp {
if (this.chat.input) {
this.chat.input.addEventListener('keydown', this.onChatInputKeyDown.bind(this));
}
this.charts = {/* will hold rendered charts */};
this.scrollToHash();
}
async scrollToHash ( ) {
const { hash } = window.location;
if (hash === '') {
return;
}
const target = document.getElementById(hash.slice(1));
if (target && target.scrollIntoView) {
target.scrollIntoView({ behavior: 'smooth' });
}
}
async connect ( ) {
@ -151,7 +174,7 @@ export default class DtpSiteApp extends DtpApp {
}
return false;
}
async submitForm (event, userAction) {
event.preventDefault();
event.stopPropagation();
@ -184,6 +207,51 @@ export default class DtpSiteApp extends DtpApp {
return;
}
async submitImageForm (event) {
event.preventDefault();
event.stopPropagation();
const formElement = event.currentTarget || event.target;
const form = new FormData(formElement);
this.cropper.getCroppedCanvas().toBlob(async (imageData) => {
try {
form.append('imageFile', imageData, 'profile.png');
this.log.info('submitImageForm', 'updating user settings', { event, action: formElement.action });
const response = await fetch(formElement.action, {
method: formElement.method,
body: form,
});
if (!response.ok) {
let json;
try {
json = await response.json();
} catch (error) {
throw new Error('Server error');
}
throw new Error(json.message || 'Server error');
}
await this.processResponse(response);
window.location.reload();
} catch (error) {
UIkit.modal.alert(`Failed to update profile photo: ${error.message}`);
}
});
return;
}
async closeCurrentDialog ( ) {
if (!this.currentDialog) {
return;
}
this.currentDialog.hide();
delete this.currentDialog;
}
async copyHtmlToText (event, textContentId) {
const content = this.editor.getContent({ format: 'text' });
const text = document.getElementById(textContentId);
@ -196,7 +264,10 @@ export default class DtpSiteApp extends DtpApp {
const imageId = event.target.getAttribute('data-image-id');
//z read the cropper options from the element on the page
const cropperOptions = event.target.getAttribute('data-cropper-options');
let cropperOptions = event.target.getAttribute('data-cropper-options');
if (cropperOptions) {
cropperOptions = JSON.parse(cropperOptions);
}
this.log.debug('selectImageFile', 'cropper options', { cropperOptions }); //z remove when done
const fileSelectContainerId = event.target.getAttribute('data-file-select-container');
@ -238,20 +309,11 @@ export default class DtpSiteApp extends DtpApp {
return;
}
// const IMAGE_WIDTH = parseInt(event.target.getAttribute('data-image-w'));
// const IMAGE_HEIGHT = parseInt(event.target.getAttribute('data-image-h'));
const reader = new FileReader();
reader.onload = (e) => {
const img = document.getElementById(imageId);
img.onload = (e) => {
console.log('image loaded', e, img.naturalWidth, img.naturalHeight);
// if (img.naturalWidth !== IMAGE_WIDTH || img.naturalHeight !== IMAGE_HEIGHT) {
// UIkit.modal.alert(`This image must be ${IMAGE_WIDTH}x${IMAGE_HEIGHT}`);
// img.setAttribute('hidden', '');
// img.src = '';
// return;
// }
fileSelectContainer.querySelector('#file-name').textContent = selectedFile.name;
fileSelectContainer.querySelector('#file-modified').textContent = moment(selectedFile.lastModifiedDate).fromNow();
@ -268,16 +330,15 @@ export default class DtpSiteApp extends DtpApp {
img.src = e.target.result;
//z create cropper and set options here
this.createImageCropper(img);
this.createImageCropper(img, cropperOptions);
};
// read in the file, which will trigger everything else in the event handler above.
reader.readAsDataURL(selectedFile);
}
async createImageCropper (img) {
this.log.info("createImageCropper", "Creating image cropper", { img });
this.cropper = new Cropper(img, {
async createImageCropper (img, options) {
options = Object.assign({
aspectRatio: 1,
dragMode: 'move',
autoCropArea: 0.85,
@ -285,11 +346,13 @@ export default class DtpSiteApp extends DtpApp {
guides: false,
center: false,
highlight: false,
cropBoxMovable: false,
cropBoxResizable: false,
cropBoxMovable: true,
cropBoxResizable: true,
toggleDragModeOnDblclick: false,
modal: true,
});
}, options);
this.log.info("createImageCropper", "Creating image cropper", { img });
this.cropper = new Cropper(img, options);
}
async attachTinyMCE (editor) {
@ -458,11 +521,8 @@ export default class DtpSiteApp extends DtpApp {
let response;
switch (imageType) {
case 'channel-app-icon':
const channelId = (event.target || event.currentTarget).getAttribute('data-channel-id');
response = await fetch(`/channel/${channelId}/app-icon`, {
method: 'DELETE',
});
case 'profile-picture-file':
response = await fetch(`/user/${this.user._id}/profile-photo`, { method: 'DELETE' });
break;
default:
@ -474,11 +534,11 @@ export default class DtpSiteApp extends DtpApp {
}
await this.processResponse(response);
window.location.reload();
} catch (error) {
UIkit.modal.alert(`Failed to remove image: ${error.message}`);
}
}
async onCommentInput (event) {
const label = document.getElementById('comment-character-count');
label.textContent = numeral(event.target.value.length).format('0,0');
@ -494,6 +554,193 @@ export default class DtpSiteApp extends DtpApp {
async onEmojiSelected (selection) {
this.emojiTargetElement.value += selection.emoji;
}
async showReportCommentForm (event) {
event.preventDefault();
event.stopPropagation();
const resourceType = event.currentTarget.getAttribute('data-resource-type');
const resourceId = event.currentTarget.getAttribute('data-resource-id');
const commentId = event.currentTarget.getAttribute('data-comment-id');
this.closeCommentDropdownMenu(commentId);
try {
const response = await fetch('/content-report/comment/form', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
resourceType, resourceId, commentId
}),
});
if (!response.ok) {
throw new Error('failed to load report form');
}
const html = await response.text();
this.currentDialog = UIkit.modal.dialog(html);
} catch (error) {
this.log.error('reportComment', 'failed to process comment request', { resourceType, resourceId, commentId, error });
UIkit.modal.alert(`Failed to report comment: ${error.message}`);
}
return true;
}
async deleteComment (event) {
event.preventDefault();
event.stopPropagation();
const commentId = (event.currentTarget || event.target).getAttribute('data-comment-id');
try {
const response = fetch(`/comment/${commentId}`, { method: 'DELETE' });
if (!response.ok) {
throw new Error('Server error');
}
this.processResponse(response);
} catch (error) {
UIkit.modal.alert(`Failed to delete comment: ${error.message}`);
}
}
async submitDialogForm (event, userAction) {
await this.submitForm(event, userAction);
await this.closeCurrentDialog();
}
async blockCommentAuthor (event) {
event.preventDefault();
event.stopPropagation();
const resourceType = event.currentTarget.getAttribute('data-resource-type');
const resourceId = event.currentTarget.getAttribute('data-resource-id');
const commentId = event.currentTarget.getAttribute('data-comment-id');
const actionUrl = this.getCommentActionUrl(resourceType, resourceId, commentId, 'block-author');
this.closeCommentDropdownMenu(commentId);
try {
this.log.info('blockCommentAuthor', 'blocking comment author', { resourceType, resourceId, commentId });
const response = await fetch(actionUrl, { method: 'POST'});
await this.processResponse(response);
} catch (error) {
this.log.error('reportComment', 'failed to process comment request', { resourceType, resourceId, commentId, error });
UIkit.modal.alert(`Failed to block comment author: ${error.message}`);
}
return true;
}
closeCommentDropdownMenu (commentId) {
const dropdown = document.querySelector(`[data-comment-id="${commentId}"][uk-dropdown]`);
UIkit.dropdown(dropdown).hide(false);
}
getCommentActionUrl (resourceType, resourceId, commentId, action) {
switch (resourceType) {
case 'Newsletter':
return `/newsletter/${resourceId}/comment/${commentId}/${action}`;
case 'Page':
return `/page/${resourceId}/comment/${commentId}/${action}`;
case 'Post':
return `/post/${resourceId}/comment/${commentId}/${action}`;
default:
break;
}
throw new Error('Invalid resource type for comment operation');
}
async submitCommentVote (event) {
const target = (event.currentTarget || event.target);
const commentId = target.getAttribute('data-comment-id');
const vote = target.getAttribute('data-vote');
try {
const response = await fetch(`/comment/${commentId}/vote`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ vote }),
});
await this.processResponse(response);
} catch (error) {
UIkit.modal.alert(`Failed to submit vote: ${error.message}`);
}
}
async renderStatsGraph (selector, title, data) {
try {
const canvas = document.querySelector(selector);
const ctx = canvas.getContext('2d');
this.charts.profileStats = new Chart(ctx, {
type: 'bar',
data: {
labels: data.map((item) => new Date(item.date)),
datasets: [
{
label: title,
data: data.map((item) => item.count),
borderColor: CHART_LINE_USER,
borderWidth: 1,
backgroundColor: CHART_FILL_USER,
tension: 0,
},
],
},
options: {
scales: {
yAxis: {
display: true,
ticks: {
color: AXIS_TICK_COLOR,
callback: (value) => {
return numeral(value).format(value > 1000 ? '0,0.0a' : '0,0');
},
},
grid: {
color: GRID_COLOR,
tickColor: GRID_TICK_COLOR,
},
},
x: {
type: 'time',
},
xAxis: {
display: false,
grid: {
color: GRID_COLOR,
tickColor: GRID_TICK_COLOR,
},
},
},
plugins: {
title: { display: false },
subtitle: { display: false },
legend: { display: false },
},
maintainAspectRatio: true,
aspectRatio: 16.0 / 9.0,
onResize: (chart, event) => {
if (event.width >= 960) {
chart.config.options.aspectRatio = 16.0 / 5.0;
}
else if (event.width >= 640) {
chart.config.options.aspectRatio = 16.0 / 9.0;
} else if (event.width >= 480) {
chart.config.options.aspectRatio = 16.0 / 12.0;
} else {
chart.config.options.aspectRatio = 16.0 / 16.0;
}
},
},
});
} catch (error) {
this.log.error('renderStatsGraph', 'failed to render stats graph', { title, error });
UIkit.modal.alert(`Failed to render chart: ${error.message}`);
}
}
}
dtp.DtpSiteApp = DtpSiteApp;

25
client/less/site/button.less

@ -1,3 +1,28 @@
button.dtp-link-button,
a.dtp-link-button {
display: inline-block;
padding: 6px;
line-height: 18px;
font-size: 14px;
text-transform: uppercase;
text-decoration: none;
text-align: center;
background: none;
outline: none;
border: solid 2px @global-primary-background;
color: @global-color;
transition: background-color 0.2s, color 0.2s;
&:hover {
background-color: @global-primary-background;
color: @global-inverse-color;
text-decoration: none;
}
}
.share-button {
background: #00d178;
color: white;

61
client/less/site/comment.less

@ -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;
}
}
}

7
client/less/site/dashboard.less

@ -1,5 +1,10 @@
.dtp-dashboard-cluster {
canvas.visit-graph {
width: 960px;
height: 360px;
}
fieldset {
border-color: #9e9e9e;
color: #c8c8c8;
@ -78,4 +83,4 @@
}
}
}
}
}

6
client/less/site/image.less

@ -44,15 +44,12 @@ img.site-channel-icon,
img.site-profile-picture {
&.sb-xxsmall {
max-width: 32px;
border-radius: 4px;
}
&.sb-xsmall {
max-width: 48px;
border-radius: 6px;
}
&.sb-list-item {
max-width: 48px;
border-radius: 6px;
}
&.sb-navbar {
max-width: 48px;
@ -62,14 +59,11 @@ img.site-profile-picture {
}
&.sb-medium {
max-width: 128px;
border-radius: 12px;
}
&.sb-large {
max-width: 256px;
border-radius: 16px;
}
&.sb-full {
max-width: 512px;
border-radius: 20px;
}
}

1
client/less/style.less

@ -5,6 +5,7 @@
@import "site/main.less";
@import "site/border.less";
@import "site/comment.less";
@import "site/image.less";
@import "site/figure.less";
@import "site/header-section.less";

36
config/limiter.js

@ -46,6 +46,30 @@ module.exports = {
},
},
comment: {
deleteComment: {
total: 1,
expire: ONE_MINUTE,
message: 'You are deleting comments too quickly',
},
},
/*
* ContentReportController
*/
contentReport: {
postCommentReportForm: {
total: 5,
expire: ONE_MINUTE,
message: 'You are reporting comments too quickly',
},
postCommentReport: {
total: 1,
expire: ONE_MINUTE,
message: 'You are reporting comments too quickly',
},
},
/*
* CryptoExchangeController
*/
@ -171,6 +195,11 @@ module.exports = {
expire: ONE_MINUTE,
message: 'You are creating accounts too quickly',
},
postProfilePhoto: {
total: 5,
expire: ONE_MINUTE * 5,
message: 'You are updating your profile photo too quickly',
},
postUpdateSettings: {
total: 4,
expire: ONE_MINUTE,
@ -186,6 +215,11 @@ module.exports = {
expire: ONE_MINUTE,
message: 'You are requesting user profiles too quickly',
},
deleteProfilePhoto: {
total: 5,
expire: ONE_MINUTE * 5,
message: 'You are deleting your profile photo too quickly',
},
},
welcome: {
@ -193,4 +227,4 @@ module.exports = {
expire: ONE_MINUTE,
message: 'You are loading these pages too quickly',
},
};
};

8
deploy

@ -0,0 +1,8 @@
#!/bin/sh
git pull origin master
yarn --production=false
gulp build
./stop-production
./start-production

6
lib/client/js/dtp-display-engine.js

@ -209,6 +209,10 @@ export default class DtpDisplayEngine {
}
async showModal (displayList, command) {
UIkit.modal.dialog(command.html);
UIkit.modal.dialog(command.params.html);
}
async navigateTo (displayList, command) {
window.location = command.params.href;
}
}

8
lib/site-controller.js

@ -18,7 +18,6 @@ class SiteController extends SiteCommon {
}
async loadChild (filename) {
this.log.info('loading child controller', { script: filename });
let child = await require(filename)(this.dtp);
return await child.start();
}
@ -38,10 +37,15 @@ class SiteController extends SiteCommon {
return pagination;
}
createDisplayList (name) {
const { displayEngine: displayEngineService } = this.dtp.services;
return displayEngineService.createDisplayList(name);
}
async createCsrfToken (req, name) {
const { csrfToken } = this.dtp.platform.services;
return csrfToken.create(req, { name });
}
}
module.exports.SiteController = SiteController;
module.exports.SiteController = SiteController;

39
lib/site-log.js

@ -8,6 +8,8 @@ const util = require('util');
const moment = require('moment');
const rfs = require('rotating-file-stream');
const color = require('ansicolor');
var LogModel, LogStream;
if (process.env.DTP_LOG_FILE === 'enabled') {
@ -74,16 +76,47 @@ class SiteLog {
async writeLog (level, message, metadata) {
const NOW = new Date();
const { componentName } = this;
if (process.env.DTP_LOG_CONSOLE === 'enabled') {
let clevel = level.padEnd(5);
switch (level) {
case 'debug':
clevel = color.black(clevel);
break;
case 'info':
clevel = color.green(clevel);
break;
case 'warn':
clevel = color.yellow(clevel);
break;
case 'alert':
clevel = color.red(clevel);
break;
case 'error':
clevel = color.bgRed.white(clevel);
break;
case 'crit':
clevel = color.bgRed.yellow(clevel);
break;
case 'fatal':
clevel = color.bgRed.black(clevel);
break;
}
const ctimestamp = color.black(moment(NOW).format('YYYY-MM-DD HH:mm:ss.SSS'));
const ccomponentName = color.cyan(componentName);
const cmessage = color.darkGray(message);
if (metadata) {
console.log(`${moment(NOW).format('YYYY-MM-DD HH:mm:ss.SSS')} ${componentName} ${level} ${message}`, util.inspect(metadata, false, Infinity, true));
console.log(`${ctimestamp} ${clevel} ${ccomponentName} ${cmessage}`, util.inspect(metadata, false, Infinity, true));
} else {
console.log(`${moment(NOW).format('YYYY-MM-DD HH:mm:ss.SSS')} ${componentName} ${level} ${message}`);
console.log(`${ctimestamp} ${clevel} ${ccomponentName} ${cmessage}`);
}
}
if (LogModel && (process.env.DTP_LOG_MONGODB === 'enabled')) {
await LogModel.create({ created: NOW, level, componentName, message, metadata });
}
if (LogStream && (process.env.DTP_LOG_FILE === 'enabled')) {
const logEntry = {
t: NOW, c: componentName, l: level, m: message, d: metadata,
@ -93,4 +126,4 @@ class SiteLog {
}
}
module.exports.SiteLog = SiteLog;
module.exports.SiteLog = SiteLog;

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save