2017-02-18 01:56:23 -07:00
'use strict' ;
2014-11-17 13:37:07 -05:00
2019-08-06 17:23:50 -04:00
const winston = require ( 'winston' ) ;
const nconf = require ( 'nconf' ) ;
2016-04-01 15:12:01 +03:00
2019-11-02 13:11:02 -04:00
const db = require ( '../database' ) ;
2019-08-06 17:23:50 -04:00
const batch = require ( '../batch' ) ;
const meta = require ( '../meta' ) ;
2020-04-01 21:57:28 -04:00
const user = require ( './index' ) ;
2019-08-06 17:23:50 -04:00
const topics = require ( '../topics' ) ;
2023-07-18 13:12:06 -04:00
const messaging = require ( '../messaging' ) ;
2019-08-06 17:23:50 -04:00
const plugins = require ( '../plugins' ) ;
const emailer = require ( '../emailer' ) ;
const utils = require ( '../utils' ) ;
2014-11-17 13:37:07 -05:00
2019-08-06 17:23:50 -04:00
const Digest = module . exports ;
2016-11-23 15:52:35 +03:00
2021-02-10 17:41:39 -05:00
const baseUrl = nconf . get ( 'base_url' ) ;
2019-07-14 23:11:16 -04:00
Digest . execute = async function ( payload ) {
const digestsDisabled = meta . config . disableEmailSubscriptions === 1 ;
2017-05-16 17:14:50 -04:00
if ( digestsDisabled ) {
2021-02-03 23:59:08 -07:00
winston . info ( ` [user/jobs] Did not send digests ( ${ payload . interval } ) because subscription system is disabled. ` ) ;
2023-05-11 20:21:57 -04:00
return false ;
2019-07-14 23:11:16 -04:00
}
2021-02-06 14:10:15 -07:00
let { subscribers } = payload ;
2019-07-14 23:11:16 -04:00
if ( ! subscribers ) {
subscribers = await Digest . getSubscribers ( payload . interval ) ;
}
if ( ! subscribers . length ) {
2023-05-11 20:21:57 -04:00
return false ;
2019-07-14 23:11:16 -04:00
}
try {
2021-02-09 15:27:08 -05:00
winston . info ( ` [user/jobs] Digest ( ${ payload . interval } ) scheduling completed ( ${ subscribers . length } subscribers). Sending emails; this may take some time... ` ) ;
2019-11-02 13:11:02 -04:00
await Digest . send ( {
2019-07-14 23:11:16 -04:00
interval : payload . interval ,
subscribers : subscribers ,
} ) ;
2021-02-03 23:59:08 -07:00
winston . info ( ` [user/jobs] Digest ( ${ payload . interval } ) complete. ` ) ;
2023-05-11 20:21:57 -04:00
return true ;
2019-07-14 23:11:16 -04:00
} catch ( err ) {
2021-02-03 23:59:08 -07:00
winston . error ( ` [user/jobs] Could not send digests ( ${ payload . interval } ) \n ${ err . stack } ` ) ;
2021-10-18 20:17:35 -04:00
throw err ;
2017-05-16 17:14:50 -04:00
}
} ;
2016-11-23 15:52:35 +03:00
2019-11-02 13:11:02 -04:00
Digest . getUsersInterval = async ( uids ) => {
2022-05-19 12:32:52 -04:00
// Checks whether user specifies digest setting, or false for system default setting
2019-11-02 13:11:02 -04:00
let single = false ;
if ( ! Array . isArray ( uids ) && ! isNaN ( parseInt ( uids , 10 ) ) ) {
uids = [ uids ] ;
single = true ;
}
2022-05-19 12:32:52 -04:00
const settings = await db . getObjects ( uids . map ( uid => ` user: ${ uid } :settings ` ) ) ;
const interval = uids . map ( ( uid , index ) => ( settings [ index ] && settings [ index ] . dailyDigestFreq ) || false ) ;
2019-12-11 10:25:16 -05:00
return single ? interval [ 0 ] : interval ;
2019-11-02 13:11:02 -04:00
} ;
2019-07-14 23:11:16 -04:00
Digest . getSubscribers = async function ( interval ) {
2021-02-04 00:06:15 -07:00
let subscribers = [ ] ;
2017-06-23 12:41:40 -04:00
2021-02-04 00:01:39 -07:00
await batch . processSortedSet ( 'users:joindate' , async ( uids ) => {
2019-07-14 23:11:16 -04:00
const settings = await user . getMultipleUserSettings ( uids ) ;
let subUids = [ ] ;
2021-02-04 00:01:39 -07:00
settings . forEach ( ( hash ) => {
2019-07-14 23:11:16 -04:00
if ( hash . dailyDigestFreq === interval ) {
subUids . push ( hash . uid ) ;
}
} ) ;
subUids = await user . bans . filterBanned ( subUids ) ;
subscribers = subscribers . concat ( subUids ) ;
2020-07-21 18:19:46 -04:00
} , {
interval : 1000 ,
batch : 500 ,
} ) ;
2019-07-14 23:11:16 -04:00
2020-11-20 16:06:26 -05:00
const results = await plugins . hooks . fire ( 'filter:digest.subscribers' , {
2019-07-14 23:11:16 -04:00
interval : interval ,
subscribers : subscribers ,
} ) ;
return results . subscribers ;
2017-05-16 17:14:50 -04:00
} ;
2014-11-17 13:37:07 -05:00
2019-07-14 23:11:16 -04:00
Digest . send = async function ( data ) {
2020-07-21 18:19:46 -04:00
let emailsSent = 0 ;
2026-01-22 11:31:59 -05:00
let emailsFailed = 0 ;
2017-05-16 17:14:50 -04:00
if ( ! data || ! data . subscribers || ! data . subscribers . length ) {
2019-07-14 23:11:16 -04:00
return emailsSent ;
2017-05-16 17:14:50 -04:00
}
2022-01-28 15:25:33 -05:00
let errorLogged = false ;
2025-05-15 09:42:55 -04:00
const date = new Date ( ) ;
2021-02-09 15:27:08 -05:00
await batch . processArray ( data . subscribers , async ( uids ) => {
2025-05-15 09:38:43 -04:00
let userData = await user . getUsersFields ( uids , [
'uid' , 'email' , 'email:confirmed' , 'username' , 'userslug' , 'lastonline' ,
] ) ;
userData = userData . filter (
u => u && u . email && ( meta . config . includeUnverifiedEmails || u [ 'email:confirmed' ] )
) ;
2021-02-09 15:27:08 -05:00
if ( ! userData . length ) {
2019-07-14 23:11:16 -04:00
return ;
}
2025-05-15 09:38:43 -04:00
const userSettings = await user . getMultipleUserSettings ( userData . map ( u => u . uid ) ) ;
2026-01-22 11:31:59 -05:00
const successfullUids = [ ] ;
2025-05-15 09:38:43 -04:00
await Promise . all ( userData . map ( async ( userObj , index ) => {
const userSetting = userSettings [ index ] ;
2023-07-18 13:12:06 -04:00
const [ publicRooms , notifications , topics ] = await Promise . all ( [
getUnreadPublicRooms ( userObj . uid ) ,
2021-02-09 15:27:08 -05:00
user . notifications . getUnreadInterval ( userObj . uid , data . interval ) ,
2021-02-10 17:37:22 -05:00
getTermTopics ( data . interval , userObj . uid ) ,
2021-02-09 15:27:08 -05:00
] ) ;
const unreadNotifs = notifications . filter ( Boolean ) ;
2023-07-18 13:12:06 -04:00
// If there are no notifications and no new topics and no unread chats, don't bother sending a digest
if ( ! unreadNotifs . length &&
! topics . top . length && ! topics . popular . length && ! topics . recent . length &&
! publicRooms . length ) {
2021-02-09 15:27:08 -05:00
return ;
2019-07-14 23:11:16 -04:00
}
2017-07-06 15:42:37 -04:00
2021-02-09 15:27:08 -05:00
unreadNotifs . forEach ( ( n ) => {
if ( n . image && ! n . image . startsWith ( 'http' ) ) {
2021-02-10 17:41:39 -05:00
n . image = baseUrl + n . image ;
2021-02-09 15:27:08 -05:00
}
if ( n . path ) {
2021-02-10 17:41:39 -05:00
n . notification _url = n . path . startsWith ( 'http' ) ? n . path : baseUrl + n . path ;
2021-02-09 15:27:08 -05:00
}
} ) ;
2026-01-22 11:31:59 -05:00
try {
await emailer . send ( 'digest' , userObj . uid , {
subject : ` [[email:digest.subject, ${ date . toLocaleDateString ( userSetting . userLang ) } ]] ` ,
username : userObj . username ,
userslug : userObj . userslug ,
notifications : unreadNotifs ,
publicRooms : publicRooms ,
recent : topics . recent ,
topTopics : topics . top ,
popularTopics : topics . popular ,
interval : data . interval ,
showUnsubscribe : true ,
} ) ;
emailsSent += 1 ;
successfullUids . push ( userObj . uid ) ;
} catch ( err ) {
emailsFailed += 1 ;
2022-01-28 15:25:33 -05:00
if ( ! errorLogged ) {
winston . error ( ` [user/jobs] Could not send digest email \n [emailer.send] ${ err . stack } ` ) ;
errorLogged = true ;
}
2026-01-22 11:31:59 -05:00
}
2021-02-09 15:27:08 -05:00
} ) ) ;
2026-01-22 11:31:59 -05:00
if ( data . interval !== 'alltime' && successfullUids . length ) {
2021-02-10 00:58:07 -05:00
const now = Date . now ( ) ;
2026-01-22 11:31:59 -05:00
await db . sortedSetAdd ( 'digest:delivery' , successfullUids . map ( ( ) => now ) , successfullUids ) ;
2021-02-10 00:58:07 -05:00
}
2021-02-09 15:27:08 -05:00
} , {
interval : 1000 ,
batch : 100 ,
2017-05-16 17:14:50 -04:00
} ) ;
2026-01-22 11:31:59 -05:00
winston . info ( ` [user/jobs] Digest ( ${ data . interval } ) sending completed. ${ emailsSent } emails sent. ${ emailsFailed } failures. ` ) ;
2023-05-11 19:01:28 -04:00
return emailsSent ;
2019-11-02 13:11:02 -04:00
} ;
Digest . getDeliveryTimes = async ( start , stop ) => {
2026-01-22 11:49:16 -05:00
const [ count , uids ] = await Promise . all ( [
db . sortedSetCard ( 'users:joindate' ) ,
user . getUidsFromSet ( 'users:joindate' , start , stop ) ,
] ) ;
2022-05-19 12:32:52 -04:00
if ( ! uids . length ) {
2026-01-22 11:49:16 -05:00
return { users : [ ] , count } ;
2019-11-02 13:11:02 -04:00
}
2026-01-22 11:49:16 -05:00
const [ scores , settings , userData ] = await Promise . all ( [
2022-05-19 12:32:52 -04:00
// Grab the last time a digest was successfully delivered to these uids
db . sortedSetScores ( 'digest:delivery' , uids ) ,
// Get users' digest settings
Digest . getUsersInterval ( uids ) ,
2026-01-22 11:49:16 -05:00
user . getUsersFields ( uids , [ 'username' , 'picture' ] ) ,
2022-05-19 12:32:52 -04:00
] ) ;
2019-11-02 13:11:02 -04:00
2026-01-22 11:49:16 -05:00
userData . forEach ( ( user , idx ) => {
2019-11-02 13:11:02 -04:00
user . lastDelivery = scores [ idx ] ? new Date ( scores [ idx ] ) . toISOString ( ) : '[[admin/manage/digest:null]]' ;
user . setting = settings [ idx ] ;
} ) ;
return {
users : userData ,
count : count ,
} ;
2019-07-14 23:11:16 -04:00
} ;
2018-02-20 12:52:59 -05:00
2021-02-10 17:37:22 -05:00
async function getTermTopics ( term , uid ) {
const data = await topics . getSortedTopics ( {
2019-07-14 23:11:16 -04:00
uid : uid ,
2021-02-10 17:37:22 -05:00
start : 0 ,
stop : 199 ,
2019-07-14 23:11:16 -04:00
term : term ,
2024-04-02 11:32:41 -04:00
sort : 'posts' ,
2021-02-10 17:37:22 -05:00
teaserPost : 'first' ,
} ) ;
data . topics = data . topics . filter ( topic => topic && ! topic . deleted ) ;
const popular = data . topics
2024-04-02 11:32:41 -04:00
. filter ( t => t . postcount > 1 )
2021-02-10 17:37:22 -05:00
. sort ( ( a , b ) => b . postcount - a . postcount )
. slice ( 0 , 10 ) ;
const popularTids = popular . map ( t => t . tid ) ;
2024-04-02 11:32:41 -04:00
const top = data . topics
. filter ( t => t . votes > 0 && ! popularTids . includes ( t . tid ) )
. sort ( ( a , b ) => b . votes - a . votes )
. slice ( 0 , 10 ) ;
const topTids = top . map ( t => t . tid ) ;
2021-02-10 17:37:22 -05:00
const recent = data . topics
. filter ( t => ! topTids . includes ( t . tid ) && ! popularTids . includes ( t . tid ) )
. sort ( ( a , b ) => b . lastposttime - a . lastposttime )
. slice ( 0 , 10 ) ;
2020-10-01 22:07:33 -04:00
2021-02-10 17:37:22 -05:00
[ ... top , ... popular , ... recent ] . forEach ( ( topicObj ) => {
2020-10-01 22:07:33 -04:00
if ( topicObj ) {
if ( topicObj . teaser && topicObj . teaser . content && topicObj . teaser . content . length > 255 ) {
2021-02-03 23:59:08 -07:00
topicObj . teaser . content = ` ${ topicObj . teaser . content . slice ( 0 , 255 ) } ... ` ;
2020-10-01 22:07:33 -04:00
}
// Fix relative paths in topic data
const user = topicObj . hasOwnProperty ( 'teaser' ) && topicObj . teaser && topicObj . teaser . user ?
topicObj . teaser . user : topicObj . user ;
if ( user && user . picture && utils . isRelativeUrl ( user . picture ) ) {
2021-02-10 17:41:39 -05:00
user . picture = baseUrl + user . picture ;
2020-10-01 22:07:33 -04:00
}
2019-07-14 23:11:16 -04:00
}
} ) ;
2021-02-10 17:37:22 -05:00
return { top , popular , recent } ;
2019-07-14 23:11:16 -04:00
}
2023-07-18 13:12:06 -04:00
async function getUnreadPublicRooms ( uid ) {
const publicRooms = await messaging . getPublicRooms ( uid , uid ) ;
return publicRooms . filter ( r => r && r . unread ) ;
}