Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add membersByHighestRole endpoints #29870

Open
wants to merge 68 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
c0c0227
Add groups.membersByRole endpoint
matheusbsilva137 Jul 19, 2023
82ea93c
Update endpoint name and fix logic
matheusbsilva137 Jul 19, 2023
1c25855
Merge branch 'develop' of https://github.com/RocketChat/Rocket.Chat i…
matheusbsilva137 Jul 19, 2023
6c5db96
Add channels endpoint
matheusbsilva137 Jul 20, 2023
577c89a
Update endpoint response
matheusbsilva137 Jul 21, 2023
7690fa9
Merge branch 'develop' into feat/members-by-role
matheusbsilva137 Jul 21, 2023
66f3ce5
Remove let on aggregation
matheusbsilva137 Jul 21, 2023
b8f313a
Improve typing
matheusbsilva137 Jul 21, 2023
c838f9f
Improve formatting
matheusbsilva137 Jul 21, 2023
d6f0dcc
Evaluate length as boolean
matheusbsilva137 Jul 21, 2023
c224675
Create new variable to regex string
matheusbsilva137 Jul 21, 2023
6e87c8c
Make options field optional
matheusbsilva137 Jul 21, 2023
120ce5d
Return highestRole object instead of fields
matheusbsilva137 Jul 21, 2023
c6252da
Update reduce to map
matheusbsilva137 Jul 21, 2023
4f7b113
Merge branch 'develop' into feat/members-by-role
matheusbsilva137 Jul 23, 2023
d17470e
Fix params order
matheusbsilva137 Jul 25, 2023
8aa7f00
Merge branch 'develop' of https://github.com/RocketChat/Rocket.Chat i…
matheusbsilva137 Jul 25, 2023
6c90dbb
Merge branch 'feat/members-by-role' of https://github.com/RocketChat/…
matheusbsilva137 Jul 25, 2023
58f0a59
Merge branch 'develop' of https://github.com/RocketChat/Rocket.Chat i…
matheusbsilva137 Jul 28, 2023
569b80a
Fix lint
matheusbsilva137 Jul 31, 2023
46c8a18
Add allowDiskUse aggregation option
matheusbsilva137 Jul 31, 2023
96168d3
Use ajv on channels.membersByHighestRole endpoint typing
matheusbsilva137 Jul 31, 2023
2dda7cd
Merge branch 'develop' of https://github.com/RocketChat/Rocket.Chat i…
matheusbsilva137 Aug 2, 2023
0353898
Add end-to-end tests
matheusbsilva137 Aug 2, 2023
5a668fc
Make lookup query compatible with mongo 4.4
matheusbsilva137 Aug 3, 2023
1a736d5
Check if result is undefined before extracting props
matheusbsilva137 Aug 3, 2023
6ee11bd
Merge branch 'develop' of https://github.com/RocketChat/Rocket.Chat i…
matheusbsilva137 Aug 3, 2023
2f5ccc4
Improve tests
matheusbsilva137 Aug 10, 2023
a0e011b
Improve typing
matheusbsilva137 Aug 10, 2023
c6ffd7b
Change roles object key
matheusbsilva137 Aug 10, 2023
dba1c87
Add IUserWithRoles to core-typings
matheusbsilva137 Aug 10, 2023
61f97dc
Fix tests
matheusbsilva137 Aug 14, 2023
50bef1c
Merge branch 'develop' into feat/members-by-role
matheusbsilva137 Aug 14, 2023
1784481
Merge branch 'develop' into feat/members-by-role
matheusbsilva137 Aug 16, 2023
5b5a46e
Fix tests
matheusbsilva137 Aug 16, 2023
ae7355d
Fix endpoint fails for members with no roles
matheusbsilva137 Aug 16, 2023
cb83fbf
Fix tests
matheusbsilva137 Aug 17, 2023
d4ed1d3
Remove statusConnection check on tests
matheusbsilva137 Aug 17, 2023
a11dc35
Update roles to roomRoles in tests
matheusbsilva137 Aug 18, 2023
9164db8
oops
matheusbsilva137 Aug 18, 2023
5e3f29c
Improve translations
matheusbsilva137 Aug 18, 2023
a85e403
Revert "Improve translations"
matheusbsilva137 Aug 18, 2023
78f908f
Create thick-swans-drop.md
matheusbsilva137 Aug 18, 2023
865348d
Update changesets
matheusbsilva137 Aug 21, 2023
c5951dc
Merge branch 'develop' into feat/members-by-role
matheusbsilva137 Aug 24, 2023
ef014af
Merge branch 'develop' into feat/members-by-role
scuciatto Aug 24, 2023
8689619
Improve aggregation performance
matheusbsilva137 Aug 31, 2023
e6242ea
Fix tests
matheusbsilva137 Aug 31, 2023
c464f41
Merge branch 'develop' into feat/members-by-role
matheusbsilva137 Aug 31, 2023
5369d2f
Merge branch 'develop' into feat/members-by-role
matheusbsilva137 Aug 31, 2023
cd10d92
Merge branch 'develop' of https://github.com/RocketChat/Rocket.Chat i…
matheusbsilva137 Aug 31, 2023
6323a7c
Merge branch 'develop' into feat/members-by-role
matheusbsilva137 Aug 31, 2023
48874bc
Merge branch 'feat/members-by-role' of https://github.com/RocketChat/…
matheusbsilva137 Sep 5, 2023
806a2a4
Merge branch 'develop' into feat/members-by-role
matheusbsilva137 Sep 22, 2023
efefd11
Merge branch 'feat/members-by-role' of https://github.com/RocketChat/…
matheusbsilva137 Sep 22, 2023
432ac79
Remove skip param
matheusbsilva137 Sep 26, 2023
9627c99
Replace aggregation by 3 finds
matheusbsilva137 Sep 28, 2023
caa78a4
Remove offset from end-to-end tests
matheusbsilva137 Sep 28, 2023
ebbc00c
Replace owner/moderator finds by aggregation
matheusbsilva137 Sep 28, 2023
89da189
Fix typecheck
matheusbsilva137 Sep 29, 2023
8c5d745
Merge branch 'develop' of https://github.com/RocketChat/Rocket.Chat i…
matheusbsilva137 Sep 29, 2023
1b7112b
Merge branch 'develop' of https://github.com/RocketChat/Rocket.Chat i…
matheusbsilva137 Oct 2, 2023
62d6c0e
Merge branch 'develop' of https://github.com/RocketChat/Rocket.Chat i…
matheusbsilva137 Oct 6, 2023
c9052a5
Merge branch 'develop' into feat/members-by-role
matheusbsilva137 Oct 6, 2023
806040c
Merge branch 'develop' into feat/members-by-role
matheusbsilva137 Oct 9, 2023
c7caccc
Merge branch 'develop' into feat/members-by-role
matheusbsilva137 Dec 5, 2023
9db6554
Merge branch 'develop' of https://github.com/RocketChat/Rocket.Chat i…
matheusbsilva137 Feb 15, 2024
cd44cfc
Fix typecheck
matheusbsilva137 Feb 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/thick-swans-drop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@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
47 changes: 47 additions & 0 deletions apps/meteor/app/api/server/v1/channels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { Meteor } from 'meteor/meteor';

