Skip to content

Commit ae9d2e1

Browse files
committed
feat: added endpoint rooms.membersOrderedByRole
Signed-off-by: Abhinav Kumar <abhinav@avitechlab.com>
1 parent 75643e1 commit ae9d2e1

File tree

5 files changed

+473
-1
lines changed

5 files changed

+473
-1
lines changed

.changeset/silly-kings-approve.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@rocket.chat/meteor': patch
3+
'@rocket.chat/rest-typings': patch
4+
---
5+
6+
Adds `rooms.membersOrderedByRole` endpoint to retrieve members of groups and channels sorted according to their respective role in the room.

apps/meteor/app/api/server/v1/rooms.ts

+41
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import {
99
isRoomsExportProps,
1010
isRoomsIsMemberProps,
1111
isRoomsCleanHistoryProps,
12+
isRoomsMembersOrderedByRoleProps,
1213
} from '@rocket.chat/rest-typings';
1314
import { Meteor } from 'meteor/meteor';
1415

1516
import { isTruthy } from '../../../../lib/isTruthy';
1617
import { omit } from '../../../../lib/utils/omit';
1718
import * as dataExport from '../../../../server/lib/dataExport';
1819
import { eraseRoom } from '../../../../server/lib/eraseRoom';
20+
import { findUsersOfRoomOrderedByRole } from '../../../../server/lib/findUsersOfRoomOrderedByRole';
1921
import { muteUserInRoom } from '../../../../server/methods/muteUserInRoom';
2022
import { unmuteUserInRoom } from '../../../../server/methods/unmuteUserInRoom';
2123
import { canAccessRoomAsync, canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom';
@@ -858,6 +860,45 @@ API.v1.addRoute(
858860
},
859861
);
860862

