Merge branch 'api-v3-groups' into api-v3-challenges-tasks

This commit is contained in:
Matteo Pagliazzi
2016-01-14 18:25:59 +01:00
377 changed files with 14951 additions and 13066 deletions
+39 -17
View File
@@ -30,12 +30,12 @@ api.createChallenge = {
async handler (req, res) {
let user = res.locals.user;
req.checkBody('group', res.t('groupIdRequired')).notEmpty();
req.checkBody('groupId', res.t('groupIdRequired')).notEmpty();
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let groupId = req.body.group;
let groupId = req.body.groupId;
let prize = req.body.prize;
let group = await Group.getGroup(user, groupId, '-chat');
@@ -73,22 +73,11 @@ api.createChallenge = {
group.challengeCount += 1;
let tasks = req.body.tasks || []; // TODO validate
req.body.leader = user._id;
req.body.official = user.contributor.admin && req.body.official;
let challenge = new Challenge(Challenge.sanitize(req.body));
let toSave = tasks.map(tasks, taskToCreate => {
// TODO validate type
let task = new Tasks[taskToCreate.type](Tasks.Task.sanitizeCreate(taskToCreate));
task.challenge.id = challenge._id;
challenge.tasksOrder[`${task.type}s`].push(task._id);
return task.save();
});
toSave.unshift(challenge, group);
let results = await Q.all(toSave);
let results = await Q.all(challenge.save(), group.save());
let savedChal = results[0];
await savedChal.syncToUser(user); // (it also saves the user)
@@ -118,7 +107,7 @@ api.getChallenges = {
let challenges = await Challenge.find({
$or: [
{_id: {$in: user.challenges}}, // Challenges where the user is participating
{group: {$in: groups}}, // Challenges in groups where I'm a member
{groupId: {$in: groups}}, // Challenges in groups where I'm a member
{leader: user._id}, // Challenges where I'm the leader
],
_id: {$ne: '95533e05-1ff9-4e46-970b-d77219f199e9'}, // remove the Spread the Word Challenge for now, will revisit when we fix the closing-challenge bug TODO revisit
@@ -133,6 +122,39 @@ api.getChallenges = {
},
};
/**
* @api {get} /challenges/:challengeId Get a challenge given its id
* @apiVersion 3.0.0
* @apiName GetChallenge
* @apiGroup Challenge
*
* @apiSuccess {object} challenge The challenge object
*/
api.getChallenge = {
method: 'GET',
url: '/challenges/:challengeId',
middlewares: [authWithHeaders(), cron],
async handler (req, res) {
req.checkQuery('challengeId', res.t('challengeIdRequired')).notEmpty().isUUID();
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let user = res.local.user;
let challengeId = req.params.challengeId;
let challenge = await Challenge.findOne({_id: challengeId}).exec(); // TODO populate
// If the challenge does not exist, or if it exists but user is not a member, not the leader and not an admin -> throw error
// TODO support challenges in groups I'm a member of
if (!challenge || (user.challenges.indexOf(challengeId) === -1 && challenge.leader !== user._id && !user.contributor.admin)) { // eslint-disable-line no-extra-parens
throw new NotFound(res.t('challengeNotFound'));
}
res.respond(200, challenge);
},
};
// TODO everything here should be moved to a worker
// actually even for a worker it's probably just to big and will kill mongo
function _closeChal (challenge, broken = {}) {
@@ -162,11 +184,11 @@ function _closeChal (challenge, broken = {}) {
},
}, {multi: true}).exec(),
// Update the challengeCount on the group
Group.update({_id: challenge.group}, {$inc: {challengeCount: -1}}).exec(),
Group.update({_id: challenge.groupId}, {$inc: {challengeCount: -1}}).exec(),
];
// Refund the leader if the challenge is closed and the group not the tavern
if (challenge.group !== 'habitrpg' && brokenReason === 'CHALLENGE_DELETED') {
if (challenge.groupId !== 'habitrpg' && brokenReason === 'CHALLENGE_DELETED') {
tasks.push(User.update({_id: challenge.leader}, {$inc: {balance: challenge.prize / 4}}).exec());
}
+146 -147
View File
@@ -2,16 +2,20 @@ import { authWithHeaders } from '../../middlewares/api-v3/auth';
import Q from 'q';
import _ from 'lodash';
import cron from '../../middlewares/api-v3/cron';
import { model as Group } from '../../models/group';
import {
INVITES_LIMIT,
model as Group,
} from '../../models/group';
import { model as User } from '../../models/user';
import { model as EmailUnsubscription } from '../../models/emailUnsubscription';
import {
NotFound,
BadRequest,
NotAuthorized,
} from '../../libs/api-v3/errors';
import * as firebase from '../../libs/api-v3/firebase';
import { txnEmail } from '../../libs/api-v3/email';
// import { encrypt } from '../../libs/api-v3/encryption';
import { sendTxn as sendTxnEmail } from '../../libs/api-v3/email';
import { encrypt } from '../../libs/api-v3/encryption';
let api = {};
@@ -31,7 +35,6 @@ api.createGroup = {
middlewares: [authWithHeaders(), cron],
async handler (req, res) {
let user = res.locals.user;
let group = new Group(Group.sanitize(req.body)); // TODO validate empty req.body
group.leader = user._id;
@@ -43,6 +46,7 @@ api.createGroup = {
user.balance--;
user.guilds.push(group._id);
} else {
if (group.privacy !== 'private') throw new NotAuthorized(res.t('partyMustbePrivate'));
if (user.party._id) throw new NotAuthorized(res.t('messageGroupAlreadyInParty'));
user.party._id = group._id;
@@ -53,6 +57,7 @@ api.createGroup = {
firebase.updateGroupData(savedGroup);
firebase.addUserToGroup(savedGroup._id, user._id);
return res.respond(201, savedGroup); // TODO populate
},
};
@@ -95,13 +100,13 @@ api.getGroups = {
type: 'guild',
privacy: 'private',
_id: {$in: user.guilds},
}).select(groupFields).sort(sort).exec()); // TODO isMember
}).select(groupFields).sort(sort).exec());
break;
case 'publicGuilds':
queries.push(Group.find({
type: 'guild',
privacy: 'public',
}).select(groupFields).sort(sort).exec()); // TODO use lean? isMember
}).select(groupFields).sort(sort).exec()); // TODO use lean?
break;
case 'tavern':
queries.push(Group.getGroup(user, 'habitrpg', groupFields));
@@ -305,7 +310,7 @@ api.leaveGroup = {
// Send an email to the removed user with an optional message from the leader
function _sendMessageToRemoved (group, removedUser, message) {
if (removedUser.preferences.emailNotifications.kickedGroup !== false) {
txnEmail(removedUser, `kicked-from-${group.type}`, [
sendTxnEmail(removedUser, `kicked-from-${group.type}`, [
{name: 'GROUP_NAME', content: group.name},
{name: 'MESSAGE', content: message},
{name: 'GUILDS_LINK', content: '/#/options/groups/guilds/public'},
@@ -392,145 +397,105 @@ api.removeGroupMember = {
},
};
/* function _inviteByUUIDs (uuids, group, inviter, req, res, next) {
async.each(uuids, function(uuid, cb){
User.findById(uuid, function(err,invite){
if (err) return cb(err);
if (!invite)
return cb({code:400,err:'User with id "' + uuid + '" not found'});
if (group.type == 'guild') {
if (_.contains(group.members,uuid))
return cb({code:400, err: "User already in that group"});
if (invite.invitations && invite.invitations.guilds && _.find(invite.invitations.guilds, {id:group._id}))
return cb({code:400, err:"User already invited to that group"});
sendInvite();
} else if (group.type == 'party') {
if (invite.invitations && !_.isEmpty(invite.invitations.party))
return cb({code: 400,err:"User already pending invitation."});
Group.find({type: 'party', members: {$in: [uuid]}}, function(err, groups){
if (err) return cb(err);
if (!_.isEmpty(groups) && groups[0].members.length > 1) {
return cb({code: 400, err: "User already in a party."})
}
sendInvite();
});
}
async function _inviteByUUID (uuid, group, inviter, req, res) {
// @TODO: Add Push Notifications
let userToInvite = await User.findById(uuid).exec();
function sendInvite (){
if(group.type === 'guild'){
invite.invitations.guilds.push({id: group._id, name: group.name, inviter:res.locals.user._id});
if (!userToInvite) {
throw new NotFound(res.t('userWithIDNotFound', {userId: uuid}));
}
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};
if (group.type === 'guild') {
if (_.contains(userToInvite.guilds, group._id)) {
throw new NotAuthorized(res.t('userAlreadyInGroup'));
}
if (_.find(userToInvite.invitations.guilds, {id: group._id})) {
throw new NotAuthorized(res.t('userAlreadyInvitedToGroup'));
}
userToInvite.invitations.guilds.push({id: group._id, name: group.name, inviter: inviter._id});
} else if (group.type === 'party') {
if (!_.isEmpty(userToInvite.invitations.party)) {
throw new NotAuthorized(res.t('userAlreadyPendingInvitation'));
}
if (userToInvite.party._id) {
throw new NotAuthorized(res.t('userAlreadyInAParty'));
}
// @TODO: Why was this here?
// req.body.type in 'guild', 'party'
userToInvite.invitations.party = {id: group._id, name: group.name, inviter: inviter._id};
}
pushNotify.sendNotify(invite, shared.i18n.t('invitedParty'), group.name);
}
let groupLabel = group.type === 'guild' ? 'Guild' : 'Party';
if (userToInvite.preferences.emailNotifications[`invited${groupLabel}`] !== false) {
let emailVars = [
{name: 'INVITER', content: inviter.profile.name},
{name: 'REPLY_TO_ADDRESS', content: inviter.email},
];
group.invites.push(invite._id);
async.series([
function(cb){
invite.save(cb);
}
], function(err, results){
if (err) return cb(err);
if(invite.preferences.emailNotifications['invited' + (group.type == 'guild' ? 'Guild' : 'Party')] !== false){
var inviterVars = utils.getUserInfo(res.locals.user, ['name', 'email']);
var emailVars = [
{name: 'INVITER', content: inviterVars.name},
{name: 'REPLY_TO_ADDRESS', content: inviterVars.email}
];
if(group.type == 'guild'){
emailVars.push(
{name: 'GUILD_NAME', content: group.name},
{name: 'GUILD_URL', content: '/#/options/groups/guilds/public'}
);
}else{
emailVars.push(
{name: 'PARTY_NAME', content: group.name},
{name: 'PARTY_URL', content: '/#/options/groups/party'}
)
}
utils.txnEmail(invite, ('invited-' + (group.type == 'guild' ? 'guild' : 'party')), emailVars);
}
cb();
});
}
});
}, function(err){
if(err) return err.code ? res.json(err.code, {err: err.err}) : next(err);
async.series([
function(cb) {
group.save(cb);
},
function(cb) {
// TODO pass group from save above don't find it again, or you have to find it again in order to run populate?
populateQuery(group.type, Group.findById(group._id)).exec(function(err, populatedGroup){
if(err) return next(err);
res.json(populatedGroup);
});
}
]);
});
};
function _inviteByEmails (emails, group, inviter, req, res, next) {
let usersAlreadyRegistered = [];
let invitesToSend = [];
return Q.all(emails.forEach(invite => {
if (!invite.email) throw new BadRequest(res.t('inviteMissingEmail'));
return User.findOne({$or: [
{'auth.local.email': invite.email},
{'auth.facebook.emails.value': invite.email}
]})
.select({_id: true, 'preferences.emailNotifications': true})
.exec()
.then(userToContact => {
if(userToContact){
usersAlreadyRegistered.push(userToContact._id); // TODO does it work not returning
} else {
// yeah, it supports guild too but for backward compatibility we'll use partyInvite as query
// TODO absolutely refactor this horrible code
let link = `?partyInvite=${utils.encrypt(JSON.stringify({id: group._id, inviter: inviter, name: group.name}))}`;
let inviterVars = getUserInfo(inviter, ['name', 'email']);
let variables = [
{name: 'LINK', content: link},
{name: 'INVITER', content: req.body.inviter || inviterVars.name},
{name: 'REPLY_TO_ADDRESS', content: inviterVars.email}
];
if(group.type == 'guild'){
variables.push({name: 'GUILD_NAME', content: group.name});
}
// TODO implement "users can only be invited once"
// Check for the email address not to be unsubscribed
return EmailUnsubscription.findOne({email: invite.email}).exec()
.then(unsubscribed => {
if (!unsubscribed) utils.txnEmail(invite, ('invite-friend' + (group.type == 'guild' ? '-guild' : '')), variables);
});
}
});
}))
.then(() => {
if (usersAlreadyRegistered.length > 0){
return _inviteByUUIDs(usersAlreadyRegistered, group, inviter, req, res, next);
if (group.type === 'guild') {
emailVars.push(
{name: 'GUILD_NAME', content: group.name},
{name: 'GUILD_URL', content: '/#/options/groups/guilds/public'},
);
} else {
emailVars.push(
{name: 'PARTY_NAME', content: group.name},
{name: 'PARTY_URL', content: '/#/options/groups/party'},
);
}
res.respond(200, {}); // TODO what to return?
});
}; */
sendTxnEmail(userToInvite, `invited-${groupLabel}`, emailVars);
}
let userInvited = await userToInvite.save();
if (group.type === 'guild') {
return userInvited.invitations.guilds[userToInvite.invitations.guilds.length - 1];
} else if (group.type === 'party') {
return userInvited.invitations.party;
}
}
async function _inviteByEmail (invite, group, inviter, req, res) {
let userReturnInfo;
if (!invite.email) throw new BadRequest(res.t('inviteMissingEmail'));
let userToContact = await User.findOne({$or: [
{'auth.local.email': invite.email},
{'auth.facebook.emails.value': invite.email},
]})
.select({_id: true, 'preferences.emailNotifications': true})
.exec();
if (userToContact) {
userReturnInfo = await _inviteByUUID(userToContact._id, group, inviter, req, res);
} else {
userReturnInfo = invite.email;
// yeah, it supports guild too but for backward compatibility we'll use partyInvite as query
// TODO absolutely refactor this horrible code
const partyQueryString = JSON.stringify({id: group._id, inviter, name: group.name});
const encryptedPartyqueryString = encrypt(partyQueryString);
let link = `?partyInvite=${encryptedPartyqueryString}`;
let variables = [
{name: 'LINK', content: link},
{name: 'INVITER', content: inviter || inviter.profile.name},
{name: 'REPLY_TO_ADDRESS', content: inviter.email},
];
if (group.type === 'guild') {
variables.push({name: 'GUILD_NAME', content: group.name});
}
// TODO implement "users can only be invited once"
// Check for the email address not to be unsubscribed
let userIsUnsubscribed = await EmailUnsubscription.findOne({email: invite.email}).exec();
let groupLabel = group.type === 'guild' ? '-guild' : '';
if (!userIsUnsubscribed) sendTxnEmail(invite, `invite-friend${groupLabel}`, variables);
}
return userReturnInfo;
}
/**
* @api {post} /groups/:groupId/invite Invite users to a group using their UUIDs or email addresses
@@ -564,15 +529,49 @@ api.inviteToGroup = {
let uuids = req.body.uuids;
let emails = req.body.emails;
if (uuids && emails) { // TODO fix this, low priority, allow for inviting by both at the same time
throw new BadRequest(res.t('canOnlyInviteEmailUuid'));
} else if (Array.isArray(uuids)) {
// return _inviteByUUIDs(uuids, group, user, req, res, next);
} else if (Array.isArray(emails)) {
// return _inviteByEmails(emails, group, user, req, res, next);
} else {
let uuidsIsArray = Array.isArray(uuids);
let emailsIsArray = Array.isArray(emails);
if (!uuids && !emails) {
throw new BadRequest(res.t('canOnlyInviteEmailUuid'));
}
let results = [];
let totalInvites = 0;
if (uuids) {
if (!uuidsIsArray) {
throw new BadRequest(res.t('uuidsMustBeAnArray'));
} else {
totalInvites += uuids.length;
}
}
if (emails) {
if (!emailsIsArray) {
throw new BadRequest(res.t('emailsMustBeAnArray'));
} else {
totalInvites += emails.length;
}
}
if (totalInvites > INVITES_LIMIT) {
throw new BadRequest(res.t('canOnlyInviteMaxInvites', {maxInvites: INVITES_LIMIT}));
}
if (uuids) {
let uuidInvites = uuids.map((uuid) => _inviteByUUID(uuid, group, user, req, res));
let uuidResults = await Q.all(uuidInvites);
results.push(...uuidResults);
}
if (emails) {
let emailInvites = emails.map((invite) => _inviteByEmail(invite, group, user, req, res));
let emailResults = await Q.all(emailInvites);
results.push(...emailResults);
}
res.respond(200, results);
},
};
+22 -5
View File
@@ -11,8 +11,9 @@ import {
import shared from '../../../../common';
import Q from 'q';
import _ from 'lodash';
import moment from 'moment';
import scoreTask from '../../../../common/script/api-v3/scoreTask';
import { preenHistory } from '../../../../common/script/api-v3/preenHistory';
import { preenHistory } from '../../../../common/script/api-v3/preening';
let api = {};
@@ -474,11 +475,27 @@ api.scoreTask = {
}).exec();
chalTask.value += delta;
if (chalTask.type === 'habit' || chalTask.type === 'daily') {
chalTask.history.push({value: chalTask.value, date: Number(new Date())});
// TODO 1. treat challenges as subscribed users for preening 2. it's expensive to do it at every score - how to have it happen once like for cron?
chalTask.history = preenHistory(user, chalTask.history);
chalTask.markModified('history');
// Add only one history entry per day
if (moment(chalTask.history[chalTask.history.length - 1].date).isSame(new Date(), 'day')) {
chalTask.history[chalTask.history.length - 1] = {
date: Number(new Date()),
value: chalTask.value,
};
chalTask.markModified(`history.${chalTask.history.length - 1}`);
} else {
chalTask.history.push({
date: Number(new Date()),
value: chalTask.value,
});
// Only preen task history once a day when the task is scored first
if (chalTask.history.length > 365) {
chalTask.history = preenHistory(chalTask.history, true); // true means the challenge will retain as much entries as a subscribed user
chalTask.markModified(`history.${chalTask.history.length - 1}`);
}
}
}
await chalTask.save();