mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-15 13:47:33 +01:00
* add date check
* achievements modal polishing
* refresh private-messages page when you are already on it
* add countbadge knob to change the example
* fix lint
* typos
* typos
* typos
* add toggle for achievements categories
* typo
* fix test
* fix edit avatar modal cannot be closed
* WIP(settings): subscriber page improvements
* WIP(subscriptions): more design build-out
* fix(css): disabled button styles
* fix(css): better Amazon targeting
* fix hide tooltip + align header correctly
* disable perfect scroll
* load messages on refresh event
* fix header label + conversation actions not breaking layout on hover
* WIP(g1g1): notif
* WIP(g1g1): notif cont'd
* fix(test): snowball change
* fix(event): feature NYE card
* chore(sprites): compile
* fix(bgs): include TT required field
* add gifting banner to the max height calculation
* chore(event): enable winter customizations
* WIP(gifting): partial modal implementation
* feat(gifting): select giftee modal
* fix(gifting): notification order, modal dismiss
* Begin implementing sign in with apple
# Conflicts:
# package-lock.json
# website/common/script/constants.js
# website/server/libs/auth/social.js
# website/server/models/user/schema.js
* Add apple sign in button to website
* fix lint errors
* fix config json
* fix(modals): correct some repops
* fix(gifting): style updates
* fix(buy): modal style changes
* fix(modals): also clean out "prev"
* Attempt workaround for sign in with apple on android
* temporarily log everything as error
* refactor(modals): hide in dismiss event
* fix temporary test failure
* changes to sign in with apple
* fix: first batch of layout issues for private messages + auto sizing textarea
* fix(modals): new dismiss logic
* fix(modals): new dismiss no go??
* Only use email scope
* print debugging
* .
* ..
* ...
* username second line - open profile on face-avatar/conversation name - fix textarea height
* temporarily disable apple auth and just return data for debugging
* Hopefully this works
* .....
* WIP(subscription): unsubscribed state
* .
* ..
* MAYBE THIS ACTUALLY WORKS???
* Implement apple sign in
* fix some urls
* fix urls
* fix redirect and auth
* attempt to also request name
* fix lint error
* WIP(subscription): partial subscribed
* chore(sprites): compile
* Change approach so that it actually works
* fix config error
* fix lint errors
* Fix
* fix lint error
* lint error
* WIP(subscription): finish subscribed
* refresh on sync
* new "you dont have any messages" style + changed min textarea height
* new conversationItem style / layout
* reset message unread on reload
* chore(npm): update package-locks
* fix styles / textarea height
* feat(subscription): revised sub page RC
* list optOut / chatRevoked informations for each conversation + show why its disabled
* Improve apple redirect view
* Fix apple icon on group task registration page
* WIP(adventure): prereqs
* Block / Unblock - correct disabled states - $gray-200 instead of 300/400
* canReceive not checking chatRevoked
* fix: faceAvatar / userLink open the selected conversation user
* check if the target user is blocking the logged-in user
* fix(subs): style tweaks
* fix(profiles): short circuit contributor
Attempted fix for #11830
* chore(sprites): compile
* fix(content): missing potion data
* fix(content): missing string
* WIP(drops): new modal
* fix(subs): moar style tweaks
* check if blocks is undefined
* max-height instead of height
* fix "no messages" state + canReceive on a new conversation
* WIP(adventure): analytics fixes etc
* Improve apple signin handling
* fixed conversations width (280px on max 768 width page)
* feat(adventure): random egg+potion on 2nd task
* fix(lint): noworkies
* fix(modal): correctly construct classes
* fix(tests): expectations and escape
* Fix typo
* use base url from env variables
* fix lint
* call autosize after message is sent
* fix urls
* always verify token
* throw error when social auth could not retrieve id
* Store emails correctly for apple auth
* Retrieve name when authenticating through apple
* Fix lint errors
* fix all lint errors
* fix(content): missing strings
* Revert "always verify token"
This reverts commit 8ac40c76bf.
# Conflicts:
# website/server/libs/auth/social.js
* Correctly load name
* remove extra changes
* remove extra logger call
* reset package and package-lock
* add back missing packages
* use name from apple
* add support for multiple apple public keys
* add some unit and integration tests
* add apple auth integration test
* tweak social signup buttons
* pixel pushing
Co-authored-by: Matteo Pagliazzi <matteopagliazzi@gmail.com>
Co-authored-by: Sabe Jones <sabrecat@gmail.com>
Co-authored-by: negue <eugen.bolz@gmail.com>
Co-authored-by: Phillip Thelen <phillip@habitica.com>
663 lines
22 KiB
JavaScript
663 lines
22 KiB
JavaScript
import mongoose 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 { Schema } = mongoose;
|
|
|
|
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,
|
|
// Onboarding Guide
|
|
createdTask: Boolean,
|
|
completedTask: Boolean,
|
|
hatchedPet: Boolean,
|
|
fedPet: Boolean,
|
|
purchasedEquipment: Boolean,
|
|
},
|
|
|
|
backer: {
|
|
tier: Number,
|
|
npc: String,
|
|
tokensApplied: Boolean,
|
|
},
|
|
|
|
contributor: {
|
|
// 1-9, see https://trello.com/c/wkFzONhE/277-contributor-gear
|
|
// https://github.com/HabitRPG/habitica/issues/3801
|
|
level: {
|
|
$type: Number,
|
|
min: 0,
|
|
max: 9,
|
|
},
|
|
admin: Boolean,
|
|
sudo: Boolean,
|
|
// Artisan, Friend, Blacksmith, etc
|
|
text: String,
|
|
// a markdown textarea to list their contributions + links
|
|
contributions: String,
|
|
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 },
|
|
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 },
|
|
}],
|
|
}, {
|
|
skipVersioning: { notifications: true },
|
|
strict: true,
|
|
minimize: false, // So empty objects are returned
|
|
typeKey: '$type', // So that we can use fields named `type`
|
|
});
|