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 {
{
value: 'excludeMuted',
label: $localize`Exclude muted accounts`
+ },
+ {
+ value: 'excludePublic',
+ label: $localize`Exclude public videos`
}
]
}
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: {
excludeMuted: {
prefix: 'excludeMuted',
handler: () => true
+ },
+ excludePublic: {
+ prefix: 'excludePublic',
+ handler: () => true
}
})
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 })
}
}
isLocal?: boolean
categoryOneOf?: number[]
languageOneOf?: string[]
+ privacyOneOf?: VideoPrivacy[]
isLive?: boolean
skipCount?: boolean
include,
categoryOneOf,
languageOneOf,
+ privacyOneOf,
skipCount,
nsfwPolicy,
isLive,
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
}
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
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
search: this.search,
isLocal,
include,
+ privacyOneOf,
isLive
}
}
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()
'categoryOneOf',
'licenceOneOf',
'languageOneOf',
+ 'privacyOneOf',
'tagsOneOf',
'tagsAllOf',
'isLocal',
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'
.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)
// 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
}
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.'
languageOneOf?: string[]
tagsOneOf?: string[]
tagsAllOf?: string[]
+ privacyOneOf?: VideoPrivacy[]
uuids?: string[]
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)
}
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)
}
)
}
+ 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) + ')')
}
languageOneOf?: string[]
tagsOneOf?: string[]
tagsAllOf?: string[]
+ privacyOneOf?: VideoPrivacy[]
accountId?: number
videoChannelId?: number
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
'languageOneOf',
'tagsOneOf',
'tagsAllOf',
+ 'privacyOneOf',
'isLocal',
'include',
'displayOnlyForFollower',
languageOneOf?: string[]
tagsOneOf?: string[]
tagsAllOf?: string[]
+ privacyOneOf?: VideoPrivacy[]
displayOnlyForFollower: DisplayOnlyForFollowerOptions | null
uuids?: string[]
}) {
VideoModel.throwIfPrivateIncludeWithoutUser(options.include, options.user)
+ VideoModel.throwIfPrivacyOneOfWithoutUser(options.privacyOneOf, options.user)
const serverActor = await getServerActor()
'languageOneOf',
'tagsOneOf',
'tagsAllOf',
+ 'privacyOneOf',
'user',
'isLocal',
'host',
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
}
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
const validIncludes = [
VideoInclude.NONE,
- VideoInclude.HIDDEN_PRIVACY,
+ VideoInclude.BLOCKED_OWNER,
VideoInclude.NOT_PUBLISHED_STATE | VideoInclude.BLACKLISTED
]
token?: string
isLocal?: boolean
include?: VideoInclude
+ privacyOneOf?: VideoPrivacy[]
expectedStatus: HttpStatusCode
}) {
const paths = [
token: options.token || server.accessToken,
query: {
isLocal: options.isLocal,
+ privacyOneOf: options.privacyOneOf,
include: options.include
},
expectedStatus: options.expectedStatus
}
}
+ 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 })
})
hasWebtorrentFiles?: boolean
hasHLSFiles?: boolean
include?: VideoInclude
+ privacyOneOf?: VideoPrivacy[]
category?: number
tagsAllOf?: string[]
token?: string
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'
},
server: PeerTubeServer
isLocal?: boolean
include?: VideoInclude
+ privacyOneOf?: VideoPrivacy[]
token?: string
expectedStatus?: HttpStatusCode
}) {
server,
token,
isLocal: true,
- include: VideoInclude.HIDDEN_PRIVACY
+ privacyOneOf: [ VideoPrivacy.UNLISTED, VideoPrivacy.PUBLIC, VideoPrivacy.PRIVATE ]
})
for (const names of namesResults) {
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)
export * from './bitrate'
+export * from './privacy'
--- /dev/null
+import { VideoPrivacy } from '../../models/videos/video-privacy.enum'
+
+function getAllPrivacies () {
+ return [ VideoPrivacy.PUBLIC, VideoPrivacy.INTERNAL, VideoPrivacy.PRIVATE, VideoPrivacy.UNLISTED ]
+}
+
+export {
+ getAllPrivacies
+}
+import { VideoPrivacy } from '@shared/models'
import { VideoInclude } from '../videos/video-include.enum'
import { BooleanBothQuery } from './boolean-both-query.model'
languageOneOf?: string[]
+ privacyOneOf?: VideoPrivacy[]
+
tagsOneOf?: string[]
tagsAllOf?: string[]
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
}
- $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'
- $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'
- $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'
- $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'
- $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'
- $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:
- $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:
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
- `1` NOT_PUBLISHED_STATE
- - `2` HIDDEN_PRIVACY
+ - `2` BLACKLISTED
- - `4` BLACKLISTED
+ - `4` BLOCKED_OWNER
- - `8` BLOCKED
+ - `8` FILES
subscriptionsUris:
name: uris
in: query