From 9d473cc92eb5457b8fb95d858518b963182e09de Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Thu, 25 Apr 2019 22:41:43 +0200 Subject: [PATCH 01/10] wip: fix setting (some) items values in the hall of heroes (#11133) --- test/api/unit/libs/items/utils.test.js | 46 +++++++++++++++++++++++ website/server/controllers/api-v3/hall.js | 7 +++- website/server/libs/items/utils.js | 20 ++++++++++ 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/test/api/unit/libs/items/utils.test.js b/test/api/unit/libs/items/utils.test.js index 029d26a07c..274dc665fe 100644 --- a/test/api/unit/libs/items/utils.test.js +++ b/test/api/unit/libs/items/utils.test.js @@ -2,6 +2,7 @@ import { validateItemPath, getDefaultOwnedGear, + castItemVal, } from '../../../../../website/server/libs/items/utils'; describe('Items Utils', () => { @@ -64,4 +65,49 @@ describe('Items Utils', () => { expect(validateItemPath('items.quests.invalid')).to.equal(false); }); }); + + describe('castItemVal', () => { + it('returns the item val untouched if not an item path', () => { + expect(castItemVal('notitems.gear.owned.item', 'a string')).to.equal('a string'); + }); + + it('returns the item val untouched if an unsupported path', () => { + expect(validateItemPath('items.gear.equipped.weapon', 'a string')).to.equal('a string'); + expect(validateItemPath('items.currentPet', 'a string')).to.equal('a string'); + expect(validateItemPath('items.special.snowball', 'a string')).to.equal('a string'); + }); + + it('converts values for pets paths to numbers', () => { + expect(validateItemPath('items.pets.Wolf-CottonCandyPink', '5')).to.equal(5); + expect(validateItemPath('items.pets.Wolf-Invalid', '5')).to.equal(5); + }); + + it('converts values for eggs paths to numbers', () => { + expect(validateItemPath('items.eggs.LionCub', '5')).to.equal(5); + expect(validateItemPath('items.eggs.Armadillo', '5')).to.equal(5); + expect(validateItemPath('items.eggs.NotAnArmadillo', '5')).to.equal(5); + }); + + it('converts values for hatching potions paths to numbers', () => { + expect(validateItemPath('items.hatchingPotions.Base', '5')).to.equal(5); + expect(validateItemPath('items.hatchingPotions.StarryNight', '5')).to.equal(5); + expect(validateItemPath('items.hatchingPotions.Invalid', '5')).to.equal(5); + }); + + it('converts values for food paths to numbers', () => { + expect(validateItemPath('items.food.Cake_Base', '5')).to.equal(5); + expect(validateItemPath('items.food.Cake_Invalid', '5')).to.equal(5); + }); + + it('converts values for mounts paths to numbers', () => { + expect(validateItemPath('items.mounts.Cactus-Base', '5')).to.equal(5); + expect(validateItemPath('items.mounts.Aether-Invisible', '5')).to.equal(5); + expect(validateItemPath('items.mounts.Aether-Invalid', '5')).to.equal(5); + }); + + it('converts values for quests paths to numbers', () => { + expect(validateItemPath('items.quests.atom3', '5')).to.equal(5); + expect(validateItemPath('items.quests.invalid', '5')).to.equal(5); + }); + }); }); diff --git a/website/server/controllers/api-v3/hall.js b/website/server/controllers/api-v3/hall.js index 5309b56e4f..a8ac1456f0 100644 --- a/website/server/controllers/api-v3/hall.js +++ b/website/server/controllers/api-v3/hall.js @@ -7,7 +7,10 @@ import { import _ from 'lodash'; import apiError from '../../libs/apiError'; import validator from 'validator'; -import { validateItemPath } from '../../libs/items/utils'; +import { + validateItemPath, + castItemVal, +} from '../../libs/items/utils'; let api = {}; @@ -271,7 +274,7 @@ api.updateHero = { hero.markModified('items.pets'); } if (updateData.itemPath && updateData.itemVal && validateItemPath(updateData.itemPath)) { - _.set(hero, updateData.itemPath, updateData.itemVal); // Sanitization at 5c30944 (deemed unnecessary) + _.set(hero, updateData.itemPath, castItemVal(updateData.itemPath, updateData.itemVal)); // Sanitization at 5c30944 (deemed unnecessary) } if (updateData.auth && updateData.auth.blocked === true) { diff --git a/website/server/libs/items/utils.js b/website/server/libs/items/utils.js index 0c0d9adf2f..308bfcc8a7 100644 --- a/website/server/libs/items/utils.js +++ b/website/server/libs/items/utils.js @@ -54,4 +54,24 @@ export function validateItemPath (itemPath) { if (itemPath.indexOf('items.quests') === 0) { return Boolean(shared.content.quests[key]); } +} + +// When passed a value of an item in the user object it'll convert the +// value to the correct format. +// Example a numeric string like "5" applied to a food item (expecting an interger) +// will be converted to the number 5 +// TODO cast the correct value for `items.gear.owned` +export function castItemVal (itemPath, itemVal) { + if ( + itemPath.indexOf('items.pets') === 0 || + itemPath.indexOf('items.eggs') === 0 || + itemPath.indexOf('items.hatchingPotions') === 0 || + itemPath.indexOf('items.food') === 0 || + itemPath.indexOf('items.mounts') === 0 || + itemPath.indexOf('items.quests') === 0 + ) { + return Number(itemVal); + } + + return itemVal; } \ No newline at end of file From 043e0fb819bb4a850ae7797acedb2cc7df295675 Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Thu, 25 Apr 2019 22:49:58 +0200 Subject: [PATCH 02/10] fix #9514: await user update client sid --- website/client/store/actions/user.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/website/client/store/actions/user.js b/website/client/store/actions/user.js index fe6094fcc9..c8edffe287 100644 --- a/website/client/store/actions/user.js +++ b/website/client/store/actions/user.js @@ -51,10 +51,8 @@ export async function set (store, changes) { } } - axios.put('/api/v4/user', changes); - // TODO - // .then((res) => console.log('set', res)) - // .catch((err) => console.error('set', err)); + let response = await axios.put('/api/v4/user', changes); + return response.data.data; } export async function sleep (store) { From 3be075ad43dd039bcbeb284bf56a8e17c99c69bf Mon Sep 17 00:00:00 2001 From: Matteo Pagliazzi Date: Fri, 26 Apr 2019 00:02:53 +0200 Subject: [PATCH 03/10] fix(tests): Items Utils > castItemVal fix fn call --- test/api/unit/libs/items/utils.test.js | 36 +++++++++++++------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/test/api/unit/libs/items/utils.test.js b/test/api/unit/libs/items/utils.test.js index 274dc665fe..3238fbd1dc 100644 --- a/test/api/unit/libs/items/utils.test.js +++ b/test/api/unit/libs/items/utils.test.js @@ -72,42 +72,42 @@ describe('Items Utils', () => { }); it('returns the item val untouched if an unsupported path', () => { - expect(validateItemPath('items.gear.equipped.weapon', 'a string')).to.equal('a string'); - expect(validateItemPath('items.currentPet', 'a string')).to.equal('a string'); - expect(validateItemPath('items.special.snowball', 'a string')).to.equal('a string'); + expect(castItemVal('items.gear.equipped.weapon', 'a string')).to.equal('a string'); + expect(castItemVal('items.currentPet', 'a string')).to.equal('a string'); + expect(castItemVal('items.special.snowball', 'a string')).to.equal('a string'); }); it('converts values for pets paths to numbers', () => { - expect(validateItemPath('items.pets.Wolf-CottonCandyPink', '5')).to.equal(5); - expect(validateItemPath('items.pets.Wolf-Invalid', '5')).to.equal(5); + expect(castItemVal('items.pets.Wolf-CottonCandyPink', '5')).to.equal(5); + expect(castItemVal('items.pets.Wolf-Invalid', '5')).to.equal(5); }); it('converts values for eggs paths to numbers', () => { - expect(validateItemPath('items.eggs.LionCub', '5')).to.equal(5); - expect(validateItemPath('items.eggs.Armadillo', '5')).to.equal(5); - expect(validateItemPath('items.eggs.NotAnArmadillo', '5')).to.equal(5); + expect(castItemVal('items.eggs.LionCub', '5')).to.equal(5); + expect(castItemVal('items.eggs.Armadillo', '5')).to.equal(5); + expect(castItemVal('items.eggs.NotAnArmadillo', '5')).to.equal(5); }); it('converts values for hatching potions paths to numbers', () => { - expect(validateItemPath('items.hatchingPotions.Base', '5')).to.equal(5); - expect(validateItemPath('items.hatchingPotions.StarryNight', '5')).to.equal(5); - expect(validateItemPath('items.hatchingPotions.Invalid', '5')).to.equal(5); + expect(castItemVal('items.hatchingPotions.Base', '5')).to.equal(5); + expect(castItemVal('items.hatchingPotions.StarryNight', '5')).to.equal(5); + expect(castItemVal('items.hatchingPotions.Invalid', '5')).to.equal(5); }); it('converts values for food paths to numbers', () => { - expect(validateItemPath('items.food.Cake_Base', '5')).to.equal(5); - expect(validateItemPath('items.food.Cake_Invalid', '5')).to.equal(5); + expect(castItemVal('items.food.Cake_Base', '5')).to.equal(5); + expect(castItemVal('items.food.Cake_Invalid', '5')).to.equal(5); }); it('converts values for mounts paths to numbers', () => { - expect(validateItemPath('items.mounts.Cactus-Base', '5')).to.equal(5); - expect(validateItemPath('items.mounts.Aether-Invisible', '5')).to.equal(5); - expect(validateItemPath('items.mounts.Aether-Invalid', '5')).to.equal(5); + expect(castItemVal('items.mounts.Cactus-Base', '5')).to.equal(5); + expect(castItemVal('items.mounts.Aether-Invisible', '5')).to.equal(5); + expect(castItemVal('items.mounts.Aether-Invalid', '5')).to.equal(5); }); it('converts values for quests paths to numbers', () => { - expect(validateItemPath('items.quests.atom3', '5')).to.equal(5); - expect(validateItemPath('items.quests.invalid', '5')).to.equal(5); + expect(castItemVal('items.quests.atom3', '5')).to.equal(5); + expect(castItemVal('items.quests.invalid', '5')).to.equal(5); }); }); }); From 83070e211da63e8f2d894359bb233fc3f3622f87 Mon Sep 17 00:00:00 2001 From: negue Date: Fri, 26 Apr 2019 18:45:05 +0200 Subject: [PATCH 04/10] Inbox: Add API to list conversations (#11110) * Add API to list inbox conversations * fix test + add api doc * use `.lean()` * orderBy after the the grouped conversations are loaded * fix ordering --- .../inbox/GET-inbox_messages.test.js | 6 +++ .../v4/inbox/GET-inbox-conversations.test.js | 44 ++++++++++++++++ website/server/controllers/api-v3/inbox.js | 4 +- website/server/controllers/api-v4/inbox.js | 21 ++++++++ website/server/libs/inbox/index.js | 52 +++++++++++++++++-- 5 files changed, 122 insertions(+), 5 deletions(-) create mode 100644 test/api/v4/inbox/GET-inbox-conversations.test.js diff --git a/test/api/v3/integration/inbox/GET-inbox_messages.test.js b/test/api/v3/integration/inbox/GET-inbox_messages.test.js index 39e5e37c37..c0c86a8a00 100644 --- a/test/api/v3/integration/inbox/GET-inbox_messages.test.js +++ b/test/api/v3/integration/inbox/GET-inbox_messages.test.js @@ -60,4 +60,10 @@ describe('GET /inbox/messages', () => { expect(messages.length).to.equal(4); }); + + it('returns only the messages of one conversation', async () => { + const messages = await user.get(`/inbox/messages?conversation=${otherUser.id}`); + + expect(messages.length).to.equal(3); + }); }); diff --git a/test/api/v4/inbox/GET-inbox-conversations.test.js b/test/api/v4/inbox/GET-inbox-conversations.test.js new file mode 100644 index 0000000000..33131b871a --- /dev/null +++ b/test/api/v4/inbox/GET-inbox-conversations.test.js @@ -0,0 +1,44 @@ +import { + generateUser, +} from '../../../helpers/api-integration/v4'; + +describe('GET /inbox/conversations', () => { + let user; + let otherUser; + let thirdUser; + + before(async () => { + [user, otherUser, thirdUser] = await Promise.all([generateUser(), generateUser(), generateUser()]); + + await otherUser.post('/members/send-private-message', { + toUserId: user.id, + message: 'first', + }); + await user.post('/members/send-private-message', { + toUserId: otherUser.id, + message: 'second', + }); + await user.post('/members/send-private-message', { + toUserId: thirdUser.id, + message: 'third', + }); + await otherUser.post('/members/send-private-message', { + toUserId: user.id, + message: 'fourth', + }); + + // message to yourself + await user.post('/members/send-private-message', { + toUserId: user.id, + message: 'fifth', + }); + }); + + it('returns the conversations', async () => { + const result = await user.get('/inbox/conversations'); + + expect(result.length).to.be.equal(3); + expect(result[0].user).to.be.equal(user.profile.name); + expect(result[0].username).to.be.equal(user.auth.local.username); + }); +}); diff --git a/website/server/controllers/api-v3/inbox.js b/website/server/controllers/api-v3/inbox.js index be32148a12..b806931a8d 100644 --- a/website/server/controllers/api-v3/inbox.js +++ b/website/server/controllers/api-v3/inbox.js @@ -12,6 +12,7 @@ let api = {}; * @apiDescription Get inbox messages for a user * * @apiParam (Query) {Number} page Load the messages of the selected Page - 10 Messages per Page + * @apiParam (Query) {GUID} conversation Loads only the messages of a conversation * * @apiSuccess {Array} data An array of inbox messages */ @@ -22,9 +23,10 @@ api.getInboxMessages = { async handler (req, res) { const user = res.locals.user; const page = req.query.page; + const conversation = req.query.conversation; const userInbox = await inboxLib.getUserInbox(user, { - page, + page, conversation, }); res.respond(200, userInbox); diff --git a/website/server/controllers/api-v4/inbox.js b/website/server/controllers/api-v4/inbox.js index bfceab993c..1a474c3220 100644 --- a/website/server/controllers/api-v4/inbox.js +++ b/website/server/controllers/api-v4/inbox.js @@ -72,4 +72,25 @@ api.clearMessages = { }, }; +/** + * @api {get} /inbox/conversations Get the conversations for a user + * @apiName conversations + * @apiGroup Inbox + * @apiDescription Get the conversations for a user + * + * @apiSuccess {Array} data An array of inbox conversations + */ +api.conversations = { + method: 'GET', + middlewares: [authWithHeaders()], + url: '/inbox/conversations', + async handler (req, res) { + const user = res.locals.user; + + const result = await inboxLib.listConversations(user); + + res.respond(200, result); + }, +}; + module.exports = api; diff --git a/website/server/libs/inbox/index.js b/website/server/libs/inbox/index.js index c131d11fa4..dac3e2f135 100644 --- a/website/server/libs/inbox/index.js +++ b/website/server/libs/inbox/index.js @@ -1,14 +1,25 @@ -import { inboxModel as Inbox } from '../../models/message'; +import {inboxModel as Inbox} from '../../models/message'; +import { + model as User, +} from '../../models/user'; +import orderBy from 'lodash/orderBy'; +import keyBy from 'lodash/keyBy'; const PM_PER_PAGE = 10; -export async function getUserInbox (user, options = {asArray: true, page: 0}) { +export async function getUserInbox (user, options = {asArray: true, page: 0, conversation: null}) { if (typeof options.asArray === 'undefined') { options.asArray = true; } + const findObj = {ownerId: user._id}; + + if (options.conversation) { + findObj.uuid = options.conversation; + } + let query = Inbox - .find({ownerId: user._id}) + .find(findObj) .sort({timestamp: -1}); if (typeof options.page !== 'undefined') { @@ -29,12 +40,45 @@ export async function getUserInbox (user, options = {asArray: true, page: 0}) { } } +export async function listConversations (user) { + let query = Inbox + .aggregate([ + { + $match: { + ownerId: user._id, + }, + }, + { + $group: { + _id: '$uuid', + timestamp: {$max: '$timestamp'}, // sort before group doesn't work - use the max value to sort it again after + }, + }, + ]); + + const conversationsList = orderBy(await query.exec(), ['timestamp'], ['desc']).map(c => c._id); + + const users = await User.find({_id: {$in: conversationsList}}) + .select('_id profile.name auth.local.username') + .lean() + .exec(); + + const usersMap = keyBy(users, '_id'); + const conversations = conversationsList.map(userId => ({ + uuid: usersMap[userId]._id, + user: usersMap[userId].profile.name, + username: usersMap[userId].auth.local.username, + })); + + return conversations; +} + export async function getUserInboxMessage (user, messageId) { return Inbox.findOne({ownerId: user._id, _id: messageId}).exec(); } export async function deleteMessage (user, messageId) { - const message = await Inbox.findOne({_id: messageId, ownerId: user._id }).exec(); + const message = await Inbox.findOne({_id: messageId, ownerId: user._id}).exec(); if (!message) return false; await Inbox.remove({_id: message._id, ownerId: user._id}).exec(); From 251563690ed65aa3689737602878e122c61fe35f Mon Sep 17 00:00:00 2001 From: HydeHunter2 <46248839+HydeHunter2@users.noreply.github.com> Date: Sat, 27 Apr 2019 20:21:05 +0300 Subject: [PATCH 05/10] Fix tag text overlapping (#11124) * Fix word-wrapping in user * Fix word-wrapping in taskModal --- website/client/components/tasks/taskModal.vue | 4 ++-- website/client/components/tasks/user.vue | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/website/client/components/tasks/taskModal.vue b/website/client/components/tasks/taskModal.vue index 0cd33c9dcc..4e9c0aca40 100644 --- a/website/client/components/tasks/taskModal.vue +++ b/website/client/components/tasks/taskModal.vue @@ -360,8 +360,8 @@ margin-top: 12px; position: relative; - label { - max-height: 30px; + .custom-control-label p { + word-break: break-word; } } diff --git a/website/client/components/tasks/user.vue b/website/client/components/tasks/user.vue index bd65fed494..b42ffbfc02 100644 --- a/website/client/components/tasks/user.vue +++ b/website/client/components/tasks/user.vue @@ -201,6 +201,7 @@ .custom-control-label { margin-left: 10px; + word-break: break-word; } .filter-panel-footer { From 40e0017b1740e0de43c0f754fb90ceba84df0910 Mon Sep 17 00:00:00 2001 From: HydeHunter2 <46248839+HydeHunter2@users.noreply.github.com> Date: Sat, 27 Apr 2019 20:22:09 +0300 Subject: [PATCH 06/10] Separate tags of different types (#11123) Challenge, group and user tags are separated --- website/client/components/tasks/tagsPopup.vue | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/website/client/components/tasks/tagsPopup.vue b/website/client/components/tasks/tagsPopup.vue index b1df9647f3..89987e9949 100644 --- a/website/client/components/tasks/tagsPopup.vue +++ b/website/client/components/tasks/tagsPopup.vue @@ -1,11 +1,15 @@