diff options
56 files changed, 665 insertions, 146 deletions
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5a484fbbd..3c8e6413c 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md | |||
@@ -146,3 +146,9 @@ Build the application and run the unit/integration tests: | |||
146 | $ npm run build | 146 | $ npm run build |
147 | $ npm test | 147 | $ npm test |
148 | ``` | 148 | ``` |
149 | |||
150 | If you just want to run 1 test: | ||
151 | |||
152 | ``` | ||
153 | $ npm run mocha -- --exit --require ts-node/register/type-check --bail server/tests/api/index.ts | ||
154 | ``` | ||
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html index 714a3af15..df40bba9f 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html | |||
@@ -62,6 +62,22 @@ | |||
62 | </div> | 62 | </div> |
63 | </div> | 63 | </div> |
64 | 64 | ||
65 | <div class="form-group"> | ||
66 | <label for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label> | ||
67 | <my-help helpType="custom" customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video."></my-help> | ||
68 | |||
69 | <div class="peertube-select-container"> | ||
70 | <select id="instanceDefaultNSFWPolicy" formControlName="instanceDefaultNSFWPolicy"> | ||
71 | <option value="do_not_list">Do not list</option> | ||
72 | <option value="blur">Blur thumbnails</option> | ||
73 | <option value="display">Display</option> | ||
74 | </select> | ||
75 | </div> | ||
76 | <div *ngIf="formErrors.instanceDefaultNSFWPolicy" class="form-error"> | ||
77 | {{ formErrors.instanceDefaultNSFWPolicy }} | ||
78 | </div> | ||
79 | </div> | ||
80 | |||
65 | <div class="inner-form-title">Cache</div> | 81 | <div class="inner-form-title">Cache</div> |
66 | 82 | ||
67 | <div class="form-group"> | 83 | <div class="form-group"> |
diff --git a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts index d73ee71e4..2ab371cbb 100644 --- a/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts +++ b/client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts | |||
@@ -48,6 +48,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
48 | instanceDescription: '', | 48 | instanceDescription: '', |
49 | instanceTerms: '', | 49 | instanceTerms: '', |
50 | instanceDefaultClientRoute: '', | 50 | instanceDefaultClientRoute: '', |
51 | instanceDefaultNSFWPolicy: '', | ||
51 | cachePreviewsSize: '', | 52 | cachePreviewsSize: '', |
52 | signupLimit: '', | 53 | signupLimit: '', |
53 | adminEmail: '', | 54 | adminEmail: '', |
@@ -90,6 +91,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
90 | instanceDescription: [ '' ], | 91 | instanceDescription: [ '' ], |
91 | instanceTerms: [ '' ], | 92 | instanceTerms: [ '' ], |
92 | instanceDefaultClientRoute: [ '' ], | 93 | instanceDefaultClientRoute: [ '' ], |
94 | instanceDefaultNSFWPolicy: [ '' ], | ||
93 | cachePreviewsSize: [ '', CACHE_PREVIEWS_SIZE.VALIDATORS ], | 95 | cachePreviewsSize: [ '', CACHE_PREVIEWS_SIZE.VALIDATORS ], |
94 | signupEnabled: [ ], | 96 | signupEnabled: [ ], |
95 | signupLimit: [ '', SIGNUP_LIMIT.VALIDATORS ], | 97 | signupLimit: [ '', SIGNUP_LIMIT.VALIDATORS ], |
@@ -167,6 +169,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
167 | description: this.form.value['instanceDescription'], | 169 | description: this.form.value['instanceDescription'], |
168 | terms: this.form.value['instanceTerms'], | 170 | terms: this.form.value['instanceTerms'], |
169 | defaultClientRoute: this.form.value['instanceDefaultClientRoute'], | 171 | defaultClientRoute: this.form.value['instanceDefaultClientRoute'], |
172 | defaultNSFWPolicy: this.form.value['instanceDefaultNSFWPolicy'], | ||
170 | customizations: { | 173 | customizations: { |
171 | javascript: this.form.value['customizationJavascript'], | 174 | javascript: this.form.value['customizationJavascript'], |
172 | css: this.form.value['customizationCSS'] | 175 | css: this.form.value['customizationCSS'] |
@@ -224,6 +227,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit { | |||
224 | instanceDescription: this.customConfig.instance.description, | 227 | instanceDescription: this.customConfig.instance.description, |
225 | instanceTerms: this.customConfig.instance.terms, | 228 | instanceTerms: this.customConfig.instance.terms, |
226 | instanceDefaultClientRoute: this.customConfig.instance.defaultClientRoute, | 229 | instanceDefaultClientRoute: this.customConfig.instance.defaultClientRoute, |
230 | instanceDefaultNSFWPolicy: this.customConfig.instance.defaultNSFWPolicy, | ||
227 | cachePreviewsSize: this.customConfig.cache.previews.size, | 231 | cachePreviewsSize: this.customConfig.cache.previews.size, |
228 | signupEnabled: this.customConfig.signup.enabled, | 232 | signupEnabled: this.customConfig.signup.enabled, |
229 | signupLimit: this.customConfig.signup.limit, | 233 | signupLimit: this.customConfig.signup.limit, |
diff --git a/client/src/app/account/account-settings/account-details/account-details.component.html b/client/src/app/account/account-settings/account-details/account-details.component.html index 8f1475a4d..0e8598e9e 100644 --- a/client/src/app/account/account-settings/account-details/account-details.component.html +++ b/client/src/app/account/account-settings/account-details/account-details.component.html | |||
@@ -1,11 +1,15 @@ | |||
1 | <form role="form" (ngSubmit)="updateDetails()" [formGroup]="form"> | 1 | <form role="form" (ngSubmit)="updateDetails()" [formGroup]="form"> |
2 | <div class="form-group"> | 2 | <div class="form-group"> |
3 | <input | 3 | <label for="nsfwPolicy">Default policy on videos containing sensitive content</label> |
4 | type="checkbox" id="displayNSFW" | 4 | <my-help helpType="custom" customHtml="With <strong>Do not list</strong> or <strong>Blur thumbnails</strong>, a confirmation will be requested to watch the video."></my-help> |
5 | formControlName="displayNSFW" | 5 | |
6 | > | 6 | <div class="peertube-select-container"> |
7 | <label for="displayNSFW"></label> | 7 | <select id="nsfwPolicy" formControlName="nsfwPolicy"> |
8 | <label for="displayNSFW">Display videos that contain mature or explicit content</label> | 8 | <option value="do_not_list">Do not list</option> |
9 | <option value="blur">Blur thumbnails</option> | ||
10 | <option value="display">Display</option> | ||
11 | </select> | ||
12 | </div> | ||
9 | </div> | 13 | </div> |
10 | 14 | ||
11 | <div class="form-group"> | 15 | <div class="form-group"> |
diff --git a/client/src/app/account/account-settings/account-details/account-details.component.scss b/client/src/app/account/account-settings/account-details/account-details.component.scss index 4e8dfde1d..ed59e4689 100644 --- a/client/src/app/account/account-settings/account-details/account-details.component.scss +++ b/client/src/app/account/account-settings/account-details/account-details.component.scss | |||
@@ -12,3 +12,9 @@ input[type=submit] { | |||
12 | display: block; | 12 | display: block; |
13 | margin-top: 15px; | 13 | margin-top: 15px; |
14 | } | 14 | } |
15 | |||
16 | .peertube-select-container { | ||
17 | @include peertube-select-container(340px); | ||
18 | |||
19 | margin-bottom: 30px; | ||
20 | } \ No newline at end of file | ||
diff --git a/client/src/app/account/account-settings/account-details/account-details.component.ts b/client/src/app/account/account-settings/account-details/account-details.component.ts index 917f31651..de213717e 100644 --- a/client/src/app/account/account-settings/account-details/account-details.component.ts +++ b/client/src/app/account/account-settings/account-details/account-details.component.ts | |||
@@ -29,7 +29,7 @@ export class AccountDetailsComponent extends FormReactive implements OnInit { | |||
29 | 29 | ||
30 | buildForm () { | 30 | buildForm () { |
31 | this.form = this.formBuilder.group({ | 31 | this.form = this.formBuilder.group({ |
32 | displayNSFW: [ this.user.displayNSFW ], | 32 | nsfwPolicy: [ this.user.nsfwPolicy ], |
33 | autoPlayVideo: [ this.user.autoPlayVideo ] | 33 | autoPlayVideo: [ this.user.autoPlayVideo ] |
34 | }) | 34 | }) |
35 | 35 | ||
@@ -41,10 +41,10 @@ export class AccountDetailsComponent extends FormReactive implements OnInit { | |||
41 | } | 41 | } |
42 | 42 | ||
43 | updateDetails () { | 43 | updateDetails () { |
44 | const displayNSFW = this.form.value['displayNSFW'] | 44 | const nsfwPolicy = this.form.value['nsfwPolicy'] |
45 | const autoPlayVideo = this.form.value['autoPlayVideo'] | 45 | const autoPlayVideo = this.form.value['autoPlayVideo'] |
46 | const details: UserUpdateMe = { | 46 | const details: UserUpdateMe = { |
47 | displayNSFW, | 47 | nsfwPolicy, |
48 | autoPlayVideo | 48 | autoPlayVideo |
49 | } | 49 | } |
50 | 50 | ||
diff --git a/client/src/app/account/account-videos/account-videos.component.html b/client/src/app/account/account-videos/account-videos.component.html index d7e2230b0..66ce3a77b 100644 --- a/client/src/app/account/account-videos/account-videos.component.html +++ b/client/src/app/account/account-videos/account-videos.component.html | |||
@@ -18,6 +18,7 @@ | |||
18 | <div class="video-info"> | 18 | <div class="video-info"> |
19 | <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a> | 19 | <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a> |
20 | <span class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span> | 20 | <span class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span> |
21 | <div class="video-info-private">{{ video.privacy.label }}</div> | ||
21 | </div> | 22 | </div> |
22 | 23 | ||
23 | <!-- Display only once --> | 24 | <!-- Display only once --> |
diff --git a/client/src/app/account/account-videos/account-videos.component.scss b/client/src/app/account/account-videos/account-videos.component.scss index 449cc6af4..f276ea389 100644 --- a/client/src/app/account/account-videos/account-videos.component.scss +++ b/client/src/app/account/account-videos/account-videos.component.scss | |||
@@ -79,8 +79,12 @@ | |||
79 | font-weight: $font-semibold; | 79 | font-weight: $font-semibold; |
80 | } | 80 | } |
81 | 81 | ||
82 | .video-info-date-views { | 82 | .video-info-date-views, .video-info-private { |
83 | font-size: 13px; | 83 | font-size: 13px; |
84 | |||
85 | &.video-info-private { | ||
86 | font-weight: $font-semibold; | ||
87 | } | ||
84 | } | 88 | } |
85 | } | 89 | } |
86 | 90 | ||
diff --git a/client/src/app/core/auth/auth-user.model.ts b/client/src/app/core/auth/auth-user.model.ts index 366eea110..74ed1c580 100644 --- a/client/src/app/core/auth/auth-user.model.ts +++ b/client/src/app/core/auth/auth-user.model.ts | |||
@@ -3,6 +3,7 @@ import { UserRight } from '../../../../../shared/models/users/user-right.enum' | |||
3 | // Do not use the barrel (dependency loop) | 3 | // Do not use the barrel (dependency loop) |
4 | import { hasUserRight, UserRole } from '../../../../../shared/models/users/user-role' | 4 | import { hasUserRight, UserRole } from '../../../../../shared/models/users/user-role' |
5 | import { User, UserConstructorHash } from '../../shared/users/user.model' | 5 | import { User, UserConstructorHash } from '../../shared/users/user.model' |
6 | import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type' | ||
6 | 7 | ||
7 | export type TokenOptions = { | 8 | export type TokenOptions = { |
8 | accessToken: string | 9 | accessToken: string |
@@ -70,7 +71,7 @@ export class AuthUser extends User { | |||
70 | ROLE: 'role', | 71 | ROLE: 'role', |
71 | EMAIL: 'email', | 72 | EMAIL: 'email', |
72 | USERNAME: 'username', | 73 | USERNAME: 'username', |
73 | DISPLAY_NSFW: 'display_nsfw', | 74 | NSFW_POLICY: 'nsfw_policy', |
74 | AUTO_PLAY_VIDEO: 'auto_play_video' | 75 | AUTO_PLAY_VIDEO: 'auto_play_video' |
75 | } | 76 | } |
76 | 77 | ||
@@ -85,7 +86,7 @@ export class AuthUser extends User { | |||
85 | username: peertubeLocalStorage.getItem(this.KEYS.USERNAME), | 86 | username: peertubeLocalStorage.getItem(this.KEYS.USERNAME), |
86 | email: peertubeLocalStorage.getItem(this.KEYS.EMAIL), | 87 | email: peertubeLocalStorage.getItem(this.KEYS.EMAIL), |
87 | role: parseInt(peertubeLocalStorage.getItem(this.KEYS.ROLE), 10) as UserRole, | 88 | role: parseInt(peertubeLocalStorage.getItem(this.KEYS.ROLE), 10) as UserRole, |
88 | displayNSFW: peertubeLocalStorage.getItem(this.KEYS.DISPLAY_NSFW) === 'true', | 89 | nsfwPolicy: peertubeLocalStorage.getItem(this.KEYS.NSFW_POLICY) as NSFWPolicyType, |
89 | autoPlayVideo: peertubeLocalStorage.getItem(this.KEYS.AUTO_PLAY_VIDEO) === 'true' | 90 | autoPlayVideo: peertubeLocalStorage.getItem(this.KEYS.AUTO_PLAY_VIDEO) === 'true' |
90 | }, | 91 | }, |
91 | Tokens.load() | 92 | Tokens.load() |
@@ -99,7 +100,7 @@ export class AuthUser extends User { | |||
99 | peertubeLocalStorage.removeItem(this.KEYS.USERNAME) | 100 | peertubeLocalStorage.removeItem(this.KEYS.USERNAME) |
100 | peertubeLocalStorage.removeItem(this.KEYS.ID) | 101 | peertubeLocalStorage.removeItem(this.KEYS.ID) |
101 | peertubeLocalStorage.removeItem(this.KEYS.ROLE) | 102 | peertubeLocalStorage.removeItem(this.KEYS.ROLE) |
102 | peertubeLocalStorage.removeItem(this.KEYS.DISPLAY_NSFW) | 103 | peertubeLocalStorage.removeItem(this.KEYS.NSFW_POLICY) |
103 | peertubeLocalStorage.removeItem(this.KEYS.AUTO_PLAY_VIDEO) | 104 | peertubeLocalStorage.removeItem(this.KEYS.AUTO_PLAY_VIDEO) |
104 | peertubeLocalStorage.removeItem(this.KEYS.EMAIL) | 105 | peertubeLocalStorage.removeItem(this.KEYS.EMAIL) |
105 | Tokens.flush() | 106 | Tokens.flush() |
@@ -136,7 +137,7 @@ export class AuthUser extends User { | |||
136 | peertubeLocalStorage.setItem(AuthUser.KEYS.USERNAME, this.username) | 137 | peertubeLocalStorage.setItem(AuthUser.KEYS.USERNAME, this.username) |
137 | peertubeLocalStorage.setItem(AuthUser.KEYS.EMAIL, this.email) | 138 | peertubeLocalStorage.setItem(AuthUser.KEYS.EMAIL, this.email) |
138 | peertubeLocalStorage.setItem(AuthUser.KEYS.ROLE, this.role.toString()) | 139 | peertubeLocalStorage.setItem(AuthUser.KEYS.ROLE, this.role.toString()) |
139 | peertubeLocalStorage.setItem(AuthUser.KEYS.DISPLAY_NSFW, JSON.stringify(this.displayNSFW)) | 140 | peertubeLocalStorage.setItem(AuthUser.KEYS.NSFW_POLICY, this.nsfwPolicy.toString()) |
140 | peertubeLocalStorage.setItem(AuthUser.KEYS.AUTO_PLAY_VIDEO, JSON.stringify(this.autoPlayVideo)) | 141 | peertubeLocalStorage.setItem(AuthUser.KEYS.AUTO_PLAY_VIDEO, JSON.stringify(this.autoPlayVideo)) |
141 | this.tokens.save() | 142 | this.tokens.save() |
142 | } | 143 | } |
diff --git a/client/src/app/core/server/server.service.ts b/client/src/app/core/server/server.service.ts index 987d64d2a..a8beb242d 100644 --- a/client/src/app/core/server/server.service.ts +++ b/client/src/app/core/server/server.service.ts | |||
@@ -5,7 +5,6 @@ import 'rxjs/add/operator/do' | |||
5 | import { ReplaySubject } from 'rxjs/ReplaySubject' | 5 | import { ReplaySubject } from 'rxjs/ReplaySubject' |
6 | import { ServerConfig } from '../../../../../shared' | 6 | import { ServerConfig } from '../../../../../shared' |
7 | import { About } from '../../../../../shared/models/server/about.model' | 7 | import { About } from '../../../../../shared/models/server/about.model' |
8 | import { ServerStats } from '../../../../../shared/models/server/server-stats.model' | ||
9 | import { environment } from '../../../environments/environment' | 8 | import { environment } from '../../../environments/environment' |
10 | 9 | ||
11 | @Injectable() | 10 | @Injectable() |
@@ -26,6 +25,7 @@ export class ServerService { | |||
26 | shortDescription: 'PeerTube, a federated (ActivityPub) video streaming platform ' + | 25 | shortDescription: 'PeerTube, a federated (ActivityPub) video streaming platform ' + |
27 | 'using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.', | 26 | 'using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.', |
28 | defaultClientRoute: '', | 27 | defaultClientRoute: '', |
28 | defaultNSFWPolicy: 'do_not_list' as 'do_not_list', | ||
29 | customizations: { | 29 | customizations: { |
30 | javascript: '', | 30 | javascript: '', |
31 | css: '' | 31 | css: '' |
diff --git a/client/src/app/shared/misc/help.component.html b/client/src/app/shared/misc/help.component.html index e37d93b62..3da5701a0 100644 --- a/client/src/app/shared/misc/help.component.html +++ b/client/src/app/shared/misc/help.component.html | |||
@@ -13,6 +13,9 @@ | |||
13 | </ng-template> | 13 | </ng-template> |
14 | 14 | ||
15 | <span | 15 | <span |
16 | class="help-tooltip-button" containerClass="help-tooltip" title="Click to get help" | 16 | class="help-tooltip-button" |
17 | #tooltipDirective="bs-tooltip" [tooltip]="tooltipTemplate" triggers="click" | 17 | title="Get help" |
18 | [popover]="tooltipTemplate" | ||
19 | [placement]="tooltipPlacement" | ||
20 | [outsideClick]="true" | ||
18 | ></span> | 21 | ></span> |
diff --git a/client/src/app/shared/misc/help.component.scss b/client/src/app/shared/misc/help.component.scss index b8bf3a7a5..0df8b86fa 100644 --- a/client/src/app/shared/misc/help.component.scss +++ b/client/src/app/shared/misc/help.component.scss | |||
@@ -12,20 +12,16 @@ | |||
12 | } | 12 | } |
13 | 13 | ||
14 | /deep/ { | 14 | /deep/ { |
15 | .help-tooltip { | 15 | .popover-body { |
16 | opacity: 1 !important; | 16 | text-align: left; |
17 | padding: 10px; | ||
18 | max-width: 300px; | ||
17 | 19 | ||
18 | .tooltip-inner { | 20 | font-size: 13px; |
19 | text-align: left; | 21 | font-family: $main-fonts; |
20 | padding: 10px; | 22 | background-color: #fff; |
21 | max-width: 300px; | 23 | color: #000; |
22 | 24 | box-shadow: 0 0 6px rgba(0, 0, 0, 0.5); | |
23 | font-size: 13px; | ||
24 | font-family: $main-fonts; | ||
25 | background-color: #fff; | ||
26 | color: #000; | ||
27 | box-shadow: 0 0 6px rgba(0, 0, 0, 0.5); | ||
28 | } | ||
29 | 25 | ||
30 | ul { | 26 | ul { |
31 | padding-left: 20px; | 27 | padding-left: 20px; |
diff --git a/client/src/app/shared/misc/help.component.ts b/client/src/app/shared/misc/help.component.ts index 89dd1dae5..0373a63de 100644 --- a/client/src/app/shared/misc/help.component.ts +++ b/client/src/app/shared/misc/help.component.ts | |||
@@ -1,6 +1,5 @@ | |||
1 | import { Component, ElementRef, HostListener, Input, OnInit, ViewChild, OnChanges } from '@angular/core' | 1 | import { Component, Input, OnChanges, OnInit } from '@angular/core' |
2 | import { MarkdownService } from '@app/videos/shared' | 2 | import { MarkdownService } from '@app/videos/shared' |
3 | import { TooltipDirective } from 'ngx-bootstrap/tooltip' | ||
4 | 3 | ||
5 | @Component({ | 4 | @Component({ |
6 | selector: 'my-help', | 5 | selector: 'my-help', |
@@ -9,16 +8,14 @@ import { TooltipDirective } from 'ngx-bootstrap/tooltip' | |||
9 | }) | 8 | }) |
10 | 9 | ||
11 | export class HelpComponent implements OnInit, OnChanges { | 10 | export class HelpComponent implements OnInit, OnChanges { |
12 | @ViewChild('tooltipDirective') tooltipDirective: TooltipDirective | ||
13 | @Input() preHtml = '' | 11 | @Input() preHtml = '' |
14 | @Input() postHtml = '' | 12 | @Input() postHtml = '' |
15 | @Input() customHtml = '' | 13 | @Input() customHtml = '' |
16 | @Input() helpType: 'custom' | 'markdownText' | 'markdownEnhanced' = 'custom' | 14 | @Input() helpType: 'custom' | 'markdownText' | 'markdownEnhanced' = 'custom' |
15 | @Input() tooltipPlacement = 'right' | ||
17 | 16 | ||
18 | mainHtml = '' | 17 | mainHtml = '' |
19 | 18 | ||
20 | constructor (private elementRef: ElementRef) { } | ||
21 | |||
22 | ngOnInit () { | 19 | ngOnInit () { |
23 | this.init() | 20 | this.init() |
24 | } | 21 | } |
@@ -27,15 +24,6 @@ export class HelpComponent implements OnInit, OnChanges { | |||
27 | this.init() | 24 | this.init() |
28 | } | 25 | } |
29 | 26 | ||
30 | @HostListener('document:click', ['$event.target']) | ||
31 | public onClick (targetElement) { | ||
32 | const clickedInside = this.elementRef.nativeElement.contains(targetElement) | ||
33 | |||
34 | if (this.tooltipDirective.isOpen && !clickedInside) { | ||
35 | this.tooltipDirective.hide() | ||
36 | } | ||
37 | } | ||
38 | |||
39 | private init () { | 27 | private init () { |
40 | if (this.helpType === 'custom') { | 28 | if (this.helpType === 'custom') { |
41 | this.mainHtml = this.customHtml | 29 | this.mainHtml = this.customHtml |
diff --git a/client/src/app/shared/users/user.model.ts b/client/src/app/shared/users/user.model.ts index 4a94b032d..2bdc48a1d 100644 --- a/client/src/app/shared/users/user.model.ts +++ b/client/src/app/shared/users/user.model.ts | |||
@@ -1,5 +1,6 @@ | |||
1 | import { hasUserRight, User as UserServerModel, UserRight, UserRole, VideoChannel } from '../../../../../shared' | 1 | import { hasUserRight, User as UserServerModel, UserRight, UserRole, VideoChannel } from '../../../../../shared' |
2 | import { Account } from '../account/account.model' | 2 | import { Account } from '../account/account.model' |
3 | import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type' | ||
3 | 4 | ||
4 | export type UserConstructorHash = { | 5 | export type UserConstructorHash = { |
5 | id: number, | 6 | id: number, |
@@ -7,7 +8,7 @@ export type UserConstructorHash = { | |||
7 | email: string, | 8 | email: string, |
8 | role: UserRole, | 9 | role: UserRole, |
9 | videoQuota?: number, | 10 | videoQuota?: number, |
10 | displayNSFW?: boolean, | 11 | nsfwPolicy?: NSFWPolicyType, |
11 | autoPlayVideo?: boolean, | 12 | autoPlayVideo?: boolean, |
12 | createdAt?: Date, | 13 | createdAt?: Date, |
13 | account?: Account, | 14 | account?: Account, |
@@ -18,7 +19,7 @@ export class User implements UserServerModel { | |||
18 | username: string | 19 | username: string |
19 | email: string | 20 | email: string |
20 | role: UserRole | 21 | role: UserRole |
21 | displayNSFW: boolean | 22 | nsfwPolicy: NSFWPolicyType |
22 | autoPlayVideo: boolean | 23 | autoPlayVideo: boolean |
23 | videoQuota: number | 24 | videoQuota: number |
24 | account: Account | 25 | account: Account |
@@ -40,8 +41,8 @@ export class User implements UserServerModel { | |||
40 | this.videoQuota = hash.videoQuota | 41 | this.videoQuota = hash.videoQuota |
41 | } | 42 | } |
42 | 43 | ||
43 | if (hash.displayNSFW !== undefined) { | 44 | if (hash.nsfwPolicy !== undefined) { |
44 | this.displayNSFW = hash.displayNSFW | 45 | this.nsfwPolicy = hash.nsfwPolicy |
45 | } | 46 | } |
46 | 47 | ||
47 | if (hash.autoPlayVideo !== undefined) { | 48 | if (hash.autoPlayVideo !== undefined) { |
diff --git a/client/src/app/shared/video/video-details.model.ts b/client/src/app/shared/video/video-details.model.ts index a1f7207a2..5397aa37f 100644 --- a/client/src/app/shared/video/video-details.model.ts +++ b/client/src/app/shared/video/video-details.model.ts | |||
@@ -1,17 +1,9 @@ | |||
1 | import { | 1 | import { UserRight, VideoChannel, VideoDetails as VideoDetailsServerModel, VideoFile } from '../../../../../shared' |
2 | UserRight, | ||
3 | VideoChannel, | ||
4 | VideoDetails as VideoDetailsServerModel, | ||
5 | VideoFile, | ||
6 | VideoPrivacy | ||
7 | } from '../../../../../shared' | ||
8 | import { Account } from '../../../../../shared/models/actors' | 2 | import { Account } from '../../../../../shared/models/actors' |
9 | import { VideoConstant } from '../../../../../shared/models/videos/video.model' | ||
10 | import { AuthUser } from '../../core' | 3 | import { AuthUser } from '../../core' |
11 | import { Video } from '../../shared/video/video.model' | 4 | import { Video } from '../../shared/video/video.model' |
12 | 5 | ||
13 | export class VideoDetails extends Video implements VideoDetailsServerModel { | 6 | export class VideoDetails extends Video implements VideoDetailsServerModel { |
14 | privacy: VideoConstant<VideoPrivacy> | ||
15 | descriptionPath: string | 7 | descriptionPath: string |
16 | support: string | 8 | support: string |
17 | channel: VideoChannel | 9 | channel: VideoChannel |
@@ -26,7 +18,6 @@ export class VideoDetails extends Video implements VideoDetailsServerModel { | |||
26 | constructor (hash: VideoDetailsServerModel) { | 18 | constructor (hash: VideoDetailsServerModel) { |
27 | super(hash) | 19 | super(hash) |
28 | 20 | ||
29 | this.privacy = hash.privacy | ||
30 | this.descriptionPath = hash.descriptionPath | 21 | this.descriptionPath = hash.descriptionPath |
31 | this.files = hash.files | 22 | this.files = hash.files |
32 | this.channel = hash.channel | 23 | this.channel = hash.channel |
diff --git a/client/src/app/shared/video/video-miniature.component.html b/client/src/app/shared/video/video-miniature.component.html index f28e9b8d9..233432142 100644 --- a/client/src/app/shared/video/video-miniature.component.html +++ b/client/src/app/shared/video/video-miniature.component.html | |||
@@ -1,11 +1,11 @@ | |||
1 | <div class="video-miniature"> | 1 | <div class="video-miniature"> |
2 | <my-video-thumbnail [video]="video" [nsfw]="isVideoNSFWForThisUser()"></my-video-thumbnail> | 2 | <my-video-thumbnail [video]="video" [nsfw]="isVideoBlur()"></my-video-thumbnail> |
3 | 3 | ||
4 | <div class="video-miniature-information"> | 4 | <div class="video-miniature-information"> |
5 | <span class="video-miniature-name"> | 5 | <span class="video-miniature-name"> |
6 | <a | 6 | <a |
7 | class="video-miniature-name" | 7 | class="video-miniature-name" |
8 | [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoNSFWForThisUser() }" | 8 | [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur() }" |
9 | > | 9 | > |
10 | {{ video.name }} | 10 | {{ video.name }} |
11 | </a> | 11 | </a> |
diff --git a/client/src/app/shared/video/video-miniature.component.ts b/client/src/app/shared/video/video-miniature.component.ts index 4d79a74bb..d3f6dc1f6 100644 --- a/client/src/app/shared/video/video-miniature.component.ts +++ b/client/src/app/shared/video/video-miniature.component.ts | |||
@@ -1,6 +1,7 @@ | |||
1 | import { Component, Input } from '@angular/core' | 1 | import { Component, Input } from '@angular/core' |
2 | import { User } from '../users' | 2 | import { User } from '../users' |
3 | import { Video } from './video.model' | 3 | import { Video } from './video.model' |
4 | import { ServerService } from '@app/core' | ||
4 | 5 | ||
5 | @Component({ | 6 | @Component({ |
6 | selector: 'my-video-miniature', | 7 | selector: 'my-video-miniature', |
@@ -11,7 +12,9 @@ export class VideoMiniatureComponent { | |||
11 | @Input() user: User | 12 | @Input() user: User |
12 | @Input() video: Video | 13 | @Input() video: Video |
13 | 14 | ||
14 | isVideoNSFWForThisUser () { | 15 | constructor (private serverService: ServerService) { } |
15 | return this.video.isVideoNSFWForUser(this.user) | 16 | |
17 | isVideoBlur () { | ||
18 | return this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig()) | ||
16 | } | 19 | } |
17 | } | 20 | } |
diff --git a/client/src/app/shared/video/video.model.ts b/client/src/app/shared/video/video.model.ts index 0c02cbcb9..e25c172e0 100644 --- a/client/src/app/shared/video/video.model.ts +++ b/client/src/app/shared/video/video.model.ts | |||
@@ -1,9 +1,10 @@ | |||
1 | import { Account } from '@app/shared/account/account.model' | 1 | import { Account } from '@app/shared/account/account.model' |
2 | import { User } from '../' | 2 | import { User } from '../' |
3 | import { Video as VideoServerModel } from '../../../../../shared' | 3 | import { Video as VideoServerModel, VideoPrivacy } from '../../../../../shared' |
4 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' | 4 | import { Avatar } from '../../../../../shared/models/avatars/avatar.model' |
5 | import { VideoConstant } from '../../../../../shared/models/videos/video.model' | 5 | import { VideoConstant } from '../../../../../shared/models/videos/video.model' |
6 | import { getAbsoluteAPIUrl } from '../misc/utils' | 6 | import { getAbsoluteAPIUrl } from '../misc/utils' |
7 | import { ServerConfig } from '../../../../../shared/models' | ||
7 | 8 | ||
8 | export class Video implements VideoServerModel { | 9 | export class Video implements VideoServerModel { |
9 | by: string | 10 | by: string |
@@ -13,6 +14,7 @@ export class Video implements VideoServerModel { | |||
13 | category: VideoConstant<number> | 14 | category: VideoConstant<number> |
14 | licence: VideoConstant<number> | 15 | licence: VideoConstant<number> |
15 | language: VideoConstant<number> | 16 | language: VideoConstant<number> |
17 | privacy: VideoConstant<VideoPrivacy> | ||
16 | description: string | 18 | description: string |
17 | duration: number | 19 | duration: number |
18 | durationLabel: string | 20 | durationLabel: string |
@@ -61,6 +63,7 @@ export class Video implements VideoServerModel { | |||
61 | this.category = hash.category | 63 | this.category = hash.category |
62 | this.licence = hash.licence | 64 | this.licence = hash.licence |
63 | this.language = hash.language | 65 | this.language = hash.language |
66 | this.privacy = hash.privacy | ||
64 | this.description = hash.description | 67 | this.description = hash.description |
65 | this.duration = hash.duration | 68 | this.duration = hash.duration |
66 | this.durationLabel = Video.createDurationString(hash.duration) | 69 | this.durationLabel = Video.createDurationString(hash.duration) |
@@ -83,8 +86,14 @@ export class Video implements VideoServerModel { | |||
83 | this.by = Account.CREATE_BY_STRING(hash.account.name, hash.account.host) | 86 | this.by = Account.CREATE_BY_STRING(hash.account.name, hash.account.host) |
84 | } | 87 | } |
85 | 88 | ||
86 | isVideoNSFWForUser (user: User) { | 89 | isVideoNSFWForUser (user: User, serverConfig: ServerConfig) { |
87 | // If the video is NSFW and the user is not logged in, or the user does not want to display NSFW videos... | 90 | // Video is not NSFW, skip |
88 | return (this.nsfw && (!user || user.displayNSFW === false)) | 91 | if (this.nsfw === false) return false |
92 | |||
93 | // Return user setting if logged in | ||
94 | if (user) return user.nsfwPolicy !== 'display' | ||
95 | |||
96 | // Return default instance config | ||
97 | return serverConfig.instance.defaultNSFWPolicy !== 'display' | ||
89 | } | 98 | } |
90 | } | 99 | } |
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.html b/client/src/app/videos/+video-edit/shared/video-edit.component.html index 6d0a1600a..9cd3454a0 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.html +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.html | |||
@@ -100,6 +100,7 @@ | |||
100 | <input type="checkbox" id="nsfw" formControlName="nsfw" /> | 100 | <input type="checkbox" id="nsfw" formControlName="nsfw" /> |
101 | <label for="nsfw"></label> | 101 | <label for="nsfw"></label> |
102 | <label for="nsfw">This video contains mature or explicit content</label> | 102 | <label for="nsfw">This video contains mature or explicit content</label> |
103 | <my-help tooltipPlacement="top" helpType="custom" customHtml="Some instances do not list NSFW videos by default."></my-help> | ||
103 | </div> | 104 | </div> |
104 | 105 | ||
105 | <div class="form-group form-group-checkbox"> | 106 | <div class="form-group form-group-checkbox"> |
diff --git a/client/src/app/videos/+video-edit/shared/video-edit.component.scss b/client/src/app/videos/+video-edit/shared/video-edit.component.scss index 1317f7426..cf64ff589 100644 --- a/client/src/app/videos/+video-edit/shared/video-edit.component.scss +++ b/client/src/app/videos/+video-edit/shared/video-edit.component.scss | |||
@@ -9,6 +9,10 @@ | |||
9 | @include peertube-select-disabled-container(auto); | 9 | @include peertube-select-disabled-container(auto); |
10 | } | 10 | } |
11 | 11 | ||
12 | .form-group-checkbox { | ||
13 | my-help { margin-left: 5px } | ||
14 | } | ||
15 | |||
12 | .video-edit { | 16 | .video-edit { |
13 | height: 100%; | 17 | height: 100%; |
14 | 18 | ||
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 182703cdf..6f6f02378 100644 --- a/client/src/app/videos/+video-watch/video-watch.component.ts +++ b/client/src/app/videos/+video-watch/video-watch.component.ts | |||
@@ -22,6 +22,7 @@ import { VideoDownloadComponent } from './modal/video-download.component' | |||
22 | import { VideoReportComponent } from './modal/video-report.component' | 22 | import { VideoReportComponent } from './modal/video-report.component' |
23 | import { VideoShareComponent } from './modal/video-share.component' | 23 | import { VideoShareComponent } from './modal/video-share.component' |
24 | import { getVideojsOptions } from '../../../assets/player/peertube-player' | 24 | import { getVideojsOptions } from '../../../assets/player/peertube-player' |
25 | import { ServerService } from '@app/core' | ||
25 | 26 | ||
26 | @Component({ | 27 | @Component({ |
27 | selector: 'my-video-watch', | 28 | selector: 'my-video-watch', |
@@ -66,6 +67,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
66 | private confirmService: ConfirmService, | 67 | private confirmService: ConfirmService, |
67 | private metaService: MetaService, | 68 | private metaService: MetaService, |
68 | private authService: AuthService, | 69 | private authService: AuthService, |
70 | private serverService: ServerService, | ||
69 | private notificationsService: NotificationsService, | 71 | private notificationsService: NotificationsService, |
70 | private markdownService: MarkdownService, | 72 | private markdownService: MarkdownService, |
71 | private zone: NgZone, | 73 | private zone: NgZone, |
@@ -335,7 +337,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy { | |||
335 | 337 | ||
336 | this.updateOtherVideosDisplayed() | 338 | this.updateOtherVideosDisplayed() |
337 | 339 | ||
338 | if (this.video.isVideoNSFWForUser(this.user)) { | 340 | if (this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())) { |
339 | const res = await this.confirmService.confirm( | 341 | const res = await this.confirmService.confirm( |
340 | 'This video contains mature or explicit content. Are you sure you want to watch it?', | 342 | 'This video contains mature or explicit content. Are you sure you want to watch it?', |
341 | 'Mature or explicit content' | 343 | 'Mature or explicit content' |
diff --git a/client/src/standalone/videos/embed.html b/client/src/standalone/videos/embed.html index a359255da..b60b03a22 100644 --- a/client/src/standalone/videos/embed.html +++ b/client/src/standalone/videos/embed.html | |||
@@ -11,6 +11,12 @@ | |||
11 | 11 | ||
12 | <body> | 12 | <body> |
13 | 13 | ||
14 | <div id="error-block"> | ||
15 | <h1 id="error-title">Sorry</h1> | ||
16 | |||
17 | <div id="error-content"></div> | ||
18 | </div> | ||
19 | |||
14 | <video id="video-container" class="video-js vjs-peertube-skin"> | 20 | <video id="video-container" class="video-js vjs-peertube-skin"> |
15 | </video> | 21 | </video> |
16 | 22 | ||
diff --git a/client/src/standalone/videos/embed.scss b/client/src/standalone/videos/embed.scss index fc7135d64..37c1df6c4 100644 --- a/client/src/standalone/videos/embed.scss +++ b/client/src/standalone/videos/embed.scss | |||
@@ -4,6 +4,16 @@ | |||
4 | @import '~videojs-dock/dist/videojs-dock.css'; | 4 | @import '~videojs-dock/dist/videojs-dock.css'; |
5 | @import '../../sass/video-js-custom.scss'; | 5 | @import '../../sass/video-js-custom.scss'; |
6 | 6 | ||
7 | [hidden] { | ||
8 | display: none !important; | ||
9 | } | ||
10 | |||
11 | body { | ||
12 | font-family: $main-fonts; | ||
13 | font-weight: $font-regular; | ||
14 | color: #000; | ||
15 | } | ||
16 | |||
7 | video { | 17 | video { |
8 | width: 99%; | 18 | width: 99%; |
9 | } | 19 | } |
@@ -43,3 +53,38 @@ html, body { | |||
43 | } | 53 | } |
44 | } | 54 | } |
45 | } | 55 | } |
56 | |||
57 | #error-block { | ||
58 | display: none; | ||
59 | |||
60 | flex-direction: column; | ||
61 | align-content: center; | ||
62 | justify-content: center; | ||
63 | text-align: center; | ||
64 | background-color: #141313; | ||
65 | width: 100%; | ||
66 | height: 100%; | ||
67 | color: white; | ||
68 | box-sizing: border-box; | ||
69 | font-family: sans-serif; | ||
70 | |||
71 | #error-title { | ||
72 | font-size: 45px; | ||
73 | margin-bottom: 5px; | ||
74 | } | ||
75 | |||
76 | #error-content { | ||
77 | font-size: 24px; | ||
78 | } | ||
79 | } | ||
80 | |||
81 | @media screen and (max-width: 300px) { | ||
82 | #error-block { | ||
83 | font-size: 36px; | ||
84 | |||
85 | #error-content { | ||
86 | font-size: 14px; | ||
87 | } | ||
88 | } | ||
89 | } | ||
90 | |||
diff --git a/client/src/standalone/videos/embed.ts b/client/src/standalone/videos/embed.ts index a99bc586f..aa418d2d4 100644 --- a/client/src/standalone/videos/embed.ts +++ b/client/src/standalone/videos/embed.ts | |||
@@ -9,19 +9,53 @@ function getVideoUrl (id: string) { | |||
9 | return window.location.origin + '/api/v1/videos/' + id | 9 | return window.location.origin + '/api/v1/videos/' + id |
10 | } | 10 | } |
11 | 11 | ||
12 | async function loadVideoInfo (videoId: string): Promise<VideoDetails> { | 12 | function loadVideoInfo (videoId: string): Promise<Response> { |
13 | const response = await fetch(getVideoUrl(videoId)) | 13 | return fetch(getVideoUrl(videoId)) |
14 | return response.json() | 14 | } |
15 | |||
16 | function removeElement (element: HTMLElement) { | ||
17 | element.parentElement.removeChild(element) | ||
18 | } | ||
19 | |||
20 | function displayError (videoElement: HTMLVideoElement, text: string) { | ||
21 | // Remove video element | ||
22 | removeElement(videoElement) | ||
23 | |||
24 | document.title = 'Sorry - ' + text | ||
25 | |||
26 | const errorBlock = document.getElementById('error-block') | ||
27 | errorBlock.style.display = 'flex' | ||
28 | |||
29 | const errorText = document.getElementById('error-content') | ||
30 | errorText.innerHTML = text | ||
31 | } | ||
32 | |||
33 | function videoNotFound (videoElement: HTMLVideoElement) { | ||
34 | const text = 'This video does not exist.' | ||
35 | displayError(videoElement, text) | ||
36 | } | ||
37 | |||
38 | function videoFetchError (videoElement: HTMLVideoElement) { | ||
39 | const text = 'We cannot fetch the video. Please try again later.' | ||
40 | displayError(videoElement, text) | ||
15 | } | 41 | } |
16 | 42 | ||
17 | const urlParts = window.location.href.split('/') | 43 | const urlParts = window.location.href.split('/') |
18 | const videoId = urlParts[urlParts.length - 1] | 44 | const videoId = urlParts[urlParts.length - 1] |
19 | 45 | ||
20 | loadVideoInfo(videoId) | 46 | loadVideoInfo(videoId) |
21 | .then(videoInfo => { | 47 | .then(async response => { |
22 | const videoContainerId = 'video-container' | 48 | const videoContainerId = 'video-container' |
23 | |||
24 | const videoElement = document.getElementById(videoContainerId) as HTMLVideoElement | 49 | const videoElement = document.getElementById(videoContainerId) as HTMLVideoElement |
50 | |||
51 | if (!response.ok) { | ||
52 | if (response.status === 404) return videoNotFound(videoElement) | ||
53 | |||
54 | return videoFetchError(videoElement) | ||
55 | } | ||
56 | |||
57 | const videoInfo: VideoDetails = await response.json() | ||
58 | |||
25 | let autoplay = false | 59 | let autoplay = false |
26 | let startTime = 0 | 60 | let startTime = 0 |
27 | 61 | ||
diff --git a/config/default.yaml b/config/default.yaml index 9f4a76621..25dde72c9 100644 --- a/config/default.yaml +++ b/config/default.yaml | |||
@@ -84,6 +84,9 @@ instance: | |||
84 | description: 'Welcome to this PeerTube instance!' # Support markdown | 84 | description: 'Welcome to this PeerTube instance!' # Support markdown |
85 | terms: 'No terms for now.' # Support markdown | 85 | terms: 'No terms for now.' # Support markdown |
86 | default_client_route: '/videos/trending' | 86 | default_client_route: '/videos/trending' |
87 | # By default, "do_not_list" or "blur" or "display" NSFW videos | ||
88 | # Could be overridden per user with a setting | ||
89 | default_nsfw_policy: 'do_not_list' | ||
87 | customizations: | 90 | customizations: |
88 | javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime | 91 | javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime |
89 | css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime | 92 | css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime |
diff --git a/config/production.yaml.example b/config/production.yaml.example index fc06c51fb..1d7d35c9c 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example | |||
@@ -100,6 +100,9 @@ instance: | |||
100 | description: '' # Support markdown | 100 | description: '' # Support markdown |
101 | terms: '' # Support markdown | 101 | terms: '' # Support markdown |
102 | default_client_route: '/videos/trending' | 102 | default_client_route: '/videos/trending' |
103 | # By default, "do_not_list" or "blur" or "display" NSFW videos | ||
104 | # Could be overridden per user with a setting | ||
105 | default_nsfw_policy: 'do_not_list' | ||
103 | customizations: | 106 | customizations: |
104 | javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime | 107 | javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime |
105 | css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime | 108 | css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime |
diff --git a/config/test.yaml b/config/test.yaml index 7e395db04..020987920 100644 --- a/config/test.yaml +++ b/config/test.yaml | |||
@@ -32,3 +32,6 @@ transcoding: | |||
32 | 480p: true | 32 | 480p: true |
33 | 720p: true | 33 | 720p: true |
34 | 1080p: true | 34 | 1080p: true |
35 | |||
36 | instance: | ||
37 | default_nsfw_policy: 'display' \ No newline at end of file | ||
diff --git a/package.json b/package.json index 6a6275fb9..5f7299b9d 100644 --- a/package.json +++ b/package.json | |||
@@ -47,6 +47,7 @@ | |||
47 | "nodemon": "nodemon", | 47 | "nodemon": "nodemon", |
48 | "ts-node": "ts-node", | 48 | "ts-node": "ts-node", |
49 | "tslint": "tslint", | 49 | "tslint": "tslint", |
50 | "mocha": "mocha", | ||
50 | "travis": "scripty", | 51 | "travis": "scripty", |
51 | "release": "scripty", | 52 | "release": "scripty", |
52 | "client-report": "scripty" | 53 | "client-report": "scripty" |
diff --git a/server/controllers/api/config.ts b/server/controllers/api/config.ts index 88f047adc..e47b71f44 100644 --- a/server/controllers/api/config.ts +++ b/server/controllers/api/config.ts | |||
@@ -46,6 +46,7 @@ async function getConfig (req: express.Request, res: express.Response, next: exp | |||
46 | name: CONFIG.INSTANCE.NAME, | 46 | name: CONFIG.INSTANCE.NAME, |
47 | shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, | 47 | shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, |
48 | defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE, | 48 | defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE, |
49 | defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, | ||
49 | customizations: { | 50 | customizations: { |
50 | javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT, | 51 | javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT, |
51 | css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS | 52 | css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS |
@@ -128,6 +129,7 @@ async function updateCustomConfig (req: express.Request, res: express.Response, | |||
128 | toUpdateJSON.user['video_quota'] = toUpdate.user.videoQuota | 129 | toUpdateJSON.user['video_quota'] = toUpdate.user.videoQuota |
129 | toUpdateJSON.instance['default_client_route'] = toUpdate.instance.defaultClientRoute | 130 | toUpdateJSON.instance['default_client_route'] = toUpdate.instance.defaultClientRoute |
130 | toUpdateJSON.instance['short_description'] = toUpdate.instance.shortDescription | 131 | toUpdateJSON.instance['short_description'] = toUpdate.instance.shortDescription |
132 | toUpdateJSON.instance['default_nsfw_policy'] = toUpdate.instance.defaultNSFWPolicy | ||
131 | 133 | ||
132 | await writeFilePromise(CONFIG.CUSTOM_FILE, JSON.stringify(toUpdateJSON, undefined, 2)) | 134 | await writeFilePromise(CONFIG.CUSTOM_FILE, JSON.stringify(toUpdateJSON, undefined, 2)) |
133 | 135 | ||
@@ -153,6 +155,7 @@ function customConfig (): CustomConfig { | |||
153 | description: CONFIG.INSTANCE.DESCRIPTION, | 155 | description: CONFIG.INSTANCE.DESCRIPTION, |
154 | terms: CONFIG.INSTANCE.TERMS, | 156 | terms: CONFIG.INSTANCE.TERMS, |
155 | defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE, | 157 | defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE, |
158 | defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, | ||
156 | customizations: { | 159 | customizations: { |
157 | css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS, | 160 | css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS, |
158 | javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT | 161 | javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT |
diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts index 56cbf9448..6540adb1c 100644 --- a/server/controllers/api/users.ts +++ b/server/controllers/api/users.ts | |||
@@ -42,6 +42,7 @@ import { AccountVideoRateModel } from '../../models/account/account-video-rate' | |||
42 | import { UserModel } from '../../models/account/user' | 42 | import { UserModel } from '../../models/account/user' |
43 | import { OAuthTokenModel } from '../../models/oauth/oauth-token' | 43 | import { OAuthTokenModel } from '../../models/oauth/oauth-token' |
44 | import { VideoModel } from '../../models/video/video' | 44 | import { VideoModel } from '../../models/video/video' |
45 | import { VideoSortField } from '../../../client/src/app/shared/video/sort-field.type' | ||
45 | 46 | ||
46 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) | 47 | const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) |
47 | const loginRateLimiter = new RateLimit({ | 48 | const loginRateLimiter = new RateLimit({ |
@@ -161,7 +162,13 @@ export { | |||
161 | 162 | ||
162 | async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | 163 | async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) { |
163 | const user = res.locals.oauth.token.User as UserModel | 164 | const user = res.locals.oauth.token.User as UserModel |
164 | const resultList = await VideoModel.listAccountVideosForApi(user.Account.id ,req.query.start, req.query.count, req.query.sort) | 165 | const resultList = await VideoModel.listAccountVideosForApi( |
166 | user.Account.id, | ||
167 | req.query.start as number, | ||
168 | req.query.count as number, | ||
169 | req.query.sort as VideoSortField, | ||
170 | false // Display my NSFW videos | ||
171 | ) | ||
165 | 172 | ||
166 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 173 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
167 | } | 174 | } |
@@ -188,7 +195,7 @@ async function createUser (req: express.Request) { | |||
188 | username: body.username, | 195 | username: body.username, |
189 | password: body.password, | 196 | password: body.password, |
190 | email: body.email, | 197 | email: body.email, |
191 | displayNSFW: false, | 198 | nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, |
192 | autoPlayVideo: true, | 199 | autoPlayVideo: true, |
193 | role: body.role, | 200 | role: body.role, |
194 | videoQuota: body.videoQuota | 201 | videoQuota: body.videoQuota |
@@ -219,7 +226,7 @@ async function registerUser (req: express.Request) { | |||
219 | username: body.username, | 226 | username: body.username, |
220 | password: body.password, | 227 | password: body.password, |
221 | email: body.email, | 228 | email: body.email, |
222 | displayNSFW: false, | 229 | nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, |
223 | autoPlayVideo: true, | 230 | autoPlayVideo: true, |
224 | role: UserRole.USER, | 231 | role: UserRole.USER, |
225 | videoQuota: CONFIG.USER.VIDEO_QUOTA | 232 | videoQuota: CONFIG.USER.VIDEO_QUOTA |
@@ -286,7 +293,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr | |||
286 | 293 | ||
287 | if (body.password !== undefined) user.password = body.password | 294 | if (body.password !== undefined) user.password = body.password |
288 | if (body.email !== undefined) user.email = body.email | 295 | if (body.email !== undefined) user.email = body.email |
289 | if (body.displayNSFW !== undefined) user.displayNSFW = body.displayNSFW | 296 | if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy |
290 | if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo | 297 | if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo |
291 | 298 | ||
292 | await sequelizeTypescript.transaction(async t => { | 299 | await sequelizeTypescript.transaction(async t => { |
diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index b4cd67158..6e8601fa1 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts | |||
@@ -19,13 +19,18 @@ import { | |||
19 | VIDEO_MIMETYPE_EXT, | 19 | VIDEO_MIMETYPE_EXT, |
20 | VIDEO_PRIVACIES | 20 | VIDEO_PRIVACIES |
21 | } from '../../../initializers' | 21 | } from '../../../initializers' |
22 | import { fetchRemoteVideoDescription, getVideoActivityPubUrl, shareVideoByServerAndChannel } from '../../../lib/activitypub' | 22 | import { |
23 | fetchRemoteVideoDescription, | ||
24 | getVideoActivityPubUrl, | ||
25 | shareVideoByServerAndChannel | ||
26 | } from '../../../lib/activitypub' | ||
23 | import { sendCreateVideo, sendCreateView, sendUpdateVideo } from '../../../lib/activitypub/send' | 27 | import { sendCreateVideo, sendCreateView, sendUpdateVideo } from '../../../lib/activitypub/send' |
24 | import { JobQueue } from '../../../lib/job-queue' | 28 | import { JobQueue } from '../../../lib/job-queue' |
25 | import { Redis } from '../../../lib/redis' | 29 | import { Redis } from '../../../lib/redis' |
26 | import { | 30 | import { |
27 | asyncMiddleware, | 31 | asyncMiddleware, |
28 | authenticate, | 32 | authenticate, |
33 | optionalAuthenticate, | ||
29 | paginationValidator, | 34 | paginationValidator, |
30 | setDefaultPagination, | 35 | setDefaultPagination, |
31 | setDefaultSort, | 36 | setDefaultSort, |
@@ -44,6 +49,9 @@ import { blacklistRouter } from './blacklist' | |||
44 | import { videoChannelRouter } from './channel' | 49 | import { videoChannelRouter } from './channel' |
45 | import { videoCommentRouter } from './comment' | 50 | import { videoCommentRouter } from './comment' |
46 | import { rateVideoRouter } from './rate' | 51 | import { rateVideoRouter } from './rate' |
52 | import { User } from '../../../../shared/models/users' | ||
53 | import { VideoFilter } from '../../../../shared/models/videos/video-query.type' | ||
54 | import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type' | ||
47 | 55 | ||
48 | const videosRouter = express.Router() | 56 | const videosRouter = express.Router() |
49 | 57 | ||
@@ -81,6 +89,7 @@ videosRouter.get('/', | |||
81 | videosSortValidator, | 89 | videosSortValidator, |
82 | setDefaultSort, | 90 | setDefaultSort, |
83 | setDefaultPagination, | 91 | setDefaultPagination, |
92 | optionalAuthenticate, | ||
84 | asyncMiddleware(listVideos) | 93 | asyncMiddleware(listVideos) |
85 | ) | 94 | ) |
86 | videosRouter.get('/search', | 95 | videosRouter.get('/search', |
@@ -89,6 +98,7 @@ videosRouter.get('/search', | |||
89 | videosSortValidator, | 98 | videosSortValidator, |
90 | setDefaultSort, | 99 | setDefaultSort, |
91 | setDefaultPagination, | 100 | setDefaultPagination, |
101 | optionalAuthenticate, | ||
92 | asyncMiddleware(searchVideos) | 102 | asyncMiddleware(searchVideos) |
93 | ) | 103 | ) |
94 | videosRouter.put('/:id', | 104 | videosRouter.put('/:id', |
@@ -391,7 +401,13 @@ async function getVideoDescription (req: express.Request, res: express.Response) | |||
391 | } | 401 | } |
392 | 402 | ||
393 | async function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | 403 | async function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) { |
394 | const resultList = await VideoModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.filter) | 404 | const resultList = await VideoModel.listForApi( |
405 | req.query.start as number, | ||
406 | req.query.count as number, | ||
407 | req.query.sort as VideoSortField, | ||
408 | isNSFWHidden(res), | ||
409 | req.query.filter as VideoFilter | ||
410 | ) | ||
395 | 411 | ||
396 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 412 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
397 | } | 413 | } |
@@ -419,11 +435,21 @@ async function removeVideo (req: express.Request, res: express.Response) { | |||
419 | 435 | ||
420 | async function searchVideos (req: express.Request, res: express.Response, next: express.NextFunction) { | 436 | async function searchVideos (req: express.Request, res: express.Response, next: express.NextFunction) { |
421 | const resultList = await VideoModel.searchAndPopulateAccountAndServer( | 437 | const resultList = await VideoModel.searchAndPopulateAccountAndServer( |
422 | req.query.search, | 438 | req.query.search as string, |
423 | req.query.start, | 439 | req.query.start as number, |
424 | req.query.count, | 440 | req.query.count as number, |
425 | req.query.sort | 441 | req.query.sort as VideoSortField, |
442 | isNSFWHidden(res) | ||
426 | ) | 443 | ) |
427 | 444 | ||
428 | return res.json(getFormattedObjects(resultList.data, resultList.total)) | 445 | return res.json(getFormattedObjects(resultList.data, resultList.total)) |
429 | } | 446 | } |
447 | |||
448 | function isNSFWHidden (res: express.Response) { | ||
449 | if (res.locals.oauth) { | ||
450 | const user: User = res.locals.oauth.token.User | ||
451 | if (user) return user.nsfwPolicy === 'do_not_list' | ||
452 | } | ||
453 | |||
454 | return CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list' | ||
455 | } | ||
diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts index 3e384c48a..27ebecc40 100644 --- a/server/controllers/feeds.ts +++ b/server/controllers/feeds.ts | |||
@@ -6,6 +6,7 @@ import * as Feed from 'pfeed' | |||
6 | import { ResultList } from '../../shared/models' | 6 | import { ResultList } from '../../shared/models' |
7 | import { AccountModel } from '../models/account/account' | 7 | import { AccountModel } from '../models/account/account' |
8 | import { cacheRoute } from '../middlewares/cache' | 8 | import { cacheRoute } from '../middlewares/cache' |
9 | import { VideoSortField } from '../../client/src/app/shared/video/sort-field.type' | ||
9 | 10 | ||
10 | const feedsRouter = express.Router() | 11 | const feedsRouter = express.Router() |
11 | 12 | ||
@@ -31,20 +32,22 @@ async function generateFeed (req: express.Request, res: express.Response, next: | |||
31 | 32 | ||
32 | let resultList: ResultList<VideoModel> | 33 | let resultList: ResultList<VideoModel> |
33 | const account: AccountModel = res.locals.account | 34 | const account: AccountModel = res.locals.account |
35 | const hideNSFW = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list' | ||
34 | 36 | ||
35 | if (account) { | 37 | if (account) { |
36 | resultList = await VideoModel.listAccountVideosForApi( | 38 | resultList = await VideoModel.listAccountVideosForApi( |
37 | account.id, | 39 | account.id, |
38 | start, | 40 | start, |
39 | FEEDS.COUNT, | 41 | FEEDS.COUNT, |
40 | req.query.sort, | 42 | req.query.sort as VideoSortField, |
41 | true | 43 | hideNSFW |
42 | ) | 44 | ) |
43 | } else { | 45 | } else { |
44 | resultList = await VideoModel.listForApi( | 46 | resultList = await VideoModel.listForApi( |
45 | start, | 47 | start, |
46 | FEEDS.COUNT, | 48 | FEEDS.COUNT, |
47 | req.query.sort, | 49 | req.query.sort as VideoSortField, |
50 | hideNSFW, | ||
48 | req.query.filter, | 51 | req.query.filter, |
49 | true | 52 | true |
50 | ) | 53 | ) |
diff --git a/server/helpers/custom-validators/users.ts b/server/helpers/custom-validators/users.ts index bbc7cc199..c0acb8218 100644 --- a/server/helpers/custom-validators/users.ts +++ b/server/helpers/custom-validators/users.ts | |||
@@ -1,9 +1,10 @@ | |||
1 | import 'express-validator' | 1 | import 'express-validator' |
2 | import * as validator from 'validator' | 2 | import * as validator from 'validator' |
3 | import { UserRole } from '../../../shared' | 3 | import { UserRole } from '../../../shared' |
4 | import { CONSTRAINTS_FIELDS } from '../../initializers' | 4 | import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers' |
5 | 5 | ||
6 | import { exists, isFileValid } from './misc' | 6 | import { exists, isFileValid } from './misc' |
7 | import { values } from 'lodash' | ||
7 | 8 | ||
8 | const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS | 9 | const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS |
9 | 10 | ||
@@ -29,8 +30,9 @@ function isBoolean (value: any) { | |||
29 | return typeof value === 'boolean' || (typeof value === 'string' && validator.isBoolean(value)) | 30 | return typeof value === 'boolean' || (typeof value === 'string' && validator.isBoolean(value)) |
30 | } | 31 | } |
31 | 32 | ||
32 | function isUserDisplayNSFWValid (value: any) { | 33 | const nsfwPolicies = values(NSFW_POLICY_TYPES) |
33 | return isBoolean(value) | 34 | function isUserNSFWPolicyValid (value: any) { |
35 | return exists(value) && nsfwPolicies.indexOf(value) !== -1 | ||
34 | } | 36 | } |
35 | 37 | ||
36 | function isUserAutoPlayVideoValid (value: any) { | 38 | function isUserAutoPlayVideoValid (value: any) { |
@@ -56,7 +58,7 @@ export { | |||
56 | isUserRoleValid, | 58 | isUserRoleValid, |
57 | isUserVideoQuotaValid, | 59 | isUserVideoQuotaValid, |
58 | isUserUsernameValid, | 60 | isUserUsernameValid, |
59 | isUserDisplayNSFWValid, | 61 | isUserNSFWPolicyValid, |
60 | isUserAutoPlayVideoValid, | 62 | isUserAutoPlayVideoValid, |
61 | isUserDescriptionValid, | 63 | isUserDescriptionValid, |
62 | isAvatarFile | 64 | isAvatarFile |
diff --git a/server/initializers/checker.ts b/server/initializers/checker.ts index 71f303963..739f623c6 100644 --- a/server/initializers/checker.ts +++ b/server/initializers/checker.ts | |||
@@ -5,12 +5,12 @@ import { ApplicationModel } from '../models/application/application' | |||
5 | import { OAuthClientModel } from '../models/oauth/oauth-client' | 5 | import { OAuthClientModel } from '../models/oauth/oauth-client' |
6 | 6 | ||
7 | // Some checks on configuration files | 7 | // Some checks on configuration files |
8 | // Return an error message, or null if everything is okay | ||
8 | function checkConfig () { | 9 | function checkConfig () { |
9 | if (config.has('webserver.host')) { | 10 | const defaultNSFWPolicy = config.get<string>('instance.default_nsfw_policy') |
10 | let errorMessage = '`host` config key was renamed to `hostname` but it seems you still have a `host` key in your configuration files!' | ||
11 | errorMessage += ' Please ensure to rename your `host` configuration to `hostname`.' | ||
12 | 11 | ||
13 | return errorMessage | 12 | if ([ 'do_not_list', 'blur', 'display' ].indexOf(defaultNSFWPolicy) === -1) { |
13 | return 'NSFW policy setting should be "do_not_list" or "blur" or "display" instead of ' + defaultNSFWPolicy | ||
14 | } | 14 | } |
15 | 15 | ||
16 | return null | 16 | return null |
@@ -28,7 +28,8 @@ function checkMissedConfig () { | |||
28 | 'log.level', | 28 | 'log.level', |
29 | 'user.video_quota', | 29 | 'user.video_quota', |
30 | 'cache.previews.size', 'admin.email', 'signup.enabled', 'signup.limit', 'transcoding.enabled', 'transcoding.threads', | 30 | 'cache.previews.size', 'admin.email', 'signup.enabled', 'signup.limit', 'transcoding.enabled', 'transcoding.threads', |
31 | 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route' | 31 | 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route', |
32 | 'instance.default_nsfw_policy' | ||
32 | ] | 33 | ] |
33 | const miss: string[] = [] | 34 | const miss: string[] = [] |
34 | 35 | ||
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 5ee13389d..d1915586a 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts | |||
@@ -6,13 +6,14 @@ import { FollowState } from '../../shared/models/actors' | |||
6 | import { VideoPrivacy } from '../../shared/models/videos' | 6 | import { VideoPrivacy } from '../../shared/models/videos' |
7 | // Do not use barrels, remain constants as independent as possible | 7 | // Do not use barrels, remain constants as independent as possible |
8 | import { buildPath, isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' | 8 | import { buildPath, isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' |
9 | import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type' | ||
9 | 10 | ||
10 | // Use a variable to reload the configuration if we need | 11 | // Use a variable to reload the configuration if we need |
11 | let config: IConfig = require('config') | 12 | let config: IConfig = require('config') |
12 | 13 | ||
13 | // --------------------------------------------------------------------------- | 14 | // --------------------------------------------------------------------------- |
14 | 15 | ||
15 | const LAST_MIGRATION_VERSION = 200 | 16 | const LAST_MIGRATION_VERSION = 205 |
16 | 17 | ||
17 | // --------------------------------------------------------------------------- | 18 | // --------------------------------------------------------------------------- |
18 | 19 | ||
@@ -167,6 +168,7 @@ const CONFIG = { | |||
167 | get DESCRIPTION () { return config.get<string>('instance.description') }, | 168 | get DESCRIPTION () { return config.get<string>('instance.description') }, |
168 | get TERMS () { return config.get<string>('instance.terms') }, | 169 | get TERMS () { return config.get<string>('instance.terms') }, |
169 | get DEFAULT_CLIENT_ROUTE () { return config.get<string>('instance.default_client_route') }, | 170 | get DEFAULT_CLIENT_ROUTE () { return config.get<string>('instance.default_client_route') }, |
171 | get DEFAULT_NSFW_POLICY () { return config.get<NSFWPolicyType>('instance.default_nsfw_policy') }, | ||
170 | CUSTOMIZATIONS: { | 172 | CUSTOMIZATIONS: { |
171 | get JAVASCRIPT () { return config.get<string>('instance.customizations.javascript') }, | 173 | get JAVASCRIPT () { return config.get<string>('instance.customizations.javascript') }, |
172 | get CSS () { return config.get<string>('instance.customizations.css') } | 174 | get CSS () { return config.get<string>('instance.customizations.css') } |
@@ -378,6 +380,12 @@ const BCRYPT_SALT_SIZE = 10 | |||
378 | 380 | ||
379 | const USER_PASSWORD_RESET_LIFETIME = 60000 * 5 // 5 minutes | 381 | const USER_PASSWORD_RESET_LIFETIME = 60000 * 5 // 5 minutes |
380 | 382 | ||
383 | const NSFW_POLICY_TYPES: { [ id: string]: NSFWPolicyType } = { | ||
384 | DO_NOT_LIST: 'do_not_list', | ||
385 | BLUR: 'blur', | ||
386 | DISPLAY: 'display' | ||
387 | } | ||
388 | |||
381 | // --------------------------------------------------------------------------- | 389 | // --------------------------------------------------------------------------- |
382 | 390 | ||
383 | // Express static paths (router) | 391 | // Express static paths (router) |
@@ -474,6 +482,7 @@ export { | |||
474 | PRIVATE_RSA_KEY_SIZE, | 482 | PRIVATE_RSA_KEY_SIZE, |
475 | SORTABLE_COLUMNS, | 483 | SORTABLE_COLUMNS, |
476 | FEEDS, | 484 | FEEDS, |
485 | NSFW_POLICY_TYPES, | ||
477 | STATIC_MAX_AGE, | 486 | STATIC_MAX_AGE, |
478 | STATIC_PATHS, | 487 | STATIC_PATHS, |
479 | ACTIVITY_PUB, | 488 | ACTIVITY_PUB, |
diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts index 09c6d5473..b0084b368 100644 --- a/server/initializers/installer.ts +++ b/server/initializers/installer.ts | |||
@@ -120,6 +120,7 @@ async function createOAuthAdminIfNotExist () { | |||
120 | email, | 120 | email, |
121 | password, | 121 | password, |
122 | role, | 122 | role, |
123 | nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY, | ||
123 | videoQuota: -1 | 124 | videoQuota: -1 |
124 | } | 125 | } |
125 | const user = new UserModel(userData) | 126 | const user = new UserModel(userData) |
diff --git a/server/initializers/migrations/0205-user-nsfw-policy.ts b/server/initializers/migrations/0205-user-nsfw-policy.ts new file mode 100644 index 000000000..d0f6e8962 --- /dev/null +++ b/server/initializers/migrations/0205-user-nsfw-policy.ts | |||
@@ -0,0 +1,46 @@ | |||
1 | import * as Sequelize from 'sequelize' | ||
2 | |||
3 | async function up (utils: { | ||
4 | transaction: Sequelize.Transaction, | ||
5 | queryInterface: Sequelize.QueryInterface, | ||
6 | sequelize: Sequelize.Sequelize | ||
7 | }): Promise<void> { | ||
8 | |||
9 | { | ||
10 | const data = { | ||
11 | type: Sequelize.ENUM('do_not_list', 'blur', 'display'), | ||
12 | allowNull: true, | ||
13 | defaultValue: null | ||
14 | } | ||
15 | await utils.queryInterface.addColumn('user', 'nsfwPolicy', data) | ||
16 | } | ||
17 | |||
18 | { | ||
19 | const query = 'UPDATE "user" SET "nsfwPolicy" = \'do_not_list\'' | ||
20 | await utils.sequelize.query(query) | ||
21 | } | ||
22 | |||
23 | { | ||
24 | const query = 'UPDATE "user" SET "nsfwPolicy" = \'display\' WHERE "displayNSFW" = true' | ||
25 | await utils.sequelize.query(query) | ||
26 | } | ||
27 | |||
28 | { | ||
29 | const query = 'ALTER TABLE "user" ALTER COLUMN "nsfwPolicy" SET NOT NULL' | ||
30 | await utils.sequelize.query(query) | ||
31 | } | ||
32 | |||
33 | { | ||
34 | await utils.queryInterface.removeColumn('user', 'displayNSFW') | ||
35 | } | ||
36 | |||
37 | } | ||
38 | |||
39 | function down (options) { | ||
40 | throw new Error('Not implemented.') | ||
41 | } | ||
42 | |||
43 | export { | ||
44 | up, | ||
45 | down | ||
46 | } | ||
diff --git a/server/middlewares/oauth.ts b/server/middlewares/oauth.ts index 41a3fb718..a6f28dd5b 100644 --- a/server/middlewares/oauth.ts +++ b/server/middlewares/oauth.ts | |||
@@ -2,6 +2,7 @@ import * as express from 'express' | |||
2 | import * as OAuthServer from 'express-oauth-server' | 2 | import * as OAuthServer from 'express-oauth-server' |
3 | import 'express-validator' | 3 | import 'express-validator' |
4 | import { OAUTH_LIFETIME } from '../initializers' | 4 | import { OAUTH_LIFETIME } from '../initializers' |
5 | import { logger } from '../helpers/logger' | ||
5 | 6 | ||
6 | const oAuthServer = new OAuthServer({ | 7 | const oAuthServer = new OAuthServer({ |
7 | useErrorHandler: true, | 8 | useErrorHandler: true, |
@@ -13,6 +14,8 @@ const oAuthServer = new OAuthServer({ | |||
13 | function authenticate (req: express.Request, res: express.Response, next: express.NextFunction) { | 14 | function authenticate (req: express.Request, res: express.Response, next: express.NextFunction) { |
14 | oAuthServer.authenticate()(req, res, err => { | 15 | oAuthServer.authenticate()(req, res, err => { |
15 | if (err) { | 16 | if (err) { |
17 | logger.warn('Cannot authenticate.', { err }) | ||
18 | |||
16 | return res.status(err.status) | 19 | return res.status(err.status) |
17 | .json({ | 20 | .json({ |
18 | error: 'Token is invalid.', | 21 | error: 'Token is invalid.', |
@@ -25,6 +28,12 @@ function authenticate (req: express.Request, res: express.Response, next: expres | |||
25 | }) | 28 | }) |
26 | } | 29 | } |
27 | 30 | ||
31 | function optionalAuthenticate (req: express.Request, res: express.Response, next: express.NextFunction) { | ||
32 | if (req.header('authorization')) return authenticate(req, res, next) | ||
33 | |||
34 | return next() | ||
35 | } | ||
36 | |||
28 | function token (req: express.Request, res: express.Response, next: express.NextFunction) { | 37 | function token (req: express.Request, res: express.Response, next: express.NextFunction) { |
29 | return oAuthServer.token()(req, res, err => { | 38 | return oAuthServer.token()(req, res, err => { |
30 | if (err) { | 39 | if (err) { |
@@ -44,5 +53,6 @@ function token (req: express.Request, res: express.Response, next: express.NextF | |||
44 | 53 | ||
45 | export { | 54 | export { |
46 | authenticate, | 55 | authenticate, |
56 | optionalAuthenticate, | ||
47 | token | 57 | token |
48 | } | 58 | } |
diff --git a/server/middlewares/validators/config.ts b/server/middlewares/validators/config.ts index ee6f6efa4..f58c0676c 100644 --- a/server/middlewares/validators/config.ts +++ b/server/middlewares/validators/config.ts | |||
@@ -1,6 +1,6 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { body } from 'express-validator/check' | 2 | import { body } from 'express-validator/check' |
3 | import { isUserVideoQuotaValid } from '../../helpers/custom-validators/users' | 3 | import { isUserNSFWPolicyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users' |
4 | import { logger } from '../../helpers/logger' | 4 | import { logger } from '../../helpers/logger' |
5 | import { areValidationErrors } from './utils' | 5 | import { areValidationErrors } from './utils' |
6 | 6 | ||
@@ -9,6 +9,7 @@ const customConfigUpdateValidator = [ | |||
9 | body('instance.description').exists().withMessage('Should have a valid instance description'), | 9 | body('instance.description').exists().withMessage('Should have a valid instance description'), |
10 | body('instance.terms').exists().withMessage('Should have a valid instance terms'), | 10 | body('instance.terms').exists().withMessage('Should have a valid instance terms'), |
11 | body('instance.defaultClientRoute').exists().withMessage('Should have a valid instance default client route'), | 11 | body('instance.defaultClientRoute').exists().withMessage('Should have a valid instance default client route'), |
12 | body('instance.defaultNSFWPolicy').custom(isUserNSFWPolicyValid).withMessage('Should have a valid NSFW policy'), | ||
12 | body('instance.customizations.css').exists().withMessage('Should have a valid instance CSS customization'), | 13 | body('instance.customizations.css').exists().withMessage('Should have a valid instance CSS customization'), |
13 | body('instance.customizations.javascript').exists().withMessage('Should have a valid instance JavaScript customization'), | 14 | body('instance.customizations.javascript').exists().withMessage('Should have a valid instance JavaScript customization'), |
14 | body('cache.previews.size').isInt().withMessage('Should have a valid previews size'), | 15 | body('cache.previews.size').isInt().withMessage('Should have a valid previews size'), |
diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index 6ea3d0b6c..5dd8caa3f 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts | |||
@@ -8,7 +8,7 @@ import { | |||
8 | isAvatarFile, | 8 | isAvatarFile, |
9 | isUserAutoPlayVideoValid, | 9 | isUserAutoPlayVideoValid, |
10 | isUserDescriptionValid, | 10 | isUserDescriptionValid, |
11 | isUserDisplayNSFWValid, | 11 | isUserNSFWPolicyValid, |
12 | isUserPasswordValid, | 12 | isUserPasswordValid, |
13 | isUserRoleValid, | 13 | isUserRoleValid, |
14 | isUserUsernameValid, | 14 | isUserUsernameValid, |
@@ -101,7 +101,7 @@ const usersUpdateMeValidator = [ | |||
101 | body('description').optional().custom(isUserDescriptionValid).withMessage('Should have a valid description'), | 101 | body('description').optional().custom(isUserDescriptionValid).withMessage('Should have a valid description'), |
102 | body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'), | 102 | body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'), |
103 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), | 103 | body('email').optional().isEmail().withMessage('Should have a valid email attribute'), |
104 | body('displayNSFW').optional().custom(isUserDisplayNSFWValid).withMessage('Should have a valid display Not Safe For Work attribute'), | 104 | body('nsfwPolicy').optional().custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'), |
105 | body('autoPlayVideo').optional().custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'), | 105 | body('autoPlayVideo').optional().custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'), |
106 | 106 | ||
107 | (req: express.Request, res: express.Response, next: express.NextFunction) => { | 107 | (req: express.Request, res: express.Response, next: express.NextFunction) => { |
diff --git a/server/models/account/user.ts b/server/models/account/user.ts index 8afd246b2..56af2f30a 100644 --- a/server/models/account/user.ts +++ b/server/models/account/user.ts | |||
@@ -21,7 +21,7 @@ import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared' | |||
21 | import { User, UserRole } from '../../../shared/models/users' | 21 | import { User, UserRole } from '../../../shared/models/users' |
22 | import { | 22 | import { |
23 | isUserAutoPlayVideoValid, | 23 | isUserAutoPlayVideoValid, |
24 | isUserDisplayNSFWValid, | 24 | isUserNSFWPolicyValid, |
25 | isUserPasswordValid, | 25 | isUserPasswordValid, |
26 | isUserRoleValid, | 26 | isUserRoleValid, |
27 | isUserUsernameValid, | 27 | isUserUsernameValid, |
@@ -32,6 +32,9 @@ import { OAuthTokenModel } from '../oauth/oauth-token' | |||
32 | import { getSort, throwIfNotValid } from '../utils' | 32 | import { getSort, throwIfNotValid } from '../utils' |
33 | import { VideoChannelModel } from '../video/video-channel' | 33 | import { VideoChannelModel } from '../video/video-channel' |
34 | import { AccountModel } from './account' | 34 | import { AccountModel } from './account' |
35 | import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type' | ||
36 | import { values } from 'lodash' | ||
37 | import { NSFW_POLICY_TYPES } from '../../initializers' | ||
35 | 38 | ||
36 | @DefaultScope({ | 39 | @DefaultScope({ |
37 | include: [ | 40 | include: [ |
@@ -83,10 +86,9 @@ export class UserModel extends Model<UserModel> { | |||
83 | email: string | 86 | email: string |
84 | 87 | ||
85 | @AllowNull(false) | 88 | @AllowNull(false) |
86 | @Default(false) | 89 | @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy')) |
87 | @Is('UserDisplayNSFW', value => throwIfNotValid(value, isUserDisplayNSFWValid, 'display NSFW boolean')) | 90 | @Column(DataType.ENUM(values(NSFW_POLICY_TYPES))) |
88 | @Column | 91 | nsfwPolicy: NSFWPolicyType |
89 | displayNSFW: boolean | ||
90 | 92 | ||
91 | @AllowNull(false) | 93 | @AllowNull(false) |
92 | @Default(true) | 94 | @Default(true) |
@@ -265,7 +267,7 @@ export class UserModel extends Model<UserModel> { | |||
265 | id: this.id, | 267 | id: this.id, |
266 | username: this.username, | 268 | username: this.username, |
267 | email: this.email, | 269 | email: this.email, |
268 | displayNSFW: this.displayNSFW, | 270 | nsfwPolicy: this.nsfwPolicy, |
269 | autoPlayVideo: this.autoPlayVideo, | 271 | autoPlayVideo: this.autoPlayVideo, |
270 | role: this.role, | 272 | role: this.role, |
271 | roleLabel: USER_ROLE_LABELS[ this.role ], | 273 | roleLabel: USER_ROLE_LABELS[ this.role ], |
diff --git a/server/models/video/video.ts b/server/models/video/video.ts index a7923b477..aef75d206 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts | |||
@@ -95,7 +95,7 @@ enum ScopeNames { | |||
95 | } | 95 | } |
96 | 96 | ||
97 | @Scopes({ | 97 | @Scopes({ |
98 | [ScopeNames.AVAILABLE_FOR_LIST]: (actorId: number, filter?: VideoFilter, withFiles?: boolean) => { | 98 | [ScopeNames.AVAILABLE_FOR_LIST]: (actorId: number, hideNSFW: boolean, filter?: VideoFilter, withFiles?: boolean) => { |
99 | const query: IFindOptions<VideoModel> = { | 99 | const query: IFindOptions<VideoModel> = { |
100 | where: { | 100 | where: { |
101 | id: { | 101 | id: { |
@@ -161,6 +161,11 @@ enum ScopeNames { | |||
161 | }) | 161 | }) |
162 | } | 162 | } |
163 | 163 | ||
164 | // Hide nsfw videos? | ||
165 | if (hideNSFW === true) { | ||
166 | query.where['nsfw'] = false | ||
167 | } | ||
168 | |||
164 | return query | 169 | return query |
165 | }, | 170 | }, |
166 | [ScopeNames.WITH_ACCOUNT_DETAILS]: { | 171 | [ScopeNames.WITH_ACCOUNT_DETAILS]: { |
@@ -640,7 +645,7 @@ export class VideoModel extends Model<VideoModel> { | |||
640 | }) | 645 | }) |
641 | } | 646 | } |
642 | 647 | ||
643 | static listAccountVideosForApi (accountId: number, start: number, count: number, sort: string, withFiles = false) { | 648 | static listAccountVideosForApi (accountId: number, start: number, count: number, sort: string, hideNSFW: boolean, withFiles = false) { |
644 | const query: IFindOptions<VideoModel> = { | 649 | const query: IFindOptions<VideoModel> = { |
645 | offset: start, | 650 | offset: start, |
646 | limit: count, | 651 | limit: count, |
@@ -669,6 +674,12 @@ export class VideoModel extends Model<VideoModel> { | |||
669 | }) | 674 | }) |
670 | } | 675 | } |
671 | 676 | ||
677 | if (hideNSFW === true) { | ||
678 | query.where = { | ||
679 | nsfw: false | ||
680 | } | ||
681 | } | ||
682 | |||
672 | return VideoModel.findAndCountAll(query).then(({ rows, count }) => { | 683 | return VideoModel.findAndCountAll(query).then(({ rows, count }) => { |
673 | return { | 684 | return { |
674 | data: rows, | 685 | data: rows, |
@@ -677,7 +688,7 @@ export class VideoModel extends Model<VideoModel> { | |||
677 | }) | 688 | }) |
678 | } | 689 | } |
679 | 690 | ||
680 | static async listForApi (start: number, count: number, sort: string, filter?: VideoFilter, withFiles = false) { | 691 | static async listForApi (start: number, count: number, sort: string, hideNSFW: boolean, filter?: VideoFilter, withFiles = false) { |
681 | const query = { | 692 | const query = { |
682 | offset: start, | 693 | offset: start, |
683 | limit: count, | 694 | limit: count, |
@@ -685,8 +696,7 @@ export class VideoModel extends Model<VideoModel> { | |||
685 | } | 696 | } |
686 | 697 | ||
687 | const serverActor = await getServerActor() | 698 | const serverActor = await getServerActor() |
688 | 699 | return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, hideNSFW, filter, withFiles ] }) | |
689 | return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, filter, withFiles ] }) | ||
690 | .findAndCountAll(query) | 700 | .findAndCountAll(query) |
691 | .then(({ rows, count }) => { | 701 | .then(({ rows, count }) => { |
692 | return { | 702 | return { |
@@ -696,7 +706,7 @@ export class VideoModel extends Model<VideoModel> { | |||
696 | }) | 706 | }) |
697 | } | 707 | } |
698 | 708 | ||
699 | static async searchAndPopulateAccountAndServer (value: string, start: number, count: number, sort: string) { | 709 | static async searchAndPopulateAccountAndServer (value: string, start: number, count: number, sort: string, hideNSFW: boolean) { |
700 | const query: IFindOptions<VideoModel> = { | 710 | const query: IFindOptions<VideoModel> = { |
701 | offset: start, | 711 | offset: start, |
702 | limit: count, | 712 | limit: count, |
@@ -724,7 +734,7 @@ export class VideoModel extends Model<VideoModel> { | |||
724 | 734 | ||
725 | const serverActor = await getServerActor() | 735 | const serverActor = await getServerActor() |
726 | 736 | ||
727 | return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id ] }) | 737 | return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, hideNSFW ] }) |
728 | .findAndCountAll(query) | 738 | .findAndCountAll(query) |
729 | .then(({ rows, count }) => { | 739 | .then(({ rows, count }) => { |
730 | return { | 740 | return { |
@@ -874,6 +884,13 @@ export class VideoModel extends Model<VideoModel> { | |||
874 | return languageLabel | 884 | return languageLabel |
875 | } | 885 | } |
876 | 886 | ||
887 | private static getPrivacyLabel (id: number) { | ||
888 | let privacyLabel = VIDEO_PRIVACIES[id] | ||
889 | if (!privacyLabel) privacyLabel = 'Unknown' | ||
890 | |||
891 | return privacyLabel | ||
892 | } | ||
893 | |||
877 | getOriginalFile () { | 894 | getOriginalFile () { |
878 | if (Array.isArray(this.VideoFiles) === false) return undefined | 895 | if (Array.isArray(this.VideoFiles) === false) return undefined |
879 | 896 | ||
@@ -927,8 +944,11 @@ export class VideoModel extends Model<VideoModel> { | |||
927 | return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) | 944 | return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) |
928 | } | 945 | } |
929 | 946 | ||
930 | createTorrentAndSetInfoHash = async function (videoFile: VideoFileModel) { | 947 | async createTorrentAndSetInfoHash (videoFile: VideoFileModel) { |
931 | const options = { | 948 | const options = { |
949 | // Keep the extname, it's used by the client to stream the file inside a web browser | ||
950 | name: `${this.name} ${videoFile.resolution}p${videoFile.extname}`, | ||
951 | createdBy: 'PeerTube', | ||
932 | announceList: [ | 952 | announceList: [ |
933 | [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ], | 953 | [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ], |
934 | [ CONFIG.WEBSERVER.URL + '/tracker/announce' ] | 954 | [ CONFIG.WEBSERVER.URL + '/tracker/announce' ] |
@@ -980,6 +1000,10 @@ export class VideoModel extends Model<VideoModel> { | |||
980 | id: this.language, | 1000 | id: this.language, |
981 | label: VideoModel.getLanguageLabel(this.language) | 1001 | label: VideoModel.getLanguageLabel(this.language) |
982 | }, | 1002 | }, |
1003 | privacy: { | ||
1004 | id: this.privacy, | ||
1005 | label: VideoModel.getPrivacyLabel(this.privacy) | ||
1006 | }, | ||
983 | nsfw: this.nsfw, | 1007 | nsfw: this.nsfw, |
984 | description: this.getTruncatedDescription(), | 1008 | description: this.getTruncatedDescription(), |
985 | isLocal: this.isOwned(), | 1009 | isLocal: this.isOwned(), |
@@ -1006,15 +1030,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1006 | toFormattedDetailsJSON (): VideoDetails { | 1030 | toFormattedDetailsJSON (): VideoDetails { |
1007 | const formattedJson = this.toFormattedJSON() | 1031 | const formattedJson = this.toFormattedJSON() |
1008 | 1032 | ||
1009 | // Maybe our server is not up to date and there are new privacy settings since our version | ||
1010 | let privacyLabel = VIDEO_PRIVACIES[this.privacy] | ||
1011 | if (!privacyLabel) privacyLabel = 'Unknown' | ||
1012 | |||
1013 | const detailsJson = { | 1033 | const detailsJson = { |
1014 | privacy: { | ||
1015 | id: this.privacy, | ||
1016 | label: privacyLabel | ||
1017 | }, | ||
1018 | support: this.support, | 1034 | support: this.support, |
1019 | descriptionPath: this.getDescriptionPath(), | 1035 | descriptionPath: this.getDescriptionPath(), |
1020 | channel: this.VideoChannel.toFormattedJSON(), | 1036 | channel: this.VideoChannel.toFormattedJSON(), |
@@ -1227,7 +1243,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1227 | return peertubeTruncate(this.description, maxLength) | 1243 | return peertubeTruncate(this.description, maxLength) |
1228 | } | 1244 | } |
1229 | 1245 | ||
1230 | optimizeOriginalVideofile = async function () { | 1246 | async optimizeOriginalVideofile () { |
1231 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | 1247 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR |
1232 | const newExtname = '.mp4' | 1248 | const newExtname = '.mp4' |
1233 | const inputVideoFile = this.getOriginalFile() | 1249 | const inputVideoFile = this.getOriginalFile() |
@@ -1264,7 +1280,7 @@ export class VideoModel extends Model<VideoModel> { | |||
1264 | } | 1280 | } |
1265 | } | 1281 | } |
1266 | 1282 | ||
1267 | transcodeOriginalVideofile = async function (resolution: VideoResolution, isPortraitMode: boolean) { | 1283 | async transcodeOriginalVideofile (resolution: VideoResolution, isPortraitMode: boolean) { |
1268 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR | 1284 | const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR |
1269 | const extname = '.mp4' | 1285 | const extname = '.mp4' |
1270 | 1286 | ||
diff --git a/server/tests/api/check-params/config.ts b/server/tests/api/check-params/config.ts index 3fe517fad..58b780f38 100644 --- a/server/tests/api/check-params/config.ts +++ b/server/tests/api/check-params/config.ts | |||
@@ -6,7 +6,7 @@ import { CustomConfig } from '../../../../shared/models/server/custom-config.mod | |||
6 | 6 | ||
7 | import { | 7 | import { |
8 | createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, runServer, ServerInfo, | 8 | createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, runServer, ServerInfo, |
9 | setAccessTokensToServers, userLogin | 9 | setAccessTokensToServers, userLogin, immutableAssign |
10 | } from '../../utils' | 10 | } from '../../utils' |
11 | 11 | ||
12 | describe('Test config API validators', function () { | 12 | describe('Test config API validators', function () { |
@@ -20,6 +20,7 @@ describe('Test config API validators', function () { | |||
20 | description: 'my super description', | 20 | description: 'my super description', |
21 | terms: 'my super terms', | 21 | terms: 'my super terms', |
22 | defaultClientRoute: '/videos/recently-added', | 22 | defaultClientRoute: '/videos/recently-added', |
23 | defaultNSFWPolicy: 'blur', | ||
23 | customizations: { | 24 | customizations: { |
24 | javascript: 'alert("coucou")', | 25 | javascript: 'alert("coucou")', |
25 | css: 'body { background-color: red; }' | 26 | css: 'body { background-color: red; }' |
@@ -122,6 +123,22 @@ describe('Test config API validators', function () { | |||
122 | }) | 123 | }) |
123 | }) | 124 | }) |
124 | 125 | ||
126 | it('Should fail with a bad default NSFW policy', async function () { | ||
127 | const newUpdateParams = immutableAssign(updateParams, { | ||
128 | instance: { | ||
129 | defaultNSFWPolicy: 'hello' | ||
130 | } | ||
131 | }) | ||
132 | |||
133 | await makePutBodyRequest({ | ||
134 | url: server.url, | ||
135 | path, | ||
136 | fields: newUpdateParams, | ||
137 | token: server.accessToken, | ||
138 | statusCodeExpected: 400 | ||
139 | }) | ||
140 | }) | ||
141 | |||
125 | it('Should success with the correct parameters', async function () { | 142 | it('Should success with the correct parameters', async function () { |
126 | await makePutBodyRequest({ | 143 | await makePutBodyRequest({ |
127 | url: server.url, | 144 | url: server.url, |
diff --git a/server/tests/api/check-params/users.ts b/server/tests/api/check-params/users.ts index a3e415b94..e8a6ffd19 100644 --- a/server/tests/api/check-params/users.ts +++ b/server/tests/api/check-params/users.ts | |||
@@ -231,9 +231,9 @@ describe('Test users API validators', function () { | |||
231 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields }) | 231 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields }) |
232 | }) | 232 | }) |
233 | 233 | ||
234 | it('Should fail with an invalid display NSFW attribute', async function () { | 234 | it('Should fail with an invalid NSFW policy attribute', async function () { |
235 | const fields = { | 235 | const fields = { |
236 | displayNSFW: -1 | 236 | nsfwPolicy: 'hello' |
237 | } | 237 | } |
238 | 238 | ||
239 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields }) | 239 | await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields }) |
@@ -266,7 +266,7 @@ describe('Test users API validators', function () { | |||
266 | it('Should succeed with the correct params', async function () { | 266 | it('Should succeed with the correct params', async function () { |
267 | const fields = { | 267 | const fields = { |
268 | password: 'my super password', | 268 | password: 'my super password', |
269 | displayNSFW: true, | 269 | nsfwPolicy: 'blur', |
270 | autoPlayVideo: false, | 270 | autoPlayVideo: false, |
271 | email: 'super_email@example.com' | 271 | email: 'super_email@example.com' |
272 | } | 272 | } |
diff --git a/server/tests/api/index-fast.ts b/server/tests/api/index-fast.ts index aa063b97a..2454ec2f9 100644 --- a/server/tests/api/index-fast.ts +++ b/server/tests/api/index-fast.ts | |||
@@ -7,6 +7,7 @@ import './videos/video-abuse' | |||
7 | import './videos/video-blacklist' | 7 | import './videos/video-blacklist' |
8 | import './videos/video-blacklist-management' | 8 | import './videos/video-blacklist-management' |
9 | import './videos/video-description' | 9 | import './videos/video-description' |
10 | import './videos/video-nsfw' | ||
10 | import './videos/video-privacy' | 11 | import './videos/video-privacy' |
11 | import './videos/services' | 12 | import './videos/services' |
12 | import './server/email' | 13 | import './server/email' |
diff --git a/server/tests/api/server/config.ts b/server/tests/api/server/config.ts index e17588142..3f1b1532c 100644 --- a/server/tests/api/server/config.ts +++ b/server/tests/api/server/config.ts | |||
@@ -59,6 +59,7 @@ describe('Test config', function () { | |||
59 | expect(data.instance.description).to.equal('Welcome to this PeerTube instance!') | 59 | expect(data.instance.description).to.equal('Welcome to this PeerTube instance!') |
60 | expect(data.instance.terms).to.equal('No terms for now.') | 60 | expect(data.instance.terms).to.equal('No terms for now.') |
61 | expect(data.instance.defaultClientRoute).to.equal('/videos/trending') | 61 | expect(data.instance.defaultClientRoute).to.equal('/videos/trending') |
62 | expect(data.instance.defaultNSFWPolicy).to.equal('display') | ||
62 | expect(data.instance.customizations.css).to.be.empty | 63 | expect(data.instance.customizations.css).to.be.empty |
63 | expect(data.instance.customizations.javascript).to.be.empty | 64 | expect(data.instance.customizations.javascript).to.be.empty |
64 | expect(data.cache.previews.size).to.equal(1) | 65 | expect(data.cache.previews.size).to.equal(1) |
@@ -83,6 +84,7 @@ describe('Test config', function () { | |||
83 | description: 'my super description', | 84 | description: 'my super description', |
84 | terms: 'my super terms', | 85 | terms: 'my super terms', |
85 | defaultClientRoute: '/videos/recently-added', | 86 | defaultClientRoute: '/videos/recently-added', |
87 | defaultNSFWPolicy: 'blur' as 'blur', | ||
86 | customizations: { | 88 | customizations: { |
87 | javascript: 'alert("coucou")', | 89 | javascript: 'alert("coucou")', |
88 | css: 'body { background-color: red; }' | 90 | css: 'body { background-color: red; }' |
@@ -125,6 +127,7 @@ describe('Test config', function () { | |||
125 | expect(data.instance.description).to.equal('my super description') | 127 | expect(data.instance.description).to.equal('my super description') |
126 | expect(data.instance.terms).to.equal('my super terms') | 128 | expect(data.instance.terms).to.equal('my super terms') |
127 | expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added') | 129 | expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added') |
130 | expect(data.instance.defaultNSFWPolicy).to.equal('blur') | ||
128 | expect(data.instance.customizations.javascript).to.equal('alert("coucou")') | 131 | expect(data.instance.customizations.javascript).to.equal('alert("coucou")') |
129 | expect(data.instance.customizations.css).to.equal('body { background-color: red; }') | 132 | expect(data.instance.customizations.css).to.equal('body { background-color: red; }') |
130 | expect(data.cache.previews.size).to.equal(2) | 133 | expect(data.cache.previews.size).to.equal(2) |
@@ -156,6 +159,7 @@ describe('Test config', function () { | |||
156 | expect(data.instance.description).to.equal('my super description') | 159 | expect(data.instance.description).to.equal('my super description') |
157 | expect(data.instance.terms).to.equal('my super terms') | 160 | expect(data.instance.terms).to.equal('my super terms') |
158 | expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added') | 161 | expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added') |
162 | expect(data.instance.defaultNSFWPolicy).to.equal('blur') | ||
159 | expect(data.instance.customizations.javascript).to.equal('alert("coucou")') | 163 | expect(data.instance.customizations.javascript).to.equal('alert("coucou")') |
160 | expect(data.instance.customizations.css).to.equal('body { background-color: red; }') | 164 | expect(data.instance.customizations.css).to.equal('body { background-color: red; }') |
161 | expect(data.cache.previews.size).to.equal(2) | 165 | expect(data.cache.previews.size).to.equal(2) |
@@ -198,6 +202,7 @@ describe('Test config', function () { | |||
198 | expect(data.instance.description).to.equal('Welcome to this PeerTube instance!') | 202 | expect(data.instance.description).to.equal('Welcome to this PeerTube instance!') |
199 | expect(data.instance.terms).to.equal('No terms for now.') | 203 | expect(data.instance.terms).to.equal('No terms for now.') |
200 | expect(data.instance.defaultClientRoute).to.equal('/videos/trending') | 204 | expect(data.instance.defaultClientRoute).to.equal('/videos/trending') |
205 | expect(data.instance.defaultNSFWPolicy).to.equal('display') | ||
201 | expect(data.instance.customizations.css).to.be.empty | 206 | expect(data.instance.customizations.css).to.be.empty |
202 | expect(data.instance.customizations.javascript).to.be.empty | 207 | expect(data.instance.customizations.javascript).to.be.empty |
203 | expect(data.cache.previews.size).to.equal(1) | 208 | expect(data.cache.previews.size).to.equal(1) |
diff --git a/server/tests/api/users/users.ts b/server/tests/api/users/users.ts index b6ab4f660..1192ef9e4 100644 --- a/server/tests/api/users/users.ts +++ b/server/tests/api/users/users.ts | |||
@@ -168,7 +168,7 @@ describe('Test users', function () { | |||
168 | 168 | ||
169 | expect(user.username).to.equal('user_1') | 169 | expect(user.username).to.equal('user_1') |
170 | expect(user.email).to.equal('user_1@example.com') | 170 | expect(user.email).to.equal('user_1@example.com') |
171 | expect(user.displayNSFW).to.be.false | 171 | expect(user.nsfwPolicy).to.equal('display') |
172 | expect(user.videoQuota).to.equal(2 * 1024 * 1024) | 172 | expect(user.videoQuota).to.equal(2 * 1024 * 1024) |
173 | expect(user.roleLabel).to.equal('User') | 173 | expect(user.roleLabel).to.equal('User') |
174 | expect(user.id).to.be.a('number') | 174 | expect(user.id).to.be.a('number') |
@@ -215,12 +215,12 @@ describe('Test users', function () { | |||
215 | const user = users[ 0 ] | 215 | const user = users[ 0 ] |
216 | expect(user.username).to.equal('user_1') | 216 | expect(user.username).to.equal('user_1') |
217 | expect(user.email).to.equal('user_1@example.com') | 217 | expect(user.email).to.equal('user_1@example.com') |
218 | expect(user.displayNSFW).to.be.false | 218 | expect(user.nsfwPolicy).to.equal('display') |
219 | 219 | ||
220 | const rootUser = users[ 1 ] | 220 | const rootUser = users[ 1 ] |
221 | expect(rootUser.username).to.equal('root') | 221 | expect(rootUser.username).to.equal('root') |
222 | expect(rootUser.email).to.equal('admin1@example.com') | 222 | expect(rootUser.email).to.equal('admin1@example.com') |
223 | expect(rootUser.displayNSFW).to.be.false | 223 | expect(user.nsfwPolicy).to.equal('display') |
224 | 224 | ||
225 | userId = user.id | 225 | userId = user.id |
226 | }) | 226 | }) |
@@ -239,7 +239,7 @@ describe('Test users', function () { | |||
239 | expect(user.username).to.equal('root') | 239 | expect(user.username).to.equal('root') |
240 | expect(user.email).to.equal('admin1@example.com') | 240 | expect(user.email).to.equal('admin1@example.com') |
241 | expect(user.roleLabel).to.equal('Administrator') | 241 | expect(user.roleLabel).to.equal('Administrator') |
242 | expect(user.displayNSFW).to.be.false | 242 | expect(user.nsfwPolicy).to.equal('display') |
243 | }) | 243 | }) |
244 | 244 | ||
245 | it('Should list only the first user by username desc', async function () { | 245 | it('Should list only the first user by username desc', async function () { |
@@ -254,7 +254,7 @@ describe('Test users', function () { | |||
254 | const user = users[ 0 ] | 254 | const user = users[ 0 ] |
255 | expect(user.username).to.equal('user_1') | 255 | expect(user.username).to.equal('user_1') |
256 | expect(user.email).to.equal('user_1@example.com') | 256 | expect(user.email).to.equal('user_1@example.com') |
257 | expect(user.displayNSFW).to.be.false | 257 | expect(user.nsfwPolicy).to.equal('display') |
258 | }) | 258 | }) |
259 | 259 | ||
260 | it('Should list only the second user by createdAt desc', async function () { | 260 | it('Should list only the second user by createdAt desc', async function () { |
@@ -269,7 +269,7 @@ describe('Test users', function () { | |||
269 | const user = users[ 0 ] | 269 | const user = users[ 0 ] |
270 | expect(user.username).to.equal('user_1') | 270 | expect(user.username).to.equal('user_1') |
271 | expect(user.email).to.equal('user_1@example.com') | 271 | expect(user.email).to.equal('user_1@example.com') |
272 | expect(user.displayNSFW).to.be.false | 272 | expect(user.nsfwPolicy).to.equal('display') |
273 | }) | 273 | }) |
274 | 274 | ||
275 | it('Should list all the users by createdAt asc', async function () { | 275 | it('Should list all the users by createdAt asc', async function () { |
@@ -283,11 +283,11 @@ describe('Test users', function () { | |||
283 | 283 | ||
284 | expect(users[ 0 ].username).to.equal('root') | 284 | expect(users[ 0 ].username).to.equal('root') |
285 | expect(users[ 0 ].email).to.equal('admin1@example.com') | 285 | expect(users[ 0 ].email).to.equal('admin1@example.com') |
286 | expect(users[ 0 ].displayNSFW).to.be.false | 286 | expect(users[ 0 ].nsfwPolicy).to.equal('display') |
287 | 287 | ||
288 | expect(users[ 1 ].username).to.equal('user_1') | 288 | expect(users[ 1 ].username).to.equal('user_1') |
289 | expect(users[ 1 ].email).to.equal('user_1@example.com') | 289 | expect(users[ 1 ].email).to.equal('user_1@example.com') |
290 | expect(users[ 1 ].displayNSFW).to.be.false | 290 | expect(users[ 1 ].nsfwPolicy).to.equal('display') |
291 | }) | 291 | }) |
292 | 292 | ||
293 | it('Should update my password', async function () { | 293 | it('Should update my password', async function () { |
@@ -305,7 +305,7 @@ describe('Test users', function () { | |||
305 | await updateMyUser({ | 305 | await updateMyUser({ |
306 | url: server.url, | 306 | url: server.url, |
307 | accessToken: accessTokenUser, | 307 | accessToken: accessTokenUser, |
308 | displayNSFW: true | 308 | nsfwPolicy: 'do_not_list' |
309 | }) | 309 | }) |
310 | 310 | ||
311 | const res = await getMyUserInformation(server.url, accessTokenUser) | 311 | const res = await getMyUserInformation(server.url, accessTokenUser) |
@@ -313,7 +313,7 @@ describe('Test users', function () { | |||
313 | 313 | ||
314 | expect(user.username).to.equal('user_1') | 314 | expect(user.username).to.equal('user_1') |
315 | expect(user.email).to.equal('user_1@example.com') | 315 | expect(user.email).to.equal('user_1@example.com') |
316 | expect(user.displayNSFW).to.be.ok | 316 | expect(user.nsfwPolicy).to.equal('do_not_list') |
317 | expect(user.videoQuota).to.equal(2 * 1024 * 1024) | 317 | expect(user.videoQuota).to.equal(2 * 1024 * 1024) |
318 | expect(user.id).to.be.a('number') | 318 | expect(user.id).to.be.a('number') |
319 | expect(user.account.description).to.be.null | 319 | expect(user.account.description).to.be.null |
@@ -344,7 +344,7 @@ describe('Test users', function () { | |||
344 | 344 | ||
345 | expect(user.username).to.equal('user_1') | 345 | expect(user.username).to.equal('user_1') |
346 | expect(user.email).to.equal('updated@example.com') | 346 | expect(user.email).to.equal('updated@example.com') |
347 | expect(user.displayNSFW).to.be.ok | 347 | expect(user.nsfwPolicy).to.equal('do_not_list') |
348 | expect(user.videoQuota).to.equal(2 * 1024 * 1024) | 348 | expect(user.videoQuota).to.equal(2 * 1024 * 1024) |
349 | expect(user.id).to.be.a('number') | 349 | expect(user.id).to.be.a('number') |
350 | expect(user.account.description).to.be.null | 350 | expect(user.account.description).to.be.null |
@@ -377,7 +377,7 @@ describe('Test users', function () { | |||
377 | 377 | ||
378 | expect(user.username).to.equal('user_1') | 378 | expect(user.username).to.equal('user_1') |
379 | expect(user.email).to.equal('updated@example.com') | 379 | expect(user.email).to.equal('updated@example.com') |
380 | expect(user.displayNSFW).to.be.ok | 380 | expect(user.nsfwPolicy).to.equal('do_not_list') |
381 | expect(user.videoQuota).to.equal(2 * 1024 * 1024) | 381 | expect(user.videoQuota).to.equal(2 * 1024 * 1024) |
382 | expect(user.id).to.be.a('number') | 382 | expect(user.id).to.be.a('number') |
383 | expect(user.account.description).to.equal('my super description updated') | 383 | expect(user.account.description).to.equal('my super description updated') |
@@ -398,7 +398,7 @@ describe('Test users', function () { | |||
398 | 398 | ||
399 | expect(user.username).to.equal('user_1') | 399 | expect(user.username).to.equal('user_1') |
400 | expect(user.email).to.equal('updated2@example.com') | 400 | expect(user.email).to.equal('updated2@example.com') |
401 | expect(user.displayNSFW).to.be.ok | 401 | expect(user.nsfwPolicy).to.equal('do_not_list') |
402 | expect(user.videoQuota).to.equal(42) | 402 | expect(user.videoQuota).to.equal(42) |
403 | expect(user.roleLabel).to.equal('Moderator') | 403 | expect(user.roleLabel).to.equal('Moderator') |
404 | expect(user.id).to.be.a('number') | 404 | expect(user.id).to.be.a('number') |
diff --git a/server/tests/api/videos/video-nsfw.ts b/server/tests/api/videos/video-nsfw.ts new file mode 100644 index 000000000..4e5ab11ce --- /dev/null +++ b/server/tests/api/videos/video-nsfw.ts | |||
@@ -0,0 +1,197 @@ | |||
1 | /* tslint:disable:no-unused-expression */ | ||
2 | |||
3 | import * as chai from 'chai' | ||
4 | import 'mocha' | ||
5 | import { flushTests, getVideosList, killallServers, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../utils/index' | ||
6 | import { userLogin } from '../../utils/users/login' | ||
7 | import { createUser } from '../../utils/users/users' | ||
8 | import { getMyVideos } from '../../utils/videos/videos' | ||
9 | import { | ||
10 | getConfig, getCustomConfig, | ||
11 | getMyUserInformation, | ||
12 | getVideosListWithToken, | ||
13 | runServer, | ||
14 | searchVideo, | ||
15 | searchVideoWithToken, updateCustomConfig, | ||
16 | updateMyUser | ||
17 | } from '../../utils' | ||
18 | import { ServerConfig } from '../../../../shared/models' | ||
19 | import { CustomConfig } from '../../../../shared/models/server/custom-config.model' | ||
20 | |||
21 | const expect = chai.expect | ||
22 | |||
23 | describe('Test video NSFW policy', function () { | ||
24 | let server: ServerInfo | ||
25 | let userAccessToken: string | ||
26 | let customConfig: CustomConfig | ||
27 | |||
28 | before(async function () { | ||
29 | this.timeout(50000) | ||
30 | |||
31 | await flushTests() | ||
32 | server = await runServer(1) | ||
33 | |||
34 | // Get the access tokens | ||
35 | await setAccessTokensToServers([ server ]) | ||
36 | |||
37 | { | ||
38 | const attributes = { name: 'nsfw', nsfw: true } | ||
39 | await uploadVideo(server.url, server.accessToken, attributes) | ||
40 | } | ||
41 | |||
42 | { | ||
43 | const attributes = { name: 'normal', nsfw: false } | ||
44 | await uploadVideo(server.url, server.accessToken, attributes) | ||
45 | } | ||
46 | |||
47 | { | ||
48 | const res = await getCustomConfig(server.url, server.accessToken) | ||
49 | customConfig = res.body | ||
50 | } | ||
51 | }) | ||
52 | |||
53 | describe('Instance default NSFW policy', function () { | ||
54 | it('Should display NSFW videos with display default NSFW policy', async function () { | ||
55 | const resConfig = await getConfig(server.url) | ||
56 | const serverConfig: ServerConfig = resConfig.body | ||
57 | expect(serverConfig.instance.defaultNSFWPolicy).to.equal('display') | ||
58 | |||
59 | for (const res of [ await getVideosList(server.url), await searchVideo(server.url, 'n') ]) { | ||
60 | expect(res.body.total).to.equal(2) | ||
61 | |||
62 | const videos = res.body.data | ||
63 | expect(videos).to.have.lengthOf(2) | ||
64 | expect(videos[ 0 ].name).to.equal('normal') | ||
65 | expect(videos[ 1 ].name).to.equal('nsfw') | ||
66 | } | ||
67 | }) | ||
68 | |||
69 | it('Should not display NSFW videos with do_not_list default NSFW policy', async function () { | ||
70 | customConfig.instance.defaultNSFWPolicy = 'do_not_list' | ||
71 | await updateCustomConfig(server.url, server.accessToken, customConfig) | ||
72 | |||
73 | const resConfig = await getConfig(server.url) | ||
74 | const serverConfig: ServerConfig = resConfig.body | ||
75 | expect(serverConfig.instance.defaultNSFWPolicy).to.equal('do_not_list') | ||
76 | |||
77 | for (const res of [ await getVideosList(server.url), await searchVideo(server.url, 'n') ]) { | ||
78 | expect(res.body.total).to.equal(1) | ||
79 | |||
80 | const videos = res.body.data | ||
81 | expect(videos).to.have.lengthOf(1) | ||
82 | expect(videos[ 0 ].name).to.equal('normal') | ||
83 | } | ||
84 | }) | ||
85 | |||
86 | it('Should display NSFW videos with blur default NSFW policy', async function () { | ||
87 | customConfig.instance.defaultNSFWPolicy = 'blur' | ||
88 | await updateCustomConfig(server.url, server.accessToken, customConfig) | ||
89 | |||
90 | const resConfig = await getConfig(server.url) | ||
91 | const serverConfig: ServerConfig = resConfig.body | ||
92 | expect(serverConfig.instance.defaultNSFWPolicy).to.equal('blur') | ||
93 | |||
94 | for (const res of [ await getVideosList(server.url), await searchVideo(server.url, 'n') ]) { | ||
95 | expect(res.body.total).to.equal(2) | ||
96 | |||
97 | const videos = res.body.data | ||
98 | expect(videos).to.have.lengthOf(2) | ||
99 | expect(videos[ 0 ].name).to.equal('normal') | ||
100 | expect(videos[ 1 ].name).to.equal('nsfw') | ||
101 | } | ||
102 | }) | ||
103 | }) | ||
104 | |||
105 | describe('User NSFW policy', function () { | ||
106 | |||
107 | it('Should create a user having the default nsfw policy', async function () { | ||
108 | const username = 'user1' | ||
109 | const password = 'my super password' | ||
110 | await createUser(server.url, server.accessToken, username, password) | ||
111 | |||
112 | userAccessToken = await userLogin(server, { username, password }) | ||
113 | |||
114 | const res = await getMyUserInformation(server.url, userAccessToken) | ||
115 | const user = res.body | ||
116 | |||
117 | expect(user.nsfwPolicy).to.equal('blur') | ||
118 | }) | ||
119 | |||
120 | it('Should display NSFW videos with blur user NSFW policy', async function () { | ||
121 | const results = [ | ||
122 | await getVideosListWithToken(server.url, userAccessToken), | ||
123 | await searchVideoWithToken(server.url, 'n', userAccessToken) | ||
124 | ] | ||
125 | |||
126 | for (const res of results) { | ||
127 | expect(res.body.total).to.equal(2) | ||
128 | |||
129 | const videos = res.body.data | ||
130 | expect(videos).to.have.lengthOf(2) | ||
131 | expect(videos[ 0 ].name).to.equal('normal') | ||
132 | expect(videos[ 1 ].name).to.equal('nsfw') | ||
133 | } | ||
134 | }) | ||
135 | |||
136 | it('Should display NSFW videos with display user NSFW policy', async function () { | ||
137 | await updateMyUser({ | ||
138 | url: server.url, | ||
139 | accessToken: server.accessToken, | ||
140 | nsfwPolicy: 'display' | ||
141 | }) | ||
142 | |||
143 | const results = [ | ||
144 | await getVideosListWithToken(server.url, server.accessToken), | ||
145 | await searchVideoWithToken(server.url, 'n', server.accessToken) | ||
146 | ] | ||
147 | |||
148 | for (const res of results) { | ||
149 | expect(res.body.total).to.equal(2) | ||
150 | |||
151 | const videos = res.body.data | ||
152 | expect(videos).to.have.lengthOf(2) | ||
153 | expect(videos[ 0 ].name).to.equal('normal') | ||
154 | expect(videos[ 1 ].name).to.equal('nsfw') | ||
155 | } | ||
156 | }) | ||
157 | |||
158 | it('Should not display NSFW videos with do_not_list user NSFW policy', async function () { | ||
159 | await updateMyUser({ | ||
160 | url: server.url, | ||
161 | accessToken: server.accessToken, | ||
162 | nsfwPolicy: 'do_not_list' | ||
163 | }) | ||
164 | |||
165 | const results = [ | ||
166 | await getVideosListWithToken(server.url, server.accessToken), | ||
167 | await searchVideoWithToken(server.url, 'n', server.accessToken) | ||
168 | ] | ||
169 | for (const res of results) { | ||
170 | expect(res.body.total).to.equal(1) | ||
171 | |||
172 | const videos = res.body.data | ||
173 | expect(videos).to.have.lengthOf(1) | ||
174 | expect(videos[ 0 ].name).to.equal('normal') | ||
175 | } | ||
176 | }) | ||
177 | |||
178 | it('Should be able to see my NSFW videos even with do_not_list user NSFW policy', async function () { | ||
179 | const res = await getMyVideos(server.url, server.accessToken, 0, 5) | ||
180 | expect(res.body.total).to.equal(2) | ||
181 | |||
182 | const videos = res.body.data | ||
183 | expect(videos).to.have.lengthOf(2) | ||
184 | expect(videos[ 0 ].name).to.equal('normal') | ||
185 | expect(videos[ 1 ].name).to.equal('nsfw') | ||
186 | }) | ||
187 | }) | ||
188 | |||
189 | after(async function () { | ||
190 | killallServers([ server ]) | ||
191 | |||
192 | // Keep the logs if the test failed | ||
193 | if (this['ok']) { | ||
194 | await flushTests() | ||
195 | } | ||
196 | }) | ||
197 | }) | ||
diff --git a/server/tests/utils/users/users.ts b/server/tests/utils/users/users.ts index daf731a14..fc6b26c50 100644 --- a/server/tests/utils/users/users.ts +++ b/server/tests/utils/users/users.ts | |||
@@ -3,6 +3,7 @@ import * as request from 'supertest' | |||
3 | import { makePostBodyRequest, makeUploadRequest, makePutBodyRequest } from '../' | 3 | import { makePostBodyRequest, makeUploadRequest, makePutBodyRequest } from '../' |
4 | 4 | ||
5 | import { UserRole } from '../../../../shared/index' | 5 | import { UserRole } from '../../../../shared/index' |
6 | import { NSFWPolicyType } from '../../../../shared/models/videos/nsfw-policy.type' | ||
6 | 7 | ||
7 | function createUser ( | 8 | function createUser ( |
8 | url: string, | 9 | url: string, |
@@ -128,7 +129,7 @@ function updateMyUser (options: { | |||
128 | url: string | 129 | url: string |
129 | accessToken: string, | 130 | accessToken: string, |
130 | newPassword?: string, | 131 | newPassword?: string, |
131 | displayNSFW?: boolean, | 132 | nsfwPolicy?: NSFWPolicyType, |
132 | email?: string, | 133 | email?: string, |
133 | autoPlayVideo?: boolean | 134 | autoPlayVideo?: boolean |
134 | description?: string | 135 | description?: string |
@@ -137,7 +138,7 @@ function updateMyUser (options: { | |||
137 | 138 | ||
138 | const toSend = {} | 139 | const toSend = {} |
139 | if (options.newPassword !== undefined && options.newPassword !== null) toSend['password'] = options.newPassword | 140 | if (options.newPassword !== undefined && options.newPassword !== null) toSend['password'] = options.newPassword |
140 | if (options.displayNSFW !== undefined && options.displayNSFW !== null) toSend['displayNSFW'] = options.displayNSFW | 141 | if (options.nsfwPolicy !== undefined && options.nsfwPolicy !== null) toSend['nsfwPolicy'] = options.nsfwPolicy |
141 | if (options.autoPlayVideo !== undefined && options.autoPlayVideo !== null) toSend['autoPlayVideo'] = options.autoPlayVideo | 142 | if (options.autoPlayVideo !== undefined && options.autoPlayVideo !== null) toSend['autoPlayVideo'] = options.autoPlayVideo |
142 | if (options.email !== undefined && options.email !== null) toSend['email'] = options.email | 143 | if (options.email !== undefined && options.email !== null) toSend['email'] = options.email |
143 | if (options.description !== undefined && options.description !== null) toSend['description'] = options.description | 144 | if (options.description !== undefined && options.description !== null) toSend['description'] = options.description |
diff --git a/server/tests/utils/videos/videos.ts b/server/tests/utils/videos/videos.ts index 01e7fa5a1..5e186147e 100644 --- a/server/tests/utils/videos/videos.ts +++ b/server/tests/utils/videos/videos.ts | |||
@@ -128,6 +128,18 @@ function getVideosList (url: string) { | |||
128 | .expect('Content-Type', /json/) | 128 | .expect('Content-Type', /json/) |
129 | } | 129 | } |
130 | 130 | ||
131 | function getVideosListWithToken (url: string, token: string) { | ||
132 | const path = '/api/v1/videos' | ||
133 | |||
134 | return request(url) | ||
135 | .get(path) | ||
136 | .set('Authorization', 'Bearer ' + token) | ||
137 | .query({ sort: 'name' }) | ||
138 | .set('Accept', 'application/json') | ||
139 | .expect(200) | ||
140 | .expect('Content-Type', /json/) | ||
141 | } | ||
142 | |||
131 | function getLocalVideos (url: string) { | 143 | function getLocalVideos (url: string) { |
132 | const path = '/api/v1/videos' | 144 | const path = '/api/v1/videos' |
133 | 145 | ||
@@ -202,6 +214,18 @@ function searchVideo (url: string, search: string) { | |||
202 | .expect('Content-Type', /json/) | 214 | .expect('Content-Type', /json/) |
203 | } | 215 | } |
204 | 216 | ||
217 | function searchVideoWithToken (url: string, search: string, token: string) { | ||
218 | const path = '/api/v1/videos' | ||
219 | const req = request(url) | ||
220 | .get(path + '/search') | ||
221 | .set('Authorization', 'Bearer ' + token) | ||
222 | .query({ search }) | ||
223 | .set('Accept', 'application/json') | ||
224 | |||
225 | return req.expect(200) | ||
226 | .expect('Content-Type', /json/) | ||
227 | } | ||
228 | |||
205 | function searchVideoWithPagination (url: string, search: string, start: number, count: number, sort?: string) { | 229 | function searchVideoWithPagination (url: string, search: string, start: number, count: number, sort?: string) { |
206 | const path = '/api/v1/videos' | 230 | const path = '/api/v1/videos' |
207 | 231 | ||
@@ -418,6 +442,8 @@ async function completeVideoCheck ( | |||
418 | expect(video.licence.label).to.equal(VIDEO_LICENCES[attributes.licence] || 'Unknown') | 442 | expect(video.licence.label).to.equal(VIDEO_LICENCES[attributes.licence] || 'Unknown') |
419 | expect(video.language.id).to.equal(attributes.language) | 443 | expect(video.language.id).to.equal(attributes.language) |
420 | expect(video.language.label).to.equal(VIDEO_LANGUAGES[attributes.language] || 'Unknown') | 444 | expect(video.language.label).to.equal(VIDEO_LANGUAGES[attributes.language] || 'Unknown') |
445 | expect(video.privacy.id).to.deep.equal(attributes.privacy) | ||
446 | expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy]) | ||
421 | expect(video.nsfw).to.equal(attributes.nsfw) | 447 | expect(video.nsfw).to.equal(attributes.nsfw) |
422 | expect(video.description).to.equal(attributes.description) | 448 | expect(video.description).to.equal(attributes.description) |
423 | expect(video.account.host).to.equal(attributes.account.host) | 449 | expect(video.account.host).to.equal(attributes.account.host) |
@@ -435,8 +461,6 @@ async function completeVideoCheck ( | |||
435 | 461 | ||
436 | expect(videoDetails.files).to.have.lengthOf(attributes.files.length) | 462 | expect(videoDetails.files).to.have.lengthOf(attributes.files.length) |
437 | expect(videoDetails.tags).to.deep.equal(attributes.tags) | 463 | expect(videoDetails.tags).to.deep.equal(attributes.tags) |
438 | expect(videoDetails.privacy.id).to.deep.equal(attributes.privacy) | ||
439 | expect(videoDetails.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy]) | ||
440 | expect(videoDetails.account.name).to.equal(attributes.account.name) | 464 | expect(videoDetails.account.name).to.equal(attributes.account.name) |
441 | expect(videoDetails.account.host).to.equal(attributes.account.host) | 465 | expect(videoDetails.account.host).to.equal(attributes.account.host) |
442 | expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled) | 466 | expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled) |
@@ -490,6 +514,7 @@ export { | |||
490 | getVideoPrivacies, | 514 | getVideoPrivacies, |
491 | getVideoLanguages, | 515 | getVideoLanguages, |
492 | getMyVideos, | 516 | getMyVideos, |
517 | searchVideoWithToken, | ||
493 | getVideo, | 518 | getVideo, |
494 | getVideoWithToken, | 519 | getVideoWithToken, |
495 | getVideosList, | 520 | getVideosList, |
@@ -499,6 +524,7 @@ export { | |||
499 | searchVideo, | 524 | searchVideo, |
500 | searchVideoWithPagination, | 525 | searchVideoWithPagination, |
501 | searchVideoWithSort, | 526 | searchVideoWithSort, |
527 | getVideosListWithToken, | ||
502 | uploadVideo, | 528 | uploadVideo, |
503 | updateVideo, | 529 | updateVideo, |
504 | rateVideo, | 530 | rateVideo, |
diff --git a/shared/models/server/custom-config.model.ts b/shared/models/server/custom-config.model.ts index b4d24cfbe..30956bd47 100644 --- a/shared/models/server/custom-config.model.ts +++ b/shared/models/server/custom-config.model.ts | |||
@@ -1,3 +1,5 @@ | |||
1 | import { NSFWPolicyType } from '../videos/nsfw-policy.type' | ||
2 | |||
1 | export interface CustomConfig { | 3 | export interface CustomConfig { |
2 | instance: { | 4 | instance: { |
3 | name: string | 5 | name: string |
@@ -5,6 +7,7 @@ export interface CustomConfig { | |||
5 | description: string | 7 | description: string |
6 | terms: string | 8 | terms: string |
7 | defaultClientRoute: string | 9 | defaultClientRoute: string |
10 | defaultNSFWPolicy: NSFWPolicyType | ||
8 | customizations: { | 11 | customizations: { |
9 | javascript?: string | 12 | javascript?: string |
10 | css?: string | 13 | css?: string |
diff --git a/shared/models/server/server-config.model.ts b/shared/models/server/server-config.model.ts index 611d0efe5..d1f956163 100644 --- a/shared/models/server/server-config.model.ts +++ b/shared/models/server/server-config.model.ts | |||
@@ -1,3 +1,5 @@ | |||
1 | import { NSFWPolicyType } from '../videos/nsfw-policy.type' | ||
2 | |||
1 | export interface ServerConfig { | 3 | export interface ServerConfig { |
2 | serverVersion: string | 4 | serverVersion: string |
3 | 5 | ||
@@ -5,6 +7,7 @@ export interface ServerConfig { | |||
5 | name: string | 7 | name: string |
6 | shortDescription: string | 8 | shortDescription: string |
7 | defaultClientRoute: string | 9 | defaultClientRoute: string |
10 | defaultNSFWPolicy: NSFWPolicyType | ||
8 | customizations: { | 11 | customizations: { |
9 | javascript: string | 12 | javascript: string |
10 | css: string | 13 | css: string |
diff --git a/shared/models/users/user-update-me.model.ts b/shared/models/users/user-update-me.model.ts index b84233329..0a73879ed 100644 --- a/shared/models/users/user-update-me.model.ts +++ b/shared/models/users/user-update-me.model.ts | |||
@@ -1,6 +1,8 @@ | |||
1 | import { NSFWPolicyType } from '../videos/nsfw-policy.type' | ||
2 | |||
1 | export interface UserUpdateMe { | 3 | export interface UserUpdateMe { |
2 | description?: string | 4 | description?: string |
3 | displayNSFW?: boolean | 5 | nsfwPolicy?: NSFWPolicyType |
4 | autoPlayVideo?: boolean | 6 | autoPlayVideo?: boolean |
5 | email?: string | 7 | email?: string |
6 | password?: string | 8 | password?: string |
diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts index b5f459f31..188e29ede 100644 --- a/shared/models/users/user.model.ts +++ b/shared/models/users/user.model.ts | |||
@@ -1,12 +1,13 @@ | |||
1 | import { Account } from '../actors' | 1 | import { Account } from '../actors' |
2 | import { VideoChannel } from '../videos/video-channel.model' | 2 | import { VideoChannel } from '../videos/video-channel.model' |
3 | import { UserRole } from './user-role' | 3 | import { UserRole } from './user-role' |
4 | import { NSFWPolicyType } from '../videos/nsfw-policy.type' | ||
4 | 5 | ||
5 | export interface User { | 6 | export interface User { |
6 | id: number | 7 | id: number |
7 | username: string | 8 | username: string |
8 | email: string | 9 | email: string |
9 | displayNSFW: boolean | 10 | nsfwPolicy: NSFWPolicyType |
10 | autoPlayVideo: boolean | 11 | autoPlayVideo: boolean |
11 | role: UserRole | 12 | role: UserRole |
12 | videoQuota: number | 13 | videoQuota: number |
diff --git a/shared/models/videos/nsfw-policy.type.ts b/shared/models/videos/nsfw-policy.type.ts new file mode 100644 index 000000000..dc0032a14 --- /dev/null +++ b/shared/models/videos/nsfw-policy.type.ts | |||
@@ -0,0 +1 @@ | |||
export type NSFWPolicyType = 'do_not_list' | 'blur' | 'display' | |||
diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts index 1b5f1a09c..70132c050 100644 --- a/shared/models/videos/video.model.ts +++ b/shared/models/videos/video.model.ts | |||
@@ -26,6 +26,7 @@ export interface Video { | |||
26 | category: VideoConstant<number> | 26 | category: VideoConstant<number> |
27 | licence: VideoConstant<number> | 27 | licence: VideoConstant<number> |
28 | language: VideoConstant<number> | 28 | language: VideoConstant<number> |
29 | privacy: VideoConstant<VideoPrivacy> | ||
29 | description: string | 30 | description: string |
30 | duration: number | 31 | duration: number |
31 | isLocal: boolean | 32 | isLocal: boolean |
@@ -48,7 +49,6 @@ export interface Video { | |||
48 | } | 49 | } |
49 | 50 | ||
50 | export interface VideoDetails extends Video { | 51 | export interface VideoDetails extends Video { |
51 | privacy: VideoConstant<VideoPrivacy> | ||
52 | descriptionPath: string | 52 | descriptionPath: string |
53 | support: string | 53 | support: string |
54 | channel: VideoChannel | 54 | channel: VideoChannel |