mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-19 15:48:04 +01:00
between client & server. If the op is called on the client, it updates the user & then POSTs to the server with op of the same name. If called on server, it updates the user and user.save()s
347 lines
11 KiB
JavaScript
347 lines
11 KiB
JavaScript
// @see ../routes for routing
|
|
|
|
var _ = require('lodash');
|
|
var nconf = require('nconf');
|
|
var async = require('async');
|
|
var shared = require('habitrpg-shared');
|
|
var User = require('./../models/user').model;
|
|
var Group = require('./../models/group').model;
|
|
var Challenge = require('./../models/challenge').model;
|
|
var api = module.exports;
|
|
|
|
|
|
/*
|
|
------------------------------------------------------------------------
|
|
Challenges
|
|
------------------------------------------------------------------------
|
|
*/
|
|
|
|
api.list = function(req, res) {
|
|
var user = res.locals.user;
|
|
async.waterfall([
|
|
function(cb){
|
|
// Get all available groups I belong to
|
|
Group.find({members: {$in: [user._id]}}).select('_id').exec(cb);
|
|
},
|
|
function(gids, cb){
|
|
// and their challenges
|
|
Challenge.find({
|
|
$or:[
|
|
{leader: user._id},
|
|
{members:{$in:[user._id]}}, // all challenges I belong to (is this necessary? thought is a left a group, but not its challenge)
|
|
{group:{$in:gids}}, // all challenges in my groups
|
|
{group: 'habitrpg'} // public group
|
|
]
|
|
})
|
|
.select('name leader description group members prize')
|
|
.populate('group', '_id name')
|
|
.populate('leader', 'profile.name')
|
|
.sort('-timestamp')
|
|
.exec(cb);
|
|
}
|
|
], function(err, challenges){
|
|
if (err) return res.json(500,{err:err});
|
|
_.each(challenges, function(c){
|
|
c._isMember = !!~c.members.indexOf(user._id);
|
|
c.memberCount = _.size(c.members);
|
|
c.members = undefined;
|
|
})
|
|
res.json(challenges);
|
|
});
|
|
}
|
|
|
|
// GET
|
|
api.get = function(req, res) {
|
|
// TODO use mapReduce() or aggregate() here to
|
|
// 1) Find the sum of users.tasks.values within the challnege (eg, {'profile.name':'tyler', 'sum': 100})
|
|
// 2) Sort by the sum
|
|
// 3) Limit 30 (only show the 30 users currently in the lead)
|
|
Challenge.findById(req.params.cid)
|
|
.populate('members', 'profile.name _id')
|
|
.exec(function(err, challenge){
|
|
if(err) return res.json(500, {err:err});
|
|
if (!challenge) return res.json(404, {err: 'Challenge ' + req.params.cid + ' not found'});
|
|
res.json(challenge);
|
|
})
|
|
}
|
|
|
|
api.getMember = function(req, res) {
|
|
var cid = req.params.cid, uid = req.params.uid;
|
|
// TMK we can't use $elemMatch (which would make things much cleaner) @see http://goo.gl/MxmWdQ & http://goo.gl/Iku44w
|
|
// Revert back to 9fbb45c to see the $elemMatch solution
|
|
User.findById(uid)
|
|
.select('profile.name habits dailys rewards todos')
|
|
.exec(function(err, member){
|
|
if(err) return res.json(500, {err:err});
|
|
if (!member) return res.json(404, {err: 'Member '+uid+' for challenge '+cid+' not found'});
|
|
_.each(['habits','dailys','todos', 'rewards'], function(type){
|
|
member[type] = _.filter(member[type], function(task){
|
|
return task.challenge && task.challenge.id && task.challenge.id == cid;
|
|
});
|
|
});
|
|
res.json(member);
|
|
})
|
|
}
|
|
|
|
// CREATE
|
|
api.create = function(req, res){
|
|
var user = res.locals.user;
|
|
var group, chal;
|
|
|
|
// First, make sure they've selected a legit group, and store it for later
|
|
var waterfall = [
|
|
function(cb){
|
|
Group.findById(req.body.group).exec(cb);
|
|
},
|
|
function(_group, cb){
|
|
if (!_group) return cb("Group." + req.body.group + " not found");
|
|
group = _group;
|
|
cb(null);
|
|
}
|
|
];
|
|
|
|
// If they're adding a prize, do some validation
|
|
if (+req.body.prize < 0) return res.json(401, {err: 'Challenge prize must be >= 0'});
|
|
if (req.body.group=='habitrpg' && +req.body.prize < 1) return res.json(401, {err: 'Prize must be at least 1 Gem for public challenges.'});
|
|
if (+req.body.prize > 0) {
|
|
waterfall.push(function(cb){
|
|
var groupBalance = ((group.balance && group.leader==user._id) ? group.balance : 0);
|
|
if (req.body.prize > (user.balance*4 + groupBalance*4))
|
|
return cb("Challenge.prize > (your gems + group balance). Purchase more gems or lower prize amount.s")
|
|
|
|
var net = req.body.prize/4; // I really should have stored user.balance as gems rather than dollars... stupid...
|
|
|
|
// user is group leader, and group has balance. Subtract from that first, then take the rest from user
|
|
if (groupBalance > 0) {
|
|
group.balance -= net;
|
|
if (group.balance < 0) {
|
|
net = Math.abs(group.balance);
|
|
group.balance = 0;
|
|
}
|
|
}
|
|
user.balance -= net;
|
|
cb(null)
|
|
});
|
|
}
|
|
|
|
waterfall = waterfall.concat([
|
|
function(cb) { // if we're dealing with prize above, arguemnts will be `group, numRows, cb` - else `cb`
|
|
var chal = new Challenge(req.body); // FIXME sanitize
|
|
chal.members.push(user._id);
|
|
chal.save(cb)
|
|
},
|
|
function(_chal, num, cb){
|
|
chal = _chal;
|
|
group.challenges.push(chal._id);
|
|
group.save(cb);
|
|
},
|
|
function(_group, num, cb) {
|
|
// Auto-join creator to challenge (see members.push above)
|
|
chal.syncToUser(user, cb);
|
|
}
|
|
]);
|
|
async.waterfall(waterfall, function(err){
|
|
if (err) return res.json(500, {err:err});
|
|
res.json(chal);
|
|
});
|
|
}
|
|
|
|
// UPDATE
|
|
api.update = function(req, res){
|
|
var cid = req.params.cid;
|
|
var user = res.locals.user;
|
|
var before;
|
|
async.waterfall([
|
|
function(cb){
|
|
// We first need the original challenge data, since we're going to compare against new & decide to sync users
|
|
Challenge.findById(cid, cb);
|
|
},
|
|
function(_before, cb) {
|
|
if (!_before) return cb('Challenge ' + cid + ' not found');
|
|
if (_before.leader != user._id) return cb("You don't have permissions to edit this challenge");
|
|
// Update the challenge, since syncing will need the updated challenge. But store `before` we're going to do some
|
|
// before-save / after-save comparison to determine if we need to sync to users
|
|
before = _before;
|
|
var attrs = _.pick(req.body, 'name shortName description habits dailys todos rewards date'.split(' '));
|
|
Challenge.findByIdAndUpdate(cid, {$set:attrs}, cb);
|
|
},
|
|
function(saved, cb) {
|
|
// after saving, we're done as far as the client's concerned. We kick of syncing (heavy task) in the background
|
|
cb(null, saved);
|
|
|
|
// Compare whether any changes have been made to tasks. If so, we'll want to sync those changes to subscribers
|
|
if (before.isOutdated(req.body)) {
|
|
User.find({_id: {$in: saved.members}}, function(err, users){
|
|
console.log('Challenge updated, sync to subscribers');
|
|
if (err) throw err;
|
|
_.each(users, function(user){
|
|
saved.syncToUser(user);
|
|
})
|
|
})
|
|
}
|
|
|
|
}
|
|
], function(err, saved){
|
|
if(err) res.json(500, {err:err});
|
|
res.json(saved);
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Called by either delete() or selectWinner(). Will delete the challenge and set the "broken" property on all users' subscribed tasks
|
|
* @param {cid} the challenge id
|
|
* @param {broken} the object representing the broken status of the challenge. Eg:
|
|
* {broken: 'CHALLENGE_DELETED', id: CHALLENGE_ID}
|
|
* {broken: 'CHALLENGE_CLOSED', id: CHALLENGE_ID, winner: USER_NAME}
|
|
*/
|
|
function closeChal(cid, broken, cb) {
|
|
var removed;
|
|
async.waterfall([
|
|
function(cb2){
|
|
Challenge.findOneAndRemove({_id:cid}, cb2)
|
|
},
|
|
function(_removed, cb2) {
|
|
removed = _removed;
|
|
var pull = {'$pull':{}}; pull['$pull'][_removed._id] = 1;
|
|
Group.findByIdAndUpdate(_removed.group, pull);
|
|
User.find({_id:{$in: removed.members}}, cb2);
|
|
},
|
|
function(users, cb2) {
|
|
var parallel = [];
|
|
_.each(users, function(user){
|
|
var tag = _.find(user.tags, {id:cid});
|
|
if (tag) tag.challenge = undefined;
|
|
_.each(user.tasks, function(task){
|
|
if (task.challenge && task.challenge.id == removed._id) {
|
|
_.merge(task.challenge, broken);
|
|
}
|
|
})
|
|
parallel.push(function(cb3){
|
|
user.save(cb3);
|
|
})
|
|
})
|
|
async.parallel(parallel, cb2);
|
|
}
|
|
], cb);
|
|
}
|
|
|
|
/**
|
|
* Delete & close
|
|
*/
|
|
api['delete'] = function(req, res){
|
|
var user = res.locals.user;
|
|
var cid = req.params.cid;
|
|
async.waterfall([
|
|
function(cb){
|
|
Challenge.findById(cid, cb);
|
|
},
|
|
function(chal, cb){
|
|
if (!chal) return cb('Challenge ' + cid + ' not found');
|
|
if (chal.leader != user._id) return cb("You don't have permissions to edit this challenge");
|
|
closeChal(req.params.cid, {broken: 'CHALLENGE_DELETED'}, cb);
|
|
}
|
|
], function(err){
|
|
if (err) return res.json(500, {err: err});
|
|
res.send(200);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Select Winner & Close
|
|
*/
|
|
api.selectWinner = function(req, res) {
|
|
if (!req.query.uid) return res.json(401, {err: 'Must select a winner'});
|
|
var user = res.locals.user;
|
|
var cid = req.params.cid;
|
|
var chal;
|
|
async.waterfall([
|
|
function(cb){
|
|
Challenge.findById(cid, cb);
|
|
},
|
|
function(_chal, cb){
|
|
chal = _chal;
|
|
if (!chal) return cb('Challenge ' + cid + ' not found');
|
|
if (chal.leader != user._id) return cb("You don't have permissions to edit this challenge");
|
|
User.findById(req.query.uid, cb)
|
|
},
|
|
function(winner, cb){
|
|
if (!winner) return cb('Winner ' + req.query.uid + ' not found.');
|
|
_.defaults(winner.achievements, {challenges: []});
|
|
winner.achievements.challenges.push(chal.name);
|
|
winner.balance += chal.prize/4;
|
|
winner.save(cb);
|
|
},
|
|
function(saved, num, cb) {
|
|
closeChal(cid, {broken: 'CHALLENGE_CLOSED', winner: saved.profile.name}, cb);
|
|
}
|
|
], function(err){
|
|
if (err) return res.json(500, {err: err});
|
|
res.send(200);
|
|
})
|
|
}
|
|
|
|
api.join = function(req, res){
|
|
var user = res.locals.user;
|
|
var cid = req.params.cid;
|
|
|
|
async.waterfall([
|
|
function(cb){
|
|
Challenge.findByIdAndUpdate(cid, {$addToSet:{members:user._id}}, cb);
|
|
},
|
|
function(challenge, cb){
|
|
if (!~user.challenges.indexOf(cid))
|
|
user.challenges.unshift(cid);
|
|
// Add all challenge's tasks to user's tasks
|
|
challenge.syncToUser(user, function(err){
|
|
if (err) return cb(err);
|
|
cb(null, challenge); // we want the saved challenge in the return results, due to ng-resource
|
|
});
|
|
}
|
|
], function(err, result){
|
|
if(err) return res.json(500,{err:err});
|
|
result._isMember = true;
|
|
res.json(result);
|
|
});
|
|
}
|
|
|
|
|
|
api.leave = function(req, res){
|
|
var user = res.locals.user;
|
|
var cid = req.params.cid;
|
|
// whether or not to keep challenge's tasks. strictly default to true if "keep-all" isn't provided
|
|
var keep = (/^remove-all/i).test(req.query.keep) ? 'remove-all' : 'keep-all';
|
|
|
|
async.waterfall([
|
|
function(cb){
|
|
Challenge.findByIdAndUpdate(cid, {$pull:{members:user._id}}, cb);
|
|
},
|
|
function(chal, cb){
|
|
var i = user.challenges.indexOf(cid)
|
|
if (~i) user.challenges.splice(i,1);
|
|
user.unlink({cid:chal._id, keep:keep}, function(err){
|
|
if (err) return cb(err);
|
|
cb(null, chal);
|
|
})
|
|
}
|
|
], function(err, result){
|
|
if(err) return res.json(500,{err:err});
|
|
result._isMember = false;
|
|
res.json(result);
|
|
});
|
|
}
|
|
|
|
api.unlink = function(req, res, next) {
|
|
// they're scoring the task - commented out, we probably don't need it due to route ordering in api.js
|
|
//var urlParts = req.originalUrl.split('/');
|
|
//if (_.contains(['up','down'], urlParts[urlParts.length -1])) return next();
|
|
|
|
var user = res.locals.user;
|
|
var tid = req.params.id;
|
|
var cid = user.tasks[tid].challenge.id;
|
|
if (!req.query.keep)
|
|
return res.json(400, {err: 'Provide unlink method as ?keep=keep-all (keep, keep-all, remove, remove-all)'});
|
|
user.unlink({cid:cid, keep:req.query.keep, tid:tid}, function(err, saved){
|
|
if (err) return res.json(500,{err:err});
|
|
res.send(200);
|
|
});
|
|
} |