aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2018-08-14 09:08:47 +0200
committerChocobozzz <me@florianbigard.com>2018-08-14 09:27:18 +0200
commit191764f30b0a812bf3a9dbdc7daf1d5afe25e12a (patch)
treea5592f8d89949cde832f025e393a3821ad2aca37
parent26b7305a232e547709f433a6edf700bf495935d8 (diff)
downloadPeerTube-191764f30b0a812bf3a9dbdc7daf1d5afe25e12a.tar.gz
PeerTube-191764f30b0a812bf3a9dbdc7daf1d5afe25e12a.tar.zst
PeerTube-191764f30b0a812bf3a9dbdc7daf1d5afe25e12a.zip
Improve blacklist management
-rw-r--r--client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html2
-rw-r--r--client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.ts5
-rw-r--r--client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.html11
-rw-r--r--client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.ts13
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.html4
-rw-r--r--client/src/app/+my-account/my-account-videos/my-account-videos.component.scss17
-rw-r--r--client/src/app/+page-not-found/page-not-found.component.scss2
-rw-r--r--client/src/app/shared/video-blacklist/video-blacklist.service.ts6
-rw-r--r--client/src/app/shared/video/video-details.model.ts6
-rw-r--r--client/src/app/shared/video/video.model.ts9
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.html13
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.scss12
-rw-r--r--client/src/app/videos/+video-watch/video-watch.component.ts32
-rw-r--r--client/src/assets/images/global/undo.svg11
-rw-r--r--server/controllers/api/videos/blacklist.ts4
-rw-r--r--server/middlewares/validators/videos.ts33
-rw-r--r--server/models/video/video-abuse.ts1
-rw-r--r--server/models/video/video-blacklist.ts11
-rw-r--r--server/models/video/video.ts31
-rw-r--r--server/tests/utils/videos/video-blacklist.ts3
-rw-r--r--shared/models/users/user-right.enum.ts5
-rw-r--r--shared/models/videos/video-abuse.model.ts1
-rw-r--r--shared/models/videos/video-blacklist.model.ts2
23 files changed, 189 insertions, 45 deletions
diff --git a/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html
index aa0e18c70..f213ab4b0 100644
--- a/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html
+++ b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.html
@@ -42,7 +42,7 @@
42 <td>{{ videoAbuse.createdAt }}</td> 42 <td>{{ videoAbuse.createdAt }}</td>
43 43
44 <td> 44 <td>
45 <a [href]="videoAbuse.video.url" i18n-title title="Go to the video" target="_blank" rel="noopener noreferrer"> 45 <a [href]="getVideoUrl(videoAbuse)" i18n-title title="Go to the video" target="_blank" rel="noopener noreferrer">
46 {{ videoAbuse.video.name }} 46 {{ videoAbuse.video.name }}
47 </a> 47 </a>
48 </td> 48 </td>
diff --git a/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.ts b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.ts
index a850c0ec2..377e9c80f 100644
--- a/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.ts
+++ b/client/src/app/+admin/video-abuses/video-abuse-list/video-abuse-list.component.ts
@@ -8,6 +8,7 @@ import { I18n } from '@ngx-translate/i18n-polyfill'
8import { DropdownAction } from '@app/shared/buttons/action-dropdown.component' 8import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
9import { ConfirmService } from '@app/core' 9import { ConfirmService } from '@app/core'
10import { ModerationCommentModalComponent } from './moderation-comment-modal.component' 10import { ModerationCommentModalComponent } from './moderation-comment-modal.component'
11import { Video } from '@app/shared/video/video.model'
11 12
12@Component({ 13@Component({
13 selector: 'my-video-abuse-list', 14 selector: 'my-video-abuse-list',
@@ -79,6 +80,10 @@ export class VideoAbuseListComponent extends RestTable implements OnInit {
79 return videoAbuse.state.id === VideoAbuseState.REJECTED 80 return videoAbuse.state.id === VideoAbuseState.REJECTED
80 } 81 }
81 82
83 getVideoUrl (videoAbuse: VideoAbuse) {
84 return Video.buildClientUrl(videoAbuse.video.uuid)
85 }
86
82 async removeVideoAbuse (videoAbuse: VideoAbuse) { 87 async removeVideoAbuse (videoAbuse: VideoAbuse) {
83 const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse?'), this.i18n('Delete')) 88 const res = await this.confirmService.confirm(this.i18n('Do you really want to delete this abuse?'), this.i18n('Delete'))
84 if (res === false) return 89 if (res === false) return
diff --git a/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.html b/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.html
index 78989dc58..05b3a300c 100644
--- a/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.html
+++ b/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.html
@@ -10,8 +10,7 @@
10 <tr> 10 <tr>
11 <th style="width: 40px"></th> 11 <th style="width: 40px"></th>
12 <th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th> 12 <th i18n pSortableColumn="name">Video name <p-sortIcon field="name"></p-sortIcon></th>
13 <th i18n>NSFW</th> 13 <th i18n>Sensitive</th>
14 <th i18n>UUID</th>
15 <th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th> 14 <th i18n pSortableColumn="createdAt">Date <p-sortIcon field="createdAt"></p-sortIcon></th>
16 <th style="width: 50px;"></th> 15 <th style="width: 50px;"></th>
17 </tr> 16 </tr>
@@ -25,9 +24,13 @@
25 </span> 24 </span>
26 </td> 25 </td>
27 26
28 <td>{{ videoBlacklist.video.name }}</td> 27 <td>
28 <a [href]="getVideoUrl(videoBlacklist)" i18n-title title="Go to the video" target="_blank" rel="noopener noreferrer">
29 {{ videoBlacklist.video.name }}
30 </a>
31 </td>
32
29 <td>{{ videoBlacklist.video.nsfw }}</td> 33 <td>{{ videoBlacklist.video.nsfw }}</td>
30 <td>{{ videoBlacklist.video.uuid }}</td>
31 <td>{{ videoBlacklist.createdAt }}</td> 34 <td>{{ videoBlacklist.createdAt }}</td>
32 35
33 <td class="action-cell"> 36 <td class="action-cell">
diff --git a/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.ts b/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.ts
index 00b0ac57e..0618252b8 100644
--- a/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.ts
+++ b/client/src/app/+admin/video-blacklist/video-blacklist-list/video-blacklist-list.component.ts
@@ -3,9 +3,10 @@ import { SortMeta } from 'primeng/components/common/sortmeta'
3import { NotificationsService } from 'angular2-notifications' 3import { NotificationsService } from 'angular2-notifications'
4import { ConfirmService } from '../../../core' 4import { ConfirmService } from '../../../core'
5import { RestPagination, RestTable, VideoBlacklistService } from '../../../shared' 5import { RestPagination, RestTable, VideoBlacklistService } from '../../../shared'
6import { BlacklistedVideo } from '../../../../../../shared' 6import { VideoBlacklist } from '../../../../../../shared'
7import { I18n } from '@ngx-translate/i18n-polyfill' 7import { I18n } from '@ngx-translate/i18n-polyfill'
8import { DropdownAction } from '@app/shared/buttons/action-dropdown.component' 8import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
9import { Video } from '@app/shared/video/video.model'
9 10
10@Component({ 11@Component({
11 selector: 'my-video-blacklist-list', 12 selector: 'my-video-blacklist-list',
@@ -13,13 +14,13 @@ import { DropdownAction } from '@app/shared/buttons/action-dropdown.component'
13 styleUrls: [ './video-blacklist-list.component.scss' ] 14 styleUrls: [ './video-blacklist-list.component.scss' ]
14}) 15})
15export class VideoBlacklistListComponent extends RestTable implements OnInit { 16export class VideoBlacklistListComponent extends RestTable implements OnInit {
16 blacklist: BlacklistedVideo[] = [] 17 blacklist: VideoBlacklist[] = []
17 totalRecords = 0 18 totalRecords = 0
18 rowsPerPage = 10 19 rowsPerPage = 10
19 sort: SortMeta = { field: 'createdAt', order: 1 } 20 sort: SortMeta = { field: 'createdAt', order: 1 }
20 pagination: RestPagination = { count: this.rowsPerPage, start: 0 } 21 pagination: RestPagination = { count: this.rowsPerPage, start: 0 }
21 22
22 videoBlacklistActions: DropdownAction<BlacklistedVideo>[] = [] 23 videoBlacklistActions: DropdownAction<VideoBlacklist>[] = []
23 24
24 constructor ( 25 constructor (
25 private notificationsService: NotificationsService, 26 private notificationsService: NotificationsService,
@@ -41,7 +42,11 @@ export class VideoBlacklistListComponent extends RestTable implements OnInit {
41 this.loadSort() 42 this.loadSort()
42 } 43 }
43 44
44 async removeVideoFromBlacklist (entry: BlacklistedVideo) { 45 getVideoUrl (videoBlacklist: VideoBlacklist) {
46 return Video.buildClientUrl(videoBlacklist.video.uuid)
47 }
48
49 async removeVideoFromBlacklist (entry: VideoBlacklist) {
45 const confirmMessage = this.i18n( 50 const confirmMessage = this.i18n(
46 'Do you really want to remove this video from the blacklist? It will be available again in the videos list.' 51 'Do you really want to remove this video from the blacklist? It will be available again in the videos list.'
47 ) 52 )
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html
index 4823e2db9..8a6cb5c32 100644
--- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.html
+++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.html
@@ -18,6 +18,10 @@
18 <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a> 18 <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
19 <span i18n class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span> 19 <span i18n class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
20 <div class="video-info-private">{{ video.privacy.label }}{{ getStateLabel(video) }}</div> 20 <div class="video-info-private">{{ video.privacy.label }}{{ getStateLabel(video) }}</div>
21 <div *ngIf="video.blacklisted" class="video-info-blacklisted">
22 <span class="blacklisted-label" i18n>Blacklisted</span>
23 <span class="blacklisted-reason" *ngIf="video.blacklistedReason">{{ video.blacklistedReason }}</span>
24 </div>
21 </div> 25 </div>
22 26
23 <!-- Display only once --> 27 <!-- Display only once -->
diff --git a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
index 9df28f472..64a04fa20 100644
--- a/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
+++ b/client/src/app/+my-account/my-account-videos/my-account-videos.component.scss
@@ -76,12 +76,25 @@
76 font-weight: $font-semibold; 76 font-weight: $font-semibold;
77 } 77 }
78 78
79 .video-info-date-views, .video-info-private { 79 .video-info-date-views,
80 .video-info-private,
81 .video-info-blacklisted {
80 font-size: 13px; 82 font-size: 13px;
81 83
82 &.video-info-private { 84 &.video-info-private,
85 &.video-info-blacklisted .blacklisted-label {
83 font-weight: $font-semibold; 86 font-weight: $font-semibold;
84 } 87 }
88
89 &.video-info-blacklisted {
90 color: red;
91
92 .blacklisted-reason {
93 &::before {
94 content: ' - ';
95 }
96 }
97 }
85 } 98 }
86 } 99 }
87 100
diff --git a/client/src/app/+page-not-found/page-not-found.component.scss b/client/src/app/+page-not-found/page-not-found.component.scss
index 53b6142e1..f3f0354a3 100644
--- a/client/src/app/+page-not-found/page-not-found.component.scss
+++ b/client/src/app/+page-not-found/page-not-found.component.scss
@@ -2,7 +2,7 @@ div {
2 height: 100%; 2 height: 100%;
3 width: 100%; 3 width: 100%;
4 text-align: center; 4 text-align: center;
5 margin-top: 150px; 5 padding-top: 150px;
6 6
7 font-size: 32px; 7 font-size: 32px;
8} \ No newline at end of file 8} \ No newline at end of file
diff --git a/client/src/app/shared/video-blacklist/video-blacklist.service.ts b/client/src/app/shared/video-blacklist/video-blacklist.service.ts
index a014260b1..7d39fd4f2 100644
--- a/client/src/app/shared/video-blacklist/video-blacklist.service.ts
+++ b/client/src/app/shared/video-blacklist/video-blacklist.service.ts
@@ -3,7 +3,7 @@ import { HttpClient, HttpParams } from '@angular/common/http'
3import { Injectable } from '@angular/core' 3import { Injectable } from '@angular/core'
4import { SortMeta } from 'primeng/components/common/sortmeta' 4import { SortMeta } from 'primeng/components/common/sortmeta'
5import { Observable } from 'rxjs' 5import { Observable } from 'rxjs'
6import { BlacklistedVideo, ResultList } from '../../../../../shared' 6import { VideoBlacklist, ResultList } from '../../../../../shared'
7import { environment } from '../../../environments/environment' 7import { environment } from '../../../environments/environment'
8import { RestExtractor, RestPagination, RestService } from '../rest' 8import { RestExtractor, RestPagination, RestService } from '../rest'
9 9
@@ -17,11 +17,11 @@ export class VideoBlacklistService {
17 private restExtractor: RestExtractor 17 private restExtractor: RestExtractor
18 ) {} 18 ) {}
19 19
20 listBlacklist (pagination: RestPagination, sort: SortMeta): Observable<ResultList<BlacklistedVideo>> { 20 listBlacklist (pagination: RestPagination, sort: SortMeta): Observable<ResultList<VideoBlacklist>> {
21 let params = new HttpParams() 21 let params = new HttpParams()
22 params = this.restService.addRestGetParams(params, pagination, sort) 22 params = this.restService.addRestGetParams(params, pagination, sort)
23 23
24 return this.authHttp.get<ResultList<BlacklistedVideo>>(VideoBlacklistService.BASE_VIDEOS_URL + 'blacklist', { params }) 24 return this.authHttp.get<ResultList<VideoBlacklist>>(VideoBlacklistService.BASE_VIDEOS_URL + 'blacklist', { params })
25 .pipe( 25 .pipe(
26 map(res => this.restExtractor.convertResultListDateToHuman(res)), 26 map(res => this.restExtractor.convertResultListDateToHuman(res)),
27 catchError(res => this.restExtractor.handleError(res)) 27 catchError(res => this.restExtractor.handleError(res))
diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts
index bdcc0bbba..d346f985c 100644
--- a/client/src/app/shared/video/video-details.model.ts
+++ b/client/src/app/shared/video/video-details.model.ts
@@ -44,7 +44,11 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
44 } 44 }
45 45
46 isBlackistableBy (user: AuthUser) { 46 isBlackistableBy (user: AuthUser) {
47 return user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true 47 return this.blacklisted !== true && user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true
48 }
49
50 isUnblacklistableBy (user: AuthUser) {
51 return this.blacklisted === true && user && user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST) === true
48 } 52 }
49 53
50 isUpdatableBy (user: AuthUser) { 54 isUpdatableBy (user: AuthUser) {
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts
index 6b1a299ea..ec0afcccb 100644
--- a/client/src/app/shared/video/video.model.ts
+++ b/client/src/app/shared/video/video.model.ts
@@ -41,6 +41,8 @@ export class Video implements VideoServerModel {
41 waitTranscoding?: boolean 41 waitTranscoding?: boolean
42 state?: VideoConstant<VideoState> 42 state?: VideoConstant<VideoState>
43 scheduledUpdate?: VideoScheduleUpdate 43 scheduledUpdate?: VideoScheduleUpdate
44 blacklisted?: boolean
45 blacklistedReason?: string
44 46
45 account: { 47 account: {
46 id: number 48 id: number
@@ -62,6 +64,10 @@ export class Video implements VideoServerModel {
62 avatar: Avatar 64 avatar: Avatar
63 } 65 }
64 66
67 static buildClientUrl (videoUUID: string) {
68 return '/videos/watch/' + videoUUID
69 }
70
65 private static createDurationString (duration: number) { 71 private static createDurationString (duration: number) {
66 const hours = Math.floor(duration / 3600) 72 const hours = Math.floor(duration / 3600)
67 const minutes = Math.floor((duration % 3600) / 60) 73 const minutes = Math.floor((duration % 3600) / 60)
@@ -116,6 +122,9 @@ export class Video implements VideoServerModel {
116 122
117 this.scheduledUpdate = hash.scheduledUpdate 123 this.scheduledUpdate = hash.scheduledUpdate
118 if (this.state) this.state.label = peertubeTranslate(this.state.label, translations) 124 if (this.state) this.state.label = peertubeTranslate(this.state.label, translations)
125
126 this.blacklisted = hash.blacklisted
127 this.blacklistedReason = hash.blacklistedReason
119 } 128 }
120 129
121 isVideoNSFWForUser (user: User, serverConfig: ServerConfig) { 130 isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {
diff --git a/client/src/app/videos/+video-watch/video-watch.component.html b/client/src/app/videos/+video-watch/video-watch.component.html
index f82f1c554..8d4a4a5ca 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.html
+++ b/client/src/app/videos/+video-watch/video-watch.component.html
@@ -1,4 +1,4 @@
1<div class="row"> 1<div class="root-row row">
2 <!-- We need the video container for videojs so we just hide it --> 2 <!-- We need the video container for videojs so we just hide it -->
3 <div id="video-element-wrapper"> 3 <div id="video-element-wrapper">
4 <div *ngIf="remoteServerDown" class="remote-server-down"> 4 <div *ngIf="remoteServerDown" class="remote-server-down">
@@ -17,7 +17,12 @@
17 </div> 17 </div>
18 18
19 <div i18n class="alert alert-info" *ngIf="hasVideoScheduledPublication()"> 19 <div i18n class="alert alert-info" *ngIf="hasVideoScheduledPublication()">
20 This video will be published on {{ video.scheduledUpdate.updateAt | date: 'full' }} 20 This video will be published on {{ video.scheduledUpdate.updateAt | date: 'full' }}.
21 </div>
22
23 <div class="alert alert-danger" *ngIf="video?.blacklisted">
24 <div class="blacklisted-label" i18n>This video is blacklisted.</div>
25 {{ video.blacklistedReason }}
21 </div> 26 </div>
22 27
23 <!-- Video information --> 28 <!-- Video information -->
@@ -98,6 +103,10 @@
98 <span class="icon icon-blacklist"></span> <ng-container i18n>Blacklist</ng-container> 103 <span class="icon icon-blacklist"></span> <ng-container i18n>Blacklist</ng-container>
99 </a> 104 </a>
100 105
106 <a *ngIf="isVideoUnblacklistable()" class="dropdown-item" i18n-title title="Unblacklist this video" href="#" (click)="unblacklistVideo($event)">
107 <span class="icon icon-unblacklist"></span> <ng-container i18n>Unblacklist</ng-container>
108 </a>
109
101 <a *ngIf="isVideoRemovable()" class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)"> 110 <a *ngIf="isVideoRemovable()" class="dropdown-item" i18n-title title="Delete this video" href="#" (click)="removeVideo($event)">
102 <span class="icon icon-delete"></span> <ng-container i18n>Delete</ng-container> 111 <span class="icon icon-delete"></span> <ng-container i18n>Delete</ng-container>
103 </a> 112 </a>
diff --git a/client/src/app/videos/+video-watch/video-watch.component.scss b/client/src/app/videos/+video-watch/video-watch.component.scss
index e63ab7bbd..1354de32e 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.scss
+++ b/client/src/app/videos/+video-watch/video-watch.component.scss
@@ -1,6 +1,14 @@
1@import '_variables'; 1@import '_variables';
2@import '_mixins'; 2@import '_mixins';
3 3
4.root-row {
5 flex-direction: column;
6}
7
8.blacklisted-label {
9 font-weight: $font-semibold;
10}
11
4#video-element-wrapper { 12#video-element-wrapper {
5 background-color: #000; 13 background-color: #000;
6 display: flex; 14 display: flex;
@@ -259,6 +267,10 @@
259 background-image: url('../../../assets/images/video/blacklist.svg'); 267 background-image: url('../../../assets/images/video/blacklist.svg');
260 } 268 }
261 269
270 &.icon-unblacklist {
271 background-image: url('../../../assets/images/global/undo.svg');
272 }
273
262 &.icon-delete { 274 &.icon-delete {
263 background-image: url('../../../assets/images/global/delete-black.svg'); 275 background-image: url('../../../assets/images/global/delete-black.svg');
264 } 276 }
diff --git a/client/src/app/videos/+video-watch/video-watch.component.ts b/client/src/app/videos/+video-watch/video-watch.component.ts
index 878655d4a..bea13ec99 100644
--- a/client/src/app/videos/+video-watch/video-watch.component.ts
+++ b/client/src/app/videos/+video-watch/video-watch.component.ts
@@ -121,7 +121,8 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
121 this.videoCaptionService.listCaptions(uuid) 121 this.videoCaptionService.listCaptions(uuid)
122 ) 122 )
123 .pipe( 123 .pipe(
124 catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 404 ])) 124 // If 401, the video is private or blacklisted so redirect to 404
125 catchError(err => this.restExtractor.redirectTo404IfNotFound(err, [ 400, 401, 404 ]))
125 ) 126 )
126 .subscribe(([ video, captionsResult ]) => { 127 .subscribe(([ video, captionsResult ]) => {
127 const startTime = this.route.snapshot.queryParams.start 128 const startTime = this.route.snapshot.queryParams.start
@@ -217,6 +218,31 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
217 this.videoBlacklistModal.show() 218 this.videoBlacklistModal.show()
218 } 219 }
219 220
221 async unblacklistVideo (event: Event) {
222 event.preventDefault()
223
224 const confirmMessage = this.i18n(
225 'Do you really want to remove this video from the blacklist? It will be available again in the videos list.'
226 )
227
228 const res = await this.confirmService.confirm(confirmMessage, this.i18n('Unblacklist'))
229 if (res === false) return
230
231 this.videoBlacklistService.removeVideoFromBlacklist(this.video.id).subscribe(
232 () => {
233 this.notificationsService.success(
234 this.i18n('Success'),
235 this.i18n('Video {{name}} removed from the blacklist.', { name: this.video.name })
236 )
237
238 this.video.blacklisted = false
239 this.video.blacklistedReason = null
240 },
241
242 err => this.notificationsService.error(this.i18n('Error'), err.message)
243 )
244 }
245
220 isUserLoggedIn () { 246 isUserLoggedIn () {
221 return this.authService.isLoggedIn() 247 return this.authService.isLoggedIn()
222 } 248 }
@@ -229,6 +255,10 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
229 return this.video.isBlackistableBy(this.user) 255 return this.video.isBlackistableBy(this.user)
230 } 256 }
231 257
258 isVideoUnblacklistable () {
259 return this.video.isUnblacklistableBy(this.user)
260 }
261
232 getVideoPoster () { 262 getVideoPoster () {
233 if (!this.video) return '' 263 if (!this.video) return ''
234 264
diff --git a/client/src/assets/images/global/undo.svg b/client/src/assets/images/global/undo.svg
new file mode 100644
index 000000000..f1cca03f7
--- /dev/null
+++ b/client/src/assets/images/global/undo.svg
@@ -0,0 +1,11 @@
1<?xml version="1.0" encoding="UTF-8"?>
2<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">
3 <defs></defs>
4 <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
5 <g id="Artboard-4" transform="translate(-180.000000, -115.000000)" fill="#000">
6 <g id="4" transform="translate(180.000000, 115.000000)">
7 <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>
8 </g>
9 </g>
10 </g>
11</svg>
diff --git a/server/controllers/api/videos/blacklist.ts b/server/controllers/api/videos/blacklist.ts
index 358f339ed..7f803c8e9 100644
--- a/server/controllers/api/videos/blacklist.ts
+++ b/server/controllers/api/videos/blacklist.ts
@@ -1,5 +1,5 @@
1import * as express from 'express' 1import * as express from 'express'
2import { BlacklistedVideo, UserRight, VideoBlacklistCreate } from '../../../../shared' 2import { VideoBlacklist, UserRight, VideoBlacklistCreate } from '../../../../shared'
3import { logger } from '../../../helpers/logger' 3import { logger } from '../../../helpers/logger'
4import { getFormattedObjects } from '../../../helpers/utils' 4import { getFormattedObjects } from '../../../helpers/utils'
5import { 5import {
@@ -87,7 +87,7 @@ async function updateVideoBlacklistController (req: express.Request, res: expres
87async function listBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) { 87async function listBlacklist (req: express.Request, res: express.Response, next: express.NextFunction) {
88 const resultList = await VideoBlacklistModel.listForApi(req.query.start, req.query.count, req.query.sort) 88 const resultList = await VideoBlacklistModel.listForApi(req.query.start, req.query.count, req.query.sort)
89 89
90 return res.json(getFormattedObjects<BlacklistedVideo, VideoBlacklistModel>(resultList.data, resultList.total)) 90 return res.json(getFormattedObjects<VideoBlacklist, VideoBlacklistModel>(resultList.data, resultList.total))
91} 91}
92 92
93async function removeVideoFromBlacklistController (req: express.Request, res: express.Response, next: express.NextFunction) { 93async function removeVideoFromBlacklistController (req: express.Request, res: express.Response, next: express.NextFunction) {
diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos.ts
index 203a00876..77d601a4d 100644
--- a/server/middlewares/validators/videos.ts
+++ b/server/middlewares/validators/videos.ts
@@ -35,6 +35,8 @@ import { VideoShareModel } from '../../models/video/video-share'
35import { authenticate } from '../oauth' 35import { authenticate } from '../oauth'
36import { areValidationErrors } from './utils' 36import { areValidationErrors } from './utils'
37import { cleanUpReqFiles } from '../../helpers/utils' 37import { cleanUpReqFiles } from '../../helpers/utils'
38import { VideoModel } from '../../models/video/video'
39import { UserModel } from '../../models/account/user'
38 40
39const videosAddValidator = getCommonVideoAttributes().concat([ 41const videosAddValidator = getCommonVideoAttributes().concat([
40 body('videofile') 42 body('videofile')
@@ -131,7 +133,25 @@ const videosGetValidator = [
131 if (areValidationErrors(req, res)) return 133 if (areValidationErrors(req, res)) return
132 if (!await isVideoExist(req.params.id, res)) return 134 if (!await isVideoExist(req.params.id, res)) return
133 135
134 const video = res.locals.video 136 const video: VideoModel = res.locals.video
137
138 // Video private or blacklisted
139 if (video.privacy === VideoPrivacy.PRIVATE || video.VideoBlacklist) {
140 authenticate(req, res, () => {
141 const user: UserModel = res.locals.oauth.token.User
142
143 // Only the owner or a user that have blacklist rights can see the video
144 if (video.VideoChannel.Account.userId !== user.id && !user.hasRight(UserRight.MANAGE_VIDEO_BLACKLIST)) {
145 return res.status(403)
146 .json({ error: 'Cannot get this private or blacklisted video.' })
147 .end()
148 }
149
150 return next()
151 })
152
153 return
154 }
135 155
136 // Video is public, anyone can access it 156 // Video is public, anyone can access it
137 if (video.privacy === VideoPrivacy.PUBLIC) return next() 157 if (video.privacy === VideoPrivacy.PUBLIC) return next()
@@ -143,17 +163,6 @@ const videosGetValidator = [
143 // Don't leak this unlisted video 163 // Don't leak this unlisted video
144 return res.status(404).end() 164 return res.status(404).end()
145 } 165 }
146
147 // Video is private, check the user
148 authenticate(req, res, () => {
149 if (video.VideoChannel.Account.userId !== res.locals.oauth.token.User.id) {
150 return res.status(403)
151 .json({ error: 'Cannot get this private video of another user' })
152 .end()
153 }
154
155 return next()
156 })
157 } 166 }
158] 167]
159 168
diff --git a/server/models/video/video-abuse.ts b/server/models/video/video-abuse.ts
index 10a191372..dbb88ca45 100644
--- a/server/models/video/video-abuse.ts
+++ b/server/models/video/video-abuse.ts
@@ -137,7 +137,6 @@ export class VideoAbuseModel extends Model<VideoAbuseModel> {
137 video: { 137 video: {
138 id: this.Video.id, 138 id: this.Video.id,
139 uuid: this.Video.uuid, 139 uuid: this.Video.uuid,
140 url: this.Video.url,
141 name: this.Video.name 140 name: this.Video.name
142 }, 141 },
143 createdAt: this.createdAt 142 createdAt: this.createdAt
diff --git a/server/models/video/video-blacklist.ts b/server/models/video/video-blacklist.ts
index 1b8a338cb..eabc37ef0 100644
--- a/server/models/video/video-blacklist.ts
+++ b/server/models/video/video-blacklist.ts
@@ -16,7 +16,7 @@ import { getSortOnModel, throwIfNotValid } from '../utils'
16import { VideoModel } from './video' 16import { VideoModel } from './video'
17import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist' 17import { isVideoBlacklistReasonValid } from '../../helpers/custom-validators/video-blacklist'
18import { Emailer } from '../../lib/emailer' 18import { Emailer } from '../../lib/emailer'
19import { BlacklistedVideo } from '../../../shared/models/videos' 19import { VideoBlacklist } from '../../../shared/models/videos'
20import { CONSTRAINTS_FIELDS } from '../../initializers' 20import { CONSTRAINTS_FIELDS } from '../../initializers'
21 21
22@Table({ 22@Table({
@@ -68,7 +68,12 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
68 offset: start, 68 offset: start,
69 limit: count, 69 limit: count,
70 order: getSortOnModel(sort.sortModel, sort.sortValue), 70 order: getSortOnModel(sort.sortModel, sort.sortValue),
71 include: [ { model: VideoModel } ] 71 include: [
72 {
73 model: VideoModel,
74 required: true
75 }
76 ]
72 } 77 }
73 78
74 return VideoBlacklistModel.findAndCountAll(query) 79 return VideoBlacklistModel.findAndCountAll(query)
@@ -90,7 +95,7 @@ export class VideoBlacklistModel extends Model<VideoBlacklistModel> {
90 return VideoBlacklistModel.findOne(query) 95 return VideoBlacklistModel.findOne(query)
91 } 96 }
92 97
93 toFormattedJSON (): BlacklistedVideo { 98 toFormattedJSON (): VideoBlacklist {
94 const video = this.Video 99 const video = this.Video
95 100
96 return { 101 return {
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index f3a900bc9..b13dee403 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -127,7 +127,8 @@ export enum ScopeNames {
127 WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS', 127 WITH_ACCOUNT_DETAILS = 'WITH_ACCOUNT_DETAILS',
128 WITH_TAGS = 'WITH_TAGS', 128 WITH_TAGS = 'WITH_TAGS',
129 WITH_FILES = 'WITH_FILES', 129 WITH_FILES = 'WITH_FILES',
130 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE' 130 WITH_SCHEDULED_UPDATE = 'WITH_SCHEDULED_UPDATE',
131 WITH_BLACKLISTED = 'WITH_BLACKLISTED'
131} 132}
132 133
133type AvailableForListOptions = { 134type AvailableForListOptions = {
@@ -374,6 +375,15 @@ type AvailableForListOptions = {
374 [ScopeNames.WITH_TAGS]: { 375 [ScopeNames.WITH_TAGS]: {
375 include: [ () => TagModel ] 376 include: [ () => TagModel ]
376 }, 377 },
378 [ScopeNames.WITH_BLACKLISTED]: {
379 include: [
380 {
381 attributes: [ 'id', 'reason' ],
382 model: () => VideoBlacklistModel,
383 required: false
384 }
385 ]
386 },
377 [ScopeNames.WITH_FILES]: { 387 [ScopeNames.WITH_FILES]: {
378 include: [ 388 include: [
379 { 389 {
@@ -1004,7 +1014,13 @@ export class VideoModel extends Model<VideoModel> {
1004 } 1014 }
1005 1015
1006 return VideoModel 1016 return VideoModel
1007 .scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_SCHEDULED_UPDATE ]) 1017 .scope([
1018 ScopeNames.WITH_TAGS,
1019 ScopeNames.WITH_BLACKLISTED,
1020 ScopeNames.WITH_FILES,
1021 ScopeNames.WITH_ACCOUNT_DETAILS,
1022 ScopeNames.WITH_SCHEDULED_UPDATE
1023 ])
1008 .findById(id, options) 1024 .findById(id, options)
1009 } 1025 }
1010 1026
@@ -1030,7 +1046,13 @@ export class VideoModel extends Model<VideoModel> {
1030 } 1046 }
1031 1047
1032 return VideoModel 1048 return VideoModel
1033 .scope([ ScopeNames.WITH_TAGS, ScopeNames.WITH_FILES, ScopeNames.WITH_ACCOUNT_DETAILS, ScopeNames.WITH_SCHEDULED_UPDATE ]) 1049 .scope([
1050 ScopeNames.WITH_TAGS,
1051 ScopeNames.WITH_BLACKLISTED,
1052 ScopeNames.WITH_FILES,
1053 ScopeNames.WITH_ACCOUNT_DETAILS,
1054 ScopeNames.WITH_SCHEDULED_UPDATE
1055 ])
1034 .findOne(options) 1056 .findOne(options)
1035 } 1057 }
1036 1058
@@ -1276,7 +1298,8 @@ export class VideoModel extends Model<VideoModel> {
1276 toFormattedDetailsJSON (): VideoDetails { 1298 toFormattedDetailsJSON (): VideoDetails {
1277 const formattedJson = this.toFormattedJSON({ 1299 const formattedJson = this.toFormattedJSON({
1278 additionalAttributes: { 1300 additionalAttributes: {
1279 scheduledUpdate: true 1301 scheduledUpdate: true,
1302 blacklistInfo: true
1280 } 1303 }
1281 }) 1304 })
1282 1305
diff --git a/server/tests/utils/videos/video-blacklist.ts b/server/tests/utils/videos/video-blacklist.ts
index 7819f4b25..2c176fde0 100644
--- a/server/tests/utils/videos/video-blacklist.ts
+++ b/server/tests/utils/videos/video-blacklist.ts
@@ -19,7 +19,8 @@ function updateVideoBlacklist (url: string, token: string, videoId: number, reas
19 .send({ reason }) 19 .send({ reason })
20 .set('Accept', 'application/json') 20 .set('Accept', 'application/json')
21 .set('Authorization', 'Bearer ' + token) 21 .set('Authorization', 'Bearer ' + token)
22 .expect(specialStatus)} 22 .expect(specialStatus)
23}
23 24
24function removeVideoFromBlacklist (url: string, token: string, videoId: number | string, specialStatus = 204) { 25function removeVideoFromBlacklist (url: string, token: string, videoId: number | string, specialStatus = 204) {
25 const path = '/api/v1/videos/' + videoId + '/blacklist' 26 const path = '/api/v1/videos/' + videoId + '/blacklist'
diff --git a/shared/models/users/user-right.enum.ts b/shared/models/users/user-right.enum.ts
index ff6ec61f4..142a0474b 100644
--- a/shared/models/users/user-right.enum.ts
+++ b/shared/models/users/user-right.enum.ts
@@ -1,11 +1,14 @@
1export enum UserRight { 1export enum UserRight {
2 ALL, 2 ALL,
3
3 MANAGE_USERS, 4 MANAGE_USERS,
4 MANAGE_SERVER_FOLLOW, 5 MANAGE_SERVER_FOLLOW,
5 MANAGE_VIDEO_ABUSES, 6 MANAGE_VIDEO_ABUSES,
6 MANAGE_VIDEO_BLACKLIST,
7 MANAGE_JOBS, 7 MANAGE_JOBS,
8 MANAGE_CONFIGURATION, 8 MANAGE_CONFIGURATION,
9
10 MANAGE_VIDEO_BLACKLIST,
11
9 REMOVE_ANY_VIDEO, 12 REMOVE_ANY_VIDEO,
10 REMOVE_ANY_VIDEO_CHANNEL, 13 REMOVE_ANY_VIDEO_CHANNEL,
11 REMOVE_ANY_VIDEO_COMMENT, 14 REMOVE_ANY_VIDEO_COMMENT,
diff --git a/shared/models/videos/video-abuse.model.ts b/shared/models/videos/video-abuse.model.ts
index 1fecce037..b2319aa00 100644
--- a/shared/models/videos/video-abuse.model.ts
+++ b/shared/models/videos/video-abuse.model.ts
@@ -14,7 +14,6 @@ export interface VideoAbuse {
14 id: number 14 id: number
15 name: string 15 name: string
16 uuid: string 16 uuid: string
17 url: string
18 } 17 }
19 18
20 createdAt: Date 19 createdAt: Date
diff --git a/shared/models/videos/video-blacklist.model.ts b/shared/models/videos/video-blacklist.model.ts
index a060da357..ef4e5e3a2 100644
--- a/shared/models/videos/video-blacklist.model.ts
+++ b/shared/models/videos/video-blacklist.model.ts
@@ -1,4 +1,4 @@
1export interface BlacklistedVideo { 1export interface VideoBlacklist {
2 id: number 2 id: number
3 createdAt: Date 3 createdAt: Date
4 updatedAt: Date 4 updatedAt: Date