Browse Source

Merge branch 'develop' of git.digitaltelepresence.com:digital-telepresence/dtp-base into develop

develop
Rob Colbert 2 years ago
parent
commit
942fd8316d
  1. 1
      .vscode/launch.json
  2. 1
      app/controllers/admin.js
  3. 164
      app/controllers/admin/newsroom.js
  4. 9
      app/controllers/home.js
  5. 85
      app/controllers/newsroom.js
  6. 26
      app/models/feed-entry.js
  7. 21
      app/models/feed.js
  8. 148
      app/services/feed.js
  9. 7
      app/views/admin/components/menu.pug
  10. 39
      app/views/admin/newsroom/editor.pug
  11. 16
      app/views/admin/newsroom/index.pug
  12. 24
      app/views/components/library.pug
  13. 4
      app/views/components/off-canvas.pug
  14. 20
      app/views/components/page-sidebar.pug
  15. 2
      app/views/components/social-card/facebook.pug
  16. 9
      app/views/newsroom/components/feed-entry-list-item.pug
  17. 35
      app/views/newsroom/feed-view.pug
  18. 19
      app/views/newsroom/index.pug
  19. 60
      app/workers/newsroom.js
  20. 85
      app/workers/newsroom/cron/update-feeds.js
  21. 2
      app/workers/reeeper.js
  22. 23
      client/js/site-admin-app.js
  23. 22
      config/limiter.js
  24. 12
      lib/site-platform.js
  25. 1
      package.json
  26. 31
      yarn.lock

1
.vscode/launch.json

@ -15,6 +15,7 @@
"console": "integratedTerminal",
"env": {
"HTTP_BIND_PORT": "3020",
"HTTPS_ENABLE": "enabled",
"HTTPS_BIND_PORT": "3420"
}
},

1
app/controllers/admin.js

