Merge branch 'api-v3' into api-v3-client

This commit is contained in:
Matteo Pagliazzi 2016-04-24 15:09:10 +02:00
commit f2d5c921ed
9 changed files with 1329 additions and 297 deletions

View file

@ -3,10 +3,15 @@ common/dist/
common/transpiled-babel/
coverage/
database_reports/
migrations/
website/build/
website/transpiled-babel/
migrations/*
!migrations/api_v3
!migrations/api_v3
!migrations/api_v3
migrations/api_v3/old_models
# The files in website/public/js should be moved out and browserified
website/public/

229
migrations/api_v3/users.js Normal file
View file

@ -0,0 +1,229 @@
/* eslint-disable no-console */
// Migrate users collection to new schema
// This should run AFTER challenges migration
// This code makes heavy use of ES6 / 7 features and should be compiled / run with BabelJS.
// It requires two environment variables: MONGODB_OLD and MONGODB_NEW
console.log('Starting migrations/api_v3/users.js.');
import Q from 'q';
import MongoDB from 'mongodb';
import nconf from 'nconf';
import mongoose from 'mongoose';
import _ from 'lodash';
// Initialize configuration
import setupNconf from '../../website/src/libs/api-v3/setupNconf';
setupNconf();
const MONGODB_OLD = nconf.get('MONGODB_OLD');
const MONGODB_NEW = nconf.get('MONGODB_NEW');
mongoose.Promise = Q.Promise; // otherwise mongoose models won't work
// Load old and new models
import { model as NewUser } from '../../website/src/models/user';
import * as Tasks from '../../website/src/models/task';
// To be defined later when MongoClient connects
let mongoDbOldInstance;
let oldUserCollection;
let mongoDbNewInstance;
let newUserCollection;
let newTaskCollection;
async function processUser (_id) {
let [oldUser] = await oldUserCollection
.find({_id})
.limit(1)
.toArray();
let oldTasks = oldUser.habits.concat(oldUser.dailys).concat(oldUser.rewards).concat(oldUser.todos);
oldUser.habits = oldUser.dailys = oldUser.rewards = oldUser.todos = undefined;
oldUser.challenges = [];
oldUser.invitations.guilds = [];
oldUser.invitations.party = {};
oldUser.party = {};
oldUser.tags = oldUser.tags.map(tag => {
return {
_id: tag.id,
name: tag.name,
challenge: tag.challenge,
};
});
let newUser = new NewUser(oldUser);
let batchInsertTasks = newTaskCollection.initializeUnorderedBulkOp();
oldTasks.forEach(oldTask => {
let newTask = new Tasks[oldTask.type](oldTask);
newTask.userId = newUser._id;
newTask.challenge = {};
if (!oldTask.text) newTask.text = 'text';
newTask.tags = _.map(oldTask.tags, (tagPresent, tagId) => {
return tagPresent && tagId;
});
newUser.tasksOrder[`${oldTask.type}s`].push(newTask._id);
// newTask.legacyId = oldTask.id;
batchInsertTasks.insert(newTask.toObject());
});
await Q.all([
newUserCollection.insertOne(newUser.toObject()),
batchInsertTasks.execute(),
]);
console.log(`Saved user ${newUser._id} and their tasks.`);
}
/*
TODO var challengeTasksChangedId = {};
... given a user
let processed = 0;
let batchSize = 1000;
var db; // defined later by MongoClient
var dbNewUsers;
var dbTasks;
var processUser = function(gt) {
var query = {
_id: {}
};
if(gt) query._id.$gt = gt;
console.log('Launching query', query);
// take batchsize docs from users and process them
OldUserModel
.find(query)
.lean() // Use plain JS objects as old user data won't match the new model
.limit(batchSize)
.sort({_id: 1})
.exec(function(err, users) {
if(err) throw err;
console.log('Processing ' + users.length + ' users.', 'Already processed: ' + processed);
var lastUser = null;
if(users.length === batchSize){
lastUser = users[users.length - 1];
}
var tasksToSave = 0;
// Initialize batch operation for later
var batchInsertUsers = dbNewUsers.initializeUnorderedBulkOp();
var batchInsertTasks = dbTasks.initializeUnorderedBulkOp();
users.forEach(function(user){
// user obj is a plain js object because we used .lean()
// add tasks order arrays
user.tasksOrder = {
habits: [],
rewards: [],
todos: [],
dailys: []
};
// ... convert tasks to individual models
var tasksArr = user.dailys
.concat(user.habits)
.concat(user.todos)
.concat(user.rewards);
// free memory?
user.dailys = user.habits = user.todos = user.rewards = undefined;
tasksArr.forEach(function(task){
task.userId = user._id;
task._id = shared.uuid(); // we rely on these to be unique... hopefully!
task.legacyId = task.id;
task.id = undefined;
task.challenge = task.challenge || {};
if(task.challenge.id) {
// If challengeTasksChangedId[task._id] then we got on of the duplicates from the challenges migration
if (challengeTasksChangedId[task.legacyId]) {
var res = _.find(challengeTasksChangedId[task.legacyId], function(arr){
return arr[1] === task.challenge.id;
});
// If res, id changed, otherwise matches the original one
task.challenge.taskId = res ? res[0] : task.legacyId;
} else {
task.challenge.taskId = task.legacyId;
}
}
if(!task.type) console.log('Task without type ', task._id, ' user ', user._id);
task = new TaskModel(task); // this should also fix dailies that wen to the habits array or vice-versa
user.tasksOrder[task.type + 's'].push(task._id);
tasksToSave++;
batchInsertTasks.insert(task.toObject());
});
batchInsertUsers.insert((new NewUserModel(user)).toObject());
});
console.log('Saving', users.length, 'users and', tasksToSave, 'tasks');
// Save in the background and dispatch another processUser();
batchInsertUsers.execute(function(err, result){
if(err) throw err // we can't simply accept errors
console.log('Saved', result.nInserted, 'users')
});
batchInsertTasks.execute(function(err, result){
if(err) throw err // we can't simply accept errors
console.log('Saved', result.nInserted, 'tasks')
});
processed = processed + users.length;
if(lastUser && lastUser._id){
processUser(lastUser._id);
} else {
console.log('Done!');
}
});
};
*/
// Connect to the databases
const MongoClient = MongoDB.MongoClient;
Q.all([
MongoClient.connect(MONGODB_OLD),
MongoClient.connect(MONGODB_NEW),
])
.then(([oldInstance, newInstance]) => {
mongoDbOldInstance = oldInstance;
oldUserCollection = mongoDbOldInstance.collection('users');
mongoDbNewInstance = newInstance;
newUserCollection = mongoDbNewInstance.collection('users');
newTaskCollection = mongoDbNewInstance.collection('tasks');
console.log(`Connected with MongoClient to ${MONGODB_OLD} and ${MONGODB_NEW}.`);
return processUser(nconf.get('USER_ID'));
})
.catch(err => {
throw err;
});

