Merged in develop

This commit is contained in:
Keith Holliday
2017-06-27 22:23:13 -06:00
897 changed files with 50434 additions and 41464 deletions

View File

@@ -228,6 +228,12 @@ api.createChallenge = {
let challengeValidationErrors = challenge.validateSync();
if (challengeValidationErrors) throw challengeValidationErrors;
// Add achievement if user's first challenge
if (!user.achievements.joinedChallenge) {
user.achievements.joinedChallenge = true;
user.addNotification('CHALLENGE_JOINED_ACHIEVEMENT');
}
let results = await Bluebird.all([challenge.save({
validateBeforeSave: false, // already validate
}), group.save()]);
@@ -286,6 +292,12 @@ api.joinChallenge = {
challenge.memberCount += 1;
// Add achievement if user's first challenge
if (!user.achievements.joinedChallenge) {
user.achievements.joinedChallenge = true;
user.addNotification('CHALLENGE_JOINED_ACHIEVEMENT');
}
// Add all challenge's tasks to user's tasks and save the challenge
let results = await Bluebird.all([challenge.syncToUser(user), challenge.save()]);

View File

@@ -117,8 +117,6 @@ function textContainsBannedWords (message) {
* @apiParam (Body) {String} message Message The message to post
* @apiParam (Query) {UUID} previousMsg The previous chat message's UUID which will force a return of the full group chat
*
* @apiSuccess data An array of <a href='https://github.com/HabitRPG/habitica/blob/develop/website/server/models/group.js#L51' target='_blank'>chat messages</a> if a new message was posted after previousMsg, otherwise the posted message
*
* @apiUse GroupNotFound
* @apiUse GroupIdRequired
* @apiError (400) {NotFound} ChatPriviledgesRevoked Your chat privileges have been revoked
@@ -143,7 +141,7 @@ api.postChat = {
if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.privacy !== 'private' && user.flags.chatRevoked) {
throw new NotFound('Your chat privileges have been revoked.');
throw new NotAuthorized(res.t('chatPrivilegesRevoked'));
}
if (group._id === TAVERN_ID && textContainsBannedWords(req.body.message)) {

View File

@@ -396,7 +396,7 @@ api.getGroup = {
* @apiUse groupIdRequired
* @apiUse GroupNotFound
*
* @apiPermission GroupLeader
* @apiPermission GroupLeader, Admin
*/
api.updateGroup = {
method: 'PUT',
@@ -409,11 +409,13 @@ api.updateGroup = {
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let optionalMembership = Boolean(user.contributor.admin);
let group = await Group.getGroup({user, groupId: req.params.groupId, optionalMembership});
let group = await Group.getGroup({user, groupId: req.params.groupId});
if (!group) throw new NotFound(res.t('groupNotFound'));
if (group.leader !== user._id) throw new NotAuthorized(res.t('messageGroupOnlyLeaderCanUpdate'));
if (group.leader !== user._id && group.type === 'party') throw new NotAuthorized(res.t('messageGroupOnlyLeaderCanUpdate'));
else if (group.leader !== user._id && !user.contributor.admin) throw new NotAuthorized(res.t('messageGroupOnlyLeaderCanUpdate'));
if (req.body.leader !== user._id && group.hasNotCancelled()) throw new NotAuthorized(res.t('cannotChangeLeaderWithActiveGroupPlan'));
@@ -472,7 +474,7 @@ api.joinGroup = {
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
// Works even if the user is not yet a member of the group
// Works even if the user is not yet a member of the group
let group = await Group.getGroup({user, groupId: req.params.groupId, optionalMembership: true}); // Do not fetch chat and work even if the user is not yet a member of the group
if (!group) throw new NotFound(res.t('groupNotFound'));
@@ -760,7 +762,7 @@ function _sendMessageToRemoved (group, removedUser, message, isInGroup) {
*
* @apiSuccess {Object} data An empty object
*
* @apiPermission GroupLeader
* @apiPermission GroupLeader, Admin
*
* @apiUse groupIdRequired
* @apiUse GroupNotFound
@@ -777,13 +779,18 @@ api.removeGroupMember = {
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let optionalMembership = Boolean(user.contributor.admin);
let group = await Group.getGroup({user, groupId: req.params.groupId, optionalMembership, fields: '-chat'}); // Do not fetch chat
let group = await Group.getGroup({user, groupId: req.params.groupId, fields: '-chat'}); // Do not fetch chat
if (!group) throw new NotFound(res.t('groupNotFound'));
let uuid = req.params.memberId;
if (group.leader !== user._id) throw new NotAuthorized(res.t('onlyLeaderCanRemoveMember'));
if (group.leader !== user._id && group.type === 'party') throw new NotAuthorized(res.t('onlyLeaderCanRemoveMember'));
if (group.leader !== user._id && !user.contributor.admin) throw new NotAuthorized(res.t('onlyLeaderCanRemoveMember'));
if (group.leader === uuid && user.contributor.admin) throw new NotAuthorized(res.t('cannotRemoveCurrentLeader'));
if (user._id === uuid) throw new NotAuthorized(res.t('memberCannotRemoveYourself'));
let member = await User.findOne({_id: uuid}).exec();
@@ -946,12 +953,12 @@ async function _inviteByEmail (invite, group, inviter, req, res) {
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},
{'auth.google.emails.value': invite.email},
{'auth.local.email': invite.email},
{'auth.facebook.emails.value': invite.email},
{'auth.google.emails.value': invite.email},
]})
.select({_id: true, 'preferences.emailNotifications': true})
.exec();
.select({_id: true, 'preferences.emailNotifications': true})
.exec();
if (userToContact) {
userReturnInfo = await _inviteByUUID(userToContact._id, group, inviter, req, res);

View File

@@ -6,22 +6,56 @@ import {
} from '../../libs/errors';
import _ from 'lodash';
/**
* @apiDefine Admin Moderators
* Contributors of tier 8 or higher can use this route.
*/
let api = {};
/**
* @api {get} /api/v3/hall/patrons Get all patrons
* @apiDescription Only the first 50 patrons are returned. More can be accessed passing ?page=n
* @apiDescription Returns an array of objects containing the patrons who backed Habitica's
* original kickstarter. The array is sorted by the backer tier in descending order.
* By default, only the first 50 patrons are returned. More can be accessed by passing ?page=n
* @apiName GetPatrons
* @apiGroup Hall
*
* @apiParam {Number} page Query Parameter - The result page. Default is 0
*
* @apiParam (Query) {Number} [page=0] The result page.
* @apiSuccess {Array} data An array of patrons
*
* @apiSuccessExample {json} Example response
* {
* "success": true,
* "data": [
* {
* "_id": "3adb52a9-0dfb-4752-81f2-a62d911d1bf5",
* "profile": {
* "name": "mattboch"
* },
* "contributor": {},
* "backer": {
* "tier": 800,
* "npc": "Beast Master"
* }
* },
* {
* "_id": "9da65443-ed43-4c21-804f-d260c1361596",
* "profile": {
* "name": "ʎǝlᴉɐq s,┴I"
* },
* "contributor": {
* "text": "Pollen Purveyor",
* "admin": true,
* "level": 8
* },
* "backer": {
* "npc": "Town Crier",
* "tier": 800,
* "tokensApplied": true
* }
* }
* ]
* }
*
*
* @apiUse NoAuthHeaders
* @apiUse NoAccount
*/
api.getPatrons = {
method: 'GET',
@@ -56,7 +90,32 @@ api.getPatrons = {
* @apiName GetHeroes
* @apiGroup Hall
*
* @apiSuccess {Array} data An array of heroes
* @apiDescription Returns an array of objects containing the heroes who have
* contributed for Habitica. The array is sorted by the contribution level in descending order.
*
* @apiSuccess {Array} heroes An array of heroes
*
* @apiSuccessExample {json} Example response:
* {
* "success": true,
* "data": [
* {
* "_id": "e6e01d2a-c2fa-4b9f-9c0f-7865b777e7b5",
* "profile": {
* "name": "test2"
* },
* "contributor": {
* "admin": false,
* "level": 2,
* "text": "Linguist"
* },
* "backer": {}
* }
* ]
* }
*
* @apiUse NoAuthHeaders
* @apiUse NoAccount
*/
api.getHeroes = {
method: 'GET',
@@ -83,14 +142,19 @@ const heroAdminFields = 'contributor balance profile.name purchased items auth f
/**
* @api {get} /api/v3/hall/heroes/:heroId Get any user ("hero") given the UUID
* @apiParam {UUID} heroId user ID
* @apiName GetHero
* @apiGroup Hall
* @apiPermission Admin
*
* @apiDescription Returns the profile of the given user
*
* @apiSuccess {Object} data The user object
*
* @apiPermission Admin
*
* @apiUse UserNotFound
* @apiUse NoAuthHeaders
* @apiUse NoAccount
* @apiUse NoUser
* @apiUse NotAdmin
*/
api.getHero = {
method: 'GET',
@@ -123,15 +187,35 @@ const gemsPerTier = {1: 3, 2: 3, 3: 3, 4: 4, 5: 4, 6: 4, 7: 4, 8: 0, 9: 0};
/**
* @api {put} /api/v3/hall/heroes/:heroId Update any user ("hero")
* @apiDescription Must be an admin to make this request.
* @apiParam {UUID} heroId user ID
* @apiName UpdateHero
* @apiGroup Hall
* @apiPermission Admin
*
* @apiDescription Update user's gem balance, contributions & contribution tier and admin status. Grant items, block / unblock user's account and revoke / unrevoke chat privileges.
*
* @apiExample Example Body:
* {
* "balance": 1000,
* "auth": {"blocked": false},
* "flags": {"chatRevoked": true},
* "purchased": {"ads": true},
* "contributor": {
* "admin": true,
* "contributions": "Improving API documentation",
* "level": 5,
* "text": "Scribe, Blacksmith"
* },
* "itemPath": "items.pets.BearCub-Skeleton",
* "itemVal": 1
* }
*
* @apiSuccess {Object} data The updated user object
*
* @apiPermission Admin
*
* @apiUse UserNotFound
* @apiUse NoAuthHeaders
* @apiUse NoAccount
* @apiUse NoUser
* @apiUse NotAdmin
*/
api.updateHero = {
method: 'PUT',

View File

@@ -4,6 +4,9 @@ import {
publicFields as memberFields,
nameFields,
} from '../../models/user';
import {
KNOWN_INTERACTIONS,
} from '../../models/user/methods';
import { model as Group } from '../../models/group';
import { model as Challenge } from '../../models/challenge';
import {
@@ -385,6 +388,39 @@ api.getChallengeMemberProgress = {
},
};
/**
* @api {get} /api/v3/members/:toUserId/objections/:interaction Get any objections that would occur if the given interaction was attempted - BETA
* @apiVersion 3.0.0
* @apiName GetObjectionsToInteraction
* @apiGroup Member
*
* @apiParam {UUID} toUserId The user to interact with
* @apiParam {String="send-private-message","transfer-gems"} interaction Name of the interaction to query
*
* @apiSuccess {Array} data Return an array of objections, if the interaction would be blocked; otherwise an empty array
*/
api.getObjectionsToInteraction = {
method: 'GET',
url: '/members/:toUserId/objections/:interaction',
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkParams('toUserId', res.t('toUserIDRequired')).notEmpty().isUUID();
req.checkParams('interaction', res.t('interactionRequired')).notEmpty().isIn(KNOWN_INTERACTIONS);
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let sender = res.locals.user;
let receiver = await User.findById(req.params.toUserId).exec();
if (!receiver) throw new NotFound(res.t('userWithIDNotFound', {userId: req.params.toUserId}));
let interaction = req.params.interaction;
let response = sender.getObjectionsToInteraction(interaction, receiver);
res.respond(200, response.map(res.t));
},
};
/**
* @api {posts} /api/v3/members/send-private-message Send a private message to a member
* @apiName SendPrivateMessage
@@ -410,17 +446,11 @@ api.sendPrivateMessage = {
let sender = res.locals.user;
let message = req.body.message;
let receiver = await User.findById(req.body.toUserId).exec();
if (!receiver) throw new NotFound(res.t('userNotFound'));
let userBlockedSender = receiver.inbox.blocks.indexOf(sender._id) !== -1;
let userIsBlockBySender = sender.inbox.blocks.indexOf(receiver._id) !== -1;
let userOptedOutOfMessaging = receiver.inbox.optOut;
if (userBlockedSender || userIsBlockBySender || userOptedOutOfMessaging) {
throw new NotAuthorized(res.t('notAuthorizedToSendMessageToThisUser'));
}
let objections = sender.getObjectionsToInteraction('send-private-message', receiver);
if (objections.length > 0) throw new NotAuthorized(res.t(objections[0]));
await sender.sendMessage(receiver, { receiverMsg: message });
@@ -472,13 +502,11 @@ api.transferGems = {
if (validationErrors) throw validationErrors;
let sender = res.locals.user;
let receiver = await User.findById(req.body.toUserId).exec();
if (!receiver) throw new NotFound(res.t('userNotFound'));
if (receiver._id === sender._id) {
throw new NotAuthorized(res.t('cannotSendGemsToYourself'));
}
let objections = sender.getObjectionsToInteraction('transfer-gems', receiver);
if (objections.length > 0) throw new NotAuthorized(res.t(objections[0]));
let gemAmount = req.body.gemAmount;
let amount = gemAmount / 4;

View File

@@ -39,7 +39,7 @@ function canStartQuestAutomatically (group) {
let api = {};
/**
* @api {post} /api/v3/groups/:groupId/quests/invite Invite users to a quest
* @api {post} /api/v3/groups/:groupId/quests/invite/:questKey Invite users to a quest
* @apiName InviteToQuest
* @apiGroup Quest
*

View File

@@ -107,8 +107,8 @@ api.getSeasonalShopItems = {
let resObject = {
identifier: 'seasonalShop',
text: res.t('seasonalShop'),
notes: res.t('seasonalShopClosedText'),
imageName: 'seasonalshop_closed',
notes: res.t('seasonalShopSummerText'),
imageName: 'seasonalshop_open',
categories: shops.getSeasonalShopCategories(user, req.language),
};

View File

@@ -333,7 +333,7 @@ api.deleteUser = {
await user.remove();
if (feedback) {
txnEmail(TECH_ASSISTANCE_EMAIL, 'admin-feedback', [
txnEmail({email: TECH_ASSISTANCE_EMAIL}, 'admin-feedback', [
{name: 'PROFILE_NAME', content: user.profile.name},
{name: 'UUID', content: user._id},
{name: 'EMAIL', content: getUserInfo(user, ['email']).email},
@@ -1037,8 +1037,8 @@ api.buySpecialSpell = {
*
* @apiParam {String} egg The egg to use
* @apiParam {String} hatchingPotion The hatching potion to use
* @apiParamExample {URL}
* /api/v3/user/hatch/Dragon/CottonCandyPink
* @apiParamExample {URL} Example-URL
* https://habitica.com/api/v3/user/hatch/Dragon/CottonCandyPink
*
* @apiSuccess {Object} data user.items
* @apiSuccess {String} message
@@ -1081,8 +1081,8 @@ api.hatch = {
* @apiParam {String="mount","pet","costume","equipped"} type The type of item to equip
* @apiParam {String} key The item to equip
*
* @apiParamExample {URL}
* /api/v3/user/equip/equipped/weapon_warrior_2
* @apiParamExample {URL} Example-URL
* https://habitica.com/api/v3/user/equip/equipped/weapon_warrior_2
*
* @apiSuccess {Object} data user.items
* @apiSuccess {String} message Optional success message for unequipping an items
@@ -1122,7 +1122,7 @@ api.equip = {
* @apiParam {String} pet
* @apiParam {String} food
*
* @apiParamExample {url}
* @apiParamExample {url} Example-URL
* https://habitica.com/api/v3/user/feed/Armadillo-Shade/Chocolate
*
* @apiSuccess {Number} data The pet value
@@ -1206,12 +1206,20 @@ api.disableClasses = {
* @apiName UserPurchase
* @apiGroup User
*
* @apiParam {String} type Type of item to purchase. Must be one of: gems, eggs, hatchingPotions, food, quests, or gear
* @apiParam {String="gems","eggs","hatchingPotions","premiumHatchingPotions",food","quests","gear"} type Type of item to purchase.
* @apiParam {String} key Item's key (use "gem" for purchasing gems)
*
* @apiSuccess {Object} data.items user.items
* @apiSuccess {Number} data.balance user.balance
* @apiSuccess {String} message Success message
*
* @apiError {NotAuthorized} NotAvailable Item is not available to be purchased (not unlocked for the user).
* @apiError {NotAuthorized} Gems Not enough gems
* @apiError {NotFound} Key Key not found for Content type.
* @apiError {NotFound} Type Type invalid.
*
* @apiErrorExample {json}
* {"success":false,"error":"NotAuthorized","message":"This item is not currently available for purchase."}
*/
api.purchase = {
method: 'POST',
@@ -1230,12 +1238,19 @@ api.purchase = {
* @apiName UserPurchaseHourglass
* @apiGroup User
*
* @apiParam {String} type The type of item to purchase (pets or mounts)
* @apiParam {String} key Ex: {MantisShrimp-Base}. The key for the mount/pet
* @apiParam {String="pets","mounts"} type The type of item to purchase
* @apiParam {String} key Ex: {Phoenix-Base}. The key for the mount/pet
*
* @apiSuccess {Object} data.items user.items
* @apiSuccess {Object} data.purchasedPlanConsecutive user.purchased.plan.consecutive
* @apiSuccess {String} message Success message
*
* @apiError {NotAuthorized} NotAvailable Item is not available to be purchased or is not valid.
* @apiError {NotAuthorized} Hourglasses User does not have enough Mystic Hourglasses.
* @apiError {NotFound} Type Type invalid.
*
* @apiErrorExample {json}
* {"success":false,"error":"NotAuthorized","message":"You don't have enough Mystic Hourglasses."}
*/
api.userPurchaseHourglass = {
method: 'POST',
@@ -1254,11 +1269,40 @@ api.userPurchaseHourglass = {
* @apiName UserReadCard
* @apiGroup User
*
* @apiParam {String} cardType Type of card to read
* @apiParam {String} cardType Type of card to read (e.g. - birthday, greeting, nye, thankyou, valentine)
*
* @apiSuccess {Object} data.specialItems user.items.special
* @apiSuccess {Boolean} data.cardReceived user.flags.cardReceived
* @apiSuccess {String} message Success message
*
* @apiSuccessExample {json}
* {
* "success": true,
* "data": {
* "specialItems": {
* "snowball": 0,
* "spookySparkles": 0,
* "shinySeed": 0,
* "seafoam": 0,
* "valentine": 0,
* "valentineReceived": [],
* "nye": 0,
* "nyeReceived": [],
* "greeting": 0,
* "greetingReceived": [
* "MadPink"
* ],
* "thankyou": 0,
* "thankyouReceived": [],
* "birthday": 0,
* "birthdayReceived": []
* },
* "cardReceived": false
* },
* "message": "valentine has been read"
* }
*
* @apiError {NotAuthorized} CardType Unknown card type.
*/
api.readCard = {
method: 'POST',
@@ -1279,6 +1323,28 @@ api.readCard = {
*
* @apiSuccess {Object} data The item obtained
* @apiSuccess {String} message Success message
*
* @apiSuccessExample {json}
* { "success": true,
* "data": {
* "mystery": "201612",
* "value": 0,
* "type": "armor",
* "key": "armor_mystery_201612",
* "set": "mystery-201612",
* "klass": "mystery",
* "index": "201612",
* "str": 0,
* "int": 0,
* "per": 0,
* "con": 0
* },
* "message": "Mystery item opened."
*
* @apiError {BadRequest} Empty No mystery items to open.
*
* @apiErrorExample {json}
* {"success":false,"error":"BadRequest","message":"Mystery items are empty"}
*/
api.userOpenMysteryItem = {
method: 'POST',
@@ -1298,6 +1364,19 @@ api.userOpenMysteryItem = {
*
* @apiSuccess {Object} data.items `user.items.pets`
* @apiSuccess {String} message Success message
*
* @apiSuccessExample {json}
* {
* "success": true,
* "data": {
* },
* "message": "Pets released"
* }
*
* @apiError {NotAuthorized} Not enough gems
*
* @apiErrorExample {json}
* {"success":false,"error":"NotAuthorized","message":"Not enough Gems"}
*/
api.userReleasePets = {
method: 'POST',
@@ -1315,11 +1394,38 @@ api.userReleasePets = {
* @api {post} /api/v3/user/release-both Release pets and mounts and grants Triad Bingo
* @apiName UserReleaseBoth
* @apiGroup User
*
* @apiSuccess {Object} data.achievements
* @apiSuccess {Object} data.items
* @apiSuccess {Number} data.balance
* @apiSuccess {String} message Success message
*
* @apiSuccessExample {json}
* {
* "success": true,
* "data": {
* "achievements": {
* "ultimateGearSets": {},
* "challenges": [],
* "quests": {},
* "perfect": 0,
* "beastMaster": true,
* "beastMasterCount": 1,
* "mountMasterCount": 1,
* "triadBingoCount": 1,
* "mountMaster": true,
* "triadBingo": true
* },
* "items": {}
* },
* "message": "Mounts and pets released"
* }
*
* @apiError {NotAuthorized} Not enough gems
*
* @apiErrorExample {json}
* {"success":false,"error":"NotAuthorized","message":"Not enough Gems"}
*/
api.userReleaseBoth = {
method: 'POST',
@@ -1340,6 +1446,22 @@ api.userReleaseBoth = {
*
* @apiSuccess {Object} data user.items.mounts
* @apiSuccess {String} message Success message
*
* @apiSuccessExample {json}
* {
* "success": true,
* "data": {
* },
* "items": {}
* },
* "message": "Mounts released"
* }
*
* @apiError {NotAuthorized} Not enough gems
*
* @apiErrorExample {json}
* {"success":false,"error":"NotAuthorized","message":"Not enough Gems"}
*
*/
api.userReleaseMounts = {
method: 'POST',
@@ -1358,12 +1480,17 @@ api.userReleaseMounts = {
* @apiName UserSell
* @apiGroup User
*
* @apiParam {String} type The type of item to sell. Must be one of: eggs, hatchingPotions, or food
* @apiParam {String="eggs","hatchingPotions","food"} type The type of item to sell.
* @apiParam {String} key The key of the item
*
* @apiSuccess {Object} data.stats
* @apiSuccess {Object} data.items
* @apiSuccess {String} message Success message
*
* @apiError {NotFound} InvalidKey Key not found for user.items eggs (either the key does not exist or the user has none in inventory)
* @apiError {NotAuthorized} InvalidType Type is not a valid type.
*
* @apiErrorExample {json}
* {"success":false,"error":"NotAuthorized","message":"Type is not sellable. Must be one of the following eggs, hatchingPotions, food"}
*/
api.userSell = {
method: 'POST',
@@ -1382,12 +1509,31 @@ api.userSell = {
* @apiName UserUnlock
* @apiGroup User
*
* @apiParam {String} path Query parameter. The path to unlock
* @apiParam {String} path Query parameter. Full path to unlock. See "content" API call for list of items.
*
* @apiParamExample {curl}
* curl -x POST http://habitica.com/api/v3/user/unlock?path=background.midnight_clouds
* curl -x POST http://habitica.com/api/v3/user/unlock?path=hair.color.midnight
*
* @apiSuccess {Object} data.purchased
* @apiSuccess {Object} data.items
* @apiSuccess {Object} data.preferences
* @apiSuccess {String} message
* @apiSuccess {String} message "Items have been unlocked"
*
* @apiSuccessExample {json}
* {
* "success": true,
* "data": {},
* "message": "Items have been unlocked"
* }
*
* @apiError {BadRequest} Path Path to unlock not specified
* @apiError {NotAuthorized} Gems Not enough gems available.
* @apiError {NotAuthorized} Unlocked Full set already unlocked.
*
* @apiErrorExample {json}
* {"success":false,"error":"BadRequest","message":"Path string is required"}
8 {"success":false,"error":"NotAuthorized","message":"Full set already unlocked."}
*/
api.userUnlock = {
method: 'POST',
@@ -1664,3 +1810,4 @@ api.setCustomDayStart = {
};
module.exports = api;