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 e3436a3cc4..85b223bcab 100644 --- a/test/api/v3/integration/inbox/GET-inbox_messages.test.js +++ b/test/api/v3/integration/inbox/GET-inbox_messages.test.js @@ -47,7 +47,7 @@ describe('GET /inbox/messages', () => { it('returns four messages when using page-query ', async () => { const promises = []; - for (let i = 0; i < 10; i += 1) { + for (let i = 0; i < 50; i += 1) { promises.push(user.post('/members/send-private-message', { toUserId: user.id, message: 'fourth', diff --git a/test/api/v4/inbox/GET-inbox-conversations.test.js b/test/api/v4/inbox/GET-inbox-conversations.test.js index 1ee66dda51..2803bd0fbb 100644 --- a/test/api/v4/inbox/GET-inbox-conversations.test.js +++ b/test/api/v4/inbox/GET-inbox-conversations.test.js @@ -66,7 +66,7 @@ describe('GET /inbox/conversations', () => { it('returns five messages when using page-query ', async () => { const promises = []; - for (let i = 0; i < 10; i += 1) { + for (let i = 0; i < 50; i += 1) { promises.push(user.post('/members/send-private-message', { toUserId: user.id, message: 'fourth', diff --git a/website/client/src/components/appFooter.vue b/website/client/src/components/appFooter.vue index 39d02c94a8..0eae4cbf8f 100644 --- a/website/client/src/components/appFooter.vue +++ b/website/client/src/components/appFooter.vue @@ -396,6 +396,32 @@ class="btn btn-secondary" @click="makeAdmin()" >Make Admin +
+ + Send Party Chat Messages +
+
+ + Send Inbox Messages +
@@ -886,6 +912,8 @@ export default { DEBUG_ENABLED, TIME_TRAVEL_ENABLED, lastTimeJump: null, + partyChatCount: 450, + inboxCount: 450, }; }, computed: { @@ -1004,6 +1032,32 @@ export default { // Reload the website then go to Help > Admin Panel to set contributor level, etc.'); // @TODO: sync() }, + async seedPartyChat () { + try { + const count = this.partyChatCount; + if (!Number.isInteger(count) || count < 1) { + window.alert('Please enter a positive integer'); // eslint-disable-line no-alert + return; + } + await axios.post('/api/v4/debug/seed-party-chat', { messageCount: count }); + window.alert(`Successfully sent ${count} messages to your party chat!`); // eslint-disable-line no-alert + } catch (e) { + window.alert(e.response?.data?.message || 'Error sending party chat messages'); // eslint-disable-line no-alert + } + }, + async seedInbox () { + try { + const count = this.inboxCount; + if (!Number.isInteger(count) || count < 1) { + window.alert('Please enter a positive integer'); // eslint-disable-line no-alert + return; + } + await axios.post('/api/v4/debug/seed-inbox', { messageCount: count }); + window.alert(`Successfully sent ${count} messages to your inbox!`); // eslint-disable-line no-alert + } catch (e) { + window.alert(e.response?.data?.message || 'Error sending inbox messages'); // eslint-disable-line no-alert + } + }, donate () { this.$root.$emit('bv::show::modal', 'buy-gems', { alreadyTracked: true }); }, diff --git a/website/client/src/pages/private-messages/index.vue b/website/client/src/pages/private-messages/index.vue index 4e051a3b00..4e347ce870 100644 --- a/website/client/src/pages/private-messages/index.vue +++ b/website/client/src/pages/private-messages/index.vue @@ -679,7 +679,7 @@ import NotificationMixins from '@/mixins/notifications'; // extract to a shared path const CONVERSATIONS_PER_PAGE = 10; -const PM_PER_PAGE = 10; +const PM_PER_PAGE = 50; const UI_STATES = Object.freeze({ LOADING: 'LOADING', diff --git a/website/client/src/store/actions/chat.js b/website/client/src/store/actions/chat.js index 855eba8696..836e6d0f7c 100644 --- a/website/client/src/store/actions/chat.js +++ b/website/client/src/store/actions/chat.js @@ -3,7 +3,7 @@ import Vue from 'vue'; import * as Analytics from '@/libs/analytics'; export async function getChat (store, payload) { - const response = await axios.get(`/api/v4/groups/${payload.groupId}/chat`); + const response = await axios.get(`/api/v4/groups/${payload.groupId}/chat?limit=400`); return response.data.data; } diff --git a/website/server/controllers/api-v3/chat.js b/website/server/controllers/api-v3/chat.js index 65dce42abe..32ac046a70 100644 --- a/website/server/controllers/api-v3/chat.js +++ b/website/server/controllers/api-v3/chat.js @@ -64,6 +64,8 @@ function textContainsBannedSlur (message) { * * @apiParam (Path) {String} groupId The group _id ('party' for the user party and * 'habitrpg' for tavern are accepted). + * @apiParam (Query) {Number} [limit=50] The number of messages to fetch (max 400). + * @apiParam (Query) {String} [before] Fetch messages older than this message ID. * * @apiSuccess {Array} data An array of chat messages * @@ -78,18 +80,21 @@ api.getChat = { const { user } = res.locals; req.checkParams('groupId', apiError('groupIdRequired')).notEmpty(); + req.checkQuery('before').optional().isUUID(); const validationErrors = req.validationErrors(); if (validationErrors) throw validationErrors; const { groupId } = req.params; + const limit = req.query.limit ? Math.min(parseInt(req.query.limit, 10), 400) : 50; + const { before } = req.query; const group = await Group.getGroup({ user, groupId, fields: 'chat privacy' }); if (!group) throw new NotFound(res.t('groupNotFound')); if (group.privacy === 'public') { throw new BadRequest(res.t('featureRetired')); } - const groupChat = await Group.toJSONCleanChat(group, user); + const groupChat = await Group.toJSONCleanChat(group, user, { limit, before }); res.respond(200, groupChat.chat); }, }; diff --git a/website/server/controllers/api-v3/debug.js b/website/server/controllers/api-v3/debug.js index 108cf2dbaa..d70eeeeb86 100644 --- a/website/server/controllers/api-v3/debug.js +++ b/website/server/controllers/api-v3/debug.js @@ -2,6 +2,7 @@ import mongoose from 'mongoose'; import get from 'lodash/get'; import sinon from 'sinon'; import moment from 'moment'; +import { v4 as uuid } from 'uuid'; import { authWithHeaders } from '../../middlewares/auth'; import ensureDevelopmentMode from '../../middlewares/ensureDevelopmentMode'; import ensureTimeTravelMode from '../../middlewares/ensureTimeTravelMode'; @@ -11,6 +12,7 @@ import { model as Group, // basicFields as basicGroupFields, } from '../../models/group'; +import { chatModel as Chat, inboxModel as Inbox } from '../../models/message'; import connectToMongoDB from '../../libs/mongoose'; const { content } = common; @@ -311,4 +313,93 @@ api.timeTravelAdjust = { }, }; +api.seedPartyChat = { + method: 'POST', + url: '/debug/seed-party-chat', + middlewares: [ensureDevelopmentMode, authWithHeaders()], + async handler (req, res) { + const { user } = res.locals; + const messageCount = Number(req.body.messageCount); + + if (!Number.isInteger(messageCount) || messageCount < 1) { + throw new BadRequest('messageCount must be a positive integer.'); + } + + if (!user.party._id) { + throw new BadRequest('You are not in a party.'); + } + + const party = await Group.findOne({ _id: user.party._id, type: 'party' }).exec(); + if (!party) { + throw new BadRequest('Party not found.'); + } + + const messages = []; + const baseTimestamp = Date.now(); + + for (let i = 1; i <= messageCount; i += 1) { + const id = uuid(); + messages.push({ + _id: id, + id, + groupId: party._id, + text: `#${i}`, + unformattedText: `#${i}`, + timestamp: new Date(baseTimestamp - (messageCount - i) * 1000), + likes: {}, + flags: {}, + flagCount: 0, + uuid: 'system', + user: 'System', + client: 'debug-seed', + }); + } + + await Chat.insertMany(messages); + + res.respond(200, { messageCount }); + }, +}; + +// Messaging ourselves for testing +api.seedInbox = { + method: 'POST', + url: '/debug/seed-inbox', + middlewares: [ensureDevelopmentMode, authWithHeaders()], + async handler (req, res) { + const { user } = res.locals; + const messageCount = Number(req.body.messageCount); + + if (!Number.isInteger(messageCount) || messageCount < 1) { + throw new BadRequest('messageCount must be a positive integer.'); + } + + const messages = []; + const baseTimestamp = Date.now(); + + for (let i = 1; i <= messageCount; i += 1) { + const id = uuid(); + messages.push({ + _id: id, + id, + ownerId: user._id, + uuid: user._id, + user: user.profile.name, + text: `#${i}`, + unformattedText: `#${i}`, + timestamp: new Date(baseTimestamp - (messageCount - i) * 1000), + likes: {}, + flags: {}, + flagCount: 0, + sent: true, + client: 'debug-seed', + }); + } + + await Inbox.insertMany(messages); + + res.respond(200, { messageCount }); + }, +}; + export default api; diff --git a/website/server/libs/chat/group-chat.js b/website/server/libs/chat/group-chat.js index 445c72528d..028f987b4a 100644 --- a/website/server/libs/chat/group-chat.js +++ b/website/server/libs/chat/group-chat.js @@ -9,7 +9,9 @@ import { // eslint-disable-line import/no-cycle const questScrolls = shared.content.quests; // @TODO: Don't use this method when the group can be saved. -export async function getGroupChat (group) { +export async function getGroupChat (group, options = {}) { + const { limit, before } = options; + let maxChatCount = MAX_CHAT_COUNT; if (group.chatLimitCount && group.chatLimitCount >= MAX_CHAT_COUNT) { maxChatCount = group.chatLimitCount; @@ -17,10 +19,19 @@ export async function getGroupChat (group) { maxChatCount = MAX_SUBBED_GROUP_CHAT_COUNT; } - const groupChat = await Chat.find({ groupId: group._id }) - .limit(maxChatCount) - .sort('-timestamp') - .exec(); + const effectiveLimit = limit !== undefined ? Math.min(limit, maxChatCount) : maxChatCount; + + let query = Chat.find({ groupId: group._id }) + .sort('-timestamp'); + + if (before) { + const beforeMessage = await Chat.findOne({ _id: before }).exec(); + if (beforeMessage) { + query = query.where('timestamp').lt(beforeMessage.timestamp); + } + } + + const groupChat = await query.limit(effectiveLimit).exec(); // @TODO: Concat old chat to keep continuity of chat stored on group object const currentGroupChat = group.chat || []; diff --git a/website/server/libs/inbox/index.js b/website/server/libs/inbox/index.js index 3cc2c2228b..af0d7ae07d 100644 --- a/website/server/libs/inbox/index.js +++ b/website/server/libs/inbox/index.js @@ -37,7 +37,9 @@ export async function sentMessage (sender, receiver, message, translate) { return messageSent; } -const PM_PER_PAGE = 10; +// Paginate per every 50 +const PM_PER_PAGE = 50; +const MAX_PM_COUNT = 400; const getUserInboxDefaultOptions = { asArray: true, @@ -61,12 +63,18 @@ export async function getUserInbox (user, optionParams = getUserInboxDefaultOpti .sort({ timestamp: -1 }); if (typeof options.page !== 'undefined') { + const page = Number(options.page); + const skip = PM_PER_PAGE * page; + if (skip >= MAX_PM_COUNT) { + return options.asArray ? [] : {}; + } + const remainingAllowed = MAX_PM_COUNT - skip; + const limit = Math.min(PM_PER_PAGE, remainingAllowed); query = query - .skip(PM_PER_PAGE * Number(options.page)) - .limit(PM_PER_PAGE); + .skip(skip) + .limit(limit); } else { - // Limit for legacy calls that are not paginated to prevent database issues - query = query.limit(200); + query = query.limit(MAX_PM_COUNT); } const messages = (await query.lean().exec()).map(msgObj => { diff --git a/website/server/models/group.js b/website/server/models/group.js index 7d181c818e..36c678c2cd 100644 --- a/website/server/models/group.js +++ b/website/server/models/group.js @@ -345,12 +345,12 @@ schema.statics.getGroups = async function getGroups (options = {}) { // unless the user is an admin or said chat is posted by that user // Not putting into toJSON because there we can't access user // It also removes the _meta field that can be stored inside a chat message -schema.statics.toJSONCleanChat = async function groupToJSONCleanChat (group, user) { +schema.statics.toJSONCleanChat = async function groupToJSONCleanChat (group, user, options = {}) { // @TODO: Adding this here for support the old chat, // but we should depreciate accessing chat like this // Also only return chat if requested, eventually we don't want to return chat here if (group && group.chat) { - await getGroupChat(group); + await getGroupChat(group, options); } const groupToJson = group.toJSON();