Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d89b1e3b08 | |||
| 8520987afb | |||
| c327baad41 | |||
| bbafe3d52d | |||
| d23f79d001 | |||
| db3fd62af8 | |||
| 00aebed8c0 | |||
| 4ad81c4c28 | |||
| 36042d42f9 | |||
| 7090ed5f4b | |||
| 2d81b31c3a | |||
| 01a7880bc9 | |||
| 20ce394999 | |||
| 3ce80bae2a | |||
| 4deb5ada2f | |||
| 39a9753fd6 | |||
| a9d4bbd87d | |||
| c08df9978f | |||
| 74087d23c9 | |||
| b40d8ce09d | |||
| 91aba965b0 | |||
| 9b7458c022 | |||
| 31d2c5d604 | |||
| cb4676980d | |||
| 10cf22cd4e | |||
| c6192dd24b | |||
| 792aafd737 | |||
| 0ca44fd5c2 | |||
| 457264faa1 | |||
| 3c338ddcd7 | |||
| 8292903444 | |||
| 275d29e3f4 | |||
| 9b529fff52 | |||
| f9762f4f81 | |||
| ed815d4947 | |||
| 31850830a0 | |||
| 1a26965542 |
@@ -1,4 +1,4 @@
|
||||
[//]: # (Before logging this issue, look through common problems at https://github.com/HabitRPG/habitrpg/issue If you find your issue there, read at least the first post to see if there is a workaround for you)
|
||||
[//]: # (Before logging this issue, look through common problems at https://github.com/HabitRPG/habitrpg/issues If you find your issue there, read at least the first post to see if there is a workaround for you)
|
||||
|
||||
[//]: # (Github is primarily used for reporting bugs. If you have a feature request, use "Help > Request a Feature" so that the feature request can be vetted by the larger Habitica community)
|
||||
|
||||
|
||||
+1
-1
@@ -25,7 +25,7 @@ RUN apt-get clean
|
||||
RUN rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install global packages
|
||||
RUN npm install -g gulp grunt-cli bower
|
||||
RUN npm install -g gulp grunt-cli bower npm@3
|
||||
|
||||
# Clone Habitica repo and install dependencies
|
||||
WORKDIR /habitrpg
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ VAGRANTFILE_API_VERSION = "2"
|
||||
|
||||
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
|
||||
config.vm.provider "virtualbox" do |v|
|
||||
v.memory = 768
|
||||
v.memory = 4096
|
||||
v.cpus = 1
|
||||
v.customize ["setextradata", :id, "VBoxInternal2/SharedFoldersEnableSymlinksCreate/vagrant", "1"]
|
||||
end
|
||||
|
||||
@@ -177,6 +177,7 @@
|
||||
"inviteMissingEmail": "Missing email address in invite.",
|
||||
"partyMustbePrivate": "Parties must be private",
|
||||
"userAlreadyInGroup": "User already in that group.",
|
||||
"cannotInviteSelfToGroup": "You cannot invite yourself to a group.",
|
||||
"userAlreadyInvitedToGroup": "User already invited to that group.",
|
||||
"userAlreadyPendingInvitation": "User already pending invitation.",
|
||||
"userAlreadyInAParty": "User already in a party.",
|
||||
|
||||
+2
-7
@@ -61,13 +61,8 @@
|
||||
"client_secret":"client_secret"
|
||||
},
|
||||
"IAP_GOOGLE_KEYDIR": "/path/to/google/public/key/dir/",
|
||||
"LOGGLY": {
|
||||
"enabled": false,
|
||||
"subdomain": "subdomain",
|
||||
"token": "token",
|
||||
"username": "username",
|
||||
"password": "password"
|
||||
},
|
||||
"LOGGLY_TOKEN": "token",
|
||||
"LOGGLY_ACCOUNT": "account",
|
||||
"PUSH_CONFIGS": {
|
||||
"GCM_SERVER_API_KEY": "",
|
||||
"APN_PEM_FILES": {
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
var uuid = require('uuid').v4;
|
||||
var mongo = require('mongodb').MongoClient;
|
||||
var _ = require('lodash');
|
||||
|
||||
var taskIds = require('checklists-no-id.json').map(function (obj) {
|
||||
return obj._id;
|
||||
});
|
||||
|
||||
// Fix empty task.checklistt.id
|
||||
|
||||
var progressCount = 100;
|
||||
var count = 0;
|
||||
|
||||
function displayData() {
|
||||
console.warn('\n' + count + ' tasks processed\n');
|
||||
return exiting(0);
|
||||
}
|
||||
|
||||
function exiting(code, msg) {
|
||||
code = code || 0; // 0 = success
|
||||
|
||||
if (code && !msg) { msg = 'ERROR!'; }
|
||||
if (msg) {
|
||||
if (code) { console.error(msg); }
|
||||
else { console.log( msg); }
|
||||
}
|
||||
}
|
||||
|
||||
mongo.connect('db url')
|
||||
.then(function (db) {
|
||||
var dbTasks = db.collection('tasks');
|
||||
|
||||
// specify a query to limit the affected tasks (empty for all tasks):
|
||||
var query = {
|
||||
'_id':{ $in: taskIds },
|
||||
};
|
||||
|
||||
// specify fields we are interested in to limit retrieved data (empty if we're not reading data):
|
||||
var fields = {
|
||||
'checklist': 1,
|
||||
};
|
||||
|
||||
console.warn('Updating tasks...');
|
||||
|
||||
dbTasks.find(query, fields, {batchSize: 250}).toArray(function(err, tasks) {
|
||||
if (err) { return exiting(1, 'ERROR! ' + err); }
|
||||
|
||||
tasks.forEach(function (task) {
|
||||
var checklist = task.checklist || [];
|
||||
checklist.forEach(function (item) {
|
||||
if (!item.id || item.id === "") {
|
||||
item.id = uuid();
|
||||
}
|
||||
});
|
||||
|
||||
// specify user data to change:
|
||||
var set = {
|
||||
checklist: checklist,
|
||||
};
|
||||
//console.log(set);
|
||||
|
||||
dbTasks.update({_id: task._id}, {$set: set}, function (err, res) {
|
||||
if (err) console.error('Error while updating', err);
|
||||
});
|
||||
|
||||
count++;
|
||||
if (count % progressCount == 0) console.warn(count + ' ' + task._id);
|
||||
});
|
||||
|
||||
if (count === tasks.length) {
|
||||
console.warn('All appropriate tasks found and modified.');
|
||||
return displayData();
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(function (err) {
|
||||
throw err;
|
||||
});
|
||||
+1
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "habitica",
|
||||
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
|
||||
"version": "3.3.2",
|
||||
"version": "3.4.2",
|
||||
"main": "./website/server/index.js",
|
||||
"dependencies": {
|
||||
"accepts": "^1.3.2",
|
||||
@@ -59,7 +59,6 @@
|
||||
"js2xmlparser": "~1.0.0",
|
||||
"lodash": "^3.10.1",
|
||||
"lodash.setwith": "^4.2.0",
|
||||
"loggly": "~1.0.8",
|
||||
"markdown-it": "^6.0.1",
|
||||
"merge-stream": "^1.0.0",
|
||||
"method-override": "^2.3.5",
|
||||
|
||||
@@ -33,6 +33,17 @@ describe('Post /groups/:groupId/invite', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when inviting yourself to a group', async () => {
|
||||
await expect(inviter.post(`/groups/${group._id}/invite`, {
|
||||
uuids: [inviter._id],
|
||||
}))
|
||||
.to.eventually.be.rejected.and.eql({
|
||||
code: 400,
|
||||
error: 'BadRequest',
|
||||
message: t('cannotInviteSelfToGroup'),
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an error when uuids is not an array', async () => {
|
||||
let fakeID = generateUUID();
|
||||
|
||||
|
||||
+5
@@ -3,6 +3,7 @@ import {
|
||||
generateGroup,
|
||||
generateChallenge,
|
||||
} from '../../../../../helpers/api-integration/v3';
|
||||
import Bluebird from 'bluebird';
|
||||
import { find } from 'lodash';
|
||||
|
||||
describe('POST /tasks/:id/score/:direction', () => {
|
||||
@@ -26,6 +27,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
text: 'test habit',
|
||||
type: 'habit',
|
||||
});
|
||||
await Bluebird.delay(1000);
|
||||
let updatedUser = await user.get('/user');
|
||||
usersChallengeTaskId = updatedUser.tasksOrder.habits[0];
|
||||
});
|
||||
@@ -63,6 +65,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
text: 'test daily',
|
||||
type: 'daily',
|
||||
});
|
||||
await Bluebird.delay(1000);
|
||||
let updatedUser = await user.get('/user');
|
||||
usersChallengeTaskId = updatedUser.tasksOrder.dailys[0];
|
||||
});
|
||||
@@ -99,6 +102,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
text: 'test todo',
|
||||
type: 'todo',
|
||||
});
|
||||
await Bluebird.delay(1000);
|
||||
let updatedUser = await user.get('/user');
|
||||
usersChallengeTaskId = updatedUser.tasksOrder.todos[0];
|
||||
});
|
||||
@@ -123,6 +127,7 @@ describe('POST /tasks/:id/score/:direction', () => {
|
||||
text: 'test reward',
|
||||
type: 'reward',
|
||||
});
|
||||
await Bluebird.delay(1000);
|
||||
let updatedUser = await user.get('/user');
|
||||
usersChallengeTaskId = updatedUser.tasksOrder.todos[0];
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ describe('GET /user/anonymized', () => {
|
||||
|
||||
before(async () => {
|
||||
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',
|
||||
webhooks: 'some', 'achievements.challenges': 'some',
|
||||
'inbox.messages': [{ text: 'some text' }],
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable global-require */
|
||||
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 * as Tasks from '../../../../../website/server/models/task';
|
||||
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', () => {
|
||||
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 {
|
||||
generateRes,
|
||||
generateReq,
|
||||
generateNext,
|
||||
generateTodo,
|
||||
generateDaily,
|
||||
} from '../../../../helpers/api-unit.helper';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import cronMiddleware from '../../../../../website/server/middlewares/api-v3/cron';
|
||||
import moment from 'moment';
|
||||
import { model as User } from '../../../../../website/server/models/user';
|
||||
import { model as Group } from '../../../../../website/server/models/group';
|
||||
import * as Tasks from '../../../../../website/server/models/task';
|
||||
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';
|
||||
|
||||
describe('cron middleware', () => {
|
||||
let res, req, next;
|
||||
let res, req;
|
||||
let user;
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach((done) => {
|
||||
res = generateRes();
|
||||
req = generateReq();
|
||||
next = generateNext();
|
||||
user = new User({
|
||||
auth: {
|
||||
local: {
|
||||
@@ -33,24 +33,31 @@ describe('cron middleware', () => {
|
||||
},
|
||||
});
|
||||
|
||||
user._statsComputed = {
|
||||
mp: 10,
|
||||
maxMP: 100,
|
||||
};
|
||||
user.save()
|
||||
.then(savedUser => {
|
||||
savedUser._statsComputed = {
|
||||
mp: 10,
|
||||
maxMP: 100,
|
||||
};
|
||||
|
||||
res.locals.user = user;
|
||||
res.analytics = analyticsService;
|
||||
res.locals.user = savedUser;
|
||||
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;
|
||||
cronMiddleware(req, res, next);
|
||||
expect(next).to.be.calledOnce;
|
||||
cronMiddleware(req, res, done);
|
||||
});
|
||||
|
||||
it('calls next when days have not been missed', () => {
|
||||
cronMiddleware(req, res, next);
|
||||
expect(next).to.be.calledOnce;
|
||||
it('calls next when days have not been missed', (done) => {
|
||||
cronMiddleware(req, res, 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.completed = true;
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
cronMiddleware(req, res, () => {
|
||||
Tasks.Task.findOne({_id: task}, function (err, taskFound) {
|
||||
expect(err).to.not.exist;
|
||||
cronMiddleware(req, res, (err) => {
|
||||
Tasks.Task.findOne({_id: task}, function (secondErr, taskFound) {
|
||||
expect(secondErr).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.dateUpdated = moment('012013', 'MMYYYY');
|
||||
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||
let task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({days: 31});
|
||||
task.completed = true;
|
||||
task.save();
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
cronMiddleware(req, res, () => {
|
||||
Tasks.Task.findOne({_id: task}, function (err, taskFound) {
|
||||
expect(err).to.not.exist;
|
||||
cronMiddleware(req, res, (err) => {
|
||||
Tasks.Task.findOne({_id: task}, function (secondErr, taskFound) {
|
||||
expect(secondErr).to.not.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.dateUpdated = moment('012013', 'MMYYYY');
|
||||
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||
@@ -95,46 +104,60 @@ describe('cron middleware', () => {
|
||||
let task = generateTodo(user);
|
||||
task.dateCompleted = moment(new Date()).subtract({days: 91});
|
||||
task.completed = true;
|
||||
task.save();
|
||||
await task.save();
|
||||
await user.save();
|
||||
|
||||
cronMiddleware(req, res, () => {
|
||||
Tasks.Task.findOne({_id: task}, function (err, taskFound) {
|
||||
expect(err).to.not.exist;
|
||||
cronMiddleware(req, res, (err) => {
|
||||
Tasks.Task.findOne({_id: task}, function (secondErr, taskFound) {
|
||||
expect(secondErr).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;
|
||||
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||
await user.save();
|
||||
|
||||
user.save().then(function () {
|
||||
cronMiddleware(req, res, function () {
|
||||
expect(hpBefore).to.equal(user.stats.hp);
|
||||
done();
|
||||
});
|
||||
cronMiddleware(req, res, (err) => {
|
||||
expect(hpBefore).to.equal(user.stats.hp);
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
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;
|
||||
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||
let daily = generateDaily(user);
|
||||
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);
|
||||
done();
|
||||
done(err);
|
||||
});
|
||||
});
|
||||
|
||||
it('updates tasks', (done) => {
|
||||
it('updates tasks', async (done) => {
|
||||
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||
let todo = generateTodo(user);
|
||||
let todoValueBefore = todo.value;
|
||||
await user.save();
|
||||
|
||||
cronMiddleware(req, res, () => {
|
||||
Tasks.Task.findOne({_id: todo._id}, function (err, todoFound) {
|
||||
@@ -150,7 +173,7 @@ describe('cron middleware', () => {
|
||||
user.lastCron = moment(new Date()).subtract({days: 2});
|
||||
let daily = generateDaily(user);
|
||||
daily.startDate = moment(new Date()).subtract({days: 2});
|
||||
daily.save();
|
||||
await daily.save();
|
||||
|
||||
let questKey = 'dilatory';
|
||||
user.party.quest.key = questKey;
|
||||
@@ -174,4 +197,28 @@ describe('cron middleware', () => {
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -168,6 +168,8 @@ describe('errorHandler', () => {
|
||||
originalUrl: req.originalUrl,
|
||||
headers: req.headers,
|
||||
body: req.body,
|
||||
httpCode: 400,
|
||||
isHandledError: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,7 +16,7 @@ nvm use
|
||||
nvm alias default current
|
||||
|
||||
echo Update npm...
|
||||
npm install -g npm@2
|
||||
npm install -g npm@3
|
||||
|
||||
echo Installing global modules...
|
||||
npm install -g gulp bower grunt-cli mocha
|
||||
|
||||
@@ -117,9 +117,6 @@ api.getGroups = {
|
||||
api.getGroup = {
|
||||
method: 'GET',
|
||||
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()],
|
||||
async handler (req, res) {
|
||||
let user = res.locals.user;
|
||||
@@ -490,6 +487,8 @@ async function _inviteByUUID (uuid, group, inviter, req, res) {
|
||||
|
||||
if (!userToInvite) {
|
||||
throw new NotFound(res.t('userWithIDNotFound', {userId: uuid}));
|
||||
} else if (inviter._id === userToInvite._id) {
|
||||
throw new BadRequest(res.t('cannotInviteSelfToGroup'));
|
||||
}
|
||||
|
||||
if (group.type === 'guild') {
|
||||
|
||||
@@ -34,7 +34,7 @@ api.createTag = {
|
||||
};
|
||||
|
||||
/**
|
||||
* @api {get} /api/v3/tag Get a user's tags
|
||||
* @api {get} /api/v3/tags Get a user's tags
|
||||
* @apiVersion 3.0.0
|
||||
* @apiName GetTags
|
||||
* @apiGroup Tag
|
||||
|
||||
@@ -203,7 +203,8 @@ api.exportUserAvatarPng = {
|
||||
try {
|
||||
response = await got.head(s3url);
|
||||
} catch (gotError) {
|
||||
if (gotError.code !== 'ENOTFOUND' && gotError.statusCode !== 404) {
|
||||
// If the file does not exist AWS S3 can return a 403 error
|
||||
if (gotError.code !== 'ENOTFOUND' && gotError.statusCode !== 404 && gotError.statusCode !== 403) {
|
||||
throw gotError;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,7 @@
|
||||
var nconf = require('nconf');
|
||||
var winston = require('winston');
|
||||
|
||||
var logger, loggly;
|
||||
|
||||
// Currently disabled
|
||||
if (nconf.get('LOGGLY:enabled')){
|
||||
loggly = require('loggly').createClient({
|
||||
token: nconf.get('LOGGLY:token'),
|
||||
subdomain: nconf.get('LOGGLY:subdomain'),
|
||||
auth: {
|
||||
username: nconf.get('LOGGLY:username'),
|
||||
password: nconf.get('LOGGLY:password')
|
||||
},
|
||||
//
|
||||
// Optional: Tag to send with EVERY log message
|
||||
//
|
||||
tags: [('heroku-'+nconf.get('BASE_URL'))],
|
||||
json: true
|
||||
});
|
||||
}
|
||||
var logger;
|
||||
|
||||
if (!logger) {
|
||||
logger = new (winston.Logger)({});
|
||||
@@ -49,9 +32,4 @@ module.exports.warn = function(/* variable args */) {
|
||||
module.exports.error = function(/* variable args */) {
|
||||
if (logger)
|
||||
logger.error.apply(logger, arguments);
|
||||
};
|
||||
|
||||
module.exports.loggly = function(/* variable args */){
|
||||
if (loggly)
|
||||
loggly.log.apply(loggly, arguments);
|
||||
};
|
||||
};
|
||||
@@ -1,4 +1,6 @@
|
||||
import moment from 'moment';
|
||||
import Bluebird from 'bluebird';
|
||||
import { model as User } from '../../models/user';
|
||||
import common from '../../../../common/';
|
||||
import { preenUserHistory } from '../../libs/api-v3/preening';
|
||||
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.
|
||||
export function cron (options = {}) {
|
||||
let {user, tasksByType, analytics, now = new Date(), daysMissed, timezoneOffsetFromUserPrefs} = options;
|
||||
|
||||
user.auth.timestamps.loggedin = now;
|
||||
user.lastCron = now;
|
||||
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;
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
import winston from 'winston';
|
||||
import nconf from 'nconf';
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
CustomError,
|
||||
} from './errors';
|
||||
|
||||
const IS_PROD = nconf.get('IS_PROD');
|
||||
const IS_TEST = nconf.get('IS_TEST');
|
||||
@@ -13,6 +16,7 @@ const logger = new winston.Logger();
|
||||
if (IS_PROD) {
|
||||
if (ENABLE_LOGS_IN_PROD) {
|
||||
logger.add(winston.transports.Console, {
|
||||
timestamp: true,
|
||||
colorize: false,
|
||||
prettyPrint: false,
|
||||
});
|
||||
@@ -20,6 +24,7 @@ if (IS_PROD) {
|
||||
} else if (!IS_TEST || IS_TEST && ENABLE_LOGS_IN_TEST) { // Do not log anything when testing unless specified
|
||||
logger
|
||||
.add(winston.transports.Console, {
|
||||
timestamp: true,
|
||||
colorize: true,
|
||||
prettyPrint: true,
|
||||
});
|
||||
@@ -42,8 +47,28 @@ let loggerInterface = {
|
||||
// pass the error stack as the first parameter to logger.error
|
||||
let stack = err.stack || err.message || err;
|
||||
|
||||
if (_.isPlainObject(errorData) && !errorData.fullError) errorData.fullError = err;
|
||||
logger.error(stack, errorData, ...otherArgs);
|
||||
if (_.isPlainObject(errorData) && !errorData.fullError) {
|
||||
// If the error object has interesting data (not only httpCode, message and name from the CustomError class)
|
||||
// add it to the logs
|
||||
if (err instanceof CustomError) {
|
||||
let errWithoutCommonProps = _.omit(err, ['name', 'httpCode', 'message']);
|
||||
|
||||
if (Object.keys(errWithoutCommonProps).length > 0) {
|
||||
errorData.fullError = errWithoutCommonProps;
|
||||
}
|
||||
} else {
|
||||
errorData.fullError = err;
|
||||
}
|
||||
}
|
||||
|
||||
let loggerArgs = [stack, errorData, ...otherArgs];
|
||||
|
||||
// Treat 4xx errors that are handled as warnings, 5xx and uncaught errors as serious problems
|
||||
if (!errorData || !errorData.isHandledError || errorData.httpCode >= 500) {
|
||||
logger.error(...loggerArgs);
|
||||
} else {
|
||||
logger.warn(...loggerArgs);
|
||||
}
|
||||
} else {
|
||||
logger.error(...args);
|
||||
}
|
||||
|
||||
@@ -11,13 +11,6 @@ module.exports = function(err, req, res, next) {
|
||||
"\n\nbody: " + JSON.stringify(req.body) +
|
||||
(res.locals.ops ? "\n\ncompleted ops: " + JSON.stringify(res.locals.ops) : "");
|
||||
logging.error(stack);
|
||||
/*logging.loggly({
|
||||
error: "Uncaught error",
|
||||
stack: (err.stack || err.message || err),
|
||||
body: req.body, headers: req.header,
|
||||
auth: req.headers['x-api-user'],
|
||||
originalUrl: req.originalUrl
|
||||
});*/
|
||||
var message = err.message ? err.message : err;
|
||||
message = (message.length < 200) ? message : message.substring(0,100) + message.substring(message.length-100,message.length);
|
||||
res.status(500).json({err:message}); //res.end(err.message);
|
||||
|
||||
@@ -5,108 +5,128 @@ import * as Tasks from '../../models/task';
|
||||
import Bluebird from 'bluebird';
|
||||
import { model as Group } from '../../models/group';
|
||||
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;
|
||||
|
||||
module.exports = function cronMiddleware (req, res, next) {
|
||||
async function cronAsync (req, res) {
|
||||
let user = res.locals.user;
|
||||
|
||||
if (!user) return next(); // User might not be available when authentication is not mandatory
|
||||
if (!user) return null; // User might not be available when authentication is not mandatory
|
||||
|
||||
let analytics = res.analytics;
|
||||
|
||||
let now = new Date();
|
||||
|
||||
// If the user's timezone has changed (due to travel or daylight savings),
|
||||
// cron can be triggered twice in one day, so we check for that and use
|
||||
// both timezones to work out if cron should run.
|
||||
// CDS = Custom Day Start time.
|
||||
let timezoneOffsetFromUserPrefs = user.preferences.timezoneOffset || 0;
|
||||
let timezoneOffsetAtLastCron = _.isFinite(user.preferences.timezoneOffsetAtLastCron) ? user.preferences.timezoneOffsetAtLastCron : timezoneOffsetFromUserPrefs;
|
||||
let timezoneOffsetFromBrowser = Number(req.header('x-user-timezoneoffset'));
|
||||
timezoneOffsetFromBrowser = _.isFinite(timezoneOffsetFromBrowser) ? timezoneOffsetFromBrowser : timezoneOffsetFromUserPrefs;
|
||||
// NB: All timezone offsets can be 0, so can't use `... || ...` to apply non-zero defaults
|
||||
try {
|
||||
// If the user's timezone has changed (due to travel or daylight savings),
|
||||
// cron can be triggered twice in one day, so we check for that and use
|
||||
// both timezones to work out if cron should run.
|
||||
// CDS = Custom Day Start time.
|
||||
let timezoneOffsetFromUserPrefs = user.preferences.timezoneOffset;
|
||||
let timezoneOffsetAtLastCron = _.isFinite(user.preferences.timezoneOffsetAtLastCron) ? user.preferences.timezoneOffsetAtLastCron : timezoneOffsetFromUserPrefs;
|
||||
let timezoneOffsetFromBrowser = Number(req.header('x-user-timezoneoffset'));
|
||||
timezoneOffsetFromBrowser = _.isFinite(timezoneOffsetFromBrowser) ? timezoneOffsetFromBrowser : timezoneOffsetFromUserPrefs;
|
||||
// NB: All timezone offsets can be 0, so can't use `... || ...` to apply non-zero defaults
|
||||
|
||||
if (timezoneOffsetFromBrowser !== timezoneOffsetFromUserPrefs) {
|
||||
// The user's browser has just told Habitica that the user's timezone has
|
||||
// changed so store and use the new zone.
|
||||
user.preferences.timezoneOffset = 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 (timezoneOffsetFromBrowser !== timezoneOffsetFromUserPrefs) {
|
||||
// The user's browser has just told Habitica that the user's timezone has
|
||||
// changed so store and use the new zone.
|
||||
user.preferences.timezoneOffset = timezoneOffsetFromBrowser;
|
||||
timezoneOffsetFromUserPrefs = timezoneOffsetFromBrowser;
|
||||
}
|
||||
}
|
||||
|
||||
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: []};
|
||||
tasks.forEach(task => tasksByType[`${task.type}s`].push(task));
|
||||
|
||||
@@ -125,11 +145,7 @@ module.exports = function cronMiddleware (req, res, next) {
|
||||
'challenge.id': {$exists: false},
|
||||
}).exec();
|
||||
|
||||
let ranCron = user.isModified();
|
||||
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();
|
||||
res.locals.wasModified = true; // TODO remove after v2 is retired
|
||||
|
||||
// Group.tavernBoss(user, progress);
|
||||
|
||||
@@ -138,23 +154,65 @@ module.exports = function cronMiddleware (req, res, next) {
|
||||
tasks.forEach(task => {
|
||||
if (task.isModified()) toSave.push(task.save());
|
||||
});
|
||||
await Bluebird.all(toSave);
|
||||
|
||||
return Bluebird.all(toSave)
|
||||
.then(saved => {
|
||||
user = res.locals.user = saved[0];
|
||||
if (!quest) return;
|
||||
let quest = common.content.quests[user.party.quest.key];
|
||||
|
||||
if (quest) {
|
||||
// If user is on a quest, roll for boss & player, or handle collections
|
||||
let questType = quest.boss ? 'boss' : 'collect';
|
||||
// TODO this saves user, runs db updates, loads user. Is there a better way to handle this?
|
||||
return Group[`${questType}Quest`](user, progress)
|
||||
.then(() => User.findById(user._id).exec()) // fetch the updated user...
|
||||
.then(updatedUser => {
|
||||
res.locals.user = updatedUser;
|
||||
await Group[`${questType}Quest`](user, progress);
|
||||
}
|
||||
|
||||
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(next);
|
||||
});
|
||||
.catch(err => {
|
||||
next(err);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -12,12 +12,6 @@ import {
|
||||
} from 'lodash';
|
||||
|
||||
module.exports = function errorHandler (err, req, res, next) { // eslint-disable-line no-unused-vars
|
||||
logger.error(err, {
|
||||
originalUrl: req.originalUrl,
|
||||
headers: omit(req.headers, ['x-api-key']),
|
||||
body: req.body,
|
||||
});
|
||||
|
||||
// In case of a CustomError class, use it's data
|
||||
// Otherwise try to identify the type of error (mongoose validation, mongodb unique, ...)
|
||||
// If we can't identify it, respond with a generic 500 error
|
||||
@@ -70,6 +64,15 @@ module.exports = function errorHandler (err, req, res, next) { // eslint-disable
|
||||
responseErr = new InternalServerError();
|
||||
}
|
||||
|
||||
// log the error
|
||||
logger.error(err, {
|
||||
originalUrl: req.originalUrl,
|
||||
headers: omit(req.headers, ['x-api-key', 'cookie']), // don't send sensitive information that only adds noise
|
||||
body: req.body,
|
||||
httpCode: responseErr.httpCode,
|
||||
isHandledError: responseErr.httpCode < 500,
|
||||
});
|
||||
|
||||
let jsonRes = {
|
||||
success: false,
|
||||
error: responseErr.name,
|
||||
|
||||
@@ -25,6 +25,9 @@ const Schema = mongoose.Schema;
|
||||
export const INVITES_LIMIT = 100;
|
||||
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
|
||||
// changes made directly to the db will cause Firebase to get out of sync
|
||||
export let schema = new Schema({
|
||||
@@ -281,7 +284,7 @@ schema.methods.sendChat = function sendChat (message, user) {
|
||||
this.chat.splice(200);
|
||||
|
||||
// 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};
|
||||
|
||||
// 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['stats.gp'] = Number(quest.drop.gp);
|
||||
updates.$inc['stats.exp'] = Number(quest.drop.exp);
|
||||
updates.$inc._v = 1;
|
||||
|
||||
if (this._id === TAVERN_ID) {
|
||||
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
|
||||
if (_.find(shared.content.quests[group.quest.key].collect, (v, k) => {
|
||||
return group.quest.progress.collect[k] < v.count;
|
||||
})) return group.save();
|
||||
})) return await group.save();
|
||||
|
||||
await group.finishQuest(quest);
|
||||
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) {
|
||||
@@ -517,7 +519,7 @@ schema.statics.bossQuest = async function bossQuest (user, progress) {
|
||||
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!
|
||||
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
|
||||
group.sendChat(`\`${playerAttack}\` \`${bossAttack}\``);
|
||||
|
||||
@@ -538,7 +540,7 @@ schema.statics.bossQuest = async function bossQuest (user, progress) {
|
||||
await User.update({
|
||||
_id: {$in: _.keys(group.quest.members)},
|
||||
}, {
|
||||
$inc: {'stats.hp': down, _v: 1},
|
||||
$inc: {'stats.hp': down},
|
||||
}, {multi: true}).exec();
|
||||
// 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
|
||||
@@ -552,10 +554,9 @@ schema.statics.bossQuest = async function bossQuest (user, progress) {
|
||||
|
||||
// Participants: Grant rewards & achievements, finish quest
|
||||
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}}}})`
|
||||
|
||||
@@ -181,7 +181,7 @@ let dailyTodoSchema = () => {
|
||||
completed: {type: Boolean, default: false},
|
||||
text: {type: String, required: false, default: ''}, // required:false because it can be empty on creation
|
||||
_id: false,
|
||||
id: {type: String, default: shared.uuid, validate: [validator.isUUID, 'Invalid uuid.']},
|
||||
id: {type: String, default: shared.uuid, required: true, validate: [validator.isUUID, 'Invalid uuid.']},
|
||||
}],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -345,6 +345,7 @@ export let schema = new Schema({
|
||||
},
|
||||
|
||||
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: () => {
|
||||
@@ -528,7 +529,7 @@ export let schema = new 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'],
|
||||
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
|
||||
|
||||
@@ -1,33 +1,46 @@
|
||||
h2 5/25/2016 - ANDROID UPDATE AND MAY SUBSCRIBER ITEMS
|
||||
h2 5/30/2016 - LAST CHANCE FOR MARCHING BARD ITEM SET AND FLORAL POTIONS
|
||||
hr
|
||||
tr
|
||||
td
|
||||
h3 iOS App Update Available
|
||||
p We've released <a href='https://itunes.apple.com/us/app/habitica-stay-motivated-gamified/id994882113' target='_blank'>a new iOS app update</a> that contains tons of bug fixes, including for the pet feeding crash and the annoying fake death popups! Be sure to download it now for a more stable Habitica.
|
||||
br
|
||||
p Thank you so much to everyone who reported bugs that cropped up after our massive code overhaul! It was very helpful. And if you have a spare moment, we'd love it if you could review this app. It really helps us out! iOS hides all old reviews, but if you've already written one, it's very easy to repost with a single button click.
|
||||
p.small.muted by viirus
|
||||
tr
|
||||
td
|
||||
h3 Android Update: Bug Fixes
|
||||
p We've released <a href='https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica&hl=en' target='_blank'>a new Android update</a> with fixes for lots of bugs and crashes. Download it now for a more stable experience!
|
||||
br
|
||||
p Thank you for your patience as we worked out some of the hiccups that came with our major code overhaul this weekend. If you like the direction that we're taking the app, we'd love it if you could take the time to leave us a review :) It really helps us out!
|
||||
p.small.muted by viirus
|
||||
tr
|
||||
td
|
||||
.promo_mystery_201605.pull-right
|
||||
h3 May Subscriber Items
|
||||
p The May Subscriber Items have been revealed: the Marching Bard Item Set! You still have six days to <a href='/#/options/settings/subscription'>subscribe</a> and receive the item set.
|
||||
br
|
||||
p Subscribers also receive the ability to buy Gems for Gold -- the longer you subscribe, the more Gems you can buy per month! There are other perks as well, such as longer access to uncompressed data. Best of all, your support directly keeps Habitica running. Thank you very much -- it means a lot to us!
|
||||
.promo_floral_potions.pull-left.slight-right-margin
|
||||
h3 Last Chance for Marching Bard Item Set
|
||||
p Reminder: this is the final day to <a href='/#/options/settings/subscription'>subscribe</a> and receive the Marching Bard Item Set! If you want the Marching Bard Hat or the Marching Bard Uniform, now's the time. Thanks so much for your support -- we hope you enjoy your Gems.
|
||||
p.small.muted by Lemoness
|
||||
tr
|
||||
td
|
||||
h3 Last Chance for Floral Hatching Potions
|
||||
p Reminder: this is the final day to <a href='/#/options/inventory/drops'>buy Floral Hatching Potions</a>! If they come back, it won't be until next year at the earliest, so don't delay!
|
||||
p.small.muted by Mako413
|
||||
|
||||
if menuItem !== 'oldNews'
|
||||
hr
|
||||
a(href='/static/old-news', target='_blank') Read older news
|
||||
|
||||
mixin oldNews
|
||||
h2 5/25/2016 - iOS UPDATE, ANDROID UPDATE, AND MAY SUBSCRIBER ITEMS
|
||||
tr
|
||||
td
|
||||
h3 iOS App Update Available
|
||||
p We've released <a href='https://itunes.apple.com/us/app/habitica-stay-motivated-gamified/id994882113' target='_blank'>a new iOS app update</a> that contains tons of bug fixes, including for the pet feeding crash and the annoying fake death popups! Be sure to download it now for a more stable Habitica.
|
||||
br
|
||||
p Thank you so much to everyone who reported bugs that cropped up after our massive code overhaul! It was very helpful. And if you have a spare moment, we'd love it if you could review this app. It really helps us out! iOS hides all old reviews, but if you've already written one, it's very easy to repost with a single button click.
|
||||
p.small.muted by viirus
|
||||
tr
|
||||
td
|
||||
h3 Android Update: Bug Fixes
|
||||
p We've released <a href='https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica&hl=en' target='_blank'>a new Android update</a> with fixes for lots of bugs and crashes. Download it now for a more stable experience!
|
||||
br
|
||||
p Thank you for your patience as we worked out some of the hiccups that came with our major code overhaul this weekend. If you like the direction that we're taking the app, we'd love it if you could take the time to leave us a review :) It really helps us out!
|
||||
p.small.muted by viirus
|
||||
tr
|
||||
td
|
||||
.promo_mystery_201605.pull-right
|
||||
h3 May Subscriber Items
|
||||
p The May Subscriber Items have been revealed: the Marching Bard Item Set! You still have six days to <a href='/#/options/settings/subscription'>subscribe</a> and receive the item set.
|
||||
br
|
||||
p Subscribers also receive the ability to buy Gems for Gold -- the longer you subscribe, the more Gems you can buy per month! There are other perks as well, such as longer access to uncompressed data. Best of all, your support directly keeps Habitica running. Thank you very much -- it means a lot to us!
|
||||
p.small.muted by Lemoness
|
||||
h2 5/21/2016 - WELCOME BACK, HABITICA!
|
||||
tr
|
||||
td
|
||||
|
||||
Reference in New Issue
Block a user