View file

@ -0,0 +1,573 @@
/* eslint-disable global-require */
import moment from 'moment';
import { cron } from '../../../../../website/src/libs/api-v3/cron';
import { model as User } from '../../../../../website/src/models/user';
import * as Tasks from '../../../../../website/src/models/task';
import { clone } from 'lodash';
import common from '../../../../../common';
// const scoreTask = common.ops.scoreTask;
describe('cron', () => {
let user;
let tasksByType = {habits: [], dailys: [], todos: [], rewards: []};
let daysMissed = 0;
let analytics = {
track: sinon.spy(),
};
beforeEach(() => {
user = new User({
auth: {
local: {
username: 'username',
lowerCaseUsername: 'username',
email: 'email@email.email',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
user._statsComputed = {
mp: 10,
};
});
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;
cron({user, tasksByType, daysMissed, analytics, timezoneOffsetFromUserPrefs});
expect(user.preferences.timezoneOffsetAtLastCron).to.equal(timezoneOffsetFromUserPrefs);
});
it('resets user.items.lastDrop.count', () => {
user.items.lastDrop.count = 4;
cron({user, tasksByType, daysMissed, analytics});
expect(user.items.lastDrop.count).to.equal(0);
});
it('increments user cron count', () => {
let cronCountBefore = user.flags.cronCount;
cron({user, tasksByType, daysMissed, analytics});
expect(user.flags.cronCount).to.be.greaterThan(cronCountBefore);
});
describe('end of the month perks', () => {
beforeEach(() => {
user.purchased.plan.customerId = 'subscribedId';
user.purchased.plan.dateUpdated = moment('012013', 'MMYYYY');
});
it('resets plan.gemsBought on a new month', () => {
user.purchased.plan.gemsBought = 10;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.gemsBought).to.equal(0);
});
it('resets plan.dateUpdated on a new month', () => {
let currentMonth = moment().format('MMYYYY');
cron({user, tasksByType, daysMissed, analytics});
expect(moment(user.purchased.plan.dateUpdated).format('MMYYYY')).to.equal(currentMonth);
});
it('increments plan.consecutive.count', () => {
user.purchased.plan.consecutive.count = 0;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.count).to.equal(1);
});
it('decrements plan.consecutive.offset when offset is greater than 0', () => {
user.purchased.plan.consecutive.offset = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.offset).to.equal(0);
});
it('increments plan.consecutive.trinkets when user has reached a month that is a multiple of 3', () => {
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
});
it('increments plan.consecutive.gemCapExtra when user has reached a month that is a multiple of 3', () => {
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(5);
});
it('does not increment plan.consecutive.gemCapExtra when user has reached the gemCap limit', () => {
user.purchased.plan.consecutive.gemCapExtra = 25;
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(25);
});
it('does not reset plan stats if we are before the last day of the cancelled month', () => {
user.purchased.plan.dateTerminated = moment(new Date()).add({days: 1});
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.customerId).to.exist;
});
it('does reset plan stats until we are after the last day of the cancelled month', () => {
user.purchased.plan.dateTerminated = moment(new Date()).subtract({days: 1});
user.purchased.plan.consecutive.gemCapExtra = 20;
user.purchased.plan.consecutive.count = 5;
user.purchased.plan.consecutive.offset = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.customerId).to.not.exist;
expect(user.purchased.plan.consecutive.gemCapExtra).to.be.empty;
expect(user.purchased.plan.consecutive.count).to.be.empty;
expect(user.purchased.plan.consecutive.offset).to.be.empty;
});
});
describe('end of the month perks when user is not subscribed', () => {
it('does not reset plan.gemsBought on a new month', () => {
user.purchased.plan.gemsBought = 10;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.gemsBought).to.equal(10);
});
it('does not reset plan.dateUpdated on a new month', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.dateUpdated).to.be.empty;
});
it('does not increment plan.consecutive.count', () => {
user.purchased.plan.consecutive.count = 0;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.count).to.equal(0);
});
it('does not decrement plan.consecutive.offset when offset is greater than 0', () => {
user.purchased.plan.consecutive.offset = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.offset).to.equal(1);
});
it('does not increment plan.consecutive.trinkets when user has reached a month that is a multiple of 3', () => {
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.trinkets).to.equal(0);
});
it('doest not increment plan.consecutive.gemCapExtra when user has reached a month that is a multiple of 3', () => {
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(0);
});
it('does not increment plan.consecutive.gemCapExtra when user has reached the gemCap limit', () => {
user.purchased.plan.consecutive.gemCapExtra = 25;
user.purchased.plan.consecutive.count = 5;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(25);
});
it('does nothing to plan stats if we are before the last day of the cancelled month', () => {
user.purchased.plan.dateTerminated = moment(new Date()).add({days: 1});
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.customerId).to.not.exist;
});
xit('does nothing to plan stats when we are after the last day of the cancelled month', () => {
user.purchased.plan.dateTerminated = moment(new Date()).subtract({days: 1});
user.purchased.plan.consecutive.gemCapExtra = 20;
user.purchased.plan.consecutive.count = 5;
user.purchased.plan.consecutive.offset = 1;
cron({user, tasksByType, daysMissed, analytics});
expect(user.purchased.plan.customerId).to.exist;
expect(user.purchased.plan.consecutive.gemCapExtra).to.exist;
expect(user.purchased.plan.consecutive.count).to.exist;
expect(user.purchased.plan.consecutive.offset).to.exist;
});
});
describe('user is sleeping', () => {
beforeEach(() => {
user.preferences.sleep = true;
});
it('clears user buffs', () => {
user.stats.buffs = {
str: 1,
int: 1,
per: 1,
con: 1,
stealth: 1,
streaks: true,
};
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.buffs.str).to.equal(0);
expect(user.stats.buffs.int).to.equal(0);
expect(user.stats.buffs.per).to.equal(0);
expect(user.stats.buffs.con).to.equal(0);
expect(user.stats.buffs.stealth).to.equal(0);
expect(user.stats.buffs.streaks).to.be.false;
});
it('resets all dailies without damaging user', () => {
let daily = {
text: 'test daily',
type: 'daily',
frequency: 'daily',
everyX: 5,
startDate: new Date(),
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
tasksByType.dailys.push(task);
tasksByType.dailys[0].completed = true;
let healthBefore = user.stats.hp;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].completed).to.be.false;
expect(user.stats.hp).to.equal(healthBefore);
});
});
describe('todos', () => {
beforeEach(() => {
let todo = {
text: 'test todo',
type: 'todo',
value: 0,
};
let task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line babel/new-cap
tasksByType.todos.push(task);
});
it('should make uncompleted todos redder', () => {
let valueBefore = tasksByType.todos[0].value;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.todos[0].value).to.be.lessThan(valueBefore);
});
it('should add history of completed todos to user history', () => {
tasksByType.todos[0].completed = true;
cron({user, tasksByType, daysMissed, analytics});
expect(user.history.todos).to.be.lengthOf(1);
});
});
describe('dailys', () => {
beforeEach(() => {
let daily = {
text: 'test daily',
type: 'daily',
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
tasksByType.dailys = [];
tasksByType.dailys.push(task);
user._statsComputed = {
con: 1,
};
});
it('should add history', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].history).to.be.lengthOf(1);
});
it('should set tasks completed to false', () => {
tasksByType.dailys[0].completed = true;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].completed).to.be.false;
});
it('should set task checklist to completed for completed dailys', () => {
tasksByType.dailys[0].checklist.push({title: 'test', completed: false});
tasksByType.dailys[0].completed = true;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].checklist[0].completed).to.be.true;
});
it('should set task checklist to completed for dailys with scheduled misses', () => {
daysMissed = 10;
tasksByType.dailys[0].checklist.push({title: 'test', completed: false});
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.dailys[0].checklist[0].completed).to.be.true;
});
it('should do damage for missing a daily', () => {
daysMissed = 1;
let hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.hp).to.be.lessThan(hpBefore);
});
it('should not do damage for missing a daily if user stealth buff is greater than or equal to days missed', () => {
daysMissed = 1;
let hpBefore = user.stats.hp;
user.stats.buffs.stealth = 2;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.hp).to.equal(hpBefore);
});
it('should do less damage for missing a daily with partial completion', () => {
daysMissed = 1;
let hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
cron({user, tasksByType, daysMissed, analytics});
let hpDifferenceOfFullyIncompleteDaily = hpBefore - user.stats.hp;
hpBefore = user.stats.hp;
tasksByType.dailys[0].checklist.push({title: 'test', completed: true});
tasksByType.dailys[0].checklist.push({title: 'test2', completed: false});
cron({user, tasksByType, daysMissed, analytics});
let hpDifferenceOfPartiallyIncompleteDaily = hpBefore - user.stats.hp;
expect(hpDifferenceOfPartiallyIncompleteDaily).to.be.lessThan(hpDifferenceOfFullyIncompleteDaily);
});
it('should decrement quest progress down for missing a daily', () => {
daysMissed = 1;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
let progress = cron({user, tasksByType, daysMissed, analytics});
expect(progress.down).to.equal(-1);
});
});
describe('habits', () => {
beforeEach(() => {
let habit = {
text: 'test habit',
type: 'habit',
};
let task = new Tasks.habit(Tasks.Task.sanitize(habit)); // eslint-disable-line babel/new-cap
tasksByType.habits = [];
tasksByType.habits.push(task);
});
it('should decrement only up value', () => {
tasksByType.habits[0].value = 1;
tasksByType.habits[0].down = false;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].value).to.be.lessThan(1);
});
it('should decrement only down value', () => {
tasksByType.habits[0].value = 1;
tasksByType.habits[0].up = false;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].value).to.be.lessThan(1);
});
it('should do nothing to habits with both up and down', () => {
tasksByType.habits[0].value = 1;
tasksByType.habits[0].up = true;
tasksByType.habits[0].down = true;
cron({user, tasksByType, daysMissed, analytics});
expect(tasksByType.habits[0].value).to.equal(1);
});
});
describe('perfect day', () => {
beforeEach(() => {
let daily = {
text: 'test daily',
type: 'daily',
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
tasksByType.dailys = [];
tasksByType.dailys.push(task);
user._statsComputed = {
con: 1,
};
});
it('stores a new entry in user.history.exp', () => {
user.stats.lvl = 2;
cron({user, tasksByType, daysMissed, analytics});
expect(user.history.exp).to.have.lengthOf(1);
expect(user.history.exp[0].value).to.equal(150);
});
it('increments perfect day achievement', () => {
tasksByType.dailys[0].completed = true;
cron({user, tasksByType, daysMissed, analytics});
expect(user.achievements.perfect).to.equal(1);
});
it('increments user buffs if they have a perfect day', () => {
tasksByType.dailys[0].completed = true;
let previousBuffs = clone(user.stats.buffs);
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
expect(user.stats.buffs.int).to.be.greaterThan(previousBuffs.int);
expect(user.stats.buffs.per).to.be.greaterThan(previousBuffs.per);
expect(user.stats.buffs.con).to.be.greaterThan(previousBuffs.con);
});
it('clears buffs if user does not have a perfect day', () => {
daysMissed = 1;
tasksByType.dailys[0].completed = false;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
user.stats.buffs = {
str: 1,
int: 1,
per: 1,
con: 1,
stealth: 0,
streaks: true,
};
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.buffs.str).to.equal(0);
expect(user.stats.buffs.int).to.equal(0);
expect(user.stats.buffs.per).to.equal(0);
expect(user.stats.buffs.con).to.equal(0);
expect(user.stats.buffs.stealth).to.equal(0);
expect(user.stats.buffs.streaks).to.be.false;
});
});
describe('adding mp', () => {
it('should add mp to user', () => {
let mpBefore = user.stats.mp;
tasksByType.dailys[0].completed = true;
user._statsComputed.maxMP = 100;
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.mp).to.be.greaterThan(mpBefore);
});
it('set user\'s mp to user._statsComputed.maxMP when user.stats.mp is greater', () => {
user.stats.mp = 120;
user._statsComputed.maxMP = 100;
cron({user, tasksByType, daysMissed, analytics});
expect(user.stats.mp).to.equal(user._statsComputed.maxMP);
});
});
describe('quest progress', () => {
beforeEach(() => {
let daily = {
text: 'test daily',
type: 'daily',
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
tasksByType.dailys = [];
tasksByType.dailys.push(task);
user._statsComputed = {
con: 1,
};
daysMissed = 1;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({days: 1});
});
it('resets user progress', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(user.party.quest.progress.up).to.equal(0);
expect(user.party.quest.progress.down).to.equal(0);
expect(user.party.quest.progress.collect).to.be.empty;
});
it('applies the user progress', () => {
let progress = cron({user, tasksByType, daysMissed, analytics});
expect(progress.down).to.equal(-1);
});
});
describe('private messages', () => {
let lastMessageId;
beforeEach(() => {
let maxPMs = 200;
for (let index = 0; index < maxPMs - 1; index += 1) {
let messageId = common.uuid();
user.inbox.messages[messageId] = {
id: messageId,
text: `test ${index}`,
timestamp: Number(new Date()),
likes: {},
flags: {},
flagCount: 0,
};
}
lastMessageId = common.uuid();
user.inbox.messages[lastMessageId] = {
id: lastMessageId,
text: `test ${lastMessageId}`,
timestamp: Number(new Date()),
likes: {},
flags: {},
flagCount: 0,
};
});
xit('does not clear pms under 200', () => {
cron({user, tasksByType, daysMissed, analytics});
expect(user.inbox.messages[lastMessageId]).to.exist;
});
xit('clears pms over 200', () => {
let messageId = common.uuid();
user.inbox.messages[messageId] = {
id: messageId,
text: `test ${messageId}`,
timestamp: Number(new Date()),
likes: {},
flags: {},
flagCount: 0,
};
cron({user, tasksByType, daysMissed, analytics});
expect(user.inbox.messages[messageId]).to.not.exist;
});
});
});

