329 lines
13 KiB
JavaScript
329 lines
13 KiB
JavaScript
// RSS feed route registration.
|
|
|
|
const { buildPagination } = require('../../../lib/pagination');
|
|
const renderRssFeedsPage = require('./list');
|
|
const renderRssFeedAddPage = require('./add');
|
|
const renderRssFeedEditPage = require('./edit');
|
|
const { buildDuplicateRssFeedName, buildDuplicateRssFeed } = require('./duplicate');
|
|
const { fetchAppSettings } = require('#src/data/app-settings');
|
|
|
|
async function getDataSourceUsageMaps(pool, common) {
|
|
const [slides] = await pool.query('SELECT content_json FROM c_slides WHERE content_json IS NOT NULL');
|
|
const rssFeedIds = new Set();
|
|
|
|
for (let index = 0; index < slides.length; index += 1) {
|
|
const content = typeof common.parseJsonSafe === 'function' ? common.parseJsonSafe(slides[index].content_json) : null;
|
|
if (!content || typeof content !== 'object') {
|
|
continue;
|
|
}
|
|
|
|
(function walk(value) {
|
|
if (!value || typeof value !== 'object') {
|
|
return;
|
|
}
|
|
|
|
if (Array.isArray(value)) {
|
|
value.forEach(walk);
|
|
return;
|
|
}
|
|
|
|
if (Object.prototype.hasOwnProperty.call(value, 'feed_id')) {
|
|
const feedId = Number(value.feed_id);
|
|
if (Number.isFinite(feedId)) {
|
|
rssFeedIds.add(feedId);
|
|
}
|
|
}
|
|
|
|
Object.keys(value).forEach(function (key) {
|
|
walk(value[key]);
|
|
});
|
|
})(content);
|
|
}
|
|
|
|
return rssFeedIds;
|
|
}
|
|
|
|
async function isRssFeedInUse(pool, common, rssFeedId) {
|
|
const usageIds = await getDataSourceUsageMaps(pool, common);
|
|
return usageIds.has(Number(rssFeedId));
|
|
}
|
|
|
|
async function sendRefreshTaskState(req, res, dataSourceTasks, sourceId) {
|
|
const taskId = Number(req.query.refresh_task_id);
|
|
if (!Number.isFinite(taskId) || taskId <= 0) {
|
|
return res.status(400).json({ error: 'Missing refresh task id.' });
|
|
}
|
|
|
|
const task = await dataSourceTasks.getTaskStatusById(taskId);
|
|
const expectedKey = 'rss-feed-refresh:' + Number(sourceId);
|
|
if (!task || task.key !== expectedKey) {
|
|
return res.status(404).json({ error: 'Refresh task not found.' });
|
|
}
|
|
|
|
res.json(task);
|
|
}
|
|
|
|
module.exports = function registerRssFeedRoutes(app, deps) {
|
|
const pool = deps.pool;
|
|
const common = deps.common;
|
|
const pages = deps.pages;
|
|
const dataSourceTasks = deps.dataSourceTasks;
|
|
const backgroundTaskQueue = deps.backgroundTaskQueue;
|
|
const getAuditUserId = deps.getAuditUserId;
|
|
const redirectAfterSave = deps.redirectAfterSave;
|
|
const formatDashboardDate = deps.formatDashboardDate || function (value) {
|
|
return value ? String(value) : '';
|
|
};
|
|
const requirePermission = deps.requirePermission;
|
|
|
|
const LIST_PAGE_SIZE = 25;
|
|
|
|
app.get('/data-sources/rss-feeds', requirePermission('rss-feeds.read'), async function (req, res, next) {
|
|
try {
|
|
const page = Math.max(1, Math.floor(Number(req.query.page) || 1));
|
|
const search = common.getSearchQuery(req);
|
|
const sort = common.getSortQuery(req);
|
|
const direction = common.getSortDirectionQuery(req);
|
|
const data = await common.fetchRssFeedsPage(pool, page, LIST_PAGE_SIZE, search, sort, direction);
|
|
const usageIds = await getDataSourceUsageMaps(pool, common);
|
|
const rssFeeds = (data.rssFeeds || []).map(function (rssFeed) {
|
|
return Object.assign({}, rssFeed, {
|
|
inUse: usageIds.has(Number(rssFeed.id))
|
|
});
|
|
});
|
|
res.send(renderRssFeedsPage({
|
|
rssFeeds: rssFeeds,
|
|
pagination: buildPagination(data.totalItems, data.currentPage, 'page', { search: search, sort: sort, direction: direction }, LIST_PAGE_SIZE, 'RSS feeds', 'RSS feed pages')
|
|
}, req.query.message ? String(req.query.message) : '', req.currentUser));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.get('/data-sources/rss-feeds/new', requirePermission('rss-feeds.create'), async function (req, res, next) {
|
|
try {
|
|
const settings = await fetchAppSettings(pool);
|
|
res.send(renderRssFeedAddPage({
|
|
updateIntervalValue: settings['data-sources.rss_default_interval_value'],
|
|
updateIntervalUnit: settings['data-sources.rss_default_interval_unit']
|
|
}, req.query.message ? String(req.query.message) : '', req.currentUser));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.get('/data-sources/rss-feeds/:id/edit', requirePermission('rss-feeds.update'), async function (req, res, next) {
|
|
try {
|
|
const rssFeed = await common.fetchRssFeedById(pool, Number(req.params.id));
|
|
if (!rssFeed) {
|
|
return res.status(404).send('RSS feed not found');
|
|
}
|
|
|
|
const [slides] = await pool.query('SELECT content_json FROM c_slides WHERE content_json IS NOT NULL');
|
|
const inUse = (slides || []).some(function (row) {
|
|
const content = typeof common.parseJsonSafe === 'function' ? common.parseJsonSafe(row.content_json) : null;
|
|
if (!content || typeof content !== 'object') {
|
|
return false;
|
|
}
|
|
|
|
const stack = [content];
|
|
while (stack.length) {
|
|
const value = stack.pop();
|
|
if (!value || typeof value !== 'object') {
|
|
continue;
|
|
}
|
|
if (Array.isArray(value)) {
|
|
value.forEach(function (item) { stack.push(item); });
|
|
continue;
|
|
}
|
|
if (Object.prototype.hasOwnProperty.call(value, 'feed_id') && Number(value.feed_id) === Number(rssFeed.id)) {
|
|
return true;
|
|
}
|
|
Object.keys(value).forEach(function (key) {
|
|
stack.push(value[key]);
|
|
});
|
|
}
|
|
return false;
|
|
});
|
|
|
|
const pulledItems = typeof common.fetchRssFeedItemsByFeedId === 'function'
|
|
? await common.fetchRssFeedItemsByFeedId(pool, rssFeed.id)
|
|
: [];
|
|
|
|
res.send(renderRssFeedEditPage(Object.assign({}, rssFeed, {
|
|
feedUrl: rssFeed.feed_url,
|
|
updateIntervalValue: rssFeed.update_interval_value,
|
|
updateIntervalUnit: rssFeed.update_interval_unit || 'minutes',
|
|
itemLimit: rssFeed.item_limit,
|
|
inUse: inUse
|
|
}), {
|
|
pulledItems: pulledItems,
|
|
pullError: ''
|
|
}, req.query.message ? String(req.query.message) : '', req.currentUser));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.get('/data-sources/rss-feeds/:id/duplicate', requirePermission('rss-feeds.read'), requirePermission('rss-feeds.create'), async function (req, res, next) {
|
|
try {
|
|
const rssFeed = await common.fetchRssFeedById(pool, Number(req.params.id));
|
|
if (!rssFeed) {
|
|
return res.status(404).send('RSS feed not found');
|
|
}
|
|
|
|
let duplicateName = buildDuplicateRssFeedName(rssFeed.name);
|
|
let duplicateIndex = 2;
|
|
while (await common.fetchDuplicateName(pool, 'i_rss_feeds', duplicateName)) {
|
|
duplicateName = buildDuplicateRssFeedName(rssFeed.name) + ' (' + duplicateIndex + ')';
|
|
duplicateIndex += 1;
|
|
}
|
|
|
|
res.send(renderRssFeedAddPage(buildDuplicateRssFeed(rssFeed, duplicateName), req.query.message ? String(req.query.message) : 'Review the copied values and save when ready.', req.currentUser, {
|
|
messageVariant: 'info',
|
|
showSaveSecondaryActions: true
|
|
}));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
app.get('/data-sources/rss-feeds/:id/state', requirePermission('rss-feeds.update'), function (req, res, next) {
|
|
sendRefreshTaskState(req, res, dataSourceTasks, Number(req.params.id)).catch(next);
|
|
});
|
|
|
|
app.post('/data-sources/rss-feeds', requirePermission('rss-feeds.create'), async function (req, res, next) {
|
|
const connection = await pool.getConnection();
|
|
try {
|
|
const payload = common.buildRssFeedPayload(req, null);
|
|
if (await common.fetchDuplicateName(pool, 'i_rss_feeds', payload.name)) {
|
|
return res.redirect('/data-sources/rss-feeds/new?message=' + encodeURIComponent('An RSS feed with that name already exists.'));
|
|
}
|
|
|
|
const actorId = getAuditUserId(req);
|
|
await connection.beginTransaction();
|
|
const [result] = await connection.query(
|
|
'INSERT INTO i_rss_feeds (name, feed_url, update_interval_value, update_interval_unit, item_limit, created_by, modified_by) VALUES (?, ?, ?, ?, ?, ?, ?)',
|
|
[payload.name, payload.feedUrl, payload.updateIntervalValue, payload.updateIntervalUnit, payload.itemLimit, actorId, actorId]
|
|
);
|
|
await connection.commit();
|
|
dataSourceTasks.registerRecurringRefresh('rss-feed', result.insertId, payload.name, payload.updateIntervalValue, payload.updateIntervalUnit, function () {
|
|
return dataSourceTasks.refreshRssFeedInBackground(result.insertId, payload.feedUrl, payload.itemLimit, actorId);
|
|
});
|
|
const refreshTask = await backgroundTaskQueue.enqueueTask({
|
|
key: 'rss-feed-refresh:' + result.insertId,
|
|
title: 'RSS feed refresh',
|
|
category: 'data-source',
|
|
taskType: 'data-source-refresh',
|
|
payload: {
|
|
sourceType: 'rss-feed',
|
|
sourceId: result.insertId,
|
|
sourceName: payload.name,
|
|
actorId: actorId
|
|
},
|
|
metadata: {
|
|
sourceType: 'rss-feed',
|
|
sourceId: result.insertId,
|
|
sourceName: payload.name
|
|
}
|
|
});
|
|
const message = 'RSS feed created. Refresh is running in the background.';
|
|
res.redirect('/data-sources/rss-feeds/' + result.insertId + '/edit?message=' + encodeURIComponent(message) + '&refresh_task_id=' + encodeURIComponent(refreshTask && refreshTask.id ? refreshTask.id : ''));
|
|
} catch (error) {
|
|
try {
|
|
await connection.rollback();
|
|
} catch (_rollbackError) {
|
|
// Ignore rollback failures and surface the original error.
|
|
}
|
|
next(error);
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
});
|
|
|
|
app.post('/data-sources/rss-feeds/:id', requirePermission('rss-feeds.update'), async function (req, res, next) {
|
|
const connection = await pool.getConnection();
|
|
try {
|
|
const rssFeed = await common.fetchRssFeedById(pool, Number(req.params.id));
|
|
if (!rssFeed) {
|
|
return res.status(404).send('RSS feed not found');
|
|
}
|
|
|
|
const payload = common.buildRssFeedPayload(req, rssFeed);
|
|
if (await common.fetchDuplicateName(pool, 'i_rss_feeds', payload.name, rssFeed.id)) {
|
|
return res.redirect('/data-sources/rss-feeds/' + rssFeed.id + '/edit?message=' + encodeURIComponent('An RSS feed with that name already exists.'));
|
|
}
|
|
const actorId = getAuditUserId(req);
|
|
await connection.beginTransaction();
|
|
await connection.query(
|
|
'UPDATE i_rss_feeds SET name = ?, feed_url = ?, update_interval_value = ?, update_interval_unit = ?, item_limit = ?, modified_by = ? WHERE id = ?',
|
|
[payload.name, payload.feedUrl, payload.updateIntervalValue, payload.updateIntervalUnit, payload.itemLimit, actorId, rssFeed.id]
|
|
);
|
|
await connection.commit();
|
|
dataSourceTasks.registerRecurringRefresh('rss-feed', rssFeed.id, payload.name, payload.updateIntervalValue, payload.updateIntervalUnit, function () {
|
|
return dataSourceTasks.refreshRssFeedInBackground(rssFeed.id, payload.feedUrl, payload.itemLimit, actorId);
|
|
});
|
|
const message = 'RSS feed updated. Refresh is running in the background.';
|
|
const refreshTask = await backgroundTaskQueue.enqueueTask({
|
|
key: 'rss-feed-refresh:' + rssFeed.id,
|
|
title: 'RSS feed refresh',
|
|
category: 'data-source',
|
|
taskType: 'data-source-refresh',
|
|
payload: {
|
|
sourceType: 'rss-feed',
|
|
sourceId: rssFeed.id,
|
|
sourceName: payload.name,
|
|
actorId: actorId
|
|
},
|
|
metadata: {
|
|
sourceType: 'rss-feed',
|
|
sourceId: rssFeed.id,
|
|
sourceName: payload.name
|
|
}
|
|
});
|
|
redirectAfterSave(req, res, '/data-sources/rss-feeds/' + rssFeed.id + '/edit?refresh_task_id=' + encodeURIComponent(refreshTask && refreshTask.id ? refreshTask.id : ''), {
|
|
closeUrl: '/data-sources/rss-feeds',
|
|
newUrl: '/data-sources/rss-feeds/new',
|
|
message: message
|
|
});
|
|
} catch (error) {
|
|
try {
|
|
await connection.rollback();
|
|
} catch (_rollbackError) {
|
|
// Ignore rollback failures and surface the original error.
|
|
}
|
|
next(error);
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
});
|
|
|
|
app.post('/data-sources/rss-feeds/:id/delete', requirePermission('rss-feeds.delete'), async function (req, res, next) {
|
|
try {
|
|
const rssFeed = await common.fetchRssFeedById(pool, Number(req.params.id));
|
|
if (!rssFeed) {
|
|
return res.status(404).send('RSS feed not found');
|
|
}
|
|
|
|
if (await isRssFeedInUse(pool, common, rssFeed.id)) {
|
|
return res.redirect('/data-sources/rss-feeds?message=' + encodeURIComponent('This RSS feed is still used by one or more slides.'));
|
|
}
|
|
|
|
const connection = await pool.getConnection();
|
|
try {
|
|
await connection.beginTransaction();
|
|
await connection.query('DELETE FROM i_rss_feeds WHERE id = ?', [rssFeed.id]);
|
|
await connection.commit();
|
|
dataSourceTasks.removeRecurringRefresh('rss-feed', rssFeed.id);
|
|
} catch (error) {
|
|
await connection.rollback();
|
|
throw error;
|
|
} finally {
|
|
connection.release();
|
|
}
|
|
res.redirect('/data-sources/rss-feeds?message=' + encodeURIComponent('RSS feed deleted.'));
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
}; |