From edd695f41ca0d6d918c35edf3f91c3a191ff61d3 Mon Sep 17 00:00:00 2001
From: Yash Rajpal <58601732+yash-rajpal@users.noreply.github.com>
Date: Sun, 7 May 2023 16:18:27 +0530
Subject: [PATCH 1/4] regression: custom emojis are not visible (#29084)
---
apps/meteor/client/components/Emoji.tsx | 12 ++++++++++--
1 file changed, 10 insertions(+), 2 deletions(-)
diff --git a/apps/meteor/client/components/Emoji.tsx b/apps/meteor/client/components/Emoji.tsx
index b2aa27435f1..69eeadc9d9f 100644
--- a/apps/meteor/client/components/Emoji.tsx
+++ b/apps/meteor/client/components/Emoji.tsx
@@ -22,8 +22,16 @@ const EmojiComponent = styled('span', ({ fillContainer: _fillContainer, ...props
`;
function Emoji({ emojiHandle, className = undefined, fillContainer }: EmojiProps): ReactElement {
- const { className: emojiClassName, ...props } = getEmojiClassNameAndDataTitle(emojiHandle);
- return ;
+ const { className: emojiClassName, image, ...props } = getEmojiClassNameAndDataTitle(emojiHandle);
+
+ return (
+
+ );
}
export default Emoji;
From 9829a8aa4380282d7fb340b5921e8a548658b5de Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=83=87=E3=83=B4=E3=81=81=E3=82=93=E3=81=99?=
<61188295+Dnouv@users.noreply.github.com>
Date: Mon, 8 May 2023 19:01:07 +0530
Subject: [PATCH 2/4] feat: moderation dashboard (#28962)
Co-authored-by: Debdut Chakraborty
Co-authored-by: Hugo Costa
Co-authored-by: Diego Sampaio
Co-authored-by: Guilherme Gazzo
---
apps/meteor/app/api/server/index.ts | 1 +
apps/meteor/app/api/server/v1/chat.ts | 6 +-
apps/meteor/app/api/server/v1/moderation.ts | 240 +++++++
apps/meteor/app/api/server/v1/users.ts | 10 +-
.../meteor/app/apps/server/bridges/bridges.js | 6 +
.../app/apps/server/bridges/messages.ts | 14 +
.../app/apps/server/bridges/moderation.ts | 46 ++
apps/meteor/app/apps/server/bridges/users.ts | 14 +
.../server/functions/upsertPermissions.ts | 2 +
.../app/lib/server/methods/deleteMessage.ts | 3 +
.../AdministrationList/AdministrationList.tsx | 1 +
apps/meteor/client/lib/createSidebarItems.ts | 2 +-
.../admin/moderation/MessageContextFooter.tsx | 35 +
.../admin/moderation/MessageReportInfo.tsx | 102 +++
.../moderation/ModerationConsoleActions.tsx | 37 ++
.../moderation/ModerationConsolePage.tsx | 45 ++
.../moderation/ModerationConsoleRoute.tsx | 16 +
.../moderation/ModerationConsoleTable.tsx | 175 +++++
.../moderation/ModerationConsoleTableRow.tsx | 71 +++
.../views/admin/moderation/UserMessages.tsx | 112 ++++
.../moderation/helpers/ContextMessage.tsx | 90 +++
.../moderation/helpers/DateRangePicker.tsx | 126 ++++
.../hooks/useDeactivateUserAction.tsx | 65 ++
.../moderation/hooks/useDeleteMessage.tsx | 62 ++
.../hooks/useDeleteMessagesAction.tsx | 52 ++
.../moderation/hooks/useDismissUserAction.tsx | 53 ++
.../moderation/hooks/useResetAvatarAction.tsx | 51 ++
apps/meteor/client/views/admin/routes.tsx | 5 +
.../meteor/client/views/admin/sidebarItems.ts | 7 +
.../actions/useRedirectModerationConsole.ts | 25 +
.../useUserInfoActions/useUserInfoActions.ts | 14 +-
apps/meteor/package.json | 3 +-
.../rocketchat-i18n/i18n/en.i18n.json | 30 +
.../lib/moderation/deleteReportedMessages.ts | 42 ++
.../server/lib/moderation/reportMessage.ts | 55 ++
apps/meteor/server/methods/reportMessage.ts | 32 +-
.../meteor/server/models/ModerationReports.ts | 6 +
apps/meteor/server/models/raw/Messages.ts | 42 ++
.../server/models/raw/ModerationReports.ts | 242 +++++++
apps/meteor/server/models/startup.ts | 2 +-
.../meteor/server/startup/migrations/index.ts | 1 +
apps/meteor/server/startup/migrations/v293.ts | 9 +
.../tests/end-to-end/api/27-moderation.ts | 600 ++++++++++++++++++
.../core-typings/src/IModerationReport.ts | 33 +
packages/core-typings/src/index.ts | 2 +
packages/model-typings/src/index.ts | 1 +
.../src/models/IMessagesModel.ts | 2 +
.../src/models/IModerationReportsModel.ts | 51 ++
packages/models/src/index.ts | 2 +
packages/rest-typings/package.json | 3 +-
packages/rest-typings/src/index.ts | 3 +
packages/rest-typings/src/v1/Ajv.ts | 6 +-
.../src/v1/moderation/ArchiveReportProps.ts | 34 +
.../ModerationDeleteMsgHistoryParams.ts | 24 +
.../src/v1/moderation/ReportHistoryProps.ts | 44 ++
.../src/v1/moderation/ReportInfoParams.ts | 24 +
.../moderation/ReportMessageHistoryParams.ts | 40 ++
.../src/v1/moderation/ReportsByMsgIdParams.ts | 38 ++
.../rest-typings/src/v1/moderation/index.ts | 8 +
.../src/v1/moderation/moderation.ts | 42 ++
yarn.lock | 48 +-
61 files changed, 2939 insertions(+), 18 deletions(-)
create mode 100644 apps/meteor/app/api/server/v1/moderation.ts
create mode 100644 apps/meteor/app/apps/server/bridges/moderation.ts
create mode 100644 apps/meteor/client/views/admin/moderation/MessageContextFooter.tsx
create mode 100644 apps/meteor/client/views/admin/moderation/MessageReportInfo.tsx
create mode 100644 apps/meteor/client/views/admin/moderation/ModerationConsoleActions.tsx
create mode 100644 apps/meteor/client/views/admin/moderation/ModerationConsolePage.tsx
create mode 100644 apps/meteor/client/views/admin/moderation/ModerationConsoleRoute.tsx
create mode 100644 apps/meteor/client/views/admin/moderation/ModerationConsoleTable.tsx
create mode 100644 apps/meteor/client/views/admin/moderation/ModerationConsoleTableRow.tsx
create mode 100644 apps/meteor/client/views/admin/moderation/UserMessages.tsx
create mode 100644 apps/meteor/client/views/admin/moderation/helpers/ContextMessage.tsx
create mode 100644 apps/meteor/client/views/admin/moderation/helpers/DateRangePicker.tsx
create mode 100644 apps/meteor/client/views/admin/moderation/hooks/useDeactivateUserAction.tsx
create mode 100644 apps/meteor/client/views/admin/moderation/hooks/useDeleteMessage.tsx
create mode 100644 apps/meteor/client/views/admin/moderation/hooks/useDeleteMessagesAction.tsx
create mode 100644 apps/meteor/client/views/admin/moderation/hooks/useDismissUserAction.tsx
create mode 100644 apps/meteor/client/views/admin/moderation/hooks/useResetAvatarAction.tsx
create mode 100644 apps/meteor/client/views/room/hooks/useUserInfoActions/actions/useRedirectModerationConsole.ts
create mode 100644 apps/meteor/server/lib/moderation/deleteReportedMessages.ts
create mode 100644 apps/meteor/server/lib/moderation/reportMessage.ts
create mode 100644 apps/meteor/server/models/ModerationReports.ts
create mode 100644 apps/meteor/server/models/raw/ModerationReports.ts
create mode 100644 apps/meteor/server/startup/migrations/v293.ts
create mode 100644 apps/meteor/tests/end-to-end/api/27-moderation.ts
create mode 100644 packages/core-typings/src/IModerationReport.ts
create mode 100644 packages/model-typings/src/models/IModerationReportsModel.ts
create mode 100644 packages/rest-typings/src/v1/moderation/ArchiveReportProps.ts
create mode 100644 packages/rest-typings/src/v1/moderation/ModerationDeleteMsgHistoryParams.ts
create mode 100644 packages/rest-typings/src/v1/moderation/ReportHistoryProps.ts
create mode 100644 packages/rest-typings/src/v1/moderation/ReportInfoParams.ts
create mode 100644 packages/rest-typings/src/v1/moderation/ReportMessageHistoryParams.ts
create mode 100644 packages/rest-typings/src/v1/moderation/ReportsByMsgIdParams.ts
create mode 100644 packages/rest-typings/src/v1/moderation/index.ts
create mode 100644 packages/rest-typings/src/v1/moderation/moderation.ts
diff --git a/apps/meteor/app/api/server/index.ts b/apps/meteor/app/api/server/index.ts
index 48b7e0d8699..e357c2e3ce7 100644
--- a/apps/meteor/app/api/server/index.ts
+++ b/apps/meteor/app/api/server/index.ts
@@ -47,5 +47,6 @@ import './v1/voip/extensions';
import './v1/voip/queues';
import './v1/voip/omnichannel';
import './v1/voip';
+import './v1/moderation';
export { API, APIClass, defaultRateLimiterOptions } from './api';
diff --git a/apps/meteor/app/api/server/v1/chat.ts b/apps/meteor/app/api/server/v1/chat.ts
index 2ede3edc55a..b3570b91676 100644
--- a/apps/meteor/app/api/server/v1/chat.ts
+++ b/apps/meteor/app/api/server/v1/chat.ts
@@ -4,6 +4,7 @@ import { Messages, Users, Rooms, Subscriptions } from '@rocket.chat/models';
import { escapeRegExp } from '@rocket.chat/string-helpers';
import { Message } from '@rocket.chat/core-services';
import type { IMessage } from '@rocket.chat/core-typings';
+import { isChatReportMessageProps } from '@rocket.chat/rest-typings';
import { roomAccessAttributes } from '../../../authorization/server';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
@@ -17,6 +18,7 @@ import { executeSendMessage } from '../../../lib/server/methods/sendMessage';
import { getPaginationItems } from '../helpers/getPaginationItems';
import { canAccessRoomAsync, canAccessRoomIdAsync } from '../../../authorization/server/functions/canAccessRoom';
import { canSendMessageAsync } from '../../../authorization/server/functions/canSendMessage';
+import { reportMessage } from '../../../../server/lib/moderation/reportMessage';
API.v1.addRoute(
'chat.delete',
@@ -359,7 +361,7 @@ API.v1.addRoute(
API.v1.addRoute(
'chat.reportMessage',
- { authRequired: true },
+ { authRequired: true, validateParams: isChatReportMessageProps },
{
async post() {
const { messageId, description } = this.bodyParams;
@@ -371,7 +373,7 @@ API.v1.addRoute(
return API.v1.failure('The required "description" param is missing.');
}
- await Meteor.callAsync('reportMessage', messageId, description);
+ await reportMessage(messageId, description, this.userId);
return API.v1.success();
},
diff --git a/apps/meteor/app/api/server/v1/moderation.ts b/apps/meteor/app/api/server/v1/moderation.ts
new file mode 100644
index 00000000000..01828d98275
--- /dev/null
+++ b/apps/meteor/app/api/server/v1/moderation.ts
@@ -0,0 +1,240 @@
+import {
+ isReportHistoryProps,
+ isArchiveReportProps,
+ isReportInfoParams,
+ isReportMessageHistoryParams,
+ isModerationDeleteMsgHistoryParams,
+ isReportsByMsgIdParams,
+} from '@rocket.chat/rest-typings';
+import { ModerationReports, Users, Messages } from '@rocket.chat/models';
+import type { IModerationReport } from '@rocket.chat/core-typings';
+
+import { API } from '../api';
+import { deleteReportedMessages } from '../../../../server/lib/moderation/deleteReportedMessages';
+import { getPaginationItems } from '../helpers/getPaginationItems';
+
+type ReportMessage = Pick;
+
+API.v1.addRoute(
+ 'moderation.reportsByUsers',
+ {
+ authRequired: true,
+ validateParams: isReportHistoryProps,
+ permissionsRequired: ['view-moderation-console'],
+ },
+ {
+ async get() {
+ const { latest: _latest, oldest: _oldest, selector = '' } = this.queryParams;
+
+ const { count = 20, offset = 0 } = await getPaginationItems(this.queryParams);
+ const { sort } = await this.parseJsonQuery();
+
+ const latest = _latest ? new Date(_latest) : new Date();
+ const oldest = _oldest ? new Date(_oldest) : new Date(0);
+
+ const reports = await ModerationReports.findReportsGroupedByUser(latest, oldest, selector, { offset, count, sort }).toArray();
+
+ if (reports.length === 0) {
+ return API.v1.success({
+ reports,
+ count: 0,
+ offset,
+ total: 0,
+ });
+ }
+
+ const total = await ModerationReports.countReportsInRange(latest, oldest, selector);
+
+ return API.v1.success({
+ reports,
+ count: reports.length,
+ offset,
+ total,
+ });
+ },
+ },
+);
+
+API.v1.addRoute(
+ 'moderation.user.reportedMessages',
+ {
+ authRequired: true,
+ validateParams: isReportMessageHistoryParams,
+ permissionsRequired: ['view-moderation-console'],
+ },
+ {
+ async get() {
+ const { userId, selector = '' } = this.queryParams;
+
+ const { sort } = await this.parseJsonQuery();
+
+ const { count = 50, offset = 0 } = await getPaginationItems(this.queryParams);
+
+ const user = await Users.findOneById(userId, { projection: { _id: 1 } });
+ if (!user) {
+ return API.v1.failure('error-invalid-user');
+ }
+
+ const { cursor, totalCount } = ModerationReports.findReportedMessagesByReportedUserId(userId, selector, { offset, count, sort });
+
+ const [reports, total] = await Promise.all([cursor.toArray(), totalCount]);
+
+ const uniqueMessages: ReportMessage[] = [];
+ const visited = new Set();
+ for (const report of reports) {
+ if (visited.has(report.message._id)) {
+ continue;
+ }
+ visited.add(report.message._id);
+ uniqueMessages.push(report);
+ }
+
+ return API.v1.success({
+ messages: uniqueMessages,
+ count: reports.length,
+ total,
+ offset,
+ });
+ },
+ },
+);
+
+API.v1.addRoute(
+ 'moderation.user.deleteReportedMessages',
+ {
+ authRequired: true,
+ validateParams: isModerationDeleteMsgHistoryParams,
+ permissionsRequired: ['manage-moderation-actions'],
+ },
+ {
+ async post() {
+ // TODO change complicated params
+ const { userId, reason } = this.bodyParams;
+
+ const sanitizedReason = reason?.trim() ? reason : 'No reason provided';
+
+ const { user: moderator } = this;
+
+ const { count = 50, offset = 0 } = await getPaginationItems(this.queryParams);
+
+ const user = await Users.findOneById(userId, { projection: { _id: 1 } });
+ if (!user) {
+ return API.v1.failure('error-invalid-user');
+ }
+
+ const { cursor, totalCount } = ModerationReports.findReportedMessagesByReportedUserId(userId, '', {
+ offset,
+ count,
+ sort: { ts: -1 },
+ });
+
+ const [messages, total] = await Promise.all([cursor.toArray(), totalCount]);
+
+ if (total === 0) {
+ return API.v1.failure('No reported messages found for this user.');
+ }
+
+ await deleteReportedMessages(
+ messages.map((message) => message.message),
+ moderator,
+ );
+
+ await ModerationReports.hideReportsByUserId(userId, this.userId, sanitizedReason, 'DELETE Messages');
+
+ return API.v1.success();
+ },
+ },
+);
+
+API.v1.addRoute(
+ 'moderation.dismissReports',
+ {
+ authRequired: true,
+ validateParams: isArchiveReportProps,
+ permissionsRequired: ['manage-moderation-actions'],
+ },
+ {
+ async post() {
+ // TODO change complicated camelcases to simple verbs/nouns
+ const { userId, msgId, reason, action: actionParam } = this.bodyParams;
+
+ if (userId) {
+ const user = await Users.findOneById(userId, { projection: { _id: 1 } });
+ if (!user) {
+ return API.v1.failure('user-not-found');
+ }
+ }
+
+ if (msgId) {
+ const message = await Messages.findOneById(msgId, { projection: { _id: 1 } });
+ if (!message) {
+ return API.v1.failure('error-message-not-found');
+ }
+ }
+
+ const sanitizedReason: string = reason?.trim() ? reason : 'No reason provided';
+ const action: string = actionParam ?? 'None';
+
+ const { userId: moderatorId } = this;
+
+ if (userId) {
+ await ModerationReports.hideReportsByUserId(userId, moderatorId, sanitizedReason, action);
+ } else {
+ await ModerationReports.hideReportsByMessageId(msgId as string, moderatorId, sanitizedReason, action);
+ }
+
+ return API.v1.success();
+ },
+ },
+);
+
+API.v1.addRoute(
+ 'moderation.reports',
+ {
+ authRequired: true,
+ validateParams: isReportsByMsgIdParams,
+ permissionsRequired: ['view-moderation-console'],
+ },
+ {
+ async get() {
+ const { msgId } = this.queryParams;
+
+ const { count = 50, offset = 0 } = await getPaginationItems(this.queryParams);
+ const { sort } = await this.parseJsonQuery();
+ const { selector = '' } = this.queryParams;
+
+ const { cursor, totalCount } = ModerationReports.findReportsByMessageId(msgId, selector, { count, sort, offset });
+
+ const [reports, total] = await Promise.all([cursor.toArray(), totalCount]);
+
+ return API.v1.success({
+ reports,
+ count: reports.length,
+ offset,
+ total,
+ });
+ },
+ },
+);
+
+API.v1.addRoute(
+ 'moderation.reportInfo',
+ {
+ authRequired: true,
+ permissionsRequired: ['view-moderation-console'],
+ validateParams: isReportInfoParams,
+ },
+ {
+ async get() {
+ const { reportId } = this.queryParams;
+
+ const report = await ModerationReports.findOneById(reportId);
+
+ if (!report) {
+ return API.v1.failure('error-report-not-found');
+ }
+
+ return API.v1.success({ report });
+ },
+ },
+);
diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts
index 61af64eb45c..db3d87d2544 100644
--- a/apps/meteor/app/api/server/v1/users.ts
+++ b/apps/meteor/app/api/server/v1/users.ts
@@ -337,7 +337,10 @@ API.v1.addRoute(
{ authRequired: true, validateParams: isUserSetActiveStatusParamsPOST },
{
async post() {
- if (!(await hasPermissionAsync(this.userId, 'edit-other-user-active-status'))) {
+ if (
+ !(await hasPermissionAsync(this.userId, 'edit-other-user-active-status')) &&
+ !(await hasPermissionAsync(this.userId, 'manage-moderation-actions'))
+ ) {
return API.v1.unauthorized();
}
@@ -585,7 +588,10 @@ API.v1.addRoute(
if (settings.get('Accounts_AllowUserAvatarChange') && user._id === this.userId) {
await Meteor.callAsync('resetAvatar');
- } else if (await hasPermissionAsync(this.userId, 'edit-other-user-avatar')) {
+ } else if (
+ (await hasPermissionAsync(this.userId, 'edit-other-user-avatar')) ||
+ (await hasPermissionAsync(this.userId, 'manage-moderation-actions'))
+ ) {
await Meteor.callAsync('resetAvatar', user._id);
} else {
throw new Meteor.Error('error-not-allowed', 'Reset avatar is not allowed', {
diff --git a/apps/meteor/app/apps/server/bridges/bridges.js b/apps/meteor/app/apps/server/bridges/bridges.js
index 9e31d3f2f51..450bae2e323 100644
--- a/apps/meteor/app/apps/server/bridges/bridges.js
+++ b/apps/meteor/app/apps/server/bridges/bridges.js
@@ -21,6 +21,7 @@ import { AppSchedulerBridge } from './scheduler';
import { AppVideoConferenceBridge } from './videoConferences';
import { AppOAuthAppsBridge } from './oauthApps';
import { AppInternalFederationBridge } from './internalFederation';
+import { AppModerationBridge } from './moderation';
export class RealAppBridges extends AppBridges {
constructor(orch) {
@@ -47,6 +48,7 @@ export class RealAppBridges extends AppBridges {
this._videoConfBridge = new AppVideoConferenceBridge(orch);
this._oAuthBridge = new AppOAuthAppsBridge(orch);
this._internalFedBridge = new AppInternalFederationBridge(orch);
+ this._moderationBridge = new AppModerationBridge(orch);
}
getCommandBridge() {
@@ -132,4 +134,8 @@ export class RealAppBridges extends AppBridges {
getInternalFederationBridge() {
return this._internalFedBridge;
}
+
+ getModerationBridge() {
+ return this._moderationBridge;
+ }
}
diff --git a/apps/meteor/app/apps/server/bridges/messages.ts b/apps/meteor/app/apps/server/bridges/messages.ts
index 28797d262e3..24dfb2a7b0a 100644
--- a/apps/meteor/app/apps/server/bridges/messages.ts
+++ b/apps/meteor/app/apps/server/bridges/messages.ts
@@ -10,6 +10,7 @@ import { updateMessage } from '../../../lib/server/functions/updateMessage';
import { executeSendMessage } from '../../../lib/server/methods/sendMessage';
import notifications from '../../../notifications/server/lib/Notifications';
import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator';
+import { deleteMessage } from '../../../lib/server';
export class AppMessageBridge extends MessageBridge {
// eslint-disable-next-line no-empty-function
@@ -54,6 +55,19 @@ export class AppMessageBridge extends MessageBridge {
await updateMessage(msg, editor);
}
+ protected async delete(message: IMessage, user: IUser, appId: string): Promise {
+ this.orch.debugLog(`The App ${appId} is deleting a message.`);
+
+ if (!message.id) {
+ throw new Error('Invalid message id');
+ }
+
+ const convertedMsg = await this.orch.getConverters()?.get('messages').convertAppMessage(message);
+ const convertedUser = await this.orch.getConverters()?.get('users').convertById(user.id);
+
+ await deleteMessage(convertedMsg, convertedUser);
+ }
+
protected async notifyUser(user: IUser, message: IMessage, appId: string): Promise {
this.orch.debugLog(`The App ${appId} is notifying a user.`);
diff --git a/apps/meteor/app/apps/server/bridges/moderation.ts b/apps/meteor/app/apps/server/bridges/moderation.ts
new file mode 100644
index 00000000000..68d2260c571
--- /dev/null
+++ b/apps/meteor/app/apps/server/bridges/moderation.ts
@@ -0,0 +1,46 @@
+import { ModerationBridge } from '@rocket.chat/apps-engine/server/bridges/ModerationBridge';
+import { ModerationReports } from '@rocket.chat/models';
+import type { IMessage } from '@rocket.chat/apps-engine/definition/messages';
+import type { IUser } from '@rocket.chat/apps-engine/definition/users';
+
+import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator';
+import { reportMessage } from '../../../../server/lib/moderation/reportMessage';
+
+export class AppModerationBridge extends ModerationBridge {
+ constructor(private readonly orch: AppServerOrchestrator) {
+ super();
+ }
+
+ protected async report(messageId: IMessage['id'], description: string, userId: string, appId: string): Promise {
+ this.orch.debugLog(`The App ${appId} is creating a new report.`);
+
+ if (!messageId) {
+ throw new Error('Invalid message id');
+ }
+
+ if (!description) {
+ throw new Error('Invalid description');
+ }
+
+ await reportMessage(messageId, description, userId || 'rocket.cat');
+ }
+
+ protected async dismissReportsByMessageId(messageId: IMessage['id'], reason: string, action: string, appId: string): Promise {
+ this.orch.debugLog(`The App ${appId} is dismissing reports by message id.`);
+
+ if (!messageId) {
+ throw new Error('Invalid message id');
+ }
+
+ await ModerationReports.hideReportsByMessageId(messageId, appId, reason, action);
+ }
+
+ protected async dismissReportsByUserId(userId: IUser['id'], reason: string, action: string, appId: string): Promise {
+ this.orch.debugLog(`The App ${appId} is dismissing reports by user id.`);
+
+ if (!userId) {
+ throw new Error('Invalid user id');
+ }
+ await ModerationReports.hideReportsByUserId(userId, appId, reason, action);
+ }
+}
diff --git a/apps/meteor/app/apps/server/bridges/users.ts b/apps/meteor/app/apps/server/bridges/users.ts
index 14861588d42..27bac79b103 100644
--- a/apps/meteor/app/apps/server/bridges/users.ts
+++ b/apps/meteor/app/apps/server/bridges/users.ts
@@ -8,6 +8,7 @@ import type { UserStatus } from '@rocket.chat/core-typings';
import { setUserAvatar, deleteUser, getUserCreatedByApp } from '../../../lib/server/functions';
import { checkUsernameAvailability } from '../../../lib/server/functions/checkUsernameAvailability';
import type { AppServerOrchestrator } from '../../../../ee/server/apps/orchestrator';
+import { setUserActiveStatus } from '../../../lib/server/functions/setUserActiveStatus';
export class AppUserBridge extends UserBridge {
// eslint-disable-next-line no-empty-function
@@ -131,6 +132,19 @@ export class AppUserBridge extends UserBridge {
return true;
}
+ protected async deactivate(userId: IUser['id'], confirmRelinquish: boolean, appId: string): Promise {
+ this.orch.debugLog(`The App ${appId} is deactivating a user.`);
+
+ if (!userId) {
+ throw new Error('Invalid user id');
+ }
+ const convertedUser = await this.orch.getConverters()?.get('users').convertById(userId);
+
+ await setUserActiveStatus(convertedUser.id, false, confirmRelinquish);
+
+ return true;
+ }
+
protected async getActiveUserCount(): Promise {
return Users.getActiveLocalUserCount();
}
diff --git a/apps/meteor/app/authorization/server/functions/upsertPermissions.ts b/apps/meteor/app/authorization/server/functions/upsertPermissions.ts
index e08034ce1a8..2236b113433 100644
--- a/apps/meteor/app/authorization/server/functions/upsertPermissions.ts
+++ b/apps/meteor/app/authorization/server/functions/upsertPermissions.ts
@@ -222,6 +222,8 @@ export const upsertPermissions = async (): Promise => {
{ _id: 'view-import-operations', roles: ['admin'] },
{ _id: 'clear-oembed-cache', roles: ['admin'] },
{ _id: 'videoconf-ring-users', roles: ['admin', 'owner', 'moderator', 'user'] },
+ { _id: 'view-moderation-console', roles: ['admin'] },
+ { _id: 'manage-moderation-actions', roles: ['admin'] },
{ _id: 'bypass-time-limit-edit-and-delete', roles: ['bot', 'app'] },
];
diff --git a/apps/meteor/app/lib/server/methods/deleteMessage.ts b/apps/meteor/app/lib/server/methods/deleteMessage.ts
index 72101603d96..92881511702 100644
--- a/apps/meteor/app/lib/server/methods/deleteMessage.ts
+++ b/apps/meteor/app/lib/server/methods/deleteMessage.ts
@@ -6,6 +6,7 @@ import type { ServerMethods } from '@rocket.chat/ui-contexts';
import { canDeleteMessageAsync } from '../../../authorization/server/functions/canDeleteMessage';
import { deleteMessage } from '../functions';
+import { methodDeprecationLogger } from '../lib/deprecationWarningLogger';
declare module '@rocket.chat/ui-contexts' {
// eslint-disable-next-line @typescript-eslint/naming-convention
@@ -16,6 +17,8 @@ declare module '@rocket.chat/ui-contexts' {
Meteor.methods({
async deleteMessage(message) {
+ methodDeprecationLogger.warn('deleteMessage method is deprecated, and will be removed in future versions');
+
check(
message,
Match.ObjectIncluding({
diff --git a/apps/meteor/client/components/AdministrationList/AdministrationList.tsx b/apps/meteor/client/components/AdministrationList/AdministrationList.tsx
index 8270738b0e1..1de4319ed62 100644
--- a/apps/meteor/client/components/AdministrationList/AdministrationList.tsx
+++ b/apps/meteor/client/components/AdministrationList/AdministrationList.tsx
@@ -31,6 +31,7 @@ const ADMIN_PERMISSIONS = [
'manage-own-outgoing-integrations',
'manage-own-incoming-integrations',
'view-engagement-dashboard',
+ 'view-moderation-console',
];
const AdministrationList = ({ accountBoxItems, onDismiss }: AdministrationListProps): ReactElement => {
diff --git a/apps/meteor/client/lib/createSidebarItems.ts b/apps/meteor/client/lib/createSidebarItems.ts
index 89d8dac42a9..22af68db347 100644
--- a/apps/meteor/client/lib/createSidebarItems.ts
+++ b/apps/meteor/client/lib/createSidebarItems.ts
@@ -5,7 +5,7 @@ type Item = {
i18nLabel: string;
href?: string;
icon?: IconProps['name'];
- tag?: 'Alpha';
+ tag?: 'Alpha' | 'Beta';
permissionGranted?: boolean | (() => boolean);
pathSection?: string;
pathGroup?: string;
diff --git a/apps/meteor/client/views/admin/moderation/MessageContextFooter.tsx b/apps/meteor/client/views/admin/moderation/MessageContextFooter.tsx
new file mode 100644
index 00000000000..8b2e31763fc
--- /dev/null
+++ b/apps/meteor/client/views/admin/moderation/MessageContextFooter.tsx
@@ -0,0 +1,35 @@
+import { Button, Icon, Menu, Option, ButtonGroup } from '@rocket.chat/fuselage';
+import { useTranslation } from '@rocket.chat/ui-contexts';
+import React from 'react';
+import type { FC } from 'react';
+
+import useDeactivateUserAction from './hooks/useDeactivateUserAction';
+import useDeleteMessagesAction from './hooks/useDeleteMessagesAction';
+import useDismissUserAction from './hooks/useDismissUserAction';
+import useResetAvatarAction from './hooks/useResetAvatarAction';
+
+const MessageContextFooter: FC<{ userId: string }> = ({ userId }) => {
+ const t = useTranslation();
+ const { action } = useDeleteMessagesAction(userId);
+
+ return (
+
+
+
+
+ );
+};
+
+export default MessageContextFooter;
diff --git a/apps/meteor/client/views/admin/moderation/MessageReportInfo.tsx b/apps/meteor/client/views/admin/moderation/MessageReportInfo.tsx
new file mode 100644
index 00000000000..8f956b710eb
--- /dev/null
+++ b/apps/meteor/client/views/admin/moderation/MessageReportInfo.tsx
@@ -0,0 +1,102 @@
+import { Box, Message, MessageName, MessageUsername } from '@rocket.chat/fuselage';
+import { useEndpoint, useRoute, useSetting, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts';
+import { useQuery } from '@tanstack/react-query';
+import React from 'react';
+
+import { getUserDisplayName } from '../../../../lib/getUserDisplayName';
+import VerticalBar from '../../../components/VerticalBar';
+import UserAvatar from '../../../components/avatar/UserAvatar';
+import { useFormatDate } from '../../../hooks/useFormatDate';
+import { useFormatDateAndTime } from '../../../hooks/useFormatDateAndTime';
+import { useFormatTime } from '../../../hooks/useFormatTime';
+
+const MessageReportInfo = ({ msgId }: { msgId: string }): JSX.Element => {
+ const t = useTranslation();
+ const dispatchToastMessage = useToastMessageDispatch();
+ const getReportsByMessage = useEndpoint('GET', `/v1/moderation.reports`);
+ const moderationRoute = useRoute('moderation-console');
+
+ const formatDateAndTime = useFormatDateAndTime();
+ const formatTime = useFormatTime();
+ const formatDate = useFormatDate();
+ const useRealName = Boolean(useSetting('UI_Use_Real_Name'));
+
+ const {
+ data: reportsByMessage,
+ isLoading: isLoadingReportsByMessage,
+ isSuccess: isSuccessReportsByMessage,
+ isError: isErrorReportsByMessage,
+ } = useQuery(
+ ['moderation.reports', { msgId }],
+ async () => {
+ const reports = await getReportsByMessage({ msgId });
+ return reports;
+ },
+ {
+ onError: (error) => {
+ dispatchToastMessage({ type: 'error', message: error });
+ },
+ },
+ );
+
+ if (isLoadingReportsByMessage) {
+ return (
+
+ {t('Loading')}
+
+ );
+ }
+
+ if (isErrorReportsByMessage) {
+ return (
+
+ {t('Error')}
+
+ );
+ }
+
+ const { reports } = reportsByMessage;
+
+ return (
+ <>
+
+ window.history.go(-1)} />
+ {t('Report')}
+ moderationRoute.push({})} />
+
+ {isSuccessReportsByMessage && reportsByMessage?.reports && (
+
+ {reports.map((report) => (
+
+ {formatDate(report.ts)}
+
+
+
+
+
+
+
+ {report.reportedBy
+ ? getUserDisplayName(report.reportedBy.name, report.reportedBy.username, useRealName)
+ : 'Rocket.Cat'}
+
+ <>
+ {useRealName && (
+ @{report.reportedBy ? report.reportedBy.username : 'rocket.cat'}
+ )}
+ >
+
+ {formatTime(report.ts)}
+
+ {report.description}
+
+
+
+ ))}
+
+ )}
+ >
+ );
+};
+
+export default MessageReportInfo;
diff --git a/apps/meteor/client/views/admin/moderation/ModerationConsoleActions.tsx b/apps/meteor/client/views/admin/moderation/ModerationConsoleActions.tsx
new file mode 100644
index 00000000000..1dd063cea36
--- /dev/null
+++ b/apps/meteor/client/views/admin/moderation/ModerationConsoleActions.tsx
@@ -0,0 +1,37 @@
+import { Menu, Option } from '@rocket.chat/fuselage';
+import { useTranslation } from '@rocket.chat/ui-contexts';
+import React from 'react';
+
+import type { ModerationConsoleRowProps } from './ModerationConsoleTableRow';
+import useDeactivateUserAction from './hooks/useDeactivateUserAction';
+import useDeleteMessagesAction from './hooks/useDeleteMessagesAction';
+import useDismissUserAction from './hooks/useDismissUserAction';
+import useResetAvatarAction from './hooks/useResetAvatarAction';
+
+const ModerationConsoleActions = ({ report, onClick }: Omit): JSX.Element => {
+ const t = useTranslation();
+ const { userId: uid } = report;
+
+ return (
+ <>
+