From 4b60698527416ba099288249baa2f0f76b249f83 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Sun, 8 Nov 2015 16:11:55 +0100 Subject: [PATCH] migrate user model to new code style and es6 --- .eslintrc | 4 +- tasks/gulp-eslint.js | 5 +- website/src/models/user.js | 797 +++++++++++++++++++------------------ 3 files changed, 414 insertions(+), 392 deletions(-) diff --git a/.eslintrc b/.eslintrc index 2b2ebd92ab..69c945431e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -22,8 +22,6 @@ "no-loop-func": 2, "no-implicit-coercion": 2, "no-implied-eval": 2, - "no-invalid-this": 2, - "no-magic-numbers": 2, "no-native-reassign": 2, "no-new-func": 2, "no-new-wrappers": 2, @@ -97,7 +95,7 @@ "no-nested-ternary": 2, "one-var": [2, "never"], "operator-linebreak": [2, "after"], - "quote-props": [2, "as-needed", { "keywords": true }], + "quote-props": [2, "as-needed"], "semi-spacing": [2, {"before": false, "after": true}], "space-after-keywords": 2, "space-before-blocks": 2, diff --git a/tasks/gulp-eslint.js b/tasks/gulp-eslint.js index 7a30f35ca1..eaee185f5c 100644 --- a/tasks/gulp-eslint.js +++ b/tasks/gulp-eslint.js @@ -9,7 +9,10 @@ import eslint from 'gulp-eslint'; gulp.task('lint:server', () => { // Ignore .coffee files return gulp - .src(['./website/src/**/api-v3/**/*.js']) + .src([ + './website/src/**/api-v3/**/*.js', + './website/src/models/user.js' + ]) .pipe(eslint()) .pipe(eslint.format()) .pipe(eslint.failAfterError()); diff --git a/website/src/models/user.js b/website/src/models/user.js index e36bedef11..47b790785f 100644 --- a/website/src/models/user.js +++ b/website/src/models/user.js @@ -1,39 +1,52 @@ -// User.js -// ======= -// Defines the user data model (schema) for use via the API. +// User schema and model +import mongoose from 'mongoose'; +import shared from '../../../common'; +import _ from 'lodash'; +import moment from 'moment'; +import TaskSchemas from './task'; +// import {model as Challenge} from './challenge'; -// Dependencies -// ------------ -var mongoose = require("mongoose"); -var Schema = mongoose.Schema; -var shared = require('../../../common'); -var _ = require('lodash'); -var TaskSchemas = require('./task'); -var Challenge = require('./challenge').model; -var moment = require('moment'); +let Schema = mongoose.Schema; -// User Schema -// ----------- - -var UserSchema = new Schema({ - // ### UUID and API Token +// User schema definition +export let schema = new Schema({ + // The user _id, stored as a string + // TODO validation _id: { type: String, - 'default': shared.uuid + default: shared.uuid, }, + // TODO validation apiToken: { type: String, - 'default': shared.uuid + default: shared.uuid, + }, + + auth: { + blocked: Boolean, + facebook: Schema.Types.Mixed, // TODO validate + local: { + email: String, + hashed_password: String, + salt: String, + username: String, + // Store a lowercase version of username to check for duplicates + lowerCaseUsername: String, + }, + timestamps: { + created: {type: Date, default: Date.now}, + loggedin: {type: Date, default: Date.now}, + }, }, - // ### Mongoose Update Object // We want to know *every* time an object updates. Mongoose uses __v to designate when an object contains arrays which // have been updated (http://goo.gl/gQLz41), but we want *every* update - _v: { type: Number, 'default': 0 }, + _v: { type: Number, default: 0 }, + achievements: { originalUser: Boolean, habitSurveys: Number, - ultimateGearSets: Schema.Types.Mixed, + ultimateGearSets: Schema.Types.Mixed, // TODO remove, use dictionary? beastMaster: Boolean, beastMasterCount: Number, mountMaster: Boolean, @@ -47,7 +60,7 @@ var UserSchema = new Schema({ seafoam: Number, streak: Number, challenges: Array, - quests: Schema.Types.Mixed, + quests: Schema.Types.Mixed, // TODO remove, use dictionary? rebirths: Number, rebirthLevel: Number, perfect: Number, @@ -57,183 +70,178 @@ var UserSchema = new Schema({ nye: Number, habiticaDays: Number, greeting: Number, - thankyou: Number - }, - auth: { - blocked: Boolean, - facebook: Schema.Types.Mixed, - local: { - email: String, - hashed_password: String, - salt: String, - username: String, - lowerCaseUsername: String // Store a lowercase version of username to check for duplicates - }, - timestamps: { - created: {type: Date,'default': Date.now}, - loggedin: {type: Date,'default': Date.now} - } + thankyou: Number, }, backer: { tier: Number, npc: String, - tokensApplied: Boolean + tokensApplied: Boolean, }, contributor: { - level: Number, // 1-9, see https://trello.com/c/wkFzONhE/277-contributor-gear https://github.com/HabitRPG/habitrpg/issues/3801 + // 1-9, see https://trello.com/c/wkFzONhE/277-contributor-gear https://github.com/HabitRPG/habitrpg/issues/3801 + // TODO validate + level: Number, admin: Boolean, sudo: Boolean, - text: String, // Artisan, Friend, Blacksmith, etc - contributions: String, // a markdown textarea to list their contributions + links - critical: String + // Artisan, Friend, Blacksmith, etc + text: String, + // a markdown textarea to list their contributions + links + contributions: String, + critical: String, }, - balance: {type: Number, 'default':0}, - filters: {type: Schema.Types.Mixed, 'default': {}}, + balance: {type: Number, default: 0}, + filters: {type: Schema.Types.Mixed, default: {}}, // TODO dictionary purchased: { - ads: {type: Boolean, 'default': false}, - skin: {type: Schema.Types.Mixed, 'default': {}}, // eg, {skeleton: true, pumpkin: true, eb052b: true} - hair: {type: Schema.Types.Mixed, 'default': {}}, - shirt: {type: Schema.Types.Mixed, 'default': {}}, - background: {type: Schema.Types.Mixed, 'default': {}}, - txnCount: {type: Number, 'default':0}, + ads: {type: Boolean, default: false}, + // eg, {skeleton: true, pumpkin: true, eb052b: true} + // TODO dictionary + skin: {type: Schema.Types.Mixed, default: {}}, + hair: {type: Schema.Types.Mixed, default: {}}, + shirt: {type: Schema.Types.Mixed, default: {}}, + background: {type: Schema.Types.Mixed, default: {}}, + txnCount: {type: Number, default: 0}, mobileChat: Boolean, plan: { planId: String, - paymentMethod: String, //enum: ['Paypal','Stripe', 'Gift', 'Amazon Payments', '']} + paymentMethod: String, // enum: ['Paypal','Stripe', 'Gift', 'Amazon Payments', '']} customerId: String, // Billing Agreement Id in case of Amazon Payments dateCreated: Date, dateTerminated: Date, dateUpdated: Date, - extraMonths: {type:Number, 'default':0}, - gemsBought: {type: Number, 'default': 0}, - mysteryItems: {type: Array, 'default': []}, + extraMonths: {type: Number, default: 0}, + gemsBought: {type: Number, default: 0}, + mysteryItems: {type: Array, default: []}, lastBillingDate: Date, // Used only for Amazon Payments to keep track of billing date consecutive: { - count: {type:Number, 'default':0}, - offset: {type:Number, 'default':0}, // when gifted subs, offset++ for each month. offset-- each new-month (cron). count doesn't ++ until offset==0 - gemCapExtra: {type:Number, 'default':0}, - trinkets: {type:Number, 'default':0} - } - } + count: {type: Number, default: 0}, + offset: {type: Number, default: 0}, // when gifted subs, offset++ for each month. offset-- each new-month (cron). count doesn't ++ until offset==0 + gemCapExtra: {type: Number, default: 0}, + trinkets: {type: Number, default: 0}, + }, + }, }, flags: { - customizationsNotification: {type: Boolean, 'default': false}, - showTour: {type: Boolean, 'default': true}, + customizationsNotification: {type: Boolean, default: false}, + showTour: {type: Boolean, default: true}, tour: { // -1 indicates "uninitiated", -2 means "complete", any other number is the current tour step (0-index) - intro: {type: Number, 'default': -1}, - classes: {type: Number, 'default': -1}, - stats: {type: Number, 'default': -1}, - tavern: {type: Number, 'default': -1}, - party: {type: Number, 'default': -1}, - guilds: {type: Number, 'default': -1}, - challenges: {type: Number, 'default': -1}, - market: {type: Number, 'default': -1}, - pets: {type: Number, 'default': -1}, - mounts: {type: Number, 'default': -1}, - hall: {type: Number, 'default': -1}, - equipment: {type: Number, 'default': -1} + intro: {type: Number, default: -1}, + classes: {type: Number, default: -1}, + stats: {type: Number, default: -1}, + tavern: {type: Number, default: -1}, + party: {type: Number, default: -1}, + guilds: {type: Number, default: -1}, + challenges: {type: Number, default: -1}, + market: {type: Number, default: -1}, + pets: {type: Number, default: -1}, + mounts: {type: Number, default: -1}, + hall: {type: Number, default: -1}, + equipment: {type: Number, default: -1}, }, tutorial: { common: { - habits: {type: Boolean, 'default': false}, - dailies: {type: Boolean, 'default': false}, - todos: {type: Boolean, 'default': false}, - rewards: {type: Boolean, 'default': false}, - party: {type: Boolean, 'default': false}, - pets: {type: Boolean, 'default': false}, - gems: {type: Boolean, 'default': false}, - skills: {type: Boolean, 'default': false}, - classes: {type: Boolean, 'default': false}, - tavern: {type: Boolean, 'default': false}, - equipment: {type: Boolean, 'default': false}, - items: {type: Boolean, 'default': false}, + habits: {type: Boolean, default: false}, + dailies: {type: Boolean, default: false}, + todos: {type: Boolean, default: false}, + rewards: {type: Boolean, default: false}, + party: {type: Boolean, default: false}, + pets: {type: Boolean, default: false}, + gems: {type: Boolean, default: false}, + skills: {type: Boolean, default: false}, + classes: {type: Boolean, default: false}, + tavern: {type: Boolean, default: false}, + equipment: {type: Boolean, default: false}, + items: {type: Boolean, default: false}, }, ios: { - addTask: {type: Boolean, 'default': false}, - editTask: {type: Boolean, 'default': false}, - deleteTask: {type: Boolean, 'default': false}, - filterTask: {type: Boolean, 'default': false}, - groupPets: {type: Boolean, 'default': false}, - } + addTask: {type: Boolean, default: false}, + editTask: {type: Boolean, default: false}, + deleteTask: {type: Boolean, default: false}, + filterTask: {type: Boolean, default: false}, + groupPets: {type: Boolean, default: false}, + }, }, - dropsEnabled: {type: Boolean, 'default': false}, - itemsEnabled: {type: Boolean, 'default': false}, - newStuff: {type: Boolean, 'default': false}, - rewrite: {type: Boolean, 'default': true}, + dropsEnabled: {type: Boolean, default: false}, + itemsEnabled: {type: Boolean, default: false}, + newStuff: {type: Boolean, default: false}, + rewrite: {type: Boolean, default: true}, contributor: Boolean, - classSelected: {type: Boolean, 'default': false}, + classSelected: {type: Boolean, default: false}, mathUpdates: Boolean, - rebirthEnabled: {type: Boolean, 'default': false}, - levelDrops: {type:Schema.Types.Mixed, 'default':{}}, + rebirthEnabled: {type: Boolean, default: false}, + levelDrops: {type: Schema.Types.Mixed, default: {}}, chatRevoked: Boolean, // Used to track the status of recapture emails sent to each user, // can be 0 - no email sent - 1, 2, 3 or 4 - 4 means no more email will be sent to the user - recaptureEmailsPhase: {type: Number, 'default': 0}, + recaptureEmailsPhase: {type: Number, default: 0}, // Needed to track the tip to send inside the email - weeklyRecapEmailsPhase: {type: Number, 'default': 0}, + weeklyRecapEmailsPhase: {type: Number, default: 0}, // Used to track when the next weekly recap should be sent - lastWeeklyRecap: {type: Date, 'default': Date.now}, + lastWeeklyRecap: {type: Date, default: Date.now}, // Used to enable weekly recap emails as users login lastWeeklyRecapDiscriminator: Boolean, - communityGuidelinesAccepted: {type: Boolean, 'default': false}, - cronCount: {type:Number, 'default':0}, - welcomed: {type: Boolean, 'default': false}, - armoireEnabled: {type: Boolean, 'default': false}, - armoireOpened: {type: Boolean, 'default': false}, - armoireEmpty: {type: Boolean, 'default': false}, - cardReceived: {type: Boolean, 'default': false}, - warnedLowHealth: {type: Boolean, 'default': false} + communityGuidelinesAccepted: {type: Boolean, default: false}, + cronCount: {type: Number, default: 0}, + welcomed: {type: Boolean, default: false}, + armoireEnabled: {type: Boolean, default: false}, + armoireOpened: {type: Boolean, default: false}, + armoireEmpty: {type: Boolean, default: false}, + cardReceived: {type: Boolean, default: false}, + warnedLowHealth: {type: Boolean, default: false}, }, + history: { + // TODO absolutely preen these for everyone exp: Array, // [{date: Date, value: Number}], // big peformance issues if these are defined - todos: Array //[{data: Date, value: Number}] // big peformance issues if these are defined + todos: Array, // [{data: Date, value: Number}] // big peformance issues if these are defined }, invitations: { - guilds: {type: Array, 'default': []}, - party: Schema.Types.Mixed + guilds: {type: Array, default: []}, + party: Schema.Types.Mixed, // TODO dictionary }, + items: { gear: { - owned: _.transform(shared.content.gear.flat, function(m,v,k){ + owned: _.transform(shared.content.gear.flat, (m, v, k) => { m[v.key] = {type: Boolean}; - if (v.key.match(/[armor|head|shield]_warrior_0/)) - m[v.key]['default'] = true; + if (v.key.match(/[armor|head|shield]_warrior_0/)) { + m[v.key].default = true; + } }), equipped: { weapon: String, - armor: {type: String, 'default': 'armor_base_0'}, - head: {type: String, 'default': 'head_base_0'}, - shield: {type: String, 'default': 'shield_base_0'}, + armor: {type: String, default: 'armor_base_0'}, + head: {type: String, default: 'head_base_0'}, + shield: {type: String, default: 'shield_base_0'}, back: String, headAccessory: String, eyewear: String, - body: String + body: String, }, costume: { weapon: String, - armor: {type: String, 'default': 'armor_base_0'}, - head: {type: String, 'default': 'head_base_0'}, - shield: {type: String, 'default': 'shield_base_0'}, + armor: {type: String, default: 'armor_base_0'}, + head: {type: String, default: 'head_base_0'}, + shield: {type: String, default: 'shield_base_0'}, back: String, headAccessory: String, eyewear: String, - body: String - } + body: String, + }, }, - special:{ - snowball: {type: Number, 'default': 0}, - spookDust: {type: Number, 'default': 0}, - shinySeed: {type: Number, 'default': 0}, - seafoam: {type: Number, 'default': 0}, + special: { + snowball: {type: Number, default: 0}, + spookDust: {type: Number, default: 0}, + shinySeed: {type: Number, default: 0}, + seafoam: {type: Number, default: 0}, valentine: Number, valentineReceived: Array, // array of strings, by sender name nye: Number, @@ -241,7 +249,7 @@ var UserSchema = new Schema({ greeting: Number, greetingReceived: Array, thankyou: Number, - thankyouReceived: Array + thankyouReceived: Array, }, // -------------- Animals ------------------- @@ -251,14 +259,13 @@ var UserSchema = new Schema({ // 'PandaCub-Red': 10, // Number represents "Growth Points" // etc... // } - pets: - _.defaults( + pets: _.defaults( // First transform to a 1D eggs/potions mapping - _.transform(shared.content.pets, function(m,v,k){ m[k] = Number; }), + _.transform(shared.content.pets, (m, v, k) => m[k] = Number), // Then add additional pets (quest, backer, contributor, premium) - _.transform(shared.content.questPets, function(m,v,k){ m[k] = Number; }), - _.transform(shared.content.specialPets, function(m,v,k){ m[k] = Number; }), - _.transform(shared.content.premiumPets, function(m,v,k){ m[k] = Number; }) + _.transform(shared.content.questPets, (m, v, k) => m[k] = Number), + _.transform(shared.content.specialPets, (m, v, k) => m[k] = Number), + _.transform(shared.content.premiumPets, (m, v, k) => m[k] = Number) ), currentPet: String, // Cactus-Desert @@ -266,19 +273,19 @@ var UserSchema = new Schema({ // 'PandaCub': 0, // 0 indicates "doesn't own" // 'Wolf': 5 // Number indicates "stacking" // } - eggs: _.transform(shared.content.eggs, function(m,v,k){ m[k] = Number; }), + eggs: _.transform(shared.content.eggs, (m, v, k) => m[k] = Number), // hatchingPotions: { // 'Desert': 0, // 0 indicates "doesn't own" // 'CottonCandyBlue': 5 // Number indicates "stacking" // } - hatchingPotions: _.transform(shared.content.hatchingPotions, function(m,v,k){ m[k] = Number; }), + hatchingPotions: _.transform(shared.content.hatchingPotions, (m, v, k) => m[k] = Number), // Food: { // 'Watermelon': 0, // 0 indicates "doesn't own" // 'RottenMeat': 5 // Number indicates "stacking" // } - food: _.transform(shared.content.food, function(m,v,k){ m[k] = Number; }), + food: _.transform(shared.content.food, (m, v, k) => m[k] = Number), // mounts: { // 'Wolf-Desert': true, @@ -287,12 +294,12 @@ var UserSchema = new Schema({ // } mounts: _.defaults( // First transform to a 1D eggs/potions mapping - _.transform(shared.content.pets, function(m,v,k){ m[k] = Boolean; }), + _.transform(shared.content.pets, (m, v, k) => m[k] = Boolean), // Then add quest and premium pets - _.transform(shared.content.questPets, function(m,v,k){ m[k] = Boolean; }), - _.transform(shared.content.premiumPets, function(m,v,k){ m[k] = Boolean; }), + _.transform(shared.content.questPets, (m, v, k) => m[k] = Boolean), + _.transform(shared.content.premiumPets, (m, v, k) => m[k] = Boolean), // Then add additional mounts (backer, contributor) - _.transform(shared.content.specialMounts, function(m,v,k){ m[k] = Boolean; }) + _.transform(shared.content.specialMounts, (m, v, k) => m[k] = Boolean) ), currentMount: String, @@ -300,164 +307,167 @@ var UserSchema = new Schema({ // 'boss_0': 0, // 0 indicates "doesn't own" // 'collection_honey': 5 // Number indicates "stacking" // } - quests: _.transform(shared.content.quests, function(m,v,k){ m[k] = Number; }), + quests: _.transform(shared.content.quests, (m, v, k) => m[k] = Number), lastDrop: { - date: {type: Date, 'default': Date.now}, - count: {type: Number, 'default': 0} - } + date: {type: Date, default: Date.now}, + count: {type: Number, default: 0}, + }, }, - lastCron: {type: Date, 'default': Date.now}, + lastCron: {type: Date, default: Date.now}, // {GROUP_ID: Boolean}, represents whether they have unseen chat messages - newMessages: {type: Schema.Types.Mixed, 'default': {}}, + newMessages: {type: Schema.Types.Mixed, default: {}}, party: { // id // FIXME can we use a populated doc instead of fetching party separate from user? - order: {type:String, 'default':'level'}, - orderAscending: {type:String, 'default':'ascending'}, + order: {type: String, default: 'level'}, + orderAscending: {type: String, default: 'ascending'}, quest: { key: String, progress: { - up: {type: Number, 'default': 0}, - down: {type: Number, 'default': 0}, - collect: {type: Schema.Types.Mixed, 'default': {}} // {feather:1, ingot:2} + up: {type: Number, default: 0}, + down: {type: Number, default: 0}, + collect: {type: Schema.Types.Mixed, default: {}}, // {feather:1, ingot:2} }, completed: String, // When quest is done, we move it from key => completed, and it's a one-time flag (for modal) that they unset by clicking "ok" in browser - RSVPNeeded: {type: Boolean, 'default': false} // Set to true when invite is pending, set to false when quest invite is accepted or rejected, quest starts, or quest is cancelled - } + RSVPNeeded: {type: Boolean, default: false}, // Set to true when invite is pending, set to false when quest invite is accepted or rejected, quest starts, or quest is cancelled + }, }, preferences: { - dayStart: {type:Number, 'default': 0, min: 0, max: 23}, - size: {type:String, enum: ['broad','slim'], 'default': 'slim'}, + dayStart: {type: Number, default: 0, min: 0, max: 23}, + size: {type: String, enum: ['broad', 'slim'], default: 'slim'}, hair: { - color: {type: String, 'default': 'red'}, - base: {type: Number, 'default': 3}, - bangs: {type: Number, 'default': 1}, - beard: {type: Number, 'default': 0}, - mustache: {type: Number, 'default': 0}, - flower: {type: Number, 'default': 1} + color: {type: String, default: 'red'}, + base: {type: Number, default: 3}, + bangs: {type: Number, default: 1}, + beard: {type: Number, default: 0}, + mustache: {type: Number, default: 0}, + flower: {type: Number, default: 1}, }, - hideHeader: {type:Boolean, 'default':false}, - skin: {type:String, 'default':'915533'}, - shirt: {type: String, 'default': 'blue'}, + hideHeader: {type: Boolean, default: false}, + skin: {type: String, default: '915533'}, + shirt: {type: String, default: 'blue'}, timezoneOffset: Number, - sound: {type:String, 'default':'off', enum: ['off', 'danielTheBard', 'gokulTheme', 'luneFoxTheme', 'wattsTheme']}, + sound: {type: String, default: 'off', enum: ['off', 'danielTheBard', 'gokulTheme', 'luneFoxTheme', 'wattsTheme']}, language: String, automaticAllocation: Boolean, - allocationMode: {type:String, enum: ['flat','classbased','taskbased'], 'default': 'flat'}, - autoEquip: {type: Boolean, 'default': true}, + allocationMode: {type: String, enum: ['flat', 'classbased', 'taskbased'], default: 'flat'}, + autoEquip: {type: Boolean, default: true}, costume: Boolean, - dateFormat: {type: String, enum:['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy/MM/dd'], 'default': 'MM/dd/yyyy'}, - sleep: {type: Boolean, 'default': false}, - stickyHeader: {type: Boolean, 'default': true}, - disableClasses: {type: Boolean, 'default': false}, - newTaskEdit: {type: Boolean, 'default': false}, - dailyDueDefaultView: {type: Boolean, 'default': false}, - tagsCollapsed: {type: Boolean, 'default': false}, - advancedCollapsed: {type: Boolean, 'default': false}, - toolbarCollapsed: {type:Boolean, 'default':false}, + dateFormat: {type: String, enum: ['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy/MM/dd'], default: 'MM/dd/yyyy'}, + sleep: {type: Boolean, default: false}, + stickyHeader: {type: Boolean, default: true}, + disableClasses: {type: Boolean, default: false}, + newTaskEdit: {type: Boolean, default: false}, + dailyDueDefaultView: {type: Boolean, default: false}, + tagsCollapsed: {type: Boolean, default: false}, + advancedCollapsed: {type: Boolean, default: false}, + toolbarCollapsed: {type: Boolean, default: false}, background: String, - displayInviteToPartyWhenPartyIs1: { type:Boolean, 'default':true}, - webhooks: {type: Schema.Types.Mixed, 'default': {}}, - // For this fields make sure to use strict comparison when searching for falsey values (=== false) + displayInviteToPartyWhenPartyIs1: {type: Boolean, default: true}, + webhooks: {type: Schema.Types.Mixed, default: {}}, + // For the following fields make sure to use strict comparison when searching for falsey values (=== false) // As users who didn't login after these were introduced may have them undefined/null emailNotifications: { - unsubscribeFromAll: {type: Boolean, 'default': false}, - newPM: {type: Boolean, 'default': true}, - kickedGroup: {type: Boolean, 'default': true}, - wonChallenge: {type: Boolean, 'default': true}, - giftedGems: {type: Boolean, 'default': true}, - giftedSubscription: {type: Boolean, 'default': true}, - invitedParty: {type: Boolean, 'default': true}, - invitedGuild: {type: Boolean, 'default': true}, - questStarted: {type: Boolean, 'default': true}, - invitedQuest: {type: Boolean, 'default': true}, - //remindersToLogin: {type: Boolean, 'default': true}, - // Those importantAnnouncements are in fact the recapture emails - importantAnnouncements: {type: Boolean, 'default': true}, - weeklyRecaps: {type: Boolean, 'default': true} - } + unsubscribeFromAll: {type: Boolean, default: false}, + newPM: {type: Boolean, default: true}, + kickedGroup: {type: Boolean, default: true}, + wonChallenge: {type: Boolean, default: true}, + giftedGems: {type: Boolean, default: true}, + giftedSubscription: {type: Boolean, default: true}, + invitedParty: {type: Boolean, default: true}, + invitedGuild: {type: Boolean, default: true}, + questStarted: {type: Boolean, default: true}, + invitedQuest: {type: Boolean, default: true}, + // remindersToLogin: {type: Boolean, default: true}, + // importantAnnouncements are in fact the recapture emails + importantAnnouncements: {type: Boolean, default: true}, + weeklyRecaps: {type: Boolean, default: true}, + }, }, profile: { blurb: String, imageUrl: String, - name: String + name: String, }, stats: { - hp: {type: Number, 'default': shared.maxHealth}, - mp: {type: Number, 'default': 10}, - exp: {type: Number, 'default': 0}, - gp: {type: Number, 'default': 0}, - lvl: {type: Number, 'default': 1}, + hp: {type: Number, default: shared.maxHealth}, + mp: {type: Number, default: 10}, + exp: {type: Number, default: 0}, + gp: {type: Number, default: 0}, + lvl: {type: Number, default: 1}, // Class System - 'class': {type: String, enum: ['warrior','rogue','wizard','healer'], 'default': 'warrior'}, - points: {type: Number, 'default': 0}, - str: {type: Number, 'default': 0}, - con: {type: Number, 'default': 0}, - int: {type: Number, 'default': 0}, - per: {type: Number, 'default': 0}, + class: {type: String, enum: ['warrior', 'rogue', 'wizard', 'healer'], default: 'warrior'}, + points: {type: Number, default: 0}, + str: {type: Number, default: 0}, + con: {type: Number, default: 0}, + int: {type: Number, default: 0}, + per: {type: Number, default: 0}, buffs: { - str: {type: Number, 'default': 0}, - int: {type: Number, 'default': 0}, - per: {type: Number, 'default': 0}, - con: {type: Number, 'default': 0}, - stealth: {type: Number, 'default': 0}, - streaks: {type: Boolean, 'default': false}, - snowball: {type: Boolean, 'default': false}, - spookDust: {type: Boolean, 'default': false}, - shinySeed: {type: Boolean, 'default': false}, - seafoam: {type: Boolean, 'default': false} + str: {type: Number, default: 0}, + int: {type: Number, default: 0}, + per: {type: Number, default: 0}, + con: {type: Number, default: 0}, + stealth: {type: Number, default: 0}, + streaks: {type: Boolean, default: false}, + snowball: {type: Boolean, default: false}, + spookDust: {type: Boolean, default: false}, + shinySeed: {type: Boolean, default: false}, + seafoam: {type: Boolean, default: false}, }, training: { - int: {type: Number, 'default': 0}, - per: {type: Number, 'default': 0}, - str: {type: Number, 'default': 0}, - con: {type: Number, 'default': 0} - } + int: {type: Number, default: 0}, + per: {type: Number, default: 0}, + str: {type: Number, default: 0}, + con: {type: Number, default: 0}, + }, }, tags: {type: [{ _id: false, - id: { type: String, 'default': shared.uuid }, + id: {type: String, default: shared.uuid}, name: String, - challenge: String + challenge: String, }]}, - challenges: [{type: 'String', ref:'Challenge'}], + challenges: [{type: String, ref: 'Challenge'}], inbox: { - newMessages: {type:Number, 'default':0}, - blocks: {type:Array, 'default':[]}, - messages: {type:Schema.Types.Mixed, 'default':{}}, //reflist - optOut: {type:Boolean, 'default':false} + newMessages: {type: Number, default: 0}, + blocks: {type: Array, default: []}, + messages: {type: Schema.Types.Mixed, default: {}}, + optOut: {type: Boolean, default: false}, }, - habits: {type:[TaskSchemas.HabitSchema]}, - dailys: {type:[TaskSchemas.DailySchema]}, - todos: {type:[TaskSchemas.TodoSchema]}, - rewards: {type:[TaskSchemas.RewardSchema]}, + habits: {type: [TaskSchemas.HabitSchema]}, + dailys: {type: [TaskSchemas.DailySchema]}, + todos: {type: [TaskSchemas.TodoSchema]}, + rewards: {type: [TaskSchemas.RewardSchema]}, extra: Schema.Types.Mixed, - pushDevices: {type: [{ - regId: {type: String}, - type: {type: String} - }],'default': []} - + pushDevices: { + type: [{ + regId: {type: String}, + type: {type: String}, + }], + default: [], + }, }, { strict: true, - minimize: false // So empty objects are returned + minimize: false, // So empty objects are returned }); -UserSchema.methods.deleteTask = function(tid) { - this.ops.deleteTask({params:{id:tid}},function(){}); // TODO remove this whole method, since it just proxies, and change all references to this method -} +schema.methods.deleteTask = function (tid) { + this.ops.deleteTask({params: {id: tid}}, () => {}); // TODO remove this whole method, since it just proxies, and change all references to this method +}; + +schema.methods.toJSON = function () { + let doc = this.toObject(); -UserSchema.methods.toJSON = function() { - var doc = this.toObject(); doc.id = doc._id; // FIXME? Is this a reference to `doc.filters` or just disabled code? Remove? @@ -467,120 +477,52 @@ UserSchema.methods.toJSON = function() { return doc; }; -//UserSchema.virtual('tasks').get(function () { -// var tasks = this.habits.concat(this.dailys).concat(this.todos).concat(this.rewards); -// var tasks = _.object(_.pluck(tasks,'id'), tasks); -// return tasks; -//}); +// schema.virtual('tasks').get(function () { +// var tasks = this.habits.concat(this.dailys).concat(this.todos).concat(this.rewards); +// var tasks = _.object(_.pluck(tasks,'id'), tasks); +// return tasks; +// }); -UserSchema.post('init', function(doc){ +schema.post('init', function postInitUser (doc) { shared.wrap(doc); -}) - -UserSchema.pre('save', function(next) { - - // Populate new users with default content - if (this.isNew){ - _populateDefaultsForNewUser(this); - } - - //this.markModified('tasks'); - if (_.isNaN(this.preferences.dayStart) || this.preferences.dayStart < 0 || this.preferences.dayStart > 23) { - this.preferences.dayStart = 0; - } - - if (!this.profile.name) { - var fb = this.auth.facebook; - this.profile.name = - (this.auth.local && this.auth.local.username) || - (fb && (fb.displayName || fb.name || fb.username || (fb.first_name && fb.first_name + ' ' + fb.last_name))) || - 'Anonymous'; - } - - // Determines if Beast Master should be awarded - var beastMasterProgress = shared.count.beastMasterProgress(this.items.pets); - if (beastMasterProgress >= 90 || this.achievements.beastMasterCount > 0) { - this.achievements.beastMaster = true; - } - - // Determines if Mount Master should be awarded - var mountMasterProgress = shared.count.mountMasterProgress(this.items.mounts); - - if (mountMasterProgress >= 90 || this.achievements.mountMasterCount > 0) { - this.achievements.mountMaster = true - } - - // Determines if Triad Bingo should be awarded - - var dropPetCount = shared.count.dropPetsCurrentlyOwned(this.items.pets); - var qualifiesForTriad = dropPetCount >= 90 && mountMasterProgress >= 90; - - if (qualifiesForTriad || this.achievements.triadBingoCount > 0) { - this.achievements.triadBingo = true; - } - - // Enable weekly recap emails for old users who sign in - if(this.flags.lastWeeklyRecapDiscriminator){ - // Enable weekly recap emails in 24 hours - this.flags.lastWeeklyRecap = moment().subtract(6, 'days').toDate(); - // Unset the field so this is run only once - this.flags.lastWeeklyRecapDiscriminator = undefined; - } - - // EXAMPLE CODE for allowing all existing and new players to be - // automatically granted an item during a certain time period: - // if (!this.items.pets['JackOLantern-Base'] && moment().isBefore('2014-11-01')) - // this.items.pets['JackOLantern-Base'] = 5; - - //our own version incrementer - if (_.isNaN(this._v) || !_.isNumber(this._v)) this._v = 0; - this._v++; - - next(); }); -UserSchema.methods.unlink = function(options, cb) { - var cid = options.cid, keep = options.keep, tid = options.tid; - if (!cid) { - return cb("Could not remove challenge tasks. Please delete them manually."); - } - var self = this; - switch (keep) { - case 'keep': - self.tasks[tid].challenge = {}; - break; - case 'remove': - self.deleteTask(tid); - break; - case 'keep-all': - _.each(self.tasks, function(t){ - if (t.challenge && t.challenge.id == cid) { - t.challenge = {}; +function _populateDefaultTasks (user, taskTypes) { + _.each(taskTypes, (taskType) => { + user[taskType] = _.map(shared.content.userDefaults[taskType], (task) => { + let newTask = _.cloneDeep(task); + + // Render task's text and notes in user's language + if (taskType === 'tags') { + // tasks automatically get id=helpers.uuid() from TaskSchema id.default, but tags are Schema.Types.Mixed - so we need to manually invoke here + newTask.id = shared.uuid(); + newTask.name = newTask.name(user.preferences.language); + } else { + newTask.text = newTask.text(user.preferences.language); + if (newTask.notes) { + newTask.notes = newTask.notes(user.preferences.language); } - }); - break; - case 'remove-all': - _.each(self.tasks, function(t){ - if (t.challenge && t.challenge.id == cid) { - self.deleteTask(t.id); + + if (newTask.checklist) { + newTask.checklist = _.map(newTask.checklist, (checklistItem) => { + checklistItem.text = checklistItem.text(user.preferences.language); + return checklistItem; + }); } - }) - break; - } - self.markModified('habits'); - self.markModified('dailys'); - self.markModified('todos'); - self.markModified('rewards'); - self.save(cb); + } + + return newTask; + }); + }); } -function _populateDefaultsForNewUser(user) { - var taskTypes; +function _populateDefaultsForNewUser (user) { + let taskTypes; - if (user.registeredThrough === "habitica-web") { + if (user.registeredThrough === 'habitica-web') { taskTypes = ['habits', 'dailys', 'todos', 'rewards', 'tags']; - var tutorialCommonSections = [ + let tutorialCommonSections = [ 'habits', 'dailies', 'todos', @@ -595,15 +537,15 @@ function _populateDefaultsForNewUser(user) { 'items', ]; - _.each(tutorialCommonSections, function(section) { + _.each(tutorialCommonSections, (section) => { user.flags.tutorial.common[section] = true; }); } else { - taskTypes = ['todos', 'tags'] + taskTypes = ['todos', 'tags']; user.flags.showTour = false; - var tourSections = [ + let tourSections = [ 'showTour', 'intro', 'classes', @@ -619,7 +561,7 @@ function _populateDefaultsForNewUser(user) { 'equipment', ]; - _.each(tourSections, function(section) { + _.each(tourSections, (section) => { user.flags.tour[section] = -2; }); } @@ -627,47 +569,126 @@ function _populateDefaultsForNewUser(user) { _populateDefaultTasks(user, taskTypes); } -function _populateDefaultTasks (user, taskTypes) { - _.each(taskTypes, function(taskType){ - user[taskType] = _.map(shared.content.userDefaults[taskType], function(task){ - var newTask = _.cloneDeep(task); +schema.pre('save', function postSaveUser (next) { + // Populate new users with default content + if (this.isNew) { + _populateDefaultsForNewUser(this); + } - // Render task's text and notes in user's language - if(taskType === 'tags'){ - // tasks automatically get id=helpers.uuid() from TaskSchema id.default, but tags are Schema.Types.Mixed - so we need to manually invoke here - newTask.id = shared.uuid(); - newTask.name = newTask.name(user.preferences.language); - }else{ - newTask.text = newTask.text(user.preferences.language); - if(newTask.notes) { - newTask.notes = newTask.notes(user.preferences.language); + // this.markModified('tasks'); + if (_.isNaN(this.preferences.dayStart) || this.preferences.dayStart < 0 || this.preferences.dayStart > 23) { + this.preferences.dayStart = 0; + } + + if (!this.profile.name) { + let fb = this.auth.facebook; + + this.profile.name = + (this.auth.local && this.auth.local.username) || + (fb && (fb.displayName || fb.name || fb.username || `${fb.first_name && fb.first_name} ${fb.last_name}`)) || + 'Anonymous'; + } + + // Determines if Beast Master should be awarded + let beastMasterProgress = shared.count.beastMasterProgress(this.items.pets); + + if (beastMasterProgress >= 90 || this.achievements.beastMasterCount > 0) { + this.achievements.beastMaster = true; + } + + // Determines if Mount Master should be awarded + let mountMasterProgress = shared.count.mountMasterProgress(this.items.mounts); + + if (mountMasterProgress >= 90 || this.achievements.mountMasterCount > 0) { + this.achievements.mountMaster = true; + } + + // Determines if Triad Bingo should be awarded + + let dropPetCount = shared.count.dropPetsCurrentlyOwned(this.items.pets); + let qualifiesForTriad = dropPetCount >= 90 && mountMasterProgress >= 90; + + if (qualifiesForTriad || this.achievements.triadBingoCount > 0) { + this.achievements.triadBingo = true; + } + + // Enable weekly recap emails for old users who sign in + if (this.flags.lastWeeklyRecapDiscriminator) { + // Enable weekly recap emails in 24 hours + this.flags.lastWeeklyRecap = moment().subtract(6, 'days').toDate(); + // Unset the field so this is run only once + this.flags.lastWeeklyRecapDiscriminator = undefined; + } + + // EXAMPLE CODE for allowing all existing and new players to be + // automatically granted an item during a certain time period: + // if (!this.items.pets['JackOLantern-Base'] && moment().isBefore('2014-11-01')) + // this.items.pets['JackOLantern-Base'] = 5; + + // our own version incrementer + if (_.isNaN(this._v) || !_.isNumber(this._v)) this._v = 0; + this._v++; + + next(); +}); + +schema.methods.unlink = function (options, cb) { + let cid = options.cid; + let keep = options.keep; + let tid = options.tid; + + if (!cid) { + return cb('Could not remove challenge tasks. Please delete them manually.'); + } + + let self = this; + + switch (keep) { + case 'keep': + self.tasks[tid].challenge = {}; + break; + case 'remove': + self.deleteTask(tid); + break; + case 'keep-all': + _.each(self.tasks, (t) => { + if (t.challenge && t.challenge.id === cid) { + t.challenge = {}; } - - if(newTask.checklist){ - newTask.checklist = _.map(newTask.checklist, function(checklistItem){ - checklistItem.text = checklistItem.text(user.preferences.language); - return checklistItem; - }); + }); + break; + case 'remove-all': + _.each(self.tasks, (t) => { + if (t.challenge && t.challenge.id === cid) { + self.deleteTask(t.id); } - } + }); + break; + } - return newTask; - }); - }); -} + self.markModified('habits'); + self.markModified('dailys'); + self.markModified('todos'); + self.markModified('rewards'); + self.save(cb); +}; + +export let model = mongoose.model('User', schema); -module.exports.schema = UserSchema; -module.exports.model = mongoose.model("User", UserSchema); // Initially export an empty object so external requires will get // the right object by reference when it's defined later // Otherwise it would remain undefined if requested before the query executes -module.exports.mods = []; +export let mods = []; -mongoose.model("User") - .find({'contributor.admin':true}) +mongoose.model('User') + .find({'contributor.admin': true}) .sort('-contributor.level -backer.npc profile.name') .select('profile contributor backer') - .exec(function(err,mods){ + .exec() + .then((foundMods) => { // Using push to maintain the reference to mods - module.exports.mods.push.apply(module.exports.mods, mods); -}); + mods.push(...foundMods); + }) + .catch((err) => { + throw err; // TODO ? + });