mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-19 07:37:25 +01:00
Deleted website/src
This commit is contained in:
@@ -1,307 +0,0 @@
|
|||||||
var _ = require('lodash');
|
|
||||||
var validator = require('validator');
|
|
||||||
var passport = require('passport');
|
|
||||||
var shared = require('../../../common');
|
|
||||||
var async = require('async');
|
|
||||||
var utils = require('../utils');
|
|
||||||
var nconf = require('nconf');
|
|
||||||
var request = require('request');
|
|
||||||
var User = require('../models/user').model;
|
|
||||||
var ga = require('./../utils').ga;
|
|
||||||
var i18n = require('./../i18n');
|
|
||||||
|
|
||||||
var isProd = nconf.get('NODE_ENV') === 'production';
|
|
||||||
|
|
||||||
var api = module.exports;
|
|
||||||
|
|
||||||
var NO_TOKEN_OR_UID = { err: "You must include a token and uid (user id) in your request"};
|
|
||||||
var NO_USER_FOUND = {err: "No user found."};
|
|
||||||
var NO_SESSION_FOUND = { err: "You must be logged in." };
|
|
||||||
var accountSuspended = function(uuid){
|
|
||||||
return {
|
|
||||||
err: 'Account has been suspended, please contact leslie@habitrpg.com with your UUID ('+uuid+') for assistance.',
|
|
||||||
code: 'ACCOUNT_SUSPENDED'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// Allow case-insensitive regex searching for Mongo queries. See http://stackoverflow.com/a/3561711/362790
|
|
||||||
var RegexEscape = function(s){
|
|
||||||
return new RegExp('^' + s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') + '$', 'i');
|
|
||||||
}
|
|
||||||
|
|
||||||
api.auth = function(req, res, next) {
|
|
||||||
var uid = req.headers['x-api-user'];
|
|
||||||
var token = req.headers['x-api-key'];
|
|
||||||
if (!(uid && token)) return res.json(401, NO_TOKEN_OR_UID);
|
|
||||||
User.findOne({_id: uid,apiToken: token}, function(err, user) {
|
|
||||||
if (err) return next(err);
|
|
||||||
if (_.isEmpty(user)) return res.json(401, NO_USER_FOUND);
|
|
||||||
if (user.auth.blocked) return res.json(401, accountSuspended(user._id));
|
|
||||||
|
|
||||||
res.locals.wasModified = req.query._v ? +user._v !== +req.query._v : true;
|
|
||||||
res.locals.user = user;
|
|
||||||
req.session.userId = user._id;
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
api.authWithSession = function(req, res, next) { //[todo] there is probably a more elegant way of doing this...
|
|
||||||
if (!(req.session && req.session.userId))
|
|
||||||
return res.json(401, NO_SESSION_FOUND);
|
|
||||||
User.findOne({_id: req.session.userId}, function(err, user) {
|
|
||||||
if (err) return next(err);
|
|
||||||
if (_.isEmpty(user)) return res.json(401, NO_USER_FOUND);
|
|
||||||
res.locals.user = user;
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
api.authWithUrl = function(req, res, next) {
|
|
||||||
User.findOne({_id:req.query._id, apiToken:req.query.apiToken}, function(err,user){
|
|
||||||
if (err) return next(err);
|
|
||||||
if (_.isEmpty(user)) return res.json(401, NO_USER_FOUND);
|
|
||||||
res.locals.user = user;
|
|
||||||
next();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
api.registerUser = function(req, res, next) {
|
|
||||||
async.auto({
|
|
||||||
validate: function(cb) {
|
|
||||||
if (!(req.body.username && req.body.password && req.body.email))
|
|
||||||
return cb({code:401, err: ":username, :email, :password, :confirmPassword required"});
|
|
||||||
if (req.body.password !== req.body.confirmPassword)
|
|
||||||
return cb({code:401, err: ":password and :confirmPassword don't match"});
|
|
||||||
if (!validator.isEmail(req.body.email))
|
|
||||||
return cb({code:401, err: ":email invalid"});
|
|
||||||
cb();
|
|
||||||
},
|
|
||||||
findEmail: function(cb) {
|
|
||||||
User.findOne({'auth.local.email': RegexEscape(req.body.email)}, {_id:1}, cb);
|
|
||||||
},
|
|
||||||
findUname: function(cb) {
|
|
||||||
User.findOne({'auth.local.username': RegexEscape(req.body.username)}, {_id:1}, cb);
|
|
||||||
},
|
|
||||||
findFacebook: function(cb){
|
|
||||||
User.findOne({_id: req.headers['x-api-user'], apiToken: req.headers['x-api-key']}, {auth:1}, cb);
|
|
||||||
},
|
|
||||||
register: ['validate', 'findEmail', 'findUname', 'findFacebook', function(cb, data) {
|
|
||||||
if (data.findEmail) return cb({code:401, err:"Email already taken"});
|
|
||||||
if (data.findUname) return cb({code:401, err:"Username already taken"});
|
|
||||||
var salt = utils.makeSalt();
|
|
||||||
var newUser = {
|
|
||||||
auth: {
|
|
||||||
local: {
|
|
||||||
username: req.body.username,
|
|
||||||
email: req.body.email,
|
|
||||||
salt: salt,
|
|
||||||
hashed_password: utils.encryptPassword(req.body.password, salt)
|
|
||||||
},
|
|
||||||
timestamps: {created: +new Date(), loggedIn: +new Date()}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// existing user, allow them to add local authentication
|
|
||||||
if (data.findFacebook) {
|
|
||||||
data.findFacebook.auth.local = newUser.auth.local;
|
|
||||||
data.findFacebook.save(cb);
|
|
||||||
// new user, register them
|
|
||||||
} else {
|
|
||||||
newUser.preferences = newUser.preferences || {};
|
|
||||||
newUser.preferences.language = req.language; // User language detected from browser, not saved
|
|
||||||
var user = new User(newUser);
|
|
||||||
utils.txnEmail(user, 'welcome');
|
|
||||||
ga.event('register', 'Local').send();
|
|
||||||
user.save(cb);
|
|
||||||
}
|
|
||||||
}]
|
|
||||||
}, function(err, data) {
|
|
||||||
if (err) return err.code ? res.json(err.code, err) : next(err);
|
|
||||||
res.json(200, data.register[0]);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
Register new user with uname / password
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
api.loginLocal = function(req, res, next) {
|
|
||||||
var username = req.body.username;
|
|
||||||
var password = req.body.password;
|
|
||||||
if (!(username && password)) return res.json(401, {err:'Missing :username or :password in request body, please provide both'});
|
|
||||||
var login = validator.isEmail(username) ? {'auth.local.email':username} : {'auth.local.username':username};
|
|
||||||
User.findOne(login, {auth:1}, function(err, user){
|
|
||||||
if (err) return next(err);
|
|
||||||
if (!user) return res.json(401, {err:"Username or password incorrect. Click 'Forgot Password' for help with either. (Note: usernames are case-sensitive)"});
|
|
||||||
if (user.auth.blocked) return res.json(401, accountSuspended(user._id));
|
|
||||||
// We needed the whole user object first so we can get his salt to encrypt password comparison
|
|
||||||
User.findOne(
|
|
||||||
{$and: [login, {'auth.local.hashed_password': utils.encryptPassword(password, user.auth.local.salt)}]}
|
|
||||||
, {_id:1, apiToken:1}
|
|
||||||
, function(err, user){
|
|
||||||
if (err) return next(err);
|
|
||||||
if (!user) return res.json(401,{err:"Username or password incorrect. Click 'Forgot Password' for help with either. (Note: usernames are case-sensitive)"});
|
|
||||||
res.json({id: user._id,token: user.apiToken});
|
|
||||||
password = null;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
POST /user/auth/social
|
|
||||||
*/
|
|
||||||
api.loginSocial = function(req, res, next) {
|
|
||||||
var access_token = req.body.authResponse.access_token,
|
|
||||||
network = req.body.network;
|
|
||||||
if (network!=='facebook')
|
|
||||||
return res.json(401, {err:"Only Facebook supported currently."});
|
|
||||||
async.auto({
|
|
||||||
profile: function (cb) {
|
|
||||||
passport._strategies[network].userProfile(access_token, cb);
|
|
||||||
},
|
|
||||||
user: ['profile', function (cb, results) {
|
|
||||||
var q = {};
|
|
||||||
q['auth.' + network + '.id'] = results.profile.id;
|
|
||||||
User.findOne(q, {_id: 1, apiToken: 1, auth: 1}, cb);
|
|
||||||
}],
|
|
||||||
register: ['profile', 'user', function (cb, results) {
|
|
||||||
if (results.user) return cb(null, results.user);
|
|
||||||
// Create new user
|
|
||||||
var prof = results.profile;
|
|
||||||
var user = {
|
|
||||||
preferences: {
|
|
||||||
language: req.language // User language detected from browser, not saved
|
|
||||||
},
|
|
||||||
auth: {
|
|
||||||
timestamps: {created: +new Date(), loggedIn: +new Date()}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
user.auth[network] = prof;
|
|
||||||
user = new User(user);
|
|
||||||
user.save(cb);
|
|
||||||
|
|
||||||
utils.txnEmail(user, 'welcome');
|
|
||||||
ga.event('register', network).send();
|
|
||||||
}]
|
|
||||||
}, function(err, results){
|
|
||||||
if (err) return res.json(401, {err: err.toString ? err.toString() : err});
|
|
||||||
var acct = results.register[0] ? results.register[0] : results.register;
|
|
||||||
if (acct.auth.blocked) return res.json(401, accountSuspended(acct._id));
|
|
||||||
return res.json(200, {id:acct._id, token:acct.apiToken});
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DELETE /user/auth/social
|
|
||||||
*/
|
|
||||||
api.deleteSocial = function(req,res,next){
|
|
||||||
if (!res.locals.user.auth.local.username)
|
|
||||||
return res.json(401, {err:"Account lacks another authentication method, can't detach Facebook"});
|
|
||||||
//FIXME for some reason, the following gives https://gist.github.com/lefnire/f93eb306069b9089d123
|
|
||||||
//res.locals.user.auth.facebook = null;
|
|
||||||
//res.locals.user.auth.save(function(err, saved){
|
|
||||||
User.update({_id:res.locals.user._id}, {$unset:{'auth.facebook':1}}, function(err){
|
|
||||||
if (err) return next(err);
|
|
||||||
res.send(200);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
api.resetPassword = function(req, res, next){
|
|
||||||
var email = req.body.email,
|
|
||||||
salt = utils.makeSalt(),
|
|
||||||
newPassword = utils.makeSalt(), // use a salt as the new password too (they'll change it later)
|
|
||||||
hashed_password = utils.encryptPassword(newPassword, salt);
|
|
||||||
|
|
||||||
User.findOne({'auth.local.email': RegexEscape(email)}, function(err, user){
|
|
||||||
if (err) return next(err);
|
|
||||||
if (!user) return res.send(500, {err:"Couldn't find a user registered for email " + email});
|
|
||||||
user.auth.local.salt = salt;
|
|
||||||
user.auth.local.hashed_password = hashed_password;
|
|
||||||
utils.txnEmail(user, 'reset-password', [
|
|
||||||
{name: "NEW_PASSWORD", content: newPassword},
|
|
||||||
{name: "USERNAME", content: user.auth.local.username}
|
|
||||||
]);
|
|
||||||
user.save(function(err){
|
|
||||||
if(err) return next(err);
|
|
||||||
res.send('New password sent to '+ email);
|
|
||||||
email = salt = newPassword = hashed_password = null;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var invalidPassword = function(user, password){
|
|
||||||
var hashed_password = utils.encryptPassword(password, user.auth.local.salt);
|
|
||||||
if (hashed_password !== user.auth.local.hashed_password)
|
|
||||||
return {code:401, err:"Incorrect password"};
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
api.changeUsername = function(req, res, next) {
|
|
||||||
async.waterfall([
|
|
||||||
function(cb){
|
|
||||||
User.findOne({'auth.local.username': RegexEscape(req.body.username)}, {auth:1}, cb);
|
|
||||||
},
|
|
||||||
function(found, cb){
|
|
||||||
if (found) return cb({code:401, err: "Username already taken"});
|
|
||||||
if (invalidPassword(res.locals.user, req.body.password)) return cb(invalidPassword(res.locals.user, req.body.password));
|
|
||||||
res.locals.user.auth.local.username = req.body.username;
|
|
||||||
res.locals.user.save(cb);
|
|
||||||
}
|
|
||||||
], function(err){
|
|
||||||
if (err) return err.code ? res.json(err.code, err) : next(err);
|
|
||||||
res.send(200);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
api.changeEmail = function(req, res, next){
|
|
||||||
async.waterfall([
|
|
||||||
function(cb){
|
|
||||||
User.findOne({'auth.local.email': RegexEscape(req.body.email)}, {auth:1}, cb);
|
|
||||||
},
|
|
||||||
function(found, cb){
|
|
||||||
if(found) return cb({code:401, err: "Email already taken"});
|
|
||||||
if (invalidPassword(res.locals.user, req.body.password)) return cb(invalidPassword(res.locals.user, req.body.password));
|
|
||||||
res.locals.user.auth.local.email = req.body.email;
|
|
||||||
res.locals.user.save(cb);
|
|
||||||
}
|
|
||||||
], function(err){
|
|
||||||
if (err) return err.code ? res.json(err.code,err) : next(err);
|
|
||||||
res.send(200);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
api.changePassword = function(req, res, next) {
|
|
||||||
var user = res.locals.user,
|
|
||||||
oldPassword = req.body.oldPassword,
|
|
||||||
newPassword = req.body.newPassword,
|
|
||||||
confirmNewPassword = req.body.confirmNewPassword;
|
|
||||||
|
|
||||||
if (newPassword != confirmNewPassword)
|
|
||||||
return res.json(401, {err: "Password & Confirm don't match"});
|
|
||||||
|
|
||||||
var salt = user.auth.local.salt,
|
|
||||||
hashed_old_password = utils.encryptPassword(oldPassword, salt),
|
|
||||||
hashed_new_password = utils.encryptPassword(newPassword, salt);
|
|
||||||
|
|
||||||
if (hashed_old_password !== user.auth.local.hashed_password)
|
|
||||||
return res.json(401, {err:"Old password doesn't match"});
|
|
||||||
|
|
||||||
user.auth.local.hashed_password = hashed_new_password;
|
|
||||||
user.save(function(err, saved){
|
|
||||||
if (err) next(err);
|
|
||||||
res.send(200);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Registers a new user. Only accepting username/password registrations, no Facebook
|
|
||||||
*/
|
|
||||||
|
|
||||||
api.setupPassport = function(router) {
|
|
||||||
|
|
||||||
router.get('/logout', i18n.getUserLanguage, function(req, res) {
|
|
||||||
req.logout();
|
|
||||||
delete req.session.userId;
|
|
||||||
res.redirect('/');
|
|
||||||
})
|
|
||||||
|
|
||||||
};
|
|
||||||
@@ -1,431 +0,0 @@
|
|||||||
// @see ../routes for routing
|
|
||||||
|
|
||||||
var _ = require('lodash');
|
|
||||||
var nconf = require('nconf');
|
|
||||||
var async = require('async');
|
|
||||||
var shared = require('../../../common');
|
|
||||||
var User = require('./../models/user').model;
|
|
||||||
var Group = require('./../models/group').model;
|
|
||||||
var Challenge = require('./../models/challenge').model;
|
|
||||||
var logging = require('./../logging');
|
|
||||||
var csv = require('express-csv');
|
|
||||||
var utils = require('../utils');
|
|
||||||
var api = module.exports;
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
Challenges
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
api.list = function(req, res, next) {
|
|
||||||
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
|
|
||||||
],
|
|
||||||
_id:{$ne:'95533e05-1ff9-4e46-970b-d77219f199e9'} // remove the Spread the Word Challenge for now, will revisit when we fix the closing-challenge bug
|
|
||||||
})
|
|
||||||
.select('name leader description group memberCount prize official')
|
|
||||||
.select({members:{$elemMatch:{$in:[user._id]}}})
|
|
||||||
.sort('-official -timestamp')
|
|
||||||
.populate('group', '_id name')
|
|
||||||
.populate('leader', 'profile.name')
|
|
||||||
.exec(cb);
|
|
||||||
}
|
|
||||||
], function(err, challenges){
|
|
||||||
if (err) return next(err);
|
|
||||||
_.each(challenges, function(c){
|
|
||||||
c._isMember = c.members.length > 0;
|
|
||||||
})
|
|
||||||
res.json(challenges);
|
|
||||||
user = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// GET
|
|
||||||
api.get = function(req, res, next) {
|
|
||||||
// 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 next(err);
|
|
||||||
if (!challenge) return res.json(404, {err: 'Challenge ' + req.params.cid + ' not found'});
|
|
||||||
res.json(challenge);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
api.csv = function(req, res, next) {
|
|
||||||
var cid = req.params.cid;
|
|
||||||
var challenge;
|
|
||||||
async.waterfall([
|
|
||||||
function(cb){
|
|
||||||
Challenge.findById(cid,cb)
|
|
||||||
},
|
|
||||||
function(_challenge,cb) {
|
|
||||||
challenge = _challenge;
|
|
||||||
if (!challenge) return cb('Challenge ' + cid + ' not found');
|
|
||||||
User.aggregate([
|
|
||||||
{$match:{'_id':{ '$in': challenge.members}}}, //yes, we want members
|
|
||||||
{$project:{'profile.name':1,tasks:{$setUnion:["$habits","$dailys","$todos","$rewards"]}}},
|
|
||||||
{$unwind:"$tasks"},
|
|
||||||
{$match:{"tasks.challenge.id":cid}},
|
|
||||||
{$sort:{'tasks.type':1,'tasks.id':1}},
|
|
||||||
{$group:{_id:"$_id", "tasks":{$push:"$tasks"},"name":{$first:"$profile.name"}}}
|
|
||||||
], cb);
|
|
||||||
}
|
|
||||||
],function(err,users){
|
|
||||||
if(err) return next(err);
|
|
||||||
var output = ['UUID','name'];
|
|
||||||
_.each(challenge.tasks,function(t){
|
|
||||||
//output.push(t.type+':'+t.text);
|
|
||||||
//not the right order yet
|
|
||||||
output.push('Task');
|
|
||||||
output.push('Value');
|
|
||||||
output.push('Notes');
|
|
||||||
})
|
|
||||||
output = [output];
|
|
||||||
_.each(users, function(u){
|
|
||||||
var uData = [u._id,u.name];
|
|
||||||
_.each(u.tasks,function(t){
|
|
||||||
uData = uData.concat([t.type+':'+t.text, t.value, t.notes]);
|
|
||||||
})
|
|
||||||
output.push(uData);
|
|
||||||
});
|
|
||||||
res.header('Content-disposition', 'attachment; filename='+cid+'.csv');
|
|
||||||
res.csv(output);
|
|
||||||
challenge = cid = null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
api.getMember = function(req, res, next) {
|
|
||||||
var cid = req.params.cid;
|
|
||||||
var uid = req.params.uid;
|
|
||||||
|
|
||||||
// We need to start using the aggregation framework instead of in-app filtering, see http://docs.mongodb.org/manual/aggregation/
|
|
||||||
// See code at 32c0e75 for unwind/group example
|
|
||||||
|
|
||||||
//http://stackoverflow.com/questions/24027213/how-to-match-multiple-array-elements-without-using-unwind
|
|
||||||
var proj = {'profile.name':'$profile.name'};
|
|
||||||
_.each(['habits','dailys','todos','rewards'], function(type){
|
|
||||||
proj[type] = {
|
|
||||||
$setDifference: [{
|
|
||||||
$map: {
|
|
||||||
input: '$'+type,
|
|
||||||
as: "el",
|
|
||||||
in: {
|
|
||||||
$cond: [{$eq: ["$$el.challenge.id", cid]}, '$$el', false]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [false]]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
User.aggregate()
|
|
||||||
.match({_id: uid})
|
|
||||||
.project(proj)
|
|
||||||
.exec(function(err, member){
|
|
||||||
if (err) return next(err);
|
|
||||||
if (!member) return res.json(404, {err: 'Member '+uid+' for challenge '+cid+' not found'});
|
|
||||||
res.json(member[0]);
|
|
||||||
uid = cid = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// CREATE
|
|
||||||
api.create = function(req, res, next){
|
|
||||||
var user = res.locals.user;
|
|
||||||
|
|
||||||
async.auto({
|
|
||||||
get_group: function(cb){
|
|
||||||
var q = {_id:req.body.group};
|
|
||||||
if (req.body.group!='habitrpg') q.members = {$in:[user._id]}; // make sure they're a member of the group
|
|
||||||
Group.findOne(q, cb);
|
|
||||||
},
|
|
||||||
save_chal: ['get_group', function(cb, results){
|
|
||||||
var group = results.get_group,
|
|
||||||
prize = +req.body.prize;
|
|
||||||
if (!group)
|
|
||||||
return cb({code:404, err:"Group." + req.body.group + " not found"});
|
|
||||||
if (group.leaderOnly && group.leaderOnly.challenges && group.leader !== user._id)
|
|
||||||
return cb({code:401, err: "Only the group leader can create challenges"});
|
|
||||||
// If they're adding a prize, do some validation
|
|
||||||
if (prize < 0)
|
|
||||||
return cb({code:401, err: 'Challenge prize must be >= 0'});
|
|
||||||
if (req.body.group=='habitrpg' && prize < 1)
|
|
||||||
return cb({code:401, err: 'Prize must be at least 1 Gem for public challenges.'});
|
|
||||||
if (prize > 0) {
|
|
||||||
var groupBalance = ((group.balance && group.leader==user._id) ? group.balance : 0);
|
|
||||||
var prizeCost = prize/4; // I really should have stored user.balance as gems rather than dollars... stupid...
|
|
||||||
if (prizeCost > user.balance + groupBalance)
|
|
||||||
return cb("You can't afford this prize. Purchase more gems or lower the prize amount.")
|
|
||||||
|
|
||||||
if (groupBalance >= prizeCost) {
|
|
||||||
// Group pays for all of prize
|
|
||||||
group.balance -= prizeCost;
|
|
||||||
} else if (groupBalance > 0) {
|
|
||||||
// User pays remainder of prize cost after group
|
|
||||||
var remainder = prizeCost - group.balance;
|
|
||||||
group.balance = 0;
|
|
||||||
user.balance -= remainder;
|
|
||||||
} else {
|
|
||||||
// User pays for all of prize
|
|
||||||
user.balance -= prizeCost;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
req.body.leader = user._id;
|
|
||||||
req.body.official = user.contributor.admin && req.body.official;
|
|
||||||
var chal = new Challenge(req.body); // FIXME sanitize
|
|
||||||
chal.members.push(user._id);
|
|
||||||
chal.save(cb);
|
|
||||||
}],
|
|
||||||
save_group: ['save_chal', function(cb, results){
|
|
||||||
results.get_group.challenges.push(results.save_chal[0]._id);
|
|
||||||
results.get_group.save(cb);
|
|
||||||
}],
|
|
||||||
sync_user: ['save_group', function(cb, results){
|
|
||||||
// Auto-join creator to challenge (see members.push above)
|
|
||||||
results.save_chal[0].syncToUser(user, cb);
|
|
||||||
}]
|
|
||||||
}, function(err, results){
|
|
||||||
if (err) return err.code? res.json(err.code, err) : next(err);
|
|
||||||
return res.json(results.save_chal[0]);
|
|
||||||
user = null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UPDATE
|
|
||||||
api.update = function(req, res, next){
|
|
||||||
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) {
|
|
||||||
|
|
||||||
// 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){
|
|
||||||
logging.info('Challenge updated, sync to subscribers');
|
|
||||||
if (err) throw err;
|
|
||||||
_.each(users, function(user){
|
|
||||||
saved.syncToUser(user);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// after saving, we're done as far as the client's concerned. We kick off syncing (heavy task) in the background
|
|
||||||
cb(null, saved);
|
|
||||||
}
|
|
||||||
], function(err, saved){
|
|
||||||
if(err) next(err);
|
|
||||||
res.json(saved);
|
|
||||||
cid = user = before = null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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);
|
|
||||||
removed = null;
|
|
||||||
}
|
|
||||||
], cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete & close
|
|
||||||
*/
|
|
||||||
api['delete'] = function(req, res, next){
|
|
||||||
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 next(err);
|
|
||||||
res.send(200);
|
|
||||||
user = cid = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select Winner & Close
|
|
||||||
*/
|
|
||||||
api.selectWinner = function(req, res, next) {
|
|
||||||
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) {
|
|
||||||
if(saved.preferences.emailNotifications.wonChallenge !== false){
|
|
||||||
utils.txnEmail(saved, 'won-challenge', [
|
|
||||||
{name: 'CHALLENGE_NAME', content: chal.name}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
closeChal(cid, {broken: 'CHALLENGE_CLOSED', winner: saved.profile.name}, cb);
|
|
||||||
}
|
|
||||||
], function(err){
|
|
||||||
if (err) return next(err);
|
|
||||||
res.send(200);
|
|
||||||
user = cid = chal = null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
api.join = function(req, res, next){
|
|
||||||
var user = res.locals.user;
|
|
||||||
var cid = req.params.cid;
|
|
||||||
|
|
||||||
async.waterfall([
|
|
||||||
function(cb) {
|
|
||||||
Challenge.findByIdAndUpdate(cid, {$addToSet:{members:user._id}}, cb);
|
|
||||||
},
|
|
||||||
function(chal, cb) {
|
|
||||||
|
|
||||||
// Trigger updating challenge member count in the background. We can't do it above because we don't have
|
|
||||||
// _.size(challenge.members). We can't do it in pre(save) because we're calling findByIdAndUpdate above.
|
|
||||||
Challenge.update({_id:cid}, {$set:{memberCount:_.size(chal.members)}}).exec();
|
|
||||||
|
|
||||||
if (!~user.challenges.indexOf(cid))
|
|
||||||
user.challenges.unshift(cid);
|
|
||||||
// Add all challenge's tasks to user's tasks
|
|
||||||
chal.syncToUser(user, function(err){
|
|
||||||
if (err) return cb(err);
|
|
||||||
cb(null, chal); // we want the saved challenge in the return results, due to ng-resource
|
|
||||||
});
|
|
||||||
}
|
|
||||||
], function(err, chal){
|
|
||||||
if(err) return next(err);
|
|
||||||
chal._isMember = true;
|
|
||||||
res.json(chal);
|
|
||||||
user = cid = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
api.leave = function(req, res, next){
|
|
||||||
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){
|
|
||||||
|
|
||||||
// Trigger updating challenge member count in the background. We can't do it above because we don't have
|
|
||||||
// _.size(challenge.members). We can't do it in pre(save) because we're calling findByIdAndUpdate above.
|
|
||||||
if (chal)
|
|
||||||
Challenge.update({_id:cid}, {$set:{memberCount:_.size(chal.members)}}).exec();
|
|
||||||
|
|
||||||
var i = user.challenges.indexOf(cid)
|
|
||||||
if (~i) user.challenges.splice(i,1);
|
|
||||||
user.unlink({cid:cid, keep:keep}, function(err){
|
|
||||||
if (err) return cb(err);
|
|
||||||
cb(null, chal);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
], function(err, chal){
|
|
||||||
if(err) return next(err);
|
|
||||||
if (chal) chal._isMember = false;
|
|
||||||
res.json(chal);
|
|
||||||
user = cid = keep = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 next(err);
|
|
||||||
res.send(200);
|
|
||||||
user = tid = cid = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
var _ = require('lodash');
|
|
||||||
var Coupon = require('./../models/coupon').model;
|
|
||||||
var api = module.exports;
|
|
||||||
var csv = require('express-csv');
|
|
||||||
var async = require('async');
|
|
||||||
|
|
||||||
api.ensureAdmin = function(req, res, next) {
|
|
||||||
if (!res.locals.user.contributor.sudo) return res.json(401, {err:"You don't have admin access"});
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
api.generateCoupons = function(req,res,next) {
|
|
||||||
Coupon.generate(req.params.event, req.query.count, function(err){
|
|
||||||
if(err) return next(err);
|
|
||||||
res.send(200);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
api.getCoupons = function(req,res,next) {
|
|
||||||
var options = {sort:'seq'};
|
|
||||||
if (req.query.limit) options.limit = req.query.limit;
|
|
||||||
if (req.query.skip) options.skip = req.query.skip;
|
|
||||||
Coupon.find({},{}, options, function(err,coupons){
|
|
||||||
//res.header('Content-disposition', 'attachment; filename=coupons.csv');
|
|
||||||
res.csv([['code']].concat(_.map(coupons, function(c){
|
|
||||||
return [c._id];
|
|
||||||
})));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
api.enterCode = function(req,res,next) {
|
|
||||||
Coupon.apply(res.locals.user,req.params.code,function(err,user){
|
|
||||||
if (err) return res.json(400,{err:err});
|
|
||||||
res.json(user);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
var _ = require('lodash');
|
|
||||||
var csv = require('express-csv');
|
|
||||||
var express = require('express');
|
|
||||||
var nconf = require('nconf');
|
|
||||||
var moment = require('moment');
|
|
||||||
var dataexport = module.exports;
|
|
||||||
var js2xmlparser = require("js2xmlparser");
|
|
||||||
var pd = require('pretty-data').pd;
|
|
||||||
var User = require('../models/user').model;
|
|
||||||
|
|
||||||
// Avatar screenshot/static-page includes
|
|
||||||
var Pageres = require('pageres'); //https://github.com/sindresorhus/pageres
|
|
||||||
var AWS = require('aws-sdk');
|
|
||||||
AWS.config.update({accessKeyId: nconf.get("S3:accessKeyId"), secretAccessKey: nconf.get("S3:secretAccessKey")});
|
|
||||||
var s3Stream = require('s3-upload-stream')(new AWS.S3()); //https://github.com/nathanpeck/s3-upload-stream
|
|
||||||
var bucket = nconf.get("S3:bucket");
|
|
||||||
var request = require('request');
|
|
||||||
|
|
||||||
/*
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
Data export
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
dataexport.history = function(req, res) {
|
|
||||||
var user = res.locals.user;
|
|
||||||
var output = [
|
|
||||||
["Task Name", "Task ID", "Task Type", "Date", "Value"]
|
|
||||||
];
|
|
||||||
_.each(user.tasks, function(task) {
|
|
||||||
_.each(task.history, function(history) {
|
|
||||||
output.push(
|
|
||||||
[task.text, task.id, task.type, moment(history.date).format("MM-DD-YYYY HH:mm:ss"), history.value]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return res.csv(output);
|
|
||||||
}
|
|
||||||
|
|
||||||
var userdata = function(user) {
|
|
||||||
if(user.auth && user.auth.local) {
|
|
||||||
delete user.auth.local.salt;
|
|
||||||
delete user.auth.local.hashed_password;
|
|
||||||
}
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
dataexport.leanuser = function(req, res, next) {
|
|
||||||
User.findOne({_id: res.locals.user._id}).lean().exec(function(err, user) {
|
|
||||||
if (err) return res.json(500, {err: err});
|
|
||||||
if (_.isEmpty(user)) return res.json(401, NO_USER_FOUND);
|
|
||||||
res.locals.user = user;
|
|
||||||
return next();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
dataexport.userdata = {
|
|
||||||
xml: function(req, res) {
|
|
||||||
var user = userdata(res.locals.user);
|
|
||||||
return res.xml({data: JSON.stringify(user), rootname: 'user'});
|
|
||||||
},
|
|
||||||
json: function(req, res) {
|
|
||||||
var user = userdata(res.locals.user);
|
|
||||||
return res.jsonstring(user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
Express Extensions (should be refactored into a module)
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
var expressres = express.response || http.ServerResponse.prototype;
|
|
||||||
|
|
||||||
expressres.xml = function(obj, headers, status) {
|
|
||||||
var body = '';
|
|
||||||
this.charset = this.charset || 'utf-8';
|
|
||||||
this.header('Content-Type', 'text/xml');
|
|
||||||
this.header('Content-Disposition', 'attachment');
|
|
||||||
body = pd.xml(js2xmlparser(obj.rootname,obj.data));
|
|
||||||
return this.send(body, headers, status);
|
|
||||||
};
|
|
||||||
|
|
||||||
expressres.jsonstring = function(obj, headers, status) {
|
|
||||||
var body = '';
|
|
||||||
this.charset = this.charset || 'utf-8';
|
|
||||||
this.header('Content-Type', 'application/json');
|
|
||||||
this.header('Content-Disposition', 'attachment');
|
|
||||||
body = pd.json(JSON.stringify(obj));
|
|
||||||
return this.send(body, headers, status);
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
Static page and image screenshot of avatar
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
dataexport.avatarPage = function(req, res) {
|
|
||||||
User.findById(req.params.uuid).select('stats profile items achievements preferences backer contributor').exec(function(err, user){
|
|
||||||
res.render('avatar-static', {
|
|
||||||
title: user.profile.name,
|
|
||||||
env: _.defaults({user:user},res.locals.habitrpg)
|
|
||||||
});
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
dataexport.avatarImage = function(req, res, next) {
|
|
||||||
var filename = 'avatar-'+req.params.uuid+'.png';
|
|
||||||
request.head('https://'+bucket+'.s3.amazonaws.com/'+filename, function(err,response,body) {
|
|
||||||
// cache images for 10 minutes on aws, else upload a new one
|
|
||||||
if (response.statusCode==200 && moment().diff(response.headers['last-modified'], 'minutes') < 10)
|
|
||||||
return res.redirect(301, 'https://' + bucket + '.s3.amazonaws.com/' + filename);
|
|
||||||
new Pageres()//{delay:1}
|
|
||||||
.src(nconf.get('BASE_URL') + '/export/avatar-' + req.params.uuid + '.html', ['140x147'], {crop: true, filename: filename.replace('.png', '')})
|
|
||||||
.run(function (err, file) {
|
|
||||||
if (err) return next(err);
|
|
||||||
// see http://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#createMultipartUpload-property
|
|
||||||
var upload = s3Stream.upload({
|
|
||||||
Bucket: bucket,
|
|
||||||
Key: filename,
|
|
||||||
ACL: "public-read",
|
|
||||||
StorageClass: "REDUCED_REDUNDANCY",
|
|
||||||
ContentType: "image/png",
|
|
||||||
Expires: +moment().add({minutes: 3})
|
|
||||||
});
|
|
||||||
upload.on('error', function (err) {
|
|
||||||
next(err);
|
|
||||||
});
|
|
||||||
upload.on('uploaded', function (details) {
|
|
||||||
res.redirect(details.Location);
|
|
||||||
});
|
|
||||||
file[0].pipe(upload);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
};
|
|
||||||
@@ -1,866 +0,0 @@
|
|||||||
// @see ../routes for routing
|
|
||||||
|
|
||||||
function clone(a) {
|
|
||||||
return JSON.parse(JSON.stringify(a));
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ = require('lodash');
|
|
||||||
var nconf = require('nconf');
|
|
||||||
var async = require('async');
|
|
||||||
var utils = require('./../utils');
|
|
||||||
var shared = require('../../../common');
|
|
||||||
var User = require('./../models/user').model;
|
|
||||||
var Group = require('./../models/group').model;
|
|
||||||
var Challenge = require('./../models/challenge').model;
|
|
||||||
var isProd = nconf.get('NODE_ENV') === 'production';
|
|
||||||
var api = module.exports;
|
|
||||||
|
|
||||||
/*
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
Groups
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
var partyFields = api.partyFields = 'profile preferences stats achievements party backer contributor auth.timestamps items';
|
|
||||||
var nameFields = 'profile.name';
|
|
||||||
var challengeFields = '_id name';
|
|
||||||
var guildPopulate = {path: 'members', select: nameFields, options: {limit: 15} };
|
|
||||||
/**
|
|
||||||
* For parties, we want a lot of member details so we can show their avatars in the header. For guilds, we want very
|
|
||||||
* limited fields - and only a sampling of the members, beacuse they can be in the thousands
|
|
||||||
* @param type: 'party' or otherwise
|
|
||||||
* @param q: the Mongoose query we're building up
|
|
||||||
* @param additionalFields: if we want to populate some additional field not fetched normally
|
|
||||||
* pass it as a string, parties only
|
|
||||||
*/
|
|
||||||
var populateQuery = function(type, q, additionalFields){
|
|
||||||
if (type == 'party')
|
|
||||||
q.populate('members', partyFields + (additionalFields ? (' ' + additionalFields) : ''));
|
|
||||||
else
|
|
||||||
q.populate(guildPopulate);
|
|
||||||
q.populate('invites', nameFields);
|
|
||||||
q.populate({
|
|
||||||
path: 'challenges',
|
|
||||||
match: (type=='habitrpg') ? {_id:{$ne:'95533e05-1ff9-4e46-970b-d77219f199e9'}} : undefined, // remove the Spread the Word Challenge for now, will revisit when we fix the closing-challenge bug
|
|
||||||
select: challengeFields,
|
|
||||||
options: {sort: {official: -1, timestamp: -1}}
|
|
||||||
});
|
|
||||||
return q;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch groups list. This no longer returns party or tavern, as those can be requested indivdually
|
|
||||||
* as /groups/party or /groups/tavern
|
|
||||||
*/
|
|
||||||
api.list = function(req, res, next) {
|
|
||||||
var user = res.locals.user;
|
|
||||||
var groupFields = 'name description memberCount balance leader';
|
|
||||||
var sort = '-memberCount';
|
|
||||||
var type = req.query.type || 'party,guilds,public,tavern';
|
|
||||||
|
|
||||||
async.parallel({
|
|
||||||
|
|
||||||
// unecessary given our ui-router setup
|
|
||||||
party: function(cb){
|
|
||||||
if (!~type.indexOf('party')) return cb(null, {});
|
|
||||||
Group.findOne({type: 'party', members: {'$in': [user._id]}})
|
|
||||||
.select(groupFields).exec(function(err, party){
|
|
||||||
if (err) return cb(err);
|
|
||||||
cb(null, (party === null ? [] : [party])); // return as an array for consistent ngResource use
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
guilds: function(cb) {
|
|
||||||
if (!~type.indexOf('guilds')) return cb(null, []);
|
|
||||||
Group.find({members: {'$in': [user._id]}, type:'guild'})
|
|
||||||
.select(groupFields).sort(sort).exec(cb);
|
|
||||||
},
|
|
||||||
|
|
||||||
'public': function(cb) {
|
|
||||||
if (!~type.indexOf('public')) return cb(null, []);
|
|
||||||
Group.find({privacy: 'public'})
|
|
||||||
.select(groupFields + ' members')
|
|
||||||
.sort(sort)
|
|
||||||
.exec(function(err, groups){
|
|
||||||
if (err) return cb(err);
|
|
||||||
_.each(groups, function(g){
|
|
||||||
// To save some client-side performance, don't send down the full members arr, just send down temp var _isMember
|
|
||||||
if (~g.members.indexOf(user._id)) g._isMember = true;
|
|
||||||
g.members = undefined;
|
|
||||||
});
|
|
||||||
cb(null, groups);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
// unecessary given our ui-router setup
|
|
||||||
tavern: function(cb) {
|
|
||||||
if (!~type.indexOf('tavern')) return cb(null, {});
|
|
||||||
Group.findById('habitrpg').select(groupFields).exec(function(err, tavern){
|
|
||||||
if (err) return cb(err);
|
|
||||||
cb(null, [tavern]); // return as an array for consistent ngResource use
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
}, function(err, results){
|
|
||||||
if (err) return next(err);
|
|
||||||
// ngResource expects everything as arrays. We used to send it down as a structured object: {public:[], party:{}, guilds:[], tavern:{}}
|
|
||||||
// but unfortunately ngResource top-level attrs are considered the ngModels in the list, so we had to do weird stuff and multiple
|
|
||||||
// requests to get it to work properly. Instead, we're not depending on the client to do filtering / organization, and we're
|
|
||||||
// just sending down a merged array. Revisit
|
|
||||||
var arr = _.reduce(results, function(m,v){
|
|
||||||
if (_.isEmpty(v)) return m;
|
|
||||||
return m.concat(_.isArray(v) ? v : [v]);
|
|
||||||
}, [])
|
|
||||||
res.json(arr);
|
|
||||||
|
|
||||||
user = groupFields = sort = type = null;
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get group
|
|
||||||
* TODO: implement requesting fields ?fields=chat,members
|
|
||||||
*/
|
|
||||||
api.get = function(req, res, next) {
|
|
||||||
var user = res.locals.user;
|
|
||||||
var gid = req.params.gid;
|
|
||||||
|
|
||||||
var q = (gid == 'party')
|
|
||||||
? Group.findOne({type: 'party', members: {'$in': [user._id]}})
|
|
||||||
: Group.findOne({$or:[
|
|
||||||
{_id:gid, privacy:'public'},
|
|
||||||
{_id:gid, privacy:'private', members: {$in:[user._id]}} // if the group is private, only return if they have access
|
|
||||||
]});
|
|
||||||
populateQuery(gid, q);
|
|
||||||
q.exec(function(err, group){
|
|
||||||
if (err) return next(err);
|
|
||||||
if (!group && gid!=='party') return res.json(404,{err: "Group not found or you don't have access."});
|
|
||||||
res.json(group);
|
|
||||||
gid = null;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
api.create = function(req, res, next) {
|
|
||||||
var group = new Group(req.body);
|
|
||||||
var user = res.locals.user;
|
|
||||||
group.members = [user._id];
|
|
||||||
group.leader = user._id;
|
|
||||||
|
|
||||||
if(group.type === 'guild'){
|
|
||||||
if(user.balance < 1) return res.json(401, {err: 'Not enough gems!'});
|
|
||||||
|
|
||||||
group.balance = 1;
|
|
||||||
user.balance--;
|
|
||||||
|
|
||||||
async.waterfall([
|
|
||||||
function(cb){user.save(cb)},
|
|
||||||
function(saved,ct,cb){group.save(cb)},
|
|
||||||
function(saved,ct,cb){saved.populate('members',nameFields,cb)}
|
|
||||||
],function(err,saved){
|
|
||||||
if (err) return next(err);
|
|
||||||
res.json(saved);
|
|
||||||
group = user = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
}else{
|
|
||||||
async.waterfall([
|
|
||||||
function(cb){
|
|
||||||
Group.findOne({type:'party',members:{$in:[user._id]}},cb);
|
|
||||||
},
|
|
||||||
function(found, cb){
|
|
||||||
if (found) return cb('Already in a party, try refreshing.');
|
|
||||||
group.save(cb);
|
|
||||||
},
|
|
||||||
function(saved, count, cb){
|
|
||||||
saved.populate('members', nameFields, cb);
|
|
||||||
}
|
|
||||||
], function(err, populated){
|
|
||||||
if (err == 'Already in a party, try refreshing.') return res.json(400,{err:err});
|
|
||||||
if (err) return next(err);
|
|
||||||
return res.json(populated);
|
|
||||||
group = user = null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.update = function(req, res, next) {
|
|
||||||
var group = res.locals.group;
|
|
||||||
var user = res.locals.user;
|
|
||||||
|
|
||||||
if(group.leader !== user._id)
|
|
||||||
return res.json(401, {err: "Only the group leader can update the group!"});
|
|
||||||
|
|
||||||
'name description logo logo leaderMessage leader leaderOnly'.split(' ').forEach(function(attr){
|
|
||||||
group[attr] = req.body[attr];
|
|
||||||
});
|
|
||||||
|
|
||||||
group.save(function(err, saved){
|
|
||||||
if (err) return next(err);
|
|
||||||
res.send(204);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
api.attachGroup = function(req, res, next) {
|
|
||||||
var gid = req.params.gid;
|
|
||||||
var q = (gid == 'party') ? Group.findOne({type: 'party', members: {'$in': [res.locals.user._id]}}) : Group.findById(gid);
|
|
||||||
q.exec(function(err, group){
|
|
||||||
if(err) return next(err);
|
|
||||||
if(!group) return res.json(404, {err: "Group not found"});
|
|
||||||
res.locals.group = group;
|
|
||||||
next();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
api.getChat = function(req, res, next) {
|
|
||||||
// TODO: This code is duplicated from api.get - pull it out into a function to remove duplication.
|
|
||||||
var user = res.locals.user;
|
|
||||||
var gid = req.params.gid;
|
|
||||||
var q = (gid == 'party')
|
|
||||||
? Group.findOne({type: 'party', members: {$in:[user._id]}})
|
|
||||||
: Group.findOne({$or:[
|
|
||||||
{_id:gid, privacy:'public'},
|
|
||||||
{_id:gid, privacy:'private', members: {$in:[user._id]}}
|
|
||||||
]});
|
|
||||||
populateQuery(gid, q);
|
|
||||||
q.exec(function(err, group){
|
|
||||||
if (err) return next(err);
|
|
||||||
if (!group && gid!=='party') return res.json(404,{err: "Group not found or you don't have access."});
|
|
||||||
res.json(res.locals.group.chat);
|
|
||||||
gid = null;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO make this it's own ngResource so we don't have to send down group data with each chat post
|
|
||||||
*/
|
|
||||||
api.postChat = function(req, res, next) {
|
|
||||||
var user = res.locals.user
|
|
||||||
var group = res.locals.group;
|
|
||||||
if (group.type!='party' && user.flags.chatRevoked) return res.json(401,{err:'Your chat privileges have been revoked.'});
|
|
||||||
var lastClientMsg = req.query.previousMsg;
|
|
||||||
var chatUpdated = (lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg) ? true : false;
|
|
||||||
|
|
||||||
group.sendChat(req.query.message, user); // FIXME this should be body, but ngResource is funky
|
|
||||||
|
|
||||||
if (group.type === 'party') {
|
|
||||||
user.party.lastMessageSeen = group.chat[0].id;
|
|
||||||
user.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
group.save(function(err, saved){
|
|
||||||
if (err) return next(err);
|
|
||||||
return chatUpdated ? res.json({chat: group.chat}) : res.json({message: saved.chat[0]});
|
|
||||||
group = chatUpdated = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
api.deleteChatMessage = function(req, res, next){
|
|
||||||
var user = res.locals.user
|
|
||||||
var group = res.locals.group;
|
|
||||||
var message = _.find(group.chat, {id: req.params.messageId});
|
|
||||||
|
|
||||||
if(!message) return res.json(404, {err: "Message not found!"});
|
|
||||||
|
|
||||||
if(user._id !== message.uuid && !(user.backer && user.contributor.admin))
|
|
||||||
return res.json(401, {err: "Not authorized to delete this message!"})
|
|
||||||
|
|
||||||
var lastClientMsg = req.query.previousMsg;
|
|
||||||
var chatUpdated = (lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg) ? true : false;
|
|
||||||
|
|
||||||
Group.update({_id:group._id}, {$pull:{chat:{id: req.params.messageId}}}, function(err){
|
|
||||||
if(err) return next(err);
|
|
||||||
chatUpdated ? res.json({chat: group.chat}) : res.send(204);
|
|
||||||
group = chatUpdated = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
api.flagChatMessage = function(req, res, next){
|
|
||||||
var user = res.locals.user
|
|
||||||
var group = res.locals.group;
|
|
||||||
var message = _.find(group.chat, {id: req.params.mid});
|
|
||||||
|
|
||||||
if(!message) return res.json(404, {err: "Message not found!"});
|
|
||||||
if(message.uuid == user._id) return res.json(401, {err: "Can't report your own message."});
|
|
||||||
|
|
||||||
User.findOne({_id: message.uuid}, {auth: 1}, function(err, author){
|
|
||||||
if(err) return next(err);
|
|
||||||
|
|
||||||
// Log user ids that have flagged the message
|
|
||||||
if(!message.flags) message.flags = {};
|
|
||||||
if(message.flags[user._id] && !user.contributor.admin) return res.json(401, {err: "You have already reported this message"});
|
|
||||||
message.flags[user._id] = true;
|
|
||||||
|
|
||||||
// Log total number of flags (publicly viewable)
|
|
||||||
if(!message.flagCount) message.flagCount = 0;
|
|
||||||
if(user.contributor.admin){
|
|
||||||
// Arbitraty amount, higher than 2
|
|
||||||
message.flagCount = 5;
|
|
||||||
} else {
|
|
||||||
message.flagCount++
|
|
||||||
}
|
|
||||||
|
|
||||||
group.markModified('chat');
|
|
||||||
group.save(function(err,_saved){
|
|
||||||
if(err) return next(err);
|
|
||||||
var addressesToSendTo = JSON.parse(nconf.get('FLAG_REPORT_EMAIL'));
|
|
||||||
|
|
||||||
if(Array.isArray(addressesToSendTo)){
|
|
||||||
addressesToSendTo = addressesToSendTo.map(function(email){
|
|
||||||
return {email: email, canSend: true}
|
|
||||||
});
|
|
||||||
}else{
|
|
||||||
addressesToSendTo = {email: addressesToSendTo}
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.txnEmail(addressesToSendTo, 'flag-report-to-mods', [
|
|
||||||
{name: "MESSAGE_TIME", content: (new Date(message.timestamp)).toString()},
|
|
||||||
{name: "MESSAGE_TEXT", content: message.text},
|
|
||||||
|
|
||||||
{name: "REPORTER_USERNAME", content: user.profile.name},
|
|
||||||
{name: "REPORTER_UUID", content: user._id},
|
|
||||||
{name: "REPORTER_EMAIL", content: user.auth.local ? user.auth.local.email : ((user.auth.facebook && user.auth.facebook.emails && user.auth.facebook.emails[0]) ? user.auth.facebook.emails[0].value : null)},
|
|
||||||
{name: "REPORTER_MODAL_URL", content: "https://habitrpg.com/static/front/#?memberId=" + user._id},
|
|
||||||
|
|
||||||
{name: "AUTHOR_USERNAME", content: message.user},
|
|
||||||
{name: "AUTHOR_UUID", content: message.uuid},
|
|
||||||
{name: "AUTHOR_EMAIL", content: author.auth.local ? author.auth.local.email : ((author.auth.facebook && author.auth.facebook.emails && author.auth.facebook.emails[0]) ? author.auth.facebook.emails[0].value : null)},
|
|
||||||
{name: "AUTHOR_MODAL_URL", content: "https://habitrpg.com/static/front/#?memberId=" + message.uuid},
|
|
||||||
|
|
||||||
{name: "GROUP_NAME", content: group.name},
|
|
||||||
{name: "GROUP_TYPE", content: group.type},
|
|
||||||
{name: "GROUP_ID", content: group._id},
|
|
||||||
{name: "GROUP_URL", content: group._id == 'habitrpg' ? (nconf.get('BASE_URL') + '/#/options/groups/tavern') : (group.type === 'guild' ? (nconf.get('BASE_URL')+ '/#/options/groups/guilds/' + group._id) : 'party')},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return res.send(204);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
api.clearFlagCount = function(req, res, next){
|
|
||||||
var user = res.locals.user
|
|
||||||
var group = res.locals.group;
|
|
||||||
var message = _.find(group.chat, {id: req.params.mid});
|
|
||||||
|
|
||||||
if(!message) return res.json(404, {err: "Message not found!"});
|
|
||||||
|
|
||||||
if(user.contributor.admin){
|
|
||||||
message.flagCount = 0;
|
|
||||||
|
|
||||||
group.markModified('chat');
|
|
||||||
group.save(function(err,_saved){
|
|
||||||
if(err) return next(err);
|
|
||||||
return res.send(204);
|
|
||||||
});
|
|
||||||
}else{
|
|
||||||
return res.json(401, {err: "Only an admin can clear the flag count!"})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
api.seenMessage = function(req,res,next){
|
|
||||||
// Skip the auth step, we want this to be fast. If !found with uuid/token, then it just doesn't save
|
|
||||||
// Check for req.params.gid to exist
|
|
||||||
if(req.params.gid){
|
|
||||||
var update = {$unset:{}};
|
|
||||||
update['$unset']['newMessages.'+req.params.gid] = '';
|
|
||||||
User.update({_id:req.headers['x-api-user'], apiToken:req.headers['x-api-key']},update).exec();
|
|
||||||
}
|
|
||||||
res.send(200);
|
|
||||||
}
|
|
||||||
|
|
||||||
api.likeChatMessage = function(req, res, next) {
|
|
||||||
var user = res.locals.user;
|
|
||||||
var group = res.locals.group;
|
|
||||||
var message = _.find(group.chat, {id: req.params.mid});
|
|
||||||
if (!message) return res.json(404, {err: "Message not found!"});
|
|
||||||
if (message.uuid == user._id) return res.json(401, {err: "Can't like your own message. Don't be that person."});
|
|
||||||
if (!message.likes) message.likes = {};
|
|
||||||
if (message.likes[user._id]) {
|
|
||||||
delete message.likes[user._id];
|
|
||||||
} else {
|
|
||||||
message.likes[user._id] = true;
|
|
||||||
}
|
|
||||||
group.markModified('chat');
|
|
||||||
group.save(function(err,_saved){
|
|
||||||
if (err) return next(err);
|
|
||||||
return res.send(_saved.chat);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
api.join = function(req, res, next) {
|
|
||||||
var user = res.locals.user,
|
|
||||||
group = res.locals.group;
|
|
||||||
|
|
||||||
if (group.type == 'party' && group._id == (user.invitations && user.invitations.party && user.invitations.party.id)) {
|
|
||||||
User.update({_id:user.invitations.party.inviter}, {$inc:{'items.quests.basilist':1}}).exec(); // Reward inviter
|
|
||||||
user.invitations.party = undefined; // Clear invite
|
|
||||||
user.save();
|
|
||||||
// invite new user to pending quest
|
|
||||||
if (group.quest.key && !group.quest.active) {
|
|
||||||
group.quest.members[user._id] = undefined;
|
|
||||||
group.markModified('quest.members');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (group.type == 'guild' && user.invitations && user.invitations.guilds) {
|
|
||||||
var i = _.findIndex(user.invitations.guilds, {id:group._id});
|
|
||||||
if (~i) user.invitations.guilds.splice(i,1);
|
|
||||||
user.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_.contains(group.members, user._id)){
|
|
||||||
group.members.push(user._id);
|
|
||||||
group.invites.splice(_.indexOf(group.invites, user._id), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
function(cb){
|
|
||||||
group.save(cb);
|
|
||||||
},
|
|
||||||
function(cb){
|
|
||||||
populateQuery(group.type, Group.findById(group._id)).exec(cb);
|
|
||||||
}
|
|
||||||
], function(err, results){
|
|
||||||
if (err) return next(err);
|
|
||||||
|
|
||||||
// Return the group? Or not?
|
|
||||||
res.json(results[1]);
|
|
||||||
group = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
api.leave = function(req, res, next) {
|
|
||||||
var user = res.locals.user,
|
|
||||||
group = res.locals.group;
|
|
||||||
// When removing the user from challenges, should we keep the tasks?
|
|
||||||
var keep = (/^remove-all/i).test(req.query.keep) ? 'remove-all' : 'keep-all';
|
|
||||||
async.parallel([
|
|
||||||
// Remove active quest from user if they're leaving the party
|
|
||||||
function(cb){
|
|
||||||
if (group.type != 'party') return cb(null,{},1);
|
|
||||||
user.party.quest = Group.cleanQuestProgress();
|
|
||||||
user.save(cb);
|
|
||||||
},
|
|
||||||
// Remove user from group challenges
|
|
||||||
function(cb){
|
|
||||||
async.waterfall([
|
|
||||||
// Find relevant challenges
|
|
||||||
function(cb2) {
|
|
||||||
Challenge.find({
|
|
||||||
_id: {$in: user.challenges}, // Challenges I am in
|
|
||||||
group: group._id // that belong to the group I am leaving
|
|
||||||
}, cb2);
|
|
||||||
},
|
|
||||||
// Update each challenge
|
|
||||||
function(challenges, cb2) {
|
|
||||||
Challenge.update(
|
|
||||||
{_id:{$in: _.pluck(challenges, '_id')}},
|
|
||||||
{$pull:{members:user._id}},
|
|
||||||
{multi: true},
|
|
||||||
function(err) {
|
|
||||||
cb2(err, challenges); // pass `challenges` above to cb
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
// Unlink the challenge tasks from user
|
|
||||||
function(challenges, cb2) {
|
|
||||||
async.waterfall(challenges.map(function(chal) {
|
|
||||||
return function(cb3) {
|
|
||||||
var i = user.challenges.indexOf(chal._id)
|
|
||||||
if (~i) user.challenges.splice(i,1);
|
|
||||||
user.unlink({cid:chal._id, keep:keep}, cb3);
|
|
||||||
}
|
|
||||||
}), cb2);
|
|
||||||
}
|
|
||||||
], cb);
|
|
||||||
},
|
|
||||||
// Update the group
|
|
||||||
function(cb){
|
|
||||||
var update = {$pull:{members:user._id}};
|
|
||||||
if (group.type == 'party' && group.quest.key){
|
|
||||||
update['$unset'] = {};
|
|
||||||
update['$unset']['quest.members.' + user._id] = 1;
|
|
||||||
}
|
|
||||||
// FIXME do we want to remove the group `if group.members.length == 0` ? (well, 1 since the update hasn't gone through yet)
|
|
||||||
if (group.members.length > 1) {
|
|
||||||
var seniorMember = _.find(group.members, function (m) {return m != user._id});
|
|
||||||
// If the leader is leaving (or if the leader previously left, and this wasn't accounted for)
|
|
||||||
var leader = group.leader;
|
|
||||||
if (leader == user._id || !~group.members.indexOf(leader)) {
|
|
||||||
update['$set'] = update['$set'] || {};
|
|
||||||
update['$set'].leader = seniorMember;
|
|
||||||
}
|
|
||||||
leader = group.quest && group.quest.leader;
|
|
||||||
if (leader && (leader == user._id || !~group.members.indexOf(leader))) {
|
|
||||||
update['$set'] = update['$set'] || {};
|
|
||||||
update['$set']['quest.leader'] = seniorMember;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
update['$inc'] = {memberCount: -1};
|
|
||||||
Group.update({_id:group._id},update,cb);
|
|
||||||
}
|
|
||||||
],function(err){
|
|
||||||
if (err) return next(err);
|
|
||||||
return res.send(204);
|
|
||||||
user = group = keep = null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
api.invite = function(req, res, next) {
|
|
||||||
var group = res.locals.group;
|
|
||||||
var uuid = req.query.uuid;
|
|
||||||
|
|
||||||
User.findById(uuid, function(err,invite){
|
|
||||||
if (err) return next(err);
|
|
||||||
if (!invite)
|
|
||||||
return res.json(400,{err:'User with id "' + uuid + '" not found'});
|
|
||||||
if (group.type == 'guild') {
|
|
||||||
if (_.contains(group.members,uuid))
|
|
||||||
return res.json(400,{err: "User already in that group"});
|
|
||||||
if (invite.invitations && invite.invitations.guilds && _.find(invite.invitations.guilds, {id:group._id}))
|
|
||||||
return res.json(400, {err:"User already invited to that group"});
|
|
||||||
sendInvite();
|
|
||||||
} else if (group.type == 'party') {
|
|
||||||
if (invite.invitations && !_.isEmpty(invite.invitations.party))
|
|
||||||
return res.json(400,{err:"User already pending invitation."});
|
|
||||||
Group.find({type:'party', members:{$in:[uuid]}}, function(err, groups){
|
|
||||||
if (err) return next(err);
|
|
||||||
if (!_.isEmpty(groups))
|
|
||||||
return res.json(400,{err:"User already in a party."})
|
|
||||||
sendInvite();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendInvite (){
|
|
||||||
if(group.type === 'guild'){
|
|
||||||
invite.invitations.guilds.push({id: group._id, name: group.name, inviter:res.locals.user._id});
|
|
||||||
}else{
|
|
||||||
//req.body.type in 'guild', 'party'
|
|
||||||
invite.invitations.party = {id: group._id, name: group.name, inviter:res.locals.user._id};
|
|
||||||
}
|
|
||||||
|
|
||||||
group.invites.push(invite._id);
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
function(cb){
|
|
||||||
invite.save(cb);
|
|
||||||
},
|
|
||||||
function(cb){
|
|
||||||
group.save(cb);
|
|
||||||
},
|
|
||||||
function(cb){
|
|
||||||
populateQuery(group.type, Group.findById(group._id)).exec(cb);
|
|
||||||
}
|
|
||||||
], function(err, results){
|
|
||||||
if (err) return next(err);
|
|
||||||
|
|
||||||
if(invite.preferences.emailNotifications['invited' + (group.type == 'guild' ? 'Guild' : 'Party')] !== false){
|
|
||||||
var emailVars = [
|
|
||||||
{name: 'INVITER', content: utils.getUserInfo(res.locals.user, ['name']).name}
|
|
||||||
];
|
|
||||||
|
|
||||||
if(group.type == 'guild'){
|
|
||||||
emailVars.push(
|
|
||||||
{name: 'GUILD_NAME', content: group.name},
|
|
||||||
{name: 'GUILD_URL', content: nconf.get('BASE_URL') + '/#/options/groups/guilds/public'}
|
|
||||||
);
|
|
||||||
}else{
|
|
||||||
emailVars.push(
|
|
||||||
{name: 'PARTY_NAME', content: group.name},
|
|
||||||
{name: 'PARTY_URL', content: nconf.get('BASE_URL') + '/#/options/groups/party'}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.txnEmail(invite, ('invited-' + (group.type == 'guild' ? 'guild' : 'party')), emailVars);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Have to return whole group and its members for angular to show the invited user
|
|
||||||
res.json(results[2]);
|
|
||||||
group = uuid = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
api.removeMember = function(req, res, next){
|
|
||||||
var group = res.locals.group;
|
|
||||||
var uuid = req.query.uuid;
|
|
||||||
var user = res.locals.user;
|
|
||||||
|
|
||||||
if(group.leader !== user._id){
|
|
||||||
return res.json(401, {err: "Only group leader can remove a member!"});
|
|
||||||
}
|
|
||||||
|
|
||||||
if(_.contains(group.members, uuid)){
|
|
||||||
var update = {$pull:{members:uuid}};
|
|
||||||
if(group.quest && group.quest.members){
|
|
||||||
// remove member from quest
|
|
||||||
update['$unset'] = {};
|
|
||||||
update['$unset']['quest.members.' + uuid] = "";
|
|
||||||
// TODO: run cleanQuestProgress and return scroll to member if member was quest owner
|
|
||||||
}
|
|
||||||
update['$inc'] = {memberCount: -1};
|
|
||||||
Group.update({_id:group._id},update, function(err, saved){
|
|
||||||
if (err) return next(err);
|
|
||||||
|
|
||||||
// Sending an empty 204 because Group.update doesn't return the group
|
|
||||||
// see http://mongoosejs.com/docs/api.html#model_Model.update
|
|
||||||
return res.send(204);
|
|
||||||
});
|
|
||||||
}else if(_.contains(group.invites, uuid)){
|
|
||||||
User.findById(uuid, function(err,invited){
|
|
||||||
var invitations = invited.invitations;
|
|
||||||
if(group.type === 'guild'){
|
|
||||||
invitations.guilds.splice(_.indexOf(invitations.guilds, group._id), 1);
|
|
||||||
}else{
|
|
||||||
invitations.party = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
function(cb){
|
|
||||||
invited.save(cb);
|
|
||||||
},
|
|
||||||
function(cb){
|
|
||||||
Group.update({_id:group._id},{$pull:{invites:uuid}}, cb);
|
|
||||||
}
|
|
||||||
], function(err, results){
|
|
||||||
if (err) return next(err);
|
|
||||||
|
|
||||||
// Sending an empty 204 because Group.update doesn't return the group
|
|
||||||
// see http://mongoosejs.com/docs/api.html#model_Model.update
|
|
||||||
return res.send(204);
|
|
||||||
group = uuid = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
}else{
|
|
||||||
return res.json(400, {err: "User not found among group's members!"});
|
|
||||||
group = uuid = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------------------------------
|
|
||||||
// Quests
|
|
||||||
// ------------------------------------
|
|
||||||
|
|
||||||
questStart = function(req, res, next) {
|
|
||||||
var group = res.locals.group;
|
|
||||||
var force = req.query.force;
|
|
||||||
|
|
||||||
// if (group.quest.active) return res.json(400,{err:'Quest already began.'});
|
|
||||||
// temporarily send error email, until we know more about this issue (then remove below, uncomment above).
|
|
||||||
if (group.quest.active) return next('Quest already began.');
|
|
||||||
|
|
||||||
group.markModified('quest');
|
|
||||||
|
|
||||||
// Not ready yet, wait till everyone's accepted, rejected, or we force-start
|
|
||||||
var statuses = _.values(group.quest.members);
|
|
||||||
if (!force && (~statuses.indexOf(undefined) || ~statuses.indexOf(null))) {
|
|
||||||
return group.save(function(err,saved){
|
|
||||||
if (err) return next(err);
|
|
||||||
res.json(saved);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
var parallel = [],
|
|
||||||
questMembers = {},
|
|
||||||
key = group.quest.key,
|
|
||||||
quest = shared.content.quests[key],
|
|
||||||
collected = quest.collect ? _.transform(quest.collect, function(m,v,k){m[k]=0}) : {};
|
|
||||||
|
|
||||||
_.each(group.members, function(m){
|
|
||||||
var updates = {$set:{},$inc:{'_v':1}};
|
|
||||||
if (m == group.quest.leader)
|
|
||||||
updates['$inc']['items.quests.'+key] = -1;
|
|
||||||
if (group.quest.members[m] == true) {
|
|
||||||
// See https://github.com/HabitRPG/habitrpg/issues/2168#issuecomment-31556322 , we need to *not* reset party.quest.progress.up
|
|
||||||
//updates['$set']['party.quest'] = Group.cleanQuestProgress({key:key,progress:{collect:collected}});
|
|
||||||
updates['$set']['party.quest.key'] = key;
|
|
||||||
updates['$set']['party.quest.progress.down'] = 0;
|
|
||||||
updates['$set']['party.quest.progress.collect'] = collected;
|
|
||||||
updates['$set']['party.quest.completed'] = null;
|
|
||||||
questMembers[m] = true;
|
|
||||||
} else {
|
|
||||||
updates['$set']['party.quest'] = Group.cleanQuestProgress();
|
|
||||||
}
|
|
||||||
parallel.push(function(cb2){
|
|
||||||
User.update({_id:m},updates,cb2);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
group.quest.active = true;
|
|
||||||
if (quest.boss) {
|
|
||||||
group.quest.progress.hp = quest.boss.hp;
|
|
||||||
if (quest.boss.rage) group.quest.progress.rage = 0;
|
|
||||||
} else {
|
|
||||||
group.quest.progress.collect = collected;
|
|
||||||
}
|
|
||||||
group.quest.members = questMembers;
|
|
||||||
group.markModified('quest'); // members & progress.collect are both Mixed types
|
|
||||||
parallel.push(function(cb2){group.save(cb2)});
|
|
||||||
|
|
||||||
parallel.push(function(cb){
|
|
||||||
// Fetch user.auth to send email, then remove it from data sent to the client
|
|
||||||
populateQuery(group.type, Group.findById(group._id), 'auth.facebook auth.local').exec(cb);
|
|
||||||
});
|
|
||||||
|
|
||||||
async.parallel(parallel,function(err, results){
|
|
||||||
if (err) return next(err);
|
|
||||||
|
|
||||||
var lastIndex = results.length -1;
|
|
||||||
var groupClone = clone(group);
|
|
||||||
|
|
||||||
groupClone.members = results[lastIndex].members;
|
|
||||||
|
|
||||||
// Send quest started email and remove auth information
|
|
||||||
_.each(groupClone.members, function(user){
|
|
||||||
|
|
||||||
if(user.preferences.emailNotifications.questStarted !== false &&
|
|
||||||
user._id !== res.locals.user._id &&
|
|
||||||
group.quest.members[user._id] == true
|
|
||||||
){
|
|
||||||
utils.txnEmail(user, 'quest-started', [
|
|
||||||
{name: 'PARTY_URL', content: nconf.get('BASE_URL') + '/#/options/groups/party'}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove sensitive data from what is sent to the public
|
|
||||||
user.auth.facebook = undefined;
|
|
||||||
user.auth.local = undefined;
|
|
||||||
});
|
|
||||||
|
|
||||||
group = null;
|
|
||||||
|
|
||||||
return res.json(groupClone);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
api.questAccept = function(req, res, next) {
|
|
||||||
var group = res.locals.group;
|
|
||||||
var user = res.locals.user;
|
|
||||||
var key = req.query.key;
|
|
||||||
|
|
||||||
if (!group) return res.json(400, {err: "Must be in a party to start quests."});
|
|
||||||
|
|
||||||
// If ?key=xxx is provided, we're starting a new quest and inviting the party. Otherwise, we're a party member accepting the invitation
|
|
||||||
if (key) {
|
|
||||||
var quest = shared.content.quests[key];
|
|
||||||
if (!quest) return res.json(404,{err:'Quest ' + key + ' not found'});
|
|
||||||
if (quest.lvl && user.stats.lvl < quest.lvl) return res.json(400, {err: "You must be level "+quest.lvl+" to begin this quest."});
|
|
||||||
if (group.quest.key) return res.json(400, {err: 'Party already on a quest (and only have one quest at a time)'});
|
|
||||||
if (!user.items.quests[key]) return res.json(400, {err: "You don't own that quest scroll"});
|
|
||||||
group.quest.key = key;
|
|
||||||
group.quest.members = {};
|
|
||||||
// Invite everyone. true means "accepted", false="rejected", undefined="pending". Once we click "start quest"
|
|
||||||
// or everyone has either accepted/rejected, then we store quest key in user object.
|
|
||||||
_.each(group.members, function(m){
|
|
||||||
if (m == user._id) {
|
|
||||||
group.quest.members[m] = true;
|
|
||||||
group.quest.leader = user._id;
|
|
||||||
} else {
|
|
||||||
group.quest.members[m] = undefined;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
User.find({
|
|
||||||
_id: {
|
|
||||||
$in: _.without(group.members, user._id)
|
|
||||||
}
|
|
||||||
}, {auth: 1, preferences: 1, profile: 1}, function(err, members){
|
|
||||||
if(err) return next(err);
|
|
||||||
|
|
||||||
var inviterName = utils.getUserInfo(user, ['name']).name;
|
|
||||||
|
|
||||||
_.each(members, function(member){
|
|
||||||
if(member.preferences.emailNotifications.invitedQuest !== false){
|
|
||||||
utils.txnEmail(member, ('invite-' + (quest.boss ? 'boss' : 'collection') + '-quest'), [
|
|
||||||
{name: 'QUEST_NAME', content: quest.text()},
|
|
||||||
{name: 'INVITER', content: inviterName},
|
|
||||||
{name: 'PARTY_URL', content: nconf.get('BASE_URL') + '/#/options/groups/party'}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
questStart(req,res,next);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Party member accepting the invitation
|
|
||||||
} else {
|
|
||||||
if (!group.quest.key) return res.json(400,{err:'No quest invitation has been sent out yet.'});
|
|
||||||
group.quest.members[user._id] = true;
|
|
||||||
questStart(req,res,next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
api.questReject = function(req, res, next) {
|
|
||||||
var group = res.locals.group;
|
|
||||||
var user = res.locals.user;
|
|
||||||
|
|
||||||
if (!group.quest.key) return res.json(400,{err:'No quest invitation has been sent out yet.'});
|
|
||||||
group.quest.members[user._id] = false;
|
|
||||||
questStart(req,res,next);
|
|
||||||
}
|
|
||||||
|
|
||||||
api.questCancel = function(req, res, next){
|
|
||||||
// Cancel a quest BEFORE it has begun (i.e., in the invitation stage)
|
|
||||||
// Quest scroll has not yet left quest owner's inventory so no need to return it.
|
|
||||||
// Do not wipe quest progress for members because they'll want it to be applied to the next quest that's started.
|
|
||||||
var group = res.locals.group;
|
|
||||||
async.parallel([
|
|
||||||
function(cb){
|
|
||||||
if (! group.quest.active) {
|
|
||||||
// Do not cancel active quests because this function does
|
|
||||||
// not do the clean-up required for that.
|
|
||||||
// TODO: return an informative error when quest is active
|
|
||||||
group.quest = {key:null,progress:{},leader:null};
|
|
||||||
group.markModified('quest');
|
|
||||||
group.save(cb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
], function(err){
|
|
||||||
if (err) return next(err);
|
|
||||||
res.json(group);
|
|
||||||
group = null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
api.questAbort = function(req, res, next){
|
|
||||||
// Abort a quest AFTER it has begun (see questCancel for BEFORE)
|
|
||||||
var group = res.locals.group;
|
|
||||||
async.parallel([
|
|
||||||
function(cb){
|
|
||||||
User.update(
|
|
||||||
{_id:{$in: _.keys(group.quest.members)}},
|
|
||||||
{
|
|
||||||
$set: {'party.quest':Group.cleanQuestProgress()},
|
|
||||||
$inc: {_v:1}
|
|
||||||
},
|
|
||||||
{multi:true},
|
|
||||||
cb);
|
|
||||||
},
|
|
||||||
// Refund party leader quest scroll
|
|
||||||
function(cb){
|
|
||||||
if (group.quest.active) {
|
|
||||||
var update = {$inc:{}};
|
|
||||||
update['$inc']['items.quests.' + group.quest.key] = 1;
|
|
||||||
User.update({_id:group.quest.leader}, update).exec();
|
|
||||||
}
|
|
||||||
group.quest = {key:null,progress:{},leader:null};
|
|
||||||
group.markModified('quest');
|
|
||||||
group.save(cb);
|
|
||||||
}, function(cb){
|
|
||||||
populateQuery(group.type, Group.findById(group._id)).exec(cb);
|
|
||||||
}
|
|
||||||
], function(err, results){
|
|
||||||
if (err) return next(err);
|
|
||||||
|
|
||||||
var groupClone = clone(group);
|
|
||||||
|
|
||||||
groupClone.members = results[2].members;
|
|
||||||
|
|
||||||
res.json(groupClone);
|
|
||||||
group = null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
var _ = require('lodash');
|
|
||||||
var nconf = require('nconf');
|
|
||||||
var async = require('async');
|
|
||||||
var shared = require('../../../common');
|
|
||||||
var User = require('./../models/user').model;
|
|
||||||
var Group = require('./../models/group').model;
|
|
||||||
var api = module.exports;
|
|
||||||
|
|
||||||
api.ensureAdmin = function(req, res, next) {
|
|
||||||
var user = res.locals.user;
|
|
||||||
if (!(user.contributor && user.contributor.admin)) return res.json(401, {err:"You don't have admin access"});
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
|
|
||||||
api.getHeroes = function(req,res,next) {
|
|
||||||
User.find({'contributor.level':{$gt:0}})
|
|
||||||
.select('contributor backer balance profile.name')
|
|
||||||
.sort('-contributor.level')
|
|
||||||
.exec(function(err, users){
|
|
||||||
if (err) return next(err);
|
|
||||||
res.json(users);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
api.getPatrons = function(req,res,next){
|
|
||||||
var page = req.query.page || 0,
|
|
||||||
perPage = 50;
|
|
||||||
User.find({'backer.tier':{$gt:0}})
|
|
||||||
.select('contributor backer profile.name')
|
|
||||||
.sort('-backer.tier')
|
|
||||||
.skip(page*perPage)
|
|
||||||
.limit(perPage)
|
|
||||||
.exec(function(err, users){
|
|
||||||
if (err) return next(err);
|
|
||||||
res.json(users);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
api.getHero = function(req,res,next) {
|
|
||||||
User.findById(req.params.uid)
|
|
||||||
.select('contributor balance profile.name purchased items')
|
|
||||||
.select('auth.local.username auth.local.email auth.facebook auth.blocked')
|
|
||||||
.exec(function(err, user){
|
|
||||||
if (err) return next(err)
|
|
||||||
if (!user) return res.json(400,{err:'User not found'});
|
|
||||||
res.json(user);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
api.updateHero = function(req,res,next) {
|
|
||||||
async.waterfall([
|
|
||||||
function(cb){
|
|
||||||
User.findById(req.params.uid, cb);
|
|
||||||
},
|
|
||||||
function(member, cb){
|
|
||||||
if (!member) return res.json(404, {err: "User not found"});
|
|
||||||
member.balance = req.body.balance || 0;
|
|
||||||
var newTier = req.body.contributor.level; // tier = level in this context
|
|
||||||
var oldTier = member.contributor && member.contributor.level || 0;
|
|
||||||
if (newTier > oldTier) {
|
|
||||||
member.flags.contributor = true;
|
|
||||||
var gemsPerTier = {1:3, 2:3, 3:3, 4:4, 5:4, 6:4, 7:4, 8:0, 9:0}; // e.g., tier 5 gives 4 gems. Tier 8 = moderator. Tier 9 = staff
|
|
||||||
var tierDiff = newTier - oldTier; // can be 2+ tier increases at once
|
|
||||||
while (tierDiff) {
|
|
||||||
member.balance += gemsPerTier[newTier] / 4; // balance is in $
|
|
||||||
tierDiff--;
|
|
||||||
newTier--; // give them gems for the next tier down if they weren't aready that tier
|
|
||||||
}
|
|
||||||
}
|
|
||||||
member.contributor = req.body.contributor;
|
|
||||||
member.purchased.ads = req.body.purchased.ads;
|
|
||||||
if (member.contributor.level >= 6) member.items.pets['Dragon-Hydra'] = 5;
|
|
||||||
if (req.body.itemPath && req.body.itemVal
|
|
||||||
&& req.body.itemPath.indexOf('items.') === 0
|
|
||||||
&& User.schema.paths[req.body.itemPath]) {
|
|
||||||
shared.dotSet(member, req.body.itemPath, req.body.itemVal); // Sanitization at 5c30944 (deemed unnecessary)
|
|
||||||
}
|
|
||||||
if (_.isBoolean(req.body.auth.blocked)) member.auth.blocked = req.body.auth.blocked;
|
|
||||||
member.save(cb);
|
|
||||||
}
|
|
||||||
], function(err, saved){
|
|
||||||
if (err) return next(err);
|
|
||||||
res.json(204);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
var User = require('mongoose').model('User');
|
|
||||||
var groups = require('../models/group');
|
|
||||||
var partyFields = require('./groups').partyFields
|
|
||||||
var api = module.exports;
|
|
||||||
var async = require('async');
|
|
||||||
var _ = require('lodash');
|
|
||||||
var shared = require('../../../common');
|
|
||||||
var utils = require('../utils');
|
|
||||||
var nconf = require('nconf');
|
|
||||||
|
|
||||||
var fetchMember = function(uuid, restrict){
|
|
||||||
return function(cb){
|
|
||||||
var q = User.findById(uuid);
|
|
||||||
if (restrict) q.select(partyFields);
|
|
||||||
q.exec(function(err, member){
|
|
||||||
if (err) return cb(err);
|
|
||||||
if (!member) return cb({code:404, err: 'User not found'});
|
|
||||||
return cb(null, member);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var sendErr = function(err, res, next){
|
|
||||||
err.code ? res.json(err.code, {err: err.err}) : next(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
api.getMember = function(req, res, next) {
|
|
||||||
fetchMember(req.params.uuid, true)(function(err, member){
|
|
||||||
if (err) return sendErr(err, res, next);
|
|
||||||
res.json(member);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
api.sendMessage = function(user, member, data){
|
|
||||||
var msg;
|
|
||||||
if (!data.type) {
|
|
||||||
msg = data.message
|
|
||||||
} else {
|
|
||||||
msg = "`Hello " + member.profile.name + ", " + user.profile.name + " has sent you ";
|
|
||||||
msg += (data.type=='gems') ? data.gems.amount + " gems!`" : shared.content.subscriptionBlocks[data.subscription.key].months + " months of subscription!`";
|
|
||||||
msg += data.message;
|
|
||||||
}
|
|
||||||
shared.refPush(member.inbox.messages, groups.chatDefaults(msg, user));
|
|
||||||
member.inbox.newMessages++;
|
|
||||||
member._v++;
|
|
||||||
member.markModified('inbox.messages');
|
|
||||||
|
|
||||||
shared.refPush(user.inbox.messages, _.defaults({sent:true}, groups.chatDefaults(msg, member)));
|
|
||||||
user.markModified('inbox.messages');
|
|
||||||
}
|
|
||||||
|
|
||||||
api.sendPrivateMessage = function(req, res, next){
|
|
||||||
var fetchedMember;
|
|
||||||
async.waterfall([
|
|
||||||
fetchMember(req.params.uuid),
|
|
||||||
function(member, cb) {
|
|
||||||
fetchedMember = member;
|
|
||||||
if (~member.inbox.blocks.indexOf(res.locals.user._id) // can't send message if that user blocked me
|
|
||||||
|| ~res.locals.user.inbox.blocks.indexOf(member._id) // or if I blocked them
|
|
||||||
|| member.inbox.optOut) { // or if they've opted out of messaging
|
|
||||||
return cb({code: 401, err: "Can't send message to this user."});
|
|
||||||
}
|
|
||||||
api.sendMessage(res.locals.user, member, {message:req.body.message});
|
|
||||||
async.parallel([
|
|
||||||
function (cb2) { member.save(cb2) },
|
|
||||||
function (cb2) { res.locals.user.save(cb2) }
|
|
||||||
], cb);
|
|
||||||
}
|
|
||||||
], function(err){
|
|
||||||
if (err) return sendErr(err, res, next);
|
|
||||||
|
|
||||||
if(fetchedMember.preferences.emailNotifications.newPM !== false){
|
|
||||||
utils.txnEmail(fetchedMember, 'new-pm', [
|
|
||||||
{name: 'SENDER', content: utils.getUserInfo(res.locals.user, ['name']).name},
|
|
||||||
{name: 'PMS_INBOX_URL', content: nconf.get('BASE_URL') + '/#/options/groups/inbox'}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
res.send(200);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
api.sendGift = function(req, res, next){
|
|
||||||
async.waterfall([
|
|
||||||
fetchMember(req.params.uuid),
|
|
||||||
function(member, cb) {
|
|
||||||
// Gems
|
|
||||||
switch (req.body.type) {
|
|
||||||
case "gems":
|
|
||||||
var amt = req.body.gems.amount / 4,
|
|
||||||
user = res.locals.user;
|
|
||||||
if (member.id == user.id)
|
|
||||||
return cb({code: 401, err: "Cannot send gems to yourself. Try a subscription instead."});
|
|
||||||
if (!amt || amt <=0 || user.balance < amt)
|
|
||||||
return cb({code: 401, err: "Amount must be within 0 and your current number of gems."});
|
|
||||||
member.balance += amt;
|
|
||||||
user.balance -= amt;
|
|
||||||
api.sendMessage(user, member, req.body);
|
|
||||||
if(member.preferences.emailNotifications.giftedGems !== false){
|
|
||||||
utils.txnEmail(member, 'gifted-gems', [
|
|
||||||
{name: 'GIFTER', content: utils.getUserInfo(user, ['name']).name},
|
|
||||||
{name: 'X_GEMS_GIFTED', content: req.body.gems.amount}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
return async.parallel([
|
|
||||||
function (cb2) { member.save(cb2) },
|
|
||||||
function (cb2) { user.save(cb2) }
|
|
||||||
], cb);
|
|
||||||
case "subscription":
|
|
||||||
return cb();
|
|
||||||
default:
|
|
||||||
return cb({code:400, err:"Body must contain a gems:{amount,fromBalance} or subscription:{months} object"});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
], function(err) {
|
|
||||||
if (err) return sendErr(err, res, next);
|
|
||||||
res.send(200);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
var iap = require('in-app-purchase');
|
|
||||||
var async = require('async');
|
|
||||||
var payments = require('./index');
|
|
||||||
var nconf = require('nconf');
|
|
||||||
|
|
||||||
var inAppPurchase = require('in-app-purchase');
|
|
||||||
inAppPurchase.config({
|
|
||||||
// this is the path to the directory containing iap-sanbox/iap-live files
|
|
||||||
googlePublicKeyPath: nconf.get("IAP_GOOGLE_KEYDIR")
|
|
||||||
});
|
|
||||||
|
|
||||||
// Validation ERROR Codes
|
|
||||||
var INVALID_PAYLOAD = 6778001;
|
|
||||||
var CONNECTION_FAILED = 6778002;
|
|
||||||
var PURCHASE_EXPIRED = 6778003;
|
|
||||||
|
|
||||||
exports.androidVerify = function(req, res, next) {
|
|
||||||
var iapBody = req.body;
|
|
||||||
var user = res.locals.user;
|
|
||||||
|
|
||||||
iap.setup(function (error) {
|
|
||||||
if (error) {
|
|
||||||
var resObj = {
|
|
||||||
ok: false,
|
|
||||||
data: 'IAP Error'
|
|
||||||
};
|
|
||||||
|
|
||||||
console.error('IAP Setup ERROR');
|
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
res.json(resObj);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
google receipt must be provided as an object
|
|
||||||
{
|
|
||||||
"data": "{stringified data object}",
|
|
||||||
"signature": "signature from google"
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
var testObj = {
|
|
||||||
data: iapBody.transaction.receipt,
|
|
||||||
signature: iapBody.transaction.signature
|
|
||||||
};
|
|
||||||
|
|
||||||
// iap is ready
|
|
||||||
iap.validate(iap.GOOGLE, testObj, function (err, googleRes) {
|
|
||||||
if (err) {
|
|
||||||
var resObj = {
|
|
||||||
ok: false,
|
|
||||||
data: {
|
|
||||||
code: INVALID_PAYLOAD,
|
|
||||||
message: err.toString()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
res.json(resObj);
|
|
||||||
console.error(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (iap.isValidated(googleRes)) {
|
|
||||||
var resObj = {
|
|
||||||
ok: true,
|
|
||||||
data: googleRes
|
|
||||||
};
|
|
||||||
|
|
||||||
payments.buyGems({user:user, paymentMethod:'IAP GooglePlay'});
|
|
||||||
|
|
||||||
// yay good!
|
|
||||||
res.json(resObj);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.iosVerify = function(req, res, next) {
|
|
||||||
console.info(req.body);
|
|
||||||
|
|
||||||
var iapBody = req.body;
|
|
||||||
var user = res.locals.user;
|
|
||||||
|
|
||||||
iap.setup(function (error) {
|
|
||||||
if (error) {
|
|
||||||
var resObj = {
|
|
||||||
ok: false,
|
|
||||||
data: 'IAP Error'
|
|
||||||
};
|
|
||||||
|
|
||||||
console.error('IAP Setup ERROR');
|
|
||||||
console.error(error);
|
|
||||||
|
|
||||||
res.json(resObj);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// iap is ready
|
|
||||||
iap.validate(iap.APPLE, iapBody.transaction.receipt, function (err, appleRes) {
|
|
||||||
if (err) {
|
|
||||||
var resObj = {
|
|
||||||
ok: false,
|
|
||||||
data: {
|
|
||||||
code: INVALID_PAYLOAD,
|
|
||||||
message: err.toString()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
res.json(resObj);
|
|
||||||
console.error(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (iap.isValidated(appleRes)) {
|
|
||||||
var resObj = {
|
|
||||||
ok: true,
|
|
||||||
data: appleRes
|
|
||||||
};
|
|
||||||
|
|
||||||
payments.buyGems({user:user, paymentMethod:'IAP AppleStore'});
|
|
||||||
|
|
||||||
// yay good!
|
|
||||||
res.json(resObj);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
/* @see ./routes.coffee for routing*/
|
|
||||||
var _ = require('lodash');
|
|
||||||
var shared = require('../../../../common');
|
|
||||||
var nconf = require('nconf');
|
|
||||||
var utils = require('./../../utils');
|
|
||||||
var moment = require('moment');
|
|
||||||
var isProduction = nconf.get("NODE_ENV") === "production";
|
|
||||||
var stripe = require('./stripe');
|
|
||||||
var paypal = require('./paypal');
|
|
||||||
var members = require('../members')
|
|
||||||
var async = require('async');
|
|
||||||
var iap = require('./iap');
|
|
||||||
var mongoose= require('mongoose');
|
|
||||||
var cc = require('coupon-code');
|
|
||||||
|
|
||||||
function revealMysteryItems(user) {
|
|
||||||
_.each(shared.content.gear.flat, function(item) {
|
|
||||||
if (
|
|
||||||
item.klass === 'mystery' &&
|
|
||||||
moment().isAfter(shared.content.mystery[item.mystery].start) &&
|
|
||||||
moment().isBefore(shared.content.mystery[item.mystery].end) &&
|
|
||||||
!user.items.gear.owned[item.key] &&
|
|
||||||
!~user.purchased.plan.mysteryItems.indexOf(item.key)
|
|
||||||
) {
|
|
||||||
user.purchased.plan.mysteryItems.push(item.key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.createSubscription = function(data, cb) {
|
|
||||||
var recipient = data.gift ? data.gift.member : data.user;
|
|
||||||
//if (!recipient.purchased.plan) recipient.purchased.plan = {}; // FIXME double-check, this should never be the case
|
|
||||||
var p = recipient.purchased.plan;
|
|
||||||
var block = shared.content.subscriptionBlocks[data.gift ? data.gift.subscription.key : data.sub.key];
|
|
||||||
var months = +block.months;
|
|
||||||
|
|
||||||
if (data.gift) {
|
|
||||||
if (p.customerId && !p.dateTerminated) { // User has active plan
|
|
||||||
p.extraMonths += months;
|
|
||||||
} else {
|
|
||||||
p.dateTerminated = moment(p.dateTerminated).add({months: months}).toDate();
|
|
||||||
if (!p.dateUpdated) p.dateUpdated = new Date();
|
|
||||||
}
|
|
||||||
if (!p.customerId) p.customerId = 'Gift'; // don't override existing customer, but all sub need a customerId
|
|
||||||
} else {
|
|
||||||
_(p).merge({ // override with these values
|
|
||||||
planId: block.key,
|
|
||||||
customerId: data.customerId,
|
|
||||||
dateUpdated: new Date(),
|
|
||||||
gemsBought: 0,
|
|
||||||
paymentMethod: data.paymentMethod,
|
|
||||||
extraMonths: +p.extraMonths
|
|
||||||
+ +(p.dateTerminated ? moment(p.dateTerminated).diff(new Date(),'months',true) : 0),
|
|
||||||
dateTerminated: null
|
|
||||||
}).defaults({ // allow non-override if a plan was previously used
|
|
||||||
dateCreated: new Date(),
|
|
||||||
mysteryItems: []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Block sub perks
|
|
||||||
var perks = Math.floor(months/3);
|
|
||||||
if (perks) {
|
|
||||||
p.consecutive.offset += months;
|
|
||||||
p.consecutive.gemCapExtra += perks*5;
|
|
||||||
if (p.consecutive.gemCapExtra > 25) p.consecutive.gemCapExtra = 25;
|
|
||||||
p.consecutive.trinkets += perks;
|
|
||||||
}
|
|
||||||
revealMysteryItems(recipient);
|
|
||||||
if(isProduction) {
|
|
||||||
if (!data.gift) utils.txnEmail(data.user, 'subscription-begins');
|
|
||||||
utils.ga.event('subscribe', data.paymentMethod).send();
|
|
||||||
utils.ga.transaction(data.user._id, block.price).item(block.price, 1, data.paymentMethod.toLowerCase() + '-subscription', data.paymentMethod).send();
|
|
||||||
}
|
|
||||||
data.user.purchased.txnCount++;
|
|
||||||
if (data.gift){
|
|
||||||
members.sendMessage(data.user, data.gift.member, data.gift);
|
|
||||||
if(data.gift.member.preferences.emailNotifications.giftedSubscription !== false){
|
|
||||||
utils.txnEmail(data.gift.member, 'gifted-subscription', [
|
|
||||||
{name: 'GIFTER', content: utils.getUserInfo(data.user, ['name']).name},
|
|
||||||
{name: 'X_MONTHS_SUBSCRIPTION', content: months}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async.parallel([
|
|
||||||
function(cb2){data.user.save(cb2)},
|
|
||||||
function(cb2){data.gift ? data.gift.member.save(cb2) : cb2(null);}
|
|
||||||
], cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets their subscription to be cancelled later
|
|
||||||
*/
|
|
||||||
exports.cancelSubscription = function(data, cb) {
|
|
||||||
var p = data.user.purchased.plan,
|
|
||||||
now = moment(),
|
|
||||||
remaining = data.nextBill ? moment(data.nextBill).diff(new Date, 'days') : 30;
|
|
||||||
|
|
||||||
p.dateTerminated =
|
|
||||||
moment( now.format('MM') + '/' + moment(p.dateUpdated).format('DD') + '/' + now.format('YYYY') )
|
|
||||||
.add({days: remaining}) // end their subscription 1mo from their last payment
|
|
||||||
.add({months: Math.ceil(p.extraMonths)})// plus any extra time (carry-over, gifted subscription, etc) they have. FIXME: moment can't add months in fractions...
|
|
||||||
.toDate();
|
|
||||||
p.extraMonths = 0; // clear extra time. If they subscribe again, it'll be recalculated from p.dateTerminated
|
|
||||||
|
|
||||||
data.user.save(cb);
|
|
||||||
utils.txnEmail(data.user, 'cancel-subscription');
|
|
||||||
utils.ga.event('unsubscribe', data.paymentMethod).send();
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.buyGems = function(data, cb) {
|
|
||||||
var amt = data.gift ? data.gift.gems.amount/4 : 5;
|
|
||||||
(data.gift ? data.gift.member : data.user).balance += amt;
|
|
||||||
data.user.purchased.txnCount++;
|
|
||||||
if(isProduction) {
|
|
||||||
if (!data.gift) utils.txnEmail(data.user, 'donation');
|
|
||||||
utils.ga.event('checkout', data.paymentMethod).send();
|
|
||||||
//TODO ga.transaction to reflect whether this is gift or self-purchase
|
|
||||||
utils.ga.transaction(data.user._id, amt).item(amt, 1, data.paymentMethod.toLowerCase() + "-checkout", "Gems > " + data.paymentMethod).send();
|
|
||||||
}
|
|
||||||
if (data.gift){
|
|
||||||
members.sendMessage(data.user, data.gift.member, data.gift);
|
|
||||||
if(data.gift.member.preferences.emailNotifications.giftedGems !== false){
|
|
||||||
utils.txnEmail(data.gift.member, 'gifted-gems', [
|
|
||||||
{name: 'GIFTER', content: utils.getUserInfo(data.user, ['name']).name},
|
|
||||||
{name: 'X_GEMS_GIFTED', content: data.gift.gems.amount || 20}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async.parallel([
|
|
||||||
function(cb2){data.user.save(cb2)},
|
|
||||||
function(cb2){data.gift ? data.gift.member.save(cb2) : cb2(null);}
|
|
||||||
], cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.validCoupon = function(req, res, next){
|
|
||||||
mongoose.model('Coupon').findOne({_id:cc.validate(req.params.code), event:'google_6mo'}, function(err, coupon){
|
|
||||||
if (err) return next(err);
|
|
||||||
if (!coupon) return res.json(401, {err:"Invalid coupon code"});
|
|
||||||
return res.send(200);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.stripeCheckout = stripe.checkout;
|
|
||||||
exports.stripeSubscribeCancel = stripe.subscribeCancel;
|
|
||||||
exports.stripeSubscribeEdit = stripe.subscribeEdit;
|
|
||||||
|
|
||||||
exports.paypalSubscribe = paypal.createBillingAgreement;
|
|
||||||
exports.paypalSubscribeSuccess = paypal.executeBillingAgreement;
|
|
||||||
exports.paypalSubscribeCancel = paypal.cancelSubscription;
|
|
||||||
exports.paypalCheckout = paypal.createPayment;
|
|
||||||
exports.paypalCheckoutSuccess = paypal.executePayment;
|
|
||||||
exports.paypalIPN = paypal.ipn;
|
|
||||||
|
|
||||||
exports.iapAndroidVerify = iap.androidVerify;
|
|
||||||
exports.iapIosVerify = iap.iosVerify;
|
|
||||||
@@ -1,216 +0,0 @@
|
|||||||
var nconf = require('nconf');
|
|
||||||
var moment = require('moment');
|
|
||||||
var async = require('async');
|
|
||||||
var _ = require('lodash');
|
|
||||||
var url = require('url');
|
|
||||||
var User = require('mongoose').model('User');
|
|
||||||
var payments = require('./index');
|
|
||||||
var logger = require('../../logging');
|
|
||||||
var ipn = require('paypal-ipn');
|
|
||||||
var paypal = require('paypal-rest-sdk');
|
|
||||||
var shared = require('../../../../common');
|
|
||||||
var mongoose = require('mongoose');
|
|
||||||
var cc = require('coupon-code');
|
|
||||||
|
|
||||||
// This is the plan.id for paypal subscriptions. You have to set up billing plans via their REST sdk (they don't have
|
|
||||||
// a web interface for billing-plan creation), see ./paypalBillingSetup.js for how. After the billing plan is created
|
|
||||||
// there, get it's plan.id and store it in config.json
|
|
||||||
_.each(shared.content.subscriptionBlocks, function(block){
|
|
||||||
block.paypalKey = nconf.get("PAYPAL:billing_plans:"+block.key);
|
|
||||||
});
|
|
||||||
|
|
||||||
paypal.configure({
|
|
||||||
'mode': nconf.get("PAYPAL:mode"), //sandbox or live
|
|
||||||
'client_id': nconf.get("PAYPAL:client_id"),
|
|
||||||
'client_secret': nconf.get("PAYPAL:client_secret")
|
|
||||||
});
|
|
||||||
|
|
||||||
var parseErr = function(res, err){
|
|
||||||
//var error = err.response ? err.response.message || err.response.details[0].issue : err;
|
|
||||||
var error = JSON.stringify(err);
|
|
||||||
return res.json(400,{err:error});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.createBillingAgreement = function(req,res,next){
|
|
||||||
var sub = shared.content.subscriptionBlocks[req.query.sub];
|
|
||||||
async.waterfall([
|
|
||||||
function(cb){
|
|
||||||
if (!sub.discount) return cb(null, null);
|
|
||||||
if (!req.query.coupon) return cb('Please provide a coupon code for this plan.');
|
|
||||||
mongoose.model('Coupon').findOne({_id:cc.validate(req.query.coupon), event:sub.key}, cb);
|
|
||||||
},
|
|
||||||
function(coupon, cb){
|
|
||||||
if (sub.discount && !coupon) return cb('Invalid coupon code.');
|
|
||||||
var billingPlanTitle = "HabitRPG Subscription" + ' ($'+sub.price+' every '+sub.months+' months, recurring)';
|
|
||||||
var billingAgreementAttributes = {
|
|
||||||
"name": billingPlanTitle,
|
|
||||||
"description": billingPlanTitle,
|
|
||||||
"start_date": moment().add({minutes:5}).format(),
|
|
||||||
"plan": {
|
|
||||||
"id": sub.paypalKey
|
|
||||||
},
|
|
||||||
"payer": {
|
|
||||||
"payment_method": "paypal"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
paypal.billingAgreement.create(billingAgreementAttributes, cb);
|
|
||||||
}
|
|
||||||
], function(err, billingAgreement){
|
|
||||||
if (err) return parseErr(res, err);
|
|
||||||
// For approving subscription via Paypal, first redirect user to: approval_url
|
|
||||||
req.session.paypalBlock = req.query.sub;
|
|
||||||
var approval_url = _.find(billingAgreement.links, {rel:'approval_url'}).href;
|
|
||||||
res.redirect(approval_url);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.executeBillingAgreement = function(req,res,next){
|
|
||||||
var block = shared.content.subscriptionBlocks[req.session.paypalBlock];
|
|
||||||
delete req.session.paypalBlock;
|
|
||||||
async.auto({
|
|
||||||
exec: function (cb) {
|
|
||||||
paypal.billingAgreement.execute(req.query.token, {}, cb);
|
|
||||||
},
|
|
||||||
get_user: function (cb) {
|
|
||||||
User.findById(req.session.userId, cb);
|
|
||||||
},
|
|
||||||
create_sub: ['exec', 'get_user', function (cb, results) {
|
|
||||||
payments.createSubscription({
|
|
||||||
user: results.get_user,
|
|
||||||
customerId: results.exec.id,
|
|
||||||
paymentMethod: 'Paypal',
|
|
||||||
sub: block
|
|
||||||
}, cb);
|
|
||||||
}]
|
|
||||||
},function(err){
|
|
||||||
if (err) return parseErr(res, err);
|
|
||||||
res.redirect('/');
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.createPayment = function(req, res) {
|
|
||||||
// if we're gifting to a user, put it in session for the `execute()`
|
|
||||||
req.session.gift = req.query.gift || undefined;
|
|
||||||
var gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
|
|
||||||
var price = !gift ? 5.00
|
|
||||||
: gift.type=='gems' ? Number(gift.gems.amount/4).toFixed(2)
|
|
||||||
: Number(shared.content.subscriptionBlocks[gift.subscription.key].price).toFixed(2);
|
|
||||||
var description = !gift ? "HabitRPG Gems"
|
|
||||||
: gift.type=='gems' ? "HabitRPG Gems (Gift)"
|
|
||||||
: shared.content.subscriptionBlocks[gift.subscription.key].months + "mo. HabitRPG Subscription (Gift)";
|
|
||||||
var create_payment = {
|
|
||||||
"intent": "sale",
|
|
||||||
"payer": {
|
|
||||||
"payment_method": "paypal"
|
|
||||||
},
|
|
||||||
"redirect_urls": {
|
|
||||||
"return_url": nconf.get('BASE_URL') + '/paypal/checkout/success',
|
|
||||||
"cancel_url": nconf.get('BASE_URL')
|
|
||||||
},
|
|
||||||
"transactions": [{
|
|
||||||
"item_list": {
|
|
||||||
"items": [{
|
|
||||||
"name": description,
|
|
||||||
//"sku": "1",
|
|
||||||
"price": price,
|
|
||||||
"currency": "USD",
|
|
||||||
"quantity": 1
|
|
||||||
}]
|
|
||||||
},
|
|
||||||
"amount": {
|
|
||||||
"currency": "USD",
|
|
||||||
"total": price
|
|
||||||
},
|
|
||||||
"description": description
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
paypal.payment.create(create_payment, function (err, payment) {
|
|
||||||
if (err) return parseErr(res, err);
|
|
||||||
var link = _.find(payment.links, {rel: 'approval_url'}).href;
|
|
||||||
res.redirect(link);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.executePayment = function(req, res) {
|
|
||||||
var paymentId = req.query.paymentId,
|
|
||||||
PayerID = req.query.PayerID,
|
|
||||||
gift = req.session.gift ? JSON.parse(req.session.gift) : undefined;
|
|
||||||
delete req.session.gift;
|
|
||||||
async.waterfall([
|
|
||||||
function(cb){
|
|
||||||
paypal.payment.execute(paymentId, {payer_id: PayerID}, cb);
|
|
||||||
},
|
|
||||||
function(payment, cb){
|
|
||||||
async.parallel([
|
|
||||||
function(cb2){ User.findById(req.session.userId, cb2); },
|
|
||||||
function(cb2){ User.findById(gift ? gift.uuid : undefined, cb2); }
|
|
||||||
], cb);
|
|
||||||
},
|
|
||||||
function(results, cb){
|
|
||||||
if (_.isEmpty(results[0])) return cb("User not found when completing paypal transaction");
|
|
||||||
var data = {user:results[0], customerId:PayerID, paymentMethod:'Paypal', gift:gift}
|
|
||||||
var method = 'buyGems';
|
|
||||||
if (gift) {
|
|
||||||
gift.member = results[1];
|
|
||||||
if (gift.type=='subscription') method = 'createSubscription';
|
|
||||||
data.paymentMethod = 'Gift';
|
|
||||||
}
|
|
||||||
payments[method](data, cb);
|
|
||||||
}
|
|
||||||
],function(err){
|
|
||||||
if (err) return parseErr(res, err);
|
|
||||||
res.redirect('/');
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.cancelSubscription = function(req, res, next){
|
|
||||||
var user = res.locals.user;
|
|
||||||
if (!user.purchased.plan.customerId)
|
|
||||||
return res.json(401, {err: "User does not have a plan subscription"});
|
|
||||||
async.auto({
|
|
||||||
get_cus: function(cb){
|
|
||||||
paypal.billingAgreement.get(user.purchased.plan.customerId, cb);
|
|
||||||
},
|
|
||||||
verify_cus: ['get_cus', function(cb, results){
|
|
||||||
var hasntBilledYet = results.get_cus.agreement_details.cycles_completed == "0";
|
|
||||||
if (hasntBilledYet)
|
|
||||||
return cb("The plan hasn't activated yet (due to a PayPal bug). It will begin "+results.get_cus.agreement_details.next_billing_date+", after which you can cancel to retain your full benefits");
|
|
||||||
cb();
|
|
||||||
}],
|
|
||||||
del_cus: ['verify_cus', function(cb, results){
|
|
||||||
paypal.billingAgreement.cancel(user.purchased.plan.customerId, {note: "Canceling the subscription"}, cb);
|
|
||||||
}],
|
|
||||||
cancel_sub: ['get_cus', 'verify_cus', function(cb, results){
|
|
||||||
var data = {user: user, paymentMethod: 'Paypal', nextBill: results.get_cus.agreement_details.next_billing_date};
|
|
||||||
payments.cancelSubscription(data, cb)
|
|
||||||
}]
|
|
||||||
}, function(err){
|
|
||||||
if (err) return parseErr(res, err);
|
|
||||||
res.redirect('/');
|
|
||||||
user = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* General IPN handler. We catch cancelled HabitRPG subscriptions for users who manually cancel their
|
|
||||||
* recurring paypal payments in their paypal dashboard. Remove this when we can move to webhooks or some other solution
|
|
||||||
*/
|
|
||||||
exports.ipn = function(req, res, next) {
|
|
||||||
console.log('IPN Called');
|
|
||||||
res.send(200); // Must respond to PayPal IPN request with an empty 200 first
|
|
||||||
ipn.verify(req.body, function(err, msg) {
|
|
||||||
if (err) return logger.error(msg);
|
|
||||||
switch (req.body.txn_type) {
|
|
||||||
// TODO what's the diff b/w the two data.txn_types below? The docs recommend subscr_cancel, but I'm getting the other one instead...
|
|
||||||
case 'recurring_payment_profile_cancel':
|
|
||||||
case 'subscr_cancel':
|
|
||||||
User.findOne({'purchased.plan.customerId':req.body.recurring_payment_id},function(err, user){
|
|
||||||
if (err) return logger.error(err);
|
|
||||||
if (_.isEmpty(user)) return; // looks like the cancellation was already handled properly above (see api.paypalSubscribeCancel)
|
|
||||||
payments.cancelSubscription({user:user, paymentMethod: 'Paypal'});
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
// This file is used for creating paypal billing plans. PayPal doesn't have a web interface for setting up recurring
|
|
||||||
// payment plan definitions, instead you have to create it via their REST SDK and keep it updated the same way. So this
|
|
||||||
// file will be used once for initing your billing plan (then you get the resultant plan.id to store in config.json),
|
|
||||||
// and once for any time you need to edit the plan thereafter
|
|
||||||
require('coffee-script');
|
|
||||||
var path = require('path');
|
|
||||||
var nconf = require('nconf');
|
|
||||||
_ = require('lodash');
|
|
||||||
nconf.argv().env().file('user', path.join(path.resolve(__dirname, '../../../config.json')));
|
|
||||||
var paypal = require('paypal-rest-sdk');
|
|
||||||
var blocks = require('../../../../common').content.subscriptionBlocks;
|
|
||||||
var live = nconf.get('PAYPAL:mode')=='live';
|
|
||||||
|
|
||||||
var OP = 'create'; // list create update remove
|
|
||||||
|
|
||||||
paypal.configure({
|
|
||||||
'mode': nconf.get("PAYPAL:mode"), //sandbox or live
|
|
||||||
'client_id': nconf.get("PAYPAL:client_id"),
|
|
||||||
'client_secret': nconf.get("PAYPAL:client_secret")
|
|
||||||
});
|
|
||||||
|
|
||||||
// https://developer.paypal.com/docs/api/#billing-plans-and-agreements
|
|
||||||
var billingPlanTitle ="HabitRPG Subscription";
|
|
||||||
var billingPlanAttributes = {
|
|
||||||
"name": billingPlanTitle,
|
|
||||||
"description": billingPlanTitle,
|
|
||||||
"type": "INFINITE",
|
|
||||||
"merchant_preferences": {
|
|
||||||
"auto_bill_amount": "yes",
|
|
||||||
"cancel_url": live ? 'https://habitrpg.com' : 'http://localhost:3000',
|
|
||||||
"return_url": (live ? 'https://habitrpg.com' : 'http://localhost:3000') + '/paypal/subscribe/success'
|
|
||||||
},
|
|
||||||
payment_definitions: [{
|
|
||||||
"type": "REGULAR",
|
|
||||||
"frequency": "MONTH",
|
|
||||||
"cycles": "0"
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
_.each(blocks, function(block){
|
|
||||||
block.definition = _.cloneDeep(billingPlanAttributes);
|
|
||||||
_.merge(block.definition.payment_definitions[0], {
|
|
||||||
"name": billingPlanTitle + ' ($'+block.price+' every '+block.months+' months, recurring)',
|
|
||||||
"frequency_interval": ""+block.months,
|
|
||||||
"amount": {
|
|
||||||
"currency": "USD",
|
|
||||||
"value": ""+block.price
|
|
||||||
}
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
switch(OP) {
|
|
||||||
case "list":
|
|
||||||
paypal.billingPlan.list({status: 'ACTIVE'}, function(err, plans){
|
|
||||||
console.log({err:err, plans:plans});
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "get":
|
|
||||||
paypal.billingPlan.get(nconf.get("PAYPAL:billing_plans:12"), function (err, plan) {
|
|
||||||
console.log({err:err, plan:plan});
|
|
||||||
})
|
|
||||||
break;
|
|
||||||
case "update":
|
|
||||||
var update = {
|
|
||||||
"op": "replace",
|
|
||||||
"path": "/merchant_preferences",
|
|
||||||
"value": {
|
|
||||||
"cancel_url": "https://habitrpg.com"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
paypal.billingPlan.update(nconf.get("PAYPAL:billing_plans:12"), update, function (err, res) {
|
|
||||||
console.log({err:err, plan:res});
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "create":
|
|
||||||
paypal.billingPlan.create(blocks["google_6mo"].definition, function(err,plan){
|
|
||||||
if (err) return console.log(err);
|
|
||||||
if (plan.state == "ACTIVE")
|
|
||||||
return console.log({err:err, plan:plan});
|
|
||||||
var billingPlanUpdateAttributes = [{
|
|
||||||
"op": "replace",
|
|
||||||
"path": "/",
|
|
||||||
"value": {
|
|
||||||
"state": "ACTIVE"
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
// Activate the plan by changing status to Active
|
|
||||||
paypal.billingPlan.update(plan.id, billingPlanUpdateAttributes, function(err, response){
|
|
||||||
console.log({err:err, response:response, id:plan.id});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "remove": break;
|
|
||||||
}
|
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
var nconf = require('nconf');
|
|
||||||
var stripe = require("stripe")(nconf.get('STRIPE_API_KEY'));
|
|
||||||
var async = require('async');
|
|
||||||
var payments = require('./index');
|
|
||||||
var User = require('mongoose').model('User');
|
|
||||||
var shared = require('../../../../common');
|
|
||||||
var mongoose = require('mongoose');
|
|
||||||
var cc = require('coupon-code');
|
|
||||||
|
|
||||||
/*
|
|
||||||
Setup Stripe response when posting payment
|
|
||||||
*/
|
|
||||||
exports.checkout = function(req, res, next) {
|
|
||||||
var token = req.body.id;
|
|
||||||
var user = res.locals.user;
|
|
||||||
var gift = req.query.gift ? JSON.parse(req.query.gift) : undefined;
|
|
||||||
var sub = req.query.sub ? shared.content.subscriptionBlocks[req.query.sub] : false;
|
|
||||||
|
|
||||||
async.waterfall([
|
|
||||||
function(cb){
|
|
||||||
if (sub) {
|
|
||||||
async.waterfall([
|
|
||||||
function(cb2){
|
|
||||||
if (!sub.discount) return cb2(null, null);
|
|
||||||
if (!req.query.coupon) return cb2('Please provide a coupon code for this plan.');
|
|
||||||
mongoose.model('Coupon').findOne({_id:cc.validate(req.query.coupon), event:sub.key}, cb2);
|
|
||||||
},
|
|
||||||
function(coupon, cb2){
|
|
||||||
if (sub.discount && !coupon) return cb2('Invalid coupon code.');
|
|
||||||
var customer = {
|
|
||||||
email: req.body.email,
|
|
||||||
metadata: {uuid: user._id},
|
|
||||||
card: token,
|
|
||||||
plan: sub.key
|
|
||||||
};
|
|
||||||
stripe.customers.create(customer, cb2);
|
|
||||||
}
|
|
||||||
], cb);
|
|
||||||
} else {
|
|
||||||
stripe.charges.create({
|
|
||||||
amount: !gift ? "500" //"500" = $5
|
|
||||||
: gift.type=='subscription' ? ""+shared.content.subscriptionBlocks[gift.subscription.key].price*100
|
|
||||||
: ""+gift.gems.amount/4*100,
|
|
||||||
currency: "usd",
|
|
||||||
card: token
|
|
||||||
}, cb);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function(response, cb) {
|
|
||||||
if (sub) return payments.createSubscription({user:user, customerId:response.id, paymentMethod:'Stripe', sub:sub}, cb);
|
|
||||||
async.waterfall([
|
|
||||||
function(cb2){ User.findById(gift ? gift.uuid : undefined, cb2) },
|
|
||||||
function(member, cb2){
|
|
||||||
var data = {user:user, customerId:response.id, paymentMethod:'Stripe', gift:gift};
|
|
||||||
var method = 'buyGems';
|
|
||||||
if (gift) {
|
|
||||||
gift.member = member;
|
|
||||||
if (gift.type=='subscription') method = 'createSubscription';
|
|
||||||
data.paymentMethod = 'Gift';
|
|
||||||
}
|
|
||||||
payments[method](data, cb2);
|
|
||||||
}
|
|
||||||
], cb);
|
|
||||||
}
|
|
||||||
], function(err){
|
|
||||||
if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors
|
|
||||||
res.send(200);
|
|
||||||
user = token = null;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.subscribeCancel = function(req, res, next) {
|
|
||||||
var user = res.locals.user;
|
|
||||||
if (!user.purchased.plan.customerId)
|
|
||||||
return res.json(401, {err: "User does not have a plan subscription"});
|
|
||||||
|
|
||||||
async.auto({
|
|
||||||
get_cus: function(cb){
|
|
||||||
stripe.customers.retrieve(user.purchased.plan.customerId, cb);
|
|
||||||
},
|
|
||||||
del_cus: ['get_cus', function(cb, results){
|
|
||||||
stripe.customers.del(user.purchased.plan.customerId, cb);
|
|
||||||
}],
|
|
||||||
cancel_sub: ['get_cus', function(cb, results) {
|
|
||||||
var data = {
|
|
||||||
user: user,
|
|
||||||
nextBill: results.get_cus.subscription.current_period_end*1000, // timestamp is in seconds
|
|
||||||
paymentMethod: 'Stripe'
|
|
||||||
};
|
|
||||||
payments.cancelSubscription(data, cb);
|
|
||||||
}]
|
|
||||||
}, function(err, results){
|
|
||||||
if (err) return res.send(500, err.toString()); // don't json this, let toString() handle errors
|
|
||||||
res.redirect('/');
|
|
||||||
user = null;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
exports.subscribeEdit = function(req, res, next) {
|
|
||||||
var token = req.body.id;
|
|
||||||
var user = res.locals.user;
|
|
||||||
var user_id = user.purchased.plan.customerId;
|
|
||||||
var sub_id;
|
|
||||||
|
|
||||||
async.waterfall([
|
|
||||||
function(cb){
|
|
||||||
stripe.customers.listSubscriptions(user_id, cb);
|
|
||||||
},
|
|
||||||
function(response, cb) {
|
|
||||||
sub_id = response.data[0].id;
|
|
||||||
console.warn(sub_id);
|
|
||||||
console.warn([user_id, sub_id, { card: token }]);
|
|
||||||
stripe.customers.updateSubscription(user_id, sub_id, { card: token }, cb);
|
|
||||||
},
|
|
||||||
function(response, cb) {
|
|
||||||
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);
|
|
||||||
token = user = user_id = sub_id;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,572 +0,0 @@
|
|||||||
/* @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('../../../common');
|
|
||||||
var User = require('./../models/user').model;
|
|
||||||
var utils = require('./../utils');
|
|
||||||
var ga = utils.ga;
|
|
||||||
var Group = require('./../models/group').model;
|
|
||||||
var Challenge = require('./../models/challenge').model;
|
|
||||||
var moment = require('moment');
|
|
||||||
var logging = require('./../logging');
|
|
||||||
var acceptablePUTPaths;
|
|
||||||
var api = module.exports;
|
|
||||||
var qs = require('qs');
|
|
||||||
var request = require('request');
|
|
||||||
var validator = require('validator');
|
|
||||||
|
|
||||||
// api.purchase // Shared.ops
|
|
||||||
|
|
||||||
api.getContent = function(req, res, next) {
|
|
||||||
var language = 'en';
|
|
||||||
|
|
||||||
if(typeof req.query.language != 'undefined')
|
|
||||||
language = req.query.language.toString(); //|| 'en' in i18n
|
|
||||||
|
|
||||||
var content = _.cloneDeep(shared.content);
|
|
||||||
var walk = function(obj, lang){
|
|
||||||
_.each(obj, function(item, key, source){
|
|
||||||
if(_.isPlainObject(item) || _.isArray(item)) return walk(item, lang);
|
|
||||||
if(_.isFunction(item) && item.i18nLangFunc) source[key] = item(lang);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
walk(content, language);
|
|
||||||
res.json(content);
|
|
||||||
}
|
|
||||||
|
|
||||||
api.getModelPaths = function(req,res,next){
|
|
||||||
res.json(_.reduce(User.schema.paths,function(m,v,k){
|
|
||||||
m[k] = v.instance || 'Boolean';
|
|
||||||
return m;
|
|
||||||
},{}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
Tasks
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
Local Methods
|
|
||||||
---------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
var findTask = function(req, res) {
|
|
||||||
return 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;
|
|
||||||
|
|
||||||
var clearMemory = function(){user = task = id = direction = null;}
|
|
||||||
|
|
||||||
// Send error responses for improper API call
|
|
||||||
if (!id) return res.json(400, {err: ':id required'});
|
|
||||||
if (direction !== 'up' && direction !== 'down') {
|
|
||||||
if (direction == 'unlink' || direction == 'sort') 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
|
|
||||||
// Defaults. Other defaults are handled in user.ops.addTask()
|
|
||||||
task = {
|
|
||||||
id: id,
|
|
||||||
type: req.body && req.body.type,
|
|
||||||
text: req.body && req.body.text,
|
|
||||||
notes: (req.body && req.body.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."
|
|
||||||
};
|
|
||||||
task = user.ops.addTask({body:task});
|
|
||||||
if (task.type === 'daily' || task.type === 'todo')
|
|
||||||
task.completed = direction === 'up';
|
|
||||||
}
|
|
||||||
var delta = user.ops.score({params:{id:task.id, direction:direction}, language: req.language});
|
|
||||||
|
|
||||||
user.save(function(err,saved){
|
|
||||||
if (err) return next(err);
|
|
||||||
// TODO this should be return {_v,task,stats,_tmp}, instead of merging everything togther at top-level response
|
|
||||||
// However, this is the most commonly used API route, and changing it will mess with all 3rd party consumers. Bad idea :(
|
|
||||||
res.json(200, _.extend({
|
|
||||||
delta: delta,
|
|
||||||
_tmp: user._tmp
|
|
||||||
}, saved.toJSON().stats));
|
|
||||||
|
|
||||||
// Webhooks
|
|
||||||
_.each(user.preferences.webhooks, function(h){
|
|
||||||
if (!h.enabled || !validator.isURL(h.url)) return;
|
|
||||||
request.post({
|
|
||||||
url: h.url,
|
|
||||||
//form: {task: task, delta: delta, user: _.pick(user, ['stats', '_tmp'])} // this is causing "Maximum Call Stack Exceeded"
|
|
||||||
body: {direction:direction, task: task, delta: delta, user: _.pick(user, ['_id', 'stats', '_tmp'])}, json:true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
(!task.challenge || !task.challenge.id || task.challenge.broken) // If it's a challenge task, sync the score. Do it in the background, we've already sent down a response and the user doesn't care what happens back there
|
|
||||||
|| (task.type == 'reward') // we don't want to update the reward GP cost
|
|
||||||
) return clearMemory();
|
|
||||||
Challenge.findById(task.challenge.id, 'habits dailys todos rewards', function(err, chal){
|
|
||||||
if (err) return next(err);
|
|
||||||
if (!chal) {
|
|
||||||
task.challenge.broken = 'CHALLENGE_DELETED';
|
|
||||||
user.save();
|
|
||||||
return clearMemory();
|
|
||||||
}
|
|
||||||
var t = chal.tasks[task.id];
|
|
||||||
// this task was removed from the challenge, notify user
|
|
||||||
if (!t) {
|
|
||||||
chal.syncToUser(user);
|
|
||||||
return clearMemory();
|
|
||||||
}
|
|
||||||
t.value += delta;
|
|
||||||
if (t.type == 'habit' || t.type == 'daily')
|
|
||||||
t.history.push({value: t.value, date: +new Date});
|
|
||||||
chal.save();
|
|
||||||
clearMemory();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
|
|
||||||
api.getBuyList = function (req, res, next) {
|
|
||||||
var list = shared.updateStore(res.locals.user);
|
|
||||||
return res.json(200, list);
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
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;
|
|
||||||
user.stats.maxMP = res.locals.user._statsComputed.maxMP;
|
|
||||||
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 inbox'.split(' '), function(root){
|
|
||||||
return leaf.indexOf(root) == 0;
|
|
||||||
});
|
|
||||||
if (found) m[leaf]=true;
|
|
||||||
return m;
|
|
||||||
}, {})
|
|
||||||
|
|
||||||
//// Uncomment this if we we want to disable GP-restoring (eg, holiday events)
|
|
||||||
//_.each('stats.gp'.split(' '), function(removePath){
|
|
||||||
// delete acceptablePUTPaths[removePath];
|
|
||||||
//})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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(401, {err: errors});
|
|
||||||
if (err) return next(err);
|
|
||||||
res.json(200, user);
|
|
||||||
user = errors = null;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
api.cron = function(req, res, next) {
|
|
||||||
var user = res.locals.user,
|
|
||||||
progress = user.fns.cron(),
|
|
||||||
ranCron = user.isModified(),
|
|
||||||
quest = shared.content.quests[user.party.quest.key];
|
|
||||||
|
|
||||||
if (ranCron) res.locals.wasModified = true;
|
|
||||||
if (!ranCron) return next(null,user);
|
|
||||||
Group.tavernBoss(user,progress);
|
|
||||||
if (!quest) return user.save(next);
|
|
||||||
|
|
||||||
// If user is on a quest, roll for boss & player, or handle collections
|
|
||||||
// FIXME this saves user, runs db updates, loads user. Is there a better way to handle this?
|
|
||||||
async.waterfall([
|
|
||||||
function(cb){
|
|
||||||
user.save(cb); // make sure to save the cron effects
|
|
||||||
},
|
|
||||||
function(saved, count, cb){
|
|
||||||
var type = quest.boss ? 'boss' : 'collect';
|
|
||||||
Group[type+'Quest'](user,progress,cb);
|
|
||||||
},
|
|
||||||
function(){
|
|
||||||
var cb = arguments[arguments.length-1];
|
|
||||||
// User has been updated in boss-grapple, reload
|
|
||||||
User.findById(user._id, cb);
|
|
||||||
}
|
|
||||||
], function(err, saved) {
|
|
||||||
res.locals.user = saved;
|
|
||||||
next(err,saved);
|
|
||||||
user = progress = quest = null;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// api.reroll // Shared.ops
|
|
||||||
// api.reset // Shared.ops
|
|
||||||
|
|
||||||
api['delete'] = function(req, res, next) {
|
|
||||||
var plan = res.locals.user.purchased.plan;
|
|
||||||
if (plan && plan.customerId && !plan.dateTerminated)
|
|
||||||
return res.json(400,{err:"You have an active subscription, cancel your plan before deleting your account."});
|
|
||||||
res.locals.user.remove(function(err){
|
|
||||||
if (err) return next(err);
|
|
||||||
res.send(200);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
Gems
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
// api.unlock // see Shared.ops
|
|
||||||
|
|
||||||
api.addTenGems = function(req, res, next) {
|
|
||||||
var user = res.locals.user;
|
|
||||||
user.balance += 2.5;
|
|
||||||
user.save(function(err){
|
|
||||||
if (err) return next(err);
|
|
||||||
res.send(204);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
Tags
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
// api.deleteTag // handled in Shared.ops
|
|
||||||
// api.addTag // handled in Shared.ops
|
|
||||||
// api.updateTag // handled in Shared.ops
|
|
||||||
// api.sortTag // handled in Shared.ops
|
|
||||||
|
|
||||||
/*
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
Spells
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
api.cast = function(req, res, next) {
|
|
||||||
var user = res.locals.user,
|
|
||||||
targetType = req.query.targetType,
|
|
||||||
targetId = req.query.targetId,
|
|
||||||
klass = shared.content.spells.special[req.params.spell] ? 'special' : user.stats.class,
|
|
||||||
spell = shared.content.spells[klass][req.params.spell];
|
|
||||||
|
|
||||||
if (!spell) return res.json(404, {err: 'Spell "' + req.params.spell + '" not found.'});
|
|
||||||
if (spell.mana > user.stats.mp) return res.json(400, {err: 'Not enough mana to cast spell'});
|
|
||||||
|
|
||||||
var done = function(){
|
|
||||||
var err = arguments[0];
|
|
||||||
var saved = _.size(arguments == 3) ? arguments[2] : arguments[1];
|
|
||||||
if (err) return next(err);
|
|
||||||
res.json(saved);
|
|
||||||
user = targetType = targetId = klass = spell = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (targetType) {
|
|
||||||
case 'task':
|
|
||||||
if (!user.tasks[targetId]) return res.json(404, {err: 'Task "' + targetId + '" not found.'});
|
|
||||||
spell.cast(user, user.tasks[targetId]);
|
|
||||||
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', 'profile.name stats achievements items.special').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 (targetType == '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: targetId})
|
|
||||||
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() + (targetType=='user' ? ' on '+found.profile.name : ' for the party')+'.`';
|
|
||||||
group.sendChat(message);
|
|
||||||
group.save(cb2);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
series.push(function(cb2){g = group = series = found = null;cb2();})
|
|
||||||
|
|
||||||
async.series(series, cb);
|
|
||||||
},
|
|
||||||
function(whatever, cb){
|
|
||||||
user.save(cb);
|
|
||||||
}
|
|
||||||
], done);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* POST /user/invite-friends
|
|
||||||
*/
|
|
||||||
api.inviteFriends = function(req, res, next) {
|
|
||||||
Group.findOne({type:'party', members:{'$in': [res.locals.user._id]}}).select('_id name').exec(function(err,party){
|
|
||||||
if (err) return next(err);
|
|
||||||
|
|
||||||
_.each(req.body.emails, function(invite){
|
|
||||||
if (invite.email) {
|
|
||||||
|
|
||||||
User.findOne({$or: [
|
|
||||||
{'auth.local.email': invite.email},
|
|
||||||
{'auth.facebook.emails.value': invite.email}
|
|
||||||
]}).select({_id: true, 'preferences.emailNotifications': true})
|
|
||||||
.exec(function(err, userToContact){
|
|
||||||
if(err) return next(err);
|
|
||||||
|
|
||||||
var link = nconf.get('BASE_URL')+'?partyInvite='+ utils.encrypt(JSON.stringify({id:party._id, inviter:res.locals.user._id, name:party.name}));
|
|
||||||
|
|
||||||
var variables = [
|
|
||||||
{name: 'LINK', content: link},
|
|
||||||
{name: 'INVITER', content: req.body.inviter || utils.getUserInfo(res.locals.user, ['name']).name}
|
|
||||||
];
|
|
||||||
|
|
||||||
invite.canSend = true;
|
|
||||||
|
|
||||||
// We check for unsubscribeFromAll here because don't pass through utils.getUserInfo
|
|
||||||
if(!userToContact || (userToContact.preferences.emailNotifications.invitedParty !== false &&
|
|
||||||
userToContact.preferences.emailNotifications.unsubscribeFromAll !== true)){
|
|
||||||
// TODO implement "users can only be invited once"
|
|
||||||
utils.txnEmail(invite, 'invite-friend', variables);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
res.send(200);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
api.sessionPartyInvite = function(req,res,next){
|
|
||||||
if (!req.session.partyInvite) return next();
|
|
||||||
var inv = res.locals.user.invitations;
|
|
||||||
if (inv.party && inv.party.id) return next(); // already invited to a party
|
|
||||||
async.waterfall([
|
|
||||||
function(cb){
|
|
||||||
Group.findOne({_id:req.session.partyInvite.id, type:'party', members:{$in:[req.session.partyInvite.inviter]}})
|
|
||||||
.select('invites members').exec(cb);
|
|
||||||
},
|
|
||||||
function(group, cb){
|
|
||||||
if (!group){
|
|
||||||
// Don't send error as it will prevent users from using the site
|
|
||||||
delete req.session.partyInvite;
|
|
||||||
return cb();
|
|
||||||
}
|
|
||||||
inv.party = req.session.partyInvite;
|
|
||||||
delete req.session.partyInvite;
|
|
||||||
if (!~group.invites.indexOf(res.locals.user._id))
|
|
||||||
group.invites.push(res.locals.user._id); //$addToSt
|
|
||||||
group.save(cb);
|
|
||||||
},
|
|
||||||
function(saved, cb){
|
|
||||||
res.locals.user.save(cb);
|
|
||||||
}
|
|
||||||
], next);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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, response){
|
|
||||||
// If we want to send something other than 500, pass err as {code: 200, message: "Not enough GP"}
|
|
||||||
if (err) {
|
|
||||||
if (!err.code) return next(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 next(err);
|
|
||||||
res.json(200,response);
|
|
||||||
})
|
|
||||||
}, ga);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
/*
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
Batch Update
|
|
||||||
Run a bunch of updates all at once
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
api.batchUpdate = function(req, res, next) {
|
|
||||||
if (_.isEmpty(req.body)) req.body = []; // cases of {} or null
|
|
||||||
if (req.body[0] && req.body[0].data)
|
|
||||||
return res.json(501, {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;
|
|
||||||
|
|
||||||
// Stash user.save, we'll queue the save op till the end (so we don't overload the server)
|
|
||||||
var oldSave = user.save;
|
|
||||||
user.save = function(cb){cb(null,user)}
|
|
||||||
|
|
||||||
// Setup the array of functions we're going to call in parallel with async
|
|
||||||
res.locals.ops = [];
|
|
||||||
var ops = _.transform(req.body, function(m,_req){
|
|
||||||
if (_.isEmpty(_req)) return;
|
|
||||||
_req.language = req.language;
|
|
||||||
|
|
||||||
m.push(function() {
|
|
||||||
var cb = arguments[arguments.length-1];
|
|
||||||
res.locals.ops.push(_req);
|
|
||||||
res.send = res.json = function(code, data) {
|
|
||||||
if (_.isNumber(code) && code >= 500)
|
|
||||||
return cb(code+": "+ (data.message ? data.message : data.err ? data.err : JSON.stringify(data)));
|
|
||||||
return cb();
|
|
||||||
};
|
|
||||||
api[_req.op](_req, res, cb);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
// Finally, save user at the end
|
|
||||||
.concat(function(){
|
|
||||||
user.save = oldSave;
|
|
||||||
user.save(arguments[arguments.length-1]);
|
|
||||||
});
|
|
||||||
|
|
||||||
// call all the operations, then return the user object to the requester
|
|
||||||
async.waterfall(ops, function(err,_user) {
|
|
||||||
res.json = oldJson;
|
|
||||||
res.send = oldSend;
|
|
||||||
if (err) return next(err);
|
|
||||||
|
|
||||||
var response = _user.toJSON();
|
|
||||||
response.wasModified = res.locals.wasModified;
|
|
||||||
|
|
||||||
user.fns.nullify();
|
|
||||||
user = res.locals.user = oldSend = oldJson = oldSave = null;
|
|
||||||
|
|
||||||
// return only drops & streaks
|
|
||||||
if (response._tmp && response._tmp.drop){
|
|
||||||
res.json(200, {_tmp: {drop: response._tmp.drop}, _v: response._v});
|
|
||||||
|
|
||||||
// Fetch full user object
|
|
||||||
}else if(response.wasModified){
|
|
||||||
// Preen 3-day past-completed To-Dos from Angular & mobile app
|
|
||||||
response.todos = _.where(response.todos, function(t) {
|
|
||||||
return !t.completed || (t.challenge && t.challenge.id) || moment(t.dateCompleted).isAfter(moment().subtract({days:3}));
|
|
||||||
});
|
|
||||||
res.json(200, response);
|
|
||||||
|
|
||||||
// return only the version number
|
|
||||||
}else{
|
|
||||||
res.json(200, {_v: response._v});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
var fs = require('fs'),
|
|
||||||
path = require('path'),
|
|
||||||
_ = require('lodash'),
|
|
||||||
User = require('./models/user').model,
|
|
||||||
shared = require('../../common'),
|
|
||||||
translations = {};
|
|
||||||
|
|
||||||
var localePath = path.join(__dirname, "/../../common/locales/")
|
|
||||||
|
|
||||||
var loadTranslations = function(locale){
|
|
||||||
var files = fs.readdirSync(path.join(localePath, locale));
|
|
||||||
translations[locale] = {};
|
|
||||||
_.each(files, function(file){
|
|
||||||
if(path.extname(file) !== '.json') return;
|
|
||||||
_.merge(translations[locale], require(path.join(localePath, locale, file)));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// First fetch english so we can merge with missing strings in other languages
|
|
||||||
loadTranslations('en');
|
|
||||||
|
|
||||||
fs.readdirSync(localePath).forEach(function(file) {
|
|
||||||
if(file === 'en' || fs.statSync(path.join(localePath, file)).isDirectory() === false) return;
|
|
||||||
loadTranslations(file);
|
|
||||||
// Merge missing strings from english
|
|
||||||
_.defaults(translations[file], translations.en);
|
|
||||||
});
|
|
||||||
|
|
||||||
var langCodes = Object.keys(translations);
|
|
||||||
|
|
||||||
var avalaibleLanguages = _.map(langCodes, function(langCode){
|
|
||||||
return {
|
|
||||||
code: langCode,
|
|
||||||
name: translations[langCode].languageName
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load MomentJS localization files
|
|
||||||
var momentLangs = {};
|
|
||||||
|
|
||||||
// Handle different language codes from MomentJS and /locales
|
|
||||||
var momentLangsMapping = {
|
|
||||||
'en': 'en-gb',
|
|
||||||
'en_GB': 'en-gb',
|
|
||||||
'no': 'nn',
|
|
||||||
'zh': 'zh-cn',
|
|
||||||
'es_419': 'es'
|
|
||||||
};
|
|
||||||
|
|
||||||
var momentLangs = {};
|
|
||||||
|
|
||||||
_.each(langCodes, function(code){
|
|
||||||
var lang = _.find(avalaibleLanguages, {code: code});
|
|
||||||
lang.momentLangCode = (momentLangsMapping[code] || code);
|
|
||||||
try{
|
|
||||||
// MomentJS lang files are JS files that has to be executed in the browser so we load them as plain text files
|
|
||||||
var f = fs.readFileSync(path.join(__dirname, '/../../node_modules/moment/locale/' + lang.momentLangCode + '.js'), 'utf8');
|
|
||||||
momentLangs[code] = f;
|
|
||||||
}catch (e){}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove en_GB from langCodes checked by browser to avaoi it being
|
|
||||||
// used in place of plain original 'en'
|
|
||||||
var defaultLangCodes = _.without(langCodes, 'en_GB');
|
|
||||||
|
|
||||||
var getUserLanguage = function(req, res, next){
|
|
||||||
var getFromBrowser = function(){
|
|
||||||
var acceptable = _(req.acceptedLanguages).map(function(lang){
|
|
||||||
return lang.slice(0, 2);
|
|
||||||
}).uniq().value();
|
|
||||||
var matches = _.intersection(acceptable, defaultLangCodes);
|
|
||||||
if(matches.length > 0 && matches[0].toLowerCase() === 'es'){
|
|
||||||
var acceptedCompleteLang = _.find(req.acceptedLanguages, function(accepted){
|
|
||||||
return accepted.slice(0, 2) == 'es';
|
|
||||||
});
|
|
||||||
|
|
||||||
if(acceptedCompleteLang){
|
|
||||||
acceptedCompleteLang = acceptedCompleteLang.toLowerCase();
|
|
||||||
}else{
|
|
||||||
return 'en';
|
|
||||||
}
|
|
||||||
|
|
||||||
var latinAmericanSpanishes = ['es-419', 'es-mx', 'es-gt', 'es-cr', 'es-pa', 'es-do', 'es-ve', 'es-co', 'es-pe',
|
|
||||||
'es-ar', 'es-ec', 'es-cl', 'es-uy', 'es-py', 'es-bo', 'es-sv', 'es-hn',
|
|
||||||
'es-ni', 'es-pr'];
|
|
||||||
|
|
||||||
return (latinAmericanSpanishes.indexOf(acceptedCompleteLang) !== -1) ? 'es_419' : 'es';
|
|
||||||
}else if(matches.length > 0){
|
|
||||||
return matches[0].toLowerCase();
|
|
||||||
}else{
|
|
||||||
return 'en';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var getFromUser = function(user){
|
|
||||||
var lang;
|
|
||||||
if(user && user.preferences.language && translations[user.preferences.language]){
|
|
||||||
lang = user.preferences.language;
|
|
||||||
}else{
|
|
||||||
var preferred = getFromBrowser();
|
|
||||||
lang = translations[preferred] ? preferred : 'en';
|
|
||||||
}
|
|
||||||
req.language = lang;
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
if(req.query.lang){
|
|
||||||
req.language = translations[req.query.lang] ? (req.query.lang) : 'en';
|
|
||||||
next();
|
|
||||||
}else if(req.locals && req.locals.user){
|
|
||||||
getFromUser(req.locals.user);
|
|
||||||
}else if(req.session && req.session.userId){
|
|
||||||
User.findOne({_id: req.session.userId}, function(err, user){
|
|
||||||
if(err) return next(err);
|
|
||||||
getFromUser(user);
|
|
||||||
});
|
|
||||||
}else{
|
|
||||||
getFromUser(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
shared.i18n.translations = translations;
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
translations: translations,
|
|
||||||
avalaibleLanguages: avalaibleLanguages,
|
|
||||||
langCodes: langCodes,
|
|
||||||
getUserLanguage: getUserLanguage,
|
|
||||||
momentLangs: momentLangs
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
// Export en strings only, temporary solution for mobile
|
|
||||||
// This is copied from middleware.js#module.exports.locals#t()
|
|
||||||
module.exports.enTranslations = function(){ // stringName and vars are the allowed parameters
|
|
||||||
var language = _.find(avalaibleLanguages, {code: 'en'});
|
|
||||||
//language.momentLang = ((!isStaticPage && i18n.momentLangs[language.code]) || undefined);
|
|
||||||
var args = Array.prototype.slice.call(arguments, 0);
|
|
||||||
args.push(language.code);
|
|
||||||
return shared.i18n.t.apply(null, args);
|
|
||||||
};
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
var nconf = require('nconf');
|
|
||||||
var winston = require('winston');
|
|
||||||
require('winston-mail').Mail;
|
|
||||||
require('winston-newrelic');
|
|
||||||
|
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (logger == null) {
|
|
||||||
logger = new (winston.Logger)({});
|
|
||||||
if (nconf.get('NODE_ENV') == 'production') {
|
|
||||||
//logger.add(winston.transports.newrelic, {});
|
|
||||||
if (!nconf.get('DISABLE_ERROR_EMAILS')) {
|
|
||||||
logger.add(winston.transports.Mail, {
|
|
||||||
to: nconf.get('ADMIN_EMAIL') || nconf.get('SMTP_USER'),
|
|
||||||
from: "HabitRPG <" + nconf.get('SMTP_USER') + ">",
|
|
||||||
subject: "HabitRPG Error",
|
|
||||||
host: nconf.get('SMTP_HOST'),
|
|
||||||
port: nconf.get('SMTP_PORT'),
|
|
||||||
tls: nconf.get('SMTP_TLS'),
|
|
||||||
username: nconf.get('SMTP_USER'),
|
|
||||||
password: nconf.get('SMTP_PASS'),
|
|
||||||
level: 'error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.add(winston.transports.Console, {colorize:true});
|
|
||||||
logger.add(winston.transports.File, {filename: 'habitrpg.log'});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A custom log function that wraps Winston. Makes it easy to instrument code
|
|
||||||
// and still possible to replace Winston in the future.
|
|
||||||
module.exports.log = function(/* variable args */) {
|
|
||||||
if (logger)
|
|
||||||
logger.log.apply(logger, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.info = function(/* variable args */) {
|
|
||||||
if (logger)
|
|
||||||
logger.info.apply(logger, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.warn = function(/* variable args */) {
|
|
||||||
if (logger)
|
|
||||||
logger.warn.apply(logger, arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
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,210 +0,0 @@
|
|||||||
var nconf = require('nconf');
|
|
||||||
var _ = require('lodash');
|
|
||||||
var fs = require('fs');
|
|
||||||
var path = require('path');
|
|
||||||
var User = require('./models/user').model
|
|
||||||
var limiter = require('connect-ratelimit');
|
|
||||||
var logging = require('./logging');
|
|
||||||
var domainMiddleware = require('domain-middleware');
|
|
||||||
var cluster = require('cluster');
|
|
||||||
var i18n = require('./i18n.js');
|
|
||||||
var shared = require('../../common');
|
|
||||||
var request = require('request');
|
|
||||||
var os = require('os');
|
|
||||||
var moment = require('moment');
|
|
||||||
var utils = require('./utils');
|
|
||||||
|
|
||||||
module.exports.apiThrottle = function(app) {
|
|
||||||
if (nconf.get('NODE_ENV') !== 'production') return;
|
|
||||||
app.use(limiter({
|
|
||||||
end:false,
|
|
||||||
catagories:{
|
|
||||||
normal: {
|
|
||||||
// 2 req/s, but split as minutes
|
|
||||||
totalRequests: 80,
|
|
||||||
every: 60000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})).use(function(req,res,next){
|
|
||||||
//logging.info(res.ratelimit);
|
|
||||||
if (res.ratelimit.exceeded) return res.json(429,{err:'Rate limit exceeded'});
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.domainMiddleware = function(server,mongoose) {
|
|
||||||
if (nconf.get('NODE_ENV')=='production') {
|
|
||||||
var mins = 3, // how often to run this check
|
|
||||||
useAvg = false, // use average over 3 minutes, or simply the last minute's report
|
|
||||||
url = 'https://api.newrelic.com/v2/applications/'+nconf.get('NEW_RELIC_APPLICATION_ID')+'/metrics/data.json?names[]=Apdex&values[]=score';
|
|
||||||
setInterval(function(){
|
|
||||||
// see https://docs.newrelic.com/docs/apm/apis/api-v2-examples/average-response-time-examples-api-v2, https://rpm.newrelic.com/api/explore/applications/data
|
|
||||||
request({
|
|
||||||
url: useAvg ? url+'&from='+moment().subtract({minutes:mins}).utc().format()+'&to='+moment().utc().format()+'&summarize=true' : url,
|
|
||||||
headers: {'X-Api-Key': nconf.get('NEW_RELIC_API_KEY')}
|
|
||||||
}, function(err, response, body){
|
|
||||||
var ts = JSON.parse(body).metric_data.metrics[0].timeslices,
|
|
||||||
score = ts[ts.length-1].values.score,
|
|
||||||
apdexBad = score < .75 || score == 1,
|
|
||||||
memory = os.freemem() / os.totalmem(),
|
|
||||||
memoryHigh = false; //memory < 0.1;
|
|
||||||
if (apdexBad || memoryHigh) throw "[Memory Leak] Apdex="+score+" Memory="+parseFloat(memory).toFixed(3)+" Time="+moment().format();
|
|
||||||
})
|
|
||||||
}, mins*60*1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
return domainMiddleware({
|
|
||||||
server: {
|
|
||||||
close:function(){
|
|
||||||
server.close();
|
|
||||||
mongoose.connection.close();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
killTimeout: 10000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.errorHandler = function(err, req, res, next) {
|
|
||||||
//res.locals.domain.emit('error', err);
|
|
||||||
// when we hit an error, send it to admin as an email. If no ADMIN_EMAIL is present, just send it to yourself (SMTP_USER)
|
|
||||||
var stack = (err.stack ? err.stack : err.message ? err.message : err) +
|
|
||||||
"\n ----------------------------\n" +
|
|
||||||
"\n\noriginalUrl: " + req.originalUrl +
|
|
||||||
"\n\nauth: " + req.headers['x-api-user'] + ' | ' + req.headers['x-api-key'] +
|
|
||||||
"\n\nheaders: " + JSON.stringify(req.headers) +
|
|
||||||
"\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.json(500,{err:message}); //res.end(err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
module.exports.forceSSL = function(req, res, next){
|
|
||||||
var baseUrl = nconf.get("BASE_URL");
|
|
||||||
// Note x-forwarded-proto is used by Heroku & nginx, you'll have to do something different if you're not using those
|
|
||||||
if (req.headers['x-forwarded-proto'] && req.headers['x-forwarded-proto'] !== 'https'
|
|
||||||
&& nconf.get('NODE_ENV') === 'production'
|
|
||||||
&& baseUrl.indexOf('https') === 0) {
|
|
||||||
return res.redirect(baseUrl + req.url);
|
|
||||||
}
|
|
||||||
next()
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.cors = function(req, res, next) {
|
|
||||||
res.header("Access-Control-Allow-Origin", req.headers.origin || "*");
|
|
||||||
res.header("Access-Control-Allow-Methods", "OPTIONS,GET,POST,PUT,HEAD,DELETE");
|
|
||||||
res.header("Access-Control-Allow-Headers", "Content-Type,Accept,Content-Encoding,X-Requested-With,x-api-user,x-api-key");
|
|
||||||
if (req.method === 'OPTIONS') return res.send(200);
|
|
||||||
return next();
|
|
||||||
};
|
|
||||||
|
|
||||||
var siteVersion = 1;
|
|
||||||
|
|
||||||
module.exports.forceRefresh = function(req, res, next){
|
|
||||||
if(req.query.siteVersion && req.query.siteVersion != siteVersion){
|
|
||||||
return res.json(400, {needRefresh: true});
|
|
||||||
}
|
|
||||||
|
|
||||||
return next();
|
|
||||||
};
|
|
||||||
|
|
||||||
var buildFiles = [];
|
|
||||||
|
|
||||||
var walk = function(folder){
|
|
||||||
var res = fs.readdirSync(folder);
|
|
||||||
|
|
||||||
res.forEach(function(fileName){
|
|
||||||
file = folder + '/' + fileName;
|
|
||||||
if(fs.statSync(file).isDirectory()){
|
|
||||||
walk(file);
|
|
||||||
}else{
|
|
||||||
var relFolder = path.relative(path.join(__dirname, "/../build"), folder);
|
|
||||||
var old = fileName.replace(/-.{8}(\.[\d\w]+)$/, '$1');
|
|
||||||
|
|
||||||
if(relFolder){
|
|
||||||
old = relFolder + '/' + old;
|
|
||||||
fileName = relFolder + '/' + fileName;
|
|
||||||
}
|
|
||||||
|
|
||||||
buildFiles[old] = fileName
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
walk(path.join(__dirname, "/../build"));
|
|
||||||
|
|
||||||
var getBuildUrl = function(url){
|
|
||||||
if(buildFiles[url]) return '/' + buildFiles[url];
|
|
||||||
|
|
||||||
return '/' + url;
|
|
||||||
}
|
|
||||||
|
|
||||||
var manifestFiles = require("../public/manifest.json");
|
|
||||||
|
|
||||||
var getManifestFiles = function(page){
|
|
||||||
var files = manifestFiles[page];
|
|
||||||
|
|
||||||
if(!files) throw new Error("Page not found!");
|
|
||||||
|
|
||||||
var code = '';
|
|
||||||
|
|
||||||
if(nconf.get('NODE_ENV') === 'production'){
|
|
||||||
code += '<link rel="stylesheet" type="text/css" href="' + getBuildUrl(page + '.css') + '">';
|
|
||||||
code += '<script type="text/javascript" src="' + getBuildUrl(page + '.js') + '"></script>';
|
|
||||||
}else{
|
|
||||||
_.each(files.css, function(file){
|
|
||||||
code += '<link rel="stylesheet" type="text/css" href="' + getBuildUrl(file) + '">';
|
|
||||||
});
|
|
||||||
_.each(files.js, function(file){
|
|
||||||
code += '<script type="text/javascript" src="' + getBuildUrl(file) + '"></script>';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.locals = function(req, res, next) {
|
|
||||||
var language = _.find(i18n.avalaibleLanguages, {code: req.language});
|
|
||||||
var isStaticPage = req.url.split('/')[1] === 'static'; // If url contains '/static/'
|
|
||||||
|
|
||||||
// Load moment.js language file only when not on static pages
|
|
||||||
language.momentLang = ((!isStaticPage && i18n.momentLangs[language.code]) || undefined);
|
|
||||||
|
|
||||||
var tavern = require('./models/group').tavern;
|
|
||||||
var envVars = _.pick(nconf.get(), 'NODE_ENV BASE_URL GA_ID STRIPE_PUB_KEY FACEBOOK_KEY'.split(' '));
|
|
||||||
res.locals.habitrpg = _.merge(envVars, {
|
|
||||||
IS_MOBILE: /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(req.header('User-Agent')),
|
|
||||||
getManifestFiles: getManifestFiles,
|
|
||||||
getBuildUrl: getBuildUrl,
|
|
||||||
avalaibleLanguages: i18n.avalaibleLanguages,
|
|
||||||
language: language,
|
|
||||||
isStaticPage: isStaticPage,
|
|
||||||
translations: i18n.translations[language.code],
|
|
||||||
t: function(){ // stringName and vars are the allowed parameters
|
|
||||||
var args = Array.prototype.slice.call(arguments, 0);
|
|
||||||
args.push(language.code);
|
|
||||||
return shared.i18n.t.apply(null, args);
|
|
||||||
},
|
|
||||||
siteVersion: siteVersion,
|
|
||||||
Content: shared.content,
|
|
||||||
mods: require('./models/user').mods,
|
|
||||||
tavern: tavern, // for world boss
|
|
||||||
worldDmg: (tavern && tavern.quest && tavern.quest.extra && tavern.quest.extra.worldDmg) || {}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Put query-string party invitations into session to be handled later
|
|
||||||
try{
|
|
||||||
req.session.partyInvite = JSON.parse(utils.decrypt(req.query.partyInvite))
|
|
||||||
} catch(e){}
|
|
||||||
|
|
||||||
next();
|
|
||||||
}
|
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
var mongoose = require("mongoose");
|
|
||||||
var Schema = mongoose.Schema;
|
|
||||||
var shared = require('../../../common');
|
|
||||||
var _ = require('lodash');
|
|
||||||
var TaskSchemas = require('./task');
|
|
||||||
|
|
||||||
var ChallengeSchema = new Schema({
|
|
||||||
_id: {type: String, 'default': shared.uuid},
|
|
||||||
name: String,
|
|
||||||
shortName: String,
|
|
||||||
description: String,
|
|
||||||
official: {type: Boolean,'default':false},
|
|
||||||
habits: [TaskSchemas.HabitSchema],
|
|
||||||
dailys: [TaskSchemas.DailySchema],
|
|
||||||
todos: [TaskSchemas.TodoSchema],
|
|
||||||
rewards: [TaskSchemas.RewardSchema],
|
|
||||||
leader: {type: String, ref: 'User'},
|
|
||||||
group: {type: String, ref: 'Group'},
|
|
||||||
timestamp: {type: Date, 'default': Date.now},
|
|
||||||
members: [{type: String, ref: 'User'}],
|
|
||||||
memberCount: {type: Number, 'default': 0},
|
|
||||||
prize: {type: Number, 'default': 0}
|
|
||||||
});
|
|
||||||
|
|
||||||
ChallengeSchema.virtual('tasks').get(function () {
|
|
||||||
var tasks = this.habits.concat(this.dailys).concat(this.todos).concat(this.rewards);
|
|
||||||
var tasks = _.object(_.pluck(tasks,'id'), tasks);
|
|
||||||
return tasks;
|
|
||||||
});
|
|
||||||
|
|
||||||
ChallengeSchema.methods.toJSON = function(){
|
|
||||||
var doc = this.toObject();
|
|
||||||
doc._isMember = this._isMember;
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------
|
|
||||||
// Syncing logic
|
|
||||||
// --------------
|
|
||||||
|
|
||||||
function syncableAttrs(task) {
|
|
||||||
var t = (task.toObject) ? task.toObject() : task; // lodash doesn't seem to like _.omit on EmbeddedDocument
|
|
||||||
// only sync/compare important attrs
|
|
||||||
var omitAttrs = 'challenge history tags completed streak notes'.split(' ');
|
|
||||||
if (t.type != 'reward') omitAttrs.push('value');
|
|
||||||
return _.omit(t, omitAttrs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Compare whether any changes have been made to tasks. If so, we'll want to sync those changes to subscribers
|
|
||||||
*/
|
|
||||||
function comparableData(obj) {
|
|
||||||
return JSON.stringify(
|
|
||||||
_(obj.habits.concat(obj.dailys).concat(obj.todos).concat(obj.rewards))
|
|
||||||
.sortBy('id') // we don't want to update if they're sort-order is different
|
|
||||||
.transform(function(result, task){
|
|
||||||
result.push(syncableAttrs(task));
|
|
||||||
})
|
|
||||||
.value())
|
|
||||||
}
|
|
||||||
|
|
||||||
ChallengeSchema.methods.isOutdated = function(newData) {
|
|
||||||
return comparableData(this) !== comparableData(newData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Syncs all new tasks, deleted tasks, etc to the user object
|
|
||||||
* @param user
|
|
||||||
* @return nothing, user is modified directly. REMEMBER to save the user!
|
|
||||||
*/
|
|
||||||
ChallengeSchema.methods.syncToUser = function(user, cb) {
|
|
||||||
if (!user) return;
|
|
||||||
var self = this;
|
|
||||||
self.shortName = self.shortName || self.name;
|
|
||||||
|
|
||||||
// Add challenge to user.challenges
|
|
||||||
if (!_.contains(user.challenges, self._id)) {
|
|
||||||
user.challenges.push(self._id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync tags
|
|
||||||
var tags = user.tags || [];
|
|
||||||
var i = _.findIndex(tags, {id: self._id})
|
|
||||||
if (~i) {
|
|
||||||
if (tags[i].name !== self.shortName) {
|
|
||||||
// update the name - it's been changed since
|
|
||||||
user.tags[i].name = self.shortName;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
user.tags.push({
|
|
||||||
id: self._id,
|
|
||||||
name: self.shortName,
|
|
||||||
challenge: true
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync new tasks and updated tasks
|
|
||||||
_.each(self.tasks, function(task){
|
|
||||||
var list = user[task.type+'s'];
|
|
||||||
var userTask = user.tasks[task.id] || (list.push(syncableAttrs(task)), list[list.length-1]);
|
|
||||||
if (!userTask.notes) userTask.notes = task.notes; // don't override the notes, but provide it if not provided
|
|
||||||
userTask.challenge = {id:self._id};
|
|
||||||
userTask.tags = userTask.tags || {};
|
|
||||||
userTask.tags[self._id] = true;
|
|
||||||
_.merge(userTask, syncableAttrs(task));
|
|
||||||
})
|
|
||||||
|
|
||||||
// Flag deleted tasks as "broken"
|
|
||||||
_.each(user.tasks, function(task){
|
|
||||||
if (task.challenge && task.challenge.id==self._id && !self.tasks[task.id]) {
|
|
||||||
task.challenge.broken = 'TASK_DELETED';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
user.save(cb);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
module.exports.schema = ChallengeSchema;
|
|
||||||
module.exports.model = mongoose.model("Challenge", ChallengeSchema);
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
var mongoose = require("mongoose");
|
|
||||||
var shared = require('../../../common');
|
|
||||||
var _ = require('lodash');
|
|
||||||
var async = require('async');
|
|
||||||
var cc = require('coupon-code');
|
|
||||||
var autoinc = require('mongoose-id-autoinc');
|
|
||||||
|
|
||||||
var CouponSchema = new mongoose.Schema({
|
|
||||||
_id: {type: String, 'default': cc.generate},
|
|
||||||
event: {type:String, enum:['wondercon','google_6mo']},
|
|
||||||
user: {type: 'String', ref: 'User'}
|
|
||||||
});
|
|
||||||
|
|
||||||
CouponSchema.statics.generate = function(event, count, callback) {
|
|
||||||
async.times(count, function(n,cb){
|
|
||||||
mongoose.model('Coupon').create({event: event}, cb);
|
|
||||||
}, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
CouponSchema.statics.apply = function(user, code, next){
|
|
||||||
async.auto({
|
|
||||||
get_coupon: function (cb) {
|
|
||||||
mongoose.model('Coupon').findById(cc.validate(code), cb);
|
|
||||||
},
|
|
||||||
apply_coupon: ['get_coupon', function (cb, results) {
|
|
||||||
if (!results.get_coupon) return cb("Invalid coupon code");
|
|
||||||
if (results.get_coupon.user) return cb("Coupon already used");
|
|
||||||
switch (results.get_coupon.event) {
|
|
||||||
case 'wondercon':
|
|
||||||
user.items.gear.owned.eyewear_special_wondercon_red = true;
|
|
||||||
user.items.gear.owned.eyewear_special_wondercon_black = true;
|
|
||||||
user.items.gear.owned.back_special_wondercon_black = true;
|
|
||||||
user.items.gear.owned.back_special_wondercon_red = true;
|
|
||||||
user.items.gear.owned.body_special_wondercon_red = true;
|
|
||||||
user.items.gear.owned.body_special_wondercon_black = true;
|
|
||||||
user.items.gear.owned.body_special_wondercon_gold = true;
|
|
||||||
user.extra = {signupEvent: 'wondercon'};
|
|
||||||
user.save(cb);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
expire_coupon: ['apply_coupon', function (cb, results) {
|
|
||||||
results.get_coupon.user = user._id;
|
|
||||||
results.get_coupon.save(cb);
|
|
||||||
}]
|
|
||||||
}, function(err, results){
|
|
||||||
if (err) return next(err);
|
|
||||||
next(null,results.apply_coupon[0]);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
CouponSchema.plugin(autoinc.plugin, {
|
|
||||||
model: 'Coupon',
|
|
||||||
field: 'seq'
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports.schema = CouponSchema;
|
|
||||||
module.exports.model = mongoose.model("Coupon", CouponSchema);
|
|
||||||
|
|
||||||
@@ -1,370 +0,0 @@
|
|||||||
var mongoose = require("mongoose");
|
|
||||||
var Schema = mongoose.Schema;
|
|
||||||
var shared = require('../../../common');
|
|
||||||
var _ = require('lodash');
|
|
||||||
var async = require('async');
|
|
||||||
var logging = require('../logging');
|
|
||||||
|
|
||||||
var GroupSchema = new Schema({
|
|
||||||
_id: {type: String, 'default': shared.uuid},
|
|
||||||
name: String,
|
|
||||||
description: String,
|
|
||||||
leader: {type: String, ref: 'User'},
|
|
||||||
members: [{type: String, ref: 'User'}],
|
|
||||||
invites: [{type: String, ref: 'User'}],
|
|
||||||
type: {type: String, "enum": ['guild', 'party']},
|
|
||||||
privacy: {type: String, "enum": ['private', 'public'], 'default':'private'},
|
|
||||||
//_v: {type: Number,'default': 0},
|
|
||||||
chat: Array,
|
|
||||||
/*
|
|
||||||
# [{
|
|
||||||
# timestamp: Date
|
|
||||||
# user: String
|
|
||||||
# text: String
|
|
||||||
# contributor: String
|
|
||||||
# uuid: String
|
|
||||||
# id: String
|
|
||||||
# }]
|
|
||||||
*/
|
|
||||||
leaderOnly: { // restrict group actions to leader (members can't do them)
|
|
||||||
challenges: {type:Boolean, 'default':false},
|
|
||||||
//invites: {type:Boolean, 'default':false}
|
|
||||||
},
|
|
||||||
memberCount: {type: Number, 'default': 0},
|
|
||||||
challengeCount: {type: Number, 'default': 0},
|
|
||||||
balance: Number,
|
|
||||||
logo: String,
|
|
||||||
leaderMessage: String,
|
|
||||||
challenges: [{type:'String', ref:'Challenge'}], // do we need this? could depend on back-ref instead (Challenge.find({group:GID}))
|
|
||||||
quest: {
|
|
||||||
key: String,
|
|
||||||
active: {type:Boolean, 'default':false},
|
|
||||||
leader: {type:String, ref:'User'},
|
|
||||||
progress:{
|
|
||||||
hp: Number,
|
|
||||||
collect: {type:Schema.Types.Mixed, 'default':{}}, // {feather: 5, ingot: 3}
|
|
||||||
rage: Number, // limit break / "energy stored in shell", for explosion-attacks
|
|
||||||
},
|
|
||||||
|
|
||||||
//Shows boolean for each party-member who has accepted the quest. Eg {UUID: true, UUID: false}. Once all users click
|
|
||||||
//'Accept', the quest begins. If a false user waits too long, probably a good sign to prod them or boot them.
|
|
||||||
//TODO when booting user, remove from .joined and check again if we can now start the quest
|
|
||||||
members: Schema.Types.Mixed,
|
|
||||||
extra: Schema.Types.Mixed
|
|
||||||
}
|
|
||||||
}, {
|
|
||||||
strict: 'throw',
|
|
||||||
minimize: false // So empty objects are returned
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Derby duplicated stuff. This is a temporary solution, once we're completely off derby we'll run an mongo migration
|
|
||||||
* to remove duplicates, then take these fucntions out
|
|
||||||
*/
|
|
||||||
function removeDuplicates(doc){
|
|
||||||
// Remove duplicate members
|
|
||||||
if (doc.members) {
|
|
||||||
var uniqMembers = _.uniq(doc.members);
|
|
||||||
if (uniqMembers.length != doc.members.length) {
|
|
||||||
doc.members = uniqMembers;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME this isn't always triggered, since we sometimes use update() or findByIdAndUpdate()
|
|
||||||
// @see https://github.com/LearnBoost/mongoose/issues/964
|
|
||||||
GroupSchema.pre('save', function(next){
|
|
||||||
removeDuplicates(this);
|
|
||||||
this.memberCount = _.size(this.members);
|
|
||||||
this.challengeCount = _.size(this.challenges);
|
|
||||||
next();
|
|
||||||
})
|
|
||||||
|
|
||||||
GroupSchema.methods.toJSON = function(){
|
|
||||||
var doc = this.toObject();
|
|
||||||
removeDuplicates(doc);
|
|
||||||
doc._isMember = this._isMember;
|
|
||||||
|
|
||||||
//fix(groups): temp fix to remove chat entries stored as strings (not sure why that's happening..).
|
|
||||||
// Required as angular 1.3 is strict on dupes, and no message.id to `track by`
|
|
||||||
_.remove(doc.chat,function(msg){return !msg.id});
|
|
||||||
|
|
||||||
// @see pre('save') comment above
|
|
||||||
this.memberCount = _.size(this.members);
|
|
||||||
this.challengeCount = _.size(this.challenges);
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
}
|
|
||||||
|
|
||||||
var chatDefaults = module.exports.chatDefaults = function(msg,user){
|
|
||||||
var message = {
|
|
||||||
id: shared.uuid(),
|
|
||||||
text: msg,
|
|
||||||
timestamp: +new Date,
|
|
||||||
likes: {},
|
|
||||||
flags: {},
|
|
||||||
flagCount: 0
|
|
||||||
};
|
|
||||||
if (user) {
|
|
||||||
_.defaults(message, {
|
|
||||||
uuid: user._id,
|
|
||||||
contributor: user.contributor && user.contributor.toObject(),
|
|
||||||
backer: user.backer && user.backer.toObject(),
|
|
||||||
user: user.profile.name
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
message.uuid = 'system';
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
GroupSchema.methods.sendChat = function(message, user){
|
|
||||||
var group = this;
|
|
||||||
group.chat.unshift(chatDefaults(message,user));
|
|
||||||
group.chat.splice(200);
|
|
||||||
// Kick off chat notifications in the background.
|
|
||||||
var lastSeenUpdate = {$set:{}, $inc:{_v:1}};
|
|
||||||
lastSeenUpdate['$set']['newMessages.'+group._id] = {name:group.name,value:true};
|
|
||||||
if (group._id == 'habitrpg') {
|
|
||||||
// TODO For Tavern, only notify them if their name was mentioned
|
|
||||||
// var profileNames = [] // get usernames from regex of @xyz. how to handle space-delimited profile names?
|
|
||||||
// User.update({'profile.name':{$in:profileNames}},lastSeenUpdate,{multi:true}).exec();
|
|
||||||
} else {
|
|
||||||
mongoose.model('User').update({_id:{$in:group.members, $ne: user ? user._id : ''}},lastSeenUpdate,{multi:true}).exec();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var cleanQuestProgress = function(merge){
|
|
||||||
var clean = {
|
|
||||||
key: null,
|
|
||||||
progress: {
|
|
||||||
up: 0,
|
|
||||||
down: 0,
|
|
||||||
collect: {}
|
|
||||||
},
|
|
||||||
completed: null
|
|
||||||
};
|
|
||||||
merge = merge || {progress:{}};
|
|
||||||
_.merge(clean, _.omit(merge,'progress'));
|
|
||||||
_.merge(clean.progress, merge.progress);
|
|
||||||
return clean;
|
|
||||||
}
|
|
||||||
GroupSchema.statics.cleanQuestProgress = cleanQuestProgress;
|
|
||||||
|
|
||||||
// Participants: Grant rewards & achievements, finish quest
|
|
||||||
GroupSchema.methods.finishQuest = function(quest, cb) {
|
|
||||||
var group = this;
|
|
||||||
var questK = quest.key;
|
|
||||||
var updates = {$inc:{},$set:{}};
|
|
||||||
|
|
||||||
updates['$inc']['achievements.quests.' + questK] = 1;
|
|
||||||
updates['$inc']['stats.gp'] = +quest.drop.gp;
|
|
||||||
updates['$inc']['stats.exp'] = +quest.drop.exp;
|
|
||||||
updates['$inc']['_v'] = 1;
|
|
||||||
if (group._id == 'habitrpg') {
|
|
||||||
updates['$set']['party.quest.completed'] = questK; // Just show the notif
|
|
||||||
} else {
|
|
||||||
updates['$set']['party.quest'] = cleanQuestProgress({completed: questK}); // clear quest progress
|
|
||||||
}
|
|
||||||
|
|
||||||
_.each(quest.drop.items, function(item){
|
|
||||||
var dropK = item.key;
|
|
||||||
switch (item.type) {
|
|
||||||
case 'gear':
|
|
||||||
// TODO This means they can lose their new gear on death, is that what we want?
|
|
||||||
updates['$set']['items.gear.owned.'+dropK] = true;
|
|
||||||
break;
|
|
||||||
case 'eggs':
|
|
||||||
case 'food':
|
|
||||||
case 'hatchingPotions':
|
|
||||||
case 'quests':
|
|
||||||
updates['$inc']['items.'+item.type+'.'+dropK] = _.where(quest.drop.items,{type:item.type,key:item.key}).length;
|
|
||||||
break;
|
|
||||||
case 'pets':
|
|
||||||
updates['$set']['items.pets.'+dropK] = 5;
|
|
||||||
break;
|
|
||||||
case 'mounts':
|
|
||||||
updates['$set']['items.mounts.'+dropK] = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
var q = group._id === 'habitrpg' ? {} : {_id:{$in:_.keys(group.quest.members)}};
|
|
||||||
group.quest = {};group.markModified('quest');
|
|
||||||
mongoose.model('User').update(q, updates, {multi:true}, cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME this is a temporary measure, we need to remove quests from users when they traverse parties
|
|
||||||
function isOnQuest(user,progress,group){
|
|
||||||
return group && progress && user.party.quest.key && user.party.quest.key == group.quest.key;
|
|
||||||
}
|
|
||||||
|
|
||||||
GroupSchema.statics.collectQuest = function(user, progress, cb) {
|
|
||||||
this.findOne({type: 'party', members: {'$in': [user._id]}},function(err, group){
|
|
||||||
if (!isOnQuest(user,progress,group)) return cb(null);
|
|
||||||
var quest = shared.content.quests[group.quest.key];
|
|
||||||
|
|
||||||
_.each(progress.collect,function(v,k){
|
|
||||||
group.quest.progress.collect[k] += v;
|
|
||||||
});
|
|
||||||
|
|
||||||
var foundText = _.reduce(progress.collect, function(m,v,k){
|
|
||||||
m.push(v + ' ' + quest.collect[k].text('en'));
|
|
||||||
return m;
|
|
||||||
}, []);
|
|
||||||
foundText = foundText ? foundText.join(', ') : 'nothing';
|
|
||||||
group.sendChat("`" + user.profile.name + " found "+foundText+".`");
|
|
||||||
group.markModified('quest.progress.collect');
|
|
||||||
|
|
||||||
// Still needs completing
|
|
||||||
if (_.find(shared.content.quests[group.quest.key].collect, function(v,k){
|
|
||||||
return group.quest.progress.collect[k] < v.count;
|
|
||||||
})) return group.save(cb);
|
|
||||||
|
|
||||||
async.series([
|
|
||||||
function(cb2){
|
|
||||||
group.finishQuest(quest,cb2);
|
|
||||||
},
|
|
||||||
function(cb2){
|
|
||||||
group.sendChat('`All items found! Party has received their rewards.`');
|
|
||||||
group.save(cb2);
|
|
||||||
}
|
|
||||||
],cb);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// to set a boss: `db.groups.update({_id:'habitrpg'},{$set:{quest:{key:'dilatory',active:true,progress:{hp:1000,rage:1500}}}})`
|
|
||||||
module.exports.tavern = {};
|
|
||||||
var tavernQ = {_id:'habitrpg','quest.key':{$ne:null}};
|
|
||||||
process.nextTick(function(){
|
|
||||||
mongoose.model('Group').findOne(tavernQ,function(err,tavern){
|
|
||||||
module.exports.tavern = tavern;
|
|
||||||
});
|
|
||||||
})
|
|
||||||
GroupSchema.statics.tavernBoss = function(user,progress) {
|
|
||||||
if (!progress) return;
|
|
||||||
|
|
||||||
// hack: prevent crazy damage to world boss
|
|
||||||
var dmg = Math.min(900, Math.abs(progress.up||0)),
|
|
||||||
rage = -Math.min(900, Math.abs(progress.down||0));
|
|
||||||
|
|
||||||
async.waterfall([
|
|
||||||
function(cb){
|
|
||||||
mongoose.model('Group').findOne(tavernQ,cb);
|
|
||||||
},
|
|
||||||
function(tavern,cb){
|
|
||||||
if (!(tavern && tavern.quest && tavern.quest.key)) return cb(true);
|
|
||||||
module.exports.tavern = tavern;
|
|
||||||
|
|
||||||
var quest = shared.content.quests[tavern.quest.key];
|
|
||||||
if (tavern.quest.progress.hp <= 0) {
|
|
||||||
tavern.sendChat(quest.completionChat('en'));
|
|
||||||
tavern.finishQuest(quest, function(){});
|
|
||||||
tavern.save(cb);
|
|
||||||
module.exports.tavern = undefined;
|
|
||||||
} else {
|
|
||||||
// Deal damage. Note a couple things here, str & def are calculated. If str/def are defined in the database,
|
|
||||||
// use those first - which allows us to update the boss on the go if things are too easy/hard.
|
|
||||||
if (!tavern.quest.extra) tavern.quest.extra = {};
|
|
||||||
tavern.quest.progress.hp -= dmg / (tavern.quest.extra.def || quest.boss.def);
|
|
||||||
tavern.quest.progress.rage -= rage * (tavern.quest.extra.str || quest.boss.str);
|
|
||||||
if (tavern.quest.progress.rage >= quest.boss.rage.value) {
|
|
||||||
if (!tavern.quest.extra.worldDmg) tavern.quest.extra.worldDmg = {};
|
|
||||||
var wd = tavern.quest.extra.worldDmg;
|
|
||||||
// var scene = wd.tavern ? wd.stables ? wd.market ? false : 'market' : 'stables' : 'tavern'; // Dilatory attacks tavern, stables, market
|
|
||||||
var scene = wd.stables ? wd.bailey ? wd.guide ? false : 'guide' : 'bailey' : 'stables'; // Stressbeast attacks stables, Bailey, Justin
|
|
||||||
if (!scene) {
|
|
||||||
tavern.sendChat('`'+quest.boss.name('en')+' tries to unleash '+quest.boss.rage.title('en')+', but is too tired.`');
|
|
||||||
tavern.quest.progress.rage = 0 //quest.boss.rage.value;
|
|
||||||
} else {
|
|
||||||
tavern.sendChat(quest.boss.rage[scene]('en'));
|
|
||||||
tavern.quest.extra.worldDmg[scene] = true;
|
|
||||||
tavern.quest.extra.worldDmg.recent = scene;
|
|
||||||
tavern.markModified('quest.extra.worldDmg');
|
|
||||||
tavern.quest.progress.rage = 0;
|
|
||||||
tavern.quest.progress.hp += (quest.boss.rage.healing * tavern.quest.progress.hp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ((tavern.quest.progress.hp < quest.boss.desperation.threshold) && !tavern.quest.extra.desperate) {
|
|
||||||
tavern.sendChat(quest.boss.desperation.text('en'));
|
|
||||||
tavern.quest.extra.desperate = true;
|
|
||||||
tavern.quest.extra.def = quest.boss.desperation.def;
|
|
||||||
tavern.quest.extra.str = quest.boss.desperation.str;
|
|
||||||
tavern.markModified('quest.extra');
|
|
||||||
}
|
|
||||||
tavern.save(cb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],function(err,res){
|
|
||||||
if (err === true) return; // no current quest
|
|
||||||
if (err) return logging.error(err);
|
|
||||||
dmg = rage = null;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
GroupSchema.statics.bossQuest = function(user, progress, cb) {
|
|
||||||
this.findOne({type: 'party', members: {'$in': [user._id]}},function(err, group){
|
|
||||||
if (!isOnQuest(user,progress,group)) return cb(null);
|
|
||||||
var quest = shared.content.quests[group.quest.key];
|
|
||||||
if (!progress || !quest) return cb(null); // FIXME why is this ever happening, progress should be defined at this point
|
|
||||||
var down = progress.down * quest.boss.str; // multiply by boss strength
|
|
||||||
|
|
||||||
group.quest.progress.hp -= progress.up;
|
|
||||||
group.sendChat("`" + user.profile.name + " attacks " + quest.boss.name('en') + " for " + (progress.up.toFixed(1)) + " damage, " + quest.boss.name('en') + " attacks party for " + Math.abs(down).toFixed(1) + " damage.`"); //TODO Create a party preferred language option so emits like this can be localized
|
|
||||||
|
|
||||||
// If boss has Rage, increment Rage as well
|
|
||||||
if (quest.boss.rage) {
|
|
||||||
group.quest.progress.rage += Math.abs(down);
|
|
||||||
if (group.quest.progress.rage >= quest.boss.rage.value) {
|
|
||||||
group.sendChat(quest.boss.rage.effect('en'));
|
|
||||||
group.quest.progress.rage = 0;
|
|
||||||
if (quest.boss.rage.healing) group.quest.progress.hp += (group.quest.progress.hp * quest.boss.rage.healing); //TODO To make Rage effects more expandable, let's turn these into functions in quest.boss.rage
|
|
||||||
if (group.quest.progress.hp > quest.boss.hp) group.quest.progress.hp = quest.boss.hp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Everyone takes damage
|
|
||||||
var series = [
|
|
||||||
function(cb2){
|
|
||||||
mongoose.models.User.update({_id:{$in: _.keys(group.quest.members)}}, {$inc:{'stats.hp':down, _v:1}}, {multi:true}, cb2);
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
// Boss slain, finish quest
|
|
||||||
if (group.quest.progress.hp <= 0) {
|
|
||||||
group.sendChat('`You defeated ' + quest.boss.name('en') + '! Questing party members receive the rewards of victory.`');
|
|
||||||
// Participants: Grant rewards & achievements, finish quest
|
|
||||||
series.push(function(cb2){
|
|
||||||
group.finishQuest(quest,cb2);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
series.push(function(cb2){group.save(cb2)});
|
|
||||||
async.series(series,cb);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
GroupSchema.methods.toJSON = function() {
|
|
||||||
var doc = this.toObject();
|
|
||||||
if(doc.chat){
|
|
||||||
doc.chat.forEach(function(msg){
|
|
||||||
msg.flags = {};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
module.exports.schema = GroupSchema;
|
|
||||||
var Group = module.exports.model = mongoose.model("Group", GroupSchema);
|
|
||||||
|
|
||||||
// initialize tavern if !exists (fresh installs)
|
|
||||||
Group.count({_id:'habitrpg'},function(err,ct){
|
|
||||||
if (ct > 0) return;
|
|
||||||
new Group({
|
|
||||||
_id: 'habitrpg',
|
|
||||||
chat: [],
|
|
||||||
leader: '9',
|
|
||||||
name: 'HabitRPG',
|
|
||||||
type: 'guild',
|
|
||||||
privacy:'public'
|
|
||||||
}).save();
|
|
||||||
})
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
// User.js
|
|
||||||
// =======
|
|
||||||
// Defines the user data model (schema) for use via the API.
|
|
||||||
|
|
||||||
// Dependencies
|
|
||||||
// ------------
|
|
||||||
var mongoose = require("mongoose");
|
|
||||||
var Schema = mongoose.Schema;
|
|
||||||
var shared = require('../../../common');
|
|
||||||
var _ = require('lodash');
|
|
||||||
|
|
||||||
// Task Schema
|
|
||||||
// -----------
|
|
||||||
|
|
||||||
var TaskSchema = {
|
|
||||||
//_id:{type: String,'default': helpers.uuid},
|
|
||||||
id: {type: String,'default': shared.uuid},
|
|
||||||
dateCreated: {type:Date, 'default':Date.now},
|
|
||||||
text: String,
|
|
||||||
notes: {type: String, 'default': ''},
|
|
||||||
tags: {type: Schema.Types.Mixed, 'default': {}}, //{ "4ddf03d9-54bd-41a3-b011-ca1f1d2e9371" : true },
|
|
||||||
value: {type: Number, 'default': 0}, // redness
|
|
||||||
priority: {type: Number, 'default': '1'},
|
|
||||||
attribute: {type: String, 'default': "str", enum: ['str','con','int','per']},
|
|
||||||
challenge: {
|
|
||||||
id: {type: 'String', ref:'Challenge'},
|
|
||||||
broken: String, // CHALLENGE_DELETED, TASK_DELETED, UNSUBSCRIBED, CHALLENGE_CLOSED
|
|
||||||
winner: String // user.profile.name
|
|
||||||
// group: {type: 'Strign', ref: 'Group'} // if we restore this, rename `id` above to `challenge`
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var HabitSchema = new Schema(
|
|
||||||
_.defaults({
|
|
||||||
type: {type:String, 'default': 'habit'},
|
|
||||||
history: Array, // [{date:Date, value:Number}], // this causes major performance problems
|
|
||||||
up: {type: Boolean, 'default': true},
|
|
||||||
down: {type: Boolean, 'default': true}
|
|
||||||
}, TaskSchema)
|
|
||||||
, { _id: false, minimize:false }
|
|
||||||
);
|
|
||||||
|
|
||||||
var collapseChecklist = {type:Boolean, 'default':false};
|
|
||||||
var checklist = [{
|
|
||||||
completed:{type:Boolean,'default':false},
|
|
||||||
text: String,
|
|
||||||
_id:false,
|
|
||||||
id: {type:String,'default':shared.uuid}
|
|
||||||
}];
|
|
||||||
|
|
||||||
var DailySchema = new Schema(
|
|
||||||
_.defaults({
|
|
||||||
type: {type:String, 'default': 'daily'},
|
|
||||||
history: Array,
|
|
||||||
completed: {type: Boolean, 'default': false},
|
|
||||||
repeat: {
|
|
||||||
m: {type: Boolean, 'default': true},
|
|
||||||
t: {type: Boolean, 'default': true},
|
|
||||||
w: {type: Boolean, 'default': true},
|
|
||||||
th: {type: Boolean, 'default': true},
|
|
||||||
f: {type: Boolean, 'default': true},
|
|
||||||
s: {type: Boolean, 'default': true},
|
|
||||||
su: {type: Boolean, 'default': true}
|
|
||||||
},
|
|
||||||
collapseChecklist:collapseChecklist,
|
|
||||||
checklist:checklist,
|
|
||||||
streak: {type: Number, 'default': 0}
|
|
||||||
}, TaskSchema)
|
|
||||||
, { _id: false, minimize:false }
|
|
||||||
)
|
|
||||||
|
|
||||||
var TodoSchema = new Schema(
|
|
||||||
_.defaults({
|
|
||||||
type: {type:String, 'default': 'todo'},
|
|
||||||
completed: {type: Boolean, 'default': false},
|
|
||||||
dateCompleted: Date,
|
|
||||||
date: String, // due date for todos // FIXME we're getting parse errors, people have stored as "today" and "3/13". Need to run a migration & put this back to type: Date
|
|
||||||
collapseChecklist:collapseChecklist,
|
|
||||||
checklist:checklist
|
|
||||||
}, TaskSchema)
|
|
||||||
, { _id: false, minimize:false }
|
|
||||||
);
|
|
||||||
|
|
||||||
var RewardSchema = new Schema(
|
|
||||||
_.defaults({
|
|
||||||
type: {type:String, 'default': 'reward'}
|
|
||||||
}, TaskSchema)
|
|
||||||
, { _id: false, minimize:false }
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Workaround for bug when _id & id were out of sync, we can remove this after challenges has been running for a while
|
|
||||||
*/
|
|
||||||
//_.each([HabitSchema, DailySchema, TodoSchema, RewardSchema], function(schema){
|
|
||||||
// schema.post('init', function(doc){
|
|
||||||
// if (!doc.id && doc._id) doc.id = doc._id;
|
|
||||||
// })
|
|
||||||
//})
|
|
||||||
|
|
||||||
module.exports.TaskSchema = TaskSchema;
|
|
||||||
module.exports.HabitSchema = HabitSchema;
|
|
||||||
module.exports.DailySchema = DailySchema;
|
|
||||||
module.exports.TodoSchema = TodoSchema;
|
|
||||||
module.exports.RewardSchema = RewardSchema;
|
|
||||||
@@ -1,533 +0,0 @@
|
|||||||
// User.js
|
|
||||||
// =======
|
|
||||||
// Defines the user data model (schema) for use via the API.
|
|
||||||
|
|
||||||
// Dependencies
|
|
||||||
// ------------
|
|
||||||
var mongoose = require("mongoose");
|
|
||||||
var Schema = mongoose.Schema;
|
|
||||||
var shared = require('../../../common');
|
|
||||||
var _ = require('lodash');
|
|
||||||
var TaskSchemas = require('./task');
|
|
||||||
var Challenge = require('./challenge').model;
|
|
||||||
var moment = require('moment');
|
|
||||||
|
|
||||||
// User Schema
|
|
||||||
// -----------
|
|
||||||
|
|
||||||
var UserSchema = new Schema({
|
|
||||||
// ### UUID and API Token
|
|
||||||
_id: {
|
|
||||||
type: String,
|
|
||||||
'default': shared.uuid
|
|
||||||
},
|
|
||||||
apiToken: {
|
|
||||||
type: String,
|
|
||||||
'default': shared.uuid
|
|
||||||
},
|
|
||||||
|
|
||||||
// ### Mongoose Update Object
|
|
||||||
// We want to know *every* time an object updates. Mongoose uses __v to designate when an object contains arrays which
|
|
||||||
// have been updated (http://goo.gl/gQLz41), but we want *every* update
|
|
||||||
_v: { type: Number, 'default': 0 },
|
|
||||||
achievements: {
|
|
||||||
originalUser: Boolean,
|
|
||||||
helpedHabit: Boolean,
|
|
||||||
ultimateGear: Boolean,
|
|
||||||
beastMaster: Boolean,
|
|
||||||
beastMasterCount: Number,
|
|
||||||
mountMaster: Boolean,
|
|
||||||
mountMasterCount: Number,
|
|
||||||
triadBingo: Boolean,
|
|
||||||
triadBingoCount: Number,
|
|
||||||
veteran: Boolean,
|
|
||||||
snowball: Number,
|
|
||||||
spookDust: Number,
|
|
||||||
streak: Number,
|
|
||||||
challenges: Array,
|
|
||||||
quests: Schema.Types.Mixed,
|
|
||||||
rebirths: Number,
|
|
||||||
rebirthLevel: Number,
|
|
||||||
perfect: Number,
|
|
||||||
habitBirthday: Boolean, // TODO: Deprecate this. Superseded by habitBirthdays
|
|
||||||
habitBirthdays: Number,
|
|
||||||
valentine: Number,
|
|
||||||
costumeContest: Boolean,
|
|
||||||
nye: Number
|
|
||||||
},
|
|
||||||
auth: {
|
|
||||||
blocked: Boolean,
|
|
||||||
facebook: Schema.Types.Mixed,
|
|
||||||
local: {
|
|
||||||
email: String,
|
|
||||||
hashed_password: String,
|
|
||||||
salt: String,
|
|
||||||
username: String
|
|
||||||
},
|
|
||||||
timestamps: {
|
|
||||||
created: {type: Date,'default': Date.now},
|
|
||||||
loggedin: {type: Date,'default': Date.now}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
backer: {
|
|
||||||
tier: Number,
|
|
||||||
npc: String,
|
|
||||||
tokensApplied: Boolean
|
|
||||||
},
|
|
||||||
|
|
||||||
contributor: {
|
|
||||||
level: Number, // 1-9, see https://trello.com/c/wkFzONhE/277-contributor-gear https://github.com/HabitRPG/habitrpg/issues/3801
|
|
||||||
admin: Boolean,
|
|
||||||
sudo: Boolean,
|
|
||||||
text: String, // Artisan, Friend, Blacksmith, etc
|
|
||||||
contributions: String, // a markdown textarea to list their contributions + links
|
|
||||||
critical: String
|
|
||||||
},
|
|
||||||
|
|
||||||
balance: {type: Number, 'default':0},
|
|
||||||
filters: {type: Schema.Types.Mixed, 'default': {}},
|
|
||||||
|
|
||||||
purchased: {
|
|
||||||
ads: {type: Boolean, 'default': false},
|
|
||||||
skin: {type: Schema.Types.Mixed, 'default': {}}, // eg, {skeleton: true, pumpkin: true, eb052b: true}
|
|
||||||
hair: {type: Schema.Types.Mixed, 'default': {}},
|
|
||||||
shirt: {type: Schema.Types.Mixed, 'default': {}},
|
|
||||||
background: {type: Schema.Types.Mixed, 'default': {}},
|
|
||||||
txnCount: {type: Number, 'default':0},
|
|
||||||
mobileChat: Boolean,
|
|
||||||
plan: {
|
|
||||||
planId: String,
|
|
||||||
paymentMethod: String, //enum: ['Paypal','Stripe', 'Gift', '']}
|
|
||||||
customerId: String,
|
|
||||||
dateCreated: Date,
|
|
||||||
dateTerminated: Date,
|
|
||||||
dateUpdated: Date,
|
|
||||||
extraMonths: {type:Number, 'default':0},
|
|
||||||
gemsBought: {type: Number, 'default': 0},
|
|
||||||
mysteryItems: {type: Array, 'default': []},
|
|
||||||
consecutive: {
|
|
||||||
count: {type:Number, 'default':0},
|
|
||||||
offset: {type:Number, 'default':0}, // when gifted subs, offset++ for each month. offset-- each new-month (cron). count doesn't ++ until offset==0
|
|
||||||
gemCapExtra: {type:Number, 'default':0},
|
|
||||||
trinkets: {type:Number, 'default':0}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
flags: {
|
|
||||||
customizationsNotification: {type: Boolean, 'default': false},
|
|
||||||
showTour: {type: Boolean, 'default': true},
|
|
||||||
dropsEnabled: {type: Boolean, 'default': false},
|
|
||||||
itemsEnabled: {type: Boolean, 'default': false},
|
|
||||||
newStuff: {type: Boolean, 'default': false},
|
|
||||||
rewrite: {type: Boolean, 'default': true},
|
|
||||||
partyEnabled: Boolean, // FIXME do we need this?
|
|
||||||
contributor: Boolean,
|
|
||||||
classSelected: {type: Boolean, 'default': false},
|
|
||||||
mathUpdates: Boolean,
|
|
||||||
rebirthEnabled: {type: Boolean, 'default': false},
|
|
||||||
freeRebirth: {type: Boolean, 'default': false},
|
|
||||||
levelDrops: {type:Schema.Types.Mixed, 'default':{}},
|
|
||||||
chatRevoked: Boolean,
|
|
||||||
// Used to track the status of recapture emails sent to each user,
|
|
||||||
// can be 0 - no email sent - 1, 2, 3 or 4 - 4 means no more email will be sent to the user
|
|
||||||
recaptureEmailsPhase: {type: Number, 'default': 0},
|
|
||||||
communityGuidelinesAccepted: {type: Boolean, 'default': false}
|
|
||||||
},
|
|
||||||
history: {
|
|
||||||
exp: Array, // [{date: Date, value: Number}], // big peformance issues if these are defined
|
|
||||||
todos: Array //[{data: Date, value: Number}] // big peformance issues if these are defined
|
|
||||||
},
|
|
||||||
|
|
||||||
// FIXME remove?
|
|
||||||
invitations: {
|
|
||||||
guilds: {type: Array, 'default': []},
|
|
||||||
party: Schema.Types.Mixed
|
|
||||||
},
|
|
||||||
items: {
|
|
||||||
gear: {
|
|
||||||
owned: _.transform(shared.content.gear.flat, function(m,v,k){
|
|
||||||
m[v.key] = {type: Boolean};
|
|
||||||
if (v.key.match(/[weapon|armor|head|shield]_warrior_0/))
|
|
||||||
m[v.key]['default'] = true;
|
|
||||||
}),
|
|
||||||
|
|
||||||
equipped: {
|
|
||||||
weapon: {type: String, 'default': 'weapon_warrior_0'},
|
|
||||||
armor: {type: String, 'default': 'armor_base_0'},
|
|
||||||
head: {type: String, 'default': 'head_base_0'},
|
|
||||||
shield: {type: String, 'default': 'shield_base_0'},
|
|
||||||
back: String,
|
|
||||||
headAccessory: String,
|
|
||||||
eyewear: String,
|
|
||||||
body: String
|
|
||||||
},
|
|
||||||
costume: {
|
|
||||||
weapon: {type: String, 'default': 'weapon_base_0'},
|
|
||||||
armor: {type: String, 'default': 'armor_base_0'},
|
|
||||||
head: {type: String, 'default': 'head_base_0'},
|
|
||||||
shield: {type: String, 'default': 'shield_base_0'},
|
|
||||||
back: String,
|
|
||||||
headAccessory: String,
|
|
||||||
eyewear: String,
|
|
||||||
body: String
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
special:{
|
|
||||||
snowball: {type: Number, 'default': 0},
|
|
||||||
spookDust: {type: Number, 'default': 0},
|
|
||||||
valentine: Number,
|
|
||||||
valentineReceived: Array, // array of strings, by sender name
|
|
||||||
nye: Number,
|
|
||||||
nyeReceived: Array
|
|
||||||
},
|
|
||||||
|
|
||||||
// -------------- Animals -------------------
|
|
||||||
// Complex bit here. The result looks like:
|
|
||||||
// pets: {
|
|
||||||
// 'Wolf-Desert': 0, // 0 means does not own
|
|
||||||
// 'PandaCub-Red': 10, // Number represents "Growth Points"
|
|
||||||
// etc...
|
|
||||||
// }
|
|
||||||
pets:
|
|
||||||
_.defaults(
|
|
||||||
// First transform to a 1D eggs/potions mapping
|
|
||||||
_.transform(shared.content.pets, function(m,v,k){ m[k] = Number; }),
|
|
||||||
// Then add quest pets
|
|
||||||
_.transform(shared.content.questPets, function(m,v,k){ m[k] = Number; }),
|
|
||||||
// Then add additional pets (backer, contributor)
|
|
||||||
_.transform(shared.content.specialPets, function(m,v,k){ m[k] = Number; })
|
|
||||||
),
|
|
||||||
currentPet: String, // Cactus-Desert
|
|
||||||
|
|
||||||
// eggs: {
|
|
||||||
// 'PandaCub': 0, // 0 indicates "doesn't own"
|
|
||||||
// 'Wolf': 5 // Number indicates "stacking"
|
|
||||||
// }
|
|
||||||
eggs: _.transform(shared.content.eggs, function(m,v,k){ m[k] = Number; }),
|
|
||||||
|
|
||||||
// hatchingPotions: {
|
|
||||||
// 'Desert': 0, // 0 indicates "doesn't own"
|
|
||||||
// 'CottonCandyBlue': 5 // Number indicates "stacking"
|
|
||||||
// }
|
|
||||||
hatchingPotions: _.transform(shared.content.hatchingPotions, function(m,v,k){ m[k] = Number; }),
|
|
||||||
|
|
||||||
// Food: {
|
|
||||||
// 'Watermelon': 0, // 0 indicates "doesn't own"
|
|
||||||
// 'RottenMeat': 5 // Number indicates "stacking"
|
|
||||||
// }
|
|
||||||
food: _.transform(shared.content.food, function(m,v,k){ m[k] = Number; }),
|
|
||||||
|
|
||||||
// mounts: {
|
|
||||||
// 'Wolf-Desert': true,
|
|
||||||
// 'PandaCub-Red': false,
|
|
||||||
// etc...
|
|
||||||
// }
|
|
||||||
mounts: _.defaults(
|
|
||||||
// First transform to a 1D eggs/potions mapping
|
|
||||||
_.transform(shared.content.pets, function(m,v,k){ m[k] = Boolean; }),
|
|
||||||
// Then add quest pets
|
|
||||||
_.transform(shared.content.questPets, function(m,v,k){ m[k] = Boolean; }),
|
|
||||||
// Then add additional pets (backer, contributor)
|
|
||||||
_.transform(shared.content.specialMounts, function(m,v,k){ m[k] = Boolean; })
|
|
||||||
),
|
|
||||||
currentMount: String,
|
|
||||||
|
|
||||||
// Quests: {
|
|
||||||
// 'boss_0': 0, // 0 indicates "doesn't own"
|
|
||||||
// 'collection_honey': 5 // Number indicates "stacking"
|
|
||||||
// }
|
|
||||||
quests: _.transform(shared.content.quests, function(m,v,k){ m[k] = Number; }),
|
|
||||||
|
|
||||||
lastDrop: {
|
|
||||||
date: {type: Date, 'default': Date.now},
|
|
||||||
count: {type: Number, 'default': 0}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
lastCron: {type: Date, 'default': Date.now},
|
|
||||||
|
|
||||||
// {GROUP_ID: Boolean}, represents whether they have unseen chat messages
|
|
||||||
newMessages: {type: Schema.Types.Mixed, 'default': {}},
|
|
||||||
|
|
||||||
party: {
|
|
||||||
// id // FIXME can we use a populated doc instead of fetching party separate from user?
|
|
||||||
order: {type:String, 'default':'level'},
|
|
||||||
orderAscending: {type:String, 'default':'ascending'},
|
|
||||||
quest: {
|
|
||||||
key: String,
|
|
||||||
progress: {
|
|
||||||
up: {type: Number, 'default': 0},
|
|
||||||
down: {type: Number, 'default': 0},
|
|
||||||
collect: {type: Schema.Types.Mixed, 'default': {}} // {feather:1, ingot:2}
|
|
||||||
},
|
|
||||||
completed: String // When quest is done, we move it from key => completed, and it's a one-time flag (for modal) that they unset by clicking "ok" in browser
|
|
||||||
}
|
|
||||||
},
|
|
||||||
preferences: {
|
|
||||||
armorSet: String,
|
|
||||||
dayStart: {type:Number, 'default': 0, min: 0, max: 23},
|
|
||||||
size: {type:String, enum: ['broad','slim'], 'default': 'slim'},
|
|
||||||
hair: {
|
|
||||||
color: {type: String, 'default': 'red'},
|
|
||||||
base: {type: Number, 'default': 3},
|
|
||||||
bangs: {type: Number, 'default': 1},
|
|
||||||
beard: {type: Number, 'default': 0},
|
|
||||||
mustache: {type: Number, 'default': 0},
|
|
||||||
flower: {type: Number, 'default': 1}
|
|
||||||
},
|
|
||||||
hideHeader: {type:Boolean, 'default':false},
|
|
||||||
skin: {type:String, 'default':'915533'},
|
|
||||||
shirt: {type: String, 'default': 'blue'},
|
|
||||||
timezoneOffset: Number,
|
|
||||||
sound: {type:String, 'default':'off', enum: ['off','danielTheBard', 'wattsTheme']},
|
|
||||||
language: String,
|
|
||||||
automaticAllocation: Boolean,
|
|
||||||
allocationMode: {type:String, enum: ['flat','classbased','taskbased'], 'default': 'flat'},
|
|
||||||
costume: Boolean,
|
|
||||||
dateFormat: {type: String, enum:['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy/MM/dd'], 'default': 'MM/dd/yyyy'},
|
|
||||||
sleep: {type: Boolean, 'default': false},
|
|
||||||
stickyHeader: {type: Boolean, 'default': true},
|
|
||||||
disableClasses: {type: Boolean, 'default': false},
|
|
||||||
newTaskEdit: {type: Boolean, 'default': false},
|
|
||||||
dailyDueDefaultView: {type: Boolean, 'default': false},
|
|
||||||
tagsCollapsed: {type: Boolean, 'default': false},
|
|
||||||
advancedCollapsed: {type: Boolean, 'default': false},
|
|
||||||
toolbarCollapsed: {type:Boolean, 'default':false},
|
|
||||||
background: String,
|
|
||||||
webhooks: {type: Schema.Types.Mixed, 'default': {}},
|
|
||||||
// For this fields make sure to use strict comparison when searching for falsey values (=== false)
|
|
||||||
// As users who didn't login after these were introduced may have them undefined/null
|
|
||||||
emailNotifications: {
|
|
||||||
unsubscribeFromAll: {type: Boolean, 'default': false},
|
|
||||||
newPM: {type: Boolean, 'default': true},
|
|
||||||
wonChallenge: {type: Boolean, 'default': true},
|
|
||||||
giftedGems: {type: Boolean, 'default': true},
|
|
||||||
giftedSubscription: {type: Boolean, 'default': true},
|
|
||||||
invitedParty: {type: Boolean, 'default': true},
|
|
||||||
invitedGuild: {type: Boolean, 'default': true},
|
|
||||||
questStarted: {type: Boolean, 'default': true},
|
|
||||||
invitedQuest: {type: Boolean, 'default': true},
|
|
||||||
//remindersToLogin: {type: Boolean, 'default': true},
|
|
||||||
importantAnnouncements: {type: Boolean, 'default': true}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
profile: {
|
|
||||||
blurb: String,
|
|
||||||
imageUrl: String,
|
|
||||||
name: String,
|
|
||||||
},
|
|
||||||
stats: {
|
|
||||||
hp: {type: Number, 'default': 50},
|
|
||||||
mp: {type: Number, 'default': 10},
|
|
||||||
exp: {type: Number, 'default': 0},
|
|
||||||
gp: {type: Number, 'default': 0},
|
|
||||||
lvl: {type: Number, 'default': 1},
|
|
||||||
|
|
||||||
// Class System
|
|
||||||
'class': {type: String, enum: ['warrior','rogue','wizard','healer'], 'default': 'warrior'},
|
|
||||||
points: {type: Number, 'default': 0},
|
|
||||||
str: {type: Number, 'default': 0},
|
|
||||||
con: {type: Number, 'default': 0},
|
|
||||||
int: {type: Number, 'default': 0},
|
|
||||||
per: {type: Number, 'default': 0},
|
|
||||||
buffs: {
|
|
||||||
str: {type: Number, 'default': 0},
|
|
||||||
int: {type: Number, 'default': 0},
|
|
||||||
per: {type: Number, 'default': 0},
|
|
||||||
con: {type: Number, 'default': 0},
|
|
||||||
stealth: {type: Number, 'default': 0},
|
|
||||||
streaks: {type: Boolean, 'default': false},
|
|
||||||
snowball: {type: Boolean, 'default': false},
|
|
||||||
spookDust: {type: Boolean, 'default': false}
|
|
||||||
},
|
|
||||||
training: {
|
|
||||||
int: {type: Number, 'default': 0},
|
|
||||||
per: {type: Number, 'default': 0},
|
|
||||||
str: {type: Number, 'default': 0},
|
|
||||||
con: {type: Number, 'default': 0}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
tags: {type: [{
|
|
||||||
_id: false,
|
|
||||||
id: { type: String, 'default': shared.uuid },
|
|
||||||
name: String,
|
|
||||||
challenge: String
|
|
||||||
}]},
|
|
||||||
|
|
||||||
challenges: [{type: 'String', ref:'Challenge'}],
|
|
||||||
|
|
||||||
inbox: {
|
|
||||||
newMessages: {type:Number, 'default':0},
|
|
||||||
blocks: {type:Array, 'default':[]},
|
|
||||||
messages: {type:Schema.Types.Mixed, 'default':{}}, //reflist
|
|
||||||
optOut: {type:Boolean, 'default':false}
|
|
||||||
},
|
|
||||||
|
|
||||||
habits: {type:[TaskSchemas.HabitSchema]},
|
|
||||||
dailys: {type:[TaskSchemas.DailySchema]},
|
|
||||||
todos: {type:[TaskSchemas.TodoSchema]},
|
|
||||||
rewards: {type:[TaskSchemas.RewardSchema]},
|
|
||||||
|
|
||||||
extra: Schema.Types.Mixed
|
|
||||||
|
|
||||||
}, {
|
|
||||||
strict: true,
|
|
||||||
minimize: false // So empty objects are returned
|
|
||||||
});
|
|
||||||
|
|
||||||
UserSchema.methods.deleteTask = function(tid) {
|
|
||||||
this.ops.deleteTask({params:{id:tid}},function(){}); // TODO remove this whole method, since it just proxies, and change all references to this method
|
|
||||||
}
|
|
||||||
|
|
||||||
UserSchema.methods.toJSON = function() {
|
|
||||||
var doc = this.toObject();
|
|
||||||
doc.id = doc._id;
|
|
||||||
|
|
||||||
// FIXME? Is this a reference to `doc.filters` or just disabled code? Remove?
|
|
||||||
doc.filters = {};
|
|
||||||
doc._tmp = this._tmp; // be sure to send down drop notifs
|
|
||||||
|
|
||||||
return doc;
|
|
||||||
};
|
|
||||||
|
|
||||||
//UserSchema.virtual('tasks').get(function () {
|
|
||||||
// var tasks = this.habits.concat(this.dailys).concat(this.todos).concat(this.rewards);
|
|
||||||
// var tasks = _.object(_.pluck(tasks,'id'), tasks);
|
|
||||||
// return tasks;
|
|
||||||
//});
|
|
||||||
|
|
||||||
UserSchema.post('init', function(doc){
|
|
||||||
shared.wrap(doc);
|
|
||||||
})
|
|
||||||
|
|
||||||
UserSchema.pre('save', function(next) {
|
|
||||||
|
|
||||||
// Populate new users with default content
|
|
||||||
if (this.isNew){
|
|
||||||
//TODO for some reason this doesn't work here: `_.merge(this, shared.content.userDefaults);`
|
|
||||||
var self = this;
|
|
||||||
_.each(['habits', 'dailys', 'todos', 'rewards', 'tags'], function(taskType){
|
|
||||||
self[taskType] = _.map(shared.content.userDefaults[taskType], function(task){
|
|
||||||
var newTask = _.cloneDeep(task);
|
|
||||||
|
|
||||||
// Render task's text and notes in user's language
|
|
||||||
if(taskType === 'tags'){
|
|
||||||
// tasks automatically get id=helpers.uuid() from TaskSchema id.default, but tags are Schema.Types.Mixed - so we need to manually invoke here
|
|
||||||
newTask.id = shared.uuid();
|
|
||||||
newTask.name = newTask.name(self.preferences.language);
|
|
||||||
}else{
|
|
||||||
newTask.text = newTask.text(self.preferences.language);
|
|
||||||
newTask.notes = newTask.notes(self.preferences.language);
|
|
||||||
|
|
||||||
if(newTask.checklist){
|
|
||||||
newTask.checklist = _.map(newTask.checklist, function(checklistItem){
|
|
||||||
checklistItem.text = checklistItem.text(self.preferences.language);
|
|
||||||
return checklistItem;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return newTask;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
//this.markModified('tasks');
|
|
||||||
if (_.isNaN(this.preferences.dayStart) || this.preferences.dayStart < 0 || this.preferences.dayStart > 23) {
|
|
||||||
this.preferences.dayStart = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.profile.name) {
|
|
||||||
var fb = this.auth.facebook;
|
|
||||||
this.profile.name =
|
|
||||||
(this.auth.local && this.auth.local.username) ||
|
|
||||||
(fb && (fb.displayName || fb.name || fb.username || (fb.first_name && fb.first_name + ' ' + fb.last_name))) ||
|
|
||||||
'Anonymous';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determines if Beast Master should be awarded
|
|
||||||
var petCount = shared.countPets(_.reduce(this.items.pets,function(m,v){
|
|
||||||
//HOTFIX - Remove when solution is found, the first argument passed to reduce is a function
|
|
||||||
if(_.isFunction(v)) return m;
|
|
||||||
return m+(v?1:0)},0), this.items.pets);
|
|
||||||
|
|
||||||
if (petCount >= 90 || this.achievements.beastMasterCount > 0) {
|
|
||||||
this.achievements.beastMaster = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determines if Mount Master should be awarded
|
|
||||||
var mountCount = shared.countMounts(_.reduce(this.items.mounts,function(m,v){
|
|
||||||
//HOTFIX - Remove when solution is found, the first argument passed to reduce is a function
|
|
||||||
if(_.isFunction(v)) return m;
|
|
||||||
return m+(v?1:0)},0), this.items.mounts);
|
|
||||||
|
|
||||||
if (mountCount >= 90 || this.achievements.mountMasterCount > 0) {
|
|
||||||
this.achievements.mountMaster = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determines if Triad Bingo should be awarded
|
|
||||||
|
|
||||||
var triadCount = shared.countTriad(this.items.pets);
|
|
||||||
|
|
||||||
if ((mountCount >= 90 && triadCount >= 90) || this.achievements.triadBingoCount > 0) {
|
|
||||||
this.achievements.triadBingo = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// EXAMPLE CODE for allowing all existing and new players to be
|
|
||||||
// automatically granted an item during a certain time period:
|
|
||||||
// if (!this.items.pets['JackOLantern-Base'] && moment().isBefore('2014-11-01'))
|
|
||||||
// this.items.pets['JackOLantern-Base'] = 5;
|
|
||||||
|
|
||||||
//our own version incrementer
|
|
||||||
if (_.isNaN(this._v) || !_.isNumber(this._v)) this._v = 0;
|
|
||||||
this._v++;
|
|
||||||
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
|
|
||||||
UserSchema.methods.unlink = function(options, cb) {
|
|
||||||
var cid = options.cid, keep = options.keep, tid = options.tid;
|
|
||||||
var self = this;
|
|
||||||
switch (keep) {
|
|
||||||
case 'keep':
|
|
||||||
self.tasks[tid].challenge = {};
|
|
||||||
break;
|
|
||||||
case 'remove':
|
|
||||||
self.deleteTask(tid);
|
|
||||||
break;
|
|
||||||
case 'keep-all':
|
|
||||||
_.each(self.tasks, function(t){
|
|
||||||
if (t.challenge && t.challenge.id == cid) {
|
|
||||||
t.challenge = {};
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'remove-all':
|
|
||||||
_.each(self.tasks, function(t){
|
|
||||||
if (t.challenge && t.challenge.id == cid) {
|
|
||||||
self.deleteTask(t.id);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
self.markModified('habits');
|
|
||||||
self.markModified('dailys');
|
|
||||||
self.markModified('todos');
|
|
||||||
self.markModified('rewards');
|
|
||||||
self.save(cb);
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.schema = UserSchema;
|
|
||||||
module.exports.model = mongoose.model("User", UserSchema);
|
|
||||||
|
|
||||||
mongoose.model("User")
|
|
||||||
.find({'contributor.admin':true})
|
|
||||||
.sort('-contributor.level -backer.npc profile.name')
|
|
||||||
.select('profile contributor backer')
|
|
||||||
.exec(function(err,mods){
|
|
||||||
module.exports.mods = mods
|
|
||||||
});
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
var express = require('express');
|
|
||||||
var router = new express.Router();
|
|
||||||
var _ = require('lodash');
|
|
||||||
var async = require('async');
|
|
||||||
var icalendar = require('icalendar');
|
|
||||||
var api = require('./../controllers/user');
|
|
||||||
var auth = require('./../controllers/auth');
|
|
||||||
var middleware = require('../middleware');
|
|
||||||
var logging = require('./../logging');
|
|
||||||
var i18n = require('./../i18n');
|
|
||||||
|
|
||||||
/* ---------- Deprecated API ------------*/
|
|
||||||
|
|
||||||
var initDeprecated = function(req, res, next) {
|
|
||||||
req.headers['x-api-user'] = req.params.uid;
|
|
||||||
req.headers['x-api-key'] = req.body.apiToken;
|
|
||||||
return next();
|
|
||||||
};
|
|
||||||
|
|
||||||
router.post('/v1/users/:uid/tasks/:taskId/:direction', initDeprecated, auth.auth, i18n.getUserLanguage, api.score);
|
|
||||||
|
|
||||||
// FIXME add this back in
|
|
||||||
router.get('/v1/users/:uid/calendar.ics', i18n.getUserLanguage, function(req, res, next) {
|
|
||||||
return next() //disable for now
|
|
||||||
|
|
||||||
var apiToken, model, query, uid;
|
|
||||||
uid = req.params.uid;
|
|
||||||
apiToken = req.query.apiToken;
|
|
||||||
model = req.getModel();
|
|
||||||
query = model.query('users').withIdAndToken(uid, apiToken);
|
|
||||||
return query.fetch(function(err, result) {
|
|
||||||
var formattedIcal, ical, tasks, tasksWithDates;
|
|
||||||
if (err) {
|
|
||||||
return res.send(500, err);
|
|
||||||
}
|
|
||||||
tasks = result.get('tasks');
|
|
||||||
/* tasks = result[0].tasks*/
|
|
||||||
|
|
||||||
tasksWithDates = _.filter(tasks, function(task) {
|
|
||||||
return !!task.date;
|
|
||||||
});
|
|
||||||
if (_.isEmpty(tasksWithDates)) {
|
|
||||||
return res.send(500, "No events found");
|
|
||||||
}
|
|
||||||
ical = new icalendar.iCalendar();
|
|
||||||
ical.addProperty('NAME', 'HabitRPG');
|
|
||||||
_.each(tasksWithDates, function(task) {
|
|
||||||
var d, event;
|
|
||||||
event = new icalendar.VEvent(task.id);
|
|
||||||
event.setSummary(task.text);
|
|
||||||
d = new Date(task.date);
|
|
||||||
d.date_only = true;
|
|
||||||
event.setDate(d);
|
|
||||||
ical.addComponent(event);
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
res.type('text/calendar');
|
|
||||||
formattedIcal = ical.toString().replace(/DTSTART\:/g, 'DTSTART;VALUE=DATE:');
|
|
||||||
return res.send(200, formattedIcal);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
Batch Update
|
|
||||||
This is super-deprecated, and will be removed once apiv2 is running against mobile for a while
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
var batchUpdate = function(req, res, next) {
|
|
||||||
var user = res.locals.user;
|
|
||||||
var oldSend = res.send;
|
|
||||||
var oldJson = res.json;
|
|
||||||
var performAction = function(action, cb) {
|
|
||||||
|
|
||||||
// req.body=action.data; delete action.data; _.defaults(req.params, action)
|
|
||||||
// Would require changing action.dir on mobile app
|
|
||||||
req.params.id = action.data && action.data.id;
|
|
||||||
req.params.direction = action.dir;
|
|
||||||
req.params.type = action.type;
|
|
||||||
req.body = action.data;
|
|
||||||
res.send = res.json = function(code, data) {
|
|
||||||
if (_.isNumber(code) && code >= 400) {
|
|
||||||
logging.error({
|
|
||||||
code: code,
|
|
||||||
data: data
|
|
||||||
});
|
|
||||||
}
|
|
||||||
//FIXME send error messages down
|
|
||||||
return cb();
|
|
||||||
};
|
|
||||||
switch (action.op) {
|
|
||||||
case "score":
|
|
||||||
api.score(req, res);
|
|
||||||
break;
|
|
||||||
case "addTask":
|
|
||||||
api.addTask(req, res);
|
|
||||||
break;
|
|
||||||
case "delTask":
|
|
||||||
api.deleteTask(req, res);
|
|
||||||
break;
|
|
||||||
case "revive":
|
|
||||||
api.revive(req, res);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
cb();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Setup the array of functions we're going to call in parallel with async
|
|
||||||
var actions = _.transform(req.body || [], function(result, action) {
|
|
||||||
if (!_.isEmpty(action)) {
|
|
||||||
result.push(function(cb) {
|
|
||||||
performAction(action, cb);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// call all the operations, then return the user object to the requester
|
|
||||||
async.series(actions, 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});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
API v1 Routes
|
|
||||||
------------------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
var cron = api.cron;
|
|
||||||
|
|
||||||
router.get('/status', i18n.getUserLanguage, function(req, res) {
|
|
||||||
return res.json({
|
|
||||||
status: 'up'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Scoring
|
|
||||||
router.post('/user/task/:id/:direction', auth.auth, i18n.getUserLanguage, cron, api.score);
|
|
||||||
router.post('/user/tasks/:id/:direction', auth.auth, i18n.getUserLanguage, cron, api.score);
|
|
||||||
|
|
||||||
// Tasks
|
|
||||||
router.get('/user/tasks', auth.auth, i18n.getUserLanguage, cron, api.getTasks);
|
|
||||||
router.get('/user/task/:id', auth.auth, i18n.getUserLanguage, cron, api.getTask);
|
|
||||||
router["delete"]('/user/task/:id', auth.auth, i18n.getUserLanguage, cron, api.deleteTask);
|
|
||||||
router.post('/user/task', auth.auth, i18n.getUserLanguage, cron, api.addTask);
|
|
||||||
|
|
||||||
// User
|
|
||||||
router.get('/user', auth.auth, i18n.getUserLanguage, cron, api.getUser);
|
|
||||||
router.post('/user/revive', auth.auth, i18n.getUserLanguage, cron, api.revive);
|
|
||||||
router.post('/user/batch-update', middleware.forceRefresh, auth.auth, i18n.getUserLanguage, cron, batchUpdate);
|
|
||||||
|
|
||||||
function deprecated(req, res) {
|
|
||||||
res.json(404, {err:'API v1 is no longer supported, please use API v2 instead (https://github.com/HabitRPG/habitrpg/blob/develop/API.md)'});
|
|
||||||
}
|
|
||||||
router.get('*', i18n.getUserLanguage, deprecated);
|
|
||||||
router.post('*', i18n.getUserLanguage, deprecated);
|
|
||||||
router.put('*', i18n.getUserLanguage, deprecated);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,799 +0,0 @@
|
|||||||
###
|
|
||||||
---------- /api/v2 API ------------
|
|
||||||
see https://github.com/wordnik/swagger-node-express
|
|
||||||
Every url added to router is prefaced by /api/v2
|
|
||||||
Note: Many user-route ops exist in ../../common/script/index.coffee#user.ops, so that they can (1) be called both
|
|
||||||
client and server.
|
|
||||||
v1 user. Requires x-api-user (user id) and x-api-key (api key) headers, Test with:
|
|
||||||
$ mocha test/user.mocha.coffee
|
|
||||||
###
|
|
||||||
|
|
||||||
user = require("../controllers/user")
|
|
||||||
groups = require("../controllers/groups")
|
|
||||||
members = require("../controllers/members")
|
|
||||||
auth = require("../controllers/auth")
|
|
||||||
hall = require("../controllers/hall")
|
|
||||||
challenges = require("../controllers/challenges")
|
|
||||||
dataexport = require("../controllers/dataexport")
|
|
||||||
nconf = require("nconf")
|
|
||||||
middleware = require("../middleware")
|
|
||||||
cron = user.cron
|
|
||||||
_ = require('lodash')
|
|
||||||
content = require('../../../common').content
|
|
||||||
i18n = require('../i18n')
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = (swagger, v2) ->
|
|
||||||
[path,body,query] = [swagger.pathParam, swagger.bodyParam, swagger.queryParam]
|
|
||||||
|
|
||||||
swagger.setAppHandler(v2)
|
|
||||||
swagger.setErrorHandler("next")
|
|
||||||
swagger.setHeaders = -> #disable setHeaders, since we have our own thing going on in middleware.js (and which requires `req`, which swagger doesn't pass in)
|
|
||||||
swagger.configureSwaggerPaths("", "/api-docs", "")
|
|
||||||
|
|
||||||
api =
|
|
||||||
|
|
||||||
'/status':
|
|
||||||
spec:
|
|
||||||
description: "Returns the status of the server (up or down)"
|
|
||||||
action: (req, res) ->
|
|
||||||
res.json status: "up"
|
|
||||||
|
|
||||||
'/content':
|
|
||||||
spec:
|
|
||||||
description: "Get all available content objects. This is essential, since Habit often depends on item keys (eg, when purchasing a weapon)."
|
|
||||||
parameters: [
|
|
||||||
query("language","Optional language to use for content's strings. Default is english.","string")
|
|
||||||
]
|
|
||||||
action: user.getContent
|
|
||||||
|
|
||||||
'/content/paths':
|
|
||||||
spec:
|
|
||||||
description: "Show user model tree"
|
|
||||||
action: user.getModelPaths
|
|
||||||
|
|
||||||
"/export/history":
|
|
||||||
spec:
|
|
||||||
description: "Export user history"
|
|
||||||
method: 'GET'
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage]
|
|
||||||
action: dataexport.history #[todo] encode data output options in the data controller and use these to build routes
|
|
||||||
|
|
||||||
# ---------------------------------
|
|
||||||
# User
|
|
||||||
# ---------------------------------
|
|
||||||
|
|
||||||
# Scoring
|
|
||||||
|
|
||||||
"/user/tasks/{id}/{direction}":
|
|
||||||
spec:
|
|
||||||
#notes: "Simple scoring of a task."
|
|
||||||
description: "Simple scoring of a task. This is most-likely the only API route you'll be using as a 3rd-party developer. The most common operation is for the user to gain or lose points based on some action (browsing Reddit, running a mile, 1 Pomodor, etc). Call this route, if the task you're trying to score doesn't exist, it will be created for you. When random events occur, the <b>user._tmp</b> variable will be filled. Critical hits can be accessed through <b>user._tmp.crit</b>. The Streakbonus can be accessed through <b>user._tmp.streakBonus</b>. Both will contain the multiplier value. When random drops occur, the following values are available: <b>user._tmp.drop = {text,type,dialog,value,key,notes}</b>"
|
|
||||||
parameters: [
|
|
||||||
path("id", "ID of the task to score. If this task doesn't exist, a task will be created automatically", "string")
|
|
||||||
path("direction", "Either 'up' or 'down'", "string")
|
|
||||||
body '',"If you're creating a 3rd-party task, pass up any task attributes in the body (see TaskSchema).",'object'
|
|
||||||
]
|
|
||||||
method: 'POST'
|
|
||||||
action: user.score
|
|
||||||
|
|
||||||
# Tasks
|
|
||||||
"/user/tasks:GET":
|
|
||||||
spec:
|
|
||||||
path: '/user/tasks'
|
|
||||||
description: "Get all user's tasks"
|
|
||||||
action: user.getTasks
|
|
||||||
|
|
||||||
"/user/tasks:POST":
|
|
||||||
spec:
|
|
||||||
path: '/user/tasks'
|
|
||||||
description: "Create a task"
|
|
||||||
method: 'POST'
|
|
||||||
parameters: [ body "","Send up the whole task (see TaskSchema)","object" ]
|
|
||||||
action: user.addTask
|
|
||||||
|
|
||||||
"/user/tasks/{id}:GET":
|
|
||||||
spec:
|
|
||||||
path: '/user/tasks/{id}'
|
|
||||||
description: "Get an individual task"
|
|
||||||
parameters: [
|
|
||||||
path("id", "Task ID", "string")
|
|
||||||
]
|
|
||||||
action: user.getTask
|
|
||||||
|
|
||||||
"/user/tasks/{id}:PUT":
|
|
||||||
spec:
|
|
||||||
path: '/user/tasks/{id}'
|
|
||||||
description: "Update a user's task"
|
|
||||||
method: 'PUT'
|
|
||||||
parameters: [
|
|
||||||
path "id", "Task ID", "string"
|
|
||||||
body "","Send up the whole task (see TaskSchema)","object"
|
|
||||||
]
|
|
||||||
action: user.updateTask
|
|
||||||
|
|
||||||
"/user/tasks/{id}:DELETE":
|
|
||||||
spec:
|
|
||||||
path: '/user/tasks/{id}'
|
|
||||||
description: "Delete a task"
|
|
||||||
method: 'DELETE'
|
|
||||||
parameters: [ path("id", "Task ID", "string") ]
|
|
||||||
action: user.deleteTask
|
|
||||||
|
|
||||||
|
|
||||||
"/user/tasks/{id}/sort":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: 'Sort tasks'
|
|
||||||
parameters: [
|
|
||||||
path("id", "Task ID", "string")
|
|
||||||
query("from","Index where you're sorting from (0-based)","integer")
|
|
||||||
query("to","Index where you're sorting to (0-based)","integer")
|
|
||||||
]
|
|
||||||
action: user.sortTask
|
|
||||||
|
|
||||||
|
|
||||||
"/user/tasks/clear-completed":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Clears competed To-Dos (needed periodically for performance)."
|
|
||||||
action: user.clearCompleted
|
|
||||||
|
|
||||||
|
|
||||||
"/user/tasks/{id}/unlink":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: 'Unlink a task from its challenge'
|
|
||||||
parameters: [
|
|
||||||
path("id", "Task ID", "string")
|
|
||||||
query 'keep',"When unlinking a challenge task, how to handle the orphans?",'string',['keep','keep-all','remove','remove-all']
|
|
||||||
]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage] ## removing cron since they may want to remove task first
|
|
||||||
action: challenges.unlink
|
|
||||||
|
|
||||||
|
|
||||||
# Inventory
|
|
||||||
"/user/inventory/buy":
|
|
||||||
spec:
|
|
||||||
description: "Get a list of buyable gear"
|
|
||||||
action: user.getBuyList
|
|
||||||
|
|
||||||
"/user/inventory/buy/{key}":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Buy a gear piece and equip it automatically"
|
|
||||||
parameters:[
|
|
||||||
path 'key',"The key of the item to buy (call /content route for available keys)",'string', _.keys(content.gear.flat)
|
|
||||||
]
|
|
||||||
action: user.buy
|
|
||||||
|
|
||||||
"/user/inventory/sell/{type}/{key}":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Sell inventory items back to Alexander"
|
|
||||||
parameters: [
|
|
||||||
#TODO verify these are the correct types
|
|
||||||
path('type',"The type of object you're selling back.",'string',['eggs','hatchingPotions','food'])
|
|
||||||
path('key',"The object key you're selling back (call /content route for available keys)",'string')
|
|
||||||
]
|
|
||||||
action: user.sell
|
|
||||||
|
|
||||||
"/user/inventory/purchase/{type}/{key}":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Purchase a gem-purchaseable item from Alexander"
|
|
||||||
parameters:[
|
|
||||||
path('type',"The type of object you're purchasing.",'string',['eggs','hatchingPotions','food','quests','special'])
|
|
||||||
path('key',"The object key you're purchasing (call /content route for available keys)",'string')
|
|
||||||
]
|
|
||||||
action: user.purchase
|
|
||||||
|
|
||||||
|
|
||||||
"/user/inventory/feed/{pet}/{food}":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Feed your pet some food"
|
|
||||||
parameters: [
|
|
||||||
path 'pet',"The key of the pet you're feeding",'string',_.keys(content.pets)
|
|
||||||
path 'food',"The key of the food to feed your pet",'string',_.keys(content.food)
|
|
||||||
]
|
|
||||||
action: user.feed
|
|
||||||
|
|
||||||
"/user/inventory/equip/{type}/{key}":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Equip an item (either pet, mount, equipped or costume)"
|
|
||||||
parameters: [
|
|
||||||
path 'type',"Type to equip",'string',['pet','mount','equipped', 'costume']
|
|
||||||
path 'key',"The object key you're equipping (call /content route for available keys)",'string'
|
|
||||||
]
|
|
||||||
action: user.equip
|
|
||||||
|
|
||||||
"/user/inventory/hatch/{egg}/{hatchingPotion}":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Pour a hatching potion on an egg"
|
|
||||||
parameters: [
|
|
||||||
path 'egg',"The egg key to hatch",'string',_.keys(content.eggs)
|
|
||||||
path 'hatchingPotion',"The hatching potion to pour",'string',_.keys(content.hatchingPotions)
|
|
||||||
]
|
|
||||||
action: user.hatch
|
|
||||||
|
|
||||||
|
|
||||||
# User
|
|
||||||
"/user:GET":
|
|
||||||
spec:
|
|
||||||
path: '/user'
|
|
||||||
description: "Get the full user object"
|
|
||||||
action: user.getUser
|
|
||||||
|
|
||||||
"/user:PUT":
|
|
||||||
spec:
|
|
||||||
path: '/user'
|
|
||||||
method: 'PUT'
|
|
||||||
description: "Update the user object (only certain attributes are supported)"
|
|
||||||
parameters: [
|
|
||||||
body '','The user object (see UserSchema)','object'
|
|
||||||
]
|
|
||||||
action: user.update
|
|
||||||
|
|
||||||
"/user:DELETE":
|
|
||||||
spec:
|
|
||||||
path: '/user'
|
|
||||||
method: 'DELETE'
|
|
||||||
description: "Delete a user object entirely, USE WITH CAUTION!"
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage]
|
|
||||||
action: user["delete"]
|
|
||||||
|
|
||||||
"/user/revive":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Revive your dead user"
|
|
||||||
action: user.revive
|
|
||||||
|
|
||||||
"/user/reroll":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: 'Drink the Fortify Potion (Note, it used to be called re-roll)'
|
|
||||||
action: user.reroll
|
|
||||||
|
|
||||||
"/user/reset":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Completely reset your account"
|
|
||||||
action: user.reset
|
|
||||||
|
|
||||||
"/user/sleep":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Toggle whether you're resting in the inn"
|
|
||||||
action: user.sleep
|
|
||||||
|
|
||||||
"/user/rebirth":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Rebirth your avatar"
|
|
||||||
action: user.rebirth
|
|
||||||
|
|
||||||
"/user/class/change":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Either remove your avatar's class, or change it to something new"
|
|
||||||
parameters: [
|
|
||||||
query 'class',"The key of the class to change to. If not provided, user's class is removed.",'string',['warrior','healer','rogue','wizard','']
|
|
||||||
]
|
|
||||||
action: user.changeClass
|
|
||||||
|
|
||||||
"/user/class/allocate":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Allocate one point towards an attribute"
|
|
||||||
parameters: [
|
|
||||||
query 'stat','The stat to allocate towards','string',['str','per','int','con']
|
|
||||||
]
|
|
||||||
action:user.allocate
|
|
||||||
|
|
||||||
"/user/class/cast/{spell}":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Casts a spell on a target."
|
|
||||||
parameters: [
|
|
||||||
path 'spell',"The key of the spell to cast (see ../../common#content.coffee)",'string'
|
|
||||||
query 'targetType',"The type of object you're targeting",'string',['party','self','user','task']
|
|
||||||
query 'targetId',"The ID of the object you're targeting",'string'
|
|
||||||
|
|
||||||
]
|
|
||||||
action: user.cast
|
|
||||||
|
|
||||||
"/user/unlock":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Unlock a certain gem-purchaseable path (or multiple paths)"
|
|
||||||
parameters: [
|
|
||||||
query 'path',"The path to unlock, such as hair.green or shirts.red,shirts.blue",'string'
|
|
||||||
]
|
|
||||||
action: user.unlock
|
|
||||||
|
|
||||||
"/user/batch-update":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "This is an advanced route which is useful for apps which might for example need offline support. You can send a whole batch of user-based operations, which allows you to queue them up offline and send them all at once. The format is {op:'nameOfOperation',parameters:{},body:{},query:{}}"
|
|
||||||
parameters:[
|
|
||||||
body '','The array of batch-operations to perform','object'
|
|
||||||
]
|
|
||||||
middleware: [middleware.forceRefresh, auth.auth, i18n.getUserLanguage, cron, user.sessionPartyInvite]
|
|
||||||
action: user.batchUpdate
|
|
||||||
|
|
||||||
# Tags
|
|
||||||
"/user/tags":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: 'Create a new tag'
|
|
||||||
parameters: [
|
|
||||||
body '','New tag (see UserSchema.tags)','object'
|
|
||||||
]
|
|
||||||
action: user.addTag
|
|
||||||
|
|
||||||
"/user/tags/sort":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: 'Sort tags'
|
|
||||||
parameters: [
|
|
||||||
query("from","Index where you're sorting from (0-based)","integer")
|
|
||||||
query("to","Index where you're sorting to (0-based)","integer")
|
|
||||||
]
|
|
||||||
action: user.sortTag
|
|
||||||
|
|
||||||
"/user/tags/{id}:PUT":
|
|
||||||
spec:
|
|
||||||
path: '/user/tags/{id}'
|
|
||||||
method: 'PUT'
|
|
||||||
description: "Edit a tag"
|
|
||||||
parameters: [
|
|
||||||
path 'id','The id of the tag to edit','string'
|
|
||||||
body '','Tag edits (see UserSchema.tags)','object'
|
|
||||||
]
|
|
||||||
action: user.updateTag
|
|
||||||
|
|
||||||
"/user/tags/{id}:DELETE":
|
|
||||||
spec:
|
|
||||||
path: '/user/tags/{id}'
|
|
||||||
method: 'DELETE'
|
|
||||||
description: 'Delete a tag'
|
|
||||||
parameters: [
|
|
||||||
path 'id','Id of tag to delete','string'
|
|
||||||
]
|
|
||||||
action: user.deleteTag
|
|
||||||
|
|
||||||
"/user/social/invite-friends":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: 'Invite friends via email'
|
|
||||||
parameters: [
|
|
||||||
body 'invites','Array of [{name:"Friend\'s Name", email:"friends@email.com"}] to invite to play in your party','object'
|
|
||||||
]
|
|
||||||
action: user.inviteFriends
|
|
||||||
|
|
||||||
# Webhooks
|
|
||||||
"/user/webhooks":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: 'Create a new webhook'
|
|
||||||
parameters: [
|
|
||||||
body '','New Webhook {url:"webhook endpoint (required)", id:"id of webhook (shared.uuid(), optional)", enabled:"whether webhook is enabled (true by default, optional)"}','object'
|
|
||||||
]
|
|
||||||
action: user.addWebhook
|
|
||||||
|
|
||||||
"/user/webhooks/{id}:PUT":
|
|
||||||
spec:
|
|
||||||
path: '/user/webhooks/{id}'
|
|
||||||
method: 'PUT'
|
|
||||||
description: "Edit a webhook"
|
|
||||||
parameters: [
|
|
||||||
path 'id','The id of the webhook to edit','string'
|
|
||||||
body '','New Webhook {url:"webhook endpoint (required)", id:"id of webhook (shared.uuid(), optional)", enabled:"whether webhook is enabled (true by default, optional)"}','object'
|
|
||||||
]
|
|
||||||
action: user.updateWebhook
|
|
||||||
|
|
||||||
"/user/webhooks/{id}:DELETE":
|
|
||||||
spec:
|
|
||||||
path: '/user/webhooks/{id}'
|
|
||||||
method: 'DELETE'
|
|
||||||
description: 'Delete a webhook'
|
|
||||||
parameters: [
|
|
||||||
path 'id','Id of webhook to delete','string'
|
|
||||||
]
|
|
||||||
action: user.deleteWebhook
|
|
||||||
|
|
||||||
# ---------------------------------
|
|
||||||
# Groups
|
|
||||||
# ---------------------------------
|
|
||||||
"/groups:GET":
|
|
||||||
spec:
|
|
||||||
path: '/groups'
|
|
||||||
description: "Get a list of groups"
|
|
||||||
parameters: [
|
|
||||||
query 'type',"Comma-separated types of groups to return, eg 'party,guilds,public,tavern'",'string'
|
|
||||||
]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage]
|
|
||||||
action: groups.list
|
|
||||||
|
|
||||||
|
|
||||||
"/groups:POST":
|
|
||||||
spec:
|
|
||||||
path: '/groups'
|
|
||||||
method: 'POST'
|
|
||||||
description: 'Create a group'
|
|
||||||
parameters: [
|
|
||||||
body '','Group object (see GroupSchema)','object'
|
|
||||||
]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage]
|
|
||||||
action: groups.create
|
|
||||||
|
|
||||||
"/groups/{gid}:GET":
|
|
||||||
spec:
|
|
||||||
path: '/groups/{gid}'
|
|
||||||
description: "Get a group. The party the user currently is in can be accessed with the gid 'party'."
|
|
||||||
parameters: [path('gid','Group ID','string')]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage]
|
|
||||||
action: groups.get
|
|
||||||
|
|
||||||
"/groups/{gid}:POST":
|
|
||||||
spec:
|
|
||||||
path: '/groups/{gid}'
|
|
||||||
method: 'POST'
|
|
||||||
description: "Edit a group"
|
|
||||||
parameters: [body('','Group object (see GroupSchema)','object')]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
|
|
||||||
action: groups.update
|
|
||||||
|
|
||||||
"/groups/{gid}/join":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: 'Join a group'
|
|
||||||
parameters: [path('gid','Id of the group to join','string')]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
|
|
||||||
action: groups.join
|
|
||||||
|
|
||||||
"/groups/{gid}/leave":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: 'Leave a group'
|
|
||||||
parameters: [path('gid','ID of the group to leave','string')]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
|
|
||||||
action: groups.leave
|
|
||||||
|
|
||||||
"/groups/{gid}/invite":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Invite a user to a group"
|
|
||||||
parameters: [
|
|
||||||
path 'gid','Group id','string'
|
|
||||||
query 'uuid','User id to invite','string'
|
|
||||||
]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
|
|
||||||
action:groups.invite
|
|
||||||
|
|
||||||
"/groups/{gid}/removeMember":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Remove / boot a member from a group"
|
|
||||||
parameters: [
|
|
||||||
path 'gid','Group id','string'
|
|
||||||
query 'uuid','User id to boot','string'
|
|
||||||
]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
|
|
||||||
action:groups.removeMember
|
|
||||||
|
|
||||||
"/groups/{gid}/questAccept":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Accept a quest invitation"
|
|
||||||
parameters: [
|
|
||||||
path 'gid',"Group id",'string'
|
|
||||||
query 'key',"optional. if provided, trigger new invite, if not, accept existing invite",'string'
|
|
||||||
]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
|
|
||||||
action:groups.questAccept
|
|
||||||
|
|
||||||
"/groups/{gid}/questReject":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: 'Reject quest invitation'
|
|
||||||
parameters: [
|
|
||||||
path 'gid','Group id','string'
|
|
||||||
]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
|
|
||||||
action: groups.questReject
|
|
||||||
|
|
||||||
"/groups/{gid}/questCancel":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: 'Cancel quest before it starts (in invitation stage)'
|
|
||||||
parameters: [path('gid','Group to cancel quest in','string')]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
|
|
||||||
action: groups.questCancel
|
|
||||||
|
|
||||||
"/groups/{gid}/questAbort":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: 'Abort quest after it has started (all progress will be lost)'
|
|
||||||
parameters: [path('gid','Group to abort quest in','string')]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
|
|
||||||
action: groups.questAbort
|
|
||||||
|
|
||||||
#TODO PUT /groups/:gid/chat/:messageId
|
|
||||||
|
|
||||||
"/groups/{gid}/chat:GET":
|
|
||||||
spec:
|
|
||||||
path: "/groups/{gid}/chat"
|
|
||||||
description: "Get all chat messages"
|
|
||||||
parameters: [path('gid','Group to return the chat from ','string')]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
|
|
||||||
action: groups.getChat
|
|
||||||
|
|
||||||
|
|
||||||
"/groups/{gid}/chat:POST":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
path: "/groups/{gid}/chat"
|
|
||||||
description: "Send a chat message"
|
|
||||||
parameters: [
|
|
||||||
query 'message', 'Chat message','string'
|
|
||||||
path 'gid','Group id','string'
|
|
||||||
]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
|
|
||||||
action: groups.postChat
|
|
||||||
|
|
||||||
# placing before route below, so that if !=='seen' it goes to next()
|
|
||||||
"/groups/{gid}/chat/seen":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Flag chat messages for a particular group as seen"
|
|
||||||
parameters: [
|
|
||||||
path 'gid','Group id','string'
|
|
||||||
]
|
|
||||||
action: groups.seenMessage
|
|
||||||
|
|
||||||
"/groups/{gid}/chat/{messageId}":
|
|
||||||
spec:
|
|
||||||
method: 'DELETE'
|
|
||||||
description: 'Delete a chat message in a given group'
|
|
||||||
parameters: [
|
|
||||||
path 'gid', 'ID of the group containing the message to be deleted', 'string'
|
|
||||||
path 'messageId', 'ID of message to be deleted', 'string'
|
|
||||||
]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
|
|
||||||
action: groups.deleteChatMessage
|
|
||||||
|
|
||||||
"/groups/{gid}/chat/{mid}/like":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Like a chat message"
|
|
||||||
parameters: [
|
|
||||||
path 'gid','Group id','string'
|
|
||||||
path 'mid','Message id','string'
|
|
||||||
]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
|
|
||||||
action: groups.likeChatMessage
|
|
||||||
|
|
||||||
"/groups/{gid}/chat/{mid}/flag":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Flag a chat message"
|
|
||||||
parameters: [
|
|
||||||
path 'gid','Group id','string'
|
|
||||||
path 'mid','Message id','string'
|
|
||||||
]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
|
|
||||||
action: groups.flagChatMessage
|
|
||||||
|
|
||||||
"/groups/{gid}/chat/{mid}/clearflags":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Clear flag count from message and unhide it"
|
|
||||||
parameters: [
|
|
||||||
path 'gid','Group id','string'
|
|
||||||
path 'mid','Message id','string'
|
|
||||||
]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup]
|
|
||||||
action: groups.clearFlagCount
|
|
||||||
|
|
||||||
# ---------------------------------
|
|
||||||
# Members
|
|
||||||
# ---------------------------------
|
|
||||||
"/members/{uuid}:GET":
|
|
||||||
spec:
|
|
||||||
path: '/members/{uuid}'
|
|
||||||
description: "Get a member."
|
|
||||||
parameters: [path('uuid','Member ID','string')]
|
|
||||||
middleware: [i18n.getUserLanguage] # removed auth.auth, so anon users can view shared avatars
|
|
||||||
action: members.getMember
|
|
||||||
"/members/{uuid}/message":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: 'Send a private message to a member'
|
|
||||||
parameters: [
|
|
||||||
path 'uuid', 'The UUID of the member to message', 'string'
|
|
||||||
body '', '{"message": "The private message to send"}', 'object'
|
|
||||||
]
|
|
||||||
middleware: [auth.auth]
|
|
||||||
action: members.sendPrivateMessage
|
|
||||||
"/members/{uuid}/block":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: 'Block a member from sending private messages'
|
|
||||||
parameters: [
|
|
||||||
path 'uuid', 'The UUID of the member to message', 'string'
|
|
||||||
]
|
|
||||||
middleware: [auth.auth]
|
|
||||||
action: user.blockUser
|
|
||||||
"/members/{uuid}/gift":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: 'Send a gift to a member'
|
|
||||||
parameters: [
|
|
||||||
path 'uuid', 'The UUID of the member', 'string'
|
|
||||||
body '', '{"type": "gems or subscription", "gems":{"amount":Number, "fromBalance":Boolean}, "subscription":{"months":Number}}', 'object'
|
|
||||||
]
|
|
||||||
middleware: [auth.auth]
|
|
||||||
action: members.sendGift
|
|
||||||
|
|
||||||
# ---------------------------------
|
|
||||||
# Hall of Heroes / Patrons
|
|
||||||
# ---------------------------------
|
|
||||||
"/hall/heroes":
|
|
||||||
spec: {}
|
|
||||||
middleware:[auth.auth, i18n.getUserLanguage]
|
|
||||||
action: hall.getHeroes
|
|
||||||
|
|
||||||
"/hall/heroes/{uid}:GET":
|
|
||||||
spec: path: "/hall/heroes/{uid}"
|
|
||||||
middleware:[auth.auth, i18n.getUserLanguage, hall.ensureAdmin]
|
|
||||||
action: hall.getHero
|
|
||||||
|
|
||||||
"/hall/heroes/{uid}:POST":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
path: "/hall/heroes/{uid}"
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage, hall.ensureAdmin]
|
|
||||||
action: hall.updateHero
|
|
||||||
|
|
||||||
"/hall/patrons":
|
|
||||||
spec:
|
|
||||||
parameters: [
|
|
||||||
query 'page','Page number to fetch (this list is long)','string'
|
|
||||||
]
|
|
||||||
middleware:[auth.auth, i18n.getUserLanguage]
|
|
||||||
action: hall.getPatrons
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------
|
|
||||||
# Challenges
|
|
||||||
# ---------------------------------
|
|
||||||
|
|
||||||
# Note: while challenges belong to groups, and would therefore make sense as a nested resource
|
|
||||||
# (eg /groups/:gid/challenges/:cid), they will also be referenced by users from the "challenges" tab
|
|
||||||
# without knowing which group they belong to. So to prevent unecessary lookups, we have them as a top-level resource
|
|
||||||
"/challenges:GET":
|
|
||||||
spec:
|
|
||||||
path: '/challenges'
|
|
||||||
description: "Get a list of challenges"
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage]
|
|
||||||
action: challenges.list
|
|
||||||
|
|
||||||
|
|
||||||
"/challenges:POST":
|
|
||||||
spec:
|
|
||||||
path: '/challenges'
|
|
||||||
method: 'POST'
|
|
||||||
description: "Create a challenge"
|
|
||||||
parameters: [body('','Challenge object (see ChallengeSchema)','object')]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage]
|
|
||||||
action: challenges.create
|
|
||||||
|
|
||||||
"/challenges/{cid}:GET":
|
|
||||||
spec:
|
|
||||||
path: '/challenges/{cid}'
|
|
||||||
description: 'Get a challenge'
|
|
||||||
parameters: [path('cid','Challenge id','string')]
|
|
||||||
action: challenges.get
|
|
||||||
|
|
||||||
"/challenges/{cid}/csv":
|
|
||||||
spec:
|
|
||||||
description: 'Get a challenge (csv format)'
|
|
||||||
parameters: [path('cid','Challenge id','string')]
|
|
||||||
action: challenges.csv
|
|
||||||
|
|
||||||
"/challenges/{cid}:POST":
|
|
||||||
spec:
|
|
||||||
path: '/challenges/{cid}'
|
|
||||||
method: 'POST'
|
|
||||||
description: "Update a challenge"
|
|
||||||
parameters: [
|
|
||||||
path 'cid','Challenge id','string'
|
|
||||||
body('','Challenge object (see ChallengeSchema)','object')
|
|
||||||
]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage]
|
|
||||||
action: challenges.update
|
|
||||||
|
|
||||||
"/challenges/{cid}:DELETE":
|
|
||||||
spec:
|
|
||||||
path: '/challenges/{cid}'
|
|
||||||
method: 'DELETE'
|
|
||||||
description: "Delete a challenge"
|
|
||||||
parameters: [path('cid','Challenge id','string')]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage]
|
|
||||||
action: challenges["delete"]
|
|
||||||
|
|
||||||
"/challenges/{cid}/close":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: 'Close a challenge'
|
|
||||||
parameters: [
|
|
||||||
path 'cid','Challenge id','string'
|
|
||||||
query 'uid','User ID of the winner','string',true
|
|
||||||
]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage]
|
|
||||||
action: challenges.selectWinner
|
|
||||||
|
|
||||||
"/challenges/{cid}/join":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: "Join a challenge"
|
|
||||||
parameters: [path('cid','Challenge id','string')]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage]
|
|
||||||
action: challenges.join
|
|
||||||
|
|
||||||
"/challenges/{cid}/leave":
|
|
||||||
spec:
|
|
||||||
method: 'POST'
|
|
||||||
description: 'Leave a challenge'
|
|
||||||
parameters: [path('cid','Challenge id','string')]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage]
|
|
||||||
action: challenges.leave
|
|
||||||
|
|
||||||
"/challenges/{cid}/member/{uid}":
|
|
||||||
spec:
|
|
||||||
description: "Get a member's progress in a particular challenge"
|
|
||||||
parameters: [
|
|
||||||
path 'cid','Challenge id','string'
|
|
||||||
path 'uid','User id','string'
|
|
||||||
]
|
|
||||||
middleware: [auth.auth, i18n.getUserLanguage]
|
|
||||||
action: challenges.getMember
|
|
||||||
|
|
||||||
|
|
||||||
if nconf.get("NODE_ENV") is "development"
|
|
||||||
api["/user/addTenGems"] =
|
|
||||||
spec: method:'POST'
|
|
||||||
action: user.addTenGems
|
|
||||||
|
|
||||||
_.each api, (route, path) ->
|
|
||||||
## Spec format is:
|
|
||||||
# spec:
|
|
||||||
# path: "/pet/{petId}"
|
|
||||||
# description: "Operations about pets"
|
|
||||||
# notes: "Returns a pet based on ID"
|
|
||||||
# summary: "Find pet by ID"
|
|
||||||
# method: "GET"
|
|
||||||
# parameters: [path("petId", "ID of pet that needs to be fetched", "string")]
|
|
||||||
# type: "Pet"
|
|
||||||
# errorResponses: [swagger.errors.invalid("id"), swagger.errors.notFound("pet")]
|
|
||||||
# nickname: "getPetById"
|
|
||||||
|
|
||||||
route.spec.description ?= ''
|
|
||||||
_.defaults route.spec,
|
|
||||||
path: path
|
|
||||||
nickname: path
|
|
||||||
notes: route.spec.description
|
|
||||||
summary: route.spec.description
|
|
||||||
parameters: []
|
|
||||||
#type: 'Pet'
|
|
||||||
errorResponses: []
|
|
||||||
method: 'GET'
|
|
||||||
route.middleware ?= if path.indexOf('/user') is 0 then [auth.auth, i18n.getUserLanguage, cron] else [i18n.getUserLanguage]
|
|
||||||
swagger["add#{route.spec.method}"](route);true
|
|
||||||
|
|
||||||
|
|
||||||
swagger.configure("#{nconf.get('BASE_URL')}/api/v2", "2")
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
var auth = require('../controllers/auth');
|
|
||||||
var express = require('express');
|
|
||||||
var i18n = require('../i18n');
|
|
||||||
var router = new express.Router();
|
|
||||||
|
|
||||||
/* auth.auth*/
|
|
||||||
auth.setupPassport(router); //FIXME make this consistent with the others
|
|
||||||
router.post('/api/v2/register', i18n.getUserLanguage, auth.registerUser);
|
|
||||||
router.post('/api/v2/user/auth/local', i18n.getUserLanguage, auth.loginLocal);
|
|
||||||
router.post('/api/v2/user/auth/social', i18n.getUserLanguage, auth.loginSocial);
|
|
||||||
router.delete('/api/v2/user/auth/social', i18n.getUserLanguage, auth.auth, auth.deleteSocial);
|
|
||||||
router.post('/api/v2/user/reset-password', i18n.getUserLanguage, auth.resetPassword);
|
|
||||||
router.post('/api/v2/user/change-password', i18n.getUserLanguage, auth.auth, auth.changePassword);
|
|
||||||
router.post('/api/v2/user/change-username', i18n.getUserLanguage, auth.auth, auth.changeUsername);
|
|
||||||
router.post('/api/v2/user/change-email', i18n.getUserLanguage, auth.auth, auth.changeEmail);
|
|
||||||
|
|
||||||
router.post('/api/v1/register', i18n.getUserLanguage, auth.registerUser);
|
|
||||||
router.post('/api/v1/user/auth/local', i18n.getUserLanguage, auth.loginLocal);
|
|
||||||
router.post('/api/v1/user/auth/social', i18n.getUserLanguage, auth.loginSocial);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
var nconf = require('nconf');
|
|
||||||
var express = require('express');
|
|
||||||
var router = new express.Router();
|
|
||||||
var auth = require('../controllers/auth');
|
|
||||||
var coupon = require('../controllers/coupon');
|
|
||||||
var i18n = require('../i18n');
|
|
||||||
|
|
||||||
router.get('/api/v2/coupons', auth.authWithUrl, i18n.getUserLanguage, coupon.ensureAdmin, coupon.getCoupons);
|
|
||||||
router.post('/api/v2/coupons/generate/:event', auth.auth, i18n.getUserLanguage, coupon.ensureAdmin, coupon.generateCoupons);
|
|
||||||
router.post('/api/v2/user/coupon/:code', auth.auth, i18n.getUserLanguage, coupon.enterCode);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
var express = require('express');
|
|
||||||
var router = new express.Router();
|
|
||||||
var dataexport = require('../controllers/dataexport');
|
|
||||||
var auth = require('../controllers/auth');
|
|
||||||
var nconf = require('nconf');
|
|
||||||
var i18n = require('../i18n');
|
|
||||||
var middleware = require('../middleware.js');
|
|
||||||
|
|
||||||
/* Data export */
|
|
||||||
router.get('/history.csv',auth.authWithSession,i18n.getUserLanguage,dataexport.history); //[todo] encode data output options in the data controller and use these to build routes
|
|
||||||
router.get('/userdata.xml',auth.authWithSession,i18n.getUserLanguage,dataexport.leanuser,dataexport.userdata.xml);
|
|
||||||
router.get('/userdata.json',auth.authWithSession,i18n.getUserLanguage,dataexport.leanuser,dataexport.userdata.json);
|
|
||||||
router.get('/avatar-:uuid.html', i18n.getUserLanguage, middleware.locals, dataexport.avatarPage);
|
|
||||||
router.get('/avatar-:uuid.png', i18n.getUserLanguage, middleware.locals, dataexport.avatarImage);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
var nconf = require('nconf');
|
|
||||||
var express = require('express');
|
|
||||||
var router = new express.Router();
|
|
||||||
var _ = require('lodash');
|
|
||||||
var middleware = require('../middleware');
|
|
||||||
var user = require('../controllers/user');
|
|
||||||
var auth = require('../controllers/auth');
|
|
||||||
var i18n = require('../i18n');
|
|
||||||
|
|
||||||
// -------- App --------
|
|
||||||
router.get('/', i18n.getUserLanguage, middleware.locals, function(req, res) {
|
|
||||||
if (!req.headers['x-api-user'] && !req.headers['x-api-key'] && !(req.session && req.session.userId))
|
|
||||||
return res.redirect('/static/front')
|
|
||||||
|
|
||||||
return res.render('index', {
|
|
||||||
title: 'HabitRPG | Your Life, The Role Playing Game',
|
|
||||||
env: res.locals.habitrpg
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// -------- Marketing --------
|
|
||||||
|
|
||||||
var pages = ['front', 'privacy', 'terms', 'api', 'features', 'videos', 'contact', 'plans', 'new-stuff', 'community-guidelines', 'old-news', 'press-kit'];
|
|
||||||
|
|
||||||
_.each(pages, function(name){
|
|
||||||
router.get('/static/' + name, i18n.getUserLanguage, middleware.locals, function(req, res) {
|
|
||||||
res.render('static/' + name, {env: res.locals.habitrpg});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
// --------- Redirects --------
|
|
||||||
|
|
||||||
router.get('/static/extensions', function(req, res) {
|
|
||||||
res.redirect('http://habitrpg.wikia.com/wiki/App_and_Extension_Integrations');
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
var nconf = require('nconf');
|
|
||||||
var express = require('express');
|
|
||||||
var router = new express.Router();
|
|
||||||
var auth = require('../controllers/auth');
|
|
||||||
var payments = require('../controllers/payments');
|
|
||||||
var i18n = require('../i18n');
|
|
||||||
|
|
||||||
router.get('/paypal/checkout', auth.authWithUrl, i18n.getUserLanguage, payments.paypalCheckout);
|
|
||||||
router.get('/paypal/checkout/success', i18n.getUserLanguage, payments.paypalCheckoutSuccess);
|
|
||||||
router.get('/paypal/subscribe', auth.authWithUrl, i18n.getUserLanguage, payments.paypalSubscribe);
|
|
||||||
router.get('/paypal/subscribe/success', i18n.getUserLanguage, payments.paypalSubscribeSuccess);
|
|
||||||
router.get('/paypal/subscribe/cancel', auth.authWithUrl, i18n.getUserLanguage, payments.paypalSubscribeCancel);
|
|
||||||
router.post('/paypal/ipn', i18n.getUserLanguage, payments.paypalIPN); // misc ipn handling
|
|
||||||
|
|
||||||
router.post("/stripe/checkout", auth.auth, i18n.getUserLanguage, payments.stripeCheckout);
|
|
||||||
router.post("/stripe/subscribe/edit", auth.auth, i18n.getUserLanguage, payments.stripeSubscribeEdit)
|
|
||||||
//router.get("/stripe/subscribe", auth.authWithUrl, i18n.getUserLanguage, payments.stripeSubscribe); // checkout route is used (above) with ?plan= instead
|
|
||||||
router.get("/stripe/subscribe/cancel", auth.authWithUrl, i18n.getUserLanguage, payments.stripeSubscribeCancel);
|
|
||||||
|
|
||||||
router.post("/iap/android/verify", auth.authWithUrl, /*i18n.getUserLanguage, */payments.iapAndroidVerify);
|
|
||||||
router.post("/iap/ios/verify", /*auth.authWithUrl, i18n.getUserLanguage, */ payments.iapIosVerify);
|
|
||||||
|
|
||||||
router.get("/api/v2/coupons/valid-discount/:code", /*auth.authWithUrl, i18n.getUserLanguage, */ payments.validCoupon);
|
|
||||||
|
|
||||||
module.exports = router;
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
/*
|
|
||||||
* This script is no longer required due to this code in src/models/group.js:
|
|
||||||
* // initialize tavern if !exists (fresh installs)
|
|
||||||
* Group.count({_id:'habitrpg'},function(err,ct){
|
|
||||||
* ...
|
|
||||||
* })
|
|
||||||
*
|
|
||||||
* However we're keeping this script in case future seed updates are needed.
|
|
||||||
*
|
|
||||||
* Reference: https://github.com/HabitRPG/habitrpg/issues/3852#issuecomment-55334572
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
require('coffee-script') // for habitrpg-shared
|
|
||||||
var nconf = require('nconf');
|
|
||||||
var utils = require('./utils');
|
|
||||||
var logging = require('./logging');
|
|
||||||
utils.setupConfig();
|
|
||||||
var async = require('async');
|
|
||||||
var mongoose = require('mongoose');
|
|
||||||
User = require('./models/user').model;
|
|
||||||
Group = require('./models/group').model;
|
|
||||||
|
|
||||||
async.waterfall([
|
|
||||||
function(cb){
|
|
||||||
mongoose.connect(nconf.get('NODE_DB_URI'), cb);
|
|
||||||
},
|
|
||||||
function(cb){
|
|
||||||
Group.findById('habitrpg', cb);
|
|
||||||
},
|
|
||||||
function(tavern, cb){
|
|
||||||
logging.info({tavern:tavern,cb:cb});
|
|
||||||
if (!tavern) {
|
|
||||||
tavern = new Group({
|
|
||||||
_id: 'habitrpg',
|
|
||||||
chat: [],
|
|
||||||
leader: '9',
|
|
||||||
name: 'HabitRPG',
|
|
||||||
type: 'guild',
|
|
||||||
privacy:'public'
|
|
||||||
});
|
|
||||||
tavern.save(cb)
|
|
||||||
} else {
|
|
||||||
cb();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],function(err){
|
|
||||||
if (err) throw err;
|
|
||||||
logging.info("Done initializing database");
|
|
||||||
mongoose.disconnect();
|
|
||||||
})
|
|
||||||
|
|
||||||
*/
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
// Only do the minimal amount of work before forking just in case of a dyno restart
|
|
||||||
var cluster = require("cluster");
|
|
||||||
var _ = require('lodash');
|
|
||||||
var nconf = require('nconf');
|
|
||||||
var utils = require('./utils');
|
|
||||||
utils.setupConfig();
|
|
||||||
var logging = require('./logging');
|
|
||||||
var isProd = nconf.get('NODE_ENV') === 'production';
|
|
||||||
var isDev = nconf.get('NODE_ENV') === 'development';
|
|
||||||
var cores = +nconf.get("CORES");
|
|
||||||
|
|
||||||
if (cores!==0 && cluster.isMaster && (isDev || isProd)) {
|
|
||||||
// Fork workers. If config.json has CORES=x, use that - otherwise, use all cpus-1 (production)
|
|
||||||
_.times(cores || require('os').cpus().length-1, cluster.fork);
|
|
||||||
|
|
||||||
cluster.on('disconnect', function(worker, code, signal) {
|
|
||||||
var w = cluster.fork(); // replace the dead worker
|
|
||||||
logging.info('[%s] [master:%s] worker:%s disconnect! new worker:%s fork', new Date(), process.pid, worker.process.pid, w.process.pid);
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
|
||||||
require('coffee-script/register'); // remove this once we've fully converted over
|
|
||||||
var express = require("express");
|
|
||||||
var http = require("http");
|
|
||||||
var path = require("path");
|
|
||||||
var swagger = require("swagger-node-express");
|
|
||||||
var autoinc = require('mongoose-id-autoinc');
|
|
||||||
var shared = require('../../common');
|
|
||||||
|
|
||||||
// Setup translations
|
|
||||||
var i18n = require('./i18n');
|
|
||||||
|
|
||||||
var middleware = require('./middleware');
|
|
||||||
|
|
||||||
var TWO_WEEKS = 1000 * 60 * 60 * 24 * 14;
|
|
||||||
var app = express();
|
|
||||||
var server = http.createServer();
|
|
||||||
|
|
||||||
// ------------ MongoDB Configuration ------------
|
|
||||||
mongoose = require('mongoose');
|
|
||||||
var mongooseOptions = !isProd ? {} : {
|
|
||||||
replset: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } },
|
|
||||||
server: { socketOptions: { keepAlive: 1, connectTimeoutMS: 30000 } }
|
|
||||||
};
|
|
||||||
var db = mongoose.connect(nconf.get('NODE_DB_URI'), mongooseOptions, function(err) {
|
|
||||||
if (err) throw err;
|
|
||||||
logging.info('Connected with Mongoose');
|
|
||||||
});
|
|
||||||
autoinc.init(db);
|
|
||||||
|
|
||||||
// load schemas & models
|
|
||||||
require('./models/challenge');
|
|
||||||
require('./models/group');
|
|
||||||
require('./models/user');
|
|
||||||
|
|
||||||
// ------------ Passport Configuration ------------
|
|
||||||
var passport = require('passport')
|
|
||||||
var util = require('util')
|
|
||||||
var FacebookStrategy = require('passport-facebook').Strategy;
|
|
||||||
// Passport session setup.
|
|
||||||
// To support persistent login sessions, Passport needs to be able to
|
|
||||||
// serialize users into and deserialize users out of the session. Typically,
|
|
||||||
// this will be as simple as storing the user ID when serializing, and finding
|
|
||||||
// the user by ID when deserializing. However, since this example does not
|
|
||||||
// have a database of user records, the complete Facebook profile is serialized
|
|
||||||
// and deserialized.
|
|
||||||
passport.serializeUser(function(user, done) {
|
|
||||||
done(null, user);
|
|
||||||
});
|
|
||||||
|
|
||||||
passport.deserializeUser(function(obj, done) {
|
|
||||||
done(null, obj);
|
|
||||||
});
|
|
||||||
|
|
||||||
// FIXME
|
|
||||||
// This auth strategy is no longer used. It's just kept around for auth.js#loginFacebook() (passport._strategies.facebook.userProfile)
|
|
||||||
// The proper fix would be to move to a general OAuth module simply to verify accessTokens
|
|
||||||
passport.use(new FacebookStrategy({
|
|
||||||
clientID: nconf.get("FACEBOOK_KEY"),
|
|
||||||
clientSecret: nconf.get("FACEBOOK_SECRET"),
|
|
||||||
//callbackURL: nconf.get("BASE_URL") + "/auth/facebook/callback"
|
|
||||||
},
|
|
||||||
function(accessToken, refreshToken, profile, done) {
|
|
||||||
done(null, profile);
|
|
||||||
}
|
|
||||||
));
|
|
||||||
|
|
||||||
// ------------ Server Configuration ------------
|
|
||||||
var publicDir = path.join(__dirname, "/../public");
|
|
||||||
|
|
||||||
app.set("port", nconf.get('PORT'));
|
|
||||||
middleware.apiThrottle(app);
|
|
||||||
app.use(middleware.domainMiddleware(server,mongoose));
|
|
||||||
if (!isProd) app.use(express.logger("dev"));
|
|
||||||
app.use(express.compress());
|
|
||||||
app.set("views", __dirname + "/../views");
|
|
||||||
app.set("view engine", "jade");
|
|
||||||
app.use(express.favicon(publicDir + '/favicon.ico'));
|
|
||||||
app.use(middleware.cors);
|
|
||||||
app.use(middleware.forceSSL);
|
|
||||||
app.use(express.urlencoded());
|
|
||||||
app.use(express.json());
|
|
||||||
app.use(require('method-override')());
|
|
||||||
//app.use(express.cookieParser(nconf.get('SESSION_SECRET')));
|
|
||||||
app.use(express.cookieParser());
|
|
||||||
app.use(express.cookieSession({ secret: nconf.get('SESSION_SECRET'), httpOnly: false, cookie: { maxAge: TWO_WEEKS }}));
|
|
||||||
//app.use(express.session());
|
|
||||||
|
|
||||||
// Initialize Passport! Also use passport.session() middleware, to support
|
|
||||||
// persistent login sessions (recommended).
|
|
||||||
app.use(passport.initialize());
|
|
||||||
app.use(passport.session());
|
|
||||||
|
|
||||||
app.use(app.router);
|
|
||||||
|
|
||||||
var maxAge = isProd ? 31536000000 : 0;
|
|
||||||
// Cache emojis without copying them to build, they are too many
|
|
||||||
app.use(express['static'](path.join(__dirname, "/../build"), { maxAge: maxAge }));
|
|
||||||
app.use('/common/dist', express['static'](publicDir + "/../../common/dist", { maxAge: maxAge }));
|
|
||||||
app.use('/common/audio', express['static'](publicDir + "/../../common/audio", { maxAge: maxAge }));
|
|
||||||
app.use('/common/script/public', express['static'](publicDir + "/../../common/script/public", { maxAge: maxAge }));
|
|
||||||
app.use('/common/img/emoji/unicode', express['static'](publicDir + "/../../common/img/emoji/unicode", { maxAge: maxAge }));
|
|
||||||
app.use(express['static'](publicDir));
|
|
||||||
|
|
||||||
// Custom Directives
|
|
||||||
app.use(require('./routes/pages').middleware);
|
|
||||||
app.use(require('./routes/payments').middleware);
|
|
||||||
app.use(require('./routes/auth').middleware);
|
|
||||||
app.use(require('./routes/coupon').middleware);
|
|
||||||
var v2 = express();
|
|
||||||
app.use('/api/v2', v2);
|
|
||||||
app.use('/api/v1', require('./routes/apiv1').middleware);
|
|
||||||
app.use('/export', require('./routes/dataexport').middleware);
|
|
||||||
require('./routes/apiv2.coffee')(swagger, v2);
|
|
||||||
app.use(middleware.errorHandler);
|
|
||||||
|
|
||||||
server.on('request', app);
|
|
||||||
server.listen(app.get("port"), function() {
|
|
||||||
return logging.info("Express server listening on port " + app.get("port"));
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = server;
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
var nodemailer = require('nodemailer');
|
|
||||||
var nconf = require('nconf');
|
|
||||||
var crypto = require('crypto');
|
|
||||||
var path = require("path");
|
|
||||||
var request = require('request');
|
|
||||||
|
|
||||||
// Set when utils.setupConfig is run
|
|
||||||
var isProd, baseUrl;
|
|
||||||
|
|
||||||
module.exports.ga = undefined; // set Google Analytics on nconf init
|
|
||||||
|
|
||||||
module.exports.sendEmail = function(mailData) {
|
|
||||||
var smtpTransport = nodemailer.createTransport("SMTP",{
|
|
||||||
service: nconf.get('SMTP_SERVICE'),
|
|
||||||
auth: {
|
|
||||||
user: nconf.get('SMTP_USER'),
|
|
||||||
pass: nconf.get('SMTP_PASS')
|
|
||||||
}
|
|
||||||
});
|
|
||||||
smtpTransport.sendMail(mailData, function(error, response){
|
|
||||||
var logging = require('./logging');
|
|
||||||
if(error) logging.error(error);
|
|
||||||
else logging.info("Message sent: " + response.message);
|
|
||||||
smtpTransport.close(); // shut down the connection pool, no more messages
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUserInfo(user, fields) {
|
|
||||||
var info = {};
|
|
||||||
|
|
||||||
if(fields.indexOf('name') != -1){
|
|
||||||
if(user.auth.local){
|
|
||||||
info.name = user.profile.name || user.auth.local.username;
|
|
||||||
}else if(user.auth.facebook){
|
|
||||||
info.name = user.auth.facebook.displayName || user.auth.facebook.username;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(fields.indexOf('email') != -1){
|
|
||||||
if(user.auth.local){
|
|
||||||
info.email = user.auth.local.email;
|
|
||||||
}else if(user.auth.facebook && user.auth.facebook.emails && user.auth.facebook.emails[0] && user.auth.facebook.emails[0].value){
|
|
||||||
info.email = user.auth.facebook.emails[0].value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if(fields.indexOf('canSend') != -1){
|
|
||||||
info.canSend = user.preferences.emailNotifications.unsubscribeFromAll !== true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.getUserInfo = getUserInfo;
|
|
||||||
|
|
||||||
module.exports.txnEmail = function(mailingInfoArray, emailType, variables){
|
|
||||||
var mailingInfoArray = Array.isArray(mailingInfoArray) ? mailingInfoArray : [mailingInfoArray];
|
|
||||||
var variables = [
|
|
||||||
{name: 'BASE_URL', content: baseUrl},
|
|
||||||
{name: 'EMAIL_SETTINGS_URL', content: baseUrl + '/#/options/settings/notifications'}
|
|
||||||
].concat(variables || []);
|
|
||||||
|
|
||||||
// It's important to pass at least a user with its `preferences` as we need to check if he unsubscribed
|
|
||||||
mailingInfoArray = mailingInfoArray.map(function(mailingInfo){
|
|
||||||
return mailingInfo._id ? getUserInfo(mailingInfo, ['email', 'name', 'canSend']) : mailingInfo;
|
|
||||||
}).filter(function(mailingInfo){
|
|
||||||
return (mailingInfo.email && mailingInfo.canSend);
|
|
||||||
});
|
|
||||||
|
|
||||||
// When only one recipient send his info as variables
|
|
||||||
if(mailingInfoArray.length === 1 && mailingInfoArray[0].name){
|
|
||||||
variables.push({name: 'RECIPIENT_NAME', content: mailingInfoArray[0].name});
|
|
||||||
}
|
|
||||||
|
|
||||||
if(isProd && mailingInfoArray.length > 0){
|
|
||||||
request({
|
|
||||||
url: nconf.get('EMAIL_SERVER:url') + '/job',
|
|
||||||
method: 'POST',
|
|
||||||
auth: {
|
|
||||||
user: nconf.get('EMAIL_SERVER:authUser'),
|
|
||||||
pass: nconf.get('EMAIL_SERVER:authPassword')
|
|
||||||
},
|
|
||||||
json: {
|
|
||||||
type: 'email',
|
|
||||||
data: {
|
|
||||||
emailType: emailType,
|
|
||||||
to: mailingInfoArray,
|
|
||||||
variables: variables
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
attemps: 5,
|
|
||||||
backoff: {delay: 10*60*1000, type: 'fixed'}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encryption using http://dailyjs.com/2010/12/06/node-tutorial-5/
|
|
||||||
// Note: would use [password-hash](https://github.com/davidwood/node-password-hash), but we need to run
|
|
||||||
// model.query().equals(), so it's a PITA to work in their verify() function
|
|
||||||
|
|
||||||
module.exports.encryptPassword = function(password, salt) {
|
|
||||||
return crypto.createHmac('sha1', salt).update(password).digest('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.makeSalt = function() {
|
|
||||||
var len = 10;
|
|
||||||
return crypto.randomBytes(Math.ceil(len / 2)).toString('hex').substring(0, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load nconf and define default configuration values if config.json or ENV vars are not found
|
|
||||||
*/
|
|
||||||
module.exports.setupConfig = function(){
|
|
||||||
nconf.argv()
|
|
||||||
.env()
|
|
||||||
//.file('defaults', path.join(path.resolve(__dirname, '../config.json.example')))
|
|
||||||
.file('user', path.join(path.resolve(__dirname, '../config.json')));
|
|
||||||
|
|
||||||
if (nconf.get('NODE_ENV') === "development")
|
|
||||||
Error.stackTraceLimit = Infinity;
|
|
||||||
if (nconf.get('NODE_ENV') === 'production')
|
|
||||||
require('newrelic');
|
|
||||||
|
|
||||||
isProd = nconf.get('NODE_ENV') === 'production';
|
|
||||||
baseUrl = nconf.get('BASE_URL');
|
|
||||||
|
|
||||||
module.exports.ga = require('universal-analytics')(nconf.get('GA_ID'));
|
|
||||||
};
|
|
||||||
|
|
||||||
var algorithm = 'aes-256-ctr';
|
|
||||||
module.exports.encrypt = function(text){
|
|
||||||
var cipher = crypto.createCipher(algorithm,nconf.get('SESSION_SECRET'))
|
|
||||||
var crypted = cipher.update(text,'utf8','hex')
|
|
||||||
crypted += cipher.final('hex');
|
|
||||||
return crypted;
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports.decrypt = function(text){
|
|
||||||
var decipher = crypto.createDecipher(algorithm,nconf.get('SESSION_SECRET'))
|
|
||||||
var dec = decipher.update(text,'hex','utf8')
|
|
||||||
dec += decipher.final('utf8');
|
|
||||||
return dec;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user