1
0
Fork 0

Add ability to filter out public videos from admin

This commit is contained in:
Chocobozzz 2021-11-12 14:19:56 +01:00
parent 8f2608e9a9
commit 527a52ac42
No known key found for this signature in database
GPG key ID: 583A612D890159BE
15 changed files with 125 additions and 30 deletions

View file

@ -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 })
}
}

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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()

View file

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

View file

@ -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.'

View file

@ -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) + ')')
}

View file

@ -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
}

View file

@ -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 })
})

View file

@ -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)

View file

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

View file

@ -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
}

View file

@ -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[]

View file

@ -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
}

View file

@ -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