mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-19 15:48:04 +01:00
Merge branch 'double_cron_fix' into develop
This commit is contained in:
@@ -13,7 +13,7 @@ describe('GET /user/anonymized', () => {
|
|||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
user = await generateUser();
|
user = await generateUser();
|
||||||
await user.update({ newMessages: ['some', 'new', 'messages'], profile: 'profile', 'purchased.plan': 'purchased plan',
|
await user.update({ newMessages: ['some', 'new', 'messages'], 'profile.name': 'profile', 'purchased.plan': 'purchased plan',
|
||||||
contributor: 'contributor', invitations: 'invitations', 'items.special.nyeReceived': 'some', 'items.special.valentineReceived': 'some',
|
contributor: 'contributor', invitations: 'invitations', 'items.special.nyeReceived': 'some', 'items.special.valentineReceived': 'some',
|
||||||
webhooks: 'some', 'achievements.challenges': 'some',
|
webhooks: 'some', 'achievements.challenges': 'some',
|
||||||
'inbox.messages': [{ text: 'some text' }],
|
'inbox.messages': [{ text: 'some text' }],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* eslint-disable global-require */
|
/* eslint-disable global-require */
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { cron } from '../../../../../website/server/libs/api-v3/cron';
|
import Bluebird from 'bluebird';
|
||||||
|
import { recoverCron, cron } from '../../../../../website/server/libs/api-v3/cron';
|
||||||
import { model as User } from '../../../../../website/server/models/user';
|
import { model as User } from '../../../../../website/server/models/user';
|
||||||
import * as Tasks from '../../../../../website/server/models/task';
|
import * as Tasks from '../../../../../website/server/models/task';
|
||||||
import { clone } from 'lodash';
|
import { clone } from 'lodash';
|
||||||
@@ -34,15 +35,6 @@ describe('cron', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates user.auth.timestamps.loggedin and lastCron', () => {
|
|
||||||
let now = new Date();
|
|
||||||
|
|
||||||
cron({user, tasksByType, daysMissed, analytics, now});
|
|
||||||
|
|
||||||
expect(user.auth.timestamps.loggedin).to.equal(now);
|
|
||||||
expect(user.lastCron).to.equal(now);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('updates user.preferences.timezoneOffsetAtLastCron', () => {
|
it('updates user.preferences.timezoneOffsetAtLastCron', () => {
|
||||||
let timezoneOffsetFromUserPrefs = 1;
|
let timezoneOffsetFromUserPrefs = 1;
|
||||||
|
|
||||||
@@ -571,3 +563,68 @@ describe('cron', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('recoverCron', () => {
|
||||||
|
let locals, status, execStub;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
execStub = sandbox.stub();
|
||||||
|
sandbox.stub(User, 'findOne').returns({ exec: execStub });
|
||||||
|
|
||||||
|
status = { times: 0 };
|
||||||
|
locals = {
|
||||||
|
user: new User({
|
||||||
|
auth: {
|
||||||
|
local: {
|
||||||
|
username: 'username',
|
||||||
|
lowerCaseUsername: 'username',
|
||||||
|
email: 'email@email.email',
|
||||||
|
salt: 'salt',
|
||||||
|
hashed_password: 'hashed_password', // eslint-disable-line camelcase
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error if user cannot be found', async (done) => {
|
||||||
|
execStub.returns(Bluebird.resolve(null));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await recoverCron(status, locals);
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.message).to.eql(`User ${locals.user._id} not found while recovering.`);
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('increases status.times count and reruns up to 3 times', async (done) => {
|
||||||
|
execStub.returns(Bluebird.resolve({_cronSignature: 'RUNNING_CRON'}));
|
||||||
|
execStub.onCall(3).returns(Bluebird.resolve({_cronSignature: 'NOT_RUNNING'}));
|
||||||
|
|
||||||
|
await recoverCron(status, locals);
|
||||||
|
|
||||||
|
expect(status.times).to.eql(3);
|
||||||
|
expect(locals.user).to.eql({_cronSignature: 'NOT_RUNNING'});
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error if recoverCron runs 4 times', async (done) => {
|
||||||
|
execStub.returns(Bluebird.resolve({_cronSignature: 'RUNNING_CRON'}));
|
||||||
|
|
||||||
|
try {
|
||||||
|
await recoverCron(status, locals);
|
||||||
|
} catch (err) {
|
||||||
|
expect(status.times).to.eql(4);
|
||||||
|
expect(err.message).to.eql(`Impossible to recover from cron for user ${locals.user._id}.`);
|
||||||
|
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
import {
|
import {
|
||||||
generateRes,
|
generateRes,
|
||||||
generateReq,
|
generateReq,
|
||||||
generateNext,
|
|
||||||
generateTodo,
|
generateTodo,
|
||||||
generateDaily,
|
generateDaily,
|
||||||
} from '../../../../helpers/api-unit.helper';
|
} from '../../../../helpers/api-unit.helper';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
import cronMiddleware from '../../../../../website/server/middlewares/api-v3/cron';
|
import cronMiddleware from '../../../../../website/server/middlewares/api-v3/cron';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { model as User } from '../../../../../website/server/models/user';
|
import { model as User } from '../../../../../website/server/models/user';
|
||||||
import { model as Group } from '../../../../../website/server/models/group';
|
import { model as Group } from '../../../../../website/server/models/group';
|
||||||
import * as Tasks from '../../../../../website/server/models/task';
|
import * as Tasks from '../../../../../website/server/models/task';
|
||||||
import analyticsService from '../../../../../website/server/libs/api-v3/analyticsService';
|
import analyticsService from '../../../../../website/server/libs/api-v3/analyticsService';
|
||||||
|
import * as cronLib from '../../../../../website/server/libs/api-v3/cron';
|
||||||
import { v4 as generateUUID } from 'uuid';
|
import { v4 as generateUUID } from 'uuid';
|
||||||
|
|
||||||
describe('cron middleware', () => {
|
describe('cron middleware', () => {
|
||||||
let res, req, next;
|
let res, req;
|
||||||
let user;
|
let user;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach((done) => {
|
||||||
res = generateRes();
|
res = generateRes();
|
||||||
req = generateReq();
|
req = generateReq();
|
||||||
next = generateNext();
|
|
||||||
user = new User({
|
user = new User({
|
||||||
auth: {
|
auth: {
|
||||||
local: {
|
local: {
|
||||||
@@ -33,24 +33,31 @@ describe('cron middleware', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
user._statsComputed = {
|
user.save()
|
||||||
mp: 10,
|
.then(savedUser => {
|
||||||
maxMP: 100,
|
savedUser._statsComputed = {
|
||||||
};
|
mp: 10,
|
||||||
|
maxMP: 100,
|
||||||
|
};
|
||||||
|
|
||||||
res.locals.user = user;
|
res.locals.user = savedUser;
|
||||||
res.analytics = analyticsService;
|
res.analytics = analyticsService;
|
||||||
|
done();
|
||||||
|
})
|
||||||
|
.catch(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls next when user is not attached', () => {
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls next when user is not attached', (done) => {
|
||||||
res.locals.user = null;
|
res.locals.user = null;
|
||||||
cronMiddleware(req, res, next);
|
cronMiddleware(req, res, (err) => done(err));
|
||||||
expect(next).to.be.calledOnce;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls next when days have not been missed', () => {
|
it('calls next when days have not been missed', (done) => {
|
||||||
cronMiddleware(req, res, next);
|
cronMiddleware(req, res, (err) => done(err));
|
||||||
expect(next).to.be.calledOnce;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should clear todos older than 30 days for free users', async (done) => {
|
it('should clear todos older than 30 days for free users', async (done) => {
|
||||||
@@ -59,35 +66,37 @@ describe('cron middleware', () => {
|
|||||||
task.dateCompleted = moment(new Date()).subtract({days: 31});
|
task.dateCompleted = moment(new Date()).subtract({days: 31});
|
||||||
task.completed = true;
|
task.completed = true;
|
||||||
await task.save();
|
await task.save();
|
||||||
|
await user.save();
|
||||||
|
|
||||||
cronMiddleware(req, res, () => {
|
cronMiddleware(req, res, (err) => {
|
||||||
Tasks.Task.findOne({_id: task}, function (err, taskFound) {
|
Tasks.Task.findOne({_id: task}, function (secondErr, taskFound) {
|
||||||
expect(err).to.not.exist;
|
expect(secondErr).to.not.exist;
|
||||||
expect(taskFound).to.not.exist;
|
expect(taskFound).to.not.exist;
|
||||||
done();
|
done(err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not clear todos older than 30 days for subscribed users', (done) => {
|
it('should not clear todos older than 30 days for subscribed users', async (done) => {
|
||||||
user.purchased.plan.customerId = 'subscribedId';
|
user.purchased.plan.customerId = 'subscribedId';
|
||||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||||
user.lastCron = moment(new Date()).subtract({days: 2});
|
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||||
let task = generateTodo(user);
|
let task = generateTodo(user);
|
||||||
task.dateCompleted = moment(new Date()).subtract({days: 31});
|
task.dateCompleted = moment(new Date()).subtract({days: 31});
|
||||||
task.completed = true;
|
task.completed = true;
|
||||||
task.save();
|
await task.save();
|
||||||
|
await user.save();
|
||||||
|
|
||||||
cronMiddleware(req, res, () => {
|
cronMiddleware(req, res, (err) => {
|
||||||
Tasks.Task.findOne({_id: task}, function (err, taskFound) {
|
Tasks.Task.findOne({_id: task}, function (secondErr, taskFound) {
|
||||||
expect(err).to.not.exist;
|
expect(secondErr).to.not.exist;
|
||||||
expect(taskFound).to.exist;
|
expect(taskFound).to.exist;
|
||||||
done();
|
done(err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should clear todos older than 90 days for subscribed users', (done) => {
|
it('should clear todos older than 90 days for subscribed users', async (done) => {
|
||||||
user.purchased.plan.customerId = 'subscribedId';
|
user.purchased.plan.customerId = 'subscribedId';
|
||||||
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
|
||||||
user.lastCron = moment(new Date()).subtract({days: 2});
|
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||||
@@ -95,46 +104,60 @@ describe('cron middleware', () => {
|
|||||||
let task = generateTodo(user);
|
let task = generateTodo(user);
|
||||||
task.dateCompleted = moment(new Date()).subtract({days: 91});
|
task.dateCompleted = moment(new Date()).subtract({days: 91});
|
||||||
task.completed = true;
|
task.completed = true;
|
||||||
task.save();
|
await task.save();
|
||||||
|
await user.save();
|
||||||
|
|
||||||
cronMiddleware(req, res, () => {
|
cronMiddleware(req, res, (err) => {
|
||||||
Tasks.Task.findOne({_id: task}, function (err, taskFound) {
|
Tasks.Task.findOne({_id: task}, function (secondErr, taskFound) {
|
||||||
expect(err).to.not.exist;
|
expect(secondErr).to.not.exist;
|
||||||
expect(taskFound).to.not.exist;
|
expect(taskFound).to.not.exist;
|
||||||
done();
|
done(err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call next is user was not modified after cron', (done) => {
|
it('should call next if user was not modified after cron', async (done) => {
|
||||||
let hpBefore = user.stats.hp;
|
let hpBefore = user.stats.hp;
|
||||||
user.lastCron = moment(new Date()).subtract({days: 2});
|
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||||
|
await user.save();
|
||||||
|
|
||||||
user.save().then(function () {
|
cronMiddleware(req, res, (err) => {
|
||||||
cronMiddleware(req, res, function () {
|
expect(hpBefore).to.equal(user.stats.hp);
|
||||||
expect(hpBefore).to.equal(user.stats.hp);
|
done(err);
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does damage for missing dailies', (done) => {
|
it('updates user.auth.timestamps.loggedin and lastCron', async (done) => {
|
||||||
|
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||||
|
let now = new Date();
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
cronMiddleware(req, res, (err) => {
|
||||||
|
expect(moment(now).isSame(user.lastCron, 'day'));
|
||||||
|
expect(moment(now).isSame(user.auth.timestamps.loggedin, 'day'));
|
||||||
|
done(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does damage for missing dailies', async (done) => {
|
||||||
let hpBefore = user.stats.hp;
|
let hpBefore = user.stats.hp;
|
||||||
user.lastCron = moment(new Date()).subtract({days: 2});
|
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||||
let daily = generateDaily(user);
|
let daily = generateDaily(user);
|
||||||
daily.startDate = moment(new Date()).subtract({days: 2});
|
daily.startDate = moment(new Date()).subtract({days: 2});
|
||||||
daily.save();
|
await daily.save();
|
||||||
|
await user.save();
|
||||||
|
|
||||||
cronMiddleware(req, res, () => {
|
cronMiddleware(req, res, (err) => {
|
||||||
expect(user.stats.hp).to.be.lessThan(hpBefore);
|
expect(user.stats.hp).to.be.lessThan(hpBefore);
|
||||||
done();
|
done(err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('updates tasks', (done) => {
|
it('updates tasks', async (done) => {
|
||||||
user.lastCron = moment(new Date()).subtract({days: 2});
|
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||||
let todo = generateTodo(user);
|
let todo = generateTodo(user);
|
||||||
let todoValueBefore = todo.value;
|
let todoValueBefore = todo.value;
|
||||||
|
await user.save();
|
||||||
|
|
||||||
cronMiddleware(req, res, () => {
|
cronMiddleware(req, res, () => {
|
||||||
Tasks.Task.findOne({_id: todo._id}, function (err, todoFound) {
|
Tasks.Task.findOne({_id: todo._id}, function (err, todoFound) {
|
||||||
@@ -150,7 +173,7 @@ describe('cron middleware', () => {
|
|||||||
user.lastCron = moment(new Date()).subtract({days: 2});
|
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||||
let daily = generateDaily(user);
|
let daily = generateDaily(user);
|
||||||
daily.startDate = moment(new Date()).subtract({days: 2});
|
daily.startDate = moment(new Date()).subtract({days: 2});
|
||||||
daily.save();
|
await daily.save();
|
||||||
|
|
||||||
let questKey = 'dilatory';
|
let questKey = 'dilatory';
|
||||||
user.party.quest.key = questKey;
|
user.party.quest.key = questKey;
|
||||||
@@ -174,4 +197,28 @@ describe('cron middleware', () => {
|
|||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('recovers from failed cron and does not error when user is already cronning', async (done) => {
|
||||||
|
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
let updatedUser = cloneDeep(user);
|
||||||
|
updatedUser.nMatched = 0;
|
||||||
|
|
||||||
|
sandbox.spy(cronLib, 'recoverCron');
|
||||||
|
|
||||||
|
sandbox.stub(User, 'update')
|
||||||
|
.withArgs({ _id: user._id, _cronSignature: 'NOT_RUNNING' })
|
||||||
|
.returns({
|
||||||
|
exec () {
|
||||||
|
return Promise.resolve(updatedUser);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
cronMiddleware(req, res, () => {
|
||||||
|
expect(cronLib.recoverCron).to.be.calledOnce;
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -117,9 +117,6 @@ api.getGroups = {
|
|||||||
api.getGroup = {
|
api.getGroup = {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/groups/:groupId',
|
url: '/groups/:groupId',
|
||||||
// Disable cron when getting groups to avoid race conditions when the site is loaded
|
|
||||||
// and requests for party and user data are concurrent
|
|
||||||
runCron: false,
|
|
||||||
middlewares: [authWithHeaders()],
|
middlewares: [authWithHeaders()],
|
||||||
async handler (req, res) {
|
async handler (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
import Bluebird from 'bluebird';
|
||||||
|
import { model as User } from '../../models/user';
|
||||||
import common from '../../../../common/';
|
import common from '../../../../common/';
|
||||||
import { preenUserHistory } from '../../libs/api-v3/preening';
|
import { preenUserHistory } from '../../libs/api-v3/preening';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
@@ -81,12 +83,33 @@ function performSleepTasks (user, tasksByType, now) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function recoverCron (status, locals) {
|
||||||
|
let {user} = locals;
|
||||||
|
|
||||||
|
await Bluebird.delay(300);
|
||||||
|
|
||||||
|
let reloadedUser = await User.findOne({_id: user._id}).exec();
|
||||||
|
|
||||||
|
if (!reloadedUser) {
|
||||||
|
throw new Error(`User ${user._id} not found while recovering.`);
|
||||||
|
} else if (reloadedUser._cronSignature !== 'NOT_RUNNING') {
|
||||||
|
status.times++;
|
||||||
|
|
||||||
|
if (status.times < 4) {
|
||||||
|
await recoverCron(status, locals);
|
||||||
|
} else {
|
||||||
|
throw new Error(`Impossible to recover from cron for user ${user._id}.`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
locals.user = reloadedUser;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Perform various beginning-of-day reset actions.
|
// Perform various beginning-of-day reset actions.
|
||||||
export function cron (options = {}) {
|
export function cron (options = {}) {
|
||||||
let {user, tasksByType, analytics, now = new Date(), daysMissed, timezoneOffsetFromUserPrefs} = options;
|
let {user, tasksByType, analytics, now = new Date(), daysMissed, timezoneOffsetFromUserPrefs} = options;
|
||||||
|
|
||||||
user.auth.timestamps.loggedin = now;
|
|
||||||
user.lastCron = now;
|
|
||||||
user.preferences.timezoneOffsetAtLastCron = timezoneOffsetFromUserPrefs;
|
user.preferences.timezoneOffsetAtLastCron = timezoneOffsetFromUserPrefs;
|
||||||
// User is only allowed a certain number of drops a day. This resets the count.
|
// 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;
|
if (user.items.lastDrop.count > 0) user.items.lastDrop.count = 0;
|
||||||
|
|||||||
@@ -5,108 +5,128 @@ import * as Tasks from '../../models/task';
|
|||||||
import Bluebird from 'bluebird';
|
import Bluebird from 'bluebird';
|
||||||
import { model as Group } from '../../models/group';
|
import { model as Group } from '../../models/group';
|
||||||
import { model as User } from '../../models/user';
|
import { model as User } from '../../models/user';
|
||||||
import { cron } from '../../libs/api-v3/cron';
|
import { recoverCron, cron } from '../../libs/api-v3/cron';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
const daysSince = common.daysSince;
|
const daysSince = common.daysSince;
|
||||||
|
|
||||||
module.exports = function cronMiddleware (req, res, next) {
|
async function cronAsync (req, res) {
|
||||||
let user = res.locals.user;
|
let user = res.locals.user;
|
||||||
|
if (!user) return null; // User might not be available when authentication is not mandatory
|
||||||
if (!user) return next(); // User might not be available when authentication is not mandatory
|
|
||||||
|
|
||||||
let analytics = res.analytics;
|
let analytics = res.analytics;
|
||||||
|
|
||||||
let now = new Date();
|
let now = new Date();
|
||||||
|
|
||||||
// If the user's timezone has changed (due to travel or daylight savings),
|
try {
|
||||||
// cron can be triggered twice in one day, so we check for that and use
|
// If the user's timezone has changed (due to travel or daylight savings),
|
||||||
// both timezones to work out if cron should run.
|
// cron can be triggered twice in one day, so we check for that and use
|
||||||
// CDS = Custom Day Start time.
|
// both timezones to work out if cron should run.
|
||||||
let timezoneOffsetFromUserPrefs = user.preferences.timezoneOffset || 0;
|
// CDS = Custom Day Start time.
|
||||||
let timezoneOffsetAtLastCron = _.isFinite(user.preferences.timezoneOffsetAtLastCron) ? user.preferences.timezoneOffsetAtLastCron : timezoneOffsetFromUserPrefs;
|
let timezoneOffsetFromUserPrefs = user.preferences.timezoneOffset;
|
||||||
let timezoneOffsetFromBrowser = Number(req.header('x-user-timezoneoffset'));
|
let timezoneOffsetAtLastCron = _.isFinite(user.preferences.timezoneOffsetAtLastCron) ? user.preferences.timezoneOffsetAtLastCron : timezoneOffsetFromUserPrefs;
|
||||||
timezoneOffsetFromBrowser = _.isFinite(timezoneOffsetFromBrowser) ? timezoneOffsetFromBrowser : timezoneOffsetFromUserPrefs;
|
let timezoneOffsetFromBrowser = Number(req.header('x-user-timezoneoffset'));
|
||||||
// NB: All timezone offsets can be 0, so can't use `... || ...` to apply non-zero defaults
|
timezoneOffsetFromBrowser = _.isFinite(timezoneOffsetFromBrowser) ? timezoneOffsetFromBrowser : timezoneOffsetFromUserPrefs;
|
||||||
|
// NB: All timezone offsets can be 0, so can't use `... || ...` to apply non-zero defaults
|
||||||
|
|
||||||
if (timezoneOffsetFromBrowser !== timezoneOffsetFromUserPrefs) {
|
if (timezoneOffsetFromBrowser !== timezoneOffsetFromUserPrefs) {
|
||||||
// The user's browser has just told Habitica that the user's timezone has
|
// The user's browser has just told Habitica that the user's timezone has
|
||||||
// changed so store and use the new zone.
|
// changed so store and use the new zone.
|
||||||
user.preferences.timezoneOffset = timezoneOffsetFromBrowser;
|
user.preferences.timezoneOffset = timezoneOffsetFromBrowser;
|
||||||
timezoneOffsetFromUserPrefs = timezoneOffsetFromBrowser;
|
timezoneOffsetFromUserPrefs = timezoneOffsetFromBrowser;
|
||||||
}
|
|
||||||
|
|
||||||
// How many days have we missed using the user's current timezone:
|
|
||||||
let daysMissed = daysSince(user.lastCron, _.defaults({now}, user.preferences));
|
|
||||||
|
|
||||||
if (timezoneOffsetAtLastCron !== timezoneOffsetFromUserPrefs) {
|
|
||||||
// Since cron last ran, the user's timezone has changed.
|
|
||||||
// How many days have we missed using the old timezone:
|
|
||||||
let daysMissedNewZone = daysMissed;
|
|
||||||
let daysMissedOldZone = daysSince(user.lastCron, _.defaults({
|
|
||||||
now,
|
|
||||||
timezoneOffsetOverride: timezoneOffsetAtLastCron,
|
|
||||||
}, user.preferences));
|
|
||||||
|
|
||||||
if (timezoneOffsetAtLastCron < timezoneOffsetFromUserPrefs) {
|
|
||||||
// The timezone change was in the unsafe direction.
|
|
||||||
// E.g., timezone changes from UTC+1 (offset -60) to UTC+0 (offset 0).
|
|
||||||
// or timezone changes from UTC-4 (offset 240) to UTC-5 (offset 300).
|
|
||||||
// Local time changed from, for example, 03:00 to 02:00.
|
|
||||||
|
|
||||||
if (daysMissedOldZone > 0 && daysMissedNewZone > 0) {
|
|
||||||
// Both old and new timezones indicate that we SHOULD run cron, so
|
|
||||||
// it is safe to do so immediately.
|
|
||||||
daysMissed = Math.min(daysMissedOldZone, daysMissedNewZone);
|
|
||||||
// use minimum value to be nice to user
|
|
||||||
} else if (daysMissedOldZone > 0) {
|
|
||||||
// The old timezone says that cron should run; the new timezone does not.
|
|
||||||
// This should be impossible for this direction of timezone change, but
|
|
||||||
// just in case I'm wrong...
|
|
||||||
// TODO
|
|
||||||
// console.log("zone has changed - old zone says run cron, NEW zone says no - stop cron now only -- SHOULD NOT HAVE GOT TO HERE", timezoneOffsetAtLastCron, timezoneOffsetFromUserPrefs, now); // used in production for confirming this never happens
|
|
||||||
} else if (daysMissedNewZone > 0) {
|
|
||||||
// The old timezone says that cron should NOT run -- i.e., cron has
|
|
||||||
// already run today, from the old timezone's point of view.
|
|
||||||
// The new timezone says that cron SHOULD run, but this is almost
|
|
||||||
// certainly incorrect.
|
|
||||||
// This happens when cron occurred at a time soon after the CDS. When
|
|
||||||
// you reinterpret that time in the new timezone, it looks like it
|
|
||||||
// was before the CDS, because local time has stepped backwards.
|
|
||||||
// To fix this, rewrite the cron time to a time that the new
|
|
||||||
// timezone interprets as being in today.
|
|
||||||
|
|
||||||
daysMissed = 0; // prevent cron running now
|
|
||||||
let timezoneOffsetDiff = timezoneOffsetAtLastCron - timezoneOffsetFromUserPrefs;
|
|
||||||
// e.g., for dangerous zone change: 240 - 300 = -60 or -660 - -600 = -60
|
|
||||||
|
|
||||||
user.lastCron = moment(user.lastCron).subtract(timezoneOffsetDiff, 'minutes');
|
|
||||||
// NB: We don't change user.auth.timestamps.loggedin so that will still record the time that the previous cron actually ran.
|
|
||||||
// From now on we can ignore the old timezone:
|
|
||||||
user.preferences.timezoneOffsetAtLastCron = timezoneOffsetFromUserPrefs;
|
|
||||||
} else {
|
|
||||||
// Both old and new timezones indicate that cron should
|
|
||||||
// NOT run.
|
|
||||||
daysMissed = 0; // prevent cron running now
|
|
||||||
}
|
|
||||||
} else if (timezoneOffsetAtLastCron > timezoneOffsetFromUserPrefs) {
|
|
||||||
daysMissed = daysMissedNewZone;
|
|
||||||
// TODO: Either confirm that there is nothing that could possibly go wrong here and remove the need for this else branch, or fix stuff.
|
|
||||||
// There are probably situations where the Dailies do not reset early enough for a user who was expecting the zone change and wants to use all their Dailies immediately in the new zone;
|
|
||||||
// if so, we should provide an option for easy reset of Dailies (can't be automatic because there will be other situations where the user was not prepared).
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (daysMissed <= 0) return next();
|
// How many days have we missed using the user's current timezone:
|
||||||
|
let daysMissed = daysSince(user.lastCron, _.defaults({now}, user.preferences));
|
||||||
|
|
||||||
|
if (timezoneOffsetAtLastCron !== timezoneOffsetFromUserPrefs) {
|
||||||
|
// Since cron last ran, the user's timezone has changed.
|
||||||
|
// How many days have we missed using the old timezone:
|
||||||
|
let daysMissedNewZone = daysMissed;
|
||||||
|
let daysMissedOldZone = daysSince(user.lastCron, _.defaults({
|
||||||
|
now,
|
||||||
|
timezoneOffsetOverride: timezoneOffsetAtLastCron,
|
||||||
|
}, user.preferences));
|
||||||
|
|
||||||
|
if (timezoneOffsetAtLastCron < timezoneOffsetFromUserPrefs) {
|
||||||
|
// The timezone change was in the unsafe direction.
|
||||||
|
// E.g., timezone changes from UTC+1 (offset -60) to UTC+0 (offset 0).
|
||||||
|
// or timezone changes from UTC-4 (offset 240) to UTC-5 (offset 300).
|
||||||
|
// Local time changed from, for example, 03:00 to 02:00.
|
||||||
|
|
||||||
|
if (daysMissedOldZone > 0 && daysMissedNewZone > 0) {
|
||||||
|
// Both old and new timezones indicate that we SHOULD run cron, so
|
||||||
|
// it is safe to do so immediately.
|
||||||
|
daysMissed = Math.min(daysMissedOldZone, daysMissedNewZone);
|
||||||
|
// use minimum value to be nice to user
|
||||||
|
} else if (daysMissedOldZone > 0) {
|
||||||
|
// The old timezone says that cron should run; the new timezone does not.
|
||||||
|
// This should be impossible for this direction of timezone change, but
|
||||||
|
// just in case I'm wrong...
|
||||||
|
// TODO
|
||||||
|
// console.log("zone has changed - old zone says run cron, NEW zone says no - stop cron now only -- SHOULD NOT HAVE GOT TO HERE", timezoneOffsetAtLastCron, timezoneOffsetFromUserPrefs, now); // used in production for confirming this never happens
|
||||||
|
} else if (daysMissedNewZone > 0) {
|
||||||
|
// The old timezone says that cron should NOT run -- i.e., cron has
|
||||||
|
// already run today, from the old timezone's point of view.
|
||||||
|
// The new timezone says that cron SHOULD run, but this is almost
|
||||||
|
// certainly incorrect.
|
||||||
|
// This happens when cron occurred at a time soon after the CDS. When
|
||||||
|
// you reinterpret that time in the new timezone, it looks like it
|
||||||
|
// was before the CDS, because local time has stepped backwards.
|
||||||
|
// To fix this, rewrite the cron time to a time that the new
|
||||||
|
// timezone interprets as being in today.
|
||||||
|
|
||||||
|
daysMissed = 0; // prevent cron running now
|
||||||
|
let timezoneOffsetDiff = timezoneOffsetAtLastCron - timezoneOffsetFromUserPrefs;
|
||||||
|
// e.g., for dangerous zone change: 240 - 300 = -60 or -660 - -600 = -60
|
||||||
|
|
||||||
|
user.lastCron = moment(user.lastCron).subtract(timezoneOffsetDiff, 'minutes');
|
||||||
|
// NB: We don't change user.auth.timestamps.loggedin so that will still record the time that the previous cron actually ran.
|
||||||
|
// From now on we can ignore the old timezone:
|
||||||
|
user.preferences.timezoneOffsetAtLastCron = timezoneOffsetFromUserPrefs;
|
||||||
|
} else {
|
||||||
|
// Both old and new timezones indicate that cron should
|
||||||
|
// NOT run.
|
||||||
|
daysMissed = 0; // prevent cron running now
|
||||||
|
}
|
||||||
|
} else if (timezoneOffsetAtLastCron > timezoneOffsetFromUserPrefs) {
|
||||||
|
daysMissed = daysMissedNewZone;
|
||||||
|
// TODO: Either confirm that there is nothing that could possibly go wrong here and remove the need for this else branch, or fix stuff.
|
||||||
|
// There are probably situations where the Dailies do not reset early enough for a user who was expecting the zone change and wants to use all their Dailies immediately in the new zone;
|
||||||
|
// if so, we should provide an option for easy reset of Dailies (can't be automatic because there will be other situations where the user was not prepared).
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (daysMissed <= 0) {
|
||||||
|
if (user.isModified()) await user.save();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let _cronSignature = uuid();
|
||||||
|
|
||||||
|
// To avoid double cron we first set _cronSignature to now and then check that it's not changed while processing
|
||||||
|
let userUpdateResult = await User.update({
|
||||||
|
_id: user._id,
|
||||||
|
_cronSignature: 'NOT_RUNNING', // Check that in the meantime another cron has not started
|
||||||
|
}, {
|
||||||
|
$set: {
|
||||||
|
_cronSignature,
|
||||||
|
},
|
||||||
|
}).exec();
|
||||||
|
|
||||||
|
// If the cron signature is already set, cron is running in another request
|
||||||
|
// throw an error and recover later,
|
||||||
|
if (userUpdateResult.nMatched === 0 || userUpdateResult.nModified === 0) {
|
||||||
|
throw new Error('CRON_ALREADY_RUNNING');
|
||||||
|
}
|
||||||
|
|
||||||
|
let tasks = await Tasks.Task.find({
|
||||||
|
userId: user._id,
|
||||||
|
$or: [ // Exclude completed todos
|
||||||
|
{type: 'todo', completed: false},
|
||||||
|
{type: {$in: ['habit', 'daily', 'reward']}},
|
||||||
|
],
|
||||||
|
}).exec();
|
||||||
|
|
||||||
// Fetch active tasks (no completed todos)
|
|
||||||
Tasks.Task.find({
|
|
||||||
userId: user._id,
|
|
||||||
$or: [ // Exclude completed todos
|
|
||||||
{type: 'todo', completed: false},
|
|
||||||
{type: {$in: ['habit', 'daily', 'reward']}},
|
|
||||||
],
|
|
||||||
}).exec()
|
|
||||||
.then(tasks => {
|
|
||||||
let tasksByType = {habits: [], dailys: [], todos: [], rewards: []};
|
let tasksByType = {habits: [], dailys: [], todos: [], rewards: []};
|
||||||
tasks.forEach(task => tasksByType[`${task.type}s`].push(task));
|
tasks.forEach(task => tasksByType[`${task.type}s`].push(task));
|
||||||
|
|
||||||
@@ -125,11 +145,7 @@ module.exports = function cronMiddleware (req, res, next) {
|
|||||||
'challenge.id': {$exists: false},
|
'challenge.id': {$exists: false},
|
||||||
}).exec();
|
}).exec();
|
||||||
|
|
||||||
let ranCron = user.isModified();
|
res.locals.wasModified = true; // TODO remove after v2 is retired
|
||||||
let quest = common.content.quests[user.party.quest.key];
|
|
||||||
|
|
||||||
if (ranCron) res.locals.wasModified = true; // TODO remove after v2 is retired
|
|
||||||
if (!ranCron) return next();
|
|
||||||
|
|
||||||
// Group.tavernBoss(user, progress);
|
// Group.tavernBoss(user, progress);
|
||||||
|
|
||||||
@@ -138,23 +154,65 @@ module.exports = function cronMiddleware (req, res, next) {
|
|||||||
tasks.forEach(task => {
|
tasks.forEach(task => {
|
||||||
if (task.isModified()) toSave.push(task.save());
|
if (task.isModified()) toSave.push(task.save());
|
||||||
});
|
});
|
||||||
|
await Bluebird.all(toSave);
|
||||||
|
|
||||||
return Bluebird.all(toSave)
|
let quest = common.content.quests[user.party.quest.key];
|
||||||
.then(saved => {
|
|
||||||
user = res.locals.user = saved[0];
|
if (quest) {
|
||||||
if (!quest) return;
|
|
||||||
// If user is on a quest, roll for boss & player, or handle collections
|
// If user is on a quest, roll for boss & player, or handle collections
|
||||||
let questType = quest.boss ? 'boss' : 'collect';
|
let questType = quest.boss ? 'boss' : 'collect';
|
||||||
// TODO this saves user, runs db updates, loads user. Is there a better way to handle this?
|
await Group[`${questType}Quest`](user, progress);
|
||||||
return Group[`${questType}Quest`](user, progress)
|
}
|
||||||
.then(() => User.findById(user._id).exec()) // fetch the updated user...
|
|
||||||
.then(updatedUser => {
|
|
||||||
res.locals.user = updatedUser;
|
|
||||||
|
|
||||||
return null;
|
// Set _cronSignature, lastCron and auth.timestamps.loggedin to signal end of cron
|
||||||
});
|
await User.update({
|
||||||
|
_id: user._id,
|
||||||
|
}, {
|
||||||
|
$set: {
|
||||||
|
_cronSignature: 'NOT_RUNNING',
|
||||||
|
lastCron: now,
|
||||||
|
'auth.timestamps.loggedin': now,
|
||||||
|
},
|
||||||
|
}).exec();
|
||||||
|
|
||||||
|
// Reload user
|
||||||
|
res.locals.user = await User.findOne({_id: user._id}).exec();
|
||||||
|
return null;
|
||||||
|
} catch (err) {
|
||||||
|
// If cron was aborted for a race condition try to recover from it
|
||||||
|
if (err.message === 'CRON_ALREADY_RUNNING') {
|
||||||
|
// Recovering after abort, wait 300ms and reload user
|
||||||
|
// do it for max 4 times then reset _cronSignature so that it doesn't prevent cron from running
|
||||||
|
// at the next request
|
||||||
|
let recoveryStatus = {
|
||||||
|
times: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
recoverCron(recoveryStatus, res.locals);
|
||||||
|
} else {
|
||||||
|
// For any other error make sure to reset _cronSignature so that it doesn't prevent cron from running
|
||||||
|
// at the next request
|
||||||
|
try {
|
||||||
|
await User.update({
|
||||||
|
_id: user._id,
|
||||||
|
}, {
|
||||||
|
_cronSignature: 'NOT_RUNNING',
|
||||||
|
}).exec();
|
||||||
|
|
||||||
|
throw err; // re-throw original error
|
||||||
|
} catch (secondErr) {
|
||||||
|
throw secondErr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function cronMiddleware (req, res, next) {
|
||||||
|
cronAsync(req, res)
|
||||||
|
.then(() => {
|
||||||
|
next();
|
||||||
})
|
})
|
||||||
.then(() => next())
|
.catch(err => {
|
||||||
.catch(next);
|
next(err);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ const Schema = mongoose.Schema;
|
|||||||
export const INVITES_LIMIT = 100;
|
export const INVITES_LIMIT = 100;
|
||||||
export const TAVERN_ID = shared.TAVERN_ID;
|
export const TAVERN_ID = shared.TAVERN_ID;
|
||||||
|
|
||||||
|
const CRON_SAFE_MODE = nconf.get('CRON_SAFE_MODE') === 'true';
|
||||||
|
const CRON_SEMI_SAFE_MODE = nconf.get('CRON_SEMI_SAFE_MODE') === 'true';
|
||||||
|
|
||||||
// NOTE once Firebase is enabled any change to groups' members in MongoDB will have to be run through the API
|
// NOTE once Firebase is enabled any change to groups' members in MongoDB will have to be run through the API
|
||||||
// changes made directly to the db will cause Firebase to get out of sync
|
// changes made directly to the db will cause Firebase to get out of sync
|
||||||
export let schema = new Schema({
|
export let schema = new Schema({
|
||||||
@@ -281,7 +284,7 @@ schema.methods.sendChat = function sendChat (message, user) {
|
|||||||
this.chat.splice(200);
|
this.chat.splice(200);
|
||||||
|
|
||||||
// Kick off chat notifications in the background.
|
// Kick off chat notifications in the background.
|
||||||
let lastSeenUpdate = {$set: {}, $inc: {_v: 1}};
|
let lastSeenUpdate = {$set: {}};
|
||||||
lastSeenUpdate.$set[`newMessages.${this._id}`] = {name: this.name, value: true};
|
lastSeenUpdate.$set[`newMessages.${this._id}`] = {name: this.name, value: true};
|
||||||
|
|
||||||
// do not send notifications for guilds with more than 5000 users and for the tavern
|
// do not send notifications for guilds with more than 5000 users and for the tavern
|
||||||
@@ -431,7 +434,6 @@ schema.methods.finishQuest = function finishQuest (quest) {
|
|||||||
updates.$inc[`achievements.quests.${questK}`] = 1;
|
updates.$inc[`achievements.quests.${questK}`] = 1;
|
||||||
updates.$inc['stats.gp'] = Number(quest.drop.gp);
|
updates.$inc['stats.gp'] = Number(quest.drop.gp);
|
||||||
updates.$inc['stats.exp'] = Number(quest.drop.exp);
|
updates.$inc['stats.exp'] = Number(quest.drop.exp);
|
||||||
updates.$inc._v = 1;
|
|
||||||
|
|
||||||
if (this._id === TAVERN_ID) {
|
if (this._id === TAVERN_ID) {
|
||||||
updates.$set['party.quest.completed'] = questK; // Just show the notif
|
updates.$set['party.quest.completed'] = questK; // Just show the notif
|
||||||
@@ -498,11 +500,11 @@ schema.statics.collectQuest = async function collectQuest (user, progress) {
|
|||||||
// Still needs completing
|
// Still needs completing
|
||||||
if (_.find(shared.content.quests[group.quest.key].collect, (v, k) => {
|
if (_.find(shared.content.quests[group.quest.key].collect, (v, k) => {
|
||||||
return group.quest.progress.collect[k] < v.count;
|
return group.quest.progress.collect[k] < v.count;
|
||||||
})) return group.save();
|
})) return await group.save();
|
||||||
|
|
||||||
await group.finishQuest(quest);
|
await group.finishQuest(quest);
|
||||||
group.sendChat('`All items found! Party has received their rewards.`');
|
group.sendChat('`All items found! Party has received their rewards.`');
|
||||||
return group.save();
|
return await group.save();
|
||||||
};
|
};
|
||||||
|
|
||||||
schema.statics.bossQuest = async function bossQuest (user, progress) {
|
schema.statics.bossQuest = async function bossQuest (user, progress) {
|
||||||
@@ -517,7 +519,7 @@ schema.statics.bossQuest = async function bossQuest (user, progress) {
|
|||||||
group.quest.progress.hp -= progress.up;
|
group.quest.progress.hp -= progress.up;
|
||||||
// TODO Create a party preferred language option so emits like this can be localized. Suggestion: Always display the English version too. Or, if English is not displayed to the players, at least include it in a new field in the chat object that's visible in the database - essential for admins when troubleshooting quests!
|
// TODO Create a party preferred language option so emits like this can be localized. Suggestion: Always display the English version too. Or, if English is not displayed to the players, at least include it in a new field in the chat object that's visible in the database - essential for admins when troubleshooting quests!
|
||||||
let playerAttack = `${user.profile.name} attacks ${quest.boss.name('en')} for ${progress.up.toFixed(1)} damage.`;
|
let playerAttack = `${user.profile.name} attacks ${quest.boss.name('en')} for ${progress.up.toFixed(1)} damage.`;
|
||||||
let bossAttack = nconf.get('CRON_SAFE_MODE') === 'true' || nconf.get('CRON_SEMI_SAFE_MODE') === 'true' ? `${quest.boss.name('en')} does not attack, because it respects the fact that there are some bugs\` \`post-maintenance and it doesn't want to hurt anyone unfairly. It will continue its rampage soon!` : `${quest.boss.name('en')} attacks party for ${Math.abs(down).toFixed(1)} damage.`;
|
let bossAttack = CRON_SAFE_MODE || CRON_SEMI_SAFE_MODE ? `${quest.boss.name('en')} does not attack, because it respects the fact that there are some bugs\` \`post-maintenance and it doesn't want to hurt anyone unfairly. It will continue its rampage soon!` : `${quest.boss.name('en')} attacks party for ${Math.abs(down).toFixed(1)} damage.`;
|
||||||
// TODO Consider putting the safe mode boss attack message in an ENV var
|
// TODO Consider putting the safe mode boss attack message in an ENV var
|
||||||
group.sendChat(`\`${playerAttack}\` \`${bossAttack}\``);
|
group.sendChat(`\`${playerAttack}\` \`${bossAttack}\``);
|
||||||
|
|
||||||
@@ -538,7 +540,7 @@ schema.statics.bossQuest = async function bossQuest (user, progress) {
|
|||||||
await User.update({
|
await User.update({
|
||||||
_id: {$in: _.keys(group.quest.members)},
|
_id: {$in: _.keys(group.quest.members)},
|
||||||
}, {
|
}, {
|
||||||
$inc: {'stats.hp': down, _v: 1},
|
$inc: {'stats.hp': down},
|
||||||
}, {multi: true}).exec();
|
}, {multi: true}).exec();
|
||||||
// Apply changes the currently cronning user locally so we don't have to reload it to get the updated state
|
// Apply changes the currently cronning user locally so we don't have to reload it to get the updated state
|
||||||
// TODO how to mark not modified? https://github.com/Automattic/mongoose/pull/1167
|
// TODO how to mark not modified? https://github.com/Automattic/mongoose/pull/1167
|
||||||
@@ -552,10 +554,9 @@ schema.statics.bossQuest = async function bossQuest (user, progress) {
|
|||||||
|
|
||||||
// Participants: Grant rewards & achievements, finish quest
|
// Participants: Grant rewards & achievements, finish quest
|
||||||
await group.finishQuest(shared.content.quests[group.quest.key]);
|
await group.finishQuest(shared.content.quests[group.quest.key]);
|
||||||
return group.save();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return group.save();
|
return await group.save();
|
||||||
};
|
};
|
||||||
|
|
||||||
// to set a boss: `db.groups.update({_id:TAVERN_ID},{$set:{quest:{key:'dilatory',active:true,progress:{hp:1000,rage:1500}}}})`
|
// to set a boss: `db.groups.update({_id:TAVERN_ID},{$set:{quest:{key:'dilatory',active:true,progress:{hp:1000,rage:1500}}}})`
|
||||||
|
|||||||
@@ -345,6 +345,7 @@ export let schema = new Schema({
|
|||||||
},
|
},
|
||||||
|
|
||||||
lastCron: {type: Date, default: Date.now},
|
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
|
// {GROUP_ID: Boolean}, represents whether they have unseen chat messages
|
||||||
newMessages: {type: Schema.Types.Mixed, default: () => {
|
newMessages: {type: Schema.Types.Mixed, default: () => {
|
||||||
@@ -528,7 +529,7 @@ export let schema = new Schema({
|
|||||||
schema.plugin(baseModel, {
|
schema.plugin(baseModel, {
|
||||||
// noSet is not used as updating uses a whitelist and creating only accepts specific params (password, email, username, ...)
|
// noSet is not used as updating uses a whitelist and creating only accepts specific params (password, email, username, ...)
|
||||||
noSet: [],
|
noSet: [],
|
||||||
private: ['auth.local.hashed_password', 'auth.local.salt'],
|
private: ['auth.local.hashed_password', 'auth.local.salt', '_cronSignature'],
|
||||||
toJSONTransform: function userToJSON (plainObj, originalDoc) {
|
toJSONTransform: function userToJSON (plainObj, originalDoc) {
|
||||||
// plainObj.filters = {}; // TODO Not saved, remove?
|
// plainObj.filters = {}; // TODO Not saved, remove?
|
||||||
plainObj._tmp = originalDoc._tmp; // be sure to send down drop notifs
|
plainObj._tmp = originalDoc._tmp; // be sure to send down drop notifs
|
||||||
|
|||||||
Reference in New Issue
Block a user