@ -50,6 +50,7 @@ class AdminController extends SiteController {
router.use('/job-queue', await this.loadChild(path.join(__dirname, 'admin', 'job-queue')));
router.use('/log', await this.loadChild(path.join(__dirname, 'admin', 'log')));
router.use('/newsletter', await this.loadChild(path.join(__dirname, 'admin', 'newsletter')));
router.use('/newsroom', await this.loadChild(path.join(__dirname, 'admin', 'newsroom')));
router.use('/page', await this.loadChild(path.join(__dirname, 'admin', 'page')));
router.use('/post', await this.loadChild(path.join(__dirname, 'admin', 'post')));
router.use('/settings', await this.loadChild(path.join(__dirname, 'admin', 'settings')));

164
app/controllers/admin/newsroom.js

@ -0,0 +1,164 @@
// admin/newsroom.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const express = require('express');
const { SiteController, SiteError } = require('../../../lib/site-lib');
class NewsroomAdminController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
}
async start ( ) {
const upload = this.createMulter('newsroom-admin');
const router = express.Router();
router.use(async (req, res, next) => {
res.locals.currentView = 'admin';
res.locals.adminView = 'newsroom';
return next();
});
router.param('feedId', this.populateFeedId.bind(this));
router.param('feedEntryId', this.populateFeedEntryId.bind(this));
router.post('/resolve', upload.none(), this.postResolveFeed.bind(this));
router.post('/:feedId', upload.none(), this.postUpdateFeed.bind(this));
router.post('/', upload.none(), this.postCreateFeed.bind(this));
router.get('/create', this.getFeedEditor.bind(this));
router.get('/:feedId', this.getFeedEditor.bind(this));
router.get('/', this.getHomeView.bind(this));
router.delete('/:feedId', this.deleteFeed.bind(this));
return router;
}
async populateFeedId (req, res, next, feedId) {
const { feed: feedService } = this.dtp.services;
try {
res.locals.feed = await feedService.getById(feedId);
if (!res.locals.feed) {
throw new SiteError(404, 'Feed not found');
}
return next();
} catch (error) {
this.log.error('failed to populate feedId', { feedId, error });
return next(error);
}
}
async populateFeedEntryId (req, res, next, feedEntryId) {
const { feed: feedService } = this.dtp.services;
try {
res.locals.feedEntry = await feedService.getEntryById(feedEntryId);
if (!res.locals.feedEntry) {
throw new SiteError(404, 'Feed entry not found');
}
return next();
} catch (error) {
this.log.error('failed to populate feed entry', { feedEntryId, error });
return next(error);
}
}
async postResolveFeed (req, res) {
const { feed: feedService } = this.dtp.services;
try {
const feed = await feedService.load(req.body.feedUrl);
this.log.info('request body', { body: req.body, feed });
const displayList = this.createDisplayList('resolve-feed');
displayList.setInputValue(`input#title`, feed.title);
displayList.setInputValue(`input#link`, feed.link);
displayList.setInputValue(`textarea#description`, feed.description);
if (feed.generator) {
displayList.setInputValue(`input[type="hidden"][name="generator"]`, feed.generator);
}
if (feed.language) {
displayList.setInputValue(`input[type="hidden"][name="language"]`, feed.language);
}
if (feed.published) {
displayList.setInputValue(`input[type="hidden"][name="published"]`, feed.published);
}
res.status(200).json({ success: true, displayList });
} catch (error) {
this.log.error('failed to present the Newsroom Admin home', { error });
return res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
async postUpdateFeed (req, res, next) {
const { feed: feedService } = this.dtp.services;
try {
await feedService.update(res.locals.feed, req.body);
res.redirect('/admin/newsroom');
} catch (error) {
this.log.error('failed to create feed', { error });
return next(error);
}
}
async postCreateFeed (req, res, next) {
const { feed: feedService } = this.dtp.services;
try {
res.locals.feed = await feedService.create(req.body);
res.redirect(`/admin/newsroom/${res.locals.feed._id}`);
} catch (error) {
this.log.error('failed to create feed', { error });
return next(error);
}
}
async getFeedEditor (req, res) {
res.render('admin/newsroom/editor');
}
async getHomeView (req, res, next) {
const { feed: feedService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.newsroom = await feedService.getFeeds(res.locals.pagination);
res.render('admin/newsroom/index');
} catch (error) {
this.log.error('failed to present the Newsroom Admin home', { error });
return next(error);
}
}
async deleteFeed (req, res) {
const { feed: feedService } = this.dtp.services;
try {
await feedService.remove(res.locals.feed);
const displayList = this.createDisplayList('delete-feed');
displayList.navigateTo('/admin/newsroom');
res.status(200).json({ success: true, displayList });
} catch (error) {
this.log.error('failed to remove feed', { error });
return res.status(error.statusCode || 500).json({
success: false,
message: error.message,
});
}
}
}
module.exports = {
name: 'newsroomAdmin',
slug: 'newsroom-admin',
create: async (dtp) => { return new NewsroomAdminController(dtp); },
};

9
app/controllers/home.js

@ -66,7 +66,12 @@ class HomeController extends SiteController {
}
async getHome (req, res, next) {
const { announcement: announcementService, hive: hiveService, post: postService } = this.dtp.services;
const {
announcement: announcementService,
feed: feedService,
hive: hiveService,
post: postService,
} = this.dtp.services;
try {
res.locals.announcements = await announcementService.getLatest(req.user);
res.locals.featuredPosts = await postService.getFeaturedPosts(3);
@ -75,6 +80,8 @@ class HomeController extends SiteController {
res.locals.pagination = this.getPaginationParameters(req, 20);
res.locals.posts = await postService.getPosts(res.locals.pagination);
res.locals.newsfeed = await feedService.getNewsfeed();
res.render('index');
} catch (error) {
this.log.error('failed to render home view', { error });

85
app/controllers/newsroom.js

@ -0,0 +1,85 @@
// newsroom.js
// Copyright (C) 2021 Digital Telepresence, LLC
// License: Apache-2.0
'use strict';
const express = require('express');
const { SiteController, SiteError } = require('../../lib/site-lib');
class NewsroomController extends SiteController {
constructor (dtp) {
super(dtp, module.exports);
}
async start ( ) {
const { dtp } = this;
const { limiter: limiterService } = dtp.services;
const router = express.Router();
dtp.app.use('/newsroom', router);
router.use(async (req, res, next) => {
res.locals.currentView = module.exports.slug;
return next();
});
router.param('feedId', this.populateFeedId.bind(this));
router.get('/:feedId',
limiterService.createMiddleware(limiterService.config.newsroom.getFeedView),
this.getFeedView.bind(this),
);
router.get('/',
limiterService.createMiddleware(limiterService.config.newsletter.getIndex),
this.getHome.bind(this),
);
}
async populateFeedId (req, res, next, feedId) {
const { feed: feedService } = this.dtp.services;
try {
res.locals.feed = await feedService.getById(feedId);
if (!res.locals.feed) {
throw new SiteError(404, 'Feed not found');
}
return next();
} catch (error) {
this.log.error('failed to populate feedId', { feedId, error });
return next(error);
}
}
async getFeedView (req, res, next) {
const { feed: feedService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 10);
res.locals.newsroom = await feedService.getFeedEntries(res.locals.feed, res.locals.pagination);
res.render('newsroom/feed-view');
} catch (error) {
this.log.error('failed to present newsroom home', { error });
return next(error);
}
}
async getHome (req, res, next) {
const { feed: feedService } = this.dtp.services;
try {
res.locals.pagination = this.getPaginationParameters(req, 10);
res.locals.newsroom = await feedService.getFeeds(res.locals.pagination);
res.render('newsroom/index');
} catch (error) {
this.log.error('failed to present newsroom home', { error });
return next(error);
}
}
}
module.exports = {
slug: 'newsroom',
name: 'newsroom',
create: (dtp) => { return new NewsroomController(dtp); },
};

26
app/models/feed-entry.js

@ -0,0 +1,26 @@
// feed-entry.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const FeedEntrySchema = new Schema({
feed: { type: Schema.ObjectId, required: true, index: 1, ref: 'Feed' },
published: { type: Date },
title: { type: String },
description: { type: String },
link: { type: String, index: 1 },
});
FeedEntrySchema.index({
feed: 1,
link: 1,
}, {
name: 'feed_entry_by_feed_idx',
});
module.exports = mongoose.model('FeedEntry', FeedEntrySchema);

21
app/models/feed.js

@ -0,0 +1,21 @@
// feed.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const FeedSchema = new Schema({
url: { type: String, required: true, unique: true },
title: { type: String },
link: { type: String },
description: { type: String },
language: { type: String },
generator: { type: String },
published: { type: Date },
});
module.exports = mongoose.model('Feed', FeedSchema);

