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 2s
655 lines
19 KiB
TypeScript
655 lines
19 KiB
TypeScript
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();
|
|
},
|
|
);
|