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 ( + + + + ( +