148
app/services/feed.js

@ -0,0 +1,148 @@
// announcement.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const mongoose = require('mongoose');
const Feed = mongoose.model('Feed');
const FeedEntry = mongoose.model('FeedEntry');
const { SiteService, SiteError } = require('../../lib/site-lib');
const { read: feedReader } = require('feed-reader');
class FeedService extends SiteService {
constructor (dtp) {
super(dtp, module.exports);
this.populateFeedEntry = [
{
path: 'feed',
},
];
}
async create (feedDefinition) {
feedDefinition.url = feedDefinition.url.trim();
const feedContent = await this.load(feedDefinition.url);
if (!feedContent) {
throw new SiteError(404, 'Feed failed to load');
}
const feed = new Feed();
feed.url = feedDefinition.url;
feed.title = feedDefinition.title || feedContent.title || 'New Feed';
feed.link = feedDefinition.link || feedContent.link;
feed.description = feedDefinition.description || feedContent.description;
feed.language = feedContent.language;
feed.generator = feedContent.generator;
feed.published = feedContent.published;
await feed.save();
return feed.toObject();
}
async update (feed, feedDefinition) {
feedDefinition.url = feedDefinition.url.trim();
const feedContent = await this.load(feedDefinition.url);
if (!feedContent) {
throw new SiteError(404, 'Feed failed to load');
}
const updateOp = { $set: { }, $unset: { } };
updateOp.$set.url = feedDefinition.url;
updateOp.$set.title = feedDefinition.title || feedContent.title || 'New Feed';
updateOp.$set.link = feedDefinition.link || feedContent.link;
updateOp.$set.description = feedDefinition.description || feedContent.description;
updateOp.$set.language = feedDefinition.language || feedContent.language;
updateOp.$set.generator = feedDefinition.generator || feedContent.generator;
await Feed.updateOne({ _id: feed._id }, updateOp);
}
async getFeeds (pagination) {
pagination = Object.assign({ skip: 0, cpp: 10 }, pagination);
const feeds = await Feed
.find()
.sort({ title: 1 })
.skip(pagination.skip)
.limit(pagination.cpp)
.lean();
const totalFeedCount = await Feed.countDocuments();
return { feeds, totalFeedCount };
}
async getById (feedId) {
const feed = await Feed.findOne({ _id: feedId }).lean();
return feed;
}
async getFeedEntries (feed, pagination) {
pagination = Object.assign({ skip: 0, cpp: 10 }, pagination);
const entries = await FeedEntry
.find({ feed: feed._id })
.sort({ published: -1 })
.skip(pagination.skip)
.limit(pagination.cpp)
.populate(this.populateFeedEntry)
.lean();
const totalFeedEntryCount = await FeedEntry.countDocuments({ feed: feed._id });
return { entries, totalFeedEntryCount };
}
async getNewsfeed (pagination) {
pagination = Object.assign({ skip: 0, cpp: 5 }, pagination);
const entries = await FeedEntry
.find()
.sort({ published: -1 })
.skip(pagination.skip)
.limit(pagination.cpp)
.populate(this.populateFeedEntry)
.lean();
const totalFeedEntryCount = await FeedEntry.estimatedDocumentCount();
return { entries, totalFeedEntryCount };
}
async remove (feed) {
this.log.info('removing all feed entries', { feedId: feed._id, title: feed.title });
await FeedEntry.deleteMany({ feed: feed._id });
this.log.info('removing feed', { feedId: feed._id, title: feed.title });
await Feed.deleteOne({ _id: feed._id });
}
async load (url) {
const response = await feedReader(url);
return response;
}
async createEntry (feed, entryDefinition) {
const NOW = new Date();
const updateOp = { $setOnInsert: { }, $set: { }, $unset: { } };
updateOp.$setOnInsert.feed = feed._id;
updateOp.$setOnInsert.link = entryDefinition.link.trim();
updateOp.$setOnInsert.published = new Date(entryDefinition.published || NOW);
updateOp.$set.title = entryDefinition.title.trim();
updateOp.$set.description = entryDefinition.description.trim();
await FeedEntry.updateOne(
{
feed: feed._id,
link: updateOp.$setOnInsert.link,
},
updateOp,
{ upsert: true },
);
}
}
module.exports = {
slug: 'feed',
name: 'feed',
create: (dtp) => { return new FeedService(dtp); },
};

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