863+
API.v1.addRoute(
864+
'rooms.membersOrderedByRole',
865+
{ authRequired: true, validateParams: isRoomsMembersOrderedByRoleProps },
866+
{
867+
async get() {
868+
const findResult = await findRoomByIdOrName({
869+
params: this.queryParams,
870+
checkedArchived: false,
871+
});
872+
873+
if (findResult.broadcast && !(await hasPermissionAsync(this.userId, 'view-broadcast-member-list', findResult._id))) {
874+
return API.v1.unauthorized();
875+
}
876+
877+
const { offset: skip, count: limit } = await getPaginationItems(this.queryParams);
878+
const { sort = {} } = await this.parseJsonQuery();
879+
880+
const { status, filter, rolesOrder = ['owner', 'moderator'] } = this.queryParams;
881+
882+
const { members, total } = await findUsersOfRoomOrderedByRole({
883+
rid: findResult._id,
884+
...(status && { status: { $in: status } }),
885+
skip,
886+
limit,
887+
filter,
888+
...(sort?.username && { sort: { username: sort.username } }),
889+
rolesInOrder: rolesOrder,
890+
});
891+
892+
return API.v1.success({
893+
members,
894+
count: members.length,
895+
offset: skip,
896+
total,
897+
});
898+
},
899+
},
900+
);
901+
861902
API.v1.addRoute(
862903
'rooms.muteUser',
863904
{ authRequired: true, validateParams: isRoomsMuteUnmuteUserProps },
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
import type { IUser, IRole } from '@rocket.chat/core-typings';
2+
import { Subscriptions } from '@rocket.chat/models';
3+
import { escapeRegExp } from '@rocket.chat/string-helpers';
4+
import type { Document, FilterOperators } from 'mongodb';
5+
6+
import { settings } from '../../app/settings/server';
7+
8+
type FindUsersParam = {
9+
rid: string;
10+
status?: FilterOperators<string>;
11+
skip?: number;
12+
limit?: number;
13+
filter?: string;
14+
sort?: Record<string, any>;
15+
rolesInOrder?: IRole['_id'][];
16+
exceptions?: string[];
17+
extraQuery?: Document[];
18+
};
19+
20+
type UserWithRoleData = IUser & {
21+
roles: IRole['_id'][];
22+
};
23+
24+
export async function findUsersOfRoomOrderedByRole({
25+
rid,
26+
status,
27+
skip = 0,
28+
limit = 0,
29+
filter = '',
30+
sort,
31+
rolesInOrder = [],
32+
exceptions = [],
33+
extraQuery = [],
34+
}: FindUsersParam): Promise<{ members: UserWithRoleData[]; total: number }> {
35+
const searchFields = settings.get<string>('Accounts_SearchFields').trim().split(',');
36+
const termRegex = new RegExp(escapeRegExp(filter), 'i');
37+
const orStmt = filter && searchFields.length ? searchFields.map((field) => ({ [field.trim()]: termRegex })) : [];
38+
39+
const useRealName = settings.get('UI_Use_Real_Name');
40+
const defaultSort = useRealName ? { name: 1 } : { username: 1 };
41+
42+
const sortCriteria = {
43+
rolePriority: 1,
44+
statusConnection: -1,
45+
...(sort || defaultSort),
46+
};
47+
48+
const userLookupPipeline: Document[] = [{ $match: { $expr: { $eq: ['$_id', '$$userId'] } } }];
49+
50+
if (status) {
51+
userLookupPipeline.push({ $match: { status } });
52+
}
53+
54+
userLookupPipeline.push({
55+
$match: {
56+
$and: [
57+
{
58+
active: true,
59+
username: {
60+
$exists: true,
61+
...(exceptions.length > 0 && { $nin: exceptions }),
62+
},
63+
...(filter && orStmt.length > 0 && { $or: orStmt }),
64+
},
65+
...extraQuery,
66+
],
67+
},
68+
});
69+
70+
userLookupPipeline.push({
71+
$project: {
72+
_id: 1,
73+
username: 1,
74+
name: 1,
75+
nickname: 1,
76+
status: 1,
77+
avatarETag: 1,
78+
_updatedAt: 1,
79+
federated: 1,
80+
statusConnection: 1,
81+
},
82+
});
83+
84+
const defaultPriority = rolesInOrder.length + 1;
85+
86+
const branches = rolesInOrder.map((role, index) => ({
87+
case: { $eq: ['$$this', role] },
88+
then: index + 1,
89+
}));
90+
91+
const filteredPipeline: Document[] = [
92+
{
93+
$lookup: {
94+
from: 'users',
95+
let: { userId: '$u._id' },
96+
pipeline: userLookupPipeline,
97+
as: 'userDetails',
98+
},
99+
},
100+
{ $unwind: '$userDetails' },
101+
{
102+
$addFields: {
103+
primaryRole: {
104+
$reduce: {
105+
input: '$roles',
106+
initialValue: { role: null, priority: defaultPriority },
107+
in: {
108+
$let: {
109+
vars: {
110+
currentPriority: {
111+
$switch: {
112+
branches,
113+
default: defaultPriority,
114+
},
115+
},
116+
},
117+
in: {
118+
$cond: [
119+
{
120+
$and: [{ $in: ['$$this', rolesInOrder] }, { $lt: ['$$currentPriority', '$$value.priority'] }],
121+
},
122+
{ role: '$$this', priority: '$$currentPriority' },
123+
'$$value',
124+
],
125+
},
126+
},
127+
},
128+
},
129+
},
130+
},
131+
},
132+
{
133+
$addFields: {
134+
rolePriority: { $ifNull: ['$primaryRole.priority', defaultPriority] },
135+
},
136+
},
137+
{
138+
$project: {
139+
_id: '$userDetails._id',
140+
rid: 1,
141+
roles: 1,
142+
primaryRole: '$primaryRole.role',
143+
rolePriority: 1,
144+
username: '$userDetails.username',
145+
name: '$userDetails.name',
146+
nickname: '$userDetails.nickname',
147+
status: '$userDetails.status',
148+
avatarETag: '$userDetails.avatarETag',
149+
_updatedAt: '$userDetails._updatedAt',
150+
federated: '$userDetails.federated',
151+
statusConnection: '$userDetails.statusConnection',
152+
},
153+
},
154+
];
155+
156+
const facetPipeline: Document[] = [
157+
{ $match: { rid } },
158+
{
159+
$facet: {
160+
totalCount: [{ $match: { rid } }, ...filteredPipeline, { $count: 'total' }],
161+
members: [
162+
{ $match: { rid } },
163+
...filteredPipeline,
164+
{ $sort: sortCriteria },
165+
...(skip > 0 ? [{ $skip: skip }] : []),
166+
...(limit > 0 ? [{ $limit: limit }] : []),
167+
],
168+
},
169+
},
170+
{
171+
$project: {
172+
members: 1,
173+
totalCount: { $arrayElemAt: ['$totalCount.total', 0] },
174+
},
175+
},
176+
];
177+
178+
const [result] = await Subscriptions.col.aggregate(facetPipeline, { allowDiskUse: true }).toArray();
179+
180+
return {
181+
members: result.members.map((member: any) => {
182+
delete member.primaryRole;
183+
delete member.rolePriority;
184+
return member;
185+
}),
186+
total: result.totalCount,
187+
};
188+
}

0 commit comments

Comments
 (0)