Make sure a report doesn't get deleted upon the deletion of its video

This commit is contained in:
Rigel Kent 2020-04-16 14:22:27 +02:00 committed by Rigel Kent
parent 165ee2929b
commit 68d19a0ace
15 changed files with 141 additions and 38 deletions

View File

@ -14,10 +14,10 @@
<ng-template pTemplate="header"> <ng-template pTemplate="header">
<tr> <tr>
<th i18n>Follower handle</th> <th i18n>Follower handle</th>
<th i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th> <th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
<th i18n pSortableColumn="score">Score <p-sortIcon field="score"></p-sortIcon></th> <th style="width: 100px;" i18n pSortableColumn="score">Score <p-sortIcon field="score"></p-sortIcon></th>
<th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> <th style="width: 200px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
<th></th> <th style="width: 100px;"></th>
</tr> </tr>
</ng-template> </ng-template>

View File

@ -20,10 +20,10 @@
<ng-template pTemplate="header"> <ng-template pTemplate="header">
<tr> <tr>
<th i18n>Host</th> <th i18n>Host</th>
<th i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th> <th style="width: 100px;" i18n pSortableColumn="state">State <p-sortIcon field="state"></p-sortIcon></th>
<th i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th> <th style="width: 200px;" i18n pSortableColumn="createdAt">Created <p-sortIcon field="createdAt"></p-sortIcon></th>
<th i18n pSortableColumn="redundancyAllowed">Redundancy allowed <p-sortIcon field="redundancyAllowed"></p-sortIcon></th> <th style="width: 160px;" i18n pSortableColumn="redundancyAllowed">Redundancy allowed <p-sortIcon field="redundancyAllowed"></p-sortIcon></th>
<th></th> <th style="width: 100px;"></th>
</tr> </tr>
</ng-template> </ng-template>

View File

@ -17,11 +17,11 @@
> >
<ng-template pTemplate="header"> <ng-template pTemplate="header">
<tr> <tr>
<th style="width: 40px"></th> <th style="width: 40px;"></th>
<th i18n *ngIf="isDisplayingRemoteVideos()">Strategy</th> <th style="width: 160px;" i18n *ngIf="isDisplayingRemoteVideos()">Strategy</th>
<th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th> <th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th>
<th i18n>Video URL</th> <th i18n>Video URL</th>
<th i18n *ngIf="isDisplayingRemoteVideos()">Total size</th> <th style="width: 100px;" i18n *ngIf="isDisplayingRemoteVideos()">Total size</th>
<th style="width: 80px;"></th> <th style="width: 80px;"></th>
</tr> </tr>
</ng-template> </ng-template>

View File

@ -8,8 +8,8 @@
<ng-template pTemplate="header"> <ng-template pTemplate="header">
<tr> <tr>
<th i18n>Account</th> <th i18n>Account</th>
<th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th> <th style="width: 200px;" i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
<th></th> <!-- column for action buttons --> <th style="width: 100px;"></th> <!-- column for action buttons -->
</tr> </tr>
</ng-template> </ng-template>

View File

@ -16,8 +16,8 @@
<ng-template pTemplate="header"> <ng-template pTemplate="header">
<tr> <tr>
<th i18n>Instance</th> <th i18n>Instance</th>
<th i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th> <th style="width: 200px;" i18n pSortableColumn="createdAt">Muted at <p-sortIcon field="createdAt"></p-sortIcon></th>
<th></th> <!-- column for action buttons --> <th style="width: 100px;"></th> <!-- column for action buttons -->
</tr> </tr>
</ng-template> </ng-template>

View File