@ -30,6 +30,13 @@ ul(uk-nav).uk-nav-default
span.nav-item-icon
i.fas.fa-file
span.uk-margin-small-left Pages
li(class={ 'uk-active': (adminView === 'newsroom') })
a(href="/admin/newsroom")
span.nav-item-icon
i.fas.fa-newspaper
span.uk-margin-small-left Newsroom
li(class={ 'uk-active': (adminView === 'newsletter') }).uk-parent
a
span.nav-item-icon

39
app/views/admin/newsroom/editor.pug

@ -0,0 +1,39 @@
extends ../layouts/main
block content
- var formActionUrl = feed ? `/admin/newsroom/${feed._id}` : '/admin/newsroom';
form#add-feed-form(method="POST", action= formActionUrl).uk-form
input(type="hidden", name="generator", value="DTP News")
input(type="hidden", name="language", value="en")
input(type="hidden", name="published")
.uk-card.uk-card-default.uk-card-small
.uk-card-header
h1.uk-card-title= feed ? 'Update Feed' : 'Add Feed'
.uk-card-body
.uk-margin
label(for="url").uk-form-label Feed URL
input(id="url", name="url", type="url", placeholder="Enter feed URL", value= feed ? feed.url : undefined).uk-input
.uk-margin-small
button(type="button", onclick="return dtp.adminApp.resolveNewsroomFeed(event);").uk-button.uk-button-default.uk-button-small.uk-border-rounded Resolve Feed
span.uk-margin-small-left to load the following information automatically.
.uk-margin
label(for="title").uk-form-label Title
input(id="title", name="title", type="text", placeholder="Enter feed title", value= feed ? feed.title : undefined).uk-input
.uk-margin
label(for="link").uk-form-label Link
input(id="link", name="link", type="url", placeholder="Enter feed website link", value= feed ? feed.link : undefined).uk-input
.uk-margin
label(for="description").uk-form-label Description
textarea(id="description", name="description", rows="4", placeholder="Enter feed description").uk-textarea.uk-resize-vertical= feed ? feed.description : undefined
.uk-card-footer
button(type="submit").uk-button.uk-button-primary.uk-border-rounded= feed ? 'Update Feed' : 'Add Feed'
if feed
pre= JSON.stringify(feed, null, 2)

