Skip to content

Commit

Permalink
Merge branch 'develop' into feat/new-user-panel-active
Browse files Browse the repository at this point in the history
  • Loading branch information
kodiakhq[bot] committed Jul 16, 2024
2 parents 2346fa6 + fa82159 commit 79b0324
Show file tree
Hide file tree
Showing 30 changed files with 398 additions and 77 deletions.
8 changes: 8 additions & 0 deletions .changeset/empty-readers-teach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@rocket.chat/meteor": patch
"@rocket.chat/tools": patch
"@rocket.chat/account-service": patch
---

Fixed an inconsistent evaluation of the `Accounts_LoginExpiration` setting over the codebase. In some places, it was being used as milliseconds while in others as days. Invalid values produced different results. A helper function was created to centralize the setting validation and the proper value being returned to avoid edge cases.
Negative values may be saved on the settings UI panel but the code will interpret any negative, NaN or 0 value to the default expiration which is 90 days.
5 changes: 5 additions & 0 deletions .changeset/grumpy-worms-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/i18n": patch
---

Fixed wrong wording on a federation setting
5 changes: 5 additions & 0 deletions .changeset/happy-peaches-nail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": patch
---

Fixed issue with livechat agents not being able to leave omnichannel rooms if joining after a room has been closed by the visitor (due to race conditions)
5 changes: 5 additions & 0 deletions .changeset/sour-forks-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rocket.chat/meteor": minor
---

