mirror of
https://github.com/sudoxnym/habitica.git
synced 2026-08-05 12:02:13 +00:00
469 lines
No EOL
14 KiB
JavaScript
469 lines
No EOL
14 KiB
JavaScript
/* @see ./routes.coffee for routing*/
|
|
|
|
var url = require('url');
|
|
var ipn = require('paypal-ipn');
|
|
var _ = require('lodash');
|
|
var nconf = require('nconf');
|
|
var async = require('async');
|
|
var shared = require('habitrpg-shared');
|
|
var validator = require('validator');
|
|
var check = validator.check;
|
|
var sanitize = validator.sanitize;
|
|
var User = require('./../models/user').model;
|
|
var Group = require('./../models/group').model;
|
|
var Challenge = require('./../models/challenge').model;
|
|
var acceptablePUTPaths;
|
|
var api = module.exports;
|
|
|
|
// api.purchase // Shared.ops
|
|
|
|
api.getContent = function(req, res, next) {
|
|
res.json(shared.content);
|
|
}
|
|
|
|
/*
|
|
------------------------------------------------------------------------
|
|
Tasks
|
|
------------------------------------------------------------------------
|
|
*/
|
|
|
|
|
|
/*
|
|
Local Methods
|
|
---------------
|
|
*/
|
|
|
|
findTask = function(req, res) {
|
|
return task = res.locals.user.tasks[req.params.id];
|
|
};
|
|
|
|
/*
|
|
API Routes
|
|
---------------
|
|
*/
|
|
|
|
/**
|
|
This is called form deprecated.coffee's score function, and the req.headers are setup properly to handle the login
|
|
Export it also so we can call it from deprecated.coffee
|
|
*/
|
|
api.score = function(req, res, next) {
|
|
var id = req.params.id,
|
|
direction = req.params.direction,
|
|
user = res.locals.user,
|
|
task;
|
|
|
|
// Send error responses for improper API call
|
|
if (!id) return res.json(400, {err: ':id required'});
|
|
if (direction !== 'up' && direction !== 'down') {
|
|
if (direction == 'unlink') return next();
|
|
return res.json(400, {err: ":direction must be 'up' or 'down'"});
|
|
}
|
|
// If exists already, score it
|
|
if (task = user.tasks[id]) {
|
|
// Set completed if type is daily or todo and task exists
|
|
if (task.type === 'daily' || task.type === 'todo') {
|
|
task.completed = direction === 'up';
|
|
}
|
|
} else {
|
|
// If it doesn't exist, this is likely a 3rd party up/down - create a new one, then score it
|
|
task = {
|
|
id: id,
|
|
value: 0,
|
|
type: req.body.type || 'habit',
|
|
text: req.body.title || id,
|
|
notes: "This task was created by a third-party service. Feel free to edit, it won't harm the connection to that service. Additionally, multiple services may piggy-back off this task."
|
|
};
|
|
if (task.type === 'habit') {
|
|
task.up = task.down = true;
|
|
}
|
|
if (task.type === 'daily' || task.type === 'todo') {
|
|
task.completed = direction === 'up';
|
|
}
|
|
task = user.ops.addTask({body:task});
|
|
}
|
|
var delta = user.ops.score({params:{id:task.id, direction:direction}});
|
|
|
|
user.save(function(err,saved){
|
|
if (err) return res.json(500, {err: err});
|
|
res.json(200, _.extend({
|
|
delta: delta,
|
|
}, saved.toJSON().stats));
|
|
});
|
|
|
|
// if it's a challenge task, sync the score
|
|
user.syncScoreToChallenge(task, delta);
|
|
};
|
|
|
|
/**
|
|
* Get all tasks
|
|
*/
|
|
api.getTasks = function(req, res, next) {
|
|
var user = res.locals.user;
|
|
if (req.query.type) {
|
|
return res.json(user[req.query.type+'s']);
|
|
} else {
|
|
return res.json(_.toArray(user.tasks));
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get Task
|
|
*/
|
|
api.getTask = function(req, res, next) {
|
|
var task = findTask(req,res);
|
|
if (!task) return res.json(404, {err: "No task found."});
|
|
return res.json(200, task);
|
|
};
|
|
|
|
|
|
/*
|
|
Update Task
|
|
*/
|
|
|
|
//api.deleteTask // see Shared.ops
|
|
// api.updateTask // handled in Shared.ops
|
|
// api.addTask // handled in Shared.ops
|
|
// api.sortTask // handled in Shared.ops #TODO updated api, mention in docs
|
|
|
|
/*
|
|
------------------------------------------------------------------------
|
|
Items
|
|
------------------------------------------------------------------------
|
|
*/
|
|
// api.buy // handled in Shard.ops
|
|
|
|
/*
|
|
------------------------------------------------------------------------
|
|
User
|
|
------------------------------------------------------------------------
|
|
*/
|
|
|
|
/**
|
|
* Get User
|
|
*/
|
|
api.getUser = function(req, res, next) {
|
|
var user = res.locals.user.toJSON();
|
|
user.stats.toNextLevel = shared.tnl(user.stats.lvl);
|
|
user.stats.maxHealth = 50;
|
|
delete user.apiToken;
|
|
if (user.auth) {
|
|
delete user.auth.hashed_password;
|
|
delete user.auth.salt;
|
|
}
|
|
return res.json(200, user);
|
|
};
|
|
|
|
|
|
/**
|
|
* This tells us for which paths users can call `PUT /user` (or batch-update equiv, which use `User.set()` on our client).
|
|
* The trick here is to only accept leaf paths, not root/intermediate paths (see http://goo.gl/OEzkAs)
|
|
* FIXME - one-by-one we want to widdle down this list, instead replacing each needed set path with API operations
|
|
*/
|
|
acceptablePUTPaths = _.reduce(require('./../models/user').schema.paths, function(m,v,leaf){
|
|
var found= _.find('achievements filters flags invitations lastCron party preferences profile stats'.split(' '), function(root){
|
|
return leaf.indexOf(root) == 0;
|
|
});
|
|
if (found) m[leaf]=true;
|
|
return m;
|
|
}, {})
|
|
|
|
/**
|
|
* Update user
|
|
* Send up PUT /user as `req.body={path1:val, path2:val, etc}`. Example:
|
|
* PUT /user {'stats.hp':50, 'tasks.TASK_ID.repeat.m':false}
|
|
* See acceptablePUTPaths for which user paths are supported
|
|
*/
|
|
api.update = function(req, res, next) {
|
|
var user = res.locals.user;
|
|
var errors = [];
|
|
if (_.isEmpty(req.body)) return res.json(200, user);
|
|
|
|
_.each(req.body, function(v, k) {
|
|
if (acceptablePUTPaths[k])
|
|
user.fns.dotSet(k, v);
|
|
else
|
|
errors.push("path `" + k + "` was not saved, as it's a protected path. See https://github.com/HabitRPG/habitrpg/blob/develop/API.md for PUT /api/v2/user.");
|
|
return true;
|
|
});
|
|
user.save(function(err) {
|
|
if (!_.isEmpty(errors)) return res.json(500, {err: errors});
|
|
if (err) {return res.json(500, {err: err})}
|
|
res.json(200, user);
|
|
});
|
|
};
|
|
|
|
api.cron = function(req, res, next) {
|
|
var user = res.locals.user;
|
|
tally = user.fns.cron();
|
|
if (user.isModified()) res.locals.wasModified = true;
|
|
|
|
// If user is on a quest, roll for boss & player
|
|
if (user.party.quest.key && tally && (tally.up || tally.down)) {
|
|
async.waterfall([
|
|
function(cb){
|
|
Group.findOne({type: 'party', members: {'$in': [user._id]}})
|
|
.populate({
|
|
path: 'members',
|
|
match: {'party.quest.key':user.party.quest.key}
|
|
})
|
|
.exec(cb);
|
|
},
|
|
function(group, cb){
|
|
var quest = shared.content.quests[group.quest.key];
|
|
var down = tally.down * quest.stats.str; // multiply by boss strength
|
|
var parallel = [];
|
|
_.each(group.members, function(m){
|
|
parallel.push(function(cb2){
|
|
m.stats.hp += down;
|
|
m._v++;
|
|
m.save(cb2);
|
|
})
|
|
})
|
|
// Use http://js2coffee.org/ for building this string. Man I wish JS had interpolation...
|
|
group.sendChat("`<" + user.profile.name + "> attacks <" + quest.name + "> for " + (tally.up.toFixed(1)) + " damage, <" + quest.name + "> attacks party for " + (tally.down.toFixed(1)) + " damage.`");
|
|
parallel.push(function(cb2){
|
|
group.hurtBoss(tally.up,cb2);
|
|
});
|
|
async.parallel(parallel,cb);
|
|
}
|
|
], next);
|
|
|
|
} else {
|
|
user.save(next);
|
|
}
|
|
};
|
|
|
|
// api.reroll // Shared.ops
|
|
// api.reset // Shared.ops
|
|
|
|
api['delete'] = function(req, res) {
|
|
res.locals.user.remove(function(err){
|
|
if (err) return res.json(500,{err:err});
|
|
res.send(200);
|
|
})
|
|
}
|
|
|
|
/*
|
|
------------------------------------------------------------------------
|
|
Unlock Preferences
|
|
------------------------------------------------------------------------
|
|
*/
|
|
|
|
// api.unlock // see Shared.ops
|
|
|
|
/*
|
|
------------------------------------------------------------------------
|
|
Buy Gems
|
|
------------------------------------------------------------------------
|
|
*/
|
|
|
|
api.addTenGems = function(req, res) {
|
|
var user = res.locals.user;
|
|
user.balance += 2.5;
|
|
user.save(function(err){
|
|
if (err) return res.json(500,{err:err});
|
|
res.send(204);
|
|
})
|
|
}
|
|
|
|
/*
|
|
Setup Stripe response when posting payment
|
|
*/
|
|
api.buyGems = function(req, res) {
|
|
var api_key = nconf.get('STRIPE_API_KEY');
|
|
var stripe = require("stripe")(api_key);
|
|
var token = req.body.id;
|
|
// console.dir {token:token, req:req}, 'stripe'
|
|
|
|
async.waterfall([
|
|
function(cb){
|
|
stripe.charges.create({
|
|
amount: "500", // $5
|
|
currency: "usd",
|
|
card: token
|
|
}, cb);
|
|
},
|
|
function(response, cb) {
|
|
res.locals.user.balance += 5;
|
|
res.locals.user.purchased.ads = true;
|
|
res.locals.user.save(cb);
|
|
}
|
|
], function(err, saved){
|
|
if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors
|
|
res.send(200, saved);
|
|
});
|
|
};
|
|
|
|
api.buyGemsPaypalIPN = function(req, res) {
|
|
res.send(200);
|
|
ipn.verify(req.body, function callback(err, msg) {
|
|
if (err) {
|
|
console.error(msg);
|
|
res.send(500, msg);
|
|
} else {
|
|
if (req.body.payment_status == 'Completed') {
|
|
//Payment has been confirmed as completed
|
|
var parts = url.parse(req.body.custom, true);
|
|
var uid = parts.query.uid; //, apiToken = query.apiToken;
|
|
if (!uid) throw new Error("uuid or apiToken not found when completing paypal transaction");
|
|
User.findById(uid, function(err, user) {
|
|
if (err) throw err;
|
|
if (_.isEmpty(user)) throw "user not found with uuid " + uuid + " when completing paypal transaction"
|
|
user.balance += 5;
|
|
user.purchased.ads = true;
|
|
user.save();
|
|
console.log('PayPal transaction completed and user updated');
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/*
|
|
------------------------------------------------------------------------
|
|
Tags
|
|
------------------------------------------------------------------------
|
|
*/
|
|
// api.deleteTag // handled in Shared.ops
|
|
// api.addTag // handled in Shared.ops
|
|
// api.updateTag // handled in Shared.ops
|
|
|
|
/*
|
|
------------------------------------------------------------------------
|
|
Spells
|
|
------------------------------------------------------------------------
|
|
*/
|
|
api.cast = function(req, res) {
|
|
var user = res.locals.user;
|
|
var type = req.body.type, target = req.body.target;
|
|
var klass = shared.content.spells.special[req.params.spell] ? 'special' : user.stats.class
|
|
var spell = shared.content.spells[klass][req.params.spell];
|
|
|
|
var done = function(){
|
|
var err = arguments[0];
|
|
var saved = _.size(arguments == 3) ? arguments[2] : arguments[1];
|
|
if (err) return res.json(500, {err:err});
|
|
res.json(saved);
|
|
}
|
|
|
|
switch (type) {
|
|
case 'task':
|
|
spell.cast(user, user.tasks[target.id]);
|
|
user.save(done);
|
|
break;
|
|
|
|
case 'self':
|
|
spell.cast(user);
|
|
user.save(done);
|
|
break;
|
|
|
|
case 'party':
|
|
case 'user':
|
|
async.waterfall([
|
|
function(cb){
|
|
Group.findOne({type: 'party', members: {'$in': [user._id]}}).populate('members').exec(cb);
|
|
},
|
|
function(group, cb) {
|
|
// Solo player? let's just create a faux group for simpler code
|
|
var g = group ? group : {members:[user]};
|
|
var series = [], found;
|
|
if (type == 'party') {
|
|
spell.cast(user, g.members);
|
|
series = _.transform(g.members, function(m,v,k){
|
|
m.push(function(cb2){v.save(cb2)});
|
|
});
|
|
} else {
|
|
found = _.find(g.members, {_id: target._id})
|
|
spell.cast(user, found);
|
|
series.push(function(cb2){found.save(cb2)});
|
|
}
|
|
|
|
if (group) {
|
|
series.push(function(cb2){
|
|
var message = '`<'+user.profile.name+'> casts '+spell.text + (type=='user' ? ' on @'+found.profile.name : ' for the party')+'.`';
|
|
group.sendChat(message);
|
|
group.save(cb2);
|
|
})
|
|
}
|
|
|
|
async.series(series, cb);
|
|
},
|
|
function(whatever, cb){
|
|
user.save(cb);
|
|
}
|
|
], done);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* All other user.ops which can easily be mapped to habitrpg-shared/index.coffee, not requiring custom API-wrapping
|
|
*/
|
|
_.each(shared.wrap({}).ops, function(op,k){
|
|
if (!api[k]) {
|
|
api[k] = function(req, res, next) {
|
|
res.locals.user.ops[k](req,function(err){
|
|
// If we want to send something other than 500, pass err as {code: 200, message: "Not enough GP"}
|
|
if (err) {
|
|
if (!err.code) return res.json(500,{err:err});
|
|
if (err.code >= 400) return res.json(err.code,{err:err.message});
|
|
// In the case of 200s, they're friendly alert messages like "You're pet has hatched!" - still send the op
|
|
}
|
|
res.locals.user.save(function(err){
|
|
if (err) return res.json(500,{err:err});
|
|
res.send(200);
|
|
})
|
|
})
|
|
}
|
|
}
|
|
})
|
|
|
|
/*
|
|
------------------------------------------------------------------------
|
|
Batch Update
|
|
Run a bunch of updates all at once
|
|
------------------------------------------------------------------------
|
|
*/
|
|
api.batchUpdate = function(req, res, next) {
|
|
if (req.body[0] && req.body[0].data)
|
|
return res.json(400, {err: "API has been updated, please refresh your browser or upgrade your mobile app."})
|
|
|
|
var user = res.locals.user;
|
|
var oldSend = res.send;
|
|
var oldJson = res.json;
|
|
|
|
var callOp = function(_req, cb) {
|
|
res.send = res.json = function(code, data) {
|
|
if (_.isNumber(code) && code >= 400)
|
|
console.error({code: code, data: data});
|
|
//FIXME send error messages down
|
|
return cb();
|
|
};
|
|
api[_req.op](_req, res);
|
|
};
|
|
|
|
// Setup the array of functions we're going to call in parallel with async
|
|
var ops = _.transform(req.body || [], function(result, _req) {
|
|
if (!_.isEmpty(_req)) {
|
|
result.push(function(cb) {
|
|
callOp(_req, cb);
|
|
});
|
|
}
|
|
});
|
|
|
|
// call all the operations, then return the user object to the requester
|
|
async.series(ops, function(err) {
|
|
res.json = oldJson;
|
|
res.send = oldSend;
|
|
if (err) return res.json(500, {err: err});
|
|
var response = user.toJSON();
|
|
response.wasModified = res.locals.wasModified;
|
|
if (response._tmp && response._tmp.drop){
|
|
res.json(200, {_tmp: {drop: response._tmp.drop}, _v: response._v});
|
|
}else if(response.wasModified){
|
|
res.json(200, response);
|
|
}else{
|
|
res.json(200, {_v: response._v});
|
|
}
|
|
});
|
|
}; |