aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--client/src/app/+admin/overview/videos/video-admin.service.ts24
-rw-r--r--client/src/app/shared/shared-main/video/video.service.ts3
-rw-r--r--client/src/app/shared/shared-video-miniature/video-filters.model.ts9
-rw-r--r--server/controllers/client.ts2
-rw-r--r--server/helpers/query.ts1
-rw-r--r--server/middlewares/validators/videos/videos.ts13
-rw-r--r--server/models/video/sql/videos-id-list-query-builder.ts18
-rw-r--r--server/models/video/video.ts15
-rw-r--r--server/tests/api/check-params/videos-common-filters.ts22
-rw-r--r--server/tests/api/videos/videos-common-filters.ts8
-rw-r--r--shared/core-utils/videos/index.ts1
-rw-r--r--shared/core-utils/videos/privacy.ts9
-rw-r--r--shared/models/search/videos-common-query.model.ts3
-rw-r--r--shared/models/videos/video-include.enum.ts7
-rw-r--r--support/doc/api/openapi.yaml20
15 files changed, 125 insertions, 30 deletions
diff --git a/client/src/app/+admin/overview/videos/video-admin.service.ts b/client/src/app/+admin/overview/videos/video-admin.service.ts
index f80de7acd..6a0e8dade 100644
--- a/client/src/app/+admin/overview/videos/video-admin.service.ts
+++ b/client/src/app/+admin/overview/videos/video-admin.service.ts
@@ -5,7 +5,8 @@ import { Injectable } from '@angular/core'
5import { RestExtractor, RestPagination, RestService } from '@app/core' 5import { RestExtractor, RestPagination, RestService } from '@app/core'
6import { AdvancedInputFilter } from '@app/shared/shared-forms' 6import { AdvancedInputFilter } from '@app/shared/shared-forms'
7import { CommonVideoParams, Video, VideoService } from '@app/shared/shared-main' 7import { CommonVideoParams, Video, VideoService } from '@app/shared/shared-main'
8import { ResultList, VideoInclude } from '@shared/models' 8import { ResultList, VideoInclude, VideoPrivacy } from '@shared/models'
9import { getAllPrivacies } from '@shared/core-utils'
9 10
10@Injectable() 11@Injectable()
11export class VideoAdminService { 12export class VideoAdminService {
@@ -96,6 +97,10 @@ export class VideoAdminService {
96 { 97 {
97 value: 'excludeMuted', 98 value: 'excludeMuted',
98 label: $localize`Exclude muted accounts` 99 label: $localize`Exclude muted accounts`
100 },
101 {
102 value: 'excludePublic',
103 label: $localize`Exclude public videos`
99 } 104 }
100 ] 105 ]
101 } 106 }
@@ -105,11 +110,12 @@ export class VideoAdminService {
105 private buildAdminParamsFromSearch (search: string, params: HttpParams) { 110 private buildAdminParamsFromSearch (search: string, params: HttpParams) {
106 let include = VideoInclude.BLACKLISTED | 111 let include = VideoInclude.BLACKLISTED |
107 VideoInclude.BLOCKED_OWNER | 112 VideoInclude.BLOCKED_OWNER |
108 VideoInclude.HIDDEN_PRIVACY |
109 VideoInclude.NOT_PUBLISHED_STATE | 113 VideoInclude.NOT_PUBLISHED_STATE |
110 VideoInclude.FILES 114 VideoInclude.FILES
111 115
112 if (!search) return this.restService.addObjectParams(params, { include }) 116 let privacyOneOf = getAllPrivacies()
117
118 if (!search) return this.restService.addObjectParams(params, { include, privacyOneOf })
113 119
114 const filters = this.restService.parseQueryStringFilter(search, { 120 const filters = this.restService.parseQueryStringFilter(search, {
115 isLocal: { 121 isLocal: {
@@ -131,6 +137,10 @@ export class VideoAdminService {
131 excludeMuted: { 137 excludeMuted: {
132 prefix: 'excludeMuted', 138 prefix: 'excludeMuted',
133 handler: () => true 139 handler: () => true
140 },
141 excludePublic: {
142 prefix: 'excludePublic',
143 handler: () => true
134 } 144 }
135 }) 145 })
136 146
@@ -140,6 +150,12 @@ export class VideoAdminService {
140 filters.excludeMuted = undefined 150 filters.excludeMuted = undefined
141 } 151 }
142 152
143 return this.restService.addObjectParams(params, { ...filters, include }) 153 if (filters.excludePublic) {
154 privacyOneOf = [ VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED, VideoPrivacy.INTERNAL ]
155
156 filters.excludePublic = undefined
157 }
158
159 return this.restService.addObjectParams(params, { ...filters, include, privacyOneOf })
144 } 160 }
145} 161}
diff --git a/client/src/app/shared/shared-main/video/video.service.ts b/client/src/app/shared/shared-main/video/video.service.ts
index 6edcc3fe0..570e8e3be 100644
--- a/client/src/app/shared/shared-main/video/video.service.ts
+++ b/client/src/app/shared/shared-main/video/video.service.ts
@@ -38,6 +38,7 @@ export type CommonVideoParams = {
38 isLocal?: boolean 38 isLocal?: boolean
39 categoryOneOf?: number[] 39 categoryOneOf?: number[]
40 languageOneOf?: string[] 40 languageOneOf?: string[]
41 privacyOneOf?: VideoPrivacy[]
41 isLive?: boolean 42 isLive?: boolean
42 skipCount?: boolean 43 skipCount?: boolean
43 44
@@ -392,6 +393,7 @@ export class VideoService {
392 include, 393 include,
393 categoryOneOf, 394 categoryOneOf,
394 languageOneOf, 395 languageOneOf,
396 privacyOneOf,
395 skipCount, 397 skipCount,
396 nsfwPolicy, 398 nsfwPolicy,
397 isLive, 399 isLive,
@@ -413,6 +415,7 @@ export class VideoService {
413 if (nsfwPolicy) newParams = newParams.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy)) 415 if (nsfwPolicy) newParams = newParams.set('nsfw', this.nsfwPolicyToParam(nsfwPolicy))
414 if (languageOneOf) newParams = this.restService.addArrayParams(newParams, 'languageOneOf', languageOneOf) 416 if (languageOneOf) newParams = this.restService.addArrayParams(newParams, 'languageOneOf', languageOneOf)
415 if (categoryOneOf) newParams = this.restService.addArrayParams(newParams, 'categoryOneOf', categoryOneOf) 417 if (categoryOneOf) newParams = this.restService.addArrayParams(newParams, 'categoryOneOf', categoryOneOf)
418 if (privacyOneOf) newParams = this.restService.addArrayParams(newParams, 'privacyOneOf', privacyOneOf)
416 419
417 return newParams 420 return newParams
418 } 421 }
diff --git a/client/src/app/shared/shared-video-miniature/video-filters.model.ts b/client/src/app/shared/shared-video-miniature/video-filters.model.ts
index 5ad7cf3f7..982880b0e 100644
--- a/client/src/app/shared/shared-video-miniature/video-filters.model.ts
+++ b/client/src/app/shared/shared-video-miniature/video-filters.model.ts
@@ -1,6 +1,6 @@
1import { intoArray, toBoolean } from '@app/helpers' 1import { intoArray, toBoolean } from '@app/helpers'
2import { AttributesOnly } from '@shared/core-utils' 2import { AttributesOnly, getAllPrivacies } from '@shared/core-utils'
3import { BooleanBothQuery, NSFWPolicyType, VideoInclude, VideoSortField } from '@shared/models' 3import { BooleanBothQuery, NSFWPolicyType, VideoInclude, VideoPrivacy, VideoSortField } from '@shared/models'
4 4
5type VideoFiltersKeys = { 5type VideoFiltersKeys = {
6 [ id in keyof AttributesOnly<VideoFilters> ]: any 6 [ id in keyof AttributesOnly<VideoFilters> ]: any
@@ -198,13 +198,15 @@ export class VideoFilters {
198 toVideosAPIObject () { 198 toVideosAPIObject () {
199 let isLocal: boolean 199 let isLocal: boolean
200 let include: VideoInclude 200 let include: VideoInclude
201 let privacyOneOf: VideoPrivacy[]
201 202
202 if (this.scope === 'local') { 203 if (this.scope === 'local') {
203 isLocal = true 204 isLocal = true
204 } 205 }
205 206
206 if (this.allVideos) { 207 if (this.allVideos) {
207 include = VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.HIDDEN_PRIVACY 208 include = VideoInclude.NOT_PUBLISHED_STATE
209 privacyOneOf = getAllPrivacies()
208 } 210 }
209 211
210 let isLive: boolean 212 let isLive: boolean
@@ -219,6 +221,7 @@ export class VideoFilters {
219 search: this.search, 221 search: this.search,
220 isLocal, 222 isLocal,
221 include, 223 include,
224 privacyOneOf,
222 isLive 225 isLive
223 } 226 }
224 } 227 }
diff --git a/server/controllers/client.ts b/server/controllers/client.ts
index 0a27ace76..703166c01 100644
--- a/server/controllers/client.ts
+++ b/server/controllers/client.ts
@@ -10,7 +10,7 @@ import { HttpStatusCode } from '@shared/models'
10import { root } from '../helpers/core-utils' 10import { root } from '../helpers/core-utils'
11import { STATIC_MAX_AGE } from '../initializers/constants' 11import { STATIC_MAX_AGE } from '../initializers/constants'
12import { ClientHtml, sendHTML, serveIndexHTML } from '../lib/client-html' 12import { ClientHtml, sendHTML, serveIndexHTML } from '../lib/client-html'
13import { asyncMiddleware, disableRobots, embedCSP } from '../middlewares' 13import { asyncMiddleware, embedCSP } from '../middlewares'
14 14
15const clientsRouter = express.Router() 15const clientsRouter = express.Router()
16 16
diff --git a/server/helpers/query.ts b/server/helpers/query.ts
index 97bbdfc65..1142d02e4 100644
--- a/server/helpers/query.ts
+++ b/server/helpers/query.ts
@@ -16,6 +16,7 @@ function pickCommonVideoQuery (query: VideosCommonQueryAfterSanitize) {
16 'categoryOneOf', 16 'categoryOneOf',
17 'licenceOneOf', 17 'licenceOneOf',
18 'languageOneOf', 18 'languageOneOf',
19 'privacyOneOf',
19 'tagsOneOf', 20 'tagsOneOf',
20 'tagsAllOf', 21 'tagsAllOf',
21 'isLocal', 22 'isLocal',
diff --git a/server/middlewares/validators/videos/videos.ts b/server/middlewares/validators/videos/videos.ts
index 53643635c..4916decbf 100644
--- a/server/middlewares/validators/videos/videos.ts
+++ b/server/middlewares/validators/videos/videos.ts
@@ -7,6 +7,7 @@ import { isAbleToUploadVideo } from '@server/lib/user'
7import { getServerActor } from '@server/models/application/application' 7import { getServerActor } from '@server/models/application/application'
8import { ExpressPromiseHandler } from '@server/types/express' 8import { ExpressPromiseHandler } from '@server/types/express'
9import { MUserAccountId, MVideoFullLight } from '@server/types/models' 9import { MUserAccountId, MVideoFullLight } from '@server/types/models'
10import { getAllPrivacies } from '@shared/core-utils'
10import { VideoInclude } from '@shared/models' 11import { VideoInclude } from '@shared/models'
11import { ServerErrorCode, UserRight, VideoPrivacy } from '../../../../shared' 12import { ServerErrorCode, UserRight, VideoPrivacy } from '../../../../shared'
12import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes' 13import { HttpStatusCode } from '../../../../shared/models/http/http-error-codes'
@@ -487,6 +488,10 @@ const commonVideosFiltersValidator = [
487 .optional() 488 .optional()
488 .customSanitizer(toArray) 489 .customSanitizer(toArray)
489 .custom(isStringArray).withMessage('Should have a valid one of language array'), 490 .custom(isStringArray).withMessage('Should have a valid one of language array'),
491 query('privacyOneOf')
492 .optional()
493 .customSanitizer(toArray)
494 .custom(isNumberArray).withMessage('Should have a valid one of privacy array'),
490 query('tagsOneOf') 495 query('tagsOneOf')
491 .optional() 496 .optional()
492 .customSanitizer(toArray) 497 .customSanitizer(toArray)
@@ -536,10 +541,12 @@ const commonVideosFiltersValidator = [
536 // FIXME: deprecated in 4.0, to remove 541 // FIXME: deprecated in 4.0, to remove
537 { 542 {
538 if (req.query.filter === 'all-local') { 543 if (req.query.filter === 'all-local') {
539 req.query.include = VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.HIDDEN_PRIVACY 544 req.query.include = VideoInclude.NOT_PUBLISHED_STATE
540 req.query.isLocal = true 545 req.query.isLocal = true
546 req.query.privacyOneOf = getAllPrivacies()
541 } else if (req.query.filter === 'all') { 547 } else if (req.query.filter === 'all') {
542 req.query.include = VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.HIDDEN_PRIVACY 548 req.query.include = VideoInclude.NOT_PUBLISHED_STATE
549 req.query.privacyOneOf = getAllPrivacies()
543 } else if (req.query.filter === 'local') { 550 } else if (req.query.filter === 'local') {
544 req.query.isLocal = true 551 req.query.isLocal = true
545 } 552 }
@@ -550,7 +557,7 @@ const commonVideosFiltersValidator = [
550 const user = res.locals.oauth?.token.User 557 const user = res.locals.oauth?.token.User
551 558
552 if ((!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) { 559 if ((!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) !== true)) {
553 if (req.query.include) { 560 if (req.query.include || req.query.privacyOneOf) {
554 return res.fail({ 561 return res.fail({
555 status: HttpStatusCode.UNAUTHORIZED_401, 562 status: HttpStatusCode.UNAUTHORIZED_401,
556 message: 'You are not allowed to see all videos.' 563 message: 'You are not allowed to see all videos.'
diff --git a/server/models/video/sql/videos-id-list-query-builder.ts b/server/models/video/sql/videos-id-list-query-builder.ts
index 4a882e790..d825225ab 100644
--- a/server/models/video/sql/videos-id-list-query-builder.ts
+++ b/server/models/video/sql/videos-id-list-query-builder.ts
@@ -40,6 +40,7 @@ export type BuildVideosListQueryOptions = {
40 languageOneOf?: string[] 40 languageOneOf?: string[]
41 tagsOneOf?: string[] 41 tagsOneOf?: string[]
42 tagsAllOf?: string[] 42 tagsAllOf?: string[]
43 privacyOneOf?: VideoPrivacy[]
43 44
44 uuids?: string[] 45 uuids?: string[]
45 46
@@ -138,11 +139,6 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
138 this.whereStateAvailable() 139 this.whereStateAvailable()
139 } 140 }
140 141
141 // Only list videos with the appropriate priavcy
142 if (!(options.include & VideoInclude.HIDDEN_PRIVACY)) {
143 this.wherePrivacyAvailable(options.user)
144 }
145
146 if (options.videoPlaylistId) { 142 if (options.videoPlaylistId) {
147 this.joinPlaylist(options.videoPlaylistId) 143 this.joinPlaylist(options.videoPlaylistId)
148 } 144 }
@@ -187,6 +183,13 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
187 this.whereTagsAllOf(options.tagsAllOf) 183 this.whereTagsAllOf(options.tagsAllOf)
188 } 184 }
189 185
186 if (options.privacyOneOf) {
187 this.wherePrivacyOneOf(options.privacyOneOf)
188 } else {
189 // Only list videos with the appropriate priavcy
190 this.wherePrivacyAvailable(options.user)
191 }
192
190 if (options.uuids) { 193 if (options.uuids) {
191 this.whereUUIDs(options.uuids) 194 this.whereUUIDs(options.uuids)
192 } 195 }
@@ -435,6 +438,11 @@ export class VideosIdListQueryBuilder extends AbstractRunQuery {
435 ) 438 )
436 } 439 }
437 440
441 private wherePrivacyOneOf (privacyOneOf: VideoPrivacy[]) {
442 this.and.push('"video"."privacy" IN (:privacyOneOf)')
443 this.replacements.privacyOneOf = privacyOneOf
444 }
445
438 private whereUUIDs (uuids: string[]) { 446 private whereUUIDs (uuids: string[]) {
439 this.and.push('"video"."uuid" IN (' + createSafeIn(this.sequelize, uuids) + ')') 447 this.and.push('"video"."uuid" IN (' + createSafeIn(this.sequelize, uuids) + ')')
440 } 448 }
diff --git a/server/models/video/video.ts b/server/models/video/video.ts
index 003741da0..69d009e04 100644
--- a/server/models/video/video.ts
+++ b/server/models/video/video.ts
@@ -1041,6 +1041,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1041 languageOneOf?: string[] 1041 languageOneOf?: string[]
1042 tagsOneOf?: string[] 1042 tagsOneOf?: string[]
1043 tagsAllOf?: string[] 1043 tagsAllOf?: string[]
1044 privacyOneOf?: VideoPrivacy[]
1044 1045
1045 accountId?: number 1046 accountId?: number
1046 videoChannelId?: number 1047 videoChannelId?: number
@@ -1059,6 +1060,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1059 search?: string 1060 search?: string
1060 }) { 1061 }) {
1061 VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user) 1062 VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user)
1063 VideoModel.throwIfPrivacyOneOfWithoutUser(options.privacyOneOf, options.user)
1062 1064
1063 const trendingDays = options.sort.endsWith('trending') 1065 const trendingDays = options.sort.endsWith('trending')
1064 ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS 1066 ? CONFIG.TRENDING.VIDEOS.INTERVAL_DAYS
@@ -1082,6 +1084,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1082 'languageOneOf', 1084 'languageOneOf',
1083 'tagsOneOf', 1085 'tagsOneOf',
1084 'tagsAllOf', 1086 'tagsAllOf',
1087 'privacyOneOf',
1085 'isLocal', 1088 'isLocal',
1086 'include', 1089 'include',
1087 'displayOnlyForFollower', 1090 'displayOnlyForFollower',
@@ -1119,6 +1122,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1119 languageOneOf?: string[] 1122 languageOneOf?: string[]
1120 tagsOneOf?: string[] 1123 tagsOneOf?: string[]
1121 tagsAllOf?: string[] 1124 tagsAllOf?: string[]
1125 privacyOneOf?: VideoPrivacy[]
1122 1126
1123 displayOnlyForFollower: DisplayOnlyForFollowerOptions | null 1127 displayOnlyForFollower: DisplayOnlyForFollowerOptions | null
1124 1128
@@ -1140,6 +1144,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1140 uuids?: string[] 1144 uuids?: string[]
1141 }) { 1145 }) {
1142 VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user) 1146 VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user)
1147 VideoModel.throwIfPrivacyOneOfWithoutUser(options.privacyOneOf, options.user)
1143 1148
1144 const serverActor = await getServerActor() 1149 const serverActor = await getServerActor()
1145 1150
@@ -1153,6 +1158,7 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1153 'languageOneOf', 1158 'languageOneOf',
1154 'tagsOneOf', 1159 'tagsOneOf',
1155 'tagsAllOf', 1160 'tagsAllOf',
1161 'privacyOneOf',
1156 'user', 1162 'user',
1157 'isLocal', 1163 'isLocal',
1158 'host', 1164 'host',
@@ -1510,14 +1516,19 @@ export class VideoModel extends Model<Partial<AttributesOnly<VideoModel>>> {
1510 1516
1511 private static throwIfPrivateIncludeWithoutUser (include: VideoInclude, user: MUserAccountId) { 1517 private static throwIfPrivateIncludeWithoutUser (include: VideoInclude, user: MUserAccountId) {
1512 if (VideoModel.isPrivateInclude(include) && !user?.hasRight(UserRight.SEE_ALL_VIDEOS)) { 1518 if (VideoModel.isPrivateInclude(include) && !user?.hasRight(UserRight.SEE_ALL_VIDEOS)) {
1513 throw new Error('Try to filter all-local but no user has not the see all videos right') 1519 throw new Error('Try to filter all-local but user cannot see all videos')
1520 }
1521 }
1522
1523 private static throwIfPrivacyOneOfWithoutUser (privacyOneOf: VideoPrivacy[], user: MUserAccountId) {
1524 if (privacyOneOf && !user?.hasRight(UserRight.SEE_ALL_VIDEOS)) {
1525 throw new Error('Try to choose video privacies but user cannot see all videos')
1514 } 1526 }
1515 } 1527 }
1516 1528
1517 private static isPrivateInclude (include: VideoInclude) { 1529 private static isPrivateInclude (include: VideoInclude) {
1518 return include & VideoInclude.BLACKLISTED || 1530 return include & VideoInclude.BLACKLISTED ||
1519 include & VideoInclude.BLOCKED_OWNER || 1531 include & VideoInclude.BLOCKED_OWNER ||
1520 include & VideoInclude.HIDDEN_PRIVACY ||
1521 include & VideoInclude.NOT_PUBLISHED_STATE 1532 include & VideoInclude.NOT_PUBLISHED_STATE
1522 } 1533 }
1523 1534
diff --git a/server/tests/api/check-params/videos-common-filters.ts b/server/tests/api/check-params/videos-common-filters.ts
index afe42b0d5..f2b5bee8e 100644
--- a/server/tests/api/check-params/videos-common-filters.ts
+++ b/server/tests/api/check-params/videos-common-filters.ts
@@ -9,7 +9,7 @@ import {
9 setAccessTokensToServers, 9 setAccessTokensToServers,
10 setDefaultVideoChannel 10 setDefaultVideoChannel
11} from '@shared/extra-utils' 11} from '@shared/extra-utils'
12import { HttpStatusCode, UserRole, VideoInclude } from '@shared/models' 12import { HttpStatusCode, UserRole, VideoInclude, VideoPrivacy } from '@shared/models'
13 13
14describe('Test video filters validators', function () { 14describe('Test video filters validators', function () {
15 let server: PeerTubeServer 15 let server: PeerTubeServer
@@ -112,7 +112,7 @@ describe('Test video filters validators', function () {
112 112
113 const validIncludes = [ 113 const validIncludes = [
114 VideoInclude.NONE, 114 VideoInclude.NONE,
115 VideoInclude.HIDDEN_PRIVACY, 115 VideoInclude.BLOCKED_OWNER,
116 VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED 116 VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED
117 ] 117 ]
118 118
@@ -120,6 +120,7 @@ describe('Test video filters validators', function () {
120 token?: string 120 token?: string
121 isLocal?: boolean 121 isLocal?: boolean
122 include?: VideoInclude 122 include?: VideoInclude
123 privacyOneOf?: VideoPrivacy[]
123 expectedStatus: HttpStatusCode 124 expectedStatus: HttpStatusCode
124 }) { 125 }) {
125 const paths = [ 126 const paths = [
@@ -136,6 +137,7 @@ describe('Test video filters validators', function () {
136 token: options.token || server.accessToken, 137 token: options.token || server.accessToken,
137 query: { 138 query: {
138 isLocal: options.isLocal, 139 isLocal: options.isLocal,
140 privacyOneOf: options.privacyOneOf,
139 include: options.include 141 include: options.include
140 }, 142 },
141 expectedStatus: options.expectedStatus 143 expectedStatus: options.expectedStatus
@@ -143,6 +145,22 @@ describe('Test video filters validators', function () {
143 } 145 }
144 } 146 }
145 147
148 it('Should fail with a bad privacyOneOf', async function () {
149 await testEndpoints({ privacyOneOf: [ 'toto' ] as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
150 })
151
152 it('Should succeed with a good privacyOneOf', async function () {
153 await testEndpoints({ privacyOneOf: [ VideoPrivacy.INTERNAL ], expectedStatus: HttpStatusCode.OK_200 })
154 })
155
156 it('Should fail to use privacyOneOf with a simple user', async function () {
157 await testEndpoints({
158 privacyOneOf: [ VideoPrivacy.INTERNAL ],
159 token: userAccessToken,
160 expectedStatus: HttpStatusCode.UNAUTHORIZED_401
161 })
162 })
163
146 it('Should fail with a bad include', async function () { 164 it('Should fail with a bad include', async function () {
147 await testEndpoints({ include: 'toto' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 }) 165 await testEndpoints({ include: 'toto' as any, expectedStatus: HttpStatusCode.BAD_REQUEST_400 })
148 }) 166 })
diff --git a/server/tests/api/videos/videos-common-filters.ts b/server/tests/api/videos/videos-common-filters.ts
index 4f22d4ac3..ca5f42173 100644
--- a/server/tests/api/videos/videos-common-filters.ts
+++ b/server/tests/api/videos/videos-common-filters.ts
@@ -138,6 +138,7 @@ describe('Test videos filter', function () {
138 hasWebtorrentFiles?: boolean 138 hasWebtorrentFiles?: boolean
139 hasHLSFiles?: boolean 139 hasHLSFiles?: boolean
140 include?: VideoInclude 140 include?: VideoInclude
141 privacyOneOf?: VideoPrivacy[]
141 category?: number 142 category?: number
142 tagsAllOf?: string[] 143 tagsAllOf?: string[]
143 token?: string 144 token?: string
@@ -148,7 +149,7 @@ describe('Test videos filter', function () {
148 path: options.path, 149 path: options.path,
149 token: options.token ?? options.server.accessToken, 150 token: options.token ?? options.server.accessToken,
150 query: { 151 query: {
151 ...pick(options, [ 'isLocal', 'include', 'category', 'tagsAllOf', 'hasWebtorrentFiles', 'hasHLSFiles' ]), 152 ...pick(options, [ 'isLocal', 'include', 'category', 'tagsAllOf', 'hasWebtorrentFiles', 'hasHLSFiles', 'privacyOneOf' ]),
152 153
153 sort: 'createdAt' 154 sort: 'createdAt'
154 }, 155 },
@@ -162,6 +163,7 @@ describe('Test videos filter', function () {
162 server: PeerTubeServer 163 server: PeerTubeServer
163 isLocal?: boolean 164 isLocal?: boolean
164 include?: VideoInclude 165 include?: VideoInclude
166 privacyOneOf?: VideoPrivacy[]
165 token?: string 167 token?: string
166 expectedStatus?: HttpStatusCode 168 expectedStatus?: HttpStatusCode
167 }) { 169 }) {
@@ -195,7 +197,7 @@ describe('Test videos filter', function () {
195 server, 197 server,
196 token, 198 token,
197 isLocal: true, 199 isLocal: true,
198 include: VideoInclude.HIDDEN_PRIVACY 200 privacyOneOf: [ VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC, VideoPrivacy.PRIVATE ]
199 }) 201 })
200 202
201 for (const names of namesResults) { 203 for (const names of namesResults) {
@@ -216,7 +218,7 @@ describe('Test videos filter', function () {
216 const [ channelVideos, accountVideos, videos, searchVideos ] = await getVideosNames({ 218 const [ channelVideos, accountVideos, videos, searchVideos ] = await getVideosNames({
217 server, 219 server,
218 token, 220 token,
219 include: VideoInclude.HIDDEN_PRIVACY 221 privacyOneOf: [ VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC, VideoPrivacy.PRIVATE ]
220 }) 222 })
221 223
222 expect(channelVideos).to.have.lengthOf(3) 224 expect(channelVideos).to.have.lengthOf(3)
diff --git a/shared/core-utils/videos/index.ts b/shared/core-utils/videos/index.ts
index 5a1145f1a..620e3a716 100644
--- a/shared/core-utils/videos/index.ts
+++ b/shared/core-utils/videos/index.ts
@@ -1 +1,2 @@
1export * from './bitrate' 1export * from './bitrate'
2export * from './privacy'
diff --git a/shared/core-utils/videos/privacy.ts b/shared/core-utils/videos/privacy.ts
new file mode 100644
index 000000000..7d3b67d50
--- /dev/null
+++ b/shared/core-utils/videos/privacy.ts
@@ -0,0 +1,9 @@
1import { VideoPrivacy } from '../../models/videos/video-privacy.enum'
2
3function getAllPrivacies () {
4 return [ VideoPrivacy.PUBLIC, VideoPrivacy.INTERNAL, VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED ]
5}
6
7export {
8 getAllPrivacies
9}
diff --git a/shared/models/search/videos-common-query.model.ts b/shared/models/search/videos-common-query.model.ts
index e9edb91b0..2cbf7b014 100644
--- a/shared/models/search/videos-common-query.model.ts
+++ b/shared/models/search/videos-common-query.model.ts
@@ -1,3 +1,4 @@
1import { VideoPrivacy } from '@shared/models'
1import { VideoInclude } from '../videos/video-include.enum' 2import { VideoInclude } from '../videos/video-include.enum'
2import { BooleanBothQuery } from './boolean-both-query.model' 3import { BooleanBothQuery } from './boolean-both-query.model'
3 4
@@ -23,6 +24,8 @@ export interface VideosCommonQuery {
23 24
24 languageOneOf?: string[] 25 languageOneOf?: string[]
25 26
27 privacyOneOf?: VideoPrivacy[]
28
26 tagsOneOf?: string[] 29 tagsOneOf?: string[]
27 tagsAllOf?: string[] 30 tagsAllOf?: string[]
28 31
diff --git a/shared/models/videos/video-include.enum.ts b/shared/models/videos/video-include.enum.ts
index 72fa8cd30..7e16b129a 100644
--- a/shared/models/videos/video-include.enum.ts
+++ b/shared/models/videos/video-include.enum.ts
@@ -1,8 +1,7 @@
1export const enum VideoInclude { 1export const enum VideoInclude {
2 NONE = 0, 2 NONE = 0,
3 NOT_PUBLISHED_STATE = 1 << 0, 3 NOT_PUBLISHED_STATE = 1 << 0,
4 HIDDEN_PRIVACY = 1 << 1, 4 BLACKLISTED = 1 << 1,
5 BLACKLISTED = 1 << 2, 5 BLOCKED_OWNER = 1 << 2,
6 BLOCKED_OWNER = 1 << 3, 6 FILES = 1 << 3
7 FILES = 1 << 4
8} 7}
diff --git a/support/doc/api/openapi.yaml b/support/doc/api/openapi.yaml
index 13757152c..88a089fc7 100644
--- a/support/doc/api/openapi.yaml
+++ b/support/doc/api/openapi.yaml
@@ -369,6 +369,7 @@ paths:
369 - $ref: '#/components/parameters/nsfw' 369 - $ref: '#/components/parameters/nsfw'
370 - $ref: '#/components/parameters/isLocal' 370 - $ref: '#/components/parameters/isLocal'
371 - $ref: '#/components/parameters/include' 371 - $ref: '#/components/parameters/include'
372 - $ref: '#/components/parameters/privacyOneOf'
372 - $ref: '#/components/parameters/hasHLSFiles' 373 - $ref: '#/components/parameters/hasHLSFiles'
373 - $ref: '#/components/parameters/hasWebtorrentFiles' 374 - $ref: '#/components/parameters/hasWebtorrentFiles'
374 - $ref: '#/components/parameters/skipCount' 375 - $ref: '#/components/parameters/skipCount'
@@ -1305,6 +1306,7 @@ paths:
1305 - $ref: '#/components/parameters/nsfw' 1306 - $ref: '#/components/parameters/nsfw'
1306 - $ref: '#/components/parameters/isLocal' 1307 - $ref: '#/components/parameters/isLocal'
1307 - $ref: '#/components/parameters/include' 1308 - $ref: '#/components/parameters/include'
1309 - $ref: '#/components/parameters/privacyOneOf'
1308 - $ref: '#/components/parameters/hasHLSFiles' 1310 - $ref: '#/components/parameters/hasHLSFiles'
1309 - $ref: '#/components/parameters/hasWebtorrentFiles' 1311 - $ref: '#/components/parameters/hasWebtorrentFiles'
1310 - $ref: '#/components/parameters/skipCount' 1312 - $ref: '#/components/parameters/skipCount'
@@ -1628,6 +1630,7 @@ paths:
1628 - $ref: '#/components/parameters/nsfw' 1630 - $ref: '#/components/parameters/nsfw'
1629 - $ref: '#/components/parameters/isLocal' 1631 - $ref: '#/components/parameters/isLocal'
1630 - $ref: '#/components/parameters/include' 1632 - $ref: '#/components/parameters/include'
1633 - $ref: '#/components/parameters/privacyOneOf'
1631 - $ref: '#/components/parameters/hasHLSFiles' 1634 - $ref: '#/components/parameters/hasHLSFiles'
1632 - $ref: '#/components/parameters/hasWebtorrentFiles' 1635 - $ref: '#/components/parameters/hasWebtorrentFiles'
1633 - $ref: '#/components/parameters/skipCount' 1636 - $ref: '#/components/parameters/skipCount'
@@ -2867,6 +2870,7 @@ paths:
2867 - $ref: '#/components/parameters/nsfw' 2870 - $ref: '#/components/parameters/nsfw'
2868 - $ref: '#/components/parameters/isLocal' 2871 - $ref: '#/components/parameters/isLocal'
2869 - $ref: '#/components/parameters/include' 2872 - $ref: '#/components/parameters/include'
2873 - $ref: '#/components/parameters/privacyOneOf'
2870 - $ref: '#/components/parameters/hasHLSFiles' 2874 - $ref: '#/components/parameters/hasHLSFiles'
2871 - $ref: '#/components/parameters/hasWebtorrentFiles' 2875 - $ref: '#/components/parameters/hasWebtorrentFiles'
2872 - $ref: '#/components/parameters/skipCount' 2876 - $ref: '#/components/parameters/skipCount'
@@ -3590,6 +3594,7 @@ paths:
3590 - $ref: '#/components/parameters/nsfw' 3594 - $ref: '#/components/parameters/nsfw'
3591 - $ref: '#/components/parameters/isLocal' 3595 - $ref: '#/components/parameters/isLocal'
3592 - $ref: '#/components/parameters/include' 3596 - $ref: '#/components/parameters/include'
3597 - $ref: '#/components/parameters/privacyOneOf'
3593 - $ref: '#/components/parameters/hasHLSFiles' 3598 - $ref: '#/components/parameters/hasHLSFiles'
3594 - $ref: '#/components/parameters/hasWebtorrentFiles' 3599 - $ref: '#/components/parameters/hasWebtorrentFiles'
3595 - $ref: '#/components/parameters/skipCount' 3600 - $ref: '#/components/parameters/skipCount'
@@ -4095,6 +4100,7 @@ paths:
4095 - $ref: '#/components/parameters/nsfw' 4100 - $ref: '#/components/parameters/nsfw'
4096 - $ref: '#/components/parameters/isLocal' 4101 - $ref: '#/components/parameters/isLocal'
4097 - $ref: '#/components/parameters/include' 4102 - $ref: '#/components/parameters/include'
4103 - $ref: '#/components/parameters/privacyOneOf'
4098 - $ref: '#/components/parameters/hasHLSFiles' 4104 - $ref: '#/components/parameters/hasHLSFiles'
4099 - $ref: '#/components/parameters/hasWebtorrentFiles' 4105 - $ref: '#/components/parameters/hasWebtorrentFiles'
4100 responses: 4106 responses:
@@ -4179,6 +4185,7 @@ paths:
4179 - $ref: '#/components/parameters/nsfw' 4185 - $ref: '#/components/parameters/nsfw'
4180 - $ref: '#/components/parameters/isLocal' 4186 - $ref: '#/components/parameters/isLocal'
4181 - $ref: '#/components/parameters/include' 4187 - $ref: '#/components/parameters/include'
4188 - $ref: '#/components/parameters/privacyOneOf'
4182 - $ref: '#/components/parameters/hasHLSFiles' 4189 - $ref: '#/components/parameters/hasHLSFiles'
4183 - $ref: '#/components/parameters/hasWebtorrentFiles' 4190 - $ref: '#/components/parameters/hasWebtorrentFiles'
4184 responses: 4191 responses:
@@ -4834,6 +4841,13 @@ components:
4834 schema: 4841 schema:
4835 type: boolean 4842 type: boolean
4836 description: '**PeerTube >= 4.0** Display only videos that have WebTorrent files' 4843 description: '**PeerTube >= 4.0** Display only videos that have WebTorrent files'
4844 privacyOneOf:
4845 name: privacyOneOf
4846 in: query
4847 required: false
4848 schema:
4849 $ref: '#/components/schemas/VideoPrivacySet'
4850 description: '**PeerTube >= 4.0** Display only videos in this specific privacy/privacies'
4837 include: 4851 include:
4838 name: include 4852 name: include
4839 in: query 4853 in: query
@@ -4853,11 +4867,11 @@ components:
4853 4867
4854 - `1` NOT_PUBLISHED_STATE 4868 - `1` NOT_PUBLISHED_STATE
4855 4869
4856 - `2` HIDDEN_PRIVACY 4870 - `2` BLACKLISTED
4857 4871
4858 - `4` BLACKLISTED 4872 - `4` BLOCKED_OWNER
4859 4873
4860 - `8` BLOCKED 4874 - `8` FILES
4861 subscriptionsUris: 4875 subscriptionsUris:
4862 name: uris 4876 name: uris
4863 in: query 4877 in: query