View file

@ -0,0 +1,176 @@
import {
generateRes,
generateReq,
generateNext,
generateTodo,
generateDaily,
} from '../../../../helpers/api-unit.helper';
import cronMiddleware from '../../../../../website/src/middlewares/api-v3/cron';
import moment from 'moment';
import { model as User } from '../../../../../website/src/models/user';
import { model as Group } from '../../../../../website/src/models/group';
import * as Tasks from '../../../../../website/src/models/task';
import analyticsService from '../../../../../website/src/libs/api-v3/analyticsService';
import { v4 as generateUUID } from 'uuid';
describe('cron middleware', () => {
let res, req, next;
let user;
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
user = new User({
auth: {
local: {
username: 'username',
lowerCaseUsername: 'username',
email: 'email@email.email',
salt: 'salt',
hashed_password: 'hashed_password', // eslint-disable-line camelcase
},
},
});
user._statsComputed = {
mp: 10,
maxMP: 100,
};
res.locals.user = user;
res.analytics = analyticsService;
});
it('calls next when user is not attached', () => {
res.locals.user = null;
cronMiddleware(req, res, next);
expect(next).to.be.calledOnce;
});
it('calls next when days have not been missed', () => {
cronMiddleware(req, res, next);
expect(next).to.be.calledOnce;
});
it('should clear todos older than 30 days for free users', async (done) => {
user.lastCron = moment(new Date()).subtract({days: 2});
let task = generateTodo(user);
task.dateCompleted = moment(new Date()).subtract({days: 31});
task.completed = true;
await task.save();
cronMiddleware(req, res, () => {
Tasks.Task.findOne({_id: task}, function (err, taskFound) {
expect(err).to.not.exist;
expect(taskFound).to.not.exist;
done();
});
});
});
it('should not clear todos older than 30 days for subscribed users', (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();
cronMiddleware(req, res, () => {
Tasks.Task.findOne({_id: task}, function (err, taskFound) {
expect(err).to.not.exist;
expect(taskFound).to.exist;
done();
});
});
});
it('should clear todos older than 90 days for subscribed users', (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: 91});
task.completed = true;
task.save();
cronMiddleware(req, res, () => {
Tasks.Task.findOne({_id: task}, function (err, taskFound) {
expect(err).to.not.exist;
expect(taskFound).to.not.exist;
done();
});
});
});
it('should call next is user was not modified after cron', (done) => {
let hpBefore = user.stats.hp;
user.lastCron = moment(new Date()).subtract({days: 2});
generateDaily(user);
cronMiddleware(req, res, () => {
expect(user.stats.hp).to.be.equal(hpBefore);
done();
});
});
it('does damage for missing dailies', (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();
cronMiddleware(req, res, () => {
expect(user.stats.hp).to.be.lessThan(hpBefore);
done();
});
});
it('updates tasks', (done) => {
user.lastCron = moment(new Date()).subtract({days: 2});
let todo = generateTodo(user);
let todoValueBefore = todo.value;
cronMiddleware(req, res, () => {
Tasks.Task.findOne({_id: todo._id}, function (err, todoFound) {
expect(err).to.not.exist;
expect(todoFound.value).to.be.lessThan(todoValueBefore);
done();
});
});
});
it('applies quest progress', 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();
let questKey = 'dilatory';
user.party.quest.key = questKey;
let party = new Group({
type: 'party',
name: generateUUID(),
leader: user._id,
});
party.quest.members[user._id] = true;
party.quest.key = questKey;
await party.save();
user.party._id = party._id;
await user.save();
party.startQuest(user);
cronMiddleware(req, res, () => {
expect(user.stats.hp).to.be.lessThan(hpBefore);
done();
});
});
});

