]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/commitdiff
Add ability to choose what policy we have for NSFW videos
authorChocobozzz <me@florianbigard.com>
Thu, 19 Apr 2018 09:01:34 +0000 (11:01 +0200)
committerChocobozzz <me@florianbigard.com>
Thu, 19 Apr 2018 09:01:34 +0000 (11:01 +0200)
There is a global instance setting and a per user setting

41 files changed:
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.html
client/src/app/+admin/config/edit-custom-config/edit-custom-config.component.ts
client/src/app/account/account-settings/account-details/account-details.component.html
client/src/app/account/account-settings/account-details/account-details.component.scss
client/src/app/account/account-settings/account-details/account-details.component.ts
client/src/app/core/auth/auth-user.model.ts
client/src/app/core/server/server.service.ts
client/src/app/shared/users/user.model.ts
client/src/app/shared/video/video-miniature.component.html
client/src/app/shared/video/video-miniature.component.ts
client/src/app/shared/video/video.model.ts
client/src/app/videos/+video-watch/video-watch.component.ts
config/default.yaml
config/production.yaml.example
config/test.yaml
server/controllers/api/config.ts
server/controllers/api/users.ts
server/controllers/api/videos/index.ts
server/controllers/feeds.ts
server/helpers/custom-validators/users.ts
server/initializers/checker.ts
server/initializers/constants.ts
server/initializers/installer.ts
server/initializers/migrations/0205-user-nsfw-policy.ts [new file with mode: 0644]
server/middlewares/oauth.ts
server/middlewares/validators/config.ts
server/middlewares/validators/users.ts
server/models/account/user.ts
server/models/video/video.ts
server/tests/api/check-params/config.ts
server/tests/api/check-params/users.ts
server/tests/api/server/config.ts
server/tests/api/users/users.ts
server/tests/api/videos/video-nsfw.ts [new file with mode: 0644]
server/tests/utils/users/users.ts
server/tests/utils/videos/videos.ts
shared/models/server/custom-config.model.ts
shared/models/server/server-config.model.ts
shared/models/users/user-update-me.model.ts
shared/models/users/user.model.ts
shared/models/videos/nsfw-policy.type.ts [new file with mode: 0644]

index 714a3af15fa2d6c4adda3d02f13c4bfb8d23b7dc..df40bba9f61ba6aae46e87ef0009dc0026803f65 100644 (file)
     </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">
index d73ee71e4513c4bcba7629d175da1549030b8138..2ab371cbbdc0c2e457d82f9f389ad94fc729565f 100644 (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,
index 8f1475a4d0d848161d9886dae57a583e4a723618..9dcc66a7535ec802e7bc65d28b32f7376a89f56d 100644 (file)
@@ -1,11 +1,18 @@
 <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">
index 4e8dfde1db83d031b822cb6e1523a49ce8f3254c..ed59e4689b7338e2394ebde65cd232b8a0afa5af 100644 (file)
@@ -12,3 +12,9 @@ input[type=submit] {
   display: block;
   margin-top: 15px;
 }
+
+.peertube-select-container {
+  @include peertube-select-container(340px);
+
+  margin-bottom: 30px;
+}
\ No newline at end of file
index 917f316519a564ae828015b7f39f4db79dd16fca..de213717ec1a9f1d65b06473c1141f33447959d7 100644 (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
     }
 
index 366eea110bb0657a714d1126275796adaa0e28b5..60fe578996835d736455243899cc65a09b330149 100644 (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',
+    DEFAULT_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.DEFAULT_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.DEFAULT_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.DEFAULT_NSFW_POLICY, this.nsfwPolicy.toString())
     peertubeLocalStorage.setItem(AuthUser.KEYS.AUTO_PLAY_VIDEO, JSON.stringify(this.autoPlayVideo))
     this.tokens.save()
   }
index 987d64d2aeb7f77938ac0b2379bf4c349bd302be..a8beb242dc48dcb6fc819c3a0d4068763926e704 100644 (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: ''
index 4a94b032d6f6670bbe2a613540c871ce0778ef19..2bdc48a1dc81a381ba0cda7915e7f0069aca296d 100644 (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) {
index f28e9b8d9a35cba38069213bac241a2007f37b41..2334321425cdc189af2d4f0cc8bc091b6f106cfb 100644 (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>
index 4d79a74bb88effbbf43e0b625ce85be6f97bf781..d3f6dc1f6dae9c8b32567f888bc16a30faa20883 100644 (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())
   }
 }
index 0c02cbcb9eac3675a2e70ecf1c666e418a9f5b3a..adc248a1ef902209c6ea5d41e791e9375a5a7472 100644 (file)
@@ -4,6 +4,7 @@ import { Video as VideoServerModel } 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
@@ -83,8 +84,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'
   }
 }
