Merge branch 'develop' into pushNotifications2

This commit is contained in:
Negue
2015-04-27 17:20:25 +02:00
910 changed files with 28551 additions and 32361 deletions
+19 -6
View File
@@ -7,6 +7,7 @@ var utils = require('../utils');
var nconf = require('nconf');
var request = require('request');
var User = require('../models/user').model;
var EmailUnsubscription = require('../models/emailUnsubscription').model;
var ga = require('./../utils').ga;
var i18n = require('./../i18n');
@@ -109,9 +110,14 @@ 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);
ga.event('acquisition', 'register', 'local').send();
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) {
@@ -178,10 +184,17 @@ 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();
ga.event('acquisition', 'register', network).send();
}]
}, function(err, results){
if (err) return res.json(401, {err: err.toString ? err.toString() : err});
+8 -2
View File
@@ -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) {
+33 -23
View File
@@ -12,6 +12,7 @@ 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');
@@ -31,7 +32,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){
@@ -237,24 +238,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);
return 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){
@@ -306,7 +311,7 @@ api.flagChatMessage = function(req, res, next){
group.save(function(err,_saved){
if(err) return next(err);
var addressesToSendTo = JSON.parse(nconf.get('FLAG_REPORT_EMAIL'));
if(Array.isArray(addressesToSendTo)){
addressesToSendTo = addressesToSendTo.map(function(email){
return {email: email, canSend: true}
@@ -359,7 +364,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){
@@ -582,7 +587,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);
@@ -628,10 +633,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();
@@ -643,7 +653,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);
}
+3 -3
View File
@@ -70,7 +70,7 @@ 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.event('commerce', 'subscribe', data.paymentMethod, block.price).send();
utils.ga.transaction(data.user._id, block.price).item(block.price, 1, data.paymentMethod.toLowerCase() + '-subscription', data.paymentMethod).send();
}
data.user.purchased.txnCount++;
@@ -111,7 +111,7 @@ exports.cancelSubscription = function(data, cb) {
data.user.save(cb);
utils.txnEmail(data.user, 'cancel-subscription');
utils.ga.event('unsubscribe', data.paymentMethod).send();
utils.ga.event('commerce', 'unsubscribe', data.paymentMethod).send();
}
exports.buyGems = function(data, cb) {
@@ -120,7 +120,7 @@ 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();
utils.ga.event('commerce', 'checkout', data.paymentMethod, amt).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();
}
+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);
})
});
}
};
+23 -6
View File
@@ -63,15 +63,28 @@ _.each(langCodes, function(code){
// used in place of plain original 'en'
var defaultLangCodes = _.without(langCodes, 'en_GB');
// A list of languages that have different versions
var multipleVersionsLanguages = ['es', 'zh'];
var latinAmericanSpanishes = ['es-419', 'es-mx', 'es-gt', 'es-cr', 'es-pa', 'es-do', 'es-ve', 'es-co', 'es-pe',
'es-ar', 'es-ec', 'es-cl', 'es-uy', 'es-py', 'es-bo', 'es-sv', 'es-hn',
'es-ni', 'es-pr'];
var chineseVersions = ['zh-tw'];
var getUserLanguage = function(req, res, next){
var getFromBrowser = function(){
var acceptable = _(req.acceptedLanguages).map(function(lang){
return lang.slice(0, 2);
}).uniq().value();
var matches = _.intersection(acceptable, defaultLangCodes);
if(matches.length > 0 && matches[0].toLowerCase() === 'es'){
var iAcceptedCompleteLang = (matches.length > 0) ? multipleVersionsLanguages.indexOf(matches[0].toLowerCase()) : -1;
if(iAcceptedCompleteLang !== -1){
var acceptedCompleteLang = _.find(req.acceptedLanguages, function(accepted){
return accepted.slice(0, 2) == 'es';
return accepted.slice(0, 2) == multipleVersionsLanguages[iAcceptedCompleteLang];
});
if(acceptedCompleteLang){
@@ -80,11 +93,15 @@ var getUserLanguage = function(req, res, next){
return 'en';
}
var latinAmericanSpanishes = ['es-419', 'es-mx', 'es-gt', 'es-cr', 'es-pa', 'es-do', 'es-ve', 'es-co', 'es-pe',
'es-ar', 'es-ec', 'es-cl', 'es-uy', 'es-py', 'es-bo', 'es-sv', 'es-hn',
'es-ni', 'es-pr'];
if(matches[0] === 'es'){
return (latinAmericanSpanishes.indexOf(acceptedCompleteLang) !== -1) ? 'es_419' : 'es';
}else if(matches[0] === 'zh'){
var iChinese = chineseVersions.indexOf(acceptedCompleteLang.toLowerCase());
return (iChinese !== -1) ? chineseVersions[iChinese] : 'zh';
}else{
return en;
}
return (latinAmericanSpanishes.indexOf(acceptedCompleteLang) !== -1) ? 'es_419' : 'es';
}else if(matches.length > 0){
return matches[0].toLowerCase();
}else{
+14
View File
@@ -0,0 +1,14 @@
var mongoose = require("mongoose");
var shared = require('../../../common');
// A collection used to store mailing list unsubscription for non registered email addresses
var EmailUnsubscriptionSchema = new mongoose.Schema({
_id: {
type: String,
'default': shared.uuid
},
email: String
});
module.exports.schema = EmailUnsubscriptionSchema;
module.exports.model = mongoose.model('EmailUnsubscription', EmailUnsubscriptionSchema);
+8 -2
View File
@@ -44,6 +44,7 @@ var UserSchema = new Schema({
veteran: Boolean,
snowball: Number,
spookDust: Number,
shinySeed: Number,
streak: Number,
challenges: Array,
quests: Schema.Types.Mixed,
@@ -145,6 +146,8 @@ var UserSchema = new Schema({
// Used to track the status of recapture emails sent to each user,
// can be 0 - no email sent - 1, 2, 3 or 4 - 4 means no more email will be sent to the user
recaptureEmailsPhase: {type: Number, 'default': 0},
// Needed to track the tip to send inside the email
weeklyRecapEmailsPhase: {type: Number, 'default': 0},
communityGuidelinesAccepted: {type: Boolean, 'default': false},
cronCount: {type:Number, 'default':0}
},
@@ -191,6 +194,7 @@ var UserSchema = new Schema({
special:{
snowball: {type: Number, 'default': 0},
spookDust: {type: Number, 'default': 0},
shinySeed: {type: Number, 'default': 0},
valentine: Number,
valentineReceived: Array, // array of strings, by sender name
nye: Number,
@@ -326,7 +330,8 @@ var UserSchema = new Schema({
invitedQuest: {type: Boolean, 'default': true},
//remindersToLogin: {type: Boolean, 'default': true},
// Those importantAnnouncements are in fact the recapture emails
importantAnnouncements: {type: Boolean, 'default': true}
importantAnnouncements: {type: Boolean, 'default': true},
weeklyRecaps: {type: Boolean, 'default': true}
}
},
profile: {
@@ -356,7 +361,8 @@ var UserSchema = new Schema({
stealth: {type: Number, 'default': 0},
streaks: {type: Boolean, 'default': false},
snowball: {type: Boolean, 'default': false},
spookDust: {type: Boolean, 'default': false}
spookDust: {type: Boolean, 'default': false},
shinySeed: {type: Boolean, 'default': false}
},
training: {
int: {type: Number, 'default': 0},
+5 -4
View File
@@ -35,13 +35,13 @@ module.exports = (swagger, v2) ->
'/status':
spec:
description: "Returns the status of the server (up or down)"
description: "Returns the status of the server (up or down). Does not require authentication."
action: (req, res) ->
res.json status: "up"
'/content':
spec:
description: "Get all available content objects. This is essential, since Habit often depends on item keys (eg, when purchasing a weapon)."
description: "Get all available content objects. This is essential, since Habit often depends on item keys (eg, when purchasing a weapon). Does not require authentication."
parameters: [
query("language","Optional language to use for content's strings. Default is english.","string")
]
@@ -49,7 +49,7 @@ module.exports = (swagger, v2) ->
'/content/paths':
spec:
description: "Show user model tree"
description: "Show user model tree. Does not require authentication."
action: user.getModelPaths
"/export/history":
@@ -68,7 +68,7 @@ module.exports = (swagger, v2) ->
"/user/tasks/{id}/{direction}":
spec:
#notes: "Simple scoring of a task."
description: "Simple scoring of a task. This is most-likely the only API route you'll be using as a 3rd-party developer. The most common operation is for the user to gain or lose points based on some action (browsing Reddit, running a mile, 1 Pomodor, etc). Call this route, if the task you're trying to score doesn't exist, it will be created for you. When random events occur, the <b>user._tmp</b> variable will be filled. Critical hits can be accessed through <b>user._tmp.crit</b>. The Streakbonus can be accessed through <b>user._tmp.streakBonus</b>. Both will contain the multiplier value. When random drops occur, the following values are available: <b>user._tmp.drop = {text,type,dialog,value,key,notes}</b>"
description: "Simple scoring of a task (Habit, Daily, To-Do, or Reward). This is most-likely the only API route you'll be using as a 3rd-party developer. The most common operation is for the user to gain or lose points based on some action (browsing Reddit, running a mile, 1 Pomodor, etc). Call this route, if the task you're trying to score doesn't exist, it will be created for you. When random events occur, the <b>user._tmp</b> variable will be filled. Critical hits can be accessed through <b>user._tmp.crit</b>. The Streakbonus can be accessed through <b>user._tmp.streakBonus</b>. Both will contain the multiplier value. When random drops occur, the following values are available: <b>user._tmp.drop = {text,type,dialog,value,key,notes}</b>"
parameters: [
path("id", "ID of the task to score. If this task doesn't exist, a task will be created automatically", "string")
path("direction", "Either 'up' or 'down'", "string")
@@ -698,6 +698,7 @@ module.exports = (swagger, v2) ->
path: '/challenges/{cid}'
description: 'Get a challenge'
parameters: [path('cid','Challenge id','string')]
middleware: [auth.auth, i18n.getUserLanguage]
action: challenges.get
"/challenges/{cid}/csv":
+8
View File
@@ -0,0 +1,8 @@
var express = require('express');
var router = new express.Router();
var i18n = require('../i18n');
var unsubscription = require('../controllers/unsubscription');
router.get('/unsubscribe', i18n.getUserLanguage, unsubscription.unsubscribe);
module.exports = router;
+1
View File
@@ -127,6 +127,7 @@ if (cores!==0 && cluster.isMaster && (isDev || isProd)) {
app.use(require('./routes/payments').middleware);
app.use(require('./routes/auth').middleware);
app.use(require('./routes/coupon').middleware);
app.use(require('./routes/unsubscription').middleware);
var v2 = express();
app.use('/api/v2', v2);
app.use('/api/v1', require('./routes/apiv1').middleware);
+39 -13
View File
@@ -44,6 +44,10 @@ function getUserInfo(user, fields) {
}
}
if(fields.indexOf('_id') != -1){
info._id = user._id;
}
if(fields.indexOf('canSend') != -1){
info.canSend = user.preferences.emailNotifications.unsubscribeFromAll !== true;
}
@@ -62,37 +66,59 @@ module.exports.txnEmail = function(mailingInfoArray, emailType, variables, perso
// It's important to pass at least a user with its `preferences` as we need to check if he unsubscribed
mailingInfoArray = mailingInfoArray.map(function(mailingInfo){
return mailingInfo._id ? getUserInfo(mailingInfo, ['email', 'name', 'canSend']) : mailingInfo;
return mailingInfo._id ? getUserInfo(mailingInfo, ['_id', 'email', 'name', 'canSend']) : mailingInfo;
}).filter(function(mailingInfo){
// Always send reset-password emails
return (mailingInfo.email && (mailingInfo.canSend || emailType === 'reset-password'));
// Don't check canSend for non registered users as already checked before
return (mailingInfo.email && ((!mailingInfo._id || mailingInfo.canSend) || emailType === 'reset-password'));
});
// Personal variables are personal to each email recipient, if they are missing
// we manually create a structure for them with RECIPIENT_NAME
// otherwise we just add RECIPIENT_NAME to the existing personal variables
// we manually create a structure for them with RECIPIENT_NAME and RECIPIENT_UNSUB_URL
// otherwise we just add RECIPIENT_NAME and RECIPIENT_UNSUB_URL to the existing personal variables
if(!personalVariables || personalVariables.length === 0){
personalVariables = mailingInfoArray.map(function(mailingInfo){
return {
rcpt: mailingInfo.email,
vars: [{
name: 'RECIPIENT_NAME',
content: mailingInfo.name
}]
vars: [
{
name: 'RECIPIENT_NAME',
content: mailingInfo.name
},
{
name: 'RECIPIENT_UNSUB_URL',
content: baseUrl + '/unsubscribe?code=' + module.exports.encrypt(JSON.stringify({
_id: mailingInfo._id,
email: mailingInfo.email
}))
}
]
}
});
}else{
var temporaryPersonalVariables = {};
mailingInfoArray.forEach(function(mailingInfo){
temporaryPersonalVariables[mailingInfo.email] = mailingInfo.name;
temporaryPersonalVariables[mailingInfo.email] = {
name: mailingInfo.name,
_id: mailingInfo._id
}
});
personalVariables.forEach(function(singlePersonalVariables){
singlePersonalVariables.vars.push({
name: 'RECIPIENT_NAME',
content: temporaryPersonalVariables[singlePersonalVariables.rcpt]
});
singlePersonalVariables.vars.push(
{
name: 'RECIPIENT_NAME',
content: temporaryPersonalVariables[singlePersonalVariables.rcpt].name
},
{
name: 'RECIPIENT_UNSUB_URL',
content: baseUrl + '/unsubscribe?code=' + module.exports.encrypt(JSON.stringify({
_id: temporaryPersonalVariables[singlePersonalVariables.rcpt]._id,
email: singlePersonalVariables.rcpt
}))
}
)
});
}