Merge branch 'develop' into 3onyc-fix-hash-leak

This commit is contained in:
Blade Barringer
2015-08-22 17:45:47 -05:00
1848 changed files with 63651 additions and 41605 deletions
+42 -16
View File
@@ -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);
+20 -6
View File
@@ -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){
+2 -2
View File
@@ -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)
+155 -51
View File
@@ -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){
+9 -2
View File
@@ -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) }
+271
View File
@@ -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;
});
};
+43 -35
View File
@@ -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);
});
});
};
};
+63 -12
View File
@@ -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;
}
});
};
+36
View File
@@ -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
View File
@@ -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
}
}