<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>
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',
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
<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>
</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">
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',
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,
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.'
)
<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 -->
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: ' - ';
+ }
+ }
+ }
}
}
height: 100%;
width: 100%;
text-align: center;
- margin-top: 150px;
+ padding-top: 150px;
font-size: 32px;
}
\ No newline at end of file
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'
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))
}
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) {
waitTranscoding?: boolean
state?: VideoConstant<VideoState>
scheduledUpdate?: VideoScheduleUpdate
+ blacklisted?: boolean
+ blacklistedReason?: string
account: {
id: number
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)
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) {
-<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">
</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 -->
<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>
@import '_variables';
@import '_mixins';
+.root-row {
+ flex-direction: column;
+}
+
+.blacklisted-label {
+ font-weight: $font-semibold;
+}
+
#video-element-wrapper {
background-color: #000;
display: flex;
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');
}
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
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()
}
return this.video.isBlackistableBy(this.user)
}
+ isVideoUnblacklistable () {
+ return this.video.isUnblacklistableBy(this.user)
+ }
+
getVideoPoster () {
if (!this.video) return ''
--- /dev/null
+<?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>
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 {
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) {
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')
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()
// 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()
- })
}
]
video: {
id: this.Video.id,
uuid: this.Video.uuid,
- url: this.Video.url,
name: this.Video.name
},
createdAt: this.createdAt
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({
offset: start,
limit: count,
order: getSortOnModel(sort.sortModel, sort.sortValue),
- include: [ { model: VideoModel } ]
+ include: [
+ {
+ model: VideoModel,
+ required: true
+ }
+ ]
}
return VideoBlacklistModel.findAndCountAll(query)
return VideoBlacklistModel.findOne(query)
}
- toFormattedJSON (): BlacklistedVideo {
+ toFormattedJSON (): VideoBlacklist {
const video = this.Video
return {
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 = {
[ScopeNames.WITH_TAGS]: {
include: [ () => TagModel ]
},
+ [ScopeNames.WITH_BLACKLISTED]: {
+ include: [
+ {
+ attributes: [ 'id', 'reason' ],
+ model: () => VideoBlacklistModel,
+ required: false
+ }
+ ]
+ },
[ScopeNames.WITH_FILES]: {
include: [
{
}
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)
}
}
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)
}
toFormattedDetailsJSON (): VideoDetails {
const formattedJson = this.toFormattedJSON({
additionalAttributes: {
- scheduledUpdate: true
+ scheduledUpdate: true,
+ blacklistInfo: true
}
})
.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'
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,
id: number
name: string
uuid: string
- url: string
}
createdAt: Date
-export interface BlacklistedVideo {
+export interface VideoBlacklist {
id: number
createdAt: Date
updatedAt: Date