Add ability to list all local videos

Including private/unlisted for moderators/admins
pull/1241/head
Chocobozzz 2018-10-10 11:46:50 +02:00
parent b014b6b9c7
commit 1cd3facc3d
No known key found for this signature in database
GPG Key ID: 583A612D890159BE
17 changed files with 363 additions and 54 deletions

View File

@ -86,9 +86,11 @@ async function listAccountVideos (req: express.Request, res: express.Response, n
languageOneOf: req.query.languageOneOf,
tagsOneOf: req.query.tagsOneOf,
tagsAllOf: req.query.tagsAllOf,
filter: req.query.filter,
nsfw: buildNSFWFilter(res, req.query.nsfw),
withFiles: false,
accountId: account.id
accountId: account.id,
userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined
})
return res.json(getFormattedObjects(resultList.data, resultList.total))

View File

@ -118,6 +118,7 @@ async function searchVideosDB (query: VideosSearchQuery, res: express.Response)
const options = Object.assign(query, {
includeLocalVideos: true,
nsfw: buildNSFWFilter(res, query.nsfw),
filter: query.filter,
userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined
})
const resultList = await VideoModel.searchAndPopulateAccountAndServer(options)

View File

@ -215,9 +215,11 @@ async function listVideoChannelVideos (req: express.Request, res: express.Respon
languageOneOf: req.query.languageOneOf,
tagsOneOf: req.query.tagsOneOf,
tagsAllOf: req.query.tagsAllOf,
filter: req.query.filter,
nsfw: buildNSFWFilter(res, req.query.nsfw),
withFiles: false,
videoChannelId: videoChannelInstance.id
videoChannelId: videoChannelInstance.id,
userId: res.locals.oauth ? res.locals.oauth.token.User.id : undefined
})
return res.json(getFormattedObjects(resultList.data, resultList.total))

View File

@ -1,7 +1,14 @@
import * as express from 'express'
import { CONFIG, FEEDS, ROUTE_CACHE_LIFETIME } from '../initializers/constants'
import { THUMBNAILS_SIZE } from '../initializers'
import { asyncMiddleware, setDefaultSort, videoCommentsFeedsValidator, videoFeedsValidator, videosSortValidator } from '../middlewares'
import {
asyncMiddleware,
commonVideosFiltersValidator,
setDefaultSort,
videoCommentsFeedsValidator,
videoFeedsValidator,
videosSortValidator
} from '../middlewares'
import { VideoModel } from '../models/video/video'
import * as Feed from 'pfeed'
import { AccountModel } from '../models/account/account'
@ -22,6 +29,7 @@ feedsRouter.get('/feeds/videos.:format',
videosSortValidator,
setDefaultSort,
asyncMiddleware(cacheRoute(ROUTE_CACHE_LIFETIME.FEEDS)),
commonVideosFiltersValidator,
asyncMiddleware(videoFeedsValidator),
asyncMiddleware(generateVideoFeed)
)

View File

