Merge branch 'develop' into party-chat-translations

# Conflicts:
#	website/server/controllers/api-v3/quests.js
#	website/server/controllers/api-v3/tasks/groups.js
#	website/server/controllers/api-v3/user/spells.js
#	website/server/models/group.js
This commit is contained in:
Mateus Etto
2018-04-25 21:15:49 +09:00
953 changed files with 37528 additions and 30517 deletions

View File

@@ -629,7 +629,7 @@ api.updateEmail = {
if (validationErrors) throw validationErrors;
let emailAlreadyInUse = await User.findOne({
'auth.local.email': req.body.newEmail,
'auth.local.email': req.body.newEmail.toLowerCase(),
}).select({_id: 1}).lean().exec();
if (emailAlreadyInUse) throw new NotAuthorized(res.t('cannotFulfillReq', { techAssistanceEmail: TECH_ASSISTANCE_EMAIL }));
@@ -643,7 +643,7 @@ api.updateEmail = {
await passwordUtils.convertToBcrypt(user, password);
}
user.auth.local.email = req.body.newEmail;
user.auth.local.email = req.body.newEmail.toLowerCase();
await user.save();
return res.respond(200, { email: user.auth.local.email });

View File

@@ -340,28 +340,72 @@ api.getUserChallenges = {
url: '/challenges/user',
middlewares: [authWithHeaders()],
async handler (req, res) {
let user = res.locals.user;
const CHALLENGES_PER_PAGE = 10;
const page = req.query.page;
const user = res.locals.user;
let orOptions = [
{_id: {$in: user.challenges}}, // Challenges where the user is participating
{leader: user._id}, // Challenges where I'm the leader
];
const owned = req.query.owned;
if (!owned) {
orOptions.push({leader: user._id});
}
if (!req.query.member) {
orOptions.push({
group: {$in: user.getGroups()},
}); // Challenges in groups where I'm a member
}
let challenges = await Challenge.find({
$or: orOptions,
})
.sort('-official -createdAt')
// see below why we're not using populate
// .populate('group', basicGroupFields)
// .populate('leader', nameFields)
.exec();
let query = {
$and: [{$or: orOptions}],
};
if (owned && owned === 'not_owned') {
query.$and.push({leader: {$ne: user._id}});
}
if (owned && owned === 'owned') {
query.$and.push({leader: user._id});
}
if (req.query.search) {
const searchOr = {$or: []};
const searchWords = _.escapeRegExp(req.query.search).split(' ').join('|');
const searchQuery = { $regex: new RegExp(`${searchWords}`, 'i') };
searchOr.$or.push({name: searchQuery});
searchOr.$or.push({description: searchQuery});
query.$and.push(searchOr);
}
if (req.query.categories) {
let categorySlugs = req.query.categories.split(',');
query.categories = { $elemMatch: { slug: {$in: categorySlugs} } };
}
let mongoQuery = Challenge.find(query)
.sort('-createdAt');
if (page) {
mongoQuery = mongoQuery
.limit(CHALLENGES_PER_PAGE)
.skip(CHALLENGES_PER_PAGE * page);
}
// see below why we're not using populate
// .populate('group', basicGroupFields)
// .populate('leader', nameFields)
const challenges = await mongoQuery.exec();
let resChals = challenges.map(challenge => challenge.toJSON());
resChals = _.orderBy(resChals, [challenge => {
return challenge.categories.map(category => category.slug).includes('habitica_official');
}], ['desc']);
// Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833
await Promise.all(resChals.map((chal, index) => {
return Promise.all([
@@ -412,11 +456,16 @@ api.getGroupChallenges = {
if (!group) throw new NotFound(res.t('groupNotFound'));
let challenges = await Challenge.find({group: groupId})
.sort('-official -createdAt')
.sort('-createdAt')
// .populate('leader', nameFields) // Only populate the leader as the group is implicit
.exec();
let resChals = challenges.map(challenge => challenge.toJSON());
resChals = _.orderBy(resChals, [challenge => {
return challenge.categories.map(category => category.slug).includes('habitica_official');
}], ['desc']);
// Instead of populate we make a find call manually because of https://github.com/Automattic/mongoose/issues/3833
await Promise.all(resChals.map((chal, index) => {
return User

View File

@@ -1,12 +1,12 @@
import { authWithHeaders } from '../../middlewares/auth';
import { model as Group } from '../../models/group';
import { model as User } from '../../models/user';
import { model as Chat } from '../../models/chat';
import {
BadRequest,
NotFound,
NotAuthorized,
} from '../../libs/errors';
import _ from 'lodash';
import { removeFromArray } from '../../libs/collectionManipulators';
import { getUserInfo, getGroupUrl, sendTxn } from '../../libs/email';
import slack from '../../libs/slack';
@@ -70,10 +70,12 @@ api.getChat = {
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let group = await Group.getGroup({user, groupId: req.params.groupId, fields: 'chat'});
const groupId = req.params.groupId;
let group = await Group.getGroup({user, groupId, fields: 'chat'});
if (!group) throw new NotFound(res.t('groupNotFound'));
res.respond(200, Group.toJSONCleanChat(group, user).chat);
const groupChat = await Group.toJSONCleanChat(group, user);
res.respond(200, groupChat.chat);
},
};
@@ -160,42 +162,39 @@ api.postChat = {
if (group.privacy !== 'private' && !guildsAllowingBannedWords[group._id]) {
let matchedBadWords = getBannedWordsFromText(req.body.message);
if (matchedBadWords.length > 0) {
// @TODO replace this split mechanism with something that works properly in translations
let message = res.t('bannedWordUsed').split('.');
message[0] += ` (${matchedBadWords.join(', ')})`;
throw new BadRequest(message.join('.'));
throw new BadRequest(res.t('bannedWordUsed', {swearWordsUsed: matchedBadWords.join(', ')}));
}
}
let lastClientMsg = req.query.previousMsg;
const chatRes = await Group.toJSONCleanChat(group, user);
const lastClientMsg = req.query.previousMsg;
chatUpdated = lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg ? true : false;
if (group.checkChatSpam(user)) {
throw new NotAuthorized(res.t('messageGroupChatSpam'));
}
let newChatMessage = group.sendChat(req.body.message, user);
let toSave = [group.save()];
const newChatMessage = group.sendChat(req.body.message, user);
let toSave = [newChatMessage.save()];
if (group.type === 'party') {
user.party.lastMessageSeen = group.chat[0].id;
user.party.lastMessageSeen = newChatMessage.id;
toSave.push(user.save());
}
let [savedGroup] = await Promise.all(toSave);
await Promise.all(toSave);
// realtime chat is only enabled for private groups (for now only for parties)
if (savedGroup.privacy === 'private' && savedGroup.type === 'party') {
// @TODO: rethink if we want real-time
if (group.privacy === 'private' && group.type === 'party') {
// req.body.pusherSocketId is sent from official clients to identify the sender user's real time socket
// see https://pusher.com/docs/server_api_guide/server_excluding_recipients
pusher.trigger(`presencegroup${savedGroup._id}`, 'newchat', newChatMessage, req.body.pusherSocketId);
pusher.trigger(`presence-group-${group._id}`, 'new-chat', newChatMessage, req.body.pusherSocketId);
}
if (chatUpdated) {
res.respond(200, {chat: Group.toJSONCleanChat(savedGroup, user).chat});
res.respond(200, {chat: chatRes.chat});
} else {
res.respond(200, {message: savedGroup.chat[0]});
res.respond(200, {message: newChatMessage});
}
group.sendGroupChatReceivedWebhooks(newChatMessage);
@@ -236,22 +235,16 @@ api.likeChat = {
let group = await Group.getGroup({user, groupId});
if (!group) throw new NotFound(res.t('groupNotFound'));
let message = _.find(group.chat, {id: req.params.chatId});
let message = await Chat.findOne({id: req.params.chatId}).exec();
if (!message) throw new NotFound(res.t('messageGroupChatNotFound'));
// TODO correct this error type
// @TODO correct this error type
if (message.uuid === user._id) throw new NotFound(res.t('messageGroupChatLikeOwnMessage'));
let update = {$set: {}};
if (!message.likes) message.likes = {};
message.likes[user._id] = !message.likes[user._id];
update.$set[`chat.$.likes.${user._id}`] = message.likes[user._id];
message.markModified('likes');
await message.save();
await Group.update(
{_id: group._id, 'chat.id': message.id},
update
).exec();
res.respond(200, message); // TODO what if the message is flagged and shouldn't be returned?
},
};
@@ -337,15 +330,11 @@ api.clearChatFlags = {
});
if (!group) throw new NotFound(res.t('groupNotFound'));
let message = _.find(group.chat, {id: chatId});
let message = await Chat.findOne({id: chatId}).exec();
if (!message) throw new NotFound(res.t('messageGroupChatNotFound'));
message.flagCount = 0;
await Group.update(
{_id: group._id, 'chat.id': message.id},
{$set: {'chat.$.flagCount': message.flagCount}}
).exec();
await message.save();
let adminEmailContent = getUserInfo(user, ['email']).email;
let authorEmail = getAuthorEmailFromMessage(message);
@@ -469,25 +458,22 @@ api.deleteChat = {
let group = await Group.getGroup({user, groupId, fields: 'chat'});
if (!group) throw new NotFound(res.t('groupNotFound'));
let message = _.find(group.chat, {id: chatId});
let message = await Chat.findOne({id: chatId}).exec();
if (!message) throw new NotFound(res.t('messageGroupChatNotFound'));
if (user._id !== message.uuid && !user.contributor.admin) {
throw new NotAuthorized(res.t('onlyCreatorOrAdminCanDeleteChat'));
}
let lastClientMsg = req.query.previousMsg;
let chatUpdated = lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg ? true : false;
const chatRes = await Group.toJSONCleanChat(group, user);
const lastClientMsg = req.query.previousMsg;
const chatUpdated = lastClientMsg && group.chat && group.chat[0] && group.chat[0].id !== lastClientMsg ? true : false;
await Group.update(
{_id: group._id},
{$pull: {chat: {id: chatId}}}
).exec();
await Chat.remove({_id: message._id}).exec();
if (chatUpdated) {
let chatRes = Group.toJSONCleanChat(group, user).chat;
removeFromArray(chatRes, {id: chatId});
res.respond(200, chatRes);
removeFromArray(chatRes.chat, {id: chatId});
res.respond(200, chatRes.chat);
} else {
res.respond(200, {});
}

View File

@@ -21,9 +21,9 @@ import { encrypt } from '../../libs/encryption';
import { sendNotification as sendPushNotification } from '../../libs/pushNotifications';
import pusher from '../../libs/pusher';
import common from '../../../common';
import payments from '../../libs/payments';
import stripePayments from '../../libs/stripePayments';
import amzLib from '../../libs/amazonPayments';
import payments from '../../libs/payments/payments';
import stripePayments from '../../libs/payments/stripe';
import amzLib from '../../libs/payments/amazon';
import shared from '../../../common';
import apiMessages from '../../libs/apiMessages';
@@ -78,9 +78,10 @@ let api = {};
* "privacy": "private"
* }
*
* @apiError (400) {NotAuthorized} messageInsufficientGems User does not have enough gems (4)
* @apiError (400) {NotAuthorized} partyMustbePrivate Party must have privacy set to private
* @apiError (400) {NotAuthorized} messageGroupAlreadyInParty
* @apiError (401) {NotAuthorized} messageInsufficientGems User does not have enough gems (4)
* @apiError (401) {NotAuthorized} partyMustbePrivate Party must have privacy set to private
* @apiError (401) {NotAuthorized} messageGroupAlreadyInParty
* @apiError (401) {NotAuthorized} cannotCreatePublicGuildWhenMuted You cannot create a public guild because your chat privileges have been revoked.
*
* @apiSuccess (201) {Object} data The created group (See <a href="https://github.com/HabitRPG/habitica/blob/develop/website/server/models/group.js" target="_blank">/website/server/models/group.js</a>)
*
@@ -115,6 +116,7 @@ api.createGroup = {
group.leader = user._id;
if (group.type === 'guild') {
if (group.privacy === 'public' && user.flags.chatRevoked) throw new NotAuthorized(res.t('cannotCreatePublicGuildWhenMuted'));
if (user.balance < 1) throw new NotAuthorized(res.t('messageInsufficientGems'));
group.balance = 1;
@@ -336,7 +338,7 @@ api.getGroups = {
if (req.query.search) {
filters.$or = [];
const searchWords = req.query.search.split(' ').join('|');
const searchWords = _.escapeRegExp(req.query.search).split(' ').join('|');
const searchQuery = { $regex: new RegExp(`${searchWords}`, 'i') };
filters.$or.push({name: searchQuery});
filters.$or.push({description: searchQuery});
@@ -390,7 +392,7 @@ api.getGroup = {
throw new NotFound(res.t('groupNotFound'));
}
let groupJson = Group.toJSONCleanChat(group, user);
let groupJson = await Group.toJSONCleanChat(group, user);
if (groupJson.leader === user._id) {
groupJson.purchased.plan = group.purchased.plan.toObject();
@@ -454,7 +456,7 @@ api.updateGroup = {
_.assign(group, _.merge(group.toObject(), Group.sanitizeUpdate(req.body)));
let savedGroup = await group.save();
let response = Group.toJSONCleanChat(savedGroup, user);
let response = await Group.toJSONCleanChat(savedGroup, user);
// If the leader changed fetch new data, otherwise use authenticated user
if (response.leader !== user._id) {
@@ -518,6 +520,18 @@ api.joinGroup = {
if (inviterParty) {
inviter = inviterParty.inviter;
// If user was in a different party (when partying solo you can be invited to a new party)
// make them leave that party before doing anything
if (user.party._id) {
let userPreviousParty = await Group.getGroup({user, groupId: user.party._id});
if (userPreviousParty.memberCount === 1 && user.party.quest.key) {
throw new NotAuthorized(res.t('messageCannotLeaveWhileQuesting'));
}
if (userPreviousParty) await userPreviousParty.leave(user);
}
// Clear all invitations of new user
user.invitations.parties = [];
user.invitations.party = {};
@@ -530,13 +544,6 @@ api.joinGroup = {
group.markModified('quest.members');
}
// If user was in a different party (when partying solo you can be invited to a new party)
// make them leave that party before doing anything
if (user.party._id) {
let userPreviousParty = await Group.getGroup({user, groupId: user.party._id});
if (userPreviousParty) await userPreviousParty.leave(user);
}
user.party._id = group._id; // Set group as user's party
isUserInvited = true;
@@ -554,7 +561,7 @@ api.joinGroup = {
if (isUserInvited && group.type === 'guild') {
if (user.guilds.indexOf(group._id) !== -1) { // if user is already a member (party is checked previously)
throw new NotAuthorized(res.t('userAlreadyInGroup'));
throw new NotAuthorized(res.t('youAreAlreadyInGroup'));
}
user.guilds.push(group._id); // Add group to user's guilds
if (!user.achievements.joinedGuild) {
@@ -618,7 +625,7 @@ api.joinGroup = {
promises = await Promise.all(promises);
let response = Group.toJSONCleanChat(promises[0], user);
let response = await Group.toJSONCleanChat(promises[0], user);
let leader = await User.findById(response.leader).select(nameFields).exec();
if (leader) {
response.leader = leader.toJSON({minimize: true});
@@ -1133,6 +1140,7 @@ async function _inviteByEmail (invite, group, inviter, req, res) {
*
* @apiError (401) {NotAuthorized} UserAlreadyInvited The user has already been invited to the group.
* @apiError (401) {NotAuthorized} UserAlreadyInGroup The user is already a member of the group.
* @apiError (401) {NotAuthorized} CannotInviteWhenMuted You cannot invite anyone to a guild or party because your chat privileges have been revoked.
*
* @apiUse GroupNotFound
* @apiUse UserNotFound
@@ -1145,6 +1153,8 @@ api.inviteToGroup = {
async handler (req, res) {
let user = res.locals.user;
if (user.flags.chatRevoked) throw new NotAuthorized(res.t('cannotInviteWhenMuted'));
req.checkParams('groupId', res.t('groupIdRequired')).notEmpty();
if (user.invitesSent >= MAX_EMAIL_INVITES_BY_USER) throw new NotAuthorized(res.t('inviteLimitReached', { techAssistanceEmail: TECH_ASSISTANCE_EMAIL }));

View File

@@ -60,7 +60,9 @@ let api = {};
api.getPatrons = {
method: 'GET',
url: '/hall/patrons',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
req.checkQuery('page', res.t('pageMustBeNumber')).optional().isNumeric();
@@ -120,7 +122,9 @@ api.getPatrons = {
api.getHeroes = {
method: 'GET',
url: '/hall/heroes',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
let heroes = await User
.find({

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 = 'KEYS TO THE KENNELS AND USE CASE SPOTLIGHT';
const LAST_ANNOUNCEMENT_TITLE = 'BLOG: USING HABITICA TO MAKE A DIFFERENCE, AND VIDEO GAMES BEHIND THE SCENES!';
const worldDmg = { // @TODO
bailey: false,
};
@@ -32,25 +32,20 @@ api.getNews = {
<h1 class="align-self-center">${res.t('newStuff')}</h1>
</div>
</div>
<h2>3/15/2018 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
<h2>4/19/2018 - ${LAST_ANNOUNCEMENT_TITLE}</h2>
<hr/>
<div class="scene_positivity center-block"></div>
<h3>Use Case Spotlight: Making a Difference</h3>
<p>This month's <a href='https://habitica.wordpress.com/2018/04/12/use-case-spotlight-making-a-difference/' target='_blank'>Use Case Spotlight</a> is about Making a Difference! It features a number of great suggestions submitted by Habiticans in the <a href='/groups/guild/1d3a10bf-60aa-4806-a38b-82d1084a59e6' target='_blank'>Use Case Spotlights Guild</a>. We hope it helps any of you who might be working to make a positive difference!</p>
<p>Plus, we're collecting user submissions for the next spotlight! How do you use Habitica to manage your Mental Health and Wellness? Well be featuring player-submitted examples in Use Case Spotlights on the Habitica Blog next month, so post your suggestions in the Use Case Spotlight Guild now. We look forward to learning more about how you use Habitica to improve your life and get things done!</p>
<div class="small mb-3">by Beffymaroo</div>
<div class="media align-items-center">
<div class="media-body">
<h3>Release Pets & Mounts!</h3>
<p>The Keys to the Kennels have returned! Now, when you collect all 90 standard pets or mounts, you can release them for 4 Gems, letting you collect them all over again! If you want a real challenge, you can attain the elusive Triad Bingo by filling your stable with all of both, then set them all free at once for 6 Gems!</p>
</div>
<div class="pet_key ml-3"></div>
</div>
<p>Scroll to the bottom of <a href='/shops/market' target='_blank'>the Market</a> to purchase a Key. It takes effect immediately on purchase, so say your goodbyes first!</p>
<div class="small mb-3">by TheHollidayInn, Apollo, Lemoness, deilann, and Megan</div>
<div class="media align-items-center">
<div class="scene_sweeping mr-3"></div>
<div class="media-body">
<h3>Use Case Spotlight: Spring Cleaning</h3>
<p>This month's <a href='https://habitica.wordpress.com/2018/03/15/use-case-spotlight-spring-cleaning/' target='_blank'>Use Case Spotlight</a> is about Spring Cleaning! It features a number of great suggestions submitted by Habiticans in the <a href='/groups/guild/1d3a10bf-60aa-4806-a38b-82d1084a59e6' target='_blank'>Use Case Spotlights Guild</a>. We hope it helps any of you who might be looking to start spring with a nice, clean dwelling.</p>
<p>Plus, we're collecting user submissions for the next spotlight! How do you use Habitica to Make a Difference? Well be featuring player-submitted examples in Use Case Spotlights on the Habitica Blog next month, so post your suggestions in the Use Case Spotlight Guild now. We look forward to learning more about how you use Habitica to improve your life and get things done!</p>
<div class="small mb-3">by Beffymaroo</div>
<h3>Behind the Scenes: What We're Playing</h3>
<p>Like many of you Habiticans out there, our team loves video and mobile games, and in this special post we wanted to share what we're currently playing (besides Habitica, of course!) and what we love about these games. Come check them out in <a href='https://habitica.wordpress.com/2018/04/19/behind-the-scenes-what-were-playing/' target='_blank'>this month's Behind the Scenes feature</a>!</p>
<div class="small mb-3">by Beffymaroo and the Habitica Staff</div>
</div>
<div class="scene_video_games ml-3"></div>
</div>
</div>
`,

View File

@@ -23,7 +23,9 @@ let api = {};
api.readNotification = {
method: 'POST',
url: '/notifications/:notificationId/read',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
let user = res.locals.user;
@@ -65,7 +67,9 @@ api.readNotification = {
api.readNotifications = {
method: 'POST',
url: '/notifications/read',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
let user = res.locals.user;
@@ -113,7 +117,9 @@ api.readNotifications = {
api.seeNotification = {
method: 'POST',
url: '/notifications/:notificationId/see',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
let user = res.locals.user;
@@ -162,7 +168,9 @@ api.seeNotification = {
api.seeNotifications = {
method: 'POST',
url: '/notifications/see',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
let user = res.locals.user;

View File

@@ -21,7 +21,9 @@ let api = {};
api.addPushDevice = {
method: 'POST',
url: '/user/push-devices',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
let user = res.locals.user;
@@ -64,7 +66,9 @@ api.addPushDevice = {
api.removePushDevice = {
method: 'DELETE',
url: '/user/push-devices/:regId',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
let user = res.locals.user;

View File

@@ -421,11 +421,12 @@ api.abortQuest = {
if (user._id !== group.leader && user._id !== group.quest.leader) throw new NotAuthorized(res.t('onlyLeaderAbortQuest'));
let questName = questScrolls[group.quest.key].text('en');
group.sendChat(`\`${common.i18n.t('chatQuestAborted', {username: user.profile.name, questName}, 'en')}\``, null, null, {
const newChatMessage = group.sendChat(`\`${common.i18n.t('chatQuestAborted', {username: user.profile.name, questName}, 'en')}\``, null, null, {
type: 'quest_abort',
user: user.profile.name,
quest: group.quest.key,
});
await newChatMessage.save();
let memberUpdates = User.update({
'party._id': groupId,

View File

@@ -15,7 +15,9 @@ let api = {};
api.getMarketItems = {
method: 'GET',
url: '/shops/market',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
let user = res.locals.user;
@@ -36,7 +38,9 @@ api.getMarketItems = {
api.getMarketGear = {
method: 'GET',
url: '/shops/market-gear',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
let user = res.locals.user;
@@ -60,7 +64,9 @@ api.getMarketGear = {
api.getQuestShopItems = {
method: 'GET',
url: '/shops/quests',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
let user = res.locals.user;
@@ -82,7 +88,9 @@ api.getQuestShopItems = {
api.getTimeTravelerShopItems = {
method: 'GET',
url: '/shops/time-travelers',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
let user = res.locals.user;
@@ -104,7 +112,9 @@ api.getTimeTravelerShopItems = {
api.getSeasonalShopItems = {
method: 'GET',
url: '/shops/seasonal',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
let user = res.locals.user;
@@ -126,7 +136,9 @@ api.getSeasonalShopItems = {
api.getBackgroundShopItems = {
method: 'GET',
url: '/shops/backgrounds',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
let user = res.locals.user;

View File

@@ -38,7 +38,9 @@ let api = {};
api.createTag = {
method: 'POST',
url: '/tags',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
let user = res.locals.user;
@@ -64,7 +66,9 @@ api.createTag = {
api.getTags = {
method: 'GET',
url: '/tags',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
let user = res.locals.user;
res.respond(200, user.tags);
@@ -89,7 +93,9 @@ api.getTags = {
api.getTag = {
method: 'GET',
url: '/tags/:tagId',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
let user = res.locals.user;
@@ -126,7 +132,9 @@ api.getTag = {
api.updateTag = {
method: 'PUT',
url: '/tags/:tagId',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
let user = res.locals.user;
@@ -168,7 +176,9 @@ api.updateTag = {
api.reorderTags = {
method: 'POST',
url: '/reorder-tags',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
let user = res.locals.user;
@@ -207,7 +217,9 @@ api.reorderTags = {
api.deleteTag = {
method: 'DELETE',
url: '/tags/:tagId',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
async handler (req, res) {
let user = res.locals.user;

View File

@@ -270,6 +270,7 @@ api.createChallengeTasks = {
* @apiGroup Task
*
* @apiParam (Query) {String="habits","dailys","todos","rewards","completedTodos"} type Optional query parameter to return just a type of tasks. By default all types will be returned except completed todos that must be requested separately. The "completedTodos" type returns only the 30 most recently completed.
* @apiParam (Query) [dueDate]
*
* @apiSuccess {Array} data An array of tasks
*
@@ -457,7 +458,6 @@ api.updateTask = {
let oldCheckList = task.checklist;
// we have to convert task to an object because otherwise things don't get merged correctly. Bad for performances?
let [updatedTaskObj] = common.ops.updateTask(task.toObject(), req);
// Sanitize differently user tasks linked to a challenge
let sanitizedObj;
@@ -470,6 +470,7 @@ api.updateTask = {
}
_.assign(task, sanitizedObj);
// console.log(task.modifiedPaths(), task.toObject().repeat === tep)
// repeat is always among modifiedPaths because mongoose changes the other of the keys when using .toObject()
// see https://github.com/Automattic/mongoose/issues/2749
@@ -480,7 +481,6 @@ api.updateTask = {
}
setNextDue(task, user);
let savedTask = await task.save();
if (group && task.group.id && task.group.assignedUsers.length > 0) {

View File

@@ -195,17 +195,19 @@ api.assignTask = {
if (canNotEditTasks(group, user, assignedUserId)) throw new NotAuthorized(res.t('onlyGroupLeaderCanEditTasks'));
let promises = [];
// User is claiming the task
if (user._id === assignedUserId) {
let message = res.t('userIsClamingTask', {username: user.profile.name, task: task.text});
group.sendChat(message, null, null, {
const newMessage = group.sendChat(message, null, null, {
type: 'claim_task',
user: user.profile.name,
task: task.text,
});
promises.push(newMessage.save());
}
let promises = [];
promises.push(group.syncTask(task, assignedUser));
promises.push(group.save());
await Promise.all(promises);

View File

@@ -55,6 +55,11 @@ let api = {};
* Tags
* TasksOrder (list of all ids for dailys, habits, rewards and todos)
*
* @apiParam (Query) {UUID} userFields A list of comma separated user fields to be returned instead of the entire document. Notifications are always returned.
*
* @apiExample {curl} Example use:
* curl -i https://habitica.com/api/v3/user?userFields=achievements,items.mounts
*
* @apiSuccess {Object} data The user object
*
* @apiSuccessExample {json} Result:
@@ -1764,4 +1769,68 @@ api.togglePinnedItem = {
},
};
/**
* @api {post} /api/v3/user/move-pinned-item/:type/:path/move/to/:position Move a pinned item in the rewards column to a new position after being sorted
* @apiName MovePinnedItem
* @apiGroup User
*
* @apiParam (Path) {String} path The unique item path used for pinning
* @apiParam (Path) {Number} position Where to move the task. 0 = top of the list. -1 = bottom of the list. (-1 means push to bottom). First position is 0
*
* @apiSuccess {Array} data The new pinned items order.
*
* @apiSuccessExample {json}
* {"success":true,"data":{"path":"quests.mayhemMistiflying3","type":"quests","_id": "5a32d357232feb3bc94c2bdf"},"notifications":[]}
*
* @apiUse TaskNotFound
*/
api.movePinnedItem = {
method: 'POST',
url: '/user/move-pinned-item/:path/move/to/:position',
middlewares: [authWithHeaders()],
async handler (req, res) {
req.checkParams('path', res.t('taskIdRequired')).notEmpty();
req.checkParams('position', res.t('positionRequired')).notEmpty().isNumeric();
let validationErrors = req.validationErrors();
if (validationErrors) throw validationErrors;
let user = res.locals.user;
let path = req.params.path;
let position = Number(req.params.position);
// If something has been added or removed from the inAppRewards, we need
// to reset pinnedItemsOrder to have the correct length. Since inAppRewards
// Uses the current pinnedItemsOrder to return these in the right order,
// the new reset array will be in the right order before we do the swap
let currentPinnedItems = common.inAppRewards(user);
if (user.pinnedItemsOrder.length !== currentPinnedItems.length) {
user.pinnedItemsOrder = currentPinnedItems.map(item => item.path);
}
// Adjust the order
let currentIndex = user.pinnedItemsOrder.findIndex(item => item === path);
let currentPinnedItemPath = user.pinnedItemsOrder[currentIndex];
if (currentIndex === -1) {
throw new BadRequest(res.t('wrongItemPath', req.language));
}
// Remove the one we will move
user.pinnedItemsOrder.splice(currentIndex, 1);
// reinsert the item in position (or just at the end)
if (position === -1) {
user.pinnedItemsOrder.push(currentPinnedItemPath);
} else {
user.pinnedItemsOrder.splice(position, 0, currentPinnedItemPath);
}
await user.save();
let userJson = user.toJSON();
res.respond(200, userJson.pinnedItemsOrder);
},
};
module.exports = api;

View File

@@ -130,22 +130,23 @@ api.castSpell = {
if (party && !spell.silent) {
if (targetType === 'user') {
party.sendChat(`\`${common.i18n.t('chatCastSpellUser', {username: user.profile.name, spell: spell.text(), target: partyMembers.profile.name}, 'en')}\``, null, null, {
const newChatMessage = party.sendChat(`\`${common.i18n.t('chatCastSpellUser', {username: user.profile.name, spell: spell.text(), target: partyMembers.profile.name}, 'en')}\``, null, null, {
type: 'spell_cast_user',
user: user.profile.name,
class: klass,
spell: spellId,
target: partyMembers.profile.name,
});
await newChatMessage.save();
} else {
party.sendChat(`\`${common.i18n.t('chatCastSpellParty', {username: user.profile.name, spell: spell.text()}, 'en')}\``, null, null, {
const newChatMessage = party.sendChat(`\`${common.i18n.t('chatCastSpellParty', {username: user.profile.name, spell: spell.text()}, 'en')}\``, null, null, {
type: 'spell_cast_party',
user: user.profile.name,
class: klass,
spell: spellId,
});
await newChatMessage.save();
}
await party.save();
}
}
},

View File

@@ -73,7 +73,9 @@ let api = {};
*/
api.addWebhook = {
method: 'POST',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
url: '/user/webhook',
async handler (req, res) {
let user = res.locals.user;
@@ -133,7 +135,9 @@ api.addWebhook = {
*/
api.updateWebhook = {
method: 'PUT',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
url: '/user/webhook/:id',
async handler (req, res) {
let user = res.locals.user;
@@ -184,7 +188,9 @@ api.updateWebhook = {
*/
api.deleteWebhook = {
method: 'DELETE',
middlewares: [authWithHeaders()],
middlewares: [authWithHeaders({
userFieldsToExclude: ['inbox'],
})],
url: '/user/webhook/:id',
async handler (req, res) {
let user = res.locals.user;

View File

@@ -24,9 +24,10 @@ async function getWorldBoss () {
*
* @apiSuccess {Object} data.worldBoss.active Boolean, true if world boss quest is underway
* @apiSuccess {Object} data.worldBoss.extra.worldDmg Object with NPC names as Boolean properties, true if they are affected by Rage Strike
* @apiSuccess {Object} data.worldBoss.key Quest content key for the world boss
* @apiSuccess {Object} data.worldBoss.progress.hp Current Health of the world boss
* @apiSuccess {Object} data.worldBoss.progress.rage Current Rage of the world boss
* @apiSuccess {Object} data.worldBoss.key String, Quest content key for the world boss
* @apiSuccess {Object} data.worldBoss.progress.hp Number, Current Health of the world boss
* @apiSuccess {Object} data.worldBoss.progress.rage Number, Current Rage of the world boss
* @apiSuccess {Object} data.npcImageSuffix String, trailing component of NPC image filenames
*
*/
api.getWorldState = {
@@ -36,6 +37,7 @@ api.getWorldState = {
let worldState = {};
worldState.worldBoss = await getWorldBoss();
worldState.npcImageSuffix = 'spring';
res.respond(200, worldState);
},