@ -1,5 +1,6 @@
@import 'variables'; @import 'variables';
@import 'mixins'; @import 'mixins';
@import 'miniature';
.form-sub-title { .form-sub-title {
flex-grow: 0; flex-grow: 0;
@ -22,12 +23,28 @@
} }
} }
.video-abuse-states {
& > :not(:first-child) {
margin-left: .4rem;
}
}
.screenratio { .screenratio {
position: relative; position: relative;
width: 100%; width: 100%;
height: 0; height: 0;
padding-bottom: 56%; padding-bottom: 56%;
div {
@include miniature-thumbnail;
position: absolute;
height: 100%;
width: 100%;
display: inline-flex;
justify-content: center;
align-items: center;
}
::ng-deep iframe { ::ng-deep iframe {
position: absolute; position: absolute;
width: 100% !important; width: 100% !important;

View File

@ -48,9 +48,10 @@
</a> </a>
</td> </td>
<td class="c-hand" [pRowToggler]="videoAbuse"> <td class="c-hand video-abuse-states" [pRowToggler]="videoAbuse">
<span *ngIf="isVideoAbuseAccepted(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-ok"></span> <span *ngIf="isVideoAbuseAccepted(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-ok"></span>
<span *ngIf="isVideoAbuseRejected(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-remove"></span> <span *ngIf="isVideoAbuseRejected(videoAbuse)" [title]="videoAbuse.state.label" class="glyphicon glyphicon-remove"></span>
<span *ngIf="videoAbuse.moderationComment" [title]="videoAbuse.moderationComment" class="glyphicon glyphicon-comment"></span>
</td> </td>
<td class="action-cell"> <td class="action-cell">
@ -75,7 +76,12 @@
</div> </div>
<div class="col-4"> <div class="col-4">
<div class="screenratio" [innerHTML]="videoAbuse.embedHtml"></div> <div class="screenratio">
<div *ngIf="videoAbuse.video.deleted">
<span i18n>The video was {{ videoAbuse.video.deleted ? 'deleted' : 'blacklisted' }}</span>
</div>
<div *ngIf="!videoAbuse.video.deleted" [innerHTML]="videoAbuse.embedHtml"></div>
</div>
</div> </div>
</div> </div>
</td> </td>

View File

@ -1,9 +1,17 @@
import { Response } from 'express' import { Response } from 'express'
import { VideoAbuseModel } from '../../models/video/video-abuse' import { VideoAbuseModel } from '../../models/video/video-abuse'
import { fetchVideo } from '../video'
async function doesVideoAbuseExist (abuseIdArg: number | string, videoId: number, res: Response) { async function doesVideoAbuseExist (abuseIdArg: number | string, videoUUID: string, res: Response) {
const abuseId = parseInt(abuseIdArg + '', 10) const abuseId = parseInt(abuseIdArg + '', 10)
const videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, videoId) let videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, null, videoUUID)
if (!videoAbuse) {
const userId = res.locals.oauth ? res.locals.oauth.token.User.id : undefined
const video = await fetchVideo(videoUUID, 'all', userId)
if (video) videoAbuse = await VideoAbuseModel.loadByIdAndVideoId(abuseId, video.id)
}
if (videoAbuse === null) { if (videoAbuse === null) {
res.status(404) res.status(404)

View File

@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 485 const LAST_MIGRATION_VERSION = 490
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@ -0,0 +1,28 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction
queryInterface: Sequelize.QueryInterface
sequelize: Sequelize.Sequelize
}): Promise<void> {
const deletedVideo = {
type: Sequelize.JSONB,
allowNull: true
}
await utils.queryInterface.addColumn('videoAbuse', 'deletedVideo', deletedVideo)
await utils.sequelize.query(`ALTER TABLE "videoAbsue" ALTER COLUMN "videoId" DROP NOT NULL;`)
await utils.sequelize.query(`ALTER TABLE "videoAbuse" DROP CONSTRAINT IF EXISTS "videoAbuse_videoId_fkey";`)
await utils.sequelize.query(`ALTER TABLE "videoAbuse" ADD CONSTRAINT "videoAbuse_videoId_fkey"
FOREIGN KEY ("videoId") REFERENCES video(id) ON UPDATE CASCADE ON DELETE SET NULL;`)
}
function down (options) {
throw new Error('Not implemented.')
}
export {
up,
down
}

View File

@ -32,8 +32,7 @@ const videoAbuseGetValidator = [
logger.debug('Checking videoAbuseGetValidator parameters', { parameters: req.body }) logger.debug('Checking videoAbuseGetValidator parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
if (!await doesVideoExist(req.params.videoId, res)) return if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return
if (!await doesVideoAbuseExist(req.params.id, res.locals.videoAll.id, res)) return
return next() return next()
} }
@ -53,8 +52,7 @@ const videoAbuseUpdateValidator = [
logger.debug('Checking videoAbuseUpdateValidator parameters', { parameters: req.body }) logger.debug('Checking videoAbuseUpdateValidator parameters', { parameters: req.body })
if (areValidationErrors(req, res)) return if (areValidationErrors(req, res)) return
if (!await doesVideoExist(req.params.videoId, res)) return if (!await doesVideoAbuseExist(req.params.id, req.params.videoId, res)) return
if (!await doesVideoAbuseExist(req.params.id, res.locals.videoAll.id, res)) return
return next() return next()
} }

View File

@ -9,7 +9,7 @@ import {
import { AccountModel } from '../account/account' import { AccountModel } from '../account/account'
import { buildBlockedAccountSQL, getSort, throwIfNotValid } from '../utils' import { buildBlockedAccountSQL, getSort, throwIfNotValid } from '../utils'
import { VideoModel } from './video' import { VideoModel } from './video'
import { VideoAbuseState } from '../../../shared' import { VideoAbuseState, Video } from '../../../shared'
import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants' import { CONSTRAINTS_FIELDS, VIDEO_ABUSE_STATES } from '../../initializers/constants'
import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../typings/models' import { MUserAccountId, MVideoAbuse, MVideoAbuseFormattable, MVideoAbuseVideo } from '../../typings/models'
import * as Bluebird from 'bluebird' import * as Bluebird from 'bluebird'
@ -46,6 +46,11 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
@Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max)) @Column(DataType.STRING(CONSTRAINTS_FIELDS.VIDEO_ABUSES.MODERATION_COMMENT.max))
moderationComment: string moderationComment: string
@AllowNull(true)
@Default(null)
@Column(DataType.JSONB)
deletedVideo: Video
@CreatedAt @CreatedAt
createdAt: Date createdAt: Date
@ -58,9 +63,9 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
@BelongsTo(() => AccountModel, { @BelongsTo(() => AccountModel, {
foreignKey: { foreignKey: {
allowNull: false allowNull: true
}, },
onDelete: 'cascade' onDelete: 'set null'
}) })
Account: AccountModel Account: AccountModel
@ -70,17 +75,21 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
@BelongsTo(() => VideoModel, { @BelongsTo(() => VideoModel, {
foreignKey: { foreignKey: {
allowNull: false allowNull: true
}, },
onDelete: 'cascade' onDelete: 'set null'
}) })
Video: VideoModel Video: VideoModel
static loadByIdAndVideoId (id: number, videoId: number): Bluebird<MVideoAbuse> { static loadByIdAndVideoId (id: number, videoId?: number, uuid?: string): Bluebird<MVideoAbuse> {
const videoAttributes = {}
if (videoId) videoAttributes['videoId'] = videoId
if (uuid) videoAttributes['deletedVideo'] = { uuid }
const query = { const query = {
where: { where: {
id, id,
videoId ...videoAttributes
} }
} }
return VideoAbuseModel.findOne(query) return VideoAbuseModel.findOne(query)
@ -112,7 +121,7 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
}, },
{ {
model: VideoModel, model: VideoModel,
required: true required: false
} }
] ]
} }
@ -124,6 +133,10 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
} }
toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse { toFormattedJSON (this: MVideoAbuseFormattable): VideoAbuse {
const video = this.Video
? this.Video
: this.deletedVideo
return { return {
id: this.id, id: this.id,
reason: this.reason, reason: this.reason,
@ -134,9 +147,11 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
}, },
moderationComment: this.moderationComment, moderationComment: this.moderationComment,
video: { video: {
id: this.Video.id, id: video.id,
uuid: this.Video.uuid, uuid: video.uuid,
name: this.Video.name name: video.name,
nsfw: video.nsfw,
deleted: !this.Video
}, },
createdAt: this.createdAt createdAt: this.createdAt
} }

