From 72c7248b6fdcdb2175e726ff51b42e7555f2bd84 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 24 Oct 2017 19:41:09 +0200 Subject: [PATCH] Add video channels --- server/controllers/api/remote/pods.ts | 4 +- server/controllers/api/remote/videos.ts | 301 ++++++++++++--- server/controllers/api/users.ts | 33 +- server/controllers/api/videos/channel.ts | 196 ++++++++++ server/controllers/api/videos/index.ts | 48 ++- server/controllers/services.ts | 2 +- server/helpers/custom-validators/index.ts | 2 + server/helpers/custom-validators/misc.ts | 24 +- .../custom-validators/remote/videos.ts | 104 ++++-- .../custom-validators/video-authors.ts | 45 +++ .../custom-validators/video-channels.ts | 57 +++ server/helpers/custom-validators/videos.ts | 20 - server/initializers/constants.ts | 23 +- server/initializers/database.ts | 2 + server/initializers/installer.ts | 19 +- server/lib/cache/videos-preview-cache.ts | 2 +- server/lib/friends.ts | 138 ++++++- server/lib/index.ts | 2 + server/lib/user.ts | 46 +++ server/lib/video-channel.ts | 42 +++ server/middlewares/sort.ts | 7 + server/middlewares/validators/index.ts | 1 + server/middlewares/validators/oembed.ts | 11 +- server/middlewares/validators/sort.ts | 3 + server/middlewares/validators/users.ts | 4 +- .../middlewares/validators/video-blacklist.ts | 6 +- .../middlewares/validators/video-channels.ts | 142 +++++++ server/middlewares/validators/videos.ts | 33 +- server/models/oauth/oauth-token.ts | 24 +- server/models/request/request-video-event.ts | 5 +- server/models/user/user-interface.ts | 8 +- server/models/user/user.ts | 69 +++- server/models/video/author-interface.ts | 29 +- server/models/video/author.ts | 103 +++++- server/models/video/index.ts | 1 + .../models/video/video-channel-interface.ts | 64 ++++ server/models/video/video-channel.ts | 349 ++++++++++++++++++ server/models/video/video-interface.ts | 16 +- server/models/video/video.ts | 180 +++++++-- shared/models/pods/remote-video/index.ts | 5 + ...emote-video-author-create-request.model.ts | 11 + ...emote-video-author-remove-request.model.ts | 10 + ...mote-video-channel-create-request.model.ts | 15 + ...mote-video-channel-remove-request.model.ts | 10 + ...mote-video-channel-update-request.model.ts | 15 + .../remote-video-create-request.model.ts | 4 +- .../remote-video-remove-request.model.ts | 2 +- .../remote-video-request.model.ts | 7 +- .../remote-video-update-request.model.ts | 6 +- shared/models/users/user.model.ts | 8 +- shared/models/videos/index.ts | 3 + .../videos/video-channel-create.model.ts | 4 + .../videos/video-channel-update.model.ts | 4 + shared/models/videos/video-channel.model.ts | 15 + shared/models/videos/video-create.model.ts | 1 + shared/models/videos/video.model.ts | 6 + 56 files changed, 2011 insertions(+), 280 deletions(-) create mode 100644 server/controllers/api/videos/channel.ts create mode 100644 server/helpers/custom-validators/video-authors.ts create mode 100644 server/helpers/custom-validators/video-channels.ts create mode 100644 server/lib/user.ts create mode 100644 server/lib/video-channel.ts create mode 100644 server/middlewares/validators/video-channels.ts create mode 100644 server/models/video/video-channel-interface.ts create mode 100644 server/models/video/video-channel.ts create mode 100644 shared/models/pods/remote-video/remote-video-author-create-request.model.ts create mode 100644 shared/models/pods/remote-video/remote-video-author-remove-request.model.ts create mode 100644 shared/models/pods/remote-video/remote-video-channel-create-request.model.ts create mode 100644 shared/models/pods/remote-video/remote-video-channel-remove-request.model.ts create mode 100644 shared/models/pods/remote-video/remote-video-channel-update-request.model.ts create mode 100644 shared/models/videos/video-channel-create.model.ts create mode 100644 shared/models/videos/video-channel-update.model.ts create mode 100644 shared/models/videos/video-channel.model.ts diff --git a/server/controllers/api/remote/pods.ts b/server/controllers/api/remote/pods.ts index 6f7b5f651..a62b9c684 100644 --- a/server/controllers/api/remote/pods.ts +++ b/server/controllers/api/remote/pods.ts @@ -7,7 +7,7 @@ import { setBodyHostPort, remotePodsAddValidator } from '../../../middlewares' -import { sendOwnedVideosToPod } from '../../../lib' +import { sendOwnedDataToPod } from '../../../lib' import { getMyPublicCert, getFormattedObjects } from '../../../helpers' import { CONFIG } from '../../../initializers' import { PodInstance } from '../../../models' @@ -43,7 +43,7 @@ function addPods (req: express.Request, res: express.Response, next: express.Nex const pod = db.Pod.build(information) pod.save() .then(podCreated => { - return sendOwnedVideosToPod(podCreated.id) + return sendOwnedDataToPod(podCreated.id) }) .then(() => { return getMyPublicCert() diff --git a/server/controllers/api/remote/videos.ts b/server/controllers/api/remote/videos.ts index 23023211f..c8f531490 100644 --- a/server/controllers/api/remote/videos.ts +++ b/server/controllers/api/remote/videos.ts @@ -1,5 +1,6 @@ import * as express from 'express' import * as Promise from 'bluebird' +import * as Sequelize from 'sequelize' import { database as db } from '../../../initializers/database' import { @@ -27,17 +28,28 @@ import { RemoteQaduVideoRequest, RemoteQaduVideoData, RemoteVideoEventRequest, - RemoteVideoEventData + RemoteVideoEventData, + RemoteVideoChannelCreateData, + RemoteVideoChannelUpdateData, + RemoteVideoChannelRemoveData, + RemoteVideoAuthorRemoveData, + RemoteVideoAuthorCreateData } from '../../../../shared' const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] // Functions to call when processing a remote request +// FIXME: use RemoteVideoRequestType as id type const functionsHash: { [ id: string ]: (...args) => Promise } = {} -functionsHash[ENDPOINT_ACTIONS.ADD] = addRemoteVideoRetryWrapper -functionsHash[ENDPOINT_ACTIONS.UPDATE] = updateRemoteVideoRetryWrapper -functionsHash[ENDPOINT_ACTIONS.REMOVE] = removeRemoteVideo -functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideo +functionsHash[ENDPOINT_ACTIONS.ADD_VIDEO] = addRemoteVideoRetryWrapper +functionsHash[ENDPOINT_ACTIONS.UPDATE_VIDEO] = updateRemoteVideoRetryWrapper +functionsHash[ENDPOINT_ACTIONS.REMOVE_VIDEO] = removeRemoteVideoRetryWrapper +functionsHash[ENDPOINT_ACTIONS.ADD_CHANNEL] = addRemoteVideoChannelRetryWrapper +functionsHash[ENDPOINT_ACTIONS.UPDATE_CHANNEL] = updateRemoteVideoChannelRetryWrapper +functionsHash[ENDPOINT_ACTIONS.REMOVE_CHANNEL] = removeRemoteVideoChannelRetryWrapper +functionsHash[ENDPOINT_ACTIONS.REPORT_ABUSE] = reportAbuseRemoteVideoRetryWrapper +functionsHash[ENDPOINT_ACTIONS.ADD_AUTHOR] = addRemoteVideoAuthorRetryWrapper +functionsHash[ENDPOINT_ACTIONS.REMOVE_AUTHOR] = removeRemoteVideoAuthorRetryWrapper const remoteVideosRouter = express.Router() @@ -133,7 +145,7 @@ function processVideosEventsRetryWrapper (eventData: RemoteVideoEventData, fromP function processVideosEvents (eventData: RemoteVideoEventData, fromPod: PodInstance) { return db.sequelize.transaction(t => { - return fetchVideoByUUID(eventData.uuid) + return fetchVideoByUUID(eventData.uuid, t) .then(videoInstance => { const options = { transaction: t } @@ -196,7 +208,7 @@ function quickAndDirtyUpdateVideo (videoData: RemoteQaduVideoData, fromPod: PodI let videoUUID = '' return db.sequelize.transaction(t => { - return fetchVideoByHostAndUUID(fromPod.host, videoData.uuid) + return fetchVideoByHostAndUUID(fromPod.host, videoData.uuid, t) .then(videoInstance => { const options = { transaction: t } @@ -239,22 +251,16 @@ function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod: PodI .then(video => { if (video) throw new Error('UUID already exists.') - return undefined + return db.VideoChannel.loadByHostAndUUID(fromPod.host, videoToCreateData.channelUUID, t) }) - .then(() => { - const name = videoToCreateData.author - const podId = fromPod.id - // This author is from another pod so we do not associate a user - const userId = null + .then(videoChannel => { + if (!videoChannel) throw new Error('Video channel ' + videoToCreateData.channelUUID + ' not found.') - return db.Author.findOrCreateAuthor(name, podId, userId, t) - }) - .then(author => { const tags = videoToCreateData.tags - return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ author, tagInstances })) + return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ videoChannel, tagInstances })) }) - .then(({ author, tagInstances }) => { + .then(({ videoChannel, tagInstances }) => { const videoData = { name: videoToCreateData.name, uuid: videoToCreateData.uuid, @@ -263,7 +269,7 @@ function addRemoteVideo (videoToCreateData: RemoteVideoCreateData, fromPod: PodI language: videoToCreateData.language, nsfw: videoToCreateData.nsfw, description: videoToCreateData.description, - authorId: author.id, + channelId: videoChannel.id, duration: videoToCreateData.duration, createdAt: videoToCreateData.createdAt, // FIXME: updatedAt does not seems to be considered by Sequelize @@ -336,7 +342,7 @@ function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, from logger.debug('Updating remote video "%s".', videoAttributesToUpdate.uuid) return db.sequelize.transaction(t => { - return fetchVideoByHostAndUUID(fromPod.host, videoAttributesToUpdate.uuid) + return fetchVideoByHostAndUUID(fromPod.host, videoAttributesToUpdate.uuid, t) .then(videoInstance => { const tags = videoAttributesToUpdate.tags @@ -365,7 +371,7 @@ function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, from // Remove old video files videoInstance.VideoFiles.forEach(videoFile => { - tasks.push(videoFile.destroy()) + tasks.push(videoFile.destroy({ transaction: t })) }) return Promise.all(tasks).then(() => ({ tagInstances, videoInstance })) @@ -404,37 +410,231 @@ function updateRemoteVideo (videoAttributesToUpdate: RemoteVideoUpdateData, from }) } +function removeRemoteVideoRetryWrapper (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) { + const options = { + arguments: [ videoToRemoveData, fromPod ], + errorMessage: 'Cannot remove the remote video channel with many retries.' + } + + return retryTransactionWrapper(removeRemoteVideo, options) +} + function removeRemoteVideo (videoToRemoveData: RemoteVideoRemoveData, fromPod: PodInstance) { - // We need the instance because we have to remove some other stuffs (thumbnail etc) - return fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid) - .then(video => { - logger.debug('Removing remote video with uuid %s.', video.uuid) - return video.destroy() - }) + logger.debug('Removing remote video "%s".', videoToRemoveData.uuid) + + return db.sequelize.transaction(t => { + // We need the instance because we have to remove some other stuffs (thumbnail etc) + return fetchVideoByHostAndUUID(fromPod.host, videoToRemoveData.uuid, t) + .then(video => video.destroy({ transaction: t })) + }) + .then(() => logger.info('Remote video with uuid %s removed.', videoToRemoveData.uuid)) + .catch(err => { + logger.debug('Cannot remove the remote video.', err) + throw err + }) +} + +function addRemoteVideoAuthorRetryWrapper (authorToCreateData: RemoteVideoAuthorCreateData, fromPod: PodInstance) { + const options = { + arguments: [ authorToCreateData, fromPod ], + errorMessage: 'Cannot insert the remote video author with many retries.' + } + + return retryTransactionWrapper(addRemoteVideoAuthor, options) +} + +function addRemoteVideoAuthor (authorToCreateData: RemoteVideoAuthorCreateData, fromPod: PodInstance) { + logger.debug('Adding remote video author "%s".', authorToCreateData.uuid) + + return db.sequelize.transaction(t => { + return db.Author.loadAuthorByPodAndUUID(authorToCreateData.uuid, fromPod.id, t) + .then(author => { + if (author) throw new Error('UUID already exists.') + + return undefined + }) + .then(() => { + const videoAuthorData = { + name: authorToCreateData.name, + uuid: authorToCreateData.uuid, + userId: null, // Not on our pod + podId: fromPod.id + } + + const author = db.Author.build(videoAuthorData) + return author.save({ transaction: t }) + }) + }) + .then(() => logger.info('Remote video author with uuid %s inserted.', authorToCreateData.uuid)) .catch(err => { - logger.debug('Could not fetch remote video.', { host: fromPod.host, uuid: videoToRemoveData.uuid, error: err.stack }) + logger.debug('Cannot insert the remote video author.', err) + throw err }) } +function removeRemoteVideoAuthorRetryWrapper (authorAttributesToRemove: RemoteVideoAuthorRemoveData, fromPod: PodInstance) { + const options = { + arguments: [ authorAttributesToRemove, fromPod ], + errorMessage: 'Cannot remove the remote video author with many retries.' + } + + return retryTransactionWrapper(removeRemoteVideoAuthor, options) +} + +function removeRemoteVideoAuthor (authorAttributesToRemove: RemoteVideoAuthorRemoveData, fromPod: PodInstance) { + logger.debug('Removing remote video author "%s".', authorAttributesToRemove.uuid) + + return db.sequelize.transaction(t => { + return db.Author.loadAuthorByPodAndUUID(authorAttributesToRemove.uuid, fromPod.id, t) + .then(videoAuthor => videoAuthor.destroy({ transaction: t })) + }) + .then(() => logger.info('Remote video author with uuid %s removed.', authorAttributesToRemove.uuid)) + .catch(err => { + logger.debug('Cannot remove the remote video author.', err) + throw err + }) +} + +function addRemoteVideoChannelRetryWrapper (videoChannelToCreateData: RemoteVideoChannelCreateData, fromPod: PodInstance) { + const options = { + arguments: [ videoChannelToCreateData, fromPod ], + errorMessage: 'Cannot insert the remote video channel with many retries.' + } + + return retryTransactionWrapper(addRemoteVideoChannel, options) +} + +function addRemoteVideoChannel (videoChannelToCreateData: RemoteVideoChannelCreateData, fromPod: PodInstance) { + logger.debug('Adding remote video channel "%s".', videoChannelToCreateData.uuid) + + return db.sequelize.transaction(t => { + return db.VideoChannel.loadByUUID(videoChannelToCreateData.uuid) + .then(videoChannel => { + if (videoChannel) throw new Error('UUID already exists.') + + return undefined + }) + .then(() => { + const authorUUID = videoChannelToCreateData.ownerUUID + const podId = fromPod.id + + return db.Author.loadAuthorByPodAndUUID(authorUUID, podId, t) + }) + .then(author => { + if (!author) throw new Error('Unknown author UUID.') + + const videoChannelData = { + name: videoChannelToCreateData.name, + description: videoChannelToCreateData.description, + uuid: videoChannelToCreateData.uuid, + createdAt: videoChannelToCreateData.createdAt, + updatedAt: videoChannelToCreateData.updatedAt, + remote: true, + authorId: author.id + } + + const videoChannel = db.VideoChannel.build(videoChannelData) + return videoChannel.save({ transaction: t }) + }) + }) + .then(() => logger.info('Remote video channel with uuid %s inserted.', videoChannelToCreateData.uuid)) + .catch(err => { + logger.debug('Cannot insert the remote video channel.', err) + throw err + }) +} + +function updateRemoteVideoChannelRetryWrapper (videoChannelAttributesToUpdate: RemoteVideoChannelUpdateData, fromPod: PodInstance) { + const options = { + arguments: [ videoChannelAttributesToUpdate, fromPod ], + errorMessage: 'Cannot update the remote video channel with many retries.' + } + + return retryTransactionWrapper(updateRemoteVideoChannel, options) +} + +function updateRemoteVideoChannel (videoChannelAttributesToUpdate: RemoteVideoChannelUpdateData, fromPod: PodInstance) { + logger.debug('Updating remote video channel "%s".', videoChannelAttributesToUpdate.uuid) + + return db.sequelize.transaction(t => { + return fetchVideoChannelByHostAndUUID(fromPod.host, videoChannelAttributesToUpdate.uuid, t) + .then(videoChannelInstance => { + const options = { transaction: t } + + videoChannelInstance.set('name', videoChannelAttributesToUpdate.name) + videoChannelInstance.set('description', videoChannelAttributesToUpdate.description) + videoChannelInstance.set('createdAt', videoChannelAttributesToUpdate.createdAt) + videoChannelInstance.set('updatedAt', videoChannelAttributesToUpdate.updatedAt) + + return videoChannelInstance.save(options) + }) + }) + .then(() => logger.info('Remote video channel with uuid %s updated', videoChannelAttributesToUpdate.uuid)) + .catch(err => { + // This is just a debug because we will retry the insert + logger.debug('Cannot update the remote video channel.', err) + throw err + }) +} + +function removeRemoteVideoChannelRetryWrapper (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) { + const options = { + arguments: [ videoChannelAttributesToRemove, fromPod ], + errorMessage: 'Cannot remove the remote video channel with many retries.' + } + + return retryTransactionWrapper(removeRemoteVideoChannel, options) +} + +function removeRemoteVideoChannel (videoChannelAttributesToRemove: RemoteVideoChannelRemoveData, fromPod: PodInstance) { + logger.debug('Removing remote video channel "%s".', videoChannelAttributesToRemove.uuid) + + return db.sequelize.transaction(t => { + return fetchVideoChannelByHostAndUUID(fromPod.host, videoChannelAttributesToRemove.uuid, t) + .then(videoChannel => videoChannel.destroy({ transaction: t })) + }) + .then(() => logger.info('Remote video channel with uuid %s removed.', videoChannelAttributesToRemove.uuid)) + .catch(err => { + logger.debug('Cannot remove the remote video channel.', err) + throw err + }) +} + +function reportAbuseRemoteVideoRetryWrapper (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) { + const options = { + arguments: [ reportData, fromPod ], + errorMessage: 'Cannot create remote abuse video with many retries.' + } + + return retryTransactionWrapper(reportAbuseRemoteVideo, options) +} + function reportAbuseRemoteVideo (reportData: RemoteVideoReportAbuseData, fromPod: PodInstance) { - return fetchVideoByUUID(reportData.videoUUID) - .then(video => { - logger.debug('Reporting remote abuse for video %s.', video.id) + logger.debug('Reporting remote abuse for video %s.', reportData.videoUUID) - const videoAbuseData = { - reporterUsername: reportData.reporterUsername, - reason: reportData.reportReason, - reporterPodId: fromPod.id, - videoId: video.id - } + return db.sequelize.transaction(t => { + return fetchVideoByUUID(reportData.videoUUID, t) + .then(video => { + const videoAbuseData = { + reporterUsername: reportData.reporterUsername, + reason: reportData.reportReason, + reporterPodId: fromPod.id, + videoId: video.id + } - return db.VideoAbuse.create(videoAbuseData) - }) - .catch(err => logger.error('Cannot create remote abuse video.', err)) + return db.VideoAbuse.create(videoAbuseData) + }) + }) + .then(() => logger.info('Remote abuse for video uuid %s created', reportData.videoUUID)) + .catch(err => { + // This is just a debug because we will retry the insert + logger.debug('Cannot create remote abuse video', err) + throw err + }) } -function fetchVideoByUUID (id: string) { - return db.Video.loadByUUID(id) +function fetchVideoByUUID (id: string, t: Sequelize.Transaction) { + return db.Video.loadByUUID(id, t) .then(video => { if (!video) throw new Error('Video not found') @@ -446,8 +646,8 @@ function fetchVideoByUUID (id: string) { }) } -function fetchVideoByHostAndUUID (podHost: string, uuid: string) { - return db.Video.loadByHostAndUUID(podHost, uuid) +function fetchVideoByHostAndUUID (podHost: string, uuid: string, t: Sequelize.Transaction) { + return db.Video.loadByHostAndUUID(podHost, uuid, t) .then(video => { if (!video) throw new Error('Video not found') @@ -458,3 +658,16 @@ function fetchVideoByHostAndUUID (podHost: string, uuid: string) { throw err }) } + +function fetchVideoChannelByHostAndUUID (podHost: string, uuid: string, t: Sequelize.Transaction) { + return db.VideoChannel.loadByHostAndUUID(podHost, uuid, t) + .then(videoChannel => { + if (!videoChannel) throw new Error('Video channel not found') + + return videoChannel + }) + .catch(err => { + logger.error('Cannot load video channel from host and uuid.', { error: err.stack, podHost, uuid }) + throw err + }) +} diff --git a/server/controllers/api/users.ts b/server/controllers/api/users.ts index 1ecaaf93f..6576e4333 100644 --- a/server/controllers/api/users.ts +++ b/server/controllers/api/users.ts @@ -2,7 +2,7 @@ import * as express from 'express' import { database as db } from '../../initializers/database' import { USER_ROLES, CONFIG } from '../../initializers' -import { logger, getFormattedObjects } from '../../helpers' +import { logger, getFormattedObjects, retryTransactionWrapper } from '../../helpers' import { authenticate, ensureIsAdmin, @@ -26,6 +26,7 @@ import { UserUpdate, UserUpdateMe } from '../../../shared' +import { createUserAuthorAndChannel } from '../../lib' import { UserInstance } from '../../models' const usersRouter = express.Router() @@ -58,7 +59,7 @@ usersRouter.post('/', authenticate, ensureIsAdmin, usersAddValidator, - createUser + createUserRetryWrapper ) usersRouter.post('/register', @@ -98,9 +99,22 @@ export { // --------------------------------------------------------------------------- +function createUserRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { + const options = { + arguments: [ req, res ], + errorMessage: 'Cannot insert the user with many retries.' + } + + retryTransactionWrapper(createUser, options) + .then(() => { + // TODO : include Location of the new user -> 201 + res.type('json').status(204).end() + }) + .catch(err => next(err)) +} + function createUser (req: express.Request, res: express.Response, next: express.NextFunction) { const body: UserCreate = req.body - const user = db.User.build({ username: body.username, password: body.password, @@ -110,9 +124,12 @@ function createUser (req: express.Request, res: express.Response, next: express. videoQuota: body.videoQuota }) - user.save() - .then(() => res.type('json').status(204).end()) - .catch(err => next(err)) + return createUserAuthorAndChannel(user) + .then(() => logger.info('User %s with its channel and author created.', body.username)) + .catch((err: Error) => { + logger.debug('Cannot insert the user.', err) + throw err + }) } function registerUser (req: express.Request, res: express.Response, next: express.NextFunction) { @@ -127,13 +144,13 @@ function registerUser (req: express.Request, res: express.Response, next: expres videoQuota: CONFIG.USER.VIDEO_QUOTA }) - user.save() + return createUserAuthorAndChannel(user) .then(() => res.type('json').status(204).end()) .catch(err => next(err)) } function getUserInformation (req: express.Request, res: express.Response, next: express.NextFunction) { - db.User.loadByUsername(res.locals.oauth.token.user.username) + db.User.loadByUsernameAndPopulateChannels(res.locals.oauth.token.user.username) .then(user => res.json(user.toFormattedJSON())) .catch(err => next(err)) } diff --git a/server/controllers/api/videos/channel.ts b/server/controllers/api/videos/channel.ts new file mode 100644 index 000000000..630fc4f53 --- /dev/null +++ b/server/controllers/api/videos/channel.ts @@ -0,0 +1,196 @@ +import * as express from 'express' + +import { database as db } from '../../../initializers' +import { + logger, + getFormattedObjects, + retryTransactionWrapper +} from '../../../helpers' +import { + authenticate, + paginationValidator, + videoChannelsSortValidator, + videoChannelsAddValidator, + setVideoChannelsSort, + setPagination, + videoChannelsRemoveValidator, + videoChannelGetValidator, + videoChannelsUpdateValidator, + listVideoAuthorChannelsValidator +} from '../../../middlewares' +import { + createVideoChannel, + updateVideoChannelToFriends +} from '../../../lib' +import { VideoChannelInstance, AuthorInstance } from '../../../models' +import { VideoChannelCreate, VideoChannelUpdate } from '../../../../shared' + +const videoChannelRouter = express.Router() + +videoChannelRouter.get('/channels', + paginationValidator, + videoChannelsSortValidator, + setVideoChannelsSort, + setPagination, + listVideoChannels +) + +videoChannelRouter.get('/authors/:authorId/channels', + listVideoAuthorChannelsValidator, + listVideoAuthorChannels +) + +videoChannelRouter.post('/channels', + authenticate, + videoChannelsAddValidator, + addVideoChannelRetryWrapper +) + +videoChannelRouter.put('/channels/:id', + authenticate, + videoChannelsUpdateValidator, + updateVideoChannelRetryWrapper +) + +videoChannelRouter.delete('/channels/:id', + authenticate, + videoChannelsRemoveValidator, + removeVideoChannelRetryWrapper +) + +videoChannelRouter.get('/channels/:id', + videoChannelGetValidator, + getVideoChannel +) + +// --------------------------------------------------------------------------- + +export { + videoChannelRouter +} + +// --------------------------------------------------------------------------- + +function listVideoChannels (req: express.Request, res: express.Response, next: express.NextFunction) { + db.VideoChannel.listForApi(req.query.start, req.query.count, req.query.sort) + .then(result => res.json(getFormattedObjects(result.data, result.total))) + .catch(err => next(err)) +} + +function listVideoAuthorChannels (req: express.Request, res: express.Response, next: express.NextFunction) { + db.VideoChannel.listByAuthor(res.locals.author.id) + .then(result => res.json(getFormattedObjects(result.data, result.total))) + .catch(err => next(err)) +} + +// Wrapper to video channel add that retry the function if there is a database error +// We need this because we run the transaction in SERIALIZABLE isolation that can fail +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.' + } + + retryTransactionWrapper(addVideoChannel, options) + .then(() => { + // TODO : include Location of the new video channel -> 201 + res.type('json').status(204).end() + }) + .catch(err => next(err)) +} + +function addVideoChannel (req: express.Request, res: express.Response) { + const videoChannelInfo: VideoChannelCreate = req.body + const author: AuthorInstance = res.locals.oauth.token.User.Author + + return db.sequelize.transaction(t => { + return createVideoChannel(videoChannelInfo, author, t) + }) + .then(videoChannelUUID => logger.info('Video channel with uuid %s created.', videoChannelUUID)) + .catch((err: Error) => { + logger.debug('Cannot insert the video channel.', err) + throw err + }) +} + +function updateVideoChannelRetryWrapper (req: express.Request, res: express.Response, next: express.NextFunction) { + const options = { + arguments: [ req, res ], + errorMessage: 'Cannot update the video with many retries.' + } + + retryTransactionWrapper(updateVideoChannel, options) + .then(() => res.type('json').status(204).end()) + .catch(err => next(err)) +} + +function updateVideoChannel (req: express.Request, res: express.Response) { + const videoChannelInstance: VideoChannelInstance = res.locals.videoChannel + const videoChannelFieldsSave = videoChannelInstance.toJSON() + const videoChannelInfoToUpdate: VideoChannelUpdate = req.body + + return db.sequelize.transaction(t => { + const options = { + transaction: t + } + + if (videoChannelInfoToUpdate.name !== undefined) videoChannelInstance.set('name', videoChannelInfoToUpdate.name) + if (videoChannelInfoToUpdate.description !== undefined) videoChannelInstance.set('description', videoChannelInfoToUpdate.description) + + return videoChannelInstance.save(options) + .then(() => { + const json = videoChannelInstance.toUpdateRemoteJSON() + + // Now we'll update the video channel's meta data to our friends + return updateVideoChannelToFriends(json, t) + }) + }) + .then(() => { + logger.info('Video channel with name %s and uuid %s updated.', videoChannelInstance.name, videoChannelInstance.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! + Object.keys(videoChannelFieldsSave).forEach(key => { + const value = videoChannelFieldsSave[key] + videoChannelInstance.set(key, value) + }) + + throw err + }) +} + +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.' + } + + retryTransactionWrapper(removeVideoChannel, options) + .then(() => res.type('json').status(204).end()) + .catch(err => next(err)) +} + +function removeVideoChannel (req: express.Request, res: express.Response) { + const videoChannelInstance: VideoChannelInstance = res.locals.videoChannel + + return db.sequelize.transaction(t => { + return videoChannelInstance.destroy({ transaction: t }) + }) + .then(() => { + logger.info('Video channel with name %s and uuid %s deleted.', videoChannelInstance.name, videoChannelInstance.uuid) + }) + .catch(err => { + logger.error('Errors when removed the video channel.', err) + throw err + }) +} + +function getVideoChannel (req: express.Request, res: express.Response, next: express.NextFunction) { + db.VideoChannel.loadAndPopulateAuthorAndVideos(res.locals.videoChannel.id) + .then(videoChannelWithVideos => res.json(videoChannelWithVideos.toFormattedJSON())) + .catch(err => next(err)) +} diff --git a/server/controllers/api/videos/index.ts b/server/controllers/api/videos/index.ts index 2b7ead954..ec855ee8e 100644 --- a/server/controllers/api/videos/index.ts +++ b/server/controllers/api/videos/index.ts @@ -46,6 +46,7 @@ import { VideoCreate, VideoUpdate } from '../../../../shared' import { abuseVideoRouter } from './abuse' import { blacklistRouter } from './blacklist' import { rateVideoRouter } from './rate' +import { videoChannelRouter } from './channel' const videosRouter = express.Router() @@ -76,6 +77,7 @@ const reqFiles = multer({ storage: storage }).fields([{ name: 'videofile', maxCo videosRouter.use('/', abuseVideoRouter) videosRouter.use('/', blacklistRouter) videosRouter.use('/', rateVideoRouter) +videosRouter.use('/', videoChannelRouter) videosRouter.get('/categories', listVideoCategories) videosRouter.get('/licences', listVideoLicences) @@ -161,21 +163,13 @@ function addVideo (req: express.Request, res: express.Response, videoPhysicalFil let videoUUID = '' return db.sequelize.transaction(t => { - const user = res.locals.oauth.token.User + let p: Promise - const name = user.username - // null because it is OUR pod - const podId = null - const userId = user.id + if (!videoInfo.tags) p = Promise.resolve(undefined) + else p = db.Tag.findOrCreateTags(videoInfo.tags, t) - return db.Author.findOrCreateAuthor(name, podId, userId, t) - .then(author => { - const tags = videoInfo.tags - if (!tags) return { author, tagInstances: undefined } - - return db.Tag.findOrCreateTags(tags, t).then(tagInstances => ({ author, tagInstances })) - }) - .then(({ author, tagInstances }) => { + return p + .then(tagInstances => { const videoData = { name: videoInfo.name, remote: false, @@ -186,18 +180,18 @@ function addVideo (req: express.Request, res: express.Response, videoPhysicalFil nsfw: videoInfo.nsfw, description: videoInfo.description, duration: videoPhysicalFile['duration'], // duration was added by a previous middleware - authorId: author.id + channelId: res.locals.videoChannel.id } const video = db.Video.build(videoData) - return { author, tagInstances, video } + return { tagInstances, video } }) - .then(({ author, tagInstances, video }) => { + .then(({ tagInstances, video }) => { const videoFilePath = join(CONFIG.STORAGE.VIDEOS_DIR, videoPhysicalFile.filename) return getVideoFileHeight(videoFilePath) - .then(height => ({ author, tagInstances, video, videoFileHeight: height })) + .then(height => ({ tagInstances, video, videoFileHeight: height })) }) - .then(({ author, tagInstances, video, videoFileHeight }) => { + .then(({ tagInstances, video, videoFileHeight }) => { const videoFileData = { extname: extname(videoPhysicalFile.filename), resolution: videoFileHeight, @@ -205,9 +199,9 @@ function addVideo (req: express.Request, res: express.Response, videoPhysicalFil } const videoFile = db.VideoFile.build(videoFileData) - return { author, tagInstances, video, videoFile } + return { tagInstances, video, videoFile } }) - .then(({ author, tagInstances, video, videoFile }) => { + .then(({ tagInstances, video, videoFile }) => { const videoDir = CONFIG.STORAGE.VIDEOS_DIR const source = join(videoDir, videoPhysicalFile.filename) const destination = join(videoDir, video.getVideoFilename(videoFile)) @@ -216,10 +210,10 @@ function addVideo (req: express.Request, res: express.Response, videoPhysicalFil .then(() => { // This is important in case if there is another attempt in the retry process videoPhysicalFile.filename = video.getVideoFilename(videoFile) - return { author, tagInstances, video, videoFile } + return { tagInstances, video, videoFile } }) }) - .then(({ author, tagInstances, video, videoFile }) => { + .then(({ tagInstances, video, videoFile }) => { const tasks = [] tasks.push( @@ -239,15 +233,15 @@ function addVideo (req: express.Request, res: express.Response, videoPhysicalFil ) } - return Promise.all(tasks).then(() => ({ author, tagInstances, video, videoFile })) + return Promise.all(tasks).then(() => ({ tagInstances, video, videoFile })) }) - .then(({ author, tagInstances, video, videoFile }) => { + .then(({ tagInstances, video, videoFile }) => { const options = { transaction: t } return video.save(options) .then(videoCreated => { - // Do not forget to add Author information to the created video - videoCreated.Author = author + // Do not forget to add video channel information to the created video + videoCreated.VideoChannel = res.locals.videoChannel videoUUID = videoCreated.uuid return { tagInstances, video: videoCreated, videoFile } @@ -392,7 +386,7 @@ function getVideo (req: express.Request, res: express.Response) { } // Do not wait the view system - res.json(videoInstance.toFormattedJSON()) + res.json(videoInstance.toFormattedDetailsJSON()) } function listVideos (req: express.Request, res: express.Response, next: express.NextFunction) { diff --git a/server/controllers/services.ts b/server/controllers/services.ts index 4bbe56a8a..99a33a716 100644 --- a/server/controllers/services.ts +++ b/server/controllers/services.ts @@ -47,7 +47,7 @@ function generateOEmbed (req: express.Request, res: express.Response, next: expr width: embedWidth, height: embedHeight, title: video.name, - author_name: video.Author.name, + author_name: video.VideoChannel.Author.name, provider_name: 'PeerTube', provider_url: webserverUrl } diff --git a/server/helpers/custom-validators/index.ts b/server/helpers/custom-validators/index.ts index 1dcab624a..c79982660 100644 --- a/server/helpers/custom-validators/index.ts +++ b/server/helpers/custom-validators/index.ts @@ -3,4 +3,6 @@ export * from './misc' export * from './pods' export * from './pods' export * from './users' +export * from './video-authors' +export * from './video-channels' export * from './videos' diff --git a/server/helpers/custom-validators/misc.ts b/server/helpers/custom-validators/misc.ts index 60fcdd5bb..160ec91f3 100644 --- a/server/helpers/custom-validators/misc.ts +++ b/server/helpers/custom-validators/misc.ts @@ -1,4 +1,4 @@ -import 'express-validator' +import * as validator from 'validator' function exists (value: any) { return value !== undefined && value !== null @@ -8,9 +8,29 @@ function isArray (value: any) { return Array.isArray(value) } +function isDateValid (value: string) { + return exists(value) && validator.isISO8601(value) +} + +function isIdValid (value: string) { + return exists(value) && validator.isInt('' + value) +} + +function isUUIDValid (value: string) { + return exists(value) && validator.isUUID('' + value, 4) +} + +function isIdOrUUIDValid (value: string) { + return isIdValid(value) || isUUIDValid(value) +} + // --------------------------------------------------------------------------- export { exists, - isArray + isArray, + isIdValid, + isUUIDValid, + isIdOrUUIDValid, + isDateValid } diff --git a/server/helpers/custom-validators/remote/videos.ts b/server/helpers/custom-validators/remote/videos.ts index e261e05a8..057996f1c 100644 --- a/server/helpers/custom-validators/remote/videos.ts +++ b/server/helpers/custom-validators/remote/videos.ts @@ -6,18 +6,15 @@ import { REQUEST_ENDPOINT_ACTIONS, REQUEST_VIDEO_EVENT_TYPES } from '../../../initializers' -import { isArray } from '../misc' +import { isArray, isDateValid, isUUIDValid } from '../misc' import { - isVideoAuthorValid, isVideoThumbnailDataValid, - isVideoUUIDValid, isVideoAbuseReasonValid, isVideoAbuseReporterUsernameValid, isVideoViewsValid, isVideoLikesValid, isVideoDislikesValid, isVideoEventCountValid, - isVideoDateValid, isVideoCategoryValid, isVideoLicenceValid, isVideoLanguageValid, @@ -30,9 +27,22 @@ import { isVideoFileExtnameValid, isVideoFileResolutionValid } from '../videos' +import { isVideoChannelDescriptionValid, isVideoChannelNameValid } from '../video-channels' +import { isVideoAuthorNameValid } from '../video-authors' const ENDPOINT_ACTIONS = REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] +const checkers: { [ id: string ]: (obj: any) => boolean } = {} +checkers[ENDPOINT_ACTIONS.ADD_VIDEO] = checkAddVideo +checkers[ENDPOINT_ACTIONS.UPDATE_VIDEO] = checkUpdateVideo +checkers[ENDPOINT_ACTIONS.REMOVE_VIDEO] = checkRemoveVideo +checkers[ENDPOINT_ACTIONS.REPORT_ABUSE] = checkReportVideo +checkers[ENDPOINT_ACTIONS.ADD_CHANNEL] = checkAddVideoChannel +checkers[ENDPOINT_ACTIONS.UPDATE_CHANNEL] = checkUpdateVideoChannel +checkers[ENDPOINT_ACTIONS.REMOVE_CHANNEL] = checkRemoveVideoChannel +checkers[ENDPOINT_ACTIONS.ADD_AUTHOR] = checkAddAuthor +checkers[ENDPOINT_ACTIONS.REMOVE_AUTHOR] = checkRemoveAuthor + function isEachRemoteRequestVideosValid (requests: any[]) { return isArray(requests) && requests.every(request => { @@ -40,26 +50,11 @@ function isEachRemoteRequestVideosValid (requests: any[]) { if (!video) return false - return ( - isRequestTypeAddValid(request.type) && - isCommonVideoAttributesValid(video) && - isVideoAuthorValid(video.author) && - isVideoThumbnailDataValid(video.thumbnailData) - ) || - ( - isRequestTypeUpdateValid(request.type) && - isCommonVideoAttributesValid(video) - ) || - ( - isRequestTypeRemoveValid(request.type) && - isVideoUUIDValid(video.uuid) - ) || - ( - isRequestTypeReportAbuseValid(request.type) && - isVideoUUIDValid(request.data.videoUUID) && - isVideoAbuseReasonValid(request.data.reportReason) && - isVideoAbuseReporterUsernameValid(request.data.reporterUsername) - ) + const checker = checkers[request.type] + // We don't know the request type + if (checker === undefined) return false + + return checker(video) }) } @@ -71,7 +66,7 @@ function isEachRemoteRequestVideosQaduValid (requests: any[]) { if (!video) return false return ( - isVideoUUIDValid(video.uuid) && + isUUIDValid(video.uuid) && (has(video, 'views') === false || isVideoViewsValid(video.views)) && (has(video, 'likes') === false || isVideoLikesValid(video.likes)) && (has(video, 'dislikes') === false || isVideoDislikesValid(video.dislikes)) @@ -87,7 +82,7 @@ function isEachRemoteRequestVideosEventsValid (requests: any[]) { if (!eventData) return false return ( - isVideoUUIDValid(eventData.uuid) && + isUUIDValid(eventData.uuid) && values(REQUEST_VIDEO_EVENT_TYPES).indexOf(eventData.eventType) !== -1 && isVideoEventCountValid(eventData.count) ) @@ -105,8 +100,8 @@ export { // --------------------------------------------------------------------------- function isCommonVideoAttributesValid (video: any) { - return isVideoDateValid(video.createdAt) && - isVideoDateValid(video.updatedAt) && + return isDateValid(video.createdAt) && + isDateValid(video.updatedAt) && isVideoCategoryValid(video.category) && isVideoLicenceValid(video.licence) && isVideoLanguageValid(video.language) && @@ -115,7 +110,7 @@ function isCommonVideoAttributesValid (video: any) { isVideoDurationValid(video.duration) && isVideoNameValid(video.name) && isVideoTagsValid(video.tags) && - isVideoUUIDValid(video.uuid) && + isUUIDValid(video.uuid) && isVideoViewsValid(video.views) && isVideoLikesValid(video.likes) && isVideoDislikesValid(video.dislikes) && @@ -131,18 +126,53 @@ function isCommonVideoAttributesValid (video: any) { }) } -function isRequestTypeAddValid (value: string) { - return value === ENDPOINT_ACTIONS.ADD +function checkAddVideo (video: any) { + return isCommonVideoAttributesValid(video) && + isUUIDValid(video.channelUUID) && + isVideoThumbnailDataValid(video.thumbnailData) } -function isRequestTypeUpdateValid (value: string) { - return value === ENDPOINT_ACTIONS.UPDATE +function checkUpdateVideo (video: any) { + return isCommonVideoAttributesValid(video) } -function isRequestTypeRemoveValid (value: string) { - return value === ENDPOINT_ACTIONS.REMOVE +function checkRemoveVideo (video: any) { + return isUUIDValid(video.uuid) } -function isRequestTypeReportAbuseValid (value: string) { - return value === ENDPOINT_ACTIONS.REPORT_ABUSE +function checkReportVideo (abuse: any) { + return isUUIDValid(abuse.videoUUID) && + isVideoAbuseReasonValid(abuse.reportReason) && + isVideoAbuseReporterUsernameValid(abuse.reporterUsername) +} + +function checkAddVideoChannel (videoChannel: any) { + return isUUIDValid(videoChannel.uuid) && + isVideoChannelNameValid(videoChannel.name) && + isVideoChannelDescriptionValid(videoChannel.description) && + isDateValid(videoChannel.createdAt) && + isDateValid(videoChannel.updatedAt) && + isUUIDValid(videoChannel.ownerUUID) +} + +function checkUpdateVideoChannel (videoChannel: any) { + return isUUIDValid(videoChannel.uuid) && + isVideoChannelNameValid(videoChannel.name) && + isVideoChannelDescriptionValid(videoChannel.description) && + isDateValid(videoChannel.createdAt) && + isDateValid(videoChannel.updatedAt) && + isUUIDValid(videoChannel.ownerUUID) +} + +function checkRemoveVideoChannel (videoChannel: any) { + return isUUIDValid(videoChannel.uuid) +} + +function checkAddAuthor (author: any) { + return isUUIDValid(author.uuid) && + isVideoAuthorNameValid(author.name) +} + +function checkRemoveAuthor (author: any) { + return isUUIDValid(author.uuid) } diff --git a/server/helpers/custom-validators/video-authors.ts b/server/helpers/custom-validators/video-authors.ts new file mode 100644 index 000000000..48ca9b200 --- /dev/null +++ b/server/helpers/custom-validators/video-authors.ts @@ -0,0 +1,45 @@ +import * as Promise from 'bluebird' +import * as validator from 'validator' +import * as express from 'express' +import 'express-validator' + +import { database as db } from '../../initializers' +import { AuthorInstance } from '../../models' +import { logger } from '../logger' + +import { isUserUsernameValid } from './users' + +function isVideoAuthorNameValid (value: string) { + return isUserUsernameValid(value) +} + +function checkVideoAuthorExists (id: string, res: express.Response, callback: () => void) { + let promise: Promise + if (validator.isInt(id)) { + promise = db.Author.load(+id) + } else { // UUID + promise = db.Author.loadByUUID(id) + } + + promise.then(author => { + if (!author) { + return res.status(404) + .json({ error: 'Video author not found' }) + .end() + } + + res.locals.author = author + callback() + }) + .catch(err => { + logger.error('Error in video author request validator.', err) + return res.sendStatus(500) + }) +} + +// --------------------------------------------------------------------------- + +export { + checkVideoAuthorExists, + isVideoAuthorNameValid +} diff --git a/server/helpers/custom-validators/video-channels.ts b/server/helpers/custom-validators/video-channels.ts new file mode 100644 index 000000000..b6be557e6 --- /dev/null +++ b/server/helpers/custom-validators/video-channels.ts @@ -0,0 +1,57 @@ +import * as Promise from 'bluebird' +import * as validator from 'validator' +import * as express from 'express' +import 'express-validator' +import 'multer' + +import { database as db, CONSTRAINTS_FIELDS } from '../../initializers' +import { VideoChannelInstance } from '../../models' +import { logger } from '../logger' +import { exists } from './misc' + +const VIDEO_CHANNELS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_CHANNELS + +function isVideoChannelDescriptionValid (value: string) { + return value === null || validator.isLength(value, VIDEO_CHANNELS_CONSTRAINTS_FIELDS.DESCRIPTION) +} + +function isVideoChannelNameValid (value: string) { + return exists(value) && validator.isLength(value, VIDEO_CHANNELS_CONSTRAINTS_FIELDS.NAME) +} + +function isVideoChannelUUIDValid (value: string) { + return exists(value) && validator.isUUID('' + value, 4) +} + +function checkVideoChannelExists (id: string, res: express.Response, callback: () => void) { + let promise: Promise + if (validator.isInt(id)) { + promise = db.VideoChannel.loadAndPopulateAuthor(+id) + } else { // UUID + promise = db.VideoChannel.loadByUUIDAndPopulateAuthor(id) + } + + promise.then(videoChannel => { + if (!videoChannel) { + return res.status(404) + .json({ error: 'Video channel not found' }) + .end() + } + + res.locals.videoChannel = videoChannel + callback() + }) + .catch(err => { + logger.error('Error in video channel request validator.', err) + return res.sendStatus(500) + }) +} + +// --------------------------------------------------------------------------- + +export { + isVideoChannelDescriptionValid, + isVideoChannelNameValid, + isVideoChannelUUIDValid, + checkVideoChannelExists +} diff --git a/server/helpers/custom-validators/videos.ts b/server/helpers/custom-validators/videos.ts index 05d1dc607..4e441fe5f 100644 --- a/server/helpers/custom-validators/videos.ts +++ b/server/helpers/custom-validators/videos.ts @@ -23,18 +23,6 @@ const VIDEOS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEOS const VIDEO_ABUSES_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_ABUSES const VIDEO_EVENTS_CONSTRAINTS_FIELDS = CONSTRAINTS_FIELDS.VIDEO_EVENTS -function isVideoIdOrUUIDValid (value: string) { - return validator.isInt(value) || isVideoUUIDValid(value) -} - -function isVideoAuthorValid (value: string) { - return isUserUsernameValid(value) -} - -function isVideoDateValid (value: string) { - return exists(value) && validator.isISO8601(value) -} - function isVideoCategoryValid (value: number) { return VIDEO_CATEGORIES[value] !== undefined } @@ -79,10 +67,6 @@ function isVideoThumbnailDataValid (value: string) { return exists(value) && validator.isByteLength(value, VIDEOS_CONSTRAINTS_FIELDS.THUMBNAIL_DATA) } -function isVideoUUIDValid (value: string) { - return exists(value) && validator.isUUID('' + value, 4) -} - function isVideoAbuseReasonValid (value: string) { return exists(value) && validator.isLength(value, VIDEO_ABUSES_CONSTRAINTS_FIELDS.REASON) } @@ -170,9 +154,6 @@ function checkVideoExists (id: string, res: express.Response, callback: () => vo // --------------------------------------------------------------------------- export { - isVideoIdOrUUIDValid, - isVideoAuthorValid, - isVideoDateValid, isVideoCategoryValid, isVideoLicenceValid, isVideoLanguageValid, @@ -185,7 +166,6 @@ export { isVideoThumbnailValid, isVideoThumbnailDataValid, isVideoFileExtnameValid, - isVideoUUIDValid, isVideoAbuseReasonValid, isVideoAbuseReporterUsernameValid, isVideoFile, diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 132164746..54dce980f 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -10,6 +10,7 @@ import { RequestEndpoint, RequestVideoEventType, RequestVideoQaduType, + RemoteVideoRequestType, JobState } from '../../shared/models' @@ -35,6 +36,7 @@ const SORTABLE_COLUMNS = { PODS: [ 'id', 'host', 'score', 'createdAt' ], USERS: [ 'id', 'username', 'createdAt' ], VIDEO_ABUSES: [ 'id', 'createdAt' ], + VIDEO_CHANNELS: [ 'id', 'name', 'updatedAt', 'createdAt' ], VIDEOS: [ 'name', 'duration', 'createdAt', 'views', 'likes' ], BLACKLISTS: [ 'id', 'name', 'duration', 'views', 'likes', 'dislikes', 'uuid', 'createdAt' ] } @@ -115,6 +117,10 @@ const CONSTRAINTS_FIELDS = { VIDEO_ABUSES: { REASON: { min: 2, max: 300 } // Length }, + VIDEO_CHANNELS: { + NAME: { min: 3, max: 50 }, // Length + DESCRIPTION: { min: 3, max: 250 } // Length + }, VIDEOS: { NAME: { min: 3, max: 50 }, // Length DESCRIPTION: { min: 3, max: 250 }, // Length @@ -232,11 +238,20 @@ const REQUEST_ENDPOINTS: { [ id: string ]: RequestEndpoint } = { VIDEOS: 'videos' } -const REQUEST_ENDPOINT_ACTIONS: { [ id: string ]: any } = {} +const REQUEST_ENDPOINT_ACTIONS: { + [ id: string ]: { + [ id: string ]: RemoteVideoRequestType + } +} = {} REQUEST_ENDPOINT_ACTIONS[REQUEST_ENDPOINTS.VIDEOS] = { - ADD: 'add', - UPDATE: 'update', - REMOVE: 'remove', + ADD_VIDEO: 'add-video', + UPDATE_VIDEO: 'update-video', + REMOVE_VIDEO: 'remove-video', + ADD_CHANNEL: 'add-channel', + UPDATE_CHANNEL: 'update-channel', + REMOVE_CHANNEL: 'remove-channel', + ADD_AUTHOR: 'add-author', + REMOVE_AUTHOR: 'remove-author', REPORT_ABUSE: 'report-abuse' } diff --git a/server/initializers/database.ts b/server/initializers/database.ts index c5a385361..d461cb440 100644 --- a/server/initializers/database.ts +++ b/server/initializers/database.ts @@ -14,6 +14,7 @@ import { VideoTagModel } from './../models/video/video-tag-interface' import { BlacklistedVideoModel } from './../models/video/video-blacklist-interface' import { VideoFileModel } from './../models/video/video-file-interface' import { VideoAbuseModel } from './../models/video/video-abuse-interface' +import { VideoChannelModel } from './../models/video/video-channel-interface' import { UserModel } from './../models/user/user-interface' import { UserVideoRateModel } from './../models/user/user-video-rate-interface' import { TagModel } from './../models/video/tag-interface' @@ -50,6 +51,7 @@ const database: { UserVideoRate?: UserVideoRateModel, User?: UserModel, VideoAbuse?: VideoAbuseModel, + VideoChannel?: VideoChannelModel, VideoFile?: VideoFileModel, BlacklistedVideo?: BlacklistedVideoModel, VideoTag?: VideoTagModel, diff --git a/server/initializers/installer.ts b/server/initializers/installer.ts index 10b74b85f..b997de07f 100644 --- a/server/initializers/installer.ts +++ b/server/initializers/installer.ts @@ -5,6 +5,7 @@ import { database as db } from './database' import { USER_ROLES, CONFIG, LAST_MIGRATION_VERSION, CACHE } from './constants' import { clientsExist, usersExist } from './checker' import { logger, createCertsIfNotExist, mkdirpPromise, rimrafPromise } from '../helpers' +import { createUserAuthorAndChannel } from '../lib' function installApplication () { return db.sequelize.sync() @@ -91,7 +92,7 @@ function createOAuthAdminIfNotExist () { const username = 'root' const role = USER_ROLES.ADMIN const email = CONFIG.ADMIN.EMAIL - const createOptions: { validate?: boolean } = {} + let validatePassword = true let password = '' // Do not generate a random password for tests @@ -103,7 +104,7 @@ function createOAuthAdminIfNotExist () { } // Our password is weak so do not validate it - createOptions.validate = false + validatePassword = false } else { password = passwordGenerator(8, true) } @@ -115,13 +116,15 @@ function createOAuthAdminIfNotExist () { role, videoQuota: -1 } + const user = db.User.build(userData) - return db.User.create(userData, createOptions).then(createdUser => { - logger.info('Username: ' + username) - logger.info('User password: ' + password) + return createUserAuthorAndChannel(user, validatePassword) + .then(({ user }) => { + logger.info('Username: ' + username) + logger.info('User password: ' + password) - logger.info('Creating Application table.') - return db.Application.create({ migrationVersion: LAST_MIGRATION_VERSION }) - }) + logger.info('Creating Application table.') + return db.Application.create({ migrationVersion: LAST_MIGRATION_VERSION }) + }) }) } diff --git a/server/lib/cache/videos-preview-cache.ts b/server/lib/cache/videos-preview-cache.ts index 09e2e9a0d..fecdca6ef 100644 --- a/server/lib/cache/videos-preview-cache.ts +++ b/server/lib/cache/videos-preview-cache.ts @@ -55,7 +55,7 @@ class VideosPreviewCache { } private saveRemotePreviewAndReturnPath (video: VideoInstance) { - const req = fetchRemotePreview(video.Author.Pod, video) + const req = fetchRemotePreview(video) return new Promise((res, rej) => { const path = join(CACHE.DIRECTORIES.PREVIEWS, video.getPreviewName()) diff --git a/server/lib/friends.ts b/server/lib/friends.ts index 65349ef5f..f035b099b 100644 --- a/server/lib/friends.ts +++ b/server/lib/friends.ts @@ -42,7 +42,13 @@ import { RemoteVideoRemoveData, RemoteVideoReportAbuseData, ResultList, - Pod as FormattedPod + RemoteVideoRequestType, + Pod as FormattedPod, + RemoteVideoChannelCreateData, + RemoteVideoChannelUpdateData, + RemoteVideoChannelRemoveData, + RemoteVideoAuthorCreateData, + RemoteVideoAuthorRemoveData } from '../../shared' type QaduParam = { videoId: number, type: RequestVideoQaduType } @@ -62,7 +68,7 @@ function activateSchedulers () { function addVideoToFriends (videoData: RemoteVideoCreateData, transaction: Sequelize.Transaction) { const options = { - type: ENDPOINT_ACTIONS.ADD, + type: ENDPOINT_ACTIONS.ADD_VIDEO, endpoint: REQUEST_ENDPOINTS.VIDEOS, data: videoData, transaction @@ -72,7 +78,7 @@ function addVideoToFriends (videoData: RemoteVideoCreateData, transaction: Seque function updateVideoToFriends (videoData: RemoteVideoUpdateData, transaction: Sequelize.Transaction) { const options = { - type: ENDPOINT_ACTIONS.UPDATE, + type: ENDPOINT_ACTIONS.UPDATE_VIDEO, endpoint: REQUEST_ENDPOINTS.VIDEOS, data: videoData, transaction @@ -82,7 +88,7 @@ function updateVideoToFriends (videoData: RemoteVideoUpdateData, transaction: Se function removeVideoToFriends (videoParams: RemoteVideoRemoveData, transaction: Sequelize.Transaction) { const options = { - type: ENDPOINT_ACTIONS.REMOVE, + type: ENDPOINT_ACTIONS.REMOVE_VIDEO, endpoint: REQUEST_ENDPOINTS.VIDEOS, data: videoParams, transaction @@ -90,12 +96,62 @@ function removeVideoToFriends (videoParams: RemoteVideoRemoveData, transaction: return createRequest(options) } +function addVideoAuthorToFriends (authorData: RemoteVideoAuthorCreateData, transaction: Sequelize.Transaction) { + const options = { + type: ENDPOINT_ACTIONS.ADD_AUTHOR, + endpoint: REQUEST_ENDPOINTS.VIDEOS, + data: authorData, + transaction + } + return createRequest(options) +} + +function removeVideoAuthorToFriends (authorData: RemoteVideoAuthorRemoveData, transaction: Sequelize.Transaction) { + const options = { + type: ENDPOINT_ACTIONS.REMOVE_AUTHOR, + endpoint: REQUEST_ENDPOINTS.VIDEOS, + data: authorData, + transaction + } + return createRequest(options) +} + +function addVideoChannelToFriends (videoChannelData: RemoteVideoChannelCreateData, transaction: Sequelize.Transaction) { + const options = { + type: ENDPOINT_ACTIONS.ADD_CHANNEL, + endpoint: REQUEST_ENDPOINTS.VIDEOS, + data: videoChannelData, + transaction + } + return createRequest(options) +} + +function updateVideoChannelToFriends (videoChannelData: RemoteVideoChannelUpdateData, transaction: Sequelize.Transaction) { + const options = { + type: ENDPOINT_ACTIONS.UPDATE_CHANNEL, + endpoint: REQUEST_ENDPOINTS.VIDEOS, + data: videoChannelData, + transaction + } + return createRequest(options) +} + +function removeVideoChannelToFriends (videoChannelParams: RemoteVideoChannelRemoveData, transaction: Sequelize.Transaction) { + const options = { + type: ENDPOINT_ACTIONS.REMOVE_CHANNEL, + endpoint: REQUEST_ENDPOINTS.VIDEOS, + data: videoChannelParams, + transaction + } + return createRequest(options) +} + function reportAbuseVideoToFriend (reportData: RemoteVideoReportAbuseData, video: VideoInstance, transaction: Sequelize.Transaction) { const options = { type: ENDPOINT_ACTIONS.REPORT_ABUSE, endpoint: REQUEST_ENDPOINTS.VIDEOS, data: reportData, - toIds: [ video.Author.podId ], + toIds: [ video.VideoChannel.Author.podId ], transaction } return createRequest(options) @@ -207,15 +263,66 @@ function quitFriends () { .finally(() => requestScheduler.activate()) } +function sendOwnedDataToPod (podId: number) { + // First send authors + return sendOwnedAuthorsToPod(podId) + .then(() => sendOwnedChannelsToPod(podId)) + .then(() => sendOwnedVideosToPod(podId)) +} + +function sendOwnedChannelsToPod (podId: number) { + return db.VideoChannel.listOwned() + .then(videoChannels => { + const tasks = [] + videoChannels.forEach(videoChannel => { + const remoteVideoChannel = videoChannel.toAddRemoteJSON() + const options = { + type: 'add-channel' as 'add-channel', + endpoint: REQUEST_ENDPOINTS.VIDEOS, + data: remoteVideoChannel, + toIds: [ podId ], + transaction: null + } + + const p = createRequest(options) + tasks.push(p) + }) + + return Promise.all(tasks) + }) +} + +function sendOwnedAuthorsToPod (podId: number) { + return db.Author.listOwned() + .then(authors => { + const tasks = [] + authors.forEach(author => { + const remoteAuthor = author.toAddRemoteJSON() + const options = { + type: 'add-author' as 'add-author', + endpoint: REQUEST_ENDPOINTS.VIDEOS, + data: remoteAuthor, + toIds: [ podId ], + transaction: null + } + + const p = createRequest(options) + tasks.push(p) + }) + + return Promise.all(tasks) + }) +} + function sendOwnedVideosToPod (podId: number) { - db.Video.listOwnedAndPopulateAuthorAndTags() + return db.Video.listOwnedAndPopulateAuthorAndTags() .then(videosList => { const tasks = [] videosList.forEach(video => { const promise = video.toAddRemoteJSON() .then(remoteVideo => { const options = { - type: 'add', + type: 'add-video' as 'add-video', endpoint: REQUEST_ENDPOINTS.VIDEOS, data: remoteVideo, toIds: [ podId ], @@ -236,8 +343,8 @@ function sendOwnedVideosToPod (podId: number) { }) } -function fetchRemotePreview (pod: PodInstance, video: VideoInstance) { - const host = video.Author.Pod.host +function fetchRemotePreview (video: VideoInstance) { + const host = video.VideoChannel.Author.Pod.host const path = join(STATIC_PATHS.PREVIEWS, video.getPreviewName()) return request.get(REMOTE_SCHEME.HTTP + '://' + host + path) @@ -274,7 +381,9 @@ function getRequestVideoEventScheduler () { export { activateSchedulers, addVideoToFriends, + removeVideoAuthorToFriends, updateVideoToFriends, + addVideoAuthorToFriends, reportAbuseVideoToFriend, quickAndDirtyUpdateVideoToFriends, quickAndDirtyUpdatesVideoToFriends, @@ -285,11 +394,14 @@ export { quitFriends, removeFriend, removeVideoToFriends, - sendOwnedVideosToPod, + sendOwnedDataToPod, getRequestScheduler, getRequestVideoQaduScheduler, getRequestVideoEventScheduler, - fetchRemotePreview + fetchRemotePreview, + addVideoChannelToFriends, + updateVideoChannelToFriends, + removeVideoChannelToFriends } // --------------------------------------------------------------------------- @@ -373,7 +485,7 @@ function makeRequestsToWinningPods (cert: string, podsList: PodInstance[]) { .then(podCreated => { // Add our videos to the request scheduler - sendOwnedVideosToPod(podCreated.id) + sendOwnedDataToPod(podCreated.id) }) .catch(err => { logger.error('Cannot add friend %s pod.', pod.host, err) @@ -397,7 +509,7 @@ function makeRequestsToWinningPods (cert: string, podsList: PodInstance[]) { // Wrapper that populate "toIds" argument with all our friends if it is not specified type CreateRequestOptions = { - type: string + type: RemoteVideoRequestType endpoint: RequestEndpoint data: Object toIds?: number[] diff --git a/server/lib/index.ts b/server/lib/index.ts index 8628da4dd..d1534b085 100644 --- a/server/lib/index.ts +++ b/server/lib/index.ts @@ -3,3 +3,5 @@ export * from './jobs' export * from './request' export * from './friends' export * from './oauth-model' +export * from './user' +export * from './video-channel' diff --git a/server/lib/user.ts b/server/lib/user.ts new file mode 100644 index 000000000..8609e72d8 --- /dev/null +++ b/server/lib/user.ts @@ -0,0 +1,46 @@ +import { database as db } from '../initializers' +import { UserInstance } from '../models' +import { addVideoAuthorToFriends } from './friends' +import { createVideoChannel } from './video-channel' + +function createUserAuthorAndChannel (user: UserInstance, validateUser = true) { + return db.sequelize.transaction(t => { + const userOptions = { + transaction: t, + validate: validateUser + } + + return user.save(userOptions) + .then(user => { + const author = db.Author.build({ + name: user.username, + podId: null, // It is our pod + userId: user.id + }) + + return author.save({ transaction: t }) + .then(author => ({ author, user })) + }) + .then(({ author, user }) => { + const remoteVideoAuthor = author.toAddRemoteJSON() + + // Now we'll add the video channel's meta data to our friends + return addVideoAuthorToFriends(remoteVideoAuthor, t) + .then(() => ({ author, user })) + }) + .then(({ author, user }) => { + const videoChannelInfo = { + name: `Default ${user.username} channel` + } + + return createVideoChannel(videoChannelInfo, author, t) + .then(videoChannel => ({ author, user, videoChannel })) + }) + }) +} + +// --------------------------------------------------------------------------- + +export { + createUserAuthorAndChannel +} diff --git a/server/lib/video-channel.ts b/server/lib/video-channel.ts new file mode 100644 index 000000000..224179973 --- /dev/null +++ b/server/lib/video-channel.ts @@ -0,0 +1,42 @@ +import * as Sequelize from 'sequelize' + +import { addVideoChannelToFriends } from './friends' +import { database as db } from '../initializers' +import { AuthorInstance } from '../models' +import { VideoChannelCreate } from '../../shared/models' + +function createVideoChannel (videoChannelInfo: VideoChannelCreate, author: AuthorInstance, t: Sequelize.Transaction) { + let videoChannelUUID = '' + + const videoChannelData = { + name: videoChannelInfo.name, + description: videoChannelInfo.description, + remote: false, + authorId: author.id + } + + const videoChannel = db.VideoChannel.build(videoChannelData) + const options = { transaction: t } + + return videoChannel.save(options) + .then(videoChannelCreated => { + // Do not forget to add Author information to the created video channel + videoChannelCreated.Author = author + videoChannelUUID = videoChannelCreated.uuid + + return videoChannelCreated + }) + .then(videoChannel => { + const remoteVideoChannel = videoChannel.toAddRemoteJSON() + + // Now we'll add the video channel's meta data to our friends + return addVideoChannelToFriends(remoteVideoChannel, t) + }) + .then(() => videoChannelUUID) // Return video channel UUID +} + +// --------------------------------------------------------------------------- + +export { + createVideoChannel +} diff --git a/server/middlewares/sort.ts b/server/middlewares/sort.ts index 2c70ff5f0..91aa3e5b6 100644 --- a/server/middlewares/sort.ts +++ b/server/middlewares/sort.ts @@ -22,6 +22,12 @@ function setVideoAbusesSort (req: express.Request, res: express.Response, next: return next() } +function setVideoChannelsSort (req: express.Request, res: express.Response, next: express.NextFunction) { + if (!req.query.sort) req.query.sort = '-createdAt' + + return next() +} + function setVideosSort (req: express.Request, res: express.Response, next: express.NextFunction) { if (!req.query.sort) req.query.sort = '-createdAt' @@ -55,6 +61,7 @@ export { setPodsSort, setUsersSort, setVideoAbusesSort, + setVideoChannelsSort, setVideosSort, setBlacklistSort } diff --git a/server/middlewares/validators/index.ts b/server/middlewares/validators/index.ts index 068c41b24..247f6039e 100644 --- a/server/middlewares/validators/index.ts +++ b/server/middlewares/validators/index.ts @@ -6,3 +6,4 @@ export * from './sort' export * from './users' export * from './videos' export * from './video-blacklist' +export * from './video-channels' diff --git a/server/middlewares/validators/oembed.ts b/server/middlewares/validators/oembed.ts index 4b8c03faf..f8e34d2d4 100644 --- a/server/middlewares/validators/oembed.ts +++ b/server/middlewares/validators/oembed.ts @@ -4,9 +4,12 @@ import { join } from 'path' import { checkErrors } from './utils' import { CONFIG } from '../../initializers' -import { logger } from '../../helpers' -import { checkVideoExists, isVideoIdOrUUIDValid } from '../../helpers/custom-validators/videos' -import { isTestInstance } from '../../helpers/core-utils' +import { + logger, + isTestInstance, + checkVideoExists, + isIdOrUUIDValid +} from '../../helpers' const urlShouldStartWith = CONFIG.WEBSERVER.SCHEME + '://' + join(CONFIG.WEBSERVER.HOST, 'videos', 'watch') + '/' const videoWatchRegex = new RegExp('([^/]+)$') @@ -45,7 +48,7 @@ const oembedValidator = [ } const videoId = matches[1] - if (isVideoIdOrUUIDValid(videoId) === false) { + if (isIdOrUUIDValid(videoId) === false) { return res.status(400) .json({ error: 'Invalid video id.' }) .end() diff --git a/server/middlewares/validators/sort.ts b/server/middlewares/validators/sort.ts index 227f309ad..d23a95537 100644 --- a/server/middlewares/validators/sort.ts +++ b/server/middlewares/validators/sort.ts @@ -11,12 +11,14 @@ const SORTABLE_USERS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.USERS) const SORTABLE_VIDEO_ABUSES_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_ABUSES) const SORTABLE_VIDEOS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEOS) const SORTABLE_BLACKLISTS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.BLACKLISTS) +const SORTABLE_VIDEO_CHANNELS_COLUMNS = createSortableColumns(SORTABLE_COLUMNS.VIDEO_CHANNELS) const podsSortValidator = checkSort(SORTABLE_PODS_COLUMNS) const usersSortValidator = checkSort(SORTABLE_USERS_COLUMNS) const videoAbusesSortValidator = checkSort(SORTABLE_VIDEO_ABUSES_COLUMNS) const videosSortValidator = checkSort(SORTABLE_VIDEOS_COLUMNS) const blacklistSortValidator = checkSort(SORTABLE_BLACKLISTS_COLUMNS) +const videoChannelsSortValidator = checkSort(SORTABLE_VIDEO_CHANNELS_COLUMNS) // --------------------------------------------------------------------------- @@ -24,6 +26,7 @@ export { podsSortValidator, usersSortValidator, videoAbusesSortValidator, + videoChannelsSortValidator, videosSortValidator, blacklistSortValidator } diff --git a/server/middlewares/validators/users.ts b/server/middlewares/validators/users.ts index ab9d0938c..1a33cfd8c 100644 --- a/server/middlewares/validators/users.ts +++ b/server/middlewares/validators/users.ts @@ -13,7 +13,7 @@ import { isUserPasswordValid, isUserVideoQuotaValid, isUserDisplayNSFWValid, - isVideoIdOrUUIDValid + isIdOrUUIDValid } from '../../helpers' import { UserInstance, VideoInstance } from '../../models' @@ -109,7 +109,7 @@ const usersGetValidator = [ ] const usersVideoRatingValidator = [ - param('videoId').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid video id'), (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking usersVideoRating parameters', { parameters: req.params }) diff --git a/server/middlewares/validators/video-blacklist.ts b/server/middlewares/validators/video-blacklist.ts index 30c6d4bd9..3c8c31519 100644 --- a/server/middlewares/validators/video-blacklist.ts +++ b/server/middlewares/validators/video-blacklist.ts @@ -3,10 +3,10 @@ import * as express from 'express' import { database as db } from '../../initializers/database' import { checkErrors } from './utils' -import { logger, isVideoIdOrUUIDValid, checkVideoExists } from '../../helpers' +import { logger, isIdOrUUIDValid, checkVideoExists } from '../../helpers' const videosBlacklistRemoveValidator = [ - param('videoId').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking blacklistRemove parameters.', { parameters: req.params }) @@ -20,7 +20,7 @@ const videosBlacklistRemoveValidator = [ ] const videosBlacklistAddValidator = [ - param('videoId').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), + param('videoId').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid videoId'), (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking videosBlacklist parameters', { parameters: req.params }) diff --git a/server/middlewares/validators/video-channels.ts b/server/middlewares/validators/video-channels.ts new file mode 100644 index 000000000..979fbd34a --- /dev/null +++ b/server/middlewares/validators/video-channels.ts @@ -0,0 +1,142 @@ +import { body, param } from 'express-validator/check' +import * as express from 'express' + +import { checkErrors } from './utils' +import { database as db } from '../../initializers' +import { + logger, + isIdOrUUIDValid, + isVideoChannelDescriptionValid, + isVideoChannelNameValid, + checkVideoChannelExists, + checkVideoAuthorExists +} from '../../helpers' + +const listVideoAuthorChannelsValidator = [ + param('authorId').custom(isIdOrUUIDValid).withMessage('Should have a valid author id'), + + (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking listVideoAuthorChannelsValidator parameters', { parameters: req.body }) + + checkErrors(req, res, () => { + checkVideoAuthorExists(req.params.authorId, res, next) + }) + } +] + +const videoChannelsAddValidator = [ + body('name').custom(isVideoChannelNameValid).withMessage('Should have a valid name'), + body('description').custom(isVideoChannelDescriptionValid).withMessage('Should have a valid description'), + + (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoChannelsAdd parameters', { parameters: req.body }) + + checkErrors(req, res, next) + } +] + +const videoChannelsUpdateValidator = [ + param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + body('name').optional().custom(isVideoChannelNameValid).withMessage('Should have a valid name'), + body('description').optional().custom(isVideoChannelDescriptionValid).withMessage('Should have a valid description'), + + (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoChannelsUpdate parameters', { parameters: req.body }) + + checkErrors(req, res, () => { + checkVideoChannelExists(req.params.id, res, () => { + // We need to make additional checks + if (res.locals.videoChannel.isOwned() === false) { + return res.status(403) + .json({ error: 'Cannot update video channel of another pod' }) + .end() + } + + if (res.locals.videoChannel.Author.userId !== res.locals.oauth.token.User.id) { + return res.status(403) + .json({ error: 'Cannot update video channel of another user' }) + .end() + } + + next() + }) + }) + } +] + +const videoChannelsRemoveValidator = [ + param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + + (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoChannelsRemove parameters', { parameters: req.params }) + + checkErrors(req, res, () => { + checkVideoChannelExists(req.params.id, res, () => { + // Check if the user who did the request is able to delete the video + checkUserCanDeleteVideoChannel(res, () => { + checkVideoChannelIsNotTheLastOne(res, next) + }) + }) + }) + } +] + +const videoChannelGetValidator = [ + param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + + (req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.debug('Checking videoChannelsGet parameters', { parameters: req.params }) + + checkErrors(req, res, () => { + checkVideoChannelExists(req.params.id, res, next) + }) + } +] + +// --------------------------------------------------------------------------- + +export { + listVideoAuthorChannelsValidator, + videoChannelsAddValidator, + videoChannelsUpdateValidator, + videoChannelsRemoveValidator, + videoChannelGetValidator +} + +// --------------------------------------------------------------------------- + +function checkUserCanDeleteVideoChannel (res: express.Response, callback: () => void) { + const user = res.locals.oauth.token.User + + // Retrieve the user who did the request + if (res.locals.videoChannel.isOwned() === false) { + return res.status(403) + .json({ error: 'Cannot remove video channel of another pod.' }) + .end() + } + + // Check if the user can delete the video channel + // The user can delete it if s/he is an admin + // Or if s/he is the video channel's author + if (user.isAdmin() === false && res.locals.videoChannel.Author.userId !== user.id) { + return res.status(403) + .json({ error: 'Cannot remove video channel of another user' }) + .end() + } + + // If we reach this comment, we can delete the video + callback() +} + +function checkVideoChannelIsNotTheLastOne (res: express.Response, callback: () => void) { + db.VideoChannel.countByAuthor(res.locals.oauth.token.User.Author.id) + .then(count => { + if (count <= 1) { + return res.status(409) + .json({ error: 'Cannot remove the last channel of this user' }) + .end() + } + + callback() + }) +} diff --git a/server/middlewares/validators/videos.ts b/server/middlewares/validators/videos.ts index 3f881e1b5..8a9b383b8 100644 --- a/server/middlewares/validators/videos.ts +++ b/server/middlewares/validators/videos.ts @@ -15,11 +15,12 @@ import { isVideoLanguageValid, isVideoTagsValid, isVideoNSFWValid, - isVideoIdOrUUIDValid, + isIdOrUUIDValid, isVideoAbuseReasonValid, isVideoRatingTypeValid, getDurationFromVideoFile, - checkVideoExists + checkVideoExists, + isIdValid } from '../../helpers' const videosAddValidator = [ @@ -33,6 +34,7 @@ const videosAddValidator = [ body('language').optional().custom(isVideoLanguageValid).withMessage('Should have a valid language'), body('nsfw').custom(isVideoNSFWValid).withMessage('Should have a valid NSFW attribute'), body('description').custom(isVideoDescriptionValid).withMessage('Should have a valid description'), + body('channelId').custom(isIdValid).withMessage('Should have correct video channel id'), body('tags').optional().custom(isVideoTagsValid).withMessage('Should have correct tags'), (req: express.Request, res: express.Response, next: express.NextFunction) => { @@ -42,7 +44,20 @@ const videosAddValidator = [ const videoFile: Express.Multer.File = req.files['videofile'][0] const user = res.locals.oauth.token.User - user.isAbleToUploadVideo(videoFile) + return db.VideoChannel.loadByIdAndAuthor(req.body.channelId, user.Author.id) + .then(videoChannel => { + if (!videoChannel) { + res.status(400) + .json({ error: 'Unknown video video channel for this author.' }) + .end() + + return undefined + } + + res.locals.videoChannel = videoChannel + + return user.isAbleToUploadVideo(videoFile) + }) .then(isAble => { if (isAble === false) { res.status(403) @@ -88,7 +103,7 @@ const videosAddValidator = [ ] const videosUpdateValidator = [ - param('id').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), body('name').optional().custom(isVideoNameValid).withMessage('Should have a valid name'), body('category').optional().custom(isVideoCategoryValid).withMessage('Should have a valid category'), body('licence').optional().custom(isVideoLicenceValid).withMessage('Should have a valid licence'), @@ -109,7 +124,7 @@ const videosUpdateValidator = [ .end() } - if (res.locals.video.Author.userId !== res.locals.oauth.token.User.id) { + if (res.locals.video.VideoChannel.Author.userId !== res.locals.oauth.token.User.id) { return res.status(403) .json({ error: 'Cannot update video of another user' }) .end() @@ -122,7 +137,7 @@ const videosUpdateValidator = [ ] const videosGetValidator = [ - param('id').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking videosGet parameters', { parameters: req.params }) @@ -134,7 +149,7 @@ const videosGetValidator = [ ] const videosRemoveValidator = [ - param('id').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking videosRemove parameters', { parameters: req.params }) @@ -162,7 +177,7 @@ const videosSearchValidator = [ ] const videoAbuseReportValidator = [ - param('id').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), body('reason').custom(isVideoAbuseReasonValid).withMessage('Should have a valid reason'), (req: express.Request, res: express.Response, next: express.NextFunction) => { @@ -175,7 +190,7 @@ const videoAbuseReportValidator = [ ] const videoRateValidator = [ - param('id').custom(isVideoIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), + param('id').custom(isIdOrUUIDValid).not().isEmpty().withMessage('Should have a valid id'), body('rating').custom(isVideoRatingTypeValid).withMessage('Should have a valid rate type'), (req: express.Request, res: express.Response, next: express.NextFunction) => { diff --git a/server/models/oauth/oauth-token.ts b/server/models/oauth/oauth-token.ts index e3de9468e..dc8bcd872 100644 --- a/server/models/oauth/oauth-token.ts +++ b/server/models/oauth/oauth-token.ts @@ -126,7 +126,17 @@ getByTokenAndPopulateUser = function (bearerToken: string) { where: { accessToken: bearerToken }, - include: [ OAuthToken['sequelize'].models.User ] + include: [ + { + model: OAuthToken['sequelize'].models.User, + include: [ + { + model: OAuthToken['sequelize'].models.Author, + required: true + } + ] + } + ] } return OAuthToken.findOne(query).then(token => { @@ -141,7 +151,17 @@ getByRefreshTokenAndPopulateUser = function (refreshToken: string) { where: { refreshToken: refreshToken }, - include: [ OAuthToken['sequelize'].models.User ] + include: [ + { + model: OAuthToken['sequelize'].models.User, + include: [ + { + model: OAuthToken['sequelize'].models.Author, + required: true + } + ] + } + ] } return OAuthToken.findOne(query).then(token => { diff --git a/server/models/request/request-video-event.ts b/server/models/request/request-video-event.ts index 4862a5745..34d5c7162 100644 --- a/server/models/request/request-video-event.ts +++ b/server/models/request/request-video-event.ts @@ -85,7 +85,8 @@ listWithLimitAndRandom = function (limitPods: number, limitRequestsPerPod: numbe const Pod = db.Pod // We make a join between videos and authors to find the podId of our video event requests - const podJoins = 'INNER JOIN "Videos" ON "Videos"."authorId" = "Authors"."id" ' + + const podJoins = 'INNER JOIN "VideoChannels" ON "VideoChannels"."authorId" = "Authors"."id" ' + + 'INNER JOIN "Videos" ON "Videos"."channelId" = "VideoChannels"."id" ' + 'INNER JOIN "RequestVideoEvents" ON "RequestVideoEvents"."videoId" = "Videos"."id"' return Pod.listRandomPodIdsWithRequest(limitPods, 'Authors', podJoins).then(podIds => { @@ -161,7 +162,7 @@ function groupAndTruncateRequests (events: RequestVideoEventInstance[], limitReq const eventsGrouped: RequestsVideoEventGrouped = {} events.forEach(event => { - const pod = event.Video.Author.Pod + const pod = event.Video.VideoChannel.Author.Pod if (!eventsGrouped[pod.id]) eventsGrouped[pod.id] = [] diff --git a/server/models/user/user-interface.ts b/server/models/user/user-interface.ts index 8974a9a97..1b5233eaf 100644 --- a/server/models/user/user-interface.ts +++ b/server/models/user/user-interface.ts @@ -5,6 +5,7 @@ import * as Promise from 'bluebird' import { User as FormattedUser } from '../../../shared/models/users/user.model' import { UserRole } from '../../../shared/models/users/user-role.type' import { ResultList } from '../../../shared/models/result-list.model' +import { AuthorInstance } from '../video/author-interface' export namespace UserMethods { export type IsPasswordMatch = (this: UserInstance, password: string) => Promise @@ -17,13 +18,12 @@ export namespace UserMethods { export type GetByUsername = (username: string) => Promise - export type List = () => Promise - export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList > export type LoadById = (id: number) => Promise export type LoadByUsername = (username: string) => Promise + export type LoadByUsernameAndPopulateChannels = (username: string) => Promise export type LoadByUsernameOrEmail = (username: string, email: string) => Promise } @@ -36,10 +36,10 @@ export interface UserClass { countTotal: UserMethods.CountTotal, getByUsername: UserMethods.GetByUsername, - list: UserMethods.List, listForApi: UserMethods.ListForApi, loadById: UserMethods.LoadById, loadByUsername: UserMethods.LoadByUsername, + loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels, loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail } @@ -51,6 +51,8 @@ export interface UserAttributes { displayNSFW?: boolean role: UserRole videoQuota: number + + Author?: AuthorInstance } export interface UserInstance extends UserClass, UserAttributes, Sequelize.Instance { diff --git a/server/models/user/user.ts b/server/models/user/user.ts index 0dc52d3cf..f8598c40f 100644 --- a/server/models/user/user.ts +++ b/server/models/user/user.ts @@ -27,10 +27,10 @@ let toFormattedJSON: UserMethods.ToFormattedJSON let isAdmin: UserMethods.IsAdmin let countTotal: UserMethods.CountTotal let getByUsername: UserMethods.GetByUsername -let list: UserMethods.List let listForApi: UserMethods.ListForApi let loadById: UserMethods.LoadById let loadByUsername: UserMethods.LoadByUsername +let loadByUsernameAndPopulateChannels: UserMethods.LoadByUsernameAndPopulateChannels let loadByUsernameOrEmail: UserMethods.LoadByUsernameOrEmail let isAbleToUploadVideo: UserMethods.IsAbleToUploadVideo @@ -113,10 +113,10 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da countTotal, getByUsername, - list, listForApi, loadById, loadByUsername, + loadByUsernameAndPopulateChannels, loadByUsernameOrEmail ] const instanceMethods = [ @@ -144,15 +144,34 @@ isPasswordMatch = function (this: UserInstance, password: string) { } toFormattedJSON = function (this: UserInstance) { - return { + const json = { id: this.id, username: this.username, email: this.email, displayNSFW: this.displayNSFW, role: this.role, videoQuota: this.videoQuota, - createdAt: this.createdAt + createdAt: this.createdAt, + author: { + id: this.Author.id, + uuid: this.Author.uuid + } } + + if (Array.isArray(this.Author.VideoChannels) === true) { + const videoChannels = this.Author.VideoChannels + .map(c => c.toFormattedJSON()) + .sort((v1, v2) => { + if (v1.createdAt < v2.createdAt) return -1 + if (v1.createdAt === v2.createdAt) return 0 + + return 1 + }) + + json['videoChannels'] = videoChannels + } + + return json } isAdmin = function (this: UserInstance) { @@ -189,21 +208,19 @@ getByUsername = function (username: string) { const query = { where: { username: username - } + }, + include: [ { model: User['sequelize'].models.Author, required: true } ] } return User.findOne(query) } -list = function () { - return User.findAll() -} - listForApi = function (start: number, count: number, sort: string) { const query = { offset: start, limit: count, - order: [ getSort(sort) ] + order: [ getSort(sort) ], + include: [ { model: User['sequelize'].models.Author, required: true } ] } return User.findAndCountAll(query).then(({ rows, count }) => { @@ -215,14 +232,36 @@ listForApi = function (start: number, count: number, sort: string) { } loadById = function (id: number) { - return User.findById(id) + const options = { + include: [ { model: User['sequelize'].models.Author, required: true } ] + } + + return User.findById(id, options) } loadByUsername = function (username: string) { const query = { where: { username - } + }, + include: [ { model: User['sequelize'].models.Author, required: true } ] + } + + return User.findOne(query) +} + +loadByUsernameAndPopulateChannels = function (username: string) { + const query = { + where: { + username + }, + include: [ + { + model: User['sequelize'].models.Author, + required: true, + include: [ User['sequelize'].models.VideoChannel ] + } + ] } return User.findOne(query) @@ -230,6 +269,7 @@ loadByUsername = function (username: string) { loadByUsernameOrEmail = function (username: string, email: string) { const query = { + include: [ { model: User['sequelize'].models.Author, required: true } ], where: { $or: [ { username }, { email } ] } @@ -242,11 +282,12 @@ loadByUsernameOrEmail = function (username: string, email: string) { // --------------------------------------------------------------------------- function getOriginalVideoFileTotalFromUser (user: UserInstance) { - // Don't use sequelize because we need to use a subquery + // Don't use sequelize because we need to use a sub query const query = 'SELECT SUM("size") AS "total" FROM ' + '(SELECT MAX("VideoFiles"."size") AS "size" FROM "VideoFiles" ' + 'INNER JOIN "Videos" ON "VideoFiles"."videoId" = "Videos"."id" ' + - 'INNER JOIN "Authors" ON "Videos"."authorId" = "Authors"."id" ' + + 'INNER JOIN "VideoChannels" ON "VideoChannels"."id" = "Videos"."channelId" ' + + 'INNER JOIN "Authors" ON "VideoChannels"."authorId" = "Authors"."id" ' + 'INNER JOIN "Users" ON "Authors"."userId" = "Users"."id" ' + 'WHERE "Users"."id" = $userId GROUP BY "Videos"."id") t' diff --git a/server/models/video/author-interface.ts b/server/models/video/author-interface.ts index 52a00a1d3..fc69ff3c2 100644 --- a/server/models/video/author-interface.ts +++ b/server/models/video/author-interface.ts @@ -2,31 +2,44 @@ import * as Sequelize from 'sequelize' import * as Promise from 'bluebird' import { PodInstance } from '../pod/pod-interface' +import { RemoteVideoAuthorCreateData } from '../../../shared/models/pods/remote-video/remote-video-author-create-request.model' +import { VideoChannelInstance } from './video-channel-interface' export namespace AuthorMethods { - export type FindOrCreateAuthor = ( - name: string, - podId: number, - userId: number, - transaction: Sequelize.Transaction - ) => Promise + export type Load = (id: number) => Promise + export type LoadByUUID = (uuid: string) => Promise + export type LoadAuthorByPodAndUUID = (uuid: string, podId: number, transaction: Sequelize.Transaction) => Promise + export type ListOwned = () => Promise + + export type ToAddRemoteJSON = (this: AuthorInstance) => RemoteVideoAuthorCreateData + export type IsOwned = (this: AuthorInstance) => boolean } export interface AuthorClass { - findOrCreateAuthor: AuthorMethods.FindOrCreateAuthor + loadAuthorByPodAndUUID: AuthorMethods.LoadAuthorByPodAndUUID + load: AuthorMethods.Load + loadByUUID: AuthorMethods.LoadByUUID + listOwned: AuthorMethods.ListOwned } export interface AuthorAttributes { name: string + uuid?: string + + podId?: number + userId?: number } export interface AuthorInstance extends AuthorClass, AuthorAttributes, Sequelize.Instance { + isOwned: AuthorMethods.IsOwned + toAddRemoteJSON: AuthorMethods.ToAddRemoteJSON + id: number createdAt: Date updatedAt: Date - podId: number Pod: PodInstance + VideoChannels: VideoChannelInstance[] } export interface AuthorModel extends AuthorClass, Sequelize.Model {} diff --git a/server/models/video/author.ts b/server/models/video/author.ts index fd0f44f6b..6f27ea7bd 100644 --- a/server/models/video/author.ts +++ b/server/models/video/author.ts @@ -1,6 +1,7 @@ import * as Sequelize from 'sequelize' import { isUserUsernameValid } from '../../helpers' +import { removeVideoAuthorToFriends } from '../../lib' import { addMethodsToModel } from '../utils' import { @@ -11,11 +12,24 @@ import { } from './author-interface' let Author: Sequelize.Model -let findOrCreateAuthor: AuthorMethods.FindOrCreateAuthor +let loadAuthorByPodAndUUID: AuthorMethods.LoadAuthorByPodAndUUID +let load: AuthorMethods.Load +let loadByUUID: AuthorMethods.LoadByUUID +let listOwned: AuthorMethods.ListOwned +let isOwned: AuthorMethods.IsOwned +let toAddRemoteJSON: AuthorMethods.ToAddRemoteJSON export default function defineAuthor (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { Author = sequelize.define('Author', { + uuid: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + allowNull: false, + validate: { + isUUID: 4 + } + }, name: { type: DataTypes.STRING, allowNull: false, @@ -43,12 +57,23 @@ export default function defineAuthor (sequelize: Sequelize.Sequelize, DataTypes: fields: [ 'name', 'podId' ], unique: true } - ] + ], + hooks: { afterDestroy } } ) - const classMethods = [ associate, findOrCreateAuthor ] - addMethodsToModel(Author, classMethods) + const classMethods = [ + associate, + loadAuthorByPodAndUUID, + load, + loadByUUID, + listOwned + ] + const instanceMethods = [ + isOwned, + toAddRemoteJSON + ] + addMethodsToModel(Author, classMethods, instanceMethods) return Author } @@ -72,27 +97,75 @@ function associate (models) { onDelete: 'cascade' }) - Author.hasMany(models.Video, { + Author.hasMany(models.VideoChannel, { foreignKey: { name: 'authorId', allowNull: false }, - onDelete: 'cascade' + onDelete: 'cascade', + hooks: true }) } -findOrCreateAuthor = function (name: string, podId: number, userId: number, transaction: Sequelize.Transaction) { - const author = { - name, - podId, - userId +function afterDestroy (author: AuthorInstance, options: { transaction: Sequelize.Transaction }) { + if (author.isOwned()) { + const removeVideoAuthorToFriendsParams = { + uuid: author.uuid + } + + return removeVideoAuthorToFriends(removeVideoAuthorToFriendsParams, options.transaction) } - const query: Sequelize.FindOrInitializeOptions = { - where: author, - defaults: author, + return undefined +} + +toAddRemoteJSON = function (this: AuthorInstance) { + const json = { + uuid: this.uuid, + name: this.name + } + + return json +} + +isOwned = function (this: AuthorInstance) { + return this.podId === null +} + +// ------------------------------ STATICS ------------------------------ + +listOwned = function () { + const query: Sequelize.FindOptions = { + where: { + podId: null + } + } + + return Author.findAll(query) +} + +load = function (id: number) { + return Author.findById(id) +} + +loadByUUID = function (uuid: string) { + const query: Sequelize.FindOptions = { + where: { + uuid + } + } + + return Author.findOne(query) +} + +loadAuthorByPodAndUUID = function (uuid: string, podId: number, transaction: Sequelize.Transaction) { + const query: Sequelize.FindOptions = { + where: { + podId, + uuid + }, transaction } - return Author.findOrCreate(query).then(([ authorInstance ]) => authorInstance) + return Author.find(query) } diff --git a/server/models/video/index.ts b/server/models/video/index.ts index 08b360376..dba6a5590 100644 --- a/server/models/video/index.ts +++ b/server/models/video/index.ts @@ -2,6 +2,7 @@ export * from './author-interface' export * from './tag-interface' export * from './video-abuse-interface' export * from './video-blacklist-interface' +export * from './video-channel-interface' export * from './video-tag-interface' export * from './video-file-interface' export * from './video-interface' diff --git a/server/models/video/video-channel-interface.ts b/server/models/video/video-channel-interface.ts new file mode 100644 index 000000000..b8d3e0f42 --- /dev/null +++ b/server/models/video/video-channel-interface.ts @@ -0,0 +1,64 @@ +import * as Sequelize from 'sequelize' +import * as Promise from 'bluebird' + +import { ResultList, RemoteVideoChannelCreateData, RemoteVideoChannelUpdateData } from '../../../shared' + +// Don't use barrel, import just what we need +import { VideoChannel as FormattedVideoChannel } from '../../../shared/models/videos/video-channel.model' +import { AuthorInstance } from './author-interface' +import { VideoInstance } from './video-interface' + +export namespace VideoChannelMethods { + export type ToFormattedJSON = (this: VideoChannelInstance) => FormattedVideoChannel + export type ToAddRemoteJSON = (this: VideoChannelInstance) => RemoteVideoChannelCreateData + export type ToUpdateRemoteJSON = (this: VideoChannelInstance) => RemoteVideoChannelUpdateData + export type IsOwned = (this: VideoChannelInstance) => boolean + + export type CountByAuthor = (authorId: number) => Promise + export type ListOwned = () => Promise + export type ListForApi = (start: number, count: number, sort: string) => Promise< ResultList > + export type LoadByIdAndAuthor = (id: number, authorId: number) => Promise + export type ListByAuthor = (authorId: number) => Promise< ResultList > + export type LoadAndPopulateAuthor = (id: number) => Promise + export type LoadByUUIDAndPopulateAuthor = (uuid: string) => Promise + export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise + export type LoadByHostAndUUID = (uuid: string, podHost: string, t?: Sequelize.Transaction) => Promise + export type LoadAndPopulateAuthorAndVideos = (id: number) => Promise +} + +export interface VideoChannelClass { + countByAuthor: VideoChannelMethods.CountByAuthor + listForApi: VideoChannelMethods.ListForApi + listByAuthor: VideoChannelMethods.ListByAuthor + listOwned: VideoChannelMethods.ListOwned + loadByIdAndAuthor: VideoChannelMethods.LoadByIdAndAuthor + loadByUUID: VideoChannelMethods.LoadByUUID + loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID + loadAndPopulateAuthor: VideoChannelMethods.LoadAndPopulateAuthor + loadByUUIDAndPopulateAuthor: VideoChannelMethods.LoadByUUIDAndPopulateAuthor + loadAndPopulateAuthorAndVideos: VideoChannelMethods.LoadAndPopulateAuthorAndVideos +} + +export interface VideoChannelAttributes { + id?: number + uuid?: string + name: string + description: string + remote: boolean + + Author?: AuthorInstance + Videos?: VideoInstance[] +} + +export interface VideoChannelInstance extends VideoChannelClass, VideoChannelAttributes, Sequelize.Instance { + id: number + createdAt: Date + updatedAt: Date + + isOwned: VideoChannelMethods.IsOwned + toFormattedJSON: VideoChannelMethods.ToFormattedJSON + toAddRemoteJSON: VideoChannelMethods.ToAddRemoteJSON + toUpdateRemoteJSON: VideoChannelMethods.ToUpdateRemoteJSON +} + +export interface VideoChannelModel extends VideoChannelClass, Sequelize.Model {} diff --git a/server/models/video/video-channel.ts b/server/models/video/video-channel.ts new file mode 100644 index 000000000..e469383e9 --- /dev/null +++ b/server/models/video/video-channel.ts @@ -0,0 +1,349 @@ +import * as Sequelize from 'sequelize' + +import { isVideoChannelNameValid, isVideoChannelDescriptionValid } from '../../helpers' +import { removeVideoChannelToFriends } from '../../lib' + +import { addMethodsToModel, getSort } from '../utils' +import { + VideoChannelInstance, + VideoChannelAttributes, + + VideoChannelMethods +} from './video-channel-interface' + +let VideoChannel: Sequelize.Model +let toFormattedJSON: VideoChannelMethods.ToFormattedJSON +let toAddRemoteJSON: VideoChannelMethods.ToAddRemoteJSON +let toUpdateRemoteJSON: VideoChannelMethods.ToUpdateRemoteJSON +let isOwned: VideoChannelMethods.IsOwned +let countByAuthor: VideoChannelMethods.CountByAuthor +let listOwned: VideoChannelMethods.ListOwned +let listForApi: VideoChannelMethods.ListForApi +let listByAuthor: VideoChannelMethods.ListByAuthor +let loadByIdAndAuthor: VideoChannelMethods.LoadByIdAndAuthor +let loadByUUID: VideoChannelMethods.LoadByUUID +let loadAndPopulateAuthor: VideoChannelMethods.LoadAndPopulateAuthor +let loadByUUIDAndPopulateAuthor: VideoChannelMethods.LoadByUUIDAndPopulateAuthor +let loadByHostAndUUID: VideoChannelMethods.LoadByHostAndUUID +let loadAndPopulateAuthorAndVideos: VideoChannelMethods.LoadAndPopulateAuthorAndVideos + +export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.DataTypes) { + VideoChannel = sequelize.define('VideoChannel', + { + uuid: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + allowNull: false, + validate: { + isUUID: 4 + } + }, + name: { + type: DataTypes.STRING, + allowNull: false, + validate: { + nameValid: value => { + const res = isVideoChannelNameValid(value) + if (res === false) throw new Error('Video channel name is not valid.') + } + } + }, + description: { + type: DataTypes.STRING, + allowNull: true, + validate: { + descriptionValid: value => { + const res = isVideoChannelDescriptionValid(value) + if (res === false) throw new Error('Video channel description is not valid.') + } + } + }, + remote: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false + } + }, + { + indexes: [ + { + fields: [ 'authorId' ] + } + ], + hooks: { + afterDestroy + } + } + ) + + const classMethods = [ + associate, + + listForApi, + listByAuthor, + listOwned, + loadByIdAndAuthor, + loadAndPopulateAuthor, + loadByUUIDAndPopulateAuthor, + loadByUUID, + loadByHostAndUUID, + loadAndPopulateAuthorAndVideos, + countByAuthor + ] + const instanceMethods = [ + isOwned, + toFormattedJSON, + toAddRemoteJSON, + toUpdateRemoteJSON + ] + addMethodsToModel(VideoChannel, classMethods, instanceMethods) + + return VideoChannel +} + +// ------------------------------ METHODS ------------------------------ + +isOwned = function (this: VideoChannelInstance) { + return this.remote === false +} + +toFormattedJSON = function (this: VideoChannelInstance) { + const json = { + id: this.id, + uuid: this.uuid, + name: this.name, + description: this.description, + isLocal: this.isOwned(), + createdAt: this.createdAt, + updatedAt: this.updatedAt + } + + if (this.Author !== undefined) { + json['owner'] = { + name: this.Author.name, + uuid: this.Author.uuid + } + } + + if (Array.isArray(this.Videos)) { + json['videos'] = this.Videos.map(v => v.toFormattedJSON()) + } + + return json +} + +toAddRemoteJSON = function (this: VideoChannelInstance) { + const json = { + uuid: this.uuid, + name: this.name, + description: this.description, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + ownerUUID: this.Author.uuid + } + + return json +} + +toUpdateRemoteJSON = function (this: VideoChannelInstance) { + const json = { + uuid: this.uuid, + name: this.name, + description: this.description, + createdAt: this.createdAt, + updatedAt: this.updatedAt, + ownerUUID: this.Author.uuid + } + + return json +} + +// ------------------------------ STATICS ------------------------------ + +function associate (models) { + VideoChannel.belongsTo(models.Author, { + foreignKey: { + name: 'authorId', + allowNull: false + }, + onDelete: 'CASCADE' + }) + + VideoChannel.hasMany(models.Video, { + foreignKey: { + name: 'channelId', + allowNull: false + }, + onDelete: 'CASCADE' + }) +} + +function afterDestroy (videoChannel: VideoChannelInstance, options: { transaction: Sequelize.Transaction }) { + if (videoChannel.isOwned()) { + const removeVideoChannelToFriendsParams = { + uuid: videoChannel.uuid + } + + return removeVideoChannelToFriends(removeVideoChannelToFriendsParams, options.transaction) + } + + return undefined +} + +countByAuthor = function (authorId: number) { + const query = { + where: { + authorId + } + } + + return VideoChannel.count(query) +} + +listOwned = function () { + const query = { + where: { + remote: false + }, + include: [ VideoChannel['sequelize'].models.Author ] + } + + return VideoChannel.findAll(query) +} + +listForApi = function (start: number, count: number, sort: string) { + const query = { + offset: start, + limit: count, + order: [ getSort(sort) ], + include: [ + { + model: VideoChannel['sequelize'].models.Author, + required: true, + include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] + } + ] + } + + return VideoChannel.findAndCountAll(query).then(({ rows, count }) => { + return { total: count, data: rows } + }) +} + +listByAuthor = function (authorId: number) { + const query = { + order: [ getSort('createdAt') ], + include: [ + { + model: VideoChannel['sequelize'].models.Author, + where: { + id: authorId + }, + required: true, + include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] + } + ] + } + + return VideoChannel.findAndCountAll(query).then(({ rows, count }) => { + return { total: count, data: rows } + }) +} + +loadByUUID = function (uuid: string, t?: Sequelize.Transaction) { + const query: Sequelize.FindOptions = { + where: { + uuid + } + } + + if (t !== undefined) query.transaction = t + + return VideoChannel.findOne(query) +} + +loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) { + const query: Sequelize.FindOptions = { + where: { + uuid + }, + include: [ + { + model: VideoChannel['sequelize'].models.Author, + include: [ + { + model: VideoChannel['sequelize'].models.Pod, + required: true, + where: { + host: fromHost + } + } + ] + } + ] + } + + if (t !== undefined) query.transaction = t + + return VideoChannel.findOne(query) +} + +loadByIdAndAuthor = function (id: number, authorId: number) { + const options = { + where: { + id, + authorId + }, + include: [ + { + model: VideoChannel['sequelize'].models.Author, + include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] + } + ] + } + + return VideoChannel.findOne(options) +} + +loadAndPopulateAuthor = function (id: number) { + const options = { + include: [ + { + model: VideoChannel['sequelize'].models.Author, + include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] + } + ] + } + + return VideoChannel.findById(id, options) +} + +loadByUUIDAndPopulateAuthor = function (uuid: string) { + const options = { + where: { + uuid + }, + include: [ + { + model: VideoChannel['sequelize'].models.Author, + include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] + } + ] + } + + return VideoChannel.findOne(options) +} + +loadAndPopulateAuthorAndVideos = function (id: number) { + const options = { + include: [ + { + model: VideoChannel['sequelize'].models.Author, + include: [ { model: VideoChannel['sequelize'].models.Pod, required: false } ] + }, + VideoChannel['sequelize'].models.Video + ] + } + + return VideoChannel.findById(id, options) +} diff --git a/server/models/video/video-interface.ts b/server/models/video/video-interface.ts index 86ce84dd9..4b5ae08c2 100644 --- a/server/models/video/video-interface.ts +++ b/server/models/video/video-interface.ts @@ -6,16 +6,21 @@ import { TagAttributes, TagInstance } from './tag-interface' import { VideoFileAttributes, VideoFileInstance } from './video-file-interface' // Don't use barrel, import just what we need -import { Video as FormattedVideo } from '../../../shared/models/videos/video.model' +import { + Video as FormattedVideo, + VideoDetails as FormattedDetailsVideo +} from '../../../shared/models/videos/video.model' import { RemoteVideoUpdateData } from '../../../shared/models/pods/remote-video/remote-video-update-request.model' import { RemoteVideoCreateData } from '../../../shared/models/pods/remote-video/remote-video-create-request.model' import { ResultList } from '../../../shared/models/result-list.model' +import { VideoChannelInstance } from './video-channel-interface' export namespace VideoMethods { export type GetThumbnailName = (this: VideoInstance) => string export type GetPreviewName = (this: VideoInstance) => string export type IsOwned = (this: VideoInstance) => boolean export type ToFormattedJSON = (this: VideoInstance) => FormattedVideo + export type ToFormattedDetailsJSON = (this: VideoInstance) => FormattedDetailsVideo export type GetOriginalFile = (this: VideoInstance) => VideoFileInstance export type GetTorrentFileName = (this: VideoInstance, videoFile: VideoFileInstance) => string @@ -52,8 +57,8 @@ export namespace VideoMethods { ) => Promise< ResultList > export type Load = (id: number) => Promise - export type LoadByUUID = (uuid: string) => Promise - export type LoadByHostAndUUID = (fromHost: string, uuid: string) => Promise + export type LoadByUUID = (uuid: string, t?: Sequelize.Transaction) => Promise + export type LoadByHostAndUUID = (fromHost: string, uuid: string, t?: Sequelize.Transaction) => Promise export type LoadAndPopulateAuthor = (id: number) => Promise export type LoadAndPopulateAuthorAndPodAndTags = (id: number) => Promise export type LoadByUUIDAndPopulateAuthorAndPodAndTags = (uuid: string) => Promise @@ -94,7 +99,9 @@ export interface VideoAttributes { dislikes?: number remote: boolean - Author?: AuthorInstance + channelId?: number + + VideoChannel?: VideoChannelInstance Tags?: TagInstance[] VideoFiles?: VideoFileInstance[] } @@ -121,6 +128,7 @@ export interface VideoInstance extends VideoClass, VideoAttributes, Sequelize.In removeTorrent: VideoMethods.RemoveTorrent toAddRemoteJSON: VideoMethods.ToAddRemoteJSON toFormattedJSON: VideoMethods.ToFormattedJSON + toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile transcodeOriginalVideofile: VideoMethods.TranscodeOriginalVideofile diff --git a/server/models/video/video.ts b/server/models/video/video.ts index 0b1af4d21..d9b976404 100644 --- a/server/models/video/video.ts +++ b/server/models/video/video.ts @@ -60,6 +60,7 @@ let getPreviewPath: VideoMethods.GetPreviewPath let getTorrentFileName: VideoMethods.GetTorrentFileName let isOwned: VideoMethods.IsOwned let toFormattedJSON: VideoMethods.ToFormattedJSON +let toFormattedDetailsJSON: VideoMethods.ToFormattedDetailsJSON let toAddRemoteJSON: VideoMethods.ToAddRemoteJSON let toUpdateRemoteJSON: VideoMethods.ToUpdateRemoteJSON let optimizeOriginalVideofile: VideoMethods.OptimizeOriginalVideofile @@ -205,9 +206,6 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da }, { indexes: [ - { - fields: [ 'authorId' ] - }, { fields: [ 'name' ] }, @@ -225,6 +223,9 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da }, { fields: [ 'uuid' ] + }, + { + fields: [ 'channelId' ] } ], hooks: { @@ -268,6 +269,7 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da removeTorrent, toAddRemoteJSON, toFormattedJSON, + toFormattedDetailsJSON, toUpdateRemoteJSON, optimizeOriginalVideofile, transcodeOriginalVideofile, @@ -282,9 +284,9 @@ export default function (sequelize: Sequelize.Sequelize, DataTypes: Sequelize.Da // ------------------------------ METHODS ------------------------------ function associate (models) { - Video.belongsTo(models.Author, { + Video.belongsTo(models.VideoChannel, { foreignKey: { - name: 'authorId', + name: 'channelId', allowNull: false }, onDelete: 'cascade' @@ -439,8 +441,8 @@ getPreviewPath = function (this: VideoInstance) { toFormattedJSON = function (this: VideoInstance) { let podHost - if (this.Author.Pod) { - podHost = this.Author.Pod.host + if (this.VideoChannel.Author.Pod) { + podHost = this.VideoChannel.Author.Pod.host } else { // It means it's our video podHost = CONFIG.WEBSERVER.HOST @@ -472,7 +474,59 @@ toFormattedJSON = function (this: VideoInstance) { description: this.description, podHost, isLocal: this.isOwned(), - author: this.Author.name, + author: this.VideoChannel.Author.name, + duration: this.duration, + views: this.views, + likes: this.likes, + dislikes: this.dislikes, + tags: map(this.Tags, 'name'), + thumbnailPath: this.getThumbnailPath(), + previewPath: this.getPreviewPath(), + embedPath: this.getEmbedPath(), + createdAt: this.createdAt, + updatedAt: this.updatedAt + } + + return json +} + +toFormattedDetailsJSON = function (this: VideoInstance) { + let podHost + + if (this.VideoChannel.Author.Pod) { + podHost = this.VideoChannel.Author.Pod.host + } else { + // It means it's our video + podHost = CONFIG.WEBSERVER.HOST + } + + // Maybe our pod is not up to date and there are new categories since our version + let categoryLabel = VIDEO_CATEGORIES[this.category] + if (!categoryLabel) categoryLabel = 'Misc' + + // Maybe our pod is not up to date and there are new licences since our version + let licenceLabel = VIDEO_LICENCES[this.licence] + if (!licenceLabel) licenceLabel = 'Unknown' + + // Language is an optional attribute + let languageLabel = VIDEO_LANGUAGES[this.language] + if (!languageLabel) languageLabel = 'Unknown' + + const json = { + id: this.id, + uuid: this.uuid, + name: this.name, + category: this.category, + categoryLabel, + licence: this.licence, + licenceLabel, + language: this.language, + languageLabel, + nsfw: this.nsfw, + description: this.description, + podHost, + isLocal: this.isOwned(), + author: this.VideoChannel.Author.name, duration: this.duration, views: this.views, likes: this.likes, @@ -483,6 +537,7 @@ toFormattedJSON = function (this: VideoInstance) { embedPath: this.getEmbedPath(), createdAt: this.createdAt, updatedAt: this.updatedAt, + channel: this.VideoChannel.toFormattedJSON(), files: [] } @@ -525,7 +580,7 @@ toAddRemoteJSON = function (this: VideoInstance) { language: this.language, nsfw: this.nsfw, description: this.description, - author: this.Author.name, + channelUUID: this.VideoChannel.uuid, duration: this.duration, thumbnailData: thumbnailData.toString('binary'), tags: map(this.Tags, 'name'), @@ -559,7 +614,6 @@ toUpdateRemoteJSON = function (this: VideoInstance) { language: this.language, nsfw: this.nsfw, description: this.description, - author: this.Author.name, duration: this.duration, tags: map(this.Tags, 'name'), createdAt: this.createdAt, @@ -723,8 +777,18 @@ listForApi = function (start: number, count: number, sort: string) { order: [ getSort(sort), [ Video['sequelize'].models.Tag, 'name', 'ASC' ] ], include: [ { - model: Video['sequelize'].models.Author, - include: [ { model: Video['sequelize'].models.Pod, required: false } ] + model: Video['sequelize'].models.VideoChannel, + include: [ + { + model: Video['sequelize'].models.Author, + include: [ + { + model: Video['sequelize'].models.Pod, + required: false + } + ] + } + ] }, Video['sequelize'].models.Tag, Video['sequelize'].models.VideoFile @@ -740,8 +804,8 @@ listForApi = function (start: number, count: number, sort: string) { }) } -loadByHostAndUUID = function (fromHost: string, uuid: string) { - const query = { +loadByHostAndUUID = function (fromHost: string, uuid: string, t?: Sequelize.Transaction) { + const query: Sequelize.FindOptions = { where: { uuid }, @@ -750,20 +814,27 @@ loadByHostAndUUID = function (fromHost: string, uuid: string) { model: Video['sequelize'].models.VideoFile }, { - model: Video['sequelize'].models.Author, + model: Video['sequelize'].models.VideoChannel, include: [ { - model: Video['sequelize'].models.Pod, - required: true, - where: { - host: fromHost - } + model: Video['sequelize'].models.Author, + include: [ + { + model: Video['sequelize'].models.Pod, + required: true, + where: { + host: fromHost + } + } + ] } ] } ] } + if (t !== undefined) query.transaction = t + return Video.findOne(query) } @@ -774,7 +845,10 @@ listOwnedAndPopulateAuthorAndTags = function () { }, include: [ Video['sequelize'].models.VideoFile, - Video['sequelize'].models.Author, + { + model: Video['sequelize'].models.VideoChannel, + include: [ Video['sequelize'].models.Author ] + }, Video['sequelize'].models.Tag ] } @@ -792,10 +866,15 @@ listOwnedByAuthor = function (author: string) { model: Video['sequelize'].models.VideoFile }, { - model: Video['sequelize'].models.Author, - where: { - name: author - } + model: Video['sequelize'].models.VideoChannel, + include: [ + { + model: Video['sequelize'].models.Author, + where: { + name: author + } + } + ] } ] } @@ -807,19 +886,28 @@ load = function (id: number) { return Video.findById(id) } -loadByUUID = function (uuid: string) { - const query = { +loadByUUID = function (uuid: string, t?: Sequelize.Transaction) { + const query: Sequelize.FindOptions = { where: { uuid }, include: [ Video['sequelize'].models.VideoFile ] } + + if (t !== undefined) query.transaction = t + return Video.findOne(query) } loadAndPopulateAuthor = function (id: number) { const options = { - include: [ Video['sequelize'].models.VideoFile, Video['sequelize'].models.Author ] + include: [ + Video['sequelize'].models.VideoFile, + { + model: Video['sequelize'].models.VideoChannel, + include: [ Video['sequelize'].models.Author ] + } + ] } return Video.findById(id, options) @@ -829,8 +917,13 @@ loadAndPopulateAuthorAndPodAndTags = function (id: number) { const options = { include: [ { - model: Video['sequelize'].models.Author, - include: [ { model: Video['sequelize'].models.Pod, required: false } ] + model: Video['sequelize'].models.VideoChannel, + include: [ + { + model: Video['sequelize'].models.Author, + include: [ { model: Video['sequelize'].models.Pod, required: false } ] + } + ] }, Video['sequelize'].models.Tag, Video['sequelize'].models.VideoFile @@ -847,8 +940,13 @@ loadByUUIDAndPopulateAuthorAndPodAndTags = function (uuid: string) { }, include: [ { - model: Video['sequelize'].models.Author, - include: [ { model: Video['sequelize'].models.Pod, required: false } ] + model: Video['sequelize'].models.VideoChannel, + include: [ + { + model: Video['sequelize'].models.Author, + include: [ { model: Video['sequelize'].models.Pod, required: false } ] + } + ] }, Video['sequelize'].models.Tag, Video['sequelize'].models.VideoFile @@ -866,9 +964,13 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s const authorInclude: Sequelize.IncludeOptions = { model: Video['sequelize'].models.Author, - include: [ - podInclude - ] + include: [ podInclude ] + } + + const videoChannelInclude: Sequelize.IncludeOptions = { + model: Video['sequelize'].models.VideoChannel, + include: [ authorInclude ], + required: true } const tagInclude: Sequelize.IncludeOptions = { @@ -915,8 +1017,6 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s $iLike: '%' + value + '%' } } - - // authorInclude.or = true } else { query.where[field] = { $iLike: '%' + value + '%' @@ -924,7 +1024,7 @@ searchAndPopulateAuthorAndPodAndTags = function (value: string, field: string, s } query.include = [ - authorInclude, tagInclude, videoFileInclude + videoChannelInclude, tagInclude, videoFileInclude ] return Video.findAndCountAll(query).then(({ rows, count }) => { @@ -955,8 +1055,8 @@ function getBaseUrls (video: VideoInstance) { baseUrlHttp = CONFIG.WEBSERVER.URL baseUrlWs = CONFIG.WEBSERVER.WS + '://' + CONFIG.WEBSERVER.HOSTNAME + ':' + CONFIG.WEBSERVER.PORT } else { - baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.Author.Pod.host - baseUrlWs = REMOTE_SCHEME.WS + '://' + video.Author.Pod.host + baseUrlHttp = REMOTE_SCHEME.HTTP + '://' + video.VideoChannel.Author.Pod.host + baseUrlWs = REMOTE_SCHEME.WS + '://' + video.VideoChannel.Author.Pod.host } return { baseUrlHttp, baseUrlWs } diff --git a/shared/models/pods/remote-video/index.ts b/shared/models/pods/remote-video/index.ts index c88116849..918b9e1f6 100644 --- a/shared/models/pods/remote-video/index.ts +++ b/shared/models/pods/remote-video/index.ts @@ -1,7 +1,12 @@ export * from './remote-qadu-video-request.model' +export * from './remote-video-author-create-request.model' +export * from './remote-video-author-remove-request.model' export * from './remote-video-event-request.model' export * from './remote-video-request.model' export * from './remote-video-create-request.model' export * from './remote-video-update-request.model' export * from './remote-video-remove-request.model' +export * from './remote-video-channel-create-request.model' +export * from './remote-video-channel-update-request.model' +export * from './remote-video-channel-remove-request.model' export * from './remote-video-report-abuse-request.model' diff --git a/shared/models/pods/remote-video/remote-video-author-create-request.model.ts b/shared/models/pods/remote-video/remote-video-author-create-request.model.ts new file mode 100644 index 000000000..ae364d177 --- /dev/null +++ b/shared/models/pods/remote-video/remote-video-author-create-request.model.ts @@ -0,0 +1,11 @@ +import { RemoteVideoRequest } from './remote-video-request.model' + +export interface RemoteVideoAuthorCreateData { + uuid: string + name: string +} + +export interface RemoteVideoAuthorCreateRequest extends RemoteVideoRequest { + type: 'add-author' + data: RemoteVideoAuthorCreateData +} diff --git a/shared/models/pods/remote-video/remote-video-author-remove-request.model.ts b/shared/models/pods/remote-video/remote-video-author-remove-request.model.ts new file mode 100644 index 000000000..8738e92f0 --- /dev/null +++ b/shared/models/pods/remote-video/remote-video-author-remove-request.model.ts @@ -0,0 +1,10 @@ +import { RemoteVideoRequest } from './remote-video-request.model' + +export interface RemoteVideoAuthorRemoveData { + uuid: string +} + +export interface RemoteVideoAuthorRemoveRequest extends RemoteVideoRequest { + type: 'remove-author' + data: RemoteVideoAuthorRemoveData +} diff --git a/shared/models/pods/remote-video/remote-video-channel-create-request.model.ts b/shared/models/pods/remote-video/remote-video-channel-create-request.model.ts new file mode 100644 index 000000000..54163a2db --- /dev/null +++ b/shared/models/pods/remote-video/remote-video-channel-create-request.model.ts @@ -0,0 +1,15 @@ +import { RemoteVideoRequest } from './remote-video-request.model' + +export interface RemoteVideoChannelCreateData { + uuid: string + name: string + description: string + createdAt: Date + updatedAt: Date + ownerUUID: string +} + +export interface RemoteVideoChannelCreateRequest extends RemoteVideoRequest { + type: 'add-channel' + data: RemoteVideoChannelCreateData +} diff --git a/shared/models/pods/remote-video/remote-video-channel-remove-request.model.ts b/shared/models/pods/remote-video/remote-video-channel-remove-request.model.ts new file mode 100644 index 000000000..7aa3fe112 --- /dev/null +++ b/shared/models/pods/remote-video/remote-video-channel-remove-request.model.ts @@ -0,0 +1,10 @@ +import { RemoteVideoRequest } from './remote-video-request.model' + +export interface RemoteVideoChannelRemoveData { + uuid: string +} + +export interface RemoteVideoChannelRemoveRequest extends RemoteVideoRequest { + type: 'remove-channel' + data: RemoteVideoChannelRemoveData +} diff --git a/shared/models/pods/remote-video/remote-video-channel-update-request.model.ts b/shared/models/pods/remote-video/remote-video-channel-update-request.model.ts new file mode 100644 index 000000000..70250d109 --- /dev/null +++ b/shared/models/pods/remote-video/remote-video-channel-update-request.model.ts @@ -0,0 +1,15 @@ +import { RemoteVideoRequest } from './remote-video-request.model' + +export interface RemoteVideoChannelUpdateData { + uuid: string + name: string + description: string + createdAt: Date + updatedAt: Date + ownerUUID: string +} + +export interface RemoteVideoChannelUpdateRequest extends RemoteVideoRequest { + type: 'update-channel' + data: RemoteVideoChannelUpdateData +} diff --git a/shared/models/pods/remote-video/remote-video-create-request.model.ts b/shared/models/pods/remote-video/remote-video-create-request.model.ts index 98425e4d9..e00e81214 100644 --- a/shared/models/pods/remote-video/remote-video-create-request.model.ts +++ b/shared/models/pods/remote-video/remote-video-create-request.model.ts @@ -2,7 +2,7 @@ import { RemoteVideoRequest } from './remote-video-request.model' export interface RemoteVideoCreateData { uuid: string - author: string + channelUUID: string tags: string[] name: string category: number @@ -26,6 +26,6 @@ export interface RemoteVideoCreateData { } export interface RemoteVideoCreateRequest extends RemoteVideoRequest { - type: 'add' + type: 'add-video' data: RemoteVideoCreateData } diff --git a/shared/models/pods/remote-video/remote-video-remove-request.model.ts b/shared/models/pods/remote-video/remote-video-remove-request.model.ts index 0686dc8ab..79a5e0a5f 100644 --- a/shared/models/pods/remote-video/remote-video-remove-request.model.ts +++ b/shared/models/pods/remote-video/remote-video-remove-request.model.ts @@ -5,6 +5,6 @@ export interface RemoteVideoRemoveData { } export interface RemoteVideoRemoveRequest extends RemoteVideoRequest { - type: 'remove' + type: 'remove-video' data: RemoteVideoRemoveData } diff --git a/shared/models/pods/remote-video/remote-video-request.model.ts b/shared/models/pods/remote-video/remote-video-request.model.ts index e5052a23d..56f8136c2 100644 --- a/shared/models/pods/remote-video/remote-video-request.model.ts +++ b/shared/models/pods/remote-video/remote-video-request.model.ts @@ -1,4 +1,9 @@ export interface RemoteVideoRequest { - type: 'add' | 'update' | 'remove' | 'report-abuse' + type: RemoteVideoRequestType data: any } + +export type RemoteVideoRequestType = 'add-video' | 'update-video' | 'remove-video' | + 'add-channel' | 'update-channel' | 'remove-channel' | + 'report-abuse' | + 'add-author' | 'remove-author' diff --git a/shared/models/pods/remote-video/remote-video-update-request.model.ts b/shared/models/pods/remote-video/remote-video-update-request.model.ts index 7f34a30ae..90c42fc28 100644 --- a/shared/models/pods/remote-video/remote-video-update-request.model.ts +++ b/shared/models/pods/remote-video/remote-video-update-request.model.ts @@ -1,3 +1,5 @@ +import { RemoteVideoRequest } from './remote-video-request.model' + export interface RemoteVideoUpdateData { uuid: string tags: string[] @@ -21,7 +23,7 @@ export interface RemoteVideoUpdateData { }[] } -export interface RemoteVideoUpdateRequest { - type: 'update' +export interface RemoteVideoUpdateRequest extends RemoteVideoRequest { + type: 'update-video' data: RemoteVideoUpdateData } diff --git a/shared/models/users/user.model.ts b/shared/models/users/user.model.ts index 867a6dde5..175e72f28 100644 --- a/shared/models/users/user.model.ts +++ b/shared/models/users/user.model.ts @@ -1,4 +1,5 @@ import { UserRole } from './user-role.type' +import { VideoChannel } from '../videos/video-channel.model' export interface User { id: number @@ -7,5 +8,10 @@ export interface User { displayNSFW: boolean role: UserRole videoQuota: number - createdAt: Date + createdAt: Date, + author: { + id: number + uuid: string + } + videoChannels?: VideoChannel[] } diff --git a/shared/models/videos/index.ts b/shared/models/videos/index.ts index 35144dbad..2a3912f06 100644 --- a/shared/models/videos/index.ts +++ b/shared/models/videos/index.ts @@ -4,6 +4,9 @@ export * from './user-video-rate.type' export * from './video-abuse-create.model' export * from './video-abuse.model' export * from './video-blacklist.model' +export * from './video-channel-create.model' +export * from './video-channel-update.model' +export * from './video-channel.model' export * from './video-create.model' export * from './video-rate.type' export * from './video-resolution.enum' diff --git a/shared/models/videos/video-channel-create.model.ts b/shared/models/videos/video-channel-create.model.ts new file mode 100644 index 000000000..f309c8f45 --- /dev/null +++ b/shared/models/videos/video-channel-create.model.ts @@ -0,0 +1,4 @@ +export interface VideoChannelCreate { + name: string + description?: string +} diff --git a/shared/models/videos/video-channel-update.model.ts b/shared/models/videos/video-channel-update.model.ts new file mode 100644 index 000000000..4e98e39a8 --- /dev/null +++ b/shared/models/videos/video-channel-update.model.ts @@ -0,0 +1,4 @@ +export interface VideoChannelUpdate { + name: string + description: string +} diff --git a/shared/models/videos/video-channel.model.ts b/shared/models/videos/video-channel.model.ts new file mode 100644 index 000000000..ee56c54b6 --- /dev/null +++ b/shared/models/videos/video-channel.model.ts @@ -0,0 +1,15 @@ +import { Video } from './video.model' + +export interface VideoChannel { + id: number + name: string + description: string + isLocal: boolean + createdAt: Date | string + updatedAt: Date | string + owner?: { + name: string + uuid: string + } + videos?: Video[] +} diff --git a/shared/models/videos/video-create.model.ts b/shared/models/videos/video-create.model.ts index 5c0b498ce..4d0e83520 100644 --- a/shared/models/videos/video-create.model.ts +++ b/shared/models/videos/video-create.model.ts @@ -3,6 +3,7 @@ export interface VideoCreate { licence: number language: number description: string + channelId: number nsfw: boolean name: string tags: string[] diff --git a/shared/models/videos/video.model.ts b/shared/models/videos/video.model.ts index 8e47ac069..32463933d 100644 --- a/shared/models/videos/video.model.ts +++ b/shared/models/videos/video.model.ts @@ -1,3 +1,5 @@ +import { VideoChannel } from './video-channel.model' + export interface VideoFile { magnetUri: string resolution: number @@ -32,5 +34,9 @@ export interface Video { likes: number dislikes: number nsfw: boolean +} + +export interface VideoDetails extends Video { + channel: VideoChannel files: VideoFile[] }