snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
This commit is contained in:
654
apps/meteor/server/api/v1/moderation.ts
Normal file
654
apps/meteor/server/api/v1/moderation.ts
Normal file
@@ -0,0 +1,654 @@
|
||||
import type { IModerationAudit, IModerationReport, IUser, IUserEmail, UserReport } from '@rocket.chat/core-typings';
|
||||
import { ModerationReports, Users } from '@rocket.chat/models';
|
||||
import {
|
||||
ajv,
|
||||
isReportHistoryProps,
|
||||
isArchiveReportProps,
|
||||
isReportInfoParams,
|
||||
isGetUserReportsParams,
|
||||
isModerationReportUserPost,
|
||||
isModerationDeleteMsgHistoryParams,
|
||||
isReportsByMsgIdParams,
|
||||
validateUnauthorizedErrorResponse,
|
||||
validateForbiddenErrorResponse,
|
||||
validateBadRequestErrorResponse,
|
||||
} from '@rocket.chat/rest-typings';
|
||||
import { escapeRegExp } from '@rocket.chat/tools';
|
||||
|
||||
import { deleteReportedMessages } from '../../lib/moderation/deleteReportedMessages';
|
||||
import { API } from '../api';
|
||||
import { getPaginationItems } from '../lib/getPaginationItems';
|
||||
|
||||
type ReportMessage = Pick<IModerationReport, '_id' | 'message' | 'ts' | 'room'>;
|
||||
|
||||
// TODO: IModerationAudit defines `ts` as `Date | string` which generates a oneOf in JSON Schema.
|
||||
// When the aggregation returns `ts` as an ISO date string, it matches both `Date` (format: "date-time")
|
||||
// and `string` schemas simultaneously, causing AJV oneOf validation to fail with "passingSchemas: 0,1".
|
||||
// Until the core-typings type is revised (either narrowing `ts` to `string` to match what MongoDB
|
||||
// aggregation actually returns, or adjusting the AJV schema generation for union types), we use a
|
||||
// relaxed inline schema here that accepts `ts` as a string.
|
||||
const paginatedReportsResponseSchema = ajv.compile<{ reports: IModerationAudit[]; count: number; offset: number; total: number }>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
reports: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
userId: { type: 'string' },
|
||||
username: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
message: { type: 'string' },
|
||||
msgId: { type: 'string' },
|
||||
ts: { type: 'string' },
|
||||
rooms: { type: 'array', items: { type: 'object' } },
|
||||
roomIds: { type: 'array', items: { type: 'string' } },
|
||||
count: { type: 'number' },
|
||||
isUserDeleted: { type: 'boolean' },
|
||||
},
|
||||
required: ['userId', 'ts', 'rooms', 'roomIds', 'count', 'isUserDeleted'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
count: { type: 'number' },
|
||||
offset: { type: 'number' },
|
||||
total: { type: 'number' },
|
||||
success: { type: 'boolean', enum: [true] },
|
||||
},
|
||||
required: ['reports', 'count', 'offset', 'total', 'success'],
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
// TODO: IModerationAudit defines `ts` as `Date | string` which generates a oneOf in JSON Schema.
|
||||
// When the aggregation returns `ts` as an ISO date string, it matches both `Date` (format: "date-time")
|
||||
// and `string` schemas simultaneously, causing AJV oneOf validation to fail with "passingSchemas: 0,1".
|
||||
// Until the core-typings type is revised (either narrowing `ts` to `string` to match what MongoDB
|
||||
// aggregation actually returns, or adjusting the AJV schema generation for union types), we use a
|
||||
// relaxed inline schema here that accepts `ts` as a string.
|
||||
const paginatedUserReportsResponseSchema = ajv.compile<{
|
||||
reports: (Pick<UserReport, '_id' | 'reportedUser' | 'ts'> & { count: number })[];
|
||||
count: number;
|
||||
offset: number;
|
||||
total: number;
|
||||
}>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
reports: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
reportedUser: { type: 'object' },
|
||||
ts: { type: 'string' },
|
||||
count: { type: 'number' },
|
||||
},
|
||||
required: ['reportedUser', 'ts', 'count'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
count: { type: 'number' },
|
||||
offset: { type: 'number' },
|
||||
total: { type: 'number' },
|
||||
success: { type: 'boolean', enum: [true] },
|
||||
},
|
||||
required: ['reports', 'count', 'offset', 'total', 'success'],
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
const reportedMessagesResponseSchema = ajv.compile<{
|
||||
user: Pick<IUser, 'username' | 'name' | '_id'> | null;
|
||||
messages: Pick<IModerationReport, '_id' | 'message' | 'ts' | 'room'>[];
|
||||
count: number;
|
||||
total: number;
|
||||
offset: number;
|
||||
}>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
user: {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
_id: { type: 'string' },
|
||||
username: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
},
|
||||
required: ['_id', 'username'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
{ type: 'null' },
|
||||
],
|
||||
},
|
||||
messages: { type: 'array', items: { type: 'object' } },
|
||||
count: { type: 'number' },
|
||||
total: { type: 'number' },
|
||||
offset: { type: 'number' },
|
||||
success: { type: 'boolean', enum: [true] },
|
||||
},
|
||||
required: ['user', 'messages', 'count', 'total', 'offset', 'success'],
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
// TODO: IModerationAudit defines `ts` as `Date | string` which generates a oneOf in JSON Schema.
|
||||
// When the aggregation returns `ts` as an ISO date string, it matches both `Date` (format: "date-time")
|
||||
// and `string` schemas simultaneously, causing AJV oneOf validation to fail with "passingSchemas: 0,1".
|
||||
// Until the core-typings type is revised (either narrowing `ts` to `string` to match what MongoDB
|
||||
// aggregation actually returns, or adjusting the AJV schema generation for union types), we use a
|
||||
// relaxed inline schema here that accepts `ts` as a string.
|
||||
const reportsByUserIdResponseSchema = ajv.compile<{
|
||||
user: Pick<IUser, '_id' | 'username' | 'name' | 'avatarETag' | 'active' | 'roles' | 'emails' | 'createdAt'> | null;
|
||||
reports: IModerationReport[];
|
||||
count: number;
|
||||
total: number;
|
||||
offset: number;
|
||||
}>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
user: {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
_id: { type: 'string' },
|
||||
username: { type: 'string' },
|
||||
name: { type: 'string' },
|
||||
avatarETag: { type: 'string' },
|
||||
active: { type: 'boolean' },
|
||||
roles: { type: 'array', items: { type: 'string' } },
|
||||
emails: { type: 'array', items: { type: 'object' } },
|
||||
createdAt: { type: 'string' },
|
||||
},
|
||||
required: ['_id', 'username'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
{ type: 'null' },
|
||||
],
|
||||
},
|
||||
reports: { type: 'array', items: { type: 'object' } },
|
||||
count: { type: 'number' },
|
||||
total: { type: 'number' },
|
||||
offset: { type: 'number' },
|
||||
success: { type: 'boolean', enum: [true] },
|
||||
},
|
||||
required: ['user', 'reports', 'count', 'total', 'offset', 'success'],
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
const successResponseSchema = ajv.compile<void>({
|
||||
type: 'object',
|
||||
properties: { success: { type: 'boolean', enum: [true] } },
|
||||
required: ['success'],
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
// TODO: IModerationAudit defines `ts` as `Date | string` which generates a oneOf in JSON Schema.
|
||||
// When the aggregation returns `ts` as an ISO date string, it matches both `Date` (format: "date-time")
|
||||
// and `string` schemas simultaneously, causing AJV oneOf validation to fail with "passingSchemas: 0,1".
|
||||
// Until the core-typings type is revised (either narrowing `ts` to `string` to match what MongoDB
|
||||
// aggregation actually returns, or adjusting the AJV schema generation for union types), we use a
|
||||
// relaxed inline schema here that accepts `ts` as a string.
|
||||
const reportInfoResponseSchema = ajv.compile<{ report: IModerationReport | null }>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
report: { type: ['object', 'null'] },
|
||||
success: { type: 'boolean', enum: [true] },
|
||||
},
|
||||
required: ['report', 'success'],
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
API.v1.get(
|
||||
'moderation.reportsByUsers',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-moderation-console'],
|
||||
query: isReportHistoryProps,
|
||||
response: {
|
||||
200: paginatedReportsResponseSchema,
|
||||
400: validateBadRequestErrorResponse,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
403: validateForbiddenErrorResponse,
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
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 escapedSelector = escapeRegExp(selector);
|
||||
|
||||
const reports = await ModerationReports.findMessageReportsGroupedByUser(latest, oldest, escapedSelector, {
|
||||
offset,
|
||||
count,
|
||||
sort,
|
||||
}).toArray();
|
||||
|
||||
if (reports.length === 0) {
|
||||
return API.v1.success({
|
||||
reports,
|
||||
count: 0,
|
||||
offset,
|
||||
total: 0,
|
||||
});
|
||||
}
|
||||
|
||||
const total = await ModerationReports.getTotalUniqueReportedUsers(latest, oldest, escapedSelector, true);
|
||||
|
||||
return API.v1.success({
|
||||
reports,
|
||||
count: reports.length,
|
||||
offset,
|
||||
total,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.get(
|
||||
'moderation.userReports',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-moderation-console'],
|
||||
query: isReportHistoryProps,
|
||||
response: {
|
||||
200: paginatedUserReportsResponseSchema,
|
||||
400: validateBadRequestErrorResponse,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
403: validateForbiddenErrorResponse,
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
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 escapedSelector = escapeRegExp(selector);
|
||||
|
||||
const reports = await ModerationReports.findUserReports(latest, oldest, escapedSelector, {
|
||||
offset,
|
||||
count,
|
||||
sort,
|
||||
}).toArray();
|
||||
|
||||
if (reports.length === 0) {
|
||||
return API.v1.success({
|
||||
reports,
|
||||
count: 0,
|
||||
offset,
|
||||
total: 0,
|
||||
});
|
||||
}
|
||||
|
||||
const total = await ModerationReports.getTotalUniqueReportedUsers(latest, oldest, escapedSelector);
|
||||
|
||||
return API.v1.success({
|
||||
reports,
|
||||
count: reports.length,
|
||||
offset,
|
||||
total,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.get(
|
||||
'moderation.user.reportedMessages',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-moderation-console'],
|
||||
query: isGetUserReportsParams,
|
||||
response: {
|
||||
200: reportedMessagesResponseSchema,
|
||||
400: validateBadRequestErrorResponse,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
403: validateForbiddenErrorResponse,
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
const { userId, selector = '' } = this.queryParams;
|
||||
|
||||
const { sort } = await this.parseJsonQuery();
|
||||
|
||||
const { count = 50, offset = 0 } = await getPaginationItems(this.queryParams);
|
||||
|
||||
const user = await Users.findOneById<Pick<IUser, '_id' | 'username' | 'name'>>(userId, {
|
||||
projection: { _id: 1, username: 1, name: 1 },
|
||||
});
|
||||
|
||||
const escapedSelector = escapeRegExp(selector);
|
||||
|
||||
const { cursor, totalCount } = ModerationReports.findReportedMessagesByReportedUserId(userId, escapedSelector, {
|
||||
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({
|
||||
user,
|
||||
messages: uniqueMessages,
|
||||
count: reports.length,
|
||||
total,
|
||||
offset,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.get(
|
||||
'moderation.user.reportsByUserId',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-moderation-console'],
|
||||
query: isGetUserReportsParams,
|
||||
response: {
|
||||
200: reportsByUserIdResponseSchema,
|
||||
400: validateBadRequestErrorResponse,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
403: validateForbiddenErrorResponse,
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
const { userId, selector = '' } = this.queryParams;
|
||||
const { sort } = await this.parseJsonQuery();
|
||||
const { count = 50, offset = 0 } = await getPaginationItems(this.queryParams);
|
||||
|
||||
const user = await Users.findOneById<IUser>(userId, {
|
||||
projection: {
|
||||
_id: 1,
|
||||
username: 1,
|
||||
name: 1,
|
||||
avatarETag: 1,
|
||||
active: 1,
|
||||
roles: 1,
|
||||
emails: 1,
|
||||
createdAt: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const escapedSelector = escapeRegExp(selector);
|
||||
const { cursor, totalCount } = ModerationReports.findUserReportsByReportedUserId(userId, escapedSelector, {
|
||||
offset,
|
||||
count,
|
||||
sort,
|
||||
});
|
||||
|
||||
const [reports, total] = await Promise.all([cursor.toArray(), totalCount]);
|
||||
|
||||
const emailSet = new Map<IUserEmail['address'], IUserEmail>();
|
||||
|
||||
reports.forEach((report) => {
|
||||
const email = report.reportedUser?.emails?.[0];
|
||||
if (email) {
|
||||
emailSet.set(email.address, email);
|
||||
}
|
||||
});
|
||||
if (user) {
|
||||
user.emails = Array.from(emailSet.values());
|
||||
}
|
||||
|
||||
return API.v1.success({
|
||||
user,
|
||||
reports,
|
||||
count: reports.length,
|
||||
total,
|
||||
offset,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.post(
|
||||
'moderation.user.deleteReportedMessages',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['manage-moderation-actions'],
|
||||
body: isModerationDeleteMsgHistoryParams,
|
||||
response: {
|
||||
200: successResponseSchema,
|
||||
400: validateBadRequestErrorResponse,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
403: validateForbiddenErrorResponse,
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
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 { 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.hideMessageReportsByUserId(userId, this.userId, sanitizedReason, 'DELETE Messages');
|
||||
|
||||
return API.v1.success();
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.post(
|
||||
'moderation.dismissReports',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['manage-moderation-actions'],
|
||||
body: isArchiveReportProps,
|
||||
response: {
|
||||
200: successResponseSchema,
|
||||
400: validateBadRequestErrorResponse,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
403: validateForbiddenErrorResponse,
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
const { userId, msgId, reason, action: actionParam } = this.bodyParams;
|
||||
|
||||
if (userId) {
|
||||
const report = await ModerationReports.findOne({ 'message.u._id': userId, '_hidden': { $ne: true } }, { projection: { _id: 1 } });
|
||||
if (!report) {
|
||||
return API.v1.failure('no-reports-found');
|
||||
}
|
||||
}
|
||||
|
||||
if (msgId) {
|
||||
const report = await ModerationReports.findOne({ 'message._id': msgId, '_hidden': { $ne: true } }, { projection: { _id: 1 } });
|
||||
if (!report) {
|
||||
return API.v1.failure('no-reports-found');
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizedReason: string = reason?.trim() ? reason : 'No reason provided';
|
||||
const action: string = actionParam ?? 'None';
|
||||
|
||||
const { userId: moderatorId } = this;
|
||||
|
||||
if (userId) {
|
||||
await ModerationReports.hideMessageReportsByUserId(userId, moderatorId, sanitizedReason, action);
|
||||
} else {
|
||||
await ModerationReports.hideMessageReportsByMessageId(msgId as string, moderatorId, sanitizedReason, action);
|
||||
}
|
||||
|
||||
return API.v1.success();
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.post(
|
||||
'moderation.dismissUserReports',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['manage-moderation-actions'],
|
||||
body: isArchiveReportProps,
|
||||
response: {
|
||||
200: successResponseSchema,
|
||||
400: validateBadRequestErrorResponse,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
403: validateForbiddenErrorResponse,
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
const { userId, reason, action: actionParam } = this.bodyParams;
|
||||
|
||||
if (!userId) {
|
||||
return API.v1.failure('error-user-id-param-not-provided');
|
||||
}
|
||||
|
||||
const sanitizedReason: string = reason ?? 'No reason provided';
|
||||
const action: string = actionParam ?? 'None';
|
||||
|
||||
const { userId: moderatorId } = this;
|
||||
|
||||
await ModerationReports.hideUserReportsByUserId(userId, moderatorId, sanitizedReason, action);
|
||||
|
||||
return API.v1.success();
|
||||
},
|
||||
);
|
||||
|
||||
// TODO: IModerationAudit defines `ts` as `Date | string` which generates a oneOf in JSON Schema.
|
||||
// When the aggregation returns `ts` as an ISO date string, it matches both `Date` (format: "date-time")
|
||||
// and `string` schemas simultaneously, causing AJV oneOf validation to fail with "passingSchemas: 0,1".
|
||||
// Until the core-typings type is revised (either narrowing `ts` to `string` to match what MongoDB
|
||||
// aggregation actually returns, or adjusting the AJV schema generation for union types), we use a
|
||||
// relaxed inline schema here that accepts `ts` as a string.
|
||||
const reportsByMsgIdResponseSchema = ajv.compile<{
|
||||
reports: Pick<IModerationReport, '_id' | 'description' | 'reportedBy' | 'ts' | 'room'>[];
|
||||
count: number;
|
||||
offset: number;
|
||||
total: number;
|
||||
}>({
|
||||
type: 'object',
|
||||
properties: {
|
||||
reports: { type: 'array', items: { type: 'object' } },
|
||||
count: { type: 'number' },
|
||||
offset: { type: 'number' },
|
||||
total: { type: 'number' },
|
||||
success: { type: 'boolean', enum: [true] },
|
||||
},
|
||||
required: ['reports', 'count', 'offset', 'total', 'success'],
|
||||
additionalProperties: false,
|
||||
});
|
||||
|
||||
API.v1.get(
|
||||
'moderation.reports',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-moderation-console'],
|
||||
query: isReportsByMsgIdParams,
|
||||
response: {
|
||||
200: reportsByMsgIdResponseSchema,
|
||||
400: validateBadRequestErrorResponse,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
403: validateForbiddenErrorResponse,
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
const { msgId } = this.queryParams;
|
||||
|
||||
const { count = 50, offset = 0 } = await getPaginationItems(this.queryParams);
|
||||
const { sort } = await this.parseJsonQuery();
|
||||
const { selector = '' } = this.queryParams;
|
||||
|
||||
const escapedSelector = escapeRegExp(selector);
|
||||
|
||||
const { cursor, totalCount } = ModerationReports.findReportsByMessageId(msgId, escapedSelector, { count, sort, offset });
|
||||
|
||||
const [reports, total] = await Promise.all([cursor.toArray(), totalCount]);
|
||||
|
||||
return API.v1.success({
|
||||
reports,
|
||||
count: reports.length,
|
||||
offset,
|
||||
total,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.get(
|
||||
'moderation.reportInfo',
|
||||
{
|
||||
authRequired: true,
|
||||
permissionsRequired: ['view-moderation-console'],
|
||||
query: isReportInfoParams,
|
||||
response: {
|
||||
200: reportInfoResponseSchema,
|
||||
400: validateBadRequestErrorResponse,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
403: validateForbiddenErrorResponse,
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
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 });
|
||||
},
|
||||
);
|
||||
|
||||
API.v1.post(
|
||||
'moderation.reportUser',
|
||||
{
|
||||
authRequired: true,
|
||||
body: isModerationReportUserPost,
|
||||
response: {
|
||||
200: successResponseSchema,
|
||||
400: validateBadRequestErrorResponse,
|
||||
401: validateUnauthorizedErrorResponse,
|
||||
},
|
||||
},
|
||||
async function action() {
|
||||
const { userId, description } = this.bodyParams;
|
||||
|
||||
const {
|
||||
user: { _id, name, username, createdAt },
|
||||
} = this;
|
||||
|
||||
const reportedUser = await Users.findOneById(userId, {
|
||||
projection: { _id: 1, name: 1, username: 1, emails: 1, createdAt: 1 },
|
||||
});
|
||||
|
||||
if (!reportedUser) {
|
||||
return API.v1.failure('Invalid user id provided.');
|
||||
}
|
||||
|
||||
await ModerationReports.createWithDescriptionAndUser(reportedUser, description, { _id, name, username, createdAt });
|
||||
|
||||
return API.v1.success();
|
||||
},
|
||||
);
|
||||
Reference in New Issue
Block a user