16
app/views/admin/newsroom/index.pug

@ -0,0 +1,16 @@
extends ../layouts/main
block content
div(uk-grid)
.uk-width-expand
h1 Newsroom Feeds
.uk-width-auto
a(href='/admin/newsroom/create').uk-button.uk-button-primary #[i.fas.fa-plus]#[span.uk-margin-small-left Add Feed]
if Array.isArray(newsroom.feeds) && (newsroom.feeds.length > 0)
ul.uk-list.uk-list-divider
each feed in newsroom.feeds
li
a(href=`/admin/newsroom/${feed._id}`)= feed.title
else
div There are no feeds.

24
app/views/components/library.pug

@ -10,6 +10,30 @@ include section-title
return (value < 1000) ? numeral(value).format('0,0') : numeral(value).format('0,0.0a');
}
function formatBitRate (value) {
if ((value !== 0) && !value) { return '---'; }
return numeral(value).format('0,0.0ib').slice(0, -2) + 'bps';
}
function formatDataSize (value) {
if ((value !== 0) && !value) { return '---'; }
return numeral(value).format('0,0.0b');
}
function formatTimestamp (value, withFractional = false) {
if (value !== 0 && !value) {
return withFractional ? '-:--:--.---' : '-:--:--';
}
return numeral(value).format(withFractional ? 'h:mm:ss.sss' : 'h:mm:ss');
}
function formatDuration (value) {
let duration = formatTimestamp(value);
if (duration.startsWith('0:')) { duration = duration.slice(2); }
if (duration.startsWith('0')) { duration = duration.slice(1); }
return duration;
}
function displayIntegerValue (value) {
return numeral(value).format(value > 1000 ? '0,0.0a' : '0,0');
}

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

@ -34,6 +34,10 @@ mixin renderMenuItem (iconClass, label)
a(href='/announcement').uk-display-block
+renderMenuItem('fa-bullhorn', 'Announcements')
li(class={ "uk-active": (currentView === 'newsroom') })
a(href='/newsroom').uk-display-block
+renderMenuItem('fa-newspaper', 'Newsroom')
if user
li.uk-nav-header Member Menu

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

