1
0
Fork 0

Improve blacklist management

This commit is contained in:
Chocobozzz 2018-08-14 09:08:47 +02:00
parent 26b7305a23
commit 191764f30b
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
23 changed files with 189 additions and 45 deletions

View File

@ -42,7 +42,7 @@
<td>{{ videoAbuse.createdAt }}</td>
<td>
<a [href]="videoAbuse.video.url" i18n-title title="Go to the video" target="_blank" rel="noopener noreferrer">
<a [href]="getVideoUrl(videoAbuse)" i18n-title title="Go to the video" target="_blank" rel="noopener noreferrer">
{{ videoAbuse.video.name }}
</a>
</td>

View File

@ -8,6 +8,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
import { ConfirmService } from '@app/core'
import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
import { Video } from '@app/shared/video/video.model'
@Component({
selector: 'my-video-abuse-list',
@ -79,6 +80,10 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
return videoAbuse.state.id === VideoAbuseState.REJECTED
}
getVideoUrl (videoAbuse: VideoAbuse) {
return Video.buildClientUrl(videoAbuse.video.uuid)
}
async removeVideoAbuse (videoAbuse: VideoAbuse) {
const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse?'), this.i18n('Delete'))
if (res === false) return

View File

@ -10,8 +10,7 @@
<tr>
<th style="width: 40px"></th>
<th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th>
<th i18n>NSFW</th>
<th i18n>UUID</th>
<th i18n>Sensitive</th>
<th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th>
<th style="width: 50px;"></th>
</tr>
@ -25,9 +24,13 @@
</span>
</td>
<td>{{ videoBlacklist.video.name }}</td>
<td>
<a [href]="getVideoUrl(videoBlacklist)" i18n-title title="Go to the video" target="_blank" rel="noopener noreferrer">
{{ videoBlacklist.video.name }}
</a>
</td>
<td>{{ videoBlacklist.video.nsfw }}</td>
<td>{{ videoBlacklist.video.uuid }}</td>
<td>{{ videoBlacklist.createdAt }}</td>
<td class="action-cell">

View File

@ -3,9 +3,10 @@ import { SortMeta } from 'primeng/components/common/sortmeta'
import { NotificationsService } from 'angular2-notifications'
import { ConfirmService } from '../../../core'
import { RestPagination, RestTable, VideoBlacklistService } from '../../../shared'
import { BlacklistedVideo } from '../../../../../../shared'
import { VideoBlacklist } from '../../../../../../shared'
import { I18n } from '@ngx-translate/i18n-polyfill'
import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
import { Video } from '@app/shared/video/video.model'
@Component({
selector: 'my-video-blacklist-list',
@ -13,13 +14,13 @@ import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
styleUrls: [ './video-blacklist-list.component.scss' ]
})
export class VideoBlacklistListComponent extends RestTable implements OnInit {
blacklist: BlacklistedVideo[] = []
blacklist: VideoBlacklist[] = []
totalRecords = 0
rowsPerPage = 10
sort: SortMeta = { field: 'createdAt', order: 1 }
pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
videoBlacklistActions: DropdownAction<BlacklistedVideo>[] = []
videoBlacklistActions: DropdownAction<VideoBlacklist>[] = []
constructor (
private notificationsService: NotificationsService,
@ -41,7 +42,11 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
this.loadSort()
}
async removeVideoFromBlacklist (entry: BlacklistedVideo) {
getVideoUrl (videoBlacklist: VideoBlacklist) {
return Video.buildClientUrl(videoBlacklist.video.uuid)
}
async removeVideoFromBlacklist (entry: VideoBlacklist) {
const confirmMessage = this.i18n(
'Do you really want to remove this video from the blacklist? It will be available again in the videos list.'
)

View File

@ -18,6 +18,10 @@
<a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
<span i18n class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
<div class="video-info-private">{{ video.privacy.label }}{{ getStateLabel(video) }}</div>
<div *ngIf="video.blacklisted" class="video-info-blacklisted">
<span class="blacklisted-label" i18n>Blacklisted</span>
<span class="blacklisted-reason" *ngIf="video.blacklistedReason">{{ video.blacklistedReason }}</span>
</div>
</div>
<!-- Display only once -->

View File

@ -76,12 +76,25 @@
font-weight: $font-semibold;
}
.video-info-date-views, .video-info-private {
.video-info-date-views,
.video-info-private,
.video-info-blacklisted {
font-size: 13px;
&.video-info-private {
&.video-info-private,
&.video-info-blacklisted .blacklisted-label {
font-weight: $font-semibold;
}
&.video-info-blacklisted {
color: red;
.blacklisted-reason {
&::before {
content: ' - ';
}
}
}
}
}

View File

@ -2,7 +2,7 @@ div {
height: 100%;
width: 100%;
text-align: center;
margin-top: 150px;
padding-top: 150px;
font-size: 32px;
}

View File

@ -3,7 +3,7 @@ import { HttpClient, HttpParams } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { SortMeta } from 'primeng/components/common/sortmeta'
import { Observable } from 'rxjs'
import { BlacklistedVideo, ResultList } from '../../../../../shared'
import { VideoBlacklist, ResultList } from '../../../../../shared'
import { environment } from '../../../environments/environment'
import { RestExtractor, RestPagination, RestService } from '../rest'
@ -17,11 +17,11 @@ export class VideoBlacklistService {
private restExtractor: RestExtractor
) {}
listBlacklist (pagination: RestPagination, sort: SortMeta): Observable<ResultList<BlacklistedVideo>> {
listBlacklist (pagination: RestPagination, sort: SortMeta): Observable<ResultList<VideoBlacklist>> {
let params = new HttpParams()
params = this.restService.addRestGetParams(params, pagination, sort)
return this.authHttp.get<ResultList<BlacklistedVideo>>(VideoBlacklistService.BASE_VIDEOS_URL + 'blacklist', { params })
return this.authHttp.get<ResultList<VideoBlacklist>>(VideoBlacklistService.BASE_VIDEOS_URL + 'blacklist', { params })
.pipe(
map(res => this.restExtractor.convertResultListDateToHuman(res)),
catchError(res => this.restExtractor.handleError(res))

View File

@ -44,7 +44,11 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
}
isBlackistableBy (user: AuthUser) {
return user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true
return this.blacklisted !== true && user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true
}
isUnblacklistableBy (user: AuthUser) {
return this.blacklisted === true && user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true
}
isUpdatableBy (user: AuthUser) {

View File

@ -41,6 +41,8 @@ export class Video implements VideoServerModel {
waitTranscoding?: boolean
state?: VideoConstant<VideoState>
scheduledUpdate?: VideoScheduleUpdate
blacklisted?: boolean
blacklistedReason?: string
account: {
id: number
@ -62,6 +64,10 @@ export class Video implements VideoServerModel {
avatar: Avatar
}
static buildClientUrl (videoUUID: string) {
return '/videos/watch/' + videoUUID
}
private static createDurationString (duration: number) {
const hours = Math.floor(duration / 3600)
const minutes = Math.floor((duration % 3600) / 60)
@ -116,6 +122,9 @@ export class Video implements VideoServerModel {
this.scheduledUpdate = hash.scheduledUpdate
if (this.state) this.state.label = peertubeTranslate(this.state.label, translations)
this.blacklisted = hash.blacklisted
this.blacklistedReason = hash.blacklistedReason
}
isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {

View File

@ -1,4 +1,4 @@
<div class="row">
<div class="root-row row">
<!-- We need the video container for videojs so we just hide it -->
<div id="video-element-wrapper">
<div *ngIf="remoteServerDown" class="remote-server-down">
@ -17,7 +17,12 @@
</div>
<div i18n class="alert alert-info" *ngIf="hasVideoScheduledPublication()">
This video will be published on {{ video.scheduledUpdate.updateAt | date: 'full' }}
This video will be published on {{ video.scheduledUpdate.updateAt | date: 'full' }}.
</div>
<div class="alert alert-danger" *ngIf="video?.blacklisted">
<div class="blacklisted-label" i18n>This video is blacklisted.</div>
{{ video.blacklistedReason }}
</div>
<!-- Video information -->
@ -98,6 +103,10 @@
<span class="icon icon-blacklist"></span> <ng-container i18n>Blacklist</ng-container>
</a>
<a *ngIf="isVideoUnblacklistable()" class="dropdown-item" i18n-title title="Unblacklist this video" href="#" (click)="unblacklistVideo($event)">
<span class="icon icon-unblacklist"></span> <ng-container i18n>Unblacklist</ng-container>
</a>
<a *ngIf="isVideoRemovable()" class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)">
<span class="icon icon-delete"></span> <ng-container i18n>Delete</ng-container>
</a>

View File

@ -1,6 +1,14 @@
@import '_variables';
@import '_mixins';
.root-row {
flex-direction: column;
}
.blacklisted-label {
font-weight: $font-semibold;
}
#video-element-wrapper {
background-color: #000;
display: flex;
@ -259,6 +267,10 @@
background-image: url('../../../assets/images/video/blacklist.svg');
}
&.icon-unblacklist {
background-image: url('../../../assets/images/global/undo.svg');
}
&.icon-delete {
background-image: url('../../../assets/images/global/delete-black.svg');
}

View File

@ -121,7 +121,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.videoCaptionService.listCaptions(uuid)
)
.pipe(
catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ]))
// If 401, the video is private or blacklisted so redirect to 404
catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 404 ]))
)
.subscribe(([ video, captionsResult ]) => {
const startTime = this.route.snapshot.queryParams.start
@ -217,6 +218,31 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.videoBlacklistModal.show()
}
async unblacklistVideo (event: Event) {
event.preventDefault()
const confirmMessage = this.i18n(
'Do you really want to remove this video from the blacklist? It will be available again in the videos list.'
)
const res = await this.confirmService.confirm(confirmMessage, this.i18n('Unblacklist'))
if (res === false) return
this.videoBlacklistService.removeVideoFromBlacklist(this.video.id).subscribe(
() => {
this.notificationsService.success(
this.i18n('Success'),
this.i18n('Video {{name}} removed from the blacklist.', { name: this.video.name })
)
this.video.blacklisted = false
this.video.blacklistedReason = null
},
err => this.notificationsService.error(this.i18n('Error'), err.message)
)
}
isUserLoggedIn () {
return this.authService.isLoggedIn()
}
@ -229,6 +255,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
return this.video.isBlackistableBy(this.user)
}
isVideoUnblacklistable () {
return this.video.isUnblacklistableBy(this.user)
}
getVideoPoster () {
if (!this.video) return ''

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Artboard-4" transform="translate(-180.000000, -115.000000)" fill="#000">
<g id="4" transform="translate(180.000000, 115.000000)">
<path d="M10,19 C10.5522847,19 11,19.4477153 11,20 C11,20.5522847 10.5522847,21 10,21 C9.99404288,21 9.98809793,20.9999479 9.98216558,20.9998442 C5.01980239,20.990358 1,16.9646166 1,12 C1,7.02943725 5.02943725,3 10,3 C14.9705627,3 19,7.02943725 19,12 L17,12 C17,8.13400675 13.8659932,5 10,5 C6.13400675,5 3,8.13400675 3,12 C3,15.8659932 6.13400675,19 10,19 Z M14,12 L22,12 L18,16 L14,12 Z" id="Combined-Shape" transform="translate(11.500000, 12.000000) scale(-1, 1) translate(-11.500000, -12.000000) "></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1014 B

View File

@ -1,5 +1,5 @@
import * as express from 'express'
import { BlacklistedVideo, UserRight, VideoBlacklistCreate } from '../../../../shared'
import { VideoBlacklist, UserRight, VideoBlacklistCreate } from '../../../../shared'
import { logger } from '../../../helpers/logger'
import { getFormattedObjects } from '../../../helpers/utils'
import {
@ -87,7 +87,7 @@ async function updateVideoBlacklistController (req: express.Request, res: expres
async function listBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) {
const resultList = await VideoBlacklistModel.listForApi(req.query.start, req.query.count, req.query.sort)
return res.json(getFormattedObjects<BlacklistedVideo, VideoBlacklistModel>(resultList.data, resultList.total))
return res.json(getFormattedObjects<VideoBlacklist, VideoBlacklistModel>(resultList.data, resultList.total))
}
async function removeVideoFromBlacklistController (req: express.Request, res: express.Response, next: express.NextFunction) {

View File

@ -35,6 +35,8 @@ import { VideoShareModel } from '../../models/video/video-share'
import { authenticate } from '../oauth'
import { areValidationErrors } from './utils'
import { cleanUpReqFiles } from '../../helpers/utils'
import { VideoModel } from '../../models/video/video'
import { UserModel } from '../../models/account/user'
const videosAddValidator = getCommonVideoAttributes().concat([
body('videofile')
@ -131,7 +133,25 @@ const videosGetValidator = [
if (areValidationErrors(req, res)) return
if (!await isVideoExist(req.params.id, res)) return
const video = res.locals.video
const video: VideoModel = res.locals.video
// Video private or blacklisted
if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
authenticate(req, res, () => {
const user: UserModel = res.locals.oauth.token.User
// Only the owner or a user that have blacklist rights can see the video
if (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) {
return res.status(403)
.json({ error: 'Cannot get this private or blacklisted video.' })
.end()
}
return next()
})
return
}
// Video is public, anyone can access it
if (video.privacy === VideoPrivacy.PUBLIC) return next()
@ -143,17 +163,6 @@ const videosGetValidator = [
// Don't leak this unlisted video
return res.status(404).end()
}
// Video is private, check the user
authenticate(req, res, () => {
if (video.VideoChannel.Account.userId !== res.locals.oauth.token.User.id) {
return res.status(403)
.json({ error: 'Cannot get this private video of another user' })
.end()
}
return next()
})
}
]

View File

@ -137,7 +137,6 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
video: {
id: this.Video.id,
uuid: this.Video.uuid,
url: this.Video.url,
name: this.Video.name
},
createdAt: this.createdAt

View File

@ -16,7 +16,7 @@ import { getSortOnModel, throwIfNotValid } from '../utils'
import { VideoModel } from './video'
import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist'
import { Emailer } from '../../lib/emailer'
import { BlacklistedVideo } from '../../../shared/models/videos'
import { VideoBlacklist } from '../../../shared/models/videos'
import { CONSTRAINTS_FIELDS } from '../../initializers'
@Table({
@ -68,7 +68,12 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
offset: start,
limit: count,
order: getSortOnModel(sort.sortModel, sort.sortValue),
include: [ { model: VideoModel } ]
include: [
{
model: VideoModel,
required: true
}
]
}
return VideoBlacklistModel.findAndCountAll(query)
@ -90,7 +95,7 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
return VideoBlacklistModel.findOne(query)
}
toFormattedJSON (): BlacklistedVideo {
toFormattedJSON (): VideoBlacklist {
const video = this.Video
return {

View File

@ -127,7 +127,8 @@ export enum ScopeNames {
WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
WITH_TAGS = 'WITH_TAGS',
WITH_FILES = 'WITH_FILES',
WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE'
WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
WITH_BLACKLISTED = 'WITH_BLACKLISTED'
}
type AvailableForListOptions = {
@ -374,6 +375,15 @@ type AvailableForListOptions = {
[ScopeNames.WITH_TAGS]: {
include: [ () => TagModel ]
},
[ScopeNames.WITH_BLACKLISTED]: {
include: [
{
attributes: [ 'id', 'reason' ],
model: () => VideoBlacklistModel,
required: false
}
]
},
[ScopeNames.WITH_FILES]: {
include: [
{
@ -1004,7 +1014,13 @@ export class VideoModel extends Model<VideoModel> {
}
return VideoModel
.scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_SCHEDULED_UPDATE ])
.scope([
ScopeNames.WITH_TAGS,
ScopeNames.WITH_BLACKLISTED,
ScopeNames.WITH_FILES,
ScopeNames.WITH_ACCOUNT_DETAILS,
ScopeNames.WITH_SCHEDULED_UPDATE
])
.findById(id, options)
}
@ -1030,7 +1046,13 @@ export class VideoModel extends Model<VideoModel> {
}
return VideoModel
.scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_SCHEDULED_UPDATE ])
.scope([
ScopeNames.WITH_TAGS,
ScopeNames.WITH_BLACKLISTED,
ScopeNames.WITH_FILES,
ScopeNames.WITH_ACCOUNT_DETAILS,
ScopeNames.WITH_SCHEDULED_UPDATE
])
.findOne(options)
}
@ -1276,7 +1298,8 @@ export class VideoModel extends Model<VideoModel> {
toFormattedDetailsJSON (): VideoDetails {
const formattedJson = this.toFormattedJSON({
additionalAttributes: {
scheduledUpdate: true
scheduledUpdate: true,
blacklistInfo: true
}
})

View File

@ -19,7 +19,8 @@ function updateVideoBlacklist (url: string, token: string, videoId: number, reas
.send({ reason })
.set('Accept', 'application/json')
.set('Authorization', 'Bearer ' + token)
.expect(specialStatus)}
.expect(specialStatus)
}
function removeVideoFromBlacklist (url: string, token: string, videoId: number | string, specialStatus = 204) {
const path = '/api/v1/videos/' + videoId + '/blacklist'

View File

@ -1,11 +1,14 @@
export enum UserRight {
ALL,
MANAGE_USERS,
MANAGE_SERVER_FOLLOW,
MANAGE_VIDEO_ABUSES,
MANAGE_VIDEO_BLACKLIST,
MANAGE_JOBS,
MANAGE_CONFIGURATION,
MANAGE_VIDEO_BLACKLIST,
REMOVE_ANY_VIDEO,
REMOVE_ANY_VIDEO_CHANNEL,
REMOVE_ANY_VIDEO_COMMENT,

View File

@ -14,7 +14,6 @@ export interface VideoAbuse {
id: number
name: string
uuid: string
url: string
}
createdAt: Date

View File

@ -1,4 +1,4 @@
export interface BlacklistedVideo {
export interface VideoBlacklist {
id: number
createdAt: Date
updatedAt: Date