</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">
instanceDescription: '',
instanceTerms: '',
instanceDefaultClientRoute: '',
+ instanceDefaultNSFWPolicy: '',
cachePreviewsSize: '',
signupLimit: '',
adminEmail: '',
instanceDescription: [ '' ],
instanceTerms: [ '' ],
instanceDefaultClientRoute: [ '' ],
+ instanceDefaultNSFWPolicy: [ '' ],
cachePreviewsSize: [ '', CACHE_PREVIEWS_SIZE.VALIDATORS ],
signupEnabled: [ ],
signupLimit: [ '', SIGNUP_LIMIT.VALIDATORS ],
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']
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,
<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 *ngIf="formErrors.nsfwPolicy" class="form-error">
+ {{ formErrors.nsfwPolicy }}
+ </div>
</div>
<div class="form-group">
display: block;
margin-top: 15px;
}
+
+.peertube-select-container {
+ @include peertube-select-container(340px);
+
+ margin-bottom: 30px;
+}
\ No newline at end of file
buildForm () {
this.form = this.formBuilder.group({
- displayNSFW: [ this.user.displayNSFW ],
+ nsfwPolicy: [ this.user.nsfwPolicy ],
autoPlayVideo: [ this.user.autoPlayVideo ]
})
}
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
}
// 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
ROLE: 'role',
EMAIL: 'email',
USERNAME: 'username',
- DISPLAY_NSFW: 'display_nsfw',
+ DEFAULT_NSFW_POLICY: 'nsfw_policy',
AUTO_PLAY_VIDEO: 'auto_play_video'
}
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.DEFAULT_NSFW_POLICY) as NSFWPolicyType,
autoPlayVideo: peertubeLocalStorage.getItem(this.KEYS.AUTO_PLAY_VIDEO) === 'true'
},
Tokens.load()
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.DEFAULT_NSFW_POLICY)
peertubeLocalStorage.removeItem(this.KEYS.AUTO_PLAY_VIDEO)
peertubeLocalStorage.removeItem(this.KEYS.EMAIL)
Tokens.flush()
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.DEFAULT_NSFW_POLICY, this.nsfwPolicy.toString())
peertubeLocalStorage.setItem(AuthUser.KEYS.AUTO_PLAY_VIDEO, JSON.stringify(this.autoPlayVideo))
this.tokens.save()
}
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()
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: ''
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,
email: string,
role: UserRole,
videoQuota?: number,
- displayNSFW?: boolean,
+ nsfwPolicy?: NSFWPolicyType,
autoPlayVideo?: boolean,
createdAt?: Date,
account?: Account,
username: string
email: string
role: UserRole
- displayNSFW: boolean
+ nsfwPolicy: NSFWPolicyType
autoPlayVideo: boolean
videoQuota: number
account: Account
this.videoQuota = hash.videoQuota
}
- if (hash.displayNSFW !== undefined) {
- this.displayNSFW = hash.displayNSFW
+ if (hash.nsfwPolicy !== undefined) {
+ this.nsfwPolicy = hash.nsfwPolicy
}
if (hash.autoPlayVideo !== undefined) {
<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>
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',
@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())
}
}
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
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'
}
}
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',
private confirmService: ConfirmService,
private metaService: MetaService,
private authService: AuthService,
+ private serverService: ServerService,
private notificationsService: NotificationsService,
private markdownService: MarkdownService,
private zone: NgZone,
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'
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
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
480p: true
720p: true
1080p: true
+
+instance:
+ default_nsfw_policy: 'display'
\ No newline at end of file
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
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))
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
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({
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))
}
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
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
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 => {
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,
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()
videosSortValidator,
setDefaultSort,
setDefaultPagination,
+ optionalAuthenticate,
asyncMiddleware(listVideos)
)
videosRouter.get('/search',
videosSortValidator,
setDefaultSort,
setDefaultPagination,
+ optionalAuthenticate,
asyncMiddleware(searchVideos)
)
videosRouter.put('/:id',
}
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))
}
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'
+}
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()
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
)
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
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) {
isUserRoleValid,
isUserVideoQuotaValid,
isUserUsernameValid,
- isUserDisplayNSFWValid,
+ isUserNSFWPolicyValid,
isUserAutoPlayVideoValid,
isUserDescriptionValid,
isAvatarFile
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
'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[] = []
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
// ---------------------------------------------------------------------------
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') }
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)
PRIVATE_RSA_KEY_SIZE,
SORTABLE_COLUMNS,
FEEDS,
+ NSFW_POLICY_TYPES,
STATIC_MAX_AGE,
STATIC_PATHS,
ACTIVITY_PUB,
email,
password,
role,
+ nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
videoQuota: -1
}
const user = new UserModel(userData)
--- /dev/null
+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
+}
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,
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.',
})
}
+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) {
export {
authenticate,
+ optionalAuthenticate,
token
}
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'
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'),
isAvatarFile,
isUserAutoPlayVideoValid,
isUserDescriptionValid,
- isUserDisplayNSFWValid,
+ isUserNSFWPolicyValid,
isUserPasswordValid,
isUserRoleValid,
isUserUsernameValid,
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) => {
import { User, UserRole } from '../../../shared/models/users'
import {
isUserAutoPlayVideoValid,
- isUserDisplayNSFWValid,
+ isUserNSFWPolicyValid,
isUserPasswordValid,
isUserRoleValid,
isUserUsernameValid,
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: [
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)
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 ],
}
@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: {
})
}
+ // Hide nsfw videos?
+ if (hideNSFW === true) {
+ query.where['nsfw'] = false
+ }
+
return query
},
[ScopeNames.WITH_ACCOUNT_DETAILS]: {
})
}
- 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,
})
}
+ if (hideNSFW === true) {
+ query.where = {
+ nsfw: false
+ }
+ }
+
return VideoModel.findAndCountAll(query).then(({ rows, count }) => {
return {
data: rows,
})
}
- 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,
}
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 {
})
}
- 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,
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 {
import {
createUser, flushTests, killallServers, makeDeleteRequest, makeGetRequest, makePutBodyRequest, runServer, ServerInfo,
- setAccessTokensToServers, userLogin
+ setAccessTokensToServers, userLogin, immutableAssign
} from '../../utils'
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; }'
})
})
+ 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,
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 })
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'
}
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)
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; }'
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)
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)
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)
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')
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
})
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 () {
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 () {
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 () {
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 () {
await updateMyUser({
url: server.url,
accessToken: accessTokenUser,
- displayNSFW: true
+ nsfwPolicy: 'do_not_list'
})
const res = await getMyUserInformation(server.url, accessTokenUser)
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
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
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')
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')
--- /dev/null
+/* 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()
+ }
+ })
+})
import { makePostBodyRequest, makeUploadRequest, makePutBodyRequest } from '../'
import { UserRole } from '../../../../shared/index'
+import { NSFWPolicyType } from '../../../../shared/models/videos/nsfw-policy.type'
function createUser (
url: string,
url: string
accessToken: string,
newPassword?: string,
- displayNSFW?: boolean,
+ nsfwPolicy?: NSFWPolicyType,
email?: string,
autoPlayVideo?: boolean
description?: string
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
.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'
.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'
getVideoPrivacies,
getVideoLanguages,
getMyVideos,
+ searchVideoWithToken,
getVideo,
getVideoWithToken,
getVideosList,
searchVideo,
searchVideoWithPagination,
searchVideoWithSort,
+ getVideosListWithToken,
uploadVideo,
updateVideo,
rateVideo,
+import { NSFWPolicyType } from '../videos/nsfw-policy.type'
+
export interface CustomConfig {
instance: {
name: string
description: string
terms: string
defaultClientRoute: string
+ defaultNSFWPolicy: NSFWPolicyType
customizations: {
javascript?: string
css?: string
+import { NSFWPolicyType } from '../videos/nsfw-policy.type'
+
export interface ServerConfig {
serverVersion: string
name: string
shortDescription: string
defaultClientRoute: string
+ defaultNSFWPolicy: NSFWPolicyType
customizations: {
javascript: string
css: string
+import { NSFWPolicyType } from '../videos/nsfw-policy.type'
+
export interface UserUpdateMe {
description?: string
- displayNSFW?: boolean
+ nsfwPolicy?: NSFWPolicyType
autoPlayVideo?: boolean
email?: string
password?: string
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
--- /dev/null
+export type NSFWPolicyType = 'do_not_list' | 'blur' | 'display'