@ -1,4 +1,5 @@
include ../announcement/components/announcement
include ../newsroom/components/feed-entry-list-item
- var isLive = !!shingChannelStatus && shingChannelStatus.isLive && !!shingChannelStatus.liveEpisode;
@ -58,6 +59,9 @@ mixin renderPageSidebar ( )
div Started: #{moment(shingChannelStatus.liveEpisode.created).fromNow()}
.uk-width-auto #[i.fas.fa-eye] #{formatCount(shingChannelStatus.liveEpisode.stats.currentViewerCount)}
//-
//- Shing.tv Channel Feed
//-
if shingChannelFeed && Array.isArray(shingChannelFeed.items) && (shingChannelFeed.items.length > 0)
.uk-margin
+renderSectionTitle(shingChannelFeed.title, {
@ -91,6 +95,22 @@ mixin renderPageSidebar ( )
li
+renderSidebarEpisode(episode)
//-
//- Newsroom Integration
//-
if newsfeed
.uk-margin
+renderSectionTitle('Newsfeed', {
label: 'See All',
title: 'Browse all news feeds',
url: '/newsroom',
})
if Array.isArray(newsfeed.entries) && (newsfeed.entries.length > 0)
ul.uk-list
each entry in newsfeed.entries
li
+renderNewsroomFeedEntryListItem(entry)
//-
//- Newsletter Signup
//-

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

@ -2,7 +2,7 @@ block facebook-card
meta(property='og:site_name', content= site.name)
meta(property='og:type', content='website')
meta(property='og:image', content= `https://${site.domain}/img/social-cards/${site.domainKey}.png?v=${pkg.version}`)
meta(property='og:url', content= `https://${site.domain}${dtp.request.url}`)
meta(property='og:url', content= `https://${site.domain}${request.url}`)
meta(property='og:title', content= pageTitle || site.name)
meta(property='og:description', content= pageDescription || site.description)
meta(property='og:image:alt', content= `${site.name} | ${site.description}`)

9
app/views/newsroom/components/feed-entry-list-item.pug

@ -0,0 +1,9 @@
mixin renderNewsroomFeedEntryListItem (entry)
.uk-text-bold
a(href= entry.link, target="_blank").uk-link-reset= entry.title
.uk-text-small
div(uk-grid).uk-grid-small
.uk-width-expand
a(href= entry.feed.link, target="_blank").uk-link-reset= entry.feed.title
.uk-width-auto
div= moment(entry.published).fromNow()

35
app/views/newsroom/feed-view.pug

@ -0,0 +1,35 @@
extends ../layouts/main
block content
include ../components/pagination-bar
section.uk-section.uk-section-default.uk-section-small
.uk-container
article.uk-article
.uk-margin-large
.uk-margin
h1.uk-article-title.uk-margin-remove= feed.title
.uk-article-meta
div(uk-grid)
.uk-width-auto
a(href= feed.link, target="_blank") #[i.fas.fa-external-link-alt]#[span.uk-margin-small-left Visit site]
.uk-width-auto
div last updated #{moment(feed.published).fromNow()}
.uk-text-lead= feed.description
.uk-margin-large
if Array.isArray(newsroom.entries) && (newsroom.entries.length > 0)
ul.uk-list.uk-list-divider
each entry in newsroom.entries
li
.uk-text-bold
a(href= entry.link, target="_blank")= entry.title
.uk-text-small= entry.description
//- pre= JSON.stringify(entry, null, 2)
else
div There are no news feed entries.
.uk-margin
+renderPaginationBar(`/newsroom/${feed._id}`, newsroom.totalFeedEntryCount)

19
app/views/newsroom/index.pug

@ -0,0 +1,19 @@
extends ../layouts/main
block content
section.uk-section.uk-section-default.uk-section-small
.uk-container
h1 #{site.name} Newsroom
if Array.isArray(newsroom.feeds) && (newsroom.feeds.length > 0)
div(uk-grid).uk-grid-match
each feed in newsroom.feeds
.uk-width-1-3
.uk-tile.uk-tile-secondary.uk-padding-small.uk-border-rounded
.uk-text-bold
a(href=`/newsroom/${feed._id}`)= feed.title
.uk-text-small.uk-text-muted
div last update #{moment(feed.published).fromNow()}
div= feed.description
else
div There are no configured news feeds.

60
app/workers/newsroom.js

@ -0,0 +1,60 @@
// newsroom.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const path = require('path');
require('dotenv').config({ path: path.resolve(__dirname, '..', '..', '.env') });
const {
SiteLog,
SiteWorker,
} = require(path.join(__dirname, '..', '..', 'lib', 'site-lib'));
module.rootPath = path.resolve(__dirname, '..', '..');
module.pkg = require(path.resolve(__dirname, '..', '..', 'package.json'));
module.config = {
environment: process.env.NODE_ENV,
root: module.rootPath,
component: { name: 'newsroom', slug: 'newsroom' },
};
module.config.site = require(path.join(module.rootPath, 'config', 'site'));
module.config.http = require(path.join(module.rootPath, 'config', 'http'));
class NewsroomWorker extends SiteWorker {
constructor (dtp) {
super(dtp, dtp.config.component);
}
async start ( ) {
await super.start();
await this.loadProcessor(path.join(__dirname, 'newsroom', 'cron', 'update-feeds.js'));
await this.startProcessors();
}
async stop ( ) {
await super.stop();
}
}
(async ( ) => {
try {
module.log = new SiteLog(module, module.config.component);
module.worker = new NewsroomWorker(module);
await module.worker.start();
module.log.info(`${module.pkg.name} v${module.pkg.version} ${module.config.component.name} started`);
} catch (error) {
module.log.error('failed to start worker', { component: module.config.component, error });
process.exit(-1);
}
})();

85
app/workers/newsroom/cron/update-feeds.js

@ -0,0 +1,85 @@
// newsroom/cron/update-feeds.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0
'use strict';
const path = require('path');
const mongoose = require('mongoose');
const Feed = mongoose.model('Feed');
const { CronJob } = require('cron');
const { read: feedReader } = require('feed-reader');
const { SiteAsync } = require('../../../../lib/site-lib');
const { SiteWorkerProcess } = require(path.join(__dirname, '..', '..', '..', '..', 'lib', 'site-lib'));
class UpdateFeedsCron extends SiteWorkerProcess {
static get COMPONENT ( ) {
return {
name: 'updateFeeds',
slug: 'update-feeds-cron',
};
}
constructor (worker) {
super(worker, UpdateFeedsCron.COMPONENT);
}
async start ( ) {
await super.start();
await this.updateFeeds();
this.job = new CronJob(
'0 */15 * * * *',
this.updateFeeds.bind(this),
null,
true,
process.env.DTP_CRON_TIMEZONE || 'America/New_York',
);
}
async stop ( ) {
if (this.job) {
this.log.info('stopping feed update job');
this.job.stop();
delete this.job;
}
await super.stop();
}
async updateFeeds ( ) {
try {
await Feed
.find()
.lean()
.cursor()
.eachAsync(async (feed) => {
await this.updateFeed(feed);
}, 4);
} catch (error) {
this.log.error('failed to update feeds', { error });
}
}
async updateFeed (feed) {
const NOW = new Date();
const { feed: feedService } = this.dtp.services;
try {
this.log.info('loading latest feed data', { feedId: feed._id, title: feed.title });
const response = await feedReader(feed.url);
await SiteAsync.each(response.entries, async (entry) => {
await Feed.updateOne({ _id: feed._id }, { $set: { published: feed.published || NOW }});
await feedService.createEntry(feed, entry);
}, 4);
this.log.info('feed updated', { entries: response.entries.length });
} catch (error) {
this.log.error('failed to update feed', { feedId: feed._id, title: feed.title, error });
}
}
}
module.exports = UpdateFeedsCron;

2
app/workers/reeeper.js

@ -1,4 +1,4 @@
// host-services.js
// reeeper.js
// Copyright (C) 2022 DTP Technologies, LLC
// License: Apache-2.0

23
client/js/site-admin-app.js

@ -344,6 +344,29 @@ export default class DtpSiteAdminHostStatsApp extends DtpApp {
UIkit.modal.alert(`Failed to delete Service Node: ${error.message}`);
}
}
async resolveNewsroomFeed (event) {
event.preventDefault();
event.stopPropagation();
const form = document.querySelector('form#add-feed-form');
const input = form.querySelector('input#url');
const feedUrl = input.value;
try {
const response = await fetch(`/admin/newsroom/resolve`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ feedUrl }),
});
await this.processResponse(response);
} catch (error) {
UIkit.modal.alert(`Failed to resolve feed: ${error.message}`);
}
return false;
}
}
dtp.DtpSiteAdminHostStatsApp = DtpSiteAdminHostStatsApp;

