Merged in develop

This commit is contained in:
Keith Holliday
2018-09-10 09:42:51 -05:00
578 changed files with 36458 additions and 33407 deletions

View File

@@ -251,7 +251,7 @@ api.joinChallenge = {
if (challenge.isMember(user)) throw new NotAuthorized(res.t('userAlreadyInChallenge'));
let group = await Group.getGroup({user, groupId: challenge.group, fields: basicGroupFields, optionalMembership: true});
if (!group || !challenge.hasAccess(user, group)) throw new NotFound(res.t('challengeNotFound'));
if (!group || !challenge.canJoin(user, group)) throw new NotFound(res.t('challengeNotFound'));
challenge.memberCount += 1;
@@ -329,7 +329,7 @@ api.leaveChallenge = {
};
/**
* @api {get} /api/v3/challenges/user Get challenges for a user.
* @api {get} /api/v3/challenges/user Get challenges for a user
* @apiName GetUserChallenges
* @apiGroup Challenge
* @apiDescription Get challenges the user has access to. Includes public challenges, challenges belonging to the user's group, and challenges the user has already joined.
@@ -640,7 +640,7 @@ api.exportChallengeCsv = {
};
/**
* @api {put} /api/v3/challenges/:challengeId Update the name, description, or leader of a challenge.
* @api {put} /api/v3/challenges/:challengeId Update the name, description, or leader of a challenge
*
* @apiName UpdateChallenge
* @apiGroup Challenge

View File

@@ -12,7 +12,6 @@ import { getUserInfo, getGroupUrl, sendTxn } from '../../libs/email';
import slack from '../../libs/slack';
import pusher from '../../libs/pusher';
import { getAuthorEmailFromMessage } from '../../libs/chat';
import { userIsMuted, muteUserForLife } from '../../libs/chat/mute';
import { chatReporterFactory } from '../../libs/chatReporting/chatReporterFactory';
import nconf from 'nconf';
import bannedWords from '../../libs/bannedWords';
@@ -125,7 +124,6 @@ api.postChat = {
if (textContainsBannedSlur(req.body.message)) {
let message = req.body.message;
user.flags.chatRevoked = true;
muteUserForLife(user);
await user.save();
// Email the mods
@@ -162,7 +160,7 @@ api.postChat = {
if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.privacy !== 'private' && userIsMuted(user)) {
if (group.privacy !== 'private' && user.flags.chatRevoked) {
throw new NotAuthorized(res.t('chatPrivilegesRevoked'));
}

View File

@@ -592,11 +592,6 @@ api.joinGroup = {
// @TODO: Review the need for this and if still needed, don't base this on memberCount
if (!group.hasNotCancelled() && group.memberCount === 0) group.leader = user._id; // If new user is only member -> set as leader
if (group.hasNotCancelled()) {
await payments.addSubToGroupUser(user, group);
await group.updateGroupPlan();
}
group.memberCount += 1;
let promises = [group.save(), user.save()];
@@ -640,6 +635,11 @@ api.joinGroup = {
promises = await Promise.all(promises);
if (group.hasNotCancelled()) {
await payments.addSubToGroupUser(user, group);
await group.updateGroupPlan();
}
let response = await Group.toJSONCleanChat(promises[0], user);
let leader = await User.findById(response.leader).select(nameFields).exec();
if (leader) {
@@ -792,7 +792,6 @@ api.leaveGroup = {
}
await group.leave(user, req.query.keep, req.body.keepChallenges);
if (group.hasNotCancelled()) await group.updateGroupPlan(true);
_removeMessagesFromMember(user, group._id);
await user.save();
@@ -806,6 +805,7 @@ api.leaveGroup = {
await payments.cancelGroupSubscriptionForUser(user, group);
}
if (group.hasNotCancelled()) await group.updateGroupPlan(true);
res.respond(200, {});
},
};
@@ -894,10 +894,6 @@ api.removeGroupMember = {
if (isInGroup) {
group.memberCount -= 1;
if (group.hasNotCancelled()) {
await group.updateGroupPlan(true);
await payments.cancelGroupSubscriptionForUser(member, group, true);
}
if (group.quest && group.quest.leader === member._id) {
group.quest.key = undefined;
@@ -946,6 +942,12 @@ api.removeGroupMember = {
member.save(),
group.save(),
]);
if (isInGroup && group.hasNotCancelled()) {
await group.updateGroupPlan(true);
await payments.cancelGroupSubscriptionForUser(member, group, true);
}
res.respond(200, {});
},
};
@@ -1086,6 +1088,16 @@ api.inviteToGroup = {
results.push(...usernameResults);
}
let analyticsObject = {
uuid: user._id,
hitType: 'event',
category: 'behavior',
groupType: group.type,
headers: req.headers,
};
res.analytics.track('group invite', analyticsObject);
res.respond(200, results);
},
};

View File

@@ -143,7 +143,7 @@ api.getHeroes = {
// Note, while the following routes are called getHero / updateHero
// they can be used by admins to get/update any user
const heroAdminFields = 'contributor balance profile.name purchased items auth flags.chatRevoked flags.chatRevokedEndDate';
const heroAdminFields = 'contributor balance profile.name purchased items auth flags.chatRevoked';
/**
* @api {get} /api/v3/hall/heroes/:heroId Get any user ("hero") given the UUID
@@ -275,7 +275,6 @@ api.updateHero = {
}
if (updateData.flags && _.isBoolean(updateData.flags.chatRevoked)) hero.flags.chatRevoked = updateData.flags.chatRevoked;
if (updateData.flags && updateData.flags.chatRevokedEndDate) hero.flags.chatRevokedEndDate = updateData.flags.chatRevokedEndDate;
let savedHero = await hero.save();
let heroJSON = savedHero.toJSON();

View File

@@ -0,0 +1,29 @@
import { authWithHeaders } from '../../middlewares/auth';
import { toArray, orderBy } from 'lodash';
let api = {};
/* NOTE most inbox routes are either in the user or members controller */
/**
* @api {get} /api/v3/inbox/messages Get inbox messages for a user
* @apiPrivate
* @apiName GetInboxMessages
* @apiGroup Inbox
* @apiDescription Get inbox messages for a user
*
* @apiSuccess {Array} data An array of inbox messages
*/
api.getInboxMessages = {
method: 'GET',
url: '/inbox/messages',
middlewares: [authWithHeaders()],
async handler (req, res) {
const messagesObj = res.locals.user.inbox.messages;
const messagesArray = orderBy(toArray(messagesObj), ['timestamp'], ['desc']);
res.respond(200, messagesArray);
},
};
module.exports = api;

View File

@@ -32,6 +32,62 @@ let api = {};
*
* @apiSuccess {Object} data The member object
*
* @apiSuccess (Object) data.inbox Basic information about person's inbox
* @apiSuccess (Object) data.stats Includes current stats and buffs
* @apiSuccess (Object) data.profile Includes name
* @apiSuccess (Object) data.preferences Includes info about appearance and public prefs
* @apiSuccess (Object) data.party Includes basic info about current party and quests
* @apiSuccess (Object) data.items Basic inventory information includes quests, food, potions, eggs, gear, special items
* @apiSuccess (Object) data.achievements Lists current achievements
* @apiSuccess (Object) data.auth Includes latest timestamps
*
* @apiSuccessExample {json} Success-Response:
* {
* "success": true,
* "data": {
* "_id": "99999999-9999-9999-9999-8f14c101aeff",
* "inbox": {
* "optOut": false
* },
* "stats": {
* ---INCLUDES STATS AND BUFFS---
* },
* "profile": {
* "name": "Ezra"
* },
* "preferences": {
* ---INCLUDES INFO ABOUT APPEARANCE AND PUBLIC PREFS---
* },
* "party": {
* "_id": "12345678-0987-abcd-82a6-837c81db4c1e",
* "quest": {
* "RSVPNeeded": false,
* "progress": {}
* },
* },
* "items": {
* "lastDrop": {
* "count": 0,
* "date": "2017-01-15T02:41:35.009Z"
* },
* ----INCLUDES QUESTS, FOOD, POTIONS, EGGS, GEAR, CARDS, SPECIAL ITEMS (E.G. SNOWBALLS)----
* }
* },
* "achievements": {
* "partyUp": true,
* "habitBirthdays": 2,
* },
* "auth": {
* "timestamps": {
* "loggedin": "2017-03-05T12:30:54.545Z",
* "created": "2017-01-12T03:30:11.842Z"
* }
* },
* "id": "99999999-9999-9999-9999-8f14c101aeff"
* }
* }
*)
*
* @apiUse UserNotFound
*/
api.getMember = {
@@ -302,6 +358,21 @@ function _getMembersForItem (type) {
* @apiParam (Query) {Boolean} includeAllPublicFields Query parameter available only when fetching a party. If === `true` then all public fields for members will be returned (like when making a request for a single member)
*
* @apiSuccess {Array} data An array of members, sorted by _id
*
* @apiSuccessExample {json} Success-Response:
* {
* "success": true,
* "data": [
* {
* "_id": "00000001-1111-9999-9000-111111111111",
* "profile": {
* "name": "Jiminy"
* },
* "id": "00000001-1111-9999-9000-111111111111"
* },
* }
*
*
* @apiUse ChallengeNotFound
* @apiUse GroupNotFound
*/
@@ -325,6 +396,21 @@ api.getMembersForGroup = {
*
* @apiSuccess {array} data An array of invites, sorted by _id
*
* @apiSuccessExample {json} Success-Response:
* {
* "success": true,
* "data": [
* {
* "_id": "99f3cb9d-4af8-4ca4-9b82-6b2a6bf59b7a",
* "profile": {
* "name": "DoomSmoocher"
* },
* "id": "99f3cb9d-4af8-4ca4-9b82-6b2a6bf59b7a"
* }
* ]
* }
*
*
* @apiUse ChallengeNotFound
* @apiUse GroupNotFound
*/
@@ -375,6 +461,48 @@ api.getMembersForChallenge = {
*
* @apiSuccess {Object} data Return an object with member _id, profile.name and a tasks object with the challenge tasks for the member
*
* @apiSuccessExample {json} Success-Response:
* {
* "data": {
* "_id": "b0413351-405f-416f-8787-947ec1c85199",
* "profile": {"name": "MadPink"},
* "tasks": [
* {
* "_id": "9cd37426-0604-48c3-a950-894a6e72c156",
* "text": "Make sure the place where you sleep is quiet, dark, and cool.",
* "updatedAt": "2017-06-17T17:44:15.916Z",
* "createdAt": "2017-06-17T17:44:15.916Z",
* "reminders": [],
* "group": {
* "approval": {
* "requested": false,
* "approved": false,
* "required": false
* },
* "assignedUsers": []
* },
* "challenge": {
* "taskId": "6d3758b1-071b-4bfa-acd6-755147a7b5f6",
* "id": "4db6bd82-b829-4bf2-bad2-535c14424a3d",
* "shortName": "Take This June 2017"
* },
* "attribute": "str",
* "priority": 1,
* "value": 0,
* "notes": "",
* "type": "todo",
* "checklist": [],
* "collapseChecklist": false,
* "completed": false,
* },
* "startDate": "2016-09-01T05:00:00.000Z",
* "everyX": 1,
* "frequency": "weekly",
* "id": "b207a15e-8bfd-4aa7-9e64-1ba89699da06"
* }
* ]
* }
*
* @apiUse ChallengeNotFound
* @apiUse UserNotFound
*/
@@ -478,25 +606,25 @@ api.sendPrivateMessage = {
req.checkBody('message', res.t('messageRequired')).notEmpty();
req.checkBody('toUserId', res.t('toUserIDRequired')).notEmpty().isUUID();
let validationErrors = req.validationErrors();
const validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let sender = res.locals.user;
let message = req.body.message;
let receiver = await User.findById(req.body.toUserId).exec();
const sender = res.locals.user;
const message = req.body.message;
const receiver = await User.findById(req.body.toUserId).exec();
if (!receiver) throw new NotFound(res.t('userNotFound'));
let objections = sender.getObjectionsToInteraction('send-private-message', receiver);
const objections = sender.getObjectionsToInteraction('send-private-message', receiver);
if (objections.length > 0 && !sender.isAdmin()) throw new NotAuthorized(res.t(objections[0]));
await sender.sendMessage(receiver, { receiverMsg: message });
const newMessage = await sender.sendMessage(receiver, { receiverMsg: message });
if (receiver.preferences.emailNotifications.newPM !== false) {
sendTxnEmail(receiver, 'new-pm', [
{name: 'SENDER', content: getUserInfo(sender, ['name']).name},
]);
}
if (receiver.preferences.pushNotifications.newPM !== false) {
sendPushNotification(
receiver,
@@ -510,7 +638,7 @@ api.sendPrivateMessage = {
);
}
res.respond(200, {});
res.respond(200, { message: newMessage });
},
};
@@ -519,7 +647,7 @@ api.sendPrivateMessage = {
* @apiName TransferGems
* @apiGroup Member
*
* @apiParam (Body) {String} message The message
* @apiParam (Body) {String} message The message to the user
* @apiParam (Body) {UUID} toUserId The toUser _id
* @apiParam (Body) {Integer} gemAmount The number of gems to send
*

View File

@@ -3,7 +3,7 @@ import { authWithHeaders } from '../../middlewares/auth';
let api = {};
// @TODO export this const, cannot export it from here because only routes are exported from controllers
const LAST_ANNOUNCEMENT_TITLE = 'OFFICIAL CHALLENGE: BACK-TO-SCHOOL PREPARATION!';
const LAST_ANNOUNCEMENT_TITLE = 'NEW BACKGROUNDS, ARMOIRE ITEMS, RESOLUTION SUCCESS CHALLENGE, AND TAKE THIS CHALLENGE';
const worldDmg = { // @TODO
bailey: false,
};
@@ -27,20 +27,39 @@ api.getNews = {
html: `
<div class="bailey">
<div class="media align-items-center">
<div class="mr-3 ${baileyClass}"></div>
<div class="media-body">
<h1 class="align-self-center">${res.t('newStuff')}</h1>
<h2>8/8/2018 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
<div class="media align-items-center">
<div class="mr-3 ${baileyClass}"></div>
<div class="media-body">
<h1 class="align-self-center">${res.t('newStuff')}</h1>
<h2>9/4/2018 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
</div>
</div>
<hr/>
<h3>September Backgrounds and Armoire Items!</h3>
<p>Weve added three new backgrounds to the Background Shop! Now your avatar can go Apple Picking, stand on a Giant Book, and hang out with your pets and mounts in a Cozy Barn. Check them out under User Icon > Backgrounds!</p>
<p>Plus, theres new gold-purchasable equipment in the Enchanted Armoire, including the Bookbinder Set. Better work hard on your real-life tasks to earn all the pieces! Enjoy :)</p>
<div class="small mb-3">by GeraldThePixel, Maans, virginiamoon, shanaqui, and fasteagle190</div>
</div>
<div class="promo_armoire_backgrounds_201809 ml-3 mb-3"></div>
</div>
<div class="media align-items-center">
<div class="scene_perfect_day mr-3"></div>
<div class="media-body">
<h3>September 2018 Resolution Success Challenge and New Take This Challenge</h3>
<p>The Habitica team has launched a special official Challenge series hosted in the <a href='/groups/guild/6e6a8bd3-9f5f-4351-9188-9f11fcd80a99' target='_blank'>Official New Year's Resolution Guild</a>. These Challenges are designed to help you build and maintain goals that are destined for success and then stick with them as the year progresses. For this month's Challenge, <a href='https://habitica.com/challenges/8d08b298-1716-4553-8739-5071ae002de4' target='_blank'>Celebrate your Triumphs</a>, we're focusing on looking back to see all the progress you've made so far! It has a 15 Gem prize, which will be awarded to five lucky winners on October 1st.</p>
<p>Congratulations to the winners of the August Challenge, Enkia the Wicked, wondergrrl, renko, Mibbs, and TereLiz!</p>
</div>
</div>
<hr/>
<div class="media align-items-center">
<div class="media-body">
<p>The school year is looming large for many scholarly Habiticans, so we've prepared <a href='/challenges/0acb1d56-1660-41a4-af80-9259f080b62b' target='_blank'>a special Back-to-School Challenge</a> to help with the transition between summer and semester. Check it out now for a chance to win: five lucky winners will get a badge for their profile and their choice of a <a href='https://habitica.wikia.com/wiki/Subscription' target='_blank'>gift subscription</a> or Gems!</p>
<div class="small mb-3">by Beffymaroo</div>
<p>The next Take This Challenge has also launched, "<a href='/challenges/32818ef2-18de-48b6-ab6e-2b52640c47f7' target='_blank'>Gaining Inspiration Points</a>", with a focus on creative endeavors. Be sure to check it out to earn additional pieces of the Take This armor set!</p>
</div>
<div class="scene_reading ml-3 mb-3"></div>
<div class="promo_take_this mb-3"></div>
</div>
<p><a href='http://www.takethis.org/' target='_blank'>Take This</a> is a nonprofit that seeks to inform the gamer community about mental health issues, to provide education about mental disorders and mental illness prevention, and to reduce the stigma of mental illness.</p>
<p>Congratulations to the winners of the last Take This Challenge, "Notice Me, Senpai!": grand prize winner Sebem.seme, and runners-up Jessie, MaxClayson, kayote, Madison Walrath, and LaChistosa. Plus, all participants in that Challenge have received a piece of the <a href='http://habitica.wikia.com/wiki/Event_Item_Sequences#Take_This_Armor_Set' target='_blank'>Take This item set</a> if they hadn't completed it already. It is located in your Rewards column. Enjoy!</p>
<div class="small mb-3">by Doctor B, the Take This team, Lemoness, Beffymaroo, and SabreCat</div>
</div>
`,
});

View File

@@ -3,6 +3,7 @@ import {
NotAuthorized,
NotFound,
} from '../../libs/errors';
import { model as PushDevice } from '../../models/pushDevice';
let api = {};
@@ -25,17 +26,17 @@ api.addPushDevice = {
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
let user = res.locals.user;
const user = res.locals.user;
req.checkBody('regId', res.t('regIdRequired')).notEmpty();
req.checkBody('type', res.t('typeRequired')).notEmpty().isIn(['ios', 'android']);
let validationErrors = req.validationErrors();
const validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let pushDevices = user.pushDevices;
const pushDevices = user.pushDevices;
let item = {
const item = {
regId: req.body.regId,
type: req.body.type,
};
@@ -44,9 +45,14 @@ api.addPushDevice = {
throw new NotAuthorized(res.t('pushDeviceAlreadyAdded'));
}
pushDevices.push(item);
// Concurrency safe update
const pushDevice = (new PushDevice(item)).toJSON(); // Create a mongo doc
await user.update({
$push: { pushDevices: pushDevice },
}).exec();
await user.save();
// Update the response
user.pushDevices.push(pushDevice);
res.respond(200, user.pushDevices, res.t('pushDeviceAdded'));
},
@@ -70,16 +76,18 @@ api.removePushDevice = {
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
let user = res.locals.user;
const user = res.locals.user;
req.checkParams('regId', res.t('regIdRequired')).notEmpty();
let validationErrors = req.validationErrors();
const validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let regId = req.params.regId;
let pushDevices = user.pushDevices;
const regId = req.params.regId;
let indexOfPushDevice = pushDevices.findIndex((element) => {
const pushDevices = user.pushDevices;
const indexOfPushDevice = pushDevices.findIndex((element) => {
return element.regId === regId;
});
@@ -87,8 +95,12 @@ api.removePushDevice = {
throw new NotFound(res.t('pushDeviceNotFound'));
}
// Concurrency safe update
const pullQuery = { $pull: { pushDevices: { $elemMatch: { regId } } } };
await user.update(pullQuery).exec();
// Update the response
pushDevices.splice(indexOfPushDevice, 1);
await user.save();
res.respond(200, user.pushDevices, res.t('pushDeviceRemoved'));
},

View File

@@ -175,6 +175,7 @@ api.createUserTasks = {
hitType: 'event',
category: 'behavior',
taskType: task.type,
headers: req.headers,
});
}
@@ -702,6 +703,7 @@ api.scoreTask = {
category: 'behavior',
taskType: task.type,
direction,
headers: req.headers,
});
}
},

View File

@@ -147,7 +147,7 @@ api.getBuyList = {
};
/**
* @api {get} /api/v3/user/in-app-rewards Get the in app items appaearing in the user's reward column
* @api {get} /api/v3/user/in-app-rewards Get the in app items appearing in the user's reward column
* @apiName UserGetInAppRewards
* @apiGroup User
*