Merge branch 'develop' into 3onyc-fix-hash-leak
This commit is contained in:
@@ -7,7 +7,8 @@ var utils = require('../utils');
|
||||
var nconf = require('nconf');
|
||||
var request = require('request');
|
||||
var User = require('../models/user').model;
|
||||
var ga = require('./../utils').ga;
|
||||
var EmailUnsubscription = require('../models/emailUnsubscription').model;
|
||||
var analytics = utils.analytics;
|
||||
var i18n = require('./../i18n');
|
||||
|
||||
var isProd = nconf.get('NODE_ENV') === 'production';
|
||||
@@ -19,7 +20,7 @@ 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.',
|
||||
err: 'Account has been suspended, please contact leslie@habitica.com with your UUID ('+uuid+') for assistance.',
|
||||
code: 'ACCOUNT_SUSPENDED'
|
||||
};
|
||||
}
|
||||
@@ -61,7 +62,7 @@ api.authWithUrl = function(req, res, next) {
|
||||
if (_.isEmpty(user)) return res.json(401, NO_USER_FOUND);
|
||||
res.locals.user = user;
|
||||
next();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
api.registerUser = function(req, res, next) {
|
||||
@@ -109,9 +110,21 @@ api.registerUser = function(req, res, next) {
|
||||
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);
|
||||
|
||||
var analyticsData = {
|
||||
category: 'acquisition',
|
||||
type: 'local',
|
||||
gaLabel: 'local'
|
||||
};
|
||||
analytics.track('register', analyticsData)
|
||||
|
||||
user.save(function(err, savedUser){
|
||||
// Clean previous email preferences
|
||||
EmailUnsubscription.remove({email: savedUser.auth.local.email}, function(){
|
||||
utils.txnEmail(savedUser, 'welcome');
|
||||
});
|
||||
cb.apply(cb, arguments);
|
||||
});
|
||||
}
|
||||
}]
|
||||
}, function(err, data) {
|
||||
@@ -132,7 +145,7 @@ api.loginLocal = function(req, res, next) {
|
||||
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) return res.json(401, {err:"Uh-oh - your username or password is incorrect.\n- Make sure your username or email is typed correctly.\n- You may have signed up with Facebook, not email. Double-check by trying Facebook login.\n- If you forgot your password, click \"Forgot Password\"."});
|
||||
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(
|
||||
@@ -140,7 +153,7 @@ api.loginLocal = function(req, res, next) {
|
||||
, {_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)"});
|
||||
if (!user) return res.json(401,{err:"Uh-oh - your username or password is incorrect.\n- Make sure your username or email is typed correctly.\n- You may have signed up with Facebook, not email. Double-check by trying Facebook login.\n- If you forgot your password, click \"Forgot Password\"."});
|
||||
res.json({id: user._id,token: user.apiToken});
|
||||
password = null;
|
||||
});
|
||||
@@ -178,10 +191,22 @@ api.loginSocial = function(req, res, next) {
|
||||
};
|
||||
user.auth[network] = prof;
|
||||
user = new User(user);
|
||||
user.save(cb);
|
||||
user.save(function(err, savedUser){
|
||||
// Clean previous email preferences
|
||||
if(savedUser.auth.facebook.emails && savedUser.auth.facebook.emails[0] && savedUser.auth.facebook.emails[0].value){
|
||||
EmailUnsubscription.remove({email: savedUser.auth.facebook.emails[0].value}, function(){
|
||||
utils.txnEmail(savedUser, 'welcome');
|
||||
});
|
||||
}
|
||||
cb.apply(cb, arguments);
|
||||
});
|
||||
|
||||
utils.txnEmail(user, 'welcome');
|
||||
ga.event('register', network).send();
|
||||
var analyticsData = {
|
||||
category: 'acquisition',
|
||||
type: network,
|
||||
gaLabel: network
|
||||
};
|
||||
analytics.track('register', analyticsData)
|
||||
}]
|
||||
}, function(err, results){
|
||||
if (err) return res.json(401, {err: err.toString ? err.toString() : err});
|
||||
@@ -214,16 +239,17 @@ api.resetPassword = function(req, res, next){
|
||||
|
||||
User.findOne({'auth.local.email': RegexEscape(email)}, function(err, user){
|
||||
if (err) return next(err);
|
||||
if (!user) return res.send(401, {err:"Couldn't find a user registered for email " + email});
|
||||
if (!user) return res.send(401, {err:"Sorry, we can't find a user registered with email " + email + "\n- Make sure your email address is typed correctly.\n- You may have signed up with Facebook, not email. Double-check by trying Facebook login."});
|
||||
user.auth.local.salt = salt;
|
||||
user.auth.local.hashed_password = hashed_password;
|
||||
utils.sendEmail({
|
||||
from: "HabitRPG <admin@habitrpg.com>",
|
||||
from: "Habitica <admin@habitica.com>",
|
||||
to: email,
|
||||
subject: "Password Reset for HabitRPG",
|
||||
text: "Password for " + user.auth.local.username + " has been reset to " + newPassword + ". Log in at " + nconf.get('BASE_URL') + ". After you've logged in, head to "+nconf.get('BASE_URL')+"/#/options/settings/settings and change your password.",
|
||||
html: "Password for <strong>" + user.auth.local.username + "</strong> has been reset to <strong>" + newPassword + "</strong>. Log in at " + nconf.get('BASE_URL') + ". After you've logged in, head to "+nconf.get('BASE_URL')+"/#/options/settings/settings and change your password."
|
||||
subject: "Password Reset for Habitica",
|
||||
text: "Password for " + user.auth.local.username + " has been reset to " + newPassword + " Important! Both username and password are case-sensitive -- you must enter both exactly as shown here. We recommend copying and pasting both instead of typing them. Log in at https://habitica.com/. After you've logged in, head to https://habitica.com/#/options/settings/settings and change your password.",
|
||||
html: "Password for <strong>" + user.auth.local.username + "</strong> has been reset to <strong>" + newPassword + "</strong><br /><br />Important! Both username and password are case-sensitive -- you must enter both exactly as shown here. We recommend copying and pasting both instead of typing them.<br /><br />Log in at https://habitica.com/. After you've logged in, head to https://habitica.com/#/options/settings/settings and change your password."
|
||||
});
|
||||
// TODO: change all four instances of habitica.com above to use BASE_URL when it has been updated. Previous version: https://github.com/HabitRPG/habitrpg/blob/2069936603aca7f8139a6c98e063136248262bdf/website/src/controllers/auth.js#L249
|
||||
user.save(function(err){
|
||||
if(err) return next(err);
|
||||
res.send('New password sent to '+ email);
|
||||
|
||||
@@ -11,7 +11,7 @@ var logging = require('./../logging');
|
||||
var csv = require('express-csv');
|
||||
var utils = require('../utils');
|
||||
var api = module.exports;
|
||||
|
||||
var pushNotify = require('./pushNotifications');
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
@@ -40,7 +40,7 @@ api.list = function(req, res, next) {
|
||||
.select('name leader description group memberCount prize official')
|
||||
.select({members:{$elemMatch:{$in:[user._id]}}})
|
||||
.sort('-official -timestamp')
|
||||
.populate('group', '_id name')
|
||||
.populate('group', '_id name type')
|
||||
.populate('leader', 'profile.name')
|
||||
.exec(cb);
|
||||
}
|
||||
@@ -56,17 +56,23 @@ api.list = function(req, res, next) {
|
||||
|
||||
// GET
|
||||
api.get = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
// 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')
|
||||
.populate('group', '_id name type')
|
||||
.populate('leader', 'profile.name')
|
||||
.exec(function(err, challenge){
|
||||
if(err) return next(err);
|
||||
if (!challenge) return res.json(404, {err: 'Challenge ' + req.params.cid + ' not found'});
|
||||
challenge._isMember = !!(_.find(challenge.members, function(member) {
|
||||
return member._id === user._id;
|
||||
}));
|
||||
res.json(challenge);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
api.csv = function(req, res, next) {
|
||||
@@ -219,7 +225,7 @@ api.update = function(req, res, next){
|
||||
},
|
||||
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");
|
||||
if (_before.leader != user._id && !user.contributor.admin) return cb(shared.i18n.t('noPermissionEditChallenge', req.language));
|
||||
// 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;
|
||||
@@ -294,13 +300,18 @@ function closeChal(cid, broken, cb) {
|
||||
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");
|
||||
if (chal.leader != user._id && !user.contributor.admin) return cb(shared.i18n.t('noPermissionDeleteChallenge', req.language));
|
||||
if (chal.group != 'habitrpg') user.balance += chal.prize/4; // Refund gems to user if a non-tavern challenge
|
||||
user.save(cb);
|
||||
},
|
||||
function(save, num, cb){
|
||||
closeChal(req.params.cid, {broken: 'CHALLENGE_DELETED'}, cb);
|
||||
}
|
||||
], function(err){
|
||||
@@ -325,7 +336,7 @@ api.selectWinner = function(req, res, next) {
|
||||
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");
|
||||
if (chal.leader != user._id && !user.contributor.admin) return cb(shared.i18n.t('noPermissionCloseChallenge', req.language));
|
||||
User.findById(req.query.uid, cb)
|
||||
},
|
||||
function(winner, cb){
|
||||
@@ -341,6 +352,9 @@ api.selectWinner = function(req, res, next) {
|
||||
{name: 'CHALLENGE_NAME', content: chal.name}
|
||||
]);
|
||||
}
|
||||
|
||||
pushNotify.sendNotify(saved, shared.i18n.t('wonChallenge'), chal.name);
|
||||
|
||||
closeChal(cid, {broken: 'CHALLENGE_CLOSED', winner: saved.profile.name}, cb);
|
||||
}
|
||||
], function(err){
|
||||
|
||||
@@ -102,13 +102,13 @@ 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)
|
||||
env: _.defaults({user:user}, res.locals.habitrpg)
|
||||
});
|
||||
})
|
||||
};
|
||||
|
||||
dataexport.avatarImage = function(req, res, next) {
|
||||
var filename = 'avatar-'+req.params.uuid+'.png';
|
||||
var filename = 'avatars/'+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)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
'use strict';
|
||||
// @see ../routes for routing
|
||||
|
||||
function clone(a) {
|
||||
@@ -12,8 +13,11 @@ var shared = require('../../../common');
|
||||
var User = require('./../models/user').model;
|
||||
var Group = require('./../models/group').model;
|
||||
var Challenge = require('./../models/challenge').model;
|
||||
var EmailUnsubscription = require('./../models/emailUnsubscription').model;
|
||||
var isProd = nconf.get('NODE_ENV') === 'production';
|
||||
var api = module.exports;
|
||||
var pushNotify = require('./pushNotifications');
|
||||
var analytics = utils.analytics;
|
||||
|
||||
/*
|
||||
------------------------------------------------------------------------
|
||||
@@ -30,7 +34,7 @@ var guildPopulate = {path: 'members', select: nameFields, options: {limit: 15} }
|
||||
* 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
|
||||
* @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){
|
||||
@@ -38,6 +42,7 @@ var populateQuery = function(type, q, additionalFields){
|
||||
q.populate('members', partyFields + (additionalFields ? (' ' + additionalFields) : ''));
|
||||
else
|
||||
q.populate(guildPopulate);
|
||||
q.populate('leader', nameFields);
|
||||
q.populate('invites', nameFields);
|
||||
q.populate({
|
||||
path: 'challenges',
|
||||
@@ -135,8 +140,36 @@ api.get = function(req, res, next) {
|
||||
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);
|
||||
if(!group){
|
||||
if(gid !== 'party') return res.json(404,{err: "Group not found or you don't have access."});
|
||||
|
||||
// Don't send a 404 when querying for a party even if it doesn't exist
|
||||
// so that users with no party don't get a 404 on every access to the site
|
||||
return res.json(group);
|
||||
}
|
||||
//Since we have a limit on how many members are populate to the group, we want to make sure the user is always in the group
|
||||
var userInGroup = _.find(group.members, function(member){ return member._id == user._id; });
|
||||
//If the group is private or the group is a party, then the user must be a member of the group based on access restrictions above
|
||||
if (group.privacy === 'private' || gid === 'party') {
|
||||
//If the user is not in the group query, remove a user and add the current user
|
||||
if (!userInGroup) {
|
||||
group.members.splice(0,1);
|
||||
group.members.push(user);
|
||||
}
|
||||
res.json(group);
|
||||
} else if ( group.privacy === "public" ) { //The group is public, we must do an extra check to see if the user is already in the group query
|
||||
//We must see how to check if a user is a member of a public group, so we requery
|
||||
var q2 = Group.findOne({ _id: group._id, privacy:'public', members: {$in:[user._id]} });
|
||||
q2.exec(function(err, group2){
|
||||
if (err) return next(err);
|
||||
if (group2 && !userInGroup) {
|
||||
group.members.splice(0,1);
|
||||
group.members.push(user);
|
||||
}
|
||||
res.json(group);
|
||||
});
|
||||
}
|
||||
|
||||
gid = null;
|
||||
});
|
||||
};
|
||||
@@ -164,7 +197,7 @@ api.create = function(req, res, next) {
|
||||
group = user = null;
|
||||
});
|
||||
|
||||
}else{
|
||||
} else{
|
||||
async.waterfall([
|
||||
function(cb){
|
||||
Group.findOne({type:'party',members:{$in:[user._id]}},cb);
|
||||
@@ -179,8 +212,8 @@ api.create = function(req, res, next) {
|
||||
], 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;
|
||||
return res.json(populated);
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -236,24 +269,28 @@ api.getChat = function(req, res, next) {
|
||||
* 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;
|
||||
if(!req.query.message) {
|
||||
return res.json(400,{err:'You cannot send a blank message'});
|
||||
} else {
|
||||
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
|
||||
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();
|
||||
if (group.type === 'party') {
|
||||
user.party.lastMessageSeen = group.chat[0].id;
|
||||
user.save();
|
||||
}
|
||||
|
||||
group.save(function(err, saved){
|
||||
if (err) return next(err);
|
||||
chatUpdated ? res.json({chat: group.chat}) : res.json({message: saved.chat[0]});
|
||||
group = chatUpdated = null;
|
||||
});
|
||||
}
|
||||
|
||||
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){
|
||||
@@ -304,8 +341,9 @@ api.flagChatMessage = function(req, res, next){
|
||||
group.markModified('chat');
|
||||
group.save(function(err,_saved){
|
||||
if(err) return next(err);
|
||||
var addressesToSendTo = JSON.parse(nconf.get('FLAG_REPORT_EMAIL'));
|
||||
|
||||
var addressesToSendTo = nconf.get('FLAG_REPORT_EMAIL');
|
||||
addressesToSendTo = (typeof addressesToSendTo == 'string') ? JSON.parse(addressesToSendTo) : addressesToSendTo;
|
||||
|
||||
if(Array.isArray(addressesToSendTo)){
|
||||
addressesToSendTo = addressesToSendTo.map(function(email){
|
||||
return {email: email, canSend: true}
|
||||
@@ -321,17 +359,17 @@ api.flagChatMessage = function(req, res, next){
|
||||
{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: "REPORTER_MODAL_URL", content: "/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: "AUTHOR_MODAL_URL", content: "/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')},
|
||||
{name: "GROUP_URL", content: group._id == 'habitrpg' ? '/#/options/groups/tavern' : (group.type === 'guild' ? ('/#/options/groups/guilds/' + group._id) : 'party')},
|
||||
]);
|
||||
|
||||
return res.send(204);
|
||||
@@ -358,7 +396,7 @@ api.clearFlagCount = function(req, res, next){
|
||||
}else{
|
||||
return res.json(401, {err: "Only an admin can clear the flag count!"})
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
api.seenMessage = function(req,res,next){
|
||||
@@ -387,13 +425,17 @@ api.likeChatMessage = function(req, res, next) {
|
||||
group.markModified('chat');
|
||||
group.save(function(err,_saved){
|
||||
if (err) return next(err);
|
||||
// @TODO: We're sending back the entire array of chats back
|
||||
// Should we just send back the object of the single chat message?
|
||||
// If not, should we update the group chat when a chat is liked?
|
||||
return res.send(_saved.chat);
|
||||
})
|
||||
}
|
||||
|
||||
api.join = function(req, res, next) {
|
||||
var user = res.locals.user,
|
||||
group = res.locals.group;
|
||||
group = res.locals.group,
|
||||
isUserInvited = false;
|
||||
|
||||
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
|
||||
@@ -401,19 +443,29 @@ api.join = function(req, res, next) {
|
||||
user.save();
|
||||
// invite new user to pending quest
|
||||
if (group.quest.key && !group.quest.active) {
|
||||
User.update({_id:user._id},{$set: {'party.quest.RSVPNeeded': true, 'party.quest.key': group.quest.key}}).exec();
|
||||
group.quest.members[user._id] = undefined;
|
||||
group.markModified('quest.members');
|
||||
}
|
||||
}
|
||||
else if (group.type == 'guild' && user.invitations && user.invitations.guilds) {
|
||||
isUserInvited = true;
|
||||
} 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 (~i){
|
||||
isUserInvited = true;
|
||||
user.invitations.guilds.splice(i,1);
|
||||
user.save();
|
||||
}else{
|
||||
isUserInvited = group.privacy === 'private' ? false : true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!isUserInvited) return res.json(401, {err: "Can't join a group you're not invited to."});
|
||||
|
||||
if (!_.contains(group.members, user._id)){
|
||||
group.members.push(user._id);
|
||||
group.invites.splice(_.indexOf(group.invites, user._id), 1);
|
||||
if (group.invites.length > 0) {
|
||||
group.invites.splice(_.indexOf(group.invites, user._id), 1);
|
||||
}
|
||||
}
|
||||
|
||||
async.series([
|
||||
@@ -425,7 +477,6 @@ api.join = function(req, res, next) {
|
||||
}
|
||||
], function(err, results){
|
||||
if (err) return next(err);
|
||||
|
||||
// Return the group? Or not?
|
||||
res.json(results[1]);
|
||||
group = null;
|
||||
@@ -504,8 +555,8 @@ api.leave = function(req, res, next) {
|
||||
}
|
||||
],function(err){
|
||||
if (err) return next(err);
|
||||
return res.send(204);
|
||||
user = group = keep = null;
|
||||
return res.send(204);
|
||||
})
|
||||
}
|
||||
|
||||
@@ -535,9 +586,13 @@ var inviteByUUIDs = function(uuids, group, req, res, next){
|
||||
function sendInvite (){
|
||||
if(group.type === 'guild'){
|
||||
invite.invitations.guilds.push({id: group._id, name: group.name, inviter:res.locals.user._id});
|
||||
|
||||
pushNotify.sendNotify(invite, shared.i18n.t('invitedGuild'), group.name);
|
||||
}else{
|
||||
//req.body.type in 'guild', 'party'
|
||||
invite.invitations.party = {id: group._id, name: group.name, inviter:res.locals.user._id};
|
||||
|
||||
pushNotify.sendNotify(invite, shared.i18n.t('invitedParty'), group.name);
|
||||
}
|
||||
|
||||
group.invites.push(invite._id);
|
||||
@@ -562,12 +617,12 @@ var inviteByUUIDs = function(uuids, group, req, res, next){
|
||||
if(group.type == 'guild'){
|
||||
emailVars.push(
|
||||
{name: 'GUILD_NAME', content: group.name},
|
||||
{name: 'GUILD_URL', content: nconf.get('BASE_URL') + '/#/options/groups/guilds/public'}
|
||||
{name: 'GUILD_URL', content: '/#/options/groups/guilds/public'}
|
||||
);
|
||||
}else{
|
||||
emailVars.push(
|
||||
{name: 'PARTY_NAME', content: group.name},
|
||||
{name: 'PARTY_URL', content: nconf.get('BASE_URL') + '/#/options/groups/party'}
|
||||
{name: 'PARTY_URL', content: '/#/options/groups/party'}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -577,7 +632,7 @@ var inviteByUUIDs = function(uuids, group, req, res, next){
|
||||
cb();
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}, function(err){
|
||||
if(err) return err.code ? res.json(err.code, {err: err.err}) : next(err);
|
||||
|
||||
@@ -609,7 +664,7 @@ var inviteByEmails = function(invites, group, req, res, next){
|
||||
}
|
||||
|
||||
// yeah, it supports guild too but for backward compatibility we'll use partyInvite as query
|
||||
var link = nconf.get('BASE_URL')+'?partyInvite='+ utils.encrypt(JSON.stringify({id:group._id, inviter:res.locals.user._id, name:group.name}));
|
||||
var link = '?partyInvite='+ utils.encrypt(JSON.stringify({id:group._id, inviter:res.locals.user._id, name:group.name}));
|
||||
|
||||
var inviterVars = utils.getUserInfo(res.locals.user, ['name', 'email']);
|
||||
var variables = [
|
||||
@@ -623,10 +678,15 @@ var inviteByEmails = function(invites, group, req, res, next){
|
||||
}
|
||||
|
||||
// TODO implement "users can only be invited once"
|
||||
invite.canSend = true; // Requested by utils.txnEmail
|
||||
utils.txnEmail(invite, ('invite-friend' + (group.type == 'guild' ? '-guild' : '')), variables);
|
||||
// Check for the email address not to be unsubscribed
|
||||
EmailUnsubscription.findOne({email: invite.email}, function(err, unsubscribed){
|
||||
if(err) return cb(err);
|
||||
if(unsubscribed) return cb();
|
||||
|
||||
cb();
|
||||
utils.txnEmail(invite, ('invite-friend' + (group.type == 'guild' ? '-guild' : '')), variables);
|
||||
|
||||
cb();
|
||||
});
|
||||
});
|
||||
}else{
|
||||
cb();
|
||||
@@ -638,7 +698,7 @@ var inviteByEmails = function(invites, group, req, res, next){
|
||||
inviteByUUIDs(usersAlreadyRegistered, group, req, res, next);
|
||||
}else{
|
||||
|
||||
// Send only status code down the line because it doesn't need
|
||||
// Send only status code down the line because it doesn't need
|
||||
// info on invited users since they are not yet registered
|
||||
res.send(200);
|
||||
}
|
||||
@@ -669,8 +729,8 @@ api.removeMember = function(req, res, next){
|
||||
utils.txnEmail(removedUser, ('kicked-from-' + group.type), [
|
||||
{name: 'GROUP_NAME', content: group.name},
|
||||
{name: 'MESSAGE', content: message},
|
||||
{name: 'GUILDS_LINK', content: nconf.get('BASE_URL') + '/#/options/groups/guilds/public'},
|
||||
{name: 'PARTY_WANTED_GUILD', content: nconf.get('BASE_URL') + '/#/options/groups/guilds/f2db2a7f-13c5-454d-b3ee-ea1f5089e601'}
|
||||
{name: 'GUILDS_LINK', content: '/#/options/groups/guilds/public'},
|
||||
{name: 'PARTY_WANTED_GUILD', content: '/#/options/groups/guilds/f2db2a7f-13c5-454d-b3ee-ea1f5089e601'}
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -696,6 +756,11 @@ api.removeMember = function(req, res, next){
|
||||
|
||||
sendMessage(removedUser);
|
||||
|
||||
//Mark removed users messages as seen
|
||||
var update = {$unset:{}};
|
||||
update.$unset['newMessages.' + group._id] = '';
|
||||
User.update({_id: removedUser._id, apiToken: removedUser.apiToken}, update).exec();
|
||||
|
||||
// Sending an empty 204 because Group.update doesn't return the group
|
||||
// see http://mongoosejs.com/docs/api.html#model_Model.update
|
||||
group = uuid = null;
|
||||
@@ -732,8 +797,8 @@ api.removeMember = function(req, res, next){
|
||||
|
||||
});
|
||||
}else{
|
||||
return res.json(400, {err: "User not found among group's members!"});
|
||||
group = uuid = null;
|
||||
return res.json(400, {err: "User not found among group's members!"});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -741,7 +806,7 @@ api.removeMember = function(req, res, next){
|
||||
// Quests
|
||||
// ------------------------------------
|
||||
|
||||
questStart = function(req, res, next) {
|
||||
function questStart(req, res, next) {
|
||||
var group = res.locals.group;
|
||||
var force = req.query.force;
|
||||
|
||||
@@ -778,13 +843,18 @@ questStart = function(req, res, next) {
|
||||
updates['$set']['party.quest.progress.collect'] = collected;
|
||||
updates['$set']['party.quest.completed'] = null;
|
||||
questMembers[m] = true;
|
||||
|
||||
User.findOne({_id: m}, {pushDevices: 1}, function(err, user){
|
||||
pushNotify.sendNotify(user, "HabitRPG", shared.i18n.t('questStarted') + ": "+ quest.text() );
|
||||
});
|
||||
} else {
|
||||
updates['$set']['party.quest'] = Group.cleanQuestProgress();
|
||||
}
|
||||
|
||||
parallel.push(function(cb2){
|
||||
User.update({_id:m},updates,cb2);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
group.quest.active = true;
|
||||
if (quest.boss) {
|
||||
@@ -820,7 +890,7 @@ questStart = function(req, res, next) {
|
||||
});
|
||||
|
||||
utils.txnEmail(usersToEmail, 'quest-started', [
|
||||
{name: 'PARTY_URL', content: nconf.get('BASE_URL') + '/#/options/groups/party'}
|
||||
{name: 'PARTY_URL', content: '/#/options/groups/party'}
|
||||
]);
|
||||
|
||||
_.each(groupClone.members, function(user){
|
||||
@@ -848,7 +918,7 @@ api.questAccept = function(req, res, next) {
|
||||
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 (group.quest.key) return res.json(400, {err: 'Your party is already on a quest. Try again when the current quest has ended.'});
|
||||
if (!user.items.quests[key]) return res.json(400, {err: "You don't own that quest scroll"});
|
||||
group.quest.key = key;
|
||||
group.quest.members = {};
|
||||
@@ -856,9 +926,18 @@ api.questAccept = function(req, res, next) {
|
||||
// or everyone has either accepted/rejected, then we store quest key in user object.
|
||||
_.each(group.members, function(m){
|
||||
if (m == user._id) {
|
||||
var analyticsData = {
|
||||
category: 'behavior',
|
||||
owner: true,
|
||||
response: 'accept',
|
||||
gaLabel: 'accept',
|
||||
questName: key
|
||||
};
|
||||
analytics.track('quest',analyticsData);
|
||||
group.quest.members[m] = true;
|
||||
group.quest.leader = user._id;
|
||||
} else {
|
||||
User.update({_id:m},{$set: {'party.quest.RSVPNeeded': true, 'party.quest.key': group.quest.key}}).exec();
|
||||
group.quest.members[m] = undefined;
|
||||
}
|
||||
});
|
||||
@@ -867,7 +946,7 @@ api.questAccept = function(req, res, next) {
|
||||
_id: {
|
||||
$in: _.without(group.members, user._id)
|
||||
}
|
||||
}, {auth: 1, preferences: 1, profile: 1}, function(err, members){
|
||||
}, {auth: 1, preferences: 1, profile: 1, pushDevices: 1}, function(err, members){
|
||||
if(err) return next(err);
|
||||
|
||||
var inviterVars = utils.getUserInfo(user, ['name', 'email']);
|
||||
@@ -880,16 +959,29 @@ api.questAccept = function(req, res, next) {
|
||||
{name: 'QUEST_NAME', content: quest.text()},
|
||||
{name: 'INVITER', content: inviterVars.name},
|
||||
{name: 'REPLY_TO_ADDRESS', content: inviterVars.email},
|
||||
{name: 'PARTY_URL', content: nconf.get('BASE_URL') + '/#/options/groups/party'}
|
||||
{name: 'PARTY_URL', content: '/#/options/groups/party'}
|
||||
]);
|
||||
|
||||
_.each(members, function(groupMember){
|
||||
pushNotify.sendNotify(groupMember, shared.i18n.t('questInvitationTitle'), shared.i18n.t('questInvitationInfo', { quest: quest.text() }));
|
||||
});
|
||||
|
||||
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.'});
|
||||
var analyticsData = {
|
||||
category: 'behavior',
|
||||
owner: false,
|
||||
response: 'accept',
|
||||
gaLabel: 'accept',
|
||||
questName: group.quest.key
|
||||
};
|
||||
analytics.track('quest',analyticsData);
|
||||
group.quest.members[user._id] = true;
|
||||
User.update({_id:user._id}, {$set: {'party.quest.RSVPNeeded': false}}).exec();
|
||||
questStart(req,res,next);
|
||||
}
|
||||
}
|
||||
@@ -899,7 +991,16 @@ api.questReject = function(req, res, next) {
|
||||
var user = res.locals.user;
|
||||
|
||||
if (!group.quest.key) return res.json(400,{err:'No quest invitation has been sent out yet.'});
|
||||
var analyticsData = {
|
||||
category: 'behavior',
|
||||
owner: false,
|
||||
response: 'reject',
|
||||
gaLabel: 'reject',
|
||||
questName: group.quest.key
|
||||
};
|
||||
analytics.track('quest',analyticsData);
|
||||
group.quest.members[user._id] = false;
|
||||
User.update({_id:user._id}, {$set: {'party.quest.RSVPNeeded': false, 'party.quest.key': null}}).exec();
|
||||
questStart(req,res,next);
|
||||
}
|
||||
|
||||
@@ -917,6 +1018,9 @@ api.questCancel = function(req, res, next){
|
||||
group.quest = {key:null,progress:{},leader:null};
|
||||
group.markModified('quest');
|
||||
group.save(cb);
|
||||
_.each(group.members, function(m){
|
||||
User.update({_id:m}, {$set: {'party.quest.RSVPNeeded': false, 'party.quest.key': null}}).exec();
|
||||
});
|
||||
}
|
||||
}
|
||||
], function(err){
|
||||
|
||||
@@ -7,6 +7,7 @@ var _ = require('lodash');
|
||||
var shared = require('../../../common');
|
||||
var utils = require('../utils');
|
||||
var nconf = require('nconf');
|
||||
var pushNotify = require('./pushNotifications');
|
||||
|
||||
var fetchMember = function(uuid, restrict){
|
||||
return function(cb){
|
||||
@@ -72,7 +73,7 @@ api.sendPrivateMessage = function(req, 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'}
|
||||
{name: 'PMS_INBOX_URL', content: '/#/options/groups/inbox'}
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -96,12 +97,18 @@ api.sendGift = function(req, res, next){
|
||||
member.balance += amt;
|
||||
user.balance -= amt;
|
||||
api.sendMessage(user, member, req.body);
|
||||
|
||||
var byUsername = utils.getUserInfo(user, ['name']).name;
|
||||
|
||||
if(member.preferences.emailNotifications.giftedGems !== false){
|
||||
utils.txnEmail(member, 'gifted-gems', [
|
||||
{name: 'GIFTER', content: utils.getUserInfo(user, ['name']).name},
|
||||
{name: 'GIFTER', content: byUsername},
|
||||
{name: 'X_GEMS_GIFTED', content: req.body.gems.amount}
|
||||
]);
|
||||
}
|
||||
|
||||
pushNotify.sendNotify(member, shared.i18n.t('giftedGems'), shared.i18n.t('giftedGemsInfo', { amount: req.body.gems.amount, name: byUsername }));
|
||||
|
||||
return async.parallel([
|
||||
function (cb2) { member.save(cb2) },
|
||||
function (cb2) { user.save(cb2) }
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
var amazonPayments = require('amazon-payments');
|
||||
var mongoose = require('mongoose');
|
||||
var moment = require('moment');
|
||||
var nconf = require('nconf');
|
||||
var async = require('async');
|
||||
var User = require('mongoose').model('User');
|
||||
var shared = require('../../../../common');
|
||||
var payments = require('./index');
|
||||
var cc = require('coupon-code');
|
||||
var isProd = nconf.get("NODE_ENV") === 'production';
|
||||
|
||||
var amzPayment = amazonPayments.connect({
|
||||
environment: amazonPayments.Environment[isProd ? 'Production' : 'Sandbox'],
|
||||
sellerId: nconf.get('AMAZON_PAYMENTS:SELLER_ID'),
|
||||
mwsAccessKey: nconf.get('AMAZON_PAYMENTS:MWS_KEY'),
|
||||
mwsSecretKey: nconf.get('AMAZON_PAYMENTS:MWS_SECRET'),
|
||||
clientId: nconf.get('AMAZON_PAYMENTS:CLIENT_ID')
|
||||
});
|
||||
|
||||
exports.verifyAccessToken = function(req, res, next){
|
||||
if(!req.body || !req.body['access_token']){
|
||||
return res.json(400, {err: 'Access token not supplied.'});
|
||||
}
|
||||
|
||||
amzPayment.api.getTokenInfo(req.body['access_token'], function(err, tokenInfo){
|
||||
if(err) return res.json(400, {err:err});
|
||||
|
||||
res.send(200);
|
||||
});
|
||||
};
|
||||
|
||||
exports.createOrderReferenceId = function(req, res, next){
|
||||
if(!req.body || !req.body.billingAgreementId){
|
||||
return res.json(400, {err: 'Billing Agreement Id not supplied.'});
|
||||
}
|
||||
|
||||
amzPayment.offAmazonPayments.createOrderReferenceForId({
|
||||
Id: req.body.billingAgreementId,
|
||||
IdType: 'BillingAgreement',
|
||||
ConfirmNow: false
|
||||
}, function(err, response){
|
||||
if(err) return next(err);
|
||||
if(!response.OrderReferenceDetails || !response.OrderReferenceDetails.AmazonOrderReferenceId){
|
||||
return next(new Error('Missing attributes in Amazon response.'));
|
||||
}
|
||||
|
||||
res.json({
|
||||
orderReferenceId: response.OrderReferenceDetails.AmazonOrderReferenceId
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.checkout = function(req, res, next){
|
||||
if(!req.body || !req.body.orderReferenceId){
|
||||
return res.json(400, {err: 'Billing Agreement Id not supplied.'});
|
||||
}
|
||||
|
||||
var gift = req.body.gift;
|
||||
var user = res.locals.user;
|
||||
var orderReferenceId = req.body.orderReferenceId;
|
||||
var amount = 5;
|
||||
|
||||
if(gift){
|
||||
if(gift.type === 'gems'){
|
||||
amount = gift.gems.amount/4;
|
||||
}else if(gift.type === 'subscription'){
|
||||
amount = shared.content.subscriptionBlocks[gift.subscription.key].price;
|
||||
}
|
||||
}
|
||||
|
||||
async.series({
|
||||
setOrderReferenceDetails: function(cb){
|
||||
amzPayment.offAmazonPayments.setOrderReferenceDetails({
|
||||
AmazonOrderReferenceId: orderReferenceId,
|
||||
OrderReferenceAttributes: {
|
||||
OrderTotal: {
|
||||
CurrencyCode: 'USD',
|
||||
Amount: amount
|
||||
},
|
||||
SellerNote: 'HabitRPG Payment',
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: shared.uuid(),
|
||||
StoreName: 'HabitRPG'
|
||||
}
|
||||
}
|
||||
}, cb);
|
||||
},
|
||||
|
||||
confirmOrderReference: function(cb){
|
||||
amzPayment.offAmazonPayments.confirmOrderReference({
|
||||
AmazonOrderReferenceId: orderReferenceId
|
||||
}, cb);
|
||||
},
|
||||
|
||||
authorize: function(cb){
|
||||
amzPayment.offAmazonPayments.authorize({
|
||||
AmazonOrderReferenceId: orderReferenceId,
|
||||
AuthorizationReferenceId: shared.uuid().substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: 'USD',
|
||||
Amount: amount
|
||||
},
|
||||
SellerAuthorizationNote: 'HabitRPG Payment',
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true
|
||||
}, function(err, res){
|
||||
if(err) return cb(err);
|
||||
|
||||
if(res.AuthorizationDetails.AuthorizationStatus.State === 'Declined'){
|
||||
return cb(new Error('The payment was not successfull.'));
|
||||
}
|
||||
|
||||
return cb();
|
||||
});
|
||||
},
|
||||
|
||||
closeOrderReference: function(cb){
|
||||
amzPayment.offAmazonPayments.closeOrderReference({
|
||||
AmazonOrderReferenceId: orderReferenceId
|
||||
}, cb);
|
||||
},
|
||||
|
||||
executePayment: function(cb){
|
||||
async.waterfall([
|
||||
function(cb2){ User.findById(gift ? gift.uuid : undefined, cb2) },
|
||||
function(member, cb2){
|
||||
var data = {user:user, paymentMethod:'Amazon Payments'};
|
||||
var method = 'buyGems';
|
||||
|
||||
if (gift){
|
||||
if (gift.type == 'subscription') method = 'createSubscription';
|
||||
gift.member = member;
|
||||
data.gift = gift;
|
||||
data.paymentMethod = 'Gift';
|
||||
}
|
||||
|
||||
payments[method](data, cb2);
|
||||
}
|
||||
], cb);
|
||||
}
|
||||
}, function(err, results){
|
||||
if(err) return next(err);
|
||||
|
||||
res.send(200);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
exports.subscribe = function(req, res, next){
|
||||
if(!req.body || !req.body['billingAgreementId']){
|
||||
return res.json(400, {err: 'Billing Agreement Id not supplied.'});
|
||||
}
|
||||
|
||||
var billingAgreementId = req.body.billingAgreementId
|
||||
var sub = req.body.subscription ? shared.content.subscriptionBlocks[req.body.subscription] : false;
|
||||
var coupon = req.body.coupon;
|
||||
var user = res.locals.user;
|
||||
|
||||
if(!sub){
|
||||
return res.json(400, {err: 'Subscription plan not found.'});
|
||||
}
|
||||
|
||||
async.series({
|
||||
applyDiscount: function(cb){
|
||||
if (!sub.discount) return cb();
|
||||
if (!coupon) return cb(new Error('Please provide a coupon code for this plan.'));
|
||||
mongoose.model('Coupon').findOne({_id:cc.validate(coupon), event:sub.key}, function(err, coupon){
|
||||
if(err) return cb(err);
|
||||
if(!coupon) return cb(new Error('Coupon code not found.'));
|
||||
cb()
|
||||
});
|
||||
},
|
||||
|
||||
setBillingAgreementDetails: function(cb){
|
||||
amzPayment.offAmazonPayments.setBillingAgreementDetails({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
BillingAgreementAttributes: {
|
||||
SellerNote: 'HabitRPG Subscription',
|
||||
SellerBillingAgreementAttributes: {
|
||||
SellerBillingAgreementId: shared.uuid(),
|
||||
StoreName: 'HabitRPG',
|
||||
CustomInformation: 'HabitRPG Subscription'
|
||||
}
|
||||
}
|
||||
}, cb);
|
||||
},
|
||||
|
||||
confirmBillingAgreement: function(cb){
|
||||
amzPayment.offAmazonPayments.confirmBillingAgreement({
|
||||
AmazonBillingAgreementId: billingAgreementId
|
||||
}, cb);
|
||||
},
|
||||
|
||||
authorizeOnBillingAgreeement: function(cb){
|
||||
amzPayment.offAmazonPayments.authorizeOnBillingAgreement({
|
||||
AmazonBillingAgreementId: billingAgreementId,
|
||||
AuthorizationReferenceId: shared.uuid().substring(0, 32),
|
||||
AuthorizationAmount: {
|
||||
CurrencyCode: 'USD',
|
||||
Amount: sub.price
|
||||
},
|
||||
SellerAuthorizationNote: 'HabitRPG Subscription Payment',
|
||||
TransactionTimeout: 0,
|
||||
CaptureNow: true,
|
||||
SellerNote: 'HabitRPG Subscription Payment',
|
||||
SellerOrderAttributes: {
|
||||
SellerOrderId: shared.uuid(),
|
||||
StoreName: 'HabitRPG'
|
||||
}
|
||||
}, function(err, res){
|
||||
if(err) return cb(err);
|
||||
|
||||
if(res.AuthorizationDetails.AuthorizationStatus.State === 'Declined'){
|
||||
return cb(new Error('The payment was not successfull.'));
|
||||
}
|
||||
|
||||
return cb();
|
||||
});
|
||||
},
|
||||
|
||||
createSubscription: function(cb){
|
||||
payments.createSubscription({
|
||||
user: user,
|
||||
customerId: billingAgreementId,
|
||||
paymentMethod: 'Amazon Payments',
|
||||
sub: sub
|
||||
}, cb);
|
||||
}
|
||||
}, function(err, results){
|
||||
if(err) return next(err);
|
||||
|
||||
res.send(200);
|
||||
});
|
||||
};
|
||||
|
||||
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"});
|
||||
|
||||
var billingAgreementId = user.purchased.plan.customerId;
|
||||
|
||||
async.series({
|
||||
closeBillingAgreement: function(cb){
|
||||
amzPayment.offAmazonPayments.closeBillingAgreement({
|
||||
AmazonBillingAgreementId: billingAgreementId
|
||||
}, cb);
|
||||
},
|
||||
|
||||
cancelSubscription: function(cb){
|
||||
var data = {
|
||||
user: user,
|
||||
// Date of next bill
|
||||
nextBill: moment(user.purchased.plan.lastBillingDate).add({days: 30}),
|
||||
paymentMethod: 'Amazon Payments'
|
||||
};
|
||||
|
||||
payments.cancelSubscription(data, cb);
|
||||
}
|
||||
}, function(err, results){
|
||||
if (err) return next(err); // don't json this, let toString() handle errors
|
||||
|
||||
if(req.query.noRedirect){
|
||||
res.send(200);
|
||||
}else{
|
||||
res.redirect('/');
|
||||
}
|
||||
|
||||
user = null;
|
||||
});
|
||||
};
|
||||
@@ -6,7 +6,7 @@ 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")
|
||||
googlePublicKeyPath: nconf.get("IAP_GOOGLE_KEYDIR")
|
||||
});
|
||||
|
||||
// Validation ERROR Codes
|
||||
@@ -24,15 +24,11 @@ exports.androidVerify = function(req, res, next) {
|
||||
ok: false,
|
||||
data: 'IAP Error'
|
||||
};
|
||||
|
||||
console.error('IAP Setup ERROR');
|
||||
console.error(error);
|
||||
|
||||
res.json(resObj);
|
||||
|
||||
return;
|
||||
|
||||
return res.json(resObj);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
google receipt must be provided as an object
|
||||
{
|
||||
@@ -44,7 +40,7 @@ exports.androidVerify = function(req, res, next) {
|
||||
data: iapBody.transaction.receipt,
|
||||
signature: iapBody.transaction.signature
|
||||
};
|
||||
|
||||
|
||||
// iap is ready
|
||||
iap.validate(iap.GOOGLE, testObj, function (err, googleRes) {
|
||||
if (err) {
|
||||
@@ -56,9 +52,7 @@ exports.androidVerify = function(req, res, next) {
|
||||
}
|
||||
};
|
||||
|
||||
res.json(resObj);
|
||||
console.error(err);
|
||||
return;
|
||||
return res.json(resObj);
|
||||
}
|
||||
|
||||
if (iap.isValidated(googleRes)) {
|
||||
@@ -69,16 +63,13 @@ exports.androidVerify = function(req, res, next) {
|
||||
|
||||
payments.buyGems({user:user, paymentMethod:'IAP GooglePlay'});
|
||||
|
||||
// yay good!
|
||||
res.json(resObj);
|
||||
return res.json(resObj);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
exports.iosVerify = function(req, res, next) {
|
||||
console.info(req.body);
|
||||
|
||||
var iapBody = req.body;
|
||||
var user = res.locals.user;
|
||||
|
||||
@@ -89,15 +80,11 @@ exports.iosVerify = function(req, res, next) {
|
||||
data: 'IAP Error'
|
||||
};
|
||||
|
||||
console.error('IAP Setup ERROR');
|
||||
console.error(error);
|
||||
return res.json(resObj);
|
||||
|
||||
res.json(resObj);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// iap is ready
|
||||
|
||||
//iap is ready
|
||||
iap.validate(iap.APPLE, iapBody.transaction.receipt, function (err, appleRes) {
|
||||
if (err) {
|
||||
var resObj = {
|
||||
@@ -108,22 +95,43 @@ exports.iosVerify = function(req, res, next) {
|
||||
}
|
||||
};
|
||||
|
||||
res.json(resObj);
|
||||
console.error(err);
|
||||
return;
|
||||
return res.json(resObj);
|
||||
}
|
||||
|
||||
if (iap.isValidated(appleRes)) {
|
||||
var purchaseDataList = iap.getPurchaseData(appleRes);
|
||||
if (purchaseDataList.length > 0) {
|
||||
if (purchaseDataList[0].productId === "com.habitrpg.ios.Habitica.20gems") {
|
||||
//Correct receipt
|
||||
payments.buyGems({user:user, paymentMethod:'IAP AppleStore'});
|
||||
var resObj = {
|
||||
ok: true,
|
||||
data: appleRes
|
||||
};
|
||||
// yay good!
|
||||
return res.json(resObj);
|
||||
}
|
||||
}
|
||||
//wrong receipt content
|
||||
var resObj = {
|
||||
ok: true,
|
||||
data: appleRes
|
||||
ok: false,
|
||||
data: {
|
||||
code: INVALID_PAYLOAD,
|
||||
message: "Incorrect receipt content"
|
||||
}
|
||||
};
|
||||
|
||||
payments.buyGems({user:user, paymentMethod:'IAP AppleStore'});
|
||||
|
||||
// yay good!
|
||||
res.json(resObj);
|
||||
return res.json(resObj);
|
||||
}
|
||||
//invalid receipt
|
||||
var resObj = {
|
||||
ok: false,
|
||||
data: {
|
||||
code: INVALID_PAYLOAD,
|
||||
message: "Invalid receipt"
|
||||
}
|
||||
};
|
||||
|
||||
return res.json(resObj);
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,11 +7,13 @@ var moment = require('moment');
|
||||
var isProduction = nconf.get("NODE_ENV") === "production";
|
||||
var stripe = require('./stripe');
|
||||
var paypal = require('./paypal');
|
||||
var amazon = require('./amazon');
|
||||
var members = require('../members')
|
||||
var async = require('async');
|
||||
var iap = require('./iap');
|
||||
var mongoose= require('mongoose');
|
||||
var cc = require('coupon-code');
|
||||
var pushNotify = require('./../pushNotifications');
|
||||
|
||||
function revealMysteryItems(user) {
|
||||
_.each(shared.content.gear.flat, function(item) {
|
||||
@@ -51,7 +53,10 @@ exports.createSubscription = function(data, cb) {
|
||||
paymentMethod: data.paymentMethod,
|
||||
extraMonths: +p.extraMonths
|
||||
+ +(p.dateTerminated ? moment(p.dateTerminated).diff(new Date(),'months',true) : 0),
|
||||
dateTerminated: null
|
||||
dateTerminated: null,
|
||||
// Specify a lastBillingDate just for Amazon Payments
|
||||
// Resetted every time the subscription restarts
|
||||
lastBillingDate: data.paymentMethod === 'Amazon Payments' ? new Date() : undefined
|
||||
}).defaults({ // allow non-override if a plan was previously used
|
||||
dateCreated: new Date(),
|
||||
mysteryItems: []
|
||||
@@ -69,18 +74,35 @@ exports.createSubscription = function(data, cb) {
|
||||
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();
|
||||
|
||||
var analyticsData = {
|
||||
uuid: data.user._id,
|
||||
itemPurchased: 'Subscription',
|
||||
sku: data.paymentMethod.toLowerCase() + '-subscription',
|
||||
purchaseType: 'subscribe',
|
||||
paymentMethod: data.paymentMethod,
|
||||
quantity: 1,
|
||||
gift: !!data.gift, // coerced into a boolean
|
||||
purchaseValue: block.price
|
||||
}
|
||||
utils.analytics.trackPurchase(analyticsData);
|
||||
}
|
||||
data.user.purchased.txnCount++;
|
||||
if (data.gift){
|
||||
members.sendMessage(data.user, data.gift.member, data.gift);
|
||||
|
||||
var byUserName = utils.getUserInfo(data.user, ['name']).name;
|
||||
|
||||
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: 'GIFTER', content: byUserName},
|
||||
{name: 'X_MONTHS_SUBSCRIPTION', content: months}
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (data.gift.member._id != data.user._id) { // Only send push notifications if sending to a user other than yourself
|
||||
pushNotify.sendNotify(data.gift.member, shared.i18n.t('giftedSubscription'), months + " months - by "+ byUserName);
|
||||
}
|
||||
}
|
||||
async.parallel([
|
||||
function(cb2){data.user.save(cb2)},
|
||||
@@ -105,7 +127,13 @@ exports.cancelSubscription = function(data, cb) {
|
||||
|
||||
data.user.save(cb);
|
||||
utils.txnEmail(data.user, 'cancel-subscription');
|
||||
utils.ga.event('unsubscribe', data.paymentMethod).send();
|
||||
var analyticsData = {
|
||||
uuid: data.user._id,
|
||||
gaCategory: 'commerce',
|
||||
gaLabel: data.paymentMethod,
|
||||
paymentMethod: data.paymentMethod
|
||||
}
|
||||
utils.analytics.track('unsubscribe', analyticsData);
|
||||
}
|
||||
|
||||
exports.buyGems = function(data, cb) {
|
||||
@@ -114,18 +142,35 @@ exports.buyGems = function(data, cb) {
|
||||
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();
|
||||
|
||||
var analyticsData = {
|
||||
uuid: data.user._id,
|
||||
itemPurchased: 'Gems',
|
||||
sku: data.paymentMethod.toLowerCase() + '-checkout',
|
||||
purchaseType: 'checkout',
|
||||
paymentMethod: data.paymentMethod,
|
||||
quantity: 1,
|
||||
gift: !!data.gift, // coerced into a boolean
|
||||
purchaseValue: amt
|
||||
}
|
||||
utils.analytics.trackPurchase(analyticsData);
|
||||
}
|
||||
|
||||
if (data.gift){
|
||||
var byUsername = utils.getUserInfo(data.user, ['name']).name;
|
||||
var gemAmount = data.gift.gems.amount || 20;
|
||||
|
||||
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}
|
||||
{name: 'GIFTER', content: byUsername},
|
||||
{name: 'X_GEMS_GIFTED', content: gemAmount}
|
||||
]);
|
||||
}
|
||||
|
||||
if (data.gift.member._id != data.user._id) { // Only send push notifications if sending to a user other than yourself
|
||||
pushNotify.sendNotify(data.gift.member, shared.i18n.t('giftedGems'), gemAmount + ' Gems - by '+byUsername);
|
||||
}
|
||||
}
|
||||
async.parallel([
|
||||
function(cb2){data.user.save(cb2)},
|
||||
@@ -152,5 +197,11 @@ exports.paypalCheckout = paypal.createPayment;
|
||||
exports.paypalCheckoutSuccess = paypal.executePayment;
|
||||
exports.paypalIPN = paypal.ipn;
|
||||
|
||||
exports.amazonVerifyAccessToken = amazon.verifyAccessToken;
|
||||
exports.amazonCreateOrderReferenceId = amazon.createOrderReferenceId;
|
||||
exports.amazonCheckout = amazon.checkout;
|
||||
exports.amazonSubscribe = amazon.subscribe;
|
||||
exports.amazonSubscribeCancel = amazon.subscribeCancel;
|
||||
|
||||
exports.iapAndroidVerify = iap.androidVerify;
|
||||
exports.iapIosVerify = iap.iosVerify;
|
||||
exports.iapIosVerify = iap.iosVerify;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
var api = module.exports;
|
||||
var _ = require('lodash');
|
||||
var nconf = require('nconf');
|
||||
|
||||
var pushNotify = require('push-notify');
|
||||
|
||||
var gcmApiKey = nconf.get("PUSH_CONFIGS:GCM_SERVER_API_KEY");
|
||||
|
||||
var gcm = gcmApiKey ? pushNotify.gcm({
|
||||
apiKey: gcmApiKey,
|
||||
retries: 3
|
||||
}) : undefined;
|
||||
|
||||
if(gcm){
|
||||
gcm.on('transmitted', function (result, message, registrationId) {
|
||||
console.info("transmitted", result, message, registrationId);
|
||||
});
|
||||
|
||||
gcm.on('transmissionError', function (error, message, registrationId) {
|
||||
console.info("transmissionError", error, message, registrationId);
|
||||
});
|
||||
gcm.on('updated', function (result, registrationId) {
|
||||
console.info("updated", result, registrationId);
|
||||
});
|
||||
}
|
||||
|
||||
api.sendNotify = function(user, title, msg, timeToLive){
|
||||
timeToLive = timeToLive || 15;
|
||||
|
||||
// need investigation:
|
||||
// https://github.com/HabitRPG/habitrpg/issues/5252
|
||||
if(!user)
|
||||
return;
|
||||
|
||||
_.forEach(user.pushDevices, function(pushDevice){
|
||||
switch(pushDevice.type){
|
||||
case "android":
|
||||
if(gcm){
|
||||
gcm.send({
|
||||
registrationId: pushDevice.regId,
|
||||
//collapseKey: 'COLLAPSE_KEY',
|
||||
delayWhileIdle: true,
|
||||
timeToLive: timeToLive,
|
||||
data: {
|
||||
title: title,
|
||||
message: msg
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "ios":
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
var User = require('../models/user').model;
|
||||
var EmailUnsubscription = require('../models/emailUnsubscription').model;
|
||||
var utils = require('../utils');
|
||||
var i18n = require('../../../common').i18n;
|
||||
|
||||
var api = module.exports = {};
|
||||
|
||||
api.unsubscribe = function(req, res, next){
|
||||
if(!req.query.code) return res.json(500, {err: 'Missing unsubscription code.'});
|
||||
|
||||
var data = JSON.parse(utils.decrypt(req.query.code));
|
||||
|
||||
if(data._id){
|
||||
User.update({_id: data._id}, {
|
||||
$set: {'preferences.emailNotifications.unsubscribeFromAll': true}
|
||||
}, {multi: false}, function(err, nAffected){
|
||||
if(err) return next(err);
|
||||
if(nAffected !== 1) return res.json(404, {err: 'User not found'});
|
||||
|
||||
res.send('<h1>' + i18n.t('unsubscribedSuccessfully', null, req.language) + '</h1>' + i18n.t('unsubscribedTextUsers', null, req.language));
|
||||
});
|
||||
}else{
|
||||
EmailUnsubscription.findOne({email: data.email}, function(err, doc){
|
||||
if(err) return next(err);
|
||||
var okRes = '<h1>' + i18n.t('unsubscribedSuccessfully', null, req.language) + '</h1>' + i18n.t('unsubscribedTextOthers', null, req.language);
|
||||
|
||||
if(doc) return res.send(okRes);
|
||||
|
||||
EmailUnsubscription.create({email: data.email}, function(err, doc){
|
||||
if(err) return next(err);
|
||||
|
||||
res.send(okRes);
|
||||
})
|
||||
});
|
||||
}
|
||||
};
|
||||
+124
-31
@@ -8,7 +8,7 @@ var async = require('async');
|
||||
var shared = require('../../../common');
|
||||
var User = require('./../models/user').model;
|
||||
var utils = require('./../utils');
|
||||
var ga = utils.ga;
|
||||
var analytics = utils.analytics;
|
||||
var Group = require('./../models/group').model;
|
||||
var Challenge = require('./../models/challenge').model;
|
||||
var moment = require('moment');
|
||||
@@ -16,22 +16,21 @@ var logging = require('./../logging');
|
||||
var acceptablePUTPaths;
|
||||
var api = module.exports;
|
||||
var qs = require('qs');
|
||||
var request = require('request');
|
||||
var validator = require('validator');
|
||||
var webhook = require('../webhook');
|
||||
|
||||
// api.purchase // Shared.ops
|
||||
|
||||
api.getContent = function(req, res, next) {
|
||||
var language = 'en';
|
||||
|
||||
if(typeof req.query.language != 'undefined')
|
||||
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);
|
||||
if (_.isPlainObject(item) || _.isArray(item)) return walk(item, lang);
|
||||
if (_.isFunction(item) && item.i18nLangFunc) source[key] = item(lang);
|
||||
});
|
||||
}
|
||||
walk(content, language);
|
||||
@@ -99,36 +98,32 @@ api.score = function(req, res, next) {
|
||||
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';
|
||||
|
||||
task = user.ops.addTask({body:task});
|
||||
}
|
||||
var delta = user.ops.score({params:{id:task.id, direction:direction}, language: req.language});
|
||||
|
||||
user.save(function(err,saved){
|
||||
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
|
||||
});
|
||||
});
|
||||
var userStats = saved.toJSON().stats;
|
||||
var resJsonData = _.extend({ delta: delta, _tmp: user._tmp }, userStats);
|
||||
res.json(200, resJsonData);
|
||||
|
||||
var webhookData = _generateWebhookTaskData(
|
||||
task, direction, delta, userStats, user
|
||||
);
|
||||
webhook.sendTaskWebhook(user.preferences.webhooks, webhookData);
|
||||
|
||||
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){
|
||||
|
||||
Challenge.findById(task.challenge.id, 'habits dailys todos rewards', function(err, chal) {
|
||||
if (err) return next(err);
|
||||
if (!chal) {
|
||||
task.challenge.broken = 'CHALLENGE_DELETED';
|
||||
@@ -141,6 +136,7 @@ api.score = function(req, res, next) {
|
||||
chal.syncToUser(user);
|
||||
return clearMemory();
|
||||
}
|
||||
|
||||
t.value += delta;
|
||||
if (t.type == 'habit' || t.type == 'daily')
|
||||
t.history.push({value: t.value, date: +new Date});
|
||||
@@ -205,7 +201,7 @@ api.getBuyList = function (req, res, next) {
|
||||
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.maxHealth = shared.maxHealth;
|
||||
user.stats.maxMP = res.locals.user._statsComputed.maxMP;
|
||||
delete user.apiToken;
|
||||
if (user.auth && user.auth.local) {
|
||||
@@ -215,6 +211,78 @@ api.getUser = function(req, res, next) {
|
||||
return res.json(200, user);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get anonymized User
|
||||
*/
|
||||
api.getUserAnonymized = function(req, res, next) {
|
||||
var user = res.locals.user.toJSON();
|
||||
user.stats.toNextLevel = shared.tnl(user.stats.lvl);
|
||||
user.stats.maxHealth = shared.maxHealth;
|
||||
user.stats.maxMP = res.locals.user._statsComputed.maxMP;
|
||||
|
||||
delete user.apiToken;
|
||||
|
||||
if (user.auth) {
|
||||
delete user.auth.local;
|
||||
delete user.auth.facebook;
|
||||
}
|
||||
|
||||
delete user.newMessages;
|
||||
|
||||
delete user.profile;
|
||||
delete user.purchased.plan;
|
||||
delete user.contributor;
|
||||
delete user.invitations;
|
||||
|
||||
delete user.items.special.nyeReceived;
|
||||
delete user.items.special.valentineReceived;
|
||||
|
||||
delete user.webhooks;
|
||||
delete user.achievements.challenges;
|
||||
|
||||
_.forEach(user.inbox.messages, function(msg){
|
||||
msg.text = "inbox message text";
|
||||
});
|
||||
|
||||
_.forEach(user.tags, function(tag){
|
||||
tag.name = "tag";
|
||||
tag.challenge = "challenge";
|
||||
});
|
||||
|
||||
function cleanChecklist(task){
|
||||
var checklistIndex = 0;
|
||||
|
||||
_.forEach(task.checklist, function(c){
|
||||
c.text = "item" + checklistIndex++;
|
||||
});
|
||||
}
|
||||
|
||||
_.forEach(user.habits, function(task){
|
||||
task.text = "task text";
|
||||
task.notes = "task notes";
|
||||
});
|
||||
|
||||
_.forEach(user.rewards, function(task){
|
||||
task.text = "task text";
|
||||
task.notes = "task notes";
|
||||
});
|
||||
|
||||
_.forEach(user.dailys, function(task){
|
||||
task.text = "task text";
|
||||
task.notes = "task notes";
|
||||
|
||||
cleanChecklist(task);
|
||||
});
|
||||
|
||||
_.forEach(user.todos, function(task){
|
||||
task.text = "task text";
|
||||
task.notes = "task notes";
|
||||
|
||||
cleanChecklist(task);
|
||||
});
|
||||
|
||||
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).
|
||||
@@ -262,7 +330,7 @@ api.update = function(req, res, next) {
|
||||
|
||||
api.cron = function(req, res, next) {
|
||||
var user = res.locals.user,
|
||||
progress = user.fns.cron({ga:ga}),
|
||||
progress = user.fns.cron({analytics:utils.analytics}),
|
||||
ranCron = user.isModified(),
|
||||
quest = shared.content.quests[user.party.quest.key];
|
||||
|
||||
@@ -426,9 +494,9 @@ api.sessionPartyInvite = function(req,res,next){
|
||||
return cb();
|
||||
}
|
||||
|
||||
if(group.type == 'guild'){
|
||||
if (group.type == 'guild'){
|
||||
inv.guilds.push(req.session.partyInvite);
|
||||
}else{
|
||||
} else{
|
||||
//req.body.type in 'guild', 'party'
|
||||
inv.party = req.session.partyInvite;
|
||||
}
|
||||
@@ -461,7 +529,7 @@ _.each(shared.wrap({}).ops, function(op,k){
|
||||
if (err) return next(err);
|
||||
res.json(200,response);
|
||||
})
|
||||
}, ga);
|
||||
}, analytics);
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -525,7 +593,7 @@ api.batchUpdate = function(req, res, next) {
|
||||
res.json(200, {_tmp: {drop: response._tmp.drop}, _v: response._v});
|
||||
|
||||
// Fetch full user object
|
||||
}else if(response.wasModified){
|
||||
} 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}));
|
||||
@@ -533,8 +601,33 @@ api.batchUpdate = function(req, res, next) {
|
||||
res.json(200, response);
|
||||
|
||||
// return only the version number
|
||||
}else{
|
||||
} else{
|
||||
res.json(200, {_v: response._v});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function _generateWebhookTaskData(task, direction, delta, stats, user) {
|
||||
var extendedStats = _.extend(stats, {
|
||||
toNextLevel: shared.tnl(user.stats.lvl),
|
||||
maxHealth: shared.maxHealth,
|
||||
maxMP: user._statsComputed.maxMP
|
||||
});
|
||||
|
||||
var userData = {
|
||||
_id: user._id,
|
||||
_tmp: user._tmp,
|
||||
stats: extendedStats
|
||||
};
|
||||
|
||||
var taskData = {
|
||||
details: task,
|
||||
direction: direction,
|
||||
delta: delta
|
||||
}
|
||||
|
||||
return {
|
||||
task: taskData,
|
||||
user: userData
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user