2014-06-30 22:16:26 -04:00
mongoose = require ' mongoose '
jsonschema = require ' ../../app/schemas/models/user '
crypto = require ' crypto '
{ salt , isProduction } = require ' ../../server_config '
2014-01-24 14:47:14 -05:00
mail = require ' ../commons/mail '
2014-03-11 22:17:58 -04:00
log = require ' winston '
2014-07-09 14:23:05 -04:00
plugins = require ' ../plugins/plugins '
2014-12-04 15:57:57 -05:00
AnalyticsUsersActive = require ' ../analytics/AnalyticsUsersActive '
2014-01-03 13:32:13 -05:00
2014-12-03 19:35:53 -05:00
config = require ' ../../server_config '
stripe = require ( ' stripe ' ) ( config . stripe . secretKey )
2014-01-17 12:53:17 -05:00
sendwithus = require ' ../sendwithus '
2014-08-10 02:11:26 -04:00
delighted = require ' ../delighted '
2014-01-17 12:53:17 -05:00
2014-01-03 13:32:13 -05:00
UserSchema = new mongoose . Schema ( {
dateCreated:
type: Date
' default ' : Date . now
2015-01-09 21:30:05 -05:00
} , { strict: false } )
2014-01-03 13:32:13 -05:00
2015-01-27 13:02:47 -05:00
UserSchema . index ( { ' dateCreated ' : 1 } )
UserSchema . index ( { ' emailLower ' : 1 } , { unique: true , sparse: true , name: ' emailLower_1 ' } )
UserSchema . index ( { ' facebookID ' : 1 } , { sparse: true } )
UserSchema . index ( { ' gplusID ' : 1 } , { sparse: true } )
UserSchema . index ( { ' iosIdentifierForVendor ' : 1 } , { name: ' iOS identifier for vendor ' , sparse: true , unique: true } )
UserSchema . index ( { ' mailChimp.leid ' : 1 } , { sparse: true } )
UserSchema . index ( { ' nameLower ' : 1 } , { sparse: true , name: ' nameLower_1 ' } )
UserSchema . index ( { ' simulatedBy ' : 1 } )
UserSchema . index ( { ' slug ' : 1 } , { name: ' slug index ' , sparse: true , unique: true } )
UserSchema . index ( { ' stripe.subscriptionID ' : 1 } , { unique: true , sparse: true } )
2015-03-09 20:50:36 -04:00
UserSchema . index ( { ' siteref ' : 1 } , { name: ' siteref index ' , sparse: true } )
2015-12-09 14:55:46 -05:00
UserSchema . index ( { ' schoolName ' : 1 } , { name: ' schoolName index ' , sparse: true } )
2016-01-11 12:50:44 -05:00
UserSchema . index ( { ' country ' : 1 } , { name: ' country index ' , sparse: true } )
2016-02-02 15:48:19 -05:00
UserSchema . index ( { ' role ' : 1 } , { name: ' role index ' , sparse: true } )
2015-01-27 13:02:47 -05:00
2014-01-03 13:32:13 -05:00
UserSchema . post ( ' init ' , ->
@ set ( ' anonymous ' , false ) if @ get ( ' email ' )
)
2015-02-12 14:42:05 -05:00
UserSchema.methods.isInGodMode = ->
p = @ get ( ' permissions ' )
return p and ' godmode ' in p
2014-01-03 13:32:13 -05:00
UserSchema.methods.isAdmin = ->
p = @ get ( ' permissions ' )
return p and ' admin ' in p
2015-12-16 20:09:22 -05:00
UserSchema.methods.hasPermission = (neededPermissions) ->
permissions = @ get ( ' permissions ' ) or [ ]
if _ . contains ( permissions , ' admin ' )
return true
if _ . isString ( neededPermissions )
neededPermissions = [ neededPermissions ]
return _ . size ( _ . intersection ( permissions , neededPermissions ) )
2014-04-25 14:12:52 -04:00
2015-02-25 21:41:39 -05:00
UserSchema.methods.isArtisan = ->
p = @ get ( ' permissions ' )
return p and ' artisan ' in p
2014-07-10 12:00:32 -04:00
UserSchema.methods.isAnonymous = ->
@ get ' anonymous '
2015-02-17 04:12:12 -05:00
UserSchema.methods.getUserInfo = ->
2015-08-13 14:17:38 -04:00
id: @ get ( ' _id ' )
email: if @ get ( ' anonymous ' ) then ' Unregistered User ' else @ get ( ' email ' )
2015-02-17 04:12:12 -05:00
2014-06-10 19:30:07 -04:00
UserSchema.methods.trackActivity = (activityName, increment) ->
now = new Date ( )
increment ? = parseInt increment or 1
increment = Math . max increment , 0
activity = @ get ( ' activity ' ) ? { }
activity [ activityName ] ? = { first: now , count: 0 }
activity [ activityName ] . count += increment
activity [ activityName ] . last = now
@ set ' activity ' , activity
activity
2014-04-21 19:15:23 -04:00
emailNameMap =
generalNews: ' announcement '
adventurerNews: ' tester '
artisanNews: ' level_creator '
archmageNews: ' developer '
scribeNews: ' article_editor '
diplomatNews: ' translator '
ambassadorNews: ' support '
anyNotes: ' notification '
2016-01-17 10:39:30 -05:00
teacherNews: ' teacher '
2014-04-25 14:12:52 -04:00
2014-04-21 19:15:23 -04:00
UserSchema.methods.setEmailSubscription = (newName, enabled) ->
oldSubs = _ . clone @ get ( ' emailSubscriptions ' )
if oldSubs and oldName = emailNameMap [ newName ]
oldSubs = ( s for s in oldSubs when s isnt oldName )
oldSubs . push ( oldName ) if enabled
@ set ( ' emailSubscriptions ' , oldSubs )
2014-04-25 14:12:52 -04:00
2014-04-23 12:19:07 -04:00
newSubs = _ . clone ( @ get ( ' emails ' ) or _ . cloneDeep ( jsonschema . properties . emails . default ) )
2014-04-21 19:15:23 -04:00
newSubs [ newName ] ? = { }
newSubs [ newName ] . enabled = enabled
@ set ( ' emails ' , newSubs )
@newsSubsChanged = true if newName in mail . NEWS_GROUPS
2014-12-08 11:09:31 -05:00
2014-11-01 17:15:57 -04:00
UserSchema.methods.gems = ->
gemsEarned = @ get ( ' earned ' ) ? . gems ? 0
2015-02-12 14:42:05 -05:00
gemsEarned = gemsEarned + 100000 if @ isInGodMode ( )
2014-11-01 17:15:57 -04:00
gemsPurchased = @ get ( ' purchased ' ) ? . gems ? 0
gemsSpent = @ get ( ' spent ' ) ? 0
gemsEarned + gemsPurchased - gemsSpent
2014-04-25 14:12:52 -04:00
2014-04-21 19:15:23 -04:00
UserSchema.methods.isEmailSubscriptionEnabled = (newName) ->
emails = @ get ' emails '
if not emails
oldSubs = @ get ( ' emailSubscriptions ' )
oldName = emailNameMap [ newName ]
return oldName and oldName in oldSubs if oldSubs
emails ? = { }
2014-04-23 12:19:07 -04:00
_ . defaults emails , _ . cloneDeep ( jsonschema . properties . emails . default )
2014-04-21 19:15:23 -04:00
return emails [ newName ] ? . enabled
2014-03-11 22:17:58 -04:00
2014-12-03 19:35:53 -05:00
UserSchema.statics.updateServiceSettings = (doc, callback) ->
2014-04-21 19:15:23 -04:00
return callback ? ( ) unless isProduction or GLOBAL . testing
2014-01-03 13:32:13 -05:00
return callback ? ( ) if doc . updatedMailChimp
return callback ? ( ) unless doc . get ( ' email ' )
existingProps = doc . get ( ' mailChimp ' )
emailChanged = ( not existingProps ) or existingProps ? . email isnt doc . get ( ' email ' )
2014-12-08 11:09:31 -05:00
2014-12-03 19:35:53 -05:00
if emailChanged and customerID = doc . get ( ' stripe ' ) ? . customerID
2014-12-08 11:09:31 -05:00
unless stripe ? . customers
console . error ( ' Oh my god, Stripe is not imported correctly-how could we have done this (again)? ' )
stripe ? . customers ? . update customerID , { email : doc . get ( ' email ' ) } , (err, customer) ->
2014-12-03 19:35:53 -05:00
console . error ( ' Error updating stripe customer... ' , err ) if err
2014-12-08 11:09:31 -05:00
2014-04-21 19:15:23 -04:00
return callback ? ( ) unless emailChanged or doc . newsSubsChanged
newGroups = [ ]
for [ mailchimpEmailGroup , emailGroup ] in _ . zip ( mail . MAILCHIMP_GROUPS , mail . NEWS_GROUPS )
newGroups . push ( mailchimpEmailGroup ) if doc . isEmailSubscriptionEnabled ( emailGroup )
2014-04-25 14:12:52 -04:00
2014-01-03 13:32:13 -05:00
if ( not existingProps ) and newGroups . length is 0
return callback ? ( ) # don't add totally unsubscribed people to the list
2014-04-25 14:12:52 -04:00
2014-01-03 13:32:13 -05:00
params = { }
2014-01-24 14:47:14 -05:00
params.id = mail . MAILCHIMP_LIST_ID
2014-06-30 22:16:26 -04:00
params.email = if existingProps then { leid: existingProps . leid } else { email: doc . get ( ' email ' ) }
2014-12-03 18:26:39 -05:00
params.merge_vars = {
groupings: [ { id: mail . MAILCHIMP_GROUP_ID , groups: newGroups } ]
' new-email ' : doc . get ( ' email ' )
}
2014-01-03 13:32:13 -05:00
params.update_existing = true
2014-03-11 22:17:58 -04:00
2014-01-03 13:32:13 -05:00
onSuccess = (data) ->
2015-03-23 18:26:44 -04:00
data.email = doc . get ( ' email ' ) # Make sure that we don't spam opt-in emails even if MailChimp doesn't update the email it gets in this object until they have confirmed.
2014-01-03 13:32:13 -05:00
doc . set ( ' mailChimp ' , data )
doc.updatedMailChimp = true
doc . save ( )
callback ? ( )
2014-03-11 22:17:58 -04:00
2014-01-03 13:32:13 -05:00
onFailure = (error) ->
2014-03-11 22:17:58 -04:00
log . error ' failed to subscribe ' , error , callback ?
2014-01-03 13:32:13 -05:00
doc.updatedMailChimp = true
callback ? ( )
2014-03-11 22:17:58 -04:00
2015-12-09 17:27:10 -05:00
mc ? . lists . subscribe params , onSuccess , onFailure
2014-01-03 13:32:13 -05:00
2014-06-27 15:30:31 -04:00
UserSchema.statics.statsMapping =
edits:
article: ' stats.articleEdits '
level: ' stats.levelEdits '
' level.component ' : ' stats.levelComponentEdits '
' level.system ' : ' stats.levelSystemEdits '
' thang.type ' : ' stats.thangTypeEdits '
2015-03-28 16:54:44 -04:00
' Achievement ' : ' stats.achievementEdits '
' campaign ' : ' stats.campaignEdits '
' poll ' : ' stats.pollEdits '
2014-06-28 11:44:07 -04:00
translations:
article: ' stats.articleTranslationPatches '
level: ' stats.levelTranslationPatches '
' level.component ' : ' stats.levelComponentTranslationPatches '
' level.system ' : ' stats.levelSystemTranslationPatches '
' thang.type ' : ' stats.thangTypeTranslationPatches '
2015-03-28 16:54:44 -04:00
' Achievement ' : ' stats.achievementTranslationPatches '
' campaign ' : ' stats.campaignTranslationPatches '
' poll ' : ' stats.pollTranslationPatches '
2014-06-28 11:44:07 -04:00
misc:
article: ' stats.articleMiscPatches '
level: ' stats.levelMiscPatches '
' level.component ' : ' stats.levelComponentMiscPatches '
' level.system ' : ' stats.levelSystemMiscPatches '
' thang.type ' : ' stats.thangTypeMiscPatches '
2015-03-28 16:54:44 -04:00
' Achievement ' : ' stats.achievementMiscPatches '
' campaign ' : ' stats.campaignMiscPatches '
' poll ' : ' stats.pollMiscPatches '
2014-09-11 00:56:21 -04:00
2014-06-27 15:30:31 -04:00
UserSchema.statics.incrementStat = (id, statName, done, inc=1) ->
2014-07-24 08:41:06 -04:00
id = mongoose . Types . ObjectId id if _ . isString id
@ findById id , (err, user) ->
log . error err if err ?
err = new Error " Could ' t find user with id ' #{ id } ' " unless user or err
2014-09-03 20:26:43 -04:00
return done ( ) if err ?
2015-03-28 16:54:44 -04:00
user . incrementStat statName , done , inc
2014-07-05 11:02:48 -04:00
UserSchema.methods.incrementStat = (statName, done, inc=1) ->
2015-11-04 16:42:01 -05:00
if /^concepts\./ . test statName
# Concept stats are nested a level deeper.
concepts = @ get ( ' concepts ' ) or { }
concept = statName . split ( ' . ' ) [ 1 ]
concepts [ concept ] = ( concepts [ concept ] or 0 ) + inc
@ set ' concepts ' , concepts
else
@ set statName , ( @ get ( statName ) or 0 ) + inc
2014-08-07 16:03:00 -04:00
@ save (err) -> done ? ( err )
2014-01-03 13:32:13 -05:00
2014-07-10 12:00:32 -04:00
UserSchema.statics.unconflictName = unconflictName = (name, done) ->
User . findOne { slug: _ . str . slugify ( name ) } , (err, otherUser) ->
return done err if err ?
return done null , name unless otherUser
suffix = _ . random ( 0 , 9 ) + ' '
unconflictName name + suffix , done
UserSchema.methods.register = (done) ->
@ set ( ' anonymous ' , false )
if ( name = @ get ' name ' ) ? and name isnt ' '
unconflictName name , (err, uniqueName) =>
return done err if err
@ set ' name ' , uniqueName
done ( )
else done ( )
2015-04-27 18:06:26 -04:00
if @ isEmailSubscriptionEnabled ' generalNews '
data =
email_id: sendwithus . templates . welcome_email
recipient:
address: @ get ' email '
sendwithus . api . send data , (err, result) ->
log . error " sendwithus post-save error: #{ err } , result: #{ result } " if err
delighted . addDelightedUser @
2014-12-04 15:57:57 -05:00
@ saveActiveUser ' register '
2015-08-21 14:10:06 -04:00
UserSchema.methods.hasSubscription = ->
2014-12-08 11:09:31 -05:00
return false unless stripeObject = @ get ( ' stripe ' )
2015-03-13 18:19:20 -04:00
return true if stripeObject . sponsorID
2014-12-08 11:09:31 -05:00
return true if stripeObject . subscriptionID
return true if stripeObject . free is true
return true if _ . isString ( stripeObject . free ) and new Date ( ) < new Date ( stripeObject . free )
2015-08-21 14:10:06 -04:00
UserSchema.methods.isPremium = ->
return true if @ isInGodMode ( )
return true if @ isAdmin ( )
return true if @ hasSubscription ( )
2014-12-05 20:19:44 -05:00
return false
2016-02-25 18:24:16 -05:00
UserSchema.methods.formatEntity = (req, publicOnly=false) ->
obj = @ toObject ( )
serverProperties = [ ' passwordHash ' , ' emailLower ' , ' nameLower ' , ' passwordReset ' , ' lastIP ' ]
delete obj [ prop ] for prop in serverProperties
candidateProperties = [ ' jobProfile ' , ' jobProfileApproved ' , ' jobProfileNotes ' ]
delete obj [ prop ] for prop in candidateProperties
includePrivates = not publicOnly and ( req . user and ( req . user . isAdmin ( ) or req . user . _id . equals ( @ _id ) ) )
delete obj [ prop ] for prop in User . privateProperties unless includePrivates
return obj
2015-10-09 11:05:34 -04:00
UserSchema.methods.isOnPremiumServer = ->
@ get ( ' country ' ) in [ ' china ' , ' brazil ' ]
2015-03-07 19:30:25 -05:00
UserSchema.methods.level = ->
xp = @ get ( ' points ' ) or 0
a = 5
b = c = 100
2016-03-22 16:57:00 -04:00
if xp > 0 then Math . floor ( a * Math . log ( ( 1 / b ) * ( xp + c ) ) ) + 1 else 1
2015-03-07 19:30:25 -05:00
2014-12-04 15:57:57 -05:00
UserSchema.statics.saveActiveUser = (id, event, done=null) ->
2014-12-17 17:17:50 -05:00
# TODO: Disabling this until we know why our app servers CPU grows out of control.
return done ? ( )
2014-12-04 15:57:57 -05:00
id = mongoose . Types . ObjectId id if _ . isString id
@ findById id , (err, user) ->
if err ?
log . error err
else
user ? . saveActiveUser event
done ? ( )
UserSchema.methods.saveActiveUser = (event, done=null) ->
2014-12-17 17:17:50 -05:00
# TODO: Disabling this until we know why our app servers CPU grows out of control.
return done ? ( )
2014-12-04 15:57:57 -05:00
try
return done ? ( ) if @ isAdmin ( )
userID = @ get ( ' _id ' )
2014-12-08 11:09:31 -05:00
2014-12-04 15:57:57 -05:00
# Create if no active user entry for today
today = new Date ( )
minDate = new Date ( Date . UTC ( today . getUTCFullYear ( ) , today . getUTCMonth ( ) , today . getUTCDate ( ) ) )
AnalyticsUsersActive . findOne ( { created: { $gte: minDate } , creator: mongoose . Types . ObjectId ( userID ) } ) . exec (err, activeUser) ->
if err ?
log . error " saveActiveUser error retrieving active users: #{ err } "
else if not activeUser
newActiveUser = new AnalyticsUsersActive ( )
newActiveUser . set ' creator ' , userID
newActiveUser . set ' event ' , event
newActiveUser . save (err) ->
log . error " Level session saveActiveUser error saving active user: #{ err } " if err ?
done ? ( )
catch err
log . error err
done ? ( )
2014-07-10 12:00:32 -04:00
2014-01-03 13:32:13 -05:00
UserSchema . pre ( ' save ' , (next) ->
2015-01-12 14:51:42 -05:00
if email = @ get ( ' email ' )
@ set ( ' emailLower ' , email . toLowerCase ( ) )
if name = @ get ( ' name ' )
@ set ( ' nameLower ' , name . toLowerCase ( ) )
2014-01-03 13:32:13 -05:00
pwd = @ get ( ' password ' )
if @ get ( ' password ' )
@ set ( ' passwordHash ' , User . hashPassword ( pwd ) )
@ set ( ' password ' , undefined )
2014-07-10 12:00:32 -04:00
if @ get ( ' email ' ) and @ get ( ' anonymous ' ) # a user registers
@ register next
else
next ( )
2014-01-03 13:32:13 -05:00
)
UserSchema . post ' save ' , (doc) ->
2014-12-03 18:26:39 -05:00
doc.newsSubsChanged = not _ . isEqual ( _ . pick ( doc . get ( ' emails ' ) , mail . NEWS_GROUPS ) , _ . pick ( doc . startingEmails , mail . NEWS_GROUPS ) )
2014-12-03 19:35:53 -05:00
UserSchema . statics . updateServiceSettings ( doc )
2014-01-03 13:32:13 -05:00
2014-12-03 18:26:39 -05:00
UserSchema . post ' init ' , (doc) ->
doc.startingEmails = _ . cloneDeep ( doc . get ( ' emails ' ) )
2014-12-08 11:09:31 -05:00
2014-01-03 13:32:13 -05:00
UserSchema.statics.hashPassword = (password) ->
password = password . toLowerCase ( )
shasum = crypto . createHash ( ' sha512 ' )
shasum . update ( salt + password )
shasum . digest ( ' hex ' )
2014-07-22 14:07:00 -04:00
UserSchema.statics.privateProperties = [
2014-09-11 00:56:21 -04:00
' permissions ' , ' email ' , ' mailChimp ' , ' firstName ' , ' lastName ' , ' gender ' , ' facebookID ' ,
' gplusID ' , ' music ' , ' volume ' , ' aceConfig ' , ' employerAt ' , ' signedEmployerAgreement ' ,
2015-12-01 20:32:02 -05:00
' emailSubscriptions ' , ' emails ' , ' activity ' , ' stripe ' , ' stripeCustomerID ' , ' chinaVersion ' , ' country ' ,
2016-02-02 15:48:19 -05:00
' schoolName ' , ' ageRange ' , ' role '
2014-07-22 14:07:00 -04:00
]
UserSchema.statics.jsonSchema = jsonschema
UserSchema.statics.editableProperties = [
' name ' , ' photoURL ' , ' password ' , ' anonymous ' , ' wizardColor1 ' , ' volume ' ,
2015-03-09 12:30:51 -04:00
' firstName ' , ' lastName ' , ' gender ' , ' ageRange ' , ' facebookID ' , ' gplusID ' , ' emails ' ,
2014-07-22 14:07:00 -04:00
' testGroupNumber ' , ' music ' , ' hourOfCode ' , ' hourOfCodeComplete ' , ' preferredLanguage ' ,
2014-09-20 18:18:21 -04:00
' wizard ' , ' aceConfig ' , ' autocastDelay ' , ' lastLevel ' , ' jobProfile ' , ' savedEmployerFilterAlerts ' ,
2016-02-02 15:48:19 -05:00
' heroConfig ' , ' iosIdentifierForVendor ' , ' siteref ' , ' referrer ' , ' schoolName ' , ' role '
2014-07-22 14:07:00 -04:00
]
2016-03-03 17:22:50 -05:00
UserSchema.statics.serverProperties = [ ' passwordHash ' , ' emailLower ' , ' nameLower ' , ' passwordReset ' , ' lastIP ' ]
UserSchema.statics.candidateProperties = [ ' jobProfile ' , ' jobProfileApproved ' , ' jobProfileNotes ' ]
UserSchema . set ( ' toObject ' , {
transform: (doc, ret, options) ->
req = options . req
return ret unless req # TODO: Make deleting properties the default, but the consequences are far reaching
publicOnly = options . publicOnly
delete ret [ prop ] for prop in User . serverProperties
includePrivates = not publicOnly and ( req . user and ( req . user . isAdmin ( ) or req . user . _id . equals ( doc . _id ) or req . session . amActually is doc . id ) )
delete ret [ prop ] for prop in User . privateProperties unless includePrivates
delete ret [ prop ] for prop in User . candidateProperties
return ret
} )
2014-07-09 14:23:05 -04:00
UserSchema . plugin plugins . NamedPlugin
2014-03-11 22:17:58 -04:00
module.exports = User = mongoose . model ( ' User ' , UserSchema )
2014-05-31 17:19:55 -04:00
AchievablePlugin = require ' ../plugins/achievements '
UserSchema . plugin ( AchievablePlugin )