From c0c0227917d7601e4c491af6d1c2a69e67bded5f Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Wed, 19 Jul 2023 14:02:16 -0300 Subject: [PATCH 01/42] Add groups.membersByRole endpoint --- apps/meteor/app/api/server/v1/groups.ts | 51 +++++++++++++++++ .../server/lib/findUsersOfRoomByRole.ts | 57 +++++++++++++++++++ .../meteor/server/models/raw/Subscriptions.ts | 14 +++++ .../src/models/ISubscriptionsModel.ts | 2 + .../src/v1/groups/GroupsMembersByRoleProps.ts | 37 ++++++++++++ packages/rest-typings/src/v1/groups/groups.ts | 9 +++ packages/rest-typings/src/v1/groups/index.ts | 1 + 7 files changed, 171 insertions(+) create mode 100644 apps/meteor/server/lib/findUsersOfRoomByRole.ts create mode 100644 packages/rest-typings/src/v1/groups/GroupsMembersByRoleProps.ts diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index e9ae57b5228b..aedaa88cf339 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -6,6 +6,7 @@ import { Meteor } from 'meteor/meteor'; import type { Filter } from 'mongodb'; import { findUsersOfRoom } from '../../../../server/lib/findUsersOfRoom'; +import { findUsersOfRoomByRole } from '../../../../server/lib/findUsersOfRoomByRole'; import { canAccessRoomAsync, roomAccessAttributes } from '../../../authorization/server'; import { hasAllPermissionAsync, @@ -738,6 +739,56 @@ API.v1.addRoute( }, ); +API.v1.addRoute( + 'groups.membersByRole', + { authRequired: true }, + { + async get() { + const findResult = await findPrivateGroupByIdOrName({ + params: this.queryParams, + userId: this.userId, + }); + + if (findResult.broadcast && !(await hasPermissionAsync(this.userId, 'view-broadcast-member-list', findResult.rid))) { + return API.v1.unauthorized(); + } + + const { offset: skip, count: limit } = await getPaginationItems(this.queryParams); + const { sort = {} } = await this.parseJsonQuery(); + + check( + this.queryParams, + Match.ObjectIncluding({ + role: Match.Maybe(String), + status: Match.Maybe([String]), + filter: Match.Maybe(String), + }), + ); + + const { status, filter, role } = this.queryParams; + + const { cursor, totalCount } = await findUsersOfRoomByRole({ + rid: findResult.rid, + role, + ...(status && { status: { $in: status } }), + skip, + limit, + filter, + ...(sort?.username && { sort: { username: sort.username } }), + }); + + const [members, total] = await Promise.all([cursor.toArray(), totalCount]); + + return API.v1.success({ + members, + count: members.length, + offset: skip, + total, + }); + }, + }, +); + API.v1.addRoute( 'groups.messages', { authRequired: true }, diff --git a/apps/meteor/server/lib/findUsersOfRoomByRole.ts b/apps/meteor/server/lib/findUsersOfRoomByRole.ts new file mode 100644 index 000000000000..3d5363a8a3b3 --- /dev/null +++ b/apps/meteor/server/lib/findUsersOfRoomByRole.ts @@ -0,0 +1,57 @@ +import type { IUser } from '@rocket.chat/core-typings'; +import type { FilterOperators, FindCursor } from 'mongodb'; +import type { FindPaginated } from '@rocket.chat/model-typings'; +import { Users, Subscriptions } from '@rocket.chat/models'; +import { compact } from 'lodash'; + +import { settings } from '../../app/settings/server'; + +type FindUsersParam = { + rid: string; + role?: string; + status?: FilterOperators; + skip?: number; + limit?: number; + filter?: string; + sort?: Record; +}; + +export async function findUsersOfRoomByRole({ + rid, + role = '', + status, + skip = 0, + limit = 0, + filter = '', + sort, +}: FindUsersParam): Promise>> { + const subscriptions = await Subscriptions.findUsersInRole(rid, role).toArray(); + const uids = compact(subscriptions.map((subscription) => subscription.u?._id).filter(Boolean)); + + const options = { + projection: { + name: 1, + username: 1, + nickname: 1, + status: 1, + avatarETag: 1, + _updatedAt: 1, + federated: 1, + }, + sort: { + statusConnection: -1, + ...(sort || { ...(settings.get('UI_Use_Real_Name') && { name: 1 }), username: 1 }), + }, + ...(skip > 0 && { skip }), + ...(limit > 0 && { limit }), + }; + + const searchFields = settings.get('Accounts_SearchFields').trim().split(','); + + return Users.findPaginatedByActiveUsersExcept(filter, undefined, options, searchFields, [ + { + _id: { $in: uids }, + ...(status && { status }), + }, + ]); +} diff --git a/apps/meteor/server/models/raw/Subscriptions.ts b/apps/meteor/server/models/raw/Subscriptions.ts index 59976dc1753c..4290d393e526 100644 --- a/apps/meteor/server/models/raw/Subscriptions.ts +++ b/apps/meteor/server/models/raw/Subscriptions.ts @@ -250,6 +250,20 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return Users.find

({ _id: { $in: users } }, options || {}); } + findUsersInRole(rid: IRoom['_id'], role?: IRole['_id']): FindCursor { + const mainRoles = ['owner', 'leader', 'moderator']; + const rolesToExclude = mainRoles.filter((item) => item !== role); + const query = { + rid, + roles: { + $nin: rolesToExclude, + ...(role && { $eq: role }), + }, + }; + + return this.find(query, { projection: { 'u._id': 1 } }); + } + addRolesByUserId(uid: IUser['_id'], roles: IRole['_id'][], rid?: IRoom['_id']): Promise { if (!Array.isArray(roles)) { roles = [roles]; diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index f4dd7080597a..626fd7e660a9 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -50,6 +50,8 @@ export interface ISubscriptionsModel extends IBaseModel { options?: FindOptions

, ): Promise>; + findUsersInRole(rid: IRoom['_id'], role?: IRole['_id']): FindCursor; + addRolesByUserId(uid: IUser['_id'], roles: IRole['_id'][], rid?: IRoom['_id']): Promise; isUserInRoleScope(uid: IUser['_id'], rid?: IRoom['_id']): Promise; diff --git a/packages/rest-typings/src/v1/groups/GroupsMembersByRoleProps.ts b/packages/rest-typings/src/v1/groups/GroupsMembersByRoleProps.ts new file mode 100644 index 000000000000..99b0e0695d02 --- /dev/null +++ b/packages/rest-typings/src/v1/groups/GroupsMembersByRoleProps.ts @@ -0,0 +1,37 @@ +import Ajv from 'ajv'; + +import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; +import type { GroupsBaseProps } from './BaseProps'; +import { withGroupBaseProperties } from './BaseProps'; + +const ajv = new Ajv({ + coerceTypes: true, +}); + +export type GroupsMembersByRoleProps = PaginatedRequest; + +const GroupsMembersByRolePropsSchema = withGroupBaseProperties({ + offset: { + type: 'number', + nullable: true, + }, + count: { + type: 'number', + nullable: true, + }, + filter: { + type: 'string', + nullable: true, + }, + role: { + type: 'string', + nullable: true, + }, + status: { + type: 'array', + items: { type: 'string' }, + nullable: true, + }, +}); + +export const isGroupsMembersByRoleProps = ajv.compile(GroupsMembersByRolePropsSchema); diff --git a/packages/rest-typings/src/v1/groups/groups.ts b/packages/rest-typings/src/v1/groups/groups.ts index 46ad2f6bdcb8..b1cad57d2f20 100644 --- a/packages/rest-typings/src/v1/groups/groups.ts +++ b/packages/rest-typings/src/v1/groups/groups.ts @@ -3,6 +3,7 @@ import type { IMessage, IRoom, ITeam, IGetRoomRoles, IUser, IUpload, IIntegratio import type { PaginatedResult } from '../../helpers/PaginatedResult'; import type { GroupsArchiveProps } from './GroupsArchiveProps'; import type { GroupsMembersProps } from './GroupsMembersProps'; +import type { GroupsMembersByRoleProps } from './GroupsMembersByRoleProps'; import type { GroupsFilesProps } from './GroupsFilesProps'; import type { GroupsUnarchiveProps } from './GroupsUnarchiveProps'; import type { GroupsCreateProps } from './GroupsCreateProps'; @@ -53,6 +54,14 @@ export type GroupsEndpoints = { total: number; }; }; + '/v1/groups.membersByRole': { + GET: (params: GroupsMembersByRoleProps) => { + count: number; + offset: number; + members: IUser[]; + total: number; + }; + }; '/v1/groups.history': { GET: (params: GroupsHistoryProps) => PaginatedResult<{ messages: IMessage[]; diff --git a/packages/rest-typings/src/v1/groups/index.ts b/packages/rest-typings/src/v1/groups/index.ts index 49907d3a08e5..50dbbe5f36d0 100644 --- a/packages/rest-typings/src/v1/groups/index.ts +++ b/packages/rest-typings/src/v1/groups/index.ts @@ -10,6 +10,7 @@ export * from './GroupsFilesProps'; export * from './GroupsKickProps'; export * from './GroupsLeaveProps'; export * from './GroupsMembersProps'; +export * from './GroupsMembersByRoleProps'; export * from './GroupsMessageProps'; export * from './GroupsRolesProps'; export * from './GroupsUnarchiveProps'; From 82ea93cd1f2ea96029d2c5d4ed7a3a58a0401093 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Wed, 19 Jul 2023 20:45:51 -0300 Subject: [PATCH 02/42] Update endpoint name and fix logic --- apps/meteor/app/api/server/v1/groups.ts | 6 +++--- ...rsOfRoomByRole.ts => findUsersOfRoomByHighestRole.ts} | 4 ++-- apps/meteor/server/models/raw/Subscriptions.ts | 9 ++++++--- packages/model-typings/src/models/ISubscriptionsModel.ts | 2 +- ...ByRoleProps.ts => GroupsMembersByHighestRoleProps.ts} | 6 +++--- packages/rest-typings/src/v1/groups/groups.ts | 6 +++--- packages/rest-typings/src/v1/groups/index.ts | 2 +- 7 files changed, 19 insertions(+), 16 deletions(-) rename apps/meteor/server/lib/{findUsersOfRoomByRole.ts => findUsersOfRoomByHighestRole.ts} (90%) rename packages/rest-typings/src/v1/groups/{GroupsMembersByRoleProps.ts => GroupsMembersByHighestRoleProps.ts} (68%) diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index aedaa88cf339..e52cb5431022 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -6,7 +6,7 @@ import { Meteor } from 'meteor/meteor'; import type { Filter } from 'mongodb'; import { findUsersOfRoom } from '../../../../server/lib/findUsersOfRoom'; -import { findUsersOfRoomByRole } from '../../../../server/lib/findUsersOfRoomByRole'; +import { findUsersOfRoomByHighestRole } from '../../../../server/lib/findUsersOfRoomByHighestRole'; import { canAccessRoomAsync, roomAccessAttributes } from '../../../authorization/server'; import { hasAllPermissionAsync, @@ -740,7 +740,7 @@ API.v1.addRoute( ); API.v1.addRoute( - 'groups.membersByRole', + 'groups.membersByHighestRole', { authRequired: true }, { async get() { @@ -767,7 +767,7 @@ API.v1.addRoute( const { status, filter, role } = this.queryParams; - const { cursor, totalCount } = await findUsersOfRoomByRole({ + const { cursor, totalCount } = await findUsersOfRoomByHighestRole({ rid: findResult.rid, role, ...(status && { status: { $in: status } }), diff --git a/apps/meteor/server/lib/findUsersOfRoomByRole.ts b/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts similarity index 90% rename from apps/meteor/server/lib/findUsersOfRoomByRole.ts rename to apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts index 3d5363a8a3b3..b7b23dfe0445 100644 --- a/apps/meteor/server/lib/findUsersOfRoomByRole.ts +++ b/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts @@ -16,7 +16,7 @@ type FindUsersParam = { sort?: Record; }; -export async function findUsersOfRoomByRole({ +export async function findUsersOfRoomByHighestRole({ rid, role = '', status, @@ -25,7 +25,7 @@ export async function findUsersOfRoomByRole({ filter = '', sort, }: FindUsersParam): Promise>> { - const subscriptions = await Subscriptions.findUsersInRole(rid, role).toArray(); + const subscriptions = await Subscriptions.findUsersByHighestRole(rid, role).toArray(); const uids = compact(subscriptions.map((subscription) => subscription.u?._id).filter(Boolean)); const options = { diff --git a/apps/meteor/server/models/raw/Subscriptions.ts b/apps/meteor/server/models/raw/Subscriptions.ts index 4290d393e526..1ff1b4704210 100644 --- a/apps/meteor/server/models/raw/Subscriptions.ts +++ b/apps/meteor/server/models/raw/Subscriptions.ts @@ -250,9 +250,12 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return Users.find

({ _id: { $in: users } }, options || {}); } - findUsersInRole(rid: IRoom['_id'], role?: IRole['_id']): FindCursor { - const mainRoles = ['owner', 'leader', 'moderator']; - const rolesToExclude = mainRoles.filter((item) => item !== role); + findUsersByHighestRole(rid: IRoom['_id'], role?: IRole['_id']): FindCursor { + // roles ordered from lowest to highest + const mainRoles = ['leader', 'moderator', 'owner']; + + const roleIndex = mainRoles.indexOf(role || ''); + const rolesToExclude = mainRoles.slice(roleIndex + 1); const query = { rid, roles: { diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index 626fd7e660a9..67d1fd48feb8 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -50,7 +50,7 @@ export interface ISubscriptionsModel extends IBaseModel { options?: FindOptions

, ): Promise>; - findUsersInRole(rid: IRoom['_id'], role?: IRole['_id']): FindCursor; + findUsersByHighestRole(rid: IRoom['_id'], role?: IRole['_id']): FindCursor; addRolesByUserId(uid: IUser['_id'], roles: IRole['_id'][], rid?: IRoom['_id']): Promise; diff --git a/packages/rest-typings/src/v1/groups/GroupsMembersByRoleProps.ts b/packages/rest-typings/src/v1/groups/GroupsMembersByHighestRoleProps.ts similarity index 68% rename from packages/rest-typings/src/v1/groups/GroupsMembersByRoleProps.ts rename to packages/rest-typings/src/v1/groups/GroupsMembersByHighestRoleProps.ts index 99b0e0695d02..5a16883d979a 100644 --- a/packages/rest-typings/src/v1/groups/GroupsMembersByRoleProps.ts +++ b/packages/rest-typings/src/v1/groups/GroupsMembersByHighestRoleProps.ts @@ -8,9 +8,9 @@ const ajv = new Ajv({ coerceTypes: true, }); -export type GroupsMembersByRoleProps = PaginatedRequest; +export type GroupsMembersByHighestRoleProps = PaginatedRequest; -const GroupsMembersByRolePropsSchema = withGroupBaseProperties({ +const GroupsMembersByHighestRolePropsSchema = withGroupBaseProperties({ offset: { type: 'number', nullable: true, @@ -34,4 +34,4 @@ const GroupsMembersByRolePropsSchema = withGroupBaseProperties({ }, }); -export const isGroupsMembersByRoleProps = ajv.compile(GroupsMembersByRolePropsSchema); +export const isGroupsMembersByRoleProps = ajv.compile(GroupsMembersByHighestRolePropsSchema); diff --git a/packages/rest-typings/src/v1/groups/groups.ts b/packages/rest-typings/src/v1/groups/groups.ts index b1cad57d2f20..4c8109629691 100644 --- a/packages/rest-typings/src/v1/groups/groups.ts +++ b/packages/rest-typings/src/v1/groups/groups.ts @@ -3,7 +3,7 @@ import type { IMessage, IRoom, ITeam, IGetRoomRoles, IUser, IUpload, IIntegratio import type { PaginatedResult } from '../../helpers/PaginatedResult'; import type { GroupsArchiveProps } from './GroupsArchiveProps'; import type { GroupsMembersProps } from './GroupsMembersProps'; -import type { GroupsMembersByRoleProps } from './GroupsMembersByRoleProps'; +import type { GroupsMembersByHighestRoleProps } from './GroupsMembersByHighestRoleProps'; import type { GroupsFilesProps } from './GroupsFilesProps'; import type { GroupsUnarchiveProps } from './GroupsUnarchiveProps'; import type { GroupsCreateProps } from './GroupsCreateProps'; @@ -54,8 +54,8 @@ export type GroupsEndpoints = { total: number; }; }; - '/v1/groups.membersByRole': { - GET: (params: GroupsMembersByRoleProps) => { + '/v1/groups.membersByHighestRole': { + GET: (params: GroupsMembersByHighestRoleProps) => { count: number; offset: number; members: IUser[]; diff --git a/packages/rest-typings/src/v1/groups/index.ts b/packages/rest-typings/src/v1/groups/index.ts index 50dbbe5f36d0..fc7955e72932 100644 --- a/packages/rest-typings/src/v1/groups/index.ts +++ b/packages/rest-typings/src/v1/groups/index.ts @@ -10,7 +10,7 @@ export * from './GroupsFilesProps'; export * from './GroupsKickProps'; export * from './GroupsLeaveProps'; export * from './GroupsMembersProps'; -export * from './GroupsMembersByRoleProps'; +export * from './GroupsMembersByHighestRoleProps'; export * from './GroupsMessageProps'; export * from './GroupsRolesProps'; export * from './GroupsUnarchiveProps'; From 6c5db965ab97ee35d3ed2d0e4266aa4f3d35ec86 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Wed, 19 Jul 2023 21:25:42 -0300 Subject: [PATCH 03/42] Add channels endpoint --- apps/meteor/app/api/server/v1/channels.ts | 49 +++++++++++++++++++ .../rest-typings/src/v1/channels/channels.ts | 10 ++++ 2 files changed, 59 insertions(+) diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index 8b2a6786f5db..1c928cff665d 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -28,6 +28,7 @@ import { API } from '../api'; import { addUserToFileObj } from '../helpers/addUserToFileObj'; import { mountIntegrationQueryBasedOnPermissions } from '../../../integrations/server/lib/mountQueriesBasedOnPermission'; import { findUsersOfRoom } from '../../../../server/lib/findUsersOfRoom'; +import { findUsersOfRoomByHighestRole } from '../../../../server/lib/findUsersOfRoomByHighestRole'; import { settings } from '../../../settings/server'; import { composeRoomWithLastMessage } from '../helpers/composeRoomWithLastMessage'; import { getLoggedInUser } from '../helpers/getLoggedInUser'; @@ -1044,6 +1045,54 @@ API.v1.addRoute( }, ); +API.v1.addRoute( + 'channels.membersByHighestRole', + { authRequired: true }, + { + async get() { + const findResult = await findChannelByIdOrName({ + params: this.queryParams, + checkedArchived: false, + }); + + if (findResult.broadcast && !(await hasPermissionAsync(this.userId, 'view-broadcast-member-list', findResult._id))) { + return API.v1.unauthorized(); + } + + const { offset: skip, count: limit } = await getPaginationItems(this.queryParams); + const { sort = {} } = await this.parseJsonQuery(); + + check( + this.queryParams, + Match.ObjectIncluding({ + status: Match.Maybe([String]), + filter: Match.Maybe(String), + }), + ); + const { status, filter, role } = this.queryParams; + + const { cursor, totalCount } = await findUsersOfRoomByHighestRole({ + rid: findResult._id, + role, + ...(status && { status: { $in: status } }), + skip, + limit, + filter, + ...(sort?.username && { sort: { username: sort.username } }), + }); + + const [members, total] = await Promise.all([cursor.toArray(), totalCount]); + + return API.v1.success({ + members, + count: members.length, + offset: skip, + total, + }); + }, + }, +); + API.v1.addRoute( 'channels.online', { authRequired: true }, diff --git a/packages/rest-typings/src/v1/channels/channels.ts b/packages/rest-typings/src/v1/channels/channels.ts index 965e22e216e2..ef2db0fc6141 100644 --- a/packages/rest-typings/src/v1/channels/channels.ts +++ b/packages/rest-typings/src/v1/channels/channels.ts @@ -47,6 +47,16 @@ export type ChannelsEndpoints = { members: IUser[]; }>; }; + '/v1/channels.membersByHighestRole': { + GET: ( + params: PaginatedRequest< + | { roomId: string; role?: string; filter?: string; status?: string[] } + | { roomName: string; role?: string; filter?: string; status?: string[] } + >, + ) => PaginatedResult<{ + members: IUser[]; + }>; + }; '/v1/channels.history': { GET: (params: ChannelsHistoryProps) => PaginatedResult<{ messages: IMessage[]; From 577c89a9deddd654ad6d6d690711b695f7abab64 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Fri, 21 Jul 2023 12:05:43 -0300 Subject: [PATCH 04/42] Update endpoint response --- apps/meteor/app/api/server/v1/channels.ts | 11 +- apps/meteor/app/api/server/v1/groups.ts | 11 +- .../lib/findUsersOfRoomByHighestRole.ts | 21 ++-- .../meteor/server/models/raw/Subscriptions.ts | 17 --- apps/meteor/server/models/raw/Users.js | 107 ++++++++++++++++++ .../src/models/ISubscriptionsModel.ts | 2 - .../model-typings/src/models/IUsersModel.ts | 10 +- .../rest-typings/src/v1/channels/channels.ts | 3 +- .../groups/GroupsMembersByHighestRoleProps.ts | 37 ------ packages/rest-typings/src/v1/groups/groups.ts | 3 +- packages/rest-typings/src/v1/groups/index.ts | 1 - 11 files changed, 139 insertions(+), 84 deletions(-) delete mode 100644 packages/rest-typings/src/v1/groups/GroupsMembersByHighestRoleProps.ts diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index 1c928cff665d..ee2d60e52b82 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -1069,11 +1069,10 @@ API.v1.addRoute( filter: Match.Maybe(String), }), ); - const { status, filter, role } = this.queryParams; + const { status, filter } = this.queryParams; - const { cursor, totalCount } = await findUsersOfRoomByHighestRole({ + const cursor = await findUsersOfRoomByHighestRole({ rid: findResult._id, - role, ...(status && { status: { $in: status } }), skip, limit, @@ -1081,7 +1080,11 @@ API.v1.addRoute( ...(sort?.username && { sort: { username: sort.username } }), }); - const [members, total] = await Promise.all([cursor.toArray(), totalCount]); + const result = await cursor.toArray(); + const { + members, + totalCount: [{ total } = { total: 0 }], + } = result[0]; return API.v1.success({ members, diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index e52cb5431022..82f3bd1577c6 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -765,11 +765,10 @@ API.v1.addRoute( }), ); - const { status, filter, role } = this.queryParams; + const { status, filter } = this.queryParams; - const { cursor, totalCount } = await findUsersOfRoomByHighestRole({ + const cursor = await findUsersOfRoomByHighestRole({ rid: findResult.rid, - role, ...(status && { status: { $in: status } }), skip, limit, @@ -777,7 +776,11 @@ API.v1.addRoute( ...(sort?.username && { sort: { username: sort.username } }), }); - const [members, total] = await Promise.all([cursor.toArray(), totalCount]); + const result = await cursor.toArray(); + const { + members, + totalCount: [{ total } = { total: 0 }], + } = result[0]; return API.v1.success({ members, diff --git a/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts b/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts index b7b23dfe0445..05a587c4cd65 100644 --- a/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts +++ b/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts @@ -1,14 +1,11 @@ import type { IUser } from '@rocket.chat/core-typings'; -import type { FilterOperators, FindCursor } from 'mongodb'; -import type { FindPaginated } from '@rocket.chat/model-typings'; -import { Users, Subscriptions } from '@rocket.chat/models'; -import { compact } from 'lodash'; +import type { AggregationCursor, FilterOperators } from 'mongodb'; +import { Users } from '@rocket.chat/models'; import { settings } from '../../app/settings/server'; type FindUsersParam = { rid: string; - role?: string; status?: FilterOperators; skip?: number; limit?: number; @@ -18,16 +15,12 @@ type FindUsersParam = { export async function findUsersOfRoomByHighestRole({ rid, - role = '', status, skip = 0, limit = 0, filter = '', sort, -}: FindUsersParam): Promise>> { - const subscriptions = await Subscriptions.findUsersByHighestRole(rid, role).toArray(); - const uids = compact(subscriptions.map((subscription) => subscription.u?._id).filter(Boolean)); - +}: FindUsersParam): Promise> { const options = { projection: { name: 1, @@ -37,20 +30,20 @@ export async function findUsersOfRoomByHighestRole({ avatarETag: 1, _updatedAt: 1, federated: 1, + statusConnection: 1, }, sort: { statusConnection: -1, ...(sort || { ...(settings.get('UI_Use_Real_Name') && { name: 1 }), username: 1 }), }, - ...(skip > 0 && { skip }), - ...(limit > 0 && { limit }), + skip, + limit, }; const searchFields = settings.get('Accounts_SearchFields').trim().split(','); - return Users.findPaginatedByActiveUsersExcept(filter, undefined, options, searchFields, [ + return Users.findPaginatedActiveUsersByRoomIdWithHighestRole(filter, rid, options, searchFields, [ { - _id: { $in: uids }, ...(status && { status }), }, ]); diff --git a/apps/meteor/server/models/raw/Subscriptions.ts b/apps/meteor/server/models/raw/Subscriptions.ts index 1ff1b4704210..59976dc1753c 100644 --- a/apps/meteor/server/models/raw/Subscriptions.ts +++ b/apps/meteor/server/models/raw/Subscriptions.ts @@ -250,23 +250,6 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return Users.find

({ _id: { $in: users } }, options || {}); } - findUsersByHighestRole(rid: IRoom['_id'], role?: IRole['_id']): FindCursor { - // roles ordered from lowest to highest - const mainRoles = ['leader', 'moderator', 'owner']; - - const roleIndex = mainRoles.indexOf(role || ''); - const rolesToExclude = mainRoles.slice(roleIndex + 1); - const query = { - rid, - roles: { - $nin: rolesToExclude, - ...(role && { $eq: role }), - }, - }; - - return this.find(query, { projection: { 'u._id': 1 } }); - } - addRolesByUserId(uid: IUser['_id'], roles: IRole['_id'][], rid?: IRoom['_id']): Promise { if (!Array.isArray(roles)) { roles = [roles]; diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index d9195da6a437..97749222d87b 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -258,6 +258,113 @@ export class UsersRaw extends BaseRaw { return this.findPaginated(query, options); } + findPaginatedActiveUsersByRoomIdWithHighestRole( + searchTerm, + rid, + options, + searchFields, + extraQuery = [], + { startsWith = false, endsWith = false } = {}, + ) { + if (options == null) { + options = {}; + } + + const termRegex = new RegExp((startsWith ? '^' : '') + escapeRegExp(searchTerm) + (endsWith ? '$' : ''), 'i'); + + const orStmt = (searchFields || []).reduce(function (acc, el) { + acc.push({ [el.trim()]: termRegex }); + return acc; + }, []); + + const query = { + $and: [ + { + active: true, + __rooms: rid, + username: { $exists: true }, + // if the search term is empty, don't need to have the $or statement (because it would be an empty regex) + ...(searchTerm && orStmt.length > 0 && { $or: orStmt }), + }, + ...extraQuery, + ], + }; + + const skip = + options.skip !== 0 + ? [ + { + $skip: options.skip, + }, + ] + : []; + + const limit = + options.limit !== 0 + ? [ + { + $limit: options.limit, + }, + ] + : []; + + return this.col.aggregate([ + { + $match: query, + }, + { + $lookup: { + from: 'rocketchat_subscription', + localField: '_id', + foreignField: 'u._id', + as: 'sub', + let: { sub_rid: '$rid' }, + pipeline: [ + { + $match: { + $expr: { + $eq: ['$rid', rid], + }, + }, + }, + ], + }, + }, + { $unwind: '$sub' }, + { + $project: { + ...options.projection, + roles: '$sub.roles', + }, + }, + { + $addFields: { + highestRole: { + $cond: [{ $in: ['owner', '$roles'] }, 'owner', { $cond: [{ $in: ['moderator', '$roles'] }, 'moderator', 'member'] }], + }, + roleLevel: { + $cond: [{ $in: ['owner', '$roles'] }, 0, { $cond: [{ $in: ['moderator', '$roles'] }, 1, 2] }], + }, + }, + }, + { + $facet: { + members: [ + { + $sort: { + roleLevel: 1, + ...options.sort, + }, + }, + ...skip, + ...limit, + ], + totalCount: [{ $group: { _id: null, total: { $sum: 1 } } }], + }, + }, + ]); + } + findPaginatedByActiveLocalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localDomain) { const extraQuery = [ { diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index 67d1fd48feb8..f4dd7080597a 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -50,8 +50,6 @@ export interface ISubscriptionsModel extends IBaseModel { options?: FindOptions

, ): Promise>; - findUsersByHighestRole(rid: IRoom['_id'], role?: IRole['_id']): FindCursor; - addRolesByUserId(uid: IUser['_id'], roles: IRole['_id'][], rid?: IRoom['_id']): Promise; isUserInRoleScope(uid: IUser['_id'], rid?: IRoom['_id']): Promise; diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index fecc7386f401..427dd9f6d070 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -1,4 +1,4 @@ -import type { Document, UpdateResult, FindCursor, FindOptions, Filter, InsertOneResult, DeleteResult } from 'mongodb'; +import type { Document, UpdateResult, FindCursor, FindOptions, Filter, InsertOneResult, DeleteResult, AggregationCursor } from 'mongodb'; import type { IUser, IRole, @@ -44,6 +44,14 @@ export interface IUsersModel extends IBaseModel { extraQuery?: any, params?: { startsWith?: boolean; endsWith?: boolean }, ): FindPaginated>; + findPaginatedActiveUsersByRoomIdWithHighestRole( + searchTerm: any, + rid: any, + options: any, + searchFields: any, + extraQuery?: any, + params?: { startsWith?: boolean; endsWith?: boolean }, + ): Promise>; findPaginatedByActiveLocalUsersExcept( searchTerm: any, diff --git a/packages/rest-typings/src/v1/channels/channels.ts b/packages/rest-typings/src/v1/channels/channels.ts index ef2db0fc6141..36097edbcb24 100644 --- a/packages/rest-typings/src/v1/channels/channels.ts +++ b/packages/rest-typings/src/v1/channels/channels.ts @@ -50,8 +50,7 @@ export type ChannelsEndpoints = { '/v1/channels.membersByHighestRole': { GET: ( params: PaginatedRequest< - | { roomId: string; role?: string; filter?: string; status?: string[] } - | { roomName: string; role?: string; filter?: string; status?: string[] } + { roomId: string; filter?: string; status?: string[] } | { roomName: string; filter?: string; status?: string[] } >, ) => PaginatedResult<{ members: IUser[]; diff --git a/packages/rest-typings/src/v1/groups/GroupsMembersByHighestRoleProps.ts b/packages/rest-typings/src/v1/groups/GroupsMembersByHighestRoleProps.ts deleted file mode 100644 index 5a16883d979a..000000000000 --- a/packages/rest-typings/src/v1/groups/GroupsMembersByHighestRoleProps.ts +++ /dev/null @@ -1,37 +0,0 @@ -import Ajv from 'ajv'; - -import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; -import type { GroupsBaseProps } from './BaseProps'; -import { withGroupBaseProperties } from './BaseProps'; - -const ajv = new Ajv({ - coerceTypes: true, -}); - -export type GroupsMembersByHighestRoleProps = PaginatedRequest; - -const GroupsMembersByHighestRolePropsSchema = withGroupBaseProperties({ - offset: { - type: 'number', - nullable: true, - }, - count: { - type: 'number', - nullable: true, - }, - filter: { - type: 'string', - nullable: true, - }, - role: { - type: 'string', - nullable: true, - }, - status: { - type: 'array', - items: { type: 'string' }, - nullable: true, - }, -}); - -export const isGroupsMembersByRoleProps = ajv.compile(GroupsMembersByHighestRolePropsSchema); diff --git a/packages/rest-typings/src/v1/groups/groups.ts b/packages/rest-typings/src/v1/groups/groups.ts index 4c8109629691..5f886990c6fd 100644 --- a/packages/rest-typings/src/v1/groups/groups.ts +++ b/packages/rest-typings/src/v1/groups/groups.ts @@ -3,7 +3,6 @@ import type { IMessage, IRoom, ITeam, IGetRoomRoles, IUser, IUpload, IIntegratio import type { PaginatedResult } from '../../helpers/PaginatedResult'; import type { GroupsArchiveProps } from './GroupsArchiveProps'; import type { GroupsMembersProps } from './GroupsMembersProps'; -import type { GroupsMembersByHighestRoleProps } from './GroupsMembersByHighestRoleProps'; import type { GroupsFilesProps } from './GroupsFilesProps'; import type { GroupsUnarchiveProps } from './GroupsUnarchiveProps'; import type { GroupsCreateProps } from './GroupsCreateProps'; @@ -55,7 +54,7 @@ export type GroupsEndpoints = { }; }; '/v1/groups.membersByHighestRole': { - GET: (params: GroupsMembersByHighestRoleProps) => { + GET: (params: GroupsMembersProps) => { count: number; offset: number; members: IUser[]; diff --git a/packages/rest-typings/src/v1/groups/index.ts b/packages/rest-typings/src/v1/groups/index.ts index fc7955e72932..49907d3a08e5 100644 --- a/packages/rest-typings/src/v1/groups/index.ts +++ b/packages/rest-typings/src/v1/groups/index.ts @@ -10,7 +10,6 @@ export * from './GroupsFilesProps'; export * from './GroupsKickProps'; export * from './GroupsLeaveProps'; export * from './GroupsMembersProps'; -export * from './GroupsMembersByHighestRoleProps'; export * from './GroupsMessageProps'; export * from './GroupsRolesProps'; export * from './GroupsUnarchiveProps'; From 66f3ce578a637486dfbbebbfe1b743e7322f86b0 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Fri, 21 Jul 2023 12:49:27 -0300 Subject: [PATCH 05/42] Remove let on aggregation --- apps/meteor/server/models/raw/Users.js | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index 97749222d87b..ea44a02f5d82 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -318,7 +318,6 @@ export class UsersRaw extends BaseRaw { localField: '_id', foreignField: 'u._id', as: 'sub', - let: { sub_rid: '$rid' }, pipeline: [ { $match: { From b8f313a827f39b49eca19b09fd09cd74a4e08631 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Fri, 21 Jul 2023 13:21:29 -0300 Subject: [PATCH 06/42] Improve typing --- apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts b/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts index 05a587c4cd65..596e49efc498 100644 --- a/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts +++ b/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts @@ -4,6 +4,11 @@ import { Users } from '@rocket.chat/models'; import { settings } from '../../app/settings/server'; +type UserWithRoleInfo = IUser & { + highestRole: string; + roleLevel: number; +}; + type FindUsersParam = { rid: string; status?: FilterOperators; @@ -20,7 +25,7 @@ export async function findUsersOfRoomByHighestRole({ limit = 0, filter = '', sort, -}: FindUsersParam): Promise> { +}: FindUsersParam): Promise> { const options = { projection: { name: 1, From c838f9f68cf75df0de79b52efe05b07f9679e488 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Fri, 21 Jul 2023 13:27:09 -0300 Subject: [PATCH 07/42] Improve formatting --- apps/meteor/server/models/raw/Users.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index ea44a02f5d82..955cf319c469 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -320,11 +320,7 @@ export class UsersRaw extends BaseRaw { as: 'sub', pipeline: [ { - $match: { - $expr: { - $eq: ['$rid', rid], - }, - }, + $match: { $expr: { $eq: ['$rid', rid] } }, }, ], }, From d6f0dcc85f1de360b15824f8c73e3a727d081cf0 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Fri, 21 Jul 2023 13:32:10 -0300 Subject: [PATCH 08/42] Evaluate length as boolean --- apps/meteor/server/models/raw/Users.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index 955cf319c469..d0c787af4168 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -246,10 +246,10 @@ export class UsersRaw extends BaseRaw { active: true, username: { $exists: true, - ...(exceptions.length > 0 && { $nin: exceptions }), + ...(exceptions.length && { $nin: exceptions }), }, // if the search term is empty, don't need to have the $or statement (because it would be an empty regex) - ...(searchTerm && orStmt.length > 0 && { $or: orStmt }), + ...(searchTerm && orStmt.length && { $or: orStmt }), }, ...extraQuery, ], @@ -284,7 +284,7 @@ export class UsersRaw extends BaseRaw { __rooms: rid, username: { $exists: true }, // if the search term is empty, don't need to have the $or statement (because it would be an empty regex) - ...(searchTerm && orStmt.length > 0 && { $or: orStmt }), + ...(searchTerm && orStmt.length && { $or: orStmt }), }, ...extraQuery, ], From c2246752853832a87a0ae4000e227e9c5b302f85 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Fri, 21 Jul 2023 13:39:46 -0300 Subject: [PATCH 09/42] Create new variable to regex string --- apps/meteor/server/models/raw/Users.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index d0c787af4168..c4358fda9861 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -233,7 +233,8 @@ export class UsersRaw extends BaseRaw { exceptions = [exceptions]; } - const termRegex = new RegExp((startsWith ? '^' : '') + escapeRegExp(searchTerm) + (endsWith ? '$' : ''), 'i'); + const regexString = (startsWith ? '^' : '') + escapeRegExp(searchTerm) + (endsWith ? '$' : ''); + const termRegex = new RegExp(regexString, 'i'); const orStmt = (searchFields || []).reduce(function (acc, el) { acc.push({ [el.trim()]: termRegex }); @@ -270,7 +271,8 @@ export class UsersRaw extends BaseRaw { options = {}; } - const termRegex = new RegExp((startsWith ? '^' : '') + escapeRegExp(searchTerm) + (endsWith ? '$' : ''), 'i'); + const regexString = (startsWith ? '^' : '') + escapeRegExp(searchTerm) + (endsWith ? '$' : ''); + const termRegex = new RegExp(regexString, 'i'); const orStmt = (searchFields || []).reduce(function (acc, el) { acc.push({ [el.trim()]: termRegex }); From 6e87c8c7a82cbe35272d41ea5ff6935f9e9fb6c3 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Fri, 21 Jul 2023 13:45:34 -0300 Subject: [PATCH 10/42] Make options field optional --- apps/meteor/app/api/server/v1/im.ts | 2 +- apps/meteor/server/lib/findUsersOfRoom.ts | 2 +- .../server/lib/findUsersOfRoomByHighestRole.ts | 2 +- apps/meteor/server/methods/browseChannels.ts | 2 +- apps/meteor/server/models/raw/Users.js | 13 +++---------- packages/model-typings/src/models/IUsersModel.ts | 4 ++-- 6 files changed, 9 insertions(+), 16 deletions(-) diff --git a/apps/meteor/app/api/server/v1/im.ts b/apps/meteor/app/api/server/v1/im.ts index b18c691bc962..5e29d2e4ea1b 100644 --- a/apps/meteor/app/api/server/v1/im.ts +++ b/apps/meteor/app/api/server/v1/im.ts @@ -324,7 +324,7 @@ API.v1.addRoute( const searchFields = settings.get('Accounts_SearchFields').trim().split(','); - const { cursor, totalCount } = Users.findPaginatedByActiveUsersExcept(filter, [], options, searchFields, [extraQuery]); + const { cursor, totalCount } = Users.findPaginatedByActiveUsersExcept(filter, [], searchFields, options, [extraQuery]); const [members, total] = await Promise.all([cursor.toArray(), totalCount]); diff --git a/apps/meteor/server/lib/findUsersOfRoom.ts b/apps/meteor/server/lib/findUsersOfRoom.ts index 0ff7853a1c66..49544ee0106a 100644 --- a/apps/meteor/server/lib/findUsersOfRoom.ts +++ b/apps/meteor/server/lib/findUsersOfRoom.ts @@ -35,7 +35,7 @@ export function findUsersOfRoom({ rid, status, skip = 0, limit = 0, filter = '', const searchFields = settings.get('Accounts_SearchFields').trim().split(','); - return Users.findPaginatedByActiveUsersExcept(filter, undefined, options, searchFields, [ + return Users.findPaginatedByActiveUsersExcept(filter, undefined, searchFields, options, [ { __rooms: rid, ...(status && { status }), diff --git a/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts b/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts index 596e49efc498..85af42b16489 100644 --- a/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts +++ b/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts @@ -47,7 +47,7 @@ export async function findUsersOfRoomByHighestRole({ const searchFields = settings.get('Accounts_SearchFields').trim().split(','); - return Users.findPaginatedActiveUsersByRoomIdWithHighestRole(filter, rid, options, searchFields, [ + return Users.findPaginatedActiveUsersByRoomIdWithHighestRole(filter, rid, searchFields, options, [ { ...(status && { status }), }, diff --git a/apps/meteor/server/methods/browseChannels.ts b/apps/meteor/server/methods/browseChannels.ts index 540d5a754987..924d0ec6335e 100644 --- a/apps/meteor/server/methods/browseChannels.ts +++ b/apps/meteor/server/methods/browseChannels.ts @@ -225,7 +225,7 @@ const findUsers = async ({ }; if (workspace === 'all') { - const { cursor, totalCount } = Users.findPaginatedByActiveUsersExcept(text, [], options, searchFields); + const { cursor, totalCount } = Users.findPaginatedByActiveUsersExcept(text, [], searchFields, options); const [results, total] = await Promise.all([cursor.toArray(), totalCount]); return { total, diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index c4358fda9861..49466fa72d69 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -218,17 +218,14 @@ export class UsersRaw extends BaseRaw { findPaginatedByActiveUsersExcept( searchTerm, exceptions, - options, searchFields, + options = {}, extraQuery = [], { startsWith = false, endsWith = false } = {}, ) { if (exceptions == null) { exceptions = []; } - if (options == null) { - options = {}; - } if (!Array.isArray(exceptions)) { exceptions = [exceptions]; } @@ -262,15 +259,11 @@ export class UsersRaw extends BaseRaw { findPaginatedActiveUsersByRoomIdWithHighestRole( searchTerm, rid, - options, searchFields, + options = {}, extraQuery = [], { startsWith = false, endsWith = false } = {}, ) { - if (options == null) { - options = {}; - } - const regexString = (startsWith ? '^' : '') + escapeRegExp(searchTerm) + (endsWith ? '$' : ''); const termRegex = new RegExp(regexString, 'i'); @@ -373,7 +366,7 @@ export class UsersRaw extends BaseRaw { findPaginatedByActiveExternalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localDomain) { const extraQuery = [{ federation: { $exists: true } }, { 'federation.origin': { $ne: localDomain } }]; - return this.findPaginatedByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery); + return this.findPaginatedByActiveUsersExcept(searchTerm, exceptions, forcedSearchFields, options, extraQuery); } findActive(query, options = {}) { diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index 427dd9f6d070..62688756bc6e 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -39,16 +39,16 @@ export interface IUsersModel extends IBaseModel { findPaginatedByActiveUsersExcept( searchTerm: any, exceptions: any, - options: any, searchFields: any, + options?: any, extraQuery?: any, params?: { startsWith?: boolean; endsWith?: boolean }, ): FindPaginated>; findPaginatedActiveUsersByRoomIdWithHighestRole( searchTerm: any, rid: any, - options: any, searchFields: any, + options?: any, extraQuery?: any, params?: { startsWith?: boolean; endsWith?: boolean }, ): Promise>; From 120ce5d89fb613fb98f37fd38ecdfcadd1590358 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Fri, 21 Jul 2023 13:52:11 -0300 Subject: [PATCH 11/42] Return highestRole object instead of fields --- apps/meteor/server/models/raw/Users.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index 49466fa72d69..9d18f0019211 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -330,10 +330,11 @@ export class UsersRaw extends BaseRaw { { $addFields: { highestRole: { - $cond: [{ $in: ['owner', '$roles'] }, 'owner', { $cond: [{ $in: ['moderator', '$roles'] }, 'moderator', 'member'] }], - }, - roleLevel: { - $cond: [{ $in: ['owner', '$roles'] }, 0, { $cond: [{ $in: ['moderator', '$roles'] }, 1, 2] }], + $cond: [ + { $in: ['owner', '$roles'] }, + { role: 'owner', level: 0 }, + { $cond: [{ $in: ['moderator', '$roles'] }, { role: 'moderator', level: 1 }, { role: 'member', level: 2 }] }, + ], }, }, }, @@ -342,7 +343,7 @@ export class UsersRaw extends BaseRaw { members: [ { $sort: { - roleLevel: 1, + 'highestRole.level': 1, ...options.sort, }, }, From c6252da737a681e1cfd1e5a905ab946044de3775 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Fri, 21 Jul 2023 14:07:31 -0300 Subject: [PATCH 12/42] Update reduce to map --- apps/meteor/server/models/raw/Users.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index 9d18f0019211..9dfd261ffd9f 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -192,10 +192,7 @@ export class UsersRaw extends BaseRaw { const termRegex = new RegExp((startsWith ? '^' : '') + escapeRegExp(searchTerm) + (endsWith ? '$' : ''), 'i'); - const orStmt = (searchFields || []).reduce(function (acc, el) { - acc.push({ [el.trim()]: termRegex }); - return acc; - }, []); + const orStmt = (searchFields || []).map((el) => ({ [el.trim()]: termRegex })); const query = { $and: [ @@ -233,10 +230,7 @@ export class UsersRaw extends BaseRaw { const regexString = (startsWith ? '^' : '') + escapeRegExp(searchTerm) + (endsWith ? '$' : ''); const termRegex = new RegExp(regexString, 'i'); - const orStmt = (searchFields || []).reduce(function (acc, el) { - acc.push({ [el.trim()]: termRegex }); - return acc; - }, []); + const orStmt = (searchFields || []).map((el) => ({ [el.trim()]: termRegex })); const query = { $and: [ From d17470e0339b5e25ba42260ac8c35f0f34c7db3f Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Tue, 25 Jul 2023 14:47:20 -0300 Subject: [PATCH 13/42] Fix params order --- apps/meteor/server/models/raw/Users.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index 9dfd261ffd9f..0cbeb40d979f 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -261,10 +261,7 @@ export class UsersRaw extends BaseRaw { const regexString = (startsWith ? '^' : '') + escapeRegExp(searchTerm) + (endsWith ? '$' : ''); const termRegex = new RegExp(regexString, 'i'); - const orStmt = (searchFields || []).reduce(function (acc, el) { - acc.push({ [el.trim()]: termRegex }); - return acc; - }, []); + const orStmt = (searchFields || []).map((el) => ({ [el.trim()]: termRegex })); const query = { $and: [ @@ -356,7 +353,7 @@ export class UsersRaw extends BaseRaw { $or: [{ federation: { $exists: false } }, { 'federation.origin': localDomain }], }, ]; - return this.findPaginatedByActiveUsersExcept(searchTerm, exceptions, options, forcedSearchFields, extraQuery); + return this.findPaginatedByActiveUsersExcept(searchTerm, exceptions, forcedSearchFields, options, extraQuery); } findPaginatedByActiveExternalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localDomain) { From 569b80abe458531235040ca5e55a6d3648e7fd0f Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Mon, 31 Jul 2023 12:20:34 -0300 Subject: [PATCH 14/42] Fix lint --- apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts b/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts index 85af42b16489..1a5ed66495c6 100644 --- a/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts +++ b/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts @@ -1,6 +1,6 @@ import type { IUser } from '@rocket.chat/core-typings'; -import type { AggregationCursor, FilterOperators } from 'mongodb'; import { Users } from '@rocket.chat/models'; +import type { AggregationCursor, FilterOperators } from 'mongodb'; import { settings } from '../../app/settings/server'; From 46c8a187ec580d05e4426ed93f36d480f3aec64c Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Mon, 31 Jul 2023 12:57:10 -0300 Subject: [PATCH 15/42] Add allowDiskUse aggregation option --- apps/meteor/server/models/raw/Users.js | 93 +++++++++++++------------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index 51829f6b8f14..dc1bf4f6f67b 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -306,57 +306,60 @@ export class UsersRaw extends BaseRaw { ] : []; - return this.col.aggregate([ - { - $match: query, - }, - { - $lookup: { - from: 'rocketchat_subscription', - localField: '_id', - foreignField: 'u._id', - as: 'sub', - pipeline: [ - { - $match: { $expr: { $eq: ['$rid', rid] } }, - }, - ], - }, - }, - { $unwind: '$sub' }, - { - $project: { - ...options.projection, - roles: '$sub.roles', + return this.col.aggregate( + [ + { + $match: query, }, - }, - { - $addFields: { - highestRole: { - $cond: [ - { $in: ['owner', '$roles'] }, - { role: 'owner', level: 0 }, - { $cond: [{ $in: ['moderator', '$roles'] }, { role: 'moderator', level: 1 }, { role: 'member', level: 2 }] }, + { + $lookup: { + from: 'rocketchat_subscription', + localField: '_id', + foreignField: 'u._id', + as: 'sub', + pipeline: [ + { + $match: { $expr: { $eq: ['$rid', rid] } }, + }, ], }, }, - }, - { - $facet: { - members: [ - { - $sort: { - 'highestRole.level': 1, - ...options.sort, - }, + { $unwind: '$sub' }, + { + $project: { + ...options.projection, + roles: '$sub.roles', + }, + }, + { + $addFields: { + highestRole: { + $cond: [ + { $in: ['owner', '$roles'] }, + { role: 'owner', level: 0 }, + { $cond: [{ $in: ['moderator', '$roles'] }, { role: 'moderator', level: 1 }, { role: 'member', level: 2 }] }, + ], }, - ...skip, - ...limit, - ], - totalCount: [{ $group: { _id: null, total: { $sum: 1 } } }], + }, }, - }, - ]); + { + $facet: { + members: [ + { + $sort: { + 'highestRole.level': 1, + ...options.sort, + }, + }, + ...skip, + ...limit, + ], + totalCount: [{ $group: { _id: null, total: { $sum: 1 } } }], + }, + }, + ], + { allowDiskUse: true }, + ); } findPaginatedByActiveLocalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localDomain) { From 96168d3601ec1c3ee8a69a7a43809d76fa518143 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Mon, 31 Jul 2023 15:59:40 -0300 Subject: [PATCH 16/42] Use ajv on channels.membersByHighestRole endpoint typing --- apps/meteor/app/api/server/v1/channels.ts | 8 --- apps/meteor/app/api/server/v1/groups.ts | 10 ---- .../ChannelsMembersByHighestRoleProps.ts | 51 +++++++++++++++++++ .../rest-typings/src/v1/channels/channels.ts | 7 +-- .../groups/GroupsMembersByHighestRoleProps.ts | 51 +++++++++++++++++++ 5 files changed, 104 insertions(+), 23 deletions(-) create mode 100644 packages/rest-typings/src/v1/channels/ChannelsMembersByHighestRoleProps.ts create mode 100644 packages/rest-typings/src/v1/groups/GroupsMembersByHighestRoleProps.ts diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index b92f59d38cb6..1355ecc64633 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -1061,14 +1061,6 @@ API.v1.addRoute( const { offset: skip, count: limit } = await getPaginationItems(this.queryParams); const { sort = {} } = await this.parseJsonQuery(); - - check( - this.queryParams, - Match.ObjectIncluding({ - status: Match.Maybe([String]), - filter: Match.Maybe(String), - }), - ); const { status, filter } = this.queryParams; const cursor = await findUsersOfRoomByHighestRole({ diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index 712719109510..bb0a7382228f 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -755,16 +755,6 @@ API.v1.addRoute( const { offset: skip, count: limit } = await getPaginationItems(this.queryParams); const { sort = {} } = await this.parseJsonQuery(); - - check( - this.queryParams, - Match.ObjectIncluding({ - role: Match.Maybe(String), - status: Match.Maybe([String]), - filter: Match.Maybe(String), - }), - ); - const { status, filter } = this.queryParams; const cursor = await findUsersOfRoomByHighestRole({ diff --git a/packages/rest-typings/src/v1/channels/ChannelsMembersByHighestRoleProps.ts b/packages/rest-typings/src/v1/channels/ChannelsMembersByHighestRoleProps.ts new file mode 100644 index 000000000000..a8a5fba6afd5 --- /dev/null +++ b/packages/rest-typings/src/v1/channels/ChannelsMembersByHighestRoleProps.ts @@ -0,0 +1,51 @@ +import type { IRoom } from '@rocket.chat/core-typings'; + +import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; +import { ajv } from '../Ajv'; + +type MembersByHighestRoleProps = { + roomId?: IRoom['_id']; + roomName?: IRoom['name']; + status?: string[]; + filter?: string; +}; + +export type ChannelsMembersByHighestRoleProps = PaginatedRequest; + +const membersByHighestRolePropsSchema = { + properties: { + roomId: { + type: 'string', + }, + roomName: { + type: 'string', + }, + status: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + filter: { + type: 'string', + nullable: true, + }, + count: { + type: 'integer', + nullable: true, + }, + offset: { + type: 'integer', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + }, + oneOf: [{ required: ['roomId'] }, { required: ['roomName'] }], + additionalProperties: false, +}; + +export const isChannelsMembersByHighestRoleProps = ajv.compile(membersByHighestRolePropsSchema); diff --git a/packages/rest-typings/src/v1/channels/channels.ts b/packages/rest-typings/src/v1/channels/channels.ts index aefc44ccb0ec..2441ecf92bc0 100644 --- a/packages/rest-typings/src/v1/channels/channels.ts +++ b/packages/rest-typings/src/v1/channels/channels.ts @@ -15,6 +15,7 @@ import type { ChannelsJoinProps } from './ChannelsJoinProps'; import type { ChannelsKickProps } from './ChannelsKickProps'; import type { ChannelsLeaveProps } from './ChannelsLeaveProps'; import type { ChannelsListProps } from './ChannelsListProps'; +import type { ChannelsMembersByHighestRoleProps } from './ChannelsMembersByHighestRoleProps'; import type { ChannelsMessagesProps } from './ChannelsMessagesProps'; import type { ChannelsModeratorsProps } from './ChannelsModeratorsProps'; import type { ChannelsOnlineProps } from './ChannelsOnlineProps'; @@ -48,11 +49,7 @@ export type ChannelsEndpoints = { }>; }; '/v1/channels.membersByHighestRole': { - GET: ( - params: PaginatedRequest< - { roomId: string; filter?: string; status?: string[] } | { roomName: string; filter?: string; status?: string[] } - >, - ) => PaginatedResult<{ + GET: (params: ChannelsMembersByHighestRoleProps) => PaginatedResult<{ members: IUser[]; }>; }; diff --git a/packages/rest-typings/src/v1/groups/GroupsMembersByHighestRoleProps.ts b/packages/rest-typings/src/v1/groups/GroupsMembersByHighestRoleProps.ts new file mode 100644 index 000000000000..b20d37dfc3c0 --- /dev/null +++ b/packages/rest-typings/src/v1/groups/GroupsMembersByHighestRoleProps.ts @@ -0,0 +1,51 @@ +import type { IRoom } from '@rocket.chat/core-typings'; + +import type { PaginatedRequest } from '../../helpers/PaginatedRequest'; +import { ajv } from '../Ajv'; + +type MembersByHighestRoleProps = { + roomId?: IRoom['_id']; + roomName?: IRoom['name']; + status?: string[]; + filter?: string; +}; + +export type GroupsMembersByHighestRoleProps = PaginatedRequest; + +const membersByHighestRolePropsSchema = { + properties: { + roomId: { + type: 'string', + }, + roomName: { + type: 'string', + }, + status: { + type: 'array', + items: { + type: 'string', + }, + nullable: true, + }, + filter: { + type: 'string', + nullable: true, + }, + count: { + type: 'integer', + nullable: true, + }, + offset: { + type: 'integer', + nullable: true, + }, + sort: { + type: 'string', + nullable: true, + }, + }, + oneOf: [{ required: ['roomId'] }, { required: ['roomName'] }], + additionalProperties: false, +}; + +export const isGroupsMembersByHighestRoleProps = ajv.compile(membersByHighestRolePropsSchema); From 03538984b118f9d395275feb4b242cb8814318b8 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Wed, 2 Aug 2023 12:56:47 -0300 Subject: [PATCH 17/42] Add end-to-end tests --- .../tests/end-to-end/api/02-channels.js | 214 ++++++++++++++++++ apps/meteor/tests/end-to-end/api/03-groups.js | 214 ++++++++++++++++++ 2 files changed, 428 insertions(+) diff --git a/apps/meteor/tests/end-to-end/api/02-channels.js b/apps/meteor/tests/end-to-end/api/02-channels.js index d9d094b716d2..9f3d13768574 100644 --- a/apps/meteor/tests/end-to-end/api/02-channels.js +++ b/apps/meteor/tests/end-to-end/api/02-channels.js @@ -1133,6 +1133,220 @@ describe('[Channels]', function () { }); }); + describe('/channels.membersByHighestRole', () => { + let testChannel; + let testUser; + before('create a channel', async () => { + testUser = await createUser(); + const result = await createRoom({ + type: 'c', + name: `channel-test-highest-role-${Date.now()}`, + members: [testUser.username, 'rocket.cat'], + }); + testChannel = result.body.channel; + }); + before('assign roles to the users added to the channel', async () => { + await request.post(api('channels.addLeader')).set(credentials).send({ + roomId: testChannel._id, + userId: testUser._id, + }); + await request.post(api('channels.addModerator')).set(credentials).send({ + roomId: testChannel._id, + userId: 'rocket.cat', + }); + }); + + it('should return an array of members by channel when roomId is provided', (done) => { + request + .get(api('channels.membersByHighestRole')) + .set(credentials) + .query({ + roomId: testChannel._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('members').and.to.be.an('array'); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('offset'); + }) + .end(done); + }); + + it('should return an array of members by channel when roomName is provided', (done) => { + request + .get(api('channels.membersByHighestRole')) + .set(credentials) + .query({ + roomName: testChannel.name, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('members').and.to.be.an('array'); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('offset'); + }) + .end(done); + }); + + it('should return an array of members by channel even when requested with count and offset params', (done) => { + request + .get(api('channels.membersByHighestRole')) + .set(credentials) + .query({ + roomId: testChannel._id, + count: 5, + offset: 0, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('members').and.to.be.an('array'); + expect(res.body).to.have.property('count', 3); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('offset'); + }) + .end(done); + }); + + it('should return a filtered array of members by channel', (done) => { + request + .get(api('channels.membersByHighestRole')) + .set(credentials) + .query({ + roomId: testChannel._id, + filter: 'rocket.cat', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('members').and.to.be.an('array'); + expect(res.body).to.have.property('count', 1); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('offset'); + + const member = res.body.members[0]; + expect(member).to.have.property('roles'); + expect(member).to.have.property('_id'); + expect(member).to.have.property('username'); + expect(member).to.have.property('name'); + expect(member).to.have.property('status'); + expect(member).to.have.property('highestRole'); + }) + .end(done); + }); + + it('should return the correct highest role when searching for a moderator user', (done) => { + request + .get(api('channels.membersByHighestRole')) + .set(credentials) + .query({ + roomId: testChannel._id, + filter: 'rocket.cat', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('members').and.to.be.an('array'); + + const member = res.body.members[0]; + expect(member).to.have.property('highestRole'); + expect(member.roles).to.have.length(1); + expect(member.roles[0]).to.be.equal('moderator'); + + const { highestRole } = member; + expect(highestRole).to.have.property('role', 'moderator'); + expect(highestRole).to.have.property('level', 1); + }) + .end(done); + }); + + it('should return the correct highest role when searching for a moderator user', (done) => { + request + .get(api('channels.membersByHighestRole')) + .set(credentials) + .query({ + roomId: testChannel._id, + filter: adminUsername, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('members').and.to.be.an('array'); + + const member = res.body.members[0]; + expect(member).to.have.property('highestRole'); + expect(member.roles).to.have.length(1); + expect(member.roles[0]).to.be.equal('owner'); + + const { highestRole } = member; + expect(highestRole).to.have.property('role', 'owner'); + expect(highestRole).to.have.property('level', 0); + }) + .end(done); + }); + + it('should return the correct highest role when searching for a leader', (done) => { + request + .get(api('channels.membersByHighestRole')) + .set(credentials) + .query({ + roomId: testChannel._id, + filter: testUser.username, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('members').and.to.be.an('array'); + + const member = res.body.members[0]; + expect(member).to.have.property('highestRole'); + expect(member.roles).to.have.length(1); + expect(member.roles[0]).to.be.equal('leader'); + + const { highestRole } = member; + expect(highestRole).to.have.property('role', 'member'); + expect(highestRole).to.have.property('level', 2); + }) + .end(done); + }); + + it('should return members correctly sorted by highest role', (done) => { + request + .get(api('channels.membersByHighestRole')) + .set(credentials) + .query({ + roomId: testChannel._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('members').and.to.be.an('array'); + expect(res.body.members).to.have.length(3); + + const highestRoles = ['owner', 'moderator', 'member']; + for (let i = 0; i < 3; i++) { + const member = res.body.members[i]; + expect(member).to.have.property('highestRole'); + expect(member.highestRole).to.have.property('role', highestRoles[i]); + expect(member.highestRole).to.have.property('level', i); + } + }) + .end(done); + }); + }); + it('/channels.rename', async () => { const roomInfo = await getRoomInfo(channel._id); diff --git a/apps/meteor/tests/end-to-end/api/03-groups.js b/apps/meteor/tests/end-to-end/api/03-groups.js index 3f62322efd84..6afccdc30701 100644 --- a/apps/meteor/tests/end-to-end/api/03-groups.js +++ b/apps/meteor/tests/end-to-end/api/03-groups.js @@ -863,6 +863,220 @@ describe('[Groups]', function () { }); }); + describe('/groups.membersByHighestRole', () => { + let testGroup; + let testUser; + before('create a group', async () => { + testUser = await createUser(); + const result = await createRoom({ + type: 'p', + name: `group-test-highest-role-${Date.now()}`, + members: [testUser.username, 'rocket.cat'], + }); + testGroup = result.body.group; + }); + before('assign roles to the users added to the group', async () => { + await request.post(api('groups.addLeader')).set(credentials).send({ + roomId: testGroup._id, + userId: testUser._id, + }); + await request.post(api('groups.addModerator')).set(credentials).send({ + roomId: testGroup._id, + userId: 'rocket.cat', + }); + }); + + it('should return an array of members by channel when roomId is provided', (done) => { + request + .get(api('groups.membersByHighestRole')) + .set(credentials) + .query({ + roomId: testGroup._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('members').and.to.be.an('array'); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('offset'); + }) + .end(done); + }); + + it('should return an array of members by channel when roomName is provided', (done) => { + request + .get(api('groups.membersByHighestRole')) + .set(credentials) + .query({ + roomName: testGroup.name, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('members').and.to.be.an('array'); + expect(res.body).to.have.property('count'); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('offset'); + }) + .end(done); + }); + + it('should return an array of members by channel even when requested with count and offset params', (done) => { + request + .get(api('groups.membersByHighestRole')) + .set(credentials) + .query({ + roomId: testGroup._id, + count: 5, + offset: 0, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('members').and.to.be.an('array'); + expect(res.body).to.have.property('count', 3); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('offset'); + }) + .end(done); + }); + + it('should return a filtered array of members by channel', (done) => { + request + .get(api('groups.membersByHighestRole')) + .set(credentials) + .query({ + roomId: testGroup._id, + filter: 'rocket.cat', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('members').and.to.be.an('array'); + expect(res.body).to.have.property('count', 1); + expect(res.body).to.have.property('total'); + expect(res.body).to.have.property('offset'); + + const member = res.body.members[0]; + expect(member).to.have.property('roles'); + expect(member).to.have.property('_id'); + expect(member).to.have.property('username'); + expect(member).to.have.property('name'); + expect(member).to.have.property('status'); + expect(member).to.have.property('highestRole'); + }) + .end(done); + }); + + it('should return the correct highest role when searching for a moderator user', (done) => { + request + .get(api('groups.membersByHighestRole')) + .set(credentials) + .query({ + roomId: testGroup._id, + filter: 'rocket.cat', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('members').and.to.be.an('array'); + + const member = res.body.members[0]; + expect(member).to.have.property('highestRole'); + expect(member.roles).to.have.length(1); + expect(member.roles[0]).to.be.equal('moderator'); + + const { highestRole } = member; + expect(highestRole).to.have.property('role', 'moderator'); + expect(highestRole).to.have.property('level', 1); + }) + .end(done); + }); + + it('should return the correct highest role when searching for a moderator user', (done) => { + request + .get(api('groups.membersByHighestRole')) + .set(credentials) + .query({ + roomId: testGroup._id, + filter: adminUsername, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('members').and.to.be.an('array'); + + const member = res.body.members[0]; + expect(member).to.have.property('highestRole'); + expect(member.roles).to.have.length(1); + expect(member.roles[0]).to.be.equal('owner'); + + const { highestRole } = member; + expect(highestRole).to.have.property('role', 'owner'); + expect(highestRole).to.have.property('level', 0); + }) + .end(done); + }); + + it('should return the correct highest role when searching for a leader', (done) => { + request + .get(api('groups.membersByHighestRole')) + .set(credentials) + .query({ + roomId: testGroup._id, + filter: testUser.username, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('members').and.to.be.an('array'); + + const member = res.body.members[0]; + expect(member).to.have.property('highestRole'); + expect(member.roles).to.have.length(1); + expect(member.roles[0]).to.be.equal('leader'); + + const { highestRole } = member; + expect(highestRole).to.have.property('role', 'member'); + expect(highestRole).to.have.property('level', 2); + }) + .end(done); + }); + + it('should return members correctly sorted by highest role', (done) => { + request + .get(api('groups.membersByHighestRole')) + .set(credentials) + .query({ + roomId: testGroup._id, + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('members').and.to.be.an('array'); + expect(res.body.members).to.have.length(3); + + const highestRoles = ['owner', 'moderator', 'member']; + for (let i = 0; i < 3; i++) { + const member = res.body.members[i]; + expect(member).to.have.property('highestRole'); + expect(member.highestRole).to.have.property('role', highestRoles[i]); + expect(member.highestRole).to.have.property('level', i); + } + }) + .end(done); + }); + }); + describe('[/groups.files]', async () => { await testFileUploads('groups.files', group); }); From 5a668fc025d2bfc180656ab473238c697e96fd56 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Wed, 2 Aug 2023 21:36:29 -0300 Subject: [PATCH 18/42] Make lookup query compatible with mongo 4.4 --- apps/meteor/server/models/raw/Users.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index dc1bf4f6f67b..2c7907fbbeb5 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -314,12 +314,15 @@ export class UsersRaw extends BaseRaw { { $lookup: { from: 'rocketchat_subscription', - localField: '_id', - foreignField: 'u._id', + let: { id: '$_id' }, as: 'sub', pipeline: [ { - $match: { $expr: { $eq: ['$rid', rid] } }, + $match: { + $expr: { + $and: [{ $eq: ['$u._id', '$$id'] }, { $eq: ['$rid', rid] }], + }, + }, }, ], }, From 1a736d56b4404ef90c0af5836cb807e95ae9f702 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Thu, 3 Aug 2023 12:05:49 -0300 Subject: [PATCH 19/42] Check if result is undefined before extracting props --- apps/meteor/app/api/server/v1/channels.ts | 3 +++ apps/meteor/app/api/server/v1/groups.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index 1355ecc64633..0ca84f9bd4fc 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -1073,6 +1073,9 @@ API.v1.addRoute( }); const result = await cursor.toArray(); + if (!result?.[0]) { + return API.v1.failure('No user info could be found for the channel provided'); + } const { members, totalCount: [{ total } = { total: 0 }], diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index bb0a7382228f..afd1fcca4884 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -767,6 +767,9 @@ API.v1.addRoute( }); const result = await cursor.toArray(); + if (!result?.[0]) { + return API.v1.failure('No user info could be found for the group provided'); + } const { members, totalCount: [{ total } = { total: 0 }], From 2f5ccc4f8b332ce1b5a3c3977d3200e01ff8ba2a Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Thu, 10 Aug 2023 17:19:11 -0300 Subject: [PATCH 20/42] Improve tests --- apps/meteor/tests/end-to-end/api/02-channels.js | 9 +++++++-- apps/meteor/tests/end-to-end/api/03-groups.js | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/02-channels.js b/apps/meteor/tests/end-to-end/api/02-channels.js index 9f3d13768574..72c83c69334d 100644 --- a/apps/meteor/tests/end-to-end/api/02-channels.js +++ b/apps/meteor/tests/end-to-end/api/02-channels.js @@ -4,7 +4,7 @@ import { getCredentials, api, request, credentials, apiPublicChannelName, channe import { CI_MAX_ROOMS_PER_GUEST as maxRoomsPerGuest } from '../../data/constants'; import { createIntegration, removeIntegration } from '../../data/integration.helper'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; -import { createRoom } from '../../data/rooms.helper'; +import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { testFileUploads } from '../../data/uploads.helper'; import { adminUsername, password } from '../../data/user'; import { createUser, login, deleteUser } from '../../data/users.helper'; @@ -1155,6 +1155,10 @@ describe('[Channels]', function () { userId: 'rocket.cat', }); }); + after(async () => { + await deleteUser(testUser); + await deleteRoom(testChannel); + }); it('should return an array of members by channel when roomId is provided', (done) => { request @@ -1239,6 +1243,7 @@ describe('[Channels]', function () { expect(member).to.have.property('name'); expect(member).to.have.property('status'); expect(member).to.have.property('highestRole'); + expect(member).to.have.property('statusConnection'); }) .end(done); }); @@ -1269,7 +1274,7 @@ describe('[Channels]', function () { .end(done); }); - it('should return the correct highest role when searching for a moderator user', (done) => { + it('should return the correct highest role when searching for an owner user', (done) => { request .get(api('channels.membersByHighestRole')) .set(credentials) diff --git a/apps/meteor/tests/end-to-end/api/03-groups.js b/apps/meteor/tests/end-to-end/api/03-groups.js index 6afccdc30701..08254398d5bc 100644 --- a/apps/meteor/tests/end-to-end/api/03-groups.js +++ b/apps/meteor/tests/end-to-end/api/03-groups.js @@ -4,7 +4,7 @@ import { getCredentials, api, request, credentials, group, apiPrivateChannelName import { CI_MAX_ROOMS_PER_GUEST as maxRoomsPerGuest } from '../../data/constants'; import { createIntegration, removeIntegration } from '../../data/integration.helper'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; -import { createRoom } from '../../data/rooms.helper'; +import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { testFileUploads } from '../../data/uploads.helper'; import { adminUsername, password } from '../../data/user'; import { createUser, login, deleteUser } from '../../data/users.helper'; @@ -885,6 +885,10 @@ describe('[Groups]', function () { userId: 'rocket.cat', }); }); + after(async () => { + await deleteUser(testUser); + await deleteRoom(testGroup); + }); it('should return an array of members by channel when roomId is provided', (done) => { request @@ -969,6 +973,7 @@ describe('[Groups]', function () { expect(member).to.have.property('name'); expect(member).to.have.property('status'); expect(member).to.have.property('highestRole'); + expect(member).to.have.property('statusConnection'); }) .end(done); }); @@ -999,7 +1004,7 @@ describe('[Groups]', function () { .end(done); }); - it('should return the correct highest role when searching for a moderator user', (done) => { + it('should return the correct highest role when searching for an owner user', (done) => { request .get(api('groups.membersByHighestRole')) .set(credentials) From a0e011be048e6e55173f0efda627c26ccdd3d709 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Thu, 10 Aug 2023 17:19:47 -0300 Subject: [PATCH 21/42] Improve typing --- .../meteor/server/lib/findUsersOfRoomByHighestRole.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts b/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts index 1a5ed66495c6..20b17a7427fc 100644 --- a/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts +++ b/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts @@ -1,14 +1,9 @@ -import type { IUser } from '@rocket.chat/core-typings'; +import type { IUserWithRoleInfo } from '@rocket.chat/core-typings'; import { Users } from '@rocket.chat/models'; import type { AggregationCursor, FilterOperators } from 'mongodb'; import { settings } from '../../app/settings/server'; -type UserWithRoleInfo = IUser & { - highestRole: string; - roleLevel: number; -}; - type FindUsersParam = { rid: string; status?: FilterOperators; @@ -25,7 +20,7 @@ export async function findUsersOfRoomByHighestRole({ limit = 0, filter = '', sort, -}: FindUsersParam): Promise> { +}: FindUsersParam): Promise> { const options = { projection: { name: 1, @@ -38,7 +33,7 @@ export async function findUsersOfRoomByHighestRole({ statusConnection: 1, }, sort: { - statusConnection: -1, + statusConnection: -1 as const, ...(sort || { ...(settings.get('UI_Use_Real_Name') && { name: 1 }), username: 1 }), }, skip, From c6ffd7b80c0ac8b2228961a50fea901e58cbf590 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Thu, 10 Aug 2023 17:30:49 -0300 Subject: [PATCH 22/42] Change roles object key --- apps/meteor/server/models/raw/Users.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index 3319abede627..858e8cdc7040 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -345,16 +345,16 @@ export class UsersRaw extends BaseRaw { { $project: { ...options.projection, - roles: '$sub.roles', + roomRoles: '$sub.roles', }, }, { $addFields: { highestRole: { $cond: [ - { $in: ['owner', '$roles'] }, + { $in: ['owner', '$roomRoles'] }, { role: 'owner', level: 0 }, - { $cond: [{ $in: ['moderator', '$roles'] }, { role: 'moderator', level: 1 }, { role: 'member', level: 2 }] }, + { $cond: [{ $in: ['moderator', '$roomRoles'] }, { role: 'moderator', level: 1 }, { role: 'member', level: 2 }] }, ], }, }, From dba1c87c5931fa341c3fb2b82a25a476add39863 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Thu, 10 Aug 2023 17:46:38 -0300 Subject: [PATCH 23/42] Add IUserWithRoles to core-typings --- packages/core-typings/src/IUser.ts | 8 ++++++ .../model-typings/src/models/IUsersModel.ts | 25 +++++++++++++------ packages/rest-typings/src/v1/groups/groups.ts | 14 +++++++++-- 3 files changed, 38 insertions(+), 9 deletions(-) diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index 74b3491e0d4b..52a6e72dddd4 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -191,6 +191,14 @@ export interface IRegisterUser extends IUser { export const isRegisterUser = (user: IUser): user is IRegisterUser => user.username !== undefined && user.name !== undefined; export const isUserFederated = (user: Partial) => 'federated' in user && user.federated === true; +export interface IUserWithRoleInfo extends IUser { + roomRoles?: IRole['_id'][]; + highestRole: { + role: string; + level: number; + }; +} + export type IUserDataEvent = { id: unknown; } & ( diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index a12f615da954..d2aeba3d618b 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -8,8 +8,19 @@ import type { IPersonalAccessToken, AtLeast, ILivechatAgentStatus, + IUserWithRoleInfo, } from '@rocket.chat/core-typings'; -import type { Document, UpdateResult, FindCursor, FindOptions, Filter, InsertOneResult, DeleteResult, AggregationCursor } from 'mongodb'; +import type { + Document, + UpdateResult, + FindCursor, + FindOptions, + Filter, + FilterOperators, + InsertOneResult, + DeleteResult, + AggregationCursor, +} from 'mongodb'; import type { FindPaginated, IBaseModel } from './IBaseModel'; @@ -44,12 +55,12 @@ export interface IUsersModel extends IBaseModel { extraQuery?: any, params?: { startsWith?: boolean; endsWith?: boolean }, ): FindPaginated>; - findPaginatedActiveUsersByRoomIdWithHighestRole( - searchTerm: any, - rid: any, - searchFields: any, - options?: any, - extraQuery?: any, + findPaginatedActiveUsersByRoomIdWithHighestRole( + searchTerm: string, + rid: string, + searchFields: string[], + options?: FindOptions, + extraQuery?: FilterOperators[], params?: { startsWith?: boolean; endsWith?: boolean }, ): Promise>; diff --git a/packages/rest-typings/src/v1/groups/groups.ts b/packages/rest-typings/src/v1/groups/groups.ts index 3007b8c91b3c..76b98f0a639f 100644 --- a/packages/rest-typings/src/v1/groups/groups.ts +++ b/packages/rest-typings/src/v1/groups/groups.ts @@ -1,4 +1,14 @@ -import type { IMessage, IRoom, ITeam, IGetRoomRoles, IUser, IUpload, IIntegration, ISubscription } from '@rocket.chat/core-typings'; +import type { + IMessage, + IRoom, + ITeam, + IGetRoomRoles, + IUser, + IUpload, + IIntegration, + ISubscription, + IUserWithRoleInfo, +} from '@rocket.chat/core-typings'; import type { PaginatedResult } from '../../helpers/PaginatedResult'; import type { GroupsAddAllProps } from './GroupsAddAllProps'; @@ -57,7 +67,7 @@ export type GroupsEndpoints = { GET: (params: GroupsMembersProps) => { count: number; offset: number; - members: IUser[]; + members: IUserWithRoleInfo[]; total: number; }; }; From 61f97dc46c0da31cefd9a98bd7a1da53d244b15e Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Mon, 14 Aug 2023 10:01:34 -0300 Subject: [PATCH 24/42] Fix tests --- apps/meteor/tests/end-to-end/api/02-channels.js | 2 +- apps/meteor/tests/end-to-end/api/03-groups.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/02-channels.js b/apps/meteor/tests/end-to-end/api/02-channels.js index 72c83c69334d..26a0a81a4a76 100644 --- a/apps/meteor/tests/end-to-end/api/02-channels.js +++ b/apps/meteor/tests/end-to-end/api/02-channels.js @@ -1157,7 +1157,7 @@ describe('[Channels]', function () { }); after(async () => { await deleteUser(testUser); - await deleteRoom(testChannel); + await deleteRoom({ type: 'c', roomId: testChannel._id }); }); it('should return an array of members by channel when roomId is provided', (done) => { diff --git a/apps/meteor/tests/end-to-end/api/03-groups.js b/apps/meteor/tests/end-to-end/api/03-groups.js index 08254398d5bc..fd722f73744c 100644 --- a/apps/meteor/tests/end-to-end/api/03-groups.js +++ b/apps/meteor/tests/end-to-end/api/03-groups.js @@ -887,7 +887,7 @@ describe('[Groups]', function () { }); after(async () => { await deleteUser(testUser); - await deleteRoom(testGroup); + await deleteRoom({ type: 'p', roomId: testGroup._id }); }); it('should return an array of members by channel when roomId is provided', (done) => { From 5b5a46ea9ba1be65a2da52def26e6c7ff2f4067c Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Wed, 16 Aug 2023 16:08:53 -0300 Subject: [PATCH 25/42] Fix tests --- apps/meteor/tests/end-to-end/api/02-channels.js | 2 +- apps/meteor/tests/end-to-end/api/03-groups.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/02-channels.js b/apps/meteor/tests/end-to-end/api/02-channels.js index 2214a6e197fc..dd86c05fcb82 100644 --- a/apps/meteor/tests/end-to-end/api/02-channels.js +++ b/apps/meteor/tests/end-to-end/api/02-channels.js @@ -1238,7 +1238,7 @@ describe('[Channels]', function () { expect(res.body).to.have.property('offset'); const member = res.body.members[0]; - expect(member).to.have.property('roles'); + expect(member).to.have.property('roomRoles'); expect(member).to.have.property('_id'); expect(member).to.have.property('username'); expect(member).to.have.property('name'); diff --git a/apps/meteor/tests/end-to-end/api/03-groups.js b/apps/meteor/tests/end-to-end/api/03-groups.js index 8b3fc2d7d1e1..d3e98490d8e3 100644 --- a/apps/meteor/tests/end-to-end/api/03-groups.js +++ b/apps/meteor/tests/end-to-end/api/03-groups.js @@ -968,7 +968,7 @@ describe('[Groups]', function () { expect(res.body).to.have.property('offset'); const member = res.body.members[0]; - expect(member).to.have.property('roles'); + expect(member).to.have.property('roomRoles'); expect(member).to.have.property('_id'); expect(member).to.have.property('username'); expect(member).to.have.property('name'); From ae7355d41ccd0252cc2cb747013cfb22dd7301a1 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Wed, 16 Aug 2023 16:10:51 -0300 Subject: [PATCH 26/42] Fix endpoint fails for members with no roles --- apps/meteor/server/models/raw/Users.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index 858e8cdc7040..47c08f3c86b3 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -345,7 +345,7 @@ export class UsersRaw extends BaseRaw { { $project: { ...options.projection, - roomRoles: '$sub.roles', + roomRoles: { $ifNull: ['$sub.roles', []] }, }, }, { From cb83fbf9d323081c29a66d48b22bc72651c5bf42 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Thu, 17 Aug 2023 14:31:10 -0300 Subject: [PATCH 27/42] Fix tests --- apps/meteor/tests/end-to-end/api/02-channels.js | 2 +- apps/meteor/tests/end-to-end/api/03-groups.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/02-channels.js b/apps/meteor/tests/end-to-end/api/02-channels.js index dd86c05fcb82..019f9ccdcc4f 100644 --- a/apps/meteor/tests/end-to-end/api/02-channels.js +++ b/apps/meteor/tests/end-to-end/api/02-channels.js @@ -1226,7 +1226,7 @@ describe('[Channels]', function () { .set(credentials) .query({ roomId: testChannel._id, - filter: 'rocket.cat', + filter: testUser.username, }) .expect('Content-Type', 'application/json') .expect(200) diff --git a/apps/meteor/tests/end-to-end/api/03-groups.js b/apps/meteor/tests/end-to-end/api/03-groups.js index d3e98490d8e3..0094e57470a4 100644 --- a/apps/meteor/tests/end-to-end/api/03-groups.js +++ b/apps/meteor/tests/end-to-end/api/03-groups.js @@ -950,13 +950,13 @@ describe('[Groups]', function () { .end(done); }); - it('should return a filtered array of members by channel', (done) => { + it('should return a filtered array of members by group', (done) => { request .get(api('groups.membersByHighestRole')) .set(credentials) .query({ roomId: testGroup._id, - filter: 'rocket.cat', + filter: testUser.username, }) .expect('Content-Type', 'application/json') .expect(200) From d4ed1d39b4d1cbc14a9634e824990ede768bd5eb Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Thu, 17 Aug 2023 17:51:11 -0300 Subject: [PATCH 28/42] Remove statusConnection check on tests --- apps/meteor/tests/end-to-end/api/02-channels.js | 1 - apps/meteor/tests/end-to-end/api/03-groups.js | 1 - 2 files changed, 2 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/02-channels.js b/apps/meteor/tests/end-to-end/api/02-channels.js index 019f9ccdcc4f..6fdfa8034774 100644 --- a/apps/meteor/tests/end-to-end/api/02-channels.js +++ b/apps/meteor/tests/end-to-end/api/02-channels.js @@ -1244,7 +1244,6 @@ describe('[Channels]', function () { expect(member).to.have.property('name'); expect(member).to.have.property('status'); expect(member).to.have.property('highestRole'); - expect(member).to.have.property('statusConnection'); }) .end(done); }); diff --git a/apps/meteor/tests/end-to-end/api/03-groups.js b/apps/meteor/tests/end-to-end/api/03-groups.js index 0094e57470a4..66f1ecb16030 100644 --- a/apps/meteor/tests/end-to-end/api/03-groups.js +++ b/apps/meteor/tests/end-to-end/api/03-groups.js @@ -974,7 +974,6 @@ describe('[Groups]', function () { expect(member).to.have.property('name'); expect(member).to.have.property('status'); expect(member).to.have.property('highestRole'); - expect(member).to.have.property('statusConnection'); }) .end(done); }); From a11dc35bf653773c77c22d382fc18def12c47a78 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Fri, 18 Aug 2023 10:23:37 -0300 Subject: [PATCH 29/42] Update roles to roomRoles in tests --- apps/meteor/tests/end-to-end/api/02-channels.js | 8 ++++---- apps/meteor/tests/end-to-end/api/03-groups.js | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/02-channels.js b/apps/meteor/tests/end-to-end/api/02-channels.js index 6fdfa8034774..6db1690efece 100644 --- a/apps/meteor/tests/end-to-end/api/02-channels.js +++ b/apps/meteor/tests/end-to-end/api/02-channels.js @@ -1264,8 +1264,8 @@ describe('[Channels]', function () { const member = res.body.members[0]; expect(member).to.have.property('highestRole'); - expect(member.roles).to.have.length(1); - expect(member.roles[0]).to.be.equal('moderator'); + expect(member.roomRoles).to.have.length(1); + expect(member.roomRoles[0]).to.be.equal('moderator'); const { highestRole } = member; expect(highestRole).to.have.property('role', 'moderator'); @@ -1290,8 +1290,8 @@ describe('[Channels]', function () { const member = res.body.members[0]; expect(member).to.have.property('highestRole'); - expect(member.roles).to.have.length(1); - expect(member.roles[0]).to.be.equal('owner'); + expect(member.roomRoles).to.have.length(1); + expect(member.roomRoles[0]).to.be.equal('owner'); const { highestRole } = member; expect(highestRole).to.have.property('role', 'owner'); diff --git a/apps/meteor/tests/end-to-end/api/03-groups.js b/apps/meteor/tests/end-to-end/api/03-groups.js index 66f1ecb16030..040fdf1c098e 100644 --- a/apps/meteor/tests/end-to-end/api/03-groups.js +++ b/apps/meteor/tests/end-to-end/api/03-groups.js @@ -994,8 +994,8 @@ describe('[Groups]', function () { const member = res.body.members[0]; expect(member).to.have.property('highestRole'); - expect(member.roles).to.have.length(1); - expect(member.roles[0]).to.be.equal('moderator'); + expect(member.roomRoles).to.have.length(1); + expect(member.roomRoles[0]).to.be.equal('moderator'); const { highestRole } = member; expect(highestRole).to.have.property('role', 'moderator'); @@ -1020,8 +1020,8 @@ describe('[Groups]', function () { const member = res.body.members[0]; expect(member).to.have.property('highestRole'); - expect(member.roles).to.have.length(1); - expect(member.roles[0]).to.be.equal('owner'); + expect(member.roomRoles).to.have.length(1); + expect(member.roomRoles[0]).to.be.equal('owner'); const { highestRole } = member; expect(highestRole).to.have.property('role', 'owner'); From 9164db8b4f936d66455bd7d2ce879c0b35b2433d Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Fri, 18 Aug 2023 11:35:48 -0300 Subject: [PATCH 30/42] oops --- apps/meteor/tests/end-to-end/api/02-channels.js | 4 ++-- apps/meteor/tests/end-to-end/api/03-groups.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/02-channels.js b/apps/meteor/tests/end-to-end/api/02-channels.js index 6db1690efece..828f41099483 100644 --- a/apps/meteor/tests/end-to-end/api/02-channels.js +++ b/apps/meteor/tests/end-to-end/api/02-channels.js @@ -1316,8 +1316,8 @@ describe('[Channels]', function () { const member = res.body.members[0]; expect(member).to.have.property('highestRole'); - expect(member.roles).to.have.length(1); - expect(member.roles[0]).to.be.equal('leader'); + expect(member.roomRoles).to.have.length(1); + expect(member.roomRoles[0]).to.be.equal('leader'); const { highestRole } = member; expect(highestRole).to.have.property('role', 'member'); diff --git a/apps/meteor/tests/end-to-end/api/03-groups.js b/apps/meteor/tests/end-to-end/api/03-groups.js index 040fdf1c098e..45b730fc19b3 100644 --- a/apps/meteor/tests/end-to-end/api/03-groups.js +++ b/apps/meteor/tests/end-to-end/api/03-groups.js @@ -1046,8 +1046,8 @@ describe('[Groups]', function () { const member = res.body.members[0]; expect(member).to.have.property('highestRole'); - expect(member.roles).to.have.length(1); - expect(member.roles[0]).to.be.equal('leader'); + expect(member.roomRoles).to.have.length(1); + expect(member.roomRoles[0]).to.be.equal('leader'); const { highestRole } = member; expect(highestRole).to.have.property('role', 'member'); From 5e3f29c8bb314ba88284e81ecd360c51d86f9035 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Fri, 18 Aug 2023 13:35:01 -0300 Subject: [PATCH 31/42] Improve translations --- apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json | 10 ++++++---- .../packages/rocketchat-i18n/i18n/pt-BR.i18n.json | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 918e4278eced..b51286b8010d 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -751,16 +751,18 @@ "Bio": "Bio", "Bio_Placeholder": "Bio Placeholder", "Block": "Block", - "Block_Multiple_Failed_Logins_Attempts_Until_Block_By_Ip": "How many failed attempts until block by IP", - "Block_Multiple_Failed_Logins_Attempts_Until_Block_by_User": "How many failed attempts until block by User", + "Block_Multiple_Failed_Logins_Attempts_Until_Block_By_Ip": "Amount of failed attempts before block by IP", + "Block_Multiple_Failed_Logins_Attempts_Until_Block_by_User": "Amount of failed attempts before block by User", "Block_Multiple_Failed_Logins_By_Ip": "Block failed login attempts by IP", "Block_Multiple_Failed_Logins_By_User": "Block failed login attempts by Username", "Block_Multiple_Failed_Logins_Enable_Collect_Login_data_Description": "Stores IP and username from log in attempts to a collection on database", "Block_Multiple_Failed_Logins_Enabled": "Enable collect log in data", "Block_Multiple_Failed_Logins_Ip_Whitelist": "IP Whitelist", "Block_Multiple_Failed_Logins_Ip_Whitelist_Description": "Comma-separated list of whitelisted IPs", - "Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes": "Time to unblock IP (In Minutes)", - "Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes": "Time to unblock User (In Minutes)", + "Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes": "Duration of IP address block (in minutes)", + "Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes_Description": "This is the time the IP address is blocked by, and the time in which the failed attempts can happen before the counter resets", + "Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes": "Duration of user block (in minutes)", + "Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes_Description": "This is the time the user is blocked by, and the time in which the failed attempts can happen before the counter resets", "Block_Multiple_Failed_Logins_Notify_Failed": "Notify of failed login attempts", "Block_Multiple_Failed_Logins_Notify_Failed_Channel": "Channel to send the notifications", "Block_Multiple_Failed_Logins_Notify_Failed_Channel_Desc": "This is where notifications will be received. Make sure the channel exists. The channel name should not include # symbol", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index 10735c8f1d29..cc19a4ef809f 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -674,16 +674,18 @@ "Better": "Melhor", "Bio": "Biografia", "Bio_Placeholder": "Placeholder da biografia", - "Block_Multiple_Failed_Logins_Attempts_Until_Block_By_Ip": "Quantas tentativas falhas até bloquear por IP", - "Block_Multiple_Failed_Logins_Attempts_Until_Block_by_User": "Quantas tentativas falhas até bloquear por usuário", + "Block_Multiple_Failed_Logins_Attempts_Until_Block_By_Ip": "Quantidade de tentativas falhas antes de bloquear por IP", + "Block_Multiple_Failed_Logins_Attempts_Until_Block_by_User": "Quantidade de tentativas falhas antes de bloquear por usuário", "Block_Multiple_Failed_Logins_By_Ip": "Bloquear tentativas falhas de login por IP", "Block_Multiple_Failed_Logins_By_User": "Bloquear tentativas falhas de login por nome de usuário", "Block_Multiple_Failed_Logins_Enable_Collect_Login_data_Description": "Armazena IP e nome de usuário das tentativas de login em uma coleção no banco de dados", "Block_Multiple_Failed_Logins_Enabled": "Habilitar a coleta de dados do login", "Block_Multiple_Failed_Logins_Ip_Whitelist": "Lista de IPs permitidos", "Block_Multiple_Failed_Logins_Ip_Whitelist_Description": "Lista de IPs permitidos separados por vírgulas", - "Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes": "Tempo para desbloquear o IP (em minutos)", - "Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes": "Tempo para desbloquear o usuário (em minutos)", + "Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes": "Duração do bloqueio de IP (em minutos)", + "Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes_Description": "Esse é o tempo que o IP é bloqueado, e o tempo em que as tentativas falhas podem ocorrer antes do contador ser reiniciado", + "Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes": "Duração do bloqueio de usuário (em minutos)", + "Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes_Description": "Esse é o tempo que o usuário é bloqueado, e o tempo em que as tentativas falhas podem ocorrer antes do contador ser reiniciado", "Block_Multiple_Failed_Logins_Notify_Failed": "Notificar tentativas falhas de login", "Block_Multiple_Failed_Logins_Notify_Failed_Channel": "Canal para enviar notificações", "Block_Multiple_Failed_Logins_Notify_Failed_Channel_Desc": "Aqui é o local em que as notificações serão recebidas. Certifique-se de que o canal exista. O nome do canal não deve incluir o símbolo #", From a85e403719ca5d56a3f4867614d577034877c0a5 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Fri, 18 Aug 2023 13:39:19 -0300 Subject: [PATCH 32/42] Revert "Improve translations" This reverts commit 5e3f29c8bb314ba88284e81ecd360c51d86f9035. --- apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json | 10 ++++------ .../packages/rocketchat-i18n/i18n/pt-BR.i18n.json | 10 ++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index b51286b8010d..918e4278eced 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -751,18 +751,16 @@ "Bio": "Bio", "Bio_Placeholder": "Bio Placeholder", "Block": "Block", - "Block_Multiple_Failed_Logins_Attempts_Until_Block_By_Ip": "Amount of failed attempts before block by IP", - "Block_Multiple_Failed_Logins_Attempts_Until_Block_by_User": "Amount of failed attempts before block by User", + "Block_Multiple_Failed_Logins_Attempts_Until_Block_By_Ip": "How many failed attempts until block by IP", + "Block_Multiple_Failed_Logins_Attempts_Until_Block_by_User": "How many failed attempts until block by User", "Block_Multiple_Failed_Logins_By_Ip": "Block failed login attempts by IP", "Block_Multiple_Failed_Logins_By_User": "Block failed login attempts by Username", "Block_Multiple_Failed_Logins_Enable_Collect_Login_data_Description": "Stores IP and username from log in attempts to a collection on database", "Block_Multiple_Failed_Logins_Enabled": "Enable collect log in data", "Block_Multiple_Failed_Logins_Ip_Whitelist": "IP Whitelist", "Block_Multiple_Failed_Logins_Ip_Whitelist_Description": "Comma-separated list of whitelisted IPs", - "Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes": "Duration of IP address block (in minutes)", - "Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes_Description": "This is the time the IP address is blocked by, and the time in which the failed attempts can happen before the counter resets", - "Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes": "Duration of user block (in minutes)", - "Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes_Description": "This is the time the user is blocked by, and the time in which the failed attempts can happen before the counter resets", + "Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes": "Time to unblock IP (In Minutes)", + "Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes": "Time to unblock User (In Minutes)", "Block_Multiple_Failed_Logins_Notify_Failed": "Notify of failed login attempts", "Block_Multiple_Failed_Logins_Notify_Failed_Channel": "Channel to send the notifications", "Block_Multiple_Failed_Logins_Notify_Failed_Channel_Desc": "This is where notifications will be received. Make sure the channel exists. The channel name should not include # symbol", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index cc19a4ef809f..10735c8f1d29 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -674,18 +674,16 @@ "Better": "Melhor", "Bio": "Biografia", "Bio_Placeholder": "Placeholder da biografia", - "Block_Multiple_Failed_Logins_Attempts_Until_Block_By_Ip": "Quantidade de tentativas falhas antes de bloquear por IP", - "Block_Multiple_Failed_Logins_Attempts_Until_Block_by_User": "Quantidade de tentativas falhas antes de bloquear por usuário", + "Block_Multiple_Failed_Logins_Attempts_Until_Block_By_Ip": "Quantas tentativas falhas até bloquear por IP", + "Block_Multiple_Failed_Logins_Attempts_Until_Block_by_User": "Quantas tentativas falhas até bloquear por usuário", "Block_Multiple_Failed_Logins_By_Ip": "Bloquear tentativas falhas de login por IP", "Block_Multiple_Failed_Logins_By_User": "Bloquear tentativas falhas de login por nome de usuário", "Block_Multiple_Failed_Logins_Enable_Collect_Login_data_Description": "Armazena IP e nome de usuário das tentativas de login em uma coleção no banco de dados", "Block_Multiple_Failed_Logins_Enabled": "Habilitar a coleta de dados do login", "Block_Multiple_Failed_Logins_Ip_Whitelist": "Lista de IPs permitidos", "Block_Multiple_Failed_Logins_Ip_Whitelist_Description": "Lista de IPs permitidos separados por vírgulas", - "Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes": "Duração do bloqueio de IP (em minutos)", - "Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes_Description": "Esse é o tempo que o IP é bloqueado, e o tempo em que as tentativas falhas podem ocorrer antes do contador ser reiniciado", - "Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes": "Duração do bloqueio de usuário (em minutos)", - "Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes_Description": "Esse é o tempo que o usuário é bloqueado, e o tempo em que as tentativas falhas podem ocorrer antes do contador ser reiniciado", + "Block_Multiple_Failed_Logins_Time_To_Unblock_By_Ip_In_Minutes": "Tempo para desbloquear o IP (em minutos)", + "Block_Multiple_Failed_Logins_Time_To_Unblock_By_User_In_Minutes": "Tempo para desbloquear o usuário (em minutos)", "Block_Multiple_Failed_Logins_Notify_Failed": "Notificar tentativas falhas de login", "Block_Multiple_Failed_Logins_Notify_Failed_Channel": "Canal para enviar notificações", "Block_Multiple_Failed_Logins_Notify_Failed_Channel_Desc": "Aqui é o local em que as notificações serão recebidas. Certifique-se de que o canal exista. O nome do canal não deve incluir o símbolo #", From 78f908fe6f9caf1a12a3369ea4ecd8e9f796cc06 Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Fri, 18 Aug 2023 17:21:28 -0300 Subject: [PATCH 33/42] Create thick-swans-drop.md --- .changeset/thick-swans-drop.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/thick-swans-drop.md diff --git a/.changeset/thick-swans-drop.md b/.changeset/thick-swans-drop.md new file mode 100644 index 000000000000..fbdb5cc9d769 --- /dev/null +++ b/.changeset/thick-swans-drop.md @@ -0,0 +1,8 @@ +--- +"@rocket.chat/meteor": patch +"@rocket.chat/core-typings": patch +"@rocket.chat/model-typings": patch +"@rocket.chat/rest-typings": patch +--- + +Added `membersByHighestRole` endpoints, which enables users to retrieve room members sorted by their room-scoped roles From 865348da5d9c86683326d29fdec34090b6e9a6bb Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Mon, 21 Aug 2023 18:32:05 -0300 Subject: [PATCH 34/42] Update changesets Co-authored-by: Guilherme Gazzo --- .changeset/thick-swans-drop.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.changeset/thick-swans-drop.md b/.changeset/thick-swans-drop.md index fbdb5cc9d769..e4e670e2b64d 100644 --- a/.changeset/thick-swans-drop.md +++ b/.changeset/thick-swans-drop.md @@ -1,8 +1,8 @@ --- -"@rocket.chat/meteor": patch -"@rocket.chat/core-typings": patch -"@rocket.chat/model-typings": patch -"@rocket.chat/rest-typings": patch +"@rocket.chat/meteor": minor +"@rocket.chat/core-typings": minor +"@rocket.chat/model-typings": minor +"@rocket.chat/rest-typings": minor --- Added `membersByHighestRole` endpoints, which enables users to retrieve room members sorted by their room-scoped roles From 8689619afa2a137b333e263ac42cb082a8027eb6 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Thu, 31 Aug 2023 11:16:25 -0300 Subject: [PATCH 35/42] Improve aggregation performance --- .../lib/findUsersOfRoomByHighestRole.ts | 12 ++- .../meteor/server/models/raw/Subscriptions.ts | 14 ++++ apps/meteor/server/models/raw/Users.js | 78 ++++++++----------- packages/core-typings/src/IUser.ts | 1 - .../src/models/ISubscriptionsModel.ts | 1 + .../model-typings/src/models/IUsersModel.ts | 2 + 6 files changed, 59 insertions(+), 49 deletions(-) diff --git a/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts b/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts index 20b17a7427fc..38719ba2fa15 100644 --- a/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts +++ b/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts @@ -1,5 +1,5 @@ -import type { IUserWithRoleInfo } from '@rocket.chat/core-typings'; -import { Users } from '@rocket.chat/models'; +import type { IUserWithRoleInfo, ISubscription } from '@rocket.chat/core-typings'; +import { Users, Subscriptions } from '@rocket.chat/models'; import type { AggregationCursor, FilterOperators } from 'mongodb'; import { settings } from '../../app/settings/server'; @@ -42,7 +42,13 @@ export async function findUsersOfRoomByHighestRole({ const searchFields = settings.get('Accounts_SearchFields').trim().split(','); - return Users.findPaginatedActiveUsersByRoomIdWithHighestRole(filter, rid, searchFields, options, [ + const ownersIds = (await Subscriptions.findByRoomIdAndHighestRole(rid, 'owner', { projection: { 'u._id': 1 } }).toArray()).map( + (s: ISubscription) => s.u?._id, + ); + const moderatorsIds = (await Subscriptions.findByRoomIdAndHighestRole(rid, 'moderator', { projection: { 'u._id': 1 } }).toArray()).map( + (s: ISubscription) => s.u?._id, + ); + return Users.findPaginatedActiveUsersByRoomIdWithHighestRole(filter, rid, searchFields, ownersIds, moderatorsIds, options, [ { ...(status && { status }), }, diff --git a/apps/meteor/server/models/raw/Subscriptions.ts b/apps/meteor/server/models/raw/Subscriptions.ts index 90cce9116116..65104e9200c4 100644 --- a/apps/meteor/server/models/raw/Subscriptions.ts +++ b/apps/meteor/server/models/raw/Subscriptions.ts @@ -943,6 +943,20 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.find(query, options); } + findByRoomIdAndHighestRole(roomId: string, role: string, options?: FindOptions): FindCursor { + // roles ordered from lowest to highest + const mainRoles = ['moderator', 'owner']; + + const roleIndex = mainRoles.indexOf(role || ''); + const rolesToExclude = mainRoles.slice(roleIndex + 1); + const query = { + rid: roomId, + roles: { $eq: role, $nin: rolesToExclude }, + }; + + return this.find(query, options); + } + countByRoomIdAndRoles(roomId: string, roles: string[]): Promise { roles = ([] as string[]).concat(roles); const query = { diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index 10e80eec1579..7b44468194f2 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -326,6 +326,8 @@ export class UsersRaw extends BaseRaw { searchTerm, rid, searchFields, + ownersIds, + moderatorsIds, options = {}, extraQuery = [], { startsWith = false, endsWith = false } = {}, @@ -334,19 +336,20 @@ export class UsersRaw extends BaseRaw { const termRegex = new RegExp(regexString, 'i'); const orStmt = (searchFields || []).map((el) => ({ [el.trim()]: termRegex })); + const userSearchConditions = [ + { + active: true, + __rooms: rid, + username: { $exists: true }, + // if the search term is empty, don't need to have the $or statement (because it would be an empty regex) + ...(searchTerm && orStmt.length && { $or: orStmt }), + }, + ...extraQuery, + ]; - const query = { - $and: [ - { - active: true, - __rooms: rid, - username: { $exists: true }, - // if the search term is empty, don't need to have the $or statement (because it would be an empty regex) - ...(searchTerm && orStmt.length && { $or: orStmt }), - }, - ...extraQuery, - ], - }; + const ownerQuery = { $and: [...userSearchConditions, { _id: { $in: ownersIds } }] }; + const moderatorQuery = { $and: [...userSearchConditions, { _id: { $in: moderatorsIds } }] }; + const memberQuery = { $and: [...userSearchConditions, { _id: { $nin: [...ownersIds, ...moderatorsIds] } }] }; const skip = options.skip !== 0 @@ -369,42 +372,27 @@ export class UsersRaw extends BaseRaw { return this.col.aggregate( [ { - $match: query, - }, - { - $lookup: { - from: 'rocketchat_subscription', - let: { id: '$_id' }, - as: 'sub', - pipeline: [ - { - $match: { - $expr: { - $and: [{ $eq: ['$u._id', '$$id'] }, { $eq: ['$rid', rid] }], - }, - }, - }, + $facet: { + owners: [ + { $match: ownerQuery }, + { $project: options.projection }, + { $addFields: { highestRole: { role: 'owner', level: 0 } } }, + ], + moderators: [ + { $match: moderatorQuery }, + { $project: options.projection }, + { $addFields: { highestRole: { role: 'moderator', level: 1 } } }, + ], + members: [ + { $match: memberQuery }, + { $project: options.projection }, + { $addFields: { highestRole: { role: 'member', level: 2 } } }, ], }, }, - { $unwind: '$sub' }, - { - $project: { - ...options.projection, - roomRoles: { $ifNull: ['$sub.roles', []] }, - }, - }, - { - $addFields: { - highestRole: { - $cond: [ - { $in: ['owner', '$roomRoles'] }, - { role: 'owner', level: 0 }, - { $cond: [{ $in: ['moderator', '$roomRoles'] }, { role: 'moderator', level: 1 }, { role: 'member', level: 2 }] }, - ], - }, - }, - }, + { $project: { allMembers: { $concatArrays: ['$owners', '$moderators', '$members'] } } }, + { $unwind: '$allMembers' }, + { $replaceRoot: { newRoot: '$allMembers' } }, { $facet: { members: [ diff --git a/packages/core-typings/src/IUser.ts b/packages/core-typings/src/IUser.ts index edce75240381..a7985061fc37 100644 --- a/packages/core-typings/src/IUser.ts +++ b/packages/core-typings/src/IUser.ts @@ -192,7 +192,6 @@ export const isRegisterUser = (user: IUser): user is IRegisterUser => user.usern export const isUserFederated = (user: Partial) => 'federated' in user && user.federated === true; export interface IUserWithRoleInfo extends IUser { - roomRoles?: IRole['_id'][]; highestRole: { role: string; level: number; diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index aebda87c78cb..927420115e7b 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -148,6 +148,7 @@ export interface ISubscriptionsModel extends IBaseModel { options?: FindOptions, ): FindCursor; findByRoomIdAndRoles(roomId: string, roles: string[], options?: FindOptions): FindCursor; + findByRoomIdAndHighestRole(roomId: string, role: string, options?: FindOptions): FindCursor; findByRoomIdAndUserIds(roomId: string, userIds: string[], options?: FindOptions): FindCursor; findByUserIdUpdatedAfter(userId: string, updatedAt: Date, options?: FindOptions): FindCursor; diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index 31c70ea6150a..d1483cf7cbb0 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -59,6 +59,8 @@ export interface IUsersModel extends IBaseModel { searchTerm: string, rid: string, searchFields: string[], + ownersIds: IUser['_id'][], + moderatorsIds: IUser['_id'][], options?: FindOptions, extraQuery?: FilterOperators[], params?: { startsWith?: boolean; endsWith?: boolean }, From e6242ea9bc7a9112a0089cbbea61dfe96e2eb930 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Thu, 31 Aug 2023 11:16:44 -0300 Subject: [PATCH 36/42] Fix tests --- apps/meteor/tests/end-to-end/api/02-channels.js | 7 ------- apps/meteor/tests/end-to-end/api/03-groups.js | 7 ------- 2 files changed, 14 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/02-channels.js b/apps/meteor/tests/end-to-end/api/02-channels.js index 828f41099483..58eb42da1979 100644 --- a/apps/meteor/tests/end-to-end/api/02-channels.js +++ b/apps/meteor/tests/end-to-end/api/02-channels.js @@ -1238,7 +1238,6 @@ describe('[Channels]', function () { expect(res.body).to.have.property('offset'); const member = res.body.members[0]; - expect(member).to.have.property('roomRoles'); expect(member).to.have.property('_id'); expect(member).to.have.property('username'); expect(member).to.have.property('name'); @@ -1264,8 +1263,6 @@ describe('[Channels]', function () { const member = res.body.members[0]; expect(member).to.have.property('highestRole'); - expect(member.roomRoles).to.have.length(1); - expect(member.roomRoles[0]).to.be.equal('moderator'); const { highestRole } = member; expect(highestRole).to.have.property('role', 'moderator'); @@ -1290,8 +1287,6 @@ describe('[Channels]', function () { const member = res.body.members[0]; expect(member).to.have.property('highestRole'); - expect(member.roomRoles).to.have.length(1); - expect(member.roomRoles[0]).to.be.equal('owner'); const { highestRole } = member; expect(highestRole).to.have.property('role', 'owner'); @@ -1316,8 +1311,6 @@ describe('[Channels]', function () { const member = res.body.members[0]; expect(member).to.have.property('highestRole'); - expect(member.roomRoles).to.have.length(1); - expect(member.roomRoles[0]).to.be.equal('leader'); const { highestRole } = member; expect(highestRole).to.have.property('role', 'member'); diff --git a/apps/meteor/tests/end-to-end/api/03-groups.js b/apps/meteor/tests/end-to-end/api/03-groups.js index 45b730fc19b3..d535b86dc062 100644 --- a/apps/meteor/tests/end-to-end/api/03-groups.js +++ b/apps/meteor/tests/end-to-end/api/03-groups.js @@ -968,7 +968,6 @@ describe('[Groups]', function () { expect(res.body).to.have.property('offset'); const member = res.body.members[0]; - expect(member).to.have.property('roomRoles'); expect(member).to.have.property('_id'); expect(member).to.have.property('username'); expect(member).to.have.property('name'); @@ -994,8 +993,6 @@ describe('[Groups]', function () { const member = res.body.members[0]; expect(member).to.have.property('highestRole'); - expect(member.roomRoles).to.have.length(1); - expect(member.roomRoles[0]).to.be.equal('moderator'); const { highestRole } = member; expect(highestRole).to.have.property('role', 'moderator'); @@ -1020,8 +1017,6 @@ describe('[Groups]', function () { const member = res.body.members[0]; expect(member).to.have.property('highestRole'); - expect(member.roomRoles).to.have.length(1); - expect(member.roomRoles[0]).to.be.equal('owner'); const { highestRole } = member; expect(highestRole).to.have.property('role', 'owner'); @@ -1046,8 +1041,6 @@ describe('[Groups]', function () { const member = res.body.members[0]; expect(member).to.have.property('highestRole'); - expect(member.roomRoles).to.have.length(1); - expect(member.roomRoles[0]).to.be.equal('leader'); const { highestRole } = member; expect(highestRole).to.have.property('role', 'member'); From 432ac79337401ea5cd6bc3cb1b4ac93848ed8dda Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Tue, 26 Sep 2023 18:06:29 -0300 Subject: [PATCH 37/42] Remove skip param --- apps/meteor/app/api/server/v1/channels.ts | 4 +--- apps/meteor/app/api/server/v1/groups.ts | 4 +--- apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts | 2 -- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index 0ca84f9bd4fc..0c386b7c7ba2 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -1059,14 +1059,13 @@ API.v1.addRoute( return API.v1.unauthorized(); } - const { offset: skip, count: limit } = await getPaginationItems(this.queryParams); + const { count: limit } = await getPaginationItems(this.queryParams); const { sort = {} } = await this.parseJsonQuery(); const { status, filter } = this.queryParams; const cursor = await findUsersOfRoomByHighestRole({ rid: findResult._id, ...(status && { status: { $in: status } }), - skip, limit, filter, ...(sort?.username && { sort: { username: sort.username } }), @@ -1084,7 +1083,6 @@ API.v1.addRoute( return API.v1.success({ members, count: members.length, - offset: skip, total, }); }, diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index afd1fcca4884..5cbaec143ce2 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -753,14 +753,13 @@ API.v1.addRoute( return API.v1.unauthorized(); } - const { offset: skip, count: limit } = await getPaginationItems(this.queryParams); + const { count: limit } = await getPaginationItems(this.queryParams); const { sort = {} } = await this.parseJsonQuery(); const { status, filter } = this.queryParams; const cursor = await findUsersOfRoomByHighestRole({ rid: findResult.rid, ...(status && { status: { $in: status } }), - skip, limit, filter, ...(sort?.username && { sort: { username: sort.username } }), @@ -778,7 +777,6 @@ API.v1.addRoute( return API.v1.success({ members, count: members.length, - offset: skip, total, }); }, diff --git a/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts b/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts index 38719ba2fa15..7c1cd68eed9b 100644 --- a/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts +++ b/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts @@ -16,7 +16,6 @@ type FindUsersParam = { export async function findUsersOfRoomByHighestRole({ rid, status, - skip = 0, limit = 0, filter = '', sort, @@ -36,7 +35,6 @@ export async function findUsersOfRoomByHighestRole({ statusConnection: -1 as const, ...(sort || { ...(settings.get('UI_Use_Real_Name') && { name: 1 }), username: 1 }), }, - skip, limit, }; From 9627c99d676da597debf40f7fa6fa4ee18e7d6e6 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Thu, 28 Sep 2023 13:06:42 -0300 Subject: [PATCH 38/42] Replace aggregation by 3 finds --- apps/meteor/app/api/server/v1/channels.ts | 11 +- apps/meteor/app/api/server/v1/groups.ts | 11 +- .../lib/findUsersOfRoomByHighestRole.ts | 102 ++++++++++++--- .../meteor/server/models/raw/Subscriptions.ts | 14 -- apps/meteor/server/models/raw/Users.js | 123 +++++++----------- .../src/models/ISubscriptionsModel.ts | 1 - .../model-typings/src/models/IUsersModel.ts | 28 ++-- 7 files changed, 144 insertions(+), 146 deletions(-) diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index 0c386b7c7ba2..0ab14b0e0a0d 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -1063,7 +1063,7 @@ API.v1.addRoute( const { sort = {} } = await this.parseJsonQuery(); const { status, filter } = this.queryParams; - const cursor = await findUsersOfRoomByHighestRole({ + const { members, total } = await findUsersOfRoomByHighestRole({ rid: findResult._id, ...(status && { status: { $in: status } }), limit, @@ -1071,15 +1071,6 @@ API.v1.addRoute( ...(sort?.username && { sort: { username: sort.username } }), }); - const result = await cursor.toArray(); - if (!result?.[0]) { - return API.v1.failure('No user info could be found for the channel provided'); - } - const { - members, - totalCount: [{ total } = { total: 0 }], - } = result[0]; - return API.v1.success({ members, count: members.length, diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index 5cbaec143ce2..403ceb62bd9d 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -757,7 +757,7 @@ API.v1.addRoute( const { sort = {} } = await this.parseJsonQuery(); const { status, filter } = this.queryParams; - const cursor = await findUsersOfRoomByHighestRole({ + const { members, total } = await findUsersOfRoomByHighestRole({ rid: findResult.rid, ...(status && { status: { $in: status } }), limit, @@ -765,15 +765,6 @@ API.v1.addRoute( ...(sort?.username && { sort: { username: sort.username } }), }); - const result = await cursor.toArray(); - if (!result?.[0]) { - return API.v1.failure('No user info could be found for the group provided'); - } - const { - members, - totalCount: [{ total } = { total: 0 }], - } = result[0]; - return API.v1.success({ members, count: members.length, diff --git a/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts b/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts index 7c1cd68eed9b..10eeb13c996f 100644 --- a/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts +++ b/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts @@ -1,6 +1,6 @@ -import type { IUserWithRoleInfo, ISubscription } from '@rocket.chat/core-typings'; +import type { IUserWithRoleInfo, ISubscription, IUser } from '@rocket.chat/core-typings'; import { Users, Subscriptions } from '@rocket.chat/models'; -import type { AggregationCursor, FilterOperators } from 'mongodb'; +import type { FilterOperators, FindOptions } from 'mongodb'; import { settings } from '../../app/settings/server'; @@ -13,14 +13,62 @@ type FindUsersParam = { sort?: Record; }; +async function findUsersWithRolesOfRoom( + { rid, status, limit = 0, filter = '' }: FindUsersParam, + options: FindOptions, +): Promise<{ members: IUserWithRoleInfo[]; totalCount: number; allMembersIds: string[] }> { + // Sort roles in descending order so that owners are listed before moderators + // This could possibly cause issues with custom roles, but that's intended to improve performance + const highestRolesMembersSubscriptions = await Subscriptions.findByRoomIdAndRoles(rid, ['owner', 'moderator'], { + projection: { 'u._id': 1, 'roles': 1 }, + sort: { roles: -1 }, + }).toArray(); + + const searchFields = settings.get('Accounts_SearchFields').trim().split(','); + + const totalCount = highestRolesMembersSubscriptions.length; + const allMembersIds = highestRolesMembersSubscriptions.map((s: ISubscription) => s.u?._id); + const highestRolesMembersIdsToFind = allMembersIds.slice(0, limit); + + const { cursor } = Users.findPaginatedActiveUsersByIds(filter, searchFields, highestRolesMembersIdsToFind, options, [ + { + __rooms: rid, + ...(status && { status }), + }, + ]); + const highestRolesMembers = await cursor.toArray(); + + // maps for efficient lookup of highest roles and sort order + const ordering: Record = {}; + const highestRoleById: Record = {}; + + const limitedHighestRolesMembersSubs = highestRolesMembersSubscriptions.slice(0, limit); + limitedHighestRolesMembersSubs.forEach((sub, index) => { + ordering[sub.u._id] = index; + highestRoleById[sub.u._id] = sub.roles?.includes('owner') ? 'owner' : 'moderator'; + }); + + highestRolesMembers.sort((a, b) => ordering[a._id] - ordering[b._id]); + const membersWithHighestRoles = highestRolesMembers.map( + (member): IUserWithRoleInfo => ({ + ...member, + highestRole: { + role: highestRoleById[member._id], + level: highestRoleById[member._id] === 'owner' ? 0 : 1, + }, + }), + ); + return { members: membersWithHighestRoles, totalCount, allMembersIds }; +} + export async function findUsersOfRoomByHighestRole({ rid, status, limit = 0, filter = '', sort, -}: FindUsersParam): Promise> { - const options = { +}: FindUsersParam): Promise<{ members: IUserWithRoleInfo[]; total: number }> { + const options: FindOptions = { projection: { name: 1, username: 1, @@ -37,18 +85,42 @@ export async function findUsersOfRoomByHighestRole({ }, limit, }; - + const extraQuery = { + __rooms: rid, + ...(status && { status }), + }; const searchFields = settings.get('Accounts_SearchFields').trim().split(','); - const ownersIds = (await Subscriptions.findByRoomIdAndHighestRole(rid, 'owner', { projection: { 'u._id': 1 } }).toArray()).map( - (s: ISubscription) => s.u?._id, - ); - const moderatorsIds = (await Subscriptions.findByRoomIdAndHighestRole(rid, 'moderator', { projection: { 'u._id': 1 } }).toArray()).map( - (s: ISubscription) => s.u?._id, - ); - return Users.findPaginatedActiveUsersByRoomIdWithHighestRole(filter, rid, searchFields, ownersIds, moderatorsIds, options, [ - { - ...(status && { status }), - }, + // Find highest roles members (owners and moderator) + const { + members: highestRolesMembers, + totalCount: totalMembersWithRoles, + allMembersIds: highestRolesMembersIds, + } = await findUsersWithRolesOfRoom({ rid, status, limit, filter }, options); + + if (limit <= highestRolesMembers.length) { + const totalMembersCount = await Users.countActiveUsersExcept(filter, highestRolesMembersIds, searchFields, [extraQuery]); + return { members: highestRolesMembers, total: totalMembersWithRoles + totalMembersCount }; + } + if (options.limit) { + options.limit -= highestRolesMembers.length; + } + + // Find average members + const { cursor, totalCount } = Users.findPaginatedByActiveUsersExcept(filter, highestRolesMembersIds, searchFields, options, [ + extraQuery, ]); + const [members, totalMembersCount] = await Promise.all([await cursor.toArray(), totalCount]); + const membersWithHighestRoles = members.map( + (member): IUserWithRoleInfo => ({ + ...member, + highestRole: { + role: 'member', + level: 2, + }, + }), + ); + + const allMembers = highestRolesMembers.concat(membersWithHighestRoles); + return { members: allMembers, total: totalMembersWithRoles + totalMembersCount }; } diff --git a/apps/meteor/server/models/raw/Subscriptions.ts b/apps/meteor/server/models/raw/Subscriptions.ts index fcffedf1d813..4b42367bad05 100644 --- a/apps/meteor/server/models/raw/Subscriptions.ts +++ b/apps/meteor/server/models/raw/Subscriptions.ts @@ -943,20 +943,6 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri return this.find(query, options); } - findByRoomIdAndHighestRole(roomId: string, role: string, options?: FindOptions): FindCursor { - // roles ordered from lowest to highest - const mainRoles = ['moderator', 'owner']; - - const roleIndex = mainRoles.indexOf(role || ''); - const rolesToExclude = mainRoles.slice(roleIndex + 1); - const query = { - rid: roomId, - roles: { $eq: role, $nin: rolesToExclude }, - }; - - return this.find(query, options); - } - countByRoomIdAndRoles(roomId: string, roles: string[]): Promise { roles = ([] as string[]).concat(roles); const query = { diff --git a/apps/meteor/server/models/raw/Users.js b/apps/meteor/server/models/raw/Users.js index 7bf9e24d8a43..773314bdac22 100644 --- a/apps/meteor/server/models/raw/Users.js +++ b/apps/meteor/server/models/raw/Users.js @@ -311,10 +311,8 @@ export class UsersRaw extends BaseRaw { $and: [ { active: true, - username: { - $exists: true, - ...(exceptions.length && { $nin: exceptions }), - }, + ...(exceptions.length && { _id: { $nin: exceptions } }), + username: { $exists: true }, // if the search term is empty, don't need to have the $or statement (because it would be an empty regex) ...(searchTerm && orStmt.length && { $or: orStmt }), }, @@ -325,12 +323,39 @@ export class UsersRaw extends BaseRaw { return this.findPaginated(query, options); } - findPaginatedActiveUsersByRoomIdWithHighestRole( + countActiveUsersExcept(searchTerm, exceptions, searchFields, extraQuery = [], { startsWith = false, endsWith = false } = {}) { + if (exceptions == null) { + exceptions = []; + } + if (!Array.isArray(exceptions)) { + exceptions = [exceptions]; + } + + const regexString = (startsWith ? '^' : '') + escapeRegExp(searchTerm) + (endsWith ? '$' : ''); + const termRegex = new RegExp(regexString, 'i'); + + const orStmt = (searchFields || []).map((el) => ({ [el.trim()]: termRegex })); + + const query = { + $and: [ + { + active: true, + ...(exceptions.length && { _id: { $nin: exceptions } }), + username: { $exists: true }, + // if the search term is empty, don't need to have the $or statement (because it would be an empty regex) + ...(searchTerm && orStmt.length && { $or: orStmt }), + }, + ...extraQuery, + ], + }; + + return this.col.countDocuments(query); + } + + findPaginatedActiveUsersByIds( searchTerm, - rid, searchFields, - ownersIds, - moderatorsIds, + ids = [], options = {}, extraQuery = [], { startsWith = false, endsWith = false } = {}, @@ -339,81 +364,21 @@ export class UsersRaw extends BaseRaw { const termRegex = new RegExp(regexString, 'i'); const orStmt = (searchFields || []).map((el) => ({ [el.trim()]: termRegex })); - const userSearchConditions = [ - { - active: true, - __rooms: rid, - username: { $exists: true }, - // if the search term is empty, don't need to have the $or statement (because it would be an empty regex) - ...(searchTerm && orStmt.length && { $or: orStmt }), - }, - ...extraQuery, - ]; - - const ownerQuery = { $and: [...userSearchConditions, { _id: { $in: ownersIds } }] }; - const moderatorQuery = { $and: [...userSearchConditions, { _id: { $in: moderatorsIds } }] }; - const memberQuery = { $and: [...userSearchConditions, { _id: { $nin: [...ownersIds, ...moderatorsIds] } }] }; - - const skip = - options.skip !== 0 - ? [ - { - $skip: options.skip, - }, - ] - : []; - - const limit = - options.limit !== 0 - ? [ - { - $limit: options.limit, - }, - ] - : []; - return this.col.aggregate( - [ - { - $facet: { - owners: [ - { $match: ownerQuery }, - { $project: options.projection }, - { $addFields: { highestRole: { role: 'owner', level: 0 } } }, - ], - moderators: [ - { $match: moderatorQuery }, - { $project: options.projection }, - { $addFields: { highestRole: { role: 'moderator', level: 1 } } }, - ], - members: [ - { $match: memberQuery }, - { $project: options.projection }, - { $addFields: { highestRole: { role: 'member', level: 2 } } }, - ], - }, - }, - { $project: { allMembers: { $concatArrays: ['$owners', '$moderators', '$members'] } } }, - { $unwind: '$allMembers' }, - { $replaceRoot: { newRoot: '$allMembers' } }, + const query = { + $and: [ { - $facet: { - members: [ - { - $sort: { - 'highestRole.level': 1, - ...options.sort, - }, - }, - ...skip, - ...limit, - ], - totalCount: [{ $group: { _id: null, total: { $sum: 1 } } }], - }, + active: true, + ...(ids.length && { _id: { $in: ids } }), + username: { $exists: true }, + // if the search term is empty, don't need to have the $or statement (because it would be an empty regex) + ...(searchTerm && orStmt.length && { $or: orStmt }), }, + ...extraQuery, ], - { allowDiskUse: true }, - ); + }; + + return this.findPaginated(query, options); } findPaginatedByActiveLocalUsersExcept(searchTerm, exceptions, options, forcedSearchFields, localDomain) { diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index 927420115e7b..aebda87c78cb 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -148,7 +148,6 @@ export interface ISubscriptionsModel extends IBaseModel { options?: FindOptions, ): FindCursor; findByRoomIdAndRoles(roomId: string, roles: string[], options?: FindOptions): FindCursor; - findByRoomIdAndHighestRole(roomId: string, role: string, options?: FindOptions): FindCursor; findByRoomIdAndUserIds(roomId: string, userIds: string[], options?: FindOptions): FindCursor; findByUserIdUpdatedAfter(userId: string, updatedAt: Date, options?: FindOptions): FindCursor; diff --git a/packages/model-typings/src/models/IUsersModel.ts b/packages/model-typings/src/models/IUsersModel.ts index 68f98e2b3416..03ee0aa93349 100644 --- a/packages/model-typings/src/models/IUsersModel.ts +++ b/packages/model-typings/src/models/IUsersModel.ts @@ -8,19 +8,8 @@ import type { IPersonalAccessToken, AtLeast, ILivechatAgentStatus, - IUserWithRoleInfo, } from '@rocket.chat/core-typings'; -import type { - Document, - UpdateResult, - FindCursor, - FindOptions, - Filter, - FilterOperators, - InsertOneResult, - DeleteResult, - AggregationCursor, -} from 'mongodb'; +import type { Document, UpdateResult, FindCursor, FindOptions, Filter, FilterOperators, InsertOneResult, DeleteResult } from 'mongodb'; import type { FindPaginated, IBaseModel } from './IBaseModel'; @@ -55,16 +44,21 @@ export interface IUsersModel extends IBaseModel { extraQuery?: any, params?: { startsWith?: boolean; endsWith?: boolean }, ): FindPaginated>; - findPaginatedActiveUsersByRoomIdWithHighestRole( + countActiveUsersExcept( + searchTerm: string, + exceptions: IUser['_id'][], + searchFields: string[], + extraQuery?: FilterOperators[], + params?: { startsWith?: boolean; endsWith?: boolean }, + ): Promise; + findPaginatedActiveUsersByIds( searchTerm: string, - rid: string, searchFields: string[], - ownersIds: IUser['_id'][], - moderatorsIds: IUser['_id'][], + ids: IUser['_id'][], options?: FindOptions, extraQuery?: FilterOperators[], params?: { startsWith?: boolean; endsWith?: boolean }, - ): Promise>; + ): FindPaginated>; findPaginatedByActiveLocalUsersExcept( searchTerm: any, From caa78a49723849ff4eb9eae5af22adf753cdade3 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Thu, 28 Sep 2023 14:27:30 -0300 Subject: [PATCH 39/42] Remove offset from end-to-end tests --- apps/meteor/tests/end-to-end/api/02-channels.js | 7 +------ apps/meteor/tests/end-to-end/api/03-groups.js | 7 +------ 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/02-channels.js b/apps/meteor/tests/end-to-end/api/02-channels.js index 67aeb20f2f38..4a2c35a212e2 100644 --- a/apps/meteor/tests/end-to-end/api/02-channels.js +++ b/apps/meteor/tests/end-to-end/api/02-channels.js @@ -1175,7 +1175,6 @@ describe('[Channels]', function () { expect(res.body).to.have.property('members').and.to.be.an('array'); expect(res.body).to.have.property('count'); expect(res.body).to.have.property('total'); - expect(res.body).to.have.property('offset'); }) .end(done); }); @@ -1194,19 +1193,17 @@ describe('[Channels]', function () { expect(res.body).to.have.property('members').and.to.be.an('array'); expect(res.body).to.have.property('count'); expect(res.body).to.have.property('total'); - expect(res.body).to.have.property('offset'); }) .end(done); }); - it('should return an array of members by channel even when requested with count and offset params', (done) => { + it('should return an array of members by channel even when requested with count param', (done) => { request .get(api('channels.membersByHighestRole')) .set(credentials) .query({ roomId: testChannel._id, count: 5, - offset: 0, }) .expect('Content-Type', 'application/json') .expect(200) @@ -1215,7 +1212,6 @@ describe('[Channels]', function () { expect(res.body).to.have.property('members').and.to.be.an('array'); expect(res.body).to.have.property('count', 3); expect(res.body).to.have.property('total'); - expect(res.body).to.have.property('offset'); }) .end(done); }); @@ -1235,7 +1231,6 @@ describe('[Channels]', function () { expect(res.body).to.have.property('members').and.to.be.an('array'); expect(res.body).to.have.property('count', 1); expect(res.body).to.have.property('total'); - expect(res.body).to.have.property('offset'); const member = res.body.members[0]; expect(member).to.have.property('_id'); diff --git a/apps/meteor/tests/end-to-end/api/03-groups.js b/apps/meteor/tests/end-to-end/api/03-groups.js index d535b86dc062..0f3d238ea04c 100644 --- a/apps/meteor/tests/end-to-end/api/03-groups.js +++ b/apps/meteor/tests/end-to-end/api/03-groups.js @@ -905,7 +905,6 @@ describe('[Groups]', function () { expect(res.body).to.have.property('members').and.to.be.an('array'); expect(res.body).to.have.property('count'); expect(res.body).to.have.property('total'); - expect(res.body).to.have.property('offset'); }) .end(done); }); @@ -924,19 +923,17 @@ describe('[Groups]', function () { expect(res.body).to.have.property('members').and.to.be.an('array'); expect(res.body).to.have.property('count'); expect(res.body).to.have.property('total'); - expect(res.body).to.have.property('offset'); }) .end(done); }); - it('should return an array of members by channel even when requested with count and offset params', (done) => { + it('should return an array of members by channel even when requested with count param', (done) => { request .get(api('groups.membersByHighestRole')) .set(credentials) .query({ roomId: testGroup._id, count: 5, - offset: 0, }) .expect('Content-Type', 'application/json') .expect(200) @@ -945,7 +942,6 @@ describe('[Groups]', function () { expect(res.body).to.have.property('members').and.to.be.an('array'); expect(res.body).to.have.property('count', 3); expect(res.body).to.have.property('total'); - expect(res.body).to.have.property('offset'); }) .end(done); }); @@ -965,7 +961,6 @@ describe('[Groups]', function () { expect(res.body).to.have.property('members').and.to.be.an('array'); expect(res.body).to.have.property('count', 1); expect(res.body).to.have.property('total'); - expect(res.body).to.have.property('offset'); const member = res.body.members[0]; expect(member).to.have.property('_id'); From ebbc00ccf9cfac36479112bc1adc741015eec3c2 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Thu, 28 Sep 2023 20:26:40 -0300 Subject: [PATCH 40/42] Replace owner/moderator finds by aggregation --- .../lib/findUsersOfRoomByHighestRole.ts | 73 +++---------- .../meteor/server/models/raw/Subscriptions.ts | 101 +++++++++++++++++- .../src/models/ISubscriptionsModel.ts | 11 +- 3 files changed, 124 insertions(+), 61 deletions(-) diff --git a/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts b/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts index 10eeb13c996f..93c18d33632b 100644 --- a/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts +++ b/apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts @@ -1,66 +1,18 @@ -import type { IUserWithRoleInfo, ISubscription, IUser } from '@rocket.chat/core-typings'; +import type { IUserWithRoleInfo, IUser, IRoom, UserStatus } from '@rocket.chat/core-typings'; import { Users, Subscriptions } from '@rocket.chat/models'; -import type { FilterOperators, FindOptions } from 'mongodb'; +import type { Filter, FilterOperators, FindOptions } from 'mongodb'; import { settings } from '../../app/settings/server'; type FindUsersParam = { rid: string; - status?: FilterOperators; + status?: FilterOperators; skip?: number; limit?: number; filter?: string; sort?: Record; }; -async function findUsersWithRolesOfRoom( - { rid, status, limit = 0, filter = '' }: FindUsersParam, - options: FindOptions, -): Promise<{ members: IUserWithRoleInfo[]; totalCount: number; allMembersIds: string[] }> { - // Sort roles in descending order so that owners are listed before moderators - // This could possibly cause issues with custom roles, but that's intended to improve performance - const highestRolesMembersSubscriptions = await Subscriptions.findByRoomIdAndRoles(rid, ['owner', 'moderator'], { - projection: { 'u._id': 1, 'roles': 1 }, - sort: { roles: -1 }, - }).toArray(); - - const searchFields = settings.get('Accounts_SearchFields').trim().split(','); - - const totalCount = highestRolesMembersSubscriptions.length; - const allMembersIds = highestRolesMembersSubscriptions.map((s: ISubscription) => s.u?._id); - const highestRolesMembersIdsToFind = allMembersIds.slice(0, limit); - - const { cursor } = Users.findPaginatedActiveUsersByIds(filter, searchFields, highestRolesMembersIdsToFind, options, [ - { - __rooms: rid, - ...(status && { status }), - }, - ]); - const highestRolesMembers = await cursor.toArray(); - - // maps for efficient lookup of highest roles and sort order - const ordering: Record = {}; - const highestRoleById: Record = {}; - - const limitedHighestRolesMembersSubs = highestRolesMembersSubscriptions.slice(0, limit); - limitedHighestRolesMembersSubs.forEach((sub, index) => { - ordering[sub.u._id] = index; - highestRoleById[sub.u._id] = sub.roles?.includes('owner') ? 'owner' : 'moderator'; - }); - - highestRolesMembers.sort((a, b) => ordering[a._id] - ordering[b._id]); - const membersWithHighestRoles = highestRolesMembers.map( - (member): IUserWithRoleInfo => ({ - ...member, - highestRole: { - role: highestRoleById[member._id], - level: highestRoleById[member._id] === 'owner' ? 0 : 1, - }, - }), - ); - return { members: membersWithHighestRoles, totalCount, allMembersIds }; -} - export async function findUsersOfRoomByHighestRole({ rid, status, @@ -85,22 +37,25 @@ export async function findUsersOfRoomByHighestRole({ }, limit, }; - const extraQuery = { + const extraQuery: Filter = { __rooms: rid, ...(status && { status }), }; const searchFields = settings.get('Accounts_SearchFields').trim().split(','); - // Find highest roles members (owners and moderator) + // Find highest roles members (owners and moderators) + const result = await Subscriptions.findPaginatedActiveHighestRoleUsers(filter, rid, searchFields, options, extraQuery); const { - members: highestRolesMembers, - totalCount: totalMembersWithRoles, - allMembersIds: highestRolesMembersIds, - } = await findUsersWithRolesOfRoom({ rid, status, limit, filter }, options); + members: highestRolesMembers = [], + totalCount: totalMembersWithRoles = { total: 0 }, + ids = { allMembersIds: [] }, + } = result[0] || {}; + const { total: totalMembersWithRolesCount } = totalMembersWithRoles; + const { allMembersIds: highestRolesMembersIds } = ids; if (limit <= highestRolesMembers.length) { const totalMembersCount = await Users.countActiveUsersExcept(filter, highestRolesMembersIds, searchFields, [extraQuery]); - return { members: highestRolesMembers, total: totalMembersWithRoles + totalMembersCount }; + return { members: highestRolesMembers, total: totalMembersWithRolesCount + totalMembersCount }; } if (options.limit) { options.limit -= highestRolesMembers.length; @@ -122,5 +77,5 @@ export async function findUsersOfRoomByHighestRole({ ); const allMembers = highestRolesMembers.concat(membersWithHighestRoles); - return { members: allMembers, total: totalMembersWithRoles + totalMembersCount }; + return { members: allMembers, total: totalMembersWithRolesCount + totalMembersCount }; } diff --git a/apps/meteor/server/models/raw/Subscriptions.ts b/apps/meteor/server/models/raw/Subscriptions.ts index 4b42367bad05..f5498fc729d0 100644 --- a/apps/meteor/server/models/raw/Subscriptions.ts +++ b/apps/meteor/server/models/raw/Subscriptions.ts @@ -1,4 +1,13 @@ -import type { IRole, IRoom, ISubscription, IUser, RocketChatRecordDeleted, RoomType, SpotlightUser } from '@rocket.chat/core-typings'; +import type { + IRole, + IRoom, + ISubscription, + IUser, + IUserWithRoleInfo, + RocketChatRecordDeleted, + RoomType, + SpotlightUser, +} from '@rocket.chat/core-typings'; import type { ISubscriptionsModel } from '@rocket.chat/model-typings'; import { Rooms, Users } from '@rocket.chat/models'; import { escapeRegExp } from '@rocket.chat/string-helpers'; @@ -450,6 +459,96 @@ export class SubscriptionsRaw extends BaseRaw implements ISubscri .toArray(); } + async findPaginatedActiveHighestRoleUsers( + searchTerm: string, + rid: IRoom['_id'], + searchFields: string[], + options: FindOptions = {}, + extraQuery?: Filter, + { startsWith = false, endsWith = false }: { startsWith?: string | false; endsWith?: string | false } = {}, + ): Promise<{ members: IUserWithRoleInfo[]; totalCount: { total: number }; ids: { allMembersIds: string[] } }[]> { + const termRegex = new RegExp((startsWith ? '^' : '') + escapeRegExp(searchTerm) + (endsWith ? '$' : ''), 'i'); + const orStatement = (searchFields || []).map((el) => ({ [el.trim()]: termRegex })) as { [x: string]: RegExp }[]; + + const limit = + options.limit !== 0 + ? [ + { + $limit: options.limit, + }, + ] + : []; + + return this.col + .aggregate<{ members: IUserWithRoleInfo[]; totalCount: { total: number }; ids: { allMembersIds: string[] } }>( + [ + { + $match: { + rid, + roles: { $in: ['owner', 'moderator'] }, + }, + }, + { + $lookup: { + from: 'users', + as: 'user', + let: { id: '$u._id' }, + pipeline: [ + { + $match: { + $expr: { $eq: ['$_id', '$$id'] }, + username: { $exists: true }, + active: true, + ...(searchTerm && orStatement.length > 0 && { $or: orStatement }), + ...extraQuery, + }, + }, + { $project: options.projection }, + ], + }, + }, + { + $unwind: { + path: '$user', + }, + }, + { + $addFields: { + 'user.highestRole': { + $cond: [{ $in: ['owner', '$roles'] }, { role: 'owner', level: 0 }, { role: 'moderator', level: 1 }], + }, + }, + }, + { + $replaceRoot: { newRoot: '$user' }, + }, + { + $facet: { + members: [ + { + $sort: { + 'highestRole.level': 1, + ...(options.sort as object), + }, + }, + ...limit, + ], + ids: [{ $group: { _id: null, allMembersIds: { $push: '$_id' } } }], + totalCount: [{ $count: 'total' }], + }, + }, + { + $unwind: { path: '$totalCount' }, + }, + { + $unwind: { path: '$ids' }, + }, + ], + { allowDiskUse: true }, + ) + .toArray(); + } + incUnreadForRoomIdExcludingUserIds(roomId: IRoom['_id'], userIds: IUser['_id'][], inc: number): Promise { if (inc == null) { inc = 1; diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index aebda87c78cb..6b5e8c7c8829 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -1,4 +1,4 @@ -import type { ISubscription, IRole, IUser, IRoom, RoomType, SpotlightUser } from '@rocket.chat/core-typings'; +import type { ISubscription, IRole, IUser, IRoom, RoomType, SpotlightUser, IUserWithRoleInfo } from '@rocket.chat/core-typings'; import type { FindOptions, FindCursor, UpdateResult, DeleteResult, Document, AggregateOptions, Filter, InsertOneResult } from 'mongodb'; import type { IBaseModel } from './IBaseModel'; @@ -76,6 +76,15 @@ export interface ISubscriptionsModel extends IBaseModel { options?: AggregateOptions, ): Promise; + findPaginatedActiveHighestRoleUsers( + searchTerm: string, + rid: IRoom['_id'], + searchFields: string[], + options?: FindOptions, + extraQuery?: Filter, + { startsWith = false, endsWith = false }?: { startsWith?: string | false; endsWith?: string | false }, + ): Promise<{ members: IUserWithRoleInfo[]; totalCount: { total: number }; ids: { allMembersIds: string[] } }[]>; + incUnreadForRoomIdExcludingUserIds(roomId: IRoom['_id'], userIds: IUser['_id'][], inc: number): Promise; setAlertForRoomIdExcludingUserId(roomId: IRoom['_id'], userId: IUser['_id']): Promise; From 89da189a3032c7bc7bbf9ed39d6b9d5005c7dd98 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Fri, 29 Sep 2023 11:11:00 -0300 Subject: [PATCH 41/42] Fix typecheck --- apps/meteor/app/api/server/v1/channels.ts | 4 ++-- apps/meteor/app/api/server/v1/groups.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/meteor/app/api/server/v1/channels.ts b/apps/meteor/app/api/server/v1/channels.ts index 0ab14b0e0a0d..0937ff22f469 100644 --- a/apps/meteor/app/api/server/v1/channels.ts +++ b/apps/meteor/app/api/server/v1/channels.ts @@ -1,5 +1,5 @@ import { Team } from '@rocket.chat/core-services'; -import type { IRoom, ISubscription, IUser, RoomType } from '@rocket.chat/core-typings'; +import type { IRoom, ISubscription, IUser, RoomType, UserStatus } from '@rocket.chat/core-typings'; import { Integrations, Messages, Rooms, Subscriptions, Uploads, Users } from '@rocket.chat/models'; import { isChannelsAddAllProps, @@ -1065,7 +1065,7 @@ API.v1.addRoute( const { members, total } = await findUsersOfRoomByHighestRole({ rid: findResult._id, - ...(status && { status: { $in: status } }), + ...(status && { status: { $in: status as UserStatus[] } }), limit, filter, ...(sort?.username && { sort: { username: sort.username } }), diff --git a/apps/meteor/app/api/server/v1/groups.ts b/apps/meteor/app/api/server/v1/groups.ts index 403ceb62bd9d..8300413fbc0a 100644 --- a/apps/meteor/app/api/server/v1/groups.ts +++ b/apps/meteor/app/api/server/v1/groups.ts @@ -1,5 +1,5 @@ import { Team } from '@rocket.chat/core-services'; -import type { IIntegration, IUser, IRoom, RoomType } from '@rocket.chat/core-typings'; +import type { IIntegration, IUser, IRoom, RoomType, UserStatus } from '@rocket.chat/core-typings'; import { Integrations, Messages, Rooms, Subscriptions, Uploads, Users } from '@rocket.chat/models'; import { check, Match } from 'meteor/check'; import { Meteor } from 'meteor/meteor'; @@ -759,7 +759,7 @@ API.v1.addRoute( const { members, total } = await findUsersOfRoomByHighestRole({ rid: findResult.rid, - ...(status && { status: { $in: status } }), + ...(status && { status: { $in: status as UserStatus[] } }), limit, filter, ...(sort?.username && { sort: { username: sort.username } }), From cd44cfc09ea5cb6f1ba4f46617a468aa384e7e78 Mon Sep 17 00:00:00 2001 From: matheusbsilva137 Date: Thu, 15 Feb 2024 17:41:14 -0300 Subject: [PATCH 42/42] Fix typecheck --- packages/model-typings/src/models/ISubscriptionsModel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/model-typings/src/models/ISubscriptionsModel.ts b/packages/model-typings/src/models/ISubscriptionsModel.ts index 527bdd1efbe8..078cd6348d42 100644 --- a/packages/model-typings/src/models/ISubscriptionsModel.ts +++ b/packages/model-typings/src/models/ISubscriptionsModel.ts @@ -92,7 +92,7 @@ export interface ISubscriptionsModel extends IBaseModel { searchFields: string[], options?: FindOptions, extraQuery?: Filter, - { startsWith = false, endsWith = false }?: { startsWith?: string | false; endsWith?: string | false }, + { startsWith, endsWith }?: { startsWith?: string | false; endsWith?: string | false }, ): Promise<{ members: IUserWithRoleInfo[]; totalCount: { total: number }; ids: { allMembersIds: string[] } }[]>; incUnreadForRoomIdExcludingUserIds(roomId: IRoom['_id'], userIds: IUser['_id'][], inc: number): Promise;