22
config/limiter.js

@ -117,6 +117,9 @@ module.exports = {
},
},
/*
* CommentController
*/
comment: {
deleteComment: {
total: 1,
@ -259,6 +262,22 @@ module.exports = {
*/
newsletter: {
getView: {
total: 15,
expire: ONE_MINUTE,
message: 'You are loading newsfeed views too quickly. Please try again later.',
},
getIndex: {
total: 60,
expire: ONE_MINUTE,
message: 'You are loading the newsroom too quickly. Please try again later.',
},
},
/*
* NewsroomController
*/
newsroom: {
getFeedView: {
total: 5,
expire: ONE_MINUTE,
message: 'You are reading newsletters too quickly',
@ -364,6 +383,9 @@ module.exports = {
},
},
/*
* WelcomeController
*/
welcome: {
total: 12,
expire: ONE_MINUTE,

12
lib/site-platform.js

@ -342,7 +342,7 @@ module.exports.startWebServer = async (dtp) => {
module.app.use(async (req, res, next) => {
const { cache: cacheService } = dtp.services;
try {
// expect incoming change from Base
res.locals.request = req;
const settingsKey = `settings:${dtp.config.site.domainKey}:site`;
res.locals.site = Object.assign({ }, dtp.config.site);
@ -358,6 +358,16 @@ module.exports.startWebServer = async (dtp) => {
}
});
/*
* Call out to application to register their custom middleware at the right
* point in the processing chain.
*/
module.log.debug('typeof dtp.config.registerMiddleware', { type: (typeof dtp.config.registerMiddleware) });
if (dtp.config && (typeof dtp.config.registerMiddleware === 'function')) {
module.log.info('registering custom application middleware');
await dtp.config.registerMiddleware(dtp, module.app);
}
/*
* System Init
*/

1
package.json

@ -37,6 +37,7 @@
"express-limiter": "^1.6.1",
"express-session": "^1.17.2",
"feed": "^4.2.2",
"feed-reader": "^6.1.2",
"geoip-lite": "^1.4.3",
"glob": "^7.2.0",
"highlight.js": "^11.4.0",

31
yarn.lock

@ -1762,6 +1762,11 @@ [email protected]:
resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
integrity sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=
bellajs@^11.0.7:
version "11.1.1"
resolved "https://registry.yarnpkg.com/bellajs/-/bellajs-11.1.1.tgz#1828dae65e396bf6c199fa8e0e402597b387ce29"
integrity sha512-Fjsx2ZVarl3UWeTq3YJbbPoQPyh4dWtduw+DMnDYhKya9agbEg/8eXP7yHOvv88zUEHoVl9O/XmgrNTMcMTVSQ==
binary-extensions@^1.0.0:
version "1.13.1"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65"
@ -2689,7 +2694,7 @@ cropperjs@^1.5.12:
resolved "https://registry.yarnpkg.com/cropperjs/-/cropperjs-1.5.12.tgz#d9c0db2bfb8c0d769d51739e8f916bbc44e10f50"
integrity sha512-re7UdjE5UnwdrovyhNzZ6gathI4Rs3KGCBSc8HCIjUo5hO42CtzyblmWLj6QWVw7huHyDMfpKxhiO2II77nhDw==
[email protected]:
[email protected], cross-fetch@^3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f"
integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==
@ -3675,6 +3680,13 @@ fast-xml-parser@^3.17.5:
dependencies:
strnum "^1.0.4"
fast-xml-parser@^4.0.10:
version "4.0.11"
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.0.11.tgz#42332a9aca544520631c8919e6ea871c0185a985"
integrity sha512-4aUg3aNRR/WjQAcpceODG1C3x3lFANXRo8+1biqfieHmg9pyMt7qB4lQV/Ta6sJCTbA5vfD8fnA8S54JATiFUA==
dependencies:
strnum "^1.0.5"
fd-slicer@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
@ -3682,6 +3694,16 @@ fd-slicer@~1.1.0:
dependencies:
pend "~1.2.0"
feed-reader@^6.1.2:
version "6.1.2"
resolved "https://registry.yarnpkg.com/feed-reader/-/feed-reader-6.1.2.tgz#6e6fb0c3d9bbdba85874676603fc86a50a1d3b5f"
integrity sha512-uvp5w3+mqNLFtdqQ89EJPWkLn/CKdxJkgEU4Erhft/5jGnjz3uepYlT5EWoijiFMO3rmK013/p6nKFqojke27g==
dependencies:
bellajs "^11.0.7"
cross-fetch "^3.1.5"
fast-xml-parser "^4.0.10"
html-entities "^2.3.3"
feed@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/feed/-/feed-4.2.2.tgz#865783ef6ed12579e2c44bbef3c9113bc4956a7e"
@ -4384,6 +4406,11 @@ html-encoding-sniffer@^3.0.0:
dependencies:
whatwg-encoding "^2.0.0"
html-entities@^2.3.3:
version "2.3.3"
resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.3.3.tgz#117d7626bece327fc8baace8868fa6f5ef856e46"
integrity sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==
html-filter@^4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/html-filter/-/html-filter-4.3.2.tgz#44bd2cee365699e8d0674d3253911b97d9381aa6"
@ -8074,7 +8101,7 @@ striptags@^3.2.0:
resolved "https://registry.yarnpkg.com/striptags/-/striptags-3.2.0.tgz#cc74a137db2de8b0b9a370006334161f7dd67052"
integrity sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==
strnum@^1.0.4:
strnum@^1.0.4, strnum@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/strnum/-/strnum-1.0.5.tgz#5c4e829fe15ad4ff0d20c3db5ac97b73c9b072db"
integrity sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==

Loading…
Cancel
Save