Merge branch 'develop' of framagit.org:chocobozzz/PeerTube into develop

pull/525/head
Gérald Niel 2018-04-19 19:28:55 +02:00
commit 0db1a22650
56 changed files with 665 additions and 146 deletions

View File

@ -146,3 +146,9 @@ Build the application and run the unit/integration tests:
$ npm run build $ npm run build
$ npm test $ npm test
``` ```
If you just want to run 1 test:
```
$ npm run mocha -- --exit --require ts-node/register/type-check --bail server/tests/api/index.ts
```

View File

@ -62,6 +62,22 @@
</div> </div>
</div> </div>
<div class="form-group">
<label for="instanceDefaultNSFWPolicy">Policy on videos containing sensitive content</label>
<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>
<div class="peertube-select-container">
<select id="instanceDefaultNSFWPolicy" formControlName="instanceDefaultNSFWPolicy">
<option value="do_not_list">Do not list</option>
<option value="blur">Blur thumbnails</option>
<option value="display">Display</option>
</select>
</div>
<div *ngIf="formErrors.instanceDefaultNSFWPolicy" class="form-error">
{{ formErrors.instanceDefaultNSFWPolicy }}
</div>
</div>
<div class="inner-form-title">Cache</div> <div class="inner-form-title">Cache</div>
<div class="form-group"> <div class="form-group">

View File

