Remove localstorage and add notifications (#7588)

* move remaining files frm /common/script/public to website/public

* remove localstorage

* add back noscript template and put all javascript in the footer

* fixes client side tests

* remove double quotes where possible

* simplify jade code and add tests for buildManifest

* loading page with logo and spinner

* better loading screen in landscape mode

* icon on top of text logo

* wip: user.notifications

* notifications: simpler and working code

* finish implementing notifications

* correct loading screen css and re-inline images

* add tests for user notifications

* split User model in multiple files

* remove old comment about missing .catch()

* correctly setup hooks and methods for User model. Cleanup localstorage

* include UserNotificationsService in static page js and split loading-screen css in its own file

* add cron notification and misc fixes

* remove console.log

* fix tests

* fix multiple notifications
This commit is contained in:
Matteo Pagliazzi
2016-06-07 16:14:19 +02:00
parent e0aff79ee4
commit f7be7205e7
49 changed files with 915 additions and 436 deletions

View File

@@ -154,6 +154,8 @@ api.updateHero = {
tierDiff--;
newTier--; // give them gems for the next tier down if they weren't aready that tier
}
hero.addNotification('NEW_CONTRIBUTOR_LEVEL');
}
if (updateData.contributor) _.assign(hero.contributor, updateData.contributor);

View File

@@ -0,0 +1,47 @@
import { authWithHeaders } from '../../middlewares/api-v3/auth';
import _ from 'lodash';
import {
NotFound,
} from '../../libs/api-v3/errors';
let api = {};
/**
* @apiIgnore Not yet part of the public API
* @api {post} /api/v3/notifications/:notificationId/read Mark one notification as read
* @apiVersion 3.0.0
* @apiName ReadNotification
* @apiGroup Notification
*
* @apiParam {UUID} notificationId
*
* @apiSuccess {Object} data user.notifications
*/
api.readNotification = {
method: 'POST',
url: '/notifications/:notificationId/read',
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
req.checkParams('notificationId', res.t('notificationIdRequired')).notEmpty();
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let index = _.findIndex(user.notifications, {
id: req.params.notificationId,
});
if (index === -1) {
throw new NotFound(res.t('messageNotificationNotFound'));
}
user.notifications.splice(index, 1);
await user.save();
res.respond(200, user.notifications);
},
};
module.exports = api;

View File

