Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
a8ee3f7
[NEW] New API endpoints for Moderation feature (#27798)
Dnouv Mar 20, 2023
be5babb
Merge 'develop' info feat/moderation-dashboard
debdutdeb Mar 20, 2023
0e15772
[NEW] New API endpoints for Moderation feature (#27798)
debdutdeb Mar 28, 2023
7ed6011
merge develop into moderation dashboard
Dnouv Apr 6, 2023
bdc3f40
fix merge conflict for initial data
Dnouv Apr 6, 2023
e08a6ba
Merge branch 'develop' into feat/moderation-dashboard
Dnouv Apr 12, 2023
347af7f
fix merge conflicts including fix for previous merge
Dnouv Apr 12, 2023
3b93e9e
Merge remote-tracking branch 'origin/develop' into feat/moderation-da…
hugocostadev Apr 12, 2023
106050a
fix type errors
Dnouv Apr 12, 2023
b2bb017
fix: removing duplicated files and code to prevent error
hugocostadev Apr 12, 2023
42bd198
Merge remote-tracking branch 'refs/remotes/origin/feat/moderation-das…
hugocostadev Apr 12, 2023
f847a2f
fix type for audit.ts
Dnouv Apr 13, 2023
2d20e41
remove another leftover js file
Dnouv Apr 13, 2023
2cf02c7
merge develop
Dnouv Apr 13, 2023
88bf1f4
fix moderation type in base feature branch
Dnouv Apr 13, 2023
30fc3b5
fix lint error in moderation.ts
Dnouv Apr 13, 2023
eed6d86
Merge remote-tracking branch 'origin/develop' into feat/moderation-da…
hugocostadev Apr 13, 2023
0c9d9b2
Merge branch 'develop' into feat/moderation-dashboard
Dnouv Apr 17, 2023
3cf851c
sync develop
Dnouv Apr 17, 2023
808af61
review
debdutdeb Apr 17, 2023
a067e82
review [wip]
debdutdeb Apr 17, 2023
c8b7db4
fix review changes errors
Dnouv Apr 17, 2023
d4b8497
re-fix review errors
Dnouv Apr 17, 2023
9c73bfe
fix review errors in api test
Dnouv Apr 17, 2023
742cffa
add ajv checks
Dnouv Apr 17, 2023
656da79
fix ajv import error
Dnouv Apr 18, 2023
1184673
Merge branch 'develop' into feat/moderation-dashboard
Dnouv Apr 18, 2023
a6af770
feat: Moderation Console (#27961)
Dnouv Apr 18, 2023
e766ec2
refactor: convert migration model to raw (#28945)
Apr 18, 2023
ae520dd
remove mongo collections from migrations (#28941)
KevLehman Apr 18, 2023
e480563
refactor: Convert trash to raw (#28939)
KevLehman Apr 18, 2023
c9af73f
refactor: internalize user-presence package (#28876)
Apr 18, 2023
547845b
Merge branch 'release-6.2.0' into develop
sampaiodiego Apr 19, 2023
3d3bf4d
Bump version to 6.3.0-develop
sampaiodiego Apr 19, 2023
fb463e2
Merge branch 'release-6.2.0' into feat/moderation-dashboard
hugocostadev Apr 19, 2023
faa5300
wip(review): endpoint typings
debdutdeb Apr 19, 2023
df87d78
Merge branch 'develop' into feat/moderation-dashboard
debdutdeb Apr 19, 2023
0bfd70b
wip(review): i need sleep
debdutdeb Apr 19, 2023
6713e3a
wip(review): i actually fell asleep
debdutdeb Apr 20, 2023
798948d
review: i think i can go to sleep now, good night
debdutdeb Apr 20, 2023
b4c1464
chore: unnecessary comment
debdutdeb Apr 20, 2023
28a1b6a
Merge branch 'release-6.2.0' into feat/moderation-dashboard
debdutdeb Apr 20, 2023
167a942
rename api endpoints
Dnouv Apr 20, 2023
a93dd8b
fix owner4 and rephrase deprecation warning
Dnouv Apr 20, 2023
85e5911
actionTaken <-> action & reasonForHiding <-> reason
Dnouv Apr 20, 2023
020c435
fix empty string issue
Dnouv Apr 20, 2023
6e74b71
fix lint
Dnouv Apr 20, 2023
3ceb6c3
add validation for chat.reportmessage & rename post date
Dnouv Apr 20, 2023
83a6dc5
Revert "Merge branch 'develop' into feat/moderation-dashboard"
debdutdeb Apr 20, 2023
76c0f50
smalls code changes
ggazzo Apr 25, 2023
7ed2827
clone history.md from release branch
Dnouv Apr 25, 2023
6515741
remove reload.current use queryclient.invalidatequeries & use useRoute
Dnouv Apr 25, 2023
797814c
remove usememo & rename medeiaquery variable
Dnouv Apr 25, 2023
e5429ca
remove scoped roles
Dnouv Apr 28, 2023
5f656d9
Merge branch 'release-6.2.0' into feat/moderation-dashboard
Dnouv May 1, 2023
7341c59
Merge branch 'release-6.2.0' into feat/moderation-dashboard
debdutdeb May 4, 2023
bc9f229
remove redundant avatarEtag and active from reports
Dnouv May 5, 2023
a493a59
undo files
ggazzo May 5, 2023
cfa4f31
feat: App bridge for Moderation Features (#28518)
Dnouv May 5, 2023
f610565
Merge branch 'release-6.2.0' into feat/moderation-dashboard
debdutdeb May 5, 2023
3deb547
Merge branch 'release-6.2.0' into feat/moderation-dashboard
d-gubert May 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/meteor/app/api/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
6 changes: 4 additions & 2 deletions apps/meteor/app/api/server/v1/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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',
Expand Down Expand Up @@ -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;
Expand All @@ -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();
},
Expand Down
240 changes: 240 additions & 0 deletions apps/meteor/app/api/server/v1/moderation.ts
Original file line number Diff line number Diff line change
@@ -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<IModerationReport, '_id' | 'message' | 'ts' | 'room'>;

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<string>();
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 });
},
},
);
10 changes: 8 additions & 2 deletions apps/meteor/app/api/server/v1/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}

Expand Down Expand Up @@ -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', {
Expand Down
6 changes: 6 additions & 0 deletions apps/meteor/app/apps/server/bridges/bridges.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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() {
Expand Down Expand Up @@ -132,4 +134,8 @@ export class RealAppBridges extends AppBridges {
getInternalFederationBridge() {
return this._internalFedBridge;
}

getModerationBridge() {
return this._moderationBridge;
}
}
14 changes: 14 additions & 0 deletions apps/meteor/app/apps/server/bridges/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -54,6 +55,19 @@ export class AppMessageBridge extends MessageBridge {
await updateMessage(msg, editor);
}

protected async delete(message: IMessage, user: IUser, appId: string): Promise<void> {
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<void> {
this.orch.debugLog(`The App ${appId} is notifying a user.`);

Expand Down
Loading