Files
habitica/website/server/models/user/schema.js
Phillip Thelen d9e774dd77 Implement Bailey CMS API (#10739)
* Begin refactoring news API to return individual markdown posts

* Implement simple bailey CMS

* Prevented users with lvl less than 10 from seeing mana

* Added in class checks and notification tests

* Added getter use

* Fixed class check

* chore(i18n): update locales

* 4.60.2

* remove tests that are no longer needed because we won't be purging private messages (#10670)

Ref: this comment from paglias: https://github.com/HabitRPG/habitica/issues/7940#issuecomment-406489506

* remove .only

* allow challenge leader/owner to view/join/modify challenge in private group they've left - fixes #9753 (#10606)

* rename hasAccess to canJoin for challenges

This is so the function won't be used accidentally for other
purposes, since hasAccess could be misinterpretted.

* add isLeader function for challenges

* allow challenge leader to join/modify/end challenge when they're not in the private group it's in

* delete duplicate test

* clarify title of existing tests

* add tests and adjust existing tests to reduce privileges of test users

* fix lint errors

* remove pointless isLeader check (it's checked in canJoin)

* Correct Challenges tooltip in Guild view (#10667)

* Fix new party member cannot join pending quest (#10648)

* Saved sort selection into local storage for later use - fixes #10432 (#10655)

* Saved sort selection into local storage for later use

* Updated code to use userLocalManager module

* Fix initial position item info when selecting one item after another (fixes #10077) (#10661)

* Update lastMouseMoveEvent even when dragging an egg or potion.

* Update lastMouseMoveEvent even when dragging a food item.

* Refactor/market vue (#10601)

* extract inventoryDrawer from market

* show scrollbar only if needed

* extract featuredItemsHeader / pinUtils

* extract pageLayout

* extract layoutSection / filterDropdown - fix sortByNumber

* rollback sortByNumber order-fix

* move equipment lists out of the layout-section (for now)

* refactor sellModal

* extract checkbox

* extract equipment section

* extract category row

* revert scroll - remove sellModal item template

* fix(lint): commas and semis

* Created category item component (#10613)

* extract filter sidebar

* fix gemCount - fix raising the item count if the item wasn't previously owned

* fixes #10659

* remove unneeded method

* fix typo when importing component

* feat(content): Forest Friends Quest Bundle

* chore(sprites): compile

* chore(i18n): update locales

* 4.60.3

* fix(bcrypt): install fork compatible with Node 8

* chore(i18n): update locales

* 4.60.4

* add swear words - TRIGGER / CONTENT WARNING: assault, slurs, swearwords, etc

* add pinUtils-mixin   - fixes #10682 (#10683)

* chore(news): Bailey

* chore(i18n): update locales

* 4.60.5

* Improve rendering banner about sleeping in the inn

See #10695

* Display settings in one column

* Small Updates (#10701)

* small updates

* fix client unit test

* fix uuid validation

* Revert "Small Updates (#10701)" (#10702)

This reverts commit dd7fa73961.

* feat(event): Fall Festival 2018

* chore(sprites): compile

* chore(i18n): update locales

* 4.61.0

* Move inbox to its own model (#10428)

* shared model for chat and inbox

* disable inbox schema

* inbox: use separate model

* remove old code that used group.chat

* add back chat field (not used) and remove old tests

* remove inbox exclusions when loading user

* add GET /api/v3/inbox/messages

* add comment

* implement DELETE /inbox/messages/:messageid in v4

* implement GET /inbox/messages in v4 and update tests

* implement DELETE /api/v4/inbox/clear

* fix url

* fix doc

* update /export/inbox.html

* update other data exports

* add back messages in user schema

* add user.toJSONWithInbox

* add compativility until migration is done

* more compatibility

* fix tojson called twice

* add compatibility methods

* fix common tests

* fix v4 integration tests

* v3 get user -> with inbox

* start to fix tests

* fix v3 integration tests

* wip

* wip, client use new route

* update tests for members/send-private-message

* tests for get user in v4

* add tests for DELETE /inbox/messages/:messageId

* add tests for DELETE /inbox/clear in v4

* update docs

* fix tests

* initial migration

* fix migration

* fix migration

* migration fixes

* migrate api.enterCouponCode

* migrate api.castSpell

* migrate reset, reroll, rebirth

* add routes to v4 version

* fix tests

* fixes

* api.updateUser

* remove .only

* get user -> userLib

* refactor inbox.vue to work with new data model

* fix return message when messaging yourself

* wip fix bug with new conversation

* wip

* fix remaining ui issues

* move api.registerLocal, fixes

* keep only v3 version of GET /inbox/messages

* Fix API early Stat Point allocation (#10680)

* Refactor hasClass check to common so it can be used in shared & server-side code

* Check that user has selected class before allocating stat points

* chore(event): end Ember Hatching Potions

* chore(analytics): reenable navigation tracking

* update bcrypt

* Point achievement modal links to main site (#10709)

* Animal ears after death (#10691)

* Animal Ears purchasable with Gold if lost in Death

* remove ears from pinned items when set is bought

* standardise css and error handling for gems and coins

* revert accidental new line

* fix client tests

* Reduce margin-bottom of checklist-item from 10px to -3px. (#10684)

* chore(i18n): update locales

* 4.61.1

* Position inn banner when window is resized

* feat(content): Subscriber Items and Magic Potions

* chore(sprites): compile

* chore(i18n): update locales

* 4.62.0

* Update inn banner handling

* Fix banner offset on initial load

* Fix minor issues.

* Issue: 10660 - Fixed. Changed default to Please Enter A Value (#10718)

* Issue: 10660 - Fixed. Changed default to Please Enter A Value

* Issue: 10660 - Fixed/revision 2 Changed default to Enter A Value

* chore(news): Bailey announcements

* chore(i18n): update locales

* 4.62.1

* adjust wiki link for usernameInfo string

https://github.com/HabitRPG/habitica-private/issues/7#issuecomment-425405425

* raise coverage for tasks api calls (#10029)

* - updates a group task - approval is required
- updates a group task with checklist

* add expect to test the new checklist length

* - moves tasks to a specified position out of length

* remove unused line

* website getter tasks tests

* re-add sanitizeUserChallengeTask

* change config.json.example variable to be a string not a boolean

* fix tests - pick the text / up/down props too

* fix test - remove changes on text/up/down - revert sanitize condition - revert sanitization props

* chore(i18n): update locales

* 4.62.2

* chore(news): Bailey

* chore(i18n): update locales

* 4.62.3

* inbox: fix avatar display and order

* Username announcement (#10729)

* Change update username API call

The call no longer requires a password and also validates the username.

* Implement API call to verify username without setting it

* Improve coding style

* Apply username verification to registration

* Update error messages

* Validate display names.

* Fix API early Stat Point allocation (#10680)

* Refactor hasClass check to common so it can be used in shared & server-side code

* Check that user has selected class before allocating stat points

* chore(event): end Ember Hatching Potions

* chore(analytics): reenable navigation tracking

* update bcrypt

* Point achievement modal links to main site (#10709)

* Animal ears after death (#10691)

* Animal Ears purchasable with Gold if lost in Death

* remove ears from pinned items when set is bought

* standardise css and error handling for gems and coins

* revert accidental new line

* fix client tests

* Reduce margin-bottom of checklist-item from 10px to -3px. (#10684)

* chore(i18n): update locales

* 4.61.1

* feat(content): Subscriber Items and Magic Potions

* chore(sprites): compile

* chore(i18n): update locales

* 4.62.0

* Display notification for users to confirm their username

* fix typo

* WIP(usernames): Changes to address #10694

* WIP(usernames): Further changes for #10694

* fix(usernames): don't show spurious headings

* Change verify username notification to new version

* Improve feedback for invalid usernames

* Allow user to set their username again to confirm it

* Improve validation display for usernames

* Temporarily move display name validation outside of schema

* Improve rendering banner about sleeping in the inn

See #10695

* Display settings in one column

* Position inn banner when window is resized

* Update inn banner handling

* Fix banner offset on initial load

* Fix minor issues.

* Issue: 10660 - Fixed. Changed default to Please Enter A Value (#10718)

* Issue: 10660 - Fixed. Changed default to Please Enter A Value

* Issue: 10660 - Fixed/revision 2 Changed default to Enter A Value

* chore(news): Bailey announcements

* chore(i18n): update locales

* 4.62.1

* adjust wiki link for usernameInfo string

https://github.com/HabitRPG/habitica-private/issues/7#issuecomment-425405425

* raise coverage for tasks api calls (#10029)

* - updates a group task - approval is required
- updates a group task with checklist

* add expect to test the new checklist length

* - moves tasks to a specified position out of length

* remove unused line

* website getter tasks tests

* re-add sanitizeUserChallengeTask

* change config.json.example variable to be a string not a boolean

* fix tests - pick the text / up/down props too

* fix test - remove changes on text/up/down - revert sanitize condition - revert sanitization props

* Change update username API call

The call no longer requires a password and also validates the username.

* feat(content): Subscriber Items and Magic Potions

* Re-add register call

* Fix merge issue

* Fix issue with setting username

* Implement new alert style

* Display username confirmation status in settings

* Add disclaimer to change username field

* validate username in settings

* Allow specific fields to be focused when opening site settings

* Implement requested changes.

* Fix merge issue

* Fix failing tests

* verify username when users register with username and password

* Set ID for change username notification

* Disable submit button if username is invalid

* Improve username confirmation handling

* refactor(settings): address remaining code comments on auth form

* Revert "refactor(settings): address remaining code comments on auth form"

This reverts commit 9b6609ad64.

* Social user username (#10620)

* Refactored private functions to library

* Refactored social login code

* Added username to social registration

* Changed id library

* Added new local auth check

* Fixed export error. Fixed password check error

* fix(settings): password not available on client

* refactor(settings): more sensible placement of methods

* chore(migration): script to hand out procgen usernames

* fix(migration): don't give EVERYONE new names you doofus

* fix(migration): limit data retrieved, be extra careful about updates

* fix(migration): use missing field, not migration tag, for query

* fix(migration): unused var

* fix(usernames): only generate 20 characters

* fix(migration): set lowerCaseUsername

* fix(lint): comma

* fix(lint): comma spacing

* chore(i18n): update locales

* 4.63.0

* chore(news): Bailey

* chore(i18n): update locales

* 4.63.1

* fix(usernames): various
Reword invalid characters error
Correct typo in slur error
Remove extraneous Confirm button
Reset username field if empty on blur
Restore ability to add local auth to social login

* fix(auth): account for new username paradigm in add-local flow

* fix(auth): alert on successful addLocal

* chore(i18n): update locales

* 4.63.2

* fix(auth): Don't try to check existing username on new reg

* 4.63.3

* feat(content): Armoire and BGs 2018/10

* chore(sprites): compile

* fix(passport): use graph API v2.8

* chore(i18n): update locales

* 4.64.0

* Begin refactoring news API to return individual markdown posts

* Implement simple bailey CMS

* remove old news markdown

* Correctly display images in bailey modal

* Remove need for newStuff migration

* Add basic tests

* Fix authentication issue

* Fix tests

* Update news model

* add API route to get single post

* remove news admin frontend code

* fix lint error

* Fix merge mixups

* Fix lint errors

* fix api call

* fix lint error

* Fix issues caused by merging

* remove console log

* Improve news display

* Correctly update users notifications

* Fix date display for news posts

* Fix tests

* remove old cache file

* correctly create date

* correctly create promise

* Better check for existance.

* Improve docs

* Fix minor issues

* Add method to get latest post

* fix lint errors

* use correct call for 404

* add comment about old newStuff field

* paginate news

* Fix lint errors

* Remove unnecessary await

* Fix broken tests

* ...

* correct existence check

* fix database queries

* change approach to cached news posts

* fix tests

* Change how news posts are cached

* Fetch last news post at an interval

* Fix typos and other small things

* add new permission for modifying bailey posts

* add test for ensureNewsPoster

* return last news post with legacy api

* Fix test

* Hopefully fix test

* change fields to _id

* Fixes

* Fixes

* fix test

* Fixes

* make all tests pass

* fix lint

* id -> _id

* _id -> id

* remove identical tell me later route from api v4

* fix lint

* user model: fix issues with newStuff

* improve user#toJSONTransform

* fix typo

* improve newsPost.js

* fix(integration tests): do not return flags.newStuff if it was not selected

* fix news controller

* server side fixes, start refactoring client

* more client fixes

* automatically set author

* new stuff: show one post per user + drafts

* change default border radius for modals to 8px

* required fields and defaults

* slit news into its own component and fix static page

* noNewsPoster: move from i18n to apiError

* remove unused strings

* fix unit tests

* update apidocs

* add backward comparibility for flags.newStuff in api v3

* fix integration tests

* POST news: make integration test independent of number of posts

* api v3 news: render markdown

* static new-stuff: add padding and fix when user not logged in

* test flags.newStuff

* api v3: test setting flags.newStuff on PUT /user

* refactor news post cache and add tests

* remove new locales file

* more resilient tests

* more resilient tests

* refactor tests for NewsPost.updateLastNewsPost

* api v4: fix tests

* api v3: fix tests

* can set flags.newStuff in api v4

Co-authored-by: Keith Holliday <keithrholliday@gmail.com>
Co-authored-by: Sabe Jones <sabrecat@gmail.com>
Co-authored-by: Alys <Alys@users.noreply.github.com>
Co-authored-by: Matteo Pagliazzi <matteopagliazzi@gmail.com>
Co-authored-by: Carl Vuorinen <carl.vuorinen@gmail.com>
Co-authored-by: Rene Cordier <rene.cordier@gmail.com>
Co-authored-by: Forrest Hatfield <github@forresthatfield.com>
Co-authored-by: lucubro <88whacko@gmail.com>
Co-authored-by: negue <negue@users.noreply.github.com>
Co-authored-by: Alys <alice.harris@oldgods.net>
Co-authored-by: J.D. Sandifer <sandifer.jd@gmail.com>
Co-authored-by: Kirsty <kirsty-tortoise@users.noreply.github.com>
Co-authored-by: beatscribe <rattjp@gmail.com>
Co-authored-by: Phillip Thelen <phillip@habitica.com>
2020-10-13 10:15:52 -05:00

675 lines
23 KiB
JavaScript

import { Schema } from 'mongoose';
import validator from 'validator';
import shared from '../../../common';
import { // eslint-disable-line import/no-cycle
getDefaultOwnedGear,
} from '../../libs/items/utils';
import { schema as PushDeviceSchema } from '../pushDevice';
import { schema as SubscriptionPlanSchema } from '../subscriptionPlan';
import { schema as TagSchema } from '../tag';
import { schema as UserNotificationSchema } from '../userNotification';
import { schema as WebhookSchema } from '../webhook';
const RESTRICTED_EMAIL_DOMAINS = Object.freeze(['habitica.com', 'habitrpg.com']);
// User schema definition
export default new Schema({
apiToken: {
$type: String,
default: shared.uuid,
},
auth: {
blocked: Boolean,
facebook: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
google: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
apple: { $type: Schema.Types.Mixed, default: () => ({}) },
local: {
email: {
$type: String,
validate: [{
validator: v => validator.isEmail(v),
message: shared.i18n.t('invalidEmail'),
}, {
validator (email) {
const lowercaseEmail = email.toLowerCase();
return RESTRICTED_EMAIL_DOMAINS.every(domain => !lowercaseEmail.endsWith(`@${domain}`));
},
message: shared.i18n.t('invalidEmailDomain', { domains: RESTRICTED_EMAIL_DOMAINS.join(', ') }),
}],
},
username: {
$type: String,
},
// Store a lowercase version of username to check for duplicates
lowerCaseUsername: String,
hashed_password: String, // eslint-disable-line camelcase
// Legacy password are hashed with SHA1, new ones with bcrypt
passwordHashMethod: {
$type: String,
enum: ['bcrypt', 'sha1'],
},
salt: String, // Salt for SHA1 encrypted passwords, not stored for bcrypt,
// Used to validate password reset codes and make sure only the most recent one can be used
passwordResetCode: String,
},
timestamps: {
created: { $type: Date, default: Date.now },
loggedin: { $type: Date, default: Date.now },
updated: { $type: Date, default: Date.now },
},
},
// 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 },
migration: String,
achievements: {
originalUser: Boolean,
habitSurveys: Number,
ultimateGearSets: {
healer: { $type: Boolean, default: false },
wizard: { $type: Boolean, default: false },
rogue: { $type: Boolean, default: false },
warrior: { $type: Boolean, default: false },
},
beastMaster: Boolean,
beastMasterCount: Number,
mountMaster: Boolean,
mountMasterCount: Number,
triadBingo: Boolean,
triadBingoCount: Number,
veteran: Boolean,
snowball: Number,
spookySparkles: Number,
shinySeed: Number,
seafoam: Number,
streak: { $type: Number, default: 0 },
challenges: Array,
quests: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
rebirths: Number,
rebirthLevel: Number,
perfect: { $type: Number, default: 0 },
habitBirthdays: Number,
valentine: Number,
nye: Number,
habiticaDays: Number,
greeting: Number,
thankyou: Number,
costumeContests: Number,
birthday: Number,
partyUp: Boolean,
partyOn: Boolean,
congrats: Number,
getwell: Number,
goodluck: Number,
royallyLoyal: Boolean,
joinedGuild: Boolean,
joinedChallenge: Boolean,
invitedFriend: Boolean,
lostMasterclasser: Boolean,
mindOverMatter: Boolean,
justAddWater: Boolean,
backToBasics: Boolean,
allYourBase: Boolean,
dustDevil: Boolean,
aridAuthority: Boolean,
kickstarter2019: Boolean,
monsterMagus: Boolean,
undeadUndertaker: Boolean,
primedForPainting: Boolean,
pearlyPro: Boolean,
tickledPink: Boolean,
rosyOutlook: Boolean,
bugBonanza: Boolean,
bareNecessities: Boolean,
freshwaterFriends: Boolean,
goodAsGold: Boolean,
allThatGlitters: Boolean,
// Onboarding Guide
createdTask: Boolean,
completedTask: Boolean,
hatchedPet: Boolean,
fedPet: Boolean,
purchasedEquipment: Boolean,
},
backer: {
tier: Number,
npc: String,
tokensApplied: Boolean,
},
contributor: {
// 1-9, see https://habitica.fandom.com/wiki/Contributor_Rewards
level: {
$type: Number,
min: 0,
max: 9,
},
admin: Boolean,
newsPoster: Boolean,
sudo: Boolean,
// Artisan, Friend, Blacksmith, etc
text: String,
// a markdown textarea to list their contributions + links
contributions: String,
// user can own Critical Hammer of Bug-Crushing if this has a truthy value
critical: String,
},
balance: { $type: Number, default: 0 },
purchased: {
ads: { $type: Boolean, default: false },
// eg, {skeleton: true, pumpkin: true, eb052b: true}
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: {
$type: SubscriptionPlanSchema,
default: () => ({}),
},
},
flags: {
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 },
},
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 },
mounts: { $type: Boolean, default: false },
inbox: { $type: Boolean, default: false },
stats: { $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 },
inviteParty: { $type: Boolean, default: false },
reorderTask: { $type: Boolean, default: false },
},
},
dropsEnabled: { $type: Boolean, default: false }, // unused
itemsEnabled: { $type: Boolean, default: false },
lastNewStuffRead: { $type: String, default: '' },
// The newStuff field was changed to be a computed property when returning the user in json,
// so that it doesn't have to be updated for each bailey post.
// See models/user/hooks#toJSONTransform
// newStuff: { $type: Boolean, default: false },
rewrite: { $type: Boolean, default: true },
classSelected: { $type: Boolean, default: false },
mathUpdates: Boolean,
rebirthEnabled: { $type: Boolean, default: false },
lastFreeRebirth: Date,
levelDrops: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
chatRevoked: Boolean,
chatShadowMuted: 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 },
// Needed to track the tip to send inside the email
weeklyRecapEmailsPhase: { $type: Number, default: 0 },
// Used to track when the next weekly recap should be sent
lastWeeklyRecap: { $type: Date, default: Date.now },
// Used to enable weekly recap emails as users login
lastWeeklyRecapDiscriminator: Boolean,
onboardingEmailsPhase: String, // Keep track of the latest onboarding email sent
communityGuidelinesAccepted: { $type: Boolean, default: false },
cronCount: { $type: Number, default: 0 },
welcomed: { $type: Boolean, default: false },
armoireEnabled: { $type: Boolean, default: true },
armoireOpened: { $type: Boolean, default: false },
armoireEmpty: { $type: Boolean, default: false },
cardReceived: { $type: Boolean, default: false },
warnedLowHealth: { $type: Boolean, default: false },
verifiedUsername: { $type: Boolean, default: false },
},
history: {
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
},
items: {
gear: {
owned: {
$type: Schema.Types.Mixed,
default: () => getDefaultOwnedGear(),
},
equipped: {
weapon: String,
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,
},
costume: {
weapon: String,
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,
},
},
special: {
snowball: { $type: Number, default: 0 },
spookySparkles: { $type: Number, default: 0 },
shinySeed: { $type: Number, default: 0 },
seafoam: { $type: Number, default: 0 },
valentine: { $type: Number, default: 0 },
valentineReceived: Array, // array of strings, by sender name
nye: { $type: Number, default: 0 },
nyeReceived: Array,
greeting: { $type: Number, default: 0 },
greetingReceived: Array,
thankyou: { $type: Number, default: 0 },
thankyouReceived: Array,
birthday: { $type: Number, default: 0 },
birthdayReceived: Array,
congrats: { $type: Number, default: 0 },
congratsReceived: Array,
getwell: { $type: Number, default: 0 },
getwellReceived: Array,
goodluck: { $type: Number, default: 0 },
goodluckReceived: Array,
},
// -------------- Animals -------------------
// Complex bit here. The result looks like:
// pets: {
// 'Wolf-Desert': 0, // 0 means does not own
// 'PandaCub-Red': 10, // Number represents "Growth Points"
// etc...
// }
pets: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
currentPet: String, // Cactus-Desert
// eggs: {
// 'PandaCub': 0, // 0 indicates "doesn't own"
// 'Wolf': 5 // Number indicates "stacking"
// }
eggs: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
// hatchingPotions: {
// 'Desert': 0, // 0 indicates "doesn't own"
// 'CottonCandyBlue': 5 // Number indicates "stacking"
// }
hatchingPotions: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
// Food: {
// 'Watermelon': 0, // 0 indicates "doesn't own"
// 'RottenMeat': 5 // Number indicates "stacking"
// }
food: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
// mounts: {
// 'Wolf-Desert': true,
// 'PandaCub-Red': false,
// etc...
// }
mounts: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
currentMount: String,
// Quests: {
// 'boss_0': 0, // 0 indicates "doesn't own"
// 'collection_honey': 5 // Number indicates "stacking"
// }
quests: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
lastDrop: {
date: { $type: Date, default: Date.now },
count: { $type: Number, default: 0 },
},
},
lastCron: { $type: Date, default: Date.now },
_cronSignature: { $type: String, default: 'NOT_RUNNING' }, // Private property used to avoid double cron
// {GROUP_ID: Boolean}, represents whether they have unseen chat messages
newMessages: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
challenges: [{ $type: String, ref: 'Challenge', validate: [v => validator.isUUID(v), 'Invalid uuid for user challenges.'] }],
invitations: {
// Using an array without validation because otherwise mongoose
// treat this as a subdocument and applies _id by default
// Schema is (id, name, inviter, publicGuild)
// TODO one way to fix is http://mongoosejs.com/docs/guide.html#_id
guilds: { $type: Array, default: () => [] },
// Using a Mixed type because otherwise user.invitations.party = {}
// to reset invitation, causes validation to fail TODO
// schema is the same as for guild invitations (id, name, inviter)
party: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
parties: [{
id: {
$type: String,
ref: 'Group',
required: true,
validate: [v => validator.isUUID(v), 'Invalid uuid for user invitation party id.'],
},
name: {
$type: String,
required: true,
},
inviter: {
$type: String,
ref: 'User',
required: true,
validate: [v => validator.isUUID(v), 'Invalid uuid for user invitation inviter id.'],
},
}],
},
guilds: [{ $type: String, ref: 'Group', validate: [v => validator.isUUID(v), 'Invalid uuid for user guild.'] }],
party: {
_id: { $type: String, validate: [v => validator.isUUID(v), 'Invalid uuid for user party.'], ref: 'Group' },
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: () => ({}),
},
collectedItems: { $type: Number, default: 0 },
},
// 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
completed: String,
// 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 },
},
},
preferences: {
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 },
},
hideHeader: { $type: Boolean, default: false },
skin: { $type: String, default: '915533' },
shirt: { $type: String, default: 'blue' },
timezoneOffset: { $type: Number, default: 0 },
sound: { $type: String, default: 'rosstavoTheme', enum: ['off', ...shared.content.audioThemes] },
chair: { $type: String, default: 'none' },
timezoneOffsetAtLastCron: Number,
language: String,
automaticAllocation: Boolean,
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 },
advancedCollapsed: { $type: Boolean, default: false },
toolbarCollapsed: { $type: Boolean, default: false },
reverseChatOrder: { $type: Boolean, default: false },
background: String,
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},
// importantAnnouncements are in fact the recapture emails
importantAnnouncements: { $type: Boolean, default: true },
weeklyRecaps: { $type: Boolean, default: true },
onboarding: { $type: Boolean, default: true },
majorUpdates: { $type: Boolean, default: true },
subscriptionReminders: { $type: Boolean, default: true },
},
pushNotifications: {
unsubscribeFromAll: { $type: Boolean, default: false },
newPM: { $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 },
majorUpdates: { $type: Boolean, default: true },
mentionParty: { $type: Boolean, default: true },
mentionJoinedGuild: { $type: Boolean, default: true },
mentionUnjoinedGuild: { $type: Boolean, default: true },
partyActivity: { $type: Boolean, default: true },
},
suppressModals: {
levelUp: { $type: Boolean, default: false },
hatchPet: { $type: Boolean, default: false },
raisePet: { $type: Boolean, default: false },
streak: { $type: Boolean, default: false },
},
tasks: {
groupByChallenge: { $type: Boolean, default: false }, // @TODO remove? not used
confirmScoreNotes: { $type: Boolean, default: false }, // @TODO remove? not used
},
improvementCategories: {
$type: Array,
validate: categories => {
const validCategories = ['work', 'exercise', 'healthWellness', 'school', 'teams', 'chores', 'creativity'];
const isValidCategory = categories
.every(category => validCategories.indexOf(category) !== -1);
return isValidCategory;
},
},
},
profile: {
blurb: String,
imageUrl: String,
name: {
$type: String,
required: true,
trim: true,
},
},
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, min: 1 },
// Class System
class: {
$type: String, enum: ['warrior', 'rogue', 'wizard', 'healer'], default: 'warrior', required: true,
},
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 },
spookySparkles: { $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 },
},
},
notifications: [UserNotificationSchema],
tags: [TagSchema],
inbox: {
// messages are stored in the Inbox collection
newMessages: { $type: Number, default: 0 },
blocks: { $type: Array, default: () => [] },
optOut: { $type: Boolean, default: false },
},
tasksOrder: {
habits: [{ $type: String, ref: 'Task' }],
dailys: [{ $type: String, ref: 'Task' }],
todos: [{ $type: String, ref: 'Task' }],
rewards: [{ $type: String, ref: 'Task' }],
},
extra: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
pushDevices: [PushDeviceSchema],
_ABtests: {
$type: Schema.Types.Mixed,
default: () => ({}),
},
webhooks: [WebhookSchema],
loginIncentives: { $type: Number, default: 0 },
invitesSent: { $type: Number, default: 0 },
// Items manually pinned by the user
pinnedItems: [{
_id: false,
path: { $type: String },
type: { $type: String },
}],
// Ordered array of shown pinned items,
// necessary for sorting because seasonal items are not stored in pinnedItems
pinnedItemsOrder: [{ $type: String }],
// Items the user manually unpinned from the ones suggested by Habitica
unpinnedItems: [{
_id: false,
path: { $type: String },
type: { $type: String },
}],
// only visible to staff and moderators
secret: {
text: String,
},
}, {
skipVersioning: { notifications: true },
strict: true,
minimize: false, // So empty objects are returned
typeKey: '$type', // So that we can use fields named `type`
});