From edbc9325462ddf4536775871ebc25e06f46612d1 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 24 Jul 2020 15:05:51 +0200 Subject: Add server API to abuse messages --- server/models/abuse/abuse-message.ts | 103 ++++++++++++++++ server/models/abuse/abuse-query-builder.ts | 15 ++- server/models/abuse/abuse.ts | 182 +++++++++++++++++++++-------- 3 files changed, 247 insertions(+), 53 deletions(-) create mode 100644 server/models/abuse/abuse-message.ts (limited to 'server/models/abuse') diff --git a/server/models/abuse/abuse-message.ts b/server/models/abuse/abuse-message.ts new file mode 100644 index 000000000..f7721c87d --- /dev/null +++ b/server/models/abuse/abuse-message.ts @@ -0,0 +1,103 @@ +import { AllowNull, BelongsTo, Column, CreatedAt, DataType, ForeignKey, Is, Model, Table, UpdatedAt } from 'sequelize-typescript' +import { isAbuseMessageValid } from '@server/helpers/custom-validators/abuses' +import { AbuseMessage } from '@shared/models' +import { AccountModel, ScopeNames as AccountScopeNames } from '../account/account' +import { throwIfNotValid, getSort } from '../utils' +import { AbuseModel } from './abuse' +import { MAbuseMessageFormattable, MAbuseMessage } from '@server/types/models' + +@Table({ + tableName: 'abuseMessage', + indexes: [ + { + fields: [ 'abuseId' ] + }, + { + fields: [ 'accountId' ] + } + ] +}) +export class AbuseMessageModel extends Model { + + @AllowNull(false) + @Is('AbuseMessage', value => throwIfNotValid(value, isAbuseMessageValid, 'message')) + @Column(DataType.TEXT) + message: string + + @AllowNull(false) + @Column + byModerator: boolean + + @CreatedAt + createdAt: Date + + @UpdatedAt + updatedAt: Date + + @ForeignKey(() => AccountModel) + @Column + accountId: number + + @BelongsTo(() => AccountModel, { + foreignKey: { + name: 'accountId', + allowNull: true + }, + onDelete: 'set null' + }) + Account: AccountModel + + @ForeignKey(() => AbuseModel) + @Column + abuseId: number + + @BelongsTo(() => AbuseModel, { + foreignKey: { + name: 'abuseId', + allowNull: false + }, + onDelete: 'cascade' + }) + Abuse: AbuseModel + + static listForApi (abuseId: number) { + const options = { + where: { abuseId }, + + order: getSort('createdAt'), + + include: [ + { + model: AccountModel.scope(AccountScopeNames.SUMMARY), + required: false + } + ] + } + + return AbuseMessageModel.findAndCountAll(options) + .then(({ rows, count }) => ({ data: rows, total: count })) + } + + static loadByIdAndAbuseId (messageId: number, abuseId: number): Promise { + return AbuseMessageModel.findOne({ + where: { + id: messageId, + abuseId + } + }) + } + + toFormattedJSON (this: MAbuseMessageFormattable): AbuseMessage { + const account = this.Account + ? this.Account.toFormattedSummaryJSON() + : null + + return { + id: this.id, + byModerator: this.byModerator, + message: this.message, + + account + } + } +} diff --git a/server/models/abuse/abuse-query-builder.ts b/server/models/abuse/abuse-query-builder.ts index 5fddcf3c4..9d7cb75aa 100644 --- a/server/models/abuse/abuse-query-builder.ts +++ b/server/models/abuse/abuse-query-builder.ts @@ -26,8 +26,10 @@ export type BuildAbusesQueryOptions = { state?: AbuseState // accountIds - serverAccountId: number - userAccountId: number + serverAccountId?: number + userAccountId?: number + + reporterAccountId?: number } function buildAbuseListQuery (options: BuildAbusesQueryOptions, type: 'count' | 'id') { @@ -45,7 +47,14 @@ function buildAbuseListQuery (options: BuildAbusesQueryOptions, type: 'count' | 'LEFT JOIN "videoComment" ON "commentAbuse"."videoCommentId" = "videoComment"."id"' ] - whereAnd.push('"abuse"."reporterAccountId" NOT IN (' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')') + if (options.serverAccountId || options.userAccountId) { + whereAnd.push('"abuse"."reporterAccountId" NOT IN (' + buildBlockedAccountSQL([ options.serverAccountId, options.userAccountId ]) + ')') + } + + if (options.reporterAccountId) { + whereAnd.push('"abuse"."reporterAccountId" = :reporterAccountId') + replacements.reporterAccountId = options.reporterAccountId + } if (options.search) { const searchWhereOr = [ diff --git a/server/models/abuse/abuse.ts b/server/models/abuse/abuse.ts index bd96cf79c..7002502d5 100644 --- a/server/models/abuse/abuse.ts +++ b/server/models/abuse/abuse.ts @@ -18,7 +18,6 @@ import { } from 'sequelize-typescript' import { isAbuseModerationCommentValid, isAbuseReasonValid, isAbuseStateValid } from '@server/helpers/custom-validators/abuses' import { - Abuse, AbuseFilter, AbuseObject, AbusePredefinedReasons, @@ -26,11 +25,14 @@ import { AbusePredefinedReasonsString, AbuseState, AbuseVideoIs, - VideoAbuse, - VideoCommentAbuse + AdminVideoAbuse, + AdminAbuse, + AdminVideoCommentAbuse, + UserAbuse, + UserVideoAbuse } from '@shared/models' import { ABUSE_STATES, CONSTRAINTS_FIELDS } from '../../initializers/constants' -import { MAbuse, MAbuseAP, MAbuseFormattable, MUserAccountId } from '../../types/models' +import { MAbuse, MAbuseAdminFormattable, MAbuseAP, MUserAccountId, MAbuseUserFormattable } from '../../types/models' import { AccountModel, ScopeNames as AccountScopeNames, SummaryOptions as AccountSummaryOptions } from '../account/account' import { getSort, throwIfNotValid } from '../utils' import { ThumbnailModel } from '../video/thumbnail' @@ -51,6 +53,16 @@ export enum ScopeNames { return { attributes: { include: [ + [ + literal( + '(' + + 'SELECT count(*) ' + + 'FROM "abuseMessage" ' + + 'WHERE "abuseId" = "AbuseModel"."id"' + + ')' + ), + 'countMessages' + ], [ // we don't care about this count for deleted videos, so there are not included literal( @@ -285,7 +297,7 @@ export class AbuseModel extends Model { return AbuseModel.findOne(query) } - static async listForApi (parameters: { + static async listForAdminApi (parameters: { start: number count: number sort: string @@ -353,71 +365,98 @@ export class AbuseModel extends Model { return { total, data } } - toFormattedJSON (this: MAbuseFormattable): Abuse { - const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) + static async listForUserApi (parameters: { + user: MUserAccountId - const countReportsForVideo = this.get('countReportsForVideo') as number - const nthReportForVideo = this.get('nthReportForVideo') as number + start: number + count: number + sort: string - const countReportsForReporter = this.get('countReportsForReporter') as number - const countReportsForReportee = this.get('countReportsForReportee') as number + id?: number + search?: string + state?: AbuseState + }) { + const { + start, + count, + sort, + search, + user, + state, + id + } = parameters - let video: VideoAbuse = null - let comment: VideoCommentAbuse = null + const queryOptions: BuildAbusesQueryOptions = { + start, + count, + sort, + id, + search, + state, + reporterAccountId: user.Account.id + } + + const [ total, data ] = await Promise.all([ + AbuseModel.internalCountForApi(queryOptions), + AbuseModel.internalListForApi(queryOptions) + ]) + + return { total, data } + } - if (this.VideoAbuse) { - const abuseModel = this.VideoAbuse - const entity = abuseModel.Video || abuseModel.deletedVideo + buildBaseVideoCommentAbuse (this: MAbuseUserFormattable) { + if (!this.VideoCommentAbuse) return null - video = { - id: entity.id, - uuid: entity.uuid, - name: entity.name, - nsfw: entity.nsfw, + const abuseModel = this.VideoCommentAbuse + const entity = abuseModel.VideoComment - startAt: abuseModel.startAt, - endAt: abuseModel.endAt, + return { + id: entity.id, + threadId: entity.getThreadId(), - deleted: !abuseModel.Video, - blacklisted: abuseModel.Video?.isBlacklisted() || false, - thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(), + text: entity.text ?? '', - channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel, + deleted: entity.isDeleted(), - countReports: countReportsForVideo, - nthReport: nthReportForVideo + video: { + id: entity.Video.id, + name: entity.Video.name, + uuid: entity.Video.uuid } } + } - if (this.VideoCommentAbuse) { - const abuseModel = this.VideoCommentAbuse - const entity = abuseModel.VideoComment + buildBaseVideoAbuse (this: MAbuseUserFormattable): UserVideoAbuse { + if (!this.VideoAbuse) return null - comment = { - id: entity.id, - threadId: entity.getThreadId(), + const abuseModel = this.VideoAbuse + const entity = abuseModel.Video || abuseModel.deletedVideo - text: entity.text ?? '', + return { + id: entity.id, + uuid: entity.uuid, + name: entity.name, + nsfw: entity.nsfw, - deleted: entity.isDeleted(), + startAt: abuseModel.startAt, + endAt: abuseModel.endAt, - video: { - id: entity.Video.id, - name: entity.Video.name, - uuid: entity.Video.uuid - } - } + deleted: !abuseModel.Video, + blacklisted: abuseModel.Video?.isBlacklisted() || false, + thumbnailPath: abuseModel.Video?.getMiniatureStaticPath(), + + channel: abuseModel.Video?.VideoChannel.toFormattedJSON() || abuseModel.deletedVideo?.channel, } + } + + buildBaseAbuse (this: MAbuseUserFormattable, countMessages: number): UserAbuse { + const predefinedReasons = AbuseModel.getPredefinedReasonsStrings(this.predefinedReasons) return { id: this.id, reason: this.reason, predefinedReasons, - reporterAccount: this.ReporterAccount - ? this.ReporterAccount.toFormattedJSON() - : null, - flaggedAccount: this.FlaggedAccount ? this.FlaggedAccount.toFormattedJSON() : null, @@ -429,11 +468,41 @@ export class AbuseModel extends Model { moderationComment: this.moderationComment, + countMessages, + + createdAt: this.createdAt, + updatedAt: this.updatedAt + } + } + + toFormattedAdminJSON (this: MAbuseAdminFormattable): AdminAbuse { + const countReportsForVideo = this.get('countReportsForVideo') as number + const nthReportForVideo = this.get('nthReportForVideo') as number + + const countReportsForReporter = this.get('countReportsForReporter') as number + const countReportsForReportee = this.get('countReportsForReportee') as number + + const countMessages = this.get('countMessages') as number + + const baseVideo = this.buildBaseVideoAbuse() + const video: AdminVideoAbuse = baseVideo + ? Object.assign(baseVideo, { + countReports: countReportsForVideo, + nthReport: nthReportForVideo + }) + : null + + const comment: AdminVideoCommentAbuse = this.buildBaseVideoCommentAbuse() + + const abuse = this.buildBaseAbuse(countMessages || 0) + + return Object.assign(abuse, { video, comment, - createdAt: this.createdAt, - updatedAt: this.updatedAt, + reporterAccount: this.ReporterAccount + ? this.ReporterAccount.toFormattedJSON() + : null, countReportsForReporter: (countReportsForReporter || 0), countReportsForReportee: (countReportsForReportee || 0), @@ -443,7 +512,20 @@ export class AbuseModel extends Model { endAt: null, count: countReportsForVideo || 0, nth: nthReportForVideo || 0 - } + }) + } + + toFormattedUserJSON (this: MAbuseUserFormattable): UserAbuse { + const countMessages = this.get('countMessages') as number + + const video = this.buildBaseVideoAbuse() + const comment: AdminVideoCommentAbuse = this.buildBaseVideoCommentAbuse() + const abuse = this.buildBaseAbuse(countMessages || 0) + + return Object.assign(abuse, { + video, + comment + }) } toActivityPubObject (this: MAbuseAP): AbuseObject { -- cgit v1.2.3