View file

@ -6,6 +6,7 @@ import { model as Group } from '../../website/src/models/group';
import mongo from './mongo'; // eslint-disable-line
import moment from 'moment';
import i18n from '../../common/script/i18n';
import * as Tasks from '../../website/src/models/task';
afterEach((done) => {
sandbox.restore();
@ -70,3 +71,33 @@ export function generateHistory (days) {
return history;
}
export function generateTodo (user) {
let todo = {
text: 'test todo',
type: 'todo',
value: 0,
completed: false,
};
let task = new Tasks.todo(Tasks.Task.sanitize(todo)); // eslint-disable-line babel/new-cap
task.userId = user._id;
task.save();
return task;
}
export function generateDaily (user) {
let daily = {
text: 'test daily',
type: 'daily',
value: 0,
completed: false,
};
let task = new Tasks.daily(Tasks.Task.sanitize(daily)); // eslint-disable-line babel/new-cap
task.userId = user._id;
task.save();
return task;
}

View file

@ -0,0 +1,273 @@
import moment from 'moment';
import common from '../../../../common/';
import { preenUserHistory } from '../../libs/api-v3/preening';
import _ from 'lodash';
const shouldDo = common.shouldDo;
const scoreTask = common.ops.scoreTask;
// const maxPMs = 200;
let CLEAR_BUFFS = {
str: 0,
int: 0,
per: 0,
con: 0,
stealth: 0,
streaks: false,
};
function grantEndOfTheMonthPerks (user, now) {
let plan = user.purchased.plan;
if (moment(plan.dateUpdated).format('MMYYYY') !== moment().format('MMYYYY')) {
plan.gemsBought = 0; // reset gem-cap
plan.dateUpdated = now;
// For every month, inc their "consecutive months" counter. Give perks based on consecutive blocks
// If they already got perks for those blocks (eg, 6mo subscription, subscription gifts, etc) - then dec the offset until it hits 0
// TODO use month diff instead of ++ / --?
_.defaults(plan.consecutive, {count: 0, offset: 0, trinkets: 0, gemCapExtra: 0}); // FIXME see https://github.com/HabitRPG/habitrpg/issues/4317
plan.consecutive.count++;
if (plan.consecutive.offset > 0) {
plan.consecutive.offset--;
} else if (plan.consecutive.count % 3 === 0) { // every 3 months
plan.consecutive.trinkets++;
plan.consecutive.gemCapExtra += 5;
if (plan.consecutive.gemCapExtra > 25) plan.consecutive.gemCapExtra = 25; // cap it at 50 (hard 25 limit + extra 25)
}
}
// If user cancelled subscription, we give them until 30day's end until it terminates
if (plan.dateTerminated && moment(plan.dateTerminated).isBefore(new Date())) {
_.merge(plan, {
planId: null,
customerId: null,
paymentMethod: null,
});
_.merge(plan.consecutive, {
count: 0,
offset: 0,
gemCapExtra: 0,
});
user.markModified('purchased.plan');
}
}
function performSleepTasks (user, tasksByType, now) {
user.stats.buffs = _.cloneDeep(CLEAR_BUFFS);
tasksByType.dailys.forEach((daily) => {
let completed = daily.completed;
let thatDay = moment(now).subtract({days: 1});
if (shouldDo(thatDay.toDate(), daily, user.preferences) || completed) {
daily.checklist.forEach(box => box.completed = false);
}
daily.completed = false;
});
}
// At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
// For incomplete Dailys, deduct experience
// Make sure to run this function once in a while as server will not take care of overnight calculations.
// And you have to run it every time client connects.
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;
// Reset the lastDrop count to zero
if (user.items.lastDrop.count > 0) user.items.lastDrop.count = 0;
// "Perfect Day" achievement for perfect-days
let perfect = true;
if (user.isSubscribed()) {
grantEndOfTheMonthPerks(user, now);
}
// User is resting at the inn.
// On cron, buffs are cleared and all dailies are reset without performing damage
if (user.preferences.sleep === true) {
performSleepTasks(user, tasksByType, now);
return;
}
let multiDaysCountAsOneDay = true;
// If the user does not log in for two or more days, cron (mostly) acts as if it were only one day.
// When site-wide difficulty settings are introduced, this can be a user preference option.
// Tally each task
let todoTally = 0;
tasksByType.todos.forEach(task => { // make uncompleted todos redder
scoreTask({
task,
user,
direction: 'down',
cron: true,
times: multiDaysCountAsOneDay ? 1 : daysMissed,
});
todoTally += task.value;
});
let dailyChecked = 0; // how many dailies were checked?
let dailyDueUnchecked = 0; // how many dailies were cun-hecked?
if (!user.party.quest.progress.down) user.party.quest.progress.down = 0;
tasksByType.dailys.forEach((task) => {
let completed = task.completed;
// Deduct points for missed Daily tasks
let EvadeTask = 0;
let scheduleMisses = daysMissed;
if (completed) {
dailyChecked += 1;
} else {
// dailys repeat, so need to calculate how many they've missed according to their own schedule
scheduleMisses = 0;
for (let i = 0; i < daysMissed; i++) {
let thatDay = moment(now).subtract({days: i + 1});
if (shouldDo(thatDay.toDate(), task, user.preferences)) {
scheduleMisses++;
if (user.stats.buffs.stealth) {
user.stats.buffs.stealth--;
EvadeTask++;
}
if (multiDaysCountAsOneDay) break;
}
}
if (scheduleMisses > EvadeTask) {
perfect = false;
if (task.checklist && task.checklist.length > 0) { // Partially completed checklists dock fewer mana points
let fractionChecked = _.reduce(task.checklist, (m, i) => m + (i.completed ? 1 : 0), 0) / task.checklist.length;
dailyDueUnchecked += 1 - fractionChecked;
dailyChecked += fractionChecked;
} else {
dailyDueUnchecked += 1;
}
let delta = scoreTask({
user,
task,
direction: 'down',
times: multiDaysCountAsOneDay ? 1 : scheduleMisses - EvadeTask,
cron: true,
});
// Apply damage from a boss, less damage for Trivial priority (difficulty)
user.party.quest.progress.down += delta * (task.priority < 1 ? task.priority : 1);
// NB: Medium and Hard priorities do not increase damage from boss. This was by accident
// initially, and when we realised, we could not fix it because users are used to
// their Medium and Hard Dailies doing an Easy amount of damage from boss.
// Easy is task.priority = 1. Anything < 1 will be Trivial (0.1) or any future
// setting between Trivial and Easy.
}
}
task.history.push({
date: Number(new Date()),
value: task.value,
});
task.completed = false;
if (completed || scheduleMisses > 0) {
task.checklist.forEach(i => i.completed = true); // FIXME this should not happen for grey tasks unless they are completed
}
});
tasksByType.habits.forEach((task) => { // slowly reset 'onlies' value to 0
if (task.up === false || task.down === false) {
task.value = Math.abs(task.value) < 0.1 ? 0 : task.value = task.value / 2;
}
});
// Finished tallying
user.history.todos.push({date: now, value: todoTally});
// tally experience
let expTally = user.stats.exp;
let lvl = 0; // iterator
while (lvl < user.stats.lvl - 1) {
lvl++;
expTally += common.tnl(lvl);
}
user.history.exp.push({date: now, value: expTally});
// preen user history so that it doesn't become a performance problem
// also for subscribed users but differentyly
// premium subscribers can keep their full history.
preenUserHistory(user, tasksByType, user.preferences.timezoneOffset);
if (perfect) {
user.achievements.perfect++;
let lvlDiv2 = Math.ceil(common.capByLevel(user.stats.lvl) / 2);
user.stats.buffs = {
str: lvlDiv2,
int: lvlDiv2,
per: lvlDiv2,
con: lvlDiv2,
stealth: 0,
streaks: false,
};
} else {
user.stats.buffs = _.cloneDeep(CLEAR_BUFFS);
}
// Add 10 MP, or 10% of max MP if that'd be more. Perform this after Perfect Day for maximum benefit
// Adjust for fraction of dailies completed
user.stats.mp += _.max([10, 0.1 * user._statsComputed.maxMP]) * dailyChecked / (dailyDueUnchecked + dailyChecked);
if (user.stats.mp > user._statsComputed.maxMP) user.stats.mp = user._statsComputed.maxMP;
if (dailyDueUnchecked === 0 && dailyChecked === 0) dailyChecked = 1;
user.stats.mp += _.max([10, 0.1 * user._statsComputed.maxMP]) * dailyChecked / (dailyDueUnchecked + dailyChecked);
if (user.stats.mp > user._statsComputed.maxMP) {
user.stats.mp = user._statsComputed.maxMP;
}
// After all is said and done, progress up user's effect on quest, return those values & reset the user's
let progress = user.party.quest.progress;
let _progress = _.cloneDeep(progress);
_.merge(progress, {down: 0, up: 0});
progress.collect = _.transform(progress.collect, (m, v, k) => m[k] = 0);
// @TODO: Clean PMs - keep 200 for subscribers and 50 for free users
// let numberOfPMs = Object.keys(user.inbox.messages).length;
// if (numberOfPMs > maxPMs) {
// _(user.inbox.messages)
// .sortBy('timestamp')
// .takeRight(numberOfPMs - maxPMs)
// .each(pm => {
// delete user.inbox.messages[pm.id];
// }).value();
//
// user.markModified('inbox.messages');
// }
// Analytics
user.flags.cronCount++;
analytics.track('Cron', {
category: 'behavior',
gaLabel: 'Cron Count',
gaValue: user.flags.cronCount,
uuid: user._id,
user, // TODO is it really necessary passing the whole user object?
resting: user.preferences.sleep,
cronCount: user.flags.cronCount,
progressUp: _.min([_progress.up, 900]),
progressDown: _progress.down,
});
return _progress;
}

View file

@ -5,274 +5,13 @@ import * as Tasks from '../../models/task';
import Q from 'q';
import { model as Group } from '../../models/group';
import { model as User } from '../../models/user';
import { preenUserHistory } from '../../libs/api-v3/preening';
import { cron } from '../../libs/api-v3/cron';
const daysSince = common.daysSince;
const shouldDo = common.shouldDo;
const scoreTask = common.ops.scoreTask;
let clearBuffs = {
str: 0,
int: 0,
per: 0,
con: 0,
stealth: 0,
streaks: false,
};
// At end of day, add value to all incomplete Daily & Todo tasks (further incentive)
// For incomplete Dailys, deduct experience
// Make sure to run this function once in a while as server will not take care of overnight calculations.
// And you have to run it every time client connects.
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;
// Reset the lastDrop count to zero
if (user.items.lastDrop.count > 0) user.items.lastDrop.count = 0;
// "Perfect Day" achievement for perfect-days
let perfect = true;
// end-of-month perks for subscribers
let plan = user.purchased.plan;
if (user.isSubscribed()) {
if (moment(plan.dateUpdated).format('MMYYYY') !== moment().format('MMYYYY')) {
plan.gemsBought = 0; // reset gem-cap
plan.dateUpdated = now;
// For every month, inc their "consecutive months" counter. Give perks based on consecutive blocks
// If they already got perks for those blocks (eg, 6mo subscription, subscription gifts, etc) - then dec the offset until it hits 0
// TODO use month diff instead of ++ / --?
_.defaults(plan.consecutive, {count: 0, offset: 0, trinkets: 0, gemCapExtra: 0}); // TODO see https://github.com/HabitRPG/habitrpg/issues/4317
plan.consecutive.count++;
if (plan.consecutive.offset > 0) {
plan.consecutive.offset--;
} else if (plan.consecutive.count % 3 === 0) { // every 3 months
plan.consecutive.trinkets++;
plan.consecutive.gemCapExtra += 5;
if (plan.consecutive.gemCapExtra > 25) plan.consecutive.gemCapExtra = 25; // cap it at 50 (hard 25 limit + extra 25)
}
}
// If user cancelled subscription, we give them until 30day's end until it terminates
if (plan.dateTerminated && moment(plan.dateTerminated).isBefore(new Date())) {
_.merge(plan, {
planId: null,
customerId: null,
paymentMethod: null,
});
_.merge(plan.consecutive, {
count: 0,
offset: 0,
gemCapExtra: 0,
});
user.markModified('purchased.plan');
}
}
// User is resting at the inn.
// On cron, buffs are cleared and all dailies are reset without performing damage
if (user.preferences.sleep === true) {
user.stats.buffs = _.cloneDeep(clearBuffs);
tasksByType.dailys.forEach((daily) => {
let completed = daily.completed;
let thatDay = moment(now).subtract({days: 1});
if (shouldDo(thatDay.toDate(), daily, user.preferences) || completed) {
daily.checklist.forEach(box => box.completed = false);
}
daily.completed = false;
});
return;
}
let multiDaysCountAsOneDay = true;
// If the user does not log in for two or more days, cron (mostly) acts as if it were only one day.
// When site-wide difficulty settings are introduced, this can be a user preference option.
// Tally each task
let todoTally = 0;
tasksByType.todos.forEach(task => { // make uncompleted todos redder
scoreTask({
task,
user,
direction: 'down',
cron: true,
times: multiDaysCountAsOneDay ? 1 : daysMissed,
});
todoTally += task.value;
});
let dailyChecked = 0; // how many dailies were checked?
let dailyDueUnchecked = 0; // how many dailies were cun-hecked?
if (!user.party.quest.progress.down) user.party.quest.progress.down = 0;
tasksByType.dailys.forEach((task) => {
let completed = task.completed;
// Deduct points for missed Daily tasks
let EvadeTask = 0;
let scheduleMisses = daysMissed;
if (completed) {
dailyChecked += 1;
} else {
// dailys repeat, so need to calculate how many they've missed according to their own schedule
scheduleMisses = 0;
for (let i = 0; i < daysMissed; i++) {
let thatDay = moment(now).subtract({days: i + 1});
if (shouldDo(thatDay.toDate(), task, user.preferences)) {
scheduleMisses++;
if (user.stats.buffs.stealth) {
user.stats.buffs.stealth--;
EvadeTask++;
}
if (multiDaysCountAsOneDay) break;
}
}
if (scheduleMisses > EvadeTask) {
perfect = false;
if (task.checklist && task.checklist.length > 0) { // Partially completed checklists dock fewer mana points
let fractionChecked = _.reduce(task.checklist, (m, i) => m + (i.completed ? 1 : 0), 0) / task.checklist.length;
dailyDueUnchecked += 1 - fractionChecked;
dailyChecked += fractionChecked;
} else {
dailyDueUnchecked += 1;
}
let delta = scoreTask({
user,
task,
direction: 'down',
times: multiDaysCountAsOneDay ? 1 : scheduleMisses - EvadeTask,
cron: true,
});
// Apply damage from a boss, less damage for Trivial priority (difficulty)
user.party.quest.progress.down += delta * (task.priority < 1 ? task.priority : 1);
// NB: Medium and Hard priorities do not increase damage from boss. This was by accident
// initially, and when we realised, we could not fix it because users are used to
// their Medium and Hard Dailies doing an Easy amount of damage from boss.
// Easy is task.priority = 1. Anything < 1 will be Trivial (0.1) or any future
// setting between Trivial and Easy.
}
}
task.history.push({
date: Number(new Date()),
value: task.value,
});
task.completed = false;
if (completed || scheduleMisses > 0) {
task.checklist.forEach(i => i.completed = true); // TODO this should not happen for grey tasks unless they are completed
}
});
tasksByType.habits.forEach((task) => { // slowly reset 'onlies' value to 0
if (task.up === false || task.down === false) {
task.value = Math.abs(task.value) < 0.1 ? 0 : task.value = task.value / 2;
}
});
// Finished tallying
user.history.todos.push({date: now, value: todoTally});
// tally experience
let expTally = user.stats.exp;
let lvl = 0; // iterator
while (lvl < user.stats.lvl - 1) {
lvl++;
expTally += common.tnl(lvl);
}
user.history.exp.push({date: now, value: expTally});
// preen user history so that it doesn't become a performance problem
// also for subscribed users but differentyly
// premium subscribers can keep their full history.
preenUserHistory(user, tasksByType, user.preferences.timezoneOffset);
if (perfect) {
user.achievements.perfect++;
let lvlDiv2 = Math.ceil(common.capByLevel(user.stats.lvl) / 2);
user.stats.buffs = {
str: lvlDiv2,
int: lvlDiv2,
per: lvlDiv2,
con: lvlDiv2,
stealth: 0,
streaks: false,
};
} else {
user.stats.buffs = _.cloneDeep(clearBuffs);
}
// Add 10 MP, or 10% of max MP if that'd be more. Perform this after Perfect Day for maximum benefit
// Adjust for fraction of dailies completed
user.stats.mp += _.max([10, 0.1 * user._statsComputed.maxMP]) * dailyChecked / (dailyDueUnchecked + dailyChecked);
if (user.stats.mp > user._statsComputed.maxMP) user.stats.mp = user._statsComputed.maxMP;
if (dailyDueUnchecked === 0 && dailyChecked === 0) dailyChecked = 1;
user.stats.mp += _.max([10, 0.1 * user._statsComputed.maxMP]) * dailyChecked / (dailyDueUnchecked + dailyChecked);
if (user.stats.mp > user._statsComputed.maxMP) {
user.stats.mp = user._statsComputed.maxMP;
}
// After all is said and done, progress up user's effect on quest, return those values & reset the user's
let progress = user.party.quest.progress;
let _progress = _.cloneDeep(progress);
progress.down = 0;
progress.up = 0;
progress.collect = _.transform(progress.collect, (m, v, k) => m[k] = 0);
// Clean PMs - keep 200 for subscribers and 50 for free users
// TODO tests
let maxPMs = user.isSubscribed() ? 200 : 50; // TODO 200 limit for contributors too
let numberOfPMs = Object.keys(user.inbox.messages).length;
if (Object.keys(user.inbox.messages).length > maxPMs) {
_(user.inbox.messages)
.sortBy('timestamp')
.takeRight(numberOfPMs - maxPMs)
.each(pm => {
user.inbox.messages[pm.id] = undefined;
}).value();
user.markModified('inbox.messages');
}
// Analytics
user.flags.cronCount++;
analytics.track('Cron', {
category: 'behavior',
gaLabel: 'Cron Count',
gaValue: user.flags.cronCount,
uuid: user._id,
user,
resting: user.preferences.sleep,
cronCount: user.flags.cronCount,
progressUp: _.min([_progress.up, 900]),
progressDown: _progress.down,
});
return _progress;
}
module.exports = function cronMiddleware (req, res, next) {
let user = res.locals.user;
if (!user) return next(); // User might not be available when authentication is not mandatory
let analytics = res.analytics;
@ -381,7 +120,7 @@ module.exports = function cronMiddleware (req, res, next) {
type: 'todo',
completed: true,
dateCompleted: {
$lt: moment(now).subtract(user.isSubscribed() ? 90 : 30, 'days'),
$lt: moment(now).subtract(user.isSubscribed() ? 90 : 30, 'days').toDate(),
},
'challenge.id': {$exists: false},
}).exec(); // TODO wait before returning?
@ -397,13 +136,13 @@ module.exports = function cronMiddleware (req, res, next) {
// Save user and tasks
let toSave = [user.save()];
tasks.forEach(task => {
if (task.isModified) toSave.push(task.save());
toSave.push(task.save());
});
Q.all(toSave)
.then(saved => {
user = res.locals.user = saved[0];
if (!quest) return;
// 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?

View file

@ -475,7 +475,6 @@ function _isOnQuest (user, progress, group) {
// Returns a promise
schema.statics.collectQuest = async function collectQuest (user, progress) {
let group = await this.getGroup({user, groupId: 'party'});
if (!_isOnQuest(user, progress, group)) return;
let quest = shared.content.quests[group.quest.key];

View file

@ -1,42 +1,49 @@
h2 4/15/2016 - iOS AND ANDROID UPDATES, AND ANOTHER BEGUILEMENT STRIKE!
h2 4/23/2016 - THE BE-WILDER STRIKES AGAIN!
hr
tr
td
h3 iOS Update: Fixes Galore
p We've released <a href='https://itunes.apple.com/us/app/habitica/id994882113?ls=1&mt=8'>a new iOS update</a> focusing on stability and bug fixes!
h3 World Boss: Third Beguilement Strike!
p Look out! In the middle of reporting the news, Bailey the Town Crier has been possessed by the Be-Wilder! She lets out an evil, uninformative screech as she rises into the air. Now how will we know whats going on?
br
p It includes multiple crash fixes, most notably for iOS 7 and for quests with rage bars, and clears up frustrating bugs such as the issues with timezones, the duplicating items bug, and the fact that completed to-dos used to still cause reminders. Be sure to download it now for a better Habitica experience!
br
p Thank you very much for your patience! If you like the improvements that weve been making to our app, please consider reviewing this new version. It really helps us out! Also, old reviews get hidden, but if you go to the review section you can re-post it again with a single tap. We hope you enjoy the update!
p.small.muted by viirus, schrockblock, a-ayyash, and nivl4
tr
td
.Pet-Wolf-Base.pull-left.slight-right-margin
h3 Android App Update: Pets, Enchanted Armoire, and More!
p In case you missed it, yesterday we updated <a href='https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica'>the Android app</a> to include a ton of features, including:
br
ul
li Pets and Mounts! Now you can use the app to hatch pets, feed them, and equip pets and mounts.
li The World Boss! Now you can view the World Boss Battle from the Tavern on the app!
li The Enchanted Armoire! Tap right from the app for a chance at equipment, Experience, or food.
li Edit Task Attributes! Assign attributes to tasks for use with task-based point allocation.
li Crash and Bug Fixes! Including fixes for incorrectly-locked backgrounds, date display, editing task attribution allocation, and more.
br
p If you like what we're doing with the app, please consider leaving a review! It means a lot to us.
p.small.muted by viirus, saranlert, schrockblock, ablx and jeubank12
tr
td
.npc_matt_broken.pull-right
h3 World Boss: Second Beguilement Strike!
p In more frightening news, the Be-Wilder has used another Beguilement Strike!
br
p Once again the Be-Wilder has dazzled us into neglecting our Dailies, and now it has attacked Matt the Beast Master! With a swirl of mist, Matt transforms into a terrifying winged creature, and all the pets and mounts howl sadly in their stables. Quickly, stay focused on your tasks to defeat this dastardly distraction!
p Don't give up... we're so close to defeating this bothersome bird for once and for all!
if menuItem !== 'oldNews'
hr
a(href='/static/old-news', target='_blank') Read older news
mixin oldNews
h2 4/15/2016 - iOS AND ANDROID UPDATES, AND ANOTHER BEGUILEMENT STRIKE!
tr
td
h3 iOS Update: Fixes Galore
p We've released <a href='https://itunes.apple.com/us/app/habitica/id994882113?ls=1&mt=8'>a new iOS update</a> focusing on stability and bug fixes!
br
p It includes multiple crash fixes, most notably for iOS 7 and for quests with rage bars, and clears up frustrating bugs such as the issues with timezones, the duplicating items bug, and the fact that completed to-dos used to still cause reminders. Be sure to download it now for a better Habitica experience!
br
p Thank you very much for your patience! If you like the improvements that weve been making to our app, please consider reviewing this new version. It really helps us out! Also, old reviews get hidden, but if you go to the review section you can re-post it again with a single tap. We hope you enjoy the update!
p.small.muted by viirus, schrockblock, a-ayyash, and nivl4
tr
td
.Pet-Wolf-Base.pull-left.slight-right-margin
h3 Android App Update: Pets, Enchanted Armoire, and More!
p In case you missed it, yesterday we updated <a href='https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica'>the Android app</a> to include a ton of features, including:
br
ul
li Pets and Mounts! Now you can use the app to hatch pets, feed them, and equip pets and mounts.
li The World Boss! Now you can view the World Boss Battle from the Tavern on the app!
li The Enchanted Armoire! Tap right from the app for a chance at equipment, Experience, or food.
li Edit Task Attributes! Assign attributes to tasks for use with task-based point allocation.
li Crash and Bug Fixes! Including fixes for incorrectly-locked backgrounds, date display, editing task attribution allocation, and more.
br
p If you like what we're doing with the app, please consider leaving a review! It means a lot to us.
p.small.muted by viirus, saranlert, schrockblock, ablx and jeubank12
tr
td
.npc_matt_broken.pull-right
h3 World Boss: Second Beguilement Strike!
p In more frightening news, the Be-Wilder has used another Beguilement Strike!
br
p Once again the Be-Wilder has dazzled us into neglecting our Dailies, and now it has attacked Matt the Beast Master! With a swirl of mist, Matt transforms into a terrifying winged creature, and all the pets and mounts howl sadly in their stables. Quickly, stay focused on your tasks to defeat this dastardly distraction!
h2 4/14/2016 - ANDROID APP UPDATES!
tr
td