Make sure a report doesn't get deleted upon the deletion of its video
This commit is contained in:
parent
165ee2929b
commit
68d19a0ace
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { CONFIG, registerConfigChangedHandler } from './config'
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const LAST_MIGRATION_VERSION = 485
|
const LAST_MIGRATION_VERSION = 490
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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'>>
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue