mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-04 21:01:06 -05:00
bb6262483f
Address some code review feedback Correct error code in test Don't try to send emails to empty addresses Add tests for subscriptions Add tests for Next Steps email Fix specs Add reason for disabled test
790 lines
34 KiB
CoffeeScript
790 lines
34 KiB
CoffeeScript
mail = require '../commons/mail'
|
|
MailSent = require '../models/MailSent'
|
|
UserRemark = require '../models/UserRemark'
|
|
User = require '../models/User'
|
|
async = require 'async'
|
|
errors = require '../commons/errors'
|
|
config = require '../../server_config'
|
|
LevelSession = require '../models/LevelSession'
|
|
Level = require '../models/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) ->
|
|
return if not contact.email
|
|
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) ->
|
|
return if not user.get('email')
|
|
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 'Anonymous'
|
|
|
|
# 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 ? 'Anonymous', 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 ? 'Anonymous', 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 courseInstances'
|
|
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
|
|
|
|
module.exports.sendNextStepsEmail = sendNextStepsEmail = (user, now, daysAgo) ->
|
|
return log.info "Not sending next steps email to user with no email address" if not user.get('email')
|
|
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')} #{user.get('name')}: #{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')} #{user.get('name')}: #{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 in ['Anoner', 'Anonymous']
|
|
#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
|
|
# Used to use these categories to customize the email; not doing it right now. TODO: customize it again in Sendwithus.
|
|
# 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
|
|
isCoursePlayer: user.get('courseInstances')?.length > 0
|
|
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
|