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 .env
data/minio
node_modules node_modules
dist dist
data/minio data/minio

15
.vscode/launch.json

@ -7,7 +7,7 @@
{ {
"type": "pwa-node", "type": "pwa-node",
"request": "launch", "request": "launch",
"name": "dtp-sites", "name": "web:dtp-sites",
"skipFiles": [ "skipFiles": [
"<node_internals>/**" "<node_internals>/**"
], ],
@ -16,6 +16,17 @@
"env": { "env": {
"HTTP_BIND_PORT": "3333" "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('/host',await this.loadChild(path.join(__dirname, 'admin', 'host')));
router.use('/job-queue',await this.loadChild(path.join(__dirname, 'admin', 'job-queue'))); 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('/log', await this.loadChild(path.join(__dirname, 'admin', 'log')));
router.use('/page',await this.loadChild(path.join(__dirname, 'admin', 'page'))); router.use('/newsletter', await this.loadChild(path.join(__dirname, 'admin', 'newsletter')));
router.use('/post',await this.loadChild(path.join(__dirname, 'admin', 'post'))); router.use('/page', await this.loadChild(path.join(__dirname, 'admin', 'page')));
router.use('/settings',await this.loadChild(path.join(__dirname, 'admin', 'settings'))); 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.use('/user', await this.loadChild(path.join(__dirname, 'admin', 'user')));
router.get('/diagnostics', this.getDiagnostics.bind(this));
router.get('/', this.getHomeView.bind(this)); router.get('/', this.getHomeView.bind(this));
return router; 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) { async getHomeView (req, res) {
res.locals.stats = { res.locals.stats = {
memberCount: await User.estimatedDocumentCount(), memberCount: await User.estimatedDocumentCount(),
@ -65,7 +78,11 @@ class AdminController extends SiteController {
} }
} }
module.exports = async (dtp) => { module.exports = {
let controller = new AdminController(dtp); slug: 'admin',
return controller; 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.param('hostId', this.populateHostId.bind(this));
router.post('/:hostId/deactivate', this.postDeactiveHost.bind(this));
router.get('/:hostId', this.getHostView.bind(this)); router.get('/:hostId', this.getHostView.bind(this));
router.get('/', this.getHomeView.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) { async getHostView (req, res, next) {
try { try {
res.locals.stats = await NetHostStats res.locals.stats = await NetHostStats
@ -62,7 +96,20 @@ class HostController extends SiteController {
async getHomeView (req, res, next) { async getHomeView (req, res, next) {
try { 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'); res.render('admin/host/index');
} catch (error) { } catch (error) {
return next(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) { async deleteNewsletter (req, res) {
const { newsletter: newsletterService, displayEngine: displayEngineService } = this.dtp.services; const { newsletter: newsletterService } = this.dtp.services;
try { try {
const displayList = displayEngineService.createDisplayList('delete-newsletter'); const displayList = this.createDisplayList('delete-newsletter');
await newsletterService.deleteNewsletter(res.locals.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) { async deletePage (req, res) {
const { page: pageService, displayEngine: displayEngineService } = this.dtp.services; const { page: pageService } = this.dtp.services;
try { try {
const displayList = displayEngineService.createDisplayList('delete-page'); const displayList = this.createDisplayList('delete-page');
await pageService.deletePage(res.locals.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) { async deletePost (req, res) {
const { post: postService, displayEngine: displayEngineService } = this.dtp.services; const { post: postService } = this.dtp.services;
try { try {
const displayList = displayEngineService.createDisplayList('delete-post'); const displayList = this.createDisplayList('delete-post');
await postService.deletePost(res.locals.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.param('userId', this.populateUserId.bind(this));
router.post('/:userId', this.postUpdateUser.bind(this)); router.post('/:userId', this.postUpdateUser.bind(this));
router.get('/:userId', this.getUserView.bind(this)); router.get('/:userId', this.getUserView.bind(this));
router.get('/', this.getHomeView.bind(this)); router.get('/', this.getHomeView.bind(this));
@ -61,7 +62,8 @@ class UserController extends SiteController {
const { user: userService } = this.dtp.services; const { user: userService } = this.dtp.services;
try { try {
res.locals.pagination = this.getPaginationParameters(req, 10); 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'); res.render('admin/user/index');
} catch (error) { } catch (error) {
return next(error); return next(error);

14
app/controllers/auth.js

@ -24,7 +24,7 @@ class AuthController extends SiteController {
async start ( ) { async start ( ) {
const { limiter: limiterService } = this.dtp.services; 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(); const router = express.Router();
this.dtp.app.use('/auth', router); this.dtp.app.use('/auth', router);
@ -189,7 +189,11 @@ class AuthController extends SiteController {
} }
} }
module.exports = async (dtp) => { module.exports = {
let controller = new AuthController(dtp); slug: 'auth',
return controller; 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) => { module.exports = {
let controller = new HomeController(dtp); slug: 'home',
return controller; 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); dtp.app.use('/image', router);
const imageUpload = multer({ const imageUpload = multer({
dest: '/tmp/dtp-sites/upload/image', dest: `/tmp/${this.dtp.config.site.domainKey}/uploads/${DTP_COMPONENT_NAME}`,
limits: { limits: {
fileSize: 1024 * 1000 * 12, fileSize: 1024 * 1000 * 12,
}, },
@ -135,7 +135,11 @@ class ImageController extends SiteController {
} }
} }
module.exports = async (dtp) => { module.exports = {
let controller = new ImageController(dtp); slug: 'image',
return controller; 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) => { module.exports = {
let controller = new ManifestController(dtp); slug: 'manifest',
return controller; 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 { dtp } = this;
const { limiter: limiterService } = dtp.services; 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(); const router = express.Router();
dtp.app.use('/newsletter', router); dtp.app.use('/newsletter', router);
@ -58,9 +58,9 @@ class NewsletterController extends SiteController {
} }
async postAddRecipient (req, res) { async postAddRecipient (req, res) {
const { newsletter: newsletterService, displayEngine: displayEngineService } = this.dtp.services; const { newsletter: newsletterService } = this.dtp.services;
try { try {
const displayList = displayEngineService.createDisplayList('add-recipient'); const displayList = this.createDisplayList('add-recipient');
await newsletterService.addRecipient(req.body.email); await newsletterService.addRecipient(req.body.email);
displayList.showNotification( displayList.showNotification(
'You have been added to the newsletter. Please check your email and verify your email address.', '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) => { module.exports = {
let controller = new NewsletterController(dtp); slug: 'newsletter',
return controller; 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(this.dtp.services.gabTV.channelMiddleware('mrjoeprich'));
router.use(async (req, res, next) => { router.use(async (req, res, next) => {
res.locals.currentView = 'home'; res.locals.currentView = 'page';
return next(); return next();
}); });
@ -55,6 +55,7 @@ class PageController extends SiteController {
const { resource: resourceService } = this.dtp.services; const { resource: resourceService } = this.dtp.services;
try { try {
await resourceService.recordView(req, 'Page', res.locals.page._id); await resourceService.recordView(req, 'Page', res.locals.page._id);
res.locals.pageSlug = res.locals.page.slug;
res.render('page/view'); res.render('page/view');
} catch (error) { } catch (error) {
this.log.error('failed to service page view', { pageId: res.locals.page._id, 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) => { module.exports = {
let controller = new PageController(dtp); slug: 'page',
return controller; 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 ( ) { async start ( ) {
const { dtp } = this; 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 upload = multer({ dest: `/tmp/dtp-sites/${this.dtp.config.site.domainKey}`});
const router = express.Router(); const router = express.Router();
@ -33,8 +38,10 @@ class PostController extends SiteController {
}); });
router.param('postSlug', this.populatePostSlug.bind(this)); 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', router.get('/:postSlug',
limiterService.create(limiterService.config.post.getView), 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) { async postComment (req, res) {
const { const { comment: commentService } = this.dtp.services;
comment: commentService,
displayEngine: displayEngineService,
} = this.dtp.services;
try { 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); 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); await resourceService.recordView(req, 'Post', res.locals.post._id);
res.locals.pagination = this.getPaginationParameters(req, 20); 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.comments = await commentService.getForResource(
res.locals.post, res.locals.post,
['published', 'mod-warn'], ['published', 'mod-warn', 'mod-removed', 'removed'],
res.locals.pagination, res.locals.pagination,
); );
@ -123,7 +153,11 @@ class PostController extends SiteController {
} }
} }
module.exports = async (dtp) => { module.exports = {
let controller = new PostController(dtp); slug: 'post',
return controller; 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 ( ) { async start ( ) {
const { dtp } = this; 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(); const router = express.Router();
dtp.app.use('/user', router); dtp.app.use('/user', router);
const authRequired = sessionService.authCheckMiddleware({ requireLogin: true });
const otpMiddleware = otpAuthService.middleware('Account', { const otpMiddleware = otpAuthService.middleware('Account', {
adminRequired: false, adminRequired: false,
otpRequired: false, otpRequired: false,
@ -53,6 +58,12 @@ class UserController extends SiteController {
router.param('userId', this.populateUser.bind(this)); 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', router.post('/:userId/settings',
limiterService.create(limiterService.config.user.postUpdateSettings), limiterService.create(limiterService.config.user.postUpdateSettings),
upload.none(), upload.none(),
@ -66,16 +77,25 @@ class UserController extends SiteController {
router.get('/:userId/settings', router.get('/:userId/settings',
limiterService.create(limiterService.config.user.getSettings), limiterService.create(limiterService.config.user.getSettings),
authRequired,
otpMiddleware, otpMiddleware,
checkProfileOwner, checkProfileOwner,
this.getUserSettingsView.bind(this), this.getUserSettingsView.bind(this),
); );
router.get('/:userId', router.get('/:userId',
limiterService.create(limiterService.config.user.getUserProfile), limiterService.create(limiterService.config.user.getUserProfile),
authRequired,
otpMiddleware, otpMiddleware,
checkProfileOwner, checkProfileOwner,
this.getUserView.bind(this), 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) { async populateUser (req, res, next, userId) {
@ -100,7 +120,23 @@ class UserController extends SiteController {
async postCreateUser (req, res, next) { async postCreateUser (req, res, next) {
const { user: userService } = this.dtp.services; const { user: userService } = this.dtp.services;
try { 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); res.locals.user = await userService.create(req.body);
// log the user in
req.login(res.locals.user, (error) => { req.login(res.locals.user, (error) => {
if (error) { if (error) {
return next(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) { async postUpdateSettings (req, res) {
const { user: userService, displayEngine: displayEngineService } = this.dtp.services; const { user: userService } = this.dtp.services;
try { try {
const displayList = displayEngineService.createDisplayList('app-settings'); const displayList = this.createDisplayList('app-settings');
await userService.updateSettings(req.user, req.body); await userService.updateSettings(req.user, req.body);
@ -147,16 +225,65 @@ class UserController extends SiteController {
} }
async getUserView (req, res, next) { async getUserView (req, res, next) {
const { comment: commentService } = this.dtp.services;
try { try {
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.commentHistory = await commentService.getForAuthor(req.user, res.locals.pagination);
res.render('user/profile'); res.render('user/profile');
} catch (error) { } catch (error) {
this.log.error('failed to produce user profile view', { error }); this.log.error('failed to produce user profile view', { error });
return next(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) => { module.exports = {
let controller = new UserController(dtp); slug: 'user',
return controller; 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) => { module.exports = {
let controller = new WelcomeController(dtp); slug: 'welcome',
return controller; 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 }, 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 COMMENT_STATUS_LIST = ['published', 'removed', 'mod-warn', 'mod-removed'];
const CommentSchema = new Schema({ const CommentSchema = new Schema({
created: { type: Date, default: Date.now, required: true, index: 1 }, 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' }, resource: { type: Schema.ObjectId, required: true, index: 1, refPath: 'resourceType' },
author: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' }, author: { type: Schema.ObjectId, required: true, index: 1, ref: 'User' },
replyTo: { type: Schema.ObjectId, index: 1, ref: 'Comment' }, replyTo: { type: Schema.ObjectId, index: 1, ref: 'Comment' },
status: { type: String, enum: COMMENT_STATUS_LIST, default: 'published', required: true }, status: { type: String, enum: COMMENT_STATUS_LIST, default: 'published', required: true },
content: { type: String, required: true, maxlength: 3000 }, content: { type: String, required: true, maxlength: 3000 },
contentHistory: { type: [CommentHistorySchema], select: false }, contentHistory: { type: [CommentHistorySchema], select: false },
flags: {
isNSFW: { type: Boolean, default: false, required: true },
},
stats: { type: CommentStats, default: CommentStatsDefaults, 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({ EmailBlacklistSchema.index({
email: 1, email: 1,
'flags.isVerified': true, 'flags.isVerified': 1,
}, { }, {
partialFilterExpression: { partialFilterExpression: {
'flags.isVerified': true, '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; const Schema = mongoose.Schema;
module.exports.ResourceStats = new Schema({ module.exports.ResourceStats = new Schema({
totalViewCount: { type: Number, default: 0, required: true }, uniqueVisitCount: { type: Number, default: 0, required: true, index: -1 },
upvoteCount: { type: Number, default: 0, required: true }, totalVisitCount: { type: Number, default: 0, required: true },
downvoteCount: { type: Number, default: 0, required: true },
commentCount: { type: Number, default: 0, required: true },
}); });
module.exports.ResourceStatsDefaults = { module.exports.ResourceStatsDefaults = {
totalViewCount: 0, uniqueVisitCount: 0,
upvoteCount: 0, totalVisitCount: 0,
downvoteCount: 0,
commentCount: 0,
}; };
module.exports.CommentStats = new Schema({ module.exports.CommentStats = new Schema({

4
app/models/log.js

@ -20,10 +20,10 @@ const LOG_LEVEL_LIST = [
const LogSchema = new Schema({ const LogSchema = new Schema({
created: { type: Date, default: Date.now, required: true, index: -1, expires: '7d' }, 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 }, level: { type: String, enum: LOG_LEVEL_LIST, required: true, index: true },
message: { type: String }, message: { type: String },
metadata: { type: Schema.Types.Mixed }, 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 }, content: { type: String, required: true, select: false },
status: { type: String, enum: PAGE_STATUS_LIST, default: 'draft', index: true }, status: { type: String, enum: PAGE_STATUS_LIST, default: 'draft', index: true },
menu: { menu: {
icon: { type: String, required: true },
label: { type: String, required: true }, label: { type: String, required: true },
order: { type: Number, default: 0, required: true }, order: { type: Number, default: 0, required: true },
parent: { type: Schema.ObjectId, index: 1, ref: 'Page' }, 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 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']; 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 }, resourceType: { type: String, enum: RESOURCE_TYPE_LIST, required: true },
resource: { type: Schema.ObjectId, required: true, index: 1, refPath: 'resourceType' }, resource: { type: Schema.ObjectId, required: true, index: 1, refPath: 'resourceType' },
uniqueKey: { type: String, required: true, index: 1 }, uniqueKey: { type: String, required: true, index: 1 },
viewCount: { type: Number, default: 0, required: true }, visitCount: { type: Number, default: 0, required: true },
}); });
ResourceViewSchema.index({ ResourceViewSchema.index({
@ -27,4 +27,4 @@ ResourceViewSchema.index({
name: 'res_view_daily_unique', 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 Schema = mongoose.Schema;
const { ResourceStats, ResourceStatsDefaults } = require('./lib/resource-stats');
const UserFlagsSchema = new Schema({ const UserFlagsSchema = new Schema({
isAdmin: { type: Boolean, default: false, required: true }, isAdmin: { type: Boolean, default: false, required: true },
isModerator: { 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({ const UserPermissionsSchema = new Schema({
canLogin: { type: Boolean, default: true, required: true }, canLogin: { type: Boolean, default: true, required: true },
canChat: { 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({ const UserSchema = new Schema({
@ -26,12 +30,14 @@ const UserSchema = new Schema({
passwordSalt: { type: String, required: true }, passwordSalt: { type: String, required: true },
password: { type: String, required: true }, password: { type: String, required: true },
displayName: { type: String }, displayName: { type: String },
bio: { type: String, maxlength: 300 },
picture: { picture: {
large: { type: Schema.ObjectId, ref: 'Image' }, large: { type: Schema.ObjectId, ref: 'Image' },
small: { type: Schema.ObjectId, ref: 'Image' }, small: { type: Schema.ObjectId, ref: 'Image' },
}, },
flags: { type: UserFlagsSchema, select: false }, flags: { type: UserFlagsSchema, select: false },
permissions: { type: UserPermissionsSchema, 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 = [ this.populateComment = [
{ {
path: 'author', path: 'author',
select: '', select: '_id username username_lc displayName picture',
}, },
{ {
path: 'replyTo', 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 ( ) { 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')); 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) { async create (author, resourceType, resource, commentDefinition) {
const NOW = new Date(); const NOW = new Date();
let comment = new Comment(); let comment = new Comment();
@ -50,6 +81,10 @@ class CommentService extends SiteService {
comment.content = striptags(commentDefinition.content.trim()); comment.content = striptags(commentDefinition.content.trim());
} }
comment.flags = {
isNSFW: commentDefinition.isNSFW === 'on',
};
await comment.save(); await comment.save();
const model = mongoose.model(resourceType); const model = mongoose.model(resourceType);
@ -116,6 +151,7 @@ class CommentService extends SiteService {
* @param {String} status * @param {String} status
*/ */
async remove (comment, status = 'removed') { async remove (comment, status = 'removed') {
const { contentReport: contentReportService } = this.dtp.services;
await Comment.updateOne( await Comment.updateOne(
{ _id: comment._id }, { _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) { async getForResource (resource, statuses, pagination) {
@ -144,6 +189,17 @@ class CommentService extends SiteService {
return comments; 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) { async getContentHistory (comment, pagination) {
/* /*
* Extract a page from the contentHistory using $slice on the array * Extract a page from the contentHistory using $slice on the array
@ -170,4 +226,4 @@ module.exports = {
slug: 'comment', slug: 'comment',
name: 'comment', name: 'comment',
create: (dtp) => { return new CommentService(dtp); }, 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 }, params: { remove, add },
}); });
} }
navigateTo (href) {
this.commands.push({
action: 'navigateTo',
params: { href },
});
}
} }
class DisplayEngineService extends SiteService { 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`); 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) { async uploadFile (fileInfo) {
try { try {
const result = await this.minio.fPutObject( const result = await this.minio.fPutObject(
@ -75,4 +85,4 @@ module.exports = {
slug: 'minio', slug: 'minio',
name: 'minio', name: 'minio',
create: (dtp) => { return new MinioService(dtp); }, create: (dtp) => { return new MinioService(dtp); },
}; };

18
app/services/otp-auth.js

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

18
app/services/page.js

@ -22,11 +22,18 @@ class PageService extends SiteService {
async menuMiddleware (req, res, next) { async menuMiddleware (req, res, next) {
try { 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 res.locals.mainMenu = pages
.filter((page) => !page.parent)
.map((page) => { .map((page) => {
return { return {
url: `/page/${page.slug}`, url: `/page/${page.slug}`,
slug: page.slug,
icon: page.menu.icon,
label: page.menu.label, label: page.menu.label,
order: page.menu.order, order: page.menu.order,
}; };
@ -47,10 +54,13 @@ class PageService extends SiteService {
page.slug = this.createPageSlug(page._id, page.title); page.slug = this.createPageSlug(page._id, page.title);
page.content = pageDefinition.content.trim(); page.content = pageDefinition.content.trim();
page.status = pageDefinition.status || 'draft'; page.status = pageDefinition.status || 'draft';
page.menu = { 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), order: parseInt(pageDefinition.menuOrder || '0', 10),
}; };
if (pageDefinition.parentPageId && (pageDefinition.parentPageId !== 'none')) { if (pageDefinition.parentPageId && (pageDefinition.parentPageId !== 'none')) {
page.menu.parent = pageDefinition.parentPageId; page.menu.parent = pageDefinition.parentPageId;
} }
@ -90,9 +100,11 @@ class PageService extends SiteService {
} }
updateOp.$set.menu = { 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), order: parseInt(pageDefinition.menuOrder || '0', 10),
}; };
if (pageDefinition.parentPageId && (pageDefinition.parentPageId !== 'none')) { if (pageDefinition.parentPageId && (pageDefinition.parentPageId !== 'none')) {
updateOp.$set.menu.parent = pageDefinition.parentPageId; updateOp.$set.menu.parent = pageDefinition.parentPageId;
} }

74
app/services/resource.js

@ -6,9 +6,12 @@
const { SiteService } = require('../../lib/site-lib'); const { SiteService } = require('../../lib/site-lib');
const geoip = require('geoip-lite');
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const ResourceView = mongoose.model('ResourceView'); const ResourceView = mongoose.model('ResourceView');
const ResourceVisit = mongoose.model('ResourceVisit');
class ResourceService extends SiteService { class ResourceService extends SiteService {
@ -32,7 +35,11 @@ class ResourceService extends SiteService {
* a view is being tracked. * a view is being tracked.
*/ */
async recordView (req, resourceType, resourceId) { 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); CURRENT_DAY.setHours(0, 0, 0, 0);
let uniqueKey = req.ip.toString().trim().toLowerCase(); let uniqueKey = req.ip.toString().trim().toLowerCase();
@ -48,20 +55,65 @@ class ResourceService extends SiteService {
uniqueKey, uniqueKey,
}, },
{ {
$inc: { viewCount: 1 }, $inc: { 'stats.visitCount': 1 },
}, },
{ upsert: true }, { upsert: true },
); );
this.log.debug('resource view', { response });
if (response.upsertedCount > 0) { if (response.upsertedCount > 0) {
const Model = mongoose.model(resourceType); modelUpdate.$inc['stats.uniqueViewCount'] = 1;
await Model.updateOne( }
{ _id: resourceId },
{ /*
$inc: { 'stats.totalViewCount': 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', slug: 'resource',
name: 'resource', name: 'resource',
create: (dtp) => { return new ResourceService(dtp); }, 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'; 'use strict';
const mongoose = require('mongoose'); const mongoose = require('mongoose');
const User = mongoose.model('User'); const User = mongoose.model('User');
const UserBlock = mongoose.model('UserBlock');
const passport = require('passport'); const passport = require('passport');
const PassportLocal = require('passport-local'); const PassportLocal = require('passport-local');
@ -20,6 +22,15 @@ class UserService {
constructor (dtp) { constructor (dtp) {
this.dtp = dtp; this.dtp = dtp;
this.log = new SiteLog(dtp, `svc:${module.exports.slug}`); this.log = new SiteLog(dtp, `svc:${module.exports.slug}`);
this.populateUser = [
{
path: 'picture.large',
},
{
path: 'picture.small',
},
];
} }
async start ( ) { async start ( ) {
@ -47,6 +58,7 @@ class UserService {
// strip characters we don't want to allow in username // strip characters we don't want to allow in username
userDefinition.username = userDefinition.username.trim().replace(/[^A-Za-z0-9\-_]/gi, ''); userDefinition.username = userDefinition.username.trim().replace(/[^A-Za-z0-9\-_]/gi, '');
const username_lc = userDefinition.username.toLowerCase(); const username_lc = userDefinition.username.toLowerCase();
await this.checkUsername(username_lc);
// test the email address for validity, blacklisting, etc. // test the email address for validity, blacklisting, etc.
await mailService.checkEmailAddress(userDefinition.email); await mailService.checkEmailAddress(userDefinition.email);
@ -72,6 +84,7 @@ class UserService {
user.email = userDefinition.email; user.email = userDefinition.email;
user.username = userDefinition.username; user.username = userDefinition.username;
user.username_lc = username_lc; user.username_lc = username_lc;
user.displayName = striptags(userDefinition.displayName || userDefinition.username);
user.passwordSalt = passwordSalt; user.passwordSalt = passwordSalt;
user.password = maskedPassword; user.password = maskedPassword;
@ -84,6 +97,8 @@ class UserService {
user.permissions = { user.permissions = {
canLogin: true, canLogin: true,
canChat: true, canChat: true,
canComment: true,
canReport: true,
}; };
this.log.info('creating new user account', { email: userDefinition.email }); this.log.info('creating new user account', { email: userDefinition.email });
@ -102,6 +117,7 @@ class UserService {
const username_lc = userDefinition.username.toLowerCase(); const username_lc = userDefinition.username.toLowerCase();
userDefinition.displayName = striptags(userDefinition.displayName.trim()); userDefinition.displayName = striptags(userDefinition.displayName.trim());
userDefinition.bio = striptags(userDefinition.bio.trim());
this.log.info('updating user', { userDefinition }); this.log.info('updating user', { userDefinition });
await User.updateOne( await User.updateOne(
@ -111,10 +127,13 @@ class UserService {
username: userDefinition.username, username: userDefinition.username,
username_lc, username_lc,
displayName: userDefinition.displayName, displayName: userDefinition.displayName,
bio: userDefinition.bio,
'flags.isAdmin': userDefinition.isAdmin === 'on', 'flags.isAdmin': userDefinition.isAdmin === 'on',
'flags.isModerator': userDefinition.isModerator === 'on', 'flags.isModerator': userDefinition.isModerator === 'on',
'permissions.canLogin': userDefinition.canLogin === 'on', 'permissions.canLogin': userDefinition.canLogin === 'on',
'permissions.canChat': userDefinition.canChat === '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(); const username_lc = userDefinition.username.toLowerCase();
userDefinition.displayName = striptags(userDefinition.displayName.trim()); userDefinition.displayName = striptags(userDefinition.displayName.trim());
userDefinition.bio = striptags(userDefinition.bio.trim());
this.log.info('updating user settings', { userDefinition }); this.log.info('updating user settings', { userDefinition });
await User.updateOne( await User.updateOne(
@ -135,6 +155,7 @@ class UserService {
username: userDefinition.username, username: userDefinition.username,
username_lc, username_lc,
displayName: userDefinition.displayName, displayName: userDefinition.displayName,
bio: userDefinition.bio,
}, },
}, },
); );
@ -253,6 +274,7 @@ class UserService {
const user = await User const user = await User
.findById(userId) .findById(userId)
.select('+email +flags +permissions') .select('+email +flags +permissions')
.populate(this.populateUser)
.lean(); .lean();
if (!user) { if (!user) {
throw new SiteError(404, 'Member account not found'); throw new SiteError(404, 'Member account not found');
@ -260,9 +282,13 @@ class UserService {
return user; return user;
} }
async getUserAccounts (pagination) { async getUserAccounts (pagination, username) {
let search = { };
if (username) {
search.username_lc = { $regex: `^${username.toLowerCase().trim()}` };
}
const users = await User const users = await User
.find() .find(search)
.sort({ username_lc: 1 }) .sort({ username_lc: 1 })
.select('+email +flags +permissions') .select('+email +flags +permissions')
.skip(pagination.skip) .skip(pagination.skip)
@ -280,10 +306,36 @@ class UserService {
} catch (error) { } catch (error) {
user = User.findOne({ username: userId }); 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; 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) { async setUserSettings (user, settings) {
const { const {
crypto: cryptoService, crypto: cryptoService,
@ -302,10 +354,7 @@ class UserService {
if (settings.username && (settings.username !== user.username)) { if (settings.username && (settings.username !== user.username)) {
update.username = this.filterUsername(settings.username); update.username = this.filterUsername(settings.username);
const isReserved = await this.isUsernameReserved(update.username); await this.checkUsername(update.username);
if (!isReserved) {
throw new SiteError(403, 'The username you entered is taken');
}
} }
if (settings.email && (settings.email !== user.email)) { if (settings.email && (settings.email !== user.email)) {
@ -360,20 +409,128 @@ class UserService {
return striptags(username.trim().toLowerCase()).replace(/\W/g, ''); return striptags(username.trim().toLowerCase()).replace(/\W/g, '');
} }
async isUsernameReserved (username) { async checkUsername (username) {
const reservedNames = ['digitaltelepresence', 'dtp', 'rob', 'amy', 'zack']; if (!username || (typeof username !== 'string') || (username.length === 0)) {
if (reservedNames.includes(username)) { throw new SiteError(406, 'Invalid username');
this.log.alert('prohibiting use of reserved username', { username }); }
return true; 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(); const user = await User.findOne({ username: username}).select('username').lean();
if (user) { if (user) {
this.log.alert('username is already registered', { username }); 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 span.nav-item-icon
i.fas.fa-home i.fas.fa-home
span.uk-margin-small-left Home span.uk-margin-small-left Home
li(class={ 'uk-active': (adminView === 'settings') })
a(href="/admin/settings") a(href="/admin/settings")
span.nav-item-icon span.nav-item-icon
i.fas.fa-cog i.fas.fa-cog
@ -29,6 +30,19 @@ ul.uk-nav.uk-nav-default
i.fas.fa-newspaper i.fas.fa-newspaper
span.uk-margin-small-left Newsletter 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.uk-nav-divider
li(class={ 'uk-active': (adminView === 'host') }) li(class={ 'uk-active': (adminView === 'host') })
@ -41,6 +55,11 @@ ul.uk-nav.uk-nav-default
span.nav-item-icon span.nav-item-icon
i.fas.fa-microchip i.fas.fa-microchip
span.uk-margin-small-left Jobs 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 li.uk-nav-divider
@ -48,4 +67,4 @@ ul.uk-nav.uk-nav-default
a(href="/admin/user") a(href="/admin/user")
span.nav-item-icon span.nav-item-icon
i.fas.fa-user 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 extends ../layouts/main
block content block content
table.uk-table.uk-table-small.uk-table-divider mixin renderHostList (hosts)
thead if Array.isArray(hosts) && (hosts.length > 0)
th Host table.uk-table.uk-table-small.uk-table-divider
th Status thead
th Memory th Host
th Platform th Status
th Arch th Memory
th Created th Platform
th Updated th Arch
tbody th Created
each host in hosts th Updated
tr tbody
td each host in hosts
a(href=`/admin/host/${host._id}`)= host.hostname tr(data-host-id= host._id)
td= host.status td
td= numeral((host.totalmem - host.freemem) / host.totalmem).format('0.00%') a(href=`/admin/host/${host._id}`)= host.hostname
td= host.platform td= host.status
td= host.arch td= numeral((host.totalmem - host.freemem) / host.totalmem).format('0.00%')
td= moment(host.created).fromNow() td= host.platform
td= host.updated ? moment(host.updated).fromNow() : 'N/A' td= host.arch
td= moment(host.created).fromNow()
td= host.updated ? moment(host.updated).fromNow() : 'N/A'
else
div The host list is empty
if Array.isArray(activeHosts) && (activeHosts.length > 0)
h2 Active hosts
+renderHostList(activeHosts)
if Array.isArray(crashedHosts) && (crashedHosts.length > 0)
h2 Crashed hosts
+renderHostList(crashedHosts)

22
app/views/admin/index.pug

@ -1,12 +1,22 @@
extends layouts/main extends layouts/main
block content block content
div(uk-grid).uk-grid-small.uk-flex-between.uk-flex-middle .uk-margin
canvas(id="hourly-signups")
div(uk-grid).uk-grid-small.uk-flex-middle
.uk-width-auto .uk-width-auto
+renderCell('Members', formatCount(stats.memberCount)) +renderCell('Members', formatCount(stats.memberCount))
.uk-width-auto .uk-width-auto
+renderCell('Channels', formatCount(stats.channelCount)) +renderCell('Posts', formatCount(stats.postCount))
.uk-width-auto .uk-width-auto
+renderCell('Streams', formatCount(stats.streamCount)) +renderCell('Comments', formatCount(stats.commentCount))
.uk-width-auto
+renderCell('Viewers', formatCount(stats.viewerCount)) block viewjs
script(src="/chart.js/chart.min.js")
script(src="/chartjs-adapter-moment/chartjs-adapter-moment.min.js")
script.
window.addEventListener('dtp-load', ( ) => {
const graphData = !{JSON.stringify(stats.userSignupHourly)};
dtp.app.renderStatsGraph('#hourly-signups', 'Hourly Signups', graphData);
});

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

@ -1,4 +1,7 @@
extends ../../layouts/main extends ../../layouts/main
block vendorcss
link(rel="stylesheet", href="/highlight.js/styles/default.css")
block content-container block content-container
block page-header block page-header
@ -16,4 +19,4 @@ block content-container
include ../components/menu include ../components/menu
div(class="uk-width-1-1 uk-flex-first uk-width-expand@m").uk-width-expand 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 fieldset
legend Menu 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 .uk-margin
label(for="menu-label").uk-form-label Menu item label 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 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 block content
form(method="POST", action="/admin/settings").uk-form form(method="POST", action="/admin/settings").uk-form
.uk-margin fieldset
label(for="name").uk-form-label Site name legend Site Information
input(id="name", name="name", type="text", maxlength="200", placeholder="Enter site name", value= site.name).uk-input .uk-margin
.uk-margin label(for="name").uk-form-label Site name
label(for="description").uk-form-label Site description input(id="name", name="name", type="text", maxlength="200", placeholder="Enter site name", value= site.name).uk-input
input(id="description", name="description", type="text", maxlength="500", placeholder="Enter site description", value= site.description).uk-input .uk-margin
.uk-margin label(for="description").uk-form-label Site description
label(for="company").uk-form-label Company name input(id="description", name="description", type="text", maxlength="500", placeholder="Enter site description", value= site.description).uk-input
input(id="company", name="company", type="text", maxlength="200", placeholder="Enter company name", value= site.company).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 button(type="submit").uk-button.dtp-button-primary Save Settings

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

@ -33,5 +33,11 @@ block content
label label
input(id="can-chat", name="canChat", type="checkbox", checked= userAccount.permissions.canChat) input(id="can-chat", name="canChat", type="checkbox", checked= userAccount.permissions.canChat)
| Can Chat | 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 button(type="submit").uk-button.uk-button-primary Update User

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

@ -1,6 +1,16 @@
extends ../layouts/main extends ../layouts/main
block content 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 .uk-overflow-auto
table.uk-table.uk-table-divider.uk-table-hover.uk-table-small.uk-table-justify table.uk-table.uk-table-divider.uk-table-hover.uk-table-small.uk-table-justify
thead thead
@ -19,4 +29,7 @@ block content
else else
.uk-text-muted N/A .uk-text-muted N/A
td= moment(userAccount.created).format('YYYY-MM-DD hh:mm 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) mixin renderComment (comment)
.uk-card.uk-card-secondary.uk-card-small.uk-border-rounded - var resourceId = comment.resource._id || comment.resource;
.uk-card-body article(data-comment-id= comment._id).uk-comment.dtp-site-comment
div(uk-grid).uk-grid-small header.uk-comment-header
div(uk-grid).uk-grid-medium.uk-flex-middle
.uk-width-auto .uk-width-auto
img(src="/img/default-member.png").site-profile-picture.sb-small img(src="/img/default-member.png").site-profile-picture.sb-small.uk-comment-avatar
.uk-width-expand .uk-width-expand
div(uk-grid).uk-grid-small.uk-flex-middle.uk-text-small h4.uk-comment-title.uk-margin-remove= comment.author.displayName || comment.author.username
if comment.author.displayName .uk-comment-meta= moment(comment.created).fromNow()
.uk-width-auto
span= comment.author.displayName if user && (comment.status === 'published')
.uk-width-auto= comment.author.username .uk-width-auto
.uk-width-auto= moment(comment.created).fromNow() button(type="button").uk-button.uk-button-link
div!= marked.parse(comment.content) span
div(uk-grid).uk-grid-small.uk-text-small i.fas.fa-ellipsis-h
.uk-width-auto div(data-comment-id= comment._id, uk-dropdown={ mode: 'click', pos: 'bottom-right' })
button( ul.uk-nav.uk-dropdown-nav
type="button", if user && user._id.equals(comment.author._id)
data-comment-id= comment._id, li.uk-nav-header.no-select Author menu
onclick="return dtp.app.upvoteComment(event);", li
title="Upvote this comment", a(
).uk-button.uk-button-link href="",
+renderButtonIcon('fa-chevron-up', formatCount(comment.stats.upvoteCount)) data-comment-id= comment._id,
.uk-width-auto onclick="return dtp.app.deleteComment(event);",
button( ) Delete
type="button", else if user
data-comment-id= comment._id, li.uk-nav-header.no-select Moderation menu
onclick="return dtp.app.downvoteComment(event);", li
title="Downvote this comment", a(
).uk-button.uk-button-link href="",
+renderButtonIcon('fa-chevron-down', formatCount(comment.stats.downvoteCount)) data-resource-type= comment.resourceType,
.uk-width-auto data-resource-id= resourceId,
button( data-comment-id= comment._id,
type="button", onclick="return dtp.app.showReportCommentForm(event);",
data-comment-id= comment._id, ) Report
onclick="return dtp.app.openReplies(event);", li
title="Load replies to this comment", a(
).uk-button.uk-button-link href="",
+renderButtonIcon('fa-comment', formatCount(comment.stats.replyCount)) data-resource-type= comment.resourceType,
.uk-width-auto data-resource-id= resourceId,
button( data-comment-id= comment._id,
type="button", onclick="return dtp.app.blockCommentAuthor(event);",
data-comment-id= comment._id, ) Block author
onclick="return dtp.app.openReplyComposer(event);",
title="Write a reply to this comment", .uk-comment-body
).uk-button.uk-button-link case comment.status
+renderButtonIcon('fa-reply', 'reply') when 'published'
if comment.flags && comment.flags.isNSFW
div.uk-alert.uk-alert-info.uk-border-rounded
div(uk-grid).uk-grid-small.uk-text-small.uk-flex-middle
.uk-width-expand NSFW comment hidden by default. Use the eye to show/hide.
.uk-width-auto
button(
type="button",
uk-toggle={ target: `.comment-content[data-comment-id="${comment._id}"]` },
title="Show/hide the comment text",
).uk-button.uk-button-link
span
i.fas.fa-eye
.comment-content(data-comment-id= comment._id, hidden= comment.flags ? comment.flags.isNSFW : false)!= marked.parse(comment.content)
when 'removed'
.comment-content.uk-text-muted [comment removed]
when 'mod-warn'
alert
span A warning has been added to this comment.
button(type="button", uk-toggle={ target: `.comment-content[data-comment-id="${comment._id}"]` })
.comment-content(data-comment-id= comment._id, hidden)!= marked.parse(comment.content)
when 'mod-removed'
.comment-content.uk-text-muted [comment removed]
//- Comment meta bar
div(uk-grid).uk-grid-small
.uk-width-auto
button(
type="button",
data-comment-id= comment._id,
data-vote="up",
onclick="return dtp.app.submitCommentVote(event);",
title="Upvote this comment",
).uk-button.uk-button-link
+renderLabeledIcon('fa-chevron-up', formatCount(comment.stats.upvoteCount))
.uk-width-auto
button(
type="button",
data-comment-id= comment._id,
data-vote="down",
onclick="return dtp.app.submitCommentVote(event);",
title="Downvote this comment",
).uk-button.uk-button-link
+renderLabeledIcon('fa-chevron-down', formatCount(comment.stats.downvoteCount))
.uk-width-auto
button(
type="button",
data-comment-id= comment._id,
onclick="return dtp.app.openReplies(event);",
title="Load replies to this comment",
).uk-button.uk-button-link
+renderLabeledIcon('fa-comment', formatCount(comment.stats.replyCount))
.uk-width-auto
button(
type="button",
data-comment-id= comment._id,
onclick="return dtp.app.openReplyComposer(event);",
title="Write a reply to this comment",
).uk-button.uk-button-link
+renderLabeledIcon('fa-reply', 'reply')
//- Comment replies and reply composer
div(data-comment-id= comment._id)
if user && user.flags.canComment
.uk-margin
+renderCommentComposer(`/post`, { replyTo: comment._id })

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) mixin renderFileUploadImage (actionUrl, containerId, imageId, imageClass, defaultImage, currentImage, cropperOptions)
div(id= containerId).dtp-file-upload 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-margin
.uk-card.uk-card-default.uk-card-small .uk-card.uk-card-default.uk-card-small
.uk-card-body .uk-card-body
@ -8,9 +8,9 @@ mixin renderFileUploadImage (actionUrl, containerId, imageId, imageClass, defaul
div(class="uk-width-1-1 uk-width-auto@m") div(class="uk-width-1-1 uk-width-auto@m")
.upload-image-container.size-512 .upload-image-container.size-512
if !!currentImage 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 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") div(class="uk-width-1-1 uk-width-auto@m")
.uk-text-small.uk-margin(hidden= !!currentImage) .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 div(uk-form-custom).uk-margin-small-left
input( input(
type="file", type="file",
name="imageFile",
formenctype="multipart/form-data", formenctype="multipart/form-data",
accept=".jpg,.png,image/jpeg,image/png", accept=".jpg,.png,image/jpeg,image/png",
data-file-select-container= containerId, data-file-select-container= containerId,
@ -29,8 +28,7 @@ mixin renderFileUploadImage (actionUrl, containerId, imageId, imageClass, defaul
data-file-size-element= "file-size", data-file-size-element= "file-size",
data-file-max-size= 15 * 1024000, data-file-max-size= 15 * 1024000,
data-image-id= imageId, data-image-id= imageId,
data-image-w= 512, data-cropper-options= cropperOptions,
data-image-h= 512,
onchange="return dtp.app.selectImageFile(event);", onchange="return dtp.app.selectImageFile(event);",
) )
button(type="button", tabindex="-1").uk-button.dtp-button-default Select 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 #remove-btn(hidden= !currentImage).uk-width-auto
button( button(
type= "button", type= "button",
data-image-type= imageId,
onclick= "return dtp.app.removeImageFile(event);", onclick= "return dtp.app.removeImageFile(event);",
).uk-button.uk-button-danger Remove ).uk-button.uk-button-danger Remove
#file-save-btn(hidden).uk-width-auto #file-save-btn(hidden).uk-width-auto
button( button(
type="submit", 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 //- common routines for all views everywhere
include button-icon include button-icon
include labeled-icon
include section-title 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 .uk-navbar-item
button(type="button", uk-toggle="target: #dtp-offcanvas").uk-button.uk-button-link.uk-padding-small button(type="button", uk-toggle="target: #dtp-offcanvas").uk-button.uk-button-link.uk-padding-small
i.fas.fa-bars i.fas.fa-bars
div(class="uk-visible@m").uk-navbar-left
//- Site icon //- Site icon
a(href="/", class="uk-visible@m").uk-navbar-item a(href="/").uk-navbar-item
img(src=`/img/icon/icon-48x48.png`) img(src=`/img/icon/icon-48x48.png`)
//- Site name //- Site name
a(href="/", class="uk-visible@xl").uk-navbar-item.uk-logo ul.uk-navbar-nav
span= site.name 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 div(class="uk-hidden@m").uk-navbar-center
a(href="/").uk-navbar-item //- Site name
img(src=`/img/icon/icon-48x48.png`) 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 .uk-navbar-right
if user
ul.uk-navbar-nav
.uk-navbar-item .uk-navbar-item
if user if user
div.no-select div.no-select
@ -36,7 +50,7 @@ nav(uk-navbar).uk-navbar-container.uk-position-fixed.uk-position-top
).site-profile-picture.sb-navbar ).site-profile-picture.sb-navbar
div(uk-dropdown={ mode: 'click' }).uk-navbar-dropdown 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-heading.uk-text-center= user.displayName || user.username
li.uk-nav-divider li.uk-nav-divider
if (user.channel) 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;") #dtp-offcanvas(uk-offcanvas="mode: slide; overlay: true; bg-close: true;")
.uk-offcanvas-bar .uk-offcanvas-bar
.uk-margin .uk-margin
a(href="/", style="color: white;").uk-display-block a(href="/").uk-display-block
.uk-text-large= site.name .uk-text-large= site.name
.uk-text-small.uk-text-muted= site.description .uk-text-small.uk-text-muted= site.description
ul.uk-nav.uk-nav-default.dtp-app-menu ul.uk-nav.uk-nav-default.dtp-app-menu
li.uk-nav-header Site Menu
li(class={ "uk-active": (currentView === 'home') }) li(class={ "uk-active": (currentView === 'home') })
a(href='/').uk-display-block a(href='/').uk-display-block
div(uk-grid).uk-grid-collapse +renderMenuItem('fa-home', 'Home')
.uk-width-auto
.app-menu-icon each menuItem in mainMenu
i(class=`fas fa-home`) li(class={ 'uk-active': (pageSlug === menuItem.slug) })
.uk-width-expand Home a(href= menuItem.url, title= menuItem.label)
+renderMenuItem(menuItem.icon || 'fa-file', menuItem.label)
if user if user
li.uk-nav-header Member Menu 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 section.uk-section.uk-section-muted.uk-section-small.dtp-site-footer
.uk-container.uk-text-small.uk-text-center .uk-container.uk-text-small.uk-text-center
ul.uk-subnav.uk-flex-center ul.uk-subnav.uk-flex-center
each socialIcon in socialIcons if site.gabUrl
li li
a(href=socialIcon.url).dtp-social-link a(href= site.gabUrl).dtp-social-link
span span
i(class=`fab ${socialIcon.icon}`) img(src="/img/gab-g.svg", style="width: auto; height: 1em;")
span.uk-margin-small-left= socialIcon.label span.uk-margin-small-left Gab Social
if site.gabtvUrl
li
a(href= site.gabtvUrl).dtp-social-link
span
img(src="/img/gab-g.svg", style="width: auto; height: 1em;")
span.uk-margin-small-left Gab TV
if site.telegramUrl
li
+renderSocialIcon('fa-telegram', 'Telegram', site.telegramUrl)
if site.twitterUrl
li
+renderSocialIcon('fa-twitter', 'Twitter', site.twitterUrl)
if site.facebookUrl
li
+renderSocialIcon('fa-facebook', 'Facebook', site.facebookUrl)
if site.instagramUrl
li
+renderSocialIcon('fa-instagram', 'Instagram', site.instagramUrl)
if site.bitchuteUrl
li
a(href= site.bitchuteUrl).dtp-social-link
span
img(src="/img/social-icons/bitchute.svg", style="width: auto; height: 1em;")
span.uk-margin-small-left BitChute
if site.odyseeUrl
li
a(href= site.odyseeUrl).dtp-social-link
span
img(src="/img/social-icons/odysee.svg", style="width: auto; height: 1em;")
span.uk-margin-small-left Odysee
if site.rumbleUrl
li
a(href= site.odyseeUrl).dtp-social-link
span
img(src="/img/social-icons/rumble.svg", style="width: auto; height: 1em;")
span.uk-margin-small-left Rumble
if site.twitchUrl
li
+renderSocialIcon('fa-twitch', 'Twitch', site.twitchUrl)
if site.youtubeUrl
li
+renderSocialIcon('fa-youtube', 'YouTube', site.youtubeUrl)
if site.dliveUrl
li
a(href= site.dliveUrl).dtp-social-link
span
img(src="/img/social-icons/dlive.svg", style="width: auto; height: 1em;")
span.uk-margin-small-left DLive
.uk-width-medium.uk-margin-auto .uk-width-medium.uk-margin-auto
hr hr
div Copyright &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) +renderSidebarEpisode(episode)
//- Newsletter Signup //- 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') +renderSectionTitle('Mailing List')
.uk-margin .uk-margin
form(method="post", action="/newsletter", onsubmit="return dtp.app.submitForm(event, 'Subscribe to newsletter');").uk-form 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 block facebook-card
meta(property='og:site_name', content= site.name) meta(property='og:site_name', content= site.name)
meta(property='og:type', content='website') meta(property='og:type', content='website')
meta(property='og:image', content= `https://${site.domain}/img/social-cards/${site.domain}.png?v=${pkg.version}`) meta(property='og:image', content= `https://${site.domain}/img/social-cards/${site.domainKey}.png?v=${pkg.version}`)
meta(property='og:url', content= `https://${site.domain}${dtp.request.url}`) meta(property='og:url', content= `https://${site.domain}${dtp.request.url}`)
meta(property='og:title', content= `${site.name} | ${site.description}`) meta(property='og:title', content= pageTitle || site.name)
meta(property='og:description', content= site.description) meta(property='og:description', content= pageDescription || site.description)
meta(property='og:image:alt', content= `${site.name} | ${site.description}`) meta(property='og:image:alt', content= `${site.name} | ${site.description}`)

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

@ -1,5 +1,5 @@
block twitter-card block twitter-card
meta(name='twitter:card', content='summary_large_image') meta(name='twitter:card', content='summary_large_image')
meta(name='twitter:image' content= `https://${site.domain}/img/social-cards/${site.domain}.png?v=${pkg.version}`) meta(name='twitter:image' content= `https://${site.domain}/img/social-cards/${site.domainKey}.png?v=${pkg.version}`)
meta(name='twitter:title', content= `${site.name} | ${site.description}`) meta(name='twitter:title', content= pageTitle || site.name)
meta(name='twitter:description', content= site.description) meta(name='twitter:description', content= pageDescription || site.description)

5
app/views/error.pug

@ -3,9 +3,12 @@ block content
section.uk-section.uk-section-default.uk-section-xsmall section.uk-section.uk-section-default.uk-section-xsmall
.uk-container .uk-container
h1 Well, That's Not Right!
.uk-text-large= message .uk-text-large= message
//- if error.stack //- if error.stack
//- pre= error.stack //- pre= error.stack
if error && error.status if error && error.status
div.uk-text-small.uk-text-muted status:#{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 a(href="/").uk-button.uk-button-default.uk-border-rounded
span.uk-margin-small-right span.uk-margin-small-right
i.fas.fa-home 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), 'uk-flex-last': ((postIndex % postIndexModulus) === 0),
}) })
article.uk-article article.uk-article
h4(style="line-height: 1;").uk-article-title.uk-margin-remove= post.title h4(style="line-height: 1.1;").uk-article-title.uk-margin-small= post.title
.uk-text-truncate= post.summary
.uk-article-meta .uk-article-meta
div(uk-grid).uk-grid-small if post.updated
.uk-width-auto published: #{moment(post.created).format("MMM DD YYYY HH:MM a")} span updated: #{moment(post.updated).format("MMM DD YYYY HH:MM a")}
if post.updated else
.uk-width-auto updated: #{moment(post.updated).format("MMM DD YYYY HH:MM a")} span published: #{moment(post.created).format("MMM DD YYYY HH:MM a")}
+renderSectionTitle('Featured') +renderSectionTitle('Featured')
.uk-margin .uk-margin
div(style="position: relative; overflow: hidden; width: 100%; padding-top: 56.25%") div(style="position: relative; overflow: hidden; width: 100%; padding-top: 56.25%")
iframe( iframe(
@ -47,7 +47,7 @@ block content
ul.uk-list.uk-list-divider.uk-list-small ul.uk-list.uk-list-divider.uk-list-small
each post in posts each post in posts
li li
+renderBlogPostListItem(post, postIndex, 2) +renderBlogPostListItem(post, postIndex, 4)
- postIndex += 1; - postIndex += 1;
else else
div There are no posts at this time. Please check back later! 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 extends main
block content-container block content-container
section.uk-section.uk-section-default section.uk-section.uk-section-default
.uk-container .uk-container
@ -7,7 +6,4 @@ block content-container
div(class="uk-width-1-1 uk-width-2-3@m") div(class="uk-width-1-1 uk-width-2-3@m")
block content block content
div(class="uk-width-1-1 uk-width-1-3@m") div(class="uk-width-1-1 uk-width-1-3@m")
+renderPageSidebar() +renderPageSidebar()
block page-footer
include ../components/page-footer

5
app/views/layouts/main.pug

@ -59,8 +59,9 @@ html(lang='en')
block content-container block content-container
block content block content
block page-footer
include ../components/page-footer block page-footer
include ../components/page-footer
block dtp-navbar block dtp-navbar
include ../components/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 .uk-margin
div(uk-grid) div(uk-grid)
.uk-width-expand .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 if user && user.flags.isAdmin
.uk-width-auto .uk-width-auto
a(href=`/admin/page/${page._id}`).uk-button.dtp-button-text EDIT 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 extends ../layouts/main-sidebar
block content block content
include ../comment/components/comment include ../comment/components/comment-list
include ../comment/components/composer
article(dtp-post-id= post._id) article(dtp-post-id= post._id)
.uk-margin .uk-margin
@ -37,43 +38,16 @@ block content
+renderSectionTitle('Add a comment') +renderSectionTitle('Add a comment')
.uk-margin .uk-margin
form(method="POST", action=`/post/${post._id}/comment`, onsubmit="return dtp.app.submitForm(event, 'create-comment');").uk-form +renderCommentComposer(`/post/${post._id}/comment`)
.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')
.uk-margin if featuredComment
if Array.isArray(comments) && (comments.length > 0) #featured-comment.uk-margin-large
ul#post-comment-list.uk-list .uk-margin
each comment in comments +renderSectionTitle('Linked Comment')
+renderComment(comment) +renderComment(featuredComment)
else
ul#post-comment-list.uk-list .uk-margin
div There are no comments at this time. Please check back later. +renderSectionTitle('Comments')
.uk-margin
+renderCommentList(comments)

21
app/views/user/profile.pug

@ -1,9 +1,26 @@
extends ../layouts/main extends ../layouts/main
block content block content
include ../comment/components/comment
section.uk-section.uk-section-default section.uk-section.uk-section-default
.uk-container .uk-container
h1= user.displayName || user.username || user.email h1= user.displayName || user.username || user.email
p Viewers do not have public profiles on #[+renderSiteLink()]. You must be logged in to view your profile. Only you can view your profile. p People don't have public profiles on #[+renderSiteLink()]. You must be logged in to view your profile. And, only you can view your profile.
p Your profile is where you edit your account settings, configure your commenting defaults, and otherwise manage how you use #[+renderSiteLink()].
section.uk-section.uk-section-default
.uk-container
.uk-margin
+renderSectionTitle('Comment History')
p Your profile is where you manage your channel subscriptions, edit account settings, configure your chat defaults, and otherwise manage how you use #[+renderSiteLink()]. if Array.isArray(commentHistory) && (commentHistory.length > 0)
ul.uk-list.uk-list-divider
each comment in commentHistory
li
.uk-margin-small
.uk-text-small commenting on #[a(href=`/post/${comment.resource.slug}?comment=${comment._id}#featured-comment`)= comment.resource.title]
+renderComment(comment)
else
div You haven't written any comments on posts.

22
app/views/user/settings.pug

@ -14,11 +14,21 @@ block content
div(uk-grid) div(uk-grid)
div(class="uk-width-1-1 uk-width-1-3@m") div(class="uk-width-1-1 uk-width-1-3@m")
- -
var currentImage = null; var currentProfile = null;
if (user.picture && user.picture.large) { 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") 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 form(method="POST", action=`/user/${user._id}/settings`, onsubmit="return dtp.app.submitForm(event, 'user account update');").uk-form
.uk-margin .uk-margin
@ -28,4 +38,8 @@ block content
label(for="display-name").uk-form-label Display Name 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 input(id="display-name", name="displayName", type="text", placeholder="Enter display name", value= user.displayName).uk-input
.uk-margin .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 section.uk-section.uk-section-secondary
.uk-container.uk-text-center .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-text-lead= site.description
.uk-margin-medium-top .uk-margin-medium-top
@ -11,4 +14,4 @@ block content
.uk-width-auto .uk-width-auto
a(href="/welcome/signup").uk-button.dtp-button-primary Create Account a(href="/welcome/signup").uk-button.dtp-button-primary Create Account
.uk-width-auto .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 block content
form(method="POST", action="/user").uk-form 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 .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. 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 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 input(id="captcha", name="captcha", type="text", placeholder="Enter captcha text").uk-input.uk-text-center
.uk-margin-small .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'; 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 { export default class DtpSiteApp extends DtpApp {
constructor (user) { constructor (user) {
@ -39,6 +47,21 @@ export default class DtpSiteApp extends DtpApp {
if (this.chat.input) { if (this.chat.input) {
this.chat.input.addEventListener('keydown', this.onChatInputKeyDown.bind(this)); 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 ( ) { async connect ( ) {
@ -151,7 +174,7 @@ export default class DtpSiteApp extends DtpApp {
} }
return false; return false;
} }
async submitForm (event, userAction) { async submitForm (event, userAction) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
@ -184,6 +207,51 @@ export default class DtpSiteApp extends DtpApp {
return; 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) { async copyHtmlToText (event, textContentId) {
const content = this.editor.getContent({ format: 'text' }); const content = this.editor.getContent({ format: 'text' });
const text = document.getElementById(textContentId); const text = document.getElementById(textContentId);
@ -196,7 +264,10 @@ export default class DtpSiteApp extends DtpApp {
const imageId = event.target.getAttribute('data-image-id'); const imageId = event.target.getAttribute('data-image-id');
//z read the cropper options from the element on the page //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 this.log.debug('selectImageFile', 'cropper options', { cropperOptions }); //z remove when done
const fileSelectContainerId = event.target.getAttribute('data-file-select-container'); const fileSelectContainerId = event.target.getAttribute('data-file-select-container');
@ -238,20 +309,11 @@ export default class DtpSiteApp extends DtpApp {
return; 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(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
const img = document.getElementById(imageId); const img = document.getElementById(imageId);
img.onload = (e) => { img.onload = (e) => {
console.log('image loaded', e, img.naturalWidth, img.naturalHeight); 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-name').textContent = selectedFile.name;
fileSelectContainer.querySelector('#file-modified').textContent = moment(selectedFile.lastModifiedDate).fromNow(); fileSelectContainer.querySelector('#file-modified').textContent = moment(selectedFile.lastModifiedDate).fromNow();
@ -268,16 +330,15 @@ export default class DtpSiteApp extends DtpApp {
img.src = e.target.result; img.src = e.target.result;
//z create cropper and set options here //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. // read in the file, which will trigger everything else in the event handler above.
reader.readAsDataURL(selectedFile); reader.readAsDataURL(selectedFile);
} }
async createImageCropper (img) { async createImageCropper (img, options) {
this.log.info("createImageCropper", "Creating image cropper", { img }); options = Object.assign({
this.cropper = new Cropper(img, {
aspectRatio: 1, aspectRatio: 1,
dragMode: 'move', dragMode: 'move',
autoCropArea: 0.85, autoCropArea: 0.85,
@ -285,11 +346,13 @@ export default class DtpSiteApp extends DtpApp {
guides: false, guides: false,
center: false, center: false,
highlight: false, highlight: false,
cropBoxMovable: false, cropBoxMovable: true,
cropBoxResizable: false, cropBoxResizable: true,
toggleDragModeOnDblclick: false, toggleDragModeOnDblclick: false,
modal: true, modal: true,
}); }, options);
this.log.info("createImageCropper", "Creating image cropper", { img });
this.cropper = new Cropper(img, options);
} }
async attachTinyMCE (editor) { async attachTinyMCE (editor) {
@ -458,11 +521,8 @@ export default class DtpSiteApp extends DtpApp {
let response; let response;
switch (imageType) { switch (imageType) {
case 'channel-app-icon': case 'profile-picture-file':
const channelId = (event.target || event.currentTarget).getAttribute('data-channel-id'); response = await fetch(`/user/${this.user._id}/profile-photo`, { method: 'DELETE' });
response = await fetch(`/channel/${channelId}/app-icon`, {
method: 'DELETE',
});
break; break;
default: default:
@ -474,11 +534,11 @@ export default class DtpSiteApp extends DtpApp {
} }
await this.processResponse(response); await this.processResponse(response);
window.location.reload();
} catch (error) { } catch (error) {
UIkit.modal.alert(`Failed to remove image: ${error.message}`); UIkit.modal.alert(`Failed to remove image: ${error.message}`);
} }
} }
async onCommentInput (event) { async onCommentInput (event) {
const label = document.getElementById('comment-character-count'); const label = document.getElementById('comment-character-count');
label.textContent = numeral(event.target.value.length).format('0,0'); label.textContent = numeral(event.target.value.length).format('0,0');
@ -494,6 +554,193 @@ export default class DtpSiteApp extends DtpApp {
async onEmojiSelected (selection) { async onEmojiSelected (selection) {
this.emojiTargetElement.value += selection.emoji; 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; 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 { .share-button {
background: #00d178; background: #00d178;
color: white; 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 { .dtp-dashboard-cluster {
canvas.visit-graph {
width: 960px;
height: 360px;
}
fieldset { fieldset {
border-color: #9e9e9e; border-color: #9e9e9e;
color: #c8c8c8; color: #c8c8c8;
@ -78,4 +83,4 @@
} }
} }
} }
} }

6
client/less/site/image.less

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

1
client/less/style.less

@ -5,6 +5,7 @@
@import "site/main.less"; @import "site/main.less";
@import "site/border.less"; @import "site/border.less";
@import "site/comment.less";
@import "site/image.less"; @import "site/image.less";
@import "site/figure.less"; @import "site/figure.less";
@import "site/header-section.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 * CryptoExchangeController
*/ */
@ -171,6 +195,11 @@ module.exports = {
expire: ONE_MINUTE, expire: ONE_MINUTE,
message: 'You are creating accounts too quickly', message: 'You are creating accounts too quickly',
}, },
postProfilePhoto: {
total: 5,
expire: ONE_MINUTE * 5,
message: 'You are updating your profile photo too quickly',
},
postUpdateSettings: { postUpdateSettings: {
total: 4, total: 4,
expire: ONE_MINUTE, expire: ONE_MINUTE,
@ -186,6 +215,11 @@ module.exports = {
expire: ONE_MINUTE, expire: ONE_MINUTE,
message: 'You are requesting user profiles too quickly', 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: { welcome: {
@ -193,4 +227,4 @@ module.exports = {
expire: ONE_MINUTE, expire: ONE_MINUTE,
message: 'You are loading these pages too quickly', 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) { 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) { async loadChild (filename) {
this.log.info('loading child controller', { script: filename });
let child = await require(filename)(this.dtp); let child = await require(filename)(this.dtp);
return await child.start(); return await child.start();
} }
@ -38,10 +37,15 @@ class SiteController extends SiteCommon {
return pagination; return pagination;
} }
createDisplayList (name) {
const { displayEngine: displayEngineService } = this.dtp.services;
return displayEngineService.createDisplayList(name);
}
async createCsrfToken (req, name) { async createCsrfToken (req, name) {
const { csrfToken } = this.dtp.platform.services; const { csrfToken } = this.dtp.platform.services;
return csrfToken.create(req, { name }); 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 moment = require('moment');
const rfs = require('rotating-file-stream'); const rfs = require('rotating-file-stream');
const color = require('ansicolor');
var LogModel, LogStream; var LogModel, LogStream;
if (process.env.DTP_LOG_FILE === 'enabled') { if (process.env.DTP_LOG_FILE === 'enabled') {
@ -74,16 +76,47 @@ class SiteLog {
async writeLog (level, message, metadata) { async writeLog (level, message, metadata) {
const NOW = new Date(); const NOW = new Date();
const { componentName } = this; const { componentName } = this;
if (process.env.DTP_LOG_CONSOLE === 'enabled') { 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) { 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 { } 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')) { if (LogModel && (process.env.DTP_LOG_MONGODB === 'enabled')) {
await LogModel.create({ created: NOW, level, componentName, message, metadata }); await LogModel.create({ created: NOW, level, componentName, message, metadata });
} }
if (LogStream && (process.env.DTP_LOG_FILE === 'enabled')) { if (LogStream && (process.env.DTP_LOG_FILE === 'enabled')) {
const logEntry = { const logEntry = {
t: NOW, c: componentName, l: level, m: message, d: metadata, 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