Skip to content

Commit

Permalink
feat: added endpoint rooms.membersOrderedByRole
Browse files Browse the repository at this point in the history
Signed-off-by: Abhinav Kumar <[email protected]>
  • Loading branch information
abhinavkrin committed Dec 17, 2024
1 parent 75643e1 commit ae9d2e1
Show file tree
Hide file tree
Showing 5 changed files with 473 additions and 1 deletion.
6 changes: 6 additions & 0 deletions .changeset/silly-kings-approve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@rocket.chat/meteor': patch
'@rocket.chat/rest-typings': patch
---

Adds `rooms.membersOrderedByRole` endpoint to retrieve members of groups and channels sorted according to their respective role in the room.
41 changes: 41 additions & 0 deletions apps/meteor/app/api/server/v1/rooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ import {
isRoomsExportProps,
isRoomsIsMemberProps,
isRoomsCleanHistoryProps,
isRoomsMembersOrderedByRoleProps,
} from '@rocket.chat/rest-typings';
import { Meteor } from 'meteor/meteor';

import { isTruthy } from '../../../../lib/isTruthy';
import { omit } from '../../../../lib/utils/omit';
import * as dataExport from '../../../../server/lib/dataExport';
import { eraseRoom } from '../../../../server/lib/eraseRoom';
import { findUsersOfRoomOrderedByRole } from '../../../../server/lib/findUsersOfRoomOrderedByRole';
import { muteUserInRoom } from '../../../../server/methods/muteUserInRoom';
import { unmuteUserInRoom } from '../../../../server/methods/unmuteUserInRoom';
import { canAccessRoomAsync, canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom';
Expand Down Expand Up @@ -858,6 +860,45 @@ API.v1.addRoute(
},
);

API.v1.addRoute(
'rooms.membersOrderedByRole',
{ authRequired: true, validateParams: isRoomsMembersOrderedByRoleProps },
{
async get() {
const findResult = await findRoomByIdOrName({
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, rolesOrder = ['owner', 'moderator'] } = this.queryParams;

const { members, total } = await findUsersOfRoomOrderedByRole({
rid: findResult._id,
...(status && { status: { $in: status } }),
skip,
limit,
filter,
...(sort?.username && { sort: { username: sort.username } }),
rolesInOrder: rolesOrder,
});

return API.v1.success({
members,
count: members.length,
offset: skip,
total,
});
},
},
);

API.v1.addRoute(
'rooms.muteUser',
{ authRequired: true, validateParams: isRoomsMuteUnmuteUserProps },
Expand Down
188 changes: 188 additions & 0 deletions apps/meteor/server/lib/findUsersOfRoomOrderedByRole.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
import type { IUser, IRole } from '@rocket.chat/core-typings';
import { Subscriptions } from '@rocket.chat/models';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import type { Document, 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>;
rolesInOrder?: IRole['_id'][];
exceptions?: string[];
extraQuery?: Document[];
};

type UserWithRoleData = IUser & {
roles: IRole['_id'][];
};

export async function findUsersOfRoomOrderedByRole({
rid,
status,
skip = 0,
limit = 0,
filter = '',
sort,
rolesInOrder = [],
exceptions = [],
extraQuery = [],
}: FindUsersParam): Promise<{ members: UserWithRoleData[]; total: number }> {
const searchFields = settings.get<string>('Accounts_SearchFields').trim().split(',');
const termRegex = new RegExp(escapeRegExp(filter), 'i');
const orStmt = filter && searchFields.length ? searchFields.map((field) => ({ [field.trim()]: termRegex })) : [];

const useRealName = settings.get('UI_Use_Real_Name');
const defaultSort = useRealName ? { name: 1 } : { username: 1 };

const sortCriteria = {
rolePriority: 1,
statusConnection: -1,
...(sort || defaultSort),
};

const userLookupPipeline: Document[] = [{ $match: { $expr: { $eq: ['$_id', '$$userId'] } } }];

if (status) {
userLookupPipeline.push({ $match: { status } });
}

userLookupPipeline.push({
$match: {
$and: [
{
active: true,
username: {
$exists: true,
...(exceptions.length > 0 && { $nin: exceptions }),
},
...(filter && orStmt.length > 0 && { $or: orStmt }),
},
...extraQuery,
],
},
});

userLookupPipeline.push({
$project: {
_id: 1,
username: 1,
name: 1,
nickname: 1,
status: 1,
avatarETag: 1,
_updatedAt: 1,
federated: 1,
statusConnection: 1,
},
});

const defaultPriority = rolesInOrder.length + 1;

const branches = rolesInOrder.map((role, index) => ({
case: { $eq: ['$$this', role] },
then: index + 1,
}));

const filteredPipeline: Document[] = [
{
$lookup: {
from: 'users',
let: { userId: '$u._id' },
pipeline: userLookupPipeline,
as: 'userDetails',
},
},
{ $unwind: '$userDetails' },
{
$addFields: {
primaryRole: {
$reduce: {
input: '$roles',
initialValue: { role: null, priority: defaultPriority },
in: {
$let: {
vars: {
currentPriority: {
$switch: {
branches,
default: defaultPriority,
},
},
},
in: {
$cond: [
{
$and: [{ $in: ['$$this', rolesInOrder] }, { $lt: ['$$currentPriority', '$$value.priority'] }],
},
{ role: '$$this', priority: '$$currentPriority' },
'$$value',
],
},
},
},
},
},
},
},
{
$addFields: {
rolePriority: { $ifNull: ['$primaryRole.priority', defaultPriority] },
},
},
{
$project: {
_id: '$userDetails._id',
rid: 1,
roles: 1,
primaryRole: '$primaryRole.role',
rolePriority: 1,
username: '$userDetails.username',
name: '$userDetails.name',
nickname: '$userDetails.nickname',
status: '$userDetails.status',
avatarETag: '$userDetails.avatarETag',
_updatedAt: '$userDetails._updatedAt',
federated: '$userDetails.federated',
statusConnection: '$userDetails.statusConnection',
},
},
];

const facetPipeline: Document[] = [
{ $match: { rid } },
{
$facet: {
totalCount: [{ $match: { rid } }, ...filteredPipeline, { $count: 'total' }],
members: [
{ $match: { rid } },
...filteredPipeline,
{ $sort: sortCriteria },
...(skip > 0 ? [{ $skip: skip }] : []),
...(limit > 0 ? [{ $limit: limit }] : []),
],
},
},
{
$project: {
members: 1,
totalCount: { $arrayElemAt: ['$totalCount.total', 0] },
},
},
];

const [result] = await Subscriptions.col.aggregate(facetPipeline, { allowDiskUse: true }).toArray();

return {
members: result.members.map((member: any) => {
delete member.primaryRole;
delete member.rolePriority;
return member;
}),
total: result.totalCount,
};
}
Loading

0 comments on commit ae9d2e1

Please sign in to comment.