2017-07-11 09:28:44 +00:00
'use strict' ;
const knex = require ( '../lib/knex' ) ;
2018-08-01 10:00:20 +00:00
const hasher = require ( 'node-object-hash' ) ( ) ;
2017-07-11 09:28:44 +00:00
const dtHelpers = require ( '../lib/dt-helpers' ) ;
2018-11-18 14:38:52 +00:00
const interoperableErrors = require ( '../../shared/interoperable-errors' ) ;
2018-07-31 04:34:28 +00:00
const shortid = require ( 'shortid' ) ;
2018-08-01 10:00:20 +00:00
const { enforce , filterObject } = require ( '../lib/helpers' ) ;
2017-08-13 18:11:58 +00:00
const shares = require ( './shares' ) ;
2018-08-01 10:00:20 +00:00
const namespaceHelpers = require ( '../lib/namespace-helpers' ) ;
2018-07-31 04:34:28 +00:00
const files = require ( './files' ) ;
2018-08-02 10:19:27 +00:00
const templates = require ( './templates' ) ;
2019-07-03 09:58:58 +00:00
const { allTagLanguages } = require ( '../../shared/templates' ) ;
2019-07-16 15:33:37 +00:00
const { CampaignMessageStatus , CampaignStatus , CampaignSource , CampaignType , getSendConfigurationPermissionRequiredForSend } = require ( '../../shared/campaigns' ) ;
2018-08-03 16:07:46 +00:00
const sendConfigurations = require ( './send-configurations' ) ;
const triggers = require ( './triggers' ) ;
2018-11-18 14:38:52 +00:00
const { SubscriptionStatus } = require ( '../../shared/lists' ) ;
2018-09-09 22:55:44 +00:00
const subscriptions = require ( './subscriptions' ) ;
const segments = require ( './segments' ) ;
const senders = require ( '../lib/senders' ) ;
2019-06-29 21:19:56 +00:00
const links = require ( './links' ) ;
2018-12-29 14:12:42 +00:00
const feedcheck = require ( '../lib/feedcheck' ) ;
2019-01-12 10:21:38 +00:00
const contextHelpers = require ( '../lib/context-helpers' ) ;
2019-02-18 20:36:44 +00:00
const { convertFileURLs } = require ( '../lib/campaign-content' ) ;
2019-06-29 21:19:56 +00:00
const messageSender = require ( '../lib/message-sender' ) ;
2019-06-25 05:18:06 +00:00
const lists = require ( './lists' ) ;
2017-07-11 09:28:44 +00:00
2019-02-07 14:38:32 +00:00
const { EntityActivityType , CampaignActivityType } = require ( '../../shared/activity-log' ) ;
const activityLog = require ( '../lib/activity-log' ) ;
2018-09-02 18:17:42 +00:00
const allowedKeysCommon = [ 'name' , 'description' , 'segment' , 'namespace' ,
2019-06-25 05:18:06 +00:00
'send_configuration' , 'from_name_override' , 'from_email_override' , 'reply_to_override' , 'subject' , 'data' , 'click_tracking_disabled' , 'open_tracking_disabled' , 'unsubscribe_url' ] ;
2018-07-31 04:34:28 +00:00
2018-08-01 10:00:20 +00:00
const allowedKeysCreate = new Set ( [ 'type' , 'source' , ... allowedKeysCommon ] ) ;
2018-11-17 01:54:23 +00:00
const allowedKeysCreateRssEntry = new Set ( [ 'type' , 'source' , 'parent' , ... allowedKeysCommon ] ) ;
2018-07-31 04:34:28 +00:00
const allowedKeysUpdate = new Set ( [ ... allowedKeysCommon ] ) ;
2018-08-03 11:35:55 +00:00
const Content = {
ALL : 0 ,
WITHOUT _SOURCE _CUSTOM : 1 ,
2018-09-02 12:59:02 +00:00
ONLY _SOURCE _CUSTOM : 2 ,
2018-09-09 22:55:44 +00:00
RSS _ENTRY : 3 ,
SETTINGS _WITH _STATS : 4
2018-08-03 11:35:55 +00:00
} ;
function hash ( entity , content ) {
let filteredEntity ;
if ( content === Content . ALL ) {
filteredEntity = filterObject ( entity , allowedKeysUpdate ) ;
2018-09-02 18:17:42 +00:00
filteredEntity . lists = entity . lists ;
2018-08-03 11:35:55 +00:00
} else if ( content === Content . WITHOUT _SOURCE _CUSTOM ) {
filteredEntity = filterObject ( entity , allowedKeysUpdate ) ;
2018-09-02 18:17:42 +00:00
filteredEntity . lists = entity . lists ;
2018-08-03 11:35:55 +00:00
filteredEntity . data = { ... filteredEntity . data } ;
delete filteredEntity . data . sourceCustom ;
} else if ( content === Content . ONLY _SOURCE _CUSTOM ) {
filteredEntity = {
data : {
sourceCustom : entity . data . sourceCustom
}
} ;
}
return hasher . hash ( filteredEntity ) ;
2018-08-01 10:00:20 +00:00
}
2019-03-26 23:41:18 +00:00
async function _listDTAjax ( context , namespaceId , params ) {
2017-08-13 18:11:58 +00:00
return await dtHelpers . ajaxListWithPermissions (
context ,
[ { entityTypeId : 'campaign' , requiredOperations : [ 'view' ] } ] ,
params ,
2019-03-26 23:41:18 +00:00
builder => {
builder = builder . from ( 'campaigns' )
. innerJoin ( 'namespaces' , 'namespaces.id' , 'campaigns.namespace' )
. whereNull ( 'campaigns.parent' ) ;
if ( namespaceId ) {
builder = builder . where ( 'namespaces.id' , namespaceId ) ;
}
return builder ;
} ,
2018-11-17 01:54:23 +00:00
[ 'campaigns.id' , 'campaigns.name' , 'campaigns.cid' , 'campaigns.description' , 'campaigns.type' , 'campaigns.status' , 'campaigns.scheduled' , 'campaigns.source' , 'campaigns.created' , 'namespaces.name' ]
) ;
}
2019-03-26 23:41:18 +00:00
async function listDTAjax ( context , params ) {
return await _listDTAjax ( context , undefined , params ) ;
}
2019-03-13 08:52:02 +00:00
async function listByNamespaceDTAjax ( context , namespaceId , params ) {
2019-03-26 23:41:18 +00:00
return await _listDTAjax ( context , namespaceId , params ) ;
2019-03-13 08:52:02 +00:00
}
2018-11-17 01:54:23 +00:00
async function listChildrenDTAjax ( context , campaignId , params ) {
return await dtHelpers . ajaxListWithPermissions (
context ,
[ { entityTypeId : 'campaign' , requiredOperations : [ 'view' ] } ] ,
params ,
builder => builder . from ( 'campaigns' )
. innerJoin ( 'namespaces' , 'namespaces.id' , 'campaigns.namespace' )
. where ( 'campaigns.parent' , campaignId ) ,
2018-09-27 10:34:54 +00:00
[ 'campaigns.id' , 'campaigns.name' , 'campaigns.cid' , 'campaigns.description' , 'campaigns.type' , 'campaigns.status' , 'campaigns.scheduled' , 'campaigns.source' , 'campaigns.created' , 'namespaces.name' ]
2017-08-13 18:11:58 +00:00
) ;
}
2017-07-13 11:27:03 +00:00
2018-11-17 01:54:23 +00:00
2018-08-03 11:35:55 +00:00
async function listWithContentDTAjax ( context , params ) {
return await dtHelpers . ajaxListWithPermissions (
context ,
[ { entityTypeId : 'campaign' , requiredOperations : [ 'view' ] } ] ,
params ,
builder => builder . from ( 'campaigns' )
. innerJoin ( 'namespaces' , 'namespaces.id' , 'campaigns.namespace' )
. whereIn ( 'campaigns.source' , [ CampaignSource . CUSTOM , CampaignSource . CUSTOM _FROM _TEMPLATE , CampaignSource . CUSTOM _FROM _CAMPAIGN ] ) ,
2018-09-27 10:34:54 +00:00
[ 'campaigns.id' , 'campaigns.name' , 'campaigns.cid' , 'campaigns.description' , 'campaigns.type' , 'campaigns.created' , 'namespaces.name' ]
2018-08-03 11:35:55 +00:00
) ;
}
2018-09-02 18:17:42 +00:00
async function listOthersWhoseListsAreIncludedDTAjax ( context , campaignId , listIds , params ) {
2018-08-04 09:30:37 +00:00
return await dtHelpers . ajaxListWithPermissions (
context ,
[ { entityTypeId : 'campaign' , requiredOperations : [ 'view' ] } ] ,
params ,
builder => builder . from ( 'campaigns' )
. innerJoin ( 'namespaces' , 'namespaces.id' , 'campaigns.namespace' )
. whereNot ( 'campaigns.id' , campaignId )
2018-09-02 18:17:42 +00:00
. whereNotExists ( qry => qry . from ( 'campaign_lists' ) . whereRaw ( 'campaign_lists.campaign = campaigns.id' ) . whereNotIn ( 'campaign_lists.list' , listIds ) ) ,
2018-09-27 10:34:54 +00:00
[ 'campaigns.id' , 'campaigns.name' , 'campaigns.cid' , 'campaigns.description' , 'campaigns.type' , 'campaigns.created' , 'namespaces.name' ]
2018-08-04 09:30:37 +00:00
) ;
}
2018-09-09 22:55:44 +00:00
async function listTestUsersDTAjax ( context , campaignId , params ) {
return await knex . transaction ( async tx => {
await shares . enforceEntityPermissionTx ( tx , context , 'campaign' , campaignId , 'view' ) ;
2018-09-10 18:15:59 +00:00
/ *
This is supposed to produce queries like this :
select * from (
2018-09-27 16:30:23 +00:00
( select ` subscription__1 ` . ` email ` , ` subscription__1 ` . ` cid ` , 1 AS list , NULL AS segment from ` subscription__1 ` where ` subscription__1 ` . ` status ` = 1 and ` subscription__1 ` . ` is_test ` = true )
2018-09-10 18:15:59 +00:00
UNION ALL
2018-09-27 16:30:23 +00:00
( select ` subscription__2 ` . ` email ` , ` subscription__2 ` . ` cid ` , 2 AS list , NULL AS segment from ` subscription__2 ` where ` subscription__2 ` . ` status ` = 1 and ` subscription__2 ` . ` is_test ` = true )
2018-09-10 18:15:59 +00:00
) as ` test_subscriptions ` inner join ` lists ` on ` test_subscriptions ` . ` list ` = ` lists ` . ` id ` inner join ` segments ` on ` test_subscriptions ` . ` segment ` = ` segments ` . ` id `
inner join ` namespaces ` on ` lists ` . ` namespace ` = ` namespaces ` . ` id `
This was too much for Knex , so we partially construct these queries directly as strings ;
* /
const subsQrys = [ ] ;
2018-09-09 22:55:44 +00:00
const cpgLists = await tx ( 'campaign_lists' ) . where ( 'campaign' , campaignId ) ;
for ( const cpgList of cpgLists ) {
2018-09-10 18:15:59 +00:00
const addSegmentQuery = cpgList . segment ? await segments . getQueryGeneratorTx ( tx , cpgList . list , cpgList . segment ) : ( ) => { } ;
2018-09-09 22:55:44 +00:00
const subsTable = subscriptions . getSubscriptionTableName ( cpgList . list ) ;
2018-09-10 18:15:59 +00:00
const sqlQry = knex . from ( subsTable )
. where ( subsTable + '.status' , SubscriptionStatus . SUBSCRIBED )
. where ( subsTable + '.is_test' , true )
. where ( function ( ) {
addSegmentQuery ( this ) ;
} )
2018-09-27 16:30:23 +00:00
. select ( [ subsTable + '.email' , subsTable + '.cid' , knex . raw ( '? AS list' , [ cpgList . list ] ) , knex . raw ( '? AS segment' , [ cpgList . segment ] ) ] )
2018-09-10 18:15:59 +00:00
. toSQL ( ) . toNative ( ) ;
subsQrys . push ( sqlQry ) ;
2018-09-09 22:55:44 +00:00
}
2018-09-10 18:15:59 +00:00
if ( subsQrys . length > 0 ) {
let subsQry ;
if ( subsQrys . length === 1 ) {
2019-06-25 05:18:06 +00:00
const subsUnionSql = '(' + subsQrys [ 0 ] . sql + ') as `test_subscriptions`' ;
2018-09-10 18:15:59 +00:00
subsQry = knex . raw ( subsUnionSql , subsQrys [ 0 ] . bindings ) ;
} else {
const subsUnionSql = '(' +
subsQrys . map ( qry => '(' + qry . sql + ')' ) . join ( ' UNION ALL ' ) +
') as `test_subscriptions`' ;
const subsUnionBindings = Array . prototype . concat ( ... subsQrys . map ( qry => qry . bindings ) ) ;
subsQry = knex . raw ( subsUnionSql , subsUnionBindings ) ;
}
2018-12-16 12:47:08 +00:00
return await dtHelpers . ajaxListWithPermissionsTx (
tx ,
2018-09-09 22:55:44 +00:00
context ,
2019-08-01 05:46:40 +00:00
[ { entityTypeId : 'list' , requiredOperations : [ 'viewTestSubscriptions' ] , column : 'subs.list_id' } ] ,
2018-09-09 22:55:44 +00:00
params ,
builder => {
2018-09-27 16:30:23 +00:00
return builder . from ( function ( ) {
return this . from ( subsQry )
. innerJoin ( 'lists' , 'test_subscriptions.list' , 'lists.id' )
. innerJoin ( 'namespaces' , 'lists.namespace' , 'namespaces.id' )
. select ( [
knex . raw ( 'CONCAT_WS(":", lists.cid, test_subscriptions.cid) AS cid' ) ,
'test_subscriptions.email' , 'test_subscriptions.cid AS subscription_cid' , 'lists.cid AS list_cid' ,
'lists.name as list_name' , 'namespaces.name AS namespace_name' , 'lists.id AS list_id'
] )
. as ( 'subs' ) ;
} ) ;
2018-09-09 22:55:44 +00:00
} ,
2018-09-27 16:30:23 +00:00
[ 'subs.cid' , 'subs.email' , 'subs.subscription_cid' , 'subs.list_cid' , 'subs.list_name' , 'subs.namespace_name' ]
2018-09-09 22:55:44 +00:00
) ;
} else {
const result = {
draw : params . draw ,
recordsTotal : 0 ,
recordsFiltered : 0 ,
data : [ ]
} ;
return result ;
}
} ) ;
}
2018-12-16 12:47:08 +00:00
async function _listSubscriberResultsDTAjax ( context , campaignId , getSubsQrys , columns , params ) {
return await knex . transaction ( async tx => {
await shares . enforceEntityPermissionTx ( tx , context , 'campaign' , campaignId , 'view' ) ;
const subsQrys = [ ] ;
const cpgLists = await tx ( 'campaign_lists' ) . where ( 'campaign' , campaignId ) ;
for ( const cpgList of cpgLists ) {
const subsTable = subscriptions . getSubscriptionTableName ( cpgList . list ) ;
subsQrys . push ( getSubsQrys ( subsTable , cpgList ) ) ;
}
if ( subsQrys . length > 0 ) {
let subsSql , subsBindings ;
if ( subsQrys . length === 1 ) {
subsSql = '(' + subsQrys [ 0 ] . sql + ') as `subs`'
subsBindings = subsQrys [ 0 ] . bindings ;
} else {
subsSql = '(' +
subsQrys . map ( qry => '(' + qry . sql + ')' ) . join ( ' UNION ALL ' ) +
') as `subs`' ;
subsBindings = Array . prototype . concat ( ... subsQrys . map ( qry => qry . bindings ) ) ;
}
return await dtHelpers . ajaxListWithPermissionsTx (
tx ,
context ,
[ { entityTypeId : 'list' , requiredOperations : [ 'viewSubscriptions' ] , column : 'lists.id' } ] ,
params ,
( builder , tx ) => builder . from ( knex . raw ( subsSql , subsBindings ) )
. innerJoin ( 'lists' , 'subs.list' , 'lists.id' )
. innerJoin ( 'namespaces' , 'lists.namespace' , 'namespaces.id' )
,
columns
) ;
} else {
const result = {
draw : params . draw ,
recordsTotal : 0 ,
recordsFiltered : 0 ,
data : [ ]
} ;
return result ;
}
} ) ;
}
async function listSentByStatusDTAjax ( context , campaignId , status , params ) {
return await _listSubscriberResultsDTAjax (
context ,
campaignId ,
( subsTable , cpgList ) => knex . from ( subsTable )
. innerJoin (
function ( ) {
return this . from ( 'campaign_messages' )
. where ( 'campaign_messages.campaign' , campaignId )
. where ( 'campaign_messages.list' , cpgList . list )
. where ( 'campaign_messages.status' , status )
. as ( 'related_campaign_messages' ) ;
} ,
'related_campaign_messages.subscription' , subsTable + '.id' )
. select ( [ subsTable + '.email' , subsTable + '.cid' , knex . raw ( '? AS list' , [ cpgList . list ] ) ] )
. toSQL ( ) . toNative ( ) ,
[ 'subs.email' , 'subs.cid' , 'lists.cid' , 'lists.name' , 'namespaces.name' ] ,
params
) ;
}
async function listOpensDTAjax ( context , campaignId , params ) {
return await _listSubscriberResultsDTAjax (
context ,
campaignId ,
( subsTable , cpgList ) => knex . from ( subsTable )
. innerJoin (
function ( ) {
return this . from ( 'campaign_links' )
. where ( 'campaign_links.campaign' , campaignId )
. where ( 'campaign_links.list' , cpgList . list )
2019-06-29 21:19:56 +00:00
. where ( 'campaign_links.link' , links . LinkId . OPEN )
2018-12-16 12:47:08 +00:00
. as ( 'related_campaign_links' ) ;
} ,
'related_campaign_links.subscription' , subsTable + '.id' )
. select ( [ subsTable + '.email' , subsTable + '.cid' , knex . raw ( '? AS list' , [ cpgList . list ] ) , 'related_campaign_links.count' ] )
. toSQL ( ) . toNative ( ) ,
[ 'subs.email' , 'subs.cid' , 'lists.cid' , 'lists.name' , 'namespaces.name' , 'subs.count' ] ,
params
) ;
}
async function listLinkClicksDTAjax ( context , campaignId , params ) {
return await knex . transaction ( async ( tx ) => {
await shares . enforceEntityPermissionTx ( tx , context , 'campaign' , campaignId , 'viewStats' ) ;
return await dtHelpers . ajaxListTx (
tx ,
params ,
builder => builder . from ( 'links' )
. where ( 'links.campaign' , campaignId ) ,
[ 'links.url' , 'links.visits' , 'links.hits' ]
) ;
} ) ;
}
2018-09-23 20:28:58 +00:00
async function getTrackingSettingsByCidTx ( tx , cid ) {
const entity = await tx ( 'campaigns' ) . where ( 'campaigns.cid' , cid )
. select ( [
'campaigns.id' , 'campaigns.click_tracking_disabled' , 'campaigns.open_tracking_disabled'
] )
. first ( ) ;
if ( ! entity ) {
throw new interoperableErrors . NotFoundError ( ) ;
}
return entity ;
}
2019-07-09 22:06:56 +00:00
async function lockByIdTx ( tx , id ) {
// This locks the entry for update
await tx ( 'campaigns' ) . where ( 'id' , id ) . forUpdate ( ) ;
}
2018-09-22 16:12:22 +00:00
async function rawGetByTx ( tx , key , id ) {
const entity = await tx ( 'campaigns' ) . where ( 'campaigns.' + key , id )
2018-09-10 18:15:59 +00:00
. leftJoin ( 'campaign_lists' , 'campaigns.id' , 'campaign_lists.campaign' )
2018-09-02 18:17:42 +00:00
. groupBy ( 'campaigns.id' )
. select ( [
2018-09-27 10:34:54 +00:00
'campaigns.id' , 'campaigns.cid' , 'campaigns.name' , 'campaigns.description' , 'campaigns.namespace' , 'campaigns.status' , 'campaigns.type' , 'campaigns.source' ,
2019-06-25 05:18:06 +00:00
'campaigns.send_configuration' , 'campaigns.from_name_override' , 'campaigns.from_email_override' , 'campaigns.reply_to_override' , 'campaigns.subject' ,
2018-09-27 21:37:50 +00:00
'campaigns.data' , 'campaigns.click_tracking_disabled' , 'campaigns.open_tracking_disabled' , 'campaigns.unsubscribe_url' , 'campaigns.scheduled' ,
2018-12-23 19:27:29 +00:00
'campaigns.delivered' , 'campaigns.unsubscribed' , 'campaigns.bounced' , 'campaigns.complained' , 'campaigns.blacklisted' , 'campaigns.opened' , 'campaigns.clicks' ,
2018-09-02 18:17:42 +00:00
knex . raw ( ` GROUP_CONCAT(CONCAT_WS( \' : \' , campaign_lists.list, campaign_lists.segment) ORDER BY campaign_lists.id SEPARATOR \' ; \' ) as lists ` )
] )
. first ( ) ;
if ( ! entity ) {
throw new interoperableErrors . NotFoundError ( ) ;
}
2018-09-10 18:15:59 +00:00
if ( entity . lists ) {
entity . lists = entity . lists . split ( ';' ) . map ( x => {
const entries = x . split ( ':' ) ;
const list = Number . parseInt ( entries [ 0 ] ) ;
const segment = entries [ 1 ] ? Number . parseInt ( entries [ 1 ] ) : null ;
return { list , segment } ;
} ) ;
} else {
entity . lists = [ ] ;
}
2018-09-02 18:17:42 +00:00
entity . data = JSON . parse ( entity . data ) ;
return entity ;
}
2018-08-03 11:35:55 +00:00
async function getByIdTx ( tx , context , id , withPermissions = true , content = Content . ALL ) {
2018-08-02 10:19:27 +00:00
await shares . enforceEntityPermissionTx ( tx , context , 'campaign' , id , 'view' ) ;
2018-08-03 11:35:55 +00:00
2018-09-22 16:12:22 +00:00
let entity = await rawGetByTx ( tx , 'id' , id ) ;
2018-08-03 11:35:55 +00:00
2018-09-09 22:55:44 +00:00
if ( content === Content . ALL || content === Content . RSS _ENTRY ) {
// Return everything
} else if ( content === Content . SETTINGS _WITH _STATS ) {
delete entity . data . sourceCustom ;
await shares . enforceEntityPermissionTx ( tx , context , 'campaign' , id , 'viewStats' ) ;
2019-07-22 18:24:24 +00:00
const totalRes = await tx ( 'campaign_messages' )
. where ( { campaign : id } )
. whereIn ( 'status' , [ CampaignMessageStatus . SCHEDULED , CampaignMessageStatus . SENT ,
CampaignMessageStatus . COMPLAINED , CampaignMessageStatus . UNSUBSCRIBED , CampaignMessageStatus . BOUNCED ] )
. count ( '* as count' ) . first ( ) ;
entity . total = totalRes . count ;
2018-09-09 22:55:44 +00:00
} else if ( content === Content . WITHOUT _SOURCE _CUSTOM ) {
2018-08-03 11:35:55 +00:00
delete entity . data . sourceCustom ;
} else if ( content === Content . ONLY _SOURCE _CUSTOM ) {
entity = {
id : entity . id ,
2018-11-14 21:29:31 +00:00
send _configuration : entity . send _configuration ,
2018-08-03 11:35:55 +00:00
data : {
sourceCustom : entity . data . sourceCustom
}
} ;
}
2018-08-01 10:00:20 +00:00
2018-08-02 10:19:27 +00:00
if ( withPermissions ) {
2017-08-13 18:11:58 +00:00
entity . permissions = await shares . getPermissionsTx ( tx , context , 'campaign' , id ) ;
2018-08-02 10:19:27 +00:00
}
return entity ;
}
2018-08-01 10:00:20 +00:00
2018-08-03 11:35:55 +00:00
async function getById ( context , id , withPermissions = true , content = Content . ALL ) {
2018-08-02 10:19:27 +00:00
return await knex . transaction ( async tx => {
2018-08-03 11:35:55 +00:00
return await getByIdTx ( tx , context , id , withPermissions , content ) ;
2017-08-13 18:11:58 +00:00
} ) ;
2017-07-13 11:27:03 +00:00
}
2017-07-11 09:28:44 +00:00
2018-08-03 11:35:55 +00:00
async function _validateAndPreprocess ( tx , context , entity , isCreate , content ) {
2018-09-02 12:59:02 +00:00
if ( content === Content . ALL || content === Content . WITHOUT _SOURCE _CUSTOM || content === Content . RSS _ENTRY ) {
2018-08-03 11:35:55 +00:00
await namespaceHelpers . validateEntity ( tx , entity ) ;
2018-07-31 04:34:28 +00:00
2018-08-03 11:35:55 +00:00
if ( isCreate ) {
2018-09-02 12:59:02 +00:00
enforce ( entity . type === CampaignType . REGULAR || entity . type === CampaignType . RSS || entity . type === CampaignType . TRIGGERED ||
( content === Content . RSS _ENTRY && entity . type === CampaignType . RSS _ENTRY ) ,
'Unknown campaign type' ) ;
2018-08-01 10:00:20 +00:00
2018-08-03 11:35:55 +00:00
if ( entity . source === CampaignSource . TEMPLATE || entity . source === CampaignSource . CUSTOM _FROM _TEMPLATE ) {
await shares . enforceEntityPermissionTx ( tx , context , 'template' , entity . data . sourceTemplate , 'view' ) ;
}
enforce ( Number . isInteger ( entity . source ) ) ;
enforce ( entity . source >= CampaignSource . MIN && entity . source <= CampaignSource . MAX , 'Unknown campaign source' ) ;
2018-08-01 10:00:20 +00:00
}
2018-08-03 11:35:55 +00:00
2018-09-02 18:17:42 +00:00
for ( const lstSeg of entity . lists ) {
await shares . enforceEntityPermissionTx ( tx , context , 'list' , lstSeg . list , 'view' ) ;
2018-08-03 11:35:55 +00:00
2018-09-02 18:17:42 +00:00
if ( lstSeg . segment ) {
// Check that the segment under the list exists
await segments . getByIdTx ( tx , context , lstSeg . list , lstSeg . segment ) ;
}
2018-08-03 11:35:55 +00:00
}
await shares . enforceEntityPermissionTx ( tx , context , 'sendConfiguration' , entity . send _configuration , 'viewPublic' ) ;
2018-07-31 04:34:28 +00:00
}
2019-07-03 09:58:58 +00:00
if ( ( isCreate && entity . source === CampaignSource . CUSTOM ) || ( content === Content . ONLY _SOURCE _CUSTOM ) ) {
enforce ( allTagLanguages . includes ( entity . data . sourceCustom . tag _language ) , ` Invalid tag language ' ${ entity . data . sourceCustom . tag _language } ' ` ) ;
}
2018-08-03 11:35:55 +00:00
}
2018-09-02 12:59:02 +00:00
async function _createTx ( tx , context , entity , content ) {
2018-07-31 04:34:28 +00:00
return await knex . transaction ( async tx => {
await shares . enforceEntityPermissionTx ( tx , context , 'namespace' , entity . namespace , 'createCampaign' ) ;
2018-08-02 10:19:27 +00:00
let copyFilesFrom = null ;
2018-07-31 04:34:28 +00:00
if ( entity . source === CampaignSource . CUSTOM _FROM _TEMPLATE ) {
2018-08-02 10:19:27 +00:00
copyFilesFrom = {
entityType : 'template' ,
entityId : entity . data . sourceTemplate
} ;
const template = await templates . getByIdTx ( tx , context , entity . data . sourceTemplate , false ) ;
entity . data . sourceCustom = {
type : template . type ,
2019-07-03 09:58:58 +00:00
tag _language : template . tag _language ,
2018-08-02 10:19:27 +00:00
data : template . data ,
html : template . html ,
text : template . text
} ;
2018-08-03 11:35:55 +00:00
2018-08-02 10:19:27 +00:00
} else if ( entity . source === CampaignSource . CUSTOM _FROM _CAMPAIGN ) {
copyFilesFrom = {
entityType : 'campaign' ,
entityId : entity . data . sourceCampaign
} ;
const sourceCampaign = await getByIdTx ( tx , context , entity . data . sourceCampaign , false ) ;
2018-08-03 11:35:55 +00:00
enforce ( sourceCampaign . source === CampaignSource . CUSTOM || sourceCampaign . source === CampaignSource . CUSTOM _FROM _TEMPLATE || sourceCampaign . source === CampaignSource . CUSTOM _FROM _CAMPAIGN , 'Incorrect source type of the source campaign.' ) ;
2018-08-02 10:19:27 +00:00
entity . data . sourceCustom = sourceCampaign . data . sourceCustom ;
2018-07-31 04:34:28 +00:00
}
2018-09-02 12:59:02 +00:00
await _validateAndPreprocess ( tx , context , entity , true , content ) ;
2018-07-31 04:34:28 +00:00
2018-11-17 01:54:23 +00:00
const filteredEntity = filterObject ( entity , entity . type === CampaignType . RSS _ENTRY ? allowedKeysCreateRssEntry : allowedKeysCreate ) ;
2018-07-31 04:34:28 +00:00
filteredEntity . cid = shortid . generate ( ) ;
2018-08-03 11:35:55 +00:00
const data = filteredEntity . data ;
filteredEntity . data = JSON . stringify ( filteredEntity . data ) ;
2018-11-17 01:54:23 +00:00
if ( filteredEntity . type === CampaignType . RSS || filteredEntity . type === CampaignType . TRIGGERED ) {
filteredEntity . status = CampaignStatus . ACTIVE ;
} else if ( filteredEntity . type === CampaignType . RSS _ENTRY ) {
filteredEntity . status = CampaignStatus . SCHEDULED ;
} else {
filteredEntity . status = CampaignStatus . IDLE ;
}
2018-07-31 04:34:28 +00:00
const ids = await tx ( 'campaigns' ) . insert ( filteredEntity ) ;
const id = ids [ 0 ] ;
2018-09-02 18:17:42 +00:00
await tx ( 'campaign_lists' ) . insert ( entity . lists . map ( x => ( { campaign : id , ... x } ) ) ) ;
2018-09-29 11:30:29 +00:00
if ( entity . source === CampaignSource . TEMPLATE ) {
await tx ( 'template_dep_campaigns' ) . insert ( {
campaign : id ,
template : entity . data . sourceTemplate
} ) ;
}
2018-11-17 01:54:23 +00:00
if ( filteredEntity . parent ) {
await shares . rebuildPermissionsTx ( tx , { entityTypeId : 'campaign' , entityId : id , parentId : filteredEntity . parent } ) ;
} else {
await shares . rebuildPermissionsTx ( tx , { entityTypeId : 'campaign' , entityId : id } ) ;
}
2018-07-31 04:34:28 +00:00
2018-08-02 10:19:27 +00:00
if ( copyFilesFrom ) {
2018-08-03 11:35:55 +00:00
await files . copyAllTx ( tx , context , copyFilesFrom . entityType , 'file' , copyFilesFrom . entityId , 'campaign' , 'file' , id ) ;
2019-08-07 12:29:58 +00:00
convertFileURLs ( data . sourceCustom , copyFilesFrom . entityType , copyFilesFrom . entityId , 'campaign' , id ) ;
2018-08-03 11:35:55 +00:00
await tx ( 'campaigns' )
. update ( {
data : JSON . stringify ( data )
} ) . where ( 'id' , id ) ;
2018-07-31 04:34:28 +00:00
}
2019-02-07 14:38:32 +00:00
await activityLog . logEntityActivity ( 'campaign' , EntityActivityType . CREATE , id , { status : filteredEntity . status } ) ;
2018-07-31 04:34:28 +00:00
return id ;
} ) ;
}
2018-09-02 12:59:02 +00:00
async function create ( context , entity ) {
return await knex . transaction ( async tx => {
return await _createTx ( tx , context , entity , Content . ALL ) ;
} ) ;
}
async function createRssTx ( tx , context , entity ) {
return await _createTx ( tx , context , entity , Content . RSS _ENTRY ) ;
}
2018-08-03 11:35:55 +00:00
async function updateWithConsistencyCheck ( context , entity , content ) {
2018-07-31 04:34:28 +00:00
await knex . transaction ( async tx => {
await shares . enforceEntityPermissionTx ( tx , context , 'campaign' , entity . id , 'edit' ) ;
2018-09-22 16:12:22 +00:00
const existing = await rawGetByTx ( tx , 'id' , entity . id ) ;
2018-07-31 04:34:28 +00:00
2018-08-03 11:35:55 +00:00
const existingHash = hash ( existing , content ) ;
2018-07-31 04:34:28 +00:00
if ( existingHash !== entity . originalHash ) {
throw new interoperableErrors . ChangedError ( ) ;
}
2018-08-03 11:35:55 +00:00
await _validateAndPreprocess ( tx , context , entity , false , content ) ;
2018-07-31 04:34:28 +00:00
2018-08-03 11:35:55 +00:00
let filteredEntity = filterObject ( entity , allowedKeysUpdate ) ;
if ( content === Content . ALL ) {
await namespaceHelpers . validateMove ( context , entity , existing , 'campaign' , 'createCampaign' , 'delete' ) ;
} else if ( content === Content . WITHOUT _SOURCE _CUSTOM ) {
filteredEntity . data . sourceCustom = existing . data . sourceCustom ;
2019-07-23 16:44:35 +00:00
await namespaceHelpers . validateMove ( context , entity , existing , 'campaign' , 'createCampaign' , 'delete' ) ;
2018-08-03 11:35:55 +00:00
} else if ( content === Content . ONLY _SOURCE _CUSTOM ) {
const data = existing . data ;
data . sourceCustom = filteredEntity . data . sourceCustom ;
filteredEntity = {
data
} ;
}
2018-07-31 04:34:28 +00:00
2018-09-27 10:34:54 +00:00
if ( content === Content . ALL || content === Content . WITHOUT _SOURCE _CUSTOM ) {
await tx ( 'campaign_lists' ) . where ( 'campaign' , entity . id ) . del ( ) ;
await tx ( 'campaign_lists' ) . insert ( entity . lists . map ( x => ( { campaign : entity . id , ... x } ) ) ) ;
2018-09-29 11:30:29 +00:00
if ( existing . source === CampaignSource . TEMPLATE ) {
await tx ( 'template_dep_campaigns' )
. where ( 'campaign' , entity . id )
. update ( 'template' , entity . data . sourceTemplate ) ;
}
2018-09-27 10:34:54 +00:00
}
2018-08-03 11:35:55 +00:00
filteredEntity . data = JSON . stringify ( filteredEntity . data ) ;
await tx ( 'campaigns' ) . where ( 'id' , entity . id ) . update ( filteredEntity ) ;
2018-07-31 04:34:28 +00:00
await shares . rebuildPermissionsTx ( tx , { entityTypeId : 'campaign' , entityId : entity . id } ) ;
2019-02-07 14:38:32 +00:00
await activityLog . logEntityActivity ( 'campaign' , EntityActivityType . UPDATE , entity . id , { status : filteredEntity . status } ) ;
2018-07-31 04:34:28 +00:00
} ) ;
}
2018-11-17 01:54:23 +00:00
async function _removeTx ( tx , context , id , existing = null ) {
await shares . enforceEntityPermissionTx ( tx , context , 'campaign' , id , 'delete' ) ;
2018-07-31 04:34:28 +00:00
2018-11-17 01:54:23 +00:00
if ( ! existing ) {
existing = await tx ( 'campaigns' ) . where ( 'id' , id ) . select ( [ 'id' , 'status' , 'type' ] ) . first ( ) ;
}
if ( existing . status === CampaignStatus . SENDING ) {
return new interoperableErrors . InvalidStateError ;
}
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 files . removeAllTx ( tx , context , 'campaign' , 'file' , id ) ;
await files . removeAllTx ( tx , context , 'campaign' , 'attachment' , id ) ;
await tx ( 'campaign_lists' ) . where ( 'campaign' , id ) . del ( ) ;
await tx ( 'campaign_messages' ) . where ( 'campaign' , id ) . del ( ) ;
await tx ( 'campaign_links' ) . where ( 'campaign' , id ) . del ( ) ;
2018-09-09 22:55:44 +00:00
2018-12-16 12:47:08 +00:00
await tx ( 'links' ) . where ( 'campaign' , id ) . del ( ) ;
2018-11-17 01:54:23 +00:00
await triggers . removeAllByCampaignIdTx ( tx , context , id ) ;
2018-09-29 18:08:49 +00:00
2018-11-17 01:54:23 +00:00
await tx ( 'template_dep_campaigns' )
. where ( 'campaign' , id )
. del ( ) ;
2018-08-01 10:00:20 +00:00
2018-11-17 01:54:23 +00:00
await tx ( 'campaigns' ) . where ( 'id' , id ) . del ( ) ;
2019-02-07 14:38:32 +00:00
await activityLog . logEntityActivity ( 'campaign' , EntityActivityType . REMOVE , id ) ;
2018-11-17 01:54:23 +00:00
}
2018-08-03 16:07:46 +00:00
2018-09-29 11:30:29 +00:00
2018-11-17 01:54:23 +00:00
async function remove ( context , id ) {
await knex . transaction ( async tx => {
await _removeTx ( tx , context , id ) ;
2018-07-31 04:34:28 +00:00
} ) ;
}
2019-06-25 05:18:06 +00:00
async function enforceSendPermissionTx ( tx , context , campaignOrCampaignId , isToTestUsers , listId ) {
2019-01-12 10:21:38 +00:00
let campaign ;
2019-06-25 05:18:06 +00:00
if ( typeof campaignOrCampaignId === 'object' ) {
campaign = campaignOrCampaignId ;
2019-01-12 10:21:38 +00:00
} else {
2019-06-25 05:18:06 +00:00
campaign = await getByIdTx ( tx , context , campaignOrCampaignId , false ) ;
2019-01-12 10:21:38 +00:00
}
const sendConfiguration = await sendConfigurations . getByIdTx ( tx , contextHelpers . getAdminContext ( ) , campaign . send _configuration , false , false ) ;
2018-08-03 16:07:46 +00:00
2019-06-25 05:18:06 +00:00
const requiredSendConfigurationPermission = getSendConfigurationPermissionRequiredForSend ( campaign , sendConfiguration ) ;
await shares . enforceEntityPermissionTx ( tx , context , 'sendConfiguration' , campaign . send _configuration , requiredSendConfigurationPermission ) ;
2018-08-03 16:07:46 +00:00
2019-06-25 05:18:06 +00:00
const requiredListAndCampaignPermission = isToTestUsers ? 'sendToTestUsers' : 'send' ;
await shares . enforceEntityPermissionTx ( tx , context , 'campaign' , campaign . id , requiredListAndCampaignPermission ) ;
if ( listId ) {
await shares . enforceEntityPermissionTx ( tx , context , 'list' , listId , requiredListAndCampaignPermission ) ;
} else {
for ( const listIds of campaign . lists ) {
await shares . enforceEntityPermissionTx ( tx , context , 'list' , listIds . list , requiredListAndCampaignPermission ) ;
}
}
2018-08-03 16:07:46 +00:00
}
2018-07-31 04:34:28 +00:00
2018-09-09 22:55:44 +00:00
// Message API
function getMessageCid ( campaignCid , listCid , subscriptionCid ) {
return [ campaignCid , listCid , subscriptionCid ] . join ( '.' )
}
2018-12-25 21:46:52 +00:00
async function getMessageByCid ( messageCid , withVerpHostname = false ) { // withVerpHostname is used by verp-server.js
2018-11-14 21:29:31 +00:00
const messageCidElems = messageCid . split ( '.' ) ;
2018-09-09 22:55:44 +00:00
2018-11-14 21:29:31 +00:00
if ( messageCidElems . length !== 3 ) {
2018-09-09 22:55:44 +00:00
return null ;
}
2018-11-14 21:29:31 +00:00
const [ campaignCid , listCid , subscriptionCid ] = messageCidElems ;
2018-09-09 22:55:44 +00:00
2018-12-16 21:35:21 +00:00
return await knex . transaction ( async tx => {
2018-12-25 21:46:52 +00:00
const list = await tx ( 'lists' ) . where ( 'cid' , listCid ) . select ( 'id' ) . first ( ) ;
2018-09-09 22:55:44 +00:00
const subscrTblName = subscriptions . getSubscriptionTableName ( list . id ) ;
2018-12-25 21:46:52 +00:00
const baseQuery = tx ( 'campaign_messages' )
2018-09-09 22:55:44 +00:00
. innerJoin ( 'campaigns' , 'campaign_messages.campaign' , 'campaigns.id' )
. innerJoin ( subscrTblName , subscrTblName + '.id' , 'campaign_messages.subscription' )
. where ( subscrTblName + '.cid' , subscriptionCid )
. where ( 'campaigns.cid' , campaignCid )
. select ( [
2019-07-22 18:24:24 +00:00
'campaign_messages.id' , 'campaign_messages.campaign' , 'campaign_messages.list' , 'campaign_messages.subscription' , 'campaign_messages.hash_email' , 'campaign_messages.status'
2018-12-16 21:35:21 +00:00
] )
. first ( ) ;
2018-09-09 22:55:44 +00:00
2018-12-25 21:46:52 +00:00
if ( withVerpHostname ) {
return await baseQuery
. innerJoin ( 'send_configurations' , 'send_configurations.id' , 'campaigns.send_configuration' )
. select ( 'send_configurations.verp_hostname' ) ;
} else {
return await baseQuery ;
}
2018-09-09 22:55:44 +00:00
return message ;
} ) ;
}
async function getMessageByResponseId ( responseId ) {
2019-06-29 21:19:56 +00:00
return await knex ( 'campaign_messages' )
. where ( 'campaign_messages.response_id' , responseId )
. select ( [
2019-07-22 18:24:24 +00:00
'campaign_messages.id' , 'campaign_messages.campaign' , 'campaign_messages.list' , 'campaign_messages.subscription' , 'campaign_messages.hash_email' , 'campaign_messages.status'
2019-06-29 21:19:56 +00:00
] )
. first ( ) ;
2018-09-09 22:55:44 +00:00
}
2019-07-16 15:33:37 +00:00
const statusFieldMapping = new Map ( ) ;
statusFieldMapping . set ( CampaignMessageStatus . UNSUBSCRIBED , 'unsubscribed' ) ;
statusFieldMapping . set ( CampaignMessageStatus . BOUNCED , 'bounced' ) ;
statusFieldMapping . set ( CampaignMessageStatus . COMPLAINED , 'complained' ) ;
2018-09-09 22:55:44 +00:00
2019-07-16 15:33:37 +00:00
async function _changeStatusByMessageTx ( tx , context , message , campaignMessageStatus ) {
enforce ( statusFieldMapping . has ( campaignMessageStatus ) ) ;
2018-09-09 22:55:44 +00:00
2019-07-16 15:33:37 +00:00
if ( message . status === SubscriptionStatus . SENT ) {
2018-09-09 22:55:44 +00:00
await shares . enforceEntityPermissionTx ( tx , context , 'campaign' , message . campaign , 'manageMessages' ) ;
2019-07-16 15:33:37 +00:00
const statusField = statusFieldMapping . get ( campaignMessageStatus ) ;
2018-09-09 22:55:44 +00:00
2019-02-18 22:42:57 +00:00
await tx ( 'campaigns' ) . increment ( statusField , 1 ) . where ( 'id' , message . campaign ) ;
2018-09-09 22:55:44 +00:00
await tx ( 'campaign_messages' )
. where ( 'id' , message . id )
. update ( {
2019-07-16 15:33:37 +00:00
status : campaignMessageStatus ,
2018-09-09 22:55:44 +00:00
updated : knex . fn . now ( )
} ) ;
}
}
2019-07-16 15:33:37 +00:00
async function changeStatusByCampaignCidAndSubscriptionIdTx ( tx , context , campaignCid , listId , subscriptionId , campaignMessageStatus ) {
2018-09-09 22:55:44 +00:00
const message = await tx ( 'campaign_messages' )
. innerJoin ( 'campaigns' , 'campaign_messages.campaign' , 'campaigns.id' )
. where ( 'campaigns.cid' , campaignCid )
2019-02-18 22:42:57 +00:00
. where ( { subscription : subscriptionId , list : listId } )
. select ( [
2019-07-22 18:24:24 +00:00
'campaign_messages.id' , 'campaign_messages.campaign' , 'campaign_messages.list' , 'campaign_messages.subscription' , 'campaign_messages.hash_email' , 'campaign_messages.status'
2019-02-18 22:42:57 +00:00
] )
. first ( ) ;
2018-09-09 22:55:44 +00:00
2019-03-26 23:41:18 +00:00
if ( ! message ) {
throw new Error ( 'Invalid campaign.' ) ;
}
2018-09-09 22:55:44 +00:00
2019-07-16 15:33:37 +00:00
await _changeStatusByMessageTx ( tx , context , message , campaignMessageStatus ) ;
2018-09-09 22:55:44 +00:00
}
2019-07-16 15:33:37 +00:00
const campaignMessageStatusToSubscriptionStatusMapping = new Map ( ) ;
campaignMessageStatusToSubscriptionStatusMapping . set ( CampaignMessageStatus . BOUNCED , SubscriptionStatus . BOUNCED ) ;
campaignMessageStatusToSubscriptionStatusMapping . set ( CampaignMessageStatus . UNSUBSCRIBED , SubscriptionStatus . UNSUBSCRIBED ) ;
campaignMessageStatusToSubscriptionStatusMapping . set ( CampaignMessageStatus . COMPLAINED , SubscriptionStatus . COMPLAINED ) ;
async function changeStatusByMessage ( context , message , campaignMessageStatus , updateSubscription ) {
2018-09-09 22:55:44 +00:00
await knex . transaction ( async tx => {
if ( updateSubscription ) {
2019-07-16 15:33:37 +00:00
enforce ( campaignMessageStatusToSubscriptionStatusMapping . has ( campaignMessageStatus ) ) ;
await subscriptions . changeStatusTx ( tx , context , message . list , message . subscription , campaignMessageStatusToSubscriptionStatusMapping . get ( campaignMessageStatus ) ) ;
2018-09-09 22:55:44 +00:00
}
2019-07-16 15:33:37 +00:00
await _changeStatusByMessageTx ( tx , context , message , campaignMessageStatus ) ;
2018-09-09 22:55:44 +00:00
} ) ;
}
2018-09-18 09:03:36 +00:00
async function updateMessageResponse ( context , message , response , responseId ) {
await knex . transaction ( async tx => {
await shares . enforceEntityPermissionTx ( tx , context , 'campaign' , message . campaign , 'manageMessages' ) ;
await tx ( 'campaign_messages' ) . where ( 'id' , message . id ) . update ( {
response ,
response _id : responseId
} ) ;
} ) ;
}
2019-07-22 18:24:24 +00:00
async function prepareCampaignMessages ( campaignId ) {
const campaign = await getById ( contextHelpers . getAdminContext ( ) , campaignId , false ) ;
await knex ( 'campaign_messages' ) . where ( { campaign : campaignId , status : CampaignMessageStatus . SCHEDULED } ) . del ( ) ;
for ( const cpgList of campaign . lists ) {
let addSegmentQuery ;
await knex . transaction ( async tx => {
addSegmentQuery = cpgList . segment ? await segments . getQueryGeneratorTx ( tx , cpgList . list , cpgList . segment ) : ( ) => { } ;
} ) ;
2018-09-09 22:55:44 +00:00
const subsTable = subscriptions . getSubscriptionTableName ( cpgList . list ) ;
2019-07-22 18:24:24 +00:00
const subsQry = knex . from ( subsTable )
2018-09-10 18:15:59 +00:00
. where ( subsTable + '.status' , SubscriptionStatus . SUBSCRIBED )
. where ( function ( ) {
addSegmentQuery ( this ) ;
} )
2019-07-22 18:24:24 +00:00
. select ( [
'hash_email' ,
'id' ,
knex . raw ( '? AS campaign' , [ campaign . id ] ) ,
knex . raw ( '? AS list' , [ cpgList . list ] ) ,
knex . raw ( '? AS send_configuration' , [ campaign . send _configuration ] ) ,
knex . raw ( '? AS status' , [ CampaignMessageStatus . SCHEDULED ] )
] )
2018-09-10 18:15:59 +00:00
. toSQL ( ) . toNative ( ) ;
2019-07-22 18:24:24 +00:00
await knex . raw ( 'INSERT IGNORE INTO `campaign_messages` (`hash_email`, `subscription`, `campaign`, `list`, `send_configuration`, `status`) ' + subsQry . sql , subsQry . bindings ) ;
2018-09-09 22:55:44 +00:00
}
}
2019-07-09 22:06:56 +00:00
async function _changeStatus ( context , campaignId , permittedCurrentStates , newState , invalidStateMessage , extraData ) {
2018-09-09 22:55:44 +00:00
await knex . transaction ( async tx => {
2019-07-09 22:06:56 +00:00
// This is quite inefficient because it selects the same row 3 times. However as status is changed
// rather infrequently, we keep it this way for simplicity
await lockByIdTx ( tx , campaignId ) ;
2019-06-25 05:18:06 +00:00
const entity = await getByIdTx ( tx , context , campaignId , false ) ;
2018-09-09 22:55:44 +00:00
2019-06-25 05:18:06 +00:00
await enforceSendPermissionTx ( tx , context , entity , false ) ;
2019-01-12 10:21:38 +00:00
2018-09-09 22:55:44 +00:00
if ( ! permittedCurrentStates . includes ( entity . status ) ) {
throw new interoperableErrors . InvalidStateError ( invalidStateMessage ) ;
}
2019-07-09 22:06:56 +00:00
if ( Array . isArray ( newState ) ) {
const newStateIdx = permittedCurrentStates . indexOf ( entity . status ) ;
enforce ( newStateIdx != - 1 ) ;
newState = newState [ newStateIdx ] ;
}
2019-06-29 21:19:56 +00:00
const updateData = {
2019-07-09 22:06:56 +00:00
status : newState
2019-06-29 21:19:56 +00:00
} ;
2019-07-09 22:06:56 +00:00
if ( ! extraData ) {
updateData . scheduled = null ;
updateData . start _at = null ;
} else {
const startAt = extraData . startAt ;
// If campaign is started without "scheduled" specified, startAt === null
2019-06-29 21:19:56 +00:00
updateData . scheduled = startAt ;
if ( ! startAt || startAt . valueOf ( ) < Date . now ( ) ) {
updateData . start _at = new Date ( ) ;
2019-07-09 22:06:56 +00:00
} else {
updateData . start _at = startAt ;
}
const timezone = extraData . timezone ;
if ( timezone ) {
updateData . data = JSON . stringify ( {
... entity . data ,
timezone
} ) ;
2019-06-29 21:19:56 +00:00
}
}
await tx ( 'campaigns' ) . where ( 'id' , campaignId ) . update ( updateData ) ;
2019-02-07 14:38:32 +00:00
await activityLog . logEntityActivity ( 'campaign' , CampaignActivityType . STATUS _CHANGE , campaignId , { status : newState } ) ;
2018-09-09 22:55:44 +00:00
} ) ;
senders . scheduleCheck ( ) ;
}
2019-07-09 22:06:56 +00:00
async function start ( context , campaignId , extraData ) {
await _changeStatus ( context , campaignId , [ CampaignStatus . IDLE , CampaignStatus . SCHEDULED , CampaignStatus . PAUSED , CampaignStatus . FINISHED ] , CampaignStatus . SCHEDULED , 'Cannot start campaign until it is in IDLE, PAUSED, or FINISHED state' , extraData ) ;
2018-09-09 22:55:44 +00:00
}
async function stop ( context , campaignId ) {
2019-07-09 22:06:56 +00:00
await _changeStatus ( context , campaignId , [ CampaignStatus . SCHEDULED , CampaignStatus . SENDING ] , [ CampaignStatus . PAUSED , CampaignStatus . PAUSING ] , 'Cannot stop campaign until it is in SCHEDULED or SENDING state' ) ;
2018-09-09 22:55:44 +00:00
}
async function reset ( context , campaignId ) {
await knex . transaction ( async tx => {
2019-07-09 22:06:56 +00:00
// This is quite inefficient because it selects the same row 3 times. However as RESET is
// going to be called rather infrequently, we keep it this way for simplicity
await lockByIdTx ( tx , campaignId ) ;
2018-09-09 22:55:44 +00:00
await shares . enforceEntityPermissionTx ( tx , context , 'campaign' , campaignId , 'send' ) ;
const entity = await tx ( 'campaigns' ) . where ( 'id' , campaignId ) . first ( ) ;
if ( entity . status !== CampaignStatus . FINISHED && entity . status !== CampaignStatus . PAUSED ) {
throw new interoperableErrors . InvalidStateError ( 'Cannot reset campaign until it is FINISHED or PAUSED state' ) ;
}
await tx ( 'campaigns' ) . where ( 'id' , campaignId ) . update ( {
2018-12-16 12:47:08 +00:00
status : CampaignStatus . IDLE ,
delivered : 0 ,
unsubscribed : 0 ,
bounced : 0 ,
complained : 0 ,
blacklisted : 0 ,
opened : 0 ,
clicks : 0
2018-09-09 22:55:44 +00:00
} ) ;
await tx ( 'campaign_messages' ) . where ( 'campaign' , campaignId ) . del ( ) ;
await tx ( 'campaign_links' ) . where ( 'campaign' , campaignId ) . del ( ) ;
2018-12-16 12:47:08 +00:00
await tx ( 'links' ) . where ( 'campaign' , campaignId ) . del ( ) ;
2018-09-09 22:55:44 +00:00
} ) ;
}
2018-11-17 01:54:23 +00:00
async function enable ( context , campaignId ) {
await _changeStatus ( context , campaignId , [ CampaignStatus . INACTIVE ] , CampaignStatus . ACTIVE , 'Cannot enable campaign unless it is in INACTIVE state' ) ;
}
async function disable ( context , campaignId ) {
await _changeStatus ( context , campaignId , [ CampaignStatus . ACTIVE ] , CampaignStatus . INACTIVE , 'Cannot disable campaign unless it is in ACTIVE state' ) ;
}
2018-12-16 12:47:08 +00:00
async function getStatisticsOpened ( context , id ) {
return await knex . transaction ( async tx => {
await shares . enforceEntityPermissionTx ( tx , context , 'campaign' , id , 'viewStats' ) ;
2019-06-29 21:19:56 +00:00
const devices = await tx ( 'campaign_links' ) . where ( 'campaign' , id ) . where ( 'link' , links . LinkId . OPEN ) . groupBy ( 'device_type' ) . select ( 'device_type AS key' ) . count ( '* as count' ) ;
const countries = await tx ( 'campaign_links' ) . where ( 'campaign' , id ) . where ( 'link' , links . LinkId . OPEN ) . groupBy ( 'country' ) . select ( 'country AS key' ) . count ( '* as count' ) ;
2018-12-16 12:47:08 +00:00
return {
devices ,
countries
} ;
} ) ;
}
2018-11-17 01:54:23 +00:00
2018-12-29 14:12:42 +00:00
async function fetchRssCampaign ( context , cid ) {
return await knex . transaction ( async tx => {
const campaign = await tx ( 'campaigns' ) . where ( 'cid' , cid ) . select ( [ 'id' , 'type' ] ) . first ( ) ;
await shares . enforceEntityPermissionTx ( tx , context , 'campaign' , campaign . id , 'fetchRss' ) ;
enforce ( campaign . type === CampaignType . RSS , 'Invalid campaign type' ) ;
await tx ( 'campaigns' ) . where ( 'id' , campaign . id ) . update ( 'last_check' , null ) ;
feedcheck . scheduleCheck ( ) ;
} ) ;
}
2019-06-25 05:18:06 +00:00
async function testSend ( context , data ) {
2019-08-01 05:46:40 +00:00
// Though it's a bit counter-intuitive, this handles also test sends of a template (i.e. without any campaign id)
2019-06-25 05:18:06 +00:00
await knex . transaction ( async tx => {
const processSubscriber = async ( sendConfigurationId , listId , subscriptionId , messageData ) => {
2019-06-29 21:19:56 +00:00
await messageSender . queueCampaignMessageTx ( tx , sendConfigurationId , listId , subscriptionId , messageSender . MessageType . TEST , messageData ) ;
2019-06-25 05:18:06 +00:00
await activityLog . logEntityActivity ( 'campaign' , CampaignActivityType . TEST _SEND , campaignId , { list : listId , subscription : subscriptionId } ) ;
} ;
const campaignId = data . campaignId ;
if ( campaignId ) { // This means we are sending a campaign
/ *
Data coming from the client :
- html , text
- subjectPrepend , subjectAppend
- listCid , subscriptionCid
- listId , segmentId
* /
const campaign = await getByIdTx ( tx , context , campaignId , false ) ;
const sendConfigurationId = campaign . send _configuration ;
const messageData = {
campaignId : campaignId ,
subject : data . subjectPrepend + campaign . subject + data . subjectAppend ,
2019-07-05 21:23:02 +00:00
html : data . html , // The html, text and tagLanguage may be undefined
2019-06-25 05:18:06 +00:00
text : data . text ,
2019-07-05 21:23:02 +00:00
tagLanguage : data . tagLanguage ,
2019-06-25 05:18:06 +00:00
attachments : [ ]
} ;
const attachments = await files . listTx ( tx , contextHelpers . getAdminContext ( ) , 'campaign' , 'attachment' , campaignId ) ;
for ( const attachment of attachments ) {
messageData . attachments . push ( {
filename : attachment . originalname ,
path : files . getFilePath ( 'campaign' , 'attachment' , campaign . id , attachment . filename ) ,
id : attachment . id
} ) ;
}
let listId = data . listId ;
if ( ! listId && data . listCid ) {
const list = await lists . getByCidTx ( tx , context , data . listCid ) ;
listId = list . id ;
}
const segmentId = data . segmentId ;
if ( listId ) {
await enforceSendPermissionTx ( tx , context , campaign , true , listId ) ;
if ( data . subscriptionCid ) {
2019-08-01 05:46:40 +00:00
const subscriber = await subscriptions . getByCidTx ( tx , context , listId , data . subscriptionCid , true , true ) ;
2019-06-25 05:18:06 +00:00
await processSubscriber ( sendConfigurationId , listId , subscriber . id , messageData ) ;
} else {
const subscribers = await subscriptions . listTestUsersTx ( tx , context , listId , segmentId ) ;
for ( const subscriber of subscribers ) {
await processSubscriber ( sendConfigurationId , listId , subscriber . id , messageData ) ;
}
}
} else {
for ( const lstSeg of campaign . lists ) {
await enforceSendPermissionTx ( tx , context , campaign , true , lstSeg . list ) ;
const subscribers = await subscriptions . listTestUsersTx ( tx , context , lstSeg . list , segmentId ) ;
for ( const subscriber of subscribers ) {
await processSubscriber ( sendConfigurationId , lstSeg . list , subscriber . id , messageData ) ;
}
}
}
} else { // This means we are sending a template
/ *
Data coming from the client :
- html , text
- listCid , subscriptionCid , sendConfigurationId
* /
const messageData = {
subject : 'Test' ,
html : data . html ,
2019-07-05 21:23:02 +00:00
text : data . text ,
tagLanguage : data . tagLanguage
2019-06-25 05:18:06 +00:00
} ;
const list = await lists . getByCidTx ( tx , context , data . listCid ) ;
2019-08-01 05:46:40 +00:00
const subscriber = await subscriptions . getByCidTx ( tx , context , list . id , data . subscriptionCid , true , true ) ;
2019-06-25 05:18:06 +00:00
await processSubscriber ( data . sendConfigurationId , list . id , subscriber . id , messageData ) ;
}
} ) ;
senders . scheduleCheck ( ) ;
}
2018-09-09 22:55:44 +00:00
module . exports . Content = Content ;
module . exports . hash = hash ;
2018-12-16 12:47:08 +00:00
2018-09-09 22:55:44 +00:00
module . exports . listDTAjax = listDTAjax ;
2019-03-13 08:52:02 +00:00
module . exports . listByNamespaceDTAjax = listByNamespaceDTAjax ;
2018-11-17 01:54:23 +00:00
module . exports . listChildrenDTAjax = listChildrenDTAjax ;
2018-09-09 22:55:44 +00:00
module . exports . listWithContentDTAjax = listWithContentDTAjax ;
module . exports . listOthersWhoseListsAreIncludedDTAjax = listOthersWhoseListsAreIncludedDTAjax ;
module . exports . listTestUsersDTAjax = listTestUsersDTAjax ;
2018-12-16 12:47:08 +00:00
module . exports . listSentByStatusDTAjax = listSentByStatusDTAjax ;
module . exports . listOpensDTAjax = listOpensDTAjax ;
module . exports . listLinkClicksDTAjax = listLinkClicksDTAjax ;
2018-09-09 22:55:44 +00:00
module . exports . getByIdTx = getByIdTx ;
module . exports . getById = getById ;
module . exports . create = create ;
module . exports . createRssTx = createRssTx ;
module . exports . updateWithConsistencyCheck = updateWithConsistencyCheck ;
module . exports . remove = remove ;
module . exports . enforceSendPermissionTx = enforceSendPermissionTx ;
module . exports . getMessageCid = getMessageCid ;
module . exports . getMessageByCid = getMessageByCid ;
module . exports . getMessageByResponseId = getMessageByResponseId ;
module . exports . changeStatusByCampaignCidAndSubscriptionIdTx = changeStatusByCampaignCidAndSubscriptionIdTx ;
module . exports . changeStatusByMessage = changeStatusByMessage ;
2018-09-18 09:03:36 +00:00
module . exports . updateMessageResponse = updateMessageResponse ;
2018-09-09 22:55:44 +00:00
2019-07-22 18:24:24 +00:00
module . exports . prepareCampaignMessages = prepareCampaignMessages ;
2018-09-09 22:55:44 +00:00
module . exports . start = start ;
module . exports . stop = stop ;
2018-09-22 16:12:22 +00:00
module . exports . reset = reset ;
2018-11-17 01:54:23 +00:00
module . exports . enable = enable ;
module . exports . disable = disable ;
2018-09-22 16:12:22 +00:00
2018-09-23 20:28:58 +00:00
module . exports . rawGetByTx = rawGetByTx ;
2018-12-16 12:47:08 +00:00
module . exports . getTrackingSettingsByCidTx = getTrackingSettingsByCidTx ;
module . exports . getStatisticsOpened = getStatisticsOpened ;
2018-12-29 14:12:42 +00:00
2019-06-25 05:18:06 +00:00
module . exports . fetchRssCampaign = fetchRssCampaign ;
module . exports . testSend = testSend ;