@@ -39,7 +39,7 @@ export function getBuildUrl (url) {
return `/${buildFiles[url] || url}`;
}
export function getManifestFiles (page) {
export function getManifestFiles (page, type) {
let files = manifestFiles[page];
if (!files) throw new Error(`Page "${page}" not found!`);
@@ -47,15 +47,25 @@ export function getManifestFiles (page) {
let htmlCode = '';
if (IS_PROD) {
htmlCode += `<link rel="stylesheet" type="text/css" href="${getBuildUrl(page + '.css')}">`; // eslint-disable-line prefer-template
htmlCode += `<script type="text/javascript" src="${getBuildUrl(page + '.js')}"></script>`; // eslint-disable-line prefer-template
if (type !== 'js') {
htmlCode += `<link rel="stylesheet" type="text/css" href="${getBuildUrl(page + '.css')}">`; // eslint-disable-line prefer-template
}
if (type !== 'css') {
htmlCode += `<script type="text/javascript" src="${getBuildUrl(page + '.js')}"></script>`; // eslint-disable-line prefer-template
}
} else {
files.css.forEach((file) => {
htmlCode += `<link rel="stylesheet" type="text/css" href="${getBuildUrl(file)}">`;
});
files.js.forEach((file) => {
htmlCode += `<script type="text/javascript" src="${getBuildUrl(file)}"></script>`;
});
if (type !== 'js') {
files.css.forEach((file) => {
htmlCode += `<link rel="stylesheet" type="text/css" href="${getBuildUrl(file)}">`;
});
}
if (type !== 'css') {
files.js.forEach((file) => {
htmlCode += `<script type="text/javascript" src="${getBuildUrl(file)}"></script>`;
});
}
}
return htmlCode;

View File

@@ -111,6 +111,9 @@ function performSleepTasks (user, tasksByType, now) {
export function cron (options = {}) {
let {user, tasksByType, analytics, now = new Date(), daysMissed, timezoneOffsetFromUserPrefs} = options;
// Record pre-cron values of HP and MP to show notifications later
let beforeCronStats = _.pick(user.stats, ['hp', 'mp']);
user.preferences.timezoneOffsetAtLastCron = timezoneOffsetFromUserPrefs;
// User is only allowed a certain number of drops a day. This resets the count.
if (user.items.lastDrop.count > 0) user.items.lastDrop.count = 0;
@@ -279,6 +282,25 @@ export function cron (options = {}) {
let _progress = _.cloneDeep(progress);
_.merge(progress, {down: 0, up: 0, collectedItems: 0});
// Send notification for changes in HP and MP
// First remove a possible previous cron notification
// we don't want to flood the users with many cron notifications at once
let oldCronNotif = user.notifications.toObject().find((notif, index) => {
if (notif.type === 'CRON') {
user.notifications.splice(index, 1);
return true;
} else {
return false;
}
});
user.addNotification('CRON', {
hp: user.stats.hp - beforeCronStats.hp - (oldCronNotif ? oldCronNotif.data.hp : 0),
mp: user.stats.mp - beforeCronStats.mp - (oldCronNotif ? oldCronNotif.data.mp : 0),
});
// TODO: Clean PMs - keep 200 for subscribers and 50 for free users. Should also be done while resting in the inn
// let numberOfPMs = Object.keys(user.inbox.messages).length;
// if (numberOfPMs > maxPMs) {

View File

@@ -14,8 +14,11 @@ module.exports = function responseHandler (req, res, next) {
// sends back the current user._v in the response so that the client
// can verify if it's the most up to date data.
// Considered part of the private API for now and not officially supported
if (user && req.query.userV) {
response.userV = user._v;
if (user) {
response.notifications = user.notifications.map(notification => notification.toJSON());
if (req.query.userV) {
response.userV = user._v;
}
}
res.status(status).json(response);

View File

@@ -12,7 +12,6 @@ module.exports = function staticMiddleware (expressApp) {
expressApp.use(express.static(BUILD_DIR, { maxAge: MAX_AGE }));
expressApp.use('/common/dist', express.static(`${PUBLIC_DIR}/../../common/dist`, { maxAge: MAX_AGE }));
expressApp.use('/common/audio', express.static(`${PUBLIC_DIR}/../../common/audio`, { maxAge: MAX_AGE }));
expressApp.use('/common/script/public', express.static(`${PUBLIC_DIR}/../../common/script/public`, { maxAge: MAX_AGE }));
expressApp.use('/common/img', express.static(`${PUBLIC_DIR}/../../common/img`, { maxAge: MAX_AGE }));
expressApp.use(express.static(PUBLIC_DIR));
};

View File

@@ -15,7 +15,7 @@ import { sendTxn as txnEmail } from '../libs/api-v3/email';
import sendPushNotification from '../libs/api-v3/pushNotifications';
import cwait from 'cwait';
let Schema = mongoose.Schema;
const Schema = mongoose.Schema;
let schema = new Schema({
name: {type: String, required: true},
@@ -286,7 +286,11 @@ schema.methods.closeChal = async function closeChal (broken = {}) {
if (winner) {
winner.achievements.challenges.push(challenge.name);
winner.balance += challenge.prize / 4;
winner.addNotification('WON_CHALLENGE');
let savedWinner = await winner.save();
if (savedWinner.preferences.emailNotifications.wonChallenge !== false) {
txnEmail(savedWinner, 'won-challenge', [
{name: 'CHALLENGE_NAME', content: challenge.name},

View File

@@ -3,7 +3,7 @@ import baseModel from '../libs/api-v3/baseModel';
import { v4 as uuid } from 'uuid';
import validator from 'validator';
let Schema = mongoose.Schema;
const Schema = mongoose.Schema;
export let schema = new Schema({
id: {

View File

@@ -6,7 +6,8 @@ import baseModel from '../libs/api-v3/baseModel';
import _ from 'lodash';
import { preenHistory } from '../libs/api-v3/preening';
let Schema = mongoose.Schema;
const Schema = mongoose.Schema;
let discriminatorOptions = {
discriminatorKey: 'type', // the key that distinguishes task types
};

View File

@@ -0,0 +1,170 @@
import shared from '../../../../common';
import _ from 'lodash';
import moment from 'moment';
import * as Tasks from '../task';
import Bluebird from 'bluebird';
import baseModel from '../../libs/api-v3/baseModel';
import schema from './schema';
schema.plugin(baseModel, {
// noSet is not used as updating uses a whitelist and creating only accepts specific params (password, email, username, ...)
noSet: [],
private: ['auth.local.hashed_password', 'auth.local.salt', '_cronSignature'],
toJSONTransform: function userToJSON (plainObj, originalDoc) {
plainObj._tmp = originalDoc._tmp; // be sure to send down drop notifs
return plainObj;
},
});
schema.post('init', function postInitUser (doc) {
shared.wrap(doc);
});
function _populateDefaultTasks (user, taskTypes) {
let tagsI = taskTypes.indexOf('tag');
if (tagsI !== -1) {
user.tags = _.map(shared.content.userDefaults.tags, (tag) => {
let newTag = _.cloneDeep(tag);
// tasks automatically get _id=helpers.uuid() from TaskSchema id.default, but tags are Schema.Types.Mixed - so we need to manually invoke here
newTag.id = shared.uuid();
// Render tag's name in user's language
newTag.name = newTag.name(user.preferences.language);
return newTag;
});
}
let tasksToCreate = [];
if (tagsI !== -1) {
taskTypes = _.clone(taskTypes);
taskTypes.splice(tagsI, 1);
}
_.each(taskTypes, (taskType) => {
let tasksOfType = _.map(shared.content.userDefaults[`${taskType}s`], (taskDefaults) => {
let newTask = new Tasks[taskType](taskDefaults);
newTask.userId = user._id;
newTask.text = taskDefaults.text(user.preferences.language);
if (newTask.notes) newTask.notes = taskDefaults.notes(user.preferences.language);
if (taskDefaults.checklist) {
newTask.checklist = _.map(taskDefaults.checklist, (checklistItem) => {
checklistItem.text = checklistItem.text(user.preferences.language);
return checklistItem;
});
}
return newTask.save();
});
tasksToCreate.push(...tasksOfType);
});
return Bluebird.all(tasksToCreate)
.then((tasksCreated) => {
_.each(tasksCreated, (task) => {
user.tasksOrder[`${task.type}s`].push(task._id);
});
});
}
function _populateDefaultsForNewUser (user) {
let taskTypes;
let iterableFlags = user.flags.toObject();
if (user.registeredThrough === 'habitica-web' || user.registeredThrough === 'habitica-android') {
taskTypes = ['habit', 'daily', 'todo', 'reward', 'tag'];
_.each(iterableFlags.tutorial.common, (val, section) => {
user.flags.tutorial.common[section] = true;
});
} else {
taskTypes = ['todo', 'tag'];
user.flags.showTour = false;
_.each(iterableFlags.tour, (val, section) => {
user.flags.tour[section] = -2;
});
}
return _populateDefaultTasks(user, taskTypes);
}
function _setProfileName (user) {
let fb = user.auth.facebook;
let localUsername = user.auth.local && user.auth.local.username;
let facebookUsername = fb && (fb.displayName || fb.name || fb.username || `${fb.first_name && fb.first_name} ${fb.last_name}`);
let anonymous = 'Anonymous';
return localUsername || facebookUsername || anonymous;
}
schema.pre('save', true, function preSaveUser (next, done) {
next();
if (_.isNaN(this.preferences.dayStart) || this.preferences.dayStart < 0 || this.preferences.dayStart > 23) {
this.preferences.dayStart = 0;
}
if (!this.profile.name) {
this.profile.name = _setProfileName(this);
}
// 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++;
// Populate new users with default content
if (this.isNew) {
_populateDefaultsForNewUser(this)
.then(() => done())
.catch(done);
} else {
done();
}
});
schema.pre('update', function preUpdateUser () {
this.update({}, {$inc: {_v: 1}});
});

View File

@@ -0,0 +1,33 @@
import mongoose from 'mongoose';
import schema from './schema';
require('./hooks');
require('./methods');
// A list of publicly accessible fields (not everything from preferences because there are also a lot of settings tha should remain private)
export let publicFields = `preferences.size preferences.hair preferences.skin preferences.shirt
preferences.chair preferences.costume preferences.sleep preferences.background profile stats
achievements party backer contributor auth.timestamps items`;
// The minimum amount of data needed when populating multiple users
export let nameFields = 'profile.name';
export { schema };
export let model = mongoose.model('User', schema);
// 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
export let mods = [];
mongoose.model('User')
.find({'contributor.admin': true})
.sort('-contributor.level -backer.npc profile.name')
.select('profile contributor backer')
.exec()
.then((foundMods) => {
// Using push to maintain the reference to mods
mods.push(...foundMods);
});

View File

@@ -0,0 +1,129 @@
import shared from '../../../../common';
import _ from 'lodash';
import * as Tasks from '../task';
import Bluebird from 'bluebird';
import {
chatDefaults,
TAVERN_ID,
} from '../group';
import { defaults } from 'lodash';
import schema from './schema';
schema.methods.isSubscribed = function isSubscribed () {
return !!this.purchased.plan.customerId; // eslint-disable-line no-implicit-coercion
};
// Get an array of groups ids the user is member of
schema.methods.getGroups = function getUserGroups () {
let userGroups = this.guilds.slice(0); // clone user.guilds so we don't modify the original
if (this.party._id) userGroups.push(this.party._id);
userGroups.push(TAVERN_ID);
return userGroups;
};
schema.methods.sendMessage = async function sendMessage (userToReceiveMessage, message) {
let sender = this;
shared.refPush(userToReceiveMessage.inbox.messages, chatDefaults(message, sender));
userToReceiveMessage.inbox.newMessages++;
userToReceiveMessage._v++;
userToReceiveMessage.markModified('inbox.messages');
shared.refPush(sender.inbox.messages, defaults({sent: true}, chatDefaults(message, userToReceiveMessage)));
sender.markModified('inbox.messages');
let promises = [userToReceiveMessage.save(), sender.save()];
await Bluebird.all(promises);
};
schema.methods.addNotification = function addUserNotification (type, data = {}) {
this.notifications.push({
type,
data,
});
};
// Methods to adapt the new schema to API v2 responses (mostly tasks inside the user model)
// These will be removed once API v2 is discontinued
// Get all the tasks belonging to a user,
schema.methods.getTasks = function getUserTasks () {
let args = Array.from(arguments);
let cb;
let type;
if (args.length === 1) {
cb = args[0];
} else {
type = args[0];
cb = args[1];
}
let query = {
userId: this._id,
};
if (type) query.type = type;
Tasks.Task.find(query, cb);
};
// Given user and an array of tasks, return an API compatible user + tasks obj
schema.methods.addTasksToUser = function addTasksToUser (tasks) {
let obj = this.toJSON();
obj.id = obj._id;
obj.filters = {};
obj.tags = obj.tags.map(tag => {
return {
id: tag.id,
name: tag.name,
challenge: tag.challenge,
};
});
let tasksOrder = obj.tasksOrder; // Saving a reference because we won't return it
obj.habits = [];
obj.dailys = [];
obj.todos = [];
obj.rewards = [];
obj.tasksOrder = undefined;
let unordered = [];
tasks.forEach((task) => {
// We want to push the task at the same position where it's stored in tasksOrder
let pos = tasksOrder[`${task.type}s`].indexOf(task._id);
if (pos === -1) { // Should never happen, it means the lists got out of sync
unordered.push(task.toJSONV2());
} else {
obj[`${task.type}s`][pos] = task.toJSONV2();
}
});
// Reconcile unordered items
unordered.forEach((task) => {
obj[`${task.type}s`].push(task);
});
// Remove null values that can be created when inserting tasks at an index > length
['habits', 'dailys', 'rewards', 'todos'].forEach((type) => {
obj[type] = _.compact(obj[type]);
});
return obj;
};
// Return the data maintaining backward compatibility
schema.methods.getTransformedData = function getTransformedData (cb) {
let self = this;
this.getTasks((err, tasks) => {
if (err) return cb(err);
cb(null, self.addTasksToUser(tasks));
});
};
// END of API v2 methods

View File

@@ -1,22 +1,16 @@
import mongoose from 'mongoose';
import shared from '../../../common';
import shared from '../../../../common';
import _ from 'lodash';
import validator from 'validator';
import moment from 'moment';
import * as Tasks from './task';
import Bluebird from 'bluebird';
import { schema as TagSchema } from './tag';
import baseModel from '../libs/api-v3/baseModel';
import { schema as TagSchema } from '../tag';
import {
chatDefaults,
TAVERN_ID,
} from './group';
import { defaults } from 'lodash';
schema as UserNotificationSchema,
} from '../userNotification';
let Schema = mongoose.Schema;
const Schema = mongoose.Schema;
// User schema definition
export let schema = new Schema({
let schema = new Schema({
apiToken: {
type: String,
default: shared.uuid,
@@ -495,6 +489,7 @@ export let schema = new Schema({
},
},
notifications: [UserNotificationSchema],
tags: [TagSchema],
inbox: {
@@ -514,312 +509,13 @@ export let schema = new Schema({
extra: {type: Schema.Types.Mixed, default: () => {
return {};
}},
pushDevices: {
type: [{
regId: {type: String},
type: {type: String},
}],
default: () => [],
},
pushDevices: [{
regId: {type: String},
type: {type: String},
}],
}, {
strict: true,
minimize: false, // So empty objects are returned
});
schema.plugin(baseModel, {
// noSet is not used as updating uses a whitelist and creating only accepts specific params (password, email, username, ...)
noSet: [],
private: ['auth.local.hashed_password', 'auth.local.salt', '_cronSignature'],
toJSONTransform: function userToJSON (plainObj, originalDoc) {
// plainObj.filters = {}; // TODO Not saved, remove?
plainObj._tmp = originalDoc._tmp; // be sure to send down drop notifs
return plainObj;
},
});
// A list of publicly accessible fields (not everything from preferences because there are also a lot of settings tha should remain private)
export let publicFields = `preferences.size preferences.hair preferences.skin preferences.shirt
preferences.chair preferences.costume preferences.sleep preferences.background profile stats
achievements party backer contributor auth.timestamps items`;
// The minimum amount of data needed when populating multiple users
export let nameFields = 'profile.name';
schema.post('init', function postInitUser (doc) {
shared.wrap(doc);
});
function _populateDefaultTasks (user, taskTypes) {
let tagsI = taskTypes.indexOf('tag');
if (tagsI !== -1) {
user.tags = _.map(shared.content.userDefaults.tags, (tag) => {
let newTag = _.cloneDeep(tag);
// tasks automatically get _id=helpers.uuid() from TaskSchema id.default, but tags are Schema.Types.Mixed - so we need to manually invoke here
newTag.id = shared.uuid();
// Render tag's name in user's language
newTag.name = newTag.name(user.preferences.language);
return newTag;
});
}
let tasksToCreate = [];
if (tagsI !== -1) {
taskTypes = _.clone(taskTypes);
taskTypes.splice(tagsI, 1);
}
_.each(taskTypes, (taskType) => {
let tasksOfType = _.map(shared.content.userDefaults[`${taskType}s`], (taskDefaults) => {
let newTask = new Tasks[taskType](taskDefaults);
newTask.userId = user._id;
newTask.text = taskDefaults.text(user.preferences.language);
if (newTask.notes) newTask.notes = taskDefaults.notes(user.preferences.language);
if (taskDefaults.checklist) {
newTask.checklist = _.map(taskDefaults.checklist, (checklistItem) => {
checklistItem.text = checklistItem.text(user.preferences.language);
return checklistItem;
});
}
return newTask.save();
});
tasksToCreate.push(...tasksOfType);
});
return Bluebird.all(tasksToCreate)
.then((tasksCreated) => {
_.each(tasksCreated, (task) => {
user.tasksOrder[`${task.type}s`].push(task._id);
});
});
}
function _populateDefaultsForNewUser (user) {
let taskTypes;
let iterableFlags = user.flags.toObject();
if (user.registeredThrough === 'habitica-web' || user.registeredThrough === 'habitica-android') {
taskTypes = ['habit', 'daily', 'todo', 'reward', 'tag'];
_.each(iterableFlags.tutorial.common, (val, section) => {
user.flags.tutorial.common[section] = true;
});
} else {
taskTypes = ['todo', 'tag'];
user.flags.showTour = false;
_.each(iterableFlags.tour, (val, section) => {
user.flags.tour[section] = -2;
});
}
return _populateDefaultTasks(user, taskTypes);
}
function _setProfileName (user) {
let fb = user.auth.facebook;
let localUsername = user.auth.local && user.auth.local.username;
let facebookUsername = fb && (fb.displayName || fb.name || fb.username || `${fb.first_name && fb.first_name} ${fb.last_name}`);
let anonymous = 'Anonymous';
return localUsername || facebookUsername || anonymous;
}
schema.pre('save', true, function preSaveUser (next, done) {
next();
if (_.isNaN(this.preferences.dayStart) || this.preferences.dayStart < 0 || this.preferences.dayStart > 23) {
this.preferences.dayStart = 0;
}
if (!this.profile.name) {
this.profile.name = _setProfileName(this);
}
// 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++;
// Populate new users with default content
if (this.isNew) {
_populateDefaultsForNewUser(this)
.then(() => done())
.catch(done);
} else {
done();
}
});
schema.pre('update', function preUpdateUser () {
this.update({}, {$inc: {_v: 1}});
});
schema.methods.isSubscribed = function isSubscribed () {
return !!this.purchased.plan.customerId; // eslint-disable-line no-implicit-coercion
};
// Get an array of groups ids the user is member of
schema.methods.getGroups = function getUserGroups () {
let userGroups = this.guilds.slice(0); // clone user.guilds so we don't modify the original
if (this.party._id) userGroups.push(this.party._id);
userGroups.push(TAVERN_ID);
return userGroups;
};
schema.methods.sendMessage = async function sendMessage (userToReceiveMessage, message) {
let sender = this;
shared.refPush(userToReceiveMessage.inbox.messages, chatDefaults(message, sender));
userToReceiveMessage.inbox.newMessages++;
userToReceiveMessage._v++;
userToReceiveMessage.markModified('inbox.messages');
shared.refPush(sender.inbox.messages, defaults({sent: true}, chatDefaults(message, userToReceiveMessage)));
sender.markModified('inbox.messages');
let promises = [userToReceiveMessage.save(), sender.save()];
await Bluebird.all(promises);
};
// Methods to adapt the new schema to API v2 responses (mostly tasks inside the user model)
// These will be removed once API v2 is discontinued
// Get all the tasks belonging to a user,
schema.methods.getTasks = function getUserTasks () {
let args = Array.from(arguments);
let cb;
let type;
if (args.length === 1) {
cb = args[0];
} else {
type = args[0];
cb = args[1];
}
let query = {
userId: this._id,
};
if (type) query.type = type;
Tasks.Task.find(query, cb);
};
// Given user and an array of tasks, return an API compatible user + tasks obj
schema.methods.addTasksToUser = function addTasksToUser (tasks) {
let obj = this.toJSON();
obj.id = obj._id;
obj.filters = {};
obj.tags = obj.tags.map(tag => {
return {
id: tag.id,
name: tag.name,
challenge: tag.challenge,
};
});
let tasksOrder = obj.tasksOrder; // Saving a reference because we won't return it
obj.habits = [];
obj.dailys = [];
obj.todos = [];
obj.rewards = [];
obj.tasksOrder = undefined;
let unordered = [];
tasks.forEach((task) => {
// We want to push the task at the same position where it's stored in tasksOrder
let pos = tasksOrder[`${task.type}s`].indexOf(task._id);
if (pos === -1) { // Should never happen, it means the lists got out of sync
unordered.push(task.toJSONV2());
} else {
obj[`${task.type}s`][pos] = task.toJSONV2();
}
});
// Reconcile unordered items
unordered.forEach((task) => {
obj[`${task.type}s`].push(task);
});
// Remove null values that can be created when inserting tasks at an index > length
['habits', 'dailys', 'rewards', 'todos'].forEach((type) => {
obj[type] = _.compact(obj[type]);
});
return obj;
};
// Return the data maintaining backward compatibility
schema.methods.getTransformedData = function getTransformedData (cb) {
let self = this;
this.getTasks((err, tasks) => {
if (err) return cb(err);
cb(null, self.addTasksToUser(tasks));
});
};
// END of API v2 methods
export let model = mongoose.model('User', schema);
// 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
export let mods = [];
mongoose.model('User')
.find({'contributor.admin': true})
.sort('-contributor.level -backer.npc profile.name')
.select('profile contributor backer')
.exec()
.then((foundMods) => {
// Using push to maintain the reference to mods
mods.push(...foundMods);
}); // In case of failure we don't want this to crash the whole server
module.exports = schema;

View File

@@ -0,0 +1,42 @@
import mongoose from 'mongoose';
import baseModel from '../libs/api-v3/baseModel';
import { v4 as uuid } from 'uuid';
import validator from 'validator';
const NOTIFICATION_TYPES = [
'DROPS_ENABLED',
'REBIRTH_ENABLED',
'WON_CHALLENGE',
'STREAK_ACHIEVEMENT',
'ULTIMATE_GEAR_ACHIEVEMENT',
'REBIRTH_ACHIEVEMENT',
'NEW_CONTRIBUTOR_LEVEL',
'CRON',
];
const Schema = mongoose.Schema;
export let schema = new Schema({
id: {
type: String,
default: uuid,
validate: [validator.isUUID, 'Invalid uuid.'],
},
type: {type: String, required: true, enum: NOTIFICATION_TYPES},
data: {type: Schema.Types.Mixed, default: () => {
return {};
}},
}, {
strict: true,
minimize: false, // So empty objects are returned
_id: false, // use id instead of _id
});
schema.plugin(baseModel, {
noSet: ['_id', 'id'],
timestamps: true,
private: ['updatedAt'],
_id: false, // use id instead of _id
});
export let model = mongoose.model('UserNotification', schema);