diff --git a/client/src/campaigns/Status.js b/client/src/campaigns/Status.js index cd8c256c..4866d922 100644 --- a/client/src/campaigns/Status.js +++ b/client/src/campaigns/Status.js @@ -74,7 +74,7 @@ class PreviewForTestUserModalDialog extends Component { const accessToken = result.data; window.open(getSandboxUrl(`cpgs/rss-preview/${campaignCid}/${listCid}/${subscriptionCid}`, accessToken, {withLocale: true}), '_blank'); - } else if (entity.type === CampaignType.REGULAR) { + } else if (entity.type === CampaignType.REGULAR || entity.type === CampaignType.RSS_ENTRY) { window.open(getPublicUrl(`archive/${campaignCid}/${listCid}/${subscriptionCid}`, {withLocale: true}), '_blank'); } else { @@ -322,6 +322,7 @@ class SendControls extends Component { const dialogs = ( <> + PreviewForTestUserModalDialog , + label: , link: `/campaigns/${data[0]}/status` }); } @@ -659,7 +660,7 @@ export default class Status extends Component {

RSS Entries

{t('ifANewEntryIsFoundFromCampaignFeedANew')}

- +
} diff --git a/server/lib/feedcheck.js b/server/lib/feedcheck.js index 4c4dd8e6..de70a3c3 100644 --- a/server/lib/feedcheck.js +++ b/server/lib/feedcheck.js @@ -57,9 +57,14 @@ async function fetch(url) { const entries = []; for (const item of items) { + let date = item.date || item.pubdate || item.pubDate; + if (date) { + date = (new Date(date)).toISOString(); + } + const entry = { title: item.title, - date: item.date || item.pubdate || item.pubDate || new Date(), + date: date, guid: item.guid || item.link, link: item.link, content: item.description || item.summary, @@ -84,7 +89,7 @@ async function getEntryForPreview(url) { if (entries.length === 0) { entry = { title: "Lorem Ipsum", - date: new Date(), + date: (new Date()).toISOString(), guid: "c21bc6c8-d351-4000-aa1f-e7ff928084cd", link: "http://www.example.com/sample-item.html", content: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer gravida a purus in commodo. Sed risus eros, pharetra sit amet sagittis vel, porta nec magna. Sed sollicitudin blandit ornare. Pellentesque a lacinia dui. Etiam ullamcorper, nisl at pharetra fringilla, enim nunc blandit quam, nec vestibulum purus lorem in urna.", diff --git a/server/lib/message-sender.js b/server/lib/message-sender.js index e784baf0..79b47476 100644 --- a/server/lib/message-sender.js +++ b/server/lib/message-sender.js @@ -741,14 +741,14 @@ async function queueSubscriptionMessage(sendConfigurationId, to, subject, encryp senders.scheduleCheck(); } -async function getMessage(campaignCid, listCid, subscriptionCid, settings) { +async function getMessage(campaignCid, listCid, subscriptionCid, settings, isTest = false) { const cs = new MessageSender(); await cs._init({type: MessageType.REGULAR, campaignCid, listCid, ...settings}); const campaign = cs.campaign; const list = cs.listsByCid.get(listCid); - const subscriptionGrouped = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, subscriptionCid); + const subscriptionGrouped = await subscriptions.getByCid(contextHelpers.getAdminContext(), list.id, subscriptionCid, true, isTest); let listOk = false; diff --git a/server/lib/tools.js b/server/lib/tools.js index e0ac497c..c9dd1ccc 100644 --- a/server/lib/tools.js +++ b/server/lib/tools.js @@ -166,6 +166,10 @@ function _formatTemplateSimple(source, mergeTags, isHTML) { } } + if (value === undefined) { // in RSS it may happen that the key is present, but the value is undefined + return ''; + } + const containsHTML = /<[a-z][\s\S]*>/.test(value); return isHTML ? he.encode((containsHTML ? value : value.replace(/(?:\r\n|\r|\n)/g, '
')), { useNamedReferences: true, diff --git a/server/models/campaigns.js b/server/models/campaigns.js index fe0b866f..31eef5c1 100644 --- a/server/models/campaigns.js +++ b/server/models/campaigns.js @@ -530,6 +530,7 @@ async function _createTx(tx, context, entity, content) { filteredEntity.status = CampaignStatus.ACTIVE; } else if (filteredEntity.type === CampaignType.RSS_ENTRY) { filteredEntity.status = CampaignStatus.SCHEDULED; + filteredEntity.start_at = new Date(); } else { filteredEntity.status = CampaignStatus.IDLE; } @@ -635,7 +636,7 @@ async function updateWithConsistencyCheck(context, entity, content) { }); } -async function _removeTx(tx, context, id, existing = null) { +async function _removeTx(tx, context, id, existing = null, overrideTypeCheck = false) { await shares.enforceEntityPermissionTx(tx, context, 'campaign', id, 'delete'); if (!existing) { @@ -646,11 +647,13 @@ async function _removeTx(tx, context, id, existing = null) { return new interoperableErrors.InvalidStateError; } - enforce(existing.type === CampaignType.REGULAR || existing.type === CampaignType.RSS || existing.type === CampaignType.TRIGGERED, 'This campaign cannot be removed by user.'); + if (!overrideTypeCheck) { + enforce(existing.type === CampaignType.REGULAR || existing.type === CampaignType.RSS || existing.type === CampaignType.TRIGGERED, 'This campaign cannot be removed by user.'); + } const childCampaigns = await tx('campaigns').where('parent', id).select(['id', 'status', 'type']); for (const childCampaign of childCampaigns) { - await _removeTx(tx, contect, childCampaign.id, childCampaign); + await _removeTx(tx, context, childCampaign.id, childCampaign, true); } await files.removeAllTx(tx, context, 'campaign', 'file', id); @@ -1104,21 +1107,12 @@ async function getRssPreview(context, campaignCid, listCid, subscriptionCid) { enforce(campaign.type === CampaignType.RSS); - const list = await lists.getByCid(context, listCid); - await shares.enforceEntityPermission(context, 'list', list.id, 'viewTestSubscriptions'); - - const subscription = await subscriptions.getByCid(context, list.id, subscriptionCid); - - if (!subscription.is_test) { - shares.throwPermissionDenied(); - } - const settings = { campaign, // this prevents message sender from fetching the campaign again rssEntry: await feedcheck.getEntryForPreview(campaign.data.feedUrl) }; - return await messageSender.getMessage(campaignCid, listCid, subscriptionCid, settings); + return await messageSender.getMessage(campaignCid, listCid, subscriptionCid, settings, true); } diff --git a/server/services/feedcheck.js b/server/services/feedcheck.js index b731649d..d06acc9b 100644 --- a/server/services/feedcheck.js +++ b/server/services/feedcheck.js @@ -1,8 +1,10 @@ 'use strict'; const config = require('../lib/config'); +const process = require('process'); const log = require('../lib/log'); const knex = require('../lib/knex'); +const senders = require('../lib/senders'); const { CampaignType, CampaignStatus, CampaignSource } = require('../../shared/campaigns'); const campaigns = require('../models/campaigns'); const contextHelpers = require('../lib/context-helpers'); @@ -26,106 +28,116 @@ async function run() { running = true; - let rssCampaignIdRow; + try { + let rssCampaignIdRow; - while (rssCampaignIdRow = await knex('campaigns') - .where('type', CampaignType.RSS) - .where('status', CampaignStatus.ACTIVE) - .where(qry => qry.whereNull('last_check').orWhere('last_check', '<', new Date(Date.now() - feedCheckInterval))) - .select('id') - .first()) { + while (rssCampaignIdRow = await knex('campaigns') + .where('type', CampaignType.RSS) + .where('status', CampaignStatus.ACTIVE) + .where(qry => qry.whereNull('last_check').orWhere('last_check', '<', new Date(Date.now() - feedCheckInterval))) + .select('id') + .first()) { - const rssCampaign = await campaigns.getById(contextHelpers.getAdminContext(), rssCampaignIdRow.id, false); + const rssCampaign = await campaigns.getById(contextHelpers.getAdminContext(), rssCampaignIdRow.id, false); - let checkStatus = null; + let checkStatus = null; - try { - const entries = await fetch(rssCampaign.data.feedUrl); + try { + const entries = await fetch(rssCampaign.data.feedUrl); - let added = 0; + let added = 0; - for (const entry of entries) { - let entryId = null; + for (const entry of entries) { + let entryId = null; - await knex.transaction(async tx => { - const existingEntry = await tx('rss').where({ - parent: rssCampaign.id, - guid: entry.guid - }).first(); + await knex.transaction(async tx => { + const existingEntry = await tx('rss').where({ + parent: rssCampaign.id, + guid: entry.guid + }).first(); - if (!existingEntry) { - const campaignData = {}; + if (!existingEntry) { + const campaignData = {}; - let source = rssCampaign.source; - if (source === CampaignSource.CUSTOM_FROM_TEMPLATE || source === CampaignSource.CUSTOM) { - source = CampaignSource.CUSTOM_FROM_CAMPAIGN; - campaignData.sourceCampaign = rssCampaign.id; - } else { - Object.assign(campaignData, rssCampaign.data); + let source = rssCampaign.source; + if (source === CampaignSource.CUSTOM_FROM_TEMPLATE || source === CampaignSource.CUSTOM) { + source = CampaignSource.CUSTOM_FROM_CAMPAIGN; + campaignData.sourceCampaign = rssCampaign.id; + } else { + Object.assign(campaignData, rssCampaign.data); + } + + campaignData.rssEntry = entry; + + const campaign = { + parent: rssCampaign.id, + type: CampaignType.RSS_ENTRY, + source, + name: entry.title || `RSS entry ${entry.guid.substr(0, 67)}`, + lists: rssCampaign.lists, + namespace: rssCampaign.namespace, + send_configuration: rssCampaign.send_configuration, + + from_name_override: rssCampaign.from_name_override, + from_email_override: rssCampaign.from_email_override, + reply_to_override: rssCampaign.reply_to_override, + subject: rssCampaign.subject, + data: campaignData, + + click_tracking_disabled: rssCampaign.click_tracking_disabled, + open_tracking_disabled: rssCampaign.open_tracking_disabled, + unsubscribe_url: rssCampaign.unsubscribe_url + }; + + const ids = await campaigns.createRssTx(tx, contextHelpers.getAdminContext(), campaign); + const campaignId = ids[0]; + + await tx('rss').insert({ + parent: rssCampaign.id, + campaign: campaignId, + guid: entry.guid, + pubdate: entry.date ? new Date(entry.date) : null, + }); + + added += 1; } + }); + } - campaignData.rssEntry = entry; + if (added > 0) { + checkStatus = tLog('foundAddedMessagesNewCampaignMessages', { + addedMessages: added, + campaignId: rssCampaign.id + }); + log.verbose('Feed', `Found ${added} new campaigns messages from feed ${rssCampaign.id}`); - const campaign = { - parent: rssCampaign.id, - type: CampaignType.RSS_ENTRY, - source, - name: entry.title || `RSS entry ${entry.guid.substr(0, 67)}`, - lists: rssCampaign.lists, - namespace: rssCampaign.namespace, - send_configuration: rssCampaign.send_configuration, + process.send({ + type: 'entries-added' + }); + } else { + checkStatus = tLog('foundNothingNewFromTheFeed'); + } - from_name_override: rssCampaign.from_name_override, - from_email_override: rssCampaign.from_email_override, - reply_to_override: rssCampaign.reply_to_override, - subject: rssCampaign.subject, - data: campaignData, + rssCampaign.data.checkStatus = checkStatus; + await knex('campaigns').where('id', rssCampaign.id).update({ + last_check: new Date(), + data: JSON.stringify(rssCampaign.data) + }); - click_tracking_disabled: rssCampaign.click_tracking_disabled, - open_tracking_disabled: rssCampaign.open_tracking_disabled, - unsubscribe_url: rssCampaign.unsubscribe_url - }; - - const ids = await campaigns.createRssTx(tx, contextHelpers.getAdminContext(), campaign); - const campaignId = ids[0]; - - await tx('rss').insert({ - parent: rssCampaign.id, - campaign: campaignId, - guid: entry.guid, - pubdate: entry.date, - }); - - added += 1; - } + } catch (err) { + log.error('Feed', err.message); + log.verbose(err.stack); + rssCampaign.data.checkStatus = err.message; + await knex('campaigns').where('id', rssCampaign.id).update({ + last_check: new Date(), + data: JSON.stringify(rssCampaign.data) }); } - - if (added > 0) { - checkStatus = tLog('foundAddedMessagesNewCampaignMessages', {addedMessages: added, campaignId: rssCampaign.id}); - log.verbose('Feed', `Found ${added} new campaigns messages from feed ${rssCampaign.id}`); - - process.send({ - type: 'entries-added' - }); - } else { - checkStatus = tLog('foundNothingNewFromTheFeed'); - } - - rssCampaign.data.checkStatus = checkStatus; - await knex('campaigns').where('id', rssCampaign.id).update({ - last_check: new Date(), - data: JSON.stringify(rssCampaign.data) - }); - - } catch (err) { - log.error('Feed', err.message); - rssCampaign.data.checkStatus = err.message; - await knex('campaigns').where('id', rssCampaign.id).update({ - last_check: new Date(), - data: JSON.stringify(rssCampaign.data) - }); } + + } catch (err) { + log.error('Feed', `Feedcheck failed with error: ${err.message}`); + log.verbose(err.stack); } running = false;