Extended apps-engine events for users leaving a room to also fire when being removed by another user. Also added the triggering user's information to the event's context payload.
6 changes: 4 additions & 2 deletions apps/meteor/app/api/server/v1/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
isUsersCheckUsernameAvailabilityParamsGET,
isUsersSendConfirmationEmailParamsPOST,
} from '@rocket.chat/rest-typings';
import { getLoginExpirationInMs } from '@rocket.chat/tools';
import { Accounts } from 'meteor/accounts-base';
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
Expand Down Expand Up @@ -1048,8 +1049,9 @@ API.v1.addRoute(

const token = me.services?.resume?.loginTokens?.find((token) => token.hashedToken === hashedToken);

const tokenExpires =
(token && 'when' in token && new Date(token.when.getTime() + settings.get<number>('Accounts_LoginExpiration') * 1000)) || undefined;
const loginExp = settings.get<number>('Accounts_LoginExpiration');

const tokenExpires = (token && 'when' in token && new Date(token.when.getTime() + getLoginExpirationInMs(loginExp))) || undefined;

return API.v1.success({
token: xAuthToken,
Expand Down
3 changes: 2 additions & 1 deletion apps/meteor/app/apps/server/bridges/listeners.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,11 @@ export class AppListenerBridge {
};
case AppInterface.IPreRoomUserLeave:
case AppInterface.IPostRoomUserLeave:
const [leavingUser] = payload;
const [leavingUser, removedBy] = payload;
return {
room: rm,
leavingUser: this.orch.getConverters().get('users').convertToApp(leavingUser),
removedBy: this.orch.getConverters().get('users').convertToApp(removedBy),
};
default:
return rm;
Expand Down
3 changes: 2 additions & 1 deletion apps/meteor/app/authentication/server/startup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Apps, AppEvents } from '@rocket.chat/apps';
import { User } from '@rocket.chat/core-services';
import { Roles, Settings, Users } from '@rocket.chat/models';
import { escapeRegExp, escapeHTML } from '@rocket.chat/string-helpers';
import { getLoginExpirationInDays } from '@rocket.chat/tools';
import { Accounts } from 'meteor/accounts-base';
import { Match } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
Expand Down Expand Up @@ -31,7 +32,7 @@ Accounts.config({

Meteor.startup(() => {
settings.watchMultiple(['Accounts_LoginExpiration', 'Site_Name', 'From_Email'], () => {
Accounts._options.loginExpirationInDays = settings.get('Accounts_LoginExpiration');
Accounts._options.loginExpirationInDays = getLoginExpirationInDays(settings.get('Accounts_LoginExpiration'));

Accounts.emailTemplates.siteName = settings.get('Site_Name');

Expand Down
81 changes: 81 additions & 0 deletions apps/meteor/app/lib/server/functions/closeLivechatRoom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { IUser, IRoom, IOmnichannelRoom } from '@rocket.chat/core-typings';
import { isOmnichannelRoom } from '@rocket.chat/core-typings';
import { LivechatRooms, Subscriptions } from '@rocket.chat/models';

import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
import type { CloseRoomParams } from '../../../livechat/server/lib/LivechatTyped';
import { Livechat } from '../../../livechat/server/lib/LivechatTyped';

export const closeLivechatRoom = async (
user: IUser,
roomId: IRoom['_id'],
{
comment,
tags,
generateTranscriptPdf,
transcriptEmail,
}: {
comment?: string;
tags?: string[];
generateTranscriptPdf?: boolean;
transcriptEmail?:
| {
sendToVisitor: false;
}
| {
sendToVisitor: true;
requestData: Pick<NonNullable<IOmnichannelRoom['transcriptRequest']>, 'email' | 'subject'>;
};
},
): Promise<void> => {
const room = await LivechatRooms.findOneById(roomId);
if (!room || !isOmnichannelRoom(room)) {
throw new Error('error-invalid-room');
}

if (!room.open) {
const subscriptionsLeft = await Subscriptions.countByRoomId(roomId);
if (subscriptionsLeft) {
await Subscriptions.removeByRoomId(roomId);
return;
}
throw new Error('error-room-already-closed');
}

const subscription = await Subscriptions.findOneByRoomIdAndUserId(roomId, user._id, { projection: { _id: 1 } });
if (!subscription && !(await hasPermissionAsync(user._id, 'close-others-livechat-room'))) {
throw new Error('error-not-authorized');
}

const options: CloseRoomParams['options'] = {
clientAction: true,
tags,
...(generateTranscriptPdf && { pdfTranscript: { requestedBy: user._id } }),
...(transcriptEmail && {
...(transcriptEmail.sendToVisitor
? {
emailTranscript: {
sendToVisitor: true,
requestData: {
email: transcriptEmail.requestData.email,
subject: transcriptEmail.requestData.subject,
requestedAt: new Date(),
requestedBy: user,
},
},
}
: {
emailTranscript: {
sendToVisitor: false,
},
}),
}),
};

await Livechat.closeRoom({
room,
user,
options,
comment,
});
};
10 changes: 3 additions & 7 deletions apps/meteor/app/lib/server/functions/removeUserFromRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,15 @@ import { beforeLeaveRoomCallback } from '../../../../lib/callbacks/beforeLeaveRo
import { settings } from '../../../settings/server';
import { notifyOnRoomChangedById } from '../lib/notifyListener';

export const removeUserFromRoom = async function (
rid: string,
user: IUser,
options?: { byUser: Pick<IUser, '_id' | 'username'> },
): Promise<void> {
export const removeUserFromRoom = async function (rid: string, user: IUser, options?: { byUser: IUser }): Promise<void> {
const room = await Rooms.findOneById(rid);

if (!room) {
return;
}

try {
await Apps.self?.triggerEvent(AppEvents.IPreRoomUserLeave, room, user);
await Apps.self?.triggerEvent(AppEvents.IPreRoomUserLeave, room, user, options?.byUser);
} catch (error: any) {
if (error.name === AppsEngineException.name) {
throw new Meteor.Error('error-app-prevented', error.message);
Expand Down Expand Up @@ -75,5 +71,5 @@ export const removeUserFromRoom = async function (

void notifyOnRoomChangedById(rid);

await Apps.self?.triggerEvent(AppEvents.IPostRoomUserLeave, room, user);
await Apps.self?.triggerEvent(AppEvents.IPostRoomUserLeave, room, user, options?.byUser);
};
49 changes: 3 additions & 46 deletions apps/meteor/app/livechat/server/api/v1/room.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Omnichannel } from '@rocket.chat/core-services';
import type { ILivechatAgent, IUser, SelectedAgent, TransferByData } from '@rocket.chat/core-typings';
import { isOmnichannelRoom, OmnichannelSourceType } from '@rocket.chat/core-typings';
import { LivechatVisitors, Users, LivechatRooms, Subscriptions, Messages } from '@rocket.chat/models';
import { LivechatVisitors, Users, LivechatRooms, Messages } from '@rocket.chat/models';
import {
isLiveChatRoomForwardProps,
isPOSTLivechatRoomCloseParams,
Expand All @@ -21,6 +21,7 @@ import { isWidget } from '../../../../api/server/helpers/isWidget';
import { canAccessRoomAsync, roomAccessAttributes } from '../../../../authorization/server';
import { hasPermissionAsync } from '../../../../authorization/server/functions/hasPermission';
import { addUserToRoom } from '../../../../lib/server/functions/addUserToRoom';
import { closeLivechatRoom } from '../../../../lib/server/functions/closeLivechatRoom';
import { settings as rcSettings } from '../../../../settings/server';
import { normalizeTransferredByData } from '../../lib/Helper';
import type { CloseRoomParams } from '../../lib/LivechatTyped';
Expand Down Expand Up @@ -178,51 +179,7 @@ API.v1.addRoute(
async post() {
const { rid, comment, tags, generateTranscriptPdf, transcriptEmail } = this.bodyParams;

const room = await LivechatRooms.findOneById(rid);
if (!room || !isOmnichannelRoom(room)) {
throw new Error('error-invalid-room');
}

if (!room.open) {
throw new Error('error-room-already-closed');
}

const subscription = await Subscriptions.findOneByRoomIdAndUserId(rid, this.userId, { projection: { _id: 1 } });
if (!subscription && !(await hasPermissionAsync(this.userId, 'close-others-livechat-room'))) {
throw new Error('error-not-authorized');
}

const options: CloseRoomParams['options'] = {
clientAction: true,
tags,
...(generateTranscriptPdf && { pdfTranscript: { requestedBy: this.userId } }),
...(transcriptEmail && {
...(transcriptEmail.sendToVisitor
? {
emailTranscript: {
sendToVisitor: true,
requestData: {
email: transcriptEmail.requestData.email,
subject: transcriptEmail.requestData.subject,
requestedAt: new Date(),
requestedBy: this.user,
},
},
}
: {
emailTranscript: {
sendToVisitor: false,
},
}),
}),
};

await LivechatTyped.closeRoom({
room,
user: this.user,
options,
comment,
});
await closeLivechatRoom(this.user, rid, { comment, tags, generateTranscriptPdf, transcriptEmail });

return API.v1.success();
},
Expand Down
15 changes: 10 additions & 5 deletions apps/meteor/app/livechat/server/methods/closeRoom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ Meteor.methods<ServerMethods>({
});
}

const subscription = await SubscriptionRaw.findOneByRoomIdAndUserId(roomId, userId, {
projection: {
_id: 1,
},
});
if (!room.open && subscription) {
await SubscriptionRaw.removeByRoomId(roomId);
return;
}

if (!room.open) {
throw new Meteor.Error('room-closed', 'Room closed', { method: 'livechat:closeRoom' });
}
Expand All @@ -71,11 +81,6 @@ Meteor.methods<ServerMethods>({
});
}

const subscription = await SubscriptionRaw.findOneByRoomIdAndUserId(roomId, user._id, {
projection: {
_id: 1,
},
});
if (!subscription && !(await hasPermissionAsync(userId, 'close-others-livechat-room'))) {
throw new Meteor.Error('error-not-authorized', 'Not authorized', {
method: 'livechat:closeRoom',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
useMethod,
useTranslation,
useRouter,
useUserSubscription,
} from '@rocket.chat/ui-contexts';
import React, { useCallback, useState, useEffect } from 'react';

Expand Down Expand Up @@ -47,6 +48,7 @@ export const useQuickActions = (): {
const visitorRoomId = room.v._id;
const rid = room._id;
const uid = useUserId();
const subscription = useUserSubscription(rid);
const roomLastMessage = room.lastMessage;

const getVisitorInfo = useEndpoint('GET', '/v1/livechat/visitors.info');
Expand Down Expand Up @@ -330,7 +332,7 @@ export const useQuickActions = (): {
case QuickActionsEnum.TranscriptPDF:
return hasLicense && !isRoomOverMacLimit && canSendTranscriptPDF;
case QuickActionsEnum.CloseChat:
return !!roomOpen && (canCloseRoom || canCloseOthersRoom);
return (subscription && (canCloseRoom || canCloseOthersRoom)) || (!!roomOpen && canCloseOthersRoom);
case QuickActionsEnum.OnHoldChat:
return !!roomOpen && canPlaceChatOnHold;
default:
Expand Down
14 changes: 14 additions & 0 deletions apps/meteor/server/methods/removeUserFromRoom.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Apps, AppEvents } from '@rocket.chat/apps';
import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions';
import { Message, Team } from '@rocket.chat/core-services';
import { Subscriptions, Rooms, Users } from '@rocket.chat/models';
import type { ServerMethods } from '@rocket.chat/ui-contexts';
Expand Down Expand Up @@ -75,6 +77,16 @@ export const removeUserFromRoomMethod = async (fromId: string, data: { rid: stri
}
}

try {
await Apps.self?.triggerEvent(AppEvents.IPreRoomUserLeave, room, removedUser, fromUser);
} catch (error: any) {
if (error.name === AppsEngineException.name) {
throw new Meteor.Error('error-app-prevented', error.message);
}

throw error;
}

await callbacks.run('beforeRemoveFromRoom', { removedUser, userWhoRemoved: fromUser }, room);

await Subscriptions.removeByRoomIdAndUserId(data.rid, removedUser._id);
Expand All @@ -99,6 +111,8 @@ export const removeUserFromRoomMethod = async (fromId: string, data: { rid: stri
void notifyOnRoomChanged(room);
});

await Apps.self?.triggerEvent(AppEvents.IPostRoomUserLeave, room, removedUser, fromUser);

return true;
};

Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/server/services/room/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class RoomService extends ServiceClassInternal implements IRoomService {
return addUserToRoom(roomId, user, inviter, silenced);
}

async removeUserFromRoom(roomId: string, user: IUser, options?: { byUser: Pick<IUser, '_id' | 'username'> }): Promise<void> {
async removeUserFromRoom(roomId: string, user: IUser, options?: { byUser: IUser }): Promise<void> {
return removeUserFromRoom(roomId, user, options);
}

Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/server/services/team/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,7 @@ export class TeamService extends ServiceClassInternal implements ITeamService {
const usersToRemove = await Users.findByIds(membersIds, {
projection: { _id: 1, username: 1 },
}).toArray();
const byUser = (await Users.findOneById(uid, { projection: { _id: 1, username: 1 } })) as Pick<IUser, '_id' | 'username'>;
const byUser = await Users.findOneById(uid);

for await (const member of members) {
if (!member.userId) {
Expand Down Expand Up @@ -802,7 +802,7 @@ export class TeamService extends ServiceClassInternal implements ITeamService {
await removeUserFromRoom(
team.roomId,
removedUser,
uid !== member.userId
uid !== member.userId && byUser
? {
byUser,
}
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/tests/e2e/message-mentions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ test.describe.serial('Should not allow to send @all mention if permission to do
await expect(page).toHaveURL(`/group/${targetChannel2}`);
});
await test.step('receive notify message', async () => {
await adminPage.content.dispatchSlashCommand('@all');
await adminPage.content.sendMessage('@all ');
await expect(adminPage.content.lastUserMessage).toContainText('Notify all in this room is not allowed');
});
});
Expand Down Expand Up @@ -98,7 +98,7 @@ test.describe.serial('Should not allow to send @here mention if permission to do
await expect(page).toHaveURL(`/group/${targetChannel2}`);
});
await test.step('receive notify message', async () => {
await adminPage.content.dispatchSlashCommand('@here');
await adminPage.content.sendMessage('@here ');
await expect(adminPage.content.lastUserMessage).toContainText('Notify all in this room is not allowed');
});
});
Expand Down
Loading

0 comments on commit 79b0324

Please sign in to comment.