From 9ea35c8e45483501dbe1bddeb16036e1ae718ada Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Tue, 28 Mar 2023 11:19:05 -0600 Subject: [PATCH] refactor: move subscriptions 4x (#28548) --- apps/meteor/app/api/server/lib/rooms.ts | 17 ++- apps/meteor/app/api/server/v1/channels.ts | 28 ++-- apps/meteor/app/api/server/v1/groups.ts | 12 +- apps/meteor/app/api/server/v1/users.ts | 8 +- apps/meteor/app/apps/server/bridges/rooms.ts | 8 +- .../irc-bridge/localHandlers/onSaveMessage.js | 6 +- .../app/lib/server/functions/deleteUser.ts | 16 ++- .../functions/getRoomsWithSingleOwner.ts | 30 +++-- .../server/functions/setUserActiveStatus.ts | 2 +- .../server/functions/updateGroupDMsName.ts | 16 +-- apps/meteor/app/livechat/server/lib/Helper.js | 4 +- .../app/livechat/server/lib/RoutingManager.js | 10 +- apps/meteor/app/mentions/server/Mentions.js | 12 +- apps/meteor/app/mentions/server/server.js | 5 +- .../app/models/server/models/Subscriptions.js | 120 ------------------ apps/meteor/app/models/server/models/Users.js | 7 +- .../lib/dataExport/processDataDownloads.ts | 9 +- apps/meteor/server/lib/roles/getRoomRoles.ts | 10 +- apps/meteor/server/lib/spotlight.js | 17 +-- apps/meteor/server/methods/browseChannels.ts | 7 +- apps/meteor/server/methods/channelsList.ts | 14 +- apps/meteor/server/methods/messageSearch.ts | 9 +- apps/meteor/server/models/raw/Rooms.js | 2 +- .../meteor/server/models/raw/Subscriptions.ts | 18 +++ .../server/publications/subscription/index.ts | 14 +- apps/meteor/server/services/team/service.ts | 2 +- .../src/models/ISubscriptionsModel.ts | 2 + 27 files changed, 153 insertions(+), 252 deletions(-) diff --git a/apps/meteor/app/api/server/lib/rooms.ts b/apps/meteor/app/api/server/lib/rooms.ts index 95928ea9723..ee001268310 100644 --- a/apps/meteor/app/api/server/lib/rooms.ts +++ b/apps/meteor/app/api/server/lib/rooms.ts @@ -1,8 +1,7 @@ import type { IRoom, ISubscription, RoomAdminFieldsType } from '@rocket.chat/core-typings'; -import { Rooms } from '@rocket.chat/models'; +import { Rooms, Subscriptions } from '@rocket.chat/models'; import { hasPermissionAsync, hasAtLeastOnePermissionAsync } from '../../../authorization/server/functions/hasPermission'; -import { Subscriptions } from '../../../models/server'; import { adminFields } from '../../../../lib/rooms/adminFields'; export async function findAdminRooms({ @@ -82,9 +81,9 @@ export async function findChannelAndPrivateAutocomplete({ uid, selector }: { uid }, }; - const userRoomsIds = Subscriptions.cachedFindByUserId(uid, { fields: { rid: 1 } }) - .fetch() - .map((item: Pick) => item.rid); + const userRoomsIds = (await Subscriptions.findByUserId(uid, { projection: { rid: 1 } }).toArray()).map( + (item: Pick) => item.rid, + ); const rooms = await Rooms.findRoomsWithoutDiscussionsByRoomIds(selector.name, userRoomsIds, options).toArray(); @@ -132,9 +131,9 @@ export async function findChannelAndPrivateAutocompleteWithPagination({ items: IRoom[]; total: number; }> { - const userRoomsIds = Subscriptions.cachedFindByUserId(uid, { fields: { rid: 1 } }) - .fetch() - .map((item: Pick) => item.rid); + const userRoomsIds = (await Subscriptions.findByUserId(uid, { projection: { rid: 1 } }).toArray()).map( + (item: Pick) => item.rid, + ); const options = { projection: { @@ -177,7 +176,7 @@ export async function findRoomsAvailableForTeams({ uid, name }: { uid: string; n }; const userRooms = ( - Subscriptions.findByUserIdAndRoles(uid, ['owner'], { fields: { rid: 1 } }).fetch() as Pick[] + (await Subscriptions.findByUserIdAndRoles(uid, ['owner'], { projection: { rid: 1 } }).toArray()) as Pick[] ).map((item) => item.rid); const rooms = await Rooms.findChannelAndGroupListWithoutTeamsByNameStartingByOwner(uid, name, userRooms, options).toArray(); diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index b5820b1b9b6..706d9888667 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -21,7 +21,7 @@ import { import { Integrations, Messages, Rooms, Subscriptions, Uploads } from '@rocket.chat/models'; import { Team } from '@rocket.chat/core-services'; -import { Subscriptions as SubscriptionsSync, Users as UsersSync } from '../../../models/server'; +import { Users as UsersSync } from '../../../models/server'; import { canAccessRoomAsync, hasAtLeastOnePermission } from '../../../authorization/server'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; @@ -425,11 +425,11 @@ API.v1.addRoute( const findResult = await findChannelByIdOrName({ params }); - const moderators = await SubscriptionsSync.findByRoomIdAndRoles(findResult._id, ['moderator'], { - fields: { u: 1 }, - }) - .fetch() - .map((sub: ISubscription) => sub.u); + const moderators = ( + await Subscriptions.findByRoomIdAndRoles(findResult._id, ['moderator'], { + projection: { u: 1 }, + }).toArray() + ).map((sub: ISubscription) => sub.u); return API.v1.success({ moderators, @@ -896,18 +896,18 @@ API.v1.addRoute( if (!(await hasPermissionAsync(this.userId, 'view-joined-room'))) { return API.v1.unauthorized(); } - const roomIds = await SubscriptionsSync.findByUserIdAndType(this.userId, 'c', { - fields: { rid: 1 }, - }) - .fetch() - .map((s: Record) => s.rid); + const roomIds = ( + await Subscriptions.findByUserIdAndType(this.userId, 'c', { + projection: { rid: 1 }, + }).toArray() + ).map((s) => s.rid); ourQuery._id = { $in: roomIds }; } // teams filter - I would love to have a way to apply this filter @ db level :( - const ids = await SubscriptionsSync.cachedFindByUserId(this.userId, { fields: { rid: 1 } }) - .fetch() - .map((item: Record) => item.rid); + const ids = (await Subscriptions.findByUserId(this.userId, { projection: { rid: 1 } }).toArray()).map( + (item: Record) => item.rid, + ); ourQuery.$or = [ { diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index 1582e341a1c..33b3dfc75a7 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -5,7 +5,7 @@ import { Subscriptions, Rooms, Messages, Users, Uploads, Integrations } from '@r import { Team } from '@rocket.chat/core-services'; import type { Filter } from 'mongodb'; -import { Users as UsersSync, Subscriptions as SubscriptionsSync } from '../../../models/server'; +import { Users as UsersSync } from '../../../models/server'; import { hasAtLeastOnePermission, canAccessRoomAsync, hasAllPermission, roomAccessAttributes } from '../../../authorization/server'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { API } from '../api'; @@ -1150,11 +1150,11 @@ API.v1.addRoute( userId: this.userId, }); - const moderators = await SubscriptionsSync.findByRoomIdAndRoles(findResult.rid, ['moderator'], { - fields: { u: 1 }, - }) - .fetch() - .map((sub: any) => sub.u); + const moderators = ( + await Subscriptions.findByRoomIdAndRoles(findResult.rid, ['moderator'], { + projection: { u: 1 }, + }).toArray() + ).map((sub: any) => sub.u); return API.v1.success({ moderators, diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 8307c4064ea..d1425bca59a 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -19,11 +19,11 @@ import { Accounts } from 'meteor/accounts-base'; import { Match, check } from 'meteor/check'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import type { IExportOperation, ILoginToken, IPersonalAccessToken, IUser } from '@rocket.chat/core-typings'; -import { Users as UsersRaw } from '@rocket.chat/models'; +import { Users as UsersRaw, Subscriptions } from '@rocket.chat/models'; import type { Filter } from 'mongodb'; import { Team, api } from '@rocket.chat/core-services'; -import { Users, Subscriptions } from '../../../models/server'; +import { Users } from '../../../models/server'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; import { settings } from '../../../settings/server'; import { validateCustomFields, saveUser, saveCustomFieldsWithoutValidation, setUserAvatar, saveCustomFields } from '../../../lib/server'; @@ -376,7 +376,7 @@ API.v1.addRoute( return API.v1.success({ user: { ...user, - rooms: Subscriptions.findByUserId(user._id, { + rooms: await Subscriptions.findByUserId(user._id, { projection: { rid: 1, name: 1, @@ -389,7 +389,7 @@ API.v1.addRoute( t: 1, name: 1, }, - }).fetch(), + }).toArray(), }, }); } diff --git a/apps/meteor/app/apps/server/bridges/rooms.ts b/apps/meteor/app/apps/server/bridges/rooms.ts index a9c62c058bb..8db82f7645b 100644 --- a/apps/meteor/app/apps/server/bridges/rooms.ts +++ b/apps/meteor/app/apps/server/bridges/rooms.ts @@ -5,9 +5,10 @@ import type { IUser } from '@rocket.chat/apps-engine/definition/users'; import type { IMessage } from '@rocket.chat/apps-engine/definition/messages'; import { Meteor } from 'meteor/meteor'; import type { ISubscription, IUser as ICoreUser } from '@rocket.chat/core-typings'; +import { Subscriptions } from '@rocket.chat/models'; import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator'; -import { Rooms, Subscriptions, Users } from '../../../models/server'; +import { Rooms, Users } from '../../../models/server'; import { addUserToRoom } from '../../../lib/server/functions/addUserToRoom'; import { deleteRoom } from '../../../lib/server/functions/deleteRoom'; @@ -95,7 +96,7 @@ export class AppRoomBridge extends RoomBridge { protected async getMembers(roomId: string, appId: string): Promise> { this.orch.debugLog(`The App ${appId} is getting the room's members by room id: "${roomId}"`); const subscriptions = await Subscriptions.findByRoomId(roomId, {}); - return subscriptions.map((sub: ISubscription) => this.orch.getConverters()?.get('users').convertById(sub.u?._id)); + return (await subscriptions.toArray()).map((sub: ISubscription) => this.orch.getConverters()?.get('users').convertById(sub.u?._id)); } protected async getDirectByUsernames(usernames: Array, appId: string): Promise { @@ -188,7 +189,8 @@ export class AppRoomBridge extends RoomBridge { private async getUsersByRoomIdAndSubscriptionRole(roomId: string, role: string): Promise { const subs = await Subscriptions.findByRoomIdAndRoles(roomId, [role], { projection: { uid: '$u._id', _id: 0 } }); - const users = await Users.findByIds(subs.map((user: { uid: string }) => user.uid)); + // Was this a bug? + const users = await Users.findByIds(subs.map((user) => user.u._id)); const userConverter = this.orch.getConverters()!.get('users'); return users.map((user: ICoreUser) => userConverter!.convertToApp(user)); } diff --git a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onSaveMessage.js b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onSaveMessage.js index 3f532074bf0..e2f514e484d 100644 --- a/apps/meteor/app/irc/server/irc-bridge/localHandlers/onSaveMessage.js +++ b/apps/meteor/app/irc/server/irc-bridge/localHandlers/onSaveMessage.js @@ -1,12 +1,14 @@ +import { Subscriptions } from '@rocket.chat/models'; + import { SystemLogger } from '../../../../../server/lib/logger/system'; -import { Subscriptions, Users } from '../../../../models/server'; +import { Users } from '../../../../models/server'; export default async function handleOnSaveMessage(message, to) { let toIdentification = ''; // Direct message if (to.t === 'd') { const subscriptions = Subscriptions.findByRoomId(to._id); - subscriptions.forEach((subscription) => { + await subscriptions.forEach((subscription) => { if (subscription.u._id !== message.u._id) { const userData = Users.findOne({ username: subscription.u.username }); if (userData) { diff --git a/apps/meteor/app/lib/server/functions/deleteUser.ts b/apps/meteor/app/lib/server/functions/deleteUser.ts index 029d2a6a1e7..2cfcc8c9c98 100644 --- a/apps/meteor/app/lib/server/functions/deleteUser.ts +++ b/apps/meteor/app/lib/server/functions/deleteUser.ts @@ -1,10 +1,18 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { Integrations, FederationServers, LivechatVisitors, LivechatDepartmentAgents, Messages, Rooms } from '@rocket.chat/models'; +import { + Integrations, + FederationServers, + LivechatVisitors, + LivechatDepartmentAgents, + Messages, + Rooms, + Subscriptions, +} from '@rocket.chat/models'; import { api } from '@rocket.chat/core-services'; import { FileUpload } from '../../../file-upload/server'; -import { Users, Subscriptions } from '../../../models/server'; +import { Users } from '../../../models/server'; import { settings } from '../../../settings/server'; import { updateGroupDMsName } from './updateGroupDMsName'; import { relinquishRoomOwnerships } from './relinquishRoomOwnerships'; @@ -21,7 +29,7 @@ export async function deleteUser(userId: string, confirmRelinquish = false): Pro return; } - const subscribedRooms = getSubscribedRoomsForUserWithDetails(userId); + const subscribedRooms = await getSubscribedRoomsForUserWithDetails(userId); if (shouldRemoveOrChangeOwner(subscribedRooms) && !confirmRelinquish) { const rooms = await getUserSingleOwnedRooms(subscribedRooms); @@ -57,7 +65,7 @@ export async function deleteUser(userId: string, confirmRelinquish = false): Pro await Rooms.updateGroupDMsRemovingUsernamesByUsername(user.username, userId); // Remove direct rooms with the user await Rooms.removeDirectRoomContainingUsername(user.username); // Remove direct rooms with the user - Subscriptions.removeByUserId(userId); // Remove user subscriptions + await Subscriptions.removeByUserId(userId); // Remove user subscriptions if (user.roles.includes('livechat-agent')) { // Remove user as livechat agent diff --git a/apps/meteor/app/lib/server/functions/getRoomsWithSingleOwner.ts b/apps/meteor/app/lib/server/functions/getRoomsWithSingleOwner.ts index a331ef146f7..596bd301d99 100644 --- a/apps/meteor/app/lib/server/functions/getRoomsWithSingleOwner.ts +++ b/apps/meteor/app/lib/server/functions/getRoomsWithSingleOwner.ts @@ -1,7 +1,7 @@ -import type { IUser, ISubscription } from '@rocket.chat/core-typings'; +import type { IUser } from '@rocket.chat/core-typings'; +import { Subscriptions, Users } from '@rocket.chat/models'; import { subscriptionHasRole } from '../../../authorization/server'; -import { Users, Subscriptions } from '../../../models/server'; export type SubscribedRoomsForUserWithDetails = { rid: string; @@ -16,18 +16,18 @@ export function shouldRemoveOrChangeOwner(subscribedRooms: SubscribedRoomsForUse return subscribedRooms.some(({ shouldBeRemoved, shouldChangeOwner }) => shouldBeRemoved || shouldChangeOwner); } -export function getSubscribedRoomsForUserWithDetails( +export async function getSubscribedRoomsForUserWithDetails( userId: string, assignNewOwner = true, roomIds: string[] = [], -): SubscribedRoomsForUserWithDetails[] { +): Promise { const subscribedRooms: SubscribedRoomsForUserWithDetails[] = []; const cursor = roomIds.length > 0 ? Subscriptions.findByUserIdAndRoomIds(userId, roomIds) : Subscriptions.findByUserIdExceptType(userId, 'd'); // Iterate through all the rooms the user is subscribed to, to check if he is the last owner of any of them. - cursor.forEach((subscription: ISubscription) => { + for await (const subscription of cursor) { const roomData: SubscribedRoomsForUserWithDetails = { rid: subscription.rid, t: subscription.t, @@ -39,27 +39,29 @@ export function getSubscribedRoomsForUserWithDetails( if (subscriptionHasRole(subscription, 'owner')) { // Fetch the number of owners - const numOwners = Subscriptions.findByRoomIdAndRoles(subscription.rid, ['owner']).count(); + const numOwners = await Subscriptions.countByRoomIdAndRoles(subscription.rid, ['owner']); // If it's only one, then this user is the only owner. roomData.userIsLastOwner = numOwners === 1; if (numOwners === 1 && assignNewOwner) { // Let's check how many subscribers the room has. - const options = { projection: { 'u._id': 1 }, sort: { ts: 1 } }; + const options = { projection: { 'u._id': 1 }, sort: { ts: 1 as const } }; const subscribersCursor = Subscriptions.findByRoomId(subscription.rid, options); - subscribersCursor.forEach(({ u: { _id: uid } }: ISubscription) => { + for await (const { + u: { _id: uid }, + } of subscribersCursor) { // If we already changed the owner or this subscription is for the user we are removing, then don't try to give it ownership if (roomData.shouldChangeOwner || uid === userId) { - return; + continue; } - const newOwner = Users.findOneActiveById(uid, { fields: { _id: 1 } }); + const newOwner = await Users.findOneActiveById(uid, { projection: { _id: 1 } }); if (!newOwner) { - return; + continue; } roomData.newOwner = uid; roomData.shouldChangeOwner = true; - }); + } // If there's no subscriber available to be the new owner and it's not a public room, we can remove it. if (!roomData.shouldChangeOwner && roomData.t !== 'c') { @@ -68,11 +70,11 @@ export function getSubscribedRoomsForUserWithDetails( } } else if (roomData.t !== 'c') { // If the user is not an owner, remove the room if the user is the only subscriber - roomData.shouldBeRemoved = Subscriptions.findByRoomId(roomData.rid).count() === 1; + roomData.shouldBeRemoved = (await Subscriptions.countByRoomId(roomData.rid)) === 1; } subscribedRooms.push(roomData); - }); + } return subscribedRooms; } diff --git a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts index e012b95c6ca..0f127d52153 100644 --- a/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts +++ b/apps/meteor/app/lib/server/functions/setUserActiveStatus.ts @@ -68,7 +68,7 @@ export async function setUserActiveStatus(userId: string, active: boolean, confi }); } - const subscribedRooms = getSubscribedRoomsForUserWithDetails(userId); + const subscribedRooms = await getSubscribedRoomsForUserWithDetails(userId); // give omnichannel rooms a special treatment :) const chatSubscribedRooms = subscribedRooms.filter(({ t }) => t !== 'l'); const livechatSubscribedRooms = subscribedRooms.filter(({ t }) => t === 'l'); diff --git a/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts b/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts index 785b07f999b..92e85ba5bb0 100644 --- a/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts +++ b/apps/meteor/app/lib/server/functions/updateGroupDMsName.ts @@ -1,7 +1,7 @@ -import type { IUser, ISubscription } from '@rocket.chat/core-typings'; -import { Rooms } from '@rocket.chat/models'; +import type { IUser } from '@rocket.chat/core-typings'; +import { Rooms, Subscriptions } from '@rocket.chat/models'; -import { Subscriptions, Users } from '../../../models/server'; +import { Users } from '../../../models/server'; const getFname = (members: IUser[]): string => members.map(({ name, username }) => name || username).join(', '); const getName = (members: IUser[]): string => members.map(({ username }) => username).join(','); @@ -34,7 +34,7 @@ function sortUsersAlphabetically(u1: IUser, u2: IUser): number { return (u1.name! || u1.username!).localeCompare(u2.name! || u2.username!); } -export const updateGroupDMsName = async (userThatChangedName: IUser) => { +export const updateGroupDMsName = async (userThatChangedName: IUser): Promise => { if (!userThatChangedName.username) { return; } @@ -60,10 +60,10 @@ export const updateGroupDMsName = async (userThatChangedName: IUser) => { const members = getMembers(room.uids); const sortedMembers = members.sort(sortUsersAlphabetically); - const subs = Subscriptions.findByRoomId(room._id, { fields: { '_id': 1, 'u._id': 1 } }); - subs.forEach((sub: ISubscription) => { + const subs = Subscriptions.findByRoomId(room._id, { projection: { '_id': 1, 'u._id': 1 } }); + for await (const sub of subs) { const otherMembers = sortedMembers.filter(({ _id }) => _id !== sub.u._id); - Subscriptions.updateNameAndFnameById(sub._id, getName(otherMembers), getFname(otherMembers)); - }); + await Subscriptions.updateNameAndFnameById(sub._id, getName(otherMembers), getFname(otherMembers)); + } } }; diff --git a/apps/meteor/app/livechat/server/lib/Helper.js b/apps/meteor/app/livechat/server/lib/Helper.js index 42dfb9270f4..d1268c64c6b 100644 --- a/apps/meteor/app/livechat/server/lib/Helper.js +++ b/apps/meteor/app/livechat/server/lib/Helper.js @@ -375,7 +375,7 @@ export const forwardRoomToAgent = async (room, transferData) => { const { servedBy } = roomTaken; if (servedBy) { if (oldServedBy && servedBy._id !== oldServedBy._id) { - RoutingManager.removeAllRoomSubscriptions(room, servedBy); + await RoutingManager.removeAllRoomSubscriptions(room, servedBy); } Messages.createUserJoinWithRoomIdAndUser(rid, { _id: servedBy._id, @@ -489,7 +489,7 @@ export const forwardRoomToDepartment = async (room, guest, transferData) => { if (oldServedBy) { // if chat is queued then we don't ignore the new servedBy agent bcs at this // point the chat is not assigned to him/her and it is still in the queue - RoutingManager.removeAllRoomSubscriptions(room, !chatQueued && servedBy); + await RoutingManager.removeAllRoomSubscriptions(room, !chatQueued && servedBy); } if (!chatQueued && servedBy) { Messages.createUserJoinWithRoomIdAndUser(rid, servedBy); diff --git a/apps/meteor/app/livechat/server/lib/RoutingManager.js b/apps/meteor/app/livechat/server/lib/RoutingManager.js index c5ddf87ec1d..fa2dda18cd2 100644 --- a/apps/meteor/app/livechat/server/lib/RoutingManager.js +++ b/apps/meteor/app/livechat/server/lib/RoutingManager.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import { LivechatInquiry, LivechatRooms } from '@rocket.chat/models'; +import { LivechatInquiry, LivechatRooms, Subscriptions } from '@rocket.chat/models'; import { createLivechatSubscription, @@ -14,7 +14,7 @@ import { } from './Helper'; import { callbacks } from '../../../../lib/callbacks'; import { Logger } from '../../../../server/lib/logger/Logger'; -import { Rooms, Messages, Users, Subscriptions } from '../../../models/server'; +import { Rooms, Messages, Users } from '../../../models/server'; import { Apps, AppEvents } from '../../../../ee/server/apps'; const logger = new Logger('RoutingManager'); @@ -140,7 +140,7 @@ export const RoutingManager = { if (servedBy) { logger.debug(`Unassigning current agent for inquiry ${inquiry._id}`); await LivechatRooms.removeAgentByRoomId(rid); - this.removeAllRoomSubscriptions(room); + await this.removeAllRoomSubscriptions(room); dispatchAgentDelegated(rid, null); } @@ -239,10 +239,10 @@ export const RoutingManager = { return defaultAgent; }, - removeAllRoomSubscriptions(room, ignoreUser) { + async removeAllRoomSubscriptions(room, ignoreUser) { const { _id: roomId } = room; - const subscriptions = Subscriptions.findByRoomId(roomId).fetch(); + const subscriptions = await Subscriptions.findByRoomId(roomId).toArray(); subscriptions?.forEach(({ u }) => { if (ignoreUser && ignoreUser._id === u._id) { return; diff --git a/apps/meteor/app/mentions/server/Mentions.js b/apps/meteor/app/mentions/server/Mentions.js index 326cd95f843..c243f26ac34 100644 --- a/apps/meteor/app/mentions/server/Mentions.js +++ b/apps/meteor/app/mentions/server/Mentions.js @@ -53,19 +53,21 @@ export default class MentionsServer extends MentionsParser { const mentionsAll = []; const userMentions = []; - mentions.forEach((m) => { + for await (const m of mentions) { const mention = m.trim().substr(1); if (mention !== 'all' && mention !== 'here') { - return userMentions.push(mention); + userMentions.push(mention); + continue; } - if (this.messageMaxAll > 0 && this.getTotalChannelMembers(rid) > this.messageMaxAll) { - return this.onMaxRoomMembersExceeded({ sender, rid }); + if (this.messageMaxAll > 0 && (await this.getTotalChannelMembers(rid)) > this.messageMaxAll) { + this.onMaxRoomMembersExceeded({ sender, rid }); + continue; } mentionsAll.push({ _id: mention, username: mention, }); - }); + } mentions = userMentions.length ? await this.getUsers(userMentions) : []; return [...mentionsAll, ...mentions]; } diff --git a/apps/meteor/app/mentions/server/server.js b/apps/meteor/app/mentions/server/server.js index 6e05005f7f4..12a2a36a70a 100644 --- a/apps/meteor/app/mentions/server/server.js +++ b/apps/meteor/app/mentions/server/server.js @@ -1,11 +1,12 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { api } from '@rocket.chat/core-services'; +import { Subscriptions } from '@rocket.chat/models'; import MentionsServer from './Mentions'; import { settings } from '../../settings/server'; import { callbacks } from '../../../lib/callbacks'; -import { Users, Subscriptions, Rooms } from '../../models/server'; +import { Users, Rooms } from '../../models/server'; export class MentionQueries { async getUsers(usernames) { @@ -24,7 +25,7 @@ export class MentionQueries { } getTotalChannelMembers(rid) { - return Subscriptions.findByRoomId(rid).count(); + return Subscriptions.countByRoomId(rid); } getChannels(channels) { diff --git a/apps/meteor/app/models/server/models/Subscriptions.js b/apps/meteor/app/models/server/models/Subscriptions.js index 24faae92706..a890251c7b2 100644 --- a/apps/meteor/app/models/server/models/Subscriptions.js +++ b/apps/meteor/app/models/server/models/Subscriptions.js @@ -1,6 +1,5 @@ import { Meteor } from 'meteor/meteor'; import { Match } from 'meteor/check'; -import mem from 'mem'; import { Base } from './_Base'; import Rooms from './Rooms'; @@ -68,126 +67,7 @@ class Subscriptions extends Base { return this.findOne(query, options); } - findOneByRoomNameAndUserId(roomName, userId) { - const query = { - 'name': roomName, - 'u._id': userId, - }; - - return this.findOne(query); - } - // FIND - findByUserId(userId, options) { - const query = { 'u._id': userId }; - - return this.find(query, options); - } - - cachedFindByUserId = mem(this.findByUserId.bind(this), { maxAge: 5000 }); - - findByUserIdExceptType(userId, typeException, options) { - const query = { - 'u._id': userId, - 't': { $ne: typeException }, - }; - - return this.find(query, options); - } - - findByUserIdAndRoomIds(userId, roomIds, options) { - const query = { - 'u._id': userId, - 'rid': { $in: roomIds }, - }; - - return this.find(query, options); - } - - findByUserIdAndType(userId, type, options) { - const query = { - 'u._id': userId, - 't': type, - }; - - return this.find(query, options); - } - - findByUserIdAndTypes(userId, types, options) { - const query = { - 'u._id': userId, - 't': { - $in: types, - }, - }; - - return this.find(query, options); - } - - /** - * @param {IUser['_id']} userId - * @param {IRole['_id'][]} roles - * @param {any} options - */ - findByUserIdAndRoles(userId, roles, options) { - const query = { - 'u._id': userId, - 'roles': { $in: roles }, - }; - - return this.find(query, options); - } - - findByUserIdUpdatedAfter(userId, updatedAt, options) { - const query = { - 'u._id': userId, - '_updatedAt': { - $gt: updatedAt, - }, - }; - - return this.find(query, options); - } - - /** - * @param {string} roomId - * @param {IRole['_id'][]} roles the list of roles - * @param {any} options - */ - findByRoomIdAndRoles(roomId, roles, options = undefined) { - roles = [].concat(roles); - const query = { - rid: roomId, - roles: { $in: roles }, - }; - - return this.find(query, options); - } - - findByType(types, options) { - const query = { - t: { - $in: types, - }, - }; - - return this.find(query, options); - } - - findByTypeAndUserId(type, userId, options) { - const query = { - 't': type, - 'u._id': userId, - }; - - return this.find(query, options); - } - - findByRoomId(roomId, options) { - const query = { rid: roomId }; - return this.find(query, options); - } - findByRoomIdAndNotUserId(roomId, userId, options = {}) { const query = { 'rid': roomId, diff --git a/apps/meteor/app/models/server/models/Users.js b/apps/meteor/app/models/server/models/Users.js index fa80dbc16e3..c75d8868e1b 100644 --- a/apps/meteor/app/models/server/models/Users.js +++ b/apps/meteor/app/models/server/models/Users.js @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { escapeRegExp } from '@rocket.chat/string-helpers'; +import { Subscriptions } from '@rocket.chat/models'; import { Base } from './_Base'; -import Subscriptions from './Subscriptions'; import { settings } from '../../../settings/server'; import { trim } from '../../../../lib/utils/stringUtils'; @@ -31,6 +31,7 @@ const queryStatusAgentOnline = (extraFilters = {}) => ({ statusConnection: { $ne: 'away' }, }), }); +// The promise.await will die with the model :) export class Users extends Base { constructor(...args) { super(...args); @@ -693,9 +694,7 @@ export class Users extends Base { } findByRoomId(rid, options) { - const data = Subscriptions.findByRoomId(rid) - .fetch() - .map((item) => item.u._id); + const data = Promise.await(Subscriptions.findByRoomId(rid).toArray()).map((item) => item.u._id); const query = { _id: { $in: data, diff --git a/apps/meteor/server/lib/dataExport/processDataDownloads.ts b/apps/meteor/server/lib/dataExport/processDataDownloads.ts index 87f5e8cff19..58ac7fc2790 100644 --- a/apps/meteor/server/lib/dataExport/processDataDownloads.ts +++ b/apps/meteor/server/lib/dataExport/processDataDownloads.ts @@ -4,11 +4,10 @@ import { access, mkdir, rm, writeFile } from 'fs/promises'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import moment from 'moment'; import { v4 as uuidv4 } from 'uuid'; -import { Avatars, ExportOperations, UserDataFiles } from '@rocket.chat/models'; +import { Avatars, ExportOperations, UserDataFiles, Subscriptions } from '@rocket.chat/models'; import type { IExportOperation, ISubscription, IUser, RoomType } from '@rocket.chat/core-typings'; import { settings } from '../../../app/settings/server'; -import { Subscriptions } from '../../../app/models/server'; import { FileUpload } from '../../../app/file-upload/server'; import { getPath } from './getPath'; import { joinPath } from '../fileUtils'; @@ -20,7 +19,7 @@ import { copyFileUpload } from './copyFileUpload'; import { uploadZipFile } from './uploadZipFile'; import { exportRoomMessagesToFile } from './exportRoomMessagesToFile'; -const loadUserSubscriptions = (_exportOperation: IExportOperation, fileType: 'json' | 'html', userId: IUser['_id']) => { +const loadUserSubscriptions = async (_exportOperation: IExportOperation, fileType: 'json' | 'html', userId: IUser['_id']) => { const roomList: ( | { roomId: string; @@ -35,7 +34,7 @@ const loadUserSubscriptions = (_exportOperation: IExportOperation, fileType: 'js )[] = []; const cursor = Subscriptions.findByUserId(userId); - cursor.forEach((subscription: ISubscription) => { + await cursor.forEach((subscription: ISubscription) => { const roomData = getRoomData(subscription.rid, userId); roomData.targetFile = `${(fileType === 'json' && roomData.roomName) || subscription.rid}.${fileType}`; @@ -147,7 +146,7 @@ const continueExportOperation = async function (exportOperation: IExportOperatio const exportType = exportOperation.fullExport ? 'json' : 'html'; if (!exportOperation.roomList) { - exportOperation.roomList = loadUserSubscriptions(exportOperation, exportType, exportOperation.userId); + exportOperation.roomList = await loadUserSubscriptions(exportOperation, exportType, exportOperation.userId); if (exportOperation.fullExport) { exportOperation.status = 'exporting-rooms'; diff --git a/apps/meteor/server/lib/roles/getRoomRoles.ts b/apps/meteor/server/lib/roles/getRoomRoles.ts index d8fc35d474d..4b0d9b092ed 100644 --- a/apps/meteor/server/lib/roles/getRoomRoles.ts +++ b/apps/meteor/server/lib/roles/getRoomRoles.ts @@ -1,16 +1,16 @@ import _ from 'underscore'; import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; -import { Roles } from '@rocket.chat/models'; +import { Roles, Subscriptions } from '@rocket.chat/models'; import { settings } from '../../../app/settings/server'; -import { Subscriptions, Users } from '../../../app/models/server'; +import { Users } from '../../../app/models/server'; export async function getRoomRoles(rid: IRoom['_id']): Promise { const options = { sort: { - 'u.username': 1, + 'u.username': 1 as const, }, - fields: { + projection: { rid: 1, u: 1, roles: 1, @@ -20,7 +20,7 @@ export async function getRoomRoles(rid: IRoom['_id']): Promise const useRealName = settings.get('UI_Use_Real_Name') === true; const roles = await Roles.find({ scope: 'Subscriptions', description: { $exists: true, $ne: '' } }).toArray(); - const subscriptions = Subscriptions.findByRoomIdAndRoles(rid, _.pluck(roles, '_id'), options).fetch() as ISubscription[]; + const subscriptions = await Subscriptions.findByRoomIdAndRoles(rid, _.pluck(roles, '_id'), options).toArray(); if (!useRealName) { return subscriptions; diff --git a/apps/meteor/server/lib/spotlight.js b/apps/meteor/server/lib/spotlight.js index eae1e3bae5b..0af9cc1c25e 100644 --- a/apps/meteor/server/lib/spotlight.js +++ b/apps/meteor/server/lib/spotlight.js @@ -3,7 +3,7 @@ import { Users, Subscriptions as SubscriptionsRaw, Rooms as RoomsRaw } from '@ro import { hasAllPermission, canAccessRoomAsync, roomAccessAttributes } from '../../app/authorization/server'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; -import { Subscriptions, Rooms } from '../../app/models/server'; +import { Rooms } from '../../app/models/server'; import { settings } from '../../app/settings/server'; import { readSecondaryPreferred } from '../database/readSecondaryPreferred'; import { roomCoordinator } from './rooms/roomCoordinator'; @@ -53,11 +53,11 @@ export class Spotlight { const searchableRoomTypeIds = roomCoordinator.searchableRoomTypes(); - const roomIds = Subscriptions.findByUserIdAndTypes(userId, searchableRoomTypeIds, { - fields: { rid: 1 }, - }) - .fetch() - .map((s) => s.rid); + const roomIds = ( + await SubscriptionsRaw.findByUserIdAndTypes(userId, searchableRoomTypeIds, { + projection: { rid: 1 }, + }).toArray() + ).map((s) => s.rid); const exactRoom = await RoomsRaw.findOneByNameAndType(text, searchableRoomTypeIds, roomOptions, includeFederatedRooms); if (exactRoom) { roomIds.push(exactRoom.rid); @@ -177,10 +177,7 @@ export class Spotlight { case 'l': insiderExtraQuery.push({ _id: { - $in: Subscriptions.findByRoomId(room._id) - .fetch() - .map((s) => s.u?._id) - .filter((id) => id && id !== userId), + $in: (await SubscriptionsRaw.findByRoomId(room._id).toArray()).map((s) => s.u?._id).filter((id) => id && id !== userId), }, }); break; diff --git a/apps/meteor/server/methods/browseChannels.ts b/apps/meteor/server/methods/browseChannels.ts index e1a1e76ac3a..540d5a75498 100644 --- a/apps/meteor/server/methods/browseChannels.ts +++ b/apps/meteor/server/methods/browseChannels.ts @@ -2,13 +2,12 @@ import { Meteor } from 'meteor/meteor'; import { DDPRateLimiter } from 'meteor/ddp-rate-limiter'; import mem from 'mem'; import { escapeRegExp } from '@rocket.chat/string-helpers'; -import { Rooms, Users } from '@rocket.chat/models'; +import { Rooms, Users, Subscriptions } from '@rocket.chat/models'; import { Team } from '@rocket.chat/core-services'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; -import type { IRoom, ISubscription, IUser } from '@rocket.chat/core-typings'; +import type { IRoom, IUser } from '@rocket.chat/core-typings'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; -import { Subscriptions } from '../../app/models/server'; import { settings } from '../../app/settings/server'; import { getFederationDomain } from '../../app/federation/server/lib/getFederationDomain'; import { isFederationEnabled } from '../../app/federation/server/lib/isFederationEnabled'; @@ -134,7 +133,7 @@ const getTeams = async ( return; } - const userSubs: ISubscription[] = Subscriptions.cachedFindByUserId(user._id).fetch(); + const userSubs = await Subscriptions.findByUserId(user._id).toArray(); const ids = userSubs.map((sub) => sub.rid); const { cursor, totalCount } = Rooms.findPaginatedContainingNameOrFNameInIdsAsTeamMain( searchTerm ? new RegExp(searchTerm, 'i') : null, diff --git a/apps/meteor/server/methods/channelsList.ts b/apps/meteor/server/methods/channelsList.ts index 69e939aac31..1c51c2dfda8 100644 --- a/apps/meteor/server/methods/channelsList.ts +++ b/apps/meteor/server/methods/channelsList.ts @@ -2,12 +2,12 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import _ from 'underscore'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; -import type { IRoom, ISubscription } from '@rocket.chat/core-typings'; -import { Rooms } from '@rocket.chat/models'; +import type { IRoom } from '@rocket.chat/core-typings'; +import { Rooms, Subscriptions } from '@rocket.chat/models'; import type { FindOptions } from 'mongodb'; import { hasPermissionAsync } from '../../app/authorization/server/functions/hasPermission'; -import { Subscriptions, Users } from '../../app/models/server'; +import { Users } from '../../app/models/server'; import { getUserPreference } from '../../app/utils/server'; import { settings } from '../../app/settings/server'; import { trim } from '../../lib/utils/stringUtils'; @@ -72,9 +72,7 @@ Meteor.methods({ channels = channels.concat(await Rooms.findByType('c', options).toArray()); } } else if (await hasPermissionAsync(userId, 'view-joined-room')) { - const roomIds = Subscriptions.findByTypeAndUserId('c', userId, { fields: { rid: 1 } }) - .fetch() - .map((s: ISubscription) => s.rid); + const roomIds = (await Subscriptions.findByTypeAndUserId('c', userId, { projection: { rid: 1 } }).toArray()).map((s) => s.rid); if (filter) { channels = channels.concat(await Rooms.findByTypeInIdsAndNameContaining('c', roomIds, filter, options).toArray()); } else { @@ -95,9 +93,7 @@ Meteor.methods({ const groupByType = userPref !== undefined ? userPref : settings.get('UI_Group_Channels_By_Type'); if (!groupByType) { - const roomIds = Subscriptions.findByTypeAndUserId('p', userId, { fields: { rid: 1 } }) - .fetch() - .map((s: ISubscription) => s.rid); + const roomIds = (await Subscriptions.findByTypeAndUserId('p', userId, { projection: { rid: 1 } }).toArray()).map((s) => s.rid); if (filter) { channels = channels.concat(await Rooms.findByTypeInIdsAndNameContaining('p', roomIds, filter, options).toArray()); } else { diff --git a/apps/meteor/server/methods/messageSearch.ts b/apps/meteor/server/methods/messageSearch.ts index 89b4dedbe43..3e79477aeec 100644 --- a/apps/meteor/server/methods/messageSearch.ts +++ b/apps/meteor/server/methods/messageSearch.ts @@ -1,11 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import { Messages } from '@rocket.chat/models'; +import { Messages, Subscriptions } from '@rocket.chat/models'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import type { ISubscription, IUser } from '@rocket.chat/core-typings'; import { canAccessRoomIdAsync } from '../../app/authorization/server/functions/canAccessRoom'; -import { Subscriptions } from '../../app/models/server'; import { settings } from '../../app/settings/server'; import { readSecondaryPreferred } from '../database/readSecondaryPreferred'; import { parseMessageSearchQuery } from '../lib/parseMessageSearchQuery'; @@ -73,11 +72,7 @@ Meteor.methods({ query.rid = rid; } else { query.rid = { - $in: user?._id - ? Subscriptions.findByUserId(user._id) - .fetch() - .map((subscription: ISubscription) => subscription.rid) - : [], + $in: user?._id ? (await Subscriptions.findByUserId(user._id).toArray()).map((subscription: ISubscription) => subscription.rid) : [], }; } diff --git a/apps/meteor/server/models/raw/Rooms.js b/apps/meteor/server/models/raw/Rooms.js index 0d0c493abcb..9c796f2beb8 100644 --- a/apps/meteor/server/models/raw/Rooms.js +++ b/apps/meteor/server/models/raw/Rooms.js @@ -1056,7 +1056,7 @@ export class RoomsRaw extends BaseRaw { } async findBySubscriptionUserId(userId, options) { - const data = (await Subscriptions.cachedFindByUserId(userId, { projection: { rid: 1 } }).toArray()).map((item) => item.rid); + const data = (await Subscriptions.findByUserId(userId, { projection: { rid: 1 } }).toArray()).map((item) => item.rid); const query = { _id: { diff --git a/apps/meteor/server/models/raw/Subscriptions.ts b/apps/meteor/server/models/raw/Subscriptions.ts index ca7052e8e55..db65546945a 100644 --- a/apps/meteor/server/models/raw/Subscriptions.ts +++ b/apps/meteor/server/models/raw/Subscriptions.ts @@ -924,6 +924,24 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.find(query, options); } + countByRoomIdAndRoles(roomId: string, roles: string[]): Promise { + roles = ([] as string[]).concat(roles); + const query = { + rid: roomId, + roles: { $in: roles }, + }; + + return this.col.countDocuments(query); + } + + countByRoomId(roomId: string): Promise { + const query = { + rid: roomId, + }; + + return this.col.countDocuments(query); + } + findByType(types: ISubscription['t'][], options?: FindOptions): FindCursor { const query: Filter = { t: { diff --git a/apps/meteor/server/publications/subscription/index.ts b/apps/meteor/server/publications/subscription/index.ts index 58036f82bec..a03ff76e397 100644 --- a/apps/meteor/server/publications/subscription/index.ts +++ b/apps/meteor/server/publications/subscription/index.ts @@ -1,8 +1,8 @@ import type { ISubscription } from '@rocket.chat/core-typings'; import type { ServerMethods } from '@rocket.chat/ui-contexts'; import { Meteor } from 'meteor/meteor'; +import { Subscriptions } from '@rocket.chat/models'; -import { Subscriptions } from '../../../app/models/server'; import { subscriptionFields } from '../../modules/watchers/publishFields'; declare module '@rocket.chat/ui-contexts' { @@ -13,33 +13,33 @@ declare module '@rocket.chat/ui-contexts' { } Meteor.methods({ - 'subscriptions/get'(updatedAt) { + async 'subscriptions/get'(updatedAt) { const uid = Meteor.userId(); if (!uid) { return []; } - const options = { fields: subscriptionFields }; + const options = { projection: subscriptionFields }; - const records: ISubscription[] = Subscriptions.findByUserId(uid, options).fetch(); + const records: ISubscription[] = await Subscriptions.findByUserId(uid, options).toArray(); if (updatedAt instanceof Date) { return { update: records.filter((record) => { return record._updatedAt > updatedAt; }), - remove: Subscriptions.trashFindDeletedAfter( + remove: await Subscriptions.trashFindDeletedAfter( updatedAt, { 'u._id': uid, }, { - fields: { + projection: { _id: 1, _deletedAt: 1, }, }, - ).fetch(), + ).toArray(), }; } diff --git a/apps/meteor/server/services/team/service.ts b/apps/meteor/server/services/team/service.ts index 1041c1cd885..7f3cb1267f3 100644 --- a/apps/meteor/server/services/team/service.ts +++ b/apps/meteor/server/services/team/service.ts @@ -596,7 +596,7 @@ export class TeamService extends ServiceClassInternal implements ITeamService { const [rooms, total] = await Promise.all([cursor.toArray(), totalCount]); - const roomData = getSubscribedRoomsForUserWithDetails(userId, false, teamRoomIds); + const roomData = await getSubscribedRoomsForUserWithDetails(userId, false, teamRoomIds); const records = []; for (const room of rooms) { diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index a18630d58ab..98cd0285d07 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -227,4 +227,6 @@ export interface ISubscriptionsModel extends IBaseModel { removeUnreadThreadByRoomIdAndUserId(rid: string, userId: string, tmid: string, clearAlert?: boolean): Promise; removeUnreadThreadsByRoomId(rid: string, tunread: string[]): Promise; + countByRoomIdAndRoles(roomId: string, roles: string[]): Promise; + countByRoomId(roomId: string): Promise; }