index 182703cdf4d5169537f5951adb0c7b0f8e86ea3c..6f6f02378ca577b2ebfb48d2fe80533de72051b1 100644 (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'
index 9f4a76621d08ba74e87144d2e1377db7d28182f0..25dde72c96db5af92d0fff3e880e85fd76e5861b 100644 (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
index fc06c51fb7d2e4e7e72a5bbdf16e2a99b90a0fc5..1d7d35c9c899ed97bd4cf4fd6bd56d4f8d43093d 100644 (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
index 7e395db04e09a4bd871850ac97f16b9bf8696560..020987920d1d63a8ef22dabdbe99f99f6cc4fc3f 100644 (file)
@@ -32,3 +32,6 @@ transcoding:
     480p: true
     720p: true
     1080p: true
+
+instance:
+  default_nsfw_policy: 'display'
\ No newline at end of file
index 88f047adc8a1472b07a10155deddf4c6544e43c7..e47b71f44995c0c62983b52249da11da35fe5139 100644 (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
index 56cbf9448a27f96bb3e7027cb895991f6f3ec650..6540adb1c1a797f9199a9309bdbc2258bf5c58bd 100644 (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 => {
index b4cd67158100a18f0cb946014ff6988397807d87..6e8601fa1c65cdab1c8e3b81961f91c31c6a718d 100644 (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'
+}
index 3e384c48a127a57a6356a71a793bd9cdaa6d3916..27ebecc404138a92e43fbbc8ebc9617fff5df43d 100644 (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
     )
index bbc7cc1994e31ac7dbe463ac1fc95381e3f1d103..c0acb8218b3bc9468a8313b7578ff0521b91d3b5 100644 (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
index 71f3039634075ed7532a802b9d30dc55938b87d7..739f623c605a656e2f5e10e0e85596188589a05b 100644 (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[] = []
 
index 5ee13389d1e182d2e464ba540ed1b4b3d2392c29..d1915586ae78537c044ca195c9b811dfbfd425b6 100644 (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,
index 09c6d54735b14dd29f3d95293fbb573501b4c41f..b0084b368de84e26893c82a143e9d1315e724df1 100644 (file)
@@ -120,6 +120,7 @@ async function createOAuthAdminIfNotExist () {
     email,
     password,
     role,
+    nsfwPolicy: CONFIG.INSTANCE.DEFAULT_NSFW_POLICY,
     videoQuota: -1
   }
   const user = new UserModel(userData)
diff --git a/server/initializers/migrations/0205-user-nsfw-policy.ts b/server/initializers/migrations/0205-user-nsfw-policy.ts
new file mode 100644 (file)
index 0000000..d0f6e89
--- /dev/null
@@ -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
+}
index 41a3fb71894133d759f867a288bc930ea0e62362..a6f28dd5b56a58050cc12d5f0b87aa2ad9e1e640 100644 (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
 }
index ee6f6efa4d3de1a29f9f9481de3793c521ffd456..f58c0676c3e3ea08ebbc5da36f37e4bc6333d6c0 100644 (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'),
index 6ea3d0b6c6b0bd64212d51bdfbc5ba851ad6a381..5dd8caa3f575467a7507d240e3620de7bf7cf163 100644 (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) => {
index 8afd246b2c17dc6cc2d0ebae470e30f4b40e9d2a..56af2f30a979605248dfa3d8de08dce2cb328ae7 100644 (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 ],
index a7923b477dfe82254c4abe914570a6a9d7863769..2e66f9aa7d441c312227413e72cd8e033e894139 100644 (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 {
index 3fe517fadb810dc115b8c81d29255be22964bef6..58b780f38f9677d4ad79fb228e264bec18c1f173 100644 (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,
index a3e415b94b388da4450cb548b073004496be7d6c..e8a6ffd196d701c302ee513671ed7157e46a15fb 100644 (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'
       }
index e17588142c49a9f3a35bf1de203521b75e2c3e4a..3f1b1532cb319399b73bc3857061020284cdf8ed 100644 (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)
index b6ab4f6602495aa7124895ce8c4b6a0e0e6051d1..1192ef9e4ad9fc5b248a718a6c0d307ec6ab8137 100644 (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')
diff --git a/server/tests/api/videos/video-nsfw.ts b/server/tests/api/videos/video-nsfw.ts
new file mode 100644 (file)
index 0000000..4e5ab11
--- /dev/null
@@ -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()
+    }
+  })
+})
index daf731a14b9e01604b49c8d88a9c520d89ff67c2..fc6b26c50af4462ba9688eb77857497a6db488d6 100644 (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
index 01e7fa5a1551e975f2ea835463d9f43705026b72..df9071c292fa8957082f69910da24755a040907b 100644 (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'
 
@@ -490,6 +514,7 @@ export {
   getVideoPrivacies,
   getVideoLanguages,
   getMyVideos,
+  searchVideoWithToken,
   getVideo,
   getVideoWithToken,
   getVideosList,
@@ -499,6 +524,7 @@ export {
   searchVideo,
   searchVideoWithPagination,
   searchVideoWithSort,
+  getVideosListWithToken,
   uploadVideo,
   updateVideo,
   rateVideo,
index b4d24cfbe158abc0495228bf94232369c94d2f50..30956bd47eed643f6c4d2a58d832a0e729843dfa 100644 (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
index 611d0efe5db9e2392b4adda1a1515486d49d812b..d1f9561637b637818f73c13f5d2fd794e70eecd7 100644 (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
index b8423332910edc1f9f148163b9246b74c9f79b58..0a73879ed358180ca3e3bd0022d14c5b8d6362c6 100644 (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
index b5f459f31e882135246b1ef520688e538bec4c1f..188e29ede5fda28d8f002db85b133dba25e8baf0 100644 (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
diff --git a/shared/models/videos/nsfw-policy.type.ts b/shared/models/videos/nsfw-policy.type.ts
new file mode 100644 (file)
index 0000000..dc0032a
--- /dev/null
@@ -0,0 +1 @@
+export type NSFWPolicyType = 'do_not_list' | 'blur' | 'display'