@ -3,7 +3,7 @@ import 'express-validator'
import { values } from 'lodash'
import 'multer'
import * as validator from 'validator'
import { UserRight, VideoPrivacy, VideoRateType } from '../../../shared'
import { UserRight, VideoFilter, VideoPrivacy, VideoRateType } from '../../../shared'
import {
CONSTRAINTS_FIELDS,
VIDEO_CATEGORIES,
@ -22,6 +22,10 @@ import { fetchVideo, VideoFetchType } from '../video'
const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS
function isVideoFilterValid (filter: VideoFilter) {
return filter === 'local' || filter === 'all-local'
}
function isVideoCategoryValid (value: any) {
return value === null || VIDEO_CATEGORIES[ value ] !== undefined
}
@ -225,5 +229,6 @@ export {
isVideoExist,
isVideoImage,
isVideoChannelOfAccountExist,
isVideoSupportValid
isVideoSupportValid,
isVideoFilterValid
}

View File

@ -2,7 +2,6 @@ import * as express from 'express'
import * as multer from 'multer'
import { CONFIG, REMOTE_SCHEME } from '../initializers'
import { logger } from './logger'
import { User } from '../../shared/models/users'
import { deleteFileAsync, generateRandomString } from './utils'
import { extname } from 'path'
import { isArray } from './custom-validators/misc'
@ -101,7 +100,7 @@ function createReqFiles (
}
function isUserAbleToSearchRemoteURI (res: express.Response) {
const user: User = res.locals.oauth ? res.locals.oauth.token.User : undefined
const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
return CONFIG.SEARCH.REMOTE_URI.ANONYMOUS === true ||
(CONFIG.SEARCH.REMOTE_URI.USERS === true && user !== undefined)

View File

@ -2,8 +2,7 @@ import * as express from 'express'
import { areValidationErrors } from './utils'
import { logger } from '../../helpers/logger'
import { query } from 'express-validator/check'
import { isNumberArray, isStringArray, isNSFWQueryValid } from '../../helpers/custom-validators/search'
import { isBooleanValid, isDateValid, toArray } from '../../helpers/custom-validators/misc'
import { isDateValid } from '../../helpers/custom-validators/misc'
const videosSearchValidator = [
query('search').optional().not().isEmpty().withMessage('Should have a valid search'),
@ -35,44 +34,9 @@ const videoChannelsSearchValidator = [
}
]
const commonVideosFiltersValidator = [
query('categoryOneOf')
.optional()
.customSanitizer(toArray)
.custom(isNumberArray).withMessage('Should have a valid one of category array'),
query('licenceOneOf')
.optional()
.customSanitizer(toArray)
.custom(isNumberArray).withMessage('Should have a valid one of licence array'),
query('languageOneOf')
.optional()
.customSanitizer(toArray)
.custom(isStringArray).withMessage('Should have a valid one of language array'),
query('tagsOneOf')
.optional()
.customSanitizer(toArray)
.custom(isStringArray).withMessage('Should have a valid one of tags array'),
query('tagsAllOf')
.optional()
.customSanitizer(toArray)
.custom(isStringArray).withMessage('Should have a valid all of tags array'),
query('nsfw')
.optional()
.custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking commons video filters query', { parameters: req.query })
if (areValidationErrors(req, res)) return
return next()
}
]
// ---------------------------------------------------------------------------
export {
commonVideosFiltersValidator,
videoChannelsSearchValidator,
videosSearchValidator
}

View File

@ -1,6 +1,6 @@
import * as express from 'express'
import 'express-validator'
import { body, param, ValidationChain } from 'express-validator/check'
import { body, param, query, ValidationChain } from 'express-validator/check'
import { UserRight, VideoChangeOwnershipStatus, VideoPrivacy } from '../../../../shared'
import {
isBooleanValid,
@ -8,6 +8,7 @@ import {
isIdOrUUIDValid,
isIdValid,
isUUIDValid,
toArray,
toIntOrNull,
toValueOrNull
} from '../../../helpers/custom-validators/misc'
@ -19,6 +20,7 @@ import {
isVideoDescriptionValid,
isVideoExist,
isVideoFile,
isVideoFilterValid,
isVideoImage,
isVideoLanguageValid,
isVideoLicenceValid,
@ -42,6 +44,7 @@ import { VideoChangeOwnershipAccept } from '../../../../shared/models/videos/vid
import { VideoChangeOwnershipModel } from '../../../models/video/video-change-ownership'
import { AccountModel } from '../../../models/account/account'
import { VideoFetchType } from '../../../helpers/video'
import { isNSFWQueryValid, isNumberArray, isStringArray } from '../../../helpers/custom-validators/search'
const videosAddValidator = getCommonVideoAttributes().concat([
body('videofile')
@ -359,6 +362,51 @@ function getCommonVideoAttributes () {
] as (ValidationChain | express.Handler)[]
}
const commonVideosFiltersValidator = [
query('categoryOneOf')
.optional()
.customSanitizer(toArray)
.custom(isNumberArray).withMessage('Should have a valid one of category array'),
query('licenceOneOf')
.optional()
.customSanitizer(toArray)
.custom(isNumberArray).withMessage('Should have a valid one of licence array'),
query('languageOneOf')
.optional()
.customSanitizer(toArray)
.custom(isStringArray).withMessage('Should have a valid one of language array'),
query('tagsOneOf')
.optional()
.customSanitizer(toArray)
.custom(isStringArray).withMessage('Should have a valid one of tags array'),
query('tagsAllOf')
.optional()
.customSanitizer(toArray)
.custom(isStringArray).withMessage('Should have a valid all of tags array'),
query('nsfw')
.optional()
.custom(isNSFWQueryValid).withMessage('Should have a valid NSFW attribute'),
query('filter')
.optional()
.custom(isVideoFilterValid).withMessage('Should have a valid filter attribute'),
(req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.debug('Checking commons video filters query', { parameters: req.query })
if (areValidationErrors(req, res)) return
const user: UserModel = res.locals.oauth ? res.locals.oauth.token.User : undefined
if (req.query.filter === 'all-local' && (!user || user.hasRight(UserRight.SEE_ALL_VIDEOS) === false)) {
res.status(401)
.json({ error: 'You are not allowed to see all local videos.' })
return
}
return next()
}
]
// ---------------------------------------------------------------------------
export {
@ -375,7 +423,9 @@ export {
videosTerminateChangeOwnershipValidator,
videosAcceptChangeOwnershipValidator,
getCommonVideoAttributes
getCommonVideoAttributes,
commonVideosFiltersValidator
}
// ---------------------------------------------------------------------------

View File

@ -235,7 +235,14 @@ type AvailableForListIDsOptions = {
)
}
]
},
}
},
include: []
}
// Only list public/published videos
if (!options.filter || options.filter !== 'all-local') {
const privacyWhere = {
// Always list public videos
privacy: VideoPrivacy.PUBLIC,
// Always list published videos, or videos that are being transcoded but on which we don't want to wait for transcoding
@ -250,8 +257,9 @@ type AvailableForListIDsOptions = {
}
}
]
},
include: []
}
Object.assign(query.where, privacyWhere)
}
if (options.filter || options.accountId || options.videoChannelId) {
@ -969,6 +977,10 @@ export class VideoModel extends Model<VideoModel> {
trendingDays?: number,
userId?: number
}, countVideos = true) {
if (options.filter && options.filter === 'all-local' && !options.userId) {
throw new Error('Try to filter all-local but no userId is provided')
}
const query: IFindOptions<VideoModel> = {
offset: options.start,
limit: options.count,
@ -1021,7 +1033,8 @@ export class VideoModel extends Model<VideoModel> {
tagsAllOf?: string[]
durationMin?: number // seconds
durationMax?: number // seconds
userId?: number
userId?: number,
filter?: VideoFilter
}) {
const whereAnd = []
@ -1098,7 +1111,8 @@ export class VideoModel extends Model<VideoModel> {
languageOneOf: options.languageOneOf,
tagsOneOf: options.tagsOneOf,
tagsAllOf: options.tagsAllOf,
userId: options.userId
userId: options.userId,
filter: options.filter
}
return VideoModel.getAvailableForApi(query, queryOptions)
@ -1262,7 +1276,7 @@ export class VideoModel extends Model<VideoModel> {
}
private static buildActorWhereWithFilter (filter?: VideoFilter) {
if (filter && filter === 'local') {
if (filter && (filter === 'local' || filter === 'all-local')) {
return {
serverId: null
}

View File

@ -15,4 +15,5 @@ import './video-channels'
import './video-comments'
import './video-imports'
import './videos'
import './videos-filter'
import './videos-history'

View File

@ -0,0 +1,127 @@
/* tslint:disable:no-unused-expression */
import * as chai from 'chai'
import 'mocha'
import {
createUser,
flushTests,
killallServers,
makeGetRequest,
runServer,
ServerInfo,
setAccessTokensToServers,
userLogin
} from '../../utils'
import { UserRole } from '../../../../shared/models/users'
const expect = chai.expect
async function testEndpoints (server: ServerInfo, token: string, filter: string, statusCodeExpected: number) {
const paths = [
'/api/v1/video-channels/root_channel/videos',
'/api/v1/accounts/root/videos',
'/api/v1/videos',
'/api/v1/search/videos'
]
for (const path of paths) {
await makeGetRequest({
url: server.url,
path,
token,
query: {
filter
},
statusCodeExpected
})
}
}
describe('Test videos filters', function () {
let server: ServerInfo
let userAccessToken: string
let moderatorAccessToken: string
// ---------------------------------------------------------------
before(async function () {
this.timeout(30000)
await flushTests()
server = await runServer(1)
await setAccessTokensToServers([ server ])
const user = { username: 'user1', password: 'my super password' }
await createUser(server.url, server.accessToken, user.username, user.password)
userAccessToken = await userLogin(server, user)
const moderator = { username: 'moderator', password: 'my super password' }
await createUser(
server.url,
server.accessToken,
moderator.username,
moderator.password,
undefined,
undefined,
UserRole.MODERATOR
)
moderatorAccessToken = await userLogin(server, moderator)
})
describe('When setting a video filter', function () {
it('Should fail with a bad filter', async function () {
await testEndpoints(server, server.accessToken, 'bad-filter', 400)
})
it('Should succeed with a good filter', async function () {
await testEndpoints(server, server.accessToken,'local', 200)
})
it('Should fail to list all-local with a simple user', async function () {
await testEndpoints(server, userAccessToken, 'all-local', 401)
})
it('Should succeed to list all-local with a moderator', async function () {
await testEndpoints(server, moderatorAccessToken, 'all-local', 200)
})
it('Should succeed to list all-local with an admin', async function () {
await testEndpoints(server, server.accessToken, 'all-local', 200)
})
// Because we cannot authenticate the user on the RSS endpoint
it('Should fail on the feeds endpoint with the all-local filter', async function () {
await makeGetRequest({
url: server.url,
path: '/feeds/videos.json',
statusCodeExpected: 401,
query: {
filter: 'all-local'
}
})
})
it('Should succed on the feeds endpoint with the local filter', async function () {
await makeGetRequest({
url: server.url,
path: '/feeds/videos.json',
statusCodeExpected: 200,
query: {
filter: 'local'
}
})
})
})
after(async function () {
killallServers([ server ])
// Keep the logs if the test failed
if (this['ok']) {
await flushTests()
}
})
})

View File

@ -14,5 +14,6 @@ import './video-nsfw'
import './video-privacy'
import './video-schedule-update'
import './video-transcoder'
import './videos-filter'
import './videos-history'
import './videos-overview'

View File

@ -0,0 +1,130 @@
/* tslint:disable:no-unused-expression */
import * as chai from 'chai'
import 'mocha'
import {
createUser,
doubleFollow,
flushAndRunMultipleServers,
flushTests,
killallServers,
makeGetRequest,
ServerInfo,
setAccessTokensToServers,
uploadVideo,
userLogin
} from '../../utils'
import { Video, VideoPrivacy } from '../../../../shared/models/videos'
import { UserRole } from '../../../../shared/models/users'
const expect = chai.expect
async function getVideosNames (server: ServerInfo, token: string, filter: string, statusCodeExpected = 200) {
const paths = [
'/api/v1/video-channels/root_channel/videos',
'/api/v1/accounts/root/videos',
'/api/v1/videos',
'/api/v1/search/videos'
]
const videosResults: Video[][] = []
for (const path of paths) {
const res = await makeGetRequest({
url: server.url,
path,
token,
query: {
sort: 'createdAt',
filter
},
statusCodeExpected
})
videosResults.push(res.body.data.map(v => v.name))
}
return videosResults
}
describe('Test videos filter validator', function () {
let servers: ServerInfo[]
// ---------------------------------------------------------------
before(async function () {
this.timeout(120000)
await flushTests()
servers = await flushAndRunMultipleServers(2)
await setAccessTokensToServers(servers)
for (const server of servers) {
const moderator = { username: 'moderator', password: 'my super password' }
await createUser(
server.url,
server.accessToken,
moderator.username,
moderator.password,
undefined,
undefined,
UserRole.MODERATOR
)
server['moderatorAccessToken'] = await userLogin(server, moderator)
await uploadVideo(server.url, server.accessToken, { name: 'public ' + server.serverNumber })
{
const attributes = { name: 'unlisted ' + server.serverNumber, privacy: VideoPrivacy.UNLISTED }
await uploadVideo(server.url, server.accessToken, attributes)
}
{
const attributes = { name: 'private ' + server.serverNumber, privacy: VideoPrivacy.PRIVATE }
await uploadVideo(server.url, server.accessToken, attributes)
}
}
await doubleFollow(servers[0], servers[1])
})
describe('Check videos filter', function () {
it('Should display local videos', async function () {
for (const server of servers) {
const namesResults = await getVideosNames(server, server.accessToken, 'local')
for (const names of namesResults) {
expect(names).to.have.lengthOf(1)
expect(names[ 0 ]).to.equal('public ' + server.serverNumber)
}
}
})
it('Should display all local videos by the admin or the moderator', async function () {
for (const server of servers) {
for (const token of [ server.accessToken, server['moderatorAccessToken'] ]) {
const namesResults = await getVideosNames(server, token, 'all-local')
for (const names of namesResults) {
expect(names).to.have.lengthOf(3)
expect(names[ 0 ]).to.equal('public ' + server.serverNumber)
expect(names[ 1 ]).to.equal('unlisted ' + server.serverNumber)
expect(names[ 2 ]).to.equal('private ' + server.serverNumber)
}
}
}
})
})
after(async function () {
killallServers(servers)
// Keep the logs if the test failed
if (this['ok']) {
await flushTests()
}
})
})

View File

@ -1,4 +1,5 @@
import { NSFWQuery } from './nsfw-query.model'
import { VideoFilter } from '../videos'
export interface VideosSearchQuery {
search?: string
@ -23,4 +24,6 @@ export interface VideosSearchQuery {
durationMin?: number // seconds
durationMax?: number // seconds
filter?: VideoFilter
}

View File

@ -14,5 +14,6 @@ export enum UserRight {
REMOVE_ANY_VIDEO_CHANNEL,
REMOVE_ANY_VIDEO_COMMENT,
UPDATE_ANY_VIDEO,
SEE_ALL_VIDEOS,
CHANGE_VIDEO_OWNERSHIP
}

View File

@ -26,7 +26,8 @@ const userRoleRights: { [ id: number ]: UserRight[] } = {
UserRight.REMOVE_ANY_VIDEO,
UserRight.REMOVE_ANY_VIDEO_CHANNEL,
UserRight.REMOVE_ANY_VIDEO_COMMENT,
UserRight.UPDATE_ANY_VIDEO
UserRight.UPDATE_ANY_VIDEO,
UserRight.SEE_ALL_VIDEOS
],
[UserRole.USER]: []

View File

@ -1 +1 @@
export type VideoFilter = 'local'
export type VideoFilter = 'local' | 'all-local'