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

This commit is contained in:
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 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 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="form-group">

View File

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

View File

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

View File

@ -12,3 +12,9 @@ input[type=submit] {
display: block;
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 () {
this.form = this.formBuilder.group({
displayNSFW: [ this.user.displayNSFW ],
nsfwPolicy: [ this.user.nsfwPolicy ],
autoPlayVideo: [ this.user.autoPlayVideo ]
})
@ -41,10 +41,10 @@ export class AccountDetailsComponent extends FormReactive implements OnInit {
}
updateDetails () {
const displayNSFW = this.form.value['displayNSFW']
const nsfwPolicy = this.form.value['nsfwPolicy']
const autoPlayVideo = this.form.value['autoPlayVideo']
const details: UserUpdateMe = {
displayNSFW,
nsfwPolicy,
autoPlayVideo
}

View File

@ -18,6 +18,7 @@
<div class="video-info">
<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>
<div class="video-info-private">{{ video.privacy.label }}</div>
</div>
<!-- Display only once -->

View File

@ -79,8 +79,12 @@
font-weight: $font-semibold;
}
.video-info-date-views {
.video-info-date-views, .video-info-private {
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)
import { hasUserRight, UserRole } from '../../../../../shared/models/users/user-role'
import { User, UserConstructorHash } from '../../shared/users/user.model'
import { NSFWPolicyType } from '../../../../../shared/models/videos/nsfw-policy.type'
export type TokenOptions = {
accessToken: string
@ -70,7 +71,7 @@ export class AuthUser extends User {
ROLE: 'role',
EMAIL: 'email',
USERNAME: 'username',
DISPLAY_NSFW: 'display_nsfw',
NSFW_POLICY: 'nsfw_policy',
AUTO_PLAY_VIDEO: 'auto_play_video'
}
@ -85,7 +86,7 @@ export class AuthUser extends User {
username: peertubeLocalStorage.getItem(this.KEYS.USERNAME),
email: peertubeLocalStorage.getItem(this.KEYS.EMAIL),
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'
},
Tokens.load()
@ -99,7 +100,7 @@ export class AuthUser extends User {
peertubeLocalStorage.removeItem(this.KEYS.USERNAME)
peertubeLocalStorage.removeItem(this.KEYS.ID)
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.EMAIL)
Tokens.flush()
@ -136,7 +137,7 @@ export class AuthUser extends User {
peertubeLocalStorage.setItem(AuthUser.KEYS.USERNAME, this.username)
peertubeLocalStorage.setItem(AuthUser.KEYS.EMAIL, this.email)
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))
this.tokens.save()
}

View File

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

View File

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

View File

@ -12,20 +12,16 @@
}
/deep/ {
.help-tooltip {
opacity: 1 !important;
.popover-body {
text-align: left;
padding: 10px;
max-width: 300px;
.tooltip-inner {
text-align: left;
padding: 10px;
max-width: 300px;
font-size: 13px;
font-family: $main-fonts;
background-color: #fff;
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 {
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 { TooltipDirective } from 'ngx-bootstrap/tooltip'
@Component({
selector: 'my-help',
@ -9,16 +8,14 @@ import { TooltipDirective } from 'ngx-bootstrap/tooltip'
})
export class HelpComponent implements OnInit, OnChanges {
@ViewChild('tooltipDirective') tooltipDirective: TooltipDirective
@Input() preHtml = ''
@Input() postHtml = ''
@Input() customHtml = ''
@Input() helpType: 'custom' | 'markdownText' | 'markdownEnhanced' = 'custom'
@Input() tooltipPlacement = 'right'
mainHtml = ''
constructor (private elementRef: ElementRef) { }
ngOnInit () {
this.init()
}
@ -27,15 +24,6 @@ export class HelpComponent implements OnInit, OnChanges {
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 () {
if (this.helpType === 'custom') {
this.mainHtml = this.customHtml

View File

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

View File

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

View File

@ -1,11 +1,11 @@
<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">
<span class="video-miniature-name">
<a
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 }}
</a>

View File

@ -1,6 +1,7 @@
import { Component, Input } from '@angular/core'
import { User } from '../users'
import { Video } from './video.model'
import { ServerService } from '@app/core'
@Component({
selector: 'my-video-miniature',
@ -11,7 +12,9 @@ export class VideoMiniatureComponent {
@Input() user: User
@Input() video: Video
isVideoNSFWForThisUser () {
return this.video.isVideoNSFWForUser(this.user)
constructor (private serverService: ServerService) { }
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 { User } from '../'
import { Video as VideoServerModel } from '../../../../../shared'
import { Video as VideoServerModel, VideoPrivacy } from '../../../../../shared'
import { Avatar } from '../../../../../shared/models/avatars/avatar.model'
import { VideoConstant } from '../../../../../shared/models/videos/video.model'
import { getAbsoluteAPIUrl } from '../misc/utils'
import { ServerConfig } from '../../../../../shared/models'
export class Video implements VideoServerModel {
by: string
@ -13,6 +14,7 @@ export class Video implements VideoServerModel {
category: VideoConstant<number>
licence: VideoConstant<number>
language: VideoConstant<number>
privacy: VideoConstant<VideoPrivacy>
description: string
duration: number
durationLabel: string
@ -61,6 +63,7 @@ export class Video implements VideoServerModel {
this.category = hash.category
this.licence = hash.licence
this.language = hash.language
this.privacy = hash.privacy
this.description = hash.description
this.duration = 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)
}
isVideoNSFWForUser (user: User) {
// If the video is NSFW and the user is not logged in, or the user does not want to display NSFW videos...
return (this.nsfw && (!user || user.displayNSFW === false))
isVideoNSFWForUser (user: User, serverConfig: ServerConfig) {
// Video is not NSFW, skip
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" />
<label for="nsfw"></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 class="form-group form-group-checkbox">

View File

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

View File

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

View File

@ -11,6 +11,12 @@
<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>

View File

@ -4,6 +4,16 @@
@import '~videojs-dock/dist/videojs-dock.css';
@import '../../sass/video-js-custom.scss';
[hidden] {
display: none !important;
}
body {
font-family: $main-fonts;
font-weight: $font-regular;
color: #000;
}
video {
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
}
async function loadVideoInfo (videoId: string): Promise<VideoDetails> {
const response = await fetch(getVideoUrl(videoId))
return response.json()
function loadVideoInfo (videoId: string): Promise<Response> {
return fetch(getVideoUrl(videoId))
}
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 videoId = urlParts[urlParts.length - 1]
loadVideoInfo(videoId)
.then(videoInfo => {
.then(async response => {
const videoContainerId = 'video-container'
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 startTime = 0

View File

@ -84,6 +84,9 @@ instance:
description: 'Welcome to this PeerTube instance!' # Support markdown
terms: 'No terms for now.' # Support markdown
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:
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

View File

@ -100,6 +100,9 @@ instance:
description: '' # Support markdown
terms: '' # Support markdown
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:
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

View File

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

View File

@ -47,6 +47,7 @@
"nodemon": "nodemon",
"ts-node": "ts-node",
"tslint": "tslint",
"mocha": "mocha",
"travis": "scripty",
"release": "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,
shortDescription: CONFIG.INSTANCE.SHORT_DESCRIPTION,
defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
customizations: {
javascript: CONFIG.INSTANCE.CUSTOMIZATIONS.JAVASCRIPT,
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.instance['default_client_route'] = toUpdate.instance.defaultClientRoute
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))
@ -153,6 +155,7 @@ function customConfig (): CustomConfig {
description: CONFIG.INSTANCE.DESCRIPTION,
terms: CONFIG.INSTANCE.TERMS,
defaultClientRoute: CONFIG.INSTANCE.DEFAULT_CLIENT_ROUTE,
defaultNSFWPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
customizations: {
css: CONFIG.INSTANCE.CUSTOMIZATIONS.CSS,
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 { OAuthTokenModel } from '../../models/oauth/oauth-token'
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 loginRateLimiter = new RateLimit({
@ -161,7 +162,13 @@ export {
async function getUserVideos (req: express.Request, res: express.Response, next: express.NextFunction) {
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))
}
@ -188,7 +195,7 @@ async function createUser (req: express.Request) {
username: body.username,
password: body.password,
email: body.email,
displayNSFW: false,
nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
autoPlayVideo: true,
role: body.role,
videoQuota: body.videoQuota
@ -219,7 +226,7 @@ async function registerUser (req: express.Request) {
username: body.username,
password: body.password,
email: body.email,
displayNSFW: false,
nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
autoPlayVideo: true,
role: UserRole.USER,
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.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
await sequelizeTypescript.transaction(async t => {

View File

@ -19,13 +19,18 @@ import {
VIDEO_MIMETYPE_EXT,
VIDEO_PRIVACIES
} 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 { JobQueue } from '../../../lib/job-queue'
import { Redis } from '../../../lib/redis'
import {
asyncMiddleware,
authenticate,
optionalAuthenticate,
paginationValidator,
setDefaultPagination,
setDefaultSort,
@ -44,6 +49,9 @@ import { blacklistRouter } from './blacklist'
import { videoChannelRouter } from './channel'
import { videoCommentRouter } from './comment'
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()
@ -81,6 +89,7 @@ videosRouter.get('/',
videosSortValidator,
setDefaultSort,
setDefaultPagination,
optionalAuthenticate,
asyncMiddleware(listVideos)
)
videosRouter.get('/search',
@ -89,6 +98,7 @@ videosRouter.get('/search',
videosSortValidator,
setDefaultSort,
setDefaultPagination,
optionalAuthenticate,
asyncMiddleware(searchVideos)
)
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) {
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))
}
@ -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) {
const resultList = await VideoModel.searchAndPopulateAccountAndServer(
req.query.search,
req.query.start,
req.query.count,
req.query.sort
req.query.search as string,
req.query.start as number,
req.query.count as number,
req.query.sort as VideoSortField,
isNSFWHidden(res)
)
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 { AccountModel } from '../models/account/account'
import { cacheRoute } from '../middlewares/cache'
import { VideoSortField } from '../../client/src/app/shared/video/sort-field.type'
const feedsRouter = express.Router()
@ -31,20 +32,22 @@ async function generateFeed (req: express.Request, res: express.Response, next:
let resultList: ResultList<VideoModel>
const account: AccountModel = res.locals.account
const hideNSFW = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list'
if (account) {
resultList = await VideoModel.listAccountVideosForApi(
account.id,
start,
FEEDS.COUNT,
req.query.sort,
true
req.query.sort as VideoSortField,
hideNSFW
)
} else {
resultList = await VideoModel.listForApi(
start,
FEEDS.COUNT,
req.query.sort,
req.query.sort as VideoSortField,
hideNSFW,
req.query.filter,
true
)

View File

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

View File

@ -5,12 +5,12 @@ import { ApplicationModel } from '../models/application/application'
import { OAuthClientModel } from '../models/oauth/oauth-client'
// Some checks on configuration files
// Return an error message, or null if everything is okay
function checkConfig () {
if (config.has('webserver.host')) {
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`.'
const defaultNSFWPolicy = config.get<string>('instance.default_nsfw_policy')
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
@ -28,7 +28,8 @@ function checkMissedConfig () {
'log.level',
'user.video_quota',
'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[] = []

View File

@ -6,13 +6,14 @@ import { FollowState } from '../../shared/models/actors'
import { VideoPrivacy } from '../../shared/models/videos'
// Do not use barrels, remain constants as independent as possible
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
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 TERMS () { return config.get<string>('instance.terms') },
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: {
get JAVASCRIPT () { return config.get<string>('instance.customizations.javascript') },
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 NSFW_POLICY_TYPES: { [ id: string]: NSFWPolicyType } = {
DO_NOT_LIST: 'do_not_list',
BLUR: 'blur',
DISPLAY: 'display'
}
// ---------------------------------------------------------------------------
// Express static paths (router)
@ -474,6 +482,7 @@ export {
PRIVATE_RSA_KEY_SIZE,
SORTABLE_COLUMNS,
FEEDS,
NSFW_POLICY_TYPES,
STATIC_MAX_AGE,
STATIC_PATHS,
ACTIVITY_PUB,

View File

@ -120,6 +120,7 @@ async function createOAuthAdminIfNotExist () {
email,
password,
role,
nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
videoQuota: -1
}
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 'express-validator'
import { OAUTH_LIFETIME } from '../initializers'
import { logger } from '../helpers/logger'
const oAuthServer = new OAuthServer({
useErrorHandler: true,
@ -13,6 +14,8 @@ const oAuthServer = new OAuthServer({
function authenticate (req: express.Request, res: express.Response, next: express.NextFunction) {
oAuthServer.authenticate()(req, res, err => {
if (err) {
logger.warn('Cannot authenticate.', { err })
return res.status(err.status)
.json({
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) {
return oAuthServer.token()(req, res, err => {
if (err) {
@ -44,5 +53,6 @@ function token (req: express.Request, res: express.Response, next: express.NextF
export {
authenticate,
optionalAuthenticate,
token
}

View File

@ -1,6 +1,6 @@
import * as express from 'express'
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 { areValidationErrors } from './utils'
@ -9,6 +9,7 @@ const customConfigUpdateValidator = [
body('instance.description').exists().withMessage('Should have a valid instance description'),
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.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.javascript').exists().withMessage('Should have a valid instance JavaScript customization'),
body('cache.previews.size').isInt().withMessage('Should have a valid previews size'),

View File

@ -8,7 +8,7 @@ import {
isAvatarFile,
isUserAutoPlayVideoValid,
isUserDescriptionValid,
isUserDisplayNSFWValid,
isUserNSFWPolicyValid,
isUserPasswordValid,
isUserRoleValid,
isUserUsernameValid,
@ -101,7 +101,7 @@ const usersUpdateMeValidator = [
body('description').optional().custom(isUserDescriptionValid).withMessage('Should have a valid description'),
body('password').optional().custom(isUserPasswordValid).withMessage('Should have a valid password'),
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'),
(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 {
isUserAutoPlayVideoValid,
isUserDisplayNSFWValid,
isUserNSFWPolicyValid,
isUserPasswordValid,
isUserRoleValid,
isUserUsernameValid,
@ -32,6 +32,9 @@ import { OAuthTokenModel } from '../oauth/oauth-token'
import { getSort, throwIfNotValid } from '../utils'
import { VideoChannelModel } from '../video/video-channel'
import { AccountModel } from './account'
import { NSFWPolicyType } from '../../../shared/models/videos/nsfw-policy.type'
import { values } from 'lodash'
import { NSFW_POLICY_TYPES } from '../../initializers'
@DefaultScope({
include: [
@ -83,10 +86,9 @@ export class UserModel extends Model<UserModel> {
email: string
@AllowNull(false)
@Default(false)
@Is('UserDisplayNSFW', value => throwIfNotValid(value, isUserDisplayNSFWValid, 'display NSFW boolean'))
@Column
displayNSFW: boolean
@Is('UserNSFWPolicy', value => throwIfNotValid(value, isUserNSFWPolicyValid, 'NSFW policy'))
@Column(DataType.ENUM(values(NSFW_POLICY_TYPES)))
nsfwPolicy: NSFWPolicyType
@AllowNull(false)
@Default(true)
@ -265,7 +267,7 @@ export class UserModel extends Model<UserModel> {
id: this.id,
username: this.username,
email: this.email,
displayNSFW: this.displayNSFW,
nsfwPolicy: this.nsfwPolicy,
autoPlayVideo: this.autoPlayVideo,
role: this.role,
roleLabel: USER_ROLE_LABELS[ this.role ],

View File

@ -95,7 +95,7 @@ enum ScopeNames {
}
@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> = {
where: {
id: {
@ -161,6 +161,11 @@ enum ScopeNames {
})
}
// Hide nsfw videos?
if (hideNSFW === true) {
query.where['nsfw'] = false
}
return query
},
[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> = {
offset: start,
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 {
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 = {
offset: start,
limit: count,
@ -685,8 +696,7 @@ export class VideoModel extends Model<VideoModel> {
}
const serverActor = await getServerActor()
return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, filter, withFiles ] })
return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, hideNSFW, filter, withFiles ] })
.findAndCountAll(query)
.then(({ rows, count }) => {
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> = {
offset: start,
limit: count,
@ -724,7 +734,7 @@ export class VideoModel extends Model<VideoModel> {
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)
.then(({ rows, count }) => {
return {
@ -874,6 +884,13 @@ export class VideoModel extends Model<VideoModel> {
return languageLabel
}
private static getPrivacyLabel (id: number) {
let privacyLabel = VIDEO_PRIVACIES[id]
if (!privacyLabel) privacyLabel = 'Unknown'
return privacyLabel
}
getOriginalFile () {
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))
}
createTorrentAndSetInfoHash = async function (videoFile: VideoFileModel) {
async createTorrentAndSetInfoHash (videoFile: VideoFileModel) {
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: [
[ CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT + '/tracker/socket' ],
[ CONFIG.WEBSERVER.URL + '/tracker/announce' ]
@ -980,6 +1000,10 @@ export class VideoModel extends Model<VideoModel> {
id: this.language,
label: VideoModel.getLanguageLabel(this.language)
},
privacy: {
id: this.privacy,
label: VideoModel.getPrivacyLabel(this.privacy)
},
nsfw: this.nsfw,
description: this.getTruncatedDescription(),
isLocal: this.isOwned(),
@ -1006,15 +1030,7 @@ export class VideoModel extends Model<VideoModel> {
toFormattedDetailsJSON (): VideoDetails {
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 = {
privacy: {
id: this.privacy,
label: privacyLabel
},
support: this.support,
descriptionPath: this.getDescriptionPath(),
channel: this.VideoChannel.toFormattedJSON(),
@ -1227,7 +1243,7 @@ export class VideoModel extends Model<VideoModel> {
return peertubeTruncate(this.description, maxLength)
}
optimizeOriginalVideofile = async function () {
async optimizeOriginalVideofile () {
const videosDirectory = CONFIG.STORAGE.VIDEOS_DIR
const newExtname = '.mp4'
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 extname = '.mp4'

View File

@ -6,7 +6,7 @@ import { CustomConfig } from '../../../../shared/models/server/custom-config.mod
import {
createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, runServer, ServerInfo,
setAccessTokensToServers, userLogin
setAccessTokensToServers, userLogin, immutableAssign
} from '../../utils'
describe('Test config API validators', function () {
@ -20,6 +20,7 @@ describe('Test config API validators', function () {
description: 'my super description',
terms: 'my super terms',
defaultClientRoute: '/videos/recently-added',
defaultNSFWPolicy: 'blur',
customizations: {
javascript: 'alert("coucou")',
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 () {
await makePutBodyRequest({
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 })
})
it('Should fail with an invalid display NSFW attribute', async function () {
it('Should fail with an invalid NSFW policy attribute', async function () {
const fields = {
displayNSFW: -1
nsfwPolicy: 'hello'
}
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 () {
const fields = {
password: 'my super password',
displayNSFW: true,
nsfwPolicy: 'blur',
autoPlayVideo: false,
email: 'super_email@example.com'
}

View File

@ -7,6 +7,7 @@ import './videos/video-abuse'
import './videos/video-blacklist'
import './videos/video-blacklist-management'
import './videos/video-description'
import './videos/video-nsfw'
import './videos/video-privacy'
import './videos/services'
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.terms).to.equal('No terms for now.')
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.javascript).to.be.empty
expect(data.cache.previews.size).to.equal(1)
@ -83,6 +84,7 @@ describe('Test config', function () {
description: 'my super description',
terms: 'my super terms',
defaultClientRoute: '/videos/recently-added',
defaultNSFWPolicy: 'blur' as 'blur',
customizations: {
javascript: 'alert("coucou")',
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.terms).to.equal('my super terms')
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.css).to.equal('body { background-color: red; }')
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.terms).to.equal('my super terms')
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.css).to.equal('body { background-color: red; }')
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.terms).to.equal('No terms for now.')
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.javascript).to.be.empty
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.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.roleLabel).to.equal('User')
expect(user.id).to.be.a('number')
@ -215,12 +215,12 @@ describe('Test users', function () {
const user = users[ 0 ]
expect(user.username).to.equal('user_1')
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 ]
expect(rootUser.username).to.equal('root')
expect(rootUser.email).to.equal('admin1@example.com')
expect(rootUser.displayNSFW).to.be.false
expect(user.nsfwPolicy).to.equal('display')
userId = user.id
})
@ -239,7 +239,7 @@ describe('Test users', function () {
expect(user.username).to.equal('root')
expect(user.email).to.equal('admin1@example.com')
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 () {
@ -254,7 +254,7 @@ describe('Test users', function () {
const user = users[ 0 ]
expect(user.username).to.equal('user_1')
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 () {
@ -269,7 +269,7 @@ describe('Test users', function () {
const user = users[ 0 ]
expect(user.username).to.equal('user_1')
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 () {
@ -283,11 +283,11 @@ describe('Test users', function () {
expect(users[ 0 ].username).to.equal('root')
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 ].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 () {
@ -305,7 +305,7 @@ describe('Test users', function () {
await updateMyUser({
url: server.url,
accessToken: accessTokenUser,
displayNSFW: true
nsfwPolicy: 'do_not_list'
})
const res = await getMyUserInformation(server.url, accessTokenUser)
@ -313,7 +313,7 @@ describe('Test users', function () {
expect(user.username).to.equal('user_1')
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.id).to.be.a('number')
expect(user.account.description).to.be.null
@ -344,7 +344,7 @@ describe('Test users', function () {
expect(user.username).to.equal('user_1')
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.id).to.be.a('number')
expect(user.account.description).to.be.null
@ -377,7 +377,7 @@ describe('Test users', function () {
expect(user.username).to.equal('user_1')
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.id).to.be.a('number')
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.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.roleLabel).to.equal('Moderator')
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 { UserRole } from '../../../../shared/index'
import { NSFWPolicyType } from '../../../../shared/models/videos/nsfw-policy.type'
function createUser (
url: string,
@ -128,7 +129,7 @@ function updateMyUser (options: {
url: string
accessToken: string,
newPassword?: string,
displayNSFW?: boolean,
nsfwPolicy?: NSFWPolicyType,
email?: string,
autoPlayVideo?: boolean
description?: string
@ -137,7 +138,7 @@ function updateMyUser (options: {
const toSend = {}
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.email !== undefined && options.email !== null) toSend['email'] = options.email
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/)
}
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) {
const path = '/api/v1/videos'
@ -202,6 +214,18 @@ function searchVideo (url: string, search: string) {
.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) {
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.language.id).to.equal(attributes.language)
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.description).to.equal(attributes.description)
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.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.host).to.equal(attributes.account.host)
expect(videoDetails.commentsEnabled).to.equal(attributes.commentsEnabled)
@ -490,6 +514,7 @@ export {
getVideoPrivacies,
getVideoLanguages,
getMyVideos,
searchVideoWithToken,
getVideo,
getVideoWithToken,
getVideosList,
@ -499,6 +524,7 @@ export {
searchVideo,
searchVideoWithPagination,
searchVideoWithSort,
getVideosListWithToken,
uploadVideo,
updateVideo,
rateVideo,

View File

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

View File

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

View File

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

View File

@ -1,12 +1,13 @@
import { Account } from '../actors'
import { VideoChannel } from '../videos/video-channel.model'
import { UserRole } from './user-role'
import { NSFWPolicyType } from '../videos/nsfw-policy.type'
export interface User {
id: number
username: string
email: string
displayNSFW: boolean
nsfwPolicy: NSFWPolicyType
autoPlayVideo: boolean
role: UserRole
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>
licence: VideoConstant<number>
language: VideoConstant<number>
privacy: VideoConstant<VideoPrivacy>
description: string
duration: number
isLocal: boolean
@ -48,7 +49,6 @@ export interface Video {
}
export interface VideoDetails extends Video {
privacy: VideoConstant<VideoPrivacy>
descriptionPath: string
support: string
channel: VideoChannel