@ -48,6 +48,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
instanceDescription: '', instanceDescription: '',
instanceTerms: '', instanceTerms: '',
instanceDefaultClientRoute: '', instanceDefaultClientRoute: '',
instanceDefaultNSFWPolicy: '',
cachePreviewsSize: '', cachePreviewsSize: '',
signupLimit: '', signupLimit: '',
adminEmail: '', adminEmail: '',
@ -90,6 +91,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
instanceDescription: [ '' ], instanceDescription: [ '' ],
instanceTerms: [ '' ], instanceTerms: [ '' ],
instanceDefaultClientRoute: [ '' ], instanceDefaultClientRoute: [ '' ],
instanceDefaultNSFWPolicy: [ '' ],
cachePreviewsSize: [ '', CACHE_PREVIEWS_SIZE.VALIDATORS ], cachePreviewsSize: [ '', CACHE_PREVIEWS_SIZE.VALIDATORS ],
signupEnabled: [ ], signupEnabled: [ ],
signupLimit: [ '', SIGNUP_LIMIT.VALIDATORS ], signupLimit: [ '', SIGNUP_LIMIT.VALIDATORS ],
@ -167,6 +169,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
description: this.form.value['instanceDescription'], description: this.form.value['instanceDescription'],
terms: this.form.value['instanceTerms'], terms: this.form.value['instanceTerms'],
defaultClientRoute: this.form.value['instanceDefaultClientRoute'], defaultClientRoute: this.form.value['instanceDefaultClientRoute'],
defaultNSFWPolicy: this.form.value['instanceDefaultNSFWPolicy'],
customizations: { customizations: {
javascript: this.form.value['customizationJavascript'], javascript: this.form.value['customizationJavascript'],
css: this.form.value['customizationCSS'] css: this.form.value['customizationCSS']
@ -224,6 +227,7 @@ export class EditCustomConfigComponent extends FormReactive implements OnInit {
instanceDescription: this.customConfig.instance.description, instanceDescription: this.customConfig.instance.description,
instanceTerms: this.customConfig.instance.terms, instanceTerms: this.customConfig.instance.terms,
instanceDefaultClientRoute: this.customConfig.instance.defaultClientRoute, instanceDefaultClientRoute: this.customConfig.instance.defaultClientRoute,
instanceDefaultNSFWPolicy: this.customConfig.instance.defaultNSFWPolicy,
cachePreviewsSize: this.customConfig.cache.previews.size, cachePreviewsSize: this.customConfig.cache.previews.size,
signupEnabled: this.customConfig.signup.enabled, signupEnabled: this.customConfig.signup.enabled,
signupLimit: this.customConfig.signup.limit, signupLimit: this.customConfig.signup.limit,

View File

@ -1,11 +1,15 @@
<form role="form" (ngSubmit)="updateDetails()" [formGroup]="form"> <form role="form" (ngSubmit)="updateDetails()" [formGroup]="form">
<div class="form-group"> <div class="form-group">
<input <label for="nsfwPolicy">Default policy on videos containing sensitive content</label>
type="checkbox" id="displayNSFW" <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>
formControlName="displayNSFW"
> <div class="peertube-select-container">
<label for="displayNSFW"></label> <select id="nsfwPolicy" formControlName="nsfwPolicy">
<label for="displayNSFW">Display videos that contain mature or explicit content</label> <option value="do_not_list">Do not list</option>
<option value="blur">Blur thumbnails</option>
<option value="display">Display</option>
</select>
</div>
</div> </div>
<div class="form-group"> <div class="form-group">

View File

@ -12,3 +12,9 @@ input[type=submit] {
display: block; display: block;
margin-top: 15px; margin-top: 15px;
} }
.peertube-select-container {
@include peertube-select-container(340px);
margin-bottom: 30px;
}

View File

@ -29,7 +29,7 @@ export class AccountDetailsComponent extends FormReactive implements OnInit {
buildForm () { buildForm () {
this.form = this.formBuilder.group({ this.form = this.formBuilder.group({
displayNSFW: [ this.user.displayNSFW ], nsfwPolicy: [ this.user.nsfwPolicy ],
autoPlayVideo: [ this.user.autoPlayVideo ] autoPlayVideo: [ this.user.autoPlayVideo ]
}) })
@ -41,10 +41,10 @@ export class AccountDetailsComponent extends FormReactive implements OnInit {
} }
updateDetails () { updateDetails () {
const displayNSFW = this.form.value['displayNSFW'] const nsfwPolicy = this.form.value['nsfwPolicy']
const autoPlayVideo = this.form.value['autoPlayVideo'] const autoPlayVideo = this.form.value['autoPlayVideo']
const details: UserUpdateMe = { const details: UserUpdateMe = {
displayNSFW, nsfwPolicy,
autoPlayVideo autoPlayVideo
} }

View File

@ -18,6 +18,7 @@
<div class="video-info"> <div class="video-info">
<a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a> <a class="video-info-name" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name">{{ video.name }}</a>
<span class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span> <span class="video-info-date-views">{{ video.createdAt | myFromNow }} - {{ video.views | myNumberFormatter }} views</span>
<div class="video-info-private">{{ video.privacy.label }}</div>
</div> </div>
<!-- Display only once --> <!-- Display only once -->

View File

@ -79,8 +79,12 @@
font-weight: $font-semibold; font-weight: $font-semibold;
} }
.video-info-date-views { .video-info-date-views, .video-info-private {
font-size: 13px; font-size: 13px;
&.video-info-private {
font-weight: $font-semibold;
}
} }
} }

View File

@ -3,6 +3,7 @@ import { UserRight } from '../../../../../shared/models/users/user-right.enum'
// Do not use the barrel (dependency loop) // Do not use the barrel (dependency loop)
import { hasUserRight, UserRole } from '../../../../../shared/models/users/user-role' import { hasUserRight, UserRole } from '../../../../../shared/models/users/user-role'
import { User, UserConstructorHash } from '../../shared/users/user.model' import { User, UserConstructorHash } from '../../shared/users/user.model'
import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type'
export type TokenOptions = { export type TokenOptions = {
accessToken: string accessToken: string
@ -70,7 +71,7 @@ export class AuthUser extends User {
ROLE: 'role', ROLE: 'role',
EMAIL: 'email', EMAIL: 'email',
USERNAME: 'username', USERNAME: 'username',
DISPLAY_NSFW: 'display_nsfw', NSFW_POLICY: 'nsfw_policy',
AUTO_PLAY_VIDEO: 'auto_play_video' AUTO_PLAY_VIDEO: 'auto_play_video'
} }
@ -85,7 +86,7 @@ export class AuthUser extends User {
username: peertubeLocalStorage.getItem(this.KEYS.USERNAME), username: peertubeLocalStorage.getItem(this.KEYS.USERNAME),
email: peertubeLocalStorage.getItem(this.KEYS.EMAIL), email: peertubeLocalStorage.getItem(this.KEYS.EMAIL),
role: parseInt(peertubeLocalStorage.getItem(this.KEYS.ROLE), 10) as UserRole, role: parseInt(peertubeLocalStorage.getItem(this.KEYS.ROLE), 10) as UserRole,
displayNSFW: peertubeLocalStorage.getItem(this.KEYS.DISPLAY_NSFW) === 'true', nsfwPolicy: peertubeLocalStorage.getItem(this.KEYS.NSFW_POLICY) as NSFWPolicyType,
autoPlayVideo: peertubeLocalStorage.getItem(this.KEYS.AUTO_PLAY_VIDEO) === 'true' autoPlayVideo: peertubeLocalStorage.getItem(this.KEYS.AUTO_PLAY_VIDEO) === 'true'
}, },
Tokens.load() Tokens.load()
@ -99,7 +100,7 @@ export class AuthUser extends User {
peertubeLocalStorage.removeItem(this.KEYS.USERNAME) peertubeLocalStorage.removeItem(this.KEYS.USERNAME)
peertubeLocalStorage.removeItem(this.KEYS.ID) peertubeLocalStorage.removeItem(this.KEYS.ID)
peertubeLocalStorage.removeItem(this.KEYS.ROLE) peertubeLocalStorage.removeItem(this.KEYS.ROLE)
peertubeLocalStorage.removeItem(this.KEYS.DISPLAY_NSFW) peertubeLocalStorage.removeItem(this.KEYS.NSFW_POLICY)
peertubeLocalStorage.removeItem(this.KEYS.AUTO_PLAY_VIDEO) peertubeLocalStorage.removeItem(this.KEYS.AUTO_PLAY_VIDEO)
peertubeLocalStorage.removeItem(this.KEYS.EMAIL) peertubeLocalStorage.removeItem(this.KEYS.EMAIL)
Tokens.flush() Tokens.flush()
@ -136,7 +137,7 @@ export class AuthUser extends User {
peertubeLocalStorage.setItem(AuthUser.KEYS.USERNAME, this.username) peertubeLocalStorage.setItem(AuthUser.KEYS.USERNAME, this.username)
peertubeLocalStorage.setItem(AuthUser.KEYS.EMAIL, this.email) peertubeLocalStorage.setItem(AuthUser.KEYS.EMAIL, this.email)
peertubeLocalStorage.setItem(AuthUser.KEYS.ROLE, this.role.toString()) peertubeLocalStorage.setItem(AuthUser.KEYS.ROLE, this.role.toString())
peertubeLocalStorage.setItem(AuthUser.KEYS.DISPLAY_NSFW, JSON.stringify(this.displayNSFW)) peertubeLocalStorage.setItem(AuthUser.KEYS.NSFW_POLICY, this.nsfwPolicy.toString())
peertubeLocalStorage.setItem(AuthUser.KEYS.AUTO_PLAY_VIDEO, JSON.stringify(this.autoPlayVideo)) peertubeLocalStorage.setItem(AuthUser.KEYS.AUTO_PLAY_VIDEO, JSON.stringify(this.autoPlayVideo))
this.tokens.save() this.tokens.save()
} }

View File

@ -5,7 +5,6 @@ import 'rxjs/add/operator/do'
import { ReplaySubject } from 'rxjs/ReplaySubject' import { ReplaySubject } from 'rxjs/ReplaySubject'
import { ServerConfig } from '../../../../../shared' import { ServerConfig } from '../../../../../shared'
import { About } from '../../../../../shared/models/server/about.model' import { About } from '../../../../../shared/models/server/about.model'
import { ServerStats } from '../../../../../shared/models/server/server-stats.model'
import { environment } from '../../../environments/environment' import { environment } from '../../../environments/environment'
@Injectable() @Injectable()
@ -26,6 +25,7 @@ export class ServerService {
shortDescription: 'PeerTube, a federated (ActivityPub) video streaming platform ' + shortDescription: 'PeerTube, a federated (ActivityPub) video streaming platform ' +
'using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.', 'using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.',
defaultClientRoute: '', defaultClientRoute: '',
defaultNSFWPolicy: 'do_not_list' as 'do_not_list',
customizations: { customizations: {
javascript: '', javascript: '',
css: '' css: ''

View File

@ -13,6 +13,9 @@
</ng-template> </ng-template>
<span <span
class="help-tooltip-button" containerClass="help-tooltip" title="Click to get help" class="help-tooltip-button"
#tooltipDirective="bs-tooltip" [tooltip]="tooltipTemplate" triggers="click" title="Get help"
[popover]="tooltipTemplate"
[placement]="tooltipPlacement"
[outsideClick]="true"
></span> ></span>

View File

@ -12,20 +12,16 @@
} }
/deep/ { /deep/ {
.help-tooltip { .popover-body {
opacity: 1 !important; text-align: left;
padding: 10px;
max-width: 300px;
.tooltip-inner { font-size: 13px;
text-align: left; font-family: $main-fonts;
padding: 10px; background-color: #fff;
max-width: 300px; color: #000;
box-shadow: 0 0 6px rgba(0, 0, 0, 0.5);
font-size: 13px;
font-family: $main-fonts;
background-color: #fff;
color: #000;
box-shadow: 0 0 6px rgba(0, 0, 0, 0.5);
}
ul { ul {
padding-left: 20px; padding-left: 20px;

View File

@ -1,6 +1,5 @@
import { Component, ElementRef, HostListener, Input, OnInit, ViewChild, OnChanges } from '@angular/core' import { Component, Input, OnChanges, OnInit } from '@angular/core'
import { MarkdownService } from '@app/videos/shared' import { MarkdownService } from '@app/videos/shared'
import { TooltipDirective } from 'ngx-bootstrap/tooltip'
@Component({ @Component({
selector: 'my-help', selector: 'my-help',
@ -9,16 +8,14 @@ import { TooltipDirective } from 'ngx-bootstrap/tooltip'
}) })
export class HelpComponent implements OnInit, OnChanges { export class HelpComponent implements OnInit, OnChanges {
@ViewChild('tooltipDirective') tooltipDirective: TooltipDirective
@Input() preHtml = '' @Input() preHtml = ''
@Input() postHtml = '' @Input() postHtml = ''
@Input() customHtml = '' @Input() customHtml = ''
@Input() helpType: 'custom' | 'markdownText' | 'markdownEnhanced' = 'custom' @Input() helpType: 'custom' | 'markdownText' | 'markdownEnhanced' = 'custom'
@Input() tooltipPlacement = 'right'
mainHtml = '' mainHtml = ''
constructor (private elementRef: ElementRef) { }
ngOnInit () { ngOnInit () {
this.init() this.init()
} }
@ -27,15 +24,6 @@ export class HelpComponent implements OnInit, OnChanges {
this.init() this.init()
} }
@HostListener('document:click', ['$event.target'])
public onClick (targetElement) {
const clickedInside = this.elementRef.nativeElement.contains(targetElement)
if (this.tooltipDirective.isOpen && !clickedInside) {
this.tooltipDirective.hide()
}
}
private init () { private init () {
if (this.helpType === 'custom') { if (this.helpType === 'custom') {
this.mainHtml = this.customHtml this.mainHtml = this.customHtml

View File

@ -1,5 +1,6 @@
import { hasUserRight, User as UserServerModel, UserRight, UserRole, VideoChannel } from '../../../../../shared' import { hasUserRight, User as UserServerModel, UserRight, UserRole, VideoChannel } from '../../../../../shared'
import { Account } from '../account/account.model' import { Account } from '../account/account.model'
import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type'
export type UserConstructorHash = { export type UserConstructorHash = {
id: number, id: number,
@ -7,7 +8,7 @@ export type UserConstructorHash = {
email: string, email: string,
role: UserRole, role: UserRole,
videoQuota?: number, videoQuota?: number,
displayNSFW?: boolean, nsfwPolicy?: NSFWPolicyType,
autoPlayVideo?: boolean, autoPlayVideo?: boolean,
createdAt?: Date, createdAt?: Date,
account?: Account, account?: Account,
@ -18,7 +19,7 @@ export class User implements UserServerModel {
username: string username: string
email: string email: string
role: UserRole role: UserRole
displayNSFW: boolean nsfwPolicy: NSFWPolicyType
autoPlayVideo: boolean autoPlayVideo: boolean
videoQuota: number videoQuota: number
account: Account account: Account
@ -40,8 +41,8 @@ export class User implements UserServerModel {
this.videoQuota = hash.videoQuota this.videoQuota = hash.videoQuota
} }
if (hash.displayNSFW !== undefined) { if (hash.nsfwPolicy !== undefined) {
this.displayNSFW = hash.displayNSFW this.nsfwPolicy = hash.nsfwPolicy
} }
if (hash.autoPlayVideo !== undefined) { if (hash.autoPlayVideo !== undefined) {

View File

@ -1,17 +1,9 @@
import { import { UserRight, VideoChannel, VideoDetails as VideoDetailsServerModel, VideoFile } from '../../../../../shared'
UserRight,
VideoChannel,
VideoDetails as VideoDetailsServerModel,
VideoFile,
VideoPrivacy
} from '../../../../../shared'
import { Account } from '../../../../../shared/models/actors' import { Account } from '../../../../../shared/models/actors'
import { VideoConstant } from '../../../../../shared/models/videos/video.model'
import { AuthUser } from '../../core' import { AuthUser } from '../../core'
import { Video } from '../../shared/video/video.model' import { Video } from '../../shared/video/video.model'
export class VideoDetails extends Video implements VideoDetailsServerModel { export class VideoDetails extends Video implements VideoDetailsServerModel {
privacy: VideoConstant<VideoPrivacy>
descriptionPath: string descriptionPath: string
support: string support: string
channel: VideoChannel channel: VideoChannel
@ -26,7 +18,6 @@ export class VideoDetails extends Video implements VideoDetailsServerModel {
constructor (hash: VideoDetailsServerModel) { constructor (hash: VideoDetailsServerModel) {
super(hash) super(hash)
this.privacy = hash.privacy
this.descriptionPath = hash.descriptionPath this.descriptionPath = hash.descriptionPath
this.files = hash.files this.files = hash.files
this.channel = hash.channel this.channel = hash.channel

View File

@ -1,11 +1,11 @@
<div class="video-miniature"> <div class="video-miniature">
<my-video-thumbnail [video]="video" [nsfw]="isVideoNSFWForThisUser()"></my-video-thumbnail> <my-video-thumbnail [video]="video" [nsfw]="isVideoBlur()"></my-video-thumbnail>
<div class="video-miniature-information"> <div class="video-miniature-information">
<span class="video-miniature-name"> <span class="video-miniature-name">
<a <a
class="video-miniature-name" class="video-miniature-name"
[routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoNSFWForThisUser() }" [routerLink]="['/videos/watch', video.uuid]" [attr.title]="video.name" [ngClass]="{ 'blur-filter': isVideoBlur() }"
> >
{{ video.name }} {{ video.name }}
</a> </a>

View File

@ -1,6 +1,7 @@
import { Component, Input } from '@angular/core' import { Component, Input } from '@angular/core'
import { User } from '../users' import { User } from '../users'
import { Video } from './video.model' import { Video } from './video.model'
import { ServerService } from '@app/core'
@Component({ @Component({
selector: 'my-video-miniature', selector: 'my-video-miniature',
@ -11,7 +12,9 @@ export class VideoMiniatureComponent {
@Input() user: User @Input() user: User
@Input() video: Video @Input() video: Video
isVideoNSFWForThisUser () { constructor (private serverService: ServerService) { }
return this.video.isVideoNSFWForUser(this.user)
isVideoBlur () {
return this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())
} }
} }

View File

@ -1,9 +1,10 @@
import { Account } from '@app/shared/account/account.model' import { Account } from '@app/shared/account/account.model'
import { User } from '../' import { User } from '../'
import { Video as VideoServerModel } from '../../../../../shared' import { Video as VideoServerModel, VideoPrivacy } from '../../../../../shared'
import { Avatar } from '../../../../../shared/models/avatars/avatar.model' import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
import { VideoConstant } from '../../../../../shared/models/videos/video.model' import { VideoConstant } from '../../../../../shared/models/videos/video.model'
import { getAbsoluteAPIUrl } from '../misc/utils' import { getAbsoluteAPIUrl } from '../misc/utils'
import { ServerConfig } from '../../../../../shared/models'
export class Video implements VideoServerModel { export class Video implements VideoServerModel {
by: string by: string
@ -13,6 +14,7 @@ export class Video implements VideoServerModel {
category: VideoConstant<number> category: VideoConstant<number>
licence: VideoConstant<number> licence: VideoConstant<number>
language: VideoConstant<number> language: VideoConstant<number>
privacy: VideoConstant<VideoPrivacy>
description: string description: string
duration: number duration: number
durationLabel: string durationLabel: string
@ -61,6 +63,7 @@ export class Video implements VideoServerModel {
this.category = hash.category this.category = hash.category
this.licence = hash.licence this.licence = hash.licence
this.language = hash.language this.language = hash.language
this.privacy = hash.privacy
this.description = hash.description this.description = hash.description
this.duration = hash.duration this.duration = hash.duration
this.durationLabel = Video.createDurationString(hash.duration) this.durationLabel = Video.createDurationString(hash.duration)
@ -83,8 +86,14 @@ export class Video implements VideoServerModel {
this.by = Account.CREATE_BY_STRING(hash.account.name, hash.account.host) this.by = Account.CREATE_BY_STRING(hash.account.name, hash.account.host)
} }
isVideoNSFWForUser (user: User) { isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {
// If the video is NSFW and the user is not logged in, or the user does not want to display NSFW videos... // Video is not NSFW, skip
return (this.nsfw && (!user || user.displayNSFW === false)) if (this.nsfw === false) return false
// Return user setting if logged in
if (user) return user.nsfwPolicy !== 'display'
// Return default instance config
return serverConfig.instance.defaultNSFWPolicy !== 'display'
} }
} }

View File

@ -100,6 +100,7 @@
<input type="checkbox" id="nsfw" formControlName="nsfw" /> <input type="checkbox" id="nsfw" formControlName="nsfw" />
<label for="nsfw"></label> <label for="nsfw"></label>
<label for="nsfw">This video contains mature or explicit content</label> <label for="nsfw">This video contains mature or explicit content</label>
<my-help tooltipPlacement="top" helpType="custom" customHtml="Some instances do not list NSFW videos by default."></my-help>
</div> </div>
<div class="form-group form-group-checkbox"> <div class="form-group form-group-checkbox">

View File

@ -9,6 +9,10 @@
@include peertube-select-disabled-container(auto); @include peertube-select-disabled-container(auto);
} }
.form-group-checkbox {
my-help { margin-left: 5px }
}
.video-edit { .video-edit {
height: 100%; height: 100%;

View File

@ -22,6 +22,7 @@ import { VideoDownloadComponent } from './modal/video-download.component'
import { VideoReportComponent } from './modal/video-report.component' import { VideoReportComponent } from './modal/video-report.component'
import { VideoShareComponent } from './modal/video-share.component' import { VideoShareComponent } from './modal/video-share.component'
import { getVideojsOptions } from '../../../assets/player/peertube-player' import { getVideojsOptions } from '../../../assets/player/peertube-player'
import { ServerService } from '@app/core'
@Component({ @Component({
selector: 'my-video-watch', selector: 'my-video-watch',
@ -66,6 +67,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
private confirmService: ConfirmService, private confirmService: ConfirmService,
private metaService: MetaService, private metaService: MetaService,
private authService: AuthService, private authService: AuthService,
private serverService: ServerService,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
private markdownService: MarkdownService, private markdownService: MarkdownService,
private zone: NgZone, private zone: NgZone,
@ -335,7 +337,7 @@ export class VideoWatchComponent implements OnInit, OnDestroy {
this.updateOtherVideosDisplayed() this.updateOtherVideosDisplayed()
if (this.video.isVideoNSFWForUser(this.user)) { if (this.video.isVideoNSFWForUser(this.user, this.serverService.getConfig())) {
const res = await this.confirmService.confirm( const res = await this.confirmService.confirm(
'This video contains mature or explicit content. Are you sure you want to watch it?', 'This video contains mature or explicit content. Are you sure you want to watch it?',
'Mature or explicit content' 'Mature or explicit content'

View File

@ -11,6 +11,12 @@
<body> <body>
<div id="error-block">
<h1 id="error-title">Sorry</h1>
<div id="error-content"></div>
</div>
<video id="video-container" class="video-js vjs-peertube-skin"> <video id="video-container" class="video-js vjs-peertube-skin">
</video> </video>

View File

@ -4,6 +4,16 @@
@import '~videojs-dock/dist/videojs-dock.css'; @import '~videojs-dock/dist/videojs-dock.css';
@import '../../sass/video-js-custom.scss'; @import '../../sass/video-js-custom.scss';
[hidden] {
display: none !important;
}
body {
font-family: $main-fonts;
font-weight: $font-regular;
color: #000;
}
video { video {
width: 99%; width: 99%;
} }
@ -43,3 +53,38 @@ html, body {
} }
} }
} }
#error-block {
display: none;
flex-direction: column;
align-content: center;
justify-content: center;
text-align: center;
background-color: #141313;
width: 100%;
height: 100%;
color: white;
box-sizing: border-box;
font-family: sans-serif;
#error-title {
font-size: 45px;
margin-bottom: 5px;
}
#error-content {
font-size: 24px;
}
}
@media screen and (max-width: 300px) {
#error-block {
font-size: 36px;
#error-content {
font-size: 14px;
}
}
}

View File

@ -9,19 +9,53 @@ function getVideoUrl (id: string) {
return window.location.origin + '/api/v1/videos/' + id return window.location.origin + '/api/v1/videos/' + id
} }
async function loadVideoInfo (videoId: string): Promise<VideoDetails> { function loadVideoInfo (videoId: string): Promise<Response> {
const response = await fetch(getVideoUrl(videoId)) return fetch(getVideoUrl(videoId))
return response.json() }
function removeElement (element: HTMLElement) {
element.parentElement.removeChild(element)
}
function displayError (videoElement: HTMLVideoElement, text: string) {
// Remove video element
removeElement(videoElement)
document.title = 'Sorry - ' + text
const errorBlock = document.getElementById('error-block')
errorBlock.style.display = 'flex'
const errorText = document.getElementById('error-content')
errorText.innerHTML = text
}
function videoNotFound (videoElement: HTMLVideoElement) {
const text = 'This video does not exist.'
displayError(videoElement, text)
}
function videoFetchError (videoElement: HTMLVideoElement) {
const text = 'We cannot fetch the video. Please try again later.'
displayError(videoElement, text)
} }
const urlParts = window.location.href.split('/') const urlParts = window.location.href.split('/')
const videoId = urlParts[urlParts.length - 1] const videoId = urlParts[urlParts.length - 1]
loadVideoInfo(videoId) loadVideoInfo(videoId)
.then(videoInfo => { .then(async response => {
const videoContainerId = 'video-container' const videoContainerId = 'video-container'
const videoElement = document.getElementById(videoContainerId) as HTMLVideoElement const videoElement = document.getElementById(videoContainerId) as HTMLVideoElement
if (!response.ok) {
if (response.status === 404) return videoNotFound(videoElement)
return videoFetchError(videoElement)
}
const videoInfo: VideoDetails = await response.json()
let autoplay = false let autoplay = false
let startTime = 0 let startTime = 0

View File

@ -84,6 +84,9 @@ instance:
description: 'Welcome to this PeerTube instance!' # Support markdown description: 'Welcome to this PeerTube instance!' # Support markdown
terms: 'No terms for now.' # Support markdown terms: 'No terms for now.' # Support markdown
default_client_route: '/videos/trending' default_client_route: '/videos/trending'
# By default, "do_not_list" or "blur" or "display" NSFW videos
# Could be overridden per user with a setting
default_nsfw_policy: 'do_not_list'
customizations: customizations:
javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime
css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime

View File

@ -100,6 +100,9 @@ instance:
description: '' # Support markdown description: '' # Support markdown
terms: '' # Support markdown terms: '' # Support markdown
default_client_route: '/videos/trending' default_client_route: '/videos/trending'
# By default, "do_not_list" or "blur" or "display" NSFW videos
# Could be overridden per user with a setting
default_nsfw_policy: 'do_not_list'
customizations: customizations:
javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime javascript: '' # Directly your JavaScript code (without <script> tags). Will be eval at runtime
css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime css: '' # Directly your CSS code (without <style> tags). Will be injected at runtime

View File

@ -32,3 +32,6 @@ transcoding:
480p: true 480p: true
720p: true 720p: true
1080p: true 1080p: true
instance:
default_nsfw_policy: 'display'

View File

@ -47,6 +47,7 @@
"nodemon": "nodemon", "nodemon": "nodemon",
"ts-node": "ts-node", "ts-node": "ts-node",
"tslint": "tslint", "tslint": "tslint",
"mocha": "mocha",
"travis": "scripty", "travis": "scripty",
"release": "scripty", "release": "scripty",
"client-report": "scripty" "client-report": "scripty"

View File

@ -46,6 +46,7 @@ async function getConfig (req: express.Request, res: express.Response, next: exp
name: CONFIG.INSTANCE.NAME, name: CONFIG.INSTANCE.NAME,
shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION, shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE, defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
customizations: { customizations: {
javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT, javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT,
css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS
@ -128,6 +129,7 @@ async function updateCustomConfig (req: express.Request, res: express.Response,
toUpdateJSON.user['video_quota'] = toUpdate.user.videoQuota toUpdateJSON.user['video_quota'] = toUpdate.user.videoQuota
toUpdateJSON.instance['default_client_route'] = toUpdate.instance.defaultClientRoute toUpdateJSON.instance['default_client_route'] = toUpdate.instance.defaultClientRoute
toUpdateJSON.instance['short_description'] = toUpdate.instance.shortDescription toUpdateJSON.instance['short_description'] = toUpdate.instance.shortDescription
toUpdateJSON.instance['default_nsfw_policy'] = toUpdate.instance.defaultNSFWPolicy
await writeFilePromise(CONFIG.CUSTOM_FILE, JSON.stringify(toUpdateJSON, undefined, 2)) await writeFilePromise(CONFIG.CUSTOM_FILE, JSON.stringify(toUpdateJSON, undefined, 2))
@ -153,6 +155,7 @@ function customConfig (): CustomConfig {
description: CONFIG.INSTANCE.DESCRIPTION, description: CONFIG.INSTANCE.DESCRIPTION,
terms: CONFIG.INSTANCE.TERMS, terms: CONFIG.INSTANCE.TERMS,
defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE, defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
customizations: { customizations: {
css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS, css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS,
javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT

View File

@ -42,6 +42,7 @@ import { AccountVideoRateModel } from '../../models/account/account-video-rate'
import { UserModel } from '../../models/account/user' import { UserModel } from '../../models/account/user'
import { OAuthTokenModel } from '../../models/oauth/oauth-token' import { OAuthTokenModel } from '../../models/oauth/oauth-token'
import { VideoModel } from '../../models/video/video' import { VideoModel } from '../../models/video/video'
import { VideoSortField } from '../../../client/src/app/shared/video/sort-field.type'
const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR }) const reqAvatarFile = createReqFiles([ 'avatarfile' ], IMAGE_MIMETYPE_EXT, { avatarfile: CONFIG.STORAGE.AVATARS_DIR })
const loginRateLimiter = new RateLimit({ const loginRateLimiter = new RateLimit({
@ -161,7 +162,13 @@ export {
async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) { async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
const user = res.locals.oauth.token.User as UserModel const user = res.locals.oauth.token.User as UserModel
const resultList = await VideoModel.listAccountVideosForApi(user.Account.id ,req.query.start, req.query.count, req.query.sort) const resultList = await VideoModel.listAccountVideosForApi(
user.Account.id,
req.query.start as number,
req.query.count as number,
req.query.sort as VideoSortField,
false // Display my NSFW videos
)
return res.json(getFormattedObjects(resultList.data, resultList.total)) return res.json(getFormattedObjects(resultList.data, resultList.total))
} }
@ -188,7 +195,7 @@ async function createUser (req: express.Request) {
username: body.username, username: body.username,
password: body.password, password: body.password,
email: body.email, email: body.email,
displayNSFW: false, nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
autoPlayVideo: true, autoPlayVideo: true,
role: body.role, role: body.role,
videoQuota: body.videoQuota videoQuota: body.videoQuota
@ -219,7 +226,7 @@ async function registerUser (req: express.Request) {
username: body.username, username: body.username,
password: body.password, password: body.password,
email: body.email, email: body.email,
displayNSFW: false, nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
autoPlayVideo: true, autoPlayVideo: true,
role: UserRole.USER, role: UserRole.USER,
videoQuota: CONFIG.USER.VIDEO_QUOTA videoQuota: CONFIG.USER.VIDEO_QUOTA
@ -286,7 +293,7 @@ async function updateMe (req: express.Request, res: express.Response, next: expr
if (body.password !== undefined) user.password = body.password if (body.password !== undefined) user.password = body.password
if (body.email !== undefined) user.email = body.email if (body.email !== undefined) user.email = body.email
if (body.displayNSFW !== undefined) user.displayNSFW = body.displayNSFW if (body.nsfwPolicy !== undefined) user.nsfwPolicy = body.nsfwPolicy
if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo if (body.autoPlayVideo !== undefined) user.autoPlayVideo = body.autoPlayVideo
await sequelizeTypescript.transaction(async t => { await sequelizeTypescript.transaction(async t => {

View File

@ -19,13 +19,18 @@ import {
VIDEO_MIMETYPE_EXT, VIDEO_MIMETYPE_EXT,
VIDEO_PRIVACIES VIDEO_PRIVACIES
} from '../../../initializers' } from '../../../initializers'
import { fetchRemoteVideoDescription, getVideoActivityPubUrl, shareVideoByServerAndChannel } from '../../../lib/activitypub' import {
fetchRemoteVideoDescription,
getVideoActivityPubUrl,
shareVideoByServerAndChannel
} from '../../../lib/activitypub'
import { sendCreateVideo, sendCreateView, sendUpdateVideo } from '../../../lib/activitypub/send' import { sendCreateVideo, sendCreateView, sendUpdateVideo } from '../../../lib/activitypub/send'
import { JobQueue } from '../../../lib/job-queue' import { JobQueue } from '../../../lib/job-queue'
import { Redis } from '../../../lib/redis' import { Redis } from '../../../lib/redis'
import { import {
asyncMiddleware, asyncMiddleware,
authenticate, authenticate,
optionalAuthenticate,
paginationValidator, paginationValidator,
setDefaultPagination, setDefaultPagination,
setDefaultSort, setDefaultSort,
@ -44,6 +49,9 @@ import { blacklistRouter } from './blacklist'
import { videoChannelRouter } from './channel' import { videoChannelRouter } from './channel'
import { videoCommentRouter } from './comment' import { videoCommentRouter } from './comment'
import { rateVideoRouter } from './rate' import { rateVideoRouter } from './rate'
import { User } from '../../../../shared/models/users'
import { VideoFilter } from '../../../../shared/models/videos/video-query.type'
import { VideoSortField } from '../../../../client/src/app/shared/video/sort-field.type'
const videosRouter = express.Router() const videosRouter = express.Router()
@ -81,6 +89,7 @@ videosRouter.get('/',
videosSortValidator, videosSortValidator,
setDefaultSort, setDefaultSort,
setDefaultPagination, setDefaultPagination,
optionalAuthenticate,
asyncMiddleware(listVideos) asyncMiddleware(listVideos)
) )
videosRouter.get('/search', videosRouter.get('/search',
@ -89,6 +98,7 @@ videosRouter.get('/search',
videosSortValidator, videosSortValidator,
setDefaultSort, setDefaultSort,
setDefaultPagination, setDefaultPagination,
optionalAuthenticate,
asyncMiddleware(searchVideos) asyncMiddleware(searchVideos)
) )
videosRouter.put('/:id', videosRouter.put('/:id',
@ -391,7 +401,13 @@ async function getVideoDescription (req: express.Request, res: express.Response)
} }
async function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) { async function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
const resultList = await VideoModel.listForApi(req.query.start, req.query.count, req.query.sort, req.query.filter) const resultList = await VideoModel.listForApi(
req.query.start as number,
req.query.count as number,
req.query.sort as VideoSortField,
isNSFWHidden(res),
req.query.filter as VideoFilter
)
return res.json(getFormattedObjects(resultList.data, resultList.total)) return res.json(getFormattedObjects(resultList.data, resultList.total))
} }
@ -419,11 +435,21 @@ async function removeVideo (req: express.Request, res: express.Response) {
async function searchVideos (req: express.Request, res: express.Response, next: express.NextFunction) { async function searchVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
const resultList = await VideoModel.searchAndPopulateAccountAndServer( const resultList = await VideoModel.searchAndPopulateAccountAndServer(
req.query.search, req.query.search as string,
req.query.start, req.query.start as number,
req.query.count, req.query.count as number,
req.query.sort req.query.sort as VideoSortField,
isNSFWHidden(res)
) )
return res.json(getFormattedObjects(resultList.data, resultList.total)) return res.json(getFormattedObjects(resultList.data, resultList.total))
} }
function isNSFWHidden (res: express.Response) {
if (res.locals.oauth) {
const user: User = res.locals.oauth.token.User
if (user) return user.nsfwPolicy === 'do_not_list'
}
return CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list'
}

View File

@ -6,6 +6,7 @@ import * as Feed from 'pfeed'
import { ResultList } from '../../shared/models' import { ResultList } from '../../shared/models'
import { AccountModel } from '../models/account/account' import { AccountModel } from '../models/account/account'
import { cacheRoute } from '../middlewares/cache' import { cacheRoute } from '../middlewares/cache'
import { VideoSortField } from '../../client/src/app/shared/video/sort-field.type'
const feedsRouter = express.Router() const feedsRouter = express.Router()
@ -31,20 +32,22 @@ async function generateFeed (req: express.Request, res: express.Response, next:
let resultList: ResultList<VideoModel> let resultList: ResultList<VideoModel>
const account: AccountModel = res.locals.account const account: AccountModel = res.locals.account
const hideNSFW = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list'
if (account) { if (account) {
resultList = await VideoModel.listAccountVideosForApi( resultList = await VideoModel.listAccountVideosForApi(
account.id, account.id,
start, start,
FEEDS.COUNT, FEEDS.COUNT,
req.query.sort, req.query.sort as VideoSortField,
true hideNSFW
) )
} else { } else {
resultList = await VideoModel.listForApi( resultList = await VideoModel.listForApi(
start, start,
FEEDS.COUNT, FEEDS.COUNT,
req.query.sort, req.query.sort as VideoSortField,
hideNSFW,
req.query.filter, req.query.filter,
true true
) )

View File

@ -1,9 +1,10 @@
import 'express-validator' import 'express-validator'
import * as validator from 'validator' import * as validator from 'validator'
import { UserRole } from '../../../shared' import { UserRole } from '../../../shared'
import { CONSTRAINTS_FIELDS } from '../../initializers' import { CONSTRAINTS_FIELDS, NSFW_POLICY_TYPES } from '../../initializers'
import { exists, isFileValid } from './misc' import { exists, isFileValid } from './misc'
import { values } from 'lodash'
const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS const USERS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.USERS
@ -29,8 +30,9 @@ function isBoolean (value: any) {
return typeof value === 'boolean' || (typeof value === 'string' && validator.isBoolean(value)) return typeof value === 'boolean' || (typeof value === 'string' && validator.isBoolean(value))
} }
function isUserDisplayNSFWValid (value: any) { const nsfwPolicies = values(NSFW_POLICY_TYPES)
return isBoolean(value) function isUserNSFWPolicyValid (value: any) {
return exists(value) && nsfwPolicies.indexOf(value) !== -1
} }
function isUserAutoPlayVideoValid (value: any) { function isUserAutoPlayVideoValid (value: any) {
@ -56,7 +58,7 @@ export {
isUserRoleValid, isUserRoleValid,
isUserVideoQuotaValid, isUserVideoQuotaValid,
isUserUsernameValid, isUserUsernameValid,
isUserDisplayNSFWValid, isUserNSFWPolicyValid,
isUserAutoPlayVideoValid, isUserAutoPlayVideoValid,
isUserDescriptionValid, isUserDescriptionValid,
isAvatarFile isAvatarFile

View File

@ -5,12 +5,12 @@ import { ApplicationModel } from '../models/application/application'
import { OAuthClientModel } from '../models/oauth/oauth-client' import { OAuthClientModel } from '../models/oauth/oauth-client'
// Some checks on configuration files // Some checks on configuration files
// Return an error message, or null if everything is okay
function checkConfig () { function checkConfig () {
if (config.has('webserver.host')) { const defaultNSFWPolicy = config.get<string>('instance.default_nsfw_policy')
let errorMessage = '`host` config key was renamed to `hostname` but it seems you still have a `host` key in your configuration files!'
errorMessage += ' Please ensure to rename your `host` configuration to `hostname`.'
return errorMessage if ([ 'do_not_list', 'blur', 'display' ].indexOf(defaultNSFWPolicy) === -1) {
return 'NSFW policy setting should be "do_not_list" or "blur" or "display" instead of ' + defaultNSFWPolicy
} }
return null return null
@ -28,7 +28,8 @@ function checkMissedConfig () {
'log.level', 'log.level',
'user.video_quota', 'user.video_quota',
'cache.previews.size', 'admin.email', 'signup.enabled', 'signup.limit', 'transcoding.enabled', 'transcoding.threads', 'cache.previews.size', 'admin.email', 'signup.enabled', 'signup.limit', 'transcoding.enabled', 'transcoding.threads',
'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route' 'instance.name', 'instance.short_description', 'instance.description', 'instance.terms', 'instance.default_client_route',
'instance.default_nsfw_policy'
] ]
const miss: string[] = [] const miss: string[] = []

View File

@ -6,13 +6,14 @@ import { FollowState } from '../../shared/models/actors'
import { VideoPrivacy } from '../../shared/models/videos' import { VideoPrivacy } from '../../shared/models/videos'
// Do not use barrels, remain constants as independent as possible // Do not use barrels, remain constants as independent as possible
import { buildPath, isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils' import { buildPath, isTestInstance, root, sanitizeHost, sanitizeUrl } from '../helpers/core-utils'
import { NSFWPolicyType } from '../../shared/models/videos/nsfw-policy.type'
// Use a variable to reload the configuration if we need // Use a variable to reload the configuration if we need
let config: IConfig = require('config') let config: IConfig = require('config')
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
const LAST_MIGRATION_VERSION = 200 const LAST_MIGRATION_VERSION = 205
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -167,6 +168,7 @@ const CONFIG = {
get DESCRIPTION () { return config.get<string>('instance.description') }, get DESCRIPTION () { return config.get<string>('instance.description') },
get TERMS () { return config.get<string>('instance.terms') }, get TERMS () { return config.get<string>('instance.terms') },
get DEFAULT_CLIENT_ROUTE () { return config.get<string>('instance.default_client_route') }, get DEFAULT_CLIENT_ROUTE () { return config.get<string>('instance.default_client_route') },
get DEFAULT_NSFW_POLICY () { return config.get<NSFWPolicyType>('instance.default_nsfw_policy') },
CUSTOMIZATIONS: { CUSTOMIZATIONS: {
get JAVASCRIPT () { return config.get<string>('instance.customizations.javascript') }, get JAVASCRIPT () { return config.get<string>('instance.customizations.javascript') },
get CSS () { return config.get<string>('instance.customizations.css') } get CSS () { return config.get<string>('instance.customizations.css') }
@ -378,6 +380,12 @@ const BCRYPT_SALT_SIZE = 10
const USER_PASSWORD_RESET_LIFETIME = 60000 * 5 // 5 minutes const USER_PASSWORD_RESET_LIFETIME = 60000 * 5 // 5 minutes
const NSFW_POLICY_TYPES: { [ id: string]: NSFWPolicyType } = {
DO_NOT_LIST: 'do_not_list',
BLUR: 'blur',
DISPLAY: 'display'
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Express static paths (router) // Express static paths (router)
@ -474,6 +482,7 @@ export {
PRIVATE_RSA_KEY_SIZE, PRIVATE_RSA_KEY_SIZE,
SORTABLE_COLUMNS, SORTABLE_COLUMNS,
FEEDS, FEEDS,
NSFW_POLICY_TYPES,
STATIC_MAX_AGE, STATIC_MAX_AGE,
STATIC_PATHS, STATIC_PATHS,
ACTIVITY_PUB, ACTIVITY_PUB,

View File

@ -120,6 +120,7 @@ async function createOAuthAdminIfNotExist () {
email, email,
password, password,
role, role,
nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
videoQuota: -1 videoQuota: -1
} }
const user = new UserModel(userData) const user = new UserModel(userData)

View File

@ -0,0 +1,46 @@
import * as Sequelize from 'sequelize'
async function up (utils: {
transaction: Sequelize.Transaction,
queryInterface: Sequelize.QueryInterface,
sequelize: Sequelize.Sequelize
}): Promise<void> {
{
const data = {
type: Sequelize.ENUM('do_not_list', 'blur', 'display'),
allowNull: true,
defaultValue: null
}
await utils.queryInterface.addColumn('user', 'nsfwPolicy', data)
}
{
const query = 'UPDATE "user" SET "nsfwPolicy" = \'do_not_list\''
await utils.sequelize.query(query)
}
{
const query = 'UPDATE "user" SET "nsfwPolicy" = \'display\' WHERE "displayNSFW" = true'
await utils.sequelize.query(query)
}
{
const query = 'ALTER TABLE "user" ALTER COLUMN "nsfwPolicy" SET NOT NULL'
await utils.sequelize.query(query)
}
{
await utils.queryInterface.removeColumn('user', 'displayNSFW')
}
}
function down (options) {
throw new Error('Not implemented.')
}
export {
up,
down
}

View File

@ -2,6 +2,7 @@ import * as express from 'express'
import * as OAuthServer from 'express-oauth-server' import * as OAuthServer from 'express-oauth-server'
import 'express-validator' import 'express-validator'
import { OAUTH_LIFETIME } from '../initializers' import { OAUTH_LIFETIME } from '../initializers'
import { logger } from '../helpers/logger'
const oAuthServer = new OAuthServer({ const oAuthServer = new OAuthServer({
useErrorHandler: true, useErrorHandler: true,
@ -13,6 +14,8 @@ const oAuthServer = new OAuthServer({
function authenticate (req: express.Request, res: express.Response, next: express.NextFunction) { function authenticate (req: express.Request, res: express.Response, next: express.NextFunction) {
oAuthServer.authenticate()(req, res, err => { oAuthServer.authenticate()(req, res, err => {
if (err) { if (err) {
logger.warn('Cannot authenticate.', { err })
return res.status(err.status) return res.status(err.status)
.json({ .json({
error: 'Token is invalid.', error: 'Token is invalid.',
@ -25,6 +28,12 @@ function authenticate (req: express.Request, res: express.Response, next: expres
}) })
} }
function optionalAuthenticate (req: express.Request, res: express.Response, next: express.NextFunction) {
if (req.header('authorization')) return authenticate(req, res, next)
return next()
}
function token (req: express.Request, res: express.Response, next: express.NextFunction) { function token (req: express.Request, res: express.Response, next: express.NextFunction) {
return oAuthServer.token()(req, res, err => { return oAuthServer.token()(req, res, err => {
if (err) { if (err) {
@ -44,5 +53,6 @@ function token (req: express.Request, res: express.Response, next: express.NextF
export { export {
authenticate, authenticate,
optionalAuthenticate,
token token
} }

View File

@ -1,6 +1,6 @@
import * as express from 'express' import * as express from 'express'
import { body } from 'express-validator/check' import { body } from 'express-validator/check'
import { isUserVideoQuotaValid } from '../../helpers/custom-validators/users' import { isUserNSFWPolicyValid, isUserVideoQuotaValid } from '../../helpers/custom-validators/users'
import { logger } from '../../helpers/logger' import { logger } from '../../helpers/logger'
import { areValidationErrors } from './utils' import { areValidationErrors } from './utils'
@ -9,6 +9,7 @@ const customConfigUpdateValidator = [
body('instance.description').exists().withMessage('Should have a valid instance description'), body('instance.description').exists().withMessage('Should have a valid instance description'),
body('instance.terms').exists().withMessage('Should have a valid instance terms'), body('instance.terms').exists().withMessage('Should have a valid instance terms'),
body('instance.defaultClientRoute').exists().withMessage('Should have a valid instance default client route'), body('instance.defaultClientRoute').exists().withMessage('Should have a valid instance default client route'),
body('instance.defaultNSFWPolicy').custom(isUserNSFWPolicyValid).withMessage('Should have a valid NSFW policy'),
body('instance.customizations.css').exists().withMessage('Should have a valid instance CSS customization'), body('instance.customizations.css').exists().withMessage('Should have a valid instance CSS customization'),
body('instance.customizations.javascript').exists().withMessage('Should have a valid instance JavaScript customization'), body('instance.customizations.javascript').exists().withMessage('Should have a valid instance JavaScript customization'),
body('cache.previews.size').isInt().withMessage('Should have a valid previews size'), body('cache.previews.size').isInt().withMessage('Should have a valid previews size'),

View File

@ -8,7 +8,7 @@ import {
isAvatarFile, isAvatarFile,
isUserAutoPlayVideoValid, isUserAutoPlayVideoValid,
isUserDescriptionValid, isUserDescriptionValid,
isUserDisplayNSFWValid, isUserNSFWPolicyValid,
isUserPasswordValid, isUserPasswordValid,
isUserRoleValid, isUserRoleValid,
isUserUsernameValid, isUserUsernameValid,
@ -101,7 +101,7 @@ const usersUpdateMeValidator = [
body('description').optional().custom(isUserDescriptionValid).withMessage('Should have a valid description'), body('description').optional().custom(isUserDescriptionValid).withMessage('Should have a valid description'),
body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'), body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'),
body('email').optional().isEmail().withMessage('Should have a valid email attribute'), body('email').optional().isEmail().withMessage('Should have a valid email attribute'),
body('displayNSFW').optional().custom(isUserDisplayNSFWValid).withMessage('Should have a valid display Not Safe For Work attribute'), body('nsfwPolicy').optional().custom(isUserNSFWPolicyValid).withMessage('Should have a valid display Not Safe For Work policy'),
body('autoPlayVideo').optional().custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'), body('autoPlayVideo').optional().custom(isUserAutoPlayVideoValid).withMessage('Should have a valid automatically plays video attribute'),
(req: express.Request, res: express.Response, next: express.NextFunction) => { (req: express.Request, res: express.Response, next: express.NextFunction) => {

View File

@ -21,7 +21,7 @@ import { hasUserRight, USER_ROLE_LABELS, UserRight } from '../../../shared'
import { User, UserRole } from '../../../shared/models/users' import { User, UserRole } from '../../../shared/models/users'
import { import {
isUserAutoPlayVideoValid, isUserAutoPlayVideoValid,
isUserDisplayNSFWValid, isUserNSFWPolicyValid,
isUserPasswordValid, isUserPasswordValid,
isUserRoleValid, isUserRoleValid,
isUserUsernameValid, isUserUsernameValid,
@ -32,6 +32,9 @@ import { OAuthTokenModel } from '../oauth/oauth-token'
import { getSort, throwIfNotValid } from '../utils' import { getSort, throwIfNotValid } from '../utils'
import { VideoChannelModel } from '../video/video-channel' import { VideoChannelModel } from '../video/video-channel'
import { AccountModel } from './account' import { AccountModel } from './account'
import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
import { values } from 'lodash'
import { NSFW_POLICY_TYPES } from '../../initializers'
@DefaultScope({ @DefaultScope({
include: [ include: [
@ -83,10 +86,9 @@ export class UserModel extends Model<UserModel> {
email: string email: string
@AllowNull(false) @AllowNull(false)
@Default(false) @Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy'))
@Is('UserDisplayNSFW', value => throwIfNotValid(value, isUserDisplayNSFWValid, 'display NSFW boolean')) @Column(DataType.ENUM(values(NSFW_POLICY_TYPES)))
@Column nsfwPolicy: NSFWPolicyType
displayNSFW: boolean
@AllowNull(false) @AllowNull(false)
@Default(true) @Default(true)
@ -265,7 +267,7 @@ export class UserModel extends Model<UserModel> {
id: this.id, id: this.id,
username: this.username, username: this.username,
email: this.email, email: this.email,
displayNSFW: this.displayNSFW, nsfwPolicy: this.nsfwPolicy,
autoPlayVideo: this.autoPlayVideo, autoPlayVideo: this.autoPlayVideo,
role: this.role, role: this.role,
roleLabel: USER_ROLE_LABELS[ this.role ], roleLabel: USER_ROLE_LABELS[ this.role ],

View File

@ -95,7 +95,7 @@ enum ScopeNames {
} }
@Scopes({ @Scopes({
[ScopeNames.AVAILABLE_FOR_LIST]: (actorId: number, filter?: VideoFilter, withFiles?: boolean) => { [ScopeNames.AVAILABLE_FOR_LIST]: (actorId: number, hideNSFW: boolean, filter?: VideoFilter, withFiles?: boolean) => {
const query: IFindOptions<VideoModel> = { const query: IFindOptions<VideoModel> = {
where: { where: {
id: { id: {
@ -161,6 +161,11 @@ enum ScopeNames {
}) })
} }
// Hide nsfw videos?
if (hideNSFW === true) {
query.where['nsfw'] = false
}
return query return query
}, },
[ScopeNames.WITH_ACCOUNT_DETAILS]: { [ScopeNames.WITH_ACCOUNT_DETAILS]: {
@ -640,7 +645,7 @@ export class VideoModel extends Model<VideoModel> {
}) })
} }
static listAccountVideosForApi (accountId: number, start: number, count: number, sort: string, withFiles = false) { static listAccountVideosForApi (accountId: number, start: number, count: number, sort: string, hideNSFW: boolean, withFiles = false) {
const query: IFindOptions<VideoModel> = { const query: IFindOptions<VideoModel> = {
offset: start, offset: start,
limit: count, limit: count,
@ -669,6 +674,12 @@ export class VideoModel extends Model<VideoModel> {
}) })
} }
if (hideNSFW === true) {
query.where = {
nsfw: false
}
}
return VideoModel.findAndCountAll(query).then(({ rows, count }) => { return VideoModel.findAndCountAll(query).then(({ rows, count }) => {
return { return {
data: rows, data: rows,
@ -677,7 +688,7 @@ export class VideoModel extends Model<VideoModel> {
}) })
} }
static async listForApi (start: number, count: number, sort: string, filter?: VideoFilter, withFiles = false) { static async listForApi (start: number, count: number, sort: string, hideNSFW: boolean, filter?: VideoFilter, withFiles = false) {
const query = { const query = {
offset: start, offset: start,
limit: count, limit: count,
@ -685,8 +696,7 @@ export class VideoModel extends Model<VideoModel> {
} }
const serverActor = await getServerActor() const serverActor = await getServerActor()
return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, hideNSFW, filter, withFiles ] })
return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, filter, withFiles ] })
.findAndCountAll(query) .findAndCountAll(query)
.then(({ rows, count }) => { .then(({ rows, count }) => {
return { return {
@ -696,7 +706,7 @@ export class VideoModel extends Model<VideoModel> {
}) })
} }
static async searchAndPopulateAccountAndServer (value: string, start: number, count: number, sort: string) { static async searchAndPopulateAccountAndServer (value: string, start: number, count: number, sort: string, hideNSFW: boolean) {
const query: IFindOptions<VideoModel> = { const query: IFindOptions<VideoModel> = {
offset: start, offset: start,
limit: count, limit: count,
@ -724,7 +734,7 @@ export class VideoModel extends Model<VideoModel> {
const serverActor = await getServerActor() const serverActor = await getServerActor()
return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id ] }) return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, hideNSFW ] })
.findAndCountAll(query) .findAndCountAll(query)
.then(({ rows, count }) => { .then(({ rows, count }) => {
return { return {
@ -874,6 +884,13 @@ export class VideoModel extends Model<VideoModel> {
return languageLabel return languageLabel
} }
private static getPrivacyLabel (id: number) {
let privacyLabel = VIDEO_PRIVACIES[id]
if (!privacyLabel) privacyLabel = 'Unknown'
return privacyLabel
}
getOriginalFile () { getOriginalFile () {
if (Array.isArray(this.VideoFiles) === false) return undefined if (Array.isArray(this.VideoFiles) === false) return undefined
@ -927,8 +944,11 @@ export class VideoModel extends Model<VideoModel> {
return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile)) return join(CONFIG.STORAGE.VIDEOS_DIR, this.getVideoFilename(videoFile))
} }
createTorrentAndSetInfoHash = async function (videoFile: VideoFileModel) { async createTorrentAndSetInfoHash (videoFile: VideoFileModel) {
const options = { const options = {
// Keep the extname, it's used by the client to stream the file inside a web browser
name: `${this.name} ${videoFile.resolution}p${videoFile.extname}`,
createdBy: 'PeerTube',
announceList: [ announceList: [
[ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ], [ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ],
[ CONFIG.WEBSERVER.URL + '/tracker/announce' ] [ CONFIG.WEBSERVER.URL + '/tracker/announce' ]
@ -980,6 +1000,10 @@ export class VideoModel extends Model<VideoModel> {
id: this.language, id: this.language,
label: VideoModel.getLanguageLabel(this.language) label: VideoModel.getLanguageLabel(this.language)
}, },
privacy: {
id: this.privacy,
label: VideoModel.getPrivacyLabel(this.privacy)
},
nsfw: this.nsfw, nsfw: this.nsfw,
description: this.getTruncatedDescription(), description: this.getTruncatedDescription(),
isLocal: this.isOwned(), isLocal: this.isOwned(),
@ -1006,15 +1030,7 @@ export class VideoModel extends Model<VideoModel> {
toFormattedDetailsJSON (): VideoDetails { toFormattedDetailsJSON (): VideoDetails {
const formattedJson = this.toFormattedJSON() const formattedJson = this.toFormattedJSON()
// Maybe our server is not up to date and there are new privacy settings since our version
let privacyLabel = VIDEO_PRIVACIES[this.privacy]
if (!privacyLabel) privacyLabel = 'Unknown'
const detailsJson = { const detailsJson = {
privacy: {
id: this.privacy,
label: privacyLabel
},
support: this.support, support: this.support,
descriptionPath: this.getDescriptionPath(), descriptionPath: this.getDescriptionPath(),
channel: this.VideoChannel.toFormattedJSON(), channel: this.VideoChannel.toFormattedJSON(),
@ -1227,7 +1243,7 @@ export class VideoModel extends Model<VideoModel> {
return peertubeTruncate(this.description, maxLength) return peertubeTruncate(this.description, maxLength)
} }
optimizeOriginalVideofile = async function () { async optimizeOriginalVideofile () {
const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
const newExtname = '.mp4' const newExtname = '.mp4'
const inputVideoFile = this.getOriginalFile() const inputVideoFile = this.getOriginalFile()
@ -1264,7 +1280,7 @@ export class VideoModel extends Model<VideoModel> {
} }
} }
transcodeOriginalVideofile = async function (resolution: VideoResolution, isPortraitMode: boolean) { async transcodeOriginalVideofile (resolution: VideoResolution, isPortraitMode: boolean) {
const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
const extname = '.mp4' const extname = '.mp4'

View File

@ -6,7 +6,7 @@ import { CustomConfig } from '../../../../shared/models/server/custom-config.mod
import { import {
createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, runServer, ServerInfo, createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, runServer, ServerInfo,
setAccessTokensToServers, userLogin setAccessTokensToServers, userLogin, immutableAssign
} from '../../utils' } from '../../utils'
describe('Test config API validators', function () { describe('Test config API validators', function () {
@ -20,6 +20,7 @@ describe('Test config API validators', function () {
description: 'my super description', description: 'my super description',
terms: 'my super terms', terms: 'my super terms',
defaultClientRoute: '/videos/recently-added', defaultClientRoute: '/videos/recently-added',
defaultNSFWPolicy: 'blur',
customizations: { customizations: {
javascript: 'alert("coucou")', javascript: 'alert("coucou")',
css: 'body { background-color: red; }' css: 'body { background-color: red; }'
@ -122,6 +123,22 @@ describe('Test config API validators', function () {
}) })
}) })
it('Should fail with a bad default NSFW policy', async function () {
const newUpdateParams = immutableAssign(updateParams, {
instance: {
defaultNSFWPolicy: 'hello'
}
})
await makePutBodyRequest({
url: server.url,
path,
fields: newUpdateParams,
token: server.accessToken,
statusCodeExpected: 400
})
})
it('Should success with the correct parameters', async function () { it('Should success with the correct parameters', async function () {
await makePutBodyRequest({ await makePutBodyRequest({
url: server.url, url: server.url,

View File

@ -231,9 +231,9 @@ describe('Test users API validators', function () {
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields }) await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
}) })
it('Should fail with an invalid display NSFW attribute', async function () { it('Should fail with an invalid NSFW policy attribute', async function () {
const fields = { const fields = {
displayNSFW: -1 nsfwPolicy: 'hello'
} }
await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields }) await makePutBodyRequest({ url: server.url, path: path + 'me', token: userAccessToken, fields })
@ -266,7 +266,7 @@ describe('Test users API validators', function () {
it('Should succeed with the correct params', async function () { it('Should succeed with the correct params', async function () {
const fields = { const fields = {
password: 'my super password', password: 'my super password',
displayNSFW: true, nsfwPolicy: 'blur',
autoPlayVideo: false, autoPlayVideo: false,
email: 'super_email@example.com' email: 'super_email@example.com'
} }

View File

@ -7,6 +7,7 @@ import './videos/video-abuse'
import './videos/video-blacklist' import './videos/video-blacklist'
import './videos/video-blacklist-management' import './videos/video-blacklist-management'
import './videos/video-description' import './videos/video-description'
import './videos/video-nsfw'
import './videos/video-privacy' import './videos/video-privacy'
import './videos/services' import './videos/services'
import './server/email' import './server/email'

View File

@ -59,6 +59,7 @@ describe('Test config', function () {
expect(data.instance.description).to.equal('Welcome to this PeerTube instance!') expect(data.instance.description).to.equal('Welcome to this PeerTube instance!')
expect(data.instance.terms).to.equal('No terms for now.') expect(data.instance.terms).to.equal('No terms for now.')
expect(data.instance.defaultClientRoute).to.equal('/videos/trending') expect(data.instance.defaultClientRoute).to.equal('/videos/trending')
expect(data.instance.defaultNSFWPolicy).to.equal('display')
expect(data.instance.customizations.css).to.be.empty expect(data.instance.customizations.css).to.be.empty
expect(data.instance.customizations.javascript).to.be.empty expect(data.instance.customizations.javascript).to.be.empty
expect(data.cache.previews.size).to.equal(1) expect(data.cache.previews.size).to.equal(1)
@ -83,6 +84,7 @@ describe('Test config', function () {
description: 'my super description', description: 'my super description',
terms: 'my super terms', terms: 'my super terms',
defaultClientRoute: '/videos/recently-added', defaultClientRoute: '/videos/recently-added',
defaultNSFWPolicy: 'blur' as 'blur',
customizations: { customizations: {
javascript: 'alert("coucou")', javascript: 'alert("coucou")',
css: 'body { background-color: red; }' css: 'body { background-color: red; }'
@ -125,6 +127,7 @@ describe('Test config', function () {
expect(data.instance.description).to.equal('my super description') expect(data.instance.description).to.equal('my super description')
expect(data.instance.terms).to.equal('my super terms') expect(data.instance.terms).to.equal('my super terms')
expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added') expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added')
expect(data.instance.defaultNSFWPolicy).to.equal('blur')
expect(data.instance.customizations.javascript).to.equal('alert("coucou")') expect(data.instance.customizations.javascript).to.equal('alert("coucou")')
expect(data.instance.customizations.css).to.equal('body { background-color: red; }') expect(data.instance.customizations.css).to.equal('body { background-color: red; }')
expect(data.cache.previews.size).to.equal(2) expect(data.cache.previews.size).to.equal(2)
@ -156,6 +159,7 @@ describe('Test config', function () {
expect(data.instance.description).to.equal('my super description') expect(data.instance.description).to.equal('my super description')
expect(data.instance.terms).to.equal('my super terms') expect(data.instance.terms).to.equal('my super terms')
expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added') expect(data.instance.defaultClientRoute).to.equal('/videos/recently-added')
expect(data.instance.defaultNSFWPolicy).to.equal('blur')
expect(data.instance.customizations.javascript).to.equal('alert("coucou")') expect(data.instance.customizations.javascript).to.equal('alert("coucou")')
expect(data.instance.customizations.css).to.equal('body { background-color: red; }') expect(data.instance.customizations.css).to.equal('body { background-color: red; }')
expect(data.cache.previews.size).to.equal(2) expect(data.cache.previews.size).to.equal(2)
@ -198,6 +202,7 @@ describe('Test config', function () {
expect(data.instance.description).to.equal('Welcome to this PeerTube instance!') expect(data.instance.description).to.equal('Welcome to this PeerTube instance!')
expect(data.instance.terms).to.equal('No terms for now.') expect(data.instance.terms).to.equal('No terms for now.')
expect(data.instance.defaultClientRoute).to.equal('/videos/trending') expect(data.instance.defaultClientRoute).to.equal('/videos/trending')
expect(data.instance.defaultNSFWPolicy).to.equal('display')
expect(data.instance.customizations.css).to.be.empty expect(data.instance.customizations.css).to.be.empty
expect(data.instance.customizations.javascript).to.be.empty expect(data.instance.customizations.javascript).to.be.empty
expect(data.cache.previews.size).to.equal(1) expect(data.cache.previews.size).to.equal(1)

View File

@ -168,7 +168,7 @@ describe('Test users', function () {
expect(user.username).to.equal('user_1') expect(user.username).to.equal('user_1')
expect(user.email).to.equal('user_1@example.com') expect(user.email).to.equal('user_1@example.com')
expect(user.displayNSFW).to.be.false expect(user.nsfwPolicy).to.equal('display')
expect(user.videoQuota).to.equal(2 * 1024 * 1024) expect(user.videoQuota).to.equal(2 * 1024 * 1024)
expect(user.roleLabel).to.equal('User') expect(user.roleLabel).to.equal('User')
expect(user.id).to.be.a('number') expect(user.id).to.be.a('number')
@ -215,12 +215,12 @@ describe('Test users', function () {
const user = users[ 0 ] const user = users[ 0 ]
expect(user.username).to.equal('user_1') expect(user.username).to.equal('user_1')
expect(user.email).to.equal('user_1@example.com') expect(user.email).to.equal('user_1@example.com')
expect(user.displayNSFW).to.be.false expect(user.nsfwPolicy).to.equal('display')
const rootUser = users[ 1 ] const rootUser = users[ 1 ]
expect(rootUser.username).to.equal('root') expect(rootUser.username).to.equal('root')
expect(rootUser.email).to.equal('admin1@example.com') expect(rootUser.email).to.equal('admin1@example.com')
expect(rootUser.displayNSFW).to.be.false expect(user.nsfwPolicy).to.equal('display')
userId = user.id userId = user.id
}) })
@ -239,7 +239,7 @@ describe('Test users', function () {
expect(user.username).to.equal('root') expect(user.username).to.equal('root')
expect(user.email).to.equal('admin1@example.com') expect(user.email).to.equal('admin1@example.com')
expect(user.roleLabel).to.equal('Administrator') expect(user.roleLabel).to.equal('Administrator')
expect(user.displayNSFW).to.be.false expect(user.nsfwPolicy).to.equal('display')
}) })
it('Should list only the first user by username desc', async function () { it('Should list only the first user by username desc', async function () {
@ -254,7 +254,7 @@ describe('Test users', function () {
const user = users[ 0 ] const user = users[ 0 ]
expect(user.username).to.equal('user_1') expect(user.username).to.equal('user_1')
expect(user.email).to.equal('user_1@example.com') expect(user.email).to.equal('user_1@example.com')
expect(user.displayNSFW).to.be.false expect(user.nsfwPolicy).to.equal('display')
}) })
it('Should list only the second user by createdAt desc', async function () { it('Should list only the second user by createdAt desc', async function () {
@ -269,7 +269,7 @@ describe('Test users', function () {
const user = users[ 0 ] const user = users[ 0 ]
expect(user.username).to.equal('user_1') expect(user.username).to.equal('user_1')
expect(user.email).to.equal('user_1@example.com') expect(user.email).to.equal('user_1@example.com')
expect(user.displayNSFW).to.be.false expect(user.nsfwPolicy).to.equal('display')
}) })
it('Should list all the users by createdAt asc', async function () { it('Should list all the users by createdAt asc', async function () {
@ -283,11 +283,11 @@ describe('Test users', function () {
expect(users[ 0 ].username).to.equal('root') expect(users[ 0 ].username).to.equal('root')
expect(users[ 0 ].email).to.equal('admin1@example.com') expect(users[ 0 ].email).to.equal('admin1@example.com')
expect(users[ 0 ].displayNSFW).to.be.false expect(users[ 0 ].nsfwPolicy).to.equal('display')
expect(users[ 1 ].username).to.equal('user_1') expect(users[ 1 ].username).to.equal('user_1')
expect(users[ 1 ].email).to.equal('user_1@example.com') expect(users[ 1 ].email).to.equal('user_1@example.com')
expect(users[ 1 ].displayNSFW).to.be.false expect(users[ 1 ].nsfwPolicy).to.equal('display')
}) })
it('Should update my password', async function () { it('Should update my password', async function () {
@ -305,7 +305,7 @@ describe('Test users', function () {
await updateMyUser({ await updateMyUser({
url: server.url, url: server.url,
accessToken: accessTokenUser, accessToken: accessTokenUser,
displayNSFW: true nsfwPolicy: 'do_not_list'
}) })
const res = await getMyUserInformation(server.url, accessTokenUser) const res = await getMyUserInformation(server.url, accessTokenUser)
@ -313,7 +313,7 @@ describe('Test users', function () {
expect(user.username).to.equal('user_1') expect(user.username).to.equal('user_1')
expect(user.email).to.equal('user_1@example.com') expect(user.email).to.equal('user_1@example.com')
expect(user.displayNSFW).to.be.ok expect(user.nsfwPolicy).to.equal('do_not_list')
expect(user.videoQuota).to.equal(2 * 1024 * 1024) expect(user.videoQuota).to.equal(2 * 1024 * 1024)
expect(user.id).to.be.a('number') expect(user.id).to.be.a('number')
expect(user.account.description).to.be.null expect(user.account.description).to.be.null
@ -344,7 +344,7 @@ describe('Test users', function () {
expect(user.username).to.equal('user_1') expect(user.username).to.equal('user_1')
expect(user.email).to.equal('updated@example.com') expect(user.email).to.equal('updated@example.com')
expect(user.displayNSFW).to.be.ok expect(user.nsfwPolicy).to.equal('do_not_list')
expect(user.videoQuota).to.equal(2 * 1024 * 1024) expect(user.videoQuota).to.equal(2 * 1024 * 1024)
expect(user.id).to.be.a('number') expect(user.id).to.be.a('number')
expect(user.account.description).to.be.null expect(user.account.description).to.be.null
@ -377,7 +377,7 @@ describe('Test users', function () {
expect(user.username).to.equal('user_1') expect(user.username).to.equal('user_1')
expect(user.email).to.equal('updated@example.com') expect(user.email).to.equal('updated@example.com')
expect(user.displayNSFW).to.be.ok expect(user.nsfwPolicy).to.equal('do_not_list')
expect(user.videoQuota).to.equal(2 * 1024 * 1024) expect(user.videoQuota).to.equal(2 * 1024 * 1024)
expect(user.id).to.be.a('number') expect(user.id).to.be.a('number')
expect(user.account.description).to.equal('my super description updated') expect(user.account.description).to.equal('my super description updated')
@ -398,7 +398,7 @@ describe('Test users', function () {
expect(user.username).to.equal('user_1') expect(user.username).to.equal('user_1')
expect(user.email).to.equal('updated2@example.com') expect(user.email).to.equal('updated2@example.com')
expect(user.displayNSFW).to.be.ok expect(user.nsfwPolicy).to.equal('do_not_list')
expect(user.videoQuota).to.equal(42) expect(user.videoQuota).to.equal(42)
expect(user.roleLabel).to.equal('Moderator') expect(user.roleLabel).to.equal('Moderator')
expect(user.id).to.be.a('number') expect(user.id).to.be.a('number')

View File

@ -0,0 +1,197 @@
/* tslint:disable:no-unused-expression */
import * as chai from 'chai'
import 'mocha'
import { flushTests, getVideosList, killallServers, ServerInfo, setAccessTokensToServers, uploadVideo } from '../../utils/index'
import { userLogin } from '../../utils/users/login'
import { createUser } from '../../utils/users/users'
import { getMyVideos } from '../../utils/videos/videos'
import {
getConfig, getCustomConfig,
getMyUserInformation,
getVideosListWithToken,
runServer,
searchVideo,
searchVideoWithToken, updateCustomConfig,
updateMyUser
} from '../../utils'
import { ServerConfig } from '../../../../shared/models'
import { CustomConfig } from '../../../../shared/models/server/custom-config.model'
const expect = chai.expect
describe('Test video NSFW policy', function () {
let server: ServerInfo
let userAccessToken: string
let customConfig: CustomConfig
before(async function () {
this.timeout(50000)
await flushTests()
server = await runServer(1)
// Get the access tokens
await setAccessTokensToServers([ server ])
{
const attributes = { name: 'nsfw', nsfw: true }
await uploadVideo(server.url, server.accessToken, attributes)
}
{
const attributes = { name: 'normal', nsfw: false }
await uploadVideo(server.url, server.accessToken, attributes)
}
{
const res = await getCustomConfig(server.url, server.accessToken)
customConfig = res.body
}
})
describe('Instance default NSFW policy', function () {
it('Should display NSFW videos with display default NSFW policy', async function () {
const resConfig = await getConfig(server.url)
const serverConfig: ServerConfig = resConfig.body
expect(serverConfig.instance.defaultNSFWPolicy).to.equal('display')
for (const res of [ await getVideosList(server.url), await searchVideo(server.url, 'n') ]) {
expect(res.body.total).to.equal(2)
const videos = res.body.data
expect(videos).to.have.lengthOf(2)
expect(videos[ 0 ].name).to.equal('normal')
expect(videos[ 1 ].name).to.equal('nsfw')
}
})
it('Should not display NSFW videos with do_not_list default NSFW policy', async function () {
customConfig.instance.defaultNSFWPolicy = 'do_not_list'
await updateCustomConfig(server.url, server.accessToken, customConfig)
const resConfig = await getConfig(server.url)
const serverConfig: ServerConfig = resConfig.body
expect(serverConfig.instance.defaultNSFWPolicy).to.equal('do_not_list')
for (const res of [ await getVideosList(server.url), await searchVideo(server.url, 'n') ]) {
expect(res.body.total).to.equal(1)
const videos = res.body.data
expect(videos).to.have.lengthOf(1)
expect(videos[ 0 ].name).to.equal('normal')
}
})
it('Should display NSFW videos with blur default NSFW policy', async function () {
customConfig.instance.defaultNSFWPolicy = 'blur'
await updateCustomConfig(server.url, server.accessToken, customConfig)
const resConfig = await getConfig(server.url)
const serverConfig: ServerConfig = resConfig.body
expect(serverConfig.instance.defaultNSFWPolicy).to.equal('blur')
for (const res of [ await getVideosList(server.url), await searchVideo(server.url, 'n') ]) {
expect(res.body.total).to.equal(2)
const videos = res.body.data
expect(videos).to.have.lengthOf(2)
expect(videos[ 0 ].name).to.equal('normal')
expect(videos[ 1 ].name).to.equal('nsfw')
}
})
})
describe('User NSFW policy', function () {
it('Should create a user having the default nsfw policy', async function () {
const username = 'user1'
const password = 'my super password'
await createUser(server.url, server.accessToken, username, password)
userAccessToken = await userLogin(server, { username, password })
const res = await getMyUserInformation(server.url, userAccessToken)
const user = res.body
expect(user.nsfwPolicy).to.equal('blur')
})
it('Should display NSFW videos with blur user NSFW policy', async function () {
const results = [
await getVideosListWithToken(server.url, userAccessToken),
await searchVideoWithToken(server.url, 'n', userAccessToken)
]
for (const res of results) {
expect(res.body.total).to.equal(2)
const videos = res.body.data
expect(videos).to.have.lengthOf(2)
expect(videos[ 0 ].name).to.equal('normal')
expect(videos[ 1 ].name).to.equal('nsfw')
}
})
it('Should display NSFW videos with display user NSFW policy', async function () {
await updateMyUser({
url: server.url,
accessToken: server.accessToken,
nsfwPolicy: 'display'
})
const results = [
await getVideosListWithToken(server.url, server.accessToken),
await searchVideoWithToken(server.url, 'n', server.accessToken)
]
for (const res of results) {
expect(res.body.total).to.equal(2)
const videos = res.body.data
expect(videos).to.have.lengthOf(2)
expect(videos[ 0 ].name).to.equal('normal')
expect(videos[ 1 ].name).to.equal('nsfw')
}
})
it('Should not display NSFW videos with do_not_list user NSFW policy', async function () {
await updateMyUser({
url: server.url,
accessToken: server.accessToken,
nsfwPolicy: 'do_not_list'
})
const results = [
await getVideosListWithToken(server.url, server.accessToken),
await searchVideoWithToken(server.url, 'n', server.accessToken)
]
for (const res of results) {
expect(res.body.total).to.equal(1)
const videos = res.body.data
expect(videos).to.have.lengthOf(1)
expect(videos[ 0 ].name).to.equal('normal')
}
})
it('Should be able to see my NSFW videos even with do_not_list user NSFW policy', async function () {
const res = await getMyVideos(server.url, server.accessToken, 0, 5)
expect(res.body.total).to.equal(2)
const videos = res.body.data
expect(videos).to.have.lengthOf(2)
expect(videos[ 0 ].name).to.equal('normal')
expect(videos[ 1 ].name).to.equal('nsfw')
})
})
after(async function () {
killallServers([ server ])
// Keep the logs if the test failed
if (this['ok']) {
await flushTests()
}
})
})

View File

@ -3,6 +3,7 @@ import * as request from 'supertest'
import { makePostBodyRequest, makeUploadRequest, makePutBodyRequest } from '../' import { makePostBodyRequest, makeUploadRequest, makePutBodyRequest } from '../'
import { UserRole } from '../../../../shared/index' import { UserRole } from '../../../../shared/index'
import { NSFWPolicyType } from '../../../../shared/models/videos/nsfw-policy.type'
function createUser ( function createUser (
url: string, url: string,
@ -128,7 +129,7 @@ function updateMyUser (options: {
url: string url: string
accessToken: string, accessToken: string,
newPassword?: string, newPassword?: string,
displayNSFW?: boolean, nsfwPolicy?: NSFWPolicyType,
email?: string, email?: string,
autoPlayVideo?: boolean autoPlayVideo?: boolean
description?: string description?: string
@ -137,7 +138,7 @@ function updateMyUser (options: {
const toSend = {} const toSend = {}
if (options.newPassword !== undefined && options.newPassword !== null) toSend['password'] = options.newPassword if (options.newPassword !== undefined && options.newPassword !== null) toSend['password'] = options.newPassword
if (options.displayNSFW !== undefined && options.displayNSFW !== null) toSend['displayNSFW'] = options.displayNSFW if (options.nsfwPolicy !== undefined && options.nsfwPolicy !== null) toSend['nsfwPolicy'] = options.nsfwPolicy
if (options.autoPlayVideo !== undefined && options.autoPlayVideo !== null) toSend['autoPlayVideo'] = options.autoPlayVideo if (options.autoPlayVideo !== undefined && options.autoPlayVideo !== null) toSend['autoPlayVideo'] = options.autoPlayVideo
if (options.email !== undefined && options.email !== null) toSend['email'] = options.email if (options.email !== undefined && options.email !== null) toSend['email'] = options.email
if (options.description !== undefined && options.description !== null) toSend['description'] = options.description if (options.description !== undefined && options.description !== null) toSend['description'] = options.description

View File

@ -128,6 +128,18 @@ function getVideosList (url: string) {
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
} }
function getVideosListWithToken (url: string, token: string) {
const path = '/api/v1/videos'
return request(url)
.get(path)
.set('Authorization', 'Bearer ' + token)
.query({ sort: 'name' })
.set('Accept', 'application/json')
.expect(200)
.expect('Content-Type', /json/)
}
function getLocalVideos (url: string) { function getLocalVideos (url: string) {
const path = '/api/v1/videos' const path = '/api/v1/videos'
@ -202,6 +214,18 @@ function searchVideo (url: string, search: string) {
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
} }
function searchVideoWithToken (url: string, search: string, token: string) {
const path = '/api/v1/videos'
const req = request(url)
.get(path + '/search')
.set('Authorization', 'Bearer ' + token)
.query({ search })
.set('Accept', 'application/json')
return req.expect(200)
.expect('Content-Type', /json/)
}
function searchVideoWithPagination (url: string, search: string, start: number, count: number, sort?: string) { function searchVideoWithPagination (url: string, search: string, start: number, count: number, sort?: string) {
const path = '/api/v1/videos' const path = '/api/v1/videos'
@ -418,6 +442,8 @@ async function completeVideoCheck (
expect(video.licence.label).to.equal(VIDEO_LICENCES[attributes.licence] || 'Unknown') expect(video.licence.label).to.equal(VIDEO_LICENCES[attributes.licence] || 'Unknown')
expect(video.language.id).to.equal(attributes.language) expect(video.language.id).to.equal(attributes.language)
expect(video.language.label).to.equal(VIDEO_LANGUAGES[attributes.language] || 'Unknown') expect(video.language.label).to.equal(VIDEO_LANGUAGES[attributes.language] || 'Unknown')
expect(video.privacy.id).to.deep.equal(attributes.privacy)
expect(video.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy])
expect(video.nsfw).to.equal(attributes.nsfw) expect(video.nsfw).to.equal(attributes.nsfw)
expect(video.description).to.equal(attributes.description) expect(video.description).to.equal(attributes.description)
expect(video.account.host).to.equal(attributes.account.host) expect(video.account.host).to.equal(attributes.account.host)
@ -435,8 +461,6 @@ async function completeVideoCheck (
expect(videoDetails.files).to.have.lengthOf(attributes.files.length) expect(videoDetails.files).to.have.lengthOf(attributes.files.length)
expect(videoDetails.tags).to.deep.equal(attributes.tags) expect(videoDetails.tags).to.deep.equal(attributes.tags)
expect(videoDetails.privacy.id).to.deep.equal(attributes.privacy)
expect(videoDetails.privacy.label).to.deep.equal(VIDEO_PRIVACIES[attributes.privacy])
expect(videoDetails.account.name).to.equal(attributes.account.name) expect(videoDetails.account.name).to.equal(attributes.account.name)
expect(videoDetails.account.host).to.equal(attributes.account.host) expect(videoDetails.account.host).to.equal(attributes.account.host)
expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled) expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
@ -490,6 +514,7 @@ export {
getVideoPrivacies, getVideoPrivacies,
getVideoLanguages, getVideoLanguages,
getMyVideos, getMyVideos,
searchVideoWithToken,
getVideo, getVideo,
getVideoWithToken, getVideoWithToken,
getVideosList, getVideosList,
@ -499,6 +524,7 @@ export {
searchVideo, searchVideo,
searchVideoWithPagination, searchVideoWithPagination,
searchVideoWithSort, searchVideoWithSort,
getVideosListWithToken,
uploadVideo, uploadVideo,
updateVideo, updateVideo,
rateVideo, rateVideo,

View File

@ -1,3 +1,5 @@
import { NSFWPolicyType } from '../videos/nsfw-policy.type'
export interface CustomConfig { export interface CustomConfig {
instance: { instance: {
name: string name: string
@ -5,6 +7,7 @@ export interface CustomConfig {
description: string description: string
terms: string terms: string
defaultClientRoute: string defaultClientRoute: string
defaultNSFWPolicy: NSFWPolicyType
customizations: { customizations: {
javascript?: string javascript?: string
css?: string css?: string

View File

@ -1,3 +1,5 @@
import { NSFWPolicyType } from '../videos/nsfw-policy.type'
export interface ServerConfig { export interface ServerConfig {
serverVersion: string serverVersion: string
@ -5,6 +7,7 @@ export interface ServerConfig {
name: string name: string
shortDescription: string shortDescription: string
defaultClientRoute: string defaultClientRoute: string
defaultNSFWPolicy: NSFWPolicyType
customizations: { customizations: {
javascript: string javascript: string
css: string css: string

View File

@ -1,6 +1,8 @@
import { NSFWPolicyType } from '../videos/nsfw-policy.type'
export interface UserUpdateMe { export interface UserUpdateMe {
description?: string description?: string
displayNSFW?: boolean nsfwPolicy?: NSFWPolicyType
autoPlayVideo?: boolean autoPlayVideo?: boolean
email?: string email?: string
password?: string password?: string

View File

@ -1,12 +1,13 @@
import { Account } from '../actors' import { Account } from '../actors'
import { VideoChannel } from '../videos/video-channel.model' import { VideoChannel } from '../videos/video-channel.model'
import { UserRole } from './user-role' import { UserRole } from './user-role'
import { NSFWPolicyType } from '../videos/nsfw-policy.type'
export interface User { export interface User {
id: number id: number
username: string username: string
email: string email: string
displayNSFW: boolean nsfwPolicy: NSFWPolicyType
autoPlayVideo: boolean autoPlayVideo: boolean
role: UserRole role: UserRole
videoQuota: number videoQuota: number

View File

@ -0,0 +1 @@
export type NSFWPolicyType = 'do_not_list' | 'blur' | 'display'

View File

@ -26,6 +26,7 @@ export interface Video {
category: VideoConstant<number> category: VideoConstant<number>
licence: VideoConstant<number> licence: VideoConstant<number>
language: VideoConstant<number> language: VideoConstant<number>
privacy: VideoConstant<VideoPrivacy>
description: string description: string
duration: number duration: number
isLocal: boolean isLocal: boolean
@ -48,7 +49,6 @@ export interface Video {
} }
export interface VideoDetails extends Video { export interface VideoDetails extends Video {
privacy: VideoConstant<VideoPrivacy>
descriptionPath: string descriptionPath: string
support: string support: string
channel: VideoChannel channel: VideoChannel