Skip to content

Commit

Permalink
fix: not able do see reports about deleted users (#29832)
Browse files Browse the repository at this point in the history
Co-authored-by: デヴぁんす <sdevanshu90@yahoo.com>
  • Loading branch information
ggazzo and Dnouv committed Jul 29, 2023
1 parent 5b5b65f commit 8f5e05c
Show file tree
Hide file tree
Showing 15 changed files with 108 additions and 57 deletions.
5 changes: 5 additions & 0 deletions .changeset/long-ants-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@rocket.chat/meteor': patch
---

Introduces a fix to let the Admin view reported messages of the deleted users on the Moderation Console
15 changes: 5 additions & 10 deletions apps/meteor/app/api/server/v1/moderation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
isReportsByMsgIdParams,
} from '@rocket.chat/rest-typings';
import { ModerationReports, Users } from '@rocket.chat/models';
import type { IModerationReport } from '@rocket.chat/core-typings';
import type { IModerationReport, IUser } from '@rocket.chat/core-typings';
import { escapeRegExp } from '@rocket.chat/string-helpers';

import { API } from '../api';
Expand Down Expand Up @@ -73,10 +73,9 @@ API.v1.addRoute(

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 user = await Users.findOneById<Pick<IUser, '_id' | 'username' | 'name'>>(userId, {
projection: { _id: 1, username: 1, name: 1 },
});

const escapedSelector = escapeRegExp(selector);

Expand All @@ -99,6 +98,7 @@ API.v1.addRoute(
}

return API.v1.success({
user,
messages: uniqueMessages,
count: reports.length,
total,
Expand Down Expand Up @@ -126,11 +126,6 @@ API.v1.addRoute(

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,
Expand Down
12 changes: 11 additions & 1 deletion apps/meteor/app/lib/server/functions/deleteUser.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Meteor } from 'meteor/meteor';
import type { IUser } from '@rocket.chat/core-typings';
import {
Integrations,
FederationServers,
Expand All @@ -9,6 +10,7 @@ import {
Subscriptions,
Users,
LivechatUnitMonitors,
ModerationReports,
} from '@rocket.chat/models';
import { api } from '@rocket.chat/core-services';

Expand All @@ -20,7 +22,7 @@ import { getSubscribedRoomsForUserWithDetails, shouldRemoveOrChangeOwner } from
import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms';
import { i18n } from '../../../../server/lib/i18n';

export async function deleteUser(userId: string, confirmRelinquish = false): Promise<void> {
export async function deleteUser(userId: string, confirmRelinquish = false, deletedBy?: IUser['_id']): Promise<void> {
const user = await Users.findOneById(userId, {
projection: { username: 1, avatarOrigin: 1, roles: 1, federated: 1 },
});
Expand Down Expand Up @@ -54,6 +56,14 @@ export async function deleteUser(userId: string, confirmRelinquish = false): Pro
}

await Messages.removeByUserId(userId);

await ModerationReports.hideReportsByUserId(
userId,
deletedBy || userId,
deletedBy === userId ? 'user deleted own account' : 'user account deleted',
'DELETE_USER',
);

break;
case 'Unlink':
const rocketCat = await Users.findOneById('rocket.cat');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import useDeleteMessagesAction from './hooks/useDeleteMessagesAction';
import useDismissUserAction from './hooks/useDismissUserAction';
import useResetAvatarAction from './hooks/useResetAvatarAction';

const MessageContextFooter: FC<{ userId: string }> = ({ userId }) => {
const MessageContextFooter: FC<{ userId: string; deleted: boolean }> = ({ userId, deleted }) => {
const t = useTranslation();
const { action } = useDeleteMessagesAction(userId);

Expand All @@ -21,8 +21,8 @@ const MessageContextFooter: FC<{ userId: string }> = ({ userId }) => {
<Menu
options={{
approve: useDismissUserAction(userId),
deactivate: useDeactivateUserAction(userId),
resetAvatar: useResetAvatarAction(userId),
deactivate: { ...useDeactivateUserAction(userId), ...(deleted && { disabled: true }) },
resetAvatar: { ...useResetAvatarAction(userId), ...(deleted && { disabled: true }) },
}}
renderItem={({ label: { label, icon }, ...props }): JSX.Element => (
<Option aria-label={label} label={label} icon={icon} {...props} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import useResetAvatarAction from './hooks/useResetAvatarAction';

const ModerationConsoleActions = ({ report, onClick }: Omit<ModerationConsoleRowProps, 'isDesktopOrLarger'>): JSX.Element => {
const t = useTranslation();
const { userId: uid } = report;
const { userId: uid, isUserDeleted } = report;

return (
<>
Expand All @@ -25,8 +25,8 @@ const ModerationConsoleActions = ({ report, onClick }: Omit<ModerationConsoleRow
},
approve: useDismissUserAction(uid),
deleteAll: useDeleteMessagesAction(uid),
deactiveUser: useDeactivateUserAction(uid),
resetAvatar: useResetAvatarAction(uid),
deactiveUser: { ...useDeactivateUserAction(uid), ...(isUserDeleted && { disabled: true }) },
resetAvatar: { ...useResetAvatarAction(uid), ...(isUserDeleted && { disabled: true }) },
}}
renderItem={({ label: { label, icon }, ...props }) => <Option label={label} icon={icon} {...props} />}
/>
Expand Down
55 changes: 32 additions & 23 deletions apps/meteor/client/views/admin/moderation/UserMessages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const UserMessages = ({ userId, onRedirect }: { userId: string; onRedirect: (mid
const getUserMessages = useEndpoint('GET', '/v1/moderation.user.reportedMessages');

const {
data: userMessages,
data: report,
refetch: reloadUserMessages,
isLoading: isLoadingUserMessages,
isSuccess: isSuccessUserMessages,
Expand Down Expand Up @@ -47,19 +47,15 @@ const UserMessages = ({ userId, onRedirect }: { userId: string; onRedirect: (mid
reloadUserMessages();
});

const username = useMemo(() => {
if (userMessages?.messages[0]?.message?.u?.username) {
return userMessages?.messages[0].message.u.username;
}
return '';
}, [userMessages?.messages]);

const name = useMemo(() => {
if (userMessages?.messages[0]?.message?.u?.name) {
return userMessages?.messages[0].message.u.name;
}
return '';
}, [userMessages?.messages]);
const { username, name } = useMemo(() => {
return (
report?.user ??
report?.messages?.[0]?.message?.u ?? {
username: t('Deleted_user'),
name: t('Deleted_user'),
}
);
}, [report?.messages, report?.user, t]);

const displayName =
useUserDisplayName({
Expand All @@ -74,29 +70,42 @@ const UserMessages = ({ userId, onRedirect }: { userId: string; onRedirect: (mid
<ContextualbarClose onClick={() => moderationRoute.push({})} />
</ContextualbarHeader>
<Box display='flex' flexDirection='column' width='full' height='full' overflowY='auto' overflowX='hidden'>
{isSuccessUserMessages && userMessages.messages.length > 0 && (
<Callout margin={15} title={t('Moderation_Duplicate_messages')} type='warning' icon='warning'>
{t('Moderation_Duplicate_messages_warning')}
</Callout>
)}{' '}
{isLoadingUserMessages && <Message>{t('Loading')}</Message>}

{isSuccessUserMessages && (
<Box padding='x15'>
{report.messages.length > 0 && (
<Callout title={t('Moderation_Duplicate_messages')} type='warning' icon='warning'>
{t('Moderation_Duplicate_messages_warning')}
</Callout>
)}

{!report.user && (
<Callout mbs='x8' type='warning' icon='warning'>
{t('Moderation_User_deleted_warning')}
</Callout>
)}
</Box>
)}

{isSuccessUserMessages &&
userMessages.messages.length > 0 &&
userMessages.messages.map((message) => (
report.messages.length > 0 &&
report.messages.map((message) => (
<Box key={message._id}>
<ContextMessage
message={message.message}
room={message.room}
handleClick={handleClick}
onRedirect={onRedirect}
onChange={handleChange}
deleted={!report.user}
/>
</Box>
))}
{isSuccessUserMessages && userMessages.messages.length === 0 && <GenericNoResults />}
{isSuccessUserMessages && report.messages.length === 0 && <GenericNoResults />}
</Box>
<ContextualbarFooter display='flex'>
{isSuccessUserMessages && userMessages.messages.length > 0 && <MessageContextFooter userId={userId} />}
{isSuccessUserMessages && report.messages.length > 0 && <MessageContextFooter userId={userId} deleted={!report.user} />}
</ContextualbarFooter>
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ import useDeleteMessage from '../hooks/useDeleteMessage';
const ContextMessage = ({
message,
room,
deleted,
handleClick,
onRedirect,
onChange,
}: {
message: any;
room: IModerationReport['room'];
deleted: boolean;
handleClick: (id: IMessage['_id']) => void;
onRedirect: (id: IMessage['_id']) => void;
onChange: () => void;
Expand Down Expand Up @@ -79,7 +81,7 @@ const ContextMessage = ({
<Message.Toolbox>
<MessageToolboxItem icon='document-eye' title={t('Moderation_View_reports')} onClick={() => handleClick(message._id)} />
<MessageToolboxItem icon='arrow-forward' title={t('Moderation_Go_to_message')} onClick={() => onRedirect(message._id)} />
<MessageToolboxItem icon='trash' title={t('Moderation_Delete_message')} onClick={() => deleteMessage()} />
<MessageToolboxItem disabled={deleted} icon='trash' title={t('Moderation_Delete_message')} onClick={() => deleteMessage()} />
</Message.Toolbox>
</MessageToolboxWrapper>
</Message>
Expand Down
2 changes: 2 additions & 0 deletions apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -1549,6 +1549,7 @@
"delete-user": "Delete User",
"delete-user_description": "Permission to delete users",
"Deleted": "Deleted!",
"Deleted_user": "Deleted user",
"Deleted__roomName__": "<strong>deleted</strong> #{{roomName}}",
"Deleted__roomName__room": "deleted #{{roomName}}",
"Department": "Department",
Expand Down Expand Up @@ -3519,6 +3520,7 @@
"Moderation_Delete_this_message": "Delete this message",
"Moderation_Message_context_header": "Message(s) from {{displayName}}",
"Moderation_Action_View_reports": "View reported messages",
"Moderation_User_deleted_warning": "The user who sent the message(s) no longer exists or has been removed.",
"Monday": "Monday",
"Mongo_storageEngine": "Mongo Storage Engine",
"Mongo_version": "Mongo Version",
Expand Down
9 changes: 8 additions & 1 deletion apps/meteor/server/lib/moderation/deleteReportedMessages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ export async function deleteReportedMessages(messages: IMessage[], user: IUser):
}
if (keepHistory) {
if (showDeletedStatus) {
await Promise.all(messageIds.map((id) => Messages.cloneAndSaveAsHistoryById(id, user as any)));
const cursor = Messages.find({ _id: { $in: messageIds } });

for await (const doc of cursor) {
await Messages.cloneAndSaveAsHistoryByRecord(
doc,
user as Required<Pick<IUser, '_id' | 'name'>> & { username: NonNullable<IUser['username']> },
);
}
} else {
await Messages.setHiddenByIds(messageIds, true);
}
Expand Down
2 changes: 1 addition & 1 deletion apps/meteor/server/methods/deleteUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ Meteor.methods<ServerMethods>({
});
}

await deleteUser(userId, confirmRelinquish);
await deleteUser(userId, confirmRelinquish, uid);

await callbacks.run('afterDeleteUser', user);

Expand Down
29 changes: 16 additions & 13 deletions apps/meteor/server/models/raw/Messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1006,25 +1006,28 @@ export class MessagesRaw extends BaseRaw<IMessage> implements IMessagesModel {
return this.findOne(query, options);
}

async cloneAndSaveAsHistoryByRecord(record: IMessage, user: IMessage['u']): Promise<InsertOneResult<IMessage>> {
const { _id: _, ...nRecord } = record;
return this.insertOne({
...nRecord,
_hidden: true,
// @ts-expect-error - mongo allows it, but types don't :(
parent: record._id,
editedAt: new Date(),
editedBy: {
_id: user._id,
username: user.username,
},
});
}

async cloneAndSaveAsHistoryById(_id: string, user: IMessage['u']): Promise<InsertOneResult<IMessage>> {
const record = await this.findOneById(_id);
if (!record) {
throw new Error('Record not found');
}

record._hidden = true;
// @ts-expect-error - :)
record.parent = record._id;
// @ts-expect-error - :)
record.editedAt = new Date();
// @ts-expect-error - :)
record.editedBy = {
_id: user._id,
username: user.username,
};

const { _id: ignoreId, ...nRecord } = record;
return this.insertOne(nRecord);
return this.cloneAndSaveAsHistoryByRecord(record, user);
}

// UPDATE
Expand Down
15 changes: 15 additions & 0 deletions apps/meteor/server/models/raw/ModerationReports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,20 @@ export class ModerationReportsRaw extends BaseRaw<IModerationReport> implements
{
$limit: count,
},
{
$lookup: {
from: 'users',
localField: '_id.user',
foreignField: '_id',
as: 'user',
},
},
{
$unwind: {
path: '$user',
preserveNullAndEmptyArrays: true,
},
},
{
// TODO: maybe clean up the projection, i.e. exclude things we don't need
$project: {
Expand All @@ -85,6 +99,7 @@ export class ModerationReportsRaw extends BaseRaw<IModerationReport> implements
username: '$reports.message.u.username',
name: '$reports.message.u.name',
userId: '$reports.message.u._id',
isUserDeleted: { $cond: ['$user', false, true] },
count: 1,
rooms: 1,
},
Expand Down
1 change: 1 addition & 0 deletions packages/core-typings/src/IModerationReport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ export interface IModerationAudit {
ts: IModerationReport['ts'];
rooms: IModerationReport['room'][];
count: number;
isUserDeleted: boolean;
}
1 change: 1 addition & 0 deletions packages/model-typings/src/models/IMessagesModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export interface IMessagesModel extends IBaseModel<IMessage> {
findOneBySlackTs(slackTs: Date): Promise<IMessage | null>;

cloneAndSaveAsHistoryById(_id: string, user: IMessage['u']): Promise<InsertOneResult<IMessage>>;
cloneAndSaveAsHistoryByRecord(record: IMessage, user: IMessage['u']): Promise<InsertOneResult<IMessage>>;

setAsDeletedByIdAndUser(_id: string, user: IMessage['u']): Promise<UpdateResult>;
setAsDeletedByIdsAndUser(_ids: string[], user: IMessage['u']): Promise<Document | UpdateResult>;
Expand Down
3 changes: 2 additions & 1 deletion packages/rest-typings/src/v1/moderation/moderation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IModerationReport, IModerationAudit } from '@rocket.chat/core-typings';
import type { IModerationReport, IModerationAudit, IUser } from '@rocket.chat/core-typings';

import type { PaginatedResult } from '../../helpers/PaginatedResult';
import type { ArchiveReportPropsPOST } from './ArchiveReportProps';
Expand All @@ -20,6 +20,7 @@ export type ModerationEndpoints = {
};
'/v1/moderation.user.reportedMessages': {
GET: (params: ReportMessageHistoryParamsGET) => PaginatedResult<{
user: Pick<IUser, 'username' | 'name' | '_id'> | null;
messages: Pick<IModerationReport, 'message' | 'ts' | 'room' | '_id'>[];
}>;
};
Expand Down

0 comments on commit 8f5e05c

Please sign in to comment.