Selaa lähdekoodia

Add ability to filter out public videos from admin

Chocobozzz 2 vuotta sitten
vanhempi
commit
527a52ac42

+ 20 - 4
client/src/app/+admin/overview/videos/video-admin.service.ts

@@ -5,7 +5,8 @@ import { Injectable } from '@angular/core'
 import { RestExtractor, RestPagination, RestService } from '@app/core'
 import { AdvancedInputFilter } from '@app/shared/shared-forms'
 import { CommonVideoParams, Video, VideoService } from '@app/shared/shared-main'
-import { ResultList, VideoInclude } from '@shared/models'
+import { ResultList, VideoInclude, VideoPrivacy } from '@shared/models'
+import { getAllPrivacies } from '@shared/core-utils'
 
 @Injectable()
 export class VideoAdminService {
@@ -96,6 +97,10 @@ export class VideoAdminService {
           {
             value: 'excludeMuted',
             label: $localize`Exclude muted accounts`
+          },
+          {
+            value: 'excludePublic',
+            label: $localize`Exclude public videos`
           }
         ]
       }
@@ -105,11 +110,12 @@ export class VideoAdminService {
   private buildAdminParamsFromSearch (search: string, params: HttpParams) {
     let include = VideoInclude.BLACKLISTED |
       VideoInclude.BLOCKED_OWNER |
-      VideoInclude.HIDDEN_PRIVACY |
       VideoInclude.NOT_PUBLISHED_STATE |
       VideoInclude.FILES
 
-    if (!search) return this.restService.addObjectParams(params, { include })
+    let privacyOneOf = getAllPrivacies()
+
+    if (!search) return this.restService.addObjectParams(params, { include, privacyOneOf })
 
     const filters = this.restService.parseQueryStringFilter(search, {
       isLocal: {
@@ -131,6 +137,10 @@ export class VideoAdminService {
       excludeMuted: {
         prefix: 'excludeMuted',
         handler: () => true
+      },
+      excludePublic: {
+        prefix: 'excludePublic',
+        handler: () => true
       }
     })
 
@@ -140,6 +150,12 @@ export class VideoAdminService {
       filters.excludeMuted = undefined
     }
 
-    return this.restService.addObjectParams(params, { ...filters, include })
+    if (filters.excludePublic) {
+      privacyOneOf = [ VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED, VideoPrivacy.INTERNAL ]
+
+      filters.excludePublic = undefined
+    }
+
+    return this.restService.addObjectParams(params, { ...filters, include, privacyOneOf })
   }
 }

+ 3 - 0
client/src/app/shared/shared-main/video/video.service.ts

@@ -38,6 +38,7 @@ export type CommonVideoParams = {
   isLocal?: boolean
   categoryOneOf?: number[]
   languageOneOf?: string[]
+  privacyOneOf?: VideoPrivacy[]
   isLive?: boolean
   skipCount?: boolean
 
@@ -392,6 +393,7 @@ export class VideoService {
       include,
       categoryOneOf,
       languageOneOf,
+      privacyOneOf,
       skipCount,
       nsfwPolicy,
       isLive,
@@ -413,6 +415,7 @@ export class VideoService {
     if (nsfwPolicy) newParams = newParams.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy))
     if (languageOneOf) newParams = this.restService.addArrayParams(newParams, 'languageOneOf', languageOneOf)
     if (categoryOneOf) newParams = this.restService.addArrayParams(newParams, 'categoryOneOf', categoryOneOf)
+    if (privacyOneOf) newParams = this.restService.addArrayParams(newParams, 'privacyOneOf', privacyOneOf)
 
     return newParams
   }

+ 6 - 3
client/src/app/shared/shared-video-miniature/video-filters.model.ts

@@ -1,6 +1,6 @@
 import { intoArray, toBoolean } from '@app/helpers'
-import { AttributesOnly } from '@shared/core-utils'
-import { BooleanBothQuery, NSFWPolicyType, VideoInclude, VideoSortField } from '@shared/models'
+import { AttributesOnly, getAllPrivacies } from '@shared/core-utils'
+import { BooleanBothQuery, NSFWPolicyType, VideoInclude, VideoPrivacy, VideoSortField } from '@shared/models'
 
 type VideoFiltersKeys = {
   [ id in keyof AttributesOnly<VideoFilters> ]: any
@@ -198,13 +198,15 @@ export class VideoFilters {
   toVideosAPIObject () {
     let isLocal: boolean
     let include: VideoInclude
+    let privacyOneOf: VideoPrivacy[]
 
     if (this.scope === 'local') {
       isLocal = true
     }
 
     if (this.allVideos) {
-      include = VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.HIDDEN_PRIVACY
+      include = VideoInclude.NOT_PUBLISHED_STATE
+      privacyOneOf = getAllPrivacies()
     }
 
     let isLive: boolean
@@ -219,6 +221,7 @@ export class VideoFilters {
       search: this.search,
       isLocal,
       include,
+      privacyOneOf,
       isLive
     }
   }

+ 1 - 1
server/controllers/client.ts

@@ -10,7 +10,7 @@ import { HttpStatusCode } from '@shared/models'
 import { root } from '../helpers/core-utils'
 import { STATIC_MAX_AGE } from '../initializers/constants'
 import { ClientHtml, sendHTML, serveIndexHTML } from '../lib/client-html'
-import { asyncMiddleware, disableRobots, embedCSP } from '../middlewares'
+import { asyncMiddleware, embedCSP } from '../middlewares'
 
 const clientsRouter = express.Router()
 

+ 1 - 0
server/helpers/query.ts

@@ -16,6 +16,7 @@ function pickCommonVideoQuery (query: VideosCommonQueryAfterSanitize) {
     'categoryOneOf',
     'licenceOneOf',
     'languageOneOf',
+    'privacyOneOf',
     'tagsOneOf',
     'tagsAllOf',
     'isLocal',

+ 10 - 3
server/middlewares/validators/videos/videos.ts

@@ -7,6 +7,7 @@ import { isAbleToUploadVideo } from '@server/lib/user'
 import { getServerActor } from '@server/models/application/application'
 import { ExpressPromiseHandler } from '@server/types/express'
 import { MUserAccountId, MVideoFullLight } from '@server/types/models'
+import { getAllPrivacies } from '@shared/core-utils'
 import { VideoInclude } from '@shared/models'
 import { ServerErrorCode, UserRight, VideoPrivacy } from '../../../../shared'
 import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
@@ -487,6 +488,10 @@ const commonVideosFiltersValidator = [
     .optional()
     .customSanitizer(toArray)
     .custom(isStringArray).withMessage('Should have a valid one of language array'),
+  query('privacyOneOf')
+    .optional()
+    .customSanitizer(toArray)
+    .custom(isNumberArray).withMessage('Should have a valid one of privacy array'),
   query('tagsOneOf')
     .optional()
     .customSanitizer(toArray)
@@ -536,10 +541,12 @@ const commonVideosFiltersValidator = [
     // FIXME: deprecated in 4.0, to remove
     {
       if (req.query.filter === 'all-local') {
-        req.query.include = VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.HIDDEN_PRIVACY
+        req.query.include = VideoInclude.NOT_PUBLISHED_STATE
         req.query.isLocal = true
+        req.query.privacyOneOf = getAllPrivacies()
       } else if (req.query.filter === 'all') {
-        req.query.include = VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.HIDDEN_PRIVACY
+        req.query.include = VideoInclude.NOT_PUBLISHED_STATE
+        req.query.privacyOneOf = getAllPrivacies()
       } else if (req.query.filter === 'local') {
         req.query.isLocal = true
       }
@@ -550,7 +557,7 @@ const commonVideosFiltersValidator = [
     const user = res.locals.oauth?.token.User
 
     if ((!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) {
-      if (req.query.include) {
+      if (req.query.include || req.query.privacyOneOf) {
         return res.fail({
           status: HttpStatusCode.UNAUTHORIZED_401,
           message: 'You are not allowed to see all videos.'

+ 13 - 5
server/models/video/sql/videos-id-list-query-builder.ts

@@ -40,6 +40,7 @@ export type BuildVideosListQueryOptions = {
   languageOneOf?: string[]
   tagsOneOf?: string[]
   tagsAllOf?: string[]
+  privacyOneOf?: VideoPrivacy[]
 
   uuids?: string[]
 
@@ -138,11 +139,6 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
       this.whereStateAvailable()
     }
 
-    // Only list videos with the appropriate priavcy
-    if (!(options.include & VideoInclude.HIDDEN_PRIVACY)) {
-      this.wherePrivacyAvailable(options.user)
-    }
-
     if (options.videoPlaylistId) {
       this.joinPlaylist(options.videoPlaylistId)
     }
@@ -187,6 +183,13 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
       this.whereTagsAllOf(options.tagsAllOf)
     }
 
+    if (options.privacyOneOf) {
+      this.wherePrivacyOneOf(options.privacyOneOf)
+    } else {
+      // Only list videos with the appropriate priavcy
+      this.wherePrivacyAvailable(options.user)
+    }
+
     if (options.uuids) {
       this.whereUUIDs(options.uuids)
     }
@@ -435,6 +438,11 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
     )
   }
 
+  private wherePrivacyOneOf (privacyOneOf: VideoPrivacy[]) {
+    this.and.push('"video"."privacy" IN (:privacyOneOf)')
+    this.replacements.privacyOneOf = privacyOneOf
+  }
+
   private whereUUIDs (uuids: string[]) {
     this.and.push('"video"."uuid" IN (' + createSafeIn(this.sequelize, uuids) + ')')
   }

+ 13 - 2
server/models/video/video.ts

@@ -1041,6 +1041,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
     languageOneOf?: string[]
     tagsOneOf?: string[]
     tagsAllOf?: string[]
+    privacyOneOf?: VideoPrivacy[]
 
     accountId?: number
     videoChannelId?: number
@@ -1059,6 +1060,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
     search?: string
   }) {
     VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user)
+    VideoModel.throwIfPrivacyOneOfWithoutUser(options.privacyOneOf, options.user)
 
     const trendingDays = options.sort.endsWith('trending')
       ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
@@ -1082,6 +1084,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
         'languageOneOf',
         'tagsOneOf',
         'tagsAllOf',
+        'privacyOneOf',
         'isLocal',
         'include',
         'displayOnlyForFollower',
@@ -1119,6 +1122,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
     languageOneOf?: string[]
     tagsOneOf?: string[]
     tagsAllOf?: string[]
+    privacyOneOf?: VideoPrivacy[]
 
     displayOnlyForFollower: DisplayOnlyForFollowerOptions | null
 
@@ -1140,6 +1144,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
     uuids?: string[]
   }) {
     VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user)
+    VideoModel.throwIfPrivacyOneOfWithoutUser(options.privacyOneOf, options.user)
 
     const serverActor = await getServerActor()
 
@@ -1153,6 +1158,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
         'languageOneOf',
         'tagsOneOf',
         'tagsAllOf',
+        'privacyOneOf',
         'user',
         'isLocal',
         'host',
@@ -1510,14 +1516,19 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
 
   private static throwIfPrivateIncludeWithoutUser (include: VideoInclude, user: MUserAccountId) {
     if (VideoModel.isPrivateInclude(include) && !user?.hasRight(UserRight.SEE_ALL_VIDEOS)) {
-      throw new Error('Try to filter all-local but no user has not the see all videos right')
+      throw new Error('Try to filter all-local but user cannot see all videos')
+    }
+  }
+
+  private static throwIfPrivacyOneOfWithoutUser (privacyOneOf: VideoPrivacy[], user: MUserAccountId) {
+    if (privacyOneOf && !user?.hasRight(UserRight.SEE_ALL_VIDEOS)) {
+      throw new Error('Try to choose video privacies but user cannot see all videos')
     }
   }
 
   private static isPrivateInclude (include: VideoInclude) {
     return include & VideoInclude.BLACKLISTED ||
            include & VideoInclude.BLOCKED_OWNER ||
-           include & VideoInclude.HIDDEN_PRIVACY ||
            include & VideoInclude.NOT_PUBLISHED_STATE
   }
 

+ 20 - 2
server/tests/api/check-params/videos-common-filters.ts

@@ -9,7 +9,7 @@ import {
   setAccessTokensToServers,
   setDefaultVideoChannel
 } from '@shared/extra-utils'
-import { HttpStatusCode, UserRole, VideoInclude } from '@shared/models'
+import { HttpStatusCode, UserRole, VideoInclude, VideoPrivacy } from '@shared/models'
 
 describe('Test video filters validators', function () {
   let server: PeerTubeServer
@@ -112,7 +112,7 @@ describe('Test video filters validators', function () {
 
     const validIncludes = [
       VideoInclude.NONE,
-      VideoInclude.HIDDEN_PRIVACY,
+      VideoInclude.BLOCKED_OWNER,
       VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED
     ]
 
@@ -120,6 +120,7 @@ describe('Test video filters validators', function () {
       token?: string
       isLocal?: boolean
       include?: VideoInclude
+      privacyOneOf?: VideoPrivacy[]
       expectedStatus: HttpStatusCode
     }) {
       const paths = [
@@ -136,6 +137,7 @@ describe('Test video filters validators', function () {
           token: options.token || server.accessToken,
           query: {
             isLocal: options.isLocal,
+            privacyOneOf: options.privacyOneOf,
             include: options.include
           },
           expectedStatus: options.expectedStatus
@@ -143,6 +145,22 @@ describe('Test video filters validators', function () {
       }
     }
 
+    it('Should fail with a bad privacyOneOf', async function () {
+      await testEndpoints({ privacyOneOf: [ 'toto' ] as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
+    })
+
+    it('Should succeed with a good privacyOneOf', async function () {
+      await testEndpoints({ privacyOneOf: [ VideoPrivacy.INTERNAL ], expectedStatus: HttpStatusCode.OK_200 })
+    })
+
+    it('Should fail to use privacyOneOf with a simple user', async function () {
+      await testEndpoints({
+        privacyOneOf: [ VideoPrivacy.INTERNAL ],
+        token: userAccessToken,
+        expectedStatus: HttpStatusCode.UNAUTHORIZED_401
+      })
+    })
+
     it('Should fail with a bad include', async function () {
       await testEndpoints({ include: 'toto' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
     })

+ 5 - 3
server/tests/api/videos/videos-common-filters.ts

@@ -138,6 +138,7 @@ describe('Test videos filter', function () {
       hasWebtorrentFiles?: boolean
       hasHLSFiles?: boolean
       include?: VideoInclude
+      privacyOneOf?: VideoPrivacy[]
       category?: number
       tagsAllOf?: string[]
       token?: string
@@ -148,7 +149,7 @@ describe('Test videos filter', function () {
         path: options.path,
         token: options.token ?? options.server.accessToken,
         query: {
-          ...pick(options, [ 'isLocal', 'include', 'category', 'tagsAllOf', 'hasWebtorrentFiles', 'hasHLSFiles' ]),
+          ...pick(options, [ 'isLocal', 'include', 'category', 'tagsAllOf', 'hasWebtorrentFiles', 'hasHLSFiles', 'privacyOneOf' ]),
 
           sort: 'createdAt'
         },
@@ -162,6 +163,7 @@ describe('Test videos filter', function () {
       server: PeerTubeServer
       isLocal?: boolean
       include?: VideoInclude
+      privacyOneOf?: VideoPrivacy[]
       token?: string
       expectedStatus?: HttpStatusCode
     }) {
@@ -195,7 +197,7 @@ describe('Test videos filter', function () {
             server,
             token,
             isLocal: true,
-            include: VideoInclude.HIDDEN_PRIVACY
+            privacyOneOf: [ VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC, VideoPrivacy.PRIVATE ]
           })
 
           for (const names of namesResults) {
@@ -216,7 +218,7 @@ describe('Test videos filter', function () {
           const [ channelVideos, accountVideos, videos, searchVideos ] = await getVideosNames({
             server,
             token,
-            include: VideoInclude.HIDDEN_PRIVACY
+            privacyOneOf: [ VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC, VideoPrivacy.PRIVATE ]
           })
 
           expect(channelVideos).to.have.lengthOf(3)

+ 1 - 0
shared/core-utils/videos/index.ts

@@ -1 +1,2 @@
 export * from './bitrate'
+export * from './privacy'

+ 9 - 0
shared/core-utils/videos/privacy.ts

@@ -0,0 +1,9 @@
+import { VideoPrivacy } from '../../models/videos/video-privacy.enum'
+
+function getAllPrivacies () {
+  return [ VideoPrivacy.PUBLIC, VideoPrivacy.INTERNAL, VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED ]
+}
+
+export {
+  getAllPrivacies
+}

+ 3 - 0
shared/models/search/videos-common-query.model.ts

@@ -1,3 +1,4 @@
+import { VideoPrivacy } from '@shared/models'
 import { VideoInclude } from '../videos/video-include.enum'
 import { BooleanBothQuery } from './boolean-both-query.model'
 
@@ -23,6 +24,8 @@ export interface VideosCommonQuery {
 
   languageOneOf?: string[]
 
+  privacyOneOf?: VideoPrivacy[]
+
   tagsOneOf?: string[]
   tagsAllOf?: string[]
 

+ 3 - 4
shared/models/videos/video-include.enum.ts

@@ -1,8 +1,7 @@
 export const enum VideoInclude {
   NONE = 0,
   NOT_PUBLISHED_STATE = 1 << 0,
-  HIDDEN_PRIVACY = 1 << 1,
-  BLACKLISTED = 1 << 2,
-  BLOCKED_OWNER = 1 << 3,
-  FILES = 1 << 4
+  BLACKLISTED = 1 << 1,
+  BLOCKED_OWNER = 1 << 2,
+  FILES = 1 << 3
 }

+ 17 - 3
support/doc/api/openapi.yaml

@@ -369,6 +369,7 @@ paths:
         - $ref: '#/components/parameters/nsfw'
         - $ref: '#/components/parameters/isLocal'
         - $ref: '#/components/parameters/include'
+        - $ref: '#/components/parameters/privacyOneOf'
         - $ref: '#/components/parameters/hasHLSFiles'
         - $ref: '#/components/parameters/hasWebtorrentFiles'
         - $ref: '#/components/parameters/skipCount'
@@ -1305,6 +1306,7 @@ paths:
         - $ref: '#/components/parameters/nsfw'
         - $ref: '#/components/parameters/isLocal'
         - $ref: '#/components/parameters/include'
+        - $ref: '#/components/parameters/privacyOneOf'
         - $ref: '#/components/parameters/hasHLSFiles'
         - $ref: '#/components/parameters/hasWebtorrentFiles'
         - $ref: '#/components/parameters/skipCount'
@@ -1628,6 +1630,7 @@ paths:
         - $ref: '#/components/parameters/nsfw'
         - $ref: '#/components/parameters/isLocal'
         - $ref: '#/components/parameters/include'
+        - $ref: '#/components/parameters/privacyOneOf'
         - $ref: '#/components/parameters/hasHLSFiles'
         - $ref: '#/components/parameters/hasWebtorrentFiles'
         - $ref: '#/components/parameters/skipCount'
@@ -2867,6 +2870,7 @@ paths:
         - $ref: '#/components/parameters/nsfw'
         - $ref: '#/components/parameters/isLocal'
         - $ref: '#/components/parameters/include'
+        - $ref: '#/components/parameters/privacyOneOf'
         - $ref: '#/components/parameters/hasHLSFiles'
         - $ref: '#/components/parameters/hasWebtorrentFiles'
         - $ref: '#/components/parameters/skipCount'
@@ -3590,6 +3594,7 @@ paths:
         - $ref: '#/components/parameters/nsfw'
         - $ref: '#/components/parameters/isLocal'
         - $ref: '#/components/parameters/include'
+        - $ref: '#/components/parameters/privacyOneOf'
         - $ref: '#/components/parameters/hasHLSFiles'
         - $ref: '#/components/parameters/hasWebtorrentFiles'
         - $ref: '#/components/parameters/skipCount'
@@ -4095,6 +4100,7 @@ paths:
         - $ref: '#/components/parameters/nsfw'
         - $ref: '#/components/parameters/isLocal'
         - $ref: '#/components/parameters/include'
+        - $ref: '#/components/parameters/privacyOneOf'
         - $ref: '#/components/parameters/hasHLSFiles'
         - $ref: '#/components/parameters/hasWebtorrentFiles'
       responses:
@@ -4179,6 +4185,7 @@ paths:
         - $ref: '#/components/parameters/nsfw'
         - $ref: '#/components/parameters/isLocal'
         - $ref: '#/components/parameters/include'
+        - $ref: '#/components/parameters/privacyOneOf'
         - $ref: '#/components/parameters/hasHLSFiles'
         - $ref: '#/components/parameters/hasWebtorrentFiles'
       responses:
@@ -4834,6 +4841,13 @@ components:
       schema:
         type: boolean
       description: '**PeerTube >= 4.0** Display only videos that have WebTorrent files'
+    privacyOneOf:
+      name: privacyOneOf
+      in: query
+      required: false
+      schema:
+        $ref: '#/components/schemas/VideoPrivacySet'
+      description: '**PeerTube >= 4.0** Display only videos in this specific privacy/privacies'
     include:
       name: include
       in: query
@@ -4853,11 +4867,11 @@ components:
 
         - `1` NOT_PUBLISHED_STATE
 
-        - `2` HIDDEN_PRIVACY
+        - `2` BLACKLISTED
 
-        - `4` BLACKLISTED
+        - `4` BLOCKED_OWNER
 
-        - `8` BLOCKED
+        - `8` FILES
     subscriptionsUris:
       name: uris
       in: query