codecombat/server/routes/mail.coffee

408 lines
18 KiB
CoffeeScript
Raw Normal View History

2014-01-24 14:48:00 -05:00
mail = require '../commons/mail'
MailTask = require '../mail/tasks/MailTask'
2014-07-16 15:13:21 -04:00
MailSent = require '../mail/sent/MailSent'
2014-06-19 11:38:07 -04:00
User = require '../users/User'
2014-07-16 15:13:21 -04:00
async = require 'async'
2014-01-24 14:48:00 -05:00
errors = require '../commons/errors'
config = require '../../server_config'
2014-06-19 11:38:07 -04:00
LevelSession = require '../levels/sessions/LevelSession'
Level = require '../levels/Level'
2014-03-08 21:49:09 -05:00
log = require 'winston'
sendwithus = require '../sendwithus'
2014-07-16 15:13:21 -04:00
if config.isProduction or config.redis.host isnt "localhost"
lockManager = require '../commons/LockManager'
2014-03-08 21:49:09 -05:00
module.exports.setup = (app) ->
2014-03-08 21:49:09 -05:00
app.all config.mail.mailchimpWebhook, handleMailchimpWebHook
app.get '/mail/cron/ladder-update', handleLadderUpdate
app.post '/mail/task', createMailTask
2014-07-16 15:13:21 -04:00
if lockManager
setupScheduledEmails()
setupScheduledEmails = ->
testForLockManager()
mailTaskMap =
"test_mail_task": candidateUpdateProfileTask
MailTask.find({}).lean().exec (err, mailTasks) ->
if err? then throw "Failed to schedule mailTasks! #{err}"
for mailTask in mailTasks
setInterval mailTaskMap[mailTask.url], mailTask.frequency*2
testForLockManager = -> unless lockManager then throw "The system isn't configured to do distributed locking!"
### Candidate Update Reminder Task ###
emailTimeRange = (timeRange, finalCallback) ->
waterfallContext =
"timeRange": timeRange
"mailTaskName": @mailTaskName
async.waterfall [
findAllCandidatesWithinTimeRange.bind(waterfallContext)
(unfilteredCandidates, cb) -> #now filter the candidates to see if they are eligible
async.reject unfilteredCandidates, candidateFilter.bind(waterfallContext), (filtered) -> cb null, filtered
(filteredCandidates, cb) -> #Now send emails to the eligible candidates and record.
async.each filteredCandidates, sendReminderEmailToCandidate.bind(waterfallContext), cb
], finalCallback
findAllCandidatesWithinTimeRange = (cb) ->
findParameters =
"jobProfile.updated":
$gt: @timeRange.start
$lte: @timeRange.end
selection = "_id email jobProfile.name jobProfile.updated"
User.find(findParameters).select(selection).lean().exec cb
candidateFilter = (candidate, sentEmailFilterCallback) ->
findParameters =
"user": candidate._id
"mailTask": @mailTaskName
"metadata.timeRangeName": @timeRange.name
"metadata.updated": candidate.jobProfile.updated
MailSent.find(findParameters).lean().exec (err, sentMail) ->
if err? then return errors.serverError("Error fetching sent mail in email task")
sentEmailFilterCallback Boolean(sentMail.length)
2014-07-16 15:13:21 -04:00
findEmployersSignedUpAfterDate = (dateObject, cb) ->
countParameters =
$or: [{"dateCreated": {$gte: dateObject}},{"signedEmployerAgreement":{$gte: dateObject}}]
employerAt: {$exists: true}
permissions: "employer"
User.count countParameters, cb
2014-07-08 19:26:51 -04:00
2014-07-16 15:13:21 -04:00
sendReminderEmailToCandidate = (candidate, sendEmailCallback) ->
findEmployersSignedUpAfterDate new Date(candidate.jobProfile.updated), (err, employersAfterCount) =>
context =
email_id: "tem_CtTLsKQufxrxoPMn7upKiL"
recipient:
address: candidate.email
name: candidate.jobProfile.name
email_data:
profile_updated: candidate.jobProfile.updated #format nicely
new_company: employersAfterCount
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 ladder update email: #{err} with result #{result}" if err
sendEmailCallback err
generateWeekOffset = (originalDate, numberOfWeeks) ->
return (new Date(originalDate.getTime() - numberOfWeeks * 7 * 24 * 60 * 60 * 1000)).toISOString()
candidateUpdateProfileTask = ->
mailTaskName = "candidateUpdateProfileTask"
lockDurationMs = 6000
currentDate = new Date()
timeRanges = []
for weekPair in [[4, 2,'two weeks'], [8, 4, 'four weeks'], [8, 52, 'eight weeks']]
timeRanges.push
start: generateWeekOffset currentDate, weekPair[0]
end: generateWeekOffset currentDate, weekPair[1]
name: weekPair[2]
lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) ->
if err? then return log.error "Error getting a task lock!"
async.each timeRanges, emailTimeRange.bind(mailTaskName: mailTaskName), (err) ->
if err then return log.error JSON.stringify err else log.info "Sent candidate update reminders!"
lockManager.releaseLock mailTaskName, (err, result) -> if err? then return log.error err
### End Candidate Update Reminder Task ###
### Internal Candidate Update Reminder Email ###
emailInternalCandidateUpdateReminder = (cb) ->
currentTime = new Date()
beginningOfUTCDay = new Date()
beginningOfUTCDay.setUTCHours(0,0,0,0)
asyncContext =
"beginningOfUTCDay": beginningOfUTCDay
"currentTime": currentTime
"mailTaskName": @mailTaskName
async.waterfall [
findCandidatesWhoUpdatedJobProfileToday.bind(asyncContext)
(unfilteredCandidates, cb) ->
async.reject unfilteredCandidates, candidatesUpdatedTodayFilter.bind(asyncContext), cb.bind(null,null)
(filteredCandidates, cb) ->
async.each filteredCandidates, sendInternalCandidateUpdateReminder.bind(asyncContext), cb
], cb
findCandidatesWhoUpdatedJobProfileToday = (cb) ->
findParameters =
"jobProfile.updated":
$lte: @currentTime.toISOString()
gt: @beginningOfUTCDay.toISOString()
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? then return errors.serverError("Error fetching sent mail in #{@mailTaskName}")
cb Boolean(sentMail.length)
sendInternalCandidateUpdateReminder = (candidate, cb) ->
context =
email_id: "tem_Ac7nhgKqatTHBCgDgjF5pE"
recipient:
address: "team@codecombat.com" #Change to whatever email address is necessary
name: "The CodeCombat Team"
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 ladder update email: #{err} with result #{result}" if err
cb err
internalCandidateUpdateTask = ->
mailTaskName = "internalCandidateUpdateTask"
lockDurationMs = 6000
lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) ->
if err? then return log.error "Error getting a task lock!"
emailInternalCandidateUpdateReminder.apply {"mailTaskName":mailTaskName}, (err) ->
if err? then return log.error "There was an error sending the internal candidate update reminder."
lockManager.releaseLock mailTaskName, (err, result) -> if err? then return log.error err
### End Internal Candidate Update Reminder Email ###
### Employer New Candidates Available Email ###
employerNewCandidatesAvailableTask = ->
# tem_CCcHKr95Nvu5bT7c7iHCtm
#initialize featuredDate to job profile updated
mailTaskName = "employerNewCandidatesAvailableTask"
lockDurationMs = 6000
lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) ->
### End Employer New Candidates Available Email ###
### 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 ###
### Ladder Update Email ###
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
2014-03-12 11:11:48 -04:00
isRequestFromDesignatedCronHandler = (req, res) ->
2014-06-30 22:16:26 -04:00
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']}"
2014-06-30 22:16:26 -04:00
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.')
2014-03-12 11:11:48 -04:00
res.end()
2014-03-13 19:11:44 -04:00
return false
return true
createMailTask = (req, res) ->
#unless req.user?.isAdmin() then return errors.forbidden(res)
unless req.body.url and req.body.frequency then return errors.badInput(res)
console.log "Creating mail task with url #{req.body.url} and frequency #{req.body.frequency}"
newMailTask = new MailTask {}
newMailTask.set("url",req.body.url)
newMailTask.set("frequency",req.body.frequency)
newMailTask.save (err) ->
if err? then return errors.serverError(res, err)
res.send("Created mail task!")
res.end()
2014-03-08 21:49:09 -05:00
handleLadderUpdate = (req, res) ->
2014-06-30 22:16:26 -04:00
log.info('Going to see about sending ladder update emails.')
2014-03-12 11:11:48 -04:00
requestIsFromDesignatedCronHandler = isRequestFromDesignatedCronHandler req, res
return unless requestIsFromDesignatedCronHandler or DEBUGGING
2014-03-09 00:29:41 -05:00
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]
2014-03-08 21:49:09 -05:00
now = new Date()
for daysAgo in emailDays
2014-03-09 00:29:41 -05:00
# 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
2014-03-09 00:29:41 -05:00
findParameters = {submitted: true, submitDate: {$gt: new Date(startTime), $lte: new Date(endTime)}}
2014-03-08 21:49:09 -05:00
# TODO: think about putting screenshots in the email
2014-06-30 22:16:26 -04:00
selectString = 'creator team levelName levelID totalScore matches submitted submitDate scoreHistory'
2014-03-08 21:49:09 -05:00
query = LevelSession.find(findParameters)
.select(selectString)
.lean()
2014-03-09 00:29:41 -05:00
do (daysAgo) ->
query.exec (err, results) ->
if err
2014-03-10 16:20:00 -04:00
log.error "Couldn't fetch ladder updates for #{findParameters}\nError: #{err}"
2014-03-09 00:29:41 -05:00
return errors.serverError res, "Ladder update email query failed: #{JSON.stringify(err)}"
2014-03-10 16:20:00 -04:00
log.info "Found #{results.length} ladder sessions to email updates about for #{daysAgo} day(s) ago."
sendLadderUpdateEmail result, now, daysAgo for result in results
2014-03-08 21:49:09 -05:00
sendLadderUpdateEmail = (session, now, daysAgo) ->
2014-06-30 22:16:26 -04:00
User.findOne({_id: session.creator}).select('name email firstName lastName emailSubscriptions emails preferredLanguage').exec (err, user) ->
2014-03-08 21:49:09 -05:00
if err
2014-03-10 16:20:00 -04:00
log.error "Couldn't find user for #{session.creator} from session #{session._id}"
return
allowNotes = user.isEmailSubscriptionEnabled 'anyNotes'
2014-05-05 18:11:00 -04:00
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}"
2014-03-08 21:49:09 -05:00
return
unless session.levelName
2014-05-05 18:11:00 -04:00
log.info "Not sending email to #{user.get('email')} #{user.get('name')} because the session had no levelName in it."
return
2014-05-05 18:11:00 -04:00
name = if user.get('firstName') and user.get('lastName') then "#{user.get('firstName')}" else user.get('name')
2014-06-30 22:16:26 -04:00
name = 'Wizard' if not name or name is 'Anoner'
2014-03-08 21:49:09 -05:00
# 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
2014-03-11 16:20:52 -04:00
victories = _.filter matches, (match) -> match.metrics.rank is 0 and match.opponents[0].metrics.rank is 1
2014-07-08 19:28:45 -04:00
defeat = _.last defeats
victory = _.last victories
2014-03-08 21:49:09 -05:00
sendEmail = (defeatContext, victoryContext) ->
# TODO: do something with the preferredLanguage?
context =
email_id: sendwithus.templates.ladder_update_email
recipient:
2014-05-05 18:41:02 -04:00
address: if DEBUGGING then 'nick@codecombat.com' else user.get('email')
2014-03-09 00:29:41 -05:00
name: name
email_data:
name: name
days_ago: daysAgo
wins: victories.length
losses: defeats.length
2014-03-09 00:29:41 -05:00
total_score: Math.round(session.totalScore * 100)
team: session.team
team_name: session.team[0].toUpperCase() + session.team.substr(1)
2014-03-09 00:29:41 -05:00
level_name: session.levelName
session_id: session._id
2014-03-09 00:29:41 -05:00
ladder_url: "http://codecombat.com/play/ladder/#{session.levelID}#my-matches"
score_history_graph_url: getScoreHistoryGraphURL session, daysAgo
2014-03-09 00:29:41 -05:00
defeat: defeatContext
victory: victoryContext
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."
2014-03-08 21:49:09 -05:00
sendwithus.api.send context, (err, result) ->
2014-03-10 16:20:00 -04:00
log.error "Error sending ladder update email: #{err} with result #{result}" if err
2014-03-08 21:49:09 -05:00
urlForMatch = (match) ->
2014-03-09 00:29:41 -05:00
"http://codecombat.com/play/level/#{session.levelID}?team=#{session.team}&session=#{session._id}&opponent=#{match.opponents[0].sessionID}"
2014-03-08 21:49:09 -05:00
onFetchedDefeatedOpponent = (err, defeatedOpponent) ->
if err
log.error "Couldn't find defeateded opponent: #{err}"
defeatedOpponent = null
2014-06-30 22:16:26 -04:00
victoryContext = {opponent_name: defeatedOpponent?.name ? 'Anoner', url: urlForMatch(victory)} if victory
2014-03-08 21:49:09 -05:00
onFetchedVictoriousOpponent = (err, victoriousOpponent) ->
if err
log.error "Couldn't find victorious opponent: #{err}"
victoriousOpponent = null
2014-06-30 22:16:26 -04:00
defeatContext = {opponent_name: victoriousOpponent?.name ? 'Anoner', url: urlForMatch(defeat)} if defeat
2014-03-08 21:49:09 -05:00
sendEmail defeatContext, victoryContext
if defeat
2014-06-30 22:16:26 -04:00
User.findOne({_id: defeat.opponents[0].userID}).select('name').lean().exec onFetchedVictoriousOpponent
2014-03-08 21:49:09 -05:00
else
onFetchedVictoriousOpponent null, null
if victory
2014-06-30 22:16:26 -04:00
User.findOne({_id: victory.opponents[0].userID}).select('name').lean().exec onFetchedDefeatedOpponent
2014-03-08 21:49:09 -05:00
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)
2014-03-20 18:40:02 -04:00
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}"
2014-07-16 15:13:21 -04:00
### End Ladder Update Email ###
2014-03-08 21:49:09 -05:00
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()
2014-06-30 22:16:26 -04:00
query = {'mailChimp.leid': post.data.web_id}
2014-03-08 21:49:09 -05:00
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')
2014-01-24 15:23:14 -05:00
module.exports.handleProfileUpdate = handleProfileUpdate = (user, post) ->
mailchimpSubs = post.data.merges.INTERESTS.split(', ')
2014-01-24 15:23:14 -05:00
for [mailchimpEmailGroup, emailGroup] in _.zip(mail.MAILCHIMP_GROUPS, mail.NEWS_GROUPS)
user.setEmailSubscription emailGroup, mailchimpEmailGroup in mailchimpSubs
2014-03-08 21:49:09 -05:00
2014-01-24 20:19:20 -05:00
fname = post.data.merges.FNAME
user.set('firstName', fname) if fname
lname = post.data.merges.LNAME
user.set('lastName', lname) if lname
2014-03-08 21:49:09 -05:00
2014-01-24 20:35:34 -05:00
user.set 'mailChimp.email', post.data.email
user.set 'mailChimp.euid', post.data.id
2014-03-08 21:49:09 -05:00
module.exports.handleUnsubscribe = handleUnsubscribe = (user) ->
2014-01-24 15:23:14 -05:00
user.set 'emailSubscriptions', []
for emailGroup in mail.NEWS_GROUPS
user.setEmailSubscription emailGroup, false