From 48dce1c90dff4e90a4bcffefaecf157336cf904b Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 24 Apr 2018 17:05:32 +0200 Subject: Update video channel routes --- server/controllers/api/accounts.ts | 209 ++++++++++++++++++++++-- server/controllers/api/index.ts | 2 + server/controllers/api/video-channel.ts | 34 ++++ server/controllers/api/videos/channel.ts | 177 -------------------- server/controllers/api/videos/index.ts | 17 +- server/controllers/feeds.ts | 14 +- server/middlewares/validators/follows.ts | 2 +- server/middlewares/validators/video-channels.ts | 8 + server/models/video/video.ts | 92 ++++++++--- server/tests/api/check-params/video-channels.ts | 101 ++++++++---- server/tests/api/videos/multiple-servers.ts | 21 ++- server/tests/api/videos/video-channels.ts | 15 +- server/tests/utils/videos/video-channels.ts | 35 ++-- 13 files changed, 441 insertions(+), 286 deletions(-) create mode 100644 server/controllers/api/video-channel.ts delete mode 100644 server/controllers/api/videos/channel.ts (limited to 'server') diff --git a/server/controllers/api/accounts.ts b/server/controllers/api/accounts.ts index 06ab04033..04c5897c5 100644 --- a/server/controllers/api/accounts.ts +++ b/server/controllers/api/accounts.ts @@ -1,11 +1,30 @@ import * as express from 'express' -import { getFormattedObjects } from '../../helpers/utils' -import { asyncMiddleware, optionalAuthenticate, paginationValidator, setDefaultPagination, setDefaultSort } from '../../middlewares' +import { getFormattedObjects, resetSequelizeInstance } from '../../helpers/utils' +import { + asyncMiddleware, + authenticate, + listVideoAccountChannelsValidator, + optionalAuthenticate, + paginationValidator, + setDefaultPagination, + setDefaultSort, + videoChannelsAddValidator, + videoChannelsGetValidator, + videoChannelsRemoveValidator, + videoChannelsUpdateValidator +} from '../../middlewares' import { accountsGetValidator, accountsSortValidator, videosSortValidator } from '../../middlewares/validators' import { AccountModel } from '../../models/account/account' import { VideoModel } from '../../models/video/video' -import { VideoSortField } from '../../../client/src/app/shared/video/sort-field.type' import { isNSFWHidden } from '../../helpers/express-utils' +import { VideoChannelModel } from '../../models/video/video-channel' +import { VideoChannelCreate, VideoChannelUpdate } from '../../../shared' +import { sendUpdateActor } from '../../lib/activitypub/send' +import { createVideoChannel } from '../../lib/video-channel' +import { setAsyncActorKeys } from '../../lib/activitypub' +import { sequelizeTypescript } from '../../initializers' +import { logger } from '../../helpers/logger' +import { retryTransactionWrapper } from '../../helpers/database-utils' const accountsRouter = express.Router() @@ -29,7 +48,45 @@ accountsRouter.get('/:id/videos', setDefaultSort, setDefaultPagination, optionalAuthenticate, - asyncMiddleware(getAccountVideos) + asyncMiddleware(listAccountVideos) +) + +accountsRouter.get('/:accountId/video-channels', + asyncMiddleware(listVideoAccountChannelsValidator), + asyncMiddleware(listVideoAccountChannels) +) + +accountsRouter.post('/:accountId/video-channels', + authenticate, + videoChannelsAddValidator, + asyncMiddleware(addVideoChannelRetryWrapper) +) + +accountsRouter.put('/:accountId/video-channels/:id', + authenticate, + asyncMiddleware(videoChannelsUpdateValidator), + updateVideoChannelRetryWrapper +) + +accountsRouter.delete('/:accountId/video-channels/:id', + authenticate, + asyncMiddleware(videoChannelsRemoveValidator), + asyncMiddleware(removeVideoChannelRetryWrapper) +) + +accountsRouter.get('/:accountId/video-channels/:id', + asyncMiddleware(videoChannelsGetValidator), + asyncMiddleware(getVideoChannel) +) + +accountsRouter.get('/:accountId/video-channels/:id/videos', + asyncMiddleware(videoChannelsGetValidator), + paginationValidator, + videosSortValidator, + setDefaultSort, + setDefaultPagination, + optionalAuthenticate, + asyncMiddleware(listVideoChannelVideos) ) // --------------------------------------------------------------------------- @@ -52,18 +109,142 @@ async function listAccounts (req: express.Request, res: express.Response, next: return res.json(getFormattedObjects(resultList.data, resultList.total)) } -async function getAccountVideos (req: express.Request, res: express.Response, next: express.NextFunction) { +async function listVideoAccountChannels (req: express.Request, res: express.Response, next: express.NextFunction) { + const resultList = await VideoChannelModel.listByAccount(res.locals.account.id) + + return res.json(getFormattedObjects(resultList.data, resultList.total)) +} + +// Wrapper to video channel add that retry the async function if there is a database error +// We need this because we run the transaction in SERIALIZABLE isolation that can fail +async function addVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { + const options = { + arguments: [ req, res ], + errorMessage: 'Cannot insert the video video channel with many retries.' + } + + const videoChannel = await retryTransactionWrapper(addVideoChannel, options) + return res.json({ + videoChannel: { + id: videoChannel.id + } + }).end() +} + +async function addVideoChannel (req: express.Request, res: express.Response) { + const videoChannelInfo: VideoChannelCreate = req.body + const account: AccountModel = res.locals.oauth.token.User.Account + + const videoChannelCreated: VideoChannelModel = await sequelizeTypescript.transaction(async t => { + return createVideoChannel(videoChannelInfo, account, t) + }) + + setAsyncActorKeys(videoChannelCreated.Actor) + .catch(err => logger.error('Cannot set async actor keys for account %s.', videoChannelCreated.Actor.uuid, { err })) + + logger.info('Video channel with uuid %s created.', videoChannelCreated.Actor.uuid) + + return videoChannelCreated +} + +async function updateVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { + const options = { + arguments: [ req, res ], + errorMessage: 'Cannot update the video with many retries.' + } + + await retryTransactionWrapper(updateVideoChannel, options) + + return res.type('json').status(204).end() +} + +async function updateVideoChannel (req: express.Request, res: express.Response) { + const videoChannelInstance = res.locals.videoChannel as VideoChannelModel + const videoChannelFieldsSave = videoChannelInstance.toJSON() + const videoChannelInfoToUpdate = req.body as VideoChannelUpdate + + try { + await sequelizeTypescript.transaction(async t => { + const sequelizeOptions = { + transaction: t + } + + if (videoChannelInfoToUpdate.name !== undefined) videoChannelInstance.set('name', videoChannelInfoToUpdate.name) + if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.set('description', videoChannelInfoToUpdate.description) + if (videoChannelInfoToUpdate.support !== undefined) videoChannelInstance.set('support', videoChannelInfoToUpdate.support) + + const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) + await sendUpdateActor(videoChannelInstanceUpdated, t) + }) + + logger.info('Video channel with name %s and uuid %s updated.', videoChannelInstance.name, videoChannelInstance.Actor.uuid) + } catch (err) { + logger.debug('Cannot update the video channel.', { err }) + + // Force fields we want to update + // If the transaction is retried, sequelize will think the object has not changed + // So it will skip the SQL request, even if the last one was ROLLBACKed! + resetSequelizeInstance(videoChannelInstance, videoChannelFieldsSave) + + throw err + } +} + +async function removeVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { + const options = { + arguments: [ req, res ], + errorMessage: 'Cannot remove the video channel with many retries.' + } + + await retryTransactionWrapper(removeVideoChannel, options) + + return res.type('json').status(204).end() +} + +async function removeVideoChannel (req: express.Request, res: express.Response) { + const videoChannelInstance: VideoChannelModel = res.locals.videoChannel + + return sequelizeTypescript.transaction(async t => { + await videoChannelInstance.destroy({ transaction: t }) + + logger.info('Video channel with name %s and uuid %s deleted.', videoChannelInstance.name, videoChannelInstance.Actor.uuid) + }) + +} + +async function getVideoChannel (req: express.Request, res: express.Response, next: express.NextFunction) { + const videoChannelWithVideos = await VideoChannelModel.loadAndPopulateAccountAndVideos(res.locals.videoChannel.id) + + return res.json(videoChannelWithVideos.toFormattedJSON()) +} + +async function listVideoChannelVideos (req: express.Request, res: express.Response, next: express.NextFunction) { + const videoChannelInstance: VideoChannelModel = res.locals.videoChannel + + const resultList = await VideoModel.listForApi({ + start: req.query.start, + count: req.query.count, + sort: req.query.sort, + hideNSFW: isNSFWHidden(res), + withFiles: false, + videoChannelId: videoChannelInstance.id + }) + + return res.json(getFormattedObjects(resultList.data, resultList.total)) +} + + +async function listAccountVideos (req: express.Request, res: express.Response, next: express.NextFunction) { const account: AccountModel = res.locals.account - const resultList = await VideoModel.listForApi( - req.query.start as number, - req.query.count as number, - req.query.sort as VideoSortField, - isNSFWHidden(res), - null, - false, - account.id - ) + const resultList = await VideoModel.listForApi({ + start: req.query.start, + count: req.query.count, + sort: req.query.sort, + hideNSFW: isNSFWHidden(res), + withFiles: false, + accountId: account.id + }) return res.json(getFormattedObjects(resultList.data, resultList.total)) } diff --git a/server/controllers/api/index.ts b/server/controllers/api/index.ts index 964d5d04c..8f63b9535 100644 --- a/server/controllers/api/index.ts +++ b/server/controllers/api/index.ts @@ -7,6 +7,7 @@ import { usersRouter } from './users' import { accountsRouter } from './accounts' import { videosRouter } from './videos' import { badRequest } from '../../helpers/express-utils' +import { videoChannelRouter } from './video-channel' const apiRouter = express.Router() @@ -15,6 +16,7 @@ apiRouter.use('/oauth-clients', oauthClientsRouter) apiRouter.use('/config', configRouter) apiRouter.use('/users', usersRouter) apiRouter.use('/accounts', accountsRouter) +apiRouter.use('/video-channels', videoChannelRouter) apiRouter.use('/videos', videosRouter) apiRouter.use('/jobs', jobsRouter) apiRouter.use('/ping', pong) diff --git a/server/controllers/api/video-channel.ts b/server/controllers/api/video-channel.ts new file mode 100644 index 000000000..d57273747 --- /dev/null +++ b/server/controllers/api/video-channel.ts @@ -0,0 +1,34 @@ +import * as express from 'express' +import { getFormattedObjects } from '../../helpers/utils' +import { + asyncMiddleware, + paginationValidator, + setDefaultPagination, + setDefaultSort, + videoChannelsSortValidator +} from '../../middlewares' +import { VideoChannelModel } from '../../models/video/video-channel' + +const videoChannelRouter = express.Router() + +videoChannelRouter.get('/', + paginationValidator, + videoChannelsSortValidator, + setDefaultSort, + setDefaultPagination, + asyncMiddleware(listVideoChannels) +) + +// --------------------------------------------------------------------------- + +export { + videoChannelRouter +} + +// --------------------------------------------------------------------------- + +async function listVideoChannels (req: express.Request, res: express.Response, next: express.NextFunction) { + const resultList = await VideoChannelModel.listForApi(req.query.start, req.query.count, req.query.sort) + + return res.json(getFormattedObjects(resultList.data, resultList.total)) +} diff --git a/server/controllers/api/videos/channel.ts b/server/controllers/api/videos/channel.ts deleted file mode 100644 index e547d375f..000000000 --- a/server/controllers/api/videos/channel.ts +++ /dev/null @@ -1,177 +0,0 @@ -import * as express from 'express' -import { VideoChannelCreate, VideoChannelUpdate } from '../../../../shared' -import { retryTransactionWrapper } from '../../../helpers/database-utils' -import { logger } from '../../../helpers/logger' -import { getFormattedObjects, resetSequelizeInstance } from '../../../helpers/utils' -import { sequelizeTypescript } from '../../../initializers' -import { setAsyncActorKeys } from '../../../lib/activitypub' -import { sendUpdateActor } from '../../../lib/activitypub/send' -import { createVideoChannel } from '../../../lib/video-channel' -import { - asyncMiddleware, authenticate, listVideoAccountChannelsValidator, paginationValidator, setDefaultSort, setDefaultPagination, - videoChannelsAddValidator, videoChannelsGetValidator, videoChannelsRemoveValidator, videoChannelsSortValidator, - videoChannelsUpdateValidator -} from '../../../middlewares' -import { AccountModel } from '../../../models/account/account' -import { VideoChannelModel } from '../../../models/video/video-channel' - -const videoChannelRouter = express.Router() - -videoChannelRouter.get('/channels', - paginationValidator, - videoChannelsSortValidator, - setDefaultSort, - setDefaultPagination, - asyncMiddleware(listVideoChannels) -) - -videoChannelRouter.get('/accounts/:accountId/channels', - asyncMiddleware(listVideoAccountChannelsValidator), - asyncMiddleware(listVideoAccountChannels) -) - -videoChannelRouter.post('/channels', - authenticate, - videoChannelsAddValidator, - asyncMiddleware(addVideoChannelRetryWrapper) -) - -videoChannelRouter.put('/channels/:id', - authenticate, - asyncMiddleware(videoChannelsUpdateValidator), - updateVideoChannelRetryWrapper -) - -videoChannelRouter.delete('/channels/:id', - authenticate, - asyncMiddleware(videoChannelsRemoveValidator), - asyncMiddleware(removeVideoChannelRetryWrapper) -) - -videoChannelRouter.get('/channels/:id', - asyncMiddleware(videoChannelsGetValidator), - asyncMiddleware(getVideoChannel) -) - -// --------------------------------------------------------------------------- - -export { - videoChannelRouter -} - -// --------------------------------------------------------------------------- - -async function listVideoChannels (req: express.Request, res: express.Response, next: express.NextFunction) { - const resultList = await VideoChannelModel.listForApi(req.query.start, req.query.count, req.query.sort) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -async function listVideoAccountChannels (req: express.Request, res: express.Response, next: express.NextFunction) { - const resultList = await VideoChannelModel.listByAccount(res.locals.account.id) - - return res.json(getFormattedObjects(resultList.data, resultList.total)) -} - -// Wrapper to video channel add that retry the async function if there is a database error -// We need this because we run the transaction in SERIALIZABLE isolation that can fail -async function addVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { - const options = { - arguments: [ req, res ], - errorMessage: 'Cannot insert the video video channel with many retries.' - } - - const videoChannel = await retryTransactionWrapper(addVideoChannel, options) - return res.json({ - videoChannel: { - id: videoChannel.id - } - }).end() -} - -async function addVideoChannel (req: express.Request, res: express.Response) { - const videoChannelInfo: VideoChannelCreate = req.body - const account: AccountModel = res.locals.oauth.token.User.Account - - const videoChannelCreated: VideoChannelModel = await sequelizeTypescript.transaction(async t => { - return createVideoChannel(videoChannelInfo, account, t) - }) - - setAsyncActorKeys(videoChannelCreated.Actor) - .catch(err => logger.error('Cannot set async actor keys for account %s.', videoChannelCreated.Actor.uuid, { err })) - - logger.info('Video channel with uuid %s created.', videoChannelCreated.Actor.uuid) - - return videoChannelCreated -} - -async function updateVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { - const options = { - arguments: [ req, res ], - errorMessage: 'Cannot update the video with many retries.' - } - - await retryTransactionWrapper(updateVideoChannel, options) - - return res.type('json').status(204).end() -} - -async function updateVideoChannel (req: express.Request, res: express.Response) { - const videoChannelInstance = res.locals.videoChannel as VideoChannelModel - const videoChannelFieldsSave = videoChannelInstance.toJSON() - const videoChannelInfoToUpdate = req.body as VideoChannelUpdate - - try { - await sequelizeTypescript.transaction(async t => { - const sequelizeOptions = { - transaction: t - } - - if (videoChannelInfoToUpdate.name !== undefined) videoChannelInstance.set('name', videoChannelInfoToUpdate.name) - if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.set('description', videoChannelInfoToUpdate.description) - if (videoChannelInfoToUpdate.support !== undefined) videoChannelInstance.set('support', videoChannelInfoToUpdate.support) - - const videoChannelInstanceUpdated = await videoChannelInstance.save(sequelizeOptions) - await sendUpdateActor(videoChannelInstanceUpdated, t) - }) - - logger.info('Video channel with name %s and uuid %s updated.', videoChannelInstance.name, videoChannelInstance.Actor.uuid) - } catch (err) { - logger.debug('Cannot update the video channel.', { err }) - - // Force fields we want to update - // If the transaction is retried, sequelize will think the object has not changed - // So it will skip the SQL request, even if the last one was ROLLBACKed! - resetSequelizeInstance(videoChannelInstance, videoChannelFieldsSave) - - throw err - } -} - -async function removeVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { - const options = { - arguments: [ req, res ], - errorMessage: 'Cannot remove the video channel with many retries.' - } - - await retryTransactionWrapper(removeVideoChannel, options) - - return res.type('json').status(204).end() -} - -async function removeVideoChannel (req: express.Request, res: express.Response) { - const videoChannelInstance: VideoChannelModel = res.locals.videoChannel - - return sequelizeTypescript.transaction(async t => { - await videoChannelInstance.destroy({ transaction: t }) - - logger.info('Video channel with name %s and uuid %s deleted.', videoChannelInstance.name, videoChannelInstance.Actor.uuid) - }) - -} - -async function getVideoChannel (req: express.Request, res: express.Response, next: express.NextFunction) { - const videoChannelWithVideos = await VideoChannelModel.loadAndPopulateAccountAndVideos(res.locals.videoChannel.id) - - return res.json(videoChannelWithVideos.toFormattedJSON()) -} diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 61b6c5826..4b3198a74 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts @@ -42,7 +42,6 @@ import { VideoModel } from '../../../models/video/video' import { VideoFileModel } from '../../../models/video/video-file' import { abuseVideoRouter } from './abuse' import { blacklistRouter } from './blacklist' -import { videoChannelRouter } from './channel' import { videoCommentRouter } from './comment' import { rateVideoRouter } from './rate' import { VideoFilter } from '../../../../shared/models/videos/video-query.type' @@ -72,7 +71,6 @@ const reqVideoFileUpdate = createReqFiles( videosRouter.use('/', abuseVideoRouter) videosRouter.use('/', blacklistRouter) videosRouter.use('/', rateVideoRouter) -videosRouter.use('/', videoChannelRouter) videosRouter.use('/', videoCommentRouter) videosRouter.get('/categories', listVideoCategories) @@ -397,13 +395,14 @@ async function getVideoDescription (req: express.Request, res: express.Response) } async function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) { - const resultList = await VideoModel.listForApi( - req.query.start as number, - req.query.count as number, - req.query.sort as VideoSortField, - isNSFWHidden(res), - req.query.filter as VideoFilter - ) + const resultList = await VideoModel.listForApi({ + start: req.query.start, + count: req.query.count, + sort: req.query.sort, + hideNSFW: isNSFWHidden(res), + filter: req.query.filter as VideoFilter, + withFiles: false + }) return res.json(getFormattedObjects(resultList.data, resultList.total)) } diff --git a/server/controllers/feeds.ts b/server/controllers/feeds.ts index 6a6af3e09..7dcaf7004 100644 --- a/server/controllers/feeds.ts +++ b/server/controllers/feeds.ts @@ -33,15 +33,15 @@ async function generateFeed (req: express.Request, res: express.Response, next: const account: AccountModel = res.locals.account const hideNSFW = CONFIG.INSTANCE.DEFAULT_NSFW_POLICY === 'do_not_list' - const resultList = await VideoModel.listForApi( + const resultList = await VideoModel.listForApi({ start, - FEEDS.COUNT, - req.query.sort as VideoSortField, + count: FEEDS.COUNT, + sort: req.query.sort, hideNSFW, - req.query.filter, - true, - account ? account.id : null - ) + filter: req.query.filter, + withFiles: true, + accountId: account ? account.id : null + }) // Adding video items to the feed, one at a time resultList.data.forEach(video => { diff --git a/server/middlewares/validators/follows.ts b/server/middlewares/validators/follows.ts index 991a2e175..bdf39eb9c 100644 --- a/server/middlewares/validators/follows.ts +++ b/server/middlewares/validators/follows.ts @@ -16,7 +16,7 @@ const followValidator = [ if (isTestInstance() === false && CONFIG.WEBSERVER.SCHEME === 'http') { return res.status(400) .json({ - error: 'Cannot follow non HTTPS web server.' + error: 'Cannot follow on a non HTTPS web server.' }) .end() } diff --git a/server/middlewares/validators/video-channels.ts b/server/middlewares/validators/video-channels.ts index fe42105e8..e3a11a41b 100644 --- a/server/middlewares/validators/video-channels.ts +++ b/server/middlewares/validators/video-channels.ts @@ -26,6 +26,7 @@ const listVideoAccountChannelsValidator = [ ] const videoChannelsAddValidator = [ + param('accountId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid account id'), body('name').custom(isVideoChannelNameValid).withMessage('Should have a valid name'), body('description').optional().custom(isVideoChannelDescriptionValid).withMessage('Should have a valid description'), body('support').optional().custom(isVideoChannelSupportValid).withMessage('Should have a valid support text'), @@ -41,6 +42,7 @@ const videoChannelsAddValidator = [ const videoChannelsUpdateValidator = [ param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + param('accountId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid account id'), body('name').optional().custom(isVideoChannelNameValid).withMessage('Should have a valid name'), body('description').optional().custom(isVideoChannelDescriptionValid).withMessage('Should have a valid description'), body('support').optional().custom(isVideoChannelSupportValid).withMessage('Should have a valid support text'), @@ -49,6 +51,7 @@ const videoChannelsUpdateValidator = [ logger.debug('Checking videoChannelsUpdate parameters', { parameters: req.body }) if (areValidationErrors(req, res)) return + if (!await isAccountIdExist(req.params.accountId, res)) return if (!await isVideoChannelExist(req.params.id, res)) return // We need to make additional checks @@ -70,11 +73,13 @@ const videoChannelsUpdateValidator = [ const videoChannelsRemoveValidator = [ param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + param('accountId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid account id'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking videoChannelsRemove parameters', { parameters: req.params }) if (areValidationErrors(req, res)) return + if (!await isAccountIdExist(req.params.accountId, res)) return if (!await isVideoChannelExist(req.params.id, res)) return // Check if the user who did the request is able to delete the video @@ -87,11 +92,14 @@ const videoChannelsRemoveValidator = [ const videoChannelsGetValidator = [ param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + param('accountId').optional().custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid account id'), async (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking videoChannelsGet parameters', { parameters: req.params }) if (areValidationErrors(req, res)) return + // On some routes, accountId is optional (for example in the ActivityPub route) + if (req.params.accountId && !await isAccountIdExist(req.params.accountId, res)) return if (!await isVideoChannelExist(req.params.id, res)) return return next() diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 2ad9c00dd..7ababbf23 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -95,7 +95,14 @@ enum ScopeNames { } @Scopes({ - [ScopeNames.AVAILABLE_FOR_LIST]: (actorId: number, hideNSFW: boolean, filter?: VideoFilter, withFiles?: boolean, accountId?: number) => { + [ScopeNames.AVAILABLE_FOR_LIST]: (options: { + actorId: number, + hideNSFW: boolean, + filter?: VideoFilter, + withFiles?: boolean, + accountId?: number, + videoChannelId?: number + }) => { const accountInclude = { attributes: [ 'name' ], model: AccountModel.unscoped(), @@ -106,7 +113,7 @@ enum ScopeNames { attributes: [ 'preferredUsername', 'url', 'serverId', 'avatarId' ], model: ActorModel.unscoped(), required: true, - where: VideoModel.buildActorWhereWithFilter(filter), + where: VideoModel.buildActorWhereWithFilter(options.filter), include: [ { attributes: [ 'host' ], @@ -122,6 +129,18 @@ enum ScopeNames { ] } + const videoChannelInclude = { + attributes: [ 'name', 'description' ], + model: VideoChannelModel.unscoped(), + required: true, + where: {}, + include: [ + accountInclude + ] + } + + // Force actorId to be a number to avoid SQL injections + const actorIdNumber = parseInt(options.actorId.toString(), 10) const query: IFindOptions = { where: { id: { @@ -132,32 +151,23 @@ enum ScopeNames { '(' + 'SELECT "videoShare"."videoId" AS "id" FROM "videoShare" ' + 'INNER JOIN "actorFollow" ON "actorFollow"."targetActorId" = "videoShare"."actorId" ' + - 'WHERE "actorFollow"."actorId" = ' + parseInt(actorId.toString(), 10) + + 'WHERE "actorFollow"."actorId" = ' + actorIdNumber + ' UNION ' + 'SELECT "video"."id" AS "id" FROM "video" ' + 'INNER JOIN "videoChannel" ON "videoChannel"."id" = "video"."channelId" ' + 'INNER JOIN "account" ON "account"."id" = "videoChannel"."accountId" ' + 'INNER JOIN "actor" ON "account"."actorId" = "actor"."id" ' + 'LEFT JOIN "actorFollow" ON "actorFollow"."targetActorId" = "actor"."id" ' + - 'WHERE "actor"."serverId" IS NULL OR "actorFollow"."actorId" = ' + parseInt(actorId.toString(), 10) + + 'WHERE "actor"."serverId" IS NULL OR "actorFollow"."actorId" = ' + actorIdNumber + ')' ) }, privacy: VideoPrivacy.PUBLIC }, - include: [ - { - attributes: [ 'name', 'description' ], - model: VideoChannelModel.unscoped(), - required: true, - include: [ - accountInclude - ] - } - ] + include: [ videoChannelInclude ] } - if (withFiles === true) { + if (options.withFiles === true) { query.include.push({ model: VideoFileModel.unscoped(), required: true @@ -165,13 +175,19 @@ enum ScopeNames { } // Hide nsfw videos? - if (hideNSFW === true) { + if (options.hideNSFW === true) { query.where['nsfw'] = false } - if (accountId) { + if (options.accountId) { accountInclude.where = { - id: accountId + id: options.accountId + } + } + + if (options.videoChannelId) { + videoChannelInclude.where = { + id: options.videoChannelId } } @@ -697,23 +713,37 @@ export class VideoModel extends Model { }) } - static async listForApi ( + static async listForApi (options: { start: number, count: number, sort: string, hideNSFW: boolean, + withFiles: boolean, filter?: VideoFilter, - withFiles = false, - accountId?: number - ) { + accountId?: number, + videoChannelId?: number + }) { const query = { - offset: start, - limit: count, - order: getSort(sort) + offset: options.start, + limit: options.count, + order: getSort(options.sort) } const serverActor = await getServerActor() - return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, hideNSFW, filter, withFiles, accountId ] }) + const scopes = { + method: [ + ScopeNames.AVAILABLE_FOR_LIST, { + actorId: serverActor.id, + hideNSFW: options.hideNSFW, + filter: options.filter, + withFiles: options.withFiles, + accountId: options.accountId, + videoChannelId: options.videoChannelId + } + ] + } + + return VideoModel.scope(scopes) .findAndCountAll(query) .then(({ rows, count }) => { return { @@ -750,8 +780,16 @@ export class VideoModel extends Model { } const serverActor = await getServerActor() + const scopes = { + method: [ + ScopeNames.AVAILABLE_FOR_LIST, { + actorId: serverActor.id, + hideNSFW + } + ] + } - return VideoModel.scope({ method: [ ScopeNames.AVAILABLE_FOR_LIST, serverActor.id, hideNSFW ] }) + return VideoModel.scope(scopes) .findAndCountAll(query) .then(({ rows, count }) => { return { diff --git a/server/tests/api/check-params/video-channels.ts b/server/tests/api/check-params/video-channels.ts index 43c5462ee..acb6bdd57 100644 --- a/server/tests/api/check-params/video-channels.ts +++ b/server/tests/api/check-params/video-channels.ts @@ -4,15 +4,29 @@ import * as chai from 'chai' import { omit } from 'lodash' import 'mocha' import { - createUser, deleteVideoChannel, flushTests, getAccountVideoChannelsList, getVideoChannelsList, immutableAssign, killallServers, - makeGetRequest, makePostBodyRequest, makePutBodyRequest, runServer, ServerInfo, setAccessTokensToServers, userLogin + createUser, + deleteVideoChannel, + flushTests, + getAccountVideoChannelsList, + getVideoChannelsList, + immutableAssign, + killallServers, + makeGetRequest, + makePostBodyRequest, + makePutBodyRequest, + runServer, + ServerInfo, + setAccessTokensToServers, + userLogin } from '../../utils' import { checkBadCountPagination, checkBadSortPagination, checkBadStartPagination } from '../../utils/requests/check-api-params' +import { getAccountsList } from '../../utils/users/accounts' const expect = chai.expect describe('Test videos API validator', function () { - const path = '/api/v1/videos/channels' + const videoChannelPath = '/api/v1/video-channels' + const accountPath = '/api/v1/accounts/' let server: ServerInfo let accessTokenUser: string @@ -37,15 +51,15 @@ describe('Test videos API validator', function () { describe('When listing a video channels', function () { it('Should fail with a bad start pagination', async function () { - await checkBadStartPagination(server.url, path, server.accessToken) + await checkBadStartPagination(server.url, videoChannelPath, server.accessToken) }) it('Should fail with a bad count pagination', async function () { - await checkBadCountPagination(server.url, path, server.accessToken) + await checkBadCountPagination(server.url, videoChannelPath, server.accessToken) }) it('Should fail with an incorrect sort', async function () { - await checkBadSortPagination(server.url, path, server.accessToken) + await checkBadSortPagination(server.url, videoChannelPath, server.accessToken) }) }) @@ -60,12 +74,20 @@ describe('Test videos API validator', function () { }) describe('When adding a video channel', function () { + let path: string + const baseCorrectParams = { name: 'hello', description: 'super description', support: 'super support text' } + before(async function () { + const res = await getAccountsList(server.url) + const accountId = res.body.data[0].id + path = accountPath + accountId + '/video-channels' + }) + it('Should fail with a non authenticated user', async function () { await makePostBodyRequest({ url: server.url, path, token: 'none', fields: baseCorrectParams, statusCodeExpected: 401 }) }) @@ -107,22 +129,27 @@ describe('Test videos API validator', function () { }) describe('When updating a video channel', function () { + let path: string + const baseCorrectParams = { name: 'hello', description: 'super description' } - let videoChannelId - before(async function () { - const res = await getVideoChannelsList(server.url, 0, 1) - videoChannelId = res.body.data[0].id + const res1 = await getVideoChannelsList(server.url, 0, 1) + const videoChannelId = res1.body.data[0].id + + const res2 = await getAccountsList(server.url) + const accountId = res2.body.data[0].id + + path = accountPath + accountId + '/video-channels/' + videoChannelId }) it('Should fail with a non authenticated user', async function () { await makePutBodyRequest({ url: server.url, - path: path + '/' + videoChannelId, + path, token: 'hi', fields: baseCorrectParams, statusCodeExpected: 401 @@ -132,7 +159,7 @@ describe('Test videos API validator', function () { it('Should fail with another authenticated user', async function () { await makePutBodyRequest({ url: server.url, - path: path + '/' + videoChannelId, + path, token: accessTokenUser, fields: baseCorrectParams, statusCodeExpected: 403 @@ -141,23 +168,23 @@ describe('Test videos API validator', function () { it('Should fail with a long name', async function () { const fields = immutableAssign(baseCorrectParams, { name: 'super'.repeat(25) }) - await makePutBodyRequest({ url: server.url, path: path + '/' + videoChannelId, token: server.accessToken, fields }) + await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) }) it('Should fail with a long description', async function () { const fields = immutableAssign(baseCorrectParams, { description: 'super'.repeat(60) }) - await makePutBodyRequest({ url: server.url, path: path + '/' + videoChannelId, token: server.accessToken, fields }) + await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) }) it('Should fail with a long support text', async function () { const fields = immutableAssign(baseCorrectParams, { support: 'super'.repeat(70) }) - await makePutBodyRequest({ url: server.url, path: path + '/' + videoChannelId, token: server.accessToken, fields }) + await makePutBodyRequest({ url: server.url, path, token: server.accessToken, fields }) }) it('Should succeed with the correct parameters', async function () { await makePutBodyRequest({ url: server.url, - path: path + '/' + videoChannelId, + path, token: server.accessToken, fields: baseCorrectParams, statusCodeExpected: 204 @@ -166,17 +193,23 @@ describe('Test videos API validator', function () { }) describe('When getting a video channel', function () { + let basePath: string let videoChannelId: number before(async function () { - const res = await getVideoChannelsList(server.url, 0, 1) - videoChannelId = res.body.data[0].id + const res1 = await getVideoChannelsList(server.url, 0, 1) + videoChannelId = res1.body.data[0].id + + const res2 = await getAccountsList(server.url) + const accountId = res2.body.data[0].id + + basePath = accountPath + accountId + '/video-channels' }) it('Should return the list of the video channels with nothing', async function () { const res = await makeGetRequest({ url: server.url, - path, + path: basePath, statusCodeExpected: 200 }) @@ -186,7 +219,7 @@ describe('Test videos API validator', function () { it('Should fail without a correct uuid', async function () { await makeGetRequest({ url: server.url, - path: path + '/coucou', + path: basePath + '/coucou', statusCodeExpected: 400 }) }) @@ -194,7 +227,7 @@ describe('Test videos API validator', function () { it('Should return 404 with an incorrect video channel', async function () { await makeGetRequest({ url: server.url, - path: path + '/4da6fde3-88f7-4d16-b119-108df5630b06', + path: basePath + '/4da6fde3-88f7-4d16-b119-108df5630b06', statusCodeExpected: 404 }) }) @@ -202,7 +235,7 @@ describe('Test videos API validator', function () { it('Should succeed with the correct parameters', async function () { await makeGetRequest({ url: server.url, - path: path + '/' + videoChannelId, + path: basePath + '/' + videoChannelId, statusCodeExpected: 200 }) }) @@ -210,33 +243,41 @@ describe('Test videos API validator', function () { describe('When deleting a video channel', function () { let videoChannelId: number + let accountId: number before(async function () { - const res = await getVideoChannelsList(server.url, 0, 1) - videoChannelId = res.body.data[0].id + const res1 = await getVideoChannelsList(server.url, 0, 1) + videoChannelId = res1.body.data[0].id + + const res2 = await getAccountsList(server.url) + accountId = res2.body.data[0].id }) it('Should fail with a non authenticated user', async function () { - await deleteVideoChannel(server.url, 'coucou', videoChannelId, 401) + await deleteVideoChannel(server.url, 'coucou', accountId, videoChannelId, 401) }) it('Should fail with another authenticated user', async function () { - await deleteVideoChannel(server.url, accessTokenUser, videoChannelId, 403) + await deleteVideoChannel(server.url, accessTokenUser, accountId, videoChannelId, 403) + }) + + it('Should fail with an unknown account id', async function () { + await deleteVideoChannel(server.url, server.accessToken, 454554,videoChannelId, 404) }) - it('Should fail with an unknown id', async function () { - await deleteVideoChannel(server.url, server.accessToken, 454554, 404) + it('Should fail with an unknown video channel id', async function () { + await deleteVideoChannel(server.url, server.accessToken, accountId,454554, 404) }) it('Should succeed with the correct parameters', async function () { - await deleteVideoChannel(server.url, server.accessToken, videoChannelId) + await deleteVideoChannel(server.url, server.accessToken, accountId, videoChannelId) }) it('Should fail to delete the last user video channel', async function () { const res = await getVideoChannelsList(server.url, 0, 1) videoChannelId = res.body.data[0].id - await deleteVideoChannel(server.url, server.accessToken, videoChannelId, 409) + await deleteVideoChannel(server.url, server.accessToken, accountId, videoChannelId, 409) }) }) diff --git a/server/tests/api/videos/multiple-servers.ts b/server/tests/api/videos/multiple-servers.ts index 2563939ec..6238cdc08 100644 --- a/server/tests/api/videos/multiple-servers.ts +++ b/server/tests/api/videos/multiple-servers.ts @@ -39,6 +39,7 @@ import { getVideoCommentThreads, getVideoThreadComments } from '../../utils/videos/video-comments' +import { getAccountsList } from '../../utils/users/accounts' const expect = chai.expect @@ -46,6 +47,7 @@ describe('Test multiple servers', function () { let servers: ServerInfo[] = [] const toRemove = [] let videoUUID = '' + let accountId: number let videoChannelId: number before(async function () { @@ -56,13 +58,20 @@ describe('Test multiple servers', function () { // Get the access tokens await setAccessTokensToServers(servers) - const videoChannel = { - name: 'my channel', - description: 'super channel' + { + const res = await getAccountsList(servers[0].url) + accountId = res.body.data[0].id + } + + { + const videoChannel = { + name: 'my channel', + description: 'super channel' + } + await addVideoChannel(servers[ 0 ].url, servers[ 0 ].accessToken, accountId, videoChannel) + const channelRes = await getVideoChannelsList(servers[ 0 ].url, 0, 1) + videoChannelId = channelRes.body.data[ 0 ].id } - await addVideoChannel(servers[0].url, servers[0].accessToken, videoChannel) - const channelRes = await getVideoChannelsList(servers[0].url, 0, 1) - videoChannelId = channelRes.body.data[0].id // Server 1 and server 2 follow each other await doubleFollow(servers[0], servers[1]) diff --git a/server/tests/api/videos/video-channels.ts b/server/tests/api/videos/video-channels.ts index b9c9bbf3c..a7552a83a 100644 --- a/server/tests/api/videos/video-channels.ts +++ b/server/tests/api/videos/video-channels.ts @@ -17,12 +17,14 @@ import { setAccessTokensToServers, updateVideoChannel } from '../../utils/index' +import { getAccountsList } from '../../utils/users/accounts' const expect = chai.expect describe('Test video channels', function () { let servers: ServerInfo[] let userInfo: User + let accountId: number let videoChannelId: number before(async function () { @@ -35,6 +37,11 @@ describe('Test video channels', function () { await setAccessTokensToServers(servers) await doubleFollow(servers[0], servers[1]) + { + const res = await getAccountsList(servers[0].url) + accountId = res.body.data[0].id + } + await wait(5000) }) @@ -54,7 +61,7 @@ describe('Test video channels', function () { description: 'super video channel description', support: 'super video channel support text' } - const res = await addVideoChannel(servers[0].url, servers[0].accessToken, videoChannel) + const res = await addVideoChannel(servers[0].url, servers[0].accessToken, accountId, videoChannel) videoChannelId = res.body.videoChannel.id // The channel is 1 is propagated to servers 2 @@ -120,7 +127,7 @@ describe('Test video channels', function () { support: 'video channel support text updated' } - await updateVideoChannel(servers[0].url, servers[0].accessToken, videoChannelId, videoChannelAttributes) + await updateVideoChannel(servers[0].url, servers[0].accessToken, accountId, videoChannelId, videoChannelAttributes) await wait(3000) }) @@ -139,7 +146,7 @@ describe('Test video channels', function () { }) it('Should get video channel', async function () { - const res = await getVideoChannel(servers[0].url, videoChannelId) + const res = await getVideoChannel(servers[0].url, accountId, videoChannelId) const videoChannel = res.body expect(videoChannel.displayName).to.equal('video channel updated') @@ -148,7 +155,7 @@ describe('Test video channels', function () { }) it('Should delete video channel', async function () { - await deleteVideoChannel(servers[0].url, servers[0].accessToken, videoChannelId) + await deleteVideoChannel(servers[0].url, servers[0].accessToken, accountId, videoChannelId) }) it('Should have video channel deleted', async function () { diff --git a/server/tests/utils/videos/video-channels.ts b/server/tests/utils/videos/video-channels.ts index 2d095d8ab..cfc541431 100644 --- a/server/tests/utils/videos/video-channels.ts +++ b/server/tests/utils/videos/video-channels.ts @@ -7,7 +7,7 @@ type VideoChannelAttributes = { } function getVideoChannelsList (url: string, start: number, count: number, sort?: string) { - const path = '/api/v1/videos/channels' + const path = '/api/v1/video-channels' const req = request(url) .get(path) @@ -22,7 +22,7 @@ function getVideoChannelsList (url: string, start: number, count: number, sort?: } function getAccountVideoChannelsList (url: string, accountId: number | string, specialStatus = 200) { - const path = '/api/v1/videos/accounts/' + accountId + '/channels' + const path = '/api/v1/accounts/' + accountId + '/video-channels' return request(url) .get(path) @@ -31,8 +31,14 @@ function getAccountVideoChannelsList (url: string, accountId: number | string, s .expect('Content-Type', /json/) } -function addVideoChannel (url: string, token: string, videoChannelAttributesArg: VideoChannelAttributes, expectedStatus = 200) { - const path = '/api/v1/videos/channels' +function addVideoChannel ( + url: string, + token: string, + accountId: number, + videoChannelAttributesArg: VideoChannelAttributes, + expectedStatus = 200 +) { + const path = '/api/v1/accounts/' + accountId + '/video-channels/' // Default attributes let attributes = { @@ -50,9 +56,16 @@ function addVideoChannel (url: string, token: string, videoChannelAttributesArg: .expect(expectedStatus) } -function updateVideoChannel (url: string, token: string, channelId: number, attributes: VideoChannelAttributes, expectedStatus = 204) { +function updateVideoChannel ( + url: string, + token: string, + accountId: number, + channelId: number, + attributes: VideoChannelAttributes, + expectedStatus = 204 +) { const body = {} - const path = '/api/v1/videos/channels/' + channelId + const path = '/api/v1/accounts/' + accountId + '/video-channels/' + channelId if (attributes.name) body['name'] = attributes.name if (attributes.description) body['description'] = attributes.description @@ -66,18 +79,18 @@ function updateVideoChannel (url: string, token: string, channelId: number, attr .expect(expectedStatus) } -function deleteVideoChannel (url: string, token: string, channelId: number, expectedStatus = 204) { - const path = '/api/v1/videos/channels/' +function deleteVideoChannel (url: string, token: string, accountId: number, channelId: number, expectedStatus = 204) { + const path = '/api/v1/accounts/' + accountId + '/video-channels/' + channelId return request(url) - .delete(path + channelId) + .delete(path) .set('Accept', 'application/json') .set('Authorization', 'Bearer ' + token) .expect(expectedStatus) } -function getVideoChannel (url: string, channelId: number) { - const path = '/api/v1/videos/channels/' + channelId +function getVideoChannel (url: string, accountId: number, channelId: number) { + const path = '/api/v1/accounts/' + accountId + '/video-channels/' + channelId return request(url) .get(path) -- cgit v1.2.3