import { isTruthy } from '../../../../lib/isTruthy';
import { findUsersOfRoom } from '../../../../server/lib/findUsersOfRoom';
import { findUsersOfRoomByHighestRole } from '../../../../server/lib/findUsersOfRoomByHighestRole';
import { hideRoomMethod } from '../../../../server/methods/hideRoom';
import { removeUserFromRoomMethod } from '../../../../server/methods/removeUserFromRoom';
import { canAccessRoomAsync } from '../../../authorization/server';
Expand Down Expand Up @@ -1044,6 +1045,52 @@ 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();
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 } }),
});

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,
offset: skip,
total,
});
},
},
);

API.v1.addRoute(
'channels.online',
{ authRequired: true },
Expand Down
47 changes: 47 additions & 0 deletions apps/meteor/app/api/server/v1/groups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Meteor } from 'meteor/meteor';
import type { Filter } from 'mongodb';

import { findUsersOfRoom } from '../../../../server/lib/findUsersOfRoom';
import { findUsersOfRoomByHighestRole } from '../../../../server/lib/findUsersOfRoomByHighestRole';
import { hideRoomMethod } from '../../../../server/methods/hideRoom';
import { removeUserFromRoomMethod } from '../../../../server/methods/removeUserFromRoom';
import { canAccessRoomAsync, roomAccessAttributes } from '../../../authorization/server';
Expand Down Expand Up @@ -738,6 +739,52 @@ API.v1.addRoute(
},
);

API.v1.addRoute(
'groups.membersByHighestRole',
{ 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();
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 } }),
});

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];
matheusbsilva137 marked this conversation as resolved.
Show resolved Hide resolved

