Skip to content

Commit aa02c7a

Browse files
committed
feat: added endpoints groups.membersOrderedByRole channels.membersOrderedByRole
Signed-off-by: Abhinav Kumar <abhinav@avitechlab.com>
1 parent 2c2a35a commit aa02c7a

File tree

10 files changed

+420
-3
lines changed

10 files changed

+420
-3
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 `groups.membersOrderedByRole` and `channels.membersOrderedByRole` endpoints to retrieve members of groups and channels sorted according to their respective role in the room.

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

+41
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ import {
2121
isChannelsListProps,
2222
isChannelsFilesListProps,
2323
isChannelsOnlineProps,
24+
isChannelsMembersOrderedByRoleProps,
2425
} from '@rocket.chat/rest-typings';
2526
import { Meteor } from 'meteor/meteor';
2627

2728
import { isTruthy } from '../../../../lib/isTruthy';
2829
import { eraseRoom } from '../../../../server/lib/eraseRoom';
2930
import { findUsersOfRoom } from '../../../../server/lib/findUsersOfRoom';
31+
import { findUsersOfRoomOrderedByRole } from '../../../../server/lib/findUsersOfRoomOrderedByRole';
3032
import { hideRoomMethod } from '../../../../server/methods/hideRoom';
3133
import { removeUserFromRoomMethod } from '../../../../server/methods/removeUserFromRoom';
3234
import { canAccessRoomAsync } from '../../../authorization/server';
@@ -1092,6 +1094,45 @@ API.v1.addRoute(
10921094
},
10931095
);
10941096

1097+
API.v1.addRoute(
1098+
'channels.membersOrderedByRole',
1099+
{ authRequired: true, validateParams: isChannelsMembersOrderedByRoleProps },
1100+
{
1101+
async get() {
1102+
const findResult = await findChannelByIdOrName({
1103+
params: this.queryParams,
1104+
checkedArchived: false,
1105+
});
1106+
1107+
if (findResult.broadcast && !(await hasPermissionAsync(this.userId, 'view-broadcast-member-list', findResult._id))) {
1108+
return API.v1.unauthorized();
1109+
}
1110+
1111+
const { offset: skip, count: limit } = await getPaginationItems(this.queryParams);
1112+
const { sort = {} } = await this.parseJsonQuery();
1113+
1114+
const { status, filter, rolesOrder = ['leader', 'owner'] } = this.queryParams;
1115+
1116+
const { members, total } = await findUsersOfRoomOrderedByRole({
1117+
rid: findResult._id,
1118+
...(status && { status: { $in: status } }),
1119+
skip,
1120+
limit,
1121+
filter,
1122+
...(sort?.username && { sort: { username: sort.username } }),
1123+
rolesInOrder: rolesOrder,
1124+
});
1125+
1126+
return API.v1.success({
1127+
members,
1128+
count: members.length,
1129+
offset: skip,
1130+
total,
1131+
});
1132+
},
1133+
},
1134+
);
1135+
10951136
API.v1.addRoute(
10961137
'channels.online',
10971138
{ authRequired: true, validateParams: isChannelsOnlineProps },

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

+41-1
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { Team, isMeteorError } from '@rocket.chat/core-services';
22
import type { IIntegration, IUser, IRoom, RoomType } from '@rocket.chat/core-typings';
33
import { Integrations, Messages, Rooms, Subscriptions, Uploads, Users } from '@rocket.chat/models';
4-
import { isGroupsOnlineProps, isGroupsMessagesProps } from '@rocket.chat/rest-typings';
4+
import { isGroupsOnlineProps, isGroupsMessagesProps, isGroupsMembersOrderedByRoleProps } from '@rocket.chat/rest-typings';
55
import { check, Match } from 'meteor/check';
66
import { Meteor } from 'meteor/meteor';
77
import type { Filter } from 'mongodb';
88

99
import { eraseRoom } from '../../../../server/lib/eraseRoom';
1010
import { findUsersOfRoom } from '../../../../server/lib/findUsersOfRoom';
11+
import { findUsersOfRoomOrderedByRole } from '../../../../server/lib/findUsersOfRoomOrderedByRole';
1112
import { hideRoomMethod } from '../../../../server/methods/hideRoom';
1213
import { removeUserFromRoomMethod } from '../../../../server/methods/removeUserFromRoom';
1314
import { canAccessRoomAsync, roomAccessAttributes } from '../../../authorization/server';
@@ -744,6 +745,45 @@ API.v1.addRoute(
744745
},
745746
);
746747

