Skip to content

Commit

Permalink
feat: Limit of members that can be added using the autojoin feature i…
Browse files Browse the repository at this point in the history
…n a team's channel to the value of the API_Users_Limit setting (#31859)

Co-authored-by: Matheus Barbosa Silva <[email protected]>
  • Loading branch information
Gustrb and matheusbsilva137 committed Jun 12, 2024
1 parent 972ba42 commit 16b67aa
Show file tree
Hide file tree
Showing 10 changed files with 148 additions and 16 deletions.
6 changes: 6 additions & 0 deletions .changeset/clean-moose-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@rocket.chat/meteor": minor
"@rocket.chat/i18n": minor
---

Introduced the use of the `API_User_Limit` setting to limit amount of members to simultaneously auto-join a room in a team
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ import TeamsChannelItemMenu from './TeamsChannelItemMenu';

type TeamsChannelItemProps = {
room: IRoom;
mainRoom: IRoom;
onClickView: (room: IRoom) => void;
reload: () => void;
};

const TeamsChannelItem = ({ room, onClickView, reload }: TeamsChannelItemProps) => {
const TeamsChannelItem = ({ room, mainRoom, onClickView, reload }: TeamsChannelItemProps) => {
const t = useTranslation();
const rid = room._id;
const type = room.t;
Expand Down Expand Up @@ -68,7 +69,7 @@ const TeamsChannelItem = ({ room, onClickView, reload }: TeamsChannelItemProps)
</OptionContent>
{(canRemoveTeamChannel || canEditTeamChannel || canDeleteTeamChannel) && (
<OptionMenu onClick={onClick}>
{showButton ? <TeamsChannelItemMenu room={room} reload={reload} /> : <IconButton tiny icon='kebab' />}
{showButton ? <TeamsChannelItemMenu room={room} mainRoom={mainRoom} reload={reload} /> : <IconButton tiny icon='kebab' />}
</OptionMenu>
)}
</Option>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import { useDeleteRoom } from '../../../hooks/roomActions/useDeleteRoom';
import { useRemoveRoomFromTeam } from './hooks/useRemoveRoomFromTeam';
import { useToggleAutoJoin } from './hooks/useToggleAutoJoin';

const TeamsChannelItemMenu = ({ room, reload }: { room: IRoom; reload?: () => void }) => {
const TeamsChannelItemMenu = ({ room, mainRoom, reload }: { room: IRoom; mainRoom: IRoom; reload?: () => void }) => {
const t = useTranslation();

const { handleRemoveRoom, canRemoveTeamChannel } = useRemoveRoomFromTeam(room, { reload });
const { handleDelete, canDeleteRoom } = useDeleteRoom(room, { reload });
const { handleToggleAutoJoin, canEditTeamChannel } = useToggleAutoJoin(room, { reload });
const { handleToggleAutoJoin, canEditTeamChannel } = useToggleAutoJoin(room, { reload, mainRoom });

const toggleAutoJoin = {
id: 'toggleAutoJoin',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import TeamsChannelItem from './TeamsChannelItem';
type TeamsChannelsProps = {
loading: boolean;
channels: IRoom[];
mainRoom: IRoom;
text: string;
type: 'all' | 'autoJoin';
setType: Dispatch<SetStateAction<'all' | 'autoJoin'>>;
Expand All @@ -39,6 +40,7 @@ type TeamsChannelsProps = {
const TeamsChannels = ({
loading,
channels = [],
mainRoom,
text,
type,
setText,
Expand Down Expand Up @@ -123,7 +125,9 @@ const TeamsChannels = ({
data={channels}
// eslint-disable-next-line react/no-multi-comp
components={{ Scroller: VirtuosoScrollbars, Footer: () => <InfiniteListAnchor loadMore={loadMoreChannels} /> }}
itemContent={(index, data) => <TeamsChannelItem onClickView={onClickView} room={data} reload={reload} key={index} />}
itemContent={(index, data) => (
<TeamsChannelItem onClickView={onClickView} room={data} mainRoom={mainRoom} reload={reload} key={index} />
)}
/>
</Box>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const TeamsChannelsWithData = () => {
return (
<TeamsChannels
loading={phase === AsyncStatePhase.LOADING}
mainRoom={room}
type={type}
text={text}
setType={setType}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,41 @@
import type { IRoom } from '@rocket.chat/core-typings';
import { useEndpoint, usePermission, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import { useEndpoint, usePermission, useSetting, useToastMessageDispatch } from '@rocket.chat/ui-contexts';
import { t } from 'i18next';

export const useToggleAutoJoin = (room: IRoom, { reload }: { reload?: () => void }) => {
import { roomCoordinator } from '../../../../../lib/rooms/roomCoordinator';

export const useToggleAutoJoin = (room: IRoom, { reload, mainRoom }: { reload?: () => void; mainRoom: IRoom }) => {
const dispatchToastMessage = useToastMessageDispatch();
const updateRoomEndpoint = useEndpoint('POST', '/v1/teams.updateRoom');
const canEditTeamChannel = usePermission('edit-team-channel', room._id);
const maxNumberOfAutoJoinMembers = useSetting<number>('API_User_Limit');

const handleToggleAutoJoin = async () => {
// Sanity check, the setting has a default value, therefore it should always be defined
if (!maxNumberOfAutoJoinMembers) {
return;
}

try {
await updateRoomEndpoint({
const { room: updatedRoom } = await updateRoomEndpoint({
roomId: room._id,
isDefault: !room.teamDefault,
});
dispatchToastMessage({ type: 'success', message: room.teamDefault ? 'channel set as non autojoin' : 'channel set as autojoin' });
if (updatedRoom.teamDefault) {
// If the number of members in the mainRoom (the team) is greater than the limit, show an info message
// informing that not all members will be auto-joined to the channel
const messageType = mainRoom.usersCount > maxNumberOfAutoJoinMembers ? 'info' : 'success';
const message = mainRoom.usersCount > maxNumberOfAutoJoinMembers ? 'Team_Auto-join_exceeded_user_limit' : 'Team_Auto-join_updated';

dispatchToastMessage({
type: messageType,
message: t(message, {
channelName: roomCoordinator.getRoomName(room.t, room),
numberOfMembers: updatedRoom.usersCount,
limit: maxNumberOfAutoJoinMembers,
}),
});
}
reload?.();
} catch (error) {
dispatchToastMessage({ type: 'error', message: error });
Expand Down
17 changes: 14 additions & 3 deletions apps/meteor/server/services/team/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { addUserToRoom } from '../../../app/lib/server/functions/addUserToRoom';
import { checkUsernameAvailability } from '../../../app/lib/server/functions/checkUsernameAvailability';
import { getSubscribedRoomsForUserWithDetails } from '../../../app/lib/server/functions/getRoomsWithSingleOwner';
import { removeUserFromRoom } from '../../../app/lib/server/functions/removeUserFromRoom';
import { settings } from '../../../app/settings/server';

export class TeamService extends ServiceClassInternal implements ITeamService {
protected name = 'team';
Expand Down Expand Up @@ -472,11 +473,21 @@ export class TeamService extends ServiceClassInternal implements ITeamService {
room.teamDefault = isDefault;
await Rooms.setTeamDefaultById(rid, isDefault);

if (room.teamDefault) {
const teamMembers = await this.members(uid, room.teamId, true, undefined, undefined);
if (isDefault) {
const maxNumberOfAutoJoinMembers = settings.get<number>('API_User_Limit');
const teamMembers = await this.members(
uid,
room.teamId,
true,
{ offset: 0, count: maxNumberOfAutoJoinMembers },
// We should not get the owner of the room, since he is already a member
{ _id: { $ne: room.u._id } },
);

for await (const m of teamMembers.records) {
await addUserToRoom(room._id, m.user, user);
if (await addUserToRoom(room._id, m.user, user)) {
room.usersCount++;
}
}
}

Expand Down
13 changes: 12 additions & 1 deletion apps/meteor/tests/data/teams.helper.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ITeam, TEAM_TYPE } from "@rocket.chat/core-typings"
import { api, request } from "./api-data"
import { IUser } from "@rocket.chat/apps-engine/definition/users";

export const createTeam = async (credentials: Record<string, any>, teamName: string, type: TEAM_TYPE): Promise<ITeam> => {
const response = await request.post(api('teams.create')).set(credentials).send({
Expand All @@ -14,4 +15,14 @@ export const deleteTeam = async (credentials: Record<string, any>, teamName: str
await request.post(api('teams.delete')).set(credentials).send({
teamName,
});
};
};

export const addMembers = async (credentials: Record<string, any>, teamName: string, members: IUser['id'][]): Promise<void> => {
await request
.post(api('teams.addMembers'))
.set(credentials)
.send({
teamName: teamName,
members: members.map((userId) => ({ userId, roles: ['member'] }))
});
};
79 changes: 76 additions & 3 deletions apps/meteor/tests/end-to-end/api/25-teams.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { TEAM_TYPE } from '@rocket.chat/core-typings';
import { expect } from 'chai';
import { before, describe, it, after } from 'mocha';
import { after, afterEach, before, beforeEach, describe, it } from 'mocha';

import { getCredentials, api, request, credentials, methodCall } from '../../data/api-data';
import { updatePermission } from '../../data/permissions.helper';
import { updatePermission, updateSetting } from '../../data/permissions.helper';
import { createRoom, deleteRoom } from '../../data/rooms.helper';
import { createTeam, deleteTeam } from '../../data/teams.helper';
import { addMembers, createTeam, deleteTeam } from '../../data/teams.helper';
import { adminUsername, password } from '../../data/user';
import { createUser, deleteUser, login } from '../../data/users.helper';

Expand Down Expand Up @@ -1594,6 +1594,79 @@ describe('[Teams]', () => {
.end(done);
});
});

describe('team auto-join', () => {
let testTeam;
let createdRoom;
let testUser1;
let testUser2;

before(async () => {
const [testUserResult, testUser1Result] = await Promise.all([createUser(), createUser()]);
testUser1 = testUserResult;
testUser2 = testUser1Result;
});

beforeEach(async () => {
const createTeamPromise = createTeam(credentials, `test-team-name${Date.now()}`, 0);
const createRoomPromise = createRoom({ name: `test-room-name${Date.now()}`, type: 'c' });
const [testTeamCreationResult, testRoomCreationResult] = await Promise.all([createTeamPromise, createRoomPromise]);

testTeam = testTeamCreationResult;
createdRoom = testRoomCreationResult;

await request
.post(api('teams.addRooms'))
.set(credentials)
.expect(200)
.send({
rooms: [createdRoom.body.channel._id],
teamName: testTeam.name,
});
});

afterEach(() =>
Promise.all([deleteTeam(credentials, testTeam.name), deleteRoom({ roomId: createdRoom.body.channel._id, type: 'c' })]),
);

after(() => Promise.all([updateSetting('API_User_Limit', 500), deleteUser(testUser1), deleteUser(testUser2)]));

it('should add members when the members count is less than or equal to the API_User_Limit setting', async () => {
await updateSetting('API_User_Limit', 2);

await addMembers(credentials, testTeam.name, [testUser1._id, testUser2._id]);
await request
.post(api('teams.updateRoom'))
.set(credentials)
.send({
roomId: createdRoom.body.channel._id,
isDefault: true,
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.nested.property('room.usersCount').and.to.be.equal(3);
});
});

it('should not add all members when we update a team channel to be auto-join and the members count is greater than the API_User_Limit setting', async () => {
await updateSetting('API_User_Limit', 1);

await addMembers(credentials, testTeam.name, [testUser1._id, testUser2._id]);
await request
.post(api('teams.updateRoom'))
.set(credentials)
.send({
roomId: createdRoom.body.channel._id,
isDefault: true,
})
.expect(200)
.expect((res) => {
expect(res.body).to.have.property('success', true);
expect(res.body).to.have.nested.property('room.usersCount').and.to.be.equal(2);
});
});
});
});

describe('/teams.removeRoom', () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -5127,6 +5127,8 @@
"Team_Add_existing_channels": "Add Existing Channels",
"Team_Add_existing": "Add Existing",
"Team_Auto-join": "Auto-join",
"Team_Auto-join_exceeded_user_limit": "Auto-join has a limit of {{limit}} members, #{{channelName}} now has {{numberOfMembers}} members",
"Team_Auto-join_updated": "#{{channelName}} now has {{numberOfMembers}} members",
"Team_Channels": "Team Channels",
"Team_Delete_Channel_modal_content_danger": "This can’t be undone.",
"Team_Delete_Channel_modal_content": "Would you like to delete this Channel?",
Expand Down

0 comments on commit 16b67aa

Please sign in to comment.