return API.v1.success({
members,
count: members.length,
MarcosSpessatto marked this conversation as resolved.
Show resolved Hide resolved
offset: skip,
total,
});
},
},
);

API.v1.addRoute(
'groups.messages',
{ authRequired: true },
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/app/api/server/v1/im.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ API.v1.addRoute(

const searchFields = settings.get<string>('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]);

Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/server/lib/findUsersOfRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function findUsersOfRoom({ rid, status, skip = 0, limit = 0, filter = '',

const searchFields = settings.get<string>('Accounts_SearchFields').trim().split(',');

return Users.findPaginatedByActiveUsersExcept(filter, undefined, options, searchFields, [
return Users.findPaginatedByActiveUsersExcept(filter, undefined, searchFields, options, [
{
__rooms: rid,
...(status && { status }),
Expand Down
50 changes: 50 additions & 0 deletions apps/meteor/server/lib/findUsersOfRoomByHighestRole.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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 FindUsersParam = {
rid: string;
status?: FilterOperators<string>;
skip?: number;
limit?: number;
filter?: string;
sort?: Record<string, any>;
};

export async function findUsersOfRoomByHighestRole({
rid,
status,
skip = 0,
limit = 0,
filter = '',
sort,
}: FindUsersParam): Promise<AggregationCursor<{ members: IUserWithRoleInfo[]; totalCount: { total: number }[] }>> {
const options = {
projection: {
name: 1,
username: 1,
nickname: 1,
status: 1,
avatarETag: 1,
_updatedAt: 1,
federated: 1,
statusConnection: 1,
},
sort: {
statusConnection: -1 as const,
...(sort || { ...(settings.get('UI_Use_Real_Name') && { name: 1 }), username: 1 }),
},
skip,
limit,
};

const searchFields = settings.get<string>('Accounts_SearchFields').trim().split(',');

return Users.findPaginatedActiveUsersByRoomIdWithHighestRole(filter, rid, searchFields, options, [
{
...(status && { status }),
},
]);
}
2 changes: 1 addition & 1 deletion apps/meteor/server/methods/browseChannels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ const findUsers = async ({
};

if (workspace === 'all') {
const { cursor, totalCount } = Users.findPaginatedByActiveUsersExcept<FederatedUser>(text, [], options, searchFields);
const { cursor, totalCount } = Users.findPaginatedByActiveUsersExcept<FederatedUser>(text, [], searchFields, options);
const [results, total] = await Promise.all([cursor.toArray(), totalCount]);
return {
total,
Expand Down
129 changes: 112 additions & 17 deletions apps/meteor/server/models/raw/Users.js
Original file line number Diff line number Diff line change
Expand Up @@ -264,10 +264,7 @@ export class UsersRaw extends BaseRaw {

const termRegex = new RegExp((startsWith ? '^' : '') + escapeRegExp(searchTerm) + (endsWith ? '$' : ''), 'i');

const orStmt = (searchFields || []).reduce((acc, el) => {
acc.push({ [el.trim()]: termRegex });
return acc;
}, []);
const orStmt = (searchFields || []).map((el) => ({ [el.trim()]: termRegex }));

const query = {
$and: [
Expand All @@ -290,38 +287,33 @@ 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];
}

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((acc, el) => {
acc.push({ [el.trim()]: termRegex });
return acc;
}, []);
const orStmt = (searchFields || []).map((el) => ({ [el.trim()]: termRegex }));

const query = {
$and: [
{
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,
],
Expand All @@ -330,18 +322,121 @@ export class UsersRaw extends BaseRaw {
return this.findPaginated(query, options);
}

findPaginatedActiveUsersByRoomIdWithHighestRole(
searchTerm,
rid,
searchFields,
options = {},
extraQuery = [],
{ startsWith = false, endsWith = false } = {},
) {
const regexString = (startsWith ? '^' : '') + escapeRegExp(searchTerm) + (endsWith ? '$' : '');
matheusbsilva137 marked this conversation as resolved.
Show resolved Hide resolved
const termRegex = new RegExp(regexString, 'i');

const orStmt = (searchFields || []).map((el) => ({ [el.trim()]: termRegex }));

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 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',
let: { id: '$_id' },
as: 'sub',
pipeline: [
{
$match: {
$expr: {
$and: [{ $eq: ['$u._id', '$$id'] }, { $eq: ['$rid', rid] }],
},
},
},
],
},
},
{ $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 }] },
],
},
},
},
{
$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) {
const extraQuery = [
{
$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) {
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 = {}) {
Expand Down
Loading