748+
API.v1.addRoute(
749+
'groups.membersOrderedByRole',
750+
{ authRequired: true, validateParams: isGroupsMembersOrderedByRoleProps },
751+
{
752+
async get() {
753+
const findResult = await findPrivateGroupByIdOrName({
754+
params: this.queryParams,
755+
userId: this.userId,
756+
});
757+
758+
if (findResult.broadcast && !(await hasPermissionAsync(this.userId, 'view-broadcast-member-list', findResult.rid))) {
759+
return API.v1.unauthorized();
760+
}
761+
762+
const { offset: skip, count: limit } = await getPaginationItems(this.queryParams);
763+
const { sort = {} } = await this.parseJsonQuery();
764+
765+
const { status, filter, rolesOrder } = this.queryParams;
766+
767+
const { members, total } = await findUsersOfRoomOrderedByRole({
768+
rid: findResult.rid,
769+
...(status && { status: { $in: status } }),
770+
skip,
771+
limit,
772+
filter,
773+
...(sort?.username && { sort: { username: sort.username } }),
774+
rolesInOrder: rolesOrder,
775+
});
776+
777+
return API.v1.success({
778+
members,
779+
count: members.length,
780+
offset: skip,
781+
total,
782+
});
783+
},
784+
},
785+
);
786+
747787
API.v1.addRoute(
748788
'groups.messages',
749789
{ authRequired: true, validateParams: isGroupsMessagesProps },
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { IRole, IRoom } from '@rocket.chat/core-typings';
2+
3+
import type { PaginatedRequest } from '../../helpers/PaginatedRequest';
4+
import { ajv } from '../Ajv';
5+
6+
type MembersOrderedByRoleProps = {
7+
roomId?: IRoom['_id'];
8+
roomName?: IRoom['name'];
9+
status?: string[];
10+
filter?: string;
11+
rolesOrder?: IRole['_id'][];
12+
};
13+
14+
export type ChannelsMembersOrderedByRoleProps = PaginatedRequest<MembersOrderedByRoleProps>;
15+
16+
const membersOrderedByRoleRolePropsSchema = {
17+
properties: {
18+
roomId: {
19+
type: 'string',
20+
},
21+
roomName: {
22+
type: 'string',
23+
},
24+
rolesOrder: {
25+
type: 'array',
26+
items: {
27+
type: 'string',
28+
},
29+
nullable: true,
30+
},
31+
status: {
32+
type: 'array',
33+
items: {
34+
type: 'string',
35+
},
36+
nullable: true,
37+
},
38+
filter: {
39+
type: 'string',
40+
nullable: true,
41+
},
42+
count: {
43+
type: 'integer',
44+
nullable: true,
45+
},
46+
offset: {
47+
type: 'integer',
48+
nullable: true,
49+
},
50+
sort: {
51+
type: 'string',
52+
nullable: true,
53+
},
54+
},
55+
oneOf: [{ required: ['roomId'] }, { required: ['roomName'] }],
56+
additionalProperties: false,
57+
};
58+
59+
export const isChannelsMembersOrderedByRoleProps = ajv.compile<ChannelsMembersOrderedByRoleProps>(membersOrderedByRoleRolePropsSchema);

0 commit comments

Comments
 (0)