View File

@ -628,9 +628,9 @@ export class VideoModel extends Model<VideoModel> {
@HasMany(() => VideoAbuseModel, { @HasMany(() => VideoAbuseModel, {
foreignKey: { foreignKey: {
name: 'videoId', name: 'videoId',
allowNull: false allowNull: true
}, },
onDelete: 'cascade' onDelete: 'set null'
}) })
VideoAbuses: VideoAbuseModel[] VideoAbuses: VideoAbuseModel[]
@ -798,6 +798,35 @@ export class VideoModel extends Model<VideoModel> {
ModelCache.Instance.invalidateCache('video', instance.id) ModelCache.Instance.invalidateCache('video', instance.id)
} }
@BeforeDestroy
static async saveEssentialDataToAbuses (instance: VideoModel, options) {
const tasks: Promise<any>[] = []
logger.info('Saving video abuses details of video %s.', instance.url)
if (!Array.isArray(instance.VideoAbuses)) {
instance.VideoAbuses = await instance.$get('VideoAbuses')
if (instance.VideoAbuses.length === 0) return undefined
}
const details = instance.toFormattedJSON()
for (const abuse of instance.VideoAbuses) {
tasks.push((_ => {
abuse.deletedVideo = details
return abuse.save({ transaction: options.transaction })
})())
}
Promise.all(tasks)
.catch(err => {
logger.error('Some errors when saving details of video %s in its abuses before destroy hook.', instance.uuid, { err })
})
return undefined
}
static listLocal (): Bluebird<MVideoWithAllFiles[]> { static listLocal (): Bluebird<MVideoWithAllFiles[]> {
const query = { const query = {
where: { where: {

View File

@ -31,4 +31,4 @@ export type MVideoAbuseAccountVideo =
export type MVideoAbuseFormattable = export type MVideoAbuseFormattable =
MVideoAbuse & MVideoAbuse &
Use<'Account', MAccountFormattable> & Use<'Account', MAccountFormattable> &
Use<'Video', Pick<MVideo, 'id' | 'uuid' | 'name'>> Use<'Video', Pick<MVideo, 'id' | 'uuid' | 'name' | 'nsfw'>>

View File

@ -14,6 +14,8 @@ export interface VideoAbuse {
id: number id: number
name: string name: string
uuid: string uuid: string
nsfw: boolean
deleted: boolean
} }
createdAt: Date createdAt: Date