forked from digital-telepresence/dtp-sites
Rob Colbert
2 years ago
26 changed files with 860 additions and 6 deletions
@ -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); }, |
|||
}; |
@ -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); }, |
|||
}; |
@ -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); |
@ -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); |
@ -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); }, |
|||
}; |
@ -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) |
@ -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. |
@ -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() |
@ -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) |
@ -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. |
@ -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); |
|||
} |
|||
|
|||
})(); |
@ -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; |
@ -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…
Reference in new issue