mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-25 00:28:31 -05:00
803 lines
35 KiB
CoffeeScript
803 lines
35 KiB
CoffeeScript
mail = require '../commons/mail'
|
|
MailSent = require '../mail/sent/MailSent'
|
|
UserRemark = require '../users/remarks/UserRemark'
|
|
User = require '../users/User'
|
|
async = require 'async'
|
|
errors = require '../commons/errors'
|
|
config = require '../../server_config'
|
|
LevelSession = require '../levels/sessions/LevelSession'
|
|
Level = require '../levels/Level'
|
|
log = require 'winston'
|
|
sendwithus = require '../sendwithus'
|
|
if config.isProduction and config.redis.host isnt 'localhost'
|
|
lockManager = require '../commons/LockManager'
|
|
|
|
module.exports.setup = (app) ->
|
|
app.all config.mail.mailchimpWebhook, handleMailchimpWebHook
|
|
app.get '/mail/cron/ladder-update', handleLadderUpdate
|
|
app.get '/mail/cron/next-steps', handleNextSteps
|
|
if lockManager
|
|
setupScheduledEmails()
|
|
|
|
setupScheduledEmails = ->
|
|
testForLockManager()
|
|
mailTasks = [
|
|
# taskFunction: candidateUpdateProfileTask
|
|
# frequencyMs: 10 * 60 * 1000 #10 minutes
|
|
#,
|
|
# taskFunction: internalCandidateUpdateTask
|
|
# frequencyMs: 10 * 60 * 1000 #10 minutes
|
|
#,
|
|
# taskFunction: employerNewCandidatesAvailableTask
|
|
# frequencyMs: 10 * 60 * 1000 #10 minutes
|
|
#,
|
|
# taskFunction: unapprovedCandidateFinishProfileTask
|
|
# frequencyMs: 10 * 60 * 1000
|
|
#,
|
|
# taskFunction: emailUserRemarkTaskRemindersTask
|
|
# frequencyMs: 10 * 60 * 1000
|
|
]
|
|
|
|
for mailTask in mailTasks
|
|
setInterval mailTask.taskFunction, mailTask.frequencyMs
|
|
|
|
testForLockManager = -> unless lockManager then throw "The system isn't configured to do distributed locking!"
|
|
|
|
### Approved Candidate Update Reminder Task ###
|
|
candidateUpdateProfileTask = ->
|
|
mailTaskName = "candidateUpdateProfileTask"
|
|
lockDurationMs = 2 * 60 * 1000
|
|
currentDate = new Date()
|
|
timeRanges = []
|
|
for weekPair in [[4, 2,'two weeks'], [8, 4, 'four weeks'], [52, 8, 'eight weeks']]
|
|
timeRanges.push
|
|
start: generateWeekOffset currentDate, weekPair[0]
|
|
end: generateWeekOffset currentDate, weekPair[1]
|
|
name: weekPair[2]
|
|
lockManager.setLock mailTaskName, lockDurationMs, (err) ->
|
|
if err? then return log.error "Error getting a distributed lock for task #{mailTaskName}: #{err}"
|
|
async.each timeRanges, emailTimeRange.bind({mailTaskName: mailTaskName}), (err) ->
|
|
if err
|
|
log.error "There was an error sending the candidate profile update reminder emails: #{err}"
|
|
lockManager.releaseLock mailTaskName, (err) ->
|
|
if err? then return log.error "There was an error releasing the distributed lock for task #{mailTaskName}: #{err}"
|
|
|
|
generateWeekOffset = (originalDate, numberOfWeeks) ->
|
|
return (new Date(originalDate.getTime() - numberOfWeeks * 7 * 24 * 60 * 60 * 1000)).toISOString()
|
|
|
|
emailTimeRange = (timeRange, emailTimeRangeCallback) ->
|
|
waterfallContext =
|
|
"timeRange": timeRange
|
|
"mailTaskName": @mailTaskName
|
|
async.waterfall [
|
|
findAllCandidatesWithinTimeRange.bind(waterfallContext)
|
|
(unfilteredCandidates, cb) ->
|
|
async.reject unfilteredCandidates, candidateFilter.bind(waterfallContext), cb.bind(null, null)
|
|
(filteredCandidates, cb) ->
|
|
async.each filteredCandidates, sendReminderEmailToCandidate.bind(waterfallContext), cb
|
|
], emailTimeRangeCallback
|
|
|
|
findAllCandidatesWithinTimeRange = (cb) ->
|
|
findParameters =
|
|
"jobProfile.updated":
|
|
$gt: @timeRange.start
|
|
$lte: @timeRange.end
|
|
"jobProfileApproved": true
|
|
selection = "_id email jobProfile.name jobProfile.updated emails" #make sure to check for anyNotes too.
|
|
User.find(findParameters).select(selection).lean().exec cb
|
|
|
|
candidateFilter = (candidate, sentEmailFilterCallback) ->
|
|
if candidate.emails?.anyNotes?.enabled is false or candidate.emails?.recruitNotes?.enabled is false
|
|
return sentEmailFilterCallback true
|
|
findParameters =
|
|
"user": candidate._id
|
|
"mailTask": @mailTaskName
|
|
"metadata.timeRangeName": @timeRange.name
|
|
"metadata.updated": candidate.jobProfile.updated
|
|
MailSent.find(findParameters).lean().exec (err, sentMail) ->
|
|
if err?
|
|
log.error "Error finding mail sent for task #{@mailTaskName} and user #{candidate._id}!"
|
|
sentEmailFilterCallback true
|
|
else
|
|
sentEmailFilterCallback Boolean(sentMail.length)
|
|
|
|
findEmployersSignedUpAfterDate = (dateObject, cb) ->
|
|
countParameters =
|
|
$or: [{"dateCreated": {$gte: dateObject}},{"signedEmployerAgreement":{$gte: dateObject}}]
|
|
employerAt: {$exists: true}
|
|
permissions: "employer"
|
|
User.count countParameters, cb
|
|
|
|
sendReminderEmailToCandidate = (candidate, sendEmailCallback) ->
|
|
findEmployersSignedUpAfterDate new Date(candidate.jobProfile.updated), (err, employersAfterCount) =>
|
|
if err?
|
|
log.error "There was an error finding employers who signed up after #{candidate.jobProfile.updated}: #{err}"
|
|
return sendEmailCallback err
|
|
if employersAfterCount < 2
|
|
employersAfterCount = 2
|
|
context =
|
|
email_id: "tem_CtTLsKQufxrxoPMn7upKiL"
|
|
recipient:
|
|
address: candidate.email
|
|
name: candidate.jobProfile.name
|
|
email_data:
|
|
new_company: employersAfterCount
|
|
company_name: "CodeCombat"
|
|
user_profile: "http://codecombat.com/account/profile/#{candidate._id}"
|
|
recipient_address: encodeURIComponent(candidate.email)
|
|
#log.info "Sending #{@timeRange.name} update reminder to #{context.recipient.name}(#{context.recipient.address})"
|
|
newSentMail =
|
|
mailTask: @mailTaskName
|
|
user: candidate._id
|
|
metadata:
|
|
timeRangeName: @timeRange.name
|
|
updated: candidate.jobProfile.updated
|
|
MailSent.create newSentMail, (err) ->
|
|
if err? then return sendEmailCallback err
|
|
sendwithus.api.send context, (err, result) ->
|
|
log.error "Error sending candidate update reminder email: #{err} with result #{result}" if err
|
|
sendEmailCallback null
|
|
### End Approved Candidate Update Reminder Task ###
|
|
|
|
### Unapproved Candidate Finish Reminder Task ###
|
|
unapprovedCandidateFinishProfileTask = ->
|
|
mailTaskName = "unapprovedCandidateFinishProfileTask"
|
|
lockDurationMs = 2 * 60 * 1000
|
|
currentDate = new Date()
|
|
timeRanges = []
|
|
for weekPair in [[4, 2,'two weeks'], [8, 4, 'four weeks'], [52, 8, 'eight weeks']]
|
|
timeRanges.push
|
|
start: generateWeekOffset currentDate, weekPair[0]
|
|
end: generateWeekOffset currentDate, weekPair[1]
|
|
name: weekPair[2]
|
|
lockManager.setLock mailTaskName, lockDurationMs, (err) ->
|
|
if err? then return log.error "Error getting a distributed lock for task #{mailTaskName}: #{err}"
|
|
async.each timeRanges, emailUnapprovedCandidateTimeRange.bind({mailTaskName: mailTaskName}), (err) ->
|
|
if err
|
|
log.error "There was an error sending the candidate profile update reminder emails: #{err}"
|
|
lockManager.releaseLock mailTaskName, (err) ->
|
|
if err? then return log.error "There was an error releasing the distributed lock for task #{mailTaskName}: #{err}"
|
|
|
|
emailUnapprovedCandidateTimeRange = (timeRange, emailTimeRangeCallback) ->
|
|
waterfallContext =
|
|
"timeRange": timeRange
|
|
"mailTaskName": @mailTaskName
|
|
async.waterfall [
|
|
findAllUnapprovedCandidatesWithinTimeRange.bind(waterfallContext)
|
|
(unfilteredCandidates, cb) ->
|
|
async.reject unfilteredCandidates, ignoredCandidateFilter, cb.bind(null,null)
|
|
(unfilteredPotentialCandidates, cb) ->
|
|
async.reject unfilteredPotentialCandidates, unapprovedCandidateFilter.bind(waterfallContext), cb.bind(null, null)
|
|
(filteredCandidates, cb) ->
|
|
async.each filteredCandidates, sendReminderEmailToUnapprovedCandidate.bind(waterfallContext), cb
|
|
], emailTimeRangeCallback
|
|
|
|
findAllUnapprovedCandidatesWithinTimeRange = (cb) ->
|
|
findParameters =
|
|
"jobProfile":
|
|
$exists: true
|
|
"jobProfile.updated":
|
|
$gt: @timeRange.start
|
|
$lte: @timeRange.end
|
|
"jobProfileApproved": false
|
|
selection = "_id email jobProfile.name jobProfile.updated emails"
|
|
User.find(findParameters).select(selection).lean().exec cb
|
|
|
|
ignoredCandidateFilter = (candidate, cb) ->
|
|
findParameters =
|
|
"user": candidate._id
|
|
"contactName": "Ignore"
|
|
UserRemark.count findParameters, (err, results) ->
|
|
if err? then return true
|
|
return cb Boolean(results.length)
|
|
|
|
unapprovedCandidateFilter = (candidate, sentEmailFilterCallback) ->
|
|
if candidate.emails?.anyNotes?.enabled is false or candidate.emails?.recruitNotes?.enabled is false
|
|
return sentEmailFilterCallback true
|
|
findParameters =
|
|
"user": candidate._id
|
|
"mailTask": @mailTaskName
|
|
"metadata.timeRangeName": @timeRange.name
|
|
"metadata.updated": candidate.jobProfile.updated
|
|
MailSent.find(findParameters).lean().exec (err, sentMail) ->
|
|
if err?
|
|
log.error "Error finding mail sent for task #{@mailTaskName} and user #{candidate._id}!"
|
|
sentEmailFilterCallback true
|
|
else
|
|
sentEmailFilterCallback Boolean(sentMail.length)
|
|
|
|
sendReminderEmailToUnapprovedCandidate = (candidate, sendEmailCallback) ->
|
|
if err?
|
|
log.error "There was an error finding employers who signed up after #{candidate.jobProfile.updated}: #{err}"
|
|
return sendEmailCallback err
|
|
context =
|
|
email_id: "tem_RXyjzmc7S2HJH287pfoSPN"
|
|
recipient:
|
|
address: candidate.email
|
|
name: candidate.jobProfile.name
|
|
email_data:
|
|
user_profile: "http://codecombat.com/account/profile/#{candidate._id}"
|
|
recipient_address: encodeURIComponent(candidate.email)
|
|
#log.info "Sending #{@timeRange.name} finish profile reminder to #{context.recipient.name}(#{context.recipient.address})"
|
|
newSentMail =
|
|
mailTask: @mailTaskName
|
|
user: candidate._id
|
|
metadata:
|
|
timeRangeName: @timeRange.name
|
|
updated: candidate.jobProfile.updated
|
|
MailSent.create newSentMail, (err) ->
|
|
if err? then return sendEmailCallback err
|
|
sendwithus.api.send context, (err, result) ->
|
|
log.error "Error sending candidate finish profile reminder email: #{err} with result #{result}" if err
|
|
sendEmailCallback null
|
|
### End Unapproved Candidate Finish Reminder Task ###
|
|
|
|
### Internal Candidate Update Reminder Email ###
|
|
internalCandidateUpdateTask = ->
|
|
mailTaskName = "internalCandidateUpdateTask"
|
|
lockDurationMs = 2 * 60 * 1000
|
|
lockManager.setLock mailTaskName, lockDurationMs, (err) ->
|
|
if err? then return log.error "Error getting a distributed lock for task #{mailTaskName}: #{err}"
|
|
emailInternalCandidateUpdateReminder.call {"mailTaskName":mailTaskName}, (err) ->
|
|
if err
|
|
log.error "There was an error sending the internal candidate update reminder.: #{err}"
|
|
lockManager.releaseLock mailTaskName, (err) ->
|
|
if err? then return log.error "There was an error releasing the distributed lock for task #{mailTaskName}: #{err}"
|
|
|
|
emailInternalCandidateUpdateReminder = (internalCandidateUpdateReminderCallback) ->
|
|
currentTime = new Date()
|
|
beginningOfUTCDay = new Date()
|
|
beginningOfUTCDay.setUTCHours(0,0,0,0)
|
|
asyncContext =
|
|
"beginningOfUTCDay": beginningOfUTCDay
|
|
"currentTime": currentTime
|
|
"mailTaskName": @mailTaskName
|
|
async.waterfall [
|
|
findNonApprovedCandidatesWhoUpdatedJobProfileToday.bind(asyncContext)
|
|
(unfilteredCandidates, cb) ->
|
|
async.reject unfilteredCandidates, candidatesUpdatedTodayFilter.bind(asyncContext), cb.bind(null,null)
|
|
(filteredCandidates, cb) ->
|
|
async.each filteredCandidates, sendInternalCandidateUpdateReminder.bind(asyncContext), cb
|
|
], internalCandidateUpdateReminderCallback
|
|
|
|
findNonApprovedCandidatesWhoUpdatedJobProfileToday = (cb) ->
|
|
findParameters =
|
|
"jobProfile.updated":
|
|
$lte: @currentTime.toISOString()
|
|
$gt: @beginningOfUTCDay.toISOString()
|
|
"jobProfileApproved": false
|
|
User.find(findParameters).select("_id jobProfile.name jobProfile.updated").lean().exec cb
|
|
|
|
candidatesUpdatedTodayFilter = (candidate, cb) ->
|
|
findParameters =
|
|
"user": candidate._id
|
|
"mailTask": @mailTaskName
|
|
"metadata.beginningOfUTCDay": @beginningOfUTCDay
|
|
MailSent.find(findParameters).lean().exec (err, sentMail) ->
|
|
if err?
|
|
log.error "Error finding mail sent for task #{@mailTaskName} and user #{candidate._id}!"
|
|
cb true
|
|
else
|
|
cb Boolean(sentMail.length)
|
|
|
|
sendInternalCandidateUpdateReminder = (candidate, cb) ->
|
|
context =
|
|
email_id: "tem_Ac7nhgKqatTHBCgDgjF5pE"
|
|
recipient:
|
|
address: "team@codecombat.com"
|
|
name: "The CodeCombat Team"
|
|
email_data:
|
|
new_candidate_profile: "http://codecombat.com/account/profile/#{candidate._id}"
|
|
#log.info "Sending candidate updated reminder for #{candidate.jobProfile.name}"
|
|
newSentMail =
|
|
mailTask: @mailTaskName
|
|
user: candidate._id
|
|
metadata:
|
|
beginningOfUTCDay: @beginningOfUTCDay
|
|
|
|
MailSent.create newSentMail, (err) ->
|
|
if err? then return cb err
|
|
sendwithus.api.send context, (err, result) ->
|
|
log.error "Error sending interal candidate update email: #{err} with result #{result}" if err
|
|
cb null
|
|
|
|
### End Internal Candidate Update Reminder Email ###
|
|
### Employer New Candidates Available Email ###
|
|
employerNewCandidatesAvailableTask = ->
|
|
mailTaskName = "employerNewCandidatesAvailableTask"
|
|
lockDurationMs = 2 * 60 * 1000
|
|
lockManager.setLock mailTaskName, lockDurationMs, (err) ->
|
|
if err? then return log.error "Error getting a distributed lock for task #{mailTaskName}: #{err}"
|
|
emailEmployerNewCandidatesAvailable.call {"mailTaskName":mailTaskName}, (err) ->
|
|
if err
|
|
log.error "There was an error completing the new candidates available task: #{err}"
|
|
lockManager.releaseLock mailTaskName, (err) ->
|
|
if err? then return log.error "There was an error releasing the distributed lock for task #{mailTaskName}: #{err}"
|
|
|
|
emailEmployerNewCandidatesAvailable = (emailEmployerNewCandidatesAvailableCallback) ->
|
|
currentTime = new Date()
|
|
asyncContext =
|
|
"currentTime": currentTime
|
|
"mailTaskName": @mailTaskName
|
|
|
|
async.waterfall [
|
|
findAllEmployers
|
|
makeEmployerNamesEasilyAccessible
|
|
(allEmployers, cb) ->
|
|
async.reject allEmployers, employersEmailedDigestMoreThanWeekAgoFilter.bind(asyncContext), cb.bind(null,null)
|
|
(employersToEmail, cb) ->
|
|
async.each employersToEmail, sendEmployerNewCandidatesAvailableEmail.bind(asyncContext), cb
|
|
], emailEmployerNewCandidatesAvailableCallback
|
|
|
|
findAllEmployers = (cb) ->
|
|
findParameters =
|
|
"employerAt":
|
|
$exists: true
|
|
permissions: "employer"
|
|
selection = "_id email employerAt signedEmployerAgreement.data.firstName signedEmployerAgreement.data.lastName activity dateCreated emails"
|
|
User.find(findParameters).select(selection).lean().exec cb
|
|
|
|
makeEmployerNamesEasilyAccessible = (allEmployers, cb) ->
|
|
for employer, index in allEmployers
|
|
if employer.signedEmployerAgreement?.data?.firstName
|
|
employer.name = employer.signedEmployerAgreement.data.firstName + " " + employer.signedEmployerAgreement.data.lastName
|
|
delete employer.signedEmployerAgreement
|
|
allEmployers[index] = employer
|
|
cb null, allEmployers
|
|
|
|
employersEmailedDigestMoreThanWeekAgoFilter = (employer, cb) ->
|
|
if employer.emails?.employerNotes?.enabled is false
|
|
return cb true
|
|
if not employer.signedEmployerAgreement and not employer.activity?.login?
|
|
return cb true
|
|
findParameters =
|
|
"user": employer._id
|
|
"mailTask": @mailTaskName
|
|
"sent":
|
|
$gt: new Date(@currentTime.getTime() - 14 * 24 * 60 * 60 * 1000)
|
|
MailSent.find(findParameters).lean().exec (err, sentMail) ->
|
|
if err?
|
|
log.error "Error finding mail sent for task #{@mailTaskName} and employer #employer._id}!"
|
|
cb true
|
|
else
|
|
cb Boolean(sentMail.length)
|
|
|
|
sendEmployerNewCandidatesAvailableEmail = (employer, cb) ->
|
|
lastLoginDate = employer.activity?.login?.last ? employer.dateCreated
|
|
countParameters =
|
|
"jobProfileApproved": true
|
|
$or: [
|
|
jobProfileApprovedDate:
|
|
$gt: lastLoginDate.toISOString()
|
|
,
|
|
jobProfileApprovedDate:
|
|
$exists: false
|
|
"jobProfile.updated":
|
|
$gt: lastLoginDate.toISOString()
|
|
]
|
|
User.count countParameters, (err, numberOfCandidatesSinceLogin) =>
|
|
if err? then return cb err
|
|
if numberOfCandidatesSinceLogin < 4
|
|
return cb null
|
|
context =
|
|
email_id: "tem_CCcHKr95Nvu5bT7c7iHCtm"
|
|
recipient:
|
|
address: employer.email
|
|
email_data:
|
|
new_candidates: numberOfCandidatesSinceLogin
|
|
employer_company_name: employer.employerAt
|
|
company_name: "CodeCombat"
|
|
recipient_address: encodeURIComponent(employer.email)
|
|
if employer.name
|
|
context.recipient.name = employer.name
|
|
log.info "Sending available candidates update reminder to #{context.recipient.name}(#{context.recipient.address})"
|
|
newSentMail =
|
|
mailTask: @mailTaskName
|
|
user: employer._id
|
|
MailSent.create newSentMail, (err) ->
|
|
if err? then return cb err
|
|
sendwithus.api.send context, (err, result) ->
|
|
log.error "Error sending employer candidates available email: #{err} with result #{result}" if err
|
|
cb null
|
|
|
|
### End Employer New Candidates Available Email ###
|
|
|
|
### Task Emails ###
|
|
emailUserRemarkTaskRemindersTask = ->
|
|
mailTaskName = "emailUserRemarkTaskRemindersTask"
|
|
lockDurationMs = 2 * 60 * 1000
|
|
lockManager.setLock mailTaskName, lockDurationMs, (err) ->
|
|
if err? then return log.error "Error getting a distributed lock for task #{mailTaskName}: #{err}"
|
|
emailUserRemarkTaskReminders.call {"mailTaskName":mailTaskName}, (err) ->
|
|
if err
|
|
log.error "There was an error completing the #{mailTaskName}: #{err}"
|
|
lockManager.releaseLock mailTaskName, (err) ->
|
|
if err? then return log.error "There was an error releasing the distributed lock for task #{mailTaskName}: #{err}"
|
|
|
|
emailUserRemarkTaskReminders = (cb) ->
|
|
currentTime = new Date()
|
|
asyncContext =
|
|
"currentTime": currentTime
|
|
"mailTaskName": @mailTaskName
|
|
|
|
async.waterfall [
|
|
findAllIncompleteUserRemarkTasksDue.bind(asyncContext)
|
|
processRemarksIntoTasks.bind(asyncContext)
|
|
(allTasks, cb) ->
|
|
async.reject allTasks, taskReminderAlreadySentThisWeekFilter.bind(asyncContext), cb.bind(null,null)
|
|
(tasksToRemind, cb) ->
|
|
async.each tasksToRemind, sendUserRemarkTaskEmail.bind(asyncContext), cb
|
|
], cb
|
|
|
|
findAllIncompleteUserRemarkTasksDue = (cb) ->
|
|
findParameters =
|
|
tasks:
|
|
$exists: true
|
|
$elemMatch:
|
|
date:
|
|
$lte: @currentTime.toISOString()
|
|
status:
|
|
$ne: 'Completed'
|
|
selection = "contact user tasks"
|
|
UserRemark.find(findParameters).select(selection).lean().exec cb
|
|
|
|
processRemarksIntoTasks = (remarks, cb) ->
|
|
tasks = []
|
|
for remark in remarks
|
|
for task in remark.tasks
|
|
taskObject =
|
|
date: task.date
|
|
action: task.action
|
|
contact: remark.contact
|
|
user: remark.user
|
|
remarkID: remark._id
|
|
tasks.push taskObject
|
|
cb null, tasks
|
|
|
|
taskReminderAlreadySentThisWeekFilter = (task, cb) ->
|
|
findParameters =
|
|
"user": task.contact
|
|
"mailTask": @mailTaskName
|
|
"sent":
|
|
$gt: new Date(@currentTime.getTime() - 7 * 24 * 60 * 60 * 1000)
|
|
"metadata":
|
|
remarkID: task.remarkID
|
|
taskAction: task.action
|
|
date: task.date
|
|
MailSent.count findParameters, (err, count) ->
|
|
if err? then return cb true
|
|
return cb Boolean(count)
|
|
|
|
sendUserRemarkTaskEmail = (task, cb) ->
|
|
mailTaskName = @mailTaskName
|
|
User.findOne("_id":task.contact).select("email").lean().exec (err, contact) ->
|
|
if err? then return cb err
|
|
User.findOne("_id":task.user).select("jobProfile.name").lean().exec (err, user) ->
|
|
if err? then return cb err
|
|
context =
|
|
email_id: "tem_aryDjyw6JmEmbKtCMTSwAM"
|
|
recipient:
|
|
address: contact.email
|
|
email_data:
|
|
task_text: task.action
|
|
candidate_name: user.jobProfile?.name ? "(Name not listed in job profile)"
|
|
candidate_link: "http://codecombat.com/account/profile/#{task.user}"
|
|
due_date: task.date
|
|
#log.info "Sending recruitment task reminder to #{contact.email}"
|
|
newSentMail =
|
|
mailTask: mailTaskName
|
|
user: task.contact
|
|
"metadata":
|
|
remarkID: task.remarkID
|
|
taskAction: task.action
|
|
date: task.date
|
|
MailSent.create newSentMail, (err) ->
|
|
if err? then return cb err
|
|
sendwithus.api.send context, (err, result) ->
|
|
log.error "Error sending #{mailTaskName} to #{contact.email}: #{err} with result #{result}" if err
|
|
cb null
|
|
|
|
### New Recruit Leaderboard Email ###
|
|
###
|
|
newRecruitLeaderboardEmailTask = ->
|
|
# tem_kMQFCKX3v4DNAQDsMAsPJC
|
|
#maxRank and maxRankTime should be recorded if isSimulating is false
|
|
mailTaskName = "newRecruitLeaderboardEmailTask"
|
|
lockDurationMs = 6000
|
|
lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) ->
|
|
###
|
|
### End New Recruit Leaderboard Email ###
|
|
|
|
### Employer Matching Candidate Notification Email ###
|
|
###
|
|
employerMatchingCandidateNotificationTask = ->
|
|
# tem_mYsepTfWQ265noKfZJcbBH
|
|
#save email filters in their own collection
|
|
mailTaskName = "employerMatchingCandidateNotificationTask"
|
|
lockDurationMs = 6000
|
|
lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) ->
|
|
###
|
|
### End Employer Matching Candidate Notification Email ###
|
|
### Employer ignore ###
|
|
|
|
DEBUGGING = false
|
|
LADDER_PREGAME_INTERVAL = 2 * 3600 * 1000 # Send emails two hours before players last submitted.
|
|
getTimeFromDaysAgo = (now, daysAgo) ->
|
|
t = now - 86400 * 1000 * daysAgo - LADDER_PREGAME_INTERVAL
|
|
|
|
isRequestFromDesignatedCronHandler = (req, res) ->
|
|
requestIP = req.headers['x-forwarded-for']?.replace(' ', '').split(',')[0]
|
|
if requestIP isnt config.mail.cronHandlerPublicIP and requestIP isnt config.mail.cronHandlerPrivateIP
|
|
console.log "RECEIVED REQUEST FROM IP #{requestIP}(headers indicate #{req.headers['x-forwarded-for']}"
|
|
console.log 'UNAUTHORIZED ATTEMPT TO SEND TRANSACTIONAL LADDER EMAIL THROUGH CRON MAIL HANDLER'
|
|
res.send('You aren\'t authorized to perform that action. Only the specified Cron handler may perform that action.')
|
|
res.end()
|
|
return false
|
|
return true
|
|
|
|
### Ladder Update Email ###
|
|
|
|
handleLadderUpdate = (req, res) ->
|
|
return unless DEBUGGING or isRequestFromDesignatedCronHandler req, res
|
|
res.send('Great work, Captain Cron! I can take it from here.')
|
|
res.end()
|
|
# TODO: somehow fetch the histograms
|
|
#emailDays = [1, 2, 4, 7, 14, 30]
|
|
emailDays = [1, 3, 7] # Reduced to keep smaller monthly recipient footprint
|
|
now = new Date()
|
|
for daysAgo in emailDays
|
|
# Get every session that was submitted in a 5-minute window after the time.
|
|
startTime = getTimeFromDaysAgo now, daysAgo
|
|
endTime = startTime + 5 * 60 * 1000
|
|
if DEBUGGING
|
|
endTime = startTime + 15 * 60 * 1000 # Debugging: make sure there's something to send
|
|
findParameters = {submitted: true, submitDate: {$gt: new Date(startTime), $lte: new Date(endTime)}}
|
|
# TODO: think about putting screenshots in the email
|
|
selectString = 'creator team levelName levelID totalScore matches submitted submitDate scoreHistory level.original unsubscribed'
|
|
query = LevelSession.find(findParameters)
|
|
.select(selectString)
|
|
.lean()
|
|
do (daysAgo) ->
|
|
query.exec (err, results) ->
|
|
if err
|
|
log.error "Couldn't fetch ladder updates for #{findParameters}\nError: #{err}"
|
|
return errors.serverError res, "Ladder update email query failed: #{JSON.stringify(err)}"
|
|
#log.info "Found #{results.length} ladder sessions to email updates about for #{daysAgo} day(s) ago."
|
|
sendLadderUpdateEmail result, now, daysAgo for result in results
|
|
|
|
sendLadderUpdateEmail = (session, now, daysAgo) ->
|
|
User.findOne({_id: session.creator}).select('name email firstName lastName emailSubscriptions emails preferredLanguage').exec (err, user) ->
|
|
if err
|
|
log.error "Couldn't find user for #{session.creator} from session #{session._id}"
|
|
return
|
|
allowNotes = user.isEmailSubscriptionEnabled 'anyNotes'
|
|
unless user.get('email') and allowNotes and not session.unsubscribed
|
|
#log.info "Not sending email to #{user.get('email')} #{user.get('name')} because they only want emails about #{user.get('emailSubscriptions')}, #{user.get('emails')} - session unsubscribed: #{session.unsubscribed}"
|
|
return
|
|
unless session.levelName and session.team
|
|
#log.info "Not sending email to #{user.get('email')} #{user.get('name')} because the session had levelName #{session.levelName} or team #{session.team} in it."
|
|
return
|
|
name = if user.get('firstName') and user.get('lastName') then "#{user.get('firstName')}" else user.get('name')
|
|
name = 'Wizard' if not name or name is 'Anoner'
|
|
|
|
# Fetch the most recent defeat and victory, if there are any.
|
|
# (We could look at strongest/weakest, but we'd have to fetch everyone, or denormalize more.)
|
|
matches = _.filter session.matches, (match) -> match.date >= getTimeFromDaysAgo now, daysAgo
|
|
defeats = _.filter matches, (match) -> match.metrics.rank is 1 and match.opponents[0].metrics.rank is 0
|
|
victories = _.filter matches, (match) -> match.metrics.rank is 0 and match.opponents[0].metrics.rank is 1
|
|
|
|
defeat = _.last defeats
|
|
victory = _.last victories
|
|
|
|
sendEmail = (defeatContext, victoryContext, levelVersionsContext) ->
|
|
# TODO: do something with the preferredLanguage?
|
|
context =
|
|
email_id: sendwithus.templates.ladder_update_email
|
|
recipient:
|
|
address: if DEBUGGING then 'nick@codecombat.com' else user.get('email')
|
|
name: name
|
|
email_data:
|
|
name: name
|
|
days_ago: daysAgo
|
|
wins: victories.length
|
|
losses: defeats.length
|
|
total_score: Math.round(session.totalScore * 100)
|
|
team: session.team
|
|
team_name: session.team[0].toUpperCase() + session.team.substr(1)
|
|
level_name: session.levelName
|
|
session_id: session._id
|
|
ladder_url: "http://codecombat.com/play/ladder/#{session.levelID}#my-matches"
|
|
score_history_graph_url: getScoreHistoryGraphURL session, daysAgo
|
|
defeat: defeatContext
|
|
victory: victoryContext
|
|
levelVersions: levelVersionsContext
|
|
#log.info "Sending ladder update email to #{context.recipient.address} with #{context.email_data.wins} wins and #{context.email_data.losses} losses since #{daysAgo} day(s) ago."
|
|
sendwithus.api.send context, (err, result) ->
|
|
log.error "Error sending ladder update email: #{err} with result #{result}" if err
|
|
|
|
urlForMatch = (match) ->
|
|
"http://codecombat.com/play/level/#{session.levelID}?team=#{session.team}&opponent=#{match.opponents[0].sessionID}"
|
|
|
|
onFetchedDefeatedOpponent = (err, defeatedOpponent) ->
|
|
if err
|
|
log.error "Couldn't find defeateded opponent: #{err}"
|
|
defeatedOpponent = null
|
|
victoryContext = {opponent_name: defeatedOpponent?.name ? 'Anoner', url: urlForMatch(victory)} if victory
|
|
|
|
onFetchedVictoriousOpponent = (err, victoriousOpponent) ->
|
|
if err
|
|
log.error "Couldn't find victorious opponent: #{err}"
|
|
victoriousOpponent = null
|
|
defeatContext = {opponent_name: victoriousOpponent?.name ? 'Anoner', url: urlForMatch(defeat)} if defeat
|
|
|
|
Level.find({original: session.level.original, created: {$gt: session.submitDate}}).select('created commitMessage version').sort('-created').lean().exec (err, levelVersions) ->
|
|
sendEmail defeatContext, victoryContext, (if levelVersions.length then levelVersions else null)
|
|
|
|
if defeat
|
|
User.findOne({_id: defeat.opponents[0].userID}).select('name').lean().exec onFetchedVictoriousOpponent
|
|
else
|
|
onFetchedVictoriousOpponent null, null
|
|
|
|
if victory
|
|
User.findOne({_id: victory.opponents[0].userID}).select('name').lean().exec onFetchedDefeatedOpponent
|
|
else
|
|
onFetchedDefeatedOpponent null, null
|
|
|
|
getScoreHistoryGraphURL = (session, daysAgo) ->
|
|
# Totally duplicated in My Matches tab for now until we figure out what we're doing.
|
|
since = new Date() - 86400 * 1000 * daysAgo
|
|
scoreHistory = (s for s in session.scoreHistory ? [] when s[0] >= since)
|
|
return '' unless scoreHistory.length > 1
|
|
scoreHistory = _.last scoreHistory, 100 # Chart URL needs to be under 2048 characters for GET
|
|
times = (s[0] for s in scoreHistory)
|
|
times = ((100 * (t - times[0]) / (times[times.length - 1] - times[0])).toFixed(1) for t in times)
|
|
scores = (s[1] for s in scoreHistory)
|
|
lowest = _.min scores #.concat([0])
|
|
highest = _.max scores #.concat(50)
|
|
scores = (Math.round(100 * (s - lowest) / (highest - lowest)) for s in scores)
|
|
currentScore = Math.round scoreHistory[scoreHistory.length - 1][1] * 100
|
|
minScore = Math.round(100 * lowest)
|
|
maxScore = Math.round(100 * highest)
|
|
chartData = times.join(',') + '|' + scores.join(',')
|
|
"https://chart.googleapis.com/chart?chs=600x75&cht=lxy&chtt=Score%3A+#{currentScore}&chts=222222,12,r&chf=a,s,000000FF&chls=2&chd=t:#{chartData}&chxt=y&chxr=0,#{minScore},#{maxScore}"
|
|
|
|
### End Ladder Update Email ###
|
|
|
|
### Next Steps Email ###
|
|
|
|
handleNextSteps = (req, res) ->
|
|
return unless DEBUGGING or isRequestFromDesignatedCronHandler req, res
|
|
res.send('Great work, Captain Cron! I can take it from here.')
|
|
res.end()
|
|
emailDays = [1]
|
|
now = new Date()
|
|
for daysAgo in emailDays
|
|
# Get every User that was created in a 5-minute window after the time.
|
|
startTime = getTimeFromDaysAgo now, daysAgo
|
|
endTime = startTime + 5 * 60 * 1000
|
|
findParameters = {dateCreated: {$gt: new Date(startTime), $lte: new Date(endTime)}, emailLower: {$exists: true}}
|
|
selectString = 'name firstName lastName lastLevel points email gender emailSubscriptions emails dateCreated preferredLanguage aceConfig.language activity stats earned testGroupNumber ageRange'
|
|
query = User.find(findParameters).select(selectString)
|
|
do (daysAgo) ->
|
|
query.exec (err, results) ->
|
|
if err
|
|
log.error "Couldn't fetch next steps users for #{findParameters}\nError: #{err}"
|
|
return errors.serverError res, "Next steps email query failed: #{JSON.stringify(err)}"
|
|
log.info "Found #{results.length} next-steps users to email updates about for #{daysAgo} day(s) ago." if DEBUGGING
|
|
sendNextStepsEmail result, now, daysAgo for result in results
|
|
|
|
sendNextStepsEmail = (user, now, daysAgo) ->
|
|
unless user.isEmailSubscriptionEnabled('generalNews') and user.isEmailSubscriptionEnabled('anyNotes')
|
|
log.info "Not sending email to #{user.get('email')} #{user.get('name')} because they only want emails about #{JSON.stringify(user.get('emails'))}" if DEBUGGING
|
|
return
|
|
|
|
LevelSession.find({creator: user.get('_id') + ''}).select('levelName levelID changed state.complete playtime').lean().exec (err, sessions) ->
|
|
return log.error "Couldn't find sessions for #{user.get('email')}: #{err}" if err
|
|
complete = (s for s in sessions when s.state?.complete)
|
|
incomplete = (s for s in sessions when not s.state?.complete)
|
|
return if complete.length < 2
|
|
|
|
# TODO: find the next level to do somehow, for real
|
|
if incomplete.length
|
|
nextLevel = name: incomplete[0].levelName, slug: incomplete[0].levelID
|
|
else
|
|
nextLevel = null
|
|
err = null
|
|
do (err, nextLevel) ->
|
|
return log.error "Couldn't find next level for #{user.get('email')}: #{err}" if err
|
|
name = if user.get('firstName') and user.get('lastName') then "#{user.get('firstName')}" else user.get('name')
|
|
name = 'hero' if not name or name is 'Anoner'
|
|
#secretLevel = switch user.get('testGroupNumber') % 8
|
|
# when 0, 1, 2, 3 then name: 'Forgetful Gemsmith', slug: 'forgetful-gemsmith'
|
|
# when 4, 5, 6, 7 then name: 'Signs and Portents', slug: 'signs-and-portents'
|
|
secretLevel = name: 'Signs and Portents', slug: 'signs-and-portents' # We turned off this test for now and are sending everyone to forgetful-gemsmith
|
|
|
|
# TODO: make this smarter, actually data-driven, looking at all available sessions
|
|
shadowGuardSession = _.find sessions, levelID: 'shadow-guard'
|
|
isFast = shadowGuardSession and shadowGuardSession.playtime < 90 # Average is 107s
|
|
isVeryFast = shadowGuardSession and shadowGuardSession.playtime < 75
|
|
isAdult = user.get('ageRange') in ['18-24', '25-34', '35-44', '45-100']
|
|
isKid = not isAdult # Assume kid if not specified
|
|
offers =
|
|
'app-academy': isAdult and isVeryFast
|
|
'designlab': isAdult and Math.random() < 0.25
|
|
'tealeaf-academy': isAdult and isFast
|
|
'talent-buddy': isAdult and Math.random() < 0.25
|
|
'coding-campus': isAdult and Math.random() < 0.25 # TODO: geodetect UT and give priority
|
|
'viking': isAdult and isFast
|
|
'maker-square': isAdult and isFast
|
|
'the-firehose-project': isAdult and isFast
|
|
#'mv-code-club': isKid # TODO: geodetect, get landing page URL
|
|
'breakout-mentors': isKid
|
|
nAdditionalOffers = Math.max 0, 4 - _.filter(offers).length
|
|
possibleAdditionalOffers = ['ostraining', 'code-school', 'one-month', 'pluralsight']
|
|
for offer in _.sample possibleAdditionalOffers, nAdditionalOffers
|
|
offers[offer] = true
|
|
if user.isPremium()
|
|
offers = null
|
|
# TODO: do something with the preferredLanguage?
|
|
context =
|
|
email_id: sendwithus.templates.next_steps_email
|
|
recipient:
|
|
address: if DEBUGGING then 'nick@codecombat.com' else user.get('email')
|
|
name: name
|
|
email_data:
|
|
name: name
|
|
days_ago: daysAgo
|
|
nextLevelName: nextLevel?.name
|
|
nextLevelLink: if nextLevel then "http://codecombat.com/play/level/#{nextLevel.slug}" else null
|
|
secretLevelName: secretLevel.name
|
|
secretLevelLink: "http://codecombat.com/play/level/#{secretLevel.slug}"
|
|
levelsComplete: complete.length
|
|
offers: offers
|
|
log.info "Sending next steps email to #{context.recipient.address} with #{context.email_data.nextLevelName} next and #{context.email_data.levelsComplete} levels complete since #{daysAgo} day(s) ago." if DEBUGGING
|
|
sendwithus.api.send context, (err, result) ->
|
|
log.error "Error sending next steps email: #{err} with result #{result}" if err
|
|
|
|
### End Next Steps Email ###
|
|
|
|
handleMailchimpWebHook = (req, res) ->
|
|
post = req.body
|
|
|
|
unless post.type in ['unsubscribe', 'profile']
|
|
res.send 'Bad post type'
|
|
return res.end()
|
|
|
|
unless post.data.email
|
|
res.send 'No email provided'
|
|
return res.end()
|
|
|
|
query = {'mailChimp.leid': post.data.web_id}
|
|
User.findOne query, (err, user) ->
|
|
return errors.serverError(res) if err
|
|
if not user
|
|
return errors.notFound(res)
|
|
|
|
handleProfileUpdate(user, post) if post.type is 'profile'
|
|
handleUnsubscribe(user) if post.type is 'unsubscribe'
|
|
|
|
user.updatedMailChimp = true # so as not to echo back to mailchimp
|
|
user.save (err) ->
|
|
return errors.serverError(res) if err
|
|
res.end('Success')
|
|
|
|
module.exports.handleProfileUpdate = handleProfileUpdate = (user, post) ->
|
|
mailchimpSubs = post.data.merges.INTERESTS.split(', ')
|
|
|
|
for [mailchimpEmailGroup, emailGroup] in _.zip(mail.MAILCHIMP_GROUPS, mail.NEWS_GROUPS)
|
|
user.setEmailSubscription emailGroup, mailchimpEmailGroup in mailchimpSubs
|
|
|
|
fname = post.data.merges.FNAME
|
|
user.set('firstName', fname) if fname
|
|
|
|
lname = post.data.merges.LNAME
|
|
user.set('lastName', lname) if lname
|
|
|
|
user.set 'mailChimp.email', post.data.email
|
|
user.set 'mailChimp.euid', post.data.id
|
|
|
|
|
|
module.exports.handleUnsubscribe = handleUnsubscribe = (user) ->
|
|
user.set 'emailSubscriptions', []
|
|
for emailGroup in mail.NEWS_GROUPS
|
|
user.setEmailSubscription emailGroup, false
|